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,172 @@
1
+ import { type } from "arktype";
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ import { Data, Document } from "../ai";
5
+ import { alchemy } from "../alchemy";
6
+ import { Folder } from "../fs";
7
+ export async function AlchemyProviderDocs({ srcDir, outDir, filter, }) {
8
+ outDir = typeof outDir === "string" ? outDir : outDir.path;
9
+ const exclude = [
10
+ "util",
11
+ "test",
12
+ "vitepress",
13
+ "vite",
14
+ "shadcn",
15
+ "internal",
16
+ "web",
17
+ ];
18
+ // Get all folders in the alchemy/src directory
19
+ let providers = (await fs.readdir(srcDir, {
20
+ withFileTypes: true,
21
+ }))
22
+ .filter((dirent) => dirent.isDirectory() && !exclude.includes(dirent.name))
23
+ .map((dirent) => path.join(dirent.parentPath, dirent.name));
24
+ // For each provider, list all files
25
+ if (filter === false) {
26
+ return [];
27
+ }
28
+ else if (typeof filter === "number") {
29
+ providers = providers.slice(0, filter);
30
+ }
31
+ return await Promise.all(providers.map(async (provider) => {
32
+ const providerName = path.basename(provider);
33
+ const files = (await fs.readdir(path.resolve(provider), {
34
+ withFileTypes: true,
35
+ }))
36
+ .filter((dirent) => dirent.isFile())
37
+ .map((dirent) => path.relative(process.cwd(), path.resolve(provider, dirent.name)))
38
+ .filter((file) => file.endsWith(".ts") && !file.endsWith("index.ts"));
39
+ const { object: { groups }, } = await Data(`docs/${providerName}`, {
40
+ model: {
41
+ id: "o3-mini",
42
+ provider: "openai",
43
+ options: {
44
+ reasoningEffort: "high",
45
+ },
46
+ },
47
+ temperature: 0.1,
48
+ schema: type({
49
+ groups: type({
50
+ title: type("string").describe("The title of the group, should be the Resource Name exactly as it's defined in code (const ResourceName translates to 'Resource Name') without spaces, e.g. Bucket or Static Site."),
51
+ filename: type("string").describe("The filename of the Resource's Document, e.g. bucket.md or static-site.md"),
52
+ category: type("'Resource'|'Client'|'Utility'|'Types'").describe("The classification of the Resource's Document, one of: Resource, Client, Utility, or Types."),
53
+ }).array(),
54
+ }),
55
+ system: await alchemy `
56
+ You are a technical writer tasked with identifying the distinct documents that need to be written for a document group (folder) in a documentation site.
57
+ You will be provided with a list of documents and instructions on how to classify them.
58
+ Each document has a title, file name, and category.
59
+ `,
60
+ prompt: await alchemy `
61
+ Identify and classify the documents that need to be written for the '${provider}' Service's Alchemy Resources.
62
+ For background knowledge on Alchemy, see ${alchemy.file("./README.md")}.
63
+ For background knowledge on the structure of an Alchemy Resource, see ${alchemy.file("./.cursorrules")}.
64
+
65
+ The ${provider} Service has the following resources:
66
+ ${alchemy.files(files)}
67
+
68
+ A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...).
69
+ A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
70
+ A file is considered a "Utility" if it contains utility functions that are not resources or clients.
71
+ A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
72
+
73
+ The title should be simply the name of the resource's const in code (with spaces added in between each word), e.g. "Bucket" or "Function", except with spaces, e.g. "Static Site" for "const StaticSite". Maintain all other casing.
74
+
75
+ // "Resource Name"
76
+ const ResourceName = Resource(...)
77
+ `,
78
+ });
79
+ // console.log(groups);
80
+ const providerDocsDir = (await Folder(path.join(outDir, providerName)))
81
+ .path;
82
+ const documents = await Promise.allSettled(groups
83
+ .filter((g) => g.category === "Resource")
84
+ .map(async (g) => Document(`docs/${providerName}/${g.title}`, {
85
+ title: g.title,
86
+ path: path.join(providerDocsDir, `${g.filename.replace(".ts", "").replace(".md", "")}.md`),
87
+ model: {
88
+ id: "claude-3-5-sonnet-latest",
89
+ provider: "anthropic",
90
+ // options: {
91
+ // reasoningEffort: "high",
92
+ // },
93
+ },
94
+ prompt: await alchemy `
95
+ You are a technical writer writing API documentation for an Alchemy IaC Resource.
96
+ See ${alchemy.file("./README.md")} to understand the overview of Alchemy.
97
+ See ${alchemy.file("./.cursorrules")} to better understand the structure and convention of an Alchemy Resource.
98
+
99
+ Relevant files for the ${providerName} Service:
100
+ ${alchemy.files(files)}
101
+
102
+ Write concise documentation for the "${g.title}" Resource.
103
+
104
+ > [!CAUTION]
105
+ > Avoid the temptation to over explain or over describe. Focus on concise, simple, high value snippets. One heading and 0-1 descriptions per snippet.
106
+
107
+ > [!TIP]
108
+ > Make sure the examples follow a natural progression from the minimal example to logical next steps of how the Resource might be used.
109
+
110
+ Each document must follow the following format:
111
+
112
+ # ${g.title}
113
+
114
+ (simple description with an external link to the provider's website)
115
+ e.g.
116
+ The Efs component lets you add [Amazon Elastic File System (EFS)](https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html) to your app.
117
+
118
+ # Minimal Example
119
+
120
+ (brief 1-2 sentences of what it does)
121
+
122
+ \`\`\`ts
123
+ import { ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
124
+
125
+ (example)
126
+ \`\`\`
127
+
128
+
129
+ # Create the ${g.title}
130
+
131
+ \`\`\`ts
132
+ import { ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
133
+
134
+ (example)
135
+ \`\`\`
136
+
137
+ ${providerName === "cloudflare"
138
+ ? await alchemy `# Bind to a Worker
139
+ (if it is a Cloudflare Resource)
140
+
141
+ \`\`\`ts
142
+ import { Worker, ${g.title.replaceAll(" ", "")} } from "alchemy/${providerName}";
143
+
144
+ const myResource = await ${g.title.replaceAll(" ", "")}("my-resource", {
145
+ // ...
146
+ });
147
+
148
+ await Worker("my-worker", {
149
+ name: "my-worker",
150
+ script: "console.log('Hello, world!')",
151
+ bindings: {
152
+ myResource,
153
+ },
154
+ });
155
+ \`\`\``
156
+ : ""}
157
+ `,
158
+ })));
159
+ // Unwrap all documents, fail if any rejected
160
+ const results = await Promise.all(documents.map((r) => {
161
+ if (r.status === "rejected") {
162
+ throw r.reason;
163
+ }
164
+ return r.value;
165
+ }));
166
+ return {
167
+ dir: providerDocsDir,
168
+ provider: providerName,
169
+ documents: results,
170
+ };
171
+ }));
172
+ }
@@ -0,0 +1,104 @@
1
+ import type { CoreMessage } from "ai";
2
+ import { Document } from "../ai";
3
+ import type { ModelConfig } from "../ai/client";
4
+ import type { Context } from "../context";
5
+ import type { Folder } from "../fs";
6
+ import { Resource } from "../resource";
7
+ /**
8
+ * Properties for creating or updating a Tutorial
9
+ */
10
+ export interface TutorialProps {
11
+ /**
12
+ * The output directory for the tutorial document.
13
+ */
14
+ path: string | Folder;
15
+ /**
16
+ * The title of the tutorial.
17
+ */
18
+ title: string;
19
+ /**
20
+ * The prompt to use for generating the tutorial.
21
+ * This should include specific details about the tool or framework.
22
+ */
23
+ prompt: string;
24
+ /**
25
+ * Optional difficulty level for the tutorial.
26
+ * Defaults to "beginner".
27
+ */
28
+ difficulty?: "beginner" | "intermediate" | "advanced";
29
+ /**
30
+ * Optional estimated time to complete the tutorial in minutes.
31
+ * Defaults to 30.
32
+ */
33
+ estimatedTime?: number;
34
+ /**
35
+ * Optional model configuration for generating the tutorial.
36
+ * Defaults to Claude 3.5 Sonnet.
37
+ */
38
+ model?: ModelConfig;
39
+ /**
40
+ * Maximum number of review iterations.
41
+ * Defaults to 3.
42
+ */
43
+ maxIterations?: number;
44
+ /**
45
+ * Initial message history for the conversation.
46
+ * If not provided, a new conversation will be started.
47
+ */
48
+ messages?: CoreMessage[];
49
+ }
50
+ /**
51
+ * Output returned after Tutorial creation/update
52
+ */
53
+ export interface Tutorial extends TutorialProps, Resource<"docs::Tutorial"> {
54
+ /**
55
+ * The Document resource
56
+ */
57
+ document: Document;
58
+ /**
59
+ * The content of the tutorial
60
+ */
61
+ content: string;
62
+ /**
63
+ * Message history from the conversation.
64
+ */
65
+ messages: CoreMessage[];
66
+ /**
67
+ * Time at which the tutorial was created
68
+ */
69
+ createdAt: number;
70
+ /**
71
+ * Time at which the tutorial was last updated
72
+ */
73
+ updatedAt: number;
74
+ }
75
+ /**
76
+ * Creates a tutorial document with iterative review and improvement
77
+ *
78
+ * @example
79
+ * // Create a basic tutorial
80
+ * const tutorial = await Tutorial("getting-started", {
81
+ * path: "docs/tutorials/getting-started",
82
+ * title: "Getting Started with Alchemy",
83
+ * prompt: "Create a tutorial for getting started with the Alchemy framework",
84
+ * difficulty: "beginner",
85
+ * estimatedTime: 30
86
+ * });
87
+ *
88
+ * @example
89
+ * // Create an advanced tutorial with custom model and continue from existing messages
90
+ * const advancedTutorial = await Tutorial("advanced-features", {
91
+ * path: "docs/tutorials/advanced-features",
92
+ * title: "Advanced Alchemy Features",
93
+ * prompt: "Create a tutorial covering advanced features of the Alchemy framework",
94
+ * difficulty: "advanced",
95
+ * estimatedTime: 60,
96
+ * model: {
97
+ * id: "claude-3-7-sonnet-latest",
98
+ * provider: "anthropic"
99
+ * },
100
+ * maxIterations: 5,
101
+ * messages: basicTutorial.messages
102
+ * });
103
+ */
104
+ export declare const Tutorial: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<Tutorial>, id: string, props: TutorialProps) => Promise<Tutorial>);
@@ -0,0 +1,251 @@
1
+ import { Approve, Document, Review } from "../ai";
2
+ import { alchemy } from "../alchemy";
3
+ import { Resource } from "../resource";
4
+ /**
5
+ * Creates a tutorial document with iterative review and improvement
6
+ *
7
+ * @example
8
+ * // Create a basic tutorial
9
+ * const tutorial = await Tutorial("getting-started", {
10
+ * path: "docs/tutorials/getting-started",
11
+ * title: "Getting Started with Alchemy",
12
+ * prompt: "Create a tutorial for getting started with the Alchemy framework",
13
+ * difficulty: "beginner",
14
+ * estimatedTime: 30
15
+ * });
16
+ *
17
+ * @example
18
+ * // Create an advanced tutorial with custom model and continue from existing messages
19
+ * const advancedTutorial = await Tutorial("advanced-features", {
20
+ * path: "docs/tutorials/advanced-features",
21
+ * title: "Advanced Alchemy Features",
22
+ * prompt: "Create a tutorial covering advanced features of the Alchemy framework",
23
+ * difficulty: "advanced",
24
+ * estimatedTime: 60,
25
+ * model: {
26
+ * id: "claude-3-7-sonnet-latest",
27
+ * provider: "anthropic"
28
+ * },
29
+ * maxIterations: 5,
30
+ * messages: basicTutorial.messages
31
+ * });
32
+ */
33
+ export const Tutorial = Resource("docs::Tutorial", async function (id, props) {
34
+ const { path: outFile, title, prompt, difficulty = "beginner", estimatedTime = 30, model = {
35
+ id: "claude-3-5-sonnet-latest",
36
+ provider: "anthropic",
37
+ }, maxIterations = 3, messages: initialMessages = [], } = props;
38
+ console.log(`Tutorial: Starting creation of "${title}" (ID: ${id})`);
39
+ // Handle deletion phase
40
+ if (this.phase === "delete") {
41
+ console.log(`Tutorial: Deleting tutorial "${title}" (ID: ${id})`);
42
+ return this.destroy();
43
+ }
44
+ // System prompts
45
+ const docSystemPrompt = `You are a technical writer creating a comprehensive tutorial.
46
+
47
+ Your task is to create a detailed tutorial about ${title} with difficulty level: ${difficulty} and estimated time to complete: ${estimatedTime} minutes.
48
+
49
+ The tutorial should take users from zero knowledge to a working understanding of the tool or framework.
50
+
51
+ The tutorial should be structured as follows:
52
+
53
+ # ${title}
54
+
55
+ ## Overview
56
+
57
+ (Provide a brief introduction to the tool/framework, its purpose, and what users will learn in this tutorial)
58
+
59
+ ## Prerequisites
60
+
61
+ (List any prerequisites, tools, or knowledge required before starting the tutorial)
62
+
63
+ ## Setup
64
+
65
+ ### Installation
66
+
67
+ \`\`\`bash
68
+ # Installation commands
69
+ \`\`\`
70
+
71
+ ### Configuration
72
+
73
+ (Explain any necessary configuration steps)
74
+
75
+ \`\`\`bash
76
+ # Configuration commands or code
77
+ \`\`\`
78
+
79
+ ## Step 1: [First Step Title]
80
+
81
+ (Explain the first step in detail)
82
+
83
+ \`\`\`bash
84
+ # Commands or code for the first step
85
+ \`\`\`
86
+
87
+ (Explain what the code does and why it's important)
88
+
89
+ ## Step 2: [Second Step Title]
90
+
91
+ (Explain the second step in detail)
92
+
93
+ \`\`\`bash
94
+ # Commands or code for the second step
95
+ \`\`\`
96
+
97
+ (Explain what the code does and why it's important)
98
+
99
+ ## Step 3: [Third Step Title]
100
+
101
+ (Explain the third step in detail)
102
+
103
+ \`\`\`bash
104
+ # Commands or code for the third step
105
+ \`\`\`
106
+
107
+ (Explain what the code does and why it's important)
108
+
109
+ ## Step 4: [Fourth Step Title]
110
+
111
+ (Explain the fourth step in detail)
112
+
113
+ \`\`\`bash
114
+ # Commands or code for the fourth step
115
+ \`\`\`
116
+
117
+ (Explain what the code does and why it's important)
118
+
119
+ ## Step 5: [Fifth Step Title]
120
+
121
+ (Explain the fifth step in detail)
122
+
123
+ \`\`\`bash
124
+ # Commands or code for the fifth step
125
+ \`\`\`
126
+
127
+ (Explain what the code does and why it's important)
128
+
129
+ ## Testing Your Work
130
+
131
+ (Explain how to verify that everything is working correctly)
132
+
133
+ \`\`\`bash
134
+ # Testing commands or code
135
+ \`\`\`
136
+
137
+ ## Troubleshooting
138
+
139
+ (Common issues users might encounter and how to resolve them)
140
+
141
+ ## Next Steps
142
+
143
+ (Suggest additional resources, advanced topics, or projects to try next)
144
+
145
+ ## Additional Resources
146
+
147
+ (Links to documentation, community resources, or related tutorials)`;
148
+ // Initial message if none provided
149
+ const startingMessages = initialMessages.length > 0
150
+ ? initialMessages
151
+ : [
152
+ {
153
+ role: "user",
154
+ content: `Create a comprehensive tutorial about ${title}.\n\n${prompt}\n\nDifficulty level: ${difficulty}\nEstimated time to complete: ${estimatedTime} minutes`,
155
+ },
156
+ ];
157
+ // Generate the initial tutorial
158
+ console.log(`Tutorial: Generating initial tutorial for "${title}"`);
159
+ let tutorial = await Document(`document`, {
160
+ title: title,
161
+ path: typeof outFile === "string" ? outFile : outFile.path,
162
+ model,
163
+ messages: startingMessages,
164
+ system: docSystemPrompt,
165
+ });
166
+ console.log(`Tutorial: Initial tutorial generated for "${title}" (${tutorial.content.length} chars)`);
167
+ // Review and improve the tutorial in a loop
168
+ let iteration = 0;
169
+ let approved = false;
170
+ let finalMessages = tutorial.messages;
171
+ while (!approved && iteration < maxIterations) {
172
+ iteration++;
173
+ console.log(`Tutorial: Starting review iteration ${iteration}/${maxIterations} for "${title}"`);
174
+ // Review the tutorial using system prompt
175
+ console.log(`Tutorial: Requesting review for iteration ${iteration}`);
176
+ const review = await Review(`review-${iteration}`, {
177
+ messages: finalMessages,
178
+ system: await alchemy `Please review this tutorial for correctness, conciseness, logical flow, and accurateness.
179
+
180
+ The tutorial should:
181
+ 1. Be technically accurate and free of errors
182
+ 2. Be concise without over-explaining
183
+ 3. Have a logical flow from basic to advanced concepts
184
+ 4. Be accurate in its descriptions and instructions
185
+ 5. Be appropriate for the ${difficulty} difficulty level
186
+ 6. Be completable within approximately ${estimatedTime} minutes
187
+
188
+ Provide specific feedback on areas that need improvement.`,
189
+ model,
190
+ });
191
+ // Extract the actual review content from the last assistant message
192
+ const reviewContent = review.messages[review.messages.length - 1]?.content || "";
193
+ console.log(`Tutorial: Review received for iteration ${iteration} (${reviewContent.length} chars)`);
194
+ // Check if the tutorial is approved using system prompt
195
+ console.log(`Tutorial: Evaluating approval for iteration ${iteration}`);
196
+ const approvalResult = await Approve(`approval-${iteration}`, {
197
+ messages: review.messages,
198
+ system: await alchemy `Based on the review above, determine if the tutorial meets the quality standards.
199
+ The tutorial should be approved if:
200
+ 1. It is technically accurate and free of errors
201
+ 2. It is concise without over-explaining
202
+ 3. It has a logical flow from basic to advanced concepts
203
+ 4. It is accurate in its descriptions and instructions
204
+ 5. It is appropriate for the ${difficulty} difficulty level
205
+ 6. It is completable within approximately ${estimatedTime} minutes
206
+
207
+ Respond with APPROVED or DENIED, followed by your explanation.`,
208
+ });
209
+ console.log(`Tutorial: Approval result for iteration ${iteration}: ${approvalResult.approved ? "APPROVED" : "DENIED"}`);
210
+ // Log approval status
211
+ if (approvalResult.approved) {
212
+ console.log(`Tutorial: Final tutorial approved: ${approvalResult.explanation}`);
213
+ approved = true;
214
+ finalMessages = approvalResult.messages;
215
+ }
216
+ else {
217
+ console.log(`Tutorial: Tutorial needs improvement: ${approvalResult.explanation}`);
218
+ // Add improvement request and generate an improved tutorial
219
+ const improvementMessages = [
220
+ ...approvalResult.messages,
221
+ {
222
+ role: "user",
223
+ content: `Please create an improved version of the tutorial that addresses the issues identified in the review: ${approvalResult.explanation}`,
224
+ },
225
+ ];
226
+ console.log(`Tutorial: Regenerating tutorial based on feedback for iteration ${iteration}`);
227
+ tutorial = await Document(`tutorial-${iteration}`, {
228
+ title: title,
229
+ path: typeof outFile === "string" ? outFile : outFile.path,
230
+ model,
231
+ messages: improvementMessages,
232
+ system: docSystemPrompt,
233
+ });
234
+ console.log(`Tutorial: Improved tutorial generated for iteration ${iteration} (${tutorial.content.length} chars)`);
235
+ finalMessages = tutorial.messages;
236
+ }
237
+ }
238
+ if (!approved) {
239
+ console.log(`Tutorial: Tutorial not approved after ${maxIterations} iterations. Using the best version available.`);
240
+ }
241
+ console.log(`Tutorial: Completed creation of "${title}" (ID: ${id})`);
242
+ // Return the tutorial resource
243
+ return this({
244
+ ...props,
245
+ document: tutorial,
246
+ content: tutorial.content,
247
+ messages: finalMessages,
248
+ createdAt: Date.now(),
249
+ updatedAt: Date.now(),
250
+ });
251
+ });
package/lib/resource.js CHANGED
@@ -15,7 +15,14 @@ export function Resource(type, ...args) {
15
15
  if (scope.resources.has(resourceID)) {
16
16
  // TODO(sam): do we want to throw?
17
17
  // it's kind of awesome that you can re-create a resource and call apply
18
- // console.warn(`Resource ${id} already exists in the stack: ${stack.id}`);
18
+ const otherResource = scope.resources.get(resourceID);
19
+ if (otherResource?.Kind !== type) {
20
+ scope.fail();
21
+ throw new Error(`Resource ${resourceID} already exists in the stack and is of a different type: '${otherResource?.Kind}' !== '${type}'`);
22
+ }
23
+ // console.warn(
24
+ // `Resource ${resourceID} already exists in the stack: ${scope.chain.join("/")}`,
25
+ // );
19
26
  }
20
27
  // get a sequence number (unique within the scope) for the resource
21
28
  const seq = scope.seq();
package/lib/scope.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { PendingResource, ResourceID } from "./resource";
2
- import { type StateStore, type StateStoreType } from "./state";
2
+ import type { StateStore, StateStoreType } from "./state";
3
3
  export type ScopeOptions = {
4
4
  appName?: string;
5
5
  stage: string;
package/lib/scope.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { destroy } from "./destroy";
3
- import { FileSystemStateStore, } from "./state";
3
+ import { FileSystemStateStore } from "./fs/file-system-state-store";
4
4
  const scopeStorage = new AsyncLocalStorage();
5
5
  export class Scope {
6
6
  static get() {
@@ -32,7 +32,9 @@ export class Scope {
32
32
  throw new Error("Scope name is required when creating a child scope");
33
33
  }
34
34
  this.password = options.password;
35
- this.state = new (options.stateStore ?? FileSystemStateStore)(this);
35
+ this.state = options.stateStore
36
+ ? options.stateStore(this)
37
+ : new FileSystemStateStore(this);
36
38
  }
37
39
  async delete(resourceID) {
38
40
  await this.state.delete(resourceID);
package/lib/state.d.ts CHANGED
@@ -11,7 +11,7 @@ export interface State<Kind extends string = string, Props extends ResourceProps
11
11
  oldProps?: Props;
12
12
  output: Out;
13
13
  }
14
- export type StateStoreType = new (scope: Scope) => StateStore;
14
+ export type StateStoreType = (scope: Scope) => StateStore;
15
15
  export interface StateStore {
16
16
  /** Initialize the state container if one is required */
17
17
  init?(): Promise<void>;
@@ -27,18 +27,3 @@ export interface StateStore {
27
27
  set(key: string, value: State): Promise<void>;
28
28
  delete(key: string): Promise<void>;
29
29
  }
30
- export declare class FileSystemStateStore implements StateStore {
31
- readonly scope: Scope;
32
- readonly dir: string;
33
- constructor(scope: Scope);
34
- init(): Promise<void>;
35
- deinit(): Promise<void>;
36
- count(): Promise<number>;
37
- list(): Promise<string[]>;
38
- get(key: string): Promise<State | undefined>;
39
- set(key: string, value: State): Promise<void>;
40
- delete(key: string): Promise<void>;
41
- all(): Promise<Record<string, State>>;
42
- getBatch(ids: string[]): Promise<Record<string, State>>;
43
- private getPath;
44
- }
package/lib/state.js CHANGED
@@ -1,87 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { ignore } from "./util/ignore";
4
- import { deserialize, serialize } from "./util/serde";
5
- const stateRootDir = path.join(process.cwd(), ".alchemy");
6
- export class FileSystemStateStore {
7
- scope;
8
- dir;
9
- constructor(scope) {
10
- this.scope = scope;
11
- this.dir = path.join(stateRootDir, ...scope.chain);
12
- }
13
- async init() {
14
- await fs.promises.mkdir(stateRootDir, { recursive: true });
15
- await fs.promises.mkdir(this.dir, { recursive: true });
16
- }
17
- async deinit() {
18
- await ignore("ENOENT", () => fs.promises.rmdir(this.dir));
19
- }
20
- async count() {
21
- return Object.keys(await this.list()).length;
22
- }
23
- async list() {
24
- try {
25
- const files = await fs.promises.readdir(this.dir, {
26
- withFileTypes: true,
27
- });
28
- return files
29
- .filter((dirent) => dirent.isFile() && dirent.name.endsWith(".json"))
30
- .map((dirent) => dirent.name.replace(/\.json$/, ""))
31
- .map((key) => key.replaceAll(":", "/"));
32
- }
33
- catch (error) {
34
- if (error.code === "ENOENT") {
35
- return [];
36
- }
37
- throw error;
38
- }
39
- }
40
- async get(key) {
41
- try {
42
- const content = await fs.promises.readFile(await this.getPath(key), "utf8");
43
- const state = (await deserialize(this.scope, JSON.parse(content)));
44
- if (state.output === undefined) {
45
- state.output = {};
46
- }
47
- state.output.Scope = this.scope;
48
- return state;
49
- }
50
- catch (error) {
51
- if (error.code === "ENOENT") {
52
- return undefined;
53
- }
54
- throw error;
55
- }
56
- }
57
- async set(key, value) {
58
- return fs.promises.writeFile(await this.getPath(key), JSON.stringify(await serialize(this.scope, value), null, 2));
59
- }
60
- async delete(key) {
61
- return fs.promises.unlink(await this.getPath(key));
62
- }
63
- async all() {
64
- return this.getBatch(await this.list());
65
- }
66
- async getBatch(ids) {
67
- return Object.fromEntries((await Promise.all(Array.from(ids).flatMap(async (id) => {
68
- const s = await this.get(id);
69
- if (s === undefined) {
70
- return [];
71
- }
72
- return [[id, s]];
73
- }))).flat());
74
- }
75
- async getPath(key) {
76
- if (key.includes(":")) {
77
- throw new Error(`ID cannot include colons: ${key}`);
78
- }
79
- if (key.includes("/")) {
80
- key = key.replaceAll("/", ":");
81
- }
82
- const file = path.join(this.dir, `${key}.json`);
83
- const dir = path.dirname(file);
84
- await fs.promises.mkdir(dir, { recursive: true });
85
- return file;
86
- }
87
- }