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