@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
@@ -0,0 +1,78 @@
1
+ /**
2
+ * `hyper client <outDir> [entry]` — emits client.ts + client.d.ts from the
3
+ * app's `toClientManifest()`. Codegen lives in @hyper/client; the CLI is
4
+ * responsible for loading the app and writing files.
5
+ */
6
+
7
+ import { mkdir, writeFile } from "node:fs/promises"
8
+ import { resolve } from "node:path"
9
+ import { type ParsedArgs, isJson } from "../args.ts"
10
+ import { readConfig } from "../config/index.ts"
11
+ import { resolveEntry } from "../entry.ts"
12
+ import { loadApp } from "../load-app.ts"
13
+
14
+ export async function runClient(args: ParsedArgs): Promise<number> {
15
+ const outDir = args.positional[0]
16
+ if (!outDir) {
17
+ console.error("usage: hyper client <outDir> [entry]")
18
+ return 2
19
+ }
20
+ const entryArg = args.positional[1] ? [args.positional[1]] : []
21
+ const entry = await resolveEntry(entryArg)
22
+ if (!entry) {
23
+ console.error("error: no entry file found")
24
+ return 2
25
+ }
26
+ const app = await loadApp(entry)
27
+ if (!app) {
28
+ console.error(`error: no default/named 'app' export in ${entry}`)
29
+ return 2
30
+ }
31
+
32
+ // The codegen lives at <baseDir>/client/codegen.ts after `hyper add client`.
33
+ // We resolve it locally first, with @hyper/client and @hyper/client as fallbacks.
34
+ const config = await readConfig()
35
+ const local = resolve(process.cwd(), config.baseDir, "client/codegen.ts")
36
+ let mod: typeof import("@hyper/client/codegen") | null = null
37
+ for (const spec of [local, "@hyper/client/codegen", "@hyper/client/codegen"]) {
38
+ try {
39
+ mod = (await import(spec)) as typeof import("@hyper/client/codegen")
40
+ break
41
+ } catch {
42
+ // try next
43
+ }
44
+ }
45
+ if (!mod) {
46
+ console.error(
47
+ "error: @hyper/client not installed in this project. Run `hyper add client` first.",
48
+ )
49
+ return 2
50
+ }
51
+ const baseUrl = typeof args.flags.baseUrl === "string" ? args.flags.baseUrl : ""
52
+ const result = mod.generateClient({
53
+ manifest: app.toClientManifest(),
54
+ baseUrl,
55
+ rootName: typeof args.flags.name === "string" ? args.flags.name : "api",
56
+ resultTypes: args.flags.resultTypes === true || args.flags["result-types"] === true,
57
+ })
58
+
59
+ const abs = resolve(process.cwd(), outDir)
60
+ await mkdir(abs, { recursive: true })
61
+ const runtimePath = resolve(abs, "client.ts")
62
+ const dtsPath = resolve(abs, "client.d.ts")
63
+ await writeFile(runtimePath, result.runtime)
64
+ await writeFile(dtsPath, result.declaration)
65
+
66
+ const summary = {
67
+ runtime: runtimePath,
68
+ declaration: dtsPath,
69
+ routeCount: app.routeList.length,
70
+ }
71
+ if (isJson(args.flags)) {
72
+ console.log(JSON.stringify(summary))
73
+ } else {
74
+ console.log(`client emitted -> ${abs}`)
75
+ console.log(` ${summary.routeCount} route(s)`)
76
+ }
77
+ return 0
78
+ }
@@ -0,0 +1,53 @@
1
+ import { type ChildProcess, spawn } from "node:child_process"
2
+ import type { ParsedArgs } from "../args.ts"
3
+ import { resolveEntry } from "../entry.ts"
4
+
5
+ /**
6
+ * `hyper dev` — run the app with Bun hot reload + tsgo --watch alongside.
7
+ *
8
+ * We prefer a Bun-native single-process hot reload (`bun --hot` uses
9
+ * `server.reload` under the hood to keep the open sockets alive). The
10
+ * type-checker runs as a sibling process so incremental type errors
11
+ * surface in the terminal without blocking request-handling.
12
+ *
13
+ * Flags:
14
+ * --test also run `bun test --watch` in a third sibling process
15
+ * --no-types skip `tsgo --watch` (useful when tsgo isn't available)
16
+ */
17
+ export async function runDev(args: ParsedArgs): Promise<number> {
18
+ const entry = await resolveEntry(args.positional)
19
+ if (!entry) {
20
+ console.error("error: no entry file found (tried src/app.ts, app.ts, src/index.ts)")
21
+ return 2
22
+ }
23
+ const children: ChildProcess[] = []
24
+ const bun = spawn("bun", ["--hot", entry], { stdio: "inherit" })
25
+ children.push(bun)
26
+
27
+ const runTypes = args.flags.types !== false && args.flags["no-types"] !== true
28
+ if (runTypes) {
29
+ const tsgo = spawn("tsgo", ["--noEmit", "--watch", "-p", "tsconfig.json"], {
30
+ stdio: "inherit",
31
+ })
32
+ tsgo.on("error", () => {})
33
+ children.push(tsgo)
34
+ }
35
+
36
+ if (args.flags.test === true) {
37
+ const test = spawn("bun", ["test", "--watch"], { stdio: "inherit" })
38
+ test.on("error", () => {})
39
+ children.push(test)
40
+ }
41
+
42
+ const cleanup = (): void => {
43
+ for (const c of children) c.kill("SIGTERM")
44
+ }
45
+ process.on("SIGINT", cleanup)
46
+ process.on("SIGTERM", cleanup)
47
+ return new Promise<number>((res) => {
48
+ bun.on("exit", (code) => {
49
+ for (const c of children) if (c !== bun) c.kill("SIGTERM")
50
+ res(code ?? 0)
51
+ })
52
+ })
53
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `hyper diff <component>` — inspect drift between local files and the
3
+ * registry version of a component.
4
+ *
5
+ * Output per file:
6
+ * ok local sha matches registry (after applying the user's alias)
7
+ * drift local sha differs from registry — print line-diff
8
+ * missing the file isn't installed (run `hyper add` first)
9
+ */
10
+
11
+ import type { ParsedArgs } from "../args.ts"
12
+ import { readConfig } from "../config/index.ts"
13
+ import {
14
+ createRegistryClient,
15
+ readLocalFile,
16
+ resolveTarget,
17
+ rewriteFile,
18
+ } from "../registry/index.ts"
19
+
20
+ export async function runDiff(args: ParsedArgs): Promise<number> {
21
+ const name = args.positional[0]
22
+ if (!name) {
23
+ console.error("usage: hyper diff <component>")
24
+ return 2
25
+ }
26
+ const config = await readConfig()
27
+ const client = createRegistryClient({ url: config.registryUrl })
28
+ const component = await client.getComponent(name).catch(() => null)
29
+ if (!component) {
30
+ console.error(`unknown component: ${name}`)
31
+ return 2
32
+ }
33
+
34
+ const subpathsByComponent = new Map<string, Readonly<Record<string, string>>>([
35
+ [component.name, component.subpaths],
36
+ ])
37
+
38
+ let drift = 0
39
+ for (const f of component.files) {
40
+ const resolved = resolveTarget(f, component.name, config.baseDir)
41
+ const relPath = resolved.relPath
42
+ const local = await readLocalFile(process.cwd(), config, component.name, f)
43
+ if (local === null) {
44
+ console.log(`missing ${relPath}`)
45
+ drift += 1
46
+ continue
47
+ }
48
+ const expected = resolved.rewriteImports
49
+ ? rewriteFile(f.contents, {
50
+ alias: config.alias,
51
+ targetPath: relPath,
52
+ baseDir: config.baseDir,
53
+ subpathsByComponent,
54
+ })
55
+ : f.contents
56
+ if (local === expected) {
57
+ console.log(`ok ${relPath}`)
58
+ continue
59
+ }
60
+ drift += 1
61
+ console.log(`drift ${relPath}`)
62
+ const changes = lineDiff(expected, local)
63
+ for (const c of changes.slice(0, 20)) console.log(` ${c}`)
64
+ if (changes.length > 20) console.log(` … (${changes.length - 20} more)`)
65
+ }
66
+ return drift > 0 ? 1 : 0
67
+ }
68
+
69
+ function lineDiff(a: string, b: string): string[] {
70
+ const aL = a.split("\n")
71
+ const bL = b.split("\n")
72
+ const max = Math.max(aL.length, bL.length)
73
+ const out: string[] = []
74
+ for (let i = 0; i < max; i++) {
75
+ if (aL[i] === bL[i]) continue
76
+ if (aL[i] !== undefined) out.push(`- ${aL[i]}`)
77
+ if (bL[i] !== undefined) out.push(`+ ${bL[i]}`)
78
+ }
79
+ return out
80
+ }
@@ -0,0 +1,92 @@
1
+ import { type ParsedArgs, isJson } from "../args.ts"
2
+ import { resolveEntry } from "../entry.ts"
3
+ import { loadApp, loadComponentModule } from "../load-app.ts"
4
+
5
+ /**
6
+ * `hyper env --check` — boots the app just far enough to run env parsing
7
+ * and reports why/fix. Since boot happens on the first `fetch`, we send
8
+ * a synthetic request through the app.
9
+ *
10
+ * `hyper env --unsafe-print` — resolve env against declared schemas and
11
+ * print the merged object. Secrets (as declared via `secret(...)` or
12
+ * the `secrets: [...]` list) are redacted UNLESS this flag is passed,
13
+ * so the opt-in makes leaking them an explicit, auditable action.
14
+ */
15
+ export async function runEnvCheck(args: ParsedArgs): Promise<number> {
16
+ const entry = await resolveEntry(args.positional)
17
+ if (!entry) {
18
+ console.error("error: no entry file found")
19
+ return 2
20
+ }
21
+ const app = await loadApp(entry)
22
+ if (!app) {
23
+ console.error(`error: no default/named 'app' export in ${entry}`)
24
+ return 2
25
+ }
26
+ const unsafe = args.flags["unsafe-print"] === true || args.flags.unsafePrint === true
27
+
28
+ try {
29
+ const core = await loadComponentModule<typeof import("@hyper/core")>("core")
30
+ if (!core) {
31
+ console.error("error: cannot find @hyper/core (run `hyper add core` first?)")
32
+ return 2
33
+ }
34
+ const cfg = app.__config
35
+ const schemas = collectEnvSchemas(cfg)
36
+ const secretPaths = Array.isArray(cfg.env?.secrets) ? (cfg.env?.secrets ?? []) : []
37
+ const source = cfg.env?.source ?? process.env
38
+ const merged = await core.parseEnv(schemas, source as Record<string, string | undefined>)
39
+
40
+ if (unsafe) {
41
+ const output = { ...merged }
42
+ if (isJson(args.flags)) console.log(JSON.stringify(output, null, 2))
43
+ else console.log(JSON.stringify(output, null, 2))
44
+ return 0
45
+ }
46
+
47
+ const redacted = redact(merged, secretPaths)
48
+ if (isJson(args.flags)) {
49
+ console.log(JSON.stringify({ ok: true, env: redacted }))
50
+ } else {
51
+ console.log("env ok")
52
+ console.log(JSON.stringify(redacted, null, 2))
53
+ }
54
+ return 0
55
+ } catch (e) {
56
+ const body =
57
+ e instanceof Error
58
+ ? {
59
+ error: {
60
+ name: e.name,
61
+ message: e.message,
62
+ issues: (e as unknown as { issues?: unknown }).issues,
63
+ },
64
+ }
65
+ : { error: { message: String(e) } }
66
+ if (isJson(args.flags)) console.log(JSON.stringify(body))
67
+ else console.error("env check failed:", JSON.stringify(body, null, 2))
68
+ return 1
69
+ }
70
+ }
71
+
72
+ function collectEnvSchemas(cfg: {
73
+ env?: { schema?: unknown } | { schema?: unknown }[]
74
+ }): import("@hyper/core").StandardSchemaV1[] {
75
+ const schemas: import("@hyper/core").StandardSchemaV1[] = []
76
+ const envCfg = (cfg as { env?: { schema?: unknown } }).env
77
+ if (envCfg && typeof envCfg === "object" && "schema" in envCfg && envCfg.schema) {
78
+ schemas.push(envCfg.schema as import("@hyper/core").StandardSchemaV1)
79
+ }
80
+ return schemas
81
+ }
82
+
83
+ function redact(env: Record<string, unknown>, paths: readonly string[]): Record<string, unknown> {
84
+ if (paths.length === 0) return env
85
+ const out: Record<string, unknown> = { ...env }
86
+ for (const p of paths) {
87
+ if (p in out && out[p] !== undefined) {
88
+ out[p] = "[redacted]"
89
+ }
90
+ }
91
+ return out
92
+ }
@@ -0,0 +1,42 @@
1
+ export const HELP_TEXT = `hyper — fast, opinionated, AI-native API framework
2
+
3
+ Usage:
4
+ hyper <command> [options]
5
+
6
+ Project commands:
7
+ init [template] Scaffold a new app (templates: minimal, api). Writes hyper.config.json + tsconfig paths, then installs core.
8
+ dev [entry] Run app with Bun hot reload + tsgo --watch (--test for bun test --watch)
9
+ build [entry] Bundle app + emit route graph
10
+ openapi [out] Emit OpenAPI 3.1 spec
11
+ test Run .example() contracts + bun:test (--fuzz, --types, --reporter=junit)
12
+ typecheck Run tsgo --noEmit against the project
13
+ env --check Validate env against declared schema
14
+ routes [entry] Print the route graph (--json for machine output)
15
+ client <out> [entry] Emit a typed RPC client
16
+ mcp [entry] Serve dev MCP view (use --audit to print exposed surface)
17
+ bench [entry] Run the in-process latency benchmark
18
+ security --check Audit secure-by-default posture
19
+ version Print version + toolchain info
20
+
21
+ Registry commands:
22
+ add <component>... Copy components into your repo. Resolves deps + rewrites imports.
23
+ Flags: --info --force --dry-run --json --list
24
+ diff <component> Show drift between local files and the registry
25
+ update [component] Update installed components to the latest registry version
26
+ Flags: --force --dry-run
27
+ list [query] List / search the registry catalog (--json)
28
+ search <query> Alias of \`hyper list <query>\`
29
+
30
+ Flags:
31
+ --json Machine-readable output for scripting/CI
32
+ --help, -h Show this help
33
+
34
+ Environment:
35
+ HYPER_REGISTRY_URL Override the registry base URL (default: https://hyperjs.ai)
36
+ HYPER_SKIP_LISTEN Set automatically by introspection commands; user code can opt-out
37
+ `
38
+
39
+ export function runHelp(): number {
40
+ console.log(HELP_TEXT)
41
+ return 0
42
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * `hyper init [template]` — scaffold a new Hyper app.
3
+ *
4
+ * hyper init # minimal template into the current dir
5
+ * hyper init api # api template
6
+ * hyper init --dir my-app # into a subdir
7
+ * hyper init --no-install # skip auto `hyper add core`
8
+ * hyper init --agent-rules # also install the agent-rules component
9
+ *
10
+ * The template files have NO `@usehyper/*` deps. After files are written
11
+ * we run `hyper add` for each `template.components` entry, which copies the
12
+ * framework source into `<baseDir>/<component>/` and updates the lockfile.
13
+ */
14
+
15
+ import { mkdir, stat, writeFile } from "node:fs/promises"
16
+ import { dirname, resolve } from "node:path"
17
+ import { type ParsedArgs, isJson } from "../args.ts"
18
+ import { defaultConfig, patchTsConfig, readLock, writeConfig, writeLock } from "../config/index.ts"
19
+ import { applyComponents, createRegistryClient } from "../registry/index.ts"
20
+ import { TEMPLATES } from "../templates.ts"
21
+
22
+ export async function runInit(args: ParsedArgs): Promise<number> {
23
+ const templateName = args.positional[0] ?? "minimal"
24
+ const targetDir = typeof args.flags.dir === "string" ? args.flags.dir : "."
25
+ const template = TEMPLATES[templateName]
26
+ if (!template) {
27
+ console.error(
28
+ `unknown template "${templateName}"; available: ${Object.keys(TEMPLATES).join(", ")}`,
29
+ )
30
+ return 2
31
+ }
32
+
33
+ const cwd = resolve(process.cwd(), targetDir)
34
+ await mkdir(cwd, { recursive: true })
35
+
36
+ const writtenFiles: string[] = []
37
+ for (const [rel, contents] of Object.entries(template.files)) {
38
+ const abs = resolve(cwd, rel)
39
+ if (await pathExists(abs)) continue // never clobber existing files
40
+ await mkdir(dirname(abs), { recursive: true })
41
+ await writeFile(abs, contents)
42
+ writtenFiles.push(abs)
43
+ }
44
+
45
+ // Write hyper.config.json + patch tsconfig paths.
46
+ const config = defaultConfig()
47
+ await writeConfig(config, cwd)
48
+ await patchTsConfig(config.alias, config.baseDir, cwd)
49
+
50
+ // Auto-install template components (core, optionally log, etc.).
51
+ let componentsInstalled = 0
52
+ let installPeerDeps: Record<string, string> = {}
53
+ if (args.flags["no-install"] !== true) {
54
+ const components = [...template.components]
55
+ if (args.flags["agent-rules"] === true || args.flags["with-agent-rules"] === true) {
56
+ components.push("agent-rules")
57
+ }
58
+ const client = createRegistryClient({ url: config.registryUrl })
59
+ const lock = await readLock(cwd)
60
+ const outcome = await applyComponents(components, {
61
+ cwd,
62
+ config,
63
+ client,
64
+ lock,
65
+ force: false,
66
+ dryRun: false,
67
+ })
68
+ await writeLock(outcome.lock, cwd)
69
+ componentsInstalled = outcome.components.length
70
+ installPeerDeps = outcome.peerDeps as Record<string, string>
71
+ }
72
+
73
+ if (isJson(args.flags)) {
74
+ console.log(
75
+ JSON.stringify({
76
+ template: template.name,
77
+ cwd,
78
+ files: writtenFiles,
79
+ componentsInstalled,
80
+ }),
81
+ )
82
+ return 0
83
+ }
84
+
85
+ console.log(`initialized "${template.name}" template in ${cwd}`)
86
+ for (const f of writtenFiles) console.log(` + ${f}`)
87
+ if (componentsInstalled > 0) {
88
+ console.log(`installed ${componentsInstalled} component(s) from ${config.registryUrl}`)
89
+ }
90
+
91
+ console.log("\nnext steps:")
92
+ console.log(` cd ${targetDir === "." ? "." : targetDir}`)
93
+ console.log(" bun install")
94
+ if (Object.keys(installPeerDeps).length > 0) {
95
+ console.log(
96
+ ` bun add ${Object.entries(installPeerDeps)
97
+ .map(([k, v]) => `${k}@${v}`)
98
+ .join(" ")}`,
99
+ )
100
+ }
101
+ console.log(" bun run dev")
102
+ console.log("")
103
+ console.log(" # for AI agents (Cursor / Claude Code), drop in agent rules:")
104
+ console.log(" hyper add agent-rules")
105
+ console.log("")
106
+ console.log(" # for AI tools to discover this registry, point them at:")
107
+ console.log(` ${config.registryUrl}/mcp`)
108
+
109
+ return 0
110
+ }
111
+
112
+ async function pathExists(p: string): Promise<boolean> {
113
+ try {
114
+ await stat(p)
115
+ return true
116
+ } catch {
117
+ return false
118
+ }
119
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * `hyper list` / `hyper search <q>` — browse the registry from the CLI.
3
+ *
4
+ * Reads the index manifest and prints a name/version/description table.
5
+ * Supports `--json` and a substring filter via positional `query` (used by
6
+ * `hyper search`).
7
+ */
8
+
9
+ import { type ParsedArgs, isJson } from "../args.ts"
10
+ import { readConfig } from "../config/index.ts"
11
+ import { createRegistryClient } from "../registry/index.ts"
12
+
13
+ export async function runList(args: ParsedArgs): Promise<number> {
14
+ const config = await readConfig()
15
+ const client = createRegistryClient({ url: config.registryUrl })
16
+ const index = await client.getIndex()
17
+ const q = args.positional[0]?.toLowerCase()
18
+ const filtered = q
19
+ ? index.components.filter(
20
+ (c) =>
21
+ c.name.toLowerCase().includes(q) ||
22
+ c.description.toLowerCase().includes(q) ||
23
+ c.registryDeps.some((d) => d.toLowerCase().includes(q)),
24
+ )
25
+ : index.components
26
+
27
+ if (isJson(args.flags)) {
28
+ console.log(JSON.stringify(filtered, null, 2))
29
+ return 0
30
+ }
31
+
32
+ if (filtered.length === 0) {
33
+ console.log(q ? `no components matching "${q}"` : "no components in registry")
34
+ return 0
35
+ }
36
+
37
+ const w = Math.max(8, ...filtered.map((c) => c.name.length))
38
+ console.log(`registry: ${client.url}`)
39
+ console.log(`${"name".padEnd(w)} version description`)
40
+ console.log(`${"".padEnd(w, "-")} ------- -----------`)
41
+ for (const c of filtered) {
42
+ console.log(`${c.name.padEnd(w)} ${c.version.padEnd(7)} ${c.description}`)
43
+ }
44
+ console.log(`\n${filtered.length} component(s)`)
45
+ return 0
46
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `hyper mcp` — serve the app over MCP (JSON-RPC).
3
+ * `hyper mcp --audit` — print the exposed surface without serving.
4
+ * `hyper mcp --manifest` — write the manifest JSON to stdout.
5
+ */
6
+
7
+ import { type ParsedArgs, isJson } from "../args.ts"
8
+ import { resolveEntry } from "../entry.ts"
9
+ import { loadApp, loadComponentModule } from "../load-app.ts"
10
+
11
+ export async function runMcp(args: ParsedArgs): Promise<number> {
12
+ const entry = await resolveEntry(args.positional)
13
+ if (!entry) {
14
+ console.error("error: no entry file found")
15
+ return 2
16
+ }
17
+ const app = await loadApp(entry)
18
+ if (!app) {
19
+ console.error(`error: no default/named 'app' export in ${entry}`)
20
+ return 2
21
+ }
22
+ const mod = await loadComponentModule<typeof import("@hyper/mcp")>("mcp")
23
+ if (!mod) {
24
+ console.error("error: @hyper/mcp not installed in this project. Run `hyper add mcp` first.")
25
+ return 2
26
+ }
27
+
28
+ if (args.flags.manifest === true) {
29
+ const manifest = app.toMCPManifest()
30
+ console.log(JSON.stringify(manifest, null, 2))
31
+ return 0
32
+ }
33
+
34
+ if (args.flags.audit === true) {
35
+ const report = mod.auditMcp(app)
36
+ if (isJson(args.flags)) {
37
+ console.log(JSON.stringify(report, null, 2))
38
+ } else {
39
+ console.log(mod.formatAuditHuman(report))
40
+ }
41
+ return 0
42
+ }
43
+
44
+ const server = mod.mcpServer(app)
45
+ const port = Number(args.flags.port ?? process.env.PORT ?? 5174)
46
+ const bun = Bun.serve({ port, fetch: server.handle })
47
+ console.log(`MCP server listening on http://localhost:${bun.port}`)
48
+ process.on("SIGTERM", () => bun.stop(false))
49
+ process.on("SIGINT", () => bun.stop(false))
50
+ return await new Promise<number>(() => {})
51
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * `hyper openapi [out]` — emits openapi.json for the current app.
3
+ *
4
+ * Dynamically imports @hyper/openapi so consumers without it installed
5
+ * don't incur the dependency. Falls back to `app.toOpenAPI()` (core).
6
+ */
7
+
8
+ import { mkdir, writeFile } from "node:fs/promises"
9
+ import { dirname, resolve } from "node:path"
10
+ import type { ParsedArgs } from "../args.ts"
11
+ import { isJson } from "../args.ts"
12
+ import { resolveEntry } from "../entry.ts"
13
+ import { loadApp, loadComponentModule } from "../load-app.ts"
14
+
15
+ export async function runOpenapi(args: ParsedArgs): Promise<number> {
16
+ const entry = await resolveEntry(args.positional.slice(1))
17
+ if (!entry) {
18
+ console.error("error: no entry file found")
19
+ return 2
20
+ }
21
+ const app = await loadApp(entry)
22
+ if (!app) {
23
+ console.error("error: entry did not export a Hyper app")
24
+ return 2
25
+ }
26
+
27
+ // Prefer the @hyper/openapi component if installed (richer schema converters);
28
+ // fall back to core's built-in projector otherwise.
29
+ let doc: unknown
30
+ const m = await loadComponentModule<typeof import("@hyper/openapi")>("openapi")
31
+ if (m) {
32
+ doc = m.generate(app, {
33
+ ...(typeof args.flags.title === "string" && { title: args.flags.title }),
34
+ ...(typeof args.flags.version === "string" && { version: args.flags.version }),
35
+ })
36
+ } else {
37
+ doc = app.toOpenAPI()
38
+ }
39
+
40
+ const out = args.positional[0]
41
+ if (!out) {
42
+ console.log(isJson(args.flags) ? JSON.stringify(doc) : JSON.stringify(doc, null, 2))
43
+ return 0
44
+ }
45
+ const abs = resolve(process.cwd(), out)
46
+ await mkdir(dirname(abs), { recursive: true })
47
+ await writeFile(abs, `${JSON.stringify(doc, null, 2)}\n`)
48
+ console.log(`wrote ${abs}`)
49
+ return 0
50
+ }
@@ -0,0 +1,45 @@
1
+ import type { HyperApp, Route } from "@hyper/core"
2
+ import { type ParsedArgs, isJson } from "../args.ts"
3
+ import { resolveEntry } from "../entry.ts"
4
+ import { loadApp } from "../load-app.ts"
5
+
6
+ export async function runRoutes(args: ParsedArgs): Promise<number> {
7
+ const entry = await resolveEntry(args.positional)
8
+ if (!entry) {
9
+ console.error("error: no entry file found (tried src/app.ts, app.ts, src/index.ts)")
10
+ return 2
11
+ }
12
+ const app = await loadApp(entry)
13
+ if (!app) {
14
+ console.error(`error: no default/named 'app' export in ${entry}`)
15
+ return 2
16
+ }
17
+ const list = app.routeList.map((r) => ({
18
+ method: r.method,
19
+ path: r.path,
20
+ name: r.meta.name,
21
+ tags: r.meta.tags ?? [],
22
+ mcp: Boolean(r.meta.mcp),
23
+ }))
24
+ if (isJson(args.flags)) {
25
+ console.log(JSON.stringify(list, null, 2))
26
+ return 0
27
+ }
28
+ printTable(list, app.routeList)
29
+ return 0
30
+ }
31
+
32
+ function printTable(
33
+ list: readonly { method: string; path: string; name: string | undefined; mcp: boolean }[],
34
+ routes: readonly Route[],
35
+ ): void {
36
+ const methodW = Math.max(6, ...list.map((r) => r.method.length))
37
+ const pathW = Math.max(4, ...list.map((r) => r.path.length))
38
+ console.log(`${"method".padEnd(methodW)} ${"path".padEnd(pathW)} name / meta`)
39
+ console.log(`${"".padEnd(methodW, "-")} ${"".padEnd(pathW, "-")} -----------`)
40
+ for (const r of list) {
41
+ const mcpTag = r.mcp ? " [mcp]" : ""
42
+ console.log(`${r.method.padEnd(methodW)} ${r.path.padEnd(pathW)} ${r.name ?? ""}${mcpTag}`)
43
+ }
44
+ console.log(`\n${routes.length} route(s)`)
45
+ }