feat(config): add TypeScript build and config support
This commit is contained in:
@@ -52,7 +52,7 @@ If running outside Nix, document that Firefox and geckodriver must be on `PATH`.
|
|||||||
|
|
||||||
## Code Style
|
## Code Style
|
||||||
|
|
||||||
- Use ES modules.
|
- Use TypeScript with ES modules; source lives in `src/**/*.ts` and builds to `dist/`.
|
||||||
- Keep code direct and minimal; avoid abstractions until they are needed.
|
- Keep code direct and minimal; avoid abstractions until they are needed.
|
||||||
- Add short Title Case comments above cohesive logic blocks.
|
- Add short Title Case comments above cohesive logic blocks.
|
||||||
- Prefer exact, actionable error messages.
|
- Prefer exact, actionable error messages.
|
||||||
|
|||||||
56
README.md
56
README.md
@@ -20,7 +20,8 @@ If running directly with Node.js, install dependencies and make sure `firefox` a
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
node src/index.js exec https://example.com --js='return document.title'
|
npm run build
|
||||||
|
node dist/src/index.js exec https://example.com --js='return document.title'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Glimpse CLI
|
## Glimpse CLI
|
||||||
@@ -31,6 +32,7 @@ glimpse <command> [options]
|
|||||||
|
|
||||||
Common options:
|
Common options:
|
||||||
|
|
||||||
|
- `--config=<file>` - read config from a custom path instead of `~/.config/glimpse/config.json`
|
||||||
- `--no-headless` - show Firefox instead of running headless
|
- `--no-headless` - show Firefox instead of running headless
|
||||||
- `--url=<server>` - connect to an existing WebDriver server
|
- `--url=<server>` - connect to an existing WebDriver server
|
||||||
- `--timeout=<ms>` - maximum wait time in milliseconds for command waits (default: `10000`)
|
- `--timeout=<ms>` - maximum wait time in milliseconds for command waits (default: `10000`)
|
||||||
@@ -173,22 +175,45 @@ Output:
|
|||||||
|
|
||||||
Search using a supported provider and print a JSON array of results. Currently only Kagi is supported.
|
Search using a supported provider and print a JSON array of results. Currently only Kagi is supported.
|
||||||
|
|
||||||
Kagi requires `--token=<token>` or a `KAGI_TOKEN` environment variable. The token is validated by the Kagi provider and sent to Kagi as the `token` query parameter.
|
Kagi requires a token from `--token=<token>`, `KAGI_TOKEN`, or the glimpse config file. The token is validated by the Kagi provider and sent to Kagi as the `token` query parameter.
|
||||||
|
|
||||||
|
Default config path:
|
||||||
|
|
||||||
|
```text
|
||||||
|
~/.config/glimpse/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Example config:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"search": {
|
||||||
|
"provider": "kagi"
|
||||||
|
},
|
||||||
|
"providers": {
|
||||||
|
"kagi": {
|
||||||
|
"token": "your-kagi-token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then search without exposing the token in command arguments:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
KAGI_TOKEN=... nix run .#glimpse -- search --provider=kagi "nix flakes selenium webdriver"
|
nix run .#glimpse -- search "nix flakes selenium webdriver"
|
||||||
```
|
```
|
||||||
|
|
||||||
Local usage:
|
Local usage:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
KAGI_TOKEN=... ./result/bin/glimpse search "nix flakes selenium webdriver"
|
./result/bin/glimpse search "nix flakes selenium webdriver"
|
||||||
```
|
```
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
- `--provider=<provider>` - search provider: `kagi` (default: `kagi`)
|
- `--provider=<provider>` - search provider: `kagi` (default: config or `kagi`)
|
||||||
- `--token=<token>` - Kagi token (default: `KAGI_TOKEN`)
|
- `--token=<token>` - Kagi token (default: `KAGI_TOKEN` or config)
|
||||||
- `--no-headless` - show Firefox instead of running headless
|
- `--no-headless` - show Firefox instead of running headless
|
||||||
- `--url=<server>` - connect to an existing WebDriver server
|
- `--url=<server>` - connect to an existing WebDriver server
|
||||||
- `--timeout=<ms>` - wait time for results before returning `[]` (default: `10000`)
|
- `--timeout=<ms>` - wait time for results before returning `[]` (default: `10000`)
|
||||||
@@ -217,7 +242,7 @@ Run the built tool:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
./result/bin/glimpse exec https://example.com --js='return document.title'
|
./result/bin/glimpse exec https://example.com --js='return document.title'
|
||||||
KAGI_TOKEN=... ./result/bin/glimpse search "example query"
|
./result/bin/glimpse search "example query"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
@@ -253,16 +278,19 @@ node test/smoke.js snapshot js
|
|||||||
Useful local commands:
|
Useful local commands:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node src/index.js snapshot 'data:text/html,<title>Hello</title><h1>Hello</h1>'
|
npm run build
|
||||||
node src/index.js exec 'data:text/html,<title>Hello</title>' --js='return document.title'
|
node dist/src/index.js snapshot 'data:text/html,<title>Hello</title><h1>Hello</h1>'
|
||||||
node src/index.js screenshot 'data:text/html,<title>Hello</title>' --output=/tmp/page.png
|
node dist/src/index.js exec 'data:text/html,<title>Hello</title>' --js='return document.title'
|
||||||
node src/index.js reader 'https://example.com/article'
|
node dist/src/index.js screenshot 'data:text/html,<title>Hello</title>' --output=/tmp/page.png
|
||||||
|
node dist/src/index.js reader 'https://example.com/article'
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
- `src/index.js` - `glimpse` CLI with subcommands, including Firefox Reader View extraction and provider-backed search
|
- `src/index.ts` - `glimpse` CLI with subcommands, including Firefox Reader View extraction and provider-backed search
|
||||||
- `src/driver.js` - Firefox WebDriver creation and geckodriver resolution
|
- `src/config.ts` - home-dir config loading for CLI defaults and provider settings
|
||||||
- `src/providers/kagi.js` - reusable Kagi search provider implementation
|
- `src/driver.ts` - Firefox WebDriver creation and geckodriver resolution
|
||||||
|
- `src/providers/kagi.ts` - reusable Kagi search provider implementation
|
||||||
|
- `tsconfig.json` - TypeScript compiler settings; build output goes to `dist/`
|
||||||
- `flake.nix` - Nix dev shell, package, wrappers, and apps
|
- `flake.nix` - Nix dev shell, package, wrappers, and apps
|
||||||
- `KAGI.md` - Kagi-specific notes
|
- `KAGI.md` - Kagi-specific notes
|
||||||
|
|||||||
@@ -42,8 +42,8 @@
|
|||||||
version = "1.0.0";
|
version = "1.0.0";
|
||||||
|
|
||||||
src = source;
|
src = source;
|
||||||
npmDepsHash = "sha256-IWzSvrGgkoR6gg7P1m/mwakGOOKmm2OFtBirKgE09Ds=";
|
npmDepsHash = "sha256-ycAjPZZqI3ZMIUubJbWy8G6X6LaXDcgdZGswikfkQj8=";
|
||||||
dontNpmBuild = true;
|
npmBuildScript = "build";
|
||||||
|
|
||||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||||
|
|
||||||
|
|||||||
610
package-lock.json
generated
610
package-lock.json
generated
@@ -14,7 +14,14 @@
|
|||||||
"turndown": "^7.2.4"
|
"turndown": "^7.2.4"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"glimpse": "src/index.js"
|
"glimpse": "dist/src/index.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
|
"@types/selenium-webdriver": "^4.35.5",
|
||||||
|
"@types/turndown": "^5.0.6",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@bazel/runfiles": {
|
"node_modules/@bazel/runfiles": {
|
||||||
@@ -23,6 +30,448 @@
|
|||||||
"integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
|
"integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@mixmark-io/domino": {
|
"node_modules/@mixmark-io/domino": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
|
||||||
@@ -333,12 +782,120 @@
|
|||||||
"node": "^20.19.0 || >=22.12.0"
|
"node": "^20.19.0 || >=22.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||||
|
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/selenium-webdriver": {
|
||||||
|
"version": "4.35.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-4.35.5.tgz",
|
||||||
|
"integrity": "sha512-wCQCjWmahRkUAO7S703UAvBFkxz4o/rjX4T2AOSWKXSi0sTQPsrXxR0GjtFUT0ompedLkYH4R5HO5Urz0hyeog==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"@types/ws": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/turndown": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz",
|
||||||
|
"integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/core-util-is": {
|
"node_modules/core-util-is": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.27.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||||
|
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.7",
|
||||||
|
"@esbuild/android-arm": "0.27.7",
|
||||||
|
"@esbuild/android-arm64": "0.27.7",
|
||||||
|
"@esbuild/android-x64": "0.27.7",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.7",
|
||||||
|
"@esbuild/darwin-x64": "0.27.7",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.7",
|
||||||
|
"@esbuild/linux-arm": "0.27.7",
|
||||||
|
"@esbuild/linux-arm64": "0.27.7",
|
||||||
|
"@esbuild/linux-ia32": "0.27.7",
|
||||||
|
"@esbuild/linux-loong64": "0.27.7",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.7",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.7",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.7",
|
||||||
|
"@esbuild/linux-s390x": "0.27.7",
|
||||||
|
"@esbuild/linux-x64": "0.27.7",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.7",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.7",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.7",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.7",
|
||||||
|
"@esbuild/sunos-x64": "0.27.7",
|
||||||
|
"@esbuild/win32-arm64": "0.27.7",
|
||||||
|
"@esbuild/win32-ia32": "0.27.7",
|
||||||
|
"@esbuild/win32-x64": "0.27.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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/get-tsconfig": {
|
||||||
|
"version": "4.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||||
|
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-pkg-maps": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/immediate": {
|
"node_modules/immediate": {
|
||||||
"version": "3.0.6",
|
"version": "3.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||||
@@ -449,6 +1006,16 @@
|
|||||||
"util-deprecate": "~1.0.1"
|
"util-deprecate": "~1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resolve-pkg-maps": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||||
@@ -504,6 +1071,26 @@
|
|||||||
"node": ">=14.14"
|
"node": ">=14.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tsx": {
|
||||||
|
"version": "4.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||||
|
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"esbuild": "~0.27.0",
|
||||||
|
"get-tsconfig": "^4.7.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tsx": "dist/cli.mjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "~2.3.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/turndown": {
|
"node_modules/turndown": {
|
||||||
"version": "7.2.4",
|
"version": "7.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz",
|
||||||
@@ -517,6 +1104,27 @@
|
|||||||
"npm": ">=9"
|
"npm": ">=9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||||
|
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
18
package.json
18
package.json
@@ -3,13 +3,14 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.js",
|
"main": "dist/src/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
"glimpse": "./src/index.js"
|
"glimpse": "./dist/src/index.js"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "oxlint --ignore-pattern node_modules --ignore-pattern .direnv .",
|
"build": "tsc && chmod +x dist/src/index.js",
|
||||||
"start": "node src/index.js",
|
"lint": "oxlint --ignore-pattern node_modules --ignore-pattern .direnv --ignore-pattern dist . && tsc --noEmit",
|
||||||
|
"start": "tsx src/index.ts",
|
||||||
"test": "node test/smoke.js",
|
"test": "node test/smoke.js",
|
||||||
"test:smoke": "node test/smoke.js",
|
"test:smoke": "node test/smoke.js",
|
||||||
"test:list": "node test/smoke.js --list",
|
"test:list": "node test/smoke.js --list",
|
||||||
@@ -23,10 +24,17 @@
|
|||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"oxlint": "^1.61.0",
|
"oxlint": "^1.61.0",
|
||||||
"selenium-webdriver": "^4.43.0",
|
"selenium-webdriver": "^4.43.0",
|
||||||
"turndown": "^7.2.4"
|
"turndown": "^7.2.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^25.6.0",
|
||||||
|
"@types/selenium-webdriver": "^4.35.5",
|
||||||
|
"@types/turndown": "^5.0.6",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
src/config.ts
Normal file
117
src/config.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
export interface GlimpseConfig {
|
||||||
|
search?: {
|
||||||
|
provider?: "kagi";
|
||||||
|
};
|
||||||
|
providers?: {
|
||||||
|
kagi?: {
|
||||||
|
token?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConfigError extends Error {
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
constructor(code: string, message: string) {
|
||||||
|
super(message);
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultConfigPath(
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
): string {
|
||||||
|
const configHome = env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
||||||
|
return join(configHome, "glimpse", "config.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorMessage(err: unknown): string {
|
||||||
|
return err instanceof Error ? err.message : String(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateObject(
|
||||||
|
value: unknown,
|
||||||
|
path: string,
|
||||||
|
name: string,
|
||||||
|
): asserts value is Record<string, unknown> | undefined {
|
||||||
|
if (value !== undefined && !isObject(value)) {
|
||||||
|
throw new ConfigError(
|
||||||
|
"INVALID_CONFIG",
|
||||||
|
`${name} must be an object: ${path}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateString(
|
||||||
|
value: unknown,
|
||||||
|
path: string,
|
||||||
|
name: string,
|
||||||
|
): asserts value is string | undefined {
|
||||||
|
if (value !== undefined && typeof value !== "string") {
|
||||||
|
throw new ConfigError(
|
||||||
|
"INVALID_CONFIG",
|
||||||
|
`${name} must be a string: ${path}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateConfig(
|
||||||
|
config: unknown,
|
||||||
|
path: string,
|
||||||
|
): asserts config is GlimpseConfig {
|
||||||
|
if (!isObject(config)) {
|
||||||
|
throw new ConfigError(
|
||||||
|
"INVALID_CONFIG",
|
||||||
|
`Config file must contain a JSON object: ${path}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Search Config
|
||||||
|
validateObject(config.search, path, "search");
|
||||||
|
validateString(config.search?.provider, path, "search.provider");
|
||||||
|
|
||||||
|
if (config.search?.provider && !["kagi"].includes(config.search.provider)) {
|
||||||
|
throw new ConfigError(
|
||||||
|
"INVALID_CONFIG",
|
||||||
|
`Unsupported search.provider value: ${config.search.provider}. Expected kagi: ${path}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Provider Config
|
||||||
|
validateObject(config.providers, path, "providers");
|
||||||
|
validateObject(config.providers?.kagi, path, "providers.kagi");
|
||||||
|
validateString(config.providers?.kagi?.token, path, "providers.kagi.token");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig({
|
||||||
|
path = defaultConfigPath(),
|
||||||
|
}: { path?: string } = {}): GlimpseConfig {
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: unknown;
|
||||||
|
|
||||||
|
// Parse Config File
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(readFileSync(path, "utf-8"));
|
||||||
|
} catch (err) {
|
||||||
|
throw new ConfigError(
|
||||||
|
"CONFIG_READ_FAILED",
|
||||||
|
`Failed to read config file ${path}: ${errorMessage(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Config Shape
|
||||||
|
validateConfig(parsed, path);
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { execFileSync } from "node:child_process";
|
import { execFileSync } from "node:child_process";
|
||||||
import { Builder } from "selenium-webdriver";
|
import { Builder, type WebDriver } from "selenium-webdriver";
|
||||||
import firefox from "selenium-webdriver/firefox.js";
|
import firefox from "selenium-webdriver/firefox.js";
|
||||||
|
|
||||||
/**
|
export interface DriverOptions {
|
||||||
* Resolve the geckodriver path from $PATH.
|
headless?: boolean;
|
||||||
*
|
existingUrl?: string;
|
||||||
* @returns {string}
|
}
|
||||||
*/
|
|
||||||
function findGeckodriver() {
|
function findGeckodriver(): string {
|
||||||
try {
|
try {
|
||||||
return execFileSync("which", ["geckodriver"], { encoding: "utf-8" }).trim();
|
return execFileSync("which", ["geckodriver"], { encoding: "utf-8" }).trim();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -17,16 +17,10 @@ function findGeckodriver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export async function createDriver({
|
||||||
* Create a Firefox WebDriver instance.
|
headless = false,
|
||||||
*
|
existingUrl,
|
||||||
* @param {object} opts
|
}: DriverOptions = {}): Promise<WebDriver> {
|
||||||
* @param {boolean} [opts.headless=false] - Run Firefox in headless mode.
|
|
||||||
* @param {string} [opts.existingUrl] - Connect to an already-running
|
|
||||||
* WebDriver server (e.g. "http://localhost:4444").
|
|
||||||
* @returns {Promise<import("selenium-webdriver").WebDriver>}
|
|
||||||
*/
|
|
||||||
export async function createDriver({ headless = false, existingUrl } = {}) {
|
|
||||||
const options = new firefox.Options();
|
const options = new firefox.Options();
|
||||||
|
|
||||||
// Configure Headless
|
// Configure Headless
|
||||||
@@ -34,7 +28,9 @@ export async function createDriver({ headless = false, existingUrl } = {}) {
|
|||||||
options.addArguments("--headless");
|
options.addArguments("--headless");
|
||||||
}
|
}
|
||||||
|
|
||||||
const builder = new Builder().forBrowser("firefox").setFirefoxOptions(options);
|
const builder = new Builder()
|
||||||
|
.forBrowser("firefox")
|
||||||
|
.setFirefoxOptions(options);
|
||||||
|
|
||||||
// Connect to Existing Server
|
// Connect to Existing Server
|
||||||
if (existingUrl) {
|
if (existingUrl) {
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { loadConfig, type GlimpseConfig } from "./config.js";
|
||||||
import { createDriver } from "./driver.js";
|
import { createDriver } from "./driver.js";
|
||||||
import { searchKagi } from "./providers/kagi.js";
|
import { searchKagi } from "./providers/kagi.js";
|
||||||
import { readFileSync, writeFileSync } from "node:fs";
|
import { readFileSync, writeFileSync } from "node:fs";
|
||||||
@@ -8,7 +9,7 @@ import TurndownService from "turndown";
|
|||||||
const DEFAULT_TIMEOUT_MS = 10000;
|
const DEFAULT_TIMEOUT_MS = 10000;
|
||||||
const POLL_INTERVAL_MS = 200;
|
const POLL_INTERVAL_MS = 200;
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const runContext = {};
|
const runContext: { targetUrl?: string; currentUrl?: string } = {};
|
||||||
|
|
||||||
// Parse CLI Args
|
// Parse CLI Args
|
||||||
const [command, ...args] = process.argv.slice(2);
|
const [command, ...args] = process.argv.slice(2);
|
||||||
@@ -18,6 +19,8 @@ const inlineJs = getOption("--js");
|
|||||||
const scriptPath = getOption("--script");
|
const scriptPath = getOption("--script");
|
||||||
const waitJs = getOption("--wait-js");
|
const waitJs = getOption("--wait-js");
|
||||||
const waitUntil = getOption("--wait-until") ?? "none";
|
const waitUntil = getOption("--wait-until") ?? "none";
|
||||||
|
const configPath = getOption("--config");
|
||||||
|
let appConfig: GlimpseConfig = {};
|
||||||
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
||||||
|
|
||||||
function getOption(name) {
|
function getOption(name) {
|
||||||
@@ -50,6 +53,9 @@ function printResult(result) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class CliError extends Error {
|
class CliError extends Error {
|
||||||
|
code: string;
|
||||||
|
details: Record<string, unknown>;
|
||||||
|
|
||||||
constructor(code, message, details = {}) {
|
constructor(code, message, details = {}) {
|
||||||
super(message);
|
super(message);
|
||||||
this.code = code;
|
this.code = code;
|
||||||
@@ -84,6 +90,7 @@ Common Options:
|
|||||||
--wait-until=<state> Wait for readiness: none, interactive, complete (default: none)
|
--wait-until=<state> Wait for readiness: none, interactive, complete (default: none)
|
||||||
--js=<code> Execute inline JS before command logic
|
--js=<code> Execute inline JS before command logic
|
||||||
--script=<file> Execute JS from a file before command logic
|
--script=<file> Execute JS from a file before command logic
|
||||||
|
--config=<file> Read config from a custom path
|
||||||
|
|
||||||
Exec Options:
|
Exec Options:
|
||||||
--js=<code> Return the top-level JS result
|
--js=<code> Return the top-level JS result
|
||||||
@@ -97,8 +104,8 @@ Reader Options:
|
|||||||
--output=<file> Write output to a file
|
--output=<file> Write output to a file
|
||||||
|
|
||||||
Search Options:
|
Search Options:
|
||||||
--provider=<provider> Search provider: kagi (default: kagi)
|
--provider=<provider> Search provider: kagi (default: config or kagi)
|
||||||
--token=<token> Kagi token (default: KAGI_TOKEN)
|
--token=<token> Kagi token (default: KAGI_TOKEN or config)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
glimpse snapshot https://example.com
|
glimpse snapshot https://example.com
|
||||||
@@ -114,7 +121,10 @@ function printHelp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function usage() {
|
function usage() {
|
||||||
cliError("USAGE_ERROR", "Usage: glimpse <command> <url> [options]. Run glimpse --help for details.");
|
cliError(
|
||||||
|
"USAGE_ERROR",
|
||||||
|
"Usage: glimpse <command> <url> [options]. Run glimpse --help for details.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTimeout() {
|
function parseTimeout() {
|
||||||
@@ -179,7 +189,9 @@ async function waitForReadyState(driver) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await driver.wait(async () => {
|
await driver.wait(async () => {
|
||||||
const readyState = await driver.executeScript("return document.readyState");
|
const readyState = await driver.executeScript(
|
||||||
|
"return document.readyState",
|
||||||
|
);
|
||||||
return waitUntil === "interactive"
|
return waitUntil === "interactive"
|
||||||
? ["interactive", "complete"].includes(readyState)
|
? ["interactive", "complete"].includes(readyState)
|
||||||
: readyState === "complete";
|
: readyState === "complete";
|
||||||
@@ -214,7 +226,10 @@ async function waitForJs(driver) {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
cliError("WAIT_TIMEOUT", `Timed out after ${timeoutMs}ms waiting for --wait-js`);
|
cliError(
|
||||||
|
"WAIT_TIMEOUT",
|
||||||
|
`Timed out after ${timeoutMs}ms waiting for --wait-js`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runPreludeScript(driver) {
|
async function runPreludeScript(driver) {
|
||||||
@@ -417,7 +432,8 @@ function renderReaderOutput(article, format) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function searchCommand() {
|
async function searchCommand() {
|
||||||
const provider = getOption("--provider") ?? "kagi";
|
const provider =
|
||||||
|
getOption("--provider") ?? appConfig.search?.provider ?? "kagi";
|
||||||
const query = getPositionalArgs().join(" ");
|
const query = getPositionalArgs().join(" ");
|
||||||
|
|
||||||
if (!query) usage();
|
if (!query) usage();
|
||||||
@@ -428,6 +444,7 @@ async function searchCommand() {
|
|||||||
return searchKagi({
|
return searchKagi({
|
||||||
query,
|
query,
|
||||||
token: getOption("--token"),
|
token: getOption("--token"),
|
||||||
|
config: appConfig,
|
||||||
headless,
|
headless,
|
||||||
existingUrl,
|
existingUrl,
|
||||||
timeoutMs,
|
timeoutMs,
|
||||||
@@ -458,8 +475,9 @@ async function readerCommand() {
|
|||||||
// Wait For Reader Content
|
// Wait For Reader Content
|
||||||
let article;
|
let article;
|
||||||
try {
|
try {
|
||||||
article = await driver.wait(async () => {
|
article = await driver.wait(
|
||||||
return driver.executeScript(`
|
async () => {
|
||||||
|
return driver.executeScript(`
|
||||||
const content = document.querySelector("#moz-reader-content, .moz-reader-content");
|
const content = document.querySelector("#moz-reader-content, .moz-reader-content");
|
||||||
const error = document.querySelector(".reader-error");
|
const error = document.querySelector(".reader-error");
|
||||||
const text = content?.innerText?.trim() || "";
|
const text = content?.innerText?.trim() || "";
|
||||||
@@ -481,7 +499,10 @@ async function readerCommand() {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
`);
|
`);
|
||||||
}, timeoutMs, `No readable article content found for URL: ${targetUrl}`);
|
},
|
||||||
|
timeoutMs,
|
||||||
|
`No readable article content found for URL: ${targetUrl}`,
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
cliError("TIMEOUT", err.message);
|
cliError("TIMEOUT", err.message);
|
||||||
}
|
}
|
||||||
@@ -517,6 +538,9 @@ async function main() {
|
|||||||
|
|
||||||
validateCommonOptions();
|
validateCommonOptions();
|
||||||
|
|
||||||
|
// Load Config
|
||||||
|
appConfig = loadConfig({ path: configPath });
|
||||||
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "snapshot":
|
case "snapshot":
|
||||||
return snapshotCommand();
|
return snapshotCommand();
|
||||||
@@ -537,7 +561,12 @@ main()
|
|||||||
.then(printResult)
|
.then(printResult)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
const code = err.code || "COMMAND_FAILED";
|
const code = err.code || "COMMAND_FAILED";
|
||||||
const output = {
|
const output: {
|
||||||
|
ok: false;
|
||||||
|
error: { code: string; message: string };
|
||||||
|
elapsedMs: number;
|
||||||
|
url?: string;
|
||||||
|
} = {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: {
|
error: {
|
||||||
code,
|
code,
|
||||||
@@ -1,7 +1,26 @@
|
|||||||
import { createDriver } from "../driver.js";
|
import { createDriver } from "../driver.js";
|
||||||
|
import type { GlimpseConfig } from "../config.js";
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchKagiOptions {
|
||||||
|
query?: string;
|
||||||
|
token?: string;
|
||||||
|
config?: GlimpseConfig;
|
||||||
|
headless?: boolean;
|
||||||
|
existingUrl?: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
intervalMs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class SearchProviderError extends Error {
|
export class SearchProviderError extends Error {
|
||||||
constructor(code, message) {
|
code: string;
|
||||||
|
|
||||||
|
constructor(code: string, message: string) {
|
||||||
super(message);
|
super(message);
|
||||||
this.code = code;
|
this.code = code;
|
||||||
}
|
}
|
||||||
@@ -17,28 +36,36 @@ export const kagiSearchScript = `return Array.from(document.querySelectorAll("di
|
|||||||
}));`;
|
}));`;
|
||||||
|
|
||||||
// Build Kagi Search Url
|
// Build Kagi Search Url
|
||||||
export function buildKagiSearchUrl(query, token) {
|
export function buildKagiSearchUrl(query: string, token: string): string {
|
||||||
return `https://kagi.com/search?token=${encodeURIComponent(token)}&q=${encodeURIComponent(query)}`;
|
return `https://kagi.com/search?token=${encodeURIComponent(token)}&q=${encodeURIComponent(query)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve Config Token
|
||||||
|
function configToken(config: GlimpseConfig): string | undefined {
|
||||||
|
return config.providers?.kagi?.token;
|
||||||
|
}
|
||||||
|
|
||||||
// Search Kagi
|
// Search Kagi
|
||||||
export async function searchKagi({
|
export async function searchKagi({
|
||||||
query,
|
query,
|
||||||
token = process.env.KAGI_TOKEN,
|
token,
|
||||||
|
config = {},
|
||||||
headless = true,
|
headless = true,
|
||||||
existingUrl,
|
existingUrl,
|
||||||
timeoutMs = 5000,
|
timeoutMs = 5000,
|
||||||
intervalMs = 200,
|
intervalMs = 200,
|
||||||
} = {}) {
|
}: SearchKagiOptions = {}): Promise<SearchResult[]> {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
throw new SearchProviderError("QUERY_REQUIRED", "query is required");
|
throw new SearchProviderError("QUERY_REQUIRED", "query is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Kagi Token
|
// Resolve Kagi Token
|
||||||
if (!token) {
|
const resolvedToken = token || process.env.KAGI_TOKEN || configToken(config);
|
||||||
|
|
||||||
|
if (!resolvedToken) {
|
||||||
throw new SearchProviderError(
|
throw new SearchProviderError(
|
||||||
"KAGI_TOKEN_REQUIRED",
|
"KAGI_TOKEN_REQUIRED",
|
||||||
"Kagi search requires --token or the KAGI_TOKEN environment variable.",
|
"Kagi search requires --token, the KAGI_TOKEN environment variable, or a config token.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,15 +73,18 @@ export async function searchKagi({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Navigate To Kagi
|
// Navigate To Kagi
|
||||||
await driver.get(buildKagiSearchUrl(query, token));
|
await driver.get(buildKagiSearchUrl(query, resolvedToken));
|
||||||
|
|
||||||
// Poll For Results
|
// Poll For Results
|
||||||
let result = [];
|
let result: SearchResult[] = [];
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
|
|
||||||
while (Date.now() - start < timeoutMs) {
|
while (Date.now() - start < timeoutMs) {
|
||||||
result = await driver.executeScript(kagiSearchScript);
|
const scriptResult = await driver.executeScript(kagiSearchScript);
|
||||||
if (Array.isArray(result) && result.length > 0) break;
|
result = Array.isArray(scriptResult)
|
||||||
|
? (scriptResult as SearchResult[])
|
||||||
|
: [];
|
||||||
|
if (result.length > 0) break;
|
||||||
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
||||||
}
|
}
|
||||||
|
|
||||||
212
test/smoke.js
212
test/smoke.js
@@ -1,12 +1,18 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
import {
|
||||||
|
mkdtempSync,
|
||||||
|
rmSync,
|
||||||
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { spawnSync } from "node:child_process";
|
import { spawnSync } from "node:child_process";
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
const cliPath = new URL("../src/index.js", import.meta.url).pathname;
|
const cliPath = new URL("../src/index.ts", import.meta.url).pathname;
|
||||||
const tempDir = mkdtempSync(join(tmpdir(), "glimpse-smoke-"));
|
const tempDir = mkdtempSync(join(tmpdir(), "glimpse-smoke-"));
|
||||||
const filters = process.argv.slice(2).filter((arg) => arg !== "--list");
|
const filters = process.argv.slice(2).filter((arg) => arg !== "--list");
|
||||||
const shouldList = process.argv.includes("--list");
|
const shouldList = process.argv.includes("--list");
|
||||||
@@ -21,7 +27,7 @@ function dataHtml(html) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function runCli(args, options = {}) {
|
function runCli(args, options = {}) {
|
||||||
return spawnSync(process.execPath, [cliPath, ...args], {
|
return spawnSync(process.execPath, ["--import", "tsx", cliPath, ...args], {
|
||||||
encoding: "utf-8",
|
encoding: "utf-8",
|
||||||
env: options.env ?? process.env,
|
env: options.env ?? process.env,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
@@ -36,14 +42,14 @@ function parseJson(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectSuccess(args) {
|
function expectSuccess(args, options = {}) {
|
||||||
const result = runCli(args);
|
const result = runCli(args, options);
|
||||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||||
return parseJson(result.stdout);
|
return parseJson(result.stdout);
|
||||||
}
|
}
|
||||||
|
|
||||||
function expectFailure(args) {
|
function expectFailure(args, options = {}) {
|
||||||
const result = runCli(args);
|
const result = runCli(args, options);
|
||||||
assert.notEqual(result.status, 0, result.stdout || result.stderr);
|
assert.notEqual(result.status, 0, result.stdout || result.stderr);
|
||||||
return parseJson(result.stderr);
|
return parseJson(result.stderr);
|
||||||
}
|
}
|
||||||
@@ -83,7 +89,9 @@ test("help flag prints help", ["help", "cli"], () => {
|
|||||||
test("snapshot returns page metadata and content", ["snapshot"], () => {
|
test("snapshot returns page metadata and content", ["snapshot"], () => {
|
||||||
const output = expectSuccess([
|
const output = expectSuccess([
|
||||||
"snapshot",
|
"snapshot",
|
||||||
dataHtml('<title>Hello</title><h1>Main</h1><a href="/x">X</a><button>Go</button>'),
|
dataHtml(
|
||||||
|
'<title>Hello</title><h1>Main</h1><a href="/x">X</a><button>Go</button>',
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.equal(output.ok, true);
|
assert.equal(output.ok, true);
|
||||||
@@ -98,24 +106,30 @@ test("snapshot returns page metadata and content", ["snapshot"], () => {
|
|||||||
test("snapshot extracts aria headings", ["snapshot"], () => {
|
test("snapshot extracts aria headings", ["snapshot"], () => {
|
||||||
const output = expectSuccess([
|
const output = expectSuccess([
|
||||||
"snapshot",
|
"snapshot",
|
||||||
dataHtml('<title>Hello</title><div role="heading" aria-level="2">ARIA</div>'),
|
dataHtml(
|
||||||
|
'<title>Hello</title><div role="heading" aria-level="2">ARIA</div>',
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
assert.equal(output.ok, true);
|
assert.equal(output.ok, true);
|
||||||
assert.deepEqual(output.result.headings, [{ level: 2, text: "ARIA" }]);
|
assert.deepEqual(output.result.headings, [{ level: 2, text: "ARIA" }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("snapshot runs top-level javascript before extraction", ["snapshot", "js"], () => {
|
test(
|
||||||
const output = expectSuccess([
|
"snapshot runs top-level javascript before extraction",
|
||||||
"snapshot",
|
["snapshot", "js"],
|
||||||
dataHtml("<title>Hello</title><h1>Old</h1>"),
|
() => {
|
||||||
"--js=document.querySelector('h1').textContent = 'New'",
|
const output = expectSuccess([
|
||||||
]);
|
"snapshot",
|
||||||
|
dataHtml("<title>Hello</title><h1>Old</h1>"),
|
||||||
|
"--js=document.querySelector('h1').textContent = 'New'",
|
||||||
|
]);
|
||||||
|
|
||||||
assert.equal(output.ok, true);
|
assert.equal(output.ok, true);
|
||||||
assert.deepEqual(output.result.headings, [{ level: 1, text: "New" }]);
|
assert.deepEqual(output.result.headings, [{ level: 1, text: "New" }]);
|
||||||
assert.equal(output.result.text, "New");
|
assert.equal(output.result.text, "New");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("exec returns javascript result", ["exec", "js"], () => {
|
test("exec returns javascript result", ["exec", "js"], () => {
|
||||||
const result = runCli([
|
const result = runCli([
|
||||||
@@ -128,19 +142,23 @@ test("exec returns javascript result", ["exec", "js"], () => {
|
|||||||
assert.equal(result.stdout.trim(), "Hello");
|
assert.equal(result.stdout.trim(), "Hello");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("screenshot returns standard success envelope and writes file", ["screenshot"], () => {
|
test(
|
||||||
const outputPath = join(tempDir, "page.png");
|
"screenshot returns standard success envelope and writes file",
|
||||||
const output = expectSuccess([
|
["screenshot"],
|
||||||
"screenshot",
|
() => {
|
||||||
dataHtml("<title>Hello</title>"),
|
const outputPath = join(tempDir, "page.png");
|
||||||
`--output=${outputPath}`,
|
const output = expectSuccess([
|
||||||
]);
|
"screenshot",
|
||||||
|
dataHtml("<title>Hello</title>"),
|
||||||
|
`--output=${outputPath}`,
|
||||||
|
]);
|
||||||
|
|
||||||
assert.equal(output.ok, true);
|
assert.equal(output.ok, true);
|
||||||
assert.equal(output.result.path, outputPath);
|
assert.equal(output.result.path, outputPath);
|
||||||
assert.equal(typeof output.elapsedMs, "number");
|
assert.equal(typeof output.elapsedMs, "number");
|
||||||
assert.equal(existsSync(outputPath), true);
|
assert.equal(existsSync(outputPath), true);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("search validates kagi token in provider", ["search", "errors"], () => {
|
test("search validates kagi token in provider", ["search", "errors"], () => {
|
||||||
const env = { ...process.env };
|
const env = { ...process.env };
|
||||||
@@ -152,9 +170,63 @@ test("search validates kagi token in provider", ["search", "errors"], () => {
|
|||||||
assert.equal(output.ok, false);
|
assert.equal(output.ok, false);
|
||||||
assert.equal(output.error.code, "KAGI_TOKEN_REQUIRED");
|
assert.equal(output.error.code, "KAGI_TOKEN_REQUIRED");
|
||||||
assert.match(output.error.message, /Kagi search requires/);
|
assert.match(output.error.message, /Kagi search requires/);
|
||||||
|
assert.match(output.error.message, /config token/);
|
||||||
assert.equal(typeof output.elapsedMs, "number");
|
assert.equal(typeof output.elapsedMs, "number");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"invalid config returns structured error before browser startup",
|
||||||
|
["config", "errors"],
|
||||||
|
() => {
|
||||||
|
const configPath = join(tempDir, "bad-config.json");
|
||||||
|
writeFileSync(configPath, "not json");
|
||||||
|
|
||||||
|
const output = expectFailure([
|
||||||
|
"snapshot",
|
||||||
|
dataHtml("<title>Hello</title>"),
|
||||||
|
`--config=${configPath}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(output.ok, false);
|
||||||
|
assert.equal(output.error.code, "CONFIG_READ_FAILED");
|
||||||
|
assert.match(output.error.message, /Failed to read config file/);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"invalid config schema returns structured error",
|
||||||
|
["config", "errors"],
|
||||||
|
() => {
|
||||||
|
const configPath = join(tempDir, "bad-schema.json");
|
||||||
|
writeFileSync(configPath, JSON.stringify({ search: { provider: 42 } }));
|
||||||
|
|
||||||
|
const output = expectFailure([
|
||||||
|
"snapshot",
|
||||||
|
dataHtml("<title>Hello</title>"),
|
||||||
|
`--config=${configPath}`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert.equal(output.ok, false);
|
||||||
|
assert.equal(output.error.code, "INVALID_CONFIG");
|
||||||
|
assert.match(output.error.message, /search\.provider must be a string/);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("empty home config is accepted", ["config"], () => {
|
||||||
|
const configHome = join(tempDir, "config-home");
|
||||||
|
const configDir = join(configHome, "glimpse");
|
||||||
|
mkdirSync(configDir, { recursive: true });
|
||||||
|
writeFileSync(join(configDir, "config.json"), "{}");
|
||||||
|
|
||||||
|
const output = expectSuccess(
|
||||||
|
["snapshot", dataHtml("<title>Hello</title><h1>Main</h1>")],
|
||||||
|
{ env: { ...process.env, XDG_CONFIG_HOME: configHome } },
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(output.ok, true);
|
||||||
|
assert.equal(output.title, "Hello");
|
||||||
|
});
|
||||||
|
|
||||||
test("unknown command returns structured error", ["errors", "cli"], () => {
|
test("unknown command returns structured error", ["errors", "cli"], () => {
|
||||||
const output = expectFailure(["nope", dataHtml("<title>Hello</title>")]);
|
const output = expectFailure(["nope", dataHtml("<title>Hello</title>")]);
|
||||||
|
|
||||||
@@ -164,18 +236,22 @@ test("unknown command returns structured error", ["errors", "cli"], () => {
|
|||||||
assert.equal(typeof output.elapsedMs, "number");
|
assert.equal(typeof output.elapsedMs, "number");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("invalid timeout returns invalid option before browser startup", ["errors", "timeout"], () => {
|
test(
|
||||||
const output = expectFailure([
|
"invalid timeout returns invalid option before browser startup",
|
||||||
"snapshot",
|
["errors", "timeout"],
|
||||||
dataHtml("<title>Hello</title>"),
|
() => {
|
||||||
"--timeout=abc",
|
const output = expectFailure([
|
||||||
]);
|
"snapshot",
|
||||||
|
dataHtml("<title>Hello</title>"),
|
||||||
|
"--timeout=abc",
|
||||||
|
]);
|
||||||
|
|
||||||
assert.equal(output.ok, false);
|
assert.equal(output.ok, false);
|
||||||
assert.equal(output.error.code, "INVALID_OPTION");
|
assert.equal(output.error.code, "INVALID_OPTION");
|
||||||
assert.match(output.error.message, /--timeout must be a positive integer/);
|
assert.match(output.error.message, /--timeout must be a positive integer/);
|
||||||
assert.equal(typeof output.elapsedMs, "number");
|
assert.equal(typeof output.elapsedMs, "number");
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("invalid wait-until returns invalid option", ["errors", "wait"], () => {
|
test("invalid wait-until returns invalid option", ["errors", "wait"], () => {
|
||||||
const output = expectFailure([
|
const output = expectFailure([
|
||||||
@@ -215,31 +291,39 @@ test("wait-js timeout returns wait timeout", ["wait", "errors"], () => {
|
|||||||
assert.match(output.url, /^data:text\/html,/);
|
assert.match(output.url, /^data:text\/html,/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("wait-js exception returns script failed", ["wait", "errors", "js"], () => {
|
test(
|
||||||
const output = expectFailure([
|
"wait-js exception returns script failed",
|
||||||
"snapshot",
|
["wait", "errors", "js"],
|
||||||
dataHtml("<title>Hello</title>"),
|
() => {
|
||||||
'--wait-js=throw new Error("boom")',
|
const output = expectFailure([
|
||||||
]);
|
"snapshot",
|
||||||
|
dataHtml("<title>Hello</title>"),
|
||||||
|
'--wait-js=throw new Error("boom")',
|
||||||
|
]);
|
||||||
|
|
||||||
assert.equal(output.ok, false);
|
assert.equal(output.ok, false);
|
||||||
assert.equal(output.error.code, "SCRIPT_FAILED");
|
assert.equal(output.error.code, "SCRIPT_FAILED");
|
||||||
assert.match(output.error.message, /--wait-js failed/);
|
assert.match(output.error.message, /--wait-js failed/);
|
||||||
assert.match(output.error.message, /boom/);
|
assert.match(output.error.message, /boom/);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test("top-level javascript exception returns script failed", ["errors", "js"], () => {
|
test(
|
||||||
const output = expectFailure([
|
"top-level javascript exception returns script failed",
|
||||||
"snapshot",
|
["errors", "js"],
|
||||||
dataHtml("<title>Hello</title>"),
|
() => {
|
||||||
'--js=throw new Error("boom")',
|
const output = expectFailure([
|
||||||
]);
|
"snapshot",
|
||||||
|
dataHtml("<title>Hello</title>"),
|
||||||
|
'--js=throw new Error("boom")',
|
||||||
|
]);
|
||||||
|
|
||||||
assert.equal(output.ok, false);
|
assert.equal(output.ok, false);
|
||||||
assert.equal(output.error.code, "SCRIPT_FAILED");
|
assert.equal(output.error.code, "SCRIPT_FAILED");
|
||||||
assert.match(output.error.message, /Prelude script failed/);
|
assert.match(output.error.message, /Prelude script failed/);
|
||||||
assert.match(output.error.message, /boom/);
|
assert.match(output.error.message, /boom/);
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
function listTests() {
|
function listTests() {
|
||||||
const tags = [...new Set(tests.flatMap((entry) => entry.tags))].sort();
|
const tags = [...new Set(tests.flatMap((entry) => entry.tags))].sort();
|
||||||
|
|||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user