alchemy 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/astro-file.js +7 -24
  3. package/lib/ai/client.d.ts +7 -0
  4. package/lib/ai/client.js +45 -0
  5. package/lib/ai/css-file.js +7 -24
  6. package/lib/ai/data.d.ts +26 -21
  7. package/lib/ai/data.js +33 -22
  8. package/lib/ai/document.d.ts +76 -5
  9. package/lib/ai/document.js +112 -56
  10. package/lib/ai/html-file.js +7 -24
  11. package/lib/ai/json-file.js +11 -32
  12. package/lib/ai/typescript-file.d.ts +1 -2
  13. package/lib/ai/typescript-file.js +8 -39
  14. package/lib/ai/vue-file.js +7 -24
  15. package/lib/ai/yaml-file.js +12 -48
  16. package/lib/alchemy.d.ts +5 -0
  17. package/lib/alchemy.js +61 -3
  18. package/lib/apply.js +18 -11
  19. package/lib/aws/account-id.d.ts +4 -1
  20. package/lib/aws/account-id.js +1 -1
  21. package/lib/aws/credentials.d.ts +5 -0
  22. package/lib/aws/credentials.js +0 -0
  23. package/lib/aws/oidc/oidc-provider.js +2 -2
  24. package/lib/aws/role.js +35 -23
  25. package/lib/cloudflare/account-api-token.d.ts +168 -0
  26. package/lib/cloudflare/account-api-token.js +179 -0
  27. package/lib/cloudflare/account-id.d.ts +1 -0
  28. package/lib/cloudflare/account-id.js +0 -0
  29. package/lib/cloudflare/api.d.ts +38 -1
  30. package/lib/cloudflare/api.js +55 -4
  31. package/lib/cloudflare/bucket.d.ts +118 -3
  32. package/lib/cloudflare/bucket.js +269 -89
  33. package/lib/cloudflare/custom-domain.d.ts +75 -0
  34. package/lib/cloudflare/custom-domain.js +154 -0
  35. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  36. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  37. package/lib/cloudflare/index.d.ts +5 -1
  38. package/lib/cloudflare/index.js +5 -1
  39. package/lib/cloudflare/permission-groups.d.ts +78 -0
  40. package/lib/cloudflare/permission-groups.js +48 -0
  41. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  42. package/lib/cloudflare/r2-rest-state-store.js +3 -2
  43. package/lib/cloudflare/static-site-router.js +16 -7
  44. package/lib/cloudflare/static-site.d.ts +17 -18
  45. package/lib/cloudflare/static-site.js +19 -7
  46. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  47. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  48. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  49. package/lib/fs/copy-file.d.ts +54 -0
  50. package/lib/fs/copy-file.js +63 -0
  51. package/lib/fs/file-system-state-store.js +1 -1
  52. package/lib/fs/file.d.ts +31 -0
  53. package/lib/fs/file.js +30 -5
  54. package/lib/fs/index.d.ts +6 -0
  55. package/lib/fs/index.js +6 -0
  56. package/lib/fs/static-astro-file.d.ts +34 -0
  57. package/lib/fs/static-astro-file.js +39 -0
  58. package/lib/fs/static-css-file.d.ts +24 -0
  59. package/lib/fs/static-css-file.js +29 -0
  60. package/lib/fs/static-html-file.d.ts +31 -0
  61. package/lib/fs/static-html-file.js +36 -0
  62. package/lib/fs/static-json-file.d.ts +2 -2
  63. package/lib/fs/static-json-file.js +3 -2
  64. package/lib/fs/static-text-file.d.ts +1 -1
  65. package/lib/fs/static-text-file.js +3 -2
  66. package/lib/fs/static-typescript-file.d.ts +2 -2
  67. package/lib/fs/static-typescript-file.js +3 -2
  68. package/lib/fs/static-vue-file.d.ts +28 -0
  69. package/lib/fs/static-vue-file.js +33 -0
  70. package/lib/fs/static-yaml-file.d.ts +11 -8
  71. package/lib/fs/static-yaml-file.js +3 -2
  72. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +14 -4
  73. package/lib/internal/docs/providers.js +196 -0
  74. package/lib/secret.d.ts +3 -0
  75. package/lib/secret.js +13 -0
  76. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  77. package/lib/{util/serde.js → serde.js} +12 -4
  78. package/lib/test/bun.js +3 -2
  79. package/lib/util/sha256.d.ts +1 -0
  80. package/lib/util/sha256.js +4 -0
  81. package/lib/web/vitepress/config.d.ts +11 -0
  82. package/lib/web/vitepress/config.js +32 -0
  83. package/lib/web/vitepress/home-page.d.ts +5 -3
  84. package/lib/web/vitepress/home-page.js +48 -35
  85. package/lib/web/vitepress/index.d.ts +2 -0
  86. package/lib/web/vitepress/index.js +2 -0
  87. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  88. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  89. package/lib/web/vitepress/vitepress.d.ts +5 -4
  90. package/lib/web/vitepress/vitepress.js +20 -36
  91. package/package.json +8 -4
  92. package/src/ai/astro-file.ts +11 -30
  93. package/src/ai/client.ts +59 -0
  94. package/src/ai/css-file.ts +11 -30
  95. package/src/ai/data.ts +50 -25
  96. package/src/ai/document.ts +171 -66
  97. package/src/ai/html-file.ts +11 -30
  98. package/src/ai/json-file.ts +13 -37
  99. package/src/ai/typescript-file.ts +14 -48
  100. package/src/ai/vue-file.ts +11 -30
  101. package/src/ai/yaml-file.ts +13 -53
  102. package/src/alchemy.ts +74 -9
  103. package/src/apply.ts +27 -26
  104. package/src/aws/account-id.ts +6 -2
  105. package/src/aws/credentials.ts +6 -0
  106. package/src/aws/oidc/oidc-provider.ts +17 -17
  107. package/src/aws/role.ts +68 -54
  108. package/src/cloudflare/account-api-token.ts +365 -0
  109. package/src/cloudflare/account-id.ts +0 -0
  110. package/src/cloudflare/api.ts +88 -17
  111. package/src/cloudflare/bucket.ts +493 -133
  112. package/src/cloudflare/custom-domain.ts +318 -0
  113. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  114. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  115. package/src/cloudflare/index.ts +5 -1
  116. package/src/cloudflare/permission-groups.ts +137 -0
  117. package/src/cloudflare/r2-rest-state-store.ts +14 -13
  118. package/src/cloudflare/static-site-router.ts +18 -8
  119. package/src/cloudflare/static-site.ts +27 -35
  120. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  121. package/src/cloudflare/worker.ts +44 -43
  122. package/src/dns/import-dns.ts +4 -4
  123. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  124. package/src/fs/copy-file.ts +115 -0
  125. package/src/fs/file-system-state-store.ts +5 -5
  126. package/src/fs/file.ts +64 -9
  127. package/src/fs/index.ts +6 -0
  128. package/src/fs/static-astro-file.ts +45 -0
  129. package/src/fs/static-css-file.ts +35 -0
  130. package/src/fs/static-html-file.ts +42 -0
  131. package/src/fs/static-json-file.ts +4 -3
  132. package/src/fs/static-text-file.ts +5 -2
  133. package/src/fs/static-typescript-file.ts +4 -3
  134. package/src/fs/static-vue-file.ts +39 -0
  135. package/src/fs/static-yaml-file.ts +13 -9
  136. package/src/internal/docs/providers.ts +281 -0
  137. package/src/secret.ts +16 -0
  138. package/src/{util/serde.ts → serde.ts} +16 -10
  139. package/src/test/bun.ts +8 -7
  140. package/src/util/sha256.ts +5 -0
  141. package/src/web/vitepress/config.ts +50 -0
  142. package/src/web/vitepress/home-page.ts +54 -42
  143. package/src/web/vitepress/index.ts +2 -0
  144. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  145. package/src/web/vitepress/vitepress.ts +34 -50
  146. package/lib/internal/getting-started.d.ts +0 -10
  147. package/lib/internal/getting-started.js +0 -94
  148. package/lib/internal/project.d.ts +0 -13
  149. package/lib/internal/project.js +0 -101
  150. package/lib/internal/providers.js +0 -148
  151. package/src/internal/getting-started.ts +0 -107
  152. package/src/internal/project.ts +0 -119
  153. package/src/internal/providers.ts +0 -209
  154. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
package/src/fs/file.ts CHANGED
@@ -52,6 +52,28 @@ declare module "../alchemy" {
52
52
  */
53
53
  files(paths: string[]): Promise<FileCollection>;
54
54
  files(path: string, ...paths: string[]): Promise<FileCollection>;
55
+
56
+ /**
57
+ * Gets all of the files in a directory.
58
+ * @param path Path to the directory
59
+ * @param props Optional properties
60
+ * @returns Promise resolving to a FileCollection
61
+ *
62
+ * @example
63
+ * // Get all files in a directory
64
+ * const files = await alchemy.folder("./docs");
65
+ *
66
+ */
67
+ folder(
68
+ path: string,
69
+ props?: {
70
+ /**
71
+ * Whether to recursively get all files in the directory
72
+ * @default false
73
+ */
74
+ recursive?: boolean;
75
+ }
76
+ ): Promise<FileCollection>;
55
77
  }
56
78
  }
57
79
 
@@ -72,12 +94,19 @@ alchemy.files = async (
72
94
  paths.map(async (path) => [
73
95
  path,
74
96
  await fs.promises.readFile(path, "utf-8"),
75
- ]),
76
- ),
97
+ ])
98
+ )
77
99
  ),
78
100
  };
79
101
  };
80
102
 
103
+ alchemy.folder = async (dir: string, props?: { recursive?: boolean }) => {
104
+ const files = await fs.promises.readdir(dir, {
105
+ recursive: props?.recursive ?? false,
106
+ });
107
+ return alchemy.files(files.map((file) => path.join(dir, file)));
108
+ };
109
+
81
110
  /**
82
111
  * Base file resource type
83
112
  */
@@ -111,6 +140,19 @@ export interface File extends Resource<"fs::File"> {
111
140
  * path: "logs/app.log",
112
141
  * content: "application log entry"
113
142
  * });
143
+ *
144
+ * @example
145
+ * // Update file content and path
146
+ * let file = await File("config.json", {
147
+ * path: "config.json",
148
+ * content: '{ "version": "1.0.0" }'
149
+ * });
150
+ *
151
+ * // Later, update the path and content (old file will be removed)
152
+ * file = await File("config.json", {
153
+ * path: "config/config.json",
154
+ * content: '{ "version": "1.0.1" }'
155
+ * });
114
156
  */
115
157
  export const File = Resource(
116
158
  "fs::File",
@@ -120,21 +162,34 @@ export const File = Resource(
120
162
  props: {
121
163
  path: string;
122
164
  content: string;
123
- },
165
+ }
124
166
  ): Promise<File> {
125
167
  const filePath = props?.path ?? id;
168
+
126
169
  if (this.phase === "delete") {
127
170
  await ignore("ENOENT", async () => fs.promises.unlink(filePath));
128
171
  return this.destroy();
129
- } else {
130
- await fs.promises.mkdir(path.dirname(filePath), {
131
- recursive: true,
132
- });
133
- await fs.promises.writeFile(filePath, props.content);
172
+ } else if (
173
+ this.phase === "update" &&
174
+ this.output &&
175
+ this.output.path !== filePath
176
+ ) {
177
+ // If path has changed, delete the old file
178
+ console.log(
179
+ `File: Path changed from ${this.output.path} to ${filePath}, removing old file`
180
+ );
181
+ await ignore("ENOENT", async () => fs.promises.unlink(this.output.path));
134
182
  }
183
+
184
+ // Create directory and write file
185
+ await fs.promises.mkdir(path.dirname(filePath), {
186
+ recursive: true,
187
+ });
188
+ await fs.promises.writeFile(filePath, props.content);
189
+
135
190
  return this({
136
191
  path: filePath,
137
192
  content: props.content,
138
193
  });
139
- },
194
+ }
140
195
  );
package/src/fs/index.ts CHANGED
@@ -1,8 +1,14 @@
1
+ export * from "./copy-file";
1
2
  export * from "./file";
2
3
  export * from "./file-collection";
3
4
  export * from "./file-ref";
5
+ export * from "./file-system-state-store";
4
6
  export * from "./folder";
7
+ export * from "./static-astro-file";
8
+ export * from "./static-css-file";
9
+ export * from "./static-html-file";
5
10
  export * from "./static-json-file";
6
11
  export * from "./static-text-file";
7
12
  export * from "./static-typescript-file";
13
+ export * from "./static-vue-file";
8
14
  export * from "./static-yaml-file";
@@ -0,0 +1,45 @@
1
+ import { File } from "./file";
2
+
3
+ export type StaticAstroFile = File;
4
+
5
+ /**
6
+ * Creates a static Astro component file
7
+ *
8
+ * @example
9
+ * // Create an Astro component file with content
10
+ * const header = await StaticAstroFile("Header.astro",
11
+ * `---
12
+ * import Logo from '../components/Logo.astro';
13
+ * const navItems = ['Home', 'About', 'Contact'];
14
+ * ---
15
+ *
16
+ * <header class="header">
17
+ * <Logo />
18
+ * <nav>
19
+ * <ul>
20
+ * {navItems.map(item => (
21
+ * <li><a href={`/${item.toLowerCase()}`}>{item}</a></li>
22
+ * ))}
23
+ * </ul>
24
+ * </nav>
25
+ * </header>
26
+ *
27
+ * <style>
28
+ * .header {
29
+ * display: flex;
30
+ * justify-content: space-between;
31
+ * padding: 1rem;
32
+ * }
33
+ * </style>`
34
+ * );
35
+ */
36
+ export function StaticAstroFile(
37
+ id: string,
38
+ ...args: [content: string] | [path: string, content: string]
39
+ ): Promise<StaticAstroFile> {
40
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
41
+ return File(id, {
42
+ path,
43
+ content,
44
+ });
45
+ }
@@ -0,0 +1,35 @@
1
+ import { File } from "./file";
2
+
3
+ export type StaticCSSFile = File;
4
+
5
+ /**
6
+ * Creates a static CSS file
7
+ *
8
+ * @example
9
+ * // Create a CSS file with styles
10
+ * const styles = await StaticCSSFile("styles.css",
11
+ * `.container {
12
+ * max-width: 1200px;
13
+ * margin: 0 auto;
14
+ * padding: 0 1rem;
15
+ * }
16
+ *
17
+ * .button {
18
+ * background-color: #0062ff;
19
+ * color: white;
20
+ * border: none;
21
+ * padding: 0.5rem 1rem;
22
+ * border-radius: 4px;
23
+ * }`
24
+ * );
25
+ */
26
+ export function StaticCSSFile(
27
+ id: string,
28
+ ...args: [content: string] | [path: string, content: string]
29
+ ): Promise<StaticCSSFile> {
30
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
31
+ return File(id, {
32
+ path,
33
+ content,
34
+ });
35
+ }
@@ -0,0 +1,42 @@
1
+ import { File } from "./file";
2
+
3
+ export type StaticHTMLFile = File;
4
+
5
+ /**
6
+ * Creates a static HTML file
7
+ *
8
+ * @example
9
+ * // Create an HTML file with content
10
+ * const page = await StaticHTMLFile("index.html",
11
+ * `<!DOCTYPE html>
12
+ * <html lang="en">
13
+ * <head>
14
+ * <meta charset="UTF-8">
15
+ * <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ * <title>My Website</title>
17
+ * <link rel="stylesheet" href="styles.css">
18
+ * </head>
19
+ * <body>
20
+ * <header>
21
+ * <h1>Welcome to My Website</h1>
22
+ * </header>
23
+ * <main>
24
+ * <p>This is the main content of the page.</p>
25
+ * </main>
26
+ * <footer>
27
+ * <p>&copy; 2024 My Company</p>
28
+ * </footer>
29
+ * </body>
30
+ * </html>`
31
+ * );
32
+ */
33
+ export function StaticHTMLFile(
34
+ id: string,
35
+ ...args: [content: string] | [path: string, content: string]
36
+ ): Promise<StaticHTMLFile> {
37
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
38
+ return File(id, {
39
+ path,
40
+ content,
41
+ });
42
+ }
@@ -5,7 +5,7 @@ import { File } from "./file";
5
5
  *
6
6
  * @example
7
7
  * // Create a JSON configuration file
8
- * const config = await JsonFile("config.json", {
8
+ * const config = await StaticJsonFile("config.json", {
9
9
  * api: {
10
10
  * endpoint: "https://api.example.com",
11
11
  * version: "v1"
@@ -17,11 +17,12 @@ export type StaticJsonFile = File;
17
17
 
18
18
  export async function StaticJsonFile(
19
19
  id: string,
20
- content: any,
20
+ ...args: [content: any] | [path: string, content: any]
21
21
  ): Promise<StaticJsonFile> {
22
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
22
23
  const prettier = await import("prettier");
23
24
  return File(id, {
24
- path: id,
25
+ path,
25
26
  content: await prettier.format(JSON.stringify(content), {
26
27
  parser: "json",
27
28
  editor: {
@@ -13,10 +13,13 @@ export type StaticTextFile = File;
13
13
 
14
14
  export function StaticTextFile(
15
15
  id: string,
16
- content: string,
16
+ ...args:
17
+ | [content: string]
18
+ | [path: string, content: string]
17
19
  ): Promise<StaticTextFile> {
20
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
18
21
  return File(id, {
19
- path: id,
22
+ path,
20
23
  content,
21
24
  });
22
25
  }
@@ -5,7 +5,7 @@ import { File } from "./file";
5
5
  *
6
6
  * @example
7
7
  * // Create a TypeScript file
8
- * const component = await TypeScriptFile("Component.ts", `
8
+ * const component = await StaticTypeScriptFile("Component.ts", `
9
9
  * interface Props {
10
10
  * name: string;
11
11
  * age: number;
@@ -20,11 +20,12 @@ export type StaticTypeScriptFile = File;
20
20
 
21
21
  export async function StaticTypeScriptFile(
22
22
  id: string,
23
- content: string,
23
+ ...args: [content: string] | [path: string, content: string]
24
24
  ): Promise<StaticTypeScriptFile> {
25
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
25
26
  const prettier = await import("prettier");
26
27
  return File(id, {
27
- path: id,
28
+ path,
28
29
  content: await prettier.format(content, {
29
30
  parser: "typescript",
30
31
  editor: {
@@ -0,0 +1,39 @@
1
+ import { File } from "./file";
2
+
3
+ export type StaticVueFile = File;
4
+
5
+ /**
6
+ * Creates a static Vue component file
7
+ *
8
+ * @example
9
+ * // Create a Vue component file with content
10
+ * const button = await StaticVueFile("Button.vue",
11
+ * `<template>
12
+ * <button class="btn">{{ text }}</button>
13
+ * </template>
14
+ *
15
+ * <script>
16
+ * export default {
17
+ * props: {
18
+ * text: String
19
+ * }
20
+ * }
21
+ * </script>
22
+ *
23
+ * <style>
24
+ * .btn {
25
+ * padding: 0.5rem 1rem;
26
+ * }
27
+ * </style>`
28
+ * );
29
+ */
30
+ export function StaticVueFile(
31
+ id: string,
32
+ ...args: [content: string] | [path: string, content: string]
33
+ ): Promise<StaticVueFile> {
34
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
35
+ return File(id, {
36
+ path,
37
+ content,
38
+ });
39
+ }
@@ -4,26 +4,30 @@ import { File } from "./file";
4
4
  *
5
5
  * @example
6
6
  * // Create a YAML configuration file
7
- * const config = await YamlFile("config.yaml", {
8
- * server:
9
- * host: "localhost"
7
+ * const config = await StaticYamlFile("config.yaml", {
8
+ * server: {
9
+ * host: "localhost",
10
10
  * port: 3000
11
- * database:
12
- * url: "postgresql://localhost:5432/db"
13
- * pool:
14
- * min: 1
11
+ * },
12
+ * database: {
13
+ * url: "postgresql://localhost:5432/db",
14
+ * pool: {
15
+ * min: 1,
15
16
  * max: 10
17
+ * }
18
+ * }
16
19
  * });
17
20
  */
18
21
  export type StaticYamlFile = File;
19
22
 
20
23
  export async function StaticYamlFile(
21
24
  id: string,
22
- content: any,
25
+ ...args: [content: any] | [path: string, content: any]
23
26
  ): Promise<StaticYamlFile> {
27
+ const [path, content] = args.length === 1 ? [id, args[0]] : args;
24
28
  const yaml = await import("yaml");
25
29
  return File(id, {
26
- path: id,
30
+ path,
27
31
  content: yaml.stringify(content),
28
32
  });
29
33
  }
@@ -0,0 +1,281 @@
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
+
8
+ export interface DocsProps {
9
+ /**
10
+ * The output directory for the docs.
11
+ */
12
+ outDir: string | Folder;
13
+
14
+ /**
15
+ * The source directory for the docs.
16
+ */
17
+ srcDir: string;
18
+
19
+ /**
20
+ * Whether to filter the docs.
21
+ * If true, include all docs.
22
+ * If false, include none.
23
+ * If a number, include that many providers.
24
+ *
25
+ * @default true (all docs)
26
+ */
27
+ filter?: boolean | number;
28
+
29
+ /**
30
+ * Whether to run in parallel.
31
+ *
32
+ * @default true
33
+ */
34
+ parallel?: boolean;
35
+ }
36
+
37
+ export type Providers = {
38
+ dir: string;
39
+ provider: string;
40
+ documents: Document[];
41
+ }[];
42
+
43
+ export async function Providers({
44
+ srcDir,
45
+ outDir,
46
+ filter,
47
+ parallel = true,
48
+ }: DocsProps): Promise<Providers> {
49
+ outDir = typeof outDir === "string" ? outDir : outDir.path;
50
+
51
+ const exclude = [
52
+ "util",
53
+ "test",
54
+ "vitepress",
55
+ "vite",
56
+ "shadcn",
57
+ "internal",
58
+ "web",
59
+ ];
60
+
61
+ // Get all folders in the alchemy/src directory
62
+ let providers = (
63
+ await fs.readdir(srcDir, {
64
+ withFileTypes: true,
65
+ })
66
+ )
67
+ .filter((dirent) => dirent.isDirectory() && !exclude.includes(dirent.name))
68
+ .map((dirent) => path.join(dirent.parentPath, dirent.name));
69
+
70
+ // For each provider, list all files
71
+ if (filter === false) {
72
+ return [];
73
+ } else if (typeof filter === "number") {
74
+ providers = providers.slice(0, filter);
75
+ }
76
+
77
+ if (parallel) {
78
+ return await Promise.all(
79
+ providers.map((provider) =>
80
+ generateProviderDocs({ provider, outDir, parallel })
81
+ )
82
+ );
83
+ } else {
84
+ const generatedProviders = [];
85
+ for (const provider of providers) {
86
+ generatedProviders.push(
87
+ await generateProviderDocs({ provider, outDir, parallel })
88
+ );
89
+ }
90
+ return generatedProviders;
91
+ }
92
+ }
93
+
94
+ async function generateProviderDocs({
95
+ provider,
96
+ outDir,
97
+ parallel,
98
+ }: {
99
+ provider: string;
100
+ outDir: string;
101
+ parallel: boolean;
102
+ }) {
103
+ const providerName = path.basename(provider);
104
+ const files = (
105
+ await fs.readdir(path.resolve(provider), {
106
+ withFileTypes: true,
107
+ })
108
+ )
109
+ .filter((dirent) => dirent.isFile())
110
+ .map((dirent) =>
111
+ path.relative(process.cwd(), path.resolve(provider, dirent.name))
112
+ )
113
+ .filter((file) => file.endsWith(".ts") && !file.endsWith("index.ts"));
114
+
115
+ type Group = (typeof groups)[number];
116
+
117
+ const {
118
+ object: { groups },
119
+ } = await Data(`docs/${providerName}`, {
120
+ model: {
121
+ id: "o3-mini",
122
+ provider: "openai",
123
+ options: {
124
+ reasoningEffort: "high",
125
+ },
126
+ },
127
+ temperature: 0.1,
128
+ schema: type({
129
+ groups: type({
130
+ identifier: type("string").describe(
131
+ "The identifier of the file's primary exported Resource/Function/Type, e.g. Bucket or StaticSite, AstroFile, TypeScriptFile"
132
+ ),
133
+ filename: type("string").describe(
134
+ "The filename of the Resource's Document, e.g. bucket.md or static-site.md"
135
+ ),
136
+ category: type("'Resource'|'Client'|'Utility'|'Types'").describe(
137
+ "The classification of the Resource's Document, one of: Resource, Client, Utility, or Types."
138
+ ),
139
+ }).array(),
140
+ }),
141
+ system: await alchemy`
142
+ 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.
143
+ You will be provided with a list of documents and instructions on how to classify them.
144
+ Each document has a title, file name, and category.
145
+ `,
146
+ prompt: await alchemy`
147
+ Identify and classify the documents that need to be written for the '${provider}' Service's Alchemy Resources.
148
+ For background knowledge on Alchemy, see ${alchemy.file("./README.md")}.
149
+ For background knowledge on the structure of an Alchemy Resource, see ${alchemy.file("./.cursorrules")}.
150
+
151
+ The ${provider} Service has the following resources:
152
+ ${alchemy.files(files)}
153
+
154
+ 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(...).
155
+ A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
156
+ A file is considered a "Utility" if it contains utility functions that are not resources or clients.
157
+ A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
158
+
159
+ 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.
160
+
161
+ // "Resource Name"
162
+ const ResourceName = Resource(...)
163
+ `,
164
+ });
165
+
166
+ const providerDocsDir = (await Folder(path.join(outDir, providerName))).path;
167
+
168
+ let documents: Document[] = [];
169
+ if (parallel) {
170
+ documents = await Promise.all(
171
+ groups.filter((g) => g.category === "Resource").map(generateDocument)
172
+ );
173
+ } else {
174
+ for (const g of groups.filter((g) => g.category === "Resource")) {
175
+ documents.push(await generateDocument(g));
176
+ }
177
+ }
178
+
179
+ async function generateDocument(g: Group) {
180
+ return Document(`docs/${providerName}/${g.identifier}`, {
181
+ title: g.identifier,
182
+ path: path.join(
183
+ providerDocsDir,
184
+ `${g.filename.replace(".ts", "").replace(".md", "")}.md`
185
+ ),
186
+ freeze: true,
187
+ model: {
188
+ id: "claude-3-5-sonnet-latest",
189
+ provider: "anthropic",
190
+ // options: {
191
+ // reasoningEffort: "high",
192
+ // },
193
+ },
194
+ prompt: await alchemy`
195
+ You are a technical writer writing API documentation for an Alchemy IaC Resource.
196
+ See ${alchemy.file("./README.md")} to understand the overview of Alchemy.
197
+ See ${alchemy.file("./.cursorrules")} to better understand the structure and convention of an Alchemy Resource.
198
+
199
+ Relevant files for the ${providerName} Service:
200
+ ${alchemy.files(files)}
201
+
202
+ Write concise documentation for the "${g.identifier}" Resource.
203
+
204
+ > [!CAUTION]
205
+ > Avoid the temptation to over explain or over describe. Focus on concise, simple, high value snippets. One heading and 0-1 descriptions per snippet.
206
+
207
+ > [!TIP]
208
+ > Make sure the examples follow a natural progression from the minimal example to logical next steps of how the Resource might be used.
209
+
210
+ Each document must follow the following format:
211
+
212
+ # ${g.identifier}
213
+
214
+ (simple description with an external link to the provider's website)
215
+ e.g.
216
+ The Efs component lets you add [Amazon Elastic File System (EFS)](https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html) to your app.
217
+
218
+ # Minimal Example
219
+
220
+ (brief 1-2 sentences of what it does)
221
+
222
+ \`\`\`ts
223
+ import { ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
224
+
225
+ (example)
226
+ \`\`\`
227
+
228
+ # (one heading per variation)
229
+
230
+ (brief 1-2 sentences of what it does)
231
+
232
+ \`\`\`ts
233
+ import { ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
234
+
235
+ (example)
236
+ \`\`\`
237
+
238
+ Before writing the document, think through:
239
+ 1. What is the minimal, most common example use case for this resource?
240
+ 2. What are the variations (e.g. combination of different options) that are also commonly used, e.g. specifying the memory size of a lambda function.
241
+ 3. Make sure to draw from the examples and your understanding of Alchemy.
242
+
243
+ Refer to alchemy docs to understand the context of how this documentation is consumed:
244
+ - ${alchemy.file("./alchemy-web/docs/what-is-alchemy.md")}
245
+ - ${alchemy.file("./alchemy-web/docs/getting-started.md")}
246
+ - ${alchemy.folder("./alchemy-web/docs/concepts/")}
247
+
248
+ ${
249
+ providerName === "cloudflare"
250
+ ? await alchemy`# Bind to a Worker
251
+ (if it is a Cloudflare Resource)
252
+
253
+ (brief 1-2 sentences of what it does)
254
+
255
+ \`\`\`ts
256
+ import { Worker, ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
257
+
258
+ const myResource = await ${g.identifier.replaceAll(" ", "")}("my-resource", {
259
+ // ...
260
+ });
261
+
262
+ await Worker("my-worker", {
263
+ name: "my-worker",
264
+ script: "console.log('Hello, world!')",
265
+ bindings: {
266
+ myResource,
267
+ },
268
+ });
269
+ \`\`\``
270
+ : ""
271
+ }
272
+ `,
273
+ });
274
+ }
275
+
276
+ return {
277
+ dir: providerDocsDir,
278
+ provider: providerName,
279
+ documents,
280
+ };
281
+ }
package/src/secret.ts CHANGED
@@ -83,3 +83,19 @@ export function secret<S extends string | undefined>(unencrypted: S): Secret {
83
83
  }
84
84
  return new Secret(unencrypted);
85
85
  }
86
+
87
+ export namespace secret {
88
+ export async function env(
89
+ name: string,
90
+ value?: string,
91
+ error?: string
92
+ ): Promise<Secret> {
93
+ const alchemy = await import("./alchemy");
94
+ const result = await alchemy.env(name, value, error);
95
+ if (typeof result === "string") {
96
+ return secret(result);
97
+ } else {
98
+ throw new Error(`Secret environment variable ${name} is not a string`);
99
+ }
100
+ }
101
+ }