@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 CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "@usehyper/cli",
3
- "version": "0.1.0",
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/usehyper/hyper.git"
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
- expect(TEMPLATES.minimal!.files["package.json"]).not.toContain("@usehyper/")
42
- expect(TEMPLATES.api!.files["package.json"]).not.toContain("@usehyper/")
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
+ }
@@ -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-install # skip auto `hyper add core`
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 `@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.
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 { mkdir, stat, writeFile } from "node:fs/promises"
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
- await writeFile(abs, contents)
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-install"] !== true) {
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
+ }
@@ -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-24T12:15:05.341Z",
12
- "source": "https://github.com/usehyper/hyper",
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 # browse the registry
112
- hyper add cors # add a component
113
- hyper diff log # inspect drift on an installed component
114
- hyper update # pull the latest registry versions
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