@alfredmouelle/create-stack 0.1.1 → 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 +8 -13
  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/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 +30 -47
  53. package/lib/build.mjs +6 -14
  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 +11 -11
  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/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
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,8 +22,8 @@ const PNPM_WORKSPACE = `allowBuilds:
22
22
  lightningcss: true
23
23
  `
24
24
 
25
- // Generated explicitly: npm strips `.gitignore` from published tarballs, so we
26
- // can't rely on the bundled base app's copy surviving. Both keep `.env.example`.
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
27
  const GITIGNORE = {
28
28
  tanstack: `node_modules
29
29
  .DS_Store
@@ -74,7 +74,7 @@ next-env.d.ts
74
74
  `,
75
75
  }
76
76
 
77
- /** Copy the base app into projectDir, minus build output & generated files. */
77
+ /** Copy base app into projectDir, minus build output & generated files. */
78
78
  export function forkBase(framework, projectDir) {
79
79
  const base = join(STACK_ROOT, 'apps', framework === 'next' ? 'next-base' : 'tanstack-base')
80
80
  if (!exists(base)) throw new Error(`Base app not found: ${base}`)
@@ -86,19 +86,19 @@ export function forkBase(framework, projectDir) {
86
86
 
87
87
  /** Make the fork standalone (Biome, pnpm workspace, .gitignore, identity). */
88
88
  export function makeStandalone(projectDir, projectName, framework) {
89
- // A fork needs its own Biome config (the base inherits the monorepo root's).
90
- copy(join(STACK_ROOT, 'patterns/_baseline/biome.jsonc'), join(projectDir, 'biome.jsonc'))
89
+ // fork needs its own Biome config (base inherits the monorepo root's)
90
+ copy(join(TEMPLATES, 'biome.jsonc'), join(projectDir, 'biome.jsonc'))
91
91
 
92
- // Biome's vcs.useIgnoreFile needs a .gitignore; also good project hygiene.
92
+ // Biome vcs.useIgnoreFile needs a .gitignore
93
93
  write(join(projectDir, '.gitignore'), GITIGNORE[framework === 'next' ? 'next' : 'tanstack'])
94
94
 
95
- // Avoid ERR_PNPM_IGNORED_BUILDS on a fresh install (native build scripts).
95
+ // avoid ERR_PNPM_IGNORED_BUILDS on fresh install (native build scripts)
96
96
  write(join(projectDir, 'pnpm-workspace.yaml'), PNPM_WORKSPACE)
97
97
 
98
98
  const pkgPath = join(projectDir, 'package.json')
99
99
  const pkg = readJSON(pkgPath)
100
100
  pkg.name = projectName
101
- delete pkg.private // a leaf project; let the user decide
101
+ delete pkg.private // leaf project
102
102
  pkg.private = true
103
103
  writeJSON(pkgPath, pkg)
104
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.1",
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
- }
@@ -1,21 +0,0 @@
1
- {
2
- "$schema": "../../capability.schema.json",
3
- "name": "error-tracking",
4
- "description": "Error reporting behind a swappable port. Capture exceptions, messages, breadcrumbs and user context, then ship them to a provider.",
5
- "port": "src/core/port.ts",
6
- "defaultAdapter": "sentry",
7
- "adapters": {
8
- "sentry": {
9
- "deps": ["@sentry/node"],
10
- "env": ["SENTRY_DSN", "SENTRY_ENVIRONMENT"],
11
- "files": ["src/adapters/sentry"]
12
- },
13
- "console": {
14
- "deps": [],
15
- "env": [],
16
- "files": ["src/adapters/console"]
17
- }
18
- },
19
- "sharedDeps": ["valibot"],
20
- "sharedFiles": ["src/core", "src/index.ts"]
21
- }
@@ -1,26 +0,0 @@
1
- {
2
- "$schema": "../../capability.schema.json",
3
- "name": "jobs",
4
- "description": "Background jobs / events behind a swappable port. Event-driven: define jobs against named events and trigger them; the adapter handles delivery and execution.",
5
- "port": "src/core/port.ts",
6
- "defaultAdapter": "inngest",
7
- "adapters": {
8
- "inngest": {
9
- "deps": ["inngest"],
10
- "env": ["INNGEST_EVENT_KEY", "INNGEST_SIGNING_KEY"],
11
- "files": ["src/adapters/inngest"]
12
- },
13
- "trigger": {
14
- "deps": ["@trigger.dev/sdk"],
15
- "env": ["TRIGGER_SECRET_KEY"],
16
- "files": ["src/adapters/trigger"]
17
- },
18
- "memory": {
19
- "deps": [],
20
- "env": [],
21
- "files": ["src/adapters/memory"]
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": "logger",
4
- "description": "Structured logging behind a swappable port. Application code depends only on the Logger interface; pick an adapter (pino, console) at the composition root.",
5
- "port": "src/core/port.ts",
6
- "defaultAdapter": "pino",
7
- "adapters": {
8
- "pino": {
9
- "deps": ["pino"],
10
- "env": [],
11
- "files": ["src/adapters/pino"]
12
- },
13
- "console": {
14
- "deps": [],
15
- "env": [],
16
- "files": ["src/adapters/console"]
17
- }
18
- },
19
- "sharedDeps": ["valibot"],
20
- "sharedFiles": ["src/core", "src/index.ts"]
21
- }
@@ -1,28 +0,0 @@
1
- {
2
- "$schema": "../../capability.schema.json",
3
- "name": "mailer",
4
- "description": "Transactional email. Bodies are always React Email components, rendered to HTML + plain text.",
5
- "port": "src/core/port.ts",
6
- "factory": "src/factory.ts",
7
- "defaultAdapter": "resend",
8
- "adapters": {
9
- "resend": {
10
- "deps": ["resend"],
11
- "env": ["RESEND_API_KEY"],
12
- "files": ["src/adapters/resend"]
13
- },
14
- "brevo": {
15
- "deps": ["@getbrevo/brevo"],
16
- "env": ["BREVO_API_KEY"],
17
- "files": ["src/adapters/brevo"]
18
- },
19
- "ses": {
20
- "deps": ["@aws-sdk/client-sesv2"],
21
- "env": ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
22
- "files": ["src/adapters/ses"]
23
- }
24
- },
25
- "sharedDeps": ["valibot", "@react-email/render"],
26
- "peerDeps": ["react", "react-dom"],
27
- "sharedFiles": ["src/core", "src/factory.ts", "src/index.ts"]
28
- }
@@ -1,32 +0,0 @@
1
- {
2
- "$schema": "../../capability.schema.json",
3
- "name": "storage",
4
- "description": "Object storage behind a swappable port: put/get/delete/exists plus signed URLs. Adapters for S3, Cloudflare R2, Google Cloud Storage and the local filesystem.",
5
- "port": "src/core/port.ts",
6
- "factory": "src/index.ts",
7
- "defaultAdapter": "s3",
8
- "adapters": {
9
- "s3": {
10
- "deps": ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"],
11
- "env": ["S3_BUCKET", "S3_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
12
- "files": ["src/adapters/s3"]
13
- },
14
- "r2": {
15
- "deps": ["@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner"],
16
- "env": ["R2_BUCKET", "R2_ACCOUNT_ID", "R2_ACCESS_KEY_ID", "R2_SECRET_ACCESS_KEY"],
17
- "files": ["src/adapters/r2", "src/adapters/s3"]
18
- },
19
- "gcs": {
20
- "deps": ["@google-cloud/storage"],
21
- "env": ["GCS_BUCKET", "GOOGLE_CLOUD_PROJECT"],
22
- "files": ["src/adapters/gcs"]
23
- },
24
- "local": {
25
- "deps": [],
26
- "env": ["STORAGE_LOCAL_DIR"],
27
- "files": ["src/adapters/local"]
28
- }
29
- },
30
- "sharedDeps": ["valibot"],
31
- "sharedFiles": ["src/core", "src/index.ts"]
32
- }
@@ -1,58 +0,0 @@
1
- # Patterns
2
-
3
- Foundational, framework-coupled **patterns** the `bootstrap` skill vendors into a
4
- freshly scaffolded app — the counterpart to `packages/` (swappable capabilities).
5
-
6
- A *capability* is a provider behind a port (mailer, storage, …), swappable by
7
- changing one line. A *pattern* is a foundation you don't swap but always set up
8
- the same way: tRPC wiring, the better-auth instance, the Drizzle client. They are
9
- **framework-coupled** (currently `tanstack-start`, mirrored from the reference
10
- base apps) and depend on each other.
11
-
12
- **The code lives in the base apps, not here.** `patterns/` is a pure *manifest
13
- layer*: each `pattern.json` describes a foundation (how to detect it, its deps,
14
- env, framework, dependencies) and lists the files that make it up — by pointing
15
- **into the base apps** (`apps/tanstack-base`, `apps/next-base`), the single source
16
- of truth. No code is duplicated.
17
-
18
- Each pattern is `<name>/pattern.json` (see `../pattern.schema.json`).
19
- `_baseline/` is special: real always-applied config files (Biome, tsconfig, env
20
- skeleton, the `# Author` README footer) that a standalone fork needs but the base
21
- apps don't carry on their own (they inherit the monorepo's Biome).
22
-
23
- ## How the skills use these
24
-
25
- The manifests drive two flows:
26
-
27
- - **bootstrap — create mode** (empty folder): fork a base app, then *strip* every
28
- foundation/capability the user didn't pick, using each manifest's `files`/`deps`/
29
- `env` to know its exact footprint.
30
- - **bootstrap — existing project / add-capability**: match each manifest's `detect`
31
- against the project → the opt-in set, then *vendor* the listed files (copied from
32
- the base apps) + deps + env, wire `integratesWith` when both sides are opt-in, and
33
- pull required `capabilities`. A pattern not referenced is never pulled.
34
-
35
- ## Available patterns
36
-
37
- - **drizzle** — Drizzle ORM + drizzle-kit (Postgres). Client, schema barrel,
38
- cursor pagination, seed harness.
39
- - **better-auth** — better-auth v1 with the Drizzle adapter. Email+password,
40
- verification, optional Google OAuth, rate limiting, auth tables, client +
41
- session helpers, route guard. `dependsOn` drizzle; needs the mailer + email-kit
42
- capabilities.
43
- - **trpc** — tRPC v11 + TanStack React Query. Context, procedure tiers, error
44
- formatter, client + SSR caller, fetch handler. `dependsOn` drizzle,
45
- `integratesWith` better-auth.
46
- - **data-table** — headless tables with TanStack Table (table + skeleton
47
- primitives, DataTable, InfiniteDataTable, SortableHeader). `framework: agnostic`
48
- — works in both Next and TanStack Start.
49
-
50
- Next.js variants (App Router) of the framework-coupled patterns:
51
-
52
- - **better-auth-next** — better-auth with `next/headers` session, `toNextJsHandler`
53
- catch-all, server-component guards (`requireAuth`).
54
- - **trpc-next** — tRPC with the classic `api.x.useQuery` hooks (createTRPCReact) +
55
- RSC hydration. `integratesWith` better-auth-next.
56
-
57
- bootstrap picks the variant matching the project's framework: `trpc`/`better-auth`
58
- for TanStack Start, `trpc-next`/`better-auth-next` for Next.