feat(config): add TypeScript build and config support

This commit is contained in:
2026-04-27 08:53:17 -04:00
parent 2f83fa3117
commit 6b3ec32b3a
12 changed files with 1043 additions and 127 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -52,7 +52,7 @@ If running outside Nix, document that Firefox and geckodriver must be on `PATH`.
## 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.
- Add short Title Case comments above cohesive logic blocks.
- Prefer exact, actionable error messages.

View File

@@ -20,7 +20,8 @@ If running directly with Node.js, install dependencies and make sure `firefox` a
```bash
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
@@ -31,6 +32,7 @@ glimpse <command> [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
- `--url=<server>` - connect to an existing WebDriver server
- `--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.
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
KAGI_TOKEN=... nix run .#glimpse -- search --provider=kagi "nix flakes selenium webdriver"
nix run .#glimpse -- search "nix flakes selenium webdriver"
```
Local usage:
```bash
KAGI_TOKEN=... ./result/bin/glimpse search "nix flakes selenium webdriver"
./result/bin/glimpse search "nix flakes selenium webdriver"
```
Options:
- `--provider=<provider>` - search provider: `kagi` (default: `kagi`)
- `--token=<token>` - Kagi token (default: `KAGI_TOKEN`)
- `--provider=<provider>` - search provider: `kagi` (default: config or `kagi`)
- `--token=<token>` - Kagi token (default: `KAGI_TOKEN` or config)
- `--no-headless` - show Firefox instead of running headless
- `--url=<server>` - connect to an existing WebDriver server
- `--timeout=<ms>` - wait time for results before returning `[]` (default: `10000`)
@@ -217,7 +242,7 @@ Run the built tool:
```bash
./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
@@ -253,16 +278,19 @@ node test/smoke.js snapshot js
Useful local commands:
```bash
node src/index.js snapshot 'data:text/html,<title>Hello</title><h1>Hello</h1>'
node src/index.js exec 'data:text/html,<title>Hello</title>' --js='return document.title'
node src/index.js screenshot 'data:text/html,<title>Hello</title>' --output=/tmp/page.png
node src/index.js reader 'https://example.com/article'
npm run build
node dist/src/index.js snapshot 'data:text/html,<title>Hello</title><h1>Hello</h1>'
node dist/src/index.js exec 'data:text/html,<title>Hello</title>' --js='return document.title'
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
- `src/index.js` - `glimpse` CLI with subcommands, including Firefox Reader View extraction and provider-backed search
- `src/driver.js` - Firefox WebDriver creation and geckodriver resolution
- `src/providers/kagi.js` - reusable Kagi search provider implementation
- `src/index.ts` - `glimpse` CLI with subcommands, including Firefox Reader View extraction and provider-backed search
- `src/config.ts` - home-dir config loading for CLI defaults and provider settings
- `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
- `KAGI.md` - Kagi-specific notes

View File

@@ -42,8 +42,8 @@
version = "1.0.0";
src = source;
npmDepsHash = "sha256-IWzSvrGgkoR6gg7P1m/mwakGOOKmm2OFtBirKgE09Ds=";
dontNpmBuild = true;
npmDepsHash = "sha256-ycAjPZZqI3ZMIUubJbWy8G6X6LaXDcgdZGswikfkQj8=";
npmBuildScript = "build";
nativeBuildInputs = [ pkgs.makeWrapper ];

610
package-lock.json generated
View File

@@ -14,7 +14,14 @@
"turndown": "^7.2.4"
},
"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": {
@@ -23,6 +30,448 @@
"integrity": "sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==",
"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": {
"version": "2.2.0",
"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_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": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"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": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
@@ -449,6 +1006,16 @@
"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": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@@ -504,6 +1071,26 @@
"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": {
"version": "7.2.4",
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.4.tgz",
@@ -517,6 +1104,27 @@
"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": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -3,13 +3,14 @@
"version": "1.0.0",
"description": "",
"type": "module",
"main": "src/index.js",
"main": "dist/src/index.js",
"bin": {
"glimpse": "./src/index.js"
"glimpse": "./dist/src/index.js"
},
"scripts": {
"lint": "oxlint --ignore-pattern node_modules --ignore-pattern .direnv .",
"start": "node src/index.js",
"build": "tsc && chmod +x dist/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:smoke": "node test/smoke.js",
"test:list": "node test/smoke.js --list",
@@ -23,10 +24,17 @@
},
"keywords": [],
"author": "",
"license": "ISC",
"license": "MIT",
"dependencies": {
"oxlint": "^1.61.0",
"selenium-webdriver": "^4.43.0",
"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
View 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;
}

View File

@@ -1,13 +1,13 @@
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";
/**
* Resolve the geckodriver path from $PATH.
*
* @returns {string}
*/
function findGeckodriver() {
export interface DriverOptions {
headless?: boolean;
existingUrl?: string;
}
function findGeckodriver(): string {
try {
return execFileSync("which", ["geckodriver"], { encoding: "utf-8" }).trim();
} catch {
@@ -17,16 +17,10 @@ function findGeckodriver() {
}
}
/**
* Create a Firefox WebDriver instance.
*
* @param {object} opts
* @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 } = {}) {
export async function createDriver({
headless = false,
existingUrl,
}: DriverOptions = {}): Promise<WebDriver> {
const options = new firefox.Options();
// Configure Headless
@@ -34,7 +28,9 @@ export async function createDriver({ headless = false, existingUrl } = {}) {
options.addArguments("--headless");
}
const builder = new Builder().forBrowser("firefox").setFirefoxOptions(options);
const builder = new Builder()
.forBrowser("firefox")
.setFirefoxOptions(options);
// Connect to Existing Server
if (existingUrl) {

View File

@@ -1,5 +1,6 @@
#!/usr/bin/env node
import { loadConfig, type GlimpseConfig } from "./config.js";
import { createDriver } from "./driver.js";
import { searchKagi } from "./providers/kagi.js";
import { readFileSync, writeFileSync } from "node:fs";
@@ -8,7 +9,7 @@ import TurndownService from "turndown";
const DEFAULT_TIMEOUT_MS = 10000;
const POLL_INTERVAL_MS = 200;
const startTime = Date.now();
const runContext = {};
const runContext: { targetUrl?: string; currentUrl?: string } = {};
// Parse CLI Args
const [command, ...args] = process.argv.slice(2);
@@ -18,6 +19,8 @@ const inlineJs = getOption("--js");
const scriptPath = getOption("--script");
const waitJs = getOption("--wait-js");
const waitUntil = getOption("--wait-until") ?? "none";
const configPath = getOption("--config");
let appConfig: GlimpseConfig = {};
let timeoutMs = DEFAULT_TIMEOUT_MS;
function getOption(name) {
@@ -50,6 +53,9 @@ function printResult(result) {
}
class CliError extends Error {
code: string;
details: Record<string, unknown>;
constructor(code, message, details = {}) {
super(message);
this.code = code;
@@ -84,6 +90,7 @@ Common Options:
--wait-until=<state> Wait for readiness: none, interactive, complete (default: none)
--js=<code> Execute inline JS before command logic
--script=<file> Execute JS from a file before command logic
--config=<file> Read config from a custom path
Exec Options:
--js=<code> Return the top-level JS result
@@ -97,8 +104,8 @@ Reader Options:
--output=<file> Write output to a file
Search Options:
--provider=<provider> Search provider: kagi (default: kagi)
--token=<token> Kagi token (default: KAGI_TOKEN)
--provider=<provider> Search provider: kagi (default: config or kagi)
--token=<token> Kagi token (default: KAGI_TOKEN or config)
Examples:
glimpse snapshot https://example.com
@@ -114,7 +121,10 @@ function printHelp() {
}
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() {
@@ -179,7 +189,9 @@ async function waitForReadyState(driver) {
try {
await driver.wait(async () => {
const readyState = await driver.executeScript("return document.readyState");
const readyState = await driver.executeScript(
"return document.readyState",
);
return waitUntil === "interactive"
? ["interactive", "complete"].includes(readyState)
: readyState === "complete";
@@ -214,7 +226,10 @@ async function waitForJs(driver) {
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) {
@@ -417,7 +432,8 @@ function renderReaderOutput(article, format) {
}
async function searchCommand() {
const provider = getOption("--provider") ?? "kagi";
const provider =
getOption("--provider") ?? appConfig.search?.provider ?? "kagi";
const query = getPositionalArgs().join(" ");
if (!query) usage();
@@ -428,6 +444,7 @@ async function searchCommand() {
return searchKagi({
query,
token: getOption("--token"),
config: appConfig,
headless,
existingUrl,
timeoutMs,
@@ -458,7 +475,8 @@ async function readerCommand() {
// Wait For Reader Content
let article;
try {
article = await driver.wait(async () => {
article = await driver.wait(
async () => {
return driver.executeScript(`
const content = document.querySelector("#moz-reader-content, .moz-reader-content");
const error = document.querySelector(".reader-error");
@@ -481,7 +499,10 @@ async function readerCommand() {
return null;
`);
}, timeoutMs, `No readable article content found for URL: ${targetUrl}`);
},
timeoutMs,
`No readable article content found for URL: ${targetUrl}`,
);
} catch (err) {
cliError("TIMEOUT", err.message);
}
@@ -517,6 +538,9 @@ async function main() {
validateCommonOptions();
// Load Config
appConfig = loadConfig({ path: configPath });
switch (command) {
case "snapshot":
return snapshotCommand();
@@ -537,7 +561,12 @@ main()
.then(printResult)
.catch((err) => {
const code = err.code || "COMMAND_FAILED";
const output = {
const output: {
ok: false;
error: { code: string; message: string };
elapsedMs: number;
url?: string;
} = {
ok: false,
error: {
code,

View File

@@ -1,7 +1,26 @@
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 {
constructor(code, message) {
code: string;
constructor(code: string, message: string) {
super(message);
this.code = code;
}
@@ -17,28 +36,36 @@ export const kagiSearchScript = `return Array.from(document.querySelectorAll("di
}));`;
// 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)}`;
}
// Resolve Config Token
function configToken(config: GlimpseConfig): string | undefined {
return config.providers?.kagi?.token;
}
// Search Kagi
export async function searchKagi({
query,
token = process.env.KAGI_TOKEN,
token,
config = {},
headless = true,
existingUrl,
timeoutMs = 5000,
intervalMs = 200,
} = {}) {
}: SearchKagiOptions = {}): Promise<SearchResult[]> {
if (!query) {
throw new SearchProviderError("QUERY_REQUIRED", "query is required");
}
// Validate Kagi Token
if (!token) {
// Resolve Kagi Token
const resolvedToken = token || process.env.KAGI_TOKEN || configToken(config);
if (!resolvedToken) {
throw new SearchProviderError(
"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 {
// Navigate To Kagi
await driver.get(buildKagiSearchUrl(query, token));
await driver.get(buildKagiSearchUrl(query, resolvedToken));
// Poll For Results
let result = [];
let result: SearchResult[] = [];
const start = Date.now();
while (Date.now() - start < timeoutMs) {
result = await driver.executeScript(kagiSearchScript);
if (Array.isArray(result) && result.length > 0) break;
const scriptResult = await driver.executeScript(kagiSearchScript);
result = Array.isArray(scriptResult)
? (scriptResult as SearchResult[])
: [];
if (result.length > 0) break;
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}

View File

@@ -1,12 +1,18 @@
#!/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 { join } from "node:path";
import { spawnSync } from "node:child_process";
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 filters = process.argv.slice(2).filter((arg) => arg !== "--list");
const shouldList = process.argv.includes("--list");
@@ -21,7 +27,7 @@ function dataHtml(html) {
}
function runCli(args, options = {}) {
return spawnSync(process.execPath, [cliPath, ...args], {
return spawnSync(process.execPath, ["--import", "tsx", cliPath, ...args], {
encoding: "utf-8",
env: options.env ?? process.env,
timeout: 30000,
@@ -36,14 +42,14 @@ function parseJson(text) {
}
}
function expectSuccess(args) {
const result = runCli(args);
function expectSuccess(args, options = {}) {
const result = runCli(args, options);
assert.equal(result.status, 0, result.stderr || result.stdout);
return parseJson(result.stdout);
}
function expectFailure(args) {
const result = runCli(args);
function expectFailure(args, options = {}) {
const result = runCli(args, options);
assert.notEqual(result.status, 0, result.stdout || 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"], () => {
const output = expectSuccess([
"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);
@@ -98,14 +106,19 @@ test("snapshot returns page metadata and content", ["snapshot"], () => {
test("snapshot extracts aria headings", ["snapshot"], () => {
const output = expectSuccess([
"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.deepEqual(output.result.headings, [{ level: 2, text: "ARIA" }]);
});
test("snapshot runs top-level javascript before extraction", ["snapshot", "js"], () => {
test(
"snapshot runs top-level javascript before extraction",
["snapshot", "js"],
() => {
const output = expectSuccess([
"snapshot",
dataHtml("<title>Hello</title><h1>Old</h1>"),
@@ -115,7 +128,8 @@ test("snapshot runs top-level javascript before extraction", ["snapshot", "js"],
assert.equal(output.ok, true);
assert.deepEqual(output.result.headings, [{ level: 1, text: "New" }]);
assert.equal(output.result.text, "New");
});
},
);
test("exec returns javascript result", ["exec", "js"], () => {
const result = runCli([
@@ -128,7 +142,10 @@ test("exec returns javascript result", ["exec", "js"], () => {
assert.equal(result.stdout.trim(), "Hello");
});
test("screenshot returns standard success envelope and writes file", ["screenshot"], () => {
test(
"screenshot returns standard success envelope and writes file",
["screenshot"],
() => {
const outputPath = join(tempDir, "page.png");
const output = expectSuccess([
"screenshot",
@@ -140,7 +157,8 @@ test("screenshot returns standard success envelope and writes file", ["screensho
assert.equal(output.result.path, outputPath);
assert.equal(typeof output.elapsedMs, "number");
assert.equal(existsSync(outputPath), true);
});
},
);
test("search validates kagi token in provider", ["search", "errors"], () => {
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.error.code, "KAGI_TOKEN_REQUIRED");
assert.match(output.error.message, /Kagi search requires/);
assert.match(output.error.message, /config token/);
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"], () => {
const output = expectFailure(["nope", dataHtml("<title>Hello</title>")]);
@@ -164,7 +236,10 @@ test("unknown command returns structured error", ["errors", "cli"], () => {
assert.equal(typeof output.elapsedMs, "number");
});
test("invalid timeout returns invalid option before browser startup", ["errors", "timeout"], () => {
test(
"invalid timeout returns invalid option before browser startup",
["errors", "timeout"],
() => {
const output = expectFailure([
"snapshot",
dataHtml("<title>Hello</title>"),
@@ -175,7 +250,8 @@ test("invalid timeout returns invalid option before browser startup", ["errors",
assert.equal(output.error.code, "INVALID_OPTION");
assert.match(output.error.message, /--timeout must be a positive integer/);
assert.equal(typeof output.elapsedMs, "number");
});
},
);
test("invalid wait-until returns invalid option", ["errors", "wait"], () => {
const output = expectFailure([
@@ -215,7 +291,10 @@ test("wait-js timeout returns wait timeout", ["wait", "errors"], () => {
assert.match(output.url, /^data:text\/html,/);
});
test("wait-js exception returns script failed", ["wait", "errors", "js"], () => {
test(
"wait-js exception returns script failed",
["wait", "errors", "js"],
() => {
const output = expectFailure([
"snapshot",
dataHtml("<title>Hello</title>"),
@@ -226,9 +305,13 @@ test("wait-js exception returns script failed", ["wait", "errors", "js"], () =>
assert.equal(output.error.code, "SCRIPT_FAILED");
assert.match(output.error.message, /--wait-js failed/);
assert.match(output.error.message, /boom/);
});
},
);
test("top-level javascript exception returns script failed", ["errors", "js"], () => {
test(
"top-level javascript exception returns script failed",
["errors", "js"],
() => {
const output = expectFailure([
"snapshot",
dataHtml("<title>Hello</title>"),
@@ -239,7 +322,8 @@ test("top-level javascript exception returns script failed", ["errors", "js"], (
assert.equal(output.error.code, "SCRIPT_FAILED");
assert.match(output.error.message, /Prelude script failed/);
assert.match(output.error.message, /boom/);
});
},
);
function listTests() {
const tags = [...new Set(tests.flatMap((entry) => entry.tags))].sort();

15
tsconfig.json Normal file
View 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"]
}