@strav/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.
- package/package.json +51 -0
- package/src/cli/bootstrap.ts +79 -0
- package/src/cli/command_loader.ts +180 -0
- package/src/cli/index.ts +3 -0
- package/src/cli/strav.ts +13 -0
- package/src/commands/db_seed.ts +77 -0
- package/src/commands/generate_api.ts +74 -0
- package/src/commands/generate_key.ts +47 -0
- package/src/commands/generate_models.ts +117 -0
- package/src/commands/generate_seeder.ts +68 -0
- package/src/commands/migration_compare.ts +153 -0
- package/src/commands/migration_fresh.ts +149 -0
- package/src/commands/migration_generate.ts +93 -0
- package/src/commands/migration_rollback.ts +66 -0
- package/src/commands/migration_run.ts +57 -0
- package/src/commands/package_install.ts +161 -0
- package/src/commands/queue_flush.ts +35 -0
- package/src/commands/queue_retry.ts +34 -0
- package/src/commands/queue_work.ts +40 -0
- package/src/commands/scheduler_work.ts +46 -0
- package/src/config/loader.ts +39 -0
- package/src/generators/api_generator.ts +1036 -0
- package/src/generators/config.ts +128 -0
- package/src/generators/doc_generator.ts +986 -0
- package/src/generators/index.ts +11 -0
- package/src/generators/model_generator.ts +621 -0
- package/src/generators/route_generator.ts +188 -0
- package/src/generators/test_generator.ts +1668 -0
- package/src/index.ts +2 -0
- package/tsconfig.json +5 -0
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@strav/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "CLI framework and code generators for the Strav framework",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"bun",
|
|
9
|
+
"framework",
|
|
10
|
+
"typescript",
|
|
11
|
+
"strav",
|
|
12
|
+
"cli"
|
|
13
|
+
],
|
|
14
|
+
"strav": {
|
|
15
|
+
"commands": "src/commands"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src/",
|
|
19
|
+
"package.json",
|
|
20
|
+
"tsconfig.json",
|
|
21
|
+
"CHANGELOG.md"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./src/index.ts",
|
|
25
|
+
"./cli": "./src/cli/index.ts",
|
|
26
|
+
"./cli/*": "./src/cli/*.ts",
|
|
27
|
+
"./commands": "./src/commands/index.ts",
|
|
28
|
+
"./commands/*": "./src/commands/*.ts",
|
|
29
|
+
"./generators": "./src/generators/index.ts",
|
|
30
|
+
"./generators/*": "./src/generators/*.ts"
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"strav": "./src/cli/strav.ts"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@strav/kernel": "0.1.0",
|
|
37
|
+
"@strav/http": "0.1.0",
|
|
38
|
+
"@strav/database": "0.1.0",
|
|
39
|
+
"@strav/queue": "0.1.0",
|
|
40
|
+
"@strav/signal": "0.1.0"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"chalk": "^5.6.2",
|
|
44
|
+
"commander": "^14.0.3",
|
|
45
|
+
"prettier": "^3.8.1"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"test": "bun test tests/",
|
|
49
|
+
"typecheck": "tsc --noEmit"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import Configuration from '@stravigor/kernel/config/configuration'
|
|
2
|
+
import Database from '@stravigor/database/database/database'
|
|
3
|
+
import SchemaRegistry from '@stravigor/database/schema/registry'
|
|
4
|
+
import DatabaseIntrospector from '@stravigor/database/database/introspector'
|
|
5
|
+
import Application from '@stravigor/kernel/core/application'
|
|
6
|
+
import type ServiceProvider from '@stravigor/kernel/core/service_provider'
|
|
7
|
+
import { discoverDomains } from '@stravigor/database'
|
|
8
|
+
import { getDatabasePaths } from '../config/loader.ts'
|
|
9
|
+
|
|
10
|
+
export interface BootstrapResult {
|
|
11
|
+
config: Configuration
|
|
12
|
+
db: Database
|
|
13
|
+
registry: SchemaRegistry
|
|
14
|
+
introspector: DatabaseIntrospector
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Bootstrap the core framework services needed by CLI commands.
|
|
19
|
+
*
|
|
20
|
+
* Loads configuration, connects to the database, discovers and validates
|
|
21
|
+
* schemas, and creates an introspector instance.
|
|
22
|
+
*
|
|
23
|
+
* @param scope - Optional domain to discover schemas from (e.g., 'public', 'tenant', 'factory')
|
|
24
|
+
*/
|
|
25
|
+
export async function bootstrap(scope?: string): Promise<BootstrapResult> {
|
|
26
|
+
const config = new Configuration('./config')
|
|
27
|
+
await config.load()
|
|
28
|
+
|
|
29
|
+
const db = new Database(config)
|
|
30
|
+
|
|
31
|
+
const registry = new SchemaRegistry()
|
|
32
|
+
|
|
33
|
+
// Get the configured database paths
|
|
34
|
+
const dbPaths = await getDatabasePaths()
|
|
35
|
+
|
|
36
|
+
if (scope && scope !== 'public') {
|
|
37
|
+
// For non-public domains, we need to load public schemas first since they may reference them
|
|
38
|
+
await registry.discover(dbPaths.schemas, 'public')
|
|
39
|
+
await registry.discover(dbPaths.schemas, scope)
|
|
40
|
+
} else if (scope === 'public') {
|
|
41
|
+
// For public schemas, only load public
|
|
42
|
+
await registry.discover(dbPaths.schemas, 'public')
|
|
43
|
+
} else {
|
|
44
|
+
// Default: discover all schemas (backward compatibility)
|
|
45
|
+
await registry.discover(dbPaths.schemas)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
registry.validate()
|
|
49
|
+
|
|
50
|
+
const introspector = new DatabaseIntrospector(db)
|
|
51
|
+
|
|
52
|
+
return { config, db, registry, introspector }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Cleanly close the database connection. */
|
|
56
|
+
export async function shutdown(db: Database): Promise<void> {
|
|
57
|
+
await db.close()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Bootstrap an Application with the given service providers.
|
|
62
|
+
*
|
|
63
|
+
* Creates a fresh Application, registers all providers, boots them
|
|
64
|
+
* in dependency order, and returns the running application.
|
|
65
|
+
* Signal handlers for graceful shutdown are installed automatically.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* const app = await withProviders([
|
|
69
|
+
* new ConfigProvider(),
|
|
70
|
+
* new DatabaseProvider(),
|
|
71
|
+
* new AuthProvider({ resolver: (id) => User.find(id) }),
|
|
72
|
+
* ])
|
|
73
|
+
*/
|
|
74
|
+
export async function withProviders(providers: ServiceProvider[]): Promise<Application> {
|
|
75
|
+
const app = new Application()
|
|
76
|
+
for (const provider of providers) app.use(provider)
|
|
77
|
+
await app.start()
|
|
78
|
+
return app
|
|
79
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { readdirSync, existsSync, realpathSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { Command } from 'commander'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Discovers and registers CLI commands from two sources:
|
|
8
|
+
*
|
|
9
|
+
* 1. **Package commands** — installed `@stravigor/*` packages that declare
|
|
10
|
+
* `"strav": { "commands": "src/commands" }` in their `package.json`.
|
|
11
|
+
* 2. **User commands** — `.ts` files in a `./commands/` directory at the
|
|
12
|
+
* project root.
|
|
13
|
+
*
|
|
14
|
+
* Every discovered file must export a `register(program: Command): void`
|
|
15
|
+
* function.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // In strav.ts:
|
|
19
|
+
* await CommandLoader.discover(program)
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // In a package (e.g. @stravigor/search):
|
|
23
|
+
* // package.json: { "strav": { "commands": "src/commands" } }
|
|
24
|
+
* // src/commands/search_import.ts:
|
|
25
|
+
* export function register(program: Command): void {
|
|
26
|
+
* program.command('search:import <model>').action(async () => { ... })
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // User-defined command:
|
|
31
|
+
* // commands/deploy.ts:
|
|
32
|
+
* export function register(program: Command): void {
|
|
33
|
+
* program.command('deploy').action(async () => { ... })
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
export default class CommandLoader {
|
|
37
|
+
/**
|
|
38
|
+
* Discover and register commands from packages and the user's commands directory.
|
|
39
|
+
*
|
|
40
|
+
* @param baseDir - Project root directory. Defaults to `process.cwd()`.
|
|
41
|
+
*/
|
|
42
|
+
static async discover(program: Command, baseDir?: string): Promise<void> {
|
|
43
|
+
const root = baseDir ?? process.cwd()
|
|
44
|
+
await this.loadPackageCommands(program, root)
|
|
45
|
+
await this.loadUserCommands(program, root)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Package commands ───────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
private static async loadPackageCommands(program: Command, root: string): Promise<void> {
|
|
51
|
+
const packages = await this.resolvePackages(root)
|
|
52
|
+
|
|
53
|
+
for (const { root: pkgRoot, commandsDir } of packages) {
|
|
54
|
+
try {
|
|
55
|
+
const dir = join(pkgRoot, commandsDir)
|
|
56
|
+
if (!dirExists(dir)) continue
|
|
57
|
+
|
|
58
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.ts'))
|
|
59
|
+
for (const file of files) {
|
|
60
|
+
const filePath = join(dir, file)
|
|
61
|
+
try {
|
|
62
|
+
const module = await import(filePath)
|
|
63
|
+
if (typeof module.register === 'function') {
|
|
64
|
+
module.register(program)
|
|
65
|
+
}
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(
|
|
68
|
+
chalk.yellow(
|
|
69
|
+
`Warning: Failed to load command "${file}": ${err instanceof Error ? err.message : err}`
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Skip packages whose commands directory can't be read
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── User commands ──────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
private static async loadUserCommands(program: Command, root: string): Promise<void> {
|
|
83
|
+
const userDir = join(root, 'commands')
|
|
84
|
+
if (!dirExists(userDir)) return
|
|
85
|
+
|
|
86
|
+
const files = readdirSync(userDir).filter(f => f.endsWith('.ts'))
|
|
87
|
+
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
const filePath = join(userDir, file)
|
|
90
|
+
try {
|
|
91
|
+
const module = await import(filePath)
|
|
92
|
+
if (typeof module.register === 'function') {
|
|
93
|
+
module.register(program)
|
|
94
|
+
}
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(
|
|
97
|
+
chalk.yellow(
|
|
98
|
+
`Warning: Failed to load command "${file}": ${err instanceof Error ? err.message : err}`
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Package resolution ─────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
private static async resolvePackages(
|
|
108
|
+
root: string
|
|
109
|
+
): Promise<Array<{ root: string; commandsDir: string }>> {
|
|
110
|
+
const results: Array<{ root: string; commandsDir: string }> = []
|
|
111
|
+
const seen = new Set<string>()
|
|
112
|
+
|
|
113
|
+
// 1. Check node_modules/@stravigor/*
|
|
114
|
+
const nodeModulesDir = join(root, 'node_modules', '@stravigor')
|
|
115
|
+
if (dirExists(nodeModulesDir)) {
|
|
116
|
+
const dirs = readdirSync(nodeModulesDir)
|
|
117
|
+
for (const dir of dirs) {
|
|
118
|
+
const pkgRoot = join(nodeModulesDir, dir)
|
|
119
|
+
const realRoot = realPath(pkgRoot)
|
|
120
|
+
if (seen.has(realRoot)) continue
|
|
121
|
+
seen.add(realRoot)
|
|
122
|
+
const commandsDir = await readStravCommands(pkgRoot)
|
|
123
|
+
if (commandsDir) results.push({ root: pkgRoot, commandsDir })
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2. Check Bun workspace packages
|
|
128
|
+
try {
|
|
129
|
+
const rootPkgPath = join(root, 'package.json')
|
|
130
|
+
if (existsSync(rootPkgPath)) {
|
|
131
|
+
const rootPkg = await Bun.file(rootPkgPath).json()
|
|
132
|
+
const workspaces: string[] = rootPkg.workspaces ?? []
|
|
133
|
+
for (const ws of workspaces) {
|
|
134
|
+
const wsPath = join(root, ws)
|
|
135
|
+
const realRoot = realPath(wsPath)
|
|
136
|
+
if (seen.has(realRoot)) continue
|
|
137
|
+
seen.add(realRoot)
|
|
138
|
+
const commandsDir = await readStravCommands(wsPath)
|
|
139
|
+
if (commandsDir) results.push({ root: wsPath, commandsDir })
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// No package.json or not in a workspace
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return results
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
/** Resolve symlinks to avoid double-loading workspace packages. */
|
|
153
|
+
function realPath(p: string): string {
|
|
154
|
+
try {
|
|
155
|
+
return realpathSync(p)
|
|
156
|
+
} catch {
|
|
157
|
+
return p
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function dirExists(path: string): boolean {
|
|
162
|
+
try {
|
|
163
|
+
readdirSync(path)
|
|
164
|
+
return true
|
|
165
|
+
} catch {
|
|
166
|
+
return false
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Read the `strav.commands` field from a package's `package.json`. */
|
|
171
|
+
async function readStravCommands(packageRoot: string): Promise<string | null> {
|
|
172
|
+
try {
|
|
173
|
+
const pkgPath = join(packageRoot, 'package.json')
|
|
174
|
+
if (!existsSync(pkgPath)) return null
|
|
175
|
+
const pkg = await Bun.file(pkgPath).json()
|
|
176
|
+
return pkg?.strav?.commands ?? null
|
|
177
|
+
} catch {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
}
|
package/src/cli/index.ts
ADDED
package/src/cli/strav.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import 'reflect-metadata'
|
|
2
|
+
import { Command } from 'commander'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import CommandLoader from './command_loader.ts'
|
|
5
|
+
|
|
6
|
+
const pkg = await Bun.file(join(import.meta.dir, '../../package.json')).json()
|
|
7
|
+
const program = new Command()
|
|
8
|
+
|
|
9
|
+
program.name('strav').description('Strav CLI').version(pkg.version)
|
|
10
|
+
|
|
11
|
+
await CommandLoader.discover(program)
|
|
12
|
+
|
|
13
|
+
program.parse()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import type { Command } from 'commander'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
6
|
+
import { freshDatabase, requireLocalEnv } from './migration_fresh.ts'
|
|
7
|
+
import { toSnakeCase } from '@stravigor/kernel/helpers/strings'
|
|
8
|
+
import BaseModel from '@stravigor/database/orm/base_model'
|
|
9
|
+
import { Seeder } from '@stravigor/database/database/seeder'
|
|
10
|
+
|
|
11
|
+
const SEEDERS_PATH = 'database/seeders'
|
|
12
|
+
|
|
13
|
+
export function register(program: Command): void {
|
|
14
|
+
program
|
|
15
|
+
.command('seed')
|
|
16
|
+
.alias('db:seed')
|
|
17
|
+
.description('Seed the database with records')
|
|
18
|
+
.option('-c, --class <name>', 'Run a specific seeder class')
|
|
19
|
+
.option('--fresh', 'Drop all tables and re-migrate before seeding')
|
|
20
|
+
.action(async ({ class: className, fresh }: { class?: string; fresh?: boolean }) => {
|
|
21
|
+
let db
|
|
22
|
+
try {
|
|
23
|
+
const { db: database, registry, introspector } = await bootstrap()
|
|
24
|
+
db = database
|
|
25
|
+
|
|
26
|
+
// Wire BaseModel so factories / seeders can use the ORM
|
|
27
|
+
new BaseModel(db)
|
|
28
|
+
|
|
29
|
+
// --fresh: reset the database first
|
|
30
|
+
if (fresh) {
|
|
31
|
+
requireLocalEnv('seed --fresh')
|
|
32
|
+
|
|
33
|
+
const applied = await freshDatabase(db, registry, introspector)
|
|
34
|
+
console.log(chalk.green(`\nFresh migration complete. Applied ${applied} migration(s).`))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Resolve the seeder file
|
|
38
|
+
const fileName = className
|
|
39
|
+
? toSnakeCase(className.replace(/Seeder$/i, '') || 'database') + '_seeder'
|
|
40
|
+
: 'database_seeder'
|
|
41
|
+
|
|
42
|
+
const seederPath = join(process.cwd(), SEEDERS_PATH, `${fileName}.ts`)
|
|
43
|
+
|
|
44
|
+
if (!existsSync(seederPath)) {
|
|
45
|
+
console.error(chalk.red(`Seeder not found: `) + chalk.dim(seederPath))
|
|
46
|
+
console.error(
|
|
47
|
+
chalk.dim(
|
|
48
|
+
` Run ${chalk.cyan(`strav generate:seeder ${className ?? 'DatabaseSeeder'}`)} to create it.`
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(chalk.cyan(`\nSeeding database...`))
|
|
55
|
+
|
|
56
|
+
const mod = await import(seederPath)
|
|
57
|
+
const SeederClass = mod.default
|
|
58
|
+
|
|
59
|
+
if (!SeederClass || !(SeederClass.prototype instanceof Seeder)) {
|
|
60
|
+
console.error(
|
|
61
|
+
chalk.red(`Error: `) + `Default export of ${fileName}.ts must extend Seeder.`
|
|
62
|
+
)
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const seeder = new SeederClass(db) as Seeder
|
|
67
|
+
await seeder.run()
|
|
68
|
+
|
|
69
|
+
console.log(chalk.green('Database seeding complete.'))
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
72
|
+
process.exit(1)
|
|
73
|
+
} finally {
|
|
74
|
+
if (db) await shutdown(db)
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import SchemaRegistry from '@stravigor/database/schema/registry'
|
|
5
|
+
import ApiGenerator from '../generators/api_generator.ts'
|
|
6
|
+
import RouteGenerator from '../generators/route_generator.ts'
|
|
7
|
+
import TestGenerator from '../generators/test_generator.ts'
|
|
8
|
+
import DocGenerator from '../generators/doc_generator.ts'
|
|
9
|
+
import type { ApiRoutingConfig } from '../generators/route_generator.ts'
|
|
10
|
+
import type { GeneratorConfig } from '../generators/config.ts'
|
|
11
|
+
import { loadGeneratorConfig, getDatabasePaths } from '../config/loader.ts'
|
|
12
|
+
|
|
13
|
+
export function register(program: Command): void {
|
|
14
|
+
program
|
|
15
|
+
.command('generate:api')
|
|
16
|
+
.alias('g:api')
|
|
17
|
+
.description(
|
|
18
|
+
'Generate services, controllers, policies, validators, events, and routes from schemas'
|
|
19
|
+
)
|
|
20
|
+
.action(async () => {
|
|
21
|
+
try {
|
|
22
|
+
console.log(chalk.cyan('Generating API layer from schemas...'))
|
|
23
|
+
|
|
24
|
+
// Get configured database paths
|
|
25
|
+
const dbPaths = await getDatabasePaths()
|
|
26
|
+
|
|
27
|
+
const registry = new SchemaRegistry()
|
|
28
|
+
await registry.discover(dbPaths.schemas)
|
|
29
|
+
registry.validate()
|
|
30
|
+
|
|
31
|
+
const schemas = registry.resolve()
|
|
32
|
+
const representation = registry.buildRepresentation()
|
|
33
|
+
|
|
34
|
+
// Load generator config (if available)
|
|
35
|
+
const config = await loadGeneratorConfig()
|
|
36
|
+
|
|
37
|
+
const apiGen = new ApiGenerator(schemas, representation, config)
|
|
38
|
+
const apiFiles = await apiGen.writeAll()
|
|
39
|
+
|
|
40
|
+
// Load API routing config from config/http.ts (if available)
|
|
41
|
+
let apiConfig: Partial<ApiRoutingConfig> | undefined
|
|
42
|
+
try {
|
|
43
|
+
const httpConfig = (await import(join(process.cwd(), 'config/http.ts'))).default
|
|
44
|
+
apiConfig = httpConfig.api
|
|
45
|
+
} catch {
|
|
46
|
+
// No config/http.ts or no api section — use defaults
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const routeGen = new RouteGenerator(schemas, config, apiConfig)
|
|
50
|
+
const routeFiles = await routeGen.writeAll()
|
|
51
|
+
|
|
52
|
+
const testGen = new TestGenerator(schemas, representation, config, apiConfig)
|
|
53
|
+
const testFiles = await testGen.writeAll()
|
|
54
|
+
|
|
55
|
+
const docGen = new DocGenerator(schemas, representation, config, apiConfig)
|
|
56
|
+
const docFiles = await docGen.writeAll()
|
|
57
|
+
|
|
58
|
+
const files = [...apiFiles, ...routeFiles, ...testFiles, ...docFiles]
|
|
59
|
+
|
|
60
|
+
if (files.length === 0) {
|
|
61
|
+
console.log(chalk.yellow('No API files to generate.'))
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(chalk.green(`\nGenerated ${files.length} file(s):`))
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
console.log(chalk.dim(` ${file.path}`))
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
71
|
+
process.exit(1)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
|
|
5
|
+
export function register(program: Command): void {
|
|
6
|
+
program
|
|
7
|
+
.command('generate:key')
|
|
8
|
+
.alias('g:key')
|
|
9
|
+
.description('Generate an APP_KEY and write it to the .env file')
|
|
10
|
+
.option('-f, --force', 'Overwrite existing APP_KEY if present')
|
|
11
|
+
.action(async ({ force }: { force?: boolean }) => {
|
|
12
|
+
try {
|
|
13
|
+
const key = crypto.randomUUID()
|
|
14
|
+
const envPath = join(process.cwd(), '.env')
|
|
15
|
+
const file = Bun.file(envPath)
|
|
16
|
+
|
|
17
|
+
if (await file.exists()) {
|
|
18
|
+
const contents = await file.text()
|
|
19
|
+
const hasKey = /^APP_KEY\s*=/m.test(contents)
|
|
20
|
+
|
|
21
|
+
if (hasKey && !force) {
|
|
22
|
+
const current = contents.match(/^APP_KEY\s*=\s*(.*)$/m)?.[1] ?? ''
|
|
23
|
+
if (current) {
|
|
24
|
+
console.log(chalk.yellow('APP_KEY already exists in .env. Use --force to overwrite.'))
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (hasKey) {
|
|
30
|
+
const updated = contents.replace(/^APP_KEY\s*=.*$/m, `APP_KEY=${key}`)
|
|
31
|
+
await Bun.write(envPath, updated)
|
|
32
|
+
} else {
|
|
33
|
+
const separator = contents.endsWith('\n') ? '' : '\n'
|
|
34
|
+
await Bun.write(envPath, contents + separator + `APP_KEY=${key}\n`)
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
await Bun.write(envPath, `APP_KEY=${key}\n`)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log(chalk.green('APP_KEY generated successfully.'))
|
|
41
|
+
console.log(chalk.dim(` ${key}`))
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import type { Command } from 'commander'
|
|
3
|
+
import chalk from 'chalk'
|
|
4
|
+
import SchemaRegistry from '@stravigor/database/schema/registry'
|
|
5
|
+
import ModelGenerator from '../generators/model_generator.ts'
|
|
6
|
+
import type { GeneratorConfig } from '../generators/config.ts'
|
|
7
|
+
import { discoverDomains } from '@stravigor/database'
|
|
8
|
+
import { loadGeneratorConfig, getDatabasePaths } from '../config/loader.ts'
|
|
9
|
+
|
|
10
|
+
export function register(program: Command): void {
|
|
11
|
+
program
|
|
12
|
+
.command('generate:models')
|
|
13
|
+
.alias('g:models')
|
|
14
|
+
.description('Generate model classes and enums from schema definitions')
|
|
15
|
+
.option('--scope <scope>', 'Generate models for specific domain or "all" for all domains', 'all')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
const scope = options.scope as string
|
|
19
|
+
|
|
20
|
+
// Get configured database paths
|
|
21
|
+
const dbPaths = await getDatabasePaths()
|
|
22
|
+
|
|
23
|
+
// Validate scope against available domains or 'all'
|
|
24
|
+
const availableDomains = discoverDomains(dbPaths.schemas)
|
|
25
|
+
if (scope !== 'all' && !availableDomains.includes(scope)) {
|
|
26
|
+
console.error(chalk.red(`Invalid domain: ${scope}. Available domains: ${availableDomains.join(', ')}, all`))
|
|
27
|
+
process.exit(1)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log(chalk.cyan(`Generating models from ${scope === 'all' ? 'all' : scope} schemas...`))
|
|
31
|
+
|
|
32
|
+
// Load generator config (if available)
|
|
33
|
+
const config = await loadGeneratorConfig()
|
|
34
|
+
|
|
35
|
+
const allFiles: any[] = []
|
|
36
|
+
const scopesToProcess = scope === 'all' ? availableDomains : [scope]
|
|
37
|
+
|
|
38
|
+
// When generating models for specific domains, we need all schemas loaded
|
|
39
|
+
// to handle cross-domain references properly
|
|
40
|
+
const fullRegistry = new SchemaRegistry()
|
|
41
|
+
|
|
42
|
+
// Always load public schemas first (base schemas)
|
|
43
|
+
await fullRegistry.discover(dbPaths.schemas, 'public')
|
|
44
|
+
|
|
45
|
+
// Load schemas from all other domains if needed for validation or if generating models for those domains
|
|
46
|
+
for (const domain of availableDomains) {
|
|
47
|
+
if (domain !== 'public' && (scope === 'all' || scope === domain)) {
|
|
48
|
+
await fullRegistry.discover(dbPaths.schemas, domain)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate all loaded schemas
|
|
53
|
+
fullRegistry.validate()
|
|
54
|
+
|
|
55
|
+
// Build representation from all loaded schemas
|
|
56
|
+
const fullRepresentation = fullRegistry.buildRepresentation()
|
|
57
|
+
|
|
58
|
+
// Build a map of schema name -> domain for cross-domain reference resolution
|
|
59
|
+
const allSchemasMap = new Map<string, string>()
|
|
60
|
+
|
|
61
|
+
// Load public schemas to identify which ones are public
|
|
62
|
+
const publicRegistry = new SchemaRegistry()
|
|
63
|
+
await publicRegistry.discover(dbPaths.schemas, 'public')
|
|
64
|
+
const publicSchemaNames = new Set(publicRegistry.all().map(s => s.name))
|
|
65
|
+
|
|
66
|
+
// Map schemas to their respective domains by checking which domain directory they came from
|
|
67
|
+
for (const schema of fullRegistry.all()) {
|
|
68
|
+
if (publicSchemaNames.has(schema.name)) {
|
|
69
|
+
allSchemasMap.set(schema.name, 'public')
|
|
70
|
+
} else {
|
|
71
|
+
// For non-public schemas, find which domain they belong to
|
|
72
|
+
// by checking which domains were loaded
|
|
73
|
+
for (const domain of availableDomains) {
|
|
74
|
+
if (domain !== 'public') {
|
|
75
|
+
const domainRegistry = new SchemaRegistry()
|
|
76
|
+
await domainRegistry.discover(dbPaths.schemas, domain)
|
|
77
|
+
const domainSchemaNames = new Set(domainRegistry.all().map(s => s.name))
|
|
78
|
+
if (domainSchemaNames.has(schema.name)) {
|
|
79
|
+
allSchemasMap.set(schema.name, domain)
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const currentScope of scopesToProcess) {
|
|
88
|
+
// Get just the schemas for the current scope
|
|
89
|
+
const scopeRegistry = new SchemaRegistry()
|
|
90
|
+
await scopeRegistry.discover(dbPaths.schemas, currentScope)
|
|
91
|
+
|
|
92
|
+
if (scopeRegistry.all().length === 0) {
|
|
93
|
+
console.log(chalk.yellow(`No schemas found for domain: ${currentScope}`))
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const scopeSchemas = scopeRegistry.all()
|
|
98
|
+
const generator = new ModelGenerator(scopeSchemas, fullRepresentation, config, currentScope, allSchemasMap)
|
|
99
|
+
const files = await generator.writeAll()
|
|
100
|
+
allFiles.push(...files)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (allFiles.length === 0) {
|
|
104
|
+
console.log(chalk.yellow('No models to generate.'))
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(chalk.green(`\nGenerated ${allFiles.length} file(s):`))
|
|
109
|
+
for (const file of allFiles) {
|
|
110
|
+
console.log(chalk.dim(` ${file.path}`))
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
114
|
+
process.exit(1)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
}
|