letsrunit 0.0.1 → 0.3.3

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/dist/bin.js ADDED
@@ -0,0 +1,315 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/init.ts
4
+ import { confirm, intro, log, note, outro, spinner } from "@clack/prompts";
5
+
6
+ // src/detect.ts
7
+ import { execSync } from "child_process";
8
+ import { existsSync } from "fs";
9
+ import { join } from "path";
10
+ function detectEnvironment() {
11
+ const cwd = process.cwd();
12
+ const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
13
+ let packageManager = "npm";
14
+ if (existsSync(join(cwd, "yarn.lock"))) packageManager = "yarn";
15
+ else if (existsSync(join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
16
+ else if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) packageManager = "bun";
17
+ const nodeVersion = parseInt(process.version.slice(1), 10);
18
+ const hasCucumber = existsSync(join(cwd, "node_modules", "@cucumber", "cucumber", "package.json"));
19
+ return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };
20
+ }
21
+ function pmCmd(pm, args2) {
22
+ if (pm === "yarn") return `yarn ${args2.yarn}`;
23
+ if (pm === "pnpm") return `pnpm ${args2.pnpm}`;
24
+ if (pm === "bun") return `bun ${args2.bun}`;
25
+ return `npm ${args2.npm}`;
26
+ }
27
+ function execPm(env, args2) {
28
+ const cmd = pmCmd(env.packageManager, args2);
29
+ return execSync(cmd, { stdio: "inherit", cwd: env.cwd });
30
+ }
31
+
32
+ // src/setup/cli.ts
33
+ import { existsSync as existsSync2, readFileSync } from "fs";
34
+ import { join as join2 } from "path";
35
+ function isCliInstalled({ cwd }) {
36
+ const pkgPath = join2(cwd, "package.json");
37
+ if (!existsSync2(pkgPath)) return false;
38
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
39
+ return "@letsrunit/cli" in (pkg.devDependencies ?? {}) || "@letsrunit/cli" in (pkg.dependencies ?? {});
40
+ }
41
+ function installCli(env) {
42
+ execPm(env, {
43
+ npm: "install --save-dev @letsrunit/cli",
44
+ yarn: "add --dev @letsrunit/cli",
45
+ pnpm: "add -D @letsrunit/cli",
46
+ bun: "add -d @letsrunit/cli"
47
+ });
48
+ }
49
+
50
+ // src/setup/cucumber.ts
51
+ import { existsSync as existsSync3, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
52
+ import { join as join3 } from "path";
53
+ var BDD_IMPORT = "@letsrunit/bdd/define";
54
+ var CUCUMBER_CONFIG = `export default {
55
+ worldParameters: {
56
+ baseURL: 'http://localhost:3000',
57
+ },
58
+ };
59
+ `;
60
+ var SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';
61
+ import '${BDD_IMPORT}';
62
+
63
+ setDefaultTimeout(30_000);
64
+ `;
65
+ var EXAMPLE_FEATURE = `Feature: Example
66
+ Scenario: Homepage loads
67
+ Given I'm on the homepage
68
+ Then The page contains heading "Welcome"
69
+ `;
70
+ function installCucumber(env) {
71
+ execPm(env, {
72
+ npm: "install --save-dev @cucumber/cucumber",
73
+ yarn: "add --dev @cucumber/cucumber",
74
+ pnpm: "add -D @cucumber/cucumber",
75
+ bun: "add -d @cucumber/cucumber"
76
+ });
77
+ }
78
+ function installBdd(env) {
79
+ const pkgPath = join3(env.cwd, "package.json");
80
+ if (!existsSync3(pkgPath)) return false;
81
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
82
+ const alreadyInstalled = "@letsrunit/bdd" in (pkg.devDependencies ?? {}) || "@letsrunit/bdd" in (pkg.dependencies ?? {});
83
+ if (alreadyInstalled) return false;
84
+ execPm(env, {
85
+ npm: "install --save-dev @letsrunit/bdd",
86
+ yarn: "add --dev @letsrunit/bdd",
87
+ pnpm: "add -D @letsrunit/bdd",
88
+ bun: "add -d @letsrunit/bdd"
89
+ });
90
+ return true;
91
+ }
92
+ function setupCucumberConfig({ cwd }) {
93
+ const supportDir = join3(cwd, "features", "support");
94
+ const supportPath = join3(supportDir, "world.js");
95
+ if (existsSync3(supportPath)) {
96
+ const content = readFileSync2(supportPath, "utf-8");
97
+ if (content.includes(BDD_IMPORT)) return "skipped";
98
+ return "needs-manual-update";
99
+ }
100
+ const configPath = join3(cwd, "cucumber.js");
101
+ if (!existsSync3(configPath)) {
102
+ writeFileSync(configPath, CUCUMBER_CONFIG, "utf-8");
103
+ }
104
+ mkdirSync(supportDir, { recursive: true });
105
+ writeFileSync(supportPath, SUPPORT_FILE, "utf-8");
106
+ return "created";
107
+ }
108
+ function setupFeaturesDir({ cwd }) {
109
+ if (existsSync3(join3(cwd, "features"))) {
110
+ try {
111
+ const hasFeatureFiles = readdirSync(join3(cwd, "features")).some((f) => f.endsWith(".feature"));
112
+ if (hasFeatureFiles) return false;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+ try {
118
+ const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith(".feature"));
119
+ if (hasFeatureAtRoot) return false;
120
+ } catch {
121
+ return false;
122
+ }
123
+ mkdirSync(join3(cwd, "features"), { recursive: true });
124
+ writeFileSync(join3(cwd, "features", "example.feature"), EXAMPLE_FEATURE, "utf-8");
125
+ return true;
126
+ }
127
+ function setupCucumber(env) {
128
+ const bddInstalled = installBdd(env);
129
+ const configResult = setupCucumberConfig(env);
130
+ const featuresCreated = setupFeaturesDir(env);
131
+ return { bddInstalled, configResult, featuresCreated };
132
+ }
133
+
134
+ // src/setup/github-actions.ts
135
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
136
+ import { join as join4 } from "path";
137
+ function setupStepsFor({ packageManager, nodeVersion }) {
138
+ if (packageManager === "yarn") return ` - name: Enable Corepack
139
+ run: corepack enable
140
+ - uses: actions/setup-node@v4
141
+ with:
142
+ node-version: ${nodeVersion}
143
+ cache: yarn
144
+ - name: Install dependencies
145
+ run: yarn install --immutable`;
146
+ if (packageManager === "pnpm") return ` - uses: pnpm/action-setup@v4
147
+ - uses: actions/setup-node@v4
148
+ with:
149
+ node-version: ${nodeVersion}
150
+ cache: pnpm
151
+ - name: Install dependencies
152
+ run: pnpm install --frozen-lockfile`;
153
+ if (packageManager === "bun") return ` - uses: oven-sh/setup-bun@v2
154
+ - name: Install dependencies
155
+ run: bun install --frozen-lockfile`;
156
+ return ` - uses: actions/setup-node@v4
157
+ with:
158
+ node-version: ${nodeVersion}
159
+ cache: npm
160
+ - name: Install dependencies
161
+ run: npm ci`;
162
+ }
163
+ function workflowYaml(env) {
164
+ const setupSteps = setupStepsFor(env);
165
+ return `name: Features
166
+ on:
167
+ push:
168
+ branches: [main]
169
+ pull_request:
170
+ branches: [main]
171
+ jobs:
172
+ features:
173
+ runs-on: ubuntu-latest
174
+ steps:
175
+ - uses: actions/checkout@v4
176
+ ${setupSteps}
177
+ - name: Install Playwright browsers
178
+ run: npx playwright install chromium --with-deps
179
+ - name: Run features
180
+ run: npx cucumber-js
181
+ `;
182
+ }
183
+ function installGithubAction(env) {
184
+ const workflowDir = join4(env.cwd, ".github", "workflows");
185
+ const workflowPath = join4(workflowDir, "letsrunit.yml");
186
+ if (existsSync4(workflowPath)) return "skipped";
187
+ mkdirSync2(workflowDir, { recursive: true });
188
+ writeFileSync2(workflowPath, workflowYaml(env), "utf-8");
189
+ return "created";
190
+ }
191
+
192
+ // src/setup/playwright.ts
193
+ import { execSync as execSync2 } from "child_process";
194
+ import { existsSync as existsSync5 } from "fs";
195
+ import { join as join5 } from "path";
196
+ function hasPlaywrightBrowsers({ cwd }) {
197
+ if (!existsSync5(join5(cwd, "node_modules", "playwright-core", "package.json"))) {
198
+ return false;
199
+ }
200
+ try {
201
+ const execPath = execSync2(
202
+ `node -e "console.log(require('playwright-core').chromium.executablePath())"`,
203
+ { cwd, stdio: "pipe", encoding: "utf-8" }
204
+ ).trim();
205
+ return existsSync5(execPath);
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+ function installPlaywrightBrowsers(env) {
211
+ execPm(env, {
212
+ npm: "exec playwright install chromium",
213
+ yarn: "exec playwright install chromium",
214
+ pnpm: "exec playwright install chromium",
215
+ bun: "x playwright install chromium"
216
+ });
217
+ }
218
+
219
+ // src/init.ts
220
+ var BDD_IMPORT2 = "@letsrunit/bdd/define";
221
+ async function stepInstallCli(env) {
222
+ if (isCliInstalled(env)) {
223
+ log.success("@letsrunit/cli already installed");
224
+ return;
225
+ }
226
+ const s = spinner();
227
+ s.start("Installing @letsrunit/cli\u2026");
228
+ installCli(env);
229
+ s.stop("@letsrunit/cli installed");
230
+ }
231
+ async function stepEnsureCucumber(env, { yes: yes2 }) {
232
+ if (env.hasCucumber) return true;
233
+ if (!yes2 && !env.isInteractive) {
234
+ log.warn("@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:");
235
+ note("npm install --save-dev @cucumber/cucumber\nThen run: npx letsrunit init", "Setup Cucumber");
236
+ return false;
237
+ }
238
+ if (!yes2) {
239
+ const install = await confirm({ message: "@cucumber/cucumber not found. Install it now?" });
240
+ if (install !== true) return false;
241
+ }
242
+ const s = spinner();
243
+ s.start("Installing @cucumber/cucumber\u2026");
244
+ installCucumber(env);
245
+ s.stop("@cucumber/cucumber installed");
246
+ return true;
247
+ }
248
+ function stepSetupCucumber(env) {
249
+ const result = setupCucumber(env);
250
+ if (result.bddInstalled) log.success("@letsrunit/bdd installed");
251
+ if (result.configResult === "created") {
252
+ log.success("features/support/world.js created");
253
+ } else if (result.configResult === "needs-manual-update") {
254
+ log.warn("features/support/world.js exists but does not import @letsrunit/bdd.");
255
+ note(`Add "import '${BDD_IMPORT2}';" to features/support/world.js`, "Action required");
256
+ }
257
+ if (result.featuresCreated) log.success("features/ directory created with example.feature");
258
+ }
259
+ async function stepCheckPlaywrightBrowsers(env, { yes: yes2 }) {
260
+ if (hasPlaywrightBrowsers(env)) return;
261
+ if (!yes2 && !env.isInteractive) {
262
+ log.warn("Playwright Chromium browser not found.");
263
+ note("npx playwright install chromium", "Run to install browsers");
264
+ return;
265
+ }
266
+ if (!yes2) {
267
+ const install = await confirm({ message: "Playwright Chromium browser not found. Install it now?" });
268
+ if (install !== true) return;
269
+ }
270
+ const s = spinner();
271
+ s.start("Installing Playwright Chromium\u2026");
272
+ installPlaywrightBrowsers(env);
273
+ s.stop("Playwright Chromium installed");
274
+ }
275
+ async function stepAddGithubAction(env, { yes: yes2 }) {
276
+ if (!yes2 && !env.isInteractive) return;
277
+ if (!yes2) {
278
+ const addAction = await confirm({ message: "Add a GitHub Action to run features on push?" });
279
+ if (addAction !== true) return;
280
+ }
281
+ const result = installGithubAction(env);
282
+ if (result === "created") {
283
+ log.success(".github/workflows/letsrunit.yml created");
284
+ } else {
285
+ log.info(".github/workflows/letsrunit.yml already exists, skipped");
286
+ }
287
+ }
288
+ async function init(options = {}) {
289
+ intro("letsrunit init");
290
+ const env = detectEnvironment();
291
+ await stepInstallCli(env);
292
+ const hasCucumber = await stepEnsureCucumber(env, options);
293
+ if (hasCucumber) {
294
+ stepSetupCucumber(env);
295
+ await stepCheckPlaywrightBrowsers(env, options);
296
+ await stepAddGithubAction(env, options);
297
+ }
298
+ outro("All done! Run npx letsrunit --help to get started.");
299
+ }
300
+
301
+ // src/bin.ts
302
+ var args = process.argv.slice(2);
303
+ var command = args.find((a) => !a.startsWith("-")) ?? "init";
304
+ var yes = args.includes("--yes") || args.includes("-y");
305
+ if (command === "init") {
306
+ init({ yes }).catch((err) => {
307
+ console.error(err instanceof Error ? err.message : String(err));
308
+ process.exit(1);
309
+ });
310
+ } else {
311
+ console.error(`Unknown command: ${command}`);
312
+ console.error("Usage: letsrunit init [--yes]");
313
+ process.exit(1);
314
+ }
315
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/init.ts","../src/detect.ts","../src/setup/cli.ts","../src/setup/cucumber.ts","../src/setup/github-actions.ts","../src/setup/playwright.ts","../src/bin.ts"],"sourcesContent":["import { confirm, intro, log, note, outro, spinner } from '@clack/prompts';\nimport { detectEnvironment, type Environment } from './detect.js';\nimport { installCli, isCliInstalled } from './setup/cli.js';\nimport { installCucumber, setupCucumber } from './setup/cucumber.js';\nimport { installGithubAction } from './setup/github-actions.js';\nimport { hasPlaywrightBrowsers, installPlaywrightBrowsers } from './setup/playwright.js';\n\nconst BDD_IMPORT = '@letsrunit/bdd/define';\n\nexport interface InitOptions {\n yes?: boolean;\n}\n\nasync function stepInstallCli(env: Environment): Promise<void> {\n if (isCliInstalled(env)) {\n log.success('@letsrunit/cli already installed');\n return;\n }\n\n const s = spinner();\n s.start('Installing @letsrunit/cli…');\n installCli(env);\n s.stop('@letsrunit/cli installed');\n}\n\nasync function stepEnsureCucumber(env: Environment, { yes }: InitOptions): Promise<boolean> {\n if (env.hasCucumber) return true;\n\n if (!yes && !env.isInteractive) {\n log.warn('@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:');\n note('npm install --save-dev @cucumber/cucumber\\nThen run: npx letsrunit init', 'Setup Cucumber');\n return false;\n }\n\n if (!yes) {\n const install = await confirm({ message: '@cucumber/cucumber not found. Install it now?' });\n if (install !== true) return false;\n }\n\n const s = spinner();\n s.start('Installing @cucumber/cucumber…');\n installCucumber(env);\n s.stop('@cucumber/cucumber installed');\n return true;\n}\n\nfunction stepSetupCucumber(env: Environment): void {\n const result = setupCucumber(env);\n\n if (result.bddInstalled) log.success('@letsrunit/bdd installed');\n\n if (result.configResult === 'created') {\n log.success('features/support/world.js created');\n } else if (result.configResult === 'needs-manual-update') {\n log.warn('features/support/world.js exists but does not import @letsrunit/bdd.');\n note(`Add \"import '${BDD_IMPORT}';\" to features/support/world.js`, 'Action required');\n }\n\n if (result.featuresCreated) log.success('features/ directory created with example.feature');\n}\n\nasync function stepCheckPlaywrightBrowsers(env: Environment, { yes }: InitOptions): Promise<void> {\n if (hasPlaywrightBrowsers(env)) return;\n\n if (!yes && !env.isInteractive) {\n log.warn('Playwright Chromium browser not found.');\n note('npx playwright install chromium', 'Run to install browsers');\n return;\n }\n\n if (!yes) {\n const install = await confirm({ message: 'Playwright Chromium browser not found. Install it now?' });\n if (install !== true) return;\n }\n\n const s = spinner();\n s.start('Installing Playwright Chromium…');\n installPlaywrightBrowsers(env);\n s.stop('Playwright Chromium installed');\n}\n\nasync function stepAddGithubAction(env: Environment, { yes }: InitOptions): Promise<void> {\n if (!yes && !env.isInteractive) return;\n\n if (!yes) {\n const addAction = await confirm({ message: 'Add a GitHub Action to run features on push?' });\n if (addAction !== true) return;\n }\n\n const result = installGithubAction(env);\n if (result === 'created') {\n log.success('.github/workflows/letsrunit.yml created');\n } else {\n log.info('.github/workflows/letsrunit.yml already exists, skipped');\n }\n}\n\nexport async function init(options: InitOptions = {}): Promise<void> {\n intro('letsrunit init');\n\n const env = detectEnvironment();\n\n await stepInstallCli(env);\n\n const hasCucumber = await stepEnsureCucumber(env, options);\n if (hasCucumber) {\n stepSetupCucumber(env);\n await stepCheckPlaywrightBrowsers(env, options);\n await stepAddGithubAction(env, options);\n }\n\n outro('All done! Run npx letsrunit --help to get started.');\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport type PackageManager = 'yarn' | 'pnpm' | 'bun' | 'npm';\n\nexport interface Environment {\n isInteractive: boolean;\n packageManager: PackageManager;\n nodeVersion: number;\n hasCucumber: boolean;\n cwd: string;\n}\n\nexport interface PackageManagerArgs {\n npm: string;\n yarn: string;\n pnpm: string;\n bun: string;\n}\n\nexport function detectEnvironment(): Environment {\n const cwd = process.cwd();\n const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);\n\n let packageManager: PackageManager = 'npm';\n if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';\n else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';\n else if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) packageManager = 'bun';\n\n const nodeVersion = parseInt(process.version.slice(1), 10);\n const hasCucumber = existsSync(join(cwd, 'node_modules', '@cucumber', 'cucumber', 'package.json'));\n\n return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };\n}\n\nfunction pmCmd(pm: string, args: PackageManagerArgs): string {\n if (pm === 'yarn') return `yarn ${args.yarn}`;\n if (pm === 'pnpm') return `pnpm ${args.pnpm}`;\n if (pm === 'bun') return `bun ${args.bun}`;\n return `npm ${args.npm}`;\n}\n\nexport function execPm(env: Pick<Environment, 'packageManager' | 'cwd'>, args: PackageManagerArgs) {\n const cmd = pmCmd(env.packageManager, args);\n return execSync(cmd, { stdio: 'inherit', cwd: env.cwd });\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nexport function isCliInstalled({ cwd }: Pick<Environment, 'cwd'>): boolean {\n const pkgPath = join(cwd, 'package.json');\n if (!existsSync(pkgPath)) return false;\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n };\n\n return '@letsrunit/cli' in (pkg.devDependencies ?? {}) || '@letsrunit/cli' in (pkg.dependencies ?? {});\n}\n\nexport function installCli(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'install --save-dev @letsrunit/cli',\n yarn: 'add --dev @letsrunit/cli',\n pnpm: 'add -D @letsrunit/cli',\n bun: 'add -d @letsrunit/cli',\n });\n}\n","import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nconst BDD_IMPORT = '@letsrunit/bdd/define';\n\nconst CUCUMBER_CONFIG = `export default {\n worldParameters: {\n baseURL: 'http://localhost:3000',\n },\n};\n`;\n\nconst SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';\nimport '${BDD_IMPORT}';\n\nsetDefaultTimeout(30_000);\n`;\n\nconst EXAMPLE_FEATURE = `Feature: Example\n Scenario: Homepage loads\n Given I'm on the homepage\n Then The page contains heading \"Welcome\"\n`;\n\nexport type CucumberConfigResult = 'created' | 'skipped' | 'needs-manual-update';\n\nexport interface CucumberSetupResult {\n bddInstalled: boolean;\n configResult: CucumberConfigResult;\n featuresCreated: boolean;\n}\n\nexport function installCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'install --save-dev @cucumber/cucumber',\n yarn: 'add --dev @cucumber/cucumber',\n pnpm: 'add -D @cucumber/cucumber',\n bun: 'add -d @cucumber/cucumber',\n });\n}\n\nfunction installBdd(env: Pick<Environment, 'packageManager' | 'cwd'>): boolean {\n const pkgPath = join(env.cwd, 'package.json');\n if (!existsSync(pkgPath)) return false;\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n };\n\n const alreadyInstalled =\n '@letsrunit/bdd' in (pkg.devDependencies ?? {}) || '@letsrunit/bdd' in (pkg.dependencies ?? {});\n\n if (alreadyInstalled) return false;\n\n execPm(env, {\n npm: 'install --save-dev @letsrunit/bdd',\n yarn: 'add --dev @letsrunit/bdd',\n pnpm: 'add -D @letsrunit/bdd',\n bun: 'add -d @letsrunit/bdd',\n });\n\n return true;\n}\n\nfunction setupCucumberConfig({ cwd }: Pick<Environment, 'cwd'>): CucumberConfigResult {\n const supportDir = join(cwd, 'features', 'support');\n const supportPath = join(supportDir, 'world.js');\n\n if (existsSync(supportPath)) {\n const content = readFileSync(supportPath, 'utf-8');\n if (content.includes(BDD_IMPORT)) return 'skipped';\n return 'needs-manual-update';\n }\n\n const configPath = join(cwd, 'cucumber.js');\n if (!existsSync(configPath)) {\n writeFileSync(configPath, CUCUMBER_CONFIG, 'utf-8');\n }\n\n mkdirSync(supportDir, { recursive: true });\n writeFileSync(supportPath, SUPPORT_FILE, 'utf-8');\n return 'created';\n}\n\nfunction setupFeaturesDir({ cwd }: Pick<Environment, 'cwd'>): boolean {\n if (existsSync(join(cwd, 'features'))) {\n try {\n const hasFeatureFiles = readdirSync(join(cwd, 'features')).some((f) => f.endsWith('.feature'));\n if (hasFeatureFiles) return false;\n } catch {\n return false;\n }\n }\n\n try {\n const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith('.feature'));\n if (hasFeatureAtRoot) return false;\n } catch {\n return false;\n }\n\n mkdirSync(join(cwd, 'features'), { recursive: true });\n writeFileSync(join(cwd, 'features', 'example.feature'), EXAMPLE_FEATURE, 'utf-8');\n return true;\n}\n\nexport function setupCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): CucumberSetupResult {\n const bddInstalled = installBdd(env);\n const configResult = setupCucumberConfig(env);\n const featuresCreated = setupFeaturesDir(env);\n return { bddInstalled, configResult, featuresCreated };\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Environment } from '../detect.js';\n\nexport type GithubActionsResult = 'created' | 'skipped';\n\nfunction setupStepsFor({ packageManager, nodeVersion }: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {\n if (packageManager === 'yarn') return `\\\n - name: Enable Corepack\n run: corepack enable\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: yarn\n - name: Install dependencies\n run: yarn install --immutable`;\n if (packageManager === 'pnpm') return `\\\n - uses: pnpm/action-setup@v4\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: pnpm\n - name: Install dependencies\n run: pnpm install --frozen-lockfile`;\n if (packageManager === 'bun') return `\\\n - uses: oven-sh/setup-bun@v2\n - name: Install dependencies\n run: bun install --frozen-lockfile`;\n return `\\\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: npm\n - name: Install dependencies\n run: npm ci`;\n}\n\nfunction workflowYaml(env: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {\n const setupSteps = setupStepsFor(env);\n\n return `name: Features\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\njobs:\n features:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n${setupSteps}\n - name: Install Playwright browsers\n run: npx playwright install chromium --with-deps\n - name: Run features\n run: npx cucumber-js\n`;\n}\n\nexport function installGithubAction(env: Pick<Environment, 'packageManager' | 'nodeVersion' | 'cwd'>): GithubActionsResult {\n const workflowDir = join(env.cwd, '.github', 'workflows');\n const workflowPath = join(workflowDir, 'letsrunit.yml');\n\n if (existsSync(workflowPath)) return 'skipped';\n\n mkdirSync(workflowDir, { recursive: true });\n writeFileSync(workflowPath, workflowYaml(env), 'utf-8');\n return 'created';\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nexport function hasPlaywrightBrowsers({ cwd }: Pick<Environment, 'cwd'>): boolean {\n if (!existsSync(join(cwd, 'node_modules', 'playwright-core', 'package.json'))) {\n return false;\n }\n try {\n const execPath = execSync(\n `node -e \"console.log(require('playwright-core').chromium.executablePath())\"`,\n { cwd, stdio: 'pipe', encoding: 'utf-8' },\n ).trim();\n return existsSync(execPath);\n } catch {\n return false;\n }\n}\n\nexport function installPlaywrightBrowsers(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'exec playwright install chromium',\n yarn: 'exec playwright install chromium',\n pnpm: 'exec playwright install chromium',\n bun: 'x playwright install chromium',\n });\n}\n","import { init } from './init.js';\n\nconst args = process.argv.slice(2);\nconst command = args.find((a) => !a.startsWith('-')) ?? 'init';\nconst yes = args.includes('--yes') || args.includes('-y');\n\nif (command === 'init') {\n init({ yes }).catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : String(err));\n process.exit(1);\n });\n} else {\n console.error(`Unknown command: ${command}`);\n console.error('Usage: letsrunit init [--yes]');\n process.exit(1);\n}\n"],"mappings":"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","names":["args","existsSync","join","join","existsSync","existsSync","readFileSync","join","join","existsSync","readFileSync","existsSync","mkdirSync","writeFileSync","join","execSync","existsSync","join","existsSync","join","execSync","BDD_IMPORT","yes"]}
@@ -0,0 +1,6 @@
1
+ interface InitOptions {
2
+ yes?: boolean;
3
+ }
4
+ declare function init(options?: InitOptions): Promise<void>;
5
+
6
+ export { type InitOptions, init };
package/dist/index.js ADDED
@@ -0,0 +1,301 @@
1
+ // src/init.ts
2
+ import { confirm, intro, log, note, outro, spinner } from "@clack/prompts";
3
+
4
+ // src/detect.ts
5
+ import { execSync } from "child_process";
6
+ import { existsSync } from "fs";
7
+ import { join } from "path";
8
+ function detectEnvironment() {
9
+ const cwd = process.cwd();
10
+ const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
11
+ let packageManager = "npm";
12
+ if (existsSync(join(cwd, "yarn.lock"))) packageManager = "yarn";
13
+ else if (existsSync(join(cwd, "pnpm-lock.yaml"))) packageManager = "pnpm";
14
+ else if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) packageManager = "bun";
15
+ const nodeVersion = parseInt(process.version.slice(1), 10);
16
+ const hasCucumber = existsSync(join(cwd, "node_modules", "@cucumber", "cucumber", "package.json"));
17
+ return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };
18
+ }
19
+ function pmCmd(pm, args) {
20
+ if (pm === "yarn") return `yarn ${args.yarn}`;
21
+ if (pm === "pnpm") return `pnpm ${args.pnpm}`;
22
+ if (pm === "bun") return `bun ${args.bun}`;
23
+ return `npm ${args.npm}`;
24
+ }
25
+ function execPm(env, args) {
26
+ const cmd = pmCmd(env.packageManager, args);
27
+ return execSync(cmd, { stdio: "inherit", cwd: env.cwd });
28
+ }
29
+
30
+ // src/setup/cli.ts
31
+ import { existsSync as existsSync2, readFileSync } from "fs";
32
+ import { join as join2 } from "path";
33
+ function isCliInstalled({ cwd }) {
34
+ const pkgPath = join2(cwd, "package.json");
35
+ if (!existsSync2(pkgPath)) return false;
36
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
37
+ return "@letsrunit/cli" in (pkg.devDependencies ?? {}) || "@letsrunit/cli" in (pkg.dependencies ?? {});
38
+ }
39
+ function installCli(env) {
40
+ execPm(env, {
41
+ npm: "install --save-dev @letsrunit/cli",
42
+ yarn: "add --dev @letsrunit/cli",
43
+ pnpm: "add -D @letsrunit/cli",
44
+ bun: "add -d @letsrunit/cli"
45
+ });
46
+ }
47
+
48
+ // src/setup/cucumber.ts
49
+ import { existsSync as existsSync3, mkdirSync, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
50
+ import { join as join3 } from "path";
51
+ var BDD_IMPORT = "@letsrunit/bdd/define";
52
+ var CUCUMBER_CONFIG = `export default {
53
+ worldParameters: {
54
+ baseURL: 'http://localhost:3000',
55
+ },
56
+ };
57
+ `;
58
+ var SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';
59
+ import '${BDD_IMPORT}';
60
+
61
+ setDefaultTimeout(30_000);
62
+ `;
63
+ var EXAMPLE_FEATURE = `Feature: Example
64
+ Scenario: Homepage loads
65
+ Given I'm on the homepage
66
+ Then The page contains heading "Welcome"
67
+ `;
68
+ function installCucumber(env) {
69
+ execPm(env, {
70
+ npm: "install --save-dev @cucumber/cucumber",
71
+ yarn: "add --dev @cucumber/cucumber",
72
+ pnpm: "add -D @cucumber/cucumber",
73
+ bun: "add -d @cucumber/cucumber"
74
+ });
75
+ }
76
+ function installBdd(env) {
77
+ const pkgPath = join3(env.cwd, "package.json");
78
+ if (!existsSync3(pkgPath)) return false;
79
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
80
+ const alreadyInstalled = "@letsrunit/bdd" in (pkg.devDependencies ?? {}) || "@letsrunit/bdd" in (pkg.dependencies ?? {});
81
+ if (alreadyInstalled) return false;
82
+ execPm(env, {
83
+ npm: "install --save-dev @letsrunit/bdd",
84
+ yarn: "add --dev @letsrunit/bdd",
85
+ pnpm: "add -D @letsrunit/bdd",
86
+ bun: "add -d @letsrunit/bdd"
87
+ });
88
+ return true;
89
+ }
90
+ function setupCucumberConfig({ cwd }) {
91
+ const supportDir = join3(cwd, "features", "support");
92
+ const supportPath = join3(supportDir, "world.js");
93
+ if (existsSync3(supportPath)) {
94
+ const content = readFileSync2(supportPath, "utf-8");
95
+ if (content.includes(BDD_IMPORT)) return "skipped";
96
+ return "needs-manual-update";
97
+ }
98
+ const configPath = join3(cwd, "cucumber.js");
99
+ if (!existsSync3(configPath)) {
100
+ writeFileSync(configPath, CUCUMBER_CONFIG, "utf-8");
101
+ }
102
+ mkdirSync(supportDir, { recursive: true });
103
+ writeFileSync(supportPath, SUPPORT_FILE, "utf-8");
104
+ return "created";
105
+ }
106
+ function setupFeaturesDir({ cwd }) {
107
+ if (existsSync3(join3(cwd, "features"))) {
108
+ try {
109
+ const hasFeatureFiles = readdirSync(join3(cwd, "features")).some((f) => f.endsWith(".feature"));
110
+ if (hasFeatureFiles) return false;
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+ try {
116
+ const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith(".feature"));
117
+ if (hasFeatureAtRoot) return false;
118
+ } catch {
119
+ return false;
120
+ }
121
+ mkdirSync(join3(cwd, "features"), { recursive: true });
122
+ writeFileSync(join3(cwd, "features", "example.feature"), EXAMPLE_FEATURE, "utf-8");
123
+ return true;
124
+ }
125
+ function setupCucumber(env) {
126
+ const bddInstalled = installBdd(env);
127
+ const configResult = setupCucumberConfig(env);
128
+ const featuresCreated = setupFeaturesDir(env);
129
+ return { bddInstalled, configResult, featuresCreated };
130
+ }
131
+
132
+ // src/setup/github-actions.ts
133
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
134
+ import { join as join4 } from "path";
135
+ function setupStepsFor({ packageManager, nodeVersion }) {
136
+ if (packageManager === "yarn") return ` - name: Enable Corepack
137
+ run: corepack enable
138
+ - uses: actions/setup-node@v4
139
+ with:
140
+ node-version: ${nodeVersion}
141
+ cache: yarn
142
+ - name: Install dependencies
143
+ run: yarn install --immutable`;
144
+ if (packageManager === "pnpm") return ` - uses: pnpm/action-setup@v4
145
+ - uses: actions/setup-node@v4
146
+ with:
147
+ node-version: ${nodeVersion}
148
+ cache: pnpm
149
+ - name: Install dependencies
150
+ run: pnpm install --frozen-lockfile`;
151
+ if (packageManager === "bun") return ` - uses: oven-sh/setup-bun@v2
152
+ - name: Install dependencies
153
+ run: bun install --frozen-lockfile`;
154
+ return ` - uses: actions/setup-node@v4
155
+ with:
156
+ node-version: ${nodeVersion}
157
+ cache: npm
158
+ - name: Install dependencies
159
+ run: npm ci`;
160
+ }
161
+ function workflowYaml(env) {
162
+ const setupSteps = setupStepsFor(env);
163
+ return `name: Features
164
+ on:
165
+ push:
166
+ branches: [main]
167
+ pull_request:
168
+ branches: [main]
169
+ jobs:
170
+ features:
171
+ runs-on: ubuntu-latest
172
+ steps:
173
+ - uses: actions/checkout@v4
174
+ ${setupSteps}
175
+ - name: Install Playwright browsers
176
+ run: npx playwright install chromium --with-deps
177
+ - name: Run features
178
+ run: npx cucumber-js
179
+ `;
180
+ }
181
+ function installGithubAction(env) {
182
+ const workflowDir = join4(env.cwd, ".github", "workflows");
183
+ const workflowPath = join4(workflowDir, "letsrunit.yml");
184
+ if (existsSync4(workflowPath)) return "skipped";
185
+ mkdirSync2(workflowDir, { recursive: true });
186
+ writeFileSync2(workflowPath, workflowYaml(env), "utf-8");
187
+ return "created";
188
+ }
189
+
190
+ // src/setup/playwright.ts
191
+ import { execSync as execSync2 } from "child_process";
192
+ import { existsSync as existsSync5 } from "fs";
193
+ import { join as join5 } from "path";
194
+ function hasPlaywrightBrowsers({ cwd }) {
195
+ if (!existsSync5(join5(cwd, "node_modules", "playwright-core", "package.json"))) {
196
+ return false;
197
+ }
198
+ try {
199
+ const execPath = execSync2(
200
+ `node -e "console.log(require('playwright-core').chromium.executablePath())"`,
201
+ { cwd, stdio: "pipe", encoding: "utf-8" }
202
+ ).trim();
203
+ return existsSync5(execPath);
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+ function installPlaywrightBrowsers(env) {
209
+ execPm(env, {
210
+ npm: "exec playwright install chromium",
211
+ yarn: "exec playwright install chromium",
212
+ pnpm: "exec playwright install chromium",
213
+ bun: "x playwright install chromium"
214
+ });
215
+ }
216
+
217
+ // src/init.ts
218
+ var BDD_IMPORT2 = "@letsrunit/bdd/define";
219
+ async function stepInstallCli(env) {
220
+ if (isCliInstalled(env)) {
221
+ log.success("@letsrunit/cli already installed");
222
+ return;
223
+ }
224
+ const s = spinner();
225
+ s.start("Installing @letsrunit/cli\u2026");
226
+ installCli(env);
227
+ s.stop("@letsrunit/cli installed");
228
+ }
229
+ async function stepEnsureCucumber(env, { yes }) {
230
+ if (env.hasCucumber) return true;
231
+ if (!yes && !env.isInteractive) {
232
+ log.warn("@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:");
233
+ note("npm install --save-dev @cucumber/cucumber\nThen run: npx letsrunit init", "Setup Cucumber");
234
+ return false;
235
+ }
236
+ if (!yes) {
237
+ const install = await confirm({ message: "@cucumber/cucumber not found. Install it now?" });
238
+ if (install !== true) return false;
239
+ }
240
+ const s = spinner();
241
+ s.start("Installing @cucumber/cucumber\u2026");
242
+ installCucumber(env);
243
+ s.stop("@cucumber/cucumber installed");
244
+ return true;
245
+ }
246
+ function stepSetupCucumber(env) {
247
+ const result = setupCucumber(env);
248
+ if (result.bddInstalled) log.success("@letsrunit/bdd installed");
249
+ if (result.configResult === "created") {
250
+ log.success("features/support/world.js created");
251
+ } else if (result.configResult === "needs-manual-update") {
252
+ log.warn("features/support/world.js exists but does not import @letsrunit/bdd.");
253
+ note(`Add "import '${BDD_IMPORT2}';" to features/support/world.js`, "Action required");
254
+ }
255
+ if (result.featuresCreated) log.success("features/ directory created with example.feature");
256
+ }
257
+ async function stepCheckPlaywrightBrowsers(env, { yes }) {
258
+ if (hasPlaywrightBrowsers(env)) return;
259
+ if (!yes && !env.isInteractive) {
260
+ log.warn("Playwright Chromium browser not found.");
261
+ note("npx playwright install chromium", "Run to install browsers");
262
+ return;
263
+ }
264
+ if (!yes) {
265
+ const install = await confirm({ message: "Playwright Chromium browser not found. Install it now?" });
266
+ if (install !== true) return;
267
+ }
268
+ const s = spinner();
269
+ s.start("Installing Playwright Chromium\u2026");
270
+ installPlaywrightBrowsers(env);
271
+ s.stop("Playwright Chromium installed");
272
+ }
273
+ async function stepAddGithubAction(env, { yes }) {
274
+ if (!yes && !env.isInteractive) return;
275
+ if (!yes) {
276
+ const addAction = await confirm({ message: "Add a GitHub Action to run features on push?" });
277
+ if (addAction !== true) return;
278
+ }
279
+ const result = installGithubAction(env);
280
+ if (result === "created") {
281
+ log.success(".github/workflows/letsrunit.yml created");
282
+ } else {
283
+ log.info(".github/workflows/letsrunit.yml already exists, skipped");
284
+ }
285
+ }
286
+ async function init(options = {}) {
287
+ intro("letsrunit init");
288
+ const env = detectEnvironment();
289
+ await stepInstallCli(env);
290
+ const hasCucumber = await stepEnsureCucumber(env, options);
291
+ if (hasCucumber) {
292
+ stepSetupCucumber(env);
293
+ await stepCheckPlaywrightBrowsers(env, options);
294
+ await stepAddGithubAction(env, options);
295
+ }
296
+ outro("All done! Run npx letsrunit --help to get started.");
297
+ }
298
+ export {
299
+ init
300
+ };
301
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/init.ts","../src/detect.ts","../src/setup/cli.ts","../src/setup/cucumber.ts","../src/setup/github-actions.ts","../src/setup/playwright.ts"],"sourcesContent":["import { confirm, intro, log, note, outro, spinner } from '@clack/prompts';\nimport { detectEnvironment, type Environment } from './detect.js';\nimport { installCli, isCliInstalled } from './setup/cli.js';\nimport { installCucumber, setupCucumber } from './setup/cucumber.js';\nimport { installGithubAction } from './setup/github-actions.js';\nimport { hasPlaywrightBrowsers, installPlaywrightBrowsers } from './setup/playwright.js';\n\nconst BDD_IMPORT = '@letsrunit/bdd/define';\n\nexport interface InitOptions {\n yes?: boolean;\n}\n\nasync function stepInstallCli(env: Environment): Promise<void> {\n if (isCliInstalled(env)) {\n log.success('@letsrunit/cli already installed');\n return;\n }\n\n const s = spinner();\n s.start('Installing @letsrunit/cli…');\n installCli(env);\n s.stop('@letsrunit/cli installed');\n}\n\nasync function stepEnsureCucumber(env: Environment, { yes }: InitOptions): Promise<boolean> {\n if (env.hasCucumber) return true;\n\n if (!yes && !env.isInteractive) {\n log.warn('@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:');\n note('npm install --save-dev @cucumber/cucumber\\nThen run: npx letsrunit init', 'Setup Cucumber');\n return false;\n }\n\n if (!yes) {\n const install = await confirm({ message: '@cucumber/cucumber not found. Install it now?' });\n if (install !== true) return false;\n }\n\n const s = spinner();\n s.start('Installing @cucumber/cucumber…');\n installCucumber(env);\n s.stop('@cucumber/cucumber installed');\n return true;\n}\n\nfunction stepSetupCucumber(env: Environment): void {\n const result = setupCucumber(env);\n\n if (result.bddInstalled) log.success('@letsrunit/bdd installed');\n\n if (result.configResult === 'created') {\n log.success('features/support/world.js created');\n } else if (result.configResult === 'needs-manual-update') {\n log.warn('features/support/world.js exists but does not import @letsrunit/bdd.');\n note(`Add \"import '${BDD_IMPORT}';\" to features/support/world.js`, 'Action required');\n }\n\n if (result.featuresCreated) log.success('features/ directory created with example.feature');\n}\n\nasync function stepCheckPlaywrightBrowsers(env: Environment, { yes }: InitOptions): Promise<void> {\n if (hasPlaywrightBrowsers(env)) return;\n\n if (!yes && !env.isInteractive) {\n log.warn('Playwright Chromium browser not found.');\n note('npx playwright install chromium', 'Run to install browsers');\n return;\n }\n\n if (!yes) {\n const install = await confirm({ message: 'Playwright Chromium browser not found. Install it now?' });\n if (install !== true) return;\n }\n\n const s = spinner();\n s.start('Installing Playwright Chromium…');\n installPlaywrightBrowsers(env);\n s.stop('Playwright Chromium installed');\n}\n\nasync function stepAddGithubAction(env: Environment, { yes }: InitOptions): Promise<void> {\n if (!yes && !env.isInteractive) return;\n\n if (!yes) {\n const addAction = await confirm({ message: 'Add a GitHub Action to run features on push?' });\n if (addAction !== true) return;\n }\n\n const result = installGithubAction(env);\n if (result === 'created') {\n log.success('.github/workflows/letsrunit.yml created');\n } else {\n log.info('.github/workflows/letsrunit.yml already exists, skipped');\n }\n}\n\nexport async function init(options: InitOptions = {}): Promise<void> {\n intro('letsrunit init');\n\n const env = detectEnvironment();\n\n await stepInstallCli(env);\n\n const hasCucumber = await stepEnsureCucumber(env, options);\n if (hasCucumber) {\n stepSetupCucumber(env);\n await stepCheckPlaywrightBrowsers(env, options);\n await stepAddGithubAction(env, options);\n }\n\n outro('All done! Run npx letsrunit --help to get started.');\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\n\nexport type PackageManager = 'yarn' | 'pnpm' | 'bun' | 'npm';\n\nexport interface Environment {\n isInteractive: boolean;\n packageManager: PackageManager;\n nodeVersion: number;\n hasCucumber: boolean;\n cwd: string;\n}\n\nexport interface PackageManagerArgs {\n npm: string;\n yarn: string;\n pnpm: string;\n bun: string;\n}\n\nexport function detectEnvironment(): Environment {\n const cwd = process.cwd();\n const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);\n\n let packageManager: PackageManager = 'npm';\n if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';\n else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';\n else if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) packageManager = 'bun';\n\n const nodeVersion = parseInt(process.version.slice(1), 10);\n const hasCucumber = existsSync(join(cwd, 'node_modules', '@cucumber', 'cucumber', 'package.json'));\n\n return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };\n}\n\nfunction pmCmd(pm: string, args: PackageManagerArgs): string {\n if (pm === 'yarn') return `yarn ${args.yarn}`;\n if (pm === 'pnpm') return `pnpm ${args.pnpm}`;\n if (pm === 'bun') return `bun ${args.bun}`;\n return `npm ${args.npm}`;\n}\n\nexport function execPm(env: Pick<Environment, 'packageManager' | 'cwd'>, args: PackageManagerArgs) {\n const cmd = pmCmd(env.packageManager, args);\n return execSync(cmd, { stdio: 'inherit', cwd: env.cwd });\n}\n","import { existsSync, readFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nexport function isCliInstalled({ cwd }: Pick<Environment, 'cwd'>): boolean {\n const pkgPath = join(cwd, 'package.json');\n if (!existsSync(pkgPath)) return false;\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n };\n\n return '@letsrunit/cli' in (pkg.devDependencies ?? {}) || '@letsrunit/cli' in (pkg.dependencies ?? {});\n}\n\nexport function installCli(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'install --save-dev @letsrunit/cli',\n yarn: 'add --dev @letsrunit/cli',\n pnpm: 'add -D @letsrunit/cli',\n bun: 'add -d @letsrunit/cli',\n });\n}\n","import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nconst BDD_IMPORT = '@letsrunit/bdd/define';\n\nconst CUCUMBER_CONFIG = `export default {\n worldParameters: {\n baseURL: 'http://localhost:3000',\n },\n};\n`;\n\nconst SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';\nimport '${BDD_IMPORT}';\n\nsetDefaultTimeout(30_000);\n`;\n\nconst EXAMPLE_FEATURE = `Feature: Example\n Scenario: Homepage loads\n Given I'm on the homepage\n Then The page contains heading \"Welcome\"\n`;\n\nexport type CucumberConfigResult = 'created' | 'skipped' | 'needs-manual-update';\n\nexport interface CucumberSetupResult {\n bddInstalled: boolean;\n configResult: CucumberConfigResult;\n featuresCreated: boolean;\n}\n\nexport function installCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'install --save-dev @cucumber/cucumber',\n yarn: 'add --dev @cucumber/cucumber',\n pnpm: 'add -D @cucumber/cucumber',\n bun: 'add -d @cucumber/cucumber',\n });\n}\n\nfunction installBdd(env: Pick<Environment, 'packageManager' | 'cwd'>): boolean {\n const pkgPath = join(env.cwd, 'package.json');\n if (!existsSync(pkgPath)) return false;\n\n const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {\n devDependencies?: Record<string, string>;\n dependencies?: Record<string, string>;\n };\n\n const alreadyInstalled =\n '@letsrunit/bdd' in (pkg.devDependencies ?? {}) || '@letsrunit/bdd' in (pkg.dependencies ?? {});\n\n if (alreadyInstalled) return false;\n\n execPm(env, {\n npm: 'install --save-dev @letsrunit/bdd',\n yarn: 'add --dev @letsrunit/bdd',\n pnpm: 'add -D @letsrunit/bdd',\n bun: 'add -d @letsrunit/bdd',\n });\n\n return true;\n}\n\nfunction setupCucumberConfig({ cwd }: Pick<Environment, 'cwd'>): CucumberConfigResult {\n const supportDir = join(cwd, 'features', 'support');\n const supportPath = join(supportDir, 'world.js');\n\n if (existsSync(supportPath)) {\n const content = readFileSync(supportPath, 'utf-8');\n if (content.includes(BDD_IMPORT)) return 'skipped';\n return 'needs-manual-update';\n }\n\n const configPath = join(cwd, 'cucumber.js');\n if (!existsSync(configPath)) {\n writeFileSync(configPath, CUCUMBER_CONFIG, 'utf-8');\n }\n\n mkdirSync(supportDir, { recursive: true });\n writeFileSync(supportPath, SUPPORT_FILE, 'utf-8');\n return 'created';\n}\n\nfunction setupFeaturesDir({ cwd }: Pick<Environment, 'cwd'>): boolean {\n if (existsSync(join(cwd, 'features'))) {\n try {\n const hasFeatureFiles = readdirSync(join(cwd, 'features')).some((f) => f.endsWith('.feature'));\n if (hasFeatureFiles) return false;\n } catch {\n return false;\n }\n }\n\n try {\n const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith('.feature'));\n if (hasFeatureAtRoot) return false;\n } catch {\n return false;\n }\n\n mkdirSync(join(cwd, 'features'), { recursive: true });\n writeFileSync(join(cwd, 'features', 'example.feature'), EXAMPLE_FEATURE, 'utf-8');\n return true;\n}\n\nexport function setupCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): CucumberSetupResult {\n const bddInstalled = installBdd(env);\n const configResult = setupCucumberConfig(env);\n const featuresCreated = setupFeaturesDir(env);\n return { bddInstalled, configResult, featuresCreated };\n}\n","import { existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Environment } from '../detect.js';\n\nexport type GithubActionsResult = 'created' | 'skipped';\n\nfunction setupStepsFor({ packageManager, nodeVersion }: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {\n if (packageManager === 'yarn') return `\\\n - name: Enable Corepack\n run: corepack enable\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: yarn\n - name: Install dependencies\n run: yarn install --immutable`;\n if (packageManager === 'pnpm') return `\\\n - uses: pnpm/action-setup@v4\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: pnpm\n - name: Install dependencies\n run: pnpm install --frozen-lockfile`;\n if (packageManager === 'bun') return `\\\n - uses: oven-sh/setup-bun@v2\n - name: Install dependencies\n run: bun install --frozen-lockfile`;\n return `\\\n - uses: actions/setup-node@v4\n with:\n node-version: ${nodeVersion}\n cache: npm\n - name: Install dependencies\n run: npm ci`;\n}\n\nfunction workflowYaml(env: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {\n const setupSteps = setupStepsFor(env);\n\n return `name: Features\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\njobs:\n features:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n${setupSteps}\n - name: Install Playwright browsers\n run: npx playwright install chromium --with-deps\n - name: Run features\n run: npx cucumber-js\n`;\n}\n\nexport function installGithubAction(env: Pick<Environment, 'packageManager' | 'nodeVersion' | 'cwd'>): GithubActionsResult {\n const workflowDir = join(env.cwd, '.github', 'workflows');\n const workflowPath = join(workflowDir, 'letsrunit.yml');\n\n if (existsSync(workflowPath)) return 'skipped';\n\n mkdirSync(workflowDir, { recursive: true });\n writeFileSync(workflowPath, workflowYaml(env), 'utf-8');\n return 'created';\n}\n","import { execSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { join } from 'node:path';\nimport { type Environment, execPm } from '../detect.js';\n\nexport function hasPlaywrightBrowsers({ cwd }: Pick<Environment, 'cwd'>): boolean {\n if (!existsSync(join(cwd, 'node_modules', 'playwright-core', 'package.json'))) {\n return false;\n }\n try {\n const execPath = execSync(\n `node -e \"console.log(require('playwright-core').chromium.executablePath())\"`,\n { cwd, stdio: 'pipe', encoding: 'utf-8' },\n ).trim();\n return existsSync(execPath);\n } catch {\n return false;\n }\n}\n\nexport function installPlaywrightBrowsers(env: Pick<Environment, 'packageManager' | 'cwd'>): void {\n execPm(env, {\n npm: 'exec playwright install chromium',\n yarn: 'exec playwright install chromium',\n pnpm: 'exec playwright install chromium',\n bun: 'x playwright install chromium',\n });\n}\n"],"mappings":"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","names":["existsSync","join","join","existsSync","existsSync","readFileSync","join","join","existsSync","readFileSync","existsSync","mkdirSync","writeFileSync","join","execSync","existsSync","join","existsSync","join","execSync","BDD_IMPORT"]}
package/package.json CHANGED
@@ -1,5 +1,43 @@
1
1
  {
2
2
  "name": "letsrunit",
3
- "version": "0.0.1",
4
- "packageManager": "yarn@4.9.2"
5
- }
3
+ "version": "0.3.3",
4
+ "description": "Onboarding tool for letsrunit — run npx letsrunit init to get started",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/letsrunit-hq/letsrunit.git",
9
+ "directory": "packages/letsrunit"
10
+ },
11
+ "bugs": "https://github.com/letsrunit-hq/letsrunit/issues",
12
+ "homepage": "https://github.com/letsrunit-hq/letsrunit#readme",
13
+ "type": "module",
14
+ "bin": {
15
+ "letsrunit": "./dist/bin.js"
16
+ },
17
+ "main": "./dist/index.js",
18
+ "publishConfig": {
19
+ "main": "./dist/index.js",
20
+ "bin": {
21
+ "letsrunit": "./dist/bin.js"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "scripts": {
32
+ "build": "../../node_modules/.bin/tsup",
33
+ "test": "echo \"No tests yet\""
34
+ },
35
+ "packageManager": "yarn@4.10.3",
36
+ "dependencies": {
37
+ "@clack/prompts": "^0.10.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.0.9",
41
+ "typescript": "^5.9.3"
42
+ }
43
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { init } from './init.js';
2
+
3
+ const args = process.argv.slice(2);
4
+ const command = args.find((a) => !a.startsWith('-')) ?? 'init';
5
+ const yes = args.includes('--yes') || args.includes('-y');
6
+
7
+ if (command === 'init') {
8
+ init({ yes }).catch((err: unknown) => {
9
+ console.error(err instanceof Error ? err.message : String(err));
10
+ process.exit(1);
11
+ });
12
+ } else {
13
+ console.error(`Unknown command: ${command}`);
14
+ console.error('Usage: letsrunit init [--yes]');
15
+ process.exit(1);
16
+ }
package/src/detect.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+
5
+ export type PackageManager = 'yarn' | 'pnpm' | 'bun' | 'npm';
6
+
7
+ export interface Environment {
8
+ isInteractive: boolean;
9
+ packageManager: PackageManager;
10
+ nodeVersion: number;
11
+ hasCucumber: boolean;
12
+ cwd: string;
13
+ }
14
+
15
+ export interface PackageManagerArgs {
16
+ npm: string;
17
+ yarn: string;
18
+ pnpm: string;
19
+ bun: string;
20
+ }
21
+
22
+ export function detectEnvironment(): Environment {
23
+ const cwd = process.cwd();
24
+ const isInteractive = Boolean(process.stdout.isTTY && process.stdin.isTTY);
25
+
26
+ let packageManager: PackageManager = 'npm';
27
+ if (existsSync(join(cwd, 'yarn.lock'))) packageManager = 'yarn';
28
+ else if (existsSync(join(cwd, 'pnpm-lock.yaml'))) packageManager = 'pnpm';
29
+ else if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) packageManager = 'bun';
30
+
31
+ const nodeVersion = parseInt(process.version.slice(1), 10);
32
+ const hasCucumber = existsSync(join(cwd, 'node_modules', '@cucumber', 'cucumber', 'package.json'));
33
+
34
+ return { isInteractive, packageManager, nodeVersion, hasCucumber, cwd };
35
+ }
36
+
37
+ function pmCmd(pm: string, args: PackageManagerArgs): string {
38
+ if (pm === 'yarn') return `yarn ${args.yarn}`;
39
+ if (pm === 'pnpm') return `pnpm ${args.pnpm}`;
40
+ if (pm === 'bun') return `bun ${args.bun}`;
41
+ return `npm ${args.npm}`;
42
+ }
43
+
44
+ export function execPm(env: Pick<Environment, 'packageManager' | 'cwd'>, args: PackageManagerArgs) {
45
+ const cmd = pmCmd(env.packageManager, args);
46
+ return execSync(cmd, { stdio: 'inherit', cwd: env.cwd });
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { init } from './init.js';
2
+ export type { InitOptions } from './init.js';
package/src/init.ts ADDED
@@ -0,0 +1,113 @@
1
+ import { confirm, intro, log, note, outro, spinner } from '@clack/prompts';
2
+ import { detectEnvironment, type Environment } from './detect.js';
3
+ import { installCli, isCliInstalled } from './setup/cli.js';
4
+ import { installCucumber, setupCucumber } from './setup/cucumber.js';
5
+ import { installGithubAction } from './setup/github-actions.js';
6
+ import { hasPlaywrightBrowsers, installPlaywrightBrowsers } from './setup/playwright.js';
7
+
8
+ const BDD_IMPORT = '@letsrunit/bdd/define';
9
+
10
+ export interface InitOptions {
11
+ yes?: boolean;
12
+ }
13
+
14
+ async function stepInstallCli(env: Environment): Promise<void> {
15
+ if (isCliInstalled(env)) {
16
+ log.success('@letsrunit/cli already installed');
17
+ return;
18
+ }
19
+
20
+ const s = spinner();
21
+ s.start('Installing @letsrunit/cli…');
22
+ installCli(env);
23
+ s.stop('@letsrunit/cli installed');
24
+ }
25
+
26
+ async function stepEnsureCucumber(env: Environment, { yes }: InitOptions): Promise<boolean> {
27
+ if (env.hasCucumber) return true;
28
+
29
+ if (!yes && !env.isInteractive) {
30
+ log.warn('@cucumber/cucumber not found. Install it to use letsrunit with Cucumber:');
31
+ note('npm install --save-dev @cucumber/cucumber\nThen run: npx letsrunit init', 'Setup Cucumber');
32
+ return false;
33
+ }
34
+
35
+ if (!yes) {
36
+ const install = await confirm({ message: '@cucumber/cucumber not found. Install it now?' });
37
+ if (install !== true) return false;
38
+ }
39
+
40
+ const s = spinner();
41
+ s.start('Installing @cucumber/cucumber…');
42
+ installCucumber(env);
43
+ s.stop('@cucumber/cucumber installed');
44
+ return true;
45
+ }
46
+
47
+ function stepSetupCucumber(env: Environment): void {
48
+ const result = setupCucumber(env);
49
+
50
+ if (result.bddInstalled) log.success('@letsrunit/bdd installed');
51
+
52
+ if (result.configResult === 'created') {
53
+ log.success('features/support/world.js created');
54
+ } else if (result.configResult === 'needs-manual-update') {
55
+ log.warn('features/support/world.js exists but does not import @letsrunit/bdd.');
56
+ note(`Add "import '${BDD_IMPORT}';" to features/support/world.js`, 'Action required');
57
+ }
58
+
59
+ if (result.featuresCreated) log.success('features/ directory created with example.feature');
60
+ }
61
+
62
+ async function stepCheckPlaywrightBrowsers(env: Environment, { yes }: InitOptions): Promise<void> {
63
+ if (hasPlaywrightBrowsers(env)) return;
64
+
65
+ if (!yes && !env.isInteractive) {
66
+ log.warn('Playwright Chromium browser not found.');
67
+ note('npx playwright install chromium', 'Run to install browsers');
68
+ return;
69
+ }
70
+
71
+ if (!yes) {
72
+ const install = await confirm({ message: 'Playwright Chromium browser not found. Install it now?' });
73
+ if (install !== true) return;
74
+ }
75
+
76
+ const s = spinner();
77
+ s.start('Installing Playwright Chromium…');
78
+ installPlaywrightBrowsers(env);
79
+ s.stop('Playwright Chromium installed');
80
+ }
81
+
82
+ async function stepAddGithubAction(env: Environment, { yes }: InitOptions): Promise<void> {
83
+ if (!yes && !env.isInteractive) return;
84
+
85
+ if (!yes) {
86
+ const addAction = await confirm({ message: 'Add a GitHub Action to run features on push?' });
87
+ if (addAction !== true) return;
88
+ }
89
+
90
+ const result = installGithubAction(env);
91
+ if (result === 'created') {
92
+ log.success('.github/workflows/letsrunit.yml created');
93
+ } else {
94
+ log.info('.github/workflows/letsrunit.yml already exists, skipped');
95
+ }
96
+ }
97
+
98
+ export async function init(options: InitOptions = {}): Promise<void> {
99
+ intro('letsrunit init');
100
+
101
+ const env = detectEnvironment();
102
+
103
+ await stepInstallCli(env);
104
+
105
+ const hasCucumber = await stepEnsureCucumber(env, options);
106
+ if (hasCucumber) {
107
+ stepSetupCucumber(env);
108
+ await stepCheckPlaywrightBrowsers(env, options);
109
+ await stepAddGithubAction(env, options);
110
+ }
111
+
112
+ outro('All done! Run npx letsrunit --help to get started.');
113
+ }
@@ -0,0 +1,24 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { type Environment, execPm } from '../detect.js';
4
+
5
+ export function isCliInstalled({ cwd }: Pick<Environment, 'cwd'>): boolean {
6
+ const pkgPath = join(cwd, 'package.json');
7
+ if (!existsSync(pkgPath)) return false;
8
+
9
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {
10
+ devDependencies?: Record<string, string>;
11
+ dependencies?: Record<string, string>;
12
+ };
13
+
14
+ return '@letsrunit/cli' in (pkg.devDependencies ?? {}) || '@letsrunit/cli' in (pkg.dependencies ?? {});
15
+ }
16
+
17
+ export function installCli(env: Pick<Environment, 'packageManager' | 'cwd'>): void {
18
+ execPm(env, {
19
+ npm: 'install --save-dev @letsrunit/cli',
20
+ yarn: 'add --dev @letsrunit/cli',
21
+ pnpm: 'add -D @letsrunit/cli',
22
+ bun: 'add -d @letsrunit/cli',
23
+ });
24
+ }
@@ -0,0 +1,114 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { type Environment, execPm } from '../detect.js';
4
+
5
+ const BDD_IMPORT = '@letsrunit/bdd/define';
6
+
7
+ const CUCUMBER_CONFIG = `export default {
8
+ worldParameters: {
9
+ baseURL: 'http://localhost:3000',
10
+ },
11
+ };
12
+ `;
13
+
14
+ const SUPPORT_FILE = `import { setDefaultTimeout } from '@cucumber/cucumber';
15
+ import '${BDD_IMPORT}';
16
+
17
+ setDefaultTimeout(30_000);
18
+ `;
19
+
20
+ const EXAMPLE_FEATURE = `Feature: Example
21
+ Scenario: Homepage loads
22
+ Given I'm on the homepage
23
+ Then The page contains heading "Welcome"
24
+ `;
25
+
26
+ export type CucumberConfigResult = 'created' | 'skipped' | 'needs-manual-update';
27
+
28
+ export interface CucumberSetupResult {
29
+ bddInstalled: boolean;
30
+ configResult: CucumberConfigResult;
31
+ featuresCreated: boolean;
32
+ }
33
+
34
+ export function installCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): void {
35
+ execPm(env, {
36
+ npm: 'install --save-dev @cucumber/cucumber',
37
+ yarn: 'add --dev @cucumber/cucumber',
38
+ pnpm: 'add -D @cucumber/cucumber',
39
+ bun: 'add -d @cucumber/cucumber',
40
+ });
41
+ }
42
+
43
+ function installBdd(env: Pick<Environment, 'packageManager' | 'cwd'>): boolean {
44
+ const pkgPath = join(env.cwd, 'package.json');
45
+ if (!existsSync(pkgPath)) return false;
46
+
47
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as {
48
+ devDependencies?: Record<string, string>;
49
+ dependencies?: Record<string, string>;
50
+ };
51
+
52
+ const alreadyInstalled =
53
+ '@letsrunit/bdd' in (pkg.devDependencies ?? {}) || '@letsrunit/bdd' in (pkg.dependencies ?? {});
54
+
55
+ if (alreadyInstalled) return false;
56
+
57
+ execPm(env, {
58
+ npm: 'install --save-dev @letsrunit/bdd',
59
+ yarn: 'add --dev @letsrunit/bdd',
60
+ pnpm: 'add -D @letsrunit/bdd',
61
+ bun: 'add -d @letsrunit/bdd',
62
+ });
63
+
64
+ return true;
65
+ }
66
+
67
+ function setupCucumberConfig({ cwd }: Pick<Environment, 'cwd'>): CucumberConfigResult {
68
+ const supportDir = join(cwd, 'features', 'support');
69
+ const supportPath = join(supportDir, 'world.js');
70
+
71
+ if (existsSync(supportPath)) {
72
+ const content = readFileSync(supportPath, 'utf-8');
73
+ if (content.includes(BDD_IMPORT)) return 'skipped';
74
+ return 'needs-manual-update';
75
+ }
76
+
77
+ const configPath = join(cwd, 'cucumber.js');
78
+ if (!existsSync(configPath)) {
79
+ writeFileSync(configPath, CUCUMBER_CONFIG, 'utf-8');
80
+ }
81
+
82
+ mkdirSync(supportDir, { recursive: true });
83
+ writeFileSync(supportPath, SUPPORT_FILE, 'utf-8');
84
+ return 'created';
85
+ }
86
+
87
+ function setupFeaturesDir({ cwd }: Pick<Environment, 'cwd'>): boolean {
88
+ if (existsSync(join(cwd, 'features'))) {
89
+ try {
90
+ const hasFeatureFiles = readdirSync(join(cwd, 'features')).some((f) => f.endsWith('.feature'));
91
+ if (hasFeatureFiles) return false;
92
+ } catch {
93
+ return false;
94
+ }
95
+ }
96
+
97
+ try {
98
+ const hasFeatureAtRoot = readdirSync(cwd).some((f) => f.endsWith('.feature'));
99
+ if (hasFeatureAtRoot) return false;
100
+ } catch {
101
+ return false;
102
+ }
103
+
104
+ mkdirSync(join(cwd, 'features'), { recursive: true });
105
+ writeFileSync(join(cwd, 'features', 'example.feature'), EXAMPLE_FEATURE, 'utf-8');
106
+ return true;
107
+ }
108
+
109
+ export function setupCucumber(env: Pick<Environment, 'packageManager' | 'cwd'>): CucumberSetupResult {
110
+ const bddInstalled = installBdd(env);
111
+ const configResult = setupCucumberConfig(env);
112
+ const featuresCreated = setupFeaturesDir(env);
113
+ return { bddInstalled, configResult, featuresCreated };
114
+ }
@@ -0,0 +1,69 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import type { Environment } from '../detect.js';
4
+
5
+ export type GithubActionsResult = 'created' | 'skipped';
6
+
7
+ function setupStepsFor({ packageManager, nodeVersion }: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {
8
+ if (packageManager === 'yarn') return `\
9
+ - name: Enable Corepack
10
+ run: corepack enable
11
+ - uses: actions/setup-node@v4
12
+ with:
13
+ node-version: ${nodeVersion}
14
+ cache: yarn
15
+ - name: Install dependencies
16
+ run: yarn install --immutable`;
17
+ if (packageManager === 'pnpm') return `\
18
+ - uses: pnpm/action-setup@v4
19
+ - uses: actions/setup-node@v4
20
+ with:
21
+ node-version: ${nodeVersion}
22
+ cache: pnpm
23
+ - name: Install dependencies
24
+ run: pnpm install --frozen-lockfile`;
25
+ if (packageManager === 'bun') return `\
26
+ - uses: oven-sh/setup-bun@v2
27
+ - name: Install dependencies
28
+ run: bun install --frozen-lockfile`;
29
+ return `\
30
+ - uses: actions/setup-node@v4
31
+ with:
32
+ node-version: ${nodeVersion}
33
+ cache: npm
34
+ - name: Install dependencies
35
+ run: npm ci`;
36
+ }
37
+
38
+ function workflowYaml(env: Pick<Environment, 'packageManager' | 'nodeVersion'>): string {
39
+ const setupSteps = setupStepsFor(env);
40
+
41
+ return `name: Features
42
+ on:
43
+ push:
44
+ branches: [main]
45
+ pull_request:
46
+ branches: [main]
47
+ jobs:
48
+ features:
49
+ runs-on: ubuntu-latest
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ ${setupSteps}
53
+ - name: Install Playwright browsers
54
+ run: npx playwright install chromium --with-deps
55
+ - name: Run features
56
+ run: npx cucumber-js
57
+ `;
58
+ }
59
+
60
+ export function installGithubAction(env: Pick<Environment, 'packageManager' | 'nodeVersion' | 'cwd'>): GithubActionsResult {
61
+ const workflowDir = join(env.cwd, '.github', 'workflows');
62
+ const workflowPath = join(workflowDir, 'letsrunit.yml');
63
+
64
+ if (existsSync(workflowPath)) return 'skipped';
65
+
66
+ mkdirSync(workflowDir, { recursive: true });
67
+ writeFileSync(workflowPath, workflowYaml(env), 'utf-8');
68
+ return 'created';
69
+ }
@@ -0,0 +1,28 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { type Environment, execPm } from '../detect.js';
5
+
6
+ export function hasPlaywrightBrowsers({ cwd }: Pick<Environment, 'cwd'>): boolean {
7
+ if (!existsSync(join(cwd, 'node_modules', 'playwright-core', 'package.json'))) {
8
+ return false;
9
+ }
10
+ try {
11
+ const execPath = execSync(
12
+ `node -e "console.log(require('playwright-core').chromium.executablePath())"`,
13
+ { cwd, stdio: 'pipe', encoding: 'utf-8' },
14
+ ).trim();
15
+ return existsSync(execPath);
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ export function installPlaywrightBrowsers(env: Pick<Environment, 'packageManager' | 'cwd'>): void {
22
+ execPm(env, {
23
+ npm: 'exec playwright install chromium',
24
+ yarn: 'exec playwright install chromium',
25
+ pnpm: 'exec playwright install chromium',
26
+ bun: 'x playwright install chromium',
27
+ });
28
+ }
package/.editorconfig DELETED
@@ -1,8 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- charset = utf-8
5
- end_of_line = lf
6
- indent_size = 2
7
- indent_style = space
8
- insert_final_newline = true
package/.gitattributes DELETED
@@ -1,4 +0,0 @@
1
- /.yarn/** linguist-vendored
2
- /.yarn/releases/* binary
3
- /.yarn/plugins/**/* binary
4
- /.pnp.* binary linguist-generated
package/README.md DELETED
@@ -1 +0,0 @@
1
- # letsrunit