@ronaldjdevfs/forge 1.0.2 → 1.2.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 +132 -10
- package/package.json +6 -3
- package/skills/forge/SKILL.md +86 -15
- package/skills/forge/profiles/express-drizzle.md +107 -0
- package/skills/forge/profiles/fastify-mongodb.md +103 -0
- package/skills/forge/profiles/fastify-prisma.md +81 -0
- package/skills/forge/profiles/nestjs-mongodb.md +92 -0
- package/skills/forge/profiles/nestjs-postgres.md +98 -0
- package/skills/forge/reference/api-design.md +62 -0
- package/skills/forge/reference/assay.md +82 -0
- package/skills/forge/reference/cast.md +81 -7
- package/skills/forge/reference/data-patterns.md +86 -0
- package/skills/forge/reference/di-strategies.md +50 -0
- package/skills/forge/reference/errors.md +65 -0
- package/skills/forge/reference/events.md +95 -0
- package/skills/forge/reference/help.md +40 -0
- package/skills/forge/reference/hooks.md +62 -0
- package/skills/forge/reference/observability.md +66 -0
- package/skills/forge/reference/patterns.md +52 -0
- package/skills/forge/reference/principles.md +6 -0
- package/skills/forge/reference/reforge.md +69 -5
- package/skills/forge/reference/relocate.md +15 -2
- package/skills/forge/reference/security-patterns.md +87 -0
- package/skills/forge/reference/testing-patterns.md +69 -0
- package/skills/forge/scripts/assay.mjs +481 -0
- package/skills/forge/scripts/context.mjs +147 -43
- package/skills/forge/scripts/detect.mjs +371 -22
- package/skills/forge/scripts/forge-api.mjs +373 -0
- package/skills/forge/scripts/forge-config.mjs +268 -0
- package/skills/forge/scripts/forge-signals.mjs +131 -0
- package/skills/forge/scripts/forge-state.mjs +97 -0
- package/skills/forge/scripts/formatter.mjs +133 -0
- package/skills/forge/scripts/graph.mjs +5 -21
- package/skills/forge/scripts/hook.mjs +250 -0
- package/skills/forge/scripts/inspect.mjs +171 -22
- package/skills/forge/scripts/parse-imports.mjs +249 -0
- package/skills/forge/scripts/pin.mjs +151 -0
- package/skills/forge/scripts/posttool.mjs +224 -0
- package/skills/forge/scripts/profile.mjs +124 -20
- package/skills/forge/scripts/registry/rules.mjs +344 -0
- package/skills/forge/scripts/rename.mjs +669 -0
- package/skills/forge/scripts/rollback.mjs +213 -0
- package/skills/forge/scripts/update.mjs +114 -0
- package/skills/forge/templates/agents/claude/CLAUDE.md +98 -0
- package/skills/forge/templates/agents/cursor/.cursorrules +80 -0
- package/skills/forge/templates/feature/domain-error.ts.md +9 -0
- package/skills/forge/templates/feature/domain-event.ts.md +9 -0
- package/skills/forge/templates/feature/event-handler.ts.md +10 -0
- package/skills/forge/templates/feature/use-case.ts.md +10 -2
- package/skills/forge/tests/core.test.mjs +403 -0
- package/src/agents.mjs +55 -0
- package/src/cli.js +127 -54
- package/src/wizard.mjs +474 -0
- package/logo.png +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* rollback.mjs — Backup & rollback para relocate/reforge.
|
|
5
|
+
*
|
|
6
|
+
* Uso:
|
|
7
|
+
* node rollback.mjs backup <target> → backup feature/dir
|
|
8
|
+
* node rollback.mjs restore <backup-dir> → restaurar backup
|
|
9
|
+
* node rollback.mjs list [target] → listar backups
|
|
10
|
+
* node rollback.mjs verify <backup-dir> → diff + score check
|
|
11
|
+
*
|
|
12
|
+
* Funciones exportadas para uso desde SKILL.md / agente:
|
|
13
|
+
* createBackup(target) → { id, path, timestamp }
|
|
14
|
+
* restoreBackup(id) → boolean
|
|
15
|
+
* listBackups(target?) → BackupMeta[]
|
|
16
|
+
* verifyAfterChange() → { score, improved, suggestedCommit }
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, cpSync, rmSync, statSync } from "fs";
|
|
20
|
+
import { join, relative, resolve, basename } from "path";
|
|
21
|
+
import { execFileSync } from "child_process";
|
|
22
|
+
|
|
23
|
+
const ROOT = process.cwd();
|
|
24
|
+
const BACKUP_DIR = join(ROOT, ".forge", "backups");
|
|
25
|
+
const TIMESTAMP = () => new Date().toISOString().replace(/[:.]/g, "-");
|
|
26
|
+
|
|
27
|
+
function sanitize(name) {
|
|
28
|
+
return name.replace(/[^a-zA-Z0-9_\-/]/g, "_").replace(/\//g, "--");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shell(cmd, args, cwd = ROOT) {
|
|
32
|
+
try {
|
|
33
|
+
return execFileSync(cmd, args, { encoding: "utf-8", cwd }).trim();
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createBackup(target) {
|
|
40
|
+
const targetPath = resolve(ROOT, target);
|
|
41
|
+
if (!existsSync(targetPath)) {
|
|
42
|
+
console.error(`rollback: target no existe: ${target}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ts = TIMESTAMP();
|
|
47
|
+
const id = `${sanitize(target)}--${ts}`;
|
|
48
|
+
const dest = join(BACKUP_DIR, id);
|
|
49
|
+
|
|
50
|
+
mkdirSync(dest, { recursive: true });
|
|
51
|
+
const rel = relative(ROOT, targetPath);
|
|
52
|
+
|
|
53
|
+
// Try git stash push -k first (preserves working tree, stashes only target)
|
|
54
|
+
const gitRoot = shell("git", ["rev-parse", "--show-toplevel"]);
|
|
55
|
+
let method = "copy";
|
|
56
|
+
if (gitRoot) {
|
|
57
|
+
try {
|
|
58
|
+
shell("git", ["stash", "push", "-k", "--", target]);
|
|
59
|
+
method = "git-stash";
|
|
60
|
+
// Copy from git (unstashed) — actually stash pop restores
|
|
61
|
+
// For git mode, we save the diff instead
|
|
62
|
+
} catch {
|
|
63
|
+
// fall through to copy
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Copy files to backup dir
|
|
68
|
+
if (statSync(targetPath).isDirectory()) {
|
|
69
|
+
cpSync(targetPath, join(dest, basename(targetPath)), { recursive: true });
|
|
70
|
+
} else {
|
|
71
|
+
const parent = join(dest, rel.replace(/\//g, "--"));
|
|
72
|
+
mkdirSync(parent, { recursive: true });
|
|
73
|
+
cpSync(targetPath, join(parent, basename(targetPath)));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Save metadata
|
|
77
|
+
const meta = {
|
|
78
|
+
id,
|
|
79
|
+
target: rel,
|
|
80
|
+
timestamp: new Date().toISOString(),
|
|
81
|
+
method,
|
|
82
|
+
files: collectFiles(targetPath),
|
|
83
|
+
};
|
|
84
|
+
writeFileSync(join(dest, "backup.json"), JSON.stringify(meta, null, 2) + "\n", "utf-8");
|
|
85
|
+
|
|
86
|
+
console.log(`rollback: backup creado → ${rel} (${id})`);
|
|
87
|
+
return meta;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function collectFiles(targetPath) {
|
|
91
|
+
if (!statSync(targetPath).isDirectory()) return [targetPath];
|
|
92
|
+
const files = [];
|
|
93
|
+
function walk(dir) {
|
|
94
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
95
|
+
const full = join(dir, entry.name);
|
|
96
|
+
if (entry.isDirectory()) walk(full);
|
|
97
|
+
else files.push(full);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
walk(targetPath);
|
|
101
|
+
return files;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function restoreBackup(id) {
|
|
105
|
+
const backupPath = join(BACKUP_DIR, id);
|
|
106
|
+
if (!existsSync(backupPath)) {
|
|
107
|
+
console.error(`rollback: backup no encontrado: ${id}`);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const metaPath = join(backupPath, "backup.json");
|
|
112
|
+
if (!existsSync(metaPath)) {
|
|
113
|
+
console.error(`rollback: metadatos no encontrados en ${id}`);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
118
|
+
const targetPath = resolve(ROOT, meta.target);
|
|
119
|
+
|
|
120
|
+
// Restore files
|
|
121
|
+
if (existsSync(targetPath)) {
|
|
122
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const backupData = join(backupPath, basename(targetPath));
|
|
126
|
+
if (existsSync(backupData)) {
|
|
127
|
+
cpSync(backupData, targetPath, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(`rollback: restaurado → ${meta.target} (from ${id})`);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function listBackups(target) {
|
|
135
|
+
if (!existsSync(BACKUP_DIR)) return [];
|
|
136
|
+
|
|
137
|
+
const entries = readdirSync(BACKUP_DIR, { withFileTypes: true });
|
|
138
|
+
const backups = [];
|
|
139
|
+
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
if (!entry.isDirectory()) continue;
|
|
142
|
+
const metaPath = join(BACKUP_DIR, entry.name, "backup.json");
|
|
143
|
+
if (!existsSync(metaPath)) continue;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
147
|
+
if (!target || meta.target.includes(target)) {
|
|
148
|
+
backups.push(meta);
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
// skip invalid
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function verifyAfterChange() {
|
|
159
|
+
try {
|
|
160
|
+
const out = shell("node", [join(import.meta.url, "..", "inspect.mjs"), "--diff", "--json"]);
|
|
161
|
+
if (!out) return { score: 0, improved: false, error: "inspect falló" };
|
|
162
|
+
|
|
163
|
+
const result = JSON.parse(out);
|
|
164
|
+
const score = result.total || result.diff?.totalScore || 0;
|
|
165
|
+
const improved = result.diff?.improved !== false;
|
|
166
|
+
|
|
167
|
+
let suggestedCommit = null;
|
|
168
|
+
if (improved && score > 0) {
|
|
169
|
+
const branch = shell("git", ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
170
|
+
suggestedCommit = `git add -A && git commit -m "forge: relocate/reforge (score: ${score}/110)"`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { score, improved, suggestedCommit };
|
|
174
|
+
} catch {
|
|
175
|
+
return { score: 0, improved: false, error: "verify falló" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ── Export for agent use ──
|
|
180
|
+
export const rollback = { createBackup, restoreBackup, listBackups, verifyAfterChange };
|
|
181
|
+
|
|
182
|
+
// ── CLI ──
|
|
183
|
+
const [,, action, arg] = process.argv;
|
|
184
|
+
|
|
185
|
+
if (action === "backup" && arg) {
|
|
186
|
+
process.exit(createBackup(arg) ? 0 : 1);
|
|
187
|
+
} else if (action === "restore" && arg) {
|
|
188
|
+
process.exit(restoreBackup(arg) ? 0 : 1);
|
|
189
|
+
} else if (action === "list") {
|
|
190
|
+
const backups = listBackups(arg);
|
|
191
|
+
if (backups.length === 0) {
|
|
192
|
+
console.log("No hay backups disponibles.");
|
|
193
|
+
} else {
|
|
194
|
+
console.log("\n── Backups ──\n");
|
|
195
|
+
for (const b of backups) {
|
|
196
|
+
const date = b.timestamp.slice(0, 19).replace("T", " ");
|
|
197
|
+
console.log(` ${b.id.padEnd(50)} ${date} (${b.files?.length || 0} archivos)`);
|
|
198
|
+
}
|
|
199
|
+
console.log();
|
|
200
|
+
}
|
|
201
|
+
process.exit(0);
|
|
202
|
+
} else if (action === "verify") {
|
|
203
|
+
const result = verifyAfterChange();
|
|
204
|
+
if (result.error) {
|
|
205
|
+
console.error(`rollback: ${result.error}`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
console.log(`Score: ${result.score}/110 | ${result.improved ? "✓ Mejoró/igual" : "✘ Empeoró"}${result.suggestedCommit ? `\nSugerencia: ${result.suggestedCommit}` : ""}`);
|
|
209
|
+
process.exit(result.improved ? 0 : 1);
|
|
210
|
+
} else {
|
|
211
|
+
console.log("Uso: node rollback.mjs <backup|restore|list|verify> [target|id]");
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* forge update — Version check contra forge.dev/latest.
|
|
5
|
+
* Cache de 24h en .forge/version-cache.json.
|
|
6
|
+
* No auto-actualiza; solo notifica.
|
|
7
|
+
*
|
|
8
|
+
* Uso:
|
|
9
|
+
* node update.mjs
|
|
10
|
+
* node update.mjs --json
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
|
|
16
|
+
const ROOT = process.cwd();
|
|
17
|
+
const CACHE_PATH = join(ROOT, ".forge", "version-cache.json");
|
|
18
|
+
const CACHE_TTL = 86400000; // 24h
|
|
19
|
+
const VERSION_URL = "https://forge.dev/latest";
|
|
20
|
+
const CURRENT_VERSION = "1.0.2";
|
|
21
|
+
|
|
22
|
+
function readJson(path) {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeJson(path, data) {
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(join(path, ".."), { recursive: true });
|
|
33
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchLatestVersion() {
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(VERSION_URL, { signal: AbortSignal.timeout(5000) });
|
|
43
|
+
if (!response.ok) return null;
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
return data.version || data.latest || null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compareVersions(a, b) {
|
|
52
|
+
const pa = a.split(".").map(Number);
|
|
53
|
+
const pb = b.split(".").map(Number);
|
|
54
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
55
|
+
const na = pa[i] || 0;
|
|
56
|
+
const nb = pb[i] || 0;
|
|
57
|
+
if (na > nb) return 1;
|
|
58
|
+
if (na < nb) return -1;
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
const isJson = process.argv.includes("--json");
|
|
65
|
+
|
|
66
|
+
// Check cache
|
|
67
|
+
const cached = readJson(CACHE_PATH);
|
|
68
|
+
if (cached && cached.timestamp && (Date.now() - cached.timestamp) < CACHE_TTL) {
|
|
69
|
+
const cmp = compareVersions(cached.latest, CURRENT_VERSION);
|
|
70
|
+
const updateAvailable = cmp > 0;
|
|
71
|
+
|
|
72
|
+
if (isJson) {
|
|
73
|
+
console.log(JSON.stringify({ current: CURRENT_VERSION, latest: cached.latest, updateAvailable, changelog: cached.changelog || null, cached: true }, null, 2));
|
|
74
|
+
} else if (updateAvailable) {
|
|
75
|
+
console.log(`📦 Forge ${cached.latest} disponible (tienes ${CURRENT_VERSION}).`);
|
|
76
|
+
if (cached.changelog) console.log(` Changelog: ${cached.changelog}`);
|
|
77
|
+
console.log(" Para actualizar: npm install -g @ronaldjdevfs/forge");
|
|
78
|
+
} else {
|
|
79
|
+
console.log(`Forge ${CURRENT_VERSION} — última versión.`);
|
|
80
|
+
}
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fetch from network
|
|
85
|
+
const latest = await fetchLatestVersion();
|
|
86
|
+
if (!latest) {
|
|
87
|
+
if (isJson) {
|
|
88
|
+
console.log(JSON.stringify({ current: CURRENT_VERSION, latest: null, error: "No se pudo conectar con forge.dev/latest" }, null, 2));
|
|
89
|
+
} else {
|
|
90
|
+
console.log("No se pudo verificar versión. Revisa tu conexión.");
|
|
91
|
+
}
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Update cache
|
|
96
|
+
writeJson(CACHE_PATH, { latest, timestamp: Date.now(), changelog: null });
|
|
97
|
+
|
|
98
|
+
const cmp = compareVersions(latest, CURRENT_VERSION);
|
|
99
|
+
const updateAvailable = cmp > 0;
|
|
100
|
+
|
|
101
|
+
if (isJson) {
|
|
102
|
+
console.log(JSON.stringify({ current: CURRENT_VERSION, latest, updateAvailable }, null, 2));
|
|
103
|
+
} else if (updateAvailable) {
|
|
104
|
+
console.log(`📦 Forge ${latest} disponible (tienes ${CURRENT_VERSION}).`);
|
|
105
|
+
console.log(" Para actualizar: npm install -g @ronaldjdevfs/forge");
|
|
106
|
+
} else {
|
|
107
|
+
console.log(`Forge ${CURRENT_VERSION} — última versión.`);
|
|
108
|
+
}
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (process.argv[1] && (process.argv[1].endsWith("update.mjs") || process.argv[1].endsWith("update.js"))) {
|
|
113
|
+
main().catch(console.error);
|
|
114
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Forge — Backend Architecture Operating System
|
|
2
|
+
|
|
3
|
+
Forge es un sistema operativo arquitectónico para backend. Diseña, construye, audita y evoluciona arquitecturas basadas en **Arquitectura Hexagonal**, **DDD pragmático** y **vertical slices**.
|
|
4
|
+
|
|
5
|
+
## Boot Sequence
|
|
6
|
+
|
|
7
|
+
Ejecutar en orden antes de cualquier acción:
|
|
8
|
+
|
|
9
|
+
1. `node .opencode/skills/forge/scripts/context.mjs` — stack + layers detection
|
|
10
|
+
2. `node .opencode/skills/forge/scripts/armorer.mjs` — ownership, orphans, duplicates
|
|
11
|
+
3. `node .opencode/skills/forge/scripts/profile.mjs --extended` — tech profile
|
|
12
|
+
4. `node .opencode/skills/forge/scripts/graph.mjs --json` — 4-layer graph
|
|
13
|
+
5. `node .opencode/skills/forge/scripts/chain.mjs --json` — dependency analysis
|
|
14
|
+
6. `node .opencode/skills/forge/scripts/inspect.mjs --json` — full audit
|
|
15
|
+
7. `node .opencode/skills/forge/scripts/architecture.mjs` — update ARCHITECTURE.md
|
|
16
|
+
8. Execute user command (cast, quench, relocate, etc.)
|
|
17
|
+
9. Run architecture.mjs again
|
|
18
|
+
|
|
19
|
+
## Architecture Model
|
|
20
|
+
|
|
21
|
+
Cuatro capas obligatorias:
|
|
22
|
+
|
|
23
|
+
| Layer | Propósito |
|
|
24
|
+
|-------|-----------|
|
|
25
|
+
| `src/platform/` | config, database, http, server, logger, cache, security, events, di |
|
|
26
|
+
| `src/features/<name>/` | domain/, application/use-cases/, application/mappers/, adapters/in/http/, adapters/out/persistence/ |
|
|
27
|
+
| `src/shared/` | errors/, contracts/, types/, utils/ (puro, sin lógica de negocio) |
|
|
28
|
+
| `src/infra/` | prisma/, mongodb/, redis/, mail/ (implementaciones, sin reglas de negocio) |
|
|
29
|
+
|
|
30
|
+
### Dependency Rules
|
|
31
|
+
|
|
32
|
+
**Permitido:** `feature → platform`, `feature → shared`, `platform → infra`, `adapter → infra`, `feature → domain`
|
|
33
|
+
|
|
34
|
+
**Prohibido (CRITICAL):**
|
|
35
|
+
- R1: `feature → infra`
|
|
36
|
+
- R2: `platform → feature`
|
|
37
|
+
- R3: `shared → feature`
|
|
38
|
+
- R4: `shared → infra`
|
|
39
|
+
- R5: `domain → infra`
|
|
40
|
+
- R6: `domain → platform`
|
|
41
|
+
- R7: `infra → feature`
|
|
42
|
+
- R8: cross-feature direct imports
|
|
43
|
+
- R9: cycles
|
|
44
|
+
|
|
45
|
+
## Naming Conventions
|
|
46
|
+
|
|
47
|
+
| Elemento | Formato |
|
|
48
|
+
|----------|---------|
|
|
49
|
+
| Directorios | `kebab-case/` |
|
|
50
|
+
| Archivos | `<PascalCase>.<artefacto>.ts` |
|
|
51
|
+
| Interfaces | `I<PascalCase>.<artefacto>.ts` |
|
|
52
|
+
| Use cases | `<Action>.uc.ts` |
|
|
53
|
+
| Clases | `PascalCase` |
|
|
54
|
+
| Funciones/variables | `camelCase` |
|
|
55
|
+
| Constantes | `UPPER_SNAKE_CASE` |
|
|
56
|
+
| Barrel files | `index.ts` con named exports, no `export default` |
|
|
57
|
+
| Imports ESM | con extensión `.js`: `import { X } from "./foo.js"` |
|
|
58
|
+
|
|
59
|
+
## Commands
|
|
60
|
+
|
|
61
|
+
| Comando | Acción |
|
|
62
|
+
|---------|--------|
|
|
63
|
+
| `forge` | Init project (context + bootstrap + profile + graph) |
|
|
64
|
+
| `cast` | New feature (verifica platform/shared/infra primero) |
|
|
65
|
+
| `inspect` | Full audit (6 categorías, 110pts → 0-100) |
|
|
66
|
+
| `quench` | Validate dependency rules |
|
|
67
|
+
| `chain` | Topological dependency sort |
|
|
68
|
+
| `graph` | Architecture graph con R1-R9 |
|
|
69
|
+
| `armorer` | Ownership report (orphans, duplicates, misplaced) |
|
|
70
|
+
| `smelt` | Extract code to shared/ |
|
|
71
|
+
| `relocate` | Migrate legacy to platform/, shared/, infra/ or features/ |
|
|
72
|
+
| `reforge` | Refactor considering all 4 layers |
|
|
73
|
+
| `temper` | Harden DI (constructor injection) |
|
|
74
|
+
| `inscribe` | Generate ARCHITECTURE.md |
|
|
75
|
+
|
|
76
|
+
## Architecture Principles
|
|
77
|
+
|
|
78
|
+
1. **Hexagonal basado en features** — unidad de organización = feature, no capa técnica
|
|
79
|
+
2. **DDD ligero** — sin sobreingeniería, pragmatismo sobre dogma
|
|
80
|
+
3. **Separación dominio e infraestructura** — domain/ no sabe de frameworks ni BD
|
|
81
|
+
4. **Feature autónomo** — todo lo del dominio vive dentro de `features/<name>/`
|
|
82
|
+
5. **Dependencias unidireccionales** — `adapters → application → domain → (nada)`
|
|
83
|
+
6. **Cero lógica en controllers** — parsean, delegan, responden
|
|
84
|
+
7. **Cero BD fuera de repositories** — única puerta a datos
|
|
85
|
+
8. **DI disciplinada** — constructor injection, sin service locators
|
|
86
|
+
9. **Errores tipados** — clases explícitas, no `throw Error()`
|
|
87
|
+
10. **Grafo arquitectónico vivo** — todo componente es un nodo, toda relación un edge validado
|
|
88
|
+
|
|
89
|
+
## Key Files
|
|
90
|
+
|
|
91
|
+
- `skills/forge/SKILL.md` — orchestrator principal
|
|
92
|
+
- `skills/forge/reference/principles.md` — manifiesto y 15 principios
|
|
93
|
+
- `skills/forge/reference/patterns.md` — naming conventions
|
|
94
|
+
- `skills/forge/scripts/` — context, detect, inspect, chain, profile, graph, architecture, armorer, bootstrap
|
|
95
|
+
- `skills/forge/profiles/` — 10 tech profiles (express, fastify, nestjs × mongodb, postgres, prisma, drizzle)
|
|
96
|
+
- `skills/forge/templates/` — templates para feature, platform, shared, infra
|
|
97
|
+
- `AGENTS.md` — guía para agentes de IA
|
|
98
|
+
- `ARCHITECTURE.md` — estado actual de la arquitectura
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
You are an AI assistant for a project that uses Forge — a Backend Architecture Operating System. Tu función es ayudar a diseñar, construir, auditar y evolucionar la arquitectura usando Arquitectura Hexagonal, DDD pragmático y vertical slices.
|
|
2
|
+
|
|
3
|
+
## Architecture Model
|
|
4
|
+
|
|
5
|
+
The project uses four mandatory layers:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
src/
|
|
9
|
+
├── platform/ # Config, database, http, server, logger, cache, security, events, di
|
|
10
|
+
├── features/<name>/ # domain/, application/use-cases/, application/mappers/, adapters/in/http/, adapters/out/persistence/
|
|
11
|
+
├── shared/ # errors/, contracts/, types/, utils/ (pure code, no business logic)
|
|
12
|
+
└── infra/ # prisma/, mongodb/, redis/, mail/ (implementations, no business rules)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Dependency Rules (CRITICAL — never violate)
|
|
16
|
+
|
|
17
|
+
**Allowed:** `feature → platform`, `feature → shared`, `platform → infra`, `adapter → infra`, `feature → domain`
|
|
18
|
+
|
|
19
|
+
**Prohibited:**
|
|
20
|
+
- R1: `feature → infra`
|
|
21
|
+
- R2: `platform → feature`
|
|
22
|
+
- R3: `shared → feature`
|
|
23
|
+
- R4: `shared → infra`
|
|
24
|
+
- R5: `domain → infra`
|
|
25
|
+
- R6: `domain → platform`
|
|
26
|
+
- R7: `infra → feature`
|
|
27
|
+
- R8: cross-feature direct imports
|
|
28
|
+
- R9: cycles
|
|
29
|
+
|
|
30
|
+
## Naming Conventions
|
|
31
|
+
|
|
32
|
+
| Element | Convention | Example |
|
|
33
|
+
|---------|-----------|---------|
|
|
34
|
+
| Directories | `kebab-case/` | `credit-card/` |
|
|
35
|
+
| Files | `<PascalCase>.<artifact>.ts` | `User.entity.ts` |
|
|
36
|
+
| Interfaces | `I<PascalCase>.<artifact>.ts` | `IUser.repository.ts` |
|
|
37
|
+
| Use cases | `<Action>.uc.ts` | `CreateUser.uc.ts` |
|
|
38
|
+
| Classes | `PascalCase` | `UserController` |
|
|
39
|
+
| Functions/vars | `camelCase` | `formatDate` |
|
|
40
|
+
| Constants | `UPPER_SNAKE_CASE` | `MAX_RETRY_COUNT` |
|
|
41
|
+
| Barrels | `index.ts` | named exports only, no `export default` |
|
|
42
|
+
| ESM imports | with `.js` extension | `import { X } from "./foo.js"` |
|
|
43
|
+
|
|
44
|
+
## Code Principles
|
|
45
|
+
|
|
46
|
+
1. **Zero business logic in controllers** — parse, delegate, respond
|
|
47
|
+
2. **Zero DB access outside repositories** — only repositories touch the database
|
|
48
|
+
3. **Constructor injection only** — no service locators, no `container.resolve()` in business logic
|
|
49
|
+
4. **Typed domain errors** — explicit error classes, never generic `throw Error()`
|
|
50
|
+
5. **Unidirectional dependencies** — `adapters → application → domain → (nothing)`
|
|
51
|
+
6. **Domain knows nothing about infrastructure** — no framework, database, or external service imports in domain/
|
|
52
|
+
7. **Features are autonomous** — no direct imports between features, only via injected interfaces
|
|
53
|
+
8. **Explicit over magic** — prefer explicit imports and declared types over hidden decorators
|
|
54
|
+
|
|
55
|
+
## Available Commands
|
|
56
|
+
|
|
57
|
+
| Command | Action |
|
|
58
|
+
|---------|--------|
|
|
59
|
+
| `forge` | Initialize project |
|
|
60
|
+
| `cast` | Create new feature |
|
|
61
|
+
| `inspect` | Full architecture audit |
|
|
62
|
+
| `quench` | Validate dependency rules |
|
|
63
|
+
| `chain` | Dependency graph (topological) |
|
|
64
|
+
| `graph` | Architecture graph with rules |
|
|
65
|
+
| `armorer` | Ownership report |
|
|
66
|
+
| `smelt` | Extract to shared/ |
|
|
67
|
+
| `relocate` | Migrate legacy code |
|
|
68
|
+
| `reforge` | Refactor architecture |
|
|
69
|
+
| `temper` | Harden dependency injection |
|
|
70
|
+
| `inscribe` | Generate ARCHITECTURE.md |
|
|
71
|
+
|
|
72
|
+
## Key Files Reference
|
|
73
|
+
|
|
74
|
+
- `skills/forge/SKILL.md` — orchestrator
|
|
75
|
+
- `skills/forge/reference/principles.md` — 15 principles
|
|
76
|
+
- `skills/forge/reference/patterns.md` — naming conventions
|
|
77
|
+
- `skills/forge/scripts/` — all engine scripts
|
|
78
|
+
- `skills/forge/profiles/` — tech profiles
|
|
79
|
+
- `AGENTS.md` — agent guide
|
|
80
|
+
- `ARCHITECTURE.md` — current architecture state
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
```typescript
|
|
2
|
+
// src/features/<domain>/adapters/out/events/<Domain>Created.handler.ts
|
|
3
|
+
import type { <Domain>CreatedEvent } from "../../domain/events/<Domain>Created.event.js";
|
|
4
|
+
|
|
5
|
+
export class <Domain>CreatedHandler {
|
|
6
|
+
async handle(event: <Domain>CreatedEvent): Promise<void> {
|
|
7
|
+
// Reaccionar al evento (ej: enviar email, notificar, auditar)
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
```
|
|
@@ -4,16 +4,24 @@ import { injectable, inject } from "tsyringe";
|
|
|
4
4
|
import type { <Domain> } from "../../domain/<Domain>.entity.js";
|
|
5
5
|
import type { I<Domain>Repository } from "../../domain/I<Domain>.repository.js";
|
|
6
6
|
import { UseCaseError } from "@/shared/errors/UseCaseError.js";
|
|
7
|
+
// Opcional: errores de dominio y eventos
|
|
8
|
+
// import { <Domain>NotFoundError } from "../../domain/errors/<Domain>NotFound.error.js";
|
|
9
|
+
// import type { IEventBus } from "@/platform/events/IEventBus.js";
|
|
10
|
+
// import { <Domain>CreatedEvent } from "../../domain/events/<Domain>Created.event.js";
|
|
7
11
|
|
|
8
12
|
@injectable()
|
|
9
13
|
export class Create<Domain> {
|
|
10
14
|
constructor(
|
|
11
|
-
@inject(I<Domain>Repository) private readonly repo: I<Domain>Repository
|
|
15
|
+
@inject(I<Domain>Repository) private readonly repo: I<Domain>Repository,
|
|
16
|
+
// @inject(IEventBus) private readonly eventBus: IEventBus,
|
|
12
17
|
) {}
|
|
13
18
|
|
|
14
19
|
async execute(data: Partial<<Domain>>): Promise<<Domain>> {
|
|
15
20
|
if (!data) throw new UseCaseError("Datos requeridos");
|
|
16
|
-
|
|
21
|
+
const created = await this.repo.create(data);
|
|
22
|
+
// Opcional: emitir evento de dominio
|
|
23
|
+
// this.eventBus.publish(new <Domain>CreatedEvent(created.id));
|
|
24
|
+
return created;
|
|
17
25
|
}
|
|
18
26
|
}
|
|
19
27
|
```
|