agent-workflow-kit-cli 1.0.0-mvp
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +303 -0
- package/dist/cli/commands/doctor.js +96 -0
- package/dist/cli/commands/export.js +118 -0
- package/dist/cli/commands/init.js +163 -0
- package/dist/cli/commands/sync.js +12 -0
- package/dist/cli/index.js +72 -0
- package/dist/core/detector.js +123 -0
- package/dist/core/emitter.js +119 -0
- package/dist/core/renderer.js +70 -0
- package/dist/index.js +7 -0
- package/dist/utils/clipboard.js +36 -0
- package/package.json +35 -0
- package/templates/common/AGENTS.md.hbs +44 -0
- package/templates/fastapi/AGENTS.md.hbs +28 -0
- package/templates/fastapi/rules/python-style.md +22 -0
- package/templates/fastapi/skills/fastapi-feature/SKILL.md +18 -0
- package/templates/react-ts/AGENTS.md.hbs +47 -0
- package/templates/react-ts/rules/react-style.md +34 -0
- package/templates/react-ts/skills/react-feature/SKILL.md +25 -0
- package/templates/spring-boot/AGENTS.md.hbs +79 -0
- package/templates/spring-boot/rules/java-style.md +14 -0
- package/templates/spring-boot/skills/spring-feature/SKILL.md +26 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import { detectProjectModules } from "../../core/detector.js";
|
|
10
|
+
import { renderTemplate, readStaticTemplateFile, getStackRules, getStackSkills, } from "../../core/renderer.js";
|
|
11
|
+
import { updateFileWithBlock, writeRuleWithChunking, } from "../../core/emitter.js";
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
function printSuccessAndNextSteps(options) {
|
|
15
|
+
console.log(chalk.bold.green("\n๐ Initialization completed successfully!"));
|
|
16
|
+
if (!options.dryRun) {
|
|
17
|
+
console.log(chalk.bold.cyan("\n๐ Next Steps:"));
|
|
18
|
+
console.log(chalk.dim("------------------------------------------"));
|
|
19
|
+
console.log(chalk.white(`1. Open & review the generated guidelines:`));
|
|
20
|
+
console.log(chalk.gray(` - Root: ${chalk.underline("AGENTS.md")}`));
|
|
21
|
+
console.log(chalk.gray(` - Stack rules: ${chalk.underline(".agents/rules/")}`));
|
|
22
|
+
console.log(chalk.white(`2. Setup automatic git pre-commit hook validation:`));
|
|
23
|
+
console.log(chalk.cyan(` run: npx agent-workflow-kit-cli doctor --install-hook`));
|
|
24
|
+
console.log(chalk.white(`3. Export custom skills to register with your AI agent (e.g. Antigravity):`));
|
|
25
|
+
console.log(chalk.cyan(` run: npx agent-workflow-kit-cli export antigravity`));
|
|
26
|
+
console.log(chalk.dim("------------------------------------------\n"));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function runInit(options) {
|
|
30
|
+
const cwd = process.cwd();
|
|
31
|
+
console.log(chalk.bold.cyan("\n๐ Agent Workflow Kit - Initializing..."));
|
|
32
|
+
console.log(chalk.dim("------------------------------------------"));
|
|
33
|
+
console.log(`${chalk.bold("Stack Selection:")} ${chalk.green(options.stack)}`);
|
|
34
|
+
console.log(`${chalk.bold("Agent Profile:")} ${chalk.green(options.agent)}`);
|
|
35
|
+
console.log(`${chalk.bold("Dry Run:")} ${options.dryRun ? chalk.yellow("Enabled ๐งช") : chalk.gray("Disabled")}`);
|
|
36
|
+
console.log(chalk.dim("------------------------------------------\n"));
|
|
37
|
+
let modules;
|
|
38
|
+
if (options.stack !== "auto") {
|
|
39
|
+
modules = [{ dir: cwd, name: ".", stacks: [options.stack] }];
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
modules = await detectProjectModules(cwd);
|
|
43
|
+
}
|
|
44
|
+
if (modules.length === 0) {
|
|
45
|
+
console.log(chalk.yellow("No standard stacks detected automatically. Creating general agent guidelines at root."));
|
|
46
|
+
const finalAgentsContent = await renderTemplate("common/AGENTS.md.hbs", {
|
|
47
|
+
stackContent: "",
|
|
48
|
+
});
|
|
49
|
+
if (options.dryRun) {
|
|
50
|
+
console.log(chalk.gray(`[Dry Run] Would write root AGENTS.md`));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const agentsPath = path.join(cwd, "AGENTS.md");
|
|
54
|
+
await fs.writeFile(agentsPath, finalAgentsContent, "utf8");
|
|
55
|
+
console.log(chalk.green("โ๏ธ Created root AGENTS.md with general guidelines."));
|
|
56
|
+
}
|
|
57
|
+
printSuccessAndNextSteps(options);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const isMonorepo = modules.length > 1 || (modules.length === 1 && modules[0].name !== ".");
|
|
61
|
+
// Write root AGENTS.md with monorepo details
|
|
62
|
+
if (isMonorepo) {
|
|
63
|
+
let monorepoContent = "### ๐ฆ Monorepo Multi-Module Project Structure\n\nThis repository is configured as a monorepo. Please load and follow the stack-specific guidelines in each subdirectory:\n\n";
|
|
64
|
+
for (const mod of modules) {
|
|
65
|
+
monorepoContent += `- **${mod.name}** (${mod.stacks.join(", ")}): Stack rules and guidelines are located at [${mod.name}/AGENTS.md](file:///${mod.dir.replace(/\\/g, "/")}/AGENTS.md)\n`;
|
|
66
|
+
}
|
|
67
|
+
const rootAgentsContent = await renderTemplate("common/AGENTS.md.hbs", {
|
|
68
|
+
stackContent: monorepoContent.trim(),
|
|
69
|
+
});
|
|
70
|
+
const rootAgentsPath = path.join(cwd, "AGENTS.md");
|
|
71
|
+
if (options.dryRun) {
|
|
72
|
+
console.log(chalk.gray(`[Dry Run] Would write root AGENTS.md containing monorepo navigation:\n${monorepoContent}`));
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
try {
|
|
76
|
+
await fs.access(rootAgentsPath);
|
|
77
|
+
await updateFileWithBlock(rootAgentsPath, "STACK_PACK", monorepoContent.trim());
|
|
78
|
+
console.log(chalk.green("โ๏ธ Updated STACK_PACK block in root AGENTS.md."));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
await fs.writeFile(rootAgentsPath, rootAgentsContent, "utf8");
|
|
82
|
+
console.log(chalk.green("โ๏ธ Created root AGENTS.md for monorepo."));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Process each module
|
|
87
|
+
for (const mod of modules) {
|
|
88
|
+
console.log(chalk.cyan(`\nProcessing module: ${mod.name} (stacks: ${mod.stacks.join(", ")})`));
|
|
89
|
+
let stackContent = "";
|
|
90
|
+
for (const stack of mod.stacks) {
|
|
91
|
+
try {
|
|
92
|
+
const rendered = await renderTemplate(`${stack}/AGENTS.md.hbs`, {});
|
|
93
|
+
stackContent += rendered + "\n\n";
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.warn(chalk.yellow(`Could not load template for stack '${stack}': ${err instanceof Error ? err.message : String(err)}`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
stackContent = stackContent.trim();
|
|
100
|
+
// Render AGENTS.md for this module
|
|
101
|
+
const agentsPath = path.join(mod.dir, "AGENTS.md");
|
|
102
|
+
const moduleAgentsContent = await renderTemplate("common/AGENTS.md.hbs", {
|
|
103
|
+
stackContent,
|
|
104
|
+
});
|
|
105
|
+
if (options.dryRun) {
|
|
106
|
+
console.log(chalk.gray(`[Dry Run] Would write AGENTS.md at ${agentsPath}`));
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
try {
|
|
110
|
+
await fs.access(agentsPath);
|
|
111
|
+
await updateFileWithBlock(agentsPath, "STACK_PACK", stackContent);
|
|
112
|
+
console.log(chalk.green(`โ๏ธ Updated STACK_PACK block in ${mod.name}/AGENTS.md`));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
await fs.writeFile(agentsPath, moduleAgentsContent, "utf8");
|
|
116
|
+
console.log(chalk.green(`โ๏ธ Created ${mod.name}/AGENTS.md`));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Copy rules and skills for each stack in this module
|
|
120
|
+
for (const stack of mod.stacks) {
|
|
121
|
+
// A. Rules
|
|
122
|
+
const rules = await getStackRules(stack);
|
|
123
|
+
for (const rule of rules) {
|
|
124
|
+
const relativeRulePath = `${stack}/rules/${rule}`;
|
|
125
|
+
try {
|
|
126
|
+
const ruleContent = await readStaticTemplateFile(relativeRulePath);
|
|
127
|
+
const targetRulePath = path.join(mod.dir, ".agents", "rules", rule);
|
|
128
|
+
if (options.dryRun) {
|
|
129
|
+
console.log(chalk.gray(`[Dry Run] Would write rule to ${targetRulePath} (length: ${ruleContent.length} chars)`));
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
await writeRuleWithChunking(targetRulePath, ruleContent);
|
|
133
|
+
console.log(chalk.green(`โ๏ธ Wrote rule ${rule} to ${mod.name}/.agents/rules/${rule}`));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
console.error(chalk.red(`Failed to copy rule ${rule}: ${err instanceof Error ? err.message : String(err)}`));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// B. Skills
|
|
141
|
+
const skills = await getStackSkills(stack);
|
|
142
|
+
for (const skill of skills) {
|
|
143
|
+
const relativeSkillPath = `${stack}/skills/${skill}`;
|
|
144
|
+
try {
|
|
145
|
+
const skillContent = await readStaticTemplateFile(relativeSkillPath);
|
|
146
|
+
const targetSkillPath = path.join(mod.dir, ".agents", "skills", skill);
|
|
147
|
+
if (options.dryRun) {
|
|
148
|
+
console.log(chalk.gray(`[Dry Run] Would write skill to ${targetSkillPath}`));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await fs.mkdir(path.dirname(targetSkillPath), { recursive: true });
|
|
152
|
+
await fs.writeFile(targetSkillPath, skillContent, "utf8");
|
|
153
|
+
console.log(chalk.green(`โ๏ธ Wrote skill ${skill} to ${mod.name}/.agents/skills/${skill}`));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
console.error(chalk.red(`Failed to copy skill ${skill}: ${err instanceof Error ? err.message : String(err)}`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
printSuccessAndNextSteps(options);
|
|
163
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import chalk from "chalk";
|
|
7
|
+
import { runInit } from "./commands/init.js";
|
|
8
|
+
import { runSync } from "./commands/sync.js";
|
|
9
|
+
import { runDoctor } from "./commands/doctor.js";
|
|
10
|
+
import { runExport } from "./commands/export.js";
|
|
11
|
+
const program = new Command();
|
|
12
|
+
export function runCli() {
|
|
13
|
+
program
|
|
14
|
+
.name("agent-workflow-kit")
|
|
15
|
+
.description("Generate AI coding workflows/rules/templates for Codex and Antigravity")
|
|
16
|
+
.version("1.0.0-mvp");
|
|
17
|
+
program
|
|
18
|
+
.command("init")
|
|
19
|
+
.description("Initialize agent guidelines and skills for the repository")
|
|
20
|
+
.option("--stack <stack>", "Specify target stack: auto | spring-boot | react-ts | fastapi", "auto")
|
|
21
|
+
.option("--agent <agent>", "Specify target agent profile: both | codex | antigravity", "both")
|
|
22
|
+
.option("--dry-run", "Output actions to console without writing any files", false)
|
|
23
|
+
.action(async (options) => {
|
|
24
|
+
try {
|
|
25
|
+
await runInit(options);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.error(chalk.red(`Error running init: ${err instanceof Error ? err.message : String(err)}`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
program
|
|
33
|
+
.command("sync")
|
|
34
|
+
.description("Sync generated guidelines and skills inside managed blocks")
|
|
35
|
+
.option("--dry-run", "Output changes to console without saving", false)
|
|
36
|
+
.action(async (options) => {
|
|
37
|
+
try {
|
|
38
|
+
await runSync(options);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
console.error(chalk.red(`Error running sync: ${err instanceof Error ? err.message : String(err)}`));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
program
|
|
46
|
+
.command("doctor")
|
|
47
|
+
.description("Verify active agent configurations, environment, and run validation tests")
|
|
48
|
+
.option("--install-hook", "Install a git pre-commit hook to automatically run doctor checks", false)
|
|
49
|
+
.action(async (options) => {
|
|
50
|
+
try {
|
|
51
|
+
await runDoctor(options);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
console.error(chalk.red(`Error running doctor: ${err instanceof Error ? err.message : String(err)}`));
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
program
|
|
59
|
+
.command("export <target>")
|
|
60
|
+
.description("Export custom workflows/skills for the target agent (e.g., 'antigravity')")
|
|
61
|
+
.option("--no-clipboard", "Do not copy the exported instructions to clipboard", false)
|
|
62
|
+
.action(async (target, options) => {
|
|
63
|
+
try {
|
|
64
|
+
await runExport(target, { clipboard: options.clipboard });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
console.error(chalk.red(`Error running export: ${err instanceof Error ? err.message : String(err)}`));
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
program.parse(process.argv);
|
|
72
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
/**
|
|
8
|
+
* Scans a specific folder directly for manifest configurations.
|
|
9
|
+
*/
|
|
10
|
+
async function detectProjectStackDirect(cwd) {
|
|
11
|
+
const stacks = [];
|
|
12
|
+
// 1. Detect Java Spring Boot (pom.xml)
|
|
13
|
+
try {
|
|
14
|
+
const pomPath = path.join(cwd, "pom.xml");
|
|
15
|
+
const stat = await fs.stat(pomPath);
|
|
16
|
+
if (stat.isFile()) {
|
|
17
|
+
stacks.push("spring-boot");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Ignore
|
|
22
|
+
}
|
|
23
|
+
// 2. Detect React + TypeScript (package.json containing react dependency)
|
|
24
|
+
try {
|
|
25
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
26
|
+
const content = await fs.readFile(pkgPath, "utf8");
|
|
27
|
+
const pkgJson = JSON.parse(content);
|
|
28
|
+
const hasReact = (pkgJson.dependencies && pkgJson.dependencies.react) ||
|
|
29
|
+
(pkgJson.devDependencies && pkgJson.devDependencies.react);
|
|
30
|
+
if (hasReact) {
|
|
31
|
+
stacks.push("react-ts");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Ignore
|
|
36
|
+
}
|
|
37
|
+
// 3. Detect Python FastAPI (pyproject.toml or requirements.txt containing fastapi)
|
|
38
|
+
try {
|
|
39
|
+
const pyprojectPath = path.join(cwd, "pyproject.toml");
|
|
40
|
+
const content = await fs.readFile(pyprojectPath, "utf8");
|
|
41
|
+
if (content.includes("fastapi")) {
|
|
42
|
+
stacks.push("fastapi");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
try {
|
|
47
|
+
const reqPath = path.join(cwd, "requirements.txt");
|
|
48
|
+
const content = await fs.readFile(reqPath, "utf8");
|
|
49
|
+
if (content.includes("fastapi")) {
|
|
50
|
+
stacks.push("fastapi");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Ignore
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return stacks;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Scans the workspace directory and subfolders (1-level deep) for monorepo detection.
|
|
61
|
+
*/
|
|
62
|
+
export async function detectProjectStack(cwd) {
|
|
63
|
+
const stacks = await detectProjectStackDirect(cwd);
|
|
64
|
+
// Scan 1-level deep subdirectories for potential monorepos/multimodules
|
|
65
|
+
try {
|
|
66
|
+
const entries = await fs.readdir(cwd, { withFileTypes: true });
|
|
67
|
+
for (const entry of entries) {
|
|
68
|
+
if (entry.isDirectory() &&
|
|
69
|
+
!entry.name.startsWith(".") &&
|
|
70
|
+
entry.name !== "node_modules" &&
|
|
71
|
+
entry.name !== "dist") {
|
|
72
|
+
const subPath = path.join(cwd, entry.name);
|
|
73
|
+
const subStacks = await detectProjectStackDirect(subPath);
|
|
74
|
+
for (const stack of subStacks) {
|
|
75
|
+
if (!stacks.includes(stack)) {
|
|
76
|
+
stacks.push(stack);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore
|
|
84
|
+
}
|
|
85
|
+
return stacks;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Detects stacks grouped by module directories (root + 1-level deep directories).
|
|
89
|
+
*/
|
|
90
|
+
export async function detectProjectModules(cwd) {
|
|
91
|
+
const modules = [];
|
|
92
|
+
const rootStacks = await detectProjectStackDirect(cwd);
|
|
93
|
+
if (rootStacks.length > 0) {
|
|
94
|
+
modules.push({
|
|
95
|
+
dir: cwd,
|
|
96
|
+
name: ".",
|
|
97
|
+
stacks: rootStacks,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const entries = await fs.readdir(cwd, { withFileTypes: true });
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.isDirectory() &&
|
|
104
|
+
!entry.name.startsWith(".") &&
|
|
105
|
+
entry.name !== "node_modules" &&
|
|
106
|
+
entry.name !== "dist") {
|
|
107
|
+
const subPath = path.join(cwd, entry.name);
|
|
108
|
+
const subStacks = await detectProjectStackDirect(subPath);
|
|
109
|
+
if (subStacks.length > 0) {
|
|
110
|
+
modules.push({
|
|
111
|
+
dir: subPath,
|
|
112
|
+
name: entry.name,
|
|
113
|
+
stacks: subStacks,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Ignore
|
|
121
|
+
}
|
|
122
|
+
return modules;
|
|
123
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
/**
|
|
8
|
+
* Splits the given string into chunks of at most maxChars.
|
|
9
|
+
* It tries to split at line boundaries to avoid cutting lines or words.
|
|
10
|
+
*/
|
|
11
|
+
export function chunkText(content, maxChars = 10000) {
|
|
12
|
+
const lines = content.split("\n");
|
|
13
|
+
const chunks = [];
|
|
14
|
+
let currentChunk = [];
|
|
15
|
+
let currentLength = 0;
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
const lineLen = line.length + 1; // +1 for the newline
|
|
18
|
+
if (currentLength + lineLen > maxChars && currentChunk.length > 0) {
|
|
19
|
+
chunks.push(currentChunk.join("\n"));
|
|
20
|
+
currentChunk = [];
|
|
21
|
+
currentLength = 0;
|
|
22
|
+
}
|
|
23
|
+
currentChunk.push(line);
|
|
24
|
+
currentLength += lineLen;
|
|
25
|
+
}
|
|
26
|
+
if (currentChunk.length > 0) {
|
|
27
|
+
chunks.push(currentChunk.join("\n"));
|
|
28
|
+
}
|
|
29
|
+
return chunks;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Deletes old rule part files matching the pattern `<rule-name>-part*.md`.
|
|
33
|
+
*/
|
|
34
|
+
async function cleanOldChunks(filePath) {
|
|
35
|
+
const dir = path.dirname(filePath);
|
|
36
|
+
const ext = path.extname(filePath);
|
|
37
|
+
const baseWithoutExt = path.basename(filePath, ext);
|
|
38
|
+
try {
|
|
39
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (entry.isFile() &&
|
|
42
|
+
entry.name.startsWith(`${baseWithoutExt}-part`) &&
|
|
43
|
+
entry.name.endsWith(ext)) {
|
|
44
|
+
await fs.unlink(path.join(dir, entry.name));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore directory not existing
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Writes rule file content.
|
|
54
|
+
* If the content is longer than 10,000 characters, it chunks it into part files
|
|
55
|
+
* and writes a main rule index referencing them with `@<part-file-name>.md`.
|
|
56
|
+
*/
|
|
57
|
+
export async function writeRuleWithChunking(filePath, content, maxChars = 10000) {
|
|
58
|
+
// Ensure the target directory exists
|
|
59
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
60
|
+
// Always clean up old parts first to prevent leftover files
|
|
61
|
+
await cleanOldChunks(filePath);
|
|
62
|
+
if (content.length <= maxChars) {
|
|
63
|
+
await fs.writeFile(filePath, content, "utf8");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Need to chunk
|
|
67
|
+
const chunks = chunkText(content, maxChars);
|
|
68
|
+
const ext = path.extname(filePath);
|
|
69
|
+
const baseWithoutExt = path.basename(filePath, ext);
|
|
70
|
+
const partRefs = [];
|
|
71
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
72
|
+
const partFileName = `${baseWithoutExt}-part${i + 1}${ext}`;
|
|
73
|
+
const partPath = path.join(path.dirname(filePath), partFileName);
|
|
74
|
+
await fs.writeFile(partPath, chunks[i], "utf8");
|
|
75
|
+
partRefs.push(`@${partFileName}`);
|
|
76
|
+
}
|
|
77
|
+
// Create main rule index file
|
|
78
|
+
const mainContent = `# ${baseWithoutExt} Rules
|
|
79
|
+
|
|
80
|
+
This ruleset is split into multiple parts to stay within agent context limitations. The agent MUST load and follow all referenced parts:
|
|
81
|
+
|
|
82
|
+
${partRefs.map((ref) => `- ${ref}`).join("\n")}
|
|
83
|
+
`;
|
|
84
|
+
await fs.writeFile(filePath, mainContent, "utf8");
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Updates a file with a managed block content.
|
|
88
|
+
* If the file does not exist, it creates it with the block wrapper.
|
|
89
|
+
* If the block does not exist in the file, it appends the block wrapper.
|
|
90
|
+
* If the block exists, it replaces the content inside the block.
|
|
91
|
+
*/
|
|
92
|
+
export async function updateFileWithBlock(filePath, blockId, newContent) {
|
|
93
|
+
const startTag = `<!-- AWK-START: ${blockId} -->`;
|
|
94
|
+
const endTag = `<!-- AWK-END: ${blockId} -->`;
|
|
95
|
+
const wrappedContent = `${startTag}\n${newContent}\n${endTag}`;
|
|
96
|
+
let fileContent = "";
|
|
97
|
+
try {
|
|
98
|
+
fileContent = await fs.readFile(filePath, "utf8");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// File doesn't exist, we will create it
|
|
102
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
103
|
+
await fs.writeFile(filePath, wrappedContent, "utf8");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Regex to match the block
|
|
107
|
+
const escapedStart = startTag.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
108
|
+
const escapedEnd = endTag.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
109
|
+
const regex = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}`);
|
|
110
|
+
if (regex.test(fileContent)) {
|
|
111
|
+
const updatedContent = fileContent.replace(regex, wrappedContent);
|
|
112
|
+
await fs.writeFile(filePath, updatedContent, "utf8");
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
// Append to end of file
|
|
116
|
+
const separator = fileContent.length > 0 && !fileContent.endsWith("\n") ? "\n\n" : "\n";
|
|
117
|
+
await fs.writeFile(filePath, `${fileContent}${separator}${wrappedContent}`, "utf8");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import handlebars from "handlebars";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = path.dirname(__filename);
|
|
11
|
+
// The templates directory is located at '../../templates' relative to 'dist/core/renderer.js'
|
|
12
|
+
const TEMPLATES_DIR = path.resolve(__dirname, "../../templates");
|
|
13
|
+
/**
|
|
14
|
+
* Loads a Handlebars template, compiles it, and renders it with the given context.
|
|
15
|
+
* @param templatePath Relative path to the template inside the templates directory (e.g. 'common/AGENTS.md.hbs')
|
|
16
|
+
* @param context Key-value context data for compilation
|
|
17
|
+
*/
|
|
18
|
+
export async function renderTemplate(templatePath, context) {
|
|
19
|
+
const fullPath = path.join(TEMPLATES_DIR, templatePath);
|
|
20
|
+
const fileContent = await fs.readFile(fullPath, "utf8");
|
|
21
|
+
const compiled = handlebars.compile(fileContent);
|
|
22
|
+
return compiled(context);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Reads a static rules file as-is without Handlebars interpolation.
|
|
26
|
+
* @param filePath Relative path inside templates folder (e.g. 'spring-boot/rules/java-style.md')
|
|
27
|
+
*/
|
|
28
|
+
export async function readStaticTemplateFile(filePath) {
|
|
29
|
+
const fullPath = path.join(TEMPLATES_DIR, filePath);
|
|
30
|
+
return fs.readFile(fullPath, "utf8");
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Gets all rules files for a stack.
|
|
34
|
+
*/
|
|
35
|
+
export async function getStackRules(stack) {
|
|
36
|
+
const rulesDir = path.join(TEMPLATES_DIR, stack, "rules");
|
|
37
|
+
try {
|
|
38
|
+
const files = await fs.readdir(rulesDir);
|
|
39
|
+
return files.filter((f) => f.endsWith(".md"));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Gets all skills for a stack.
|
|
47
|
+
*/
|
|
48
|
+
export async function getStackSkills(stack) {
|
|
49
|
+
const skillsDir = path.join(TEMPLATES_DIR, stack, "skills");
|
|
50
|
+
try {
|
|
51
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
52
|
+
const skills = [];
|
|
53
|
+
for (const entry of entries) {
|
|
54
|
+
if (entry.isDirectory()) {
|
|
55
|
+
const skillFilePath = path.join(skillsDir, entry.name, "SKILL.md");
|
|
56
|
+
try {
|
|
57
|
+
await fs.access(skillFilePath);
|
|
58
|
+
skills.push(`${entry.name}/SKILL.md`);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore if SKILL.md doesn't exist
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return skills;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
import { execa } from "execa";
|
|
6
|
+
/**
|
|
7
|
+
* Copies the provided text to the system clipboard in a cross-platform manner using execa.
|
|
8
|
+
*/
|
|
9
|
+
export async function writeToClipboard(text) {
|
|
10
|
+
try {
|
|
11
|
+
if (process.platform === "win32") {
|
|
12
|
+
// Use clip command on Windows
|
|
13
|
+
await execa("clip", { input: text });
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
else if (process.platform === "darwin") {
|
|
17
|
+
// Use pbcopy on macOS
|
|
18
|
+
await execa("pbcopy", { input: text });
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// Linux: try xclip first, fallback to xsel
|
|
23
|
+
try {
|
|
24
|
+
await execa("xclip", ["-selection", "clipboard"], { input: text });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
await execa("xsel", ["--clipboard", "--input"], { input: text });
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-workflow-kit-cli",
|
|
3
|
+
"version": "1.0.0-mvp",
|
|
4
|
+
"description": "AI-Ready Repository Workflow Generator & Guideline Optimizer for Codex and Antigravity",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-workflow-kit": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"templates",
|
|
13
|
+
"LICENSE",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"prepublishOnly": "npm run build && npm test"
|
|
21
|
+
},
|
|
22
|
+
"author": "Truong & Cat",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"chalk": "^5.3.0",
|
|
26
|
+
"commander": "^12.1.0",
|
|
27
|
+
"execa": "^9.2.0",
|
|
28
|
+
"handlebars": "^4.7.8"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^20.14.9",
|
|
32
|
+
"typescript": "^5.5.2",
|
|
33
|
+
"vitest": "^2.1.9"
|
|
34
|
+
}
|
|
35
|
+
}
|