@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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { readdirSync, mkdirSync, existsSync } from 'node:fs'
|
|
2
|
+
import { join, dirname, resolve } from 'node:path'
|
|
3
|
+
import type { Command } from 'commander'
|
|
4
|
+
import chalk from 'chalk'
|
|
5
|
+
|
|
6
|
+
export function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('install <name>')
|
|
9
|
+
.aliases(['package:install', 'i'])
|
|
10
|
+
.description('Copy config and schema stubs from a @stravigor/* package into your project')
|
|
11
|
+
.option('-f, --force', 'Overwrite existing files')
|
|
12
|
+
.action(async (name: string, { force }: { force?: boolean }) => {
|
|
13
|
+
try {
|
|
14
|
+
const packageName = name.startsWith('@stravigor/') ? name : `@stravigor/${name}`
|
|
15
|
+
|
|
16
|
+
const packageRoot = await resolvePackageRoot(packageName)
|
|
17
|
+
if (!packageRoot) {
|
|
18
|
+
console.error(chalk.red(`Package "${packageName}" is not installed.`))
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const stubsDir = join(packageRoot, 'stubs')
|
|
23
|
+
if (!dirExists(stubsDir)) {
|
|
24
|
+
console.log(chalk.yellow(`Package "${packageName}" has no stubs to install.`))
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let copied = 0
|
|
29
|
+
let skipped = 0
|
|
30
|
+
|
|
31
|
+
// Copy config stubs → ./config/
|
|
32
|
+
const configStubsDir = join(stubsDir, 'config')
|
|
33
|
+
if (dirExists(configStubsDir)) {
|
|
34
|
+
const result = await copyStubs(configStubsDir, join(process.cwd(), 'config'), force)
|
|
35
|
+
copied += result.copied
|
|
36
|
+
skipped += result.skipped
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Copy schema stubs → ./database/schemas/
|
|
40
|
+
const schemaStubsDir = join(stubsDir, 'schemas')
|
|
41
|
+
if (dirExists(schemaStubsDir)) {
|
|
42
|
+
const result = await copyStubs(
|
|
43
|
+
schemaStubsDir,
|
|
44
|
+
join(process.cwd(), 'database', 'schemas'),
|
|
45
|
+
force
|
|
46
|
+
)
|
|
47
|
+
copied += result.copied
|
|
48
|
+
skipped += result.skipped
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Copy action stubs → ./actions/
|
|
52
|
+
const actionStubsDir = join(stubsDir, 'actions')
|
|
53
|
+
if (dirExists(actionStubsDir)) {
|
|
54
|
+
const result = await copyStubs(actionStubsDir, join(process.cwd(), 'actions'), force)
|
|
55
|
+
copied += result.copied
|
|
56
|
+
skipped += result.skipped
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Copy email template stubs → ./resources/views/emails/
|
|
60
|
+
const emailStubsDir = join(stubsDir, 'emails')
|
|
61
|
+
if (dirExists(emailStubsDir)) {
|
|
62
|
+
const result = await copyStubs(
|
|
63
|
+
emailStubsDir,
|
|
64
|
+
join(process.cwd(), 'resources', 'views', 'emails'),
|
|
65
|
+
force,
|
|
66
|
+
['.strav']
|
|
67
|
+
)
|
|
68
|
+
copied += result.copied
|
|
69
|
+
skipped += result.skipped
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (copied === 0 && skipped === 0) {
|
|
73
|
+
console.log(chalk.yellow(`No stubs found in "${packageName}".`))
|
|
74
|
+
} else {
|
|
75
|
+
if (copied > 0) {
|
|
76
|
+
console.log(
|
|
77
|
+
chalk.green(`\nInstalled ${copied} file${copied > 1 ? 's' : ''} from ${packageName}.`)
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
if (skipped > 0) {
|
|
81
|
+
console.log(
|
|
82
|
+
chalk.dim(
|
|
83
|
+
`Skipped ${skipped} existing file${skipped > 1 ? 's' : ''}. Use --force to overwrite.`
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function resolvePackageRoot(packageName: string): Promise<string | null> {
|
|
96
|
+
// 1. Try node_modules (regular npm install)
|
|
97
|
+
const nodeModulesPath = join(process.cwd(), 'node_modules', ...packageName.split('/'))
|
|
98
|
+
if (existsSync(join(nodeModulesPath, 'package.json'))) {
|
|
99
|
+
return nodeModulesPath
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 2. Try workspace resolution (Bun workspaces / monorepo)
|
|
103
|
+
try {
|
|
104
|
+
const rootPkg = await Bun.file(resolve('package.json')).json()
|
|
105
|
+
const workspaces: string[] = rootPkg.workspaces ?? []
|
|
106
|
+
for (const ws of workspaces) {
|
|
107
|
+
const wsPath = resolve(ws)
|
|
108
|
+
const pkgPath = join(wsPath, 'package.json')
|
|
109
|
+
if (!existsSync(pkgPath)) continue
|
|
110
|
+
const wsPkg = await Bun.file(pkgPath).json()
|
|
111
|
+
if (wsPkg.name === packageName) return wsPath
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// No package.json or no workspaces
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function dirExists(path: string): boolean {
|
|
121
|
+
try {
|
|
122
|
+
readdirSync(path)
|
|
123
|
+
return true
|
|
124
|
+
} catch {
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function copyStubs(
|
|
130
|
+
srcDir: string,
|
|
131
|
+
destDir: string,
|
|
132
|
+
force?: boolean,
|
|
133
|
+
extensions: string[] = ['.ts']
|
|
134
|
+
): Promise<{ copied: number; skipped: number }> {
|
|
135
|
+
let copied = 0
|
|
136
|
+
let skipped = 0
|
|
137
|
+
|
|
138
|
+
const files = readdirSync(srcDir).filter(f => extensions.some(ext => f.endsWith(ext)))
|
|
139
|
+
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const srcPath = join(srcDir, file)
|
|
142
|
+
const destPath = join(destDir, file)
|
|
143
|
+
const destFile = Bun.file(destPath)
|
|
144
|
+
|
|
145
|
+
if ((await destFile.exists()) && !force) {
|
|
146
|
+
const relative = destPath.replace(process.cwd() + '/', '')
|
|
147
|
+
console.log(chalk.yellow(` SKIP ${relative} (already exists)`))
|
|
148
|
+
skipped++
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
mkdirSync(destDir, { recursive: true })
|
|
153
|
+
const content = await Bun.file(srcPath).text()
|
|
154
|
+
await Bun.write(destPath, content)
|
|
155
|
+
const relative = destPath.replace(process.cwd() + '/', '')
|
|
156
|
+
console.log(chalk.green(` CREATE ${relative}`))
|
|
157
|
+
copied++
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return { copied, skipped }
|
|
161
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import Queue from '@stravigor/queue/queue/queue'
|
|
5
|
+
|
|
6
|
+
export function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('queue:flush')
|
|
9
|
+
.description('Delete all jobs from a queue')
|
|
10
|
+
.option('--queue <name>', 'Queue to flush', 'default')
|
|
11
|
+
.option('--failed', 'Also flush failed jobs')
|
|
12
|
+
.action(async options => {
|
|
13
|
+
let db
|
|
14
|
+
try {
|
|
15
|
+
const { db: database, config } = await bootstrap()
|
|
16
|
+
db = database
|
|
17
|
+
|
|
18
|
+
new Queue(db, config)
|
|
19
|
+
await Queue.ensureTables()
|
|
20
|
+
|
|
21
|
+
const cleared = await Queue.clear(options.queue)
|
|
22
|
+
console.log(chalk.green(`Cleared ${cleared} pending job(s) from "${options.queue}".`))
|
|
23
|
+
|
|
24
|
+
if (options.failed) {
|
|
25
|
+
const failedCleared = await Queue.clearFailed(options.queue)
|
|
26
|
+
console.log(chalk.green(`Cleared ${failedCleared} failed job(s).`))
|
|
27
|
+
}
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
30
|
+
process.exit(1)
|
|
31
|
+
} finally {
|
|
32
|
+
if (db) await shutdown(db)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import Queue from '@stravigor/queue/queue/queue'
|
|
5
|
+
|
|
6
|
+
export function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command('queue:retry')
|
|
9
|
+
.description('Retry failed jobs by moving them back to the queue')
|
|
10
|
+
.option('--queue <name>', 'Only retry jobs from this queue')
|
|
11
|
+
.action(async options => {
|
|
12
|
+
let db
|
|
13
|
+
try {
|
|
14
|
+
const { db: database, config } = await bootstrap()
|
|
15
|
+
db = database
|
|
16
|
+
|
|
17
|
+
new Queue(db, config)
|
|
18
|
+
await Queue.ensureTables()
|
|
19
|
+
|
|
20
|
+
const count = await Queue.retryFailed(options.queue)
|
|
21
|
+
|
|
22
|
+
if (count === 0) {
|
|
23
|
+
console.log(chalk.green('No failed jobs to retry.'))
|
|
24
|
+
} else {
|
|
25
|
+
console.log(chalk.green(`Moved ${count} failed job(s) back to the queue.`))
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
29
|
+
process.exit(1)
|
|
30
|
+
} finally {
|
|
31
|
+
if (db) await shutdown(db)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
4
|
+
import Queue from '@stravigor/queue/queue/queue'
|
|
5
|
+
import Worker from '@stravigor/queue/queue/worker'
|
|
6
|
+
|
|
7
|
+
export function register(program: Command): void {
|
|
8
|
+
program
|
|
9
|
+
.command('queue:work')
|
|
10
|
+
.description('Start processing queued jobs')
|
|
11
|
+
.option('--queue <name>', 'Queue to process', 'default')
|
|
12
|
+
.option('--sleep <ms>', 'Poll interval in milliseconds', '1000')
|
|
13
|
+
.action(async options => {
|
|
14
|
+
let db
|
|
15
|
+
try {
|
|
16
|
+
const { db: database, config } = await bootstrap()
|
|
17
|
+
db = database
|
|
18
|
+
|
|
19
|
+
new Queue(db, config)
|
|
20
|
+
await Queue.ensureTables()
|
|
21
|
+
|
|
22
|
+
const queue = options.queue
|
|
23
|
+
const sleep = parseInt(options.sleep)
|
|
24
|
+
|
|
25
|
+
console.log(chalk.cyan(`Worker starting on queue "${queue}"...`))
|
|
26
|
+
console.log(chalk.dim(` poll interval: ${sleep}ms`))
|
|
27
|
+
console.log(chalk.dim(' Press Ctrl+C to stop.\n'))
|
|
28
|
+
|
|
29
|
+
const worker = new Worker({ queue, sleep })
|
|
30
|
+
await worker.start()
|
|
31
|
+
|
|
32
|
+
console.log(chalk.dim('\nWorker stopped.'))
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
35
|
+
process.exit(1)
|
|
36
|
+
} finally {
|
|
37
|
+
if (db) await shutdown(db)
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import chalk from 'chalk'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { bootstrap, shutdown } from '../cli/bootstrap.ts'
|
|
5
|
+
import Scheduler from '@stravigor/queue/scheduler/scheduler'
|
|
6
|
+
import SchedulerRunner from '@stravigor/queue/scheduler/runner'
|
|
7
|
+
|
|
8
|
+
export function register(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command('schedule')
|
|
11
|
+
.alias('scheduler:work')
|
|
12
|
+
.description('Start the task scheduler')
|
|
13
|
+
.action(async () => {
|
|
14
|
+
let db
|
|
15
|
+
try {
|
|
16
|
+
const { db: database } = await bootstrap()
|
|
17
|
+
db = database
|
|
18
|
+
|
|
19
|
+
// Load user's scheduled tasks
|
|
20
|
+
const schedulesPath = path.resolve('app/schedules.ts')
|
|
21
|
+
await import(schedulesPath)
|
|
22
|
+
|
|
23
|
+
const taskCount = Scheduler.tasks.length
|
|
24
|
+
if (taskCount === 0) {
|
|
25
|
+
console.log(chalk.yellow('No tasks registered. Add tasks in app/schedules.ts'))
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
console.log(chalk.cyan(`Scheduler starting with ${taskCount} task(s)...`))
|
|
30
|
+
for (const task of Scheduler.tasks) {
|
|
31
|
+
console.log(chalk.dim(` ${task.name}`))
|
|
32
|
+
}
|
|
33
|
+
console.log(chalk.dim(' Press Ctrl+C to stop.\n'))
|
|
34
|
+
|
|
35
|
+
const runner = new SchedulerRunner()
|
|
36
|
+
await runner.start()
|
|
37
|
+
|
|
38
|
+
console.log(chalk.dim('\nScheduler stopped.'))
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
|
|
41
|
+
process.exit(1)
|
|
42
|
+
} finally {
|
|
43
|
+
if (db) await shutdown(db)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import type { GeneratorConfig, GeneratorPaths } from '../generators/config.ts'
|
|
3
|
+
import { resolvePaths } from '../generators/config.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Load the generator configuration from the project's config/generators.ts file.
|
|
7
|
+
* Falls back to defaults if the file doesn't exist.
|
|
8
|
+
*/
|
|
9
|
+
export async function loadGeneratorConfig(): Promise<GeneratorConfig | undefined> {
|
|
10
|
+
try {
|
|
11
|
+
return (await import(join(process.cwd(), 'config/generators.ts'))).default
|
|
12
|
+
} catch {
|
|
13
|
+
// No config/generators.ts — use defaults
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get the fully resolved database paths from the configuration.
|
|
20
|
+
* This includes the schemas and migrations paths with defaults.
|
|
21
|
+
*/
|
|
22
|
+
export async function getDatabasePaths(): Promise<{ schemas: string; migrations: string }> {
|
|
23
|
+
const config = await loadGeneratorConfig()
|
|
24
|
+
const paths = resolvePaths(config)
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
schemas: paths.schemas,
|
|
28
|
+
migrations: paths.migrations,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get all resolved paths from the configuration.
|
|
34
|
+
* Useful for commands that need multiple paths.
|
|
35
|
+
*/
|
|
36
|
+
export async function getAllPaths(scope?: string): Promise<GeneratorPaths> {
|
|
37
|
+
const config = await loadGeneratorConfig()
|
|
38
|
+
return resolvePaths(config, scope)
|
|
39
|
+
}
|