@lovrozagar/honey 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 (131) hide show
  1. package/README.md +51 -0
  2. package/docs/api-ref/go.md +399 -0
  3. package/docs/api-ref/python.md +399 -0
  4. package/docs/api-ref/rust.md +399 -0
  5. package/docs/api-ref/typescript.md +399 -0
  6. package/docs/invalidate-spec.md +303 -0
  7. package/docs/sdk.md +722 -0
  8. package/package.json +120 -0
  9. package/src/accepts.ts +78 -0
  10. package/src/body-limit.ts +100 -0
  11. package/src/build/index.ts +174 -0
  12. package/src/cli-go/auth.go +15 -0
  13. package/src/cli-go/config.go +165 -0
  14. package/src/cli-go/errors.go +57 -0
  15. package/src/cli-go/multipart.go +55 -0
  16. package/src/cli-go/output.go +109 -0
  17. package/src/cli-go/root.go +30 -0
  18. package/src/cli-go/stream.go +88 -0
  19. package/src/cli-go/version.go +5 -0
  20. package/src/cli.ts +204 -0
  21. package/src/client/error.ts +27 -0
  22. package/src/client/http.ts +502 -0
  23. package/src/client/index.ts +127 -0
  24. package/src/client/sdk.ts +265 -0
  25. package/src/client/sse.ts +102 -0
  26. package/src/client/types.ts +211 -0
  27. package/src/client/ws.ts +115 -0
  28. package/src/client-go/errors.go +207 -0
  29. package/src/client-go/invalidation.go +373 -0
  30. package/src/client-go/realtime.go +317 -0
  31. package/src/client-go/result.go +20 -0
  32. package/src/client-go/runtime.go +413 -0
  33. package/src/client-go/sse.go +149 -0
  34. package/src/client-go/transport.go +452 -0
  35. package/src/client-go/ws.go +104 -0
  36. package/src/client-mcp/README.md +10 -0
  37. package/src/client-mcp/server.ts +94 -0
  38. package/src/client-mcp/types.ts +19 -0
  39. package/src/client-python/_errors.py +115 -0
  40. package/src/client-python/_invalidation.py +578 -0
  41. package/src/client-python/_realtime.py +156 -0
  42. package/src/client-python/_runtime.py +469 -0
  43. package/src/client-python/_sse.py +99 -0
  44. package/src/client-python/_transport.py +335 -0
  45. package/src/client-python/_ws.py +131 -0
  46. package/src/client-python/py.typed +0 -0
  47. package/src/client-rust/errors.rs +200 -0
  48. package/src/client-rust/invalidation.rs +441 -0
  49. package/src/client-rust/invalidation_sync.rs +306 -0
  50. package/src/client-rust/realtime.rs +695 -0
  51. package/src/client-rust/result.rs +22 -0
  52. package/src/client-rust/runtime.rs +547 -0
  53. package/src/client-rust/runtime_sync.rs +452 -0
  54. package/src/client-rust/sse.rs +167 -0
  55. package/src/client-rust/ws.rs +143 -0
  56. package/src/cloudflare/realtime.ts +236 -0
  57. package/src/codegen-go-cli.ts +1180 -0
  58. package/src/codegen-go.ts +1010 -0
  59. package/src/codegen-hash.ts +8 -0
  60. package/src/codegen-ir.ts +517 -0
  61. package/src/codegen-loaders.ts +53 -0
  62. package/src/codegen-mcp.ts +364 -0
  63. package/src/codegen-python.ts +1028 -0
  64. package/src/codegen-rust.ts +1652 -0
  65. package/src/codegen-sanitize.ts +67 -0
  66. package/src/codegen-schema-naming.ts +146 -0
  67. package/src/codegen.ts +5015 -0
  68. package/src/context.ts +210 -0
  69. package/src/cookie-sign.ts +58 -0
  70. package/src/cookie.ts +89 -0
  71. package/src/cors.ts +96 -0
  72. package/src/crypto.ts +19 -0
  73. package/src/csrf.ts +54 -0
  74. package/src/curl-logger.ts +235 -0
  75. package/src/detect-runtime.ts +19 -0
  76. package/src/error.ts +52 -0
  77. package/src/errors.ts +87 -0
  78. package/src/etag.ts +74 -0
  79. package/src/feature-load.ts +23 -0
  80. package/src/go-type-emitter.ts +404 -0
  81. package/src/honey-response.ts +354 -0
  82. package/src/i18n-slot.ts +20 -0
  83. package/src/i18n.ts +234 -0
  84. package/src/index.ts +3843 -0
  85. package/src/init.ts +149 -0
  86. package/src/input.ts +8 -0
  87. package/src/ip-restrict.ts +192 -0
  88. package/src/logger.ts +149 -0
  89. package/src/middleware.ts +199 -0
  90. package/src/node-request.ts +240 -0
  91. package/src/node.ts +212 -0
  92. package/src/openapi/register.ts +19 -0
  93. package/src/openapi/scalar.ts +33 -0
  94. package/src/openapi/spec-factory.ts +46 -0
  95. package/src/openapi/spec.ts +31 -0
  96. package/src/openapi/swagger.ts +34 -0
  97. package/src/plugin.ts +771 -0
  98. package/src/powered-by.ts +17 -0
  99. package/src/pretty-json.ts +39 -0
  100. package/src/proxy.ts +155 -0
  101. package/src/python-type-emitter.ts +144 -0
  102. package/src/realtime/buffer.ts +101 -0
  103. package/src/realtime/bus.ts +93 -0
  104. package/src/realtime/longpoll.ts +148 -0
  105. package/src/realtime/protocol.ts +128 -0
  106. package/src/realtime/route.ts +104 -0
  107. package/src/realtime/runtime.ts +430 -0
  108. package/src/request-id.ts +23 -0
  109. package/src/request-to-curl.ts +34 -0
  110. package/src/response.ts +414 -0
  111. package/src/rust-type-emitter.ts +412 -0
  112. package/src/secure-headers.ts +69 -0
  113. package/src/serve-register.ts +8 -0
  114. package/src/serve-slot.ts +20 -0
  115. package/src/serve.ts +141 -0
  116. package/src/server-timing.ts +66 -0
  117. package/src/static.ts +60 -0
  118. package/src/telemetry/otel.ts +139 -0
  119. package/src/testing.ts +114 -0
  120. package/src/timeout.ts +36 -0
  121. package/src/tree.ts +464 -0
  122. package/src/ts-type-emitter.ts +106 -0
  123. package/src/type-emitter.ts +524 -0
  124. package/src/type-extractor.ts +749 -0
  125. package/src/types.ts +638 -0
  126. package/src/validation.ts +300 -0
  127. package/src/ws/bun.ts +89 -0
  128. package/src/ws/cloudflare.ts +133 -0
  129. package/src/ws/deno.ts +109 -0
  130. package/src/ws/node.ts +161 -0
  131. package/src/yaml.ts +80 -0
package/package.json ADDED
@@ -0,0 +1,120 @@
1
+ {
2
+ "name": "@lovrozagar/honey",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe API framework built on Web Standards. Builder-pattern DX, precompiled radix tree in production.",
5
+ "keywords": [
6
+ "api",
7
+ "framework",
8
+ "honey",
9
+ "radix-tree",
10
+ "router",
11
+ "type-safe",
12
+ "web-standards"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/lovrozagar/honey"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "bin": {
23
+ "honey": "./src/cli.ts"
24
+ },
25
+ "files": [
26
+ "docs",
27
+ "src"
28
+ ],
29
+ "type": "module",
30
+ "sideEffects": [
31
+ "./src/i18n.ts",
32
+ "./src/openapi/register.ts",
33
+ "./src/serve-register.ts"
34
+ ],
35
+ "exports": {
36
+ ".": "./src/index.ts",
37
+ "./accepts": "./src/accepts.ts",
38
+ "./body-limit": "./src/body-limit.ts",
39
+ "./build": "./src/build/index.ts",
40
+ "./cli": "./src/cli.ts",
41
+ "./client": "./src/client/index.ts",
42
+ "./client/sdk": "./src/client/sdk.ts",
43
+ "./codegen": "./src/codegen.ts",
44
+ "./codegen-go-cli": "./src/codegen-go-cli.ts",
45
+ "./codegen/extract": "./src/type-extractor.ts",
46
+ "./cookie": "./src/cookie.ts",
47
+ "./cookie-sign": "./src/cookie-sign.ts",
48
+ "./cors": "./src/cors.ts",
49
+ "./crypto": "./src/crypto.ts",
50
+ "./curl-logger": "./src/curl-logger.ts",
51
+ "./csrf": "./src/csrf.ts",
52
+ "./errors": "./src/errors.ts",
53
+ "./etag": "./src/etag.ts",
54
+ "./i18n": "./src/i18n.ts",
55
+ "./input": "./src/input.ts",
56
+ "./ip-restrict": "./src/ip-restrict.ts",
57
+ "./logger": "./src/logger.ts",
58
+ "./node": "./src/node.ts",
59
+ "./openapi": "./src/openapi/register.ts",
60
+ "./openapi/spec": "./src/openapi/spec.ts",
61
+ "./openapi/scalar": "./src/openapi/scalar.ts",
62
+ "./openapi/swagger": "./src/openapi/swagger.ts",
63
+ "./plugin": "./src/plugin.ts",
64
+ "./powered-by": "./src/powered-by.ts",
65
+ "./pretty-json": "./src/pretty-json.ts",
66
+ "./proxy": "./src/proxy.ts",
67
+ "./request-to-curl": "./src/request-to-curl.ts",
68
+ "./request-id": "./src/request-id.ts",
69
+ "./secure-headers": "./src/secure-headers.ts",
70
+ "./serve": "./src/serve-register.ts",
71
+ "./server-timing": "./src/server-timing.ts",
72
+ "./static": "./src/static.ts",
73
+ "./telemetry/otel": "./src/telemetry/otel.ts",
74
+ "./testing": "./src/testing.ts",
75
+ "./timeout": "./src/timeout.ts",
76
+ "./tree": "./src/tree.ts",
77
+ "./ws/bun": "./src/ws/bun.ts",
78
+ "./ws/cloudflare": "./src/ws/cloudflare.ts",
79
+ "./ws/deno": "./src/ws/deno.ts",
80
+ "./ws/node": "./src/ws/node.ts"
81
+ },
82
+ "scripts": {
83
+ "test": "vitest run",
84
+ "test:all": "vitest run --config vitest.all.config.ts",
85
+ "test:harness": "vitest run --config vitest.harness.config.ts",
86
+ "test:harness:rust": "vitest run --config vitest.harness.rust.config.ts",
87
+ "test:watch": "vitest",
88
+ "type:check": "tsc --noEmit -p tsconfig.src.json && tsc --noEmit -p tsconfig.types.json",
89
+ "type:check:tests": "NODE_OPTIONS='--max-old-space-size=4096' tsc --noEmit",
90
+ "type:check:types": "tsc --noEmit -p tsconfig.types.json"
91
+ },
92
+ "dependencies": {
93
+ "jiti": "2.7.0"
94
+ },
95
+ "devDependencies": {
96
+ "@honey/e2e-surface": "workspace:*",
97
+ "@honey/e2e-kitchen": "workspace:*",
98
+ "@modelcontextprotocol/sdk": "^1.29.0",
99
+ "@total-typescript/ts-reset": "catalog:",
100
+ "@types/node": "catalog:",
101
+ "@types/ws": "catalog:",
102
+ "arktype": "catalog:",
103
+ "effect": "catalog:",
104
+ "ts-morph": "catalog:",
105
+ "typescript": "catalog:",
106
+ "valibot": "catalog:",
107
+ "vite": "catalog:",
108
+ "vitest": "catalog:",
109
+ "yup": "catalog:",
110
+ "zod": "catalog:"
111
+ },
112
+ "peerDependencies": {
113
+ "ts-morph": ">=25.0.0"
114
+ },
115
+ "peerDependenciesMeta": {
116
+ "ts-morph": {
117
+ "optional": true
118
+ }
119
+ }
120
+ }
package/src/accepts.ts ADDED
@@ -0,0 +1,78 @@
1
+ type ParsedType = {
2
+ q: number
3
+ subtype: string
4
+ type: string
5
+ }
6
+
7
+ function parseAccept(header: string): ParsedType[] {
8
+ const types: ParsedType[] = []
9
+ for (const part of header.split(",")) {
10
+ const trimmed = part.trim()
11
+ if (trimmed.length === 0) continue
12
+
13
+ const segments = trimmed.split(";")
14
+ const mediaType = segments[0].trim()
15
+ const slash = mediaType.indexOf("/")
16
+ if (slash === -1) continue
17
+
18
+ let q = 1.0
19
+ for (let i = 1; i < segments.length; i++) {
20
+ const param = segments[i].trim()
21
+ if (param.startsWith("q=")) {
22
+ const parsed = Number.parseFloat(param.slice(2))
23
+ if (!Number.isNaN(parsed)) q = parsed
24
+ }
25
+ }
26
+
27
+ types.push({
28
+ q,
29
+ subtype: mediaType.slice(slash + 1).toLowerCase(),
30
+ type: mediaType.slice(0, slash).toLowerCase(),
31
+ })
32
+ }
33
+ return types.sort((a, b) => b.q - a.q)
34
+ }
35
+
36
+ function matches(accept: ParsedType, supported: string): boolean {
37
+ const slash = supported.indexOf("/")
38
+ if (slash === -1) return false
39
+ const sType = supported.slice(0, slash)
40
+ const sSub = supported.slice(slash + 1)
41
+
42
+ if (accept.type === "*" && accept.subtype === "*") return true
43
+ if (accept.type === sType && accept.subtype === "*") return true
44
+ return accept.type === sType && accept.subtype === sSub
45
+ }
46
+
47
+ /**
48
+ * Pick the best supported content type based on the request's Accept header.
49
+ * Returns null if no match found. Returns first supported type if no Accept header.
50
+ */
51
+ export function accepts(req: Request, supported: string[]): string | null {
52
+ const header = req.headers.get("accept")
53
+ if (header === null || header.length === 0) {
54
+ return supported[0] ?? null
55
+ }
56
+
57
+ const parsed = parseAccept(header)
58
+ if (parsed.length === 0) {
59
+ return supported[0] ?? null
60
+ }
61
+
62
+ /* for each supported type (server preference order), find best q match */
63
+ let bestMatch: string | null = null
64
+ let bestQ = -1
65
+
66
+ for (const s of supported) {
67
+ for (const accept of parsed) {
68
+ if (accept.q === 0) continue
69
+ if (matches(accept, s) && accept.q > bestQ) {
70
+ bestQ = accept.q
71
+ bestMatch = s
72
+ break
73
+ }
74
+ }
75
+ }
76
+
77
+ return bestMatch
78
+ }
@@ -0,0 +1,100 @@
1
+ import { HoneyError } from "./error.ts"
2
+ import type { MiddlewareFn } from "./middleware.ts"
3
+ import { EK, SK } from "./types.ts"
4
+
5
+ type BodyLimitOptions = {
6
+ /** Per content-type size limits (matched via startsWith against request Content-Type header) */
7
+ limits?: Record<string, number>
8
+ /** Default max body size in bytes — used when no content-type match or no limits map */
9
+ maxSize: number
10
+ /**
11
+ * Skip stream byte-counting when Content-Length header is present and within bounds.
12
+ * HTTP/1.1 and HTTP/2 framing guarantees body cannot exceed declared Content-Length
13
+ * regardless of TLS — the protocol enforces it at the transport level.
14
+ * When false, every request body is wrapped in a counting TransformStream even
15
+ * when Content-Length is present. Chunked requests (no Content-Length) are always
16
+ * stream-counted regardless of this setting.
17
+ * Default: false.
18
+ */
19
+ trustContentLength?: boolean
20
+ }
21
+
22
+ export type { BodyLimitOptions }
23
+
24
+ export function bodyLimit(opts: BodyLimitOptions): MiddlewareFn<{ req: Request }, {}> {
25
+ const { limits, maxSize, trustContentLength } = opts
26
+ const limitEntries = limits !== undefined ? Object.entries(limits) : null
27
+
28
+ const mw: MiddlewareFn<{ req: Request }, {}> = (ctx, next) => {
29
+ const req = ctx.req
30
+ const method = req.method
31
+ if (method === "GET" || method === "HEAD" || method === "OPTIONS" || method === "DELETE") {
32
+ return next()
33
+ }
34
+
35
+ /* resolve max size from content-type */
36
+ let resolvedMax = maxSize
37
+ if (limitEntries !== null) {
38
+ const ct = req.headers.get("content-type")
39
+ if (ct !== null) {
40
+ for (const [key, limit] of limitEntries) {
41
+ if (ct.startsWith(key)) {
42
+ resolvedMax = limit
43
+ break
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ /* fast path: Content-Length header present */
50
+ const contentLength = req.headers.get("content-length")
51
+ if (contentLength !== null) {
52
+ const len = Number.parseInt(contentLength, 10)
53
+ if (!Number.isNaN(len) && len > resolvedMax) {
54
+ throw new HoneyError({
55
+ errorKey: EK.content_too_large,
56
+ status: SK.content_too_large,
57
+ })
58
+ }
59
+ if (trustContentLength === true) return next()
60
+ }
61
+
62
+ /* slow path: stream-count when no Content-Length (chunked) or trustContentLength disabled */
63
+ if (req.body !== null) {
64
+ let totalBytes = 0
65
+ const limit = resolvedMax
66
+ const transform = new TransformStream<Uint8Array, Uint8Array>({
67
+ transform(chunk, controller) {
68
+ totalBytes += chunk.byteLength
69
+ if (totalBytes > limit) {
70
+ controller.error(
71
+ new HoneyError({
72
+ errorKey: EK.content_too_large,
73
+ status: SK.content_too_large,
74
+ }),
75
+ )
76
+ return
77
+ }
78
+ controller.enqueue(chunk)
79
+ },
80
+ })
81
+
82
+ const limited = req.body.pipeThrough(transform)
83
+ const replaceBody = (req as unknown as Record<symbol, unknown>)[Symbol.for("honey.replaceBody")]
84
+ if (typeof replaceBody === "function") {
85
+ ;(replaceBody as (stream: ReadableStream<Uint8Array>) => void).call(req, limited)
86
+ } else {
87
+ /* duplex: "half" required by Node/undici when body is a ReadableStream */
88
+ const newReq = new Request(req, {
89
+ body: limited,
90
+ duplex: "half",
91
+ } as RequestInit)
92
+ Object.defineProperty(ctx, "req", { configurable: true, value: newReq })
93
+ }
94
+ }
95
+
96
+ return next()
97
+ }
98
+
99
+ return mw
100
+ }
@@ -0,0 +1,174 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { builtinModules } from "node:module"
3
+ import { resolve } from "node:path"
4
+
5
+ /* ---- Public types ---- */
6
+
7
+ export type HoneyBuildConfig = {
8
+ external?: string[]
9
+ minify?: boolean
10
+ outDir?: string
11
+ port?: number
12
+ target: "bun" | "cloudflare" | "deno" | "node"
13
+ }
14
+
15
+ /* ---- Internal types ---- */
16
+
17
+ type ResolvedBuildConfig = {
18
+ entry: string
19
+ export: string
20
+ port: number
21
+ }
22
+
23
+ type BuildAdapterDef = {
24
+ entry(config: ResolvedBuildConfig): string
25
+ ssrTarget: "node" | "webworker"
26
+ }
27
+
28
+ /* ---- Entry helpers ---- */
29
+
30
+ function featurePrelude(appSource: string): string {
31
+ const lines: string[] = []
32
+ if (/\.openapi\s*\(|\.manifest\s*\(/.test(appSource)) {
33
+ lines.push('import { enableOpenApi } from "@lovrozagar/honey/openapi"', "enableOpenApi()")
34
+ }
35
+ if (/\.errorI18n\s*\(/.test(appSource)) {
36
+ lines.push('import { enableI18n } from "@lovrozagar/honey/i18n"', "enableI18n()")
37
+ }
38
+ if (/(?<!Bun)(?<!Deno)\.serve\s*\(/.test(appSource)) {
39
+ lines.push('import { enableServe } from "@lovrozagar/honey/serve"', "enableServe()")
40
+ }
41
+ return lines.length > 0 ? `${lines.join("\n")}\n` : ""
42
+ }
43
+
44
+ function importApp(entry: string, exportName: string): string {
45
+ if (exportName === "default") return `import app from "./${entry}"`
46
+ return `import { ${exportName} as app } from "./${entry}"`
47
+ }
48
+
49
+ /* ---- Adapter definitions ---- */
50
+
51
+ const adapters: Record<HoneyBuildConfig["target"], BuildAdapterDef> = {
52
+ bun: {
53
+ entry(config) {
54
+ return [
55
+ importApp(config.entry, config.export),
56
+ "",
57
+ `const port = Number(process.env.PORT ?? ${config.port})`,
58
+ "Bun.serve({",
59
+ " fetch: (req, server) => app.fetch(req, { server }),",
60
+ ' hostname: "0.0.0.0",',
61
+ " port,",
62
+ "})",
63
+ ].join("\n")
64
+ },
65
+ ssrTarget: "node",
66
+ },
67
+ cloudflare: {
68
+ entry(config) {
69
+ return [
70
+ importApp(config.entry, config.export),
71
+ "",
72
+ "export default {",
73
+ " fetch: (req, env, ctx) => app.fetch(req, env, ctx),",
74
+ "}",
75
+ ].join("\n")
76
+ },
77
+ ssrTarget: "webworker",
78
+ },
79
+ deno: {
80
+ entry(config) {
81
+ return [
82
+ importApp(config.entry, config.export),
83
+ "",
84
+ `const port = Number(Deno.env.get("PORT") ?? "${config.port}")`,
85
+ 'Deno.serve({ hostname: "0.0.0.0", port }, (req) => app.fetch(req, {}))',
86
+ ].join("\n")
87
+ },
88
+ ssrTarget: "webworker",
89
+ },
90
+ node: {
91
+ entry(config) {
92
+ return [
93
+ 'import { serve } from "@lovrozagar/honey/node"',
94
+ importApp(config.entry, config.export),
95
+ "",
96
+ `const port = Number(process.env.PORT ?? ${config.port})`,
97
+ 'serve(app, { env: process.env, hostname: "0.0.0.0", port })',
98
+ ].join("\n")
99
+ },
100
+ ssrTarget: "node",
101
+ },
102
+ }
103
+
104
+ /* ---- Virtual module constants ---- */
105
+
106
+ const VIRTUAL_BUILD_ENTRY = "virtual:honey-build-entry"
107
+ const RESOLVED_BUILD_ENTRY = `\0${VIRTUAL_BUILD_ENTRY}`
108
+
109
+ /* ---- Build plugin factory ---- */
110
+
111
+ export function createBuildPlugin(
112
+ buildConfig: HoneyBuildConfig,
113
+ shared: { entry: string; export: string },
114
+ ) {
115
+ const adapter = adapters[buildConfig.target]
116
+ const resolvedConfig: ResolvedBuildConfig = {
117
+ entry: shared.entry,
118
+ export: shared.export,
119
+ port: buildConfig.port ?? 3000,
120
+ }
121
+
122
+ let root = ""
123
+
124
+ return {
125
+ apply: "build" as const,
126
+
127
+ config() {
128
+ return {
129
+ build: {
130
+ emptyOutDir: false,
131
+ minify: buildConfig.minify ?? true,
132
+ outDir: buildConfig.outDir ?? "./dist",
133
+ rolldownOptions: {
134
+ external: [
135
+ ...builtinModules,
136
+ ...builtinModules.map((m) => `node:${m}`),
137
+ ...(buildConfig.external ?? []),
138
+ ],
139
+ input: VIRTUAL_BUILD_ENTRY,
140
+ output: { entryFileNames: "index.js" },
141
+ },
142
+ ssr: true,
143
+ },
144
+ ssr: {
145
+ noExternal: true,
146
+ target: adapter.ssrTarget,
147
+ },
148
+ }
149
+ },
150
+
151
+ configResolved(cfg: { root: string }) {
152
+ root = cfg.root
153
+ },
154
+
155
+ load(id: string): { code: string; moduleType: string } | undefined {
156
+ if (id === RESOLVED_BUILD_ENTRY) {
157
+ const appPath = resolve(root || ".", shared.entry)
158
+ const appSource = existsSync(appPath) ? readFileSync(appPath, "utf-8") : ""
159
+ return {
160
+ code: `${featurePrelude(appSource)}${adapter.entry(resolvedConfig)}`,
161
+ moduleType: "js",
162
+ }
163
+ }
164
+ return undefined
165
+ },
166
+
167
+ name: "honey:build",
168
+
169
+ resolveId(id: string): string | undefined {
170
+ if (id === VIRTUAL_BUILD_ENTRY) return RESOLVED_BUILD_ENTRY
171
+ return undefined
172
+ },
173
+ }
174
+ }
@@ -0,0 +1,15 @@
1
+ // Code generated by honey. DO NOT EDIT.
2
+ package cli
3
+
4
+ import "errors"
5
+
6
+ // ErrMissingAPIKey is returned when no API key can be found via flags, env, or config file.
7
+ var ErrMissingAPIKey = errors.New("api key required: set --api-key, env var, or config file")
8
+
9
+ // ResolveAPIKey returns the resolved API key or ErrMissingAPIKey if none is available.
10
+ func ResolveAPIKey(cfg Config) (string, error) {
11
+ if cfg.APIKey != "" {
12
+ return cfg.APIKey, nil
13
+ }
14
+ return "", ErrMissingAPIKey
15
+ }
@@ -0,0 +1,165 @@
1
+ // Code generated by honey. DO NOT EDIT.
2
+ package cli
3
+
4
+ import (
5
+ "fmt"
6
+ "io"
7
+ "os"
8
+ "path/filepath"
9
+
10
+ "github.com/pelletier/go-toml/v2"
11
+ "github.com/spf13/pflag"
12
+ )
13
+
14
+ // Config holds resolved runtime values for the CLI binary.
15
+ //
16
+ // Precedence (high → low) — see LoadConfig below:
17
+ //
18
+ // 1. --api-key / --base-url / --output / … flag set on the command line.
19
+ // 2. Env var: <ENV_PREFIX>_API_KEY / _BASE_URL / _OUTPUT / …
20
+ // 3. Config file from --config, $<ENV_PREFIX>_CONFIG, $XDG_CONFIG_HOME/<name>/config.toml,
21
+ // $HOME/.config/<name>/config.toml, os.UserConfigDir()/<name>/config.toml.
22
+ // 4. Built-in defaults.
23
+ // 5. (missing api-key → ErrMissingAPIKey, mapped to exit 4).
24
+ type Config struct {
25
+ APIKey string
26
+ BaseURL string
27
+ Output string
28
+ Timeout string
29
+ Verbose bool
30
+ ConfigPath string
31
+ }
32
+
33
+ type tomlConfig struct {
34
+ APIKey string `toml:"api_key"`
35
+ BaseURL string `toml:"base_url"`
36
+ Output string `toml:"output"`
37
+ Timeout string `toml:"timeout"`
38
+ }
39
+
40
+ // LoadConfig merges flag values, env vars, and a TOML config file into a Config.
41
+ //
42
+ // The resolution order matches the Config doc-comment precedence list.
43
+ func LoadConfig(flags *pflag.FlagSet, envPrefix, configName string) (Config, error) {
44
+ var cfg Config
45
+
46
+ cfgPath := ""
47
+ if flags.Changed("config") {
48
+ v, _ := flags.GetString("config")
49
+ cfgPath = v
50
+ } else if v := os.Getenv(envPrefix + "_CONFIG"); v != "" {
51
+ cfgPath = v
52
+ } else {
53
+ cfgPath = defaultConfigPath(configName)
54
+ }
55
+
56
+ var fromFile tomlConfig
57
+ if cfgPath != "" {
58
+ if data, err := os.ReadFile(cfgPath); err == nil {
59
+ if err := toml.Unmarshal(data, &fromFile); err != nil {
60
+ return cfg, fmt.Errorf("parse config %s: %w", cfgPath, err)
61
+ }
62
+ }
63
+ }
64
+ cfg.ConfigPath = cfgPath
65
+
66
+ cfg.APIKey = fromFile.APIKey
67
+ if v := os.Getenv(envPrefix + "_API_KEY"); v != "" {
68
+ cfg.APIKey = v
69
+ }
70
+ if flags.Changed("api-key") {
71
+ v, _ := flags.GetString("api-key")
72
+ cfg.APIKey = v
73
+ }
74
+
75
+ cfg.BaseURL = fromFile.BaseURL
76
+ if v := os.Getenv(envPrefix + "_BASE_URL"); v != "" {
77
+ cfg.BaseURL = v
78
+ }
79
+ if flags.Changed("base-url") {
80
+ v, _ := flags.GetString("base-url")
81
+ cfg.BaseURL = v
82
+ }
83
+
84
+ cfg.Output = fromFile.Output
85
+ if v := os.Getenv(envPrefix + "_OUTPUT"); v != "" {
86
+ cfg.Output = v
87
+ }
88
+ if flags.Changed("output") {
89
+ v, _ := flags.GetString("output")
90
+ cfg.Output = v
91
+ }
92
+ if cfg.Output == "" {
93
+ cfg.Output = "json"
94
+ }
95
+
96
+ cfg.Timeout = fromFile.Timeout
97
+ if v := os.Getenv(envPrefix + "_TIMEOUT"); v != "" {
98
+ cfg.Timeout = v
99
+ }
100
+ if flags.Changed("timeout") {
101
+ v, _ := flags.GetString("timeout")
102
+ cfg.Timeout = v
103
+ }
104
+ if cfg.Timeout == "" {
105
+ cfg.Timeout = "30s"
106
+ }
107
+
108
+ if flags.Changed("verbose") {
109
+ v, _ := flags.GetBool("verbose")
110
+ cfg.Verbose = v
111
+ }
112
+
113
+ return cfg, nil
114
+ }
115
+
116
+ func defaultConfigPath(configName string) string {
117
+ if configName == "" {
118
+ return ""
119
+ }
120
+ if xdg := os.Getenv("XDG_CONFIG_HOME"); xdg != "" {
121
+ return filepath.Join(xdg, configName, "config.toml")
122
+ }
123
+ if home := os.Getenv("HOME"); home != "" {
124
+ return filepath.Join(home, ".config", configName, "config.toml")
125
+ }
126
+ if dir, err := os.UserConfigDir(); err == nil {
127
+ return filepath.Join(dir, configName, "config.toml")
128
+ }
129
+ return ""
130
+ }
131
+
132
+ // readDataFlag resolves the value of a --data flag into raw bytes.
133
+ //
134
+ // Rules:
135
+ // - "@path" → reads file at path.
136
+ // - "-" → reads os.Stdin.
137
+ // - "" → returns nil, nil (caller decides whether required).
138
+ // - anything else → treats val as literal JSON.
139
+ //
140
+ // Exported as ReadDataFlag for use from cmd/*.go.
141
+ func ReadDataFlag(val string) ([]byte, error) {
142
+ return readDataFlag(val)
143
+ }
144
+
145
+ func readDataFlag(val string) ([]byte, error) {
146
+ if val == "" {
147
+ return nil, nil
148
+ }
149
+ if val == "-" {
150
+ data, err := io.ReadAll(os.Stdin)
151
+ if err != nil {
152
+ return nil, fmt.Errorf("read stdin: %w", err)
153
+ }
154
+ return data, nil
155
+ }
156
+ if val[0] == '@' {
157
+ path := val[1:]
158
+ data, err := os.ReadFile(path)
159
+ if err != nil {
160
+ return nil, fmt.Errorf("read data file %s: %w", path, err)
161
+ }
162
+ return data, nil
163
+ }
164
+ return []byte(val), nil
165
+ }