@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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { readFileSync, existsSync, readdirSync, statSync } from "fs";
4
- import { join, basename } from "path";
4
+ import { join, basename, resolve } from "path";
5
5
  import { buildGraph } from "./graph.mjs";
6
6
  import { buildOwnershipReport } from "./armorer.mjs";
7
7
 
@@ -9,9 +9,9 @@ const ROOT = process.cwd();
9
9
  const SRC = join(ROOT, "src");
10
10
  const FEATURES = join(SRC, "features");
11
11
 
12
- const PLATFORM_KNOWN = ["config", "database", "http", "server", "logger", "cache", "security", "events", "scheduler", "observability", "di"];
13
- const SHARED_KNOWN = ["errors", "contracts", "types", "utils", "helpers", "constants", "enums"];
14
- const INFRA_KNOWN = ["prisma", "mongodb", "postgres", "redis", "mail", "s3", "cloudinary", "stripe", "sqs", "rabbitmq", "kafka", "smtp"];
12
+ export const PLATFORM_KNOWN = ["config", "database", "http", "server", "logger", "cache", "security", "events", "scheduler", "observability", "di"];
13
+ export const SHARED_KNOWN = ["errors", "contracts", "types", "utils", "helpers", "constants", "enums"];
14
+ export const INFRA_KNOWN = ["prisma", "mongodb", "postgres", "redis", "mail", "s3", "cloudinary", "stripe", "sqs", "rabbitmq", "kafka", "smtp"];
15
15
 
16
16
  function read(path) {
17
17
  try {
@@ -101,38 +101,10 @@ function detectLegacyFeatures() {
101
101
  return legacy;
102
102
  }
103
103
 
104
- function detectPlatform() {
105
- const platformDir = join(SRC, "platform");
106
- if (!isDir(platformDir)) return { components: [], exists: false };
107
- const components = listDir(platformDir).filter((d) => isDir(join(platformDir, d)) || d.endsWith(".ts") || d.endsWith(".js"));
108
- const known = components.filter((c) => PLATFORM_KNOWN.includes(c.replace(/\.(ts|js)$/, "")));
109
- const unknown = components.filter((c) => !PLATFORM_KNOWN.includes(c.replace(/\.(ts|js)$/, "")));
110
- return { components, exists: true, known, unknown };
111
- }
112
-
113
- function detectShared() {
114
- const sharedDir = join(SRC, "shared");
115
- if (!isDir(sharedDir)) return { components: [], exists: false };
116
- const components = listDir(sharedDir).filter((d) => isDir(join(sharedDir, d)) || d.endsWith(".ts") || d.endsWith(".js"));
117
- const known = components.filter((c) => SHARED_KNOWN.includes(c.replace(/\.(ts|js)$/, "")));
118
- const unknown = components.filter((c) => !SHARED_KNOWN.includes(c.replace(/\.(ts|js)$/, "")));
119
- return { components, exists: true, known, unknown };
120
- }
121
-
122
- function detectInfra() {
123
- const infraDir = join(SRC, "infra");
124
- const infraDir2 = join(SRC, "infrastructure");
125
- const dir = isDir(infraDir) ? infraDir : isDir(infraDir2) ? infraDir2 : null;
126
- if (!dir) return { components: [], exists: false };
127
- const components = listDir(dir).filter((d) => isDir(join(dir, d)) || d.endsWith(".ts") || d.endsWith(".js"));
128
- const known = components.filter((c) => INFRA_KNOWN.includes(c.replace(/\.(ts|js)$/, "")));
129
- const unknown = components.filter((c) => !INFRA_KNOWN.includes(c.replace(/\.(ts|js)$/, "")));
130
- return { components, exists: true, known, unknown };
131
- }
132
-
133
- function detectOrphans() {
134
- const report = buildOwnershipReport();
135
- return report.orphans;
104
+ function classifyComponents(components, knownList) {
105
+ const known = components.filter((c) => knownList.includes(c.replace(/\.(ts|js)$/, "")));
106
+ const unknown = components.filter((c) => !knownList.includes(c.replace(/\.(ts|js)$/, "")));
107
+ return { components, exists: components.length > 0, known, unknown };
136
108
  }
137
109
 
138
110
  function stripJsonComments(raw) {
@@ -163,8 +135,129 @@ function readJson(path) {
163
135
  }
164
136
  }
165
137
 
166
- export async function buildContext(projectRoot = ROOT) {
167
- const root = projectRoot;
138
+ // ── Monorepo detection (D2) ──
139
+
140
+ function resolveGlob(pattern, base) {
141
+ // Simple glob: supports * (single dir) and ** (recursive)
142
+ // Not full glob but enough for pnpm-workspace.yaml patterns like "packages/*"
143
+ if (!pattern) return [];
144
+ const starStar = pattern.includes("**");
145
+ const star = pattern.includes("*") && !starStar;
146
+ const baseDir = join(base, pattern.split("*")[0].replace(/\/+$/, ""));
147
+ if (!existsSync(baseDir)) return [];
148
+
149
+ const results = [];
150
+ if (star) {
151
+ for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
152
+ if (entry.isDirectory()) {
153
+ const pkgPath = join(baseDir, entry.name);
154
+ if (existsSync(join(pkgPath, "package.json"))) results.push(pkgPath);
155
+ }
156
+ }
157
+ } else if (starStar) {
158
+ function walk(dir) {
159
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
160
+ if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules") {
161
+ const sub = join(dir, entry.name);
162
+ if (existsSync(join(sub, "package.json"))) results.push(sub);
163
+ walk(sub);
164
+ }
165
+ }
166
+ }
167
+ walk(baseDir);
168
+ }
169
+ return results;
170
+ }
171
+
172
+ export function detectMonorepo(projectRoot = ROOT) {
173
+ const configs = [];
174
+
175
+ // pnpm-workspace.yaml — must have packages: key
176
+ const pnpmWorkspace = join(projectRoot, "pnpm-workspace.yaml");
177
+ if (existsSync(pnpmWorkspace)) {
178
+ const content = readFileSync(pnpmWorkspace, "utf-8");
179
+ if (content.includes("packages:")) {
180
+ configs.push({ type: "pnpm", file: "pnpm-workspace.yaml" });
181
+ }
182
+ }
183
+
184
+ // turbo.json
185
+ if (existsSync(join(projectRoot, "turbo.json"))) {
186
+ configs.push({ type: "turbo", file: "turbo.json" });
187
+ }
188
+
189
+ // nx.json
190
+ if (existsSync(join(projectRoot, "nx.json"))) {
191
+ configs.push({ type: "nx", file: "nx.json" });
192
+ }
193
+
194
+ // package.json workspaces
195
+ const pkg = readJson(join(projectRoot, "package.json"));
196
+ if (pkg?.workspaces) {
197
+ const patterns = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
198
+ if (patterns.length > 0) {
199
+ configs.push({ type: "npm", file: "package.json" });
200
+ }
201
+ }
202
+
203
+ // Lerna
204
+ if (existsSync(join(projectRoot, "lerna.json"))) {
205
+ configs.push({ type: "lerna", file: "lerna.json" });
206
+ }
207
+
208
+ return configs;
209
+ }
210
+
211
+ export function detectWorkspaces(projectRoot = ROOT) {
212
+ const workspaces = [];
213
+ const pkg = readJson(join(projectRoot, "package.json"));
214
+
215
+ // 1. pnpm-workspace.yaml
216
+ const pnpmYaml = join(projectRoot, "pnpm-workspace.yaml");
217
+ if (existsSync(pnpmYaml)) {
218
+ const content = readFileSync(pnpmYaml, "utf-8");
219
+ const lines = content.split("\n");
220
+ let inPackages = false;
221
+ for (let line of lines) {
222
+ line = line.trim();
223
+ if (line.startsWith("packages:")) { inPackages = true; continue; }
224
+ if (inPackages && line.startsWith("-")) {
225
+ const pattern = line.replace(/^-\s*['"]?|['"]?\s*$/g, "").trim();
226
+ if (pattern) {
227
+ const resolved = resolveGlob(pattern, projectRoot);
228
+ workspaces.push(...resolved);
229
+ }
230
+ }
231
+ if (inPackages && line.startsWith("#")) continue;
232
+ if (inPackages && !line.startsWith("-") && line !== "") inPackages = false;
233
+ }
234
+ }
235
+
236
+ // 2. package.json workspaces
237
+ if (pkg?.workspaces) {
238
+ const patterns = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces.packages || [];
239
+ for (const pattern of patterns) {
240
+ const resolved = resolveGlob(pattern, projectRoot);
241
+ workspaces.push(...resolved);
242
+ }
243
+ }
244
+
245
+ // Deduplicate
246
+ const seen = new Set();
247
+ return workspaces.filter(w => {
248
+ if (seen.has(w)) return false;
249
+ seen.add(w);
250
+ return true;
251
+ }).map(w => ({
252
+ path: w,
253
+ name: basename(w),
254
+ hasSrc: existsSync(join(w, "src")),
255
+ hasFeatures: existsSync(join(w, "src", "features")),
256
+ }));
257
+ }
258
+
259
+ export async function buildContext(projectRoot = ROOT, workspaceScope = null) {
260
+ const root = workspaceScope || projectRoot;
168
261
  const src = join(root, "src");
169
262
 
170
263
  const pkg = readJson(join(root, "package.json")) || {};
@@ -182,18 +275,25 @@ export async function buildContext(projectRoot = ROOT) {
182
275
 
183
276
  const migratedFeatures = detectFeatures();
184
277
  const legacyFeatures = detectLegacyFeatures();
185
- const platform = detectPlatform();
186
- const shared = detectShared();
187
- const infra = detectInfra();
188
- const orphans = detectOrphans();
189
- const graph = buildGraph(root);
190
278
  const ownership = buildOwnershipReport(root);
279
+ const platform = classifyComponents(ownership.ownership.platform, PLATFORM_KNOWN);
280
+ const shared = classifyComponents(ownership.ownership.shared, SHARED_KNOWN);
281
+ const infra = classifyComponents(ownership.ownership.infra, INFRA_KNOWN);
282
+ const orphans = ownership.orphans;
283
+ const graph = buildGraph(root);
191
284
 
192
285
  const hasFeatures = migratedFeatures.length > 0;
193
286
  const hasLegacy = legacyFeatures.length > 0;
194
287
 
288
+ // Monorepo detection
289
+ const isRoot = root === projectRoot;
290
+ const monorepo = isRoot ? detectMonorepo(projectRoot) : null;
291
+ const workspaces = isRoot ? detectWorkspaces(projectRoot) : [];
292
+
195
293
  return {
196
294
  projectName: basename(root),
295
+ isWorkspace: !isRoot,
296
+ workspaceScope: isRoot ? null : basename(root),
197
297
  framework,
198
298
  runtime: `Node ${process.version}`,
199
299
  database,
@@ -222,6 +322,10 @@ export async function buildContext(projectRoot = ROOT) {
222
322
  isFullyMigrated: hasFeatures && !hasLegacy,
223
323
  isLegacy: hasLegacy && !hasFeatures,
224
324
  isGreenfield: !hasLegacy && !hasFeatures && isDir(src),
325
+ // Monorepo
326
+ monorepo,
327
+ workspaces,
328
+ isMonorepo: monorepo !== null && monorepo.length > 0,
225
329
  graph,
226
330
  dependencies: {},
227
331
  };