Skip to content
Draft
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
2 changes: 2 additions & 0 deletions alchemy/bin/alchemy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { dev } from "./commands/dev.ts";
import { login } from "./commands/login.ts";
import { run } from "./commands/run.ts";
import { init } from "./commands/init.ts";
import { rotatePassword } from "./commands/rotate-password.ts";
import { getPackageVersion } from "./services/get-package-version.ts";
import { t } from "./trpc.ts";

Expand All @@ -17,6 +18,7 @@ const router = t.router({
destroy,
dev,
run,
"rotate-password": rotatePassword,
});

export type AppRouter = typeof router;
Expand Down
157 changes: 157 additions & 0 deletions alchemy/bin/commands/rotate-password.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { log } from "@clack/prompts";
import { spawn } from "node:child_process";
import { once } from "node:events";
import { promises as fs, unlinkSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import pc from "picocolors";
import z from "zod";
import { exists } from "../../src/util/exists.ts";
import { getRunPrefix } from "../get-run-prefix.ts";
import { entrypoint, execArgs } from "../services/execute-alchemy.ts";
import { ExitSignal, loggedProcedure } from "../trpc.ts";

export const rotatePassword = loggedProcedure
.meta({
description: "rotate the password for an alchemy project",
})
.input(
z.tuple([
entrypoint,
z.object({
...execArgs,
oldPassword: z.string().describe("the current password"),
newPassword: z.string().describe("the new password to set"),
scope: z
.string()
.optional()
.describe("the scope/FQN to rotate password for (optional)"),
}),
]),
)
.mutation(async ({ input: [main, options] }) => {
try {
const cwd = options.cwd || process.cwd();
let oldPassword = options.oldPassword;
let newPassword = options.newPassword;

// Validate passwords
if (oldPassword === newPassword) {
log.error(pc.red("New password must be different from old password"));
throw new ExitSignal(1);
}

// Check for alchemy.run.ts or alchemy.run.js (if not provided)
let alchemyFile = main;
if (!alchemyFile) {
const candidates = ["alchemy.run.ts", "alchemy.run.js"];
for (const file of candidates) {
const resolved = resolve(cwd, file);
if (await exists(resolved)) {
alchemyFile = resolved;
break;
}
}
}

if (!alchemyFile) {
log.error(
pc.red(
"No alchemy.run.ts or alchemy.run.js file found in the current directory.",
),
);
throw new ExitSignal(1);
}

// Create a wrapper script that will load the alchemy file and call rotatePassword
const wrapperScript = `
${await fs.readFile(alchemyFile, "utf8")}

// =================

const { rotatePassword: __ALCHEMY_ROTATE_PASSWORD } = await import("alchemy");
const __ALCHEMY_oldPassword = "${oldPassword.replace(/"/g, '\\"')}";
const __ALCHEMY_newPassword = "${newPassword.replace(/"/g, '\\"')}";
const __ALCHEMY_scope = ${options.scope ? `"${options.scope.replace(/"/g, '\\"')}"` : "undefined"};

try {
await __ALCHEMY_ROTATE_PASSWORD(__ALCHEMY_oldPassword, __ALCHEMY_newPassword, __ALCHEMY_scope);
console.log("\\n✅ Password rotation completed successfully");
process.exit(0);
} catch (error) {
console.error("\\n❌ Password rotation failed:", error.message);
process.exit(1);
}
`;

// Write the wrapper script to a temporary file
const tempScriptPath = resolve(
cwd,
`.alchemy-rotate-${Date.now()}.${alchemyFile.endsWith(".ts") ? "ts" : "mjs"}`,
);
writeFileSync(tempScriptPath, wrapperScript);

try {
const runPrefix = await getRunPrefix({
isTypeScript: tempScriptPath.endsWith(".ts"),
cwd,
});
let command = `${runPrefix} ${tempScriptPath}`;

// Set the old password in environment for the alchemy scope to use
const env = {
...process.env,
ALCHEMY_PASSWORD: oldPassword,
FORCE_COLOR: "1",
} as Record<string, string>;

// Handle stage if provided
if (options.stage) {
env.STAGE = options.stage;
}

// Load env file if specified
if (options.envFile && (await exists(resolve(cwd, options.envFile)))) {
// The subprocess will handle loading the env file
command = `${command} --env-file ${options.envFile}`;
}

const child = spawn(command, {
cwd,
shell: true,
stdio: "inherit",
env,
});

const exitPromise = once(child, "exit");
await exitPromise;

const exitCode = child.exitCode === 1 ? 1 : 0;

// Clean up temp file
unlinkSync(tempScriptPath);

if (exitCode !== 0) {
throw new ExitSignal(exitCode);
}
} catch (error) {
// Clean up temp file on error
try {
unlinkSync(tempScriptPath);
} catch {}
throw error;
}
} catch (error) {
if (error instanceof ExitSignal) {
throw error;
}
if (error instanceof Error) {
log.error(`${pc.red("Error:")} ${error.message}`);
if (error.stack && process.env.DEBUG) {
log.error(`${pc.gray("Stack trace:")}\n${error.stack}`);
}
} else {
log.error(pc.red(String(error)));
}
throw new ExitSignal(1);
}
});
44 changes: 44 additions & 0 deletions alchemy/bin/get-run-prefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { detectRuntime } from "../src/util/detect-node-runtime.ts";
import { detectPackageManager } from "../src/util/detect-package-manager.ts";

export async function getRunPrefix(options?: {
isTypeScript?: boolean;
cwd?: string;
}) {
const packageManager = await detectPackageManager(
options?.cwd ?? process.cwd(),
);
const runtime = detectRuntime();

// Determine the command to run based on package manager and runtime
let command: string;

switch (packageManager) {
case "bun":
command = "bun";
break;
case "deno":
command = "deno run -A";
break;
case "pnpm":
command = options?.isTypeScript ? "pnpm dlx tsx" : "pnpm node";
break;
case "yarn":
command = options?.isTypeScript ? "yarn tsx" : "yarn node";
break;
default:
switch (runtime) {
case "bun":
command = "bun";
break;
case "deno":
command = "deno run -A";
break;
default:
command = options?.isTypeScript ? "npx tsx" : "npx node";
break;
}
}

return command;
}
44 changes: 4 additions & 40 deletions alchemy/bin/services/execute-alchemy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import { once } from "node:events";
import { resolve } from "node:path";
import pc from "picocolors";
import z from "zod";
import { detectRuntime } from "../../src/util/detect-node-runtime.ts";
import { detectPackageManager } from "../../src/util/detect-package-manager.ts";
import { exists } from "../../src/util/exists.ts";
import { getRunPrefix } from "../get-run-prefix.ts";
import { ExitSignal } from "../trpc.ts";

export const entrypoint = z
Expand Down Expand Up @@ -123,48 +122,13 @@ export async function execAlchemy(
throw new ExitSignal(1);
}

// Detect package manager
const packageManager = await detectPackageManager(cwd);
const runtime = detectRuntime();

const argsString = args.join(" ");
const execArgsString = execArgs.join(" ");
// Determine the command to run based on package manager and file extension
let command: string;
const isTypeScript = main.endsWith(".ts");
const runPrefix = await getRunPrefix({ isTypeScript, cwd });

const command = `${runPrefix} ${execArgsString} ${main} ${argsString}`;

switch (packageManager) {
case "bun":
command = `bun ${execArgsString} ${main} ${argsString}`;
break;
case "deno":
command = `deno run -A ${execArgsString} ${main} ${argsString}`;
break;
case "pnpm":
command = isTypeScript
? `pnpm dlx tsx ${execArgsString} ${main} ${argsString}`
: `pnpm node ${execArgsString} ${main} ${argsString}`;
break;
case "yarn":
command = isTypeScript
? `yarn tsx ${execArgsString} ${main} ${argsString}`
: `yarn node ${execArgsString} ${main} ${argsString}`;
break;
default:
switch (runtime) {
case "bun":
command = `bun ${execArgsString} ${main} ${argsString}`;
break;
case "deno":
command = `deno run -A ${execArgsString} ${main} ${argsString}`;
break;
case "node":
command = isTypeScript
? `npx tsx ${execArgsString} ${main} ${argsString}`
: `node ${execArgsString} ${main} ${argsString}`;
break;
}
}
process.on("SIGINT", async () => {
await exitPromise;
process.exit(sanitizeExitCode(child.exitCode));
Expand Down
1 change: 1 addition & 0 deletions alchemy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type { AlchemyOptions, Phase } from "./alchemy.ts";
export type * from "./context.ts";

export * from "./resource.ts";
export * from "./rotate-password.ts";
export * from "./scope.ts";
export * from "./secret.ts";
export * from "./serde.ts";
Expand Down
Loading