alchemy 0.4.0 → 0.5.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.
- package/README.md +3 -463
- package/lib/ai/data.d.ts +5 -0
- package/lib/ai/data.js +3 -0
- package/lib/alchemy.d.ts +3 -3
- package/lib/alchemy.js +82 -85
- package/lib/cloudflare/assets.d.ts +74 -0
- package/lib/cloudflare/assets.js +75 -0
- package/lib/cloudflare/bindings.d.ts +6 -1
- package/lib/cloudflare/bindings.js +6 -0
- package/lib/cloudflare/bound.d.ts +2 -1
- package/lib/cloudflare/custom-domain.d.ts +1 -15
- package/lib/cloudflare/custom-domain.js +1 -15
- package/lib/cloudflare/index.d.ts +1 -1
- package/lib/cloudflare/index.js +1 -1
- package/lib/cloudflare/r2-rest-state-store.js +52 -22
- package/lib/cloudflare/worker.d.ts +18 -0
- package/lib/cloudflare/worker.js +153 -4
- package/lib/internal/docs/providers.js +2 -1
- package/lib/os/exec.d.ts +103 -0
- package/lib/os/exec.js +103 -0
- package/lib/os/index.d.ts +1 -0
- package/lib/os/index.js +1 -0
- package/lib/secret.d.ts +2 -2
- package/lib/secret.js +2 -2
- package/lib/web/vitepress/vitepress.js +1 -1
- package/package.json +2 -1
- package/src/ai/data.ts +10 -0
- package/src/alchemy.ts +97 -100
- package/src/cloudflare/assets.ts +158 -0
- package/src/cloudflare/bindings.ts +11 -2
- package/src/cloudflare/bound.ts +4 -1
- package/src/cloudflare/custom-domain.ts +1 -15
- package/src/cloudflare/index.ts +1 -1
- package/src/cloudflare/r2-rest-state-store.ts +78 -34
- package/src/cloudflare/worker.ts +261 -6
- package/src/internal/docs/providers.ts +2 -1
- package/src/os/exec.ts +190 -0
- package/src/os/index.ts +1 -0
- package/src/secret.ts +2 -2
- package/src/web/vitepress/vitepress.ts +1 -1
- package/lib/cloudflare/generate-asset-manifest.d.ts +0 -2
- package/lib/cloudflare/generate-asset-manifest.js +0 -68
- package/lib/cloudflare/static-site-router.d.ts +0 -18
- package/lib/cloudflare/static-site-router.js +0 -115
- package/lib/cloudflare/static-site.d.ts +0 -211
- package/lib/cloudflare/static-site.js +0 -213
- package/lib/cloudflare/upload-asset-manifest.d.ts +0 -11
- package/lib/cloudflare/upload-asset-manifest.js +0 -56
- package/src/cloudflare/generate-asset-manifest.ts +0 -93
- package/src/cloudflare/static-site-router.ts +0 -157
- package/src/cloudflare/static-site.ts +0 -416
- package/src/cloudflare/upload-asset-manifest.ts +0 -85
- package/src/web/vitepress/index.md +0 -75
package/src/cloudflare/worker.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import * as crypto from "crypto";
|
|
1
2
|
import * as fs from "fs/promises";
|
|
2
3
|
import type { Context } from "../context";
|
|
3
4
|
import { Bundle, type BundleProps } from "../esbuild/bundle";
|
|
4
5
|
import { Resource } from "../resource";
|
|
5
6
|
import { isSecret } from "../secret";
|
|
7
|
+
import { getContentType } from "../util/content-type";
|
|
6
8
|
import { withExponentialBackoff } from "../util/retry";
|
|
7
9
|
import { slugify } from "../util/slugify";
|
|
8
10
|
import { type CloudflareApi, createCloudflareApi } from "./api";
|
|
11
|
+
import type { Assets } from "./assets";
|
|
9
12
|
import {
|
|
10
13
|
type Bindings,
|
|
11
14
|
type WorkerBindingSpec,
|
|
15
|
+
isAssets,
|
|
12
16
|
isDurableObjectNamespace,
|
|
13
17
|
} from "./bindings";
|
|
14
18
|
import type { Bound } from "./bound";
|
|
@@ -97,6 +101,11 @@ export interface WorkerProps<B extends Bindings = Bindings> {
|
|
|
97
101
|
* Migrations to apply to the worker
|
|
98
102
|
*/
|
|
99
103
|
migrations?: SingleStepMigration;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether to adopt the Worker if it already exists when creating
|
|
107
|
+
*/
|
|
108
|
+
adopt?: boolean;
|
|
100
109
|
}
|
|
101
110
|
|
|
102
111
|
/**
|
|
@@ -200,6 +209,20 @@ export interface Worker<B extends Bindings = Bindings>
|
|
|
200
209
|
* }
|
|
201
210
|
* });
|
|
202
211
|
*
|
|
212
|
+
* @example
|
|
213
|
+
* // Create a worker with static assets:
|
|
214
|
+
* const staticAssets = await Assets("static", {
|
|
215
|
+
* path: "./src/assets"
|
|
216
|
+
* });
|
|
217
|
+
*
|
|
218
|
+
* const frontendWorker = await Worker("frontend", {
|
|
219
|
+
* name: "frontend-worker",
|
|
220
|
+
* entrypoint: "./src/worker.ts",
|
|
221
|
+
* bindings: {
|
|
222
|
+
* ASSETS: staticAssets
|
|
223
|
+
* }
|
|
224
|
+
* });
|
|
225
|
+
*
|
|
203
226
|
* @see https://developers.cloudflare.com/workers/
|
|
204
227
|
*/
|
|
205
228
|
export const Worker = Resource(
|
|
@@ -227,20 +250,49 @@ export const Worker = Resource(
|
|
|
227
250
|
await deleteWorker(this, api, workerName);
|
|
228
251
|
return this.destroy();
|
|
229
252
|
} else if (this.phase === "create") {
|
|
230
|
-
|
|
253
|
+
if (!props.adopt) {
|
|
254
|
+
await assertWorkerDoesNotExist(this, api, workerName);
|
|
255
|
+
}
|
|
231
256
|
}
|
|
232
257
|
|
|
233
258
|
const oldBindings = await this.get<Bindings>("bindings");
|
|
234
259
|
|
|
260
|
+
// Get the script content - either from props.script, or by bundling
|
|
261
|
+
const scriptContent = props.script ?? (await bundleWorkerScript(props));
|
|
262
|
+
|
|
263
|
+
// Find any assets bindings
|
|
264
|
+
const assetsBindings: { name: string; assets: Assets }[] = [];
|
|
265
|
+
if (props.bindings) {
|
|
266
|
+
for (const [bindingName, binding] of Object.entries(props.bindings)) {
|
|
267
|
+
if (isAssets(binding)) {
|
|
268
|
+
assetsBindings.push({ name: bindingName, assets: binding });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Upload any assets and get completion tokens
|
|
274
|
+
let assetUploadResult: AssetUploadResult | undefined;
|
|
275
|
+
if (assetsBindings.length > 0) {
|
|
276
|
+
// We'll use the first asset binding for now
|
|
277
|
+
// In the future, we might want to support multiple asset bindings
|
|
278
|
+
const assetBinding = assetsBindings[0];
|
|
279
|
+
|
|
280
|
+
// Upload the assets and get the completion token
|
|
281
|
+
assetUploadResult = await uploadAssets(
|
|
282
|
+
api,
|
|
283
|
+
workerName,
|
|
284
|
+
assetBinding.assets
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Prepare metadata with bindings
|
|
235
289
|
const scriptMetadata = await prepareWorkerMetadata(
|
|
236
290
|
this,
|
|
237
291
|
oldBindings,
|
|
238
|
-
props
|
|
292
|
+
props,
|
|
293
|
+
assetUploadResult
|
|
239
294
|
);
|
|
240
295
|
|
|
241
|
-
// Get the script content - either from props.script, or by bundling
|
|
242
|
-
const scriptContent = props.script ?? (await bundleWorkerScript(props));
|
|
243
|
-
|
|
244
296
|
// Upload the worker script
|
|
245
297
|
await putWorker(api, workerName, scriptContent, scriptMetadata);
|
|
246
298
|
|
|
@@ -437,12 +489,29 @@ interface WorkerMetadata {
|
|
|
437
489
|
main_module?: string;
|
|
438
490
|
body_part?: string;
|
|
439
491
|
tags?: string[];
|
|
492
|
+
assets?: {
|
|
493
|
+
jwt?: string;
|
|
494
|
+
keep_assets?: boolean;
|
|
495
|
+
config?: {
|
|
496
|
+
html_handling?: "auto-trailing-slash" | "none";
|
|
497
|
+
not_found_handling?: "none" | "fall-through";
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
interface AssetUploadResult {
|
|
503
|
+
completionToken: string;
|
|
504
|
+
assetConfig?: {
|
|
505
|
+
html_handling?: "auto-trailing-slash" | "none";
|
|
506
|
+
not_found_handling?: "none" | "fall-through";
|
|
507
|
+
};
|
|
440
508
|
}
|
|
441
509
|
|
|
442
510
|
async function prepareWorkerMetadata<B extends Bindings>(
|
|
443
511
|
ctx: Context<Worker<B>>,
|
|
444
512
|
oldBindings: Bindings | undefined,
|
|
445
|
-
props: WorkerProps
|
|
513
|
+
props: WorkerProps,
|
|
514
|
+
assetUploadResult?: AssetUploadResult
|
|
446
515
|
): Promise<WorkerMetadata> {
|
|
447
516
|
// Prepare metadata with bindings
|
|
448
517
|
const meta: WorkerMetadata = {
|
|
@@ -461,6 +530,17 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
461
530
|
},
|
|
462
531
|
};
|
|
463
532
|
|
|
533
|
+
// If we have asset upload results, add them to the metadata
|
|
534
|
+
if (assetUploadResult) {
|
|
535
|
+
meta.assets = {
|
|
536
|
+
jwt: assetUploadResult.completionToken,
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
if (assetUploadResult.assetConfig) {
|
|
540
|
+
meta.assets.config = assetUploadResult.assetConfig;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
464
544
|
const bindings = (props.bindings ?? {}) as Bindings;
|
|
465
545
|
|
|
466
546
|
// Convert bindings to the format expected by the API
|
|
@@ -522,6 +602,11 @@ async function prepareWorkerMetadata<B extends Bindings>(
|
|
|
522
602
|
name: bindingName,
|
|
523
603
|
bucket_name: binding.name,
|
|
524
604
|
});
|
|
605
|
+
} else if (isAssets(binding)) {
|
|
606
|
+
meta.bindings.push({
|
|
607
|
+
type: "assets",
|
|
608
|
+
name: bindingName,
|
|
609
|
+
});
|
|
525
610
|
} else if (isSecret(binding)) {
|
|
526
611
|
meta.bindings.push({
|
|
527
612
|
type: "secret_text",
|
|
@@ -802,3 +887,173 @@ async function getWorkerBindings(
|
|
|
802
887
|
|
|
803
888
|
return data.result;
|
|
804
889
|
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Interface for a file's metadata to be uploaded
|
|
893
|
+
*/
|
|
894
|
+
interface FileMetadata {
|
|
895
|
+
hash: string;
|
|
896
|
+
size: number;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Response from the assets upload session API
|
|
901
|
+
*/
|
|
902
|
+
interface UploadSessionResponse {
|
|
903
|
+
result: {
|
|
904
|
+
jwt: string;
|
|
905
|
+
buckets: string[][];
|
|
906
|
+
};
|
|
907
|
+
success: boolean;
|
|
908
|
+
errors: any[];
|
|
909
|
+
messages: any[];
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Response from the file upload API
|
|
914
|
+
*/
|
|
915
|
+
interface UploadResponse {
|
|
916
|
+
result: {
|
|
917
|
+
jwt: string;
|
|
918
|
+
buckets?: string[][];
|
|
919
|
+
};
|
|
920
|
+
success: boolean;
|
|
921
|
+
errors: any[];
|
|
922
|
+
messages: any[];
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Uploads assets to Cloudflare and returns a completion token
|
|
927
|
+
*
|
|
928
|
+
* @param api CloudflareApi instance
|
|
929
|
+
* @param workerName Name of the worker
|
|
930
|
+
* @param assets Assets resource containing files to upload
|
|
931
|
+
* @returns Completion token for the assets upload
|
|
932
|
+
*/
|
|
933
|
+
async function uploadAssets(
|
|
934
|
+
api: CloudflareApi,
|
|
935
|
+
workerName: string,
|
|
936
|
+
assets: Assets
|
|
937
|
+
): Promise<AssetUploadResult> {
|
|
938
|
+
// Generate the file manifest
|
|
939
|
+
const fileMetadata: Record<string, FileMetadata> = {};
|
|
940
|
+
|
|
941
|
+
// Process each file in the assets
|
|
942
|
+
for (const file of assets.files) {
|
|
943
|
+
const { hash, size } = await calculateFileMetadata(file.filePath);
|
|
944
|
+
// Use the relative path as the key, ensuring it starts with a slash
|
|
945
|
+
const key = file.path.startsWith("/") ? file.path : `/${file.path}`;
|
|
946
|
+
fileMetadata[key] = { hash, size };
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// Start the upload session
|
|
950
|
+
const uploadSessionUrl = `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`;
|
|
951
|
+
const uploadSessionResponse = await api.post(
|
|
952
|
+
uploadSessionUrl,
|
|
953
|
+
JSON.stringify({ manifest: fileMetadata }),
|
|
954
|
+
{
|
|
955
|
+
headers: { "Content-Type": "application/json" },
|
|
956
|
+
}
|
|
957
|
+
);
|
|
958
|
+
|
|
959
|
+
if (!uploadSessionResponse.ok) {
|
|
960
|
+
throw new Error(
|
|
961
|
+
`Failed to start assets upload session: ${uploadSessionResponse.status} ${uploadSessionResponse.statusText}`
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const sessionData =
|
|
966
|
+
(await uploadSessionResponse.json()) as UploadSessionResponse;
|
|
967
|
+
|
|
968
|
+
// If there are no buckets, assets are already uploaded or empty
|
|
969
|
+
if (!sessionData.result.buckets || sessionData.result.buckets.length === 0) {
|
|
970
|
+
return { completionToken: sessionData.result.jwt };
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
// Upload the files in batches as specified by the API
|
|
974
|
+
let completionToken = sessionData.result.jwt;
|
|
975
|
+
const buckets = sessionData.result.buckets;
|
|
976
|
+
|
|
977
|
+
// Process each bucket of files
|
|
978
|
+
for (const bucket of buckets) {
|
|
979
|
+
const formData = new FormData();
|
|
980
|
+
|
|
981
|
+
// Add each file in the bucket to the form
|
|
982
|
+
for (const fileHash of bucket) {
|
|
983
|
+
// Find the file with this hash
|
|
984
|
+
const file = assets.files.find((f) => {
|
|
985
|
+
const filePath = f.path.startsWith("/") ? f.path : `/${f.path}`;
|
|
986
|
+
return fileMetadata[filePath]?.hash === fileHash;
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
if (!file) {
|
|
990
|
+
throw new Error(`Could not find file with hash ${fileHash}`);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Read the file content
|
|
994
|
+
const fileContent = await fs.readFile(file.filePath);
|
|
995
|
+
|
|
996
|
+
// Convert to base64 as required by the API when using base64=true
|
|
997
|
+
const base64Content = fileContent.toString("base64");
|
|
998
|
+
|
|
999
|
+
// Add the file to the form with the hash as the key and set the correct content type
|
|
1000
|
+
const blob = new Blob([base64Content], {
|
|
1001
|
+
type: getContentType(file.filePath),
|
|
1002
|
+
});
|
|
1003
|
+
formData.append(fileHash, blob, fileHash);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Upload this batch of files
|
|
1007
|
+
const uploadResponse = await api.post(
|
|
1008
|
+
`/accounts/${api.accountId}/workers/assets/upload?base64=true`,
|
|
1009
|
+
formData,
|
|
1010
|
+
{
|
|
1011
|
+
headers: {
|
|
1012
|
+
Authorization: `Bearer ${completionToken}`,
|
|
1013
|
+
"Content-Type": "multipart/form-data",
|
|
1014
|
+
},
|
|
1015
|
+
}
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
if (!uploadResponse.ok) {
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
`Failed to upload asset files: ${uploadResponse.status} ${uploadResponse.statusText}`
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
const uploadData = (await uploadResponse.json()) as UploadResponse;
|
|
1025
|
+
// Update the completion token for the next batch
|
|
1026
|
+
if (uploadData.result.jwt) {
|
|
1027
|
+
completionToken = uploadData.result.jwt;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// Return the final completion token
|
|
1032
|
+
return {
|
|
1033
|
+
completionToken,
|
|
1034
|
+
assetConfig: {
|
|
1035
|
+
html_handling: "auto-trailing-slash",
|
|
1036
|
+
},
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Calculate the SHA-256 hash and size of a file
|
|
1042
|
+
*
|
|
1043
|
+
* @param filePath Path to the file
|
|
1044
|
+
* @returns Hash (first 32 chars of SHA-256) and size of the file
|
|
1045
|
+
*/
|
|
1046
|
+
async function calculateFileMetadata(
|
|
1047
|
+
filePath: string
|
|
1048
|
+
): Promise<{ hash: string; size: number }> {
|
|
1049
|
+
const hash = crypto.createHash("sha256");
|
|
1050
|
+
const fileContent = await fs.readFile(filePath);
|
|
1051
|
+
|
|
1052
|
+
hash.update(fileContent);
|
|
1053
|
+
const fileHash = hash.digest("hex").substring(0, 32); // First 32 chars of hash
|
|
1054
|
+
|
|
1055
|
+
return {
|
|
1056
|
+
hash: fileHash,
|
|
1057
|
+
size: fileContent.length,
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
@@ -124,6 +124,7 @@ async function generateProviderDocs({
|
|
|
124
124
|
reasoningEffort: "high",
|
|
125
125
|
},
|
|
126
126
|
},
|
|
127
|
+
freeze: true,
|
|
127
128
|
temperature: 0.1,
|
|
128
129
|
schema: type({
|
|
129
130
|
groups: type({
|
|
@@ -156,7 +157,7 @@ async function generateProviderDocs({
|
|
|
156
157
|
A file is considered a "Utility" if it contains utility functions that are not resources or clients.
|
|
157
158
|
A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.
|
|
158
159
|
|
|
159
|
-
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. "
|
|
160
|
+
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.
|
|
160
161
|
|
|
161
162
|
// "Resource Name"
|
|
162
163
|
const ResourceName = Resource(...)
|
package/src/os/exec.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
import { promisify } from "util";
|
|
3
|
+
import type { Context } from "../context";
|
|
4
|
+
import { Resource } from "../resource";
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Properties for executing a shell command
|
|
10
|
+
*/
|
|
11
|
+
export interface ExecProps {
|
|
12
|
+
/**
|
|
13
|
+
* The command to execute (including any arguments)
|
|
14
|
+
*/
|
|
15
|
+
command: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Whether to memoize the command (only re-run if the command changes)
|
|
19
|
+
*
|
|
20
|
+
* @default false
|
|
21
|
+
*/
|
|
22
|
+
memoize?: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Working directory for the command
|
|
26
|
+
*/
|
|
27
|
+
cwd?: string;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Environment variables to set
|
|
31
|
+
*/
|
|
32
|
+
env?: Record<string, string>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum buffer size for stdout and stderr (in bytes)
|
|
36
|
+
* @default 1024 * 1024 (1MB)
|
|
37
|
+
*/
|
|
38
|
+
maxBuffer?: number;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Whether to throw an error if the command exits with a non-zero status
|
|
42
|
+
*/
|
|
43
|
+
throwOnError?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Output returned after command execution
|
|
48
|
+
*/
|
|
49
|
+
export interface Exec extends Resource<"os::Exec">, ExecProps {
|
|
50
|
+
/**
|
|
51
|
+
* Unique identifier for this execution
|
|
52
|
+
*/
|
|
53
|
+
id: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Exit code of the command
|
|
57
|
+
*/
|
|
58
|
+
exitCode: number;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Standard output from the command
|
|
62
|
+
*/
|
|
63
|
+
stdout: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Standard error from the command
|
|
67
|
+
*/
|
|
68
|
+
stderr: string;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Time at which the command was executed
|
|
72
|
+
*/
|
|
73
|
+
executedAt: number;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Whether the command has completed execution
|
|
77
|
+
*/
|
|
78
|
+
completed: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Execute a shell command
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* // Run a simple command
|
|
86
|
+
* const result = await Exec("list-files", {
|
|
87
|
+
* command: "ls -la"
|
|
88
|
+
* });
|
|
89
|
+
*
|
|
90
|
+
* console.log(result.stdout);
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Run a command in a specific directory with custom environment
|
|
94
|
+
* const build = await Exec("build-project", {
|
|
95
|
+
* command: "npm run build",
|
|
96
|
+
* cwd: "./my-project",
|
|
97
|
+
* env: { NODE_ENV: "production" }
|
|
98
|
+
* });
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Run a command with a larger buffer for output
|
|
102
|
+
* const logs = await Exec("get-logs", {
|
|
103
|
+
* command: "cat large-log-file.log",
|
|
104
|
+
* maxBuffer: 10 * 1024 * 1024 // 10MB
|
|
105
|
+
* });
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Run a memoized command that only re-executes when the command changes
|
|
109
|
+
* const memoizedCmd = await Exec("status-check", {
|
|
110
|
+
* command: "git status",
|
|
111
|
+
* memoize: true
|
|
112
|
+
* });
|
|
113
|
+
*
|
|
114
|
+
* // This won't actually run the command again if nothing has changed
|
|
115
|
+
* await Exec("status-check", {
|
|
116
|
+
* command: "git status",
|
|
117
|
+
* memoize: true
|
|
118
|
+
* });
|
|
119
|
+
*/
|
|
120
|
+
export const Exec = Resource(
|
|
121
|
+
"os::Exec",
|
|
122
|
+
{
|
|
123
|
+
alwaysUpdate: true,
|
|
124
|
+
},
|
|
125
|
+
async function (
|
|
126
|
+
this: Context<Exec>,
|
|
127
|
+
id: string,
|
|
128
|
+
props: ExecProps
|
|
129
|
+
): Promise<Exec> {
|
|
130
|
+
if (this.phase === "delete") {
|
|
131
|
+
// Nothing to actually delete for an exec command
|
|
132
|
+
return this.destroy();
|
|
133
|
+
} else if (
|
|
134
|
+
this.phase === "update" &&
|
|
135
|
+
props.memoize &&
|
|
136
|
+
this.output?.command === props.command
|
|
137
|
+
) {
|
|
138
|
+
// If memoize is enabled and the command hasn't changed, return the existing output
|
|
139
|
+
return this.output;
|
|
140
|
+
} else {
|
|
141
|
+
// Default values
|
|
142
|
+
let stdout = "";
|
|
143
|
+
let stderr = "";
|
|
144
|
+
let exitCode = 0;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
console.log(props.command);
|
|
148
|
+
// Execute the command
|
|
149
|
+
const result = await execAsync(props.command, {
|
|
150
|
+
cwd: props.cwd || process.cwd(),
|
|
151
|
+
env: { ...process.env, ...props.env },
|
|
152
|
+
maxBuffer: props.maxBuffer || 1024 * 1024, // Default 1MB
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
stdout = result.stdout;
|
|
156
|
+
console.log(stdout);
|
|
157
|
+
stderr = result.stderr;
|
|
158
|
+
exitCode = 0; // Success
|
|
159
|
+
} catch (error: any) {
|
|
160
|
+
console.log("error", error);
|
|
161
|
+
if (props.throwOnError) {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// If not throwing, capture the error information
|
|
166
|
+
exitCode = error.code || 1;
|
|
167
|
+
stdout = error.stdout || "";
|
|
168
|
+
stderr = error.stderr || String(error);
|
|
169
|
+
console.log(stdout);
|
|
170
|
+
console.error(stderr);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Return the execution result
|
|
174
|
+
return this({
|
|
175
|
+
id,
|
|
176
|
+
command: props.command,
|
|
177
|
+
cwd: props.cwd,
|
|
178
|
+
env: props.env,
|
|
179
|
+
maxBuffer: props.maxBuffer,
|
|
180
|
+
throwOnError: props.throwOnError,
|
|
181
|
+
memoize: props.memoize,
|
|
182
|
+
exitCode,
|
|
183
|
+
stdout,
|
|
184
|
+
stderr,
|
|
185
|
+
executedAt: Date.now(),
|
|
186
|
+
completed: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
package/src/os/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./exec";
|
package/src/secret.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
|
* ```
|
|
@@ -55,7 +55,7 @@ export function isSecret(binding: any): binding is Secret {
|
|
|
55
55
|
*
|
|
56
56
|
* @example
|
|
57
57
|
* // Global password for all secrets
|
|
58
|
-
* const app = alchemy("my-app", {
|
|
58
|
+
* const app = await alchemy("my-app", {
|
|
59
59
|
* password: process.env.SECRET_PASSPHRASE
|
|
60
60
|
* });
|
|
61
61
|
*
|
|
@@ -190,7 +190,7 @@ export const VitepressProject = Resource(
|
|
|
190
190
|
// import { Document } from "alchemy/docs";
|
|
191
191
|
// import path from "path";
|
|
192
192
|
|
|
193
|
-
//
|
|
193
|
+
// const app = await alchemy("alchemy.run", {
|
|
194
194
|
// stage: "prod",
|
|
195
195
|
// phase: process.argv.includes("--destroy") ? "destroy" : "up",
|
|
196
196
|
// password: process.env.SECRET_PASSPHRASE,
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import crypto from "crypto";
|
|
2
|
-
import { promises as fs } from "fs";
|
|
3
|
-
import { glob } from "glob";
|
|
4
|
-
import path from "path";
|
|
5
|
-
import { getContentType } from "../util/content-type";
|
|
6
|
-
export async function generateAssetManifest(dir, fileOptions = [
|
|
7
|
-
{
|
|
8
|
-
files: "**",
|
|
9
|
-
cacheControl: "max-age=0,no-cache,no-store,must-revalidate",
|
|
10
|
-
},
|
|
11
|
-
{
|
|
12
|
-
files: ["**/*.js", "**/*.css", "**/*.png", "**/*.webp"],
|
|
13
|
-
cacheControl: "max-age=31536000,public,immutable",
|
|
14
|
-
},
|
|
15
|
-
]) {
|
|
16
|
-
// First, collect all file info based on fileOptions (reversed to prioritize later rules)
|
|
17
|
-
const fileInfos = [];
|
|
18
|
-
const filesProcessed = new Set();
|
|
19
|
-
// Process file options in reverse order (later rules override earlier ones)
|
|
20
|
-
// Use for...of to maintain order but allow await inside the loop
|
|
21
|
-
const reversedOptions = [...fileOptions].reverse();
|
|
22
|
-
for (const fileOption of reversedOptions) {
|
|
23
|
-
const patterns = Array.isArray(fileOption.files)
|
|
24
|
-
? fileOption.files
|
|
25
|
-
: [fileOption.files];
|
|
26
|
-
// Get all files matching the pattern
|
|
27
|
-
const files = (await Promise.all(patterns.map(async (p) => {
|
|
28
|
-
// Use glob as async function instead of globSync
|
|
29
|
-
return glob(p, {
|
|
30
|
-
cwd: path.resolve(dir),
|
|
31
|
-
nodir: true,
|
|
32
|
-
dot: true,
|
|
33
|
-
ignore: [
|
|
34
|
-
".git/**",
|
|
35
|
-
"node_modules/**",
|
|
36
|
-
...(typeof fileOption.ignore === "string"
|
|
37
|
-
? [fileOption.ignore]
|
|
38
|
-
: (fileOption.ignore ?? [])),
|
|
39
|
-
],
|
|
40
|
-
});
|
|
41
|
-
})))
|
|
42
|
-
.flat()
|
|
43
|
-
.filter((file) => !filesProcessed.has(file));
|
|
44
|
-
// Add files to processed set
|
|
45
|
-
for (const file of files) {
|
|
46
|
-
filesProcessed.add(file);
|
|
47
|
-
fileInfos.push({
|
|
48
|
-
file,
|
|
49
|
-
cacheControl: fileOption.cacheControl,
|
|
50
|
-
contentType: fileOption.contentType,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
// Process all files in parallel
|
|
55
|
-
const manifestEntries = await Promise.all(fileInfos.map(async ({ file, cacheControl, contentType }) => {
|
|
56
|
-
const source = path.resolve(dir, file);
|
|
57
|
-
const content = await fs.readFile(source);
|
|
58
|
-
const hash = crypto.createHash("sha256").update(content).digest("hex");
|
|
59
|
-
return {
|
|
60
|
-
source,
|
|
61
|
-
key: file,
|
|
62
|
-
hash,
|
|
63
|
-
cacheControl,
|
|
64
|
-
contentType: contentType ?? getContentType(file),
|
|
65
|
-
};
|
|
66
|
-
}));
|
|
67
|
-
return manifestEntries;
|
|
68
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export type Env = {
|
|
2
|
-
ASSETS: KVNamespace;
|
|
3
|
-
INDEX_PAGE: string;
|
|
4
|
-
ERROR_PAGE?: string;
|
|
5
|
-
/**
|
|
6
|
-
* Optional backend worker bound to this static site
|
|
7
|
-
* Used to handle requests that don't match any static files
|
|
8
|
-
*/
|
|
9
|
-
BACKEND_WORKER?: Service;
|
|
10
|
-
} & {
|
|
11
|
-
[key in `ROUTE_${string}`]: Service;
|
|
12
|
-
} & {
|
|
13
|
-
[key in `__ROUTE_${string}`]: string;
|
|
14
|
-
};
|
|
15
|
-
declare const _default: {
|
|
16
|
-
fetch(request: Request, env: Env): Promise<Response>;
|
|
17
|
-
};
|
|
18
|
-
export default _default;
|