@glubean/cli 0.1.2

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 (113) hide show
  1. package/bin/gb.js +2 -0
  2. package/dist/commands/init.d.ts +19 -0
  3. package/dist/commands/init.d.ts.map +1 -0
  4. package/dist/commands/init.js +842 -0
  5. package/dist/commands/init.js.map +1 -0
  6. package/dist/commands/login.d.ts +10 -0
  7. package/dist/commands/login.d.ts.map +1 -0
  8. package/dist/commands/login.js +75 -0
  9. package/dist/commands/login.js.map +1 -0
  10. package/dist/commands/patch.d.ts +8 -0
  11. package/dist/commands/patch.d.ts.map +1 -0
  12. package/dist/commands/patch.js +73 -0
  13. package/dist/commands/patch.js.map +1 -0
  14. package/dist/commands/run.d.ts +26 -0
  15. package/dist/commands/run.d.ts.map +1 -0
  16. package/dist/commands/run.js +1093 -0
  17. package/dist/commands/run.js.map +1 -0
  18. package/dist/commands/scan.d.ts +6 -0
  19. package/dist/commands/scan.d.ts.map +1 -0
  20. package/dist/commands/scan.js +62 -0
  21. package/dist/commands/scan.js.map +1 -0
  22. package/dist/commands/spec_split.d.ts +5 -0
  23. package/dist/commands/spec_split.d.ts.map +1 -0
  24. package/dist/commands/spec_split.js +56 -0
  25. package/dist/commands/spec_split.js.map +1 -0
  26. package/dist/commands/sync.d.ts +13 -0
  27. package/dist/commands/sync.d.ts.map +1 -0
  28. package/dist/commands/sync.js +252 -0
  29. package/dist/commands/sync.js.map +1 -0
  30. package/dist/commands/trigger.d.ts +13 -0
  31. package/dist/commands/trigger.d.ts.map +1 -0
  32. package/dist/commands/trigger.js +213 -0
  33. package/dist/commands/trigger.js.map +1 -0
  34. package/dist/commands/validate_metadata.d.ts +6 -0
  35. package/dist/commands/validate_metadata.d.ts.map +1 -0
  36. package/dist/commands/validate_metadata.js +103 -0
  37. package/dist/commands/validate_metadata.js.map +1 -0
  38. package/dist/commands/worker.d.ts +14 -0
  39. package/dist/commands/worker.d.ts.map +1 -0
  40. package/dist/commands/worker.js +10 -0
  41. package/dist/commands/worker.js.map +1 -0
  42. package/dist/lib/auth.d.ts +39 -0
  43. package/dist/lib/auth.d.ts.map +1 -0
  44. package/dist/lib/auth.js +82 -0
  45. package/dist/lib/auth.js.map +1 -0
  46. package/dist/lib/ci.d.ts +12 -0
  47. package/dist/lib/ci.d.ts.map +1 -0
  48. package/dist/lib/ci.js +42 -0
  49. package/dist/lib/ci.js.map +1 -0
  50. package/dist/lib/config.d.ts +116 -0
  51. package/dist/lib/config.d.ts.map +1 -0
  52. package/dist/lib/config.js +264 -0
  53. package/dist/lib/config.js.map +1 -0
  54. package/dist/lib/constants.d.ts +6 -0
  55. package/dist/lib/constants.d.ts.map +1 -0
  56. package/dist/lib/constants.js +6 -0
  57. package/dist/lib/constants.js.map +1 -0
  58. package/dist/lib/env.d.ts +19 -0
  59. package/dist/lib/env.d.ts.map +1 -0
  60. package/dist/lib/env.js +40 -0
  61. package/dist/lib/env.js.map +1 -0
  62. package/dist/lib/git.d.ts +8 -0
  63. package/dist/lib/git.d.ts.map +1 -0
  64. package/dist/lib/git.js +68 -0
  65. package/dist/lib/git.js.map +1 -0
  66. package/dist/lib/openapi_patch.d.ts +23 -0
  67. package/dist/lib/openapi_patch.d.ts.map +1 -0
  68. package/dist/lib/openapi_patch.js +232 -0
  69. package/dist/lib/openapi_patch.js.map +1 -0
  70. package/dist/lib/openapi_split.d.ts +16 -0
  71. package/dist/lib/openapi_split.d.ts.map +1 -0
  72. package/dist/lib/openapi_split.js +188 -0
  73. package/dist/lib/openapi_split.js.map +1 -0
  74. package/dist/lib/upload.d.ts +44 -0
  75. package/dist/lib/upload.d.ts.map +1 -0
  76. package/dist/lib/upload.js +297 -0
  77. package/dist/lib/upload.js.map +1 -0
  78. package/dist/main.d.ts +8 -0
  79. package/dist/main.d.ts.map +1 -0
  80. package/dist/main.js +319 -0
  81. package/dist/main.js.map +1 -0
  82. package/dist/metadata.d.ts +17 -0
  83. package/dist/metadata.d.ts.map +1 -0
  84. package/dist/metadata.js +61 -0
  85. package/dist/metadata.js.map +1 -0
  86. package/dist/update_check.d.ts +14 -0
  87. package/dist/update_check.d.ts.map +1 -0
  88. package/dist/update_check.js +130 -0
  89. package/dist/update_check.js.map +1 -0
  90. package/dist/version.d.ts +5 -0
  91. package/dist/version.d.ts.map +1 -0
  92. package/dist/version.js +11 -0
  93. package/dist/version.js.map +1 -0
  94. package/package.json +34 -0
  95. package/templates/AI-INSTRUCTIONS.md +163 -0
  96. package/templates/README.md +226 -0
  97. package/templates/claude-skill-glubean-test.md +382 -0
  98. package/templates/data/create-user.json +14 -0
  99. package/templates/data/endpoints.csv +5 -0
  100. package/templates/data/scenarios.yaml +19 -0
  101. package/templates/data/search-examples.json +14 -0
  102. package/templates/data/users.json +17 -0
  103. package/templates/data-driven.test.ts.tpl +118 -0
  104. package/templates/demo.test.result.json +398 -0
  105. package/templates/demo.test.ts.tpl +226 -0
  106. package/templates/explore-api.test.result.json +79 -0
  107. package/templates/minimal/README.md +42 -0
  108. package/templates/minimal-api.test.ts.tpl +42 -0
  109. package/templates/minimal-auth.test.ts.tpl +45 -0
  110. package/templates/minimal-search.test.ts.tpl +34 -0
  111. package/templates/openapi.sample.json +97 -0
  112. package/templates/pick.test.result.json +165 -0
  113. package/templates/pick.test.ts.tpl +126 -0
@@ -0,0 +1,842 @@
1
+ /**
2
+ * Init command - scaffolds a new Glubean test project with a 3-step wizard.
3
+ *
4
+ * Step 1: Project Type — Best Practice or Minimal
5
+ * Step 2: API Setup — Base URL and optional OpenAPI spec (Best Practice only)
6
+ * Step 3: Git & CI — Auto-detect/init git, hooks, GitHub Actions (Best Practice only)
7
+ */
8
+ import { readFile, writeFile, stat, mkdir, chmod } from "node:fs/promises";
9
+ import { readFileSync } from "node:fs";
10
+ import { dirname, resolve } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { execFile } from "node:child_process";
13
+ import { confirm, input, select } from "@inquirer/prompts";
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const colors = {
16
+ reset: "\x1b[0m",
17
+ bold: "\x1b[1m",
18
+ dim: "\x1b[2m",
19
+ green: "\x1b[32m",
20
+ yellow: "\x1b[33m",
21
+ cyan: "\x1b[36m",
22
+ };
23
+ // ---------------------------------------------------------------------------
24
+ // Prompt helpers
25
+ // ---------------------------------------------------------------------------
26
+ function isInteractive() {
27
+ return !!process.stdin.isTTY;
28
+ }
29
+ /**
30
+ * True when running in a real TTY (not piped stdin).
31
+ * @inquirer/prompts only works in a real TTY.
32
+ * Piped stdin (used by tests with GLUBEAN_FORCE_INTERACTIVE=1) falls back
33
+ * to the plain readLine-based helpers.
34
+ */
35
+ function useFancyPrompts() {
36
+ return !!process.stdin.isTTY;
37
+ }
38
+ /**
39
+ * Read a line from stdin. Works correctly with both TTY and piped input.
40
+ */
41
+ function readLine(message) {
42
+ return new Promise((res) => {
43
+ process.stdout.write(message + " ");
44
+ let data = "";
45
+ const onData = (chunk) => {
46
+ const str = chunk.toString();
47
+ data += str;
48
+ if (str.includes("\n")) {
49
+ process.stdin.removeListener("data", onData);
50
+ process.stdin.pause();
51
+ res(data.trim());
52
+ }
53
+ };
54
+ process.stdin.resume();
55
+ process.stdin.on("data", onData);
56
+ });
57
+ }
58
+ async function promptYesNo(question, defaultYes) {
59
+ if (useFancyPrompts()) {
60
+ return await confirm({ message: question, default: defaultYes });
61
+ }
62
+ const hint = defaultYes ? "[Y/n]" : "[y/N]";
63
+ while (true) {
64
+ const answer = await readLine(`${question} ${hint}`);
65
+ const normalized = answer.trim().toLowerCase();
66
+ if (!normalized)
67
+ return defaultYes;
68
+ if (normalized === "y" || normalized === "yes")
69
+ return true;
70
+ if (normalized === "n" || normalized === "no")
71
+ return false;
72
+ }
73
+ }
74
+ async function promptChoice(question, options, defaultKey) {
75
+ if (useFancyPrompts()) {
76
+ return await select({
77
+ message: question,
78
+ choices: options.map((o) => ({
79
+ name: `${o.label} ${colors.dim}${o.desc}${colors.reset}`,
80
+ value: o.key,
81
+ })),
82
+ default: defaultKey,
83
+ });
84
+ }
85
+ console.log(` ${question}\n`);
86
+ for (const opt of options) {
87
+ const marker = opt.key === defaultKey ? `${colors.green}❯${colors.reset}` : " ";
88
+ console.log(` ${marker} ${colors.bold}${opt.key}.${colors.reset} ${opt.label} ${colors.dim}${opt.desc}${colors.reset}`);
89
+ }
90
+ console.log();
91
+ while (true) {
92
+ const answer = await readLine(` Enter choice ${colors.dim}[${defaultKey}]${colors.reset}`);
93
+ const trimmed = answer.trim();
94
+ if (!trimmed)
95
+ return defaultKey;
96
+ const match = options.find((o) => o.key === trimmed);
97
+ if (match)
98
+ return match.key;
99
+ }
100
+ }
101
+ function validateBaseUrl(raw) {
102
+ const trimmed = raw.trim();
103
+ if (!trimmed) {
104
+ return { ok: false, reason: "URL cannot be empty." };
105
+ }
106
+ let parsed;
107
+ try {
108
+ parsed = new URL(trimmed);
109
+ }
110
+ catch {
111
+ return {
112
+ ok: false,
113
+ reason: "Must be a valid absolute URL, for example: https://api.example.com",
114
+ };
115
+ }
116
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
117
+ return { ok: false, reason: "Only http:// and https:// are supported." };
118
+ }
119
+ if (!parsed.hostname) {
120
+ return { ok: false, reason: "Hostname is required (for example: localhost)." };
121
+ }
122
+ const normalized = parsed.toString();
123
+ if (parsed.pathname === "/" && !parsed.search && !parsed.hash) {
124
+ return { ok: true, value: normalized.slice(0, -1) };
125
+ }
126
+ return { ok: true, value: normalized };
127
+ }
128
+ function validateBaseUrlOrExit(raw, source) {
129
+ const result = validateBaseUrl(raw);
130
+ if (result.ok)
131
+ return result.value;
132
+ console.error(`Invalid base URL from ${source}: ${result.reason}\n` +
133
+ "Example: --base-url https://api.example.com");
134
+ process.exit(1);
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // File utilities
138
+ // ---------------------------------------------------------------------------
139
+ async function fileExists(path) {
140
+ try {
141
+ await stat(path);
142
+ return true;
143
+ }
144
+ catch {
145
+ return false;
146
+ }
147
+ }
148
+ async function readCliTemplate(relativePath) {
149
+ const templatePath = resolve(__dirname, "../../templates", relativePath);
150
+ return await readFile(templatePath, "utf-8");
151
+ }
152
+ async function resolveContent(content) {
153
+ return typeof content === "function" ? await content() : content;
154
+ }
155
+ // ---------------------------------------------------------------------------
156
+ // Templates — Standard project
157
+ // ---------------------------------------------------------------------------
158
+ function resolveSdkVersion() {
159
+ // Read the SDK version from the CLI's own package.json dependencies
160
+ const pkgPath = resolve(__dirname, "../../package.json");
161
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
162
+ const sdkDep = pkg.dependencies?.["@glubean/sdk"];
163
+ if (!sdkDep) {
164
+ throw new Error('Unable to resolve "@glubean/sdk" dependency from @glubean/cli package.json');
165
+ }
166
+ // Strip workspace: prefix if present, otherwise return as-is
167
+ return sdkDep.replace(/^workspace:\*?/, "latest");
168
+ }
169
+ const SDK_VERSION = resolveSdkVersion();
170
+ function makePackageJson(_baseUrl) {
171
+ return (JSON.stringify({
172
+ name: "my-glubean-tests",
173
+ version: "0.1.0",
174
+ type: "module",
175
+ scripts: {
176
+ test: "gb run",
177
+ "test:verbose": "gb run --verbose",
178
+ "test:staging": "gb run --env-file .env.staging",
179
+ "test:log": "gb run --log-file",
180
+ "test:ci": "gb run --ci --result-json",
181
+ explore: "gb run --explore",
182
+ "explore:verbose": "gb run --explore --verbose",
183
+ scan: "gb scan",
184
+ "validate-metadata": "gb validate-metadata",
185
+ },
186
+ dependencies: {
187
+ "@glubean/sdk": SDK_VERSION,
188
+ },
189
+ glubean: {
190
+ run: {
191
+ verbose: false,
192
+ pretty: true,
193
+ emitFullTrace: false,
194
+ testDir: "./tests",
195
+ exploreDir: "./explore",
196
+ },
197
+ redaction: {
198
+ replacementFormat: "simple",
199
+ },
200
+ },
201
+ }, null, 2) + "\n");
202
+ }
203
+ function makeEnvFile(baseUrl) {
204
+ return `# Environment variables for tests
205
+ BASE_URL=${baseUrl}
206
+ `;
207
+ }
208
+ const ENV_SECRETS = `# Secrets for tests (add this file to .gitignore)
209
+ # DummyJSON test credentials (public, safe to use)
210
+ USERNAME=emilys
211
+ PASSWORD=emilyspass
212
+ `;
213
+ function makeStagingEnvFile(baseUrl) {
214
+ const stagingUrl = baseUrl.replace(/\/\/([^/]+)/, "//staging.$1");
215
+ return `# Staging environment variables
216
+ # Usage: gb run --env-file .env.staging
217
+ BASE_URL=${stagingUrl}
218
+ `;
219
+ }
220
+ const ENV_STAGING_SECRETS = `# Staging secrets (gitignored)
221
+ # Usage: auto-loaded when --env-file .env.staging is used
222
+ # API_KEY=your-staging-api-key
223
+ USERNAME=
224
+ PASSWORD=
225
+ `;
226
+ const GITIGNORE = `# Secrets (all env-specific secrets files)
227
+ .env.secrets
228
+ .env.*.secrets
229
+
230
+ # Log files
231
+ *.log
232
+
233
+ # Result files (generated by glubean run)
234
+ *.result.json
235
+
236
+ # Node
237
+ node_modules/
238
+
239
+ # Glubean internal
240
+ .glubean/
241
+ `;
242
+ const PRE_COMMIT_HOOK = `#!/bin/sh
243
+ set -e
244
+
245
+ gb scan
246
+
247
+ if [ -n "$(git diff --name-only -- metadata.json)" ]; then
248
+ echo "metadata.json updated. Please git add metadata.json"
249
+ exit 1
250
+ fi
251
+ `;
252
+ const PRE_PUSH_HOOK = `#!/bin/sh
253
+ set -e
254
+
255
+ gb validate-metadata
256
+ `;
257
+ const GITHUB_ACTION_METADATA = `name: Glubean Metadata
258
+
259
+ on:
260
+ push:
261
+ branches: [main]
262
+ pull_request:
263
+
264
+ permissions:
265
+ contents: read
266
+
267
+ jobs:
268
+ metadata:
269
+ runs-on: ubuntu-latest
270
+ steps:
271
+ - uses: actions/checkout@v4
272
+ - uses: actions/setup-node@v4
273
+ with:
274
+ node-version: '22'
275
+ - name: Install dependencies
276
+ run: npm ci
277
+ - name: Generate metadata.json
278
+ run: npx gb scan
279
+ - name: Verify metadata.json
280
+ run: git diff --exit-code metadata.json
281
+ `;
282
+ const GITHUB_ACTION_TESTS = `name: Glubean Tests
283
+
284
+ on:
285
+ push:
286
+ branches: [main]
287
+ pull_request:
288
+
289
+ permissions:
290
+ contents: read
291
+
292
+ jobs:
293
+ test:
294
+ runs-on: ubuntu-latest
295
+ steps:
296
+ - uses: actions/checkout@v4
297
+ - uses: actions/setup-node@v4
298
+ with:
299
+ node-version: '22'
300
+
301
+ - name: Install dependencies
302
+ run: npm ci
303
+
304
+ - name: Write secrets
305
+ run: |
306
+ echo "USERNAME=\${{ secrets.USERNAME }}" >> .env.secrets
307
+ echo "PASSWORD=\${{ secrets.PASSWORD }}" >> .env.secrets
308
+
309
+ - name: Run tests
310
+ run: npx gb run --ci --result-json
311
+
312
+ - name: Upload results
313
+ if: always()
314
+ uses: actions/upload-artifact@v4
315
+ with:
316
+ name: test-results
317
+ path: |
318
+ **/*.junit.xml
319
+ **/*.result.json
320
+ `;
321
+ // ---------------------------------------------------------------------------
322
+ // Templates — Minimal project
323
+ // ---------------------------------------------------------------------------
324
+ function makeMinimalPackageJson() {
325
+ return JSON.stringify({
326
+ name: "my-glubean-tests",
327
+ version: "0.1.0",
328
+ type: "module",
329
+ scripts: {
330
+ test: "gb run",
331
+ "test:verbose": "gb run --verbose",
332
+ "test:staging": "gb run --env-file .env.staging",
333
+ "test:ci": "gb run --ci --result-json",
334
+ explore: "gb run --explore --verbose",
335
+ scan: "gb scan",
336
+ },
337
+ dependencies: {
338
+ "@glubean/sdk": SDK_VERSION,
339
+ },
340
+ glubean: {
341
+ run: {
342
+ verbose: true,
343
+ pretty: true,
344
+ testDir: "./tests",
345
+ exploreDir: "./explore",
346
+ },
347
+ },
348
+ }, null, 2) + "\n";
349
+ }
350
+ const MINIMAL_ENV = `# Environment variables
351
+ # Tip: switch environments from the VS Code status bar — one click to toggle
352
+ # between default, staging, and any custom .env.* file.
353
+ BASE_URL=https://dummyjson.com
354
+ `;
355
+ const MINIMAL_ENV_SECRETS = `# Secrets (add this file to .gitignore)
356
+ # DummyJSON test credentials (public, safe to use)
357
+ USERNAME=emilys
358
+ PASSWORD=emilyspass
359
+ `;
360
+ const MINIMAL_ENV_STAGING = `# Staging environment variables
361
+ # Usage: gb run --env-file .env.staging
362
+ # Tip: or switch to "staging" from the VS Code status bar — no CLI flags needed.
363
+ BASE_URL=https://staging.dummyjson.com
364
+ `;
365
+ const MINIMAL_ENV_STAGING_SECRETS = `# Staging secrets (gitignored)
366
+ # Usage: auto-loaded when --env-file .env.staging is used
367
+ # API_KEY=your-staging-api-key
368
+ USERNAME=
369
+ PASSWORD=
370
+ `;
371
+ // ---------------------------------------------------------------------------
372
+ // Dependency installation
373
+ // ---------------------------------------------------------------------------
374
+ async function installDependencies() {
375
+ console.log(`\n${colors.dim}Installing dependencies...${colors.reset}`);
376
+ return new Promise((res) => {
377
+ execFile("npm", ["install"], { encoding: "utf-8" }, (error, _stdout, stderr) => {
378
+ if (!error) {
379
+ console.log(` ${colors.green}✓${colors.reset} Dependencies installed\n`);
380
+ }
381
+ else {
382
+ console.log(` ${colors.yellow}⚠${colors.reset} Failed to install dependencies. Run ${colors.cyan}npm install${colors.reset} manually.`);
383
+ if (stderr?.trim()) {
384
+ console.log(` ${colors.dim}${stderr.trim()}${colors.reset}\n`);
385
+ }
386
+ }
387
+ res();
388
+ });
389
+ });
390
+ }
391
+ // ---------------------------------------------------------------------------
392
+ // Main init command — 3-step wizard
393
+ // ---------------------------------------------------------------------------
394
+ const DEFAULT_BASE_URL = "https://dummyjson.com";
395
+ export async function initCommand(options = {}) {
396
+ console.log(`\n${colors.bold}${colors.cyan}🫘 Glubean Init${colors.reset}\n`);
397
+ const interactive = options.interactive ?? true;
398
+ const forceInteractive = process.env["GLUBEAN_FORCE_INTERACTIVE"] === "1";
399
+ if (interactive && !isInteractive() && !forceInteractive) {
400
+ console.error("Interactive init requires a TTY. Use --no-interactive and pass --hooks/--github-actions flags.");
401
+ process.exit(1);
402
+ }
403
+ // ── Step 1/3 — Project Type ──────────────────────────────────────────────
404
+ let isMinimal = options.minimal ?? false;
405
+ if (interactive && !options.minimal) {
406
+ console.log(`${colors.dim}━━━ Step 1/3 — Project Type ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}\n`);
407
+ const choice = await promptChoice("What would you like to create?", [
408
+ {
409
+ key: "1",
410
+ label: "Best Practice",
411
+ desc: "Full project with tests, CI, multi-env, and examples",
412
+ },
413
+ {
414
+ key: "2",
415
+ label: "Minimal",
416
+ desc: "Quick start — explore folder with GET, POST, and pick examples",
417
+ },
418
+ ], "1");
419
+ isMinimal = choice === "2";
420
+ }
421
+ if (interactive && !options.overwrite) {
422
+ const hasExisting = await fileExists("package.json") ||
423
+ await fileExists(".env");
424
+ if (hasExisting) {
425
+ console.log(`\n ${colors.yellow}⚠${colors.reset} Existing Glubean files detected in this directory.\n`);
426
+ const overwrite = await promptYesNo("Overwrite existing files?", false);
427
+ if (overwrite) {
428
+ options.overwrite = true;
429
+ }
430
+ else {
431
+ console.log(`\n ${colors.dim}Keeping existing files — new files will still be created${colors.reset}\n`);
432
+ }
433
+ }
434
+ }
435
+ if (isMinimal) {
436
+ await initMinimal(options.overwrite ?? false);
437
+ return;
438
+ }
439
+ // ── Step 2/3 — API Setup ─────────────────────────────────────────────────
440
+ let baseUrl = options.baseUrl ? validateBaseUrlOrExit(options.baseUrl, "--base-url") : DEFAULT_BASE_URL;
441
+ if (interactive) {
442
+ console.log(`\n${colors.dim}━━━ Step 2/3 — API Setup ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}\n`);
443
+ if (useFancyPrompts()) {
444
+ const urlInput = await input({
445
+ message: "Your API base URL",
446
+ default: DEFAULT_BASE_URL,
447
+ validate: (value) => {
448
+ if (!value.trim())
449
+ return true;
450
+ const result = validateBaseUrl(value);
451
+ return result.ok || result.reason;
452
+ },
453
+ });
454
+ if (urlInput.trim() && urlInput !== DEFAULT_BASE_URL) {
455
+ const validated = validateBaseUrl(urlInput);
456
+ if (validated.ok)
457
+ baseUrl = validated.value;
458
+ }
459
+ }
460
+ else {
461
+ while (true) {
462
+ const urlInput = await readLine(` Your API base URL ${colors.dim}(Enter for ${DEFAULT_BASE_URL})${colors.reset}`);
463
+ if (!urlInput.trim())
464
+ break;
465
+ const validated = validateBaseUrl(urlInput);
466
+ if (validated.ok) {
467
+ baseUrl = validated.value;
468
+ break;
469
+ }
470
+ console.log(` ${colors.yellow}⚠${colors.reset} Invalid URL: ${validated.reason}`);
471
+ console.log(` ${colors.dim}Try something like: https://api.example.com${colors.reset}\n`);
472
+ }
473
+ }
474
+ console.log(`\n ${colors.green}✓${colors.reset} Base URL: ${colors.cyan}${baseUrl}${colors.reset}`);
475
+ }
476
+ // ── Step 3/3 — Git & CI ──────────────────────────────────────────────────
477
+ let enableHooks = options.hooks;
478
+ let enableActions = options.githubActions;
479
+ let hasGit = await fileExists(".git");
480
+ if (interactive) {
481
+ console.log(`\n${colors.dim}━━━ Step 3/3 — Git & CI ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}\n`);
482
+ if (!hasGit) {
483
+ console.log(` ${colors.yellow}⚠${colors.reset} No Git repository detected\n`);
484
+ const initGit = await promptYesNo("Initialize Git repository? (recommended — enables hooks and CI)", true);
485
+ if (initGit) {
486
+ const success = await new Promise((res) => {
487
+ execFile("git", ["init"], { encoding: "utf-8" }, (error) => {
488
+ res(!error);
489
+ });
490
+ });
491
+ if (success) {
492
+ hasGit = true;
493
+ console.log(`\n ${colors.green}✓${colors.reset} Git repository initialized\n`);
494
+ }
495
+ else {
496
+ console.log(`\n ${colors.yellow}⚠${colors.reset} Failed to initialize Git — skipping hooks and actions\n`);
497
+ }
498
+ }
499
+ else {
500
+ console.log(`\n ${colors.dim}Skipping Git hooks and GitHub Actions${colors.reset}`);
501
+ console.log(` ${colors.dim}Run "git init && gb init --hooks --github-actions" later${colors.reset}\n`);
502
+ }
503
+ }
504
+ else {
505
+ console.log(` ${colors.green}✓${colors.reset} Git repository detected\n`);
506
+ }
507
+ if (hasGit) {
508
+ if (enableHooks === undefined) {
509
+ enableHooks = await promptYesNo("Enable Git hooks? (auto-updates metadata.json on commit)", true);
510
+ }
511
+ if (enableActions === undefined) {
512
+ enableActions = await promptYesNo("Enable GitHub Actions? (CI verifies metadata.json on PR)", true);
513
+ }
514
+ }
515
+ else {
516
+ enableHooks = false;
517
+ enableActions = false;
518
+ }
519
+ }
520
+ else {
521
+ // Non-interactive mode
522
+ if (enableHooks && !hasGit) {
523
+ console.error("Error: --hooks requires a Git repository. Run `git init` first.");
524
+ process.exit(1);
525
+ }
526
+ if (enableHooks === undefined)
527
+ enableHooks = false;
528
+ if (enableActions === undefined)
529
+ enableActions = false;
530
+ }
531
+ // ── Create files ─────────────────────────────────────────────────────────
532
+ console.log(`\n${colors.dim}━━━ Creating project ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${colors.reset}\n`);
533
+ const files = [
534
+ {
535
+ path: "package.json",
536
+ content: makePackageJson(baseUrl),
537
+ description: "Package config with scripts",
538
+ },
539
+ {
540
+ path: ".env",
541
+ content: makeEnvFile(baseUrl),
542
+ description: "Environment variables",
543
+ },
544
+ {
545
+ path: ".env.secrets",
546
+ content: ENV_SECRETS,
547
+ description: "Secret variables",
548
+ },
549
+ {
550
+ path: ".env.staging",
551
+ content: makeStagingEnvFile(baseUrl),
552
+ description: "Staging environment variables",
553
+ },
554
+ {
555
+ path: ".env.staging.secrets",
556
+ content: ENV_STAGING_SECRETS,
557
+ description: "Staging secret variables",
558
+ },
559
+ {
560
+ path: ".gitignore",
561
+ content: GITIGNORE,
562
+ description: "Git ignore rules",
563
+ },
564
+ {
565
+ path: "README.md",
566
+ content: () => readCliTemplate("README.md"),
567
+ description: "Project README",
568
+ },
569
+ {
570
+ path: "context/openapi.sample.json",
571
+ content: () => readCliTemplate("openapi.sample.json"),
572
+ description: "Sample OpenAPI spec (mock)",
573
+ },
574
+ {
575
+ path: "tests/demo.test.ts",
576
+ content: () => readCliTemplate("demo.test.ts.tpl"),
577
+ description: "Demo tests (rich output for dashboard preview)",
578
+ },
579
+ {
580
+ path: "tests/data-driven.test.ts",
581
+ content: () => readCliTemplate("data-driven.test.ts.tpl"),
582
+ description: "Data-driven test examples (JSON, CSV, YAML)",
583
+ },
584
+ {
585
+ path: "tests/pick.test.ts",
586
+ content: () => readCliTemplate("pick.test.ts.tpl"),
587
+ description: "Example selection with test.pick (inline + JSON)",
588
+ },
589
+ {
590
+ path: "data/users.json",
591
+ content: () => readCliTemplate("data/users.json"),
592
+ description: "Sample JSON test data",
593
+ },
594
+ {
595
+ path: "data/endpoints.csv",
596
+ content: () => readCliTemplate("data/endpoints.csv"),
597
+ description: "Sample CSV test data",
598
+ },
599
+ {
600
+ path: "data/scenarios.yaml",
601
+ content: () => readCliTemplate("data/scenarios.yaml"),
602
+ description: "Sample YAML test data",
603
+ },
604
+ {
605
+ path: "data/create-user.json",
606
+ content: () => readCliTemplate("data/create-user.json"),
607
+ description: "Named examples for test.pick",
608
+ },
609
+ {
610
+ path: "explore/api.test.ts",
611
+ content: () => readCliTemplate("minimal-api.test.ts.tpl"),
612
+ description: "Explore — GET and POST basics",
613
+ },
614
+ {
615
+ path: "explore/search.test.ts",
616
+ content: () => readCliTemplate("minimal-search.test.ts.tpl"),
617
+ description: "Explore — parameterized search with test.pick",
618
+ },
619
+ {
620
+ path: "explore/auth.test.ts",
621
+ content: () => readCliTemplate("minimal-auth.test.ts.tpl"),
622
+ description: "Explore — multi-step auth flow",
623
+ },
624
+ {
625
+ path: "data/search-examples.json",
626
+ content: () => readCliTemplate("data/search-examples.json"),
627
+ description: "Search examples for test.pick",
628
+ },
629
+ {
630
+ path: "CLAUDE.md",
631
+ content: () => readCliTemplate("AI-INSTRUCTIONS.md"),
632
+ description: "AI instructions (Claude Code, Cursor)",
633
+ },
634
+ {
635
+ path: "AGENTS.md",
636
+ content: () => readCliTemplate("AI-INSTRUCTIONS.md"),
637
+ description: "AI instructions (Codex, other agents)",
638
+ },
639
+ {
640
+ path: ".claude/skills/gb/SKILL.md",
641
+ content: () => readCliTemplate("claude-skill-glubean-test.md"),
642
+ description: "Claude Code skill — /gb test generator",
643
+ },
644
+ ];
645
+ if (enableHooks) {
646
+ files.push({
647
+ path: ".git/hooks/pre-commit",
648
+ content: PRE_COMMIT_HOOK,
649
+ description: "Git pre-commit hook",
650
+ }, {
651
+ path: ".git/hooks/pre-push",
652
+ content: PRE_PUSH_HOOK,
653
+ description: "Git pre-push hook",
654
+ });
655
+ }
656
+ if (enableActions) {
657
+ files.push({
658
+ path: ".github/workflows/glubean-metadata.yml",
659
+ content: GITHUB_ACTION_METADATA,
660
+ description: "GitHub Actions metadata workflow",
661
+ }, {
662
+ path: ".github/workflows/glubean-tests.yml",
663
+ content: GITHUB_ACTION_TESTS,
664
+ description: "GitHub Actions test workflow",
665
+ });
666
+ }
667
+ let created = 0;
668
+ let skipped = 0;
669
+ let overwritten = 0;
670
+ const shouldOverwrite = (path) => {
671
+ if (options.overwrite)
672
+ return true;
673
+ if (options.overwriteHooks && path.startsWith(".git/hooks/"))
674
+ return true;
675
+ if (options.overwriteActions &&
676
+ path.startsWith(".github/workflows/glubean-")) {
677
+ return true;
678
+ }
679
+ return false;
680
+ };
681
+ for (const file of files) {
682
+ const existedBefore = await fileExists(file.path);
683
+ if (existedBefore) {
684
+ if (!shouldOverwrite(file.path)) {
685
+ console.log(` ${colors.dim}skip${colors.reset} ${file.path} (already exists)`);
686
+ skipped++;
687
+ continue;
688
+ }
689
+ }
690
+ const parentDir = file.path.substring(0, file.path.lastIndexOf("/"));
691
+ if (parentDir) {
692
+ await mkdir(parentDir, { recursive: true });
693
+ }
694
+ const content = await resolveContent(file.content);
695
+ await writeFile(file.path, content, "utf-8");
696
+ if (file.path.startsWith(".git/hooks/")) {
697
+ try {
698
+ await chmod(file.path, 0o755);
699
+ }
700
+ catch {
701
+ // Ignore chmod errors on unsupported platforms
702
+ }
703
+ }
704
+ if (existedBefore && shouldOverwrite(file.path)) {
705
+ console.log(` ${colors.yellow}overwrite${colors.reset} ${file.path} - ${file.description}`);
706
+ overwritten++;
707
+ }
708
+ else {
709
+ console.log(` ${colors.green}create${colors.reset} ${file.path} - ${file.description}`);
710
+ created++;
711
+ }
712
+ }
713
+ console.log(`\n${colors.bold}Summary:${colors.reset} ${created} created, ${overwritten} overwritten, ${skipped} skipped\n`);
714
+ if (created > 0) {
715
+ await installDependencies();
716
+ console.log(`${colors.bold}Next steps:${colors.reset}`);
717
+ console.log(` 1. Run ${colors.cyan}npm test${colors.reset} to run all tests in tests/`);
718
+ console.log(` 2. Run ${colors.cyan}npm run test:verbose${colors.reset} for detailed output`);
719
+ console.log(` 3. Run ${colors.cyan}npm run explore${colors.reset} to run explore/ tests`);
720
+ console.log(` 4. Keep ${colors.cyan}CLAUDE.md${colors.reset} or ${colors.cyan}AGENTS.md${colors.reset} — delete whichever you don't need`);
721
+ console.log(` 5. Drop your OpenAPI spec in ${colors.cyan}context/${colors.reset} for AI-assisted test writing\n`);
722
+ }
723
+ }
724
+ // ---------------------------------------------------------------------------
725
+ // Minimal init
726
+ // ---------------------------------------------------------------------------
727
+ async function initMinimal(overwrite) {
728
+ console.log(`${colors.dim} Quick start — explore APIs with GET, POST, and pick examples${colors.reset}\n`);
729
+ const files = [
730
+ {
731
+ path: "package.json",
732
+ content: makeMinimalPackageJson(),
733
+ description: "Package config with explore scripts",
734
+ },
735
+ {
736
+ path: ".env",
737
+ content: MINIMAL_ENV,
738
+ description: "Environment variables",
739
+ },
740
+ {
741
+ path: ".env.secrets",
742
+ content: MINIMAL_ENV_SECRETS,
743
+ description: "Secret variables (placeholder)",
744
+ },
745
+ {
746
+ path: ".env.staging",
747
+ content: MINIMAL_ENV_STAGING,
748
+ description: "Staging environment variables",
749
+ },
750
+ {
751
+ path: ".env.staging.secrets",
752
+ content: MINIMAL_ENV_STAGING_SECRETS,
753
+ description: "Staging secret variables",
754
+ },
755
+ {
756
+ path: ".gitignore",
757
+ content: GITIGNORE,
758
+ description: "Git ignore rules",
759
+ },
760
+ {
761
+ path: "README.md",
762
+ content: () => readCliTemplate("minimal/README.md"),
763
+ description: "Project README",
764
+ },
765
+ {
766
+ path: "tests/demo.test.ts",
767
+ content: () => readCliTemplate("demo.test.ts.tpl"),
768
+ description: "Demo tests (GET, POST, auth flow, pagination)",
769
+ },
770
+ {
771
+ path: "explore/api.test.ts",
772
+ content: () => readCliTemplate("minimal-api.test.ts.tpl"),
773
+ description: "GET and POST examples",
774
+ },
775
+ {
776
+ path: "explore/search.test.ts",
777
+ content: () => readCliTemplate("minimal-search.test.ts.tpl"),
778
+ description: "Parameterized search with test.pick",
779
+ },
780
+ {
781
+ path: "explore/auth.test.ts",
782
+ content: () => readCliTemplate("minimal-auth.test.ts.tpl"),
783
+ description: "Multi-step auth flow (login → profile)",
784
+ },
785
+ {
786
+ path: "data/search-examples.json",
787
+ content: () => readCliTemplate("data/search-examples.json"),
788
+ description: "Search parameters for pick examples",
789
+ },
790
+ {
791
+ path: "CLAUDE.md",
792
+ content: () => readCliTemplate("AI-INSTRUCTIONS.md"),
793
+ description: "AI instructions (Claude Code, Cursor)",
794
+ },
795
+ {
796
+ path: "AGENTS.md",
797
+ content: () => readCliTemplate("AI-INSTRUCTIONS.md"),
798
+ description: "AI instructions (Codex, other agents)",
799
+ },
800
+ {
801
+ path: ".claude/skills/gb/SKILL.md",
802
+ content: () => readCliTemplate("claude-skill-glubean-test.md"),
803
+ description: "Claude Code skill — /gb test generator",
804
+ },
805
+ ];
806
+ let created = 0;
807
+ let skipped = 0;
808
+ let overwritten = 0;
809
+ for (const file of files) {
810
+ const existedBefore = await fileExists(file.path);
811
+ if (existedBefore && !overwrite) {
812
+ console.log(` ${colors.dim}skip${colors.reset} ${file.path} (already exists)`);
813
+ skipped++;
814
+ continue;
815
+ }
816
+ const parentDir = file.path.substring(0, file.path.lastIndexOf("/"));
817
+ if (parentDir) {
818
+ await mkdir(parentDir, { recursive: true });
819
+ }
820
+ const content = await resolveContent(file.content);
821
+ await writeFile(file.path, content, "utf-8");
822
+ if (existedBefore) {
823
+ console.log(` ${colors.yellow}overwrite${colors.reset} ${file.path} - ${file.description}`);
824
+ overwritten++;
825
+ }
826
+ else {
827
+ console.log(` ${colors.green}create${colors.reset} ${file.path} - ${file.description}`);
828
+ created++;
829
+ }
830
+ }
831
+ console.log(`\n${colors.bold}Summary:${colors.reset} ${created} created, ${overwritten} overwritten, ${skipped} skipped\n`);
832
+ if (created > 0) {
833
+ await installDependencies();
834
+ console.log(`${colors.bold}Next steps:${colors.reset}`);
835
+ console.log(` 1. Run ${colors.cyan}npm run explore${colors.reset} to run all explore tests`);
836
+ console.log(` 2. Open ${colors.cyan}explore/api.test.ts${colors.reset} — GET and POST basics`);
837
+ console.log(` 3. Open ${colors.cyan}explore/search.test.ts${colors.reset} — pick examples with external data`);
838
+ console.log(` 4. Open ${colors.cyan}explore/auth.test.ts${colors.reset} — multi-step flow with state`);
839
+ console.log(` 5. Read ${colors.cyan}README.md${colors.reset} for links and next steps\n`);
840
+ }
841
+ }
842
+ //# sourceMappingURL=init.js.map