@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,268 @@
1
+ /**
2
+ * Component applier.
3
+ *
4
+ * Takes a fetched `RegistryComponent`, the user's `HyperConfig`, and writes
5
+ * the files into their repo. Handles:
6
+ *
7
+ * - recursive `registryDeps` resolution (depth-first, deduplicated)
8
+ * - import rewriting per the user's alias
9
+ * - sha256-based drift detection: refuse to overwrite locally-modified
10
+ * files unless `--force` is set
11
+ * - atomic-ish lockfile updates: in-memory mutation of the lock object,
12
+ * a single write at the end
13
+ * - peer-dependency reporting (returned to the caller)
14
+ */
15
+
16
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises"
17
+ import { dirname, resolve } from "node:path"
18
+ import type { HyperConfig, HyperLock, LockedComponent } from "../config/types.ts"
19
+ import type { RegistryClient } from "./client.ts"
20
+ import { mergeEnvFile } from "./env-writer.ts"
21
+ import { resolveTarget, rewriteFile } from "./rewrite.ts"
22
+ import type { RegistryComponent, RegistryFile } from "./types.ts"
23
+
24
+ export interface ApplyOptions {
25
+ readonly cwd: string
26
+ readonly config: HyperConfig
27
+ readonly client: RegistryClient
28
+ readonly lock: HyperLock
29
+ readonly force: boolean
30
+ readonly dryRun: boolean
31
+ /** Pretend each component is at this version (snapshot/CI use case). */
32
+ readonly version?: string
33
+ }
34
+
35
+ export interface ApplyOutcome {
36
+ /** Files written or that would have been written (dry run). */
37
+ readonly written: readonly { path: string; component: string; reason: "new" | "updated" }[]
38
+ /** Files unchanged (already match the registry). */
39
+ readonly unchanged: readonly { path: string; component: string }[]
40
+ /** Files that conflict (local-modified, no --force). */
41
+ readonly conflicts: readonly { path: string; component: string }[]
42
+ /** Components touched, in install order (deps first). */
43
+ readonly components: readonly RegistryComponent[]
44
+ /** Updated lock — caller persists with `writeLock`. */
45
+ readonly lock: HyperLock
46
+ /** Peer deps surfaced after install. */
47
+ readonly peerDeps: Readonly<Record<string, string>>
48
+ readonly optionalPeerDeps: Readonly<Record<string, string>>
49
+ /** Warnings raised by `resolveTarget` (unknown placeholders). */
50
+ readonly warnings: readonly string[]
51
+ /** Env-var changes applied to `.env.local`. */
52
+ readonly envVars: {
53
+ readonly path: string
54
+ readonly added: readonly string[]
55
+ readonly preserved: readonly string[]
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Resolve the full transitive dep set for a list of root components.
61
+ * Returns components in topological order (deps before dependents).
62
+ */
63
+ export async function resolveDeps(
64
+ roots: readonly string[],
65
+ client: RegistryClient,
66
+ ): Promise<RegistryComponent[]> {
67
+ const visited = new Map<string, RegistryComponent>()
68
+ const order: RegistryComponent[] = []
69
+ const visiting = new Set<string>()
70
+
71
+ const visit = async (name: string): Promise<void> => {
72
+ if (visited.has(name)) return
73
+ if (visiting.has(name)) {
74
+ throw new Error(`registry cycle detected at ${name}`)
75
+ }
76
+ visiting.add(name)
77
+ const c = await client.getComponent(name)
78
+ for (const d of c.registryDeps) await visit(d)
79
+ visiting.delete(name)
80
+ visited.set(name, c)
81
+ order.push(c)
82
+ }
83
+
84
+ for (const r of roots) await visit(r)
85
+ return order
86
+ }
87
+
88
+ async function readIfExists(path: string): Promise<string | null> {
89
+ try {
90
+ return await readFile(path, "utf8")
91
+ } catch {
92
+ return null
93
+ }
94
+ }
95
+
96
+ async function sha256(s: string): Promise<string> {
97
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(s))
98
+ return Array.from(new Uint8Array(buf))
99
+ .map((b) => b.toString(16).padStart(2, "0"))
100
+ .join("")
101
+ }
102
+
103
+ async function pathExists(p: string): Promise<boolean> {
104
+ try {
105
+ await stat(p)
106
+ return true
107
+ } catch {
108
+ return false
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Apply (or simulate applying) a list of root components.
114
+ *
115
+ * The roots come from the user (`hyper add cors auth-jwt` → `["cors", "auth-jwt"]`).
116
+ * Their transitive deps are resolved automatically.
117
+ */
118
+ export async function applyComponents(
119
+ roots: readonly string[],
120
+ opts: ApplyOptions,
121
+ ): Promise<ApplyOutcome> {
122
+ const components = await resolveDeps(roots, opts.client)
123
+ const subpathsByComponent = new Map(components.map((c) => [c.name, c.subpaths]))
124
+
125
+ const written: { path: string; component: string; reason: "new" | "updated" }[] = []
126
+ const unchanged: { path: string; component: string }[] = []
127
+ const conflicts: { path: string; component: string }[] = []
128
+ const peerDeps: Record<string, string> = {}
129
+ const optionalPeerDeps: Record<string, string> = {}
130
+ const warnings: string[] = []
131
+
132
+ const componentsByName: Record<string, LockedComponent> = { ...opts.lock.components }
133
+
134
+ for (const c of components) {
135
+ Object.assign(peerDeps, c.peerDeps)
136
+ Object.assign(optionalPeerDeps, c.optionalPeerDeps)
137
+
138
+ const lockedFiles: { path: string; sha256: string }[] = []
139
+
140
+ for (const f of c.files) {
141
+ const resolved = resolveTarget(f, c.name, opts.config.baseDir)
142
+ const relPath = resolved.relPath
143
+ if (resolved.warning) warnings.push(`${c.name}: ${resolved.warning}`)
144
+ const abs = resolve(opts.cwd, relPath)
145
+
146
+ const rewritten = resolved.rewriteImports
147
+ ? rewriteFile(f.contents, {
148
+ alias: opts.config.alias,
149
+ targetPath: relPath,
150
+ baseDir: opts.config.baseDir,
151
+ subpathsByComponent,
152
+ })
153
+ : f.contents
154
+ const newHash = await sha256(rewritten)
155
+
156
+ const existing = await readIfExists(abs)
157
+ if (existing !== null) {
158
+ const existingHash = await sha256(existing)
159
+ if (existingHash === newHash) {
160
+ unchanged.push({ path: relPath, component: c.name })
161
+ lockedFiles.push({ path: relPath, sha256: newHash })
162
+ continue
163
+ }
164
+ // The file exists and differs.
165
+ const lockedHash = opts.lock.components[c.name]?.files.find(
166
+ (lf) => lf.path === relPath,
167
+ )?.sha256
168
+ const isUpdate = lockedHash === existingHash
169
+ // - isUpdate: matches the lockfile → user hasn't touched it → safe to overwrite as an update.
170
+ // - else: user-edited file → conflict unless --force.
171
+ if (!isUpdate && !opts.force) {
172
+ conflicts.push({ path: relPath, component: c.name })
173
+ continue
174
+ }
175
+ if (!opts.dryRun) {
176
+ await mkdir(dirname(abs), { recursive: true })
177
+ await writeFile(abs, rewritten)
178
+ }
179
+ written.push({ path: relPath, component: c.name, reason: "updated" })
180
+ lockedFiles.push({ path: relPath, sha256: newHash })
181
+ continue
182
+ }
183
+
184
+ // Net-new file.
185
+ if (!opts.dryRun) {
186
+ await mkdir(dirname(abs), { recursive: true })
187
+ await writeFile(abs, rewritten)
188
+ }
189
+ written.push({ path: relPath, component: c.name, reason: "new" })
190
+ lockedFiles.push({ path: relPath, sha256: newHash })
191
+ }
192
+
193
+ // Only update the lock entry if no conflicts blocked installs for this component.
194
+ const componentConflicts = conflicts.filter((x) => x.component === c.name)
195
+ if (componentConflicts.length === 0) {
196
+ const entry: LockedComponent = {
197
+ version: opts.version ?? c.version,
198
+ installedAt: new Date().toISOString(),
199
+ alias: opts.config.alias,
200
+ files: lockedFiles.sort((a, b) => a.path.localeCompare(b.path)),
201
+ }
202
+ componentsByName[c.name] = entry
203
+ }
204
+ }
205
+
206
+ const lock: HyperLock = {
207
+ schema: 1,
208
+ registryUrl: opts.config.registryUrl,
209
+ components: componentsByName,
210
+ }
211
+
212
+ // Collect env vars across the install. First component to declare a key
213
+ // wins; later components see it as "preserved" (idempotent re-runs).
214
+ // Components with conflicts are skipped so a half-applied install doesn't
215
+ // leak secrets.
216
+ const conflictedNames = new Set(conflicts.map((x) => x.component))
217
+ const envVarsToAdd: Record<string, string> = {}
218
+ for (const c of components) {
219
+ if (!c.envVars || conflictedNames.has(c.name)) continue
220
+ for (const [k, v] of Object.entries(c.envVars)) {
221
+ if (!(k in envVarsToAdd)) envVarsToAdd[k] = v
222
+ }
223
+ }
224
+ const envPath = ".env.local"
225
+ const envSummary = await applyEnvVars(opts.cwd, envPath, envVarsToAdd, opts.dryRun)
226
+
227
+ return {
228
+ written,
229
+ unchanged,
230
+ conflicts,
231
+ components,
232
+ lock,
233
+ peerDeps,
234
+ optionalPeerDeps,
235
+ warnings,
236
+ envVars: { path: envPath, ...envSummary },
237
+ }
238
+ }
239
+
240
+ async function applyEnvVars(
241
+ cwd: string,
242
+ relPath: string,
243
+ vars: Readonly<Record<string, string>>,
244
+ dryRun: boolean,
245
+ ): Promise<{ added: readonly string[]; preserved: readonly string[] }> {
246
+ if (Object.keys(vars).length === 0) return { added: [], preserved: [] }
247
+ const abs = resolve(cwd, relPath)
248
+ const existing = (await readIfExists(abs)) ?? ""
249
+ const merge = mergeEnvFile(existing, vars)
250
+ if (!dryRun && merge.added.length > 0) {
251
+ await mkdir(dirname(abs), { recursive: true })
252
+ await writeFile(abs, merge.merged)
253
+ }
254
+ return { added: merge.added, preserved: merge.preserved }
255
+ }
256
+
257
+ /** Read the actual on-disk version of a registry file (for `hyper diff`). */
258
+ export async function readLocalFile(
259
+ cwd: string,
260
+ config: HyperConfig,
261
+ componentName: string,
262
+ manifestFile: RegistryFile,
263
+ ): Promise<string | null> {
264
+ const relPath = resolveTarget(manifestFile, componentName, config.baseDir).relPath
265
+ const abs = resolve(cwd, relPath)
266
+ if (!(await pathExists(abs))) return null
267
+ return await readFile(abs, "utf8")
268
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Registry client.
3
+ *
4
+ * Resolution order:
5
+ * 1. fetch `<registryUrl>/r/<name>(@<version>)?.json`
6
+ * 2. on network failure or non-200, fall back to the bundled snapshot
7
+ * (regenerated by the root `postinstall` and the CLI's `prepack` hook)
8
+ * 3. on no match, throw
9
+ *
10
+ * The bundled snapshot keeps `hyper add` working in air-gapped CI and during
11
+ * registry outages. It's meant as a safety net, not a primary path — production
12
+ * installs always hit the network when reachable.
13
+ *
14
+ * The snapshot is loaded lazily because it's gitignored: in a fresh clone
15
+ * before `postinstall` runs, the file may not exist. That's fine — we just
16
+ * skip the fallback in that case.
17
+ */
18
+
19
+ import type { RegistryComponent, RegistryIndex } from "./types.ts"
20
+
21
+ interface Snapshot {
22
+ readonly index: RegistryIndex
23
+ readonly components: Readonly<Record<string, RegistryComponent>>
24
+ }
25
+
26
+ let snapshotCache: Snapshot | null | undefined
27
+
28
+ async function loadSnapshot(): Promise<Snapshot | null> {
29
+ if (snapshotCache !== undefined) return snapshotCache
30
+ try {
31
+ const mod = (await import("./snapshot.ts")) as {
32
+ SNAPSHOT_INDEX: RegistryIndex
33
+ SNAPSHOT_COMPONENTS: Readonly<Record<string, RegistryComponent>>
34
+ }
35
+ snapshotCache = { index: mod.SNAPSHOT_INDEX, components: mod.SNAPSHOT_COMPONENTS }
36
+ } catch {
37
+ snapshotCache = null
38
+ }
39
+ return snapshotCache
40
+ }
41
+
42
+ export interface RegistryClient {
43
+ readonly url: string
44
+ getIndex(): Promise<RegistryIndex>
45
+ getComponent(name: string, version?: string): Promise<RegistryComponent>
46
+ listComponents(): Promise<
47
+ readonly { name: string; version: string; title?: string; description: string }[]
48
+ >
49
+ }
50
+
51
+ export interface RegistryClientOptions {
52
+ readonly url?: string
53
+ /** Force snapshot-only mode (offline, deterministic CI). */
54
+ readonly offline?: boolean
55
+ /** Throw on snapshot fallback (used by tests + strict CI). */
56
+ readonly noFallback?: boolean
57
+ /** Override fetch for tests. */
58
+ readonly fetch?: typeof fetch
59
+ }
60
+
61
+ const DEFAULT_URL = "https://hyperjs.ai"
62
+
63
+ export class RegistryError extends Error {
64
+ override readonly name = "RegistryError"
65
+ constructor(
66
+ message: string,
67
+ readonly status?: number,
68
+ ) {
69
+ super(message)
70
+ }
71
+ }
72
+
73
+ export function createRegistryClient(opts: RegistryClientOptions = {}): RegistryClient {
74
+ const url = (opts.url ?? DEFAULT_URL).replace(/\/+$/, "")
75
+ const offline = opts.offline === true
76
+ const noFallback = opts.noFallback === true
77
+ const fetchFn = opts.fetch ?? globalThis.fetch
78
+
79
+ const tryFetch = async <T>(path: string): Promise<T | null> => {
80
+ if (offline) return null
81
+ try {
82
+ const res = await fetchFn(`${url}${path}`, {
83
+ headers: { accept: "application/json" },
84
+ })
85
+ if (!res.ok) return null
86
+ return (await res.json()) as T
87
+ } catch {
88
+ return null
89
+ }
90
+ }
91
+
92
+ return {
93
+ url,
94
+ async getIndex() {
95
+ const remote = await tryFetch<RegistryIndex>("/r/index.json")
96
+ if (remote) return remote
97
+ if (noFallback) throw new RegistryError(`failed to fetch ${url}/r/index.json`)
98
+ const snap = await loadSnapshot()
99
+ if (!snap) throw new RegistryError("registry unreachable and no offline snapshot available")
100
+ return snap.index
101
+ },
102
+ async getComponent(name, version) {
103
+ const slug = version ? `${name}@${version}` : name
104
+ const remote = await tryFetch<RegistryComponent>(`/r/${slug}.json`)
105
+ if (remote) return remote
106
+ const snap = await loadSnapshot()
107
+ const cached = snap?.components[name]
108
+ if (!cached) throw new RegistryError(`component not found: ${name}`)
109
+ if (version && cached.version !== version) {
110
+ throw new RegistryError(
111
+ `version mismatch (snapshot has ${cached.version}, requested ${version})`,
112
+ )
113
+ }
114
+ if (noFallback) throw new RegistryError(`failed to fetch ${url}/r/${slug}.json`)
115
+ return cached
116
+ },
117
+ async listComponents() {
118
+ const idx = await this.getIndex()
119
+ return idx.components.map((c) => ({
120
+ name: c.name,
121
+ version: c.version,
122
+ description: c.description,
123
+ ...(c.title !== undefined && { title: c.title }),
124
+ }))
125
+ },
126
+ }
127
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * `.env.local` merger for `hyper add`.
3
+ *
4
+ * When a component declares `envVars`, the CLI appends entries to the
5
+ * project's `.env.local`, leaving any existing keys untouched. Re-running
6
+ * `hyper add` is therefore idempotent: secrets are generated exactly once.
7
+ *
8
+ * Two kinds of `${...}` interpolation are resolved LOCALLY (so secrets
9
+ * never leave the user's machine):
10
+ *
11
+ * ${random:hex:N} -> N random bytes encoded as 2N hex chars
12
+ * ${random:base64:N} -> N random bytes encoded as base64
13
+ *
14
+ * Anything else (including unknown `${...}` forms) is written through
15
+ * verbatim. Users can fill those in by hand.
16
+ */
17
+
18
+ import { randomBytes } from "node:crypto"
19
+
20
+ export interface EnvMergeResult {
21
+ /** New file contents (unchanged + appended block). */
22
+ readonly merged: string
23
+ /** Keys we added in this run. */
24
+ readonly added: readonly string[]
25
+ /** Keys that were already present and left as-is. */
26
+ readonly preserved: readonly string[]
27
+ }
28
+
29
+ /**
30
+ * Merge a flat record of `KEY -> value-template` into an existing `.env`
31
+ * file body. Existing keys are preserved verbatim; new keys are appended
32
+ * with their `${random:...}` placeholders resolved.
33
+ *
34
+ * Pure function: no I/O, no clock, no env access. Callers handle the
35
+ * file read + write + dry-run gating.
36
+ */
37
+ export function mergeEnvFile(
38
+ existing: string,
39
+ vars: Readonly<Record<string, string>>,
40
+ ): EnvMergeResult {
41
+ const known = parseEnvKeys(existing)
42
+ const added: string[] = []
43
+ const preserved: string[] = []
44
+ const newLines: string[] = []
45
+
46
+ // Stable order: alphabetical by key, so re-runs produce reproducible diffs.
47
+ const keys = Object.keys(vars).sort()
48
+ for (const key of keys) {
49
+ if (known.has(key)) {
50
+ preserved.push(key)
51
+ continue
52
+ }
53
+ const template = vars[key] ?? ""
54
+ const value = resolveInterpolations(template)
55
+ newLines.push(`${key}=${formatValue(value)}`)
56
+ added.push(key)
57
+ }
58
+
59
+ if (newLines.length === 0) {
60
+ return { merged: existing, added, preserved }
61
+ }
62
+
63
+ // Existing body keeps its content verbatim. We strip *all* trailing
64
+ // newlines, then separate with exactly one blank line before the new
65
+ // block. Pre-empty files just get the block itself.
66
+ const trimmed = existing.replace(/(?:\r?\n)+$/g, "")
67
+ const header = "# Added by `hyper add`"
68
+ const block = `${header}\n${newLines.join("\n")}\n`
69
+ const merged = trimmed === "" ? block : `${trimmed}\n\n${block}`
70
+ return { merged, added, preserved }
71
+ }
72
+
73
+ /**
74
+ * Parse just the keys defined in an `.env` file body. Tolerates blank lines,
75
+ * `# comments`, `export FOO=…` prefixes, and quoted values. We do NOT need
76
+ * to evaluate the values — only know which keys are taken.
77
+ */
78
+ function parseEnvKeys(body: string): Set<string> {
79
+ const keys = new Set<string>()
80
+ for (const rawLine of body.split(/\r?\n/)) {
81
+ const line = stripComment(rawLine).trim()
82
+ if (line === "") continue
83
+ const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line)
84
+ if (m?.[1]) keys.add(m[1])
85
+ }
86
+ return keys
87
+ }
88
+
89
+ function stripComment(line: string): string {
90
+ // A `#` only starts a comment when not inside quotes. Cheap heuristic:
91
+ // scan once, flipping a quote state. Good enough for the limited set of
92
+ // shapes a `.env.local` ever contains.
93
+ let inSingle = false
94
+ let inDouble = false
95
+ for (let i = 0; i < line.length; i++) {
96
+ const ch = line[i]
97
+ if (ch === "\\") {
98
+ i++
99
+ continue
100
+ }
101
+ if (!inDouble && ch === "'") inSingle = !inSingle
102
+ else if (!inSingle && ch === '"') inDouble = !inDouble
103
+ else if (!inSingle && !inDouble && ch === "#") return line.slice(0, i)
104
+ }
105
+ return line
106
+ }
107
+
108
+ const RANDOM_HEX_RE = /^\$\{random:hex:(\d+)\}$/
109
+ const RANDOM_B64_RE = /^\$\{random:base64:(\d+)\}$/
110
+
111
+ function resolveInterpolations(template: string): string {
112
+ // Whole-string interpolation only. Mid-string `${...}` is rare in
113
+ // .env.local conventions and ambiguous to escape; keep the contract small.
114
+ const hex = RANDOM_HEX_RE.exec(template)
115
+ if (hex?.[1]) {
116
+ const n = Math.max(1, Math.min(1024, Number.parseInt(hex[1], 10)))
117
+ return randomBytes(n).toString("hex")
118
+ }
119
+ const b64 = RANDOM_B64_RE.exec(template)
120
+ if (b64?.[1]) {
121
+ const n = Math.max(1, Math.min(1024, Number.parseInt(b64[1], 10)))
122
+ return randomBytes(n).toString("base64")
123
+ }
124
+ return template
125
+ }
126
+
127
+ /**
128
+ * Quote values that need it. Bare values (no whitespace, no `#`, no `$`,
129
+ * etc.) stay unquoted to match the dominant `.env` convention.
130
+ */
131
+ function formatValue(value: string): string {
132
+ if (value === "") return ""
133
+ if (/^[A-Za-z0-9_./:+\-=]+$/.test(value)) return value
134
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`
135
+ }
@@ -0,0 +1,18 @@
1
+ /** Public surface for the registry client + applier. */
2
+
3
+ export {
4
+ applyComponents,
5
+ readLocalFile,
6
+ resolveDeps,
7
+ } from "./apply.ts"
8
+ export type { ApplyOptions, ApplyOutcome } from "./apply.ts"
9
+ export { createRegistryClient, RegistryError } from "./client.ts"
10
+ export type { RegistryClient, RegistryClientOptions } from "./client.ts"
11
+ export { resolveTarget, rewriteFile } from "./rewrite.ts"
12
+ export { SNAPSHOT_COMPONENTS, SNAPSHOT_INDEX } from "./snapshot.ts"
13
+ export type {
14
+ RegistryComponent,
15
+ RegistryFile,
16
+ RegistryIndex,
17
+ RegistryIndexEntry,
18
+ } from "./types.ts"