@synode/cli 1.0.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/LICENSE +35 -0
- package/dist/dist-DcZkVPq-.mjs +137 -0
- package/dist/dist-DcZkVPq-.mjs.map +1 -0
- package/dist/dist-kjD478yk.cjs +167 -0
- package/dist/dist-kjD478yk.cjs.map +1 -0
- package/dist/index.cjs +371 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +40 -0
- package/dist/index.d.mts +43 -0
- package/dist/index.mjs +370 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Synode Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Digitl Cloud GmbH. All rights reserved.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person or organization
|
|
6
|
+
obtaining a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to use the Software for personal, internal, and commercial
|
|
8
|
+
purposes, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
1. PERMITTED USE. You may use, copy, and modify the Software for your own
|
|
11
|
+
personal, internal, or commercial purposes.
|
|
12
|
+
|
|
13
|
+
2. NO REDISTRIBUTION. You may not distribute, publish, sublicense, or
|
|
14
|
+
otherwise make the Software or any derivative works available to third
|
|
15
|
+
parties, whether in source code or compiled form, free of charge or for
|
|
16
|
+
a fee.
|
|
17
|
+
|
|
18
|
+
3. NO RESALE. You may not sell, rent, lease, or otherwise commercially
|
|
19
|
+
exploit the Software itself as a standalone product or as part of a
|
|
20
|
+
software distribution.
|
|
21
|
+
|
|
22
|
+
4. NO HOSTING AS A SERVICE. You may not offer the Software to third parties
|
|
23
|
+
as a hosted, managed, or software-as-a-service product where the primary
|
|
24
|
+
value derives from the Software.
|
|
25
|
+
|
|
26
|
+
5. ATTRIBUTION. You must retain this license notice and copyright notice in
|
|
27
|
+
all copies or substantial portions of the Software.
|
|
28
|
+
|
|
29
|
+
6. NO WARRANTY. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
|
|
30
|
+
KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
31
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
32
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES
|
|
33
|
+
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
34
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
35
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import { validateFilePath } from "@synode/core";
|
|
4
|
+
|
|
5
|
+
//#region ../adapter-file/dist/index.mjs
|
|
6
|
+
const FORMAT_EXTENSION = {
|
|
7
|
+
jsonl: "jsonl",
|
|
8
|
+
json: "json",
|
|
9
|
+
csv: "csv"
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Adapter that writes events to the local filesystem.
|
|
13
|
+
*
|
|
14
|
+
* Supports JSONL (append per event), JSON (buffered array), and CSV formats
|
|
15
|
+
* with optional daily partitioning by event timestamp.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const adapter = new FileAdapter({ path: './out/events.jsonl', format: 'jsonl' });
|
|
20
|
+
* await generate(journey, { users: 10, adapter });
|
|
21
|
+
* await adapter.close();
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* // Daily-partitioned CSV output
|
|
27
|
+
* const adapter = new FileAdapter({
|
|
28
|
+
* path: './out',
|
|
29
|
+
* format: 'csv',
|
|
30
|
+
* partition: 'daily',
|
|
31
|
+
* filePattern: 'events-{date}.{ext}',
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
var FileAdapter = class {
|
|
36
|
+
options;
|
|
37
|
+
jsonBuffer = [];
|
|
38
|
+
csvHeadersWritten = /* @__PURE__ */ new Set();
|
|
39
|
+
csvHeaders = null;
|
|
40
|
+
constructor(options) {
|
|
41
|
+
this.options = {
|
|
42
|
+
partition: "none",
|
|
43
|
+
filePattern: "events-{date}.{ext}",
|
|
44
|
+
...options
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** @inheritdoc */
|
|
48
|
+
async write(event) {
|
|
49
|
+
const filePath = this.resolveFilePath(event);
|
|
50
|
+
await this.ensureDirectory(filePath);
|
|
51
|
+
switch (this.options.format) {
|
|
52
|
+
case "jsonl":
|
|
53
|
+
await this.writeJsonl(filePath, event);
|
|
54
|
+
break;
|
|
55
|
+
case "json":
|
|
56
|
+
this.jsonBuffer.push(event);
|
|
57
|
+
break;
|
|
58
|
+
case "csv":
|
|
59
|
+
await this.writeCsv(filePath, event);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** @inheritdoc */
|
|
64
|
+
async close() {
|
|
65
|
+
if (this.options.format !== "json") return;
|
|
66
|
+
if (this.options.partition === "daily") {
|
|
67
|
+
const grouped = this.groupByDate(this.jsonBuffer);
|
|
68
|
+
for (const [date, events] of grouped) {
|
|
69
|
+
const filePath = this.buildPartitionedPath(date);
|
|
70
|
+
await this.ensureDirectory(filePath);
|
|
71
|
+
await fs.writeFile(filePath, JSON.stringify(events, null, 2), "utf-8");
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const filePath = this.safePath(this.options.path);
|
|
75
|
+
await this.ensureDirectory(filePath);
|
|
76
|
+
await fs.writeFile(filePath, JSON.stringify(this.jsonBuffer, null, 2), "utf-8");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
resolveFilePath(event) {
|
|
80
|
+
if (this.options.partition === "daily") {
|
|
81
|
+
const date = this.formatDate(event.timestamp);
|
|
82
|
+
return this.buildPartitionedPath(date);
|
|
83
|
+
}
|
|
84
|
+
return this.safePath(this.options.path);
|
|
85
|
+
}
|
|
86
|
+
buildPartitionedPath(date) {
|
|
87
|
+
const ext = FORMAT_EXTENSION[this.options.format];
|
|
88
|
+
return validateFilePath(this.options.filePattern.replace("{date}", date).replace("{ext}", ext), path.resolve(this.options.path));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Resolves and validates a file path against its own parent directory.
|
|
92
|
+
* This prevents path traversal (e.g., `../../etc/passwd`) while allowing
|
|
93
|
+
* absolute paths outside the current working directory.
|
|
94
|
+
*/
|
|
95
|
+
safePath(filePath) {
|
|
96
|
+
const resolved = path.resolve(filePath);
|
|
97
|
+
return validateFilePath(resolved, path.dirname(resolved));
|
|
98
|
+
}
|
|
99
|
+
async ensureDirectory(filePath) {
|
|
100
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
async writeJsonl(filePath, event) {
|
|
103
|
+
await fs.appendFile(filePath, JSON.stringify(event) + "\n", "utf-8");
|
|
104
|
+
}
|
|
105
|
+
async writeCsv(filePath, event) {
|
|
106
|
+
this.csvHeaders ??= Object.keys(event.payload);
|
|
107
|
+
if (!this.csvHeadersWritten.has(filePath)) {
|
|
108
|
+
await fs.appendFile(filePath, this.csvHeaders.join(",") + "\n", "utf-8");
|
|
109
|
+
this.csvHeadersWritten.add(filePath);
|
|
110
|
+
}
|
|
111
|
+
const row = this.csvHeaders.map((header) => this.escapeCSVValue(event.payload[header]));
|
|
112
|
+
await fs.appendFile(filePath, row.join(",") + "\n", "utf-8");
|
|
113
|
+
}
|
|
114
|
+
escapeCSVValue(value) {
|
|
115
|
+
if (value == null) return "";
|
|
116
|
+
const str = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ? value.toString() : JSON.stringify(value);
|
|
117
|
+
if (str.includes(",") || str.includes("\"") || str.includes("\n")) return "\"" + str.replace(/"/g, "\"\"") + "\"";
|
|
118
|
+
return str;
|
|
119
|
+
}
|
|
120
|
+
formatDate(date) {
|
|
121
|
+
return `${String(date.getUTCFullYear())}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
|
122
|
+
}
|
|
123
|
+
groupByDate(events) {
|
|
124
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
125
|
+
for (const event of events) {
|
|
126
|
+
const date = this.formatDate(event.timestamp);
|
|
127
|
+
const existing = grouped.get(date);
|
|
128
|
+
if (existing) existing.push(event);
|
|
129
|
+
else grouped.set(date, [event]);
|
|
130
|
+
}
|
|
131
|
+
return grouped;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
export { FileAdapter };
|
|
137
|
+
//# sourceMappingURL=dist-DcZkVPq-.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dist-DcZkVPq-.mjs","names":[],"sources":["../../adapter-file/dist/index.mjs"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { validateFilePath } from \"@synode/core\";\n\n//#region src/index.ts\nconst FORMAT_EXTENSION = {\n\tjsonl: \"jsonl\",\n\tjson: \"json\",\n\tcsv: \"csv\"\n};\n/**\n* Adapter that writes events to the local filesystem.\n*\n* Supports JSONL (append per event), JSON (buffered array), and CSV formats\n* with optional daily partitioning by event timestamp.\n*\n* @example\n* ```ts\n* const adapter = new FileAdapter({ path: './out/events.jsonl', format: 'jsonl' });\n* await generate(journey, { users: 10, adapter });\n* await adapter.close();\n* ```\n*\n* @example\n* ```ts\n* // Daily-partitioned CSV output\n* const adapter = new FileAdapter({\n* path: './out',\n* format: 'csv',\n* partition: 'daily',\n* filePattern: 'events-{date}.{ext}',\n* });\n* ```\n*/\nvar FileAdapter = class {\n\toptions;\n\tjsonBuffer = [];\n\tcsvHeadersWritten = /* @__PURE__ */ new Set();\n\tcsvHeaders = null;\n\tconstructor(options) {\n\t\tthis.options = {\n\t\t\tpartition: \"none\",\n\t\t\tfilePattern: \"events-{date}.{ext}\",\n\t\t\t...options\n\t\t};\n\t}\n\t/** @inheritdoc */\n\tasync write(event) {\n\t\tconst filePath = this.resolveFilePath(event);\n\t\tawait this.ensureDirectory(filePath);\n\t\tswitch (this.options.format) {\n\t\t\tcase \"jsonl\":\n\t\t\t\tawait this.writeJsonl(filePath, event);\n\t\t\t\tbreak;\n\t\t\tcase \"json\":\n\t\t\t\tthis.jsonBuffer.push(event);\n\t\t\t\tbreak;\n\t\t\tcase \"csv\":\n\t\t\t\tawait this.writeCsv(filePath, event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\t/** @inheritdoc */\n\tasync close() {\n\t\tif (this.options.format !== \"json\") return;\n\t\tif (this.options.partition === \"daily\") {\n\t\t\tconst grouped = this.groupByDate(this.jsonBuffer);\n\t\t\tfor (const [date, events] of grouped) {\n\t\t\t\tconst filePath = this.buildPartitionedPath(date);\n\t\t\t\tawait this.ensureDirectory(filePath);\n\t\t\t\tawait fs.writeFile(filePath, JSON.stringify(events, null, 2), \"utf-8\");\n\t\t\t}\n\t\t} else {\n\t\t\tconst filePath = this.safePath(this.options.path);\n\t\t\tawait this.ensureDirectory(filePath);\n\t\t\tawait fs.writeFile(filePath, JSON.stringify(this.jsonBuffer, null, 2), \"utf-8\");\n\t\t}\n\t}\n\tresolveFilePath(event) {\n\t\tif (this.options.partition === \"daily\") {\n\t\t\tconst date = this.formatDate(event.timestamp);\n\t\t\treturn this.buildPartitionedPath(date);\n\t\t}\n\t\treturn this.safePath(this.options.path);\n\t}\n\tbuildPartitionedPath(date) {\n\t\tconst ext = FORMAT_EXTENSION[this.options.format];\n\t\treturn validateFilePath(this.options.filePattern.replace(\"{date}\", date).replace(\"{ext}\", ext), path.resolve(this.options.path));\n\t}\n\t/**\n\t* Resolves and validates a file path against its own parent directory.\n\t* This prevents path traversal (e.g., `../../etc/passwd`) while allowing\n\t* absolute paths outside the current working directory.\n\t*/\n\tsafePath(filePath) {\n\t\tconst resolved = path.resolve(filePath);\n\t\treturn validateFilePath(resolved, path.dirname(resolved));\n\t}\n\tasync ensureDirectory(filePath) {\n\t\tawait fs.mkdir(path.dirname(filePath), { recursive: true });\n\t}\n\tasync writeJsonl(filePath, event) {\n\t\tawait fs.appendFile(filePath, JSON.stringify(event) + \"\\n\", \"utf-8\");\n\t}\n\tasync writeCsv(filePath, event) {\n\t\tthis.csvHeaders ??= Object.keys(event.payload);\n\t\tif (!this.csvHeadersWritten.has(filePath)) {\n\t\t\tawait fs.appendFile(filePath, this.csvHeaders.join(\",\") + \"\\n\", \"utf-8\");\n\t\t\tthis.csvHeadersWritten.add(filePath);\n\t\t}\n\t\tconst row = this.csvHeaders.map((header) => this.escapeCSVValue(event.payload[header]));\n\t\tawait fs.appendFile(filePath, row.join(\",\") + \"\\n\", \"utf-8\");\n\t}\n\tescapeCSVValue(value) {\n\t\tif (value == null) return \"\";\n\t\tconst str = typeof value === \"string\" ? value : typeof value === \"number\" || typeof value === \"boolean\" || typeof value === \"bigint\" ? value.toString() : JSON.stringify(value);\n\t\tif (str.includes(\",\") || str.includes(\"\\\"\") || str.includes(\"\\n\")) return \"\\\"\" + str.replace(/\"/g, \"\\\"\\\"\") + \"\\\"\";\n\t\treturn str;\n\t}\n\tformatDate(date) {\n\t\treturn `${String(date.getUTCFullYear())}-${String(date.getUTCMonth() + 1).padStart(2, \"0\")}-${String(date.getUTCDate()).padStart(2, \"0\")}`;\n\t}\n\tgroupByDate(events) {\n\t\tconst grouped = /* @__PURE__ */ new Map();\n\t\tfor (const event of events) {\n\t\t\tconst date = this.formatDate(event.timestamp);\n\t\t\tconst existing = grouped.get(date);\n\t\t\tif (existing) existing.push(event);\n\t\t\telse grouped.set(date, [event]);\n\t\t}\n\t\treturn grouped;\n\t}\n};\n\n//#endregion\nexport { FileAdapter };\n//# sourceMappingURL=index.mjs.map"],"mappings":"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"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
//#region rolldown:runtime
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
let node_path = require("node:path");
|
|
29
|
+
node_path = __toESM(node_path);
|
|
30
|
+
let node_fs_promises = require("node:fs/promises");
|
|
31
|
+
node_fs_promises = __toESM(node_fs_promises);
|
|
32
|
+
let __synode_core = require("@synode/core");
|
|
33
|
+
|
|
34
|
+
//#region ../adapter-file/dist/index.mjs
|
|
35
|
+
const FORMAT_EXTENSION = {
|
|
36
|
+
jsonl: "jsonl",
|
|
37
|
+
json: "json",
|
|
38
|
+
csv: "csv"
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Adapter that writes events to the local filesystem.
|
|
42
|
+
*
|
|
43
|
+
* Supports JSONL (append per event), JSON (buffered array), and CSV formats
|
|
44
|
+
* with optional daily partitioning by event timestamp.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const adapter = new FileAdapter({ path: './out/events.jsonl', format: 'jsonl' });
|
|
49
|
+
* await generate(journey, { users: 10, adapter });
|
|
50
|
+
* await adapter.close();
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* // Daily-partitioned CSV output
|
|
56
|
+
* const adapter = new FileAdapter({
|
|
57
|
+
* path: './out',
|
|
58
|
+
* format: 'csv',
|
|
59
|
+
* partition: 'daily',
|
|
60
|
+
* filePattern: 'events-{date}.{ext}',
|
|
61
|
+
* });
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
var FileAdapter = class {
|
|
65
|
+
options;
|
|
66
|
+
jsonBuffer = [];
|
|
67
|
+
csvHeadersWritten = /* @__PURE__ */ new Set();
|
|
68
|
+
csvHeaders = null;
|
|
69
|
+
constructor(options) {
|
|
70
|
+
this.options = {
|
|
71
|
+
partition: "none",
|
|
72
|
+
filePattern: "events-{date}.{ext}",
|
|
73
|
+
...options
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** @inheritdoc */
|
|
77
|
+
async write(event) {
|
|
78
|
+
const filePath = this.resolveFilePath(event);
|
|
79
|
+
await this.ensureDirectory(filePath);
|
|
80
|
+
switch (this.options.format) {
|
|
81
|
+
case "jsonl":
|
|
82
|
+
await this.writeJsonl(filePath, event);
|
|
83
|
+
break;
|
|
84
|
+
case "json":
|
|
85
|
+
this.jsonBuffer.push(event);
|
|
86
|
+
break;
|
|
87
|
+
case "csv":
|
|
88
|
+
await this.writeCsv(filePath, event);
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** @inheritdoc */
|
|
93
|
+
async close() {
|
|
94
|
+
if (this.options.format !== "json") return;
|
|
95
|
+
if (this.options.partition === "daily") {
|
|
96
|
+
const grouped = this.groupByDate(this.jsonBuffer);
|
|
97
|
+
for (const [date, events] of grouped) {
|
|
98
|
+
const filePath = this.buildPartitionedPath(date);
|
|
99
|
+
await this.ensureDirectory(filePath);
|
|
100
|
+
await node_fs_promises.writeFile(filePath, JSON.stringify(events, null, 2), "utf-8");
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
const filePath = this.safePath(this.options.path);
|
|
104
|
+
await this.ensureDirectory(filePath);
|
|
105
|
+
await node_fs_promises.writeFile(filePath, JSON.stringify(this.jsonBuffer, null, 2), "utf-8");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
resolveFilePath(event) {
|
|
109
|
+
if (this.options.partition === "daily") {
|
|
110
|
+
const date = this.formatDate(event.timestamp);
|
|
111
|
+
return this.buildPartitionedPath(date);
|
|
112
|
+
}
|
|
113
|
+
return this.safePath(this.options.path);
|
|
114
|
+
}
|
|
115
|
+
buildPartitionedPath(date) {
|
|
116
|
+
const ext = FORMAT_EXTENSION[this.options.format];
|
|
117
|
+
return (0, __synode_core.validateFilePath)(this.options.filePattern.replace("{date}", date).replace("{ext}", ext), node_path.resolve(this.options.path));
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Resolves and validates a file path against its own parent directory.
|
|
121
|
+
* This prevents path traversal (e.g., `../../etc/passwd`) while allowing
|
|
122
|
+
* absolute paths outside the current working directory.
|
|
123
|
+
*/
|
|
124
|
+
safePath(filePath) {
|
|
125
|
+
const resolved = node_path.resolve(filePath);
|
|
126
|
+
return (0, __synode_core.validateFilePath)(resolved, node_path.dirname(resolved));
|
|
127
|
+
}
|
|
128
|
+
async ensureDirectory(filePath) {
|
|
129
|
+
await node_fs_promises.mkdir(node_path.dirname(filePath), { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
async writeJsonl(filePath, event) {
|
|
132
|
+
await node_fs_promises.appendFile(filePath, JSON.stringify(event) + "\n", "utf-8");
|
|
133
|
+
}
|
|
134
|
+
async writeCsv(filePath, event) {
|
|
135
|
+
this.csvHeaders ??= Object.keys(event.payload);
|
|
136
|
+
if (!this.csvHeadersWritten.has(filePath)) {
|
|
137
|
+
await node_fs_promises.appendFile(filePath, this.csvHeaders.join(",") + "\n", "utf-8");
|
|
138
|
+
this.csvHeadersWritten.add(filePath);
|
|
139
|
+
}
|
|
140
|
+
const row = this.csvHeaders.map((header) => this.escapeCSVValue(event.payload[header]));
|
|
141
|
+
await node_fs_promises.appendFile(filePath, row.join(",") + "\n", "utf-8");
|
|
142
|
+
}
|
|
143
|
+
escapeCSVValue(value) {
|
|
144
|
+
if (value == null) return "";
|
|
145
|
+
const str = typeof value === "string" ? value : typeof value === "number" || typeof value === "boolean" || typeof value === "bigint" ? value.toString() : JSON.stringify(value);
|
|
146
|
+
if (str.includes(",") || str.includes("\"") || str.includes("\n")) return "\"" + str.replace(/"/g, "\"\"") + "\"";
|
|
147
|
+
return str;
|
|
148
|
+
}
|
|
149
|
+
formatDate(date) {
|
|
150
|
+
return `${String(date.getUTCFullYear())}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
|
151
|
+
}
|
|
152
|
+
groupByDate(events) {
|
|
153
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
154
|
+
for (const event of events) {
|
|
155
|
+
const date = this.formatDate(event.timestamp);
|
|
156
|
+
const existing = grouped.get(date);
|
|
157
|
+
if (existing) existing.push(event);
|
|
158
|
+
else grouped.set(date, [event]);
|
|
159
|
+
}
|
|
160
|
+
return grouped;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
//#endregion
|
|
165
|
+
exports.FileAdapter = FileAdapter;
|
|
166
|
+
exports.__toESM = __toESM;
|
|
167
|
+
//# sourceMappingURL=dist-kjD478yk.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dist-kjD478yk.cjs","names":["fs","path"],"sources":["../../adapter-file/dist/index.mjs"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport { validateFilePath } from \"@synode/core\";\n\n//#region src/index.ts\nconst FORMAT_EXTENSION = {\n\tjsonl: \"jsonl\",\n\tjson: \"json\",\n\tcsv: \"csv\"\n};\n/**\n* Adapter that writes events to the local filesystem.\n*\n* Supports JSONL (append per event), JSON (buffered array), and CSV formats\n* with optional daily partitioning by event timestamp.\n*\n* @example\n* ```ts\n* const adapter = new FileAdapter({ path: './out/events.jsonl', format: 'jsonl' });\n* await generate(journey, { users: 10, adapter });\n* await adapter.close();\n* ```\n*\n* @example\n* ```ts\n* // Daily-partitioned CSV output\n* const adapter = new FileAdapter({\n* path: './out',\n* format: 'csv',\n* partition: 'daily',\n* filePattern: 'events-{date}.{ext}',\n* });\n* ```\n*/\nvar FileAdapter = class {\n\toptions;\n\tjsonBuffer = [];\n\tcsvHeadersWritten = /* @__PURE__ */ new Set();\n\tcsvHeaders = null;\n\tconstructor(options) {\n\t\tthis.options = {\n\t\t\tpartition: \"none\",\n\t\t\tfilePattern: \"events-{date}.{ext}\",\n\t\t\t...options\n\t\t};\n\t}\n\t/** @inheritdoc */\n\tasync write(event) {\n\t\tconst filePath = this.resolveFilePath(event);\n\t\tawait this.ensureDirectory(filePath);\n\t\tswitch (this.options.format) {\n\t\t\tcase \"jsonl\":\n\t\t\t\tawait this.writeJsonl(filePath, event);\n\t\t\t\tbreak;\n\t\t\tcase \"json\":\n\t\t\t\tthis.jsonBuffer.push(event);\n\t\t\t\tbreak;\n\t\t\tcase \"csv\":\n\t\t\t\tawait this.writeCsv(filePath, event);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\t/** @inheritdoc */\n\tasync close() {\n\t\tif (this.options.format !== \"json\") return;\n\t\tif (this.options.partition === \"daily\") {\n\t\t\tconst grouped = this.groupByDate(this.jsonBuffer);\n\t\t\tfor (const [date, events] of grouped) {\n\t\t\t\tconst filePath = this.buildPartitionedPath(date);\n\t\t\t\tawait this.ensureDirectory(filePath);\n\t\t\t\tawait fs.writeFile(filePath, JSON.stringify(events, null, 2), \"utf-8\");\n\t\t\t}\n\t\t} else {\n\t\t\tconst filePath = this.safePath(this.options.path);\n\t\t\tawait this.ensureDirectory(filePath);\n\t\t\tawait fs.writeFile(filePath, JSON.stringify(this.jsonBuffer, null, 2), \"utf-8\");\n\t\t}\n\t}\n\tresolveFilePath(event) {\n\t\tif (this.options.partition === \"daily\") {\n\t\t\tconst date = this.formatDate(event.timestamp);\n\t\t\treturn this.buildPartitionedPath(date);\n\t\t}\n\t\treturn this.safePath(this.options.path);\n\t}\n\tbuildPartitionedPath(date) {\n\t\tconst ext = FORMAT_EXTENSION[this.options.format];\n\t\treturn validateFilePath(this.options.filePattern.replace(\"{date}\", date).replace(\"{ext}\", ext), path.resolve(this.options.path));\n\t}\n\t/**\n\t* Resolves and validates a file path against its own parent directory.\n\t* This prevents path traversal (e.g., `../../etc/passwd`) while allowing\n\t* absolute paths outside the current working directory.\n\t*/\n\tsafePath(filePath) {\n\t\tconst resolved = path.resolve(filePath);\n\t\treturn validateFilePath(resolved, path.dirname(resolved));\n\t}\n\tasync ensureDirectory(filePath) {\n\t\tawait fs.mkdir(path.dirname(filePath), { recursive: true });\n\t}\n\tasync writeJsonl(filePath, event) {\n\t\tawait fs.appendFile(filePath, JSON.stringify(event) + \"\\n\", \"utf-8\");\n\t}\n\tasync writeCsv(filePath, event) {\n\t\tthis.csvHeaders ??= Object.keys(event.payload);\n\t\tif (!this.csvHeadersWritten.has(filePath)) {\n\t\t\tawait fs.appendFile(filePath, this.csvHeaders.join(\",\") + \"\\n\", \"utf-8\");\n\t\t\tthis.csvHeadersWritten.add(filePath);\n\t\t}\n\t\tconst row = this.csvHeaders.map((header) => this.escapeCSVValue(event.payload[header]));\n\t\tawait fs.appendFile(filePath, row.join(\",\") + \"\\n\", \"utf-8\");\n\t}\n\tescapeCSVValue(value) {\n\t\tif (value == null) return \"\";\n\t\tconst str = typeof value === \"string\" ? value : typeof value === \"number\" || typeof value === \"boolean\" || typeof value === \"bigint\" ? value.toString() : JSON.stringify(value);\n\t\tif (str.includes(\",\") || str.includes(\"\\\"\") || str.includes(\"\\n\")) return \"\\\"\" + str.replace(/\"/g, \"\\\"\\\"\") + \"\\\"\";\n\t\treturn str;\n\t}\n\tformatDate(date) {\n\t\treturn `${String(date.getUTCFullYear())}-${String(date.getUTCMonth() + 1).padStart(2, \"0\")}-${String(date.getUTCDate()).padStart(2, \"0\")}`;\n\t}\n\tgroupByDate(events) {\n\t\tconst grouped = /* @__PURE__ */ new Map();\n\t\tfor (const event of events) {\n\t\t\tconst date = this.formatDate(event.timestamp);\n\t\t\tconst existing = grouped.get(date);\n\t\t\tif (existing) existing.push(event);\n\t\t\telse grouped.set(date, [event]);\n\t\t}\n\t\treturn grouped;\n\t}\n};\n\n//#endregion\nexport { FileAdapter };\n//# sourceMappingURL=index.mjs.map"],"mappings":"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"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const require_dist = require('./dist-kjD478yk.cjs');
|
|
3
|
+
let node_util = require("node:util");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
let node_fs_promises = require("node:fs/promises");
|
|
6
|
+
let __synode_core = require("@synode/core");
|
|
7
|
+
|
|
8
|
+
//#region src/config.ts
|
|
9
|
+
/**
|
|
10
|
+
* Loads and validates a Synode config file from the given path.
|
|
11
|
+
*
|
|
12
|
+
* The file must export (default or named) an object with a `journeys` array.
|
|
13
|
+
*
|
|
14
|
+
* @param configPath - Path to the config file (resolved relative to cwd).
|
|
15
|
+
* @returns The loaded SynodeConfig.
|
|
16
|
+
* @throws If the file does not exist or does not export a valid config.
|
|
17
|
+
*/
|
|
18
|
+
async function loadConfig(configPath) {
|
|
19
|
+
const resolved = (0, node_path.resolve)(configPath);
|
|
20
|
+
try {
|
|
21
|
+
await (0, node_fs_promises.access)(resolved);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(`Config file not found: ${resolved}`);
|
|
24
|
+
}
|
|
25
|
+
const mod = await import(resolved);
|
|
26
|
+
const raw = mod.default ?? mod;
|
|
27
|
+
const journeys = raw.journeys;
|
|
28
|
+
if (!journeys || !Array.isArray(journeys)) throw new Error(`Config must export a 'journeys' array. Got: ${typeof journeys}`);
|
|
29
|
+
return raw;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Merges CLI flags into a SynodeConfig, with flags taking precedence.
|
|
33
|
+
*
|
|
34
|
+
* @param config - The base config from a config file.
|
|
35
|
+
* @param flags - CLI flags that override config values.
|
|
36
|
+
* @returns A new config with flags applied.
|
|
37
|
+
*/
|
|
38
|
+
function mergeFlags(config, flags) {
|
|
39
|
+
const options = { ...config.options };
|
|
40
|
+
if (flags.users !== void 0) options.users = flags.users;
|
|
41
|
+
if (flags.lanes !== void 0) options.lanes = flags.lanes;
|
|
42
|
+
if (flags.debug) options.debug = true;
|
|
43
|
+
if (flags.workers !== void 0) options.workers = flags.workers;
|
|
44
|
+
return {
|
|
45
|
+
...config,
|
|
46
|
+
options
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Infers the export format from a file path's extension.
|
|
51
|
+
*
|
|
52
|
+
* @param output - The output file path.
|
|
53
|
+
* @returns The inferred format: 'json', 'csv', or 'jsonl' (default).
|
|
54
|
+
*/
|
|
55
|
+
function inferFormat(output) {
|
|
56
|
+
const ext = output.split(".").pop()?.toLowerCase();
|
|
57
|
+
if (ext === "json") return "json";
|
|
58
|
+
if (ext === "csv") return "csv";
|
|
59
|
+
return "jsonl";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/commands/generate.ts
|
|
64
|
+
/**
|
|
65
|
+
* Resolves the output adapter based on CLI flags and config.
|
|
66
|
+
*
|
|
67
|
+
* Priority: CLI --output flag > config adapter > ConsoleAdapter (default).
|
|
68
|
+
*
|
|
69
|
+
* @param config - The loaded Synode config.
|
|
70
|
+
* @param flags - CLI flags that may override the adapter.
|
|
71
|
+
* @returns The resolved output adapter.
|
|
72
|
+
*/
|
|
73
|
+
async function resolveAdapter(config, flags) {
|
|
74
|
+
if (flags.output) {
|
|
75
|
+
const format = flags.format ?? inferFormat(flags.output);
|
|
76
|
+
try {
|
|
77
|
+
const { FileAdapter } = await Promise.resolve().then(() => require("./dist-kjD478yk.cjs"));
|
|
78
|
+
return new FileAdapter({
|
|
79
|
+
path: (0, node_path.resolve)(flags.output),
|
|
80
|
+
format
|
|
81
|
+
});
|
|
82
|
+
} catch {
|
|
83
|
+
throw new Error("File output requires @synode/adapter-file. Install it with: pnpm add @synode/adapter-file");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (config.options?.adapter) {
|
|
87
|
+
const ac = config.options.adapter;
|
|
88
|
+
if (ac.type === "file" && ac.path) try {
|
|
89
|
+
const { FileAdapter } = await Promise.resolve().then(() => require("./dist-kjD478yk.cjs"));
|
|
90
|
+
return new FileAdapter({
|
|
91
|
+
path: (0, node_path.resolve)(ac.path),
|
|
92
|
+
format: ac.format ?? "jsonl"
|
|
93
|
+
});
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error("File output requires @synode/adapter-file. Install it with: pnpm add @synode/adapter-file");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return new __synode_core.ConsoleAdapter();
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Runs the generate command: validates all journeys then executes generation.
|
|
102
|
+
*
|
|
103
|
+
* @param config - The loaded Synode config with journeys, persona, datasets, and options.
|
|
104
|
+
* @param flags - CLI flags that may override config values.
|
|
105
|
+
* @returns A promise that resolves when generation is complete.
|
|
106
|
+
* @throws If any journey fails validation or generation encounters an error.
|
|
107
|
+
*/
|
|
108
|
+
async function runGenerate(config, flags) {
|
|
109
|
+
for (const journey of config.journeys) (0, __synode_core.validateConfig)(journey, config.journeys);
|
|
110
|
+
const adapter = await resolveAdapter(config, flags);
|
|
111
|
+
const options = config.options ?? {};
|
|
112
|
+
await (0, __synode_core.generate)(config.journeys, {
|
|
113
|
+
users: options.users ?? 10,
|
|
114
|
+
persona: config.persona,
|
|
115
|
+
datasets: config.datasets,
|
|
116
|
+
preloadedDatasets: config.preloadedDatasets,
|
|
117
|
+
lanes: options.lanes,
|
|
118
|
+
adapter,
|
|
119
|
+
debug: options.debug,
|
|
120
|
+
telemetryPath: options.telemetryPath,
|
|
121
|
+
startDate: options.startDate,
|
|
122
|
+
endDate: options.endDate,
|
|
123
|
+
eventSchema: options.eventSchema,
|
|
124
|
+
workerModule: options.workerModule,
|
|
125
|
+
workers: options.workers
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/commands/validate.ts
|
|
131
|
+
/**
|
|
132
|
+
* Runs validation on all journeys in the config and reports results to stderr.
|
|
133
|
+
*
|
|
134
|
+
* @param config - The loaded Synode config containing journeys to validate.
|
|
135
|
+
* @returns True if all journeys pass validation, false otherwise.
|
|
136
|
+
*/
|
|
137
|
+
function runValidate(config) {
|
|
138
|
+
let allValid = true;
|
|
139
|
+
for (const journey of config.journeys) try {
|
|
140
|
+
(0, __synode_core.validateConfig)(journey, config.journeys);
|
|
141
|
+
process.stderr.write(` \u2713 Journey '${journey.id}' \u2014 PASS\n`);
|
|
142
|
+
} catch (err) {
|
|
143
|
+
allValid = false;
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
145
|
+
process.stderr.write(` \u2717 Journey '${journey.id}' \u2014 FAIL: ${message}\n`);
|
|
146
|
+
}
|
|
147
|
+
return allValid;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/commands/init.ts
|
|
152
|
+
const TEMPLATE = `import { defineJourney, defineAdventure, defineAction, definePersona, weighted } from 'synode';
|
|
153
|
+
import type { SynodeConfig } from 'synode';
|
|
154
|
+
|
|
155
|
+
const persona = definePersona({
|
|
156
|
+
id: 'default',
|
|
157
|
+
name: 'Default Persona',
|
|
158
|
+
attributes: {
|
|
159
|
+
locale: weighted({ en: 0.7, de: 0.3 }),
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const browseJourney = defineJourney({
|
|
164
|
+
id: 'browse',
|
|
165
|
+
name: 'Browse Session',
|
|
166
|
+
adventures: [
|
|
167
|
+
defineAdventure({
|
|
168
|
+
id: 'browse-pages',
|
|
169
|
+
name: 'Browse Pages',
|
|
170
|
+
timeSpan: { min: 1000, max: 5000 },
|
|
171
|
+
actions: [
|
|
172
|
+
defineAction({
|
|
173
|
+
id: 'page-view',
|
|
174
|
+
name: 'page_view',
|
|
175
|
+
fields: {
|
|
176
|
+
url: '/products',
|
|
177
|
+
title: 'Products',
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
],
|
|
181
|
+
}),
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const config: SynodeConfig = {
|
|
186
|
+
journeys: [browseJourney],
|
|
187
|
+
persona,
|
|
188
|
+
options: {
|
|
189
|
+
users: 100,
|
|
190
|
+
lanes: 2,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export default config;
|
|
195
|
+
`;
|
|
196
|
+
/**
|
|
197
|
+
* Scaffolds a new synode.config.ts file in the target directory.
|
|
198
|
+
*
|
|
199
|
+
* @param targetDir - Directory where the config file will be created. Defaults to cwd.
|
|
200
|
+
* @returns The absolute path to the created config file.
|
|
201
|
+
* @throws If a synode.config.ts file already exists at the target location.
|
|
202
|
+
*/
|
|
203
|
+
async function runInit(targetDir = process.cwd()) {
|
|
204
|
+
const filePath = (0, node_path.resolve)(targetDir, "synode.config.ts");
|
|
205
|
+
try {
|
|
206
|
+
await (0, node_fs_promises.access)(filePath);
|
|
207
|
+
throw new Error(`File already exists: ${filePath}`);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (err instanceof Error && err.message.startsWith("File already exists")) throw err;
|
|
210
|
+
}
|
|
211
|
+
await (0, node_fs_promises.writeFile)(filePath, TEMPLATE, "utf-8");
|
|
212
|
+
return filePath;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/index.ts
|
|
217
|
+
const USAGE = `Usage: synode <command> [options]
|
|
218
|
+
|
|
219
|
+
Commands:
|
|
220
|
+
generate <config> Generate synthetic data from config file
|
|
221
|
+
validate <config> Validate config without generating
|
|
222
|
+
init Scaffold a starter synode.config.ts
|
|
223
|
+
|
|
224
|
+
Options:
|
|
225
|
+
--users, -u <n> Override user count
|
|
226
|
+
--lanes, -l <n> Override lane count
|
|
227
|
+
--workers, -w <n> Enable worker threads with N workers
|
|
228
|
+
--output, -o <path> Output file path (default: stdout)
|
|
229
|
+
--format, -f <fmt> Output format: json, jsonl, csv
|
|
230
|
+
--debug Enable telemetry
|
|
231
|
+
--dry-run Validate only, generate 1 user
|
|
232
|
+
--quiet, -q Suppress progress output
|
|
233
|
+
--help, -h Show this help
|
|
234
|
+
`;
|
|
235
|
+
function printUsage() {
|
|
236
|
+
process.stderr.write(USAGE);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Parses CLI arguments into a command, config path, and flags.
|
|
240
|
+
*
|
|
241
|
+
* @param args - Raw CLI arguments (typically `process.argv.slice(2)`).
|
|
242
|
+
* @returns Parsed command, optional config path, and CLI flags.
|
|
243
|
+
*/
|
|
244
|
+
function parseCli(args) {
|
|
245
|
+
const { values, positionals } = (0, node_util.parseArgs)({
|
|
246
|
+
args,
|
|
247
|
+
options: {
|
|
248
|
+
users: {
|
|
249
|
+
type: "string",
|
|
250
|
+
short: "u"
|
|
251
|
+
},
|
|
252
|
+
lanes: {
|
|
253
|
+
type: "string",
|
|
254
|
+
short: "l"
|
|
255
|
+
},
|
|
256
|
+
workers: {
|
|
257
|
+
type: "string",
|
|
258
|
+
short: "w"
|
|
259
|
+
},
|
|
260
|
+
output: {
|
|
261
|
+
type: "string",
|
|
262
|
+
short: "o"
|
|
263
|
+
},
|
|
264
|
+
format: {
|
|
265
|
+
type: "string",
|
|
266
|
+
short: "f"
|
|
267
|
+
},
|
|
268
|
+
debug: {
|
|
269
|
+
type: "boolean",
|
|
270
|
+
default: false
|
|
271
|
+
},
|
|
272
|
+
"dry-run": {
|
|
273
|
+
type: "boolean",
|
|
274
|
+
default: false
|
|
275
|
+
},
|
|
276
|
+
quiet: {
|
|
277
|
+
type: "boolean",
|
|
278
|
+
short: "q",
|
|
279
|
+
default: false
|
|
280
|
+
},
|
|
281
|
+
help: {
|
|
282
|
+
type: "boolean",
|
|
283
|
+
short: "h",
|
|
284
|
+
default: false
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
allowPositionals: true,
|
|
288
|
+
strict: true
|
|
289
|
+
});
|
|
290
|
+
if (values.help) {
|
|
291
|
+
printUsage();
|
|
292
|
+
process.exit(0);
|
|
293
|
+
}
|
|
294
|
+
const command = positionals[0] ?? "";
|
|
295
|
+
const configPath = positionals[1];
|
|
296
|
+
const flags = {
|
|
297
|
+
users: values.users ? Number(values.users) : void 0,
|
|
298
|
+
lanes: values.lanes ? Number(values.lanes) : void 0,
|
|
299
|
+
workers: values.workers ? Number(values.workers) : void 0,
|
|
300
|
+
output: values.output,
|
|
301
|
+
format: values.format,
|
|
302
|
+
debug: values.debug,
|
|
303
|
+
dryRun: values["dry-run"],
|
|
304
|
+
quiet: values.quiet
|
|
305
|
+
};
|
|
306
|
+
if (flags.users !== void 0 && (!Number.isFinite(flags.users) || flags.users < 1)) {
|
|
307
|
+
process.stderr.write("Error: --users must be a positive number\n");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
if (flags.lanes !== void 0 && (!Number.isFinite(flags.lanes) || flags.lanes < 1)) {
|
|
311
|
+
process.stderr.write("Error: --lanes must be a positive number\n");
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
if (flags.format && ![
|
|
315
|
+
"json",
|
|
316
|
+
"jsonl",
|
|
317
|
+
"csv"
|
|
318
|
+
].includes(flags.format)) {
|
|
319
|
+
process.stderr.write(`Error: --format must be json, jsonl, or csv. Got: ${flags.format}\n`);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
command,
|
|
324
|
+
configPath,
|
|
325
|
+
flags
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
async function main() {
|
|
329
|
+
const { command, configPath, flags } = parseCli(process.argv.slice(2));
|
|
330
|
+
switch (command) {
|
|
331
|
+
case "generate": {
|
|
332
|
+
if (!configPath) {
|
|
333
|
+
process.stderr.write("Error: generate requires a config file path\n");
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
const merged = mergeFlags(await loadConfig(configPath), flags);
|
|
337
|
+
if (flags.dryRun) merged.options = {
|
|
338
|
+
...merged.options,
|
|
339
|
+
users: 1
|
|
340
|
+
};
|
|
341
|
+
await runGenerate(merged, flags);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case "validate": {
|
|
345
|
+
if (!configPath) {
|
|
346
|
+
process.stderr.write("Error: validate requires a config file path\n");
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
const valid = runValidate(await loadConfig(configPath));
|
|
350
|
+
process.exit(valid ? 0 : 1);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "init": {
|
|
354
|
+
const filePath = await runInit();
|
|
355
|
+
process.stderr.write(`Created ${filePath}\n`);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
default:
|
|
359
|
+
printUsage();
|
|
360
|
+
process.exit(command ? 1 : 0);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (process.argv[1] && (require("url").pathToFileURL(__filename).href === `file://${process.argv[1]}` || require("url").pathToFileURL(__filename).href === new URL(`file://${process.argv[1]}`).href)) main().catch((err) => {
|
|
364
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
365
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
//#endregion
|
|
370
|
+
exports.parseCli = parseCli;
|
|
371
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["journeys: unknown","ac: AdapterConfig","ConsoleAdapter","flags: CliFlags"],"sources":["../src/config.ts","../src/commands/generate.ts","../src/commands/validate.ts","../src/commands/init.ts","../src/index.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { access } from 'node:fs/promises';\nimport type { SynodeConfig } from './types.js';\n\n/**\n * CLI flags that can override config-file options.\n *\n * @param users - Number of synthetic users to generate.\n * @param lanes - Number of parallel processing lanes.\n * @param output - Output file path.\n * @param format - Output format (csv, json, jsonl).\n * @param debug - Enable debug mode.\n * @param dryRun - Validate config without running generation.\n * @param quiet - Suppress progress output.\n * @param workers - Number of worker threads.\n */\nexport interface CliFlags {\n users?: number;\n lanes?: number;\n output?: string;\n format?: string;\n debug?: boolean;\n dryRun?: boolean;\n quiet?: boolean;\n workers?: number;\n}\n\n/**\n * Loads and validates a Synode config file from the given path.\n *\n * The file must export (default or named) an object with a `journeys` array.\n *\n * @param configPath - Path to the config file (resolved relative to cwd).\n * @returns The loaded SynodeConfig.\n * @throws If the file does not exist or does not export a valid config.\n */\nexport async function loadConfig(configPath: string): Promise<SynodeConfig> {\n const resolved = resolve(configPath);\n\n try {\n await access(resolved);\n } catch {\n throw new Error(`Config file not found: ${resolved}`);\n }\n\n const mod = (await import(resolved)) as Record<string, unknown>;\n const raw = (mod.default ?? mod) as Record<string, unknown>;\n const journeys: unknown = raw.journeys;\n\n if (!journeys || !Array.isArray(journeys)) {\n throw new Error(`Config must export a 'journeys' array. Got: ${typeof journeys}`);\n }\n\n return raw as unknown as SynodeConfig;\n}\n\n/**\n * Merges CLI flags into a SynodeConfig, with flags taking precedence.\n *\n * @param config - The base config from a config file.\n * @param flags - CLI flags that override config values.\n * @returns A new config with flags applied.\n */\nexport function mergeFlags(config: SynodeConfig, flags: CliFlags): SynodeConfig {\n const options = { ...config.options };\n\n if (flags.users !== undefined) options.users = flags.users;\n if (flags.lanes !== undefined) options.lanes = flags.lanes;\n if (flags.debug) options.debug = true;\n if (flags.workers !== undefined) options.workers = flags.workers;\n\n return { ...config, options };\n}\n\n/**\n * Infers the export format from a file path's extension.\n *\n * @param output - The output file path.\n * @returns The inferred format: 'json', 'csv', or 'jsonl' (default).\n */\nexport function inferFormat(output: string): string {\n const ext = output.split('.').pop()?.toLowerCase();\n if (ext === 'json') return 'json';\n if (ext === 'csv') return 'csv';\n return 'jsonl';\n}\n","import { resolve } from 'node:path';\nimport { generate } from '@synode/core';\nimport { ConsoleAdapter } from '@synode/core';\nimport { validateConfig } from '@synode/core';\nimport type { OutputAdapter } from '@synode/core';\nimport type { SynodeConfig, AdapterConfig } from '../types.js';\nimport type { CliFlags } from '../config.js';\nimport { inferFormat } from '../config.js';\nimport type { ExportFormat } from '@synode/core';\n\n/**\n * Resolves the output adapter based on CLI flags and config.\n *\n * Priority: CLI --output flag > config adapter > ConsoleAdapter (default).\n *\n * @param config - The loaded Synode config.\n * @param flags - CLI flags that may override the adapter.\n * @returns The resolved output adapter.\n */\nasync function resolveAdapter(config: SynodeConfig, flags: CliFlags): Promise<OutputAdapter> {\n if (flags.output) {\n const format = (flags.format ?? inferFormat(flags.output)) as ExportFormat;\n try {\n const { FileAdapter } = await import('@synode/adapter-file');\n return new FileAdapter({ path: resolve(flags.output), format });\n } catch {\n throw new Error(\n 'File output requires @synode/adapter-file. Install it with: pnpm add @synode/adapter-file',\n );\n }\n }\n if (config.options?.adapter) {\n const ac: AdapterConfig = config.options.adapter;\n if (ac.type === 'file' && ac.path) {\n try {\n const { FileAdapter } = await import('@synode/adapter-file');\n return new FileAdapter({\n path: resolve(ac.path),\n format: ac.format ?? 'jsonl',\n });\n } catch {\n throw new Error(\n 'File output requires @synode/adapter-file. Install it with: pnpm add @synode/adapter-file',\n );\n }\n }\n }\n return new ConsoleAdapter();\n}\n\n/**\n * Runs the generate command: validates all journeys then executes generation.\n *\n * @param config - The loaded Synode config with journeys, persona, datasets, and options.\n * @param flags - CLI flags that may override config values.\n * @returns A promise that resolves when generation is complete.\n * @throws If any journey fails validation or generation encounters an error.\n */\nexport async function runGenerate(config: SynodeConfig, flags: CliFlags): Promise<void> {\n for (const journey of config.journeys) {\n validateConfig(journey, config.journeys);\n }\n\n const adapter = await resolveAdapter(config, flags);\n const options = config.options ?? {};\n\n await generate(config.journeys, {\n users: options.users ?? 10,\n persona: config.persona,\n datasets: config.datasets,\n preloadedDatasets: config.preloadedDatasets,\n lanes: options.lanes,\n adapter,\n debug: options.debug,\n telemetryPath: options.telemetryPath,\n startDate: options.startDate,\n endDate: options.endDate,\n eventSchema: options.eventSchema,\n workerModule: options.workerModule,\n workers: options.workers,\n });\n}\n","import { validateConfig } from '@synode/core';\nimport type { SynodeConfig } from '../types.js';\n\n/**\n * Runs validation on all journeys in the config and reports results to stderr.\n *\n * @param config - The loaded Synode config containing journeys to validate.\n * @returns True if all journeys pass validation, false otherwise.\n */\nexport function runValidate(config: SynodeConfig): boolean {\n let allValid = true;\n for (const journey of config.journeys) {\n try {\n validateConfig(journey, config.journeys);\n process.stderr.write(` \\u2713 Journey '${journey.id}' \\u2014 PASS\\n`);\n } catch (err) {\n allValid = false;\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(` \\u2717 Journey '${journey.id}' \\u2014 FAIL: ${message}\\n`);\n }\n }\n return allValid;\n}\n","import { writeFile, access } from 'node:fs/promises';\nimport { resolve } from 'node:path';\n\nconst TEMPLATE = `import { defineJourney, defineAdventure, defineAction, definePersona, weighted } from 'synode';\nimport type { SynodeConfig } from 'synode';\n\nconst persona = definePersona({\n id: 'default',\n name: 'Default Persona',\n attributes: {\n locale: weighted({ en: 0.7, de: 0.3 }),\n },\n});\n\nconst browseJourney = defineJourney({\n id: 'browse',\n name: 'Browse Session',\n adventures: [\n defineAdventure({\n id: 'browse-pages',\n name: 'Browse Pages',\n timeSpan: { min: 1000, max: 5000 },\n actions: [\n defineAction({\n id: 'page-view',\n name: 'page_view',\n fields: {\n url: '/products',\n title: 'Products',\n },\n }),\n ],\n }),\n ],\n});\n\nconst config: SynodeConfig = {\n journeys: [browseJourney],\n persona,\n options: {\n users: 100,\n lanes: 2,\n },\n};\n\nexport default config;\n`;\n\n/**\n * Scaffolds a new synode.config.ts file in the target directory.\n *\n * @param targetDir - Directory where the config file will be created. Defaults to cwd.\n * @returns The absolute path to the created config file.\n * @throws If a synode.config.ts file already exists at the target location.\n */\nexport async function runInit(targetDir: string = process.cwd()): Promise<string> {\n const filePath = resolve(targetDir, 'synode.config.ts');\n try {\n await access(filePath);\n throw new Error(`File already exists: ${filePath}`);\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('File already exists')) throw err;\n // File doesn't exist — proceed\n }\n await writeFile(filePath, TEMPLATE, 'utf-8');\n return filePath;\n}\n","#!/usr/bin/env node\n\nimport { parseArgs } from 'node:util';\nimport { loadConfig, mergeFlags } from './config.js';\nimport { runGenerate } from './commands/generate.js';\nimport { runValidate } from './commands/validate.js';\nimport { runInit } from './commands/init.js';\nimport type { CliFlags } from './config.js';\n\nconst USAGE = `Usage: synode <command> [options]\n\nCommands:\n generate <config> Generate synthetic data from config file\n validate <config> Validate config without generating\n init Scaffold a starter synode.config.ts\n\nOptions:\n --users, -u <n> Override user count\n --lanes, -l <n> Override lane count\n --workers, -w <n> Enable worker threads with N workers\n --output, -o <path> Output file path (default: stdout)\n --format, -f <fmt> Output format: json, jsonl, csv\n --debug Enable telemetry\n --dry-run Validate only, generate 1 user\n --quiet, -q Suppress progress output\n --help, -h Show this help\n`;\n\nfunction printUsage(): void {\n process.stderr.write(USAGE);\n}\n\n/**\n * Parses CLI arguments into a command, config path, and flags.\n *\n * @param args - Raw CLI arguments (typically `process.argv.slice(2)`).\n * @returns Parsed command, optional config path, and CLI flags.\n */\nexport function parseCli(args: string[]): {\n command: string;\n configPath?: string;\n flags: CliFlags;\n} {\n const { values, positionals } = parseArgs({\n args,\n options: {\n users: { type: 'string', short: 'u' },\n lanes: { type: 'string', short: 'l' },\n workers: { type: 'string', short: 'w' },\n output: { type: 'string', short: 'o' },\n format: { type: 'string', short: 'f' },\n debug: { type: 'boolean', default: false },\n 'dry-run': { type: 'boolean', default: false },\n quiet: { type: 'boolean', short: 'q', default: false },\n help: { type: 'boolean', short: 'h', default: false },\n },\n allowPositionals: true,\n strict: true,\n });\n\n if (values.help) {\n printUsage();\n process.exit(0);\n }\n\n const command = positionals[0] ?? '';\n const configPath = positionals[1];\n\n const flags: CliFlags = {\n users: values.users ? Number(values.users) : undefined,\n lanes: values.lanes ? Number(values.lanes) : undefined,\n workers: values.workers ? Number(values.workers) : undefined,\n output: values.output,\n format: values.format,\n debug: values.debug,\n dryRun: values['dry-run'],\n quiet: values.quiet,\n };\n\n // Validate numeric flags\n if (flags.users !== undefined && (!Number.isFinite(flags.users) || flags.users < 1)) {\n process.stderr.write('Error: --users must be a positive number\\n');\n process.exit(1);\n }\n if (flags.lanes !== undefined && (!Number.isFinite(flags.lanes) || flags.lanes < 1)) {\n process.stderr.write('Error: --lanes must be a positive number\\n');\n process.exit(1);\n }\n if (flags.format && !['json', 'jsonl', 'csv'].includes(flags.format)) {\n process.stderr.write(`Error: --format must be json, jsonl, or csv. Got: ${flags.format}\\n`);\n process.exit(1);\n }\n\n return { command, configPath, flags };\n}\n\nasync function main(): Promise<void> {\n const { command, configPath, flags } = parseCli(process.argv.slice(2));\n\n switch (command) {\n case 'generate': {\n if (!configPath) {\n process.stderr.write('Error: generate requires a config file path\\n');\n process.exit(1);\n }\n const config = await loadConfig(configPath);\n const merged = mergeFlags(config, flags);\n if (flags.dryRun) {\n merged.options = { ...merged.options, users: 1 };\n }\n await runGenerate(merged, flags);\n break;\n }\n case 'validate': {\n if (!configPath) {\n process.stderr.write('Error: validate requires a config file path\\n');\n process.exit(1);\n }\n const config = await loadConfig(configPath);\n const valid = runValidate(config);\n process.exit(valid ? 0 : 1);\n break;\n }\n case 'init': {\n const filePath = await runInit();\n process.stderr.write(`Created ${filePath}\\n`);\n break;\n }\n default:\n printUsage();\n process.exit(command ? 1 : 0);\n }\n}\n\n// Only run when executed directly, not when imported for testing\nconst isDirectRun =\n process.argv[1] &&\n (import.meta.url === `file://${process.argv[1]}` ||\n import.meta.url === new URL(`file://${process.argv[1]}`).href);\n\nif (isDirectRun) {\n main().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`Error: ${message}\\n`);\n process.exit(1);\n });\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//#region src/config.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* CLI flags that can override config-file options.
|
|
5
|
+
*
|
|
6
|
+
* @param users - Number of synthetic users to generate.
|
|
7
|
+
* @param lanes - Number of parallel processing lanes.
|
|
8
|
+
* @param output - Output file path.
|
|
9
|
+
* @param format - Output format (csv, json, jsonl).
|
|
10
|
+
* @param debug - Enable debug mode.
|
|
11
|
+
* @param dryRun - Validate config without running generation.
|
|
12
|
+
* @param quiet - Suppress progress output.
|
|
13
|
+
* @param workers - Number of worker threads.
|
|
14
|
+
*/
|
|
15
|
+
interface CliFlags {
|
|
16
|
+
users?: number;
|
|
17
|
+
lanes?: number;
|
|
18
|
+
output?: string;
|
|
19
|
+
format?: string;
|
|
20
|
+
debug?: boolean;
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
quiet?: boolean;
|
|
23
|
+
workers?: number;
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/index.d.ts
|
|
27
|
+
/**
|
|
28
|
+
* Parses CLI arguments into a command, config path, and flags.
|
|
29
|
+
*
|
|
30
|
+
* @param args - Raw CLI arguments (typically `process.argv.slice(2)`).
|
|
31
|
+
* @returns Parsed command, optional config path, and CLI flags.
|
|
32
|
+
*/
|
|
33
|
+
declare function parseCli(args: string[]): {
|
|
34
|
+
command: string;
|
|
35
|
+
configPath?: string;
|
|
36
|
+
flags: CliFlags;
|
|
37
|
+
};
|
|
38
|
+
//#endregion
|
|
39
|
+
export { parseCli };
|
|
40
|
+
//# sourceMappingURL=index.d.cts.map
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "@synode/core";
|
|
3
|
+
|
|
4
|
+
//#region src/config.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* CLI flags that can override config-file options.
|
|
8
|
+
*
|
|
9
|
+
* @param users - Number of synthetic users to generate.
|
|
10
|
+
* @param lanes - Number of parallel processing lanes.
|
|
11
|
+
* @param output - Output file path.
|
|
12
|
+
* @param format - Output format (csv, json, jsonl).
|
|
13
|
+
* @param debug - Enable debug mode.
|
|
14
|
+
* @param dryRun - Validate config without running generation.
|
|
15
|
+
* @param quiet - Suppress progress output.
|
|
16
|
+
* @param workers - Number of worker threads.
|
|
17
|
+
*/
|
|
18
|
+
interface CliFlags {
|
|
19
|
+
users?: number;
|
|
20
|
+
lanes?: number;
|
|
21
|
+
output?: string;
|
|
22
|
+
format?: string;
|
|
23
|
+
debug?: boolean;
|
|
24
|
+
dryRun?: boolean;
|
|
25
|
+
quiet?: boolean;
|
|
26
|
+
workers?: number;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/index.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* Parses CLI arguments into a command, config path, and flags.
|
|
32
|
+
*
|
|
33
|
+
* @param args - Raw CLI arguments (typically `process.argv.slice(2)`).
|
|
34
|
+
* @returns Parsed command, optional config path, and CLI flags.
|
|
35
|
+
*/
|
|
36
|
+
declare function parseCli(args: string[]): {
|
|
37
|
+
command: string;
|
|
38
|
+
configPath?: string;
|
|
39
|
+
flags: CliFlags;
|
|
40
|
+
};
|
|
41
|
+
//#endregion
|
|
42
|
+
export { parseCli };
|
|
43
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { access, writeFile } from "node:fs/promises";
|
|
5
|
+
import { ConsoleAdapter, generate, validateConfig } from "@synode/core";
|
|
6
|
+
|
|
7
|
+
//#region src/config.ts
|
|
8
|
+
/**
|
|
9
|
+
* Loads and validates a Synode config file from the given path.
|
|
10
|
+
*
|
|
11
|
+
* The file must export (default or named) an object with a `journeys` array.
|
|
12
|
+
*
|
|
13
|
+
* @param configPath - Path to the config file (resolved relative to cwd).
|
|
14
|
+
* @returns The loaded SynodeConfig.
|
|
15
|
+
* @throws If the file does not exist or does not export a valid config.
|
|
16
|
+
*/
|
|
17
|
+
async function loadConfig(configPath) {
|
|
18
|
+
const resolved = resolve(configPath);
|
|
19
|
+
try {
|
|
20
|
+
await access(resolved);
|
|
21
|
+
} catch {
|
|
22
|
+
throw new Error(`Config file not found: ${resolved}`);
|
|
23
|
+
}
|
|
24
|
+
const mod = await import(resolved);
|
|
25
|
+
const raw = mod.default ?? mod;
|
|
26
|
+
const journeys = raw.journeys;
|
|
27
|
+
if (!journeys || !Array.isArray(journeys)) throw new Error(`Config must export a 'journeys' array. Got: ${typeof journeys}`);
|
|
28
|
+
return raw;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Merges CLI flags into a SynodeConfig, with flags taking precedence.
|
|
32
|
+
*
|
|
33
|
+
* @param config - The base config from a config file.
|
|
34
|
+
* @param flags - CLI flags that override config values.
|
|
35
|
+
* @returns A new config with flags applied.
|
|
36
|
+
*/
|
|
37
|
+
function mergeFlags(config, flags) {
|
|
38
|
+
const options = { ...config.options };
|
|
39
|
+
if (flags.users !== void 0) options.users = flags.users;
|
|
40
|
+
if (flags.lanes !== void 0) options.lanes = flags.lanes;
|
|
41
|
+
if (flags.debug) options.debug = true;
|
|
42
|
+
if (flags.workers !== void 0) options.workers = flags.workers;
|
|
43
|
+
return {
|
|
44
|
+
...config,
|
|
45
|
+
options
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Infers the export format from a file path's extension.
|
|
50
|
+
*
|
|
51
|
+
* @param output - The output file path.
|
|
52
|
+
* @returns The inferred format: 'json', 'csv', or 'jsonl' (default).
|
|
53
|
+
*/
|
|
54
|
+
function inferFormat(output) {
|
|
55
|
+
const ext = output.split(".").pop()?.toLowerCase();
|
|
56
|
+
if (ext === "json") return "json";
|
|
57
|
+
if (ext === "csv") return "csv";
|
|
58
|
+
return "jsonl";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/commands/generate.ts
|
|
63
|
+
/**
|
|
64
|
+
* Resolves the output adapter based on CLI flags and config.
|
|
65
|
+
*
|
|
66
|
+
* Priority: CLI --output flag > config adapter > ConsoleAdapter (default).
|
|
67
|
+
*
|
|
68
|
+
* @param config - The loaded Synode config.
|
|
69
|
+
* @param flags - CLI flags that may override the adapter.
|
|
70
|
+
* @returns The resolved output adapter.
|
|
71
|
+
*/
|
|
72
|
+
async function resolveAdapter(config, flags) {
|
|
73
|
+
if (flags.output) {
|
|
74
|
+
const format = flags.format ?? inferFormat(flags.output);
|
|
75
|
+
try {
|
|
76
|
+
const { FileAdapter } = await import("./dist-DcZkVPq-.mjs");
|
|
77
|
+
return new FileAdapter({
|
|
78
|
+
path: resolve(flags.output),
|
|
79
|
+
format
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
throw new Error("File output requires @synode/adapter-file. Install it with: pnpm add @synode/adapter-file");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (config.options?.adapter) {
|
|
86
|
+
const ac = config.options.adapter;
|
|
87
|
+
if (ac.type === "file" && ac.path) try {
|
|
88
|
+
const { FileAdapter } = await import("./dist-DcZkVPq-.mjs");
|
|
89
|
+
return new FileAdapter({
|
|
90
|
+
path: resolve(ac.path),
|
|
91
|
+
format: ac.format ?? "jsonl"
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
throw new Error("File output requires @synode/adapter-file. Install it with: pnpm add @synode/adapter-file");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return new ConsoleAdapter();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Runs the generate command: validates all journeys then executes generation.
|
|
101
|
+
*
|
|
102
|
+
* @param config - The loaded Synode config with journeys, persona, datasets, and options.
|
|
103
|
+
* @param flags - CLI flags that may override config values.
|
|
104
|
+
* @returns A promise that resolves when generation is complete.
|
|
105
|
+
* @throws If any journey fails validation or generation encounters an error.
|
|
106
|
+
*/
|
|
107
|
+
async function runGenerate(config, flags) {
|
|
108
|
+
for (const journey of config.journeys) validateConfig(journey, config.journeys);
|
|
109
|
+
const adapter = await resolveAdapter(config, flags);
|
|
110
|
+
const options = config.options ?? {};
|
|
111
|
+
await generate(config.journeys, {
|
|
112
|
+
users: options.users ?? 10,
|
|
113
|
+
persona: config.persona,
|
|
114
|
+
datasets: config.datasets,
|
|
115
|
+
preloadedDatasets: config.preloadedDatasets,
|
|
116
|
+
lanes: options.lanes,
|
|
117
|
+
adapter,
|
|
118
|
+
debug: options.debug,
|
|
119
|
+
telemetryPath: options.telemetryPath,
|
|
120
|
+
startDate: options.startDate,
|
|
121
|
+
endDate: options.endDate,
|
|
122
|
+
eventSchema: options.eventSchema,
|
|
123
|
+
workerModule: options.workerModule,
|
|
124
|
+
workers: options.workers
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/commands/validate.ts
|
|
130
|
+
/**
|
|
131
|
+
* Runs validation on all journeys in the config and reports results to stderr.
|
|
132
|
+
*
|
|
133
|
+
* @param config - The loaded Synode config containing journeys to validate.
|
|
134
|
+
* @returns True if all journeys pass validation, false otherwise.
|
|
135
|
+
*/
|
|
136
|
+
function runValidate(config) {
|
|
137
|
+
let allValid = true;
|
|
138
|
+
for (const journey of config.journeys) try {
|
|
139
|
+
validateConfig(journey, config.journeys);
|
|
140
|
+
process.stderr.write(` \u2713 Journey '${journey.id}' \u2014 PASS\n`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
allValid = false;
|
|
143
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
144
|
+
process.stderr.write(` \u2717 Journey '${journey.id}' \u2014 FAIL: ${message}\n`);
|
|
145
|
+
}
|
|
146
|
+
return allValid;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/commands/init.ts
|
|
151
|
+
const TEMPLATE = `import { defineJourney, defineAdventure, defineAction, definePersona, weighted } from 'synode';
|
|
152
|
+
import type { SynodeConfig } from 'synode';
|
|
153
|
+
|
|
154
|
+
const persona = definePersona({
|
|
155
|
+
id: 'default',
|
|
156
|
+
name: 'Default Persona',
|
|
157
|
+
attributes: {
|
|
158
|
+
locale: weighted({ en: 0.7, de: 0.3 }),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const browseJourney = defineJourney({
|
|
163
|
+
id: 'browse',
|
|
164
|
+
name: 'Browse Session',
|
|
165
|
+
adventures: [
|
|
166
|
+
defineAdventure({
|
|
167
|
+
id: 'browse-pages',
|
|
168
|
+
name: 'Browse Pages',
|
|
169
|
+
timeSpan: { min: 1000, max: 5000 },
|
|
170
|
+
actions: [
|
|
171
|
+
defineAction({
|
|
172
|
+
id: 'page-view',
|
|
173
|
+
name: 'page_view',
|
|
174
|
+
fields: {
|
|
175
|
+
url: '/products',
|
|
176
|
+
title: 'Products',
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
],
|
|
180
|
+
}),
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const config: SynodeConfig = {
|
|
185
|
+
journeys: [browseJourney],
|
|
186
|
+
persona,
|
|
187
|
+
options: {
|
|
188
|
+
users: 100,
|
|
189
|
+
lanes: 2,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export default config;
|
|
194
|
+
`;
|
|
195
|
+
/**
|
|
196
|
+
* Scaffolds a new synode.config.ts file in the target directory.
|
|
197
|
+
*
|
|
198
|
+
* @param targetDir - Directory where the config file will be created. Defaults to cwd.
|
|
199
|
+
* @returns The absolute path to the created config file.
|
|
200
|
+
* @throws If a synode.config.ts file already exists at the target location.
|
|
201
|
+
*/
|
|
202
|
+
async function runInit(targetDir = process.cwd()) {
|
|
203
|
+
const filePath = resolve(targetDir, "synode.config.ts");
|
|
204
|
+
try {
|
|
205
|
+
await access(filePath);
|
|
206
|
+
throw new Error(`File already exists: ${filePath}`);
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (err instanceof Error && err.message.startsWith("File already exists")) throw err;
|
|
209
|
+
}
|
|
210
|
+
await writeFile(filePath, TEMPLATE, "utf-8");
|
|
211
|
+
return filePath;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
//#endregion
|
|
215
|
+
//#region src/index.ts
|
|
216
|
+
const USAGE = `Usage: synode <command> [options]
|
|
217
|
+
|
|
218
|
+
Commands:
|
|
219
|
+
generate <config> Generate synthetic data from config file
|
|
220
|
+
validate <config> Validate config without generating
|
|
221
|
+
init Scaffold a starter synode.config.ts
|
|
222
|
+
|
|
223
|
+
Options:
|
|
224
|
+
--users, -u <n> Override user count
|
|
225
|
+
--lanes, -l <n> Override lane count
|
|
226
|
+
--workers, -w <n> Enable worker threads with N workers
|
|
227
|
+
--output, -o <path> Output file path (default: stdout)
|
|
228
|
+
--format, -f <fmt> Output format: json, jsonl, csv
|
|
229
|
+
--debug Enable telemetry
|
|
230
|
+
--dry-run Validate only, generate 1 user
|
|
231
|
+
--quiet, -q Suppress progress output
|
|
232
|
+
--help, -h Show this help
|
|
233
|
+
`;
|
|
234
|
+
function printUsage() {
|
|
235
|
+
process.stderr.write(USAGE);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Parses CLI arguments into a command, config path, and flags.
|
|
239
|
+
*
|
|
240
|
+
* @param args - Raw CLI arguments (typically `process.argv.slice(2)`).
|
|
241
|
+
* @returns Parsed command, optional config path, and CLI flags.
|
|
242
|
+
*/
|
|
243
|
+
function parseCli(args) {
|
|
244
|
+
const { values, positionals } = parseArgs({
|
|
245
|
+
args,
|
|
246
|
+
options: {
|
|
247
|
+
users: {
|
|
248
|
+
type: "string",
|
|
249
|
+
short: "u"
|
|
250
|
+
},
|
|
251
|
+
lanes: {
|
|
252
|
+
type: "string",
|
|
253
|
+
short: "l"
|
|
254
|
+
},
|
|
255
|
+
workers: {
|
|
256
|
+
type: "string",
|
|
257
|
+
short: "w"
|
|
258
|
+
},
|
|
259
|
+
output: {
|
|
260
|
+
type: "string",
|
|
261
|
+
short: "o"
|
|
262
|
+
},
|
|
263
|
+
format: {
|
|
264
|
+
type: "string",
|
|
265
|
+
short: "f"
|
|
266
|
+
},
|
|
267
|
+
debug: {
|
|
268
|
+
type: "boolean",
|
|
269
|
+
default: false
|
|
270
|
+
},
|
|
271
|
+
"dry-run": {
|
|
272
|
+
type: "boolean",
|
|
273
|
+
default: false
|
|
274
|
+
},
|
|
275
|
+
quiet: {
|
|
276
|
+
type: "boolean",
|
|
277
|
+
short: "q",
|
|
278
|
+
default: false
|
|
279
|
+
},
|
|
280
|
+
help: {
|
|
281
|
+
type: "boolean",
|
|
282
|
+
short: "h",
|
|
283
|
+
default: false
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
allowPositionals: true,
|
|
287
|
+
strict: true
|
|
288
|
+
});
|
|
289
|
+
if (values.help) {
|
|
290
|
+
printUsage();
|
|
291
|
+
process.exit(0);
|
|
292
|
+
}
|
|
293
|
+
const command = positionals[0] ?? "";
|
|
294
|
+
const configPath = positionals[1];
|
|
295
|
+
const flags = {
|
|
296
|
+
users: values.users ? Number(values.users) : void 0,
|
|
297
|
+
lanes: values.lanes ? Number(values.lanes) : void 0,
|
|
298
|
+
workers: values.workers ? Number(values.workers) : void 0,
|
|
299
|
+
output: values.output,
|
|
300
|
+
format: values.format,
|
|
301
|
+
debug: values.debug,
|
|
302
|
+
dryRun: values["dry-run"],
|
|
303
|
+
quiet: values.quiet
|
|
304
|
+
};
|
|
305
|
+
if (flags.users !== void 0 && (!Number.isFinite(flags.users) || flags.users < 1)) {
|
|
306
|
+
process.stderr.write("Error: --users must be a positive number\n");
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
if (flags.lanes !== void 0 && (!Number.isFinite(flags.lanes) || flags.lanes < 1)) {
|
|
310
|
+
process.stderr.write("Error: --lanes must be a positive number\n");
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
313
|
+
if (flags.format && ![
|
|
314
|
+
"json",
|
|
315
|
+
"jsonl",
|
|
316
|
+
"csv"
|
|
317
|
+
].includes(flags.format)) {
|
|
318
|
+
process.stderr.write(`Error: --format must be json, jsonl, or csv. Got: ${flags.format}\n`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
command,
|
|
323
|
+
configPath,
|
|
324
|
+
flags
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
async function main() {
|
|
328
|
+
const { command, configPath, flags } = parseCli(process.argv.slice(2));
|
|
329
|
+
switch (command) {
|
|
330
|
+
case "generate": {
|
|
331
|
+
if (!configPath) {
|
|
332
|
+
process.stderr.write("Error: generate requires a config file path\n");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
const merged = mergeFlags(await loadConfig(configPath), flags);
|
|
336
|
+
if (flags.dryRun) merged.options = {
|
|
337
|
+
...merged.options,
|
|
338
|
+
users: 1
|
|
339
|
+
};
|
|
340
|
+
await runGenerate(merged, flags);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "validate": {
|
|
344
|
+
if (!configPath) {
|
|
345
|
+
process.stderr.write("Error: validate requires a config file path\n");
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
const valid = runValidate(await loadConfig(configPath));
|
|
349
|
+
process.exit(valid ? 0 : 1);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case "init": {
|
|
353
|
+
const filePath = await runInit();
|
|
354
|
+
process.stderr.write(`Created ${filePath}\n`);
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
default:
|
|
358
|
+
printUsage();
|
|
359
|
+
process.exit(command ? 1 : 0);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url === new URL(`file://${process.argv[1]}`).href)) main().catch((err) => {
|
|
363
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
364
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
//#endregion
|
|
369
|
+
export { parseCli };
|
|
370
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["journeys: unknown","ac: AdapterConfig","flags: CliFlags"],"sources":["../src/config.ts","../src/commands/generate.ts","../src/commands/validate.ts","../src/commands/init.ts","../src/index.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport { access } from 'node:fs/promises';\nimport type { SynodeConfig } from './types.js';\n\n/**\n * CLI flags that can override config-file options.\n *\n * @param users - Number of synthetic users to generate.\n * @param lanes - Number of parallel processing lanes.\n * @param output - Output file path.\n * @param format - Output format (csv, json, jsonl).\n * @param debug - Enable debug mode.\n * @param dryRun - Validate config without running generation.\n * @param quiet - Suppress progress output.\n * @param workers - Number of worker threads.\n */\nexport interface CliFlags {\n users?: number;\n lanes?: number;\n output?: string;\n format?: string;\n debug?: boolean;\n dryRun?: boolean;\n quiet?: boolean;\n workers?: number;\n}\n\n/**\n * Loads and validates a Synode config file from the given path.\n *\n * The file must export (default or named) an object with a `journeys` array.\n *\n * @param configPath - Path to the config file (resolved relative to cwd).\n * @returns The loaded SynodeConfig.\n * @throws If the file does not exist or does not export a valid config.\n */\nexport async function loadConfig(configPath: string): Promise<SynodeConfig> {\n const resolved = resolve(configPath);\n\n try {\n await access(resolved);\n } catch {\n throw new Error(`Config file not found: ${resolved}`);\n }\n\n const mod = (await import(resolved)) as Record<string, unknown>;\n const raw = (mod.default ?? mod) as Record<string, unknown>;\n const journeys: unknown = raw.journeys;\n\n if (!journeys || !Array.isArray(journeys)) {\n throw new Error(`Config must export a 'journeys' array. Got: ${typeof journeys}`);\n }\n\n return raw as unknown as SynodeConfig;\n}\n\n/**\n * Merges CLI flags into a SynodeConfig, with flags taking precedence.\n *\n * @param config - The base config from a config file.\n * @param flags - CLI flags that override config values.\n * @returns A new config with flags applied.\n */\nexport function mergeFlags(config: SynodeConfig, flags: CliFlags): SynodeConfig {\n const options = { ...config.options };\n\n if (flags.users !== undefined) options.users = flags.users;\n if (flags.lanes !== undefined) options.lanes = flags.lanes;\n if (flags.debug) options.debug = true;\n if (flags.workers !== undefined) options.workers = flags.workers;\n\n return { ...config, options };\n}\n\n/**\n * Infers the export format from a file path's extension.\n *\n * @param output - The output file path.\n * @returns The inferred format: 'json', 'csv', or 'jsonl' (default).\n */\nexport function inferFormat(output: string): string {\n const ext = output.split('.').pop()?.toLowerCase();\n if (ext === 'json') return 'json';\n if (ext === 'csv') return 'csv';\n return 'jsonl';\n}\n","import { resolve } from 'node:path';\nimport { generate } from '@synode/core';\nimport { ConsoleAdapter } from '@synode/core';\nimport { validateConfig } from '@synode/core';\nimport type { OutputAdapter } from '@synode/core';\nimport type { SynodeConfig, AdapterConfig } from '../types.js';\nimport type { CliFlags } from '../config.js';\nimport { inferFormat } from '../config.js';\nimport type { ExportFormat } from '@synode/core';\n\n/**\n * Resolves the output adapter based on CLI flags and config.\n *\n * Priority: CLI --output flag > config adapter > ConsoleAdapter (default).\n *\n * @param config - The loaded Synode config.\n * @param flags - CLI flags that may override the adapter.\n * @returns The resolved output adapter.\n */\nasync function resolveAdapter(config: SynodeConfig, flags: CliFlags): Promise<OutputAdapter> {\n if (flags.output) {\n const format = (flags.format ?? inferFormat(flags.output)) as ExportFormat;\n try {\n const { FileAdapter } = await import('@synode/adapter-file');\n return new FileAdapter({ path: resolve(flags.output), format });\n } catch {\n throw new Error(\n 'File output requires @synode/adapter-file. Install it with: pnpm add @synode/adapter-file',\n );\n }\n }\n if (config.options?.adapter) {\n const ac: AdapterConfig = config.options.adapter;\n if (ac.type === 'file' && ac.path) {\n try {\n const { FileAdapter } = await import('@synode/adapter-file');\n return new FileAdapter({\n path: resolve(ac.path),\n format: ac.format ?? 'jsonl',\n });\n } catch {\n throw new Error(\n 'File output requires @synode/adapter-file. Install it with: pnpm add @synode/adapter-file',\n );\n }\n }\n }\n return new ConsoleAdapter();\n}\n\n/**\n * Runs the generate command: validates all journeys then executes generation.\n *\n * @param config - The loaded Synode config with journeys, persona, datasets, and options.\n * @param flags - CLI flags that may override config values.\n * @returns A promise that resolves when generation is complete.\n * @throws If any journey fails validation or generation encounters an error.\n */\nexport async function runGenerate(config: SynodeConfig, flags: CliFlags): Promise<void> {\n for (const journey of config.journeys) {\n validateConfig(journey, config.journeys);\n }\n\n const adapter = await resolveAdapter(config, flags);\n const options = config.options ?? {};\n\n await generate(config.journeys, {\n users: options.users ?? 10,\n persona: config.persona,\n datasets: config.datasets,\n preloadedDatasets: config.preloadedDatasets,\n lanes: options.lanes,\n adapter,\n debug: options.debug,\n telemetryPath: options.telemetryPath,\n startDate: options.startDate,\n endDate: options.endDate,\n eventSchema: options.eventSchema,\n workerModule: options.workerModule,\n workers: options.workers,\n });\n}\n","import { validateConfig } from '@synode/core';\nimport type { SynodeConfig } from '../types.js';\n\n/**\n * Runs validation on all journeys in the config and reports results to stderr.\n *\n * @param config - The loaded Synode config containing journeys to validate.\n * @returns True if all journeys pass validation, false otherwise.\n */\nexport function runValidate(config: SynodeConfig): boolean {\n let allValid = true;\n for (const journey of config.journeys) {\n try {\n validateConfig(journey, config.journeys);\n process.stderr.write(` \\u2713 Journey '${journey.id}' \\u2014 PASS\\n`);\n } catch (err) {\n allValid = false;\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(` \\u2717 Journey '${journey.id}' \\u2014 FAIL: ${message}\\n`);\n }\n }\n return allValid;\n}\n","import { writeFile, access } from 'node:fs/promises';\nimport { resolve } from 'node:path';\n\nconst TEMPLATE = `import { defineJourney, defineAdventure, defineAction, definePersona, weighted } from 'synode';\nimport type { SynodeConfig } from 'synode';\n\nconst persona = definePersona({\n id: 'default',\n name: 'Default Persona',\n attributes: {\n locale: weighted({ en: 0.7, de: 0.3 }),\n },\n});\n\nconst browseJourney = defineJourney({\n id: 'browse',\n name: 'Browse Session',\n adventures: [\n defineAdventure({\n id: 'browse-pages',\n name: 'Browse Pages',\n timeSpan: { min: 1000, max: 5000 },\n actions: [\n defineAction({\n id: 'page-view',\n name: 'page_view',\n fields: {\n url: '/products',\n title: 'Products',\n },\n }),\n ],\n }),\n ],\n});\n\nconst config: SynodeConfig = {\n journeys: [browseJourney],\n persona,\n options: {\n users: 100,\n lanes: 2,\n },\n};\n\nexport default config;\n`;\n\n/**\n * Scaffolds a new synode.config.ts file in the target directory.\n *\n * @param targetDir - Directory where the config file will be created. Defaults to cwd.\n * @returns The absolute path to the created config file.\n * @throws If a synode.config.ts file already exists at the target location.\n */\nexport async function runInit(targetDir: string = process.cwd()): Promise<string> {\n const filePath = resolve(targetDir, 'synode.config.ts');\n try {\n await access(filePath);\n throw new Error(`File already exists: ${filePath}`);\n } catch (err) {\n if (err instanceof Error && err.message.startsWith('File already exists')) throw err;\n // File doesn't exist — proceed\n }\n await writeFile(filePath, TEMPLATE, 'utf-8');\n return filePath;\n}\n","#!/usr/bin/env node\n\nimport { parseArgs } from 'node:util';\nimport { loadConfig, mergeFlags } from './config.js';\nimport { runGenerate } from './commands/generate.js';\nimport { runValidate } from './commands/validate.js';\nimport { runInit } from './commands/init.js';\nimport type { CliFlags } from './config.js';\n\nconst USAGE = `Usage: synode <command> [options]\n\nCommands:\n generate <config> Generate synthetic data from config file\n validate <config> Validate config without generating\n init Scaffold a starter synode.config.ts\n\nOptions:\n --users, -u <n> Override user count\n --lanes, -l <n> Override lane count\n --workers, -w <n> Enable worker threads with N workers\n --output, -o <path> Output file path (default: stdout)\n --format, -f <fmt> Output format: json, jsonl, csv\n --debug Enable telemetry\n --dry-run Validate only, generate 1 user\n --quiet, -q Suppress progress output\n --help, -h Show this help\n`;\n\nfunction printUsage(): void {\n process.stderr.write(USAGE);\n}\n\n/**\n * Parses CLI arguments into a command, config path, and flags.\n *\n * @param args - Raw CLI arguments (typically `process.argv.slice(2)`).\n * @returns Parsed command, optional config path, and CLI flags.\n */\nexport function parseCli(args: string[]): {\n command: string;\n configPath?: string;\n flags: CliFlags;\n} {\n const { values, positionals } = parseArgs({\n args,\n options: {\n users: { type: 'string', short: 'u' },\n lanes: { type: 'string', short: 'l' },\n workers: { type: 'string', short: 'w' },\n output: { type: 'string', short: 'o' },\n format: { type: 'string', short: 'f' },\n debug: { type: 'boolean', default: false },\n 'dry-run': { type: 'boolean', default: false },\n quiet: { type: 'boolean', short: 'q', default: false },\n help: { type: 'boolean', short: 'h', default: false },\n },\n allowPositionals: true,\n strict: true,\n });\n\n if (values.help) {\n printUsage();\n process.exit(0);\n }\n\n const command = positionals[0] ?? '';\n const configPath = positionals[1];\n\n const flags: CliFlags = {\n users: values.users ? Number(values.users) : undefined,\n lanes: values.lanes ? Number(values.lanes) : undefined,\n workers: values.workers ? Number(values.workers) : undefined,\n output: values.output,\n format: values.format,\n debug: values.debug,\n dryRun: values['dry-run'],\n quiet: values.quiet,\n };\n\n // Validate numeric flags\n if (flags.users !== undefined && (!Number.isFinite(flags.users) || flags.users < 1)) {\n process.stderr.write('Error: --users must be a positive number\\n');\n process.exit(1);\n }\n if (flags.lanes !== undefined && (!Number.isFinite(flags.lanes) || flags.lanes < 1)) {\n process.stderr.write('Error: --lanes must be a positive number\\n');\n process.exit(1);\n }\n if (flags.format && !['json', 'jsonl', 'csv'].includes(flags.format)) {\n process.stderr.write(`Error: --format must be json, jsonl, or csv. Got: ${flags.format}\\n`);\n process.exit(1);\n }\n\n return { command, configPath, flags };\n}\n\nasync function main(): Promise<void> {\n const { command, configPath, flags } = parseCli(process.argv.slice(2));\n\n switch (command) {\n case 'generate': {\n if (!configPath) {\n process.stderr.write('Error: generate requires a config file path\\n');\n process.exit(1);\n }\n const config = await loadConfig(configPath);\n const merged = mergeFlags(config, flags);\n if (flags.dryRun) {\n merged.options = { ...merged.options, users: 1 };\n }\n await runGenerate(merged, flags);\n break;\n }\n case 'validate': {\n if (!configPath) {\n process.stderr.write('Error: validate requires a config file path\\n');\n process.exit(1);\n }\n const config = await loadConfig(configPath);\n const valid = runValidate(config);\n process.exit(valid ? 0 : 1);\n break;\n }\n case 'init': {\n const filePath = await runInit();\n process.stderr.write(`Created ${filePath}\\n`);\n break;\n }\n default:\n printUsage();\n process.exit(command ? 1 : 0);\n }\n}\n\n// Only run when executed directly, not when imported for testing\nconst isDirectRun =\n process.argv[1] &&\n (import.meta.url === `file://${process.argv[1]}` ||\n import.meta.url === new URL(`file://${process.argv[1]}`).href);\n\nif (isDirectRun) {\n main().catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`Error: ${message}\\n`);\n process.exit(1);\n });\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@synode/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for Synode synthetic data generation",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"synode": "./dist/index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.cjs",
|
|
10
|
+
"module": "dist/index.mjs",
|
|
11
|
+
"types": "dist/index.d.mts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"import": "./dist/index.mjs",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"author": "Digitl Cloud GmbH",
|
|
23
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/digitl-cloud/synode",
|
|
27
|
+
"directory": "packages/cli"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@synode/core": "1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"optionalDependencies": {
|
|
33
|
+
"@synode/adapter-file": "1.0.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^24.12.0",
|
|
37
|
+
"eslint": "^9.39.4",
|
|
38
|
+
"eslint-config-prettier": "^10.1.8",
|
|
39
|
+
"typescript-eslint": "^8.58.0",
|
|
40
|
+
"tsdown": "^0.16.8",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.1.2"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsdown",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"lint": "eslint src tests"
|
|
48
|
+
}
|
|
49
|
+
}
|