@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.
- package/package.json +17 -41
- package/src/binder.ts +88 -0
- package/src/command.ts +297 -0
- package/src/config_list.ts +42 -0
- package/src/config_show.ts +50 -0
- package/src/console_provider.ts +46 -0
- package/src/exit_codes.ts +26 -0
- package/src/index.ts +60 -2
- package/src/key_generate.ts +66 -0
- package/src/make/index.ts +17 -0
- package/src/make/make_command_file.ts +27 -0
- package/src/make/make_controller.ts +24 -0
- package/src/make/make_factory.ts +25 -0
- package/src/make/make_job.ts +25 -0
- package/src/make/make_mail.ts +27 -0
- package/src/make/make_middleware.ts +23 -0
- package/src/make/make_migration.ts +48 -0
- package/src/make/make_model.ts +91 -0
- package/src/make/make_notification.ts +23 -0
- package/src/make/make_policy.ts +24 -0
- package/src/make/make_provider.ts +29 -0
- package/src/make/make_repository.ts +30 -0
- package/src/make/make_request.ts +24 -0
- package/src/make/make_seeder.ts +23 -0
- package/src/make/make_test.ts +22 -0
- package/src/make_command.ts +69 -0
- package/src/run_cli.ts +121 -0
- package/src/scaffold_console_provider.ts +45 -0
- package/src/signature.ts +171 -0
- package/src/subset_boot.ts +51 -0
- package/src/util_console_provider.ts +18 -0
- package/src/cli/bootstrap.ts +0 -77
- package/src/cli/command_loader.ts +0 -180
- package/src/cli/index.ts +0 -3
- package/src/cli/strav.ts +0 -13
- package/src/commands/db_seed.ts +0 -77
- package/src/commands/db_setup_roles.ts +0 -101
- package/src/commands/generate_api.ts +0 -93
- package/src/commands/generate_key.ts +0 -47
- package/src/commands/generate_models.ts +0 -49
- package/src/commands/generate_seeder.ts +0 -68
- package/src/commands/migration_compare.ts +0 -167
- package/src/commands/migration_fresh.ts +0 -148
- package/src/commands/migration_generate.ts +0 -84
- package/src/commands/migration_rollback.ts +0 -54
- package/src/commands/migration_run.ts +0 -45
- package/src/commands/package_install.ts +0 -161
- package/src/commands/queue_flush.ts +0 -35
- package/src/commands/queue_retry.ts +0 -34
- package/src/commands/queue_work.ts +0 -101
- package/src/commands/scheduler_work.ts +0 -46
- package/src/commands/tenant_create.ts +0 -35
- package/src/commands/tenant_delete.ts +0 -64
- package/src/commands/tenant_list.ts +0 -39
- package/src/config/loader.ts +0 -50
- package/src/generators/api_generator.ts +0 -1035
- package/src/generators/config.ts +0 -113
- package/src/generators/doc_generator.ts +0 -996
- package/src/generators/index.ts +0 -11
- package/src/generators/model_generator.ts +0 -596
- package/src/generators/route_generator.ts +0 -187
- package/src/generators/test_generator.ts +0 -1667
- 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
|
-
}
|