@openworkflow/cli 0.4.5 → 0.5.1
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 +30 -4
- package/dist/commands.d.ts +10 -1
- package/dist/commands.d.ts.map +1 -1
- package/dist/commands.js +202 -140
- 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 +16 -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/dist/telemetry.d.ts +16 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +101 -0
- package/package.json +6 -5
package/dist/cli.js
CHANGED
|
@@ -2,25 +2,38 @@
|
|
|
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 {
|
|
5
|
+
import { initializeTelemetry, shutdownTelemetry, trackCommand, } from "./telemetry.js";
|
|
6
|
+
import { Command, CommanderError, Option } from "commander";
|
|
6
7
|
// openworkflow
|
|
7
8
|
const program = new Command();
|
|
9
|
+
initializeTelemetry(program);
|
|
8
10
|
program
|
|
9
11
|
.name("openworkflow")
|
|
10
12
|
.description("OpenWorkflow CLI - learn more at https://openworkflow.dev")
|
|
11
13
|
.usage("<command> [options]")
|
|
12
|
-
.
|
|
14
|
+
.exitOverride()
|
|
15
|
+
.version(getVersion())
|
|
16
|
+
.option("--no-telemetry", "disable telemetry");
|
|
13
17
|
// init
|
|
14
18
|
program
|
|
15
19
|
.command("init")
|
|
16
20
|
.description("initialize OpenWorkflow")
|
|
21
|
+
.addOption(new Option("--backend <backend>", "backend to configure").choices([
|
|
22
|
+
"sqlite",
|
|
23
|
+
"postgres",
|
|
24
|
+
"both",
|
|
25
|
+
]))
|
|
26
|
+
.option("-y, --yes", "skip prompts (requires --backend; does not allow overwrites)")
|
|
27
|
+
.option("--skip-install", "create project files without installing dependencies")
|
|
17
28
|
.option("--config <path>", "path to OpenWorkflow config file")
|
|
29
|
+
.option("--env-file <path>", "load environment variables from file")
|
|
18
30
|
.action(withErrorHandling(init));
|
|
19
31
|
// doctor
|
|
20
32
|
program
|
|
21
33
|
.command("doctor")
|
|
22
|
-
.description("check
|
|
34
|
+
.description("check worker prerequisites")
|
|
23
35
|
.option("--config <path>", "path to OpenWorkflow config file")
|
|
36
|
+
.option("--env-file <path>", "load environment variables from file")
|
|
24
37
|
.action(withErrorHandling(doctor));
|
|
25
38
|
// worker
|
|
26
39
|
const workerCmd = program.command("worker").description("manage workers");
|
|
@@ -30,6 +43,7 @@ workerCmd
|
|
|
30
43
|
.description("start a worker to process workflows")
|
|
31
44
|
.option("-c, --concurrency <number>", "number of concurrent workflows to process", Number.parseInt)
|
|
32
45
|
.option("--config <path>", "path to OpenWorkflow config file")
|
|
46
|
+
.option("--env-file <path>", "load environment variables from file")
|
|
33
47
|
.action(withErrorHandling(workerStart));
|
|
34
48
|
// dashboard
|
|
35
49
|
program
|
|
@@ -37,5 +51,17 @@ program
|
|
|
37
51
|
.description("start the dashboard to view workflow runs")
|
|
38
52
|
.option("-p, --port <number>", "custom port for the dashboard server", Number.parseInt)
|
|
39
53
|
.option("--config <path>", "path to OpenWorkflow config file")
|
|
54
|
+
.option("--env-file <path>", "load environment variables from file")
|
|
40
55
|
.action(withErrorHandling(dashboard));
|
|
41
|
-
|
|
56
|
+
try {
|
|
57
|
+
await program.parseAsync(process.argv);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (!(error instanceof CommanderError))
|
|
61
|
+
throw error;
|
|
62
|
+
process.exitCode = error.exitCode;
|
|
63
|
+
trackCommand();
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
await shutdownTelemetry();
|
|
67
|
+
}
|
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;
|
|
@@ -13,8 +20,9 @@ export declare function getVersion(): string;
|
|
|
13
20
|
/**
|
|
14
21
|
* openworkflow init
|
|
15
22
|
* @param options - Command options
|
|
23
|
+
* @returns Resolves when setup finishes.
|
|
16
24
|
*/
|
|
17
|
-
export declare function init(options?:
|
|
25
|
+
export declare function init(options?: InitOptions): Promise<void>;
|
|
18
26
|
/**
|
|
19
27
|
* openworkflow doctor
|
|
20
28
|
* @param options - Command options
|
|
@@ -63,6 +71,7 @@ export declare function dashboard(options?: DashboardOptions): Promise<void>;
|
|
|
63
71
|
*/
|
|
64
72
|
export declare function discoverWorkflowFiles(dirs: string[], baseDir: string, ignorePatterns?: string[]): string[];
|
|
65
73
|
interface PackageJsonForDoctor {
|
|
74
|
+
scripts?: Record<string, string>;
|
|
66
75
|
dependencies?: Record<string, string>;
|
|
67
76
|
devDependencies?: Record<string, string>;
|
|
68
77
|
}
|
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;AAyCrB,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;;;;GAIG;AAEH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsJnE;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;AA+WD,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,20 @@
|
|
|
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";
|
|
4
|
+
import { trackCommand } from "./telemetry.js";
|
|
3
5
|
import { CONFIG, HELLO_WORLD_RUNNER, HELLO_WORLD_WORKFLOW, POSTGRES_CLIENT, POSTGRES_PROD_SQLITE_DEV_CLIENT, SQLITE_CLIENT, } from "./templates.js";
|
|
4
6
|
import * as p from "@clack/prompts";
|
|
5
7
|
import { consola } from "consola";
|
|
6
8
|
import { config as loadDotenv } from "dotenv";
|
|
7
|
-
import { createJiti } from "jiti";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
10
|
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
10
11
|
import path from "node:path";
|
|
11
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
|
-
import { addDependency, detectPackageManager } from "nypm";
|
|
13
|
+
import { addDependency, addDependencyCommand, detectPackageManager, } from "nypm";
|
|
13
14
|
import { OpenWorkflow } from "openworkflow";
|
|
14
15
|
import { isWorkflow } from "openworkflow/internal";
|
|
15
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const workflowSources = new WeakMap();
|
|
16
18
|
/**
|
|
17
19
|
* openworkflow -V | --version
|
|
18
20
|
* @returns the version string, or "-" if it cannot be determined
|
|
@@ -39,86 +41,110 @@ export function getVersion() {
|
|
|
39
41
|
/**
|
|
40
42
|
* openworkflow init
|
|
41
43
|
* @param options - Command options
|
|
44
|
+
* @returns Resolves when setup finishes.
|
|
42
45
|
*/
|
|
46
|
+
// oxlint-disable-next-line complexity
|
|
43
47
|
export async function init(options = {}) {
|
|
44
|
-
|
|
48
|
+
if (options.yes && !options.backend) {
|
|
49
|
+
throw new CLIError("--backend is required with --yes.");
|
|
50
|
+
}
|
|
51
|
+
if (!options.yes && !process.stdin.isTTY) {
|
|
52
|
+
throw new CLIError("Interactive setup requires a terminal. Pass --backend sqlite|postgres|both --yes.");
|
|
53
|
+
}
|
|
45
54
|
p.intro("Initializing OpenWorkflow...");
|
|
46
|
-
const
|
|
55
|
+
const configFile = findConfigWithEnv(options);
|
|
47
56
|
let configFileToDelete = null;
|
|
48
|
-
if (configFile) {
|
|
57
|
+
if (configFile && existsSync(configFile)) {
|
|
58
|
+
if (options.yes) {
|
|
59
|
+
throw new CLIError(`Config file already exists at ${configFile}. --yes does not allow overwrites.`);
|
|
60
|
+
}
|
|
49
61
|
const shouldOverride = await p.confirm({
|
|
50
62
|
message: `Config file already exists at ${configFile}. Override it?`,
|
|
51
63
|
initialValue: false,
|
|
52
64
|
});
|
|
53
65
|
if (!shouldOverride || p.isCancel(shouldOverride))
|
|
54
|
-
cancelSetup();
|
|
66
|
+
return cancelSetup();
|
|
55
67
|
configFileToDelete = configFile;
|
|
56
68
|
}
|
|
57
|
-
const backendChoice =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
69
|
+
const backendChoice = options.backend ??
|
|
70
|
+
(await p.select({
|
|
71
|
+
message: "Select a backend for OpenWorkflow:",
|
|
72
|
+
options: [
|
|
73
|
+
{
|
|
74
|
+
value: "sqlite",
|
|
75
|
+
label: "SQLite",
|
|
76
|
+
hint: "Recommended for testing and development",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
value: "postgres",
|
|
80
|
+
label: "PostgreSQL",
|
|
81
|
+
hint: "Recommended for production",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
value: "both",
|
|
85
|
+
label: "Both",
|
|
86
|
+
hint: "SQLite for dev, PostgreSQL for production",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
initialValue: "sqlite",
|
|
90
|
+
}));
|
|
91
|
+
if (typeof backendChoice === "symbol")
|
|
92
|
+
return cancelSetup();
|
|
93
|
+
trackCommand(backendChoice);
|
|
80
94
|
const spinner = p.spinner();
|
|
81
95
|
// detect package manager & install packages
|
|
82
96
|
spinner.start("Detecting package manager...");
|
|
83
97
|
const pm = await detectPackageManager(process.cwd());
|
|
84
|
-
const packageManager = pm?.name ?? "
|
|
98
|
+
const packageManager = pm?.name ?? "npm";
|
|
85
99
|
spinner.stop(`Using ${packageManager}`);
|
|
86
100
|
const packageJson = readPackageJsonForDoctor();
|
|
87
101
|
if (!packageJson) {
|
|
88
102
|
throw new CLIError("No package.json found.", "Please create a package.json file first by running `npm init` or `npm init -y`.");
|
|
89
103
|
}
|
|
90
|
-
|
|
104
|
+
validateInitManifest(packageJson);
|
|
105
|
+
const configFileName = options.config ?? getConfigFileName(packageJson);
|
|
91
106
|
const clientFileName = getClientFileName(packageJson);
|
|
92
107
|
const exampleWorkflowFileName = getExampleWorkflowFileName(packageJson);
|
|
93
108
|
const runFileName = getRunFileName(packageJson);
|
|
94
109
|
const runCommand = runFileName.endsWith(".ts")
|
|
95
110
|
? `npx tsx openworkflow/${runFileName}`
|
|
96
111
|
: `node openworkflow/${runFileName}`;
|
|
97
|
-
const shouldSetup =
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
const shouldSetup = options.yes ??
|
|
113
|
+
(await p.confirm({
|
|
114
|
+
message: options.skipInstall
|
|
115
|
+
? "Set up project files?"
|
|
116
|
+
: "Install packages and set up project files?",
|
|
117
|
+
initialValue: true,
|
|
118
|
+
}));
|
|
101
119
|
if (p.isCancel(shouldSetup))
|
|
102
|
-
cancelSetup();
|
|
120
|
+
return cancelSetup();
|
|
103
121
|
if (!shouldSetup) {
|
|
104
122
|
p.outro("Setup skipped.");
|
|
105
123
|
return;
|
|
106
124
|
}
|
|
107
|
-
|
|
108
|
-
|
|
125
|
+
const dependencies = getDependenciesToInstall(backendChoice);
|
|
126
|
+
const devDependencies = getDevDependenciesToInstall();
|
|
127
|
+
if (options.skipInstall) {
|
|
128
|
+
p.note([
|
|
129
|
+
addDependencyCommand(packageManager, dependencies),
|
|
130
|
+
addDependencyCommand(packageManager, devDependencies, { dev: true }),
|
|
131
|
+
].join("\n"), "Install dependencies before running OpenWorkflow");
|
|
109
132
|
}
|
|
110
|
-
{
|
|
111
|
-
const dependencies = getDependenciesToInstall(backendChoice);
|
|
133
|
+
else {
|
|
112
134
|
spinner.start(`Installing ${dependencies.join(", ")}...`);
|
|
113
|
-
await addDependency(dependencies, { silent: true });
|
|
135
|
+
await addDependency(dependencies, { silent: true, packageManager });
|
|
114
136
|
spinner.stop(`Installed ${dependencies.join(", ")}`);
|
|
115
|
-
}
|
|
116
|
-
{
|
|
117
|
-
const devDependencies = getDevDependenciesToInstall();
|
|
118
137
|
spinner.start(`Installing ${devDependencies.join(", ")}...`);
|
|
119
|
-
await addDependency(devDependencies, {
|
|
138
|
+
await addDependency(devDependencies, {
|
|
139
|
+
silent: true,
|
|
140
|
+
dev: true,
|
|
141
|
+
packageManager,
|
|
142
|
+
});
|
|
120
143
|
spinner.stop(`Installed ${devDependencies.join(", ")}`);
|
|
121
144
|
}
|
|
145
|
+
if (configFileToDelete) {
|
|
146
|
+
unlinkSync(configFileToDelete);
|
|
147
|
+
}
|
|
122
148
|
createClientFile(backendChoice, clientFileName);
|
|
123
149
|
createExampleWorkflow(exampleWorkflowFileName);
|
|
124
150
|
createRunFile(runFileName);
|
|
@@ -136,56 +162,89 @@ export async function init(options = {}) {
|
|
|
136
162
|
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
163
|
p.outro("✅ Setup complete!");
|
|
138
164
|
}
|
|
165
|
+
// Validate the manifest fields that init reads or updates.
|
|
166
|
+
function validateInitManifest(manifest) {
|
|
167
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
168
|
+
throw new CLIError("Invalid package.json: expected an object.");
|
|
169
|
+
}
|
|
170
|
+
for (const key of ["scripts", "dependencies", "devDependencies"]) {
|
|
171
|
+
const field = manifest[key];
|
|
172
|
+
if (field === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
if (field === null ||
|
|
175
|
+
typeof field !== "object" ||
|
|
176
|
+
Array.isArray(field) ||
|
|
177
|
+
Object.values(field).some((value) => typeof value !== "string")) {
|
|
178
|
+
throw new CLIError(`Invalid package.json: ${key} must be an object containing string values.`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const { scripts } = manifest;
|
|
182
|
+
const worker = scripts?.["worker"];
|
|
183
|
+
if (worker !== undefined && worker !== "npx @openworkflow/cli worker start") {
|
|
184
|
+
throw new CLIError("Setup would overwrite package.json scripts.worker.");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
139
187
|
/**
|
|
140
188
|
* openworkflow doctor
|
|
141
189
|
* @param options - Command options
|
|
142
190
|
*/
|
|
143
191
|
export async function doctor(options = {}) {
|
|
144
|
-
const configPath = options.config;
|
|
145
192
|
consola.start("Running OpenWorkflow doctor...");
|
|
146
|
-
const
|
|
193
|
+
const timer = setTimeout(() => {
|
|
194
|
+
consola.error("Doctor timed out after 30 seconds.");
|
|
195
|
+
void exit(1);
|
|
196
|
+
}, 30_000);
|
|
197
|
+
const { config, configFile } = await loadConfigWithEnv(options);
|
|
147
198
|
if (!configFile) {
|
|
148
199
|
throw new CLIError("No config file found.", "Run `npx @openworkflow/cli init` to create a config file.");
|
|
149
200
|
}
|
|
150
201
|
const backend = config.backend;
|
|
202
|
+
let cleanupFailed = false;
|
|
151
203
|
try {
|
|
204
|
+
await checkBackendConnection(backend);
|
|
205
|
+
if (config.worker?.concurrency !== undefined) {
|
|
206
|
+
assertPositiveInteger("concurrency", config.worker.concurrency);
|
|
207
|
+
}
|
|
152
208
|
consola.log("");
|
|
153
|
-
consola.info(`Config file: ${configFile}`);
|
|
209
|
+
consola.info(`Config file: ${path.relative(process.cwd(), configFile)}`);
|
|
154
210
|
const backendName = backend.constructor.name.replace("Backend", "");
|
|
155
211
|
consola.log(` • Backend: ${backendName}`);
|
|
156
|
-
const packageJson = readPackageJsonForDoctor();
|
|
157
|
-
if (packageJson) {
|
|
158
|
-
warnIfMissingBackendPackage(backendName, packageJson);
|
|
159
|
-
warnIfMissingTsconfig(packageJson);
|
|
160
|
-
}
|
|
161
212
|
// discover directories
|
|
162
|
-
const dirs = getWorkflowDirectories(config);
|
|
213
|
+
const dirs = [...new Set(getWorkflowDirectories(config))];
|
|
163
214
|
consola.log(` • Workflow directories: ${dirs.join(", ")}`);
|
|
164
215
|
// discover files
|
|
165
216
|
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
|
-
}
|
|
217
|
+
const { workflows } = await discoverWorkflowsInDirs(dirs, configFileDir, config.ignorePatterns ?? []);
|
|
218
|
+
assertNoDuplicateWorkflows(workflows);
|
|
172
219
|
printDiscoveredWorkflows(workflows);
|
|
173
|
-
warnAboutDuplicateWorkflows(workflows);
|
|
174
|
-
consola.log("");
|
|
175
|
-
consola.success("Configuration looks good!");
|
|
176
220
|
}
|
|
177
221
|
finally {
|
|
178
|
-
|
|
222
|
+
// Imported configs can omit the backend despite the declared config type.
|
|
223
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition
|
|
224
|
+
if (typeof backend?.stop === "function") {
|
|
225
|
+
try {
|
|
226
|
+
await backend.stop();
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
cleanupFailed = true;
|
|
230
|
+
consola.error(`Backend cleanup failed: ${String(error)}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
179
233
|
}
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
if (cleanupFailed)
|
|
236
|
+
await exit(1);
|
|
237
|
+
consola.log("");
|
|
238
|
+
consola.success("Configuration looks good!");
|
|
239
|
+
await exit(0);
|
|
180
240
|
}
|
|
181
241
|
/**
|
|
182
242
|
* openworkflow worker start
|
|
183
243
|
* @param options - Worker config and command options
|
|
184
244
|
*/
|
|
185
245
|
export async function workerStart(options = {}) {
|
|
186
|
-
const { config: configPath, ...workerConfig } = options;
|
|
187
246
|
consola.start("Starting worker...");
|
|
188
|
-
const { config, configFile } = await loadConfigWithEnv(
|
|
247
|
+
const { config, configFile } = await loadConfigWithEnv(options);
|
|
189
248
|
if (!configFile) {
|
|
190
249
|
throw new CLIError("No config file found.", "Run `npx @openworkflow/cli init` to create a config file.");
|
|
191
250
|
}
|
|
@@ -208,6 +267,7 @@ export async function workerStart(options = {}) {
|
|
|
208
267
|
consola.success("Worker stopped");
|
|
209
268
|
}
|
|
210
269
|
try {
|
|
270
|
+
await checkBackendConnection(backend);
|
|
211
271
|
// discover and import workflows
|
|
212
272
|
const dirs = getWorkflowDirectories(config);
|
|
213
273
|
consola.info(`Discovering workflows from: ${dirs.join(", ")}`);
|
|
@@ -216,7 +276,9 @@ export async function workerStart(options = {}) {
|
|
|
216
276
|
consola.info(`Found ${String(files.length)} workflow file(s)`);
|
|
217
277
|
consola.success(`Loaded ${String(workflows.length)} workflow(s): ${workflows.map((w) => w.spec.name).join(", ")}`);
|
|
218
278
|
assertNoDuplicateWorkflows(workflows);
|
|
219
|
-
const workerOptions = mergeDefinedOptions(config.worker,
|
|
279
|
+
const workerOptions = mergeDefinedOptions(config.worker, {
|
|
280
|
+
concurrency: options.concurrency,
|
|
281
|
+
});
|
|
220
282
|
if (workerOptions.concurrency !== undefined) {
|
|
221
283
|
assertPositiveInteger("concurrency", workerOptions.concurrency);
|
|
222
284
|
}
|
|
@@ -231,7 +293,12 @@ export async function workerStart(options = {}) {
|
|
|
231
293
|
consola.success("Worker started.");
|
|
232
294
|
}
|
|
233
295
|
catch (error) {
|
|
234
|
-
|
|
296
|
+
try {
|
|
297
|
+
await gracefulShutdown();
|
|
298
|
+
}
|
|
299
|
+
catch (cleanupError) {
|
|
300
|
+
consola.warn(`Backend cleanup failed: ${String(cleanupError)}`);
|
|
301
|
+
}
|
|
235
302
|
throw error;
|
|
236
303
|
}
|
|
237
304
|
}
|
|
@@ -274,10 +341,9 @@ export function validateDashboardPort(port) {
|
|
|
274
341
|
* @returns Resolves when the dashboard process exits.
|
|
275
342
|
*/
|
|
276
343
|
export async function dashboard(options = {}) {
|
|
277
|
-
const configPath = options.config;
|
|
278
344
|
const port = validateDashboardPort(options.port);
|
|
279
345
|
consola.start("Starting dashboard...");
|
|
280
|
-
const { configFile } = await loadConfigWithEnv(
|
|
346
|
+
const { configFile } = await loadConfigWithEnv(options);
|
|
281
347
|
if (!configFile) {
|
|
282
348
|
throw new CLIError("No config file found.", "Run `npx @openworkflow/cli init` to create a config file before starting the dashboard.");
|
|
283
349
|
}
|
|
@@ -317,11 +383,27 @@ export async function dashboard(options = {}) {
|
|
|
317
383
|
// -----------------------------------------------------------------------------
|
|
318
384
|
/**
|
|
319
385
|
* Show a canceled-setup message and exit the process with status 0.
|
|
386
|
+
* @returns Never resolves because the process exits.
|
|
320
387
|
*/
|
|
321
388
|
function cancelSetup() {
|
|
322
389
|
p.cancel("Setup canceled.");
|
|
323
|
-
|
|
324
|
-
|
|
390
|
+
return exit(0);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Exercise backend initialization, connectivity, and workflow table access.
|
|
394
|
+
* @param backend - Configured backend
|
|
395
|
+
*/
|
|
396
|
+
async function checkBackendConnection(backend) {
|
|
397
|
+
if (typeof backend?.listWorkflowRuns !== "function" ||
|
|
398
|
+
typeof backend.stop !== "function") {
|
|
399
|
+
throw new CLIError("Missing or invalid backend.", "Set config.backend to a connected OpenWorkflow backend.");
|
|
400
|
+
}
|
|
401
|
+
try {
|
|
402
|
+
await backend.listWorkflowRuns({ limit: 1 });
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
throw new CLIError("Failed to access backend.", error instanceof Error ? error.message : String(error));
|
|
406
|
+
}
|
|
325
407
|
}
|
|
326
408
|
/**
|
|
327
409
|
* Get workflow directories from config.
|
|
@@ -354,7 +436,7 @@ function findDuplicateWorkflows(workflows) {
|
|
|
354
436
|
for (const workflow of workflows) {
|
|
355
437
|
const name = workflow.spec.name;
|
|
356
438
|
const version = workflow.spec.version ?? null;
|
|
357
|
-
const key =
|
|
439
|
+
const key = JSON.stringify([name, version]);
|
|
358
440
|
const existing = workflowKeys.get(key);
|
|
359
441
|
if (existing) {
|
|
360
442
|
existing.count += 1;
|
|
@@ -382,29 +464,18 @@ function assertNoDuplicateWorkflows(workflows) {
|
|
|
382
464
|
const suffix = remaining > 0 ? ` (+${String(remaining)} more)` : "";
|
|
383
465
|
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
466
|
}
|
|
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
467
|
/**
|
|
397
468
|
* Print discovered workflows to the console.
|
|
398
469
|
* @param workflows - Array of discovered workflows
|
|
399
470
|
*/
|
|
400
471
|
function printDiscoveredWorkflows(workflows) {
|
|
401
472
|
consola.log("");
|
|
402
|
-
consola.info(`
|
|
473
|
+
consola.info(`Found ${String(workflows.length)} workflow${workflows.length === 1 ? "" : "s"}:`);
|
|
403
474
|
for (const workflow of workflows) {
|
|
404
475
|
const name = workflow.spec.name;
|
|
405
|
-
const version = workflow.spec.version
|
|
406
|
-
const versionStr = version
|
|
407
|
-
consola.log(` • ${name}${versionStr}`);
|
|
476
|
+
const version = workflow.spec.version;
|
|
477
|
+
const versionStr = version ? ` (${version})` : "";
|
|
478
|
+
consola.log(` • ${name}${versionStr} — ${workflowSources.get(workflow)}`);
|
|
408
479
|
}
|
|
409
480
|
}
|
|
410
481
|
const WORKFLOW_EXTENSIONS = ["ts", "mts", "cts", "js", "mjs", "cjs"];
|
|
@@ -476,8 +547,8 @@ function globToRegExp(pattern) {
|
|
|
476
547
|
return new RegExp(regex);
|
|
477
548
|
}
|
|
478
549
|
/**
|
|
479
|
-
* Check whether a file path matches ignore patterns.
|
|
480
|
-
* @param filePath - Absolute
|
|
550
|
+
* Check whether a file or directory path matches ignore patterns.
|
|
551
|
+
* @param filePath - Absolute path, with a trailing separator for directories
|
|
481
552
|
* @param baseDir - Base directory for relative matching
|
|
482
553
|
* @param matchers - Compiled regex matchers
|
|
483
554
|
* @returns Whether the file should be ignored
|
|
@@ -487,7 +558,11 @@ function isIgnoredFile(filePath, baseDir, matchers) {
|
|
|
487
558
|
return false;
|
|
488
559
|
const relativePath = normalizeForGlobMatch(path.relative(baseDir, filePath));
|
|
489
560
|
const fileName = path.basename(filePath);
|
|
490
|
-
|
|
561
|
+
const isDirectory = filePath.endsWith(path.sep);
|
|
562
|
+
return matchers.some((matcher) => matcher.test(relativePath) ||
|
|
563
|
+
matcher.test(fileName) ||
|
|
564
|
+
(isDirectory &&
|
|
565
|
+
(matcher.test(`${relativePath}/`) || matcher.test(`${fileName}/`))));
|
|
491
566
|
}
|
|
492
567
|
/**
|
|
493
568
|
* Discover workflow files from directories. Recursively scans directories for
|
|
@@ -512,19 +587,18 @@ export function discoverWorkflowFiles(dirs, baseDir, ignorePatterns = []) {
|
|
|
512
587
|
entries = readdirSync(absoluteDir, { withFileTypes: true });
|
|
513
588
|
}
|
|
514
589
|
catch (error) {
|
|
515
|
-
|
|
516
|
-
const errMessage = error instanceof Error ? error.message : String(error);
|
|
517
|
-
consola.debug(`Failed to read directory: ${absoluteDir} - ${errMessage}`);
|
|
518
|
-
return;
|
|
590
|
+
throw new CLIError(`Cannot read workflow directory: ${absoluteDir}`, `${String(error)}\nCorrect config.dirs or make the directory readable.`);
|
|
519
591
|
}
|
|
520
592
|
for (const entry of entries) {
|
|
521
593
|
const fullPath = path.join(absoluteDir, entry.name);
|
|
522
594
|
if (entry.isDirectory()) {
|
|
523
|
-
|
|
595
|
+
if (!isIgnoredFile(`${fullPath}${path.sep}`, baseDir, matchers)) {
|
|
596
|
+
scanDirectory(fullPath);
|
|
597
|
+
}
|
|
524
598
|
}
|
|
525
599
|
else if (entry.isFile() &&
|
|
526
600
|
WORKFLOW_EXTENSIONS.some((ext) => entry.name.endsWith(`.${ext}`)) &&
|
|
527
|
-
|
|
601
|
+
!/\.d\.(?:ts|mts|cts)$/.test(entry.name) &&
|
|
528
602
|
!isIgnoredFile(fullPath, baseDir, matchers)) {
|
|
529
603
|
discoveredFiles.push(fullPath);
|
|
530
604
|
}
|
|
@@ -533,7 +607,7 @@ export function discoverWorkflowFiles(dirs, baseDir, ignorePatterns = []) {
|
|
|
533
607
|
for (const dir of dirs) {
|
|
534
608
|
scanDirectory(dir);
|
|
535
609
|
}
|
|
536
|
-
return discoveredFiles;
|
|
610
|
+
return [...new Set(discoveredFiles)];
|
|
537
611
|
}
|
|
538
612
|
/**
|
|
539
613
|
* Import workflow files and extract workflow exports.
|
|
@@ -543,11 +617,11 @@ export function discoverWorkflowFiles(dirs, baseDir, ignorePatterns = []) {
|
|
|
543
617
|
*/
|
|
544
618
|
async function importWorkflows(files) {
|
|
545
619
|
const workflows = [];
|
|
546
|
-
const jiti = createJiti(import.meta.url);
|
|
547
620
|
for (const file of files) {
|
|
548
621
|
// import the module
|
|
549
622
|
let module;
|
|
550
623
|
try {
|
|
624
|
+
const jiti = createModuleLoader(file);
|
|
551
625
|
module = await jiti.import(pathToFileURL(file).href);
|
|
552
626
|
}
|
|
553
627
|
catch (error) {
|
|
@@ -558,11 +632,12 @@ async function importWorkflows(files) {
|
|
|
558
632
|
for (const [key, value] of Object.entries(module)) {
|
|
559
633
|
if (isWorkflow(value)) {
|
|
560
634
|
workflows.push(value);
|
|
635
|
+
workflowSources.set(value, path.relative(process.cwd(), file));
|
|
561
636
|
consola.debug(`Found workflow "${value.spec.name}" in ${file} (${key})`);
|
|
562
637
|
}
|
|
563
638
|
}
|
|
564
639
|
}
|
|
565
|
-
return workflows;
|
|
640
|
+
return [...new Set(workflows)];
|
|
566
641
|
}
|
|
567
642
|
/**
|
|
568
643
|
* Discover workflow files and import workflows with common error handling.
|
|
@@ -717,7 +792,7 @@ function addWorkerScriptToPackageJson() {
|
|
|
717
792
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
718
793
|
packageJson.scripts ??= {};
|
|
719
794
|
packageJson.scripts["worker"] = "npx @openworkflow/cli worker start";
|
|
720
|
-
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)
|
|
795
|
+
writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf8");
|
|
721
796
|
spinner.stop('Added "worker" script to package.json');
|
|
722
797
|
}
|
|
723
798
|
catch {
|
|
@@ -766,16 +841,32 @@ function updateEnvForPostgres() {
|
|
|
766
841
|
: "OPENWORKFLOW_POSTGRES_URL already in .env");
|
|
767
842
|
}
|
|
768
843
|
/**
|
|
769
|
-
*
|
|
770
|
-
* @param
|
|
771
|
-
* @returns
|
|
844
|
+
* Find the config and load its environment without importing it.
|
|
845
|
+
* @param options - Config and environment file paths
|
|
846
|
+
* @returns Config path, if found.
|
|
772
847
|
*/
|
|
773
|
-
|
|
774
|
-
|
|
848
|
+
function findConfigWithEnv(options) {
|
|
849
|
+
const { envFile } = options;
|
|
850
|
+
const configPath = options.config
|
|
851
|
+
? path.resolve(options.config)
|
|
852
|
+
: findConfigFile();
|
|
853
|
+
const baseDir = configPath ? path.dirname(configPath) : process.cwd();
|
|
854
|
+
const { error } = loadDotenv({
|
|
855
|
+
path: envFile ?? path.join(baseDir, ".env"),
|
|
856
|
+
quiet: true,
|
|
857
|
+
});
|
|
858
|
+
if (envFile !== undefined && error) {
|
|
859
|
+
throw new CLIError(`Failed to load environment file: ${envFile}`, error.message);
|
|
860
|
+
}
|
|
861
|
+
return configPath;
|
|
862
|
+
}
|
|
863
|
+
// Load the environment before importing config for commands that use it.
|
|
864
|
+
async function loadConfigWithEnv(options) {
|
|
865
|
+
const configPath = findConfigWithEnv(options);
|
|
775
866
|
try {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
867
|
+
const loaded = await loadConfigFromPath(configPath ?? "openworkflow.config.ts");
|
|
868
|
+
trackCommand(loaded.config.backend);
|
|
869
|
+
return loaded;
|
|
779
870
|
}
|
|
780
871
|
catch (error) {
|
|
781
872
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -851,35 +942,6 @@ export function getClientFileName(packageJson) {
|
|
|
851
942
|
function hasDependency(packageJson, name) {
|
|
852
943
|
return Boolean(packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name]);
|
|
853
944
|
}
|
|
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
945
|
/**
|
|
884
946
|
* Ensure a specific environment variable exists in a .env file. Creates the
|
|
885
947
|
* 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":"
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../errors.ts"],"names":[],"mappings":"AAIA;;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,CAYvD;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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/* v8 ignore file -- @preserve */
|
|
2
|
+
import { shutdownTelemetry } from "./telemetry.js";
|
|
2
3
|
import { consola } from "consola";
|
|
3
4
|
/**
|
|
4
5
|
* User-facing CLI error.
|
|
@@ -11,6 +12,18 @@ export class CLIError extends Error {
|
|
|
11
12
|
this.detail = detail;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Finish writing CLI output before exiting, including when handles remain open.
|
|
17
|
+
* @param code - Process exit code
|
|
18
|
+
*/
|
|
19
|
+
export async function exit(code) {
|
|
20
|
+
await shutdownTelemetry();
|
|
21
|
+
await Promise.all([process.stdout, process.stderr].map((stream) => new Promise((resolve) => {
|
|
22
|
+
stream.end(resolve);
|
|
23
|
+
})));
|
|
24
|
+
// oxlint-disable-next-line unicorn/no-process-exit
|
|
25
|
+
process.exit(code);
|
|
26
|
+
}
|
|
14
27
|
/**
|
|
15
28
|
* Wraps a CLI action / handler function with error handling that catches
|
|
16
29
|
* errors, prints them to the console, then exits.
|
|
@@ -24,19 +37,15 @@ export function withErrorHandling(fn) {
|
|
|
24
37
|
}
|
|
25
38
|
catch (error) {
|
|
26
39
|
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);
|
|
40
|
+
consola.error([error.message, error.detail].filter(Boolean).join("\n"));
|
|
41
|
+
return exit(1);
|
|
32
42
|
}
|
|
33
43
|
const message = error instanceof Error ? error.message : String(error);
|
|
34
44
|
consola.error(`Unexpected error: ${message}`);
|
|
35
45
|
if (error instanceof Error && error.stack) {
|
|
36
46
|
consola.debug(error.stack);
|
|
37
47
|
}
|
|
38
|
-
|
|
39
|
-
process.exit(1);
|
|
48
|
+
return exit(1);
|
|
40
49
|
}
|
|
41
50
|
};
|
|
42
51
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { Backend } from "openworkflow/internal";
|
|
3
|
+
/**
|
|
4
|
+
* Set up telemetry before the CLI prints output.
|
|
5
|
+
* @param program - Root CLI command
|
|
6
|
+
*/
|
|
7
|
+
export declare function initializeTelemetry(program: Command): void;
|
|
8
|
+
/**
|
|
9
|
+
* Track after loading config or choosing an init backend.
|
|
10
|
+
* Help, version, and argument errors are tracked without a backend.
|
|
11
|
+
* @param backend - Configured backend or init selection
|
|
12
|
+
*/
|
|
13
|
+
export declare function trackCommand(backend?: Backend | "sqlite" | "postgres" | "both"): void;
|
|
14
|
+
/** Send queued events before the CLI exits. */
|
|
15
|
+
export declare function shutdownTelemetry(): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../telemetry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAoBrD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAsC1D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,OAAO,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,UAAU,GAAG,MAAM,GACjD,IAAI,CAoCN;AAED,+CAA+C;AAC/C,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAMvD"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { PostHog } from "posthog-node";
|
|
6
|
+
let telemetry;
|
|
7
|
+
const backendTypes = new Map([
|
|
8
|
+
["BackendSqlite", "sqlite"],
|
|
9
|
+
["BackendPostgres", "postgres"],
|
|
10
|
+
["sqlite", "sqlite"],
|
|
11
|
+
["postgres", "postgres"],
|
|
12
|
+
["both", "both"],
|
|
13
|
+
]);
|
|
14
|
+
/**
|
|
15
|
+
* Set up telemetry before the CLI prints output.
|
|
16
|
+
* @param program - Root CLI command
|
|
17
|
+
*/
|
|
18
|
+
export function initializeTelemetry(program) {
|
|
19
|
+
telemetry = undefined;
|
|
20
|
+
if (process.env["DO_NOT_TRACK"] ||
|
|
21
|
+
process.env["OPENWORKFLOW_TELEMETRY_DISABLED"] ||
|
|
22
|
+
process.env["CI"] ||
|
|
23
|
+
process.argv.includes("--no-telemetry"))
|
|
24
|
+
return;
|
|
25
|
+
try {
|
|
26
|
+
const directory = path.join(homedir(), ".openworkflow");
|
|
27
|
+
const file = path.join(directory, "telemetry-id");
|
|
28
|
+
mkdirSync(directory, { recursive: true });
|
|
29
|
+
if (!existsSync(file)) {
|
|
30
|
+
writeFileSync(file, randomUUID(), { flag: "wx", mode: 0o600 });
|
|
31
|
+
console.error("OpenWorkflow collects CLI usage via PostHog. Set DO_NOT_TRACK=1 to opt out.\n" +
|
|
32
|
+
"https://openworkflow.dev/docs/cli#telemetry\n");
|
|
33
|
+
}
|
|
34
|
+
const distinctId = readFileSync(file, "utf8").trim();
|
|
35
|
+
if (!distinctId)
|
|
36
|
+
return;
|
|
37
|
+
const state = {
|
|
38
|
+
program,
|
|
39
|
+
distinctId,
|
|
40
|
+
client: new PostHog("phc_C1Cm1NAKHDFA3eKLVqcYzR5wUk8WhSbMMXltk5Qj1Ye"),
|
|
41
|
+
};
|
|
42
|
+
program.on("beforeAllHelp", ({ command }) => {
|
|
43
|
+
state.helpCommand = command;
|
|
44
|
+
});
|
|
45
|
+
program.on("option:version", () => {
|
|
46
|
+
state.version = true;
|
|
47
|
+
});
|
|
48
|
+
telemetry = state;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// ignore, telemetry is optional
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Track after loading config or choosing an init backend.
|
|
56
|
+
* Help, version, and argument errors are tracked without a backend.
|
|
57
|
+
* @param backend - Configured backend or init selection
|
|
58
|
+
*/
|
|
59
|
+
export function trackCommand(backend) {
|
|
60
|
+
const state = telemetry;
|
|
61
|
+
if (!state)
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
let command = state.helpCommand ?? state.program;
|
|
65
|
+
let child;
|
|
66
|
+
while ((child = command.commands.find((candidate) => candidate.name() === command.args[0]))) {
|
|
67
|
+
command = child;
|
|
68
|
+
}
|
|
69
|
+
const backendName = typeof backend === "string" ? backend : backend?.constructor.name;
|
|
70
|
+
const commandName = command.parent?.parent
|
|
71
|
+
? `${command.parent.name()} ${command.name()}`
|
|
72
|
+
: command.name();
|
|
73
|
+
state.client.capture({
|
|
74
|
+
distinctId: state.distinctId,
|
|
75
|
+
event: "cli_command_invoked",
|
|
76
|
+
properties: {
|
|
77
|
+
command: state.version ? "version" : commandName,
|
|
78
|
+
backend: backendName
|
|
79
|
+
? (backendTypes.get(backendName) ?? "custom")
|
|
80
|
+
: undefined,
|
|
81
|
+
help: state.helpCommand ? true : undefined,
|
|
82
|
+
cli_version: state.program.version(),
|
|
83
|
+
os: process.platform,
|
|
84
|
+
arch: process.arch,
|
|
85
|
+
node_version: process.versions.node,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// ignore, telemetry is optional
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Send queued events before the CLI exits. */
|
|
94
|
+
export async function shutdownTelemetry() {
|
|
95
|
+
try {
|
|
96
|
+
await telemetry?.client.shutdown();
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// ignore, telemetry is optional
|
|
100
|
+
}
|
|
101
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openworkflow/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -27,18 +27,19 @@
|
|
|
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
|
+
"posthog-node": "~5.21.2"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
39
|
"openworkflow": "*",
|
|
39
|
-
"vitest": "^
|
|
40
|
+
"vitest": "^5.0.0"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
|
-
"openworkflow": "^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0"
|
|
43
|
+
"openworkflow": "^0.6.0 || ^0.7.0 || ^0.8.0 || ^0.9.0 || ^0.10.0"
|
|
43
44
|
}
|
|
44
45
|
}
|