@alfredmouelle/create-stack 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/README.md +41 -18
  2. package/_stack/apps/next-base/.turbo/turbo-typecheck.log +1 -0
  3. package/_stack/apps/next-base/.vscode/settings.json +35 -0
  4. package/_stack/apps/next-base/.zed/settings.json +45 -0
  5. package/_stack/apps/next-base/src/components/ui/spinner.tsx +1 -1
  6. package/_stack/apps/next-base/src/emails/components/components.tsx +2 -2
  7. package/_stack/apps/next-base/src/emails/components/context.tsx +1 -1
  8. package/_stack/apps/next-base/src/emails/components/theme.ts +6 -7
  9. package/_stack/apps/next-base/src/env.ts +2 -3
  10. package/_stack/apps/next-base/src/lib/date.ts +1 -1
  11. package/_stack/apps/next-base/src/server/auth/guards.ts +1 -1
  12. package/_stack/apps/next-base/src/server/better-auth/config.ts +1 -1
  13. package/_stack/apps/next-base/src/server/better-auth/server.ts +2 -2
  14. package/_stack/apps/next-base/src/server/db/schemas/index.ts +1 -1
  15. package/_stack/apps/next-base/src/server/db/seed.ts +2 -2
  16. package/_stack/apps/next-base/src/server/email/adapters/resend/index.ts +3 -3
  17. package/_stack/apps/next-base/src/server/email/core/address.ts +3 -3
  18. package/_stack/apps/next-base/src/server/email/core/port.ts +13 -20
  19. package/_stack/apps/next-base/src/server/email/core/render.ts +2 -2
  20. package/_stack/apps/next-base/src/server/email/factory.ts +7 -9
  21. package/_stack/apps/next-base/src/server/email/index.ts +1 -1
  22. package/_stack/apps/next-base/src/trpc/react.tsx +2 -2
  23. package/_stack/apps/next-base/src/trpc/server.ts +1 -1
  24. package/_stack/apps/tanstack-base/.turbo/turbo-typecheck.log +1 -0
  25. package/_stack/apps/tanstack-base/.vscode/settings.json +35 -0
  26. package/_stack/apps/tanstack-base/.zed/settings.json +45 -0
  27. package/_stack/apps/tanstack-base/src/components/form/text-field.tsx +1 -1
  28. package/_stack/apps/tanstack-base/src/components/ui/spinner.tsx +1 -1
  29. package/_stack/apps/tanstack-base/src/emails/components/components.tsx +2 -2
  30. package/_stack/apps/tanstack-base/src/emails/components/context.tsx +1 -1
  31. package/_stack/apps/tanstack-base/src/emails/components/theme.ts +6 -7
  32. package/_stack/apps/tanstack-base/src/env.ts +2 -6
  33. package/_stack/apps/tanstack-base/src/lib/date.ts +1 -1
  34. package/_stack/apps/tanstack-base/src/routes/__root.tsx +1 -1
  35. package/_stack/apps/tanstack-base/src/routes/_authed.tsx +1 -4
  36. package/_stack/apps/tanstack-base/src/routes/api/auth/$.ts +1 -1
  37. package/_stack/apps/tanstack-base/src/routes/api.trpc.$.tsx +1 -2
  38. package/_stack/apps/tanstack-base/src/server/better-auth/config.ts +1 -1
  39. package/_stack/apps/tanstack-base/src/server/better-auth/session.ts +6 -7
  40. package/_stack/apps/tanstack-base/src/server/db/schemas/index.ts +1 -1
  41. package/_stack/apps/tanstack-base/src/server/db/seed.ts +2 -2
  42. package/_stack/apps/tanstack-base/src/server/email/adapters/resend/index.ts +3 -3
  43. package/_stack/apps/tanstack-base/src/server/email/core/address.ts +3 -3
  44. package/_stack/apps/tanstack-base/src/server/email/core/port.ts +12 -22
  45. package/_stack/apps/tanstack-base/src/server/email/core/render.ts +1 -1
  46. package/_stack/apps/tanstack-base/src/server/email/factory.ts +7 -8
  47. package/_stack/apps/tanstack-base/src/server/email/index.ts +1 -1
  48. package/_stack/packages/analytics/package.json +26 -0
  49. package/_stack/packages/analytics/src/adapters/noop/index.ts +12 -0
  50. package/_stack/packages/analytics/src/adapters/plausible/config.ts +10 -0
  51. package/_stack/packages/analytics/src/adapters/plausible/index.ts +94 -0
  52. package/_stack/packages/analytics/src/adapters/posthog/config.ts +7 -0
  53. package/_stack/packages/analytics/src/adapters/posthog/index.ts +50 -0
  54. package/_stack/packages/analytics/src/core/port.ts +30 -0
  55. package/_stack/packages/analytics/src/index.ts +17 -0
  56. package/_stack/packages/cache/package.json +25 -0
  57. package/_stack/packages/cache/src/adapters/memory/index.ts +51 -0
  58. package/_stack/packages/cache/src/adapters/redis/config.ts +8 -0
  59. package/_stack/packages/cache/src/adapters/redis/index.ts +73 -0
  60. package/_stack/packages/cache/src/core/port.ts +29 -0
  61. package/_stack/packages/cache/src/core/wrap.ts +20 -0
  62. package/_stack/packages/cache/src/index.ts +12 -0
  63. package/_stack/packages/error-tracking/package.json +25 -0
  64. package/_stack/packages/error-tracking/src/adapters/console/index.ts +43 -0
  65. package/_stack/packages/error-tracking/src/adapters/sentry/config.ts +8 -0
  66. package/_stack/packages/error-tracking/src/adapters/sentry/index.ts +72 -0
  67. package/_stack/packages/error-tracking/src/core/port.ts +39 -0
  68. package/_stack/packages/error-tracking/src/index.ts +14 -0
  69. package/_stack/packages/http/package.json +20 -0
  70. package/_stack/packages/http/src/api.ts +373 -0
  71. package/_stack/packages/http/src/index.ts +14 -0
  72. package/_stack/packages/http/src/responses.ts +25 -0
  73. package/_stack/packages/http/src/types.ts +9 -0
  74. package/_stack/packages/jobs/package.json +27 -0
  75. package/_stack/packages/jobs/src/adapters/inngest/config.ts +8 -0
  76. package/_stack/packages/jobs/src/adapters/inngest/index.ts +93 -0
  77. package/_stack/packages/jobs/src/adapters/memory/index.ts +31 -0
  78. package/_stack/packages/jobs/src/adapters/trigger/config.ts +8 -0
  79. package/_stack/packages/jobs/src/adapters/trigger/index.ts +85 -0
  80. package/_stack/packages/jobs/src/core/port.ts +37 -0
  81. package/_stack/packages/jobs/src/index.ts +23 -0
  82. package/_stack/packages/logger/package.json +25 -0
  83. package/_stack/packages/logger/src/adapters/console/config.ts +7 -0
  84. package/_stack/packages/logger/src/adapters/console/index.ts +69 -0
  85. package/_stack/packages/logger/src/adapters/pino/index.ts +54 -0
  86. package/_stack/packages/logger/src/core/port.ts +21 -0
  87. package/_stack/packages/logger/src/index.ts +12 -0
  88. package/_stack/packages/mailer/src/adapters/brevo/index.ts +3 -3
  89. package/_stack/packages/mailer/src/adapters/resend/index.ts +3 -3
  90. package/_stack/packages/mailer/src/adapters/ses/config.ts +3 -3
  91. package/_stack/packages/mailer/src/adapters/ses/index.ts +4 -5
  92. package/_stack/packages/storage/package.json +27 -0
  93. package/_stack/packages/storage/src/adapters/gcs/config.ts +8 -0
  94. package/_stack/packages/storage/src/adapters/gcs/index.ts +111 -0
  95. package/_stack/packages/storage/src/adapters/local/config.ts +8 -0
  96. package/_stack/packages/storage/src/adapters/local/index.ts +78 -0
  97. package/_stack/packages/storage/src/adapters/r2/config.ts +8 -0
  98. package/_stack/packages/storage/src/adapters/r2/index.ts +39 -0
  99. package/_stack/packages/storage/src/adapters/s3/config.ts +11 -0
  100. package/_stack/packages/storage/src/adapters/s3/index.ts +143 -0
  101. package/_stack/packages/storage/src/core/port.ts +41 -0
  102. package/_stack/packages/storage/src/index.ts +21 -0
  103. package/index.mjs +89 -55
  104. package/lib/build.mjs +21 -11
  105. package/lib/capabilities.mjs +375 -0
  106. package/lib/env.mjs +26 -6
  107. package/lib/foundations.mjs +35 -0
  108. package/lib/identity.mjs +4 -5
  109. package/lib/mailer.mjs +9 -13
  110. package/lib/paths.mjs +15 -0
  111. package/lib/scaffold.mjs +12 -11
  112. package/lib/strip.mjs +9 -24
  113. package/lib/util.mjs +8 -9
  114. package/package.json +1 -1
  115. package/_stack/packages/mailer/capability.json +0 -28
  116. package/_stack/patterns/README.md +0 -58
  117. package/_stack/patterns/_baseline/env.ts +0 -31
  118. package/_stack/patterns/_baseline/tsconfig.json +0 -27
  119. package/_stack/patterns/better-auth/pattern.json +0 -73
  120. package/_stack/patterns/better-auth-next/pattern.json +0 -76
  121. package/_stack/patterns/data-table/pattern.json +0 -43
  122. package/_stack/patterns/drizzle/pattern.json +0 -61
  123. package/_stack/patterns/trpc/pattern.json +0 -61
  124. package/_stack/patterns/trpc-next/pattern.json +0 -64
  125. package/lib/manifests.mjs +0 -61
  126. /package/{_stack/patterns/_baseline → templates}/README-author.md +0 -0
  127. /package/{_stack/patterns/_baseline → templates}/biome.jsonc +0 -0
@@ -0,0 +1,143 @@
1
+ import {
2
+ DeleteObjectCommand,
3
+ GetObjectCommand,
4
+ HeadObjectCommand,
5
+ PutObjectCommand,
6
+ S3Client,
7
+ } from '@aws-sdk/client-s3'
8
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
9
+ import * as v from 'valibot'
10
+ import {
11
+ type PutOptions,
12
+ type SignedUrlOptions,
13
+ StorageError,
14
+ type StoragePort,
15
+ } from '../../core/port.js'
16
+ import { S3ConfigSchema } from './config.js'
17
+
18
+ /** Structural view of the S3 client (eases testing). */
19
+ export interface S3ClientLike {
20
+ send(command: unknown): Promise<unknown>
21
+ }
22
+
23
+ /** Presigner shape, injectable so tests never sign for real. */
24
+ export type S3Presigner = (
25
+ client: S3ClientLike,
26
+ command: unknown,
27
+ options: { expiresIn: number },
28
+ ) => Promise<string>
29
+
30
+ export interface S3AdapterOptions {
31
+ bucket: string
32
+ region: string
33
+ accessKeyId?: string
34
+ secretAccessKey?: string
35
+ endpoint?: string
36
+ /** Inject custom/mock client. Defaults to real `S3Client`. */
37
+ client?: S3ClientLike
38
+ /** Inject custom/mock presigner. Defaults to `@aws-sdk/s3-request-presigner`. */
39
+ presign?: S3Presigner
40
+ }
41
+
42
+ const DEFAULT_EXPIRES_IN = 900
43
+
44
+ interface S3GetBody {
45
+ transformToByteArray(): Promise<Uint8Array>
46
+ }
47
+
48
+ interface S3GetResult {
49
+ // biome-ignore lint/style/useNamingConvention: mirrors the S3 SDK response shape (GetObjectCommandOutput.Body)
50
+ Body?: S3GetBody
51
+ }
52
+
53
+ function isNotFound(error: unknown): boolean {
54
+ if (typeof error !== 'object' || error === null) return false
55
+ const e = error as { name?: string; $metadata?: { httpStatusCode?: number } }
56
+ return e.name === 'NoSuchKey' || e.name === 'NotFound' || e.$metadata?.httpStatusCode === 404
57
+ }
58
+
59
+ export function s3Adapter(options: S3AdapterOptions): StoragePort {
60
+ // Validate early: missing config fails at construction, not use.
61
+ const config = v.parse(S3ConfigSchema, {
62
+ bucket: options.bucket,
63
+ region: options.region,
64
+ accessKeyId: options.accessKeyId,
65
+ secretAccessKey: options.secretAccessKey,
66
+ endpoint: options.endpoint,
67
+ })
68
+
69
+ const client: S3ClientLike =
70
+ options.client ??
71
+ (new S3Client({
72
+ region: config.region,
73
+ endpoint: config.endpoint,
74
+ credentials:
75
+ config.accessKeyId && config.secretAccessKey
76
+ ? { accessKeyId: config.accessKeyId, secretAccessKey: config.secretAccessKey }
77
+ : undefined,
78
+ }) as unknown as S3ClientLike)
79
+
80
+ const presign: S3Presigner = options.presign ?? (getSignedUrl as unknown as S3Presigner)
81
+
82
+ return {
83
+ name: 's3',
84
+ async put(key: string, data: Uint8Array | string, putOptions?: PutOptions) {
85
+ try {
86
+ await client.send(
87
+ new PutObjectCommand({
88
+ Bucket: config.bucket,
89
+ Key: key,
90
+ Body: data,
91
+ ContentType: putOptions?.contentType,
92
+ }),
93
+ )
94
+ } catch (cause) {
95
+ throw new StorageError('S3 put failed', { adapter: 's3', cause })
96
+ }
97
+ },
98
+ async get(key: string) {
99
+ try {
100
+ const result = (await client.send(
101
+ new GetObjectCommand({ Bucket: config.bucket, Key: key }),
102
+ )) as S3GetResult
103
+ if (!result.Body) return null
104
+ return await result.Body.transformToByteArray()
105
+ } catch (cause) {
106
+ if (isNotFound(cause)) return null
107
+ throw new StorageError('S3 get failed', { adapter: 's3', cause })
108
+ }
109
+ },
110
+ async delete(key: string) {
111
+ try {
112
+ await client.send(new DeleteObjectCommand({ Bucket: config.bucket, Key: key }))
113
+ } catch (cause) {
114
+ throw new StorageError('S3 delete failed', { adapter: 's3', cause })
115
+ }
116
+ },
117
+ async exists(key: string) {
118
+ try {
119
+ await client.send(new HeadObjectCommand({ Bucket: config.bucket, Key: key }))
120
+ return true
121
+ } catch (cause) {
122
+ if (isNotFound(cause)) return false
123
+ throw new StorageError('S3 exists failed', { adapter: 's3', cause })
124
+ }
125
+ },
126
+ async getSignedUrl(key: string, urlOptions: SignedUrlOptions) {
127
+ const expiresIn = urlOptions.expiresInSeconds ?? DEFAULT_EXPIRES_IN
128
+ const command =
129
+ urlOptions.operation === 'put'
130
+ ? new PutObjectCommand({
131
+ Bucket: config.bucket,
132
+ Key: key,
133
+ ContentType: urlOptions.contentType,
134
+ })
135
+ : new GetObjectCommand({ Bucket: config.bucket, Key: key })
136
+ try {
137
+ return await presign(client, command, { expiresIn })
138
+ } catch (cause) {
139
+ throw new StorageError('S3 getSignedUrl failed', { adapter: 's3', cause })
140
+ }
141
+ },
142
+ }
143
+ }
@@ -0,0 +1,41 @@
1
+ /** Write options. */
2
+ export interface PutOptions {
3
+ /** MIME type stored with the object (e.g. `image/png`). */
4
+ contentType?: string
5
+ }
6
+
7
+ /** Signed-URL options. */
8
+ export interface SignedUrlOptions {
9
+ /** Download (`get`) or upload (`put`). */
10
+ operation: 'get' | 'put'
11
+ /** Validity window. Adapters apply a default. */
12
+ expiresInSeconds?: number
13
+ /** Constrain content type for `put` URLs. */
14
+ contentType?: string
15
+ }
16
+
17
+ /** The port the app depends on. Swap provider = swap adapter; this never changes. */
18
+ export interface StoragePort {
19
+ readonly name: string
20
+ /** Write `data` at `key`, creating or overwriting. */
21
+ put(key: string, data: Uint8Array | string, options?: PutOptions): Promise<void>
22
+ /** Read bytes at `key`, or `null` if absent. */
23
+ get(key: string): Promise<Uint8Array | null>
24
+ /** Remove `key`. No-op if missing. */
25
+ delete(key: string): Promise<void>
26
+ /** Whether `key` exists. */
27
+ exists(key: string): Promise<boolean>
28
+ /** Mint a time-limited URL for `key`. */
29
+ getSignedUrl(key: string, options: SignedUrlOptions): Promise<string>
30
+ }
31
+
32
+ /** Normalized adapter error so callers never catch provider types. */
33
+ export class StorageError extends Error {
34
+ readonly adapter: string
35
+
36
+ constructor(message: string, options: { adapter: string; cause?: unknown }) {
37
+ super(message, { cause: options.cause })
38
+ this.name = 'StorageError'
39
+ this.adapter = options.adapter
40
+ }
41
+ }
@@ -0,0 +1,21 @@
1
+ export { type GcsConfig, GcsConfigSchema } from './adapters/gcs/config.js'
2
+ export {
3
+ type GcsAdapterOptions,
4
+ type GcsBucketLike,
5
+ type GcsFileLike,
6
+ type GcsStorageLike,
7
+ gcsAdapter,
8
+ } from './adapters/gcs/index.js'
9
+ export { type LocalConfig, LocalConfigSchema } from './adapters/local/config.js'
10
+ export { type LocalAdapterOptions, localAdapter } from './adapters/local/index.js'
11
+ export { type R2Config, R2ConfigSchema } from './adapters/r2/config.js'
12
+ export { type R2AdapterOptions, r2Adapter } from './adapters/r2/index.js'
13
+ export { type S3Config, S3ConfigSchema } from './adapters/s3/config.js'
14
+ export {
15
+ type S3AdapterOptions,
16
+ type S3ClientLike,
17
+ type S3Presigner,
18
+ s3Adapter,
19
+ } from './adapters/s3/index.js'
20
+ export type { PutOptions, SignedUrlOptions, StoragePort } from './core/port.js'
21
+ export { StorageError } from './core/port.js'
package/index.mjs CHANGED
@@ -1,20 +1,19 @@
1
1
  #!/usr/bin/env node
2
- // create-stack — deterministic installer for the personal reference stack.
3
- // The non-LLM counterpart of the bootstrap skill's CREATE mode: fork a base
4
- // app, strip it to the selection, stamp identity, verify.
5
- //
6
- // Interactive by default. Non-interactive when any selection flag (or --yes) is
7
- // passed — useful for scripts/CI and for headless end-to-end testing:
2
+ // create-stack — fork a base app, strip to selection, stamp identity, verify.
3
+ // Interactive by default; non-interactive when any selection flag (or --yes) is passed:
8
4
  // create-stack my-app --framework next --foundations drizzle,trpc --mailer ses --no-install
9
5
 
10
6
  import { resolve } from 'node:path'
11
7
  import * as p from '@clack/prompts'
12
8
  import { buildProject } from './lib/build.mjs'
13
- import { loadCapabilities, loadPatterns } from './lib/manifests.mjs'
9
+ import {
10
+ adapterChoices,
11
+ CAPABILITIES,
12
+ capabilityChoices,
13
+ resolveAdapter,
14
+ } from './lib/capabilities.mjs'
14
15
  import { isDirEmpty, run } from './lib/util.mjs'
15
16
 
16
- // Capabilities baked into the base apps vs. the ones add-capability supplies.
17
- const BAKED_CAPS = new Set(['mailer', 'email-kit'])
18
17
  const ALL_FOUNDATIONS = ['drizzle', 'trpc', 'better-auth', 'data-table']
19
18
 
20
19
  const cancelled = (v) => {
@@ -54,7 +53,7 @@ const csv = (v) =>
54
53
  .filter(Boolean)
55
54
  : []
56
55
 
57
- /** Resolve hard deps + the mailer's better-auth requirement. */
56
+ /** Resolve hard deps + mailer's better-auth requirement. */
58
57
  function normalize(picked, mailer) {
59
58
  const kept = new Set(picked.filter((f) => ALL_FOUNDATIONS.includes(f)))
60
59
  if (kept.has('trpc') || kept.has('better-auth')) kept.add('drizzle')
@@ -63,19 +62,27 @@ function normalize(picked, mailer) {
63
62
  return { kept, mailerProvider }
64
63
  }
65
64
 
66
- function collectFromFlags(args, capabilities) {
65
+ /** Read --<capability> flags into { capability: adapter } (default adapter if bare). */
66
+ function collectCapabilityFlags(flags) {
67
+ const out = {}
68
+ for (const cap of CAPABILITIES) {
69
+ if (cap in flags) out[cap] = resolveAdapter(cap, flags[cap])
70
+ }
71
+ return out
72
+ }
73
+
74
+ function collectFromFlags(args) {
67
75
  const argDir = args._[0]
68
76
  if (!argDir) throw new Error('Project name is required (positional) in non-interactive mode')
69
77
  const framework = args.flags.framework === 'next' ? 'next' : 'tanstack'
70
78
  const picked = args.flags.foundations ? csv(args.flags.foundations) : [...ALL_FOUNDATIONS]
71
79
  const { kept, mailerProvider } = normalize(picked, args.flags.mailer)
72
- const validCaps = new Set(Object.keys(capabilities).filter((c) => !BAKED_CAPS.has(c)))
73
- const extraCaps = csv(args.flags.caps).filter((c) => validCaps.has(c))
80
+ const capabilities = collectCapabilityFlags(args.flags)
74
81
  const doInstall = !args.flags['no-install']
75
- return { argDir, projectName: argDir, framework, kept, mailerProvider, extraCaps, doInstall }
82
+ return { argDir, projectName: argDir, framework, kept, mailerProvider, capabilities, doInstall }
76
83
  }
77
84
 
78
- async function collectFromPrompts(argDir, capabilities) {
85
+ async function collectFromPrompts(argDir) {
79
86
  p.intro('create-stack — fork a base app, strip it to your selection')
80
87
 
81
88
  const name = cancelled(
@@ -127,26 +134,59 @@ async function collectFromPrompts(argDir, capabilities) {
127
134
  }),
128
135
  )
129
136
 
130
- const extraCaps = cancelled(
137
+ const capPicked = cancelled(
131
138
  await p.multiselect({
132
- message: 'Extra capabilities (optional added via add-capability)',
139
+ message: 'Capabilities (space to toggle, swappable behind a port)',
133
140
  required: false,
134
141
  initialValues: [],
135
- options: Object.keys(capabilities)
136
- .filter((c) => !BAKED_CAPS.has(c))
137
- .map((c) => ({ value: c, label: c, hint: capabilities[c].description?.slice(0, 40) })),
142
+ options: capabilityChoices(),
138
143
  }),
139
144
  )
140
145
 
146
+ const capabilities = {}
147
+ for (const cap of capPicked) {
148
+ const { defaultAdapter, options } = adapterChoices(cap)
149
+ capabilities[cap] = cancelled(
150
+ await p.select({
151
+ message: `${cap} adapter`,
152
+ options,
153
+ initialValue: defaultAdapter,
154
+ }),
155
+ )
156
+ }
157
+
141
158
  const doInstall = cancelled(
142
159
  await p.confirm({ message: 'Install dependencies and verify now?', initialValue: true }),
143
160
  )
144
161
 
145
162
  const { kept, mailerProvider } = normalize(picked, mailer)
146
- return { argDir, projectName, framework, kept, mailerProvider, extraCaps, doInstall }
163
+ return { argDir, projectName, framework, kept, mailerProvider, capabilities, doInstall }
147
164
  }
148
165
 
149
- function execute(a, patterns) {
166
+ const pnpmRun = (script, projectDir, opts = {}) =>
167
+ run('pnpm', ['--config.verify-deps-before-run=false', 'run', script], {
168
+ cwd: projectDir,
169
+ ...opts,
170
+ })
171
+
172
+ /** Install deps, normalize vendored imports, then report typecheck + biome status. */
173
+ function installAndVerify(projectDir, capabilities) {
174
+ p.log.step('pnpm install')
175
+ run('pnpm', ['install'], { cwd: projectDir })
176
+ // vendored capabilities rewrite cross-package imports (~/lib/http); let biome
177
+ // re-sort/normalize them so the initial commit is lint-clean.
178
+ if (Object.keys(capabilities ?? {}).length) {
179
+ pnpmRun('check:write', projectDir, { stdio: 'ignore' })
180
+ }
181
+ p.log.step('Verifying (typecheck + biome)')
182
+ const tc = pnpmRun('typecheck', projectDir)
183
+ const lint = pnpmRun('check', projectDir)
184
+ p.log[tc && lint ? 'success' : 'warn'](
185
+ tc && lint ? 'typecheck + biome clean' : 'verify reported issues (see output above)',
186
+ )
187
+ }
188
+
189
+ function execute(a) {
150
190
  const projectDir = resolve(process.cwd(), a.argDir ?? a.projectName)
151
191
  if (!isDirEmpty(projectDir)) {
152
192
  p.cancel(`Target directory is not empty: ${projectDir}`)
@@ -155,44 +195,40 @@ function execute(a, patterns) {
155
195
 
156
196
  const s = p.spinner()
157
197
  s.start('Forking + stripping the base app')
158
- buildProject({ ...a, projectDir, patterns })
198
+ buildProject({ ...a, projectDir })
159
199
  s.stop('Project scaffolded')
160
200
 
161
- if (a.doInstall) {
162
- p.log.step('pnpm install')
163
- run('pnpm', ['install'], { cwd: projectDir })
164
- p.log.step('Verifying (typecheck + biome)')
165
- const tc = run('pnpm', ['--config.verify-deps-before-run=false', 'run', 'typecheck'], {
166
- cwd: projectDir,
167
- })
168
- const lint = run('pnpm', ['--config.verify-deps-before-run=false', 'run', 'check'], {
169
- cwd: projectDir,
170
- })
171
- p.log[tc && lint ? 'success' : 'warn'](
172
- tc && lint ? 'typecheck + biome clean' : 'verify reported issues (see output above)',
173
- )
174
- }
201
+ if (a.doInstall) installAndVerify(projectDir, a.capabilities)
175
202
 
176
- // Initialize a fresh repo (also satisfies Biome's vcs.useIgnoreFile).
177
- if (run('git', ['init', '-q'], { cwd: projectDir })) {
203
+ // fresh repo + initial commit (also satisfies Biome vcs.useIgnoreFile).
204
+ // commit is best-effort: skipped if git identity unset, staged tree left in place.
205
+ if (run('git', ['-C', projectDir, 'init', '-q'])) {
178
206
  run('git', ['-C', projectDir, 'add', '-A'])
179
- p.log.step('git initialized')
207
+ const committed = run(
208
+ 'git',
209
+ ['-C', projectDir, 'commit', '-q', '-m', 'chore: initial commit from create-stack'],
210
+ { stdio: 'ignore' },
211
+ )
212
+ p.log.step(
213
+ committed
214
+ ? 'git repository initialized (initial commit created)'
215
+ : 'git repository initialized — set git user.name/email, then commit',
216
+ )
180
217
  }
181
218
 
182
219
  const keptMailer = a.mailerProvider !== 'none'
220
+ const capEntries = Object.entries(a.capabilities ?? {})
183
221
  const lines = [
184
222
  `Framework: ${a.framework === 'next' ? 'Next.js' : 'TanStack Start'}`,
185
223
  `Foundations: ${[...a.kept].sort().join(', ') || '(none)'}`,
186
224
  `Mailer: ${keptMailer ? a.mailerProvider : '(none)'}`,
225
+ `Capabilities: ${capEntries.map(([c, ad]) => `${c} (${ad})`).join(', ') || '(none)'}`,
226
+ '',
227
+ 'Add more tools later with the add-capability skill.',
228
+ '',
229
+ 'Next:',
230
+ ` cd ${a.argDir ?? a.projectName}`,
187
231
  ]
188
- if (a.extraCaps.length) {
189
- lines.push(
190
- '',
191
- 'Add the extra capabilities with the add-capability skill:',
192
- ...a.extraCaps.map((c) => ` • ${c}`),
193
- )
194
- }
195
- lines.push('', 'Next:', ` cd ${a.argDir ?? a.projectName}`)
196
232
  if (!a.doInstall) lines.push(' pnpm install')
197
233
  lines.push(' cp .env.example .env # fill in the values', ' pnpm dev')
198
234
  p.note(lines.join('\n'), 'Done')
@@ -200,20 +236,18 @@ function execute(a, patterns) {
200
236
  }
201
237
 
202
238
  async function main() {
203
- const patterns = loadPatterns()
204
- const capabilities = loadCapabilities()
205
239
  const args = parseArgs(process.argv.slice(2))
206
240
 
207
241
  const nonInteractive =
208
242
  args.flags.yes ||
209
243
  args.flags.y ||
210
- ['framework', 'foundations', 'mailer', 'caps', 'no-install'].some((k) => k in args.flags)
244
+ ['framework', 'foundations', 'mailer', 'no-install', ...CAPABILITIES].some(
245
+ (k) => k in args.flags,
246
+ )
211
247
 
212
- const answers = nonInteractive
213
- ? collectFromFlags(args, capabilities)
214
- : await collectFromPrompts(args._[0], capabilities)
248
+ const answers = nonInteractive ? collectFromFlags(args) : await collectFromPrompts(args._[0])
215
249
 
216
- execute(answers, patterns)
250
+ execute(answers)
217
251
  }
218
252
 
219
253
  main().catch((err) => {
package/lib/build.mjs CHANGED
@@ -1,6 +1,7 @@
1
- // Pure build phase (no prompts, no install) fork → strip → mailer → env →
2
- // identity. Shared by index.mjs (after the wizard) and the test harness.
1
+ // Pure build phase (no prompts/install): fork → strip → mailer → env → identity.
2
+ // Shared by index.mjs (post-wizard) and the test harness.
3
3
 
4
+ import { vendorCapability } from './capabilities.mjs'
4
5
  import { writeEnv } from './env.mjs'
5
6
  import { stampIdentity } from './identity.mjs'
6
7
  import { swapMailer } from './mailer.mjs'
@@ -13,10 +14,10 @@ import { join, pkgAddDeps, pkgRemoveDeps, pkgRemoveScripts, readJSON, writeJSON
13
14
  * @param {string} o.projectDir absolute target dir (must be empty)
14
15
  * @param {string} o.projectName
15
16
  * @param {'next'|'tanstack'} o.framework
16
- * @param {Set<string>} o.kept logical foundations to keep (deps pre-resolved)
17
+ * @param {Set<string>} o.kept foundations to keep (deps pre-resolved)
17
18
  * @param {'resend'|'brevo'|'ses'|'none'} o.mailerProvider
18
- * @param {object} o.patterns loadPatterns()
19
- * @returns {{ kept: string[], keptMailer: boolean, mailerProvider: string, envKeys: string[] }}
19
+ * @param {Record<string,string>} [o.capabilities] capability → adapter (e.g. { storage: 's3' })
20
+ * @returns {{ kept: string[], keptMailer: boolean, mailerProvider: string, capabilities: Record<string,string>, envKeys: string[] }}
20
21
  */
21
22
  export function buildProject({
22
23
  projectDir,
@@ -24,7 +25,7 @@ export function buildProject({
24
25
  framework,
25
26
  kept,
26
27
  mailerProvider,
27
- patterns,
28
+ capabilities = {},
28
29
  }) {
29
30
  const authKept = kept.has('better-auth')
30
31
  const keptMailer = mailerProvider !== 'none'
@@ -32,17 +33,26 @@ export function buildProject({
32
33
  forkBase(framework, projectDir)
33
34
  makeStandalone(projectDir, projectName, framework)
34
35
 
35
- const strip = stripFoundations({ projectDir, framework, kept, keptMailer, patterns })
36
+ const strip = stripFoundations({ projectDir, framework, kept, keptMailer })
36
37
  const mailer = keptMailer
37
38
  ? swapMailer(projectDir, mailerProvider)
38
39
  : { addDeps: {}, removeDeps: [], envKeys: [] }
39
40
 
41
+ // vendor each selected capability (core + adapter + composition root) into the fork.
42
+ const capAddDeps = {}
43
+ const capEnvKeys = []
44
+ for (const [cap, adapter] of Object.entries(capabilities)) {
45
+ const r = vendorCapability({ projectDir, framework, projectName, cap, adapter })
46
+ Object.assign(capAddDeps, r.addDeps)
47
+ capEnvKeys.push(...r.envKeys)
48
+ }
49
+
40
50
  const pkgPath = join(projectDir, 'package.json')
41
51
  const pkg = readJSON(pkgPath)
42
- pkg.description = `${projectName} — bootstrapped from the personal reference stack.`
52
+ pkg.description = `${projectName} — scaffolded from the personal reference stack.`
43
53
  pkgRemoveDeps(pkg, [...strip.removeDeps, ...mailer.removeDeps])
44
54
  pkgRemoveScripts(pkg, strip.removeScripts)
45
- pkgAddDeps(pkg, mailer.addDeps)
55
+ pkgAddDeps(pkg, { ...mailer.addDeps, ...capAddDeps })
46
56
  writeJSON(pkgPath, pkg)
47
57
 
48
58
  const envKeys = []
@@ -55,10 +65,10 @@ export function buildProject({
55
65
  'BETTER_AUTH_GOOGLE_CLIENT_SECRET',
56
66
  )
57
67
  }
58
- envKeys.push(...mailer.envKeys)
68
+ envKeys.push(...mailer.envKeys, ...capEnvKeys)
59
69
  writeEnv(projectDir, envKeys)
60
70
 
61
71
  stampIdentity(projectDir, projectName, framework)
62
72
 
63
- return { kept: [...kept], keptMailer, mailerProvider, envKeys }
73
+ return { kept: [...kept], keptMailer, mailerProvider, capabilities, envKeys }
64
74
  }