@strav/cli 0.4.31 → 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 -82
- 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
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `bun strav key:generate` — generate APP_KEY and write it to `.env`.
|
|
3
|
+
*
|
|
4
|
+
* Generates 32 cryptographically-random bytes and hex-encodes them (64
|
|
5
|
+
* chars). This format matches `parseEncryptionKey()` in `@strav/kernel/encryption`
|
|
6
|
+
* so the value can be used directly as `config.encryption.key`.
|
|
7
|
+
*
|
|
8
|
+
* Behaviour:
|
|
9
|
+
* - If `.env` doesn't exist, creates it with `APP_KEY=<key>`.
|
|
10
|
+
* - If `.env` exists and already has `APP_KEY=`, updates the line.
|
|
11
|
+
* - If `.env` exists without `APP_KEY=`, appends the line.
|
|
12
|
+
* - `--show` prints the key to stdout instead of writing to disk
|
|
13
|
+
* (useful when your secret manager reads from stdout).
|
|
14
|
+
* - `--force` regenerates and overwrites even when APP_KEY is already set.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync } from 'node:fs'
|
|
18
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
19
|
+
import { join } from 'node:path'
|
|
20
|
+
import { Command, type ExecuteArgs } from './command.ts'
|
|
21
|
+
import { ExitCode } from './exit_codes.ts'
|
|
22
|
+
|
|
23
|
+
export class KeyGenerate extends Command {
|
|
24
|
+
static signature = 'key:generate {--show} {--force}'
|
|
25
|
+
static description = 'Generate APP_KEY and write it to .env.'
|
|
26
|
+
static providers: string[] = []
|
|
27
|
+
|
|
28
|
+
override async execute({ flags }: ExecuteArgs): Promise<number> {
|
|
29
|
+
const envPath = join(process.cwd(), '.env')
|
|
30
|
+
const show = flags.show === true
|
|
31
|
+
const force = flags.force === true
|
|
32
|
+
|
|
33
|
+
// Read existing .env content (may not exist)
|
|
34
|
+
let existing = existsSync(envPath) ? await readFile(envPath, 'utf8') : ''
|
|
35
|
+
|
|
36
|
+
// Guard: don't overwrite unless --force
|
|
37
|
+
if (!force && existing.match(/^APP_KEY=.+/m)) {
|
|
38
|
+
this.warn('APP_KEY already set in .env. Use --force to overwrite.')
|
|
39
|
+
return ExitCode.Success
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Generate 32 random bytes → 64-char hex string
|
|
43
|
+
const bytes = new Uint8Array(32)
|
|
44
|
+
crypto.getRandomValues(bytes)
|
|
45
|
+
const key = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
46
|
+
|
|
47
|
+
if (show) {
|
|
48
|
+
this.line(`APP_KEY=${key}`)
|
|
49
|
+
return ExitCode.Success
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (existing.match(/^APP_KEY=/m)) {
|
|
53
|
+
// Replace the existing APP_KEY line
|
|
54
|
+
existing = existing.replace(/^APP_KEY=.*$/m, `APP_KEY=${key}`)
|
|
55
|
+
} else {
|
|
56
|
+
// Append (with a trailing newline)
|
|
57
|
+
existing = existing.trimEnd()
|
|
58
|
+
existing = existing ? `${existing}\nAPP_KEY=${key}\n` : `APP_KEY=${key}\n`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await writeFile(envPath, existing, 'utf8')
|
|
62
|
+
this.success(`APP_KEY written to ${envPath}`)
|
|
63
|
+
this.info('Add it to your shell with: source .env (or use dotenv)')
|
|
64
|
+
return ExitCode.Success
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// make:* scaffold commands — one class per target file type.
|
|
2
|
+
|
|
3
|
+
export { MakeCommandFile } from './make_command_file.ts'
|
|
4
|
+
export { MakeController } from './make_controller.ts'
|
|
5
|
+
export { MakeFactory } from './make_factory.ts'
|
|
6
|
+
export { MakeJob } from './make_job.ts'
|
|
7
|
+
export { MakeMail } from './make_mail.ts'
|
|
8
|
+
export { MakeMiddleware } from './make_middleware.ts'
|
|
9
|
+
export { MakeMigration } from './make_migration.ts'
|
|
10
|
+
export { MakeModel } from './make_model.ts'
|
|
11
|
+
export { MakeNotification } from './make_notification.ts'
|
|
12
|
+
export { MakePolicy } from './make_policy.ts'
|
|
13
|
+
export { MakeProvider } from './make_provider.ts'
|
|
14
|
+
export { MakeRepository } from './make_repository.ts'
|
|
15
|
+
export { MakeRequest } from './make_request.ts'
|
|
16
|
+
export { MakeSeeder } from './make_seeder.ts'
|
|
17
|
+
export { MakeTest } from './make_test.ts'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeCommandFile extends MakeCommand {
|
|
4
|
+
static signature = 'make:command {name}'
|
|
5
|
+
static description = 'Create a console Command stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `app/console/commands/${snake(name)}.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
const cls = pascal(name)
|
|
14
|
+
return `import { Command, type ExecuteArgs, ExitCode } from '@strav/cli'
|
|
15
|
+
|
|
16
|
+
export class ${cls} extends Command {
|
|
17
|
+
static signature = '${snake(name).replace(/_/g, ':')}'
|
|
18
|
+
static description = 'Describe what this command does.'
|
|
19
|
+
|
|
20
|
+
override async execute({ args, flags }: ExecuteArgs): Promise<number> {
|
|
21
|
+
// implement the command here
|
|
22
|
+
return ExitCode.Success
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeController extends MakeCommand {
|
|
4
|
+
static signature = 'make:controller {name}'
|
|
5
|
+
static description = 'Create a new HTTP controller stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
const fileName = name.toLowerCase().endsWith('controller') ? snake(name) : `${snake(name)}_controller`
|
|
10
|
+
return `app/http/controllers/${fileName}.ts`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected stub(name: string): string {
|
|
14
|
+
const cls = pascal(name).endsWith('Controller') ? pascal(name) : `${pascal(name)}Controller`
|
|
15
|
+
return `import type { HttpContext } from '@strav/http'
|
|
16
|
+
|
|
17
|
+
export class ${cls} {
|
|
18
|
+
async handle(ctx: HttpContext): Promise<Response> {
|
|
19
|
+
return ctx.response.ok('Hello from ${cls}')
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { camel, MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeFactory extends MakeCommand {
|
|
4
|
+
static signature = 'make:factory {name}'
|
|
5
|
+
static description = 'Create a factory stub for a model.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
const base = snake(name).replace(/_factory$/, '')
|
|
10
|
+
return `database/factories/${base}_factory.ts`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected stub(name: string): string {
|
|
14
|
+
const base = pascal(name).replace(/Factory$/, '')
|
|
15
|
+
return `import type { ${base} } from '../../app/models/${snake(base)}.ts'
|
|
16
|
+
|
|
17
|
+
export function ${camel(base)}Factory(overrides: Partial<${base}> = {}): ${base} {
|
|
18
|
+
return {
|
|
19
|
+
// define default attribute values here
|
|
20
|
+
...overrides,
|
|
21
|
+
} as ${base}
|
|
22
|
+
}
|
|
23
|
+
`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeJob extends MakeCommand {
|
|
4
|
+
static signature = 'make:job {name}'
|
|
5
|
+
static description = 'Create a queue job stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `app/jobs/${snake(name)}.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
const cls = pascal(name)
|
|
14
|
+
return `import { Job, type JobContext } from '@strav/queue'
|
|
15
|
+
|
|
16
|
+
export class ${cls} extends Job<unknown> {
|
|
17
|
+
static override readonly jobName = '${snake(name)}'
|
|
18
|
+
|
|
19
|
+
async handle(ctx: JobContext<unknown>): Promise<void> {
|
|
20
|
+
// handle the job
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
`
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeMail extends MakeCommand {
|
|
4
|
+
static signature = 'make:mail {name}'
|
|
5
|
+
static description = 'Create a Mailable stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `app/mail/${snake(name)}.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
const cls = pascal(name)
|
|
14
|
+
return `import { Mailable, type Message } from '@strav/signal'
|
|
15
|
+
|
|
16
|
+
export class ${cls} extends Mailable<unknown> {
|
|
17
|
+
build(payload: unknown): Message {
|
|
18
|
+
return {
|
|
19
|
+
to: [{ address: 'recipient@example.com' }],
|
|
20
|
+
subject: '${pascal(name)}',
|
|
21
|
+
html: '<p>Hello</p>',
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
`
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeMiddleware extends MakeCommand {
|
|
4
|
+
static signature = 'make:middleware {name}'
|
|
5
|
+
static description = 'Create a new HTTP middleware stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `app/http/middleware/${snake(name)}.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
const cls = pascal(name)
|
|
14
|
+
return `import type { HttpContext, MiddlewareNext } from '@strav/http'
|
|
15
|
+
|
|
16
|
+
export class ${cls} {
|
|
17
|
+
async handle(ctx: HttpContext, next: MiddlewareNext): Promise<Response> {
|
|
18
|
+
return next(ctx)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { UsageError } from '../binder.ts'
|
|
2
|
+
import type { ExecuteArgs } from '../command.ts'
|
|
3
|
+
import { MakeCommand } from '../make_command.ts'
|
|
4
|
+
|
|
5
|
+
export class MakeMigration extends MakeCommand {
|
|
6
|
+
static signature = 'make:migration {--message=}'
|
|
7
|
+
static description = 'Create a migration file stub.'
|
|
8
|
+
static providers: string[] = []
|
|
9
|
+
|
|
10
|
+
protected filePath(name: string): string {
|
|
11
|
+
return `database/migrations/${name}.ts`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
protected stub(name: string): string {
|
|
15
|
+
return `import type { Migration } from '@strav/database'
|
|
16
|
+
|
|
17
|
+
export const migration: Migration = {
|
|
18
|
+
name: '${name}',
|
|
19
|
+
async up(db) {
|
|
20
|
+
// write your migration here
|
|
21
|
+
},
|
|
22
|
+
async down(db) {
|
|
23
|
+
// write your rollback here
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override async execute({ flags }: ExecuteArgs): Promise<number> {
|
|
30
|
+
const msg = (flags.message as string | undefined)?.trim()
|
|
31
|
+
if (!msg) {
|
|
32
|
+
throw new UsageError(
|
|
33
|
+
'--message (or -m) is required: bun strav make:migration -m "create users"',
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
const slug = msg
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
39
|
+
.replace(/^_+|_+$/g, '')
|
|
40
|
+
if (!slug)
|
|
41
|
+
throw new UsageError(`--message "${msg}" produced an empty slug — use letters / digits`)
|
|
42
|
+
const now = new Date()
|
|
43
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
44
|
+
const ts = `${now.getUTCFullYear()}${pad(now.getUTCMonth() + 1)}${pad(now.getUTCDate())}${pad(now.getUTCHours())}${pad(now.getUTCMinutes())}${pad(now.getUTCSeconds())}`
|
|
45
|
+
const name = `${ts}_${slug}`
|
|
46
|
+
return super.execute({ args: { name }, flags })
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `make:model <Name>` — the model_generator.
|
|
3
|
+
*
|
|
4
|
+
* Writes three files (each skipped if it already exists):
|
|
5
|
+
* - `app/models/<name>.ts` — Model class
|
|
6
|
+
* - `app/repositories/<name>_repository.ts` — Repository<Model>
|
|
7
|
+
* - `database/factories/<name>_factory.ts` — Factory stub
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync } from 'node:fs'
|
|
11
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
12
|
+
import { dirname, join } from 'node:path'
|
|
13
|
+
import { Command, type ExecuteArgs } from '../command.ts'
|
|
14
|
+
import { ExitCode } from '../exit_codes.ts'
|
|
15
|
+
import { camel, pascal, snake } from '../make_command.ts'
|
|
16
|
+
|
|
17
|
+
export class MakeModel extends Command {
|
|
18
|
+
static signature = 'make:model {name}'
|
|
19
|
+
static description = 'Create Model + Repository + Factory stubs (model_generator).'
|
|
20
|
+
static providers: string[] = []
|
|
21
|
+
|
|
22
|
+
override async execute({ args }: ExecuteArgs): Promise<number> {
|
|
23
|
+
const raw = (args.name ?? '').trim()
|
|
24
|
+
if (!raw) {
|
|
25
|
+
this.error('A model name is required.')
|
|
26
|
+
return ExitCode.UsageError
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const name = pascal(raw)
|
|
30
|
+
const files: Array<{ path: string; content: string }> = [
|
|
31
|
+
{ path: `app/models/${snake(raw)}.ts`, content: modelStub(name) },
|
|
32
|
+
{
|
|
33
|
+
path: `app/repositories/${snake(raw)}_repository.ts`,
|
|
34
|
+
content: repositoryStub(name),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: `database/factories/${snake(raw)}_factory.ts`,
|
|
38
|
+
content: factoryStub(name),
|
|
39
|
+
},
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
for (const { path, content } of files) {
|
|
43
|
+
const dest = join(process.cwd(), path)
|
|
44
|
+
if (existsSync(dest)) {
|
|
45
|
+
this.warn(`${dest} already exists — skipping.`)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
await mkdir(dirname(dest), { recursive: true })
|
|
49
|
+
await writeFile(dest, content, 'utf8')
|
|
50
|
+
this.success(`Created ${dest}`)
|
|
51
|
+
}
|
|
52
|
+
return ExitCode.Success
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function modelStub(name: string): string {
|
|
57
|
+
return `import { Model } from '@strav/database'
|
|
58
|
+
|
|
59
|
+
export class ${name} extends Model {
|
|
60
|
+
declare id: string
|
|
61
|
+
// add your properties here
|
|
62
|
+
}
|
|
63
|
+
`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function repositoryStub(name: string): string {
|
|
67
|
+
return `import { inject } from '@strav/kernel'
|
|
68
|
+
import { PostgresDatabase, Repository } from '@strav/database'
|
|
69
|
+
import { ${name} } from '../models/${snake(name)}.ts'
|
|
70
|
+
import { ${camel(name)}Schema } from '../../database/schemas/${snake(name)}_schema.ts'
|
|
71
|
+
|
|
72
|
+
@inject()
|
|
73
|
+
export class ${name}Repository extends Repository<${name}> {
|
|
74
|
+
constructor(db: PostgresDatabase) {
|
|
75
|
+
super(db, ${camel(name)}Schema)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function factoryStub(name: string): string {
|
|
82
|
+
return `import type { ${name} } from '../../app/models/${snake(name)}.ts'
|
|
83
|
+
|
|
84
|
+
export function ${camel(name)}Factory(overrides: Partial<${name}> = {}): ${name} {
|
|
85
|
+
return {
|
|
86
|
+
// define default attribute values here
|
|
87
|
+
...overrides,
|
|
88
|
+
} as ${name}
|
|
89
|
+
}
|
|
90
|
+
`
|
|
91
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeNotification extends MakeCommand {
|
|
4
|
+
static signature = 'make:notification {name}'
|
|
5
|
+
static description = 'Create a Notification stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `app/notifications/${snake(name)}.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
const cls = pascal(name)
|
|
14
|
+
return `// ${cls} notification
|
|
15
|
+
// Implement channels (mail, database, broadcast) once @strav/signal notifications land.
|
|
16
|
+
export class ${cls} {
|
|
17
|
+
via(): string[] {
|
|
18
|
+
return ['mail']
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakePolicy extends MakeCommand {
|
|
4
|
+
static signature = 'make:policy {name}'
|
|
5
|
+
static description = 'Create an authorization policy stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
const base = snake(name).replace(/_policy$/, '')
|
|
10
|
+
return `app/policies/${base}_policy.ts`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected stub(name: string): string {
|
|
14
|
+
const base = pascal(name).replace(/Policy$/, '')
|
|
15
|
+
const cls = `${base}Policy`
|
|
16
|
+
return `export class ${cls} {
|
|
17
|
+
// async view(user: User, model: ${base}): Promise<boolean> { return true }
|
|
18
|
+
// async create(user: User): Promise<boolean> { return true }
|
|
19
|
+
// async update(user: User, model: ${base}): Promise<boolean> { return true }
|
|
20
|
+
// async delete(user: User, model: ${base}): Promise<boolean> { return true }
|
|
21
|
+
}
|
|
22
|
+
`
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeProvider extends MakeCommand {
|
|
4
|
+
static signature = 'make:provider {name}'
|
|
5
|
+
static description = 'Create a ServiceProvider stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `app/providers/${snake(name)}.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
const cls = pascal(name).endsWith('Provider') ? pascal(name) : `${pascal(name)}Provider`
|
|
14
|
+
return `import { type Application, ServiceProvider } from '@strav/kernel'
|
|
15
|
+
|
|
16
|
+
export class ${cls} extends ServiceProvider {
|
|
17
|
+
override readonly name = '${snake(cls).replace(/_provider$/, '')}'
|
|
18
|
+
|
|
19
|
+
override register(app: Application): void {
|
|
20
|
+
// bind services into the container
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
override async boot(app: Application): Promise<void> {
|
|
24
|
+
// run initialization after all providers are registered
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeRepository extends MakeCommand {
|
|
4
|
+
static signature = 'make:repository {name}'
|
|
5
|
+
static description = 'Create a Repository stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
const base = snake(name).replace(/_repository$/, '')
|
|
10
|
+
return `app/repositories/${base}_repository.ts`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
protected stub(name: string): string {
|
|
14
|
+
const base = pascal(name).replace(/Repository$/, '')
|
|
15
|
+
const cls = `${base}Repository`
|
|
16
|
+
const model = base
|
|
17
|
+
return `import { inject } from '@strav/kernel'
|
|
18
|
+
import { type Database, PostgresDatabase, Repository } from '@strav/database'
|
|
19
|
+
import { ${model} } from '../models/${snake(model)}.ts'
|
|
20
|
+
import { ${snake(model)}Schema } from '../../database/schemas/${snake(model)}_schema.ts'
|
|
21
|
+
|
|
22
|
+
@inject()
|
|
23
|
+
export class ${cls} extends Repository<${model}> {
|
|
24
|
+
constructor(db: PostgresDatabase) {
|
|
25
|
+
super(db, ${snake(model)}Schema)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeRequest extends MakeCommand {
|
|
4
|
+
static signature = 'make:request {name}'
|
|
5
|
+
static description = 'Create a new FormRequest stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `app/http/requests/${snake(name)}.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
const cls = pascal(name).endsWith('Request') ? pascal(name) : `${pascal(name)}Request`
|
|
14
|
+
return `import { FormRequest } from '@strav/http'
|
|
15
|
+
import { z } from 'zod'
|
|
16
|
+
|
|
17
|
+
export class ${cls} extends FormRequest {
|
|
18
|
+
schema = z.object({
|
|
19
|
+
// define your fields here
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
`
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { MakeCommand, pascal, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeSeeder extends MakeCommand {
|
|
4
|
+
static signature = 'make:seeder {name}'
|
|
5
|
+
static description = 'Create a database seeder stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `database/seeders/${snake(name)}.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
const cls = pascal(name).endsWith('Seeder') ? pascal(name) : `${pascal(name)}Seeder`
|
|
14
|
+
return `import type { Database } from '@strav/database'
|
|
15
|
+
|
|
16
|
+
export class ${cls} {
|
|
17
|
+
async run(db: Database): Promise<void> {
|
|
18
|
+
// seed your data here
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
`
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MakeCommand, snake } from '../make_command.ts'
|
|
2
|
+
|
|
3
|
+
export class MakeTest extends MakeCommand {
|
|
4
|
+
static signature = 'make:test {name}'
|
|
5
|
+
static description = 'Create a feature test stub.'
|
|
6
|
+
static providers: string[] = []
|
|
7
|
+
|
|
8
|
+
protected filePath(name: string): string {
|
|
9
|
+
return `tests/feature/${snake(name)}.test.ts`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
protected stub(name: string): string {
|
|
13
|
+
return `import { describe, expect, test } from 'bun:test'
|
|
14
|
+
|
|
15
|
+
describe('${snake(name).replace(/_/g, ' ')}', () => {
|
|
16
|
+
test('placeholder', () => {
|
|
17
|
+
expect(true).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
`
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `MakeCommand` — base for every `make:*` scaffold command.
|
|
3
|
+
*
|
|
4
|
+
* Subclasses implement `filePath(name)` and `stub(name)` and get the
|
|
5
|
+
* filesystem write, exists-check, and directory creation for free.
|
|
6
|
+
*
|
|
7
|
+
* Naming helpers (`pascal`, `snake`, `camel`) convert the user's input
|
|
8
|
+
* into the conventional forms each stub needs.
|
|
9
|
+
*
|
|
10
|
+
* Re-running against an existing file is a no-op with a warning —
|
|
11
|
+
* there's no overwrite mode. Stubs can't safely re-derive what the user
|
|
12
|
+
* has already edited.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync } from 'node:fs'
|
|
16
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
17
|
+
import { dirname, join } from 'node:path'
|
|
18
|
+
import { Command, type ExecuteArgs } from './command.ts'
|
|
19
|
+
import { ExitCode } from './exit_codes.ts'
|
|
20
|
+
|
|
21
|
+
export abstract class MakeCommand extends Command {
|
|
22
|
+
/** Destination path relative to `process.cwd()`. */
|
|
23
|
+
protected abstract filePath(name: string): string
|
|
24
|
+
/** File content to write. */
|
|
25
|
+
protected abstract stub(name: string): string
|
|
26
|
+
|
|
27
|
+
override async execute({ args }: ExecuteArgs): Promise<number> {
|
|
28
|
+
const raw = (args.name ?? '').trim()
|
|
29
|
+
if (!raw) {
|
|
30
|
+
this.error('A name is required.')
|
|
31
|
+
return ExitCode.UsageError
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dest = join(process.cwd(), this.filePath(raw))
|
|
35
|
+
if (existsSync(dest)) {
|
|
36
|
+
this.warn(`${dest} already exists — skipping. Delete it first to regenerate.`)
|
|
37
|
+
return ExitCode.Success
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await mkdir(dirname(dest), { recursive: true })
|
|
41
|
+
await writeFile(dest, this.stub(raw), 'utf8')
|
|
42
|
+
this.success(`Created ${dest}`)
|
|
43
|
+
return ExitCode.Success
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// Naming helpers
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/** MyFoo / my_foo / my-foo → MyFoo */
|
|
52
|
+
export function pascal(name: string): string {
|
|
53
|
+
return name
|
|
54
|
+
.replace(/[-_](.)/g, (_, c: string) => c.toUpperCase())
|
|
55
|
+
.replace(/^(.)/, (_, c: string) => c.toUpperCase())
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** MyFoo / myFoo → my_foo */
|
|
59
|
+
export function snake(name: string): string {
|
|
60
|
+
return pascal(name)
|
|
61
|
+
.replace(/([A-Z])/g, (c) => `_${c.toLowerCase()}`)
|
|
62
|
+
.replace(/^_/, '')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** MyFoo → myFoo */
|
|
66
|
+
export function camel(name: string): string {
|
|
67
|
+
const p = pascal(name)
|
|
68
|
+
return p.charAt(0).toLowerCase() + p.slice(1)
|
|
69
|
+
}
|