alchemy 0.2.5 → 0.3.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.
Files changed (151) hide show
  1. package/README.md +3 -3
  2. package/lib/ai/astro-file.d.ts +137 -0
  3. package/lib/ai/astro-file.js +173 -0
  4. package/lib/ai/client.d.ts +2 -10
  5. package/lib/ai/client.js +8 -24
  6. package/lib/ai/css-file.d.ts +131 -0
  7. package/lib/ai/css-file.js +158 -0
  8. package/lib/ai/data.d.ts +45 -5
  9. package/lib/ai/data.js +45 -10
  10. package/lib/ai/document.d.ts +44 -20
  11. package/lib/ai/document.js +95 -28
  12. package/lib/ai/html-file.d.ts +128 -0
  13. package/lib/ai/html-file.js +155 -0
  14. package/lib/ai/index.d.ts +7 -0
  15. package/lib/ai/index.js +7 -0
  16. package/lib/ai/json-file.d.ts +160 -0
  17. package/lib/ai/json-file.js +208 -0
  18. package/lib/ai/typescript-file.d.ts +140 -0
  19. package/lib/ai/typescript-file.js +176 -0
  20. package/lib/ai/vue-file.d.ts +126 -0
  21. package/lib/ai/vue-file.js +153 -0
  22. package/lib/ai/yaml-file.d.ts +169 -0
  23. package/lib/ai/yaml-file.js +236 -0
  24. package/lib/cloudflare/dns.d.ts +88 -0
  25. package/lib/cloudflare/dns.js +192 -0
  26. package/lib/cloudflare/index.d.ts +2 -1
  27. package/lib/cloudflare/index.js +2 -1
  28. package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
  29. package/lib/cloudflare/r2-rest-state-store.js +254 -0
  30. package/lib/cloudflare/response.d.ts +12 -0
  31. package/lib/cloudflare/response.js +0 -0
  32. package/lib/cloudflare/zone.d.ts +18 -0
  33. package/lib/cloudflare/zone.js +17 -4
  34. package/lib/dns/godaddy.d.ts +7 -0
  35. package/lib/dns/godaddy.js +21 -0
  36. package/lib/dns/import-dns.d.ts +82 -0
  37. package/lib/dns/import-dns.js +114 -0
  38. package/lib/dns/index.d.ts +2 -0
  39. package/lib/dns/index.js +2 -0
  40. package/lib/dns/record.d.ts +88 -0
  41. package/lib/dns/record.js +13 -0
  42. package/lib/fs/file-system-state-store.d.ts +17 -0
  43. package/lib/fs/file-system-state-store.js +87 -0
  44. package/lib/fs/folder.d.ts +10 -0
  45. package/lib/fs/folder.js +2 -2
  46. package/lib/fs/index.d.ts +4 -4
  47. package/lib/fs/index.js +4 -4
  48. package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +2 -2
  49. package/lib/fs/static-json-file.js +14 -0
  50. package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
  51. package/lib/fs/{text-file.js → static-text-file.js} +1 -1
  52. package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +2 -2
  53. package/lib/fs/{typescript-file.js → static-typescript-file.js} +1 -1
  54. package/lib/fs/{yaml-file.d.ts → static-yaml-file.d.ts} +2 -2
  55. package/lib/fs/{yaml-file.js → static-yaml-file.js} +1 -1
  56. package/lib/internal/getting-started.d.ts +10 -0
  57. package/lib/internal/getting-started.js +94 -0
  58. package/lib/internal/project.d.ts +13 -0
  59. package/lib/internal/project.js +101 -0
  60. package/lib/internal/providers.d.ts +23 -0
  61. package/lib/internal/providers.js +148 -0
  62. package/lib/resource.js +8 -1
  63. package/lib/scope.d.ts +1 -1
  64. package/lib/scope.js +4 -2
  65. package/lib/state.d.ts +1 -16
  66. package/lib/state.js +0 -87
  67. package/lib/test/bun.d.ts +11 -0
  68. package/lib/test/bun.js +36 -8
  69. package/lib/web/astro.d.ts +147 -0
  70. package/lib/web/astro.js +414 -0
  71. package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
  72. package/lib/web/shadcn.d.ts +97 -0
  73. package/lib/web/shadcn.js +99 -0
  74. package/lib/web/tailwind.d.ts +64 -0
  75. package/lib/web/tailwind.js +97 -0
  76. package/lib/{vite → web}/vite.d.ts +5 -0
  77. package/lib/{vite → web}/vite.js +44 -44
  78. package/lib/web/vitepress/custom-theme.d.ts +173 -0
  79. package/lib/web/vitepress/custom-theme.js +336 -0
  80. package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
  81. package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
  82. package/lib/web/vitepress/home-page.d.ts +244 -0
  83. package/lib/web/vitepress/home-page.js +134 -0
  84. package/lib/{vitepress → web/vitepress}/index.d.ts +1 -0
  85. package/lib/{vitepress → web/vitepress}/index.js +1 -0
  86. package/lib/{vitepress → web/vitepress}/vitepress.d.ts +10 -8
  87. package/lib/{vitepress → web/vitepress}/vitepress.js +11 -12
  88. package/package.json +6 -2
  89. package/src/ai/astro-file.ts +288 -0
  90. package/src/ai/client.ts +8 -28
  91. package/src/ai/css-file.ts +266 -0
  92. package/src/ai/data.ts +53 -16
  93. package/src/ai/document.ts +130 -33
  94. package/src/ai/html-file.ts +263 -0
  95. package/src/ai/index.ts +7 -0
  96. package/src/ai/json-file.ts +349 -0
  97. package/src/ai/typescript-file.ts +293 -0
  98. package/src/ai/vue-file.ts +261 -0
  99. package/src/ai/yaml-file.ts +368 -0
  100. package/src/apply.ts +0 -1
  101. package/src/cloudflare/dns.ts +344 -0
  102. package/src/cloudflare/index.ts +2 -1
  103. package/src/cloudflare/r2-rest-state-store.ts +353 -0
  104. package/src/cloudflare/response.ts +9 -0
  105. package/src/cloudflare/zone.ts +39 -23
  106. package/src/dns/godaddy.ts +29 -0
  107. package/src/dns/import-dns.ts +213 -0
  108. package/src/dns/index.ts +2 -0
  109. package/src/dns/record.ts +121 -0
  110. package/src/fs/file-system-state-store.ts +109 -0
  111. package/src/fs/folder.ts +14 -2
  112. package/src/fs/index.ts +4 -4
  113. package/src/fs/{json-file.ts → static-json-file.ts} +13 -3
  114. package/src/fs/{text-file.ts → static-text-file.ts} +5 -2
  115. package/src/fs/{typescript-file.ts → static-typescript-file.ts} +3 -3
  116. package/src/fs/{yaml-file.ts → static-yaml-file.ts} +5 -2
  117. package/src/internal/getting-started.ts +107 -0
  118. package/src/internal/project.ts +119 -0
  119. package/src/internal/providers.ts +209 -0
  120. package/src/resource.ts +10 -1
  121. package/src/scope.ts +5 -6
  122. package/src/state.ts +1 -111
  123. package/src/test/bun.ts +60 -10
  124. package/src/web/astro.ts +644 -0
  125. package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
  126. package/src/web/shadcn.ts +219 -0
  127. package/src/web/tailwind.ts +167 -0
  128. package/src/{vite → web}/vite.ts +54 -56
  129. package/src/web/vitepress/custom-theme.ts +514 -0
  130. package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
  131. package/src/web/vitepress/home-page.ts +363 -0
  132. package/src/{vitepress → web/vitepress}/index.ts +1 -0
  133. package/src/{vitepress → web/vitepress}/vitepress.ts +34 -27
  134. package/lib/cloudflare/state.d.ts +0 -82
  135. package/lib/cloudflare/state.js +0 -108
  136. package/lib/fs/json-file.js +0 -7
  137. package/lib/internal/docs.d.ts +0 -5
  138. package/lib/internal/docs.js +0 -287
  139. package/lib/shadcn/index.d.ts +0 -1
  140. package/lib/shadcn/index.js +0 -1
  141. package/lib/vite/index.d.ts +0 -1
  142. package/lib/vite/index.js +0 -1
  143. package/lib/vitepress/home-page.d.ts +0 -133
  144. package/lib/vitepress/home-page.js +0 -11
  145. package/src/cloudflare/state.ts +0 -138
  146. package/src/internal/docs.ts +0 -338
  147. package/src/shadcn/index.ts +0 -1
  148. package/src/vite/index.ts +0 -1
  149. package/src/vitepress/home-page.ts +0 -166
  150. /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
  151. /package/src/{vitepress → web/vitepress}/index.md +0 -0
package/README.md CHANGED
@@ -9,7 +9,7 @@ Resources are simple memoized async functions that can run in any JavaScript run
9
9
  ```ts
10
10
  import alchemy from "alchemy";
11
11
 
12
- await using _ = alchemy("cloudflare-worker");
12
+ await using app = alchemy("cloudflare-worker");
13
13
 
14
14
  export const worker = await Worker("worker", {
15
15
  name: "my-worker",
@@ -38,8 +38,8 @@ export const worker = await Worker("worker", {
38
38
 
39
39
  # Examples
40
40
 
41
- - CloudFlare ViteJS Website + API Backend with Durable Objects: [examples/cloudflare-vite/](./examples/cloudflare-vite/alchemy.config.ts)
42
- - Deploy an AWS Lambda Function with a DynamoDB Table and IAM Role: [examples/aws-app/](./examples/aws-app/alchemy.config.ts)
41
+ - CloudFlare ViteJS Website + API Backend with Durable Objects: [examples/cloudflare-vite/](./examples/cloudflare-vite/alchemy.run.ts)
42
+ - Deploy an AWS Lambda Function with a DynamoDB Table and IAM Role: [examples/aws-app/](./examples/aws-app/alchemy.run.ts)
43
43
 
44
44
  # Getting Started
45
45
 
@@ -0,0 +1,137 @@
1
+ import prettier from "prettier";
2
+ import type { Context } from "../context";
3
+ import { Resource } from "../resource";
4
+ import type { Secret } from "../secret";
5
+ import { type ModelConfig } from "./client";
6
+ /**
7
+ * Properties for creating or updating an AstroFile
8
+ */
9
+ export interface AstroFileProps {
10
+ /**
11
+ * Path to the Astro file
12
+ */
13
+ path: string;
14
+ /**
15
+ * Base URL for the OpenAI API
16
+ * @default 'https://api.openai.com/v1'
17
+ */
18
+ baseURL?: string;
19
+ /**
20
+ * Prompt for generating content
21
+ * Use alchemy template literals to include file context:
22
+ * @example
23
+ * prompt: await alchemy`
24
+ * Generate an Astro component using:
25
+ * ${alchemy.file("src/types.ts")}
26
+ * `
27
+ */
28
+ prompt: string;
29
+ /**
30
+ * System prompt for the model
31
+ * This is used to provide instructions to the model about how to format the response
32
+ * The default system prompt instructs the model to return Astro code inside ```astro fences
33
+ * @default "You are an Astro component generator. Create Astro components based on the user's requirements. Your response MUST include only Astro code inside ```astro fences. Do not include any other text, explanations, or multiple code blocks."
34
+ */
35
+ system?: string;
36
+ /**
37
+ * OpenAI API key to use for generating content
38
+ * If not provided, will use OPENAI_API_KEY environment variable
39
+ */
40
+ apiKey?: Secret;
41
+ /**
42
+ * Model configuration
43
+ */
44
+ model?: ModelConfig;
45
+ /**
46
+ * Temperature for controlling randomness in generation.
47
+ * Higher values (e.g., 0.8) make output more random,
48
+ * lower values (e.g., 0.2) make it more deterministic.
49
+ * @default 0.7
50
+ */
51
+ temperature?: number;
52
+ /**
53
+ * Prettier configuration to use for formatting the Astro code
54
+ * If not provided, will use the default Prettier configuration
55
+ */
56
+ prettierConfig?: prettier.Options;
57
+ }
58
+ /**
59
+ * An Astro file that can be created, updated, and deleted
60
+ */
61
+ export interface AstroFile extends AstroFileProps, Resource<"ai::AstroFile"> {
62
+ /**
63
+ * Content of the Astro file
64
+ */
65
+ content: string;
66
+ /**
67
+ * Time at which the file was created
68
+ */
69
+ createdAt: number;
70
+ /**
71
+ * Time at which the file was last updated
72
+ */
73
+ updatedAt: number;
74
+ }
75
+ /**
76
+ * Resource for generating Astro files using AI models.
77
+ * Extracts Astro code from between ```astro fences, validates the response,
78
+ * and formats the code with Prettier.
79
+ *
80
+ * @example
81
+ * // Create a simple Astro component
82
+ * const header = await AstroFile("header", {
83
+ * path: "./src/components/Header.astro",
84
+ * prompt: await alchemy`
85
+ * Generate an Astro header component with:
86
+ * - Site logo
87
+ * - Navigation menu with Home, About, Services, Contact links
88
+ * - Mobile responsive design
89
+ * - Dark/light mode toggle
90
+ * `,
91
+ * model: {
92
+ * id: "gpt-4o",
93
+ * provider: "openai"
94
+ * }
95
+ * });
96
+ *
97
+ * @example
98
+ * // Generate an Astro page with data fetching
99
+ * const blogPost = await AstroFile("blog-post", {
100
+ * path: "./src/pages/blog/[slug].astro",
101
+ * prompt: await alchemy`
102
+ * Create an Astro blog post page that:
103
+ * - Uses getStaticPaths to generate pages from a CMS
104
+ * - Renders markdown content
105
+ * - Includes author info, publication date, and related posts
106
+ * - Has social sharing buttons
107
+ *
108
+ * Use the following types:
109
+ * ${alchemy.file("src/types/Blog.ts")}
110
+ * `,
111
+ * temperature: 0.2,
112
+ * prettierConfig: {
113
+ * semi: false,
114
+ * singleQuote: true,
115
+ * printWidth: 120
116
+ * }
117
+ * });
118
+ *
119
+ * @example
120
+ * // Generate a layout with custom system prompt
121
+ * const mainLayout = await AstroFile("main-layout", {
122
+ * path: "./src/layouts/MainLayout.astro",
123
+ * prompt: await alchemy`
124
+ * Create the main layout for an Astro site that:
125
+ * - Includes common head metadata and SEO optimization
126
+ * - Has slots for page content, header, and footer
127
+ * - Imports and uses the Header and Footer components
128
+ * - Sets up viewport and responsive configurations
129
+ * `,
130
+ * system: "You are an expert Astro developer. Create a single Astro layout file inside ```astro fences with no additional text. Follow Astro best practices and include proper typing in the frontmatter section.",
131
+ * model: {
132
+ * id: "claude-3-opus-20240229",
133
+ * provider: "anthropic"
134
+ * }
135
+ * });
136
+ */
137
+ export declare const AstroFile: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<AstroFile>, id: string, props: AstroFileProps) => Promise<AstroFile>);
@@ -0,0 +1,173 @@
1
+ import { generateText } from "ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import prettier from "prettier";
5
+ import { Resource } from "../resource";
6
+ import { ignore } from "../util/ignore";
7
+ import { createModel } from "./client";
8
+ /**
9
+ * Default system prompt for Astro file generation
10
+ */
11
+ const DEFAULT_ASTRO_SYSTEM_PROMPT = "You are an Astro component generator. Create Astro components based on the user's requirements. Your response MUST include only Astro code inside ```astro fences. Do not include any other text, explanations, or multiple code blocks.";
12
+ /**
13
+ * Resource for generating Astro files using AI models.
14
+ * Extracts Astro code from between ```astro fences, validates the response,
15
+ * and formats the code with Prettier.
16
+ *
17
+ * @example
18
+ * // Create a simple Astro component
19
+ * const header = await AstroFile("header", {
20
+ * path: "./src/components/Header.astro",
21
+ * prompt: await alchemy`
22
+ * Generate an Astro header component with:
23
+ * - Site logo
24
+ * - Navigation menu with Home, About, Services, Contact links
25
+ * - Mobile responsive design
26
+ * - Dark/light mode toggle
27
+ * `,
28
+ * model: {
29
+ * id: "gpt-4o",
30
+ * provider: "openai"
31
+ * }
32
+ * });
33
+ *
34
+ * @example
35
+ * // Generate an Astro page with data fetching
36
+ * const blogPost = await AstroFile("blog-post", {
37
+ * path: "./src/pages/blog/[slug].astro",
38
+ * prompt: await alchemy`
39
+ * Create an Astro blog post page that:
40
+ * - Uses getStaticPaths to generate pages from a CMS
41
+ * - Renders markdown content
42
+ * - Includes author info, publication date, and related posts
43
+ * - Has social sharing buttons
44
+ *
45
+ * Use the following types:
46
+ * ${alchemy.file("src/types/Blog.ts")}
47
+ * `,
48
+ * temperature: 0.2,
49
+ * prettierConfig: {
50
+ * semi: false,
51
+ * singleQuote: true,
52
+ * printWidth: 120
53
+ * }
54
+ * });
55
+ *
56
+ * @example
57
+ * // Generate a layout with custom system prompt
58
+ * const mainLayout = await AstroFile("main-layout", {
59
+ * path: "./src/layouts/MainLayout.astro",
60
+ * prompt: await alchemy`
61
+ * Create the main layout for an Astro site that:
62
+ * - Includes common head metadata and SEO optimization
63
+ * - Has slots for page content, header, and footer
64
+ * - Imports and uses the Header and Footer components
65
+ * - Sets up viewport and responsive configurations
66
+ * `,
67
+ * system: "You are an expert Astro developer. Create a single Astro layout file inside ```astro fences with no additional text. Follow Astro best practices and include proper typing in the frontmatter section.",
68
+ * model: {
69
+ * id: "claude-3-opus-20240229",
70
+ * provider: "anthropic"
71
+ * }
72
+ * });
73
+ */
74
+ export const AstroFile = Resource("ai::AstroFile", async function (id, props) {
75
+ // Ensure directory exists
76
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
77
+ if (this.phase === "delete") {
78
+ try {
79
+ await fs.unlink(props.path);
80
+ }
81
+ catch (error) {
82
+ // Ignore if file doesn't exist
83
+ if (error.code !== "ENOENT") {
84
+ throw error;
85
+ }
86
+ }
87
+ return this.destroy();
88
+ }
89
+ // Use provided system prompt or default
90
+ const system = props.system || DEFAULT_ASTRO_SYSTEM_PROMPT;
91
+ // Generate initial content
92
+ const { text } = await generateText({
93
+ model: createModel(props),
94
+ prompt: props.prompt,
95
+ system,
96
+ providerOptions: props.model?.options,
97
+ ...(props.temperature === undefined
98
+ ? {}
99
+ : { temperature: props.temperature }),
100
+ });
101
+ // Extract and validate Astro code
102
+ let { code, error } = await extractAstroCode(text);
103
+ // Re-prompt if there are validation errors
104
+ if (error) {
105
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one Astro code block inside \`\`\`astro fences.`;
106
+ const { text: retryText } = await generateText({
107
+ model: createModel(props),
108
+ prompt: props.prompt,
109
+ system: errorSystem,
110
+ providerOptions: props.model?.options,
111
+ ...(props.temperature === undefined
112
+ ? {}
113
+ : { temperature: props.temperature }),
114
+ });
115
+ const retryResult = await extractAstroCode(retryText);
116
+ if (retryResult.error) {
117
+ throw new Error(`Failed to generate valid Astro code: ${retryResult.error}`);
118
+ }
119
+ code = retryResult.code;
120
+ }
121
+ // Format the code with Prettier
122
+ try {
123
+ // Set default parser to astro
124
+ const prettierOptions = {
125
+ parser: "astro",
126
+ ...props.prettierConfig,
127
+ };
128
+ // Format the code
129
+ code = await prettier.format(code, prettierOptions);
130
+ }
131
+ catch (error) {
132
+ // If Prettier formatting fails, just use the unformatted code
133
+ console.warn("Failed to format Astro code with Prettier:", error);
134
+ }
135
+ if (this.phase === "update" && props.path !== this.props.path) {
136
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
137
+ }
138
+ // Write content to file
139
+ await fs.writeFile(props.path, code);
140
+ // Get file stats for timestamps
141
+ const stats = await fs.stat(props.path);
142
+ // Return the resource
143
+ return this({
144
+ ...props,
145
+ content: code,
146
+ createdAt: stats.birthtimeMs,
147
+ updatedAt: stats.mtimeMs,
148
+ });
149
+ });
150
+ /**
151
+ * Extracts Astro code from between ```astro fences
152
+ * Validates that exactly one Astro code block exists
153
+ *
154
+ * @param text The text to extract Astro code from
155
+ * @returns The extracted Astro code or error message
156
+ */
157
+ async function extractAstroCode(text) {
158
+ const astroCodeRegex = /```astro\s*([\s\S]*?)```/g;
159
+ const matches = Array.from(text.matchAll(astroCodeRegex));
160
+ if (matches.length === 0) {
161
+ return {
162
+ code: "",
163
+ error: "No Astro code block found in the response. Please include your code within ```astro fences.",
164
+ };
165
+ }
166
+ if (matches.length > 1) {
167
+ return {
168
+ code: "",
169
+ error: "Multiple Astro code blocks found in the response. Please provide exactly one code block within ```astro fences.",
170
+ };
171
+ }
172
+ return { code: matches[0][1].trim() };
173
+ }
@@ -16,7 +16,7 @@ export interface ModelConfig {
16
16
  /**
17
17
  * Model-specific options
18
18
  */
19
- options?: Record<string, unknown>;
19
+ options?: Record<string, any>;
20
20
  }
21
21
  /**
22
22
  * Configuration for creating an OpenAI client
@@ -40,12 +40,4 @@ export interface ClientConfig {
40
40
  /**
41
41
  * Creates an OpenAI-compatible client with the given configuration
42
42
  */
43
- export declare function createClient(config: ClientConfig): import("@ai-sdk/openai-compatible").OpenAICompatibleProvider<string, string, string, string>;
44
- /**
45
- * Gets the model ID from the configuration or returns the default
46
- */
47
- export declare function getModelId(config: ClientConfig): string;
48
- /**
49
- * Gets the model options from the configuration
50
- */
51
- export declare function getModelOptions(config: ClientConfig): Record<string, unknown>;
43
+ export declare function createModel(config: ClientConfig): import("ai").LanguageModelV1;
package/lib/ai/client.js CHANGED
@@ -1,29 +1,13 @@
1
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
1
+ import { anthropic } from "@ai-sdk/anthropic";
2
+ import { openai } from "@ai-sdk/openai";
2
3
  /**
3
4
  * Creates an OpenAI-compatible client with the given configuration
4
5
  */
5
- export function createClient(config) {
6
- // Get API key from props or environment
7
- const apiKey = config.apiKey?.unencrypted || process.env.OPENAI_API_KEY;
8
- if (!apiKey) {
9
- throw new Error("OpenAI API key is required");
6
+ export function createModel(config) {
7
+ if (config.model?.provider === "anthropic") {
8
+ return anthropic(config.model?.id ?? "claude-3-7-sonnet-latest");
9
+ }
10
+ else {
11
+ return openai(config.model?.id ?? "gpt-4o");
10
12
  }
11
- // Initialize OpenAI compatible provider
12
- return createOpenAICompatible({
13
- name: config.model?.provider || "openai",
14
- apiKey,
15
- baseURL: config.baseURL || "https://api.openai.com/v1",
16
- });
17
- }
18
- /**
19
- * Gets the model ID from the configuration or returns the default
20
- */
21
- export function getModelId(config) {
22
- return config.model?.id || "gpt-4o";
23
- }
24
- /**
25
- * Gets the model options from the configuration
26
- */
27
- export function getModelOptions(config) {
28
- return config.model?.options || {};
29
13
  }
@@ -0,0 +1,131 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import type { Secret } from "../secret";
4
+ import { type ModelConfig } from "./client";
5
+ /**
6
+ * Properties for creating or updating a CSSFile
7
+ */
8
+ export interface CSSFileProps {
9
+ /**
10
+ * Path to the CSS file
11
+ */
12
+ path: string;
13
+ /**
14
+ * Base URL for the OpenAI API
15
+ * @default 'https://api.openai.com/v1'
16
+ */
17
+ baseURL?: string;
18
+ /**
19
+ * Prompt for generating content
20
+ * Use alchemy template literals to include file context:
21
+ * @example
22
+ * prompt: await alchemy`
23
+ * Generate CSS styles for:
24
+ * ${alchemy.file("src/components/Button.jsx")}
25
+ * `
26
+ */
27
+ prompt: string;
28
+ /**
29
+ * System prompt for the model
30
+ * This is used to provide instructions to the model about how to format the response
31
+ * The default system prompt instructs the model to return CSS code inside ```css fences
32
+ * @default "You are a CSS code generator. Create CSS code based on the user's requirements. Your response MUST include only CSS code inside ```css fences. Do not include any other text, explanations, or multiple code blocks."
33
+ */
34
+ system?: string;
35
+ /**
36
+ * OpenAI API key to use for generating content
37
+ * If not provided, will use OPENAI_API_KEY environment variable
38
+ */
39
+ apiKey?: Secret;
40
+ /**
41
+ * Model configuration
42
+ */
43
+ model?: ModelConfig;
44
+ /**
45
+ * Temperature for controlling randomness in generation.
46
+ * Higher values (e.g., 0.8) make output more random,
47
+ * lower values (e.g., 0.2) make it more deterministic.
48
+ * @default 0.7
49
+ */
50
+ temperature?: number;
51
+ }
52
+ /**
53
+ * A CSS file that can be created, updated, and deleted
54
+ */
55
+ export interface CSSFile extends CSSFileProps, Resource<"ai::CSSFile"> {
56
+ /**
57
+ * Content of the CSS file
58
+ */
59
+ content: string;
60
+ /**
61
+ * Time at which the file was created
62
+ */
63
+ createdAt: number;
64
+ /**
65
+ * Time at which the file was last updated
66
+ */
67
+ updatedAt: number;
68
+ }
69
+ /**
70
+ * Resource for generating CSS files using AI models.
71
+ * Extracts CSS code from between ```css fences and validates the response.
72
+ *
73
+ * @example
74
+ * // Create styles for a website
75
+ * const mainStyles = await CSSFile("main-styles", {
76
+ * path: "./public/css/main.css",
77
+ * prompt: await alchemy`
78
+ * Generate modern CSS styles for a company website with:
79
+ * - Clean, minimalist design
80
+ * - Primary color: #0062ff
81
+ * - Secondary color: #6c757d
82
+ * - Light gray background
83
+ * - Responsive layout for mobile, tablet, and desktop
84
+ * - Custom styles for buttons, cards, and navigation
85
+ * `,
86
+ * model: {
87
+ * id: "gpt-4o",
88
+ * provider: "openai"
89
+ * }
90
+ * });
91
+ *
92
+ * @example
93
+ * // Generate CSS based on existing HTML
94
+ * const componentStyles = await CSSFile("component-styles", {
95
+ * path: "./src/styles/component.css",
96
+ * prompt: await alchemy`
97
+ * Create CSS styles for this HTML component:
98
+ * ${alchemy.file("src/components/Card.html")}
99
+ *
100
+ * The styles should be:
101
+ * - Modern and clean
102
+ * - Include hover effects and transitions
103
+ * - Support both light and dark themes
104
+ * - Use CSS variables for colors and spacing
105
+ * `,
106
+ * temperature: 0.2
107
+ * });
108
+ *
109
+ * @example
110
+ * // Generate CSS animation with custom system prompt
111
+ * const animationStyles = await CSSFile("animations", {
112
+ * path: "./src/styles/animations.css",
113
+ * prompt: await alchemy`
114
+ * Create CSS animations for:
115
+ * - Fade in/out
116
+ * - Slide in from different directions
117
+ * - Pulse effect
118
+ * - Bounce effect
119
+ * - Scale in/out
120
+ * - Rotate
121
+ *
122
+ * Each animation should be reusable via class names.
123
+ * `,
124
+ * system: "You are an expert CSS animator. Create a single CSS file inside ```css fences with no additional text. Use modern CSS animation techniques and include vendor prefixes where needed for browser compatibility.",
125
+ * model: {
126
+ * id: "claude-3-opus-20240229",
127
+ * provider: "anthropic"
128
+ * }
129
+ * });
130
+ */
131
+ export declare const CSSFile: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<CSSFile>, id: string, props: CSSFileProps) => Promise<CSSFile>);
@@ -0,0 +1,158 @@
1
+ import { generateText } from "ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { Resource } from "../resource";
5
+ import { ignore } from "../util/ignore";
6
+ import { createModel } from "./client";
7
+ /**
8
+ * Default system prompt for CSS file generation
9
+ */
10
+ const DEFAULT_CSS_SYSTEM_PROMPT = "You are a CSS code generator. Create CSS code based on the user's requirements. Your response MUST include only CSS code inside ```css fences. Do not include any other text, explanations, or multiple code blocks.";
11
+ /**
12
+ * Resource for generating CSS files using AI models.
13
+ * Extracts CSS code from between ```css fences and validates the response.
14
+ *
15
+ * @example
16
+ * // Create styles for a website
17
+ * const mainStyles = await CSSFile("main-styles", {
18
+ * path: "./public/css/main.css",
19
+ * prompt: await alchemy`
20
+ * Generate modern CSS styles for a company website with:
21
+ * - Clean, minimalist design
22
+ * - Primary color: #0062ff
23
+ * - Secondary color: #6c757d
24
+ * - Light gray background
25
+ * - Responsive layout for mobile, tablet, and desktop
26
+ * - Custom styles for buttons, cards, and navigation
27
+ * `,
28
+ * model: {
29
+ * id: "gpt-4o",
30
+ * provider: "openai"
31
+ * }
32
+ * });
33
+ *
34
+ * @example
35
+ * // Generate CSS based on existing HTML
36
+ * const componentStyles = await CSSFile("component-styles", {
37
+ * path: "./src/styles/component.css",
38
+ * prompt: await alchemy`
39
+ * Create CSS styles for this HTML component:
40
+ * ${alchemy.file("src/components/Card.html")}
41
+ *
42
+ * The styles should be:
43
+ * - Modern and clean
44
+ * - Include hover effects and transitions
45
+ * - Support both light and dark themes
46
+ * - Use CSS variables for colors and spacing
47
+ * `,
48
+ * temperature: 0.2
49
+ * });
50
+ *
51
+ * @example
52
+ * // Generate CSS animation with custom system prompt
53
+ * const animationStyles = await CSSFile("animations", {
54
+ * path: "./src/styles/animations.css",
55
+ * prompt: await alchemy`
56
+ * Create CSS animations for:
57
+ * - Fade in/out
58
+ * - Slide in from different directions
59
+ * - Pulse effect
60
+ * - Bounce effect
61
+ * - Scale in/out
62
+ * - Rotate
63
+ *
64
+ * Each animation should be reusable via class names.
65
+ * `,
66
+ * system: "You are an expert CSS animator. Create a single CSS file inside ```css fences with no additional text. Use modern CSS animation techniques and include vendor prefixes where needed for browser compatibility.",
67
+ * model: {
68
+ * id: "claude-3-opus-20240229",
69
+ * provider: "anthropic"
70
+ * }
71
+ * });
72
+ */
73
+ export const CSSFile = Resource("ai::CSSFile", async function (id, props) {
74
+ // Ensure directory exists
75
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
76
+ if (this.phase === "delete") {
77
+ try {
78
+ await fs.unlink(props.path);
79
+ }
80
+ catch (error) {
81
+ // Ignore if file doesn't exist
82
+ if (error.code !== "ENOENT") {
83
+ throw error;
84
+ }
85
+ }
86
+ return this.destroy();
87
+ }
88
+ // Use provided system prompt or default
89
+ const system = props.system || DEFAULT_CSS_SYSTEM_PROMPT;
90
+ // Generate initial content
91
+ const { text } = await generateText({
92
+ model: createModel(props),
93
+ prompt: props.prompt,
94
+ system,
95
+ providerOptions: props.model?.options,
96
+ ...(props.temperature === undefined
97
+ ? {}
98
+ : { temperature: props.temperature }),
99
+ });
100
+ // Extract and validate CSS code
101
+ let { code, error } = await extractCSSCode(text);
102
+ // Re-prompt if there are validation errors
103
+ if (error) {
104
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one CSS code block inside \`\`\`css fences.`;
105
+ const { text: retryText } = await generateText({
106
+ model: createModel(props),
107
+ prompt: props.prompt,
108
+ system: errorSystem,
109
+ providerOptions: props.model?.options,
110
+ ...(props.temperature === undefined
111
+ ? {}
112
+ : { temperature: props.temperature }),
113
+ });
114
+ const retryResult = await extractCSSCode(retryText);
115
+ if (retryResult.error) {
116
+ throw new Error(`Failed to generate valid CSS code: ${retryResult.error}\n${retryText}`);
117
+ }
118
+ code = retryResult.code;
119
+ }
120
+ if (this.phase === "update" && props.path !== this.props.path) {
121
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
122
+ }
123
+ // Write content to file
124
+ await fs.writeFile(props.path, code);
125
+ // Get file stats for timestamps
126
+ const stats = await fs.stat(props.path);
127
+ // Return the resource
128
+ return this({
129
+ ...props,
130
+ content: code,
131
+ createdAt: stats.birthtimeMs,
132
+ updatedAt: stats.mtimeMs,
133
+ });
134
+ });
135
+ /**
136
+ * Extracts CSS code from between ```css fences
137
+ * Validates that exactly one CSS code block exists
138
+ *
139
+ * @param text The text to extract CSS code from
140
+ * @returns The extracted CSS code or error message
141
+ */
142
+ async function extractCSSCode(text) {
143
+ const cssCodeRegex = /```css\s*([\s\S]*?)```/g;
144
+ const matches = Array.from(text.matchAll(cssCodeRegex));
145
+ if (matches.length === 0) {
146
+ return {
147
+ code: "",
148
+ error: "No CSS code block found in the response. Please include your code within ```css fences.",
149
+ };
150
+ }
151
+ if (matches.length > 1) {
152
+ return {
153
+ code: "",
154
+ error: "Multiple CSS code blocks found in the response. Please provide exactly one code block within ```css fences.",
155
+ };
156
+ }
157
+ return { code: matches[0][1].trim() };
158
+ }