diff --git a/.eslintrc.json b/.eslintrc.json
deleted file mode 100644
index 83479e8..0000000
--- a/.eslintrc.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "root": true,
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "ecmaVersion": 6,
- "sourceType": "module"
- },
- "plugins": [
- "@typescript-eslint"
- ],
- "rules": {
- "@typescript-eslint/class-name-casing": "warn",
- "@typescript-eslint/semi": "warn",
- "curly": "warn",
- "eqeqeq": "warn",
- "no-throw-literal": "warn",
- "semi": "off"
- }
-}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..ccc8051
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,7 @@
+# Enforce LF for all text files — prevents the CRLF spurious-diff problem
+* text=auto eol=lf
+
+# Binary assets
+*.png binary
+*.gif binary
+*.vsix binary
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..e4fdda9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,55 @@
+name: Bug report
+description: Something inserted wrong, errored, or didn't behave as documented
+labels: [ "bug" ]
+body:
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened
+ description: What did you see, and what did you expect instead?
+ placeholder: 'e.g. "Insert Random: Record…" with a CSV shape inserted an unescaped comma inside a quoted field…'
+ validations:
+ required: true
+ - type: input
+ id: command
+ attributes:
+ label: Command used
+ placeholder: 'e.g. Insert Random: UUID · Pick… · Record… · Generate Dataset… · Randomize Selection'
+ validations:
+ required: true
+ - type: textarea
+ id: repro
+ attributes:
+ label: Steps to reproduce
+ placeholder: |
+ 1. Open a .sql file
+ 2. Add 3 cursors
+ 3. Run "Insert Random: Person"
+ validations:
+ required: true
+ - type: textarea
+ id: settings
+ attributes:
+ label: Relevant settings
+ description: Any non-default settings — insertType, withQuote, bulkCount, outputFormat, seed, locale, strictUnique, dateFormat, recordFormat…
+ placeholder: |
+ insertType: Cursor
+ insertRandomText.bulkCount: 10
+ insertRandomText.seed: 42
+ - type: dropdown
+ id: environment
+ attributes:
+ label: Where does it happen?
+ options:
+ - VS Code desktop
+ - vscode.dev / github.dev (browser)
+ - VSCodium / Cursor / Windsurf (Open VSX build)
+ validations:
+ required: true
+ - type: input
+ id: versions
+ attributes:
+ label: VS Code version · extension version
+ placeholder: e.g. 1.102.0 · 1.0.0
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
new file mode 100644
index 0000000..95cd7bf
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -0,0 +1,22 @@
+name: Feature request
+description: A new data type, format option, or capability you're missing
+labels: [ "enhancement" ]
+body:
+ - type: textarea
+ id: what
+ attributes:
+ label: What do you want to generate or do?
+ placeholder: e.g. IPv6 CIDR ranges · a "no weekends" option on Date (Between…) · Markdown table output format
+ validations:
+ required: true
+ - type: textarea
+ id: example
+ attributes:
+ label: Example of the output you'd expect
+ placeholder: |
+ 2001:db8:3c4d::/48
+ - type: textarea
+ id: workaround
+ attributes:
+ label: How do you handle it today?
+ description: A template/pattern workaround, another tool, by hand… (helps us judge urgency, and sometimes there's already a way — happy to point it out)
diff --git a/.gitignore b/.gitignore
index 5fe00fe..5d63fe8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,26 @@
+# Build output
out
+dist
+*.tsbuildinfo
+
+# Dependencies
node_modules
+
+# VS Code extension testing & packaging
.vscode-test/
*.vsix
+
+# Local-only working files — not tracked, not shipped, excluded from release & audits
+/docs/
+/qa/
+
+# Logs
+*.log
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Test coverage output
+coverage/
+.nyc_output/
diff --git a/.vscode-test.mjs b/.vscode-test.mjs
new file mode 100644
index 0000000..f6b6399
--- /dev/null
+++ b/.vscode-test.mjs
@@ -0,0 +1,25 @@
+import { defineConfig } from '@vscode/test-cli';
+import os from 'node:os';
+import path from 'node:path';
+
+// VS Code's default user-data-dir lives inside the project (.vscode-test/user-data). On deep project
+// paths that pushes the Extension Host IPC socket past macOS's ~103-char unix-socket limit
+// (listen EINVAL: ...-main.sock). Relocate it to a short temp path so `npm test` runs anywhere.
+const userDataDir = path.join(os.tmpdir(), 'irt-vscode-test');
+
+export default defineConfig({
+ tests: [
+ {
+ files: 'out/test/**/*.test.js',
+ mocha: {
+ ui: 'bdd',
+ },
+ launchArgs: [ '--user-data-dir', userDataDir ],
+ },
+ ],
+ coverage: {
+ includeAll: true,
+ exclude: [ '**/test/**', '**/*.test.*' ],
+ reporter: [ 'text', 'html' ],
+ },
+});
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 3ac9aeb..d7a3ca1 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,7 +1,5 @@
{
- // See http://go.microsoft.com/fwlink/?LinkId=827846
- // for the documentation about the extensions.json format
- "recommendations": [
- "dbaeumer.vscode-eslint"
- ]
+ // See http://go.microsoft.com/fwlink/?LinkId=827846
+ // for the documentation about the extensions.json format
+ "recommendations": ["dbaeumer.vscode-eslint", "connor4312.esbuild-problem-matchers", "ms-vscode.extension-test-runner"]
}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index b1fbaf5..b99c706 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -9,26 +9,37 @@
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
- "runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
- "${workspaceFolder}/out/**/*.js"
+ "${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
},
{
- "name": "Extension Tests",
+ "name": "Run Extension (QA Workspace)",
"type": "extensionHost",
"request": "launch",
- "runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
- "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
+ "${workspaceFolder}/qa/workspace"
],
"outFiles": [
- "${workspaceFolder}/out/test/**/*.js"
+ "${workspaceFolder}/dist/**/*.js"
+ ],
+ "preLaunchTask": "${defaultBuildTask}"
+ },
+ {
+ "name": "Run Extension (Demo Workspace)",
+ "type": "extensionHost",
+ "request": "launch",
+ "args": [
+ "--extensionDevelopmentPath=${workspaceFolder}",
+ "${workspaceFolder}/qa/demo-workspace"
+ ],
+ "outFiles": [
+ "${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 30bf8c2..16a5c02 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,11 +1,13 @@
// Place your settings in this file to overwrite default and user settings.
{
- "files.exclude": {
- "out": false // set this to true to hide the "out" folder with the compiled JS files
- },
- "search.exclude": {
- "out": true // set this to false to include "out" folder in search results
- },
- // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
- "typescript.tsc.autoDetect": "off"
-}
\ No newline at end of file
+ "files.exclude": {
+ "out": false, // set this to true to hide the "out" folder with the compiled JS files
+ "dist": false // set this to true to hide the "dist" folder with the compiled JS files
+ },
+ "search.exclude": {
+ "out": true, // set this to false to include "out" folder in search results
+ "dist": true // set this to false to include "dist" folder in search results
+ },
+ // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
+ "typescript.tsc.autoDetect": "off"
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
index 3b17e53..db5b3ad 100644
--- a/.vscode/tasks.json
+++ b/.vscode/tasks.json
@@ -4,10 +4,11 @@
"version": "2.0.0",
"tasks": [
{
- "type": "npm",
- "script": "watch",
- "problemMatcher": "$tsc-watch",
- "isBackground": true,
+ "label": "watch",
+ "dependsOn": [
+ "npm: watch:tsc",
+ "npm: watch:esbuild"
+ ],
"presentation": {
"reveal": "never"
},
@@ -15,6 +16,49 @@
"kind": "build",
"isDefault": true
}
+ },
+ {
+ "type": "npm",
+ "script": "watch:esbuild",
+ "group": "build",
+ "problemMatcher": "$esbuild-watch",
+ "isBackground": true,
+ "label": "npm: watch:esbuild",
+ "presentation": {
+ "group": "watch",
+ "reveal": "never"
+ }
+ },
+ {
+ "type": "npm",
+ "script": "watch:tsc",
+ "group": "build",
+ "problemMatcher": "$tsc-watch",
+ "isBackground": true,
+ "label": "npm: watch:tsc",
+ "presentation": {
+ "group": "watch",
+ "reveal": "never"
+ }
+ },
+ {
+ "type": "npm",
+ "script": "watch-tests",
+ "problemMatcher": "$tsc-watch",
+ "isBackground": true,
+ "presentation": {
+ "reveal": "never",
+ "group": "watchers"
+ },
+ "group": "build"
+ },
+ {
+ "label": "tasks: watch-tests",
+ "dependsOn": [
+ "npm: watch",
+ "npm: watch-tests"
+ ],
+ "problemMatcher": []
}
]
}
diff --git a/.vscodeignore b/.vscodeignore
index c06f4a7..ee4b801 100644
--- a/.vscodeignore
+++ b/.vscodeignore
@@ -1,10 +1,24 @@
.vscode/**
.vscode-test/**
-out/test/**
+.github/**
+out/**
+node_modules/**
+coverage/**
+.nyc_output/**
src/**
+qa/**
+docs/**
.gitignore
-vsc-extension-quickstart.md
+.gitattributes
+esbuild.js
**/tsconfig.json
-**/.eslintrc.json
+**/eslint.config.mjs
**/*.map
**/*.ts
+**/.vscode-test.*
+package-lock.json
+CLAUDE.md
+SPEC.md
+images/*.gif
+.claude/**
+**/.DS_Store
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4da34a4..6383242 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,27 +1,77 @@
# Changelog
-## v0.1.3 (2020-7-25)
+## v1.0.0 (2026-07-10)
-### Changes
+The first stable release — a ground-up relaunch as **Random, Fake & Mock Data Generator**.
+
+### Features
+
+- Switched the random engine to [`@faker-js/faker`](https://fakerjs.dev) for realistic, coherent data.
+- Expanded the catalog to **~130 data types** — on top of the originals (names, dates, numbers, strings, lorem, hashes, …), added email, UUID, company, commerce, finance (credit card, IBAN, currency, crypto), git, system/files, vehicle, food, music, travel, color variants, and richer identity / location / date / number / internet types, plus IDs like ULID & nanoid. Every type is a searchable command and a grouped Quick Pick entry.
+- **Multi-cursor fill** — insert a different value at every cursor in one step.
+- New **Insert Random: Pick…** command — choose any type from a searchable, grouped menu.
+- **Clipboard insert type** — set `insertType` to `Clipboard` to copy a generated value to the clipboard instead of inserting it (no editor needed; resolves #4).
+- **Automatic quoting** — inserted values wrap in the correct quote for the file's language with zero configuration, so they're always valid syntax: SQL dialects use single quotes and escape an embedded quote by doubling it (`'O''Brien'`); every other language (JSON, Go, Java, Rust, JS/TS, Python, and the rest) uses double quotes. No quote settings to configure.
+- **Multi-field records** — new **Insert Random: Record…** command: multi-select any fields and insert them together as one record — a JSON object, a SQL `INSERT` row, or a CSV line — at every cursor. Set the shape with `insertRandomText.recordFormat` (`json` / `sql` / `csv`) and the SQL table name with `insertRandomText.recordSqlTable`. Respects bulk count (a JSON array / repeated rows), multi-cursor, seed, and the insert type (cursors / top of file / clipboard).
+- **Generate whole datasets** — new **Insert Random: Generate Dataset…** command: pick fields (custom lists included), a format, and a row count, and up to **100,000 rows** open as a new untitled file — a JSON array (one record per line), SQL `INSERT` statements, or CSV **with a header row**. The format pick starts on your `recordFormat`, the row count on your bulk count (large counts confirm first); locale, seed, and date format apply, so a seeded run regenerates the identical dataset. Instant and offline, works with no editor open.
+- New settings: `insertRandomText.uniquePerCursor`, `insertRandomText.strictUnique`, `insertRandomText.seed` (reproducible output), `insertRandomText.locale`, `insertRandomText.bulkCount`, `insertRandomText.outputFormat` (`plain` / `jsonArray` / `quotedList`), `insertRandomText.dateFormat`, `insertRandomText.recordFormat`, `insertRandomText.recordSqlTable`, and an opt-in editor context-menu submenu (`insertRandomText.contextMenu.enabled`).
+- **Settings commands** — change any setting from the Command Palette: *Insert Random: Set Insert Type / Output Format / Date Format / Record Format / Record SQL Table / Bulk Count / Seed / Locale*, *Toggle* commands for each boolean setting (Wrap With Quotes, Trailing New Line, Unique Value Per Cursor, Strict Unique, Editor Context Menu), and *Reset Settings to Defaults*.
+- New **Image URL** and **Avatar URL** types (new **Media** category) — UI placeholders and Storybook props — plus **MongoDB ObjectId** under IDs.
+- **Parameterized types** — new *Insert Random: Number (Range…)*, *Float (Range…)*, *String (Length…)*, *Date (Between…)*, *Words (Count…)*, *Sentences (Count…)*, and *Paragraphs (Count…)* commands ask for a min & max, a length up to 1000, a from/to date (`YYYY-MM-DD` or full ISO 8601), or a lorem count up to 100 in validated input boxes, then insert through the normal pipeline (multi-cursor, bulk, quoting, seed all apply). Last-used inputs are remembered and prefilled; Esc cancels cleanly.
+- **Format variants** — new *Insert Random: UUID (Format…)*, *Password (Options…)*, and *Phone (Format…)* commands: pick a UUID rendering (lowercase / UPPERCASE / braced / no dashes / UPPERCASE without dashes), a password length (8–128) with or without symbols, or a phone style (human / national / international) from a Quick Pick. The last pick is remembered and floats to the top next time; same pipeline and clean Esc-cancel as the other parameterized types.
+- **Templates & patterns** — new *Insert Random: From Template…* and *From Pattern…* commands expose faker's entire surface through one input box: a mustache template (`{{person.firstName}} <{{internet.email}}>`) or a regex-like pattern (`[A-Z]{3}-[0-9]{4}`, faker's limited regex subset) is re-rendered with fresh values at every cursor and bulk item. Input is proven by test-rendering as you type — a typo shows faker's error plus a working example inline, and nothing inserts until it renders; the last template and pattern are remembered and prefilled.
+- **Your own data** — two new settings put your data in the picker: `insertRandomText.templates` (named faker templates) and `insertRandomText.customLists` (named value lists) appear as **Templates** and **Custom Lists** groups at the *top* of *Insert Random: Pick…*, and custom lists double as *Record…* fields (the name becomes the field key). Everything rides the normal pipeline — multi-cursor, bulk, quoting, seed. New *Insert Random: Manage Templates* / *Manage Custom Lists* commands jump straight to the settings; malformed entries are skipped safely (logged to the console), and both settings survive *Reset Settings to Defaults*.
+- **Sequences** — new *Insert Random: Sequence (Start/Step…)* command fills cursors with incrementing values: enter a start and a step (negative counts down) and 1, 2, 3… lands down your column — one counter runs through the cursors and bulk items of an insert, and the next insert restarts at your start. Deliberately not random — the multi-cursor machinery makes numbered columns free.
+- **Runs in the browser** — the extension now ships a web build alongside the desktop one, so it works on [vscode.dev](https://vscode.dev) and github.dev; the web bundle carries no Node dependencies and generates everything in the browser, still fully offline.
+- **Restricted Mode & virtual workspaces** — the extension now runs in untrusted (Restricted Mode) and virtual workspaces: every value is generated in-process, with no project-file reads and no network calls.
+- **Six locales** — new `insertRandomText.locale` setting (`en` / `de` / `fr` / `es` / `pt_BR` / `ja`): names, addresses, words and more come out in the chosen language, across everything — single inserts, records, templates and patterns. Switching applies to the next insert with no reload, seeded runs stay reproducible per locale, and a type without localized data falls back to English. Change it from the palette with *Insert Random: Set Locale*.
+- **Anonymize in place, type-aware** — new *Insert Random: Randomize Selection* command replaces each selection where it stands: a selection that *is* an email, UUID, or ISO date/timestamp becomes a **fresh realistic fake of the same type** (a UUID stays a *valid* UUID — case and braces preserved; a date stays a real calendar date at its own precision), and everything else gets a same-shape randomization — digits become digits, letters keep their case, punctuation and layout don't move (`3.14` → `8.77`). Works across a multi-selection in one step, honors the seed for reproducible scrubs, and joins the editor right-click submenu.
+- **Date format control** — new `insertRandomText.dateFormat` setting (`iso` / `isoDate` / `isoTime` / `unixSeconds` / `unixMillis`) renders every timestamp Time type (Date, Past/Future/Recent/Soon Date, Birthdate, Date (Between…)) as a full ISO 8601 timestamp, date only, time only, or Unix seconds/milliseconds — in single inserts and record fields alike (Weekday/Month are unaffected). Change it from the palette with *Insert Random: Set Date Format*.
+- **Strict unique (opt-in)** — new `insertRandomText.strictUnique` setting: duplicates are re-drawn so values meant to differ within one insert really do — bulk values at a cursor, and values across cursors when `uniquePerCursor` is on. Honestly bounded: after 25 re-draws a small pool (booleans, weekdays) keeps its duplicate rather than hanging, and seeded runs stay reproducible. Flip it from the palette with *Insert Random: Toggle Strict Unique*.
-- updated readme
+### Fixes
-## v0.1.2 (2020-3-21)
+- Inserted text no longer stays selected: after a Cursor-mode insert over a selection, the cursor now sits right after the inserted block — same as typing.
+- Lorem is now genuinely randomized (previously a fixed substring of one hardcoded string).
+- Random string is alphanumeric, so it no longer breaks quote-wrapping.
+- Quote-wrapped values are now escaped, so a value containing the active quote character (e.g. `O'Brien`) stays a valid string literal.
+- Removed the global notification-clearing side effect and a duplicate-draw bug in the animal command.
### Changes
-- updated readme
+- Removed the `loremSize`, `hashSize`, and `disableNotifs` settings — the base Lorem and Hash commands no longer take a configurable length (the Small / Medium / Large variants still cover the sizes), and inserts no longer show notifications, so there is nothing left to disable.
+- Removed the `quoteStyle` setting — quoting is now automatic and language-aware (see **Automatic quoting**): SQL keeps single quotes, every other language now gets double quotes where the old default wrapped in single, and a saved `quoteStyle` value is no longer read.
+- Existing `extension.insertRandom*` commands continue to work unchanged.
+- Modernized the build (esbuild bundle; now requires VS Code 1.97+).
+- Refreshed the marketplace listing: new icon, new description, added the **Testing** category, finalized the keyword set, a **Sponsor** button linking to the README's Support section, and a gallery banner tuned to the icon.
+- New demo GIFs — the multi-cursor hero, records → whole datasets, and the smart trio (language-aware quotes, locales, anonymize in place) — replacing the 2020-era demo.
-## v0.1.1 (2020-3-16)
+## v0.1.3 (2020-07-25)
+
+### Changes
-### Feature
+- Expanded the README — clearer step-by-step installation instructions, contributing notes, and a new **Support** section with donation options.
-- toggle new line
+## v0.1.2 (2020-03-21)
### Changes
-- added new configuration `withNewLine`
+- Polished the README — tidied the installation steps and links.
+
+## v0.1.1 (2020-03-16)
+
+### Features
+
+- **Trailing new line** — new `withNewLine` setting appends a newline after the inserted value, so back-to-back inserts land on their own lines.
+
+## v0.1.0 (2020-03-15)
+
+The first release, as **Insert Random Text** — insert random text on the fly, powered by [Chance](https://chancejs.com).
-## v0.1.0 (2020-3-15)
+### Features
-- initial release
+- **14 insert commands** — *Insert Random: Animal / Person / Date / Country / Number / String*, plus *Lorem* and *Hash* in four sizes each (base, Small, Medium, Large).
+- **Insert type** — the `insertType` setting places the value at the cursor (`Cursor`) or on line 1 of the file (`Top`).
+- **Quote wrapping** — `withQuote` wraps inserted values in quotes; `quoteStyle` chooses single or double.
+- **Length controls** — `loremSize` and `hashSize` set the generated length for the base Lorem and Hash commands.
+- **Notifications toggle** — `disableNotifs` silences the insert confirmation notifications.
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..3e0480d
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,82 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this is
+
+VS Code extension (display name **"Random, Fake & Mock Data Generator"**, id `insert-random-text`, publisher `ElecTreeFrying`) that inserts random / fake / mock data — names, emails, addresses, numbers, dates, UUIDs, lorem ipsum, and ~130 types in all — at **every cursor**. Randomness comes from **`@faker-js/faker`** (six locales — `en` default, plus `de`/`fr`/`es`/`pt_BR`/`ja` via `insertRandomText.locale`).
+
+### Project goals (north star)
+1. Grow active installs in the random/mock-data category (near-term: pass the category mid-tier; long-term: challenge the leader). Active installs *subtract* on uninstall, so **retention is the real metric** — every defect fix is rank-defending.
+2. Value is the engine: each generator is simultaneously a real feature, a searchable command title, and a Quick Pick entry. Breadth = discoverability + utility.
+
+## Commands
+
+- `npm run compile` — type-check + lint + **esbuild** bundles (`src/extension.ts` → `dist/extension.js` node target **and** `dist/web/extension.js` browser target, one run)
+- `npm run watch` — incremental dev build; `watch:esbuild` + `watch:tsc` in parallel (`npm-run-all`). Default build task (auto-runs on F5 via `preLaunchTask`).
+- `npm run check-types` — `tsc --noEmit` (esbuild does the actual transpile)
+- `npm run lint` — `eslint src` (flat config, `eslint.config.mjs`)
+- `npm run package` — production build: `check-types && lint && esbuild --production`. `vscode:prepublish` runs this.
+- `npx @vscode/vsce package` — build the `.vsix` (runs prepublish first). Keep the packaged `.vsix` ≤ ~1.5 MB.
+
+`main` is `./dist/extension.js` and `browser` is `./dist/web/extension.js` (same source, two esbuild platforms; **faker inlined** in both — the browser bundle is what runs on vscode.dev/github.dev). `dist/` and `out/` are gitignored — build before running. **esbuild's entry is `src/extension.ts`, so that file must keep its name/location** (both targets are configured in `esbuild.js`, which is out of the usual edit scope).
+
+### Verification
+Tests live in **`src/test/ /*.test.ts`** (mocha `describe`/`it`; `compile-tests` transpiles them to `out/`). Two tiers:
+- **Pure areas, headless** — `npm run compile-tests && npx mocha "out/test/{quote,config,catalog,engine,format,record,prompted,custom,randomize}/*.test.js"`. Don't widen the glob to `**`: `extension.test.ts` and `commands/` import `vscode` and fail under plain node.
+- **Full suite (Extension Host)** — `npm test` (`pretest` runs compile-tests + compile first; compile itself lints). Required for `commands/` and `extension.test.ts`.
+
+Gotcha: `out/` keeps orphaned compiled tests after a `.test.ts` is deleted/renamed — `rm -rf out` before a headless run when results look stale. Release gate on top of tests: `check-types` + `lint` + `node esbuild.js --production` + `npx @vscode/vsce package`.
+
+### Running / debugging
+Press F5 → **"Run Extension"** opens an Extension Development Host with the extension loaded (the `preLaunchTask` builds first).
+
+## Architecture
+
+Eleven single-responsibility modules. Generation is `vscode`-free and lives apart from the editor glue, so the logic that matters is decoupled from the API surface:
+
+- **`src/engine.ts`** — faker lifecycle: `load(locale?)` (lazy, idempotent; default `en`) imports and caches one instance per shipped locale (`LOCALES`/`LocaleId`, one **literal** dynamic import each, cached as promises so concurrent loads share the import) and makes it **active**; the `faker()` accessor returns the active instance; `seed()` seeds it. Encapsulates the ESM/dynamic-import detail (see Gotchas). No `vscode` import.
+- **`src/catalog.ts`** — the **generator registry**: the `Generator` interface and the readonly `generators` array (each `{ id, label, group, hidden?, generate(opts?) }`), plus `getGenerator(id)`. `generate` takes an optional `GenerateOptions` (`{ dateFormat }`) — only the six timestamp Time generators read it, rendering their `Date` via the exported `formatTimestamp(date, format)` (UTC-derived slices; `unixSeconds` floors); every other generator ignores it, and zero-arg `generate: () => …` entries still satisfy the contract. Single source of truth — drives generation, the commands, and the Quick Pick. No `vscode` import.
+- **`src/formatter.ts`** — **pure** rendering: `buildBlocks(cursorCount, generator, options)` returns one block per cursor; `formatBlock` applies `bulkCount` + `outputFormat` (`plain` / `jsonArray` / `quotedList`) + quote/newline wrapping (escaping per the quote policy), and hands `options.dateFormat` to every `generate()` call. `options.strictUnique` routes draws through a seen-set re-draw (`uniqueDraw`, 25-retry budget — part of the seeded-output contract; exhaustion keeps the duplicate, never hangs): one set per `buildBlocks` call, so it spans cursors when `uniquePerCursor` is on and covers just the shared block when off. Single-type inserts only — records never read it. No `vscode` import → trivially checkable.
+- **`src/quotePolicy.ts`** — **pure** language-aware quoting: `resolveQuotePolicy(languageId, opts)` maps a file's `languageId` to a `{ quote, escape }` policy (SQL-family → single quotes + `''`-doubling; everything else → double quotes); `formatter.wrap()` consumes the `escape`. No `vscode` import.
+- **`src/record.ts`** — **pure** multi-field records: `buildRecords(fields, shape, opts)` composes picked generators into one JSON object / SQL `INSERT` row / CSV line (stacked by `bulkCount`); escaping is decided by the **shape**, not the file's language; `opts.dateFormat` is handed to each field draw. `opts.dataset` renders the same records as a standalone **file** (Generate Dataset…): CSV gains a header row of field keys (csv-escaped — a custom-list name may hold a comma), JSON is always an array with one record per line, and the text ends with a trailing newline; at-cursor records never pass it. No `vscode` import.
+- **`src/randomize.ts`** — the **pure** halves of Randomize Selection (anonymize in place). Type-aware replace: `detect(text)` → `'email' | 'uuid' | 'isoDate' | 'isoTimestamp' | undefined` (anchored whole-string matches, calendar-checked dates, UTC-`Z`-only timestamps — conservative by design; numbers deliberately undetected, the scramble already serves them), plus the pure dressers `shapeUuid(fresh, original)` (case + braces follow the original) and `shapeTimestamp(freshIso, original)` (strip `.mmm` when the original had none). Fallback scramble: `randomize(text, rng)` maps each digit → random digit, `a–z` → random lowercase, `A–Z` → random uppercase, everything else untouched; iterates code points so surrogate pairs pass through whole. `rng(bound)` → `[0, bound)` is injected — the extension wraps the shared seeded faker (`number.int`), tests script it. No `vscode` import.
+- **`src/prompted.ts`** — **pure** parameterized-command registry: `promptedCommands` (each `{ id, label, group, steps }` plus exactly one rendering surface — `render(params, opts?)` for stateless draws, or `createRender(params)` returning a closure built once per insert for stateful ones (Sequence's counter; a spec test enforces the exactly-one rule); a step is a discriminated union — an **input box** (`InputStep`: prompt/placeholder/fallback plus a `validateInput`-shaped `validate(input, prior)` — `prior` enables cross-field rules like min ≤ max) or a **Quick Pick** (`PickStep`: `kind: 'pick'`, `options: { value, label, detail }[]`, fallback-first order, no validation — closed set)), `getPromptedCommand(id)`, `toGenerator(command, params)` wrapping a finished flow as a one-off `Generator`, and the pure `formatUuid(uuid, format)` post-transform behind UUID (Format…). The free-form From Template…/From Pattern… boxes validate by **test-rendering** the input through faker (`helpers.fake` / `helpers.fromRegExp`) — faker's error plus a working example on failure, empty input rejected — so their `validate` needs the engine loaded. `render(params, opts?)` receives the same `GenerateOptions` as catalog generators (Date (Between…) renders per `dateFormat`). Deliberately **not** catalog entries — absent from Pick…/Record…. No `vscode` import.
+- **`src/custom.ts`** — **pure** user-defined data pools: `templateGenerators(map)` / `customListGenerators(map)` wrap the validated `insertRandomText.templates` / `.customLists` settings as plain `Generator`s (Templates render via `helpers.fake`, lists draw via `helpers.arrayElement`). The **id is the bare user-chosen name** — it doubles as the Record… field key — and these never enter the catalog registry, so a name matching a catalog id shadows nothing. `TEMPLATES_GROUP` / `CUSTOM_LISTS_GROUP` are the picker headings. No `vscode` import.
+- **`src/configuration.ts`** — `Configuration` reads workspace settings into a typed `Settings` via `read()`; `ConfigKey` holds the package.json key constants; the two enum settings are normalized to booleans here, `locale` is validated against `LOCALES` (junk → `en`), and the two **object settings** (`templates`, `customLists`) are shape-validated — junk entries dropped, each drop logged through an injectable `WarnSink` (constructor param defaulting to `console.warn`; inject it in tests — the extension host's patched console can't be monkey-swapped). Depends only on a narrow `WorkspaceLike` seam, not `vscode`.
+- **`src/extension.ts`** — thin activation entry. `COMMAND_TO_GENERATOR` maps every command id → a generator id; `activate()` registers them all through one `insertGenerated(id)`, plus the `insertRandomText.pick` Quick Pick, the `insertRandomText.record` multi-field composer, the `insertRandomText.generateDataset` records-to-file builder, the `insertRandomText.randomizeSelection` in-place anonymizer, and every prompted command from `promptedCommands` via `runPrompted` (all but randomize and dataset honor `insertType`). The pipeline seam is **`insertWith(generator)`** — `load(settings.locale)` → `applySeed()` → read cached `settings` → `buildBlocks` over `editor.selections` (Cursor mode = multi-cursor fill via `fillSelections`, which collapses each cursor to sit after its block — inserted text never stays selected; Top mode = one block at line 1; Clipboard mode = a bare value copied to the clipboard, no editor needed); `insertGenerated(id)` is just registry lookup + `insertWith`, and `runPrompted` is `await load(settings.locale)` (validation may test-render through faker) + the step walk (input boxes prefill from `globalState`; pick steps float the remembered pick to the top marked "Last used"; Esc = clean cancel) + `insertWith`. `customPickItems(includeTemplates)` builds the user-group items that lead Pick… (Templates + Custom Lists) and the field picker (Custom Lists only; the picked lists join `fields` first, keyed by name); custom selections go through `insertCustom` — `insertWith` in a try/catch that surfaces a template render failure as a friendly error naming the Manage command. **`pickRecordFields(placeHolder)`** is the shared multi-select field picker behind Record… AND Generate Dataset… (returns picked generators in picker display order, or undefined on cancel/empty). `generateDataset()` = `load` → `pickRecordFields` → shape pick (the `recordFormat` setting floats to the top marked current; picking does NOT write the setting) → row-count box (default `bulkCount`, hard cap 100 000, modal confirm above 10 000) → `applySeed()` → `buildRecords(…, { dataset: true })` → `openTextDocument({ content, language })` + `showTextDocument` (json/sql/`plaintext` for csv) — a new untitled file, so `insertType` never applies and no editor is needed. `randomizeSelections()` bypasses the insert pipeline deliberately (a replacement, not an insertion — no quote/newline/bulk/insertType): `load` + `applySeed`, then per non-empty selection `typedReplacement(text) ?? randomize(text, rng)` through `fillSelections` — `typedReplacement` switches on `detect()` and draws the fresh typed value through `faker()` (email / re-dressed uuid / `date.anytime()` rendered per the original's format), so typed redraws are seeded like everything else (empty selections map to an identity `''` replace, so bare carets get nothing); no non-empty selection anywhere → one info message.
+- **`src/settingsCommands.ts`** — palette commands that *write or open* settings: `SETTING_COMMANDS` maps command id → handler (Quick Pick for the enum settings, toggle for the booleans, input box for bulk count / seed, `manage*` commands that open the Settings UI at a key, plus a confirm-gated reset). **The reset deliberately skips `templates`/`customLists`** (`RESET_KEEPS`) — user-authored content, not tuning; the modal says so. Writes to the open workspace when one is present, else global user settings; `extension.ts` registers them. The read side stays in `configuration.ts`.
+
+### Config flow (the key cross-file mechanism)
+`extension.ts` holds a single module-level `settings: Settings`. On activate, `watchConfiguration()` snapshots it via `Configuration.read()`, then re-snapshots on any relevant `onDidChangeConfiguration` (a `locale` change additionally fires `load(locale)` so the active faker swaps eagerly). Commands read this **cached** `settings`, never re-reading at invocation. Anything bypassing `watchConfiguration` sees stale config.
+
+### Commands & settings model
+- **Two command namespaces.** The original 14 `extension.insertRandom*` ids are kept for **back-compat** (existing keybindings); every new type uses a namespaced `insertRandomText.` id. Both register via `COMMAND_TO_GENERATOR`.
+- **Settings are split.** The 3 remaining legacy keys stay **flat & non-namespaced** (`insertType`, `withQuote`, `withNewLine`) for back-compat; **all new keys are namespaced** under `insertRandomText.*` (`uniquePerCursor`, `seed`, `locale`, `bulkCount`, `outputFormat`, `templates`, `customLists`, `contextMenu.enabled`, …). Do **not** migrate the legacy keys.
+- **Hidden generators.** The Lorem/Hash Small/Medium/Large back-compat generators carry `hidden: true` so they serve their legacy commands without cluttering the Quick Pick.
+- **Settings commands.** `settingsCommands.ts` contributes `insertRandomText.set*` / `toggle*` / `resetSettings` so every setting is changeable from the Command Palette (mirrors the sibling `auto-import-relative-path`). Each new one is a `package.json` command + an entry in `SETTING_COMMANDS` (registered in `extension.ts`). Writes are workspace-scoped when a folder is open, else global.
+- **Prompted commands.** The parameterized types (`numberRange` / `floatRange` / `stringLength` / `dateBetween` / `wordsCount` / `sentencesCount` / `paragraphsCount` / `uuidFormat` / `passwordOptions` / `phoneFormat` / `fromTemplate` / `fromPattern` / `sequence`, all `insertRandomText.*`) ask for parameters in input boxes and Quick Picks, then insert via `insertWith`. `sequence` is the stateful one — not random at all: `createRender` builds one counter per insert (start/step boxes), advancing per cursor/bulk item and restarting on the next insert. Adding one: entry in `promptedCommands` (`src/prompted.ts`) + a `package.json` command `insertRandomText.` — registration is automatic; they are intentionally **not** in `COMMAND_TO_GENERATOR` (the parity test reads `promptedCommands` and holds contributed ↔ registered in sync, and guards prompted ids against catalog-id collisions).
+
+### Adding a generator (the common case)
+1. `src/catalog.ts` — add a `{ id, label, group, generate }` entry. It appears in the Quick Pick automatically.
+2. `package.json` → `contributes.commands` — add `{ "command": "insertRandomText.", "title": "Insert Random: " }` (direct command + search visibility).
+3. `src/extension.ts` — add `'insertRandomText.': ''` to `COMMAND_TO_GENERATOR`.
+4. `README.md` → the **What it generates** table — add the type to its category row and bump that category's `(N)` count (and the "20 categories" figure if it's a new group). The headline total is deliberately soft ("130+"), so it never needs a per-type edit.
+5. `SPEC.md` → the **Data Catalog** — add the type's row under its category heading (label / registry id / command id / faker source) and bump that heading's `(N)`, plus every literal total: the front-matter summary, the Commands section, and the catalog intro state the visible / registry / modern-command / contributed-command counts **exactly** (SPEC.md has no soft numbers — grep the old totals).
+
+(activationEvents are auto-generated from `contributes.commands` since VS Code 1.74 — nothing to update there.)
+
+### Config value conventions
+- `insertType`: a 3-value enum — `Cursor` (multi-cursor fill at each selection) / `Top` (one block at line 1 / `0,0`) / `Clipboard` (copy to the clipboard, no editor) — normalized to `'cursor'` / `'top'` / `'clipboard'` in `configuration.ts`.
+- Quoting: no user quote-style setting — the quote + escape are resolved automatically from the file's `languageId` (SQL → single + `''`-doubling; everything else → double), only when `withQuote` is on.
+- `locale`: a 6-value enum (`en`/`de`/`fr`/`es`/`pt_BR`/`ja`, ids exactly as faker spells them) — validated in `configuration.ts`, junk → `en`. Every insert awaits `load(settings.locale)`, so a change applies to the next insert with no reload.
+
+## Gotchas
+
+- **faker is ESM-only; the bundle is CJS.** `engine.ts` loads each locale instance via a **dynamic `import('@faker-js/faker/locale/')`** inside `load()` — a *static* import trips `TS1479` under `module: Node16`. The faker **type** is imported with `import type { Faker } from '@faker-js/faker' with { 'resolution-mode': 'import' }` (the `TS1542` fix). esbuild inlines the dynamic imports into both CJS bundles (node + browser) — it only inlines **literal** specifiers, so every shipped locale has its own literal in `importLocale`'s switch. The browser target doubles as the **web-cleanliness gate**: esbuild hard-fails the `dist/web` build on any Node built-in, keeping the extension vscode.dev-safe. **Only import the shipped `/locale/` entries — never the faker root** (60+ locales → multi-MB bundle, blows the `.vsix` size gate; each shipped locale costs ~60 KB bundled).
+- **Fresh value per call — never a cached getter.** `Generator.generate()` is called once per cursor inside the `editor.selections` loop and must draw a new value each time. (The old code had an "animal double-draw" bug from a getter read twice — the registry shape structurally prevents it; don't reintroduce stored/memoized generator values.) With `seed` set, the sequence is reproducible.
+- **Line endings are LF**, enforced by `.gitattributes` (`* text=auto eol=lf`).
+
+## Scope & toolchain
+- **Core work lives in `src/**`, `package.json`, `README.md`, `SPEC.md`, `CLAUDE.md`, `CHANGELOG.md`.** `SPEC.md` is the tracked user-facing functionality contract (every command, setting, and catalog row — README links into it; vsix-excluded like CLAUDE.md): any behavior change lands there too. Out of scope (later waves): icon assets under `images/`, CI, Open VSX, `tsconfig.json` / `esbuild.js`.
+- TypeScript 5.9 (target `ES2022`, module `Node16`, `strict: false`), ESLint 9 flat config (rules are warnings), esbuild bundling, engine `vscode ^1.97.0`. **Runtime dependency: `@faker-js/faker`** (bundled into `dist/`). Mirrors the sibling `auto-import-relative-path` extension.
diff --git a/LICENSE.md b/LICENSE.md
index 7b669af..3318b71 100644
--- a/LICENSE.md
+++ b/LICENSE.md
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2020 John James G. Ermita¤o
+Copyright (c) 2020-2026 John James G. Ermitaño
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff --git a/README.md b/README.md
index bcf80da..a27edb2 100644
--- a/README.md
+++ b/README.md
@@ -1,102 +1,405 @@
+# Random, Fake & Mock Data Generator
+
+[![version][version svg]][package]
+[![installs][installs svg]][package]
+[![downloads][downloads svg]][package]
+[![ratings][ratings svg]][package]
+[![license][license svg]][repo]
+[![vscode][vscode svg]][package]
+
+[version svg]: https://vsmarketplacebadges.dev/version-short/electreefrying.insert-random-text.png
+[installs svg]: https://vsmarketplacebadges.dev/installs/electreefrying.insert-random-text.png
+[downloads svg]: https://vsmarketplacebadges.dev/downloads/electreefrying.insert-random-text.png
+[ratings svg]: https://vsmarketplacebadges.dev/rating-short/ElecTreeFrying.insert-random-text.png
+[license svg]: https://img.shields.io/github/license/ElecTreeFrying/insert-random-text
+[vscode svg]: https://img.shields.io/badge/vscode-%3E%3D1.97.0-blue
+[package]: https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.insert-random-text
+[repo]: https://github.com/ElecTreeFrying/insert-random-text
+
+**Random, Fake & Mock Data Generator** is a free, offline VS Code extension that inserts 130+ types of random, fake & mock data — names, emails, addresses, UUIDs, lorem ipsum, JSON/SQL/CSV records and whole datasets — at every cursor, in six locales, seeded and reproducible when you need it.
+
+> **Fill every cursor with realistic fake data — a _different_ value in each, in one step.**
+
+**Names** · **Emails** · **Addresses** · **Finance** · **Git** · **Dates** · **UUIDs** · **Lorem ipsum** · **Mock JSON**
+
+Drop a multi-cursor selection down a column and fill every row with a _different_ realistic value in one step — names, emails, IDs, dates, prices, whatever the column needs. All generated right where you're typing: no website, no signup, no waiting on a model — instant, offline, and reproducible when you seed it.
+
+**Perfect for** test fixtures & database seeds · mock API responses · whole CSV / JSON / SQL datasets · UI placeholder & Storybook props · throwaway IDs, addresses & credentials.
+
+
+
+[**See the full specification**][SPEC]
+
+[SPEC]: SPEC.md
+
+---
+
+## One extension instead of five
+
+Fake names & emails, UUIDs in any format, lorem ipsum with exact counts, numbers & dates with ranges, JSON/SQL/CSV records and whole datasets, your own templates & lists, six locales, and in-place anonymization — replace your UUID inserter, your lorem ipsum generator, your dummy-text filler, your CSV mocker, and your faker wrapper with one actively maintained extension. Offline, uncapped, seeded.
+
+---
+
+## Highlights
+
+- **Fill every cursor at once** — a _different_ value in each, in one step. Forty cursors at a bulk count of 25 is a thousand values in one command — the fastest way to seed a table, an array, or a fixture.
+- **130+ realistic types** — stop hand-typing fake data: identity, finance, git, system, network, and more across 20 categories (full list below).
+- **Six locales** — names, addresses & text in English, German, French, Spanish, Brazilian Portuguese, or Japanese. One setting, no reload.
+- **Whole records in one shot** — multi-select fields and drop a `{ name, email, phone }` object, a SQL `INSERT` row, or a CSV line at every cursor. Scales with bulk count.
+- **Whole datasets in one command** — **Generate Dataset…** builds up to 100,000 rows of JSON, CSV (with a header), or SQL `INSERT`s and opens them as a new file. No signup, no export button, and none of the row caps web generators make you pay to lift — instant, offline, free.
+- **Anonymize in place** — select real data and **Randomize Selection** re-rolls every letter and digit where it stands (`3.14` → `8.77`) — and a selection that *is* an email, UUID, or ISO date becomes a fresh realistic one of the same type instead. Shapes intact, secrets gone.
+- **Reproducible when you need it** — set a seed and every run replays the identical values — something AI generators can't promise, even with a seed parameter. Stable tests, snapshots, and demos.
+- **Drops straight into code** — optional language-aware quoting and a trailing newline, so values land as valid syntax in arrays, JSON, SQL, and configs.
+- **Stays in your editor** — fully offline: no account, no telemetry, no model calls. Your code never leaves your machine to get mock data back.
+- **Or skip the editor entirely** — Clipboard mode copies a value straight to your clipboard (filenames, terminals, anywhere).
+- **Runs everywhere VS Code does** — including [vscode.dev](https://vscode.dev) and github.dev in the browser; the extension ships a web build.
+- **Configure from the Command Palette** — insert type, quotes, bulk count, output format, seed, and locale, without ever opening Settings.
+- **Powered by [Faker][faker]** — the actively maintained, community-governed faker library, bundled into the extension (nothing fetched at runtime). Coherent, realistic data, not random noise.
+
+Need a page of plausible narrative prose? That's a job for your AI assistant — and it's welcome to it. Need a thousand structured values that parse, replay identically under a seed, and never leave your machine? That's this extension's lane.
+
+[faker]: https://fakerjs.dev
+
+---
+
+## Quick start
+
+1. Open the Command Palette — Cmd /Ctrl +Shift +P
+2. Type **Insert Random**, then pick a type — or run **Insert Random: Pick…** to browse all 130+.
+3. It drops in at every cursor. Multi-select a column first to fill every row at once.
+
+No setup required — tweak quotes, bulk count, output format, seed, and locale whenever you want them.
+
+---
+
+## Records & datasets
+
+
+
+Records to rows to a whole dataset — compose a `{ person, username, email }` object at the cursor, switch the shape to SQL from the palette, then pour up to **100,000 rows** of JSON, CSV, or SQL into a new file with [**Generate Dataset…**][records-spec]. Offline, free, seeded-reproducible.
+
+[records-spec]: SPEC.md#multi-field-records
+
+---
+
+## Why it's smart
+
+
+
+- **Quotes follow the language, nothing configured** — the same command lands `"…"` in JavaScript and `'…'` in SQL.
+- **Six locales, one setting, no reload** — and seeded runs stay reproducible per locale.
+- **Anonymize in place** — real data in, safe data out; nothing ever leaves your editor.
+
+---
+
+## What it generates
+
+**130+ types across 20 categories** — every one is a direct `Insert Random:` command *and* a Quick Pick entry.
+
+| Category | Types |
+|---|---|
+| **Identity** (17) | Full Name · First Name · Middle Name · Last Name · Name Prefix · Name Suffix · Username · Display Name · Email · Phone · Sex · Gender · Bio · Zodiac Sign · Job Title · Job Type · Job Area |
+| **Numbers** (6) | Number · Float · Boolean · Hex Number · Binary Number · Octal Number |
+| **Text** (12) | String · Alpha String · Numeric String · Word · Words · Sentence · Slug · Lorem Paragraph · Hacker Phrase · Emoji · Book Title · Book Author |
+| **Time** (8) | Date · Past Date · Future Date · Recent Date · Soon Date · Birthdate · Weekday · Month |
+| **Location** (15) | Country · Country Code · State · State Abbreviation · County · City · Zip Code · Street Name · Street Address · Secondary Address · Building Number · Direction · Latitude · Longitude · Time Zone |
+| **Network** (11) | IP Address · IPv6 Address · MAC Address · URL · Domain Name · Port · Protocol · HTTP Method · HTTP Status Code · User Agent · JWT |
+| **Media** (2) | Image URL · Avatar URL |
+| **Design** (4) | Color (hex) · Color (rgb) · Color (hsl) · Color Name |
+| **Security** (1) | Password |
+| **IDs** (5) | UUID · ULID · Nano ID · MongoDB ObjectId · Hash |
+| **Nature** (6) | Animal · Dog Breed · Cat Breed · Bird Species · Fish Species · Horse Breed |
+| **Company** (3) | Company Name · Catch Phrase · Buzz Phrase |
+| **Commerce** (7) | Product · Product Name · Price · Department · Product Material · Product Description · ISBN |
+| **Finance** (13) | Amount · Currency Code · Currency Name · Currency Symbol · Credit Card Number · Credit Card CVV · IBAN · BIC · Account Number · Routing Number · Bitcoin Address · Ethereum Address · PIN |
+| **Git** (3) | Git Branch · Git Commit SHA · Git Commit Message |
+| **System** (6) | File Name · File Path · File Extension · MIME Type · Semver · Cron Expression |
+| **Vehicle** (5) | Vehicle · Vehicle Manufacturer · Vehicle Model · VIN · License Plate (VRM) |
+| **Food** (5) | Dish · Ingredient · Fruit · Vegetable · Cuisine |
+| **Music** (4) | Song Name · Music Genre · Artist · Album |
+| **Travel** (4) | Airline · Airport · Flight Number · Seat |
+
+See [SPEC — §Data Catalog][SPEC-catalog] for every type with its registry id, command id, and faker source.
+
+[SPEC-catalog]: SPEC.md#data-catalog
+
+---
+
+## Settings
+
+Change any of these from the [Commands](#commands) below, or in VS Code Settings.
+
+| Setting | Options | Default | What it does |
+|---|---|---|---|
+| `insertType` | `Cursor` · `Top` · `Clipboard` | `Cursor` | Where values go — each cursor, the top of the file, or the clipboard. |
+| `insertRandomText.uniquePerCursor` | `true` · `false` | `true` | A different value at each cursor (multi-cursor fill), or the same value repeated. |
+| `insertRandomText.strictUnique` | `true` · `false` | `false` | Re-draw duplicates so values meant to differ within one insert really do — bulk values, and across cursors when `uniquePerCursor` is on. A small pool (booleans, weekdays) that runs out keeps its duplicate. |
+| `insertRandomText.bulkCount` | `1`–`1000` | `1` | How many values to insert at each cursor. |
+| `insertRandomText.outputFormat` | `plain` · `jsonArray` · `quotedList` | `plain` | How bulk values render — one per line, a JSON array, or a quoted comma-separated list. |
+| `insertRandomText.dateFormat` | `iso` · `isoDate` · `isoTime` · `unixSeconds` · `unixMillis` | `iso` | How the timestamp Time types render — full ISO 8601, date only, time only, or Unix seconds/milliseconds. |
+| `insertRandomText.recordFormat` | `json` · `sql` · `csv` | `json` | Shape for **Insert Random: Record…** — a JSON object, a SQL INSERT row, or a CSV line. |
+| `insertRandomText.recordSqlTable` | any string | `table` | Table name used by the SQL record shape (when `recordFormat` is `sql`). |
+| `insertRandomText.templates` | object: name → template | `{}` | Your saved faker templates — shown in a **Templates** group at the top of **Insert Random: Pick…**. See [Your own data](#your-own-data). |
+| `insertRandomText.customLists` | object: name → string array | `{}` | Your own value lists — shown in a **Custom Lists** group at the top of **Pick…**, and available as **Record…** fields. See [Your own data](#your-own-data). |
+| `insertRandomText.seed` | any number, or empty | _(empty)_ | Reproducible output — the same seed yields the same values every run; empty = random. |
+| `insertRandomText.locale` | `en` · `de` · `fr` · `es` · `pt_BR` · `ja` | `en` | Language/region generated data draws from — names, addresses, words and more come out localized. |
+| `withQuote` | `true` · `false` | `true` | Wrap each inserted value in quotes. |
+| `withNewLine` | `true` · `false` | `true` | Append a newline after each value. |
+| `insertRandomText.contextMenu.enabled` | `true` · `false` | `false` | Add an "Insert Random" submenu to the editor right-click menu. |
+
+See [SPEC — §Configuration Reference][SPEC-config] for every setting with its exact type, default, and resolution rules, and [§Quote Wrapping & Language-Aware Quoting][SPEC-quotes] for the full language buckets.
+
+[SPEC-config]: SPEC.md#configuration-reference
+[SPEC-quotes]: SPEC.md#quote-wrapping--language-aware-quoting
+
+### How the key settings behave
+
+A few settings are easier to *see* than to describe.
+
+**`outputFormat`** — how bulk values (bulk count > 1) render at each cursor. Two values, `Kohler` and `Reilly` (quotes follow the file's language — double in most, single in SQL; `jsonArray` always emits valid JSON):
+
+| Format | Result |
+|---|---|
+| `plain` | `"Kohler"`, then `"Reilly"` on the next line |
+| `jsonArray` | `[ "Kohler", "Reilly" ]` |
+| `quotedList` | `"Kohler", "Reilly"` |
+
+**`recordFormat`** — the shape **Insert Random: Record…** composes your ticked fields into. Two fields, `firstName` and `email`, with `recordSqlTable` set to `users`:
+
+| Shape | Result |
+|---|---|
+| `json` | `{ "firstName": "Cooper", "email": "Noe.Dibbert@yahoo.com" }` |
+| `sql` | `INSERT INTO users (firstName, email) VALUES ('Cooper', 'Noe.Dibbert@yahoo.com');` |
+| `csv` | `Cooper,Noe.Dibbert@yahoo.com` |
+
+Records escape by shape, not by the file's language — and a bulk count stacks them: `json` wraps into an array, `sql` emits one `INSERT` per line, `csv` one line per record.
+
+**`dateFormat`** — how the timestamp Time types (Date, Past/Future/Recent/Soon Date, Birthdate, and **Date (Between…)**) render, everywhere they're drawn — single inserts and record fields alike. One drawn instant, five renderings:
-# Insert Random Text (vscode extension)
+| Format | Result |
+|---|---|
+| `iso` | `2026-07-02T12:34:56.789Z` |
+| `isoDate` | `2026-07-02` |
+| `isoTime` | `12:34:56` |
+| `unixSeconds` | `1782995696` |
+| `unixMillis` | `1782995696789` |
-
+Weekday and Month are names, not timestamps — they're unaffected.
-Insert random text on the fly.
+**`locale`** — the language/region every type draws from. The same "insert first name · street address" under four locales:
-## Usage
+| Locale | Result |
+|---|---|
+| `en` (default) | `Ignatius` · `34683 Courtney Lakes` |
+| `de` | `Jonna` · `Im Nesselrader Kamp 346` |
+| `fr` | `Danielle` · `12 Boulevard de Presbourg` |
+| `ja` | `愛菜` · `5丁目1番3号` |
+
+Templates, patterns, and records follow it too; a type without localized data (UUIDs, numbers, …) falls back to English. Seeded runs stay reproducible per locale.
+
+**Automatic quoting** — the *same* "insert email" adapts to the file's language, so the result is always valid syntax, no setting required:
+
+- `.json` · `.go` · `.rs` · `.js` · `.py` → `"jane@example.com"` — double quotes
+- `.sql` → `'jane@example.com'`, and an embedded apostrophe doubles: `O'Brien` → `'O''Brien'`
-1. On your text editor.
-1. From the command palette `Ctrl+Shift+P`
-1. Select any Insert Random command
+**`insertType`** — where the value lands:
-
+- **Cursor** — a fresh value at *every* cursor; fill a whole column in one step
+- **Top** — one block dropped at line 1
+- **Clipboard** — copies a bare value, inserts nothing (handy for filenames, terminals)
+
+---
## Commands
-| Command | Description |
-| --------------------------- | --------------------------------------------------- |
-| Insert Random: Animal | Insert random animal |
-| Insert Random: Person | Insert random name |
-| Insert Random: Date | Insert random date |
-| Insert Random: Country | Insert random country |
-| Insert Random: Number | Insert random number |
-| Insert Random: string | Insert random string |
-| Insert Random: lorem | Insert lorem (configure length in settings) |
-| Insert Random: Lorem Small | Insert lorem, Length 177 |
-| Insert Random: Lorem Medium | Insert lorem, Length 521 |
-| Insert Random: Lorem Large | Insert lorem, Length 1368 |
-| Insert Random: hash | Insert random hash (configure length in settings |
-| Insert Random: Hash Small | Insert random, Length 7 |
-| Insert Random: Hash Medium | Insert random, Length 17 |
-| Insert Random: Hash Large | Insert random, Length 27 |
-
-## Extension Settings
-
-### General settings
-
-* `quoteStyle`: (double/single quote) Select quote style for path.
-* `insertType`: Paste import on selected line at the top or on selected line.
-* `loremSize`: Enter lorem string length..
-* `hashSize`: Enter hash string length..
-* `disableNotifs`: Disable all notifications on file drop to active pane.
-* `withQuote`: Toggle to wrap random text with quotes.
-* `withNewLine`: Toggle include newline at the end of each insert.
-
-### Settings Preview
-
-
+Open the Command Palette (Cmd /Ctrl +Shift +P ) and type **Insert Random**.
+
+| Command | What it does |
+|---|---|
+| **Insert Random: Pick…** | Choose any type — led by your saved templates and custom lists, when you've defined some — from a grouped, searchable menu, then insert at every cursor. |
+| **Insert Random: Record…** | Multi-select fields (catalog types and your custom lists) and insert them together as one record — a JSON object, SQL row, or CSV line — at every cursor (or the top / clipboard, per your insert type). |
+| **Insert Random: Generate Dataset…** | Pick fields, a format, and a row count — up to 100,000 rows of JSON, CSV (with a header row), or SQL `INSERT`s open as a new file. See [Generate whole datasets](#generate-whole-datasets). |
+| **Insert Random: Number (Range…) / Float (Range…) / String (Length…) / Date (Between…) / Words (Count…) / Sentences (Count…) / Paragraphs (Count…) / UUID (Format…) / Password (Options…) / Phone (Format…) / Sequence (Start/Step…)** | Parameterized types — enter a min & max, a length, a from/to date, a lorem count, or a sequence start & step in input boxes, or pick a format from a Quick Pick (UUID casing / braces / dashes, password length & symbols, phone style), and the value is generated to your spec, through the same pipeline (multi-cursor, bulk, quoting, seed). **Sequence** isn't random at all: 1, 2, 3… lands down your cursors, one increment per cursor and bulk item. Your last inputs and picks are remembered; Esc cancels cleanly. |
+| **Insert Random: From Template… / From Pattern…** | Free-form types — write your own faker template or regex-like pattern and every cursor gets a fresh rendering. See [Templates & patterns](#templates--patterns). |
+| **Insert Random: Randomize Selection** | Anonymize in place — every selected letter and digit is re-rolled in kind (digits stay digits, letters keep their case, punctuation doesn't move), each selection replaced where it stands. See [Anonymize in place](#anonymize-in-place). |
+| **Insert Random: _‹Type›_** | A direct command for every type — e.g. *Insert Random: Email*, *Insert Random: UUID*, *Insert Random: Credit Card Number*. |
+| **Insert Random: Set Insert Type / Output Format / Date Format / Record Format / Locale** | Pick the value from a Quick Pick. |
+| **Insert Random: Set Bulk Count / Set Seed / Set Record SQL Table** | Enter the value in an input box. |
+| **Insert Random: Toggle Wrap With Quotes / Trailing New Line / Unique Value Per Cursor / Strict Unique / Editor Context Menu** | Flip a setting on or off. |
+| **Insert Random: Manage Templates / Manage Custom Lists** | Jump to the setting where your saved templates / custom lists live. See [Your own data](#your-own-data). |
+| **Insert Random: Reset Settings to Defaults** | Restore every setting to its default — your saved templates and custom lists are kept. |
+
+See [SPEC — §Commands][SPEC-commands] for the two command namespaces and back-compat notes, [§Insert Targets][SPEC-targets] for exactly how Cursor / Top / Clipboard behave, and [§Multi-Field Records][SPEC-records] for the record composer.
+
+[SPEC-commands]: SPEC.md#commands
+[SPEC-targets]: SPEC.md#insert-targets
+[SPEC-records]: SPEC.md#multi-field-records
+
+### Templates & patterns
+
+When no built-in type fits, write your own — one input box exposes faker's entire surface:
+
+- **Insert Random: From Template…** — mustache-style placeholders, re-rendered with fresh values at every cursor and bulk item:
+ `{{person.firstName}} <{{internet.email}}>` → `Jane `
+- **Insert Random: From Pattern…** — a regex-like pattern (faker supports a limited subset: character classes, ranges, quantifiers):
+ `[A-Z]{3}-[0-9]{4}` → `WUZ-7314`
+
+Input is validated as you type by test-rendering it — a typo shows faker's error plus a working example right in the box, and nothing is inserted until it renders. Your last template and pattern are remembered and prefilled.
+
+### Your own data
+
+Save the templates you reuse, and the values only your project knows, straight into Settings — they appear at the **top** of **Insert Random: Pick…**, ahead of the catalog:
+
+```jsonc
+"insertRandomText.templates": {
+ "invoice": "INV-{{string.numeric(4)}}",
+ "contact": "{{person.fullName}} <{{internet.email}}>"
+},
+"insertRandomText.customLists": {
+ "environment": [ "dev", "staging", "production" ],
+ "plan": [ "free", "pro", "enterprise" ]
+}
+```
+
+- **Templates** render through faker on every insert — same syntax as [From Template…](#templates--patterns), so every cursor and bulk item gets fresh values.
+- **Custom lists** draw one random item per insert, and also show up as **Record…** fields — the name becomes the field key, so picking `environment` next to `Email` yields `{ "environment": "staging", "email": "…" }`.
+- **Insert Random: Manage Templates / Manage Custom Lists** jump straight to these settings. Both survive **Reset Settings to Defaults** — they're your data, not tuning.
+
+Everything rides the normal pipeline: multi-cursor, bulk count, quoting, and seed all apply.
+
+### Generate whole datasets
+
+Need a file of test data, not a value at the cursor? **Insert Random: Generate Dataset…** walks three quick steps — pick fields (your [custom lists](#your-own-data) included), pick a format, enter a row count — and opens the result as a new untitled file, ready to save:
+
+- **JSON** — an array of objects, one record per line.
+- **CSV** — one line per record, led by a **header row** of the field names.
+- **SQL** — one `INSERT INTO your_table (…) VALUES (…);` per record, using your configured table name.
+
+Up to **100,000 rows**, generated locally in an instant — no signup, no paywalled row caps, no export step, no data leaving your editor. The format pick starts on your `recordFormat` setting, the row count starts at your bulk count, and [locale, seed](#settings), and date format all apply — a seeded run regenerates the identical dataset every time. Works with no editor open.
+
+### Anonymize in place
+
+Pasted real data into a fixture, log, or bug report? Select it and run **Insert Random: Randomize Selection** — each selection is replaced where it stands, in two tiers:
+
+- **Type-aware:** a selection that *is* an email, UUID, or ISO date/timestamp becomes a **fresh realistic fake of the same type** — a UUID stays a *valid* UUID (case and braces preserved), a date stays a real calendar date:
+ ```
+ jane.doe+prod@acme.com → Elta.Kuhlman@hotmail.com
+ ```
+- **Same-shape for everything else:** digits stay digits, letters keep their case, punctuation and layout don't move:
+ ```
+ +63 (917) 555-0142 → +81 (269) 344-9518
+ ```
+
+- Works on every non-empty selection at once — multi-select a column of secrets and scrub them in one step.
+- Detection is deliberately strict — the selection must be exactly the value (no padding, no surrounding text); anything else falls back to the safe same-shape scramble.
+- It's a replacement, not an insertion: quoting, bulk count, and the insert type don't apply. The [seed](#settings) does, so a seeded run scrubs reproducibly.
+- Also in the editor right-click menu, when the context-menu submenu is enabled.
+
+### Keyboard shortcuts
+
+This extension ships **no default keyboard shortcuts** — with 130+ commands, presetting a few would only collide with bindings you already use. Bind the types you reach for most, instead:
+
+1. Open **Keyboard Shortcuts** (Cmd /Ctrl +K Cmd /Ctrl +S ).
+2. Search **Insert Random**.
+3. Click the ✎ pencil next to any command — e.g. *Insert Random: UUID* or *Insert Random: Pick…* — and press your combo.
+
+**Tip:** bind **Insert Random: Pick…** to a single shortcut and you get keyboard-fast access to *every* type through its searchable menu.
+
+---
## Installation
- 1. Install VS Code v1.42.0 or higher
- 2. Launch Visual Studio Code
- 3. Enter command `Ctrl+Shift+P` (Windows, Linux) or `Cmd+Shift+P` (OSX)
- 4. Select → `Extensions: Install Extensions`.
- 5. Choose **Insert Random Text** by _ElecTreeFrying_
- 6. Reload Visual Studio Code
+**Requires VS Code 1.97.0 or later.**
+
+- **Marketplace:** Extensions view (Cmd /Ctrl +Shift +X ) → search **Random, Fake & Mock Data Generator** by _ElecTreeFrying_ → **Install**.
+- **CLI:** `code --install-extension ElecTreeFrying.insert-random-text`
+- **Direct:** [VS Code Marketplace listing][package]
+
+---
+
+## Compatibility
+
+- **VS Code** 1.97.0 or later.
+- **Compatible hosts:** Cursor, VSCodium, Code Server, and other forks that implement the VS Code API at the same engine version.
+- **Web:** runs on [vscode.dev](https://vscode.dev) and github.dev — a browser build ships in the extension.
+- **Platforms:** macOS, Windows, Linux.
+- **Privacy:** No network calls, no telemetry, no model calls — every value is generated locally by the bundled library.
+
+---
+
+## Troubleshooting
+
+If a command doesn't insert, or a value looks wrong, please open an issue on [GitHub Issues][issues].
+
+[issues]: https://github.com/ElecTreeFrying/insert-random-text/issues
+
+---
+
+## FAQ
+
+**How do I insert a random UUID in VS Code?**
+Cmd /Ctrl +Shift +P → **Insert Random: UUID**. Need a specific rendering? **UUID (Format…)** picks uppercase, braces, or no dashes. Every cursor gets its own value.
+
+**How do I generate fake names, emails, and addresses in VS Code?**
+Run **Insert Random: Person / Email / Street Address** directly (Person inserts a full name), or browse all 130+ types with **Insert Random: Pick…**.
+
+**How do I generate mock JSON, CSV, or SQL test data?**
+**Insert Random: Record…** drops a composed object, `INSERT` row, or CSV line at every cursor; **Generate Dataset…** pours up to 100,000 rows into a new file.
+
+**Can I get the same values every run (seeded / reproducible data)?**
+Yes — set `insertRandomText.seed` and every run replays the identical sequence, per locale, datasets included.
+
+**Does it work offline — does my data stay private?**
+Fully offline: the faker library ships inside the extension, and there are no network calls, no telemetry, no model calls. It runs in the browser on vscode.dev the same way.
+
+**How do I fill multiple cursors with a different value in each?**
+Stack cursors (Cmd /Ctrl +Option /Alt +↓ ), then run any Insert Random command — each cursor draws its own value (`insertRandomText.uniquePerCursor`, on by default).
+
+---
## Changelog
-See [CHANGELOG] for more information.
+See [CHANGELOG.md][changelog] for full release notes.
+
+[changelog]: CHANGELOG.md
+
+---
## Contributing
-* File bugs, or any feature requests in [GitHub Issues].
-* Leave a review on [Visual Studio Marketplace].
+Contributions, bug reports, and feature requests are welcome in [GitHub Issues][issues].
+
+---
## Support
-### Donate by Bitcoin (BTC)
+**This extension is free and always will be.** If it's become part of your workflow, here are a few ways to give back:
-bc1q9hjnxk67c9y6tsyp8jde43xg9hacf0kgdxq6jsxl47666d3hk8aqunv0xr
+- Star the repo on [GitHub][repo]
+- Leave a review on the [VS Code Marketplace][reviews]
+- Send a donation to any address below
-
+[reviews]: https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.insert-random-text&ssr=false#review-details
-### Donate by Mining
+| Network | Address |
+|---|---|
+| **Bitcoin** | `bc1q4j2uewfphjmca83905qv37vcl4jh8va5yupl7w` |
+| **Solana** | `EHtTGyRoDAK44KBGrEoypAWyPpResHUqwufKnuLs7Tyy` |
+| **Sui** | `0xcaf8ff4a65d7e35d961abd0203180013b7fe974d4fa0313e880c39c45ada2b09` |
+| **ERC-20** (Ethereum / Base / Monad / Polygon / HyperEVM) | `0xd25f84Ed2F76dF2F0C8f1207402eF9e15b5d7855` |
-| Mining address (NiceHash) |
-|:----------------------------------:|
-| 3GJoX9cKs7eUHr6n5LcwNYEkSoD6mEqb1r |
+---
## Related
-[More extensions of mine.]
+- **[All extensions by ElecTreeFrying][all]** on the VS Code Marketplace.
-## License
+[all]: https://marketplace.visualstudio.com/publishers/ElecTreeFrying
-MIT
+---
-[version svg]: https://vsmarketplacebadge.apphb.com/version/ElecTreeFrying.insert-random-text.svg
-[downloads svg]: https://vsmarketplacebadge.apphb.com/downloads/ElecTreeFrying.insert-random-text.svg
-[ratings svg]: https://vsmarketplacebadge.apphb.com/rating-short/ElecTreeFrying.insert-random-text.svg
-[package]: https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.insert-random-text
+## License
-[VS Code]: https://code.visualstudio.com/
-[extension]: https://marketplace.visualstudio.com/VSCode
+[MIT][license]
-[CHANGELOG]: https://marketplace.visualstudio.com/items/ElecTreeFrying.insert-random-text/changelog
-[Github Issues]: https://github.com/ElecTreeFrying/insert-random-text/issues
-[Visual Studio Marketplace]: https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.insert-random-text&ssr=false#review-details
-[More extensions of mine.]: https://marketplace.visualstudio.com/publishers/ElecTreeFrying
+[license]: https://marketplace.visualstudio.com/items/ElecTreeFrying.insert-random-text/license
diff --git a/SPEC.md b/SPEC.md
new file mode 100644
index 0000000..f669084
--- /dev/null
+++ b/SPEC.md
@@ -0,0 +1,714 @@
+# Random, Fake & Mock Data Generator — Functionality Specification
+
+A VS Code extension (id `insert-random-text`, publisher `ElecTreeFrying`) that inserts random, fake & mock data — names, emails, addresses, finance, git, UUIDs, lorem ipsum, mock JSON, and ~130 types in all — at **every cursor**, at the **top of the file**, or onto the **clipboard**. A [Record command](#multi-field-records) composes several types into one structured record — a JSON object, SQL row, or CSV line — and a [Generate Dataset command](#dataset-generation) turns the same records into a whole new file, up to 100,000 rows. Every value is generated locally by [`@faker-js/faker`](https://fakerjs.dev) — in any of six [locales](#locales) (`en` by default, plus `de` / `fr` / `es` / `pt_BR` / `ja`); there are no network calls and no telemetry.
+
+**137 generator types across 20 categories** (plus 6 hidden back-compat variants — 143 registry entries in all), **176 contributed commands**, **no default keybindings**, **fifteen configuration settings**, and **one editor context-menu submenu**.
+
+The generation logic is `vscode`-free and decoupled from the editor glue: a generator produces a value, a formatter renders a block, a quote policy decides the wrapping, and a thin activation layer maps commands and cursors onto that pipeline. Each stage is documented below.
+
+---
+
+## Commands
+
+The extension contributes **176 commands**, in seven families:
+
+| Family | Count | Id shape | Purpose |
+|---|---|---|---|
+| Generator commands | 143 | `extension.insertRandom*` (legacy, 14) · `insertRandomText.` (modern, 129) | Insert one data type. Each maps to exactly one registry entry (see [Data Catalog](#data-catalog)). |
+| Quick Pick | 1 | `insertRandomText.pick` | "Insert Random: Pick…" — a searchable menu over the whole catalog. |
+| Record | 1 | `insertRandomText.record` | "Insert Random: Record…" — compose several types into one structured record (see [Multi-Field Records](#multi-field-records)). |
+| Generate Dataset | 1 | `insertRandomText.generateDataset` | "Insert Random: Generate Dataset…" — build up to 100,000 records and open them as a new file (see [Dataset Generation](#dataset-generation)). |
+| Randomize Selection | 1 | `insertRandomText.randomizeSelection` | "Insert Random: Randomize Selection" — anonymize in place: a selection that IS an email / UUID / ISO date gets a fresh fake of the same type; everything else a same-shape randomization (see [Randomize Selection](#insert-random-randomize-selection)). |
+| Prompted commands | 13 | `insertRandomText.numberRange` / `floatRange` / `stringLength` / `dateBetween` / `wordsCount` / `sentencesCount` / `paragraphsCount` / `uuidFormat` / `passwordOptions` / `phoneFormat` / `fromTemplate` / `fromPattern` / `sequence` | Ask for parameters in input boxes and Quick Picks, then insert through the normal pipeline (see [Parameterized commands](#parameterized-commands-prompted)). |
+| Settings commands | 16 | `insertRandomText.set*` / `toggle*` / `manage*` / `resetSettings` | Change any setting from the Command Palette (see [Settings Commands](#settings-commands)). |
+
+Every command title is prefixed **`Insert Random:`**, so typing "Insert Random" in the Command Palette (Cmd /Ctrl +Shift +P ) surfaces all of them. **No keybindings are contributed** — the extension ships zero default key bindings, so nothing conflicts with the user's existing bindings out of the box; any command can be bound manually in *Keyboard Shortcuts* (search **Insert Random**). Binding `insertRandomText.pick` gives one-shortcut access to the whole catalog.
+
+### Two command namespaces
+
+- **Legacy `extension.insertRandom*` (14 commands)** — the original ids, kept verbatim for **back-compat** so existing user keybindings keep working. They cover eight visible generators (`animal`, `person`, `date`, `country`, `number`, `string`, `lorem`, `hash`) and the six hidden Lorem/Hash size variants. These eight generators have **no** modern `insertRandomText.*` command — the legacy id is their only command.
+- **Modern `insertRandomText.` (129 commands)** — every other generator. The command suffix is byte-identical to the generator's registry `id` (e.g. `insertRandomText.creditCard` → generator `creditCard`).
+
+Both namespaces register through a single `COMMAND_TO_GENERATOR` map (143 entries) in `extension.ts`; each registered command calls one shared `insertGenerated(id)`. See the [Data Catalog](#data-catalog) for the exact command id of every type.
+
+### Insert Random: Pick…
+
+`insertRandomText.pick` opens a Quick Pick listing every **visible** generator (hidden back-compat variants are excluded), grouped under category separator headings in registry order. When the user has [saved templates or custom lists](#user-defined-data-saved-templates--custom-lists), those lead the picker as **Templates** and **Custom Lists** groups, ahead of the catalog. The picker placeholder reads *"Insert Random — pick a type to insert at every cursor…"*, and `matchOnDescription` is on, so typing filters against each entry's registry id as well as its label (user entries expose their template text / list values as the description instead). Selecting an entry routes through the same `insertGenerated` path as a direct command — so the active [Insert Target](#insert-targets), [output format](#output-formats), and [quote policy](#quote-wrapping--language-aware-quoting) all apply. Pressing Escape dismisses the picker with no insertion.
+
+### Insert Random: Record…
+
+`insertRandomText.record` opens the same catalog picker in **multi-select** mode — led by the user's [custom lists](#user-defined-data-saved-templates--custom-lists) when defined: tick any number of fields and one composed record — a JSON object, SQL `INSERT` row, or CSV line, per the `recordFormat` setting — is inserted at every cursor. Fully specified in [Multi-Field Records](#multi-field-records).
+
+### Insert Random: Generate Dataset…
+
+`insertRandomText.generateDataset` points the record machinery at a **file** instead of the cursor: the same multi-select field picker, then a shape pick, then a row count, and the composed dataset opens as a **new untitled document**. It is not an insertion — [`insertType`](#insert-targets) never applies and no editor needs to be open. Fully specified in [Dataset Generation](#dataset-generation).
+
+### Insert Random: Randomize Selection
+
+`insertRandomText.randomizeSelection` anonymizes **in place**, replacing every **non-empty** selection in two tiers:
+
+1. **Type-aware replace** — a selection that IS an unambiguous typed value becomes a **fresh realistic fake of the same type**, drawn through faker and dressed like the original. Detection (`detect` in the pure `src/randomize.ts`) is whole-string, anchored, and unpadded — deliberately conservative, because a false positive would replace the wrong kind of value while the tier-2 scramble is always safe:
+
+| Detected | Accepted shape | Replacement |
+|---|---|---|
+| Email | `local@domain.tld` — a TLD is required | A fresh `internet.email()` |
+| UUID | 8-4-4-4-12 hex with dashes, either case, optionally `{braced}` | A fresh `string.uuid()`, re-dressed by `shapeUuid` — uppercase iff the original's hex letters were uniformly uppercase; braces kept |
+| ISO date | `YYYY-MM-DD`, calendar-valid (the explicit check V8's rollover parsing would skip) | A fresh `date.anytime()` rendered as `YYYY-MM-DD` |
+| ISO timestamp | `YYYY-MM-DDTHH:mm:ss[.mmm]Z` — UTC `Z` only, time fields valid | A fresh `date.anytime().toISOString()`, milliseconds stripped by `shapeTimestamp` when the original carried none |
+
+ Numbers are deliberately **not** detected — tier 2 already yields a fresh same-shape number. A 32-hex hash, an offset timestamp (`…+02:00`), or any padded/embedded value falls through to tier 2.
+
+2. **Format-preserving randomization** — everything else is replaced per character: a digit becomes a random digit, `a–z` a random lowercase letter, `A–Z` a random uppercase letter, and everything else (punctuation, whitespace, non-ASCII) stays exactly where it was. `3.14` stays number-shaped. The mapping lives in the pure `src/randomize.ts` (`randomize(text, rng)`); surrogate pairs are iterated as whole code points, so emoji pass through unsplit.
+
+- **A replacement, not an insertion.** The insert pipeline is bypassed: [`insertType`](#insert-targets), quote wrapping, trailing newline, `bulkCount`, `outputFormat`, and `uniquePerCursor` never apply. After the edit each cursor collapses to sit right after its replacement — same no-stays-selected contract as a Cursor-mode insert.
+- **Seed and locale apply.** Every draw — the tier-1 typed redraws and each tier-2 character draw (`number.int`) — goes through the shared faker RNG, so a pinned [`seed`](#seeding--reproducibility) reproduces the same scrub, in draw order across selections.
+- **Empty selections are skipped**: a mixed multi-selection randomizes only the non-empty ones, and bare carets receive nothing. With no non-empty selection anywhere (or no editor at all), one info message is shown and nothing is edited (see [UX & Notifications](#ux--notifications)).
+- Contributed to the [context-menu submenu](#context-menu) in its own trailing group.
+
+### Parameterized commands (prompted)
+
+Thirteen commands ask for parameters — in input boxes, Quick Picks, or a mix — then insert through the **same pipeline** as every other command — the [insert target](#insert-targets), [output format](#output-formats), [quote policy](#quote-wrapping--language-aware-quoting), [`bulkCount`, `uniquePerCursor`](#multi-cursor-fill--bulk-generation), and [`seed`](#seeding--reproducibility) all apply:
+
+| Command | Id | Prompts | Draw |
+|---|---|---|---|
+| Insert Random: Number (Range…) | `insertRandomText.numberRange` | min, then max — whole numbers, `min ≤ max` enforced at the max box | A fresh integer in `[min, max]` per value |
+| Insert Random: Float (Range…) | `insertRandomText.floatRange` | min, then max — any finite numbers, `min ≤ max`; the range must contain a multiple of 0.01 | A fresh float in `[min, max]`, rendered with 2 decimals (same style as the Float type) |
+| Insert Random: String (Length…) | `insertRandomText.stringLength` | length — a whole number 1–1000 | A fresh alphanumeric string of exactly that length (same charset as the String type) |
+| Insert Random: Date (Between…) | `insertRandomText.dateBetween` | from, then to — `YYYY-MM-DD` or full ISO 8601, `from ≤ to` enforced at the to box; impossible calendar dates (e.g. `2026-02-31`) are rejected | A fresh instant in `[from, to]` per value, rendered per the [`dateFormat`](#configuration-reference) setting like the zero-argument Time types |
+| Insert Random: Words (Count…) | `insertRandomText.wordsCount` | count — a whole number 1–100 | Exactly that many lorem words, space-separated |
+| Insert Random: Sentences (Count…) | `insertRandomText.sentencesCount` | count — a whole number 1–100 | Exactly that many lorem sentences, space-separated |
+| Insert Random: Paragraphs (Count…) | `insertRandomText.paragraphsCount` | count — a whole number 1–100 | Exactly that many lorem paragraphs, newline-separated |
+| Insert Random: UUID (Format…) | `insertRandomText.uuidFormat` | format — a Quick Pick: Lowercase (default) / UPPERCASE / Braced / No dashes / UPPERCASE, no dashes | A fresh UUID per value, re-rendered per the picked format (a pure post-transform of faker's lowercase-dashed uuid) |
+| Insert Random: Password (Options…) | `insertRandomText.passwordOptions` | length — a whole number 8–128, then a symbols Quick Pick: No symbols (default) / Include symbols | A fresh password of exactly that length — letters and digits, plus `!@#$%^&*` when symbols are included |
+| Insert Random: Phone (Format…) | `insertRandomText.phoneFormat` | style — a Quick Pick: Human (default) / National / International | A fresh phone number in the picked faker style (`human` / `national` / `international`) |
+| Insert Random: From Template… | `insertRandomText.fromTemplate` | template — free text with `{{module.method}}` mustache placeholders, call arguments included (e.g. `{{string.numeric(3)}}`) | The template re-rendered per value — every placeholder drawn fresh (faker `helpers.fake`) |
+| Insert Random: From Pattern… | `insertRandomText.fromPattern` | pattern — a regex-like string; faker supports a limited subset (character classes, ranges, quantifiers — unsupported syntax passes through as literal text) | A fresh string matching the pattern per value (faker `helpers.fromRegExp`) |
+| Insert Random: Sequence (Start/Step…) | `insertRandomText.sequence` | start, then step — whole numbers; step may be negative (counts down) or zero (repeats) | **Not random:** an incrementing counter — the first value is `start`, each next value adds `step`, advancing per cursor and bulk item within one insert; the next insert restarts at `start` |
+
+Behaviors:
+
+- **Validation is live** (`validateInput`): invalid text shows an inline error and blocks accept — empty, non-numeric, fractional-where-integer, out-of-range, and `max < min` inputs never reach the generator; dates additionally reject non-`YYYY-MM-DD`/ISO shapes, impossible calendar dates, and `to < from`. Quick Pick steps are closed sets and need no validation. The free-form **template/pattern boxes prove their input by test-rendering it** — a failing render (unresolvable `{{expression}}`, out-of-order range, bad quantifier) shows faker's error plus a working example; empty input is rejected outright (faker would render it to an empty string).
+- **Last-used values are remembered** (`globalState`, trimmed) and prefilled on the next run; before first use the prefills reproduce the matching zero-argument type (Number: 0–1000, Float: 0–1000, String: 15, Password: 15, lorem counts: 3, Sequence: start 1 / step 1), a wide decade (Date: 2020-01-01 – 2030-12-31), or the documented examples (Template: `{{person.firstName}} <{{internet.email}}>`, Pattern: `[A-Z]{3}-[0-9]{4}`). The **last pick is remembered too** — on the next run it floats to the top of its Quick Pick, marked *Last used*; before first use the options appear in declared order, default first (UUID: Lowercase, Password: No symbols, Phone: Human).
+- **Esc at any box or pick cancels cleanly** — nothing is inserted, no error, and nothing new is remembered (values accepted *before* the cancel are remembered).
+- The seed is applied **after** the prompts, immediately before generation, so a pinned seed reproduces the same output regardless of typing time.
+- **Sequence is stateful per insert** — the one prompted command whose values must *relate*: its counter is created once per insert operation, spans that insert's cursors and bulk items in draw order, and is discarded afterwards (the next insert restarts at `start`). With `uniquePerCursor` off the cursors share one block, so the sequence runs inside the block and repeats per cursor. It draws nothing from the RNG — seed on or off, the output is identical.
+- These are **not** registry entries: they don't appear in the Pick… menu or the Record… field list. Each is a one-off generator built from the entered parameters and fed into the shared insert path (`insertWith`).
+
+### User-defined data (saved templates & custom lists)
+
+Two settings let the user grow the picker with their own types — no commands of their own; both surface through [Pick…](#insert-random-pick) (and Record…, for lists):
+
+- **`insertRandomText.templates`** — an object of *name → faker template string* (the same `{{module.method}}` syntax as [From Template…](#parameterized-commands-prompted)). Each named template becomes a **Templates**-group entry at the top of Pick…; selecting it re-renders the template per value (fresh placeholder draws per cursor and bulk item).
+- **`insertRandomText.customLists`** — an object of *name → string array*. Each named list becomes a **Custom Lists**-group entry (after Templates, before the catalog) that draws one random item per value, and is also offered — first — in the [Record…](#multi-field-records) field picker, where **the list name becomes the field key**. Templates stay Pick…-only: a record field wants one atomic value, which a list draw is and a free-form template may not be.
+
+Both are validated on read (`configuration.ts`): an entry whose value is not a non-empty string (templates) / not an array with at least one string (lists) is **dropped, never thrown on**, and each drop is logged to the extension host console; non-string items inside a list are filtered out the same way. Empty settings contribute no groups — the picker opens on the catalog as before. Template *content* is only shape-checked at read time (rendering needs the engine): a template that fails to render — e.g. `{{nope.nothing}}` — shows an error message naming the template and pointing at **Manage Templates**, and inserts nothing.
+
+Selections ride the normal pipeline (`insertWith`): insert target, quote policy, output format, bulk, multi-cursor, and seed all apply. The wrapped generators never enter the catalog registry, so a name that matches a registry id shadows nothing. Editing happens in Settings — **Insert Random: Manage Templates** / **Manage Custom Lists** jump straight to the key (`workbench.action.openSettings`); there is no bespoke editor UI. Both settings are deliberately **excluded from Reset Settings to Defaults** (see [Reset](#reset)).
+
+---
+
+## Insert Targets
+
+Where a generated value is delivered is governed by the `insertType` setting (`Cursor` / `Top` / `Clipboard`, normalized internally to `cursor` / `top` / `clipboard`). Every insert command honors it — the generator commands, the Quick Pick, and the [Record command](#multi-field-records) alike.
+
+### Cursor (default)
+
+Fill a fresh block at **every** selection in the active editor.
+
+- The extension iterates `editor.selections` and, in one `editor.edit`, calls `builder.replace(selection, block)` for each — so the block **replaces** any selected text and lands at a bare caret when the selection is empty. This is the multi-cursor fill.
+- After the edit, every cursor is **collapsed to sit right after its inserted block** — nothing stays selected, same as if the value had been typed. (Applies to the Record command's cursor fill too.)
+- Blocks are built with `buildBlocks(selections.length, generator, options)`, honoring [`uniquePerCursor`](#multi-cursor-fill--bulk-generation): a *different* value at each cursor when on, the *same* value repeated when off.
+- **Requires an active editor.** With no editor focused the command is a silent no-op.
+
+### Top
+
+Insert **one** block at the very start of the document.
+
+- A single block is built with `uniquePerCursor: false` and inserted at `Position(0, 0)` (line 1, column 1); existing content is pushed down. Cursor count is irrelevant — Top always inserts exactly one block.
+- Whether the block ends with a newline (pushing prior line-1 content onto its own line) depends on the [Trailing new line](#configuration-reference) setting.
+- **Requires an active editor.** Silent no-op when none is focused.
+
+### Clipboard
+
+Copy a value to the system clipboard — **no editor needed**.
+
+- One block is built with the quote **forced off** (`quote: ''`, a bare value) *unless* the [output format](#output-formats) is `quotedList` (whose whole point is quoting, so the resolved quote is kept), plus `newline: ''` and `uniquePerCursor: false`.
+- The value is written via `vscode.env.clipboard.writeText`, then a status-bar message `$(clippy) Copied random to clipboard` shows for ~2.5s.
+- [`bulkCount`](#multi-cursor-fill--bulk-generation) and [output format](#output-formats) still apply: a bulk clipboard copy yields multiple values laid out per the format (e.g. a JSON array, or bare values one per line for `plain`).
+- A [record](#multi-field-records) copy is the record text **verbatim** — nothing stripped or wrapped, escaping stays shape-driven — with its own `$(clippy) Copied random record to clipboard` toast.
+
+| Target | Editor required | Cursor count used | Quote wrapping | Trailing newline | Confirmation |
+|---|---|---|---|---|---|
+| **Cursor** | Yes (else no-op) | All selections | Per [quote policy](#quote-wrapping--language-aware-quoting) | Per setting | Silent |
+| **Top** | Yes (else no-op) | Ignored (one block at `0,0`) | Per quote policy | Per setting | Silent |
+| **Clipboard** | No | Ignored (one block) | Off, except `quotedList` | Off | Status-bar toast |
+
+---
+
+## Multi-Cursor Fill & Bulk Generation
+
+Two independent multipliers decide how many values a single command produces.
+
+### Unique value per cursor
+
+`insertRandomText.uniquePerCursor` (default **on**) controls the multi-cursor fill in `buildBlocks`:
+
+- **On** — each cursor gets its own `formatBlock` call, so every cursor draws **fresh** values. This is the core "fill a column with a different value in each row" behavior.
+- **Off** — one block is computed once and the identical string is repeated at every cursor.
+
+Top and Clipboard targets always build with `uniquePerCursor: false` internally (they produce a single block regardless of this setting).
+
+### Bulk count
+
+`insertRandomText.bulkCount` (default `1`, range `1`–`1000`) is how many values are generated **at each cursor**. In `formatBlock` the count is clamped defensively — `Math.max(1, Math.floor(bulkCount || 1))` — so `0`, negatives, `NaN`, or a fractional value degrade to a sane whole number ≥ 1 rather than erroring.
+
+### Strict unique
+
+`insertRandomText.strictUnique` (default **off**) re-draws duplicates wherever values are already *meant* to differ within one insert operation — the values of a bulk block, and values across cursors when [unique value per cursor](#unique-value-per-cursor) is on (one seen-set spans the whole operation). When unique value per cursor is **off**, cursors repeat one block by design, so strict unique dedups only *within* that shared block.
+
+Bounded honestly: each value gets at most **25 re-draws** (`UNIQUE_RETRIES` in `formatter.ts`); a small domain (booleans, weekdays) that runs out keeps its duplicate and moves on — never a hang, never an error. Every re-draw consumes RNG state, so the budget is part of the seeded-output contract — a seeded run is still fully reproducible with the setting on, but toggling it (or changing the budget) shifts the sequence. Applies to single-type inserts, catalog and parameterized alike; [records](#multi-field-records) and [datasets](#dataset-generation) don't read it.
+
+### Fresh value per call
+
+`Generator.generate()` is invoked **once per value** and must never be memoized — the registry shape (a `generate()` function, not a cached getter) structurally prevents the historical "double-draw" bug. With `C` cursors, `bulkCount = N`, and unique-per-cursor on, one command performs `C × N` independent `generate()` calls. With a [seed](#seeding--reproducibility) set, that whole sequence is reproducible.
+
+---
+
+## Output Formats
+
+`insertRandomText.outputFormat` decides how the `bulkCount` values **at a single cursor** are laid out. It is orthogonal to multi-cursor fill (each cursor renders its own block in the chosen format). The trailing newline (when [enabled](#configuration-reference)) is appended after the whole block in all three formats.
+
+| Format | Rendering | Example (`bulkCount = 2`) | Quote handling |
+|---|---|---|---|
+| **`plain`** (default) | One value per line — values joined by `\n`. | `alpha` `bravo` | Each value wrapped per the [quote policy](#quote-wrapping--language-aware-quoting). |
+| **`jsonArray`** | A JSON array via `JSON.stringify` per value: `[ … ]` with a leading/trailing space. | `[ "alpha", "bravo" ]` | **Bypasses** the quote policy — `JSON.stringify` always emits double-quoted, JSON-escaped strings. Numeric/boolean generators are still emitted as quoted strings (every `generate()` returns a string). |
+| **`quotedList`** | A comma-space–separated list of wrapped values. | `"alpha", "bravo"` | Each value wrapped per the quote policy (this is the one format that keeps quotes even on a Clipboard copy). |
+
+---
+
+## Quote Wrapping & Language-Aware Quoting
+
+Values are optionally wrapped in quotes so they land as valid string literals. The **generated value is universal** — only the *wrapping* is language-specific. `withQuote` and the file's `languageId` feed one resolver, `resolveQuotePolicy`, which returns a `{ quote, escape }` pair.
+
+### Settings
+
+| Setting | Effect |
+|---|---|
+| `withQuote` (default **on**) | Master switch. Off ⇒ no wrapping at all. When on, the quote + escape are resolved automatically from the file's language. |
+
+### Resolution precedence
+
+`resolveQuotePolicy(languageId, { withQuote })` evaluates in this order — the first match wins:
+
+1. **`withQuote` off** → `{ quote: '', escape: 'backslash' }` — no wrapping.
+2. **SQL-family language** → `{ "'", 'sqlDouble' }` — single quotes, doubling an embedded `'`.
+3. **Everything else** (including `languageId` `undefined` — no active editor, e.g. a Clipboard insert with nothing focused) → `{ '"', 'backslash' }`.
+
+### Language buckets
+
+| Bucket | Language IDs | Resolved quote | Escape | Why |
+|---|---|---|---|---|
+| **SQL family** (5) | `sql`, `mysql`, `pgsql`, `plsql`, `sqlite` | `'` | sqlDouble | SQL string literals are single-quoted; an embedded `'` is escaped by **doubling** it. |
+| **Everything else** | JSON, Go, Java, Rust, C/C++, C#, … and JS/TS, Python, Ruby, PHP, every unlisted language, and the no-editor case | `"` | backslash | Double quotes are a valid string literal everywhere (and the only legal quote in JSON/Go/Java/Rust/…), and stay apostrophe-clean. |
+
+### Escape styles
+
+`wrap(value, quote, escape)` wraps one value, escaping the quote character inside so the literal stays valid:
+
+- **`backslash`** (default) — backslash-escape any `\`, then the quote char. `O'Brien` → `'O\'Brien'`.
+- **`sqlDouble`** — double the quote char, no backslashing. `O'Brien` → `'O''Brien'`.
+
+With no quote (`quote === ''`) the value is returned untouched. The `jsonArray` output format never calls `wrap` — it delegates escaping to `JSON.stringify`.
+
+---
+
+## Seeding & Reproducibility
+
+`insertRandomText.seed` (a string; blank by default) makes output deterministic. Before **every** insert command, `applySeed()` runs:
+
+1. Trim the setting. **Blank ⇒ return** (faker stays random).
+2. Parse with `Number(raw)`. **`NaN` ⇒ skip** (a non-numeric value left in `settings.json` leaves faker random; the [Set Seed](#settings-commands) command's input box rejects non-numeric input up front).
+3. Otherwise call `faker().seed(value)`.
+
+Because the seed is re-applied at the **start of each command**, faker is reset to the same starting point every time — so two separate runs of the same command with the same seed produce **identical** output. Within a single command, the sequence advances, so multi-cursor and bulk values differ from one another while remaining reproducible run-to-run.
+
+Sequences are **per locale**: under the same seed, `de` draws different (German) values than `en`, but each locale reproduces its own sequence run-to-run (see [Locales](#locales)).
+
+---
+
+## Locales
+
+`insertRandomText.locale` picks the language/region every generator draws from. Six faker locale data sets ship in the bundle:
+
+| Value | Data set |
+|---|---|
+| `en` (default) | English |
+| `de` | German — Deutsch |
+| `fr` | French — Français |
+| `es` | Spanish — Español |
+| `pt_BR` | Brazilian Portuguese — Português (Brasil) |
+| `ja` | Japanese — 日本語 |
+
+- **Everything follows it** — single inserts, [record fields](#multi-field-records), [prompted commands](#parameterized-commands-prompted), and template/pattern rendering (including [saved templates](#user-defined-data-saved-templates--custom-lists)) all draw from the active locale.
+- **Fallback** — a type without localized data falls back through faker's locale chain (e.g. `de` → `en` → `base`), so every generator always yields a value; locale-free types (UUIDs, numbers, hashes, …) are unaffected.
+- **Per-locale instances, cached** — `engine.ts` holds one faker instance per locale, imported on first use and cached; `faker()` returns the **active** one. Switching applies to the **next insert** with no reload (the [config watcher](#configuration-flow) also swaps eagerly).
+- **Normalization** — an unknown or unshipped value (e.g. `zh_CN`, `pt-br`, `DE`) falls back to `en`; locale ids are exact, as faker spells them.
+
+Change it from the palette with [Set Locale](#settings-commands).
+
+---
+
+## Multi-Field Records
+
+`insertRandomText.record` — **"Insert Random: Record…"** — composes several catalog types into **one structured record**: a JSON object, a SQL `INSERT` row, or a CSV line. It is the one insert command that renders multiple fields per value; delivery follows the [`insertType` setting](#insert-targets) like every other insert.
+
+### Flow
+
+1. A **multi-select** Quick Pick (`canPickMany`) lists every visible generator under the same category separators as [Pick…](#insert-random-pick) — led by a **Custom Lists** group when the user has [saved lists](#user-defined-data-saved-templates--custom-lists) — placeholder *"Pick fields for the record…"*, `matchOnDescription` on. Tick any number of fields.
+2. Field order in the record follows **picker display order**, regardless of the order the fields were ticked in: picked custom lists first (keyed by their name), then catalog fields in catalog order.
+3. The [seed](#seeding--reproducibility) is applied, then the record is delivered per the [insert target](#insert-targets): a block at **every** selection (replacing selected text) in Cursor mode, a single block at `0,0` in Top mode, or a verbatim copy in Clipboard mode.
+4. An empty or cancelled pick inserts nothing. With **no active editor**, Cursor and Top are silent no-ops; Clipboard still copies (no editor needed).
+
+### Record shapes
+
+`insertRandomText.recordFormat` picks the shape. Escaping is decided **by the shape, not the file's language** — the [quote policy](#quote-wrapping--language-aware-quoting) is bypassed, so the record stays valid syntax for its own format wherever it lands.
+
+| Shape | One record | `bulkCount` > 1 | Escaping |
+|---|---|---|---|
+| **`json`** (default) | A bare object — `{ "firstName": "Ada", "email": "a@x.dev" }` — keys are generator ids. | Records wrap into a JSON array: `[ { … }, { … } ]`. | `JSON.stringify` per key and value. |
+| **`sql`** | `INSERT INTO (firstName, email) VALUES ('Ada', 'a@x.dev');` — `` from `insertRandomText.recordSqlTable`. | One statement per line. | Values single-quoted; an embedded `'` is doubled (`''`). |
+| **`csv`** | `Ada,a@x.dev` — values only, **no header row** (only a [dataset](#dataset-generation) adds one). | One line per record. | A value containing `,`, `"`, CR, or LF is wrapped in `"…"` with internal `"` doubled; anything else is untouched. |
+
+### Which settings apply
+
+| Setting | Effect on records |
+|---|---|
+| `insertRandomText.bulkCount` | Records per cursor, stacked per shape (same ≥ 1 clamp as [bulk generation](#multi-cursor-fill--bulk-generation)). |
+| `insertRandomText.uniquePerCursor` | On — fresh records at every cursor; off — one composed block repeated at each. |
+| `insertRandomText.strictUnique` | **Ignored** — a record composes many fields into one value; the re-draw applies to [single-type inserts](#strict-unique) only. |
+| `insertRandomText.seed` | Applied before the insert, exactly as for single-value commands. |
+| `insertType` | Honored — cursors, top of file, or clipboard, exactly as for [single values](#insert-targets). Top and Clipboard build a **single** record. |
+| `insertRandomText.dateFormat` | Honored — a timestamp Time field renders per the setting, same as a single-value insert. |
+| `insertRandomText.locale` | Honored — every field draws from the active locale's data set (see [Locales](#locales)). |
+| `withQuote` · `withNewLine` · `insertRandomText.outputFormat` | **Ignored.** Quoting/escaping is shape-driven — language-aware wrapping would corrupt the record. |
+
+---
+
+## Dataset Generation
+
+`insertRandomText.generateDataset` — **"Insert Random: Generate Dataset…"** — builds the same [records](#multi-field-records) in volume and opens them as a **new untitled document** instead of inserting at the cursor. A file, not an insertion: [`insertType`](#insert-targets) never applies, the active editor is never touched, and the command **works with no editor open at all**.
+
+### Flow
+
+1. The same **multi-select field picker** as [Record…](#multi-field-records) — custom lists lead and their name becomes the field key — with the placeholder *"Pick fields for the dataset…"*.
+2. A **shape pick** — JSON / SQL / CSV — with the current `insertRandomText.recordFormat` value floated to the top and marked *"$(check) Current record format"* (so Enter accepts the configured shape). The pick does **not** change the setting.
+3. A **row-count input box**, prefilled with the `insertRandomText.bulkCount` setting. Validated live: a whole number, `1` to the hard cap of **100,000**. Above **10,000** a modal confirmation interposes (*"Generate N rows?"* / **Generate**) — dismissing it generates nothing.
+4. The [seed](#seeding--reproducibility) is applied, the dataset is built, and it opens as a new **untitled** document via `openTextDocument({ content, language })` — focused and ready to save. Esc at any step is a clean cancel: nothing is generated, nothing opens.
+
+### Dataset shapes
+
+Same field draws and per-value escaping as [record shapes](#record-shapes), rendered as a standalone file (the `dataset` option of `buildRecords` in `record.ts` — pure and unit-tested):
+
+| Shape | File rendering | Opens as |
+|---|---|---|
+| **`json`** | Always a JSON **array** — even for one row — with **one record per line** (a 100k-row single-line array would choke the editor's tokenizer). | `json` |
+| **`sql`** | One `INSERT INTO (…) VALUES (…);` per row — `` from `insertRandomText.recordSqlTable`. | `sql` |
+| **`csv`** | A **header row of the field keys** (escaped like any CSV value — a custom-list name may contain a comma), then one line per row. Only datasets get the header — [record-at-cursor](#record-shapes) stays headerless. | `plaintext` (VS Code has no built-in csv language) |
+
+Every dataset ends with a single trailing newline, per file convention.
+
+### Which settings apply
+
+| Setting | Effect on datasets |
+|---|---|
+| `insertRandomText.bulkCount` | Only the **default** of the row-count box — the entered count is what is generated. |
+| `insertRandomText.recordFormat` | Only the **preselected** shape in the pick — the picked shape is what renders. |
+| `insertRandomText.recordSqlTable` | Table name for the `sql` shape, as for records. |
+| `insertRandomText.seed` | Applied after the prompts, before the draws — a seeded run regenerates the identical dataset. |
+| `insertRandomText.dateFormat` · `insertRandomText.locale` | Honored — exactly as for [records](#which-settings-apply). |
+| `insertType` · `withQuote` · `withNewLine` · `insertRandomText.outputFormat` · `insertRandomText.uniquePerCursor` · `insertRandomText.strictUnique` | **Ignored.** The output is a file — there is no cursor, no wrapping, and every row is a fresh draw by construction ([strict unique](#strict-unique) covers single-type inserts only). |
+
+---
+
+## Data Catalog
+
+The generator registry (`catalog.ts`) is the single source of truth — it drives generation, the contributed commands, and the Quick Pick. **137 visible generators across 20 categories**, plus **6 hidden back-compat variants**. Groups appear in the order their first member is registered (which is the Quick Pick heading order). Every value is produced by the listed faker call; numeric and boolean generators are coerced to strings before insertion.
+
+### Identity (17)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Full Name | `person` | `extension.insertRandomPerson` | `person.fullName()` |
+| First Name | `firstName` | `insertRandomText.firstName` | `person.firstName()` |
+| Middle Name | `middleName` | `insertRandomText.middleName` | `person.middleName()` |
+| Last Name | `lastName` | `insertRandomText.lastName` | `person.lastName()` |
+| Name Prefix | `prefix` | `insertRandomText.prefix` | `person.prefix()` |
+| Name Suffix | `suffix` | `insertRandomText.suffix` | `person.suffix()` |
+| Username | `username` | `insertRandomText.username` | `internet.username()` |
+| Display Name | `displayName` | `insertRandomText.displayName` | `internet.displayName()` |
+| Email | `email` | `insertRandomText.email` | `internet.email()` |
+| Phone | `phone` | `insertRandomText.phone` | `phone.number()` |
+| Sex | `sex` | `insertRandomText.sex` | `person.sex()` |
+| Gender | `gender` | `insertRandomText.gender` | `person.gender()` |
+| Bio | `bio` | `insertRandomText.bio` | `person.bio()` |
+| Zodiac Sign | `zodiac` | `insertRandomText.zodiac` | `person.zodiacSign()` |
+| Job Title | `jobTitle` | `insertRandomText.jobTitle` | `person.jobTitle()` |
+| Job Type | `jobType` | `insertRandomText.jobType` | `person.jobType()` |
+| Job Area | `jobArea` | `insertRandomText.jobArea` | `person.jobArea()` |
+
+### Numbers (6)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Number | `number` | `extension.insertRandomNumber` | `number.int({ min: 0, max: 1000 })` |
+| Float | `float` | `insertRandomText.float` | `number.float({ min: 0, max: 1000, fractionDigits: 2 })` → `toFixed(2)` |
+| Boolean | `boolean` | `insertRandomText.boolean` | `datatype.boolean()` |
+| Hex Number | `hexNumber` | `insertRandomText.hexNumber` | `number.hex({ max: 0xffffff })` |
+| Binary Number | `binary` | `insertRandomText.binary` | `number.binary({ max: 255 })` |
+| Octal Number | `octal` | `insertRandomText.octal` | `number.octal({ max: 511 })` |
+
+### Text (12)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| String | `string` | `extension.insertRandomString` | `string.alphanumeric(15)` |
+| Alpha String | `alpha` | `insertRandomText.alpha` | `string.alpha(10)` |
+| Numeric String | `numeric` | `insertRandomText.numeric` | `string.numeric(10)` |
+| Word | `word` | `insertRandomText.word` | `lorem.word()` |
+| Words | `words` | `insertRandomText.words` | `lorem.words({ min: 3, max: 6 })` |
+| Sentence | `sentence` | `insertRandomText.sentence` | `lorem.sentence()` |
+| Slug | `slug` | `insertRandomText.slug` | `lorem.slug()` |
+| Lorem Paragraph | `lorem` | `extension.insertLorem` | `lorem.paragraph()` |
+| Hacker Phrase | `hackerPhrase` | `insertRandomText.hackerPhrase` | `hacker.phrase()` |
+| Emoji | `emoji` | `insertRandomText.emoji` | `internet.emoji()` |
+| Book Title | `bookTitle` | `insertRandomText.bookTitle` | `book.title()` |
+| Book Author | `bookAuthor` | `insertRandomText.bookAuthor` | `book.author()` |
+
+### Time (8)
+
+The six timestamp types draw a `Date` and render it per the [`insertRandomText.dateFormat`](#configuration-reference) setting (default `iso` — the full ISO 8601 string, matching pre-setting behavior). Weekday and Month emit names, not timestamps, and are unaffected.
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Date | `date` | `extension.insertRandomDate` | `date.anytime()`, rendered per `dateFormat` |
+| Past Date | `pastDate` | `insertRandomText.pastDate` | `date.past()`, rendered per `dateFormat` |
+| Future Date | `futureDate` | `insertRandomText.futureDate` | `date.future()`, rendered per `dateFormat` |
+| Recent Date | `recentDate` | `insertRandomText.recentDate` | `date.recent()`, rendered per `dateFormat` |
+| Soon Date | `soonDate` | `insertRandomText.soonDate` | `date.soon()`, rendered per `dateFormat` |
+| Birthdate | `birthdate` | `insertRandomText.birthdate` | `date.birthdate()`, rendered per `dateFormat` |
+| Weekday | `weekday` | `insertRandomText.weekday` | `date.weekday()` |
+| Month | `month` | `insertRandomText.month` | `date.month()` |
+
+### Location (15)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Country | `country` | `extension.insertRandomCountry` | `location.country()` |
+| Country Code | `countryCode` | `insertRandomText.countryCode` | `location.countryCode()` |
+| State | `state` | `insertRandomText.state` | `location.state()` |
+| State Abbreviation | `stateAbbr` | `insertRandomText.stateAbbr` | `location.state({ abbreviated: true })` |
+| County | `county` | `insertRandomText.county` | `location.county()` |
+| City | `city` | `insertRandomText.city` | `location.city()` |
+| Zip Code | `zipCode` | `insertRandomText.zipCode` | `location.zipCode()` |
+| Street Name | `street` | `insertRandomText.street` | `location.street()` |
+| Street Address | `address` | `insertRandomText.address` | `location.streetAddress()` |
+| Secondary Address | `secondaryAddress` | `insertRandomText.secondaryAddress` | `location.secondaryAddress()` |
+| Building Number | `buildingNumber` | `insertRandomText.buildingNumber` | `location.buildingNumber()` |
+| Direction | `direction` | `insertRandomText.direction` | `location.direction()` |
+| Latitude | `latitude` | `insertRandomText.latitude` | `location.latitude()` |
+| Longitude | `longitude` | `insertRandomText.longitude` | `location.longitude()` |
+| Time Zone | `timeZone` | `insertRandomText.timeZone` | `location.timeZone()` |
+
+### Network (11)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| IP Address | `ipv4` | `insertRandomText.ipv4` | `internet.ipv4()` |
+| IPv6 Address | `ipv6` | `insertRandomText.ipv6` | `internet.ipv6()` |
+| MAC Address | `mac` | `insertRandomText.mac` | `internet.mac()` |
+| URL | `url` | `insertRandomText.url` | `internet.url()` |
+| Domain Name | `domainName` | `insertRandomText.domainName` | `internet.domainName()` |
+| Port | `port` | `insertRandomText.port` | `internet.port()` |
+| Protocol | `protocol` | `insertRandomText.protocol` | `internet.protocol()` |
+| HTTP Method | `httpMethod` | `insertRandomText.httpMethod` | `internet.httpMethod()` |
+| HTTP Status Code | `httpStatus` | `insertRandomText.httpStatus` | `internet.httpStatusCode()` |
+| User Agent | `userAgent` | `insertRandomText.userAgent` | `internet.userAgent()` |
+| JWT | `jwt` | `insertRandomText.jwt` | `internet.jwt()` |
+
+### Media (2)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Image URL | `imageUrl` | `insertRandomText.imageUrl` | `image.url()` |
+| Avatar URL | `avatarUrl` | `insertRandomText.avatarUrl` | `image.avatar()` |
+
+### Design (4)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Color (hex) | `color` | `insertRandomText.color` | `color.rgb({ format: 'hex' })` |
+| Color (rgb) | `rgb` | `insertRandomText.rgb` | `color.rgb({ format: 'css' })` |
+| Color (hsl) | `hsl` | `insertRandomText.hsl` | `color.hsl({ format: 'css' })` |
+| Color Name | `colorName` | `insertRandomText.colorName` | `color.human()` |
+
+### Security (1)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Password | `password` | `insertRandomText.password` | `internet.password()` |
+
+### IDs (5)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| UUID | `uuid` | `insertRandomText.uuid` | `string.uuid()` |
+| ULID | `ulid` | `insertRandomText.ulid` | `string.ulid()` |
+| Nano ID | `nanoid` | `insertRandomText.nanoid` | `string.nanoid()` |
+| MongoDB ObjectId | `mongodbObjectId` | `insertRandomText.mongodbObjectId` | `database.mongodbObjectId()` |
+| Hash | `hash` | `extension.insertRandomHash` | `string.hexadecimal({ length: 13, casing: 'lower', prefix: '' })` |
+
+### Nature (6)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Animal | `animal` | `extension.insertRandomAnimal` | `animal.type()` |
+| Dog Breed | `dog` | `insertRandomText.dog` | `animal.dog()` |
+| Cat Breed | `cat` | `insertRandomText.cat` | `animal.cat()` |
+| Bird Species | `bird` | `insertRandomText.bird` | `animal.bird()` |
+| Fish Species | `fish` | `insertRandomText.fish` | `animal.fish()` |
+| Horse Breed | `horse` | `insertRandomText.horse` | `animal.horse()` |
+
+### Company (3)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Company Name | `company` | `insertRandomText.company` | `company.name()` |
+| Catch Phrase | `catchPhrase` | `insertRandomText.catchPhrase` | `company.catchPhrase()` |
+| Buzz Phrase | `buzzPhrase` | `insertRandomText.buzzPhrase` | `company.buzzPhrase()` |
+
+### Commerce (7)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Product | `product` | `insertRandomText.product` | `commerce.product()` |
+| Product Name | `productName` | `insertRandomText.productName` | `commerce.productName()` |
+| Price | `price` | `insertRandomText.price` | `commerce.price()` |
+| Department | `department` | `insertRandomText.department` | `commerce.department()` |
+| Product Material | `productMaterial` | `insertRandomText.productMaterial` | `commerce.productMaterial()` |
+| Product Description | `productDescription` | `insertRandomText.productDescription` | `commerce.productDescription()` |
+| ISBN | `isbn` | `insertRandomText.isbn` | `commerce.isbn()` |
+
+### Finance (13)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Amount | `amount` | `insertRandomText.amount` | `finance.amount()` |
+| Currency Code | `currencyCode` | `insertRandomText.currencyCode` | `finance.currencyCode()` |
+| Currency Name | `currencyName` | `insertRandomText.currencyName` | `finance.currencyName()` |
+| Currency Symbol | `currencySymbol` | `insertRandomText.currencySymbol` | `finance.currencySymbol()` |
+| Credit Card Number | `creditCard` | `insertRandomText.creditCard` | `finance.creditCardNumber()` |
+| Credit Card CVV | `creditCardCVV` | `insertRandomText.creditCardCVV` | `finance.creditCardCVV()` |
+| IBAN | `iban` | `insertRandomText.iban` | `finance.iban()` |
+| BIC | `bic` | `insertRandomText.bic` | `finance.bic()` |
+| Account Number | `accountNumber` | `insertRandomText.accountNumber` | `finance.accountNumber()` |
+| Routing Number | `routingNumber` | `insertRandomText.routingNumber` | `finance.routingNumber()` |
+| Bitcoin Address | `bitcoin` | `insertRandomText.bitcoin` | `finance.bitcoinAddress()` |
+| Ethereum Address | `ethereum` | `insertRandomText.ethereum` | `finance.ethereumAddress()` |
+| PIN | `pin` | `insertRandomText.pin` | `finance.pin()` |
+
+### Git (3)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Git Branch | `gitBranch` | `insertRandomText.gitBranch` | `git.branch()` |
+| Git Commit SHA | `gitCommitSha` | `insertRandomText.gitCommitSha` | `git.commitSha()` |
+| Git Commit Message | `gitCommitMessage` | `insertRandomText.gitCommitMessage` | `git.commitMessage()` |
+
+### System (6)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| File Name | `fileName` | `insertRandomText.fileName` | `system.fileName()` |
+| File Path | `filePath` | `insertRandomText.filePath` | `system.filePath()` |
+| File Extension | `fileExt` | `insertRandomText.fileExt` | `system.fileExt()` |
+| MIME Type | `mimeType` | `insertRandomText.mimeType` | `system.mimeType()` |
+| Semver | `semver` | `insertRandomText.semver` | `system.semver()` |
+| Cron Expression | `cron` | `insertRandomText.cron` | `system.cron()` |
+
+### Vehicle (5)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Vehicle | `vehicle` | `insertRandomText.vehicle` | `vehicle.vehicle()` |
+| Vehicle Manufacturer | `vehicleManufacturer` | `insertRandomText.vehicleManufacturer` | `vehicle.manufacturer()` |
+| Vehicle Model | `vehicleModel` | `insertRandomText.vehicleModel` | `vehicle.model()` |
+| VIN | `vin` | `insertRandomText.vin` | `vehicle.vin()` |
+| License Plate (VRM) | `vrm` | `insertRandomText.vrm` | `vehicle.vrm()` |
+
+### Food (5)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Dish | `dish` | `insertRandomText.dish` | `food.dish()` |
+| Ingredient | `ingredient` | `insertRandomText.ingredient` | `food.ingredient()` |
+| Fruit | `fruit` | `insertRandomText.fruit` | `food.fruit()` |
+| Vegetable | `vegetable` | `insertRandomText.vegetable` | `food.vegetable()` |
+| Cuisine | `cuisine` | `insertRandomText.cuisine` | `food.ethnicCategory()` |
+
+### Music (4)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Song Name | `songName` | `insertRandomText.songName` | `music.songName()` |
+| Music Genre | `musicGenre` | `insertRandomText.musicGenre` | `music.genre()` |
+| Artist | `artist` | `insertRandomText.artist` | `music.artist()` |
+| Album | `album` | `insertRandomText.album` | `music.album()` |
+
+### Travel (4)
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Airline | `airline` | `insertRandomText.airline` | `airline.airline().name` |
+| Airport | `airport` | `insertRandomText.airport` | `airline.airport().name` |
+| Flight Number | `flightNumber` | `insertRandomText.flightNumber` | `airline.flightNumber({ addLeadingZeros: true })` |
+| Seat | `seat` | `insertRandomText.seat` | `airline.seat()` |
+
+### Hidden back-compat variants (6)
+
+These carry `hidden: true` — they never appear in the Quick Pick and exist only to serve their legacy size-variant commands.
+
+| Label | id | Command | faker source |
+|---|---|---|---|
+| Lorem (small) | `loremSmall` | `extension.insertLoremSmall` | `lorem.sentence()` |
+| Lorem (medium) | `loremMedium` | `extension.insertLoremMedium` | `lorem.paragraph()` |
+| Lorem (large) | `loremLarge` | `extension.insertLoremLarge` | `lorem.paragraphs(3)` |
+| Hash (7) | `hashSmall` | `extension.insertRandomHashSmall` | `string.hexadecimal({ length: 7, casing: 'lower', prefix: '' })` |
+| Hash (17) | `hashMedium` | `extension.insertRandomHashMedium` | `string.hexadecimal({ length: 17, casing: 'lower', prefix: '' })` |
+| Hash (27) | `hashLarge` | `extension.insertRandomHashLarge` | `string.hexadecimal({ length: 27, casing: 'lower', prefix: '' })` |
+
+---
+
+## Configuration Reference
+
+Fifteen settings. Three **legacy** keys stay flat and non-namespaced (`insertType`, `withQuote`, `withNewLine`) for back-compat with existing user settings; every newer key is namespaced under `insertRandomText.*`. All are read into a typed `Settings` snapshot by `configuration.ts` (the `insertType` enum is normalized to a target there, and the two object settings are validated — junk entries dropped with a console warning). One further key — `insertRandomText.contextMenu.enabled` — is consumed by a `package.json` `when` clause rather than read in code (see [Context Menu](#context-menu)).
+
+| Setting | Type | Default | Values | Notes |
+|---|---|---|---|---|
+| `insertType` | string (enum) | `Cursor` | `Cursor` · `Top` · `Clipboard` | Where values go — normalized to `cursor` / `top` / `clipboard`. See [Insert Targets](#insert-targets). |
+| `withQuote` | boolean | `true` | `true` / `false` | Wrap each value in quotes. Master switch for the quote policy. |
+| `withNewLine` | boolean | `true` | `true` / `false` | Append a newline (`\n`) after each block. |
+| `insertRandomText.uniquePerCursor` | boolean | `true` | `true` / `false` | A different value at each cursor, or the same value repeated. |
+| `insertRandomText.strictUnique` | boolean | `false` | `true` / `false` | Re-draw duplicates within one insert wherever values are meant to differ; bounded at 25 re-draws per value, so small pools may still repeat. See [Strict unique](#strict-unique). |
+| `insertRandomText.bulkCount` | number | `1` | `1`–`1000` | How many values to insert at each cursor. Clamped to ≥ 1 at render time. Also the default row count offered by [Generate Dataset…](#dataset-generation). |
+| `insertRandomText.outputFormat` | string (enum) | `plain` | `plain` · `jsonArray` · `quotedList` | How bulk values render. See [Output Formats](#output-formats). |
+| `insertRandomText.dateFormat` | string (enum) | `iso` | `iso` · `isoDate` · `isoTime` · `unixSeconds` · `unixMillis` | How the timestamp [Time types](#time-8) render — full ISO 8601, `YYYY-MM-DD`, `HH:mm:ss`, or Unix seconds/milliseconds. ISO slices come from the UTC string; an unknown value falls back to `iso`. |
+| `insertRandomText.seed` | string | `""` | any number, or blank | Reproducible output; blank or non-numeric = random. See [Seeding](#seeding--reproducibility). |
+| `insertRandomText.locale` | string (enum) | `en` | `en` · `de` · `fr` · `es` · `pt_BR` · `ja` | Which faker locale data set generators draw from. An unknown value falls back to `en`. See [Locales](#locales). |
+| `insertRandomText.recordFormat` | string (enum) | `json` | `json` · `sql` · `csv` | Structured shape for [Record](#multi-field-records) inserts: JSON object, SQL row, or CSV line. Also the preselected shape in [Generate Dataset…](#dataset-generation). |
+| `insertRandomText.recordSqlTable` | string | `table` | any non-empty name | Table name used by the `sql` record shape. |
+| `insertRandomText.templates` | object | `{}` | name → template string | Saved faker templates — a **Templates** group atop [Pick…](#insert-random-pick). Non-string / empty entries are dropped (console-warned). See [User-defined data](#user-defined-data-saved-templates--custom-lists). |
+| `insertRandomText.customLists` | object | `{}` | name → string array | Custom value lists — a **Custom Lists** group atop Pick…, and [Record…](#multi-field-records) fields keyed by the name. Non-string items are dropped (console-warned). |
+| `insertRandomText.contextMenu.enabled` | boolean | `false` | `true` / `false` | Show the "Insert Random" editor right-click submenu. Read by a `when` clause, not by code. |
+
+---
+
+## Settings Commands
+
+`settingsCommands.ts` contributes 16 palette commands that **write or open** settings — so every setting is changeable without hunting through the Settings UI. Each is registered in `extension.ts` from a `SETTING_COMMANDS` map.
+
+| Command | Title | Mechanism |
+|---|---|---|
+| `insertRandomText.setInsertType` | Set Insert Type | Quick Pick over `Cursor` / `Top` / `Clipboard`. |
+| `insertRandomText.setOutputFormat` | Set Output Format | Quick Pick over `Plain` / `JSON array` / `Quoted list`. |
+| `insertRandomText.setDateFormat` | Set Date Format | Quick Pick over `ISO 8601 timestamp` / `ISO date` / `ISO time` / `Unix seconds` / `Unix milliseconds`. |
+| `insertRandomText.setRecordFormat` | Set Record Format | Quick Pick over `JSON object` / `SQL row` / `CSV line`. |
+| `insertRandomText.setRecordSqlTable` | Set Record SQL Table | Input box; rejects an empty table name. |
+| `insertRandomText.setBulkCount` | Set Bulk Count | Input box; validates a whole number `1`–`1000`. |
+| `insertRandomText.setSeed` | Set Seed | Input box; validates a number, or blank for random. |
+| `insertRandomText.setLocale` | Set Locale | Quick Pick over `English` / `German — Deutsch` / `French — Français` / `Spanish — Español` / `Brazilian Portuguese — Português (Brasil)` / `Japanese — 日本語`. |
+| `insertRandomText.toggleQuotes` | Toggle Wrap With Quotes | Flip `withQuote`. |
+| `insertRandomText.toggleNewLine` | Toggle Trailing New Line | Flip `withNewLine`. |
+| `insertRandomText.toggleUniquePerCursor` | Toggle Unique Value Per Cursor | Flip `insertRandomText.uniquePerCursor`. |
+| `insertRandomText.toggleStrictUnique` | Toggle Strict Unique | Flip `insertRandomText.strictUnique`. |
+| `insertRandomText.toggleContextMenu` | Toggle Editor Context Menu | Flip `insertRandomText.contextMenu.enabled`. |
+| `insertRandomText.manageTemplates` | Manage Templates | Open the Settings UI filtered to `insertRandomText.templates`. |
+| `insertRandomText.manageCustomLists` | Manage Custom Lists | Open the Settings UI filtered to `insertRandomText.customLists`. |
+| `insertRandomText.resetSettings` | Reset Settings to Defaults | Modal-confirmed reset of every key except the two data pools. |
+
+### Write target
+
+Writes go to the **open workspace** when one is present (so a change is visible immediately even where a workspace pins the setting, and project tweaks stay project-scoped), otherwise to **global** user settings. Determined by `vscode.workspace.workspaceFolders?.length > 0`.
+
+### Enum picker behavior
+
+The enum pickers (`setInsertType` / `setOutputFormat` / `setDateFormat` / `setRecordFormat` / `setLocale`) mark the current value with a `$(check) Current` description and float it to the top of the list; `matchOnDetail` is on, so typing filters against each option's one-line detail. Selecting writes the value and shows a `$(check) → ` status-bar confirmation. Escape cancels with no write.
+
+### Reset
+
+`resetSettings` shows a **modal** warning — *"Reset all Insert Random settings to their defaults? Saved templates and custom lists are kept."* — with a single **Reset** button. Only on confirm does it clear every tuning key (the `ConfigKey` entries **plus** `insertRandomText.contextMenu.enabled`) by writing `undefined`, which restores each to its package.json default, then confirms via the status bar. `insertRandomText.templates` and `insertRandomText.customLists` are **deliberately excluded** — they are user-authored content, not tuning, and a reset never deletes them (the modal says so). Dismissing the dialog changes nothing.
+
+---
+
+## UX & Notifications
+
+The extension is deliberately quiet. It uses **status-bar messages** (not modal/toast popups) for confirmations, and inserts silently.
+
+| Event | Feedback |
+|---|---|
+| Cursor / Top insert | **Silent** — no message; the inserted text is the feedback. |
+| Clipboard insert | Status bar: `$(clippy) Copied random to clipboard` (~2.5s). |
+| Setting changed (any settings command) | Status bar: `$(check) ` (~2.5s) — e.g. `Wrap with quotes: On`, `Bulk count → 25`, `Seed → 42`, `Seed cleared (random)`. |
+| Settings reset confirmed | Status bar: `$(check) Insert Random settings reset to defaults`. |
+| Reset requested | Modal warning dialog with a **Reset** button (see [Reset](#settings-commands)). |
+| `setBulkCount` invalid input | Inline input-box validation: *"Enter a whole number between 1 and 1000."* |
+| `setSeed` invalid input | Inline input-box validation: *"Enter a number, or leave blank for random."* |
+| `setRecordSqlTable` invalid input | Inline input-box validation: *"Enter a table name."* |
+| A saved template fails to render | Error message: *"‹name›" failed to render: ‹faker's error› — fix it via Insert Random: Manage Templates.* Nothing is inserted. |
+| Randomize Selection with nothing selected (or no editor) | Info message: *Select some text first — Randomize Selection replaces each selection in place.* Nothing is edited. |
+| No active editor (Cursor / Top / Record…) | **Silent no-op** — nothing is inserted and no message is shown. |
+| Record… pick cancelled or empty | **Silent no-op** — nothing is inserted. |
+| Generate Dataset… cancelled at any step | **Silent no-op** — nothing is generated, no document opens. |
+| Generate Dataset… above 10,000 rows | Modal warning: *Generate ‹N› rows?* with a **Generate** button — dismissing generates nothing. |
+| Generate Dataset… invalid row count | Inline input-box validation: *"Enter a whole number of rows (1 or more)."* / *"Row count is capped at 100,000."* |
+
+### Quick Pick behaviors
+
+- **Insert Random: Pick…** — entries grouped under category separator headings (visible generators only), led by the user's **Templates** and **Custom Lists** groups when defined (their descriptions are the template text / list values, so content is searchable), placeholder *"Insert Random — pick a type to insert at every cursor…"*, `matchOnDescription` on (search by label **or** registry id). Selecting inserts; Escape cancels.
+- **Insert Random: Record…** — the same grouped listing in **multi-select** mode (`canPickMany`), led by the **Custom Lists** group when defined, placeholder *"Pick fields for the record…"*; ticked fields compose one record in picker display order (custom lists first, then catalog order). Escape, or confirming with nothing ticked, cancels.
+- **Insert Random: Generate Dataset…** — the same multi-select field picker (placeholder *"Pick fields for the dataset…"*), then a three-option shape pick (JSON / SQL / CSV) with the configured `recordFormat` floated to the top marked `$(check) Current record format`. Escape at either pick cancels.
+- **Enum settings pickers** — current value marked `$(check) Current` and floated to the top; `matchOnDetail` on.
+
+---
+
+## Configuration Flow
+
+`extension.ts` holds a **single module-level `settings` snapshot**. On activation, `watchConfiguration()` reads it once via `Configuration.read()`, then subscribes to `workspace.onDidChangeConfiguration`; when an event affects any key in `CONFIG_KEYS` (the fourteen `ConfigKey` values), the whole snapshot is re-read and replaced wholesale. A change to `insertRandomText.locale` additionally fires `load(locale)` right away, so the active faker instance swaps without waiting for the next insert. Commands read this **cached** snapshot at invocation — they never re-read individual settings — so anything that bypasses `watchConfiguration` would see stale config.
+
+`insertRandomText.contextMenu.enabled` is intentionally **not** in `CONFIG_KEYS`: it never influences generation, only the menu's `when` clause, which VS Code re-evaluates natively when the value changes.
+
+---
+
+## Context Menu
+
+An optional editor right-click entry, **off by default**.
+
+- A submenu `insertRandomText.contextSubmenu` labeled **"Insert Random"** is contributed to `editor/context` in group `1_modification`, gated by `when: editorTextFocus && config.insertRandomText.contextMenu.enabled`.
+- Enable it via *Insert Random: Toggle Editor Context Menu* (or the `insertRandomText.contextMenu.enabled` setting).
+- Submenu contents (a curated shortlist, not the whole catalog):
+
+| Group | Items |
+|---|---|
+| `1_pick` | Insert Random: Pick… |
+| `2_common` | UUID · Full Name · Email · Number · Date |
+| `3_text` | Lorem |
+| `4_anonymize` | Randomize Selection |
+
+---
+
+## Activation & Engine
+
+- **Activation** — the extension contributes **no explicit `activationEvents`**; since VS Code 1.74 they are auto-generated from `contributes.commands`, so invoking any of the 176 commands activates the extension from a cold start. `extensionKind` is `workspace`.
+- **Web extension** — the manifest declares both `main` (`dist/extension.js`, esbuild platform `node`) and `browser` (`dist/web/extension.js`, esbuild platform `browser`) bundles built from the same source in one `esbuild.js` run. The browser bundle contains no Node built-ins — esbuild's browser platform rejects them at build time, which doubles as the web-cleanliness gate — so the extension runs in the **web extension host** on vscode.dev and github.dev.
+- **Trust & virtual workspaces** — `capabilities.untrustedWorkspaces.supported = true` and `virtualWorkspaces = true`: the extension runs in restricted/untrusted and virtual (no-filesystem) workspaces, because it neither reads project files nor makes network calls.
+- **faker lifecycle** — `engine.ts` loads faker **lazily** on the first command via `load(locale)`: one literal dynamic `import('@faker-js/faker/locale/')` per shipped locale (`en` / `de` / `fr` / `es` / `pt_BR` / `ja`), cached in a promise map so each locale is imported once and concurrent loads share the import; `faker()` returns the **active** instance (the last locale loaded). Only those six locale entries are imported — never the package root — so faker's other 60+ locales never reach the esbuild bundle (which would blow the `.vsix` size gate). `seed(value)` forwards to the active instance's `seed`.
+- **Privacy** — every value is generated in-process. No network requests, no telemetry, fully offline.
diff --git a/esbuild.js b/esbuild.js
new file mode 100644
index 0000000..345ac2d
--- /dev/null
+++ b/esbuild.js
@@ -0,0 +1,64 @@
+const esbuild = require("esbuild");
+
+const production = process.argv.includes('--production');
+const watch = process.argv.includes('--watch');
+
+/**
+ * @type {import('esbuild').Plugin}
+ */
+const esbuildProblemMatcherPlugin = {
+ name: 'esbuild-problem-matcher',
+
+ setup(build) {
+ build.onStart(() => {
+ console.log('[watch] build started');
+ });
+ build.onEnd((result) => {
+ result.errors.forEach(({ text, location }) => {
+ console.error(`✘ [ERROR] ${text}`);
+ console.error(` ${location.file}:${location.line}:${location.column}:`);
+ });
+ console.log('[watch] build finished');
+ });
+ },
+};
+
+/** Shared bundle options; each target only picks its platform + outfile.
+ * The web bundle (package.json "browser") runs in the web extension host's
+ * worker on vscode.dev/github.dev — same CJS format, but platform 'browser'
+ * makes esbuild REJECT any Node built-in at build time, which is the
+ * web-cleanliness gate. */
+const common = {
+ entryPoints: [
+ 'src/extension.ts'
+ ],
+ bundle: true,
+ format: 'cjs',
+ minify: production,
+ sourcemap: !production,
+ sourcesContent: false,
+ external: ['vscode'],
+ logLevel: 'silent',
+ plugins: [
+ /* add to the end of plugins array */
+ esbuildProblemMatcherPlugin,
+ ],
+};
+
+async function main() {
+ const contexts = await Promise.all([
+ esbuild.context({ ...common, platform: 'node', outfile: 'dist/extension.js' }),
+ esbuild.context({ ...common, platform: 'browser', outfile: 'dist/web/extension.js' }),
+ ]);
+ if (watch) {
+ await Promise.all(contexts.map((ctx) => ctx.watch()));
+ } else {
+ await Promise.all(contexts.map((ctx) => ctx.rebuild()));
+ await Promise.all(contexts.map((ctx) => ctx.dispose()));
+ }
+}
+
+main().catch(e => {
+ console.error(e);
+ process.exit(1);
+});
diff --git a/eslint.config.mjs b/eslint.config.mjs
new file mode 100644
index 0000000..7dca3c6
--- /dev/null
+++ b/eslint.config.mjs
@@ -0,0 +1,27 @@
+import typescriptEslint from "typescript-eslint";
+
+export default [{
+ files: ["**/*.ts"],
+
+ plugins: {
+ "@typescript-eslint": typescriptEslint.plugin,
+ },
+
+ languageOptions: {
+ parser: typescriptEslint.parser,
+ ecmaVersion: 2022,
+ sourceType: "module",
+ },
+
+ rules: {
+ "@typescript-eslint/naming-convention": ["warn", {
+ selector: "import",
+ format: ["camelCase", "PascalCase"],
+ }],
+
+ curly: "warn",
+ eqeqeq: "warn",
+ "no-throw-literal": "warn",
+ semi: "warn",
+ },
+}];
diff --git a/images/BITCOIN.png b/images/BITCOIN.png
deleted file mode 100644
index 57658b5..0000000
Binary files a/images/BITCOIN.png and /dev/null differ
diff --git a/images/cerebral.gif b/images/cerebral.gif
new file mode 100644
index 0000000..5ecc4d4
Binary files /dev/null and b/images/cerebral.gif differ
diff --git a/images/github.png b/images/github.png
deleted file mode 100644
index beca624..0000000
Binary files a/images/github.png and /dev/null differ
diff --git a/images/icon.png b/images/icon.png
new file mode 100644
index 0000000..34da57f
Binary files /dev/null and b/images/icon.png differ
diff --git a/images/multi-cursor.gif b/images/multi-cursor.gif
new file mode 100644
index 0000000..345ec6a
Binary files /dev/null and b/images/multi-cursor.gif differ
diff --git a/images/playback.gif b/images/playback.gif
deleted file mode 100644
index 169ba5d..0000000
Binary files a/images/playback.gif and /dev/null differ
diff --git a/images/records.gif b/images/records.gif
new file mode 100644
index 0000000..6add07d
Binary files /dev/null and b/images/records.gif differ
diff --git a/images/settings.gif b/images/settings.gif
deleted file mode 100644
index 8572db8..0000000
Binary files a/images/settings.gif and /dev/null differ
diff --git a/package-lock.json b/package-lock.json
index 9e367b2..cf6ba74 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,1990 +1,5879 @@
{
- "name": "insert-random-text",
- "version": "0.1.3",
- "lockfileVersion": 1,
- "requires": true,
- "dependencies": {
- "@babel/code-frame": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
- "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
- "dev": true,
- "requires": {
- "@babel/highlight": "^7.8.3"
- }
- },
- "@babel/highlight": {
- "version": "7.8.3",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
- "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
- "dev": true,
- "requires": {
- "chalk": "^2.0.0",
- "esutils": "^2.0.2",
- "js-tokens": "^4.0.0"
- }
- },
- "@types/camelcase": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@types/camelcase/-/camelcase-5.2.0.tgz",
- "integrity": "sha512-zhHaryYYUUsJ1h6Rq4hisPkljY7c2bkC5PFYQbom5fyKloGJEDK+wdsw2L4hnBwXr4plGjW6D/UVJBbNbOzVpQ==",
- "dev": true,
- "requires": {
- "camelcase": "*"
- }
- },
- "@types/chance": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/@types/chance/-/chance-1.0.9.tgz",
- "integrity": "sha512-QEWzf4BICJwa9rr43hSRCAWDhxyakGl2iIXj5PsfGPuOBaJhv50tGX/P5flU8hStatLudPEhlJrZC2qwScD5nQ==",
- "dev": true
- },
- "@types/color-name": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
- "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==",
- "dev": true
- },
- "@types/eslint-visitor-keys": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
- "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
- "dev": true
- },
- "@types/events": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
- "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
- "dev": true
- },
- "@types/glob": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.1.tgz",
- "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==",
- "dev": true,
- "requires": {
- "@types/events": "*",
- "@types/minimatch": "*",
- "@types/node": "*"
- }
- },
- "@types/json-schema": {
- "version": "7.0.4",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.4.tgz",
- "integrity": "sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==",
- "dev": true
- },
- "@types/minimatch": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
- "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==",
- "dev": true
- },
- "@types/mocha": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-7.0.2.tgz",
- "integrity": "sha512-ZvO2tAcjmMi8V/5Z3JsyofMe3hasRcaw88cto5etSVMwVQfeivGAlEYmaQgceUSVYFofVjT+ioHsATjdWcFt1w==",
- "dev": true
- },
- "@types/node": {
- "version": "12.12.30",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.30.tgz",
- "integrity": "sha512-sz9MF/zk6qVr3pAnM0BSQvYIBK44tS75QC5N+VbWSE4DjCV/pJ+UzCW/F+vVnl7TkOPcuwQureKNtSSwjBTaMg==",
- "dev": true
- },
- "@types/vscode": {
- "version": "1.43.0",
- "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.43.0.tgz",
- "integrity": "sha512-kIaR9qzd80rJOxePKpCB/mdy00mz8Apt2QA5Y6rdrKFn13QNFNeP3Hzmsf37Bwh/3cS7QjtAeGSK7wSqAU0sYQ==",
- "dev": true
- },
- "@typescript-eslint/eslint-plugin": {
- "version": "2.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.23.0.tgz",
- "integrity": "sha512-8iA4FvRsz8qTjR0L/nK9RcRUN3QtIHQiOm69FzV7WS3SE+7P7DyGGwh3k4UNR2JBbk+Ej2Io+jLAaqKibNhmtw==",
- "dev": true,
- "requires": {
- "@typescript-eslint/experimental-utils": "2.23.0",
- "eslint-utils": "^1.4.3",
- "functional-red-black-tree": "^1.0.1",
- "regexpp": "^3.0.0",
- "tsutils": "^3.17.1"
- }
- },
- "@typescript-eslint/experimental-utils": {
- "version": "2.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.23.0.tgz",
- "integrity": "sha512-OswxY59RcXH3NNPmq+4Kis2CYZPurRU6mG5xPcn24CjFyfdVli5mySwZz/g/xDbJXgDsYqNGq7enV0IziWGXVQ==",
- "dev": true,
- "requires": {
- "@types/json-schema": "^7.0.3",
- "@typescript-eslint/typescript-estree": "2.23.0",
- "eslint-scope": "^5.0.0"
- }
- },
- "@typescript-eslint/parser": {
- "version": "2.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.23.0.tgz",
- "integrity": "sha512-k61pn/Nepk43qa1oLMiyqApC6x5eP5ddPz6VUYXCAuXxbmRLqkPYzkFRKl42ltxzB2luvejlVncrEpflgQoSUg==",
- "dev": true,
- "requires": {
- "@types/eslint-visitor-keys": "^1.0.0",
- "@typescript-eslint/experimental-utils": "2.23.0",
- "@typescript-eslint/typescript-estree": "2.23.0",
- "eslint-visitor-keys": "^1.1.0"
- }
- },
- "@typescript-eslint/typescript-estree": {
- "version": "2.23.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.23.0.tgz",
- "integrity": "sha512-pmf7IlmvXdlEXvE/JWNNJpEvwBV59wtJqA8MLAxMKLXNKVRC3HZBXR/SlZLPWTCcwOSg9IM7GeRSV3SIerGVqw==",
- "dev": true,
- "requires": {
- "debug": "^4.1.1",
- "eslint-visitor-keys": "^1.1.0",
- "glob": "^7.1.6",
- "is-glob": "^4.0.1",
- "lodash": "^4.17.15",
- "semver": "^6.3.0",
- "tsutils": "^3.17.1"
- }
- },
- "acorn": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz",
- "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==",
- "dev": true
- },
- "acorn-jsx": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz",
- "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==",
- "dev": true
- },
- "agent-base": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
- "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
- "dev": true,
- "requires": {
- "es6-promisify": "^5.0.0"
- }
- },
- "ajv": {
- "version": "6.12.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz",
- "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==",
- "dev": true,
- "requires": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- }
- },
- "ansi-colors": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz",
- "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==",
- "dev": true
- },
- "ansi-escapes": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz",
- "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==",
- "dev": true,
- "requires": {
- "type-fest": "^0.11.0"
- },
- "dependencies": {
- "type-fest": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz",
- "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==",
- "dev": true
- }
- }
- },
- "ansi-regex": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
- "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
- "dev": true
- },
- "ansi-styles": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
- "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
- "dev": true,
- "requires": {
- "color-convert": "^1.9.0"
- }
- },
- "anymatch": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz",
- "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==",
- "dev": true,
- "requires": {
- "normalize-path": "^3.0.0",
- "picomatch": "^2.0.4"
- }
- },
- "argparse": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
- "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
- "dev": true,
- "requires": {
- "sprintf-js": "~1.0.2"
- }
- },
- "astral-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
- "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
- "dev": true
- },
- "balanced-match": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
- "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
- "dev": true
- },
- "binary-extensions": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
- "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==",
- "dev": true
- },
- "brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
- "dev": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "braces": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
- "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
- "dev": true,
- "requires": {
- "fill-range": "^7.0.1"
- }
- },
- "browser-stdout": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
- "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
- "dev": true
- },
- "callsites": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
- "dev": true
- },
- "camelcase": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
- "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
- },
- "chalk": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
- "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
- "dev": true,
- "requires": {
- "ansi-styles": "^3.2.1",
- "escape-string-regexp": "^1.0.5",
- "supports-color": "^5.3.0"
- }
- },
- "chance": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/chance/-/chance-1.1.4.tgz",
- "integrity": "sha512-pXPDSu3knKlb6H7ahQfpq//J9mSOxYK8SMtp8MV/nRJh8aLRDIl0ipLH8At8+nVogVwtvPZzyIzY/EbcY/cLuQ=="
- },
- "chardet": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
- "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
- "dev": true
- },
- "chokidar": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.3.0.tgz",
- "integrity": "sha512-dGmKLDdT3Gdl7fBUe8XK+gAtGmzy5Fn0XkkWQuYxGIgWVPPse2CxFA5mtrlD0TOHaHjEUqkWNyP1XdHoJES/4A==",
- "dev": true,
- "requires": {
- "anymatch": "~3.1.1",
- "braces": "~3.0.2",
- "fsevents": "~2.1.1",
- "glob-parent": "~5.1.0",
- "is-binary-path": "~2.1.0",
- "is-glob": "~4.0.1",
- "normalize-path": "~3.0.0",
- "readdirp": "~3.2.0"
- }
- },
- "cli-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
- "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
- "dev": true,
- "requires": {
- "restore-cursor": "^3.1.0"
- }
- },
- "cli-width": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
- "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
- "dev": true
- },
- "cliui": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
- "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
- "dev": true,
- "requires": {
- "string-width": "^3.1.0",
- "strip-ansi": "^5.2.0",
- "wrap-ansi": "^5.1.0"
- },
- "dependencies": {
- "emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
- "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
- "dev": true,
- "requires": {
- "emoji-regex": "^7.0.1",
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^5.1.0"
- }
- }
- }
- },
- "color-convert": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
- "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
- "dev": true,
- "requires": {
- "color-name": "1.1.3"
- }
- },
- "color-name": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
- "dev": true
- },
- "concat-map": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
- "dev": true
- },
- "cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
- "dev": true,
- "requires": {
- "nice-try": "^1.0.4",
- "path-key": "^2.0.1",
- "semver": "^5.5.0",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
- },
- "dependencies": {
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
- }
- }
- },
- "debug": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
- "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "decamelize": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
- "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
- "dev": true
- },
- "deep-is": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
- "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
- "dev": true
- },
- "define-properties": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
- "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
- "dev": true,
- "requires": {
- "object-keys": "^1.0.12"
- }
- },
- "diff": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
- "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
- "dev": true
- },
- "doctrine": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
- "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
- "dev": true,
- "requires": {
- "esutils": "^2.0.2"
- }
- },
- "emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "dev": true
- },
- "es-abstract": {
- "version": "1.17.4",
- "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz",
- "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==",
- "dev": true,
- "requires": {
- "es-to-primitive": "^1.2.1",
- "function-bind": "^1.1.1",
- "has": "^1.0.3",
- "has-symbols": "^1.0.1",
- "is-callable": "^1.1.5",
- "is-regex": "^1.0.5",
- "object-inspect": "^1.7.0",
- "object-keys": "^1.1.1",
- "object.assign": "^4.1.0",
- "string.prototype.trimleft": "^2.1.1",
- "string.prototype.trimright": "^2.1.1"
- }
- },
- "es-to-primitive": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
- "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
- "dev": true,
- "requires": {
- "is-callable": "^1.1.4",
- "is-date-object": "^1.0.1",
- "is-symbol": "^1.0.2"
- }
- },
- "es6-promise": {
- "version": "4.2.8",
- "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
- "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
- "dev": true
- },
- "es6-promisify": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
- "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
- "dev": true,
- "requires": {
- "es6-promise": "^4.0.3"
- }
- },
- "escape-string-regexp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
- "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
- "dev": true
- },
- "eslint": {
- "version": "6.8.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz",
- "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==",
- "dev": true,
- "requires": {
- "@babel/code-frame": "^7.0.0",
- "ajv": "^6.10.0",
- "chalk": "^2.1.0",
- "cross-spawn": "^6.0.5",
- "debug": "^4.0.1",
- "doctrine": "^3.0.0",
- "eslint-scope": "^5.0.0",
- "eslint-utils": "^1.4.3",
- "eslint-visitor-keys": "^1.1.0",
- "espree": "^6.1.2",
- "esquery": "^1.0.1",
- "esutils": "^2.0.2",
- "file-entry-cache": "^5.0.1",
- "functional-red-black-tree": "^1.0.1",
- "glob-parent": "^5.0.0",
- "globals": "^12.1.0",
- "ignore": "^4.0.6",
- "import-fresh": "^3.0.0",
- "imurmurhash": "^0.1.4",
- "inquirer": "^7.0.0",
- "is-glob": "^4.0.0",
- "js-yaml": "^3.13.1",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.3.0",
- "lodash": "^4.17.14",
- "minimatch": "^3.0.4",
- "mkdirp": "^0.5.1",
- "natural-compare": "^1.4.0",
- "optionator": "^0.8.3",
- "progress": "^2.0.0",
- "regexpp": "^2.0.1",
- "semver": "^6.1.2",
- "strip-ansi": "^5.2.0",
- "strip-json-comments": "^3.0.1",
- "table": "^5.2.3",
- "text-table": "^0.2.0",
- "v8-compile-cache": "^2.0.3"
- },
- "dependencies": {
- "regexpp": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
- "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
- "dev": true
- }
- }
- },
- "eslint-scope": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
- "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
- "dev": true,
- "requires": {
- "esrecurse": "^4.1.0",
- "estraverse": "^4.1.1"
- }
- },
- "eslint-utils": {
- "version": "1.4.3",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
- "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
- "dev": true,
- "requires": {
- "eslint-visitor-keys": "^1.1.0"
- }
- },
- "eslint-visitor-keys": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
- "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
- "dev": true
- },
- "espree": {
- "version": "6.2.1",
- "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz",
- "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==",
- "dev": true,
- "requires": {
- "acorn": "^7.1.1",
- "acorn-jsx": "^5.2.0",
- "eslint-visitor-keys": "^1.1.0"
- }
- },
- "esprima": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
- "dev": true
- },
- "esquery": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.1.0.tgz",
- "integrity": "sha512-MxYW9xKmROWF672KqjO75sszsA8Mxhw06YFeS5VHlB98KDHbOSurm3ArsjO60Eaf3QmGMCP1yn+0JQkNLo/97Q==",
- "dev": true,
- "requires": {
- "estraverse": "^4.0.0"
- }
- },
- "esrecurse": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
- "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
- "dev": true,
- "requires": {
- "estraverse": "^4.1.0"
- }
- },
- "estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "dev": true
- },
- "esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true
- },
- "external-editor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
- "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
- "dev": true,
- "requires": {
- "chardet": "^0.7.0",
- "iconv-lite": "^0.4.24",
- "tmp": "^0.0.33"
- }
- },
- "fast-deep-equal": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
- "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==",
- "dev": true
- },
- "fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
- },
- "fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=",
- "dev": true
- },
- "figures": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
- "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==",
- "dev": true,
- "requires": {
- "escape-string-regexp": "^1.0.5"
- }
- },
- "file-entry-cache": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
- "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
- "dev": true,
- "requires": {
- "flat-cache": "^2.0.1"
- }
- },
- "fill-range": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
- "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
- "dev": true,
- "requires": {
- "to-regex-range": "^5.0.1"
- }
- },
- "find-up": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
- "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
- "dev": true,
- "requires": {
- "locate-path": "^3.0.0"
- }
- },
- "flat": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz",
- "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==",
- "dev": true,
- "requires": {
- "is-buffer": "~2.0.3"
- }
- },
- "flat-cache": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
- "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
- "dev": true,
- "requires": {
- "flatted": "^2.0.0",
- "rimraf": "2.6.3",
- "write": "1.0.3"
- }
- },
- "flatted": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz",
- "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
- "dev": true
- },
- "fs.realpath": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
- "dev": true
- },
- "fsevents": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.2.tgz",
- "integrity": "sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA==",
- "dev": true,
- "optional": true
- },
- "function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
- },
- "functional-red-black-tree": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
- "dev": true
- },
- "get-caller-file": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
- "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
- "dev": true
- },
- "glob": {
- "version": "7.1.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
- "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "glob-parent": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz",
- "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==",
- "dev": true,
- "requires": {
- "is-glob": "^4.0.1"
- }
- },
- "globals": {
- "version": "12.4.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz",
- "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==",
- "dev": true,
- "requires": {
- "type-fest": "^0.8.1"
- }
- },
- "growl": {
- "version": "1.10.5",
- "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
- "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
- "dev": true
- },
- "has": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
- "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
- "dev": true,
- "requires": {
- "function-bind": "^1.1.1"
- }
- },
- "has-flag": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
- "dev": true
- },
- "has-symbols": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
- "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
- "dev": true
- },
- "he": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
- "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
- "dev": true
- },
- "http-proxy-agent": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz",
- "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==",
- "dev": true,
- "requires": {
- "agent-base": "4",
- "debug": "3.1.0"
- },
- "dependencies": {
- "debug": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
- "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
- "dev": true
- }
- }
- },
- "https-proxy-agent": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz",
- "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==",
- "dev": true,
- "requires": {
- "agent-base": "^4.3.0",
- "debug": "^3.1.0"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- }
- }
- },
- "iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "dev": true,
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
- },
- "ignore": {
- "version": "4.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
- "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
- "dev": true
- },
- "import-fresh": {
- "version": "3.2.1",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz",
- "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==",
- "dev": true,
- "requires": {
- "parent-module": "^1.0.0",
- "resolve-from": "^4.0.0"
- }
- },
- "imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=",
- "dev": true
- },
- "inflight": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
- "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
- "dev": true,
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
- "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
- "dev": true
- },
- "inquirer": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.1.0.tgz",
- "integrity": "sha512-5fJMWEmikSYu0nv/flMc475MhGbB7TSPd/2IpFV4I4rMklboCH2rQjYY5kKiYGHqUF9gvaambupcJFFG9dvReg==",
- "dev": true,
- "requires": {
- "ansi-escapes": "^4.2.1",
- "chalk": "^3.0.0",
- "cli-cursor": "^3.1.0",
- "cli-width": "^2.0.0",
- "external-editor": "^3.0.3",
- "figures": "^3.0.0",
- "lodash": "^4.17.15",
- "mute-stream": "0.0.8",
- "run-async": "^2.4.0",
- "rxjs": "^6.5.3",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0",
- "through": "^2.3.6"
- },
- "dependencies": {
- "ansi-styles": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
- "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
- "dev": true,
- "requires": {
- "@types/color-name": "^1.1.1",
- "color-convert": "^2.0.1"
- }
- },
- "chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
- "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
- "dev": true,
- "requires": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- }
- },
- "color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
- "dev": true,
- "requires": {
- "color-name": "~1.1.4"
- }
- },
- "color-name": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
- "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
- "dev": true
- },
- "has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true
- },
- "strip-ansi": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
- "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
- "dev": true,
- "requires": {
- "ansi-regex": "^5.0.0"
- }
- },
- "supports-color": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
- "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
- "dev": true,
- "requires": {
- "has-flag": "^4.0.0"
- }
- }
- }
- },
- "is-binary-path": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
- "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
- "dev": true,
- "requires": {
- "binary-extensions": "^2.0.0"
- }
- },
- "is-buffer": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
- "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
- "dev": true
- },
- "is-callable": {
- "version": "1.1.5",
- "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz",
- "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==",
- "dev": true
- },
- "is-date-object": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz",
- "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==",
- "dev": true
- },
- "is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
- "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
- "dev": true
- },
- "is-glob": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
- "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
- "dev": true,
- "requires": {
- "is-extglob": "^2.1.1"
- }
- },
- "is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true
- },
- "is-promise": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
- "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
- "dev": true
- },
- "is-regex": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz",
- "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==",
- "dev": true,
- "requires": {
- "has": "^1.0.3"
- }
- },
- "is-symbol": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz",
- "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==",
- "dev": true,
- "requires": {
- "has-symbols": "^1.0.1"
- }
- },
- "isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
- "dev": true
- },
- "js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true
- },
- "js-yaml": {
- "version": "3.13.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
- "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
- "dev": true,
- "requires": {
- "argparse": "^1.0.7",
- "esprima": "^4.0.0"
- }
- },
- "json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
- },
- "json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
- "dev": true
- },
- "levn": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
- "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
- "dev": true,
- "requires": {
- "prelude-ls": "~1.1.2",
- "type-check": "~0.3.2"
- }
- },
- "locate-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
- "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
- "dev": true,
- "requires": {
- "p-locate": "^3.0.0",
- "path-exists": "^3.0.0"
- }
- },
- "lodash": {
- "version": "4.17.15",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
- "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
- "dev": true
- },
- "log-symbols": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",
- "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==",
- "dev": true,
- "requires": {
- "chalk": "^2.4.2"
- }
- },
- "mimic-fn": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
- "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
- "dev": true
- },
- "minimatch": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
- "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
- "dev": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "minimist": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
- "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
- "dev": true
- },
- "mkdirp": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
- "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
- "dev": true,
- "requires": {
- "minimist": "0.0.8"
- }
- },
- "mocha": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/mocha/-/mocha-7.1.0.tgz",
- "integrity": "sha512-MymHK8UkU0K15Q/zX7uflZgVoRWiTjy0fXE/QjKts6mowUvGxOdPhZ2qj3b0iZdUrNZlW9LAIMFHB4IW+2b3EQ==",
- "dev": true,
- "requires": {
- "ansi-colors": "3.2.3",
- "browser-stdout": "1.3.1",
- "chokidar": "3.3.0",
- "debug": "3.2.6",
- "diff": "3.5.0",
- "escape-string-regexp": "1.0.5",
- "find-up": "3.0.0",
- "glob": "7.1.3",
- "growl": "1.10.5",
- "he": "1.2.0",
- "js-yaml": "3.13.1",
- "log-symbols": "3.0.0",
- "minimatch": "3.0.4",
- "mkdirp": "0.5.1",
- "ms": "2.1.1",
- "node-environment-flags": "1.0.6",
- "object.assign": "4.1.0",
- "strip-json-comments": "2.0.1",
- "supports-color": "6.0.0",
- "which": "1.3.1",
- "wide-align": "1.1.3",
- "yargs": "13.3.0",
- "yargs-parser": "13.1.1",
- "yargs-unparser": "1.6.0"
- },
- "dependencies": {
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "dev": true,
- "requires": {
- "ms": "^2.1.1"
- }
- },
- "glob": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
- "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
- "dev": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
- },
- "strip-json-comments": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
- "dev": true
- },
- "supports-color": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz",
- "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==",
- "dev": true,
- "requires": {
- "has-flag": "^3.0.0"
- }
- }
- }
- },
- "ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
- },
- "mute-stream": {
- "version": "0.0.8",
- "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
- "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==",
- "dev": true
- },
- "natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
- "dev": true
- },
- "nice-try": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
- "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
- "dev": true
- },
- "node-environment-flags": {
- "version": "1.0.6",
- "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
- "integrity": "sha512-5Evy2epuL+6TM0lCQGpFIj6KwiEsGh1SrHUhTbNX+sLbBtjidPZFAnVK9y5yU1+h//RitLbRHTIMyxQPtxMdHw==",
- "dev": true,
- "requires": {
- "object.getownpropertydescriptors": "^2.0.3",
- "semver": "^5.7.0"
- },
- "dependencies": {
- "semver": {
- "version": "5.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
- "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
- "dev": true
- }
- }
- },
- "normalize-path": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
- "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
- "dev": true
- },
- "object-inspect": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz",
- "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==",
- "dev": true
- },
- "object-keys": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
- "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
- "dev": true
- },
- "object.assign": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz",
- "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.2",
- "function-bind": "^1.1.1",
- "has-symbols": "^1.0.0",
- "object-keys": "^1.0.11"
- }
- },
- "object.getownpropertydescriptors": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz",
- "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "es-abstract": "^1.17.0-next.1"
- }
- },
- "once": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
- "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
- "dev": true,
- "requires": {
- "wrappy": "1"
- }
- },
- "onetime": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz",
- "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==",
- "dev": true,
- "requires": {
- "mimic-fn": "^2.1.0"
- }
- },
- "optionator": {
- "version": "0.8.3",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
- "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
- "dev": true,
- "requires": {
- "deep-is": "~0.1.3",
- "fast-levenshtein": "~2.0.6",
- "levn": "~0.3.0",
- "prelude-ls": "~1.1.2",
- "type-check": "~0.3.2",
- "word-wrap": "~1.2.3"
- }
- },
- "os-tmpdir": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
- "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
- "dev": true
- },
- "p-limit": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.2.tgz",
- "integrity": "sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ==",
- "dev": true,
- "requires": {
- "p-try": "^2.0.0"
- }
- },
- "p-locate": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
- "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
- "dev": true,
- "requires": {
- "p-limit": "^2.0.0"
- }
- },
- "p-try": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
- "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
- "dev": true
- },
- "parent-module": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
- "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
- "dev": true,
- "requires": {
- "callsites": "^3.0.0"
- }
- },
- "path-exists": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
- "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
- "dev": true
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
- "dev": true
- },
- "path-key": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
- "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=",
- "dev": true
- },
- "picomatch": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.1.tgz",
- "integrity": "sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA==",
- "dev": true
- },
- "prelude-ls": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
- "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=",
- "dev": true
- },
- "progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
- "dev": true
- },
- "punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
- "dev": true
- },
- "readdirp": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.2.0.tgz",
- "integrity": "sha512-crk4Qu3pmXwgxdSgGhgA/eXiJAPQiX4GMOZZMXnqKxHX7TaoL+3gQVo/WeuAiogr07DpnfjIMpXXa+PAIvwPGQ==",
- "dev": true,
- "requires": {
- "picomatch": "^2.0.4"
- }
- },
- "regexpp": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
- "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
- "dev": true
- },
- "require-directory": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
- "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
- "dev": true
- },
- "require-main-filename": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
- "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
- "dev": true
- },
- "resolve-from": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
- "dev": true
- },
- "restore-cursor": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
- "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
- "dev": true,
- "requires": {
- "onetime": "^5.1.0",
- "signal-exit": "^3.0.2"
- }
- },
- "rimraf": {
- "version": "2.6.3",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
- "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
- "dev": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "run-async": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.0.tgz",
- "integrity": "sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg==",
- "dev": true,
- "requires": {
- "is-promise": "^2.1.0"
- }
- },
- "rxjs": {
- "version": "6.5.4",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.4.tgz",
- "integrity": "sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q==",
- "dev": true,
- "requires": {
- "tslib": "^1.9.0"
- }
- },
- "safer-buffer": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
- "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
- "dev": true
- },
- "semver": {
- "version": "6.3.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
- "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
- "dev": true
- },
- "set-blocking": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
- "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
- "dev": true
- },
- "shebang-command": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
- "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
- "dev": true,
- "requires": {
- "shebang-regex": "^1.0.0"
- }
- },
- "shebang-regex": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
- "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=",
- "dev": true
- },
- "signal-exit": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
- "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
- "dev": true
- },
- "slice-ansi": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
- "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
- "dev": true,
- "requires": {
- "ansi-styles": "^3.2.0",
- "astral-regex": "^1.0.0",
- "is-fullwidth-code-point": "^2.0.0"
- },
- "dependencies": {
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- }
- }
- },
- "sprintf-js": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
- "dev": true
- },
- "string-width": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
- "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
- "dev": true,
- "requires": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.0"
- },
- "dependencies": {
- "strip-ansi": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
- "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
- "dev": true,
- "requires": {
- "ansi-regex": "^5.0.0"
- }
- }
- }
- },
- "string.prototype.trimleft": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz",
- "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "function-bind": "^1.1.1"
- }
- },
- "string.prototype.trimright": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz",
- "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==",
- "dev": true,
- "requires": {
- "define-properties": "^1.1.3",
- "function-bind": "^1.1.1"
- }
- },
- "strip-ansi": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
- "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
- "dev": true,
- "requires": {
- "ansi-regex": "^4.1.0"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
- "dev": true
- }
- }
- },
- "strip-json-comments": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz",
- "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==",
- "dev": true
- },
- "supports-color": {
- "version": "5.5.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
- "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
- "dev": true,
- "requires": {
- "has-flag": "^3.0.0"
- }
- },
- "table": {
- "version": "5.4.6",
- "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz",
- "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==",
- "dev": true,
- "requires": {
- "ajv": "^6.10.2",
- "lodash": "^4.17.14",
- "slice-ansi": "^2.1.0",
- "string-width": "^3.0.0"
- },
- "dependencies": {
- "emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
- "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
- "dev": true,
- "requires": {
- "emoji-regex": "^7.0.1",
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^5.1.0"
- }
- }
- }
- },
- "text-table": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
- "dev": true
- },
- "through": {
- "version": "2.3.8",
- "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
- "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
- "dev": true
- },
- "tmp": {
- "version": "0.0.33",
- "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
- "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
- "dev": true,
- "requires": {
- "os-tmpdir": "~1.0.2"
- }
- },
- "to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "requires": {
- "is-number": "^7.0.0"
- }
- },
- "tslib": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz",
- "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==",
- "dev": true
- },
- "tsutils": {
- "version": "3.17.1",
- "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
- "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
- "dev": true,
- "requires": {
- "tslib": "^1.8.1"
- }
- },
- "type-check": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
- "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
- "dev": true,
- "requires": {
- "prelude-ls": "~1.1.2"
- }
- },
- "type-fest": {
- "version": "0.8.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
- "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
- "dev": true
- },
- "typescript": {
- "version": "3.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
- "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
- "dev": true
- },
- "uri-js": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
- "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
- "dev": true,
- "requires": {
- "punycode": "^2.1.0"
- }
- },
- "v8-compile-cache": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz",
- "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==",
- "dev": true
- },
- "vscode-test": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.3.0.tgz",
- "integrity": "sha512-LddukcBiSU2FVTDr3c1D8lwkiOvwlJdDL2hqVbn6gIz+rpTqUCkMZSKYm94Y1v0WXlHSDQBsXyY+tchWQgGVsw==",
- "dev": true,
- "requires": {
- "http-proxy-agent": "^2.1.0",
- "https-proxy-agent": "^2.2.4",
- "rimraf": "^2.6.3"
- }
- },
- "which": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
- "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
- "dev": true,
- "requires": {
- "isexe": "^2.0.0"
- }
- },
- "which-module": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
- "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
- "dev": true
- },
- "wide-align": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
- "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
- "dev": true,
- "requires": {
- "string-width": "^1.0.2 || 2"
- },
- "dependencies": {
- "ansi-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
- "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
- "dev": true,
- "requires": {
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^4.0.0"
- }
- },
- "strip-ansi": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
- "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
- "dev": true,
- "requires": {
- "ansi-regex": "^3.0.0"
- }
- }
- }
- },
- "word-wrap": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
- "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
- "dev": true
- },
- "wrap-ansi": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
- "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
- "dev": true,
- "requires": {
- "ansi-styles": "^3.2.0",
- "string-width": "^3.0.0",
- "strip-ansi": "^5.0.0"
- },
- "dependencies": {
- "emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
- "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
- "dev": true,
- "requires": {
- "emoji-regex": "^7.0.1",
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^5.1.0"
- }
- }
- }
- },
- "wrappy": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
- "dev": true
- },
- "write": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
- "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
- "dev": true,
- "requires": {
- "mkdirp": "^0.5.1"
- }
- },
- "y18n": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
- "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
- "dev": true
- },
- "yargs": {
- "version": "13.3.0",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
- "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==",
- "dev": true,
- "requires": {
- "cliui": "^5.0.0",
- "find-up": "^3.0.0",
- "get-caller-file": "^2.0.1",
- "require-directory": "^2.1.1",
- "require-main-filename": "^2.0.0",
- "set-blocking": "^2.0.0",
- "string-width": "^3.0.0",
- "which-module": "^2.0.0",
- "y18n": "^4.0.0",
- "yargs-parser": "^13.1.1"
- },
- "dependencies": {
- "emoji-regex": {
- "version": "7.0.3",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
- "dev": true
- },
- "is-fullwidth-code-point": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
- "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
- "dev": true
- },
- "string-width": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
- "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
- "dev": true,
- "requires": {
- "emoji-regex": "^7.0.1",
- "is-fullwidth-code-point": "^2.0.0",
- "strip-ansi": "^5.1.0"
- }
- }
- }
- },
- "yargs-parser": {
- "version": "13.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
- "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
- "dev": true,
- "requires": {
- "camelcase": "^5.0.0",
- "decamelize": "^1.2.0"
- }
- },
- "yargs-unparser": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.6.0.tgz",
- "integrity": "sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw==",
- "dev": true,
- "requires": {
- "flat": "^4.1.0",
- "lodash": "^4.17.15",
- "yargs": "^13.3.0"
- }
- }
- }
+ "name": "insert-random-text",
+ "version": "0.1.4",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "insert-random-text",
+ "version": "0.1.4",
+ "license": "MIT",
+ "dependencies": {
+ "@faker-js/faker": "^10.5.0"
+ },
+ "devDependencies": {
+ "@types/mocha": "^10.0.10",
+ "@types/node": "^22.19.18",
+ "@types/vscode": "1.97.0",
+ "@vscode/test-cli": "^0.0.12",
+ "@vscode/test-electron": "^2.5.2",
+ "esbuild": "^0.28.0",
+ "eslint": "^9.39.3",
+ "npm-run-all": "^4.1.5",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.59.2"
+ },
+ "engines": {
+ "vscode": "^1.97.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
+ "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
+ "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
+ "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
+ "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
+ "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
+ "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
+ "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
+ "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
+ "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
+ "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
+ "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
+ "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
+ "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
+ "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
+ "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
+ "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
+ "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
+ "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
+ "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
+ "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
+ "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
+ "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@faker-js/faker": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.5.0.tgz",
+ "integrity": "sha512-bsxD8WLS5lIj7aaoCx1YJkktqYj5vlBUE6HWzu2Q51ksrGJ0H737ECCKlFU7Yf8Br45z9t99frBp/J7kzbMPAg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fakerjs"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
+ "npm": ">=10"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz",
+ "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
+ "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/mocha": {
+ "version": "10.0.10",
+ "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz",
+ "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "22.20.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz",
+ "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@types/vscode": {
+ "version": "1.97.0",
+ "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.97.0.tgz",
+ "integrity": "sha512-ueE73loeOTe7olaVyqP9mrRI54kVPJifUPjblZo9fYcv1CuVLPOEKEkqW0GkqPC454+nCEoigLWnC2Pp7prZ9w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz",
+ "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/type-utils": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.61.1",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz",
+ "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz",
+ "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.61.1",
+ "@typescript-eslint/types": "^8.61.1",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz",
+ "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz",
+ "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz",
+ "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz",
+ "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz",
+ "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.61.1",
+ "@typescript-eslint/tsconfig-utils": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/visitor-keys": "8.61.1",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.5.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz",
+ "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.61.1",
+ "@typescript-eslint/types": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz",
+ "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.61.1",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vscode/test-cli": {
+ "version": "0.0.12",
+ "resolved": "https://registry.npmjs.org/@vscode/test-cli/-/test-cli-0.0.12.tgz",
+ "integrity": "sha512-iYN0fDg29+a2Xelle/Y56Xvv7Nc8Thzq4VwpzAF/SIE6918rDicqfsQxV6w1ttr2+SOm+10laGuY9FG2ptEKsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/mocha": "^10.0.10",
+ "c8": "^10.1.3",
+ "chokidar": "^3.6.0",
+ "enhanced-resolve": "^5.18.3",
+ "glob": "^10.3.10",
+ "minimatch": "^9.0.3",
+ "mocha": "^11.7.4",
+ "supports-color": "^10.2.2",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "vscode-test": "out/bin.mjs"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@vscode/test-electron": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz",
+ "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "jszip": "^3.10.1",
+ "ora": "^8.1.0",
+ "semver": "^7.6.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.17.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz",
+ "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
+ "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "is-array-buffer": "^3.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz",
+ "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/async-function": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
+ "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
+ "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/c8": {
+ "version": "10.1.3",
+ "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz",
+ "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.1",
+ "@istanbuljs/schema": "^0.1.3",
+ "find-up": "^5.0.0",
+ "foreground-child": "^3.1.1",
+ "istanbul-lib-coverage": "^3.2.0",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.1.6",
+ "test-exclude": "^7.0.1",
+ "v8-to-istanbul": "^9.0.0",
+ "yargs": "^17.7.2",
+ "yargs-parser": "^21.1.1"
+ },
+ "bin": {
+ "c8": "bin/c8.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "monocart-coverage-reports": "^2"
+ },
+ "peerDependenciesMeta": {
+ "monocart-coverage-reports": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz",
+ "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "get-intrinsic": "^1.3.0",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/cli-cursor": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
+ "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "restore-cursor": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-spinners": {
+ "version": "2.9.2",
+ "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
+ "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/data-view-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz",
+ "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/data-view-byte-length": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz",
+ "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/inspect-js"
+ }
+ },
+ "node_modules/data-view-byte-offset": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz",
+ "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-data-view": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/diff": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+ "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.24.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.0.tgz",
+ "integrity": "sha512-SkE2t82KlkkxQRVMVLAGKxLfORGQfrkx5dkj+vlgXRVNEdPc4eZcR+J/Fvj8C+yKSFH5L0q3NFlyufOVQnCcYQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.24.2",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz",
+ "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.2",
+ "arraybuffer.prototype.slice": "^1.0.4",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "data-view-buffer": "^1.0.2",
+ "data-view-byte-length": "^1.0.2",
+ "data-view-byte-offset": "^1.0.1",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-set-tostringtag": "^2.1.0",
+ "es-to-primitive": "^1.3.0",
+ "function.prototype.name": "^1.1.8",
+ "get-intrinsic": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "get-symbol-description": "^1.1.0",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "internal-slot": "^1.1.0",
+ "is-array-buffer": "^3.0.5",
+ "is-callable": "^1.2.7",
+ "is-data-view": "^1.0.2",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.2.1",
+ "is-set": "^2.0.3",
+ "is-shared-array-buffer": "^1.0.4",
+ "is-string": "^1.1.1",
+ "is-typed-array": "^1.1.15",
+ "is-weakref": "^1.1.1",
+ "math-intrinsics": "^1.1.0",
+ "object-inspect": "^1.13.4",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.7",
+ "own-keys": "^1.0.1",
+ "regexp.prototype.flags": "^1.5.4",
+ "safe-array-concat": "^1.1.3",
+ "safe-push-apply": "^1.0.0",
+ "safe-regex-test": "^1.1.0",
+ "set-proto": "^1.0.0",
+ "stop-iteration-iterator": "^1.1.0",
+ "string.prototype.trim": "^1.2.10",
+ "string.prototype.trimend": "^1.0.9",
+ "string.prototype.trimstart": "^1.0.8",
+ "typed-array-buffer": "^1.0.3",
+ "typed-array-byte-length": "^1.0.3",
+ "typed-array-byte-offset": "^1.0.4",
+ "typed-array-length": "^1.0.7",
+ "unbox-primitive": "^1.1.0",
+ "which-typed-array": "^1.1.19"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-abstract-get": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz",
+ "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.2",
+ "is-callable": "^1.2.7",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
+ "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.1.tgz",
+ "integrity": "sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-abstract-get": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "is-callable": "^1.2.7",
+ "is-date-object": "^1.1.0",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.28.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
+ "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.28.1",
+ "@esbuild/android-arm": "0.28.1",
+ "@esbuild/android-arm64": "0.28.1",
+ "@esbuild/android-x64": "0.28.1",
+ "@esbuild/darwin-arm64": "0.28.1",
+ "@esbuild/darwin-x64": "0.28.1",
+ "@esbuild/freebsd-arm64": "0.28.1",
+ "@esbuild/freebsd-x64": "0.28.1",
+ "@esbuild/linux-arm": "0.28.1",
+ "@esbuild/linux-arm64": "0.28.1",
+ "@esbuild/linux-ia32": "0.28.1",
+ "@esbuild/linux-loong64": "0.28.1",
+ "@esbuild/linux-mips64el": "0.28.1",
+ "@esbuild/linux-ppc64": "0.28.1",
+ "@esbuild/linux-riscv64": "0.28.1",
+ "@esbuild/linux-s390x": "0.28.1",
+ "@esbuild/linux-x64": "0.28.1",
+ "@esbuild/netbsd-arm64": "0.28.1",
+ "@esbuild/netbsd-x64": "0.28.1",
+ "@esbuild/openbsd-arm64": "0.28.1",
+ "@esbuild/openbsd-x64": "0.28.1",
+ "@esbuild/openharmony-arm64": "0.28.1",
+ "@esbuild/sunos-x64": "0.28.1",
+ "@esbuild/win32-arm64": "0.28.1",
+ "@esbuild/win32-ia32": "0.28.1",
+ "@esbuild/win32-x64": "0.28.1"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.2",
+ "@eslint/config-helpers": "^0.4.2",
+ "@eslint/core": "^0.17.0",
+ "@eslint/eslintrc": "^3.3.5",
+ "@eslint/js": "9.39.4",
+ "@eslint/plugin-kit": "^0.4.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.5",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/function.prototype.name": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz",
+ "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2",
+ "hasown": "^2.0.4",
+ "is-callable": "^1.2.7",
+ "is-document.all": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
+ "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
+ "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+ "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+ "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz",
+ "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/has-bigints": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
+ "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz",
+ "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
+ "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "2.8.9",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
+ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/internal-slot": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
+ "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
+ "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-async-function": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz",
+ "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-function": "^1.0.0",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz",
+ "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-bigints": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz",
+ "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.2",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
+ "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-data-view": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz",
+ "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "get-intrinsic": "^1.2.6",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz",
+ "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-document.all": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz",
+ "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz",
+ "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-interactive": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz",
+ "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz",
+ "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz",
+ "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz",
+ "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-symbol": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz",
+ "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
+ "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz",
+ "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz",
+ "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "get-intrinsic": "^1.2.6"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "dev": true,
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/load-json-file": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
+ "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^4.0.0",
+ "pify": "^3.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/log-symbols": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
+ "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^4.1.0",
+ "is-unicode-supported": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/memorystream": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
+ "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.9",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
+ "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "11.7.6",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz",
+ "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "browser-stdout": "^1.3.1",
+ "chokidar": "^4.0.1",
+ "debug": "^4.3.5",
+ "diff": "^7.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "find-up": "^5.0.0",
+ "glob": "^10.4.5",
+ "he": "^1.2.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "log-symbols": "^4.1.0",
+ "minimatch": "^9.0.5",
+ "ms": "^2.1.3",
+ "picocolors": "^1.1.1",
+ "serialize-javascript": "^6.0.2",
+ "strip-json-comments": "^3.1.1",
+ "supports-color": "^8.1.1",
+ "workerpool": "^9.2.0",
+ "yargs": "^17.7.2",
+ "yargs-parser": "^21.1.1",
+ "yargs-unparser": "^2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/mocha/node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/mocha/node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/mocha/node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/normalize-package-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "node_modules/normalize-package-data/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
+ "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "chalk": "^2.4.1",
+ "cross-spawn": "^6.0.5",
+ "memorystream": "^0.3.1",
+ "minimatch": "^3.0.4",
+ "pidtree": "^0.3.0",
+ "read-pkg": "^3.0.0",
+ "shell-quote": "^1.6.1",
+ "string.prototype.padend": "^3.0.0"
+ },
+ "bin": {
+ "npm-run-all": "bin/npm-run-all/index.js",
+ "run-p": "bin/run-p/index.js",
+ "run-s": "bin/run-s/index.js"
+ },
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/brace-expansion": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
+ "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/npm-run-all/node_modules/cross-spawn": {
+ "version": "6.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz",
+ "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ },
+ "engines": {
+ "node": ">=4.8"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/npm-run-all/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/onetime": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz",
+ "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mimic-function": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/ora": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz",
+ "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "cli-cursor": "^5.0.0",
+ "cli-spinners": "^2.9.2",
+ "is-interactive": "^2.0.0",
+ "is-unicode-supported": "^2.0.0",
+ "log-symbols": "^6.0.0",
+ "stdin-discarder": "^0.2.2",
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/ora/node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ora/node_modules/is-unicode-supported": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/log-symbols": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
+ "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "is-unicode-supported": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ora/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/own-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
+ "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-intrinsic": "^1.2.6",
+ "object-keys": "^1.1.1",
+ "safe-push-apply": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "dev": true,
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
+ "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "error-ex": "^1.3.1",
+ "json-parse-better-errors": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-type": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
+ "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pidtree": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz",
+ "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "pidtree": "bin/pidtree.js"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz",
+ "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "load-json-file": "^4.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/reflect.getprototypeof": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
+ "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.7",
+ "get-proto": "^1.0.1",
+ "which-builtin-type": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/regexp.prototype.flags": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
+ "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/restore-cursor": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz",
+ "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "onetime": "^7.0.0",
+ "signal-exit": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz",
+ "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "get-intrinsic": "^1.3.0",
+ "has-symbols": "^1.1.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-array-concat/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-push-apply": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz",
+ "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/safe-push-apply/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
+ "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-proto": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz",
+ "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
+ "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
+ "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4",
+ "side-channel-list": "^1.0.1",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
+ "dev": true,
+ "license": "CC-BY-3.0"
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.23",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz",
+ "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/stdin-discarder": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
+ "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stop-iteration-iterator": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz",
+ "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "internal-slot": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string.prototype.padend": {
+ "version": "3.1.6",
+ "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz",
+ "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.11",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz",
+ "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-data-property": "^1.1.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.24.2",
+ "es-object-atoms": "^1.1.2",
+ "has-property-descriptors": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz",
+ "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz",
+ "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "10.2.2",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
+ "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tapable": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz",
+ "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz",
+ "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^10.4.1",
+ "minimatch": "^10.2.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/test-exclude/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
+ "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.5"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz",
+ "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz",
+ "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "for-each": "^0.3.3",
+ "gopd": "^1.2.0",
+ "has-proto": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "reflect.getprototypeof": "^1.0.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz",
+ "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.9",
+ "for-each": "^0.3.5",
+ "gopd": "^1.2.0",
+ "is-typed-array": "^1.1.15",
+ "possible-typed-array-names": "^1.1.0",
+ "reflect.getprototypeof": "^1.0.10"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.61.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz",
+ "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.61.1",
+ "@typescript-eslint/parser": "8.61.1",
+ "@typescript-eslint/typescript-estree": "8.61.1",
+ "@typescript-eslint/utils": "8.61.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.1.0"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
+ "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "has-bigints": "^1.0.2",
+ "has-symbols": "^1.1.0",
+ "which-boxed-primitive": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
+ "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.12",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-boxed-primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
+ "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-bigint": "^1.1.0",
+ "is-boolean-object": "^1.2.1",
+ "is-number-object": "^1.1.1",
+ "is-string": "^1.1.1",
+ "is-symbol": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz",
+ "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "function.prototype.name": "^1.1.6",
+ "has-tostringtag": "^1.0.2",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.1.0",
+ "is-finalizationregistry": "^1.1.0",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.2.1",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.1.0",
+ "which-collection": "^1.0.2",
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-builtin-type/node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.22",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz",
+ "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.9",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "9.3.4",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz",
+ "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
}
diff --git a/package.json b/package.json
index f552e62..93d26b1 100644
--- a/package.json
+++ b/package.json
@@ -1,208 +1,1035 @@
{
- "name": "insert-random-text",
- "publisher": "ElecTreeFrying",
- "displayName": "Insert Random Text",
- "description": "Insert random text on the fly.",
- "version": "0.1.3",
- "icon": "images/github.png",
- "categories": [
- "Other"
- ],
- "keywords": [
- "lorem",
- "insert",
- "random",
- "insert-text",
- "random-text"
- ],
- "galleryBanner": {
- "color": "#494949",
- "theme": "dark"
- },
- "homepage": "https://github.com/ElecTreeFrying/insert-random-text/blob/master/README.md",
- "repository": {
- "type": "git",
- "url": "https://github.com/ElecTreeFrying/insert-random-text.git"
- },
- "bugs": {
- "email": "electreefrying.git@gmail.com",
- "url": "https://github.com/ElecTreeFrying/insert-random-text/issues"
- },
- "license": "MIT",
- "engines": {
- "vscode": "^1.43.0"
- },
- "activationEvents": [
- "onCommand:extension.insertRandomAnimal",
- "onCommand:extension.insertRandomPerson",
- "onCommand:extension.insertRandomDate",
- "onCommand:extension.insertRandomCountry",
- "onCommand:extension.insertRandomNumber",
- "onCommand:extension.insertRandomString",
- "onCommand:extension.insertLorem",
- "onCommand:extension.insertLoremSmall",
- "onCommand:extension.insertLoremMedium",
- "onCommand:extension.insertLoremLarge",
- "onCommand:extension.insertRandomHash",
- "onCommand:extension.insertRandomHashSmall",
- "onCommand:extension.insertRandomHashMedium",
- "onCommand:extension.insertRandomHashLarge"
- ],
- "main": "./out/extension.js",
- "contributes": {
- "commands": [
- {
- "command": "extension.insertRandomAnimal",
- "title": "Insert Random: Animal"
- },
- {
- "command": "extension.insertRandomPerson",
- "title": "Insert Random: Person"
- },
- {
- "command": "extension.insertRandomDate",
- "title": "Insert Random: Date"
- },
- {
- "command": "extension.insertRandomCountry",
- "title": "Insert Random: Country"
- },
- {
- "command": "extension.insertRandomNumber",
- "title": "Insert Random: Number"
- },
- {
- "command": "extension.insertRandomString",
- "title": "Insert Random: String"
- },
- {
- "command": "extension.insertLorem",
- "title": "Insert Random: Lorem"
- },
- {
- "command": "extension.insertLoremSmall",
- "title": "Insert Random: Lorem Small"
- },
- {
- "command": "extension.insertLoremMedium",
- "title": "Insert Random: Lorem Medium"
- },
- {
- "command": "extension.insertLoremLarge",
- "title": "Insert Random: Lorem Large"
- },
- {
- "command": "extension.insertRandomHash",
- "title": "Insert Random: Hash"
- },
- {
- "command": "extension.insertRandomHashSmall",
- "title": "Insert Random: Hash Small"
- },
- {
- "command": "extension.insertRandomHashMedium",
- "title": "Insert Random: Hash Medium"
- },
- {
- "command": "extension.insertRandomHashLarge",
- "title": "Insert Random: Hash Large"
- }
- ],
- "configuration": [
- {
- "title": "Insert Random Text",
- "properties": {
- "quoteStyle": {
- "title": "Quote style",
- "description": "Select a quote style for import path.",
- "type": "string",
- "default": "Single quotes",
- "enum": [
- "Single quotes",
- "Double quotes"
- ],
- "enumDescriptions": [
- "Wrap import paths with single quotes",
- "Wrap import paths with double quotes"
- ]
- },
- "insertType": {
- "title": "Insert type",
- "description": "Insert options on text editor.",
- "type": "string",
- "default": "Cursor",
- "enum": [
- "Cursor",
- "Top"
- ],
- "enumDescriptions": [
- "Insert on selected line.",
- "Insert in line 1."
- ]
- },
- "loremSize": {
- "title": "Lorem size (for command 'Insert Lorem')",
- "description": "Enter lorem string length. { min: 25, max: 1000 }",
- "type": "number",
- "default": 25,
- "minimum": 25,
- "maximum": 2500
- },
- "hashSize": {
- "title": "Hash size (for command 'Insert Lorem')",
- "description": "Enter hash string length. { min: 7, max: 70 }",
- "type": "number",
- "default": 13,
- "minimum": 7,
- "maximum": 70
- },
- "disableNotifs": {
- "title": "Disable notifications",
- "description": "Disable insert random animal notification.",
- "type": "boolean",
- "default": false
- },
- "withQuote": {
- "title": "Wrap with quotes",
- "description": "Toggle to wrap random text with quotes.",
- "type": "boolean",
- "default": true
- },
- "withNewLine": {
- "title": "Include newline",
- "description": "Toggle include newline at the end of each insert.",
- "type": "boolean",
- "default": true
- }
- }
- }
- ]
- },
- "scripts": {
- "vscode:prepublish": "npm run compile",
- "compile": "tsc -p ./",
- "lint": "eslint src --ext ts",
- "watch": "tsc -watch -p ./",
- "pretest": "npm run compile && npm run lint",
- "test": "node ./out/test/runTest.js"
- },
- "devDependencies": {
- "@types/camelcase": "^5.2.0",
- "@types/chance": "^1.0.9",
- "@types/glob": "^7.1.1",
- "@types/mocha": "^7.0.1",
- "@types/node": "^12.11.7",
- "@types/vscode": "^1.43.0",
- "@typescript-eslint/eslint-plugin": "^2.18.0",
- "@typescript-eslint/parser": "^2.18.0",
- "eslint": "^6.8.0",
- "glob": "^7.1.6",
- "mocha": "^7.0.1",
- "typescript": "^3.7.5",
- "vscode-test": "^1.3.0"
- },
- "dependencies": {
- "camelcase": "^5.3.1",
- "chance": "^1.1.4"
- }
+ "name": "insert-random-text",
+ "displayName": "Random, Fake & Mock Data Generator",
+ "description": "Insert 130+ types of random, fake & mock data at every cursor — names, emails, UUIDs, lorem ipsum, JSON/SQL/CSV records & whole datasets, templates, locales, anonymize in place. Bulk, seeded, fully offline. The only data faker you'll need.",
+ "publisher": "ElecTreeFrying",
+ "version": "0.1.4",
+ "author": {
+ "name": "John James Ermitaño",
+ "email": "jjgermitano@gmail.com"
+ },
+ "icon": "images/icon.png",
+ "engines": {
+ "vscode": "^1.97.0"
+ },
+ "homepage": "https://github.com/ElecTreeFrying/insert-random-text/blob/main/README.md",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/ElecTreeFrying/insert-random-text.git"
+ },
+ "bugs": {
+ "email": "electreefrying.git@gmail.com",
+ "url": "https://github.com/ElecTreeFrying/insert-random-text/issues"
+ },
+ "qna": "https://github.com/ElecTreeFrying/insert-random-text/issues",
+ "license": "MIT",
+ "categories": [
+ "Snippets",
+ "Testing",
+ "Other"
+ ],
+ "keywords": [
+ "fake data",
+ "mock data",
+ "random",
+ "test data",
+ "dummy data",
+ "data generator",
+ "faker",
+ "lorem ipsum",
+ "uuid",
+ "guid",
+ "insert random",
+ "sample data",
+ "json",
+ "csv",
+ "sql",
+ "fixtures",
+ "name generator",
+ "address",
+ "email",
+ "credit card",
+ "iban",
+ "seed",
+ "git",
+ "ulid",
+ "multi-cursor",
+ "jwt",
+ "mongodb",
+ "locale",
+ "i18n",
+ "anonymize"
+ ],
+ "galleryBanner": {
+ "color": "#212128",
+ "theme": "dark"
+ },
+ "pricing": "Free",
+ "sponsor": {
+ "url": "https://github.com/ElecTreeFrying/insert-random-text#support"
+ },
+ "badges": [
+ {
+ "url": "https://vsmarketplacebadges.dev/version-short/ElecTreeFrying.insert-random-text.png",
+ "description": "Version badge",
+ "href": "https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.insert-random-text"
+ },
+ {
+ "url": "https://vsmarketplacebadges.dev/downloads-short/ElecTreeFrying.insert-random-text.png",
+ "description": "Downloads badge",
+ "href": "https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.insert-random-text"
+ },
+ {
+ "url": "https://vsmarketplacebadges.dev/installs-short/ElecTreeFrying.insert-random-text.png",
+ "description": "Installs badge",
+ "href": "https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.insert-random-text"
+ },
+ {
+ "url": "https://vsmarketplacebadges.dev/rating-short/ElecTreeFrying.insert-random-text.png",
+ "description": "Rating badge",
+ "href": "https://marketplace.visualstudio.com/items?itemName=ElecTreeFrying.insert-random-text"
+ }
+ ],
+ "main": "./dist/extension.js",
+ "browser": "./dist/web/extension.js",
+ "extensionKind": [
+ "workspace"
+ ],
+ "capabilities": {
+ "untrustedWorkspaces": {
+ "supported": true
+ },
+ "virtualWorkspaces": true
+ },
+ "contributes": {
+ "commands": [
+ {
+ "command": "extension.insertRandomAnimal",
+ "title": "Insert Random: Animal"
+ },
+ {
+ "command": "extension.insertRandomPerson",
+ "title": "Insert Random: Person"
+ },
+ {
+ "command": "extension.insertRandomDate",
+ "title": "Insert Random: Date"
+ },
+ {
+ "command": "extension.insertRandomCountry",
+ "title": "Insert Random: Country"
+ },
+ {
+ "command": "extension.insertRandomNumber",
+ "title": "Insert Random: Number"
+ },
+ {
+ "command": "extension.insertRandomString",
+ "title": "Insert Random: String"
+ },
+ {
+ "command": "extension.insertLorem",
+ "title": "Insert Random: Lorem"
+ },
+ {
+ "command": "extension.insertLoremSmall",
+ "title": "Insert Random: Lorem Small"
+ },
+ {
+ "command": "extension.insertLoremMedium",
+ "title": "Insert Random: Lorem Medium"
+ },
+ {
+ "command": "extension.insertLoremLarge",
+ "title": "Insert Random: Lorem Large"
+ },
+ {
+ "command": "extension.insertRandomHash",
+ "title": "Insert Random: Hash"
+ },
+ {
+ "command": "extension.insertRandomHashSmall",
+ "title": "Insert Random: Hash Small"
+ },
+ {
+ "command": "extension.insertRandomHashMedium",
+ "title": "Insert Random: Hash Medium"
+ },
+ {
+ "command": "extension.insertRandomHashLarge",
+ "title": "Insert Random: Hash Large"
+ },
+ {
+ "command": "insertRandomText.pick",
+ "title": "Insert Random: Pick…"
+ },
+ {
+ "command": "insertRandomText.record",
+ "title": "Insert Random: Record…"
+ },
+ {
+ "command": "insertRandomText.generateDataset",
+ "title": "Insert Random: Generate Dataset…"
+ },
+ {
+ "command": "insertRandomText.randomizeSelection",
+ "title": "Insert Random: Randomize Selection"
+ },
+ {
+ "command": "insertRandomText.numberRange",
+ "title": "Insert Random: Number (Range…)"
+ },
+ {
+ "command": "insertRandomText.floatRange",
+ "title": "Insert Random: Float (Range…)"
+ },
+ {
+ "command": "insertRandomText.stringLength",
+ "title": "Insert Random: String (Length…)"
+ },
+ {
+ "command": "insertRandomText.dateBetween",
+ "title": "Insert Random: Date (Between…)"
+ },
+ {
+ "command": "insertRandomText.wordsCount",
+ "title": "Insert Random: Words (Count…)"
+ },
+ {
+ "command": "insertRandomText.sentencesCount",
+ "title": "Insert Random: Sentences (Count…)"
+ },
+ {
+ "command": "insertRandomText.paragraphsCount",
+ "title": "Insert Random: Paragraphs (Count…)"
+ },
+ {
+ "command": "insertRandomText.uuidFormat",
+ "title": "Insert Random: UUID (Format…)"
+ },
+ {
+ "command": "insertRandomText.passwordOptions",
+ "title": "Insert Random: Password (Options…)"
+ },
+ {
+ "command": "insertRandomText.phoneFormat",
+ "title": "Insert Random: Phone (Format…)"
+ },
+ {
+ "command": "insertRandomText.fromTemplate",
+ "title": "Insert Random: From Template…"
+ },
+ {
+ "command": "insertRandomText.fromPattern",
+ "title": "Insert Random: From Pattern…"
+ },
+ {
+ "command": "insertRandomText.sequence",
+ "title": "Insert Random: Sequence (Start/Step…)"
+ },
+ {
+ "command": "insertRandomText.uuid",
+ "title": "Insert Random: UUID"
+ },
+ {
+ "command": "insertRandomText.email",
+ "title": "Insert Random: Email"
+ },
+ {
+ "command": "insertRandomText.username",
+ "title": "Insert Random: Username"
+ },
+ {
+ "command": "insertRandomText.boolean",
+ "title": "Insert Random: Boolean"
+ },
+ {
+ "command": "insertRandomText.firstName",
+ "title": "Insert Random: First Name"
+ },
+ {
+ "command": "insertRandomText.lastName",
+ "title": "Insert Random: Last Name"
+ },
+ {
+ "command": "insertRandomText.phone",
+ "title": "Insert Random: Phone"
+ },
+ {
+ "command": "insertRandomText.city",
+ "title": "Insert Random: City"
+ },
+ {
+ "command": "insertRandomText.address",
+ "title": "Insert Random: Street Address"
+ },
+ {
+ "command": "insertRandomText.ipv4",
+ "title": "Insert Random: IP Address"
+ },
+ {
+ "command": "insertRandomText.mac",
+ "title": "Insert Random: MAC Address"
+ },
+ {
+ "command": "insertRandomText.url",
+ "title": "Insert Random: URL"
+ },
+ {
+ "command": "insertRandomText.color",
+ "title": "Insert Random: Color (hex)"
+ },
+ {
+ "command": "insertRandomText.password",
+ "title": "Insert Random: Password"
+ },
+ {
+ "command": "insertRandomText.word",
+ "title": "Insert Random: Word"
+ },
+ {
+ "command": "insertRandomText.sentence",
+ "title": "Insert Random: Sentence"
+ },
+ {
+ "command": "insertRandomText.middleName",
+ "title": "Insert Random: Middle Name"
+ },
+ {
+ "command": "insertRandomText.prefix",
+ "title": "Insert Random: Name Prefix"
+ },
+ {
+ "command": "insertRandomText.suffix",
+ "title": "Insert Random: Name Suffix"
+ },
+ {
+ "command": "insertRandomText.sex",
+ "title": "Insert Random: Sex"
+ },
+ {
+ "command": "insertRandomText.gender",
+ "title": "Insert Random: Gender"
+ },
+ {
+ "command": "insertRandomText.jobTitle",
+ "title": "Insert Random: Job Title"
+ },
+ {
+ "command": "insertRandomText.jobType",
+ "title": "Insert Random: Job Type"
+ },
+ {
+ "command": "insertRandomText.jobArea",
+ "title": "Insert Random: Job Area"
+ },
+ {
+ "command": "insertRandomText.bio",
+ "title": "Insert Random: Bio"
+ },
+ {
+ "command": "insertRandomText.zodiac",
+ "title": "Insert Random: Zodiac Sign"
+ },
+ {
+ "command": "insertRandomText.ipv6",
+ "title": "Insert Random: IPv6 Address"
+ },
+ {
+ "command": "insertRandomText.port",
+ "title": "Insert Random: Port"
+ },
+ {
+ "command": "insertRandomText.httpMethod",
+ "title": "Insert Random: HTTP Method"
+ },
+ {
+ "command": "insertRandomText.httpStatus",
+ "title": "Insert Random: HTTP Status Code"
+ },
+ {
+ "command": "insertRandomText.userAgent",
+ "title": "Insert Random: User Agent"
+ },
+ {
+ "command": "insertRandomText.domainName",
+ "title": "Insert Random: Domain Name"
+ },
+ {
+ "command": "insertRandomText.emoji",
+ "title": "Insert Random: Emoji"
+ },
+ {
+ "command": "insertRandomText.protocol",
+ "title": "Insert Random: Protocol"
+ },
+ {
+ "command": "insertRandomText.jwt",
+ "title": "Insert Random: JWT"
+ },
+ {
+ "command": "insertRandomText.displayName",
+ "title": "Insert Random: Display Name"
+ },
+ {
+ "command": "insertRandomText.company",
+ "title": "Insert Random: Company Name"
+ },
+ {
+ "command": "insertRandomText.catchPhrase",
+ "title": "Insert Random: Catch Phrase"
+ },
+ {
+ "command": "insertRandomText.buzzPhrase",
+ "title": "Insert Random: Buzz Phrase"
+ },
+ {
+ "command": "insertRandomText.product",
+ "title": "Insert Random: Product"
+ },
+ {
+ "command": "insertRandomText.productName",
+ "title": "Insert Random: Product Name"
+ },
+ {
+ "command": "insertRandomText.price",
+ "title": "Insert Random: Price"
+ },
+ {
+ "command": "insertRandomText.department",
+ "title": "Insert Random: Department"
+ },
+ {
+ "command": "insertRandomText.productMaterial",
+ "title": "Insert Random: Product Material"
+ },
+ {
+ "command": "insertRandomText.productDescription",
+ "title": "Insert Random: Product Description"
+ },
+ {
+ "command": "insertRandomText.isbn",
+ "title": "Insert Random: ISBN"
+ },
+ {
+ "command": "insertRandomText.amount",
+ "title": "Insert Random: Amount"
+ },
+ {
+ "command": "insertRandomText.currencyCode",
+ "title": "Insert Random: Currency Code"
+ },
+ {
+ "command": "insertRandomText.currencyName",
+ "title": "Insert Random: Currency Name"
+ },
+ {
+ "command": "insertRandomText.currencySymbol",
+ "title": "Insert Random: Currency Symbol"
+ },
+ {
+ "command": "insertRandomText.creditCard",
+ "title": "Insert Random: Credit Card Number"
+ },
+ {
+ "command": "insertRandomText.creditCardCVV",
+ "title": "Insert Random: Credit Card CVV"
+ },
+ {
+ "command": "insertRandomText.iban",
+ "title": "Insert Random: IBAN"
+ },
+ {
+ "command": "insertRandomText.bic",
+ "title": "Insert Random: BIC"
+ },
+ {
+ "command": "insertRandomText.accountNumber",
+ "title": "Insert Random: Account Number"
+ },
+ {
+ "command": "insertRandomText.routingNumber",
+ "title": "Insert Random: Routing Number"
+ },
+ {
+ "command": "insertRandomText.bitcoin",
+ "title": "Insert Random: Bitcoin Address"
+ },
+ {
+ "command": "insertRandomText.ethereum",
+ "title": "Insert Random: Ethereum Address"
+ },
+ {
+ "command": "insertRandomText.pin",
+ "title": "Insert Random: PIN"
+ },
+ {
+ "command": "insertRandomText.zipCode",
+ "title": "Insert Random: Zip Code"
+ },
+ {
+ "command": "insertRandomText.state",
+ "title": "Insert Random: State"
+ },
+ {
+ "command": "insertRandomText.stateAbbr",
+ "title": "Insert Random: State Abbreviation"
+ },
+ {
+ "command": "insertRandomText.countryCode",
+ "title": "Insert Random: Country Code"
+ },
+ {
+ "command": "insertRandomText.latitude",
+ "title": "Insert Random: Latitude"
+ },
+ {
+ "command": "insertRandomText.longitude",
+ "title": "Insert Random: Longitude"
+ },
+ {
+ "command": "insertRandomText.timeZone",
+ "title": "Insert Random: Time Zone"
+ },
+ {
+ "command": "insertRandomText.county",
+ "title": "Insert Random: County"
+ },
+ {
+ "command": "insertRandomText.street",
+ "title": "Insert Random: Street Name"
+ },
+ {
+ "command": "insertRandomText.secondaryAddress",
+ "title": "Insert Random: Secondary Address"
+ },
+ {
+ "command": "insertRandomText.buildingNumber",
+ "title": "Insert Random: Building Number"
+ },
+ {
+ "command": "insertRandomText.direction",
+ "title": "Insert Random: Direction"
+ },
+ {
+ "command": "insertRandomText.pastDate",
+ "title": "Insert Random: Past Date"
+ },
+ {
+ "command": "insertRandomText.futureDate",
+ "title": "Insert Random: Future Date"
+ },
+ {
+ "command": "insertRandomText.recentDate",
+ "title": "Insert Random: Recent Date"
+ },
+ {
+ "command": "insertRandomText.soonDate",
+ "title": "Insert Random: Soon Date"
+ },
+ {
+ "command": "insertRandomText.birthdate",
+ "title": "Insert Random: Birthdate"
+ },
+ {
+ "command": "insertRandomText.weekday",
+ "title": "Insert Random: Weekday"
+ },
+ {
+ "command": "insertRandomText.month",
+ "title": "Insert Random: Month"
+ },
+ {
+ "command": "insertRandomText.float",
+ "title": "Insert Random: Float"
+ },
+ {
+ "command": "insertRandomText.hexNumber",
+ "title": "Insert Random: Hex Number"
+ },
+ {
+ "command": "insertRandomText.binary",
+ "title": "Insert Random: Binary Number"
+ },
+ {
+ "command": "insertRandomText.octal",
+ "title": "Insert Random: Octal Number"
+ },
+ {
+ "command": "insertRandomText.nanoid",
+ "title": "Insert Random: Nano ID"
+ },
+ {
+ "command": "insertRandomText.ulid",
+ "title": "Insert Random: ULID"
+ },
+ {
+ "command": "insertRandomText.alpha",
+ "title": "Insert Random: Alpha String"
+ },
+ {
+ "command": "insertRandomText.numeric",
+ "title": "Insert Random: Numeric String"
+ },
+ {
+ "command": "insertRandomText.hackerPhrase",
+ "title": "Insert Random: Hacker Phrase"
+ },
+ {
+ "command": "insertRandomText.slug",
+ "title": "Insert Random: Slug"
+ },
+ {
+ "command": "insertRandomText.words",
+ "title": "Insert Random: Words"
+ },
+ {
+ "command": "insertRandomText.bookTitle",
+ "title": "Insert Random: Book Title"
+ },
+ {
+ "command": "insertRandomText.bookAuthor",
+ "title": "Insert Random: Book Author"
+ },
+ {
+ "command": "insertRandomText.gitBranch",
+ "title": "Insert Random: Git Branch"
+ },
+ {
+ "command": "insertRandomText.gitCommitSha",
+ "title": "Insert Random: Git Commit SHA"
+ },
+ {
+ "command": "insertRandomText.gitCommitMessage",
+ "title": "Insert Random: Git Commit Message"
+ },
+ {
+ "command": "insertRandomText.fileName",
+ "title": "Insert Random: File Name"
+ },
+ {
+ "command": "insertRandomText.filePath",
+ "title": "Insert Random: File Path"
+ },
+ {
+ "command": "insertRandomText.fileExt",
+ "title": "Insert Random: File Extension"
+ },
+ {
+ "command": "insertRandomText.mimeType",
+ "title": "Insert Random: MIME Type"
+ },
+ {
+ "command": "insertRandomText.semver",
+ "title": "Insert Random: Semver"
+ },
+ {
+ "command": "insertRandomText.cron",
+ "title": "Insert Random: Cron Expression"
+ },
+ {
+ "command": "insertRandomText.rgb",
+ "title": "Insert Random: Color (rgb)"
+ },
+ {
+ "command": "insertRandomText.hsl",
+ "title": "Insert Random: Color (hsl)"
+ },
+ {
+ "command": "insertRandomText.colorName",
+ "title": "Insert Random: Color Name"
+ },
+ {
+ "command": "insertRandomText.vehicle",
+ "title": "Insert Random: Vehicle"
+ },
+ {
+ "command": "insertRandomText.vehicleManufacturer",
+ "title": "Insert Random: Vehicle Manufacturer"
+ },
+ {
+ "command": "insertRandomText.vehicleModel",
+ "title": "Insert Random: Vehicle Model"
+ },
+ {
+ "command": "insertRandomText.vin",
+ "title": "Insert Random: VIN"
+ },
+ {
+ "command": "insertRandomText.vrm",
+ "title": "Insert Random: License Plate (VRM)"
+ },
+ {
+ "command": "insertRandomText.dish",
+ "title": "Insert Random: Dish"
+ },
+ {
+ "command": "insertRandomText.ingredient",
+ "title": "Insert Random: Ingredient"
+ },
+ {
+ "command": "insertRandomText.fruit",
+ "title": "Insert Random: Fruit"
+ },
+ {
+ "command": "insertRandomText.vegetable",
+ "title": "Insert Random: Vegetable"
+ },
+ {
+ "command": "insertRandomText.cuisine",
+ "title": "Insert Random: Cuisine"
+ },
+ {
+ "command": "insertRandomText.songName",
+ "title": "Insert Random: Song Name"
+ },
+ {
+ "command": "insertRandomText.musicGenre",
+ "title": "Insert Random: Music Genre"
+ },
+ {
+ "command": "insertRandomText.artist",
+ "title": "Insert Random: Artist"
+ },
+ {
+ "command": "insertRandomText.album",
+ "title": "Insert Random: Album"
+ },
+ {
+ "command": "insertRandomText.dog",
+ "title": "Insert Random: Dog Breed"
+ },
+ {
+ "command": "insertRandomText.cat",
+ "title": "Insert Random: Cat Breed"
+ },
+ {
+ "command": "insertRandomText.bird",
+ "title": "Insert Random: Bird Species"
+ },
+ {
+ "command": "insertRandomText.fish",
+ "title": "Insert Random: Fish Species"
+ },
+ {
+ "command": "insertRandomText.horse",
+ "title": "Insert Random: Horse Breed"
+ },
+ {
+ "command": "insertRandomText.airline",
+ "title": "Insert Random: Airline"
+ },
+ {
+ "command": "insertRandomText.airport",
+ "title": "Insert Random: Airport"
+ },
+ {
+ "command": "insertRandomText.flightNumber",
+ "title": "Insert Random: Flight Number"
+ },
+ {
+ "command": "insertRandomText.seat",
+ "title": "Insert Random: Seat"
+ },
+ {
+ "command": "insertRandomText.imageUrl",
+ "title": "Insert Random: Image URL"
+ },
+ {
+ "command": "insertRandomText.avatarUrl",
+ "title": "Insert Random: Avatar URL"
+ },
+ {
+ "command": "insertRandomText.mongodbObjectId",
+ "title": "Insert Random: MongoDB ObjectId"
+ },
+ {
+ "command": "insertRandomText.setInsertType",
+ "title": "Insert Random: Set Insert Type"
+ },
+ {
+ "command": "insertRandomText.setOutputFormat",
+ "title": "Insert Random: Set Output Format"
+ },
+ {
+ "command": "insertRandomText.setDateFormat",
+ "title": "Insert Random: Set Date Format"
+ },
+ {
+ "command": "insertRandomText.setRecordFormat",
+ "title": "Insert Random: Set Record Format"
+ },
+ {
+ "command": "insertRandomText.setRecordSqlTable",
+ "title": "Insert Random: Set Record SQL Table"
+ },
+ {
+ "command": "insertRandomText.setBulkCount",
+ "title": "Insert Random: Set Bulk Count"
+ },
+ {
+ "command": "insertRandomText.setSeed",
+ "title": "Insert Random: Set Seed"
+ },
+ {
+ "command": "insertRandomText.setLocale",
+ "title": "Insert Random: Set Locale"
+ },
+ {
+ "command": "insertRandomText.toggleQuotes",
+ "title": "Insert Random: Toggle Wrap With Quotes"
+ },
+ {
+ "command": "insertRandomText.toggleNewLine",
+ "title": "Insert Random: Toggle Trailing New Line"
+ },
+ {
+ "command": "insertRandomText.toggleUniquePerCursor",
+ "title": "Insert Random: Toggle Unique Value Per Cursor"
+ },
+ {
+ "command": "insertRandomText.toggleStrictUnique",
+ "title": "Insert Random: Toggle Strict Unique"
+ },
+ {
+ "command": "insertRandomText.toggleContextMenu",
+ "title": "Insert Random: Toggle Editor Context Menu"
+ },
+ {
+ "command": "insertRandomText.manageTemplates",
+ "title": "Insert Random: Manage Templates"
+ },
+ {
+ "command": "insertRandomText.manageCustomLists",
+ "title": "Insert Random: Manage Custom Lists"
+ },
+ {
+ "command": "insertRandomText.resetSettings",
+ "title": "Insert Random: Reset Settings to Defaults"
+ }
+ ],
+ "submenus": [
+ {
+ "id": "insertRandomText.contextSubmenu",
+ "label": "Insert Random"
+ }
+ ],
+ "menus": {
+ "editor/context": [
+ {
+ "submenu": "insertRandomText.contextSubmenu",
+ "group": "1_modification",
+ "when": "editorTextFocus && config.insertRandomText.contextMenu.enabled"
+ }
+ ],
+ "insertRandomText.contextSubmenu": [
+ { "command": "insertRandomText.pick", "group": "1_pick" },
+ { "command": "insertRandomText.uuid", "group": "2_common" },
+ { "command": "extension.insertRandomPerson", "group": "2_common" },
+ { "command": "insertRandomText.email", "group": "2_common" },
+ { "command": "extension.insertRandomNumber", "group": "2_common" },
+ { "command": "extension.insertRandomDate", "group": "2_common" },
+ { "command": "extension.insertLorem", "group": "3_text" },
+ { "command": "insertRandomText.randomizeSelection", "group": "4_anonymize" }
+ ]
+ },
+ "configuration": [
+ {
+ "title": "Insert Random Text",
+ "properties": {
+ "insertType": {
+ "title": "Insert type",
+ "description": "Where generated values go.",
+ "type": "string",
+ "default": "Cursor",
+ "enum": [
+ "Cursor",
+ "Top",
+ "Clipboard"
+ ],
+ "enumDescriptions": [
+ "Fill a fresh value at each cursor.",
+ "Insert one block at line 1.",
+ "Copy to the clipboard instead of inserting (no editor needed)."
+ ]
+ },
+ "withQuote": {
+ "title": "Wrap with quotes",
+ "description": "Toggle to wrap random text with quotes.",
+ "type": "boolean",
+ "default": true
+ },
+ "withNewLine": {
+ "title": "Include newline",
+ "description": "Toggle to include a newline at the end of each insert.",
+ "type": "boolean",
+ "default": true
+ },
+ "insertRandomText.uniquePerCursor": {
+ "title": "Unique value per cursor",
+ "description": "Insert a different random value at each cursor (multi-cursor fill). When off, the same value is repeated at every cursor.",
+ "type": "boolean",
+ "default": true
+ },
+ "insertRandomText.strictUnique": {
+ "title": "Strict unique",
+ "description": "Re-draw duplicates so values meant to differ within one insert really do — the values of a bulk block, and values across cursors when unique value per cursor is on. Bounded at 25 re-draws per value, so a small pool (booleans, weekdays) that runs out keeps its duplicate instead of hanging. Applies to single-type inserts, not records.",
+ "type": "boolean",
+ "default": false
+ },
+ "insertRandomText.seed": {
+ "title": "Seed",
+ "description": "Set a number for reproducible output — the same seed produces the same values every run. Leave blank for random.",
+ "type": "string",
+ "default": ""
+ },
+ "insertRandomText.locale": {
+ "title": "Locale",
+ "description": "Language/region the generated data draws from — names, addresses, words and more come out localized. A type without localized data falls back to English.",
+ "type": "string",
+ "default": "en",
+ "enum": [
+ "en",
+ "de",
+ "fr",
+ "es",
+ "pt_BR",
+ "ja"
+ ],
+ "enumDescriptions": [
+ "English (default).",
+ "German — Deutsch.",
+ "French — Français.",
+ "Spanish — Español.",
+ "Brazilian Portuguese — Português (Brasil).",
+ "Japanese — 日本語."
+ ]
+ },
+ "insertRandomText.bulkCount": {
+ "title": "Bulk count",
+ "description": "How many values to insert at each cursor.",
+ "type": "number",
+ "default": 1,
+ "minimum": 1,
+ "maximum": 1000
+ },
+ "insertRandomText.outputFormat": {
+ "title": "Output format",
+ "description": "How multiple values (bulk count > 1) are formatted at each cursor.",
+ "type": "string",
+ "default": "plain",
+ "enum": [
+ "plain",
+ "jsonArray",
+ "quotedList"
+ ],
+ "enumDescriptions": [
+ "One value per line.",
+ "A JSON array, e.g. [ \"a\", \"b\" ].",
+ "A quoted, comma-separated list, e.g. \"a\", \"b\"."
+ ]
+ },
+ "insertRandomText.dateFormat": {
+ "title": "Date format",
+ "description": "How the timestamp Time generators (Date, Past/Future/Recent/Soon Date, Birthdate, Date (Between…)) render their value. Weekday and Month are unaffected.",
+ "type": "string",
+ "default": "iso",
+ "enum": [
+ "iso",
+ "isoDate",
+ "isoTime",
+ "unixSeconds",
+ "unixMillis"
+ ],
+ "enumDescriptions": [
+ "Full ISO 8601 timestamp, e.g. 2026-07-02T12:34:56.789Z.",
+ "Date only (YYYY-MM-DD), e.g. 2026-07-02.",
+ "Time only (HH:mm:ss), e.g. 12:34:56.",
+ "Unix time in seconds, e.g. 1783082096.",
+ "Unix time in milliseconds, e.g. 1783082096123."
+ ]
+ },
+ "insertRandomText.recordFormat": {
+ "title": "Record format",
+ "description": "Shape for multi-field records (Insert Random: Record…): a JSON object, a SQL INSERT row, or a CSV line.",
+ "type": "string",
+ "default": "json",
+ "enum": [
+ "json",
+ "sql",
+ "csv"
+ ],
+ "enumDescriptions": [
+ "A JSON object, e.g. { \"name\": \"…\", \"email\": \"…\" }.",
+ "A SQL INSERT row, e.g. INSERT INTO table (…) VALUES (…);",
+ "A CSV line, e.g. value,value,…"
+ ]
+ },
+ "insertRandomText.recordSqlTable": {
+ "title": "Record SQL table",
+ "description": "Table name used by the SQL record shape.",
+ "type": "string",
+ "default": "table"
+ },
+ "insertRandomText.templates": {
+ "title": "Saved templates",
+ "description": "Named faker templates, shown in a Templates group at the top of Insert Random: Pick…. Each value is a template with {{module.method}} placeholders, e.g. { \"invoice\": \"INV-{{string.numeric(4)}}\", \"contact\": \"{{person.fullName}} <{{internet.email}}>\" }. Entries that are not non-empty strings are ignored (logged to the console).",
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "insertRandomText.customLists": {
+ "title": "Custom lists",
+ "description": "Named lists of your own values, shown in a Custom Lists group at the top of Insert Random: Pick… and offered as Record… fields (the name becomes the field key). Each insert draws one random item, e.g. { \"environment\": [ \"dev\", \"staging\", \"production\" ] }. Non-string values are ignored (logged to the console).",
+ "type": "object",
+ "default": {},
+ "additionalProperties": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "insertRandomText.contextMenu.enabled": {
+ "title": "Show in editor context menu",
+ "description": "Add an \"Insert Random\" submenu to the editor right-click menu.",
+ "type": "boolean",
+ "default": false
+ }
+ }
+ }
+ ]
+ },
+ "scripts": {
+ "vscode:prepublish": "npm run package",
+ "compile": "npm run check-types && npm run lint && node esbuild.js",
+ "watch": "npm-run-all -p watch:*",
+ "watch:esbuild": "node esbuild.js --watch",
+ "watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
+ "package": "npm run check-types && npm run lint && node esbuild.js --production",
+ "compile-tests": "tsc -p . --outDir out",
+ "watch-tests": "tsc -p . -w --outDir out",
+ "pretest": "npm run compile-tests && npm run compile",
+ "check-types": "tsc --noEmit",
+ "lint": "eslint src",
+ "test": "vscode-test",
+ "test:coverage": "npm run pretest && vscode-test --coverage"
+ },
+ "dependencies": {
+ "@faker-js/faker": "^10.5.0"
+ },
+ "devDependencies": {
+ "@types/mocha": "^10.0.10",
+ "@types/node": "^22.19.18",
+ "@types/vscode": "1.97.0",
+ "@vscode/test-cli": "^0.0.12",
+ "@vscode/test-electron": "^2.5.2",
+ "esbuild": "^0.28.0",
+ "eslint": "^9.39.3",
+ "npm-run-all": "^4.1.5",
+ "typescript": "^5.9.3",
+ "typescript-eslint": "^8.59.2"
+ }
}
diff --git a/src/catalog.ts b/src/catalog.ts
new file mode 100644
index 0000000..8f7c2c3
--- /dev/null
+++ b/src/catalog.ts
@@ -0,0 +1,245 @@
+import { faker } from './engine';
+
+/** How the timestamp-emitting Time generators render their drawn Date. */
+export type DateFormat = 'iso' | 'isoDate' | 'isoTime' | 'unixSeconds' | 'unixMillis';
+
+/** Per-call options threaded from the cached settings into {@link Generator.generate};
+ * only the timestamp-emitting Time generators read it — everything else ignores it. */
+export interface GenerateOptions {
+ /** Timestamp rendering; defaults to 'iso' (the full ISO 8601 string). */
+ readonly dateFormat?: DateFormat;
+}
+
+/** A single named data type the extension can generate and insert. */
+export interface Generator {
+ /** Stable identifier — used in command ids and registry lookup. */
+ readonly id: string;
+ /** Human-readable label shown in the Quick Pick. */
+ readonly label: string;
+ /** Quick Pick group heading this generator is listed under. */
+ readonly group: string;
+ /** When true, hidden from the Quick Pick (a back-compat-only generator). */
+ readonly hidden?: boolean;
+ /** Produce one fresh value. Called once per cursor — must never be memoized. */
+ generate(options?: GenerateOptions): string;
+}
+
+/**
+ * Render a Date per the `dateFormat` setting. The ISO slices come from the UTC
+ * `toISOString()` — never local-time getters — so a seeded run renders the same
+ * text on any machine. `unixSeconds` floors (POSIX semantics: 1.5 s before the
+ * epoch is second -2), which matters for pre-1970 draws like `birthdate`.
+ */
+export function formatTimestamp(date: Date, format?: DateFormat): string {
+ switch (format) {
+ case 'isoDate': return date.toISOString().slice(0, 10);
+ case 'isoTime': return date.toISOString().slice(11, 19);
+ case 'unixSeconds': return Math.floor(date.getTime() / 1000).toString();
+ case 'unixMillis': return date.getTime().toString();
+ default: return date.toISOString();
+ }
+}
+
+/**
+ * The generator registry — the single source of truth for what the extension can
+ * produce. It drives generation, the contributed commands, and the Quick Pick, so
+ * adding an entry here surfaces a new type everywhere at once.
+ *
+ * One unified list grouped by category — each group appears exactly once, in the
+ * order its first member is listed (which is the Quick Pick heading order). Add a
+ * new type inside its group block. `hidden` entries (the legacy Lorem/Hash size
+ * variants) are kept at the very end and never shown in the picker.
+ */
+export const generators: readonly Generator[] = [
+ // Identity
+ { id: 'person', label: 'Full Name', group: 'Identity', generate: () => faker().person.fullName() },
+ { id: 'firstName', label: 'First Name', group: 'Identity', generate: () => faker().person.firstName() },
+ { id: 'middleName', label: 'Middle Name', group: 'Identity', generate: () => faker().person.middleName() },
+ { id: 'lastName', label: 'Last Name', group: 'Identity', generate: () => faker().person.lastName() },
+ { id: 'prefix', label: 'Name Prefix', group: 'Identity', generate: () => faker().person.prefix() },
+ { id: 'suffix', label: 'Name Suffix', group: 'Identity', generate: () => faker().person.suffix() },
+ { id: 'username', label: 'Username', group: 'Identity', generate: () => faker().internet.username() },
+ { id: 'displayName', label: 'Display Name', group: 'Identity', generate: () => faker().internet.displayName() },
+ { id: 'email', label: 'Email', group: 'Identity', generate: () => faker().internet.email() },
+ { id: 'phone', label: 'Phone', group: 'Identity', generate: () => faker().phone.number() },
+ { id: 'sex', label: 'Sex', group: 'Identity', generate: () => faker().person.sex() },
+ { id: 'gender', label: 'Gender', group: 'Identity', generate: () => faker().person.gender() },
+ { id: 'bio', label: 'Bio', group: 'Identity', generate: () => faker().person.bio() },
+ { id: 'zodiac', label: 'Zodiac Sign', group: 'Identity', generate: () => faker().person.zodiacSign() },
+ { id: 'jobTitle', label: 'Job Title', group: 'Identity', generate: () => faker().person.jobTitle() },
+ { id: 'jobType', label: 'Job Type', group: 'Identity', generate: () => faker().person.jobType() },
+ { id: 'jobArea', label: 'Job Area', group: 'Identity', generate: () => faker().person.jobArea() },
+
+ // Numbers
+ { id: 'number', label: 'Number', group: 'Numbers', generate: () => faker().number.int({ min: 0, max: 1000 }).toString() },
+ { id: 'float', label: 'Float', group: 'Numbers', generate: () => faker().number.float({ min: 0, max: 1000, fractionDigits: 2 }).toFixed(2) },
+ { id: 'boolean', label: 'Boolean', group: 'Numbers', generate: () => faker().datatype.boolean().toString() },
+ { id: 'hexNumber', label: 'Hex Number', group: 'Numbers', generate: () => faker().number.hex({ max: 0xffffff }) },
+ { id: 'binary', label: 'Binary Number', group: 'Numbers', generate: () => faker().number.binary({ max: 255 }) },
+ { id: 'octal', label: 'Octal Number', group: 'Numbers', generate: () => faker().number.octal({ max: 511 }) },
+
+ // Text — `string` is alphanumeric (symbol-free, so it survives quote-wrapping); `lorem` is randomized.
+ { id: 'string', label: 'String', group: 'Text', generate: () => faker().string.alphanumeric(15) },
+ { id: 'alpha', label: 'Alpha String', group: 'Text', generate: () => faker().string.alpha(10) },
+ { id: 'numeric', label: 'Numeric String', group: 'Text', generate: () => faker().string.numeric(10) },
+ { id: 'word', label: 'Word', group: 'Text', generate: () => faker().lorem.word() },
+ { id: 'words', label: 'Words', group: 'Text', generate: () => faker().lorem.words({ min: 3, max: 6 }) },
+ { id: 'sentence', label: 'Sentence', group: 'Text', generate: () => faker().lorem.sentence() },
+ { id: 'slug', label: 'Slug', group: 'Text', generate: () => faker().lorem.slug() },
+ { id: 'lorem', label: 'Lorem Paragraph', group: 'Text', generate: () => faker().lorem.paragraph() },
+ { id: 'hackerPhrase', label: 'Hacker Phrase', group: 'Text', generate: () => faker().hacker.phrase() },
+ { id: 'emoji', label: 'Emoji', group: 'Text', generate: () => faker().internet.emoji() },
+ { id: 'bookTitle', label: 'Book Title', group: 'Text', generate: () => faker().book.title() },
+ { id: 'bookAuthor', label: 'Book Author', group: 'Text', generate: () => faker().book.author() },
+
+ // Time — the timestamp emitters render per the dateFormat setting; weekday/month are names, not timestamps.
+ { id: 'date', label: 'Date', group: 'Time', generate: (opts) => formatTimestamp(faker().date.anytime(), opts?.dateFormat) },
+ { id: 'pastDate', label: 'Past Date', group: 'Time', generate: (opts) => formatTimestamp(faker().date.past(), opts?.dateFormat) },
+ { id: 'futureDate', label: 'Future Date', group: 'Time', generate: (opts) => formatTimestamp(faker().date.future(), opts?.dateFormat) },
+ { id: 'recentDate', label: 'Recent Date', group: 'Time', generate: (opts) => formatTimestamp(faker().date.recent(), opts?.dateFormat) },
+ { id: 'soonDate', label: 'Soon Date', group: 'Time', generate: (opts) => formatTimestamp(faker().date.soon(), opts?.dateFormat) },
+ { id: 'birthdate', label: 'Birthdate', group: 'Time', generate: (opts) => formatTimestamp(faker().date.birthdate(), opts?.dateFormat) },
+ { id: 'weekday', label: 'Weekday', group: 'Time', generate: () => faker().date.weekday() },
+ { id: 'month', label: 'Month', group: 'Time', generate: () => faker().date.month() },
+
+ // Location
+ { id: 'country', label: 'Country', group: 'Location', generate: () => faker().location.country() },
+ { id: 'countryCode', label: 'Country Code', group: 'Location', generate: () => faker().location.countryCode() },
+ { id: 'state', label: 'State', group: 'Location', generate: () => faker().location.state() },
+ { id: 'stateAbbr', label: 'State Abbreviation', group: 'Location', generate: () => faker().location.state({ abbreviated: true }) },
+ { id: 'county', label: 'County', group: 'Location', generate: () => faker().location.county() },
+ { id: 'city', label: 'City', group: 'Location', generate: () => faker().location.city() },
+ { id: 'zipCode', label: 'Zip Code', group: 'Location', generate: () => faker().location.zipCode() },
+ { id: 'street', label: 'Street Name', group: 'Location', generate: () => faker().location.street() },
+ { id: 'address', label: 'Street Address', group: 'Location', generate: () => faker().location.streetAddress() },
+ { id: 'secondaryAddress', label: 'Secondary Address', group: 'Location', generate: () => faker().location.secondaryAddress() },
+ { id: 'buildingNumber', label: 'Building Number', group: 'Location', generate: () => faker().location.buildingNumber() },
+ { id: 'direction', label: 'Direction', group: 'Location', generate: () => faker().location.direction() },
+ { id: 'latitude', label: 'Latitude', group: 'Location', generate: () => faker().location.latitude().toString() },
+ { id: 'longitude', label: 'Longitude', group: 'Location', generate: () => faker().location.longitude().toString() },
+ { id: 'timeZone', label: 'Time Zone', group: 'Location', generate: () => faker().location.timeZone() },
+
+ // Network
+ { id: 'ipv4', label: 'IP Address', group: 'Network', generate: () => faker().internet.ipv4() },
+ { id: 'ipv6', label: 'IPv6 Address', group: 'Network', generate: () => faker().internet.ipv6() },
+ { id: 'mac', label: 'MAC Address', group: 'Network', generate: () => faker().internet.mac() },
+ { id: 'url', label: 'URL', group: 'Network', generate: () => faker().internet.url() },
+ { id: 'domainName', label: 'Domain Name', group: 'Network', generate: () => faker().internet.domainName() },
+ { id: 'port', label: 'Port', group: 'Network', generate: () => faker().internet.port().toString() },
+ { id: 'protocol', label: 'Protocol', group: 'Network', generate: () => faker().internet.protocol() },
+ { id: 'httpMethod', label: 'HTTP Method', group: 'Network', generate: () => faker().internet.httpMethod() },
+ { id: 'httpStatus', label: 'HTTP Status Code', group: 'Network', generate: () => faker().internet.httpStatusCode().toString() },
+ { id: 'userAgent', label: 'User Agent', group: 'Network', generate: () => faker().internet.userAgent() },
+ { id: 'jwt', label: 'JWT', group: 'Network', generate: () => faker().internet.jwt() },
+
+ // Media
+ { id: 'imageUrl', label: 'Image URL', group: 'Media', generate: () => faker().image.url() },
+ { id: 'avatarUrl', label: 'Avatar URL', group: 'Media', generate: () => faker().image.avatar() },
+
+ // Design
+ { id: 'color', label: 'Color (hex)', group: 'Design', generate: () => faker().color.rgb({ format: 'hex' }) },
+ { id: 'rgb', label: 'Color (rgb)', group: 'Design', generate: () => faker().color.rgb({ format: 'css' }) },
+ { id: 'hsl', label: 'Color (hsl)', group: 'Design', generate: () => faker().color.hsl({ format: 'css' }) },
+ { id: 'colorName', label: 'Color Name', group: 'Design', generate: () => faker().color.human() },
+
+ // Security
+ { id: 'password', label: 'Password', group: 'Security', generate: () => faker().internet.password() },
+
+ // IDs
+ { id: 'uuid', label: 'UUID', group: 'IDs', generate: () => faker().string.uuid() },
+ { id: 'ulid', label: 'ULID', group: 'IDs', generate: () => faker().string.ulid() },
+ { id: 'nanoid', label: 'Nano ID', group: 'IDs', generate: () => faker().string.nanoid() },
+ { id: 'mongodbObjectId', label: 'MongoDB ObjectId', group: 'IDs', generate: () => faker().database.mongodbObjectId() },
+ { id: 'hash', label: 'Hash', group: 'IDs', generate: () => faker().string.hexadecimal({ length: 13, casing: 'lower', prefix: '' }) },
+
+ // Nature
+ { id: 'animal', label: 'Animal', group: 'Nature', generate: () => faker().animal.type() },
+ { id: 'dog', label: 'Dog Breed', group: 'Nature', generate: () => faker().animal.dog() },
+ { id: 'cat', label: 'Cat Breed', group: 'Nature', generate: () => faker().animal.cat() },
+ { id: 'bird', label: 'Bird Species', group: 'Nature', generate: () => faker().animal.bird() },
+ { id: 'fish', label: 'Fish Species', group: 'Nature', generate: () => faker().animal.fish() },
+ { id: 'horse', label: 'Horse Breed', group: 'Nature', generate: () => faker().animal.horse() },
+
+ // Company
+ { id: 'company', label: 'Company Name', group: 'Company', generate: () => faker().company.name() },
+ { id: 'catchPhrase', label: 'Catch Phrase', group: 'Company', generate: () => faker().company.catchPhrase() },
+ { id: 'buzzPhrase', label: 'Buzz Phrase', group: 'Company', generate: () => faker().company.buzzPhrase() },
+
+ // Commerce
+ { id: 'product', label: 'Product', group: 'Commerce', generate: () => faker().commerce.product() },
+ { id: 'productName', label: 'Product Name', group: 'Commerce', generate: () => faker().commerce.productName() },
+ { id: 'price', label: 'Price', group: 'Commerce', generate: () => faker().commerce.price() },
+ { id: 'department', label: 'Department', group: 'Commerce', generate: () => faker().commerce.department() },
+ { id: 'productMaterial', label: 'Product Material', group: 'Commerce', generate: () => faker().commerce.productMaterial() },
+ { id: 'productDescription', label: 'Product Description', group: 'Commerce', generate: () => faker().commerce.productDescription() },
+ { id: 'isbn', label: 'ISBN', group: 'Commerce', generate: () => faker().commerce.isbn() },
+
+ // Finance
+ { id: 'amount', label: 'Amount', group: 'Finance', generate: () => faker().finance.amount() },
+ { id: 'currencyCode', label: 'Currency Code', group: 'Finance', generate: () => faker().finance.currencyCode() },
+ { id: 'currencyName', label: 'Currency Name', group: 'Finance', generate: () => faker().finance.currencyName() },
+ { id: 'currencySymbol', label: 'Currency Symbol', group: 'Finance', generate: () => faker().finance.currencySymbol() },
+ { id: 'creditCard', label: 'Credit Card Number', group: 'Finance', generate: () => faker().finance.creditCardNumber() },
+ { id: 'creditCardCVV', label: 'Credit Card CVV', group: 'Finance', generate: () => faker().finance.creditCardCVV() },
+ { id: 'iban', label: 'IBAN', group: 'Finance', generate: () => faker().finance.iban() },
+ { id: 'bic', label: 'BIC', group: 'Finance', generate: () => faker().finance.bic() },
+ { id: 'accountNumber', label: 'Account Number', group: 'Finance', generate: () => faker().finance.accountNumber() },
+ { id: 'routingNumber', label: 'Routing Number', group: 'Finance', generate: () => faker().finance.routingNumber() },
+ { id: 'bitcoin', label: 'Bitcoin Address', group: 'Finance', generate: () => faker().finance.bitcoinAddress() },
+ { id: 'ethereum', label: 'Ethereum Address', group: 'Finance', generate: () => faker().finance.ethereumAddress() },
+ { id: 'pin', label: 'PIN', group: 'Finance', generate: () => faker().finance.pin() },
+
+ // Git
+ { id: 'gitBranch', label: 'Git Branch', group: 'Git', generate: () => faker().git.branch() },
+ { id: 'gitCommitSha', label: 'Git Commit SHA', group: 'Git', generate: () => faker().git.commitSha() },
+ { id: 'gitCommitMessage', label: 'Git Commit Message', group: 'Git', generate: () => faker().git.commitMessage() },
+
+ // System
+ { id: 'fileName', label: 'File Name', group: 'System', generate: () => faker().system.fileName() },
+ { id: 'filePath', label: 'File Path', group: 'System', generate: () => faker().system.filePath() },
+ { id: 'fileExt', label: 'File Extension', group: 'System', generate: () => faker().system.fileExt() },
+ { id: 'mimeType', label: 'MIME Type', group: 'System', generate: () => faker().system.mimeType() },
+ { id: 'semver', label: 'Semver', group: 'System', generate: () => faker().system.semver() },
+ { id: 'cron', label: 'Cron Expression', group: 'System', generate: () => faker().system.cron() },
+
+ // Vehicle
+ { id: 'vehicle', label: 'Vehicle', group: 'Vehicle', generate: () => faker().vehicle.vehicle() },
+ { id: 'vehicleManufacturer', label: 'Vehicle Manufacturer', group: 'Vehicle', generate: () => faker().vehicle.manufacturer() },
+ { id: 'vehicleModel', label: 'Vehicle Model', group: 'Vehicle', generate: () => faker().vehicle.model() },
+ { id: 'vin', label: 'VIN', group: 'Vehicle', generate: () => faker().vehicle.vin() },
+ { id: 'vrm', label: 'License Plate (VRM)', group: 'Vehicle', generate: () => faker().vehicle.vrm() },
+
+ // Food
+ { id: 'dish', label: 'Dish', group: 'Food', generate: () => faker().food.dish() },
+ { id: 'ingredient', label: 'Ingredient', group: 'Food', generate: () => faker().food.ingredient() },
+ { id: 'fruit', label: 'Fruit', group: 'Food', generate: () => faker().food.fruit() },
+ { id: 'vegetable', label: 'Vegetable', group: 'Food', generate: () => faker().food.vegetable() },
+ { id: 'cuisine', label: 'Cuisine', group: 'Food', generate: () => faker().food.ethnicCategory() },
+
+ // Music
+ { id: 'songName', label: 'Song Name', group: 'Music', generate: () => faker().music.songName() },
+ { id: 'musicGenre', label: 'Music Genre', group: 'Music', generate: () => faker().music.genre() },
+ { id: 'artist', label: 'Artist', group: 'Music', generate: () => faker().music.artist() },
+ { id: 'album', label: 'Album', group: 'Music', generate: () => faker().music.album() },
+
+ // Travel
+ { id: 'airline', label: 'Airline', group: 'Travel', generate: () => faker().airline.airline().name },
+ { id: 'airport', label: 'Airport', group: 'Travel', generate: () => faker().airline.airport().name },
+ { id: 'flightNumber', label: 'Flight Number', group: 'Travel', generate: () => faker().airline.flightNumber({ addLeadingZeros: true }) },
+ { id: 'seat', label: 'Seat', group: 'Travel', generate: () => faker().airline.seat() },
+
+ // Back-compat sized variants — drive the legacy Lorem/Hash Small/Medium/Large commands only.
+ { id: 'loremSmall', label: 'Lorem (small)', group: 'Text', hidden: true, generate: () => faker().lorem.sentence() },
+ { id: 'loremMedium', label: 'Lorem (medium)', group: 'Text', hidden: true, generate: () => faker().lorem.paragraph() },
+ { id: 'loremLarge', label: 'Lorem (large)', group: 'Text', hidden: true, generate: () => faker().lorem.paragraphs(3) },
+ { id: 'hashSmall', label: 'Hash (7)', group: 'IDs', hidden: true, generate: () => faker().string.hexadecimal({ length: 7, casing: 'lower', prefix: '' }) },
+ { id: 'hashMedium', label: 'Hash (17)', group: 'IDs', hidden: true, generate: () => faker().string.hexadecimal({ length: 17, casing: 'lower', prefix: '' }) },
+ { id: 'hashLarge', label: 'Hash (27)', group: 'IDs', hidden: true, generate: () => faker().string.hexadecimal({ length: 27, casing: 'lower', prefix: '' }) },
+];
+
+const byId = new Map(generators.map((generator) => [ generator.id, generator ]));
+
+/** Look up a generator by id, or `undefined` if none matches. */
+export function getGenerator(id: string): Generator | undefined {
+ return byId.get(id);
+}
diff --git a/src/config-retrival.ts b/src/config-retrival.ts
deleted file mode 100644
index 11825e1..0000000
--- a/src/config-retrival.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-
-const quoteStyleEnum = [
- { value: true, description: "Single quotes" },
- { value: false, description: "Double quotes" }
-]
-
-const insertTypeEnum = [
- { value: true, description: "Cursor" },
- { value: false, description: "Top" }
-]
-
-export interface Config {
- quoteStyle: boolean;
- insertType: boolean;
- loremSize: number;
- hashSize: number;
- disableNotifs: boolean;
- withQuote: boolean;
- withNewLine: boolean;
-}
-
-export const configEnum = {
- QUOTESTYLE: 'quoteStyle',
- INSERTTYPE: 'insertType',
- LOREMSIZE: 'loremSize',
- HASHSIZE: 'hashSize',
- DISABLENOTIFS: 'disableNotifs',
- WITHQUOTE: 'withQuote',
- WITHNEWLINE: 'withNewLine'
-}
-
-export class ConfigRetrival {
-
- private workspace: any = null;
-
- constructor(workspace: any) {
-
- this.workspace = workspace;
- }
-
- get param() {
- return {
- quoteStyle: this.quoteStyle,
- insertType: this.insertType,
- loremSize: this.loremSize,
- hashSize: this.hashSize,
- disableNotifs: this.disableNotifs,
- withQuote: this.withQuote,
- withNewLine: this.withNewLine
- }
- }
-
- get quoteStyle(): boolean {
- const configValue = this.workspace.getConfiguration().get('quoteStyle');
- return quoteStyleEnum.find(e => e.description === configValue).value;
- }
-
- get insertType(): boolean {
- const configValue = this.workspace.getConfiguration().get('insertType');
- return insertTypeEnum.find(e => e.description === configValue).value;
- }
-
- get loremSize(): number { return this.workspace.getConfiguration().get('loremSize'); }
- get hashSize(): number { return this.workspace.getConfiguration().get('hashSize'); }
- get disableNotifs(): boolean { return this.workspace.getConfiguration().get('disableNotifs'); }
- get withQuote(): boolean { return this.workspace.getConfiguration().get('withQuote'); }
- get withNewLine(): boolean { return this.workspace.getConfiguration().get('withNewLine'); }
-
-}
diff --git a/src/configuration.ts b/src/configuration.ts
new file mode 100644
index 0000000..166de6c
--- /dev/null
+++ b/src/configuration.ts
@@ -0,0 +1,181 @@
+import type { DateFormat } from './catalog';
+import { LOCALES, LocaleId } from './engine';
+
+/** The narrow slice of `vscode.workspace` this reader needs (kept minimal so it
+ * can be substituted in isolation). */
+export interface WorkspaceLike {
+ getConfiguration(): { get(section: string): T | undefined };
+}
+
+/** Where a generated value is delivered. */
+export type InsertTarget = 'cursor' | 'top' | 'clipboard';
+
+// `insertType` stores a display string; map each to its internal target.
+const INSERT_TARGETS: Record = {
+ Cursor: 'cursor',
+ Top: 'top',
+ Clipboard: 'clipboard',
+};
+
+// The declared `dateFormat` values; anything else falls back to the full-ISO default.
+const DATE_FORMATS: readonly DateFormat[] = [ 'iso', 'isoDate', 'isoTime', 'unixSeconds', 'unixMillis' ];
+
+/** The fully-resolved settings the extension runs on. */
+export interface Settings {
+ /** Where generated values are delivered: cursor, top of file, or clipboard. */
+ insertType: InsertTarget;
+ withQuote: boolean;
+ withNewLine: boolean;
+ uniquePerCursor: boolean;
+ /** Re-draw duplicates within one insert wherever values are meant to differ (bounded). */
+ strictUnique: boolean;
+ seed: string;
+ /** Which faker locale data set generators draw from. */
+ locale: LocaleId;
+ bulkCount: number;
+ outputFormat: string;
+ /** How the timestamp-emitting Time generators render their Date. */
+ dateFormat: DateFormat;
+ /** Structured shape for multi-field records: 'json' | 'sql' | 'csv'. */
+ recordFormat: string;
+ /** Table name used by the `sql` record shape. */
+ recordSqlTable: string;
+ /** Saved faker templates (name → template string), validated — junk entries dropped. */
+ templates: Readonly>;
+ /** Custom value lists (name → string[]), validated — junk entries dropped. */
+ customLists: Readonly>;
+}
+
+/** Configuration keys, exactly as declared in `package.json` `contributes.configuration`. */
+export const ConfigKey = {
+ INSERT_TYPE: 'insertType',
+ WITH_QUOTE: 'withQuote',
+ WITH_NEW_LINE: 'withNewLine',
+ UNIQUE_PER_CURSOR: 'insertRandomText.uniquePerCursor',
+ STRICT_UNIQUE: 'insertRandomText.strictUnique',
+ SEED: 'insertRandomText.seed',
+ LOCALE: 'insertRandomText.locale',
+ BULK_COUNT: 'insertRandomText.bulkCount',
+ OUTPUT_FORMAT: 'insertRandomText.outputFormat',
+ DATE_FORMAT: 'insertRandomText.dateFormat',
+ RECORD_FORMAT: 'insertRandomText.recordFormat',
+ RECORD_SQL_TABLE: 'insertRandomText.recordSqlTable',
+ TEMPLATES: 'insertRandomText.templates',
+ CUSTOM_LISTS: 'insertRandomText.customLists',
+} as const;
+
+/** True for a plain JSON-style object — the only shape the two data-pool settings accept. */
+function isPlainObject(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+/** Console prefix for the dropped-entry warnings, so they are findable in the extension host log. */
+const WARN_PREFIX = '[insert-random-text]';
+
+/** Where a dropped-entry warning goes. Injectable (like {@link WorkspaceLike}) because the extension
+ * host's patched `console` can't be monkey-swapped from a test; the default is the real console. */
+export type WarnSink = (message: string) => void;
+
+/**
+ * Reads the extension's settings from the workspace. Each getter resolves one
+ * key (with a safe default); {@link read} snapshots them all into a {@link Settings}.
+ * The `insertType` enum is normalized to a target here, so the rest of the code
+ * never deals with display strings.
+ */
+export class Configuration {
+ constructor(
+ private readonly workspace: WorkspaceLike,
+ private readonly warn: WarnSink = (message) => console.warn(message),
+ ) {}
+
+ /** Snapshot every setting into a plain {@link Settings} object. */
+ read(): Settings {
+ return {
+ insertType: this.insertType,
+ withQuote: this.withQuote,
+ withNewLine: this.withNewLine,
+ uniquePerCursor: this.uniquePerCursor,
+ strictUnique: this.strictUnique,
+ seed: this.seed,
+ locale: this.locale,
+ bulkCount: this.bulkCount,
+ outputFormat: this.outputFormat,
+ dateFormat: this.dateFormat,
+ recordFormat: this.recordFormat,
+ recordSqlTable: this.recordSqlTable,
+ templates: this.templates,
+ customLists: this.customLists,
+ };
+ }
+
+ private value(key: string): T | undefined {
+ return this.workspace.getConfiguration().get(key);
+ }
+
+ get insertType(): InsertTarget { return INSERT_TARGETS[this.value(ConfigKey.INSERT_TYPE) ?? 'Cursor'] ?? 'cursor'; }
+ get withQuote(): boolean { return this.value(ConfigKey.WITH_QUOTE) ?? true; }
+ get withNewLine(): boolean { return this.value(ConfigKey.WITH_NEW_LINE) ?? true; }
+ get uniquePerCursor(): boolean { return this.value(ConfigKey.UNIQUE_PER_CURSOR) ?? true; }
+ get strictUnique(): boolean { return this.value(ConfigKey.STRICT_UNIQUE) ?? false; }
+ get seed(): string { return this.value(ConfigKey.SEED) ?? ''; }
+ get locale(): LocaleId { const value = this.value(ConfigKey.LOCALE) ?? 'en'; return LOCALES.includes(value) ? value : 'en'; }
+ get bulkCount(): number { return this.value(ConfigKey.BULK_COUNT) ?? 1; }
+ get outputFormat(): string { return this.value(ConfigKey.OUTPUT_FORMAT) ?? 'plain'; }
+ get dateFormat(): DateFormat { const value = this.value(ConfigKey.DATE_FORMAT) ?? 'iso'; return DATE_FORMATS.includes(value) ? value : 'iso'; }
+ get recordFormat(): string { return this.value(ConfigKey.RECORD_FORMAT) ?? 'json'; }
+ get recordSqlTable(): string { return this.value(ConfigKey.RECORD_SQL_TABLE) ?? 'table'; }
+
+ /**
+ * Saved templates: name → faker template string. The object is user-edited JSON,
+ * so every entry is shape-checked — a junk entry (non-string or empty value,
+ * empty name) is dropped with a console warning, never thrown on. Template
+ * *content* is not validated here (rendering needs the engine); a template that
+ * fails to render surfaces a friendly error at insert time instead.
+ */
+ get templates(): Record {
+ const raw = this.value(ConfigKey.TEMPLATES) ?? {};
+ if (!isPlainObject(raw)) {
+ this.warn(`${WARN_PREFIX} Ignoring ${ConfigKey.TEMPLATES}: expected an object of name → template string.`);
+ return {};
+ }
+ const templates: Record = {};
+ for (const [ name, template ] of Object.entries(raw)) {
+ if (name.trim() === '' || typeof template !== 'string' || template.trim() === '') {
+ this.warn(`${WARN_PREFIX} Ignoring template "${name}": the value must be a non-empty string.`);
+ continue;
+ }
+ templates[name] = template;
+ }
+ return templates;
+ }
+
+ /**
+ * Custom lists: name → array of strings. Same tolerance as {@link templates}:
+ * non-string items are filtered out (warned), and an entry that is not an array,
+ * is left empty, or has an empty name is dropped (warned).
+ */
+ get customLists(): Record {
+ const raw = this.value(ConfigKey.CUSTOM_LISTS) ?? {};
+ if (!isPlainObject(raw)) {
+ this.warn(`${WARN_PREFIX} Ignoring ${ConfigKey.CUSTOM_LISTS}: expected an object of name → string array.`);
+ return {};
+ }
+ const lists: Record = {};
+ for (const [ name, list ] of Object.entries(raw)) {
+ if (name.trim() === '' || !Array.isArray(list)) {
+ this.warn(`${WARN_PREFIX} Ignoring custom list "${name}": the value must be an array of strings.`);
+ continue;
+ }
+ const values = list.filter((item): item is string => typeof item === 'string');
+ if (values.length < list.length) {
+ this.warn(`${WARN_PREFIX} Custom list "${name}": dropped ${list.length - values.length} non-string value(s).`);
+ }
+ if (values.length === 0) {
+ this.warn(`${WARN_PREFIX} Ignoring custom list "${name}": it holds no string values.`);
+ continue;
+ }
+ lists[name] = values;
+ }
+ return lists;
+ }
+}
diff --git a/src/custom.ts b/src/custom.ts
new file mode 100644
index 0000000..e3a8c0a
--- /dev/null
+++ b/src/custom.ts
@@ -0,0 +1,42 @@
+import type { Generator } from './catalog';
+import { faker } from './engine';
+
+/**
+ * User-defined data pools as generators: the `insertRandomText.templates` and
+ * `insertRandomText.customLists` settings, wrapped as plain {@link Generator}
+ * objects so they ride the exact same insert pipeline as the catalog — settings,
+ * quoting, bulk, multi-cursor, and seed all apply. Pure (no `vscode` import):
+ * extension.ts hands in the validated maps from the cached settings and lists
+ * the wrapped generators at the top of the Pick… / Record… Quick Picks.
+ *
+ * The id is the bare user-chosen name (not prefixed): it doubles as the Record…
+ * field key, so a list named `environment` becomes the `environment` JSON key /
+ * SQL column. These generators never enter the catalog registry, so a name that
+ * matches a catalog id shadows nothing.
+ */
+
+/** Quick Pick group heading for the saved templates. */
+export const TEMPLATES_GROUP = 'Templates';
+
+/** Quick Pick group heading for the custom lists. */
+export const CUSTOM_LISTS_GROUP = 'Custom Lists';
+
+/** Wrap each saved template as a Generator; a fresh render per generate() call. */
+export function templateGenerators(templates: Readonly>): Generator[] {
+ return Object.entries(templates).map(([ name, template ]) => ({
+ id: name,
+ label: name,
+ group: TEMPLATES_GROUP,
+ generate: () => faker().helpers.fake(template),
+ }));
+}
+
+/** Wrap each custom list as a Generator; every generate() draws one random item. */
+export function customListGenerators(lists: Readonly>): Generator[] {
+ return Object.entries(lists).map(([ name, values ]) => ({
+ id: name,
+ label: name,
+ group: CUSTOM_LISTS_GROUP,
+ generate: () => faker().helpers.arrayElement(values),
+ }));
+}
diff --git a/src/engine.ts b/src/engine.ts
new file mode 100644
index 0000000..0b0392c
--- /dev/null
+++ b/src/engine.ts
@@ -0,0 +1,71 @@
+import type { Faker } from '@faker-js/faker' with { 'resolution-mode': 'import' };
+
+/**
+ * faker lifecycle for the extension.
+ *
+ * faker v10 ships as pure ESM. Under the project's `module: Node16` setting a
+ * *static* `import` from these CommonJS-emitted files fails to type-check
+ * (TS1479), so each locale instance is loaded through a dynamic `import()` that
+ * esbuild inlines into the single CJS bundle. Only the shipped `/locale/`
+ * entries are imported — never the package root — so the other 60+ locales
+ * never reach the bundle.
+ */
+
+/** The locales the extension ships, exactly as faker spells them. */
+export const LOCALES = [ 'en', 'de', 'fr', 'es', 'pt_BR', 'ja' ] as const;
+export type LocaleId = (typeof LOCALES)[number];
+
+/** The instance draws go through — whichever locale {@link load} last resolved. */
+let active: Faker | undefined;
+
+/** One import per locale, cached as promises so concurrent loads share a single import. */
+const instances = new Map>();
+
+/**
+ * esbuild only inlines an `import()` whose specifier is a string literal, so
+ * every shipped locale needs its own literal here — deriving the path from the
+ * id would leave the import unresolvable at bundle time.
+ */
+function importLocale(locale: LocaleId): Promise<{ faker: Faker }> {
+ switch (locale) {
+ case 'de': return import('@faker-js/faker/locale/de');
+ case 'fr': return import('@faker-js/faker/locale/fr');
+ case 'es': return import('@faker-js/faker/locale/es');
+ case 'pt_BR': return import('@faker-js/faker/locale/pt_BR');
+ case 'ja': return import('@faker-js/faker/locale/ja');
+ default: return import('@faker-js/faker/locale/en');
+ }
+}
+
+/**
+ * Load faker and make `locale` the active instance. Idempotent and safe to call
+ * before every command: the first call per locale performs the import, later
+ * calls reuse the cached instance — so switching locales never needs a reload.
+ */
+export async function load(locale: LocaleId = 'en'): Promise {
+ let pending = instances.get(locale);
+ if (!pending) {
+ pending = importLocale(locale).then((module) => module.faker);
+ instances.set(locale, pending);
+ }
+ active = await pending;
+}
+
+/**
+ * The active faker instance. Throws if called before {@link load} has resolved —
+ * generators only ever run after activation has awaited `load()`.
+ */
+export function faker(): Faker {
+ if (!active) {
+ throw new Error('engine.faker() called before load(); await load() first.');
+ }
+ return active;
+}
+
+/**
+ * Seed the active instance's RNG. The same seed reproduces the same sequence of
+ * values (per locale); omit the argument to reseed randomly.
+ */
+export function seed(value?: number): void {
+ faker().seed(value);
+}
diff --git a/src/extension.ts b/src/extension.ts
index 597e493..9302cc4 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,206 +1,679 @@
import * as vscode from 'vscode';
-import * as Chance from 'chance';
-import { Config, ConfigRetrival, configEnum } from './config-retrival';
-import { InsertText } from './insert-text';
-
-let param: Config;
-
-function configObserve(context: vscode.ExtensionContext, retrival = new ConfigRetrival(vscode.workspace)) {
-
- param = retrival.param;
-
- context.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => {
-
- param.quoteStyle = e.affectsConfiguration(configEnum.QUOTESTYLE) ? retrival.quoteStyle : param.quoteStyle;
- param.insertType = e.affectsConfiguration(configEnum.INSERTTYPE) ? retrival.insertType : param.insertType;
- param.loremSize = e.affectsConfiguration(configEnum.LOREMSIZE) ? retrival.loremSize : param.loremSize;
- param.hashSize = e.affectsConfiguration(configEnum.HASHSIZE) ? retrival.hashSize : param.hashSize;
- param.disableNotifs = e.affectsConfiguration(configEnum.DISABLENOTIFS) ? retrival.disableNotifs : param.disableNotifs;
- param.withQuote = e.affectsConfiguration(configEnum.WITHQUOTE) ? retrival.withQuote : param.withQuote;
- param.withNewLine = e.affectsConfiguration(configEnum.WITHNEWLINE) ? retrival.withNewLine : param.withNewLine;
- }));
+import { ConfigKey, Configuration, Settings } from './configuration';
+import { formatTimestamp, Generator, generators, getGenerator } from './catalog';
+import { CUSTOM_LISTS_GROUP, customListGenerators, TEMPLATES_GROUP, templateGenerators } from './custom';
+import { faker, load, seed } from './engine';
+import { buildBlocks, InsertOptions, OutputFormat } from './formatter';
+import { PromptedCommand, promptedCommands, toGenerator } from './prompted';
+import { detect, randomize, shapeTimestamp, shapeUuid } from './randomize';
+import { resolveQuotePolicy } from './quotePolicy';
+import { buildRecords, RecordShape } from './record';
+import { SETTING_COMMANDS } from './settingsCommands';
+
+const PICK_COMMAND = 'insertRandomText.pick';
+const RECORD_COMMAND = 'insertRandomText.record';
+const DATASET_COMMAND = 'insertRandomText.generateDataset';
+const RANDOMIZE_COMMAND = 'insertRandomText.randomizeSelection';
+const CONFIG_KEYS: readonly string[] = Object.values(ConfigKey);
+
+/** Generate Dataset… row-count bounds: a hard cap, and the point above which a
+ * modal confirmation interposes (never block, just make the user mean it). */
+const DATASET_ROW_CAP = 100_000;
+const DATASET_CONFIRM_ABOVE = 10_000;
+
+/** The language each dataset shape opens as (there is no built-in csv language). */
+const DATASET_LANGUAGES: Readonly> = { json: 'json', sql: 'sql', csv: 'plaintext' };
+
+/** Cached settings snapshot; replaced wholesale by {@link watchConfiguration}. */
+let settings: Settings;
+
+/**
+ * Every contributed command id → the generator it inserts. The original
+ * `extension.insertRandom*` ids are kept for back-compat (existing keybindings);
+ * new types use namespaced `insertRandomText.*` ids. The Lorem/Hash size variants
+ * point at dedicated hidden generators.
+ */
+const COMMAND_TO_GENERATOR: Readonly> = {
+ // Original commands — kept for back-compat.
+ 'extension.insertRandomAnimal': 'animal',
+ 'extension.insertRandomPerson': 'person',
+ 'extension.insertRandomDate': 'date',
+ 'extension.insertRandomCountry': 'country',
+ 'extension.insertRandomNumber': 'number',
+ 'extension.insertRandomString': 'string',
+ 'extension.insertLorem': 'lorem',
+ 'extension.insertLoremSmall': 'loremSmall',
+ 'extension.insertLoremMedium': 'loremMedium',
+ 'extension.insertLoremLarge': 'loremLarge',
+ 'extension.insertRandomHash': 'hash',
+ 'extension.insertRandomHashSmall': 'hashSmall',
+ 'extension.insertRandomHashMedium': 'hashMedium',
+ 'extension.insertRandomHashLarge': 'hashLarge',
+ // Modern commands for the broader catalog.
+ 'insertRandomText.uuid': 'uuid',
+ 'insertRandomText.email': 'email',
+ 'insertRandomText.username': 'username',
+ 'insertRandomText.boolean': 'boolean',
+ 'insertRandomText.firstName': 'firstName',
+ 'insertRandomText.lastName': 'lastName',
+ 'insertRandomText.phone': 'phone',
+ 'insertRandomText.city': 'city',
+ 'insertRandomText.address': 'address',
+ 'insertRandomText.ipv4': 'ipv4',
+ 'insertRandomText.mac': 'mac',
+ 'insertRandomText.url': 'url',
+ 'insertRandomText.color': 'color',
+ 'insertRandomText.password': 'password',
+ 'insertRandomText.word': 'word',
+ 'insertRandomText.sentence': 'sentence',
+ // v1.0.0 breadth pass — id === command suffix for every entry below.
+ // Identity
+ 'insertRandomText.middleName': 'middleName',
+ 'insertRandomText.prefix': 'prefix',
+ 'insertRandomText.suffix': 'suffix',
+ 'insertRandomText.sex': 'sex',
+ 'insertRandomText.gender': 'gender',
+ 'insertRandomText.jobTitle': 'jobTitle',
+ 'insertRandomText.jobType': 'jobType',
+ 'insertRandomText.jobArea': 'jobArea',
+ 'insertRandomText.bio': 'bio',
+ 'insertRandomText.zodiac': 'zodiac',
+ // Network
+ 'insertRandomText.ipv6': 'ipv6',
+ 'insertRandomText.port': 'port',
+ 'insertRandomText.httpMethod': 'httpMethod',
+ 'insertRandomText.httpStatus': 'httpStatus',
+ 'insertRandomText.userAgent': 'userAgent',
+ 'insertRandomText.domainName': 'domainName',
+ 'insertRandomText.emoji': 'emoji',
+ 'insertRandomText.protocol': 'protocol',
+ 'insertRandomText.jwt': 'jwt',
+ 'insertRandomText.displayName': 'displayName',
+ // Media
+ 'insertRandomText.imageUrl': 'imageUrl',
+ 'insertRandomText.avatarUrl': 'avatarUrl',
+ // Company
+ 'insertRandomText.company': 'company',
+ 'insertRandomText.catchPhrase': 'catchPhrase',
+ 'insertRandomText.buzzPhrase': 'buzzPhrase',
+ // Commerce
+ 'insertRandomText.product': 'product',
+ 'insertRandomText.productName': 'productName',
+ 'insertRandomText.price': 'price',
+ 'insertRandomText.department': 'department',
+ 'insertRandomText.productMaterial': 'productMaterial',
+ 'insertRandomText.productDescription': 'productDescription',
+ 'insertRandomText.isbn': 'isbn',
+ // Finance
+ 'insertRandomText.amount': 'amount',
+ 'insertRandomText.currencyCode': 'currencyCode',
+ 'insertRandomText.currencyName': 'currencyName',
+ 'insertRandomText.currencySymbol': 'currencySymbol',
+ 'insertRandomText.creditCard': 'creditCard',
+ 'insertRandomText.creditCardCVV': 'creditCardCVV',
+ 'insertRandomText.iban': 'iban',
+ 'insertRandomText.bic': 'bic',
+ 'insertRandomText.accountNumber': 'accountNumber',
+ 'insertRandomText.routingNumber': 'routingNumber',
+ 'insertRandomText.bitcoin': 'bitcoin',
+ 'insertRandomText.ethereum': 'ethereum',
+ 'insertRandomText.pin': 'pin',
+ // Location
+ 'insertRandomText.zipCode': 'zipCode',
+ 'insertRandomText.state': 'state',
+ 'insertRandomText.stateAbbr': 'stateAbbr',
+ 'insertRandomText.countryCode': 'countryCode',
+ 'insertRandomText.latitude': 'latitude',
+ 'insertRandomText.longitude': 'longitude',
+ 'insertRandomText.timeZone': 'timeZone',
+ 'insertRandomText.county': 'county',
+ 'insertRandomText.street': 'street',
+ 'insertRandomText.secondaryAddress': 'secondaryAddress',
+ 'insertRandomText.buildingNumber': 'buildingNumber',
+ 'insertRandomText.direction': 'direction',
+ // Time
+ 'insertRandomText.pastDate': 'pastDate',
+ 'insertRandomText.futureDate': 'futureDate',
+ 'insertRandomText.recentDate': 'recentDate',
+ 'insertRandomText.soonDate': 'soonDate',
+ 'insertRandomText.birthdate': 'birthdate',
+ 'insertRandomText.weekday': 'weekday',
+ 'insertRandomText.month': 'month',
+ // Numbers
+ 'insertRandomText.float': 'float',
+ 'insertRandomText.hexNumber': 'hexNumber',
+ 'insertRandomText.binary': 'binary',
+ 'insertRandomText.octal': 'octal',
+ // IDs
+ 'insertRandomText.nanoid': 'nanoid',
+ 'insertRandomText.ulid': 'ulid',
+ 'insertRandomText.mongodbObjectId': 'mongodbObjectId',
+ // Text
+ 'insertRandomText.alpha': 'alpha',
+ 'insertRandomText.numeric': 'numeric',
+ 'insertRandomText.hackerPhrase': 'hackerPhrase',
+ 'insertRandomText.slug': 'slug',
+ 'insertRandomText.words': 'words',
+ 'insertRandomText.bookTitle': 'bookTitle',
+ 'insertRandomText.bookAuthor': 'bookAuthor',
+ // Git
+ 'insertRandomText.gitBranch': 'gitBranch',
+ 'insertRandomText.gitCommitSha': 'gitCommitSha',
+ 'insertRandomText.gitCommitMessage': 'gitCommitMessage',
+ // System
+ 'insertRandomText.fileName': 'fileName',
+ 'insertRandomText.filePath': 'filePath',
+ 'insertRandomText.fileExt': 'fileExt',
+ 'insertRandomText.mimeType': 'mimeType',
+ 'insertRandomText.semver': 'semver',
+ 'insertRandomText.cron': 'cron',
+ // Design
+ 'insertRandomText.rgb': 'rgb',
+ 'insertRandomText.hsl': 'hsl',
+ 'insertRandomText.colorName': 'colorName',
+ // Vehicle
+ 'insertRandomText.vehicle': 'vehicle',
+ 'insertRandomText.vehicleManufacturer': 'vehicleManufacturer',
+ 'insertRandomText.vehicleModel': 'vehicleModel',
+ 'insertRandomText.vin': 'vin',
+ 'insertRandomText.vrm': 'vrm',
+ // Food
+ 'insertRandomText.dish': 'dish',
+ 'insertRandomText.ingredient': 'ingredient',
+ 'insertRandomText.fruit': 'fruit',
+ 'insertRandomText.vegetable': 'vegetable',
+ 'insertRandomText.cuisine': 'cuisine',
+ // Music
+ 'insertRandomText.songName': 'songName',
+ 'insertRandomText.musicGenre': 'musicGenre',
+ 'insertRandomText.artist': 'artist',
+ 'insertRandomText.album': 'album',
+ // Nature
+ 'insertRandomText.dog': 'dog',
+ 'insertRandomText.cat': 'cat',
+ 'insertRandomText.bird': 'bird',
+ 'insertRandomText.fish': 'fish',
+ 'insertRandomText.horse': 'horse',
+ // Travel
+ 'insertRandomText.airline': 'airline',
+ 'insertRandomText.airport': 'airport',
+ 'insertRandomText.flightNumber': 'flightNumber',
+ 'insertRandomText.seat': 'seat',
+};
+
+/** Seed the cached settings, then re-snapshot them whenever a relevant key changes. */
+function watchConfiguration(context: vscode.ExtensionContext, reader = new Configuration(vscode.workspace)): void {
+ settings = reader.read();
+
+ context.subscriptions.push(
+ vscode.workspace.onDidChangeConfiguration((event) => {
+ if (CONFIG_KEYS.some((key) => event.affectsConfiguration(key))) {
+ settings = reader.read();
+ // Swap the active faker eagerly on a locale change (the first use of a
+ // locale imports its data set); a failure here is harmless — the next
+ // insert awaits its own load() and surfaces any real problem there.
+ if (event.affectsConfiguration(ConfigKey.LOCALE)) {
+ void load(settings.locale).catch(() => undefined);
+ }
+ }
+ }),
+ );
}
-export function activate(context: vscode.ExtensionContext) {
-
- configObserve(context);
-
- let insertRandomAnimal = vscode.commands.registerCommand('extension.insertRandomAnimal', async (chance = new Chance()) => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
-
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.animal.random);
- });
-
- param.disableNotifs ? 0 : vscode.window.showInformationMessage(`Animal location: ${text.animal.type}`);
- });
-
- let insertRandomPerson = vscode.commands.registerCommand('extension.insertRandomPerson', async (chance = new Chance()) => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.person);
- });
- });
-
- let insertRandomDate = vscode.commands.registerCommand('extension.insertRandomDate', async (chance = new Chance()) => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.date);
- });
- });
-
- let insertRandomCountry = vscode.commands.registerCommand('extension.insertRandomCountry', async (chance = new Chance()) => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.country);
- });
- });
-
- let insertRandomNumber = vscode.commands.registerCommand('extension.insertRandomNumber', async (chance = new Chance()) => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.number);
- });
- });
-
- let insertRandomString = vscode.commands.registerCommand('extension.insertRandomString', async (chance = new Chance()) => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.string);
- });
- });
-
- let insertLorem = vscode.commands.registerCommand('extension.insertLorem', async () => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, null);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.lorem);
- });
- });
-
- let insertLoremSmall = vscode.commands.registerCommand('extension.insertLoremSmall', async () => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, null);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.loremSmall);
- });
- });
-
- let insertLoremMedium = vscode.commands.registerCommand('extension.insertLoremMedium', async () => {
-
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, null);
-
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.loremMedium);
- });
- });
-
- let insertLoremLarge = vscode.commands.registerCommand('extension.insertLoremLarge', async () => {
+/**
+ * Derive the formatter options from the current settings, resolving the quote +
+ * escape from the active file's language (automatic and always on).
+ * `languageId` is undefined when there is no active editor (e.g. clipboard).
+ */
+function currentInsertOptions(languageId?: string): InsertOptions {
+ const policy = resolveQuotePolicy(languageId, { withQuote: settings.withQuote });
+ return {
+ quote: policy.quote,
+ escape: policy.escape,
+ newline: settings.withNewLine ? '\n' : '',
+ uniquePerCursor: settings.uniquePerCursor,
+ strictUnique: settings.strictUnique,
+ bulkCount: settings.bulkCount,
+ outputFormat: settings.outputFormat as OutputFormat,
+ dateFormat: settings.dateFormat,
+ };
+}
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, null);
+/**
+ * Apply the `seed` setting before a command runs. A non-empty numeric seed makes
+ * output reproducible — the same seed yields the same values every time; anything
+ * else (blank or non-numeric) leaves faker random.
+ */
+function applySeed(): void {
+ const raw = settings.seed.trim();
+ if (raw === '') { return; }
+ const value = Number(raw);
+ if (!Number.isNaN(value)) { seed(value); }
+}
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.loremLarge);
- });
- });
+/**
+ * The Cursor-mode fill: replace every selection with its block in one edit, then
+ * collapse each cursor to sit right after its inserted text. Without the collapse
+ * a replaced selection stays highlighted (`replace` keeps the anchor); a bare
+ * caret already lands after the block, so collapsing to `end` is a no-op there.
+ */
+async function fillSelections(editor: vscode.TextEditor, blockFor: (index: number) => string): Promise {
+ const { selections } = editor;
+ const applied = await editor.edit((builder) => {
+ selections.forEach((selection, index) => builder.replace(selection, blockFor(index)));
+ });
+ if (applied) {
+ editor.selections = editor.selections.map((selection) => new vscode.Selection(selection.end, selection.end));
+ }
+}
- let insertRandomHash = vscode.commands.registerCommand('extension.insertRandomHash', async (chance = new Chance()) => {
+/** Deliver a registry generator's output through {@link insertWith}. */
+async function insertGenerated(generatorId: string): Promise {
+ const generator = getGenerator(generatorId);
+ if (!generator) { return; }
+ await insertWith(generator);
+}
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
+/**
+ * The insert pipeline for ANY generator — registry entry or a one-off built by a
+ * prompted command: load faker, apply the seed, then deliver the output to the
+ * editor cursor(s), top of file, or clipboard per the cached settings. This is
+ * the seam prompted (parameterized) commands feed into.
+ */
+async function insertWith(generator: Generator): Promise {
+ await load(settings.locale);
+ applySeed();
+ const languageId = vscode.window.activeTextEditor?.document.languageId;
+ const options = currentInsertOptions(languageId);
+
+ if (settings.insertType === 'clipboard') {
+ // Clipboard: no editor needed. Copy a bare value (no quote-wrap or trailing
+ // newline, unless the output format is itself a list) and confirm unobtrusively.
+ const [ value ] = buildBlocks(1, generator, {
+ ...options,
+ quote: options.outputFormat === 'quotedList' ? options.quote : '',
+ newline: '',
+ uniquePerCursor: false,
+ });
+ await vscode.env.clipboard.writeText(value);
+ vscode.window.setStatusBarMessage(`$(clippy) Copied random ${generator.label.toLowerCase()} to clipboard`, 2500);
+ return;
+ }
+
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) { return; }
+
+ if (settings.insertType === 'top') {
+ // Top: a single block at line 1.
+ const [ block ] = buildBlocks(1, generator, { ...options, uniquePerCursor: false });
+ await editor.edit((builder) => builder.insert(new vscode.Position(0, 0), block));
+ } else {
+ // Cursor: a fresh block at every selection — the multi-cursor fill.
+ const blocks = buildBlocks(editor.selections.length, generator, options);
+ await fillSelections(editor, (index) => blocks[index]);
+ }
+}
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.hash.plain());
- });
- });
+/** The Quick Pick item shape for a pick step's options. */
+type StepPick = vscode.QuickPickItem & { value: string };
+
+/**
+ * Run a prompted command: walk its steps in order — input boxes prefilled with
+ * the last accepted value (`globalState`), Quick Picks with the last pick
+ * floated to the top and marked — then insert the resulting one-off generator
+ * through {@link insertWith}. Esc at any step is a clean cancel: nothing is
+ * inserted, nothing is remembered.
+ */
+async function runPrompted(context: vscode.ExtensionContext, command: PromptedCommand): Promise {
+ // Validation may test-render through faker (the template/pattern boxes), so
+ // the engine must be live before the first box opens; insertWith's own
+ // load() then no-ops.
+ await load(settings.locale);
+ const params: Record = {};
+ for (const step of command.steps) {
+ const memoryKey = `prompted.${command.id}.${step.key}`;
+ if (step.kind === 'pick') {
+ // On a virgin run nothing is remembered: no marker, and declaration order
+ // already leads with the step's fallback option.
+ const remembered = context.globalState.get(memoryKey);
+ const items: StepPick[] = step.options.map((option) => ({
+ label: option.label,
+ detail: option.detail,
+ description: option.value === remembered ? '$(check) Last used' : undefined,
+ value: option.value,
+ }));
+ const rememberedIdx = items.findIndex((item) => item.value === remembered);
+ if (rememberedIdx > 0) {
+ const [ item ] = items.splice(rememberedIdx, 1);
+ items.unshift(item);
+ }
+ const picked = await vscode.window.showQuickPick(items, { placeHolder: step.prompt, matchOnDetail: true });
+ if (!picked) { return; }
+ params[step.key] = picked.value;
+ await context.globalState.update(memoryKey, picked.value);
+ } else {
+ const input = await vscode.window.showInputBox({
+ prompt: step.prompt,
+ placeHolder: step.placeholder,
+ value: context.globalState.get(memoryKey) ?? step.fallback,
+ validateInput: (raw) => step.validate(raw, params),
+ });
+ if (input === undefined) { return; }
+ const accepted = input.trim();
+ params[step.key] = accepted;
+ await context.globalState.update(memoryKey, accepted);
+ }
+ }
+ await insertWith(toGenerator(command, params));
+}
- let insertRandomHashSmall = vscode.commands.registerCommand('extension.insertRandomHashSmall', async (chance = new Chance()) => {
+type GeneratorPick = vscode.QuickPickItem & { generatorId?: string; generator?: Generator };
+
+/**
+ * The user-defined groups that lead a picker: saved templates (Pick… only —
+ * `includeTemplates`) and custom lists, each item carrying its wrapped generator.
+ * The description is the template text / the list values, so `matchOnDescription`
+ * finds an entry by its content. Empty settings contribute nothing.
+ */
+function customPickItems(includeTemplates: boolean): GeneratorPick[] {
+ const items: GeneratorPick[] = [];
+ if (includeTemplates) {
+ const templates = templateGenerators(settings.templates);
+ if (templates.length > 0) {
+ items.push({ label: TEMPLATES_GROUP, kind: vscode.QuickPickItemKind.Separator });
+ for (const generator of templates) {
+ items.push({ label: generator.label, description: settings.templates[generator.id], generator });
+ }
+ }
+ }
+ const customLists = customListGenerators(settings.customLists);
+ if (customLists.length > 0) {
+ items.push({ label: CUSTOM_LISTS_GROUP, kind: vscode.QuickPickItemKind.Separator });
+ for (const generator of customLists) {
+ items.push({ label: generator.label, description: settings.customLists[generator.id].join(' · '), generator });
+ }
+ }
+ return items;
+}
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
+/**
+ * Insert a settings-defined generator, surfacing a render failure as a friendly
+ * error: a saved template is only shape-checked when read (rendering needs the
+ * engine), so a typo'd placeholder throws here — before any edit is applied.
+ */
+async function insertCustom(generator: Generator): Promise {
+ try {
+ await insertWith(generator);
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ const manage = generator.group === TEMPLATES_GROUP ? 'Manage Templates' : 'Manage Custom Lists';
+ void vscode.window.showErrorMessage(`"${generator.label}" failed to render: ${message} — fix it via Insert Random: ${manage}.`);
+ }
+}
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.hash.small());
- });
- });
+/** "Insert Random: Pick…" — one entry point over the whole catalog, grouped by
+ * category, led by the user's saved templates and custom lists when defined.
+ * The chosen generator runs through the same insert path. */
+async function pickAndInsert(): Promise {
+ await load(settings.locale);
+
+ const byGroup = new Map();
+ for (const generator of generators) {
+ if (generator.hidden) { continue; }
+ const members = byGroup.get(generator.group) ?? [];
+ members.push(generator);
+ byGroup.set(generator.group, members);
+ }
+
+ const items: GeneratorPick[] = customPickItems(true);
+ for (const [ group, members ] of byGroup) {
+ items.push({ label: group, kind: vscode.QuickPickItemKind.Separator });
+ for (const generator of members) {
+ items.push({ label: generator.label, description: generator.id, generatorId: generator.id });
+ }
+ }
+
+ const choice = await vscode.window.showQuickPick(items, {
+ placeHolder: 'Insert Random — pick a type to insert at every cursor…',
+ matchOnDescription: true,
+ });
+ if (choice?.generator) {
+ await insertCustom(choice.generator);
+ } else if (choice?.generatorId) {
+ await insertGenerated(choice.generatorId);
+ }
+}
- let insertRandomHashMedium = vscode.commands.registerCommand('extension.insertRandomHashMedium', async (chance = new Chance()) => {
+/**
+ * The shared multi-select field picker behind Record… and Generate Dataset…:
+ * every visible generator under its category separator, led by the user's
+ * custom lists (whose name becomes the field key). Custom lists lead; templates
+ * stay Pick…-only (a field wants one atomic value, which a list draw is and a
+ * free-form template may not be). Returns the picked generators in picker
+ * display order — picked custom lists first, then catalog order — or undefined
+ * when the pick is cancelled or confirmed empty.
+ */
+async function pickRecordFields(placeHolder: string): Promise {
+ const byGroup = new Map();
+ for (const generator of generators) {
+ if (generator.hidden) { continue; }
+ const members = byGroup.get(generator.group) ?? [];
+ members.push(generator);
+ byGroup.set(generator.group, members);
+ }
+ const items: GeneratorPick[] = customPickItems(false);
+ const customLists = items.filter((item) => item.generator).map((item) => item.generator!);
+ for (const [ group, members ] of byGroup) {
+ items.push({ label: group, kind: vscode.QuickPickItemKind.Separator });
+ for (const generator of members) {
+ items.push({ label: generator.label, description: generator.id, generatorId: generator.id });
+ }
+ }
+
+ const picks = await vscode.window.showQuickPick(items, { canPickMany: true, placeHolder, matchOnDescription: true });
+ if (!picks || picks.length === 0) { return undefined; }
+
+ const pickedIds = new Set(picks.map((pick) => pick.generatorId));
+ const pickedCustom = new Set(picks.filter((pick) => pick.generator).map((pick) => pick.generator!.id));
+ return [
+ ...customLists.filter((generator) => pickedCustom.has(generator.id)),
+ ...generators.filter((generator) => !generator.hidden && pickedIds.has(generator.id)),
+ ];
+}
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
+/**
+ * "Insert Random: Record…" — multi-select fields from the catalog (plus the
+ * user's custom lists, whose name becomes the field key), then deliver one
+ * composed record (JSON object / SQL row / CSV line per `recordFormat`) to
+ * the configured insert target: every cursor, the top of the file, or the
+ * clipboard. Honors `bulkCount`, `uniquePerCursor`, and `seed`. An empty or
+ * cancelled pick inserts nothing; Cursor/Top with no active editor is a no-op.
+ */
+async function pickAndInsertRecord(): Promise {
+ await load(settings.locale);
+
+ const fields = await pickRecordFields('Pick fields for the record…');
+ if (!fields) { return; }
+
+ applySeed();
+ const shape = settings.recordFormat as RecordShape;
+ const options = { bulkCount: settings.bulkCount, sqlTable: settings.recordSqlTable, dateFormat: settings.dateFormat };
+
+ if (settings.insertType === 'clipboard') {
+ // Clipboard: no editor needed. The record is copied as-is — its shape
+ // (JSON/SQL/CSV) is already the final text, so nothing is stripped or wrapped.
+ await vscode.env.clipboard.writeText(buildRecords(fields, shape, options));
+ vscode.window.setStatusBarMessage('$(clippy) Copied random record to clipboard', 2500);
+ return;
+ }
+
+ const editor = vscode.window.activeTextEditor;
+ if (!editor) { return; }
+
+ if (settings.insertType === 'top') {
+ // Top: a single record block at line 1.
+ await editor.edit((builder) => builder.insert(new vscode.Position(0, 0), buildRecords(fields, shape, options)));
+ return;
+ }
+
+ // Cursor: a record at every selection — the multi-cursor fill.
+ const shared = settings.uniquePerCursor ? undefined : buildRecords(fields, shape, options);
+ await fillSelections(editor, () => (settings.uniquePerCursor ? buildRecords(fields, shape, options) : shared!));
+}
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.hash.medium());
- });
- });
+/** Pick the dataset shape, leading with the current `recordFormat` setting
+ * marked as such (`showQuickPick` has no true preselection — floating the
+ * configured shape to the top makes it the active row, one Enter away). */
+async function pickDatasetShape(): Promise {
+ const shapes: (vscode.QuickPickItem & { value: RecordShape })[] = [
+ { value: 'json', label: 'JSON', detail: 'An array of objects — one record per line' },
+ { value: 'sql', label: 'SQL', detail: `INSERT statements into ${settings.recordSqlTable}` },
+ { value: 'csv', label: 'CSV', detail: 'One line per record, led by a header row of field names' },
+ ];
+ const currentIdx = shapes.findIndex((shape) => shape.value === settings.recordFormat);
+ if (currentIdx >= 0) {
+ const [ current ] = shapes.splice(currentIdx, 1);
+ current.description = '$(check) Current record format';
+ shapes.unshift(current);
+ }
+ const picked = await vscode.window.showQuickPick(shapes, { placeHolder: 'Dataset format…' });
+ return picked?.value;
+}
- let insertRandomHashLarge = vscode.commands.registerCommand('extension.insertRandomHashLarge', async (chance = new Chance()) => {
+/** Row-count validation for Generate Dataset…: a whole number, 1 up to the cap. */
+function validateRowCount(raw: string): string | undefined {
+ const value = Number(raw.trim());
+ if (!Number.isInteger(value) || value < 1) { return 'Enter a whole number of rows (1 or more).'; }
+ if (value > DATASET_ROW_CAP) { return `Row count is capped at ${DATASET_ROW_CAP.toLocaleString('en-US')}.`; }
+ return undefined;
+}
- await vscode.commands.executeCommand('notifications.clearAll');
- const editor = vscode.window.activeTextEditor;
- const text = new InsertText(param, chance);
+/**
+ * "Insert Random: Generate Dataset…" — the Record… machinery pointed at a file:
+ * pick fields, pick a shape (the `recordFormat` setting leads the pick), enter
+ * a row count (default `bulkCount`, capped at 100,000, confirmed above 10,000),
+ * and the whole dataset opens as a NEW untitled document — a JSON array, SQL
+ * `INSERT` statements, or CSV with a header row. A file, not an insertion:
+ * `insertType` never applies and no editor needs to be open. Locale, seed,
+ * `dateFormat`, and `recordSqlTable` all apply; Esc anywhere generates nothing.
+ */
+async function generateDataset(): Promise {
+ await load(settings.locale);
+
+ const fields = await pickRecordFields('Pick fields for the dataset…');
+ if (!fields) { return; }
+ const shape = await pickDatasetShape();
+ if (!shape) { return; }
+
+ const input = await vscode.window.showInputBox({
+ prompt: `How many rows? (up to ${DATASET_ROW_CAP.toLocaleString('en-US')})`,
+ value: String(settings.bulkCount),
+ validateInput: validateRowCount,
+ });
+ if (input === undefined) { return; }
+ const rows = Number(input.trim());
+
+ if (rows > DATASET_CONFIRM_ABOVE) {
+ const go = await vscode.window.showWarningMessage(
+ `Generate ${rows.toLocaleString('en-US')} rows?`,
+ { modal: true, detail: 'A dataset this large can take a moment to build and open.' },
+ 'Generate',
+ );
+ if (go !== 'Generate') { return; }
+ }
+
+ applySeed();
+ const content = buildRecords(fields, shape, {
+ bulkCount: rows,
+ sqlTable: settings.recordSqlTable,
+ dateFormat: settings.dateFormat,
+ dataset: true,
+ });
+ const document = await vscode.workspace.openTextDocument({ content, language: DATASET_LANGUAGES[shape] });
+ await vscode.window.showTextDocument(document);
+}
- editor.edit((active) => {
- const pos = param.insertType ? editor.selection.anchor : new vscode.Position(0, 0);;
- active.insert(pos, text.hash.large());
- });
- });
+/**
+ * The typed redraw behind type-aware replace: a selection that IS an unambiguous
+ * email / uuid / ISO date / ISO timestamp gets a fresh REALISTIC value of the
+ * same type, dressed like the original (uuid case + braces, timestamp precision)
+ * — a scrambled uuid isn't valid hex and a scrambled date isn't a date.
+ * `undefined` means "not typed": the caller falls back to the scramble.
+ */
+function typedReplacement(text: string): string | undefined {
+ switch (detect(text)) {
+ case 'email': return faker().internet.email();
+ case 'uuid': return shapeUuid(faker().string.uuid(), text);
+ case 'isoDate': return formatTimestamp(faker().date.anytime(), 'isoDate');
+ case 'isoTimestamp': return shapeTimestamp(faker().date.anytime().toISOString(), text);
+ default: return undefined;
+ }
+}
- const disposable = [
- insertRandomAnimal, insertRandomPerson, insertRandomDate, insertRandomCountry, insertRandomNumber, insertRandomString, insertLorem, insertLoremSmall, insertLoremMedium, insertLoremLarge, insertRandomHash, insertRandomHashSmall, insertRandomHashMedium, insertRandomHashLarge
- ];
+/**
+ * "Insert Random: Randomize Selection" — anonymize in place, in two tiers:
+ * a selection that IS a typed value (email / uuid / ISO date / ISO timestamp)
+ * is replaced by a fresh realistic fake of the same type; everything else gets
+ * a format-preserving randomization of its own text (digits → digits, letters →
+ * letters matching case, everything else kept). A replacement, not an insertion
+ * — insert type, quoting, newline, bulk and output format never apply; locale
+ * and seed do, because every draw rides the shared faker RNG. Empty selections
+ * are left untouched, so a mixed multi-selection randomizes only what is
+ * actually selected.
+ */
+async function randomizeSelections(): Promise {
+ const editor = vscode.window.activeTextEditor;
+ if (!editor || editor.selections.every((selection) => selection.isEmpty)) {
+ void vscode.window.showInformationMessage('Select some text first — Randomize Selection replaces each selection in place.');
+ return;
+ }
+ await load(settings.locale);
+ applySeed();
+ const rng = (bound: number) => faker().number.int({ max: bound - 1 });
+ const blocks = editor.selections.map((selection) => {
+ if (selection.isEmpty) { return ''; }
+ const text = editor.document.getText(selection);
+ return typedReplacement(text) ?? randomize(text, rng);
+ });
+ await fillSelections(editor, (index) => blocks[index]);
+}
- context.subscriptions.push(...disposable);
+export function activate(context: vscode.ExtensionContext): void {
+ watchConfiguration(context);
+
+ for (const commandId of Object.keys(COMMAND_TO_GENERATOR)) {
+ context.subscriptions.push(
+ vscode.commands.registerCommand(commandId, () => insertGenerated(COMMAND_TO_GENERATOR[commandId])),
+ );
+ }
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand(PICK_COMMAND, () => pickAndInsert()),
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand(RECORD_COMMAND, () => pickAndInsertRecord()),
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand(DATASET_COMMAND, () => generateDataset()),
+ );
+
+ context.subscriptions.push(
+ vscode.commands.registerCommand(RANDOMIZE_COMMAND, () => randomizeSelections()),
+ );
+
+ // Prompted (parameterized) commands — input boxes first, then the normal insert path.
+ for (const prompted of promptedCommands) {
+ context.subscriptions.push(
+ vscode.commands.registerCommand(`insertRandomText.${prompted.id}`, () => runPrompted(context, prompted)),
+ );
+ }
+
+ // Settings commands — change any setting from the Command Palette (Quick Pick / toggle / input).
+ for (const [ commandId, handler ] of Object.entries(SETTING_COMMANDS)) {
+ context.subscriptions.push(vscode.commands.registerCommand(commandId, handler));
+ }
}
-export function deactivate() {}
+export function deactivate(): void { /* no teardown needed */ }
diff --git a/src/formatter.ts b/src/formatter.ts
new file mode 100644
index 0000000..09d6414
--- /dev/null
+++ b/src/formatter.ts
@@ -0,0 +1,109 @@
+import type { DateFormat, Generator } from './catalog';
+import type { EscapeStyle } from './quotePolicy';
+
+/** How a block of generated values is rendered. */
+export type OutputFormat = 'plain' | 'jsonArray' | 'quotedList';
+
+/** Everything {@link buildBlocks} needs to render values, derived from settings. */
+export interface InsertOptions {
+ /** Quote characters wrapped around each value ('' for none). */
+ readonly quote: string;
+ /** How the quote character is escaped inside a value (default 'backslash'). */
+ readonly escape?: EscapeStyle;
+ /** Trailing string after each block ('' or '\n'). */
+ readonly newline: string;
+ /** A fresh value per cursor (true) vs one value repeated across cursors (false). */
+ readonly uniquePerCursor: boolean;
+ /** How many values to generate at each cursor. */
+ readonly bulkCount: number;
+ /** How a block of values is rendered. */
+ readonly outputFormat: OutputFormat;
+ /** Timestamp rendering, handed through to each generate() (Time generators read it). */
+ readonly dateFormat?: DateFormat;
+ /** Re-draw duplicates so values meant to differ within one insert really do (bounded). */
+ readonly strictUnique?: boolean;
+}
+
+/**
+ * Wrap one value in `quote`, escaping the quote character inside it so the result
+ * is a valid string literal. Two escape styles:
+ *
+ * - `backslash` (default) — backslash-escape any `\`, then the quote char:
+ * `O'Brien` → `'O\'Brien'` (JS/TS, Python, JSON, …).
+ * - `sqlDouble` — double the quote char, no backslashing: `O'Brien` → `'O''Brien'`
+ * (SQL string literals).
+ *
+ * With no quote (`quote === ''`) the value is returned untouched. `jsonArray`
+ * skips this entirely and uses `JSON.stringify`, which escapes on its own.
+ */
+function wrap(value: string, quote: string, escape: EscapeStyle): string {
+ if (!quote) { return value; }
+ if (escape === 'sqlDouble') {
+ const escaped = value.split(quote).join(`${quote}${quote}`);
+ return `${quote}${escaped}${quote}`;
+ }
+ const escaped = value.split('\\').join('\\\\').split(quote).join(`\\${quote}`);
+ return `${quote}${escaped}${quote}`;
+}
+
+/**
+ * Re-draw budget per value under `strictUnique`. Bounded so a small value pool
+ * (booleans, weekdays) exhausts and keeps its duplicate instead of hanging.
+ * Every re-draw consumes RNG state, so this number is part of the seeded-output
+ * contract — changing it shifts every seeded strict-unique sequence.
+ */
+const UNIQUE_RETRIES = 25;
+
+/**
+ * Draw through the operation's seen-set: a value already produced is re-drawn, up
+ * to {@link UNIQUE_RETRIES} times; on exhaustion the duplicate is kept — never a
+ * hang, never an error.
+ */
+function uniqueDraw(draw: () => string, seen: Set): string {
+ let value = draw();
+ for (let retry = 0; retry < UNIQUE_RETRIES && seen.has(value); retry++) {
+ value = draw();
+ }
+ seen.add(value);
+ return value;
+}
+
+/** Render the string inserted at one cursor: `bulkCount` fresh values, formatted. */
+function formatBlock(generator: Generator, options: InsertOptions, seen?: Set): string {
+ const count = Math.max(1, Math.floor(options.bulkCount || 1));
+ const draw = () => generator.generate({ dateFormat: options.dateFormat });
+ const values = Array.from({ length: count }, () => (seen ? uniqueDraw(draw, seen) : draw()));
+ const escape = options.escape ?? 'backslash';
+
+ switch (options.outputFormat) {
+ case 'jsonArray':
+ // Each value is emitted as a JSON string (a number/boolean value stays quoted).
+ return `[ ${values.map((value) => JSON.stringify(value)).join(', ')} ]${options.newline}`;
+ case 'quotedList':
+ return `${values.map((value) => wrap(value, options.quote, escape)).join(', ')}${options.newline}`;
+ case 'plain':
+ default:
+ // One wrapped value per line; the trailing newline is controlled by `newline`.
+ return `${values.map((value) => wrap(value, options.quote, escape)).join('\n')}${options.newline}`;
+ }
+}
+
+/**
+ * Build one rendered block per cursor. With `uniquePerCursor`, every block draws
+ * fresh values — the core of multi-cursor fill. Pure by design: no editor and no
+ * `vscode` import, so it can be exercised in isolation. The editor glue (mapping
+ * these blocks onto `editor.selections`) lives in `extension.ts`.
+ *
+ * @param cursorCount one block is produced per cursor/selection
+ */
+export function buildBlocks(cursorCount: number, generator: Generator, options: InsertOptions): string[] {
+ // One seen-set per insert operation: with uniquePerCursor it spans every cursor's
+ // block; without, cursors repeat one block by design, so it covers only the values
+ // inside that shared block — strict unique never applies to values meant to match.
+ const seen = options.strictUnique ? new Set() : undefined;
+ if (!options.uniquePerCursor) {
+ const shared = formatBlock(generator, options, seen);
+ return Array.from({ length: cursorCount }, () => shared);
+ }
+ return Array.from({ length: cursorCount }, () => formatBlock(generator, options, seen));
+}
diff --git a/src/insert-text.ts b/src/insert-text.ts
deleted file mode 100644
index d0c76d2..0000000
--- a/src/insert-text.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { lorem } from './lorem';
-import { Config } from './config-retrival';
-
-const SINGLE_QUOTES = "\'";
-const DOUBLE_QUOTES = "\"";
-
-export class InsertText {
-
- chance: any;
- quote: string;
- nl: string;
- type: number;
- param: Config;
-
- constructor(param: Config, chance: any = null, type: number = 0) {
- this.chance = chance;
- this.quote = param.withQuote ? param.quoteStyle ? SINGLE_QUOTES : DOUBLE_QUOTES : '';
- this.nl = param.withNewLine ? '\n' : '';
- this.type = type;
- this.param = param;
- }
-
- get animal() {
- const types = [ 'Ocean', 'Desert', 'Grassland', 'Forest', 'Farm', 'Pet', 'Zoo' ];
- const randomInt = this.chance.integer({ min: 0, max: 6 });
- const type = types[randomInt];
- let random = this.chance.animal({ type });
- random = `${this.quote}${random}${this.quote}${this.nl}`;
-
- return { random, type };
- }
-
- get person() {
- const random = this.chance.name({ full: true });
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get date() {
- const random = this.chance.date().toString();
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get country() {
- const random = this.chance.country({ full: true });
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get number() {
- const random = this.chance.integer({ min: 0, max: 1000 }).toString();
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get string() {
- const random = this.chance.string({ alpha: true, symbols: true, length: 15 });
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get lorem() {
- const random = lorem.substring(0, this.param.loremSize);
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get loremSmall() {
- const random = lorem.substring(0, 177);
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get loremMedium() {
- const random = lorem.substring(0, 521);
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get loremLarge() {
- const random = lorem.substring(0, 1368);
- return `${this.quote}${random}${this.quote}${this.nl}`;
- }
-
- get hash() {
- return {
- plain: () => {
- const random = this.chance.hash({ length: this.param.hashSize })
- return `${this.quote}${random}${this.quote}${this.nl}`; },
- small: () => {
- const random = this.chance.hash({ length: 7 })
- return `${this.quote}${random}${this.quote}${this.nl}`; },
- medium: () => {
- const random = this.chance.hash({ length: 17 })
- return `${this.quote}${random}${this.quote}${this.nl}`; },
- large: () => {
- const random = this.chance.hash({ length: 27 })
- return `${this.quote}${random}${this.quote}${this.nl}`; },
- };
- }
-
-}
diff --git a/src/lorem.ts b/src/lorem.ts
deleted file mode 100644
index d5cdef4..0000000
--- a/src/lorem.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-
-export const lorem = 'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna. Sed consequat, leo eget bibendum sodales, augue velit cursus nunc, quis gravida magna mi a libero. Fusce vulputate eleifend sapien. Vestibulum purus quam, scelerisque ut, mollis sed, nonummy id, metus. Nullam accumsan lorem in dui. Cras ultricies mi eu turpis hendrerit fringilla. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; In ac dui quis mi consectetuer lacinia. Nam pretium turpis et arcu. Duis arcu tortor, suscipit eget, imperdiet nec, imperdiet iaculis, ipsum. Sed aliquam ultrices mauris. Integer ante arcu, accumsan a, consectetuer eget, posuere ut, mauris. Praesent adipiscing. Phasellus ullamcorper ipsum rutrum nunc. Nunc nonummy metus. Vestibulum volutpat pretium libero. Cras id dui. Aenean ut eros et nisl sagittis vestibulum. Nullam nulla eros, ultricies sit amet, nonummy id, imperdiet feugiat, pede. Sed lectus. Donec mollis hendrerit risus. Phasellus nec sem in justo pellentesque facilisis. Etiam imperdiet imperdiet orci. Nunc nec neque. Phasellus leo dolor, tempus non, auctor et, hendrerit quis, nisi. Curabitur ligula sapien, tincidunt non, euismod vitae, posuere imperdiet, leo. Maecenas malesuada. Praesent congue erat at massa. Sed cursus turpis vitae tortor. Donec posuere vulputate arcu. Phasellus accumsan cursus velit. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Sed aliquam, nisi quis porttitor congue, elit erat euismod orci, ac placerat dolor lectus quis orci. Phasellus consectetuer vestibulum elit. Aenean tellus metus, bibendum sed, posuere ac, mattis non, nunc. Vestibulum fringilla pede sit amet augue. In turpis. Pellentesque posuere. Praesent turpis. Aenean posuere, tortor sed cursus feugiat, nunc augue blandit nunc, eu sollicitudin urna dolor sagittis lacus. Donec elit libero, sodales nec, volutpat a, suscipit non, turpis. Nullam sagittis. Suspendisse pulvinar, augue ac venenatis condimentum, sem libero volutpat nibh, nec pellentesque velit pede quis nunc. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce id purus. Ut varius tincidunt libero. Phasellus dolor. Maecenas vestibulum mollis diam. Pellentesque ut neque. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. In dui magna, posuere eget, vestibulum et, tempor auctor, justo. In ac felis quis tortor malesuada pretium. Pellentesque auctor neque nec urna. Proin sapien ipsum, porta a, auctor quis, euismod ut, mi. Aenean viverra rhoncus pede. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Ut non enim eleifend felis pretium feugiat. Vivamus quis mi. Phasellus a est. Phasellus magna. In hac habitasse platea dictumst. Curabitur at lacus ac velit ornare lobortis. Curabitur a felis in nunc fringilla tristique. Morbi mattis ullamcorper velit. Phasellus gravida semper nisi. Nullam vel sem. Pellentesque libero tortor, tincidunt et, tincidunt eget, semper nec, quam. Sed hendrerit. Morbi ac felis. Nunc egestas, augue at pellentesque laoreet, felis eros vehicula leo, at malesuada velit leo quis pede. Donec interdum, metus et hendrerit aliquet, dolor diam sagittis ligula, eget egestas libero turpis vel mi. Nunc nulla. Fusce risus nisl, viverra et, tempor et, pretium in, sapien. Donec venenatis vulputate lorem. Morbi nec metus. Phasellus blandit leo ut odio. Maecenas ullamcorper, dui et placerat feugiat, eros pede varius nisi, condimentum viverra felis nunc et lorem. Sed magna purus, fermentum eu, tincidunt eu, varius ut, felis. In auctor lobortis lacus. Quisque libero metus, condimentum nec, tempor a, commodo mollis, magna. Vestibulum ullamcorper mauris at ligula. Fusce fermentum. Nullam cursus lacinia erat. Praesent blandit laoreet nibh. Fusce convallis metus id felis luctus adipiscing. Pellentesque egestas, neque sit amet convallis pulvinar, justo nulla eleifend augue, ac auctor orci leo non est. Quisque id mi. Ut tincidunt tincidunt erat. Etiam feugiat lorem non metus. Vestibulum dapibus nunc ac augue. Curabitur vestibulum aliquam leo. Praesent egestas neque eu enim. In hac habitasse platea dictumst. Fusce a quam. Etiam ut purus mattis mauris sodales aliquam. Curabitur nisi. Quisque malesuada placerat nisl. Nam ipsum risus, rutrum vitae, vestibulum eu, molestie vel, lacus. Sed augue ipsum, egestas nec, vestibulum et, malesuada adipiscing, dui. Vestibulum facilisis, purus nec pulvinar iaculis, ligula mi congue nunc, vitae euismod ligula urna in dolor. Mauris sollicitudin fermentum libero. Praesent nonummy mi in odio. Nunc interdum lacus sit amet orci. Vestibulum rutrum, mi nec elementum vehicula, eros quam gravida nisl, id fringilla neque ante vel mi. Morbi mollis tellus ac sapien. Phasellus volutpat, metus eget egestas mollis, lacus lacus blandit dui, id egestas quam mauris ut lacus. Fusce vel dui. Sed in libero ut nibh placerat accumsan. Proin faucibus arcu quis ante. In consectetuer turpis ut velit. Nulla sit amet est. Praesent metus tellus, elementum eu, semper a, adipiscing nec, purus. Cras risus ipsum, faucibus ut, ullamcorper id, varius ac, leo. Suspendisse feugiat. Suspendisse enim turpis, dictum sed, iaculis a, condimentum nec, nisi. Praesent nec nisl a purus blandit viverra. Praesent ac massa at ligula laoreet iaculis. Nulla neque dolor, sagittis eget, iaculis quis, molestie non, velit. Mauris turpis nunc, blandit et, volutpat molestie, porta ut, ligula. Fusce pharetra convallis urna. Quisque ut nisi. Donec mi odio, faucibus at, scelerisque quis,';
diff --git a/src/prompted.ts b/src/prompted.ts
new file mode 100644
index 0000000..e80d405
--- /dev/null
+++ b/src/prompted.ts
@@ -0,0 +1,501 @@
+import { formatTimestamp, GenerateOptions, Generator } from './catalog';
+import { faker } from './engine';
+
+/**
+ * Parameterized ("prompted") insert commands — types that ask for parameters in
+ * input boxes or Quick Picks before inserting: Number (Range…), Float (Range…),
+ * String (Length…), Date (Between…), Words/Sentences/Paragraphs (Count…),
+ * UUID (Format…), Password (Options…), Phone (Format…), From Template…,
+ * From Pattern…, Sequence (Start/Step…).
+ *
+ * These are deliberately NOT catalog entries: the registry stays a list of
+ * zero-argument generators, while each prompted command declares its steps
+ * here and is wrapped as a one-off {@link Generator} (via {@link toGenerator})
+ * that rides the exact same insert pipeline — settings, quoting, bulk,
+ * multi-cursor, and seed all apply. Pure by design: no `vscode` import — the
+ * box/pick walk lives in `extension.ts` (`runPrompted`), so validation, pick
+ * options, and rendering are checkable headless.
+ */
+
+/** One input box in a prompted command's flow. */
+export interface InputStep {
+ /** Step-kind discriminant; the input box is the default kind and may omit it. */
+ readonly kind?: 'input';
+ /** Params key; also the suffix of the last-used-value memory key. */
+ readonly key: string;
+ /** Input-box prompt text. */
+ readonly prompt: string;
+ /** Example text shown while the box is empty. */
+ readonly placeholder: string;
+ /** Prefill used when no last-used value is remembered. */
+ readonly fallback: string;
+ /**
+ * `showInputBox`-shaped validation: an error message keeps the box open,
+ * `undefined` accepts. `prior` holds the values accepted at earlier steps, so
+ * later boxes can enforce cross-field rules (e.g. min ≤ max).
+ */
+ validate(input: string, prior: Readonly>): string | undefined;
+}
+
+/** One selectable option in a {@link PickStep}. */
+export interface PickOption {
+ /** Params value stored under the step's key (also what gets remembered). */
+ readonly value: string;
+ /** Quick Pick row label. */
+ readonly label: string;
+ /** Explanatory line under the label — an example rendering where possible. */
+ readonly detail: string;
+}
+
+/** One Quick Pick in a prompted command's flow. A closed set of choices needs no
+ * validation; options are declared fallback-first, so a virgin pick leads with it. */
+export interface PickStep {
+ readonly kind: 'pick';
+ /** Params key; also the suffix of the last-pick memory key. */
+ readonly key: string;
+ /** Quick Pick placeholder text (the question). */
+ readonly prompt: string;
+ /** The choices, in display order. */
+ readonly options: readonly PickOption[];
+ /** Option value that acts as the default when no last pick is remembered. */
+ readonly fallback: string;
+}
+
+/** One step in a prompted command's flow: an input box (the default) or a Quick Pick. */
+export type PromptStep = InputStep | PickStep;
+
+/** A palette command that prompts for parameters, then inserts one data type. */
+export interface PromptedCommand {
+ /** Stable identifier — the command id is `insertRandomText.`. */
+ readonly id: string;
+ /** Human-readable label (the command title minus the `Insert Random:` prefix). */
+ readonly label: string;
+ /** Catalog group the type belongs with (informational — not in the Quick Pick). */
+ readonly group: string;
+ /** The input boxes, in order. Cancelling any one aborts the whole command. */
+ readonly steps: readonly PromptStep[];
+ /** Draw one fresh value from validated params. Called once per generate();
+ * `opts` carries the per-call settings the pipeline threads in (dateFormat).
+ * Exactly one of render/createRender is defined (a spec test enforces it). */
+ render?(params: Readonly>, opts?: GenerateOptions): string;
+ /** Stateful alternative to {@link render}: called once per insert operation
+ * (inside {@link toGenerator}) to build the drawing closure, so consecutive
+ * generate() calls can share state within that one insert — Sequence advances
+ * its counter per cursor/bulk item — while every new insert starts fresh. */
+ createRender?(params: Readonly>): (opts?: GenerateOptions) => string;
+}
+
+/** Parse a safe integer out of raw input-box text; `undefined` when it isn't one. */
+function parseInteger(input: string): number | undefined {
+ const trimmed = input.trim();
+ if (trimmed === '') { return undefined; }
+ const value = Number(trimmed);
+ return Number.isSafeInteger(value) ? value : undefined;
+}
+
+/** Parse a finite number out of raw input-box text; `undefined` when it isn't one. */
+function parseNumber(input: string): number | undefined {
+ const trimmed = input.trim();
+ if (trimmed === '') { return undefined; }
+ const value = Number(trimmed);
+ return Number.isFinite(value) ? value : undefined;
+}
+
+const DATE_INPUT = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d{1,3})?)?(Z|[+-]\d{2}:\d{2})?)?$/;
+
+/** Parse `YYYY-MM-DD` or full ISO 8601 out of raw input-box text; `undefined` when
+ * it isn't one. The shape gate keeps `new Date`'s looser parses (e.g. '5') out.
+ * The calendar check is explicit because V8 rolls an impossible day over
+ * (2026-02-31 → March 3) instead of rejecting it; a NaN check alone misses that. */
+function parseDate(input: string): Date | undefined {
+ const trimmed = input.trim();
+ if (!DATE_INPUT.test(trimmed)) { return undefined; }
+ const [ year, month, day ] = trimmed.slice(0, 10).split('-').map(Number);
+ // Date.UTC(year, month, 0) is the last day of `month` (1-based here, 0-based in Date.UTC).
+ if (month < 1 || month > 12 || day < 1 || day > new Date(Date.UTC(year, month, 0)).getUTCDate()) { return undefined; }
+ const value = new Date(trimmed);
+ return Number.isNaN(value.getTime()) ? undefined : value;
+}
+
+const INTEGER_ERROR = 'Enter a whole number (e.g. 42).';
+const NUMBER_ERROR = 'Enter a number (e.g. 0.5).';
+const DATE_ERROR = 'Enter a date as YYYY-MM-DD or full ISO 8601 (e.g. 2026-07-02 or 2026-07-02T12:00:00Z).';
+
+function maxAtLeastMin(min: string): string {
+ return `Max must be at least the min you entered (${min}).`;
+}
+
+const COUNT_ERROR = 'Enter a whole number between 1 and 100.';
+
+/** The single count box shared by the lorem trio (words/sentences/paragraphs). */
+function countStep(prompt: string): InputStep {
+ return {
+ key: 'count',
+ prompt,
+ placeholder: 'e.g. 3',
+ fallback: '3',
+ validate: (input) => {
+ const value = parseInteger(input);
+ return value !== undefined && value >= 1 && value <= 100 ? undefined : COUNT_ERROR;
+ },
+ };
+}
+
+/**
+ * UUID post-transform for the uuidFormat command: a pure re-rendering of faker's
+ * lowercase-dashed uuid string. Unknown formats fall through unchanged.
+ */
+export function formatUuid(uuid: string, format: string): string {
+ switch (format) {
+ case 'uppercase': return uuid.toUpperCase();
+ case 'braced': return `{${uuid}}`;
+ case 'noDashes': return uuid.replace(/-/g, '');
+ case 'uppercaseNoDashes': return uuid.replace(/-/g, '').toUpperCase();
+ default: return uuid; // 'lowercase' — faker's native rendering.
+ }
+}
+
+/** Render a mustache template — faker's `helpers.fake` — for From Template…. */
+function renderTemplate(template: string): string {
+ return faker().helpers.fake(template);
+}
+
+/** Render a regex-subset pattern — faker's `helpers.fromRegExp` — for From Pattern…. */
+function renderPattern(pattern: string): string {
+ return faker().helpers.fromRegExp(pattern);
+}
+
+const TEMPLATE_EXAMPLE = '{{person.firstName}} <{{internet.email}}>';
+const PATTERN_EXAMPLE = '[A-Z]{3}-[0-9]{4}';
+
+/**
+ * Shared validate for the free-form template/pattern boxes: the only authority
+ * on whether such input renders is faker itself, so prove it with one test
+ * render and surface faker's error plus a working example when it throws.
+ * A render's structure is deterministic (only leaf values vary), so one success
+ * here means insert-time renders cannot throw. Empty input needs the explicit
+ * reject because faker renders '' to '' without complaint.
+ */
+function validateByRendering(
+ input: string,
+ noun: string,
+ example: string,
+ render: (value: string) => string,
+): string | undefined {
+ const trimmed = input.trim();
+ if (trimmed === '') { return `Enter a ${noun} — e.g. ${example}`; }
+ try {
+ render(trimmed);
+ return undefined;
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return `${message} — a working example: ${example}`;
+ }
+}
+
+/**
+ * The prompted-command registry. Fallbacks reproduce the matching zero-argument
+ * catalog types (Number: 0–1000, Float: 0–1000 at 2 decimals, String: 15
+ * alphanumeric chars, Password: 15 chars, and the lowercase / no-symbols / human
+ * pick defaults), so accepting the prefills behaves like the plain command.
+ * The lorem counts prefill 3 — faker's own words/paragraphs default — and the
+ * template/pattern boxes prefill their documented examples.
+ */
+export const promptedCommands: readonly PromptedCommand[] = [
+ {
+ id: 'numberRange',
+ label: 'Number (Range…)',
+ group: 'Numbers',
+ steps: [
+ {
+ key: 'min',
+ prompt: 'Number range — the smallest value to draw (whole number).',
+ placeholder: 'e.g. 0',
+ fallback: '0',
+ validate: (input) => (parseInteger(input) === undefined ? INTEGER_ERROR : undefined),
+ },
+ {
+ key: 'max',
+ prompt: 'Number range — the largest value to draw (whole number).',
+ placeholder: 'e.g. 1000',
+ fallback: '1000',
+ validate: (input, prior) => {
+ const value = parseInteger(input);
+ if (value === undefined) { return INTEGER_ERROR; }
+ return value < Number(prior.min) ? maxAtLeastMin(prior.min) : undefined;
+ },
+ },
+ ],
+ render: ({ min, max }) => faker().number.int({ min: Number(min), max: Number(max) }).toString(),
+ },
+ {
+ id: 'floatRange',
+ label: 'Float (Range…)',
+ group: 'Numbers',
+ steps: [
+ {
+ key: 'min',
+ prompt: 'Float range — the smallest value to draw.',
+ placeholder: 'e.g. 0',
+ fallback: '0',
+ validate: (input) => (parseNumber(input) === undefined ? NUMBER_ERROR : undefined),
+ },
+ {
+ key: 'max',
+ prompt: 'Float range — the largest value to draw (rendered with 2 decimals).',
+ placeholder: 'e.g. 1000',
+ fallback: '1000',
+ validate: (input, prior) => {
+ const value = parseNumber(input);
+ if (value === undefined) { return NUMBER_ERROR; }
+ const min = Number(prior.min);
+ if (!Number.isFinite(min)) { return undefined; }
+ if (value < min) { return maxAtLeastMin(prior.min); }
+ // The draw is int(ceil(min·100) … floor(max·100)) / 100 — faker throws
+ // when that integer range is empty, so reject exactly those inputs.
+ if (Math.ceil(min * 100) > Math.floor(value * 100)) {
+ return 'Range too narrow — it must contain a multiple of 0.01 (output has 2 decimals).';
+ }
+ return undefined;
+ },
+ },
+ ],
+ render: ({ min, max }) =>
+ faker().number.float({ min: Number(min), max: Number(max), fractionDigits: 2 }).toFixed(2),
+ },
+ {
+ id: 'stringLength',
+ label: 'String (Length…)',
+ group: 'Text',
+ steps: [
+ {
+ key: 'length',
+ prompt: 'String length — how many alphanumeric characters (1–1000).',
+ placeholder: 'e.g. 15',
+ fallback: '15',
+ validate: (input) => {
+ const value = parseInteger(input);
+ return value !== undefined && value >= 1 && value <= 1000
+ ? undefined
+ : 'Enter a whole number between 1 and 1000.';
+ },
+ },
+ ],
+ render: ({ length }) => faker().string.alphanumeric(Number(length)),
+ },
+ {
+ id: 'dateBetween',
+ label: 'Date (Between…)',
+ group: 'Time',
+ steps: [
+ {
+ key: 'from',
+ prompt: 'Date range — the earliest date (YYYY-MM-DD or full ISO 8601).',
+ placeholder: 'e.g. 2020-01-01',
+ fallback: '2020-01-01',
+ validate: (input) => (parseDate(input) === undefined ? DATE_ERROR : undefined),
+ },
+ {
+ key: 'to',
+ prompt: 'Date range — the latest date (YYYY-MM-DD or full ISO 8601).',
+ placeholder: 'e.g. 2030-12-31',
+ fallback: '2030-12-31',
+ validate: (input, prior) => {
+ const value = parseDate(input);
+ if (value === undefined) { return DATE_ERROR; }
+ const from = parseDate(prior.from ?? '');
+ if (from === undefined) { return undefined; }
+ return value.getTime() < from.getTime()
+ ? `To must be on or after the from date you entered (${prior.from}).`
+ : undefined;
+ },
+ },
+ ],
+ // Rendered per the dateFormat setting, like the zero-argument Time generators.
+ render: ({ from, to }, opts) => formatTimestamp(faker().date.between({ from, to }), opts?.dateFormat),
+ },
+ {
+ id: 'wordsCount',
+ label: 'Words (Count…)',
+ group: 'Text',
+ steps: [ countStep('Words — how many lorem words to insert (1–100).') ],
+ render: ({ count }) => faker().lorem.words(Number(count)),
+ },
+ {
+ id: 'sentencesCount',
+ label: 'Sentences (Count…)',
+ group: 'Text',
+ steps: [ countStep('Sentences — how many lorem sentences to insert (1–100).') ],
+ render: ({ count }) => faker().lorem.sentences(Number(count)),
+ },
+ {
+ id: 'paragraphsCount',
+ label: 'Paragraphs (Count…)',
+ group: 'Text',
+ steps: [ countStep('Paragraphs — how many lorem paragraphs to insert (1–100).') ],
+ render: ({ count }) => faker().lorem.paragraphs(Number(count)),
+ },
+ {
+ id: 'uuidFormat',
+ label: 'UUID (Format…)',
+ group: 'IDs',
+ steps: [
+ {
+ kind: 'pick',
+ key: 'format',
+ prompt: 'UUID format — how the drawn UUID is rendered.',
+ options: [
+ { value: 'lowercase', label: 'Lowercase', detail: 'e.g. 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d' },
+ { value: 'uppercase', label: 'UPPERCASE', detail: 'e.g. 9B1DEB4D-3B7D-4BAD-9BDD-2B0D7B3DCB6D' },
+ { value: 'braced', label: 'Braced', detail: 'e.g. {9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d}' },
+ { value: 'noDashes', label: 'No dashes', detail: 'e.g. 9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d' },
+ { value: 'uppercaseNoDashes', label: 'UPPERCASE, no dashes', detail: 'e.g. 9B1DEB4D3B7D4BAD9BDD2B0D7B3DCB6D' },
+ ],
+ fallback: 'lowercase',
+ },
+ ],
+ render: ({ format }) => formatUuid(faker().string.uuid(), format),
+ },
+ {
+ id: 'passwordOptions',
+ label: 'Password (Options…)',
+ group: 'Security',
+ steps: [
+ {
+ key: 'length',
+ prompt: 'Password length — how many characters (8–128).',
+ placeholder: 'e.g. 15',
+ fallback: '15',
+ validate: (input) => {
+ const value = parseInteger(input);
+ return value !== undefined && value >= 8 && value <= 128
+ ? undefined
+ : 'Enter a whole number between 8 and 128.';
+ },
+ },
+ {
+ kind: 'pick',
+ key: 'symbols',
+ prompt: 'Password characters — include symbols?',
+ options: [
+ { value: 'no', label: 'No symbols', detail: 'Letters and digits only.' },
+ { value: 'yes', label: 'Include symbols', detail: 'Letters, digits, and !@#$%^&*.' },
+ ],
+ fallback: 'no',
+ },
+ ],
+ render: ({ length, symbols }) =>
+ faker().internet.password({
+ length: Number(length),
+ pattern: symbols === 'yes' ? /[A-Za-z0-9!@#$%^&*]/ : /[A-Za-z0-9]/,
+ }),
+ },
+ {
+ id: 'phoneFormat',
+ label: 'Phone (Format…)',
+ group: 'Identity',
+ steps: [
+ {
+ kind: 'pick',
+ key: 'style',
+ prompt: 'Phone format — which style to insert.',
+ options: [
+ { value: 'human', label: 'Human', detail: 'As people write them, e.g. 555.770.2411 x1234.' },
+ { value: 'national', label: 'National', detail: 'Standardized national format, e.g. (555) 770-2411.' },
+ { value: 'international', label: 'International', detail: 'E.164 format, e.g. +15557702411.' },
+ ],
+ fallback: 'human',
+ },
+ ],
+ render: ({ style }) => faker().phone.number({ style: style as 'human' | 'national' | 'international' }),
+ },
+ {
+ id: 'fromTemplate',
+ label: 'From Template…',
+ group: 'Custom',
+ steps: [
+ {
+ key: 'template',
+ prompt: 'Template — text with {{module.method}} placeholders; every cursor and bulk item re-renders with fresh values.',
+ placeholder: `e.g. ${TEMPLATE_EXAMPLE}`,
+ fallback: TEMPLATE_EXAMPLE,
+ validate: (input) => validateByRendering(input, 'template', TEMPLATE_EXAMPLE, renderTemplate),
+ },
+ ],
+ render: ({ template }) => renderTemplate(template),
+ },
+ {
+ id: 'fromPattern',
+ label: 'From Pattern…',
+ group: 'Custom',
+ steps: [
+ {
+ key: 'pattern',
+ prompt: 'Pattern — a fresh string is drawn to match it at every cursor and bulk item.',
+ placeholder: `e.g. ${PATTERN_EXAMPLE} — faker supports a limited regex subset (classes, ranges, quantifiers)`,
+ fallback: PATTERN_EXAMPLE,
+ validate: (input) => validateByRendering(input, 'pattern', PATTERN_EXAMPLE, renderPattern),
+ },
+ ],
+ render: ({ pattern }) => renderPattern(pattern),
+ },
+ {
+ id: 'sequence',
+ label: 'Sequence (Start/Step…)',
+ group: 'Numbers',
+ steps: [
+ {
+ key: 'start',
+ prompt: 'Sequence — the first value (whole number).',
+ placeholder: 'e.g. 1',
+ fallback: '1',
+ validate: (input) => (parseInteger(input) === undefined ? INTEGER_ERROR : undefined),
+ },
+ {
+ key: 'step',
+ prompt: 'Sequence — how much each next value adds (whole number; negative counts down).',
+ placeholder: 'e.g. 1',
+ fallback: '1',
+ validate: (input) => (parseInteger(input) === undefined ? INTEGER_ERROR : undefined),
+ },
+ ],
+ // Not random at all — the one prompted command whose values must RELATE
+ // across the cursors/bulk items of an insert: createRender builds one
+ // counter per insert operation (1, 2, 3… down a column), and the next
+ // insert restarts at start.
+ createRender: ({ start, step }) => {
+ let next = Number(start);
+ return () => {
+ const value = next;
+ next += Number(step);
+ return String(value);
+ };
+ },
+ },
+];
+
+/** Look a prompted command up by id; undefined when the id is unknown. */
+export function getPromptedCommand(id: string): PromptedCommand | undefined {
+ return promptedCommands.find((command) => command.id === id);
+}
+
+/**
+ * Wrap a completed prompt flow as a one-off {@link Generator} — the same
+ * contract the catalog entries satisfy, so it feeds straight into the normal
+ * insert path. `generate()` re-renders each call: a fresh draw per cursor and
+ * per bulk item, seeded through the shared faker accessor.
+ */
+export function toGenerator(command: PromptedCommand, params: Readonly>): Generator {
+ // A createRender command builds its closure HERE — once per insert operation —
+ // so state (Sequence's counter) spans the cursors/bulk items of one insert
+ // and resets on the next.
+ const draw = command.createRender
+ ? command.createRender(params)
+ : (opts?: GenerateOptions) => command.render!(params, opts);
+ return {
+ id: command.id,
+ label: command.label,
+ group: command.group,
+ generate: (opts) => draw(opts),
+ };
+}
diff --git a/src/quotePolicy.ts b/src/quotePolicy.ts
new file mode 100644
index 0000000..22907fe
--- /dev/null
+++ b/src/quotePolicy.ts
@@ -0,0 +1,59 @@
+/**
+ * Automatic, language-aware quote policy: resolve the quote character + escape
+ * style that makes an inserted value land as *valid syntax for the file's
+ * language*. Pure by design — no `vscode` import — so it can be exercised in
+ * isolation.
+ *
+ * The generated value is universal; only the **wrapping** is language-specific.
+ * Quoting is automatic and unconditional (there is no user quote-style setting),
+ * and the rules collapse into just two cases:
+ *
+ * - **SQL family** — string literals are single-quoted, and an embedded `'` is
+ * escaped by *doubling* it (`''`), not with a backslash.
+ * - **Everything else** — JS/TS, Python, JSON, Go, Java, Rust, … and every
+ * unlisted language (and the no-editor case) — uses double quotes with
+ * backslash escaping. Double is a valid string literal in every mainstream
+ * language (and the only legal quote in JSON / Go / Java / Rust / …), and it
+ * leaves apostrophe-heavy values (`O'Brien`, contractions) unescaped.
+ */
+
+/** How the quote character is escaped when it appears inside a value. */
+export type EscapeStyle = 'backslash' | 'sqlDouble';
+
+/** The effective quote + escape resolved for a file's language. */
+export interface QuotePolicy {
+ /** The quote character to wrap with (`''` = no wrapping). */
+ quote: string;
+ /** How to escape the quote character inside the value. */
+ escape: EscapeStyle;
+}
+
+/**
+ * SQL dialects: string literals are single-quoted, but an embedded `'` is
+ * escaped by doubling it (`''`) rather than backslashing it.
+ */
+const SQL_FAMILY: ReadonlySet = new Set([
+ 'sql', 'mysql', 'pgsql', 'plsql', 'sqlite',
+]);
+
+/**
+ * Resolve the effective quote + escape for a file's language.
+ *
+ * `languageId` is `undefined` when there is no active editor (e.g. a clipboard
+ * insert with nothing focused); that falls through to the double-quote default.
+ */
+export function resolveQuotePolicy(
+ languageId: string | undefined,
+ opts: { withQuote: boolean },
+): QuotePolicy {
+ // No wrapping at all.
+ if (!opts.withQuote) {
+ return { quote: '', escape: 'backslash' };
+ }
+ // SQL is the one family that needs single quotes + doubling to stay valid.
+ if (languageId !== undefined && SQL_FAMILY.has(languageId)) {
+ return { quote: "'", escape: 'sqlDouble' };
+ }
+ // Everything else — double quotes are valid everywhere and apostrophe-clean.
+ return { quote: '"', escape: 'backslash' };
+}
diff --git a/src/randomize.ts b/src/randomize.ts
new file mode 100644
index 0000000..e9c56b3
--- /dev/null
+++ b/src/randomize.ts
@@ -0,0 +1,94 @@
+/**
+ * The pure halves of "Insert Random: Randomize Selection" (anonymize in place).
+ *
+ * `detect(text)` — type-aware replace: a selection that IS an unambiguous
+ * email / UUID / ISO date / ISO timestamp is upgraded to a fresh realistic fake
+ * of the same type (the extension draws it through faker); `shapeUuid` /
+ * `shapeTimestamp` dress that fresh draw like the original (case, braces,
+ * millisecond precision).
+ *
+ * `randomize(text, rng)` — the format-preserving fallback for everything else:
+ * every digit becomes a random digit, every a–z letter a random lowercase
+ * letter, every A–Z letter a random uppercase letter; everything else
+ * (punctuation, whitespace, non-ASCII) passes through unchanged. So "3.14" →
+ * "8.77" — the shape survives, the content doesn't. Numbers are deliberately
+ * NOT detected: this fallback already yields a fresh same-shape number.
+ */
+
+/** Returns an integer in `[0, bound)`. Injected so the module stays `vscode`-free
+ * and the caller picks the randomness source — the extension wraps the shared
+ * seeded faker instance, tests script it. */
+export type Rng = (bound: number) => number;
+
+const DIGITS = '0123456789';
+const LOWER = 'abcdefghijklmnopqrstuvwxyz';
+const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+/** A selection value type that gets a typed redraw instead of a scramble. */
+export type DetectedType = 'email' | 'uuid' | 'isoDate' | 'isoTimestamp';
+
+const EMAIL = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
+const UUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
+const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
+const ISO_TIMESTAMP = /^(\d{4}-\d{2}-\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{1,3})?Z$/;
+
+/** True when `YYYY-MM-DD` names a real calendar day — V8's Date would roll
+ * 2026-02-31 over to March instead of rejecting it, so the check is explicit. */
+function isCalendarDate(date: string): boolean {
+ const [ year, month, day ] = date.split('-').map(Number);
+ return month >= 1 && month <= 12 && day >= 1 && day <= new Date(Date.UTC(year, month, 0)).getUTCDate();
+}
+
+/**
+ * Detect whether a selection IS one of the typed values, whole-string and
+ * unpadded — deliberately conservative, because a false positive replaces the
+ * user's text with the wrong kind of value while the scramble is always safe.
+ * Anything not matched exactly returns undefined and falls back to
+ * {@link randomize}.
+ */
+export function detect(text: string): DetectedType | undefined {
+ if (EMAIL.test(text)) { return 'email'; }
+ const core = text.startsWith('{') && text.endsWith('}') ? text.slice(1, -1) : text;
+ if (UUID.test(core)) { return 'uuid'; }
+ if (ISO_DATE.test(text)) { return isCalendarDate(text) ? 'isoDate' : undefined; }
+ const stamp = ISO_TIMESTAMP.exec(text);
+ if (stamp) {
+ const [ , date, hours, minutes, seconds ] = stamp;
+ const timeValid = Number(hours) <= 23 && Number(minutes) <= 59 && Number(seconds) <= 59;
+ return timeValid && isCalendarDate(date) ? 'isoTimestamp' : undefined;
+ }
+ return undefined;
+}
+
+/** Dress a fresh lowercase-dashed uuid like the original selection: keep its
+ * braces, and go uppercase when the original's hex letters were uniformly
+ * uppercase (no letters, or mixed case, stays lowercase). */
+export function shapeUuid(fresh: string, original: string): string {
+ const uppercase = /[A-F]/.test(original) && !/[a-f]/.test(original);
+ const cased = uppercase ? fresh.toUpperCase() : fresh;
+ return original.startsWith('{') && original.endsWith('}') ? `{${cased}}` : cased;
+}
+
+/** Match a fresh `toISOString()` rendering to the original's millisecond
+ * precision: strip the `.mmm` when the original carried none. */
+export function shapeTimestamp(freshIso: string, original: string): string {
+ return original.includes('.') ? freshIso : freshIso.replace(/\.\d{3}Z$/, 'Z');
+}
+
+/** Randomize `text` per character, one fresh `rng` draw per digit or ASCII letter.
+ * Iterates code points (`for…of`), so surrogate pairs are never split. */
+export function randomize(text: string, rng: Rng): string {
+ let result = '';
+ for (const char of text) {
+ if (char >= '0' && char <= '9') {
+ result += DIGITS[rng(10)];
+ } else if (char >= 'a' && char <= 'z') {
+ result += LOWER[rng(26)];
+ } else if (char >= 'A' && char <= 'Z') {
+ result += UPPER[rng(26)];
+ } else {
+ result += char;
+ }
+ }
+ return result;
+}
diff --git a/src/record.ts b/src/record.ts
new file mode 100644
index 0000000..6f62e77
--- /dev/null
+++ b/src/record.ts
@@ -0,0 +1,98 @@
+/**
+ * Multi-field records: compose selected catalog generators into one structured
+ * record — a JSON object, a SQL INSERT row, or a CSV line — with shape-driven
+ * escaping. Pure (no `vscode` import) so it can be unit-tested in isolation.
+ *
+ * A record's escaping is decided by its **shape**, not by the file's language:
+ * `json` uses `JSON.stringify`, `sql` single-quotes with `''` doubling, `csv`
+ * wraps only the values that need it. `bulkCount` stacks records per shape.
+ * The `dataset` option renders the same records as a standalone file instead
+ * of an at-cursor block: CSV gains a header row, JSON is always an array with
+ * one record per line, and the text ends with a trailing newline.
+ */
+import type { DateFormat, Generator } from './catalog';
+
+/** The structured shape a record renders as. */
+export type RecordShape = 'json' | 'sql' | 'csv';
+
+/** Options for {@link buildRecords}, derived from settings. */
+export interface RecordOptions {
+ /** How many records to emit (>1 stacks per shape). */
+ readonly bulkCount: number;
+ /** Table name for the `sql` shape. */
+ readonly sqlTable: string;
+ /** Timestamp rendering, handed through to each field draw (Time generators read it). */
+ readonly dateFormat?: DateFormat;
+ /** Render as a standalone dataset file rather than an at-cursor block: `csv`
+ * gains a header row of field keys, `json` is always an array (one record per
+ * line), and the text ends with a trailing newline. `sql` needs no change —
+ * its `INSERT` statements already stand alone. */
+ readonly dataset?: boolean;
+}
+
+/** One rendered field within a record. */
+interface Field {
+ readonly key: string;
+ readonly value: string;
+}
+
+/** SQL string-literal escaping: double an embedded single quote (`'` → `''`). */
+function sqlEscape(value: string): string {
+ return value.split("'").join("''");
+}
+
+/** CSV field escaping: wrap in `"` and double internal `"` when the value
+ * contains a comma, quote, CR, or LF; otherwise return it unchanged. */
+function csvEscape(value: string): string {
+ return /[",\n\r]/.test(value) ? `"${value.split('"').join('""')}"` : value;
+}
+
+function renderJson(records: Field[][], dataset: boolean): string {
+ const objects = records.map((rec) => {
+ const body = rec.map((f) => `${JSON.stringify(f.key)}: ${JSON.stringify(f.value)}`).join(', ');
+ return `{ ${body} }`;
+ });
+ // A dataset file is always an array — its consumer iterates it — with one
+ // record per line, so a 100k-row file stays scrollable (the editor's
+ // tokenizer gives up on a single multi-megabyte line).
+ if (dataset) { return `[\n${objects.map((object) => ` ${object}`).join(',\n')}\n]`; }
+ return objects.length === 1 ? objects[0] : `[ ${objects.join(', ')} ]`;
+}
+
+function renderSql(records: Field[][], table: string): string {
+ return records
+ .map((rec) => {
+ const cols = rec.map((f) => f.key).join(', ');
+ const vals = rec.map((f) => `'${sqlEscape(f.value)}'`).join(', ');
+ return `INSERT INTO ${table} (${cols}) VALUES (${vals});`;
+ })
+ .join('\n');
+}
+
+function renderCsv(records: Field[][], header?: readonly string[]): string {
+ const rows = records.map((rec) => rec.map((f) => csvEscape(f.value)).join(','));
+ // Header cells run through csvEscape too — a custom-list field key is a
+ // user-chosen name that may itself contain a comma or quote.
+ if (header) { rows.unshift(header.map((key) => csvEscape(key)).join(',')); }
+ return rows.join('\n');
+}
+
+/**
+ * Build the text for one record insert: `bulkCount` records, each a fresh draw
+ * from every selected generator, rendered in `shape`. Field order follows the
+ * order of `fields` (the caller passes them in catalog order).
+ */
+export function buildRecords(fields: Generator[], shape: RecordShape, opts: RecordOptions): string {
+ const count = Math.max(1, Math.floor(opts.bulkCount || 1));
+ const records: Field[][] = Array.from({ length: count }, () =>
+ fields.map((f) => ({ key: f.id, value: f.generate({ dateFormat: opts.dateFormat }) })),
+ );
+ let body: string;
+ switch (shape) {
+ case 'sql': body = renderSql(records, opts.sqlTable); break;
+ case 'csv': body = renderCsv(records, opts.dataset ? fields.map((f) => f.id) : undefined); break;
+ case 'json':
+ default: body = renderJson(records, opts.dataset === true); break;
+ }
+ return opts.dataset ? `${body}\n` : body;
+}
diff --git a/src/settingsCommands.ts b/src/settingsCommands.ts
new file mode 100644
index 0000000..aeb262c
--- /dev/null
+++ b/src/settingsCommands.ts
@@ -0,0 +1,183 @@
+import * as vscode from 'vscode';
+import { ConfigKey } from './configuration';
+
+/** The context-menu setting isn't part of {@link ConfigKey} (it's consumed by a
+ * package.json `when` clause, not read in code) — declared here for its toggle. */
+const CONTEXT_MENU_KEY = 'insertRandomText.contextMenu.enabled';
+
+/** One selectable option for an enum setting; `detail` mirrors the matching
+ * `enumDescriptions` entry in package.json. */
+interface EnumOption {
+ readonly value: string;
+ readonly label: string;
+ readonly detail: string;
+}
+
+/**
+ * Where a command writes: the open workspace if there is one — so the change is
+ * visible immediately even when a workspace pins the setting (and project tweaks
+ * stay project-scoped) — otherwise the user's global settings.
+ */
+function writeTarget(): vscode.ConfigurationTarget {
+ return (vscode.workspace.workspaceFolders?.length ?? 0) > 0
+ ? vscode.ConfigurationTarget.Workspace
+ : vscode.ConfigurationTarget.Global;
+}
+
+function read(key: string): T | undefined {
+ return vscode.workspace.getConfiguration().get(key);
+}
+
+function write(key: string, value: unknown): Thenable {
+ return vscode.workspace.getConfiguration().update(key, value, writeTarget());
+}
+
+/** Unobtrusive confirmation, matching the clipboard toast style in extension.ts. */
+function confirm(message: string): void {
+ vscode.window.setStatusBarMessage(`$(check) ${message}`, 2500);
+}
+
+/** Quick Pick over an enum setting: the current value floats to the top, marked. */
+async function chooseEnum(title: string, key: string, options: readonly EnumOption[], fallback: string): Promise {
+ const current = read(key) ?? fallback;
+ const items: (vscode.QuickPickItem & { value: string })[] = options.map((option) => ({
+ label: option.label,
+ detail: option.detail,
+ description: option.value === current ? '$(check) Current' : undefined,
+ value: option.value,
+ }));
+ const currentIdx = items.findIndex((item) => item.value === current);
+ if (currentIdx > 0) {
+ const [ item ] = items.splice(currentIdx, 1);
+ items.unshift(item);
+ }
+ const picked = await vscode.window.showQuickPick(items, { placeHolder: title, matchOnDetail: true });
+ if (!picked) { return; }
+ await write(key, picked.value);
+ confirm(`${title} → ${picked.label}`);
+}
+
+/** Flip a boolean setting and report the new state. */
+async function toggleBoolean(key: string, label: string, fallback: boolean): Promise {
+ const next = !(read(key) ?? fallback);
+ await write(key, next);
+ confirm(`${label}: ${next ? 'On' : 'Off'}`);
+}
+
+const INSERT_TYPE_OPTIONS: readonly EnumOption[] = [
+ { value: 'Cursor', label: 'Cursor', detail: 'Fill a fresh value at each cursor.' },
+ { value: 'Top', label: 'Top', detail: 'Insert one block at line 1.' },
+ { value: 'Clipboard', label: 'Clipboard', detail: 'Copy to the clipboard instead of inserting (no editor needed).' },
+];
+
+const OUTPUT_FORMAT_OPTIONS: readonly EnumOption[] = [
+ { value: 'plain', label: 'Plain', detail: 'One value per line.' },
+ { value: 'jsonArray', label: 'JSON array', detail: 'A JSON array, e.g. [ "a", "b" ].' },
+ { value: 'quotedList', label: 'Quoted list', detail: 'A quoted, comma-separated list, e.g. "a", "b".' },
+];
+
+const RECORD_FORMAT_OPTIONS: readonly EnumOption[] = [
+ { value: 'json', label: 'JSON object', detail: '{ "field": "value", … }' },
+ { value: 'sql', label: 'SQL row', detail: 'INSERT INTO table (…) VALUES (…);' },
+ { value: 'csv', label: 'CSV line', detail: 'value,value,…' },
+];
+
+const LOCALE_OPTIONS: readonly EnumOption[] = [
+ { value: 'en', label: 'English', detail: 'Names, addresses & text in English (default).' },
+ { value: 'de', label: 'German — Deutsch', detail: 'German names, addresses & text.' },
+ { value: 'fr', label: 'French — Français', detail: 'French names, addresses & text.' },
+ { value: 'es', label: 'Spanish — Español', detail: 'Spanish names, addresses & text.' },
+ { value: 'pt_BR', label: 'Brazilian Portuguese — Português (Brasil)', detail: 'Brazilian Portuguese names, addresses & text.' },
+ { value: 'ja', label: 'Japanese — 日本語', detail: 'Japanese names, addresses & text.' },
+];
+
+const DATE_FORMAT_OPTIONS: readonly EnumOption[] = [
+ { value: 'iso', label: 'ISO 8601 timestamp', detail: 'Full timestamp, e.g. 2026-07-02T12:34:56.789Z.' },
+ { value: 'isoDate', label: 'ISO date', detail: 'Date only (YYYY-MM-DD), e.g. 2026-07-02.' },
+ { value: 'isoTime', label: 'ISO time', detail: 'Time only (HH:mm:ss), e.g. 12:34:56.' },
+ { value: 'unixSeconds', label: 'Unix seconds', detail: 'Unix time in seconds, e.g. 1783082096.' },
+ { value: 'unixMillis', label: 'Unix milliseconds', detail: 'Unix time in milliseconds, e.g. 1783082096123.' },
+];
+
+async function setBulkCount(): Promise {
+ const current = read(ConfigKey.BULK_COUNT) ?? 1;
+ const input = await vscode.window.showInputBox({
+ prompt: 'Bulk count — how many values to insert at each cursor (1–1000).',
+ value: String(current),
+ validateInput: (raw) => {
+ const n = Number(raw);
+ return Number.isInteger(n) && n >= 1 && n <= 1000 ? undefined : 'Enter a whole number between 1 and 1000.';
+ },
+ });
+ if (input === undefined) { return; }
+ await write(ConfigKey.BULK_COUNT, Number(input));
+ confirm(`Bulk count → ${Number(input)}`);
+}
+
+async function setSeed(): Promise {
+ const current = read(ConfigKey.SEED) ?? '';
+ const input = await vscode.window.showInputBox({
+ prompt: 'Seed — a number for reproducible output. Leave blank for random.',
+ value: current,
+ validateInput: (raw) => (raw.trim() === '' || !Number.isNaN(Number(raw)) ? undefined : 'Enter a number, or leave blank for random.'),
+ });
+ if (input === undefined) { return; }
+ const seed = input.trim();
+ await write(ConfigKey.SEED, seed);
+ confirm(seed === '' ? 'Seed cleared (random)' : `Seed → ${seed}`);
+}
+
+async function setRecordSqlTable(): Promise {
+ const current = read(ConfigKey.RECORD_SQL_TABLE) ?? 'table';
+ const input = await vscode.window.showInputBox({
+ prompt: 'Record SQL table — the table name used by the SQL record shape.',
+ value: current,
+ validateInput: (raw) => (raw.trim() === '' ? 'Enter a table name.' : undefined),
+ });
+ if (input === undefined) { return; }
+ await write(ConfigKey.RECORD_SQL_TABLE, input.trim());
+ confirm(`Record SQL table → ${input.trim()}`);
+}
+
+/** Open the Settings UI filtered to one key — the whole "manage" surface for the
+ * settings-defined templates and custom lists (no bespoke editor UI). */
+async function openSettingsAt(key: string): Promise {
+ await vscode.commands.executeCommand('workbench.action.openSettings', key);
+}
+
+// Saved templates and custom lists are user-authored content, not behavior tuning —
+// a reset must never delete them (they stay editable via the Manage commands).
+const RESET_KEEPS: readonly string[] = [ ConfigKey.TEMPLATES, ConfigKey.CUSTOM_LISTS ];
+
+async function resetSettings(): Promise {
+ const choice = await vscode.window.showWarningMessage(
+ 'Reset all Insert Random settings to their defaults? Saved templates and custom lists are kept.',
+ { modal: true },
+ 'Reset',
+ );
+ if (choice !== 'Reset') { return; }
+ for (const key of [ ...Object.values(ConfigKey), CONTEXT_MENU_KEY ].filter((key) => !RESET_KEEPS.includes(key))) {
+ await write(key, undefined);
+ }
+ confirm('Insert Random settings reset to defaults');
+}
+
+/** Command id → handler for every settings command. Registered in extension.ts. */
+export const SETTING_COMMANDS: Readonly Promise>> = {
+ 'insertRandomText.setInsertType': () => chooseEnum('Insert type', ConfigKey.INSERT_TYPE, INSERT_TYPE_OPTIONS, 'Cursor'),
+ 'insertRandomText.setOutputFormat': () => chooseEnum('Output format', ConfigKey.OUTPUT_FORMAT, OUTPUT_FORMAT_OPTIONS, 'plain'),
+ 'insertRandomText.setDateFormat': () => chooseEnum('Date format', ConfigKey.DATE_FORMAT, DATE_FORMAT_OPTIONS, 'iso'),
+ 'insertRandomText.setRecordFormat': () => chooseEnum('Record format', ConfigKey.RECORD_FORMAT, RECORD_FORMAT_OPTIONS, 'json'),
+ 'insertRandomText.setRecordSqlTable': setRecordSqlTable,
+ 'insertRandomText.setBulkCount': setBulkCount,
+ 'insertRandomText.setSeed': setSeed,
+ 'insertRandomText.setLocale': () => chooseEnum('Locale', ConfigKey.LOCALE, LOCALE_OPTIONS, 'en'),
+ 'insertRandomText.toggleQuotes': () => toggleBoolean(ConfigKey.WITH_QUOTE, 'Wrap with quotes', true),
+ 'insertRandomText.toggleNewLine': () => toggleBoolean(ConfigKey.WITH_NEW_LINE, 'Trailing new line', true),
+ 'insertRandomText.toggleUniquePerCursor': () => toggleBoolean(ConfigKey.UNIQUE_PER_CURSOR, 'Unique value per cursor', true),
+ 'insertRandomText.toggleStrictUnique': () => toggleBoolean(ConfigKey.STRICT_UNIQUE, 'Strict unique', false),
+ 'insertRandomText.toggleContextMenu': () => toggleBoolean(CONTEXT_MENU_KEY, 'Editor context menu', false),
+ 'insertRandomText.manageTemplates': () => openSettingsAt(ConfigKey.TEMPLATES),
+ 'insertRandomText.manageCustomLists': () => openSettingsAt(ConfigKey.CUSTOM_LISTS),
+ 'insertRandomText.resetSettings': resetSettings,
+};
diff --git a/src/test/catalog/date-format.test.ts b/src/test/catalog/date-format.test.ts
new file mode 100644
index 0000000..0ac91a5
--- /dev/null
+++ b/src/test/catalog/date-format.test.ts
@@ -0,0 +1,115 @@
+import * as assert from 'assert';
+
+import { formatTimestamp, generators, getGenerator } from '../../catalog';
+import { load, seed } from '../../engine';
+
+// The dateFormat rendering seam: the timestamp-emitting Time generators accept
+// generate({ dateFormat }) and render their drawn Date accordingly; weekday/month (and every other
+// generator) ignore the option. formatTimestamp is the pure core — every ISO slice comes from the UTC
+// toISOString(), never local-time getters, so a seeded run renders the same text on any machine.
+
+// A fixed instant keeps the shape expectations exact: 2026-07-02T03:04:05.678Z.
+const INSTANT = new Date(Date.UTC(2026, 6, 2, 3, 4, 5, 678));
+
+describe('formatTimestamp — one Date, five renderings', () => {
+ it("'iso' renders the full ISO 8601 timestamp", () => {
+ assert.strictEqual(formatTimestamp(INSTANT, 'iso'), '2026-07-02T03:04:05.678Z');
+ });
+
+ it("'isoDate' renders YYYY-MM-DD", () => {
+ assert.strictEqual(formatTimestamp(INSTANT, 'isoDate'), '2026-07-02');
+ });
+
+ it("'isoTime' renders HH:mm:ss", () => {
+ assert.strictEqual(formatTimestamp(INSTANT, 'isoTime'), '03:04:05');
+ });
+
+ it("'unixSeconds' renders whole seconds since the epoch", () => {
+ assert.strictEqual(formatTimestamp(INSTANT, 'unixSeconds'), String(Math.floor(INSTANT.getTime() / 1000)));
+ });
+
+ it("'unixMillis' renders milliseconds since the epoch", () => {
+ assert.strictEqual(formatTimestamp(INSTANT, 'unixMillis'), String(INSTANT.getTime()));
+ });
+
+ it('defaults to the full ISO rendering when no format is given', () => {
+ assert.strictEqual(formatTimestamp(INSTANT), INSTANT.toISOString());
+ });
+
+ it("'unixSeconds' floors pre-1970 instants toward -∞ (birthdate can draw them)", () => {
+ // 1.5 s before the epoch is second -2 (POSIX floor semantics), not -1 (truncation).
+ assert.strictEqual(formatTimestamp(new Date(-1500), 'unixSeconds'), '-2');
+ });
+});
+
+const TIMESTAMP_GENERATORS = [ 'date', 'pastDate', 'futureDate', 'recentDate', 'soonDate', 'birthdate' ] as const;
+
+describe('Time generators — generate({ dateFormat })', function () {
+ this.timeout(15000);
+
+ before(async () => {
+ await load();
+ });
+
+ it('every timestamp generator exists in the catalog', () => {
+ for (const id of TIMESTAMP_GENERATORS) {
+ assert.ok(generators.some((g) => g.id === id), `'${id}' missing from the catalog`);
+ }
+ });
+
+ it('every timestamp generator defaults to full ISO (pre-dateFormat behavior preserved)', () => {
+ seed(20260702);
+ for (const id of TIMESTAMP_GENERATORS) {
+ const value = getGenerator(id)!.generate();
+ assert.match(value, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, `'${id}' without options must stay full ISO`);
+ }
+ });
+
+ it("every timestamp generator honors 'isoDate'", () => {
+ seed(20260702);
+ for (const id of TIMESTAMP_GENERATORS) {
+ assert.match(getGenerator(id)!.generate({ dateFormat: 'isoDate' }), /^\d{4}-\d{2}-\d{2}$/, `'${id}'`);
+ }
+ });
+
+ it("every timestamp generator honors 'isoTime'", () => {
+ seed(20260702);
+ for (const id of TIMESTAMP_GENERATORS) {
+ assert.match(getGenerator(id)!.generate({ dateFormat: 'isoTime' }), /^\d{2}:\d{2}:\d{2}$/, `'${id}'`);
+ }
+ });
+
+ it("every timestamp generator honors 'unixSeconds' and 'unixMillis' (negatives allowed pre-1970)", () => {
+ seed(20260702);
+ for (const id of TIMESTAMP_GENERATORS) {
+ assert.match(getGenerator(id)!.generate({ dateFormat: 'unixSeconds' }), /^-?\d+$/, `'${id}' seconds`);
+ assert.match(getGenerator(id)!.generate({ dateFormat: 'unixMillis' }), /^-?\d+$/, `'${id}' millis`);
+ }
+ });
+
+ it('the same seed renders the same instant across formats (format is presentation only)', () => {
+ const date = getGenerator('date')!;
+ seed(7);
+ const iso = date.generate({ dateFormat: 'iso' });
+ seed(7);
+ const millis = date.generate({ dateFormat: 'unixMillis' });
+ // The two draws share a seed but not the wall clock: faker's date generators
+ // window around a refDate defaulting to *now*, so back-to-back draws can sit
+ // a few ms apart (this exact-equality assert flaked). Equality up to that
+ // jitter still pins the threading — a real format bug (e.g. seconds instead
+ // of millis) is off by orders of magnitude, not milliseconds.
+ const delta = Math.abs(new Date(iso).getTime() - Number(millis));
+ assert.ok(delta <= 1000, `iso and unixMillis should render the same drawn instant (delta ${delta}ms)`);
+ });
+
+ it('weekday and month ignore dateFormat (they are names, not timestamps)', () => {
+ for (const id of [ 'weekday', 'month' ] as const) {
+ const generator = getGenerator(id)!;
+ seed(11);
+ const plain = generator.generate();
+ seed(11);
+ const formatted = generator.generate({ dateFormat: 'unixSeconds' });
+ assert.strictEqual(formatted, plain, `'${id}' must ignore dateFormat`);
+ }
+ });
+});
diff --git a/src/test/catalog/generate.test.ts b/src/test/catalog/generate.test.ts
new file mode 100644
index 0000000..a283c45
--- /dev/null
+++ b/src/test/catalog/generate.test.ts
@@ -0,0 +1,41 @@
+import * as assert from 'assert';
+
+import { generators, getGenerator } from '../../catalog';
+import { load, seed } from '../../engine';
+
+// Behavioral coverage for the whole registry: every generator must actually produce output. faker is
+// loaded once up front (it's vscode-free, so this runs under plain node too). The non-empty check is
+// data-driven, so a newly-added generator is exercised the moment it joins `generators`.
+describe('catalog generators — output', function () {
+ this.timeout(15000);
+
+ before(async () => {
+ await load();
+ });
+
+ it('every generator yields a non-empty string', () => {
+ // Seed up front so a failure is reproducible; the property (a non-empty string) holds for any seed.
+ seed(20260701);
+ for (const g of generators) {
+ const value = g.generate();
+ assert.strictEqual(typeof value, 'string', `'${g.id}' returned a non-string`);
+ assert.ok(value.length > 0, `'${g.id}' returned an empty string`);
+ }
+ });
+
+ it('the same seed reproduces the same value', () => {
+ const uuid = getGenerator('uuid')!;
+ seed(7);
+ const first = uuid.generate();
+ seed(7);
+ const second = uuid.generate();
+ assert.strictEqual(first, second);
+ });
+
+ it('draws a fresh value on each call — no memoization', () => {
+ seed(1);
+ const uuid = getGenerator('uuid')!;
+ const values = new Set(Array.from({ length: 5 }, () => uuid.generate()));
+ assert.ok(values.size > 1, 'expected distinct values across repeated calls');
+ });
+});
diff --git a/src/test/catalog/registry.test.ts b/src/test/catalog/registry.test.ts
new file mode 100644
index 0000000..6d89780
--- /dev/null
+++ b/src/test/catalog/registry.test.ts
@@ -0,0 +1,42 @@
+import * as assert from 'assert';
+
+import { generators, getGenerator } from '../../catalog';
+
+// The registry is the single source of truth that drives generation, the commands and the Quick Pick, so
+// its shape is load-bearing. No faker here — these are pure structural invariants that hold for every
+// entry, so they auto-cover each generator added to the list.
+
+describe('catalog registry — structure', () => {
+ it('every generator has a non-empty id, label, group and a generate function', () => {
+ for (const g of generators) {
+ assert.ok(typeof g.id === 'string' && g.id.length > 0, `bad id on ${JSON.stringify(g)}`);
+ assert.ok(typeof g.label === 'string' && g.label.length > 0, `bad label on '${g.id}'`);
+ assert.ok(typeof g.group === 'string' && g.group.length > 0, `bad group on '${g.id}'`);
+ assert.strictEqual(typeof g.generate, 'function', `missing generate() on '${g.id}'`);
+ }
+ });
+
+ it('ids are unique', () => {
+ const ids = generators.map((g) => g.id);
+ assert.strictEqual(new Set(ids).size, ids.length, 'duplicate generator id(s) in the registry');
+ });
+
+ it('getGenerator resolves every registered id back to its entry', () => {
+ for (const g of generators) {
+ assert.strictEqual(getGenerator(g.id), g, `getGenerator('${g.id}') did not return its entry`);
+ }
+ });
+
+ it('getGenerator returns undefined for an unknown id', () => {
+ assert.strictEqual(getGenerator('___no_such_generator___'), undefined);
+ });
+
+ it('hidden is set only on the legacy Lorem/Hash size variants', () => {
+ const hidden = generators.filter((g) => g.hidden).map((g) => g.id).sort();
+ assert.deepStrictEqual(
+ hidden,
+ [ 'hashLarge', 'hashMedium', 'hashSmall', 'loremLarge', 'loremMedium', 'loremSmall' ].sort(),
+ 'the hidden set drifted — a new hidden generator appeared, or a size variant lost its hidden flag',
+ );
+ });
+});
diff --git a/src/test/catalog/shapes.test.ts b/src/test/catalog/shapes.test.ts
new file mode 100644
index 0000000..f312a06
--- /dev/null
+++ b/src/test/catalog/shapes.test.ts
@@ -0,0 +1,145 @@
+import * as assert from 'assert';
+
+import { generators, getGenerator } from '../../catalog';
+import { load, seed } from '../../engine';
+
+// Objective output-shape pins for every type whose value has a checkable structure (uuid, ipv4, VIN,
+// two-decimal prices, …). generate.test.ts guarantees non-empty/fresh/seeded for the WHOLE catalog;
+// this table adds the "is it the right KIND of value" layer for the ids that have one. Prose types
+// (Dish, Catch Phrase, …) can't be shape-checked — their plausibility sweep lives in manual-qa.
+type Shape = { ok: (value: string) => boolean; want: string };
+
+const re = (pattern: RegExp, want: string): Shape => ({ ok: (v) => pattern.test(v), want });
+
+const SHAPES: Record = {
+ // Identity
+ email: re(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, 'name@domain.tld'),
+ sex: re(/^(male|female)$/, "'male' or 'female'"),
+
+ // Numbers
+ number: { ok: (v) => /^\d+$/.test(v) && Number(v) <= 1000, want: 'integer 0–1000' },
+ float: { ok: (v) => /^\d+\.\d{2}$/.test(v) && Number(v) <= 1000, want: 'two-decimal number 0–1000' },
+ boolean: re(/^(true|false)$/, "'true' or 'false'"),
+ hexNumber: { ok: (v) => /^[0-9a-f]+$/.test(v) && parseInt(v, 16) <= 0xffffff, want: 'lowercase hex ≤ ffffff' },
+ binary: { ok: (v) => /^[01]+$/.test(v) && parseInt(v, 2) <= 255, want: 'binary digits ≤ 255' },
+ octal: { ok: (v) => /^[0-7]+$/.test(v) && parseInt(v, 8) <= 511, want: 'octal digits ≤ 511' },
+
+ // Text
+ string: re(/^[A-Za-z0-9]{15}$/, '15 alphanumeric chars'),
+ alpha: re(/^[A-Za-z]{10}$/, '10 letters'),
+ numeric: re(/^\d{10}$/, '10 digits'),
+ word: re(/^\S+$/, 'a single token'),
+ words: { ok: (v) => { const n = v.split(' ').length; return n >= 3 && n <= 6; }, want: '3–6 space-separated words' },
+ sentence: re(/^[A-Z].*\.$/, 'capitalized, period-terminated'),
+ slug: re(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'kebab-case'),
+
+ // Time — the six timestamp generators are pinned per-format in date-format.test.ts; names only here.
+ weekday: re(/^[A-Z][a-z]+day$/, 'an English weekday name'),
+ month: re(/^[A-Z][a-z]+$/, 'a capitalized month name'),
+
+ // Location
+ countryCode: re(/^[A-Z]{2}$/, '2-letter code'),
+ stateAbbr: re(/^[A-Z]{2}$/, '2-letter abbreviation'),
+ zipCode: re(/^\d{5}(-\d{4})?$/, 'US zip'),
+ buildingNumber: re(/^\d+$/, 'digits'),
+ latitude: { ok: (v) => Math.abs(Number(v)) <= 90 && v.trim() !== '' && !Number.isNaN(Number(v)), want: 'signed decimal within ±90' },
+ longitude: { ok: (v) => Math.abs(Number(v)) <= 180 && v.trim() !== '' && !Number.isNaN(Number(v)), want: 'signed decimal within ±180' },
+ timeZone: { ok: (v) => v.includes('/') && !v.includes(' '), want: 'IANA zone (Area/City)' },
+ direction: re(/^[A-Za-z]+$/, 'a cardinal/ordinal name'),
+
+ // Network
+ ipv4: { ok: (v) => /^(\d{1,3}\.){3}\d{1,3}$/.test(v) && v.split('.').every((o) => Number(o) <= 255), want: 'dotted quad, octets ≤ 255' },
+ ipv6: re(/^[0-9a-f]{1,4}(:[0-9a-f]{1,4}){7}$/, '8 colon-separated hex groups'),
+ mac: re(/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/, 'colon-separated MAC'),
+ url: re(/^https?:\/\/\S+$/, 'http(s) URL'),
+ domainName: re(/^[^\s/]+\.[^\s/]+$/, 'bare domain'),
+ port: { ok: (v) => /^\d+$/.test(v) && Number(v) <= 65535, want: 'integer ≤ 65535' },
+ protocol: re(/^https?$/, "'http' or 'https'"),
+ httpMethod: re(/^[A-Z]+$/, 'an uppercase HTTP verb'),
+ httpStatus: { ok: (v) => /^\d{3}$/.test(v) && Number(v) >= 100 && Number(v) <= 599, want: '3-digit status 1xx–5xx' },
+ jwt: re(/^[\w-]+\.[\w-]+\.[\w-]+$/, 'three dot-separated base64url segments'),
+
+ // Media
+ imageUrl: re(/^https?:\/\/\S+$/, 'image URL'),
+ avatarUrl: re(/^https?:\/\/\S+$/, 'avatar URL'),
+
+ // Design
+ color: re(/^#[0-9a-f]{6}$/, '#rrggbb'),
+ rgb: re(/^rgb\([\d, ]+\)$/, 'rgb(r, g, b)'),
+ hsl: re(/^hsl\(\d+deg [\d.]+% [\d.]+%\)$/, 'hsl(Ndeg N% N%)'),
+
+ // Security
+ password: re(/^\S{15}$/, '15 non-space chars'),
+
+ // IDs
+ uuid: re(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/, '8-4-4-4-12 hex'),
+ ulid: re(/^[0-9A-HJKMNP-TV-Z]{26}$/, '26 Crockford-base32 chars'),
+ nanoid: re(/^[A-Za-z0-9_-]{21}$/, '21 URL-safe chars'),
+ mongodbObjectId: re(/^[0-9a-f]{24}$/, '24 lowercase hex'),
+ hash: re(/^[0-9a-f]{13}$/, '13 lowercase hex'),
+
+ // Commerce / Finance
+ price: re(/^\d+\.\d{2}$/, 'two-decimal price'),
+ amount: re(/^\d+\.\d{2}$/, 'two-decimal amount'),
+ currencyCode: re(/^[A-Z]{3}$/, 'ISO 4217 code'),
+ creditCardCVV: re(/^\d{3,4}$/, '3–4 digits'),
+ pin: re(/^\d{4}$/, '4 digits'),
+ iban: re(/^[A-Z]{2}\d{2}[A-Z0-9]+$/, 'country-prefixed IBAN'),
+ bic: re(/^[A-Z0-9]{8}([A-Z0-9]{3})?$/, '8 or 11 chars'),
+ accountNumber: re(/^\d+$/, 'digits'),
+ routingNumber: re(/^\d{9}$/, '9 digits'),
+ ethereum: re(/^0x[0-9a-fA-F]{40}$/, '0x + 40 hex'),
+ bitcoin: re(/^(1|3|bc1)[a-zA-Z0-9]{20,}$/, 'legacy/segwit address'),
+
+ // Git / System
+ gitCommitSha: re(/^[0-9a-f]{40}$/, '40 hex'),
+ gitBranch: re(/^\S+$/, 'no spaces'),
+ semver: re(/^\d+\.\d+\.\d+$/, 'x.y.z'),
+ cron: { ok: (v) => { const n = v.split(' ').length; return n === 5 || n === 6; }, want: '5–6 space-separated fields' },
+ mimeType: re(/^[\w.-]+\/[\w.+-]+$/, 'type/subtype'),
+ fileExt: re(/^[a-z0-9]+$/, 'bare extension'),
+
+ // Vehicle / Travel
+ vin: re(/^[A-HJ-NPR-Z0-9]{17}$/, '17-char VIN (no I/O/Q)'),
+ flightNumber: re(/^\d{4}$/, '4 digits (leading zeros kept)'),
+ seat: re(/^\d{1,2}[A-K]$/, 'row + letter, e.g. 23F'),
+
+ // Hidden back-compat sizes — a command is their only entry point, so pin the size contract too.
+ hashSmall: re(/^[0-9a-f]{7}$/, '7 lowercase hex'),
+ hashMedium: re(/^[0-9a-f]{17}$/, '17 lowercase hex'),
+ hashLarge: re(/^[0-9a-f]{27}$/, '27 lowercase hex'),
+ loremSmall: re(/^[A-Z].*\.$/, 'one sentence'),
+ loremMedium: re(/^[A-Z].*\.$/, 'one paragraph'),
+ loremLarge: { ok: (v) => v.split('\n').length === 3, want: '3 newline-separated paragraphs' },
+};
+
+describe('catalog generators — output shapes', function () {
+ this.timeout(20000);
+
+ before(async () => { await load(); });
+
+ it('every shape-table id still exists in the catalog (guards renames)', () => {
+ for (const id of Object.keys(SHAPES)) {
+ assert.ok(getGenerator(id), `shape table entry '${id}' has no catalog generator`);
+ }
+ });
+
+ for (const [ id, shape ] of Object.entries(SHAPES)) {
+ it(`'${id}' draws ${shape.want}`, () => {
+ const generator = getGenerator(id)!;
+ seed(20260702); // independent of table order — every id starts the same sequence.
+ for (let draw = 0; draw < 25; draw++) {
+ const value = generator.generate();
+ assert.ok(shape.ok(value), `'${id}' draw ${draw} → ${JSON.stringify(value)} — expected ${shape.want}`);
+ }
+ });
+ }
+
+ it('every catalog generator is either shape-pinned here or covered by the non-empty guarantee', () => {
+ // Not an assertion that SHAPES is exhaustive — prose types can't be shape-checked. This just keeps
+ // the split visible: if the unpinned list grows past the known prose set, a new structured type
+ // probably belongs in the table above.
+ const unpinned = generators.filter((g) => !SHAPES[g.id]).map((g) => g.id);
+ assert.ok(unpinned.length < generators.length / 2, `most of the catalog is unpinned (${unpinned.length}) — did the shape table rot?`);
+ });
+});
diff --git a/src/test/commands/custom.test.ts b/src/test/commands/custom.test.ts
new file mode 100644
index 0000000..e0dbf3c
--- /dev/null
+++ b/src/test/commands/custom.test.ts
@@ -0,0 +1,216 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { ConfigKey } from '../../configuration';
+import { SETTING_COMMANDS } from '../../settingsCommands';
+
+// Saved templates + custom lists end-to-end: the two settings feed user-defined groups at the TOP of
+// Pick… (and custom lists into Record… as fields), inserting through the normal pipeline. Suite baseline
+// mirrors insert.test.ts: quotes + newline OFF and a pinned seed. The two data-pool settings are set in
+// before() and MUST be cleared in after() — other suites (insert.test.ts) pin the picker's virgin shape.
+const EXTENSION_ID = 'ElecTreeFrying.insert-random-text';
+
+const TEMPLATES = { invoice: 'INV-{{string.numeric(4)}}' };
+const LISTS = { environment: [ 'dev', 'staging', 'production' ] };
+
+async function setConfig(key: string, value: unknown): Promise {
+ const changed = new Promise((resolve) => {
+ const sub = vscode.workspace.onDidChangeConfiguration((event) => {
+ if (event.affectsConfiguration(key)) { sub.dispose(); resolve(); }
+ });
+ setTimeout(() => { sub.dispose(); resolve(); }, 500);
+ });
+ await vscode.workspace.getConfiguration().update(key, value, vscode.ConfigurationTarget.Global);
+ await changed;
+}
+
+async function openDoc(content: string): Promise {
+ const doc = await vscode.workspace.openTextDocument({ content });
+ return vscode.window.showTextDocument(doc);
+}
+
+type Pick = vscode.QuickPickItem & { generatorId?: string; generator?: { id: string } };
+
+/** Run a picker command with showQuickPick stubbed: `choose` sees the resolved items and returns the
+ * pick (or undefined = dismiss). The items are captured for structural assertions. */
+async function runPicker(command: string, choose: (items: Pick[]) => Pick | Pick[] | undefined): Promise {
+ let captured: Pick[] = [];
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => { captured = await items; return choose(captured); };
+ try {
+ await vscode.commands.executeCommand(command);
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ return captured;
+}
+
+const byLabel = (items: Pick[], label: string): Pick => {
+ const item = items.find((i) => i.label === label && !i.kind);
+ assert.ok(item, `no picker entry labeled '${label}' among: ${items.map((i) => i.label).join(', ')}`);
+ return item!;
+};
+
+describe('custom data — saved templates & custom lists through Pick…/Record…', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.WITH_QUOTE, false);
+ await setConfig(ConfigKey.WITH_NEW_LINE, false);
+ await setConfig(ConfigKey.SEED, '2718');
+ await setConfig(ConfigKey.TEMPLATES, TEMPLATES);
+ await setConfig(ConfigKey.CUSTOM_LISTS, LISTS);
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, undefined);
+ await setConfig(ConfigKey.WITH_NEW_LINE, undefined);
+ await setConfig(ConfigKey.SEED, undefined);
+ await setConfig(ConfigKey.TEMPLATES, undefined);
+ await setConfig(ConfigKey.CUSTOM_LISTS, undefined);
+ });
+
+ afterEach(async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ it('Pick… leads with the Templates and Custom Lists groups, before any catalog group', async () => {
+ const items = await runPicker('insertRandomText.pick', () => undefined);
+ const separators = items.filter((i) => i.kind === vscode.QuickPickItemKind.Separator).map((i) => i.label);
+ assert.deepStrictEqual(separators.slice(0, 2), [ 'Templates', 'Custom Lists' ], 'the user groups must lead the picker');
+ assert.strictEqual(items[0].kind, vscode.QuickPickItemKind.Separator, 'the picker must open on the Templates separator');
+ assert.strictEqual(items[1].label, 'invoice', 'the saved template follows its separator');
+ assert.strictEqual(items[1].description, TEMPLATES.invoice, 'the template text is the description (searchable)');
+ });
+
+ it('picking a saved template inserts a fresh rendering through the pipeline', async () => {
+ const editor = await openDoc('');
+ await runPicker('insertRandomText.pick', (items) => byLabel(items, 'invoice'));
+ assert.match(editor.document.getText(), /^INV-\d{4}$/);
+ });
+
+ it('picking a custom list inserts one of its values', async () => {
+ const editor = await openDoc('');
+ await runPicker('insertRandomText.pick', (items) => byLabel(items, 'environment'));
+ assert.ok(LISTS.environment.includes(editor.document.getText()), `expected a list value, got '${editor.document.getText()}'`);
+ });
+
+ it('a saved template fills every cursor with a fresh rendering (multi-cursor)', async () => {
+ const editor = await openDoc('\n'); // two empty lines.
+ editor.selections = [ new vscode.Selection(0, 0, 0, 0), new vscode.Selection(1, 0, 1, 0) ];
+ await runPicker('insertRandomText.pick', (items) => byLabel(items, 'invoice'));
+ const [ line0, line1 ] = editor.document.getText().split('\n');
+ assert.match(line0, /^INV-\d{4}$/);
+ assert.match(line1, /^INV-\d{4}$/);
+ });
+
+ it('a saved template honors bulkCount through the pipeline', async () => {
+ await setConfig(ConfigKey.BULK_COUNT, 3);
+ const editor = await openDoc('');
+ await runPicker('insertRandomText.pick', (items) => byLabel(items, 'invoice'));
+ await setConfig(ConfigKey.BULK_COUNT, undefined);
+ const lines = editor.document.getText().split('\n');
+ assert.strictEqual(lines.length, 3, 'plain bulk 3 → three lines');
+ for (const line of lines) { assert.match(line, /^INV-\d{4}$/); }
+ });
+
+ it('is reproducible under the pinned seed', async () => {
+ const first = await openDoc('');
+ await runPicker('insertRandomText.pick', (items) => byLabel(items, 'invoice'));
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('');
+ await runPicker('insertRandomText.pick', (items) => byLabel(items, 'invoice'));
+ const b = second.document.getText();
+ assert.strictEqual(a, b, 'same seed + same template → same inserted value');
+ });
+
+ it('a template that fails to render shows a friendly error and inserts nothing', async () => {
+ await setConfig(ConfigKey.TEMPLATES, { broken: '{{nope.nothing}}' });
+ const messages: string[] = [];
+ const originalError = vscode.window.showErrorMessage;
+ (vscode.window as any).showErrorMessage = async (message: string) => { messages.push(message); return undefined; };
+ const editor = await openDoc('');
+ try {
+ await assert.doesNotReject(async () => {
+ await runPicker('insertRandomText.pick', (items) => byLabel(items, 'broken'));
+ });
+ } finally {
+ (vscode.window as any).showErrorMessage = originalError;
+ await setConfig(ConfigKey.TEMPLATES, TEMPLATES); // restore the suite baseline.
+ }
+ assert.strictEqual(editor.document.getText(), '', 'a failing template must insert nothing');
+ assert.strictEqual(messages.length, 1, 'exactly one friendly error');
+ assert.match(messages[0], /broken/, 'the error names the template');
+ assert.match(messages[0], /Manage Templates/, 'the error points at the manage command');
+ });
+
+ it('Record… offers custom lists (before the catalog groups) and keys the field by the list name', async () => {
+ await setConfig(ConfigKey.RECORD_FORMAT, undefined); // default: json
+ const editor = await openDoc('');
+ const items = await runPicker('insertRandomText.record', (all) => {
+ const person = all.find((i) => i.generatorId === 'person');
+ assert.ok(person, 'the catalog Full Name field must be offered');
+ return [ byLabel(all, 'environment'), person! ];
+ });
+ const separators = items.filter((i) => i.kind === vscode.QuickPickItemKind.Separator).map((i) => i.label);
+ assert.strictEqual(separators[0], 'Custom Lists', 'custom lists must lead the field picker');
+ assert.ok(!separators.includes('Templates'), 'templates are Pick…-only, never record fields');
+ const parsed = JSON.parse(editor.document.getText());
+ assert.deepStrictEqual(Object.keys(parsed), [ 'environment', 'person' ], 'custom field first, keyed by its name');
+ assert.ok(LISTS.environment.includes(parsed.environment), `the field value must come from the list, got '${parsed.environment}'`);
+ });
+
+ it('Pick… shows no user groups when both settings are empty', async () => {
+ await setConfig(ConfigKey.TEMPLATES, undefined);
+ await setConfig(ConfigKey.CUSTOM_LISTS, undefined);
+ const items = await runPicker('insertRandomText.pick', () => undefined);
+ await setConfig(ConfigKey.TEMPLATES, TEMPLATES); // restore the suite baseline.
+ await setConfig(ConfigKey.CUSTOM_LISTS, LISTS);
+ const separators = items.filter((i) => i.kind === vscode.QuickPickItemKind.Separator).map((i) => i.label);
+ assert.ok(!separators.includes('Templates') && !separators.includes('Custom Lists'), 'empty settings contribute no groups');
+ assert.strictEqual(separators[0], 'Identity', 'the picker falls back to opening on the first catalog group');
+ });
+});
+
+describe('custom data — Manage commands & reset behavior', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ });
+
+ afterEach(async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ it('the Manage commands open Settings without error (registered end-to-end)', async () => {
+ await assert.doesNotReject(async () => { await vscode.commands.executeCommand('insertRandomText.manageTemplates'); });
+ await assert.doesNotReject(async () => { await vscode.commands.executeCommand('insertRandomText.manageCustomLists'); });
+ });
+
+ it('resetSettings keeps saved templates and custom lists (user content, not tuning)', async () => {
+ const setGlobal = (key: string, value: unknown) =>
+ vscode.workspace.getConfiguration().update(key, value, vscode.ConfigurationTarget.Global);
+ await setGlobal(ConfigKey.TEMPLATES, TEMPLATES);
+ await setGlobal(ConfigKey.CUSTOM_LISTS, LISTS);
+ await setGlobal(ConfigKey.BULK_COUNT, 7);
+
+ const original = vscode.window.showWarningMessage;
+ (vscode.window as any).showWarningMessage = async () => 'Reset';
+ try {
+ await SETTING_COMMANDS['insertRandomText.resetSettings']();
+ } finally {
+ (vscode.window as any).showWarningMessage = original;
+ }
+
+ const inspect = (key: string) => vscode.workspace.getConfiguration().inspect(key)?.globalValue;
+ assert.strictEqual(inspect(ConfigKey.BULK_COUNT), undefined, 'a tuning setting must reset');
+ assert.deepStrictEqual(inspect(ConfigKey.TEMPLATES), TEMPLATES, 'saved templates must survive a reset');
+ assert.deepStrictEqual(inspect(ConfigKey.CUSTOM_LISTS), LISTS, 'custom lists must survive a reset');
+
+ await setGlobal(ConfigKey.TEMPLATES, undefined);
+ await setGlobal(ConfigKey.CUSTOM_LISTS, undefined);
+ });
+});
diff --git a/src/test/commands/dataset.test.ts b/src/test/commands/dataset.test.ts
new file mode 100644
index 0000000..3345f9a
--- /dev/null
+++ b/src/test/commands/dataset.test.ts
@@ -0,0 +1,258 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { ConfigKey } from '../../configuration';
+
+// insertRandomText.generateDataset drives the Record… machinery into a FILE: multi-select fields →
+// pick a shape (the recordFormat setting floats to the top) → enter a row count (defaults to
+// bulkCount; capped at 100,000; counts above 10,000 confirm first) → the dataset opens as a new
+// UNTITLED document (json / sql / plaintext for csv). It never touches the active editor, so the
+// whole suite runs editor-less — no test here ever opens a document of its own, which is itself
+// the pin for the "must work with no editor open" requirement. The dataset rendering contract
+// (csv header, json array one-per-line, trailing newline) is pinned headless in test/record/.
+const EXTENSION_ID = 'ElecTreeFrying.insert-random-text';
+const CMD = 'insertRandomText.generateDataset';
+
+// extension.ts reads a CACHED settings snapshot, refreshed on a config-change event. Wait for that event
+// (with a fallback in case the value didn't actually change) so the cache is fresh before we run.
+async function setConfig(key: string, value: unknown): Promise {
+ const changed = new Promise((resolve) => {
+ const sub = vscode.workspace.onDidChangeConfiguration((event) => {
+ if (event.affectsConfiguration(key)) { sub.dispose(); resolve(); }
+ });
+ setTimeout(() => { sub.dispose(); resolve(); }, 500);
+ });
+ await vscode.workspace.getConfiguration().update(key, value, vscode.ConfigurationTarget.Global);
+ await changed;
+}
+
+/** One scripted walk through the dataset flow (undefined at any step = Esc / dismiss). */
+interface FlowScript {
+ /** generatorIds (or custom-list names) to tick in the multi-select field pick. */
+ fields?: readonly string[];
+ /** Shape value to pick: 'json' | 'sql' | 'csv'. */
+ shape?: string;
+ /** Row-count box answer. */
+ rows?: string;
+ /** Button to press on the large-count confirmation; undefined dismisses it. */
+ confirm?: string;
+}
+
+/** What the stubbed prompts received, for prefill/ordering/warning assertions. */
+interface FlowCapture {
+ shapeItems: any[];
+ rowBox?: vscode.InputBoxOptions;
+ warnings: string[];
+}
+
+/** Stub the field pick (canPickMany), the shape pick, the row-count box, and the confirm-warn
+ * modal in one sweep. Scripted answers fail loudly on a typo (a missing field id / shape value)
+ * and run through the box's real validateInput, mirroring the prompted-commands stub. */
+async function runDataset(script: FlowScript): Promise {
+ const originalPick = vscode.window.showQuickPick;
+ const originalInput = vscode.window.showInputBox;
+ const originalWarn = vscode.window.showWarningMessage;
+ const capture: FlowCapture = { shapeItems: [], warnings: [] };
+
+ (vscode.window as any).showQuickPick = async (items: any, options: vscode.QuickPickOptions) => {
+ const resolved = await items;
+ if (options?.canPickMany) {
+ if (script.fields === undefined) { return undefined; }
+ return script.fields.map((id) => {
+ const item = resolved.find((entry: any) => entry.generatorId === id || entry.generator?.id === id);
+ if (!item) { throw new Error(`no field pick item for '${id}'`); }
+ return item;
+ });
+ }
+ capture.shapeItems = resolved;
+ if (script.shape === undefined) { return undefined; }
+ const picked = resolved.find((entry: any) => entry.value === script.shape);
+ if (!picked) { throw new Error(`no shape option with value '${script.shape}' among: ${resolved.map((i: any) => i.value).join(', ')}`); }
+ return picked;
+ };
+ (vscode.window as any).showInputBox = async (options: vscode.InputBoxOptions) => {
+ capture.rowBox = options;
+ if (script.rows !== undefined && options.validateInput) {
+ const error = await options.validateInput(script.rows);
+ if (error) { throw new Error(`scripted row count '${script.rows}' failed validation: ${error}`); }
+ }
+ return script.rows;
+ };
+ (vscode.window as any).showWarningMessage = async (message: string) => {
+ capture.warnings.push(message);
+ return script.confirm;
+ };
+
+ try {
+ await vscode.commands.executeCommand(CMD);
+ } finally {
+ (vscode.window as any).showQuickPick = originalPick;
+ (vscode.window as any).showInputBox = originalInput;
+ (vscode.window as any).showWarningMessage = originalWarn;
+ }
+ return capture;
+}
+
+describe('Generate Dataset… — records → new file', function () {
+ this.timeout(30000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.SEED, '777');
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.SEED, undefined);
+ });
+
+ afterEach(async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ it('opens an untitled JSON document holding an array of N objects — with no editor open', async () => {
+ assert.strictEqual(vscode.window.activeTextEditor, undefined, 'the suite runs editor-less by construction');
+ await runDataset({ fields: [ 'person', 'email' ], shape: 'json', rows: '3' });
+ const editor = vscode.window.activeTextEditor;
+ assert.ok(editor, 'the dataset should open in a new editor');
+ assert.ok(editor.document.isUntitled, 'the dataset opens as an UNTITLED document');
+ assert.strictEqual(editor.document.languageId, 'json');
+ const text = editor.document.getText();
+ assert.ok(text.endsWith('\n'), 'a dataset file ends with a trailing newline');
+ const parsed = JSON.parse(text);
+ assert.ok(Array.isArray(parsed), 'a json dataset is always an array');
+ assert.strictEqual(parsed.length, 3);
+ for (const row of parsed) {
+ assert.deepStrictEqual(Object.keys(row).sort(), [ 'email', 'person' ]);
+ }
+ });
+
+ it('csv opens as plaintext and leads with a header row of the field ids', async () => {
+ await runDataset({ fields: [ 'person', 'email' ], shape: 'csv', rows: '2' });
+ const editor = vscode.window.activeTextEditor;
+ assert.ok(editor);
+ assert.strictEqual(editor.document.languageId, 'plaintext');
+ const lines = editor.document.getText().split('\n');
+ assert.strictEqual(lines[0], 'person,email', 'the header names the field ids');
+ assert.strictEqual(lines.length, 4, 'header + 2 rows + trailing newline');
+ assert.strictEqual(lines[3], '');
+ });
+
+ it('sql emits one INSERT per row into the configured table', async () => {
+ await setConfig(ConfigKey.RECORD_SQL_TABLE, 'users');
+ try {
+ await runDataset({ fields: [ 'person' ], shape: 'sql', rows: '2' });
+ const editor = vscode.window.activeTextEditor;
+ assert.ok(editor);
+ assert.strictEqual(editor.document.languageId, 'sql');
+ const lines = editor.document.getText().split('\n').filter((line) => line.length > 0);
+ assert.strictEqual(lines.length, 2);
+ for (const line of lines) {
+ assert.ok(line.startsWith('INSERT INTO users (person) VALUES ('), `expected a users INSERT, got ${line}`);
+ }
+ } finally {
+ await setConfig(ConfigKey.RECORD_SQL_TABLE, undefined);
+ }
+ });
+
+ it('the shape pick floats the current recordFormat to the top, marked as current', async () => {
+ await setConfig(ConfigKey.RECORD_FORMAT, 'csv');
+ try {
+ const { shapeItems } = await runDataset({ fields: [ 'person' ], shape: 'csv', rows: '1' });
+ assert.strictEqual(shapeItems.length, 3, 'json / sql / csv');
+ assert.strictEqual(shapeItems[0].value, 'csv', 'the configured shape leads the pick');
+ assert.match(shapeItems[0].description ?? '', /current/i, 'the configured shape is marked');
+ } finally {
+ await setConfig(ConfigKey.RECORD_FORMAT, undefined);
+ }
+ });
+
+ it('the row-count box prefills from the bulkCount setting', async () => {
+ await setConfig(ConfigKey.BULK_COUNT, 7);
+ try {
+ const { rowBox } = await runDataset({ fields: [ 'person' ], shape: 'json', rows: undefined });
+ assert.ok(rowBox, 'the flow reached the row-count box');
+ assert.strictEqual(rowBox.value, '7', 'the default row count is the bulkCount setting');
+ } finally {
+ await setConfig(ConfigKey.BULK_COUNT, undefined);
+ }
+ });
+
+ it('rejects zero, negatives, fractions, junk, and counts over the 100,000 cap', async () => {
+ const { rowBox } = await runDataset({ fields: [ 'person' ], shape: 'json', rows: '1' });
+ const validate = rowBox?.validateInput as (raw: string) => string | undefined;
+ assert.ok(validate, 'the row-count box validates its input');
+ for (const bad of [ '0', '-5', '2.5', 'abc', '', '100001' ]) {
+ assert.ok(validate(bad), `'${bad}' must be rejected`);
+ }
+ for (const good of [ '1', '100000', ' 42 ' ]) {
+ assert.strictEqual(validate(good), undefined, `'${good}' must be accepted`);
+ }
+ });
+
+ it('cancelling the field pick opens nothing', async () => {
+ await runDataset({ fields: undefined });
+ assert.strictEqual(vscode.window.activeTextEditor, undefined, 'Esc at the field pick must be a clean cancel');
+ });
+
+ it('confirming the field pick with nothing ticked opens nothing', async () => {
+ await runDataset({ fields: [] });
+ assert.strictEqual(vscode.window.activeTextEditor, undefined, 'an empty tick set must be a clean cancel');
+ });
+
+ it('cancelling the shape pick opens nothing', async () => {
+ await runDataset({ fields: [ 'person' ], shape: undefined });
+ assert.strictEqual(vscode.window.activeTextEditor, undefined, 'Esc at the shape pick must be a clean cancel');
+ });
+
+ it('cancelling the row-count box opens nothing', async () => {
+ await runDataset({ fields: [ 'person' ], shape: 'json', rows: undefined });
+ assert.strictEqual(vscode.window.activeTextEditor, undefined, 'Esc at the row count must be a clean cancel');
+ });
+
+ it('asks for confirmation above 10,000 rows — dismissing generates nothing', async () => {
+ const { warnings } = await runDataset({ fields: [ 'boolean' ], shape: 'csv', rows: '10001', confirm: undefined });
+ assert.strictEqual(warnings.length, 1, 'one confirmation for a large dataset');
+ assert.match(warnings[0], /10,001/, 'the confirmation names the row count');
+ assert.strictEqual(vscode.window.activeTextEditor, undefined, 'dismissing the confirmation must generate nothing');
+ });
+
+ it('generates after the large-count confirmation is accepted', async () => {
+ const { warnings } = await runDataset({ fields: [ 'boolean' ], shape: 'csv', rows: '10001', confirm: 'Generate' });
+ assert.strictEqual(warnings.length, 1);
+ const editor = vscode.window.activeTextEditor;
+ assert.ok(editor, 'confirming should generate the dataset');
+ assert.strictEqual(editor.document.getText().split('\n').length, 10003, 'header + 10,001 rows + trailing newline');
+ });
+
+ it('does not ask for confirmation at exactly 10,000 rows', async () => {
+ const { warnings } = await runDataset({ fields: [ 'boolean' ], shape: 'csv', rows: '10000' });
+ assert.strictEqual(warnings.length, 0, 'the confirm threshold is strictly above 10,000');
+ const editor = vscode.window.activeTextEditor;
+ assert.ok(editor);
+ assert.strictEqual(editor.document.getText().split('\n').length, 10002, 'header + 10,000 rows + trailing newline');
+ });
+
+ it('seeded runs are reproducible', async () => {
+ await runDataset({ fields: [ 'person', 'email' ], shape: 'json', rows: '2' });
+ const first = vscode.window.activeTextEditor?.document.getText();
+ assert.ok(first, 'first run generated');
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await runDataset({ fields: [ 'person', 'email' ], shape: 'json', rows: '2' });
+ assert.strictEqual(vscode.window.activeTextEditor?.document.getText(), first, 'same seed → same dataset');
+ });
+
+ it('a picked custom list becomes a column keyed by its name', async () => {
+ await setConfig(ConfigKey.CUSTOM_LISTS, { pet: [ 'dog', 'cat' ] });
+ try {
+ await runDataset({ fields: [ 'pet', 'email' ], shape: 'csv', rows: '2' });
+ const editor = vscode.window.activeTextEditor;
+ assert.ok(editor);
+ const lines = editor.document.getText().split('\n');
+ assert.strictEqual(lines[0], 'pet,email', 'the list name leads the header (custom lists come first)');
+ assert.match(lines[1], /^(dog|cat),/);
+ assert.match(lines[2], /^(dog|cat),/);
+ } finally {
+ await setConfig(ConfigKey.CUSTOM_LISTS, undefined);
+ }
+ });
+});
diff --git a/src/test/commands/insert.test.ts b/src/test/commands/insert.test.ts
new file mode 100644
index 0000000..be15f16
--- /dev/null
+++ b/src/test/commands/insert.test.ts
@@ -0,0 +1,630 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { ConfigKey } from '../../configuration';
+import { generators, getGenerator } from '../../catalog';
+import { load, seed } from '../../engine';
+
+// Drives the real insert path end-to-end through executeCommand (insertGenerated isn't exported — the
+// command IS the public surface). Runs in the Extension Host. To keep assertions exact, the suite turns
+// quotes + trailing newline OFF and pins a numeric seed, so every insert is a bare, reproducible value.
+const EXTENSION_ID = 'ElecTreeFrying.insert-random-text';
+const CMD = 'extension.insertRandomString'; // → the 'string' generator (alphanumeric, symbol-free).
+
+// extension.ts reads a CACHED settings snapshot, refreshed on a config-change event. Wait for that event
+// (with a fallback in case the value didn't actually change) so the cache is fresh before we insert.
+async function setConfig(key: string, value: unknown): Promise {
+ const changed = new Promise((resolve) => {
+ const sub = vscode.workspace.onDidChangeConfiguration((event) => {
+ if (event.affectsConfiguration(key)) { sub.dispose(); resolve(); }
+ });
+ setTimeout(() => { sub.dispose(); resolve(); }, 500);
+ });
+ await vscode.workspace.getConfiguration().update(key, value, vscode.ConfigurationTarget.Global);
+ await changed;
+}
+
+async function openDoc(content: string): Promise {
+ const doc = await vscode.workspace.openTextDocument({ content });
+ return vscode.window.showTextDocument(doc);
+}
+
+describe('insert command — insertGenerated', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.WITH_QUOTE, false);
+ await setConfig(ConfigKey.WITH_NEW_LINE, false);
+ await setConfig(ConfigKey.SEED, '12345');
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, undefined);
+ await setConfig(ConfigKey.WITH_NEW_LINE, undefined);
+ await setConfig(ConfigKey.SEED, undefined);
+ });
+
+ afterEach(async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, undefined); // back to the default 'Cursor'.
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ it('inserts a value at the cursor (Cursor mode, the default)', async () => {
+ const editor = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ assert.ok(editor.document.getText().length > 0, 'expected a value at the cursor');
+ });
+
+ it('fills every cursor with a fresh value (multi-cursor)', async () => {
+ const editor = await openDoc('\n'); // two empty lines.
+ editor.selections = [ new vscode.Selection(0, 0, 0, 0), new vscode.Selection(1, 0, 1, 0) ];
+ await vscode.commands.executeCommand(CMD);
+ const [ line0, line1 ] = editor.document.getText().split('\n');
+ assert.ok(line0.length > 0 && line1.length > 0, 'both cursors should receive a value');
+ assert.notStrictEqual(line0, line1, 'uniquePerCursor (default) → distinct values per cursor');
+ });
+
+ it('strict unique re-draws duplicates inside a bulk block (setting threaded through the pipeline)', async () => {
+ // Premise: under the suite's seed (12345) the natural weekday × 5 draw repeats (Monday twice
+ // on faker 10.5) — proven here so this test can never pass with the setting left unthreaded.
+ // If a faker upgrade changes the sequence, pick a new duplicate-bearing seed.
+ await load('en');
+ seed(12345);
+ const weekday = getGenerator('weekday')!;
+ const natural = Array.from({ length: 5 }, () => weekday.generate());
+ assert.ok(new Set(natural).size < 5, 'premise broken: seed 12345 no longer draws a duplicate weekday — choose another');
+
+ await setConfig(ConfigKey.STRICT_UNIQUE, true);
+ await setConfig(ConfigKey.BULK_COUNT, 5);
+ const editor = await openDoc('');
+ await vscode.commands.executeCommand('insertRandomText.weekday');
+ const lines = editor.document.getText().split('\n');
+ await setConfig(ConfigKey.STRICT_UNIQUE, undefined);
+ await setConfig(ConfigKey.BULK_COUNT, undefined);
+ assert.strictEqual(lines.length, 5, 'bulk 5 → five lines');
+ assert.strictEqual(new Set(lines).size, 5, `strict unique should re-draw the duplicate weekday, got: ${lines.join(', ')}`);
+ });
+
+ it('is reproducible under a fixed seed (applySeed re-seeds every run)', async () => {
+ const first = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ const b = second.document.getText();
+ assert.strictEqual(a, b, 'same seed → same inserted value');
+ });
+
+ it('does not pin output when the seed is blank (applySeed early-returns)', async () => {
+ await setConfig(ConfigKey.SEED, '');
+ const first = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ const b = second.document.getText();
+ await setConfig(ConfigKey.SEED, '12345'); // restore the suite's pinned seed.
+ assert.notStrictEqual(a, b, 'a blank seed should leave output un-pinned (values differ run to run)');
+ });
+
+ it('does not pin output when the seed is non-numeric (applySeed skips NaN)', async () => {
+ // The third applySeed path: 'abc' survives the blank check but Number('abc') is NaN → never seed(NaN).
+ await setConfig(ConfigKey.SEED, 'abc');
+ const first = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ const b = second.document.getText();
+ await setConfig(ConfigKey.SEED, '12345'); // restore the suite's pinned seed.
+ assert.notStrictEqual(a, b, 'a non-numeric seed must leave output un-pinned, not seed with NaN');
+ });
+
+ it('inserts one block at the top in Top mode', async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, 'Top');
+ const editor = await openDoc('existing');
+ await vscode.commands.executeCommand(CMD);
+ assert.ok(
+ !editor.document.lineAt(0).text.startsWith('existing'),
+ 'Top mode should insert before the existing text at line 0',
+ );
+ });
+
+ it('copies to the clipboard and leaves the document untouched in Clipboard mode', async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, 'Clipboard');
+ await vscode.env.clipboard.writeText('SENTINEL');
+ const editor = await openDoc('untouched');
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(editor.document.getText(), 'untouched', 'Clipboard mode must not modify the document');
+ const clip = await vscode.env.clipboard.readText();
+ assert.ok(clip.length > 0 && clip !== 'SENTINEL', 'Clipboard mode should overwrite the clipboard with a value');
+ });
+
+ it('is a no-op with no active editor in Cursor mode (does not throw)', async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await assert.doesNotReject(async () => { await vscode.commands.executeCommand(CMD); });
+ });
+
+ it('Pick… inserts the chosen generator at the cursor', async () => {
+ const editor = await openDoc('');
+ const original = vscode.window.showQuickPick;
+ // Choose the first real entry (group separators carry no generatorId).
+ (vscode.window as any).showQuickPick = async (items: any) => (await items).find((i: any) => i.generatorId);
+ try {
+ await vscode.commands.executeCommand('insertRandomText.pick');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ assert.ok(editor.document.getText().length > 0, 'picking a type should insert it at the cursor');
+ });
+
+ it('Pick… inserts nothing when dismissed', async () => {
+ const editor = await openDoc('');
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async () => undefined;
+ try {
+ await vscode.commands.executeCommand('insertRandomText.pick');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ assert.strictEqual(editor.document.getText(), '', 'a dismissed picker should insert nothing');
+ });
+
+ it('Pick… lists one entry per non-hidden generator, grouped, and excludes hidden ones', async () => {
+ let captured: any[] = [];
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => { captured = await items; return undefined; };
+ try {
+ await vscode.commands.executeCommand('insertRandomText.pick');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ const entries = captured.filter((i) => i.generatorId);
+ const separators = captured.filter((i) => i.kind === vscode.QuickPickItemKind.Separator);
+ assert.strictEqual(entries.length, generators.filter((g) => !g.hidden).length, 'one entry per non-hidden generator');
+ assert.ok(separators.length > 0, 'groups should be separated in the picker');
+ const ids = new Set(entries.map((i) => i.generatorId));
+ for (const hidden of [ 'loremSmall', 'hashSmall' ]) {
+ assert.ok(!ids.has(hidden), `hidden generator '${hidden}' must not appear in the picker`);
+ }
+ });
+});
+
+// After a Cursor-mode insert the block must read as *typed*: nothing stays selected and the caret sits
+// right after the inserted text — for a bare caret, a replaced selection, several of each, and a
+// bulkCount block alike. Guards the recurring competitor complaint (inserted text left highlighted).
+describe('insert — post-insert cursor behavior (nothing stays selected)', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.WITH_QUOTE, false);
+ await setConfig(ConfigKey.WITH_NEW_LINE, false);
+ await setConfig(ConfigKey.SEED, '4242');
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, undefined);
+ await setConfig(ConfigKey.WITH_NEW_LINE, undefined);
+ await setConfig(ConfigKey.SEED, undefined);
+ await setConfig(ConfigKey.BULK_COUNT, undefined);
+ });
+
+ afterEach(async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ function assertCaretsParkedAfterBlocks(editor: vscode.TextEditor): void {
+ for (const selection of editor.selections) {
+ assert.ok(selection.isEmpty, `inserted text must not stay selected — got ${JSON.stringify(selection)}`);
+ // Bare single-line value (quotes + newline off) → the block ends where its line ends.
+ const lineEnd = editor.document.lineAt(selection.active.line).range.end;
+ assert.ok(
+ selection.active.isEqual(lineEnd),
+ `the caret should sit at the end of its inserted block — at ${selection.active.line}:${selection.active.character}, line ends at :${lineEnd.character}`,
+ );
+ }
+ }
+
+ it('collapses the caret after the block at a bare cursor', async () => {
+ const editor = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ assert.ok(editor.document.getText().length > 0, 'expected an inserted value');
+ assertCaretsParkedAfterBlocks(editor);
+ });
+
+ it('collapses the caret after replacing a non-empty selection', async () => {
+ const editor = await openDoc('REPLACEME');
+ editor.selection = new vscode.Selection(0, 0, 0, 'REPLACEME'.length);
+ await vscode.commands.executeCommand(CMD);
+ assert.ok(!editor.document.getText().includes('REPLACEME'), 'the selection should be replaced');
+ assertCaretsParkedAfterBlocks(editor);
+ });
+
+ it('collapses every caret in a mixed multi-cursor insert (bare caret + selection)', async () => {
+ const editor = await openDoc('\nREPLACEME');
+ editor.selections = [ new vscode.Selection(0, 0, 0, 0), new vscode.Selection(1, 0, 1, 'REPLACEME'.length) ];
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(editor.selections.length, 2, 'both cursors should survive the insert');
+ assertCaretsParkedAfterBlocks(editor);
+ });
+
+ it('parks the caret after the whole block when bulkCount > 1 (multi-line block)', async () => {
+ await setConfig(ConfigKey.BULK_COUNT, 3);
+ const editor = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ await setConfig(ConfigKey.BULK_COUNT, undefined);
+ assert.strictEqual(editor.document.lineCount, 3, 'plain bulk 3 → three lines');
+ const [ selection ] = editor.selections;
+ assert.ok(selection.isEmpty, 'the bulk block must not stay selected');
+ const documentEnd = editor.document.positionAt(editor.document.getText().length);
+ assert.ok(selection.active.isEqual(documentEnd), 'the caret should sit after the last bulk line');
+ });
+
+ it('collapses the caret after a Record… insert over a selection', async () => {
+ const editor = await openDoc('REPLACEME');
+ editor.selection = new vscode.Selection(0, 0, 0, 'REPLACEME'.length);
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) =>
+ [ (await items).find((i: any) => i.generatorId === 'person') ];
+ try {
+ await vscode.commands.executeCommand('insertRandomText.record');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ assert.ok(editor.document.getText().startsWith('{'), 'expected a JSON record');
+ const [ selection ] = editor.selections;
+ assert.ok(selection.isEmpty, 'the inserted record must not stay selected');
+ const documentEnd = editor.document.positionAt(editor.document.getText().length);
+ assert.ok(selection.active.isEqual(documentEnd), 'the caret should sit after the record');
+ });
+});
+
+// Automatic quoting through the REAL insert path (currentInsertOptions → resolveQuotePolicy → wrap).
+// Own hooks because this needs quotes ON — the opposite of the bare-value suite above.
+describe('insert — automatic quoting', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.WITH_QUOTE, true);
+ await setConfig(ConfigKey.WITH_NEW_LINE, false);
+ await setConfig(ConfigKey.SEED, '999');
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, undefined);
+ await setConfig(ConfigKey.WITH_NEW_LINE, undefined);
+ await setConfig(ConfigKey.SEED, undefined);
+ });
+
+ afterEach(async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ async function openLangDoc(language: string): Promise {
+ const doc = await vscode.workspace.openTextDocument({ language, content: '' });
+ return vscode.window.showTextDocument(doc);
+ }
+
+ it('uses double quotes in a JSON file', async () => {
+ const editor = await openLangDoc('json');
+ await vscode.commands.executeCommand(CMD);
+ const text = editor.document.getText();
+ assert.ok(text.startsWith('"') && text.endsWith('"'), `expected double-quoted, got ${text}`);
+ });
+
+ it('uses double quotes in a JavaScript file (no quote-style setting to honor)', async () => {
+ const editor = await openLangDoc('javascript');
+ await vscode.commands.executeCommand(CMD);
+ const text = editor.document.getText();
+ assert.ok(text.startsWith('"') && text.endsWith('"'), `expected double-quoted, got ${text}`);
+ });
+
+ it('uses single quotes in a SQL file', async () => {
+ const editor = await openLangDoc('sql');
+ await vscode.commands.executeCommand(CMD);
+ const text = editor.document.getText();
+ assert.ok(text.startsWith("'") && text.endsWith("'"), `expected single-quoted, got ${text}`);
+ });
+});
+
+// The Clipboard branch reshapes the options before building: plain output strips the quote wrapping (a
+// bare value is what you want to paste), while quotedList keeps it (there the quotes ARE the format).
+// Quotes must be ON for the two sides to be distinguishable — the earlier Clipboard test runs quotes-off.
+describe('insert — Clipboard formatting', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.WITH_QUOTE, true);
+ await setConfig(ConfigKey.WITH_NEW_LINE, false);
+ await setConfig(ConfigKey.INSERT_TYPE, 'Clipboard');
+ // No editor at all: Clipboard mode must work anyway (languageId undefined → double quotes).
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, undefined);
+ await setConfig(ConfigKey.WITH_NEW_LINE, undefined);
+ await setConfig(ConfigKey.INSERT_TYPE, undefined);
+ await setConfig(ConfigKey.OUTPUT_FORMAT, undefined);
+ });
+
+ it('strips the quote wrapping from a plain copy even with quotes on', async () => {
+ await setConfig(ConfigKey.OUTPUT_FORMAT, 'plain');
+ await vscode.env.clipboard.writeText('SENTINEL');
+ await vscode.commands.executeCommand(CMD);
+ const clip = await vscode.env.clipboard.readText();
+ assert.ok(clip.length > 0 && clip !== 'SENTINEL', 'expected a copied value');
+ assert.ok(!clip.startsWith('"') && !clip.startsWith("'"), `a plain clipboard copy should be bare, got ${clip}`);
+ });
+
+ it('keeps the quote wrapping when the output format is quotedList', async () => {
+ await setConfig(ConfigKey.OUTPUT_FORMAT, 'quotedList');
+ await vscode.env.clipboard.writeText('SENTINEL');
+ await vscode.commands.executeCommand(CMD);
+ const clip = await vscode.env.clipboard.readText();
+ assert.ok(clip.startsWith('"') && clip.endsWith('"'), `a quotedList clipboard copy should stay quoted, got ${clip}`);
+ });
+});
+
+// The Record… command end-to-end: multi-select fields → buildRecords → insert. The multi-select Quick
+// Pick is stubbed (canPickMany returns an ARRAY of items); shape + table come from the record settings.
+describe('insert — Record… (multi-field)', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.SEED, '777');
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.SEED, undefined);
+ await setConfig(ConfigKey.RECORD_FORMAT, undefined);
+ await setConfig(ConfigKey.RECORD_SQL_TABLE, undefined);
+ await setConfig(ConfigKey.UNIQUE_PER_CURSOR, undefined);
+ });
+
+ afterEach(async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, undefined); // back to the default 'Cursor'.
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ async function runRecordPicking(ids: readonly string[]): Promise {
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => {
+ const entries = (await items).filter((i: any) => i.generatorId);
+ // Return picks in the REQUESTED order (may differ from catalog order on purpose).
+ return ids.map((id) => entries.find((i: any) => i.generatorId === id));
+ };
+ try {
+ await vscode.commands.executeCommand('insertRandomText.record');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ }
+
+ it('inserts a JSON object keyed by the picked fields (default shape)', async () => {
+ await setConfig(ConfigKey.RECORD_FORMAT, undefined); // default: json
+ const editor = await openDoc('');
+ await runRecordPicking([ 'person', 'email' ]);
+ const text = editor.document.getText();
+ const parsed = JSON.parse(text);
+ assert.deepStrictEqual(Object.keys(parsed).sort(), [ 'email', 'person' ], `expected both fields, got ${text}`);
+ });
+
+ it('preserves catalog order regardless of tick order', async () => {
+ const editor = await openDoc('');
+ await runRecordPicking([ 'email', 'person' ]); // ticked backwards: email before person.
+ const keys = Object.keys(JSON.parse(editor.document.getText()));
+ assert.deepStrictEqual(keys, [ 'person', 'email' ], 'field order must follow the catalog, not the tick order');
+ });
+
+ it('renders a SQL INSERT with the configured table when recordFormat is sql', async () => {
+ await setConfig(ConfigKey.RECORD_FORMAT, 'sql');
+ await setConfig(ConfigKey.RECORD_SQL_TABLE, 'users');
+ const editor = await openDoc('');
+ await runRecordPicking([ 'person' ]);
+ const text = editor.document.getText();
+ assert.ok(text.startsWith('INSERT INTO users (person) VALUES ('), `expected a users INSERT, got ${text}`);
+ await setConfig(ConfigKey.RECORD_FORMAT, undefined);
+ await setConfig(ConfigKey.RECORD_SQL_TABLE, undefined);
+ });
+
+ it('inserts nothing when the field pick is dismissed', async () => {
+ const editor = await openDoc('');
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async () => undefined;
+ try {
+ await vscode.commands.executeCommand('insertRandomText.record');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ assert.strictEqual(editor.document.getText(), '', 'a dismissed field pick should insert nothing');
+ });
+
+ it('inserts nothing when the pick returns an empty selection', async () => {
+ // The other half of the `!picks || picks.length === 0` guard: OK with nothing ticked (canPickMany
+ // resolves to an empty ARRAY, not undefined).
+ const editor = await openDoc('');
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async () => [];
+ try {
+ await vscode.commands.executeCommand('insertRandomText.record');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ assert.strictEqual(editor.document.getText(), '', 'an empty field selection should insert nothing');
+ });
+
+ it('repeats one shared record at every cursor when uniquePerCursor is off', async () => {
+ await setConfig(ConfigKey.UNIQUE_PER_CURSOR, false);
+ const editor = await openDoc('\n'); // two empty lines.
+ editor.selections = [ new vscode.Selection(0, 0, 0, 0), new vscode.Selection(1, 0, 1, 0) ];
+ await runRecordPicking([ 'person' ]);
+ const [ line0, line1 ] = editor.document.getText().split('\n');
+ await setConfig(ConfigKey.UNIQUE_PER_CURSOR, undefined);
+ assert.ok(line0.length > 0, 'both cursors should receive the record');
+ assert.strictEqual(line0, line1, 'uniquePerCursor off → buildRecords runs once, same record everywhere');
+ });
+
+ it('is a no-op with no active editor (does not throw)', async () => {
+ // The field pick still shows (it precedes the editor check); confirming a pick with no editor bails.
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await assert.doesNotReject(async () => { await runRecordPicking([ 'person' ]); });
+ });
+
+ it('copies the record to the clipboard and leaves the document untouched in Clipboard mode', async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, 'Clipboard');
+ await vscode.env.clipboard.writeText('SENTINEL');
+ const editor = await openDoc('untouched');
+ await runRecordPicking([ 'person', 'email' ]);
+ assert.strictEqual(editor.document.getText(), 'untouched', 'Clipboard mode must not modify the document');
+ const clip = await vscode.env.clipboard.readText();
+ const parsed = JSON.parse(clip); // default shape: a bare JSON object.
+ assert.deepStrictEqual(Object.keys(parsed).sort(), [ 'email', 'person' ], `expected a record on the clipboard, got ${clip}`);
+ });
+
+ it('copies the record with no editor open in Clipboard mode', async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, 'Clipboard');
+ await vscode.env.clipboard.writeText('SENTINEL');
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await runRecordPicking([ 'person' ]);
+ const clip = await vscode.env.clipboard.readText();
+ assert.ok(clip !== 'SENTINEL' && clip.includes('person'), `a clipboard record needs no editor, got ${clip}`);
+ });
+
+ it('never appends the trailing newline to a record — even with withNewLine on (its default)', async () => {
+ // Single-value inserts get the withNewLine treatment; a record's shape IS its final text.
+ const editor = await openDoc('');
+ await runRecordPicking([ 'person' ]);
+ const text = editor.document.getText();
+ assert.ok(text.length > 0 && !text.endsWith('\n'), `expected a bare record block, got ${JSON.stringify(text)}`);
+ });
+
+ it('inserts one record at the top in Top mode', async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, 'Top');
+ const editor = await openDoc('existing');
+ // Park the cursor at the END of the text so a cursor-mode insert lands after
+ // 'existing' — only a real Top insert can put the record before it.
+ editor.selection = new vscode.Selection(0, 'existing'.length, 0, 'existing'.length);
+ await runRecordPicking([ 'person' ]);
+ assert.ok(!editor.document.lineAt(0).text.startsWith('existing'), 'Top mode should insert the record before the existing text');
+ assert.ok(editor.document.getText().includes('existing'), 'the existing text must survive a Top insert');
+ });
+});
+
+describe('insert — picker & status-bar UI contract', function () {
+ // The Host can't render a Quick Pick or read the status bar, but it CAN pin the exact data handed to
+ // them — group order, the id in each description (what matchOnDescription filters on), placeholder
+ // text, pick options, and the confirm message. The one thing left for a human eye is that VS Code
+ // draws that data correctly, which is a one-time check in qa/checklists/manual-qa.md, not a per-release sweep.
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ });
+
+ afterEach(async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, undefined);
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ // Group order in both pickers = first appearance among visible generators, in registry order.
+ const expectedGroups: string[] = [];
+ for (const generator of generators) {
+ if (!generator.hidden && !expectedGroups.includes(generator.group)) {
+ expectedGroups.push(generator.group);
+ }
+ }
+
+ async function capturePicker(command: string): Promise<{ items: any[]; options: any }> {
+ let captured: { items: any[]; options: any } = { items: [], options: undefined };
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any, options: any) => {
+ captured = { items: await items, options };
+ return undefined; // dismiss — we only want the data.
+ };
+ try {
+ await vscode.commands.executeCommand(command);
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ return captured;
+ }
+
+ it('Pick… separators appear in catalog group order', async () => {
+ const { items } = await capturePicker('insertRandomText.pick');
+ const separators = items.filter((i) => i.kind === vscode.QuickPickItemKind.Separator).map((i) => i.label);
+ assert.deepStrictEqual(separators, expectedGroups, 'picker groups must mirror catalog registry order');
+ });
+
+ it('Pick… entries carry the generator id as description, and the picker filters on it', async () => {
+ const { items, options } = await capturePicker('insertRandomText.pick');
+ for (const item of items.filter((i) => i.generatorId)) {
+ assert.strictEqual(item.description, item.generatorId, `'${item.label}' should expose its id for filtering`);
+ }
+ assert.strictEqual(options.matchOnDescription, true, 'without matchOnDescription, typing an id filters nothing');
+ });
+
+ it('Pick… shows its placeholder', async () => {
+ const { options } = await capturePicker('insertRandomText.pick');
+ assert.strictEqual(options.placeHolder, 'Insert Random — pick a type to insert at every cursor…');
+ });
+
+ it('Record… is the multi-select variant: same groups, hidden excluded, id filter, placeholder', async () => {
+ const { items, options } = await capturePicker('insertRandomText.record');
+ assert.strictEqual(options.canPickMany, true, 'the Record picker must be multi-select');
+ assert.strictEqual(options.matchOnDescription, true);
+ assert.strictEqual(options.placeHolder, 'Pick fields for the record…');
+ const separators = items.filter((i) => i.kind === vscode.QuickPickItemKind.Separator).map((i) => i.label);
+ assert.deepStrictEqual(separators, expectedGroups);
+ const ids = new Set(items.filter((i) => i.generatorId).map((i) => i.generatorId));
+ for (const hidden of generators.filter((g) => g.hidden)) {
+ assert.ok(!ids.has(hidden.id), `hidden generator '${hidden.id}' must not be a record field`);
+ }
+ });
+
+ async function captureStatusBar(run: () => Promise): Promise {
+ const messages: string[] = [];
+ const original = vscode.window.setStatusBarMessage;
+ (vscode.window as any).setStatusBarMessage = (message: string) => {
+ messages.push(message);
+ return { dispose() { } };
+ };
+ try {
+ await run();
+ } finally {
+ (vscode.window as any).setStatusBarMessage = original;
+ }
+ return messages;
+ }
+
+ it('a Clipboard-mode copy confirms in the status bar, named after the generator', async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, 'Clipboard');
+ const messages = await captureStatusBar(async () => {
+ await vscode.commands.executeCommand(CMD); // → the 'String' generator.
+ });
+ assert.deepStrictEqual(messages, [ '$(clippy) Copied random string to clipboard' ]);
+ });
+
+ it('a Clipboard-mode record copy confirms in the status bar', async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, 'Clipboard');
+ const originalPick = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => [ (await items).find((i: any) => i.generatorId === 'person') ];
+ let messages: string[];
+ try {
+ messages = await captureStatusBar(async () => {
+ await vscode.commands.executeCommand('insertRandomText.record');
+ });
+ } finally {
+ (vscode.window as any).showQuickPick = originalPick;
+ }
+ assert.deepStrictEqual(messages, [ '$(clippy) Copied random record to clipboard' ]);
+ });
+});
diff --git a/src/test/commands/locale.test.ts b/src/test/commands/locale.test.ts
new file mode 100644
index 0000000..6d8f21b
--- /dev/null
+++ b/src/test/commands/locale.test.ts
@@ -0,0 +1,104 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { ConfigKey } from '../../configuration';
+
+// insertRandomText.locale swaps which faker data set EVERY generator draws from — set 'de' and
+// insertRandomText.firstName produces German names, with no reload. Assertions compare the inserted
+// text against a locally imported locale instance under the suite's seed: the bundled extension and
+// this test each hold their own Faker copy, but identical version + identical seed → identical
+// sequence, so expectations track faker upgrades instead of pinning today's strings.
+const EXTENSION_ID = 'ElecTreeFrying.insert-random-text';
+const CMD = 'insertRandomText.firstName';
+const SEED = 2026;
+
+// extension.ts reads a CACHED settings snapshot, refreshed on a config-change event. Wait for that event
+// (with a fallback in case the value didn't actually change) so the cache is fresh before we insert.
+async function setConfig(key: string, value: unknown): Promise {
+ const changed = new Promise((resolve) => {
+ const sub = vscode.workspace.onDidChangeConfiguration((event) => {
+ if (event.affectsConfiguration(key)) { sub.dispose(); resolve(); }
+ });
+ setTimeout(() => { sub.dispose(); resolve(); }, 500);
+ });
+ await vscode.workspace.getConfiguration().update(key, value, vscode.ConfigurationTarget.Global);
+ await changed;
+}
+
+async function openDoc(content: string): Promise {
+ const doc = await vscode.workspace.openTextDocument({ content });
+ return vscode.window.showTextDocument(doc);
+}
+
+/** The first firstName the given locale draws under the suite's seed. */
+async function expectedFirstName(locale: 'en' | 'de' | 'ja'): Promise {
+ const { faker } = locale === 'de'
+ ? await import('@faker-js/faker/locale/de')
+ : locale === 'ja'
+ ? await import('@faker-js/faker/locale/ja')
+ : await import('@faker-js/faker/locale/en');
+ faker.seed(SEED);
+ return faker.person.firstName();
+}
+
+describe('locale — localized draws through the insert pipeline', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.WITH_QUOTE, false);
+ await setConfig(ConfigKey.WITH_NEW_LINE, false);
+ await setConfig(ConfigKey.SEED, String(SEED));
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, undefined);
+ await setConfig(ConfigKey.WITH_NEW_LINE, undefined);
+ await setConfig(ConfigKey.SEED, undefined);
+ // The active instance is a module singleton in the shared extension host —
+ // leave it (and the setting) on English data for the suites that run after.
+ await setConfig(ConfigKey.LOCALE, undefined);
+ });
+
+ afterEach(async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ it("locale 'de' draws German data through the normal pipeline", async () => {
+ await setConfig(ConfigKey.LOCALE, 'de');
+ const editor = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(editor.document.getText(), await expectedFirstName('de'));
+ });
+
+ it('seeded runs repeat per locale', async () => {
+ await setConfig(ConfigKey.LOCALE, 'ja');
+ const first = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(second.document.getText(), a, 'same seed + same locale → same value');
+ assert.strictEqual(a, await expectedFirstName('ja'), 'and it is the ja data set that repeats');
+ });
+
+ it('switching back to en applies to the next insert, no reload', async () => {
+ await setConfig(ConfigKey.LOCALE, 'de');
+ const german = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(german.document.getText(), await expectedFirstName('de'));
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await setConfig(ConfigKey.LOCALE, 'en');
+ const english = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(english.document.getText(), await expectedFirstName('en'));
+ });
+
+ it('an unshipped locale value falls back to en data', async () => {
+ await setConfig(ConfigKey.LOCALE, 'zh_CN');
+ const editor = await openDoc('');
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(editor.document.getText(), await expectedFirstName('en'));
+ });
+});
diff --git a/src/test/commands/prompted.test.ts b/src/test/commands/prompted.test.ts
new file mode 100644
index 0000000..cad80d8
--- /dev/null
+++ b/src/test/commands/prompted.test.ts
@@ -0,0 +1,451 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { ConfigKey } from '../../configuration';
+
+// The prompted commands end-to-end through executeCommand: input boxes and Quick Picks (stubbed) →
+// one-off generator → the normal insert pipeline. Suite baseline mirrors insert.test.ts: quotes +
+// newline OFF and a pinned seed, so assertions are exact; pinned ranges (min = max) pin the value outright.
+const EXTENSION_ID = 'ElecTreeFrying.insert-random-text';
+const NUMBER_RANGE = 'insertRandomText.numberRange';
+const FLOAT_RANGE = 'insertRandomText.floatRange';
+const STRING_LENGTH = 'insertRandomText.stringLength';
+const DATE_BETWEEN = 'insertRandomText.dateBetween';
+const WORDS_COUNT = 'insertRandomText.wordsCount';
+const SENTENCES_COUNT = 'insertRandomText.sentencesCount';
+const PARAGRAPHS_COUNT = 'insertRandomText.paragraphsCount';
+const UUID_FORMAT = 'insertRandomText.uuidFormat';
+const PASSWORD_OPTIONS = 'insertRandomText.passwordOptions';
+const PHONE_FORMAT = 'insertRandomText.phoneFormat';
+const FROM_TEMPLATE = 'insertRandomText.fromTemplate';
+const FROM_PATTERN = 'insertRandomText.fromPattern';
+const SEQUENCE = 'insertRandomText.sequence';
+
+async function setConfig(key: string, value: unknown): Promise {
+ const changed = new Promise((resolve) => {
+ const sub = vscode.workspace.onDidChangeConfiguration((event) => {
+ if (event.affectsConfiguration(key)) { sub.dispose(); resolve(); }
+ });
+ setTimeout(() => { sub.dispose(); resolve(); }, 500);
+ });
+ await vscode.workspace.getConfiguration().update(key, value, vscode.ConfigurationTarget.Global);
+ await changed;
+}
+
+async function openDoc(content: string): Promise {
+ const doc = await vscode.workspace.openTextDocument({ content });
+ return vscode.window.showTextDocument(doc);
+}
+
+type ReceivedPick = { items: (vscode.QuickPickItem & { value: string })[]; options: vscode.QuickPickOptions | undefined };
+interface ReceivedPrompts {
+ inputs: vscode.InputBoxOptions[];
+ picks: ReceivedPick[];
+}
+
+/** Stub showInputBox AND showQuickPick with one scripted answer per prompt, consumed in flow order
+ * (undefined = Esc; a pick answer selects the item whose `value` matches, failing loudly on a typo).
+ * A scripted box answer runs through the box's real validateInput first — a real input box would
+ * refuse it, so a failing answer is a test bug and throws loudly, like the pick-typo guard.
+ * Records what every box and pick received, so tests can assert prefills and item ordering. */
+function stubPrompts(answers: readonly (string | undefined)[]): { received: ReceivedPrompts; restore(): void } {
+ const originalInput = vscode.window.showInputBox;
+ const originalPick = vscode.window.showQuickPick;
+ const received: ReceivedPrompts = { inputs: [], picks: [] };
+ let cursor = 0;
+ (vscode.window as any).showInputBox = async (options: vscode.InputBoxOptions) => {
+ received.inputs.push(options);
+ const answer = answers[cursor++];
+ if (answer !== undefined && options.validateInput) {
+ const error = await options.validateInput(answer);
+ if (error) { throw new Error(`scripted answer '${answer}' failed the box's validation: ${error}`); }
+ }
+ return answer;
+ };
+ (vscode.window as any).showQuickPick = async (items: any, options: vscode.QuickPickOptions) => {
+ const resolved = await items;
+ received.picks.push({ items: resolved, options });
+ const answer = answers[cursor++];
+ if (answer === undefined) { return undefined; }
+ const picked = resolved.find((item: any) => item.value === answer);
+ if (!picked) { throw new Error(`no pick option with value '${answer}' among: ${resolved.map((i: any) => i.value).join(', ')}`); }
+ return picked;
+ };
+ return {
+ received,
+ restore: () => {
+ (vscode.window as any).showInputBox = originalInput;
+ (vscode.window as any).showQuickPick = originalPick;
+ },
+ };
+}
+
+async function runPromptedCommand(command: string, answers: readonly (string | undefined)[]): Promise {
+ const stub = stubPrompts(answers);
+ try {
+ await vscode.commands.executeCommand(command);
+ } finally {
+ stub.restore();
+ }
+ return stub.received;
+}
+
+describe('prompted commands — input boxes → normal pipeline', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.WITH_QUOTE, false);
+ await setConfig(ConfigKey.WITH_NEW_LINE, false);
+ await setConfig(ConfigKey.SEED, '31415');
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, undefined);
+ await setConfig(ConfigKey.WITH_NEW_LINE, undefined);
+ await setConfig(ConfigKey.SEED, undefined);
+ });
+
+ afterEach(async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, undefined); // back to the default 'Cursor'.
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ it('Number (Range…) inserts the pinned value when min = max', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(NUMBER_RANGE, [ '5', '5' ]);
+ assert.strictEqual(editor.document.getText(), '5');
+ });
+
+ it('Number (Range…) stays within the entered range', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(NUMBER_RANGE, [ '1', '3' ]);
+ const value = Number(editor.document.getText());
+ assert.ok(Number.isInteger(value) && value >= 1 && value <= 3, `${value} outside [1, 3]`);
+ });
+
+ it('Float (Range…) renders two decimals through the pipeline', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(FLOAT_RANGE, [ '2', '2' ]);
+ assert.strictEqual(editor.document.getText(), '2.00');
+ });
+
+ it('String (Length…) inserts exactly N alphanumeric characters', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(STRING_LENGTH, [ '12' ]);
+ assert.match(editor.document.getText(), /^[A-Za-z0-9]{12}$/);
+ });
+
+ it('Date (Between…) inserts the pinned instant when from = to (full ISO by default)', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(DATE_BETWEEN, [ '2020-06-15', '2020-06-15' ]);
+ assert.strictEqual(editor.document.getText(), '2020-06-15T00:00:00.000Z');
+ });
+
+ it('Date (Between…) stays within the entered range', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(DATE_BETWEEN, [ '2020-01-01', '2020-12-31' ]);
+ const text = editor.document.getText();
+ const drawn = new Date(text).getTime();
+ assert.ok(
+ drawn >= Date.parse('2020-01-01T00:00:00Z') && drawn <= Date.parse('2020-12-31T00:00:00Z'),
+ `${text} outside [from, to]`,
+ );
+ });
+
+ it('Date (Between…) renders per the dateFormat setting — the pipeline threads it in', async () => {
+ await setConfig(ConfigKey.DATE_FORMAT, 'unixSeconds');
+ const editor = await openDoc('');
+ await runPromptedCommand(DATE_BETWEEN, [ '2020-06-15', '2020-06-15' ]);
+ await setConfig(ConfigKey.DATE_FORMAT, undefined); // restore the suite baseline.
+ assert.strictEqual(editor.document.getText(), String(Date.parse('2020-06-15T00:00:00Z') / 1000));
+ });
+
+ it('Words (Count…) inserts exactly the entered number of words', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(WORDS_COUNT, [ '5' ]);
+ assert.strictEqual(editor.document.getText().split(' ').length, 5);
+ });
+
+ it('Sentences (Count…) inserts exactly the entered number of sentences', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(SENTENCES_COUNT, [ '4' ]);
+ const text = editor.document.getText();
+ assert.strictEqual((text.match(/\./g) ?? []).length, 4, `'${text}' should hold 4 sentences`);
+ });
+
+ it('Paragraphs (Count…) inserts the entered number of newline-separated paragraphs', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(PARAGRAPHS_COUNT, [ '2' ]);
+ const lines = editor.document.getText().split('\n');
+ assert.strictEqual(lines.length, 2, 'count 2 → two paragraphs');
+ for (const line of lines) { assert.ok(line.length > 0, 'paragraphs must be non-empty'); }
+ });
+
+ it('UUID (Format…) renders the picked format — braced', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(UUID_FORMAT, [ 'braced' ]);
+ assert.match(editor.document.getText(), /^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/);
+ });
+
+ it('UUID (Format…) renders UPPERCASE with no dashes', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(UUID_FORMAT, [ 'uppercaseNoDashes' ]);
+ assert.match(editor.document.getText(), /^[0-9A-F]{32}$/);
+ });
+
+ it('Password (Options…) inserts exactly N characters from the letters-and-digits pool', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(PASSWORD_OPTIONS, [ '12', 'no' ]);
+ assert.match(editor.document.getText(), /^[A-Za-z0-9]{12}$/);
+ });
+
+ it('Password (Options…) with symbols draws from the symbol-extended pool', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(PASSWORD_OPTIONS, [ '32', 'yes' ]);
+ assert.match(editor.document.getText(), /^[A-Za-z0-9!@#$%^&*]{32}$/);
+ });
+
+ it('Phone (Format…) renders the picked style — international', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(PHONE_FORMAT, [ 'international' ]);
+ assert.match(editor.document.getText(), /^\+\d{8,15}$/);
+ });
+
+ it('From Template… renders the entered mustache template through the pipeline', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(FROM_TEMPLATE, [ 'T-{{string.numeric(4)}}' ]);
+ assert.match(editor.document.getText(), /^T-\d{4}$/);
+ });
+
+ it('From Pattern… inserts a string matching the entered pattern', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(FROM_PATTERN, [ '[A-Z]{2}-[0-9]{2}' ]);
+ assert.match(editor.document.getText(), /^[A-Z]{2}-\d{2}$/);
+ });
+
+ it('Esc at the template box cancels cleanly — nothing inserted, no error', async () => {
+ const editor = await openDoc('');
+ await assert.doesNotReject(async () => { await runPromptedCommand(FROM_TEMPLATE, [ undefined ]); });
+ assert.strictEqual(editor.document.getText(), '', 'a cancelled template box must insert nothing');
+ });
+
+ it('fills every cursor with a fresh template rendering (multi-cursor)', async () => {
+ const editor = await openDoc('\n'); // two empty lines.
+ editor.selections = [ new vscode.Selection(0, 0, 0, 0), new vscode.Selection(1, 0, 1, 0) ];
+ await runPromptedCommand(FROM_TEMPLATE, [ '{{string.alphanumeric(12)}}' ]);
+ const [ line0, line1 ] = editor.document.getText().split('\n');
+ assert.match(line0, /^[A-Za-z0-9]{12}$/, 'first cursor gets a rendering');
+ assert.match(line1, /^[A-Za-z0-9]{12}$/, 'second cursor gets a rendering');
+ assert.notStrictEqual(line0, line1, 'uniquePerCursor (default) → distinct renders per cursor');
+ });
+
+ it('is reproducible under the pinned seed — the validation test-render never shifts the inserted draw', async () => {
+ const first = await openDoc('');
+ await runPromptedCommand(FROM_TEMPLATE, [ '{{string.alphanumeric(16)}}' ]);
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('');
+ await runPromptedCommand(FROM_TEMPLATE, [ '{{string.alphanumeric(16)}}' ]);
+ const b = second.document.getText();
+ assert.strictEqual(a, b, 'same seed + same template → same inserted value');
+ });
+
+ it('remembers the last pattern and prefills the next run', async () => {
+ await openDoc('');
+ await runPromptedCommand(FROM_PATTERN, [ '[a-f]{6}' ]);
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await openDoc('');
+ const received = await runPromptedCommand(FROM_PATTERN, [ '[a-f]{6}' ]);
+ assert.strictEqual(received.inputs[0].value, '[a-f]{6}', 'the pattern box should prefill the last accepted pattern');
+ });
+
+ it('Esc at the first box cancels cleanly — nothing inserted, no error', async () => {
+ const editor = await openDoc('');
+ await assert.doesNotReject(async () => { await runPromptedCommand(NUMBER_RANGE, [ undefined ]); });
+ assert.strictEqual(editor.document.getText(), '', 'a cancelled prompt must insert nothing');
+ });
+
+ it('Esc at the second box cancels cleanly too (min already accepted)', async () => {
+ const editor = await openDoc('');
+ await assert.doesNotReject(async () => { await runPromptedCommand(NUMBER_RANGE, [ '1', undefined ]); });
+ assert.strictEqual(editor.document.getText(), '', 'a mid-flow cancel must insert nothing');
+ });
+
+ it('Esc at the second date box cancels cleanly (from already accepted)', async () => {
+ const editor = await openDoc('');
+ await assert.doesNotReject(async () => { await runPromptedCommand(DATE_BETWEEN, [ '2020-01-01', undefined ]); });
+ assert.strictEqual(editor.document.getText(), '', 'a mid-flow cancel must insert nothing');
+ });
+
+ it('Esc at the count box cancels cleanly (single-step command)', async () => {
+ const editor = await openDoc('');
+ await assert.doesNotReject(async () => { await runPromptedCommand(WORDS_COUNT, [ undefined ]); });
+ assert.strictEqual(editor.document.getText(), '', 'a cancelled count prompt must insert nothing');
+ });
+
+ it('Esc at a pick step cancels cleanly — nothing inserted, no error', async () => {
+ const editor = await openDoc('');
+ await assert.doesNotReject(async () => { await runPromptedCommand(UUID_FORMAT, [ undefined ]); });
+ assert.strictEqual(editor.document.getText(), '', 'a cancelled pick must insert nothing');
+ });
+
+ it('Esc at the symbols pick cancels cleanly (length already accepted)', async () => {
+ const editor = await openDoc('');
+ await assert.doesNotReject(async () => { await runPromptedCommand(PASSWORD_OPTIONS, [ '12', undefined ]); });
+ assert.strictEqual(editor.document.getText(), '', 'a mid-flow pick cancel must insert nothing');
+ });
+
+ it('fills every cursor with a fresh value (multi-cursor)', async () => {
+ const editor = await openDoc('\n'); // two empty lines.
+ editor.selections = [ new vscode.Selection(0, 0, 0, 0), new vscode.Selection(1, 0, 1, 0) ];
+ await runPromptedCommand(NUMBER_RANGE, [ '1', '1000000' ]);
+ const [ line0, line1 ] = editor.document.getText().split('\n');
+ assert.ok(line0.length > 0 && line1.length > 0, 'both cursors should receive a value');
+ assert.notStrictEqual(line0, line1, 'uniquePerCursor (default) → distinct values per cursor');
+ });
+
+ it('fills every cursor with fresh words (multi-cursor, multi-word values)', async () => {
+ const editor = await openDoc('\n'); // two empty lines.
+ editor.selections = [ new vscode.Selection(0, 0, 0, 0), new vscode.Selection(1, 0, 1, 0) ];
+ await runPromptedCommand(WORDS_COUNT, [ '3' ]);
+ const [ line0, line1 ] = editor.document.getText().split('\n');
+ assert.strictEqual(line0.split(' ').length, 3, 'first cursor gets 3 words');
+ assert.strictEqual(line1.split(' ').length, 3, 'second cursor gets 3 words');
+ assert.notStrictEqual(line0, line1, 'uniquePerCursor (default) → distinct values per cursor');
+ });
+
+ it('fills every cursor with a fresh value (multi-cursor through a pick step)', async () => {
+ const editor = await openDoc('\n'); // two empty lines.
+ editor.selections = [ new vscode.Selection(0, 0, 0, 0), new vscode.Selection(1, 0, 1, 0) ];
+ await runPromptedCommand(UUID_FORMAT, [ 'noDashes' ]);
+ const [ line0, line1 ] = editor.document.getText().split('\n');
+ assert.match(line0, /^[0-9a-f]{32}$/, 'first cursor gets a dash-less uuid');
+ assert.match(line1, /^[0-9a-f]{32}$/, 'second cursor gets a dash-less uuid');
+ assert.notStrictEqual(line0, line1, 'uniquePerCursor (default) → distinct values per cursor');
+ });
+
+ it('is reproducible under the pinned seed (applySeed runs after the prompts)', async () => {
+ const first = await openDoc('');
+ await runPromptedCommand(NUMBER_RANGE, [ '1', '1000000' ]);
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('');
+ await runPromptedCommand(NUMBER_RANGE, [ '1', '1000000' ]);
+ const b = second.document.getText();
+ assert.strictEqual(a, b, 'same seed + same inputs → same inserted value');
+ });
+
+ it('remembers the last accepted inputs (trimmed) and prefills the next run', async () => {
+ await openDoc('');
+ await runPromptedCommand(NUMBER_RANGE, [ ' 7 ', '9' ]);
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await openDoc('');
+ const received = await runPromptedCommand(NUMBER_RANGE, [ '7', '9' ]);
+ assert.strictEqual(received.inputs[0].value, '7', 'min box should prefill the last accepted min, trimmed');
+ assert.strictEqual(received.inputs[1].value, '9', 'max box should prefill the last accepted max');
+ });
+
+ it('is reproducible under the pinned seed through a pick-parameterized flow', async () => {
+ const first = await openDoc('');
+ await runPromptedCommand(PASSWORD_OPTIONS, [ '16', 'yes' ]);
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('');
+ await runPromptedCommand(PASSWORD_OPTIONS, [ '16', 'yes' ]);
+ const b = second.document.getText();
+ assert.strictEqual(a, b, 'same seed + same picks → same inserted value');
+ });
+
+ it('remembers the last pick and floats it to the top, marked', async () => {
+ await openDoc('');
+ await runPromptedCommand(UUID_FORMAT, [ 'braced' ]);
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await openDoc('');
+ const received = await runPromptedCommand(UUID_FORMAT, [ 'braced' ]);
+ const [ pick ] = received.picks;
+ assert.strictEqual(pick.items.length, 5, 'all five formats stay offered');
+ assert.strictEqual(pick.items[0].value, 'braced', 'the remembered format floats to the top');
+ assert.match(pick.items[0].description ?? '', /Last used/, 'the remembered format is marked');
+ });
+
+ it('honors the quote policy — a String (Length…) in a JSON file arrives double-quoted', async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, true);
+ const doc = await vscode.workspace.openTextDocument({ language: 'json', content: '' });
+ const editor = await vscode.window.showTextDocument(doc);
+ await runPromptedCommand(STRING_LENGTH, [ '8' ]);
+ await setConfig(ConfigKey.WITH_QUOTE, false); // restore the suite baseline.
+ assert.match(editor.document.getText(), /^"[A-Za-z0-9]{8}"$/);
+ });
+
+ it('honors bulkCount — one block of N values at the cursor', async () => {
+ await setConfig(ConfigKey.BULK_COUNT, 3);
+ const editor = await openDoc('');
+ await runPromptedCommand(STRING_LENGTH, [ '6' ]);
+ await setConfig(ConfigKey.BULK_COUNT, undefined);
+ const lines = editor.document.getText().split('\n');
+ assert.strictEqual(lines.length, 3, 'plain bulk 3 → three lines');
+ for (const line of lines) { assert.match(line, /^[A-Za-z0-9]{6}$/); }
+ });
+
+ it('honors bulkCount with multi-line values — 2 paragraphs × bulk 2 → four lines', async () => {
+ await setConfig(ConfigKey.BULK_COUNT, 2);
+ const editor = await openDoc('');
+ await runPromptedCommand(PARAGRAPHS_COUNT, [ '2' ]);
+ await setConfig(ConfigKey.BULK_COUNT, undefined);
+ const lines = editor.document.getText().split('\n');
+ assert.strictEqual(lines.length, 4, '2 bulk blocks × 2-paragraph values, all newline-joined');
+ });
+
+ it('honors Clipboard mode — document untouched, value copied', async () => {
+ await setConfig(ConfigKey.INSERT_TYPE, 'Clipboard');
+ await vscode.env.clipboard.writeText('SENTINEL');
+ const editor = await openDoc('untouched');
+ await runPromptedCommand(NUMBER_RANGE, [ '5', '5' ]);
+ assert.strictEqual(editor.document.getText(), 'untouched', 'Clipboard mode must not modify the document');
+ assert.strictEqual(await vscode.env.clipboard.readText(), '5');
+ });
+
+ it('Sequence (Start/Step…) counts across cursors — one running counter per insert', async () => {
+ const editor = await openDoc('\n\n');
+ editor.selections = [ 0, 1, 2 ].map((line) => new vscode.Selection(line, 0, line, 0));
+ await runPromptedCommand(SEQUENCE, [ '10', '5' ]);
+ assert.strictEqual(editor.document.getText(), '10\n15\n20');
+ });
+
+ it('Sequence restarts at start on the next insert — the counter is per-operation', async () => {
+ const first = await openDoc('\n');
+ first.selections = [ 0, 1 ].map((line) => new vscode.Selection(line, 0, line, 0));
+ await runPromptedCommand(SEQUENCE, [ '10', '5' ]);
+ assert.strictEqual(first.document.getText(), '10\n15');
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('\n');
+ second.selections = [ 0, 1 ].map((line) => new vscode.Selection(line, 0, line, 0));
+ await runPromptedCommand(SEQUENCE, [ '10', '5' ]);
+ assert.strictEqual(second.document.getText(), '10\n15', 'a new insert must not continue 20, 25');
+ });
+
+ it('Sequence advances through bulk items within one cursor (negative step included)', async () => {
+ await setConfig(ConfigKey.BULK_COUNT, 3);
+ const editor = await openDoc('');
+ await runPromptedCommand(SEQUENCE, [ '7', '-2' ]);
+ await setConfig(ConfigKey.BULK_COUNT, undefined);
+ assert.strictEqual(editor.document.getText(), '7\n5\n3');
+ });
+
+ it('Sequence Esc at the step box cancels cleanly', async () => {
+ const editor = await openDoc('');
+ await runPromptedCommand(SEQUENCE, [ '10', undefined ]);
+ assert.strictEqual(editor.document.getText(), '');
+ });
+
+ it('Sequence remembers the last start/step and prefills the next run', async () => {
+ await openDoc('');
+ await runPromptedCommand(SEQUENCE, [ '100', '25' ]);
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ await openDoc('');
+ const received = await runPromptedCommand(SEQUENCE, [ '100', '25' ]);
+ assert.strictEqual(received.inputs[0].value, '100', 'start box should prefill the last accepted start');
+ assert.strictEqual(received.inputs[1].value, '25', 'step box should prefill the last accepted step');
+ });
+});
diff --git a/src/test/commands/randomize.test.ts b/src/test/commands/randomize.test.ts
new file mode 100644
index 0000000..d916c32
--- /dev/null
+++ b/src/test/commands/randomize.test.ts
@@ -0,0 +1,201 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { ConfigKey } from '../../configuration';
+
+// insertRandomText.randomizeSelection anonymizes IN PLACE, in two tiers: a selection that IS an
+// unambiguous email / uuid / ISO date / ISO timestamp is upgraded to a fresh REALISTIC fake of the same
+// type (type-aware replace); everything else gets a format-preserving randomization of its own text
+// (digits→digits, letters→letters matching case, all else untouched). It is a replacement, not an
+// insertion — insert type, quoting, newline and bulk never apply — but seed does: every draw rides the
+// shared faker RNG. The pure contracts (detection + character classes) are pinned headless in
+// test/randomize/; this suite pins the editor semantics end to end.
+const EXTENSION_ID = 'ElecTreeFrying.insert-random-text';
+const CMD = 'insertRandomText.randomizeSelection';
+const SEED = 424242;
+
+// extension.ts reads a CACHED settings snapshot, refreshed on a config-change event. Wait for that event
+// (with a fallback in case the value didn't actually change) so the cache is fresh before we run.
+async function setConfig(key: string, value: unknown): Promise {
+ const changed = new Promise((resolve) => {
+ const sub = vscode.workspace.onDidChangeConfiguration((event) => {
+ if (event.affectsConfiguration(key)) { sub.dispose(); resolve(); }
+ });
+ setTimeout(() => { sub.dispose(); resolve(); }, 500);
+ });
+ await vscode.workspace.getConfiguration().update(key, value, vscode.ConfigurationTarget.Global);
+ await changed;
+}
+
+async function openDoc(content: string): Promise {
+ const doc = await vscode.workspace.openTextDocument({ content });
+ return vscode.window.showTextDocument(doc);
+}
+
+describe('randomizeSelection — anonymize in place', function () {
+ this.timeout(20000);
+
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ await setConfig(ConfigKey.SEED, String(SEED));
+ });
+
+ after(async () => {
+ await setConfig(ConfigKey.SEED, undefined);
+ });
+
+ afterEach(async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ });
+
+ it('replaces each non-empty selection with a same-shape random value', async () => {
+ const editor = await openDoc('Alice Smith 42\ncontact: Bob@x.io');
+ editor.selections = [
+ new vscode.Selection(0, 0, 0, 'Alice Smith'.length),
+ new vscode.Selection(1, 'contact: '.length, 1, 'contact: Bob@x.io'.length),
+ ];
+ await vscode.commands.executeCommand(CMD);
+ assert.match(editor.document.lineAt(0).text, /^[A-Z][a-z]{4} [A-Z][a-z]{4} 42$/, 'case pattern, space and the unselected 42 must survive');
+ assert.match(editor.document.lineAt(1).text, /^contact: [^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/, 'the selected email upgrades to a fresh realistic email (type-aware) and the unselected prefix stays');
+ assert.notStrictEqual(editor.document.lineAt(0).text, 'Alice Smith 42', 'the selected text was actually re-rolled');
+ });
+
+ it('leaves empty selections alone in a mixed multi-selection', async () => {
+ const editor = await openDoc('keep\nRandomizeMe');
+ editor.selections = [
+ new vscode.Selection(0, 2, 0, 2), // bare caret — must not receive an insert
+ new vscode.Selection(1, 0, 1, 'RandomizeMe'.length),
+ ];
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(editor.document.lineAt(0).text, 'keep', 'nothing may be inserted at the bare caret');
+ assert.match(editor.document.lineAt(1).text, /^[A-Z][a-z]{8}[A-Z][a-z]$/);
+ });
+
+ it('does not leave the replacement selected — the cursor lands after it', async () => {
+ const editor = await openDoc('RandomizeMe');
+ editor.selection = new vscode.Selection(0, 0, 0, 'RandomizeMe'.length);
+ await vscode.commands.executeCommand(CMD);
+ assert.ok(editor.selections.every((selection) => selection.isEmpty), 'replaced text must not stay highlighted');
+ assert.deepStrictEqual(
+ [ editor.selection.active.line, editor.selection.active.character ],
+ [ 0, 'RandomizeMe'.length ],
+ );
+ });
+
+ it('shows one gentle info message and edits nothing when no text is selected', async () => {
+ const editor = await openDoc('untouched');
+ editor.selection = new vscode.Selection(0, 3, 0, 3);
+ const messages: string[] = [];
+ const original = vscode.window.showInformationMessage;
+ (vscode.window as any).showInformationMessage = async (message: string) => { messages.push(message); return undefined; };
+ try {
+ await vscode.commands.executeCommand(CMD);
+ } finally {
+ (vscode.window as any).showInformationMessage = original;
+ }
+ assert.strictEqual(editor.document.getText(), 'untouched');
+ assert.strictEqual(messages.length, 1, 'exactly one info message');
+ assert.match(messages[0], /select/i);
+ });
+
+ it('is the same gentle no-op with no editor open at all', async () => {
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const messages: string[] = [];
+ const original = vscode.window.showInformationMessage;
+ (vscode.window as any).showInformationMessage = async (message: string) => { messages.push(message); return undefined; };
+ try {
+ await vscode.commands.executeCommand(CMD);
+ } finally {
+ (vscode.window as any).showInformationMessage = original;
+ }
+ assert.strictEqual(messages.length, 1);
+ });
+
+ it('seeded runs repeat', async () => {
+ const first = await openDoc('Secret Value 123');
+ first.selection = new vscode.Selection(0, 0, 0, 'Secret Value 123'.length);
+ await vscode.commands.executeCommand(CMD);
+ const a = first.document.getText();
+ await vscode.commands.executeCommand('workbench.action.closeAllEditors');
+ const second = await openDoc('Secret Value 123');
+ second.selection = new vscode.Selection(0, 0, 0, 'Secret Value 123'.length);
+ await vscode.commands.executeCommand(CMD);
+ assert.strictEqual(second.document.getText(), a, 'same seed + same selection → same randomization');
+ });
+
+ it('a selected email upgrades to a fresh realistic email, not a scramble', async () => {
+ const original = 'jane.doe+prod@acme.com';
+ const editor = await openDoc(original);
+ editor.selection = new vscode.Selection(0, 0, 0, original.length);
+ await vscode.commands.executeCommand(CMD);
+ const result = editor.document.getText();
+ assert.match(result, /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/, 'still an email');
+ assert.notStrictEqual(result, original, 'freshly drawn');
+ // A scramble preserves length and punctuation positions; a typed redraw almost
+ // never does — deterministic under the pinned suite seed.
+ assert.notStrictEqual(result.length, original.length, 'not a character-for-character scramble');
+ });
+
+ it('a selected uppercase uuid upgrades to a fresh VALID uuid, case preserved', async () => {
+ const upper = '9B1DEB4D-3B7D-4BAD-9BDD-2B0D7B3DCB6D';
+ const editor = await openDoc(upper);
+ editor.selection = new vscode.Selection(0, 0, 0, upper.length);
+ await vscode.commands.executeCommand(CMD);
+ const result = editor.document.getText();
+ assert.match(result, /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/,
+ 'valid uppercase hex — a scramble would spill into G–Z');
+ assert.notStrictEqual(result, upper);
+ });
+
+ it('a braced lowercase uuid keeps its braces', async () => {
+ const braced = '{9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d}';
+ const editor = await openDoc(braced);
+ editor.selection = new vscode.Selection(0, 0, 0, braced.length);
+ await vscode.commands.executeCommand(CMD);
+ assert.match(editor.document.getText(), /^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/);
+ });
+
+ it('a selected ISO date upgrades to a real calendar date in the same format', async () => {
+ const editor = await openDoc('2024-02-29');
+ editor.selection = new vscode.Selection(0, 0, 0, '2024-02-29'.length);
+ await vscode.commands.executeCommand(CMD);
+ const result = editor.document.getText();
+ assert.match(result, /^\d{4}-\d{2}-\d{2}$/);
+ assert.ok(new Date(result).toISOString().startsWith(result), `'${result}' must be a real date — a digit scramble almost never is`);
+ });
+
+ it('a selected ISO timestamp stays a valid timestamp at its own precision', async () => {
+ const stamp = '2026-07-02T12:34:56Z';
+ const editor = await openDoc(stamp);
+ editor.selection = new vscode.Selection(0, 0, 0, stamp.length);
+ await vscode.commands.executeCommand(CMD);
+ const result = editor.document.getText();
+ assert.match(result, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, 'no milliseconds — the original carried none');
+ assert.ok(!Number.isNaN(Date.parse(result)), 'parses as a real instant');
+ });
+
+ it('mixed selections: typed values upgrade, plain text still scrambles in shape', async () => {
+ const editor = await openDoc('a@b.co\nAlice42');
+ editor.selections = [
+ new vscode.Selection(0, 0, 0, 'a@b.co'.length),
+ new vscode.Selection(1, 0, 1, 'Alice42'.length),
+ ];
+ await vscode.commands.executeCommand(CMD);
+ assert.match(editor.document.lineAt(0).text, /^[^@\s]+@[^@\s]+\.[A-Za-z]{2,}$/);
+ assert.match(editor.document.lineAt(1).text, /^[A-Z][a-z]{4}\d{2}$/, 'non-typed text keeps the format-preserving contract');
+ });
+
+ it('ignores quote/newline settings — a replacement, not an insertion', async () => {
+ await setConfig(ConfigKey.WITH_QUOTE, true);
+ await setConfig(ConfigKey.WITH_NEW_LINE, true);
+ try {
+ const editor = await openDoc('abc');
+ editor.selection = new vscode.Selection(0, 0, 0, 3);
+ await vscode.commands.executeCommand(CMD);
+ assert.match(editor.document.getText(), /^[a-z]{3}$/, 'no quotes, no trailing newline — bare same-shape text');
+ } finally {
+ await setConfig(ConfigKey.WITH_QUOTE, undefined);
+ await setConfig(ConfigKey.WITH_NEW_LINE, undefined);
+ }
+ });
+});
diff --git a/src/test/commands/settings.test.ts b/src/test/commands/settings.test.ts
new file mode 100644
index 0000000..da899b9
--- /dev/null
+++ b/src/test/commands/settings.test.ts
@@ -0,0 +1,302 @@
+import * as assert from 'assert';
+import * as vscode from 'vscode';
+
+import { SETTING_COMMANDS } from '../../settingsCommands';
+import { ConfigKey } from '../../configuration';
+
+// settingsCommands.ts writes settings from the palette. Handlers that show UI (Quick Pick / input box /
+// modal) are driven by temporarily swapping the matching vscode.window prompt (no Sinon in this repo),
+// restored in a finally. Toggles show no UI and are called directly. All writes land in Global config in
+// the test host (no folder open); afterEach clears them so nothing leaks between tests or across runs.
+function get(key: string): T | undefined {
+ return vscode.workspace.getConfiguration().get(key);
+}
+
+function setGlobal(key: string, value: unknown): Thenable {
+ return vscode.workspace.getConfiguration().update(key, value, vscode.ConfigurationTarget.Global);
+}
+
+const clear = (key: string) => setGlobal(key, undefined);
+
+describe('settingsCommands — toggles (no UI)', () => {
+ afterEach(async () => {
+ await clear(ConfigKey.UNIQUE_PER_CURSOR);
+ await clear(ConfigKey.STRICT_UNIQUE);
+ });
+
+ it('toggleUniquePerCursor flips the boolean', async () => {
+ await setGlobal(ConfigKey.UNIQUE_PER_CURSOR, false);
+ await SETTING_COMMANDS['insertRandomText.toggleUniquePerCursor']();
+ assert.strictEqual(get(ConfigKey.UNIQUE_PER_CURSOR), true);
+ });
+
+ it('toggleStrictUnique flips the boolean (off by default → first toggle turns it on)', async () => {
+ await SETTING_COMMANDS['insertRandomText.toggleStrictUnique']();
+ assert.strictEqual(get(ConfigKey.STRICT_UNIQUE), true, 'the default is false, so the first toggle lands on true');
+ await SETTING_COMMANDS['insertRandomText.toggleStrictUnique']();
+ assert.strictEqual(get(ConfigKey.STRICT_UNIQUE), false);
+ });
+});
+
+describe('settingsCommands — enum picker (setInsertType)', () => {
+ afterEach(async () => { await clear(ConfigKey.INSERT_TYPE); });
+
+ it('writes the chosen value from the Quick Pick', async () => {
+ const original = vscode.window.showQuickPick;
+ // The stub receives the (thenable) items and returns the one whose value is 'Top'.
+ (vscode.window as any).showQuickPick = async (items: any) => (await items).find((i: any) => i.value === 'Top');
+ try {
+ await SETTING_COMMANDS['insertRandomText.setInsertType']();
+ assert.strictEqual(get(ConfigKey.INSERT_TYPE), 'Top');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ });
+
+ it('writes nothing when the Quick Pick is dismissed', async () => {
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async () => undefined;
+ try {
+ await SETTING_COMMANDS['insertRandomText.setInsertType']();
+ // `get()` would return the package.json default ('Cursor'); the real invariant is that no global
+ // override was written, so inspect the override itself.
+ const override = vscode.workspace.getConfiguration().inspect(ConfigKey.INSERT_TYPE)?.globalValue;
+ assert.strictEqual(override, undefined, 'a dismissed picker must not write a global override');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ });
+});
+
+describe('settingsCommands — input box (setSeed)', () => {
+ afterEach(async () => { await clear(ConfigKey.SEED); });
+
+ it('writes the entered seed', async () => {
+ const original = vscode.window.showInputBox;
+ (vscode.window as any).showInputBox = async () => '42';
+ try {
+ await SETTING_COMMANDS['insertRandomText.setSeed']();
+ assert.strictEqual(get(ConfigKey.SEED), '42');
+ } finally {
+ (vscode.window as any).showInputBox = original;
+ }
+ });
+});
+
+describe('settingsCommands — reset (modal confirm)', () => {
+ afterEach(async () => { await clear(ConfigKey.BULK_COUNT); });
+
+ it('clears overrides when confirmed', async () => {
+ await setGlobal(ConfigKey.BULK_COUNT, 7);
+ assert.strictEqual(get(ConfigKey.BULK_COUNT), 7);
+
+ const original = vscode.window.showWarningMessage;
+ (vscode.window as any).showWarningMessage = async () => 'Reset';
+ try {
+ await SETTING_COMMANDS['insertRandomText.resetSettings']();
+ } finally {
+ (vscode.window as any).showWarningMessage = original;
+ }
+ assert.strictEqual(get(ConfigKey.BULK_COUNT), 1, 'reset should restore the package.json default (1)');
+ });
+
+ it('does nothing when the confirm is dismissed', async () => {
+ await setGlobal(ConfigKey.BULK_COUNT, 7);
+ const original = vscode.window.showWarningMessage;
+ (vscode.window as any).showWarningMessage = async () => undefined;
+ try {
+ await SETTING_COMMANDS['insertRandomText.resetSettings']();
+ } finally {
+ (vscode.window as any).showWarningMessage = original;
+ }
+ assert.strictEqual(get(ConfigKey.BULK_COUNT), 7, 'a dismissed confirm must leave settings unchanged');
+ });
+});
+
+describe('settingsCommands — input validation', () => {
+ // The set* input handlers hand a validateInput callback to showInputBox; capture it (the stub returns
+ // undefined, so nothing is written) and exercise the validator directly with valid + invalid inputs.
+ async function captureValidator(commandId: string): Promise<(value: string) => any> {
+ let validate: ((value: string) => any) | undefined;
+ const original = vscode.window.showInputBox;
+ (vscode.window as any).showInputBox = async (opts: any) => { validate = opts.validateInput; return undefined; };
+ try {
+ await SETTING_COMMANDS[commandId]();
+ } finally {
+ (vscode.window as any).showInputBox = original;
+ }
+ assert.ok(validate, `${commandId} should supply a validateInput`);
+ return validate!;
+ }
+
+ it('setBulkCount accepts whole numbers 1–1000 and rejects the rest', async () => {
+ const validate = await captureValidator('insertRandomText.setBulkCount');
+ assert.strictEqual(validate('1'), undefined);
+ assert.strictEqual(validate('1000'), undefined);
+ assert.ok(validate('0'), 'below range → error');
+ assert.ok(validate('1001'), 'above range → error');
+ assert.ok(validate('2.5'), 'non-integer → error');
+ assert.ok(validate('abc'), 'non-number → error');
+ });
+
+ it('setSeed accepts a number or blank and rejects non-numbers', async () => {
+ const validate = await captureValidator('insertRandomText.setSeed');
+ assert.strictEqual(validate('42'), undefined);
+ assert.strictEqual(validate(' '), undefined, 'blank (whitespace) is allowed');
+ assert.strictEqual(validate(''), undefined);
+ assert.ok(validate('abc'), 'non-number → error');
+ });
+});
+
+describe('settingsCommands — remaining command wirings', () => {
+ // Each SETTING_COMMANDS entry binds a command id to a specific (key, options); a copy-paste slip would
+ // wire a command to the wrong setting. These pin the last few that reuse the shared toggle/enum paths.
+ it('toggleQuotes flips withQuote', async () => {
+ await setGlobal(ConfigKey.WITH_QUOTE, true);
+ await SETTING_COMMANDS['insertRandomText.toggleQuotes']();
+ assert.strictEqual(get(ConfigKey.WITH_QUOTE), false);
+ await clear(ConfigKey.WITH_QUOTE);
+ });
+
+ it('toggleNewLine flips withNewLine', async () => {
+ await setGlobal(ConfigKey.WITH_NEW_LINE, true);
+ await SETTING_COMMANDS['insertRandomText.toggleNewLine']();
+ assert.strictEqual(get(ConfigKey.WITH_NEW_LINE), false);
+ await clear(ConfigKey.WITH_NEW_LINE);
+ });
+
+ it('toggleContextMenu flips the context-menu setting', async () => {
+ await setGlobal('insertRandomText.contextMenu.enabled', false);
+ await SETTING_COMMANDS['insertRandomText.toggleContextMenu']();
+ assert.strictEqual(get('insertRandomText.contextMenu.enabled'), true);
+ await clear('insertRandomText.contextMenu.enabled');
+ });
+
+ it('setOutputFormat writes the chosen format', async () => {
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => (await items).find((i: any) => i.value === 'jsonArray');
+ try {
+ await SETTING_COMMANDS['insertRandomText.setOutputFormat']();
+ assert.strictEqual(get(ConfigKey.OUTPUT_FORMAT), 'jsonArray');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ await clear(ConfigKey.OUTPUT_FORMAT);
+ });
+
+ it('setInsertType floats the current value to the top of the picker, marked', async () => {
+ await setGlobal(ConfigKey.INSERT_TYPE, 'Top'); // 'Top' is not first in the option list.
+ let captured: any[] = [];
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => { captured = await items; return undefined; };
+ try {
+ await SETTING_COMMANDS['insertRandomText.setInsertType']();
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ assert.strictEqual(captured[0].value, 'Top', 'the current value should float to the top');
+ assert.ok(String(captured[0].description).includes('Current'), 'the current option should be marked');
+ await clear(ConfigKey.INSERT_TYPE);
+ });
+
+ it('setRecordFormat writes the chosen record shape', async () => {
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => (await items).find((i: any) => i.value === 'csv');
+ try {
+ await SETTING_COMMANDS['insertRandomText.setRecordFormat']();
+ assert.strictEqual(get(ConfigKey.RECORD_FORMAT), 'csv');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ await clear(ConfigKey.RECORD_FORMAT);
+ });
+
+ it('setLocale writes the chosen locale', async () => {
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => (await items).find((i: any) => i.value === 'de');
+ try {
+ await SETTING_COMMANDS['insertRandomText.setLocale']();
+ assert.strictEqual(get(ConfigKey.LOCALE), 'de');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ await clear(ConfigKey.LOCALE);
+ });
+
+ it('setDateFormat writes the chosen date format', async () => {
+ const original = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => (await items).find((i: any) => i.value === 'unixMillis');
+ try {
+ await SETTING_COMMANDS['insertRandomText.setDateFormat']();
+ assert.strictEqual(get(ConfigKey.DATE_FORMAT), 'unixMillis');
+ } finally {
+ (vscode.window as any).showQuickPick = original;
+ }
+ await clear(ConfigKey.DATE_FORMAT);
+ });
+
+ it('setBulkCount writes the entered count as a number', async () => {
+ const original = vscode.window.showInputBox;
+ (vscode.window as any).showInputBox = async () => '7';
+ try {
+ await SETTING_COMMANDS['insertRandomText.setBulkCount']();
+ // strictEqual pins the Number() conversion — writing the raw string '7' would fail here.
+ assert.strictEqual(get(ConfigKey.BULK_COUNT), 7);
+ } finally {
+ (vscode.window as any).showInputBox = original;
+ }
+ await clear(ConfigKey.BULK_COUNT);
+ });
+
+ it('setRecordSqlTable writes the entered table name, trimmed', async () => {
+ const original = vscode.window.showInputBox;
+ (vscode.window as any).showInputBox = async () => ' users ';
+ try {
+ await SETTING_COMMANDS['insertRandomText.setRecordSqlTable']();
+ assert.strictEqual(get(ConfigKey.RECORD_SQL_TABLE), 'users', 'the table name should be trimmed on write');
+ } finally {
+ (vscode.window as any).showInputBox = original;
+ }
+ await clear(ConfigKey.RECORD_SQL_TABLE);
+ });
+});
+
+describe('settingsCommands — status-bar confirms', () => {
+ // Every write confirms via setStatusBarMessage. The bar itself can't be read back, but the message
+ // handed to it can — pinned for one toggle and one enum picker; all handlers share confirm().
+ async function captureStatusBar(run: () => Promise): Promise {
+ const messages: string[] = [];
+ const original = vscode.window.setStatusBarMessage;
+ (vscode.window as any).setStatusBarMessage = (message: string) => {
+ messages.push(message);
+ return { dispose() { } };
+ };
+ try {
+ await run();
+ } finally {
+ (vscode.window as any).setStatusBarMessage = original;
+ }
+ return messages;
+ }
+
+ it('a toggle names the setting and its new state', async () => {
+ await setGlobal(ConfigKey.WITH_QUOTE, true);
+ const messages = await captureStatusBar(() => SETTING_COMMANDS['insertRandomText.toggleQuotes']());
+ assert.deepStrictEqual(messages, [ '$(check) Wrap with quotes: Off' ]);
+ await clear(ConfigKey.WITH_QUOTE);
+ });
+
+ it('an enum picker confirms the chosen label', async () => {
+ const originalPick = vscode.window.showQuickPick;
+ (vscode.window as any).showQuickPick = async (items: any) => (await items).find((i: any) => i.value === 'Top');
+ let messages: string[];
+ try {
+ messages = await captureStatusBar(() => SETTING_COMMANDS['insertRandomText.setInsertType']());
+ } finally {
+ (vscode.window as any).showQuickPick = originalPick;
+ }
+ assert.strictEqual(messages.length, 1);
+ assert.ok(/^\$\(check\) Insert type → /.test(messages[0]), `expected an 'Insert type → …' confirm, got '${messages[0]}'`);
+ await clear(ConfigKey.INSERT_TYPE);
+ });
+});
diff --git a/src/test/config/command-parity.test.ts b/src/test/config/command-parity.test.ts
new file mode 100644
index 0000000..e766579
--- /dev/null
+++ b/src/test/config/command-parity.test.ts
@@ -0,0 +1,86 @@
+import * as assert from 'assert';
+import * as fs from 'fs';
+import * as path from 'path';
+
+import { getGenerator } from '../../catalog';
+import { promptedCommands } from '../../prompted';
+
+// COMMAND_TO_GENERATOR (extension.ts) is the wiring between a palette command and the generator it
+// inserts. extension.ts imports `vscode`, so we can't import the map under a plain-node run — read it
+// off disk and parse the object literal (the same source-as-contract tactic as command-contributions).
+// This pins the three ways the wiring drifts while the catalog grows: a mapping to a non-existent
+// generator, a mapping whose command isn't contributed, and a contributed generator command with no
+// mapping (a palette entry that fires nothing).
+
+const ROOT = path.join(__dirname, '..', '..', '..');
+const extensionSrc = fs.readFileSync(path.join(ROOT, 'src', 'extension.ts'), 'utf8');
+const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
+const declared: string[] = pkg.contributes.commands.map((c: { command: string }) => c.command);
+
+// Slice out just the COMMAND_TO_GENERATOR object literal, then pull every 'command': 'generator' pair.
+function readCommandMap(source: string): Record {
+ const start = source.indexOf('COMMAND_TO_GENERATOR');
+ assert.notStrictEqual(start, -1, 'COMMAND_TO_GENERATOR not found — did it get renamed?');
+ const open = source.indexOf('{', start);
+ const close = source.indexOf('\n};', open);
+ assert.notStrictEqual(close, -1, 'could not find the end of the COMMAND_TO_GENERATOR literal');
+ const body = source.slice(open, close);
+ const map: Record = {};
+ for (const match of body.matchAll(/'([\w.]+)'\s*:\s*'([\w.]+)'/g)) {
+ map[match[1]] = match[2];
+ }
+ return map;
+}
+
+const commandMap = readCommandMap(extensionSrc);
+// The commands that are NOT generator-backed: the Quick Pick, the multi-field Record command, the
+// Generate Dataset… file builder, the Randomize Selection editor action, and every settings command
+// (set/toggle/manage/reset).
+const META = /^insertRandomText\.(pick|record|generateDataset|randomizeSelection|set|toggle|reset|manage)/;
+// Prompted (parameterized) commands are registered from `promptedCommands`, not COMMAND_TO_GENERATOR.
+const PROMPTED = new Set(promptedCommands.map((command) => `insertRandomText.${command.id}`));
+
+describe('COMMAND_TO_GENERATOR ↔ catalog parity', () => {
+ it('parses a non-trivial map out of the source', () => {
+ assert.ok(Object.keys(commandMap).length >= 100, `only parsed ${Object.keys(commandMap).length} entries — is the slice right?`);
+ });
+
+ it('every mapped generator id exists in the catalog', () => {
+ for (const [ command, generatorId ] of Object.entries(commandMap)) {
+ assert.ok(getGenerator(generatorId), `${command} → '${generatorId}' has no generator in the catalog`);
+ }
+ });
+
+ it('every mapped command is declared in package.json contributes.commands', () => {
+ const declaredSet = new Set(declared);
+ for (const command of Object.keys(commandMap)) {
+ assert.ok(declaredSet.has(command), `${command} is mapped but not contributed in package.json`);
+ }
+ });
+
+ it('every contributed generator command is wired (only pick/settings/prompted commands may be unmapped)', () => {
+ const unwired = declared.filter((command) => !commandMap[command] && !META.test(command) && !PROMPTED.has(command));
+ assert.deepStrictEqual(unwired, [], `contributed but missing a COMMAND_TO_GENERATOR entry: ${unwired.join(', ')}`);
+ });
+
+ it('every prompted command is declared in package.json contributes.commands', () => {
+ const declaredSet = new Set(declared);
+ for (const command of PROMPTED) {
+ assert.ok(declaredSet.has(command), `${command} is a prompted command but not contributed in package.json`);
+ }
+ });
+
+ it('no prompted id collides with a catalog generator id (they share the insertRandomText.* namespace)', () => {
+ for (const { id } of promptedCommands) {
+ assert.strictEqual(getGenerator(id), undefined, `prompted id '${id}' shadows a catalog generator`);
+ }
+ });
+
+ it('every hidden generator is reachable through a command (else it is dead code)', () => {
+ // hidden generators are excluded from the Quick Pick, so a command is their ONLY entry point.
+ const mappedGenerators = new Set(Object.values(commandMap));
+ for (const id of [ 'loremSmall', 'loremMedium', 'loremLarge', 'hashSmall', 'hashMedium', 'hashLarge' ]) {
+ assert.ok(mappedGenerators.has(id), `hidden generator '${id}' has no command — unreachable`);
+ }
+ });
+});
diff --git a/src/test/config/contributions.test.ts b/src/test/config/contributions.test.ts
new file mode 100644
index 0000000..b9dfa70
--- /dev/null
+++ b/src/test/config/contributions.test.ts
@@ -0,0 +1,75 @@
+import * as assert from 'assert';
+import * as fs from 'fs';
+import * as path from 'path';
+
+// The package.json contribution blocks are data VS Code renders directly — a typo'd `when` clause or a
+// submenu item pointing at a nothing-command renders as a silently missing menu entry, with no error
+// anywhere. No API can open a context menu or the palette, so this pins the DATA half of those surfaces;
+// the RENDERING half (the submenu actually appearing on right-click) stays in qa/checklists/manual-qa.md.
+
+const ROOT = path.join(__dirname, '..', '..', '..');
+const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
+const commands: { command: string; title: string }[] = pkg.contributes.commands;
+const declared = new Set(commands.map((c) => c.command));
+
+const SUBMENU_ID = 'insertRandomText.contextSubmenu';
+
+describe('package.json contributions — palette & keybindings', () => {
+ it("every command title carries the 'Insert Random: ' palette prefix", () => {
+ const bare = commands.filter((c) => !c.title.startsWith('Insert Random: '));
+ assert.deepStrictEqual(bare.map((c) => c.command), [], 'commands missing the searchable prefix');
+ });
+
+ it('ships zero default keybindings (by design — never squat on user chords)', () => {
+ assert.strictEqual(pkg.contributes.keybindings, undefined);
+ });
+});
+
+describe('package.json contributions — editor context menu', () => {
+ const editorContext = pkg.contributes.menus['editor/context'] ?? [];
+ const submenuItems = pkg.contributes.menus[SUBMENU_ID] ?? [];
+
+ it('editor/context contributes exactly one entry: the Insert Random submenu', () => {
+ assert.strictEqual(editorContext.length, 1);
+ assert.strictEqual(editorContext[0].submenu, SUBMENU_ID);
+ });
+
+ it('the submenu is declared with its display label', () => {
+ const submenus = pkg.contributes.submenus ?? [];
+ const entry = submenus.find((s: { id: string }) => s.id === SUBMENU_ID);
+ assert.ok(entry, `contributes.submenus is missing '${SUBMENU_ID}'`);
+ assert.strictEqual(entry.label, 'Insert Random');
+ });
+
+ it('the when clause gates on editor focus AND the real contextMenu setting key', () => {
+ const when = editorContext[0]?.when ?? '';
+ assert.ok(when.includes('editorTextFocus'), `when clause lost the focus guard: '${when}'`);
+ assert.ok(when.includes('config.insertRandomText.contextMenu.enabled'), `when clause references a wrong/missing key: '${when}'`);
+ });
+
+ it('the when-clause setting key exists in contributes.configuration', () => {
+ const conf = pkg.contributes.configuration;
+ const properties = Array.isArray(conf)
+ ? Object.assign({}, ...conf.map((section: { properties: object }) => section.properties))
+ : conf.properties;
+ assert.ok(properties['insertRandomText.contextMenu.enabled'], 'the setting the when clause reads is not declared');
+ });
+
+ it('every submenu item is a declared command (a typo here renders as a missing menu row)', () => {
+ assert.ok(submenuItems.length > 0, 'the submenu has no items');
+ for (const item of submenuItems) {
+ assert.ok(declared.has(item.command), `submenu item '${item.command}' is not in contributes.commands`);
+ }
+ });
+
+ it('the submenu keeps its curated shape: Pick… first, 8 items total', () => {
+ assert.strictEqual(submenuItems.length, 8, 'the submenu is curated at 8 items — update this test deliberately if that changes');
+ assert.strictEqual(submenuItems[0].command, 'insertRandomText.pick');
+ });
+
+ it('Randomize Selection closes the submenu in its own group (S9 — anonymize in place)', () => {
+ const item = submenuItems.find((entry: { command: string }) => entry.command === 'insertRandomText.randomizeSelection');
+ assert.ok(item, 'the submenu lost its Randomize Selection entry');
+ assert.strictEqual(item.group, '4_anonymize', 'randomize is an action, not an insert — it gets its own trailing group');
+ });
+});
diff --git a/src/test/config/insert-target.test.ts b/src/test/config/insert-target.test.ts
new file mode 100644
index 0000000..c00c658
--- /dev/null
+++ b/src/test/config/insert-target.test.ts
@@ -0,0 +1,233 @@
+import * as assert from 'assert';
+
+import { Configuration, ConfigKey, WorkspaceLike } from '../../configuration';
+
+// Configuration reads through a WorkspaceLike seam (a plain map stands in for
+// vscode.workspace.getConfiguration()), so the whole settings surface is testable without booting VS
+// Code. This covers the insertType enum→target mapping (incl. the newer Clipboard target) plus every
+// default that applies when a key is unset.
+function workspaceWith(values: Record): WorkspaceLike {
+ return {
+ getConfiguration() {
+ return {
+ get(section: string): T | undefined {
+ return values[section] as T | undefined;
+ },
+ };
+ },
+ };
+}
+
+function read(values: Record) {
+ return new Configuration(workspaceWith(values)).read();
+}
+
+/** Read through an injected warn sink — the validators log every dropped entry there. (The sink is a
+ * constructor seam, not a console swap: the extension host's patched console resists reassignment.) */
+function readCapturing(values: Record): { settings: ReturnType; warnings: string[] } {
+ const warnings: string[] = [];
+ const settings = new Configuration(workspaceWith(values), (message) => warnings.push(message)).read();
+ return { settings, warnings };
+}
+
+describe('configuration — insertType (enum → target)', () => {
+ it("'Cursor' → 'cursor'", () => {
+ assert.strictEqual(read({ [ConfigKey.INSERT_TYPE]: 'Cursor' }).insertType, 'cursor');
+ });
+
+ it("'Top' → 'top'", () => {
+ assert.strictEqual(read({ [ConfigKey.INSERT_TYPE]: 'Top' }).insertType, 'top');
+ });
+
+ it("'Clipboard' → 'clipboard'", () => {
+ assert.strictEqual(read({ [ConfigKey.INSERT_TYPE]: 'Clipboard' }).insertType, 'clipboard');
+ });
+
+ it("unset → 'cursor' (default)", () => {
+ assert.strictEqual(read({}).insertType, 'cursor');
+ });
+
+ it("an unknown value → 'cursor' (safe fallback)", () => {
+ assert.strictEqual(read({ [ConfigKey.INSERT_TYPE]: 'Bogus' }).insertType, 'cursor');
+ });
+});
+
+describe('configuration — dateFormat (validated enum)', () => {
+ it("unset → 'iso' (default)", () => {
+ assert.strictEqual(read({}).dateFormat, 'iso');
+ });
+
+ it('every declared value passes through', () => {
+ for (const value of [ 'iso', 'isoDate', 'isoTime', 'unixSeconds', 'unixMillis' ]) {
+ assert.strictEqual(read({ [ConfigKey.DATE_FORMAT]: value }).dateFormat, value);
+ }
+ });
+
+ it("an unknown value → 'iso' (safe fallback)", () => {
+ assert.strictEqual(read({ [ConfigKey.DATE_FORMAT]: 'YYYY/MM/DD' }).dateFormat, 'iso');
+ });
+});
+
+describe('configuration — locale (validated enum)', () => {
+ it("unset → 'en' (default)", () => {
+ assert.strictEqual(read({}).locale, 'en');
+ });
+
+ it('every shipped locale passes through', () => {
+ for (const value of [ 'en', 'de', 'fr', 'es', 'pt_BR', 'ja' ]) {
+ assert.strictEqual(read({ [ConfigKey.LOCALE]: value }).locale, value);
+ }
+ });
+
+ it("an unshipped faker locale → 'en' (safe fallback)", () => {
+ assert.strictEqual(read({ [ConfigKey.LOCALE]: 'zh_CN' }).locale, 'en');
+ });
+
+ it("a case or separator mismatch → 'en' (ids are exact: 'pt_BR', not 'pt-br')", () => {
+ assert.strictEqual(read({ [ConfigKey.LOCALE]: 'DE' }).locale, 'en');
+ assert.strictEqual(read({ [ConfigKey.LOCALE]: 'pt-br' }).locale, 'en');
+ });
+});
+
+// The two settings-defined data pools (S7) are user-edited JSON objects, so the reader must survive any
+// shape: junk entries are dropped (never thrown on) and each drop is logged to the console.
+describe('configuration — templates (validated object)', () => {
+ it('unset → {} (default)', () => {
+ assert.deepStrictEqual(read({}).templates, {});
+ });
+
+ it('string-valued entries pass through, declaration order preserved', () => {
+ const templates = { invoice: 'INV-{{string.numeric(4)}}', contact: '{{person.fullName}} <{{internet.email}}>' };
+ const result = read({ [ConfigKey.TEMPLATES]: templates }).templates;
+ assert.deepStrictEqual(result, templates);
+ assert.deepStrictEqual(Object.keys(result), [ 'invoice', 'contact' ]);
+ });
+
+ it('drops non-string values and keeps the rest', () => {
+ const { settings } = readCapturing({
+ [ConfigKey.TEMPLATES]: { ok: '{{internet.email}}', num: 42, nil: null, arr: [ 'x' ], obj: { a: 1 }, bool: true },
+ });
+ assert.deepStrictEqual(settings.templates, { ok: '{{internet.email}}' });
+ });
+
+ it('drops empty and whitespace-only template strings', () => {
+ const { settings } = readCapturing({ [ConfigKey.TEMPLATES]: { ok: 'x', empty: '', blank: ' ' } });
+ assert.deepStrictEqual(settings.templates, { ok: 'x' });
+ });
+
+ it('drops entries with an empty name', () => {
+ const { settings } = readCapturing({ [ConfigKey.TEMPLATES]: { '': 'x', ' ': 'y', ok: 'z' } });
+ assert.deepStrictEqual(settings.templates, { ok: 'z' });
+ });
+
+ it('a non-object value → {} (safe fallback)', () => {
+ assert.deepStrictEqual(readCapturing({ [ConfigKey.TEMPLATES]: 'nope' }).settings.templates, {});
+ assert.deepStrictEqual(readCapturing({ [ConfigKey.TEMPLATES]: [ 'a', 'b' ] }).settings.templates, {});
+ assert.deepStrictEqual(readCapturing({ [ConfigKey.TEMPLATES]: null }).settings.templates, {});
+ });
+
+ it('logs a warning naming each dropped entry', () => {
+ const { warnings } = readCapturing({ [ConfigKey.TEMPLATES]: { ok: 'x', broken: 42 } });
+ assert.strictEqual(warnings.length, 1, 'exactly the dropped entry should be logged');
+ assert.match(warnings[0], /broken/, 'the warning should name the dropped entry');
+ });
+});
+
+describe('configuration — customLists (validated object)', () => {
+ it('unset → {} (default)', () => {
+ assert.deepStrictEqual(read({}).customLists, {});
+ });
+
+ it('string-array entries pass through, declaration order preserved', () => {
+ const lists = { environment: [ 'dev', 'staging', 'production' ], team: [ 'ada', 'grace' ] };
+ const result = read({ [ConfigKey.CUSTOM_LISTS]: lists }).customLists;
+ assert.deepStrictEqual(result, lists);
+ assert.deepStrictEqual(Object.keys(result), [ 'environment', 'team' ]);
+ });
+
+ it('filters non-string items out of a mixed list', () => {
+ const { settings, warnings } = readCapturing({ [ConfigKey.CUSTOM_LISTS]: { mixed: [ 'a', 3, 'b', null ] } });
+ assert.deepStrictEqual(settings.customLists, { mixed: [ 'a', 'b' ] });
+ assert.strictEqual(warnings.length, 1);
+ assert.match(warnings[0], /mixed/, 'the warning should name the filtered list');
+ });
+
+ it('drops a non-array value, an empty list, and a list with no string items', () => {
+ const { settings } = readCapturing({
+ [ConfigKey.CUSTOM_LISTS]: { ok: [ 'x' ], str: 'nope', empty: [], numbers: [ 1, 2, 3 ] },
+ });
+ assert.deepStrictEqual(settings.customLists, { ok: [ 'x' ] });
+ });
+
+ it('drops entries with an empty name', () => {
+ const { settings } = readCapturing({ [ConfigKey.CUSTOM_LISTS]: { '': [ 'x' ], ok: [ 'y' ] } });
+ assert.deepStrictEqual(settings.customLists, { ok: [ 'y' ] });
+ });
+
+ it('a non-object value → {} (safe fallback)', () => {
+ assert.deepStrictEqual(readCapturing({ [ConfigKey.CUSTOM_LISTS]: 'nope' }).settings.customLists, {});
+ assert.deepStrictEqual(readCapturing({ [ConfigKey.CUSTOM_LISTS]: [ 'a' ] }).settings.customLists, {});
+ });
+
+ it('logs a warning naming each dropped entry', () => {
+ const { warnings } = readCapturing({ [ConfigKey.CUSTOM_LISTS]: { ok: [ 'x' ], broken: 'nope' } });
+ assert.strictEqual(warnings.length, 1, 'exactly the dropped entry should be logged');
+ assert.match(warnings[0], /broken/, 'the warning should name the dropped entry');
+ });
+});
+
+describe('configuration — defaults when unset', () => {
+ it('every setting falls back to its default when unset', () => {
+ const settings = read({});
+ assert.strictEqual(settings.withQuote, true);
+ assert.strictEqual(settings.withNewLine, true);
+ assert.strictEqual(settings.uniquePerCursor, true);
+ assert.strictEqual(settings.strictUnique, false);
+ assert.strictEqual(settings.seed, '');
+ assert.strictEqual(settings.locale, 'en');
+ assert.strictEqual(settings.bulkCount, 1);
+ assert.strictEqual(settings.outputFormat, 'plain');
+ assert.strictEqual(settings.dateFormat, 'iso');
+ assert.strictEqual(settings.recordFormat, 'json');
+ assert.strictEqual(settings.recordSqlTable, 'table');
+ assert.deepStrictEqual(settings.templates, {});
+ assert.deepStrictEqual(settings.customLists, {});
+ });
+
+ it('explicit values pass through unchanged', () => {
+ const settings = read({
+ [ConfigKey.WITH_QUOTE]: false,
+ [ConfigKey.WITH_NEW_LINE]: false,
+ [ConfigKey.UNIQUE_PER_CURSOR]: false,
+ [ConfigKey.STRICT_UNIQUE]: true,
+ [ConfigKey.SEED]: '42',
+ [ConfigKey.LOCALE]: 'ja',
+ [ConfigKey.BULK_COUNT]: 5,
+ [ConfigKey.OUTPUT_FORMAT]: 'jsonArray',
+ [ConfigKey.DATE_FORMAT]: 'unixSeconds',
+ [ConfigKey.RECORD_FORMAT]: 'csv',
+ [ConfigKey.RECORD_SQL_TABLE]: 'users',
+ });
+ assert.strictEqual(settings.withQuote, false);
+ assert.strictEqual(settings.withNewLine, false);
+ assert.strictEqual(settings.uniquePerCursor, false);
+ assert.strictEqual(settings.strictUnique, true);
+ assert.strictEqual(settings.seed, '42');
+ assert.strictEqual(settings.locale, 'ja');
+ assert.strictEqual(settings.bulkCount, 5);
+ assert.strictEqual(settings.outputFormat, 'jsonArray');
+ assert.strictEqual(settings.dateFormat, 'unixSeconds');
+ assert.strictEqual(settings.recordFormat, 'csv');
+ assert.strictEqual(settings.recordSqlTable, 'users');
+ });
+});
+
+describe('configuration — read() snapshot', () => {
+ it('returns every setting key', () => {
+ const keys = Object.keys(read({})).sort();
+ assert.deepStrictEqual(
+ keys,
+ [ 'bulkCount', 'customLists', 'dateFormat', 'insertType', 'locale', 'outputFormat', 'recordFormat', 'recordSqlTable', 'seed', 'strictUnique', 'templates', 'uniquePerCursor', 'withNewLine', 'withQuote' ],
+ );
+ });
+});
diff --git a/src/test/custom/spec.test.ts b/src/test/custom/spec.test.ts
new file mode 100644
index 0000000..09d38a0
--- /dev/null
+++ b/src/test/custom/spec.test.ts
@@ -0,0 +1,103 @@
+import * as assert from 'assert';
+
+import { CUSTOM_LISTS_GROUP, customListGenerators, TEMPLATES_GROUP, templateGenerators } from '../../custom';
+import { load, seed } from '../../engine';
+
+// The settings-defined data pools (insertRandomText.templates / .customLists) are wrapped as plain
+// Generator objects here — pure (no vscode import), so the wrapping, rendering, and seeding are all
+// checkable headless. The maps arrive pre-validated from configuration.ts (string values only), so this
+// module never sees junk; extension.ts feeds the wrapped generators into the same insert pipeline as the
+// catalog. The id doubles as the Record… field key, which is why it is the bare name, unprefixed.
+
+describe('custom — saved templates as generators', function () {
+ this.timeout(15000);
+
+ // Rendering draws through the shared engine accessor, like every catalog generator.
+ before(async () => {
+ await load();
+ });
+
+ it('wraps each named template as a Generator in the Templates group, declaration order preserved', () => {
+ const generatorsBuilt = templateGenerators({
+ invoice: 'INV-{{string.numeric(4)}}',
+ contact: '{{person.fullName}} <{{internet.email}}>',
+ });
+ assert.deepStrictEqual(
+ generatorsBuilt.map(({ id, label, group }) => ({ id, label, group })),
+ [
+ { id: 'invoice', label: 'invoice', group: TEMPLATES_GROUP },
+ { id: 'contact', label: 'contact', group: TEMPLATES_GROUP },
+ ],
+ );
+ });
+
+ it('an empty map wraps to an empty array (nothing saved → no group)', () => {
+ assert.deepStrictEqual(templateGenerators({}), []);
+ });
+
+ it('generate() renders the template through faker', () => {
+ const [ generator ] = templateGenerators({ invoice: 'INV-{{string.numeric(4)}}' });
+ assert.match(generator.generate(), /^INV-\d{4}$/);
+ });
+
+ it('draws a fresh value per call — never memoized', () => {
+ const [ generator ] = templateGenerators({ id: '{{string.alphanumeric(16)}}' });
+ assert.notStrictEqual(generator.generate(), generator.generate());
+ });
+
+ it('is reproducible under a seed (draws ride the shared RNG)', () => {
+ const [ generator ] = templateGenerators({ id: '{{string.alphanumeric(16)}}' });
+ seed(4242);
+ const first = generator.generate();
+ seed(4242);
+ assert.strictEqual(generator.generate(), first);
+ });
+});
+
+describe('custom — custom lists as generators', function () {
+ this.timeout(15000);
+
+ before(async () => {
+ await load();
+ });
+
+ it('wraps each named list as a Generator in the Custom Lists group, declaration order preserved', () => {
+ const generatorsBuilt = customListGenerators({
+ environment: [ 'dev', 'staging', 'production' ],
+ team: [ 'ada', 'grace' ],
+ });
+ assert.deepStrictEqual(
+ generatorsBuilt.map(({ id, label, group }) => ({ id, label, group })),
+ [
+ { id: 'environment', label: 'environment', group: CUSTOM_LISTS_GROUP },
+ { id: 'team', label: 'team', group: CUSTOM_LISTS_GROUP },
+ ],
+ );
+ });
+
+ it('an empty map wraps to an empty array (nothing saved → no group)', () => {
+ assert.deepStrictEqual(customListGenerators({}), []);
+ });
+
+ it('generate() draws only from the list', () => {
+ const values = [ 'red', 'green', 'blue' ];
+ const [ generator ] = customListGenerators({ colors: values });
+ for (let draw = 0; draw < 25; draw++) {
+ assert.ok(values.includes(generator.generate()), 'every draw must come from the list');
+ }
+ });
+
+ it('a single-value list always draws that value', () => {
+ const [ generator ] = customListGenerators({ only: [ 'the-one' ] });
+ assert.strictEqual(generator.generate(), 'the-one');
+ });
+
+ it('is reproducible under a seed (draws ride the shared RNG)', () => {
+ const [ generator ] = customListGenerators({ colors: [ 'red', 'green', 'blue', 'cyan', 'plum' ] });
+ seed(31415);
+ const first = [ generator.generate(), generator.generate(), generator.generate() ];
+ seed(31415);
+ const second = [ generator.generate(), generator.generate(), generator.generate() ];
+ assert.deepStrictEqual(second, first);
+ });
+});
diff --git a/src/test/engine/lifecycle.test.ts b/src/test/engine/lifecycle.test.ts
new file mode 100644
index 0000000..099ca7e
--- /dev/null
+++ b/src/test/engine/lifecycle.test.ts
@@ -0,0 +1,105 @@
+import * as assert from 'assert';
+
+import { faker, load, LOCALES, seed } from '../../engine';
+
+// engine owns the faker lifecycle: a lazy, idempotent load(); the faker() accessor; and seed(). We skip
+// the "faker() throws before load()" guard on purpose — the loaded instance is a module-level singleton
+// shared across the whole test process, and another suite loads it first, so that guard can't be
+// observed here in isolation.
+describe('engine — faker lifecycle', function () {
+ this.timeout(15000);
+
+ before(async () => {
+ await load();
+ });
+
+ it('faker() returns a usable instance after load()', () => {
+ assert.strictEqual(typeof faker().person.firstName, 'function');
+ });
+
+ it('load() is idempotent — a second load keeps the same instance', async () => {
+ const instance = faker();
+ await load();
+ assert.strictEqual(faker(), instance);
+ });
+
+ it('seed(n) makes the next draw reproducible', () => {
+ seed(123);
+ const first = faker().string.uuid();
+ seed(123);
+ const second = faker().string.uuid();
+ assert.strictEqual(first, second);
+ });
+});
+
+// S8: load(locale) makes one of the six shipped locale instances active. In this headless run the
+// test's own locale import resolves to the very module engine imports (one module cache), so identity
+// assertions are exact; the seeded-draw comparisons additionally hold cross-instance (the Host tier
+// re-checks them against the bundled copy).
+describe('engine — locale switching', function () {
+ this.timeout(15000);
+
+ after(async () => {
+ await load('en'); // the instance is a process-wide singleton — leave later suites on English data.
+ });
+
+ it("load('de') swaps the active instance to the German data set", async () => {
+ const { faker: fakerDE } = await import('@faker-js/faker/locale/de');
+ await load('de');
+ assert.strictEqual(faker(), fakerDE, 'faker() should hand out the de instance');
+ seed(7);
+ const drawn = faker().person.firstName();
+ fakerDE.seed(7);
+ assert.strictEqual(drawn, fakerDE.person.firstName(), 'the active instance should draw de data');
+ });
+
+ it('bare load() defaults to en', async () => {
+ const { faker: fakerEN } = await import('@faker-js/faker/locale/en');
+ await load();
+ assert.strictEqual(faker(), fakerEN);
+ });
+
+ it('locale instances are cached — re-loading reuses the same instance', async () => {
+ await load('de');
+ const first = faker();
+ await load('en');
+ assert.notStrictEqual(faker(), first, 'en and de must be distinct instances');
+ await load('de');
+ assert.strictEqual(faker(), first, 'a second load(de) must reuse the cached instance');
+ });
+
+ it('the same seed draws different data under different locales (a real swap, not a relabel)', async () => {
+ await load('en');
+ seed(7);
+ const en = faker().person.firstName();
+ await load('ja');
+ seed(7);
+ const ja = faker().person.firstName();
+ assert.notStrictEqual(en, ja, 'ja names should not match en names under the same seed');
+ });
+
+ it('every shipped locale loads and draws', async () => {
+ for (const locale of LOCALES) {
+ await load(locale);
+ assert.ok(faker().person.firstName().length > 0, `locale '${locale}' should draw a first name`);
+ }
+ });
+});
+
+describe('engine — faker() before load()', () => {
+ it('throws when called before load() has resolved', () => {
+ // The loaded instance is a module-level singleton shared across the whole test process, so once any
+ // other suite has called load() this guard can't be observed directly. Re-require a *fresh* copy of
+ // the module (its `instance` still undefined) to exercise the guard, then restore the loaded module
+ // in require.cache so nothing else in the run is disturbed.
+ const key = require.resolve('../../engine');
+ const saved = require.cache[key];
+ delete require.cache[key];
+ try {
+ const fresh = require('../../engine');
+ assert.throws(() => fresh.faker(), /before load/);
+ } finally {
+ if (saved) { require.cache[key] = saved; } else { delete require.cache[key]; }
+ }
+ });
+});
diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts
new file mode 100644
index 0000000..601042b
--- /dev/null
+++ b/src/test/extension.test.ts
@@ -0,0 +1,36 @@
+import * as assert from 'assert';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as vscode from 'vscode';
+
+import { deactivate } from '../extension';
+
+// Runtime activation + command-registration smoke, the auto-import way (getExtension().activate() then
+// vscode.commands.getCommands). This complements config/command-parity.test.ts: that one statically pins
+// the COMMAND_TO_GENERATOR wiring off disk; this proves every declared command is actually REGISTERED in
+// a running VS Code — catching a command that's contributed but never reaches registerCommand (a dead
+// palette/menu entry). Runs only under the Extension Host (`npm test`), not the plain-node run.
+const EXTENSION_ID = 'ElecTreeFrying.insert-random-text';
+const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8'));
+const declared: string[] = pkg.contributes.commands.map((c: { command: string }) => c.command);
+
+describe('extension activation', () => {
+ before(async () => {
+ await vscode.extensions.getExtension(EXTENSION_ID)?.activate();
+ });
+
+ it('activates', () => {
+ assert.strictEqual(vscode.extensions.getExtension(EXTENSION_ID)?.isActive, true);
+ });
+
+ it('registers every command declared in package.json', async () => {
+ const registered = new Set(await vscode.commands.getCommands(true));
+ const missing = declared.filter((command) => !registered.has(command));
+ assert.deepStrictEqual(missing, [], `declared but not registered: ${missing.join(', ')}`);
+ });
+
+ it('deactivate() is a safe no-op', () => {
+ assert.doesNotThrow(() => deactivate());
+ assert.strictEqual(deactivate(), undefined);
+ });
+});
diff --git a/src/test/format/blocks.test.ts b/src/test/format/blocks.test.ts
new file mode 100644
index 0000000..d608b2f
--- /dev/null
+++ b/src/test/format/blocks.test.ts
@@ -0,0 +1,266 @@
+import * as assert from 'assert';
+
+import { buildBlocks, InsertOptions } from '../../formatter';
+import { getGenerator } from '../../catalog';
+import type { Generator, GenerateOptions } from '../../catalog';
+import { load, seed } from '../../engine';
+
+// buildBlocks is the formatter's pure render core: it turns (cursorCount, generator, options) into one
+// string block per cursor, each holding `bulkCount` values shaped by `outputFormat`, with
+// `uniquePerCursor` deciding whether every cursor draws fresh or shares one block. quote/escape.test.ts
+// already pins the quote-wrapping; this pins the surrounding block-shaping the editor glue in
+// extension.ts relies on (multi-cursor fill, bulk count, output formats) — all without booting VS Code.
+
+// A fixed value keeps output exact; a counter reveals how many times / how independently generate() ran.
+function fixed(value: string): Generator {
+ return { id: 't', label: 'T', group: 'G', generate: () => value };
+}
+function counter(): Generator {
+ let n = 0;
+ return { id: 'c', label: 'C', group: 'G', generate: () => String(n++) };
+}
+
+const BASE: InsertOptions = {
+ quote: '',
+ newline: '',
+ uniquePerCursor: true,
+ bulkCount: 1,
+ outputFormat: 'plain',
+};
+
+describe('buildBlocks — one block per cursor', () => {
+ it('produces an empty array for zero cursors', () => {
+ assert.deepStrictEqual(buildBlocks(0, fixed('x'), BASE), []);
+ });
+
+ it('produces exactly one block per cursor', () => {
+ assert.deepStrictEqual(buildBlocks(3, fixed('x'), BASE), [ 'x', 'x', 'x' ]);
+ });
+});
+
+describe('buildBlocks — uniquePerCursor', () => {
+ it('draws a fresh value at each cursor when true', () => {
+ // counter yields 0,1,2 — proves each cursor calls generate() independently (the multi-cursor fill).
+ assert.deepStrictEqual(buildBlocks(3, counter(), { ...BASE, uniquePerCursor: true }), [ '0', '1', '2' ]);
+ });
+
+ it('repeats one shared value at every cursor when false', () => {
+ // The block is computed once, so the counter never advances past its first draw.
+ assert.deepStrictEqual(buildBlocks(3, counter(), { ...BASE, uniquePerCursor: false }), [ '0', '0', '0' ]);
+ });
+
+ it('shares a full multi-value block across cursors when false', () => {
+ // The shared block is built once (counter → 0,1,2), then repeated verbatim at every cursor.
+ const blocks = buildBlocks(2, counter(), { ...BASE, uniquePerCursor: false, bulkCount: 3 });
+ assert.deepStrictEqual(blocks, [ '0\n1\n2', '0\n1\n2' ]);
+ });
+});
+
+describe('buildBlocks — bulkCount', () => {
+ it('emits bulkCount values at a cursor (plain = newline-joined)', () => {
+ const [ block ] = buildBlocks(1, counter(), { ...BASE, bulkCount: 3 });
+ assert.strictEqual(block, '0\n1\n2');
+ });
+
+ it('clamps bulkCount 0 up to a single value', () => {
+ assert.deepStrictEqual(buildBlocks(1, counter(), { ...BASE, bulkCount: 0 }), [ '0' ]);
+ });
+
+ it('clamps a negative bulkCount up to a single value', () => {
+ assert.deepStrictEqual(buildBlocks(1, counter(), { ...BASE, bulkCount: -5 }), [ '0' ]);
+ });
+
+ it('floors a fractional bulkCount (1.9 → 1 value, not 2)', () => {
+ assert.deepStrictEqual(buildBlocks(1, counter(), { ...BASE, bulkCount: 1.9 }), [ '0' ]);
+ });
+});
+
+describe('buildBlocks — outputFormat', () => {
+ it('plain joins values with newlines', () => {
+ const [ block ] = buildBlocks(1, counter(), { ...BASE, outputFormat: 'plain', bulkCount: 3 });
+ assert.strictEqual(block, '0\n1\n2');
+ });
+
+ it('jsonArray emits a JSON array (values JSON-stringified, quote option ignored)', () => {
+ const [ block ] = buildBlocks(1, counter(), { ...BASE, outputFormat: 'jsonArray', bulkCount: 3 });
+ assert.strictEqual(block, '[ "0", "1", "2" ]');
+ });
+
+ it('quotedList wraps each value and comma-joins them', () => {
+ const [ block ] = buildBlocks(1, counter(), { ...BASE, outputFormat: 'quotedList', quote: '"', bulkCount: 3 });
+ assert.strictEqual(block, '"0", "1", "2"');
+ });
+});
+
+describe('buildBlocks — format + newline / list escaping', () => {
+ it('jsonArray appends the trailing newline', () => {
+ const [ block ] = buildBlocks(1, counter(), { ...BASE, outputFormat: 'jsonArray', bulkCount: 2, newline: '\n' });
+ assert.strictEqual(block, '[ "0", "1" ]\n');
+ });
+
+ it('quotedList appends the trailing newline', () => {
+ const [ block ] = buildBlocks(1, counter(), { ...BASE, outputFormat: 'quotedList', quote: '"', bulkCount: 2, newline: '\n' });
+ assert.strictEqual(block, '"0", "1"\n');
+ });
+
+ it('quotedList backslash-escapes the quote char inside each value', () => {
+ const [ block ] = buildBlocks(1, fixed("O'Brien"), { ...BASE, outputFormat: 'quotedList', quote: "'", escape: 'backslash', bulkCount: 2 });
+ assert.strictEqual(block, "'O\\'Brien', 'O\\'Brien'");
+ });
+
+ it('quotedList sqlDouble-escapes the quote char inside each value', () => {
+ const [ block ] = buildBlocks(1, fixed("O'Brien"), { ...BASE, outputFormat: 'quotedList', quote: "'", escape: 'sqlDouble', bulkCount: 2 });
+ assert.strictEqual(block, "'O''Brien', 'O''Brien'");
+ });
+});
+
+describe('buildBlocks — dateFormat threading', () => {
+ // The formatter is the only caller of generate() on the insert path — it must hand the dateFormat
+ // option through so the Time generators can render per the setting. A probe generator records what
+ // each draw received.
+ function probe(): { generator: Generator; seen: unknown[] } {
+ const seen: unknown[] = [];
+ return {
+ seen,
+ generator: { id: 'p', label: 'P', group: 'G', generate: (opts?: GenerateOptions) => { seen.push(opts?.dateFormat); return 'x'; } },
+ };
+ }
+
+ it('passes options.dateFormat into every generate() call (all cursors, all bulk items)', () => {
+ const { generator, seen } = probe();
+ buildBlocks(2, generator, { ...BASE, bulkCount: 2, dateFormat: 'unixSeconds' });
+ assert.deepStrictEqual(seen, [ 'unixSeconds', 'unixSeconds', 'unixSeconds', 'unixSeconds' ]);
+ });
+
+ it('passes no format when the option is unset (generators fall back to full ISO)', () => {
+ const { generator, seen } = probe();
+ buildBlocks(1, generator, BASE);
+ assert.deepStrictEqual(seen, [ undefined ]);
+ });
+});
+
+describe('buildBlocks — strictUnique (seen-set re-draws)', () => {
+ // A scripted duplicating source: cycles through a fixed sequence, so both the duplicate draw and
+ // the value a re-draw lands on are exact.
+ function cycle(values: readonly string[]): Generator {
+ let n = 0;
+ return { id: 'y', label: 'Y', group: 'G', generate: () => values[n++ % values.length] };
+ }
+
+ it('is off by default: duplicate draws pass through', () => {
+ const [ block ] = buildBlocks(1, cycle([ 'a', 'a', 'b' ]), { ...BASE, bulkCount: 3 });
+ assert.strictEqual(block, 'a\na\nb');
+ });
+
+ it('explicit false behaves exactly like absent', () => {
+ const [ block ] = buildBlocks(1, cycle([ 'a', 'a', 'b' ]), { ...BASE, bulkCount: 3, strictUnique: false });
+ assert.strictEqual(block, 'a\na\nb');
+ });
+
+ it('re-draws a duplicate within a bulk block when on', () => {
+ const [ block ] = buildBlocks(1, cycle([ 'a', 'a', 'b' ]), { ...BASE, bulkCount: 2, strictUnique: true });
+ assert.strictEqual(block, 'a\nb');
+ });
+
+ it('spans every cursor when uniquePerCursor is on (cross-cursor values are meant to differ)', () => {
+ assert.deepStrictEqual(
+ buildBlocks(2, cycle([ 'a', 'a', 'b' ]), { ...BASE, strictUnique: true }),
+ [ 'a', 'b' ],
+ );
+ });
+
+ it('dedups only within the shared block when uniquePerCursor is off (cursors repeat by design)', () => {
+ assert.deepStrictEqual(
+ buildBlocks(2, cycle([ 'a', 'a', 'b' ]), { ...BASE, uniquePerCursor: false, bulkCount: 2, strictUnique: true }),
+ [ 'a\nb', 'a\nb' ],
+ );
+ });
+
+ it('keeps the duplicate once the re-draw budget is exhausted (never hangs)', () => {
+ const [ block ] = buildBlocks(1, fixed('x'), { ...BASE, bulkCount: 3, strictUnique: true });
+ assert.strictEqual(block, 'x\nx\nx');
+ });
+
+ it('spends exactly the 25-re-draw budget before keeping a duplicate', () => {
+ // A single-value domain: the first draw is fresh; the second exhausts the budget (one draw +
+ // 25 re-draws) and keeps the duplicate. The budget is part of the seeded-output contract —
+ // changing it shifts every seeded strict-unique sequence, so update this pin deliberately.
+ let draws = 0;
+ const constant: Generator = { id: 'k', label: 'K', group: 'G', generate: () => { draws++; return 'x'; } };
+ const [ block ] = buildBlocks(1, constant, { ...BASE, bulkCount: 2, strictUnique: true });
+ assert.strictEqual(block, 'x\nx');
+ assert.strictEqual(draws, 27, 'value 1: one draw; value 2: one draw + 25 re-draws');
+ });
+
+ it('hands dateFormat to re-draws too (every draw carries the same options)', () => {
+ const seen: unknown[] = [];
+ let n = 0;
+ const values = [ 'a', 'a', 'b' ];
+ const dupThenFresh: Generator = {
+ id: 'd', label: 'D', group: 'G',
+ generate: (opts?: GenerateOptions) => { seen.push(opts?.dateFormat); return values[n++]; },
+ };
+ const [ block ] = buildBlocks(1, dupThenFresh, { ...BASE, bulkCount: 2, strictUnique: true, dateFormat: 'isoDate' });
+ assert.strictEqual(block, 'a\nb');
+ assert.deepStrictEqual(seen, [ 'isoDate', 'isoDate', 'isoDate' ]);
+ });
+});
+
+describe('buildBlocks — strictUnique with the real engine', function () {
+ this.timeout(10000);
+ before(async () => { await load(); });
+
+ it('uuid × 200 in one bulk block → zero duplicates', () => {
+ seed(1234);
+ const uuid = getGenerator('uuid')!;
+ const [ block ] = buildBlocks(1, uuid, { ...BASE, bulkCount: 200, strictUnique: true });
+ const values = block.split('\n');
+ assert.strictEqual(values.length, 200);
+ assert.strictEqual(new Set(values).size, 200, 'strict unique must leave no duplicate among 200 uuids');
+ });
+
+ it('boolean × bulk 10 terminates, duplicates allowed once the domain is exhausted', () => {
+ seed(1);
+ const boolean = getGenerator('boolean')!;
+ const [ block ] = buildBlocks(1, boolean, { ...BASE, bulkCount: 10, strictUnique: true });
+ const values = block.split('\n');
+ assert.strictEqual(values.length, 10, 'exhaustion must keep the duplicate and move on — all 10 values delivered');
+ assert.ok(values.every((value) => value === 'true' || value === 'false'), `unexpected values: ${block}`);
+ });
+
+ it('weekday × 5 under a duplicate-bearing seed → five distinct values', () => {
+ // Premise: seed 12345's natural weekday sequence repeats (Monday twice on faker 10.5) —
+ // verified here so this test can never pass without a re-draw actually happening. If a faker
+ // upgrade changes the sequence, pick a new duplicate-bearing seed.
+ const weekday = getGenerator('weekday')!;
+ seed(12345);
+ const natural = Array.from({ length: 5 }, () => weekday.generate());
+ assert.ok(new Set(natural).size < 5, 'premise broken: this seed no longer draws a duplicate — choose another');
+
+ seed(12345);
+ const [ block ] = buildBlocks(1, weekday, { ...BASE, bulkCount: 5, strictUnique: true });
+ assert.strictEqual(new Set(block.split('\n')).size, 5, `expected five distinct weekdays, got: ${block}`);
+ });
+
+ it('same seed → same output with strict unique on (re-draws are deterministic)', () => {
+ // 3 cursors × bulk 3 from a 7-value domain forces re-draws AND exhaustion — both must replay
+ // identically under the same seed.
+ const weekday = getGenerator('weekday')!;
+ seed(42);
+ const first = buildBlocks(3, weekday, { ...BASE, bulkCount: 3, strictUnique: true });
+ seed(42);
+ const second = buildBlocks(3, weekday, { ...BASE, bulkCount: 3, strictUnique: true });
+ assert.deepStrictEqual(first, second);
+ });
+});
+
+describe('buildBlocks — newline', () => {
+ it('appends the newline string after each block', () => {
+ const blocks = buildBlocks(2, fixed('x'), { ...BASE, uniquePerCursor: false, newline: '\n' });
+ assert.deepStrictEqual(blocks, [ 'x\n', 'x\n' ]);
+ });
+
+ it('appends nothing when newline is empty', () => {
+ assert.deepStrictEqual(buildBlocks(1, fixed('x'), { ...BASE, newline: '' }), [ 'x' ]);
+ });
+});
diff --git a/src/test/prompted/spec.test.ts b/src/test/prompted/spec.test.ts
new file mode 100644
index 0000000..8638d19
--- /dev/null
+++ b/src/test/prompted/spec.test.ts
@@ -0,0 +1,583 @@
+import * as assert from 'assert';
+
+import { formatUuid, getPromptedCommand, InputStep, PickStep, promptedCommands, toGenerator } from '../../prompted';
+import { load, seed } from '../../engine';
+
+// The prompted-command registry is pure (no vscode import): each entry declares its steps — input boxes
+// (prompt/placeholder/fallback + a validateInput-shaped validator) and Quick Picks (options + fallback) —
+// and a render(params) that draws one fresh value. The vscode glue in extension.ts just walks the steps —
+// so everything that can be wrong about a prompted command (validation rules, pick options, rendering,
+// the Generator contract) is checkable here.
+
+/** The input-box steps of a command, typed; pick steps filtered out. */
+function inputSteps(id: string): readonly InputStep[] {
+ return (getPromptedCommand(id)?.steps ?? []).filter((step): step is InputStep => step.kind !== 'pick');
+}
+
+/** The Quick Pick steps of a command, typed; input-box steps filtered out. */
+function pickSteps(id: string): readonly PickStep[] {
+ return (getPromptedCommand(id)?.steps ?? []).filter((step): step is PickStep => step.kind === 'pick');
+}
+
+describe('prompted — registry', function () {
+ this.timeout(15000);
+
+ // The template/pattern validators prove their input by test-rendering through
+ // faker, so the registry checks that exercise validate() need the engine live —
+ // mirroring the glue, which awaits load() before the first box opens.
+ before(async () => {
+ await load();
+ });
+
+ it('exposes the parameterized commands, id-addressable', () => {
+ assert.deepStrictEqual(
+ promptedCommands.map((command) => command.id),
+ [
+ 'numberRange', 'floatRange', 'stringLength', 'dateBetween',
+ 'wordsCount', 'sentencesCount', 'paragraphsCount',
+ 'uuidFormat', 'passwordOptions', 'phoneFormat',
+ 'fromTemplate', 'fromPattern', 'sequence',
+ ],
+ );
+ for (const command of promptedCommands) {
+ assert.strictEqual(getPromptedCommand(command.id), command);
+ assert.ok(command.label.length > 0, `'${command.id}' needs a label`);
+ assert.ok(command.group.length > 0, `'${command.id}' needs a group`);
+ assert.ok(command.steps.length > 0, `'${command.id}' needs at least one input step`);
+ }
+ assert.strictEqual(getPromptedCommand('nope'), undefined);
+ });
+
+ it('every step carries its prompt texts and a valid prefill fallback', () => {
+ for (const command of promptedCommands) {
+ for (const step of command.steps) {
+ assert.ok(step.key.length > 0, `${command.id} step needs a key`);
+ assert.ok(step.prompt.length > 0, `${command.id}.${step.key} needs a prompt`);
+ if (step.kind === 'pick') {
+ assert.ok(step.options.length >= 2, `${command.id}.${step.key} needs at least two options`);
+ const values = step.options.map((option) => option.value);
+ assert.strictEqual(new Set(values).size, values.length, `${command.id}.${step.key} option values must be unique`);
+ assert.ok(values.includes(step.fallback), `${command.id}.${step.key} fallback '${step.fallback}' must be one of its options`);
+ for (const option of step.options) {
+ assert.ok(option.label.length > 0 && option.detail.length > 0,
+ `${command.id}.${step.key} option '${option.value}' needs a label and a detail`);
+ }
+ } else {
+ assert.ok(step.placeholder.length > 0, `${command.id}.${step.key} needs a placeholder`);
+ assert.strictEqual(step.validate(step.fallback, {}), undefined,
+ `${command.id}.${step.key} fallback '${step.fallback}' must pass its own validation`);
+ }
+ }
+ }
+ });
+
+ it('every command defines exactly one rendering surface — render or createRender', () => {
+ for (const command of promptedCommands) {
+ const surfaces = [ command.render, command.createRender ].filter(Boolean).length;
+ assert.strictEqual(surfaces, 1, `'${command.id}' must define exactly one of render/createRender`);
+ }
+ });
+
+ it('range commands validate their fallbacks as a pair (max fallback vs min fallback)', () => {
+ for (const id of [ 'numberRange', 'floatRange' ]) {
+ const [ min, max ] = inputSteps(id);
+ assert.strictEqual(max.validate(max.fallback, { min: min.fallback }), undefined,
+ `${id} fallbacks must form a valid range`);
+ }
+ const [ from, to ] = inputSteps('dateBetween');
+ assert.strictEqual(to.validate(to.fallback, { from: from.fallback }), undefined,
+ 'dateBetween fallbacks must form a valid range');
+ });
+});
+
+describe('prompted — numberRange validation', () => {
+ const [ min, max ] = inputSteps('numberRange');
+
+ it('min accepts integers (negative and padded included)', () => {
+ for (const input of [ '0', '42', '-5', ' 7 ' ]) {
+ assert.strictEqual(min.validate(input, {}), undefined, `'${input}' should be a valid min`);
+ }
+ });
+
+ it('min rejects empty, non-numeric, fractional, and unsafe-magnitude input', () => {
+ for (const input of [ '', ' ', 'abc', '1.5', '9007199254740993' ]) {
+ assert.ok(min.validate(input, {}), `'${input}' should be rejected with a message`);
+ }
+ });
+
+ it('max enforces min ≤ max against the already-entered min', () => {
+ assert.ok(max.validate('5', { min: '10' }), 'max below min must be rejected');
+ assert.strictEqual(max.validate('10', { min: '10' }), undefined, 'max equal to min is a valid (pinned) range');
+ assert.strictEqual(max.validate('11', { min: '10' }), undefined);
+ });
+});
+
+describe('prompted — floatRange validation', () => {
+ const [ min, max ] = inputSteps('floatRange');
+
+ it('min accepts any finite number', () => {
+ for (const input of [ '0', '0.5', '-1.25', '3', ' 2.5 ' ]) {
+ assert.strictEqual(min.validate(input, {}), undefined, `'${input}' should be a valid min`);
+ }
+ });
+
+ it('min rejects empty and non-numeric input', () => {
+ for (const input of [ '', ' ', 'x', 'NaN', 'Infinity' ]) {
+ assert.ok(min.validate(input, {}), `'${input}' should be rejected with a message`);
+ }
+ });
+
+ it('max enforces min ≤ max', () => {
+ assert.ok(max.validate('0.5', { min: '1' }), 'max below min must be rejected');
+ assert.strictEqual(max.validate('1', { min: '1' }), undefined);
+ });
+
+ it('max rejects a range too narrow to contain a 2-decimal value (mirrors the faker draw)', () => {
+ // faker.number.float({ fractionDigits: 2 }) draws int(ceil(min*100)..floor(max*100)) / 100 and
+ // throws when that integer range is empty — the validator must reject exactly those inputs.
+ assert.ok(max.validate('0.002', { min: '0.001' }), 'no multiple of 0.01 lies in [0.001, 0.002]');
+ assert.strictEqual(max.validate('0.005', { min: '0' }), undefined, '0.00 lies in [0, 0.005]');
+ assert.strictEqual(max.validate('0.01', { min: '0.001' }), undefined, '0.01 lies in [0.001, 0.01]');
+ });
+});
+
+describe('prompted — stringLength validation', () => {
+ const [ length ] = inputSteps('stringLength');
+
+ it('accepts 1 through 1000', () => {
+ for (const input of [ '1', '15', '1000' ]) {
+ assert.strictEqual(length.validate(input, {}), undefined, `'${input}' should be a valid length`);
+ }
+ });
+
+ it('rejects out-of-range and non-integer input', () => {
+ for (const input of [ '0', '1001', '-3', '2.5', 'abc', '' ]) {
+ assert.ok(length.validate(input, {}), `'${input}' should be rejected with a message`);
+ }
+ });
+});
+
+describe('prompted — dateBetween validation', () => {
+ const [ from, to ] = inputSteps('dateBetween');
+
+ it('from accepts YYYY-MM-DD and full ISO 8601 (padding tolerated)', () => {
+ for (const input of [ '2020-01-01', ' 2020-01-01 ', '2026-07-02T12:00:00Z', '2026-07-02T12:00', '2026-07-02T12:00:00.500+02:00' ]) {
+ assert.strictEqual(from.validate(input, {}), undefined, `'${input}' should be a valid date`);
+ }
+ });
+
+ it('from rejects empty, non-date, and impossible-calendar input', () => {
+ for (const input of [ '', ' ', 'yesterday', '01/02/2026', '20260702', '5', '2026-13-01', '2026-02-31' ]) {
+ assert.ok(from.validate(input, {}), `'${input}' should be rejected with a message`);
+ }
+ });
+
+ it('to enforces from ≤ to against the already-entered from', () => {
+ assert.ok(to.validate('2019-12-31', { from: '2020-01-01' }), 'to before from must be rejected');
+ assert.strictEqual(to.validate('2020-01-01', { from: '2020-01-01' }), undefined, 'to equal to from pins the date');
+ assert.strictEqual(to.validate('2020-01-02', { from: '2020-01-01' }), undefined);
+ });
+});
+
+describe('prompted — count validation (wordsCount / sentencesCount / paragraphsCount)', () => {
+ for (const id of [ 'wordsCount', 'sentencesCount', 'paragraphsCount' ]) {
+ describe(id, () => {
+ const [ count ] = inputSteps(id);
+
+ it('accepts 1 through 100 (padding tolerated)', () => {
+ for (const input of [ '1', '3', '100', ' 42 ' ]) {
+ assert.strictEqual(count.validate(input, {}), undefined, `'${input}' should be a valid count`);
+ }
+ });
+
+ it('rejects out-of-range and non-integer input', () => {
+ for (const input of [ '0', '101', '-3', '2.5', 'abc', '' ]) {
+ assert.ok(count.validate(input, {}), `'${input}' should be rejected with a message`);
+ }
+ });
+ });
+ }
+});
+
+describe('prompted — pick steps (uuidFormat / passwordOptions / phoneFormat)', () => {
+ it('carries the S5 format variants with their catalog groups', () => {
+ assert.deepStrictEqual(
+ [ 'uuidFormat', 'passwordOptions', 'phoneFormat' ].map((id) => {
+ const { label, group } = getPromptedCommand(id)!;
+ return { id, label, group };
+ }),
+ [
+ { id: 'uuidFormat', label: 'UUID (Format…)', group: 'IDs' },
+ { id: 'passwordOptions', label: 'Password (Options…)', group: 'Security' },
+ { id: 'phoneFormat', label: 'Phone (Format…)', group: 'Identity' },
+ ],
+ );
+ });
+
+ it('uuidFormat asks for one of the five formats, defaulting to lowercase', () => {
+ assert.strictEqual(getPromptedCommand('uuidFormat')!.steps.length, 1);
+ const [ format ] = pickSteps('uuidFormat');
+ assert.strictEqual(format.key, 'format');
+ assert.deepStrictEqual(
+ format.options.map((option) => option.value),
+ [ 'lowercase', 'uppercase', 'braced', 'noDashes', 'uppercaseNoDashes' ],
+ );
+ assert.strictEqual(format.fallback, 'lowercase');
+ });
+
+ it('passwordOptions asks for a length box, then a symbols yes/no pick', () => {
+ const steps = getPromptedCommand('passwordOptions')!.steps;
+ assert.deepStrictEqual(
+ steps.map((step) => [ step.key, step.kind === 'pick' ? 'pick' : 'input' ]),
+ [ [ 'length', 'input' ], [ 'symbols', 'pick' ] ],
+ );
+ const [ symbols ] = pickSteps('passwordOptions');
+ assert.deepStrictEqual(symbols.options.map((option) => option.value), [ 'no', 'yes' ]);
+ assert.strictEqual(symbols.fallback, 'no');
+ });
+
+ it('phoneFormat asks for one of the three faker phone styles, defaulting to human', () => {
+ assert.strictEqual(getPromptedCommand('phoneFormat')!.steps.length, 1);
+ const [ style ] = pickSteps('phoneFormat');
+ assert.strictEqual(style.key, 'style');
+ assert.deepStrictEqual(style.options.map((option) => option.value), [ 'human', 'national', 'international' ]);
+ assert.strictEqual(style.fallback, 'human');
+ });
+});
+
+describe('prompted — passwordOptions length validation', () => {
+ const [ length ] = inputSteps('passwordOptions');
+
+ it('accepts 8 through 128 (padding tolerated)', () => {
+ for (const input of [ '8', '15', '128', ' 64 ' ]) {
+ assert.strictEqual(length.validate(input, {}), undefined, `'${input}' should be a valid length`);
+ }
+ });
+
+ it('rejects out-of-range and non-integer input', () => {
+ for (const input of [ '7', '129', '0', '-8', '2.5', 'abc', '' ]) {
+ assert.ok(length.validate(input, {}), `'${input}' should be rejected with a message`);
+ }
+ });
+});
+
+describe('prompted — fromTemplate / fromPattern (custom input)', function () {
+ this.timeout(15000);
+
+ // validate() proves free-form input by test-rendering it through faker.
+ before(async () => {
+ await load();
+ });
+
+ it('carries the two custom-input commands, one box each', () => {
+ assert.deepStrictEqual(
+ [ 'fromTemplate', 'fromPattern' ].map((id) => {
+ const { label, group, steps } = getPromptedCommand(id)!;
+ return { id, label, group, keys: steps.map((step) => step.key) };
+ }),
+ [
+ { id: 'fromTemplate', label: 'From Template…', group: 'Custom', keys: [ 'template' ] },
+ { id: 'fromPattern', label: 'From Pattern…', group: 'Custom', keys: [ 'pattern' ] },
+ ],
+ );
+ });
+
+ it('prefills the documented examples as fallbacks', () => {
+ const [ template ] = inputSteps('fromTemplate');
+ assert.strictEqual(template.fallback, '{{person.firstName}} <{{internet.email}}>');
+ const [ pattern ] = inputSteps('fromPattern');
+ assert.strictEqual(pattern.fallback, '[A-Z]{3}-[0-9]{4}');
+ });
+
+ it('documents the limited regex subset in the pattern box placeholder', () => {
+ const [ pattern ] = inputSteps('fromPattern');
+ assert.match(pattern.placeholder, /limited regex subset/i);
+ });
+
+ it('template box accepts anything that renders — placeholders, call args, plain text', () => {
+ const [ template ] = inputSteps('fromTemplate');
+ for (const input of [ '{{person.firstName}} <{{internet.email}}>', 'x-{{string.numeric(3)}}', 'plain text', ' {{internet.email}} ' ]) {
+ assert.strictEqual(template.validate(input, {}), undefined, `'${input}' should render`);
+ }
+ });
+
+ it('template box rejects empty input and unresolvable expressions, offering a working example', () => {
+ const [ template ] = inputSteps('fromTemplate');
+ for (const input of [ '', ' ', '{{nope.nope}}', '{{person.nope}}' ]) {
+ const message = template.validate(input, {});
+ assert.ok(message, `'${input}' should be rejected with a message`);
+ assert.ok(message!.includes('{{person.firstName}}'), `the message must carry a working example (got: ${message})`);
+ }
+ });
+
+ it('pattern box accepts the regex subset faker renders', () => {
+ const [ pattern ] = inputSteps('fromPattern');
+ for (const input of [ '[A-Z]{3}-[0-9]{4}', 'a{2,4}', 'abc?', '[0-9]*x', ' [a-f]{8} ' ]) {
+ assert.strictEqual(pattern.validate(input, {}), undefined, `'${input}' should render`);
+ }
+ });
+
+ it('pattern box rejects empty input and throwing patterns, offering a working example', () => {
+ const [ pattern ] = inputSteps('fromPattern');
+ for (const input of [ '', ' ', '[z-a]', 'a{4,2}' ]) {
+ const message = pattern.validate(input, {});
+ assert.ok(message, `'${input}' should be rejected with a message`);
+ assert.ok(message!.includes('[A-Z]{3}-[0-9]{4}'), `the message must carry a working example (got: ${message})`);
+ }
+ });
+});
+
+describe('prompted — formatUuid (the pure uuid post-transform)', () => {
+ const SAMPLE = '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d';
+
+ it('renders each of the five formats exactly', () => {
+ assert.strictEqual(formatUuid(SAMPLE, 'lowercase'), SAMPLE);
+ assert.strictEqual(formatUuid(SAMPLE, 'uppercase'), '9B1DEB4D-3B7D-4BAD-9BDD-2B0D7B3DCB6D');
+ assert.strictEqual(formatUuid(SAMPLE, 'braced'), '{9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d}');
+ assert.strictEqual(formatUuid(SAMPLE, 'noDashes'), '9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d');
+ assert.strictEqual(formatUuid(SAMPLE, 'uppercaseNoDashes'), '9B1DEB4D3B7D4BAD9BDD2B0D7B3DCB6D');
+ });
+
+ it('leaves the uuid untouched for an unknown format (defensive default)', () => {
+ assert.strictEqual(formatUuid(SAMPLE, 'nope'), SAMPLE);
+ });
+});
+
+describe('prompted — rendering (one-off Generator through toGenerator)', function () {
+ this.timeout(15000);
+
+ before(async () => {
+ await load();
+ });
+
+ it('wraps a command as a Generator carrying its id/label/group', () => {
+ const command = getPromptedCommand('numberRange')!;
+ const generator = toGenerator(command, { min: '1', max: '2' });
+ assert.strictEqual(generator.id, command.id);
+ assert.strictEqual(generator.label, command.label);
+ assert.strictEqual(generator.group, command.group);
+ });
+
+ it('numberRange draws integers within [min, max], as strings', () => {
+ seed(20260702);
+ const generator = toGenerator(getPromptedCommand('numberRange')!, { min: '1', max: '3' });
+ for (let i = 0; i < 50; i++) {
+ const value = Number(generator.generate());
+ assert.ok(Number.isInteger(value) && value >= 1 && value <= 3, `${value} outside [1, 3]`);
+ }
+ });
+
+ it('numberRange with min = max pins the value (negative included)', () => {
+ const generator = toGenerator(getPromptedCommand('numberRange')!, { min: '-3', max: '-3' });
+ assert.strictEqual(generator.generate(), '-3');
+ });
+
+ it('floatRange renders two fraction digits, matching the catalog float style', () => {
+ const pinned = toGenerator(getPromptedCommand('floatRange')!, { min: '2', max: '2' });
+ assert.strictEqual(pinned.generate(), '2.00');
+ seed(20260702);
+ const ranged = toGenerator(getPromptedCommand('floatRange')!, { min: '0.5', max: '1.5' });
+ for (let i = 0; i < 50; i++) {
+ const value = ranged.generate();
+ assert.match(value, /^\d+\.\d{2}$/, `'${value}' is not a 2-decimal rendering`);
+ assert.ok(Number(value) >= 0.5 && Number(value) <= 1.5, `${value} outside [0.5, 1.5]`);
+ }
+ });
+
+ it('stringLength draws exactly N alphanumeric characters', () => {
+ seed(20260702);
+ const generator = toGenerator(getPromptedCommand('stringLength')!, { length: '12' });
+ for (let i = 0; i < 20; i++) {
+ assert.match(generator.generate(), /^[A-Za-z0-9]{12}$/);
+ }
+ });
+
+ it('dateBetween draws a date within [from, to], full ISO by default', () => {
+ seed(20260702);
+ const generator = toGenerator(getPromptedCommand('dateBetween')!, { from: '2020-01-01', to: '2020-12-31' });
+ for (let i = 0; i < 25; i++) {
+ const value = generator.generate();
+ assert.match(value, /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/, `'${value}' is not full ISO`);
+ const drawn = new Date(value).getTime();
+ assert.ok(
+ drawn >= Date.parse('2020-01-01T00:00:00Z') && drawn <= Date.parse('2020-12-31T00:00:00Z'),
+ `${value} outside [from, to]`,
+ );
+ }
+ });
+
+ it('dateBetween renders through generate({ dateFormat }) — the pipeline threads the setting in', () => {
+ // A pinned range (from = to) makes each rendering exact.
+ const generator = toGenerator(getPromptedCommand('dateBetween')!, { from: '2020-06-15', to: '2020-06-15' });
+ assert.strictEqual(generator.generate({ dateFormat: 'isoDate' }), '2020-06-15');
+ assert.strictEqual(generator.generate({ dateFormat: 'unixSeconds' }), String(Date.parse('2020-06-15T00:00:00Z') / 1000));
+ assert.strictEqual(generator.generate(), '2020-06-15T00:00:00.000Z');
+ });
+
+ it('wordsCount draws exactly N space-separated words (boundaries 1 and 100 included)', () => {
+ seed(20260702);
+ for (const count of [ 1, 5, 100 ]) {
+ const value = toGenerator(getPromptedCommand('wordsCount')!, { count: String(count) }).generate();
+ const words = value.split(' ');
+ assert.strictEqual(words.length, count, `'${value.slice(0, 40)}…' should hold ${count} words`);
+ for (const word of words) { assert.match(word, /^\S+$/, 'every word must be non-empty'); }
+ }
+ });
+
+ it('sentencesCount draws exactly N period-terminated sentences', () => {
+ seed(20260702);
+ for (const count of [ 1, 4 ]) {
+ const value = toGenerator(getPromptedCommand('sentencesCount')!, { count: String(count) }).generate();
+ assert.strictEqual((value.match(/\./g) ?? []).length, count, `'${value}' should hold ${count} sentences`);
+ assert.match(value, /^[A-Z]/, 'a sentence starts capitalized');
+ assert.ok(value.endsWith('.'), 'the last sentence ends with a period');
+ }
+ });
+
+ it('paragraphsCount draws exactly N newline-separated paragraphs', () => {
+ seed(20260702);
+ for (const count of [ 1, 3 ]) {
+ const value = toGenerator(getPromptedCommand('paragraphsCount')!, { count: String(count) }).generate();
+ const paragraphs = value.split('\n');
+ assert.strictEqual(paragraphs.length, count, `${count} paragraphs expected`);
+ for (const paragraph of paragraphs) { assert.match(paragraph, /\./, 'each paragraph holds sentences'); }
+ }
+ });
+
+ it('uuidFormat draws a fresh uuid and renders the picked format', () => {
+ seed(20260702);
+ const braced = toGenerator(getPromptedCommand('uuidFormat')!, { format: 'braced' });
+ assert.match(braced.generate(), /^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/);
+ const compactUpper = toGenerator(getPromptedCommand('uuidFormat')!, { format: 'uppercaseNoDashes' });
+ assert.match(compactUpper.generate(), /^[0-9A-F]{32}$/);
+ const plain = toGenerator(getPromptedCommand('uuidFormat')!, { format: 'lowercase' });
+ assert.match(plain.generate(), /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
+ assert.notStrictEqual(plain.generate(), plain.generate(), 'each generate() draws a fresh uuid');
+ });
+
+ it('passwordOptions draws exactly N characters from the picked pool', () => {
+ seed(20260702);
+ const plain = toGenerator(getPromptedCommand('passwordOptions')!, { length: '12', symbols: 'no' });
+ for (let i = 0; i < 10; i++) { assert.match(plain.generate(), /^[A-Za-z0-9]{12}$/); }
+ const symbolic = toGenerator(getPromptedCommand('passwordOptions')!, { length: '64', symbols: 'yes' });
+ const drawn = Array.from({ length: 3 }, () => symbolic.generate());
+ for (const value of drawn) { assert.match(value, /^[A-Za-z0-9!@#$%^&*]{64}$/); }
+ assert.match(drawn.join(''), /[!@#$%^&*]/, 'the symbol pool must actually be drawn from');
+ });
+
+ it('phoneFormat renders the picked faker style', () => {
+ seed(20260702);
+ const human = toGenerator(getPromptedCommand('phoneFormat')!, { style: 'human' });
+ assert.match(human.generate(), /^[0-9\s().x-]+$/, 'human style: digits with human punctuation');
+ const national = toGenerator(getPromptedCommand('phoneFormat')!, { style: 'national' });
+ assert.match(national.generate(), /^\(\d{3}\) \d{3}-\d{4}$/);
+ const international = toGenerator(getPromptedCommand('phoneFormat')!, { style: 'international' });
+ assert.match(international.generate(), /^\+\d{8,15}$/);
+ });
+
+ it('fromTemplate re-renders the mustache template with fresh draws each call', () => {
+ seed(20260702);
+ const generator = toGenerator(getPromptedCommand('fromTemplate')!, { template: 'x-{{string.numeric(3)}}' });
+ for (let i = 0; i < 10; i++) { assert.match(generator.generate(), /^x-\d{3}$/); }
+ const fresh = toGenerator(getPromptedCommand('fromTemplate')!, { template: '{{string.alphanumeric(12)}}' });
+ assert.notStrictEqual(fresh.generate(), fresh.generate(), 'each generate() re-renders with fresh values');
+ });
+
+ it('fromTemplate passes plain text through unchanged', () => {
+ const generator = toGenerator(getPromptedCommand('fromTemplate')!, { template: 'plain text' });
+ assert.strictEqual(generator.generate(), 'plain text');
+ });
+
+ it('fromPattern draws a string matching the entered pattern', () => {
+ seed(20260702);
+ const generator = toGenerator(getPromptedCommand('fromPattern')!, { pattern: '[A-Z]{3}-[0-9]{4}' });
+ for (let i = 0; i < 10; i++) { assert.match(generator.generate(), /^[A-Z]{3}-\d{4}$/); }
+ });
+
+ it('fromTemplate and fromPattern reproduce under the same seed', () => {
+ const template = toGenerator(getPromptedCommand('fromTemplate')!, { template: '{{string.alphanumeric(16)}}' });
+ const pattern = toGenerator(getPromptedCommand('fromPattern')!, { pattern: '[a-z0-9]{16}' });
+ seed(7);
+ const first = [ template.generate(), pattern.generate() ];
+ seed(7);
+ const second = [ template.generate(), pattern.generate() ];
+ assert.deepStrictEqual(first, second);
+ });
+
+ it('draws a fresh value on each generate() call — no memoization', () => {
+ seed(4242);
+ const generator = toGenerator(getPromptedCommand('numberRange')!, { min: '1', max: '1000000000' });
+ const values = new Set(Array.from({ length: 5 }, () => generator.generate()));
+ assert.ok(values.size > 1, 'expected distinct values across repeated calls');
+ });
+
+ it('draws through the shared faker accessor — the same seed reproduces the same sequence', () => {
+ const generator = toGenerator(getPromptedCommand('stringLength')!, { length: '20' });
+ seed(7);
+ const first = [ generator.generate(), generator.generate() ];
+ seed(7);
+ const second = [ generator.generate(), generator.generate() ];
+ assert.deepStrictEqual(first, second);
+ });
+});
+
+describe('prompted — sequence validation', () => {
+ const [ start, step ] = inputSteps('sequence');
+
+ it('start and step accept whole numbers (negative, zero, padded included)', () => {
+ for (const box of [ start, step ]) {
+ for (const input of [ '1', '0', '-3', ' 7 ', '100000' ]) {
+ assert.strictEqual(box.validate(input, {}), undefined, `'${input}' should be a valid whole number`);
+ }
+ }
+ });
+
+ it('start and step reject empty, non-numeric, fractional, and unsafe-magnitude input', () => {
+ for (const box of [ start, step ]) {
+ for (const input of [ '', ' ', 'abc', '1.5', '9007199254740993' ]) {
+ assert.ok(box.validate(input, {}), `'${input}' should be rejected with a message`);
+ }
+ }
+ });
+});
+
+describe('prompted — sequence rendering (a stateful counter per insert)', () => {
+ it('carries the command with its group, one box per parameter', () => {
+ const command = getPromptedCommand('sequence')!;
+ assert.strictEqual(command.label, 'Sequence (Start/Step…)');
+ assert.strictEqual(command.group, 'Numbers');
+ assert.deepStrictEqual(command.steps.map((step) => step.key), [ 'start', 'step' ]);
+ });
+
+ it('counts up from start by step across generate() calls — one insert, one running counter', () => {
+ const generator = toGenerator(getPromptedCommand('sequence')!, { start: '10', step: '5' });
+ assert.deepStrictEqual([ generator.generate(), generator.generate(), generator.generate() ], [ '10', '15', '20' ]);
+ });
+
+ it('supports negative and zero steps', () => {
+ const down = toGenerator(getPromptedCommand('sequence')!, { start: '5', step: '-2' });
+ assert.deepStrictEqual([ down.generate(), down.generate(), down.generate() ], [ '5', '3', '1' ]);
+ const flat = toGenerator(getPromptedCommand('sequence')!, { start: '4', step: '0' });
+ assert.deepStrictEqual([ flat.generate(), flat.generate() ], [ '4', '4' ]);
+ });
+
+ it('every toGenerator() wrap restarts at start — each insert operation counts fresh', () => {
+ const first = toGenerator(getPromptedCommand('sequence')!, { start: '10', step: '5' });
+ assert.deepStrictEqual([ first.generate(), first.generate() ], [ '10', '15' ]);
+ const second = toGenerator(getPromptedCommand('sequence')!, { start: '10', step: '5' });
+ assert.strictEqual(second.generate(), '10', 'a fresh wrap must not continue the previous counter');
+ });
+
+ it('ignores GenerateOptions — a sequence value has no date to format', () => {
+ const generator = toGenerator(getPromptedCommand('sequence')!, { start: '1', step: '1' });
+ assert.strictEqual(generator.generate({ dateFormat: 'unixSeconds' }), '1');
+ });
+
+ it('needs no randomness — the sequence is identical with or without a seed', () => {
+ seed(1);
+ const seeded = toGenerator(getPromptedCommand('sequence')!, { start: '3', step: '3' });
+ const first = [ seeded.generate(), seeded.generate() ];
+ const unseeded = toGenerator(getPromptedCommand('sequence')!, { start: '3', step: '3' });
+ assert.deepStrictEqual([ unseeded.generate(), unseeded.generate() ], first);
+ });
+});
diff --git a/src/test/quote/escape.test.ts b/src/test/quote/escape.test.ts
new file mode 100644
index 0000000..f0c68c6
--- /dev/null
+++ b/src/test/quote/escape.test.ts
@@ -0,0 +1,63 @@
+import * as assert from 'assert';
+
+import { buildBlocks, InsertOptions } from '../../formatter';
+import type { Generator } from '../../catalog';
+
+// The formatter's private wrap() gained a second escape style (sqlDouble). Rather than reach into the
+// private function, drive it through the public buildBlocks — that also proves the new
+// InsertOptions.escape field actually threads through to the wrapping. A fixed-value generator makes
+// the output exact and deterministic.
+function fixed(value: string): Generator {
+ return { id: 't', label: 'T', group: 'G', generate: () => value };
+}
+
+const BASE: InsertOptions = {
+ quote: "'",
+ newline: '',
+ uniquePerCursor: false,
+ bulkCount: 1,
+ outputFormat: 'plain',
+};
+
+describe('formatter escape — backslash (default)', () => {
+ it("backslash-escapes the quote char: O'Brien → 'O\\'Brien'", () => {
+ const [ block ] = buildBlocks(1, fixed("O'Brien"), { ...BASE, quote: "'", escape: 'backslash' });
+ assert.strictEqual(block, "'O\\'Brien'");
+ });
+
+ it('escape defaults to backslash when the field is omitted', () => {
+ const [ block ] = buildBlocks(1, fixed("O'Brien"), BASE);
+ assert.strictEqual(block, "'O\\'Brien'");
+ });
+
+ it('a double-quote value in double quotes is backslash-escaped', () => {
+ const [ block ] = buildBlocks(1, fixed('a"b'), { ...BASE, quote: '"', escape: 'backslash' });
+ assert.strictEqual(block, '"a\\"b"');
+ });
+
+ it('doubles a literal backslash in the value (a\\b → a\\\\b)', () => {
+ // The counterpart of the sqlDouble "leaves backslashes untouched" test below: backslash style must
+ // escape the escape character itself, or the wrapped literal would swallow the character after it.
+ const [ block ] = buildBlocks(1, fixed('a\\b'), { ...BASE, quote: "'", escape: 'backslash' });
+ assert.strictEqual(block, "'a\\\\b'");
+ });
+});
+
+describe('formatter escape — sqlDouble', () => {
+ it("doubles the quote char, no backslash: O'Brien → 'O''Brien'", () => {
+ const [ block ] = buildBlocks(1, fixed("O'Brien"), { ...BASE, quote: "'", escape: 'sqlDouble' });
+ assert.strictEqual(block, "'O''Brien'");
+ });
+
+ it('leaves backslashes untouched (SQL does not backslash-escape)', () => {
+ const [ block ] = buildBlocks(1, fixed('a\\b'), { ...BASE, quote: "'", escape: 'sqlDouble' });
+ assert.strictEqual(block, "'a\\b'");
+ });
+});
+
+describe('formatter escape — no quote', () => {
+ it('quote="" returns the bare value even with an escape style set', () => {
+ const [ block ] = buildBlocks(1, fixed("O'Brien"), { ...BASE, quote: '', escape: 'sqlDouble' });
+ assert.strictEqual(block, "O'Brien");
+ });
+});
diff --git a/src/test/quote/policy.test.ts b/src/test/quote/policy.test.ts
new file mode 100644
index 0000000..71a9d38
--- /dev/null
+++ b/src/test/quote/policy.test.ts
@@ -0,0 +1,42 @@
+import * as assert from 'assert';
+
+import { resolveQuotePolicy } from '../../quotePolicy';
+
+// resolveQuotePolicy IS the automatic-quoting logic: given a languageId and withQuote it picks the
+// quote char + escape style. Two buckets: (1) SQL keeps single quotes but uses sqlDouble escaping;
+// (2) EVERYTHING else — JS/TS, Python, JSON, Go, unlisted, no editor — gets double quotes with
+// backslash escaping. withQuote=false means no wrapping at all.
+const ON = { withQuote: true };
+
+describe('resolveQuotePolicy — SQL bucket', () => {
+ for (const id of [ 'sql', 'mysql', 'pgsql', 'plsql', 'sqlite' ]) {
+ it(`${id} uses single quotes with sqlDouble escaping`, () => {
+ const policy = resolveQuotePolicy(id, ON);
+ assert.strictEqual(policy.quote, "'");
+ assert.strictEqual(policy.escape, 'sqlDouble');
+ });
+ }
+});
+
+describe('resolveQuotePolicy — everything else gets double quotes', () => {
+ for (const id of [ 'json', 'jsonc', 'go', 'java', 'cpp', 'csharp', 'rust', 'javascript', 'typescript', 'python', 'ruby', 'php', 'some-exotic-lang' ]) {
+ it(`${id} uses double quotes with backslash escaping`, () => {
+ const policy = resolveQuotePolicy(id, ON);
+ assert.strictEqual(policy.quote, '"');
+ assert.strictEqual(policy.escape, 'backslash');
+ });
+ }
+
+ it('undefined languageId (no active editor) → double quotes', () => {
+ const policy = resolveQuotePolicy(undefined, ON);
+ assert.strictEqual(policy.quote, '"');
+ assert.strictEqual(policy.escape, 'backslash');
+ });
+});
+
+describe('resolveQuotePolicy — withQuote off', () => {
+ it('withQuote=false → no wrapping, regardless of language', () => {
+ assert.strictEqual(resolveQuotePolicy('json', { withQuote: false }).quote, '');
+ assert.strictEqual(resolveQuotePolicy('sql', { withQuote: false }).quote, '');
+ });
+});
diff --git a/src/test/randomize/detect.test.ts b/src/test/randomize/detect.test.ts
new file mode 100644
index 0000000..9c95f2b
--- /dev/null
+++ b/src/test/randomize/detect.test.ts
@@ -0,0 +1,122 @@
+import * as assert from 'assert';
+
+import { detect, shapeTimestamp, shapeUuid } from '../../randomize';
+
+// detect(text) is the type-detection half of type-aware replace (Randomize Selection): a selection
+// that IS an unambiguous email / UUID / ISO date / ISO timestamp gets a fresh REALISTIC fake of the
+// same type instead of a character scramble — a scrambled uuid isn't valid hex and a scrambled date
+// isn't a date. Detection is deliberately conservative: anchored, whole-string, no trimming — anything
+// not matched exactly falls back to the format-preserving scramble, the safe default. Numbers are
+// deliberately NOT detected: the scramble already yields a fresh same-shape number (digits redraw,
+// sign and decimal point stay), which IS the typed replacement for a number.
+
+describe('detect — typed-value detection for Randomize Selection', () => {
+ it('detects a whole-string email', () => {
+ for (const text of [ 'jane.doe+prod@acme.com', 'a@b.co', 'A_b%x-1@sub.domain.org' ]) {
+ assert.strictEqual(detect(text), 'email', `'${text}' should detect as email`);
+ }
+ });
+
+ it('rejects near-emails — no TLD, spaces, padding, or surrounding text', () => {
+ for (const text of [ 'a@b', '@x.io', 'jane doe@x.io', ' a@b.io', 'a@b.io ', 'a@b.io\n', 'mail me at a@b.io', 'not an email' ]) {
+ assert.strictEqual(detect(text), undefined, `'${text}' must fall back to the scramble`);
+ }
+ });
+
+ it('detects a dashed uuid in either case, braced or bare', () => {
+ for (const text of [
+ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
+ '9B1DEB4D-3B7D-4BAD-9BDD-2B0D7B3DCB6D',
+ '{9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d}',
+ '{9B1DEB4D-3B7D-4BAD-9BDD-2B0D7B3DCB6D}',
+ '12345678-1234-1234-1234-123456789012',
+ ]) {
+ assert.strictEqual(detect(text), 'uuid', `'${text}' should detect as uuid`);
+ }
+ });
+
+ it('rejects near-uuids — half braces, non-hex, missing dashes, trailing content', () => {
+ for (const text of [
+ '{9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
+ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d}',
+ 'gb1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d',
+ '9b1deb4d3b7d4bad9bdd2b0d7b3dcb6d', // a 32-hex hash-alike — stays a scramble by design
+ '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d x',
+ ]) {
+ assert.strictEqual(detect(text), undefined, `'${text}' must fall back to the scramble`);
+ }
+ });
+
+ it('detects a calendar-valid ISO date (leap day included)', () => {
+ for (const text of [ '2024-02-29', '2026-07-02', '0001-01-01' ]) {
+ assert.strictEqual(detect(text), 'isoDate', `'${text}' should detect as isoDate`);
+ }
+ });
+
+ it('rejects impossible or unpadded calendar dates — V8 would roll them over, we must not', () => {
+ for (const text of [ '2023-02-29', '2026-13-01', '2026-00-01', '2026-07-32', '2026-07-00', '2026-7-2', '2026-07-02 ' ]) {
+ assert.strictEqual(detect(text), undefined, `'${text}' must fall back to the scramble`);
+ }
+ });
+
+ it('detects a UTC ISO timestamp, with or without milliseconds', () => {
+ for (const text of [ '2026-07-02T12:34:56Z', '2026-07-02T12:34:56.7Z', '2026-07-02T12:34:56.789Z' ]) {
+ assert.strictEqual(detect(text), 'isoTimestamp', `'${text}' should detect as isoTimestamp`);
+ }
+ });
+
+ it('rejects near-timestamps — missing Z, offsets, impossible time or date fields', () => {
+ for (const text of [
+ '2026-07-02T12:34:56',
+ '2026-07-02T12:34:56+02:00',
+ '2026-07-02T24:00:00Z',
+ '2026-07-02T12:60:00Z',
+ '2026-07-02T12:34:61Z',
+ '2026-02-31T12:34:56Z',
+ '2026-07-02T12:34:56.1234Z',
+ ]) {
+ assert.strictEqual(detect(text), undefined, `'${text}' must fall back to the scramble`);
+ }
+ });
+
+ it('deliberately leaves numbers to the format-preserving scramble', () => {
+ for (const text of [ '42', '3.14', '-7', '+1.5', '007' ]) {
+ assert.strictEqual(detect(text), undefined, `'${text}' is served by the scramble already`);
+ }
+ });
+
+ it('detects nothing in plain or empty text', () => {
+ for (const text of [ '', 'hello', 'Alice Smith', '+63 (917) 555-0142' ]) {
+ assert.strictEqual(detect(text), undefined);
+ }
+ });
+});
+
+describe('shapeUuid — a fresh uuid dressed like the original', () => {
+ const FRESH = '0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9';
+
+ it('matches the original case: uppercase originals get an uppercase redraw', () => {
+ assert.strictEqual(shapeUuid(FRESH, '9B1DEB4D-3B7D-4BAD-9BDD-2B0D7B3DCB6D'), FRESH.toUpperCase());
+ assert.strictEqual(shapeUuid(FRESH, '9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'), FRESH);
+ });
+
+ it('keeps the original braces', () => {
+ assert.strictEqual(shapeUuid(FRESH, '{9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d}'), `{${FRESH}}`);
+ assert.strictEqual(shapeUuid(FRESH, '{9B1DEB4D-3B7D-4BAD-9BDD-2B0D7B3DCB6D}'), `{${FRESH.toUpperCase()}}`);
+ });
+
+ it('defaults to lowercase when the original carries no hex letters or mixes case', () => {
+ assert.strictEqual(shapeUuid(FRESH, '12345678-1234-1234-1234-123456789012'), FRESH);
+ assert.strictEqual(shapeUuid(FRESH, '9B1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d'), FRESH);
+ });
+});
+
+describe('shapeTimestamp — millisecond precision follows the original', () => {
+ it('strips the fresh milliseconds when the original had none', () => {
+ assert.strictEqual(shapeTimestamp('2001-02-03T04:05:06.789Z', '2026-07-02T12:34:56Z'), '2001-02-03T04:05:06Z');
+ });
+
+ it('keeps the fresh milliseconds when the original carried any', () => {
+ assert.strictEqual(shapeTimestamp('2001-02-03T04:05:06.789Z', '2026-07-02T12:34:56.100Z'), '2001-02-03T04:05:06.789Z');
+ });
+});
diff --git a/src/test/randomize/randomize.test.ts b/src/test/randomize/randomize.test.ts
new file mode 100644
index 0000000..4b6e2b2
--- /dev/null
+++ b/src/test/randomize/randomize.test.ts
@@ -0,0 +1,84 @@
+import * as assert from 'assert';
+
+import { randomize, Rng } from '../../randomize';
+
+// randomize(text, rng) is the pure half of "Insert Random: Randomize Selection": every digit becomes a
+// random digit, a–z a random lowercase letter, A–Z a random uppercase letter, everything else passes
+// through untouched — so "3.14" stays number-shaped and "Bob@x.io" stays email-shaped. The rng is
+// injected (the extension wraps the shared seeded faker instance), so these tests script it and pin the
+// exact character-class contract without any engine in the loop.
+
+/** rng that always picks index 0 → digits become '0', letters 'a' / 'A'. */
+const zeroRng: Rng = () => 0;
+
+/** rng that always picks the last index → digits become '9', letters 'z' / 'Z'. */
+const maxRng: Rng = (bound) => bound - 1;
+
+/** Deterministic little LCG — varied indices without Math.random. */
+function lcgRng(seed = 42): Rng {
+ let state = seed;
+ return (bound) => {
+ state = (state * 1103515245 + 12345) % 2147483648;
+ return state % bound;
+ };
+}
+
+/** rng that records the bound of every call. */
+function recordingRng(): { rng: Rng; bounds: number[] } {
+ const bounds: number[] = [];
+ return { rng: (bound) => { bounds.push(bound); return 0; }, bounds };
+}
+
+describe('randomize — format-preserving randomization', () => {
+ it('replaces every digit with a digit', () => {
+ assert.strictEqual(randomize('0123456789', zeroRng), '0000000000');
+ assert.strictEqual(randomize('0123456789', maxRng), '9999999999');
+ assert.match(randomize('8675309', lcgRng()), /^[0-9]{7}$/);
+ });
+
+ it('preserves letter case: a–z stays lowercase, A–Z stays uppercase', () => {
+ assert.strictEqual(randomize('abc', zeroRng), 'aaa');
+ assert.strictEqual(randomize('XYZ', maxRng), 'ZZZ');
+ assert.match(randomize('RandomizeMe', lcgRng()), /^[A-Z][a-z]{8}[A-Z][a-z]$/);
+ });
+
+ it('keeps punctuation and whitespace exactly where they were', () => {
+ assert.strictEqual(randomize('3.14', zeroRng), '0.00');
+ assert.strictEqual(randomize('Bob@x.io', zeroRng), 'Aaa@a.aa');
+ assert.strictEqual(randomize('a-b c_d!', maxRng), 'z-z z_z!');
+ assert.match(randomize('+63 (917) 555-0142', lcgRng()), /^\+\d\d \(\d\d\d\) \d\d\d-\d\d\d\d$/);
+ });
+
+ it('passes multi-line text through with its line structure intact', () => {
+ assert.strictEqual(randomize('ab\ncd\r\nef', zeroRng), 'aa\naa\r\naa');
+ });
+
+ it('passes non-ASCII text through unchanged (accents, CJK, emoji)', () => {
+ assert.strictEqual(randomize('héllo 日本 😀', zeroRng), 'aéaaa 日本 😀');
+ // for…of iterates code points, so the surrogate pair is never split.
+ assert.strictEqual(randomize('😀', lcgRng()), '😀');
+ });
+
+ it('returns an empty string for an empty string, without drawing', () => {
+ const { rng, bounds } = recordingRng();
+ assert.strictEqual(randomize('', rng), '');
+ assert.deepStrictEqual(bounds, []);
+ });
+
+ it('draws once per alphanumeric character, with the class-sized bound', () => {
+ const { rng, bounds } = recordingRng();
+ randomize('5aZ - ok', rng);
+ // '5' → 10, 'a' → 26, 'Z' → 26, then 'o' and 'k'; ' ', '-' never draw.
+ assert.deepStrictEqual(bounds, [ 10, 26, 26, 26, 26 ]);
+ });
+
+ it('characters adjacent to the ASCII ranges are left alone', () => {
+ // '/' and ':' bracket the digits; '@' and '[' bracket A–Z; '`' and '{' bracket a–z.
+ assert.strictEqual(randomize('/:@[`{', lcgRng()), '/:@[`{');
+ });
+
+ it('preserves length for any BMP input', () => {
+ const input = 'The 39 quick brown foxes — jumped!';
+ assert.strictEqual(randomize(input, lcgRng()).length, input.length);
+ });
+});
diff --git a/src/test/record/record.test.ts b/src/test/record/record.test.ts
new file mode 100644
index 0000000..729ba9e
--- /dev/null
+++ b/src/test/record/record.test.ts
@@ -0,0 +1,183 @@
+import * as assert from 'assert';
+
+import { buildRecords } from '../../record';
+import type { Generator, GenerateOptions } from '../../catalog';
+
+// buildRecords composes selected generators into one record string per shape:
+// json (object / array of objects, JSON.stringify escaping, keys = generator id),
+// sql (INSERT INTO …; single-quote values, '' doubling, one stmt per record),
+// csv (comma-joined, "…" quoting only when a value has a comma / quote / newline).
+// bulkCount stacks records per shape; field order follows the fields array (catalog order).
+
+function field(id: string, value: string): Generator {
+ return { id, label: id, group: 'G', generate: () => value };
+}
+const OPTS = { bulkCount: 1, sqlTable: 'table' };
+
+describe('buildRecords — json', () => {
+ it('renders a single object keyed by generator id', () => {
+ assert.strictEqual(
+ buildRecords([ field('fullName', 'Jane Doe'), field('email', 'j@x.com') ], 'json', OPTS),
+ '{ "fullName": "Jane Doe", "email": "j@x.com" }',
+ );
+ });
+
+ it('renders a JSON array of objects when bulkCount > 1', () => {
+ assert.strictEqual(
+ buildRecords([ field('n', 'A') ], 'json', { ...OPTS, bulkCount: 2 }),
+ '[ { "n": "A" }, { "n": "A" } ]',
+ );
+ });
+
+ it('escapes quotes so the result parses as JSON', () => {
+ const out = buildRecords([ field('q', 'say "hi"') ], 'json', OPTS);
+ assert.strictEqual(out, '{ "q": "say \\"hi\\"" }');
+ assert.doesNotThrow(() => JSON.parse(out));
+ });
+});
+
+describe('buildRecords — sql', () => {
+ it('renders an INSERT with the configured table', () => {
+ assert.strictEqual(
+ buildRecords([ field('name', 'Jane'), field('age', '42') ], 'sql', { ...OPTS, sqlTable: 'users' }),
+ "INSERT INTO users (name, age) VALUES ('Jane', '42');",
+ );
+ });
+
+ it("doubles an embedded single quote (O'Brien)", () => {
+ assert.strictEqual(
+ buildRecords([ field('name', "O'Brien") ], 'sql', OPTS),
+ "INSERT INTO table (name) VALUES ('O''Brien');",
+ );
+ });
+
+ it('emits one statement per record, newline-separated', () => {
+ assert.strictEqual(
+ buildRecords([ field('n', 'A') ], 'sql', { ...OPTS, bulkCount: 2 }),
+ "INSERT INTO table (n) VALUES ('A');\nINSERT INTO table (n) VALUES ('A');",
+ );
+ });
+});
+
+describe('buildRecords — csv', () => {
+ it('joins values with commas', () => {
+ assert.strictEqual(buildRecords([ field('a', 'x'), field('b', 'y') ], 'csv', OPTS), 'x,y');
+ });
+
+ it('quotes and doubles quotes for values with a comma or quote', () => {
+ assert.strictEqual(
+ buildRecords([ field('a', 'x,y'), field('b', 'he "said"') ], 'csv', OPTS),
+ '"x,y","he ""said"""',
+ );
+ });
+
+ it('emits one line per record', () => {
+ assert.strictEqual(buildRecords([ field('n', 'A') ], 'csv', { ...OPTS, bulkCount: 2 }), 'A\nA');
+ });
+
+ it('quotes values containing a newline or carriage return', () => {
+ // The other two csvEscape triggers — an unquoted line break would split the record across CSV rows.
+ assert.strictEqual(buildRecords([ field('a', 'x\ny') ], 'csv', OPTS), '"x\ny"');
+ assert.strictEqual(buildRecords([ field('a', 'x\ry') ], 'csv', OPTS), '"x\ry"');
+ });
+});
+
+describe('buildRecords — bulkCount clamping', () => {
+ // json makes the clamp observable: exactly one record renders as a bare object, never an array.
+ it('clamps 0 and negatives up to a single record', () => {
+ assert.strictEqual(buildRecords([ field('n', 'A') ], 'json', { ...OPTS, bulkCount: 0 }), '{ "n": "A" }');
+ assert.strictEqual(buildRecords([ field('n', 'A') ], 'json', { ...OPTS, bulkCount: -3 }), '{ "n": "A" }');
+ });
+
+ it('floors a fractional count (2.9 → 2 records, not 3)', () => {
+ assert.strictEqual(
+ buildRecords([ field('n', 'A') ], 'json', { ...OPTS, bulkCount: 2.9 }),
+ '[ { "n": "A" }, { "n": "A" } ]',
+ );
+ });
+});
+
+describe('buildRecords — dateFormat threading', () => {
+ // Records draw from the same generators as single-type inserts, so a Time field must honor the
+ // dateFormat setting here too. A probe field records what each draw received.
+ function probe(): { generator: Generator; seen: unknown[] } {
+ const seen: unknown[] = [];
+ return {
+ seen,
+ generator: { id: 'p', label: 'P', group: 'G', generate: (opts?: GenerateOptions) => { seen.push(opts?.dateFormat); return 'x'; } },
+ };
+ }
+
+ it('passes opts.dateFormat into every field draw (all records)', () => {
+ const { generator, seen } = probe();
+ buildRecords([ generator ], 'json', { ...OPTS, bulkCount: 2, dateFormat: 'isoDate' });
+ assert.deepStrictEqual(seen, [ 'isoDate', 'isoDate' ]);
+ });
+
+ it('passes no format when the option is unset', () => {
+ const { generator, seen } = probe();
+ buildRecords([ generator ], 'csv', OPTS);
+ assert.deepStrictEqual(seen, [ undefined ]);
+ });
+});
+
+describe('buildRecords — field order', () => {
+ it('follows the fields array (caller passes catalog order)', () => {
+ assert.strictEqual(buildRecords([ field('first', '1'), field('second', '2') ], 'csv', OPTS), '1,2');
+ });
+});
+
+describe('buildRecords — dataset mode (records → file)', () => {
+ // dataset: true renders the same records as a STANDALONE FILE for Generate Dataset…:
+ // csv gains a header row of field keys, json is always an array (one record per line, so a
+ // 100k-row file stays scrollable), and the text ends with a trailing newline. At-cursor
+ // records never pass the flag — the headerless/bare-object pins above are the other half
+ // of this contract.
+
+ it('csv leads with a header row of field keys and ends with a newline', () => {
+ assert.strictEqual(
+ buildRecords([ field('a', 'x'), field('b', 'y') ], 'csv', { ...OPTS, dataset: true }),
+ 'a,b\nx,y\n',
+ );
+ });
+
+ it('csv header cells are escaped (a custom-list name can hold a comma)', () => {
+ assert.strictEqual(
+ buildRecords([ field('name, formal', 'x') ], 'csv', { ...OPTS, dataset: true }),
+ '"name, formal"\nx\n',
+ );
+ });
+
+ it('csv stacks one line per record under the single header', () => {
+ assert.strictEqual(
+ buildRecords([ field('n', 'A') ], 'csv', { ...OPTS, bulkCount: 3, dataset: true }),
+ 'n\nA\nA\nA\n',
+ );
+ });
+
+ it('json is an array even for a single record, one record per line', () => {
+ assert.strictEqual(
+ buildRecords([ field('n', 'A') ], 'json', { ...OPTS, dataset: true }),
+ '[\n { "n": "A" }\n]\n',
+ );
+ });
+
+ it('json stacks records one per line and stays parseable', () => {
+ const out = buildRecords([ field('n', 'A') ], 'json', { ...OPTS, bulkCount: 2, dataset: true });
+ assert.strictEqual(out, '[\n { "n": "A" },\n { "n": "A" }\n]\n');
+ const parsed = JSON.parse(out);
+ assert.ok(Array.isArray(parsed) && parsed.length === 2);
+ });
+
+ it('sql keeps its one-statement-per-line body, plus the trailing newline', () => {
+ assert.strictEqual(
+ buildRecords([ field('n', 'A') ], 'sql', { ...OPTS, bulkCount: 2, dataset: true }),
+ "INSERT INTO table (n) VALUES ('A');\nINSERT INTO table (n) VALUES ('A');\n",
+ );
+ });
+
+ it('an explicit dataset: false behaves exactly like the at-cursor default', () => {
+ assert.strictEqual(buildRecords([ field('a', 'x') ], 'csv', { ...OPTS, dataset: false }), 'x');
+ assert.strictEqual(buildRecords([ field('n', 'A') ], 'json', { ...OPTS, dataset: false }), '{ "n": "A" }');
+ });
+});
diff --git a/src/test/runTest.ts b/src/test/runTest.ts
deleted file mode 100644
index 1eabfa3..0000000
--- a/src/test/runTest.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as path from 'path';
-
-import { runTests } from 'vscode-test';
-
-async function main() {
- try {
- // The folder containing the Extension Manifest package.json
- // Passed to `--extensionDevelopmentPath`
- const extensionDevelopmentPath = path.resolve(__dirname, '../../');
-
- // The path to test runner
- // Passed to --extensionTestsPath
- const extensionTestsPath = path.resolve(__dirname, './suite/index');
-
- // Download VS Code, unzip it and run the integration test
- await runTests({ extensionDevelopmentPath, extensionTestsPath });
- } catch (err) {
- console.error('Failed to run tests');
- process.exit(1);
- }
-}
-
-main();
diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts
deleted file mode 100644
index d59ba91..0000000
--- a/src/test/suite/extension.test.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as assert from 'assert';
-
-// You can import and use all API from the 'vscode' module
-// as well as import your extension to test it
-import * as vscode from 'vscode';
-// import * as myExtension from '../extension';
-
-suite('Extension Test Suite', () => {
- vscode.window.showInformationMessage('Start all tests.');
-
- test('Sample test', () => {
- assert.equal(-1, [1, 2, 3].indexOf(5));
- assert.equal(-1, [1, 2, 3].indexOf(0));
- });
-});
diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts
deleted file mode 100644
index 7d3ac4b..0000000
--- a/src/test/suite/index.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import * as path from 'path';
-import * as Mocha from 'mocha';
-import * as glob from 'glob';
-
-export function run(): Promise {
- // Create the mocha test
- const mocha = new Mocha({
- ui: 'tdd',
- });
- mocha.useColors(true);
-
- const testsRoot = path.resolve(__dirname, '..');
-
- return new Promise((c, e) => {
- glob('**/**.test.js', { cwd: testsRoot }, (err, files) => {
- if (err) {
- return e(err);
- }
-
- // Add files to the test suite
- files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
-
- try {
- // Run the mocha test
- mocha.run(failures => {
- if (failures > 0) {
- e(new Error(`${failures} tests failed.`));
- } else {
- c();
- }
- });
- } catch (err) {
- console.error(err);
- e(err);
- }
- });
- });
-}
diff --git a/tsconfig.json b/tsconfig.json
index 74ab958..440c4f2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,17 +1,14 @@
{
- "compilerOptions": {
- "module": "commonjs",
- "target": "es6",
- "outDir": "out",
- "lib": [
- "es6"
- ],
- "sourceMap": true,
- "rootDir": "src",
- "strict": false
- },
- "exclude": [
- "node_modules",
- ".vscode-test"
- ]
+ "compilerOptions": {
+ "module": "Node16",
+ "target": "ES2022",
+ "lib": [
+ "ES2022"
+ ],
+ "sourceMap": true,
+ "rootDir": "src",
+ "strict": false,
+ "types": ["node", "mocha"]
+ },
+ "include": ["src/**/*"]
}