feat: comma-separated flags to REQ

This commit is contained in:
Alejandro Gómez
2025-12-11 16:57:40 +01:00
parent 78082ca6db
commit 54ee43bdaf
8 changed files with 973 additions and 62 deletions

View File

@@ -78,9 +78,50 @@ Use hooks like `useProfile()`, `useNostrEvent()`, `useTimeline()` - they handle
- Don't traverse or modify layout tree manually
- Adding/removing windows handled by `logic.ts` functions
## Testing
**Test Framework**: Vitest with node environment
**Running Tests**:
```bash
npm test # Watch mode (auto-runs on file changes)
npm run test:ui # Visual UI for test exploration
npm run test:run # Single run (CI mode)
```
**Test Conventions**:
- Test files: `*.test.ts` or `*.test.tsx` colocated with source files
- Focus on testing pure functions and parsing logic
- Use descriptive test names that explain behavior
- Group related tests with `describe` blocks
**What to Test**:
- **Parsers** (`src/lib/*-parser.ts`): All argument parsing logic, edge cases, validation
- **Pure functions** (`src/core/logic.ts`): State mutations, business logic
- **Utilities** (`src/lib/*.ts`): Helper functions, data transformations
- **Not UI components**: React components tested manually (for now)
**Example Test Structure**:
```typescript
describe("parseReqCommand", () => {
describe("kind flag (-k, --kind)", () => {
it("should parse single kind", () => {
const result = parseReqCommand(["-k", "1"]);
expect(result.filter.kinds).toEqual([1]);
});
it("should deduplicate kinds", () => {
const result = parseReqCommand(["-k", "1,3,1"]);
expect(result.filter.kinds).toEqual([1, 3]);
});
});
});
```
## Critical Notes
- React 19 features in use (ensure compatibility)
- LocalStorage persistence has quota handling built-in
- Dark mode is default (controlled via HTML class)
- EventStore handles event deduplication and replaceability automatically
- Run tests before committing changes to parsers or core logic

456
package-lock.json generated
View File

@@ -45,6 +45,7 @@
"@types/react-dom": "^19.2.3",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/ui": "^4.0.15",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.1.8",
@@ -57,7 +58,8 @@
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
"vite": "^6.0.5",
"vitest": "^4.0.15"
}
},
"node_modules/@alloc/quick-lru": {
@@ -1565,6 +1567,13 @@
"url": "https://opencollective.com/pkgr"
}
},
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
"integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"dev": true,
"license": "MIT"
},
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -3283,6 +3292,13 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -3328,6 +3344,17 @@
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
}
},
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -3337,6 +3364,13 @@
"@types/ms": "*"
}
},
"node_modules/@types/deep-eql": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -3719,6 +3753,160 @@
"node": ">=0.10.0"
}
},
"node_modules/@vitest/expect": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz",
"integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2",
"@vitest/spy": "4.0.15",
"@vitest/utils": "4.0.15",
"chai": "^6.2.1",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/mocker": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz",
"integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "4.0.15",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"msw": "^2.4.9",
"vite": "^6.0.0 || ^7.0.0-0"
},
"peerDependenciesMeta": {
"msw": {
"optional": true
},
"vite": {
"optional": true
}
}
},
"node_modules/@vitest/pretty-format": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz",
"integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz",
"integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.0.15",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitest/snapshot": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz",
"integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.0.15",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitest/spy": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz",
"integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/ui": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.0.15.tgz",
"integrity": "sha512-sxSyJMaKp45zI0u+lHrPuZM1ZJQ8FaVD35k+UxVrha1yyvQ+TZuUYllUixwvQXlB7ixoDc7skf3lQPopZIvaQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "4.0.15",
"fflate": "^0.8.2",
"flatted": "^3.3.3",
"pathe": "^2.0.3",
"sirv": "^3.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"vitest": "4.0.15"
}
},
"node_modules/@vitest/ui/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/@vitest/utils": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz",
"integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "4.0.15",
"tinyrainbow": "^3.0.3"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -4016,6 +4204,16 @@
"node": ">=10"
}
},
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
}
},
"node_modules/autoprefixer": {
"version": "10.4.22",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz",
@@ -4226,6 +4424,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chai": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz",
"integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -4914,6 +5122,16 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -4937,6 +5155,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
"integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -5010,6 +5238,13 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"dev": true,
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -5800,6 +6035,16 @@
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
"node_modules/markdown-table": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
@@ -6702,6 +6947,16 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/mrmime": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6926,6 +7181,17 @@
"rxjs": ">=6.0.0"
}
},
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
"integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
"license": "MIT"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -8043,6 +8309,13 @@
"node": ">=8"
}
},
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true,
"license": "ISC"
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -8056,6 +8329,21 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sirv": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
"integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@polka/url": "^1.0.0-next.24",
"mrmime": "^2.0.0",
"totalist": "^3.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -8112,6 +8400,20 @@
"dev": true,
"license": "CC0-1.0"
},
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true,
"license": "MIT"
},
"node_modules/std-env": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
"dev": true,
"license": "MIT"
},
"node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
@@ -8448,6 +8750,23 @@
"node": ">=0.8"
}
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
"integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
"dev": true,
"license": "MIT"
},
"node_modules/tinyexec": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
"integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -8496,6 +8815,16 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tinyrainbow": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
"integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -8509,6 +8838,16 @@
"node": ">=8.0"
}
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
"integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -9013,6 +9352,104 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vitest": {
"version": "4.0.15",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz",
"integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "4.0.15",
"@vitest/mocker": "4.0.15",
"@vitest/pretty-format": "4.0.15",
"@vitest/runner": "4.0.15",
"@vitest/snapshot": "4.0.15",
"@vitest/spy": "4.0.15",
"@vitest/utils": "4.0.15",
"es-module-lexer": "^1.7.0",
"expect-type": "^1.2.2",
"magic-string": "^0.30.21",
"obug": "^2.1.1",
"pathe": "^2.0.3",
"picomatch": "^4.0.3",
"std-env": "^3.10.0",
"tinybench": "^2.9.0",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
"tinyrainbow": "^3.0.3",
"vite": "^6.0.0 || ^7.0.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
"vitest": "vitest.mjs"
},
"engines": {
"node": "^20.0.0 || ^22.0.0 || >=24.0.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
"@vitest/browser-playwright": "4.0.15",
"@vitest/browser-preview": "4.0.15",
"@vitest/browser-webdriverio": "4.0.15",
"@vitest/ui": "4.0.15",
"happy-dom": "*",
"jsdom": "*"
},
"peerDependenciesMeta": {
"@edge-runtime/vm": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
"@types/node": {
"optional": true
},
"@vitest/browser-playwright": {
"optional": true
},
"@vitest/browser-preview": {
"optional": true
},
"@vitest/browser-webdriverio": {
"optional": true
},
"@vitest/ui": {
"optional": true
},
"happy-dom": {
"optional": true
},
"jsdom": {
"optional": true
}
}
},
"node_modules/vitest/node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
"dev": true,
"license": "MIT"
},
"node_modules/vitest/node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/which": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz",
@@ -9029,6 +9466,23 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
"integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
"dev": true,
"license": "MIT",
"dependencies": {
"siginfo": "^2.0.0",
"stackback": "0.0.2"
},
"bin": {
"why-is-node-running": "cli.js"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",

View File

@@ -10,7 +10,10 @@
"lint:fix": "eslint . --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,js,jsx,json,css}\"",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
@@ -50,6 +53,7 @@
"@types/react-dom": "^19.2.3",
"@types/uuid": "^10.0.0",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/ui": "^4.0.15",
"autoprefixer": "^10.4.20",
"eslint": "^9.17.0",
"eslint-config-prettier": "^10.1.8",
@@ -62,6 +66,7 @@
"tailwindcss": "^3.4.17",
"typescript": "~5.6.2",
"typescript-eslint": "^8.18.2",
"vite": "^6.0.5"
"vite": "^6.0.5",
"vitest": "^4.0.15"
}
}

View File

@@ -163,7 +163,7 @@ export default function ReqViewer({
Authors: {filter.authors.length}
</span>
{nip05Authors && nip05Authors.length > 0 && (
<div className="text-xs text-blue-500 ml-2">
<div className="text-xs text-muted-foreground ml-2">
{nip05Authors.map((nip05) => (
<div key={nip05}> {nip05}</div>
))}
@@ -178,7 +178,7 @@ export default function ReqViewer({
#p Tags: {filter["#p"].length}
</span>
{nip05PTags && nip05PTags.length > 0 && (
<div className="text-xs text-blue-500 ml-2">
<div className="text-xs text-muted-foreground ml-2">
{nip05PTags.map((nip05) => (
<div key={nip05}> {nip05}</div>
))}

326
src/lib/req-parser.test.ts Normal file
View File

@@ -0,0 +1,326 @@
import { describe, it, expect, vi } from "vitest";
import { parseReqCommand } from "./req-parser";
describe("parseReqCommand", () => {
describe("kind flag (-k, --kind)", () => {
it("should parse single kind", () => {
const result = parseReqCommand(["-k", "1"]);
expect(result.filter.kinds).toEqual([1]);
});
it("should parse comma-separated kinds", () => {
const result = parseReqCommand(["-k", "1,3,7"]);
expect(result.filter.kinds).toEqual([1, 3, 7]);
});
it("should parse comma-separated kinds with spaces", () => {
const result = parseReqCommand(["-k", "1, 3, 7"]);
expect(result.filter.kinds).toEqual([1, 3, 7]);
});
it("should deduplicate kinds", () => {
const result = parseReqCommand(["-k", "1,3,1,3"]);
expect(result.filter.kinds).toEqual([1, 3]);
});
it("should deduplicate across multiple -k flags", () => {
const result = parseReqCommand(["-k", "1", "-k", "3", "-k", "1"]);
expect(result.filter.kinds).toEqual([1, 3]);
});
it("should handle --kind long form", () => {
const result = parseReqCommand(["--kind", "1,3,7"]);
expect(result.filter.kinds).toEqual([1, 3, 7]);
});
it("should ignore invalid kinds", () => {
const result = parseReqCommand(["-k", "1,invalid,3"]);
expect(result.filter.kinds).toEqual([1, 3]);
});
});
describe("author flag (-a, --author)", () => {
it("should parse hex pubkey", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-a", hex]);
expect(result.filter.authors).toEqual([hex]);
});
it("should parse comma-separated hex pubkeys", () => {
const hex1 = "a".repeat(64);
const hex2 = "b".repeat(64);
const result = parseReqCommand(["-a", `${hex1},${hex2}`]);
expect(result.filter.authors).toEqual([hex1, hex2]);
});
it("should deduplicate authors", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-a", `${hex},${hex}`]);
expect(result.filter.authors).toEqual([hex]);
});
it("should accumulate NIP-05 identifiers for async resolution", () => {
const result = parseReqCommand([
"-a",
"user@domain.com,alice@example.com",
]);
expect(result.nip05Authors).toEqual(["user@domain.com", "alice@example.com"]);
expect(result.filter.authors).toBeUndefined();
});
it("should handle mixed hex and NIP-05", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-a", `${hex},user@domain.com`]);
expect(result.filter.authors).toEqual([hex]);
expect(result.nip05Authors).toEqual(["user@domain.com"]);
});
it("should deduplicate NIP-05 identifiers", () => {
const result = parseReqCommand([
"-a",
"user@domain.com,user@domain.com",
]);
expect(result.nip05Authors).toEqual(["user@domain.com"]);
});
});
describe("event ID flag (-e)", () => {
it("should parse hex event ID", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-e", hex]);
expect(result.filter["#e"]).toEqual([hex]);
});
it("should parse comma-separated event IDs", () => {
const hex1 = "a".repeat(64);
const hex2 = "b".repeat(64);
const result = parseReqCommand(["-e", `${hex1},${hex2}`]);
expect(result.filter["#e"]).toEqual([hex1, hex2]);
});
it("should deduplicate event IDs", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-e", `${hex},${hex}`]);
expect(result.filter["#e"]).toEqual([hex]);
});
});
describe("pubkey tag flag (-p)", () => {
it("should parse hex pubkey for #p tag", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-p", hex]);
expect(result.filter["#p"]).toEqual([hex]);
});
it("should parse comma-separated pubkeys", () => {
const hex1 = "a".repeat(64);
const hex2 = "b".repeat(64);
const result = parseReqCommand(["-p", `${hex1},${hex2}`]);
expect(result.filter["#p"]).toEqual([hex1, hex2]);
});
it("should accumulate NIP-05 identifiers for #p tags", () => {
const result = parseReqCommand(["-p", "user@domain.com,alice@example.com"]);
expect(result.nip05PTags).toEqual(["user@domain.com", "alice@example.com"]);
expect(result.filter["#p"]).toBeUndefined();
});
it("should deduplicate #p tags", () => {
const hex = "a".repeat(64);
const result = parseReqCommand(["-p", `${hex},${hex}`]);
expect(result.filter["#p"]).toEqual([hex]);
});
});
describe("hashtag flag (-t)", () => {
it("should parse single hashtag", () => {
const result = parseReqCommand(["-t", "nostr"]);
expect(result.filter["#t"]).toEqual(["nostr"]);
});
it("should parse comma-separated hashtags", () => {
const result = parseReqCommand(["-t", "nostr,bitcoin,lightning"]);
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin", "lightning"]);
});
it("should parse comma-separated hashtags with spaces", () => {
const result = parseReqCommand(["-t", "nostr, bitcoin, lightning"]);
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin", "lightning"]);
});
it("should deduplicate hashtags", () => {
const result = parseReqCommand(["-t", "nostr,bitcoin,nostr"]);
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin"]);
});
});
describe("d-tag flag (-d)", () => {
it("should parse single d-tag", () => {
const result = parseReqCommand(["-d", "article1"]);
expect(result.filter["#d"]).toEqual(["article1"]);
});
it("should parse comma-separated d-tags", () => {
const result = parseReqCommand(["-d", "article1,article2,article3"]);
expect(result.filter["#d"]).toEqual(["article1", "article2", "article3"]);
});
it("should deduplicate d-tags", () => {
const result = parseReqCommand(["-d", "article1,article2,article1"]);
expect(result.filter["#d"]).toEqual(["article1", "article2"]);
});
});
describe("limit flag (-l, --limit)", () => {
it("should parse limit", () => {
const result = parseReqCommand(["-l", "100"]);
expect(result.filter.limit).toBe(100);
});
it("should handle --limit long form", () => {
const result = parseReqCommand(["--limit", "50"]);
expect(result.filter.limit).toBe(50);
});
});
describe("time flags (--since, --until)", () => {
it("should parse unix timestamp for --since", () => {
const result = parseReqCommand(["--since", "1234567890"]);
expect(result.filter.since).toBe(1234567890);
});
it("should parse relative time for --since (hours)", () => {
const result = parseReqCommand(["--since", "2h"]);
expect(result.filter.since).toBeDefined();
expect(result.filter.since).toBeGreaterThan(0);
});
it("should parse relative time for --since (days)", () => {
const result = parseReqCommand(["--since", "7d"]);
expect(result.filter.since).toBeDefined();
expect(result.filter.since).toBeGreaterThan(0);
});
it("should parse unix timestamp for --until", () => {
const result = parseReqCommand(["--until", "1234567890"]);
expect(result.filter.until).toBe(1234567890);
});
});
describe("search flag (--search)", () => {
it("should parse search query", () => {
const result = parseReqCommand(["--search", "bitcoin"]);
expect(result.filter.search).toBe("bitcoin");
});
});
describe("relay parsing", () => {
it("should parse relay with wss:// protocol", () => {
const result = parseReqCommand(["wss://relay.example.com"]);
expect(result.relays).toEqual(["wss://relay.example.com"]);
});
it("should parse relay domain and add wss://", () => {
const result = parseReqCommand(["relay.example.com"]);
expect(result.relays).toEqual(["wss://relay.example.com"]);
});
it("should parse multiple relays", () => {
const result = parseReqCommand([
"wss://relay1.com",
"relay2.com",
"wss://relay3.com",
]);
expect(result.relays).toEqual([
"wss://relay1.com",
"wss://relay2.com",
"wss://relay3.com",
]);
});
});
describe("close-on-eose flag", () => {
it("should parse --close-on-eose", () => {
const result = parseReqCommand(["--close-on-eose"]);
expect(result.closeOnEose).toBe(true);
});
it("should default to false when not provided", () => {
const result = parseReqCommand(["-k", "1"]);
expect(result.closeOnEose).toBe(false);
});
});
describe("complex scenarios", () => {
it("should handle multiple flags together", () => {
const hex = "a".repeat(64);
const result = parseReqCommand([
"-k",
"1,3",
"-a",
hex,
"-t",
"nostr,bitcoin",
"-l",
"100",
"--since",
"1h",
"relay.example.com",
]);
expect(result.filter.kinds).toEqual([1, 3]);
expect(result.filter.authors).toEqual([hex]);
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin"]);
expect(result.filter.limit).toBe(100);
expect(result.filter.since).toBeDefined();
expect(result.relays).toEqual(["wss://relay.example.com"]);
});
it("should handle deduplication across multiple flags and commas", () => {
const result = parseReqCommand([
"-k",
"1,3",
"-k",
"3,7",
"-k",
"1",
"-t",
"nostr",
"-t",
"bitcoin,nostr",
]);
expect(result.filter.kinds).toEqual([1, 3, 7]);
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin"]);
});
it("should handle empty comma-separated values", () => {
const result = parseReqCommand(["-k", "1,,3,,"]);
expect(result.filter.kinds).toEqual([1, 3]);
});
it("should handle whitespace in comma-separated values", () => {
const result = parseReqCommand(["-t", " nostr , bitcoin , lightning "]);
expect(result.filter["#t"]).toEqual(["nostr", "bitcoin", "lightning"]);
});
});
describe("edge cases", () => {
it("should handle empty args", () => {
const result = parseReqCommand([]);
expect(result.filter).toEqual({});
expect(result.relays).toBeUndefined();
expect(result.closeOnEose).toBe(false);
});
it("should handle flag without value", () => {
const result = parseReqCommand(["-k"]);
expect(result.filter.kinds).toBeUndefined();
});
it("should handle unknown flags gracefully", () => {
const result = parseReqCommand(["-x", "value", "-k", "1"]);
expect(result.filter.kinds).toEqual([1]);
});
});
});

View File

@@ -15,6 +15,30 @@ export interface ParsedReqCommand {
nip05PTags?: string[]; // NIP-05 identifiers for #p tags that need async resolution
}
/**
* Parse comma-separated values and apply a parser function to each
* Returns true if at least one value was successfully parsed
*/
function parseCommaSeparated<T>(
value: string,
parser: (v: string) => T | null,
target: Set<T>
): boolean {
const values = value.split(',').map(v => v.trim());
let addedAny = false;
for (const val of values) {
if (!val) continue;
const parsed = parser(val);
if (parsed !== null) {
target.add(parsed);
addedAny = true;
}
}
return addedAny;
}
/**
* Parse REQ command arguments into a Nostr filter
* Supports:
@@ -27,8 +51,17 @@ export interface ParsedReqCommand {
export function parseReqCommand(args: string[]): ParsedReqCommand {
const filter: NostrFilter = {};
const relays: string[] = [];
const nip05Authors: string[] = [];
const nip05PTags: string[] = [];
const nip05Authors = new Set<string>();
const nip05PTags = new Set<string>();
// Use sets for deduplication during accumulation
const kinds = new Set<number>();
const authors = new Set<string>();
const eventIds = new Set<string>();
const pTags = new Set<string>();
const tTags = new Set<string>();
const dTags = new Set<string>();
let closeOnEose = false;
let i = 0;
@@ -58,33 +91,47 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
switch (flag) {
case "-k":
case "--kind": {
const kind = parseInt(nextArg, 10);
if (!isNaN(kind)) {
if (!filter.kinds) filter.kinds = [];
filter.kinds.push(kind);
i += 2;
} else {
// Support comma-separated kinds: -k 1,3,7
if (!nextArg) {
i++;
break;
}
const addedAny = parseCommaSeparated(
nextArg,
(v) => {
const kind = parseInt(v, 10);
return isNaN(kind) ? null : kind;
},
kinds
);
i += addedAny ? 2 : 1;
break;
}
case "-a":
case "--author": {
// Check if it's a NIP-05 identifier
if (isNip05(nextArg)) {
nip05Authors.push(nextArg);
i += 2;
} else {
const pubkey = parseNpubOrHex(nextArg);
if (pubkey) {
if (!filter.authors) filter.authors = [];
filter.authors.push(pubkey);
i += 2;
// Support comma-separated authors: -a npub1...,npub2...,user@domain.com
if (!nextArg) {
i++;
break;
}
let addedAny = false;
const values = nextArg.split(',').map(a => a.trim());
for (const authorStr of values) {
if (!authorStr) continue;
// Check if it's a NIP-05 identifier
if (isNip05(authorStr)) {
nip05Authors.add(authorStr);
addedAny = true;
} else {
i++;
const pubkey = parseNpubOrHex(authorStr);
if (pubkey) {
authors.add(pubkey);
addedAny = true;
}
}
}
i += addedAny ? 2 : 1;
break;
}
@@ -101,41 +148,55 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
}
case "-e": {
const eventId = parseNoteOrHex(nextArg);
if (eventId) {
if (!filter["#e"]) filter["#e"] = [];
filter["#e"].push(eventId);
i += 2;
} else {
// Support comma-separated event IDs: -e id1,id2,id3
if (!nextArg) {
i++;
break;
}
const addedAny = parseCommaSeparated(
nextArg,
parseNoteOrHex,
eventIds
);
i += addedAny ? 2 : 1;
break;
}
case "-p": {
// Check if it's a NIP-05 identifier
if (isNip05(nextArg)) {
nip05PTags.push(nextArg);
i += 2;
} else {
const pubkey = parseNpubOrHex(nextArg);
if (pubkey) {
if (!filter["#p"]) filter["#p"] = [];
filter["#p"].push(pubkey);
i += 2;
// Support comma-separated pubkeys: -p npub1...,npub2...,user@domain.com
if (!nextArg) {
i++;
break;
}
let addedAny = false;
const values = nextArg.split(',').map(p => p.trim());
for (const pubkeyStr of values) {
if (!pubkeyStr) continue;
// Check if it's a NIP-05 identifier
if (isNip05(pubkeyStr)) {
nip05PTags.add(pubkeyStr);
addedAny = true;
} else {
i++;
const pubkey = parseNpubOrHex(pubkeyStr);
if (pubkey) {
pTags.add(pubkey);
addedAny = true;
}
}
}
i += addedAny ? 2 : 1;
break;
}
case "-t": {
// Hashtag filter
// Support comma-separated hashtags: -t nostr,bitcoin,lightning
if (nextArg) {
if (!filter["#t"]) filter["#t"] = [];
filter["#t"].push(nextArg);
i += 2;
const addedAny = parseCommaSeparated(
nextArg,
(v) => v, // hashtags are already strings
tTags
);
i += addedAny ? 2 : 1;
} else {
i++;
}
@@ -143,11 +204,14 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
}
case "-d": {
// D-tag filter (for replaceable events)
// Support comma-separated d-tags: -d article1,article2,article3
if (nextArg) {
if (!filter["#d"]) filter["#d"] = [];
filter["#d"].push(nextArg);
i += 2;
const addedAny = parseCommaSeparated(
nextArg,
(v) => v, // d-tags are already strings
dTags
);
i += addedAny ? 2 : 1;
} else {
i++;
}
@@ -201,16 +265,21 @@ export function parseReqCommand(args: string[]): ParsedReqCommand {
}
}
const result = {
// Convert accumulated sets to filter arrays (with deduplication)
if (kinds.size > 0) filter.kinds = Array.from(kinds);
if (authors.size > 0) filter.authors = Array.from(authors);
if (eventIds.size > 0) filter["#e"] = Array.from(eventIds);
if (pTags.size > 0) filter["#p"] = Array.from(pTags);
if (tTags.size > 0) filter["#t"] = Array.from(tTags);
if (dTags.size > 0) filter["#d"] = Array.from(dTags);
return {
filter,
relays: relays.length > 0 ? relays : undefined,
closeOnEose,
nip05Authors: nip05Authors.length > 0 ? nip05Authors : undefined,
nip05PTags: nip05PTags.length > 0 ? nip05PTags : undefined,
nip05Authors: nip05Authors.size > 0 ? Array.from(nip05Authors) : undefined,
nip05PTags: nip05PTags.size > 0 ? Array.from(nip05PTags) : undefined,
};
console.log("parseReqCommand result:", result);
return result;
}
/**

View File

@@ -144,12 +144,12 @@ export const manPages: Record<string, ManPageEntry> = {
{
flag: "-k, --kind <number>",
description:
"Filter by event kind (e.g., 0=metadata, 1=note, 7=reaction)",
"Filter by event kind (e.g., 0=metadata, 1=note, 7=reaction). Supports comma-separated values: -k 1,3,7",
},
{
flag: "-a, --author <npub|hex|nip05>",
description:
"Filter by author pubkey (supports npub, hex, NIP-05 identifier, or bare domain)",
"Filter by author pubkey (supports npub, hex, NIP-05 identifier, or bare domain). Supports comma-separated values: -a npub1...,user@domain.com",
},
{
flag: "-l, --limit <number>",
@@ -157,20 +157,20 @@ export const manPages: Record<string, ManPageEntry> = {
},
{
flag: "-e <id>",
description: "Filter by referenced event ID (#e tag)",
description: "Filter by referenced event ID (#e tag). Supports comma-separated values: -e id1,id2,id3",
},
{
flag: "-p <npub|hex|nip05>",
description:
"Filter by mentioned pubkey (#p tag, supports npub, hex, NIP-05, or bare domain)",
"Filter by mentioned pubkey (#p tag, supports npub, hex, NIP-05, or bare domain). Supports comma-separated values: -p npub1...,npub2...",
},
{
flag: "-t <hashtag>",
description: "Filter by hashtag (#t tag)",
description: "Filter by hashtag (#t tag). Supports comma-separated values: -t nostr,bitcoin,lightning",
},
{
flag: "-d <identifier>",
description: "Filter by d-tag identifier (replaceable events)",
description: "Filter by d-tag identifier (replaceable events). Supports comma-separated values: -d article1,article2",
},
{
flag: "--since <time>",
@@ -199,13 +199,15 @@ export const manPages: Record<string, ManPageEntry> = {
],
examples: [
"req -k 1 -l 20 Get 20 recent notes (streams live by default)",
"req -k 1,3,7 -l 50 Get notes, contact lists, and reactions",
"req -k 0 -a npub1... Get profile for author",
"req -k 1 -a user@domain.com Get notes from NIP-05 identifier",
"req -k 1 -a dergigi.com Get notes from bare domain (resolves to _@dergigi.com)",
"req -k 1 -a npub1...,npub2... Get notes from multiple authors",
"req -k 1 -p verbiricha@habla.news Get notes mentioning NIP-05 user",
"req -k 1 --since 1h relay.damus.io Get notes from last hour",
"req -k 1 --close-on-eose Get recent notes and close after EOSE",
"req -t nostr -l 50 Get 50 events tagged #nostr",
"req -t nostr,bitcoin -l 50 Get 50 events tagged #nostr or #bitcoin",
"req --search bitcoin -k 1 Search notes for 'bitcoin'",
"req -k 1 relay1.com relay2.com Query multiple relays",
],

14
vitest.config.ts Normal file
View File

@@ -0,0 +1,14 @@
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},
});