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,6 +1,6 @@
1
- import { Scope } from "../scope";
2
- import { Secret } from "../secret";
3
- import { decryptWithKey, encryptWithKey } from "./encrypt";
1
+ import { decryptWithKey, encrypt } from "./encrypt";
2
+ import { Scope } from "./scope";
3
+ import { Secret } from "./secret";
4
4
  // zero-dependency type guard for ArkType
5
5
  function isType(value) {
6
6
  return (value &&
@@ -17,7 +17,7 @@ export async function serialize(scope, value, options) {
17
17
  }
18
18
  return {
19
19
  "@secret": options?.encrypt !== false
20
- ? await encryptWithKey(value.unencrypted, scope.password)
20
+ ? await encrypt(value.unencrypted, scope.password)
21
21
  : value.unencrypted,
22
22
  };
23
23
  }
@@ -26,6 +26,11 @@ export async function serialize(scope, value, options) {
26
26
  "@schema": value.toJSON(),
27
27
  };
28
28
  }
29
+ else if (value instanceof Date) {
30
+ return {
31
+ "@date": value.toISOString(),
32
+ };
33
+ }
29
34
  else if (value instanceof Scope) {
30
35
  return undefined;
31
36
  }
@@ -51,6 +56,9 @@ export async function deserialize(scope, value) {
51
56
  else if ("@schema" in value) {
52
57
  return value["@schema"];
53
58
  }
59
+ else if ("@date" in value) {
60
+ return new Date(value["@date"]);
61
+ }
54
62
  else {
55
63
  return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, value]) => [
56
64
  key,
package/lib/test/bun.js CHANGED
@@ -32,12 +32,13 @@ alchemy.test = test;
32
32
  * ```
33
33
  */
34
34
  export function test(meta, defaultOptions) {
35
+ let isFailed = false;
35
36
  defaultOptions = defaultOptions ?? {};
36
37
  if (defaultOptions.stateStore === undefined &&
37
38
  // process.env.CI &&
38
39
  process.env.ALCHEMY_STATE_STORE === "cloudflare") {
39
40
  defaultOptions.stateStore = (scope) => new R2RestStateStore(scope, {
40
- apiKey: process.env.CLOUDFLARE_API_KEY,
41
+ apiKey: alchemy.secret(process.env.CLOUDFLARE_API_KEY),
41
42
  email: process.env.CLOUDFLARE_EMAIL,
42
43
  bucketName: process.env.CLOUDFLARE_BUCKET_NAME,
43
44
  });
@@ -70,7 +71,7 @@ export function test(meta, defaultOptions) {
70
71
  };
71
72
  // Clean up test scope after all tests complete
72
73
  afterAll(async () => {
73
- if (defaultOptions?.destroy !== false) {
74
+ if (defaultOptions?.destroy !== false && !isFailed) {
74
75
  await alchemy.destroy(test.scope);
75
76
  }
76
77
  });
@@ -0,0 +1 @@
1
+ export declare function sha256(value: string): string;
@@ -0,0 +1,4 @@
1
+ import { createHash } from "node:crypto";
2
+ export function sha256(value) {
3
+ return createHash("sha256").update(value).digest("hex");
4
+ }
@@ -0,0 +1,11 @@
1
+ import type { DefaultTheme, HeadConfig, ThemeOptions } from "vitepress";
2
+ export type VitePressConfigProps = {
3
+ cwd: string;
4
+ title?: string;
5
+ description?: string;
6
+ head?: HeadConfig[];
7
+ theme?: ThemeOptions;
8
+ themeConfig: DefaultTheme.Config;
9
+ };
10
+ export type VitePressConfig = Awaited<ReturnType<typeof VitePressConfig>>;
11
+ export declare function VitePressConfig(props: VitePressConfigProps): Promise<import("../../fs").File>;
@@ -0,0 +1,32 @@
1
+ import path from "path";
2
+ import { StaticTypeScriptFile } from "../../fs/static-typescript-file";
3
+ export function VitePressConfig(props) {
4
+ return StaticTypeScriptFile(path.join(props.cwd, ".vitepress", "config.mts"), `import { transformerTwoslash } from "@shikijs/vitepress-twoslash";
5
+ import footnotePlugin from "markdown-it-footnote";
6
+ import { defineConfig } from "vitepress";
7
+
8
+ // https://vitepress.dev/reference/site-config
9
+ export default defineConfig({
10
+ title: ${JSON.stringify(props.title || "Alchemy")},
11
+ description: ${JSON.stringify(props.description || "Alchemy Docs")},
12
+ head: ${JSON.stringify(props.head || [])},
13
+ markdown: {
14
+ // @ts-ignore
15
+ codeTransformers: [transformerTwoslash()],
16
+ theme: ${JSON.stringify(props.theme ?? {
17
+ light: "light-plus",
18
+ dark: "dark-plus",
19
+ })},
20
+ config: (md) => md.use(footnotePlugin),
21
+ },
22
+ // https://vitepress.dev/reference/default-theme-config
23
+ themeConfig: ${JSON.stringify({
24
+ ...props.themeConfig,
25
+ search: props.themeConfig.search ?? { provider: "local" },
26
+ nav: props.themeConfig.nav ?? [{ text: "Home", link: "/" }],
27
+ sidebar: props.themeConfig.sidebar ?? [],
28
+ socialLinks: props.themeConfig.socialLinks ?? [],
29
+ })}
30
+ });
31
+ `);
32
+ }
@@ -1,3 +1,5 @@
1
+ import { Document } from "../../ai/document";
2
+ import { StaticTextFile } from "../../fs";
1
3
  import type { Secret } from "../../secret";
2
4
  /**
3
5
  * Image that can be themed for light/dark mode
@@ -151,11 +153,11 @@ export interface HomePageProps {
151
153
  /**
152
154
  * User prompt describing the design of the home page
153
155
  */
154
- prompt: string;
156
+ prompt?: string;
155
157
  /**
156
158
  * Optional extension to the built-in system prompt
157
159
  */
158
- systemPromptExtension?: string;
160
+ system?: string;
159
161
  /**
160
162
  * Base URL for the OpenAI API
161
163
  * @default 'https://api.openai.com/v1'
@@ -179,7 +181,7 @@ export interface HomePageProps {
179
181
  /**
180
182
  * Output type for the HomePage resource
181
183
  */
182
- export type HomePage = import("../../ai/document").Document;
184
+ export type HomePage = StaticTextFile | Document;
183
185
  /**
184
186
  * Parse YAML frontmatter to home page config
185
187
  */
@@ -1,6 +1,7 @@
1
- import alchemy from "alchemy";
2
1
  import yaml from "yaml";
3
2
  import { Document } from "../../ai/document";
3
+ import { alchemy } from "../../alchemy";
4
+ import { StaticTextFile } from "../../fs";
4
5
  /**
5
6
  * Parse YAML frontmatter to home page config
6
7
  */
@@ -90,45 +91,57 @@ Follow these guidelines:
90
91
  6. Ensure all links are properly formatted
91
92
  7. Use appropriate markdown formatting for headings, lists, etc.
92
93
 
93
- ${props.systemPromptExtension || ""}
94
+ ${props.system || ""}
94
95
  `;
95
96
  // Convert hero and features to YAML strings if provided
96
97
  const heroYaml = props.hero ? JSON.stringify(props.hero, null, 2) : "";
97
98
  const featuresYaml = props.features
98
99
  ? JSON.stringify(props.features, null, 2)
99
100
  : "";
100
- // Create the document using the AI Document resource
101
- return Document(`${id}`, {
102
- title: props.title,
103
- path: props.outFile,
104
- baseURL: props.baseURL,
105
- apiKey: props.apiKey,
106
- model: props.model ?? {
107
- id: "claude-3-7-sonnet-latest",
108
- provider: "anthropic",
109
- },
110
- temperature: props.temperature ?? 0.7,
111
- prompt: await alchemy `
112
- ${systemPrompt}
113
-
114
- Create a VitePress homepage based on the following description:
115
- ${props.prompt}
116
-
117
- ${props.hero
118
- ? `Use this hero section configuration:
119
- \`\`\`json
120
- ${heroYaml}
121
- \`\`\``
122
- : ""}
123
-
124
- ${props.features
125
- ? `Use these features:
126
- \`\`\`json
127
- ${featuresYaml}
128
- \`\`\``
129
- : ""}
101
+ if (props.prompt) {
102
+ return Document(id, {
103
+ title: props.title,
104
+ path: props.outFile,
105
+ baseURL: props.baseURL,
106
+ apiKey: props.apiKey,
107
+ model: props.model ?? {
108
+ id: "claude-3-7-sonnet-latest",
109
+ provider: "anthropic",
110
+ },
111
+ temperature: props.temperature ?? 0.7,
112
+ prompt: await alchemy `
113
+ ${systemPrompt}
114
+
115
+ Create a VitePress homepage based on the following description:
116
+ ${props.prompt}
130
117
 
131
- The output should be a complete index.md file with proper YAML frontmatter and markdown content.
132
- `,
133
- });
118
+ ${props.hero
119
+ ? `Use this hero section configuration:
120
+ \`\`\`json
121
+ ${heroYaml}
122
+ \`\`\``
123
+ : ""}
124
+
125
+ ${props.features
126
+ ? `Use these features:
127
+ \`\`\`json
128
+ ${featuresYaml}
129
+ \`\`\``
130
+ : ""}
131
+
132
+ The output should be a complete index.md file with proper YAML frontmatter and markdown content.
133
+ `,
134
+ });
135
+ }
136
+ else {
137
+ return StaticTextFile(id, props.outFile, `---
138
+ ${yaml.stringify({
139
+ layout: "home",
140
+ name: props.title,
141
+ hero: props.hero,
142
+ features: props.features,
143
+ })}
144
+ ---
145
+ `);
146
+ }
134
147
  }
@@ -1,3 +1,5 @@
1
+ export * from "./config";
1
2
  export * from "./custom-theme";
2
3
  export * from "./home-page";
4
+ export * from "./process-front-matter-files";
3
5
  export * from "./vitepress";
@@ -1,3 +1,5 @@
1
+ export * from "./config";
1
2
  export * from "./custom-theme";
2
3
  export * from "./home-page";
4
+ export * from "./process-front-matter-files";
3
5
  export * from "./vitepress";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Process markdown files with frontmatter to generate navigation items
3
+ * @param directoryPath The directory containing markdown files
4
+ * @param linkPrefix The prefix for navigation links
5
+ * @returns Sorted array of navigation items with text and link properties
6
+ */
7
+ export declare function processFrontmatterFiles(directoryPath: string, linkPrefix: string): Promise<{
8
+ text: string;
9
+ link: string;
10
+ items?: {
11
+ text: string;
12
+ link: string;
13
+ items?: {
14
+ text: string;
15
+ link: string;
16
+ }[];
17
+ }[];
18
+ }[]>;
@@ -0,0 +1,68 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ /**
4
+ * Process markdown files with frontmatter to generate navigation items
5
+ * @param directoryPath The directory containing markdown files
6
+ * @param linkPrefix The prefix for navigation links
7
+ * @returns Sorted array of navigation items with text and link properties
8
+ */
9
+ export async function processFrontmatterFiles(directoryPath, linkPrefix) {
10
+ const entries = await fs.readdir(directoryPath, { withFileTypes: true });
11
+ const processedEntries = await Promise.all(entries
12
+ .filter((entry) => !entry.name.endsWith("index.md"))
13
+ .map(async (entry) => {
14
+ const fullPath = path.join(directoryPath, entry.name);
15
+ if (entry.isDirectory()) {
16
+ // Process subdirectory recursively
17
+ const items = await processFrontmatterFiles(fullPath, `${linkPrefix}/${entry.name}`);
18
+ // Create section for subdirectory
19
+ return {
20
+ text: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
21
+ collapsed: false,
22
+ items,
23
+ order: 10000, // Default order for directories
24
+ };
25
+ }
26
+ // Process markdown file
27
+ if (!entry.name.endsWith(".md")) {
28
+ return null;
29
+ }
30
+ const content = await fs.readFile(fullPath, "utf-8");
31
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
32
+ let order = 10000;
33
+ let title;
34
+ if (frontmatterMatch) {
35
+ const frontmatter = frontmatterMatch[1];
36
+ const orderMatch = frontmatter.match(/order:\s*(\d+)/);
37
+ if (orderMatch) {
38
+ order = parseInt(orderMatch[1]);
39
+ }
40
+ const titleMatch = frontmatter.match(/title:\s*(.+)/);
41
+ if (titleMatch) {
42
+ title = titleMatch[1].trim();
43
+ }
44
+ }
45
+ // If no title in frontmatter, try to get first heading
46
+ if (!title) {
47
+ const headingMatch = content.match(/^#\s+(.+)$/m);
48
+ if (headingMatch) {
49
+ title = headingMatch[1].trim();
50
+ }
51
+ }
52
+ // Fall back to filename if no title found
53
+ if (!title) {
54
+ const name = entry.name.replace(".md", "");
55
+ title = name.charAt(0).toUpperCase() + name.slice(1);
56
+ }
57
+ return {
58
+ text: title,
59
+ link: `${linkPrefix}/${entry.name}`,
60
+ order,
61
+ };
62
+ }));
63
+ // Filter out null entries and sort by order
64
+ return processedEntries
65
+ .filter((entry) => entry !== null)
66
+ .sort((a, b) => a.order - b.order)
67
+ .map(({ text, link, items }) => items ? { text, items, collapsed: true } : { text, link });
68
+ }
@@ -1,4 +1,3 @@
1
- import type { DefaultTheme, ThemeOptions } from "vitepress";
2
1
  import type { Context } from "../../context";
3
2
  import { Resource } from "../../resource";
4
3
  export interface VitePressProjectProps {
@@ -47,11 +46,9 @@ export interface VitePressProjectProps {
47
46
  /**
48
47
  * The theme options to use
49
48
  */
50
- theme?: ThemeOptions;
51
49
  /**
52
50
  * The theme config to use
53
51
  */
54
- themeConfig: DefaultTheme.Config;
55
52
  /**
56
53
  * Whether to delete the project folder during the delete phase
57
54
  * @default true
@@ -69,5 +66,9 @@ export interface VitePressProject extends VitePressProjectProps, Resource {
69
66
  * The name/path of the project
70
67
  */
71
68
  name: string;
69
+ /**
70
+ * The directory of the project
71
+ */
72
+ dir: string;
72
73
  }
73
- export declare const VitePressProject: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<VitePressProject>, id: string, props: VitePressProjectProps) => Promise<VitePressProject>);
74
+ export declare const VitepressProject: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<VitePressProject>, id: string, props: VitePressProjectProps) => Promise<VitePressProject>);
@@ -6,7 +6,7 @@ import { Folder, StaticJsonFile, StaticTextFile, StaticTypeScriptFile, } from ".
6
6
  import { Resource } from "../../resource";
7
7
  import { InstallDependencies, fixedDependencies } from "./dependencies";
8
8
  const execAsync = promisify(exec);
9
- export const VitePressProject = Resource("project::VitePressProject", {
9
+ export const VitepressProject = Resource("project::VitepressProject", {
10
10
  alwaysUpdate: true,
11
11
  }, async function (id, props) {
12
12
  const dir = props.dir ?? props.name;
@@ -14,8 +14,9 @@ export const VitePressProject = Resource("project::VitePressProject", {
14
14
  try {
15
15
  if (props.delete !== false) {
16
16
  if (await fs.exists(dir)) {
17
- // Delete the entire project directory
18
- await execAsync(`rm -rf ${dir}`);
17
+ await fs.rm(dir, { recursive: true, force: true });
18
+ // // Delete the entire project directory
19
+ // await execAsync(`rm -rf ${dir}`);
19
20
  }
20
21
  }
21
22
  }
@@ -57,11 +58,18 @@ export const VitePressProject = Resource("project::VitePressProject", {
57
58
  await Folder(path.join(cwd, ".vitepress", "theme"));
58
59
  await Promise.all([
59
60
  StaticTextFile(path.join(cwd, ".gitignore"), `.vitepress/cache\n`),
60
- StaticJsonFile(path.join(cwd, "tsconfig.json"), {
61
- extends: props.tsconfig?.extends,
62
- references: props.tsconfig?.references?.map((path) => ({ path })),
63
- compilerOptions: props.tsconfig?.compilerOptions,
64
- }),
61
+ // StaticJsonFile(path.join(cwd, "tsconfig.json"), {
62
+ // extends: props.tsconfig?.extends,
63
+ // references: props.tsconfig?.references?.map((path) => ({ path })),
64
+ // compilerOptions: props.tsconfig?.compilerOptions ?? {
65
+ // target: "ES2020",
66
+ // module: "ESNext",
67
+ // moduleResolution: "Node",
68
+ // allowJs: true,
69
+ // skipLibCheck: true,
70
+ // forceConsistentCasingInFileNames: true,
71
+ // },
72
+ // }),
65
73
  // TypeScriptFile(
66
74
  // path.join(cwd, "alchemy.run.ts"),
67
75
  // `import alchemy from "alchemy";
@@ -232,34 +240,10 @@ export default {
232
240
  .DocSearch {
233
241
  --docsearch-primary-color: var(--vp-c-brand-1) !important;
234
242
  }
235
- `),
236
- StaticTypeScriptFile(path.join(cwd, ".vitepress", "config.mts"), `import { transformerTwoslash } from "@shikijs/vitepress-twoslash";
237
- import footnotePlugin from "markdown-it-footnote";
238
- import { defineConfig } from "vitepress";
239
-
240
- // https://vitepress.dev/reference/site-config
241
- export default defineConfig({
242
- title: ${JSON.stringify(props.title || "Alchemy")},
243
- description: ${JSON.stringify(props.description || "Alchemy Docs")},
244
- markdown: {
245
- // @ts-ignore
246
- codeTransformers: [transformerTwoslash()],
247
- theme: ${JSON.stringify(props.theme ?? {
248
- light: "light-plus",
249
- dark: "dark-plus",
250
- })},
251
- config: (md) => md.use(footnotePlugin),
252
- },
253
- // https://vitepress.dev/reference/default-theme-config
254
- themeConfig: ${JSON.stringify({
255
- ...props.themeConfig,
256
- search: props.themeConfig.search ?? { provider: "local" },
257
- nav: props.themeConfig.nav ?? [{ text: "Home", link: "/" }],
258
- sidebar: props.themeConfig.sidebar ?? [],
259
- socialLinks: props.themeConfig.socialLinks ?? [],
260
- })}
261
- });
262
243
  `),
263
244
  ]);
264
- return this(props);
245
+ return this({
246
+ ...props,
247
+ dir,
248
+ });
265
249
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
@@ -38,16 +38,18 @@
38
38
  "@aws-sdk/client-sesv2": "^3.0.0",
39
39
  "@aws-sdk/client-sqs": "^3.0.0",
40
40
  "@aws-sdk/client-sts": "^3.0.0",
41
- "arktype": "^2.0.0",
42
- "prettier": "^3.0.0",
43
41
  "@octokit/rest": "^21.1.1",
44
42
  "ai": "^4.0.0",
43
+ "arktype": "^2.0.0",
44
+ "aws4fetch": "^1.0.20",
45
+ "cloudflare": "^4.0.0",
45
46
  "diff": "^7.0.0",
46
47
  "esbuild": "^0.25.1",
47
48
  "glob": "^10.0.0",
48
49
  "hono": "^4.0.0",
49
- "libsodium-wrappers": "^0.7.15",
50
50
  "jszip": "^3.0.0",
51
+ "libsodium-wrappers": "^0.7.15",
52
+ "prettier": "^3.0.0",
51
53
  "stripe": "^17.0.0",
52
54
  "turndown": "^7.0.0",
53
55
  "yaml": "^2.0.0",
@@ -56,6 +58,7 @@
56
58
  "devDependencies": {
57
59
  "@ai-sdk/anthropic": "^1.1.6",
58
60
  "@ai-sdk/openai": "^1.1.9",
61
+ "@aws-sdk/client-s3": "3.726.1",
59
62
  "@biomejs/biome": "^1.9.4",
60
63
  "@cloudflare/workers-types": "^4.20250303.0",
61
64
  "@octokit/rest": "^21.1.1",
@@ -67,6 +70,7 @@
67
70
  "ai": "^4.1.16",
68
71
  "arktype": "^2.1.16",
69
72
  "change-case": "^5.4.4",
73
+ "cloudflare": "^4.2.0",
70
74
  "libsodium-wrappers": "^0.7.15",
71
75
  "openpgp": "^6.1.0",
72
76
  "prettier": "^3.5.3",
@@ -1,11 +1,9 @@
1
1
  import { generateText } from "ai";
2
- import fs from "node:fs/promises";
3
- import path from "node:path";
4
2
  import prettier from "prettier";
5
3
  import type { Context } from "../context";
4
+ import { StaticAstroFile } from "../fs/static-astro-file";
6
5
  import { Resource } from "../resource";
7
6
  import type { Secret } from "../secret";
8
- import { ignore } from "../util/ignore";
9
7
  import { type ModelConfig, createModel } from "./client";
10
8
 
11
9
  /**
@@ -161,20 +159,10 @@ export const AstroFile = Resource(
161
159
  async function (
162
160
  this: Context<AstroFile>,
163
161
  id: string,
164
- props: AstroFileProps,
162
+ props: AstroFileProps
165
163
  ): Promise<AstroFile> {
166
- // Ensure directory exists
167
- await fs.mkdir(path.dirname(props.path), { recursive: true });
168
-
164
+ // Handle deletion phase
169
165
  if (this.phase === "delete") {
170
- try {
171
- await fs.unlink(props.path);
172
- } catch (error: any) {
173
- // Ignore if file doesn't exist
174
- if (error.code !== "ENOENT") {
175
- throw error;
176
- }
177
- }
178
166
  return this.destroy();
179
167
  }
180
168
 
@@ -213,7 +201,7 @@ export const AstroFile = Resource(
213
201
 
214
202
  if (retryResult.error) {
215
203
  throw new Error(
216
- `Failed to generate valid Astro code: ${retryResult.error}`,
204
+ `Failed to generate valid Astro code: ${retryResult.error}`
217
205
  );
218
206
  }
219
207
 
@@ -235,24 +223,17 @@ export const AstroFile = Resource(
235
223
  console.warn("Failed to format Astro code with Prettier:", error);
236
224
  }
237
225
 
238
- if (this.phase === "update" && props.path !== this.props.path) {
239
- await ignore("ENOENT", () => fs.unlink(this.props.path));
240
- }
241
-
242
- // Write content to file
243
- await fs.writeFile(props.path, code);
244
-
245
- // Get file stats for timestamps
246
- const stats = await fs.stat(props.path);
226
+ // Use StaticAstroFile to create/update the file
227
+ const file = await StaticAstroFile("file", props.path, code);
247
228
 
248
229
  // Return the resource
249
230
  return this({
250
231
  ...props,
251
- content: code,
252
- createdAt: stats.birthtimeMs,
253
- updatedAt: stats.mtimeMs,
232
+ content: file.content,
233
+ createdAt: Date.now(),
234
+ updatedAt: Date.now(),
254
235
  });
255
- },
236
+ }
256
237
  );
257
238
 
258
239
  /**
@@ -263,7 +244,7 @@ export const AstroFile = Resource(
263
244
  * @returns The extracted Astro code or error message
264
245
  */
265
246
  async function extractAstroCode(
266
- text: string,
247
+ text: string
267
248
  ): Promise<{ code: string; error?: string }> {
268
249
  const astroCodeRegex = /```astro\s*([\s\S]*?)```/g;
269
250
  const matches = Array.from(text.matchAll(astroCodeRegex));
package/src/ai/client.ts CHANGED
@@ -56,3 +56,62 @@ export function createModel(config: ClientConfig) {
56
56
  return openai(config.model?.id ?? "gpt-4o");
57
57
  }
58
58
  }
59
+
60
+ /**
61
+ * Maximum time to retry in milliseconds (5 minutes)
62
+ */
63
+ const MAX_RETRY_TIME = 5 * 60 * 1000;
64
+
65
+ /**
66
+ * Initial delay between retries in milliseconds
67
+ */
68
+ const INITIAL_RETRY_DELAY = 1000;
69
+
70
+ /**
71
+ * Maximum number of retries
72
+ */
73
+ const MAX_RETRIES = 10;
74
+
75
+ /**
76
+ * Handles rate limiting with exponential backoff
77
+ * @param fn Function to retry
78
+ * @returns Result of the function
79
+ * @throws Error if max retries or time is exceeded
80
+ */
81
+ export async function withRateLimitRetry<T>(fn: () => Promise<T>): Promise<T> {
82
+ let retryCount = 0;
83
+ let lastError: Error | null = null;
84
+ let startTime = Date.now();
85
+
86
+ while (true) {
87
+ try {
88
+ return await fn();
89
+ } catch (error: any) {
90
+ lastError = error;
91
+
92
+ console.log("retry error", error);
93
+
94
+ // Check if we should retry
95
+ const isRateLimit = error.statusCode === 429;
96
+ const timeElapsed = Date.now() - startTime;
97
+ const shouldRetry =
98
+ isRateLimit && retryCount < MAX_RETRIES && timeElapsed < MAX_RETRY_TIME;
99
+
100
+ if (!shouldRetry) {
101
+ throw error;
102
+ }
103
+
104
+ // Calculate delay with exponential backoff
105
+ const delay = Math.min(
106
+ INITIAL_RETRY_DELAY * Math.pow(2, retryCount),
107
+ MAX_RETRY_TIME - timeElapsed
108
+ );
109
+
110
+ console.log(`Retrying in ${delay}ms`);
111
+
112
+ // Wait before retrying
113
+ await new Promise((resolve) => setTimeout(resolve, delay));
114
+ retryCount++;
115
+ }
116
+ }
117
+ }