@strav/cli 0.4.0 → 0.4.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@strav/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "description": "CLI framework and code generators for the Strav framework",
6
6
  "license": "MIT",
@@ -33,11 +33,11 @@
33
33
  "strav": "./src/cli/strav.ts"
34
34
  },
35
35
  "peerDependencies": {
36
- "@strav/kernel": "0.4.0",
37
- "@strav/http": "0.4.0",
38
- "@strav/database": "0.4.0",
39
- "@strav/queue": "0.4.0",
40
- "@strav/signal": "0.4.0"
36
+ "@strav/kernel": "0.4.2",
37
+ "@strav/http": "0.4.2",
38
+ "@strav/database": "0.4.2",
39
+ "@strav/queue": "0.4.2",
40
+ "@strav/signal": "0.4.2"
41
41
  },
42
42
  "dependencies": {
43
43
  "chalk": "^5.6.2",
@@ -17,7 +17,8 @@ export function register(program: Command): void {
17
17
  .description(
18
18
  'Generate services, controllers, policies, validators, events, and routes from schemas'
19
19
  )
20
- .action(async () => {
20
+ .option('-f, --force', 'Overwrite existing generated files')
21
+ .action(async ({ force }: { force?: boolean }) => {
21
22
  try {
22
23
  console.log(chalk.cyan('Generating API layer from schemas...'))
23
24
 
@@ -35,7 +36,7 @@ export function register(program: Command): void {
35
36
  const config = await loadGeneratorConfig()
36
37
 
37
38
  const apiGen = new ApiGenerator(schemas, representation, config)
38
- const apiFiles = await apiGen.writeAll()
39
+ const apiResult = await apiGen.writeAll(force)
39
40
 
40
41
  // Load API routing config from config/http.ts (if available)
41
42
  let apiConfig: Partial<ApiRoutingConfig> | undefined
@@ -47,24 +48,41 @@ export function register(program: Command): void {
47
48
  }
48
49
 
49
50
  const routeGen = new RouteGenerator(schemas, config, apiConfig)
50
- const routeFiles = await routeGen.writeAll()
51
+ const routeResult = await routeGen.writeAll(force)
51
52
 
52
53
  const testGen = new TestGenerator(schemas, representation, config, apiConfig)
53
- const testFiles = await testGen.writeAll()
54
+ const testResult = await testGen.writeAll(force)
54
55
 
55
56
  const docGen = new DocGenerator(schemas, representation, config, apiConfig)
56
- const docFiles = await docGen.writeAll()
57
+ const docResult = await docGen.writeAll(force)
57
58
 
58
- const files = [...apiFiles, ...routeFiles, ...testFiles, ...docFiles]
59
+ const written = [
60
+ ...apiResult.written,
61
+ ...routeResult.written,
62
+ ...testResult.written,
63
+ ...docResult.written,
64
+ ]
65
+ const skipped = [
66
+ ...apiResult.skipped,
67
+ ...routeResult.skipped,
68
+ ...testResult.skipped,
69
+ ...docResult.skipped,
70
+ ]
59
71
 
60
- if (files.length === 0) {
72
+ if (written.length === 0 && skipped.length === 0) {
61
73
  console.log(chalk.yellow('No API files to generate.'))
62
74
  return
63
75
  }
64
76
 
65
- console.log(chalk.green(`\nGenerated ${files.length} file(s):`))
66
- for (const file of files) {
67
- console.log(chalk.dim(` ${file.path}`))
77
+ for (const file of written) {
78
+ console.log(chalk.green(` CREATE `) + chalk.dim(file.path))
79
+ }
80
+ for (const file of skipped) {
81
+ console.log(chalk.yellow(` SKIP `) + chalk.dim(file.path) + chalk.dim(' (already exists)'))
82
+ }
83
+
84
+ if (skipped.length > 0) {
85
+ console.log(chalk.dim(`\nSkipped ${skipped.length} existing file(s). Use --force to overwrite.`))
68
86
  }
69
87
  } catch (err) {
70
88
  console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
@@ -9,7 +9,8 @@ export function register(program: Command): void {
9
9
  .command('generate:models')
10
10
  .alias('g:models')
11
11
  .description('Generate model classes and enums from schema definitions')
12
- .action(async () => {
12
+ .option('-f, --force', 'Overwrite existing generated files')
13
+ .action(async ({ force }: { force?: boolean }) => {
13
14
  try {
14
15
  const dbPaths = await getDatabasePaths()
15
16
  const config = await loadGeneratorConfig()
@@ -22,16 +23,22 @@ export function register(program: Command): void {
22
23
 
23
24
  const representation = registry.buildRepresentation()
24
25
  const generator = new ModelGenerator(registry.all(), representation, config)
25
- const files = await generator.writeAll()
26
+ const { written, skipped } = await generator.writeAll(force)
26
27
 
27
- if (files.length === 0) {
28
+ if (written.length === 0 && skipped.length === 0) {
28
29
  console.log(chalk.yellow('No models to generate.'))
29
30
  return
30
31
  }
31
32
 
32
- console.log(chalk.green(`\nGenerated ${files.length} file(s):`))
33
- for (const file of files) {
34
- console.log(chalk.dim(` ${file.path}`))
33
+ for (const file of written) {
34
+ console.log(chalk.green(` CREATE `) + chalk.dim(file.path))
35
+ }
36
+ for (const file of skipped) {
37
+ console.log(chalk.yellow(` SKIP `) + chalk.dim(file.path) + chalk.dim(' (already exists)'))
38
+ }
39
+
40
+ if (skipped.length > 0) {
41
+ console.log(chalk.dim(`\nSkipped ${skipped.length} existing file(s). Use --force to overwrite.`))
35
42
  }
36
43
  } catch (err) {
37
44
  console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
@@ -10,7 +10,7 @@ import type { FieldDefinition, FieldValidator } from '@strav/database/schema/fie
10
10
  import type { PostgreSQLCustomType } from '@strav/database/schema/postgres'
11
11
  import { toSnakeCase, toCamelCase, toPascalCase } from '@strav/kernel/helpers/strings'
12
12
  import type { GeneratedFile } from './model_generator.ts'
13
- import type { GeneratorConfig, GeneratorPaths } from './config.ts'
13
+ import type { GeneratorConfig, GeneratorPaths, WriteResult } from './config.ts'
14
14
  import { resolvePaths, relativeImport, formatAndWrite } from './config.ts'
15
15
 
16
16
  // ---------------------------------------------------------------------------
@@ -208,10 +208,9 @@ export default class ApiGenerator {
208
208
  }
209
209
 
210
210
  /** Generate, format with Prettier, and write all files to disk. */
211
- async writeAll(): Promise<GeneratedFile[]> {
211
+ async writeAll(force?: boolean): Promise<WriteResult> {
212
212
  const files = this.generate()
213
- await formatAndWrite(files)
214
- return files
213
+ return formatAndWrite(files, { force })
215
214
  }
216
215
 
217
216
  // ---------------------------------------------------------------------------
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs'
1
2
  import { resolve, relative } from 'node:path'
2
3
  import type { GeneratedFile } from './model_generator.ts'
3
4
 
@@ -66,11 +67,22 @@ export function relativeImport(fromDir: string, toDir: string): string {
66
67
  return rel.startsWith('.') ? rel : './' + rel
67
68
  }
68
69
 
70
+ export interface WriteResult {
71
+ written: GeneratedFile[]
72
+ skipped: GeneratedFile[]
73
+ }
74
+
69
75
  /**
70
76
  * Format generated files with Prettier and write them to disk.
71
77
  * Falls back to writing unformatted content if Prettier is not installed.
78
+ *
79
+ * Skips files that already exist unless `force` is true. Returns the
80
+ * partition of written and skipped files so callers can report them.
72
81
  */
73
- export async function formatAndWrite(files: GeneratedFile[]): Promise<void> {
82
+ export async function formatAndWrite(
83
+ files: GeneratedFile[],
84
+ options: { force?: boolean } = {}
85
+ ): Promise<WriteResult> {
74
86
  let prettier: typeof import('prettier') | null = null
75
87
  try {
76
88
  prettier = await import('prettier')
@@ -78,13 +90,24 @@ export async function formatAndWrite(files: GeneratedFile[]): Promise<void> {
78
90
  // Prettier not installed — write unformatted
79
91
  }
80
92
 
93
+ const written: GeneratedFile[] = []
94
+ const skipped: GeneratedFile[] = []
95
+
81
96
  for (const file of files) {
97
+ if (existsSync(file.path) && !options.force) {
98
+ skipped.push(file)
99
+ continue
100
+ }
101
+
82
102
  let content = file.content
83
103
  if (prettier) {
84
104
  const filePath = resolve(file.path)
85
- const options = await prettier.resolveConfig(filePath)
86
- content = await prettier.format(content, { ...options, filepath: filePath })
105
+ const prettierOpts = await prettier.resolveConfig(filePath)
106
+ content = await prettier.format(content, { ...prettierOpts, filepath: filePath })
87
107
  }
88
108
  await Bun.write(file.path, content)
109
+ written.push(file)
89
110
  }
111
+
112
+ return { written, skipped }
90
113
  }
@@ -14,8 +14,9 @@ import {
14
14
  toPascalCase,
15
15
  pluralize,
16
16
  } from '@strav/kernel/helpers/strings'
17
+ import { existsSync } from 'node:fs'
17
18
  import type { GeneratedFile } from './model_generator.ts'
18
- import type { GeneratorConfig, GeneratorPaths } from './config.ts'
19
+ import type { GeneratorConfig, GeneratorPaths, WriteResult } from './config.ts'
19
20
  import { resolvePaths } from './config.ts'
20
21
  import { ApiRouting, toRouteSegment, toChildSegment } from './route_generator.ts'
21
22
  import type { ApiRoutingConfig } from './route_generator.ts'
@@ -91,12 +92,21 @@ export default class DocGenerator {
91
92
  return [this.generateIndexPage()]
92
93
  }
93
94
 
94
- async writeAll(): Promise<GeneratedFile[]> {
95
+ async writeAll(force?: boolean): Promise<WriteResult> {
95
96
  const files = this.generate()
97
+ const written: GeneratedFile[] = []
98
+ const skipped: GeneratedFile[] = []
99
+
96
100
  for (const file of files) {
101
+ if (existsSync(file.path) && !force) {
102
+ skipped.push(file)
103
+ continue
104
+ }
97
105
  await Bun.write(file.path, file.content)
106
+ written.push(file)
98
107
  }
99
- return files
108
+
109
+ return { written, skipped }
100
110
  }
101
111
 
102
112
  // ---------------------------------------------------------------------------
@@ -9,7 +9,7 @@ import type {
9
9
  } from '@strav/database/schema/database_representation'
10
10
  import type { PostgreSQLCustomType } from '@strav/database/schema/postgres'
11
11
  import { toSnakeCase, toCamelCase, toPascalCase } from '@strav/kernel/helpers/strings'
12
- import type { GeneratorConfig, GeneratorPaths } from './config.ts'
12
+ import type { GeneratorConfig, GeneratorPaths, WriteResult } from './config.ts'
13
13
  import { resolvePaths, relativeImport, formatAndWrite } from './config.ts'
14
14
 
15
15
  export interface GeneratedFile {
@@ -54,10 +54,9 @@ export default class ModelGenerator {
54
54
  }
55
55
 
56
56
  /** Generate, format with Prettier, and write all files to disk. */
57
- async writeAll(): Promise<GeneratedFile[]> {
57
+ async writeAll(force?: boolean): Promise<WriteResult> {
58
58
  const files = this.generate()
59
- await formatAndWrite(files)
60
- return files
59
+ return formatAndWrite(files, { force })
61
60
  }
62
61
 
63
62
  // ---------------------------------------------------------------------------
@@ -3,7 +3,7 @@ import { Archetype } from '@strav/database/schema/types'
3
3
  import type { SchemaDefinition } from '@strav/database/schema/types'
4
4
  import { toSnakeCase, toPascalCase, pluralize } from '@strav/kernel/helpers/strings'
5
5
  import type { GeneratedFile } from './model_generator.ts'
6
- import type { GeneratorConfig, GeneratorPaths } from './config.ts'
6
+ import type { GeneratorConfig, GeneratorPaths, WriteResult } from './config.ts'
7
7
  import { resolvePaths, relativeImport, formatAndWrite } from './config.ts'
8
8
 
9
9
  /** Archetypes that sit under a parent's /:parentId group. */
@@ -66,10 +66,9 @@ export default class RouteGenerator {
66
66
  }
67
67
 
68
68
  /** Generate, format with Prettier, and write the route file to disk. */
69
- async writeAll(): Promise<GeneratedFile[]> {
69
+ async writeAll(force?: boolean): Promise<WriteResult> {
70
70
  const files = this.generate()
71
- await formatAndWrite(files)
72
- return files
71
+ return formatAndWrite(files, { force })
73
72
  }
74
73
 
75
74
  // ---------------------------------------------------------------------------
@@ -13,7 +13,7 @@ import {
13
13
  pluralize,
14
14
  } from '@strav/kernel/helpers/strings'
15
15
  import type { GeneratedFile } from './model_generator.ts'
16
- import type { GeneratorConfig, GeneratorPaths } from './config.ts'
16
+ import type { GeneratorConfig, GeneratorPaths, WriteResult } from './config.ts'
17
17
  import { resolvePaths, relativeImport, formatAndWrite } from './config.ts'
18
18
  import { ApiRouting, toRouteSegment, toChildSegment } from './route_generator.ts'
19
19
  import type { ApiRoutingConfig } from './route_generator.ts'
@@ -82,10 +82,9 @@ export default class TestGenerator {
82
82
  return files
83
83
  }
84
84
 
85
- async writeAll(): Promise<GeneratedFile[]> {
85
+ async writeAll(force?: boolean): Promise<WriteResult> {
86
86
  const files = this.generate()
87
- await formatAndWrite(files)
88
- return files
87
+ return formatAndWrite(files, { force })
89
88
  }
90
89
 
91
90
  // ---------------------------------------------------------------------------