create-bunli 0.5.5 → 0.6.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.
@@ -21,6 +21,7 @@
21
21
  "dependencies": {
22
22
  "@bunli/core": "latest",
23
23
  "@bunli/utils": "latest",
24
+ "better-result": "^2.7.0",
24
25
  "zod": "^3.22.0"
25
26
  },
26
27
  "devDependencies": {
@@ -34,4 +35,4 @@
34
35
  "outDir": "./dist",
35
36
  "external": ["@bunli/core", "@bunli/utils", "zod"]
36
37
  }
37
- }
38
+ }
@@ -1,77 +1,150 @@
1
1
  import { defineCommand, option } from '@bunli/core'
2
+ import { Result, TaggedError } from 'better-result'
2
3
  import { z } from 'zod'
3
4
  import { loadConfig, saveConfig, getConfigPath } from '../utils/config.js'
5
+ import { DEFAULT_CONFIG } from '../utils/constants.js'
6
+
7
+ const toErrorMessage = (error: unknown): string =>
8
+ error instanceof Error ? error.message : String(error)
9
+
10
+ class ConfigCommandError extends TaggedError('ConfigCommandError')<{
11
+ message: string
12
+ cause?: unknown
13
+ }>() {
14
+ constructor(message: string, cause?: unknown) {
15
+ super(cause === undefined ? { message } : { message, cause })
16
+ }
17
+ }
4
18
 
5
19
  const configCommand = defineCommand({
6
20
  name: 'config',
7
21
  description: 'Manage configuration',
8
- subcommands: [
22
+ commands: [
9
23
  defineCommand({
10
24
  name: 'get',
11
25
  description: 'Get a config value',
12
- args: z.tuple([z.string()]).describe('Config key to get'),
13
- handler: async ({ args, colors }) => {
14
- const [key] = args
15
-
16
- try {
17
- const config = await loadConfig()
18
- const value = getNestedValue(config, key)
19
-
20
- if (value === undefined) {
21
- console.log(colors.yellow(`Config key '${key}' not found`))
22
- } else {
23
- console.log(JSON.stringify(value, null, 2))
24
- }
25
- } catch (error) {
26
- console.error(colors.red(`Failed to load config: ${error}`))
27
- process.exit(1)
26
+ handler: async ({ positional, colors }) => {
27
+ const key = positional[0]
28
+ if (!key) {
29
+ console.error(colors.red('Usage: config get <key>'))
30
+ process.exitCode = 1
31
+ return
32
+ }
33
+
34
+ const configResult = await Result.tryPromise({
35
+ try: () => loadConfig(),
36
+ catch: (cause) => new ConfigCommandError(`Failed to load config: ${toErrorMessage(cause)}`, cause)
37
+ })
38
+
39
+ if (Result.isError(configResult)) {
40
+ console.error(colors.red(configResult.error.message))
41
+ process.exitCode = 1
42
+ return
43
+ }
44
+
45
+ const value = getNestedValue(configResult.value as Record<string, unknown>, key)
46
+
47
+ if (value === undefined) {
48
+ console.log(colors.yellow(`Config key '${key}' not found`))
49
+ } else {
50
+ console.log(JSON.stringify(value, null, 2))
28
51
  }
29
52
  }
30
53
  }),
31
-
54
+
32
55
  defineCommand({
33
56
  name: 'set',
34
57
  description: 'Set a config value',
35
- args: z.tuple([z.string(), z.string()]).describe('Config key and value'),
36
- handler: async ({ args, colors, spinner }) => {
37
- const [key, value] = args
38
-
58
+ handler: async ({ positional, colors, spinner }) => {
59
+ const key = positional[0]
60
+ const rawValue = positional[1]
61
+
62
+ if (!key || rawValue === undefined) {
63
+ console.error(colors.red('Usage: config set <key> <json-value>'))
64
+ process.exitCode = 1
65
+ return
66
+ }
67
+
39
68
  const spin = spinner('Updating config...')
40
69
  spin.start()
41
-
42
- try {
43
- const config = await loadConfig()
44
- setNestedValue(config, key, JSON.parse(value))
45
- await saveConfig(config)
46
-
47
- spin.succeed(`Config '${key}' updated`)
48
- } catch (error) {
70
+
71
+ const configResult = await Result.tryPromise({
72
+ try: () => loadConfig(),
73
+ catch: (cause) => new ConfigCommandError(`Failed to load config: ${toErrorMessage(cause)}`, cause)
74
+ })
75
+
76
+ if (Result.isError(configResult)) {
77
+ spin.fail('Failed to update config')
78
+ console.error(colors.red(configResult.error.message))
79
+ process.exitCode = 1
80
+ return
81
+ }
82
+
83
+ const parsedValue = Result.try({
84
+ try: () => JSON.parse(rawValue),
85
+ catch: (cause) =>
86
+ new ConfigCommandError(`Failed to parse value as JSON: ${toErrorMessage(cause)}`, cause)
87
+ })
88
+
89
+ if (Result.isError(parsedValue)) {
90
+ spin.fail('Failed to update config')
91
+ console.error(colors.red(parsedValue.error.message))
92
+ process.exitCode = 1
93
+ return
94
+ }
95
+
96
+ const nextConfig = configResult.value as Record<string, unknown>
97
+ setNestedValue(nextConfig, key, parsedValue.value)
98
+
99
+ const saveResult = await Result.tryPromise({
100
+ try: () => saveConfig(nextConfig),
101
+ catch: (cause) => new ConfigCommandError(`Failed to save config: ${toErrorMessage(cause)}`, cause)
102
+ })
103
+
104
+ if (Result.isError(saveResult)) {
49
105
  spin.fail('Failed to update config')
50
- console.error(colors.red(String(error)))
51
- process.exit(1)
106
+ console.error(colors.red(saveResult.error.message))
107
+ process.exitCode = 1
108
+ return
52
109
  }
110
+
111
+ spin.succeed(`Config '${key}' updated`)
53
112
  }
54
113
  }),
55
-
114
+
56
115
  defineCommand({
57
116
  name: 'list',
58
117
  description: 'List all config values',
59
118
  handler: async ({ colors }) => {
60
- try {
61
- const config = await loadConfig()
62
- const configPath = await getConfigPath()
63
-
64
- console.log(colors.bold('Configuration:'))
65
- console.log(colors.dim(` File: ${configPath}`))
66
- console.log()
67
- console.log(JSON.stringify(config, null, 2))
68
- } catch (error) {
69
- console.error(colors.red(`Failed to load config: ${error}`))
70
- process.exit(1)
119
+ const configResult = await Result.tryPromise({
120
+ try: () => loadConfig(),
121
+ catch: (cause) => new ConfigCommandError(`Failed to load config: ${toErrorMessage(cause)}`, cause)
122
+ })
123
+
124
+ if (Result.isError(configResult)) {
125
+ console.error(colors.red(configResult.error.message))
126
+ process.exitCode = 1
127
+ return
71
128
  }
129
+
130
+ const configPathResult = await Result.tryPromise({
131
+ try: () => getConfigPath(),
132
+ catch: (cause) => new ConfigCommandError(`Failed to resolve config path: ${toErrorMessage(cause)}`, cause)
133
+ })
134
+
135
+ if (Result.isError(configPathResult)) {
136
+ console.error(colors.red(configPathResult.error.message))
137
+ process.exitCode = 1
138
+ return
139
+ }
140
+
141
+ console.log(colors.bold('Configuration:'))
142
+ console.log(colors.dim(` File: ${configPathResult.value}`))
143
+ console.log()
144
+ console.log(JSON.stringify(configResult.value, null, 2))
72
145
  }
73
146
  }),
74
-
147
+
75
148
  defineCommand({
76
149
  name: 'reset',
77
150
  description: 'Reset config to defaults',
@@ -90,58 +163,67 @@ const configCommand = defineCommand({
90
163
  'This will reset all config to defaults. Continue?',
91
164
  { default: false }
92
165
  )
93
-
166
+
94
167
  if (!confirmed) {
95
168
  console.log(colors.yellow('Reset cancelled'))
96
169
  return
97
170
  }
98
171
  }
99
-
172
+
100
173
  const spin = spinner('Resetting config...')
101
174
  spin.start()
102
-
103
- try {
104
- const { DEFAULT_CONFIG } = await import('../utils/constants.js')
105
- await saveConfig(DEFAULT_CONFIG)
106
-
107
- spin.succeed('Config reset to defaults')
108
- } catch (error) {
175
+
176
+ const saveResult = await Result.tryPromise({
177
+ try: () => saveConfig(DEFAULT_CONFIG),
178
+ catch: (cause) => new ConfigCommandError(`Failed to save config: ${toErrorMessage(cause)}`, cause)
179
+ })
180
+
181
+ if (Result.isError(saveResult)) {
109
182
  spin.fail('Failed to reset config')
110
- console.error(colors.red(String(error)))
111
- process.exit(1)
183
+ console.error(colors.red(saveResult.error.message))
184
+ process.exitCode = 1
185
+ return
112
186
  }
187
+
188
+ spin.succeed('Config reset to defaults')
113
189
  }
114
190
  })
115
191
  ]
116
192
  })
117
193
 
118
- function getNestedValue(obj: any, path: string): any {
194
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
119
195
  const keys = path.split('.')
120
- let current = obj
121
-
196
+ let current: unknown = obj
197
+
122
198
  for (const key of keys) {
123
- if (current === null || current === undefined) {
199
+ if (!current || typeof current !== 'object') {
124
200
  return undefined
125
201
  }
126
- current = current[key]
202
+
203
+ current = (current as Record<string, unknown>)[key]
127
204
  }
128
-
205
+
129
206
  return current
130
207
  }
131
208
 
132
- function setNestedValue(obj: any, path: string, value: any): void {
209
+ function setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {
133
210
  const keys = path.split('.')
134
- const lastKey = keys.pop()!
135
- let current = obj
136
-
211
+ const lastKey = keys.pop()
212
+ if (!lastKey) {
213
+ return
214
+ }
215
+
216
+ let current: Record<string, unknown> = obj
217
+
137
218
  for (const key of keys) {
138
- if (!(key in current) || typeof current[key] !== 'object') {
219
+ const next = current[key]
220
+ if (!next || typeof next !== 'object' || Array.isArray(next)) {
139
221
  current[key] = {}
140
222
  }
141
- current = current[key]
223
+ current = current[key] as Record<string, unknown>
142
224
  }
143
-
225
+
144
226
  current[lastKey] = value
145
227
  }
146
228
 
147
- export default configCommand
229
+ export default configCommand
@@ -7,7 +7,7 @@ const serveCommand = defineCommand({
7
7
  description: 'Start a development server',
8
8
  options: {
9
9
  port: option(
10
- z.number().int().min(1).max(65535).default(3000),
10
+ z.coerce.number().int().min(1).max(65535).default(3000),
11
11
  {
12
12
  short: 'p',
13
13
  description: 'Port to listen on'
@@ -175,4 +175,4 @@ function getHomePage(): string {
175
175
  `.trim()
176
176
  }
177
177
 
178
- export default serveCommand
178
+ export default serveCommand
@@ -7,7 +7,6 @@ import { glob } from '../utils/glob.js'
7
7
  const validateCommand = defineCommand({
8
8
  name: 'validate',
9
9
  description: 'Validate files against defined rules',
10
- args: z.array(z.string()).min(1).describe('Files to validate'),
11
10
  options: {
12
11
  config: option(
13
12
  z.string().optional(),
@@ -30,66 +29,63 @@ const validateCommand = defineCommand({
30
29
  }
31
30
  )
32
31
  },
33
- handler: async ({ args, flags, colors, spinner }) => {
32
+ handler: async ({ positional, flags, colors, spinner }) => {
34
33
  const spin = spinner('Loading configuration...')
35
34
  spin.start()
36
-
35
+
37
36
  try {
38
- // Load config
39
37
  const config = await loadConfig(flags.config)
40
38
  spin.succeed('Configuration loaded')
41
-
42
- // Resolve files
39
+
40
+ const patterns = positional.length > 0 ? positional : config.include || ['src/**/*.{js,ts}']
41
+
43
42
  const fileSpin = spinner('Resolving files...')
44
43
  fileSpin.start()
45
-
46
- const files = await glob(args, {
44
+
45
+ const files = await glob(patterns, {
47
46
  include: config.include,
48
47
  exclude: config.exclude
49
48
  })
50
-
49
+
51
50
  fileSpin.succeed(`Found ${files.length} files to validate`)
52
-
51
+
53
52
  if (files.length === 0) {
54
53
  console.log(colors.yellow('No files matched the pattern'))
55
54
  return
56
55
  }
57
-
58
- // Run validation
56
+
59
57
  const validateSpin = spinner('Validating files...')
60
58
  validateSpin.start()
61
-
59
+
62
60
  const results = await validateFiles(files, {
63
61
  rules: config.rules,
64
62
  fix: flags.fix,
65
63
  cache: flags.cache && config.cache?.enabled
66
64
  })
67
-
65
+
68
66
  validateSpin.stop()
69
-
70
- // Display results
67
+
71
68
  let hasErrors = false
72
-
69
+
73
70
  for (const result of results) {
74
71
  if (result.errors.length > 0 || result.warnings.length > 0) {
75
72
  console.log()
76
73
  console.log(colors.bold(result.file))
77
-
74
+
78
75
  for (const error of result.errors) {
79
76
  console.log(colors.red(` ✗ ${error.line}:${error.column} ${error.message}`))
80
77
  hasErrors = true
81
78
  }
82
-
79
+
83
80
  for (const warning of result.warnings) {
84
81
  console.log(colors.yellow(` ⚠ ${warning.line}:${warning.column} ${warning.message}`))
85
82
  }
86
83
  }
87
84
  }
88
-
89
- // Summary
85
+
90
86
  const totalErrors = results.reduce((sum, r) => sum + r.errors.length, 0)
91
87
  const totalWarnings = results.reduce((sum, r) => sum + r.warnings.length, 0)
92
-
88
+
93
89
  console.log()
94
90
  if (totalErrors === 0 && totalWarnings === 0) {
95
91
  console.log(colors.green('✅ All files passed validation!'))
@@ -101,12 +97,11 @@ const validateCommand = defineCommand({
101
97
  if (totalWarnings > 0) {
102
98
  console.log(colors.yellow(` ${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}`))
103
99
  }
104
-
100
+
105
101
  if (hasErrors) {
106
102
  process.exit(1)
107
103
  }
108
104
  }
109
-
110
105
  } catch (error) {
111
106
  spin.fail('Validation failed')
112
107
  console.error(colors.red(String(error)))
@@ -115,4 +110,4 @@ const validateCommand = defineCommand({
115
110
  }
116
111
  })
117
112
 
118
- export default validateCommand
113
+ export default validateCommand
@@ -1,44 +1,55 @@
1
1
  #!/usr/bin/env bun
2
2
  import { createCLI } from '@bunli/core'
3
+ import { Result, TaggedError } from 'better-result'
3
4
  import initCommand from './commands/init.js'
4
5
  import validateCommand from './commands/validate.js'
5
6
  import serveCommand from './commands/serve.js'
6
7
  import configCommand from './commands/config.js'
7
8
  import { loadConfig } from './utils/config.js'
8
9
 
10
+ const toErrorMessage = (error: unknown): string =>
11
+ error instanceof Error ? error.message : String(error)
12
+
13
+ class CliStartupError extends TaggedError('CliStartupError')<{
14
+ message: string
15
+ cause: unknown
16
+ }>() {
17
+ constructor(cause: unknown) {
18
+ super({ message: `Failed to start CLI: ${toErrorMessage(cause)}`, cause })
19
+ }
20
+ }
21
+
9
22
  const cli = await createCLI({
10
23
  name: '{{name}}',
11
24
  version: '0.1.0',
12
25
  description: '{{description}}'
13
26
  })
14
27
 
15
- // Global options
16
- cli.option('verbose', {
17
- type: 'boolean',
18
- description: 'Enable verbose output'
19
- })
20
-
21
- cli.option('quiet', {
22
- type: 'boolean',
23
- description: 'Suppress output'
24
- })
25
-
26
- // Add commands
27
28
  cli.command(initCommand)
28
29
  cli.command(validateCommand)
29
30
  cli.command(serveCommand)
30
31
  cli.command(configCommand)
31
32
 
32
- // Load config and run
33
- async function run() {
34
- try {
35
- const config = await loadConfig()
36
- // Store config in global context if needed
37
- await cli.run()
38
- } catch (error) {
39
- console.error('Failed to start CLI:', error)
40
- process.exit(1)
33
+ async function run(): Promise<Result<void, CliStartupError>> {
34
+ const configResult = await Result.tryPromise({
35
+ try: () => loadConfig(),
36
+ catch: (cause) => new CliStartupError(cause)
37
+ })
38
+
39
+ if (Result.isError(configResult)) {
40
+ return configResult
41
41
  }
42
+
43
+ return Result.tryPromise({
44
+ try: async () => {
45
+ await cli.run()
46
+ },
47
+ catch: (cause) => new CliStartupError(cause)
48
+ })
42
49
  }
43
50
 
44
- await run()
51
+ const result = await run()
52
+ if (Result.isError(result)) {
53
+ console.error(result.error.message)
54
+ process.exit(1)
55
+ }
@@ -43,7 +43,7 @@ export async function loadConfig(configPath?: string): Promise<Config> {
43
43
  const config = configModule.default || configModule
44
44
 
45
45
  // Merge with defaults
46
- cachedConfig = {
46
+ const mergedConfig: Config = {
47
47
  ...DEFAULT_CONFIG,
48
48
  ...config,
49
49
  server: {
@@ -51,8 +51,9 @@ export async function loadConfig(configPath?: string): Promise<Config> {
51
51
  ...(config.server || {})
52
52
  }
53
53
  }
54
-
55
- return cachedConfig
54
+
55
+ cachedConfig = mergedConfig
56
+ return mergedConfig
56
57
  } catch (error) {
57
58
  console.warn(`Failed to load config from ${finalPath}:`, error)
58
59
  return DEFAULT_CONFIG
@@ -80,4 +81,4 @@ export async function getConfigPath(): Promise<string> {
80
81
  }
81
82
 
82
83
  return 'No config file found'
83
- }
84
+ }
@@ -18,8 +18,8 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@bunli/core": "latest",
21
- "@{{name}}/core": "workspace:*",
22
- "@{{name}}/utils": "workspace:*"
21
+ "@{{name}}/core": "workspace:^",
22
+ "@{{name}}/utils": "workspace:^"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@bunli/test": "latest",
@@ -21,7 +21,7 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@bunli/core": "latest",
24
- "@{{name}}/utils": "workspace:*",
24
+ "@{{name}}/utils": "workspace:^",
25
25
  "zod": "^3.22.0"
26
26
  },
27
27
  "devDependencies": {
@@ -6,7 +6,6 @@ import type { AnalyzeResult } from '../types.js'
6
6
  const analyzeCommand = defineCommand({
7
7
  name: 'analyze',
8
8
  description: 'Analyze files and generate reports',
9
- args: z.array(z.string()).min(1).describe('Files to analyze'),
10
9
  options: {
11
10
  detailed: option(
12
11
  z.boolean().default(false),
@@ -16,12 +15,18 @@ const analyzeCommand = defineCommand({
16
15
  }
17
16
  )
18
17
  },
19
- handler: async ({ args, flags, colors }) => {
18
+ handler: async ({ positional, flags, colors }) => {
19
+ const files = positional
20
+ if (files.length === 0) {
21
+ logger.error('Usage: analyze <file...>')
22
+ process.exit(1)
23
+ }
24
+
20
25
  logger.info('Starting analysis...')
21
26
 
22
27
  const results: AnalyzeResult[] = []
23
28
 
24
- for (const file of args) {
29
+ for (const file of files) {
25
30
  try {
26
31
  const result = await analyzeFile(file)
27
32
  results.push(result)
@@ -83,4 +88,4 @@ async function analyzeFile(file: string): Promise<AnalyzeResult> {
83
88
  }
84
89
  }
85
90
 
86
- export default analyzeCommand
91
+ export default analyzeCommand
@@ -6,7 +6,6 @@ import type { ProcessOptions } from '../types.js'
6
6
  const processCommand = defineCommand({
7
7
  name: 'process',
8
8
  description: 'Process input files',
9
- args: z.array(z.string()).min(1).describe('Files to process'),
10
9
  options: {
11
10
  output: option(
12
11
  z.string().optional(),
@@ -30,12 +29,18 @@ const processCommand = defineCommand({
30
29
  }
31
30
  )
32
31
  },
33
- handler: async ({ args, flags, spinner }) => {
32
+ handler: async ({ positional, flags, spinner }) => {
33
+ const files = positional
34
+ if (files.length === 0) {
35
+ logger.error('Usage: process <file...>')
36
+ process.exit(1)
37
+ }
38
+
34
39
  const spin = spinner('Processing files...')
35
40
  spin.start()
36
41
 
37
42
  try {
38
- for (const file of args) {
43
+ for (const file of files) {
39
44
  if (flags.verbose) {
40
45
  logger.info(`Processing ${file}`)
41
46
  }
@@ -49,7 +54,7 @@ const processCommand = defineCommand({
49
54
  })
50
55
  }
51
56
 
52
- spin.succeed(`Processed ${args.length} files`)
57
+ spin.succeed(`Processed ${files.length} files`)
53
58
  } catch (error) {
54
59
  spin.fail('Processing failed')
55
60
  logger.error(error)
@@ -63,4 +68,4 @@ async function processFile(file: string, options: ProcessOptions): Promise<void>
63
68
  logger.debug(`Processing ${file} with options:`, options)
64
69
  }
65
70
 
66
- export default processCommand
71
+ export default processCommand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bunli",
3
- "version": "0.5.5",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Scaffold new Bunli CLI projects",
6
6
  "bin": {
@@ -47,14 +47,15 @@
47
47
  "prepublishOnly": "bun run build"
48
48
  },
49
49
  "dependencies": {
50
- "@bunli/core": "0.5.7",
51
- "@bunli/test": "0.3.5",
52
- "@bunli/utils": "0.3.3",
50
+ "@bunli/core": "^0.6.1",
51
+ "@bunli/test": "^0.4.1",
52
+ "@bunli/utils": "^0.4.0",
53
+ "better-result": "^2.7.0",
53
54
  "giget": "^2.0.0",
54
55
  "zod": "^4.3.6"
55
56
  },
56
57
  "devDependencies": {
57
- "@types/bun": "1.3.7",
58
+ "@types/bun": "1.3.9",
58
59
  "typescript": "^5.8.3"
59
60
  }
60
61
  }
@@ -21,6 +21,7 @@
21
21
  "dependencies": {
22
22
  "@bunli/core": "latest",
23
23
  "@bunli/utils": "latest",
24
+ "better-result": "^2.7.0",
24
25
  "zod": "^3.22.0"
25
26
  },
26
27
  "devDependencies": {
@@ -34,4 +35,4 @@
34
35
  "outDir": "./dist",
35
36
  "external": ["@bunli/core", "@bunli/utils", "zod"]
36
37
  }
37
- }
38
+ }