Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions packages/cli/src/lib/__tests__/helpers.exec.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { mkdtempSync, realpathSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "bun:test";

import { exec } from "../helpers";

const tempDirs: string[] = [];

afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true });
}
});

describe("exec", () => {
it("runs a command and returns stdout", async () => {
const result = await exec([process.execPath, "--version"]);

expect(result.exitCode).toBe(0);
expect(result.stdout.length).toBeGreaterThan(0);
});

it("respects cwd", async () => {
const cwd = realpathSync(
mkdtempSync(join(tmpdir(), "create-start-kit-dev-"))
);
tempDirs.push(cwd);

const result = await exec(
[process.execPath, "-e", "console.log(process.cwd())"],
{ cwd }
);

expect(result.exitCode).toBe(0);
expect(result.stdout).toBe(cwd);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("returns non-zero exit code without throwing", async () => {
const result = await exec([process.execPath, "-e", "process.exit(7)"]);

expect(result.exitCode).toBe(7);
});
});
38 changes: 28 additions & 10 deletions packages/cli/src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,35 @@ export function generateSecret(bytes = 32): string {
}

export async function exec(
command: string
command: string[],
options?: { cwd?: string }
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
// biome-ignore lint/correctness/noUndeclaredVariables: Bun global available at runtime
const proc = Bun.spawn(["sh", "-c", command], {
stdout: "pipe",
stderr: "pipe",
});
const stdout = await new Response(proc.stdout).text();
const stderr = await new Response(proc.stderr).text();
const exitCode = await proc.exited;
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
if (command.length === 0) {
return { stdout: "", stderr: "No command provided", exitCode: 1 };
}

try {
// biome-ignore lint/correctness/noUndeclaredVariables: Bun global available at runtime
const proc = Bun.spawn(command, {
cwd: options?.cwd,
stdout: "pipe",
stderr: "pipe",
});

const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
const exitCode = await proc.exited;

return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode };
} catch (error) {
return {
stdout: "",
stderr: error instanceof Error ? error.message : String(error),
exitCode: 127,
};
}
}

export async function testDbConnection(
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/lib/state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";

const STATE_FILE = ".setup-state.json";

Expand Down Expand Up @@ -39,7 +40,7 @@ const DEFAULT_STATE: SetupState = {
};

function getStatePath(): string {
return `${process.cwd()}/${STATE_FILE}`;
return join(process.cwd(), STATE_FILE);
}

export function loadState(): SetupState {
Expand Down
22 changes: 16 additions & 6 deletions packages/cli/src/phases/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,15 @@ export async function runDatabase(state: SetupState): Promise<SetupState> {
const s = spinner();
s.start("Creating instant Neon database via Instagres...");

const result = await exec(
"bunx get-db --yes --env .env --key DATABASE_URL"
);
const result = await exec([
"bunx",
"get-db",
"--yes",
"--env",
".env",
"--key",
"DATABASE_URL",
]);

if (result.exitCode !== 0) {
s.stop("Failed to create database");
Expand Down Expand Up @@ -131,9 +137,13 @@ export async function runDatabase(state: SetupState): Promise<SetupState> {
writeEnvFile(".env", envVars);

// Use --force to skip interactive confirmation prompts
const migrateResult = await exec(
"bun --env-file=.env drizzle-kit push --force"
);
const migrateResult = await exec([
"bun",
"--env-file=.env",
"drizzle-kit",
"push",
"--force",
]);

if (migrateResult.exitCode !== 0) {
ms.stop("Schema push failed");
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/phases/infra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ async function startDockerService(
): Promise<void> {
const s = spinner();
s.start(`Starting ${name}...`);
const result = await exec(`docker compose up -d ${service}`);
const result = await exec(["docker", "compose", "up", "-d", service]);
if (result.exitCode !== 0) {
s.stop(`Failed to start ${name}`);
log.error(result.stderr);
Expand Down
16 changes: 11 additions & 5 deletions packages/cli/src/phases/scaffold.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { existsSync } from "node:fs";
import { existsSync, rmSync } from "node:fs";
import { resolve } from "node:path";
import { isCancel, log, spinner, text } from "@clack/prompts";
import { downloadTemplate } from "giget";

Expand All @@ -23,14 +24,19 @@ async function fetchTemplate(targetDir: string): Promise<void> {
process.exit(1);
}

await exec(`git -C "${targetDir}" init`);
const gitInit = await exec(["git", "init"], { cwd: targetDir });

if (gitInit.exitCode !== 0) {
log.warn("Could not initialize git repository");
log.warn(gitInit.stderr || "git init failed");
}
}

async function installDeps(targetDir: string): Promise<void> {
const s = spinner();
s.start("Installing dependencies...");

const result = await exec(`cd "${targetDir}" && bun install`);
const result = await exec(["bun", "install"], { cwd: targetDir });

if (result.exitCode !== 0) {
s.stop("Install failed");
Expand Down Expand Up @@ -62,7 +68,7 @@ export async function runScaffold(projectNameArg?: string): Promise<string> {
}

const dirName = toKebabCase(projectName as string);
const targetDir = `${process.cwd()}/${dirName}`;
const targetDir = resolve(process.cwd(), dirName);

if (existsSync(targetDir)) {
log.error(`Directory "${dirName}" already exists.`);
Expand All @@ -75,7 +81,7 @@ export async function runScaffold(projectNameArg?: string): Promise<string> {
await installDeps(targetDir);

// Remove the state file if it exists from the template
await exec(`rm -f "${targetDir}/.setup-state.json"`);
rmSync(resolve(targetDir, ".setup-state.json"), { force: true });

log.success(`Project created in ./${dirName}`);

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/theme/theme-apply.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { writeFileSync } from "node:fs";
import { join } from "node:path";
import { log, spinner } from "@clack/prompts";
import type {
BaseColorName,
Expand Down Expand Up @@ -82,7 +83,7 @@ export function applyTheme(targetDir: string, config: ThemeConfig): void {
s.start("Applying theme...");

const css = generateFullAppCss(config);
writeFileSync(`${targetDir}/src/app.css`, css, "utf-8");
writeFileSync(join(targetDir, "src", "app.css"), css, "utf-8");

s.stop(
`Theme applied: ${config.theme} (base: ${config.baseColor}, radius: ${config.radius}, font: ${config.font})`
Expand Down