@strav/spring 0.4.31 → 1.0.0-alpha.28

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 (90) hide show
  1. package/README.md +11 -52
  2. package/package.json +19 -20
  3. package/src/args.ts +119 -0
  4. package/src/cli.ts +134 -0
  5. package/src/index.ts +10 -176
  6. package/src/prompts.ts +49 -127
  7. package/src/scaffold.ts +115 -36
  8. package/src/spring_error.ts +11 -0
  9. package/src/templates/shared/README.md.tt +37 -0
  10. package/src/templates/shared/_dot_env.example.tt +12 -0
  11. package/src/templates/shared/_dot_env.tt +12 -0
  12. package/src/templates/shared/_dot_gitignore +12 -0
  13. package/src/templates/shared/app/console/_dot_gitkeep +0 -0
  14. package/src/templates/shared/app/exceptions/_dot_gitkeep +0 -0
  15. package/src/templates/shared/app/http/controllers/_dot_gitkeep +0 -0
  16. package/src/templates/shared/app/http/middleware/_dot_gitkeep +0 -0
  17. package/src/templates/shared/app/http/requests/_dot_gitkeep +0 -0
  18. package/src/templates/shared/app/jobs/_dot_gitkeep +0 -0
  19. package/src/templates/shared/app/mail/_dot_gitkeep +0 -0
  20. package/src/templates/shared/app/models/_dot_gitkeep +0 -0
  21. package/src/templates/shared/app/notifications/_dot_gitkeep +0 -0
  22. package/src/templates/shared/app/policies/_dot_gitkeep +0 -0
  23. package/src/templates/shared/app/providers/app_provider.ts +18 -0
  24. package/src/templates/shared/app/repositories/_dot_gitkeep +0 -0
  25. package/src/templates/shared/bin/strav.ts +21 -0
  26. package/src/templates/shared/bootstrap/app.ts +13 -0
  27. package/src/templates/shared/bootstrap/providers.ts +29 -0
  28. package/src/templates/shared/config/app.ts.tt +13 -0
  29. package/src/templates/shared/config/http.ts +9 -0
  30. package/src/templates/shared/config/logger.ts +9 -0
  31. package/src/templates/shared/database/factories/_dot_gitkeep +0 -0
  32. package/src/templates/shared/database/migrations/_dot_gitkeep +0 -0
  33. package/src/templates/shared/database/schemas/_dot_gitkeep +0 -0
  34. package/src/templates/shared/database/seeders/_dot_gitkeep +0 -0
  35. package/src/templates/shared/package.json.tt +22 -0
  36. package/src/templates/shared/routes/api.ts +11 -0
  37. package/src/templates/shared/routes/console.ts +10 -0
  38. package/src/templates/shared/storage/cache/_dot_gitkeep +0 -0
  39. package/src/templates/shared/storage/logs/_dot_gitkeep +0 -0
  40. package/src/templates/shared/storage/uploads/_dot_gitkeep +0 -0
  41. package/src/templates/shared/tests/feature/healthz.test.ts.tt +19 -0
  42. package/src/templates/shared/tests/unit/_dot_gitkeep +0 -0
  43. package/src/templates/shared/tsconfig.json +21 -11
  44. package/src/templates/web/README.md.tt +42 -0
  45. package/src/templates/web/_dot_gitignore +13 -0
  46. package/src/templates/web/app/providers/app_provider.ts +21 -0
  47. package/src/templates/web/bootstrap/providers.ts +34 -0
  48. package/src/templates/web/config/http.ts +17 -8
  49. package/src/templates/web/config/view.ts +7 -7
  50. package/src/templates/web/package.json.tt +26 -0
  51. package/src/templates/web/public/assets/_dot_gitkeep +0 -0
  52. package/src/templates/web/resources/css/app.css +41 -0
  53. package/src/templates/web/resources/ts/islands/counter.vue +11 -0
  54. package/src/templates/web/resources/ts/islands/setup.ts +13 -0
  55. package/src/templates/web/resources/views/components/_dot_gitkeep +0 -0
  56. package/src/templates/web/resources/views/errors/404.strav +8 -0
  57. package/src/templates/web/resources/views/errors/500.strav +8 -0
  58. package/src/templates/web/resources/views/layouts/app.strav.tt +15 -0
  59. package/src/templates/web/resources/views/pages/index.strav.tt +12 -0
  60. package/src/templates/web/routes/broadcast.ts +9 -0
  61. package/src/templates/web/routes/web.ts +19 -0
  62. package/src/templates/web/tests/browser/_dot_gitkeep +0 -0
  63. package/src/version.ts +9 -0
  64. package/src/templates/api/app/controllers/controller.ts +0 -15
  65. package/src/templates/api/app/controllers/user_controller.ts +0 -69
  66. package/src/templates/api/config/database.ts +0 -9
  67. package/src/templates/api/config/http.ts +0 -17
  68. package/src/templates/api/database/factories/user_factory.ts +0 -11
  69. package/src/templates/api/database/schemas/user.ts +0 -13
  70. package/src/templates/api/database/seeders/database_seeder.ts +0 -8
  71. package/src/templates/api/database/seeders/user_seeder.ts +0 -15
  72. package/src/templates/api/index.ts +0 -11
  73. package/src/templates/api/package.json +0 -24
  74. package/src/templates/api/start/providers.ts +0 -10
  75. package/src/templates/api/start/routes.ts +0 -22
  76. package/src/templates/shared/config/app.ts +0 -7
  77. package/src/templates/shared/config/encryption.ts +0 -5
  78. package/src/templates/shared/package.json +0 -24
  79. package/src/templates/shared/storage/uploads/.gitkeep +0 -1
  80. package/src/templates/shared/strav.ts +0 -2
  81. package/src/templates/shared/tests/example.test.ts +0 -11
  82. package/src/templates/web/index.ts +0 -28
  83. package/src/templates/web/package.json +0 -26
  84. package/src/templates/web/public/builds/.gitkeep +0 -1
  85. package/src/templates/web/public/css/.gitkeep +0 -1
  86. package/src/templates/web/resources/css/app.scss +0 -77
  87. package/src/templates/web/resources/islands/counter.vue +0 -31
  88. package/src/templates/web/resources/views/layouts/app.strav +0 -18
  89. package/src/templates/web/resources/views/pages/index.strav +0 -32
  90. package/src/templates/web/start/providers.ts +0 -11
package/README.md CHANGED
@@ -1,61 +1,20 @@
1
1
  # @strav/spring
2
2
 
3
- Flagship framework scaffolding tool for the Strav ecosystem - the rite of the Bun ecosystem.
4
-
5
- ## Usage
3
+ Project scaffolder for Strav 1.0. Writes a working app skeleton you can boot in a single command.
6
4
 
7
5
  ```bash
8
- bunx @strav/spring my-app --web # full-stack with Vue islands
9
- bunx @strav/spring my-app --api # headless REST API
10
- bunx @strav/spring my-app # interactive prompt
11
- ```
12
-
13
- ## Templates
14
-
15
- - **api** — Headless REST API with CORS enabled
16
- - **web** — Full-stack with .strav views, Vue islands, and sessions
17
-
18
- ## Options
19
-
20
- ```
21
- bunx @strav/spring <project-name> [options]
22
-
23
- --api Headless REST API template
24
- --web Full-stack template with Vue islands
25
- --template, -t api|web Alias for --api / --web
26
- --db <name> Database name (default: project name)
27
- -h, --help Show help
6
+ bunx @strav/spring my-app --api
7
+ cd my-app
8
+ bun install
9
+ bun strav serve
10
+ # → listening on http://localhost:3000
28
11
  ```
29
12
 
30
- ## What's scaffolded
13
+ See [`docs/spring/`](../../docs/spring/) for the full documentation and the [template-strategy ADR](../../docs/decisions/spring-template-strategy.md) for design notes.
31
14
 
32
- ```
33
- my-app/
34
- ├── app/
35
- │ ├── controllers/ # HTTP controllers
36
- │ ├── models/ # Database models (generated from schemas)
37
- │ ├── middleware/ # Custom middleware
38
- │ ├── providers/ # Service providers
39
- │ ├── policies/ # Authorization policies
40
- │ ├── jobs/ # Queue jobs
41
- │ └── services/ # Business logic services
42
- ├── config/ # Configuration files
43
- ├── database/
44
- │ ├── schemas/ # Schema definitions
45
- │ ├── migrations/ # Generated migrations
46
- │ ├── seeders/ # Database seeders
47
- │ └── factories/ # Model factories
48
- ├── resources/
49
- │ ├── views/ # .strav templates
50
- │ ├── css/ # Stylesheets
51
- │ └── ts/islands/ # Vue.js islands
52
- ├── routes/ # Route definitions
53
- ├── tests/ # Test files
54
- ├── index.ts # Application entry point
55
- ├── strav.ts # CLI tool
56
- └── .env # Environment variables
57
- ```
15
+ ## Status
58
16
 
59
- ## License
17
+ - Slice A — `--api` template + CLI shell. **Shipped.**
18
+ - Slice B — `--web` template (`@strav/view` + Vue islands + plain CSS). **Shipped.**
60
19
 
61
- MIT
20
+ The scaffolder is feature-complete for 1.0. Slice 5.17 (`port` codemod) is cancelled — no 0.x backward-compat commitment.
package/package.json CHANGED
@@ -1,30 +1,29 @@
1
1
  {
2
2
  "name": "@strav/spring",
3
- "version": "0.4.31",
3
+ "version": "1.0.0-alpha.28",
4
+ "description": "Strav project scaffolder — `bunx @strav/spring my-app` writes a working app skeleton (bin/, bootstrap/, config/, routes/, …) per spec/directory-structure.md. Two templates: --api (headless REST) and --web (Vue islands + .strav views). Runtime-independent of the framework — no @strav/* runtime deps.",
4
5
  "type": "module",
5
- "description": "Flagship framework scaffolding tool for the Strav ecosystem.",
6
- "license": "MIT",
7
- "keywords": [
8
- "strav",
9
- "bun",
10
- "framework",
11
- "scaffold",
12
- "create",
13
- "typescript",
14
- "vue"
15
- ],
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
16
11
  "bin": {
17
- "@strav/spring": "./src/index.ts"
12
+ "@strav/spring": "./src/cli.ts"
18
13
  },
19
14
  "files": [
20
- "src/",
21
- "package.json",
15
+ "src",
22
16
  "README.md"
23
17
  ],
24
- "dependencies": {
25
- "@strav/kernel": "0.4.31"
18
+ "engines": {
19
+ "bun": ">=1.3.14"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "peerDependencies": {
25
+ "@types/bun": ">=1.3.14"
26
26
  },
27
- "devDependencies": {
28
- "@types/bun": "latest"
29
- }
27
+ "dependencies": null,
28
+ "devDependencies": null
30
29
  }
package/src/args.ts ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Pure CLI argument parser for `bunx @strav/spring`. Side-effect free so
3
+ * `tests/unit/args.test.ts` can hit every branch without spawning a process.
4
+ *
5
+ * Surface:
6
+ * bunx @strav/spring <project-name> [--api|--web|-t api|web] [--db <name>]
7
+ * [--no-install] [-h|--help] [-v|--version]
8
+ *
9
+ * Validation rules:
10
+ * - Project name is required for a real run. Help / version short-circuit it.
11
+ * - Project name matches `/^[a-z0-9][a-z0-9_-]*$/` (no uppercase, no leading
12
+ * dot, no spaces). This is what npm packageName allows minus the scope.
13
+ * - `--template` / `-t` only accepts `api` or `web`. Conflicts between
14
+ * `--api` / `--web` / `--template` → error.
15
+ */
16
+
17
+ import { SpringError } from './spring_error.ts'
18
+
19
+ export type Template = 'api' | 'web'
20
+
21
+ export interface ParsedArgs {
22
+ /** Empty when the user just asked for help or version. */
23
+ projectName?: string
24
+ template?: Template
25
+ dbName?: string
26
+ noInstall: boolean
27
+ help: boolean
28
+ version: boolean
29
+ }
30
+
31
+ const NAME_RE = /^[a-z0-9][a-z0-9_-]*$/
32
+
33
+ export function parseArgs(argv: readonly string[]): ParsedArgs {
34
+ const out: ParsedArgs = { noInstall: false, help: false, version: false }
35
+ let templateSeenAs: string | undefined
36
+
37
+ const setTemplate = (value: string, flag: string): void => {
38
+ if (value !== 'api' && value !== 'web') {
39
+ throw new SpringError(`${flag}: expected "api" or "web", got "${value}"`)
40
+ }
41
+ if (out.template !== undefined && out.template !== value) {
42
+ throw new SpringError(
43
+ `${flag} conflicts with earlier ${templateSeenAs} (resolved to "${out.template}")`,
44
+ )
45
+ }
46
+ out.template = value as Template
47
+ templateSeenAs = flag
48
+ }
49
+
50
+ for (let i = 0; i < argv.length; i++) {
51
+ const arg = argv[i] as string
52
+ switch (arg) {
53
+ case '-h':
54
+ case '--help':
55
+ out.help = true
56
+ break
57
+ case '-v':
58
+ case '--version':
59
+ out.version = true
60
+ break
61
+ case '--api':
62
+ setTemplate('api', '--api')
63
+ break
64
+ case '--web':
65
+ setTemplate('web', '--web')
66
+ break
67
+ case '--no-install':
68
+ out.noInstall = true
69
+ break
70
+ case '-t':
71
+ case '--template': {
72
+ const next = argv[++i]
73
+ if (next === undefined) {
74
+ throw new SpringError(`${arg}: missing value (expected "api" or "web")`)
75
+ }
76
+ setTemplate(next, arg)
77
+ break
78
+ }
79
+ case '--db': {
80
+ const next = argv[++i]
81
+ if (next === undefined) {
82
+ throw new SpringError(`--db: missing value (expected a database name)`)
83
+ }
84
+ out.dbName = next
85
+ break
86
+ }
87
+ default: {
88
+ if (arg.startsWith('-')) {
89
+ throw new SpringError(`unknown option: ${arg}`)
90
+ }
91
+ if (out.projectName !== undefined) {
92
+ throw new SpringError(
93
+ `unexpected positional argument "${arg}" (project name "${out.projectName}" already set)`,
94
+ )
95
+ }
96
+ if (!NAME_RE.test(arg)) {
97
+ throw new SpringError(
98
+ `invalid project name "${arg}" — must match /^[a-z0-9][a-z0-9_-]*$/ (lowercase letters, digits, hyphen, underscore)`,
99
+ )
100
+ }
101
+ out.projectName = arg
102
+ break
103
+ }
104
+ }
105
+ }
106
+ return out
107
+ }
108
+
109
+ /**
110
+ * Convert a project name to a snake_case database default. Splits on `-`
111
+ * and runs of non-alphanumerics. Mirrors what 0.x spring did so the
112
+ * default `DB_DATABASE` value reads naturally for `my-blog` → `my_blog`.
113
+ */
114
+ export function toSnakeCase(name: string): string {
115
+ return name
116
+ .toLowerCase()
117
+ .replace(/[^a-z0-9]+/g, '_')
118
+ .replace(/^_+|_+$/g, '')
119
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * `bunx @strav/spring <project>` entry. Wires argv parsing → optional
4
+ * interactive prompts → scaffold → `bun install`. Errors print to stderr
5
+ * and exit non-zero; expected (`SpringError`) errors print without the
6
+ * stack trace, unexpected ones include it.
7
+ */
8
+
9
+ import { existsSync } from 'node:fs'
10
+ import { resolve } from 'node:path'
11
+ import { parseArgs, type Template, toSnakeCase } from './args.ts'
12
+ import { input, select } from './prompts.ts'
13
+ import { scaffold } from './scaffold.ts'
14
+ import { SpringError } from './spring_error.ts'
15
+ import { STRAV_VERSION } from './version.ts'
16
+
17
+ const bold = (s: string): string => `\x1b[1m${s}\x1b[0m`
18
+ const dim = (s: string): string => `\x1b[2m${s}\x1b[0m`
19
+ const green = (s: string): string => `\x1b[32m${s}\x1b[0m`
20
+ const red = (s: string): string => `\x1b[31m${s}\x1b[0m`
21
+ const cyan = (s: string): string => `\x1b[36m${s}\x1b[0m`
22
+
23
+ const SPRING_VERSION = '1.0.0-alpha.28'
24
+
25
+ function printUsage(): void {
26
+ process.stdout.write(`
27
+ ${bold('@strav/spring')} ${dim(`v${SPRING_VERSION}`)}
28
+ ${dim('Strav project scaffolder')}
29
+
30
+ ${bold('Usage:')}
31
+ bunx @strav/spring ${cyan('<project-name>')} [options]
32
+
33
+ ${bold('Options:')}
34
+ --api Headless REST template
35
+ --web Full-stack template ${dim('(pages auto-router + Vue islands)')}
36
+ --template, -t ${dim('api|web')} Alias for --api / --web
37
+ --db ${dim('<name>')} Database name ${dim('(default: snake_case(project-name))')}
38
+ --no-install Skip ${dim('bun install')} after scaffolding
39
+ -v, --version Print spring version and exit
40
+ -h, --help Show this help and exit
41
+
42
+ ${bold('Examples:')}
43
+ bunx @strav/spring my-api --api
44
+ bunx @strav/spring my-app ${dim('# interactive prompt')}
45
+ bunx @strav/spring my-app --api --no-install
46
+ `)
47
+ }
48
+
49
+ async function main(): Promise<number> {
50
+ const args = parseArgs(process.argv.slice(2))
51
+
52
+ if (args.help) {
53
+ printUsage()
54
+ return 0
55
+ }
56
+ if (args.version) {
57
+ process.stdout.write(`${SPRING_VERSION}\n`)
58
+ return 0
59
+ }
60
+ if (args.projectName === undefined) {
61
+ printUsage()
62
+ return 1
63
+ }
64
+
65
+ const dest = resolve(args.projectName)
66
+ if (existsSync(dest)) {
67
+ throw new SpringError(`directory already exists: ${dest}`)
68
+ }
69
+
70
+ process.stdout.write(`\n ${bold('@strav/spring')} ${dim(`v${SPRING_VERSION}`)}\n`)
71
+ process.stdout.write(` ${dim('Scaffolding a Strav app')}\n`)
72
+
73
+ let template: Template
74
+ if (args.template !== undefined) {
75
+ template = args.template
76
+ } else {
77
+ template = await select<Template>('Which template?', [
78
+ { value: 'api', label: 'api', description: 'Headless REST template' },
79
+ {
80
+ value: 'web',
81
+ label: 'web',
82
+ description: 'Full-stack — pages auto-router + Vue islands + plain CSS',
83
+ },
84
+ ])
85
+ }
86
+
87
+ const dbName = args.dbName ?? (await input('Database name', toSnakeCase(args.projectName)))
88
+
89
+ process.stdout.write('\n')
90
+ const result = await scaffold({
91
+ projectName: args.projectName,
92
+ template,
93
+ dbName,
94
+ dest,
95
+ stravVersion: STRAV_VERSION,
96
+ })
97
+ process.stdout.write(` ${green('+')} wrote ${result.files.length} files into ${dest}\n`)
98
+
99
+ if (!args.noInstall) {
100
+ process.stdout.write(` ${dim('…')} installing dependencies\n`)
101
+ const proc = Bun.spawn(['bun', 'install'], {
102
+ cwd: dest,
103
+ stdout: 'ignore',
104
+ stderr: 'pipe',
105
+ })
106
+ const code = await proc.exited
107
+ if (code !== 0) {
108
+ const stderr = await new Response(proc.stderr).text()
109
+ throw new SpringError(`bun install failed (exit ${code}):\n${stderr}`)
110
+ }
111
+ process.stdout.write(` ${green('+')} installed dependencies\n`)
112
+ } else {
113
+ process.stdout.write(` ${dim('-')} skipped install (--no-install)\n`)
114
+ }
115
+
116
+ process.stdout.write(`\n ${green('Done!')} Next steps:\n\n`)
117
+ process.stdout.write(` ${dim('$')} cd ${args.projectName}\n`)
118
+ if (args.noInstall) process.stdout.write(` ${dim('$')} bun install\n`)
119
+ process.stdout.write(` ${dim('$')} bun strav serve\n\n`)
120
+ process.stdout.write(` ${dim('Then open http://localhost:3000')}\n\n`)
121
+ return 0
122
+ }
123
+
124
+ try {
125
+ process.exit(await main())
126
+ } catch (err) {
127
+ if (err instanceof SpringError) {
128
+ process.stderr.write(`\n ${red('✗')} ${err.message}\n\n`)
129
+ process.exit(1)
130
+ }
131
+ process.stderr.write(`\n ${red('✗ internal error')}\n`)
132
+ process.stderr.write(` ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n\n`)
133
+ process.exit(1)
134
+ }
package/src/index.ts CHANGED
@@ -1,176 +1,10 @@
1
- #!/usr/bin/env bun
2
- import { existsSync } from 'node:fs'
3
- import { resolve } from 'node:path'
4
- import { select, input } from './prompts.ts'
5
- import { scaffold, type ScaffoldOptions } from './scaffold.ts'
6
- import pkg from '../package.json'
7
-
8
- const VERSION = pkg.version
9
-
10
- // ── Colors ──────────────────────────────────────────────────────────
11
-
12
- const bold = (s: string) => `\x1b[1m${s}\x1b[0m`
13
- const dim = (s: string) => `\x1b[2m${s}\x1b[0m`
14
- const green = (s: string) => `\x1b[32m${s}\x1b[0m`
15
- const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`
16
- const red = (s: string) => `\x1b[31m${s}\x1b[0m`
17
-
18
- // ── Arg parsing ─────────────────────────────────────────────────────
19
-
20
- interface ParsedArgs {
21
- projectName?: string
22
- template?: 'api' | 'web'
23
- db?: string
24
- help?: boolean
25
- }
26
-
27
- function parseArgs(): ParsedArgs {
28
- const args = process.argv.slice(2)
29
- const result: ParsedArgs = {}
30
-
31
- for (let i = 0; i < args.length; i++) {
32
- const arg = args[i]
33
-
34
- if (arg === '--help' || arg === '-h') {
35
- result.help = true
36
- } else if (arg === '--api') {
37
- result.template = 'api'
38
- } else if (arg === '--web') {
39
- result.template = 'web'
40
- } else if (arg === '--template' || arg === '-t') {
41
- const val = args[++i]
42
- if (val === 'api' || val === 'web') {
43
- result.template = val
44
- } else {
45
- console.error(red(` Invalid template: ${val}. Use "api" or "web".`))
46
- process.exit(1)
47
- }
48
- } else if (arg === '--db') {
49
- result.db = args[++i]
50
- } else if (arg && !arg.startsWith('-') && !result.projectName) {
51
- result.projectName = arg
52
- }
53
- }
54
-
55
- return result
56
- }
57
-
58
- function printUsage(): void {
59
- console.log(`
60
- ${bold('@strav/spring')} ${dim(`v${VERSION}`)}
61
- ${dim('The Rite of the Bun ecosystem')}
62
-
63
- ${bold('Usage:')}
64
- bunx @strav/spring ${cyan('<project-name>')} [options]
65
-
66
- ${bold('Options:')}
67
- --api Headless REST API template
68
- --web Full-stack template with Vue islands and views
69
- --template, -t ${dim('api|web')} Alias for --api / --web
70
- --db ${dim('<name>')} Database name (default: project name)
71
- -h, --help Show this help message
72
-
73
- ${bold('Examples:')}
74
- bunx @strav/spring my-blog --web
75
- bunx @strav/spring my-api --api
76
- bunx @strav/spring my-app ${dim('# interactive prompt')}
77
- `)
78
- }
79
-
80
- function toSnakeCase(name: string): string {
81
- return name
82
- .replace(/([A-Z])/g, '_$1')
83
- .toLowerCase()
84
- .replace(/[-\s]+/g, '_')
85
- .replace(/^_+|_+$/g, '')
86
- .replace(/_+/g, '_')
87
- }
88
-
89
- // ── Main ────────────────────────────────────────────────────────────
90
-
91
- async function main(): Promise<void> {
92
- const args = parseArgs()
93
-
94
- if (args.help) {
95
- printUsage()
96
- process.exit(0)
97
- }
98
-
99
- console.log()
100
- console.log(` ${bold('@strav/spring')} ${dim(`v${VERSION}`)}`)
101
- console.log(` ${dim('The rite of the Bun ecosystem')}`)
102
- console.log()
103
-
104
- // Project name
105
- if (!args.projectName) {
106
- printUsage()
107
- process.exit(1)
108
- }
109
-
110
- const projectName = args.projectName
111
- const root = resolve(projectName)
112
-
113
- // Validate
114
- if (existsSync(root)) {
115
- console.error(red(` Directory "${projectName}" already exists.`))
116
- process.exit(1)
117
- }
118
-
119
- if (!/^[a-zA-Z0-9_-]+$/.test(projectName)) {
120
- console.error(
121
- red(` Invalid project name. Use only letters, numbers, hyphens, and underscores.`)
122
- )
123
- process.exit(1)
124
- }
125
-
126
- // Template
127
- let template = args.template
128
- if (!template) {
129
- template = (await select('Which template?', [
130
- { label: 'web', value: 'web', description: 'Full-stack with Vue islands, views, and sessions' },
131
- { label: 'api', value: 'api', description: 'Headless REST API with CORS enabled' },
132
- ])) as 'api' | 'web'
133
- }
134
-
135
- // Database name
136
- const defaultDb = toSnakeCase(projectName)
137
- const dbName = args.db ?? defaultDb
138
-
139
- console.log()
140
-
141
- // Scaffold
142
- const opts: ScaffoldOptions = { projectName, template, dbName }
143
- await scaffold(root, opts)
144
- console.log(` ${green('+')} Scaffolded project files`)
145
-
146
- // Install dependencies
147
- console.log(` ${dim('...')} Installing dependencies`)
148
- const install = Bun.spawn(['bun', 'install'], { cwd: root, stdout: 'ignore', stderr: 'pipe' })
149
- const exitCode = await install.exited
150
-
151
- if (exitCode !== 0) {
152
- const stderr = await new Response(install.stderr).text()
153
- console.error(red(` Failed to install dependencies:`))
154
- console.error(dim(` ${stderr}`))
155
- process.exit(1)
156
- }
157
-
158
- console.log(` ${green('+')} Installed dependencies`)
159
-
160
- // Done
161
- console.log()
162
- console.log(` ${green('Project created successfully!')}`)
163
- console.log()
164
- console.log(` Next steps:`)
165
- console.log()
166
- console.log(` ${dim('$')} cd ${projectName}`)
167
- console.log(` ${dim('$')} bun --hot index.ts`)
168
- console.log()
169
- console.log(` ${dim('Then open http://localhost:3000')}`)
170
- console.log()
171
- }
172
-
173
- main().catch(err => {
174
- console.error(red(` Error: ${err instanceof Error ? err.message : err}`))
175
- process.exit(1)
176
- })
1
+ /**
2
+ * Public barrel for `@strav/spring`. The package's primary surface is the
3
+ * CLI (`bin: @strav/spring`); the exports here are the programmatic API
4
+ * used by tests and any tool that wants to embed scaffolding.
5
+ */
6
+
7
+ export { type ParsedArgs, parseArgs, type Template, toSnakeCase } from './args.ts'
8
+ export { type ScaffoldOptions, type ScaffoldResult, scaffold } from './scaffold.ts'
9
+ export { SpringError } from './spring_error.ts'
10
+ export { STRAV_VERSION } from './version.ts'