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
@@ -0,0 +1,293 @@
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 type { Context } from "../context";
6
+ import { Resource } from "../resource";
7
+ import type { Secret } from "../secret";
8
+ import { ignore } from "../util/ignore";
9
+ import { type ModelConfig, createModel } from "./client";
10
+
11
+ /**
12
+ * Properties for creating or updating a TypeScriptFile
13
+ */
14
+ export interface TypeScriptFileProps {
15
+ /**
16
+ * Path to the TypeScript file
17
+ */
18
+ path: string;
19
+
20
+ /**
21
+ * Base URL for the OpenAI API
22
+ * @default 'https://api.openai.com/v1'
23
+ */
24
+ baseURL?: string;
25
+
26
+ /**
27
+ * Prompt for generating content
28
+ * Use alchemy template literals to include file context:
29
+ * @example
30
+ * prompt: await alchemy`
31
+ * Generate a TypeScript utility function using:
32
+ * ${alchemy.file("src/types.ts")}
33
+ * `
34
+ */
35
+ prompt: string;
36
+
37
+ /**
38
+ * System prompt for the model
39
+ * This is used to provide instructions to the model about how to format the response
40
+ * The default system prompt instructs the model to return TypeScript code inside ```ts fences
41
+ * @default "You are a TypeScript code generator. Create TypeScript code based on the user's requirements. Your response MUST include only TypeScript code inside ```ts fences. Do not include any other text, explanations, or multiple code blocks."
42
+ */
43
+ system?: string;
44
+
45
+ /**
46
+ * OpenAI API key to use for generating content
47
+ * If not provided, will use OPENAI_API_KEY environment variable
48
+ */
49
+ apiKey?: Secret;
50
+
51
+ /**
52
+ * Model configuration
53
+ */
54
+ model?: ModelConfig;
55
+
56
+ /**
57
+ * Temperature for controlling randomness in generation.
58
+ * Higher values (e.g., 0.8) make output more random,
59
+ * lower values (e.g., 0.2) make it more deterministic.
60
+ * @default 0.7
61
+ */
62
+ temperature?: number;
63
+
64
+ /**
65
+ * Prettier configuration to use for formatting the TypeScript code
66
+ * If not provided, will use the default Prettier configuration
67
+ */
68
+ prettierConfig?: prettier.Options;
69
+ }
70
+
71
+ /**
72
+ * A TypeScript file that can be created, updated, and deleted
73
+ */
74
+ export interface TypeScriptFile
75
+ extends TypeScriptFileProps,
76
+ Resource<"ai::TypeScriptFile"> {
77
+ /**
78
+ * Content of the TypeScript file
79
+ */
80
+ content: string;
81
+
82
+ /**
83
+ * Time at which the file was created
84
+ */
85
+ createdAt: number;
86
+
87
+ /**
88
+ * Time at which the file was last updated
89
+ */
90
+ updatedAt: number;
91
+ }
92
+
93
+ /**
94
+ * Default system prompt for TypeScript file generation
95
+ */
96
+ const DEFAULT_TS_SYSTEM_PROMPT =
97
+ "You are a TypeScript code generator. Create TypeScript code based on the user's requirements. Your response MUST include only TypeScript code inside ```ts fences. Do not include any other text, explanations, or multiple code blocks.";
98
+
99
+ /**
100
+ * Resource for generating TypeScript files using AI models.
101
+ * Extracts TypeScript code from between ```ts fences, validates the response,
102
+ * and formats the code with Prettier.
103
+ *
104
+ * @example
105
+ * // Create a utility function
106
+ * const utils = await TypeScriptFile("string-utils", {
107
+ * path: "./src/utils/string-utils.ts",
108
+ * prompt: await alchemy`
109
+ * Generate TypeScript utility functions for string manipulation:
110
+ * - Capitalize first letter
111
+ * - Truncate with ellipsis
112
+ * - Convert to camelCase and kebab-case
113
+ * - Remove special characters
114
+ * `,
115
+ * model: {
116
+ * id: "gpt-4o",
117
+ * provider: "openai"
118
+ * }
119
+ * });
120
+ *
121
+ * @example
122
+ * // Generate a TypeScript class with custom formatting
123
+ * const userService = await TypeScriptFile("user-service", {
124
+ * path: "./src/services/UserService.ts",
125
+ * prompt: await alchemy`
126
+ * Create a UserService class that handles user authentication and profile management.
127
+ * The service should use the User type from:
128
+ * ${alchemy.file("src/types/User.ts")}
129
+ *
130
+ * Include methods for:
131
+ * - login(email, password)
132
+ * - register(user)
133
+ * - updateProfile(userId, profileData)
134
+ * - deleteAccount(userId)
135
+ * `,
136
+ * temperature: 0.2,
137
+ * prettierConfig: {
138
+ * semi: false,
139
+ * singleQuote: true,
140
+ * printWidth: 120
141
+ * }
142
+ * });
143
+ *
144
+ * @example
145
+ * // Generate a React hook with custom system prompt
146
+ * const useFormHook = await TypeScriptFile("use-form", {
147
+ * path: "./src/hooks/useForm.ts",
148
+ * prompt: await alchemy`
149
+ * Create a custom React hook called useForm that handles form state, validation, and submission.
150
+ * It should support:
151
+ * - Initial values
152
+ * - Validation rules
153
+ * - Field errors
154
+ * - Form submission with loading state
155
+ * - Reset functionality
156
+ * `,
157
+ * system: "You are an expert React developer specializing in TypeScript hooks. Create a single TypeScript file inside ```ts fences with no additional text. Follow React best practices and include proper typing.",
158
+ * model: {
159
+ * id: "claude-3-opus-20240229",
160
+ * provider: "anthropic"
161
+ * }
162
+ * });
163
+ */
164
+ export const TypeScriptFile = Resource(
165
+ "ai::TypeScriptFile",
166
+ async function (
167
+ this: Context<TypeScriptFile>,
168
+ id: string,
169
+ props: TypeScriptFileProps,
170
+ ): Promise<TypeScriptFile> {
171
+ // Ensure directory exists
172
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
173
+
174
+ if (this.phase === "delete") {
175
+ try {
176
+ await fs.unlink(props.path);
177
+ } catch (error: any) {
178
+ // Ignore if file doesn't exist
179
+ if (error.code !== "ENOENT") {
180
+ throw error;
181
+ }
182
+ }
183
+ return this.destroy();
184
+ }
185
+
186
+ // Use provided system prompt or default
187
+ const system = props.system || DEFAULT_TS_SYSTEM_PROMPT;
188
+
189
+ // Generate initial content
190
+ const { text } = await generateText({
191
+ model: createModel(props),
192
+ prompt: props.prompt,
193
+ system,
194
+ providerOptions: props.model?.options,
195
+ ...(props.temperature === undefined
196
+ ? {}
197
+ : { temperature: props.temperature }),
198
+ });
199
+
200
+ // Extract and validate TypeScript code
201
+ let { code, error } = await extractTypeScriptCode(text);
202
+
203
+ // Re-prompt if there are validation errors
204
+ if (error) {
205
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one TypeScript code block inside \`\`\`ts fences.`;
206
+
207
+ const { text: retryText } = await generateText({
208
+ model: createModel(props),
209
+ prompt: props.prompt,
210
+ system: errorSystem,
211
+ providerOptions: props.model?.options,
212
+ ...(props.temperature === undefined
213
+ ? {}
214
+ : { temperature: props.temperature }),
215
+ });
216
+
217
+ const retryResult = await extractTypeScriptCode(retryText);
218
+
219
+ if (retryResult.error) {
220
+ throw new Error(
221
+ `Failed to generate valid TypeScript code: ${retryResult.error}`,
222
+ );
223
+ }
224
+
225
+ code = retryResult.code;
226
+ }
227
+
228
+ // Format the code with Prettier
229
+ try {
230
+ // Set default parser to typescript
231
+ const prettierOptions: prettier.Options = {
232
+ parser: "typescript",
233
+ ...props.prettierConfig,
234
+ };
235
+
236
+ // Format the code
237
+ code = await prettier.format(code, prettierOptions);
238
+ } catch (error) {
239
+ // If Prettier formatting fails, just use the unformatted code
240
+ console.warn("Failed to format TypeScript code with Prettier:", error);
241
+ }
242
+
243
+ if (this.phase === "update" && props.path !== this.props.path) {
244
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
245
+ }
246
+
247
+ // Write content to file
248
+ await fs.writeFile(props.path, code);
249
+
250
+ // Get file stats for timestamps
251
+ const stats = await fs.stat(props.path);
252
+
253
+ // Return the resource
254
+ return this({
255
+ ...props,
256
+ content: code,
257
+ createdAt: stats.birthtimeMs,
258
+ updatedAt: stats.mtimeMs,
259
+ });
260
+ },
261
+ );
262
+
263
+ /**
264
+ * Extracts TypeScript code from between ```ts fences
265
+ * Validates that exactly one TypeScript code block exists
266
+ *
267
+ * @param text The text to extract TypeScript code from
268
+ * @returns The extracted TypeScript code or error message
269
+ */
270
+ async function extractTypeScriptCode(
271
+ text: string,
272
+ ): Promise<{ code: string; error?: string }> {
273
+ const tsCodeRegex = /```ts\s*([\s\S]*?)```/g;
274
+ const matches = Array.from(text.matchAll(tsCodeRegex));
275
+
276
+ if (matches.length === 0) {
277
+ return {
278
+ code: "",
279
+ error:
280
+ "No TypeScript code block found in the response. Please include your code within ```ts fences.",
281
+ };
282
+ }
283
+
284
+ if (matches.length > 1) {
285
+ return {
286
+ code: "",
287
+ error:
288
+ "Multiple TypeScript code blocks found in the response. Please provide exactly one code block within ```ts fences.",
289
+ };
290
+ }
291
+
292
+ return { code: matches[0][1].trim() };
293
+ }
@@ -0,0 +1,261 @@
1
+ import { generateText } from "ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import type { Context } from "../context";
5
+ import { Resource } from "../resource";
6
+ import type { Secret } from "../secret";
7
+ import { ignore } from "../util/ignore";
8
+ import { type ModelConfig, createModel } from "./client";
9
+
10
+ /**
11
+ * Properties for creating or updating a VueFile
12
+ */
13
+ export interface VueFileProps {
14
+ /**
15
+ * Path to the Vue file
16
+ */
17
+ path: string;
18
+
19
+ /**
20
+ * Base URL for the OpenAI API
21
+ * @default 'https://api.openai.com/v1'
22
+ */
23
+ baseURL?: string;
24
+
25
+ /**
26
+ * Prompt for generating content
27
+ * Use alchemy template literals to include file context:
28
+ * @example
29
+ * prompt: await alchemy`
30
+ * Generate a Vue component using:
31
+ * ${alchemy.file("src/api.ts")}
32
+ * `
33
+ */
34
+ prompt: string;
35
+
36
+ /**
37
+ * System prompt for the model
38
+ * This is used to provide instructions to the model about how to format the response
39
+ * The default system prompt instructs the model to return a single Vue component inside ```vue fences
40
+ * @default "You are a Vue component generator. Create a single Vue component based on the user's requirements. Your response MUST include only a single Vue component inside ```vue fences. Do not include any other text, explanations, or multiple code blocks."
41
+ */
42
+ system?: string;
43
+
44
+ /**
45
+ * OpenAI API key to use for generating content
46
+ * If not provided, will use OPENAI_API_KEY environment variable
47
+ */
48
+ apiKey?: Secret;
49
+
50
+ /**
51
+ * Model configuration
52
+ */
53
+ model?: ModelConfig;
54
+
55
+ /**
56
+ * Temperature for controlling randomness in generation.
57
+ * Higher values (e.g., 0.8) make output more random,
58
+ * lower values (e.g., 0.2) make it more deterministic.
59
+ * @default 0.7
60
+ */
61
+ temperature?: number;
62
+ }
63
+
64
+ /**
65
+ * A Vue file that can be created, updated, and deleted
66
+ */
67
+ export interface VueFile extends VueFileProps, Resource<"ai::VueFile"> {
68
+ /**
69
+ * Content of the Vue file
70
+ */
71
+ content: string;
72
+
73
+ /**
74
+ * Time at which the file was created
75
+ */
76
+ createdAt: number;
77
+
78
+ /**
79
+ * Time at which the file was last updated
80
+ */
81
+ updatedAt: number;
82
+ }
83
+
84
+ /**
85
+ * Default system prompt for Vue file generation
86
+ */
87
+ const DEFAULT_VUE_SYSTEM_PROMPT =
88
+ "You are a Vue component generator. Create a single Vue component based on the user's requirements. Your response MUST include only a single Vue component inside ```vue fences. Do not include any other text, explanations, or multiple code blocks.";
89
+
90
+ /**
91
+ * Resource for generating Vue files using AI models.
92
+ * Extracts Vue code from between ```vue fences and validates the response.
93
+ *
94
+ * @example
95
+ * // Create a simple Vue component
96
+ * const button = await VueFile("button-component", {
97
+ * path: "./src/components/Button.vue",
98
+ * prompt: await alchemy`
99
+ * Generate a customizable button Vue component with:
100
+ * - Primary, secondary, and outline variants
101
+ * - Small, medium, and large sizes
102
+ * - Loading state with spinner
103
+ * - Disabled state
104
+ * `,
105
+ * model: {
106
+ * id: "gpt-4o",
107
+ * provider: "openai"
108
+ * }
109
+ * });
110
+ *
111
+ * @example
112
+ * // Generate a Vue component using existing files as reference
113
+ * const userCard = await VueFile("user-card", {
114
+ * path: "./src/components/UserCard.vue",
115
+ * prompt: await alchemy`
116
+ * Create a UserCard Vue component that displays user information.
117
+ * Follow the styling patterns from:
118
+ * ${alchemy.file("src/components/Card.vue")}
119
+ *
120
+ * Use the user type from:
121
+ * ${alchemy.file("src/types/User.ts")}
122
+ * `,
123
+ * temperature: 0.2
124
+ * });
125
+ *
126
+ * @example
127
+ * // Generate a complex form component with validation and custom system prompt
128
+ * const form = await VueFile("registration-form", {
129
+ * path: "./src/components/RegistrationForm.vue",
130
+ * prompt: await alchemy`
131
+ * Generate a registration form Vue component with:
132
+ * - Email, password, and confirm password fields
133
+ * - Form validation using Vuelidate or similar
134
+ * - Error messages for each field
135
+ * - Submit handler that emits form data
136
+ *
137
+ * Follow these style guidelines:
138
+ * ${alchemy.file("src/styles/guidelines.md")}
139
+ * `,
140
+ * system: "You are an expert Vue component creator specializing in form components with validation. Create a single Vue component inside ```vue fences with no additional text.",
141
+ * model: {
142
+ * id: "claude-3-opus-20240229",
143
+ * provider: "anthropic"
144
+ * }
145
+ * });
146
+ */
147
+ export const VueFile = Resource(
148
+ "ai::VueFile",
149
+ async function (
150
+ this: Context<VueFile>,
151
+ id: string,
152
+ props: VueFileProps,
153
+ ): Promise<VueFile> {
154
+ // Ensure directory exists
155
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
156
+
157
+ if (this.phase === "delete") {
158
+ try {
159
+ await fs.unlink(props.path);
160
+ } catch (error: any) {
161
+ // Ignore if file doesn't exist
162
+ if (error.code !== "ENOENT") {
163
+ throw error;
164
+ }
165
+ }
166
+ return this.destroy();
167
+ }
168
+
169
+ // Use provided system prompt or default
170
+ const system = props.system || DEFAULT_VUE_SYSTEM_PROMPT;
171
+
172
+ // Generate initial content
173
+ const { text } = await generateText({
174
+ model: createModel(props),
175
+ prompt: props.prompt,
176
+ system,
177
+ providerOptions: props.model?.options,
178
+ ...(props.temperature === undefined
179
+ ? {}
180
+ : { temperature: props.temperature }),
181
+ });
182
+
183
+ // Extract and validate Vue code
184
+ let { code, error } = await extractVueCode(text);
185
+
186
+ // Re-prompt if there are validation errors
187
+ if (error) {
188
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one Vue component inside \`\`\`vue fences.`;
189
+
190
+ const { text: retryText } = await generateText({
191
+ model: createModel(props),
192
+ prompt: props.prompt,
193
+ system: errorSystem,
194
+ providerOptions: props.model?.options,
195
+ ...(props.temperature === undefined
196
+ ? {}
197
+ : { temperature: props.temperature }),
198
+ });
199
+
200
+ const retryResult = await extractVueCode(retryText);
201
+
202
+ if (retryResult.error) {
203
+ throw new Error(
204
+ `Failed to generate valid Vue code: ${retryResult.error}`,
205
+ );
206
+ }
207
+
208
+ code = retryResult.code;
209
+ }
210
+
211
+ if (this.phase === "update" && props.path !== this.props.path) {
212
+ await ignore("ENOENT", () => fs.unlink(this.props.path));
213
+ }
214
+
215
+ // Write content to file
216
+ await fs.writeFile(props.path, code);
217
+
218
+ // Get file stats for timestamps
219
+ const stats = await fs.stat(props.path);
220
+
221
+ // Return the resource
222
+ return this({
223
+ ...props,
224
+ content: code,
225
+ createdAt: stats.birthtimeMs,
226
+ updatedAt: stats.mtimeMs,
227
+ });
228
+ },
229
+ );
230
+
231
+ /**
232
+ * Extracts Vue code from between ```vue fences
233
+ * Validates that exactly one Vue code block exists
234
+ *
235
+ * @param text The text to extract Vue code from
236
+ * @returns The extracted Vue code or error message
237
+ */
238
+ async function extractVueCode(
239
+ text: string,
240
+ ): Promise<{ code: string; error?: string }> {
241
+ const vueCodeRegex = /```vue\s*([\s\S]*?)```/g;
242
+ const matches = Array.from(text.matchAll(vueCodeRegex));
243
+
244
+ if (matches.length === 0) {
245
+ return {
246
+ code: "",
247
+ error:
248
+ "No Vue code block found in the response. Please include your code within ```vue fences.",
249
+ };
250
+ }
251
+
252
+ if (matches.length > 1) {
253
+ return {
254
+ code: "",
255
+ error:
256
+ "Multiple Vue code blocks found in the response. Please provide exactly one code block within ```vue fences.",
257
+ };
258
+ }
259
+
260
+ return { code: matches[0][1].trim() };
261
+ }