@esimplicitylabs/katalyst-xspec 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +7 -0
- package/README.md +69 -0
- package/bin/katalyst-xspec.cjs +54 -0
- package/cli/init.cjs +679 -0
- package/cli/stubs.cjs +365 -0
- package/cli/upgrade.cjs +1014 -0
- package/dist/chunk-ACAXOGKZ.js +1611 -0
- package/dist/index.d.ts +881 -0
- package/dist/index.js +1091 -0
- package/dist/steps/index.d.ts +151 -0
- package/dist/steps/index.js +50 -0
- package/package.json +80 -0
- package/scripts/postinstall.cjs +85 -0
- package/skills/katalyst-bdd-architecture/SKILL.md +517 -0
- package/skills/katalyst-bdd-architecture/references/adapters.md +310 -0
- package/skills/katalyst-bdd-architecture/references/custom-steps.md +360 -0
- package/skills/katalyst-bdd-architecture/references/ports.md +256 -0
- package/skills/katalyst-bdd-create-test/SKILL.md +366 -0
- package/skills/katalyst-bdd-create-test/references/api-patterns.md +371 -0
- package/skills/katalyst-bdd-create-test/references/hybrid-patterns.md +420 -0
- package/skills/katalyst-bdd-create-test/references/tui-patterns.md +458 -0
- package/skills/katalyst-bdd-create-test/references/ui-patterns.md +415 -0
- package/skills/katalyst-bdd-quickstart/SKILL.md +292 -0
- package/skills/katalyst-bdd-step-reference/SKILL.md +147 -0
- package/skills/katalyst-bdd-step-reference/references/api-steps.md +247 -0
- package/skills/katalyst-bdd-step-reference/references/shared-steps.md +340 -0
- package/skills/katalyst-bdd-step-reference/references/tui-steps.md +483 -0
- package/skills/katalyst-bdd-step-reference/references/ui-steps.md +521 -0
- package/skills/katalyst-bdd-troubleshooting/SKILL.md +449 -0
package/cli/init.cjs
ADDED
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs/promises');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
// Agent skill directories configuration
|
|
9
|
+
const SKILL_AGENTS = {
|
|
10
|
+
'opencode': '.opencode/skills',
|
|
11
|
+
'claude-code': '.claude/skills',
|
|
12
|
+
'cursor': '.cursor/skills',
|
|
13
|
+
'generic': 'skills',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SKILL_AGENT_LABELS = {
|
|
17
|
+
'opencode': 'OpenCode (.opencode/skills/)',
|
|
18
|
+
'claude-code': 'Claude Code (.claude/skills/)',
|
|
19
|
+
'cursor': 'Cursor (.cursor/skills/)',
|
|
20
|
+
'generic': 'Generic (skills/)',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
async function pathExists(p) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(p);
|
|
26
|
+
return true;
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function promptYesNo(question) {
|
|
33
|
+
const rl = readline.createInterface({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout,
|
|
36
|
+
});
|
|
37
|
+
return new Promise((resolve) => {
|
|
38
|
+
rl.question(`${question} (y/n) `, (answer) => {
|
|
39
|
+
rl.close();
|
|
40
|
+
resolve(answer.toLowerCase().startsWith('y'));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function promptMultiSelect(question, options) {
|
|
46
|
+
const rl = readline.createInterface({
|
|
47
|
+
input: process.stdin,
|
|
48
|
+
output: process.stdout,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
console.log(`\n${question}`);
|
|
52
|
+
options.forEach((opt, i) => {
|
|
53
|
+
console.log(` ${i + 1}. ${opt.label}`);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
rl.question('Enter numbers (comma-separated, e.g., 1,2,3): ', (answer) => {
|
|
58
|
+
rl.close();
|
|
59
|
+
const indices = answer.split(',')
|
|
60
|
+
.map(s => parseInt(s.trim(), 10) - 1)
|
|
61
|
+
.filter(i => i >= 0 && i < options.length);
|
|
62
|
+
const selected = indices.map(i => options[i].key);
|
|
63
|
+
resolve(selected.length > 0 ? selected : ['generic']); // Default to generic if none selected
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function copyDir(src, dest) {
|
|
69
|
+
await fs.mkdir(dest, { recursive: true });
|
|
70
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
71
|
+
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
const srcPath = path.join(src, entry.name);
|
|
74
|
+
const destPath = path.join(dest, entry.name);
|
|
75
|
+
|
|
76
|
+
if (entry.isDirectory()) {
|
|
77
|
+
await copyDir(srcPath, destPath);
|
|
78
|
+
} else {
|
|
79
|
+
await fs.copyFile(srcPath, destPath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function copySkillsToAgents(targetDir, agents, results) {
|
|
85
|
+
const skillsSourceDir = path.join(__dirname, '..', 'skills');
|
|
86
|
+
|
|
87
|
+
// Check if skills source exists
|
|
88
|
+
if (!(await pathExists(skillsSourceDir))) {
|
|
89
|
+
console.log(' Warning: Skills source directory not found, skipping skills installation');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Get list of skill directories
|
|
94
|
+
const skillDirs = await fs.readdir(skillsSourceDir, { withFileTypes: true });
|
|
95
|
+
const skills = skillDirs.filter(d => d.isDirectory()).map(d => d.name);
|
|
96
|
+
|
|
97
|
+
for (const agent of agents) {
|
|
98
|
+
const agentSkillsDir = path.join(targetDir, SKILL_AGENTS[agent]);
|
|
99
|
+
|
|
100
|
+
for (const skill of skills) {
|
|
101
|
+
const srcSkillDir = path.join(skillsSourceDir, skill);
|
|
102
|
+
const destSkillDir = path.join(agentSkillsDir, skill);
|
|
103
|
+
|
|
104
|
+
await copyDir(srcSkillDir, destSkillDir);
|
|
105
|
+
results.skills.push(path.relative(process.cwd(), destSkillDir));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function ensureDir(dir) {
|
|
111
|
+
await fs.mkdir(dir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Parse .env file content into key-value pairs
|
|
116
|
+
* Preserves comments as separate entries
|
|
117
|
+
*/
|
|
118
|
+
function parseEnvFile(content) {
|
|
119
|
+
const lines = content.split('\n');
|
|
120
|
+
const entries = [];
|
|
121
|
+
|
|
122
|
+
for (const line of lines) {
|
|
123
|
+
const trimmed = line.trim();
|
|
124
|
+
|
|
125
|
+
// Empty line
|
|
126
|
+
if (trimmed === '') {
|
|
127
|
+
entries.push({ type: 'empty' });
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Comment line
|
|
132
|
+
if (trimmed.startsWith('#')) {
|
|
133
|
+
entries.push({ type: 'comment', value: line });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Key=value line
|
|
138
|
+
const eqIndex = line.indexOf('=');
|
|
139
|
+
if (eqIndex > 0) {
|
|
140
|
+
const key = line.substring(0, eqIndex).trim();
|
|
141
|
+
const value = line.substring(eqIndex + 1);
|
|
142
|
+
entries.push({ type: 'var', key, value, line });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return entries;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Merge two .env files, preserving existing variables and adding new ones
|
|
151
|
+
*/
|
|
152
|
+
function mergeEnvFiles(existing, template) {
|
|
153
|
+
const existingEntries = parseEnvFile(existing);
|
|
154
|
+
const templateEntries = parseEnvFile(template);
|
|
155
|
+
|
|
156
|
+
// Get existing keys
|
|
157
|
+
const existingKeys = new Set(
|
|
158
|
+
existingEntries.filter(e => e.type === 'var').map(e => e.key)
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
// Find new variables from template that don't exist
|
|
162
|
+
const newVars = templateEntries.filter(e =>
|
|
163
|
+
e.type === 'var' && !existingKeys.has(e.key)
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// If no new vars, return existing as-is
|
|
167
|
+
if (newVars.length === 0) {
|
|
168
|
+
return existing;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Build merged content
|
|
172
|
+
let merged = existing;
|
|
173
|
+
|
|
174
|
+
// Add new variables at the end
|
|
175
|
+
if (!merged.endsWith('\n')) {
|
|
176
|
+
merged += '\n';
|
|
177
|
+
}
|
|
178
|
+
merged += '\n# New variables added by scaffolder update\n';
|
|
179
|
+
|
|
180
|
+
for (const entry of newVars) {
|
|
181
|
+
merged += `${entry.line}\n`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return merged;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function writeFileSafe(filePath, content, { force, results, merge }) {
|
|
188
|
+
const exists = await pathExists(filePath);
|
|
189
|
+
|
|
190
|
+
// Handle .env.example merging
|
|
191
|
+
if (exists && merge && filePath.endsWith('.env.example')) {
|
|
192
|
+
const existingContent = await fs.readFile(filePath, 'utf8');
|
|
193
|
+
const mergedContent = mergeEnvFiles(existingContent, content);
|
|
194
|
+
|
|
195
|
+
if (mergedContent !== existingContent) {
|
|
196
|
+
await fs.writeFile(filePath, mergedContent, 'utf8');
|
|
197
|
+
results.merged = results.merged || [];
|
|
198
|
+
results.merged.push(filePath);
|
|
199
|
+
} else {
|
|
200
|
+
results.skipped.push(filePath);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Never overwrite .env (user's actual config)
|
|
206
|
+
if (exists && filePath.endsWith('.env') && !filePath.endsWith('.env.example')) {
|
|
207
|
+
results.skipped.push(filePath);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (exists && !force) {
|
|
212
|
+
results.skipped.push(filePath);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
await ensureDir(path.dirname(filePath));
|
|
216
|
+
await fs.writeFile(filePath, content, 'utf8');
|
|
217
|
+
results.created.push(filePath);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function detectPackageManager(startDir) {
|
|
221
|
+
let dir = startDir;
|
|
222
|
+
while (true) {
|
|
223
|
+
if (await pathExists(path.join(dir, 'bun.lockb'))) return 'bun';
|
|
224
|
+
if (await pathExists(path.join(dir, 'bun.lock'))) return 'bun';
|
|
225
|
+
if (await pathExists(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
226
|
+
if (await pathExists(path.join(dir, 'yarn.lock'))) return 'yarn';
|
|
227
|
+
if (await pathExists(path.join(dir, 'package-lock.json'))) return 'npm';
|
|
228
|
+
const parent = path.dirname(dir);
|
|
229
|
+
if (parent === dir) break;
|
|
230
|
+
dir = parent;
|
|
231
|
+
}
|
|
232
|
+
return 'npm';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function commandsFor(pm) {
|
|
236
|
+
switch (pm) {
|
|
237
|
+
case 'bun':
|
|
238
|
+
return { install: 'bun install', test: 'bun run test' };
|
|
239
|
+
case 'pnpm':
|
|
240
|
+
return { install: 'pnpm install', test: 'pnpm test' };
|
|
241
|
+
case 'yarn':
|
|
242
|
+
return { install: 'yarn install', test: 'yarn test' };
|
|
243
|
+
default:
|
|
244
|
+
return { install: 'npm install', test: 'npm test' };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseArgs(argv) {
|
|
249
|
+
const args = {
|
|
250
|
+
dir: 'katalyst-xspec',
|
|
251
|
+
force: false,
|
|
252
|
+
withSkills: undefined, // undefined = ask, true = install, false = skip
|
|
253
|
+
skillsAgents: null, // null = ask, array = use these agents
|
|
254
|
+
};
|
|
255
|
+
for (let i = 0; i < argv.length; i++) {
|
|
256
|
+
const arg = argv[i];
|
|
257
|
+
if (arg === '--dir' && argv[i + 1]) {
|
|
258
|
+
args.dir = argv[++i];
|
|
259
|
+
} else if (arg === '--force') {
|
|
260
|
+
args.force = true;
|
|
261
|
+
} else if (arg === '--with-skills') {
|
|
262
|
+
args.withSkills = true;
|
|
263
|
+
} else if (arg === '--no-skills') {
|
|
264
|
+
args.withSkills = false;
|
|
265
|
+
} else if (arg === '--skills-agents' && argv[i + 1]) {
|
|
266
|
+
const agentList = argv[++i].split(',').map(s => s.trim()).filter(Boolean);
|
|
267
|
+
const validAgents = agentList.filter(a => SKILL_AGENTS[a]);
|
|
268
|
+
args.skillsAgents = validAgents.length > 0 ? validAgents : null;
|
|
269
|
+
} else if (!arg.startsWith('-')) {
|
|
270
|
+
// Positional target directory: `katalyst-xspec init my-tests`
|
|
271
|
+
args.dir = arg;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return args;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function templates(packageName) {
|
|
278
|
+
const pkg = {
|
|
279
|
+
name: packageName,
|
|
280
|
+
private: true,
|
|
281
|
+
version: '0.1.0',
|
|
282
|
+
type: 'module',
|
|
283
|
+
scripts: {
|
|
284
|
+
gen: 'bddgen',
|
|
285
|
+
'gen:stubs': 'katalyst-xspec stubs',
|
|
286
|
+
test: 'bddgen && playwright test',
|
|
287
|
+
'check-updates': 'katalyst-xspec upgrade --check',
|
|
288
|
+
upgrade: 'katalyst-xspec upgrade',
|
|
289
|
+
'upgrade:migrate': 'katalyst-xspec upgrade --migrate',
|
|
290
|
+
'clean:gen': 'rm -rf .features-gen',
|
|
291
|
+
clean: 'rm -rf .features-gen node_modules test-results storage cucumber-report playwright-report'
|
|
292
|
+
},
|
|
293
|
+
devDependencies: {
|
|
294
|
+
'@esimplicitylabs/katalyst-xspec': '^0.6.0',
|
|
295
|
+
'@playwright/test': '^1.49.0',
|
|
296
|
+
'playwright-bdd': '^9.1.0',
|
|
297
|
+
dotenv: '^16.1.4',
|
|
298
|
+
typescript: '^5.6.3'
|
|
299
|
+
},
|
|
300
|
+
engines: {
|
|
301
|
+
node: '>=20'
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const tsconfig = {
|
|
306
|
+
compilerOptions: {
|
|
307
|
+
target: 'ES2021',
|
|
308
|
+
module: 'NodeNext',
|
|
309
|
+
moduleResolution: 'NodeNext',
|
|
310
|
+
strict: true,
|
|
311
|
+
types: ['node', '@playwright/test']
|
|
312
|
+
},
|
|
313
|
+
include: ['features/**/*.ts', 'playwright.config.ts']
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const fixturesTs = `import {
|
|
317
|
+
createBddTest,
|
|
318
|
+
PlaywrightApiAdapter,
|
|
319
|
+
PlaywrightUiAdapter,
|
|
320
|
+
UniversalAuthAdapter,
|
|
321
|
+
DefaultCleanupAdapter,
|
|
322
|
+
TuiTesterAdapter,
|
|
323
|
+
} from '@esimplicitylabs/katalyst-xspec';
|
|
324
|
+
|
|
325
|
+
export const { test } = createBddTest({
|
|
326
|
+
createApi: ({ apiRequest }) => new PlaywrightApiAdapter(apiRequest),
|
|
327
|
+
createUi: ({ page }) => new PlaywrightUiAdapter(page),
|
|
328
|
+
createAuth: ({ api, ui }) => new UniversalAuthAdapter({ api, ui }),
|
|
329
|
+
createCleanup: () => new DefaultCleanupAdapter(),
|
|
330
|
+
// TUI testing (optional - requires tui-tester and tmux installed)
|
|
331
|
+
// Uncomment and configure for your CLI application:
|
|
332
|
+
// createTui: () => new TuiTesterAdapter({
|
|
333
|
+
// command: ['node', 'dist/cli.js'],
|
|
334
|
+
// size: { cols: 100, rows: 30 },
|
|
335
|
+
// debug: process.env.DEBUG === 'true',
|
|
336
|
+
// }),
|
|
337
|
+
});
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
const stepsTs = `import { test } from './fixtures.js';
|
|
341
|
+
import {
|
|
342
|
+
registerApiSteps,
|
|
343
|
+
registerUiSteps,
|
|
344
|
+
registerSharedSteps,
|
|
345
|
+
registerHybridSuite,
|
|
346
|
+
registerTuiSteps,
|
|
347
|
+
} from '@esimplicitylabs/katalyst-xspec/steps';
|
|
348
|
+
|
|
349
|
+
registerApiSteps(test);
|
|
350
|
+
registerUiSteps(test);
|
|
351
|
+
registerSharedSteps(test);
|
|
352
|
+
registerHybridSuite(test);
|
|
353
|
+
|
|
354
|
+
// TUI steps (optional - requires tui-tester and tmux installed)
|
|
355
|
+
// Uncomment when you have TUI testing configured:
|
|
356
|
+
// registerTuiSteps(test);
|
|
357
|
+
|
|
358
|
+
export { test };
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
const playwrightConfig = `import { defineConfig } from '@playwright/test';
|
|
362
|
+
import { defineBddProject, cucumberReporter } from 'playwright-bdd';
|
|
363
|
+
import { resolveWorkers } from '@esimplicitylabs/katalyst-xspec';
|
|
364
|
+
import dotenv from 'dotenv';
|
|
365
|
+
import fs from 'node:fs';
|
|
366
|
+
import path from 'node:path';
|
|
367
|
+
import { fileURLToPath } from 'node:url';
|
|
368
|
+
|
|
369
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
370
|
+
const localEnvPath = path.resolve(__dirname, '.env');
|
|
371
|
+
const rootEnvPath = path.resolve(process.cwd(), '.env');
|
|
372
|
+
|
|
373
|
+
if (fs.existsSync(localEnvPath)) {
|
|
374
|
+
dotenv.config({ path: localEnvPath });
|
|
375
|
+
} else if (fs.existsSync(rootEnvPath)) {
|
|
376
|
+
dotenv.config({ path: rootEnvPath });
|
|
377
|
+
} else {
|
|
378
|
+
dotenv.config();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const apiBdd = defineBddProject({
|
|
382
|
+
name: 'api',
|
|
383
|
+
features: 'features/api/**/*.feature',
|
|
384
|
+
steps: 'features/steps/**/*.ts',
|
|
385
|
+
tags: '@api',
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const uiBdd = defineBddProject({
|
|
389
|
+
name: 'ui',
|
|
390
|
+
features: 'features/ui/**/*.feature',
|
|
391
|
+
steps: 'features/steps/**/*.ts',
|
|
392
|
+
tags: '@ui',
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const hybridBdd = defineBddProject({
|
|
396
|
+
name: 'hybrid',
|
|
397
|
+
features: 'features/hybrid/**/*.feature',
|
|
398
|
+
steps: 'features/steps/**/*.ts',
|
|
399
|
+
tags: '@hybrid',
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// TUI project (optional - uncomment when TUI testing is configured)
|
|
403
|
+
// const tuiBdd = defineBddProject({
|
|
404
|
+
// name: 'tui',
|
|
405
|
+
// features: 'features/tui/**/*.feature',
|
|
406
|
+
// steps: 'features/steps/**/*.ts',
|
|
407
|
+
// tags: '@tui',
|
|
408
|
+
// });
|
|
409
|
+
|
|
410
|
+
export default defineConfig({
|
|
411
|
+
workers: resolveWorkers(),
|
|
412
|
+
reporter: [
|
|
413
|
+
cucumberReporter('html', { outputFile: 'cucumber-report/index.html' }),
|
|
414
|
+
cucumberReporter('json', { outputFile: 'cucumber-report/report.json' }),
|
|
415
|
+
],
|
|
416
|
+
// Add tuiBdd to this array when TUI testing is enabled
|
|
417
|
+
projects: [apiBdd, uiBdd, hybridBdd /* , tuiBdd */],
|
|
418
|
+
use: {
|
|
419
|
+
baseURL: process.env.BASE_URL || process.env.FRONTEND_URL || 'http://localhost:3000',
|
|
420
|
+
headless: process.env.HEADLESS === 'false' ? false : true,
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
`;
|
|
424
|
+
|
|
425
|
+
const apiFeature = `Feature: API example
|
|
426
|
+
As an API consumer
|
|
427
|
+
I want to call the service
|
|
428
|
+
So that I can verify responses
|
|
429
|
+
|
|
430
|
+
@api
|
|
431
|
+
Scenario: GET health
|
|
432
|
+
When I GET "/health"
|
|
433
|
+
Then the response status should be 200
|
|
434
|
+
`;
|
|
435
|
+
|
|
436
|
+
const uiFeature = `Feature: UI example
|
|
437
|
+
As a user
|
|
438
|
+
I want to load the homepage
|
|
439
|
+
So that I can see content
|
|
440
|
+
|
|
441
|
+
@ui
|
|
442
|
+
Scenario: Visit homepage
|
|
443
|
+
Given I navigate to "/"
|
|
444
|
+
Then the URL should contain "/"
|
|
445
|
+
`;
|
|
446
|
+
|
|
447
|
+
const hybridFeature = `Feature: Hybrid example
|
|
448
|
+
As a tester
|
|
449
|
+
I want to mix API and UI steps
|
|
450
|
+
So that I can cover flows end-to-end
|
|
451
|
+
|
|
452
|
+
@hybrid
|
|
453
|
+
Scenario: API then UI
|
|
454
|
+
When I GET "/health"
|
|
455
|
+
Then the response status should be 200
|
|
456
|
+
Given I navigate to "/"
|
|
457
|
+
Then the URL should contain "/"
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
const tuiFeature = `Feature: TUI example
|
|
461
|
+
As a CLI user
|
|
462
|
+
I want to interact with the terminal application
|
|
463
|
+
So that I can verify TUI functionality
|
|
464
|
+
|
|
465
|
+
@tui
|
|
466
|
+
Scenario: Start and verify TUI application
|
|
467
|
+
Given I start the TUI application
|
|
468
|
+
Then I should see "Welcome"
|
|
469
|
+
When I type "help"
|
|
470
|
+
And I press enter
|
|
471
|
+
Then I should see "Available commands"
|
|
472
|
+
|
|
473
|
+
@tui
|
|
474
|
+
Scenario: Navigate menu with keyboard
|
|
475
|
+
Given I start the TUI application
|
|
476
|
+
When I navigate down 2 times
|
|
477
|
+
And I press enter
|
|
478
|
+
Then I should see "Selected option"
|
|
479
|
+
|
|
480
|
+
@tui
|
|
481
|
+
Scenario: Fill form in TUI
|
|
482
|
+
Given I start the TUI application
|
|
483
|
+
When I enter "John Doe" in the "Name" field
|
|
484
|
+
And I press tab
|
|
485
|
+
And I enter "john@example.com" in the "Email" field
|
|
486
|
+
And I submit the form
|
|
487
|
+
Then I should see "Form submitted successfully"
|
|
488
|
+
|
|
489
|
+
@tui
|
|
490
|
+
Scenario: Verify screen snapshot
|
|
491
|
+
Given I start the TUI application
|
|
492
|
+
Then the screen should match snapshot "main-menu"
|
|
493
|
+
`;
|
|
494
|
+
|
|
495
|
+
const gitignore = `node_modules
|
|
496
|
+
.features-gen
|
|
497
|
+
playwright-report
|
|
498
|
+
test-results
|
|
499
|
+
cucumber-report
|
|
500
|
+
storage
|
|
501
|
+
.env
|
|
502
|
+
`;
|
|
503
|
+
|
|
504
|
+
const envExample = `# API defaults used by the auth and cleanup helpers
|
|
505
|
+
DEFAULT_ADMIN_USERNAME=admin@example.com
|
|
506
|
+
DEFAULT_ADMIN_PASSWORD=changeme
|
|
507
|
+
API_AUTH_LOGIN_PATH=/auth/login
|
|
508
|
+
API_BASE_URL=http://localhost:3000
|
|
509
|
+
|
|
510
|
+
# UI defaults
|
|
511
|
+
FRONTEND_URL=http://localhost:3000
|
|
512
|
+
HEADLESS=true
|
|
513
|
+
|
|
514
|
+
# Cleanup rules (JSON array)
|
|
515
|
+
# CLEANUP_RULES=[{"varMatch":"user","path":"/api/users/{id}"}]
|
|
516
|
+
|
|
517
|
+
# TUI testing (optional)
|
|
518
|
+
# Set DEBUG=true to see TUI tester output
|
|
519
|
+
DEBUG=false
|
|
520
|
+
|
|
521
|
+
# Worker configuration
|
|
522
|
+
# Set to a number for explicit worker count, or "auto" to let Playwright decide
|
|
523
|
+
# In CI, defaults to 1 for stability unless explicitly overridden
|
|
524
|
+
# WORKERS=auto
|
|
525
|
+
`;
|
|
526
|
+
|
|
527
|
+
const readme = `# katalyst-xspec
|
|
528
|
+
|
|
529
|
+
Generated Playwright + BDD test package powered by @esimplicitylabs/katalyst-xspec.
|
|
530
|
+
|
|
531
|
+
## Install
|
|
532
|
+
Install deps in this folder (see commands printed by the generator).
|
|
533
|
+
|
|
534
|
+
## Run
|
|
535
|
+
- Generate tests: \
|
|
536
|
+
\`npm run gen\`
|
|
537
|
+
- Run tests: \
|
|
538
|
+
\`npm test\`
|
|
539
|
+
|
|
540
|
+
## Structure
|
|
541
|
+
- \`features/api|ui|hybrid|tui\`: feature files
|
|
542
|
+
- \`features/steps/steps.ts\`: registers steps from @esimplicitylabs/katalyst-xspec
|
|
543
|
+
- \`features/steps/fixtures.ts\`: creates the Playwright-BDD test with adapters
|
|
544
|
+
- \`playwright.config.ts\`: BDD-aware Playwright config with reporters
|
|
545
|
+
|
|
546
|
+
## Notes
|
|
547
|
+
- Edit \`playwright.config.ts\` projects/tags to match your repo.
|
|
548
|
+
- Keep @playwright/test and playwright-bdd versions aligned with @esimplicitylabs/katalyst-xspec peer ranges.
|
|
549
|
+
|
|
550
|
+
## TUI Testing (Optional)
|
|
551
|
+
To enable terminal user interface testing:
|
|
552
|
+
|
|
553
|
+
1. Install tmux (required by tui-tester):
|
|
554
|
+
\`\`\`bash
|
|
555
|
+
# macOS
|
|
556
|
+
brew install tmux
|
|
557
|
+
|
|
558
|
+
# Ubuntu/Debian
|
|
559
|
+
apt-get install tmux
|
|
560
|
+
\`\`\`
|
|
561
|
+
|
|
562
|
+
2. Install tui-tester:
|
|
563
|
+
\`\`\`bash
|
|
564
|
+
npm install tui-tester
|
|
565
|
+
\`\`\`
|
|
566
|
+
|
|
567
|
+
3. Uncomment TUI configuration in:
|
|
568
|
+
- \`features/steps/fixtures.ts\`: Configure TuiTesterAdapter with your CLI command
|
|
569
|
+
- \`features/steps/steps.ts\`: Uncomment registerTuiSteps(test)
|
|
570
|
+
- \`playwright.config.ts\`: Uncomment tuiBdd project and add to projects array
|
|
571
|
+
|
|
572
|
+
4. Write @tui tagged feature files in \`features/tui/\`
|
|
573
|
+
`;
|
|
574
|
+
|
|
575
|
+
return {
|
|
576
|
+
'package.json': JSON.stringify(pkg, null, 2) + '\n',
|
|
577
|
+
'tsconfig.json': JSON.stringify(tsconfig, null, 2) + '\n',
|
|
578
|
+
'playwright.config.ts': playwrightConfig,
|
|
579
|
+
'features/steps/fixtures.ts': fixturesTs,
|
|
580
|
+
'features/steps/steps.ts': stepsTs,
|
|
581
|
+
'features/api/00_api_examples.feature': apiFeature,
|
|
582
|
+
'features/ui/00_ui_examples.feature': uiFeature,
|
|
583
|
+
'features/hybrid/00_hybrid_examples.feature': hybridFeature,
|
|
584
|
+
'features/tui/00_tui_examples.feature': tuiFeature,
|
|
585
|
+
'.gitignore': gitignore,
|
|
586
|
+
'.env.example': envExample,
|
|
587
|
+
'README.md': readme,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function main() {
|
|
592
|
+
const args = parseArgs(process.argv.slice(2));
|
|
593
|
+
const targetDir = path.resolve(process.cwd(), args.dir);
|
|
594
|
+
const detectedPm = await detectPackageManager(process.cwd());
|
|
595
|
+
const pm = commandsFor(detectedPm);
|
|
596
|
+
const files = templates('katalyst-xspec');
|
|
597
|
+
|
|
598
|
+
console.log(`Detected package manager: ${detectedPm}`);
|
|
599
|
+
|
|
600
|
+
const results = { created: [], skipped: [], merged: [], skills: [] };
|
|
601
|
+
await ensureDir(targetDir);
|
|
602
|
+
|
|
603
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
604
|
+
const filePath = path.join(targetDir, rel);
|
|
605
|
+
// Enable merging for .env.example files
|
|
606
|
+
const merge = rel.endsWith('.env.example');
|
|
607
|
+
await writeFileSafe(filePath, content, { force: args.force, results, merge });
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
console.log(`\nScaffold complete at ${targetDir}`);
|
|
611
|
+
if (results.created.length) {
|
|
612
|
+
console.log('Created files:');
|
|
613
|
+
results.created.forEach((f) => console.log(` + ${path.relative(process.cwd(), f)}`));
|
|
614
|
+
}
|
|
615
|
+
if (results.merged && results.merged.length) {
|
|
616
|
+
console.log('Merged files (new variables added):');
|
|
617
|
+
results.merged.forEach((f) => console.log(` M ${path.relative(process.cwd(), f)}`));
|
|
618
|
+
}
|
|
619
|
+
if (results.skipped.length) {
|
|
620
|
+
console.log('Skipped existing files (use --force to overwrite):');
|
|
621
|
+
results.skipped.forEach((f) => console.log(` ~ ${path.relative(process.cwd(), f)}`));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Skills installation
|
|
625
|
+
let installSkills = args.withSkills;
|
|
626
|
+
if (installSkills === undefined) {
|
|
627
|
+
// Interactive mode - ask user
|
|
628
|
+
installSkills = await promptYesNo('\nWould you like to install Katalyst BDD Agent Skills?');
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (installSkills) {
|
|
632
|
+
let agents = args.skillsAgents;
|
|
633
|
+
if (!agents) {
|
|
634
|
+
// Interactive mode - ask which agents
|
|
635
|
+
const agentOptions = Object.entries(SKILL_AGENT_LABELS).map(([key, label]) => ({ key, label }));
|
|
636
|
+
agents = await promptMultiSelect('Select agents to install skills to:', agentOptions);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
console.log('\nInstalling skills...');
|
|
640
|
+
await copySkillsToAgents(targetDir, agents, results);
|
|
641
|
+
|
|
642
|
+
if (results.skills.length) {
|
|
643
|
+
console.log('Installed skills to:');
|
|
644
|
+
// Group by agent directory
|
|
645
|
+
const byAgent = {};
|
|
646
|
+
for (const skillPath of results.skills) {
|
|
647
|
+
const parts = skillPath.split(path.sep);
|
|
648
|
+
const agentDir = parts.slice(0, -1).join(path.sep);
|
|
649
|
+
if (!byAgent[agentDir]) byAgent[agentDir] = [];
|
|
650
|
+
byAgent[agentDir].push(parts[parts.length - 1]);
|
|
651
|
+
}
|
|
652
|
+
for (const [agentDir, skillNames] of Object.entries(byAgent)) {
|
|
653
|
+
console.log(` ${agentDir}/`);
|
|
654
|
+
skillNames.forEach(name => console.log(` + ${name}/`));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
console.log('\nNext steps:');
|
|
660
|
+
console.log(` 1) cd ${path.relative(process.cwd(), targetDir) || '.'}`);
|
|
661
|
+
console.log(` 2) ${pm.install}`);
|
|
662
|
+
console.log(` 3) ${pm.test}`);
|
|
663
|
+
|
|
664
|
+
if (results.skills.length) {
|
|
665
|
+
console.log('\nSkills installed! Your AI agent can now help you:');
|
|
666
|
+
console.log(' - Create BDD tests with katalyst-bdd-create-test');
|
|
667
|
+
console.log(' - Look up step definitions with katalyst-bdd-step-reference');
|
|
668
|
+
console.log(' - Debug issues with katalyst-bdd-troubleshooting');
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (require.main === module) {
|
|
673
|
+
main().catch((err) => {
|
|
674
|
+
console.error(err);
|
|
675
|
+
process.exit(1);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
module.exports = { main };
|