alchemy 0.3.0 → 0.3.1
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/lib/ai/approve.d.ts +99 -0
- package/lib/ai/approve.js +76 -0
- package/lib/ai/astro-file.js +7 -24
- package/lib/ai/client.d.ts +7 -0
- package/lib/ai/client.js +45 -0
- package/lib/ai/css-file.js +7 -24
- package/lib/ai/data.d.ts +26 -21
- package/lib/ai/data.js +33 -22
- package/lib/ai/document.d.ts +64 -4
- package/lib/ai/document.js +102 -55
- package/lib/ai/html-file.js +7 -24
- package/lib/ai/index.d.ts +2 -0
- package/lib/ai/index.js +2 -0
- package/lib/ai/json-file.js +11 -32
- package/lib/ai/review.d.ts +122 -0
- package/lib/ai/review.js +101 -0
- package/lib/ai/typescript-file.d.ts +1 -2
- package/lib/ai/typescript-file.js +8 -39
- package/lib/ai/vue-file.js +7 -24
- package/lib/ai/yaml-file.js +12 -48
- package/lib/alchemy.js +17 -2
- package/lib/apply.js +1 -1
- package/lib/cloudflare/generate-asset-manifest.js +1 -1
- package/lib/cloudflare/static-site-router.js +16 -7
- package/lib/cloudflare/static-site.js +3 -0
- package/lib/cloudflare/upload-asset-manifest.js +4 -2
- package/lib/fs/copy-file.d.ts +54 -0
- package/lib/fs/copy-file.js +63 -0
- package/lib/fs/file.d.ts +13 -0
- package/lib/fs/file.js +24 -5
- package/lib/fs/index.d.ts +6 -0
- package/lib/fs/index.js +6 -0
- package/lib/fs/static-astro-file.d.ts +34 -0
- package/lib/fs/static-astro-file.js +39 -0
- package/lib/fs/static-css-file.d.ts +24 -0
- package/lib/fs/static-css-file.js +29 -0
- package/lib/fs/static-html-file.d.ts +31 -0
- package/lib/fs/static-html-file.js +36 -0
- package/lib/fs/static-json-file.d.ts +2 -2
- package/lib/fs/static-json-file.js +3 -2
- package/lib/fs/static-text-file.d.ts +1 -1
- package/lib/fs/static-text-file.js +3 -2
- package/lib/fs/static-typescript-file.d.ts +2 -2
- package/lib/fs/static-typescript-file.js +3 -2
- package/lib/fs/static-vue-file.d.ts +28 -0
- package/lib/fs/static-vue-file.js +33 -0
- package/lib/fs/static-yaml-file.d.ts +11 -8
- package/lib/fs/static-yaml-file.js +3 -2
- package/lib/internal/getting-started.d.ts +15 -4
- package/lib/internal/getting-started.js +34 -41
- package/lib/internal/index.d.ts +3 -0
- package/lib/internal/index.js +3 -0
- package/lib/internal/providers.d.ts +7 -3
- package/lib/internal/providers.js +42 -18
- package/lib/internal/tutorial.d.ts +104 -0
- package/lib/internal/tutorial.js +251 -0
- package/lib/web/vitepress/config.d.ts +10 -0
- package/lib/web/vitepress/config.js +31 -0
- package/lib/web/vitepress/home-page.d.ts +5 -3
- package/lib/web/vitepress/home-page.js +48 -35
- package/lib/web/vitepress/index.d.ts +1 -0
- package/lib/web/vitepress/index.js +1 -0
- package/lib/web/vitepress/vitepress.d.ts +5 -4
- package/lib/web/vitepress/vitepress.js +17 -34
- package/package.json +1 -1
- package/src/ai/approve.ts +163 -0
- package/src/ai/astro-file.ts +11 -30
- package/src/ai/client.ts +59 -0
- package/src/ai/css-file.ts +11 -30
- package/src/ai/data.ts +50 -25
- package/src/ai/document.ts +146 -65
- package/src/ai/html-file.ts +11 -30
- package/src/ai/index.ts +2 -0
- package/src/ai/json-file.ts +13 -37
- package/src/ai/review.ts +213 -0
- package/src/ai/typescript-file.ts +14 -48
- package/src/ai/vue-file.ts +11 -30
- package/src/ai/yaml-file.ts +13 -53
- package/src/alchemy.ts +18 -7
- package/src/apply.ts +8 -8
- package/src/cloudflare/generate-asset-manifest.ts +4 -4
- package/src/cloudflare/static-site-router.ts +18 -8
- package/src/cloudflare/static-site.ts +11 -7
- package/src/cloudflare/upload-asset-manifest.ts +6 -4
- package/src/cloudflare/worker.ts +44 -43
- package/src/fs/copy-file.ts +115 -0
- package/src/fs/file.ts +35 -9
- package/src/fs/index.ts +6 -0
- package/src/fs/static-astro-file.ts +45 -0
- package/src/fs/static-css-file.ts +35 -0
- package/src/fs/static-html-file.ts +42 -0
- package/src/fs/static-json-file.ts +4 -3
- package/src/fs/static-text-file.ts +5 -2
- package/src/fs/static-typescript-file.ts +4 -3
- package/src/fs/static-vue-file.ts +39 -0
- package/src/fs/static-yaml-file.ts +13 -9
- package/src/internal/getting-started.ts +51 -43
- package/src/internal/index.ts +3 -0
- package/src/internal/providers.ts +59 -27
- package/src/internal/tutorial.ts +392 -0
- package/src/web/vitepress/config.ts +48 -0
- package/src/web/vitepress/home-page.ts +54 -42
- package/src/web/vitepress/index.ts +1 -0
- package/src/web/vitepress/vitepress.ts +31 -48
- package/lib/internal/project.d.ts +0 -13
- package/lib/internal/project.js +0 -101
- package/src/internal/project.ts +0 -119
package/src/ai/review.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { generateText, type CoreMessage } from "ai";
|
|
2
|
+
import type { Context } from "../context";
|
|
3
|
+
import { Resource } from "../resource";
|
|
4
|
+
import type { Secret } from "../secret";
|
|
5
|
+
import { createModel, withRateLimitRetry, type ModelConfig } from "./client";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Properties for creating or updating a Review
|
|
9
|
+
*/
|
|
10
|
+
export interface ReviewProps {
|
|
11
|
+
/**
|
|
12
|
+
* Content to be reviewed
|
|
13
|
+
* Use alchemy template literals to include file context:
|
|
14
|
+
* @example
|
|
15
|
+
* content: await alchemy`
|
|
16
|
+
* Review this code:
|
|
17
|
+
* ${alchemy.file("src/api.ts")}
|
|
18
|
+
* `
|
|
19
|
+
*
|
|
20
|
+
* Required unless messages are provided
|
|
21
|
+
*/
|
|
22
|
+
content?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Prompt for the review
|
|
26
|
+
* This should include specific instructions for the review
|
|
27
|
+
* @example
|
|
28
|
+
* prompt: "Review this code for security vulnerabilities, performance issues, and best practices."
|
|
29
|
+
*
|
|
30
|
+
* Required unless messages are provided
|
|
31
|
+
*/
|
|
32
|
+
prompt?: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Message history for the conversation
|
|
36
|
+
* If provided, this will be used instead of the prompt and content
|
|
37
|
+
*/
|
|
38
|
+
messages?: CoreMessage[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* System prompt for the model
|
|
42
|
+
* This is used to provide instructions to the model about how to format the response
|
|
43
|
+
* The default system prompt instructs the model to act as a reviewer
|
|
44
|
+
*/
|
|
45
|
+
system?: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* AI API key to use for generating content
|
|
49
|
+
* If not provided, will use OPENAI_API_KEY environment variable
|
|
50
|
+
*/
|
|
51
|
+
apiKey?: Secret;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Model configuration
|
|
55
|
+
*/
|
|
56
|
+
model?: ModelConfig;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Temperature for controlling randomness in generation.
|
|
60
|
+
* Higher values (e.g., 0.8) make output more random,
|
|
61
|
+
* lower values (e.g., 0.2) make it more deterministic.
|
|
62
|
+
* @default 0.7
|
|
63
|
+
*/
|
|
64
|
+
temperature?: number;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Maximum number of tokens to generate.
|
|
68
|
+
* Higher values allow for longer documents but may increase cost and generation time.
|
|
69
|
+
* @default 10000
|
|
70
|
+
*/
|
|
71
|
+
maxTokens?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Output returned after Review creation/update
|
|
76
|
+
*/
|
|
77
|
+
export interface Review extends ReviewProps, Resource<"ai::Review"> {
|
|
78
|
+
/**
|
|
79
|
+
* Content of the review
|
|
80
|
+
*/
|
|
81
|
+
content: string;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Updated message history with the review response appended
|
|
85
|
+
*/
|
|
86
|
+
messages: CoreMessage[];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Time at which the review was created
|
|
90
|
+
*/
|
|
91
|
+
createdAt: number;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Time at which the review was last updated
|
|
95
|
+
*/
|
|
96
|
+
updatedAt: number;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Default system prompt for review generation
|
|
101
|
+
*/
|
|
102
|
+
const DEFAULT_REVIEW_SYSTEM_PROMPT = `You are an expert code reviewer and technical writer. Your task is to provide a thorough review of the provided content. Your review should include:
|
|
103
|
+
|
|
104
|
+
1. A high-level summary of the content
|
|
105
|
+
2. Detailed feedback on the content's quality, accuracy, and completeness
|
|
106
|
+
3. Specific suggestions for improvement
|
|
107
|
+
4. Alternative approaches or solutions where applicable
|
|
108
|
+
5. Best practices that should be followed
|
|
109
|
+
6. Potential issues or concerns that should be addressed
|
|
110
|
+
|
|
111
|
+
Provide your review in a clear, well-structured format.`;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Creates or updates a Review resource
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* // Create a review of a code file
|
|
118
|
+
* const codeReview = await Review("api-review", {
|
|
119
|
+
* content: await alchemy`
|
|
120
|
+
* Review this API implementation:
|
|
121
|
+
* ${alchemy.file("src/api.ts")}
|
|
122
|
+
* `,
|
|
123
|
+
* prompt: "Review this API implementation for security vulnerabilities, performance issues, and best practices. Suggest improvements and alternative approaches."
|
|
124
|
+
* });
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* // Create a review of a document
|
|
128
|
+
* const docReview = await Review("tutorial-review", {
|
|
129
|
+
* content: await alchemy`
|
|
130
|
+
* Review this tutorial:
|
|
131
|
+
* ${alchemy.file("docs/tutorials/getting-started.md")}
|
|
132
|
+
* `,
|
|
133
|
+
* prompt: "Review this tutorial for clarity, accuracy, and completeness. Suggest improvements to make it more user-friendly."
|
|
134
|
+
* });
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* // Create a review with message history
|
|
138
|
+
* const reviewWithHistory = await Review("code-review-iteration-2", {
|
|
139
|
+
* content: "Updated code implementation",
|
|
140
|
+
* prompt: "Review this updated implementation",
|
|
141
|
+
* messages: [
|
|
142
|
+
* { role: "user", content: "Can you review my code?" },
|
|
143
|
+
* { role: "assistant", content: "I'll review your code. Please share it." },
|
|
144
|
+
* { role: "user", content: "Here's my implementation: [code]" },
|
|
145
|
+
* { role: "assistant", content: "Here's my review of your code: [previous review]" }
|
|
146
|
+
* ]
|
|
147
|
+
* });
|
|
148
|
+
*/
|
|
149
|
+
export const Review = Resource(
|
|
150
|
+
"ai::Review",
|
|
151
|
+
async function (
|
|
152
|
+
this: Context<Review>,
|
|
153
|
+
id: string,
|
|
154
|
+
props: ReviewProps
|
|
155
|
+
): Promise<Review> {
|
|
156
|
+
if (this.phase === "delete") {
|
|
157
|
+
return this.destroy();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Use provided system prompt or default
|
|
161
|
+
const system = props.system || DEFAULT_REVIEW_SYSTEM_PROMPT;
|
|
162
|
+
|
|
163
|
+
// Create messages array if not provided
|
|
164
|
+
const messages =
|
|
165
|
+
props.messages ||
|
|
166
|
+
(props.content && props.prompt
|
|
167
|
+
? [
|
|
168
|
+
{
|
|
169
|
+
role: "user",
|
|
170
|
+
content: `${props.content}\n\n${props.prompt}`,
|
|
171
|
+
},
|
|
172
|
+
]
|
|
173
|
+
: []);
|
|
174
|
+
|
|
175
|
+
if (messages.length === 0) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
"Either messages or both content and prompt must be provided"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Generate review content with rate limit retry
|
|
182
|
+
const { text } = await withRateLimitRetry(async () => {
|
|
183
|
+
return generateText({
|
|
184
|
+
model: createModel(props),
|
|
185
|
+
messages,
|
|
186
|
+
system,
|
|
187
|
+
maxTokens: props.maxTokens || 8192,
|
|
188
|
+
providerOptions: props.model?.options,
|
|
189
|
+
...(props.temperature === undefined
|
|
190
|
+
? {}
|
|
191
|
+
: { temperature: props.temperature }),
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// Create updated message history with the AI's response
|
|
196
|
+
const updatedMessages = [
|
|
197
|
+
...messages,
|
|
198
|
+
{
|
|
199
|
+
role: "assistant" as const,
|
|
200
|
+
content: text,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
// Return the review with the direct text from the LLM and updated messages
|
|
205
|
+
return this({
|
|
206
|
+
...props,
|
|
207
|
+
content: text,
|
|
208
|
+
messages: updatedMessages,
|
|
209
|
+
createdAt: Date.now(),
|
|
210
|
+
updatedAt: Date.now(),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
);
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { generateText } from "ai";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import prettier from "prettier";
|
|
5
2
|
import type { Context } from "../context";
|
|
3
|
+
import { StaticTypeScriptFile } from "../fs/static-typescript-file";
|
|
6
4
|
import { Resource } from "../resource";
|
|
7
5
|
import type { Secret } from "../secret";
|
|
8
|
-
import {
|
|
9
|
-
import { type ModelConfig, createModel } from "./client";
|
|
6
|
+
import { createModel, type ModelConfig } from "./client";
|
|
10
7
|
|
|
11
8
|
/**
|
|
12
9
|
* Properties for creating or updating a TypeScriptFile
|
|
@@ -65,7 +62,7 @@ export interface TypeScriptFileProps {
|
|
|
65
62
|
* Prettier configuration to use for formatting the TypeScript code
|
|
66
63
|
* If not provided, will use the default Prettier configuration
|
|
67
64
|
*/
|
|
68
|
-
prettierConfig?:
|
|
65
|
+
prettierConfig?: object;
|
|
69
66
|
}
|
|
70
67
|
|
|
71
68
|
/**
|
|
@@ -166,20 +163,11 @@ export const TypeScriptFile = Resource(
|
|
|
166
163
|
async function (
|
|
167
164
|
this: Context<TypeScriptFile>,
|
|
168
165
|
id: string,
|
|
169
|
-
props: TypeScriptFileProps
|
|
166
|
+
props: TypeScriptFileProps
|
|
170
167
|
): Promise<TypeScriptFile> {
|
|
171
|
-
//
|
|
172
|
-
await fs.mkdir(path.dirname(props.path), { recursive: true });
|
|
173
|
-
|
|
168
|
+
// Handle delete phase
|
|
174
169
|
if (this.phase === "delete") {
|
|
175
|
-
|
|
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
|
-
}
|
|
170
|
+
// StaticTypeScriptFile will handle the deletion
|
|
183
171
|
return this.destroy();
|
|
184
172
|
}
|
|
185
173
|
|
|
@@ -218,46 +206,24 @@ export const TypeScriptFile = Resource(
|
|
|
218
206
|
|
|
219
207
|
if (retryResult.error) {
|
|
220
208
|
throw new Error(
|
|
221
|
-
`Failed to generate valid TypeScript code: ${retryResult.error}
|
|
209
|
+
`Failed to generate valid TypeScript code: ${retryResult.error}`
|
|
222
210
|
);
|
|
223
211
|
}
|
|
224
212
|
|
|
225
213
|
code = retryResult.code;
|
|
226
214
|
}
|
|
227
215
|
|
|
228
|
-
//
|
|
229
|
-
|
|
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);
|
|
216
|
+
// Use StaticTypeScriptFile to create/update the file
|
|
217
|
+
const file = await StaticTypeScriptFile("file", props.path, code);
|
|
252
218
|
|
|
253
219
|
// Return the resource
|
|
254
220
|
return this({
|
|
255
221
|
...props,
|
|
256
|
-
content:
|
|
257
|
-
createdAt:
|
|
258
|
-
updatedAt:
|
|
222
|
+
content: file.content,
|
|
223
|
+
createdAt: Date.now(),
|
|
224
|
+
updatedAt: Date.now(),
|
|
259
225
|
});
|
|
260
|
-
}
|
|
226
|
+
}
|
|
261
227
|
);
|
|
262
228
|
|
|
263
229
|
/**
|
|
@@ -268,7 +234,7 @@ export const TypeScriptFile = Resource(
|
|
|
268
234
|
* @returns The extracted TypeScript code or error message
|
|
269
235
|
*/
|
|
270
236
|
async function extractTypeScriptCode(
|
|
271
|
-
text: string
|
|
237
|
+
text: string
|
|
272
238
|
): Promise<{ code: string; error?: string }> {
|
|
273
239
|
const tsCodeRegex = /```ts\s*([\s\S]*?)```/g;
|
|
274
240
|
const matches = Array.from(text.matchAll(tsCodeRegex));
|
package/src/ai/vue-file.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { generateText } from "ai";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
import type { Context } from "../context";
|
|
3
|
+
import { StaticVueFile } from "../fs/static-vue-file";
|
|
5
4
|
import { Resource } from "../resource";
|
|
6
5
|
import type { Secret } from "../secret";
|
|
7
|
-
import { ignore } from "../util/ignore";
|
|
8
6
|
import { type ModelConfig, createModel } from "./client";
|
|
9
7
|
|
|
10
8
|
/**
|
|
@@ -149,20 +147,10 @@ export const VueFile = Resource(
|
|
|
149
147
|
async function (
|
|
150
148
|
this: Context<VueFile>,
|
|
151
149
|
id: string,
|
|
152
|
-
props: VueFileProps
|
|
150
|
+
props: VueFileProps
|
|
153
151
|
): Promise<VueFile> {
|
|
154
|
-
//
|
|
155
|
-
await fs.mkdir(path.dirname(props.path), { recursive: true });
|
|
156
|
-
|
|
152
|
+
// Handle deletion phase
|
|
157
153
|
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
154
|
return this.destroy();
|
|
167
155
|
}
|
|
168
156
|
|
|
@@ -201,31 +189,24 @@ export const VueFile = Resource(
|
|
|
201
189
|
|
|
202
190
|
if (retryResult.error) {
|
|
203
191
|
throw new Error(
|
|
204
|
-
`Failed to generate valid Vue code: ${retryResult.error}
|
|
192
|
+
`Failed to generate valid Vue code: ${retryResult.error}`
|
|
205
193
|
);
|
|
206
194
|
}
|
|
207
195
|
|
|
208
196
|
code = retryResult.code;
|
|
209
197
|
}
|
|
210
198
|
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
199
|
+
// Use StaticVueFile to create/update the file
|
|
200
|
+
const file = await StaticVueFile("file", props.path, code);
|
|
220
201
|
|
|
221
202
|
// Return the resource
|
|
222
203
|
return this({
|
|
223
204
|
...props,
|
|
224
|
-
content:
|
|
225
|
-
createdAt:
|
|
226
|
-
updatedAt:
|
|
205
|
+
content: file.content,
|
|
206
|
+
createdAt: Date.now(),
|
|
207
|
+
updatedAt: Date.now(),
|
|
227
208
|
});
|
|
228
|
-
}
|
|
209
|
+
}
|
|
229
210
|
);
|
|
230
211
|
|
|
231
212
|
/**
|
|
@@ -236,7 +217,7 @@ export const VueFile = Resource(
|
|
|
236
217
|
* @returns The extracted Vue code or error message
|
|
237
218
|
*/
|
|
238
219
|
async function extractVueCode(
|
|
239
|
-
text: string
|
|
220
|
+
text: string
|
|
240
221
|
): Promise<{ code: string; error?: string }> {
|
|
241
222
|
const vueCodeRegex = /```vue\s*([\s\S]*?)```/g;
|
|
242
223
|
const matches = Array.from(text.matchAll(vueCodeRegex));
|
package/src/ai/yaml-file.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { generateObject, generateText } from "ai";
|
|
2
2
|
import type { JsonSchema, Type, type } from "arktype";
|
|
3
|
-
import fs from "node:fs/promises";
|
|
4
|
-
import path from "node:path";
|
|
5
3
|
import type { Context } from "../context";
|
|
4
|
+
import { StaticYamlFile } from "../fs/static-yaml-file";
|
|
6
5
|
import { Resource } from "../resource";
|
|
7
6
|
import type { Secret } from "../secret";
|
|
8
|
-
import { ignore } from "../util/ignore";
|
|
9
7
|
import { ark } from "./ark";
|
|
10
8
|
import { type ModelConfig, createModel } from "./client";
|
|
11
9
|
|
|
@@ -200,32 +198,11 @@ export const YAMLFile = Resource("ai::YAMLFile", async function <
|
|
|
200
198
|
>(this: Context<YAMLFile<T extends Type<any, any> ? type.infer<T> : any>>, id: string, props: YAMLFileProps<T>): Promise<
|
|
201
199
|
YAMLFile<T extends Type<any, any> ? type.infer<T> : any>
|
|
202
200
|
> {
|
|
203
|
-
//
|
|
204
|
-
await fs.mkdir(path.dirname(props.path), { recursive: true });
|
|
205
|
-
|
|
201
|
+
// Handle deletion phase
|
|
206
202
|
if (this.phase === "delete") {
|
|
207
|
-
try {
|
|
208
|
-
await fs.unlink(props.path);
|
|
209
|
-
} catch (error: any) {
|
|
210
|
-
// Ignore if file doesn't exist
|
|
211
|
-
if (error.code !== "ENOENT") {
|
|
212
|
-
throw error;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
203
|
return this.destroy();
|
|
216
204
|
}
|
|
217
205
|
|
|
218
|
-
// Dynamic import js-yaml to avoid dependency issues
|
|
219
|
-
// This allows the code to work even if js-yaml is not installed
|
|
220
|
-
let yaml: typeof import("yaml");
|
|
221
|
-
try {
|
|
222
|
-
yaml = await import("yaml");
|
|
223
|
-
} catch (err) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
"The 'yaml' package is required for the YAMLFile resource. Please install it with: bun add yaml",
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
206
|
let yamlContent: string;
|
|
230
207
|
let yamlObject: any;
|
|
231
208
|
|
|
@@ -247,16 +224,9 @@ export const YAMLFile = Resource("ai::YAMLFile", async function <
|
|
|
247
224
|
|
|
248
225
|
yamlObject = object;
|
|
249
226
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
indent: 2,
|
|
254
|
-
});
|
|
255
|
-
} catch (error) {
|
|
256
|
-
throw new Error(
|
|
257
|
-
`Failed to convert object to YAML: ${(error as Error).message}`,
|
|
258
|
-
);
|
|
259
|
-
}
|
|
227
|
+
// Let StaticYamlFile handle the YAML conversion
|
|
228
|
+
const file = await StaticYamlFile("file", props.path, yamlObject);
|
|
229
|
+
yamlContent = file.content;
|
|
260
230
|
} else {
|
|
261
231
|
// Use fence-based extraction
|
|
262
232
|
// Use provided system prompt or default
|
|
@@ -301,32 +271,22 @@ export const YAMLFile = Resource("ai::YAMLFile", async function <
|
|
|
301
271
|
|
|
302
272
|
yamlContent = content;
|
|
303
273
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
yamlObject = yaml.parse(yamlContent);
|
|
307
|
-
} catch (error) {
|
|
308
|
-
throw new Error(`Failed to parse YAML: ${(error as Error).message}`);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
274
|
+
// Create the file with the string content
|
|
275
|
+
const file = await StaticYamlFile("file", props.path, yamlContent);
|
|
311
276
|
|
|
312
|
-
|
|
313
|
-
await
|
|
277
|
+
// We need to parse the YAML to get the object representation
|
|
278
|
+
const yaml = await import("yaml");
|
|
279
|
+
yamlObject = yaml.parse(yamlContent);
|
|
314
280
|
}
|
|
315
281
|
|
|
316
|
-
// Write content to file
|
|
317
|
-
await fs.writeFile(props.path, yamlContent);
|
|
318
|
-
|
|
319
|
-
// Get file stats for timestamps
|
|
320
|
-
const stats = await fs.stat(props.path);
|
|
321
|
-
|
|
322
282
|
// Return the resource
|
|
323
283
|
return this({
|
|
324
284
|
...props,
|
|
325
285
|
schema: props.schema,
|
|
326
286
|
content: yamlContent,
|
|
327
287
|
yaml: yamlObject,
|
|
328
|
-
createdAt:
|
|
329
|
-
updatedAt:
|
|
288
|
+
createdAt: Date.now(),
|
|
289
|
+
updatedAt: Date.now(),
|
|
330
290
|
});
|
|
331
291
|
});
|
|
332
292
|
|
|
@@ -338,7 +298,7 @@ export const YAMLFile = Resource("ai::YAMLFile", async function <
|
|
|
338
298
|
* @returns The extracted YAML or error message
|
|
339
299
|
*/
|
|
340
300
|
async function extractYAMLContent(
|
|
341
|
-
text: string
|
|
301
|
+
text: string
|
|
342
302
|
): Promise<{ content: string; error?: string }> {
|
|
343
303
|
// Check for yaml or yml fence blocks
|
|
344
304
|
const yamlCodeRegex = /```(yaml|yml)\s*([\s\S]*?)```/g;
|
package/src/alchemy.ts
CHANGED
|
@@ -111,6 +111,16 @@ function _alchemy(
|
|
|
111
111
|
values.map(async function resolve(value): Promise<string> {
|
|
112
112
|
if (typeof value === "string") {
|
|
113
113
|
return indent + value;
|
|
114
|
+
} else if (value === null) {
|
|
115
|
+
return "null";
|
|
116
|
+
} else if (value === undefined) {
|
|
117
|
+
return "undefined";
|
|
118
|
+
} else if (
|
|
119
|
+
typeof value === "number" ||
|
|
120
|
+
typeof value === "boolean" ||
|
|
121
|
+
typeof value === "bigint"
|
|
122
|
+
) {
|
|
123
|
+
return value.toString();
|
|
114
124
|
} else if (value instanceof Promise) {
|
|
115
125
|
return resolve(await value);
|
|
116
126
|
} else if (isFileRef(value)) {
|
|
@@ -128,14 +138,15 @@ function _alchemy(
|
|
|
128
138
|
} else if (Array.isArray(value)) {
|
|
129
139
|
return (
|
|
130
140
|
await Promise.all(
|
|
131
|
-
value.map(async (value, i) => `${i}. ${await resolve(value)}`)
|
|
141
|
+
value.map(async (value, i) => `${i}. ${await resolve(value)}`)
|
|
132
142
|
)
|
|
133
143
|
).join("\n");
|
|
134
144
|
} else {
|
|
135
145
|
// TODO: support other types
|
|
136
|
-
|
|
146
|
+
console.log(value);
|
|
147
|
+
throw new Error(`Unsupported value type: ${value}`);
|
|
137
148
|
}
|
|
138
|
-
})
|
|
149
|
+
})
|
|
139
150
|
);
|
|
140
151
|
|
|
141
152
|
// Construct the string template by joining template parts with interpolated values
|
|
@@ -144,12 +155,12 @@ function _alchemy(
|
|
|
144
155
|
part
|
|
145
156
|
.split("\n")
|
|
146
157
|
.map((line) =>
|
|
147
|
-
line.startsWith(indent) ? line.slice(indent.length) : line
|
|
158
|
+
line.startsWith(indent) ? line.slice(indent.length) : line
|
|
148
159
|
)
|
|
149
|
-
.join("\n")
|
|
160
|
+
.join("\n")
|
|
150
161
|
)
|
|
151
162
|
.flatMap((part, i) =>
|
|
152
|
-
i < stringValues.length ? [part, stringValues[i] ?? ""] : [part]
|
|
163
|
+
i < stringValues.length ? [part, stringValues[i] ?? ""] : [part]
|
|
153
164
|
)
|
|
154
165
|
.join("")
|
|
155
166
|
.split("\n");
|
|
@@ -240,7 +251,7 @@ export interface AlchemyOptions {
|
|
|
240
251
|
*/
|
|
241
252
|
function scope(
|
|
242
253
|
id: string | undefined,
|
|
243
|
-
options?: AlchemyOptions
|
|
254
|
+
options?: AlchemyOptions
|
|
244
255
|
// TODO: maybe we want to allow using _ = await alchemy.scope(import.meta)
|
|
245
256
|
// | [meta: ImportMeta]
|
|
246
257
|
): Scope {
|
package/src/apply.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface ApplyOptions {
|
|
|
19
19
|
export async function apply<Out extends Resource>(
|
|
20
20
|
resource: PendingResource<Out>,
|
|
21
21
|
props: ResourceProps | undefined,
|
|
22
|
-
options?: ApplyOptions
|
|
22
|
+
options?: ApplyOptions
|
|
23
23
|
): Promise<Awaited<Out>> {
|
|
24
24
|
const scope = resource.Scope;
|
|
25
25
|
try {
|
|
@@ -64,18 +64,18 @@ export async function apply<Out extends Resource>(
|
|
|
64
64
|
"@secret": value.unencrypted,
|
|
65
65
|
}
|
|
66
66
|
: value,
|
|
67
|
-
2
|
|
67
|
+
2
|
|
68
68
|
);
|
|
69
69
|
const newProps = JSON.stringify(
|
|
70
70
|
await serialize(scope, props, {
|
|
71
71
|
encrypt: false,
|
|
72
72
|
}),
|
|
73
73
|
null,
|
|
74
|
-
2
|
|
74
|
+
2
|
|
75
75
|
);
|
|
76
76
|
if (oldProps === newProps && alwaysUpdate !== true) {
|
|
77
77
|
if (!quiet) {
|
|
78
|
-
console.log(`Skip: "${resource.FQN}" (no changes)`);
|
|
78
|
+
// console.log(`Skip: "${resource.FQN}" (no changes)`);
|
|
79
79
|
}
|
|
80
80
|
return state.output as Awaited<Out>;
|
|
81
81
|
}
|
|
@@ -88,7 +88,7 @@ export async function apply<Out extends Resource>(
|
|
|
88
88
|
|
|
89
89
|
if (!quiet) {
|
|
90
90
|
console.log(
|
|
91
|
-
`${phase === "create" ? "Create" : "Update"}: "${resource.FQN}"
|
|
91
|
+
`${phase === "create" ? "Create" : "Update"}: "${resource.FQN}"`
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
94
|
|
|
@@ -108,7 +108,7 @@ export async function apply<Out extends Resource>(
|
|
|
108
108
|
replace: () => {
|
|
109
109
|
if (isReplaced) {
|
|
110
110
|
console.warn(
|
|
111
|
-
`Resource ${resource.Kind} ${resource.FQN} is already marked as REPLACE
|
|
111
|
+
`Resource ${resource.Kind} ${resource.FQN} is already marked as REPLACE`
|
|
112
112
|
);
|
|
113
113
|
return;
|
|
114
114
|
}
|
|
@@ -117,11 +117,11 @@ export async function apply<Out extends Resource>(
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
const output = await alchemy.run(resource.ID, async () =>
|
|
120
|
-
provider.handler.bind(ctx)(resource.ID, props)
|
|
120
|
+
provider.handler.bind(ctx)(resource.ID, props)
|
|
121
121
|
);
|
|
122
122
|
if (!quiet) {
|
|
123
123
|
console.log(
|
|
124
|
-
`${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"
|
|
124
|
+
`${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"`
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -17,10 +17,10 @@ export async function generateAssetManifest(
|
|
|
17
17
|
cacheControl: "max-age=0,no-cache,no-store,must-revalidate",
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
|
-
files: ["**/*.js", "**/*.css"],
|
|
20
|
+
files: ["**/*.js", "**/*.css", "**/*.png", "**/*.webp"],
|
|
21
21
|
cacheControl: "max-age=31536000,public,immutable",
|
|
22
22
|
},
|
|
23
|
-
]
|
|
23
|
+
]
|
|
24
24
|
): Promise<AssetManifest> {
|
|
25
25
|
// First, collect all file info based on fileOptions (reversed to prioritize later rules)
|
|
26
26
|
const fileInfos: Array<{
|
|
@@ -55,7 +55,7 @@ export async function generateAssetManifest(
|
|
|
55
55
|
: (fileOption.ignore ?? [])),
|
|
56
56
|
],
|
|
57
57
|
});
|
|
58
|
-
})
|
|
58
|
+
})
|
|
59
59
|
)
|
|
60
60
|
)
|
|
61
61
|
.flat()
|
|
@@ -86,7 +86,7 @@ export async function generateAssetManifest(
|
|
|
86
86
|
cacheControl,
|
|
87
87
|
contentType: contentType ?? getContentType(file),
|
|
88
88
|
} satisfies AssetManifestEntry;
|
|
89
|
-
})
|
|
89
|
+
})
|
|
90
90
|
);
|
|
91
91
|
|
|
92
92
|
return manifestEntries;
|