@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
@@ -1,13 +1,12 @@
1
1
  /**
2
- * The swappable design tokens for emails. Override any subset via
3
- * {@link createEmailTheme} and pass the result to `<EmailLayout theme={...}>`,
4
- * or wrap a tree in `<EmailThemeProvider>`.
2
+ * Swappable email design tokens. Override a subset via {@link createEmailTheme}
3
+ * + `<EmailLayout theme={...}>`, or wrap a tree in `<EmailThemeProvider>`.
5
4
  */
6
5
  export interface EmailTheme {
7
6
  brand: {
8
- /** Shown in the header band and footer. */
7
+ /** Header band + footer. */
9
8
  name: string
10
- /** Footer line (the year is prefixed automatically). */
9
+ /** Footer line (year prefixed automatically). */
11
10
  footer: string
12
11
  }
13
12
  fontFamily: string
@@ -21,7 +20,7 @@ export interface EmailTheme {
21
20
  border: string
22
21
  borderSubtle: string
23
22
  destructive: string
24
- /** Text color on top of accent/destructive buttons. */
23
+ /** Text over accent/destructive buttons. */
25
24
  onAccent: string
26
25
  }
27
26
  }
@@ -52,7 +51,7 @@ export type EmailThemeOverride = {
52
51
  colors?: Partial<EmailTheme['colors']>
53
52
  }
54
53
 
55
- /** Deep-merge an override onto a base theme (defaults to {@link defaultTheme}). */
54
+ /** Deep-merge override onto base (defaults to {@link defaultTheme}). */
56
55
  export function createEmailTheme(
57
56
  override: EmailThemeOverride = {},
58
57
  base: EmailTheme = defaultTheme,
@@ -1,15 +1,11 @@
1
1
  import { createEnv } from '@t3-oss/env-core'
2
2
  import * as v from 'valibot'
3
3
 
4
- /** Makes a var required only in production (optional in dev/test). */
4
+ /** Required in production, optional in dev/test. */
5
5
  export const requiredInProduction = <T extends v.GenericSchema>(schema: T) =>
6
6
  process.env.NODE_ENV === 'production' ? schema : v.optional(schema)
7
7
 
8
- /**
9
- * Typed environment. Start minimal — patterns (drizzle, better-auth, …) and
10
- * capabilities (add-capability) extend the `server` block and `runtimeEnv` with
11
- * the keys they need.
12
- */
8
+ /** Typed env; foundations/capabilities extend `server` + `runtimeEnv` as needed. */
13
9
  export const env = createEnv({
14
10
  shared: {
15
11
  NODE_ENV: v.optional(v.picklist(['development', 'test', 'production']), 'development'),
@@ -1,4 +1,4 @@
1
1
  import { format } from 'date-fns'
2
2
 
3
- /** Format a Date as an ISO date string (yyyy-MM-dd). */
3
+ /** Format as ISO date (yyyy-MM-dd). */
4
4
  export const toISODate = (date: Date): string => format(date, 'yyyy-MM-dd')
@@ -8,7 +8,7 @@ import TanStackQueryDevtools from '~/trpc/devtools'
8
8
 
9
9
  import appCss from '../styles.css?url'
10
10
 
11
- // Runs before hydration to set the theme class and avoid a flash of wrong theme.
11
+ // Pre-hydration: set theme class to avoid flash of wrong theme.
12
12
  const themeScript = `(function(){try{var t=localStorage.getItem('theme')||'system';var m=window.matchMedia('(prefers-color-scheme:dark)').matches;document.documentElement.classList.toggle('dark',t==='dark'||(t==='system'&&m));}catch(e){}})();`
13
13
 
14
14
  interface RouterContext {
@@ -1,10 +1,7 @@
1
1
  import { createFileRoute, Outlet, redirect } from '@tanstack/react-router'
2
2
  import { getServerSession } from '~/server/better-auth/session'
3
3
 
4
- /**
5
- * Layout group guarding every route under it: redirects to sign-in when there's
6
- * no session, and exposes the session to child loaders via `beforeLoad`.
7
- */
4
+ /** Guards child routes: redirects to sign-in when no session; exposes session via `beforeLoad`. */
8
5
  export const Route = createFileRoute('/_authed')({
9
6
  beforeLoad: async () => {
10
7
  const session = await getServerSession()
@@ -1,4 +1,4 @@
1
- // Loads the TanStack Start augmentation that adds `server` to route options.
1
+ // Loads the Start augmentation adding `server` to route options.
2
2
 
3
3
  import { createFileRoute } from '@tanstack/react-router'
4
4
  import type {} from '@tanstack/react-start'
@@ -1,5 +1,4 @@
1
- // Loads the TanStack Start augmentation that adds `server` to route options
2
- // (so this server route typechecks even without other react-start imports).
1
+ // Loads TanStack Start augmentation adding `server` to route options (typechecks without other react-start imports).
3
2
 
4
3
  import { createFileRoute } from '@tanstack/react-router'
5
4
  import type {} from '@tanstack/react-start'
@@ -43,7 +43,7 @@ export const auth = betterAuth({
43
43
  },
44
44
  socialProviders,
45
45
  user: {
46
- // Extend the user with extra columns here (mirror them in auth.schema.ts):
46
+ // Add extra columns here (mirror in auth.schema.ts):
47
47
  // additionalFields: {
48
48
  // role: { type: 'string', defaultValue: 'user', input: false },
49
49
  // },
@@ -3,14 +3,13 @@ import { getRequest } from '@tanstack/react-start/server'
3
3
  import { auth } from '.'
4
4
 
5
5
  /**
6
- * Server-function: resolves the better-auth session from the current request
7
- * headers. Use it in route loaders / `beforeLoad`.
6
+ * Server-fn resolving the better-auth session from request headers; use in
7
+ * loaders / `beforeLoad`.
8
8
  *
9
- * The server-only `getRequest` call lives INSIDE the handler on purpose: the
10
- * TanStack Start plugin extracts the handler server-side and strips this import
11
- * from the client bundle. A standalone module-scope helper that calls
12
- * `getRequest` would leak `@tanstack/react-start/server` into client code (it's
13
- * imported transitively by routes) and trip the import-protection plugin.
9
+ * `getRequest` is called INSIDE the handler on purpose: the Start plugin
10
+ * extracts handlers server-side and strips the import from the client bundle.
11
+ * Calling it at module scope would leak `@tanstack/react-start/server` into
12
+ * client code (routes import this transitively) and trip import-protection.
14
13
  */
15
14
  export const getServerSession = createServerFn({ method: 'GET' }).handler(() =>
16
15
  auth.api.getSession({ headers: getRequest().headers }),
@@ -1,2 +1,2 @@
1
- // Barrel of every Drizzle schema. Add one `export *` line per `*.schema.ts`.
1
+ // Drizzle schema barrel; one `export *` per `*.schema.ts`.
2
2
  export * from './auth.schema'
@@ -3,11 +3,11 @@ import { config } from 'dotenv'
3
3
 
4
4
  config({ path: ['.env.local', '.env'] })
5
5
 
6
- // Deterministic data across runs.
6
+ // Deterministic across runs.
7
7
  faker.seed(42)
8
8
 
9
9
  async function main() {
10
- // Import the db client and seed your tables here (idempotent), using faker:
10
+ // Seed your tables here (idempotent) via faker:
11
11
  // const { db } = await import('~/server/db')
12
12
  // await db.insert(user).values({
13
13
  // id: faker.string.uuid(),
@@ -4,7 +4,7 @@ import { formatAddress } from '../../core/address'
4
4
  import { type MailerAdapter, MailerError, type RenderedMessage } from '../../core/port'
5
5
  import { ResendConfigSchema } from './config'
6
6
 
7
- /** Minimal structural view of the Resend client we depend on (eases testing). */
7
+ /** Minimal structural view of the Resend client (eases testing). */
8
8
  export interface ResendClient {
9
9
  emails: {
10
10
  send(payload: ResendSendPayload): Promise<{
@@ -30,7 +30,7 @@ interface ResendSendPayload {
30
30
 
31
31
  export interface ResendAdapterOptions {
32
32
  apiKey: string
33
- /** Inject a custom/mock client. Defaults to a real `Resend` instance. */
33
+ /** Custom/mock client; defaults to a real `Resend`. */
34
34
  client?: ResendClient
35
35
  }
36
36
 
@@ -39,7 +39,7 @@ function toAttachmentContent(content: Uint8Array | string): Buffer | string {
39
39
  }
40
40
 
41
41
  export function resendAdapter(options: ResendAdapterOptions): MailerAdapter {
42
- // Validate config early so a missing key fails at construction, not at send().
42
+ // Validate early so a missing key fails at construction, not send().
43
43
  const config = v.parse(ResendConfigSchema, { apiKey: options.apiKey })
44
44
  const client: ResendClient =
45
45
  options.client ?? (new Resend(config.apiKey) as unknown as ResendClient)
@@ -2,7 +2,7 @@ import type { MailAddress, MailRecipient } from './port'
2
2
 
3
3
  const ADDRESS_RE = /^\s*(.*?)\s*<([^>]+)>\s*$/
4
4
 
5
- /** Coerce a recipient into a structured {@link MailAddress}. */
5
+ /** Coerce a recipient to a structured {@link MailAddress}. */
6
6
  export function normalizeAddress(input: MailRecipient): MailAddress {
7
7
  if (typeof input !== 'string') return input
8
8
  const match = input.match(ADDRESS_RE)
@@ -10,12 +10,12 @@ export function normalizeAddress(input: MailRecipient): MailAddress {
10
10
  return { email: input.trim() }
11
11
  }
12
12
 
13
- /** Coerce a single recipient or list into a normalized array. */
13
+ /** Coerce a recipient or list to a normalized array. */
14
14
  export function normalizeRecipients(input: MailRecipient | MailRecipient[]): MailAddress[] {
15
15
  return (Array.isArray(input) ? input : [input]).map(normalizeAddress)
16
16
  }
17
17
 
18
- /** Render an address back to an RFC-style string (`Name <email>`). */
18
+ /** Render an address to RFC-style `Name <email>`. */
19
19
  export function formatAddress(address: MailAddress): string {
20
20
  return address.name ? `${address.name} <${address.email}>` : address.email
21
21
  }
@@ -1,41 +1,37 @@
1
1
  import type { ReactElement } from 'react'
2
2
 
3
- /** A structured email address. */
3
+ /** Structured email address. */
4
4
  export interface MailAddress {
5
5
  email: string
6
6
  name?: string
7
7
  }
8
8
 
9
- /**
10
- * A recipient, either as a plain string (`"hi@acme.com"` or
11
- * `"Acme <hi@acme.com>"`) or a structured {@link MailAddress}.
12
- */
9
+ /** Recipient: string (`"hi@acme.com"` / `"Acme <hi@acme.com>"`) or {@link MailAddress}. */
13
10
  export type MailRecipient = string | MailAddress
14
11
 
15
12
  export interface MailAttachment {
16
13
  filename: string
17
- /** Raw bytes or a base64 string. */
14
+ /** Raw bytes or base64 string. */
18
15
  content: Uint8Array | string
19
16
  contentType?: string
20
17
  }
21
18
 
22
19
  /**
23
- * A message as authored by application code. We never send raw HTML strings:
24
- * the body is always a React Email component, rendered to HTML + plain text by
25
- * the mailer before it reaches the adapter.
20
+ * Message as authored by app code. Never raw HTML: the body is always a React
21
+ * Email component, rendered to HTML + text by the mailer before the adapter.
26
22
  */
27
23
  export interface MailMessage {
28
24
  to: MailRecipient | MailRecipient[]
29
25
  subject: string
30
- /** A React Email component. Rendered to HTML and plain text automatically. */
26
+ /** React Email component; rendered to HTML + text automatically. */
31
27
  react: ReactElement
32
- /** Overrides the default `from` configured on the mailer. */
28
+ /** Overrides the mailer's default `from`. */
33
29
  from?: MailRecipient
34
30
  replyTo?: MailRecipient
35
31
  cc?: MailRecipient | MailRecipient[]
36
32
  bcc?: MailRecipient | MailRecipient[]
37
33
  headers?: Record<string, string>
38
- /** Provider-agnostic key/value tags for analytics & filtering. */
34
+ /** Provider-agnostic tags for analytics & filtering. */
39
35
  tags?: Record<string, string>
40
36
  attachments?: MailAttachment[]
41
37
  }
@@ -45,18 +41,12 @@ export interface SentMail {
45
41
  id: string
46
42
  }
47
43
 
48
- /**
49
- * The port the application depends on. Swapping providers means swapping the
50
- * adapter passed to {@link createMailer}; this interface never changes.
51
- */
44
+ /** The port the app depends on; swap providers via {@link createMailer}'s adapter — this never changes. */
52
45
  export interface Mailer {
53
46
  send(message: MailMessage): Promise<SentMail>
54
47
  }
55
48
 
56
- /**
57
- * The message shape an adapter receives: addresses are normalized and the body
58
- * is already rendered to `html` + `text`. Adapters do no rendering.
59
- */
49
+ /** Shape an adapter receives: normalized addresses, body pre-rendered to `html` + `text`. Adapters don't render. */
60
50
  export interface RenderedMessage {
61
51
  to: MailAddress[]
62
52
  from: MailAddress
@@ -71,13 +61,13 @@ export interface RenderedMessage {
71
61
  attachments?: MailAttachment[]
72
62
  }
73
63
 
74
- /** The contract each provider implements. Kept intentionally tiny. */
64
+ /** Contract each provider implements; intentionally tiny. */
75
65
  export interface MailerAdapter {
76
66
  readonly name: string
77
67
  send(message: RenderedMessage): Promise<SentMail>
78
68
  }
79
69
 
80
- /** Normalized error thrown by adapters so callers never catch provider types. */
70
+ /** Normalized adapter error so callers never catch provider types. */
81
71
  export class MailerError extends Error {
82
72
  readonly adapter: string
83
73
 
@@ -6,7 +6,7 @@ export interface RenderedBody {
6
6
  text: string
7
7
  }
8
8
 
9
- /** A function that turns a React Email component into HTML + plain text. */
9
+ /** Turns a React Email component into HTML + plain text. */
10
10
  export type EmailRenderer = (react: ReactElement) => Promise<RenderedBody>
11
11
 
12
12
  /** Default renderer backed by `@react-email/render`. */
@@ -3,21 +3,20 @@ import type { Mailer, MailerAdapter, MailRecipient, RenderedMessage } from './co
3
3
  import { type EmailRenderer, renderEmail } from './core/render'
4
4
 
5
5
  export interface CreateMailerOptions {
6
- /** The provider implementation (Resend, Brevo, …). */
6
+ /** Provider impl (Resend, Brevo, …). */
7
7
  adapter: MailerAdapter
8
- /** Default sender used when a message omits `from`. */
8
+ /** Default sender when a message omits `from`. */
9
9
  from: MailRecipient
10
- /** Override the React Email renderer (mostly for tests). */
10
+ /** Override the renderer (mostly for tests). */
11
11
  render?: EmailRenderer
12
12
  }
13
13
 
14
14
  /**
15
- * Build a {@link Mailer}. This is the composition root: pick the adapter here,
16
- * the rest of the app depends only on the `Mailer` port.
15
+ * Build a {@link Mailer}. Composition root: pick the adapter here; the rest of
16
+ * the app depends only on the `Mailer` port.
17
17
  *
18
- * The mailer renders the React body to HTML + plain text, normalizes every
19
- * address, applies the default sender, then hands a {@link RenderedMessage} to
20
- * the adapter.
18
+ * Renders the React body to HTML + text, normalizes addresses, applies the
19
+ * default sender, then hands a {@link RenderedMessage} to the adapter.
21
20
  */
22
21
  export function createMailer(options: CreateMailerOptions): Mailer {
23
22
  const defaultFrom = normalizeAddress(options.from)
@@ -11,7 +11,7 @@ function required(value: string | undefined, name: string): string {
11
11
  return value
12
12
  }
13
13
 
14
- // Lazy so the app boots without RESEND_API_KEY; the adapter is built on first send.
14
+ // Lazy so the app boots without RESEND_API_KEY; adapter built on first send.
15
15
  let mailer: Mailer | null = null
16
16
  function getMailer(): Mailer {
17
17
  if (!mailer) {
@@ -28,7 +28,7 @@ interface BrevoSendResponse {
28
28
  messageIds?: string[]
29
29
  }
30
30
 
31
- /** Minimal structural view of the Brevo client we depend on (eases testing). */
31
+ /** Structural view of the Brevo client (eases testing). */
32
32
  export interface BrevoClientLike {
33
33
  transactionalEmails: {
34
34
  sendTransacEmail(request: BrevoSendRequest): Promise<BrevoSendResponse>
@@ -37,7 +37,7 @@ export interface BrevoClientLike {
37
37
 
38
38
  export interface BrevoAdapterOptions {
39
39
  apiKey: string
40
- /** Inject a custom/mock client. Defaults to a real `BrevoClient`. */
40
+ /** Inject custom/mock client. Defaults to real `BrevoClient`. */
41
41
  client?: BrevoClientLike
42
42
  }
43
43
 
@@ -50,7 +50,7 @@ function toBase64(content: Uint8Array | string): string {
50
50
  }
51
51
 
52
52
  export function brevoAdapter(options: BrevoAdapterOptions): MailerAdapter {
53
- // Validate config early so a missing key fails at construction, not at send().
53
+ // Validate early: missing key fails at construction, not send().
54
54
  const config = v.parse(BrevoConfigSchema, { apiKey: options.apiKey })
55
55
  const client: BrevoClientLike =
56
56
  options.client ?? (new BrevoClient({ apiKey: config.apiKey }) as unknown as BrevoClientLike)
@@ -4,7 +4,7 @@ import { formatAddress } from '../../core/address.js'
4
4
  import { type MailerAdapter, MailerError, type RenderedMessage } from '../../core/port.js'
5
5
  import { ResendConfigSchema } from './config.js'
6
6
 
7
- /** Minimal structural view of the Resend client we depend on (eases testing). */
7
+ /** Structural view of the Resend client (eases testing). */
8
8
  export interface ResendClient {
9
9
  emails: {
10
10
  send(payload: ResendSendPayload): Promise<{
@@ -30,7 +30,7 @@ interface ResendSendPayload {
30
30
 
31
31
  export interface ResendAdapterOptions {
32
32
  apiKey: string
33
- /** Inject a custom/mock client. Defaults to a real `Resend` instance. */
33
+ /** Inject custom/mock client. Defaults to real `Resend`. */
34
34
  client?: ResendClient
35
35
  }
36
36
 
@@ -39,7 +39,7 @@ function toAttachmentContent(content: Uint8Array | string): Buffer | string {
39
39
  }
40
40
 
41
41
  export function resendAdapter(options: ResendAdapterOptions): MailerAdapter {
42
- // Validate config early so a missing key fails at construction, not at send().
42
+ // Validate early: missing key fails at construction, not send().
43
43
  const config = v.parse(ResendConfigSchema, { apiKey: options.apiKey })
44
44
  const client: ResendClient =
45
45
  options.client ?? (new Resend(config.apiKey) as unknown as ResendClient)
@@ -1,12 +1,12 @@
1
1
  import * as v from 'valibot'
2
2
 
3
3
  export const SesConfigSchema = v.object({
4
- /** AWS region. Falls back to the SDK's resolution (e.g. `AWS_REGION`). */
4
+ /** AWS region. Falls back to SDK resolution (e.g. `AWS_REGION`). */
5
5
  region: v.optional(v.string()),
6
- /** Static credentials. Omit to let the SDK resolve them (env, profile, IAM). */
6
+ /** Static credentials. Omit to let the SDK resolve (env, profile, IAM). */
7
7
  accessKeyId: v.optional(v.string()),
8
8
  secretAccessKey: v.optional(v.string()),
9
- /** SES configuration set to attach (event publishing, dedicated IPs, …). */
9
+ /** SES configuration set (event publishing, dedicated IPs, …). */
10
10
  configurationSetName: v.optional(v.string()),
11
11
  })
12
12
 
@@ -5,7 +5,7 @@ import { formatAddress } from '../../core/address.js'
5
5
  import { type MailerAdapter, MailerError, type RenderedMessage } from '../../core/port.js'
6
6
  import { SesConfigSchema } from './config.js'
7
7
 
8
- /** Minimal structural view of the SESv2 client we depend on (eases testing). */
8
+ /** Structural view of the SESv2 client (eases testing). */
9
9
  export interface SesClientLike {
10
10
  send(command: { input: SesSendEmailInput }): Promise<{ MessageId?: string }>
11
11
  }
@@ -26,12 +26,12 @@ interface SesSendEmailInput {
26
26
  }
27
27
 
28
28
  export interface SesAdapterOptions {
29
- /** AWS region. Falls back to the SDK's resolution (e.g. `AWS_REGION`). */
29
+ /** AWS region. Falls back to SDK resolution (e.g. `AWS_REGION`). */
30
30
  region?: string
31
31
  accessKeyId?: string
32
32
  secretAccessKey?: string
33
33
  configurationSetName?: string
34
- /** Inject a custom/mock client. Defaults to a real `SESv2Client`. */
34
+ /** Inject custom/mock client. Defaults to real `SESv2Client`. */
35
35
  client?: SesClientLike
36
36
  }
37
37
 
@@ -56,8 +56,7 @@ export function sesAdapter(options: SesAdapterOptions = {}): MailerAdapter {
56
56
  return {
57
57
  name: 'ses',
58
58
  async send(message: RenderedMessage) {
59
- // SES's Simple content can't carry attachments that needs a raw MIME
60
- // message, which this adapter intentionally doesn't build. Fail loudly.
59
+ // SES Simple content can't carry attachments (needs raw MIME, not built here). Fail loudly.
61
60
  if (message.attachments?.length) {
62
61
  throw new MailerError('SES adapter does not support attachments (requires raw MIME)', {
63
62
  adapter: 'ses',
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,10 +153,20 @@ function execute(a, patterns) {
173
153
  )
174
154
  }
175
155
 
176
- // Initialize a fresh repo (also satisfies Biome's vcs.useIgnoreFile).
177
- if (run('git', ['init', '-q'], { cwd: projectDir })) {
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'])) {
178
159
  run('git', ['-C', projectDir, 'add', '-A'])
179
- p.log.step('git initialized')
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
+ )
180
170
  }
181
171
 
182
172
  const keptMailer = a.mailerProvider !== 'none'
@@ -184,15 +174,12 @@ function execute(a, patterns) {
184
174
  `Framework: ${a.framework === 'next' ? 'Next.js' : 'TanStack Start'}`,
185
175
  `Foundations: ${[...a.kept].sort().join(', ') || '(none)'}`,
186
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}`,
187
182
  ]
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
183
  if (!a.doInstall) lines.push(' pnpm install')
197
184
  lines.push(' cp .env.example .env # fill in the values', ' pnpm dev')
198
185
  p.note(lines.join('\n'), 'Done')
@@ -200,20 +187,16 @@ function execute(a, patterns) {
200
187
  }
201
188
 
202
189
  async function main() {
203
- const patterns = loadPatterns()
204
- const capabilities = loadCapabilities()
205
190
  const args = parseArgs(process.argv.slice(2))
206
191
 
207
192
  const nonInteractive =
208
193
  args.flags.yes ||
209
194
  args.flags.y ||
210
- ['framework', 'foundations', 'mailer', 'caps', 'no-install'].some((k) => k in args.flags)
195
+ ['framework', 'foundations', 'mailer', 'no-install'].some((k) => k in args.flags)
211
196
 
212
- const answers = nonInteractive
213
- ? collectFromFlags(args, capabilities)
214
- : await collectFromPrompts(args._[0], capabilities)
197
+ const answers = nonInteractive ? collectFromFlags(args) : await collectFromPrompts(args._[0])
215
198
 
216
- execute(answers, patterns)
199
+ execute(answers)
217
200
  }
218
201
 
219
202
  main().catch((err) => {