alchemy 0.1.1 → 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 +324 -0
- package/package.json +15 -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
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, unlink, writeFile } from "fs/promises";
|
|
1
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
3
2
|
import { dirname } from "path";
|
|
4
3
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
FileContext,
|
|
6
|
+
ModelId,
|
|
7
|
+
dependenciesAsMessages,
|
|
8
|
+
generateText,
|
|
9
|
+
resolveModel,
|
|
10
|
+
} from "../agent";
|
|
11
|
+
import { rm } from "../fs";
|
|
12
|
+
import { type Context, Resource } from "../resource";
|
|
13
|
+
|
|
14
|
+
export const ReasoningEffort = z.enum(["low", "medium", "high"]);
|
|
8
15
|
|
|
9
16
|
export const RequirementsInput = z.object({
|
|
10
17
|
/**
|
|
11
18
|
* The ID of the model to use for generating requirements documentation
|
|
12
19
|
* @default "gpt-4o"
|
|
13
20
|
*/
|
|
14
|
-
modelId:
|
|
21
|
+
modelId: ModelId.optional(),
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The effort to put into reasoning about the requirements
|
|
25
|
+
*/
|
|
26
|
+
reasoningEffort: ReasoningEffort.optional(),
|
|
15
27
|
|
|
16
28
|
/**
|
|
17
29
|
* List of requirements to document and analyze
|
|
@@ -19,9 +31,9 @@ export const RequirementsInput = z.object({
|
|
|
19
31
|
requirements: z.array(z.string()),
|
|
20
32
|
|
|
21
33
|
/**
|
|
22
|
-
*
|
|
34
|
+
* List of dependencies for the requirements document
|
|
23
35
|
*/
|
|
24
|
-
|
|
36
|
+
dependencies: z.array(FileContext).optional(),
|
|
25
37
|
|
|
26
38
|
/**
|
|
27
39
|
* Temperature setting for model generation (higher = more creative, lower = more focused)
|
|
@@ -32,7 +44,7 @@ export const RequirementsInput = z.object({
|
|
|
32
44
|
/**
|
|
33
45
|
* File path where the requirements document should be written
|
|
34
46
|
*/
|
|
35
|
-
|
|
47
|
+
file: z.string().optional(),
|
|
36
48
|
});
|
|
37
49
|
|
|
38
50
|
export const RequirementsOutput = z.object({
|
|
@@ -42,33 +54,44 @@ export const RequirementsOutput = z.object({
|
|
|
42
54
|
content: z.string(),
|
|
43
55
|
});
|
|
44
56
|
|
|
45
|
-
export
|
|
57
|
+
export interface RequirementsInput extends z.infer<typeof RequirementsInput> {}
|
|
46
58
|
export type RequirementsOutput = z.infer<typeof RequirementsOutput>;
|
|
47
59
|
|
|
48
|
-
export class Requirements extends
|
|
60
|
+
export class Requirements extends Resource(
|
|
49
61
|
"code::Requirements",
|
|
50
|
-
{
|
|
51
|
-
description:
|
|
52
|
-
"This Agent is responsible for writing and refining a detailed requirements document from a list of requirements.",
|
|
53
|
-
input: RequirementsInput,
|
|
54
|
-
output: RequirementsOutput,
|
|
55
|
-
},
|
|
56
|
-
async (ctx, input) => {
|
|
62
|
+
async (ctx: Context<RequirementsOutput>, props: RequirementsInput) => {
|
|
57
63
|
if (ctx.event === "delete") {
|
|
58
|
-
|
|
64
|
+
if (props.file) {
|
|
65
|
+
await rm(props.file);
|
|
66
|
+
}
|
|
59
67
|
return;
|
|
60
68
|
}
|
|
61
69
|
|
|
70
|
+
if (props.file) {
|
|
71
|
+
console.log(
|
|
72
|
+
ctx.event === "create" ? "Designing" : "Revising",
|
|
73
|
+
"requirements",
|
|
74
|
+
props.file,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
// Get the appropriate model based on the ID
|
|
63
|
-
const
|
|
79
|
+
const model = await resolveModel(props.modelId ?? "gpt-4o");
|
|
64
80
|
|
|
65
81
|
// Generate the requirements document
|
|
66
82
|
const { text } = await generateText({
|
|
67
83
|
model,
|
|
68
|
-
temperature:
|
|
84
|
+
temperature: props.temperature ?? 0.7,
|
|
69
85
|
maxSteps: 3, // Allow multiple steps for potential tool usage
|
|
86
|
+
providerOptions: props.reasoningEffort
|
|
87
|
+
? {
|
|
88
|
+
openai: {
|
|
89
|
+
reasoningEffort: props.reasoningEffort,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
: undefined,
|
|
70
93
|
tools: {
|
|
71
|
-
scrapeWebPage,
|
|
94
|
+
// scrapeWebPage,
|
|
72
95
|
},
|
|
73
96
|
messages: [
|
|
74
97
|
{
|
|
@@ -76,14 +99,13 @@ export class Requirements extends Agent(
|
|
|
76
99
|
content:
|
|
77
100
|
"You are a helpful assistant that creates markdown documents from requirements. You can use the scrapeWebPage tool to get additional requirements from relevant URLs if needed.",
|
|
78
101
|
},
|
|
102
|
+
...dependenciesAsMessages(props.dependencies ?? []),
|
|
79
103
|
{
|
|
80
104
|
role: "user",
|
|
81
105
|
content: `Please analyze the following system requirements and create a comprehensive, unambiguous markdown document that covers all aspects:
|
|
82
106
|
|
|
83
|
-
Title: ${input.title}
|
|
84
|
-
|
|
85
107
|
Requirements:
|
|
86
|
-
${
|
|
108
|
+
${props.requirements.map((req) => `- ${req}`).join("\n")}
|
|
87
109
|
|
|
88
110
|
Guidelines:
|
|
89
111
|
1. Make explicit decisions for any ambiguous requirements - do not leave open questions
|
|
@@ -98,11 +120,13 @@ Include technical considerations and implementation details, ensuring every requ
|
|
|
98
120
|
],
|
|
99
121
|
});
|
|
100
122
|
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
if (props.file) {
|
|
124
|
+
// Ensure the directory exists
|
|
125
|
+
await mkdir(dirname(props.file), { recursive: true });
|
|
103
126
|
|
|
104
|
-
|
|
105
|
-
|
|
127
|
+
// Write the requirements to the file
|
|
128
|
+
await writeFile(props.file, text);
|
|
129
|
+
}
|
|
106
130
|
|
|
107
131
|
return {
|
|
108
132
|
content: text,
|
package/src/output.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export type Eval<U> = U extends Output<infer V> ? Eval<V> : U;
|
|
2
2
|
|
|
3
|
+
export function isOutput<T>(value: any): value is Output<T> {
|
|
4
|
+
return (
|
|
5
|
+
value && typeof value === "object" && typeof value.apply === "function"
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export type Outputs<P extends readonly any[]> = P extends [
|
|
4
10
|
infer First,
|
|
5
11
|
...infer Rest,
|
package/src/resource.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
1
|
+
import { alchemize } from "./alchemize";
|
|
2
|
+
import { type ApplyOptions, apply } from "./apply";
|
|
3
|
+
import type { DestroyOptions } from "./destroy";
|
|
4
|
+
import { defaultStateStore, deletions, providers } from "./global";
|
|
5
|
+
import type { Inputs, Input as input } from "./input";
|
|
3
6
|
import { Output } from "./output";
|
|
7
|
+
import { Scope as IScope, getScope, pushScope } from "./scope";
|
|
4
8
|
import type { State, StateStore } from "./state";
|
|
5
9
|
|
|
6
10
|
export type ResourceID = string;
|
|
@@ -12,6 +16,8 @@ export const Input = Symbol.for("Input");
|
|
|
12
16
|
export const Value = Symbol.for("Value");
|
|
13
17
|
export const Apply = Symbol.for("Apply");
|
|
14
18
|
export const Provide = Symbol.for("Provide");
|
|
19
|
+
export const Scope = Symbol.for("Scope");
|
|
20
|
+
export const Options = Symbol.for("Options");
|
|
15
21
|
|
|
16
22
|
// properties that pierce through the Proxy
|
|
17
23
|
const OrthogonalProperties = [
|
|
@@ -21,8 +27,17 @@ const OrthogonalProperties = [
|
|
|
21
27
|
Value,
|
|
22
28
|
Apply,
|
|
23
29
|
Provide,
|
|
30
|
+
Scope,
|
|
31
|
+
Options,
|
|
24
32
|
] as const;
|
|
25
33
|
|
|
34
|
+
export interface ProviderOptions {
|
|
35
|
+
/**
|
|
36
|
+
* If true, the resource will be updated even if the inputs have not changed.
|
|
37
|
+
*/
|
|
38
|
+
alwaysUpdate: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
export type Resource<In extends any[] = any[], Out = any> = {
|
|
27
42
|
[ResourceID]: string;
|
|
28
43
|
[Provider]: Provider<any, any[], any>;
|
|
@@ -30,11 +45,14 @@ export type Resource<In extends any[] = any[], Out = any> = {
|
|
|
30
45
|
[Value]?: Out;
|
|
31
46
|
[Apply]: <O>(value: Out) => O;
|
|
32
47
|
[Provide]: (value: Out) => void;
|
|
48
|
+
[Options]: ProviderOptions;
|
|
33
49
|
} & Output<Out>;
|
|
34
50
|
|
|
35
|
-
export
|
|
51
|
+
export interface BaseContext {
|
|
52
|
+
quiet: boolean;
|
|
36
53
|
stage: string;
|
|
37
54
|
resourceID: ResourceID;
|
|
55
|
+
scope: IScope;
|
|
38
56
|
get<T>(key: string): Promise<T | undefined>;
|
|
39
57
|
set<T>(key: string, value: T): Promise<void>;
|
|
40
58
|
delete<T>(key: string): Promise<T | undefined>;
|
|
@@ -43,15 +61,26 @@ export type Context<Outputs> = {
|
|
|
43
61
|
* This will cause the resource to be deleted at the end of the stack's CREATE phase.
|
|
44
62
|
*/
|
|
45
63
|
replace(): void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface CreateContext extends BaseContext {
|
|
67
|
+
event: "create";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface UpdateContext<Outputs> extends BaseContext {
|
|
71
|
+
event: "update";
|
|
72
|
+
output: Outputs;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface DeleteContext<Outputs> extends BaseContext {
|
|
76
|
+
event: "delete";
|
|
77
|
+
output: Outputs;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export type Context<Outputs> =
|
|
81
|
+
| CreateContext
|
|
82
|
+
| UpdateContext<Outputs>
|
|
83
|
+
| DeleteContext<Outputs>;
|
|
55
84
|
|
|
56
85
|
export type Provider<
|
|
57
86
|
Type extends ResourceType = ResourceType,
|
|
@@ -65,16 +94,19 @@ export type Provider<
|
|
|
65
94
|
deps: Set<ResourceID>,
|
|
66
95
|
inputs: Inputs<In>,
|
|
67
96
|
stateStore: StateStore,
|
|
97
|
+
options: ApplyOptions,
|
|
68
98
|
): Promise<Awaited<Out>>;
|
|
69
99
|
delete(
|
|
70
100
|
stage: string,
|
|
101
|
+
scope: IScope | undefined,
|
|
71
102
|
resourceID: ResourceID,
|
|
72
103
|
state: State,
|
|
73
104
|
inputs: Inputs<In>,
|
|
105
|
+
options: DestroyOptions,
|
|
74
106
|
): Promise<void>;
|
|
75
107
|
} & (new (
|
|
76
108
|
id: string,
|
|
77
|
-
...inputs: Inputs<In
|
|
109
|
+
...inputs: [...Inputs<In>, ...any[]]
|
|
78
110
|
) => Resource<In, Out>);
|
|
79
111
|
|
|
80
112
|
export function isResource(value: any): value is Resource {
|
|
@@ -87,41 +119,88 @@ export function Resource<
|
|
|
87
119
|
Out,
|
|
88
120
|
>(
|
|
89
121
|
type: Type,
|
|
90
|
-
|
|
122
|
+
options: Partial<ProviderOptions>,
|
|
123
|
+
func: (
|
|
124
|
+
ctx: Context<Out>,
|
|
125
|
+
...args: Args
|
|
126
|
+
) => Promise<input<Out> | void> | input<Out> | void,
|
|
127
|
+
): Provider<Type, Args, Awaited<Out>>;
|
|
128
|
+
|
|
129
|
+
export function Resource<
|
|
130
|
+
const Type extends ResourceType,
|
|
131
|
+
Args extends any[],
|
|
132
|
+
Out,
|
|
133
|
+
>(
|
|
134
|
+
type: Type,
|
|
135
|
+
func: (
|
|
136
|
+
ctx: Context<Out>,
|
|
137
|
+
...args: Args
|
|
138
|
+
) => Promise<input<Out> | void> | input<Out> | void,
|
|
139
|
+
): Provider<Type, Args, Awaited<Out>>;
|
|
140
|
+
|
|
141
|
+
export function Resource<
|
|
142
|
+
const Type extends ResourceType,
|
|
143
|
+
Args extends any[],
|
|
144
|
+
Out,
|
|
145
|
+
>(
|
|
146
|
+
type: Type,
|
|
147
|
+
...args:
|
|
148
|
+
| [
|
|
149
|
+
Partial<ProviderOptions>,
|
|
150
|
+
(
|
|
151
|
+
ctx: Context<Out>,
|
|
152
|
+
...args: Args
|
|
153
|
+
) => Promise<input<Out> | void> | input<Out> | void,
|
|
154
|
+
]
|
|
155
|
+
| [
|
|
156
|
+
(
|
|
157
|
+
ctx: Context<Out>,
|
|
158
|
+
...args: Args
|
|
159
|
+
) => Promise<input<Out> | void> | input<Out> | void,
|
|
160
|
+
]
|
|
91
161
|
): Provider<Type, Args, Awaited<Out>> {
|
|
92
162
|
if (providers.has(type)) {
|
|
93
163
|
throw new Error(`Resource ${type} already exists`);
|
|
94
164
|
}
|
|
165
|
+
const [options, func] = args.length === 2 ? args : [undefined, args[0]];
|
|
95
166
|
|
|
96
167
|
interface Resource {
|
|
97
168
|
[ResourceID]: ResourceID;
|
|
98
169
|
[Provider]: Provider<Type, Args, Out>;
|
|
99
170
|
[Input]: Inputs<Args>;
|
|
100
171
|
[Value]?: Out;
|
|
172
|
+
[Scope]: IScope;
|
|
173
|
+
[Options]: ProviderOptions;
|
|
101
174
|
}
|
|
102
175
|
|
|
103
176
|
class Resource {
|
|
104
177
|
static readonly type = type;
|
|
105
178
|
|
|
106
179
|
constructor(id: ResourceID, ...input: Inputs<Args>) {
|
|
180
|
+
const scope = getScope();
|
|
107
181
|
const node = {
|
|
108
182
|
provider: Resource,
|
|
109
183
|
resource: this,
|
|
110
184
|
} as const;
|
|
111
|
-
|
|
185
|
+
|
|
186
|
+
if (scope.nodes.has(id)) {
|
|
112
187
|
// TODO(sam): do we want to throw?
|
|
113
188
|
// it's kind of awesome that you can re-create a resource and call apply
|
|
114
189
|
// console.warn(`Resource ${id} already exists in the stack: ${stack.id}`);
|
|
115
190
|
}
|
|
116
|
-
nodes.set(id, node as any);
|
|
191
|
+
scope.nodes.set(id, node as any);
|
|
117
192
|
|
|
118
193
|
this[ResourceID] = id;
|
|
119
194
|
this[Provider] = Resource as any;
|
|
120
195
|
this[Input] = input;
|
|
121
196
|
this[Value] = undefined;
|
|
197
|
+
this[Scope] = scope;
|
|
122
198
|
this[Provide] = (value: Out) => {
|
|
123
199
|
this[Value] = value;
|
|
124
200
|
};
|
|
201
|
+
this[Options] = {
|
|
202
|
+
alwaysUpdate: options?.alwaysUpdate ?? false,
|
|
203
|
+
};
|
|
125
204
|
|
|
126
205
|
return new Proxy(this, {
|
|
127
206
|
// TODO(sam): this won't work for the sub-class (class Table extends Resource)
|
|
@@ -175,14 +254,12 @@ export function Resource<
|
|
|
175
254
|
deps: Set<ResourceID>,
|
|
176
255
|
inputs: Args,
|
|
177
256
|
stateStore: StateStore,
|
|
257
|
+
options: ApplyOptions,
|
|
178
258
|
): Promise<Awaited<Out | void>> {
|
|
179
259
|
// const stack = resource[ResourceStack];
|
|
180
260
|
const resourceID = resource[ResourceID];
|
|
181
261
|
|
|
182
|
-
let resourceState: State | undefined = await stateStore.get(
|
|
183
|
-
stage,
|
|
184
|
-
resourceID,
|
|
185
|
-
);
|
|
262
|
+
let resourceState: State | undefined = await stateStore.get(resourceID);
|
|
186
263
|
if (resourceState === undefined) {
|
|
187
264
|
resourceState = {
|
|
188
265
|
provider: type,
|
|
@@ -192,20 +269,28 @@ export function Resource<
|
|
|
192
269
|
deps: [...deps],
|
|
193
270
|
inputs,
|
|
194
271
|
};
|
|
195
|
-
await stateStore.set(
|
|
272
|
+
await stateStore.set(resourceID, resourceState);
|
|
196
273
|
}
|
|
197
274
|
|
|
275
|
+
const resourceFQN = `${resource[Scope].getScopePath(stage)}/${resourceID}`;
|
|
276
|
+
|
|
198
277
|
// Skip update if inputs haven't changed and resource is in a stable state
|
|
199
278
|
if (
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
JSON.stringify(resourceState.inputs) === JSON.stringify(inputs)
|
|
279
|
+
resourceState.status === "created" ||
|
|
280
|
+
resourceState.status === "updated"
|
|
203
281
|
) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
resource[
|
|
282
|
+
if (
|
|
283
|
+
JSON.stringify(resourceState.inputs) === JSON.stringify(inputs) &&
|
|
284
|
+
!resource[Options].alwaysUpdate
|
|
285
|
+
) {
|
|
286
|
+
if (!options?.quiet) {
|
|
287
|
+
console.log(`Skip: ${resourceFQN} (no changes)`);
|
|
288
|
+
}
|
|
289
|
+
if (resourceState.output !== undefined) {
|
|
290
|
+
resource[Provide](resourceState.output);
|
|
291
|
+
}
|
|
292
|
+
return resourceState.output;
|
|
207
293
|
}
|
|
208
|
-
return resourceState.output;
|
|
209
294
|
}
|
|
210
295
|
|
|
211
296
|
const event = resourceState.status === "creating" ? "create" : "update";
|
|
@@ -213,78 +298,122 @@ export function Resource<
|
|
|
213
298
|
resourceState.oldInputs = resourceState.inputs;
|
|
214
299
|
resourceState.inputs = inputs;
|
|
215
300
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
301
|
+
if (!options?.quiet) {
|
|
302
|
+
console.log(
|
|
303
|
+
`${event === "create" ? "Create" : "Update"}: ${resourceFQN}`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
219
306
|
|
|
220
|
-
await stateStore.set(
|
|
307
|
+
await stateStore.set(resourceID, resourceState);
|
|
221
308
|
|
|
222
309
|
let isReplaced = false;
|
|
223
310
|
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
311
|
+
const quiet = options.quiet ?? false;
|
|
312
|
+
|
|
313
|
+
const evaluated = await pushScope(
|
|
314
|
+
resource[Scope],
|
|
315
|
+
resourceID,
|
|
316
|
+
async () => {
|
|
317
|
+
const result = await func(
|
|
318
|
+
{
|
|
319
|
+
stage,
|
|
320
|
+
resourceID,
|
|
321
|
+
event,
|
|
322
|
+
scope: getScope(),
|
|
323
|
+
output: resourceState.output,
|
|
324
|
+
replace: () => {
|
|
325
|
+
if (isReplaced) {
|
|
326
|
+
console.warn(
|
|
327
|
+
`Resource ${type} ${resourceFQN} is already marked as REPLACE`,
|
|
328
|
+
);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
isReplaced = true;
|
|
332
|
+
deletions.push({
|
|
333
|
+
id: resourceID,
|
|
334
|
+
data: {
|
|
335
|
+
...resourceState!.data,
|
|
336
|
+
},
|
|
337
|
+
inputs: inputs,
|
|
338
|
+
});
|
|
242
339
|
},
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
340
|
+
get: (key) => resourceState!.data[key],
|
|
341
|
+
set: async (key, value) => {
|
|
342
|
+
resourceState!.data[key] = value;
|
|
343
|
+
await stateStore.set(resourceID, resourceState!);
|
|
344
|
+
},
|
|
345
|
+
delete: async (key) => {
|
|
346
|
+
const value = resourceState!.data[key];
|
|
347
|
+
delete resourceState!.data[key];
|
|
348
|
+
await stateStore.set(resourceID, resourceState!);
|
|
349
|
+
return value;
|
|
350
|
+
},
|
|
351
|
+
quiet,
|
|
352
|
+
},
|
|
353
|
+
...inputs,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
if (result === undefined) {
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const evaluated = await apply(result as Out, {
|
|
361
|
+
stage,
|
|
362
|
+
scope: getScope(),
|
|
363
|
+
quiet,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
return evaluated;
|
|
257
367
|
},
|
|
258
|
-
...inputs,
|
|
259
368
|
);
|
|
260
369
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
370
|
+
if (!options?.quiet) {
|
|
371
|
+
console.log(
|
|
372
|
+
`${event === "create" ? "Created" : "Updated"}: ${resourceFQN}`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
await stateStore.set(resourceID, {
|
|
265
376
|
provider: type,
|
|
266
377
|
data: resourceState.data,
|
|
267
378
|
status: event === "create" ? "created" : "updated",
|
|
268
|
-
output:
|
|
379
|
+
output: evaluated,
|
|
269
380
|
inputs,
|
|
270
381
|
deps: [...deps],
|
|
271
382
|
});
|
|
272
|
-
if (
|
|
273
|
-
resource[Provide](
|
|
383
|
+
if (evaluated !== undefined) {
|
|
384
|
+
resource[Provide](evaluated);
|
|
274
385
|
}
|
|
275
|
-
return
|
|
386
|
+
return evaluated;
|
|
276
387
|
}
|
|
277
388
|
|
|
278
389
|
static async delete(
|
|
279
390
|
stage: string,
|
|
391
|
+
scope: IScope,
|
|
280
392
|
resourceID: ResourceID,
|
|
281
393
|
state: State,
|
|
282
394
|
inputs: Args,
|
|
395
|
+
options: DestroyOptions,
|
|
283
396
|
) {
|
|
284
|
-
|
|
397
|
+
const resourceFQN = `${scope.getScopePath(stage)}/${resourceID}`;
|
|
398
|
+
const nestedScope = new IScope(resourceID, scope);
|
|
399
|
+
|
|
400
|
+
await alchemize({
|
|
401
|
+
mode: "destroy",
|
|
402
|
+
stage,
|
|
403
|
+
scope: nestedScope,
|
|
404
|
+
// TODO(sam): should use the appropriate state store
|
|
405
|
+
stateStore: defaultStateStore,
|
|
406
|
+
quiet: options.quiet,
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (!options?.quiet) {
|
|
410
|
+
console.log(`Delete: ${resourceFQN}`);
|
|
411
|
+
}
|
|
412
|
+
|
|
285
413
|
await func(
|
|
286
414
|
{
|
|
287
415
|
stage,
|
|
416
|
+
scope,
|
|
288
417
|
resourceID: resourceID,
|
|
289
418
|
event: "delete",
|
|
290
419
|
output: state.output,
|
|
@@ -302,10 +431,14 @@ export function Resource<
|
|
|
302
431
|
delete state.data[key];
|
|
303
432
|
return value;
|
|
304
433
|
},
|
|
434
|
+
quiet: options.quiet ?? false,
|
|
305
435
|
},
|
|
306
436
|
...inputs,
|
|
307
437
|
);
|
|
308
|
-
|
|
438
|
+
|
|
439
|
+
if (!options?.quiet) {
|
|
440
|
+
console.log(`Deleted: ${resourceFQN}`);
|
|
441
|
+
}
|
|
309
442
|
}
|
|
310
443
|
}
|
|
311
444
|
providers.set(type, Resource as any);
|
package/src/scope.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { rootScope } from "./global";
|
|
4
|
+
import type { Provider, Resource, ResourceID } from "./resource";
|
|
5
|
+
|
|
6
|
+
const scopeGlobal = new Map<string | null, Scope>();
|
|
7
|
+
|
|
8
|
+
const scopeStorage = new AsyncLocalStorage<Scope>();
|
|
9
|
+
|
|
10
|
+
export class Scope {
|
|
11
|
+
public readonly nodes = new Map<
|
|
12
|
+
ResourceID,
|
|
13
|
+
{
|
|
14
|
+
provider: Provider<any, any[], any>;
|
|
15
|
+
resource: Resource<any, any>;
|
|
16
|
+
}
|
|
17
|
+
>();
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
public readonly scopeName: string | null = null,
|
|
21
|
+
public readonly parent: Scope | undefined = undefined,
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
public getScopePath(root: string): string {
|
|
25
|
+
// First, compute the parent's scope path
|
|
26
|
+
const parentPath = this.parent ? this.parent.getScopePath(root) : root;
|
|
27
|
+
// Then join the current scope name (if any) onto the parent's path
|
|
28
|
+
return this.scopeName ? path.join(parentPath, this.scopeName) : parentPath;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getScope(): Scope {
|
|
33
|
+
return scopeStorage.getStore() ?? rootScope;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function withScope<T>(
|
|
37
|
+
scope: Scope,
|
|
38
|
+
fn: () => T | Promise<T>,
|
|
39
|
+
): Promise<T> {
|
|
40
|
+
return scopeStorage.run(scope, async () => {
|
|
41
|
+
try {
|
|
42
|
+
return await fn();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function pushScope<T>(
|
|
50
|
+
parent: Scope,
|
|
51
|
+
scopeName: string,
|
|
52
|
+
fn: () => T | Promise<T>,
|
|
53
|
+
): Promise<T> {
|
|
54
|
+
const newScope = new Scope(scopeName, parent);
|
|
55
|
+
return withScope(newScope, fn);
|
|
56
|
+
}
|
package/src/slug.ts
ADDED