@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.
- package/bin/gb.js +2 -0
- package/dist/commands/init.d.ts +19 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +842 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +10 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +75 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/patch.d.ts +8 -0
- package/dist/commands/patch.d.ts.map +1 -0
- package/dist/commands/patch.js +73 -0
- package/dist/commands/patch.js.map +1 -0
- package/dist/commands/run.d.ts +26 -0
- package/dist/commands/run.d.ts.map +1 -0
- package/dist/commands/run.js +1093 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/scan.d.ts +6 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +62 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/spec_split.d.ts +5 -0
- package/dist/commands/spec_split.d.ts.map +1 -0
- package/dist/commands/spec_split.js +56 -0
- package/dist/commands/spec_split.js.map +1 -0
- package/dist/commands/sync.d.ts +13 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +252 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/trigger.d.ts +13 -0
- package/dist/commands/trigger.d.ts.map +1 -0
- package/dist/commands/trigger.js +213 -0
- package/dist/commands/trigger.js.map +1 -0
- package/dist/commands/validate_metadata.d.ts +6 -0
- package/dist/commands/validate_metadata.d.ts.map +1 -0
- package/dist/commands/validate_metadata.js +103 -0
- package/dist/commands/validate_metadata.js.map +1 -0
- package/dist/commands/worker.d.ts +14 -0
- package/dist/commands/worker.d.ts.map +1 -0
- package/dist/commands/worker.js +10 -0
- package/dist/commands/worker.js.map +1 -0
- package/dist/lib/auth.d.ts +39 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +82 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/ci.d.ts +12 -0
- package/dist/lib/ci.d.ts.map +1 -0
- package/dist/lib/ci.js +42 -0
- package/dist/lib/ci.js.map +1 -0
- package/dist/lib/config.d.ts +116 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +264 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/constants.d.ts +6 -0
- package/dist/lib/constants.d.ts.map +1 -0
- package/dist/lib/constants.js +6 -0
- package/dist/lib/constants.js.map +1 -0
- package/dist/lib/env.d.ts +19 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +40 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/git.d.ts +8 -0
- package/dist/lib/git.d.ts.map +1 -0
- package/dist/lib/git.js +68 -0
- package/dist/lib/git.js.map +1 -0
- package/dist/lib/openapi_patch.d.ts +23 -0
- package/dist/lib/openapi_patch.d.ts.map +1 -0
- package/dist/lib/openapi_patch.js +232 -0
- package/dist/lib/openapi_patch.js.map +1 -0
- package/dist/lib/openapi_split.d.ts +16 -0
- package/dist/lib/openapi_split.d.ts.map +1 -0
- package/dist/lib/openapi_split.js +188 -0
- package/dist/lib/openapi_split.js.map +1 -0
- package/dist/lib/upload.d.ts +44 -0
- package/dist/lib/upload.d.ts.map +1 -0
- package/dist/lib/upload.js +297 -0
- package/dist/lib/upload.js.map +1 -0
- package/dist/main.d.ts +8 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +319 -0
- package/dist/main.js.map +1 -0
- package/dist/metadata.d.ts +17 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +61 -0
- package/dist/metadata.js.map +1 -0
- package/dist/update_check.d.ts +14 -0
- package/dist/update_check.d.ts.map +1 -0
- package/dist/update_check.js +130 -0
- package/dist/update_check.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +11 -0
- package/dist/version.js.map +1 -0
- package/package.json +34 -0
- package/templates/AI-INSTRUCTIONS.md +163 -0
- package/templates/README.md +226 -0
- package/templates/claude-skill-glubean-test.md +382 -0
- package/templates/data/create-user.json +14 -0
- package/templates/data/endpoints.csv +5 -0
- package/templates/data/scenarios.yaml +19 -0
- package/templates/data/search-examples.json +14 -0
- package/templates/data/users.json +17 -0
- package/templates/data-driven.test.ts.tpl +118 -0
- package/templates/demo.test.result.json +398 -0
- package/templates/demo.test.ts.tpl +226 -0
- package/templates/explore-api.test.result.json +79 -0
- package/templates/minimal/README.md +42 -0
- package/templates/minimal-api.test.ts.tpl +42 -0
- package/templates/minimal-auth.test.ts.tpl +45 -0
- package/templates/minimal-search.test.ts.tpl +34 -0
- package/templates/openapi.sample.json +97 -0
- package/templates/pick.test.result.json +165 -0
- 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
|