@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
|
@@ -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
|
+
}
|