@usehyper/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +31 -0
- package/package.json +40 -0
- package/registry-sources/agent-rules/README.md +12 -0
- package/registry-sources/agent-rules/files/.cursor/rules/hyper.md +178 -0
- package/registry-sources/agent-rules/files/AGENTS.md +64 -0
- package/registry-sources/agent-rules/manifest.json +15 -0
- package/src/__tests__/add.test.ts +125 -0
- package/src/__tests__/cli.test.ts +50 -0
- package/src/__tests__/security.test.ts +101 -0
- package/src/args.ts +38 -0
- package/src/bin.ts +77 -0
- package/src/commands/add.ts +232 -0
- package/src/commands/bench.ts +185 -0
- package/src/commands/build.ts +146 -0
- package/src/commands/client.ts +78 -0
- package/src/commands/dev.ts +53 -0
- package/src/commands/diff.ts +80 -0
- package/src/commands/env.ts +92 -0
- package/src/commands/help.ts +42 -0
- package/src/commands/init.ts +119 -0
- package/src/commands/list.ts +46 -0
- package/src/commands/mcp.ts +51 -0
- package/src/commands/openapi.ts +50 -0
- package/src/commands/routes.ts +45 -0
- package/src/commands/security.ts +233 -0
- package/src/commands/test.ts +191 -0
- package/src/commands/typecheck.ts +19 -0
- package/src/commands/update.ts +91 -0
- package/src/commands/version.ts +16 -0
- package/src/config/index.ts +30 -0
- package/src/config/io.ts +112 -0
- package/src/config/tsconfig.ts +138 -0
- package/src/config/types.ts +63 -0
- package/src/entry.ts +42 -0
- package/src/index.ts +57 -0
- package/src/load-app.ts +89 -0
- package/src/registry/__tests__/env-writer.test.ts +83 -0
- package/src/registry/apply.ts +268 -0
- package/src/registry/client.ts +127 -0
- package/src/registry/env-writer.ts +135 -0
- package/src/registry/index.ts +18 -0
- package/src/registry/rewrite.ts +177 -0
- package/src/registry/snapshot.ts +1018 -0
- package/src/registry/types.ts +62 -0
- package/src/templates.ts +141 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Midday Labs AB
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @usehyper/cli
|
|
2
|
+
|
|
3
|
+
Hyper CLI — dev server, OpenAPI export, contract tests, security scan, benchmarks.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add -d @usehyper/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
bun x hyper dev # hot-reload dev server
|
|
15
|
+
bun x hyper routes # print the route table
|
|
16
|
+
bun x hyper test # run .example() contracts
|
|
17
|
+
bun x hyper test --fuzz --types # fuzz schemas + type-level assertions
|
|
18
|
+
bun x hyper security --check # static security audit
|
|
19
|
+
bun x hyper openapi --out openapi.json
|
|
20
|
+
bun x hyper bench --tests
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`hyper dev` and every introspection command set `HYPER_SKIP_LISTEN=1` before importing your app, so the same `app.ts` works as both server entrypoint and CLI input.
|
|
24
|
+
|
|
25
|
+
## Docs
|
|
26
|
+
|
|
27
|
+
See the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.
|
|
28
|
+
|
|
29
|
+
## License
|
|
30
|
+
|
|
31
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usehyper/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Hyper CLI — registry-driven scaffolding (init/add/diff/update) plus dev/build/test/openapi/mcp.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/usehyper/hyper.git"
|
|
10
|
+
},
|
|
11
|
+
"files": ["src", "registry-sources", "LICENSE", "README.md"],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./src/index.ts",
|
|
15
|
+
"import": "./src/index.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"typecheck": "tsgo --noEmit || bunx tsc --noEmit -p tsconfig.json",
|
|
20
|
+
"test": "bun test",
|
|
21
|
+
"prepack": "bun run ../../tools/build-snapshots.ts"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@hyper/client": "0.1.0",
|
|
25
|
+
"@hyper/core": "0.1.0",
|
|
26
|
+
"@hyper/mcp": "0.1.0",
|
|
27
|
+
"@hyper/openapi": "0.1.0",
|
|
28
|
+
"@hyper/testing": "0.1.0",
|
|
29
|
+
"@types/bun": "1.3.1"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"bun": ">=1.3.0"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"bin": {
|
|
38
|
+
"hyper": "./src/bin.ts"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# agent-rules
|
|
2
|
+
|
|
3
|
+
Drops `.cursor/rules/hyper.md` and `AGENTS.md` into your repo so Cursor, Claude
|
|
4
|
+
Code, and other AI assistants understand how Hyper apps are structured: the
|
|
5
|
+
chain API on `Hyper`, the explicit `route.<verb>().handle()` builder, the
|
|
6
|
+
`decorate()` typing flow, plugin authoring, and the secure-by-default posture.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
hyper add agent-rules
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
After install, AI agents get the rules automatically — no extra setup.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: How to write Hyper routes, plugins, and middleware in this repo
|
|
3
|
+
globs:
|
|
4
|
+
- src/hyper/**/*.ts
|
|
5
|
+
- src/**/*.ts
|
|
6
|
+
alwaysApply: true
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Hyper rules
|
|
10
|
+
|
|
11
|
+
This project uses [Hyper](https://hyperjs.ai), a Bun-first API framework whose
|
|
12
|
+
source code lives in this repository at `src/hyper/<component>/`. Imports use
|
|
13
|
+
the `@hyper/*` alias (mapped via `tsconfig.json` `paths`).
|
|
14
|
+
|
|
15
|
+
## Authoring routes
|
|
16
|
+
|
|
17
|
+
Two equivalent styles. Prefer the chain API for app composition; prefer the
|
|
18
|
+
builder for routes you want to attach middleware/decorators/types to.
|
|
19
|
+
|
|
20
|
+
### Chain API (preferred for top-level apps)
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { Hyper, ok } from "@hyper/core"
|
|
24
|
+
import { z } from "zod"
|
|
25
|
+
|
|
26
|
+
export default new Hyper()
|
|
27
|
+
.get("/health", "OK")
|
|
28
|
+
.post(
|
|
29
|
+
"/users",
|
|
30
|
+
{ body: z.object({ name: z.string(), email: z.email() }) },
|
|
31
|
+
({ body }) => ok({ id: crypto.randomUUID(), ...body }),
|
|
32
|
+
)
|
|
33
|
+
.listen(3000)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Route builder (preferred when you need typed responses + middleware)
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { ok, route, notFound } from "@hyper/core"
|
|
40
|
+
import { z } from "zod"
|
|
41
|
+
|
|
42
|
+
const UserParams = z.object({ id: z.string() })
|
|
43
|
+
|
|
44
|
+
export const getUser = route
|
|
45
|
+
.get("/users/:id")
|
|
46
|
+
.params(UserParams)
|
|
47
|
+
.handle(async ({ params, ctx }) => {
|
|
48
|
+
const u = await ctx.store.get(params.id)
|
|
49
|
+
if (!u) return notFound({ code: "user_not_found" })
|
|
50
|
+
return ok(u)
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Composing apps with `.use()`
|
|
55
|
+
|
|
56
|
+
`.use()` is polymorphic. It accepts:
|
|
57
|
+
|
|
58
|
+
- A sub-`Hyper` instance — its prefix is honored
|
|
59
|
+
- A `HyperApp` from `app({...})`
|
|
60
|
+
- A raw `Route` value or array of routes
|
|
61
|
+
- A plugin returned by a plugin factory (`hyperLog(...)`, `cors(...)`, etc.)
|
|
62
|
+
- A plain middleware (object with `start`/`success`/`error`/`finish`)
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { Hyper } from "@hyper/core"
|
|
66
|
+
import { hyperLog } from "@hyper/log"
|
|
67
|
+
import { cors } from "@hyper/cors"
|
|
68
|
+
import users from "./routes/users.ts"
|
|
69
|
+
import posts from "./routes/posts.ts"
|
|
70
|
+
|
|
71
|
+
export default new Hyper()
|
|
72
|
+
.use(hyperLog({ service: "api" }))
|
|
73
|
+
.use(cors({ origin: ["https://example.com"] }))
|
|
74
|
+
.use(users) // honors `users`'s own prefix
|
|
75
|
+
.use("/v1", posts) // re-prefix
|
|
76
|
+
.listen(3000)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Decorating context
|
|
80
|
+
|
|
81
|
+
Use `.decorate()` (or `decorate: [...]` in `app({...})`) to attach typed
|
|
82
|
+
services to `ctx`. Always extend `AppContext` via module augmentation so
|
|
83
|
+
handlers see the right types.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { Hyper } from "@hyper/core"
|
|
87
|
+
import { db } from "./db.ts"
|
|
88
|
+
|
|
89
|
+
declare module "@hyper/core" {
|
|
90
|
+
interface AppContext {
|
|
91
|
+
readonly db: typeof db
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default new Hyper().decorate(() => ({ db }))
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Response helpers
|
|
99
|
+
|
|
100
|
+
Always return through helpers — they project to OpenAPI/MCP/client-types
|
|
101
|
+
correctly. Never `new Response()` directly.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import {
|
|
105
|
+
ok, created, accepted, noContent,
|
|
106
|
+
badRequest, unauthorized, forbidden, notFound, conflict, unprocessable, tooManyRequests,
|
|
107
|
+
redirect, html, text, sse, stream, file,
|
|
108
|
+
} from "@hyper/core"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Errors
|
|
112
|
+
|
|
113
|
+
Throw `HyperError` for typed errors — they project to the route's `errors`
|
|
114
|
+
union and serialize consistently.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { createError } from "@hyper/core"
|
|
118
|
+
|
|
119
|
+
throw createError({ status: 409, code: "duplicate_email", message: "Email already in use" })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Validation
|
|
123
|
+
|
|
124
|
+
Body / params / query schemas are Standard Schema-compatible: Zod, Valibot,
|
|
125
|
+
ArkType all work. Schemas declared on a route project to OpenAPI input
|
|
126
|
+
schemas automatically.
|
|
127
|
+
|
|
128
|
+
## Secure-by-default — do NOT disable lightly
|
|
129
|
+
|
|
130
|
+
Hyper sets these for every response unless explicitly turned off:
|
|
131
|
+
|
|
132
|
+
- `x-content-type-options: nosniff`
|
|
133
|
+
- `x-frame-options: DENY`
|
|
134
|
+
- `referrer-policy: strict-origin-when-cross-origin`
|
|
135
|
+
- `strict-transport-security` (production only)
|
|
136
|
+
- 1MB body cap
|
|
137
|
+
- prototype-pollution guards on JSON bodies
|
|
138
|
+
- per-route timeouts
|
|
139
|
+
|
|
140
|
+
Auth endpoints default to rate-limiting via `@hyper/rate-limit`. JWT secrets
|
|
141
|
+
must be ≥32 bytes (`@hyper/auth-jwt` will refuse to start otherwise).
|
|
142
|
+
|
|
143
|
+
## Testing
|
|
144
|
+
|
|
145
|
+
Use `@hyper/testing` — `app.test()`, `call()`, memory stores, deterministic
|
|
146
|
+
clocks. Tests should run against `app.fetch(new Request(...))` directly,
|
|
147
|
+
no network.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { describe, expect, test } from "bun:test"
|
|
151
|
+
import app from "../src/app.ts"
|
|
152
|
+
|
|
153
|
+
describe("users", () => {
|
|
154
|
+
test("GET /users/:id", async () => {
|
|
155
|
+
const res = await app.fetch(new Request("http://localhost/users/u1"))
|
|
156
|
+
expect(res.status).toBe(200)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## File layout convention
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
src/
|
|
165
|
+
hyper/ # Hyper framework source (managed by `hyper` CLI)
|
|
166
|
+
core/ # @hyper/core
|
|
167
|
+
log/ # @hyper/log
|
|
168
|
+
cors/ # @hyper/cors
|
|
169
|
+
...
|
|
170
|
+
app.ts # entrypoint; default-exports a Hyper instance or HyperApp
|
|
171
|
+
routes/ # sub-app modules (preferred over inline routes)
|
|
172
|
+
schemas/ # Zod / Valibot schemas
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`src/hyper/` is owned by the registry — do not edit by hand unless you intend
|
|
176
|
+
to fork that component (and accept that `hyper update` will conflict). Run
|
|
177
|
+
`hyper diff` to inspect drift between your local copy and the upstream
|
|
178
|
+
registry.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents working in this repository.
|
|
4
|
+
|
|
5
|
+
This project uses [Hyper](https://hyperjs.ai), a Bun-first API framework. The
|
|
6
|
+
framework source is **vendored into this repo** under `src/hyper/<component>/`
|
|
7
|
+
and managed by the `hyper` CLI — components are installed from a registry and
|
|
8
|
+
copied directly into the project, not pulled in as npm dependencies. You own
|
|
9
|
+
the code; you can edit it freely; `hyper update` will pull upstream changes
|
|
10
|
+
when you ask for them.
|
|
11
|
+
|
|
12
|
+
## Quick orientation
|
|
13
|
+
|
|
14
|
+
- `src/app.ts` — entrypoint. Default-exports a `Hyper` instance or `HyperApp`.
|
|
15
|
+
- `src/hyper/core/` — framework runtime. Imports as `@hyper/core`.
|
|
16
|
+
- `src/hyper/<plugin>/` — installable plugins (log, cors, auth-jwt, …). Each is
|
|
17
|
+
imported as `@hyper/<plugin>`.
|
|
18
|
+
- `package.json` has **no `@usehyper/*` deps** — everything ships via the
|
|
19
|
+
registry.
|
|
20
|
+
- `hyper.config.json` — the registry config (URL, baseDir, alias).
|
|
21
|
+
- `hyper.lock.json` — pins each installed component's version + per-file hash.
|
|
22
|
+
|
|
23
|
+
## When you write code
|
|
24
|
+
|
|
25
|
+
1. Import from `@hyper/<component>` — never `@usehyper/<component>`.
|
|
26
|
+
2. Always return through Hyper's response helpers (`ok`, `created`, `notFound`, …).
|
|
27
|
+
3. Always declare schemas (`body`, `params`, `query`) — they project to
|
|
28
|
+
OpenAPI/MCP/client types automatically.
|
|
29
|
+
4. Use `.decorate()` for typed services on `ctx`. Augment `AppContext` via
|
|
30
|
+
`declare module "@hyper/core"`.
|
|
31
|
+
5. For protected routes, chain `.auth()` from `@hyper/auth-jwt`.
|
|
32
|
+
6. Never weaken the secure-by-default headers without an explicit reason.
|
|
33
|
+
|
|
34
|
+
## When you install components
|
|
35
|
+
|
|
36
|
+
Use the CLI, not edits:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
hyper add cors
|
|
40
|
+
hyper add auth-jwt
|
|
41
|
+
hyper add openapi openapi-zod
|
|
42
|
+
hyper diff log # inspect drift
|
|
43
|
+
hyper update log # bump to latest registry version
|
|
44
|
+
hyper add --info session # show readme/files/deps without installing
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The CLI rewrites `@hyper/*` imports to whatever alias is configured in
|
|
48
|
+
`hyper.config.json`. Do not hand-edit the `paths` mapping.
|
|
49
|
+
|
|
50
|
+
## When you debug or extend
|
|
51
|
+
|
|
52
|
+
- `hyper routes` — print the route graph
|
|
53
|
+
- `hyper openapi` — emit OpenAPI 3.1
|
|
54
|
+
- `hyper client out.ts` — emit a typed RPC client
|
|
55
|
+
- `hyper mcp` — serve the app's routes over MCP for AI introspection
|
|
56
|
+
- `hyper bench --tests` — measure per-route latency
|
|
57
|
+
|
|
58
|
+
## Style
|
|
59
|
+
|
|
60
|
+
- Imports use `.ts` extensions (`from "./schemas.ts"`) — `verbatimModuleSyntax`
|
|
61
|
+
is on.
|
|
62
|
+
- Prefer the chain API (`new Hyper().get(...)`) for top-level apps; prefer
|
|
63
|
+
the builder (`route.get(...).body(...).handle(...)`) for sub-app modules.
|
|
64
|
+
- Tests run via `bun test` against `app.fetch(new Request(...))` — no network.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agent-rules",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"title": "Agent rules",
|
|
5
|
+
"description": "Cursor / Claude Code rules teaching AI assistants how to compose Hyper routes, plugins, and middleware correctly.",
|
|
6
|
+
"registryDeps": [],
|
|
7
|
+
"peerDeps": {},
|
|
8
|
+
"optionalPeerDeps": {},
|
|
9
|
+
"subpaths": {},
|
|
10
|
+
"files": [
|
|
11
|
+
{ "path": "AGENTS.md", "target": "@root/AGENTS.md" },
|
|
12
|
+
{ "path": ".cursor/rules/hyper.md", "target": "@root/.cursor/rules/hyper.md" }
|
|
13
|
+
],
|
|
14
|
+
"docs": "Files installed:\n - AGENTS.md (project root)\n - .cursor/rules/hyper.md\n\nKeep these committed so AI assistants pick up Hyper's conventions automatically. Re-run `hyper add agent-rules` after upgrading Hyper to refresh."
|
|
15
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtemp, readFile, writeFile } from "node:fs/promises"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
import { runAdd } from "../commands/add.ts"
|
|
6
|
+
import { runDiff } from "../commands/diff.ts"
|
|
7
|
+
import { writeConfig } from "../config/index.ts"
|
|
8
|
+
import { SNAPSHOT_INDEX, createRegistryClient } from "../registry/index.ts"
|
|
9
|
+
|
|
10
|
+
async function withCwd<T>(dir: string, fn: () => Promise<T>): Promise<T> {
|
|
11
|
+
const prev = process.cwd()
|
|
12
|
+
process.chdir(dir)
|
|
13
|
+
try {
|
|
14
|
+
return await fn()
|
|
15
|
+
} finally {
|
|
16
|
+
process.chdir(prev)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function setupProject(): Promise<string> {
|
|
21
|
+
const dir = await mkdtemp(join(tmpdir(), "hyper-add-"))
|
|
22
|
+
await writeConfig(
|
|
23
|
+
{
|
|
24
|
+
registryUrl: "https://example.invalid",
|
|
25
|
+
baseDir: "src/hyper",
|
|
26
|
+
alias: "@hyper",
|
|
27
|
+
},
|
|
28
|
+
dir,
|
|
29
|
+
)
|
|
30
|
+
return dir
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("registry client (snapshot fallback)", () => {
|
|
34
|
+
test("snapshot index includes core + a representative set of components", () => {
|
|
35
|
+
const names = SNAPSHOT_INDEX.components.map((c) => c.name)
|
|
36
|
+
expect(names).toContain("core")
|
|
37
|
+
expect(names).toContain("cors")
|
|
38
|
+
expect(names).toContain("auth-jwt")
|
|
39
|
+
expect(names).toContain("agent-rules")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("offline client serves the snapshot index", async () => {
|
|
43
|
+
const c = createRegistryClient({ url: "https://example.invalid", offline: true })
|
|
44
|
+
const idx = await c.getIndex()
|
|
45
|
+
expect(idx.components.length).toBeGreaterThan(0)
|
|
46
|
+
const cors = await c.getComponent("cors")
|
|
47
|
+
expect(cors.name).toBe("cors")
|
|
48
|
+
expect(cors.registryDeps).toContain("core")
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe("hyper add", () => {
|
|
53
|
+
test("copies cors + transitive core deps into the project", async () => {
|
|
54
|
+
const dir = await setupProject()
|
|
55
|
+
await withCwd(dir, async () => {
|
|
56
|
+
// Force snapshot mode by pointing at an unreachable URL.
|
|
57
|
+
const code = await runAdd({
|
|
58
|
+
command: "add",
|
|
59
|
+
positional: ["cors"],
|
|
60
|
+
flags: {},
|
|
61
|
+
})
|
|
62
|
+
expect(code).toBe(0)
|
|
63
|
+
const corsBody = await readFile(join(dir, "src/hyper/cors/index.ts"), "utf8")
|
|
64
|
+
expect(corsBody).toContain("@hyper/core")
|
|
65
|
+
const coreIndex = await readFile(join(dir, "src/hyper/core/index.ts"), "utf8")
|
|
66
|
+
expect(coreIndex).toContain("export")
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test("refuses to overwrite drifted files without --force", async () => {
|
|
71
|
+
const dir = await setupProject()
|
|
72
|
+
await withCwd(dir, async () => {
|
|
73
|
+
await runAdd({ command: "add", positional: ["cors"], flags: {} })
|
|
74
|
+
await writeFile(join(dir, "src/hyper/cors/index.ts"), "// drifted\n")
|
|
75
|
+
const code = await runAdd({ command: "add", positional: ["cors"], flags: {} })
|
|
76
|
+
expect(code).toBe(1)
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("--force overrides drift protection", async () => {
|
|
81
|
+
const dir = await setupProject()
|
|
82
|
+
await withCwd(dir, async () => {
|
|
83
|
+
await runAdd({ command: "add", positional: ["cors"], flags: {} })
|
|
84
|
+
await writeFile(join(dir, "src/hyper/cors/index.ts"), "// drifted\n")
|
|
85
|
+
const code = await runAdd({
|
|
86
|
+
command: "add",
|
|
87
|
+
positional: ["cors"],
|
|
88
|
+
flags: { force: true },
|
|
89
|
+
})
|
|
90
|
+
expect(code).toBe(0)
|
|
91
|
+
const after = await readFile(join(dir, "src/hyper/cors/index.ts"), "utf8")
|
|
92
|
+
expect(after).not.toBe("// drifted\n")
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test("agent-rules drops .cursor/rules + AGENTS.md at project root, NOT under baseDir", async () => {
|
|
97
|
+
const dir = await setupProject()
|
|
98
|
+
await withCwd(dir, async () => {
|
|
99
|
+
const code = await runAdd({
|
|
100
|
+
command: "add",
|
|
101
|
+
positional: ["agent-rules"],
|
|
102
|
+
flags: {},
|
|
103
|
+
})
|
|
104
|
+
expect(code).toBe(0)
|
|
105
|
+
const rules = await readFile(join(dir, ".cursor/rules/hyper.md"), "utf8")
|
|
106
|
+
expect(rules).toContain("Hyper rules")
|
|
107
|
+
const agents = await readFile(join(dir, "AGENTS.md"), "utf8")
|
|
108
|
+
expect(agents).toContain("AGENTS.md")
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe("hyper diff", () => {
|
|
114
|
+
test("detects drift against the registry", async () => {
|
|
115
|
+
const dir = await setupProject()
|
|
116
|
+
await withCwd(dir, async () => {
|
|
117
|
+
await runAdd({ command: "add", positional: ["cors"], flags: {} })
|
|
118
|
+
const clean = await runDiff({ command: "diff", positional: ["cors"], flags: {} })
|
|
119
|
+
expect(clean).toBe(0)
|
|
120
|
+
await writeFile(join(dir, "src/hyper/cors/index.ts"), "// drifted\n")
|
|
121
|
+
const drift = await runDiff({ command: "diff", positional: ["cors"], flags: {} })
|
|
122
|
+
expect(drift).toBe(1)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { parseArgs } from "../args.ts"
|
|
3
|
+
import { TEMPLATES } from "../templates.ts"
|
|
4
|
+
|
|
5
|
+
describe("cli args parser", () => {
|
|
6
|
+
test("parses command + positional + flags", () => {
|
|
7
|
+
const a = parseArgs(["build", "src/app.ts", "--out", "dist", "--minify"])
|
|
8
|
+
expect(a.command).toBe("build")
|
|
9
|
+
expect(a.positional).toEqual(["src/app.ts"])
|
|
10
|
+
expect(a.flags.out).toBe("dist")
|
|
11
|
+
expect(a.flags.minify).toBe(true)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test("respects --json", () => {
|
|
15
|
+
const a = parseArgs(["routes", "--json"])
|
|
16
|
+
expect(a.flags.json).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test("no command returns undefined", () => {
|
|
20
|
+
const a = parseArgs([])
|
|
21
|
+
expect(a.command).toBeUndefined()
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe("cli templates", () => {
|
|
26
|
+
test("templates ship with @hyper/* (not @usehyper/*) imports", () => {
|
|
27
|
+
expect(TEMPLATES.minimal).toBeDefined()
|
|
28
|
+
expect(TEMPLATES.minimal!.files["src/app.ts"]).toContain("@hyper/core")
|
|
29
|
+
expect(TEMPLATES.minimal!.files["src/app.ts"]).not.toContain("@usehyper/core")
|
|
30
|
+
expect(TEMPLATES.api).toBeDefined()
|
|
31
|
+
expect(TEMPLATES.api!.files["src/app.ts"]).toContain("@hyper/log")
|
|
32
|
+
expect(TEMPLATES.api!.files["src/app.ts"]).not.toContain("@usehyper/log")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test("templates patch tsconfig with @hyper/* path mapping", () => {
|
|
36
|
+
expect(TEMPLATES.minimal!.files["tsconfig.json"]).toContain('"@hyper/*"')
|
|
37
|
+
expect(TEMPLATES.minimal!.files["tsconfig.json"]).toContain("./src/hyper/*")
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("templates have no @usehyper/* runtime deps", () => {
|
|
41
|
+
expect(TEMPLATES.minimal!.files["package.json"]).not.toContain("@usehyper/")
|
|
42
|
+
expect(TEMPLATES.api!.files["package.json"]).not.toContain("@usehyper/")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("templates declare which components to install after init", () => {
|
|
46
|
+
expect(TEMPLATES.minimal!.components).toContain("core")
|
|
47
|
+
expect(TEMPLATES.api!.components).toContain("core")
|
|
48
|
+
expect(TEMPLATES.api!.components).toContain("log")
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { app, route } from "@hyper/core"
|
|
3
|
+
import { auditApp } from "../commands/security.ts"
|
|
4
|
+
|
|
5
|
+
describe("hyper security --check — auditApp", () => {
|
|
6
|
+
test("clean app passes", async () => {
|
|
7
|
+
const a = app({ routes: [route.get("/").handle(() => "ok")] })
|
|
8
|
+
const findings = await auditApp(a)
|
|
9
|
+
const failed = findings.filter((f) => f.level === "fail")
|
|
10
|
+
expect(failed).toEqual([])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test("fails when default headers are disabled", async () => {
|
|
14
|
+
const a = app({
|
|
15
|
+
routes: [route.get("/").handle(() => "ok")],
|
|
16
|
+
security: { headers: false },
|
|
17
|
+
})
|
|
18
|
+
const findings = await auditApp(a)
|
|
19
|
+
const f = findings.find((x) => x.id === "sec-headers")!
|
|
20
|
+
expect(f.level).toBe("fail")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test("fails when method-override guard is off", async () => {
|
|
24
|
+
const a = app({
|
|
25
|
+
routes: [route.get("/").handle(() => "ok")],
|
|
26
|
+
security: { rejectMethodOverride: false },
|
|
27
|
+
})
|
|
28
|
+
const f = (await auditApp(a)).find((x) => x.id === "sec-method-override")!
|
|
29
|
+
expect(f.level).toBe("fail")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test("flags authEndpoint routes with no authRateLimitPlugin", async () => {
|
|
33
|
+
const a = app({
|
|
34
|
+
routes: [
|
|
35
|
+
route
|
|
36
|
+
.post("/login")
|
|
37
|
+
.meta({ authEndpoint: true })
|
|
38
|
+
.handle(() => ({ ok: true })),
|
|
39
|
+
],
|
|
40
|
+
})
|
|
41
|
+
const f = (await auditApp(a)).find((x) => x.id === "sec-auth-rate")!
|
|
42
|
+
expect(f.level).toBe("fail")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("warns on excessive body limit", async () => {
|
|
46
|
+
const a = app({
|
|
47
|
+
routes: [route.get("/").handle(() => "ok")],
|
|
48
|
+
security: { bodyLimitBytes: 100 * 1_048_576 },
|
|
49
|
+
})
|
|
50
|
+
const f = (await auditApp(a)).find((x) => x.id === "sec-body-limit")!
|
|
51
|
+
expect(f.level).toBe("warn")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test("warns when session() middleware is present on a mutating route without csrfGuard()", async () => {
|
|
55
|
+
const fakeSession: import("@hyper/core").Middleware = Object.assign(
|
|
56
|
+
async ({ next }: { next: () => Promise<unknown> }) => next() as Promise<Response>,
|
|
57
|
+
{ __hyperTag: "@hyper/session" },
|
|
58
|
+
) as unknown as import("@hyper/core").Middleware
|
|
59
|
+
const a = app({
|
|
60
|
+
routes: [
|
|
61
|
+
route
|
|
62
|
+
.post("/profile")
|
|
63
|
+
.use(fakeSession)
|
|
64
|
+
.handle(() => ({ ok: true })),
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
const f = (await auditApp(a)).find((x) => x.id === "sec-csrf")!
|
|
68
|
+
expect(f.level).toBe("warn")
|
|
69
|
+
expect(f.fix).toContain("POST /profile")
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test("passes sec-csrf when session + csrfGuard are chained", async () => {
|
|
73
|
+
const fakeSession: import("@hyper/core").Middleware = Object.assign(
|
|
74
|
+
async ({ next }: { next: () => Promise<unknown> }) => next() as Promise<Response>,
|
|
75
|
+
{ __hyperTag: "@hyper/session" },
|
|
76
|
+
) as unknown as import("@hyper/core").Middleware
|
|
77
|
+
const fakeCsrf: import("@hyper/core").Middleware = Object.assign(
|
|
78
|
+
async ({ next }: { next: () => Promise<unknown> }) => next() as Promise<Response>,
|
|
79
|
+
{ __hyperTag: "@hyper/session:csrf" },
|
|
80
|
+
) as unknown as import("@hyper/core").Middleware
|
|
81
|
+
const a = app({
|
|
82
|
+
routes: [
|
|
83
|
+
route
|
|
84
|
+
.post("/profile")
|
|
85
|
+
.use(fakeSession)
|
|
86
|
+
.use(fakeCsrf)
|
|
87
|
+
.handle(() => ({ ok: true })),
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
const f = (await auditApp(a)).find((x) => x.id === "sec-csrf")!
|
|
91
|
+
expect(f.level).toBe("pass")
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test("does not emit sec-csrf when session middleware is absent", async () => {
|
|
95
|
+
const a = app({
|
|
96
|
+
routes: [route.post("/anything").handle(() => ({ ok: true }))],
|
|
97
|
+
})
|
|
98
|
+
const f = (await auditApp(a)).find((x) => x.id === "sec-csrf")
|
|
99
|
+
expect(f).toBeUndefined()
|
|
100
|
+
})
|
|
101
|
+
})
|
package/src/args.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal arg parser — no deps. Supports:
|
|
3
|
+
* hyper <command> [positional] [--flag value] [--bool] [-s]
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ParsedArgs {
|
|
7
|
+
readonly command: string | undefined
|
|
8
|
+
readonly positional: readonly string[]
|
|
9
|
+
readonly flags: Readonly<Record<string, string | boolean>>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
13
|
+
const [command, ...rest] = argv
|
|
14
|
+
const positional: string[] = []
|
|
15
|
+
const flags: Record<string, string | boolean> = {}
|
|
16
|
+
for (let i = 0; i < rest.length; i++) {
|
|
17
|
+
const a = rest[i]!
|
|
18
|
+
if (a.startsWith("--")) {
|
|
19
|
+
const key = a.slice(2)
|
|
20
|
+
const next = rest[i + 1]
|
|
21
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
22
|
+
flags[key] = next
|
|
23
|
+
i++
|
|
24
|
+
} else {
|
|
25
|
+
flags[key] = true
|
|
26
|
+
}
|
|
27
|
+
} else if (a.startsWith("-")) {
|
|
28
|
+
flags[a.slice(1)] = true
|
|
29
|
+
} else {
|
|
30
|
+
positional.push(a)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { command, positional, flags }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isJson(flags: Readonly<Record<string, string | boolean>>): boolean {
|
|
37
|
+
return flags.json === true || flags.json === "true"
|
|
38
|
+
}
|