alchemy 0.3.1 → 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 (104) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/document.d.ts +12 -1
  3. package/lib/ai/document.js +10 -1
  4. package/lib/ai/index.d.ts +0 -2
  5. package/lib/ai/index.js +0 -2
  6. package/lib/alchemy.d.ts +5 -0
  7. package/lib/alchemy.js +44 -1
  8. package/lib/apply.js +17 -10
  9. package/lib/aws/account-id.d.ts +4 -1
  10. package/lib/aws/account-id.js +1 -1
  11. package/lib/aws/credentials.d.ts +5 -0
  12. package/lib/aws/credentials.js +0 -0
  13. package/lib/aws/oidc/oidc-provider.js +2 -2
  14. package/lib/aws/role.js +35 -23
  15. package/lib/cloudflare/account-api-token.d.ts +168 -0
  16. package/lib/cloudflare/account-api-token.js +179 -0
  17. package/lib/cloudflare/account-id.d.ts +1 -0
  18. package/lib/cloudflare/account-id.js +0 -0
  19. package/lib/cloudflare/api.d.ts +38 -1
  20. package/lib/cloudflare/api.js +55 -4
  21. package/lib/cloudflare/bucket.d.ts +118 -3
  22. package/lib/cloudflare/bucket.js +269 -89
  23. package/lib/cloudflare/custom-domain.d.ts +75 -0
  24. package/lib/cloudflare/custom-domain.js +154 -0
  25. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  26. package/lib/cloudflare/index.d.ts +5 -1
  27. package/lib/cloudflare/index.js +5 -1
  28. package/lib/cloudflare/permission-groups.d.ts +78 -0
  29. package/lib/cloudflare/permission-groups.js +48 -0
  30. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  31. package/lib/cloudflare/r2-rest-state-store.js +3 -2
  32. package/lib/cloudflare/static-site.d.ts +17 -18
  33. package/lib/cloudflare/static-site.js +19 -10
  34. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  35. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  36. package/lib/fs/file-system-state-store.js +1 -1
  37. package/lib/fs/file.d.ts +18 -0
  38. package/lib/fs/file.js +6 -0
  39. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +10 -4
  40. package/lib/internal/docs/providers.js +196 -0
  41. package/lib/secret.d.ts +3 -0
  42. package/lib/secret.js +13 -0
  43. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  44. package/lib/{util/serde.js → serde.js} +12 -4
  45. package/lib/test/bun.js +3 -2
  46. package/lib/util/sha256.d.ts +1 -0
  47. package/lib/util/sha256.js +4 -0
  48. package/lib/web/vitepress/config.d.ts +2 -1
  49. package/lib/web/vitepress/config.js +1 -0
  50. package/lib/web/vitepress/index.d.ts +1 -0
  51. package/lib/web/vitepress/index.js +1 -0
  52. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  53. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  54. package/lib/web/vitepress/vitepress.js +3 -2
  55. package/package.json +8 -4
  56. package/src/ai/document.ts +26 -2
  57. package/src/ai/index.ts +0 -2
  58. package/src/alchemy.ts +56 -2
  59. package/src/apply.ts +21 -20
  60. package/src/aws/account-id.ts +6 -2
  61. package/src/aws/credentials.ts +6 -0
  62. package/src/aws/oidc/oidc-provider.ts +17 -17
  63. package/src/aws/role.ts +68 -54
  64. package/src/cloudflare/account-api-token.ts +365 -0
  65. package/src/cloudflare/account-id.ts +0 -0
  66. package/src/cloudflare/api.ts +88 -17
  67. package/src/cloudflare/bucket.ts +493 -133
  68. package/src/cloudflare/custom-domain.ts +318 -0
  69. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  70. package/src/cloudflare/index.ts +5 -1
  71. package/src/cloudflare/permission-groups.ts +137 -0
  72. package/src/cloudflare/r2-rest-state-store.ts +14 -13
  73. package/src/cloudflare/static-site.ts +20 -32
  74. package/src/dns/import-dns.ts +4 -4
  75. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  76. package/src/fs/file-system-state-store.ts +5 -5
  77. package/src/fs/file.ts +29 -0
  78. package/src/internal/docs/providers.ts +281 -0
  79. package/src/secret.ts +16 -0
  80. package/src/{util/serde.ts → serde.ts} +16 -10
  81. package/src/test/bun.ts +8 -7
  82. package/src/util/sha256.ts +5 -0
  83. package/src/web/vitepress/config.ts +3 -1
  84. package/src/web/vitepress/index.ts +1 -0
  85. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  86. package/src/web/vitepress/vitepress.ts +3 -2
  87. package/lib/ai/approve.d.ts +0 -99
  88. package/lib/ai/approve.js +0 -76
  89. package/lib/ai/review.d.ts +0 -122
  90. package/lib/ai/review.js +0 -101
  91. package/lib/internal/getting-started.d.ts +0 -21
  92. package/lib/internal/getting-started.js +0 -87
  93. package/lib/internal/index.d.ts +0 -3
  94. package/lib/internal/index.js +0 -3
  95. package/lib/internal/providers.js +0 -172
  96. package/lib/internal/tutorial.d.ts +0 -104
  97. package/lib/internal/tutorial.js +0 -251
  98. package/src/ai/approve.ts +0 -163
  99. package/src/ai/review.ts +0 -213
  100. package/src/internal/getting-started.ts +0 -115
  101. package/src/internal/index.ts +0 -3
  102. package/src/internal/providers.ts +0 -241
  103. package/src/internal/tutorial.ts +0 -392
  104. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
@@ -1,7 +1,8 @@
1
1
  import type { Scope } from "../scope";
2
+ import type { Secret } from "../secret";
3
+ import { deserialize, serialize } from "../serde";
2
4
  import type { State, StateStore } from "../state";
3
5
  import { withExponentialBackoff } from "../util/retry";
4
- import { deserialize, serialize } from "../util/serde";
5
6
  import { type CloudflareApi, createCloudflareApi } from "./api";
6
7
 
7
8
  /**
@@ -23,7 +24,7 @@ export interface CloudflareR2StateStoreOptions {
23
24
  /**
24
25
  * API key to use (overrides CLOUDFLARE_API_KEY env var)
25
26
  */
26
- apiKey?: string;
27
+ apiKey?: Secret;
27
28
 
28
29
  /**
29
30
  * Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
@@ -44,7 +45,7 @@ export class R2RestStateStore implements StateStore {
44
45
  private api: CloudflareApi;
45
46
  private prefix: string;
46
47
  private bucketName: string;
47
- private apiKey: string | undefined;
48
+ private apiKey: Secret | undefined;
48
49
  private accountId: string | undefined;
49
50
  private email: string | undefined;
50
51
  private initialized = false;
@@ -57,7 +58,7 @@ export class R2RestStateStore implements StateStore {
57
58
  */
58
59
  constructor(
59
60
  public readonly scope: Scope,
60
- options: CloudflareR2StateStoreOptions,
61
+ options: CloudflareR2StateStoreOptions
61
62
  ) {
62
63
  // Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
63
64
  const scopePath = scope.chain.join("/");
@@ -129,7 +130,7 @@ export class R2RestStateStore implements StateStore {
129
130
  errors: [{ message: response.statusText }],
130
131
  }));
131
132
  throw new Error(
132
- `Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`,
133
+ `Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`
133
134
  );
134
135
  }
135
136
 
@@ -144,7 +145,7 @@ export class R2RestStateStore implements StateStore {
144
145
  objects.map((obj: any) => {
145
146
  const keyName = obj.key || obj.name;
146
147
  return this.convertKeyFromStorage(keyName.slice(this.prefix.length));
147
- }),
148
+ })
148
149
  );
149
150
 
150
151
  // Update cursor for next page if available
@@ -176,7 +177,7 @@ export class R2RestStateStore implements StateStore {
176
177
 
177
178
  try {
178
179
  const response = await this.api.get(
179
- `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`,
180
+ `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
180
181
  );
181
182
 
182
183
  if (!response.ok) {
@@ -188,7 +189,7 @@ export class R2RestStateStore implements StateStore {
188
189
  errors: [{ message: response.statusText }],
189
190
  }));
190
191
  throw new Error(
191
- `Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`,
192
+ `Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`
192
193
  );
193
194
  }
194
195
 
@@ -267,7 +268,7 @@ export class R2RestStateStore implements StateStore {
267
268
  headers: {
268
269
  "Content-Type": "application/json",
269
270
  },
270
- },
271
+ }
271
272
  );
272
273
 
273
274
  if (!response.ok) {
@@ -275,7 +276,7 @@ export class R2RestStateStore implements StateStore {
275
276
  errors: [{ message: response.statusText }],
276
277
  }));
277
278
  throw new Error(
278
- `Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`,
279
+ `Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`
279
280
  );
280
281
  }
281
282
 
@@ -285,7 +286,7 @@ export class R2RestStateStore implements StateStore {
285
286
  (error) =>
286
287
  error.message?.includes("503") || error.message?.includes("timeout"),
287
288
  5, // 5 retry attempts
288
- 1000, // Start with 1 second delay
289
+ 1000 // Start with 1 second delay
289
290
  );
290
291
  }
291
292
 
@@ -298,7 +299,7 @@ export class R2RestStateStore implements StateStore {
298
299
  await this.ensureInitialized();
299
300
 
300
301
  const response = await this.api.delete(
301
- `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`,
302
+ `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
302
303
  );
303
304
 
304
305
  if (!response.ok && response.status !== 404) {
@@ -306,7 +307,7 @@ export class R2RestStateStore implements StateStore {
306
307
  errors: [{ message: response.statusText }],
307
308
  }));
308
309
  throw new Error(
309
- `Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`,
310
+ `Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`
310
311
  );
311
312
  }
312
313
  }
@@ -96,16 +96,6 @@ export interface StaticSiteProps {
96
96
  */
97
97
  production?: boolean;
98
98
 
99
- /**
100
- * Custom domain for the site
101
- */
102
- domain?:
103
- | string
104
- | {
105
- name: string;
106
- redirects?: string[];
107
- };
108
-
109
99
  build?: {
110
100
  /**
111
101
  * Command to run before deploying the site
@@ -176,16 +166,6 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
176
166
  */
177
167
  assets: string[];
178
168
 
179
- /**
180
- * Custom domain for the site
181
- */
182
- domain?:
183
- | string
184
- | {
185
- name: string;
186
- redirects?: string[];
187
- };
188
-
189
169
  /**
190
170
  * Whether the site is deployed to production
191
171
  */
@@ -243,13 +223,26 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
243
223
  * });
244
224
  *
245
225
  * @example
246
- * // Create a static site with custom error page and index
247
- * const customSite = await StaticSite("custom-site", {
226
+ * // Create a static site with a custom domain using the CustomDomain resource
227
+ * const site = await StaticSite("custom-site", {
248
228
  * name: "custom-site",
249
229
  * dir: "./www",
250
230
  * errorPage: "404.html",
251
- * indexPage: "home.html",
252
- * domain: "www.example.com"
231
+ * indexPage: "home.html"
232
+ * });
233
+ *
234
+ * // Then configure the custom domain separately
235
+ * const domain = await CustomDomain("custom-domain", {
236
+ * name: "www.example.com",
237
+ * zoneId: "abcdef123456789",
238
+ * workerName: site.name
239
+ * });
240
+ *
241
+ * @example
242
+ * // Create a static site with multiple domains (primary + redirects)
243
+ * const site = await StaticSite("multi-domain-site", {
244
+ * name: "multi-domain-site",
245
+ * dir: "./public"
253
246
  * });
254
247
  *
255
248
  * @see https://developers.cloudflare.com/workers/platform/sites
@@ -264,6 +257,9 @@ export const StaticSite = Resource(
264
257
  id: string,
265
258
  props: StaticSiteProps
266
259
  ) {
260
+ // Create Cloudflare API client with automatic account discovery
261
+ const api = await createCloudflareApi();
262
+
267
263
  if (this.phase === "delete") {
268
264
  // For delete operations, we'll rely on the Worker delete to clean up
269
265
  // Return empty output for deleted state
@@ -322,9 +318,6 @@ export const StaticSite = Resource(
322
318
  const siteName = props.name;
323
319
  const indexPage = props.indexPage || "index.html";
324
320
 
325
- // Create Cloudflare API client with automatic account discovery
326
- const api = await createCloudflareApi();
327
-
328
321
  // Step 1: Create or get the KV namespace for assets
329
322
  const [kv, assetManifest] = await Promise.all([
330
323
  KVNamespace("assets", {
@@ -333,10 +326,6 @@ export const StaticSite = Resource(
333
326
  generateAssetManifest(props.dir),
334
327
  ]);
335
328
 
336
- console.log({
337
- assetManifest,
338
- });
339
-
340
329
  // Step 3: Upload assets to KV
341
330
  await uploadAssetManifest(api, kv.namespaceId, assetManifest);
342
331
 
@@ -419,7 +408,6 @@ export const StaticSite = Resource(
419
408
  assets: assetManifest.map((item) => item.key),
420
409
  createdAt: this.output?.createdAt || now,
421
410
  updatedAt: now,
422
- domain: props.domain,
423
411
  production: props.production !== false,
424
412
  url: worker.url,
425
413
  routes,
@@ -134,7 +134,7 @@ export const ImportDnsRecords = Resource(
134
134
  async function (
135
135
  this: Context<ImportDnsRecords>,
136
136
  id: string,
137
- props: ImportDnsRecordsProps,
137
+ props: ImportDnsRecordsProps
138
138
  ): Promise<ImportDnsRecords> {
139
139
  // For delete phase, just return destroyed state since this is a read-only resource
140
140
  if (this.phase === "delete") {
@@ -152,7 +152,7 @@ export const ImportDnsRecords = Resource(
152
152
  headers: {
153
153
  accept: "application/dns-json",
154
154
  },
155
- },
155
+ }
156
156
  );
157
157
 
158
158
  if (!res.ok) {
@@ -197,7 +197,7 @@ export const ImportDnsRecords = Resource(
197
197
  } catch (error) {
198
198
  console.warn(
199
199
  `Failed to fetch ${type} records for ${props.domain}:`,
200
- error,
200
+ error
201
201
  );
202
202
  }
203
203
  }
@@ -209,5 +209,5 @@ export const ImportDnsRecords = Resource(
209
209
  records: allRecords,
210
210
  importedAt: Date.now(),
211
211
  });
212
- },
212
+ }
213
213
  );
@@ -7,17 +7,14 @@ import sodium from "libsodium-wrappers";
7
7
  * @param key - The encryption key
8
8
  * @returns The base64-encoded encrypted value with nonce
9
9
  */
10
- export async function encryptWithKey(
11
- value: string,
12
- key: string,
13
- ): Promise<string> {
10
+ export async function encrypt(value: string, key: string): Promise<string> {
14
11
  // Initialize libsodium
15
12
  await sodium.ready;
16
13
 
17
14
  // Derive a key from the passphrase
18
15
  const cryptoKey = sodium.crypto_generichash(
19
16
  sodium.crypto_secretbox_KEYBYTES,
20
- sodium.from_string(key),
17
+ sodium.from_string(key)
21
18
  );
22
19
 
23
20
  // Generate a random nonce
@@ -27,7 +24,7 @@ export async function encryptWithKey(
27
24
  const encryptedBin = sodium.crypto_secretbox_easy(
28
25
  sodium.from_string(value),
29
26
  nonce,
30
- cryptoKey,
27
+ cryptoKey
31
28
  );
32
29
 
33
30
  // Combine nonce and ciphertext, then encode to base64
@@ -47,7 +44,7 @@ export async function encryptWithKey(
47
44
  */
48
45
  export async function decryptWithKey(
49
46
  encryptedValue: string,
50
- key: string,
47
+ key: string
51
48
  ): Promise<string> {
52
49
  // Initialize libsodium
53
50
  await sodium.ready;
@@ -55,13 +52,13 @@ export async function decryptWithKey(
55
52
  // Derive a key from the passphrase
56
53
  const cryptoKey = sodium.crypto_generichash(
57
54
  sodium.crypto_secretbox_KEYBYTES,
58
- sodium.from_string(key),
55
+ sodium.from_string(key)
59
56
  );
60
57
 
61
58
  // Decode the base64 combined value
62
59
  const combined = sodium.from_base64(
63
60
  encryptedValue,
64
- sodium.base64_variants.ORIGINAL,
61
+ sodium.base64_variants.ORIGINAL
65
62
  );
66
63
 
67
64
  // Extract nonce and ciphertext
@@ -72,7 +69,7 @@ export async function decryptWithKey(
72
69
  const decryptedBin = sodium.crypto_secretbox_open_easy(
73
70
  ciphertext,
74
71
  nonce,
75
- cryptoKey,
72
+ cryptoKey
76
73
  );
77
74
 
78
75
  return sodium.to_string(decryptedBin);
@@ -1,9 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import type { Scope } from "../scope";
4
+ import { deserialize, serialize } from "../serde";
4
5
  import type { State, StateStore } from "../state";
5
6
  import { ignore } from "../util/ignore";
6
- import { deserialize, serialize } from "../util/serde";
7
7
 
8
8
  const stateRootDir = path.join(process.cwd(), ".alchemy");
9
9
 
@@ -48,7 +48,7 @@ export class FileSystemStateStore implements StateStore {
48
48
  const content = await fs.readFile(await this.getPath(key), "utf8");
49
49
  const state = (await deserialize(
50
50
  this.scope,
51
- JSON.parse(content),
51
+ JSON.parse(content)
52
52
  )) as State;
53
53
  if (state.output === undefined) {
54
54
  state.output = {} as any;
@@ -66,7 +66,7 @@ export class FileSystemStateStore implements StateStore {
66
66
  async set(key: string, value: State): Promise<void> {
67
67
  return fs.writeFile(
68
68
  await this.getPath(key),
69
- JSON.stringify(await serialize(this.scope, value), null, 2),
69
+ JSON.stringify(await serialize(this.scope, value), null, 2)
70
70
  );
71
71
  }
72
72
 
@@ -88,9 +88,9 @@ export class FileSystemStateStore implements StateStore {
88
88
  return [] as const;
89
89
  }
90
90
  return [[id, s]] as const;
91
- }),
91
+ })
92
92
  )
93
- ).flat(),
93
+ ).flat()
94
94
  );
95
95
  }
96
96
 
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
 
@@ -78,6 +100,13 @@ alchemy.files = async (
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
  */
@@ -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
+ }