@forgeailab/create-spark 0.1.3 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgeailab/create-spark",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Interactive scaffolder for spark projects with guided pack picker.",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -20,8 +20,8 @@
20
20
  "create-spark": "./src/cli.ts"
21
21
  },
22
22
  "dependencies": {
23
- "@forgeailab/spark": "^0.1.3",
24
- "@forgeailab/spark-schema": "^0.1.3",
23
+ "@forgeailab/spark": "^0.2.0",
24
+ "@forgeailab/spark-schema": "^0.2.0",
25
25
  "@clack/prompts": "latest",
26
26
  "citty": "latest",
27
27
  "picocolors": "latest"
package/packs/README.md CHANGED
@@ -91,32 +91,30 @@ See `packs/example/pack.toml` for a manifest that exercises every field.
91
91
 
92
92
  ## v1 catalog
93
93
 
94
- Each pack is either **copy** (ships file trees the user owns) or **hybrid** (ships thin wiring + imports runtime logic from a versioned `@forgeailab/spark-*` helper under `libs/`). The mode is inferred from the presence of a `[runtime_package]` block in the manifest.
95
-
96
- | Pack | Category | Mode | Runtime helper |
97
- |---|---|---|---|
98
- | `auth-better-auth` | auth | **hybrid** | `@forgeailab/spark-auth-better-auth` (SQLite) |
99
- | `auth-better-auth-pg` | auth | **hybrid** | `@forgeailab/spark-auth-better-auth` (Postgres) |
100
- | `auth-supabase` | auth | copy | — |
101
- | `db-sqlite` | db | copy | — |
102
- | `db-postgres` | db | copy | — |
103
- | `db-supabase` | db | copy | — |
104
- | `sync-zero` | infra | **hybrid** | `@forgeailab/spark-sync-zero` |
105
- | `payments-stripe` | payments | **hybrid** | `@forgeailab/spark-stripe-helpers` |
106
- | `ai-anthropic` | ai | **hybrid** | `@forgeailab/spark-anthropic` |
107
- | `ai-openai` | ai | copy | — |
108
- | `ui-shadcn` | ui | copy | — |
109
- | `email-resend` | email | copy | — |
110
- | `analytics-posthog` | analytics | copy | — |
111
- | `docker-compose-dev` | infra | copy | — |
112
- | `testing-playwright` | testing | copy | — |
113
- | `deploy-vercel` | deploy | copy | — |
94
+ | Pack | Category |
95
+ |---|---|
96
+ | `auth-better-auth` | auth |
97
+ | `auth-better-auth-pg` | auth |
98
+ | `auth-supabase` | auth |
99
+ | `db-sqlite` | db |
100
+ | `db-postgres` | db |
101
+ | `db-supabase` | db |
102
+ | `sync-zero` | infra |
103
+ | `payments-stripe` | payments |
104
+ | `ai-anthropic` | ai |
105
+ | `ai-openai` | ai |
106
+ | `ui-shadcn` | ui |
107
+ | `email-resend` | email |
108
+ | `analytics-posthog` | analytics |
109
+ | `docker-compose-dev` | infra |
110
+ | `testing-playwright` | testing |
111
+ | `deploy-vercel` | deploy |
114
112
 
115
113
  ### Picking a db + auth pair
116
114
 
117
- `auth-better-auth` and `auth-better-auth-pg` share the same runtime helper
118
- (`@forgeailab/spark-auth-better-auth`) — they differ only in the `provider:`
119
- their generated `lib/auth.ts` template hands to `drizzleAdapter`. Pair them:
115
+ `auth-better-auth` and `auth-better-auth-pg` share the same Better Auth factory
116
+ code in their generated `lib/auth.ts` templates — they differ only in the
117
+ `provider:` handed to `drizzleAdapter`. Pair them:
120
118
 
121
119
  - `db-sqlite` + `auth-better-auth` — fastest path, single file db, no infra.
122
120
  - `db-postgres` + `auth-better-auth-pg` — production-shaped, **required for `sync-zero`** (Zero needs Postgres logical replication).
@@ -127,6 +125,6 @@ installing both. Mixing wrong pairs (e.g. `db-sqlite` + `auth-better-auth-pg`)
127
125
  typechecks but fails at runtime — the drizzle adapter will reject sqlite tables
128
126
  with `provider: 'pg'`.
129
127
 
130
- The hybrid packs were authored against [`reference/full-stack-saas/`](../reference/full-stack-saas/) — the canonical integration showing all four helpers working together. When debugging a hybrid pack, start there.
128
+ The copy-mode packs were authored against [`reference/full-stack-saas/`](../reference/full-stack-saas/) — the canonical integration showing the copied templates working together. When debugging one of these packs, start there.
131
129
 
132
130
  See the root `README.md` for the catalog summary.
@@ -1,5 +1,49 @@
1
- import type Anthropic from '@anthropic-ai/sdk';
2
- import { createAnthropicClient, streamResponse } from '@forgeailab/spark-anthropic';
1
+ import Anthropic from '@anthropic-ai/sdk';
2
+
3
+ export function createAnthropicClient(
4
+ apiKey: string,
5
+ options?: ConstructorParameters<typeof Anthropic>[0],
6
+ ): Anthropic {
7
+ return new Anthropic({ apiKey, ...options });
8
+ }
9
+
10
+ const encoder = new TextEncoder();
11
+
12
+ function isContentBlockDeltaEvent(event: unknown): event is { type: 'content_block_delta' } {
13
+ return (
14
+ typeof event === 'object' &&
15
+ event !== null &&
16
+ (event as { type?: unknown }).type === 'content_block_delta'
17
+ );
18
+ }
19
+
20
+ function encodeSse(payload: unknown) {
21
+ return encoder.encode(`data: ${JSON.stringify(payload)}\n\n`);
22
+ }
23
+
24
+ export function streamResponse(
25
+ client: Anthropic,
26
+ params: Parameters<Anthropic['messages']['stream']>[0],
27
+ ): ReadableStream<Uint8Array> {
28
+ return new ReadableStream<Uint8Array>({
29
+ async start(controller) {
30
+ try {
31
+ const stream = client.messages.stream(params);
32
+
33
+ for await (const event of stream) {
34
+ if (isContentBlockDeltaEvent(event)) {
35
+ controller.enqueue(encodeSse(event));
36
+ }
37
+ }
38
+
39
+ controller.enqueue(encoder.encode('data: [DONE]\n\n'));
40
+ controller.close();
41
+ } catch (error) {
42
+ controller.error(error);
43
+ }
44
+ },
45
+ });
46
+ }
3
47
 
4
48
  function requireEnv(name: string): string {
5
49
  const value = process.env[name];
@@ -11,5 +55,4 @@ function requireEnv(name: string): string {
11
55
 
12
56
  export const anthropic = createAnthropicClient(requireEnv('ANTHROPIC_API_KEY'));
13
57
 
14
- export { streamResponse };
15
58
  export type AnthropicChatMessage = Anthropic.Messages.MessageParam;
@@ -8,9 +8,8 @@ conflicts = []
8
8
  requires_runtime = ["server"]
9
9
  compatible_scaffolds = []
10
10
 
11
- [runtime_package]
12
- package = "@forgeailab/spark-anthropic"
13
- version = "^0.1"
11
+ [dependencies]
12
+ runtime = ["@anthropic-ai/sdk"]
14
13
 
15
14
  [env]
16
15
  required = ["ANTHROPIC_API_KEY"]
@@ -1,4 +1,4 @@
1
- import { createAuthHandler } from '@forgeailab/spark-auth-better-auth';
1
+ import { toNextJsHandler } from 'better-auth/next-js';
2
2
  import { auth } from '@/lib/auth';
3
3
 
4
- export const { GET, POST } = createAuthHandler(auth);
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -1,4 +1,43 @@
1
- import { createAuth as createBetterAuth } from '@forgeailab/spark-auth-better-auth';
1
+ import { betterAuth } from 'better-auth';
2
+
3
+ type BetterAuthOptions = Parameters<typeof betterAuth>[0];
4
+
5
+ export type AuthInstance = ReturnType<typeof betterAuth>;
6
+
7
+ export type CreateBetterAuthOptions = {
8
+ adapter: BetterAuthOptions['database'];
9
+ basePath?: BetterAuthOptions['basePath'];
10
+ plugins?: BetterAuthOptions['plugins'];
11
+ emailAndPassword?: BetterAuthOptions['emailAndPassword'];
12
+ socialProviders?: BetterAuthOptions['socialProviders'];
13
+ secret?: BetterAuthOptions['secret'];
14
+ baseURL?: BetterAuthOptions['baseURL'];
15
+ trustedOrigins?: string[];
16
+ };
17
+
18
+ export function createBetterAuth({
19
+ adapter,
20
+ basePath,
21
+ plugins,
22
+ emailAndPassword,
23
+ socialProviders,
24
+ secret,
25
+ baseURL,
26
+ trustedOrigins,
27
+ }: CreateBetterAuthOptions) {
28
+ const authOptions: BetterAuthOptions = {
29
+ database: adapter,
30
+ basePath,
31
+ plugins,
32
+ emailAndPassword,
33
+ socialProviders,
34
+ secret,
35
+ baseURL,
36
+ trustedOrigins: trustedOrigins ?? (typeof baseURL === 'string' ? [baseURL] : undefined),
37
+ };
38
+
39
+ return betterAuth(authOptions);
40
+ }
2
41
 
3
42
  // Wire your database adapter here. Example (drizzle + sqlite):
4
43
  //
@@ -8,12 +8,8 @@ conflicts = ["auth"]
8
8
  requires_runtime = ["server"]
9
9
  compatible_scaffolds = ["nextjs"]
10
10
 
11
- [runtime_package]
12
- package = "@forgeailab/spark-auth-better-auth"
13
- version = "^0.1"
14
-
15
11
  [dependencies]
16
- runtime = ["@better-auth/drizzle-adapter"]
12
+ runtime = ["@better-auth/drizzle-adapter", "better-auth"]
17
13
 
18
14
  [env]
19
15
  required = ["BETTER_AUTH_SECRET", "BETTER_AUTH_URL"]
@@ -1,4 +1,4 @@
1
- import { createAuthHandler } from '@forgeailab/spark-auth-better-auth';
1
+ import { toNextJsHandler } from 'better-auth/next-js';
2
2
  import { auth } from '@/lib/auth';
3
3
 
4
- export const { GET, POST } = createAuthHandler(auth);
4
+ export const { GET, POST } = toNextJsHandler(auth);
@@ -1,8 +1,47 @@
1
1
  import { drizzleAdapter } from '@better-auth/drizzle-adapter';
2
- import { createAuth as createBetterAuth } from '@forgeailab/spark-auth-better-auth';
2
+ import { betterAuth } from 'better-auth';
3
3
  import { db } from '@/lib/db';
4
4
  import * as schema from '@/lib/db/schema';
5
5
 
6
+ type BetterAuthOptions = Parameters<typeof betterAuth>[0];
7
+
8
+ export type AuthInstance = ReturnType<typeof betterAuth>;
9
+
10
+ export type CreateBetterAuthOptions = {
11
+ adapter: BetterAuthOptions['database'];
12
+ basePath?: BetterAuthOptions['basePath'];
13
+ plugins?: BetterAuthOptions['plugins'];
14
+ emailAndPassword?: BetterAuthOptions['emailAndPassword'];
15
+ socialProviders?: BetterAuthOptions['socialProviders'];
16
+ secret?: BetterAuthOptions['secret'];
17
+ baseURL?: BetterAuthOptions['baseURL'];
18
+ trustedOrigins?: string[];
19
+ };
20
+
21
+ export function createBetterAuth({
22
+ adapter,
23
+ basePath,
24
+ plugins,
25
+ emailAndPassword,
26
+ socialProviders,
27
+ secret,
28
+ baseURL,
29
+ trustedOrigins,
30
+ }: CreateBetterAuthOptions) {
31
+ const authOptions: BetterAuthOptions = {
32
+ database: adapter,
33
+ basePath,
34
+ plugins,
35
+ emailAndPassword,
36
+ socialProviders,
37
+ secret,
38
+ baseURL,
39
+ trustedOrigins: trustedOrigins ?? (typeof baseURL === 'string' ? [baseURL] : undefined),
40
+ };
41
+
42
+ return betterAuth(authOptions);
43
+ }
44
+
6
45
  // Postgres-flavored Better Auth wiring. Pair this pack with `db-postgres`
7
46
  // (or `db-supabase`), and add the four Better Auth tables to your
8
47
  // `lib/db/schema.ts`: `user`, `session`, `account`, `verification`. Snake-case
@@ -8,12 +8,8 @@ conflicts = ["auth"]
8
8
  requires_runtime = ["server"]
9
9
  compatible_scaffolds = ["nextjs"]
10
10
 
11
- [runtime_package]
12
- package = "@forgeailab/spark-auth-better-auth"
13
- version = "^0.1"
14
-
15
11
  [dependencies]
16
- runtime = ["@better-auth/drizzle-adapter"]
12
+ runtime = ["@better-auth/drizzle-adapter", "better-auth"]
17
13
 
18
14
  [env]
19
15
  required = ["BETTER_AUTH_SECRET", "BETTER_AUTH_URL"]
@@ -1,9 +1,107 @@
1
- import {
2
- createStripeClient,
3
- createCheckoutSession as createStripeCheckoutSession,
4
- createBillingPortalSession as createStripeBillingPortalSession,
5
- verifyWebhookSignature as verifyStripeWebhookSignature,
6
- } from '@forgeailab/spark-stripe-helpers';
1
+ import Stripe from 'stripe';
2
+
3
+ type StripeClientOptions = ConstructorParameters<typeof Stripe>[1];
4
+
5
+ declare module 'stripe' {
6
+ namespace Stripe {
7
+ export type StripeConstructorOptions = StripeClientOptions;
8
+ }
9
+ }
10
+
11
+ export function createStripeClient(
12
+ secretKey: string,
13
+ options?: Stripe.StripeConstructorOptions,
14
+ ): Stripe {
15
+ return new Stripe(secretKey, options);
16
+ }
17
+
18
+ type CheckoutSessionCreateParams = NonNullable<
19
+ Parameters<Stripe['checkout']['sessions']['create']>[0]
20
+ >;
21
+
22
+ export type CreateCheckoutSessionInput = {
23
+ priceId: string;
24
+ customerId?: string;
25
+ customerEmail?: string;
26
+ successUrl: string;
27
+ cancelUrl: string;
28
+ mode?: CheckoutSessionCreateParams['mode'];
29
+ metadata?: Record<string, string>;
30
+ };
31
+
32
+ async function createStripeCheckoutSession(
33
+ stripeClient: Stripe,
34
+ {
35
+ priceId,
36
+ customerId,
37
+ customerEmail,
38
+ successUrl,
39
+ cancelUrl,
40
+ mode = 'subscription',
41
+ metadata,
42
+ }: CreateCheckoutSessionInput,
43
+ ): Promise<{ url: string; sessionId: string }> {
44
+ if (!customerId && !customerEmail) {
45
+ throw new Error('customerId or customerEmail is required');
46
+ }
47
+
48
+ const session = await stripeClient.checkout.sessions.create({
49
+ mode,
50
+ customer: customerId,
51
+ customer_email: customerId ? undefined : customerEmail,
52
+ line_items: [
53
+ {
54
+ price: priceId,
55
+ quantity: 1,
56
+ },
57
+ ],
58
+ allow_promotion_codes: true,
59
+ success_url: successUrl,
60
+ cancel_url: cancelUrl,
61
+ metadata,
62
+ });
63
+
64
+ if (!session.url) {
65
+ throw new Error('Stripe did not return a checkout URL');
66
+ }
67
+
68
+ return {
69
+ url: session.url,
70
+ sessionId: session.id,
71
+ };
72
+ }
73
+
74
+ export type VerifyWebhookSignatureInput = {
75
+ payload: string | Buffer;
76
+ signatureHeader: string;
77
+ secret: string;
78
+ };
79
+
80
+ function verifyStripeWebhookSignature(
81
+ stripeClient: Stripe,
82
+ { payload, signatureHeader, secret }: VerifyWebhookSignatureInput,
83
+ ): Promise<Stripe.Event> {
84
+ return stripeClient.webhooks.constructEventAsync(payload, signatureHeader, secret);
85
+ }
86
+
87
+ export type CreateBillingPortalSessionInput = {
88
+ customerId: string;
89
+ returnUrl: string;
90
+ };
91
+
92
+ async function createStripeBillingPortalSession(
93
+ stripeClient: Stripe,
94
+ { customerId, returnUrl }: CreateBillingPortalSessionInput,
95
+ ): Promise<{ url: string }> {
96
+ const session = await stripeClient.billingPortal.sessions.create({
97
+ customer: customerId,
98
+ return_url: returnUrl,
99
+ });
100
+
101
+ return {
102
+ url: session.url,
103
+ };
104
+ }
7
105
 
8
106
  function requireEnv(name: string): string {
9
107
  const value = process.env[name];
@@ -8,12 +8,8 @@ conflicts = ["payments"]
8
8
  requires_runtime = ["server"]
9
9
  compatible_scaffolds = ["nextjs"]
10
10
 
11
- [runtime_package]
12
- package = "@forgeailab/spark-stripe-helpers"
13
- version = "^0.1"
14
-
15
11
  [dependencies]
16
- runtime = ["@stripe/stripe-js"]
12
+ runtime = ["@stripe/stripe-js", "stripe"]
17
13
 
18
14
  [env]
19
15
  required = [
@@ -1,3 +1,13 @@
1
1
  'use client';
2
2
 
3
- export { ZeroProvider } from '@forgeailab/spark-sync-zero';
3
+ import type { ReactNode } from 'react';
4
+ import type { ZeroOptions } from '@rocicorp/zero';
5
+ import { ZeroProvider as RocicorpZeroProvider } from '@rocicorp/zero/react';
6
+
7
+ type ZeroProviderProps = ZeroOptions & {
8
+ children: ReactNode;
9
+ };
10
+
11
+ export function ZeroProvider({ children, ...options }: ZeroProviderProps) {
12
+ return <RocicorpZeroProvider {...options}>{children}</RocicorpZeroProvider>;
13
+ }
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
- import { createZeroClient as createSparkZeroClient } from '@forgeailab/spark-sync-zero';
4
- import type { ZeroOptions } from '@forgeailab/spark-sync-zero';
3
+ import { Zero } from '@rocicorp/zero';
4
+ import type { ZeroOptions } from '@rocicorp/zero';
5
5
  import { schema } from './schema';
6
6
 
7
7
  const DEFAULT_ZERO_URL = 'http://localhost:4848';
@@ -14,5 +14,5 @@ export function createZeroOptions(): ZeroOptions {
14
14
  }
15
15
 
16
16
  export function createZeroClient(options: ZeroOptions = createZeroOptions()) {
17
- return createSparkZeroClient(options);
17
+ return new Zero(options);
18
18
  }
@@ -1,4 +1,12 @@
1
- import { defineZeroSchema, number, string, table } from '@forgeailab/spark-sync-zero';
1
+ import {
2
+ ANYONE_CAN,
3
+ createBuilder,
4
+ createSchema,
5
+ definePermissions,
6
+ number,
7
+ string,
8
+ table,
9
+ } from '@rocicorp/zero';
2
10
 
3
11
  // Example schema. Replace with your app's tables.
4
12
  const users = table('user')
@@ -10,8 +18,13 @@ const users = table('user')
10
18
  })
11
19
  .primaryKey('id');
12
20
 
13
- export const { schema, zql } = defineZeroSchema({
21
+ export const schema = createSchema({
14
22
  tables: [users],
15
23
  });
24
+ export const zql = createBuilder(schema);
16
25
 
17
26
  export type Schema = typeof schema;
27
+
28
+ export const permissions = definePermissions<unknown, Schema>(schema, () => ({
29
+ user: { row: { select: ANYONE_CAN } },
30
+ }));
@@ -8,10 +8,6 @@ conflicts = ["sync"]
8
8
  requires_runtime = ["server"]
9
9
  compatible_scaffolds = ["nextjs"]
10
10
 
11
- [runtime_package]
12
- package = "@forgeailab/spark-sync-zero"
13
- version = "^0.1"
14
-
15
11
  [dependencies]
16
12
  runtime = ["@rocicorp/zero"]
17
13
 
@@ -6,7 +6,7 @@ import {
6
6
  parseSkillFrontmatter,
7
7
  serializeSkillFrontmatter,
8
8
  toCodexFrontmatter,
9
- } from "@forgeailab/spark-skill-utils";
9
+ } from "../packages/spark/src/internal/skill-utils.ts";
10
10
 
11
11
  type SkillOutput = {
12
12
  name: string;