alchemy 0.2.2 → 0.2.3
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/lib/alchemy.d.ts +1 -0
- package/lib/alchemy.js +84 -8
- package/lib/apply.d.ts +1 -1
- package/lib/apply.js +13 -10
- package/lib/aws/bucket.d.ts +1 -1
- package/lib/aws/function.d.ts +1 -1
- package/lib/aws/oidc/oidc-provider.d.ts +1 -1
- package/lib/aws/policy-attachment.d.ts +1 -1
- package/lib/aws/policy.d.ts +1 -1
- package/lib/aws/queue.d.ts +1 -1
- package/lib/aws/role.d.ts +1 -1
- package/lib/aws/ses.d.ts +1 -1
- package/lib/aws/table.d.ts +1 -1
- package/lib/cloudflare/bucket.d.ts +1 -1
- package/lib/cloudflare/kv-namespace.d.ts +1 -1
- package/lib/cloudflare/static-site.d.ts +1 -1
- package/lib/cloudflare/worker.d.ts +1 -1
- package/lib/cloudflare/wrangler.json.d.ts +1 -1
- package/lib/cloudflare/zone.d.ts +1 -1
- package/lib/context.d.ts +9 -4
- package/lib/context.js +4 -2
- package/lib/destroy.js +8 -7
- package/lib/docs/document.d.ts +123 -0
- package/lib/docs/document.js +67 -0
- package/lib/docs/index.d.ts +1 -0
- package/lib/docs/index.js +1 -0
- package/lib/esbuild/bundle.d.ts +1 -1
- package/lib/fs/file.d.ts +30 -3
- package/lib/fs/file.js +63 -3
- package/lib/fs/folder.d.ts +3 -3
- package/lib/fs/folder.js +2 -1
- package/lib/github/secret.d.ts +1 -1
- package/lib/resource.d.ts +2 -2
- package/lib/resource.js +4 -0
- package/lib/scope.d.ts +1 -1
- package/lib/scope.js +6 -6
- package/lib/shadcn/component.d.ts +1 -1
- package/lib/state.d.ts +1 -1
- package/lib/state.js +8 -1
- package/lib/stripe/price.d.ts +1 -1
- package/lib/stripe/product.d.ts +1 -1
- package/lib/stripe/webhook.d.ts +1 -1
- package/lib/util/serde.d.ts +3 -1
- package/lib/util/serde.js +6 -4
- package/lib/vite/vite.d.ts +1 -1
- package/lib/vitepress/dependencies.d.ts +16 -0
- package/lib/vitepress/dependencies.js +35 -0
- package/lib/vitepress/home-page.d.ts +133 -0
- package/lib/vitepress/home-page.js +11 -0
- package/lib/vitepress/index.d.ts +2 -0
- package/lib/vitepress/index.js +2 -0
- package/lib/vitepress/vitepress.d.ts +60 -0
- package/lib/vitepress/vitepress.js +260 -0
- package/package.json +16 -9
- package/src/alchemy.ts +105 -13
- package/src/apply.ts +25 -13
- package/src/context.ts +30 -10
- package/src/destroy.ts +10 -7
- package/src/docs/document.ts +225 -0
- package/src/docs/index.ts +1 -0
- package/src/fs/file.ts +114 -6
- package/src/fs/folder.ts +2 -1
- package/src/resource.ts +7 -2
- package/src/scope.ts +9 -9
- package/src/state.ts +9 -2
- package/src/util/serde.ts +13 -4
- package/src/vitepress/dependencies.ts +68 -0
- package/src/vitepress/home-page.ts +166 -0
- package/src/vitepress/index.md +75 -0
- package/src/vitepress/index.ts +2 -0
- package/src/vitepress/vitepress.ts +361 -0
- package/lib/project/vite.d.ts +0 -90
- package/lib/project/vite.js +0 -228
- package/src/project/vite.ts +0 -406
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
|
|
2
|
+
import { generateText } from "ai";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { Context } from "../context";
|
|
6
|
+
import { Resource } from "../resource";
|
|
7
|
+
import type { Secret } from "../secret";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Base properties for a Document
|
|
11
|
+
*/
|
|
12
|
+
interface BaseDocumentProps {
|
|
13
|
+
/**
|
|
14
|
+
* Path to the markdown document
|
|
15
|
+
*/
|
|
16
|
+
path: string;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Base URL for the OpenAI API
|
|
20
|
+
* @default 'https://api.openai.com/v1'
|
|
21
|
+
*/
|
|
22
|
+
baseURL?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Properties for creating a document with static content
|
|
27
|
+
*/
|
|
28
|
+
interface StaticDocumentProps extends BaseDocumentProps {
|
|
29
|
+
/**
|
|
30
|
+
* Static content of the document
|
|
31
|
+
*/
|
|
32
|
+
content: string;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Prompt for generating content
|
|
36
|
+
* @internal This is marked as never to ensure content and prompt are mutually exclusive
|
|
37
|
+
*/
|
|
38
|
+
prompt?: never;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* OpenAI API key
|
|
42
|
+
* @internal This is marked as never since it's not needed for static content
|
|
43
|
+
*/
|
|
44
|
+
apiKey?: never;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Model configuration
|
|
48
|
+
* @internal This is marked as never since it's not needed for static content
|
|
49
|
+
*/
|
|
50
|
+
model?: never;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Context files
|
|
54
|
+
* @internal This is marked as never since it's not needed for static content
|
|
55
|
+
*/
|
|
56
|
+
context?: never;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Properties for creating a document with AI-generated content
|
|
61
|
+
*/
|
|
62
|
+
interface GeneratedDocumentProps extends BaseDocumentProps {
|
|
63
|
+
/**
|
|
64
|
+
* Static content of the document
|
|
65
|
+
* @internal This is marked as never to ensure content and prompt are mutually exclusive
|
|
66
|
+
*/
|
|
67
|
+
content?: never;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Prompt for generating content
|
|
71
|
+
* Can be either a single string or an array of strings that will be joined with newlines
|
|
72
|
+
*/
|
|
73
|
+
prompt: string | string[];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Optional context files to include in the prompt
|
|
77
|
+
* Key is the relative path to the file
|
|
78
|
+
* Value can be either the content string or another Document resource
|
|
79
|
+
*/
|
|
80
|
+
context?: {
|
|
81
|
+
[relativePath: string]:
|
|
82
|
+
| string
|
|
83
|
+
| Document
|
|
84
|
+
| {
|
|
85
|
+
path: string;
|
|
86
|
+
content: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* OpenAI API key to use for generating content
|
|
92
|
+
* If not provided, will use OPENAI_API_KEY environment variable
|
|
93
|
+
*/
|
|
94
|
+
apiKey?: Secret;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Model configuration
|
|
98
|
+
*/
|
|
99
|
+
model?: {
|
|
100
|
+
/**
|
|
101
|
+
* Model ID to use
|
|
102
|
+
* @default 'gpt-4o'
|
|
103
|
+
*/
|
|
104
|
+
id?: string;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Model provider name
|
|
108
|
+
* @default 'openai'
|
|
109
|
+
*/
|
|
110
|
+
provider?: string;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Model-specific options
|
|
114
|
+
*/
|
|
115
|
+
options?: Record<string, unknown>;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Properties for creating or updating a Document
|
|
121
|
+
* Must provide either static content or a prompt for AI generation
|
|
122
|
+
*/
|
|
123
|
+
export type DocumentProps = StaticDocumentProps | GeneratedDocumentProps;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A markdown document that can be created, updated, and deleted
|
|
127
|
+
*/
|
|
128
|
+
export interface Document
|
|
129
|
+
extends Omit<DocumentProps, "content" | "prompt" | "context">,
|
|
130
|
+
Resource<"docs::Document"> {
|
|
131
|
+
/**
|
|
132
|
+
* Content of the document
|
|
133
|
+
*/
|
|
134
|
+
content: string;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Time at which the document was created
|
|
138
|
+
*/
|
|
139
|
+
createdAt: number;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Time at which the document was last updated
|
|
143
|
+
*/
|
|
144
|
+
updatedAt: number;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resource for managing markdown documents using the Vercel AI SDK
|
|
149
|
+
*/
|
|
150
|
+
export const Document = Resource(
|
|
151
|
+
"docs::Document",
|
|
152
|
+
async function (
|
|
153
|
+
this: Context<Document, DocumentProps>,
|
|
154
|
+
id: string,
|
|
155
|
+
props: DocumentProps,
|
|
156
|
+
): Promise<Document> {
|
|
157
|
+
// Ensure directory exists
|
|
158
|
+
await fs.mkdir(path.dirname(props.path), { recursive: true });
|
|
159
|
+
|
|
160
|
+
if (this.phase === "delete") {
|
|
161
|
+
try {
|
|
162
|
+
await fs.unlink(props.path);
|
|
163
|
+
} catch (error: any) {
|
|
164
|
+
// Ignore if file doesn't exist
|
|
165
|
+
if (error.code !== "ENOENT") {
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return this.destroy();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let content: string;
|
|
173
|
+
|
|
174
|
+
console.log("Generating content for", props.path);
|
|
175
|
+
|
|
176
|
+
// Generate content if prompt is provided
|
|
177
|
+
if ("prompt" in props) {
|
|
178
|
+
// Get API key from props or environment
|
|
179
|
+
const apiKey = props.apiKey?.unencrypted || process.env.OPENAI_API_KEY;
|
|
180
|
+
if (!apiKey) {
|
|
181
|
+
throw new Error("OpenAI API key is required for content generation");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Initialize OpenAI compatible provider
|
|
185
|
+
const provider = createOpenAICompatible({
|
|
186
|
+
name: props.model?.provider || "openai",
|
|
187
|
+
apiKey,
|
|
188
|
+
baseURL: props.baseURL || "https://api.openai.com/v1",
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Build the full prompt with context if provided
|
|
192
|
+
const fullPrompt = Array.isArray(props.prompt)
|
|
193
|
+
? props.prompt.join("\n")
|
|
194
|
+
: props.prompt!;
|
|
195
|
+
|
|
196
|
+
// console.log(fullPrompt);
|
|
197
|
+
|
|
198
|
+
const { text } = await generateText({
|
|
199
|
+
model: provider(props.model?.id || "gpt-4o"),
|
|
200
|
+
prompt: fullPrompt,
|
|
201
|
+
...(props.model?.options || {}),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
content = text;
|
|
205
|
+
} else {
|
|
206
|
+
// Use static content
|
|
207
|
+
content = props.content;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Write content to file
|
|
211
|
+
await fs.writeFile(props.path, content);
|
|
212
|
+
|
|
213
|
+
// Get file stats for timestamps
|
|
214
|
+
const stats = await fs.stat(props.path);
|
|
215
|
+
|
|
216
|
+
// Return the resource
|
|
217
|
+
return this({
|
|
218
|
+
path: props.path,
|
|
219
|
+
baseURL: props.baseURL,
|
|
220
|
+
content,
|
|
221
|
+
createdAt: stats.birthtimeMs,
|
|
222
|
+
updatedAt: stats.mtimeMs,
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./document";
|
package/src/fs/file.ts
CHANGED
|
@@ -4,6 +4,69 @@ import type { Context } from "../context";
|
|
|
4
4
|
import { Resource } from "../resource";
|
|
5
5
|
import { ignore } from "../util/ignore";
|
|
6
6
|
|
|
7
|
+
import { alchemy } from "../alchemy";
|
|
8
|
+
|
|
9
|
+
declare module "../alchemy" {
|
|
10
|
+
interface Alchemy {
|
|
11
|
+
file(path: string): Promise<FileRef>;
|
|
12
|
+
files(paths: string[]): Promise<FileCollection>;
|
|
13
|
+
files(path: string, ...paths: string[]): Promise<FileCollection>;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type FileRef = {
|
|
18
|
+
kind: "fs::FileRef";
|
|
19
|
+
path: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function isFileRef(value: unknown): value is FileRef {
|
|
23
|
+
return (
|
|
24
|
+
typeof value === "object" &&
|
|
25
|
+
value !== null &&
|
|
26
|
+
"kind" in value &&
|
|
27
|
+
value.kind === "fs::FileRef"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type FileCollection = {
|
|
32
|
+
type: "fs::FileCollection";
|
|
33
|
+
files: {
|
|
34
|
+
[relativePath: string]: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function isFileCollection(value: unknown): value is FileCollection {
|
|
39
|
+
return (
|
|
40
|
+
typeof value === "object" &&
|
|
41
|
+
value !== null &&
|
|
42
|
+
"type" in value &&
|
|
43
|
+
value.type === "fs::FileCollection"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
alchemy.file = async (path: string) => ({
|
|
48
|
+
kind: "fs::FileRef",
|
|
49
|
+
path,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
alchemy.files = async (
|
|
53
|
+
...args: [paths: string[]] | [...paths: string[]]
|
|
54
|
+
): Promise<FileCollection> => {
|
|
55
|
+
const paths: string[] =
|
|
56
|
+
typeof args[0] === "string" ? (args as string[]) : args[0];
|
|
57
|
+
return {
|
|
58
|
+
type: "fs::FileCollection",
|
|
59
|
+
files: Object.fromEntries(
|
|
60
|
+
await Promise.all(
|
|
61
|
+
paths.map(async (path) => [
|
|
62
|
+
path,
|
|
63
|
+
await fs.promises.readFile(path, "utf-8"),
|
|
64
|
+
]),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
|
|
7
70
|
export interface File extends Resource<"fs::File"> {
|
|
8
71
|
path: string;
|
|
9
72
|
content: string;
|
|
@@ -14,14 +77,12 @@ export const File = Resource(
|
|
|
14
77
|
async function (
|
|
15
78
|
this: Context<File>,
|
|
16
79
|
id: string,
|
|
17
|
-
{
|
|
18
|
-
path: filePath,
|
|
19
|
-
content,
|
|
20
|
-
}: {
|
|
80
|
+
props: {
|
|
21
81
|
path: string;
|
|
22
82
|
content: string;
|
|
23
83
|
},
|
|
24
84
|
): Promise<File> {
|
|
85
|
+
const filePath = props?.path ?? id;
|
|
25
86
|
if (this.phase === "delete") {
|
|
26
87
|
await ignore("ENOENT", async () => fs.promises.unlink(filePath));
|
|
27
88
|
return this.destroy();
|
|
@@ -29,11 +90,58 @@ export const File = Resource(
|
|
|
29
90
|
await fs.promises.mkdir(path.dirname(filePath), {
|
|
30
91
|
recursive: true,
|
|
31
92
|
});
|
|
32
|
-
await fs.promises.writeFile(filePath, content);
|
|
93
|
+
await fs.promises.writeFile(filePath, props.content);
|
|
33
94
|
}
|
|
34
95
|
return this({
|
|
35
96
|
path: filePath,
|
|
36
|
-
content,
|
|
97
|
+
content: props.content,
|
|
37
98
|
});
|
|
38
99
|
},
|
|
39
100
|
);
|
|
101
|
+
|
|
102
|
+
export type JsonFile = File;
|
|
103
|
+
|
|
104
|
+
export function JsonFile(id: string, content: any): Promise<JsonFile> {
|
|
105
|
+
return File(id, {
|
|
106
|
+
path: id,
|
|
107
|
+
content: JSON.stringify(content, null, 2),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type TextFile = File;
|
|
112
|
+
|
|
113
|
+
export function TextFile(id: string, content: string): Promise<TextFile> {
|
|
114
|
+
return File(id, {
|
|
115
|
+
path: id,
|
|
116
|
+
content,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type YamlFile = File;
|
|
121
|
+
|
|
122
|
+
export async function YamlFile(id: string, content: any): Promise<YamlFile> {
|
|
123
|
+
const yaml = await import("yaml");
|
|
124
|
+
return File(id, {
|
|
125
|
+
path: id,
|
|
126
|
+
content: yaml.stringify(content),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type TypeScriptFile = File;
|
|
131
|
+
|
|
132
|
+
export async function TypeScriptFile(
|
|
133
|
+
id: string,
|
|
134
|
+
content: string,
|
|
135
|
+
): Promise<TypeScriptFile> {
|
|
136
|
+
const prettier = await import("prettier");
|
|
137
|
+
return File(id, {
|
|
138
|
+
path: id,
|
|
139
|
+
content: await prettier.format(content, {
|
|
140
|
+
parser: "typescript",
|
|
141
|
+
editor: {
|
|
142
|
+
tabWidth: 2,
|
|
143
|
+
indentWidth: 2,
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
}
|
package/src/fs/folder.ts
CHANGED
|
@@ -12,8 +12,9 @@ export const Folder = Resource(
|
|
|
12
12
|
async function (
|
|
13
13
|
this: Context<Folder>,
|
|
14
14
|
id: string,
|
|
15
|
-
|
|
15
|
+
props?: { path: string },
|
|
16
16
|
): Promise<Folder> {
|
|
17
|
+
const dirPath = props?.path ?? id;
|
|
17
18
|
if (this.phase === "delete") {
|
|
18
19
|
// we just do a best effort attempt
|
|
19
20
|
await ignore(["ENOENT", "ENOTEMPTY"], async () =>
|
package/src/resource.ts
CHANGED
|
@@ -67,7 +67,7 @@ type IsClass = {
|
|
|
67
67
|
};
|
|
68
68
|
|
|
69
69
|
type ResourceLifecycleHandler = (
|
|
70
|
-
this: Context<any>,
|
|
70
|
+
this: Context<any, any>,
|
|
71
71
|
id: string,
|
|
72
72
|
props: any,
|
|
73
73
|
) => Promise<Resource<string>>;
|
|
@@ -75,7 +75,7 @@ type ResourceLifecycleHandler = (
|
|
|
75
75
|
// see: https://x.com/samgoodwin89/status/1904640134097887653
|
|
76
76
|
type Handler<F extends (...args: any[]) => any> =
|
|
77
77
|
| F
|
|
78
|
-
| (((this: any, id: string, props
|
|
78
|
+
| (((this: any, id: string, props?: Parameters<F>[1]) => never) & IsClass);
|
|
79
79
|
|
|
80
80
|
export function Resource<
|
|
81
81
|
const Type extends string,
|
|
@@ -104,6 +104,11 @@ export function Resource<
|
|
|
104
104
|
): Promise<Resource<string>> => {
|
|
105
105
|
const scope = _Scope.current;
|
|
106
106
|
|
|
107
|
+
if (resourceID.includes(":")) {
|
|
108
|
+
// we want to use : as an internal separator for resources
|
|
109
|
+
throw new Error(`ID cannot include colons: ${resourceID}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
107
112
|
if (scope.resources.has(resourceID)) {
|
|
108
113
|
// TODO(sam): do we want to throw?
|
|
109
114
|
// it's kind of awesome that you can re-create a resource and call apply
|
package/src/scope.ts
CHANGED
|
@@ -98,6 +98,14 @@ export class Scope {
|
|
|
98
98
|
return [...this.chain, resourceID].join("/");
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
public async run<T>(fn: (scope: Scope) => Promise<T>): Promise<T> {
|
|
102
|
+
return scopeStorage.run(this, () => fn(this));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
[Symbol.asyncDispose]() {
|
|
106
|
+
return this.finalize();
|
|
107
|
+
}
|
|
108
|
+
|
|
101
109
|
public async finalize() {
|
|
102
110
|
if (!this.isErrored) {
|
|
103
111
|
// TODO: need to detect if it is in error
|
|
@@ -118,18 +126,10 @@ export class Scope {
|
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
public async run<T>(fn: (scope: Scope) => Promise<T>): Promise<T> {
|
|
122
|
-
return scopeStorage.run(this, () => fn(this));
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
[Symbol.asyncDispose]() {
|
|
126
|
-
return this.finalize();
|
|
127
|
-
}
|
|
128
|
-
|
|
129
129
|
/**
|
|
130
130
|
* Returns a string representation of the scope.
|
|
131
131
|
*/
|
|
132
|
-
toString() {
|
|
132
|
+
public toString() {
|
|
133
133
|
return `Scope(
|
|
134
134
|
chain=${this.chain.join("/")},
|
|
135
135
|
resources=[${Array.from(this.resources.values())
|
package/src/state.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { deserialize, serialize } from "./util/serde";
|
|
|
7
7
|
|
|
8
8
|
export interface State<
|
|
9
9
|
Kind extends string = string,
|
|
10
|
-
Props extends ResourceProps = ResourceProps,
|
|
10
|
+
Props extends ResourceProps | undefined = ResourceProps | undefined,
|
|
11
11
|
Out extends Resource = Resource,
|
|
12
12
|
> {
|
|
13
13
|
status:
|
|
@@ -74,7 +74,8 @@ export class FileSystemStateStore implements StateStore {
|
|
|
74
74
|
});
|
|
75
75
|
return files
|
|
76
76
|
.filter((dirent) => dirent.isFile() && dirent.name.endsWith(".json"))
|
|
77
|
-
.map((dirent) => dirent.name.replace(/\.json$/, ""))
|
|
77
|
+
.map((dirent) => dirent.name.replace(/\.json$/, ""))
|
|
78
|
+
.map((key) => key.replaceAll(":", "/"));
|
|
78
79
|
} catch (error: any) {
|
|
79
80
|
if (error.code === "ENOENT") {
|
|
80
81
|
return [];
|
|
@@ -138,6 +139,12 @@ export class FileSystemStateStore implements StateStore {
|
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
private async getPath(key: string): Promise<string> {
|
|
142
|
+
if (key.includes(":")) {
|
|
143
|
+
throw new Error(`ID cannot include colons: ${key}`);
|
|
144
|
+
}
|
|
145
|
+
if (key.includes("/")) {
|
|
146
|
+
key = key.replaceAll("/", ":");
|
|
147
|
+
}
|
|
141
148
|
const file = path.join(this.dir, `${key}.json`);
|
|
142
149
|
const dir = path.dirname(file);
|
|
143
150
|
await fs.promises.mkdir(dir, { recursive: true });
|
package/src/util/serde.ts
CHANGED
|
@@ -2,15 +2,24 @@ import { Scope } from "../scope";
|
|
|
2
2
|
import { Secret } from "../secret";
|
|
3
3
|
import { decryptWithKey, encryptWithKey } from "./encrypt";
|
|
4
4
|
|
|
5
|
-
export async function serialize(
|
|
5
|
+
export async function serialize(
|
|
6
|
+
scope: Scope,
|
|
7
|
+
value: any,
|
|
8
|
+
options?: {
|
|
9
|
+
encrypt?: boolean;
|
|
10
|
+
},
|
|
11
|
+
): Promise<any> {
|
|
6
12
|
if (Array.isArray(value)) {
|
|
7
|
-
return Promise.all(value.map((value) => serialize(scope, value)));
|
|
13
|
+
return Promise.all(value.map((value) => serialize(scope, value, options)));
|
|
8
14
|
} else if (value instanceof Secret) {
|
|
9
15
|
if (!scope.password) {
|
|
10
16
|
throw new Error("Cannot serialize secret without password");
|
|
11
17
|
}
|
|
12
18
|
return {
|
|
13
|
-
"@secret":
|
|
19
|
+
"@secret":
|
|
20
|
+
options?.encrypt !== false
|
|
21
|
+
? await encryptWithKey(value.unencrypted, scope.password)
|
|
22
|
+
: value.unencrypted,
|
|
14
23
|
};
|
|
15
24
|
} else if (value instanceof Scope) {
|
|
16
25
|
return undefined;
|
|
@@ -19,7 +28,7 @@ export async function serialize(scope: Scope, value: any): Promise<any> {
|
|
|
19
28
|
await Promise.all(
|
|
20
29
|
Object.entries(value).map(async ([key, value]) => [
|
|
21
30
|
key,
|
|
22
|
-
await serialize(scope, value),
|
|
31
|
+
await serialize(scope, value, options),
|
|
23
32
|
]),
|
|
24
33
|
),
|
|
25
34
|
);
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
export type InstallDependencies = Resource<"project::InstallDependencies">;
|
|
9
|
+
|
|
10
|
+
export const InstallDependencies = Resource(
|
|
11
|
+
"project::InstallDependencies",
|
|
12
|
+
{
|
|
13
|
+
alwaysUpdate: true,
|
|
14
|
+
},
|
|
15
|
+
async function (
|
|
16
|
+
this: Context<InstallDependencies>,
|
|
17
|
+
id: string,
|
|
18
|
+
{
|
|
19
|
+
cwd,
|
|
20
|
+
dependencies,
|
|
21
|
+
devDependencies,
|
|
22
|
+
}: {
|
|
23
|
+
cwd: string;
|
|
24
|
+
dependencies?: Record<string, string>;
|
|
25
|
+
devDependencies?: Record<string, string>;
|
|
26
|
+
},
|
|
27
|
+
) {
|
|
28
|
+
if (this.phase === "delete") {
|
|
29
|
+
return this.destroy();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
await installDependencies(cwd, dependencies);
|
|
33
|
+
await installDependencies(cwd, devDependencies, "-D");
|
|
34
|
+
await execAsync("bun install", { cwd });
|
|
35
|
+
|
|
36
|
+
return this({});
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function isInstallableVersion(version: string) {
|
|
41
|
+
// TODO: file:// ?
|
|
42
|
+
return !version.startsWith("workspace:");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function fixedDependencies(dependencies?: Record<string, string>) {
|
|
46
|
+
if (!dependencies) return {};
|
|
47
|
+
return Object.fromEntries(
|
|
48
|
+
Object.entries(dependencies).filter(
|
|
49
|
+
([, value]) => !isInstallableVersion(value),
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function installDependencies(
|
|
55
|
+
cwd: string,
|
|
56
|
+
dependencies: Record<string, string> | undefined,
|
|
57
|
+
...args: string[]
|
|
58
|
+
) {
|
|
59
|
+
if (!dependencies) return;
|
|
60
|
+
const deps = Object.entries(dependencies)
|
|
61
|
+
.filter(([, value]) => isInstallableVersion(value))
|
|
62
|
+
.map(([pkg, version]) => `${pkg}@${version}`)
|
|
63
|
+
.join(" ");
|
|
64
|
+
|
|
65
|
+
const cmd = `bun add ${args.join(" ")} ${deps}`;
|
|
66
|
+
console.log(cmd);
|
|
67
|
+
await execAsync(cmd, { cwd });
|
|
68
|
+
}
|