alchemy 0.2.3 → 0.2.5

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 (141) hide show
  1. package/lib/ai/ark.d.ts +11 -0
  2. package/lib/ai/ark.js +86 -0
  3. package/lib/ai/client.d.ts +51 -0
  4. package/lib/ai/client.js +29 -0
  5. package/lib/ai/data.d.ts +101 -0
  6. package/lib/ai/data.js +70 -0
  7. package/lib/ai/document.d.ts +101 -0
  8. package/lib/ai/document.js +82 -0
  9. package/lib/ai/index.d.ts +3 -0
  10. package/lib/ai/index.js +3 -0
  11. package/lib/alchemy.d.ts +83 -2
  12. package/lib/alchemy.js +31 -3
  13. package/lib/aws/bucket.d.ts +87 -1
  14. package/lib/aws/bucket.js +48 -1
  15. package/lib/aws/function.d.ts +186 -1
  16. package/lib/aws/function.js +62 -8
  17. package/lib/aws/oidc/github-oidc-provider.d.ts +81 -0
  18. package/lib/aws/oidc/github-oidc-provider.js +68 -0
  19. package/lib/aws/oidc/oidc-provider.d.ts +14 -6
  20. package/lib/aws/oidc/oidc-provider.js +26 -5
  21. package/lib/aws/policy-attachment.d.ts +44 -1
  22. package/lib/aws/policy-attachment.js +31 -0
  23. package/lib/aws/policy.d.ts +160 -1
  24. package/lib/aws/policy.js +78 -0
  25. package/lib/aws/queue.d.ts +83 -1
  26. package/lib/aws/queue.js +39 -0
  27. package/lib/aws/role.d.ts +162 -1
  28. package/lib/aws/role.js +114 -0
  29. package/lib/aws/ses.d.ts +57 -2
  30. package/lib/aws/ses.js +42 -1
  31. package/lib/aws/table.d.ts +96 -1
  32. package/lib/aws/table.js +36 -0
  33. package/lib/cloudflare/asset-manifest.js +0 -1
  34. package/lib/cloudflare/bound.js +0 -1
  35. package/lib/cloudflare/bucket.d.ts +37 -1
  36. package/lib/cloudflare/bucket.js +36 -0
  37. package/lib/cloudflare/durable-object-namespace.d.ts +25 -0
  38. package/lib/cloudflare/durable-object-namespace.js +22 -0
  39. package/lib/cloudflare/kv-namespace.d.ts +37 -1
  40. package/lib/cloudflare/kv-namespace.js +36 -0
  41. package/lib/cloudflare/static-site.d.ts +53 -1
  42. package/lib/cloudflare/static-site.js +53 -1
  43. package/lib/cloudflare/worker-metadata.js +0 -1
  44. package/lib/cloudflare/worker.d.ts +64 -1
  45. package/lib/cloudflare/worker.js +63 -0
  46. package/lib/cloudflare/wrangler.json.d.ts +1 -1
  47. package/lib/cloudflare/zone-settings.js +0 -1
  48. package/lib/cloudflare/zone.d.ts +56 -1
  49. package/lib/cloudflare/zone.js +55 -0
  50. package/lib/destroy.js +1 -1
  51. package/lib/esbuild/bundle.d.ts +43 -4
  52. package/lib/esbuild/bundle.js +16 -0
  53. package/lib/fs/file-collection.d.ts +16 -0
  54. package/lib/fs/file-collection.js +6 -0
  55. package/lib/fs/file-ref.d.ts +14 -0
  56. package/lib/fs/file-ref.js +6 -0
  57. package/lib/fs/file.d.ts +71 -25
  58. package/lib/fs/file.js +20 -44
  59. package/lib/fs/folder.d.ts +37 -5
  60. package/lib/fs/folder.js +26 -2
  61. package/lib/fs/index.d.ts +6 -0
  62. package/lib/fs/index.js +6 -0
  63. package/lib/fs/json-file.d.ts +16 -0
  64. package/lib/fs/json-file.js +7 -0
  65. package/lib/fs/text-file.d.ts +12 -0
  66. package/lib/fs/text-file.js +7 -0
  67. package/lib/fs/typescript-file.d.ts +19 -0
  68. package/lib/fs/typescript-file.js +14 -0
  69. package/lib/fs/yaml-file.d.ts +19 -0
  70. package/lib/fs/yaml-file.js +8 -0
  71. package/lib/github/secret.d.ts +63 -2
  72. package/lib/github/secret.js +61 -1
  73. package/lib/internal/docs.d.ts +5 -0
  74. package/lib/internal/docs.js +287 -0
  75. package/lib/resource.d.ts +1 -1
  76. package/lib/secret.d.ts +67 -0
  77. package/lib/secret.js +67 -0
  78. package/lib/shadcn/component.d.ts +1 -1
  79. package/lib/stripe/price.d.ts +51 -1
  80. package/lib/stripe/price.js +50 -0
  81. package/lib/stripe/product.d.ts +39 -1
  82. package/lib/stripe/product.js +38 -0
  83. package/lib/stripe/webhook.d.ts +46 -1
  84. package/lib/stripe/webhook.js +45 -0
  85. package/lib/test/bun.d.ts +64 -0
  86. package/lib/test/bun.js +35 -3
  87. package/lib/util/serde.js +14 -0
  88. package/lib/vite/vite.d.ts +1 -1
  89. package/lib/vitepress/dependencies.d.ts +2 -6
  90. package/lib/vitepress/vitepress.d.ts +12 -1
  91. package/lib/vitepress/vitepress.js +29 -23
  92. package/package.json +8 -3
  93. package/src/ai/ark.ts +118 -0
  94. package/src/ai/client.ts +78 -0
  95. package/src/ai/data.ts +149 -0
  96. package/src/ai/document.ts +165 -0
  97. package/src/ai/index.ts +3 -0
  98. package/src/alchemy.ts +88 -5
  99. package/src/aws/bucket.ts +111 -7
  100. package/src/aws/function.ts +231 -10
  101. package/src/aws/oidc/github-oidc-provider.ts +83 -0
  102. package/src/aws/oidc/oidc-provider.ts +39 -7
  103. package/src/aws/policy-attachment.ts +44 -1
  104. package/src/aws/policy.ts +177 -2
  105. package/src/aws/queue.ts +89 -0
  106. package/src/aws/role.ts +174 -2
  107. package/src/aws/ses.ts +56 -1
  108. package/src/aws/table.ts +103 -0
  109. package/src/cloudflare/bucket.ts +36 -0
  110. package/src/cloudflare/durable-object-namespace.ts +25 -0
  111. package/src/cloudflare/kv-namespace.ts +36 -0
  112. package/src/cloudflare/static-site.ts +53 -1
  113. package/src/cloudflare/worker.ts +63 -0
  114. package/src/cloudflare/zone.ts +55 -0
  115. package/src/destroy.ts +1 -1
  116. package/src/esbuild/bundle.ts +53 -3
  117. package/src/fs/file-collection.ts +24 -0
  118. package/src/fs/file-ref.ts +22 -0
  119. package/src/fs/file.ts +70 -77
  120. package/src/fs/folder.ts +44 -5
  121. package/src/fs/index.ts +6 -0
  122. package/src/fs/json-file.ts +23 -0
  123. package/src/fs/text-file.ts +19 -0
  124. package/src/fs/typescript-file.ts +36 -0
  125. package/src/fs/yaml-file.ts +26 -0
  126. package/src/github/secret.ts +65 -2
  127. package/src/internal/docs.ts +338 -0
  128. package/src/resource.ts +1 -1
  129. package/src/secret.ts +67 -0
  130. package/src/stripe/price.ts +50 -0
  131. package/src/stripe/product.ts +38 -0
  132. package/src/stripe/webhook.ts +45 -0
  133. package/src/test/bun.ts +83 -3
  134. package/src/util/serde.ts +17 -0
  135. package/src/vitepress/vitepress.ts +47 -26
  136. package/lib/docs/document.d.ts +0 -123
  137. package/lib/docs/document.js +0 -67
  138. package/lib/docs/index.d.ts +0 -1
  139. package/lib/docs/index.js +0 -1
  140. package/src/docs/document.ts +0 -225
  141. package/src/docs/index.ts +0 -1
package/src/test/bun.ts CHANGED
@@ -6,35 +6,111 @@ import { alchemy } from "../alchemy";
6
6
  import { destroy } from "../destroy";
7
7
  import type { Scope } from "../scope";
8
8
 
9
+ /**
10
+ * Extend the Alchemy interface to include test functionality
11
+ */
9
12
  declare module "../alchemy" {
10
13
  interface Alchemy {
11
14
  test: typeof test;
12
15
  }
13
16
  }
14
17
 
18
+ /**
19
+ * Add test functionality to alchemy instance
20
+ */
15
21
  alchemy.test = test;
16
22
 
23
+ /**
24
+ * Options for configuring test behavior
25
+ */
17
26
  export interface TestOptions {
27
+ /**
28
+ * Whether to automatically destroy resources after test.
29
+ * @default true.
30
+ */
18
31
  destroy?: boolean;
32
+
33
+ /**
34
+ * Whether to suppress logging output.
35
+ * @default false.
36
+ */
19
37
  quiet?: boolean;
38
+
39
+ /**
40
+ * Password to use for test resources.
41
+ * @default "test-password".
42
+ */
20
43
  password?: string;
21
44
  }
22
45
 
46
+ /**
47
+ * Global test scope for all tests
48
+ */
23
49
  const globalTestScope = alchemy.scope("test");
24
50
 
51
+ /**
52
+ * Test function type definition with overloads
53
+ */
25
54
  type test = {
55
+ /**
56
+ * Create a test with default options
57
+ * @param name Test name
58
+ * @param fn Test function
59
+ * @param timeout Optional timeout in milliseconds
60
+ */
26
61
  (name: string, fn: (scope: Scope) => Promise<any>, timeout?: number): void;
62
+
63
+ /**
64
+ * Create a test with custom options
65
+ * @param name Test name
66
+ * @param options Test configuration options
67
+ * @param fn Test function
68
+ * @param timeout Optional timeout in milliseconds
69
+ */
27
70
  (
28
71
  name: string,
29
72
  options: TestOptions,
30
73
  fn: (scope: Scope) => Promise<any>,
31
74
  timeout?: number,
32
75
  ): void;
76
+
77
+ /**
78
+ * Skip test conditionally
79
+ * @param condition If true, test will be skipped
80
+ */
33
81
  skipIf(condition: boolean): test;
82
+
83
+ /**
84
+ * Current test scope
85
+ */
34
86
  scope: Scope;
35
87
  };
36
88
 
89
+ /**
90
+ * Creates a test helper function that provides scoped resource management
91
+ *
92
+ * @param meta Import meta object from the test file
93
+ * @param defaultOptions Default options to apply to all tests
94
+ * @returns Test function with scope management
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * const test = alchemy.test(import.meta);
99
+ *
100
+ * describe("My Resource", () => {
101
+ * test("create and delete", async (scope) => {
102
+ * try {
103
+ * const resource = await MyResource("test", { ... });
104
+ * expect(resource.id).toBeTruthy();
105
+ * } finally {
106
+ * await alchemy.destroy(scope);
107
+ * }
108
+ * });
109
+ * });
110
+ * ```
111
+ */
37
112
  export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
113
+ // Add skipIf functionality
38
114
  test.skipIf = (condition: boolean) => {
39
115
  if (condition) {
40
116
  // TODO: proxy through to bun:test.skipIf
@@ -43,11 +119,13 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
43
119
  return test;
44
120
  };
45
121
 
122
+ // Create local test scope based on filename
46
123
  const localTestScope = alchemy.scope(path.basename(meta.filename), {
47
124
  parent: globalTestScope,
48
125
  });
49
126
  test.scope = localTestScope;
50
127
 
128
+ // Clean up test scope after all tests complete
51
129
  afterAll(() => alchemy.destroy(test.scope));
52
130
 
53
131
  return test as any;
@@ -71,13 +149,16 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
71
149
  ),
72
150
  )
73
151
  : {};
152
+
153
+ // Merge options with defaults
74
154
  const options = {
75
- destroy: false,
155
+ destroy: true,
76
156
  quiet: false,
77
157
  password: "test-password",
78
158
  ...spread(defaultOptions),
79
159
  ...spread(_options),
80
160
  };
161
+
81
162
  const fn = typeof args[1] === "function" ? args[1] : args[2]!;
82
163
 
83
164
  return alchemy.run(
@@ -88,13 +169,12 @@ export function test(meta: ImportMeta, defaultOptions?: TestOptions): test {
88
169
  },
89
170
  async (scope) => {
90
171
  return it(testName, async () => {
91
- // bun will call from a different scope, so we need to enter the test's scope manually
172
+ // Enter test scope since bun calls from different scope
92
173
  scope.enter();
93
174
  try {
94
175
  await fn(scope);
95
176
  } finally {
96
177
  if (options.destroy !== false) {
97
- // TODO: auto-destroy resources
98
178
  await destroy(scope);
99
179
  }
100
180
  }
package/src/util/serde.ts CHANGED
@@ -2,6 +2,17 @@ import { Scope } from "../scope";
2
2
  import { Secret } from "../secret";
3
3
  import { decryptWithKey, encryptWithKey } from "./encrypt";
4
4
 
5
+ import type { Type } from "arktype";
6
+
7
+ // zero-dependency type guard for ArkType
8
+ function isType(value: any): value is Type<any, any> {
9
+ return (
10
+ value &&
11
+ typeof value === "object" &&
12
+ typeof value.toJsonSchema === "function"
13
+ );
14
+ }
15
+
5
16
  export async function serialize(
6
17
  scope: Scope,
7
18
  value: any,
@@ -21,6 +32,10 @@ export async function serialize(
21
32
  ? await encryptWithKey(value.unencrypted, scope.password)
22
33
  : value.unencrypted,
23
34
  };
35
+ } else if (isType(value)) {
36
+ return {
37
+ "@schema": value.toJSON(),
38
+ };
24
39
  } else if (value instanceof Scope) {
25
40
  return undefined;
26
41
  } else if (value && typeof value === "object") {
@@ -47,6 +62,8 @@ export async function deserialize(scope: Scope, value: any): Promise<any> {
47
62
  throw new Error("Cannot deserialize secret without password");
48
63
  }
49
64
  return new Secret(await decryptWithKey(value["@secret"], scope.password));
65
+ } else if ("@schema" in value) {
66
+ return value["@schema"];
50
67
  } else {
51
68
  return Object.fromEntries(
52
69
  await Promise.all(
@@ -70,6 +70,19 @@ export interface VitePressProjectProps {
70
70
  * The home page configuration
71
71
  */
72
72
  home: HomePage;
73
+
74
+ /**
75
+ * Whether to delete the project folder during the delete phase
76
+ * @default true
77
+ */
78
+ delete?: boolean;
79
+
80
+ /**
81
+ * The directory to generate the docs in
82
+ *
83
+ * @default {@link name}
84
+ */
85
+ dir?: string;
73
86
  }
74
87
 
75
88
  export interface VitePressProject extends VitePressProjectProps, Resource {
@@ -89,11 +102,14 @@ export const VitePressProject = Resource(
89
102
  id: string,
90
103
  props: VitePressProjectProps,
91
104
  ): Promise<VitePressProject> {
105
+ const dir = props.dir ?? props.name;
92
106
  if (this.phase === "delete") {
93
107
  try {
94
- if (await fs.exists(props.name)) {
95
- // Delete the entire project directory
96
- await execAsync(`rm -rf ${props.name}`);
108
+ if (props.delete !== false) {
109
+ if (await fs.exists(dir)) {
110
+ // Delete the entire project directory
111
+ await execAsync(`rm -rf ${dir}`);
112
+ }
97
113
  }
98
114
  } catch (error) {
99
115
  console.error(`Error deleting VitePress project ${id}:`, error);
@@ -101,7 +117,12 @@ export const VitePressProject = Resource(
101
117
  return this.destroy();
102
118
  }
103
119
 
104
- const cwd = (await Folder(props.name)).path;
120
+ const cwd = (
121
+ await Folder("dir", {
122
+ path: dir,
123
+ delete: props.delete,
124
+ })
125
+ ).path;
105
126
 
106
127
  // Initialize package.json
107
128
  await JsonFile(path.join(cwd, "package.json"), {
@@ -142,28 +163,28 @@ export const VitePressProject = Resource(
142
163
  path.join(cwd, "index.md"),
143
164
  `---\n${yaml.stringify(props.home)}---\n`,
144
165
  ),
145
- TypeScriptFile(
146
- path.join(cwd, "alchemy.run.ts"),
147
- `import alchemy from "alchemy";
148
- import { Folder } from "alchemy/fs";
149
- import { Document } from "alchemy/docs";
150
- import path from "path";
151
-
152
- await using _ = alchemy("alchemy.run", {
153
- stage: "prod",
154
- phase: process.argv.includes("--destroy") ? "destroy" : "up",
155
- password: process.env.SECRET_PASSPHRASE,
156
- quiet: !process.argv.includes("--verbose"),
157
- });
158
-
159
- const docs = await Folder(path.join("alchemy.run", "docs"));
160
-
161
- await Document("home.md", {
162
- path: path.join(docs.path, "home.md"),
163
- prompt: await alchemy\`Generate a landing page.\`,
164
- });
165
- `,
166
- ),
166
+ // TypeScriptFile(
167
+ // path.join(cwd, "alchemy.run.ts"),
168
+ // `import alchemy from "alchemy";
169
+ // import { Folder } from "alchemy/fs";
170
+ // import { Document } from "alchemy/docs";
171
+ // import path from "path";
172
+
173
+ // await using _ = alchemy("alchemy.run", {
174
+ // stage: "prod",
175
+ // phase: process.argv.includes("--destroy") ? "destroy" : "up",
176
+ // password: process.env.SECRET_PASSPHRASE,
177
+ // quiet: !process.argv.includes("--verbose"),
178
+ // });
179
+
180
+ // const docs = await Folder(path.join("alchemy.run", "docs"));
181
+
182
+ // await Document("home.md", {
183
+ // path: path.join(docs.path, "home.md"),
184
+ // prompt: await alchemy\`Generate a landing page.\`,
185
+ // });
186
+ // `,
187
+ // ),
167
188
  TypeScriptFile(
168
189
  path.join(cwd, ".vitepress", "theme", "index.ts"),
169
190
  `import TwoslashFloatingVue from "@shikijs/vitepress-twoslash/client";
@@ -1,123 +0,0 @@
1
- import type { Context } from "../context";
2
- import { Resource } from "../resource";
3
- import type { Secret } from "../secret";
4
- /**
5
- * Base properties for a Document
6
- */
7
- interface BaseDocumentProps {
8
- /**
9
- * Path to the markdown document
10
- */
11
- path: string;
12
- /**
13
- * Base URL for the OpenAI API
14
- * @default 'https://api.openai.com/v1'
15
- */
16
- baseURL?: string;
17
- }
18
- /**
19
- * Properties for creating a document with static content
20
- */
21
- interface StaticDocumentProps extends BaseDocumentProps {
22
- /**
23
- * Static content of the document
24
- */
25
- content: string;
26
- /**
27
- * Prompt for generating content
28
- * @internal This is marked as never to ensure content and prompt are mutually exclusive
29
- */
30
- prompt?: never;
31
- /**
32
- * OpenAI API key
33
- * @internal This is marked as never since it's not needed for static content
34
- */
35
- apiKey?: never;
36
- /**
37
- * Model configuration
38
- * @internal This is marked as never since it's not needed for static content
39
- */
40
- model?: never;
41
- /**
42
- * Context files
43
- * @internal This is marked as never since it's not needed for static content
44
- */
45
- context?: never;
46
- }
47
- /**
48
- * Properties for creating a document with AI-generated content
49
- */
50
- interface GeneratedDocumentProps extends BaseDocumentProps {
51
- /**
52
- * Static content of the document
53
- * @internal This is marked as never to ensure content and prompt are mutually exclusive
54
- */
55
- content?: never;
56
- /**
57
- * Prompt for generating content
58
- * Can be either a single string or an array of strings that will be joined with newlines
59
- */
60
- prompt: string | string[];
61
- /**
62
- * Optional context files to include in the prompt
63
- * Key is the relative path to the file
64
- * Value can be either the content string or another Document resource
65
- */
66
- context?: {
67
- [relativePath: string]: string | Document | {
68
- path: string;
69
- content: string;
70
- };
71
- };
72
- /**
73
- * OpenAI API key to use for generating content
74
- * If not provided, will use OPENAI_API_KEY environment variable
75
- */
76
- apiKey?: Secret;
77
- /**
78
- * Model configuration
79
- */
80
- model?: {
81
- /**
82
- * Model ID to use
83
- * @default 'gpt-4o'
84
- */
85
- id?: string;
86
- /**
87
- * Model provider name
88
- * @default 'openai'
89
- */
90
- provider?: string;
91
- /**
92
- * Model-specific options
93
- */
94
- options?: Record<string, unknown>;
95
- };
96
- }
97
- /**
98
- * Properties for creating or updating a Document
99
- * Must provide either static content or a prompt for AI generation
100
- */
101
- export type DocumentProps = StaticDocumentProps | GeneratedDocumentProps;
102
- /**
103
- * A markdown document that can be created, updated, and deleted
104
- */
105
- export interface Document extends Omit<DocumentProps, "content" | "prompt" | "context">, Resource<"docs::Document"> {
106
- /**
107
- * Content of the document
108
- */
109
- content: string;
110
- /**
111
- * Time at which the document was created
112
- */
113
- createdAt: number;
114
- /**
115
- * Time at which the document was last updated
116
- */
117
- updatedAt: number;
118
- }
119
- /**
120
- * Resource for managing markdown documents using the Vercel AI SDK
121
- */
122
- export declare const Document: ((this: Context<Document, DocumentProps>, id: string, props: DocumentProps) => Promise<Document>) | (((this: any, id: string, props?: DocumentProps | undefined) => never) & (new (_: never) => never));
123
- export {};
@@ -1,67 +0,0 @@
1
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2
- import { generateText } from "ai";
3
- import fs from "node:fs/promises";
4
- import path from "node:path";
5
- import { Resource } from "../resource";
6
- /**
7
- * Resource for managing markdown documents using the Vercel AI SDK
8
- */
9
- export const Document = Resource("docs::Document", async function (id, props) {
10
- // Ensure directory exists
11
- await fs.mkdir(path.dirname(props.path), { recursive: true });
12
- if (this.phase === "delete") {
13
- try {
14
- await fs.unlink(props.path);
15
- }
16
- catch (error) {
17
- // Ignore if file doesn't exist
18
- if (error.code !== "ENOENT") {
19
- throw error;
20
- }
21
- }
22
- return this.destroy();
23
- }
24
- let content;
25
- console.log("Generating content for", props.path);
26
- // Generate content if prompt is provided
27
- if ("prompt" in props) {
28
- // Get API key from props or environment
29
- const apiKey = props.apiKey?.unencrypted || process.env.OPENAI_API_KEY;
30
- if (!apiKey) {
31
- throw new Error("OpenAI API key is required for content generation");
32
- }
33
- // Initialize OpenAI compatible provider
34
- const provider = createOpenAICompatible({
35
- name: props.model?.provider || "openai",
36
- apiKey,
37
- baseURL: props.baseURL || "https://api.openai.com/v1",
38
- });
39
- // Build the full prompt with context if provided
40
- const fullPrompt = Array.isArray(props.prompt)
41
- ? props.prompt.join("\n")
42
- : props.prompt;
43
- // console.log(fullPrompt);
44
- const { text } = await generateText({
45
- model: provider(props.model?.id || "gpt-4o"),
46
- prompt: fullPrompt,
47
- ...(props.model?.options || {}),
48
- });
49
- content = text;
50
- }
51
- else {
52
- // Use static content
53
- content = props.content;
54
- }
55
- // Write content to file
56
- await fs.writeFile(props.path, content);
57
- // Get file stats for timestamps
58
- const stats = await fs.stat(props.path);
59
- // Return the resource
60
- return this({
61
- path: props.path,
62
- baseURL: props.baseURL,
63
- content,
64
- createdAt: stats.birthtimeMs,
65
- updatedAt: stats.mtimeMs,
66
- });
67
- });
@@ -1 +0,0 @@
1
- export * from "./document";
package/lib/docs/index.js DELETED
@@ -1 +0,0 @@
1
- export * from "./document";