@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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +31 -0
  3. package/package.json +40 -0
  4. package/registry-sources/agent-rules/README.md +12 -0
  5. package/registry-sources/agent-rules/files/.cursor/rules/hyper.md +178 -0
  6. package/registry-sources/agent-rules/files/AGENTS.md +64 -0
  7. package/registry-sources/agent-rules/manifest.json +15 -0
  8. package/src/__tests__/add.test.ts +125 -0
  9. package/src/__tests__/cli.test.ts +50 -0
  10. package/src/__tests__/security.test.ts +101 -0
  11. package/src/args.ts +38 -0
  12. package/src/bin.ts +77 -0
  13. package/src/commands/add.ts +232 -0
  14. package/src/commands/bench.ts +185 -0
  15. package/src/commands/build.ts +146 -0
  16. package/src/commands/client.ts +78 -0
  17. package/src/commands/dev.ts +53 -0
  18. package/src/commands/diff.ts +80 -0
  19. package/src/commands/env.ts +92 -0
  20. package/src/commands/help.ts +42 -0
  21. package/src/commands/init.ts +119 -0
  22. package/src/commands/list.ts +46 -0
  23. package/src/commands/mcp.ts +51 -0
  24. package/src/commands/openapi.ts +50 -0
  25. package/src/commands/routes.ts +45 -0
  26. package/src/commands/security.ts +233 -0
  27. package/src/commands/test.ts +191 -0
  28. package/src/commands/typecheck.ts +19 -0
  29. package/src/commands/update.ts +91 -0
  30. package/src/commands/version.ts +16 -0
  31. package/src/config/index.ts +30 -0
  32. package/src/config/io.ts +112 -0
  33. package/src/config/tsconfig.ts +138 -0
  34. package/src/config/types.ts +63 -0
  35. package/src/entry.ts +42 -0
  36. package/src/index.ts +57 -0
  37. package/src/load-app.ts +89 -0
  38. package/src/registry/__tests__/env-writer.test.ts +83 -0
  39. package/src/registry/apply.ts +268 -0
  40. package/src/registry/client.ts +127 -0
  41. package/src/registry/env-writer.ts +135 -0
  42. package/src/registry/index.ts +18 -0
  43. package/src/registry/rewrite.ts +177 -0
  44. package/src/registry/snapshot.ts +1018 -0
  45. package/src/registry/types.ts +62 -0
  46. 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
+ }