Skip to content

Commit 9c5dc7f

Browse files
ryan-williamsclaude
andcommitted
cross-runtime destroy: scan all runtimes, --name without config
- `findDestroyTargets()` probes Lambda + CF for named proxy - `destroyByRuntime()` / `destroyCfByName()` / `destroyLambdaByName()` work without full config - CLI: `--name`, `--runtime`, `--region` options; shows discovery summary before confirming - `gc` subcommand deferred Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f044287 commit 9c5dc7f

File tree

5 files changed

+145
-18
lines changed

5 files changed

+145
-18
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,12 @@ Discovers all `cors-prxy`-tagged resources (via `cors-prxy ls`) and cross-refere
4040
- CF cleanup: Worker name is deterministic (`cors-prxy-{name}`), delete via API
4141
- `destroy` should confirm before deleting (unless `--yes`), showing what will be removed
4242
- `destroy` should be idempotent — silently skip resources that don't exist
43+
44+
## Implementation (done)
45+
46+
- `findDestroyTargets()` in `deploy.ts`: probes both runtimes for the named proxy, returns `DestroyTarget[]` with runtime, name, and human-readable detail
47+
- `destroyByRuntime()` in `deploy.ts`: dispatches to `destroyCfByName()` or `destroyLambdaByName()`
48+
- `destroyCfByName()` in `deploy-cf.ts`: destroy by name without full config
49+
- `destroyLambdaByName()` in `deploy-lambda.ts`: destroy by name + region without full config
50+
- CLI `destroy` updated: `--name` (no config needed), `--runtime` filter, `--region` for Lambda, shows discovery summary before confirming
51+
- `gc` subcommand deferred (nice-to-have per spec)

src/cli.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,18 +140,46 @@ program
140140

141141
program
142142
.command("destroy")
143-
.description("Remove deployed proxy")
143+
.description("Remove deployed proxy (searches all runtimes by default)")
144144
.option("-c, --config <path>", "Config file path", ".cors-prxy.json")
145+
.option("-n, --name <name>", "Proxy name (no config file needed)")
146+
.option("--runtime <runtime>", "Only destroy specific runtime (lambda, cloudflare)")
147+
.option("--region <region>", "AWS region for Lambda lookup", "us-east-1")
145148
.option("-y, --yes", "Skip confirmation")
146149
.action(async (opts) => {
147-
const config = await loadConfig(opts.config)
148-
const runtime = resolveRuntime(config)
150+
const { findDestroyTargets, destroyByRuntime } = await import("./deploy.js")
151+
const runtimeFilter = opts.runtime as Runtime | undefined
152+
153+
let name: string
154+
let region: string
155+
let config: import("./config.js").CorsProxyConfig | undefined
156+
157+
if (opts.name) {
158+
name = opts.name
159+
region = opts.region
160+
} else {
161+
config = await loadConfig(opts.config)
162+
name = config.name
163+
region = config.region
164+
}
165+
166+
const targets = await findDestroyTargets(name, region, runtimeFilter)
167+
168+
if (targets.length === 0) {
169+
console.log(`No resources found for "${name}".`)
170+
return
171+
}
172+
173+
console.log(`Found resources for "${name}":`)
174+
for (const t of targets) {
175+
console.log(` ${t.detail}`)
176+
}
149177

150178
if (!opts.yes) {
151179
const readline = await import("node:readline")
152180
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
153181
const answer = await new Promise<string>(resolve => {
154-
rl.question(`Destroy proxy "${config.name}" (${runtime})? [y/N] `, resolve)
182+
rl.question(`\nDestroy all? [y/N] `, resolve)
155183
})
156184
rl.close()
157185
if (answer.toLowerCase() !== "y") {
@@ -160,8 +188,9 @@ program
160188
}
161189
}
162190

163-
const { destroy } = await import("./deploy.js")
164-
await destroy(config)
191+
for (const target of targets) {
192+
await destroyByRuntime(target.runtime, target.name, config)
193+
}
165194
console.log("Done.")
166195
})
167196

src/deploy-cf.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,9 +171,13 @@ export async function deployCf(config: CorsProxyConfig): Promise<DeployResult> {
171171
}
172172

173173
export async function destroyCf(config: CorsProxyConfig): Promise<void> {
174+
await destroyCfByName(config.name, config)
175+
}
176+
177+
export async function destroyCfByName(name: string, config?: CorsProxyConfig): Promise<void> {
174178
const { apiToken } = getCfAuth()
175179
const accountId = await resolveAccountId(apiToken, config)
176-
const workerName = getWorkerName(config)
180+
const workerName = config?.cloudflare?.workerName ?? `cors-prxy-${name}`
177181

178182
console.log(`Deleting Cloudflare Worker: ${workerName}`)
179183
const resp = await cfApi(

src/deploy-lambda.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,17 @@ export async function deployLambda(config: CorsProxyConfig): Promise<DeployResul
267267
}
268268

269269
export async function destroyLambda(config: CorsProxyConfig): Promise<void> {
270+
await destroyLambdaByName(config.name, config.region)
271+
}
272+
273+
export async function destroyLambdaByName(name: string, region: string): Promise<void> {
270274
const { DeleteFunctionCommand, DeleteFunctionUrlConfigCommand } = await import("@aws-sdk/client-lambda")
271275
const { DeleteRoleCommand, DeleteRolePolicyCommand } = await import("@aws-sdk/client-iam")
272276

273-
const lambda = new LambdaClient({ region: config.region })
274-
const iam = new IAMClient({ region: config.region })
275-
const functionName = config.name
276-
const roleName = `cors-prxy-${config.name}-role`
277+
const lambda = new LambdaClient({ region })
278+
const iam = new IAMClient({ region })
279+
const functionName = name
280+
const roleName = `cors-prxy-${name}-role`
277281

278282
// Delete Function URL
279283
try {

src/deploy.ts

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { resolveRuntime } from "./config.js"
2-
import type { CorsProxyConfig } from "./config.js"
2+
import type { CorsProxyConfig, Runtime } from "./config.js"
33
import type { DeployResult } from "./deploy-lambda.js"
44

55
export type { DeployResult }
@@ -14,12 +14,93 @@ export async function deploy(config: CorsProxyConfig): Promise<DeployResult> {
1414
return deployLambda(config)
1515
}
1616

17-
export async function destroy(config: CorsProxyConfig): Promise<void> {
18-
const runtime = resolveRuntime(config)
17+
export interface DestroyTarget {
18+
runtime: Runtime
19+
name: string
20+
detail: string
21+
}
22+
23+
/** Discover what resources exist for a given name across runtimes. */
24+
export async function findDestroyTargets(
25+
name: string,
26+
region: string,
27+
runtimeFilter?: Runtime,
28+
): Promise<DestroyTarget[]> {
29+
const targets: DestroyTarget[] = []
30+
31+
if (!runtimeFilter || runtimeFilter === "lambda") {
32+
try {
33+
const { LambdaClient, GetFunctionCommand, GetFunctionUrlConfigCommand } = await import("@aws-sdk/client-lambda")
34+
const client = new LambdaClient({ region })
35+
await client.send(new GetFunctionCommand({ FunctionName: name }))
36+
let urlInfo = ""
37+
try {
38+
const urlResp = await client.send(new GetFunctionUrlConfigCommand({ FunctionName: name }))
39+
urlInfo = urlResp.FunctionUrl ? ` + Function URL` : ""
40+
} catch {}
41+
targets.push({
42+
runtime: "lambda",
43+
name,
44+
detail: `Lambda: ${name} (${region})${urlInfo} + IAM role`,
45+
})
46+
} catch (err) {
47+
const errName = (err as { name?: string }).name
48+
if (errName !== "ResourceNotFoundException") {
49+
const msg = err instanceof Error ? err.message : String(err)
50+
if (!msg.includes("expired") && !msg.includes("credentials") && !msg.includes("Could not load")) {
51+
if (runtimeFilter === "lambda") throw err
52+
}
53+
}
54+
}
55+
}
56+
57+
if (!runtimeFilter || runtimeFilter === "cloudflare") {
58+
const apiToken = process.env.CLOUDFLARE_API_TOKEN
59+
if (apiToken) {
60+
try {
61+
const { resolveAccountId } = await import("./deploy-cf.js")
62+
const accountId = await resolveAccountId(apiToken)
63+
const workerName = `cors-prxy-${name}`
64+
const resp = await fetch(`https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${workerName}`, {
65+
headers: { Authorization: `Bearer ${apiToken}` },
66+
})
67+
if (resp.ok) {
68+
targets.push({
69+
runtime: "cloudflare",
70+
name,
71+
detail: `CF Worker: ${workerName}`,
72+
})
73+
}
74+
} catch {}
75+
}
76+
}
77+
78+
return targets
79+
}
80+
81+
/** Destroy resources for a single runtime. */
82+
export async function destroyByRuntime(
83+
runtime: Runtime,
84+
name: string,
85+
config?: CorsProxyConfig,
86+
): Promise<void> {
1987
if (runtime === "cloudflare") {
20-
const { destroyCf } = await import("./deploy-cf.js")
21-
return destroyCf(config)
88+
const { destroyCfByName } = await import("./deploy-cf.js")
89+
await destroyCfByName(name, config)
90+
} else {
91+
const { destroyLambdaByName } = await import("./deploy-lambda.js")
92+
const region = config?.region ?? "us-east-1"
93+
await destroyLambdaByName(name, region)
94+
}
95+
}
96+
97+
/** Destroy all runtimes for a config (cross-runtime). */
98+
export async function destroy(config: CorsProxyConfig, runtimeFilter?: Runtime): Promise<void> {
99+
const targets = await findDestroyTargets(config.name, config.region, runtimeFilter)
100+
for (const target of targets) {
101+
await destroyByRuntime(target.runtime, target.name, config)
102+
}
103+
if (targets.length === 0) {
104+
console.log("No resources found to destroy.")
22105
}
23-
const { destroyLambda } = await import("./deploy-lambda.js")
24-
return destroyLambda(config)
25106
}

0 commit comments

Comments
 (0)