Skip to content

Commit 20e01cb

Browse files
marcusgollclaude
andcommitted
feat: auto-install GitHub workflows on npm install
- Automatically copies workflows to user's .github/workflows/ if directory exists - Prompts to create directory if it doesn't exist - Skips existing files to preserve user customizations - Silent failure in CI/non-interactive environments Part of v6.9.0 release 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 35906a6 commit 20e01cb

File tree

1 file changed

+124
-0
lines changed

1 file changed

+124
-0
lines changed

bin/postinstall.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
* - Works with Chalk v4 (CJS) and v5+ (ESM-only)
55
* - Skips noise in CI
66
* - Fallbacks cleanly if chalk/boxen aren't present
7+
* - Copies GitHub workflows to user's .github/workflows/ (if applicable)
78
*/
89

910
const tty = require('tty');
11+
const path = require('path');
12+
const fs = require('fs');
1013

1114
(async function main() {
1215
if (shouldSilence()) return;
@@ -61,10 +64,117 @@ const tty = require('tty');
6164
console.log(banner);
6265
console.log(body + '\n');
6366
}
67+
68+
// Install GitHub workflows (interactive, skip in CI)
69+
await installWorkflows(chalk);
6470
})().catch(() => { /* never explode a postinstall message */ });
6571

6672
/* ---------------- helpers ---------------- */
6773

74+
async function installWorkflows(chalk) {
75+
// Skip workflow installation in CI, tests, or when not in a project directory
76+
if (process.env.CI || process.env.TEST || process.env.SPEC_FLOW_SILENT) return;
77+
if (!tty.isatty(1)) return; // Non-interactive terminal
78+
79+
try {
80+
// Find the user's project root (where spec-flow is being installed)
81+
// This script runs from node_modules/spec-flow/bin/postinstall.js
82+
const packageRoot = path.resolve(__dirname, '..'); // node_modules/spec-flow/
83+
const userProjectRoot = path.resolve(packageRoot, '..', '..'); // User's project root
84+
85+
// Check if we're being installed as a local dependency (not global)
86+
const userPackageJson = path.join(userProjectRoot, 'package.json');
87+
if (!fs.existsSync(userPackageJson)) {
88+
// Global install or not in a project - skip workflow installation
89+
return;
90+
}
91+
92+
const sourceWorkflowsDir = path.join(packageRoot, '.github', 'workflows');
93+
const targetWorkflowsDir = path.join(userProjectRoot, '.github', 'workflows');
94+
95+
// Check if source workflows exist
96+
if (!fs.existsSync(sourceWorkflowsDir)) return;
97+
98+
const workflowFiles = fs.readdirSync(sourceWorkflowsDir).filter(f => f.endsWith('.yml'));
99+
if (workflowFiles.length === 0) return;
100+
101+
// Check if user already has .github/workflows directory
102+
const hasWorkflowsDir = fs.existsSync(targetWorkflowsDir);
103+
104+
if (hasWorkflowsDir) {
105+
// Directory exists - install workflows automatically
106+
console.log(chalk.cyan('\n📋 Installing GitHub Actions workflows...\n'));
107+
108+
let copiedCount = 0;
109+
let skippedCount = 0;
110+
111+
for (const file of workflowFiles) {
112+
const sourcePath = path.join(sourceWorkflowsDir, file);
113+
const targetPath = path.join(targetWorkflowsDir, file);
114+
115+
if (fs.existsSync(targetPath)) {
116+
// File already exists - skip to avoid overwriting user customizations
117+
console.log(chalk.gray(` ⊝ Skipped ${file} (already exists)`));
118+
skippedCount++;
119+
} else {
120+
// Copy workflow file
121+
fs.copyFileSync(sourcePath, targetPath);
122+
console.log(chalk.green(` ✓ Installed ${file}`));
123+
copiedCount++;
124+
}
125+
}
126+
127+
if (copiedCount > 0) {
128+
console.log(chalk.green(`\n✓ Installed ${copiedCount} workflow(s) to .github/workflows/`));
129+
}
130+
if (skippedCount > 0) {
131+
console.log(chalk.gray(` ${skippedCount} workflow(s) already exist (not overwritten)`));
132+
}
133+
} else {
134+
// Directory doesn't exist - ask user if they want to create it
135+
console.log(chalk.yellow('\n⚠ GitHub Actions workflows are available but .github/workflows/ directory not found.'));
136+
137+
// Use inquirer if available, otherwise skip
138+
const inquirer = await loadInquirer();
139+
if (!inquirer) {
140+
console.log(chalk.gray(' Run manually: mkdir -p .github/workflows && cp node_modules/spec-flow/.github/workflows/*.yml .github/workflows/\n'));
141+
return;
142+
}
143+
144+
const { install } = await inquirer.prompt([
145+
{
146+
type: 'confirm',
147+
name: 'install',
148+
message: 'Create .github/workflows/ and install GitHub Actions workflows?',
149+
default: true
150+
}
151+
]);
152+
153+
if (install) {
154+
// Create directory and copy workflows
155+
fs.mkdirSync(targetWorkflowsDir, { recursive: true });
156+
console.log(chalk.green('\n✓ Created .github/workflows/'));
157+
158+
let copiedCount = 0;
159+
for (const file of workflowFiles) {
160+
const sourcePath = path.join(sourceWorkflowsDir, file);
161+
const targetPath = path.join(targetWorkflowsDir, file);
162+
fs.copyFileSync(sourcePath, targetPath);
163+
console.log(chalk.green(` ✓ Installed ${file}`));
164+
copiedCount++;
165+
}
166+
167+
console.log(chalk.green(`\n✓ Installed ${copiedCount} workflow(s)\n`));
168+
} else {
169+
console.log(chalk.gray('\nSkipped workflow installation. Install manually later if needed.\n'));
170+
}
171+
}
172+
} catch (err) {
173+
// Silent failure - don't break installation
174+
// console.error('Workflow installation error:', err);
175+
}
176+
}
177+
68178
function shouldSilence() {
69179
// Don’t spam CI logs or non-interactive environments
70180
if (process.env.CI || process.env.TEST || process.env.SPEC_FLOW_SILENT) return true;
@@ -113,6 +223,20 @@ async function loadBoxen() {
113223
}
114224
}
115225

226+
async function loadInquirer() {
227+
try {
228+
const m = await import('inquirer'); // ESM first
229+
return m.default || m;
230+
} catch {
231+
try {
232+
// eslint-disable-next-line global-require, import/no-commonjs
233+
return require('inquirer'); // CJS fallback (v8.x)
234+
} catch {
235+
return null;
236+
}
237+
}
238+
}
239+
116240
function makeNoopChalk() {
117241
// minimal shim so chalk.cyan.bold(...) won’t crash
118242
const id = s => String(s);

0 commit comments

Comments
 (0)