@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,177 @@
1
+ /**
2
+ * File placement and import rewriting at install time.
3
+ *
4
+ * Two responsibilities:
5
+ *
6
+ * 1. `resolveTarget(file, name, baseDir)` decides where a manifest file
7
+ * lands in the user's project. Honors per-file `target` placeholders
8
+ * (`@base/`, `@root/`, `@cursor/`, `~/`) and falls back to
9
+ * `<baseDir>/<componentName>/<rel>` for package-derived files or the
10
+ * project root for hand-authored ones.
11
+ *
12
+ * 2. `rewriteFile(contents, ctx)` rewrites `@hyper/<x>` imports in source
13
+ * files to whatever alias the user configured:
14
+ * alias: "@hyper" — pass-through (most projects)
15
+ * alias: "@/lib/hyper" — `@hyper/core` → `@/lib/hyper/core`
16
+ * alias: "relative" — `@hyper/core` → computed relative path
17
+ *
18
+ * Subpath aliases declared by each component (`@hyper/core/bun` →
19
+ * `core/adapters/bun`) are applied via the `subpathsByComponent` map.
20
+ *
21
+ * Both are pure functions — no I/O.
22
+ */
23
+
24
+ import { posix } from "node:path"
25
+ import type { RegistryFile } from "./types.ts"
26
+
27
+ const MANIFEST_ALIAS = "@hyper"
28
+
29
+ /** File extensions whose contents go through `rewriteFile`. */
30
+ const REWRITABLE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"])
31
+
32
+ export interface RewriteContext {
33
+ /** User-configured alias (`@hyper`, `@/lib/hyper`, or `"relative"`). */
34
+ readonly alias: string
35
+ /** Where the file ends up, relative to the project root. Used for relative mode. */
36
+ readonly targetPath: string
37
+ /** Where components are installed, relative to the project root. */
38
+ readonly baseDir: string
39
+ /**
40
+ * Subpath maps for every component being processed in this batch. Lets
41
+ * `@hyper/core/bun` resolve to `core/adapters/bun` (since that's where
42
+ * the file lives in the manifest).
43
+ *
44
+ * Already accounted for in the manifest itself for cross-component refs,
45
+ * but we re-apply for safety in case manifests evolve.
46
+ */
47
+ readonly subpathsByComponent: ReadonlyMap<string, Readonly<Record<string, string>>>
48
+ }
49
+
50
+ function escapeRegex(s: string): string {
51
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
52
+ }
53
+
54
+ const REWRITE_PATTERN = new RegExp(
55
+ // Capture: from / import / dynamic-import / triple-slash / require, then quoted spec.
56
+ `((?:from\\s*|import\\s*\\(?\\s*|require\\s*\\(\\s*)["'\`])${escapeRegex(MANIFEST_ALIAS)}\\/([a-z0-9-]+)((?:\\/[^"'\`]+)?)["'\`]`,
57
+ "g",
58
+ )
59
+
60
+ /**
61
+ * Rewrite the contents of one file according to the user's alias choice.
62
+ *
63
+ * Pure function — no I/O.
64
+ */
65
+ export function rewriteFile(contents: string, ctx: RewriteContext): string {
66
+ if (ctx.alias === MANIFEST_ALIAS) return contents
67
+
68
+ return contents.replace(REWRITE_PATTERN, (_full, prefix: string, pkg: string, sub: string) => {
69
+ const trailingQuote = prefix.slice(-1) // " ' or `
70
+ const subPath = sub ? sub.slice(1) : ""
71
+
72
+ // Resolve subpath through the component's map if relevant.
73
+ const resolvedSub = subPath ? (ctx.subpathsByComponent.get(pkg)?.[subPath] ?? subPath) : ""
74
+
75
+ if (ctx.alias === "relative") {
76
+ const fileTarget = resolvedSub ? `${pkg}/${resolvedSub}` : `${pkg}/index`
77
+ const absInstalled = posix.join(ctx.baseDir, `${fileTarget}.ts`)
78
+ const fromDir = posix.dirname(toPosix(ctx.targetPath))
79
+ let relPath = posix.relative(fromDir, absInstalled)
80
+ if (!relPath.startsWith(".")) relPath = `./${relPath}`
81
+ return `${prefix}${relPath}${trailingQuote}`
82
+ }
83
+
84
+ // Custom alias — `@/lib/hyper`, `~/hyper`, etc.
85
+ const tail = resolvedSub ? `/${pkg}/${resolvedSub}` : `/${pkg}`
86
+ return `${prefix}${ctx.alias}${tail}${trailingQuote}`
87
+ })
88
+ }
89
+
90
+ function toPosix(p: string): string {
91
+ return p.replace(/\\/g, "/")
92
+ }
93
+
94
+ export interface ResolvedTarget {
95
+ /** Project-relative path where this file should land. */
96
+ readonly relPath: string
97
+ /**
98
+ * Whether the contents should pass through `rewriteFile` (alias rewriting).
99
+ * False for markdown, JSON, env files, and anything else that isn't source.
100
+ */
101
+ readonly rewriteImports: boolean
102
+ /**
103
+ * Set when the manifest used a placeholder we don't recognize. The CLI
104
+ * surfaces this as a warning and falls back to a literal path.
105
+ */
106
+ readonly warning?: string
107
+ }
108
+
109
+ /**
110
+ * Resolve a manifest file to a project-relative install path.
111
+ *
112
+ * Honors per-file `target` overrides with a small set of placeholders
113
+ * (`@base`, `@root`, `@cursor`, `~`). Unrecognized `@<word>/` prefixes are
114
+ * passed through verbatim with a warning. Falls back to today's behavior
115
+ * when no `target` is set:
116
+ *
117
+ * - paths starting with `<componentName>/` go under `<baseDir>/`
118
+ * - anything else is a project-root drop (e.g. `AGENTS.md`)
119
+ */
120
+ export function resolveTarget(
121
+ file: RegistryFile,
122
+ componentName: string,
123
+ baseDir: string,
124
+ ): ResolvedTarget {
125
+ const target = file.target?.trim()
126
+ if (target) {
127
+ const expansion = expandPlaceholder(target, baseDir)
128
+ return {
129
+ relPath: posix.normalize(expansion.path),
130
+ rewriteImports: shouldRewriteByExt(expansion.path),
131
+ ...(expansion.warning !== undefined && { warning: expansion.warning }),
132
+ }
133
+ }
134
+ // Fallback: package-derived files start with `<componentName>/` and live
135
+ // under `<baseDir>/`. Hand-authored files without a target stay at the
136
+ // project root.
137
+ if (file.path.startsWith(`${componentName}/`)) {
138
+ return {
139
+ relPath: posix.join(baseDir, file.path),
140
+ rewriteImports: shouldRewriteByExt(file.path),
141
+ }
142
+ }
143
+ return {
144
+ relPath: file.path,
145
+ rewriteImports: shouldRewriteByExt(file.path),
146
+ }
147
+ }
148
+
149
+ interface PlaceholderExpansion {
150
+ readonly path: string
151
+ readonly warning?: string
152
+ }
153
+
154
+ function expandPlaceholder(target: string, baseDir: string): PlaceholderExpansion {
155
+ if (target.startsWith("@base/"))
156
+ return { path: posix.join(baseDir, target.slice("@base/".length)) }
157
+ if (target === "@base") return { path: baseDir }
158
+ if (target.startsWith("@root/")) return { path: target.slice("@root/".length) }
159
+ if (target.startsWith("~/")) return { path: target.slice(2) }
160
+ if (target.startsWith("@cursor/"))
161
+ return { path: posix.join(".cursor", target.slice("@cursor/".length)) }
162
+ // Unknown placeholder — pass through, warn the caller.
163
+ if (/^@[a-z][a-z0-9_-]*\//i.test(target)) {
164
+ return {
165
+ path: target,
166
+ warning: `Unknown target placeholder in "${target}" — writing to literal path. Known placeholders: @base, @root, @cursor, ~`,
167
+ }
168
+ }
169
+ return { path: target }
170
+ }
171
+
172
+ function shouldRewriteByExt(path: string): boolean {
173
+ const dot = path.lastIndexOf(".")
174
+ if (dot < 0) return false
175
+ const ext = path.slice(dot).toLowerCase()
176
+ return REWRITABLE_EXTS.has(ext)
177
+ }