diff --git a/src/cli.ts b/src/cli.ts
index 369530d..23ea7a6 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,7 +1,10 @@
import { createApp } from './app.ts';
+import { registerBuildCommand } from './commands/build.ts';
const program = createApp();
+registerBuildCommand(program);
+
program.parseAsync(process.argv).catch((err) => {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
diff --git a/src/commands/build.ts b/src/commands/build.ts
new file mode 100644
index 0000000..491c9fd
--- /dev/null
+++ b/src/commands/build.ts
@@ -0,0 +1,54 @@
+import type { Command } from 'commander';
+import { getConfig } from '../config.ts';
+import { runCommand } from '../utils/process.ts';
+import * as output from '../utils/output.ts';
+
+/**
+ * Registers the `build` command with the CLI program.
+ *
+ * @since TBD
+ *
+ * @param {Command} program - The Commander.js program instance.
+ *
+ * @returns {void}
+ */
+export function registerBuildCommand(program: Command): void {
+ program
+ .command('build')
+ .description('Run the build commands.')
+ .option('--dev', 'Run the dev build commands.')
+ .option('--root
', 'Set the root directory for running commands.')
+ .action(async (options: { dev?: boolean; root?: string }) => {
+ const config = getConfig();
+ const buildSteps = config.getBuildCommands(options.dev);
+ const cwd = options.root ?? config.getWorkingDir();
+
+ output.log('Running build steps...');
+
+ for (const step of buildSteps) {
+ let cmd = step;
+ let bailOnFailure = true;
+
+ if (cmd.startsWith('@')) {
+ bailOnFailure = false;
+ cmd = cmd.slice(1);
+ }
+
+ output.section(`> ${cmd}`);
+
+ const result = await runCommand(cmd, {
+ cwd,
+ });
+
+ if (result.exitCode !== 0) {
+ output.error(`[FAIL] Build step failed: ${cmd}`);
+ if (bailOnFailure) {
+ output.error('Exiting...');
+ process.exit(result.exitCode);
+ }
+ }
+ }
+
+ output.success('Build complete.');
+ });
+}
diff --git a/tests/commands/build.test.ts b/tests/commands/build.test.ts
new file mode 100644
index 0000000..1975c9d
--- /dev/null
+++ b/tests/commands/build.test.ts
@@ -0,0 +1,84 @@
+import {
+ runPup,
+ writePuprc,
+ getPuprc,
+ createTempProject,
+ cleanupTempProjects,
+} from '../helpers/setup.js';
+
+describe('build command', () => {
+ let projectDir: string;
+
+ beforeEach(() => {
+ projectDir = createTempProject();
+ });
+
+ afterEach(() => {
+ cleanupTempProjects();
+ });
+
+ it('should run build', async () => {
+ const puprc = getPuprc();
+ puprc.build = ['echo "fake project, yo"'];
+ writePuprc(puprc, projectDir);
+
+ const result = await runPup('build', { cwd: projectDir });
+ expect(result.exitCode).toBe(0);
+ expect(result.output).toContain('fake project, yo');
+ });
+
+ it('should handle no build steps', async () => {
+ const puprc = getPuprc();
+ puprc.build = [];
+ writePuprc(puprc, projectDir);
+
+ const result = await runPup('build', { cwd: projectDir });
+ expect(result.exitCode).toBe(0);
+ });
+
+ it('should succeed with defaults when no .puprc exists (defaults to no build steps)', async () => {
+ const emptyDir = createTempProject();
+ const result = await runPup('build', { cwd: emptyDir });
+ expect(result.exitCode).toBe(0);
+ expect(result.output).toContain('Build complete');
+ });
+
+ it('should pass env vars that are not defined in puprc.env', async () => {
+ const puprc = getPuprc();
+ puprc.build = ['echo "TOKEN=$PUP_TEST_UNIQUE_VAR"'];
+ writePuprc(puprc, projectDir);
+
+ process.env.PUP_TEST_UNIQUE_VAR = 'test-token-12345';
+
+ try {
+ const result = await runPup('build', { cwd: projectDir });
+ expect(result.exitCode).toBe(0);
+ expect(result.output).toContain('TOKEN=test-token-12345');
+ } finally {
+ delete process.env.PUP_TEST_UNIQUE_VAR;
+ }
+ });
+
+ it('should run build in the specified --root directory', async () => {
+ const rootDir = createTempProject();
+
+ const puprc = getPuprc();
+ puprc.build = ['pwd'];
+ writePuprc(puprc, projectDir);
+
+ const result = await runPup(`build --root ${rootDir}`, { cwd: projectDir });
+ expect(result.exitCode).toBe(0);
+ expect(result.output).toContain(rootDir);
+ });
+
+ it('should run build with dev flag', async () => {
+ const puprc = getPuprc();
+ puprc.build = ['echo "production build"'];
+ (puprc as Record).build_dev = ['echo "dev build"'];
+ writePuprc(puprc, projectDir);
+
+ const result = await runPup('build --dev', { cwd: projectDir });
+ expect(result.exitCode).toBe(0);
+ expect(result.output).toContain('dev build');
+ });
+});