alchemy 0.1.2 → 0.1.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/README.md +8 -39
- package/package.json +12 -8
- package/src/$.ts +36 -0
- package/src/agent/ai.ts +215 -0
- package/src/{components/agent → agent}/anthropic.ts +6 -2
- package/src/agent/dependencies.ts +62 -0
- package/src/agent/file-context.ts +8 -0
- package/src/agent/index.ts +9 -0
- package/src/agent/model.ts +39 -0
- package/src/agent/module.ts +418 -0
- package/src/{components/agent → agent}/openai.ts +6 -0
- package/src/agent/program.md +38 -0
- package/src/agent/program.ts +37 -0
- package/src/agent/prompts.ts +20 -0
- package/src/alchemize.ts +45 -14
- package/src/apply.ts +25 -10
- package/src/{components/aws → aws}/bucket.ts +2 -2
- package/src/{components/aws → aws}/function.ts +2 -2
- package/src/{components/aws → aws}/policy.ts +2 -2
- package/src/{components/aws → aws}/queue.ts +1 -1
- package/src/{components/aws → aws}/role.ts +2 -2
- package/src/{components/aws → aws}/table.ts +2 -2
- package/src/destroy.ts +36 -12
- package/src/{components/esbuild.ts → esbuild.ts} +1 -1
- package/src/{components/fs.ts → fs.ts} +20 -18
- package/src/global.ts +7 -14
- package/src/index.ts +11 -2
- package/src/input.ts +1 -1
- package/src/markdown/design.ts +125 -0
- package/src/markdown/extract.ts +9 -0
- package/src/markdown/index.ts +3 -0
- package/src/{components/agent → markdown}/requirements.ts +53 -29
- package/src/output.ts +6 -0
- package/src/resource.ts +208 -75
- package/src/scope.ts +56 -0
- package/src/slug.ts +3 -0
- package/src/state.ts +66 -38
- package/src/typescript/check-omission.ts +47 -0
- package/src/typescript/debug-type-errors.ts +42 -0
- package/src/typescript/extract.ts +9 -0
- package/src/{components/agent → typescript}/index.ts +3 -1
- package/src/typescript/install-packages.ts +50 -0
- package/src/{components/agent → typescript}/package.ts +44 -27
- package/src/typescript/repair-omissions.ts +31 -0
- package/src/typescript/repair.ts +45 -0
- package/src/{components/agent → typescript}/tsconfig.ts +37 -17
- package/src/typescript/typescript.ts +217 -0
- package/src/typescript/validate.ts +73 -0
- package/test/scope.test.ts +67 -0
- package/src/components/agent/agent.ts +0 -45
- package/src/components/agent/model.ts +0 -22
- package/src/components/agent/typescript.ts +0 -177
- /package/src/{components/agent → agent}/scrape.ts +0 -0
- /package/src/{components/aws → aws}/index.ts +0 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import type { CoreMessage, LanguageModel } from "ai";
|
|
2
|
+
import { createPatch } from "diff";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { Folder } from "../fs";
|
|
6
|
+
import type { Input } from "../input";
|
|
7
|
+
import { Design, type DesignInput } from "../markdown/design";
|
|
8
|
+
import { Requirements, type RequirementsInput } from "../markdown/requirements";
|
|
9
|
+
import {
|
|
10
|
+
type Context,
|
|
11
|
+
type CreateContext,
|
|
12
|
+
Resource,
|
|
13
|
+
type UpdateContext,
|
|
14
|
+
} from "../resource";
|
|
15
|
+
import { slugify } from "../slug";
|
|
16
|
+
import { TypeScriptFile, type TypeScriptFileInput } from "../typescript";
|
|
17
|
+
import type { PackageJson } from "../typescript/package";
|
|
18
|
+
import type { TypeScriptConfig } from "../typescript/tsconfig";
|
|
19
|
+
import { generateObject } from "./ai";
|
|
20
|
+
import { dependenciesAsMessages } from "./dependencies";
|
|
21
|
+
import type { FileContext } from "./file-context";
|
|
22
|
+
import { type ModelId, resolveModel } from "./model";
|
|
23
|
+
import { Prompts } from "./prompts";
|
|
24
|
+
|
|
25
|
+
type ProjectFileType = z.infer<typeof ProjectFileType>;
|
|
26
|
+
const ProjectFileType = z.enum([
|
|
27
|
+
"folder",
|
|
28
|
+
"design",
|
|
29
|
+
"typescript",
|
|
30
|
+
// "package.json",
|
|
31
|
+
// "tsconfig.json",
|
|
32
|
+
"css",
|
|
33
|
+
"scss",
|
|
34
|
+
"javascript",
|
|
35
|
+
"html",
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const upstream = z
|
|
39
|
+
.array(z.string())
|
|
40
|
+
.describe(
|
|
41
|
+
[
|
|
42
|
+
"A list of all this file's upstream dependencies (files and folders).",
|
|
43
|
+
"These files and folders must exist prior to creating or updating 'this' file.",
|
|
44
|
+
"'this' meaning the file described by the key in the object.",
|
|
45
|
+
"If the plan includes a type: folder to be created, and a file is contained",
|
|
46
|
+
"within that folder, then it must also be listed in the upstream dependencies.",
|
|
47
|
+
"Don't include files as upstream dependencies if that file's content is not relevant to this file.",
|
|
48
|
+
].join(" "),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
interface CreateFile extends z.infer<typeof CreateFile> {}
|
|
52
|
+
const CreateFile = z.object({
|
|
53
|
+
type: ProjectFileType,
|
|
54
|
+
upstream,
|
|
55
|
+
operation: z.enum(["create"]),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
interface UpdateFile extends z.infer<typeof UpdateFile> {}
|
|
59
|
+
const UpdateFile = z.object({
|
|
60
|
+
type: ProjectFileType,
|
|
61
|
+
upstream,
|
|
62
|
+
operation: z
|
|
63
|
+
.enum(["create", "update"])
|
|
64
|
+
.describe(
|
|
65
|
+
[
|
|
66
|
+
"The operation to perform on this fle (one of: create, update, delete, noop).",
|
|
67
|
+
"If the document describes a new file that needs to be created, the operation is 'create'.",
|
|
68
|
+
"If the document's content has changed such that a file that was previously created must be updated, the operation is 'update'.",
|
|
69
|
+
"If the document's content has changed, but not in a way that is relevant to this file, the operation is 'noop'.",
|
|
70
|
+
"If a file that was previously created is no longer mentioned in the document, the operation is 'delete'.",
|
|
71
|
+
].join(" "),
|
|
72
|
+
),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export type CreatePlan = z.infer<typeof CreatePlan>;
|
|
76
|
+
export const CreatePlan = z.object({
|
|
77
|
+
files: z.record(CreateFile),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export type UpdatePlan = z.infer<typeof UpdatePlan>;
|
|
81
|
+
export const UpdatePlan = z.object({
|
|
82
|
+
files: z.record(z.union([UpdateFile, z.enum(["delete", "noop"])])),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
type OutputFile =
|
|
86
|
+
| TypeScriptFile
|
|
87
|
+
| PackageJson
|
|
88
|
+
| TypeScriptConfig
|
|
89
|
+
| Folder
|
|
90
|
+
| Module;
|
|
91
|
+
|
|
92
|
+
export interface ModuleOutput {
|
|
93
|
+
path: string;
|
|
94
|
+
content: string;
|
|
95
|
+
plan: CreatePlan | UpdatePlan;
|
|
96
|
+
files: Record<string, OutputFile>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ModuleInput {
|
|
100
|
+
path: string;
|
|
101
|
+
content: string;
|
|
102
|
+
model?: ModelId;
|
|
103
|
+
dependencies?: FileContext[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export class Module extends Resource(
|
|
107
|
+
"Module",
|
|
108
|
+
async (
|
|
109
|
+
ctx: Context<ModuleOutput>,
|
|
110
|
+
props: ModuleInput,
|
|
111
|
+
): Promise<ModuleOutput | void> => {
|
|
112
|
+
if (ctx.event === "delete") {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const model = await resolveModel(props.model ?? "gpt-4o");
|
|
117
|
+
|
|
118
|
+
const plan: CreatePlan | UpdatePlan =
|
|
119
|
+
ctx.event === "create"
|
|
120
|
+
? await createPlan(ctx, props, model)
|
|
121
|
+
: await updatePlan(ctx, props, model);
|
|
122
|
+
|
|
123
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
124
|
+
|
|
125
|
+
const dependencies = Object.fromEntries(
|
|
126
|
+
Object.entries(plan.files).flatMap(([path, file]) =>
|
|
127
|
+
file === "delete" || file === "noop" ? [] : [[path, file.upstream]],
|
|
128
|
+
),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const files: {
|
|
132
|
+
[path: string]: Input<FileContext>;
|
|
133
|
+
} = {};
|
|
134
|
+
|
|
135
|
+
for (const filePath of topoSort(dependencies)) {
|
|
136
|
+
const filePlan = plan.files[filePath];
|
|
137
|
+
if (filePlan !== "delete") {
|
|
138
|
+
const file = await createFile(
|
|
139
|
+
filePath,
|
|
140
|
+
filePlan === "noop"
|
|
141
|
+
? "noop"
|
|
142
|
+
: (filePlan.operation as "create" | "update"),
|
|
143
|
+
filePlan === "noop" ? null : filePlan,
|
|
144
|
+
dependencies[filePath]?.map((dep) => files[dep]),
|
|
145
|
+
);
|
|
146
|
+
if (file) {
|
|
147
|
+
files[filePath] = file;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
path: props.path,
|
|
154
|
+
plan,
|
|
155
|
+
content: props.content,
|
|
156
|
+
// @ts-ignore - TODO: fix this
|
|
157
|
+
files,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
async function createFile(
|
|
161
|
+
filePath: string,
|
|
162
|
+
op: "create" | "update" | "noop",
|
|
163
|
+
filePlan: CreateFile | UpdateFile | null,
|
|
164
|
+
dependencies: Input<FileContext>[],
|
|
165
|
+
): Promise<Input<FileContext> | undefined> {
|
|
166
|
+
const slug = slugify(filePath);
|
|
167
|
+
|
|
168
|
+
// cache inputs for no-ops
|
|
169
|
+
async function inputs<T>(
|
|
170
|
+
key: string,
|
|
171
|
+
input: Input<T>,
|
|
172
|
+
): Promise<Input<T>> {
|
|
173
|
+
return (op === "noop" ? await ctx.get<T>(key) : input) ?? input;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (filePlan === null) {
|
|
177
|
+
// restore the file plan from the cache for no-ops
|
|
178
|
+
filePlan = (await ctx.get<CreateFile | UpdateFile>(filePath))!;
|
|
179
|
+
} else {
|
|
180
|
+
await ctx.set(filePath, filePlan);
|
|
181
|
+
}
|
|
182
|
+
if (filePlan.type === "design") {
|
|
183
|
+
const design = new Design(
|
|
184
|
+
`${slug}-design`,
|
|
185
|
+
await inputs<DesignInput>(`${slug}-design`, {
|
|
186
|
+
path: filePath,
|
|
187
|
+
prompt: [
|
|
188
|
+
"Context:",
|
|
189
|
+
props.content,
|
|
190
|
+
"",
|
|
191
|
+
`Produce the design: ${filePath}`,
|
|
192
|
+
].join("\n"),
|
|
193
|
+
dependencies,
|
|
194
|
+
model: "o3-mini",
|
|
195
|
+
}),
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const module = new Module(
|
|
199
|
+
slug,
|
|
200
|
+
await inputs<ModuleInput>(slug, {
|
|
201
|
+
model: props.model,
|
|
202
|
+
path: filePath,
|
|
203
|
+
content: design.content,
|
|
204
|
+
dependencies,
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return module;
|
|
209
|
+
} else if (filePlan.type === "typescript") {
|
|
210
|
+
const requirementsSlug = slug.replace(".ts", ".md");
|
|
211
|
+
const requirements = new Requirements(
|
|
212
|
+
requirementsSlug,
|
|
213
|
+
await inputs<RequirementsInput>(requirementsSlug, {
|
|
214
|
+
// file: file.path.replace(".ts", ".md").replace("src/", "design/"),
|
|
215
|
+
requirements: [
|
|
216
|
+
"Context:",
|
|
217
|
+
props.content,
|
|
218
|
+
"Define the low-level requirements for this TypeScript file:",
|
|
219
|
+
filePath,
|
|
220
|
+
],
|
|
221
|
+
dependencies,
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
return new TypeScriptFile(
|
|
225
|
+
slug,
|
|
226
|
+
await inputs<TypeScriptFileInput>(slug, {
|
|
227
|
+
path: filePath,
|
|
228
|
+
requirements: requirements.content,
|
|
229
|
+
dependencies,
|
|
230
|
+
}),
|
|
231
|
+
);
|
|
232
|
+
// } else if (filePlan.type === "package.json") {
|
|
233
|
+
// return new PackageJson(
|
|
234
|
+
// slugify(filePath),
|
|
235
|
+
// await inputs<PackageJsonInput>(slug, {
|
|
236
|
+
// path: filePath,
|
|
237
|
+
// name: "todo",
|
|
238
|
+
// requirements: [
|
|
239
|
+
// "Context:",
|
|
240
|
+
// props.content,
|
|
241
|
+
// "Create the package.json:",
|
|
242
|
+
// filePath,
|
|
243
|
+
// ],
|
|
244
|
+
// dependencies,
|
|
245
|
+
// }),
|
|
246
|
+
// );
|
|
247
|
+
// } else if (filePlan.type === "tsconfig.json") {
|
|
248
|
+
// return new TypeScriptConfig(
|
|
249
|
+
// slugify(filePath),
|
|
250
|
+
// await inputs<TypeScriptConfigInput>(slug, {
|
|
251
|
+
// path: filePath,
|
|
252
|
+
// requirements: [props.content],
|
|
253
|
+
// dependencies,
|
|
254
|
+
// }),
|
|
255
|
+
// );
|
|
256
|
+
} else if (filePlan.type === "folder") {
|
|
257
|
+
return new Folder(slug, filePath, ...dependencies.map((d) => d.path));
|
|
258
|
+
} else {
|
|
259
|
+
console.error(`Unknown file type: ${filePlan.type}`);
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
) {}
|
|
265
|
+
|
|
266
|
+
async function createPlan(
|
|
267
|
+
ctx: CreateContext,
|
|
268
|
+
props: ModuleInput,
|
|
269
|
+
model: LanguageModel,
|
|
270
|
+
): Promise<CreatePlan> {
|
|
271
|
+
const result = await generateObject({
|
|
272
|
+
model,
|
|
273
|
+
schema: CreatePlan,
|
|
274
|
+
temperature: 0.1,
|
|
275
|
+
messages: [
|
|
276
|
+
{
|
|
277
|
+
role: "system",
|
|
278
|
+
content: Prompts.program,
|
|
279
|
+
},
|
|
280
|
+
...excludeDependenciesFromPlan(props.dependencies),
|
|
281
|
+
{
|
|
282
|
+
role: "user",
|
|
283
|
+
content: [
|
|
284
|
+
"Derive the execution plan for this document:",
|
|
285
|
+
"```md",
|
|
286
|
+
props.content,
|
|
287
|
+
"```",
|
|
288
|
+
"You must only include files explicitly mentioned in the document.",
|
|
289
|
+
"Do not preemptively assume that any other files are needed.",
|
|
290
|
+
].join("\n\n"),
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return result.object;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function updatePlan(
|
|
299
|
+
ctx: UpdateContext<ModuleOutput>,
|
|
300
|
+
props: ModuleInput,
|
|
301
|
+
model: LanguageModel,
|
|
302
|
+
): Promise<UpdatePlan> {
|
|
303
|
+
const newContent = await fs.readFile(props.path, "utf-8");
|
|
304
|
+
const oldContent = ctx.output.content;
|
|
305
|
+
const patch = createPatch(props.path, oldContent, newContent);
|
|
306
|
+
console.log({
|
|
307
|
+
role: "assistant",
|
|
308
|
+
content: JSON.stringify(ctx.output.plan),
|
|
309
|
+
});
|
|
310
|
+
try {
|
|
311
|
+
const result = await generateObject({
|
|
312
|
+
model,
|
|
313
|
+
schema: UpdatePlan,
|
|
314
|
+
temperature: 0.1,
|
|
315
|
+
messages: [
|
|
316
|
+
{
|
|
317
|
+
role: "system",
|
|
318
|
+
content: Prompts.program,
|
|
319
|
+
},
|
|
320
|
+
...excludeDependenciesFromPlan(props.dependencies),
|
|
321
|
+
{
|
|
322
|
+
role: "user",
|
|
323
|
+
content: [
|
|
324
|
+
"Derive the execution plan for this document:",
|
|
325
|
+
"```md",
|
|
326
|
+
oldContent,
|
|
327
|
+
"```",
|
|
328
|
+
].join("\n\n"),
|
|
329
|
+
} satisfies CoreMessage,
|
|
330
|
+
{
|
|
331
|
+
role: "assistant",
|
|
332
|
+
content: JSON.stringify(ctx.output.plan),
|
|
333
|
+
} satisfies CoreMessage,
|
|
334
|
+
{
|
|
335
|
+
role: "user",
|
|
336
|
+
content: [
|
|
337
|
+
"I've made the following changes to the document:",
|
|
338
|
+
"```diff",
|
|
339
|
+
patch,
|
|
340
|
+
"```",
|
|
341
|
+
"Please update the plan to reflect the changes.",
|
|
342
|
+
"You must only include files explicitly mentioned in the document.",
|
|
343
|
+
"Do not preemptively assume that any other files are needed.",
|
|
344
|
+
].join("\n"),
|
|
345
|
+
} satisfies CoreMessage,
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return result.object;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.log("Failed to update plan");
|
|
352
|
+
console.error(error);
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function excludeDependenciesFromPlan(dependencies: FileContext[] | undefined) {
|
|
358
|
+
if (dependencies) {
|
|
359
|
+
return [
|
|
360
|
+
...dependenciesAsMessages(dependencies),
|
|
361
|
+
...(dependencies
|
|
362
|
+
? [
|
|
363
|
+
{
|
|
364
|
+
role: "user" as const,
|
|
365
|
+
content: [
|
|
366
|
+
"Remember that these files don't need to be create",
|
|
367
|
+
...dependencies.map((dep) => `- ${dep.path}`),
|
|
368
|
+
].join("\n"),
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
role: "assistant" as const,
|
|
372
|
+
content: [
|
|
373
|
+
"Got it, I acknowledge that the following files will not be included as create/update/delete/noop nodes in the plan",
|
|
374
|
+
...dependencies.map((dep) => `- ${dep.path}`),
|
|
375
|
+
].join("\n"),
|
|
376
|
+
},
|
|
377
|
+
]
|
|
378
|
+
: []),
|
|
379
|
+
];
|
|
380
|
+
} else {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function topoSort(dependencies: Record<string, string[]>): string[] {
|
|
386
|
+
const visited = new Set<string>();
|
|
387
|
+
const temp = new Set<string>();
|
|
388
|
+
const order: string[] = [];
|
|
389
|
+
|
|
390
|
+
function visit(file: string) {
|
|
391
|
+
if (temp.has(file)) {
|
|
392
|
+
throw new Error(`Circular dependency detected involving ${file}`);
|
|
393
|
+
}
|
|
394
|
+
if (visited.has(file)) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
temp.add(file);
|
|
399
|
+
|
|
400
|
+
const deps = dependencies[file] || [];
|
|
401
|
+
for (const dep of deps) {
|
|
402
|
+
visit(dep);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
temp.delete(file);
|
|
406
|
+
visited.add(file);
|
|
407
|
+
order.push(file);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const files = Object.keys(dependencies);
|
|
411
|
+
for (const file of files) {
|
|
412
|
+
if (!visited.has(file)) {
|
|
413
|
+
visit(file);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return order;
|
|
418
|
+
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
export const OpenAIModels = [
|
|
3
|
+
"o3-mini",
|
|
4
|
+
|
|
2
5
|
// O1 models
|
|
3
6
|
"o1",
|
|
4
7
|
"o1-2024-12-17",
|
|
@@ -33,12 +36,15 @@ export const OpenAIModels = [
|
|
|
33
36
|
"gpt-3.5-turbo-1106",
|
|
34
37
|
] as const;
|
|
35
38
|
|
|
39
|
+
export const OpenAIModel = z.enum(OpenAIModels);
|
|
40
|
+
|
|
36
41
|
export type OpenAIChatModelId = (typeof OpenAIModels)[number] | (string & {});
|
|
37
42
|
|
|
38
43
|
export function isOpenAIModel(model: string): model is OpenAIChatModelId {
|
|
39
44
|
return (
|
|
40
45
|
model.startsWith("gpt-") ||
|
|
41
46
|
model.startsWith("o1") ||
|
|
47
|
+
model.startsWith("o3") ||
|
|
42
48
|
model.startsWith("gpt-4o")
|
|
43
49
|
);
|
|
44
50
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Alchemy is a markdown-based development framework.
|
|
2
|
+
|
|
3
|
+
A markdown document can be thought of as a module file in a programming language.
|
|
4
|
+
|
|
5
|
+
It consists of expressions that:
|
|
6
|
+
|
|
7
|
+
1. describe system requirements
|
|
8
|
+
2. enumerate and describe `1..*` files that should be created
|
|
9
|
+
3. refer to `0..*` files that should be referenced to inform the creation of files
|
|
10
|
+
|
|
11
|
+
Design documents are succinct and concise.
|
|
12
|
+
We purposely avoid over documentation and over explanation.
|
|
13
|
+
|
|
14
|
+
It should be thought more of a sequence of statements in a programming language.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
|
|
18
|
+
```md
|
|
19
|
+
# Backend Server
|
|
20
|
+
|
|
21
|
+
The backend server is a Hono app that exposes a REST API.
|
|
22
|
+
|
|
23
|
+
## API Reference
|
|
24
|
+
|
|
25
|
+
- [GetUser](src/get-user.ts) - gets a user by UserID (ULID)
|
|
26
|
+
- [CreateUser](src/create-user.ts)
|
|
27
|
+
- inserts a User (Email, FirstName, LastName)
|
|
28
|
+
- generates a UserID
|
|
29
|
+
- Email must be Unique
|
|
30
|
+
|
|
31
|
+
## Database Schema
|
|
32
|
+
|
|
33
|
+
Refer to the [Database Schema](src/schema.sql.ts)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Notice how the example document is very matter of a fact.
|
|
37
|
+
Line, line, line.
|
|
38
|
+
Simple, logical components capturing all the important details and nothing more.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import type { Context } from "../resource";
|
|
3
|
+
import { Resource } from "../resource";
|
|
4
|
+
import type { ModelId } from "./model";
|
|
5
|
+
import { Module } from "./module";
|
|
6
|
+
|
|
7
|
+
export class Program extends Resource(
|
|
8
|
+
"Program",
|
|
9
|
+
{
|
|
10
|
+
alwaysUpdate: true,
|
|
11
|
+
},
|
|
12
|
+
async (
|
|
13
|
+
ctx: Context<{
|
|
14
|
+
module: Module;
|
|
15
|
+
}>,
|
|
16
|
+
props: {
|
|
17
|
+
path: string;
|
|
18
|
+
model?: ModelId;
|
|
19
|
+
},
|
|
20
|
+
) => {
|
|
21
|
+
if (ctx.event === "delete") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const content = await fs.readFile(props.path, "utf-8");
|
|
26
|
+
|
|
27
|
+
const module = new Module("module", {
|
|
28
|
+
path: props.path,
|
|
29
|
+
content,
|
|
30
|
+
model: props.model,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
module,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
) {}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// TODO: replace with import.meta.glob
|
|
5
|
+
// see: https://github.com/oven-sh/bun/issues/6060
|
|
6
|
+
|
|
7
|
+
const __dirname = new URL(".", import.meta.url).pathname;
|
|
8
|
+
|
|
9
|
+
const prompts = ["program"] as const;
|
|
10
|
+
|
|
11
|
+
export const Prompts = Object.fromEntries(
|
|
12
|
+
await Promise.all(
|
|
13
|
+
prompts.map(async (prompt) => [
|
|
14
|
+
prompt,
|
|
15
|
+
await fs.readFile(path.join(__dirname, `${prompt}.md`), "utf-8"),
|
|
16
|
+
]),
|
|
17
|
+
),
|
|
18
|
+
) as {
|
|
19
|
+
[key in (typeof prompts)[number]]: string;
|
|
20
|
+
};
|
package/src/alchemize.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { evaluate } from "./apply";
|
|
2
2
|
import { destroy } from "./destroy";
|
|
3
|
-
import { defaultStage,
|
|
4
|
-
import type
|
|
5
|
-
|
|
6
|
-
let finalized = false;
|
|
3
|
+
import { defaultStage, defaultStateStore, providers } from "./global";
|
|
4
|
+
import { type Scope, getScope } from "./scope";
|
|
5
|
+
import type { StateStoreType } from "./state";
|
|
7
6
|
|
|
8
7
|
export interface AlchemizeOptions {
|
|
9
8
|
/**
|
|
@@ -27,7 +26,17 @@ export interface AlchemizeOptions {
|
|
|
27
26
|
/**
|
|
28
27
|
* A custom state store to use instead of the default file system store.
|
|
29
28
|
*/
|
|
30
|
-
stateStore?:
|
|
29
|
+
stateStore?: StateStoreType;
|
|
30
|
+
/**
|
|
31
|
+
* A custom scope to use instead of the default root scope.
|
|
32
|
+
*/
|
|
33
|
+
scope?: Scope;
|
|
34
|
+
/**
|
|
35
|
+
* If true, will not print any Create/Update/Delete messages.
|
|
36
|
+
*
|
|
37
|
+
* @default false
|
|
38
|
+
*/
|
|
39
|
+
quiet?: boolean;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
/**
|
|
@@ -36,20 +45,21 @@ export interface AlchemizeOptions {
|
|
|
36
45
|
* By default, this will be called when
|
|
37
46
|
*/
|
|
38
47
|
export async function alchemize(options?: AlchemizeOptions) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
42
|
-
finalized = true;
|
|
43
|
-
|
|
48
|
+
const scope = options?.scope ?? getScope();
|
|
49
|
+
const nodes = scope.nodes;
|
|
44
50
|
const stage = options?.stage ?? defaultStage;
|
|
45
|
-
const
|
|
51
|
+
const stateStore = new (options?.stateStore ?? defaultStateStore)(
|
|
52
|
+
scope.getScopePath(stage),
|
|
53
|
+
);
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
if (nodes.size > 0) {
|
|
56
|
+
await stateStore.init?.();
|
|
57
|
+
}
|
|
48
58
|
|
|
49
59
|
// Track in-progress deletions to avoid duplicate work
|
|
50
60
|
const deletionPromises = new Map<string, Promise<void>>();
|
|
51
61
|
|
|
52
|
-
const priorStates = await
|
|
62
|
+
const priorStates = await stateStore.all();
|
|
53
63
|
const priorIDs = Object.keys(priorStates);
|
|
54
64
|
|
|
55
65
|
const mode = options?.mode ?? "up";
|
|
@@ -61,6 +71,9 @@ export async function alchemize(options?: AlchemizeOptions) {
|
|
|
61
71
|
.map((node) =>
|
|
62
72
|
evaluate(node.resource, {
|
|
63
73
|
stage,
|
|
74
|
+
scope,
|
|
75
|
+
stateStore,
|
|
76
|
+
quiet: options?.quiet,
|
|
64
77
|
}),
|
|
65
78
|
),
|
|
66
79
|
);
|
|
@@ -78,6 +91,10 @@ export async function alchemize(options?: AlchemizeOptions) {
|
|
|
78
91
|
|
|
79
92
|
const orphanIDs = Array.from(priorIDs).filter((id) => !aliveIDs.has(id));
|
|
80
93
|
|
|
94
|
+
console.log("orphanIDs", orphanIDs);
|
|
95
|
+
console.log("priorIDs", priorIDs);
|
|
96
|
+
console.log("aliveIDs", aliveIDs);
|
|
97
|
+
|
|
81
98
|
const orphanStates = Object.fromEntries(
|
|
82
99
|
orphanIDs.map((id) => [id, priorStates[id]] as const),
|
|
83
100
|
);
|
|
@@ -89,6 +106,10 @@ export async function alchemize(options?: AlchemizeOptions) {
|
|
|
89
106
|
(orphanGraph[dep] ??= new Set()).add(orphanID);
|
|
90
107
|
}
|
|
91
108
|
}
|
|
109
|
+
if (Object.keys(orphanGraph).length > 0) {
|
|
110
|
+
console.log(stateStore.path);
|
|
111
|
+
console.log(orphanGraph);
|
|
112
|
+
}
|
|
92
113
|
|
|
93
114
|
// Start deletion from each orphan that has no dependents (nothing depends on it)
|
|
94
115
|
await Promise.all(
|
|
@@ -97,6 +118,11 @@ export async function alchemize(options?: AlchemizeOptions) {
|
|
|
97
118
|
.map((id) => deleteOrphan(id)),
|
|
98
119
|
);
|
|
99
120
|
|
|
121
|
+
const remaining = await stateStore.count();
|
|
122
|
+
if (remaining === 0) {
|
|
123
|
+
await stateStore.deinit?.();
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
// Recursively delete orphans in dependency order
|
|
101
127
|
async function deleteOrphan(orphanID: string): Promise<void> {
|
|
102
128
|
// Return existing deletion promise if this orphan is already being deleted
|
|
@@ -125,7 +151,12 @@ export async function alchemize(options?: AlchemizeOptions) {
|
|
|
125
151
|
`No provider found for ${providerType}. Did you forget to import it?`,
|
|
126
152
|
);
|
|
127
153
|
}
|
|
128
|
-
|
|
154
|
+
if (!options?.quiet) {
|
|
155
|
+
console.log("Destroy:", orphanID);
|
|
156
|
+
}
|
|
157
|
+
await destroy(stage, scope, orphanID, orphanState, provider, stateStore, {
|
|
158
|
+
quiet: options?.quiet,
|
|
159
|
+
});
|
|
129
160
|
|
|
130
161
|
// After this resource is deleted, we can delete its dependencies if they're orphans
|
|
131
162
|
if (orphanState.deps.length > 0) {
|