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.
- package/.claude/commands/dev.md +6 -1
- package/.claude/commands/plan.md +59 -14
- package/.claude/commands/premerge.md +10 -0
- package/.claude/commands/review.md +7 -1
- package/.claude/commands/ship.md +95 -47
- package/.claude/commands/status.md +42 -0
- package/.claude/commands/validate.md +7 -1
- package/.claude/commands/verify.md +52 -4
- package/.claude/rules/workflow.md +16 -0
- package/.claude/scripts/greptile-resolve.sh +32 -0
- package/.cline/workflows/dev.md +6 -1
- package/.cline/workflows/plan.md +59 -14
- package/.cline/workflows/premerge.md +10 -0
- package/.cline/workflows/review.md +7 -1
- package/.cline/workflows/ship.md +95 -47
- package/.cline/workflows/status.md +42 -0
- package/.cline/workflows/validate.md +7 -1
- package/.cline/workflows/verify.md +52 -4
- package/.codex/skills/dev/SKILL.md +6 -1
- package/.codex/skills/plan/SKILL.md +59 -14
- package/.codex/skills/premerge/SKILL.md +10 -0
- package/.codex/skills/review/SKILL.md +7 -1
- package/.codex/skills/ship/SKILL.md +95 -47
- package/.codex/skills/status/SKILL.md +42 -0
- package/.codex/skills/validate/SKILL.md +7 -1
- package/.codex/skills/verify/SKILL.md +52 -4
- package/.cursor/commands/dev.md +6 -1
- package/.cursor/commands/plan.md +59 -14
- package/.cursor/commands/premerge.md +10 -0
- package/.cursor/commands/review.md +7 -1
- package/.cursor/commands/ship.md +95 -47
- package/.cursor/commands/status.md +42 -0
- package/.cursor/commands/validate.md +7 -1
- package/.cursor/commands/verify.md +52 -4
- package/.cursorrules +149 -0
- package/.github/prompts/dev.prompt.md +6 -1
- package/.github/prompts/plan.prompt.md +59 -14
- package/.github/prompts/premerge.prompt.md +10 -0
- package/.github/prompts/review.prompt.md +7 -1
- package/.github/prompts/ship.prompt.md +95 -47
- package/.github/prompts/status.prompt.md +42 -0
- package/.github/prompts/validate.prompt.md +7 -1
- package/.github/prompts/verify.prompt.md +52 -4
- package/.kilocode/workflows/dev.md +6 -1
- package/.kilocode/workflows/plan.md +59 -14
- package/.kilocode/workflows/premerge.md +10 -0
- package/.kilocode/workflows/review.md +7 -1
- package/.kilocode/workflows/ship.md +95 -47
- package/.kilocode/workflows/status.md +42 -0
- package/.kilocode/workflows/validate.md +7 -1
- package/.kilocode/workflows/verify.md +52 -4
- package/.opencode/commands/dev.md +6 -1
- package/.opencode/commands/plan.md +59 -14
- package/.opencode/commands/premerge.md +10 -0
- package/.opencode/commands/review.md +7 -1
- package/.opencode/commands/ship.md +95 -47
- package/.opencode/commands/status.md +42 -0
- package/.opencode/commands/validate.md +7 -1
- package/.opencode/commands/verify.md +52 -4
- package/.roo/commands/dev.md +6 -1
- package/.roo/commands/plan.md +59 -14
- package/.roo/commands/premerge.md +10 -0
- package/.roo/commands/review.md +7 -1
- package/.roo/commands/ship.md +95 -47
- package/.roo/commands/status.md +42 -0
- package/.roo/commands/validate.md +7 -1
- package/.roo/commands/verify.md +52 -4
- package/AGENTS.md +97 -0
- package/CLAUDE.md +10 -0
- package/README.md +2 -2
- package/bin/forge-cmd.js +5 -1
- package/bin/forge-preflight.js +15 -2
- package/bin/forge.js +211 -9
- package/docs/ENHANCED_ONBOARDING.md +96 -86
- package/docs/ROADMAP.md +2 -2
- package/docs/TOOLCHAIN.md +23 -0
- package/docs/VALIDATION.md +1 -1
- package/lefthook.yml +11 -0
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +129 -81
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +173 -0
- package/lib/commands/clean.js +181 -0
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +84 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/push.js +196 -0
- package/lib/commands/recommend.js +1 -1
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +71 -0
- package/lib/commands/team.js +37 -0
- package/lib/commands/test.js +207 -0
- package/lib/commands/validate.js +13 -0
- package/lib/commands/worktree.js +310 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/docs-command.js +51 -0
- package/lib/docs-copy.js +50 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/freshness-token.js +148 -0
- package/lib/frontmatter.js +79 -0
- package/lib/greptile-match.js +80 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/reset.js +309 -0
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/task-ownership.js +117 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +16 -6
- package/scripts/beads-context.sh +165 -22
- package/scripts/beads-context.test.js +5 -1
- package/scripts/check-agents.js +103 -0
- package/scripts/check-forge-token.js +98 -0
- package/scripts/conflict-detect.sh +2 -2
- package/scripts/dep-guard.sh +6 -28
- package/scripts/file-index.sh +117 -23
- package/scripts/forge-team/index.sh +86 -0
- package/scripts/forge-team/lib/agent-prompt.sh +52 -0
- package/scripts/forge-team/lib/claim.sh +256 -0
- package/scripts/forge-team/lib/dashboard.sh +341 -0
- package/scripts/forge-team/lib/epic.sh +332 -0
- package/scripts/forge-team/lib/hooks.sh +253 -0
- package/scripts/forge-team/lib/identity.sh +235 -0
- package/scripts/forge-team/lib/sync-github.sh +317 -0
- package/scripts/forge-team/lib/verify.sh +284 -0
- package/scripts/forge-team/lib/workload.sh +296 -0
- package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
- package/scripts/forge-team/tests/claim.test.sh +179 -0
- package/scripts/forge-team/tests/dashboard.test.sh +170 -0
- package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
- package/scripts/forge-team/tests/epic.test.sh +176 -0
- package/scripts/forge-team/tests/hooks.test.sh +239 -0
- package/scripts/forge-team/tests/identity.test.sh +176 -0
- package/scripts/forge-team/tests/integration.test.sh +371 -0
- package/scripts/forge-team/tests/sync-github.test.sh +209 -0
- package/scripts/forge-team/tests/verify.test.sh +314 -0
- package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
- package/scripts/forge-team/tests/workload.test.sh +209 -0
- package/scripts/lib/eval-runner.js +39 -0
- package/scripts/lib/jsonl-lock.sh +48 -0
- package/scripts/lib/sanitize.sh +116 -0
- package/scripts/pr-coordinator.sh +756 -0
- package/scripts/smart-status.sh +58 -21
- package/scripts/sync-commands.js +49 -20
- package/scripts/sync-utils.sh +24 -29
- 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 };
|
package/lib/docs-copy.js
ADDED
|
@@ -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 };
|