alchemy 0.2.5 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/README.md +3 -3
  2. package/lib/ai/approve.d.ts +99 -0
  3. package/lib/ai/approve.js +76 -0
  4. package/lib/ai/astro-file.d.ts +137 -0
  5. package/lib/ai/astro-file.js +156 -0
  6. package/lib/ai/client.d.ts +7 -8
  7. package/lib/ai/client.js +48 -19
  8. package/lib/ai/css-file.d.ts +131 -0
  9. package/lib/ai/css-file.js +141 -0
  10. package/lib/ai/data.d.ts +51 -6
  11. package/lib/ai/data.js +58 -12
  12. package/lib/ai/document.d.ts +108 -24
  13. package/lib/ai/document.js +166 -52
  14. package/lib/ai/html-file.d.ts +128 -0
  15. package/lib/ai/html-file.js +138 -0
  16. package/lib/ai/index.d.ts +9 -0
  17. package/lib/ai/index.js +9 -0
  18. package/lib/ai/json-file.d.ts +160 -0
  19. package/lib/ai/json-file.js +187 -0
  20. package/lib/ai/review.d.ts +122 -0
  21. package/lib/ai/review.js +101 -0
  22. package/lib/ai/typescript-file.d.ts +139 -0
  23. package/lib/ai/typescript-file.js +145 -0
  24. package/lib/ai/vue-file.d.ts +126 -0
  25. package/lib/ai/vue-file.js +136 -0
  26. package/lib/ai/yaml-file.d.ts +169 -0
  27. package/lib/ai/yaml-file.js +200 -0
  28. package/lib/alchemy.js +17 -2
  29. package/lib/apply.js +1 -1
  30. package/lib/cloudflare/dns.d.ts +88 -0
  31. package/lib/cloudflare/dns.js +192 -0
  32. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  33. package/lib/cloudflare/index.d.ts +2 -1
  34. package/lib/cloudflare/index.js +2 -1
  35. package/lib/cloudflare/r2-rest-state-store.d.ts +125 -0
  36. package/lib/cloudflare/r2-rest-state-store.js +254 -0
  37. package/lib/cloudflare/response.d.ts +12 -0
  38. package/lib/cloudflare/response.js +0 -0
  39. package/lib/cloudflare/static-site-router.js +16 -7
  40. package/lib/cloudflare/static-site.js +3 -0
  41. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  42. package/lib/cloudflare/zone.d.ts +18 -0
  43. package/lib/cloudflare/zone.js +17 -4
  44. package/lib/dns/godaddy.d.ts +7 -0
  45. package/lib/dns/godaddy.js +21 -0
  46. package/lib/dns/import-dns.d.ts +82 -0
  47. package/lib/dns/import-dns.js +114 -0
  48. package/lib/dns/index.d.ts +2 -0
  49. package/lib/dns/index.js +2 -0
  50. package/lib/dns/record.d.ts +88 -0
  51. package/lib/dns/record.js +13 -0
  52. package/lib/fs/copy-file.d.ts +54 -0
  53. package/lib/fs/copy-file.js +63 -0
  54. package/lib/fs/file-system-state-store.d.ts +17 -0
  55. package/lib/fs/file-system-state-store.js +87 -0
  56. package/lib/fs/file.d.ts +13 -0
  57. package/lib/fs/file.js +24 -5
  58. package/lib/fs/folder.d.ts +10 -0
  59. package/lib/fs/folder.js +2 -2
  60. package/lib/fs/index.d.ts +10 -4
  61. package/lib/fs/index.js +10 -4
  62. package/lib/fs/static-astro-file.d.ts +34 -0
  63. package/lib/fs/static-astro-file.js +39 -0
  64. package/lib/fs/static-css-file.d.ts +24 -0
  65. package/lib/fs/static-css-file.js +29 -0
  66. package/lib/fs/static-html-file.d.ts +31 -0
  67. package/lib/fs/static-html-file.js +36 -0
  68. package/lib/fs/{json-file.d.ts → static-json-file.d.ts} +3 -3
  69. package/lib/fs/static-json-file.js +15 -0
  70. package/lib/fs/{text-file.d.ts → static-text-file.d.ts} +2 -2
  71. package/lib/fs/static-text-file.js +8 -0
  72. package/lib/fs/{typescript-file.d.ts → static-typescript-file.d.ts} +3 -3
  73. package/lib/fs/{typescript-file.js → static-typescript-file.js} +3 -2
  74. package/lib/fs/static-vue-file.d.ts +28 -0
  75. package/lib/fs/static-vue-file.js +33 -0
  76. package/lib/fs/static-yaml-file.d.ts +22 -0
  77. package/lib/fs/{yaml-file.js → static-yaml-file.js} +3 -2
  78. package/lib/internal/getting-started.d.ts +21 -0
  79. package/lib/internal/getting-started.js +87 -0
  80. package/lib/internal/index.d.ts +3 -0
  81. package/lib/internal/index.js +3 -0
  82. package/lib/internal/providers.d.ts +27 -0
  83. package/lib/internal/providers.js +172 -0
  84. package/lib/internal/tutorial.d.ts +104 -0
  85. package/lib/internal/tutorial.js +251 -0
  86. package/lib/resource.js +8 -1
  87. package/lib/scope.d.ts +1 -1
  88. package/lib/scope.js +4 -2
  89. package/lib/state.d.ts +1 -16
  90. package/lib/state.js +0 -87
  91. package/lib/test/bun.d.ts +11 -0
  92. package/lib/test/bun.js +36 -8
  93. package/lib/web/astro.d.ts +147 -0
  94. package/lib/web/astro.js +414 -0
  95. package/lib/{shadcn/component.js → web/shadcn-component.js} +2 -9
  96. package/lib/web/shadcn.d.ts +97 -0
  97. package/lib/web/shadcn.js +99 -0
  98. package/lib/web/tailwind.d.ts +64 -0
  99. package/lib/web/tailwind.js +97 -0
  100. package/lib/{vite → web}/vite.d.ts +5 -0
  101. package/lib/{vite → web}/vite.js +44 -44
  102. package/lib/web/vitepress/config.d.ts +10 -0
  103. package/lib/web/vitepress/config.js +31 -0
  104. package/lib/web/vitepress/custom-theme.d.ts +173 -0
  105. package/lib/web/vitepress/custom-theme.js +336 -0
  106. package/lib/{vitepress → web/vitepress}/dependencies.d.ts +2 -2
  107. package/lib/{vitepress → web/vitepress}/dependencies.js +1 -1
  108. package/lib/web/vitepress/home-page.d.ts +246 -0
  109. package/lib/web/vitepress/home-page.js +147 -0
  110. package/lib/{vitepress → web/vitepress}/index.d.ts +2 -0
  111. package/lib/{vitepress → web/vitepress}/index.js +2 -0
  112. package/lib/{vitepress → web/vitepress}/vitepress.d.ts +15 -12
  113. package/lib/{vitepress → web/vitepress}/vitepress.js +26 -44
  114. package/package.json +6 -2
  115. package/src/ai/approve.ts +163 -0
  116. package/src/ai/astro-file.ts +269 -0
  117. package/src/ai/client.ts +60 -21
  118. package/src/ai/css-file.ts +247 -0
  119. package/src/ai/data.ts +81 -19
  120. package/src/ai/document.ts +240 -62
  121. package/src/ai/html-file.ts +244 -0
  122. package/src/ai/index.ts +9 -0
  123. package/src/ai/json-file.ts +325 -0
  124. package/src/ai/review.ts +213 -0
  125. package/src/ai/typescript-file.ts +259 -0
  126. package/src/ai/vue-file.ts +242 -0
  127. package/src/ai/yaml-file.ts +328 -0
  128. package/src/alchemy.ts +18 -7
  129. package/src/apply.ts +8 -9
  130. package/src/cloudflare/dns.ts +344 -0
  131. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  132. package/src/cloudflare/index.ts +2 -1
  133. package/src/cloudflare/r2-rest-state-store.ts +353 -0
  134. package/src/cloudflare/response.ts +9 -0
  135. package/src/cloudflare/static-site-router.ts +18 -8
  136. package/src/cloudflare/static-site.ts +11 -7
  137. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  138. package/src/cloudflare/worker.ts +44 -43
  139. package/src/cloudflare/zone.ts +39 -23
  140. package/src/dns/godaddy.ts +29 -0
  141. package/src/dns/import-dns.ts +213 -0
  142. package/src/dns/index.ts +2 -0
  143. package/src/dns/record.ts +121 -0
  144. package/src/fs/copy-file.ts +115 -0
  145. package/src/fs/file-system-state-store.ts +109 -0
  146. package/src/fs/file.ts +35 -9
  147. package/src/fs/folder.ts +14 -2
  148. package/src/fs/index.ts +10 -4
  149. package/src/fs/static-astro-file.ts +45 -0
  150. package/src/fs/static-css-file.ts +35 -0
  151. package/src/fs/static-html-file.ts +42 -0
  152. package/src/fs/static-json-file.ts +34 -0
  153. package/src/fs/{text-file.ts → static-text-file.ts} +9 -3
  154. package/src/fs/{typescript-file.ts → static-typescript-file.ts} +7 -6
  155. package/src/fs/static-vue-file.ts +39 -0
  156. package/src/fs/static-yaml-file.ts +33 -0
  157. package/src/internal/getting-started.ts +115 -0
  158. package/src/internal/index.ts +3 -0
  159. package/src/internal/providers.ts +241 -0
  160. package/src/internal/tutorial.ts +392 -0
  161. package/src/resource.ts +10 -1
  162. package/src/scope.ts +5 -6
  163. package/src/state.ts +1 -111
  164. package/src/test/bun.ts +60 -10
  165. package/src/web/astro.ts +644 -0
  166. package/src/{shadcn/component.ts → web/shadcn-component.ts} +5 -13
  167. package/src/web/shadcn.ts +219 -0
  168. package/src/web/tailwind.ts +167 -0
  169. package/src/{vite → web}/vite.ts +54 -56
  170. package/src/web/vitepress/config.ts +48 -0
  171. package/src/web/vitepress/custom-theme.ts +514 -0
  172. package/src/{vitepress → web/vitepress}/dependencies.ts +2 -2
  173. package/src/web/vitepress/home-page.ts +375 -0
  174. package/src/{vitepress → web/vitepress}/index.ts +2 -0
  175. package/src/{vitepress → web/vitepress}/vitepress.ts +62 -72
  176. package/lib/cloudflare/state.d.ts +0 -82
  177. package/lib/cloudflare/state.js +0 -108
  178. package/lib/fs/json-file.js +0 -7
  179. package/lib/fs/text-file.js +0 -7
  180. package/lib/fs/yaml-file.d.ts +0 -19
  181. package/lib/internal/docs.d.ts +0 -5
  182. package/lib/internal/docs.js +0 -287
  183. package/lib/shadcn/index.d.ts +0 -1
  184. package/lib/shadcn/index.js +0 -1
  185. package/lib/vite/index.d.ts +0 -1
  186. package/lib/vite/index.js +0 -1
  187. package/lib/vitepress/home-page.d.ts +0 -133
  188. package/lib/vitepress/home-page.js +0 -11
  189. package/src/cloudflare/state.ts +0 -138
  190. package/src/fs/json-file.ts +0 -23
  191. package/src/fs/yaml-file.ts +0 -26
  192. package/src/internal/docs.ts +0 -338
  193. package/src/shadcn/index.ts +0 -1
  194. package/src/vite/index.ts +0 -1
  195. package/src/vitepress/home-page.ts +0 -166
  196. /package/lib/{shadcn/component.d.ts → web/shadcn-component.d.ts} +0 -0
  197. /package/src/{vitepress → web/vitepress}/index.md +0 -0
@@ -0,0 +1,169 @@
1
+ import type { JsonSchema, Type, type } from "arktype";
2
+ import type { Context } from "../context";
3
+ import { Resource } from "../resource";
4
+ import type { Secret } from "../secret";
5
+ import { type ModelConfig } from "./client";
6
+ /**
7
+ * Properties for creating or updating a YAMLFile
8
+ */
9
+ export interface YAMLFileProps<T extends Type<any, any> | undefined = undefined> {
10
+ /**
11
+ * Path to the YAML file
12
+ */
13
+ path: string;
14
+ /**
15
+ * Optional ArkType schema to validate and structure the generated YAML
16
+ * When provided, the resource will use generateObject with schema validation
17
+ * When not provided, it will extract YAML from between ```yaml fences
18
+ */
19
+ schema?: T;
20
+ /**
21
+ * Base URL for the OpenAI API
22
+ * @default 'https://api.openai.com/v1'
23
+ */
24
+ baseURL?: string;
25
+ /**
26
+ * Prompt for generating content
27
+ * Use alchemy template literals to include file context:
28
+ * @example
29
+ * prompt: await alchemy`
30
+ * Generate a YAML configuration for:
31
+ * ${alchemy.file("src/serverless.js")}
32
+ * `
33
+ */
34
+ prompt: string;
35
+ /**
36
+ * System prompt for the model
37
+ * This is used to provide instructions to the model about how to format the response
38
+ * @default Depends on whether schema is provided
39
+ */
40
+ system?: string;
41
+ /**
42
+ * OpenAI API key to use for generating content
43
+ * If not provided, will use OPENAI_API_KEY environment variable
44
+ */
45
+ apiKey?: Secret;
46
+ /**
47
+ * Model configuration
48
+ */
49
+ model?: ModelConfig;
50
+ /**
51
+ * Temperature for controlling randomness in generation.
52
+ * Higher values (e.g., 0.8) make output more random,
53
+ * lower values (e.g., 0.2) make it more deterministic.
54
+ * @default 0.7
55
+ */
56
+ temperature?: number;
57
+ }
58
+ /**
59
+ * A YAML file that can be created, updated, and deleted
60
+ */
61
+ export interface YAMLFile<T = any> extends Omit<YAMLFileProps, "schema">, Resource<"ai::YAMLFile"> {
62
+ /**
63
+ * Content of the YAML file as a string
64
+ */
65
+ content: string;
66
+ /**
67
+ * Parsed YAML object
68
+ */
69
+ yaml: T;
70
+ /**
71
+ * Schema used to validate the YAML (if provided)
72
+ */
73
+ schema?: JsonSchema;
74
+ /**
75
+ * Time at which the file was created
76
+ */
77
+ createdAt: number;
78
+ /**
79
+ * Time at which the file was last updated
80
+ */
81
+ updatedAt: number;
82
+ }
83
+ /**
84
+ * Resource for generating YAML files using AI models.
85
+ * Can operate in two modes:
86
+ * 1. With schema: Uses generateObject with type validation, then converts to YAML
87
+ * 2. Without schema: Extracts YAML from between ```yaml fences
88
+ *
89
+ * @example
90
+ * // Generate a serverless configuration file
91
+ * const serverlessConfig = await YAMLFile("serverless-config", {
92
+ * path: "./serverless.yml",
93
+ * prompt: await alchemy`
94
+ * Generate a serverless.yml configuration for an AWS Lambda API with:
95
+ * - A service name "user-api"
96
+ * - Node.js 16.x runtime
97
+ * - Three functions: createUser, getUser, and listUsers
98
+ * - API Gateway endpoints for each function
99
+ * - DynamoDB table for users
100
+ * - IAM permissions for DynamoDB access
101
+ * `,
102
+ * model: {
103
+ * id: "gpt-4o",
104
+ * provider: "openai"
105
+ * }
106
+ * });
107
+ *
108
+ * @example
109
+ * // Generate YAML with schema validation
110
+ * import { type } from "arktype";
111
+ *
112
+ * const k8sConfigSchema = type({
113
+ * apiVersion: "string",
114
+ * kind: "string",
115
+ * metadata: {
116
+ * name: "string",
117
+ * namespace: "string?",
118
+ * labels: "Record<string, string>?"
119
+ * },
120
+ * spec: {
121
+ * replicas: "number",
122
+ * selector: {
123
+ * matchLabels: "Record<string, string>"
124
+ * },
125
+ * template: {
126
+ * metadata: {
127
+ * labels: "Record<string, string>"
128
+ * },
129
+ * spec: {
130
+ * containers: [{
131
+ * name: "string",
132
+ * image: "string",
133
+ * ports: [{
134
+ * containerPort: "number"
135
+ * }]
136
+ * }]
137
+ * }
138
+ * }
139
+ * }
140
+ * });
141
+ *
142
+ * const deployment = await YAMLFile("k8s-deployment", {
143
+ * path: "./kubernetes/deployment.yaml",
144
+ * schema: k8sConfigSchema,
145
+ * prompt: "Generate a Kubernetes deployment for a web application named 'frontend' with 3 replicas using the nginx:latest image and exposing port 80",
146
+ * temperature: 0.2
147
+ * });
148
+ *
149
+ * @example
150
+ * // Generate GitHub Actions workflow with custom system prompt
151
+ * const workflow = await YAMLFile("github-workflow", {
152
+ * path: "./.github/workflows/ci.yml",
153
+ * prompt: await alchemy`
154
+ * Create a GitHub Actions workflow for a Node.js project that:
155
+ * - Runs on push to main and pull requests
156
+ * - Sets up Node.js 18
157
+ * - Installs dependencies with npm
158
+ * - Runs linting and tests
159
+ * - Builds the project
160
+ * - Deploys to GitHub Pages on success (main branch only)
161
+ * `,
162
+ * system: "You are a DevOps expert specializing in GitHub Actions workflows. Create a single YAML file inside ```yaml fences with no additional text. Follow GitHub Actions best practices and use proper YAML syntax.",
163
+ * model: {
164
+ * id: "claude-3-opus-20240229",
165
+ * provider: "anthropic"
166
+ * }
167
+ * });
168
+ */
169
+ export declare const YAMLFile: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | (<const T extends Type<any, any> | undefined = undefined>(this: Context<YAMLFile<T extends Type<any, any> ? type.infer<T> : any>>, id: string, props: YAMLFileProps<T>) => Promise<YAMLFile<T extends Type<any, any> ? type.infer<T> : any>>);
@@ -0,0 +1,200 @@
1
+ import { generateObject, generateText } from "ai";
2
+ import { StaticYamlFile } from "../fs/static-yaml-file";
3
+ import { Resource } from "../resource";
4
+ import { ark } from "./ark";
5
+ import { createModel } from "./client";
6
+ /**
7
+ * Default system prompt for YAML file generation without schema
8
+ */
9
+ const DEFAULT_YAML_SYSTEM_PROMPT = "You are a YAML generator. Create valid YAML based on the user's requirements. Your response MUST include only YAML inside ```yaml fences. Do not include any other text, explanations, or multiple code blocks. Use standard YAML syntax with proper indentation. Use quotes around strings that contain special characters when necessary.";
10
+ /**
11
+ * Resource for generating YAML files using AI models.
12
+ * Can operate in two modes:
13
+ * 1. With schema: Uses generateObject with type validation, then converts to YAML
14
+ * 2. Without schema: Extracts YAML from between ```yaml fences
15
+ *
16
+ * @example
17
+ * // Generate a serverless configuration file
18
+ * const serverlessConfig = await YAMLFile("serverless-config", {
19
+ * path: "./serverless.yml",
20
+ * prompt: await alchemy`
21
+ * Generate a serverless.yml configuration for an AWS Lambda API with:
22
+ * - A service name "user-api"
23
+ * - Node.js 16.x runtime
24
+ * - Three functions: createUser, getUser, and listUsers
25
+ * - API Gateway endpoints for each function
26
+ * - DynamoDB table for users
27
+ * - IAM permissions for DynamoDB access
28
+ * `,
29
+ * model: {
30
+ * id: "gpt-4o",
31
+ * provider: "openai"
32
+ * }
33
+ * });
34
+ *
35
+ * @example
36
+ * // Generate YAML with schema validation
37
+ * import { type } from "arktype";
38
+ *
39
+ * const k8sConfigSchema = type({
40
+ * apiVersion: "string",
41
+ * kind: "string",
42
+ * metadata: {
43
+ * name: "string",
44
+ * namespace: "string?",
45
+ * labels: "Record<string, string>?"
46
+ * },
47
+ * spec: {
48
+ * replicas: "number",
49
+ * selector: {
50
+ * matchLabels: "Record<string, string>"
51
+ * },
52
+ * template: {
53
+ * metadata: {
54
+ * labels: "Record<string, string>"
55
+ * },
56
+ * spec: {
57
+ * containers: [{
58
+ * name: "string",
59
+ * image: "string",
60
+ * ports: [{
61
+ * containerPort: "number"
62
+ * }]
63
+ * }]
64
+ * }
65
+ * }
66
+ * }
67
+ * });
68
+ *
69
+ * const deployment = await YAMLFile("k8s-deployment", {
70
+ * path: "./kubernetes/deployment.yaml",
71
+ * schema: k8sConfigSchema,
72
+ * prompt: "Generate a Kubernetes deployment for a web application named 'frontend' with 3 replicas using the nginx:latest image and exposing port 80",
73
+ * temperature: 0.2
74
+ * });
75
+ *
76
+ * @example
77
+ * // Generate GitHub Actions workflow with custom system prompt
78
+ * const workflow = await YAMLFile("github-workflow", {
79
+ * path: "./.github/workflows/ci.yml",
80
+ * prompt: await alchemy`
81
+ * Create a GitHub Actions workflow for a Node.js project that:
82
+ * - Runs on push to main and pull requests
83
+ * - Sets up Node.js 18
84
+ * - Installs dependencies with npm
85
+ * - Runs linting and tests
86
+ * - Builds the project
87
+ * - Deploys to GitHub Pages on success (main branch only)
88
+ * `,
89
+ * system: "You are a DevOps expert specializing in GitHub Actions workflows. Create a single YAML file inside ```yaml fences with no additional text. Follow GitHub Actions best practices and use proper YAML syntax.",
90
+ * model: {
91
+ * id: "claude-3-opus-20240229",
92
+ * provider: "anthropic"
93
+ * }
94
+ * });
95
+ */
96
+ export const YAMLFile = Resource("ai::YAMLFile", async function (id, props) {
97
+ // Handle deletion phase
98
+ if (this.phase === "delete") {
99
+ return this.destroy();
100
+ }
101
+ let yamlContent;
102
+ let yamlObject;
103
+ // Check if schema is provided
104
+ if (props.schema) {
105
+ // Use schema-based generation
106
+ const { object } = await generateObject({
107
+ model: createModel(props),
108
+ schema: ark.schema(props.schema),
109
+ providerOptions: props.model?.options,
110
+ system: props.system ||
111
+ "Generate a valid object based on the provided requirements.",
112
+ prompt: props.prompt,
113
+ ...(props.temperature === undefined
114
+ ? {}
115
+ : { temperature: props.temperature }),
116
+ });
117
+ yamlObject = object;
118
+ // Let StaticYamlFile handle the YAML conversion
119
+ const file = await StaticYamlFile("file", props.path, yamlObject);
120
+ yamlContent = file.content;
121
+ }
122
+ else {
123
+ // Use fence-based extraction
124
+ // Use provided system prompt or default
125
+ const system = props.system || DEFAULT_YAML_SYSTEM_PROMPT;
126
+ // Generate initial content
127
+ const { text } = await generateText({
128
+ model: createModel(props),
129
+ prompt: props.prompt,
130
+ system,
131
+ providerOptions: props.model?.options,
132
+ ...(props.temperature === undefined
133
+ ? {}
134
+ : { temperature: props.temperature }),
135
+ });
136
+ // Extract and validate YAML content
137
+ let { content, error } = await extractYAMLContent(text);
138
+ // Re-prompt if there are validation errors
139
+ if (error) {
140
+ const errorSystem = `${system}\n\nERROR: ${error}\n\nPlease try again and ensure your response contains exactly one YAML block inside \`\`\`yaml fences.`;
141
+ const { text: retryText } = await generateText({
142
+ model: createModel(props),
143
+ prompt: props.prompt,
144
+ system: errorSystem,
145
+ providerOptions: props.model?.options,
146
+ ...(props.temperature === undefined
147
+ ? {}
148
+ : { temperature: props.temperature }),
149
+ });
150
+ const retryResult = await extractYAMLContent(retryText);
151
+ if (retryResult.error) {
152
+ throw new Error(`Failed to generate valid YAML: ${retryResult.error}`);
153
+ }
154
+ content = retryResult.content;
155
+ }
156
+ yamlContent = content;
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);
162
+ }
163
+ // Return the resource
164
+ return this({
165
+ ...props,
166
+ schema: props.schema,
167
+ content: yamlContent,
168
+ yaml: yamlObject,
169
+ createdAt: Date.now(),
170
+ updatedAt: Date.now(),
171
+ });
172
+ });
173
+ /**
174
+ * Extracts YAML content from between ```yaml fences
175
+ * Validates that exactly one YAML code block exists
176
+ *
177
+ * @param text The text to extract YAML from
178
+ * @returns The extracted YAML or error message
179
+ */
180
+ async function extractYAMLContent(text) {
181
+ // Check for yaml or yml fence blocks
182
+ const yamlCodeRegex = /```(yaml|yml)\s*([\s\S]*?)```/g;
183
+ const matches = Array.from(text.matchAll(yamlCodeRegex));
184
+ if (matches.length === 0) {
185
+ return {
186
+ content: "",
187
+ error: "No YAML code block found in the response. Please include your YAML within ```yaml fences.",
188
+ };
189
+ }
190
+ if (matches.length > 1) {
191
+ return {
192
+ content: "",
193
+ error: "Multiple YAML code blocks found in the response. Please provide exactly one YAML block within ```yaml fences.",
194
+ };
195
+ }
196
+ const content = matches[0][2].trim();
197
+ // We don't validate YAML parsing here because js-yaml might not be available
198
+ // Validation will happen at usage time if needed
199
+ return { content };
200
+ }
package/lib/alchemy.js CHANGED
@@ -33,6 +33,17 @@ function _alchemy(...args) {
33
33
  if (typeof value === "string") {
34
34
  return indent + value;
35
35
  }
36
+ else if (value === null) {
37
+ return "null";
38
+ }
39
+ else if (value === undefined) {
40
+ return "undefined";
41
+ }
42
+ else if (typeof value === "number" ||
43
+ typeof value === "boolean" ||
44
+ typeof value === "bigint") {
45
+ return value.toString();
46
+ }
36
47
  else if (value instanceof Promise) {
37
48
  return resolve(await value);
38
49
  }
@@ -55,7 +66,8 @@ function _alchemy(...args) {
55
66
  }
56
67
  else {
57
68
  // TODO: support other types
58
- throw new Error(`Unsupported value type: ${JSON.stringify(value)}`);
69
+ console.log(value);
70
+ throw new Error(`Unsupported value type: ${value}`);
59
71
  }
60
72
  }));
61
73
  // Construct the string template by joining template parts with interpolated values
@@ -104,7 +116,10 @@ _alchemy.secret = secret;
104
116
  * apiKey: alchemy.secret(process.env.API_KEY)
105
117
  * });
106
118
  */
107
- function scope(id, options) {
119
+ function scope(id, options
120
+ // TODO: maybe we want to allow using _ = await alchemy.scope(import.meta)
121
+ // | [meta: ImportMeta]
122
+ ) {
108
123
  const scope = new Scope({
109
124
  ...options,
110
125
  appName: options?.appName,
package/lib/apply.js CHANGED
@@ -46,7 +46,7 @@ export async function apply(resource, props, options) {
46
46
  }), null, 2);
47
47
  if (oldProps === newProps && alwaysUpdate !== true) {
48
48
  if (!quiet) {
49
- console.log(`Skip: "${resource.FQN}" (no changes)`);
49
+ // console.log(`Skip: "${resource.FQN}" (no changes)`);
50
50
  }
51
51
  return state.output;
52
52
  }
@@ -0,0 +1,88 @@
1
+ import type { Context } from "../context";
2
+ import type { DnsRecord as BaseDnsRecord, DnsRecordType, DnsRecordWithMetadata } from "../dns/record";
3
+ import { Resource } from "../resource";
4
+ /**
5
+ * Properties for a DNS record
6
+ */
7
+ export interface DnsRecordProps extends Omit<BaseDnsRecord, "type"> {
8
+ /**
9
+ * Record type (A, AAAA, CNAME, etc.)
10
+ */
11
+ type: DnsRecordType;
12
+ }
13
+ /**
14
+ * Output returned after DNS record creation/update
15
+ */
16
+ export interface DnsRecord extends DnsRecordWithMetadata {
17
+ }
18
+ /**
19
+ * Properties for managing multiple DNS records
20
+ */
21
+ export interface DnsRecordsProps {
22
+ /**
23
+ * Zone ID or domain name where records will be created
24
+ */
25
+ zoneId: string;
26
+ /**
27
+ * Array of DNS records to manage
28
+ */
29
+ records: DnsRecordProps[];
30
+ }
31
+ /**
32
+ * Output returned after DNS records creation/update
33
+ */
34
+ export interface DnsRecords extends Resource<"cloudflare::DnsRecords"> {
35
+ /**
36
+ * Zone ID where records are created
37
+ */
38
+ zoneId: string;
39
+ /**
40
+ * Array of created/updated DNS records
41
+ */
42
+ records: DnsRecord[];
43
+ }
44
+ /**
45
+ * Manages a batch of DNS records in a Cloudflare zone.
46
+ * Supports creating, updating, and deleting multiple records at once.
47
+ *
48
+ * @example
49
+ * // Create multiple A and CNAME records
50
+ * const dnsRecords = await DnsRecords("example.com-dns", {
51
+ * zone: "example.com",
52
+ * records: [
53
+ * {
54
+ * name: "www.example.com",
55
+ * type: "A",
56
+ * content: "192.0.2.1",
57
+ * proxied: true
58
+ * },
59
+ * {
60
+ * name: "blog.example.com",
61
+ * type: "CNAME",
62
+ * content: "www.example.com",
63
+ * proxied: true
64
+ * }
65
+ * ]
66
+ * });
67
+ *
68
+ * @example
69
+ * // Create MX records for email routing
70
+ * const emailRecords = await DnsRecords("example.com-email", {
71
+ * zone: "example.com",
72
+ * records: [
73
+ * {
74
+ * name: "example.com",
75
+ * type: "MX",
76
+ * content: "aspmx.l.google.com",
77
+ * priority: 1
78
+ * },
79
+ * {
80
+ * name: "example.com",
81
+ * type: "MX",
82
+ * content: "alt1.aspmx.l.google.com",
83
+ * priority: 5
84
+ * }
85
+ * ]
86
+ * });
87
+ */
88
+ export declare const DnsRecords: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<DnsRecords>, id: string, props: DnsRecordsProps) => Promise<DnsRecords>);
@@ -0,0 +1,192 @@
1
+ import { Resource } from "../resource";
2
+ import { createCloudflareApi } from "./api";
3
+ /**
4
+ * Manages a batch of DNS records in a Cloudflare zone.
5
+ * Supports creating, updating, and deleting multiple records at once.
6
+ *
7
+ * @example
8
+ * // Create multiple A and CNAME records
9
+ * const dnsRecords = await DnsRecords("example.com-dns", {
10
+ * zone: "example.com",
11
+ * records: [
12
+ * {
13
+ * name: "www.example.com",
14
+ * type: "A",
15
+ * content: "192.0.2.1",
16
+ * proxied: true
17
+ * },
18
+ * {
19
+ * name: "blog.example.com",
20
+ * type: "CNAME",
21
+ * content: "www.example.com",
22
+ * proxied: true
23
+ * }
24
+ * ]
25
+ * });
26
+ *
27
+ * @example
28
+ * // Create MX records for email routing
29
+ * const emailRecords = await DnsRecords("example.com-email", {
30
+ * zone: "example.com",
31
+ * records: [
32
+ * {
33
+ * name: "example.com",
34
+ * type: "MX",
35
+ * content: "aspmx.l.google.com",
36
+ * priority: 1
37
+ * },
38
+ * {
39
+ * name: "example.com",
40
+ * type: "MX",
41
+ * content: "alt1.aspmx.l.google.com",
42
+ * priority: 5
43
+ * }
44
+ * ]
45
+ * });
46
+ */
47
+ export const DnsRecords = Resource("cloudflare::DnsRecords", async function (id, props) {
48
+ // Create Cloudflare API client
49
+ const api = await createCloudflareApi();
50
+ // Get zone ID if domain name was provided
51
+ const zoneId = props.zoneId;
52
+ if (this.phase === "delete") {
53
+ if (this.output?.records) {
54
+ // Delete all existing records
55
+ await Promise.all(this.output.records.map(async (record) => {
56
+ try {
57
+ const response = await api.delete(`/zones/${zoneId}/dns_records/${record.id}`);
58
+ if (!response.ok && response.status !== 404) {
59
+ console.error(`Failed to delete DNS record ${record.name}: ${response.statusText}`);
60
+ }
61
+ }
62
+ catch (error) {
63
+ console.error(`Error deleting DNS record ${record.name}:`, error);
64
+ }
65
+ }));
66
+ }
67
+ return this.destroy();
68
+ }
69
+ if (this.phase === "update" && this.output?.records) {
70
+ // Get current records to compare with desired state
71
+ const currentRecords = this.output.records;
72
+ const desiredRecords = props.records;
73
+ // Find records to delete (exist in current but not in desired)
74
+ const recordsToDelete = currentRecords.filter((current) => !desiredRecords.some((desired) => desired.name === current.name && desired.type === current.type));
75
+ // Delete orphaned records
76
+ await Promise.all(recordsToDelete.map(async (record) => {
77
+ try {
78
+ const response = await api.delete(`/zones/${zoneId}/dns_records/${record.id}`);
79
+ if (!response.ok && response.status !== 404) {
80
+ console.error(`Failed to delete DNS record ${record.name}: ${response.statusText}`);
81
+ }
82
+ }
83
+ catch (error) {
84
+ console.error(`Error deleting DNS record ${record.name}:`, error);
85
+ }
86
+ }));
87
+ // Update or create records
88
+ const updatedRecords = await Promise.all(desiredRecords.map(async (desired) => {
89
+ // Find matching existing record
90
+ const existing = currentRecords.find((current) => current.name === desired.name && current.type === desired.type);
91
+ if (existing) {
92
+ // Update if content or other properties changed
93
+ if (existing.content !== desired.content ||
94
+ existing.ttl !== (desired.ttl || 1) ||
95
+ existing.proxied !== (desired.proxied || false) ||
96
+ existing.priority !== desired.priority ||
97
+ existing.comment !== desired.comment) {
98
+ return createOrUpdateRecord(api, zoneId, desired, existing.id);
99
+ }
100
+ return existing;
101
+ }
102
+ else {
103
+ // Create new record
104
+ return createOrUpdateRecord(api, zoneId, desired);
105
+ }
106
+ }));
107
+ return this({
108
+ zoneId,
109
+ records: updatedRecords,
110
+ });
111
+ }
112
+ // Create new records
113
+ const uniqueRecords = props.records.reduce((acc, record) => {
114
+ const key = `${record.name}-${record.type}`;
115
+ acc[key] = record;
116
+ return acc;
117
+ }, {});
118
+ const createdRecords = await Promise.all(Object.values(uniqueRecords).map(async (record) => {
119
+ // First check if record exists
120
+ const listResponse = await api.get(`/zones/${zoneId}/dns_records?type=${record.type}&name=${record.name}`);
121
+ if (!listResponse.ok) {
122
+ throw new Error(`Failed to check existing DNS records: ${listResponse.statusText}`);
123
+ }
124
+ const listResult = (await listResponse.json());
125
+ const existingRecord = listResult.result[0];
126
+ return createOrUpdateRecord(api, zoneId, record, existingRecord?.id);
127
+ }));
128
+ return this({
129
+ zoneId,
130
+ records: createdRecords,
131
+ });
132
+ });
133
+ /**
134
+ * Create or update a DNS record
135
+ */
136
+ async function createOrUpdateRecord(api, zoneId, record, existingId) {
137
+ const payload = getRecordPayload(record);
138
+ const response = await (existingId
139
+ ? api.put(`/zones/${zoneId}/dns_records/${existingId}`, payload)
140
+ : api.post(`/zones/${zoneId}/dns_records`, payload));
141
+ if (!response.ok) {
142
+ const errorBody = await response.text();
143
+ // If it's an update operation and the record doesn't exist, fall back to creation
144
+ if (existingId && response.status === 404) {
145
+ try {
146
+ const createResponse = await api.post(`/zones/${zoneId}/dns_records`, payload);
147
+ if (createResponse.ok) {
148
+ return convertCloudflareRecord((await createResponse.json()).result, zoneId);
149
+ }
150
+ }
151
+ catch (err) {
152
+ // Fall through to the original error
153
+ }
154
+ }
155
+ throw new Error(`Failed to ${existingId ? "update" : "create"} DNS record ${record.name}: ${response.statusText}\nResponse: ${errorBody}`);
156
+ }
157
+ const result = (await response.json());
158
+ return convertCloudflareRecord(result.result, zoneId);
159
+ }
160
+ /**
161
+ * Get the record payload for create/update operations
162
+ */
163
+ function getRecordPayload(record) {
164
+ return {
165
+ type: record.type,
166
+ name: record.name,
167
+ content: record.content,
168
+ ttl: record.ttl || 1,
169
+ proxied: record.proxied || false,
170
+ priority: record.priority,
171
+ comment: record.comment,
172
+ };
173
+ }
174
+ /**
175
+ * Convert a Cloudflare DNS record response to our DnsRecord type
176
+ */
177
+ function convertCloudflareRecord(record, zoneId) {
178
+ return {
179
+ id: record.id,
180
+ name: record.name,
181
+ type: record.type,
182
+ content: record.content,
183
+ ttl: record.ttl,
184
+ proxied: record.proxied,
185
+ priority: record.priority,
186
+ comment: record.comment,
187
+ tags: record.tags,
188
+ createdAt: new Date(record.created_on).getTime(),
189
+ modifiedAt: new Date(record.modified_on).getTime(),
190
+ zoneId,
191
+ };
192
+ }