@testspectra/cli 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/commands/init.js +138 -65
  2. package/package.json +2 -3
@@ -1,9 +1,10 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { fileURLToPath } from "url";
4
+ import * as p from "@clack/prompts";
5
+ import chalk from "chalk";
4
6
  import { ConfigLoader } from "../config/loader.js";
5
7
  import { TypeGenerator } from "../types/generator.js";
6
- import inquirer from "inquirer";
7
8
  function parsePnpmWorkspaceGlobs(content) {
8
9
  const lines = content.split("\n");
9
10
  const globs = [];
@@ -27,6 +28,54 @@ function parsePnpmWorkspaceGlobs(content) {
27
28
  }
28
29
  return globs;
29
30
  }
31
+ function ensurePnpmWorkspaceIncludes(pnpmWorkspacePath, targetPath) {
32
+ const normalizedTarget = targetPath.replace(/\\/g, "/");
33
+ if (!fs.existsSync(pnpmWorkspacePath)) {
34
+ const newContent = `packages:\n - '${normalizedTarget}'\n`;
35
+ fs.writeFileSync(pnpmWorkspacePath, newContent, "utf-8");
36
+ return;
37
+ }
38
+ const content = fs.readFileSync(pnpmWorkspacePath, "utf-8");
39
+ const currentGlobs = parsePnpmWorkspaceGlobs(content);
40
+ // Check if target is already covered by an existing glob or exact path
41
+ const isAlreadyCovered = currentGlobs.some((glob) => {
42
+ const normGlob = glob.replace(/\\/g, "/");
43
+ if (normGlob === normalizedTarget)
44
+ return true;
45
+ if (normGlob === `${normalizedTarget}/*` || normGlob === `${normalizedTarget}/**`)
46
+ return true;
47
+ if (normGlob.endsWith("/*")) {
48
+ const parent = normGlob.slice(0, -2);
49
+ if (normalizedTarget.startsWith(`${parent}/`) && normalizedTarget.split("/").length === parent.split("/").length + 1)
50
+ return true;
51
+ }
52
+ if (normGlob.endsWith("/**")) {
53
+ const parent = normGlob.slice(0, -3);
54
+ if (normalizedTarget.startsWith(`${parent}/`))
55
+ return true;
56
+ }
57
+ return false;
58
+ });
59
+ if (!isAlreadyCovered) {
60
+ // Insert new package pattern right under packages:
61
+ const lines = content.split("\n");
62
+ let insertIndex = -1;
63
+ for (let i = 0; i < lines.length; i++) {
64
+ if (lines[i].trim().startsWith("packages:")) {
65
+ insertIndex = i + 1;
66
+ break;
67
+ }
68
+ }
69
+ if (insertIndex !== -1) {
70
+ lines.splice(insertIndex, 0, ` - '${normalizedTarget}'`);
71
+ fs.writeFileSync(pnpmWorkspacePath, lines.join("\n"), "utf-8");
72
+ }
73
+ else {
74
+ const newContent = `packages:\n - '${normalizedTarget}'\n` + content;
75
+ fs.writeFileSync(pnpmWorkspacePath, newContent, "utf-8");
76
+ }
77
+ }
78
+ }
30
79
  function scanDirectoriesForProjects(cwd, patterns) {
31
80
  const foundProjects = [];
32
81
  const visited = new Set();
@@ -112,9 +161,10 @@ export async function initCommand(options = {}) {
112
161
  const cwd = process.cwd();
113
162
  const existingConfig = ConfigLoader.findConfigFile(cwd);
114
163
  if (existingConfig && existingConfig === path.join(cwd, path.basename(existingConfig)) && !options.force) {
115
- console.log(`\x1b[33m[TestSpectra]\x1b[0m Config already exists at ${path.basename(existingConfig)}. Use --force to overwrite.`);
164
+ p.log.warn(chalk.yellow(`Config already exists at ${path.basename(existingConfig)}. Use --force to overwrite.`));
116
165
  return;
117
166
  }
167
+ p.intro(chalk.bold.cyan("✨ TestSpectra Enterprise Scaffolder"));
118
168
  // Detect monorepo environment
119
169
  let isMonorepo = false;
120
170
  let pnpmPatterns = [];
@@ -134,20 +184,27 @@ export async function initCommand(options = {}) {
134
184
  const detectedProjects = isMonorepo ? scanDirectoriesForProjects(cwd, pnpmPatterns) : [];
135
185
  let selectedTemplate = options.template;
136
186
  if (!selectedTemplate && !options.force) {
137
- const choices = [
138
- { name: "Standard (Standalone or single project)", value: "default" },
139
- { name: "Nx Monorepo (Interactive multi-module feature discovery & centralized config)", value: "nx" },
140
- ];
141
- const answers = await inquirer.prompt([
142
- {
143
- type: "list",
144
- name: "template",
145
- message: "Select TestSpectra setup mode:",
146
- choices,
147
- default: isMonorepo ? "nx" : "default",
148
- },
149
- ]);
150
- selectedTemplate = answers.template;
187
+ const templateChoice = await p.select({
188
+ message: "Select TestSpectra setup mode:",
189
+ initialValue: isMonorepo ? "nx" : "default",
190
+ options: [
191
+ {
192
+ label: "Nx Monorepo (Recommended)",
193
+ value: "nx",
194
+ hint: "Interactive workspace feature discovery & centralized config",
195
+ },
196
+ {
197
+ label: "Standard Project",
198
+ value: "default",
199
+ hint: "Standalone single-suite E2E testing project",
200
+ },
201
+ ],
202
+ });
203
+ if (p.isCancel(templateChoice)) {
204
+ p.cancel("Setup cancelled.");
205
+ return;
206
+ }
207
+ selectedTemplate = templateChoice;
151
208
  }
152
209
  const templateName = selectedTemplate || (isMonorepo ? "nx" : "default");
153
210
  // 1. Resolve template directory
@@ -161,10 +218,11 @@ export async function initCommand(options = {}) {
161
218
  templateDir = path.resolve(__dirname, `../../../templates/${templateName}`);
162
219
  }
163
220
  if (!fs.existsSync(templateDir)) {
164
- throw new Error(`[TestSpectra] Scaffold template directory not found at: ${templateDir}`);
221
+ p.cancel(chalk.red(`Template directory not found: ${templateDir}`));
222
+ return;
165
223
  }
166
224
  // 2. Resolve CLI dependency version
167
- let cliVersion = "^1.0.17";
225
+ let cliVersion = "^1.0.21";
168
226
  try {
169
227
  const cliPackageJsonPath = path.resolve(__dirname, "../../package.json");
170
228
  if (fs.existsSync(cliPackageJsonPath)) {
@@ -234,47 +292,53 @@ export async function initCommand(options = {}) {
234
292
  }
235
293
  // --- MONOREPO INTERACTIVE FLOW ---
236
294
  if (templateName === "nx" && (detectedProjects.length > 0 || isMonorepo)) {
237
- console.log(`\n\x1b[36m[TestSpectra Monorepo Discovery]\x1b[0m`);
238
- console.log(`Found ${detectedProjects.length} workspace feature project(s).`);
239
295
  let chosenProjects = [];
240
296
  if (detectedProjects.length > 0) {
241
- const projectAnswers = await inquirer.prompt([
242
- {
243
- type: "checkbox",
244
- name: "selectedProjects",
245
- message: "Select workspace features where TestSpectra should be initialized:",
246
- choices: detectedProjects.map((p) => ({
247
- name: `${p.relPath} (${p.name})${p.hasNxProject ? " [Nx project.json]" : ""}`,
248
- value: p.relPath,
249
- checked: true,
250
- })),
251
- },
252
- ]);
253
- chosenProjects = projectAnswers.selectedProjects;
254
- }
255
- const folderAnswers = await inquirer.prompt([
256
- {
257
- type: "input",
258
- name: "e2eFolderName",
259
- message: "Enter E2E test folder name for each selected feature:",
260
- default: "e2e",
261
- },
262
- {
263
- type: "input",
264
- name: "sharedTestingPath",
265
- message: "Enter path for Shared Testing library (POMs, Steps, Actions, Fixtures):",
266
- default: "shared/testing",
267
- },
268
- ]);
269
- const e2eFolderName = folderAnswers.e2eFolderName.trim() || "e2e";
270
- const sharedTestingRelPath = folderAnswers.sharedTestingPath.trim() || "shared/testing";
297
+ p.log.step(chalk.cyan(`Monorepo Auto-Discovery: Found ${detectedProjects.length} workspace feature package(s)`));
298
+ const projectChoices = await p.multiselect({
299
+ message: "Select workspace features to initialize with TestSpectra:",
300
+ options: detectedProjects.map((proj) => ({
301
+ value: proj.relPath,
302
+ label: `${proj.relPath} (${proj.name})`,
303
+ hint: proj.hasNxProject ? "Nx project.json" : "package.json",
304
+ })),
305
+ initialValues: detectedProjects.map((p) => p.relPath),
306
+ required: true,
307
+ });
308
+ if (p.isCancel(projectChoices)) {
309
+ p.cancel("Setup cancelled.");
310
+ return;
311
+ }
312
+ chosenProjects = projectChoices;
313
+ }
314
+ const e2eFolderNameInput = await p.text({
315
+ message: "Enter E2E test folder name for each selected feature:",
316
+ placeholder: "e2e",
317
+ defaultValue: "e2e",
318
+ });
319
+ if (p.isCancel(e2eFolderNameInput)) {
320
+ p.cancel("Setup cancelled.");
321
+ return;
322
+ }
323
+ const sharedTestingPathInput = await p.text({
324
+ message: "Enter path for Shared Testing library (POMs, Steps, Actions, Fixtures):",
325
+ placeholder: "shared/testing",
326
+ defaultValue: "shared/testing",
327
+ });
328
+ if (p.isCancel(sharedTestingPathInput)) {
329
+ p.cancel("Setup cancelled.");
330
+ return;
331
+ }
332
+ const e2eFolderName = e2eFolderNameInput.trim() || "e2e";
333
+ const sharedTestingRelPath = sharedTestingPathInput.trim() || "shared/testing";
271
334
  const sharedTestingAbsPath = path.join(cwd, sharedTestingRelPath);
335
+ const s = p.spinner();
336
+ s.start("Scaffolding distributed monorepo structure and ambient types...");
272
337
  // 1. Centralized Root Config
273
338
  const rootConfigSrc = path.join(templateDir, "spectra.config.ts");
274
339
  const rootConfigDest = path.join(cwd, "spectra.config.ts");
275
340
  if (fs.existsSync(rootConfigSrc) && (!fs.existsSync(rootConfigDest) || options.force)) {
276
341
  fs.copyFileSync(rootConfigSrc, rootConfigDest);
277
- console.log(`\x1b[32m[TestSpectra]\x1b[0m Created centralized configuration at ./spectra.config.ts`);
278
342
  }
279
343
  // 2. Centralized Root .testspectra folder for cache/logs
280
344
  const rootTestDataDir = path.join(cwd, ".testspectra");
@@ -285,7 +349,6 @@ export async function initCommand(options = {}) {
285
349
  const sharedTestingSrc = path.join(templateDir, "shared/testing");
286
350
  if (fs.existsSync(sharedTestingSrc)) {
287
351
  copyRecursive(sharedTestingSrc, sharedTestingAbsPath);
288
- console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized shared testing library at ./${sharedTestingRelPath}`);
289
352
  }
290
353
  // 4. Distribute E2E Folders to Selected Feature Modules
291
354
  const sampleE2eSrc = path.join(templateDir, "modules/auth/e2e");
@@ -318,9 +381,14 @@ export async function initCommand(options = {}) {
318
381
  if (fs.existsSync(localConfig))
319
382
  fs.unlinkSync(localConfig);
320
383
  featureE2eDirs.push(targetE2eDir);
321
- console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized ${e2eProjectName} in ./${path.relative(cwd, targetE2eDir)}`);
322
384
  }
323
- // 5. Update Root Solution tsconfig.json references
385
+ // 5. Ensure pnpm-workspace.yaml includes shared library and all feature E2E directories
386
+ const pnpmWorkspaceFilePath = path.join(cwd, "pnpm-workspace.yaml");
387
+ ensurePnpmWorkspaceIncludes(pnpmWorkspaceFilePath, sharedTestingRelPath);
388
+ for (const e2eDir of featureE2eDirs) {
389
+ ensurePnpmWorkspaceIncludes(pnpmWorkspaceFilePath, path.relative(cwd, e2eDir));
390
+ }
391
+ // 6. Update Root Solution tsconfig.json references
324
392
  const rootTsConfigPath = path.join(cwd, "tsconfig.json");
325
393
  const references = [];
326
394
  for (const e2eDir of featureE2eDirs) {
@@ -344,12 +412,13 @@ export async function initCommand(options = {}) {
344
412
  else {
345
413
  rootTsConfig = { files: [], references };
346
414
  }
347
- // 6. Generate Ambient Types for Shared + All E2E Feature Directories
415
+ fs.writeFileSync(rootTsConfigPath, JSON.stringify(rootTsConfig, null, 2), "utf-8");
416
+ // 7. Generate Ambient Types for Shared + All E2E Feature Directories
348
417
  TypeGenerator.writeDeclarationFiles(cwd);
349
418
  for (const e2eDir of featureE2eDirs) {
350
419
  TypeGenerator.writeDeclarationFiles(e2eDir);
351
420
  }
352
- // 7. Generate ARCHITECTURE.md in root documenting the exact generated layout
421
+ // 8. Generate ARCHITECTURE.md in root documenting the exact generated layout
353
422
  const archDocContent = `# TestSpectra Enterprise Monorepo Architecture
354
423
 
355
424
  This workspace uses TestSpectra's **"Centralized Configuration, Distributed Implementation"** model for automated cross-platform testing.
@@ -431,15 +500,18 @@ pnpm type-check
431
500
  const readmeContent = `# ${projectName}\n\nAutomated cross-platform E2E testing powered by [TestSpectra](https://github.com/testspectra).\n${archSection}`;
432
501
  fs.writeFileSync(rootReadmePath, readmeContent, "utf-8");
433
502
  }
434
- console.log(`\n\x1b[32m[TestSpectra]\x1b[0m Enterprise Nx Monorepo initialized successfully!`);
435
- console.log(`\x1b[36m- Centralized Configuration:\x1b[0m ./spectra.config.ts`);
436
- console.log(`\x1b[36m- Centralized App Data/Cache:\x1b[0m ./.testspectra/`);
437
- console.log(`\x1b[36m- Shared Testing Library:\x1b[0m ./${sharedTestingRelPath}`);
438
- console.log(`\x1b[36m- Architecture Documentation:\x1b[0m ./ARCHITECTURE.md`);
439
- console.log(`\x1b[36m- Initialized Features (${featureE2eDirs.length}):\x1b[0m\n ${featureE2eDirs.map((d) => path.relative(cwd, d)).join("\n ")}`);
503
+ s.stop("Monorepo scaffolding complete!");
504
+ p.log.success(chalk.green("Centralized Configuration: ./spectra.config.ts"));
505
+ p.log.success(chalk.green("Centralized App Data/Cache: ./.testspectra/"));
506
+ p.log.success(chalk.green(`Shared Testing Library: ./${sharedTestingRelPath}`));
507
+ p.log.success(chalk.green("Architecture Documentation: ./ARCHITECTURE.md"));
508
+ p.log.message(chalk.cyan(`Initialized Features (${featureE2eDirs.length}):\n${featureE2eDirs.map((d) => ` - ./${path.relative(cwd, d)}`).join("\n")}`));
509
+ p.outro(chalk.bold.green("🎉 Enterprise Nx Monorepo ready for testing!"));
440
510
  return;
441
511
  }
442
512
  // --- STANDALONE / DEFAULT PROJECT FLOW ---
513
+ const s = p.spinner();
514
+ s.start("Scaffolding standalone TestSpectra project...");
443
515
  copyRecursive(templateDir, cwd);
444
516
  if (!isInternalWorkspace) {
445
517
  const pnpmWorkspacePath = path.join(cwd, "pnpm-workspace.yaml");
@@ -491,7 +563,8 @@ pnpm type-check
491
563
  const standaloneReadme = `# ${projectName}\n\nTestSpectra automated testing project.\n${standaloneArchSection}`;
492
564
  fs.writeFileSync(standaloneReadmePath, standaloneReadme, "utf-8");
493
565
  }
494
- console.log(`\x1b[32m[TestSpectra]\x1b[0m Initialized project from template successfully!`);
495
- console.log(`\x1b[32m[TestSpectra]\x1b[0m Generated documentation at ./ARCHITECTURE.md`);
496
- console.log(`\x1b[32m[TestSpectra]\x1b[0m Generated ambient multi-platform types in .testspectra/types/`);
566
+ s.stop("Standalone project ready!");
567
+ p.log.success(chalk.green("Generated ambient multi-platform types in .testspectra/types/"));
568
+ p.log.success(chalk.green("Architecture documentation created at ./ARCHITECTURE.md"));
569
+ p.outro(chalk.bold.green("🎉 Project initialized successfully!"));
497
570
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testspectra/cli",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "TestSpectra Zero-Config Cross-Platform Test Runner CLI",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -20,16 +20,15 @@
20
20
  "README.md"
21
21
  ],
22
22
  "dependencies": {
23
+ "@clack/prompts": "^1.7.0",
23
24
  "@testspectra/matchers": "^1.0.0",
24
25
  "chalk": "^5.3.0",
25
26
  "commander": "^12.1.0",
26
27
  "dotenv": "^16.4.5",
27
- "inquirer": "^9.3.2",
28
28
  "ora": "^8.0.1",
29
29
  "zod": "^3.23.8"
30
30
  },
31
31
  "devDependencies": {
32
- "@types/inquirer": "^9.0.7",
33
32
  "@types/node": "^20.14.0",
34
33
  "typescript": "^5.4.5"
35
34
  },