Skip to content

Commit 5cefde3

Browse files
Add flag to CLI to specify config file
1 parent d218dcd commit 5cefde3

File tree

6 files changed

+127
-36
lines changed

6 files changed

+127
-36
lines changed

packages/cli/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,21 @@ const program = new Command();
1515
program
1616
.name("openworkflow")
1717
.description("OpenWorkflow CLI - learn more at https://openworkflow.dev")
18+
.usage("<command> [options]")
1819
.version(getVersion());
1920

2021
// init
2122
program
2223
.command("init")
2324
.description("initialize OpenWorkflow")
25+
.option("--config <path>", "path to OpenWorkflow config file")
2426
.action(withErrorHandling(init));
2527

2628
// doctor
2729
program
2830
.command("doctor")
2931
.description("check configuration and list available workflows")
32+
.option("--config <path>", "path to OpenWorkflow config file")
3033
.action(withErrorHandling(doctor));
3134

3235
// worker
@@ -41,12 +44,14 @@ workerCmd
4144
"number of concurrent workflows to process",
4245
Number.parseInt,
4346
)
47+
.option("--config <path>", "path to OpenWorkflow config file")
4448
.action(withErrorHandling(workerStart));
4549

4650
// dashboard
4751
program
4852
.command("dashboard")
4953
.description("start the dashboard to view workflow runs")
54+
.option("--config <path>", "path to OpenWorkflow config file")
5055
.action(withErrorHandling(dashboard));
5156

5257
await program.parseAsync(process.argv);

packages/cli/commands.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { WorkerConfig, loadConfig } from "./config.js";
1+
import { WorkerConfig, loadConfig, loadConfigFromPath } from "./config.js";
22
import { CLIError } from "./errors.js";
33
import {
44
CONFIG,
@@ -31,6 +31,10 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
3131

3232
type BackendChoice = "sqlite" | "postgres" | "both";
3333

34+
interface CommandOptions {
35+
config?: string;
36+
}
37+
3438
/**
3539
* openworkflow -V | --version
3640
* @returns the version string, or "-" if it cannot be determined
@@ -57,11 +61,15 @@ export function getVersion(): string {
5761
return "-";
5862
}
5963

60-
/** openworkflow init */
61-
export async function init(): Promise<void> {
64+
/**
65+
* openworkflow init
66+
* @param options - Command options
67+
*/
68+
export async function init(options: CommandOptions = {}): Promise<void> {
69+
const configPath = options.config;
6270
p.intro("Initializing OpenWorkflow...");
6371

64-
const { configFile } = await loadConfigWithEnv();
72+
const { configFile } = await loadConfigWithEnv(configPath);
6573
let configFileToDelete: string | null = null;
6674

6775
if (configFile) {
@@ -123,7 +131,7 @@ export async function init(): Promise<void> {
123131
);
124132
}
125133

126-
const configFileName = getConfigFileName(packageJson);
134+
const configFileName = configPath ?? getConfigFileName(packageJson);
127135
const clientFileName = getClientFileName(packageJson);
128136
const exampleWorkflowFileName = getExampleWorkflowFileName(packageJson);
129137
const runFileName = getRunFileName(packageJson);
@@ -191,11 +199,15 @@ export async function init(): Promise<void> {
191199
p.outro("✅ Setup complete!");
192200
}
193201

194-
/** openworkflow doctor */
195-
export async function doctor(): Promise<void> {
202+
/**
203+
* openworkflow doctor
204+
* @param options - Command options
205+
*/
206+
export async function doctor(options: CommandOptions = {}): Promise<void> {
207+
const configPath = options.config;
196208
consola.start("Running OpenWorkflow doctor...");
197209

198-
const { config, configFile } = await loadConfigWithEnv();
210+
const { config, configFile } = await loadConfigWithEnv(configPath);
199211
if (!configFile) {
200212
throw new CLIError(
201213
"No config file found.",
@@ -244,14 +256,19 @@ export async function doctor(): Promise<void> {
244256
}
245257
}
246258

259+
export type WorkerStartOptions = WorkerConfig & CommandOptions;
260+
247261
/**
248262
* openworkflow worker start
249-
* @param cliOptions - Worker config overrides
263+
* @param options - Worker config and command options
250264
*/
251-
export async function workerStart(cliOptions: WorkerConfig): Promise<void> {
265+
export async function workerStart(
266+
options: WorkerStartOptions = {},
267+
): Promise<void> {
268+
const { config: configPath, ...workerConfig } = options;
252269
consola.start("Starting worker...");
253270

254-
const { config, configFile } = await loadConfigWithEnv();
271+
const { config, configFile } = await loadConfigWithEnv(configPath);
255272
if (!configFile) {
256273
throw new CLIError(
257274
"No config file found.",
@@ -297,7 +314,7 @@ export async function workerStart(cliOptions: WorkerConfig): Promise<void> {
297314

298315
assertNoDuplicateWorkflows(workflows);
299316

300-
const workerOptions = mergeDefinedOptions(config.worker, cliOptions);
317+
const workerOptions = mergeDefinedOptions(config.worker, workerConfig);
301318
if (workerOptions.concurrency !== undefined) {
302319
assertPositiveInteger("concurrency", workerOptions.concurrency);
303320
}
@@ -323,11 +340,13 @@ export async function workerStart(cliOptions: WorkerConfig): Promise<void> {
323340
/**
324341
* openworkflow dashboard
325342
* Starts the dashboard by delegating to `@openworkflow/dashboard` via npx.
343+
* @param options - Command options
326344
*/
327-
export async function dashboard(): Promise<void> {
345+
export async function dashboard(options: CommandOptions = {}): Promise<void> {
346+
const configPath = options.config;
328347
consola.start("Starting dashboard...");
329348

330-
const { configFile } = await loadConfigWithEnv();
349+
const { configFile } = await loadConfigWithEnv(configPath);
331350
if (!configFile) {
332351
throw new CLIError(
333352
"No config file found.",
@@ -808,7 +827,11 @@ function getDevDependenciesToInstall(): string[] {
808827
function createConfigFile(configFileName: string): void {
809828
const spinner = p.spinner();
810829
spinner.start("Writing config...");
811-
const configDestPath = path.join(process.cwd(), configFileName);
830+
const configDestPath = path.resolve(process.cwd(), configFileName);
831+
832+
// mkdir if the user specified a config file, and they want it in a dir
833+
mkdirSync(path.dirname(configDestPath), { recursive: true });
834+
812835
writeFileSync(configDestPath, CONFIG, "utf8");
813836
spinner.stop(`Config written to ${configDestPath}`);
814837
}
@@ -1001,12 +1024,15 @@ function updateEnvForPostgres(): void {
10011024

10021025
/**
10031026
* Load CLI config after loading .env, and wrap errors for user-facing output.
1027+
* @param configPath - Optional explicit config file path
10041028
* @returns Loaded config and metadata.
10051029
*/
1006-
async function loadConfigWithEnv() {
1030+
async function loadConfigWithEnv(configPath?: string) {
10071031
loadDotenv({ quiet: true });
10081032
try {
1009-
return await loadConfig();
1033+
return configPath
1034+
? await loadConfigFromPath(configPath)
1035+
: await loadConfig();
10101036
} catch (error) {
10111037
const message = error instanceof Error ? error.message : String(error);
10121038
throw new CLIError("Failed to load OpenWorkflow config.", message);

packages/cli/config.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { defineConfig, loadConfig } from "./config.js";
1+
import { defineConfig, loadConfig, loadConfigFromPath } from "./config.js";
22
import fs from "node:fs";
33
import os from "node:os";
44
import path from "node:path";
@@ -107,4 +107,30 @@ describe("loadConfig", () => {
107107
process.chdir(originalCwd);
108108
}
109109
});
110+
111+
test("loads an explicit config path", async () => {
112+
const filePath = path.join(tmpDir, "src", "openworkflow.config.js");
113+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
114+
fs.writeFileSync(filePath, `export default { name: "explicit" };`);
115+
116+
const { config, configFile } = await loadConfigFromPath(
117+
"src/openworkflow.config.js",
118+
tmpDir,
119+
);
120+
const cfg = config as unknown as TestConfig;
121+
expect(cfg.name).toBe("explicit");
122+
expect(configFile).toBe(filePath);
123+
});
124+
125+
test("does not fallback to discovered config when explicit path is missing", async () => {
126+
const filePath = path.join(tmpDir, "openworkflow.config.js");
127+
fs.writeFileSync(filePath, `export default { name: "discovered" };`);
128+
129+
const { config, configFile } = await loadConfigFromPath(
130+
"src/openworkflow.config.js",
131+
tmpDir,
132+
);
133+
expect(config).toEqual({});
134+
expect(configFile).toBeUndefined();
135+
});
110136
});

packages/cli/config.ts

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ const CONFIG_NAME = "openworkflow.config";
4545
const CONFIG_EXTENSIONS = ["ts", "mts", "cts", "js", "mjs", "cjs"] as const;
4646
const jiti = createJiti(import.meta.url);
4747

48+
/**
49+
* Load OpenWorkflow config from an explicit path.
50+
* @param configPath - Explicit config file path
51+
* @param startDir - Optional base directory for resolving relative paths
52+
* @returns The loaded configuration and metadata
53+
*/
54+
export async function loadConfigFromPath(
55+
configPath: string,
56+
startDir?: string,
57+
): Promise<LoadedConfig> {
58+
const filePath = path.resolve(startDir ?? process.cwd(), configPath);
59+
return existsSync(filePath)
60+
? importConfigFile(filePath)
61+
: getEmptyLoadedConfig();
62+
}
63+
4864
/**
4965
* Load the OpenWorkflow config at openworkflow.config.{ts,mts,cts,js,mjs,cjs}.
5066
* Searches up the directory tree from the starting directory to find the
@@ -64,22 +80,7 @@ export async function loadConfig(startDir?: string): Promise<LoadedConfig> {
6480
const filePath = path.join(currentDir, fileName);
6581

6682
if (existsSync(filePath)) {
67-
try {
68-
const fileUrl = pathToFileURL(filePath).href;
69-
70-
const config = await jiti.import<OpenWorkflowConfig>(fileUrl, {
71-
default: true,
72-
});
73-
74-
return {
75-
config,
76-
configFile: filePath,
77-
};
78-
} catch (error: unknown) {
79-
throw new Error(
80-
`Failed to load config file ${filePath}: ${String(error)}`,
81-
);
82-
}
83+
return await importConfigFile(filePath);
8384
}
8485
}
8586

@@ -92,6 +93,35 @@ export async function loadConfig(startDir?: string): Promise<LoadedConfig> {
9293
currentDir = parentDir;
9394
}
9495

96+
return getEmptyLoadedConfig();
97+
}
98+
99+
/**
100+
* Import a config file and wrap load errors with a stable message.
101+
* @param filePath - Absolute config file path.
102+
* @returns Loaded config metadata.
103+
*/
104+
async function importConfigFile(filePath: string): Promise<LoadedConfig> {
105+
try {
106+
const fileUrl = pathToFileURL(filePath).href;
107+
const config = await jiti.import<OpenWorkflowConfig>(fileUrl, {
108+
default: true,
109+
});
110+
111+
return {
112+
config,
113+
configFile: filePath,
114+
};
115+
} catch (error: unknown) {
116+
throw new Error(`Failed to load config file ${filePath}: ${String(error)}`);
117+
}
118+
}
119+
120+
/**
121+
* Return an empty config result when no config file is found.
122+
* @returns Empty config metadata.
123+
*/
124+
function getEmptyLoadedConfig(): LoadedConfig {
95125
return {
96126
// not great, but meant to match the c12 api since that is what was used in
97127
// the initial implementation of loadConfig

packages/docs/docs/cli.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Command-line interface for OpenWorkflow
55

66
The OpenWorkflow CLI is the primary way to set up your project, run workers, and
77
launch the dashboard. CLI commands read your `openworkflow.config.ts`
8-
automatically.
8+
automatically, or you can override the path with `--config`.
99

1010
## Installation
1111

packages/docs/docs/configuration.mdx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ export default defineConfig({
4444
```
4545

4646
The CLI automatically finds this file by searching up from the current
47-
directory.
47+
directory. If your config lives somewhere else, pass an explicit path:
48+
49+
```bash
50+
npx @openworkflow/cli worker start --config src/openworkflow.config.ts
51+
```
4852

4953
## Verify Configuration
5054

0 commit comments

Comments
 (0)