@alfredmouelle/create-stack 0.1.0 → 0.1.2

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 (81) hide show
  1. package/README.md +101 -40
  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 +4 -2
  7. package/_stack/apps/next-base/src/emails/components/context.tsx +3 -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 +12 -5
  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/mailer/src/adapters/brevo/index.ts +3 -3
  49. package/_stack/packages/mailer/src/adapters/resend/index.ts +3 -3
  50. package/_stack/packages/mailer/src/adapters/ses/config.ts +3 -3
  51. package/_stack/packages/mailer/src/adapters/ses/index.ts +4 -5
  52. package/index.mjs +33 -44
  53. package/lib/build.mjs +7 -15
  54. package/lib/env.mjs +5 -6
  55. package/lib/foundations.mjs +35 -0
  56. package/lib/identity.mjs +4 -5
  57. package/lib/mailer.mjs +9 -13
  58. package/lib/paths.mjs +15 -0
  59. package/lib/scaffold.mjs +65 -10
  60. package/lib/strip.mjs +9 -24
  61. package/lib/util.mjs +8 -9
  62. package/package.json +1 -1
  63. package/_stack/packages/analytics/capability.json +0 -26
  64. package/_stack/packages/cache/capability.json +0 -21
  65. package/_stack/packages/error-tracking/capability.json +0 -21
  66. package/_stack/packages/jobs/capability.json +0 -26
  67. package/_stack/packages/logger/capability.json +0 -21
  68. package/_stack/packages/mailer/capability.json +0 -28
  69. package/_stack/packages/storage/capability.json +0 -32
  70. package/_stack/patterns/README.md +0 -58
  71. package/_stack/patterns/_baseline/env.ts +0 -31
  72. package/_stack/patterns/_baseline/tsconfig.json +0 -27
  73. package/_stack/patterns/better-auth/pattern.json +0 -73
  74. package/_stack/patterns/better-auth-next/pattern.json +0 -76
  75. package/_stack/patterns/data-table/pattern.json +0 -43
  76. package/_stack/patterns/drizzle/pattern.json +0 -61
  77. package/_stack/patterns/trpc/pattern.json +0 -61
  78. package/_stack/patterns/trpc-next/pattern.json +0 -64
  79. package/lib/manifests.mjs +0 -61
  80. /package/{_stack/patterns/_baseline → templates}/README-author.md +0 -0
  81. /package/{_stack/patterns/_baseline → templates}/biome.jsonc +0 -0
package/index.mjs CHANGED
@@ -1,20 +1,13 @@
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'
14
9
  import { isDirEmpty, run } from './lib/util.mjs'
15
10
 
16
- // Capabilities baked into the base apps vs. the ones add-capability supplies.
17
- const BAKED_CAPS = new Set(['mailer', 'email-kit'])
18
11
  const ALL_FOUNDATIONS = ['drizzle', 'trpc', 'better-auth', 'data-table']
19
12
 
20
13
  const cancelled = (v) => {
@@ -54,7 +47,7 @@ const csv = (v) =>
54
47
  .filter(Boolean)
55
48
  : []
56
49
 
57
- /** Resolve hard deps + the mailer's better-auth requirement. */
50
+ /** Resolve hard deps + mailer's better-auth requirement. */
58
51
  function normalize(picked, mailer) {
59
52
  const kept = new Set(picked.filter((f) => ALL_FOUNDATIONS.includes(f)))
60
53
  if (kept.has('trpc') || kept.has('better-auth')) kept.add('drizzle')
@@ -63,19 +56,17 @@ function normalize(picked, mailer) {
63
56
  return { kept, mailerProvider }
64
57
  }
65
58
 
66
- function collectFromFlags(args, capabilities) {
59
+ function collectFromFlags(args) {
67
60
  const argDir = args._[0]
68
61
  if (!argDir) throw new Error('Project name is required (positional) in non-interactive mode')
69
62
  const framework = args.flags.framework === 'next' ? 'next' : 'tanstack'
70
63
  const picked = args.flags.foundations ? csv(args.flags.foundations) : [...ALL_FOUNDATIONS]
71
64
  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))
74
65
  const doInstall = !args.flags['no-install']
75
- return { argDir, projectName: argDir, framework, kept, mailerProvider, extraCaps, doInstall }
66
+ return { argDir, projectName: argDir, framework, kept, mailerProvider, doInstall }
76
67
  }
77
68
 
78
- async function collectFromPrompts(argDir, capabilities) {
69
+ async function collectFromPrompts(argDir) {
79
70
  p.intro('create-stack — fork a base app, strip it to your selection')
80
71
 
81
72
  const name = cancelled(
@@ -127,26 +118,15 @@ async function collectFromPrompts(argDir, capabilities) {
127
118
  }),
128
119
  )
129
120
 
130
- const extraCaps = cancelled(
131
- await p.multiselect({
132
- message: 'Extra capabilities (optional — added via add-capability)',
133
- required: false,
134
- 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) })),
138
- }),
139
- )
140
-
141
121
  const doInstall = cancelled(
142
122
  await p.confirm({ message: 'Install dependencies and verify now?', initialValue: true }),
143
123
  )
144
124
 
145
125
  const { kept, mailerProvider } = normalize(picked, mailer)
146
- return { argDir, projectName, framework, kept, mailerProvider, extraCaps, doInstall }
126
+ return { argDir, projectName, framework, kept, mailerProvider, doInstall }
147
127
  }
148
128
 
149
- function execute(a, patterns) {
129
+ function execute(a) {
150
130
  const projectDir = resolve(process.cwd(), a.argDir ?? a.projectName)
151
131
  if (!isDirEmpty(projectDir)) {
152
132
  p.cancel(`Target directory is not empty: ${projectDir}`)
@@ -155,7 +135,7 @@ function execute(a, patterns) {
155
135
 
156
136
  const s = p.spinner()
157
137
  s.start('Forking + stripping the base app')
158
- buildProject({ ...a, projectDir, patterns })
138
+ buildProject({ ...a, projectDir })
159
139
  s.stop('Project scaffolded')
160
140
 
161
141
  if (a.doInstall) {
@@ -173,20 +153,33 @@ function execute(a, patterns) {
173
153
  )
174
154
  }
175
155
 
156
+ // fresh repo + initial commit (also satisfies Biome vcs.useIgnoreFile).
157
+ // commit is best-effort: skipped if git identity unset, staged tree left in place.
158
+ if (run('git', ['-C', projectDir, 'init', '-q'])) {
159
+ run('git', ['-C', projectDir, 'add', '-A'])
160
+ const committed = run(
161
+ 'git',
162
+ ['-C', projectDir, 'commit', '-q', '-m', 'chore: initial commit from create-stack'],
163
+ { stdio: 'ignore' },
164
+ )
165
+ p.log.step(
166
+ committed
167
+ ? 'git repository initialized (initial commit created)'
168
+ : 'git repository initialized — set git user.name/email, then commit',
169
+ )
170
+ }
171
+
176
172
  const keptMailer = a.mailerProvider !== 'none'
177
173
  const lines = [
178
174
  `Framework: ${a.framework === 'next' ? 'Next.js' : 'TanStack Start'}`,
179
175
  `Foundations: ${[...a.kept].sort().join(', ') || '(none)'}`,
180
176
  `Mailer: ${keptMailer ? a.mailerProvider : '(none)'}`,
177
+ '',
178
+ 'Add more tools (storage, jobs, cache, analytics, …) with the add-capability skill.',
179
+ '',
180
+ 'Next:',
181
+ ` cd ${a.argDir ?? a.projectName}`,
181
182
  ]
182
- if (a.extraCaps.length) {
183
- lines.push(
184
- '',
185
- 'Add the extra capabilities with the add-capability skill:',
186
- ...a.extraCaps.map((c) => ` • ${c}`),
187
- )
188
- }
189
- lines.push('', 'Next:', ` cd ${a.argDir ?? a.projectName}`)
190
183
  if (!a.doInstall) lines.push(' pnpm install')
191
184
  lines.push(' cp .env.example .env # fill in the values', ' pnpm dev')
192
185
  p.note(lines.join('\n'), 'Done')
@@ -194,20 +187,16 @@ function execute(a, patterns) {
194
187
  }
195
188
 
196
189
  async function main() {
197
- const patterns = loadPatterns()
198
- const capabilities = loadCapabilities()
199
190
  const args = parseArgs(process.argv.slice(2))
200
191
 
201
192
  const nonInteractive =
202
193
  args.flags.yes ||
203
194
  args.flags.y ||
204
- ['framework', 'foundations', 'mailer', 'caps', 'no-install'].some((k) => k in args.flags)
195
+ ['framework', 'foundations', 'mailer', 'no-install'].some((k) => k in args.flags)
205
196
 
206
- const answers = nonInteractive
207
- ? collectFromFlags(args, capabilities)
208
- : await collectFromPrompts(args._[0], capabilities)
197
+ const answers = nonInteractive ? collectFromFlags(args) : await collectFromPrompts(args._[0])
209
198
 
210
- execute(answers, patterns)
199
+ execute(answers)
211
200
  }
212
201
 
213
202
  main().catch((err) => {
package/lib/build.mjs CHANGED
@@ -1,5 +1,5 @@
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
4
  import { writeEnv } from './env.mjs'
5
5
  import { stampIdentity } from './identity.mjs'
@@ -13,33 +13,25 @@ import { join, pkgAddDeps, pkgRemoveDeps, pkgRemoveScripts, readJSON, writeJSON
13
13
  * @param {string} o.projectDir absolute target dir (must be empty)
14
14
  * @param {string} o.projectName
15
15
  * @param {'next'|'tanstack'} o.framework
16
- * @param {Set<string>} o.kept logical foundations to keep (deps pre-resolved)
16
+ * @param {Set<string>} o.kept foundations to keep (deps pre-resolved)
17
17
  * @param {'resend'|'brevo'|'ses'|'none'} o.mailerProvider
18
- * @param {object} o.patterns loadPatterns()
19
18
  * @returns {{ kept: string[], keptMailer: boolean, mailerProvider: string, envKeys: string[] }}
20
19
  */
21
- export function buildProject({
22
- projectDir,
23
- projectName,
24
- framework,
25
- kept,
26
- mailerProvider,
27
- patterns,
28
- }) {
20
+ export function buildProject({ projectDir, projectName, framework, kept, mailerProvider }) {
29
21
  const authKept = kept.has('better-auth')
30
22
  const keptMailer = mailerProvider !== 'none'
31
23
 
32
24
  forkBase(framework, projectDir)
33
- makeStandalone(projectDir, projectName)
25
+ makeStandalone(projectDir, projectName, framework)
34
26
 
35
- const strip = stripFoundations({ projectDir, framework, kept, keptMailer, patterns })
27
+ const strip = stripFoundations({ projectDir, framework, kept, keptMailer })
36
28
  const mailer = keptMailer
37
29
  ? swapMailer(projectDir, mailerProvider)
38
30
  : { addDeps: {}, removeDeps: [], envKeys: [] }
39
31
 
40
32
  const pkgPath = join(projectDir, 'package.json')
41
33
  const pkg = readJSON(pkgPath)
42
- pkg.description = `${projectName} — bootstrapped from the personal reference stack.`
34
+ pkg.description = `${projectName} — scaffolded from the personal reference stack.`
43
35
  pkgRemoveDeps(pkg, [...strip.removeDeps, ...mailer.removeDeps])
44
36
  pkgRemoveScripts(pkg, strip.removeScripts)
45
37
  pkgAddDeps(pkg, mailer.addDeps)
package/lib/env.mjs CHANGED
@@ -1,10 +1,9 @@
1
- // Rebuilds src/env.ts `server` + `runtimeEnv` blocks from the final key set,
2
- // and generates .env.example + .env. Adds keys (e.g. a swapped mailer provider)
3
- // and prunes keys of stripped foundations/capabilities — deterministically.
1
+ // Rebuilds src/env.ts `server` + `runtimeEnv` from the final key set; generates
2
+ // .env.example + .env. Adds swapped-mailer keys, prunes stripped ones, deterministically.
4
3
 
5
4
  import { editFile, join, write } from './util.mjs'
6
5
 
7
- /** valibot schema text per known env key. */
6
+ /** valibot schema text per env key. */
8
7
  const SCHEMAS = {
9
8
  DATABASE_URL: 'v.pipe(v.string(), v.url())',
10
9
  BETTER_AUTH_URL: 'v.pipe(v.string(), v.url())',
@@ -19,7 +18,7 @@ const SCHEMAS = {
19
18
  AWS_SECRET_ACCESS_KEY: 'v.optional(v.pipe(v.string(), v.minLength(1)))',
20
19
  }
21
20
 
22
- /** Placeholder values for the generated .env files. */
21
+ /** Placeholder values for generated .env files. */
23
22
  const PLACEHOLDERS = {
24
23
  DATABASE_URL: 'postgres://postgres:postgres@localhost:5432/app',
25
24
  BETTER_AUTH_URL: 'http://localhost:3000',
@@ -29,7 +28,7 @@ const PLACEHOLDERS = {
29
28
 
30
29
  const indent = (s) => ` ${s}`
31
30
 
32
- /** Write the final env.ts and .env files. `keys` is an ordered string[]. */
31
+ /** Write final env.ts + .env files. `keys`: ordered string[]. */
33
32
  export function writeEnv(projectDir, keys) {
34
33
  const seen = new Set()
35
34
  const ordered = keys.filter((k) => SCHEMAS[k] && !seen.has(k) && seen.add(k))
@@ -0,0 +1,35 @@
1
+ // Foundations the CLI can strip + the npm footprint each adds (deps/devDeps/scripts only).
2
+ // File deletes, env keys, code seams live in strip.mjs/build.mjs (need framework-specific surgery).
3
+
4
+ export const FOUNDATIONS = ['drizzle', 'trpc', 'better-auth', 'data-table']
5
+
6
+ const DATA = {
7
+ drizzle: {
8
+ deps: ['drizzle-orm', 'pg'],
9
+ devDeps: ['drizzle-kit', 'dotenv', 'tsx', '@types/pg', '@faker-js/faker'],
10
+ scripts: ['db:generate', 'db:migrate', 'db:push', 'db:studio', 'db:seed'],
11
+ },
12
+ trpc: {
13
+ deps: ['@trpc/server', '@trpc/client', '@tanstack/react-query', 'superjson', 'valibot'],
14
+ // React Query bridge differs per framework
15
+ perFramework: { tanstack: ['@trpc/tanstack-react-query'], next: ['@trpc/react-query'] },
16
+ },
17
+ 'better-auth': {
18
+ deps: ['better-auth'],
19
+ },
20
+ 'data-table': {
21
+ deps: ['@tanstack/react-table'],
22
+ },
23
+ }
24
+
25
+ /** Every npm dep (prod + dev) a foundation contributes, for `framework`. */
26
+ export function foundationDeps(name, framework) {
27
+ const d = DATA[name]
28
+ if (!d) return []
29
+ return [...(d.deps ?? []), ...(d.devDeps ?? []), ...(d.perFramework?.[framework] ?? [])]
30
+ }
31
+
32
+ /** package.json script names a foundation adds. */
33
+ export function foundationScripts(name) {
34
+ return DATA[name]?.scripts ?? []
35
+ }
package/lib/identity.mjs CHANGED
@@ -1,7 +1,6 @@
1
- // Step A4 — stamp the project identity: title/meta + a README that ends with
2
- // the stack's `# Author` footer verbatim.
1
+ // Stamp project identity: title/meta + README ending with the `# Author` footer verbatim.
3
2
 
4
- import { STACK_ROOT } from './manifests.mjs'
3
+ import { TEMPLATES } from './paths.mjs'
5
4
  import { editFile, join, read, write } from './util.mjs'
6
5
 
7
6
  const titleFiles = {
@@ -10,12 +9,12 @@ const titleFiles = {
10
9
  }
11
10
 
12
11
  export function stampIdentity(projectDir, projectName, framework) {
13
- // Swap the placeholder title 'App' in the root document/metadata.
12
+ // swap placeholder title 'App' in root document/metadata
14
13
  for (const rel of titleFiles[framework === 'next' ? 'next' : 'tanstack']) {
15
14
  editFile(join(projectDir, rel), (c) => c.replaceAll("title: 'App'", `title: '${projectName}'`))
16
15
  }
17
16
 
18
- const footer = read(join(STACK_ROOT, 'patterns/_baseline/README-author.md'))
17
+ const footer = read(join(TEMPLATES, 'README-author.md'))
19
18
  const readme = `# ${projectName}
20
19
 
21
20
  Bootstrapped from the personal reference stack.
package/lib/mailer.mjs CHANGED
@@ -1,13 +1,12 @@
1
- // Mailer provider swap. The base inlines the Resend adapter; if the user picks
2
- // another provider we swap the adapter files + the composition root (email/index.ts)
3
- // and return the dep/env deltas. Mirrors the mailer capability manifest.
1
+ // Mailer provider swap. Base inlines Resend; other providers swap adapter files +
2
+ // composition root (email/index.ts) and return dep/env deltas. Mirrors the mailer manifest.
4
3
 
5
- import { STACK_ROOT } from './manifests.mjs'
4
+ import { STACK_ROOT } from './paths.mjs'
6
5
  import { copy, join, readJSON, remove, write } from './util.mjs'
7
6
 
8
7
  const EMAIL_DIR = 'src/server/email'
9
8
 
10
- /** getMailer() body per provider (composition root in email/index.ts). */
9
+ /** getMailer() body per provider (composition root). */
11
10
  const FACTORY = {
12
11
  brevo: {
13
12
  import: "import { brevoAdapter } from './adapters/brevo/index'",
@@ -17,7 +16,7 @@ const FACTORY = {
17
16
  },
18
17
  ses: {
19
18
  import: "import { sesAdapter } from './adapters/ses/index'",
20
- // SESv2 SDK reads AWS_REGION / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from env.
19
+ // SESv2 SDK reads AWS_REGION / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY from env
21
20
  adapter: 'sesAdapter()',
22
21
  envKeys: ['EMAIL_FROM', 'AWS_REGION', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'],
23
22
  pkgDep: '@aws-sdk/client-sesv2',
@@ -63,10 +62,7 @@ export async function sendEmail(params: {
63
62
  }
64
63
  `
65
64
 
66
- /**
67
- * Swap the inlined mailer to `provider`. Returns { addDeps, removeDeps, envKeys }.
68
- * provider === 'resend' is a no-op (the base default).
69
- */
65
+ /** Swap inlined mailer to `provider` → { addDeps, removeDeps, envKeys }. 'resend' is a no-op (base default). */
70
66
  export function swapMailer(projectDir, provider) {
71
67
  if (provider === 'resend') {
72
68
  return { addDeps: {}, removeDeps: [], envKeys: ['EMAIL_FROM', 'RESEND_API_KEY'] }
@@ -74,17 +70,17 @@ export function swapMailer(projectDir, provider) {
74
70
  const cfg = FACTORY[provider]
75
71
  if (!cfg) throw new Error(`Unknown mailer provider: ${provider}`)
76
72
 
77
- // Swap adapter files: drop resend, copy the chosen adapter from the package.
73
+ // swap adapter files: drop resend, copy chosen adapter from package
78
74
  remove(join(projectDir, EMAIL_DIR, 'adapters/resend'))
79
75
  copy(
80
76
  join(STACK_ROOT, 'packages/mailer/src/adapters', provider),
81
77
  join(projectDir, EMAIL_DIR, 'adapters', provider),
82
78
  )
83
79
 
84
- // Rewrite the composition root.
80
+ // rewrite composition root
85
81
  write(join(projectDir, EMAIL_DIR, 'index.ts'), INDEX_TS(cfg))
86
82
 
87
- // Dep delta — pull the provider's range from the mailer package manifest.
83
+ // dep delta — pull provider's range from mailer package manifest
88
84
  const mailerPkg = readJSON(join(STACK_ROOT, 'packages/mailer/package.json'))
89
85
  const range = mailerPkg.dependencies?.[cfg.pkgDep] ?? 'latest'
90
86
  return {
package/lib/paths.mjs ADDED
@@ -0,0 +1,15 @@
1
+ // Where the bundled stack assets live: cli/_stack when published (scripts/bundle.mjs), repo root in dev.
2
+
3
+ import { existsSync } from 'node:fs'
4
+ import { dirname, resolve } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const here = dirname(fileURLToPath(import.meta.url))
8
+ const bundled = resolve(here, '..', '_stack')
9
+
10
+ // Forkable source (base apps + mailer adapters): _stack when published, monorepo root in dev.
11
+ export const STACK_ROOT = existsSync(bundled) ? bundled : resolve(here, '..', '..')
12
+
13
+ // CLI-owned templates injected into the fork (root-wiring variants, biome.jsonc,
14
+ // # Author README footer). Always shipped in the package.
15
+ export const TEMPLATES = resolve(here, '..', 'templates')
package/lib/scaffold.mjs CHANGED
@@ -1,7 +1,7 @@
1
- // Step A2 — fork a base app into the target dir and make it standalone
2
- // (its own Biome config, pnpm workspace + build allowlist, project name).
1
+ // Fork a base app into the target dir + make it standalone
2
+ // (own Biome config, pnpm workspace + build allowlist, project name).
3
3
 
4
- import { STACK_ROOT } from './manifests.mjs'
4
+ import { STACK_ROOT, TEMPLATES } from './paths.mjs'
5
5
  import { copy, exists, join, readJSON, run, write, writeJSON } from './util.mjs'
6
6
 
7
7
  const RSYNC_EXCLUDES = [
@@ -22,7 +22,59 @@ const PNPM_WORKSPACE = `allowBuilds:
22
22
  lightningcss: true
23
23
  `
24
24
 
25
- /** Copy the base app into projectDir, minus build output & generated files. */
25
+ // Generated explicitly: npm strips `.gitignore` from published tarballs, so the
26
+ // bundled base app's copy won't survive. Both keep `.env.example`.
27
+ const GITIGNORE = {
28
+ tanstack: `node_modules
29
+ .DS_Store
30
+ dist
31
+ dist-ssr
32
+ *.local
33
+ .env
34
+ .nitro
35
+ .tanstack
36
+ .wrangler
37
+ .output
38
+ .vinxi
39
+ __unconfig*
40
+ `,
41
+ next: `# dependencies
42
+ /node_modules
43
+ /.pnp
44
+ .pnp.*
45
+
46
+ # testing
47
+ /coverage
48
+
49
+ # next.js
50
+ /.next/
51
+ /out/
52
+
53
+ # production
54
+ /build
55
+
56
+ # misc
57
+ .DS_Store
58
+ *.pem
59
+
60
+ # debug
61
+ npm-debug.log*
62
+ .pnpm-debug.log*
63
+
64
+ # env files (keep .env.example committed)
65
+ .env
66
+ .env*.local
67
+
68
+ # vercel
69
+ .vercel
70
+
71
+ # typescript
72
+ *.tsbuildinfo
73
+ next-env.d.ts
74
+ `,
75
+ }
76
+
77
+ /** Copy base app into projectDir, minus build output & generated files. */
26
78
  export function forkBase(framework, projectDir) {
27
79
  const base = join(STACK_ROOT, 'apps', framework === 'next' ? 'next-base' : 'tanstack-base')
28
80
  if (!exists(base)) throw new Error(`Base app not found: ${base}`)
@@ -32,18 +84,21 @@ export function forkBase(framework, projectDir) {
32
84
  if (!run('rsync', args)) throw new Error('rsync failed while forking the base app')
33
85
  }
34
86
 
35
- /** Make the fork standalone (Biome, pnpm workspace, package.json identity). */
36
- export function makeStandalone(projectDir, projectName) {
37
- // A fork needs its own Biome config (the base inherits the monorepo root's).
38
- copy(join(STACK_ROOT, 'patterns/_baseline/biome.jsonc'), join(projectDir, 'biome.jsonc'))
87
+ /** Make the fork standalone (Biome, pnpm workspace, .gitignore, identity). */
88
+ export function makeStandalone(projectDir, projectName, framework) {
89
+ // fork needs its own Biome config (base inherits the monorepo root's)
90
+ copy(join(TEMPLATES, 'biome.jsonc'), join(projectDir, 'biome.jsonc'))
91
+
92
+ // Biome vcs.useIgnoreFile needs a .gitignore
93
+ write(join(projectDir, '.gitignore'), GITIGNORE[framework === 'next' ? 'next' : 'tanstack'])
39
94
 
40
- // Avoid ERR_PNPM_IGNORED_BUILDS on a fresh install (native build scripts).
95
+ // avoid ERR_PNPM_IGNORED_BUILDS on fresh install (native build scripts)
41
96
  write(join(projectDir, 'pnpm-workspace.yaml'), PNPM_WORKSPACE)
42
97
 
43
98
  const pkgPath = join(projectDir, 'package.json')
44
99
  const pkg = readJSON(pkgPath)
45
100
  pkg.name = projectName
46
- delete pkg.private // a leaf project; let the user decide
101
+ delete pkg.private // leaf project
47
102
  pkg.private = true
48
103
  writeJSON(pkgPath, pkg)
49
104
  }
package/lib/strip.mjs CHANGED
@@ -1,43 +1,28 @@
1
- // Step A3 reverse-strip the foundations the user did NOT select.
2
- // Whole-directory deletes (robust against orphans) + the few code "seams"
3
- // that need surgery (trpc/auth wiring) via shipped reduced variants.
1
+ // Strip unselected foundations: whole-dir deletes + code-seam variants (trpc/auth).
4
2
 
5
- import { dirname } from 'node:path'
6
- import { fileURLToPath } from 'node:url'
7
- import { foundationManifest } from './manifests.mjs'
3
+ import { FOUNDATIONS, foundationDeps, foundationScripts } from './foundations.mjs'
4
+ import { TEMPLATES } from './paths.mjs'
8
5
  import { copy, editFile, join, remove } from './util.mjs'
9
6
 
10
- const here = dirname(fileURLToPath(import.meta.url))
11
- const tpl = (rel) => join(here, '..', 'templates', rel)
7
+ const tpl = (rel) => join(TEMPLATES, rel)
12
8
 
13
9
  const ALWAYS_KEEP = new Set(['valibot'])
14
10
 
15
- const manifestDeps = (m) => [...(m?.deps ?? []), ...(m?.devDeps ?? [])]
16
-
17
- /** Logical foundations always present in a base app. */
18
- export const FOUNDATIONS = ['drizzle', 'trpc', 'better-auth', 'data-table']
19
-
20
- /**
21
- * Strip the unselected foundations from the fork.
22
- * @returns {{ removeDeps: string[], removeScripts: string[] }}
23
- */
24
- export function stripFoundations({ projectDir, framework, kept, keptMailer, patterns }) {
11
+ /** @returns {{ removeDeps: string[], removeScripts: string[] }} */
12
+ export function stripFoundations({ projectDir, framework, kept, keptMailer }) {
25
13
  const next = framework === 'next'
26
14
  const src = (p) => join(projectDir, 'src', p)
27
15
  const dropped = FOUNDATIONS.filter((f) => !kept.has(f))
28
16
 
29
17
  // Dep diff: remove a dropped foundation's deps unless a kept one still needs it.
30
- const keptDeps = new Set(
31
- [...kept].flatMap((f) => manifestDeps(patterns[foundationManifest(f, framework)])),
32
- )
18
+ const keptDeps = new Set([...kept].flatMap((f) => foundationDeps(f, framework)))
33
19
  const removeDeps = new Set()
34
20
  const removeScripts = new Set()
35
21
  for (const f of dropped) {
36
- const m = patterns[foundationManifest(f, framework)]
37
- for (const d of manifestDeps(m)) {
22
+ for (const d of foundationDeps(f, framework)) {
38
23
  if (!keptDeps.has(d) && !ALWAYS_KEEP.has(d)) removeDeps.add(d)
39
24
  }
40
- for (const s of Object.keys(m?.scripts ?? {})) removeScripts.add(s)
25
+ for (const s of foundationScripts(f)) removeScripts.add(s)
41
26
  }
42
27
 
43
28
  // --- data-table ---
package/lib/util.mjs CHANGED
@@ -1,5 +1,4 @@
1
- // Small fs / exec / package.json helpers shared by the CLI modules.
2
- // No external deps — keep the CLI lean and instantly runnable.
1
+ // fs / exec / package.json helpers shared by the CLI. No external deps.
3
2
 
4
3
  import { spawnSync } from 'node:child_process'
5
4
  import {
@@ -22,18 +21,18 @@ export const exists = (p) => existsSync(p)
22
21
  export const readJSON = (p) => JSON.parse(read(p))
23
22
  export const writeJSON = (p, obj) => write(p, `${JSON.stringify(obj, null, 2)}\n`)
24
23
 
25
- /** Remove a file or directory if it exists (recursive, never throws on absent). */
24
+ /** Remove file/dir if present (recursive, never throws on absent). */
26
25
  export const remove = (p) => {
27
26
  if (existsSync(p)) rmSync(p, { recursive: true, force: true })
28
27
  }
29
28
 
30
- /** Copy a file or directory tree. */
29
+ /** Copy a file/dir tree. */
31
30
  export const copy = (from, to) => {
32
31
  mkdirSync(dirname(to), { recursive: true })
33
32
  cpSync(from, to, { recursive: true })
34
33
  }
35
34
 
36
- /** Edit a file in place via a (content) => content transform. No-op if absent. */
35
+ /** Edit a file in place via (content) => content. No-op if absent. */
37
36
  export const editFile = (p, fn) => {
38
37
  if (!existsSync(p)) return false
39
38
  const next = fn(read(p))
@@ -41,20 +40,20 @@ export const editFile = (p, fn) => {
41
40
  return true
42
41
  }
43
42
 
44
- /** Is a directory empty (or absent)? Ignores common noise files. */
43
+ /** Dir empty (or absent)? Ignores noise files. */
45
44
  export const isDirEmpty = (p) => {
46
45
  if (!existsSync(p)) return true
47
46
  const noise = new Set(['.git', '.DS_Store'])
48
47
  return readdirSync(p).every((f) => noise.has(f))
49
48
  }
50
49
 
51
- /** Run a command, inheriting stdio. Returns true on exit 0. */
50
+ /** Run a command (inherits stdio). True on exit 0. */
52
51
  export const run = (cmd, args, opts = {}) => {
53
52
  const res = spawnSync(cmd, args, { stdio: 'inherit', ...opts })
54
53
  return res.status === 0
55
54
  }
56
55
 
57
- /** Run a command capturing stdout (trimmed). Returns '' on failure. */
56
+ /** Run a command, capturing trimmed stdout. '' on failure. */
58
57
  export const runCapture = (cmd, args, opts = {}) => {
59
58
  const res = spawnSync(cmd, args, { encoding: 'utf8', ...opts })
60
59
  return res.status === 0 ? (res.stdout || '').trim() : ''
@@ -62,7 +61,7 @@ export const runCapture = (cmd, args, opts = {}) => {
62
61
 
63
62
  export { join }
64
63
 
65
- // --- package.json helpers (operate on a parsed object, mutate in place) ---
64
+ // --- package.json helpers (mutate parsed object in place) ---
66
65
 
67
66
  export const pkgRemoveDeps = (pkg, names) => {
68
67
  for (const field of ['dependencies', 'devDependencies']) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfredmouelle/create-stack",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "Interactive, deterministic installer for the personal reference stack — forks a base app (Next.js / TanStack Start) and strips it to your selection.",
6
6
  "author": {
@@ -1,26 +0,0 @@
1
- {
2
- "$schema": "../../capability.schema.json",
3
- "name": "analytics",
4
- "description": "Product analytics behind a swappable port. Capture events and identify users; flush/shutdown to drain pending events.",
5
- "port": "src/core/port.ts",
6
- "defaultAdapter": "posthog",
7
- "adapters": {
8
- "posthog": {
9
- "deps": ["posthog-node"],
10
- "env": ["POSTHOG_API_KEY", "POSTHOG_HOST"],
11
- "files": ["src/adapters/posthog"]
12
- },
13
- "plausible": {
14
- "deps": ["@alfredmouelle/http"],
15
- "env": ["PLAUSIBLE_DOMAIN", "PLAUSIBLE_API_HOST"],
16
- "files": ["src/adapters/plausible"]
17
- },
18
- "noop": {
19
- "deps": [],
20
- "env": [],
21
- "files": ["src/adapters/noop"]
22
- }
23
- },
24
- "sharedDeps": ["valibot"],
25
- "sharedFiles": ["src/core", "src/index.ts"]
26
- }
@@ -1,21 +0,0 @@
1
- {
2
- "$schema": "../../capability.schema.json",
3
- "name": "cache",
4
- "description": "Key/value cache behind a swappable port. Values are serialized as JSON for remote stores.",
5
- "port": "src/core/port.ts",
6
- "defaultAdapter": "redis",
7
- "adapters": {
8
- "redis": {
9
- "deps": ["ioredis"],
10
- "env": ["REDIS_URL"],
11
- "files": ["src/adapters/redis"]
12
- },
13
- "memory": {
14
- "deps": [],
15
- "env": [],
16
- "files": ["src/adapters/memory"]
17
- }
18
- },
19
- "sharedDeps": ["valibot"],
20
- "sharedFiles": ["src/core", "src/index.ts"]
21
- }