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.
Files changed (74) hide show
  1. package/lib/alchemy.d.ts +1 -0
  2. package/lib/alchemy.js +84 -8
  3. package/lib/apply.d.ts +1 -1
  4. package/lib/apply.js +13 -10
  5. package/lib/aws/bucket.d.ts +1 -1
  6. package/lib/aws/function.d.ts +1 -1
  7. package/lib/aws/oidc/oidc-provider.d.ts +1 -1
  8. package/lib/aws/policy-attachment.d.ts +1 -1
  9. package/lib/aws/policy.d.ts +1 -1
  10. package/lib/aws/queue.d.ts +1 -1
  11. package/lib/aws/role.d.ts +1 -1
  12. package/lib/aws/ses.d.ts +1 -1
  13. package/lib/aws/table.d.ts +1 -1
  14. package/lib/cloudflare/bucket.d.ts +1 -1
  15. package/lib/cloudflare/kv-namespace.d.ts +1 -1
  16. package/lib/cloudflare/static-site.d.ts +1 -1
  17. package/lib/cloudflare/worker.d.ts +1 -1
  18. package/lib/cloudflare/wrangler.json.d.ts +1 -1
  19. package/lib/cloudflare/zone.d.ts +1 -1
  20. package/lib/context.d.ts +9 -4
  21. package/lib/context.js +4 -2
  22. package/lib/destroy.js +8 -7
  23. package/lib/docs/document.d.ts +123 -0
  24. package/lib/docs/document.js +67 -0
  25. package/lib/docs/index.d.ts +1 -0
  26. package/lib/docs/index.js +1 -0
  27. package/lib/esbuild/bundle.d.ts +1 -1
  28. package/lib/fs/file.d.ts +30 -3
  29. package/lib/fs/file.js +63 -3
  30. package/lib/fs/folder.d.ts +3 -3
  31. package/lib/fs/folder.js +2 -1
  32. package/lib/github/secret.d.ts +1 -1
  33. package/lib/resource.d.ts +2 -2
  34. package/lib/resource.js +4 -0
  35. package/lib/scope.d.ts +1 -1
  36. package/lib/scope.js +6 -6
  37. package/lib/shadcn/component.d.ts +1 -1
  38. package/lib/state.d.ts +1 -1
  39. package/lib/state.js +8 -1
  40. package/lib/stripe/price.d.ts +1 -1
  41. package/lib/stripe/product.d.ts +1 -1
  42. package/lib/stripe/webhook.d.ts +1 -1
  43. package/lib/util/serde.d.ts +3 -1
  44. package/lib/util/serde.js +6 -4
  45. package/lib/vite/vite.d.ts +1 -1
  46. package/lib/vitepress/dependencies.d.ts +16 -0
  47. package/lib/vitepress/dependencies.js +35 -0
  48. package/lib/vitepress/home-page.d.ts +133 -0
  49. package/lib/vitepress/home-page.js +11 -0
  50. package/lib/vitepress/index.d.ts +2 -0
  51. package/lib/vitepress/index.js +2 -0
  52. package/lib/vitepress/vitepress.d.ts +60 -0
  53. package/lib/vitepress/vitepress.js +260 -0
  54. package/package.json +16 -9
  55. package/src/alchemy.ts +105 -13
  56. package/src/apply.ts +25 -13
  57. package/src/context.ts +30 -10
  58. package/src/destroy.ts +10 -7
  59. package/src/docs/document.ts +225 -0
  60. package/src/docs/index.ts +1 -0
  61. package/src/fs/file.ts +114 -6
  62. package/src/fs/folder.ts +2 -1
  63. package/src/resource.ts +7 -2
  64. package/src/scope.ts +9 -9
  65. package/src/state.ts +9 -2
  66. package/src/util/serde.ts +13 -4
  67. package/src/vitepress/dependencies.ts +68 -0
  68. package/src/vitepress/home-page.ts +166 -0
  69. package/src/vitepress/index.md +75 -0
  70. package/src/vitepress/index.ts +2 -0
  71. package/src/vitepress/vitepress.ts +361 -0
  72. package/lib/project/vite.d.ts +0 -90
  73. package/lib/project/vite.js +0 -228
  74. package/src/project/vite.ts +0 -406
package/lib/alchemy.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface Alchemy {
10
10
  destroy: typeof destroy;
11
11
  secret: typeof secret;
12
12
  (...parameters: Parameters<typeof scope>): ReturnType<typeof scope>;
13
+ (template: TemplateStringsArray, ...values: any[]): Promise<string>;
13
14
  }
14
15
  export interface AlchemyOptions {
15
16
  /**
package/lib/alchemy.js CHANGED
@@ -1,20 +1,91 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import { DestroyedSignal, destroy } from "./destroy";
2
4
  import { Scope } from "./scope";
3
5
  import { secret } from "./secret";
4
6
  // TODO: support browser
5
7
  const DEFAULT_STAGE = process.env.ALCHEMY_STAGE ?? process.env.USER ?? "dev";
6
- function _alchemy(appName, options) {
7
- return scope(undefined, {
8
- ...options,
9
- appName,
10
- stage: options.stage,
11
- });
8
+ export const alchemy = _alchemy;
9
+ function _alchemy(...args) {
10
+ if (typeof args[0] === "string") {
11
+ const [appName, options] = args;
12
+ return scope(undefined, {
13
+ ...options,
14
+ appName,
15
+ stage: options?.stage,
16
+ });
17
+ }
18
+ else {
19
+ const [template, ...values] = args;
20
+ const [, secondLine] = template[0].split("\n");
21
+ const leadingSpaces = secondLine
22
+ ? secondLine.match(/^(\s*)/)?.[1]?.length || 0
23
+ : 0;
24
+ const indent = " ".repeat(leadingSpaces);
25
+ return (async () => {
26
+ const { isFileCollection, isFileRef } = await import("./fs/file");
27
+ const appendices = {};
28
+ const stringValues = await Promise.all(values.map(async function resolve(value) {
29
+ if (typeof value === "string") {
30
+ return indent + value;
31
+ }
32
+ else if (value instanceof Promise) {
33
+ return resolve(await value);
34
+ }
35
+ else if (isFileRef(value)) {
36
+ if (!(value.path in appendices)) {
37
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
38
+ }
39
+ return `[${path.basename(value.path)}](${value.path})`;
40
+ }
41
+ else if (isFileCollection(value)) {
42
+ return Object.entries(value.files)
43
+ .map(([filePath, content]) => {
44
+ appendices[filePath] = content;
45
+ return `[${path.basename(filePath)}](${filePath})`;
46
+ })
47
+ .join("\n\n");
48
+ }
49
+ else if (Array.isArray(value)) {
50
+ return (await Promise.all(value.map(async (value, i) => `${i}. ${await resolve(value)}`))).join("\n");
51
+ }
52
+ else {
53
+ // TODO: support other types
54
+ throw new Error(`Unsupported value type: ${JSON.stringify(value)}`);
55
+ }
56
+ }));
57
+ // Construct the string template by joining template parts with interpolated values
58
+ const lines = template
59
+ .map((part) => part
60
+ .split("\n")
61
+ .map((line) => line.startsWith(indent) ? line.slice(indent.length) : line)
62
+ .join("\n"))
63
+ .flatMap((part, i) => i < stringValues.length ? [part, stringValues[i] ?? ""] : [part])
64
+ .join("")
65
+ .split("\n");
66
+ // Collect and sort appendices by file path
67
+ return [
68
+ // format the user prompt and trim the first line if it's empty
69
+ lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
70
+ ? lines.slice(1).join("\n")
71
+ : lines.join("\n"),
72
+ // sort appendices by path and include at the end of the prompt
73
+ Object.entries(appendices)
74
+ .sort(([a], [b]) => a.localeCompare(b))
75
+ .map(([filePath, content]) => {
76
+ const extension = path.extname(filePath).slice(1);
77
+ const codeTag = extension ? extension : "";
78
+ return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
79
+ })
80
+ .join("\n\n"),
81
+ ].join("\n");
82
+ })();
83
+ }
12
84
  }
13
85
  _alchemy.destroy = destroy;
14
86
  _alchemy.run = run;
15
87
  _alchemy.scope = scope;
16
88
  _alchemy.secret = secret;
17
- export const alchemy = _alchemy;
18
89
  /**
19
90
  * Enter a new scope synchronously.
20
91
  * @param options
@@ -35,7 +106,7 @@ async function run(...args) {
35
106
  const [id, options, fn] = typeof args[1] === "function"
36
107
  ? [args[0], undefined, args[1]]
37
108
  : args;
38
- await using scope = alchemy.scope(id, options);
109
+ const scope = alchemy.scope(id, options);
39
110
  try {
40
111
  return await fn.bind(scope)(scope);
41
112
  }
@@ -43,6 +114,11 @@ async function run(...args) {
43
114
  if (!(error instanceof DestroyedSignal)) {
44
115
  scope.fail();
45
116
  }
117
+ else {
118
+ }
46
119
  throw error;
47
120
  }
121
+ finally {
122
+ await scope.finalize();
123
+ }
48
124
  }
package/lib/apply.d.ts CHANGED
@@ -3,4 +3,4 @@ export interface ApplyOptions {
3
3
  quiet?: boolean;
4
4
  alwaysUpdate?: boolean;
5
5
  }
6
- export declare function apply<Out extends Resource>(resource: PendingResource<Out>, props: ResourceProps, options?: ApplyOptions): Promise<Awaited<Out>>;
6
+ export declare function apply<Out extends Resource>(resource: PendingResource<Out>, props: ResourceProps | undefined, options?: ApplyOptions): Promise<Awaited<Out>>;
package/lib/apply.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import { alchemy } from "./alchemy";
2
2
  import { context } from "./context";
3
3
  import { PROVIDERS, } from "./resource";
4
+ import { Secret } from "./secret";
4
5
  import { serialize } from "./util/serde";
5
6
  export async function apply(resource, props, options) {
6
7
  const scope = resource.Scope;
7
8
  try {
8
- const quiet = props.quiet ?? scope.quiet;
9
+ const quiet = props?.quiet ?? scope.quiet;
9
10
  await scope.init();
10
11
  let state = (await scope.state.get(resource.ID));
11
12
  const provider = PROVIDERS.get(resource.Kind);
@@ -35,15 +36,18 @@ export async function apply(resource, props, options) {
35
36
  const alwaysUpdate = options?.alwaysUpdate ?? provider.options?.alwaysUpdate ?? false;
36
37
  // Skip update if inputs haven't changed and resource is in a stable state
37
38
  if (state.status === "created" || state.status === "updated") {
38
- if (JSON.stringify(state.props) ===
39
- JSON.stringify(await serialize(scope, props)) &&
40
- alwaysUpdate !== true) {
39
+ const oldProps = JSON.stringify(state.props, (_, value) => value instanceof Secret
40
+ ? {
41
+ "@secret": value.unencrypted,
42
+ }
43
+ : value, 2);
44
+ const newProps = JSON.stringify(await serialize(scope, props, {
45
+ encrypt: false,
46
+ }), null, 2);
47
+ if (oldProps === newProps && alwaysUpdate !== true) {
41
48
  if (!quiet) {
42
49
  console.log(`Skip: "${resource.FQN}" (no changes)`);
43
50
  }
44
- // if (resourceState.output !== undefined) {
45
- // resource[Provide](resourceState.output);
46
- // }
47
51
  return state.output;
48
52
  }
49
53
  }
@@ -63,6 +67,7 @@ export async function apply(resource, props, options) {
63
67
  id: resource.ID,
64
68
  fqn: resource.FQN,
65
69
  seq: resource.Seq,
70
+ props: state.oldProps,
66
71
  state,
67
72
  replace: () => {
68
73
  if (isReplaced) {
@@ -72,9 +77,7 @@ export async function apply(resource, props, options) {
72
77
  isReplaced = true;
73
78
  },
74
79
  });
75
- const output = await alchemy.run(resource.ID, async () => {
76
- return provider.handler.bind(ctx)(resource.ID, props);
77
- });
80
+ const output = await alchemy.run(resource.ID, async () => provider.handler.bind(ctx)(resource.ID, props));
78
81
  if (!quiet) {
79
82
  console.log(`${phase === "create" ? "Created" : "Updated"}: "${resource.FQN}"`);
80
83
  }
@@ -15,4 +15,4 @@ export interface Bucket extends Resource<"s3::Bucket">, BucketProps {
15
15
  versioningEnabled?: boolean;
16
16
  acl?: string;
17
17
  }
18
- export declare const Bucket: ((this: Context<Bucket>, id: string, props: BucketProps) => Promise<Bucket>) | (((this: any, id: string, props: BucketProps) => never) & (new (_: never) => never));
18
+ export declare const Bucket: ((this: Context<Bucket>, id: string, props: BucketProps) => Promise<Bucket>) | (((this: any, id: string, props?: BucketProps | undefined) => never) & (new (_: never) => never));
@@ -47,4 +47,4 @@ export interface Function extends Resource<"lambda::Function">, FunctionProps {
47
47
  signingProfileVersionArn?: string;
48
48
  signingJobArn?: string;
49
49
  }
50
- export declare const Function: ((this: Context<Function>, id: string, props: FunctionProps) => Promise<Function>) | (((this: any, id: string, props: FunctionProps) => never) & (new (_: never) => never));
50
+ export declare const Function: ((this: Context<Function>, id: string, props: FunctionProps) => Promise<Function>) | (((this: any, id: string, props?: FunctionProps | undefined) => never) & (new (_: never) => never));
@@ -55,4 +55,4 @@ export interface OIDCProvider extends Resource<"aws::OIDCProvider">, OIDCProvide
55
55
  /**
56
56
  * Resource for configuring AWS OIDC provider for GitHub Actions
57
57
  */
58
- export declare const OIDCProvider: ((this: Context<OIDCProvider>, id: string, props: OIDCProviderProps) => Promise<OIDCProvider>) | (((this: any, id: string, props: OIDCProviderProps) => never) & (new (_: never) => never));
58
+ export declare const OIDCProvider: ((this: Context<OIDCProvider>, id: string, props: OIDCProviderProps) => Promise<OIDCProvider>) | (((this: any, id: string, props?: OIDCProviderProps | undefined) => never) & (new (_: never) => never));
@@ -6,4 +6,4 @@ export interface PolicyAttachmentProps {
6
6
  }
7
7
  export interface PolicyAttachment extends Resource<"iam::PolicyAttachment">, PolicyAttachmentProps {
8
8
  }
9
- export declare const PolicyAttachment: ((this: Context<PolicyAttachment>, id: string, props: PolicyAttachmentProps) => Promise<PolicyAttachment>) | (((this: any, id: string, props: PolicyAttachmentProps) => never) & (new (_: never) => never));
9
+ export declare const PolicyAttachment: ((this: Context<PolicyAttachment>, id: string, props: PolicyAttachmentProps) => Promise<PolicyAttachment>) | (((this: any, id: string, props?: PolicyAttachmentProps | undefined) => never) & (new (_: never) => never));
@@ -31,4 +31,4 @@ export interface Policy extends Resource<"iam::Policy">, PolicyProps {
31
31
  updateDate: Date;
32
32
  isAttachable: boolean;
33
33
  }
34
- export declare const Policy: ((this: Context<Policy>, id: string, props: PolicyProps) => Promise<Policy>) | (((this: any, id: string, props: PolicyProps) => never) & (new (_: never) => never));
34
+ export declare const Policy: ((this: Context<Policy>, id: string, props: PolicyProps) => Promise<Policy>) | (((this: any, id: string, props?: PolicyProps | undefined) => never) & (new (_: never) => never));
@@ -29,4 +29,4 @@ export interface Queue extends Resource<"sqs::Queue">, QueueProps {
29
29
  arn: string;
30
30
  url: string;
31
31
  }
32
- export declare const Queue: ((this: Context<Queue>, id: string, props: QueueProps) => Promise<Queue>) | (((this: any, id: string, props: QueueProps) => never) & (new (_: never) => never));
32
+ export declare const Queue: ((this: Context<Queue>, id: string, props: QueueProps) => Promise<Queue>) | (((this: any, id: string, props?: QueueProps | undefined) => never) & (new (_: never) => never));
package/lib/aws/role.d.ts CHANGED
@@ -21,4 +21,4 @@ export interface Role extends Resource<"iam::Role">, RoleProps {
21
21
  roleId: string;
22
22
  createDate: Date;
23
23
  }
24
- export declare const Role: ((this: Context<Role>, id: string, props: RoleProps) => Promise<Role>) | (((this: any, id: string, props: RoleProps) => never) & (new (_: never) => never));
24
+ export declare const Role: ((this: Context<Role>, id: string, props: RoleProps) => Promise<Role>) | (((this: any, id: string, props?: RoleProps | undefined) => never) & (new (_: never) => never));
package/lib/aws/ses.d.ts CHANGED
@@ -66,4 +66,4 @@ export interface SES extends Resource<"aws::SES">, SESProps {
66
66
  /**
67
67
  * AWS SES resource for creating and configuring Simple Email Service
68
68
  */
69
- export declare const SES: ((this: Context<SES>, id: string, props: SESProps) => Promise<SES>) | (((this: any, id: string, props: SESProps) => never) & (new (_: never) => never));
69
+ export declare const SES: ((this: Context<SES>, id: string, props: SESProps) => Promise<SES>) | (((this: any, id: string, props?: SESProps | undefined) => never) & (new (_: never) => never));
@@ -20,4 +20,4 @@ export interface Table extends Resource<"dynamo::Table">, TableProps {
20
20
  streamArn?: string;
21
21
  tableId: string;
22
22
  }
23
- export declare const Table: ((this: Context<Table>, id: string, props: TableProps) => Promise<Table>) | (((this: any, id: string, props: TableProps) => never) & (new (_: never) => never));
23
+ export declare const Table: ((this: Context<Table>, id: string, props: TableProps) => Promise<Table>) | (((this: any, id: string, props?: TableProps | undefined) => never) & (new (_: never) => never));
@@ -43,4 +43,4 @@ export interface R2Bucket extends Resource<"cloudflare::R2Bucket">, BucketProps
43
43
  */
44
44
  creationDate: number;
45
45
  }
46
- export declare const R2Bucket: ((this: Context<R2Bucket>, id: string, props: BucketProps) => Promise<R2Bucket>) | (((this: any, id: string, props: BucketProps) => never) & (new (_: never) => never));
46
+ export declare const R2Bucket: ((this: Context<R2Bucket>, id: string, props: BucketProps) => Promise<R2Bucket>) | (((this: any, id: string, props?: BucketProps | undefined) => never) & (new (_: never) => never));
@@ -58,4 +58,4 @@ export interface KVNamespace extends Resource<"cloudflare::KVNamespace">, KVName
58
58
  */
59
59
  modifiedAt: number;
60
60
  }
61
- export declare const KVNamespace: ((this: Context<KVNamespace>, id: string, props: KVNamespaceProps) => Promise<KVNamespace>) | (((this: any, id: string, props: KVNamespaceProps) => never) & (new (_: never) => never));
61
+ export declare const KVNamespace: ((this: Context<KVNamespace>, id: string, props: KVNamespaceProps) => Promise<KVNamespace>) | (((this: any, id: string, props?: KVNamespaceProps | undefined) => never) & (new (_: never) => never));
@@ -157,4 +157,4 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
157
157
  */
158
158
  routes?: Record<string, string>;
159
159
  }
160
- export declare const StaticSite: ((this: Context<StaticSite>, id: string, props: StaticSiteProps) => Promise<StaticSite>) | (((this: any, id: string, props: StaticSiteProps) => never) & (new (_: never) => never));
160
+ export declare const StaticSite: ((this: Context<StaticSite>, id: string, props: StaticSiteProps) => Promise<StaticSite>) | (((this: any, id: string, props?: StaticSiteProps | undefined) => never) & (new (_: never) => never));
@@ -105,4 +105,4 @@ export interface Worker<B extends Bindings = Bindings> extends Resource<"cloudfl
105
105
  [bindingName in keyof B]: Bound<B[bindingName]>;
106
106
  };
107
107
  }
108
- export declare const Worker: (<const B extends Bindings>(this: Context<Worker<NoInfer<B>>>, id: string, props: WorkerProps<B>) => Promise<Worker<B>>) | (((this: any, id: string, props: WorkerProps<Bindings>) => never) & (new (_: never) => never));
108
+ export declare const Worker: (<const B extends Bindings>(this: Context<Worker<NoInfer<B>>>, id: string, props: WorkerProps<B>) => Promise<Worker<B>>) | (((this: any, id: string, props?: WorkerProps<Bindings> | undefined) => never) & (new (_: never) => never));
@@ -240,4 +240,4 @@ export interface WranglerJson extends Resource<"cloudflare::WranglerJson">, Wran
240
240
  /**
241
241
  * Resource for managing wrangler.json configuration files
242
242
  */
243
- export declare const WranglerJson: ((this: Context<WranglerJson>, id: string, props: WranglerJsonProps) => Promise<WranglerJson>) | (((this: any, id: string, props: WranglerJsonProps) => never) & (new (_: never) => never));
243
+ export declare const WranglerJson: ((this: Context<WranglerJson>, id: string, props: WranglerJsonProps) => Promise<WranglerJson>) | (((this: any, id: string, props?: WranglerJsonProps | undefined) => never) & (new (_: never) => never));
@@ -193,4 +193,4 @@ export interface Zone extends Resource<"cloudflare::Zone"> {
193
193
  minTlsVersion: MinTLSVersionValue;
194
194
  };
195
195
  }
196
- export declare const Zone: ((this: Context<Zone>, id: string, props: ZoneProps) => Promise<Zone>) | (((this: any, id: string, props: ZoneProps) => never) & (new (_: never) => never));
196
+ export declare const Zone: ((this: Context<Zone>, id: string, props: ZoneProps) => Promise<Zone>) | (((this: any, id: string, props?: ZoneProps | undefined) => never) & (new (_: never) => never));
package/lib/context.d.ts CHANGED
@@ -1,18 +1,21 @@
1
1
  import type { Resource, ResourceFQN, ResourceID, ResourceKind, ResourceProps } from "./resource";
2
2
  import type { Scope } from "./scope";
3
3
  import type { State } from "./state";
4
- export type Context<Out extends Resource> = CreateContext<Out> | UpdateContext<Out> | DeleteContext<Out>;
4
+ export type Context<Out extends Resource, Props extends ResourceProps = ResourceProps> = CreateContext<Out> | UpdateContext<Out, Props> | DeleteContext<Out, Props>;
5
5
  export interface CreateContext<Out extends Resource> extends BaseContext<Out> {
6
6
  phase: "create";
7
7
  output?: undefined;
8
+ props?: undefined;
8
9
  }
9
- export interface UpdateContext<Out extends Resource> extends BaseContext<Out> {
10
+ export interface UpdateContext<Out extends Resource, Props extends ResourceProps = ResourceProps> extends BaseContext<Out> {
10
11
  phase: "update";
11
12
  output: Out;
13
+ props: Props;
12
14
  }
13
- export interface DeleteContext<Out extends Resource> extends BaseContext<Out> {
15
+ export interface DeleteContext<Out extends Resource, Props extends ResourceProps = ResourceProps> extends BaseContext<Out> {
14
16
  phase: "delete";
15
17
  output: Out;
18
+ props: Props;
16
19
  }
17
20
  export interface BaseContext<Out extends Resource> {
18
21
  quiet: boolean;
@@ -44,15 +47,17 @@ export interface BaseContext<Out extends Resource> {
44
47
  /**
45
48
  * Create the Resource envelope (with Alchemy + User properties)
46
49
  */
50
+ (id: string, props: Omit<Out, keyof Resource>): Out;
47
51
  (props: Omit<Out, keyof Resource>): Out;
48
52
  }
49
- export declare function context<Kind extends string, Props extends ResourceProps, Out extends Resource>({ scope, phase, kind, id, fqn, seq, state, replace, }: {
53
+ export declare function context<Kind extends string, Props extends ResourceProps | undefined, Out extends Resource>({ scope, phase, kind, id, fqn, seq, state, replace, }: {
50
54
  scope: Scope;
51
55
  phase: "create" | "update" | "delete";
52
56
  kind: ResourceKind;
53
57
  id: ResourceID;
54
58
  fqn: ResourceFQN;
55
59
  seq: number;
60
+ props: Props;
56
61
  state: State<Kind, Props, Out>;
57
62
  replace: () => void;
58
63
  }): Context<Out>;
package/lib/context.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { DestroyedSignal } from "./destroy";
2
2
  export function context({ scope, phase, kind, id, fqn, seq, state, replace, }) {
3
- function create(props) {
3
+ function create(...args) {
4
+ const [ID, props] = typeof args[0] === "string" ? args : [id, args[0]];
4
5
  return {
5
6
  ...props,
6
7
  Kind: kind,
7
- ID: id,
8
+ ID,
8
9
  FQN: fqn,
9
10
  Scope: scope,
10
11
  Seq: seq,
@@ -17,6 +18,7 @@ export function context({ scope, phase, kind, id, fqn, seq, state, replace, }) {
17
18
  fqn: fqn,
18
19
  phase,
19
20
  output: state.output,
21
+ props: state.props,
20
22
  replace,
21
23
  get: (key) => state.data[key],
22
24
  set: async (key, value) => {
package/lib/destroy.js CHANGED
@@ -12,12 +12,13 @@ function isScopeArgs(a) {
12
12
  */
13
13
  export async function destroy(...args) {
14
14
  if (isScopeArgs(args)) {
15
- const [scope, options] = args;
16
- const strategy = options?.strategy ?? "sequential";
15
+ const [scope] = args;
16
+ const options = {
17
+ strategy: "sequential",
18
+ ...(args[1] ?? {}),
19
+ };
17
20
  // destroy all active resources
18
- await destroy.all(Array.from(scope.resources.values()), {
19
- strategy,
20
- });
21
+ await destroy.all(Array.from(scope.resources.values()), options);
21
22
  // then detect orphans and destroy them
22
23
  const orphans = await scope.state.all();
23
24
  await destroy.all(Object.values(orphans).map((orphan) => ({
@@ -45,9 +46,8 @@ export async function destroy(...args) {
45
46
  if (!quiet) {
46
47
  console.log(`Delete: "${instance.FQN}"`);
47
48
  }
48
- const state = (await scope.state.get(instance.ID));
49
+ const state = await scope.state.get(instance.ID);
49
50
  if (state === undefined) {
50
- console.warn(`Resource "${instance.FQN}" not found`);
51
51
  return;
52
52
  }
53
53
  const ctx = context({
@@ -57,6 +57,7 @@ export async function destroy(...args) {
57
57
  id: instance.ID,
58
58
  fqn: instance.FQN,
59
59
  seq: instance.Seq,
60
+ props: state.props,
60
61
  state,
61
62
  replace: () => {
62
63
  throw new Error("Cannot replace a resource that is being deleted");
@@ -0,0 +1,123 @@
1
+ import type { Context } from "../context";
2
+ import { Resource } from "../resource";
3
+ import type { Secret } from "../secret";
4
+ /**
5
+ * Base properties for a Document
6
+ */
7
+ interface BaseDocumentProps {
8
+ /**
9
+ * Path to the markdown document
10
+ */
11
+ path: string;
12
+ /**
13
+ * Base URL for the OpenAI API
14
+ * @default 'https://api.openai.com/v1'
15
+ */
16
+ baseURL?: string;
17
+ }
18
+ /**
19
+ * Properties for creating a document with static content
20
+ */
21
+ interface StaticDocumentProps extends BaseDocumentProps {
22
+ /**
23
+ * Static content of the document
24
+ */
25
+ content: string;
26
+ /**
27
+ * Prompt for generating content
28
+ * @internal This is marked as never to ensure content and prompt are mutually exclusive
29
+ */
30
+ prompt?: never;
31
+ /**
32
+ * OpenAI API key
33
+ * @internal This is marked as never since it's not needed for static content
34
+ */
35
+ apiKey?: never;
36
+ /**
37
+ * Model configuration
38
+ * @internal This is marked as never since it's not needed for static content
39
+ */
40
+ model?: never;
41
+ /**
42
+ * Context files
43
+ * @internal This is marked as never since it's not needed for static content
44
+ */
45
+ context?: never;
46
+ }
47
+ /**
48
+ * Properties for creating a document with AI-generated content
49
+ */
50
+ interface GeneratedDocumentProps extends BaseDocumentProps {
51
+ /**
52
+ * Static content of the document
53
+ * @internal This is marked as never to ensure content and prompt are mutually exclusive
54
+ */
55
+ content?: never;
56
+ /**
57
+ * Prompt for generating content
58
+ * Can be either a single string or an array of strings that will be joined with newlines
59
+ */
60
+ prompt: string | string[];
61
+ /**
62
+ * Optional context files to include in the prompt
63
+ * Key is the relative path to the file
64
+ * Value can be either the content string or another Document resource
65
+ */
66
+ context?: {
67
+ [relativePath: string]: string | Document | {
68
+ path: string;
69
+ content: string;
70
+ };
71
+ };
72
+ /**
73
+ * OpenAI API key to use for generating content
74
+ * If not provided, will use OPENAI_API_KEY environment variable
75
+ */
76
+ apiKey?: Secret;
77
+ /**
78
+ * Model configuration
79
+ */
80
+ model?: {
81
+ /**
82
+ * Model ID to use
83
+ * @default 'gpt-4o'
84
+ */
85
+ id?: string;
86
+ /**
87
+ * Model provider name
88
+ * @default 'openai'
89
+ */
90
+ provider?: string;
91
+ /**
92
+ * Model-specific options
93
+ */
94
+ options?: Record<string, unknown>;
95
+ };
96
+ }
97
+ /**
98
+ * Properties for creating or updating a Document
99
+ * Must provide either static content or a prompt for AI generation
100
+ */
101
+ export type DocumentProps = StaticDocumentProps | GeneratedDocumentProps;
102
+ /**
103
+ * A markdown document that can be created, updated, and deleted
104
+ */
105
+ export interface Document extends Omit<DocumentProps, "content" | "prompt" | "context">, Resource<"docs::Document"> {
106
+ /**
107
+ * Content of the document
108
+ */
109
+ content: string;
110
+ /**
111
+ * Time at which the document was created
112
+ */
113
+ createdAt: number;
114
+ /**
115
+ * Time at which the document was last updated
116
+ */
117
+ updatedAt: number;
118
+ }
119
+ /**
120
+ * Resource for managing markdown documents using the Vercel AI SDK
121
+ */
122
+ export declare const Document: ((this: Context<Document, DocumentProps>, id: string, props: DocumentProps) => Promise<Document>) | (((this: any, id: string, props?: DocumentProps | undefined) => never) & (new (_: never) => never));
123
+ export {};
@@ -0,0 +1,67 @@
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 { Resource } from "../resource";
6
+ /**
7
+ * Resource for managing markdown documents using the Vercel AI SDK
8
+ */
9
+ export const Document = Resource("docs::Document", async function (id, props) {
10
+ // Ensure directory exists
11
+ await fs.mkdir(path.dirname(props.path), { recursive: true });
12
+ if (this.phase === "delete") {
13
+ try {
14
+ await fs.unlink(props.path);
15
+ }
16
+ catch (error) {
17
+ // Ignore if file doesn't exist
18
+ if (error.code !== "ENOENT") {
19
+ throw error;
20
+ }
21
+ }
22
+ return this.destroy();
23
+ }
24
+ let content;
25
+ console.log("Generating content for", props.path);
26
+ // Generate content if prompt is provided
27
+ if ("prompt" in props) {
28
+ // Get API key from props or environment
29
+ const apiKey = props.apiKey?.unencrypted || process.env.OPENAI_API_KEY;
30
+ if (!apiKey) {
31
+ throw new Error("OpenAI API key is required for content generation");
32
+ }
33
+ // Initialize OpenAI compatible provider
34
+ const provider = createOpenAICompatible({
35
+ name: props.model?.provider || "openai",
36
+ apiKey,
37
+ baseURL: props.baseURL || "https://api.openai.com/v1",
38
+ });
39
+ // Build the full prompt with context if provided
40
+ const fullPrompt = Array.isArray(props.prompt)
41
+ ? props.prompt.join("\n")
42
+ : props.prompt;
43
+ // console.log(fullPrompt);
44
+ const { text } = await generateText({
45
+ model: provider(props.model?.id || "gpt-4o"),
46
+ prompt: fullPrompt,
47
+ ...(props.model?.options || {}),
48
+ });
49
+ content = text;
50
+ }
51
+ else {
52
+ // Use static content
53
+ content = props.content;
54
+ }
55
+ // Write content to file
56
+ await fs.writeFile(props.path, content);
57
+ // Get file stats for timestamps
58
+ const stats = await fs.stat(props.path);
59
+ // Return the resource
60
+ return this({
61
+ path: props.path,
62
+ baseURL: props.baseURL,
63
+ content,
64
+ createdAt: stats.birthtimeMs,
65
+ updatedAt: stats.mtimeMs,
66
+ });
67
+ });
@@ -0,0 +1 @@
1
+ export * from "./document";
@@ -0,0 +1 @@
1
+ export * from "./document";
@@ -53,7 +53,7 @@ export interface Bundle extends Resource<"esbuild::Bundle"> {
53
53
  */
54
54
  hash: string;
55
55
  }
56
- export declare const Bundle: ((this: Context<Bundle>, id: string, props: BundleProps) => Promise<Bundle>) | (((this: any, id: string, props: BundleProps) => never) & (new (_: never) => never));
56
+ export declare const Bundle: ((this: Context<Bundle>, id: string, props: BundleProps) => Promise<Bundle>) | (((this: any, id: string, props?: BundleProps | undefined) => never) & (new (_: never) => never));
57
57
  export declare function bundle(props: BundleProps): Promise<esbuild.BuildResult<{
58
58
  bundle: boolean;
59
59
  splitting?: boolean;