alchemy 0.3.1 → 0.4.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 (113) 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/assets.d.ts +74 -0
  22. package/lib/cloudflare/assets.js +73 -0
  23. package/lib/cloudflare/bindings.d.ts +6 -1
  24. package/lib/cloudflare/bindings.js +6 -0
  25. package/lib/cloudflare/bucket.d.ts +118 -3
  26. package/lib/cloudflare/bucket.js +269 -89
  27. package/lib/cloudflare/custom-domain.d.ts +75 -0
  28. package/lib/cloudflare/custom-domain.js +154 -0
  29. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  30. package/lib/cloudflare/index.d.ts +6 -1
  31. package/lib/cloudflare/index.js +6 -1
  32. package/lib/cloudflare/permission-groups.d.ts +78 -0
  33. package/lib/cloudflare/permission-groups.js +48 -0
  34. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  35. package/lib/cloudflare/r2-rest-state-store.js +55 -24
  36. package/lib/cloudflare/static-site.d.ts +17 -18
  37. package/lib/cloudflare/static-site.js +19 -10
  38. package/lib/cloudflare/worker.d.ts +14 -0
  39. package/lib/cloudflare/worker.js +150 -3
  40. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  41. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  42. package/lib/fs/file-system-state-store.js +1 -1
  43. package/lib/fs/file.d.ts +18 -0
  44. package/lib/fs/file.js +6 -0
  45. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +10 -4
  46. package/lib/internal/docs/providers.js +196 -0
  47. package/lib/secret.d.ts +3 -0
  48. package/lib/secret.js +13 -0
  49. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  50. package/lib/{util/serde.js → serde.js} +12 -4
  51. package/lib/test/bun.js +3 -2
  52. package/lib/util/sha256.d.ts +1 -0
  53. package/lib/util/sha256.js +4 -0
  54. package/lib/web/vitepress/config.d.ts +2 -1
  55. package/lib/web/vitepress/config.js +1 -0
  56. package/lib/web/vitepress/index.d.ts +1 -0
  57. package/lib/web/vitepress/index.js +1 -0
  58. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  59. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  60. package/lib/web/vitepress/vitepress.js +3 -2
  61. package/package.json +8 -4
  62. package/src/ai/document.ts +26 -2
  63. package/src/ai/index.ts +0 -2
  64. package/src/alchemy.ts +56 -2
  65. package/src/apply.ts +21 -20
  66. package/src/aws/account-id.ts +6 -2
  67. package/src/aws/credentials.ts +6 -0
  68. package/src/aws/oidc/oidc-provider.ts +17 -17
  69. package/src/aws/role.ts +68 -54
  70. package/src/cloudflare/account-api-token.ts +365 -0
  71. package/src/cloudflare/account-id.ts +0 -0
  72. package/src/cloudflare/api.ts +88 -17
  73. package/src/cloudflare/assets.ts +155 -0
  74. package/src/cloudflare/bindings.ts +11 -2
  75. package/src/cloudflare/bucket.ts +493 -133
  76. package/src/cloudflare/custom-domain.ts +318 -0
  77. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  78. package/src/cloudflare/index.ts +6 -1
  79. package/src/cloudflare/permission-groups.ts +137 -0
  80. package/src/cloudflare/r2-rest-state-store.ts +87 -42
  81. package/src/cloudflare/static-site.ts +20 -32
  82. package/src/cloudflare/worker.ts +253 -5
  83. package/src/dns/import-dns.ts +4 -4
  84. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  85. package/src/fs/file-system-state-store.ts +5 -5
  86. package/src/fs/file.ts +29 -0
  87. package/src/internal/docs/providers.ts +281 -0
  88. package/src/secret.ts +16 -0
  89. package/src/{util/serde.ts → serde.ts} +16 -10
  90. package/src/test/bun.ts +8 -7
  91. package/src/util/sha256.ts +5 -0
  92. package/src/web/vitepress/config.ts +3 -1
  93. package/src/web/vitepress/index.ts +1 -0
  94. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  95. package/src/web/vitepress/vitepress.ts +3 -2
  96. package/lib/ai/approve.d.ts +0 -99
  97. package/lib/ai/approve.js +0 -76
  98. package/lib/ai/review.d.ts +0 -122
  99. package/lib/ai/review.js +0 -101
  100. package/lib/internal/getting-started.d.ts +0 -21
  101. package/lib/internal/getting-started.js +0 -87
  102. package/lib/internal/index.d.ts +0 -3
  103. package/lib/internal/index.js +0 -3
  104. package/lib/internal/providers.js +0 -172
  105. package/lib/internal/tutorial.d.ts +0 -104
  106. package/lib/internal/tutorial.js +0 -251
  107. package/src/ai/approve.ts +0 -163
  108. package/src/ai/review.ts +0 -213
  109. package/src/internal/getting-started.ts +0 -115
  110. package/src/internal/index.ts +0 -3
  111. package/src/internal/providers.ts +0 -241
  112. package/src/internal/tutorial.ts +0 -392
  113. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
package/lib/fs/file.d.ts CHANGED
@@ -45,6 +45,24 @@ declare module "../alchemy" {
45
45
  */
46
46
  files(paths: string[]): Promise<FileCollection>;
47
47
  files(path: string, ...paths: string[]): Promise<FileCollection>;
48
+ /**
49
+ * Gets all of the files in a directory.
50
+ * @param path Path to the directory
51
+ * @param props Optional properties
52
+ * @returns Promise resolving to a FileCollection
53
+ *
54
+ * @example
55
+ * // Get all files in a directory
56
+ * const files = await alchemy.folder("./docs");
57
+ *
58
+ */
59
+ folder(path: string, props?: {
60
+ /**
61
+ * Whether to recursively get all files in the directory
62
+ * @default false
63
+ */
64
+ recursive?: boolean;
65
+ }): Promise<FileCollection>;
48
66
  }
49
67
  }
50
68
  /**
package/lib/fs/file.js CHANGED
@@ -17,6 +17,12 @@ alchemy.files = async (...args) => {
17
17
  ]))),
18
18
  };
19
19
  };
20
+ alchemy.folder = async (dir, props) => {
21
+ const files = await fs.promises.readdir(dir, {
22
+ recursive: props?.recursive ?? false,
23
+ });
24
+ return alchemy.files(files.map((file) => path.join(dir, file)));
25
+ };
20
26
  /**
21
27
  * File Resource
22
28
  *
@@ -1,5 +1,5 @@
1
- import { Document } from "../ai";
2
- import { Folder } from "../fs";
1
+ import { Document } from "../../ai";
2
+ import { Folder } from "../../fs";
3
3
  export interface DocsProps {
4
4
  /**
5
5
  * The output directory for the docs.
@@ -18,10 +18,16 @@ export interface DocsProps {
18
18
  * @default true (all docs)
19
19
  */
20
20
  filter?: boolean | number;
21
+ /**
22
+ * Whether to run in parallel.
23
+ *
24
+ * @default true
25
+ */
26
+ parallel?: boolean;
21
27
  }
22
- export type AlchemyProviderDocs = {
28
+ export type Providers = {
23
29
  dir: string;
24
30
  provider: string;
25
31
  documents: Document[];
26
32
  }[];
27
- export declare function AlchemyProviderDocs({ srcDir, outDir, filter, }: DocsProps): Promise<AlchemyProviderDocs>;
33
+ export declare function Providers({ srcDir, outDir, filter, parallel, }: DocsProps): Promise<Providers>;
@@ -0,0 +1,196 @@
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 Providers({ srcDir, outDir, filter, parallel = true, }) {
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
+ if (parallel) {
32
+ return await Promise.all(providers.map((provider) => generateProviderDocs({ provider, outDir, parallel })));
33
+ }
34
+ else {
35
+ const generatedProviders = [];
36
+ for (const provider of providers) {
37
+ generatedProviders.push(await generateProviderDocs({ provider, outDir, parallel }));
38
+ }
39
+ return generatedProviders;
40
+ }
41
+ }
42
+ async function generateProviderDocs({ provider, outDir, parallel, }) {
43
+ const providerName = path.basename(provider);
44
+ const files = (await fs.readdir(path.resolve(provider), {
45
+ withFileTypes: true,
46
+ }))
47
+ .filter((dirent) => dirent.isFile())
48
+ .map((dirent) => path.relative(process.cwd(), path.resolve(provider, dirent.name)))
49
+ .filter((file) => file.endsWith(".ts") && !file.endsWith("index.ts"));
50
+ const { object: { groups }, } = await Data(`docs/${providerName}`, {
51
+ model: {
52
+ id: "o3-mini",
53
+ provider: "openai",
54
+ options: {
55
+ reasoningEffort: "high",
56
+ },
57
+ },
58
+ temperature: 0.1,
59
+ schema: type({
60
+ groups: type({
61
+ identifier: type("string").describe("The identifier of the file's primary exported Resource/Function/Type, e.g. Bucket or StaticSite, AstroFile, TypeScriptFile"),
62
+ filename: type("string").describe("The filename of the Resource's Document, e.g. bucket.md or static-site.md"),
63
+ category: type("'Resource'|'Client'|'Utility'|'Types'").describe("The classification of the Resource's Document, one of: Resource, Client, Utility, or Types."),
64
+ }).array(),
65
+ }),
66
+ system: await alchemy `
67
+ 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.
68
+ You will be provided with a list of documents and instructions on how to classify them.
69
+ Each document has a title, file name, and category.
70
+ `,
71
+ prompt: await alchemy `
72
+ Identify and classify the documents that need to be written for the '${provider}' Service's Alchemy Resources.
73
+ For background knowledge on Alchemy, see ${alchemy.file("./README.md")}.
74
+ For background knowledge on the structure of an Alchemy Resource, see ${alchemy.file("./.cursorrules")}.
75
+
76
+ The ${provider} Service has the following resources:
77
+ ${alchemy.files(files)}
78
+
79
+ 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(...).
80
+ A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
81
+ A file is considered a "Utility" if it contains utility functions that are not resources or clients.
82
+ A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
83
+
84
+ 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.
85
+
86
+ // "Resource Name"
87
+ const ResourceName = Resource(...)
88
+ `,
89
+ });
90
+ const providerDocsDir = (await Folder(path.join(outDir, providerName))).path;
91
+ let documents = [];
92
+ if (parallel) {
93
+ documents = await Promise.all(groups.filter((g) => g.category === "Resource").map(generateDocument));
94
+ }
95
+ else {
96
+ for (const g of groups.filter((g) => g.category === "Resource")) {
97
+ documents.push(await generateDocument(g));
98
+ }
99
+ }
100
+ async function generateDocument(g) {
101
+ return Document(`docs/${providerName}/${g.identifier}`, {
102
+ title: g.identifier,
103
+ path: path.join(providerDocsDir, `${g.filename.replace(".ts", "").replace(".md", "")}.md`),
104
+ freeze: true,
105
+ model: {
106
+ id: "claude-3-5-sonnet-latest",
107
+ provider: "anthropic",
108
+ // options: {
109
+ // reasoningEffort: "high",
110
+ // },
111
+ },
112
+ prompt: await alchemy `
113
+ You are a technical writer writing API documentation for an Alchemy IaC Resource.
114
+ See ${alchemy.file("./README.md")} to understand the overview of Alchemy.
115
+ See ${alchemy.file("./.cursorrules")} to better understand the structure and convention of an Alchemy Resource.
116
+
117
+ Relevant files for the ${providerName} Service:
118
+ ${alchemy.files(files)}
119
+
120
+ Write concise documentation for the "${g.identifier}" Resource.
121
+
122
+ > [!CAUTION]
123
+ > Avoid the temptation to over explain or over describe. Focus on concise, simple, high value snippets. One heading and 0-1 descriptions per snippet.
124
+
125
+ > [!TIP]
126
+ > Make sure the examples follow a natural progression from the minimal example to logical next steps of how the Resource might be used.
127
+
128
+ Each document must follow the following format:
129
+
130
+ # ${g.identifier}
131
+
132
+ (simple description with an external link to the provider's website)
133
+ e.g.
134
+ The Efs component lets you add [Amazon Elastic File System (EFS)](https://docs.aws.amazon.com/efs/latest/ug/whatisefs.html) to your app.
135
+
136
+ # Minimal Example
137
+
138
+ (brief 1-2 sentences of what it does)
139
+
140
+ \`\`\`ts
141
+ import { ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
142
+
143
+ (example)
144
+ \`\`\`
145
+
146
+ # (one heading per variation)
147
+
148
+ (brief 1-2 sentences of what it does)
149
+
150
+ \`\`\`ts
151
+ import { ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
152
+
153
+ (example)
154
+ \`\`\`
155
+
156
+ Before writing the document, think through:
157
+ 1. What is the minimal, most common example use case for this resource?
158
+ 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.
159
+ 3. Make sure to draw from the examples and your understanding of Alchemy.
160
+
161
+ Refer to alchemy docs to understand the context of how this documentation is consumed:
162
+ - ${alchemy.file("./alchemy-web/docs/what-is-alchemy.md")}
163
+ - ${alchemy.file("./alchemy-web/docs/getting-started.md")}
164
+ - ${alchemy.folder("./alchemy-web/docs/concepts/")}
165
+
166
+ ${providerName === "cloudflare"
167
+ ? await alchemy `# Bind to a Worker
168
+ (if it is a Cloudflare Resource)
169
+
170
+ (brief 1-2 sentences of what it does)
171
+
172
+ \`\`\`ts
173
+ import { Worker, ${g.identifier.replaceAll(" ", "")} } from "alchemy/${providerName}";
174
+
175
+ const myResource = await ${g.identifier.replaceAll(" ", "")}("my-resource", {
176
+ // ...
177
+ });
178
+
179
+ await Worker("my-worker", {
180
+ name: "my-worker",
181
+ script: "console.log('Hello, world!')",
182
+ bindings: {
183
+ myResource,
184
+ },
185
+ });
186
+ \`\`\``
187
+ : ""}
188
+ `,
189
+ });
190
+ }
191
+ return {
192
+ dir: providerDocsDir,
193
+ provider: providerName,
194
+ documents,
195
+ };
196
+ }
package/lib/secret.d.ts CHANGED
@@ -72,3 +72,6 @@ export declare function isSecret(binding: any): binding is Secret;
72
72
  * @throws {Error} If no password is set in the alchemy application options or current scope
73
73
  */
74
74
  export declare function secret<S extends string | undefined>(unencrypted: S): Secret;
75
+ export declare namespace secret {
76
+ function env(name: string, value?: string, error?: string): Promise<Secret>;
77
+ }
package/lib/secret.js CHANGED
@@ -82,3 +82,16 @@ export function secret(unencrypted) {
82
82
  }
83
83
  return new Secret(unencrypted);
84
84
  }
85
+ (function (secret) {
86
+ async function env(name, value, error) {
87
+ const alchemy = await import("./alchemy");
88
+ const result = await alchemy.env(name, value, error);
89
+ if (typeof result === "string") {
90
+ return secret(result);
91
+ }
92
+ else {
93
+ throw new Error(`Secret environment variable ${name} is not a string`);
94
+ }
95
+ }
96
+ secret.env = env;
97
+ })(secret || (secret = {}));
@@ -1,4 +1,4 @@
1
- import { Scope } from "../scope";
1
+ import { Scope } from "./scope";
2
2
  export declare function serialize(scope: Scope, value: any, options?: {
3
3
  encrypt?: boolean;
4
4
  }): Promise<any>;
@@ -1,6 +1,6 @@
1
- import { Scope } from "../scope";
2
- import { Secret } from "../secret";
3
- import { decryptWithKey, encryptWithKey } from "./encrypt";
1
+ import { decryptWithKey, encrypt } from "./encrypt";
2
+ import { Scope } from "./scope";
3
+ import { Secret } from "./secret";
4
4
  // zero-dependency type guard for ArkType
5
5
  function isType(value) {
6
6
  return (value &&
@@ -17,7 +17,7 @@ export async function serialize(scope, value, options) {
17
17
  }
18
18
  return {
19
19
  "@secret": options?.encrypt !== false
20
- ? await encryptWithKey(value.unencrypted, scope.password)
20
+ ? await encrypt(value.unencrypted, scope.password)
21
21
  : value.unencrypted,
22
22
  };
23
23
  }
@@ -26,6 +26,11 @@ export async function serialize(scope, value, options) {
26
26
  "@schema": value.toJSON(),
27
27
  };
28
28
  }
29
+ else if (value instanceof Date) {
30
+ return {
31
+ "@date": value.toISOString(),
32
+ };
33
+ }
29
34
  else if (value instanceof Scope) {
30
35
  return undefined;
31
36
  }
@@ -51,6 +56,9 @@ export async function deserialize(scope, value) {
51
56
  else if ("@schema" in value) {
52
57
  return value["@schema"];
53
58
  }
59
+ else if ("@date" in value) {
60
+ return new Date(value["@date"]);
61
+ }
54
62
  else {
55
63
  return Object.fromEntries(await Promise.all(Object.entries(value).map(async ([key, value]) => [
56
64
  key,
package/lib/test/bun.js CHANGED
@@ -32,12 +32,13 @@ alchemy.test = test;
32
32
  * ```
33
33
  */
34
34
  export function test(meta, defaultOptions) {
35
+ let isFailed = false;
35
36
  defaultOptions = defaultOptions ?? {};
36
37
  if (defaultOptions.stateStore === undefined &&
37
38
  // process.env.CI &&
38
39
  process.env.ALCHEMY_STATE_STORE === "cloudflare") {
39
40
  defaultOptions.stateStore = (scope) => new R2RestStateStore(scope, {
40
- apiKey: process.env.CLOUDFLARE_API_KEY,
41
+ apiKey: alchemy.secret(process.env.CLOUDFLARE_API_KEY),
41
42
  email: process.env.CLOUDFLARE_EMAIL,
42
43
  bucketName: process.env.CLOUDFLARE_BUCKET_NAME,
43
44
  });
@@ -70,7 +71,7 @@ export function test(meta, defaultOptions) {
70
71
  };
71
72
  // Clean up test scope after all tests complete
72
73
  afterAll(async () => {
73
- if (defaultOptions?.destroy !== false) {
74
+ if (defaultOptions?.destroy !== false && !isFailed) {
74
75
  await alchemy.destroy(test.scope);
75
76
  }
76
77
  });
@@ -0,0 +1 @@
1
+ export declare function sha256(value: string): string;
@@ -0,0 +1,4 @@
1
+ import { createHash } from "node:crypto";
2
+ export function sha256(value) {
3
+ return createHash("sha256").update(value).digest("hex");
4
+ }
@@ -1,8 +1,9 @@
1
- import type { DefaultTheme, ThemeOptions } from "vitepress";
1
+ import type { DefaultTheme, HeadConfig, ThemeOptions } from "vitepress";
2
2
  export type VitePressConfigProps = {
3
3
  cwd: string;
4
4
  title?: string;
5
5
  description?: string;
6
+ head?: HeadConfig[];
6
7
  theme?: ThemeOptions;
7
8
  themeConfig: DefaultTheme.Config;
8
9
  };
@@ -9,6 +9,7 @@ import { defineConfig } from "vitepress";
9
9
  export default defineConfig({
10
10
  title: ${JSON.stringify(props.title || "Alchemy")},
11
11
  description: ${JSON.stringify(props.description || "Alchemy Docs")},
12
+ head: ${JSON.stringify(props.head || [])},
12
13
  markdown: {
13
14
  // @ts-ignore
14
15
  codeTransformers: [transformerTwoslash()],
@@ -1,4 +1,5 @@
1
1
  export * from "./config";
2
2
  export * from "./custom-theme";
3
3
  export * from "./home-page";
4
+ export * from "./process-front-matter-files";
4
5
  export * from "./vitepress";
@@ -1,4 +1,5 @@
1
1
  export * from "./config";
2
2
  export * from "./custom-theme";
3
3
  export * from "./home-page";
4
+ export * from "./process-front-matter-files";
4
5
  export * from "./vitepress";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Process markdown files with frontmatter to generate navigation items
3
+ * @param directoryPath The directory containing markdown files
4
+ * @param linkPrefix The prefix for navigation links
5
+ * @returns Sorted array of navigation items with text and link properties
6
+ */
7
+ export declare function processFrontmatterFiles(directoryPath: string, linkPrefix: string): Promise<{
8
+ text: string;
9
+ link: string;
10
+ items?: {
11
+ text: string;
12
+ link: string;
13
+ items?: {
14
+ text: string;
15
+ link: string;
16
+ }[];
17
+ }[];
18
+ }[]>;
@@ -0,0 +1,68 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ /**
4
+ * Process markdown files with frontmatter to generate navigation items
5
+ * @param directoryPath The directory containing markdown files
6
+ * @param linkPrefix The prefix for navigation links
7
+ * @returns Sorted array of navigation items with text and link properties
8
+ */
9
+ export async function processFrontmatterFiles(directoryPath, linkPrefix) {
10
+ const entries = await fs.readdir(directoryPath, { withFileTypes: true });
11
+ const processedEntries = await Promise.all(entries
12
+ .filter((entry) => !entry.name.endsWith("index.md"))
13
+ .map(async (entry) => {
14
+ const fullPath = path.join(directoryPath, entry.name);
15
+ if (entry.isDirectory()) {
16
+ // Process subdirectory recursively
17
+ const items = await processFrontmatterFiles(fullPath, `${linkPrefix}/${entry.name}`);
18
+ // Create section for subdirectory
19
+ return {
20
+ text: entry.name.charAt(0).toUpperCase() + entry.name.slice(1),
21
+ collapsed: false,
22
+ items,
23
+ order: 10000, // Default order for directories
24
+ };
25
+ }
26
+ // Process markdown file
27
+ if (!entry.name.endsWith(".md")) {
28
+ return null;
29
+ }
30
+ const content = await fs.readFile(fullPath, "utf-8");
31
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
32
+ let order = 10000;
33
+ let title;
34
+ if (frontmatterMatch) {
35
+ const frontmatter = frontmatterMatch[1];
36
+ const orderMatch = frontmatter.match(/order:\s*(\d+)/);
37
+ if (orderMatch) {
38
+ order = parseInt(orderMatch[1]);
39
+ }
40
+ const titleMatch = frontmatter.match(/title:\s*(.+)/);
41
+ if (titleMatch) {
42
+ title = titleMatch[1].trim();
43
+ }
44
+ }
45
+ // If no title in frontmatter, try to get first heading
46
+ if (!title) {
47
+ const headingMatch = content.match(/^#\s+(.+)$/m);
48
+ if (headingMatch) {
49
+ title = headingMatch[1].trim();
50
+ }
51
+ }
52
+ // Fall back to filename if no title found
53
+ if (!title) {
54
+ const name = entry.name.replace(".md", "");
55
+ title = name.charAt(0).toUpperCase() + name.slice(1);
56
+ }
57
+ return {
58
+ text: title,
59
+ link: `${linkPrefix}/${entry.name}`,
60
+ order,
61
+ };
62
+ }));
63
+ // Filter out null entries and sort by order
64
+ return processedEntries
65
+ .filter((entry) => entry !== null)
66
+ .sort((a, b) => a.order - b.order)
67
+ .map(({ text, link, items }) => items ? { text, items, collapsed: true } : { text, link });
68
+ }
@@ -14,8 +14,9 @@ export const VitepressProject = Resource("project::VitepressProject", {
14
14
  try {
15
15
  if (props.delete !== false) {
16
16
  if (await fs.exists(dir)) {
17
- // Delete the entire project directory
18
- await execAsync(`rm -rf ${dir}`);
17
+ await fs.rm(dir, { recursive: true, force: true });
18
+ // // Delete the entire project directory
19
+ // await execAsync(`rm -rf ${dir}`);
19
20
  }
20
21
  }
21
22
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
@@ -38,16 +38,18 @@
38
38
  "@aws-sdk/client-sesv2": "^3.0.0",
39
39
  "@aws-sdk/client-sqs": "^3.0.0",
40
40
  "@aws-sdk/client-sts": "^3.0.0",
41
- "arktype": "^2.0.0",
42
- "prettier": "^3.0.0",
43
41
  "@octokit/rest": "^21.1.1",
44
42
  "ai": "^4.0.0",
43
+ "arktype": "^2.0.0",
44
+ "aws4fetch": "^1.0.20",
45
+ "cloudflare": "^4.0.0",
45
46
  "diff": "^7.0.0",
46
47
  "esbuild": "^0.25.1",
47
48
  "glob": "^10.0.0",
48
49
  "hono": "^4.0.0",
49
- "libsodium-wrappers": "^0.7.15",
50
50
  "jszip": "^3.0.0",
51
+ "libsodium-wrappers": "^0.7.15",
52
+ "prettier": "^3.0.0",
51
53
  "stripe": "^17.0.0",
52
54
  "turndown": "^7.0.0",
53
55
  "yaml": "^2.0.0",
@@ -56,6 +58,7 @@
56
58
  "devDependencies": {
57
59
  "@ai-sdk/anthropic": "^1.1.6",
58
60
  "@ai-sdk/openai": "^1.1.9",
61
+ "@aws-sdk/client-s3": "3.726.1",
59
62
  "@biomejs/biome": "^1.9.4",
60
63
  "@cloudflare/workers-types": "^4.20250303.0",
61
64
  "@octokit/rest": "^21.1.1",
@@ -67,6 +70,7 @@
67
70
  "ai": "^4.1.16",
68
71
  "arktype": "^2.1.16",
69
72
  "change-case": "^5.4.4",
73
+ "cloudflare": "^4.2.0",
70
74
  "libsodium-wrappers": "^0.7.15",
71
75
  "openpgp": "^6.1.0",
72
76
  "prettier": "^3.5.3",
@@ -11,8 +11,10 @@ import { createModel, withRateLimitRetry, type ModelConfig } from "./client";
11
11
  export interface DocumentProps {
12
12
  /**
13
13
  * Title of the document
14
+ *
15
+ * @default id
14
16
  */
15
- title: string;
17
+ title?: string;
16
18
 
17
19
  /**
18
20
  * Optional path to the markdown document
@@ -82,12 +84,23 @@ export interface DocumentProps {
82
84
  * @default 10000
83
85
  */
84
86
  maxTokens?: number;
87
+
88
+ /**
89
+ * Freeze the document after creation (do not re-generate on updates)
90
+ * @default false
91
+ */
92
+ freeze?: boolean;
85
93
  }
86
94
 
87
95
  /**
88
96
  * A markdown document that can be created, updated, and deleted
89
97
  */
90
98
  export interface Document extends DocumentProps, Resource<"docs::Document"> {
99
+ /**
100
+ * The title of the document
101
+ */
102
+ title: string;
103
+
91
104
  /**
92
105
  * Content of the document
93
106
  */
@@ -221,6 +234,16 @@ export const Document = Resource(
221
234
  return this.destroy();
222
235
  }
223
236
 
237
+ if (this.phase === "update" && props.freeze) {
238
+ if (props.path) {
239
+ const filePath = props.path;
240
+ const fileId = `${id}-file`;
241
+
242
+ await StaticTextFile(fileId, filePath, this.output!.content);
243
+ }
244
+ return this(this.output);
245
+ }
246
+
224
247
  // Use provided system prompt or default
225
248
  const system = props.system || DEFAULT_MD_SYSTEM_PROMPT;
226
249
 
@@ -266,7 +289,7 @@ export const Document = Resource(
266
289
 
267
290
  if (retryResult.error) {
268
291
  throw new Error(
269
- `Failed to generate valid markdown content: ${retryResult.error}`
292
+ `Failed to generate valid markdown content: ${retryResult.error}\n${retryText}`
270
293
  );
271
294
  }
272
295
 
@@ -283,6 +306,7 @@ export const Document = Resource(
283
306
  ],
284
307
  createdAt: this.output?.createdAt || Date.now(),
285
308
  updatedAt: Date.now(),
309
+ title: props.title || id,
286
310
  };
287
311
 
288
312
  // Write file if path is provided
package/src/ai/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from "./approve";
2
1
  export * from "./ark";
3
2
  export * from "./astro-file";
4
3
  export * from "./css-file";
@@ -6,7 +5,6 @@ export * from "./data";
6
5
  export * from "./document";
7
6
  export * from "./html-file";
8
7
  export * from "./json-file";
9
- export * from "./review";
10
8
  export * from "./typescript-file";
11
9
  export * from "./vue-file";
12
10
  export * from "./yaml-file";