galbe 0.14.0 → 0.15.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.
@@ -3,12 +3,12 @@ import { $ } from 'bun'
3
3
  import { Command, Option } from 'commander'
4
4
  import { resolve, relative, dirname } from 'path'
5
5
  import { tmpdir } from 'os'
6
- import { mkdir, rm, exists } from 'fs/promises'
6
+ import { mkdir, rm } from 'fs/promises'
7
7
 
8
8
  import { CWD, fmtVal, silentExec } from '../util'
9
9
  import { Galbe } from '../../src'
10
10
  import { defineRoutes, GalbeProxy } from '../../src/routes'
11
- import { BuildConfig } from 'bun'
11
+ import type { BuildConfig } from 'bun'
12
12
  import { cpSync, existsSync } from 'fs'
13
13
  import { softMerge } from '../../src/util'
14
14
 
@@ -20,7 +20,7 @@ const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string, ou
20
20
  if (existsSync(`${indexDir}/galbe.config.ts`)) configPath = `${indexDir}/galbe.config.ts`
21
21
  else if (existsSync(`${indexDir}/galbe.config.js`)) configPath = `${indexDir}/galbe.config.js`
22
22
 
23
- const routes = new Map<string, { filepath: string, static?: { path: string, root: string } }>()
23
+ const routes = new Map<string, { filepath: string; static?: { path: string; root: string } }>()
24
24
  let errors: any[] = []
25
25
  // Create GalbeProxy here
26
26
  // use it to define routes
@@ -45,8 +45,10 @@ const createBuildIndex = async (indexPath: string, g: Galbe, buildId: string, ou
45
45
 
46
46
  let buildIndex =
47
47
  `import galbe from '${relative(buildPath, indexPath)}';\n` +
48
- (configPath ? `import config from '${relative(buildPath, configPath)}';\n` : '') +
49
- (configPath ? `import {softMerge} from '${relative(buildPath, `${indexDir}/node_modules/galbe/src/util`)}';\n` : '') +
48
+ (configPath ? `import config from '${relative(buildPath, configPath)}';\n` : '') +
49
+ (configPath
50
+ ? `import {softMerge} from '${relative(buildPath, `${indexDir}/node_modules/galbe/src/util`)}';\n`
51
+ : '') +
50
52
  (configPath ? `let conf = galbe.config;\ngalbe.config = softMerge(config, conf)\n` : '') +
51
53
  `${[...routes.values()].map((r, idx) => `import _${idx} from '${relative(buildPath, r.filepath)}'`).join(';\n')}\n` +
52
54
  `Bun.env.BUN_ENV = 'production';\n` +
@@ -86,7 +88,7 @@ export default (cmd: Command) => {
86
88
 
87
89
  const bunfig = config ? (await import(resolve(CWD, config)))?.default || {} : {}
88
90
 
89
- if(await exists(outPath)) await rm(outPath, { recursive: true })
91
+ if (existsSync(outPath)) await rm(outPath, { recursive: true })
90
92
 
91
93
  let error = null
92
94
  Bun.write(Bun.stdout, '📦 \x1b[1;30mBuilding \x1b[36mGalbe\x1b[0m\x1b[1;30m app\x1b[0m')
@@ -110,7 +112,7 @@ export default (cmd: Command) => {
110
112
  buildIndex = await createBuildIndex(index, g, buildID, outPath)
111
113
  } catch (errors) {
112
114
  console.log(`\nerror: build errors`)
113
- for (let error of errors) console.log(error)
115
+ for (let error of errors as any[]) console.log(error)
114
116
  return process.exit(1)
115
117
  }
116
118
  if (!buildIndex) {
@@ -134,7 +136,7 @@ export default (cmd: Command) => {
134
136
  console.log(...bo.logs)
135
137
  }
136
138
  if (compile) {
137
- await $`bun build --compile ${resolve(CWD, out, 'index.js')} --outfile ${outPath}/bin`
139
+ await $`bun build --compile --minify --sourcemap --bytecode ${resolve(CWD, out, 'index.js')} --outfile ${outPath}/bin`
138
140
  }
139
141
 
140
142
  await rm(dirname(buildIndex), { recursive: true })
@@ -1,9 +1,9 @@
1
1
  import { Command, Option } from 'commander'
2
2
  import { resolve } from 'path'
3
3
  import { CWD, fmtList, instanciateRoutes, silentExec, abbreviateVar } from '../../../util'
4
- import { $T, Galbe, GalbeCLICommand, GalbeCLIOptions } from '../../../../src'
4
+ import { $T, Galbe, type GalbeCLICommand, type GalbeCLIOptions } from '../../../../src'
5
5
  import { walkRoutes } from '../../../../src/util'
6
- import { schemaToTypeStr, Optional, STSchema } from '../../../../src/schema'
6
+ import { schemaToTypeStr, Optional, type STSchema } from '../../../../src/schema'
7
7
 
8
8
  const cliTargets = ['cac']
9
9
  const cliModes = ['standalone', 'module']
@@ -82,11 +82,11 @@ export default (cmd: Command) => {
82
82
  const params = Object.fromEntries(
83
83
  [...r.path.matchAll(/:([^\/]+)/g)]?.map(m => [
84
84
  m?.[1],
85
- r.schema?.params?.[m?.[1]]
85
+ (r.schema?.params as Record<string, STSchema>)?.[m[1]]
86
86
  ? {
87
- type: schemaToTypeStr(r.schema.params[m[1]]),
88
- ...(r.schema.params[m[1]]?.description
89
- ? { description: r.schema.params[m[1]].description as string }
87
+ type: schemaToTypeStr((r.schema.params as Record<string, STSchema>)[m[1]]),
88
+ ...((r.schema.params as Record<string, STSchema>)[m[1]]?.description
89
+ ? { description: (r.schema.params as Record<string, STSchema>)[m[1]].description as string }
90
90
  : {}),
91
91
  }
92
92
  : { type: 'string' },
@@ -99,13 +99,13 @@ export default (cmd: Command) => {
99
99
  description: summary || description,
100
100
  route: r,
101
101
  pathT,
102
- arguments: Object.entries(
103
- (params || {}) as Record<string, { type: string; description?: string }>
104
- ).map(([k, p]) => ({
105
- name: k,
106
- type: p.type === 'boolean' ? '' : `<${p.type}>`,
107
- description: p?.description || '',
108
- })),
102
+ arguments: Object.entries((params || {}) as Record<string, { type: string; description?: string }>).map(
103
+ ([k, p]) => ({
104
+ name: k,
105
+ type: p.type === 'boolean' ? '' : `<${p.type}>`,
106
+ description: p?.description || '',
107
+ })
108
+ ),
109
109
  options: Object.entries((r.schema?.query || {}) as Record<string, STSchema>).map(([k, o]) => {
110
110
  const type = schemaToTypeStr({ ...o, [Optional]: false })
111
111
  return {
@@ -147,10 +147,17 @@ export default (cmd: Command) => {
147
147
 
148
148
  if (target === 'cac') {
149
149
  const { generate } = await import('./targets/cac')
150
- await generate({ commands, mode, out, pckg, options: userOptions })
150
+ await generate({
151
+ commands,
152
+ mode,
153
+ out,
154
+ pckg,
155
+ options: userOptions,
156
+ configPath: config ? resolve(CWD, config) : undefined,
157
+ })
151
158
  }
152
159
 
153
- Bun.write(Bun.stdout, ' : \x1b[1;30m\x1b[32mdone\x1b[0m\n')
160
+ Bun.write(Bun.stdout, ` : \x1b[1;30m\x1b[32mdone\x1b[0m\nCLI ${mode} generated at ${out}\n`)
154
161
  process.exit(0)
155
162
  })
156
163
  }
@@ -11,6 +11,7 @@ export interface GenerateOptions {
11
11
  out: string
12
12
  pckg: any
13
13
  options?: GalbeCLIOptions
14
+ configPath?: string
14
15
  }
15
16
 
16
17
  export async function generate(opts: GenerateOptions): Promise<void> {
@@ -19,7 +20,61 @@ export async function generate(opts: GenerateOptions): Promise<void> {
19
20
  }
20
21
 
21
22
  async function buildStandalone(opts: GenerateOptions): Promise<void> {
22
- const code = generateSource(opts.commands, 'standalone', opts.pckg, opts.options)
23
+ // Bundle config as an IIFE to avoid importing the full config module at startup
24
+ // (a direct import would run all module-level side effects before the CLI is set up).
25
+ let inlinedConfigCode: string | undefined
26
+ if (opts.configPath) {
27
+ const tmpDir = await mkdtemp(resolve(tmpdir(), 'galbe-cli-cfg-'))
28
+ try {
29
+ const allExports = [
30
+ '__galbe_baseUrl',
31
+ '__galbe_headers',
32
+ '__galbe_requestInterceptor',
33
+ '__galbe_responseFormatter',
34
+ ]
35
+ const entry = [
36
+ `import { options as __cfg } from ${JSON.stringify(opts.configPath)}`,
37
+ `const __galbe_baseUrl = __cfg?.baseUrl`,
38
+ `const __galbe_headers = __cfg?.headers`,
39
+ `const __galbe_requestInterceptor = __cfg?.requestInterceptor`,
40
+ `const __galbe_responseFormatter = __cfg?.responseFormatter`,
41
+ `export { ${allExports.join(', ')} }`,
42
+ ].join('\n')
43
+ const entryPath = resolve(tmpDir, 'entry.ts')
44
+ const outPath = resolve(tmpDir, 'bundled.js')
45
+ await Bun.write(entryPath, entry)
46
+ await $`bun build --bundle --format esm ${entryPath} --outfile ${outPath}`.quiet()
47
+ const bundled = await Bun.file(outPath).text()
48
+
49
+ const exportMatch = bundled.match(/export\s*\{([^}]+)\}/)
50
+ const codeBody = exportMatch ? bundled.replace(/export\s*\{[^}]+\}\s*;?\s*/g, '').trim() : bundled.trim()
51
+
52
+ let returnExpr = `{ ${allExports.join(', ')} }`
53
+ if (exportMatch) {
54
+ const entries = exportMatch[1]
55
+ .split(',')
56
+ .map(e => {
57
+ const parts = e.trim().split(/\s+as\s+/)
58
+ return { local: parts[0].trim(), exported: (parts[1] ?? parts[0]).trim() }
59
+ })
60
+ .filter(e => allExports.includes(e.exported))
61
+ if (entries.length) {
62
+ const props = entries.map(e => (e.local === e.exported ? e.exported : `${e.exported}: ${e.local}`)).join(', ')
63
+ returnExpr = `{ ${props} }`
64
+ }
65
+ }
66
+
67
+ const indented = codeBody
68
+ .split('\n')
69
+ .map(l => ' ' + l)
70
+ .join('\n')
71
+ inlinedConfigCode = `const { ${allExports.join(', ')} } = (() => {\n${indented}\n return ${returnExpr}\n})()`
72
+ } finally {
73
+ await rm(tmpDir, { recursive: true })
74
+ }
75
+ }
76
+
77
+ const code = generateSource(opts.commands, 'standalone', opts.pckg, opts.options, undefined, inlinedConfigCode)
23
78
  const buildDir = await mkdtemp(resolve(tmpdir(), 'galbe-cli-'))
24
79
  try {
25
80
  await Bun.write(resolve(buildDir, 'package.json'), JSON.stringify({ dependencies: { cac: 'latest' } }))
@@ -33,7 +88,56 @@ async function buildStandalone(opts: GenerateOptions): Promise<void> {
33
88
  }
34
89
 
35
90
  async function buildModule(opts: GenerateOptions): Promise<void> {
36
- const code = generateSource(opts.commands, 'module', opts.pckg, opts.options)
91
+ // For module mode, the output must be self-contained (only cac as external dep).
92
+ // Bundle the config and its transitive deps into an IIFE snippet that is embedded
93
+ // directly in the generated file.
94
+ let inlinedConfigCode: string | undefined
95
+ if (opts.configPath) {
96
+ const tmpDir = await mkdtemp(resolve(tmpdir(), 'galbe-cli-cfg-'))
97
+ try {
98
+ const entry = [
99
+ `import { options as __cfg } from ${JSON.stringify(opts.configPath)}`,
100
+ `const __galbe_requestInterceptor = __cfg?.requestInterceptor`,
101
+ `const __galbe_responseFormatter = __cfg?.responseFormatter`,
102
+ `export { __galbe_requestInterceptor, __galbe_responseFormatter }`,
103
+ ].join('\n')
104
+ const entryPath = resolve(tmpDir, 'entry.ts')
105
+ const outPath = resolve(tmpDir, 'bundled.js')
106
+ await Bun.write(entryPath, entry)
107
+ await $`bun build --bundle --format esm ${entryPath} --outfile ${outPath}`.quiet()
108
+ const bundled = await Bun.file(outPath).text()
109
+
110
+ // Parse "export { localA as exportedA, ... }" and transform to a return expression
111
+ // so that renaming by the bundler doesn't break the IIFE interface.
112
+ const exportMatch = bundled.match(/export\s*\{([^}]+)\}/)
113
+ const codeBody = exportMatch ? bundled.replace(/export\s*\{[^}]+\}\s*;?\s*/g, '').trim() : bundled.trim()
114
+
115
+ let returnExpr = '{ __galbe_requestInterceptor, __galbe_responseFormatter }'
116
+ if (exportMatch) {
117
+ const entries = exportMatch[1]
118
+ .split(',')
119
+ .map(e => {
120
+ const parts = e.trim().split(/\s+as\s+/)
121
+ return { local: parts[0].trim(), exported: (parts[1] ?? parts[0]).trim() }
122
+ })
123
+ .filter(e => e.exported === '__galbe_requestInterceptor' || e.exported === '__galbe_responseFormatter')
124
+ if (entries.length) {
125
+ const props = entries.map(e => (e.local === e.exported ? e.exported : `${e.exported}: ${e.local}`)).join(', ')
126
+ returnExpr = `{ ${props} }`
127
+ }
128
+ }
129
+
130
+ const indented = codeBody
131
+ .split('\n')
132
+ .map(l => ' ' + l)
133
+ .join('\n')
134
+ inlinedConfigCode = `const { __galbe_requestInterceptor, __galbe_responseFormatter } = (() => {\n${indented}\n return ${returnExpr}\n})()`
135
+ } finally {
136
+ await rm(tmpDir, { recursive: true })
137
+ }
138
+ }
139
+
140
+ const code = generateSource(opts.commands, 'module', opts.pckg, opts.options, undefined, inlinedConfigCode)
37
141
  await Bun.write(resolve(CWD, opts.out), code)
38
142
  }
39
143
 
@@ -65,7 +169,7 @@ function generateSingleCommand(c: GalbeCLICommand, cliVar: string, includeTag: b
65
169
  const builtinShorts = new Set(['H', 'Q', 'b', 'B'])
66
170
  const hide = new Set(c.hideOptions ?? [])
67
171
  const tagPrefix = includeTag && c.tags.length > 0 ? c.tags.map(t => t.toLowerCase()).join(' ') + ' ' : ''
68
- const pathArgs = (c.arguments || []).map(a => ` <${a.name}>`).join('')
172
+ const pathArgs = (c.arguments || []).map(a => ` ${a.type.replace(/\w+/, a.name)}`).join('')
69
173
  const cmdStr = `${tagPrefix}${c.name}${pathArgs}`
70
174
  const desc = JSON.stringify(c.description || '')
71
175
 
@@ -73,7 +177,7 @@ function generateSingleCommand(c: GalbeCLICommand, cliVar: string, includeTag: b
73
177
  .map(o => {
74
178
  const cleanName = sanitizeOptionName(o.name)
75
179
  const short = o.short && !builtinShorts.has(o.short) ? `-${o.short}, ` : ''
76
- const optType = o.type || '[string]'
180
+ const optType = o.type != null ? o.type : '[string]'
77
181
  const defVal = o.default !== undefined ? `, { default: ${serializeValue(o.default)} }` : ''
78
182
  return ` .option('${short}--${cleanName} ${optType}', ${JSON.stringify(o.description || o.name)}${defVal})`
79
183
  })
@@ -91,9 +195,7 @@ function generateSingleCommand(c: GalbeCLICommand, cliVar: string, includeTag: b
91
195
  const method = c.route.method.toUpperCase()
92
196
  const customAction = c.action ? `await (${c.action.toString()})(options as any)\n ` : ''
93
197
 
94
- const argsObj = (c.arguments || []).length
95
- ? `{ ${(c.arguments || []).map(a => a.name).join(', ')} }`
96
- : '{}'
198
+ const argsObj = (c.arguments || []).length ? `{ ${(c.arguments || []).map(a => a.name).join(', ')} }` : '{}'
97
199
 
98
200
  const commandMeta: Record<string, any> = { name: c.name, tags: c.tags }
99
201
  if (c.description !== undefined) commandMeta.description = c.description
@@ -153,7 +255,10 @@ function nodeToBlock(node: CommandNode, tagPath: string[], appName: string, vers
153
255
 
154
256
  const subBlocks = subGroupEntries.map(([tag, subNode], i) => {
155
257
  const sub = nodeToBlock(subNode, [...tagPath, tag], appName, version)
156
- const indented = sub.split('\n').map(l => ' ' + l).join('\n')
258
+ const indented = sub
259
+ .split('\n')
260
+ .map(l => ' ' + l)
261
+ .join('\n')
157
262
  return `${i === 0 ? 'if' : 'else if'} (${argvExpr} === ${JSON.stringify(tag)}) {\n${indented}\n}`
158
263
  })
159
264
 
@@ -182,7 +287,10 @@ function nodeToBlock(node: CommandNode, tagPath: string[], appName: string, vers
182
287
  .join('\n')
183
288
 
184
289
  if (subBlocks.length > 0) {
185
- const indentedCli = cliBlockLines.split('\n').map(l => ' ' + l).join('\n')
290
+ const indentedCli = cliBlockLines
291
+ .split('\n')
292
+ .map(l => ' ' + l)
293
+ .join('\n')
186
294
  return `${subBlocks.join('\n')}\nelse {\n${indentedCli}\n}`
187
295
  }
188
296
  return cliBlockLines
@@ -213,20 +321,20 @@ function generateModuleCommands(commands: GalbeCLICommand[], appName: string): s
213
321
  nodeLines.push(genSubCacSetup(subNode, subTagPath))
214
322
  nodeLines.push(
215
323
  `const ${grpVar} = ${subVar}.command(${JSON.stringify(tag + ' [..._sub]')}, ${JSON.stringify(tag + ' commands')})` +
216
- `\n .allowUnknownOptions()` +
217
- `\n .action(() => {` +
218
- `\n const _rawArgv = process.argv.slice(2)` +
219
- `\n const _tagIdx = _rawArgv.findIndex((a: string) => a === ${JSON.stringify(tag)})` +
220
- `\n const _subArgv = _tagIdx >= 0 ? _rawArgv.slice(_tagIdx + 1) : []` +
221
- `\n const ${knownVar} = new Set(${subSubVar}.commands.map((c: any) => c.name))` +
222
- `\n if (!_subArgv[0] || (!_subArgv[0].startsWith('-') && !${knownVar}.has(_subArgv[0]))) {` +
223
- `\n ${subSubVar}.outputHelp()` +
224
- `\n process.exit(_subArgv[0] ? 1 : 0)` +
225
- `\n }` +
226
- `\n try { ${subSubVar}.parse(['', '', ..._subArgv]) } catch (e: any) { console.error(\`error: \${e.message ?? e}\`); process.exit(1) }` +
227
- `\n })` +
228
- `\n;(${grpVar} as any).rawName = ${JSON.stringify(tag)}` +
229
- `\n;(${grpVar} as any).outputHelp = () => ${subSubVar}.outputHelp()`
324
+ `\n .allowUnknownOptions()` +
325
+ `\n .action(() => {` +
326
+ `\n const _rawArgv = process.argv.slice(2)` +
327
+ `\n const _tagIdx = _rawArgv.findIndex((a: string) => a === ${JSON.stringify(tag)})` +
328
+ `\n const _subArgv = _tagIdx >= 0 ? _rawArgv.slice(_tagIdx + 1) : []` +
329
+ `\n const ${knownVar} = new Set(${subSubVar}.commands.map((c: any) => c.name))` +
330
+ `\n if (!_subArgv[0] || (!_subArgv[0].startsWith('-') && !${knownVar}.has(_subArgv[0]))) {` +
331
+ `\n ${subSubVar}.outputHelp()` +
332
+ `\n process.exit(_subArgv[0] ? 1 : 0)` +
333
+ `\n }` +
334
+ `\n try { ${subSubVar}.parse(['', '', ..._subArgv]) } catch (e: any) { console.error(\`error: \${e.message ?? e}\`); process.exit(1) }` +
335
+ `\n })` +
336
+ `\n;(${grpVar} as any).rawName = ${JSON.stringify(tag)}` +
337
+ `\n;(${grpVar} as any).outputHelp = () => ${subSubVar}.outputHelp()`
230
338
  )
231
339
  }
232
340
 
@@ -241,20 +349,20 @@ function generateModuleCommands(commands: GalbeCLICommand[], appName: string): s
241
349
  lines.push(genSubCacSetup(subNode, [tag]))
242
350
  lines.push(
243
351
  `const ${grpVar} = parent.command(${JSON.stringify(tag + ' [..._sub]')}, ${JSON.stringify(tag + ' commands')})` +
244
- `\n .allowUnknownOptions()` +
245
- `\n .action(() => {` +
246
- `\n const _rawArgv = process.argv.slice(2)` +
247
- `\n const _tagIdx = _rawArgv.findIndex((a: string) => a === ${JSON.stringify(tag)})` +
248
- `\n const _subArgv = _tagIdx >= 0 ? _rawArgv.slice(_tagIdx + 1) : []` +
249
- `\n const ${knownVar} = new Set(${subVar}.commands.map((c: any) => c.name))` +
250
- `\n if (!_subArgv[0] || (!_subArgv[0].startsWith('-') && !${knownVar}.has(_subArgv[0]))) {` +
251
- `\n ${subVar}.outputHelp()` +
252
- `\n process.exit(_subArgv[0] ? 1 : 0)` +
253
- `\n }` +
254
- `\n try { ${subVar}.parse(['', '', ..._subArgv]) } catch (e: any) { console.error(\`error: \${e.message ?? e}\`); process.exit(1) }` +
255
- `\n })` +
256
- `\n;(${grpVar} as any).rawName = ${JSON.stringify(tag)}` +
257
- `\n;(${grpVar} as any).outputHelp = () => ${subVar}.outputHelp()`
352
+ `\n .allowUnknownOptions()` +
353
+ `\n .action(() => {` +
354
+ `\n const _rawArgv = process.argv.slice(2)` +
355
+ `\n const _tagIdx = _rawArgv.findIndex((a: string) => a === ${JSON.stringify(tag)})` +
356
+ `\n const _subArgv = _tagIdx >= 0 ? _rawArgv.slice(_tagIdx + 1) : []` +
357
+ `\n const ${knownVar} = new Set(${subVar}.commands.map((c: any) => c.name))` +
358
+ `\n if (!_subArgv[0] || (!_subArgv[0].startsWith('-') && !${knownVar}.has(_subArgv[0]))) {` +
359
+ `\n ${subVar}.outputHelp()` +
360
+ `\n process.exit(_subArgv[0] ? 1 : 0)` +
361
+ `\n }` +
362
+ `\n try { ${subVar}.parse(['', '', ..._subArgv]) } catch (e: any) { console.error(\`error: \${e.message ?? e}\`); process.exit(1) }` +
363
+ `\n })` +
364
+ `\n;(${grpVar} as any).rawName = ${JSON.stringify(tag)}` +
365
+ `\n;(${grpVar} as any).outputHelp = () => ${subVar}.outputHelp()`
258
366
  )
259
367
  }
260
368
 
@@ -262,20 +370,21 @@ function generateModuleCommands(commands: GalbeCLICommand[], appName: string): s
262
370
 
263
371
  lines.push(
264
372
  `const _parseOrig = (parent as any).parse.bind(parent)` +
265
- `\n;(parent as any).parse = (argv?: string[]) => {` +
266
- `\n const _a: string[] = argv ?? process.argv` +
267
- `\n const _positionals = _a.slice(2).filter((x: string) => !x.startsWith('-'))` +
268
- `\n if (_positionals.length === 0 && !_a.slice(2).includes('--help') && !_a.slice(2).includes('-h')) {` +
269
- `\n parent.outputHelp()` +
270
- `\n process.exit(0)` +
271
- `\n }` +
272
- `\n const _r = _parseOrig(argv)` +
273
- `\n if (!(parent as any).matchedCommand && _positionals.length > 0 && !_a.slice(2).includes('--help') && !_a.slice(2).includes('-h')) {` +
274
- `\n console.error(\`error: Unknown command "\${_positionals[0]}"\`)` +
275
- `\n process.exit(1)` +
276
- `\n }` +
277
- `\n return _r` +
278
- `\n}`
373
+ `\n;(parent as any).parse = (argv?: string[]) => {` +
374
+ `\n const _a: string[] = argv ?? process.argv` +
375
+ `\n const _positionals = _a.slice(2).filter((x: string) => !x.startsWith('-'))` +
376
+ `\n if (_positionals.length === 0 && !_a.slice(2).includes('--help') && !_a.slice(2).includes('-h')) {` +
377
+ `\n parent.outputHelp()` +
378
+ `\n process.exit(0)` +
379
+ `\n }` +
380
+ `\n let _r: any` +
381
+ `\n try { _r = _parseOrig(argv) } catch (e: any) { console.error(\`error: \${e.message ?? e}\`); process.exit(1) }` +
382
+ `\n if (!(parent as any).matchedCommand && _positionals.length > 0 && !_a.slice(2).includes('--help') && !_a.slice(2).includes('-h')) {` +
383
+ `\n console.error(\`error: Unknown command "\${_positionals[0]}"\`)` +
384
+ `\n process.exit(1)` +
385
+ `\n }` +
386
+ `\n return _r` +
387
+ `\n}`
279
388
  )
280
389
 
281
390
  return lines.filter(Boolean).join('\n\n')
@@ -413,7 +522,7 @@ ${fetchApiSignature}
413
522
  const elapsed = (Bun.nanoseconds() - t0) / 1_000_000
414
523
 
415
524
  if (${formatterRef}) {
416
- Bun.write(Bun.stdout, await ${formatterRef}(_res))
525
+ Bun.write(Bun.stdout, await ${formatterRef}(_res, command, cliArgs, cliOptions))
417
526
  } else {
418
527
  if (fmt.has('s')) Bun.write(Bun.stdout, \`\${_res.status}\\n\`)
419
528
  if (fmt.has('h')) Bun.write(Bun.stdout, _fmtObject(_headersToObj(_res.headers), fmt.has('p')) + '\\n')
@@ -446,16 +555,26 @@ function generateSource(
446
555
  commands: GalbeCLICommand[],
447
556
  mode: 'standalone' | 'module',
448
557
  pckg: any,
449
- options?: GalbeCLIOptions
558
+ options?: GalbeCLIOptions,
559
+ _unused?: undefined,
560
+ // pre-bundled IIFE snippet; standalone exports all 4 config values, module exports only functions
561
+ inlinedConfigCode?: string
450
562
  ): string {
451
563
  const name = pckg?.name || 'galbe-cli'
452
564
  const version = pckg?.version || '0.1.0'
453
565
  const nameVersion = `${name}/${version}/cli`
454
566
 
455
- const bakedBaseUrl = serializeValue(options?.baseUrl)
456
- const bakedHeaders = serializeValue(options?.headers ?? {})
457
- const bakedRequestInterceptor = serializeValue(options?.requestInterceptor)
458
- const bakedResponseFormatter = serializeValue(options?.responseFormatter)
567
+ // Standalone with config: all values come from the IIFE (avoids module-level side effects).
568
+ // Module with config: scalars serialized from options; functions come from the IIFE.
569
+ const standaloneInlined = mode === 'standalone' && !!inlinedConfigCode
570
+ const bakedBaseUrl = standaloneInlined ? '__galbe_baseUrl' : serializeValue(options?.baseUrl)
571
+ const bakedHeaders = standaloneInlined ? '__galbe_headers ?? {}' : serializeValue(options?.headers ?? {})
572
+ const bakedRequestInterceptor = !!inlinedConfigCode
573
+ ? '__galbe_requestInterceptor'
574
+ : serializeValue(options?.requestInterceptor)
575
+ const bakedResponseFormatter = !!inlinedConfigCode
576
+ ? '__galbe_responseFormatter'
577
+ : serializeValue(options?.responseFormatter)
459
578
 
460
579
  const utils = generateUtilities()
461
580
  const fetchApi = generateFetchApi(
@@ -478,7 +597,7 @@ function generateSource(
478
597
  ${header}
479
598
 
480
599
  import { cac } from 'cac'
481
- ${utils}
600
+ ${inlinedConfigCode ? '\n' + inlinedConfigCode + '\n' : ''}${utils}
482
601
  ${fetchApi}
483
602
 
484
603
  const _argv = process.argv.slice(2)
@@ -491,10 +610,16 @@ ${block}
491
610
  const defaultOptsEntries: string[] = []
492
611
  if (options?.baseUrl !== undefined) defaultOptsEntries.push(` baseUrl: ${serializeValue(options.baseUrl)}`)
493
612
  if (options?.headers !== undefined) defaultOptsEntries.push(` headers: ${serializeValue(options.headers)}`)
494
- if (options?.requestInterceptor !== undefined)
495
- defaultOptsEntries.push(` requestInterceptor: ${serializeValue(options.requestInterceptor)}`)
496
- if (options?.responseFormatter !== undefined)
497
- defaultOptsEntries.push(` responseFormatter: ${serializeValue(options.responseFormatter)}`)
613
+ if (inlinedConfigCode) {
614
+ // Functions are defined by the inlined IIFE; always include them (undefined = no-op)
615
+ defaultOptsEntries.push(` requestInterceptor: __galbe_requestInterceptor as CLIOptions['requestInterceptor']`)
616
+ defaultOptsEntries.push(` responseFormatter: __galbe_responseFormatter as CLIOptions['responseFormatter']`)
617
+ } else {
618
+ if (options?.requestInterceptor !== undefined)
619
+ defaultOptsEntries.push(` requestInterceptor: ${serializeValue(options.requestInterceptor)}`)
620
+ if (options?.responseFormatter !== undefined)
621
+ defaultOptsEntries.push(` responseFormatter: ${serializeValue(options.responseFormatter)}`)
622
+ }
498
623
  const defaultOpts = defaultOptsEntries.length ? `{\n${defaultOptsEntries.join(',\n')}\n}` : '{}'
499
624
 
500
625
  return `${header}
@@ -515,9 +640,9 @@ export type CLIOptions = {
515
640
  baseUrl?: string | (() => string)
516
641
  headers?: Record<string, string>
517
642
  requestInterceptor?: (req: Request, command: CommandInfo, args: Record<string, string>, options: Record<string, any>) => Promise<Request> | Request
518
- responseFormatter?: (res: Response) => Promise<string> | string
643
+ responseFormatter?: (res: Response, command: CommandInfo, args: Record<string, string>, options: Record<string, any>) => Promise<string> | string
519
644
  }
520
- ${utils}
645
+ ${inlinedConfigCode ? '\n' + inlinedConfigCode + '\n' : ''}${utils}
521
646
  ${fetchApi}
522
647
 
523
648
  const _defaultOptions: CLIOptions = ${defaultOpts}