@usehyper/cli 0.1.0 → 0.1.1
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/package.json +7 -2
- package/src/__tests__/cli.test.ts +15 -2
- package/src/banner.ts +34 -0
- package/src/commands/init.ts +97 -17
- package/src/registry/snapshot.ts +2 -2
- package/src/templates.ts +14 -4
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usehyper/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hyper CLI — registry-driven scaffolding (init/add/diff/update) plus dev/build/test/openapi/mcp.",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"homepage": "https://hyperjs.ai",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/
|
|
10
|
+
"url": "git+https://github.com/pontusab/hyper.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/pontusab/hyper/issues"
|
|
10
15
|
},
|
|
11
16
|
"files": ["src", "registry-sources", "LICENSE", "README.md"],
|
|
12
17
|
"exports": {
|
|
@@ -38,8 +38,21 @@ describe("cli templates", () => {
|
|
|
38
38
|
})
|
|
39
39
|
|
|
40
40
|
test("templates have no @usehyper/* runtime deps", () => {
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
for (const name of ["minimal", "api"] as const) {
|
|
42
|
+
const pkg = JSON.parse(TEMPLATES[name]!.files["package.json"]!) as {
|
|
43
|
+
dependencies?: Record<string, string>
|
|
44
|
+
}
|
|
45
|
+
expect(pkg.dependencies ?? {}).toEqual({})
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test("templates pin @usehyper/cli as a devDependency", () => {
|
|
50
|
+
for (const name of ["minimal", "api"] as const) {
|
|
51
|
+
const pkg = JSON.parse(TEMPLATES[name]!.files["package.json"]!) as {
|
|
52
|
+
devDependencies?: Record<string, string>
|
|
53
|
+
}
|
|
54
|
+
expect(pkg.devDependencies?.["@usehyper/cli"]).toBeDefined()
|
|
55
|
+
}
|
|
43
56
|
})
|
|
44
57
|
|
|
45
58
|
test("templates declare which components to install after init", () => {
|
package/src/banner.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Welcome banner printed at the top of `hyper init`. Deliberately scoped
|
|
3
|
+
* to the scaffolding moment — every other command stays terse so output
|
|
4
|
+
* is friendly to pipes, agent loops, and CI logs.
|
|
5
|
+
*
|
|
6
|
+
* Suppressed when:
|
|
7
|
+
* - `--json` is set (the caller wants machine output)
|
|
8
|
+
* - stdout isn't a TTY (piped or redirected)
|
|
9
|
+
* - `CI` is set (CI runners log this verbatim)
|
|
10
|
+
*
|
|
11
|
+
* The shape is the figlet "small" font for "hyper". Exact widths preserved
|
|
12
|
+
* so it lines up the same in any monospace terminal.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const LOGO = String.raw`
|
|
16
|
+
_
|
|
17
|
+
| |_ _ _ __ ___ _ __
|
|
18
|
+
| ' \| || | '_ \ / -_) '_|
|
|
19
|
+
|_||_|\_, | .__/ \___|_|
|
|
20
|
+
|__/|_|
|
|
21
|
+
`
|
|
22
|
+
|
|
23
|
+
export interface BannerOptions {
|
|
24
|
+
readonly json?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function printBanner(version: string, opts: BannerOptions = {}): void {
|
|
28
|
+
if (opts.json) return
|
|
29
|
+
if (process.env.CI) return
|
|
30
|
+
if (process.env.HYPER_NO_BANNER) return
|
|
31
|
+
if (!process.stdout.isTTY) return
|
|
32
|
+
process.stdout.write(LOGO)
|
|
33
|
+
process.stdout.write(`v${version} · API framework for Bun, distributed as source\n\n`)
|
|
34
|
+
}
|
package/src/commands/init.ts
CHANGED
|
@@ -4,21 +4,49 @@
|
|
|
4
4
|
* hyper init # minimal template into the current dir
|
|
5
5
|
* hyper init api # api template
|
|
6
6
|
* hyper init --dir my-app # into a subdir
|
|
7
|
-
* hyper init --no-
|
|
7
|
+
* hyper init --no-components # skip auto `hyper add core`
|
|
8
|
+
* hyper init --no-install # skip `bun install` after scaffolding
|
|
8
9
|
* hyper init --agent-rules # also install the agent-rules component
|
|
9
10
|
*
|
|
10
|
-
* The template files have NO `@
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The template files have NO `@hyper/*` runtime deps — imports use
|
|
12
|
+
* `@hyper/core` and resolve to vendored source under `src/hyper/<name>/` via
|
|
13
|
+
* tsconfig paths.
|
|
14
|
+
*
|
|
15
|
+
* Flow:
|
|
16
|
+
* 1. Write template files into the target dir (with `@usehyper/cli` pinned
|
|
17
|
+
* to the running CLI's version in devDependencies).
|
|
18
|
+
* 2. Run the registry applier — copies component source into the project
|
|
19
|
+
* and aggregates peer-dep declarations.
|
|
20
|
+
* 3. Merge any peer deps into `package.json`.
|
|
21
|
+
* 4. Run `bun install` so `node_modules/.bin/hyper` is available
|
|
22
|
+
* immediately.
|
|
13
23
|
*/
|
|
14
24
|
|
|
15
|
-
import {
|
|
25
|
+
import { spawn } from "node:child_process"
|
|
26
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises"
|
|
16
27
|
import { dirname, resolve } from "node:path"
|
|
17
28
|
import { type ParsedArgs, isJson } from "../args.ts"
|
|
29
|
+
import { printBanner } from "../banner.ts"
|
|
18
30
|
import { defaultConfig, patchTsConfig, readLock, writeConfig, writeLock } from "../config/index.ts"
|
|
19
31
|
import { applyComponents, createRegistryClient } from "../registry/index.ts"
|
|
20
32
|
import { TEMPLATES } from "../templates.ts"
|
|
21
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The CLI's own version. Used to pin `@usehyper/cli` in scaffolded
|
|
36
|
+
* `package.json` files. Resolved at runtime so we don't drift if the bin
|
|
37
|
+
* is invoked from a workspace where the package.json on disk is newer than
|
|
38
|
+
* the constants frozen into a build.
|
|
39
|
+
*/
|
|
40
|
+
async function readOwnVersion(): Promise<string> {
|
|
41
|
+
try {
|
|
42
|
+
const url = new URL("../../package.json", import.meta.url)
|
|
43
|
+
const raw = await readFile(url, "utf8")
|
|
44
|
+
const v = (JSON.parse(raw) as { version?: string }).version
|
|
45
|
+
if (typeof v === "string" && v.length > 0) return v
|
|
46
|
+
} catch {}
|
|
47
|
+
return "latest"
|
|
48
|
+
}
|
|
49
|
+
|
|
22
50
|
export async function runInit(args: ParsedArgs): Promise<number> {
|
|
23
51
|
const templateName = args.positional[0] ?? "minimal"
|
|
24
52
|
const targetDir = typeof args.flags.dir === "string" ? args.flags.dir : "."
|
|
@@ -33,16 +61,19 @@ export async function runInit(args: ParsedArgs): Promise<number> {
|
|
|
33
61
|
const cwd = resolve(process.cwd(), targetDir)
|
|
34
62
|
await mkdir(cwd, { recursive: true })
|
|
35
63
|
|
|
64
|
+
const cliVersion = await readOwnVersion()
|
|
65
|
+
printBanner(cliVersion, { json: isJson(args.flags) })
|
|
66
|
+
|
|
36
67
|
const writtenFiles: string[] = []
|
|
37
68
|
for (const [rel, contents] of Object.entries(template.files)) {
|
|
38
69
|
const abs = resolve(cwd, rel)
|
|
39
70
|
if (await pathExists(abs)) continue // never clobber existing files
|
|
40
71
|
await mkdir(dirname(abs), { recursive: true })
|
|
41
|
-
|
|
72
|
+
const final = contents.replaceAll("__HYPER_CLI_VERSION__", cliVersion)
|
|
73
|
+
await writeFile(abs, final)
|
|
42
74
|
writtenFiles.push(abs)
|
|
43
75
|
}
|
|
44
76
|
|
|
45
|
-
// Write hyper.config.json + patch tsconfig paths.
|
|
46
77
|
const config = defaultConfig()
|
|
47
78
|
await writeConfig(config, cwd)
|
|
48
79
|
await patchTsConfig(config.alias, config.baseDir, cwd)
|
|
@@ -50,7 +81,7 @@ export async function runInit(args: ParsedArgs): Promise<number> {
|
|
|
50
81
|
// Auto-install template components (core, optionally log, etc.).
|
|
51
82
|
let componentsInstalled = 0
|
|
52
83
|
let installPeerDeps: Record<string, string> = {}
|
|
53
|
-
if (args.flags["no-
|
|
84
|
+
if (args.flags["no-components"] !== true) {
|
|
54
85
|
const components = [...template.components]
|
|
55
86
|
if (args.flags["agent-rules"] === true || args.flags["with-agent-rules"] === true) {
|
|
56
87
|
components.push("agent-rules")
|
|
@@ -70,6 +101,15 @@ export async function runInit(args: ParsedArgs): Promise<number> {
|
|
|
70
101
|
installPeerDeps = outcome.peerDeps as Record<string, string>
|
|
71
102
|
}
|
|
72
103
|
|
|
104
|
+
if (Object.keys(installPeerDeps).length > 0) {
|
|
105
|
+
await mergePackageJsonDeps(cwd, installPeerDeps)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let installRan = false
|
|
109
|
+
if (args.flags["no-install"] !== true) {
|
|
110
|
+
installRan = await runBunInstall(cwd)
|
|
111
|
+
}
|
|
112
|
+
|
|
73
113
|
if (isJson(args.flags)) {
|
|
74
114
|
console.log(
|
|
75
115
|
JSON.stringify({
|
|
@@ -77,6 +117,7 @@ export async function runInit(args: ParsedArgs): Promise<number> {
|
|
|
77
117
|
cwd,
|
|
78
118
|
files: writtenFiles,
|
|
79
119
|
componentsInstalled,
|
|
120
|
+
installRan,
|
|
80
121
|
}),
|
|
81
122
|
)
|
|
82
123
|
return 0
|
|
@@ -90,18 +131,11 @@ export async function runInit(args: ParsedArgs): Promise<number> {
|
|
|
90
131
|
|
|
91
132
|
console.log("\nnext steps:")
|
|
92
133
|
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
|
-
}
|
|
134
|
+
if (!installRan) console.log(" bun install")
|
|
101
135
|
console.log(" bun run dev")
|
|
102
136
|
console.log("")
|
|
103
137
|
console.log(" # for AI agents (Cursor / Claude Code), drop in agent rules:")
|
|
104
|
-
console.log(" hyper add agent-rules")
|
|
138
|
+
console.log(" bunx hyper add agent-rules")
|
|
105
139
|
console.log("")
|
|
106
140
|
console.log(" # for AI tools to discover this registry, point them at:")
|
|
107
141
|
console.log(` ${config.registryUrl}/mcp`)
|
|
@@ -117,3 +151,49 @@ async function pathExists(p: string): Promise<boolean> {
|
|
|
117
151
|
return false
|
|
118
152
|
}
|
|
119
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Merge peer-deps declared by registry components into the scaffolded
|
|
157
|
+
* `package.json`. Existing keys win — we never downgrade or override what
|
|
158
|
+
* the user already specified.
|
|
159
|
+
*/
|
|
160
|
+
async function mergePackageJsonDeps(
|
|
161
|
+
cwd: string,
|
|
162
|
+
peerDeps: Readonly<Record<string, string>>,
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const path = resolve(cwd, "package.json")
|
|
165
|
+
let raw: string
|
|
166
|
+
try {
|
|
167
|
+
raw = await readFile(path, "utf8")
|
|
168
|
+
} catch {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
const pkg = JSON.parse(raw) as { dependencies?: Record<string, string> }
|
|
172
|
+
const merged = { ...peerDeps, ...(pkg.dependencies ?? {}) }
|
|
173
|
+
pkg.dependencies = merged
|
|
174
|
+
await writeFile(path, `${JSON.stringify(pkg, null, 2)}\n`)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Spawn `bun install` in the project dir. Returns true on success. Errors
|
|
179
|
+
* are logged but don't abort the scaffold — the user can always retry the
|
|
180
|
+
* install manually.
|
|
181
|
+
*/
|
|
182
|
+
async function runBunInstall(cwd: string): Promise<boolean> {
|
|
183
|
+
process.stdout.write("\nrunning `bun install`…\n")
|
|
184
|
+
return await new Promise<boolean>((res) => {
|
|
185
|
+
const child = spawn("bun", ["install"], { cwd, stdio: "inherit" })
|
|
186
|
+
child.on("error", (err) => {
|
|
187
|
+
console.error(`warning: bun install failed to start (${err.message})`)
|
|
188
|
+
res(false)
|
|
189
|
+
})
|
|
190
|
+
child.on("exit", (code) => {
|
|
191
|
+
if (code !== 0) {
|
|
192
|
+
console.error(`warning: \`bun install\` exited with code ${code ?? "?"}; run it manually.`)
|
|
193
|
+
res(false)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
res(true)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
}
|
package/src/registry/snapshot.ts
CHANGED
|
@@ -8,8 +8,8 @@ import type { RegistryComponent, RegistryIndex } from "./types.ts"
|
|
|
8
8
|
|
|
9
9
|
export const SNAPSHOT_INDEX: RegistryIndex = {
|
|
10
10
|
"schema": 1,
|
|
11
|
-
"generatedAt": "2026-05-
|
|
12
|
-
"source": "https://github.com/
|
|
11
|
+
"generatedAt": "2026-05-24T13:00:38.109Z",
|
|
12
|
+
"source": "https://github.com/pontusab/hyper",
|
|
13
13
|
"components": [
|
|
14
14
|
{
|
|
15
15
|
"name": "agent-rules",
|
package/src/templates.ts
CHANGED
|
@@ -49,6 +49,13 @@ const MINIMAL_TSCONFIG = `{
|
|
|
49
49
|
}
|
|
50
50
|
`
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Template `package.json`. `__HYPER_CLI_VERSION__` is replaced by `init` with
|
|
54
|
+
* the version of the running CLI so freshly-scaffolded projects pin against
|
|
55
|
+
* the same release that scaffolded them. Once `bun install` runs, the
|
|
56
|
+
* `hyper` binary lands in `node_modules/.bin/` and is available as
|
|
57
|
+
* `bun run hyper …` (or `bunx hyper …`).
|
|
58
|
+
*/
|
|
52
59
|
const MINIMAL_PKG = `{
|
|
53
60
|
"name": "my-hyper-app",
|
|
54
61
|
"type": "module",
|
|
@@ -61,6 +68,7 @@ const MINIMAL_PKG = `{
|
|
|
61
68
|
},
|
|
62
69
|
"devDependencies": {
|
|
63
70
|
"@types/bun": "latest",
|
|
71
|
+
"@usehyper/cli": "^__HYPER_CLI_VERSION__",
|
|
64
72
|
"typescript": "^5.6.0"
|
|
65
73
|
}
|
|
66
74
|
}
|
|
@@ -107,11 +115,13 @@ bun run dev
|
|
|
107
115
|
|
|
108
116
|
## Manage components
|
|
109
117
|
|
|
118
|
+
The \`hyper\` CLI is a devDependency. Run it via \`bunx\` or \`bun run\`:
|
|
119
|
+
|
|
110
120
|
\`\`\`bash
|
|
111
|
-
hyper list
|
|
112
|
-
hyper add cors
|
|
113
|
-
hyper diff log
|
|
114
|
-
hyper update
|
|
121
|
+
bunx hyper list # browse the registry
|
|
122
|
+
bunx hyper add cors # add a component
|
|
123
|
+
bunx hyper diff log # inspect drift on an installed component
|
|
124
|
+
bunx hyper update # pull the latest registry versions
|
|
115
125
|
\`\`\`
|
|
116
126
|
`
|
|
117
127
|
|