forge-workflow 0.0.5 → 0.0.7

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 (164) hide show
  1. package/.claude/commands/dev.md +6 -1
  2. package/.claude/commands/plan.md +59 -14
  3. package/.claude/commands/premerge.md +10 -0
  4. package/.claude/commands/review.md +7 -1
  5. package/.claude/commands/ship.md +95 -47
  6. package/.claude/commands/status.md +42 -0
  7. package/.claude/commands/validate.md +7 -1
  8. package/.claude/commands/verify.md +52 -4
  9. package/.claude/rules/workflow.md +16 -0
  10. package/.claude/scripts/greptile-resolve.sh +32 -0
  11. package/.cline/workflows/dev.md +6 -1
  12. package/.cline/workflows/plan.md +59 -14
  13. package/.cline/workflows/premerge.md +10 -0
  14. package/.cline/workflows/review.md +7 -1
  15. package/.cline/workflows/ship.md +95 -47
  16. package/.cline/workflows/status.md +42 -0
  17. package/.cline/workflows/validate.md +7 -1
  18. package/.cline/workflows/verify.md +52 -4
  19. package/.codex/skills/dev/SKILL.md +6 -1
  20. package/.codex/skills/plan/SKILL.md +59 -14
  21. package/.codex/skills/premerge/SKILL.md +10 -0
  22. package/.codex/skills/review/SKILL.md +7 -1
  23. package/.codex/skills/ship/SKILL.md +95 -47
  24. package/.codex/skills/status/SKILL.md +42 -0
  25. package/.codex/skills/validate/SKILL.md +7 -1
  26. package/.codex/skills/verify/SKILL.md +52 -4
  27. package/.cursor/commands/dev.md +6 -1
  28. package/.cursor/commands/plan.md +59 -14
  29. package/.cursor/commands/premerge.md +10 -0
  30. package/.cursor/commands/review.md +7 -1
  31. package/.cursor/commands/ship.md +95 -47
  32. package/.cursor/commands/status.md +42 -0
  33. package/.cursor/commands/validate.md +7 -1
  34. package/.cursor/commands/verify.md +52 -4
  35. package/.cursorrules +149 -0
  36. package/.github/prompts/dev.prompt.md +6 -1
  37. package/.github/prompts/plan.prompt.md +59 -14
  38. package/.github/prompts/premerge.prompt.md +10 -0
  39. package/.github/prompts/review.prompt.md +7 -1
  40. package/.github/prompts/ship.prompt.md +95 -47
  41. package/.github/prompts/status.prompt.md +42 -0
  42. package/.github/prompts/validate.prompt.md +7 -1
  43. package/.github/prompts/verify.prompt.md +52 -4
  44. package/.kilocode/workflows/dev.md +6 -1
  45. package/.kilocode/workflows/plan.md +59 -14
  46. package/.kilocode/workflows/premerge.md +10 -0
  47. package/.kilocode/workflows/review.md +7 -1
  48. package/.kilocode/workflows/ship.md +95 -47
  49. package/.kilocode/workflows/status.md +42 -0
  50. package/.kilocode/workflows/validate.md +7 -1
  51. package/.kilocode/workflows/verify.md +52 -4
  52. package/.opencode/commands/dev.md +6 -1
  53. package/.opencode/commands/plan.md +59 -14
  54. package/.opencode/commands/premerge.md +10 -0
  55. package/.opencode/commands/review.md +7 -1
  56. package/.opencode/commands/ship.md +95 -47
  57. package/.opencode/commands/status.md +42 -0
  58. package/.opencode/commands/validate.md +7 -1
  59. package/.opencode/commands/verify.md +52 -4
  60. package/.roo/commands/dev.md +6 -1
  61. package/.roo/commands/plan.md +59 -14
  62. package/.roo/commands/premerge.md +10 -0
  63. package/.roo/commands/review.md +7 -1
  64. package/.roo/commands/ship.md +95 -47
  65. package/.roo/commands/status.md +42 -0
  66. package/.roo/commands/validate.md +7 -1
  67. package/.roo/commands/verify.md +52 -4
  68. package/AGENTS.md +97 -0
  69. package/CLAUDE.md +10 -0
  70. package/README.md +2 -2
  71. package/bin/forge-cmd.js +5 -1
  72. package/bin/forge-preflight.js +15 -2
  73. package/bin/forge.js +211 -9
  74. package/docs/ENHANCED_ONBOARDING.md +96 -86
  75. package/docs/ROADMAP.md +2 -2
  76. package/docs/TOOLCHAIN.md +23 -0
  77. package/docs/VALIDATION.md +1 -1
  78. package/lefthook.yml +11 -0
  79. package/lib/agents/README.md +46 -1
  80. package/lib/agents/cline.plugin.json +11 -4
  81. package/lib/agents/codex.plugin.json +2 -2
  82. package/lib/agents/copilot.plugin.json +5 -5
  83. package/lib/agents/cursor.plugin.json +1 -1
  84. package/lib/agents/kilocode.plugin.json +1 -1
  85. package/lib/agents/opencode.plugin.json +7 -4
  86. package/lib/agents/roo.plugin.json +10 -3
  87. package/lib/agents-config.js +129 -81
  88. package/lib/codex-skills.js +50 -0
  89. package/lib/commands/_registry.js +173 -0
  90. package/lib/commands/clean.js +181 -0
  91. package/lib/commands/commands-reset.js +147 -0
  92. package/lib/commands/dev.js +84 -0
  93. package/lib/commands/plan.js +18 -0
  94. package/lib/commands/push.js +196 -0
  95. package/lib/commands/recommend.js +1 -1
  96. package/lib/commands/setup.js +4295 -0
  97. package/lib/commands/ship.js +20 -0
  98. package/lib/commands/status.js +210 -44
  99. package/lib/commands/sync.js +71 -0
  100. package/lib/commands/team.js +37 -0
  101. package/lib/commands/test.js +207 -0
  102. package/lib/commands/validate.js +13 -0
  103. package/lib/commands/worktree.js +310 -0
  104. package/lib/detect-agent.js +38 -8
  105. package/lib/detection-utils.js +405 -0
  106. package/lib/docs-command.js +51 -0
  107. package/lib/docs-copy.js +50 -0
  108. package/lib/file-utils.js +260 -0
  109. package/lib/forge-context.js +42 -0
  110. package/lib/freshness-token.js +148 -0
  111. package/lib/frontmatter.js +79 -0
  112. package/lib/greptile-match.js +80 -0
  113. package/lib/husky-migration.js +113 -12
  114. package/lib/lefthook-check.js +27 -6
  115. package/lib/plugin-manager.js +225 -72
  116. package/lib/project-discovery.js +39 -5
  117. package/lib/reset.js +309 -0
  118. package/lib/runtime-health.js +305 -0
  119. package/lib/shell-utils.js +50 -0
  120. package/lib/task-ownership.js +117 -0
  121. package/lib/ui-utils.js +43 -0
  122. package/lib/validation-utils.js +163 -0
  123. package/lib/workflow/enforce-stage.js +179 -0
  124. package/lib/workflow/stages.js +201 -0
  125. package/lib/workflow/state.js +332 -0
  126. package/opencode.json +67 -0
  127. package/package.json +16 -6
  128. package/scripts/beads-context.sh +165 -22
  129. package/scripts/beads-context.test.js +5 -1
  130. package/scripts/check-agents.js +103 -0
  131. package/scripts/check-forge-token.js +98 -0
  132. package/scripts/conflict-detect.sh +2 -2
  133. package/scripts/dep-guard.sh +6 -28
  134. package/scripts/file-index.sh +117 -23
  135. package/scripts/forge-team/index.sh +86 -0
  136. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  137. package/scripts/forge-team/lib/claim.sh +256 -0
  138. package/scripts/forge-team/lib/dashboard.sh +341 -0
  139. package/scripts/forge-team/lib/epic.sh +332 -0
  140. package/scripts/forge-team/lib/hooks.sh +253 -0
  141. package/scripts/forge-team/lib/identity.sh +235 -0
  142. package/scripts/forge-team/lib/sync-github.sh +317 -0
  143. package/scripts/forge-team/lib/verify.sh +284 -0
  144. package/scripts/forge-team/lib/workload.sh +296 -0
  145. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  146. package/scripts/forge-team/tests/claim.test.sh +179 -0
  147. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  148. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  149. package/scripts/forge-team/tests/epic.test.sh +176 -0
  150. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  151. package/scripts/forge-team/tests/identity.test.sh +176 -0
  152. package/scripts/forge-team/tests/integration.test.sh +371 -0
  153. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  154. package/scripts/forge-team/tests/verify.test.sh +314 -0
  155. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  156. package/scripts/forge-team/tests/workload.test.sh +209 -0
  157. package/scripts/lib/eval-runner.js +39 -0
  158. package/scripts/lib/jsonl-lock.sh +48 -0
  159. package/scripts/lib/sanitize.sh +116 -0
  160. package/scripts/pr-coordinator.sh +756 -0
  161. package/scripts/smart-status.sh +58 -21
  162. package/scripts/sync-commands.js +49 -20
  163. package/scripts/sync-utils.sh +24 -29
  164. package/scripts/test.js +18 -1
@@ -0,0 +1,405 @@
1
+ /**
2
+ * detection-utils.js — Project detection utilities extracted from bin/forge.js
3
+ *
4
+ * Functions that previously relied on the module-level `projectRoot` variable
5
+ * now accept it as an explicit parameter. Functions that mutated the module-level
6
+ * `PKG_MANAGER` variable now return a result object instead.
7
+ */
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+ const { execSync } = require('node:child_process');
12
+
13
+ /**
14
+ * Safely execute a shell command, returning trimmed stdout or null.
15
+ * NOTE: This uses execSync intentionally for non-user-input detection commands
16
+ * like `bun --version`, `npm --version`, etc. The command strings are hardcoded
17
+ * constants, not user input, so shell injection is not a concern here.
18
+ * @param {string} cmd - Command to run (must be a hardcoded constant).
19
+ * @returns {string|null} Trimmed output or null on failure.
20
+ */
21
+ function safeExec(cmd) {
22
+ try {
23
+ return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim(); // NOSONAR — hardcoded detection commands only, no user input
24
+ } catch (_e) { // NOSONAR — intentional: safeExec returns null on any failure (command not found, etc.)
25
+ return null;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * Detect a package manager from lock file presence.
31
+ * @param {string} name - Package manager name (e.g. 'bun').
32
+ * @param {string[]} lockFiles - Lock file names to check.
33
+ * @param {string} versionPrefix - Display prefix for version string.
34
+ * @param {string} projectRoot - Absolute path to project root.
35
+ * @returns {{ found: boolean, name: string, version: string|null }} Detection result.
36
+ */
37
+ function detectFromLockFile(name, lockFiles, versionPrefix, projectRoot) {
38
+ const found = lockFiles.some(f => fs.existsSync(path.join(projectRoot, f)));
39
+ if (!found) return { found: false, name, version: null };
40
+
41
+ const version = safeExec(`${name} --version`);
42
+ if (version) console.log(` ✓ ${versionPrefix}${version} (detected from lock file)`);
43
+ return { found: true, name, version };
44
+ }
45
+
46
+ /**
47
+ * Detect a package manager from command availability.
48
+ * @param {string} name - Package manager name.
49
+ * @param {string} versionPrefix - Display prefix for version string.
50
+ * @returns {{ found: boolean, name: string, version: string|null }} Detection result.
51
+ */
52
+ function detectFromCommand(name, versionPrefix) {
53
+ const version = safeExec(`${name} --version`);
54
+ if (!version) return { found: false, name, version: null };
55
+
56
+ console.log(` ✓ ${versionPrefix}${version} (detected as package manager)`);
57
+ return { found: true, name, version };
58
+ }
59
+
60
+ /**
61
+ * Detect package manager from lock files and command availability.
62
+ * @param {string[]} errors - Array to push error messages into.
63
+ * @param {string} projectRoot - Absolute path to project root.
64
+ * @returns {{ name: string, version: string|null }|null} Detected manager, or null.
65
+ */
66
+ function detectPackageManager(errors, projectRoot) {
67
+ // Check lock files first (most authoritative)
68
+ const lockFileChecks = [
69
+ { name: 'bun', files: ['bun.lockb', 'bun.lock'], prefix: 'bun v' },
70
+ { name: 'pnpm', files: ['pnpm-lock.yaml'], prefix: 'pnpm ' },
71
+ { name: 'yarn', files: ['yarn.lock'], prefix: 'yarn ' },
72
+ ];
73
+
74
+ for (const check of lockFileChecks) {
75
+ const result = detectFromLockFile(check.name, check.files, check.prefix, projectRoot);
76
+ if (result.found) return result;
77
+ }
78
+
79
+ // Fallback: detect from installed commands
80
+ const commandChecks = [
81
+ { name: 'bun', prefix: 'bun v' },
82
+ { name: 'pnpm', prefix: 'pnpm ' },
83
+ { name: 'yarn', prefix: 'yarn ' },
84
+ { name: 'npm', prefix: 'npm ' },
85
+ ];
86
+
87
+ for (const check of commandChecks) {
88
+ const result = detectFromCommand(check.name, check.prefix);
89
+ if (result.found) return result;
90
+ }
91
+
92
+ // No package manager found
93
+ errors.push('npm, yarn, pnpm, or bun - Install a package manager');
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Detect test framework from dependency map.
99
+ * @param {Object} deps - Combined dependencies object.
100
+ * @returns {string|null} Framework name or null.
101
+ */
102
+ function detectTestFramework(deps) {
103
+ if (deps.jest) return 'jest';
104
+ if (deps.vitest) return 'vitest';
105
+ if (deps.mocha) return 'mocha';
106
+ if (deps['@playwright/test']) return 'playwright';
107
+ if (deps.cypress) return 'cypress';
108
+ if (deps.karma) return 'karma';
109
+ return null;
110
+ }
111
+
112
+ /**
113
+ * Detect language features (TypeScript, monorepo, Docker, CI/CD).
114
+ * @param {Object} pkg - Parsed package.json.
115
+ * @param {string} projectRoot - Absolute path to project root.
116
+ * @returns {{ typescript: boolean, monorepo: boolean, docker: boolean, cicd: boolean }}
117
+ */
118
+ function detectLanguageFeatures(pkg, projectRoot) {
119
+ const features = {
120
+ typescript: false,
121
+ monorepo: false,
122
+ docker: false,
123
+ cicd: false
124
+ };
125
+
126
+ // Detect TypeScript
127
+ if (pkg.devDependencies?.typescript || pkg.dependencies?.typescript) {
128
+ features.typescript = true;
129
+ }
130
+
131
+ // Detect monorepo
132
+ if (pkg.workspaces ||
133
+ fs.existsSync(path.join(projectRoot, 'pnpm-workspace.yaml')) ||
134
+ fs.existsSync(path.join(projectRoot, 'lerna.json'))) {
135
+ features.monorepo = true;
136
+ }
137
+
138
+ // Detect Docker
139
+ if (fs.existsSync(path.join(projectRoot, 'Dockerfile')) ||
140
+ fs.existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
141
+ features.docker = true;
142
+ }
143
+
144
+ // Detect CI/CD
145
+ if (fs.existsSync(path.join(projectRoot, '.github/workflows')) ||
146
+ fs.existsSync(path.join(projectRoot, '.gitlab-ci.yml')) ||
147
+ fs.existsSync(path.join(projectRoot, 'azure-pipelines.yml')) ||
148
+ fs.existsSync(path.join(projectRoot, '.circleci/config.yml'))) {
149
+ features.cicd = true;
150
+ }
151
+
152
+ return features;
153
+ }
154
+
155
+ /**
156
+ * Detect Next.js framework.
157
+ * @param {Object} deps - Combined dependencies.
158
+ * @returns {Object|null} Framework info or null.
159
+ */
160
+ function detectNextJs(deps) {
161
+ if (!deps.next) return null;
162
+
163
+ return {
164
+ framework: 'Next.js',
165
+ frameworkConfidence: 100,
166
+ projectType: 'fullstack',
167
+ buildTool: 'next',
168
+ testFramework: detectTestFramework(deps)
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Detect NestJS framework.
174
+ * @param {Object} deps - Combined dependencies.
175
+ * @returns {Object|null} Framework info or null.
176
+ */
177
+ function detectNestJs(deps) {
178
+ if (!deps['@nestjs/core'] && !deps['@nestjs/common']) return null;
179
+
180
+ return {
181
+ framework: 'NestJS',
182
+ frameworkConfidence: 100,
183
+ projectType: 'backend',
184
+ buildTool: 'nest',
185
+ testFramework: 'jest'
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Detect Angular framework.
191
+ * @param {Object} deps - Combined dependencies.
192
+ * @returns {Object|null} Framework info or null.
193
+ */
194
+ function detectAngular(deps) {
195
+ if (!deps['@angular/core'] && !deps['@angular/cli']) return null;
196
+
197
+ return {
198
+ framework: 'Angular',
199
+ frameworkConfidence: 100,
200
+ projectType: 'frontend',
201
+ buildTool: 'ng',
202
+ testFramework: 'karma'
203
+ };
204
+ }
205
+
206
+ /**
207
+ * Detect Vue.js / Nuxt framework.
208
+ * @param {Object} deps - Combined dependencies.
209
+ * @returns {Object|null} Framework info or null.
210
+ */
211
+ function detectVue(deps) {
212
+ if (!deps.vue) return null;
213
+
214
+ if (deps.nuxt) {
215
+ return {
216
+ framework: 'Nuxt',
217
+ frameworkConfidence: 100,
218
+ projectType: 'fullstack',
219
+ buildTool: 'nuxt',
220
+ testFramework: detectTestFramework(deps)
221
+ };
222
+ }
223
+
224
+ const hasVite = deps.vite;
225
+ const hasWebpack = deps.webpack;
226
+
227
+ // Determine build tool without nested ternary
228
+ let buildTool = 'vue-cli';
229
+ if (hasVite) {
230
+ buildTool = 'vite';
231
+ } else if (hasWebpack) {
232
+ buildTool = 'webpack';
233
+ }
234
+
235
+ return {
236
+ framework: 'Vue.js',
237
+ frameworkConfidence: deps['@vue/cli'] ? 100 : 90,
238
+ projectType: 'frontend',
239
+ buildTool,
240
+ testFramework: detectTestFramework(deps)
241
+ };
242
+ }
243
+
244
+ /**
245
+ * Detect React framework.
246
+ * @param {Object} deps - Combined dependencies.
247
+ * @returns {Object|null} Framework info or null.
248
+ */
249
+ function detectReact(deps) {
250
+ if (!deps.react) return null;
251
+
252
+ const hasVite = deps.vite;
253
+ const hasReactScripts = deps['react-scripts'];
254
+
255
+ // Determine build tool without nested ternary
256
+ let buildTool = 'webpack';
257
+ if (hasVite) {
258
+ buildTool = 'vite';
259
+ } else if (hasReactScripts) {
260
+ buildTool = 'create-react-app';
261
+ }
262
+
263
+ return {
264
+ framework: 'React',
265
+ frameworkConfidence: 95,
266
+ projectType: 'frontend',
267
+ buildTool,
268
+ testFramework: detectTestFramework(deps)
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Detect Express framework.
274
+ * @param {Object} deps - Combined dependencies.
275
+ * @param {{ typescript: boolean }} features - Language features.
276
+ * @returns {Object|null} Framework info or null.
277
+ */
278
+ function detectExpress(deps, features) {
279
+ if (!deps.express) return null;
280
+
281
+ return {
282
+ framework: 'Express',
283
+ frameworkConfidence: 90,
284
+ projectType: 'backend',
285
+ buildTool: features.typescript ? 'tsc' : 'node',
286
+ testFramework: detectTestFramework(deps)
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Detect Fastify framework.
292
+ * @param {Object} deps - Combined dependencies.
293
+ * @param {{ typescript: boolean }} features - Language features.
294
+ * @returns {Object|null} Framework info or null.
295
+ */
296
+ function detectFastify(deps, features) {
297
+ if (!deps.fastify) return null;
298
+
299
+ return {
300
+ framework: 'Fastify',
301
+ frameworkConfidence: 95,
302
+ projectType: 'backend',
303
+ buildTool: features.typescript ? 'tsc' : 'node',
304
+ testFramework: detectTestFramework(deps)
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Detect Svelte / SvelteKit framework.
310
+ * @param {Object} deps - Combined dependencies.
311
+ * @returns {Object|null} Framework info or null.
312
+ */
313
+ function detectSvelte(deps) {
314
+ if (!deps.svelte) return null;
315
+
316
+ if (deps['@sveltejs/kit']) {
317
+ return {
318
+ framework: 'SvelteKit',
319
+ frameworkConfidence: 100,
320
+ projectType: 'fullstack',
321
+ buildTool: 'vite',
322
+ testFramework: detectTestFramework(deps)
323
+ };
324
+ }
325
+
326
+ return {
327
+ framework: 'Svelte',
328
+ frameworkConfidence: 95,
329
+ projectType: 'frontend',
330
+ buildTool: 'vite',
331
+ testFramework: detectTestFramework(deps)
332
+ };
333
+ }
334
+
335
+ /**
336
+ * Detect Remix framework.
337
+ * @param {Object} deps - Combined dependencies.
338
+ * @returns {Object|null} Framework info or null.
339
+ */
340
+ function detectRemix(deps) {
341
+ if (!deps['@remix-run/react']) return null;
342
+
343
+ return {
344
+ framework: 'Remix',
345
+ frameworkConfidence: 100,
346
+ projectType: 'fullstack',
347
+ buildTool: 'remix',
348
+ testFramework: detectTestFramework(deps)
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Detect Astro framework.
354
+ * @param {Object} deps - Combined dependencies.
355
+ * @returns {Object|null} Framework info or null.
356
+ */
357
+ function detectAstro(deps) {
358
+ if (!deps.astro) return null;
359
+
360
+ return {
361
+ framework: 'Astro',
362
+ frameworkConfidence: 100,
363
+ projectType: 'frontend',
364
+ buildTool: 'astro',
365
+ testFramework: detectTestFramework(deps)
366
+ };
367
+ }
368
+
369
+ /**
370
+ * Detect generic Node.js project.
371
+ * @param {Object} pkg - Parsed package.json.
372
+ * @param {Object} deps - Combined dependencies.
373
+ * @param {{ typescript: boolean }} features - Language features.
374
+ * @returns {Object|null} Framework info or null.
375
+ */
376
+ function detectGenericNodeJs(pkg, deps, features) {
377
+ if (!pkg.main && !pkg.scripts?.start) return null;
378
+
379
+ return {
380
+ framework: 'Node.js',
381
+ frameworkConfidence: 70,
382
+ projectType: 'backend',
383
+ buildTool: features.typescript ? 'tsc' : 'node',
384
+ testFramework: detectTestFramework(deps)
385
+ };
386
+ }
387
+
388
+ module.exports = {
389
+ detectFromLockFile,
390
+ detectFromCommand,
391
+ detectPackageManager,
392
+ detectTestFramework,
393
+ detectLanguageFeatures,
394
+ detectNextJs,
395
+ detectNestJs,
396
+ detectAngular,
397
+ detectVue,
398
+ detectReact,
399
+ detectExpress,
400
+ detectFastify,
401
+ detectSvelte,
402
+ detectRemix,
403
+ detectAstro,
404
+ detectGenericNodeJs,
405
+ };
@@ -0,0 +1,51 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ /**
5
+ * Allowlist mapping topic names to filenames in docs/.
6
+ * Security: Only these exact keys are accepted — prevents path traversal.
7
+ */
8
+ const TOPICS = {
9
+ toolchain: 'TOOLCHAIN.md',
10
+ validation: 'VALIDATION.md',
11
+ setup: 'SETUP.md',
12
+ examples: 'EXAMPLES.md',
13
+ roadmap: 'ROADMAP.md',
14
+ };
15
+
16
+ /**
17
+ * List all available topic names.
18
+ * @returns {string[]}
19
+ */
20
+ function listTopics() {
21
+ return Object.keys(TOPICS);
22
+ }
23
+
24
+ /**
25
+ * Get the content of a documentation topic.
26
+ * Uses an allowlist to prevent path traversal attacks.
27
+ *
28
+ * @param {string} topic - Topic name (must be in TOPICS allowlist)
29
+ * @param {string} packageDir - Forge package root directory
30
+ * @returns {{ content?: string, error?: string }}
31
+ */
32
+ function getTopicContent(topic, packageDir) {
33
+ const availableList = listTopics().join(', ');
34
+
35
+ // Validate against allowlist (rejects any path traversal attempt)
36
+ const filename = TOPICS[topic];
37
+ if (!filename) {
38
+ return { error: `Unknown topic: "${topic}". Available topics: ${availableList}` };
39
+ }
40
+
41
+ const filePath = path.join(packageDir, 'docs', filename);
42
+
43
+ try {
44
+ const content = fs.readFileSync(filePath, 'utf-8');
45
+ return { content };
46
+ } catch (_error) {
47
+ return { error: `Documentation file "${filename}" not found at ${filePath}` };
48
+ }
49
+ }
50
+
51
+ module.exports = { listTopics, getTopicContent, TOPICS };
@@ -0,0 +1,50 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ /**
5
+ * Essential docs to copy from package's docs/ to consumer's docs/forge/
6
+ */
7
+ const ESSENTIAL_DOCS = ['TOOLCHAIN.md', 'VALIDATION.md'];
8
+
9
+ /**
10
+ * Copy essential documentation files from the Forge package to the consumer project.
11
+ * Creates docs/forge/ if missing. Skips files that already exist (idempotent).
12
+ *
13
+ * @param {string} projectRoot - Target project root directory
14
+ * @param {string} packageDir - Forge package directory (source of docs)
15
+ * @returns {{ created: string[], skipped: string[] }}
16
+ */
17
+ function copyEssentialDocs(projectRoot, packageDir) {
18
+ const created = [];
19
+ const skipped = [];
20
+
21
+ const targetDir = path.join(projectRoot, 'docs', 'forge');
22
+
23
+ for (const docFile of ESSENTIAL_DOCS) {
24
+ const srcPath = path.join(packageDir, 'docs', docFile);
25
+ const destPath = path.join(targetDir, docFile);
26
+ const relPath = ['docs', 'forge', docFile].join('/');
27
+
28
+ // Skip if source doesn't exist
29
+ if (!fs.existsSync(srcPath)) {
30
+ continue;
31
+ }
32
+
33
+ // Skip if destination already exists (preserve user customizations)
34
+ if (fs.existsSync(destPath)) {
35
+ skipped.push(relPath);
36
+ continue;
37
+ }
38
+
39
+ // Ensure target directory exists
40
+ fs.mkdirSync(targetDir, { recursive: true });
41
+
42
+ // Copy file
43
+ fs.copyFileSync(srcPath, destPath);
44
+ created.push(relPath);
45
+ }
46
+
47
+ return { created, skipped };
48
+ }
49
+
50
+ module.exports = { copyEssentialDocs, ESSENTIAL_DOCS };