web-tester-for-claude 0.4.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 +21 -0
- package/README.md +651 -0
- package/bin/web-tester.js +35 -0
- package/package.json +64 -0
- package/src/browser/attrs.ts +79 -0
- package/src/browser/session.ts +139 -0
- package/src/cli.ts +1488 -0
- package/src/impact.ts +165 -0
- package/src/init.ts +260 -0
- package/src/inspector/capture.ts +293 -0
- package/src/inspector/deep.ts +147 -0
- package/src/inspector/packs.ts +98 -0
- package/src/inspector/report.ts +667 -0
- package/src/inspector/run.ts +544 -0
- package/src/inspector/steps.ts +380 -0
- package/src/inspector/summarise.ts +178 -0
- package/src/inspector/verdict.ts +275 -0
- package/src/journeys.ts +78 -0
- package/src/kb.ts +84 -0
- package/src/map/classify.ts +149 -0
- package/src/map/crawl.ts +394 -0
- package/src/map/generate.ts +253 -0
- package/src/map/report.ts +112 -0
- package/src/map/run.ts +219 -0
- package/src/sitemap.ts +75 -0
- package/src/sweep.ts +476 -0
- package/src/templates/agent-section.md +77 -0
- package/src/templates/dot-web-tester/impact-rules.json +36 -0
- package/src/templates/dot-web-tester/instructions/getting-started.md +62 -0
- package/src/templates/dot-web-tester/instructions/recipes.md +105 -0
- package/src/templates/dot-web-tester/journeys/example-signup.json +17 -0
- package/src/templates/dot-web-tester/urls-smoke.txt +19 -0
- package/src/templates/skill.md +59 -0
- package/src/util/log.ts +26 -0
- package/src/util/paths.ts +141 -0
- package/src/util/prompt.ts +50 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
Copy-paste one-liners for common web-tester runs. Add new entries as you
|
|
4
|
+
discover new flows so future sessions don't re-derive the step grammar.
|
|
5
|
+
|
|
6
|
+
## Format
|
|
7
|
+
|
|
8
|
+
Each recipe has:
|
|
9
|
+
|
|
10
|
+
- A **title** (one line, action-oriented: "Verify the contact form submits")
|
|
11
|
+
- A **when** clause (one line — the trigger that should pull this recipe)
|
|
12
|
+
- A **command** block — the exact shell line, ready to copy
|
|
13
|
+
- An **expected outcome** line — what "pass" looks like
|
|
14
|
+
|
|
15
|
+
## Template
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
### <title>
|
|
19
|
+
|
|
20
|
+
**When:** <one-line trigger>
|
|
21
|
+
|
|
22
|
+
**Command:**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
web-tester inspect "<path>" \
|
|
26
|
+
--step settle --quick \
|
|
27
|
+
--expect "<assertion>" \
|
|
28
|
+
--fail-on http-5xx
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Expected:** <one line — what verdict / evidence proves the run worked>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Built-in starters
|
|
37
|
+
|
|
38
|
+
### Smoke-check the homepage
|
|
39
|
+
|
|
40
|
+
**When:** "is the site up?" / "did my change break the homepage?"
|
|
41
|
+
|
|
42
|
+
**Command:**
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
web-tester inspect / \
|
|
46
|
+
--step wait:networkidle --quick \
|
|
47
|
+
--expect "selector=header" \
|
|
48
|
+
--expect "selector=footer" \
|
|
49
|
+
--expect "selector=main" \
|
|
50
|
+
--fail-on http-5xx
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Expected:** verdict: pass, all three selectors visible.
|
|
54
|
+
|
|
55
|
+
### Verify a form submits and lands on a thank-you page
|
|
56
|
+
|
|
57
|
+
**When:** "did my form change still work?"
|
|
58
|
+
|
|
59
|
+
**Command:**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
web-tester inspect "/contact" \
|
|
63
|
+
--step "wait:networkidle" \
|
|
64
|
+
--step "fill:input[name=email]=test@example.com" \
|
|
65
|
+
--step "fill:textarea[name=message]=hello from web-tester" \
|
|
66
|
+
--step "click:button[type=submit]" \
|
|
67
|
+
--step "wait:url-contains:/thanks@10000" \
|
|
68
|
+
--quick \
|
|
69
|
+
--expect "text=Thanks" \
|
|
70
|
+
--fail-on http-5xx
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Expected:** URL transitions to `/thanks`, "Thanks" text visible.
|
|
74
|
+
|
|
75
|
+
### Sweep your smoke URL list against localhost
|
|
76
|
+
|
|
77
|
+
**When:** structural change (layout / header / shared component) — broad regression check.
|
|
78
|
+
|
|
79
|
+
**Command:**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
web-tester sweep --preset smoke --fail-on http-5xx
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Expected:** every URL reports ok in the sweep summary; the HTML report opens to a green table.
|
|
86
|
+
|
|
87
|
+
### Diff-aware advisory run
|
|
88
|
+
|
|
89
|
+
**When:** about to push — "what might my diff have broken?"
|
|
90
|
+
|
|
91
|
+
**Command:**
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
web-tester impact
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Expected:** plan prints matching rules, then advisory findings (or "nothing flagged"). Exits 0 either way.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Project-specific recipes
|
|
102
|
+
|
|
103
|
+
<!-- Add your project's recipes below. Copy the template above, fill it in,
|
|
104
|
+
point the URL at whatever you're testing. `web-tester map` can generate
|
|
105
|
+
a starter set of these for you. -->
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Example: user signs up and lands on a dashboard. Edit the URL, selectors, and expectations to match your app.",
|
|
3
|
+
"url": "/signup",
|
|
4
|
+
"steps": [
|
|
5
|
+
"wait:networkidle",
|
|
6
|
+
"fill:input[name=email]=test@example.com",
|
|
7
|
+
"fill:input[name=password]=hunter2-hunter2",
|
|
8
|
+
"click:button[type=submit]",
|
|
9
|
+
"wait:url-contains:/dashboard@15000"
|
|
10
|
+
],
|
|
11
|
+
"expectations": [
|
|
12
|
+
"text=Welcome",
|
|
13
|
+
"selector=[data-test=dashboard]"
|
|
14
|
+
],
|
|
15
|
+
"failOn": "http-5xx",
|
|
16
|
+
"persistMs": 0
|
|
17
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# urls-smoke.txt — high-value pages to sweep on any structural change.
|
|
2
|
+
#
|
|
3
|
+
# One path per line. `#pack=<name>` annotations apply a built-in expectation
|
|
4
|
+
# pack to that URL. Built-in packs:
|
|
5
|
+
# homepage — header + footer present
|
|
6
|
+
# static — header + footer present
|
|
7
|
+
# category — header + footer + at least one internal anchor with an <img> in <main>
|
|
8
|
+
# has-main — <main> present
|
|
9
|
+
# has-h1 — <h1> present
|
|
10
|
+
#
|
|
11
|
+
# Run: web-tester sweep --preset smoke --fail-on http-5xx
|
|
12
|
+
#
|
|
13
|
+
# These are placeholders — replace with your real routes (or generate this
|
|
14
|
+
# file with `web-tester map`).
|
|
15
|
+
|
|
16
|
+
/ #pack=homepage
|
|
17
|
+
/about #pack=static
|
|
18
|
+
/pricing #pack=has-h1
|
|
19
|
+
/contact #pack=static
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-tester
|
|
3
|
+
description: Drive the running dev site in a real browser (Playwright) to reproduce bugs, verify changes, and read runtime behavior — console, network, page errors, DOM, screenshots, video. Use this FIRST for any "does X work", bug-reproduction, "this page renders wrong", "the state is off", or "verify my change" question, before reading source. Also use to map a site, sweep many URLs, or run a saved journey.
|
|
4
|
+
allowed-tools: Bash(npx web-tester-for-claude *), Bash(web-tester *), Bash(npx web-tester *), Bash(npm run web-tester *), Bash(pnpm web-tester *), Read(**)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Driving the site with web-tester
|
|
8
|
+
|
|
9
|
+
For any runtime-behavior question, reach for web-tester **before** Read/Grep. The
|
|
10
|
+
browser is the source of truth for runtime bugs; read code *after* the run,
|
|
11
|
+
guided by what the run shows.
|
|
12
|
+
|
|
13
|
+
## The core loop
|
|
14
|
+
|
|
15
|
+
1. **Run it** against your **localhost** dev server, always with `--quick` and
|
|
16
|
+
`--fail-on http-5xx`. Add `--expect` for the specific thing you're checking:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx web-tester-for-claude inspect "/products/widget" \
|
|
20
|
+
--step settle --quick \
|
|
21
|
+
--expect "text=Add to Cart" \
|
|
22
|
+
--fail-on http-5xx
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
2. **Read the report** at the path the CLI prints — `result.json` for
|
|
26
|
+
programmatic reads (`ok`, `verdictTriggers`, `expectations`, `pageErrors`,
|
|
27
|
+
`console.entries`, `network.entries`, `steps[N].evalResult`), `report.html`
|
|
28
|
+
to scrub the video. **Only then** open source files.
|
|
29
|
+
|
|
30
|
+
3. **When the DOM isn't enough**, re-run with `--deep`: it adds request/response
|
|
31
|
+
bodies, the **local-scope variables at every uncaught exception**, and
|
|
32
|
+
unhandled promise rejections (`result.json` → `deepErrors`,
|
|
33
|
+
`unhandledRejections`, `network.entries[].responseBody`).
|
|
34
|
+
|
|
35
|
+
## Commands
|
|
36
|
+
|
|
37
|
+
| Command | Use it for |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `inspect <url> [--step …]` | Drive one page / flow, capture everything. |
|
|
40
|
+
| `sweep --preset <name>` | Health-check many URLs in parallel. |
|
|
41
|
+
| `journey <name>` | Run a saved flow from `.web-tester/journeys/`. |
|
|
42
|
+
| `map` | Crawl the site and auto-generate a preset, recipes, and journey drafts. |
|
|
43
|
+
| `impact` | Diff-aware advisory run (reads `.web-tester/impact-rules.json`). |
|
|
44
|
+
| `kb [topic]` | List/print project recipe notes in `.web-tester/instructions/`. |
|
|
45
|
+
|
|
46
|
+
## Before you write a `--step` chain from scratch
|
|
47
|
+
|
|
48
|
+
Run `web-tester kb` first — the project's recipes live there. Step grammar
|
|
49
|
+
gotchas: `click:` takes a Playwright CSS locator (not `role=`); `settle` only
|
|
50
|
+
does something on pages with `[data-attr-name]` markers — otherwise prefer
|
|
51
|
+
`wait:networkidle`. Run `web-tester help` for the full grammar.
|
|
52
|
+
|
|
53
|
+
## Don't
|
|
54
|
+
|
|
55
|
+
- Don't verify a **local** change against **prod** — prod doesn't have your edit.
|
|
56
|
+
localhost is the default and the right target.
|
|
57
|
+
- Don't `--fail-on page-errors` by default — most apps have baseline framework
|
|
58
|
+
warnings. `http-5xx` is the safe gate.
|
|
59
|
+
- Don't trust a single `--expect` for async/derived state — add `--persist 2500`.
|
package/src/util/log.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
|
|
3
|
+
const COLORS = {
|
|
4
|
+
reset: "\x1b[0m",
|
|
5
|
+
dim: "\x1b[2m",
|
|
6
|
+
red: "\x1b[31m",
|
|
7
|
+
green: "\x1b[32m",
|
|
8
|
+
yellow: "\x1b[33m",
|
|
9
|
+
blue: "\x1b[34m",
|
|
10
|
+
cyan: "\x1b[36m"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const log = {
|
|
14
|
+
info: (msg: string): void => console.log(msg),
|
|
15
|
+
dim: (msg: string): void =>
|
|
16
|
+
console.log(`${COLORS.dim}${msg}${COLORS.reset}`),
|
|
17
|
+
ok: (msg: string): void =>
|
|
18
|
+
console.log(`${COLORS.green}${msg}${COLORS.reset}`),
|
|
19
|
+
warn: (msg: string): void =>
|
|
20
|
+
console.log(`${COLORS.yellow}${msg}${COLORS.reset}`),
|
|
21
|
+
fail: (msg: string): void => console.log(`${COLORS.red}${msg}${COLORS.reset}`),
|
|
22
|
+
step: (msg: string): void => console.log(`${COLORS.cyan}${msg}${COLORS.reset}`),
|
|
23
|
+
header: (msg: string): void =>
|
|
24
|
+
console.log(`${COLORS.blue}\n=== ${msg} ===${COLORS.reset}`),
|
|
25
|
+
raw: (msg: string): void => console.log(msg)
|
|
26
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
/** Root of the installed package (used to resolve bundled templates). */
|
|
6
|
+
export const PACKAGE_ROOT = resolve(__dirname, "../..");
|
|
7
|
+
|
|
8
|
+
/** Bundled scaffolding consumed by `web-tester init`. */
|
|
9
|
+
export const TEMPLATES_DIR = resolve(PACKAGE_ROOT, "src/templates");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Where run artifacts are written. Defaults to `.web-tester/runs/` in the
|
|
13
|
+
* project the CLI was invoked from, so output is namespaced under one dir
|
|
14
|
+
* (which `init` adds to `.gitignore`) rather than dropping a stray top-level
|
|
15
|
+
* `runs/`. Override with `WEB_TESTER_RUNS_DIR`.
|
|
16
|
+
*/
|
|
17
|
+
export const RUNS_DIR = process.env.WEB_TESTER_RUNS_DIR
|
|
18
|
+
? resolve(process.env.WEB_TESTER_RUNS_DIR)
|
|
19
|
+
: resolve(process.cwd(), ".web-tester", "runs");
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Project config lives in `.web-tester/` at the project root, resolved
|
|
23
|
+
* against `process.cwd()`. Everything in it is optional:
|
|
24
|
+
*
|
|
25
|
+
* .web-tester/
|
|
26
|
+
* impact-rules.json rules for `web-tester impact`
|
|
27
|
+
* urls-<name>.txt URL presets for `web-tester sweep --preset <name>`
|
|
28
|
+
* journeys/<name>.json named flows for `web-tester journey <name>`
|
|
29
|
+
* instructions/*.md knowledge base for `web-tester kb [topic]`
|
|
30
|
+
*/
|
|
31
|
+
export const USER_CONFIG_DIRNAME = ".web-tester";
|
|
32
|
+
|
|
33
|
+
export function userConfigDir(cwd: string = process.cwd()): string {
|
|
34
|
+
return resolve(cwd, USER_CONFIG_DIRNAME);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function userImpactRulesPath(cwd: string = process.cwd()): string {
|
|
38
|
+
return resolve(userConfigDir(cwd), "impact-rules.json");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function userJourneysDir(cwd: string = process.cwd()): string {
|
|
42
|
+
return resolve(userConfigDir(cwd), "journeys");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function userPresetsDir(cwd: string = process.cwd()): string {
|
|
46
|
+
// Presets are flat `urls-<name>.txt` files at the top of `.web-tester/`.
|
|
47
|
+
return userConfigDir(cwd);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* KB markdown locations, in priority order: `.web-tester/instructions/`
|
|
52
|
+
* first, then `.web-tester/` itself. First existing dir wins.
|
|
53
|
+
*/
|
|
54
|
+
export function userKnowledgeDirs(cwd: string = process.cwd()): string[] {
|
|
55
|
+
return [resolve(userConfigDir(cwd), "instructions"), userConfigDir(cwd)];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `.web-tester/config.json` — persistent project defaults written by `init`. */
|
|
59
|
+
export function projectConfigPath(cwd: string = process.cwd()): string {
|
|
60
|
+
return resolve(userConfigDir(cwd), "config.json");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type ProjectConfig = {
|
|
64
|
+
/** Default base URL, used when `WEB_TESTER_BASE_URL` isn't set. */
|
|
65
|
+
baseUrl?: string;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Read `.web-tester/config.json`. Returns {} when missing or unreadable. */
|
|
69
|
+
export function readProjectConfig(cwd: string = process.cwd()): ProjectConfig {
|
|
70
|
+
try {
|
|
71
|
+
const path = projectConfigPath(cwd);
|
|
72
|
+
if (!existsSync(path)) return {};
|
|
73
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as ProjectConfig;
|
|
74
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
75
|
+
} catch {
|
|
76
|
+
return {};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The project's `.claude/` integration directory and the files `init` touches. */
|
|
81
|
+
export function claudeDir(cwd: string = process.cwd()): string {
|
|
82
|
+
return resolve(cwd, ".claude");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function claudeSettingsLocalPath(cwd: string = process.cwd()): string {
|
|
86
|
+
return resolve(claudeDir(cwd), "settings.local.json");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** `.claude/skills/web-tester/SKILL.md` — the generated Claude Code skill. */
|
|
90
|
+
export function claudeSkillPath(cwd: string = process.cwd()): string {
|
|
91
|
+
return resolve(claudeDir(cwd), "skills", "web-tester", "SKILL.md");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Machine-local state in `~/.web-tester/`, kept outside the repo so saved
|
|
96
|
+
* auth state is never committed. Holds the Playwright `storageState` dump
|
|
97
|
+
* (cookies + localStorage) when a run persists a session.
|
|
98
|
+
*/
|
|
99
|
+
export const WEB_TESTER_HOME = resolve(homedir(), ".web-tester");
|
|
100
|
+
export const SESSION_STATE_PATH = resolve(WEB_TESTER_HOME, "session.json");
|
|
101
|
+
|
|
102
|
+
export function ensureWebTesterHome(): void {
|
|
103
|
+
mkdirSync(WEB_TESTER_HOME, { recursive: true });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function newRunId(): string {
|
|
107
|
+
const now = new Date();
|
|
108
|
+
const pad = (n: number): string => String(n).padStart(2, "0");
|
|
109
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(
|
|
110
|
+
now.getDate()
|
|
111
|
+
)}T${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type RunPaths = {
|
|
115
|
+
runId: string;
|
|
116
|
+
runDir: string;
|
|
117
|
+
stepsDir: string;
|
|
118
|
+
videoDir: string;
|
|
119
|
+
resultPath: string;
|
|
120
|
+
reportHtmlPath: string;
|
|
121
|
+
consolePath: string;
|
|
122
|
+
networkPath: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export function ensureRunPaths(runId: string): RunPaths {
|
|
126
|
+
const runDir = resolve(RUNS_DIR, runId);
|
|
127
|
+
const stepsDir = resolve(runDir, "steps");
|
|
128
|
+
const videoDir = resolve(runDir, "video");
|
|
129
|
+
mkdirSync(stepsDir, { recursive: true });
|
|
130
|
+
mkdirSync(videoDir, { recursive: true });
|
|
131
|
+
return {
|
|
132
|
+
runId,
|
|
133
|
+
runDir,
|
|
134
|
+
stepsDir,
|
|
135
|
+
videoDir,
|
|
136
|
+
resultPath: resolve(runDir, "result.json"),
|
|
137
|
+
reportHtmlPath: resolve(runDir, "report.html"),
|
|
138
|
+
consolePath: resolve(runDir, "console.json"),
|
|
139
|
+
networkPath: resolve(runDir, "network.json")
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
|
|
3
|
+
/** True when we can run an interactive prompt (both ends are a TTY). */
|
|
4
|
+
export function isInteractive(): boolean {
|
|
5
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function question(query: string): Promise<string> {
|
|
9
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
+
return new Promise<string>((resolve) => {
|
|
11
|
+
rl.question(query, (answer) => {
|
|
12
|
+
rl.close();
|
|
13
|
+
resolve(answer);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Free-text prompt with a default shown in brackets. Empty input → default. */
|
|
19
|
+
export async function ask(label: string, def: string): Promise<string> {
|
|
20
|
+
const answer = (await question(`${label} [${def}]: `)).trim();
|
|
21
|
+
return answer || def;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Yes/no prompt. Empty input → default. */
|
|
25
|
+
export async function confirm(label: string, def: boolean): Promise<boolean> {
|
|
26
|
+
const hint = def ? "Y/n" : "y/N";
|
|
27
|
+
const answer = (await question(`${label} (${hint}): `)).trim().toLowerCase();
|
|
28
|
+
if (!answer) return def;
|
|
29
|
+
return answer === "y" || answer === "yes";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pick one of `options`. Accepts a full value or a unique prefix
|
|
34
|
+
* (case-insensitive). Empty input → default.
|
|
35
|
+
*/
|
|
36
|
+
export async function choice<T extends string>(
|
|
37
|
+
label: string,
|
|
38
|
+
options: readonly T[],
|
|
39
|
+
def: T
|
|
40
|
+
): Promise<T> {
|
|
41
|
+
const rendered = options
|
|
42
|
+
.map((o) => (o === def ? o.toUpperCase() : o))
|
|
43
|
+
.join("/");
|
|
44
|
+
const answer = (await question(`${label} [${rendered}]: `)).trim().toLowerCase();
|
|
45
|
+
if (!answer) return def;
|
|
46
|
+
const match = options.find(
|
|
47
|
+
(o) => o.toLowerCase() === answer || o.toLowerCase().startsWith(answer)
|
|
48
|
+
);
|
|
49
|
+
return match ?? def;
|
|
50
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*.ts"]
|
|
14
|
+
}
|