Skip to content

Commit 6f65323

Browse files
committed
fix: cli handle existing resources
1 parent d469ee1 commit 6f65323

14 files changed

+1044
-135
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,27 +77,27 @@ Demo implementations are available in the [`examples/`](./examples/) directory f
7777
**Interactive mode** (asks questions and provides helpful defaults):
7878

7979
```bash
80-
npx @better-auth-cloudflare/cli generate
80+
npx @better-auth-cloudflare/cli@latest generate
8181
```
8282

8383
**Non-interactive mode** (use arguments):
8484

8585
```bash
86-
# Simple D1 app with KV (ready to run)
87-
npx @better-auth-cloudflare/cli generate \
86+
# Simple D1 app with KV (fully deployed to Cloudflare)
87+
npx @better-auth-cloudflare/cli@latest generate \
8888
--app-name=my-auth-app \
8989
--template=hono \
9090
--database=d1 \
9191
--kv=true \
9292
--r2=false \
93-
--apply-migrations=dev
93+
--apply-migrations=prod
9494
```
9595

9696
**Migration workflow**:
9797

9898
```bash
99-
npx @better-auth-cloudflare/cli migrate # Interactive
100-
npx @better-auth-cloudflare/cli migrate --migrate-target=dev # Non-interactive
99+
npx @better-auth-cloudflare/cli@latest migrate # Interactive
100+
npx @better-auth-cloudflare/cli@latest migrate --migrate-target=prod # Non-interactive
101101
```
102102

103103
The CLI creates projects from Hono or Next.js templates and can automatically set up D1, KV, R2, and Hyperdrive resources. See [CLI Documentation](./cli/README.md) for full documentation and all available arguments.

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@better-auth-cloudflare/cli",
3-
"version": "0.1.13",
3+
"version": "0.1.15",
44
"description": "CLI to generate Better Auth Cloudflare projects (Hono or OpenNext.js)",
55
"author": "Zach Grimaldi",
66
"repository": {

cli/src/index.ts

Lines changed: 123 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
#!/usr/bin/env node
2-
32
import { cancel, confirm, group, intro, outro, select, spinner, text } from "@clack/prompts";
43
import { cpSync, existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
54
import { tmpdir } from "os";
@@ -215,8 +214,16 @@ function checkD1DatabaseExists(databaseName: string, cwd: string, accountId?: st
215214
return false;
216215
}
217216

218-
const result = runWranglerCommand(["d1", "info", databaseName], cwd, accountId);
219-
return result.code === 0;
217+
const result = runWranglerCommand(["d1", "list", "--json"], cwd, accountId);
218+
if (result.code === 0) {
219+
try {
220+
const databases = JSON.parse(result.stdout);
221+
return databases.some((db: any) => db.name === databaseName);
222+
} catch {
223+
return false;
224+
}
225+
}
226+
return false;
220227
}
221228

222229
function checkHyperdriveExists(hyperdriveId: string, cwd: string, accountId?: string): boolean {
@@ -228,6 +235,68 @@ function checkHyperdriveExists(hyperdriveId: string, cwd: string, accountId?: st
228235
return result.code === 0;
229236
}
230237

238+
// Functions to get IDs of existing resources
239+
function getExistingD1DatabaseId(databaseName: string, cwd: string, accountId?: string): string | null {
240+
try {
241+
console.log(`[DEBUG] Attempting to get D1 ID for database: ${databaseName}`);
242+
const result = runWranglerCommand(["d1", "list", "--json"], cwd, accountId);
243+
console.log(`[DEBUG] D1 list command result - code: ${result.code}`);
244+
console.log(`[DEBUG] D1 list stderr: ${result.stderr}`);
245+
console.log(`[DEBUG] D1 list stdout: ${result.stdout}`);
246+
if (result.code === 0) {
247+
// Parse the JSON output to find the database by name
248+
const databases = JSON.parse(result.stdout);
249+
const database = databases.find((db: any) => db.name === databaseName);
250+
const extractedId = database?.uuid || null;
251+
console.log(`[DEBUG] Extracted D1 ID: ${extractedId}`);
252+
return extractedId;
253+
}
254+
console.log(`[DEBUG] D1 list command failed with code: ${result.code}`);
255+
return null;
256+
} catch (error) {
257+
console.log(`[DEBUG] D1 list command threw error: ${error}`);
258+
return null;
259+
}
260+
}
261+
262+
function getExistingKvNamespaceId(namespaceName: string, cwd: string, accountId?: string): string | null {
263+
try {
264+
const result = runWranglerCommand(["kv", "namespace", "list"], cwd, accountId);
265+
if (result.code === 0) {
266+
// Parse the JSON output to find the namespace by name
267+
const namespaces = JSON.parse(result.stdout);
268+
const namespace = namespaces.find((ns: any) => ns.title === namespaceName);
269+
return namespace?.id || null;
270+
}
271+
return null;
272+
} catch {
273+
return null;
274+
}
275+
}
276+
277+
function getExistingHyperdriveId(hyperdriveName: string, cwd: string, accountId?: string): string | null {
278+
try {
279+
const result = runWranglerCommand(["hyperdrive", "list"], cwd, accountId);
280+
if (result.code === 0) {
281+
// Parse the table format from `wrangler hyperdrive list` command
282+
const lines = result.stdout.split("\n");
283+
for (const line of lines) {
284+
// Look for a line that contains the hyperdrive name and extract the ID from the first column
285+
if (line.includes(hyperdriveName)) {
286+
// Extract ID from first column: │ id │ name │ ...
287+
const idMatch = /\s*([0-9a-f]{32})\s*/.exec(line);
288+
if (idMatch) {
289+
return idMatch[1];
290+
}
291+
}
292+
}
293+
}
294+
return null;
295+
} catch {
296+
return null;
297+
}
298+
}
299+
231300
function parseAvailableAccounts(stderr: string): Array<{ name: string; id: string }> {
232301
const accounts: Array<{ name: string; id: string }> = [];
233302
const lines = stderr.split("\n");
@@ -1527,13 +1596,24 @@ export const verification = {} as any;`;
15271596
res.stderr?.includes("already exists") || res.stdout?.includes("already exists");
15281597

15291598
if (isDatabaseExists) {
1530-
// Database already exists, which is fine - just configure it in wrangler.toml
1531-
if (existsSync(wranglerPath)) {
1532-
let wrangler = readFileSync(wranglerPath, "utf8");
1533-
wrangler = updateD1Block(wrangler, answers.d1Binding!, answers.d1Name);
1534-
writeFileSync(wranglerPath, wrangler);
1599+
// Database already exists, which is fine - get its ID and configure it in wrangler.toml
1600+
const existingDatabaseId = getExistingD1DatabaseId(answers.d1Name, cwd, answers.accountId);
1601+
if (existingDatabaseId && existsSync(wranglerPath)) {
1602+
debugLog(`Updating wrangler.toml with existing D1 database ID: ${existingDatabaseId}`);
1603+
const currentWrangler = readFileSync(wranglerPath, "utf-8");
1604+
const updatedWrangler = updateD1BlockWithId(
1605+
currentWrangler,
1606+
answers.d1Binding || "DATABASE",
1607+
answers.d1Name,
1608+
existingDatabaseId
1609+
);
1610+
writeFileSync(wranglerPath, updatedWrangler);
15351611
}
1536-
creating.stop(pc.yellow(`D1 database already exists (name: ${answers.d1Name}).`));
1612+
creating.stop(
1613+
pc.yellow(
1614+
`D1 database already exists (name: ${answers.d1Name})${existingDatabaseId ? " (id: " + existingDatabaseId + ")" : ""}.`
1615+
)
1616+
);
15371617
} else if (isInternalError) {
15381618
creating.stop(pc.red("D1 database creation failed due to Cloudflare API internal error."));
15391619
console.log(pc.gray("This is usually a temporary issue with Cloudflare's API."));
@@ -1589,19 +1669,24 @@ export const verification = {} as any;`;
15891669
res.stdout?.includes("A Hyperdrive config with the given name already exists");
15901670

15911671
if (isHyperdriveExists) {
1592-
// Hyperdrive already exists, which is fine - just configure it in wrangler.toml
1593-
if (existsSync(wranglerPath)) {
1594-
let wrangler = readFileSync(wranglerPath, "utf8");
1595-
wrangler = appendOrReplaceHyperdriveBlock(
1596-
wrangler,
1672+
// Hyperdrive already exists, which is fine - get its ID and configure it in wrangler.toml
1673+
const existingHyperdriveId = getExistingHyperdriveId(answers.hdName, cwd, answers.accountId);
1674+
if (existingHyperdriveId && existsSync(wranglerPath)) {
1675+
debugLog(`Updating wrangler.toml with existing Hyperdrive ID: ${existingHyperdriveId}`);
1676+
const currentWrangler = readFileSync(wranglerPath, "utf-8");
1677+
const updatedWrangler = updateHyperdriveBlockWithId(
1678+
currentWrangler,
15971679
answers.hdBinding,
1598-
undefined,
1599-
answers.database as "hyperdrive-postgres" | "hyperdrive-mysql",
1680+
existingHyperdriveId,
16001681
answers.hdConnectionString
16011682
);
1602-
writeFileSync(wranglerPath, wrangler);
1683+
writeFileSync(wranglerPath, updatedWrangler);
16031684
}
1604-
creating.stop(pc.yellow(`Hyperdrive already exists (name: ${answers.hdName}).`));
1685+
creating.stop(
1686+
pc.yellow(
1687+
`Hyperdrive already exists (name: ${answers.hdName})${existingHyperdriveId ? " (id: " + existingHyperdriveId + ")" : ""}.`
1688+
)
1689+
);
16051690
} else {
16061691
creating.stop(pc.red("Failed to create Hyperdrive."));
16071692
assertOk(res, "Hyperdrive creation failed.");
@@ -1641,13 +1726,27 @@ export const verification = {} as any;`;
16411726
res.stdout?.includes("already exists");
16421727

16431728
if (isKvExists) {
1644-
// KV namespace already exists, which is fine - just configure it in wrangler.toml
1645-
if (existsSync(wranglerPath)) {
1646-
let wrangler = readFileSync(wranglerPath, "utf8");
1647-
wrangler = appendOrReplaceKvNamespaceBlock(wrangler, answers.kvBinding);
1648-
writeFileSync(wranglerPath, wrangler);
1729+
// KV namespace already exists, which is fine - get its ID and configure it in wrangler.toml
1730+
const existingNamespaceId = getExistingKvNamespaceId(
1731+
answers.kvNamespaceName,
1732+
cwd,
1733+
answers.accountId
1734+
);
1735+
if (existingNamespaceId && existsSync(wranglerPath)) {
1736+
debugLog(`Updating wrangler.toml with existing KV namespace ID: ${existingNamespaceId}`);
1737+
const currentWrangler = readFileSync(wranglerPath, "utf-8");
1738+
const updatedWrangler = updateKvBlockWithId(
1739+
currentWrangler,
1740+
answers.kvBinding,
1741+
existingNamespaceId
1742+
);
1743+
writeFileSync(wranglerPath, updatedWrangler);
16491744
}
1650-
creating.stop(pc.yellow(`KV namespace already exists (name: ${answers.kvNamespaceName}).`));
1745+
creating.stop(
1746+
pc.yellow(
1747+
`KV namespace already exists (name: ${answers.kvNamespaceName})${existingNamespaceId ? " (id: " + existingNamespaceId + ")" : ""}.`
1748+
)
1749+
);
16511750
} else {
16521751
creating.stop(pc.red("Failed to create KV namespace."));
16531752
assertOk(res, "KV namespace creation failed.");

cli/src/lib/helpers.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,25 @@ export function extractD1DatabaseId(wranglerOutput: string): string | null {
170170
return tomlMatch[1];
171171
}
172172

173-
// Fallback: Look for JSON response with database_id
173+
// Look for JSON response with database_id
174174
const jsonRegex = /\{[\s\S]*"database_id":\s*"([^"]+)"[\s\S]*\}/;
175175
const jsonMatch = jsonRegex.exec(wranglerOutput);
176176
if (jsonMatch) {
177177
return jsonMatch[1];
178178
}
179+
180+
// Parse table format from `wrangler d1 info` command
181+
// The ID appears in the first row of the table without a label
182+
const lines = wranglerOutput.split("\n");
183+
for (const line of lines) {
184+
// Look for a line that contains a UUID (36 characters with hyphens)
185+
const uuidRegex = /\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s*/i;
186+
const uuidMatch = uuidRegex.exec(line);
187+
if (uuidMatch) {
188+
return uuidMatch[1];
189+
}
190+
}
191+
179192
return null;
180193
} catch {
181194
return null;

cli/src/lib/project-validator.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,17 @@ export async function validateProject(projectPath: string): Promise<ProjectValid
4545
timeout: 60000, // 60 second timeout for full project compilation
4646
});
4747

48-
// If npx fails, try direct tsc command
48+
// If npx fails, try bunx tsc command
49+
if (result.status !== 0) {
50+
result = spawnSync("bunx", ["tsc", "--noEmit"], {
51+
cwd: projectPath,
52+
encoding: "utf8",
53+
stdio: "pipe",
54+
timeout: 60000, // 60 second timeout for full project compilation
55+
});
56+
}
57+
58+
// If bunx fails, try direct tsc command
4959
if (result.status !== 0) {
5060
result = spawnSync("tsc", ["--noEmit"], {
5161
cwd: projectPath,

cli/src/lib/typescript-validator.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,17 @@ export class TypeScriptValidator {
7979
timeout: 30000, // 30 second timeout
8080
});
8181

82-
// If npx fails, try direct tsc command (global installation)
82+
// If npx fails, try bunx tsc command
83+
if (result.status !== 0 && result.error) {
84+
result = spawnSync("bunx", ["tsc", "--project", this.tempDir, "--noEmit"], {
85+
cwd: cliProjectDir,
86+
encoding: "utf8",
87+
stdio: "pipe",
88+
timeout: 30000, // 30 second timeout
89+
});
90+
}
91+
92+
// If bunx fails, try direct tsc command (global installation)
8393
if (result.status !== 0 && result.error) {
8494
result = spawnSync("tsc", ["--project", this.tempDir, "--noEmit"], {
8595
cwd: cliProjectDir,

cli/tests/auth-generator.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ describe("Auth Generator", () => {
115115

116116
// Check imports
117117
expect(result).toContain('import { getCloudflareContext } from "@opennextjs/cloudflare"');
118-
expect(result).toContain('import { getDb } from "../db"');
118+
expect(result).toContain('import { getDb, schema } from "../db"');
119119
expect(result).toContain('import { anonymous, openAPI } from "better-auth/plugins"');
120120

121121
// Check D1 configuration

0 commit comments

Comments
 (0)