@totoroyyb/amag 0.1.0
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/README.md +141 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +78 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +112 -0
- package/dist/installer.d.ts +12 -0
- package/dist/installer.js +268 -0
- package/dist/registry.d.ts +8 -0
- package/dist/registry.js +141 -0
- package/dist/utils.d.ts +14 -0
- package/dist/utils.js +110 -0
- package/package.json +49 -0
- package/templates/GEMINI.md +398 -0
- package/templates/resources/debug-escalation-template.md +34 -0
- package/templates/resources/plan-template.md +149 -0
- package/templates/rules/agentic-rules.md +18 -0
- package/templates/rules/code-quality.md +71 -0
- package/templates/rules/error-recovery.md +164 -0
- package/templates/rules/todo-enforcement.md +47 -0
- package/templates/skills/architecture-advisor/SKILL.md +111 -0
- package/templates/skills/browser-testing/SKILL.md +66 -0
- package/templates/skills/codebase-explorer/SKILL.md +128 -0
- package/templates/skills/deep-work/SKILL.md +75 -0
- package/templates/skills/external-cli-runner/SKILL.md +202 -0
- package/templates/skills/external-researcher/SKILL.md +99 -0
- package/templates/skills/frontend-ui-ux/SKILL.md +70 -0
- package/templates/skills/git-master/SKILL.md +72 -0
- package/templates/skills/plan-consultant/SKILL.md +218 -0
- package/templates/skills/plan-critic/SKILL.md +225 -0
- package/templates/skills/writing/SKILL.md +68 -0
- package/templates/workflows/debug-escalate.md +41 -0
- package/templates/workflows/debug.md +208 -0
- package/templates/workflows/explore.md +233 -0
- package/templates/workflows/init-deep.md +103 -0
- package/templates/workflows/plan.md +247 -0
- package/templates/workflows/resume.md +69 -0
- package/templates/workflows/start-work.md +189 -0
- package/templates/workflows/ultrawork.md +127 -0
package/README.md
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# AMAG
|
|
2
|
+
|
|
3
|
+
Agent orchestration for [Antigravity](https://antigravity.google). Curated rules, workflows, and skills that transform Antigravity's AI agent into a disciplined engineering partner.
|
|
4
|
+
|
|
5
|
+
Inspired by [oh-my-opencode](https://github.com/code-yeongyu/oh-my-opencode) — rebuilt from scratch for Antigravity's native tooling.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @totoroyyb/amag
|
|
11
|
+
|
|
12
|
+
# Initialize in your project
|
|
13
|
+
cd /path/to/your/project
|
|
14
|
+
amag init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install from source:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
git clone https://github.com/totoroyyb/amag.git
|
|
21
|
+
cd amag && npm install && npm run build && npm link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That's it. Your project now has a `GEMINI.md` root prompt, always-on rules, slash-command workflows, and on-demand skills.
|
|
25
|
+
|
|
26
|
+
## What You Get
|
|
27
|
+
|
|
28
|
+
### Core System (`GEMINI.md`)
|
|
29
|
+
|
|
30
|
+
The root prompt that orchestrates everything:
|
|
31
|
+
|
|
32
|
+
- **Intent Gate** — classifies every request before acting (trivial, explicit, exploratory, open-ended)
|
|
33
|
+
- **True Intent Extraction** — understands what you *actually* want, not just what you typed
|
|
34
|
+
- **Auto-Ultrawork** — maximum rigor is always on: full verification, zero scope reduction
|
|
35
|
+
- **Codebase Assessment** — evaluates project maturity and adapts behavior accordingly
|
|
36
|
+
- **Verification Protocol** — evidence-based completion with a 6-step checklist (spec compliance → build → tests → debug scan)
|
|
37
|
+
|
|
38
|
+
### Rules (Always-On)
|
|
39
|
+
|
|
40
|
+
Loaded into every conversation automatically.
|
|
41
|
+
|
|
42
|
+
| Rule | What it does |
|
|
43
|
+
|------|-------------|
|
|
44
|
+
| `code-quality` | Think-first coding, surgical changes, no AI slop |
|
|
45
|
+
| `error-recovery` | 3-failure escalation, blind retry prevention, hung command detection |
|
|
46
|
+
| `todo-enforcement` | Task breakdown, progress tracking, never abandon work |
|
|
47
|
+
| `agentic-rules` | Session start checks, active plan detection, auto-resume guidance |
|
|
48
|
+
|
|
49
|
+
### Workflows (Slash Commands)
|
|
50
|
+
|
|
51
|
+
Type the command to activate.
|
|
52
|
+
|
|
53
|
+
| Command | What it does |
|
|
54
|
+
|---------|-------------|
|
|
55
|
+
| `/plan` | Planning interview with optional external consultant + critic review |
|
|
56
|
+
| `/start-work` | Execute a plan task-by-task with category-aware delegation |
|
|
57
|
+
| `/resume` | Cross-session resume from `.amag/active-plan.md` |
|
|
58
|
+
| `/debug` | 6-phase systematic debugging with external agent escalation |
|
|
59
|
+
| `/debug-escalate` | Immediately escalate to external CLI during an active `/debug` |
|
|
60
|
+
| `/explore` | Read-only multi-phase codebase exploration and architecture synthesis |
|
|
61
|
+
| `/ultrawork` | Maximum effort mode — 100% certainty before acting |
|
|
62
|
+
| `/init-deep` | Generate hierarchical `GEMINI.md` context files across the codebase |
|
|
63
|
+
|
|
64
|
+
### Skills (On-Demand)
|
|
65
|
+
|
|
66
|
+
Auto-loaded when relevant, or loaded explicitly by workflows.
|
|
67
|
+
|
|
68
|
+
| Skill | What it does |
|
|
69
|
+
|-------|-------------|
|
|
70
|
+
| `deep-work` | Autonomous exploration — read extensively, build mental model, act decisively |
|
|
71
|
+
| `git-master` | Atomic commits, conventional format, rebasing, conflict resolution |
|
|
72
|
+
| `browser-testing` | Visual testing via Antigravity's `browser_subagent` |
|
|
73
|
+
| `frontend-ui-ux` | Design-first UI with bold aesthetics and responsive patterns |
|
|
74
|
+
| `writing` | Anti-AI-slop technical writing — plain words, human tone |
|
|
75
|
+
| `architecture-advisor` | Read-only design review with simplicity bias |
|
|
76
|
+
| `codebase-explorer` | Structured parallel codebase research and cross-validation |
|
|
77
|
+
| `external-researcher` | External library/API research — official docs and best practices |
|
|
78
|
+
| `external-cli-runner` | Unified runner for Claude, Codex, and Gemini CLIs with retry logic |
|
|
79
|
+
| `plan-consultant` | Pre-plan gap analysis — surface missing requirements before generation |
|
|
80
|
+
| `plan-critic` | Post-plan adversarial review — verify references and executability |
|
|
81
|
+
|
|
82
|
+
## Typical Workflow
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
You: /plan add OAuth login with Google and GitHub
|
|
86
|
+
Agent: [explores codebase → asks clarifying questions → drafts plan]
|
|
87
|
+
[runs plan through consultant + critic if configured]
|
|
88
|
+
→ implementation_plan.md ready for review
|
|
89
|
+
|
|
90
|
+
You: /start-work
|
|
91
|
+
Agent: [executes plan task-by-task: build → type-check → test after each]
|
|
92
|
+
→ all tasks complete, verified with evidence
|
|
93
|
+
|
|
94
|
+
You: /resume # pick up where you left off in a new session
|
|
95
|
+
|
|
96
|
+
You: /debug login redirects fail on Safari
|
|
97
|
+
Agent: [reproduce → hypothesize → instrument → root-cause → fix → verify]
|
|
98
|
+
[escalates to external CLI if stuck: /debug-escalate]
|
|
99
|
+
|
|
100
|
+
You: /explore how does the auth middleware chain work?
|
|
101
|
+
Agent: [structural scan → module deep-dives → synthesizes architecture doc]
|
|
102
|
+
→ read-only, never modifies code
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## External Agent Integration
|
|
106
|
+
|
|
107
|
+
AMAG can delegate to external CLI agents (Claude, Codex, Gemini) for plan review and debugging consultation. Configure via:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
amag config show # View current config
|
|
111
|
+
amag config set review.consultant.cli claude # Set plan consultant
|
|
112
|
+
amag config set debug.consultant.cli codex # Set debug consultant
|
|
113
|
+
amag config set review.critic.thinking high # Set thinking level
|
|
114
|
+
amag config reset # Reset to defaults
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Thinking levels: `max`, `high`, `medium`, `low`, `none`.
|
|
118
|
+
|
|
119
|
+
## CLI Reference
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
amag init # Install all components
|
|
123
|
+
amag update # Overwrite with latest templates
|
|
124
|
+
amag add <type> <name> # Install one component (rule, workflow, skill)
|
|
125
|
+
amag remove <type> <name> # Remove one component
|
|
126
|
+
amag uninstall # Remove all AMAG files
|
|
127
|
+
amag list # Show available components
|
|
128
|
+
amag doctor # Check installation status
|
|
129
|
+
amag config show|set|reset # Manage configuration
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Design Philosophy
|
|
133
|
+
|
|
134
|
+
1. **System prompts are the product** — the CLI is just a delivery mechanism
|
|
135
|
+
2. **Every template earns its place** — no bloat, curated for real engineering workflows
|
|
136
|
+
3. **Antigravity-native** — built on AG's actual tools, not abstractions
|
|
137
|
+
4. **Composable** — add only what you need, remove what you don't
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type ExternalCLI = "codex" | "claude" | "gemini-cli";
|
|
2
|
+
export type ThinkingLevel = "max" | "high" | "medium" | "low" | "none";
|
|
3
|
+
export interface ExternalAgentConfig {
|
|
4
|
+
cli: ExternalCLI | null;
|
|
5
|
+
model: string | null;
|
|
6
|
+
thinking: ThinkingLevel;
|
|
7
|
+
}
|
|
8
|
+
export interface ReviewConfig {
|
|
9
|
+
consultant: ExternalAgentConfig;
|
|
10
|
+
critic: ExternalAgentConfig;
|
|
11
|
+
timeout_ms: number;
|
|
12
|
+
}
|
|
13
|
+
export interface DebugConfig {
|
|
14
|
+
consultant: ExternalAgentConfig;
|
|
15
|
+
timeout_ms: number;
|
|
16
|
+
}
|
|
17
|
+
export interface AmagConfig {
|
|
18
|
+
review: ReviewConfig;
|
|
19
|
+
debug: DebugConfig;
|
|
20
|
+
}
|
|
21
|
+
export declare function getDefaultConfig(): AmagConfig;
|
|
22
|
+
export declare function readConfig(targetDir: string): Promise<AmagConfig>;
|
|
23
|
+
export declare function writeConfig(targetDir: string, config: AmagConfig): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Set a dot-path value in the config.
|
|
26
|
+
* e.g. "review.consultant.cli" → "codex"
|
|
27
|
+
*/
|
|
28
|
+
export declare function setConfigValue(targetDir: string, dotPath: string, value: string): Promise<void>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import { resolveProjectDir, log } from "./utils.js";
|
|
4
|
+
// --- Defaults ---
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
review: {
|
|
7
|
+
consultant: { cli: "claude", model: "claude-opus-4-6", thinking: "max" },
|
|
8
|
+
critic: { cli: "codex", model: "gpt-5.2", thinking: "medium" },
|
|
9
|
+
timeout_ms: 600000,
|
|
10
|
+
},
|
|
11
|
+
debug: {
|
|
12
|
+
consultant: { cli: "codex", model: "gpt-5.2", thinking: "high" },
|
|
13
|
+
timeout_ms: 600000,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
// --- Functions ---
|
|
17
|
+
function configPath(projectDir) {
|
|
18
|
+
return path.join(projectDir, ".amag", "config.json");
|
|
19
|
+
}
|
|
20
|
+
export function getDefaultConfig() {
|
|
21
|
+
return structuredClone(DEFAULT_CONFIG);
|
|
22
|
+
}
|
|
23
|
+
export async function readConfig(targetDir) {
|
|
24
|
+
const projectDir = resolveProjectDir(targetDir);
|
|
25
|
+
const filePath = configPath(projectDir);
|
|
26
|
+
if (await fs.pathExists(filePath)) {
|
|
27
|
+
try {
|
|
28
|
+
const raw = await fs.readJson(filePath);
|
|
29
|
+
// Merge with defaults to handle missing keys from older configs
|
|
30
|
+
return {
|
|
31
|
+
review: {
|
|
32
|
+
consultant: { ...DEFAULT_CONFIG.review.consultant, ...raw?.review?.consultant },
|
|
33
|
+
critic: { ...DEFAULT_CONFIG.review.critic, ...raw?.review?.critic },
|
|
34
|
+
timeout_ms: raw?.review?.timeout_ms ?? DEFAULT_CONFIG.review.timeout_ms,
|
|
35
|
+
},
|
|
36
|
+
debug: {
|
|
37
|
+
consultant: { ...DEFAULT_CONFIG.debug.consultant, ...raw?.debug?.consultant },
|
|
38
|
+
timeout_ms: raw?.debug?.timeout_ms ?? DEFAULT_CONFIG.debug.timeout_ms,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
log.warn("Invalid .amag/config.json — using defaults");
|
|
44
|
+
return getDefaultConfig();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return getDefaultConfig();
|
|
48
|
+
}
|
|
49
|
+
export async function writeConfig(targetDir, config) {
|
|
50
|
+
const projectDir = resolveProjectDir(targetDir);
|
|
51
|
+
const filePath = configPath(projectDir);
|
|
52
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
53
|
+
await fs.writeJson(filePath, config, { spaces: 2 });
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Set a dot-path value in the config.
|
|
57
|
+
* e.g. "review.consultant.cli" → "codex"
|
|
58
|
+
*/
|
|
59
|
+
export async function setConfigValue(targetDir, dotPath, value) {
|
|
60
|
+
const config = await readConfig(targetDir);
|
|
61
|
+
const parts = dotPath.split(".");
|
|
62
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
63
|
+
let obj = config;
|
|
64
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
65
|
+
if (obj[parts[i]] === undefined) {
|
|
66
|
+
throw new Error(`Invalid config path: ${dotPath}`);
|
|
67
|
+
}
|
|
68
|
+
obj = obj[parts[i]];
|
|
69
|
+
}
|
|
70
|
+
const lastKey = parts[parts.length - 1];
|
|
71
|
+
if (obj[lastKey] === undefined) {
|
|
72
|
+
throw new Error(`Invalid config path: ${dotPath}`);
|
|
73
|
+
}
|
|
74
|
+
// Coerce numeric values
|
|
75
|
+
const numericValue = Number(value);
|
|
76
|
+
obj[lastKey] = Number.isNaN(numericValue) ? (value === "null" ? null : value) : numericValue;
|
|
77
|
+
await writeConfig(targetDir, config);
|
|
78
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { initCommand } from "./installer.js";
|
|
4
|
+
import { listComponents } from "./registry.js";
|
|
5
|
+
import { doctorCheck } from "./utils.js";
|
|
6
|
+
import readline from "node:readline/promises";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("amag")
|
|
10
|
+
.description("OmO-style agent orchestration for Antigravity — install rules, workflows, and skills")
|
|
11
|
+
.version("0.1.0");
|
|
12
|
+
program
|
|
13
|
+
.command("init")
|
|
14
|
+
.description("Install all AMAG components (skips existing files with warning)")
|
|
15
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
16
|
+
.option("--no-gemini-md", "Skip generating GEMINI.md at project root")
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
await initCommand(options.target, { skipGeminiMd: !options.geminiMd });
|
|
19
|
+
});
|
|
20
|
+
program
|
|
21
|
+
.command("update")
|
|
22
|
+
.description("Overwrite all AMAG components with latest templates")
|
|
23
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
24
|
+
.option("--no-gemini-md", "Skip updating GEMINI.md at project root")
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
const { updateCommand } = await import("./installer.js");
|
|
27
|
+
await updateCommand(options.target, { skipGeminiMd: !options.geminiMd });
|
|
28
|
+
});
|
|
29
|
+
program
|
|
30
|
+
.command("add <type> <name>")
|
|
31
|
+
.description("Add a single component (rule, workflow, or skill)")
|
|
32
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
33
|
+
.action(async (type, name, options) => {
|
|
34
|
+
const { addComponent } = await import("./installer.js");
|
|
35
|
+
await addComponent(type, name, options.target);
|
|
36
|
+
});
|
|
37
|
+
program
|
|
38
|
+
.command("uninstall")
|
|
39
|
+
.description("Remove all AMAG-managed files from the target project")
|
|
40
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
41
|
+
.option("--keep-gemini-md", "Keep GEMINI.md at project root")
|
|
42
|
+
.option("-f, --force", "Skip confirmation prompt")
|
|
43
|
+
.action(async (options) => {
|
|
44
|
+
if (!options.force) {
|
|
45
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
46
|
+
const answer = await rl.question("This will remove all AMAG components. Continue? (y/N) ");
|
|
47
|
+
rl.close();
|
|
48
|
+
if (answer.toLowerCase() !== "y") {
|
|
49
|
+
console.log("Cancelled.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const { uninstallCommand } = await import("./installer.js");
|
|
54
|
+
await uninstallCommand(options.target, { keepGeminiMd: !!options.keepGeminiMd });
|
|
55
|
+
});
|
|
56
|
+
program
|
|
57
|
+
.command("remove <type> <name>")
|
|
58
|
+
.description("Remove a single component (rule, workflow, or skill)")
|
|
59
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
60
|
+
.action(async (type, name, options) => {
|
|
61
|
+
const { removeComponent } = await import("./installer.js");
|
|
62
|
+
await removeComponent(type, name, options.target);
|
|
63
|
+
});
|
|
64
|
+
program
|
|
65
|
+
.command("list")
|
|
66
|
+
.description("List all available components")
|
|
67
|
+
.action(() => {
|
|
68
|
+
listComponents();
|
|
69
|
+
});
|
|
70
|
+
program
|
|
71
|
+
.command("doctor")
|
|
72
|
+
.description("Check what AMAG components are installed in the target project")
|
|
73
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
74
|
+
.action(async (options) => {
|
|
75
|
+
await doctorCheck(options.target);
|
|
76
|
+
});
|
|
77
|
+
const configCmd = program
|
|
78
|
+
.command("config")
|
|
79
|
+
.description("Manage AMAG review configuration");
|
|
80
|
+
configCmd
|
|
81
|
+
.command("show")
|
|
82
|
+
.description("Show current review configuration")
|
|
83
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
84
|
+
.action(async (options) => {
|
|
85
|
+
const { readConfig } = await import("./config.js");
|
|
86
|
+
const config = await readConfig(options.target);
|
|
87
|
+
console.log(JSON.stringify(config, null, 2));
|
|
88
|
+
});
|
|
89
|
+
configCmd
|
|
90
|
+
.command("set <path> <value>")
|
|
91
|
+
.description("Set a config value (e.g. review.consultant.cli codex)")
|
|
92
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
93
|
+
.action(async (dotPath, value, options) => {
|
|
94
|
+
const { setConfigValue, readConfig } = await import("./config.js");
|
|
95
|
+
await setConfigValue(options.target, dotPath, value);
|
|
96
|
+
const config = await readConfig(options.target);
|
|
97
|
+
const { log } = await import("./utils.js");
|
|
98
|
+
log.success(`Set ${dotPath} = ${value}`);
|
|
99
|
+
console.log(JSON.stringify(config, null, 2));
|
|
100
|
+
});
|
|
101
|
+
configCmd
|
|
102
|
+
.command("reset")
|
|
103
|
+
.description("Reset configuration to defaults")
|
|
104
|
+
.option("-t, --target <dir>", "Target project directory", ".")
|
|
105
|
+
.action(async (options) => {
|
|
106
|
+
const { writeConfig, getDefaultConfig } = await import("./config.js");
|
|
107
|
+
await writeConfig(options.target, getDefaultConfig());
|
|
108
|
+
const { log } = await import("./utils.js");
|
|
109
|
+
log.success("Configuration reset to defaults");
|
|
110
|
+
console.log(JSON.stringify(getDefaultConfig(), null, 2));
|
|
111
|
+
});
|
|
112
|
+
program.parse();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function initCommand(targetDir: string, options: {
|
|
2
|
+
skipGeminiMd: boolean;
|
|
3
|
+
force?: boolean;
|
|
4
|
+
}, confirmFn?: () => Promise<boolean>): Promise<void>;
|
|
5
|
+
export declare function updateCommand(targetDir: string, options: {
|
|
6
|
+
skipGeminiMd: boolean;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
export declare function addComponent(type: string, name: string, targetDir: string): Promise<void>;
|
|
9
|
+
export declare function uninstallCommand(targetDir: string, options: {
|
|
10
|
+
keepGeminiMd: boolean;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
export declare function removeComponent(type: string, name: string, targetDir: string): Promise<void>;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { COMPONENTS } from "./registry.js";
|
|
2
|
+
import { resolveProjectDir, copyTemplate, removeIfExists, cleanEmptyDirs, log } from "./utils.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fs from "fs-extra";
|
|
5
|
+
const DEST_MAP = {
|
|
6
|
+
rule: ".agent/rules",
|
|
7
|
+
workflow: ".agent/workflows",
|
|
8
|
+
skill: ".agent/skills",
|
|
9
|
+
};
|
|
10
|
+
const SRC_MAP = {
|
|
11
|
+
rule: "rules",
|
|
12
|
+
workflow: "workflows",
|
|
13
|
+
skill: "skills",
|
|
14
|
+
};
|
|
15
|
+
function getTemplatesDir() {
|
|
16
|
+
// Resolve relative to this file's location (dist/ or src/)
|
|
17
|
+
return path.resolve(import.meta.dirname, "..", "templates");
|
|
18
|
+
}
|
|
19
|
+
function getDestPath(comp, projectDir) {
|
|
20
|
+
const destDir = path.join(projectDir, DEST_MAP[comp.type]);
|
|
21
|
+
if (comp.type === "skill") {
|
|
22
|
+
return path.join(destDir, comp.name, "SKILL.md");
|
|
23
|
+
}
|
|
24
|
+
return path.join(destDir, `${comp.name}.md`);
|
|
25
|
+
}
|
|
26
|
+
async function installComponent(comp, templatesDir, projectDir, options) {
|
|
27
|
+
const srcDir = path.join(templatesDir, SRC_MAP[comp.type]);
|
|
28
|
+
const destDir = path.join(projectDir, DEST_MAP[comp.type]);
|
|
29
|
+
if (comp.type === "skill") {
|
|
30
|
+
const srcSkillDir = path.join(srcDir, comp.name);
|
|
31
|
+
const destSkillDir = path.join(destDir, comp.name);
|
|
32
|
+
await fs.ensureDir(destSkillDir);
|
|
33
|
+
await copyTemplate(path.join(srcSkillDir, "SKILL.md"), path.join(destSkillDir, "SKILL.md"), comp.name, options);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
await fs.ensureDir(destDir);
|
|
37
|
+
await copyTemplate(path.join(srcDir, `${comp.name}.md`), path.join(destDir, `${comp.name}.md`), comp.name, options);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function copyResources(templatesDir, projectDir, options) {
|
|
41
|
+
const srcDir = path.join(templatesDir, "resources");
|
|
42
|
+
const destDir = path.join(projectDir, ".agent", "resources");
|
|
43
|
+
let updated = 0;
|
|
44
|
+
let created = 0;
|
|
45
|
+
if (!(await fs.pathExists(srcDir)))
|
|
46
|
+
return { updated, created };
|
|
47
|
+
await fs.ensureDir(destDir);
|
|
48
|
+
for (const file of await fs.readdir(srcDir)) {
|
|
49
|
+
if (!file.endsWith(".md"))
|
|
50
|
+
continue;
|
|
51
|
+
const dest = path.join(destDir, file);
|
|
52
|
+
const existed = await fs.pathExists(dest);
|
|
53
|
+
await copyTemplate(path.join(srcDir, file), dest, `resource/${file}`, options);
|
|
54
|
+
if (existed)
|
|
55
|
+
updated++;
|
|
56
|
+
else
|
|
57
|
+
created++;
|
|
58
|
+
}
|
|
59
|
+
return { updated, created };
|
|
60
|
+
}
|
|
61
|
+
async function findExistingComponents(projectDir) {
|
|
62
|
+
const existing = [];
|
|
63
|
+
for (const comp of COMPONENTS) {
|
|
64
|
+
const dest = getDestPath(comp, projectDir);
|
|
65
|
+
if (await fs.pathExists(dest)) {
|
|
66
|
+
existing.push(`${comp.type}/${comp.name}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (await fs.pathExists(path.join(projectDir, "GEMINI.md"))) {
|
|
70
|
+
existing.push("GEMINI.md");
|
|
71
|
+
}
|
|
72
|
+
if (await fs.pathExists(path.join(projectDir, ".amag", "config.json"))) {
|
|
73
|
+
existing.push(".amag/config.json");
|
|
74
|
+
}
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
export async function initCommand(targetDir, options, confirmFn) {
|
|
78
|
+
const projectDir = resolveProjectDir(targetDir);
|
|
79
|
+
const templatesDir = getTemplatesDir();
|
|
80
|
+
log.header(`AMAG — Installing to ${projectDir}`);
|
|
81
|
+
// Check for existing files and warn
|
|
82
|
+
if (!options.force) {
|
|
83
|
+
const existing = await findExistingComponents(projectDir);
|
|
84
|
+
if (existing.length > 0) {
|
|
85
|
+
log.warn(`\n${existing.length} file(s) already exist and will be SKIPPED:`);
|
|
86
|
+
for (const name of existing) {
|
|
87
|
+
log.info(` • ${name}`);
|
|
88
|
+
}
|
|
89
|
+
console.log();
|
|
90
|
+
log.info("Use `amag update` to overwrite existing files with latest templates.");
|
|
91
|
+
console.log();
|
|
92
|
+
if (confirmFn) {
|
|
93
|
+
const proceed = await confirmFn();
|
|
94
|
+
if (!proceed) {
|
|
95
|
+
console.log("Cancelled.");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Install all components
|
|
102
|
+
for (const comp of COMPONENTS) {
|
|
103
|
+
await installComponent(comp, templatesDir, projectDir, { force: options.force });
|
|
104
|
+
}
|
|
105
|
+
// Copy resource files (e.g., plan-template.md)
|
|
106
|
+
await copyResources(templatesDir, projectDir, { force: options.force });
|
|
107
|
+
// Generate GEMINI.md at project root
|
|
108
|
+
if (!options.skipGeminiMd) {
|
|
109
|
+
await copyTemplate(path.join(templatesDir, "GEMINI.md"), path.join(projectDir, "GEMINI.md"), "GEMINI.md", { force: options.force });
|
|
110
|
+
}
|
|
111
|
+
// Generate default .amag/config.json
|
|
112
|
+
const configPath = path.join(projectDir, ".amag", "config.json");
|
|
113
|
+
if (options.force || !(await fs.pathExists(configPath))) {
|
|
114
|
+
const { writeConfig, getDefaultConfig } = await import("./config.js");
|
|
115
|
+
await writeConfig(targetDir, getDefaultConfig());
|
|
116
|
+
log.success(".amag/config.json");
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
log.warn(".amag/config.json (exists, skipped)");
|
|
120
|
+
}
|
|
121
|
+
log.success("\nDone! AMAG is installed.");
|
|
122
|
+
log.info("Run `amag doctor` to verify installation.");
|
|
123
|
+
}
|
|
124
|
+
export async function updateCommand(targetDir, options) {
|
|
125
|
+
const projectDir = resolveProjectDir(targetDir);
|
|
126
|
+
const templatesDir = getTemplatesDir();
|
|
127
|
+
log.header(`AMAG — Updating ${projectDir}`);
|
|
128
|
+
let updated = 0;
|
|
129
|
+
let created = 0;
|
|
130
|
+
for (const comp of COMPONENTS) {
|
|
131
|
+
const dest = getDestPath(comp, projectDir);
|
|
132
|
+
const existed = await fs.pathExists(dest);
|
|
133
|
+
await installComponent(comp, templatesDir, projectDir, { force: true });
|
|
134
|
+
if (existed)
|
|
135
|
+
updated++;
|
|
136
|
+
else
|
|
137
|
+
created++;
|
|
138
|
+
}
|
|
139
|
+
// Copy resource files
|
|
140
|
+
const resourceResult = await copyResources(templatesDir, projectDir, { force: true });
|
|
141
|
+
updated += resourceResult.updated;
|
|
142
|
+
created += resourceResult.created;
|
|
143
|
+
if (!options.skipGeminiMd) {
|
|
144
|
+
const geminiPath = path.join(projectDir, "GEMINI.md");
|
|
145
|
+
const existed = await fs.pathExists(geminiPath);
|
|
146
|
+
await copyTemplate(path.join(templatesDir, "GEMINI.md"), geminiPath, "GEMINI.md", { force: true });
|
|
147
|
+
if (existed)
|
|
148
|
+
updated++;
|
|
149
|
+
else
|
|
150
|
+
created++;
|
|
151
|
+
}
|
|
152
|
+
// Update .amag/config.json — merge with defaults to pick up new fields
|
|
153
|
+
const configFilePath = path.join(projectDir, ".amag", "config.json");
|
|
154
|
+
const { readConfig, writeConfig } = await import("./config.js");
|
|
155
|
+
if (await fs.pathExists(configFilePath)) {
|
|
156
|
+
// Re-read merges user overrides with latest defaults (picks up new fields)
|
|
157
|
+
const merged = await readConfig(targetDir);
|
|
158
|
+
await writeConfig(targetDir, merged);
|
|
159
|
+
log.success(".amag/config.json (merged with latest defaults)");
|
|
160
|
+
updated++;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const { getDefaultConfig } = await import("./config.js");
|
|
164
|
+
await writeConfig(targetDir, getDefaultConfig());
|
|
165
|
+
log.success(".amag/config.json (created)");
|
|
166
|
+
created++;
|
|
167
|
+
}
|
|
168
|
+
console.log();
|
|
169
|
+
log.success(`Done! ${updated} updated, ${created} newly created.`);
|
|
170
|
+
}
|
|
171
|
+
export async function addComponent(type, name, targetDir) {
|
|
172
|
+
const validTypes = ["rule", "workflow", "skill"];
|
|
173
|
+
if (!validTypes.includes(type)) {
|
|
174
|
+
log.error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
const comp = COMPONENTS.find((c) => c.type === type && c.name === name);
|
|
178
|
+
if (!comp) {
|
|
179
|
+
log.error(`Unknown ${type} "${name}". Run \`amag list\` to see available components.`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
const projectDir = resolveProjectDir(targetDir);
|
|
183
|
+
const templatesDir = getTemplatesDir();
|
|
184
|
+
await installComponent(comp, templatesDir, projectDir);
|
|
185
|
+
log.success(`\nAdded ${type} "${name}" to ${projectDir}`);
|
|
186
|
+
}
|
|
187
|
+
export async function uninstallCommand(targetDir, options) {
|
|
188
|
+
const projectDir = resolveProjectDir(targetDir);
|
|
189
|
+
log.header(`AMAG — Uninstalling from ${projectDir}`);
|
|
190
|
+
let removed = 0;
|
|
191
|
+
let absent = 0;
|
|
192
|
+
// Remove all registered components
|
|
193
|
+
for (const comp of COMPONENTS) {
|
|
194
|
+
const destDir = path.join(projectDir, DEST_MAP[comp.type]);
|
|
195
|
+
let filePath;
|
|
196
|
+
if (comp.type === "skill") {
|
|
197
|
+
filePath = path.join(destDir, comp.name); // entire skill directory
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
filePath = path.join(destDir, `${comp.name}.md`);
|
|
201
|
+
}
|
|
202
|
+
const wasRemoved = await removeIfExists(filePath, `${comp.type}/${comp.name}`);
|
|
203
|
+
if (wasRemoved)
|
|
204
|
+
removed++;
|
|
205
|
+
else
|
|
206
|
+
absent++;
|
|
207
|
+
}
|
|
208
|
+
// Clean up empty directories
|
|
209
|
+
const seenDirs = new Set();
|
|
210
|
+
for (const comp of COMPONENTS) {
|
|
211
|
+
const destDir = path.join(projectDir, DEST_MAP[comp.type]);
|
|
212
|
+
if (!seenDirs.has(destDir)) {
|
|
213
|
+
seenDirs.add(destDir);
|
|
214
|
+
await cleanEmptyDirs(destDir, projectDir);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Remove GEMINI.md
|
|
218
|
+
if (!options.keepGeminiMd) {
|
|
219
|
+
const wasRemoved = await removeIfExists(path.join(projectDir, "GEMINI.md"), "GEMINI.md");
|
|
220
|
+
if (wasRemoved)
|
|
221
|
+
removed++;
|
|
222
|
+
else
|
|
223
|
+
absent++;
|
|
224
|
+
}
|
|
225
|
+
// Remove .amag/config.json
|
|
226
|
+
const configRemoved = await removeIfExists(path.join(projectDir, ".amag", "config.json"), ".amag/config.json");
|
|
227
|
+
if (configRemoved)
|
|
228
|
+
removed++;
|
|
229
|
+
else
|
|
230
|
+
absent++;
|
|
231
|
+
// Remove resource files
|
|
232
|
+
const resourcesDir = path.join(projectDir, ".agent", "resources");
|
|
233
|
+
if (await fs.pathExists(resourcesDir)) {
|
|
234
|
+
const wasRemoved = await removeIfExists(resourcesDir, ".agent/resources");
|
|
235
|
+
if (wasRemoved)
|
|
236
|
+
removed++;
|
|
237
|
+
else
|
|
238
|
+
absent++;
|
|
239
|
+
}
|
|
240
|
+
// Clean .amag directory if empty
|
|
241
|
+
await cleanEmptyDirs(path.join(projectDir, ".amag"), projectDir);
|
|
242
|
+
console.log();
|
|
243
|
+
log.success(`Done! ${removed} removed, ${absent} already absent.`);
|
|
244
|
+
}
|
|
245
|
+
export async function removeComponent(type, name, targetDir) {
|
|
246
|
+
const validTypes = ["rule", "workflow", "skill"];
|
|
247
|
+
if (!validTypes.includes(type)) {
|
|
248
|
+
log.error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`);
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
const comp = COMPONENTS.find((c) => c.type === type && c.name === name);
|
|
252
|
+
if (!comp) {
|
|
253
|
+
log.error(`Unknown ${type} "${name}". Run \`amag list\` to see available components.`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
const projectDir = resolveProjectDir(targetDir);
|
|
257
|
+
const componentType = type;
|
|
258
|
+
const destDir = path.join(projectDir, DEST_MAP[componentType]);
|
|
259
|
+
let filePath;
|
|
260
|
+
if (componentType === "skill") {
|
|
261
|
+
filePath = path.join(destDir, name);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
filePath = path.join(destDir, `${name}.md`);
|
|
265
|
+
}
|
|
266
|
+
await removeIfExists(filePath, `${type}/${name}`);
|
|
267
|
+
await cleanEmptyDirs(destDir, projectDir);
|
|
268
|
+
}
|