alchemy 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/astro-file.js +7 -24
  3. package/lib/ai/client.d.ts +7 -0
  4. package/lib/ai/client.js +45 -0
  5. package/lib/ai/css-file.js +7 -24
  6. package/lib/ai/data.d.ts +26 -21
  7. package/lib/ai/data.js +33 -22
  8. package/lib/ai/document.d.ts +76 -5
  9. package/lib/ai/document.js +112 -56
  10. package/lib/ai/html-file.js +7 -24
  11. package/lib/ai/json-file.js +11 -32
  12. package/lib/ai/typescript-file.d.ts +1 -2
  13. package/lib/ai/typescript-file.js +8 -39
  14. package/lib/ai/vue-file.js +7 -24
  15. package/lib/ai/yaml-file.js +12 -48
  16. package/lib/alchemy.d.ts +5 -0
  17. package/lib/alchemy.js +61 -3
  18. package/lib/apply.js +18 -11
  19. package/lib/aws/account-id.d.ts +4 -1
  20. package/lib/aws/account-id.js +1 -1
  21. package/lib/aws/credentials.d.ts +5 -0
  22. package/lib/aws/credentials.js +0 -0
  23. package/lib/aws/oidc/oidc-provider.js +2 -2
  24. package/lib/aws/role.js +35 -23
  25. package/lib/cloudflare/account-api-token.d.ts +168 -0
  26. package/lib/cloudflare/account-api-token.js +179 -0
  27. package/lib/cloudflare/account-id.d.ts +1 -0
  28. package/lib/cloudflare/account-id.js +0 -0
  29. package/lib/cloudflare/api.d.ts +38 -1
  30. package/lib/cloudflare/api.js +55 -4
  31. package/lib/cloudflare/bucket.d.ts +118 -3
  32. package/lib/cloudflare/bucket.js +269 -89
  33. package/lib/cloudflare/custom-domain.d.ts +75 -0
  34. package/lib/cloudflare/custom-domain.js +154 -0
  35. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  36. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  37. package/lib/cloudflare/index.d.ts +5 -1
  38. package/lib/cloudflare/index.js +5 -1
  39. package/lib/cloudflare/permission-groups.d.ts +78 -0
  40. package/lib/cloudflare/permission-groups.js +48 -0
  41. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  42. package/lib/cloudflare/r2-rest-state-store.js +3 -2
  43. package/lib/cloudflare/static-site-router.js +16 -7
  44. package/lib/cloudflare/static-site.d.ts +17 -18
  45. package/lib/cloudflare/static-site.js +19 -7
  46. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  47. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  48. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  49. package/lib/fs/copy-file.d.ts +54 -0
  50. package/lib/fs/copy-file.js +63 -0
  51. package/lib/fs/file-system-state-store.js +1 -1
  52. package/lib/fs/file.d.ts +31 -0
  53. package/lib/fs/file.js +30 -5
  54. package/lib/fs/index.d.ts +6 -0
  55. package/lib/fs/index.js +6 -0
  56. package/lib/fs/static-astro-file.d.ts +34 -0
  57. package/lib/fs/static-astro-file.js +39 -0
  58. package/lib/fs/static-css-file.d.ts +24 -0
  59. package/lib/fs/static-css-file.js +29 -0
  60. package/lib/fs/static-html-file.d.ts +31 -0
  61. package/lib/fs/static-html-file.js +36 -0
  62. package/lib/fs/static-json-file.d.ts +2 -2
  63. package/lib/fs/static-json-file.js +3 -2
  64. package/lib/fs/static-text-file.d.ts +1 -1
  65. package/lib/fs/static-text-file.js +3 -2
  66. package/lib/fs/static-typescript-file.d.ts +2 -2
  67. package/lib/fs/static-typescript-file.js +3 -2
  68. package/lib/fs/static-vue-file.d.ts +28 -0
  69. package/lib/fs/static-vue-file.js +33 -0
  70. package/lib/fs/static-yaml-file.d.ts +11 -8
  71. package/lib/fs/static-yaml-file.js +3 -2
  72. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +14 -4
  73. package/lib/internal/docs/providers.js +196 -0
  74. package/lib/secret.d.ts +3 -0
  75. package/lib/secret.js +13 -0
  76. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  77. package/lib/{util/serde.js → serde.js} +12 -4
  78. package/lib/test/bun.js +3 -2
  79. package/lib/util/sha256.d.ts +1 -0
  80. package/lib/util/sha256.js +4 -0
  81. package/lib/web/vitepress/config.d.ts +11 -0
  82. package/lib/web/vitepress/config.js +32 -0
  83. package/lib/web/vitepress/home-page.d.ts +5 -3
  84. package/lib/web/vitepress/home-page.js +48 -35
  85. package/lib/web/vitepress/index.d.ts +2 -0
  86. package/lib/web/vitepress/index.js +2 -0
  87. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  88. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  89. package/lib/web/vitepress/vitepress.d.ts +5 -4
  90. package/lib/web/vitepress/vitepress.js +20 -36
  91. package/package.json +8 -4
  92. package/src/ai/astro-file.ts +11 -30
  93. package/src/ai/client.ts +59 -0
  94. package/src/ai/css-file.ts +11 -30
  95. package/src/ai/data.ts +50 -25
  96. package/src/ai/document.ts +171 -66
  97. package/src/ai/html-file.ts +11 -30
  98. package/src/ai/json-file.ts +13 -37
  99. package/src/ai/typescript-file.ts +14 -48
  100. package/src/ai/vue-file.ts +11 -30
  101. package/src/ai/yaml-file.ts +13 -53
  102. package/src/alchemy.ts +74 -9
  103. package/src/apply.ts +27 -26
  104. package/src/aws/account-id.ts +6 -2
  105. package/src/aws/credentials.ts +6 -0
  106. package/src/aws/oidc/oidc-provider.ts +17 -17
  107. package/src/aws/role.ts +68 -54
  108. package/src/cloudflare/account-api-token.ts +365 -0
  109. package/src/cloudflare/account-id.ts +0 -0
  110. package/src/cloudflare/api.ts +88 -17
  111. package/src/cloudflare/bucket.ts +493 -133
  112. package/src/cloudflare/custom-domain.ts +318 -0
  113. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  114. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  115. package/src/cloudflare/index.ts +5 -1
  116. package/src/cloudflare/permission-groups.ts +137 -0
  117. package/src/cloudflare/r2-rest-state-store.ts +14 -13
  118. package/src/cloudflare/static-site-router.ts +18 -8
  119. package/src/cloudflare/static-site.ts +27 -35
  120. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  121. package/src/cloudflare/worker.ts +44 -43
  122. package/src/dns/import-dns.ts +4 -4
  123. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  124. package/src/fs/copy-file.ts +115 -0
  125. package/src/fs/file-system-state-store.ts +5 -5
  126. package/src/fs/file.ts +64 -9
  127. package/src/fs/index.ts +6 -0
  128. package/src/fs/static-astro-file.ts +45 -0
  129. package/src/fs/static-css-file.ts +35 -0
  130. package/src/fs/static-html-file.ts +42 -0
  131. package/src/fs/static-json-file.ts +4 -3
  132. package/src/fs/static-text-file.ts +5 -2
  133. package/src/fs/static-typescript-file.ts +4 -3
  134. package/src/fs/static-vue-file.ts +39 -0
  135. package/src/fs/static-yaml-file.ts +13 -9
  136. package/src/internal/docs/providers.ts +281 -0
  137. package/src/secret.ts +16 -0
  138. package/src/{util/serde.ts → serde.ts} +16 -10
  139. package/src/test/bun.ts +8 -7
  140. package/src/util/sha256.ts +5 -0
  141. package/src/web/vitepress/config.ts +50 -0
  142. package/src/web/vitepress/home-page.ts +54 -42
  143. package/src/web/vitepress/index.ts +2 -0
  144. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  145. package/src/web/vitepress/vitepress.ts +34 -50
  146. package/lib/internal/getting-started.d.ts +0 -10
  147. package/lib/internal/getting-started.js +0 -94
  148. package/lib/internal/project.d.ts +0 -13
  149. package/lib/internal/project.js +0 -101
  150. package/lib/internal/providers.js +0 -148
  151. package/src/internal/getting-started.ts +0 -107
  152. package/src/internal/project.ts +0 -119
  153. package/src/internal/providers.ts +0 -209
  154. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
@@ -1,8 +1,12 @@
1
+ export * from "./account-api-token";
2
+ export * from "./api";
1
3
  export * from "./bindings";
2
4
  export * from "./bucket";
3
- export * from "./dns";
5
+ export * from "./custom-domain";
6
+ export * from "./dns-records";
4
7
  export * from "./durable-object-namespace";
5
8
  export * from "./kv-namespace";
9
+ export * from "./permission-groups";
6
10
  export * from "./r2-rest-state-store";
7
11
  export * from "./static-site";
8
12
  export * from "./worker";
@@ -1,8 +1,12 @@
1
+ export * from "./account-api-token";
2
+ export * from "./api";
1
3
  export * from "./bindings";
2
4
  export * from "./bucket";
3
- export * from "./dns";
5
+ export * from "./custom-domain";
6
+ export * from "./dns-records";
4
7
  export * from "./durable-object-namespace";
5
8
  export * from "./kv-namespace";
9
+ export * from "./permission-groups";
6
10
  export * from "./r2-rest-state-store";
7
11
  export * from "./static-site";
8
12
  export * from "./worker";
@@ -0,0 +1,78 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import { type CloudflareApiOptions } from "./api";
4
+ /**
5
+ * Cloudflare permission group as returned by the API
6
+ */
7
+ export interface PermissionGroup {
8
+ /**
9
+ * Unique identifier for the permission group
10
+ */
11
+ id: string;
12
+ /**
13
+ * Human-readable name of the permission group
14
+ */
15
+ name: string;
16
+ /**
17
+ * Scopes included in this permission group
18
+ */
19
+ scopes: string[];
20
+ }
21
+ /**
22
+ * All Cloudflare permission groups mapped by name to ID
23
+ *
24
+ * @see https://developers.cloudflare.com/r2/api/tokens/#permissions
25
+ */
26
+ export type PermissionGroups = Resource<"cloudflare::PermissionGroups"> & {
27
+ /**
28
+ * Admin Read & Write - Allows create, list, delete buckets and edit bucket configurations
29
+ * plus list, write, and read object access
30
+ */
31
+ "Workers R2 Storage Write": PermissionGroup;
32
+ /**
33
+ * Admin Read only - Allows list buckets and view bucket configuration
34
+ * plus list and read object access
35
+ */
36
+ "Workers R2 Storage Read": PermissionGroup;
37
+ /**
38
+ * Object Read & Write - Allows read, write, and list objects in specific buckets
39
+ */
40
+ "Workers R2 Storage Bucket Item Write": PermissionGroup;
41
+ /**
42
+ * Object Read only - Allows read and list objects in specific buckets
43
+ */
44
+ "Workers R2 Storage Bucket Item Read": PermissionGroup;
45
+ /**
46
+ * Dynamically discovered permission groups
47
+ */
48
+ [name: string]: PermissionGroup;
49
+ };
50
+ /**
51
+ * Lists all permission groups available for the Cloudflare account
52
+ * and returns a typed map of permission names to their IDs.
53
+ *
54
+ * This is primarily used when creating API tokens for Cloudflare services like R2.
55
+ *
56
+ * @example
57
+ * // Get all permission groups including those for R2
58
+ * const permissions = await PermissionGroups("cloudflare-permissions");
59
+ *
60
+ * // Use with AccountApiToken to create a token with proper permissions
61
+ * const token = await AccountApiToken("r2-token", {
62
+ * name: "R2 Read-Only Token",
63
+ * policies: [
64
+ * {
65
+ * effect: "allow",
66
+ * resources: {
67
+ * "com.cloudflare.edge.r2.bucket.abc123_default_my-bucket": "*"
68
+ * },
69
+ * permissionGroups: [
70
+ * {
71
+ * id: permissions["Workers R2 Storage Bucket Item Read"]
72
+ * }
73
+ * ]
74
+ * }
75
+ * ]
76
+ * });
77
+ */
78
+ export declare const PermissionGroups: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<PermissionGroups>, id: string, options?: CloudflareApiOptions) => Promise<PermissionGroups>);
@@ -0,0 +1,48 @@
1
+ import { Resource } from "../resource";
2
+ import { createCloudflareApi } from "./api";
3
+ /**
4
+ * Lists all permission groups available for the Cloudflare account
5
+ * and returns a typed map of permission names to their IDs.
6
+ *
7
+ * This is primarily used when creating API tokens for Cloudflare services like R2.
8
+ *
9
+ * @example
10
+ * // Get all permission groups including those for R2
11
+ * const permissions = await PermissionGroups("cloudflare-permissions");
12
+ *
13
+ * // Use with AccountApiToken to create a token with proper permissions
14
+ * const token = await AccountApiToken("r2-token", {
15
+ * name: "R2 Read-Only Token",
16
+ * policies: [
17
+ * {
18
+ * effect: "allow",
19
+ * resources: {
20
+ * "com.cloudflare.edge.r2.bucket.abc123_default_my-bucket": "*"
21
+ * },
22
+ * permissionGroups: [
23
+ * {
24
+ * id: permissions["Workers R2 Storage Bucket Item Read"]
25
+ * }
26
+ * ]
27
+ * }
28
+ * ]
29
+ * });
30
+ */
31
+ export const PermissionGroups = Resource("cloudflare::PermissionGroups", async function (id, options) {
32
+ // Only create and update phases are supported
33
+ if (this.phase === "delete") {
34
+ return this.destroy();
35
+ }
36
+ // Initialize API client
37
+ const api = await createCloudflareApi(options);
38
+ // Fetch permission groups from Cloudflare API
39
+ const response = await api.get(`/accounts/${api.accountId}/tokens/permission_groups`);
40
+ if (!response.ok) {
41
+ throw new Error(`Failed to fetch permission groups: ${response.statusText}`);
42
+ }
43
+ const data = (await response.json());
44
+ if (!data.success || !data.result) {
45
+ throw new Error(`API returned error: ${data.errors?.[0]?.message || "Unknown error"}`);
46
+ }
47
+ return this(Object.fromEntries(data.result.map((group) => [group.name, group])));
48
+ });
@@ -1,4 +1,5 @@
1
1
  import type { Scope } from "../scope";
2
+ import type { Secret } from "../secret";
2
3
  import type { State, StateStore } from "../state";
3
4
  /**
4
5
  * Options for CloudflareR2StateStore
@@ -17,7 +18,7 @@ export interface CloudflareR2StateStoreOptions {
17
18
  /**
18
19
  * API key to use (overrides CLOUDFLARE_API_KEY env var)
19
20
  */
20
- apiKey?: string;
21
+ apiKey?: Secret;
21
22
  /**
22
23
  * Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
23
24
  */
@@ -1,5 +1,5 @@
1
+ import { deserialize, serialize } from "../serde";
1
2
  import { withExponentialBackoff } from "../util/retry";
2
- import { deserialize, serialize } from "../util/serde";
3
3
  import { createCloudflareApi } from "./api";
4
4
  /**
5
5
  * State store implementation using Cloudflare R2 API
@@ -198,7 +198,8 @@ export class R2RestStateStore {
198
198
  },
199
199
  // Retry on transient errors
200
200
  (error) => error.message?.includes("503") || error.message?.includes("timeout"), 5, // 5 retry attempts
201
- 1000);
201
+ 1000 // Start with 1 second delay
202
+ );
202
203
  }
203
204
  /**
204
205
  * Delete a state by key
@@ -35,34 +35,44 @@ app.notFound(async (ctx) => {
35
35
  }
36
36
  // Fetch from KV
37
37
  {
38
- const object = await env.ASSETS.getWithMetadata(filePath);
38
+ const object = await env.ASSETS.getWithMetadata(filePath, {
39
+ type: "arrayBuffer",
40
+ });
39
41
  if (object.value) {
40
42
  return await respond(200, filePath, object);
41
43
  }
42
44
  }
43
45
  {
44
46
  const guess = filePath + (filePath.endsWith("/") ? "" : "/") + "index.html";
45
- const object = await env.ASSETS.getWithMetadata(guess);
47
+ const object = await env.ASSETS.getWithMetadata(guess, {
48
+ type: "arrayBuffer",
49
+ });
46
50
  if (object.value) {
47
51
  return await respond(200, guess, object);
48
52
  }
49
53
  }
50
54
  {
51
55
  const guess = filePath + ".html";
52
- const object = await env.ASSETS.getWithMetadata(guess);
56
+ const object = await env.ASSETS.getWithMetadata(guess, {
57
+ type: "arrayBuffer",
58
+ });
53
59
  if (object.value) {
54
60
  return await respond(200, guess, object);
55
61
  }
56
62
  }
57
63
  // Handle error page
58
64
  if (env.ERROR_PAGE) {
59
- const object = await env.ASSETS.getWithMetadata(env.ERROR_PAGE);
65
+ const object = await env.ASSETS.getWithMetadata(env.ERROR_PAGE, {
66
+ type: "arrayBuffer",
67
+ });
60
68
  if (object.value) {
61
69
  return await respond(404, env.ERROR_PAGE, object);
62
70
  }
63
71
  }
64
72
  else {
65
- const object = await env.ASSETS.getWithMetadata(env.INDEX_PAGE);
73
+ const object = await env.ASSETS.getWithMetadata(env.INDEX_PAGE, {
74
+ type: "arrayBuffer",
75
+ });
66
76
  if (object.value) {
67
77
  return await respond(200, env.INDEX_PAGE, object);
68
78
  }
@@ -92,8 +102,7 @@ app.notFound(async (ctx) => {
92
102
  headers.set("content-type", object.metadata.contentType);
93
103
  headers.set("cache-control", object.metadata.cacheControl);
94
104
  }
95
- // TODO: do we need base64 encoded here?
96
- // const response = new Response(base64ToArrayBuffer(object.value), {
105
+ // Create response with the raw data directly
97
106
  const response = new Response(object.value, {
98
107
  status,
99
108
  headers,
@@ -72,13 +72,6 @@ export interface StaticSiteProps {
72
72
  * @default true
73
73
  */
74
74
  production?: boolean;
75
- /**
76
- * Custom domain for the site
77
- */
78
- domain?: string | {
79
- name: string;
80
- redirects?: string[];
81
- };
82
75
  build?: {
83
76
  /**
84
77
  * Command to run before deploying the site
@@ -137,13 +130,6 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
137
130
  * List of uploaded asset files
138
131
  */
139
132
  assets: string[];
140
- /**
141
- * Custom domain for the site
142
- */
143
- domain?: string | {
144
- name: string;
145
- redirects?: string[];
146
- };
147
133
  /**
148
134
  * Whether the site is deployed to production
149
135
  */
@@ -198,13 +184,26 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
198
184
  * });
199
185
  *
200
186
  * @example
201
- * // Create a static site with custom error page and index
202
- * const customSite = await StaticSite("custom-site", {
187
+ * // Create a static site with a custom domain using the CustomDomain resource
188
+ * const site = await StaticSite("custom-site", {
203
189
  * name: "custom-site",
204
190
  * dir: "./www",
205
191
  * errorPage: "404.html",
206
- * indexPage: "home.html",
207
- * domain: "www.example.com"
192
+ * indexPage: "home.html"
193
+ * });
194
+ *
195
+ * // Then configure the custom domain separately
196
+ * const domain = await CustomDomain("custom-domain", {
197
+ * name: "www.example.com",
198
+ * zoneId: "abcdef123456789",
199
+ * workerName: site.name
200
+ * });
201
+ *
202
+ * @example
203
+ * // Create a static site with multiple domains (primary + redirects)
204
+ * const site = await StaticSite("multi-domain-site", {
205
+ * name: "multi-domain-site",
206
+ * dir: "./public"
208
207
  * });
209
208
  *
210
209
  * @see https://developers.cloudflare.com/workers/platform/sites
@@ -50,13 +50,26 @@ const __dirname = import.meta.dirname;
50
50
  * });
51
51
  *
52
52
  * @example
53
- * // Create a static site with custom error page and index
54
- * const customSite = await StaticSite("custom-site", {
53
+ * // Create a static site with a custom domain using the CustomDomain resource
54
+ * const site = await StaticSite("custom-site", {
55
55
  * name: "custom-site",
56
56
  * dir: "./www",
57
57
  * errorPage: "404.html",
58
- * indexPage: "home.html",
59
- * domain: "www.example.com"
58
+ * indexPage: "home.html"
59
+ * });
60
+ *
61
+ * // Then configure the custom domain separately
62
+ * const domain = await CustomDomain("custom-domain", {
63
+ * name: "www.example.com",
64
+ * zoneId: "abcdef123456789",
65
+ * workerName: site.name
66
+ * });
67
+ *
68
+ * @example
69
+ * // Create a static site with multiple domains (primary + redirects)
70
+ * const site = await StaticSite("multi-domain-site", {
71
+ * name: "multi-domain-site",
72
+ * dir: "./public"
60
73
  * });
61
74
  *
62
75
  * @see https://developers.cloudflare.com/workers/platform/sites
@@ -64,6 +77,8 @@ const __dirname = import.meta.dirname;
64
77
  export const StaticSite = Resource("cloudflare::StaticSite", {
65
78
  alwaysUpdate: true,
66
79
  }, async function (id, props) {
80
+ // Create Cloudflare API client with automatic account discovery
81
+ const api = await createCloudflareApi();
67
82
  if (this.phase === "delete") {
68
83
  // For delete operations, we'll rely on the Worker delete to clean up
69
84
  // Return empty output for deleted state
@@ -113,8 +128,6 @@ export const StaticSite = Resource("cloudflare::StaticSite", {
113
128
  // Use the provided name
114
129
  const siteName = props.name;
115
130
  const indexPage = props.indexPage || "index.html";
116
- // Create Cloudflare API client with automatic account discovery
117
- const api = await createCloudflareApi();
118
131
  // Step 1: Create or get the KV namespace for assets
119
132
  const [kv, assetManifest] = await Promise.all([
120
133
  KVNamespace("assets", {
@@ -193,7 +206,6 @@ export const StaticSite = Resource("cloudflare::StaticSite", {
193
206
  assets: assetManifest.map((item) => item.key),
194
207
  createdAt: this.output?.createdAt || now,
195
208
  updatedAt: now,
196
- domain: props.domain,
197
209
  production: props.production !== false,
198
210
  url: worker.url,
199
211
  routes,
@@ -34,8 +34,10 @@ export async function uploadAssetManifest(api, namespaceId, manifest) {
34
34
  // Create FormData for multipart upload
35
35
  const formData = new FormData();
36
36
  formData.append("metadata", JSON.stringify(metadata));
37
- // Add file content as 'value'
38
- const blob = new Blob([content]);
37
+ // Add file content as 'value' - Fix for binary file corruption
38
+ const blob = new Blob([new Uint8Array(content)], {
39
+ type: item.contentType,
40
+ });
39
41
  formData.append("value", blob);
40
42
  try {
41
43
  const response = await api.put(`/accounts/${api.accountId}/storage/kv/namespaces/${namespaceId}/values/${item.key}`, formData);
@@ -5,7 +5,7 @@
5
5
  * @param key - The encryption key
6
6
  * @returns The base64-encoded encrypted value with nonce
7
7
  */
8
- export declare function encryptWithKey(value: string, key: string): Promise<string>;
8
+ export declare function encrypt(value: string, key: string): Promise<string>;
9
9
  /**
10
10
  * Decrypt a value encrypted with a symmetric key
11
11
  *
@@ -6,7 +6,7 @@ import sodium from "libsodium-wrappers";
6
6
  * @param key - The encryption key
7
7
  * @returns The base64-encoded encrypted value with nonce
8
8
  */
9
- export async function encryptWithKey(value, key) {
9
+ export async function encrypt(value, key) {
10
10
  // Initialize libsodium
11
11
  await sodium.ready;
12
12
  // Derive a key from the passphrase
@@ -0,0 +1,54 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ /**
4
+ * Properties for creating a CopyFile resource
5
+ */
6
+ export interface CopyFileProps {
7
+ /**
8
+ * Source path of the file to copy
9
+ */
10
+ src: string;
11
+ /**
12
+ * Destination path where the file should be copied to
13
+ */
14
+ dest: string;
15
+ /**
16
+ * Whether to overwrite the destination file if it already exists
17
+ * @default true
18
+ */
19
+ overwrite?: boolean;
20
+ }
21
+ /**
22
+ * Output returned after CopyFile creation/update
23
+ */
24
+ export interface CopyFile extends Resource<"fs::CopyFile">, CopyFileProps {
25
+ /**
26
+ * Time at which the object was created
27
+ */
28
+ createdAt: number;
29
+ /**
30
+ * Whether the file was successfully copied
31
+ */
32
+ copied: boolean;
33
+ }
34
+ /**
35
+ * CopyFile Resource
36
+ *
37
+ * Copies a file from a source location to a destination location.
38
+ *
39
+ * @example
40
+ * // Copy a file to a new location
41
+ * const copiedFile = await CopyFile("config-copy", {
42
+ * src: "config.json",
43
+ * dest: "backup/config.json"
44
+ * });
45
+ *
46
+ * @example
47
+ * // Copy a file without overwriting if destination exists
48
+ * const safeCopy = await CopyFile("safe-copy", {
49
+ * src: "data.json",
50
+ * dest: "backup/data.json",
51
+ * overwrite: false
52
+ * });
53
+ */
54
+ export declare const CopyFile: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<CopyFile>, id: string, props: CopyFileProps) => Promise<CopyFile>);
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import { Resource } from "../resource";
3
+ import { ignore } from "../util/ignore";
4
+ /**
5
+ * CopyFile Resource
6
+ *
7
+ * Copies a file from a source location to a destination location.
8
+ *
9
+ * @example
10
+ * // Copy a file to a new location
11
+ * const copiedFile = await CopyFile("config-copy", {
12
+ * src: "config.json",
13
+ * dest: "backup/config.json"
14
+ * });
15
+ *
16
+ * @example
17
+ * // Copy a file without overwriting if destination exists
18
+ * const safeCopy = await CopyFile("safe-copy", {
19
+ * src: "data.json",
20
+ * dest: "backup/data.json",
21
+ * overwrite: false
22
+ * });
23
+ */
24
+ export const CopyFile = Resource("fs::CopyFile", async function (id, props) {
25
+ const { src, dest, overwrite = true } = props;
26
+ if (this.phase === "delete") {
27
+ // When deleting, remove the destination file
28
+ await ignore("ENOENT", async () => fs.promises.unlink(dest));
29
+ return this.destroy();
30
+ }
31
+ else {
32
+ try {
33
+ // Check if source file exists
34
+ await fs.promises.access(src, fs.constants.F_OK);
35
+ // If this is an update and the destination has changed, delete the old file
36
+ if (this.phase === "update" &&
37
+ this.output?.dest &&
38
+ this.output.dest !== dest) {
39
+ await ignore("ENOENT", async () => fs.promises.unlink(this.output.dest));
40
+ }
41
+ // Check if destination file exists
42
+ const destinationExists = await fs.promises
43
+ .access(dest, fs.constants.F_OK)
44
+ .then(() => true)
45
+ .catch(() => false);
46
+ // Copy file if destination doesn't exist or overwrite is true
47
+ if (!destinationExists || overwrite) {
48
+ await fs.promises.copyFile(src, dest);
49
+ }
50
+ return this({
51
+ src,
52
+ dest,
53
+ overwrite,
54
+ copied: true,
55
+ createdAt: Date.now(),
56
+ });
57
+ }
58
+ catch (error) {
59
+ console.error(`Error copying file from ${src} to ${dest}:`, error);
60
+ throw error;
61
+ }
62
+ }
63
+ });
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { deserialize, serialize } from "../serde";
3
4
  import { ignore } from "../util/ignore";
4
- import { deserialize, serialize } from "../util/serde";
5
5
  const stateRootDir = path.join(process.cwd(), ".alchemy");
6
6
  export class FileSystemStateStore {
7
7
  scope;
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
  /**
@@ -79,6 +97,19 @@ export interface File extends Resource<"fs::File"> {
79
97
  * path: "logs/app.log",
80
98
  * content: "application log entry"
81
99
  * });
100
+ *
101
+ * @example
102
+ * // Update file content and path
103
+ * let file = await File("config.json", {
104
+ * path: "config.json",
105
+ * content: '{ "version": "1.0.0" }'
106
+ * });
107
+ *
108
+ * // Later, update the path and content (old file will be removed)
109
+ * file = await File("config.json", {
110
+ * path: "config/config.json",
111
+ * content: '{ "version": "1.0.1" }'
112
+ * });
82
113
  */
83
114
  export declare const File: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<File>, id: string, props: {
84
115
  path: string;
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
  *
@@ -36,6 +42,19 @@ alchemy.files = async (...args) => {
36
42
  * path: "logs/app.log",
37
43
  * content: "application log entry"
38
44
  * });
45
+ *
46
+ * @example
47
+ * // Update file content and path
48
+ * let file = await File("config.json", {
49
+ * path: "config.json",
50
+ * content: '{ "version": "1.0.0" }'
51
+ * });
52
+ *
53
+ * // Later, update the path and content (old file will be removed)
54
+ * file = await File("config.json", {
55
+ * path: "config/config.json",
56
+ * content: '{ "version": "1.0.1" }'
57
+ * });
39
58
  */
40
59
  export const File = Resource("fs::File", async function (id, props) {
41
60
  const filePath = props?.path ?? id;
@@ -43,12 +62,18 @@ export const File = Resource("fs::File", async function (id, props) {
43
62
  await ignore("ENOENT", async () => fs.promises.unlink(filePath));
44
63
  return this.destroy();
45
64
  }
46
- else {
47
- await fs.promises.mkdir(path.dirname(filePath), {
48
- recursive: true,
49
- });
50
- await fs.promises.writeFile(filePath, props.content);
65
+ else if (this.phase === "update" &&
66
+ this.output &&
67
+ this.output.path !== filePath) {
68
+ // If path has changed, delete the old file
69
+ console.log(`File: Path changed from ${this.output.path} to ${filePath}, removing old file`);
70
+ await ignore("ENOENT", async () => fs.promises.unlink(this.output.path));
51
71
  }
72
+ // Create directory and write file
73
+ await fs.promises.mkdir(path.dirname(filePath), {
74
+ recursive: true,
75
+ });
76
+ await fs.promises.writeFile(filePath, props.content);
52
77
  return this({
53
78
  path: filePath,
54
79
  content: props.content,
package/lib/fs/index.d.ts CHANGED
@@ -1,8 +1,14 @@
1
+ export * from "./copy-file";
1
2
  export * from "./file";
2
3
  export * from "./file-collection";
3
4
  export * from "./file-ref";
5
+ export * from "./file-system-state-store";
4
6
  export * from "./folder";
7
+ export * from "./static-astro-file";
8
+ export * from "./static-css-file";
9
+ export * from "./static-html-file";
5
10
  export * from "./static-json-file";
6
11
  export * from "./static-text-file";
7
12
  export * from "./static-typescript-file";
13
+ export * from "./static-vue-file";
8
14
  export * from "./static-yaml-file";
package/lib/fs/index.js CHANGED
@@ -1,8 +1,14 @@
1
+ export * from "./copy-file";
1
2
  export * from "./file";
2
3
  export * from "./file-collection";
3
4
  export * from "./file-ref";
5
+ export * from "./file-system-state-store";
4
6
  export * from "./folder";
7
+ export * from "./static-astro-file";
8
+ export * from "./static-css-file";
9
+ export * from "./static-html-file";
5
10
  export * from "./static-json-file";
6
11
  export * from "./static-text-file";
7
12
  export * from "./static-typescript-file";
13
+ export * from "./static-vue-file";
8
14
  export * from "./static-yaml-file";