alchemy 0.2.2 → 0.2.4

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 (161) 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/document.d.ts +97 -0
  6. package/lib/ai/document.js +82 -0
  7. package/lib/ai/index.d.ts +2 -0
  8. package/lib/ai/index.js +2 -0
  9. package/lib/ai/object.d.ts +101 -0
  10. package/lib/ai/object.js +70 -0
  11. package/lib/alchemy.d.ts +84 -2
  12. package/lib/alchemy.js +114 -10
  13. package/lib/apply.d.ts +1 -1
  14. package/lib/apply.js +13 -10
  15. package/lib/aws/bucket.d.ts +87 -1
  16. package/lib/aws/bucket.js +48 -1
  17. package/lib/aws/function.d.ts +186 -1
  18. package/lib/aws/function.js +62 -8
  19. package/lib/aws/oidc/github-oidc-provider.d.ts +81 -0
  20. package/lib/aws/oidc/github-oidc-provider.js +68 -0
  21. package/lib/aws/oidc/oidc-provider.d.ts +14 -6
  22. package/lib/aws/oidc/oidc-provider.js +26 -5
  23. package/lib/aws/policy-attachment.d.ts +44 -1
  24. package/lib/aws/policy-attachment.js +31 -0
  25. package/lib/aws/policy.d.ts +160 -1
  26. package/lib/aws/policy.js +78 -0
  27. package/lib/aws/queue.d.ts +83 -1
  28. package/lib/aws/queue.js +39 -0
  29. package/lib/aws/role.d.ts +162 -1
  30. package/lib/aws/role.js +114 -0
  31. package/lib/aws/ses.d.ts +57 -2
  32. package/lib/aws/ses.js +42 -1
  33. package/lib/aws/table.d.ts +96 -1
  34. package/lib/aws/table.js +36 -0
  35. package/lib/cloudflare/asset-manifest.js +0 -1
  36. package/lib/cloudflare/bound.js +0 -1
  37. package/lib/cloudflare/bucket.d.ts +37 -1
  38. package/lib/cloudflare/bucket.js +36 -0
  39. package/lib/cloudflare/durable-object-namespace.d.ts +25 -0
  40. package/lib/cloudflare/durable-object-namespace.js +22 -0
  41. package/lib/cloudflare/kv-namespace.d.ts +37 -1
  42. package/lib/cloudflare/kv-namespace.js +36 -0
  43. package/lib/cloudflare/static-site.d.ts +53 -1
  44. package/lib/cloudflare/static-site.js +53 -1
  45. package/lib/cloudflare/worker-metadata.js +0 -1
  46. package/lib/cloudflare/worker.d.ts +64 -1
  47. package/lib/cloudflare/worker.js +63 -0
  48. package/lib/cloudflare/wrangler.json.d.ts +1 -1
  49. package/lib/cloudflare/zone-settings.js +0 -1
  50. package/lib/cloudflare/zone.d.ts +56 -1
  51. package/lib/cloudflare/zone.js +55 -0
  52. package/lib/context.d.ts +9 -4
  53. package/lib/context.js +4 -2
  54. package/lib/destroy.js +9 -8
  55. package/lib/esbuild/bundle.d.ts +43 -4
  56. package/lib/esbuild/bundle.js +16 -0
  57. package/lib/fs/file-collection.d.ts +16 -0
  58. package/lib/fs/file-collection.js +6 -0
  59. package/lib/fs/file-ref.d.ts +14 -0
  60. package/lib/fs/file-ref.js +6 -0
  61. package/lib/fs/file.d.ts +78 -5
  62. package/lib/fs/file.js +39 -3
  63. package/lib/fs/folder.d.ts +37 -5
  64. package/lib/fs/folder.js +28 -3
  65. package/lib/fs/index.d.ts +6 -0
  66. package/lib/fs/index.js +6 -0
  67. package/lib/fs/json-file.d.ts +16 -0
  68. package/lib/fs/json-file.js +7 -0
  69. package/lib/fs/text-file.d.ts +12 -0
  70. package/lib/fs/text-file.js +7 -0
  71. package/lib/fs/typescript-file.d.ts +19 -0
  72. package/lib/fs/typescript-file.js +14 -0
  73. package/lib/fs/yaml-file.d.ts +19 -0
  74. package/lib/fs/yaml-file.js +8 -0
  75. package/lib/github/secret.d.ts +63 -2
  76. package/lib/github/secret.js +61 -1
  77. package/lib/internal/docs.d.ts +5 -0
  78. package/lib/internal/docs.js +324 -0
  79. package/lib/resource.d.ts +2 -2
  80. package/lib/resource.js +4 -0
  81. package/lib/scope.d.ts +1 -1
  82. package/lib/scope.js +6 -6
  83. package/lib/secret.d.ts +67 -0
  84. package/lib/secret.js +67 -0
  85. package/lib/shadcn/component.d.ts +1 -1
  86. package/lib/state.d.ts +1 -1
  87. package/lib/state.js +8 -1
  88. package/lib/stripe/price.d.ts +51 -1
  89. package/lib/stripe/price.js +50 -0
  90. package/lib/stripe/product.d.ts +39 -1
  91. package/lib/stripe/product.js +38 -0
  92. package/lib/stripe/webhook.d.ts +46 -1
  93. package/lib/stripe/webhook.js +45 -0
  94. package/lib/test/bun.d.ts +64 -0
  95. package/lib/test/bun.js +35 -3
  96. package/lib/util/serde.d.ts +3 -1
  97. package/lib/util/serde.js +20 -4
  98. package/lib/vite/vite.d.ts +1 -1
  99. package/lib/vitepress/dependencies.d.ts +12 -0
  100. package/lib/vitepress/dependencies.js +35 -0
  101. package/lib/vitepress/home-page.d.ts +133 -0
  102. package/lib/vitepress/home-page.js +11 -0
  103. package/lib/vitepress/index.d.ts +2 -0
  104. package/lib/vitepress/index.js +2 -0
  105. package/lib/vitepress/vitepress.d.ts +71 -0
  106. package/lib/vitepress/vitepress.js +266 -0
  107. package/package.json +22 -10
  108. package/src/ai/ark.ts +118 -0
  109. package/src/ai/client.ts +78 -0
  110. package/src/ai/document.ts +160 -0
  111. package/src/ai/index.ts +2 -0
  112. package/src/ai/object.ts +149 -0
  113. package/src/alchemy.ts +192 -17
  114. package/src/apply.ts +25 -13
  115. package/src/aws/bucket.ts +111 -7
  116. package/src/aws/function.ts +231 -10
  117. package/src/aws/oidc/github-oidc-provider.ts +83 -0
  118. package/src/aws/oidc/oidc-provider.ts +39 -7
  119. package/src/aws/policy-attachment.ts +44 -1
  120. package/src/aws/policy.ts +177 -2
  121. package/src/aws/queue.ts +89 -0
  122. package/src/aws/role.ts +174 -2
  123. package/src/aws/ses.ts +56 -1
  124. package/src/aws/table.ts +103 -0
  125. package/src/cloudflare/bucket.ts +36 -0
  126. package/src/cloudflare/durable-object-namespace.ts +25 -0
  127. package/src/cloudflare/kv-namespace.ts +36 -0
  128. package/src/cloudflare/static-site.ts +53 -1
  129. package/src/cloudflare/worker.ts +63 -0
  130. package/src/cloudflare/zone.ts +55 -0
  131. package/src/context.ts +30 -10
  132. package/src/destroy.ts +11 -8
  133. package/src/esbuild/bundle.ts +53 -3
  134. package/src/fs/file-collection.ts +24 -0
  135. package/src/fs/file-ref.ts +22 -0
  136. package/src/fs/file.ts +107 -6
  137. package/src/fs/folder.ts +45 -5
  138. package/src/fs/index.ts +6 -0
  139. package/src/fs/json-file.ts +23 -0
  140. package/src/fs/text-file.ts +19 -0
  141. package/src/fs/typescript-file.ts +36 -0
  142. package/src/fs/yaml-file.ts +26 -0
  143. package/src/github/secret.ts +65 -2
  144. package/src/internal/docs.ts +369 -0
  145. package/src/resource.ts +7 -2
  146. package/src/scope.ts +9 -9
  147. package/src/secret.ts +67 -0
  148. package/src/state.ts +9 -2
  149. package/src/stripe/price.ts +50 -0
  150. package/src/stripe/product.ts +38 -0
  151. package/src/stripe/webhook.ts +45 -0
  152. package/src/test/bun.ts +83 -3
  153. package/src/util/serde.ts +30 -4
  154. package/src/vitepress/dependencies.ts +68 -0
  155. package/src/vitepress/home-page.ts +166 -0
  156. package/src/vitepress/index.md +75 -0
  157. package/src/vitepress/index.ts +2 -0
  158. package/src/vitepress/vitepress.ts +382 -0
  159. package/lib/project/vite.d.ts +0 -90
  160. package/lib/project/vite.js +0 -228
  161. package/src/project/vite.ts +0 -406
@@ -0,0 +1,160 @@
1
+ import { generateText } from "ai";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import type { Context } from "../context";
5
+ import { Resource } from "../resource";
6
+ import type { Secret } from "../secret";
7
+ import {
8
+ type ModelConfig,
9
+ createClient,
10
+ getModelId,
11
+ getModelOptions,
12
+ } from "./client";
13
+
14
+ /**
15
+ * Properties for creating or updating a Document
16
+ */
17
+ export interface DocumentProps {
18
+ /**
19
+ * Path to the markdown document
20
+ */
21
+ path: string;
22
+
23
+ /**
24
+ * Base URL for the OpenAI API
25
+ * @default 'https://api.openai.com/v1'
26
+ */
27
+ baseURL?: string;
28
+
29
+ /**
30
+ * Prompt for generating content
31
+ * Use alchemy template literals to include file context:
32
+ * @example
33
+ * prompt: await alchemy`
34
+ * Generate docs using:
35
+ * ${alchemy.file("src/api.ts")}
36
+ * `
37
+ */
38
+ prompt: string;
39
+
40
+ /**
41
+ * OpenAI API key to use for generating content
42
+ * If not provided, will use OPENAI_API_KEY environment variable
43
+ */
44
+ apiKey?: Secret;
45
+
46
+ /**
47
+ * Model configuration
48
+ */
49
+ model?: ModelConfig;
50
+ }
51
+
52
+ /**
53
+ * A markdown document that can be created, updated, and deleted
54
+ */
55
+ export interface Document extends DocumentProps, Resource<"docs::Document"> {
56
+ /**
57
+ * Content of the document
58
+ */
59
+ content: string;
60
+
61
+ /**
62
+ * Time at which the document was created
63
+ */
64
+ createdAt: number;
65
+
66
+ /**
67
+ * Time at which the document was last updated
68
+ */
69
+ updatedAt: number;
70
+ }
71
+
72
+ /**
73
+ * Resource for managing AI-generated markdown documents using the Vercel AI SDK.
74
+ * Supports powerful context handling through the alchemy template literal tag.
75
+ *
76
+ * @example
77
+ * // Create a document using alchemy template literals for context
78
+ * const apiDocs = await Document("api-docs", {
79
+ * path: "./docs/api.md",
80
+ * prompt: await alchemy`
81
+ * Generate API documentation based on these source files:
82
+ * ${alchemy.file("src/api.ts")}
83
+ * ${alchemy.file("src/types.ts")}
84
+ * `
85
+ * // The above will automatically append the file contents as code blocks:
86
+ * //
87
+ * // Generate API documentation based on these source files:
88
+ * // [api.ts](src/api.ts)
89
+ * // [types.ts](src/types.ts)
90
+ * //
91
+ * // // src/api.ts
92
+ * // ```ts
93
+ * // ... contents of api.ts ...
94
+ * // ```
95
+ * //
96
+ * // // src/types.ts
97
+ * // ```ts
98
+ * // ... contents of types.ts ...
99
+ * // ```
100
+ * });
101
+ *
102
+ * @example
103
+ * // Use alchemy template literals with file collections
104
+ * const modelDocs = await Document("models", {
105
+ * path: "./docs/models.md",
106
+ * prompt: await alchemy`
107
+ * Write documentation for these data models:
108
+ * ${alchemy.files("src/models/user.ts", "src/models/post.ts")}
109
+ * `
110
+ * // This creates a prompt with all files appended as code blocks,
111
+ * // automatically handling syntax highlighting based on file extensions
112
+ * });
113
+ */
114
+ export const Document = Resource(
115
+ "docs::Document",
116
+ async function (
117
+ this: Context<Document>,
118
+ id: string,
119
+ props: DocumentProps,
120
+ ): Promise<Document> {
121
+ // Ensure directory exists
122
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
123
+
124
+ if (this.phase === "delete") {
125
+ try {
126
+ await fs.unlink(props.path);
127
+ } catch (error: any) {
128
+ // Ignore if file doesn't exist
129
+ if (error.code !== "ENOENT") {
130
+ throw error;
131
+ }
132
+ }
133
+ return this.destroy();
134
+ }
135
+
136
+ // Initialize OpenAI compatible provider using shared client
137
+ const provider = createClient(props);
138
+
139
+ // Generate content
140
+ const { text } = await generateText({
141
+ model: provider(getModelId(props)),
142
+ prompt: props.prompt,
143
+ ...getModelOptions(props),
144
+ });
145
+
146
+ // Write content to file
147
+ await fs.writeFile(props.path, text);
148
+
149
+ // Get file stats for timestamps
150
+ const stats = await fs.stat(props.path);
151
+
152
+ // Return the resource
153
+ return this({
154
+ ...props,
155
+ content: text,
156
+ createdAt: stats.birthtimeMs,
157
+ updatedAt: stats.mtimeMs,
158
+ });
159
+ },
160
+ );
@@ -0,0 +1,2 @@
1
+ export * from "./document";
2
+ export * from "./object";
@@ -0,0 +1,149 @@
1
+ import { generateObject } from "ai";
2
+ import type { JsonSchema, Type, type } from "arktype";
3
+ import type { Context } from "../context";
4
+ import { Resource } from "../resource";
5
+ import type { Secret } from "../secret";
6
+ import { ark } from "./ark";
7
+ import {
8
+ type ModelConfig,
9
+ createClient,
10
+ getModelId,
11
+ getModelOptions,
12
+ } from "./client";
13
+
14
+ /**
15
+ * Properties for creating or updating an AI Object
16
+ */
17
+ export interface ObjectProps<T extends Type<any, any>> {
18
+ /**
19
+ * The ArkType schema to validate and structure the generated content
20
+ */
21
+ schema: T | JsonSchema;
22
+
23
+ /**
24
+ * Prompt for generating the content
25
+ * Use alchemy template literals to include file context:
26
+ * @example
27
+ * prompt: await alchemy`
28
+ * Generate a description for:
29
+ * ${alchemy.file("src/data.ts")}
30
+ * `
31
+ */
32
+ prompt: string;
33
+
34
+ /**
35
+ * System prompt to guide the AI's behavior
36
+ * @example
37
+ * system: "You are a technical writer tasked with describing code"
38
+ */
39
+ system?: string;
40
+
41
+ /**
42
+ * Base URL for the OpenAI API
43
+ * @default 'https://api.openai.com/v1'
44
+ */
45
+ baseURL?: string;
46
+
47
+ /**
48
+ * OpenAI API key to use for generating content
49
+ * If not provided, will use OPENAI_API_KEY environment variable
50
+ */
51
+ apiKey?: Secret;
52
+
53
+ /**
54
+ * Model configuration
55
+ */
56
+ model?: ModelConfig;
57
+ }
58
+
59
+ /**
60
+ * A resource that uses AI to generate structured content based on a schema
61
+ */
62
+ export interface Object<T> extends Resource<"ai::Object"> {
63
+ type: JsonSchema;
64
+
65
+ /**
66
+ * The generated content, typed according to the provided schema
67
+ */
68
+ object: T;
69
+
70
+ /**
71
+ * Time at which the content was generated
72
+ */
73
+ createdAt: number;
74
+ }
75
+
76
+ /**
77
+ * Resource for generating structured content using the Vercel AI SDK.
78
+ * Supports powerful context handling through the alchemy template literal tag.
79
+ *
80
+ * @example
81
+ * // Generate a product description with specific fields
82
+ * const productSchema = type({
83
+ * name: "string",
84
+ * description: "string",
85
+ * features: "string[]",
86
+ * price: "number"
87
+ * });
88
+ *
89
+ * const product = await Object("new-product", {
90
+ * schema: productSchema,
91
+ * prompt: "Generate a product description for a new smartphone",
92
+ * system: "You are a product copywriter specializing in tech products"
93
+ * });
94
+ *
95
+ * console.log(product.content); // Typed as per schema
96
+ *
97
+ * @example
98
+ * // Generate code documentation with context
99
+ * const docSchema = type({
100
+ * summary: "string",
101
+ * parameters: {
102
+ * name: "string",
103
+ * type: "string",
104
+ * description: "string"
105
+ * }[],
106
+ * returns: "string"
107
+ * });
108
+ *
109
+ * const docs = await Object("function-docs", {
110
+ * schema: docSchema,
111
+ * prompt: await alchemy`
112
+ * Generate documentation for this function:
113
+ * ${alchemy.file("src/utils/format.ts")}
114
+ * `,
115
+ * system: "You are a technical documentation writer"
116
+ * });
117
+ */
118
+ export const Object = Resource("ai::Object", async function <
119
+ const T extends Type<any, any>,
120
+ >(this: Context<Object<any>>, id: string, props: ObjectProps<T>): Promise<
121
+ Object<type.infer<T>>
122
+ > {
123
+ if (this.phase === "delete") {
124
+ return this.destroy();
125
+ }
126
+
127
+ // Initialize OpenAI compatible provider using shared client
128
+ const provider = createClient(props);
129
+
130
+ // Generate structured output using generateObject
131
+ const { object } = await generateObject({
132
+ model: provider(getModelId(props)),
133
+ // Convert ArkType schema to Zod schema for generateObject
134
+ // This is needed because generateObject expects a Zod schema
135
+ schema: ark.schema<type.infer<T>>(props.schema),
136
+ system:
137
+ props.system ||
138
+ "You are an AI assistant tasked with generating structured content.",
139
+ prompt: props.prompt,
140
+ ...getModelOptions(props),
141
+ });
142
+
143
+ // Return the resource with typed content
144
+ return this({
145
+ type: props.schema,
146
+ object: object,
147
+ createdAt: Date.now(),
148
+ });
149
+ });
package/src/alchemy.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
1
4
  import { DestroyedSignal, destroy } from "./destroy";
2
5
  import { Scope } from "./scope";
3
6
  import { secret } from "./secret";
@@ -6,31 +9,175 @@ import type { StateStoreType } from "./state";
6
9
  // TODO: support browser
7
10
  const DEFAULT_STAGE = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
8
11
 
9
- function _alchemy(appName: string, options: Omit<AlchemyOptions, "appName">) {
10
- return scope(undefined, {
11
- ...options,
12
- appName,
13
- stage: options.stage,
14
- });
15
- }
16
- _alchemy.destroy = destroy;
17
- _alchemy.run = run;
18
- _alchemy.scope = scope;
19
- _alchemy.secret = secret;
20
-
21
- // alchemy type is to semantically highlight `alchemy` as a type (keyword)
12
+ /**
13
+ * Type alias for semantic highlighting of `alchemy` as a type keyword
14
+ */
22
15
  export type alchemy = Alchemy;
23
16
 
24
17
  export const alchemy: Alchemy = _alchemy as any;
25
18
 
26
- // Alchemy is for module augmentation
19
+ /**
20
+ * The Alchemy interface provides core functionality and is augmented by providers.
21
+ * Supports both application scoping with secrets and template string interpolation.
22
+ *
23
+ * @example
24
+ * // Create an application scope with stage and secret handling
25
+ * const app = alchemy("github:alchemy", {
26
+ * stage: "prod",
27
+ * phase: "up",
28
+ * // Required for encrypting/decrypting secrets
29
+ * password: process.env.SECRET_PASSPHRASE
30
+ * });
31
+ *
32
+ * // Create a resource with encrypted secrets
33
+ * const resource = await Resource("my-resource", {
34
+ * apiKey: alchemy.secret(process.env.API_KEY)
35
+ * });
36
+ *
37
+ * await app.finalize();
38
+ */
27
39
  export interface Alchemy {
28
40
  scope: typeof scope;
29
41
  run: typeof run;
30
42
  destroy: typeof destroy;
43
+ /**
44
+ * Creates an encrypted secret that can be safely stored in state files.
45
+ * Requires a password to be set either globally in the application options
46
+ * or locally in the current scope.
47
+ */
31
48
  secret: typeof secret;
49
+ /**
50
+ * Creates a new application scope with the given name and options.
51
+ * Used to create and manage resources with proper secret handling.
52
+ *
53
+ * @example
54
+ * const app = alchemy("my-app", {
55
+ * stage: "prod",
56
+ * // Required for encrypting/decrypting secrets
57
+ * password: process.env.SECRET_PASSPHRASE
58
+ * });
59
+ */
32
60
  (...parameters: Parameters<typeof scope>): ReturnType<typeof scope>;
61
+ /**
62
+ * Template literal tag that supports file interpolation for documentation.
63
+ * Automatically formats the content and appends file contents as code blocks.
64
+ *
65
+ * @example
66
+ * // Generate documentation using file contents
67
+ * await Document("api-docs", {
68
+ * prompt: await alchemy`
69
+ * Generate docs using the contents of:
70
+ * ${alchemy.file("README.md")}
71
+ * ${alchemy.file("./.cursorrules")}
72
+ *
73
+ * And here are the source files:
74
+ * ${alchemy.files(files)}
75
+ * `
76
+ * });
77
+ */
78
+ (template: TemplateStringsArray, ...values: any[]): Promise<string>;
79
+ }
80
+
81
+ /**
82
+ * Implementation of the alchemy function that handles both application scoping
83
+ * and template string interpolation.
84
+ */
85
+ function _alchemy(
86
+ ...args:
87
+ | [template: TemplateStringsArray, ...values: any[]]
88
+ | [appName: string, options?: Omit<AlchemyOptions, "appName">]
89
+ ): any {
90
+ if (typeof args[0] === "string") {
91
+ const [appName, options] = args;
92
+ return scope(undefined, {
93
+ ...options,
94
+ appName,
95
+ stage: options?.stage,
96
+ });
97
+ } else {
98
+ const [template, ...values] = args;
99
+ const [, secondLine] = template[0].split("\n");
100
+ const leadingSpaces = secondLine
101
+ ? secondLine.match(/^(\s*)/)?.[1]?.length || 0
102
+ : 0;
103
+ const indent = " ".repeat(leadingSpaces);
104
+
105
+ return (async () => {
106
+ const { isFileCollection, isFileRef } = await import("./fs");
107
+
108
+ const appendices: Record<string, string> = {};
109
+
110
+ const stringValues = await Promise.all(
111
+ values.map(async function resolve(value): Promise<string> {
112
+ if (typeof value === "string") {
113
+ return indent + value;
114
+ } else if (value instanceof Promise) {
115
+ return resolve(await value);
116
+ } else if (isFileRef(value)) {
117
+ if (!(value.path in appendices)) {
118
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
119
+ }
120
+ return `[${path.basename(value.path)}](${value.path})`;
121
+ } else if (isFileCollection(value)) {
122
+ return Object.entries(value.files)
123
+ .map(([filePath, content]) => {
124
+ appendices[filePath] = content;
125
+ return `[${path.basename(filePath)}](${filePath})`;
126
+ })
127
+ .join("\n\n");
128
+ } else if (Array.isArray(value)) {
129
+ return (
130
+ await Promise.all(
131
+ value.map(async (value, i) => `${i}. ${await resolve(value)}`),
132
+ )
133
+ ).join("\n");
134
+ } else {
135
+ // TODO: support other types
136
+ throw new Error(`Unsupported value type: ${JSON.stringify(value)}`);
137
+ }
138
+ }),
139
+ );
140
+
141
+ // Construct the string template by joining template parts with interpolated values
142
+ const lines = template
143
+ .map((part) =>
144
+ part
145
+ .split("\n")
146
+ .map((line) =>
147
+ line.startsWith(indent) ? line.slice(indent.length) : line,
148
+ )
149
+ .join("\n"),
150
+ )
151
+ .flatMap((part, i) =>
152
+ i < stringValues.length ? [part, stringValues[i] ?? ""] : [part],
153
+ )
154
+ .join("")
155
+ .split("\n");
156
+
157
+ // Collect and sort appendices by file path
158
+ return [
159
+ // format the user prompt and trim the first line if it's empty
160
+ lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
161
+ ? lines.slice(1).join("\n")
162
+ : lines.join("\n"),
163
+
164
+ // sort appendices by path and include at the end of the prompt
165
+ Object.entries(appendices)
166
+ .sort(([a], [b]) => a.localeCompare(b))
167
+ .map(([filePath, content]) => {
168
+ const extension = path.extname(filePath).slice(1);
169
+ const codeTag = extension ? extension : "";
170
+ return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
171
+ })
172
+ .join("\n\n"),
173
+ ].join("\n");
174
+ })();
175
+ }
33
176
  }
177
+ _alchemy.destroy = destroy;
178
+ _alchemy.run = run;
179
+ _alchemy.scope = scope;
180
+ _alchemy.secret = secret;
34
181
 
35
182
  export interface AlchemyOptions {
36
183
  /**
@@ -72,14 +219,24 @@ export interface AlchemyOptions {
72
219
 
73
220
  /**
74
221
  * A passphrase to use to encrypt/decrypt secrets.
222
+ * Required if using alchemy.secret() in this scope.
75
223
  */
76
224
  password?: string;
77
225
  }
78
226
 
79
227
  /**
80
228
  * Enter a new scope synchronously.
81
- * @param options
82
- * @returns
229
+ *
230
+ * @example
231
+ * // Create a scope with a password for secret handling
232
+ * await using scope = alchemy.scope("my-scope", {
233
+ * password: process.env.SECRET_PASSPHRASE
234
+ * });
235
+ *
236
+ * // Use secrets within the scope
237
+ * const resource = await Resource("my-resource", {
238
+ * apiKey: alchemy.secret(process.env.API_KEY)
239
+ * });
83
240
  */
84
241
  function scope(
85
242
  id: string | undefined,
@@ -98,6 +255,21 @@ function scope(
98
255
  return scope;
99
256
  }
100
257
 
258
+ /**
259
+ * Run a function in a new scope asynchronously.
260
+ * Useful for isolating secret handling with a specific password.
261
+ *
262
+ * @example
263
+ * // Run operations in a scope with its own password
264
+ * await alchemy.run("secure-scope", {
265
+ * password: process.env.SCOPE_PASSWORD
266
+ * }, async () => {
267
+ * // Secrets in this scope will use this password
268
+ * const resource = await Resource("my-resource", {
269
+ * apiKey: alchemy.secret(process.env.API_KEY)
270
+ * });
271
+ * });
272
+ */
101
273
  async function run<T>(
102
274
  ...args:
103
275
  | [id: string, fn: (this: Scope, scope: Scope) => Promise<T>]
@@ -115,13 +287,16 @@ async function run<T>(
115
287
  AlchemyOptions | undefined,
116
288
  (this: Scope, scope: Scope) => Promise<T>,
117
289
  ]);
118
- await using scope = alchemy.scope(id, options);
290
+ const scope = alchemy.scope(id, options);
119
291
  try {
120
292
  return await fn.bind(scope)(scope);
121
293
  } catch (error) {
122
294
  if (!(error instanceof DestroyedSignal)) {
123
295
  scope.fail();
296
+ } else {
124
297
  }
125
298
  throw error;
299
+ } finally {
300
+ await scope.finalize();
126
301
  }
127
302
  }
package/src/apply.ts CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  type Resource,
8
8
  type ResourceProps,
9
9
  } from "./resource";
10
+ import { Secret } from "./secret";
10
11
  import type { State } from "./state";
11
12
  import { serialize } from "./util/serde";
12
13
 
@@ -17,12 +18,12 @@ export interface ApplyOptions {
17
18
 
18
19
  export async function apply<Out extends Resource>(
19
20
  resource: PendingResource<Out>,
20
- props: ResourceProps,
21
+ props: ResourceProps | undefined,
21
22
  options?: ApplyOptions,
22
23
  ): Promise<Awaited<Out>> {
23
24
  const scope = resource.Scope;
24
25
  try {
25
- const quiet = props.quiet ?? scope.quiet;
26
+ const quiet = props?.quiet ?? scope.quiet;
26
27
  await scope.init();
27
28
  let state: State | undefined = (await scope.state.get(resource.ID))!;
28
29
  const provider: Provider = PROVIDERS.get(resource.Kind);
@@ -55,17 +56,27 @@ export async function apply<Out extends Resource>(
55
56
 
56
57
  // Skip update if inputs haven't changed and resource is in a stable state
57
58
  if (state.status === "created" || state.status === "updated") {
58
- if (
59
- JSON.stringify(state.props) ===
60
- JSON.stringify(await serialize(scope, props)) &&
61
- alwaysUpdate !== true
62
- ) {
59
+ const oldProps = JSON.stringify(
60
+ state.props,
61
+ (_, value) =>
62
+ value instanceof Secret
63
+ ? {
64
+ "@secret": value.unencrypted,
65
+ }
66
+ : value,
67
+ 2,
68
+ );
69
+ const newProps = JSON.stringify(
70
+ await serialize(scope, props, {
71
+ encrypt: false,
72
+ }),
73
+ null,
74
+ 2,
75
+ );
76
+ if (oldProps === newProps && alwaysUpdate !== true) {
63
77
  if (!quiet) {
64
78
  console.log(`Skip: "${resource.FQN}" (no changes)`);
65
79
  }
66
- // if (resourceState.output !== undefined) {
67
- // resource[Provide](resourceState.output);
68
- // }
69
80
  return state.output as Awaited<Out>;
70
81
  }
71
82
  }
@@ -92,6 +103,7 @@ export async function apply<Out extends Resource>(
92
103
  id: resource.ID,
93
104
  fqn: resource.FQN,
94
105
  seq: resource.Seq,
106
+ props: state.oldProps,
95
107
  state,
96
108
  replace: () => {
97
109
  if (isReplaced) {
@@ -104,9 +116,9 @@ export async function apply<Out extends Resource>(
104
116
  },
105
117
  });
106
118
 
107
- const output = await alchemy.run(resource.ID, async () => {
108
- return provider.handler.bind(ctx)(resource.ID, props);
109
- });
119
+ const output = await alchemy.run(resource.ID, async () =>
120
+ provider.handler.bind(ctx)(resource.ID, props),
121
+ );
110
122
 
111
123
  if (!quiet) {
112
124
  console.log(