@strav/cli 0.4.30 → 1.0.0-alpha.4

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.
Files changed (63) hide show
  1. package/package.json +17 -41
  2. package/src/binder.ts +88 -0
  3. package/src/command.ts +297 -0
  4. package/src/config_list.ts +42 -0
  5. package/src/config_show.ts +50 -0
  6. package/src/console_provider.ts +46 -0
  7. package/src/exit_codes.ts +26 -0
  8. package/src/index.ts +60 -2
  9. package/src/key_generate.ts +66 -0
  10. package/src/make/index.ts +17 -0
  11. package/src/make/make_command_file.ts +27 -0
  12. package/src/make/make_controller.ts +24 -0
  13. package/src/make/make_factory.ts +25 -0
  14. package/src/make/make_job.ts +25 -0
  15. package/src/make/make_mail.ts +27 -0
  16. package/src/make/make_middleware.ts +23 -0
  17. package/src/make/make_migration.ts +48 -0
  18. package/src/make/make_model.ts +91 -0
  19. package/src/make/make_notification.ts +23 -0
  20. package/src/make/make_policy.ts +24 -0
  21. package/src/make/make_provider.ts +29 -0
  22. package/src/make/make_repository.ts +30 -0
  23. package/src/make/make_request.ts +24 -0
  24. package/src/make/make_seeder.ts +23 -0
  25. package/src/make/make_test.ts +22 -0
  26. package/src/make_command.ts +69 -0
  27. package/src/run_cli.ts +121 -0
  28. package/src/scaffold_console_provider.ts +45 -0
  29. package/src/signature.ts +171 -0
  30. package/src/subset_boot.ts +51 -0
  31. package/src/util_console_provider.ts +18 -0
  32. package/src/cli/bootstrap.ts +0 -77
  33. package/src/cli/command_loader.ts +0 -180
  34. package/src/cli/index.ts +0 -3
  35. package/src/cli/strav.ts +0 -13
  36. package/src/commands/db_seed.ts +0 -77
  37. package/src/commands/db_setup_roles.ts +0 -101
  38. package/src/commands/generate_api.ts +0 -93
  39. package/src/commands/generate_key.ts +0 -47
  40. package/src/commands/generate_models.ts +0 -49
  41. package/src/commands/generate_seeder.ts +0 -68
  42. package/src/commands/migration_compare.ts +0 -167
  43. package/src/commands/migration_fresh.ts +0 -148
  44. package/src/commands/migration_generate.ts +0 -84
  45. package/src/commands/migration_rollback.ts +0 -54
  46. package/src/commands/migration_run.ts +0 -45
  47. package/src/commands/package_install.ts +0 -161
  48. package/src/commands/queue_flush.ts +0 -35
  49. package/src/commands/queue_retry.ts +0 -34
  50. package/src/commands/queue_work.ts +0 -101
  51. package/src/commands/scheduler_work.ts +0 -46
  52. package/src/commands/tenant_create.ts +0 -35
  53. package/src/commands/tenant_delete.ts +0 -64
  54. package/src/commands/tenant_list.ts +0 -39
  55. package/src/config/loader.ts +0 -50
  56. package/src/generators/api_generator.ts +0 -1035
  57. package/src/generators/config.ts +0 -113
  58. package/src/generators/doc_generator.ts +0 -996
  59. package/src/generators/index.ts +0 -11
  60. package/src/generators/model_generator.ts +0 -596
  61. package/src/generators/route_generator.ts +0 -187
  62. package/src/generators/test_generator.ts +0 -1667
  63. package/tsconfig.json +0 -5
@@ -1,148 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { createInterface } from 'node:readline'
4
- import { rmSync } from 'node:fs'
5
- import { bootstrap, shutdown } from '../cli/bootstrap.ts'
6
- import type Database from '@strav/database/database/database'
7
- import type SchemaRegistry from '@strav/database/schema/registry'
8
- import type DatabaseIntrospector from '@strav/database/database/introspector'
9
- import SchemaDiffer from '@strav/database/database/migration/differ'
10
- import SqlGenerator from '@strav/database/database/migration/sql_generator'
11
- import MigrationFileGenerator from '@strav/database/database/migration/file_generator'
12
- import MigrationTracker from '@strav/database/database/migration/tracker'
13
- import MigrationRunner from '@strav/database/database/migration/runner'
14
- import { getDatabasePaths } from '../config/loader.ts'
15
-
16
- /**
17
- * Drop all tables and enum types, regenerate a single migration from
18
- * the current schema definitions, and run it.
19
- *
20
- * Shared by `fresh` and `seed --fresh`.
21
- */
22
- export async function freshDatabase(
23
- db: Database,
24
- registry: SchemaRegistry,
25
- introspector: DatabaseIntrospector,
26
- migrationsPath: string = 'database/migrations'
27
- ): Promise<number> {
28
- console.log(chalk.cyan('\nDropping all tables and types...'))
29
-
30
- const conn = db.isMultiTenant ? db.bypass : db.sql
31
-
32
- const tables = await conn.unsafe(`
33
- SELECT table_name FROM information_schema.tables
34
- WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
35
- `)
36
- for (const row of tables as Array<{ table_name: string }>) {
37
- await conn.unsafe(`DROP TABLE IF EXISTS "${row.table_name}" CASCADE`)
38
- }
39
-
40
- const types = await conn.unsafe(`
41
- SELECT t.typname
42
- FROM pg_type t
43
- JOIN pg_namespace n ON n.oid = t.typnamespace
44
- WHERE n.nspname = 'public'
45
- AND t.typtype = 'e'
46
- `)
47
- for (const row of types as Array<{ typname: string }>) {
48
- await conn.unsafe(`DROP TYPE IF EXISTS "${row.typname}" CASCADE`)
49
- }
50
-
51
- console.log(chalk.cyan('Clearing migration directory...'))
52
- rmSync(migrationsPath, { recursive: true, force: true })
53
-
54
- console.log(chalk.cyan('Generating fresh migration...'))
55
-
56
- const desired = registry.buildRepresentation(db.tenantIdType)
57
- const actual = await introspector.introspect()
58
- const diff = new SchemaDiffer().diff(desired, actual)
59
-
60
- const sql = new SqlGenerator(
61
- db.tenantIdType,
62
- db.tenantTableName,
63
- db.tenantFkColumn
64
- ).generate(diff)
65
- const version = Date.now().toString()
66
- const tableOrder = desired.tables.map(t => t.name)
67
-
68
- const fileGen = new MigrationFileGenerator(migrationsPath)
69
- await fileGen.generate(version, 'fresh', sql, diff, tableOrder)
70
-
71
- console.log(chalk.cyan('Running migration...'))
72
-
73
- const tracker = new MigrationTracker(db)
74
- const runner = new MigrationRunner(db, tracker, migrationsPath)
75
- const result = await runner.run()
76
-
77
- return result.applied.length
78
- }
79
-
80
- /**
81
- * Guard that ensures APP_ENV is "local". Exits the process if not.
82
- */
83
- export function requireLocalEnv(commandName: string): void {
84
- const appEnv = process.env.APP_ENV
85
- if (appEnv !== 'local') {
86
- console.error(
87
- chalk.red('REJECTED: ') + `${commandName} can only run when APP_ENV is set to "local".`
88
- )
89
- if (!appEnv) {
90
- console.error(chalk.dim(' APP_ENV is not defined in .env'))
91
- } else {
92
- console.error(chalk.dim(` Current APP_ENV: "${appEnv}"`))
93
- }
94
- process.exit(1)
95
- }
96
- }
97
-
98
- export function register(program: Command): void {
99
- program
100
- .command('fresh')
101
- .alias('migration:fresh')
102
- .description('Reset database and migrations, regenerate and run from scratch')
103
- .action(async () => {
104
- requireLocalEnv('fresh')
105
-
106
- // 6-digit challenge
107
- const challenge = String(Math.floor(100000 + Math.random() * 900000))
108
- console.log(
109
- chalk.red('WARNING: ') +
110
- 'This will ' +
111
- chalk.red('destroy ALL data') +
112
- ' in the database and recreate everything from schemas.'
113
- )
114
- console.log(`\n Type ${chalk.yellow(challenge)} to confirm:\n`)
115
-
116
- const answer = await prompt(' > ')
117
- if (answer.trim() !== challenge) {
118
- console.error(chalk.red('\nChallenge code does not match. Operation cancelled.'))
119
- process.exit(1)
120
- }
121
-
122
- let db
123
- try {
124
- const dbPaths = await getDatabasePaths()
125
- const { db: database, registry, introspector } = await bootstrap()
126
- db = database
127
-
128
- const applied = await freshDatabase(db, registry, introspector, dbPaths.migrations)
129
-
130
- console.log(chalk.green(`\nFresh migration complete. Applied ${applied} migration(s).`))
131
- } catch (err) {
132
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
133
- process.exit(1)
134
- } finally {
135
- if (db) await shutdown(db)
136
- }
137
- })
138
- }
139
-
140
- function prompt(question: string): Promise<string> {
141
- const rl = createInterface({ input: process.stdin, output: process.stdout })
142
- return new Promise(resolve => {
143
- rl.question(question, answer => {
144
- rl.close()
145
- resolve(answer)
146
- })
147
- })
148
- }
@@ -1,84 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
- import SchemaDiffer from '@strav/database/database/migration/differ'
5
- import SqlGenerator from '@strav/database/database/migration/sql_generator'
6
- import MigrationFileGenerator from '@strav/database/database/migration/file_generator'
7
- import { getDatabasePaths } from '../config/loader.ts'
8
-
9
- export function register(program: Command): void {
10
- program
11
- .command('generate:migration')
12
- .aliases(['migration:generate', 'g:migration'])
13
- .description('Generate migration files from schema changes')
14
- .option('-m, --message <message>', 'Migration message', 'migration')
15
- .action(async (opts: { message: string }) => {
16
- let db
17
- try {
18
- const dbPaths = await getDatabasePaths()
19
- const { db: database, registry, introspector } = await bootstrap()
20
- db = database
21
-
22
- console.log(chalk.cyan('Comparing schema with database...'))
23
-
24
- // Tenant table name + idType are read from the registered tenant
25
- // schema (the one with `tenantRegistry: true`). Defaults to
26
- // `tenant` / `bigint` if no schema is registered.
27
- const desired = registry.buildRepresentation()
28
- const actual = await introspector.introspect()
29
- const diff = new SchemaDiffer().diff(desired, actual)
30
-
31
- const hasChanges =
32
- diff.enums.length > 0 ||
33
- diff.tables.length > 0 ||
34
- diff.constraints.length > 0 ||
35
- diff.indexes.length > 0
36
-
37
- if (!hasChanges) {
38
- console.log(chalk.green('No changes detected. Schema is in sync with the database.'))
39
- return
40
- }
41
-
42
- const sql = new SqlGenerator(
43
- database.tenantIdType,
44
- database.tenantTableName,
45
- database.tenantFkColumn
46
- ).generate(diff)
47
- const version = Date.now().toString()
48
- const tableOrder = desired.tables.map(t => t.name)
49
-
50
- const fileGen = new MigrationFileGenerator(dbPaths.migrations)
51
- const dir = await fileGen.generate(version, opts.message, sql, diff, tableOrder)
52
-
53
- console.log(chalk.green(`\nMigration generated: ${version}`))
54
- console.log(chalk.dim(` Directory: ${dir}`))
55
- console.log(chalk.dim(` Message: ${opts.message}\n`))
56
-
57
- const counts: string[] = []
58
- const creates = diff.tables.filter(t => t.kind === 'create').length
59
- const drops = diff.tables.filter(t => t.kind === 'drop').length
60
- const modifies = diff.tables.filter(t => t.kind === 'modify').length
61
- if (creates > 0) counts.push(chalk.green(`${creates} table(s) to create`))
62
- if (drops > 0) counts.push(chalk.red(`${drops} table(s) to drop`))
63
- if (modifies > 0) counts.push(chalk.yellow(`${modifies} table(s) to modify`))
64
-
65
- const enumCreates = diff.enums.filter(e => e.kind === 'create').length
66
- const enumDrops = diff.enums.filter(e => e.kind === 'drop').length
67
- const enumModifies = diff.enums.filter(e => e.kind === 'modify').length
68
- if (enumCreates > 0) counts.push(chalk.green(`${enumCreates} enum(s) to create`))
69
- if (enumDrops > 0) counts.push(chalk.red(`${enumDrops} enum(s) to drop`))
70
- if (enumModifies > 0) counts.push(chalk.yellow(`${enumModifies} enum(s) to modify`))
71
-
72
- if (diff.constraints.length > 0)
73
- counts.push(`${diff.constraints.length} constraint change(s)`)
74
- if (diff.indexes.length > 0) counts.push(`${diff.indexes.length} index change(s)`)
75
-
76
- console.log(' Summary: ' + counts.join(', '))
77
- } catch (err) {
78
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
79
- process.exit(1)
80
- } finally {
81
- if (db) await shutdown(db)
82
- }
83
- })
84
- }
@@ -1,54 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
- import MigrationTracker from '@strav/database/database/migration/tracker'
5
- import MigrationRunner from '@strav/database/database/migration/runner'
6
- import { getDatabasePaths } from '../config/loader.ts'
7
-
8
- export function register(program: Command): void {
9
- program
10
- .command('rollback')
11
- .alias('migration:rollback')
12
- .description('Rollback migrations by batch')
13
- .option('--batch <number>', 'Specific batch number to rollback')
14
- .action(async (opts: { batch?: string }) => {
15
- let db
16
- try {
17
- const dbPaths = await getDatabasePaths()
18
- const { db: database } = await bootstrap()
19
- db = database
20
-
21
- const tracker = new MigrationTracker(db)
22
- const runner = new MigrationRunner(db, tracker, dbPaths.migrations)
23
-
24
- const batchNum = opts.batch ? parseInt(opts.batch, 10) : undefined
25
-
26
- console.log(
27
- batchNum
28
- ? chalk.cyan(`Rolling back batch ${batchNum}...`)
29
- : chalk.cyan('Rolling back last batch...')
30
- )
31
-
32
- const result = await runner.rollback(batchNum)
33
-
34
- if (result.rolledBack.length === 0) {
35
- console.log(chalk.yellow('Nothing to rollback.'))
36
- return
37
- }
38
-
39
- console.log(
40
- chalk.green(
41
- `\nRolled back ${result.rolledBack.length} migration(s) from batch ${result.batch}:`
42
- )
43
- )
44
- for (const version of result.rolledBack) {
45
- console.log(chalk.dim(` - ${version}`))
46
- }
47
- } catch (err) {
48
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
49
- process.exit(1)
50
- } finally {
51
- if (db) await shutdown(db)
52
- }
53
- })
54
- }
@@ -1,45 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
- import MigrationTracker from '@strav/database/database/migration/tracker'
5
- import MigrationRunner from '@strav/database/database/migration/runner'
6
- import { getDatabasePaths } from '../config/loader.ts'
7
-
8
- export function register(program: Command): void {
9
- program
10
- .command('migrate')
11
- .alias('migration:run')
12
- .description('Run pending migrations')
13
- .action(async () => {
14
- let db
15
- try {
16
- const dbPaths = await getDatabasePaths()
17
- const { db: database } = await bootstrap()
18
- db = database
19
-
20
- const tracker = new MigrationTracker(db)
21
- const runner = new MigrationRunner(db, tracker, dbPaths.migrations)
22
-
23
- console.log(chalk.cyan('Running pending migrations...'))
24
-
25
- const result = await runner.run()
26
-
27
- if (result.applied.length === 0) {
28
- console.log(chalk.green('Nothing to migrate. All migrations are up to date.'))
29
- return
30
- }
31
-
32
- console.log(
33
- chalk.green(`\nApplied ${result.applied.length} migration(s) in batch ${result.batch}:`)
34
- )
35
- for (const version of result.applied) {
36
- console.log(chalk.dim(` - ${version}`))
37
- }
38
- } catch (err) {
39
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
40
- process.exit(1)
41
- } finally {
42
- if (db) await shutdown(db)
43
- }
44
- })
45
- }
@@ -1,161 +0,0 @@
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 @strav/* 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('@strav/') ? name : `@strav/${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
- }
@@ -1,35 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
- import Queue from '@strav/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
- }
@@ -1,34 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import { bootstrap, shutdown } from '../cli/bootstrap.ts'
4
- import Queue from '@strav/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
- }
@@ -1,101 +0,0 @@
1
- import type { Command } from 'commander'
2
- import chalk from 'chalk'
3
- import path from 'node:path'
4
- import { bootstrap, shutdown, withProviders } from '../cli/bootstrap.ts'
5
- import type Application from '@strav/kernel/core/application'
6
- import type ServiceProvider from '@strav/kernel/core/service_provider'
7
- import type Database from '@strav/database/database/database'
8
- import Queue from '@strav/queue/queue/queue'
9
- import QueueProvider from '@strav/queue/providers/queue_provider'
10
- import Worker from '@strav/queue/queue/worker'
11
-
12
- /**
13
- * Conventional file the worker loads on start. Its top-level `Queue.handle(...)`
14
- * calls register the job handlers, and its `providers` export lists the service
15
- * providers the worker boots — mirroring `start/providers.ts` for the web entry.
16
- */
17
- const JOBS_FILE = 'start/jobs.ts'
18
-
19
- export function register(program: Command): void {
20
- program
21
- .command('queue:work')
22
- .description('Start processing queued jobs')
23
- .option('--queue <name>', 'Queue to process', 'default')
24
- .option('--sleep <ms>', 'Poll interval in milliseconds', '1000')
25
- .action(async options => {
26
- const queue = options.queue
27
- const sleep = parseInt(options.sleep)
28
-
29
- let app: Application | undefined
30
- let db: Database | undefined
31
-
32
- try {
33
- const jobsPath = path.resolve(JOBS_FILE)
34
-
35
- if (await Bun.file(jobsPath).exists()) {
36
- // Importing start/jobs.ts runs its top-level Queue.handle(...) calls
37
- // and exposes the provider list the worker needs. Booting those
38
- // providers wires the facades (mail, notification, …) that handlers
39
- // depend on — bootstrap() alone leaves them unregistered.
40
- const jobs = await import(jobsPath)
41
- const providers: ServiceProvider[] = Array.isArray(jobs.providers)
42
- ? [...jobs.providers]
43
- : []
44
-
45
- if (providers.length === 0) {
46
- throw new Error(
47
- `${JOBS_FILE} must export a \`providers\` array — the service ` +
48
- `providers the worker boots (ConfigProvider, DatabaseProvider, ` +
49
- `QueueProvider, plus any facade providers your handlers use).`,
50
- )
51
- }
52
-
53
- // QueueProvider wires the Queue singleton and ensures the tables —
54
- // add it if the app's provider list left it out.
55
- if (!providers.some(p => p.name === 'queue')) {
56
- providers.push(new QueueProvider())
57
- }
58
-
59
- // signalHandlers: false — the Worker installs its own SIGINT/SIGTERM
60
- // handlers that drain the in-flight job before start() returns. We
61
- // call app.shutdown() only afterwards (in finally), so providers are
62
- // never torn down while a job is still running.
63
- app = await withProviders(providers, { signalHandlers: false })
64
- } else {
65
- console.warn(
66
- chalk.yellow(`No ${JOBS_FILE} found — the worker has no registered handlers.`),
67
- )
68
- console.warn(
69
- chalk.dim(
70
- ` Create ${JOBS_FILE} with your Queue.handle(...) calls and a ` +
71
- `\`providers\` export, or every dispatched job will fail.`,
72
- ),
73
- )
74
- const { db: database, config } = await bootstrap()
75
- db = database
76
- new Queue(db, config)
77
- await Queue.ensureTables()
78
- }
79
-
80
- const handlers = [...Queue.handlers.keys()]
81
-
82
- console.log(chalk.cyan(`Worker starting on queue "${queue}"...`))
83
- console.log(chalk.dim(` poll interval: ${sleep}ms`))
84
- console.log(
85
- chalk.dim(` handlers: ${handlers.length > 0 ? handlers.join(', ') : 'none'}`),
86
- )
87
- console.log(chalk.dim(' Press Ctrl+C to stop.\n'))
88
-
89
- const worker = new Worker({ queue, sleep })
90
- await worker.start()
91
-
92
- console.log(chalk.dim('\nWorker stopped.'))
93
- } catch (err) {
94
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : err}`))
95
- process.exit(1)
96
- } finally {
97
- if (app) await app.shutdown()
98
- if (db) await shutdown(db)
99
- }
100
- })
101
- }