alchemy 0.4.1 → 0.5.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 (52) hide show
  1. package/README.md +3 -463
  2. package/lib/ai/data.d.ts +5 -0
  3. package/lib/ai/data.js +3 -0
  4. package/lib/alchemy.d.ts +3 -3
  5. package/lib/alchemy.js +82 -85
  6. package/lib/apply.js +0 -8
  7. package/lib/cloudflare/assets.js +3 -1
  8. package/lib/cloudflare/bound.d.ts +2 -1
  9. package/lib/cloudflare/bucket.d.ts +16 -2
  10. package/lib/cloudflare/bucket.js +15 -6
  11. package/lib/cloudflare/custom-domain.d.ts +1 -15
  12. package/lib/cloudflare/custom-domain.js +1 -15
  13. package/lib/cloudflare/index.d.ts +0 -1
  14. package/lib/cloudflare/index.js +0 -1
  15. package/lib/cloudflare/worker.d.ts +4 -0
  16. package/lib/cloudflare/worker.js +3 -1
  17. package/lib/internal/docs/providers.js +2 -1
  18. package/lib/os/exec.d.ts +103 -0
  19. package/lib/os/exec.js +103 -0
  20. package/lib/os/index.d.ts +1 -0
  21. package/lib/os/index.js +1 -0
  22. package/lib/secret.d.ts +2 -2
  23. package/lib/secret.js +2 -2
  24. package/lib/web/vitepress/vitepress.js +1 -1
  25. package/package.json +2 -1
  26. package/src/ai/data.ts +10 -0
  27. package/src/alchemy.ts +97 -100
  28. package/src/apply.ts +0 -9
  29. package/src/cloudflare/assets.ts +3 -0
  30. package/src/cloudflare/bound.ts +4 -1
  31. package/src/cloudflare/bucket.ts +24 -9
  32. package/src/cloudflare/custom-domain.ts +1 -15
  33. package/src/cloudflare/index.ts +0 -1
  34. package/src/cloudflare/worker.ts +8 -1
  35. package/src/internal/docs/providers.ts +2 -1
  36. package/src/os/exec.ts +190 -0
  37. package/src/os/index.ts +1 -0
  38. package/src/secret.ts +2 -2
  39. package/src/web/vitepress/vitepress.ts +1 -1
  40. package/lib/cloudflare/generate-asset-manifest.d.ts +0 -2
  41. package/lib/cloudflare/generate-asset-manifest.js +0 -68
  42. package/lib/cloudflare/static-site-router.d.ts +0 -18
  43. package/lib/cloudflare/static-site-router.js +0 -115
  44. package/lib/cloudflare/static-site.d.ts +0 -211
  45. package/lib/cloudflare/static-site.js +0 -213
  46. package/lib/cloudflare/upload-asset-manifest.d.ts +0 -11
  47. package/lib/cloudflare/upload-asset-manifest.js +0 -56
  48. package/src/cloudflare/generate-asset-manifest.ts +0 -93
  49. package/src/cloudflare/static-site-router.ts +0 -157
  50. package/src/cloudflare/static-site.ts +0 -416
  51. package/src/cloudflare/upload-asset-manifest.ts +0 -85
  52. package/src/web/vitepress/index.md +0 -75
package/lib/alchemy.js CHANGED
@@ -10,7 +10,7 @@ export const alchemy = _alchemy;
10
10
  * Implementation of the alchemy function that handles both application scoping
11
11
  * and template string interpolation.
12
12
  */
13
- function _alchemy(...args) {
13
+ async function _alchemy(...args) {
14
14
  if (typeof args[0] === "string") {
15
15
  const [appName, options] = args;
16
16
  const root = scope(undefined, {
@@ -19,9 +19,8 @@ function _alchemy(...args) {
19
19
  stage: options?.stage,
20
20
  });
21
21
  if (options?.phase === "destroy") {
22
- return destroy(root).finally(() => {
23
- process.exit(0);
24
- });
22
+ await destroy(root);
23
+ return process.exit(0);
25
24
  }
26
25
  return root;
27
26
  }
@@ -32,92 +31,90 @@ function _alchemy(...args) {
32
31
  ? secondLine.match(/^(\s*)/)?.[1]?.length || 0
33
32
  : 0;
34
33
  const indent = " ".repeat(leadingSpaces);
35
- return (async () => {
36
- const { isFileCollection, isFileRef } = await import("./fs");
37
- const appendices = {};
38
- const stringValues = await Promise.all(values.map(async function resolve(value) {
39
- if (typeof value === "string") {
40
- return indent + value;
41
- }
42
- else if (value === null) {
43
- return "null";
44
- }
45
- else if (value === undefined) {
46
- return "undefined";
47
- }
48
- else if (typeof value === "number" ||
49
- typeof value === "boolean" ||
50
- typeof value === "bigint") {
51
- return value.toString();
52
- }
53
- else if (value instanceof Promise) {
54
- return resolve(await value);
34
+ const { isFileCollection, isFileRef } = await import("./fs");
35
+ const appendices = {};
36
+ const stringValues = await Promise.all(values.map(async function resolve(value) {
37
+ if (typeof value === "string") {
38
+ return indent + value;
39
+ }
40
+ else if (value === null) {
41
+ return "null";
42
+ }
43
+ else if (value === undefined) {
44
+ return "undefined";
45
+ }
46
+ else if (typeof value === "number" ||
47
+ typeof value === "boolean" ||
48
+ typeof value === "bigint") {
49
+ return value.toString();
50
+ }
51
+ else if (value instanceof Promise) {
52
+ return resolve(await value);
53
+ }
54
+ else if (isFileRef(value)) {
55
+ if (!(value.path in appendices)) {
56
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
55
57
  }
56
- else if (isFileRef(value)) {
57
- if (!(value.path in appendices)) {
58
- appendices[value.path] = await fs.readFile(value.path, "utf-8");
59
- }
58
+ return `[${path.basename(value.path)}](${value.path})`;
59
+ }
60
+ else if (isFileCollection(value)) {
61
+ return Object.entries(value.files)
62
+ .map(([filePath, content]) => {
63
+ appendices[filePath] = content;
64
+ return `[${path.basename(filePath)}](${filePath})`;
65
+ })
66
+ .join("\n\n");
67
+ }
68
+ else if (Array.isArray(value)) {
69
+ return (await Promise.all(value.map(async (value, i) => `${i}. ${await resolve(value)}`))).join("\n");
70
+ }
71
+ else if (typeof value === "object" &&
72
+ typeof value.path === "string") {
73
+ if (typeof value.content === "string") {
74
+ appendices[value.path] = value.content;
60
75
  return `[${path.basename(value.path)}](${value.path})`;
61
76
  }
62
- else if (isFileCollection(value)) {
63
- return Object.entries(value.files)
64
- .map(([filePath, content]) => {
65
- appendices[filePath] = content;
66
- return `[${path.basename(filePath)}](${filePath})`;
67
- })
68
- .join("\n\n");
69
- }
70
- else if (Array.isArray(value)) {
71
- return (await Promise.all(value.map(async (value, i) => `${i}. ${await resolve(value)}`))).join("\n");
72
- }
73
- else if (typeof value === "object" &&
74
- typeof value.path === "string") {
75
- if (typeof value.content === "string") {
76
- appendices[value.path] = value.content;
77
- return `[${path.basename(value.path)}](${value.path})`;
78
- }
79
- else {
80
- appendices[value.path] = await fs.readFile(value.path, "utf-8");
81
- return `[${path.basename(value.path)}](${value.path})`;
82
- }
83
- }
84
- else if (typeof value === "object") {
85
- return (await Promise.all(Object.entries(value).map(async ([key, value]) => {
86
- return `* ${key}: ${await resolve(value)}`;
87
- }))).join("\n");
88
- }
89
77
  else {
90
- // TODO: support other types
91
- console.log(value);
92
- throw new Error(`Unsupported value type: ${value}`);
78
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
79
+ return `[${path.basename(value.path)}](${value.path})`;
93
80
  }
94
- }));
95
- // Construct the string template by joining template parts with interpolated values
96
- const lines = template
97
- .map((part) => part
98
- .split("\n")
99
- .map((line) => line.startsWith(indent) ? line.slice(indent.length) : line)
100
- .join("\n"))
101
- .flatMap((part, i) => i < stringValues.length ? [part, stringValues[i] ?? ""] : [part])
102
- .join("")
103
- .split("\n");
104
- // Collect and sort appendices by file path
105
- return [
106
- // format the user prompt and trim the first line if it's empty
107
- lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
108
- ? lines.slice(1).join("\n")
109
- : lines.join("\n"),
110
- // sort appendices by path and include at the end of the prompt
111
- Object.entries(appendices)
112
- .sort(([a], [b]) => a.localeCompare(b))
113
- .map(([filePath, content]) => {
114
- const extension = path.extname(filePath).slice(1);
115
- const codeTag = extension ? extension : "";
116
- return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
117
- })
118
- .join("\n\n"),
119
- ].join("\n");
120
- })();
81
+ }
82
+ else if (typeof value === "object") {
83
+ return (await Promise.all(Object.entries(value).map(async ([key, value]) => {
84
+ return `* ${key}: ${await resolve(value)}`;
85
+ }))).join("\n");
86
+ }
87
+ else {
88
+ // TODO: support other types
89
+ console.log(value);
90
+ throw new Error(`Unsupported value type: ${value}`);
91
+ }
92
+ }));
93
+ // Construct the string template by joining template parts with interpolated values
94
+ const lines = template
95
+ .map((part) => part
96
+ .split("\n")
97
+ .map((line) => line.startsWith(indent) ? line.slice(indent.length) : line)
98
+ .join("\n"))
99
+ .flatMap((part, i) => i < stringValues.length ? [part, stringValues[i] ?? ""] : [part])
100
+ .join("")
101
+ .split("\n");
102
+ // Collect and sort appendices by file path
103
+ return [
104
+ // format the user prompt and trim the first line if it's empty
105
+ lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
106
+ ? lines.slice(1).join("\n")
107
+ : lines.join("\n"),
108
+ // sort appendices by path and include at the end of the prompt
109
+ Object.entries(appendices)
110
+ .sort(([a], [b]) => a.localeCompare(b))
111
+ .map(([filePath, content]) => {
112
+ const extension = path.extname(filePath).slice(1);
113
+ const codeTag = extension ? extension : "";
114
+ return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
115
+ })
116
+ .join("\n\n"),
117
+ ].join("\n");
121
118
  }
122
119
  }
123
120
  _alchemy.destroy = destroy;
package/lib/apply.js CHANGED
@@ -48,14 +48,6 @@ export async function apply(resource, props, options) {
48
48
  }
49
49
  return state.output;
50
50
  }
51
- const diff = await import("diff");
52
- const yaml = await import("yaml");
53
- const old = yaml.stringify(oldProps);
54
- const news = yaml.stringify(newProps);
55
- const patch = diff.createPatch(resource.ID, old, news);
56
- // console.log(patch);
57
- // if (resource.ID === "what-is-alchemy") {
58
- // }
59
51
  }
60
52
  const phase = state.status === "creating" ? "create" : "update";
61
53
  state.status = phase === "create" ? "creating" : "updating";
@@ -21,7 +21,9 @@ import { getContentType } from "../util/content-type";
21
21
  * }
22
22
  * });
23
23
  */
24
- export const Assets = Resource("cloudflare::Asset", async function (id, props) {
24
+ export const Assets = Resource("cloudflare::Asset", {
25
+ alwaysUpdate: true,
26
+ }, async function (id, props) {
25
27
  if (this.phase === "delete") {
26
28
  return this.destroy();
27
29
  }
@@ -1,7 +1,8 @@
1
1
  import type { Secret } from "../secret";
2
+ import type { Assets } from "./assets";
2
3
  import type { Binding } from "./bindings";
3
4
  import type { R2Bucket as _R2Bucket } from "./bucket";
4
5
  import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace";
5
6
  import type { KVNamespace as _KVNamespace } from "./kv-namespace";
6
7
  import type { Worker as _Worker } from "./worker";
7
- export type Bound<T extends Binding> = T extends _DurableObjectNamespace ? DurableObjectNamespace : T extends _KVNamespace ? KVNamespace : T extends _Worker ? Worker : T extends _R2Bucket ? R2Bucket : T extends Secret ? string : never;
8
+ export type Bound<T extends Binding> = T extends _DurableObjectNamespace ? DurableObjectNamespace : T extends _KVNamespace ? KVNamespace : T extends _Worker ? Worker : T extends _R2Bucket ? R2Bucket : T extends Secret ? string : T extends Assets ? Service : Service;
@@ -28,11 +28,17 @@ export interface BucketProps extends CloudflareApiOptions {
28
28
  */
29
29
  allowPublicAccess?: boolean;
30
30
  /**
31
- * Whether to delete the bucket and its contents during resource deletion
31
+ * Whether to delete the bucket.
32
32
  * If set to false, the bucket will remain but the resource will be removed from state
33
- * Default: true
33
+ *
34
+ * @default true
34
35
  */
35
36
  delete?: boolean;
37
+ /**
38
+ * Whether to empty the bucket and delete all objects during resource deletion
39
+ * @default false
40
+ */
41
+ empty?: boolean;
36
42
  }
37
43
  /**
38
44
  * Output returned after R2 Bucket creation/update
@@ -85,6 +91,14 @@ export interface R2Bucket extends Resource<"cloudflare::R2Bucket">, BucketProps
85
91
  * jurisdiction: "fedramp"
86
92
  * });
87
93
  *
94
+ * @example
95
+ * // Create a bucket that will be automatically emptied when deleted
96
+ * // This will delete all objects in the bucket before deleting the bucket itself
97
+ * const temporaryBucket = await R2Bucket("temp-storage", {
98
+ * name: "temp-storage",
99
+ * empty: true // All objects will be deleted when this resource is destroyed
100
+ * });
101
+ *
88
102
  * @see https://developers.cloudflare.com/r2/buckets/
89
103
  */
90
104
  export declare const R2Bucket: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<R2Bucket>, id: string, props: BucketProps) => Promise<R2Bucket>);
@@ -35,19 +35,28 @@ import { CloudflareAccountId, CloudflareApi, CloudflareApiError, createCloudflar
35
35
  * jurisdiction: "fedramp"
36
36
  * });
37
37
  *
38
+ * @example
39
+ * // Create a bucket that will be automatically emptied when deleted
40
+ * // This will delete all objects in the bucket before deleting the bucket itself
41
+ * const temporaryBucket = await R2Bucket("temp-storage", {
42
+ * name: "temp-storage",
43
+ * empty: true // All objects will be deleted when this resource is destroyed
44
+ * });
45
+ *
38
46
  * @see https://developers.cloudflare.com/r2/buckets/
39
47
  */
40
48
  export const R2Bucket = Resource("cloudflare::R2Bucket", async function (id, props) {
41
- const [api, r2Client] = await Promise.all([
42
- createCloudflareApi(props),
43
- createR2Client(props),
44
- ]);
49
+ const api = await createCloudflareApi(props);
45
50
  const bucketName = props.name || this.id;
46
51
  if (this.phase === "delete") {
47
52
  console.log("Deleting R2 bucket:", bucketName);
48
53
  if (props.delete !== false) {
49
- // Empty the bucket first by deleting all objects
50
- await emptyBucket(r2Client, bucketName, props.jurisdiction);
54
+ if (props.empty) {
55
+ console.log("Emptying R2 bucket:", bucketName);
56
+ const r2Client = await createR2Client(props);
57
+ // Empty the bucket first by deleting all objects
58
+ await emptyBucket(r2Client, bucketName, props.jurisdiction);
59
+ }
51
60
  // Delete R2 bucket
52
61
  console.log("Deleting R2 bucket:", bucketName);
53
62
  await deleteBucket(api, bucketName, props);
@@ -41,8 +41,7 @@ export interface CustomDomain extends Resource<"cloudflare::CustomDomain">, Cust
41
41
  }
42
42
  /**
43
43
  * Configure custom domain for a Cloudflare Worker using the Cloudflare Custom Domains API
44
- * This attaches a worker (either a standard Worker or the one backing a StaticSite)
45
- * to a specific hostname within a zone.
44
+ * This attaches a worker to a specific hostname within a zone.
46
45
  *
47
46
  * @example
48
47
  * // Bind a domain to a standard Cloudflare Worker
@@ -57,19 +56,6 @@ export interface CustomDomain extends Resource<"cloudflare::CustomDomain">, Cust
57
56
  * workerName: apiWorker.name // Use the name from the Worker resource
58
57
  * });
59
58
  *
60
- * @example
61
- * // Bind a domain to a Cloudflare Static Site
62
- * const mySite = await StaticSite("landing-page", {
63
- * name: "my-landing-page-site", // This becomes the underlying worker name
64
- * dir: "./dist/landing-page"
65
- * });
66
- *
67
- * const siteDomain = await CustomDomain("site-domain-binding", {
68
- * name: "www.example.com",
69
- * zoneId: "YOUR_ZONE_ID", // Replace with actual Zone ID
70
- * workerName: mySite.name // Use the name from the StaticSite resource
71
- * });
72
- *
73
59
  * @see https://developers.cloudflare.com/api/resources/workers/subresources/domains/
74
60
  */
75
61
  export declare const CustomDomain: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<CustomDomain>, logicalId: string, props: CustomDomainProps) => Promise<CustomDomain>);
@@ -2,8 +2,7 @@ import { Resource } from "../resource";
2
2
  import { CloudflareApi, createCloudflareApi, handleApiError } from "./api";
3
3
  /**
4
4
  * Configure custom domain for a Cloudflare Worker using the Cloudflare Custom Domains API
5
- * This attaches a worker (either a standard Worker or the one backing a StaticSite)
6
- * to a specific hostname within a zone.
5
+ * This attaches a worker to a specific hostname within a zone.
7
6
  *
8
7
  * @example
9
8
  * // Bind a domain to a standard Cloudflare Worker
@@ -18,19 +17,6 @@ import { CloudflareApi, createCloudflareApi, handleApiError } from "./api";
18
17
  * workerName: apiWorker.name // Use the name from the Worker resource
19
18
  * });
20
19
  *
21
- * @example
22
- * // Bind a domain to a Cloudflare Static Site
23
- * const mySite = await StaticSite("landing-page", {
24
- * name: "my-landing-page-site", // This becomes the underlying worker name
25
- * dir: "./dist/landing-page"
26
- * });
27
- *
28
- * const siteDomain = await CustomDomain("site-domain-binding", {
29
- * name: "www.example.com",
30
- * zoneId: "YOUR_ZONE_ID", // Replace with actual Zone ID
31
- * workerName: mySite.name // Use the name from the StaticSite resource
32
- * });
33
- *
34
20
  * @see https://developers.cloudflare.com/api/resources/workers/subresources/domains/
35
21
  */
36
22
  export const CustomDomain = Resource("cloudflare::CustomDomain", async function (logicalId, // Changed param name from id to logicalId for clarity
@@ -9,7 +9,6 @@ export * from "./durable-object-namespace";
9
9
  export * from "./kv-namespace";
10
10
  export * from "./permission-groups";
11
11
  export * from "./r2-rest-state-store";
12
- export * from "./static-site";
13
12
  export * from "./worker";
14
13
  export * from "./wrangler.json";
15
14
  export * from "./zone";
@@ -9,7 +9,6 @@ export * from "./durable-object-namespace";
9
9
  export * from "./kv-namespace";
10
10
  export * from "./permission-groups";
11
11
  export * from "./r2-rest-state-store";
12
- export * from "./static-site";
13
12
  export * from "./worker";
14
13
  export * from "./wrangler.json";
15
14
  export * from "./zone";
@@ -74,6 +74,10 @@ export interface WorkerProps<B extends Bindings = Bindings> {
74
74
  * Migrations to apply to the worker
75
75
  */
76
76
  migrations?: SingleStepMigration;
77
+ /**
78
+ * Whether to adopt the Worker if it already exists when creating
79
+ */
80
+ adopt?: boolean;
77
81
  }
78
82
  /**
79
83
  * Output returned after Worker creation/update
@@ -102,7 +102,9 @@ export const Worker = Resource("cloudflare::Worker", {
102
102
  return this.destroy();
103
103
  }
104
104
  else if (this.phase === "create") {
105
- await assertWorkerDoesNotExist(this, api, workerName);
105
+ if (!props.adopt) {
106
+ await assertWorkerDoesNotExist(this, api, workerName);
107
+ }
106
108
  }
107
109
  const oldBindings = await this.get("bindings");
108
110
  // Get the script content - either from props.script, or by bundling
@@ -55,6 +55,7 @@ async function generateProviderDocs({ provider, outDir, parallel, }) {
55
55
  reasoningEffort: "high",
56
56
  },
57
57
  },
58
+ freeze: true,
58
59
  temperature: 0.1,
59
60
  schema: type({
60
61
  groups: type({
@@ -81,7 +82,7 @@ async function generateProviderDocs({ provider, outDir, parallel, }) {
81
82
  A file is considered a "Utility" if it contains utility functions that are not resources or clients.
82
83
  A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
83
84
 
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
+ 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. "StaticSite" for "const StaticSite". Maintain all other casing.
85
86
 
86
87
  // "Resource Name"
87
88
  const ResourceName = Resource(...)
@@ -0,0 +1,103 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ /**
4
+ * Properties for executing a shell command
5
+ */
6
+ export interface ExecProps {
7
+ /**
8
+ * The command to execute (including any arguments)
9
+ */
10
+ command: string;
11
+ /**
12
+ * Whether to memoize the command (only re-run if the command changes)
13
+ *
14
+ * @default false
15
+ */
16
+ memoize?: boolean;
17
+ /**
18
+ * Working directory for the command
19
+ */
20
+ cwd?: string;
21
+ /**
22
+ * Environment variables to set
23
+ */
24
+ env?: Record<string, string>;
25
+ /**
26
+ * Maximum buffer size for stdout and stderr (in bytes)
27
+ * @default 1024 * 1024 (1MB)
28
+ */
29
+ maxBuffer?: number;
30
+ /**
31
+ * Whether to throw an error if the command exits with a non-zero status
32
+ */
33
+ throwOnError?: boolean;
34
+ }
35
+ /**
36
+ * Output returned after command execution
37
+ */
38
+ export interface Exec extends Resource<"os::Exec">, ExecProps {
39
+ /**
40
+ * Unique identifier for this execution
41
+ */
42
+ id: string;
43
+ /**
44
+ * Exit code of the command
45
+ */
46
+ exitCode: number;
47
+ /**
48
+ * Standard output from the command
49
+ */
50
+ stdout: string;
51
+ /**
52
+ * Standard error from the command
53
+ */
54
+ stderr: string;
55
+ /**
56
+ * Time at which the command was executed
57
+ */
58
+ executedAt: number;
59
+ /**
60
+ * Whether the command has completed execution
61
+ */
62
+ completed: boolean;
63
+ }
64
+ /**
65
+ * Execute a shell command
66
+ *
67
+ * @example
68
+ * // Run a simple command
69
+ * const result = await Exec("list-files", {
70
+ * command: "ls -la"
71
+ * });
72
+ *
73
+ * console.log(result.stdout);
74
+ *
75
+ * @example
76
+ * // Run a command in a specific directory with custom environment
77
+ * const build = await Exec("build-project", {
78
+ * command: "npm run build",
79
+ * cwd: "./my-project",
80
+ * env: { NODE_ENV: "production" }
81
+ * });
82
+ *
83
+ * @example
84
+ * // Run a command with a larger buffer for output
85
+ * const logs = await Exec("get-logs", {
86
+ * command: "cat large-log-file.log",
87
+ * maxBuffer: 10 * 1024 * 1024 // 10MB
88
+ * });
89
+ *
90
+ * @example
91
+ * // Run a memoized command that only re-executes when the command changes
92
+ * const memoizedCmd = await Exec("status-check", {
93
+ * command: "git status",
94
+ * memoize: true
95
+ * });
96
+ *
97
+ * // This won't actually run the command again if nothing has changed
98
+ * await Exec("status-check", {
99
+ * command: "git status",
100
+ * memoize: true
101
+ * });
102
+ */
103
+ export declare const Exec: (((this: any, id: string, props?: {}) => never) & (new (_: never) => never)) | ((this: Context<Exec>, id: string, props: ExecProps) => Promise<Exec>);
package/lib/os/exec.js ADDED
@@ -0,0 +1,103 @@
1
+ import { exec } from "child_process";
2
+ import { promisify } from "util";
3
+ import { Resource } from "../resource";
4
+ const execAsync = promisify(exec);
5
+ /**
6
+ * Execute a shell command
7
+ *
8
+ * @example
9
+ * // Run a simple command
10
+ * const result = await Exec("list-files", {
11
+ * command: "ls -la"
12
+ * });
13
+ *
14
+ * console.log(result.stdout);
15
+ *
16
+ * @example
17
+ * // Run a command in a specific directory with custom environment
18
+ * const build = await Exec("build-project", {
19
+ * command: "npm run build",
20
+ * cwd: "./my-project",
21
+ * env: { NODE_ENV: "production" }
22
+ * });
23
+ *
24
+ * @example
25
+ * // Run a command with a larger buffer for output
26
+ * const logs = await Exec("get-logs", {
27
+ * command: "cat large-log-file.log",
28
+ * maxBuffer: 10 * 1024 * 1024 // 10MB
29
+ * });
30
+ *
31
+ * @example
32
+ * // Run a memoized command that only re-executes when the command changes
33
+ * const memoizedCmd = await Exec("status-check", {
34
+ * command: "git status",
35
+ * memoize: true
36
+ * });
37
+ *
38
+ * // This won't actually run the command again if nothing has changed
39
+ * await Exec("status-check", {
40
+ * command: "git status",
41
+ * memoize: true
42
+ * });
43
+ */
44
+ export const Exec = Resource("os::Exec", {
45
+ alwaysUpdate: true,
46
+ }, async function (id, props) {
47
+ if (this.phase === "delete") {
48
+ // Nothing to actually delete for an exec command
49
+ return this.destroy();
50
+ }
51
+ else if (this.phase === "update" &&
52
+ props.memoize &&
53
+ this.output?.command === props.command) {
54
+ // If memoize is enabled and the command hasn't changed, return the existing output
55
+ return this.output;
56
+ }
57
+ else {
58
+ // Default values
59
+ let stdout = "";
60
+ let stderr = "";
61
+ let exitCode = 0;
62
+ try {
63
+ console.log(props.command);
64
+ // Execute the command
65
+ const result = await execAsync(props.command, {
66
+ cwd: props.cwd || process.cwd(),
67
+ env: { ...process.env, ...props.env },
68
+ maxBuffer: props.maxBuffer || 1024 * 1024, // Default 1MB
69
+ });
70
+ stdout = result.stdout;
71
+ console.log(stdout);
72
+ stderr = result.stderr;
73
+ exitCode = 0; // Success
74
+ }
75
+ catch (error) {
76
+ console.log("error", error);
77
+ if (props.throwOnError) {
78
+ throw error;
79
+ }
80
+ // If not throwing, capture the error information
81
+ exitCode = error.code || 1;
82
+ stdout = error.stdout || "";
83
+ stderr = error.stderr || String(error);
84
+ console.log(stdout);
85
+ console.error(stderr);
86
+ }
87
+ // Return the execution result
88
+ return this({
89
+ id,
90
+ command: props.command,
91
+ cwd: props.cwd,
92
+ env: props.env,
93
+ maxBuffer: props.maxBuffer,
94
+ throwOnError: props.throwOnError,
95
+ memoize: props.memoize,
96
+ exitCode,
97
+ stdout,
98
+ stderr,
99
+ executedAt: Date.now(),
100
+ completed: true,
101
+ });
102
+ }
103
+ });
@@ -0,0 +1 @@
1
+ export * from "./exec";
@@ -0,0 +1 @@
1
+ export * from "./exec";
package/lib/secret.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * 1. Globally when initializing the alchemy application:
7
7
  * ```ts
8
- * const app = alchemy("my-app", {
8
+ * const app = await alchemy("my-app", {
9
9
  * password: process.env.SECRET_PASSPHRASE
10
10
  * });
11
11
  * ```
@@ -49,7 +49,7 @@ export declare function isSecret(binding: any): binding is Secret;
49
49
  *
50
50
  * @example
51
51
  * // Global password for all secrets
52
- * const app = alchemy("my-app", {
52
+ * const app = await alchemy("my-app", {
53
53
  * password: process.env.SECRET_PASSPHRASE
54
54
  * });
55
55
  *