@ronaldjdevfs/forge 1.0.2 → 1.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.
Files changed (48) hide show
  1. package/README.md +130 -8
  2. package/package.json +2 -2
  3. package/skills/forge/SKILL.md +86 -15
  4. package/skills/forge/profiles/express-drizzle.md +107 -0
  5. package/skills/forge/profiles/fastify-mongodb.md +103 -0
  6. package/skills/forge/profiles/fastify-prisma.md +81 -0
  7. package/skills/forge/profiles/nestjs-mongodb.md +92 -0
  8. package/skills/forge/profiles/nestjs-postgres.md +98 -0
  9. package/skills/forge/reference/api-design.md +62 -0
  10. package/skills/forge/reference/assay.md +82 -0
  11. package/skills/forge/reference/cast.md +81 -7
  12. package/skills/forge/reference/data-patterns.md +86 -0
  13. package/skills/forge/reference/di-strategies.md +50 -0
  14. package/skills/forge/reference/errors.md +65 -0
  15. package/skills/forge/reference/events.md +95 -0
  16. package/skills/forge/reference/help.md +40 -0
  17. package/skills/forge/reference/hooks.md +62 -0
  18. package/skills/forge/reference/observability.md +66 -0
  19. package/skills/forge/reference/patterns.md +52 -0
  20. package/skills/forge/reference/principles.md +6 -0
  21. package/skills/forge/reference/reforge.md +69 -5
  22. package/skills/forge/reference/relocate.md +15 -2
  23. package/skills/forge/reference/security-patterns.md +87 -0
  24. package/skills/forge/reference/testing-patterns.md +69 -0
  25. package/skills/forge/scripts/assay.mjs +481 -0
  26. package/skills/forge/scripts/context.mjs +147 -43
  27. package/skills/forge/scripts/detect.mjs +371 -22
  28. package/skills/forge/scripts/forge-api.mjs +373 -0
  29. package/skills/forge/scripts/forge-config.mjs +268 -0
  30. package/skills/forge/scripts/forge-signals.mjs +131 -0
  31. package/skills/forge/scripts/forge-state.mjs +97 -0
  32. package/skills/forge/scripts/formatter.mjs +133 -0
  33. package/skills/forge/scripts/graph.mjs +5 -21
  34. package/skills/forge/scripts/hook.mjs +250 -0
  35. package/skills/forge/scripts/inspect.mjs +171 -22
  36. package/skills/forge/scripts/parse-imports.mjs +249 -0
  37. package/skills/forge/scripts/pin.mjs +151 -0
  38. package/skills/forge/scripts/posttool.mjs +224 -0
  39. package/skills/forge/scripts/profile.mjs +124 -20
  40. package/skills/forge/scripts/registry/rules.mjs +344 -0
  41. package/skills/forge/scripts/rename.mjs +669 -0
  42. package/skills/forge/scripts/rollback.mjs +213 -0
  43. package/skills/forge/scripts/update.mjs +114 -0
  44. package/skills/forge/templates/feature/domain-error.ts.md +9 -0
  45. package/skills/forge/templates/feature/domain-event.ts.md +9 -0
  46. package/skills/forge/templates/feature/event-handler.ts.md +10 -0
  47. package/skills/forge/templates/feature/use-case.ts.md +10 -2
  48. package/skills/forge/tests/core.test.mjs +403 -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,9 @@
1
+ ```typescript
2
+ // src/features/<domain>/domain/errors/<Domain>NotFound.error.ts
3
+ export class <Domain>NotFoundError extends Error {
4
+ constructor(id: string) {
5
+ super(`<Domain> with id ${id} not found`);
6
+ this.name = "<Domain>NotFoundError";
7
+ }
8
+ }
9
+ ```
@@ -0,0 +1,9 @@
1
+ ```typescript
2
+ // src/features/<domain>/domain/events/<Domain>Created.event.ts
3
+ export class <Domain>CreatedEvent {
4
+ constructor(
5
+ public readonly <domain>Id: string,
6
+ public readonly occurredAt: Date = new Date(),
7
+ ) {}
8
+ }
9
+ ```
@@ -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
- return this.repo.create(data);
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
  ```