Skip to content

Commit ddf5508

Browse files
authored
feat: add automatic CLI argument parsing to alchemy() function (#356)
1 parent b57cbc1 commit ddf5508

File tree

13 files changed

+216
-45
lines changed

13 files changed

+216
-45
lines changed

alchemy-web/docs/guides/cli.md

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
---
2+
order: 7
3+
title: CLI Arguments
4+
description: Learn how Alchemy automatically parses CLI arguments for common operations like destroy, read, quiet mode, and staging without requiring a traditional CLI tool.
5+
---
6+
7+
# CLI Arguments
8+
9+
Alchemy doesn't have a traditional CLI tool like `wrangler` or `terraform` because it's designed to be an embeddable TypeScript library. Instead, it provides automatic CLI argument parsing when you initialize an alchemy application, making it easy to run your infrastructure scripts with common options.
10+
11+
## No CLI, but CLI Arguments
12+
13+
Rather than building a separate CLI tool, Alchemy automatically parses CLI arguments when you call:
14+
15+
```ts
16+
const app = await alchemy("my-app");
17+
```
18+
19+
This design choice keeps Alchemy simple while still providing the convenience of CLI arguments for common operations.
20+
21+
## Supported Arguments
22+
23+
### Phase Control
24+
25+
Control what phase your infrastructure script runs in:
26+
27+
```sh
28+
# Deploy/update resources (default)
29+
bun ./alchemy.run
30+
31+
# Read-only mode - doesn't modify resources
32+
bun ./alchemy.run --read
33+
34+
# Destroy all resources
35+
bun ./alchemy.run --destroy
36+
```
37+
38+
Learn more about phases in the [phase concepts guide](../concepts/phase.md).
39+
40+
### Output Control
41+
42+
Control logging output:
43+
44+
```sh
45+
# Quiet mode - suppress Create/Update/Delete messages
46+
bun ./alchemy.run --quiet
47+
```
48+
49+
### Environment Control
50+
51+
Specify which stage/environment to target:
52+
53+
```sh
54+
# Deploy to a specific stage
55+
bun ./alchemy.run --stage production
56+
bun ./alchemy.run --stage staging
57+
bun ./alchemy.run --stage dev
58+
```
59+
60+
By default, the stage is set to `process.env.USER` (your username). You can also use `ALCHEMY_STAGE` or `PASSWORD` environment variables to control staging behavior.
61+
62+
### Secret Management
63+
64+
Provide encryption password via environment variable:
65+
66+
```sh
67+
# Set password for encrypting/decrypting secrets
68+
ALCHEMY_PASSWORD=my-secret-key bun ./alchemy.run
69+
```
70+
71+
## How It Works
72+
73+
When you call `alchemy("my-app")`, it automatically:
74+
75+
1. Parses `process.argv` for supported arguments
76+
2. Merges CLI options with any explicit options you provide
77+
3. Explicit options always take precedence over CLI arguments
78+
79+
```ts
80+
// CLI args are parsed automatically
81+
const app = await alchemy("my-app");
82+
83+
// Explicit options override CLI args
84+
const app = await alchemy("my-app", {
85+
phase: "up", // This overrides --destroy or --read
86+
stage: "prod", // This overrides --stage
87+
quiet: false, // This overrides --quiet
88+
});
89+
```
90+
91+
## Environment Variables
92+
93+
Alchemy also supports these environment variables:
94+
95+
- `ALCHEMY_PASSWORD` - Password for encrypting/decrypting secrets
96+
- `ALCHEMY_STAGE` - Default stage name
97+
- `USER` - Fallback for stage name (uses your username)
98+
99+
## Complete Example
100+
101+
Here's how you might use CLI arguments in practice:
102+
103+
```ts
104+
// alchemy.run.ts
105+
import alchemy from "alchemy";
106+
import { Worker } from "alchemy/cloudflare";
107+
108+
const app = await alchemy("my-app");
109+
110+
const worker = await Worker("api", {
111+
script: "export default { fetch() { return new Response('Hello'); } }",
112+
});
113+
114+
console.log({ url: worker.url });
115+
116+
await app.finalize();
117+
```
118+
119+
Deploy commands:
120+
121+
::: code-group
122+
123+
```sh [Deploy]
124+
bun ./alchemy.run
125+
```
126+
127+
```sh [Deploy to Production]
128+
bun ./alchemy.run --stage production
129+
```
130+
131+
```sh [Read-only Check]
132+
bun ./alchemy.run --read
133+
```
134+
135+
```sh [Quiet Deploy]
136+
bun ./alchemy.run --quiet
137+
```
138+
139+
```sh [Destroy Everything]
140+
bun ./alchemy.run --destroy
141+
```
142+
143+
```sh [Deploy with Secrets]
144+
ALCHEMY_PASSWORD=my-secret-key bun ./alchemy.run
145+
```
146+
147+
:::
148+

alchemy/src/alchemy.ts

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,39 @@ import { logger } from "./util/logger.ts";
1919
import { TelemetryClient } from "./util/telemetry/client.ts";
2020
import type { LoggerApi } from "./util/cli.ts";
2121

22+
/**
23+
* Parses CLI arguments to extract alchemy options
24+
*/
25+
function parseCliArgs(): Partial<AlchemyOptions> {
26+
const args = process.argv.slice(2);
27+
const options: Partial<AlchemyOptions> = {};
28+
29+
// Parse phase from CLI arguments
30+
if (args.includes("--destroy")) {
31+
options.phase = "destroy";
32+
} else if (args.includes("--read")) {
33+
options.phase = "read";
34+
}
35+
36+
// Parse quiet flag
37+
if (args.includes("--quiet")) {
38+
options.quiet = true;
39+
}
40+
41+
// Parse stage argument (--stage my-stage)
42+
const stageIndex = args.indexOf("--stage");
43+
if (stageIndex !== -1 && stageIndex + 1 < args.length) {
44+
options.stage = args[stageIndex + 1];
45+
}
46+
47+
// Get password from environment variables
48+
if (process.env.ALCHEMY_PASSWORD) {
49+
options.password = process.env.ALCHEMY_PASSWORD;
50+
}
51+
52+
return options;
53+
}
54+
2255
/**
2356
* Type alias for semantic highlighting of `alchemy` as a type keyword
2457
*/
@@ -29,9 +62,16 @@ export const alchemy: Alchemy = _alchemy as any;
2962
/**
3063
* The Alchemy interface provides core functionality and is augmented by providers.
3164
* Supports both application scoping with secrets and template string interpolation.
65+
* Automatically parses CLI arguments for common options.
3266
*
3367
* @example
34-
* // Create an application scope with stage and secret handling
68+
* // Simple usage with automatic CLI argument parsing
69+
* const app = await alchemy("my-app");
70+
* // Now supports: --destroy, --read, --quiet, --stage my-stage
71+
* // Environment variables: PASSWORD, ALCHEMY_PASSWORD, ALCHEMY_STAGE, USER
72+
*
73+
* @example
74+
* // Create an application scope with explicit options (overrides CLI args)
3575
* const app = await alchemy("github:alchemy", {
3676
* stage: "prod",
3777
* phase: "up",
@@ -69,8 +109,15 @@ export interface Alchemy {
69109
/**
70110
* Creates a new application scope with the given name and options.
71111
* Used to create and manage resources with proper secret handling.
112+
* Automatically parses CLI arguments: --destroy, --read, --quiet, --stage <name>
113+
* Environment variables: PASSWORD, ALCHEMY_PASSWORD, ALCHEMY_STAGE, USER
114+
*
115+
* @example
116+
* // Simple usage with CLI argument parsing
117+
* const app = await alchemy("my-app");
72118
*
73119
* @example
120+
* // With explicit options (overrides CLI args)
74121
* const app = await alchemy("my-app", {
75122
* stage: "prod",
76123
* // Required for encrypting/decrypting secrets
@@ -115,20 +162,29 @@ async function _alchemy(
115162
): Promise<Scope | string | never> {
116163
if (typeof args[0] === "string") {
117164
const [appName, options] = args as [string, AlchemyOptions?];
118-
const phase = isRuntime ? "read" : (options?.phase ?? "up");
165+
166+
// Parse CLI arguments and merge with provided options (explicit options take precedence)
167+
const cliOptions = parseCliArgs();
168+
const mergedOptions = {
169+
...cliOptions,
170+
...options,
171+
};
172+
173+
const phase = isRuntime ? "read" : (mergedOptions?.phase ?? "up");
119174
const telemetryClient =
120-
options?.parent?.telemetryClient ??
175+
mergedOptions?.parent?.telemetryClient ??
121176
TelemetryClient.create({
122177
phase,
123-
enabled: options?.telemetry ?? true,
124-
quiet: options?.quiet ?? false,
178+
enabled: mergedOptions?.telemetry ?? true,
179+
quiet: mergedOptions?.quiet ?? false,
125180
});
126181
const root = new Scope({
127-
...options,
182+
...mergedOptions,
128183
appName,
129-
stage: options?.stage ?? process.env.ALCHEMY_STAGE,
184+
stage:
185+
mergedOptions?.stage ?? process.env.ALCHEMY_STAGE ?? process.env.USER,
130186
phase,
131-
password: options?.password ?? process.env.ALCHEMY_PASSWORD,
187+
password: mergedOptions?.password ?? process.env.ALCHEMY_PASSWORD,
132188
telemetryClient,
133189
});
134190
try {
@@ -138,7 +194,7 @@ async function _alchemy(
138194
// see Scope.finalize for where we pop the global scope
139195
Scope.globals.push(root);
140196
}
141-
if (options?.phase === "destroy") {
197+
if (mergedOptions?.phase === "destroy") {
142198
await destroy(root);
143199
return process.exit(0);
144200
}

examples/aws-app/alchemy.run.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,6 @@ import path from "node:path";
66
import { fileURLToPath } from "node:url";
77

88
const app = await alchemy("aws-app", {
9-
// decide the mode/stage however you want
10-
phase: process.argv[2] === "destroy" ? "destroy" : "up",
11-
stage: process.argv[3],
12-
quiet: process.argv.includes("--quiet"),
139
stateStore:
1410
process.env.ALCHEMY_STATE_STORE === "cloudflare"
1511
? (scope) => new DOStateStore(scope)

examples/cloudflare-astro/alchemy.run.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import { Astro, DOStateStore } from "alchemy/cloudflare";
33

44
const BRANCH_PREFIX = process.env.BRANCH_PREFIX ?? "";
55
const app = await alchemy("cloudflare-astro", {
6-
stage: process.env.USER ?? "dev",
7-
phase: process.argv.includes("--destroy") ? "destroy" : "up",
8-
password: process.env.ALCHEMY_PASSWORD,
96
stateStore:
107
process.env.ALCHEMY_STATE_STORE === "cloudflare"
118
? (scope) => new DOStateStore(scope)

examples/cloudflare-nuxt-pipeline/alchemy.run.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ import { DOStateStore, Nuxt, Pipeline, R2Bucket } from "alchemy/cloudflare";
44
const BRANCH_PREFIX = process.env.BRANCH_PREFIX ?? "";
55

66
const app = await alchemy("cloudflare-nuxt-pipeline", {
7-
stage: process.env.USER ?? "dev",
8-
phase: process.argv.includes("--destroy") ? "destroy" : "up",
9-
quiet: !process.argv.includes("--verbose"),
10-
password: process.env.SECRET_PASSPHRASE,
117
stateStore:
128
process.env.ALCHEMY_STATE_STORE === "cloudflare"
139
? (scope) => new DOStateStore(scope)

examples/cloudflare-react-router/alchemy.run.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,6 @@ import { DOStateStore, ReactRouter } from "alchemy/cloudflare";
55

66
const BRANCH_PREFIX = process.env.BRANCH_PREFIX ?? "";
77
const app = await alchemy("cloudflare-react-router", {
8-
stage: process.env.USER ?? "dev",
9-
phase: process.argv.includes("--destroy") ? "destroy" : "up",
10-
quiet: !process.argv.includes("--verbose"),
11-
password: process.env.ALCHEMY_PASSWORD,
128
stateStore:
139
process.env.ALCHEMY_STATE_STORE === "cloudflare"
1410
? (scope) => new DOStateStore(scope)

examples/cloudflare-redwood/alchemy.run.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { D1Database, DOStateStore, Redwood } from "alchemy/cloudflare";
44
const BRANCH_PREFIX = process.env.BRANCH_PREFIX ?? "";
55

66
const app = await alchemy("cloudflare-redwood", {
7-
phase: process.argv.includes("--destroy") ? "destroy" : "up",
87
stateStore:
98
process.env.ALCHEMY_STATE_STORE === "cloudflare"
109
? (scope) => new DOStateStore(scope)

examples/cloudflare-sveltekit/alchemy.run.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,6 @@ import {
88

99
const BRANCH_PREFIX = process.env.BRANCH_PREFIX ?? "";
1010
const app = await alchemy("cloudflare-sveltekit", {
11-
stage: process.env.USER ?? "dev",
12-
phase: process.argv.includes("--destroy") ? "destroy" : "up",
13-
quiet: !process.argv.includes("--verbose"),
14-
password: process.env.ALCHEMY_PASSWORD,
1511
stateStore:
1612
process.env.ALCHEMY_STATE_STORE === "cloudflare"
1713
? (scope) => new DOStateStore(scope)

examples/cloudflare-sveltekit/src/routes/+page.svelte

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
<script lang="ts">
22
import "../app.css";
3-
import type { PageData } from "./$types";
4-
5-
interface Props {
6-
data: PageData;
7-
}
8-
9-
let { data }: Props = $props();
3+
4+
let { data } = $props();
105

116
let getResponse = $state("");
127
let postResponse = $state("");

examples/cloudflare-tanstack-start/alchemy.run.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { DOStateStore, TanStackStart } from "alchemy/cloudflare";
44
const BRANCH_PREFIX = process.env.BRANCH_PREFIX ?? "";
55

66
const app = await alchemy("cloudflare-tanstack", {
7-
phase: process.argv.includes("--destroy") ? "destroy" : "up",
87
stateStore:
98
process.env.ALCHEMY_STATE_STORE === "cloudflare"
109
? (scope) => new DOStateStore(scope)

0 commit comments

Comments
 (0)