alchemy 0.3.0 → 0.4.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 (154) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/astro-file.js +7 -24
  3. package/lib/ai/client.d.ts +7 -0
  4. package/lib/ai/client.js +45 -0
  5. package/lib/ai/css-file.js +7 -24
  6. package/lib/ai/data.d.ts +26 -21
  7. package/lib/ai/data.js +33 -22
  8. package/lib/ai/document.d.ts +76 -5
  9. package/lib/ai/document.js +112 -56
  10. package/lib/ai/html-file.js +7 -24
  11. package/lib/ai/json-file.js +11 -32
  12. package/lib/ai/typescript-file.d.ts +1 -2
  13. package/lib/ai/typescript-file.js +8 -39
  14. package/lib/ai/vue-file.js +7 -24
  15. package/lib/ai/yaml-file.js +12 -48
  16. package/lib/alchemy.d.ts +5 -0
  17. package/lib/alchemy.js +61 -3
  18. package/lib/apply.js +18 -11
  19. package/lib/aws/account-id.d.ts +4 -1
  20. package/lib/aws/account-id.js +1 -1
  21. package/lib/aws/credentials.d.ts +5 -0
  22. package/lib/aws/credentials.js +0 -0
  23. package/lib/aws/oidc/oidc-provider.js +2 -2
  24. package/lib/aws/role.js +35 -23
  25. package/lib/cloudflare/account-api-token.d.ts +168 -0
  26. package/lib/cloudflare/account-api-token.js +179 -0
  27. package/lib/cloudflare/account-id.d.ts +1 -0
  28. package/lib/cloudflare/account-id.js +0 -0
  29. package/lib/cloudflare/api.d.ts +38 -1
  30. package/lib/cloudflare/api.js +55 -4
  31. package/lib/cloudflare/bucket.d.ts +118 -3
  32. package/lib/cloudflare/bucket.js +269 -89
  33. package/lib/cloudflare/custom-domain.d.ts +75 -0
  34. package/lib/cloudflare/custom-domain.js +154 -0
  35. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  36. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  37. package/lib/cloudflare/index.d.ts +5 -1
  38. package/lib/cloudflare/index.js +5 -1
  39. package/lib/cloudflare/permission-groups.d.ts +78 -0
  40. package/lib/cloudflare/permission-groups.js +48 -0
  41. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  42. package/lib/cloudflare/r2-rest-state-store.js +3 -2
  43. package/lib/cloudflare/static-site-router.js +16 -7
  44. package/lib/cloudflare/static-site.d.ts +17 -18
  45. package/lib/cloudflare/static-site.js +19 -7
  46. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  47. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  48. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  49. package/lib/fs/copy-file.d.ts +54 -0
  50. package/lib/fs/copy-file.js +63 -0
  51. package/lib/fs/file-system-state-store.js +1 -1
  52. package/lib/fs/file.d.ts +31 -0
  53. package/lib/fs/file.js +30 -5
  54. package/lib/fs/index.d.ts +6 -0
  55. package/lib/fs/index.js +6 -0
  56. package/lib/fs/static-astro-file.d.ts +34 -0
  57. package/lib/fs/static-astro-file.js +39 -0
  58. package/lib/fs/static-css-file.d.ts +24 -0
  59. package/lib/fs/static-css-file.js +29 -0
  60. package/lib/fs/static-html-file.d.ts +31 -0
  61. package/lib/fs/static-html-file.js +36 -0
  62. package/lib/fs/static-json-file.d.ts +2 -2
  63. package/lib/fs/static-json-file.js +3 -2
  64. package/lib/fs/static-text-file.d.ts +1 -1
  65. package/lib/fs/static-text-file.js +3 -2
  66. package/lib/fs/static-typescript-file.d.ts +2 -2
  67. package/lib/fs/static-typescript-file.js +3 -2
  68. package/lib/fs/static-vue-file.d.ts +28 -0
  69. package/lib/fs/static-vue-file.js +33 -0
  70. package/lib/fs/static-yaml-file.d.ts +11 -8
  71. package/lib/fs/static-yaml-file.js +3 -2
  72. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +14 -4
  73. package/lib/internal/docs/providers.js +196 -0
  74. package/lib/secret.d.ts +3 -0
  75. package/lib/secret.js +13 -0
  76. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  77. package/lib/{util/serde.js → serde.js} +12 -4
  78. package/lib/test/bun.js +3 -2
  79. package/lib/util/sha256.d.ts +1 -0
  80. package/lib/util/sha256.js +4 -0
  81. package/lib/web/vitepress/config.d.ts +11 -0
  82. package/lib/web/vitepress/config.js +32 -0
  83. package/lib/web/vitepress/home-page.d.ts +5 -3
  84. package/lib/web/vitepress/home-page.js +48 -35
  85. package/lib/web/vitepress/index.d.ts +2 -0
  86. package/lib/web/vitepress/index.js +2 -0
  87. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  88. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  89. package/lib/web/vitepress/vitepress.d.ts +5 -4
  90. package/lib/web/vitepress/vitepress.js +20 -36
  91. package/package.json +8 -4
  92. package/src/ai/astro-file.ts +11 -30
  93. package/src/ai/client.ts +59 -0
  94. package/src/ai/css-file.ts +11 -30
  95. package/src/ai/data.ts +50 -25
  96. package/src/ai/document.ts +171 -66
  97. package/src/ai/html-file.ts +11 -30
  98. package/src/ai/json-file.ts +13 -37
  99. package/src/ai/typescript-file.ts +14 -48
  100. package/src/ai/vue-file.ts +11 -30
  101. package/src/ai/yaml-file.ts +13 -53
  102. package/src/alchemy.ts +74 -9
  103. package/src/apply.ts +27 -26
  104. package/src/aws/account-id.ts +6 -2
  105. package/src/aws/credentials.ts +6 -0
  106. package/src/aws/oidc/oidc-provider.ts +17 -17
  107. package/src/aws/role.ts +68 -54
  108. package/src/cloudflare/account-api-token.ts +365 -0
  109. package/src/cloudflare/account-id.ts +0 -0
  110. package/src/cloudflare/api.ts +88 -17
  111. package/src/cloudflare/bucket.ts +493 -133
  112. package/src/cloudflare/custom-domain.ts +318 -0
  113. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  114. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  115. package/src/cloudflare/index.ts +5 -1
  116. package/src/cloudflare/permission-groups.ts +137 -0
  117. package/src/cloudflare/r2-rest-state-store.ts +14 -13
  118. package/src/cloudflare/static-site-router.ts +18 -8
  119. package/src/cloudflare/static-site.ts +27 -35
  120. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  121. package/src/cloudflare/worker.ts +44 -43
  122. package/src/dns/import-dns.ts +4 -4
  123. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  124. package/src/fs/copy-file.ts +115 -0
  125. package/src/fs/file-system-state-store.ts +5 -5
  126. package/src/fs/file.ts +64 -9
  127. package/src/fs/index.ts +6 -0
  128. package/src/fs/static-astro-file.ts +45 -0
  129. package/src/fs/static-css-file.ts +35 -0
  130. package/src/fs/static-html-file.ts +42 -0
  131. package/src/fs/static-json-file.ts +4 -3
  132. package/src/fs/static-text-file.ts +5 -2
  133. package/src/fs/static-typescript-file.ts +4 -3
  134. package/src/fs/static-vue-file.ts +39 -0
  135. package/src/fs/static-yaml-file.ts +13 -9
  136. package/src/internal/docs/providers.ts +281 -0
  137. package/src/secret.ts +16 -0
  138. package/src/{util/serde.ts → serde.ts} +16 -10
  139. package/src/test/bun.ts +8 -7
  140. package/src/util/sha256.ts +5 -0
  141. package/src/web/vitepress/config.ts +50 -0
  142. package/src/web/vitepress/home-page.ts +54 -42
  143. package/src/web/vitepress/index.ts +2 -0
  144. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  145. package/src/web/vitepress/vitepress.ts +34 -50
  146. package/lib/internal/getting-started.d.ts +0 -10
  147. package/lib/internal/getting-started.js +0 -94
  148. package/lib/internal/project.d.ts +0 -13
  149. package/lib/internal/project.js +0 -101
  150. package/lib/internal/providers.js +0 -148
  151. package/src/internal/getting-started.ts +0 -107
  152. package/src/internal/project.ts +0 -119
  153. package/src/internal/providers.ts +0 -209
  154. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
@@ -1,9 +1,7 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import { StaticTextFile } from "../fs/static-text-file";
4
3
  import { Resource } from "../resource";
5
- import { ignore } from "../util/ignore";
6
- import { createModel } from "./client";
4
+ import { createModel, withRateLimitRetry } from "./client";
7
5
  /**
8
6
  * Default system prompt for markdown document generation
9
7
  */
@@ -13,7 +11,22 @@ const DEFAULT_MD_SYSTEM_PROMPT = "You are a technical documentation writer. Crea
13
11
  * Supports powerful context handling through the alchemy template literal tag.
14
12
  *
15
13
  * @example
16
- * // Create a document using alchemy template literals for context
14
+ * // Create an in-memory document (no file created)
15
+ * const apiDocs = await Document("api-docs", {
16
+ * title: "API Documentation",
17
+ * prompt: await alchemy`
18
+ * Generate API documentation based on these source files:
19
+ * ${alchemy.file("src/api.ts")}
20
+ * ${alchemy.file("src/types.ts")}
21
+ * `,
22
+ * model: {
23
+ * id: "gpt-4o",
24
+ * provider: "openai"
25
+ * }
26
+ * });
27
+ *
28
+ * @example
29
+ * // Create a document and write it to disk
17
30
  * const apiDocs = await Document("api-docs", {
18
31
  * title: "API Documentation",
19
32
  * path: "./docs/api.md",
@@ -29,6 +42,23 @@ const DEFAULT_MD_SYSTEM_PROMPT = "You are a technical documentation writer. Crea
29
42
  * });
30
43
  *
31
44
  * @example
45
+ * // Use message history for iterative document generation
46
+ * const apiDocs = await Document("api-docs", {
47
+ * title: "API Documentation",
48
+ * path: "./docs/api.md",
49
+ * messages: [
50
+ * { role: "user", content: "Create API documentation for these files" },
51
+ * { role: "assistant", content: "I'll help you create API documentation. Please provide the files." },
52
+ * { role: "user", content: "Here are the files: [file contents]" }
53
+ * ],
54
+ * system: "You are a technical documentation writer. Generate clear and concise API documentation.",
55
+ * model: {
56
+ * id: "gpt-4o",
57
+ * provider: "openai"
58
+ * }
59
+ * });
60
+ *
61
+ * @example
32
62
  * // Use alchemy template literals with file collections and temperature control
33
63
  * const modelDocs = await Document("models", {
34
64
  * title: "Data Models",
@@ -61,67 +91,84 @@ const DEFAULT_MD_SYSTEM_PROMPT = "You are a technical documentation writer. Crea
61
91
  * });
62
92
  */
63
93
  export const Document = Resource("docs::Document", async function (id, props) {
64
- // Ensure directory exists
65
- await fs.mkdir(path.dirname(props.path), { recursive: true });
94
+ // Validate that either prompt or messages are provided
95
+ if (!props.prompt && !props.messages) {
96
+ throw new Error("Either prompt or messages must be provided");
97
+ }
98
+ // Handle deletion phase
66
99
  if (this.phase === "delete") {
67
- try {
68
- await fs.unlink(props.path);
69
- }
70
- catch (error) {
71
- // Ignore if file doesn't exist
72
- if (error.code !== "ENOENT") {
73
- throw error;
74
- }
75
- }
76
100
  return this.destroy();
77
101
  }
102
+ if (this.phase === "update" && props.freeze) {
103
+ if (props.path) {
104
+ const filePath = props.path;
105
+ const fileId = `${id}-file`;
106
+ await StaticTextFile(fileId, filePath, this.output.content);
107
+ }
108
+ return this(this.output);
109
+ }
78
110
  // Use provided system prompt or default
79
111
  const system = props.system || DEFAULT_MD_SYSTEM_PROMPT;
80
- // Generate initial content
81
- const { text } = await generateText({
82
- model: createModel(props),
83
- prompt: props.prompt,
84
- system,
85
- providerOptions: props.model?.options,
86
- ...(props.temperature === undefined
87
- ? {}
88
- : // some models error if you provide it (rather than ignoring it)
89
- { temperature: props.temperature }),
112
+ // Generate initial content with rate limit retry
113
+ const { text } = await withRateLimitRetry(async () => {
114
+ return generateText({
115
+ model: createModel(props),
116
+ ...(props.messages
117
+ ? { messages: props.messages }
118
+ : { prompt: props.prompt }),
119
+ system,
120
+ maxTokens: props.maxTokens || 8192,
121
+ providerOptions: props.model?.options,
122
+ ...(props.temperature === undefined
123
+ ? {}
124
+ : // some models error if you provide it (rather than ignoring it)
125
+ { temperature: props.temperature }),
126
+ });
90
127
  });
91
128
  // Extract and validate markdown content
92
- let { content, error } = await extractMarkdownContent(text);
129
+ let { content, error } = extractMarkdownContent(text);
93
130
  // Re-prompt if there are validation errors
94
131
  if (error) {
95
132
  const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one markdown document inside \`\`\`md fences.`;
96
- const { text: retryText } = await generateText({
97
- model: createModel(props),
98
- prompt: props.prompt,
99
- system: errorSystem,
100
- providerOptions: props.model?.options,
101
- ...(props.temperature === undefined
102
- ? {}
103
- : { temperature: props.temperature }),
133
+ const { text: retryText } = await withRateLimitRetry(async () => {
134
+ return generateText({
135
+ model: createModel(props),
136
+ ...(props.messages
137
+ ? { messages: props.messages }
138
+ : { prompt: props.prompt }),
139
+ system: errorSystem,
140
+ providerOptions: props.model?.options,
141
+ ...(props.temperature === undefined
142
+ ? {}
143
+ : { temperature: props.temperature }),
144
+ });
104
145
  });
105
- const retryResult = await extractMarkdownContent(retryText);
146
+ const retryResult = extractMarkdownContent(retryText);
106
147
  if (retryResult.error) {
107
- throw new Error(`Failed to generate valid markdown content: ${retryResult.error}`);
148
+ throw new Error(`Failed to generate valid markdown content: ${retryResult.error}\n${retryText}`);
108
149
  }
109
150
  content = retryResult.content;
110
151
  }
111
- if (this.phase === "update" && props.path !== this.props.path) {
112
- await ignore("ENOENT", () => fs.unlink(this.props.path));
152
+ // Create result object
153
+ const result = {
154
+ ...props,
155
+ content,
156
+ messages: [
157
+ ...(props.messages || [{ role: "user", content: props.prompt }]),
158
+ { role: "assistant", content },
159
+ ],
160
+ createdAt: this.output?.createdAt || Date.now(),
161
+ updatedAt: Date.now(),
162
+ title: props.title || id,
163
+ };
164
+ // Write file if path is provided
165
+ if (props.path) {
166
+ const filePath = props.path;
167
+ const fileId = `${id}-file`;
168
+ result.file = await StaticTextFile(fileId, filePath, content);
113
169
  }
114
- // Write content to file
115
- await fs.writeFile(props.path, content);
116
- // Get file stats for timestamps
117
- const stats = await fs.stat(props.path);
118
170
  // Return the resource
119
- return this({
120
- ...props,
121
- content: content,
122
- createdAt: stats.birthtimeMs,
123
- updatedAt: stats.mtimeMs,
124
- });
171
+ return this(result);
125
172
  });
126
173
  /**
127
174
  * Extracts markdown content from between ```md fences
@@ -130,20 +177,29 @@ export const Document = Resource("docs::Document", async function (id, props) {
130
177
  * @param text The text to extract markdown content from
131
178
  * @returns The extracted markdown content or error message
132
179
  */
133
- async function extractMarkdownContent(text) {
134
- const mdCodeRegex = /```md\s*([\s\S]*?)```/g;
135
- const matches = Array.from(text.matchAll(mdCodeRegex));
136
- if (matches.length === 0) {
180
+ function extractMarkdownContent(text) {
181
+ const lines = text.split("\n");
182
+ const startIdx = lines.findIndex((line) => line.trim() === "```md");
183
+ if (startIdx === -1) {
137
184
  return {
138
185
  content: "",
139
186
  error: "No markdown code block found in the response. Please include your markdown content within ```md fences.",
140
187
  };
141
188
  }
142
- if (matches.length > 1) {
189
+ const rest = lines.slice(startIdx + 1);
190
+ const endRelativeIdx = rest
191
+ .map((line) => line.trim() === "```")
192
+ .lastIndexOf(true);
193
+ if (endRelativeIdx === -1) {
143
194
  return {
144
195
  content: "",
145
- error: "Multiple markdown code blocks found in the response. Please provide exactly one markdown block within ```md fences.",
196
+ error: "Markdown block was not closed properly.",
146
197
  };
147
198
  }
148
- return { content: matches[0][1].trim() };
199
+ const endIdx = startIdx + 1 + endRelativeIdx;
200
+ const content = lines
201
+ .slice(startIdx + 1, endIdx)
202
+ .join("\n")
203
+ .trim();
204
+ return { content };
149
205
  }
@@ -1,8 +1,6 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import { StaticHTMLFile } from "../fs/static-html-file";
4
3
  import { Resource } from "../resource";
5
- import { ignore } from "../util/ignore";
6
4
  import { createModel } from "./client";
7
5
  /**
8
6
  * Default system prompt for HTML file generation
@@ -68,18 +66,8 @@ const DEFAULT_HTML_SYSTEM_PROMPT = "You are an HTML code generator. Create HTML
68
66
  * });
69
67
  */
70
68
  export const HTMLFile = Resource("ai::HTMLFile", async function (id, props) {
71
- // Ensure directory exists
72
- await fs.mkdir(path.dirname(props.path), { recursive: true });
69
+ // Handle deletion phase
73
70
  if (this.phase === "delete") {
74
- try {
75
- await fs.unlink(props.path);
76
- }
77
- catch (error) {
78
- // Ignore if file doesn't exist
79
- if (error.code !== "ENOENT") {
80
- throw error;
81
- }
82
- }
83
71
  return this.destroy();
84
72
  }
85
73
  // Use provided system prompt or default
@@ -114,19 +102,14 @@ export const HTMLFile = Resource("ai::HTMLFile", async function (id, props) {
114
102
  }
115
103
  code = retryResult.code;
116
104
  }
117
- if (this.phase === "update" && props.path !== this.props.path) {
118
- await ignore("ENOENT", () => fs.unlink(this.props.path));
119
- }
120
- // Write content to file
121
- await fs.writeFile(props.path, code);
122
- // Get file stats for timestamps
123
- const stats = await fs.stat(props.path);
105
+ // Use StaticHTMLFile to create/update the file
106
+ const file = await StaticHTMLFile("file", props.path, code);
124
107
  // Return the resource
125
108
  return this({
126
109
  ...props,
127
- content: code,
128
- createdAt: stats.birthtimeMs,
129
- updatedAt: stats.mtimeMs,
110
+ content: file.content,
111
+ createdAt: Date.now(),
112
+ updatedAt: Date.now(),
130
113
  });
131
114
  });
132
115
  /**
@@ -1,8 +1,6 @@
1
1
  import { generateObject, generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import { StaticJsonFile } from "../fs/static-json-file";
4
3
  import { Resource } from "../resource";
5
- import { ignore } from "../util/ignore";
6
4
  import { ark } from "./ark";
7
5
  import { createModel } from "./client";
8
6
  /**
@@ -77,23 +75,10 @@ const DEFAULT_JSON_SYSTEM_PROMPT = "You are a JSON generator. Create valid JSON
77
75
  * });
78
76
  */
79
77
  export const JSONFile = Resource("ai::JSONFile", async function (id, props) {
80
- // Ensure directory exists
81
- await fs.mkdir(path.dirname(props.path), { recursive: true });
78
+ // Handle deletion phase
82
79
  if (this.phase === "delete") {
83
- try {
84
- await fs.unlink(props.path);
85
- }
86
- catch (error) {
87
- // Ignore if file doesn't exist
88
- if (error.code !== "ENOENT") {
89
- throw error;
90
- }
91
- }
92
80
  return this.destroy();
93
81
  }
94
- // Determine if we should use pretty printing
95
- const pretty = props.pretty !== false;
96
- const indent = props.indent ?? 2;
97
82
  let jsonContent;
98
83
  let jsonObject;
99
84
  // Check if schema is provided
@@ -111,9 +96,9 @@ export const JSONFile = Resource("ai::JSONFile", async function (id, props) {
111
96
  : { temperature: props.temperature }),
112
97
  });
113
98
  jsonObject = object;
114
- jsonContent = pretty
115
- ? JSON.stringify(jsonObject, null, indent)
116
- : JSON.stringify(jsonObject);
99
+ // Use StaticJsonFile to create the file
100
+ const file = await StaticJsonFile("file", props.path, jsonObject);
101
+ jsonContent = file.content;
117
102
  }
118
103
  else {
119
104
  // Use fence-based extraction
@@ -149,26 +134,20 @@ export const JSONFile = Resource("ai::JSONFile", async function (id, props) {
149
134
  }
150
135
  content = retryResult.content;
151
136
  }
152
- // Parse JSON
137
+ // Parse JSON to get the object representation
153
138
  jsonObject = JSON.parse(content);
154
- // Format with or without indentation based on pretty option
155
- jsonContent = pretty ? JSON.stringify(jsonObject, null, indent) : content;
156
- }
157
- if (this.phase === "update" && props.path !== this.props.path) {
158
- await ignore("ENOENT", () => fs.unlink(this.props.path));
139
+ // Use StaticJsonFile to create the file
140
+ const file = await StaticJsonFile("file", props.path, jsonObject);
141
+ jsonContent = file.content;
159
142
  }
160
- // Write content to file
161
- await fs.writeFile(props.path, jsonContent);
162
- // Get file stats for timestamps
163
- const stats = await fs.stat(props.path);
164
143
  // Return the resource
165
144
  return this({
166
145
  ...props,
167
146
  schema: props.schema,
168
147
  content: jsonContent,
169
148
  json: jsonObject,
170
- createdAt: stats.birthtimeMs,
171
- updatedAt: stats.mtimeMs,
149
+ createdAt: Date.now(),
150
+ updatedAt: Date.now(),
172
151
  });
173
152
  });
174
153
  /**
@@ -1,4 +1,3 @@
1
- import prettier from "prettier";
2
1
  import type { Context } from "../context";
3
2
  import { Resource } from "../resource";
4
3
  import type { Secret } from "../secret";
@@ -53,7 +52,7 @@ export interface TypeScriptFileProps {
53
52
  * Prettier configuration to use for formatting the TypeScript code
54
53
  * If not provided, will use the default Prettier configuration
55
54
  */
56
- prettierConfig?: prettier.Options;
55
+ prettierConfig?: object;
57
56
  }
58
57
  /**
59
58
  * A TypeScript file that can be created, updated, and deleted
@@ -1,9 +1,6 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
- import prettier from "prettier";
2
+ import { StaticTypeScriptFile } from "../fs/static-typescript-file";
5
3
  import { Resource } from "../resource";
6
- import { ignore } from "../util/ignore";
7
4
  import { createModel } from "./client";
8
5
  /**
9
6
  * Default system prompt for TypeScript file generation
@@ -75,18 +72,9 @@ const DEFAULT_TS_SYSTEM_PROMPT = "You are a TypeScript code generator. Create Ty
75
72
  * });
76
73
  */
77
74
  export const TypeScriptFile = Resource("ai::TypeScriptFile", async function (id, props) {
78
- // Ensure directory exists
79
- await fs.mkdir(path.dirname(props.path), { recursive: true });
75
+ // Handle delete phase
80
76
  if (this.phase === "delete") {
81
- try {
82
- await fs.unlink(props.path);
83
- }
84
- catch (error) {
85
- // Ignore if file doesn't exist
86
- if (error.code !== "ENOENT") {
87
- throw error;
88
- }
89
- }
77
+ // StaticTypeScriptFile will handle the deletion
90
78
  return this.destroy();
91
79
  }
92
80
  // Use provided system prompt or default
@@ -121,33 +109,14 @@ export const TypeScriptFile = Resource("ai::TypeScriptFile", async function (id,
121
109
  }
122
110
  code = retryResult.code;
123
111
  }
124
- // Format the code with Prettier
125
- try {
126
- // Set default parser to typescript
127
- const prettierOptions = {
128
- parser: "typescript",
129
- ...props.prettierConfig,
130
- };
131
- // Format the code
132
- code = await prettier.format(code, prettierOptions);
133
- }
134
- catch (error) {
135
- // If Prettier formatting fails, just use the unformatted code
136
- console.warn("Failed to format TypeScript code with Prettier:", error);
137
- }
138
- if (this.phase === "update" && props.path !== this.props.path) {
139
- await ignore("ENOENT", () => fs.unlink(this.props.path));
140
- }
141
- // Write content to file
142
- await fs.writeFile(props.path, code);
143
- // Get file stats for timestamps
144
- const stats = await fs.stat(props.path);
112
+ // Use StaticTypeScriptFile to create/update the file
113
+ const file = await StaticTypeScriptFile("file", props.path, code);
145
114
  // Return the resource
146
115
  return this({
147
116
  ...props,
148
- content: code,
149
- createdAt: stats.birthtimeMs,
150
- updatedAt: stats.mtimeMs,
117
+ content: file.content,
118
+ createdAt: Date.now(),
119
+ updatedAt: Date.now(),
151
120
  });
152
121
  });
153
122
  /**
@@ -1,8 +1,6 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import { StaticVueFile } from "../fs/static-vue-file";
4
3
  import { Resource } from "../resource";
5
- import { ignore } from "../util/ignore";
6
4
  import { createModel } from "./client";
7
5
  /**
8
6
  * Default system prompt for Vue file generation
@@ -66,18 +64,8 @@ const DEFAULT_VUE_SYSTEM_PROMPT = "You are a Vue component generator. Create a s
66
64
  * });
67
65
  */
68
66
  export const VueFile = Resource("ai::VueFile", async function (id, props) {
69
- // Ensure directory exists
70
- await fs.mkdir(path.dirname(props.path), { recursive: true });
67
+ // Handle deletion phase
71
68
  if (this.phase === "delete") {
72
- try {
73
- await fs.unlink(props.path);
74
- }
75
- catch (error) {
76
- // Ignore if file doesn't exist
77
- if (error.code !== "ENOENT") {
78
- throw error;
79
- }
80
- }
81
69
  return this.destroy();
82
70
  }
83
71
  // Use provided system prompt or default
@@ -112,19 +100,14 @@ export const VueFile = Resource("ai::VueFile", async function (id, props) {
112
100
  }
113
101
  code = retryResult.code;
114
102
  }
115
- if (this.phase === "update" && props.path !== this.props.path) {
116
- await ignore("ENOENT", () => fs.unlink(this.props.path));
117
- }
118
- // Write content to file
119
- await fs.writeFile(props.path, code);
120
- // Get file stats for timestamps
121
- const stats = await fs.stat(props.path);
103
+ // Use StaticVueFile to create/update the file
104
+ const file = await StaticVueFile("file", props.path, code);
122
105
  // Return the resource
123
106
  return this({
124
107
  ...props,
125
- content: code,
126
- createdAt: stats.birthtimeMs,
127
- updatedAt: stats.mtimeMs,
108
+ content: file.content,
109
+ createdAt: Date.now(),
110
+ updatedAt: Date.now(),
128
111
  });
129
112
  });
130
113
  /**
@@ -1,8 +1,6 @@
1
1
  import { generateObject, generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
2
+ import { StaticYamlFile } from "../fs/static-yaml-file";
4
3
  import { Resource } from "../resource";
5
- import { ignore } from "../util/ignore";
6
4
  import { ark } from "./ark";
7
5
  import { createModel } from "./client";
8
6
  /**
@@ -96,29 +94,10 @@ const DEFAULT_YAML_SYSTEM_PROMPT = "You are a YAML generator. Create valid YAML
96
94
  * });
97
95
  */
98
96
  export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
99
- // Ensure directory exists
100
- await fs.mkdir(path.dirname(props.path), { recursive: true });
97
+ // Handle deletion phase
101
98
  if (this.phase === "delete") {
102
- try {
103
- await fs.unlink(props.path);
104
- }
105
- catch (error) {
106
- // Ignore if file doesn't exist
107
- if (error.code !== "ENOENT") {
108
- throw error;
109
- }
110
- }
111
99
  return this.destroy();
112
100
  }
113
- // Dynamic import js-yaml to avoid dependency issues
114
- // This allows the code to work even if js-yaml is not installed
115
- let yaml;
116
- try {
117
- yaml = await import("yaml");
118
- }
119
- catch (err) {
120
- throw new Error("The 'yaml' package is required for the YAMLFile resource. Please install it with: bun add yaml");
121
- }
122
101
  let yamlContent;
123
102
  let yamlObject;
124
103
  // Check if schema is provided
@@ -136,15 +115,9 @@ export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
136
115
  : { temperature: props.temperature }),
137
116
  });
138
117
  yamlObject = object;
139
- // Convert object to YAML
140
- try {
141
- yamlContent = yaml.stringify(yamlObject, {
142
- indent: 2,
143
- });
144
- }
145
- catch (error) {
146
- throw new Error(`Failed to convert object to YAML: ${error.message}`);
147
- }
118
+ // Let StaticYamlFile handle the YAML conversion
119
+ const file = await StaticYamlFile("file", props.path, yamlObject);
120
+ yamlContent = file.content;
148
121
  }
149
122
  else {
150
123
  // Use fence-based extraction
@@ -181,29 +154,20 @@ export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
181
154
  content = retryResult.content;
182
155
  }
183
156
  yamlContent = content;
184
- // Parse YAML to validate and get object representation
185
- try {
186
- yamlObject = yaml.parse(yamlContent);
187
- }
188
- catch (error) {
189
- throw new Error(`Failed to parse YAML: ${error.message}`);
190
- }
191
- }
192
- if (this.phase === "update" && props.path !== this.props.path) {
193
- await ignore("ENOENT", () => fs.unlink(this.props.path));
157
+ // Create the file with the string content
158
+ const file = await StaticYamlFile("file", props.path, yamlContent);
159
+ // We need to parse the YAML to get the object representation
160
+ const yaml = await import("yaml");
161
+ yamlObject = yaml.parse(yamlContent);
194
162
  }
195
- // Write content to file
196
- await fs.writeFile(props.path, yamlContent);
197
- // Get file stats for timestamps
198
- const stats = await fs.stat(props.path);
199
163
  // Return the resource
200
164
  return this({
201
165
  ...props,
202
166
  schema: props.schema,
203
167
  content: yamlContent,
204
168
  yaml: yamlObject,
205
- createdAt: stats.birthtimeMs,
206
- updatedAt: stats.mtimeMs,
169
+ createdAt: Date.now(),
170
+ updatedAt: Date.now(),
207
171
  });
208
172
  });
209
173
  /**
package/lib/alchemy.d.ts CHANGED
@@ -31,6 +31,10 @@ export interface Alchemy {
31
31
  scope: typeof scope;
32
32
  run: typeof run;
33
33
  destroy: typeof destroy;
34
+ /**
35
+ * Get an environment variable and error if it's not set.
36
+ */
37
+ env: typeof env;
34
38
  /**
35
39
  * Creates an encrypted secret that can be safely stored in state files.
36
40
  * Requires a password to be set either globally in the application options
@@ -146,4 +150,5 @@ declare function run<T>(...args: [id: string, fn: (this: Scope, scope: Scope) =>
146
150
  options: AlchemyOptions,
147
151
  fn: (this: Scope, scope: Scope) => Promise<T>
148
152
  ]): Promise<T>;
153
+ export declare function env<T = string>(name: string, value?: T | undefined, error?: string): Promise<T>;
149
154
  export {};