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,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 { StaticJsonFile } from "../fs/static-json-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
 
@@ -193,25 +191,11 @@ export const JSONFile = Resource("ai::JSONFile", async function <
193
191
  >(this: Context<JSONFile<T extends Type<any, any> ? type.infer<T> : any>>, id: string, props: JSONFileProps<T>): Promise<
194
192
  JSONFile<T extends Type<any, any> ? type.infer<T> : any>
195
193
  > {
196
- // Ensure directory exists
197
- await fs.mkdir(path.dirname(props.path), { recursive: true });
198
-
194
+ // Handle deletion phase
199
195
  if (this.phase === "delete") {
200
- try {
201
- await fs.unlink(props.path);
202
- } catch (error: any) {
203
- // Ignore if file doesn't exist
204
- if (error.code !== "ENOENT") {
205
- throw error;
206
- }
207
- }
208
196
  return this.destroy();
209
197
  }
210
198
 
211
- // Determine if we should use pretty printing
212
- const pretty = props.pretty !== false;
213
- const indent = props.indent ?? 2;
214
-
215
199
  let jsonContent: string;
216
200
  let jsonObject: any;
217
201
 
@@ -232,9 +216,10 @@ export const JSONFile = Resource("ai::JSONFile", async function <
232
216
  });
233
217
 
234
218
  jsonObject = object;
235
- jsonContent = pretty
236
- ? JSON.stringify(jsonObject, null, indent)
237
- : JSON.stringify(jsonObject);
219
+
220
+ // Use StaticJsonFile to create the file
221
+ const file = await StaticJsonFile("file", props.path, jsonObject);
222
+ jsonContent = file.content;
238
223
  } else {
239
224
  // Use fence-based extraction
240
225
  // Use provided system prompt or default
@@ -277,31 +262,22 @@ export const JSONFile = Resource("ai::JSONFile", async function <
277
262
  content = retryResult.content;
278
263
  }
279
264
 
280
- // Parse JSON
265
+ // Parse JSON to get the object representation
281
266
  jsonObject = JSON.parse(content);
282
267
 
283
- // Format with or without indentation based on pretty option
284
- jsonContent = pretty ? JSON.stringify(jsonObject, null, indent) : content;
268
+ // Use StaticJsonFile to create the file
269
+ const file = await StaticJsonFile("file", props.path, jsonObject);
270
+ jsonContent = file.content;
285
271
  }
286
272
 
287
- if (this.phase === "update" && props.path !== this.props.path) {
288
- await ignore("ENOENT", () => fs.unlink(this.props.path));
289
- }
290
-
291
- // Write content to file
292
- await fs.writeFile(props.path, jsonContent);
293
-
294
- // Get file stats for timestamps
295
- const stats = await fs.stat(props.path);
296
-
297
273
  // Return the resource
298
274
  return this({
299
275
  ...props,
300
276
  schema: props.schema,
301
277
  content: jsonContent,
302
278
  json: jsonObject,
303
- createdAt: stats.birthtimeMs,
304
- updatedAt: stats.mtimeMs,
279
+ createdAt: Date.now(),
280
+ updatedAt: Date.now(),
305
281
  });
306
282
  });
307
283
 
@@ -313,7 +289,7 @@ export const JSONFile = Resource("ai::JSONFile", async function <
313
289
  * @returns The extracted JSON or error message
314
290
  */
315
291
  async function extractJSONContent(
316
- text: string,
292
+ text: string
317
293
  ): Promise<{ content: string; error?: string }> {
318
294
  const jsonCodeRegex = /```json\s*([\s\S]*?)```/g;
319
295
  const matches = Array.from(text.matchAll(jsonCodeRegex));
@@ -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 { ignore } from "../util/ignore";
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?: prettier.Options;
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
- // Ensure directory exists
172
- await fs.mkdir(path.dirname(props.path), { recursive: true });
173
-
168
+ // Handle delete phase
174
169
  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
- }
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
- // 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);
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: code,
257
- createdAt: stats.birthtimeMs,
258
- updatedAt: stats.mtimeMs,
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));
@@ -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
- // Ensure directory exists
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
- 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);
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: code,
225
- createdAt: stats.birthtimeMs,
226
- updatedAt: stats.mtimeMs,
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));
@@ -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
- // Ensure directory exists
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
- // Convert object to YAML
251
- try {
252
- yamlContent = yaml.stringify(yamlObject, {
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
- // Parse YAML to validate and get object representation
305
- try {
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
- if (this.phase === "update" && props.path !== this.props.path) {
313
- await ignore("ENOENT", () => fs.unlink(this.props.path));
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: stats.birthtimeMs,
329
- updatedAt: stats.mtimeMs,
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
@@ -40,6 +40,12 @@ export interface Alchemy {
40
40
  scope: typeof scope;
41
41
  run: typeof run;
42
42
  destroy: typeof destroy;
43
+
44
+ /**
45
+ * Get an environment variable and error if it's not set.
46
+ */
47
+ env: typeof env;
48
+
43
49
  /**
44
50
  * Creates an encrypted secret that can be safely stored in state files.
45
51
  * Requires a password to be set either globally in the application options
@@ -88,12 +94,18 @@ function _alchemy(
88
94
  | [appName: string, options?: Omit<AlchemyOptions, "appName">]
89
95
  ): any {
90
96
  if (typeof args[0] === "string") {
91
- const [appName, options] = args;
92
- return scope(undefined, {
97
+ const [appName, options] = args as [string, AlchemyOptions?];
98
+ const root = scope(undefined, {
93
99
  ...options,
94
100
  appName,
95
101
  stage: options?.stage,
96
102
  });
103
+ if (options?.phase === "destroy") {
104
+ return destroy(root).finally(() => {
105
+ process.exit(0);
106
+ });
107
+ }
108
+ return root;
97
109
  } else {
98
110
  const [template, ...values] = args;
99
111
  const [, secondLine] = template[0].split("\n");
@@ -111,6 +123,16 @@ function _alchemy(
111
123
  values.map(async function resolve(value): Promise<string> {
112
124
  if (typeof value === "string") {
113
125
  return indent + value;
126
+ } else if (value === null) {
127
+ return "null";
128
+ } else if (value === undefined) {
129
+ return "undefined";
130
+ } else if (
131
+ typeof value === "number" ||
132
+ typeof value === "boolean" ||
133
+ typeof value === "bigint"
134
+ ) {
135
+ return value.toString();
114
136
  } else if (value instanceof Promise) {
115
137
  return resolve(await value);
116
138
  } else if (isFileRef(value)) {
@@ -128,14 +150,34 @@ function _alchemy(
128
150
  } else if (Array.isArray(value)) {
129
151
  return (
130
152
  await Promise.all(
131
- value.map(async (value, i) => `${i}. ${await resolve(value)}`),
153
+ value.map(async (value, i) => `${i}. ${await resolve(value)}`)
154
+ )
155
+ ).join("\n");
156
+ } else if (
157
+ typeof value === "object" &&
158
+ typeof value.path === "string"
159
+ ) {
160
+ if (typeof value.content === "string") {
161
+ appendices[value.path] = value.content;
162
+ return `[${path.basename(value.path)}](${value.path})`;
163
+ } else {
164
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
165
+ return `[${path.basename(value.path)}](${value.path})`;
166
+ }
167
+ } else if (typeof value === "object") {
168
+ return (
169
+ await Promise.all(
170
+ Object.entries(value).map(async ([key, value]) => {
171
+ return `* ${key}: ${await resolve(value)}`;
172
+ })
132
173
  )
133
174
  ).join("\n");
134
175
  } else {
135
176
  // TODO: support other types
136
- throw new Error(`Unsupported value type: ${JSON.stringify(value)}`);
177
+ console.log(value);
178
+ throw new Error(`Unsupported value type: ${value}`);
137
179
  }
138
- }),
180
+ })
139
181
  );
140
182
 
141
183
  // Construct the string template by joining template parts with interpolated values
@@ -144,12 +186,12 @@ function _alchemy(
144
186
  part
145
187
  .split("\n")
146
188
  .map((line) =>
147
- line.startsWith(indent) ? line.slice(indent.length) : line,
189
+ line.startsWith(indent) ? line.slice(indent.length) : line
148
190
  )
149
- .join("\n"),
191
+ .join("\n")
150
192
  )
151
193
  .flatMap((part, i) =>
152
- i < stringValues.length ? [part, stringValues[i] ?? ""] : [part],
194
+ i < stringValues.length ? [part, stringValues[i] ?? ""] : [part]
153
195
  )
154
196
  .join("")
155
197
  .split("\n");
@@ -178,6 +220,7 @@ _alchemy.destroy = destroy;
178
220
  _alchemy.run = run;
179
221
  _alchemy.scope = scope;
180
222
  _alchemy.secret = secret;
223
+ _alchemy.env = env;
181
224
 
182
225
  export interface AlchemyOptions {
183
226
  /**
@@ -240,7 +283,7 @@ export interface AlchemyOptions {
240
283
  */
241
284
  function scope(
242
285
  id: string | undefined,
243
- options?: AlchemyOptions,
286
+ options?: AlchemyOptions
244
287
  // TODO: maybe we want to allow using _ = await alchemy.scope(import.meta)
245
288
  // | [meta: ImportMeta]
246
289
  ): Scope {
@@ -300,3 +343,25 @@ async function run<T>(
300
343
  await scope.finalize();
301
344
  }
302
345
  }
346
+
347
+ export async function env<T = string>(
348
+ name: string,
349
+ value?: T | undefined,
350
+ error?: string
351
+ ): Promise<T> {
352
+ if (value !== undefined) {
353
+ return value;
354
+ } else if (typeof process !== undefined) {
355
+ // we are in a node environment
356
+ return process.env[name]! as T;
357
+ } else {
358
+ // we are in a browser environment
359
+ try {
360
+ const { env } = await import("cloudflare:workers");
361
+ if (name in env) {
362
+ return env[name as keyof typeof env];
363
+ }
364
+ } catch (error) {}
365
+ }
366
+ throw new Error(error ?? `Environment variable ${name} is not set`);
367
+ }
package/src/apply.ts CHANGED
@@ -7,9 +7,8 @@ import {
7
7
  type Resource,
8
8
  type ResourceProps,
9
9
  } from "./resource";
10
- import { Secret } from "./secret";
10
+ import { serialize } from "./serde";
11
11
  import type { State } from "./state";
12
- import { serialize } from "./util/serde";
13
12
 
14
13
  export interface ApplyOptions {
15
14
  quiet?: boolean;
@@ -19,7 +18,7 @@ export interface ApplyOptions {
19
18
  export async function apply<Out extends Resource>(
20
19
  resource: PendingResource<Out>,
21
20
  props: ResourceProps | undefined,
22
- options?: ApplyOptions,
21
+ options?: ApplyOptions
23
22
  ): Promise<Awaited<Out>> {
24
23
  const scope = resource.Scope;
25
24
  try {
@@ -56,29 +55,30 @@ export async function apply<Out extends Resource>(
56
55
 
57
56
  // Skip update if inputs haven't changed and resource is in a stable state
58
57
  if (state.status === "created" || state.status === "updated") {
59
- const oldProps = JSON.stringify(
60
- state.props,
61
- (_, value) =>
62
- value instanceof Secret
63
- ? {
64
- "@secret": value.unencrypted,
65
- }
66
- : value,
67
- 2,
68
- );
69
- const newProps = JSON.stringify(
70
- await serialize(scope, props, {
71
- encrypt: false,
72
- }),
73
- null,
74
- 2,
75
- );
76
- if (oldProps === newProps && alwaysUpdate !== true) {
58
+ const oldProps = await serialize(scope, state.props, {
59
+ encrypt: false,
60
+ });
61
+ const newProps = await serialize(scope, props, {
62
+ encrypt: false,
63
+ });
64
+ if (
65
+ JSON.stringify(oldProps) === JSON.stringify(newProps) &&
66
+ alwaysUpdate !== true
67
+ ) {
77
68
  if (!quiet) {
78
- console.log(`Skip: "${resource.FQN}" (no changes)`);
69
+ // console.log(`Skip: "${resource.FQN}" (no changes)`);
79
70
  }
80
71
  return state.output as Awaited<Out>;
81
72
  }
73
+
74
+ const diff = await import("diff");
75
+ const yaml = await import("yaml");
76
+ const old = yaml.stringify(oldProps);
77
+ const news = yaml.stringify(newProps);
78
+ const patch = diff.createPatch(resource.ID, old, news);
79
+ // console.log(patch);
80
+ // if (resource.ID === "what-is-alchemy") {
81
+ // }
82
82
  }
83
83
 
84
84
  const phase = state.status === "creating" ? "create" : "update";
@@ -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
 
@@ -141,6 +141,7 @@ export async function apply<Out extends Resource>(
141
141
  // }
142
142
  return output as any;
143
143
  } catch (error) {
144
+ console.error(new Error().stack);
144
145
  scope.fail();
145
146
  throw error;
146
147
  }
@@ -2,10 +2,14 @@ import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
2
2
 
3
3
  const sts = new STSClient({});
4
4
 
5
+ export type AccountId = string & {
6
+ readonly __brand: "AccountId";
7
+ };
8
+
5
9
  /**
6
10
  * Helper to get the current AWS account ID
7
11
  */
8
- export async function getAccountId(): Promise<string> {
12
+ export async function AccountId(): Promise<AccountId> {
9
13
  const identity = await sts.send(new GetCallerIdentityCommand({}));
10
- return identity.Account!;
14
+ return identity.Account! as AccountId;
11
15
  }
@@ -0,0 +1,6 @@
1
+ import type { Secret } from "../secret";
2
+
3
+ export interface AwsCredentials {
4
+ accessKeyId: Secret;
5
+ secretAccessKey: Secret;
6
+ }