@openworkflow/cli 0.4.5 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +13 -2
- package/dist/commands.d.ts +9 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +193 -136
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +22 -8
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -7
- package/dist/module-loader.d.ts +9 -0
- package/dist/module-loader.d.ts.map +1 -0
- package/dist/module-loader.js +17 -0
- package/package.json +5 -5
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/* v8 ignore file -- @preserve */
|
|
3
3
|
import { dashboard, doctor, getVersion, init, workerStart, } from "./commands.js";
|
|
4
4
|
import { withErrorHandling } from "./errors.js";
|
|
5
|
-
import { Command } from "commander";
|
|
5
|
+
import { Command, Option } from "commander";
|
|
6
6
|
// openworkflow
|
|
7
7
|
const program = new Command();
|
|
8
8
|
program
|
|
@@ -14,13 +14,22 @@ program
|
|
|
14
14
|
program
|
|
15
15
|
.command("init")
|
|
16
16
|
.description("initialize OpenWorkflow")
|
|
17
|
+
.addOption(new Option("--backend <backend>", "backend to configure").choices([
|
|
18
|
+
"sqlite",
|
|
19
|
+
"postgres",
|
|
20
|
+
"both",
|
|
21
|
+
]))
|
|
22
|
+
.option("-y, --yes", "skip prompts (requires --backend; does not allow overwrites)")
|
|
23
|
+
.option("--skip-install", "create project files without installing dependencies")
|
|
17
24
|
.option("--config <path>", "path to OpenWorkflow config file")
|
|
25
|
+
.option("--env-file <path>", "load environment variables from file")
|
|
18
26
|
.action(withErrorHandling(init));
|
|
19
27
|
// doctor
|
|
20
28
|
program
|
|
21
29
|
.command("doctor")
|
|
22
|
-
.description("check
|
|
30
|
+
.description("check worker prerequisites")
|
|
23
31
|
.option("--config <path>", "path to OpenWorkflow config file")
|
|
32
|
+
.option("--env-file <path>", "load environment variables from file")
|
|
24
33
|
.action(withErrorHandling(doctor));
|
|
25
34
|
// worker
|
|
26
35
|
const workerCmd = program.command("worker").description("manage workers");
|
|
@@ -30,6 +39,7 @@ workerCmd
|
|
|
30
39
|
.description("start a worker to process workflows")
|
|
31
40
|
.option("-c, --concurrency <number>", "number of concurrent workflows to process", Number.parseInt)
|
|
32
41
|
.option("--config <path>", "path to OpenWorkflow config file")
|
|
42
|
+
.option("--env-file <path>", "load environment variables from file")
|
|
33
43
|
.action(withErrorHandling(workerStart));
|
|
34
44
|
// dashboard
|
|
35
45
|
program
|
|
@@ -37,5 +47,6 @@ program
|
|
|
37
47
|
.description("start the dashboard to view workflow runs")
|
|
38
48
|
.option("-p, --port <number>", "custom port for the dashboard server", Number.parseInt)
|
|
39
49
|
.option("--config <path>", "path to OpenWorkflow config file")
|
|
50
|
+
.option("--env-file <path>", "load environment variables from file")
|
|
40
51
|
.action(withErrorHandling(dashboard));
|
|
41
52
|
await program.parseAsync(process.argv);
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { WorkerConfig } from "./config.js";
|
|
2
|
+
type BackendChoice = "sqlite" | "postgres" | "both";
|
|
2
3
|
interface CommandOptions {
|
|
3
4
|
config?: string;
|
|
5
|
+
envFile?: string;
|
|
6
|
+
}
|
|
7
|
+
interface InitOptions extends CommandOptions {
|
|
8
|
+
backend?: BackendChoice;
|
|
9
|
+
yes?: boolean;
|
|
10
|
+
skipInstall?: boolean;
|
|
4
11
|
}
|
|
5
12
|
interface DashboardOptions extends CommandOptions {
|
|
6
13
|
port?: number;
|
|
@@ -14,7 +21,7 @@ export declare function getVersion(): string;
|
|
|
14
21
|
* openworkflow init
|
|
15
22
|
* @param options - Command options
|
|
16
23
|
*/
|
|
17
|
-
export declare function init(options?:
|
|
24
|
+
export declare function init(options?: InitOptions): Promise<void>;
|
|
18
25
|
/**
|
|
19
26
|
* openworkflow doctor
|
|
20
27
|
* @param options - Command options
|
|
@@ -63,6 +70,7 @@ export declare function dashboard(options?: DashboardOptions): Promise<void>;
|
|
|
63
70
|
*/
|
|
64
71
|
export declare function discoverWorkflowFiles(dirs: string[], baseDir: string, ignorePatterns?: string[]): string[];
|
|
65
72
|
interface PackageJsonForDoctor {
|
|
73
|
+
scripts?: Record<string, string>;
|
|
66
74
|
dependencies?: Record<string, string>;
|
|
67
75
|
devDependencies?: Record<string, string>;
|
|
68
76
|
}
|
package/dist/commands.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../commands.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../commands.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAIb,MAAM,aAAa,CAAC;AAwCrB,KAAK,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAC;AAEpD,UAAU,cAAc;IACtB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,WAAY,SAAQ,cAAc;IAC1C,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,UAAU,gBAAiB,SAAQ,cAAc;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAoBnC;AAED;;;GAGG;AAEH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqJnE;AA8BD;;;GAGG;AACH,wBAAsB,MAAM,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2DxE;AAED,MAAM,MAAM,kBAAkB,GAAG,YAAY,GAAG,cAAc,CAAC;AAE/D;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,IAAI,CAAC,CA8Ef;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,YAAY,EAAE;QACZ,KAAK,EAAE,SAAS,CAAC;QACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;KACzB,CAAC;CACH,CAYA;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAavE;AAED;;;;GAIG;AACH,wBAAsB,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6D7E;AA4PD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,MAAM,EACf,cAAc,GAAE,MAAM,EAAO,GAC5B,MAAM,EAAE,CA8CV;AA2WD,UAAU,oBAAoB;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAoCD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,IAAI,GACjD,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,IAAI,GACjD,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,IAAI,GACjD,MAAM,CAER;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,QAAQ,CAAC,oBAAoB,CAAC,GAAG,IAAI,GACjD,MAAM,CAER"}
|
package/dist/commands.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { CLIError } from "./errors.js";
|
|
1
|
+
import { findConfigFile, loadConfigFromPath, } from "./config.js";
|
|
2
|
+
import { CLIError, exit } from "./errors.js";
|
|
3
|
+
import { createModuleLoader } from "./module-loader.js";
|
|
3
4
|
import { CONFIG, HELLO_WORLD_RUNNER, HELLO_WORLD_WORKFLOW, POSTGRES_CLIENT, POSTGRES_PROD_SQLITE_DEV_CLIENT, SQLITE_CLIENT, } from "./templates.js";
|
|
4
5
|
import * as p from "@clack/prompts";
|
|
5
6
|
import { consola } from "consola";
|
|
6
7
|
import { config as loadDotenv } from "dotenv";
|
|
7
|
-
import { createJiti } from "jiti";
|
|
8
8
|
import { spawn } from "node:child_process";
|
|
9
9
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
-
import { addDependency, detectPackageManager } from "nypm";
|
|
12
|
+
import { addDependency, addDependencyCommand, detectPackageManager, } from "nypm";
|
|
13
13
|
import { OpenWorkflow } from "openworkflow";
|
|
14
14
|
import { isWorkflow } from "openworkflow/internal";
|
|
15
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const workflowSources = new WeakMap();
|
|
16
17
|
/**
|
|
17
18
|
* openworkflow -V | --version
|
|
18
19
|
* @returns the version string, or "-" if it cannot be determined
|
|
@@ -40,12 +41,21 @@ export function getVersion() {
|
|
|
40
41
|
* openworkflow init
|
|
41
42
|
* @param options - Command options
|
|
42
43
|
*/
|
|
44
|
+
// oxlint-disable-next-line complexity
|
|
43
45
|
export async function init(options = {}) {
|
|
44
|
-
|
|
46
|
+
if (options.yes && !options.backend) {
|
|
47
|
+
throw new CLIError("--backend is required with --yes.");
|
|
48
|
+
}
|
|
49
|
+
if (!options.yes && !process.stdin.isTTY) {
|
|
50
|
+
throw new CLIError("Interactive setup requires a terminal. Pass --backend sqlite|postgres|both --yes.");
|
|
51
|
+
}
|
|
45
52
|
p.intro("Initializing OpenWorkflow...");
|
|
46
|
-
const
|
|
53
|
+
const configFile = findConfigWithEnv(options);
|
|
47
54
|
let configFileToDelete = null;
|
|
48
|
-
if (configFile) {
|
|
55
|
+
if (configFile && existsSync(configFile)) {
|
|
56
|
+
if (options.yes) {
|
|
57
|
+
throw new CLIError(`Config file already exists at ${configFile}. --yes does not allow overwrites.`);
|
|
58
|
+
}
|
|
49
59
|
const shouldOverride = await p.confirm({
|
|
50
60
|
message: `Config file already exists at ${configFile}. Override it?`,
|
|
51
61
|
initialValue: false,
|
|
@@ -54,71 +64,84 @@ export async function init(options = {}) {
|
|
|
54
64
|
cancelSetup();
|
|
55
65
|
configFileToDelete = configFile;
|
|
56
66
|
}
|
|
57
|
-
const backendChoice =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
67
|
+
const backendChoice = options.backend ??
|
|
68
|
+
(await p.select({
|
|
69
|
+
message: "Select a backend for OpenWorkflow:",
|
|
70
|
+
options: [
|
|
71
|
+
{
|
|
72
|
+
value: "sqlite",
|
|
73
|
+
label: "SQLite",
|
|
74
|
+
hint: "Recommended for testing and development",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
value: "postgres",
|
|
78
|
+
label: "PostgreSQL",
|
|
79
|
+
hint: "Recommended for production",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
value: "both",
|
|
83
|
+
label: "Both",
|
|
84
|
+
hint: "SQLite for dev, PostgreSQL for production",
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
initialValue: "sqlite",
|
|
88
|
+
}));
|
|
89
|
+
if (typeof backendChoice === "symbol")
|
|
79
90
|
cancelSetup();
|
|
80
91
|
const spinner = p.spinner();
|
|
81
92
|
// detect package manager & install packages
|
|
82
93
|
spinner.start("Detecting package manager...");
|
|
83
94
|
const pm = await detectPackageManager(process.cwd());
|
|
84
|
-
const packageManager = pm?.name ?? "
|
|
95
|
+
const packageManager = pm?.name ?? "npm";
|
|
85
96
|
spinner.stop(`Using ${packageManager}`);
|
|
86
97
|
const packageJson = readPackageJsonForDoctor();
|
|
87
98
|
if (!packageJson) {
|
|
88
99
|
throw new CLIError("No package.json found.", "Please create a package.json file first by running `npm init` or `npm init -y`.");
|
|
89
100
|
}
|
|
90
|
-
|
|
101
|
+
validateInitManifest(packageJson);
|
|
102
|
+
const configFileName = options.config ?? getConfigFileName(packageJson);
|
|
91
103
|
const clientFileName = getClientFileName(packageJson);
|
|
92
104
|
const exampleWorkflowFileName = getExampleWorkflowFileName(packageJson);
|
|
93
105
|
const runFileName = getRunFileName(packageJson);
|
|
94
106
|
const runCommand = runFileName.endsWith(".ts")
|
|
95
107
|
? `npx tsx openworkflow/${runFileName}`
|
|
96
108
|
: `node openworkflow/${runFileName}`;
|
|
97
|
-
const shouldSetup =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
const shouldSetup = options.yes ??
|
|
110
|
+
(await p.confirm({
|
|
111
|
+
message: options.skipInstall
|
|
112
|
+
? "Set up project files?"
|
|
113
|
+
: "Install packages and set up project files?",
|
|
114
|
+
initialValue: true,
|
|
115
|
+
}));
|
|
101
116
|
if (p.isCancel(shouldSetup))
|
|
102
117
|
cancelSetup();
|
|
103
118
|
if (!shouldSetup) {
|
|
104
119
|
p.outro("Setup skipped.");
|
|
105
120
|
return;
|
|
106
121
|
}
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
const dependencies = getDependenciesToInstall(backendChoice);
|
|
123
|
+
const devDependencies = getDevDependenciesToInstall();
|
|
124
|
+
if (options.skipInstall) {
|
|
125
|
+
p.note([
|
|
126
|
+
addDependencyCommand(packageManager, dependencies),
|
|
127
|
+
addDependencyCommand(packageManager, devDependencies, { dev: true }),
|
|
128
|
+
].join("\n"), "Install dependencies before running OpenWorkflow");
|
|
109
129
|
}
|
|
110
|
-
{
|
|
111
|
-
const dependencies = getDependenciesToInstall(backendChoice);
|
|
130
|
+
else {
|
|
112
131
|
spinner.start(`Installing ${dependencies.join(", ")}...`);
|
|
113
|
-
await addDependency(dependencies, { silent: true });
|
|
132
|
+
await addDependency(dependencies, { silent: true, packageManager });
|
|
114
133
|
spinner.stop(`Installed ${dependencies.join(", ")}`);
|
|
115
|
-
}
|
|
116
|
-
{
|
|
117
|
-
const devDependencies = getDevDependenciesToInstall();
|
|
118
134
|
spinner.start(`Installing ${devDependencies.join(", ")}...`);
|
|
119
|
-
await addDependency(devDependencies, {
|
|
135
|
+
await addDependency(devDependencies, {
|
|
136
|
+
silent: true,
|
|
137
|
+
dev: true,
|
|
138
|
+
packageManager,
|
|
139
|
+
});
|
|
120
140
|
spinner.stop(`Installed ${devDependencies.join(", ")}`);
|
|
121
141
|
}
|
|
142
|
+
if (configFileToDelete) {
|
|
143
|
+
unlinkSync(configFileToDelete);
|
|
144
|
+
}
|
|
122
145
|
createClientFile(backendChoice, clientFileName);
|
|
123
146
|
createExampleWorkflow(exampleWorkflowFileName);
|
|
124
147
|
createRunFile(runFileName);
|
|
@@ -136,56 +159,89 @@ export async function init(options = {}) {
|
|
|
136
159
|
p.note(`➡️ Start a worker:\n$ npx @openworkflow/cli worker start\n\n➡️ Run the example workflow:\n$ ${runCommand}\n\n➡️ View the dashboard:\n$ npx @openworkflow/cli dashboard`, "Next steps");
|
|
137
160
|
p.outro("✅ Setup complete!");
|
|
138
161
|
}
|
|
162
|
+
// Validate the manifest fields that init reads or updates.
|
|
163
|
+
function validateInitManifest(manifest) {
|
|
164
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
165
|
+
throw new CLIError("Invalid package.json: expected an object.");
|
|
166
|
+
}
|
|
167
|
+
for (const key of ["scripts", "dependencies", "devDependencies"]) {
|
|
168
|
+
const field = manifest[key];
|
|
169
|
+
if (field === undefined)
|
|
170
|
+
continue;
|
|
171
|
+
if (field === null ||
|
|
172
|
+
typeof field !== "object" ||
|
|
173
|
+
Array.isArray(field) ||
|
|
174
|
+
Object.values(field).some((value) => typeof value !== "string")) {
|
|
175
|
+
throw new CLIError(`Invalid package.json: ${key} must be an object containing string values.`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const { scripts } = manifest;
|
|
179
|
+
const worker = scripts?.["worker"];
|
|
180
|
+
if (worker !== undefined && worker !== "npx @openworkflow/cli worker start") {
|
|
181
|
+
throw new CLIError("Setup would overwrite package.json scripts.worker.");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
139
184
|
/**
|
|
140
185
|
* openworkflow doctor
|
|
141
186
|
* @param options - Command options
|
|
142
187
|
*/
|
|
143
188
|
export async function doctor(options = {}) {
|
|
144
|
-
const configPath = options.config;
|
|
145
189
|
consola.start("Running OpenWorkflow doctor...");
|
|
146
|
-
const
|
|
190
|
+
const timer = setTimeout(() => {
|
|
191
|
+
consola.error("Doctor timed out after 30 seconds.");
|
|
192
|
+
void exit(1);
|
|
193
|
+
}, 30_000);
|
|
194
|
+
const { config, configFile } = await loadConfigWithEnv(options);
|
|
147
195
|
if (!configFile) {
|
|
148
196
|
throw new CLIError("No config file found.", "Run `npx @openworkflow/cli init` to create a config file.");
|
|
149
197
|
}
|
|
150
198
|
const backend = config.backend;
|
|
199
|
+
let cleanupFailed = false;
|
|
151
200
|
try {
|
|
201
|
+
await checkBackendConnection(backend);
|
|
202
|
+
if (config.worker?.concurrency !== undefined) {
|
|
203
|
+
assertPositiveInteger("concurrency", config.worker.concurrency);
|
|
204
|
+
}
|
|
152
205
|
consola.log("");
|
|
153
|
-
consola.info(`Config file: ${configFile}`);
|
|
206
|
+
consola.info(`Config file: ${path.relative(process.cwd(), configFile)}`);
|
|
154
207
|
const backendName = backend.constructor.name.replace("Backend", "");
|
|
155
208
|
consola.log(` • Backend: ${backendName}`);
|
|
156
|
-
const packageJson = readPackageJsonForDoctor();
|
|
157
|
-
if (packageJson) {
|
|
158
|
-
warnIfMissingBackendPackage(backendName, packageJson);
|
|
159
|
-
warnIfMissingTsconfig(packageJson);
|
|
160
|
-
}
|
|
161
209
|
// discover directories
|
|
162
|
-
const dirs = getWorkflowDirectories(config);
|
|
210
|
+
const dirs = [...new Set(getWorkflowDirectories(config))];
|
|
163
211
|
consola.log(` • Workflow directories: ${dirs.join(", ")}`);
|
|
164
212
|
// discover files
|
|
165
213
|
const configFileDir = path.dirname(configFile);
|
|
166
|
-
const {
|
|
167
|
-
|
|
168
|
-
consola.info(`Found ${String(files.length)} workflow file(s):`);
|
|
169
|
-
for (const file of files) {
|
|
170
|
-
consola.log(` • ${file}`);
|
|
171
|
-
}
|
|
214
|
+
const { workflows } = await discoverWorkflowsInDirs(dirs, configFileDir, config.ignorePatterns ?? []);
|
|
215
|
+
assertNoDuplicateWorkflows(workflows);
|
|
172
216
|
printDiscoveredWorkflows(workflows);
|
|
173
|
-
warnAboutDuplicateWorkflows(workflows);
|
|
174
|
-
consola.log("");
|
|
175
|
-
consola.success("Configuration looks good!");
|
|
176
217
|
}
|
|
177
218
|
finally {
|
|
178
|
-
|
|
219
|
+
// Imported configs can omit the backend despite the declared config type.
|
|
220
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition
|
|
221
|
+
if (typeof backend?.stop === "function") {
|
|
222
|
+
try {
|
|
223
|
+
await backend.stop();
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
cleanupFailed = true;
|
|
227
|
+
consola.error(`Backend cleanup failed: ${String(error)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
179
230
|
}
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
if (cleanupFailed)
|
|
233
|
+
await exit(1);
|
|
234
|
+
consola.log("");
|
|
235
|
+
consola.success("Configuration looks good!");
|
|
236
|
+
await exit(0);
|
|
180
237
|
}
|
|
181
238
|
/**
|
|
182
239
|
* openworkflow worker start
|
|
183
240
|
* @param options - Worker config and command options
|
|
184
241
|
*/
|
|
185
242
|
export async function workerStart(options = {}) {
|
|
186
|
-
const { config: configPath, ...workerConfig } = options;
|
|
187
243
|
consola.start("Starting worker...");
|
|
188
|
-
const { config, configFile } = await loadConfigWithEnv(
|
|
244
|
+
const { config, configFile } = await loadConfigWithEnv(options);
|
|
189
245
|
if (!configFile) {
|
|
190
246
|
throw new CLIError("No config file found.", "Run `npx @openworkflow/cli init` to create a config file.");
|
|
191
247
|
}
|
|
@@ -208,6 +264,7 @@ export async function workerStart(options = {}) {
|
|
|
208
264
|
consola.success("Worker stopped");
|
|
209
265
|
}
|
|
210
266
|
try {
|
|
267
|
+
await checkBackendConnection(backend);
|
|
211
268
|
// discover and import workflows
|
|
212
269
|
const dirs = getWorkflowDirectories(config);
|
|
213
270
|
consola.info(`Discovering workflows from: ${dirs.join(", ")}`);
|
|
@@ -216,7 +273,9 @@ export async function workerStart(options = {}) {
|
|
|
216
273
|
consola.info(`Found ${String(files.length)} workflow file(s)`);
|
|
217
274
|
consola.success(`Loaded ${String(workflows.length)} workflow(s): ${workflows.map((w) => w.spec.name).join(", ")}`);
|
|
218
275
|
assertNoDuplicateWorkflows(workflows);
|
|
219
|
-
const workerOptions = mergeDefinedOptions(config.worker,
|
|
276
|
+
const workerOptions = mergeDefinedOptions(config.worker, {
|
|
277
|
+
concurrency: options.concurrency,
|
|
278
|
+
});
|
|
220
279
|
if (workerOptions.concurrency !== undefined) {
|
|
221
280
|
assertPositiveInteger("concurrency", workerOptions.concurrency);
|
|
222
281
|
}
|
|
@@ -231,7 +290,12 @@ export async function workerStart(options = {}) {
|
|
|
231
290
|
consola.success("Worker started.");
|
|
232
291
|
}
|
|
233
292
|
catch (error) {
|
|
234
|
-
|
|
293
|
+
try {
|
|
294
|
+
await gracefulShutdown();
|
|
295
|
+
}
|
|
296
|
+
catch (cleanupError) {
|
|
297
|
+
consola.warn(`Backend cleanup failed: ${String(cleanupError)}`);
|
|
298
|
+
}
|
|
235
299
|
throw error;
|
|
236
300
|
}
|
|
237
301
|
}
|
|
@@ -274,10 +338,9 @@ export function validateDashboardPort(port) {
|
|
|
274
338
|
* @returns Resolves when the dashboard process exits.
|
|
275
339
|
*/
|
|
276
340
|
export async function dashboard(options = {}) {
|
|
277
|
-
const configPath = options.config;
|
|
278
341
|
const port = validateDashboardPort(options.port);
|
|
279
342
|
consola.start("Starting dashboard...");
|
|
280
|
-
const { configFile } = await loadConfigWithEnv(
|
|
343
|
+
const { configFile } = await loadConfigWithEnv(options);
|
|
281
344
|
if (!configFile) {
|
|
282
345
|
throw new CLIError("No config file found.", "Run `npx @openworkflow/cli init` to create a config file before starting the dashboard.");
|
|
283
346
|
}
|
|
@@ -320,9 +383,25 @@ export async function dashboard(options = {}) {
|
|
|
320
383
|
*/
|
|
321
384
|
function cancelSetup() {
|
|
322
385
|
p.cancel("Setup canceled.");
|
|
323
|
-
//
|
|
386
|
+
// oxlint-disable-next-line unicorn/no-process-exit
|
|
324
387
|
process.exit(0);
|
|
325
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Exercise backend initialization, connectivity, and workflow table access.
|
|
391
|
+
* @param backend - Configured backend
|
|
392
|
+
*/
|
|
393
|
+
async function checkBackendConnection(backend) {
|
|
394
|
+
if (typeof backend?.listWorkflowRuns !== "function" ||
|
|
395
|
+
typeof backend.stop !== "function") {
|
|
396
|
+
throw new CLIError("Missing or invalid backend.", "Set config.backend to a connected OpenWorkflow backend.");
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
await backend.listWorkflowRuns({ limit: 1 });
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
throw new CLIError("Failed to access backend.", error instanceof Error ? error.message : String(error));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
326
405
|
/**
|
|
327
406
|
* Get workflow directories from config.
|
|
328
407
|
* @param config - The loaded config
|
|
@@ -354,7 +433,7 @@ function findDuplicateWorkflows(workflows) {
|
|
|
354
433
|
for (const workflow of workflows) {
|
|
355
434
|
const name = workflow.spec.name;
|
|
356
435
|
const version = workflow.spec.version ?? null;
|
|
357
|
-
const key =
|
|
436
|
+
const key = JSON.stringify([name, version]);
|
|
358
437
|
const existing = workflowKeys.get(key);
|
|
359
438
|
if (existing) {
|
|
360
439
|
existing.count += 1;
|
|
@@ -382,29 +461,18 @@ function assertNoDuplicateWorkflows(workflows) {
|
|
|
382
461
|
const suffix = remaining > 0 ? ` (+${String(remaining)} more)` : "";
|
|
383
462
|
throw new CLIError(`Duplicate workflow name${duplicates.length === 1 ? "" : "s"} detected: ${preview}${suffix}`, "Multiple workflow files export workflows with the same name and version. Each workflow must have a unique name and version combination.");
|
|
384
463
|
}
|
|
385
|
-
/**
|
|
386
|
-
* Warn about duplicate workflows without failing.
|
|
387
|
-
* @param workflows - Discovered workflows
|
|
388
|
-
*/
|
|
389
|
-
function warnAboutDuplicateWorkflows(workflows) {
|
|
390
|
-
const duplicates = findDuplicateWorkflows(workflows);
|
|
391
|
-
for (const duplicate of duplicates) {
|
|
392
|
-
consola.warn(`Duplicate workflow detected: ${formatWorkflowIdentity(duplicate.name, duplicate.version)}`);
|
|
393
|
-
consola.warn("Multiple files export a workflow with the same name and version.");
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
464
|
/**
|
|
397
465
|
* Print discovered workflows to the console.
|
|
398
466
|
* @param workflows - Array of discovered workflows
|
|
399
467
|
*/
|
|
400
468
|
function printDiscoveredWorkflows(workflows) {
|
|
401
469
|
consola.log("");
|
|
402
|
-
consola.info(`
|
|
470
|
+
consola.info(`Found ${String(workflows.length)} workflow${workflows.length === 1 ? "" : "s"}:`);
|
|
403
471
|
for (const workflow of workflows) {
|
|
404
472
|
const name = workflow.spec.name;
|
|
405
|
-
const version = workflow.spec.version
|
|
406
|
-
const versionStr = version
|
|
407
|
-
consola.log(` • ${name}${versionStr}`);
|
|
473
|
+
const version = workflow.spec.version;
|
|
474
|
+
const versionStr = version ? ` (${version})` : "";
|
|
475
|
+
consola.log(` • ${name}${versionStr} — ${workflowSources.get(workflow)}`);
|
|
408
476
|
}
|
|
409
477
|
}
|
|
410
478
|
const WORKFLOW_EXTENSIONS = ["ts", "mts", "cts", "js", "mjs", "cjs"];
|
|
@@ -476,8 +544,8 @@ function globToRegExp(pattern) {
|
|
|
476
544
|
return new RegExp(regex);
|
|
477
545
|
}
|
|
478
546
|
/**
|
|
479
|
-
* Check whether a file path matches ignore patterns.
|
|
480
|
-
* @param filePath - Absolute
|
|
547
|
+
* Check whether a file or directory path matches ignore patterns.
|
|
548
|
+
* @param filePath - Absolute path, with a trailing separator for directories
|
|
481
549
|
* @param baseDir - Base directory for relative matching
|
|
482
550
|
* @param matchers - Compiled regex matchers
|
|
483
551
|
* @returns Whether the file should be ignored
|
|
@@ -487,7 +555,11 @@ function isIgnoredFile(filePath, baseDir, matchers) {
|
|
|
487
555
|
return false;
|
|
488
556
|
const relativePath = normalizeForGlobMatch(path.relative(baseDir, filePath));
|
|
489
557
|
const fileName = path.basename(filePath);
|
|
490
|
-
|
|
558
|
+
const isDirectory = filePath.endsWith(path.sep);
|
|
559
|
+
return matchers.some((matcher) => matcher.test(relativePath) ||
|
|
560
|
+
matcher.test(fileName) ||
|
|
561
|
+
(isDirectory &&
|
|
562
|
+
(matcher.test(`${relativePath}/`) || matcher.test(`${fileName}/`))));
|
|
491
563
|
}
|
|
492
564
|
/**
|
|
493
565
|
* Discover workflow files from directories. Recursively scans directories for
|
|
@@ -512,19 +584,18 @@ export function discoverWorkflowFiles(dirs, baseDir, ignorePatterns = []) {
|
|
|
512
584
|
entries = readdirSync(absoluteDir, { withFileTypes: true });
|
|
513
585
|
}
|
|
514
586
|
catch (error) {
|
|
515
|
-
|
|
516
|
-
const errMessage = error instanceof Error ? error.message : String(error);
|
|
517
|
-
consola.debug(`Failed to read directory: ${absoluteDir} - ${errMessage}`);
|
|
518
|
-
return;
|
|
587
|
+
throw new CLIError(`Cannot read workflow directory: ${absoluteDir}`, `${String(error)}\nCorrect config.dirs or make the directory readable.`);
|
|
519
588
|
}
|
|
520
589
|
for (const entry of entries) {
|
|
521
590
|
const fullPath = path.join(absoluteDir, entry.name);
|
|
522
591
|
if (entry.isDirectory()) {
|
|
523
|
-
|
|
592
|
+
if (!isIgnoredFile(`${fullPath}${path.sep}`, baseDir, matchers)) {
|
|
593
|
+
scanDirectory(fullPath);
|
|
594
|
+
}
|
|
524
595
|
}
|
|
525
596
|
else if (entry.isFile() &&
|
|
526
597
|
WORKFLOW_EXTENSIONS.some((ext) => entry.name.endsWith(`.${ext}`)) &&
|
|
527
|
-
|
|
598
|
+
!/\.d\.(?:ts|mts|cts)$/.test(entry.name) &&
|
|
528
599
|
!isIgnoredFile(fullPath, baseDir, matchers)) {
|
|
529
600
|
discoveredFiles.push(fullPath);
|
|
530
601
|
}
|
|
@@ -533,7 +604,7 @@ export function discoverWorkflowFiles(dirs, baseDir, ignorePatterns = []) {
|
|
|
533
604
|
for (const dir of dirs) {
|
|
534
605
|
scanDirectory(dir);
|
|
535
606
|
}
|
|
536
|
-
return discoveredFiles;
|
|
607
|
+
return [...new Set(discoveredFiles)];
|
|
537
608
|
}
|
|
538
609
|
/**
|
|
539
610
|
* Import workflow files and extract workflow exports.
|
|
@@ -543,11 +614,11 @@ export function discoverWorkflowFiles(dirs, baseDir, ignorePatterns = []) {
|
|
|
543
614
|
*/
|
|
544
615
|
async function importWorkflows(files) {
|
|
545
616
|
const workflows = [];
|
|
546
|
-
const jiti = createJiti(import.meta.url);
|
|
547
617
|
for (const file of files) {
|
|
548
618
|
// import the module
|
|
549
619
|
let module;
|
|
550
620
|
try {
|
|
621
|
+
const jiti = createModuleLoader(file);
|
|
551
622
|
module = await jiti.import(pathToFileURL(file).href);
|
|
552
623
|
}
|
|
553
624
|
catch (error) {
|
|
@@ -558,11 +629,12 @@ async function importWorkflows(files) {
|
|
|
558
629
|
for (const [key, value] of Object.entries(module)) {
|
|
559
630
|
if (isWorkflow(value)) {
|
|
560
631
|
workflows.push(value);
|
|
632
|
+
workflowSources.set(value, path.relative(process.cwd(), file));
|
|
561
633
|
consola.debug(`Found workflow "${value.spec.name}" in ${file} (${key})`);
|
|
562
634
|
}
|
|
563
635
|
}
|
|
564
636
|
}
|
|
565
|
-
return workflows;
|
|
637
|
+
return [...new Set(workflows)];
|
|
566
638
|
}
|
|
567
639
|
/**
|
|
568
640
|
* Discover workflow files and import workflows with common error handling.
|
|
@@ -717,7 +789,7 @@ function addWorkerScriptToPackageJson() {
|
|
|
717
789
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
718
790
|
packageJson.scripts ??= {};
|
|
719
791
|
packageJson.scripts["worker"] = "npx @openworkflow/cli worker start";
|
|
720
|
-
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)
|
|
792
|
+
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
|
721
793
|
spinner.stop('Added "worker" script to package.json');
|
|
722
794
|
}
|
|
723
795
|
catch {
|
|
@@ -766,16 +838,30 @@ function updateEnvForPostgres() {
|
|
|
766
838
|
: "OPENWORKFLOW_POSTGRES_URL already in .env");
|
|
767
839
|
}
|
|
768
840
|
/**
|
|
769
|
-
*
|
|
770
|
-
* @param
|
|
771
|
-
* @returns
|
|
841
|
+
* Find the config and load its environment without importing it.
|
|
842
|
+
* @param options - Config and environment file paths
|
|
843
|
+
* @returns Config path, if found.
|
|
772
844
|
*/
|
|
773
|
-
|
|
774
|
-
|
|
845
|
+
function findConfigWithEnv(options) {
|
|
846
|
+
const { envFile } = options;
|
|
847
|
+
const configPath = options.config
|
|
848
|
+
? path.resolve(options.config)
|
|
849
|
+
: findConfigFile();
|
|
850
|
+
const baseDir = configPath ? path.dirname(configPath) : process.cwd();
|
|
851
|
+
const { error } = loadDotenv({
|
|
852
|
+
path: envFile ?? path.join(baseDir, ".env"),
|
|
853
|
+
quiet: true,
|
|
854
|
+
});
|
|
855
|
+
if (envFile !== undefined && error) {
|
|
856
|
+
throw new CLIError(`Failed to load environment file: ${envFile}`, error.message);
|
|
857
|
+
}
|
|
858
|
+
return configPath;
|
|
859
|
+
}
|
|
860
|
+
// Load the environment before importing config for commands that use it.
|
|
861
|
+
async function loadConfigWithEnv(options) {
|
|
862
|
+
const configPath = findConfigWithEnv(options);
|
|
775
863
|
try {
|
|
776
|
-
return configPath
|
|
777
|
-
? await loadConfigFromPath(configPath)
|
|
778
|
-
: await loadConfig();
|
|
864
|
+
return await loadConfigFromPath(configPath ?? "openworkflow.config.ts");
|
|
779
865
|
}
|
|
780
866
|
catch (error) {
|
|
781
867
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -851,35 +937,6 @@ export function getClientFileName(packageJson) {
|
|
|
851
937
|
function hasDependency(packageJson, name) {
|
|
852
938
|
return Boolean(packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name]);
|
|
853
939
|
}
|
|
854
|
-
/**
|
|
855
|
-
* Warn when the configured backend is missing its package.
|
|
856
|
-
* @param backendName - Configured backend name.
|
|
857
|
-
* @param packageJson - Parsed package.json.
|
|
858
|
-
*/
|
|
859
|
-
function warnIfMissingBackendPackage(backendName, packageJson) {
|
|
860
|
-
const backendNameLower = backendName.toLowerCase();
|
|
861
|
-
const isPostgres = backendNameLower.includes("postgres");
|
|
862
|
-
const isSqlite = backendNameLower.includes("sqlite");
|
|
863
|
-
if ((isPostgres || isSqlite) && !hasDependency(packageJson, "openworkflow")) {
|
|
864
|
-
consola.warn(`Backend is ${backendName} but openworkflow is not installed.`);
|
|
865
|
-
}
|
|
866
|
-
if (isPostgres && !hasDependency(packageJson, "postgres")) {
|
|
867
|
-
consola.warn(`Backend is ${backendName} but the postgres driver is not installed.`);
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
/**
|
|
871
|
-
* Warn when TypeScript is installed but tsconfig.json is missing.
|
|
872
|
-
* @param packageJson - Parsed package.json.
|
|
873
|
-
*/
|
|
874
|
-
function warnIfMissingTsconfig(packageJson) {
|
|
875
|
-
if (!hasDependency(packageJson, "typescript")) {
|
|
876
|
-
return;
|
|
877
|
-
}
|
|
878
|
-
const tsconfigPath = path.join(process.cwd(), "tsconfig.json");
|
|
879
|
-
if (!existsSync(tsconfigPath)) {
|
|
880
|
-
consola.warn("TypeScript is installed but no tsconfig.json was found.");
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
940
|
/**
|
|
884
941
|
* Ensure a specific environment variable exists in a .env file. Creates the
|
|
885
942
|
* file if it doesn't exist, appends the variable if not present.
|
package/dist/config.d.ts
CHANGED
|
@@ -46,5 +46,11 @@ export declare function loadConfigFromPath(configPath: string, startDir?: string
|
|
|
46
46
|
* @returns The loaded configuration and metadata
|
|
47
47
|
*/
|
|
48
48
|
export declare function loadConfig(startDir?: string): Promise<LoadedConfig>;
|
|
49
|
+
/**
|
|
50
|
+
* Find the nearest config without executing it, so its environment can load first.
|
|
51
|
+
* @param startDir - Directory to search from
|
|
52
|
+
* @returns Config path, if found
|
|
53
|
+
*/
|
|
54
|
+
export declare function findConfigFile(startDir?: string): string | undefined;
|
|
49
55
|
export {};
|
|
50
56
|
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CAE3E;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACzB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;AAE9D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CAE3E;AAED,UAAU,YAAY;IACpB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAKD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CACtC,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,YAAY,CAAC,CAKvB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAGzE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,SAAgB,GAAG,MAAM,GAAG,SAAS,CAyB3E"}
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createModuleLoader } from "./module-loader.js";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -12,7 +12,6 @@ export function defineConfig(config) {
|
|
|
12
12
|
}
|
|
13
13
|
const CONFIG_NAME = "openworkflow.config";
|
|
14
14
|
const CONFIG_EXTENSIONS = ["ts", "mts", "cts", "js", "mjs", "cjs"];
|
|
15
|
-
const jiti = createJiti(import.meta.url, { tryNative: false }); // bun compatibility
|
|
16
15
|
/**
|
|
17
16
|
* Load OpenWorkflow config from an explicit path.
|
|
18
17
|
* @param configPath - Explicit config file path
|
|
@@ -34,15 +33,24 @@ export async function loadConfigFromPath(configPath, startDir) {
|
|
|
34
33
|
* @returns The loaded configuration and metadata
|
|
35
34
|
*/
|
|
36
35
|
export async function loadConfig(startDir) {
|
|
37
|
-
|
|
36
|
+
const configFile = findConfigFile(startDir);
|
|
37
|
+
return configFile ? importConfigFile(configFile) : getEmptyLoadedConfig();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Find the nearest config without executing it, so its environment can load first.
|
|
41
|
+
* @param startDir - Directory to search from
|
|
42
|
+
* @returns Config path, if found
|
|
43
|
+
*/
|
|
44
|
+
export function findConfigFile(startDir = process.cwd()) {
|
|
45
|
+
let currentDir = path.resolve(startDir);
|
|
38
46
|
// search up the directory tree
|
|
39
|
-
//
|
|
47
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition
|
|
40
48
|
while (true) {
|
|
41
49
|
for (const ext of CONFIG_EXTENSIONS) {
|
|
42
50
|
const fileName = `${CONFIG_NAME}.${ext}`;
|
|
43
51
|
const filePath = path.join(currentDir, fileName);
|
|
44
52
|
if (existsSync(filePath)) {
|
|
45
|
-
return
|
|
53
|
+
return filePath;
|
|
46
54
|
}
|
|
47
55
|
}
|
|
48
56
|
const parentDir = path.dirname(currentDir);
|
|
@@ -52,7 +60,7 @@ export async function loadConfig(startDir) {
|
|
|
52
60
|
}
|
|
53
61
|
currentDir = parentDir;
|
|
54
62
|
}
|
|
55
|
-
return
|
|
63
|
+
return undefined;
|
|
56
64
|
}
|
|
57
65
|
/**
|
|
58
66
|
* Import a config file and wrap load errors with a stable message.
|
|
@@ -61,17 +69,23 @@ export async function loadConfig(startDir) {
|
|
|
61
69
|
*/
|
|
62
70
|
async function importConfigFile(filePath) {
|
|
63
71
|
try {
|
|
72
|
+
const jiti = createModuleLoader(filePath, { tryNative: false }); // bun compatibility
|
|
64
73
|
const fileUrl = pathToFileURL(filePath).href;
|
|
65
74
|
const config = await jiti.import(fileUrl, {
|
|
66
75
|
default: true,
|
|
67
76
|
});
|
|
77
|
+
if (typeof config !== "object" || config === null) {
|
|
78
|
+
throw new Error("Config must export an object.");
|
|
79
|
+
}
|
|
68
80
|
return {
|
|
69
|
-
config,
|
|
81
|
+
config: config,
|
|
70
82
|
configFile: filePath,
|
|
71
83
|
};
|
|
72
84
|
}
|
|
73
85
|
catch (error) {
|
|
74
|
-
throw new Error(`Failed to load config file ${filePath}: ${String(error)}
|
|
86
|
+
throw new Error(`Failed to load config file ${filePath}: ${String(error)}`, {
|
|
87
|
+
cause: error,
|
|
88
|
+
});
|
|
75
89
|
}
|
|
76
90
|
}
|
|
77
91
|
/**
|
package/dist/errors.d.ts
CHANGED
|
@@ -5,6 +5,11 @@ export declare class CLIError extends Error {
|
|
|
5
5
|
readonly detail: string | undefined;
|
|
6
6
|
constructor(message: string, detail?: string);
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Finish writing CLI output before exiting, including when handles remain open.
|
|
10
|
+
* @param code - Process exit code
|
|
11
|
+
*/
|
|
12
|
+
export declare function exit(code: number): Promise<never>;
|
|
8
13
|
/**
|
|
9
14
|
* Wraps a CLI action / handler function with error handling that catches
|
|
10
15
|
* errors, prints them to the console, then exits.
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../errors.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;gBAExB,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAK7C;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,EAAE,EACnD,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACvC,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../errors.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;gBAExB,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM;CAK7C;AAED;;;GAGG;AACH,wBAAsB,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAWvD;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,OAAO,EAAE,EACnD,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACvC,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAiB/B"}
|
package/dist/errors.js
CHANGED
|
@@ -11,6 +11,17 @@ export class CLIError extends Error {
|
|
|
11
11
|
this.detail = detail;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Finish writing CLI output before exiting, including when handles remain open.
|
|
16
|
+
* @param code - Process exit code
|
|
17
|
+
*/
|
|
18
|
+
export async function exit(code) {
|
|
19
|
+
await Promise.all([process.stdout, process.stderr].map((stream) => new Promise((resolve) => {
|
|
20
|
+
stream.end(resolve);
|
|
21
|
+
})));
|
|
22
|
+
// oxlint-disable-next-line unicorn/no-process-exit
|
|
23
|
+
process.exit(code);
|
|
24
|
+
}
|
|
14
25
|
/**
|
|
15
26
|
* Wraps a CLI action / handler function with error handling that catches
|
|
16
27
|
* errors, prints them to the console, then exits.
|
|
@@ -24,19 +35,15 @@ export function withErrorHandling(fn) {
|
|
|
24
35
|
}
|
|
25
36
|
catch (error) {
|
|
26
37
|
if (error instanceof CLIError) {
|
|
27
|
-
consola.error(error.message);
|
|
28
|
-
|
|
29
|
-
consola.info(error.detail);
|
|
30
|
-
// eslint-disable-next-line unicorn/no-process-exit
|
|
31
|
-
process.exit(1);
|
|
38
|
+
consola.error([error.message, error.detail].filter(Boolean).join("\n"));
|
|
39
|
+
return exit(1);
|
|
32
40
|
}
|
|
33
41
|
const message = error instanceof Error ? error.message : String(error);
|
|
34
42
|
consola.error(`Unexpected error: ${message}`);
|
|
35
43
|
if (error instanceof Error && error.stack) {
|
|
36
44
|
consola.debug(error.stack);
|
|
37
45
|
}
|
|
38
|
-
|
|
39
|
-
process.exit(1);
|
|
46
|
+
return exit(1);
|
|
40
47
|
}
|
|
41
48
|
};
|
|
42
49
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Jiti, type JitiOptions } from "jiti";
|
|
2
|
+
/**
|
|
3
|
+
* Create a loader using the requested file's nearest tsconfig when available.
|
|
4
|
+
* @param filePath - Absolute path to the file being loaded.
|
|
5
|
+
* @param options - Runtime import options.
|
|
6
|
+
* @returns A loader with optional TypeScript path resolution.
|
|
7
|
+
*/
|
|
8
|
+
export declare function createModuleLoader(filePath: string, options?: JitiOptions): Jiti;
|
|
9
|
+
//# sourceMappingURL=module-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"module-loader.d.ts","sourceRoot":"","sources":["../module-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,IAAI,EAAE,KAAK,WAAW,EAAE,MAAM,MAAM,CAAC;AAE/D;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,WAAgB,GACxB,IAAI,CAQN"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createJiti } from "jiti";
|
|
2
|
+
/**
|
|
3
|
+
* Create a loader using the requested file's nearest tsconfig when available.
|
|
4
|
+
* @param filePath - Absolute path to the file being loaded.
|
|
5
|
+
* @param options - Runtime import options.
|
|
6
|
+
* @returns A loader with optional TypeScript path resolution.
|
|
7
|
+
*/
|
|
8
|
+
export function createModuleLoader(filePath, options = {}) {
|
|
9
|
+
try {
|
|
10
|
+
return createJiti(filePath, { ...options, tsconfigPaths: true });
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
// Deployments may omit development dependencies referenced by tsconfig.
|
|
14
|
+
// Fall back before executing user code so ordinary imports still work.
|
|
15
|
+
return createJiti(filePath, { ...options, tsconfigPaths: false });
|
|
16
|
+
}
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openworkflow/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -27,18 +27,18 @@
|
|
|
27
27
|
"prepublishOnly": "npm run build"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@clack/prompts": "^1.
|
|
30
|
+
"@clack/prompts": "^1.8.0",
|
|
31
31
|
"commander": "^15.0.0",
|
|
32
32
|
"consola": "^3.4.2",
|
|
33
33
|
"dotenv": "^17.4.2",
|
|
34
34
|
"jiti": "^2.7.0",
|
|
35
|
-
"nypm": "^0.6.
|
|
35
|
+
"nypm": "^0.6.10"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"openworkflow": "*",
|
|
39
|
-
"vitest": "^
|
|
39
|
+
"vitest": "^5.0.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"openworkflow": "^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0"
|
|
42
|
+
"openworkflow": "^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0"
|
|
43
43
|
}
|
|
44
44
|
}
|