alchemy 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +3 -3
  2. package/lib/ai/astro-file.d.ts +137 -0
  3. package/lib/ai/astro-file.js +173 -0
  4. package/lib/ai/client.d.ts +2 -10
  5. package/lib/ai/client.js +8 -24
  6. package/lib/ai/css-file.d.ts +131 -0
  7. package/lib/ai/css-file.js +158 -0
  8. package/lib/ai/{object.d.ts → data.d.ts} +48 -8
  9. package/lib/ai/{object.js → data.js} +46 -11
  10. package/lib/ai/document.d.ts +48 -20
  11. package/lib/ai/document.js +95 -28
  12. package/lib/ai/html-file.d.ts +128 -0
  13. package/lib/ai/html-file.js +155 -0
  14. package/lib/ai/index.d.ts +9 -1
  15. package/lib/ai/index.js +9 -1
  16. package/lib/ai/json-file.d.ts +160 -0
  17. package/lib/ai/json-file.js +208 -0
  18. package/lib/ai/typescript-file.d.ts +140 -0
  19. package/lib/ai/typescript-file.js +176 -0
  20. package/lib/ai/vue-file.d.ts +126 -0
  21. package/lib/ai/vue-file.js +153 -0
  22. package/lib/ai/yaml-file.d.ts +169 -0
  23. package/lib/ai/yaml-file.js +236 -0
  24. package/lib/cloudflare/dns.d.ts +88 -0
  25. package/lib/cloudflare/dns.js +192 -0
  26. package/lib/cloudflare/index.d.ts +2 -1
  27. package/lib/cloudflare/index.js +2 -1
  28. package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
  29. package/lib/cloudflare/r2-rest-state-store.js +254 -0
  30. package/lib/cloudflare/response.d.ts +12 -0
  31. package/lib/cloudflare/response.js +0 -0
  32. package/lib/cloudflare/zone.d.ts +18 -0
  33. package/lib/cloudflare/zone.js +17 -4
  34. package/lib/dns/godaddy.d.ts +7 -0
  35. package/lib/dns/godaddy.js +21 -0
  36. package/lib/dns/import-dns.d.ts +82 -0
  37. package/lib/dns/import-dns.js +114 -0
  38. package/lib/dns/index.d.ts +2 -0
  39. package/lib/dns/index.js +2 -0
  40. package/lib/dns/record.d.ts +88 -0
  41. package/lib/dns/record.js +13 -0
  42. package/lib/fs/file-system-state-store.d.ts +17 -0
  43. package/lib/fs/file-system-state-store.js +87 -0
  44. package/lib/fs/folder.d.ts +10 -0
  45. package/lib/fs/folder.js +2 -2
  46. package/lib/fs/index.d.ts +4 -4
  47. package/lib/fs/index.js +4 -4
  48. package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +2 -2
  49. package/lib/fs/static-json-file.js +14 -0
  50. package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
  51. package/lib/fs/{text-file.js → static-text-file.js} +1 -1
  52. package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +2 -2
  53. package/lib/fs/{typescript-file.js → static-typescript-file.js} +1 -1
  54. package/lib/fs/{yaml-file.d.ts → static-yaml-file.d.ts} +2 -2
  55. package/lib/fs/{yaml-file.js → static-yaml-file.js} +1 -1
  56. package/lib/internal/getting-started.d.ts +10 -0
  57. package/lib/internal/getting-started.js +94 -0
  58. package/lib/internal/project.d.ts +13 -0
  59. package/lib/internal/project.js +101 -0
  60. package/lib/internal/providers.d.ts +23 -0
  61. package/lib/internal/providers.js +148 -0
  62. package/lib/resource.js +8 -1
  63. package/lib/scope.d.ts +1 -1
  64. package/lib/scope.js +4 -2
  65. package/lib/state.d.ts +1 -16
  66. package/lib/state.js +0 -87
  67. package/lib/test/bun.d.ts +11 -0
  68. package/lib/test/bun.js +36 -8
  69. package/lib/web/astro.d.ts +147 -0
  70. package/lib/web/astro.js +414 -0
  71. package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
  72. package/lib/web/shadcn.d.ts +97 -0
  73. package/lib/web/shadcn.js +99 -0
  74. package/lib/web/tailwind.d.ts +64 -0
  75. package/lib/web/tailwind.js +97 -0
  76. package/lib/{vite → web}/vite.d.ts +5 -0
  77. package/lib/{vite → web}/vite.js +44 -44
  78. package/lib/web/vitepress/custom-theme.d.ts +173 -0
  79. package/lib/web/vitepress/custom-theme.js +336 -0
  80. package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
  81. package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
  82. package/lib/web/vitepress/home-page.d.ts +244 -0
  83. package/lib/web/vitepress/home-page.js +134 -0
  84. package/lib/{vitepress → web/vitepress}/index.d.ts +1 -0
  85. package/lib/{vitepress → web/vitepress}/index.js +1 -0
  86. package/lib/{vitepress → web/vitepress}/vitepress.d.ts +10 -8
  87. package/lib/{vitepress → web/vitepress}/vitepress.js +11 -12
  88. package/package.json +6 -2
  89. package/src/ai/astro-file.ts +288 -0
  90. package/src/ai/client.ts +8 -28
  91. package/src/ai/css-file.ts +266 -0
  92. package/src/ai/{object.ts → data.ts} +58 -21
  93. package/src/ai/document.ts +135 -33
  94. package/src/ai/html-file.ts +263 -0
  95. package/src/ai/index.ts +9 -1
  96. package/src/ai/json-file.ts +349 -0
  97. package/src/ai/typescript-file.ts +293 -0
  98. package/src/ai/vue-file.ts +261 -0
  99. package/src/ai/yaml-file.ts +368 -0
  100. package/src/apply.ts +0 -1
  101. package/src/cloudflare/dns.ts +344 -0
  102. package/src/cloudflare/index.ts +2 -1
  103. package/src/cloudflare/r2-rest-state-store.ts +353 -0
  104. package/src/cloudflare/response.ts +9 -0
  105. package/src/cloudflare/zone.ts +39 -23
  106. package/src/dns/godaddy.ts +29 -0
  107. package/src/dns/import-dns.ts +213 -0
  108. package/src/dns/index.ts +2 -0
  109. package/src/dns/record.ts +121 -0
  110. package/src/fs/file-system-state-store.ts +109 -0
  111. package/src/fs/folder.ts +14 -2
  112. package/src/fs/index.ts +4 -4
  113. package/src/fs/{json-file.ts → static-json-file.ts} +13 -3
  114. package/src/fs/{text-file.ts → static-text-file.ts} +5 -2
  115. package/src/fs/{typescript-file.ts → static-typescript-file.ts} +3 -3
  116. package/src/fs/{yaml-file.ts → static-yaml-file.ts} +5 -2
  117. package/src/internal/getting-started.ts +107 -0
  118. package/src/internal/project.ts +119 -0
  119. package/src/internal/providers.ts +209 -0
  120. package/src/resource.ts +10 -1
  121. package/src/scope.ts +5 -6
  122. package/src/state.ts +1 -111
  123. package/src/test/bun.ts +60 -10
  124. package/src/web/astro.ts +644 -0
  125. package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
  126. package/src/web/shadcn.ts +219 -0
  127. package/src/web/tailwind.ts +167 -0
  128. package/src/{vite → web}/vite.ts +54 -56
  129. package/src/web/vitepress/custom-theme.ts +514 -0
  130. package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
  131. package/src/web/vitepress/home-page.ts +363 -0
  132. package/src/{vitepress → web/vitepress}/index.ts +1 -0
  133. package/src/{vitepress → web/vitepress}/vitepress.ts +34 -27
  134. package/lib/cloudflare/state.d.ts +0 -82
  135. package/lib/cloudflare/state.js +0 -108
  136. package/lib/fs/json-file.js +0 -7
  137. package/lib/internal/docs.d.ts +0 -5
  138. package/lib/internal/docs.js +0 -324
  139. package/lib/shadcn/index.d.ts +0 -1
  140. package/lib/shadcn/index.js +0 -1
  141. package/lib/vite/index.d.ts +0 -1
  142. package/lib/vite/index.js +0 -1
  143. package/lib/vitepress/home-page.d.ts +0 -133
  144. package/lib/vitepress/home-page.js +0 -11
  145. package/src/cloudflare/state.ts +0 -138
  146. package/src/internal/docs.ts +0 -369
  147. package/src/shadcn/index.ts +0 -1
  148. package/src/vite/index.ts +0 -1
  149. package/src/vitepress/home-page.ts +0 -166
  150. /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
  151. /package/src/{vitepress → web/vitepress}/index.md +0 -0
@@ -0,0 +1,134 @@
1
+ import alchemy from "alchemy";
2
+ import yaml from "yaml";
3
+ import { Document } from "../../ai/document";
4
+ /**
5
+ * Parse YAML frontmatter to home page config
6
+ */
7
+ export function parseHomePage(content) {
8
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
9
+ if (!match) {
10
+ throw new Error("Invalid frontmatter format");
11
+ }
12
+ return yaml.parse(match[1]);
13
+ }
14
+ /**
15
+ * Resource for generating a VitePress homepage using AI.
16
+ * Creates an index.md file with the appropriate frontmatter and content
17
+ * based on the provided prompt and configuration.
18
+ *
19
+ * @example
20
+ * // Create a basic home page with just a prompt
21
+ * const homePage = await HomePage("docs-home", {
22
+ * outDir: "./docs",
23
+ * title: "My Project Documentation",
24
+ * prompt: "Create a welcoming homepage for a JavaScript utility library with modern design"
25
+ * });
26
+ *
27
+ * @example
28
+ * // Create a home page with hero and features configuration
29
+ * const detailedHome = await HomePage("product-home", {
30
+ * outDir: "./website",
31
+ * title: "Product Landing Page",
32
+ * prompt: "Create an engaging product page for our cloud database service",
33
+ * hero: {
34
+ * name: "CloudDB",
35
+ * text: "The database built for the cloud era",
36
+ * tagline: "Scalable, reliable, and developer-friendly",
37
+ * image: "/images/cloud-db-logo.png",
38
+ * actions: [
39
+ * { text: "Get Started", link: "/guide/", theme: "brand" },
40
+ * { text: "View on GitHub", link: "https://github.com/org/cloud-db" }
41
+ * ]
42
+ * },
43
+ * features: [
44
+ * {
45
+ * icon: "🚀",
46
+ * title: "Lightning Fast",
47
+ * details: "Optimized for performance at any scale"
48
+ * },
49
+ * {
50
+ * icon: "🔒",
51
+ * title: "Secure by Default",
52
+ * details: "Enterprise-grade security built in"
53
+ * }
54
+ * ]
55
+ * });
56
+ *
57
+ * @example
58
+ * // Create a home page with custom model settings
59
+ * const customHome = await HomePage("docs-home", {
60
+ * outDir: "./docs",
61
+ * title: "API Documentation",
62
+ * prompt: "Create a technical homepage for our REST API documentation",
63
+ * model: {
64
+ * id: "gpt-4o",
65
+ * provider: "openai"
66
+ * },
67
+ * temperature: 0.3,
68
+ * systemPromptExtension: "Make sure to use technical terminology appropriate for developers."
69
+ * });
70
+ */
71
+ export async function HomePage(id, props) {
72
+ // Build the system prompt with optional extension
73
+ const systemPrompt = `
74
+ You are creating a VitePress homepage (index.md file).
75
+
76
+ VitePress uses YAML frontmatter to configure the homepage.
77
+ The homepage must have "layout: home" in its frontmatter.
78
+
79
+ The homepage can include:
80
+ - A hero section with name, text, tagline, image, and action buttons
81
+ - A features section with multiple feature items (title, icon, details)
82
+ - Additional markdown content below the frontmatter
83
+
84
+ Follow these guidelines:
85
+ 1. Start with YAML frontmatter (--- at beginning and end)
86
+ 2. Include "layout: home" in the frontmatter
87
+ 3. Use the provided hero and features configuration if supplied
88
+ 4. Add any additional sections or content based on the user's prompt
89
+ 5. Create visually appealing, well-structured content
90
+ 6. Ensure all links are properly formatted
91
+ 7. Use appropriate markdown formatting for headings, lists, etc.
92
+
93
+ ${props.systemPromptExtension || ""}
94
+ `;
95
+ // Convert hero and features to YAML strings if provided
96
+ const heroYaml = props.hero ? JSON.stringify(props.hero, null, 2) : "";
97
+ const featuresYaml = props.features
98
+ ? JSON.stringify(props.features, null, 2)
99
+ : "";
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
+ : ""}
130
+
131
+ The output should be a complete index.md file with proper YAML frontmatter and markdown content.
132
+ `,
133
+ });
134
+ }
@@ -1,2 +1,3 @@
1
+ export * from "./custom-theme";
1
2
  export * from "./home-page";
2
3
  export * from "./vitepress";
@@ -1,2 +1,3 @@
1
+ export * from "./custom-theme";
1
2
  export * from "./home-page";
2
3
  export * from "./vitepress";
@@ -1,7 +1,6 @@
1
1
  import type { DefaultTheme, ThemeOptions } from "vitepress";
2
- import type { Context } from "../context";
3
- import { Resource } from "../resource";
4
- import type { HomePage } from "./home-page";
2
+ import type { Context } from "../../context";
3
+ import { Resource } from "../../resource";
5
4
  export interface VitePressProjectProps {
6
5
  /**
7
6
  * The name/path of the project
@@ -20,6 +19,13 @@ export interface VitePressProjectProps {
20
19
  * @default true
21
20
  */
22
21
  typescript?: boolean;
22
+ /**
23
+ * The scripts to add to the project
24
+ */
25
+ scripts?: Record<string, string>;
26
+ /**
27
+ * The tsconfig to use
28
+ */
23
29
  tsconfig?: {
24
30
  extends?: string;
25
31
  references?: string[];
@@ -46,17 +52,13 @@ export interface VitePressProjectProps {
46
52
  * The theme config to use
47
53
  */
48
54
  themeConfig: DefaultTheme.Config;
49
- /**
50
- * The home page configuration
51
- */
52
- home: HomePage;
53
55
  /**
54
56
  * Whether to delete the project folder during the delete phase
55
57
  * @default true
56
58
  */
57
59
  delete?: boolean;
58
60
  /**
59
- * The directory to generate the docs in
61
+ * The directory to initialize the project in.
60
62
  *
61
63
  * @default {@link name}
62
64
  */
@@ -2,9 +2,8 @@ import { exec } from "child_process";
2
2
  import fs from "fs/promises";
3
3
  import path from "path";
4
4
  import { promisify } from "util";
5
- import yaml from "yaml";
6
- import { Folder, JsonFile, TextFile, TypeScriptFile } from "../fs";
7
- import { Resource } from "../resource";
5
+ import { Folder, StaticJsonFile, StaticTextFile, StaticTypeScriptFile, } from "../../fs";
6
+ import { Resource } from "../../resource";
8
7
  import { InstallDependencies, fixedDependencies } from "./dependencies";
9
8
  const execAsync = promisify(exec);
10
9
  export const VitePressProject = Resource("project::VitePressProject", {
@@ -25,18 +24,19 @@ export const VitePressProject = Resource("project::VitePressProject", {
25
24
  }
26
25
  return this.destroy();
27
26
  }
28
- const cwd = (await Folder("dir", {
27
+ const cwd = path.relative(process.cwd(), (await Folder("dir", {
29
28
  path: dir,
30
29
  delete: props.delete,
31
- })).path;
30
+ })).path);
32
31
  // Initialize package.json
33
- await JsonFile(path.join(cwd, "package.json"), {
32
+ await StaticJsonFile(path.join(cwd, "package.json"), {
34
33
  name: props.name,
35
34
  scripts: {
36
35
  "docs:gen": "bun alchemy.run.ts",
37
36
  "docs:dev": "vitepress dev",
38
37
  "docs:build": "vitepress build",
39
38
  "docs:preview": "vitepress preview",
39
+ ...props.scripts,
40
40
  },
41
41
  dependencies: fixedDependencies(props.dependencies || {}),
42
42
  devDependencies: fixedDependencies(props.devDependencies || {}),
@@ -56,13 +56,12 @@ export const VitePressProject = Resource("project::VitePressProject", {
56
56
  await Folder(path.join(cwd, ".vitepress"));
57
57
  await Folder(path.join(cwd, ".vitepress", "theme"));
58
58
  await Promise.all([
59
- TextFile(path.join(cwd, ".gitignore"), `.vitepress/cache\n`),
60
- JsonFile(path.join(cwd, "tsconfig.json"), {
59
+ StaticTextFile(path.join(cwd, ".gitignore"), `.vitepress/cache\n`),
60
+ StaticJsonFile(path.join(cwd, "tsconfig.json"), {
61
61
  extends: props.tsconfig?.extends,
62
62
  references: props.tsconfig?.references?.map((path) => ({ path })),
63
63
  compilerOptions: props.tsconfig?.compilerOptions,
64
64
  }),
65
- TextFile(path.join(cwd, "index.md"), `---\n${yaml.stringify(props.home)}---\n`),
66
65
  // TypeScriptFile(
67
66
  // path.join(cwd, "alchemy.run.ts"),
68
67
  // `import alchemy from "alchemy";
@@ -82,7 +81,7 @@ export const VitePressProject = Resource("project::VitePressProject", {
82
81
  // });
83
82
  // `,
84
83
  // ),
85
- TypeScriptFile(path.join(cwd, ".vitepress", "theme", "index.ts"), `import TwoslashFloatingVue from "@shikijs/vitepress-twoslash/client";
84
+ StaticTypeScriptFile(path.join(cwd, ".vitepress", "theme", "index.ts"), `import TwoslashFloatingVue from "@shikijs/vitepress-twoslash/client";
86
85
  import "@shikijs/vitepress-twoslash/style.css";
87
86
  import type { Theme as ThemeConfig } from "vitepress";
88
87
  import Theme from "vitepress/theme-without-fonts";
@@ -95,7 +94,7 @@ export default {
95
94
  },
96
95
  } satisfies ThemeConfig;
97
96
  `),
98
- TextFile(path.join(cwd, ".vitepress", "theme", "style.css"), `/**
97
+ StaticTextFile(path.join(cwd, ".vitepress", "theme", "style.css"), `/**
99
98
  * Customize default theme styling by overriding CSS variables:
100
99
  * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
101
100
  */
@@ -234,7 +233,7 @@ export default {
234
233
  --docsearch-primary-color: var(--vp-c-brand-1) !important;
235
234
  }
236
235
  `),
237
- TypeScriptFile(path.join(cwd, ".vitepress", "config.mts"), `import { transformerTwoslash } from "@shikijs/vitepress-twoslash";
236
+ StaticTypeScriptFile(path.join(cwd, ".vitepress", "config.mts"), `import { transformerTwoslash } from "@shikijs/vitepress-twoslash";
238
237
  import footnotePlugin from "markdown-it-footnote";
239
238
  import { defineConfig } from "vitepress";
240
239
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
@@ -14,13 +14,17 @@
14
14
  "./aws": "./lib/aws/index.js",
15
15
  "./aws/oidc": "./lib/aws/oidc/index.js",
16
16
  "./cloudflare": "./lib/cloudflare/index.js",
17
+ "./dns": "./lib/dns/index.js",
17
18
  "./esbuild": "./lib/esbuild/index.js",
18
19
  "./fs": "./lib/fs/index.js",
19
20
  "./github": "./lib/github/index.js",
20
21
  "./shadcn": "./lib/shadcn/index.js",
21
22
  "./stripe": "./lib/stripe/index.js",
22
23
  "./test/bun": "./lib/test/bun.js",
23
- "./vite": "./lib/vite/index.js"
24
+ "./web/astro": "./lib/web/astro/index.js",
25
+ "./web/tailwind": "./lib/web/tailwind/index.js",
26
+ "./web/vite": "./lib/web/vite/index.js",
27
+ "./web/vitepress": "./lib/web/vitepress/index.js"
24
28
  },
25
29
  "peerDependencies": {
26
30
  "@ai-sdk/openai": "^1.1.9",
@@ -0,0 +1,288 @@
1
+ import { generateText } from "ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import prettier from "prettier";
5
+ import type { Context } from "../context";
6
+ import { Resource } from "../resource";
7
+ import type { Secret } from "../secret";
8
+ import { ignore } from "../util/ignore";
9
+ import { type ModelConfig, createModel } from "./client";
10
+
11
+ /**
12
+ * Properties for creating or updating an AstroFile
13
+ */
14
+ export interface AstroFileProps {
15
+ /**
16
+ * Path to the Astro file
17
+ */
18
+ path: string;
19
+
20
+ /**
21
+ * Base URL for the OpenAI API
22
+ * @default 'https://api.openai.com/v1'
23
+ */
24
+ baseURL?: string;
25
+
26
+ /**
27
+ * Prompt for generating content
28
+ * Use alchemy template literals to include file context:
29
+ * @example
30
+ * prompt: await alchemy`
31
+ * Generate an Astro component using:
32
+ * ${alchemy.file("src/types.ts")}
33
+ * `
34
+ */
35
+ prompt: string;
36
+
37
+ /**
38
+ * System prompt for the model
39
+ * This is used to provide instructions to the model about how to format the response
40
+ * The default system prompt instructs the model to return Astro code inside ```astro fences
41
+ * @default "You are an Astro component generator. Create Astro components based on the user's requirements. Your response MUST include only Astro code inside ```astro fences. Do not include any other text, explanations, or multiple code blocks."
42
+ */
43
+ system?: string;
44
+
45
+ /**
46
+ * OpenAI API key to use for generating content
47
+ * If not provided, will use OPENAI_API_KEY environment variable
48
+ */
49
+ apiKey?: Secret;
50
+
51
+ /**
52
+ * Model configuration
53
+ */
54
+ model?: ModelConfig;
55
+
56
+ /**
57
+ * Temperature for controlling randomness in generation.
58
+ * Higher values (e.g., 0.8) make output more random,
59
+ * lower values (e.g., 0.2) make it more deterministic.
60
+ * @default 0.7
61
+ */
62
+ temperature?: number;
63
+
64
+ /**
65
+ * Prettier configuration to use for formatting the Astro code
66
+ * If not provided, will use the default Prettier configuration
67
+ */
68
+ prettierConfig?: prettier.Options;
69
+ }
70
+
71
+ /**
72
+ * An Astro file that can be created, updated, and deleted
73
+ */
74
+ export interface AstroFile extends AstroFileProps, Resource<"ai::AstroFile"> {
75
+ /**
76
+ * Content of the Astro file
77
+ */
78
+ content: string;
79
+
80
+ /**
81
+ * Time at which the file was created
82
+ */
83
+ createdAt: number;
84
+
85
+ /**
86
+ * Time at which the file was last updated
87
+ */
88
+ updatedAt: number;
89
+ }
90
+
91
+ /**
92
+ * Default system prompt for Astro file generation
93
+ */
94
+ const DEFAULT_ASTRO_SYSTEM_PROMPT =
95
+ "You are an Astro component generator. Create Astro components based on the user's requirements. Your response MUST include only Astro code inside ```astro fences. Do not include any other text, explanations, or multiple code blocks.";
96
+
97
+ /**
98
+ * Resource for generating Astro files using AI models.
99
+ * Extracts Astro code from between ```astro fences, validates the response,
100
+ * and formats the code with Prettier.
101
+ *
102
+ * @example
103
+ * // Create a simple Astro component
104
+ * const header = await AstroFile("header", {
105
+ * path: "./src/components/Header.astro",
106
+ * prompt: await alchemy`
107
+ * Generate an Astro header component with:
108
+ * - Site logo
109
+ * - Navigation menu with Home, About, Services, Contact links
110
+ * - Mobile responsive design
111
+ * - Dark/light mode toggle
112
+ * `,
113
+ * model: {
114
+ * id: "gpt-4o",
115
+ * provider: "openai"
116
+ * }
117
+ * });
118
+ *
119
+ * @example
120
+ * // Generate an Astro page with data fetching
121
+ * const blogPost = await AstroFile("blog-post", {
122
+ * path: "./src/pages/blog/[slug].astro",
123
+ * prompt: await alchemy`
124
+ * Create an Astro blog post page that:
125
+ * - Uses getStaticPaths to generate pages from a CMS
126
+ * - Renders markdown content
127
+ * - Includes author info, publication date, and related posts
128
+ * - Has social sharing buttons
129
+ *
130
+ * Use the following types:
131
+ * ${alchemy.file("src/types/Blog.ts")}
132
+ * `,
133
+ * temperature: 0.2,
134
+ * prettierConfig: {
135
+ * semi: false,
136
+ * singleQuote: true,
137
+ * printWidth: 120
138
+ * }
139
+ * });
140
+ *
141
+ * @example
142
+ * // Generate a layout with custom system prompt
143
+ * const mainLayout = await AstroFile("main-layout", {
144
+ * path: "./src/layouts/MainLayout.astro",
145
+ * prompt: await alchemy`
146
+ * Create the main layout for an Astro site that:
147
+ * - Includes common head metadata and SEO optimization
148
+ * - Has slots for page content, header, and footer
149
+ * - Imports and uses the Header and Footer components
150
+ * - Sets up viewport and responsive configurations
151
+ * `,
152
+ * system: "You are an expert Astro developer. Create a single Astro layout file inside ```astro fences with no additional text. Follow Astro best practices and include proper typing in the frontmatter section.",
153
+ * model: {
154
+ * id: "claude-3-opus-20240229",
155
+ * provider: "anthropic"
156
+ * }
157
+ * });
158
+ */
159
+ export const AstroFile = Resource(
160
+ "ai::AstroFile",
161
+ async function (
162
+ this: Context<AstroFile>,
163
+ id: string,
164
+ props: AstroFileProps,
165
+ ): Promise<AstroFile> {
166
+ // Ensure directory exists
167
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
168
+
169
+ 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
+ return this.destroy();
179
+ }
180
+
181
+ // Use provided system prompt or default
182
+ const system = props.system || DEFAULT_ASTRO_SYSTEM_PROMPT;
183
+
184
+ // Generate initial content
185
+ const { text } = await generateText({
186
+ model: createModel(props),
187
+ prompt: props.prompt,
188
+ system,
189
+ providerOptions: props.model?.options,
190
+ ...(props.temperature === undefined
191
+ ? {}
192
+ : { temperature: props.temperature }),
193
+ });
194
+
195
+ // Extract and validate Astro code
196
+ let { code, error } = await extractAstroCode(text);
197
+
198
+ // Re-prompt if there are validation errors
199
+ if (error) {
200
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one Astro code block inside \`\`\`astro fences.`;
201
+
202
+ const { text: retryText } = await generateText({
203
+ model: createModel(props),
204
+ prompt: props.prompt,
205
+ system: errorSystem,
206
+ providerOptions: props.model?.options,
207
+ ...(props.temperature === undefined
208
+ ? {}
209
+ : { temperature: props.temperature }),
210
+ });
211
+
212
+ const retryResult = await extractAstroCode(retryText);
213
+
214
+ if (retryResult.error) {
215
+ throw new Error(
216
+ `Failed to generate valid Astro code: ${retryResult.error}`,
217
+ );
218
+ }
219
+
220
+ code = retryResult.code;
221
+ }
222
+
223
+ // Format the code with Prettier
224
+ try {
225
+ // Set default parser to astro
226
+ const prettierOptions: prettier.Options = {
227
+ parser: "astro",
228
+ ...props.prettierConfig,
229
+ };
230
+
231
+ // Format the code
232
+ code = await prettier.format(code, prettierOptions);
233
+ } catch (error) {
234
+ // If Prettier formatting fails, just use the unformatted code
235
+ console.warn("Failed to format Astro code with Prettier:", error);
236
+ }
237
+
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);
247
+
248
+ // Return the resource
249
+ return this({
250
+ ...props,
251
+ content: code,
252
+ createdAt: stats.birthtimeMs,
253
+ updatedAt: stats.mtimeMs,
254
+ });
255
+ },
256
+ );
257
+
258
+ /**
259
+ * Extracts Astro code from between ```astro fences
260
+ * Validates that exactly one Astro code block exists
261
+ *
262
+ * @param text The text to extract Astro code from
263
+ * @returns The extracted Astro code or error message
264
+ */
265
+ async function extractAstroCode(
266
+ text: string,
267
+ ): Promise<{ code: string; error?: string }> {
268
+ const astroCodeRegex = /```astro\s*([\s\S]*?)```/g;
269
+ const matches = Array.from(text.matchAll(astroCodeRegex));
270
+
271
+ if (matches.length === 0) {
272
+ return {
273
+ code: "",
274
+ error:
275
+ "No Astro code block found in the response. Please include your code within ```astro fences.",
276
+ };
277
+ }
278
+
279
+ if (matches.length > 1) {
280
+ return {
281
+ code: "",
282
+ error:
283
+ "Multiple Astro code blocks found in the response. Please provide exactly one code block within ```astro fences.",
284
+ };
285
+ }
286
+
287
+ return { code: matches[0][1].trim() };
288
+ }
package/src/ai/client.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
1
+ import { anthropic } from "@ai-sdk/anthropic";
2
+ import { openai } from "@ai-sdk/openai";
2
3
  import type { Secret } from "../secret";
3
4
 
4
5
  /**
@@ -20,7 +21,7 @@ export interface ModelConfig {
20
21
  /**
21
22
  * Model-specific options
22
23
  */
23
- options?: Record<string, unknown>;
24
+ options?: Record<string, any>;
24
25
  }
25
26
 
26
27
  /**
@@ -48,31 +49,10 @@ export interface ClientConfig {
48
49
  /**
49
50
  * Creates an OpenAI-compatible client with the given configuration
50
51
  */
51
- export function createClient(config: ClientConfig) {
52
- // Get API key from props or environment
53
- const apiKey = config.apiKey?.unencrypted || process.env.OPENAI_API_KEY;
54
- if (!apiKey) {
55
- throw new Error("OpenAI API key is required");
52
+ export function createModel(config: ClientConfig) {
53
+ if (config.model?.provider === "anthropic") {
54
+ return anthropic(config.model?.id ?? "claude-3-7-sonnet-latest");
55
+ } else {
56
+ return openai(config.model?.id ?? "gpt-4o");
56
57
  }
57
-
58
- // Initialize OpenAI compatible provider
59
- return createOpenAICompatible({
60
- name: config.model?.provider || "openai",
61
- apiKey,
62
- baseURL: config.baseURL || "https://api.openai.com/v1",
63
- });
64
- }
65
-
66
- /**
67
- * Gets the model ID from the configuration or returns the default
68
- */
69
- export function getModelId(config: ClientConfig): string {
70
- return config.model?.id || "gpt-4o";
71
- }
72
-
73
- /**
74
- * Gets the model options from the configuration
75
- */
76
- export function getModelOptions(config: ClientConfig): Record<string, unknown> {
77
- return config.model?.options || {};
78
58
  }