@criterionx/cli 0.3.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Tomas Maritano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # @criterionx/cli
2
+
3
+ CLI for scaffolding Criterion decisions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @criterionx/cli
9
+ # or
10
+ npx @criterionx/cli
11
+ ```
12
+
13
+ ## Commands
14
+
15
+ ### `criterion init`
16
+
17
+ Initialize a new Criterion project with example decision.
18
+
19
+ ```bash
20
+ criterion init
21
+ criterion init --dir my-project
22
+ criterion init --no-install
23
+ ```
24
+
25
+ Creates:
26
+ - `package.json` with dependencies
27
+ - `tsconfig.json` configured for ESM
28
+ - `src/decisions/transaction-risk.ts` - Example decision
29
+ - `src/index.ts` - Example usage
30
+
31
+ ### `criterion new decision <name>`
32
+
33
+ Generate a new decision boilerplate.
34
+
35
+ ```bash
36
+ criterion new decision user-eligibility
37
+ criterion new decision LoanApproval
38
+ criterion new decision "payment risk" --dir src/decisions
39
+ ```
40
+
41
+ Creates a decision file with:
42
+ - Input/output/profile schemas
43
+ - Default rule
44
+ - TODO comments for customization
45
+
46
+ ### `criterion new profile <name>`
47
+
48
+ Generate a new profile template.
49
+
50
+ ```bash
51
+ criterion new profile us-standard
52
+ criterion new profile eu-premium
53
+ ```
54
+
55
+ ### `criterion list` (coming soon)
56
+
57
+ List all decisions in the project.
58
+
59
+ ### `criterion validate` (coming soon)
60
+
61
+ Validate all decisions in the project.
62
+
63
+ ## Example Workflow
64
+
65
+ ```bash
66
+ # 1. Create new project
67
+ criterion init --dir my-decisions
68
+ cd my-decisions
69
+
70
+ # 2. Generate decisions
71
+ criterion new decision loan-approval
72
+ criterion new decision fraud-detection
73
+
74
+ # 3. Run your code
75
+ npx tsx src/index.ts
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,641 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import fs from "fs";
8
+ import path from "path";
9
+ import { execSync } from "child_process";
10
+ import pc from "picocolors";
11
+ var PACKAGE_JSON = `{
12
+ "name": "my-criterion-project",
13
+ "version": "1.0.0",
14
+ "type": "module",
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "typecheck": "tsc --noEmit"
18
+ },
19
+ "dependencies": {
20
+ "@criterionx/core": "^0.3.0",
21
+ "zod": "^3.22.0"
22
+ },
23
+ "devDependencies": {
24
+ "typescript": "^5.3.0"
25
+ }
26
+ }`;
27
+ var TSCONFIG = `{
28
+ "compilerOptions": {
29
+ "target": "ES2022",
30
+ "module": "ESNext",
31
+ "moduleResolution": "bundler",
32
+ "strict": true,
33
+ "esModuleInterop": true,
34
+ "skipLibCheck": true,
35
+ "outDir": "dist",
36
+ "rootDir": "src"
37
+ },
38
+ "include": ["src/**/*"]
39
+ }`;
40
+ var EXAMPLE_DECISION = `import { defineDecision } from "@criterionx/core";
41
+ import { z } from "zod";
42
+
43
+ /**
44
+ * Example: Transaction Risk Decision
45
+ *
46
+ * Evaluates the risk level of a transaction based on amount and profile thresholds.
47
+ */
48
+ export const transactionRisk = defineDecision({
49
+ id: "transaction-risk",
50
+ version: "1.0.0",
51
+
52
+ inputSchema: z.object({
53
+ amount: z.number().positive(),
54
+ currency: z.string().length(3),
55
+ }),
56
+
57
+ outputSchema: z.object({
58
+ risk: z.enum(["HIGH", "MEDIUM", "LOW"]),
59
+ reason: z.string(),
60
+ }),
61
+
62
+ profileSchema: z.object({
63
+ highThreshold: z.number(),
64
+ mediumThreshold: z.number(),
65
+ }),
66
+
67
+ rules: [
68
+ {
69
+ id: "high-risk",
70
+ when: (input, profile) => input.amount > profile.highThreshold,
71
+ emit: () => ({ risk: "HIGH", reason: "Amount exceeds high threshold" }),
72
+ explain: (input, profile) =>
73
+ \`Amount \${input.amount} > \${profile.highThreshold}\`,
74
+ },
75
+ {
76
+ id: "medium-risk",
77
+ when: (input, profile) => input.amount > profile.mediumThreshold,
78
+ emit: () => ({ risk: "MEDIUM", reason: "Amount exceeds medium threshold" }),
79
+ explain: (input, profile) =>
80
+ \`Amount \${input.amount} > \${profile.mediumThreshold}\`,
81
+ },
82
+ {
83
+ id: "low-risk",
84
+ when: () => true,
85
+ emit: () => ({ risk: "LOW", reason: "Amount within acceptable range" }),
86
+ explain: () => "Default: amount within limits",
87
+ },
88
+ ],
89
+ });
90
+ `;
91
+ var EXAMPLE_MAIN = `import { Engine } from "@criterionx/core";
92
+ import { transactionRisk } from "./decisions/transaction-risk.js";
93
+
94
+ const engine = new Engine();
95
+
96
+ // Example: Evaluate a transaction
97
+ const result = engine.run(
98
+ transactionRisk,
99
+ { amount: 15000, currency: "USD" },
100
+ { profile: { highThreshold: 10000, mediumThreshold: 5000 } }
101
+ );
102
+
103
+ console.log("Status:", result.status);
104
+ console.log("Data:", result.data);
105
+ console.log("\\nExplanation:");
106
+ console.log(engine.explain(result));
107
+ `;
108
+ async function initCommand(options) {
109
+ const targetDir = path.resolve(options.dir);
110
+ console.log(pc.cyan("\\n\u{1F3AF} Initializing Criterion project...\\n"));
111
+ const dirs = [
112
+ targetDir,
113
+ path.join(targetDir, "src"),
114
+ path.join(targetDir, "src", "decisions")
115
+ ];
116
+ for (const dir of dirs) {
117
+ if (!fs.existsSync(dir)) {
118
+ fs.mkdirSync(dir, { recursive: true });
119
+ console.log(pc.green(" \u2713"), pc.dim("Created"), dir);
120
+ }
121
+ }
122
+ const files = [
123
+ { path: path.join(targetDir, "package.json"), content: PACKAGE_JSON },
124
+ { path: path.join(targetDir, "tsconfig.json"), content: TSCONFIG },
125
+ {
126
+ path: path.join(targetDir, "src", "decisions", "transaction-risk.ts"),
127
+ content: EXAMPLE_DECISION
128
+ },
129
+ { path: path.join(targetDir, "src", "index.ts"), content: EXAMPLE_MAIN }
130
+ ];
131
+ for (const file of files) {
132
+ if (!fs.existsSync(file.path)) {
133
+ fs.writeFileSync(file.path, file.content);
134
+ console.log(pc.green(" \u2713"), pc.dim("Created"), file.path);
135
+ } else {
136
+ console.log(pc.yellow(" \u26A0"), pc.dim("Exists"), file.path);
137
+ }
138
+ }
139
+ if (options.install) {
140
+ console.log(pc.cyan("\\n\u{1F4E6} Installing dependencies...\\n"));
141
+ try {
142
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
143
+ } catch {
144
+ console.log(pc.yellow("\\n\u26A0 Failed to install dependencies. Run 'npm install' manually."));
145
+ }
146
+ }
147
+ console.log(pc.green("\\n\u2705 Project initialized!\\n"));
148
+ console.log("Next steps:");
149
+ console.log(pc.dim(" cd " + (options.dir === "." ? "." : options.dir)));
150
+ if (!options.install) {
151
+ console.log(pc.dim(" npm install"));
152
+ }
153
+ console.log(pc.dim(" npx tsx src/index.ts"));
154
+ console.log();
155
+ }
156
+
157
+ // src/commands/new.ts
158
+ import fs2 from "fs";
159
+ import path2 from "path";
160
+ import pc2 from "picocolors";
161
+ function toKebabCase(str) {
162
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
163
+ }
164
+ function toPascalCase(str) {
165
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
166
+ }
167
+ function toCamelCase(str) {
168
+ const pascal = toPascalCase(str);
169
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
170
+ }
171
+ function generateDecision(name) {
172
+ const id = toKebabCase(name);
173
+ const varName = toCamelCase(name);
174
+ return `import { defineDecision } from "@criterionx/core";
175
+ import { z } from "zod";
176
+
177
+ /**
178
+ * ${toPascalCase(name)} Decision
179
+ *
180
+ * TODO: Add description
181
+ */
182
+ export const ${varName} = defineDecision({
183
+ id: "${id}",
184
+ version: "1.0.0",
185
+
186
+ inputSchema: z.object({
187
+ // TODO: Define input schema
188
+ value: z.string(),
189
+ }),
190
+
191
+ outputSchema: z.object({
192
+ // TODO: Define output schema
193
+ result: z.string(),
194
+ }),
195
+
196
+ profileSchema: z.object({
197
+ // TODO: Define profile schema (parameters that can vary)
198
+ }),
199
+
200
+ rules: [
201
+ {
202
+ id: "default",
203
+ when: () => true,
204
+ emit: () => ({ result: "OK" }),
205
+ explain: () => "Default rule",
206
+ },
207
+ ],
208
+ });
209
+ `;
210
+ }
211
+ function generateProfile(name) {
212
+ const varName = toCamelCase(name) + "Profile";
213
+ return `/**
214
+ * ${toPascalCase(name)} Profile
215
+ *
216
+ * Profiles parameterize decisions without changing logic.
217
+ * Create different profiles for different regions, tiers, or environments.
218
+ */
219
+ export const ${varName} = {
220
+ // TODO: Define profile values
221
+ // These should match the profileSchema of your decision
222
+ };
223
+
224
+ // Example: Multiple profiles for different contexts
225
+ // export const ${varName}US = { threshold: 10000 };
226
+ // export const ${varName}EU = { threshold: 8000 };
227
+ `;
228
+ }
229
+ async function newCommand(type, name, options) {
230
+ const validTypes = ["decision", "profile"];
231
+ if (!validTypes.includes(type)) {
232
+ console.log(pc2.red(`\\n\u274C Invalid type: ${type}`));
233
+ console.log(pc2.dim(` Valid types: ${validTypes.join(", ")}\\n`));
234
+ process.exit(1);
235
+ }
236
+ const fileName = toKebabCase(name) + ".ts";
237
+ const targetDir = path2.resolve(options.dir);
238
+ const filePath = path2.join(targetDir, fileName);
239
+ console.log(pc2.cyan(`\\n\u{1F3AF} Generating ${type}: ${name}\\n`));
240
+ if (!fs2.existsSync(targetDir)) {
241
+ fs2.mkdirSync(targetDir, { recursive: true });
242
+ console.log(pc2.green(" \u2713"), pc2.dim("Created directory"), targetDir);
243
+ }
244
+ if (fs2.existsSync(filePath)) {
245
+ console.log(pc2.red(`\\n\u274C File already exists: ${filePath}\\n`));
246
+ process.exit(1);
247
+ }
248
+ let content;
249
+ switch (type) {
250
+ case "decision":
251
+ content = generateDecision(name);
252
+ break;
253
+ case "profile":
254
+ content = generateProfile(name);
255
+ break;
256
+ default:
257
+ throw new Error(`Unknown type: ${type}`);
258
+ }
259
+ fs2.writeFileSync(filePath, content);
260
+ console.log(pc2.green(" \u2713"), pc2.dim("Created"), filePath);
261
+ console.log(pc2.green(`\\n\u2705 ${toPascalCase(type)} created!\\n`));
262
+ console.log("Next steps:");
263
+ console.log(pc2.dim(` 1. Edit ${filePath}`));
264
+ console.log(pc2.dim(` 2. Define your schemas and rules`));
265
+ console.log(pc2.dim(` 3. Import and use in your application\\n`));
266
+ }
267
+
268
+ // src/commands/list.ts
269
+ import fs3 from "fs";
270
+ import path3 from "path";
271
+ import pc3 from "picocolors";
272
+ function findDecisionFiles(dir) {
273
+ const files = [];
274
+ function walk(currentDir) {
275
+ if (!fs3.existsSync(currentDir)) return;
276
+ const entries = fs3.readdirSync(currentDir, { withFileTypes: true });
277
+ for (const entry of entries) {
278
+ const fullPath = path3.join(currentDir, entry.name);
279
+ if (entry.isDirectory()) {
280
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
281
+ continue;
282
+ }
283
+ walk(fullPath);
284
+ } else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")) {
285
+ files.push(fullPath);
286
+ }
287
+ }
288
+ }
289
+ walk(dir);
290
+ return files;
291
+ }
292
+ function isDecisionFile(content) {
293
+ return content.includes("defineDecision") && content.includes("@criterionx/core");
294
+ }
295
+ function extractDecisionInfo(content, filePath) {
296
+ const defineDecisionMatch = content.match(/defineDecision\s*\(\s*\{/);
297
+ if (!defineDecisionMatch) {
298
+ return null;
299
+ }
300
+ const startPos = defineDecisionMatch.index;
301
+ const blockEnd = findBlockEnd(content, startPos);
302
+ const block = content.slice(startPos, blockEnd);
303
+ const idMatch = block.match(/id\s*:\s*["'`]([^"'`]+)["'`]/);
304
+ const id = idMatch ? idMatch[1] : "unknown";
305
+ const versionMatch = block.match(/version\s*:\s*["'`]([^"'`]+)["'`]/);
306
+ const version = versionMatch ? versionMatch[1] : "unknown";
307
+ const rulesMatch = block.match(/rules\s*:\s*\[/);
308
+ let rulesCount = 0;
309
+ if (rulesMatch) {
310
+ const rulesStart = rulesMatch.index + rulesMatch[0].length;
311
+ const rulesEnd = findArrayEnd(block, rulesMatch.index);
312
+ const rulesBlock = block.slice(rulesStart, rulesEnd);
313
+ const ruleMatches = rulesBlock.match(/\{\s*id\s*:/g);
314
+ rulesCount = ruleMatches ? ruleMatches.length : 0;
315
+ }
316
+ return { file: filePath, id, version, rulesCount };
317
+ }
318
+ function findBlockEnd(text, start) {
319
+ let depth = 0;
320
+ let inString = false;
321
+ let stringChar = "";
322
+ for (let i = start; i < text.length; i++) {
323
+ const char = text[i];
324
+ const prevChar = i > 0 ? text[i - 1] : "";
325
+ if (inString) {
326
+ if (char === stringChar && prevChar !== "\\") {
327
+ inString = false;
328
+ }
329
+ continue;
330
+ }
331
+ if (char === '"' || char === "'" || char === "`") {
332
+ inString = true;
333
+ stringChar = char;
334
+ continue;
335
+ }
336
+ if (char === "{") {
337
+ depth++;
338
+ } else if (char === "}") {
339
+ depth--;
340
+ if (depth === 0) {
341
+ return i + 1;
342
+ }
343
+ }
344
+ }
345
+ return text.length;
346
+ }
347
+ function findArrayEnd(text, start) {
348
+ let depth = 0;
349
+ let inString = false;
350
+ let stringChar = "";
351
+ for (let i = start; i < text.length; i++) {
352
+ const char = text[i];
353
+ const prevChar = i > 0 ? text[i - 1] : "";
354
+ if (inString) {
355
+ if (char === stringChar && prevChar !== "\\") {
356
+ inString = false;
357
+ }
358
+ continue;
359
+ }
360
+ if (char === '"' || char === "'" || char === "`") {
361
+ inString = true;
362
+ stringChar = char;
363
+ continue;
364
+ }
365
+ if (char === "[") {
366
+ depth++;
367
+ } else if (char === "]") {
368
+ depth--;
369
+ if (depth === 0) {
370
+ return i;
371
+ }
372
+ }
373
+ }
374
+ return text.length;
375
+ }
376
+ async function listCommand(options) {
377
+ const targetDir = path3.resolve(options.dir);
378
+ if (!fs3.existsSync(targetDir)) {
379
+ if (options.json) {
380
+ console.log(JSON.stringify({ error: "Directory not found", decisions: [] }));
381
+ } else {
382
+ console.log(pc3.red(`
383
+ \u274C Directory not found: ${targetDir}
384
+ `));
385
+ }
386
+ process.exit(1);
387
+ }
388
+ const files = findDecisionFiles(targetDir);
389
+ const decisions = [];
390
+ for (const file of files) {
391
+ const content = fs3.readFileSync(file, "utf-8");
392
+ if (!isDecisionFile(content)) {
393
+ continue;
394
+ }
395
+ const info = extractDecisionInfo(content, file);
396
+ if (info) {
397
+ decisions.push(info);
398
+ }
399
+ }
400
+ if (options.json) {
401
+ console.log(JSON.stringify({ decisions }, null, 2));
402
+ return;
403
+ }
404
+ if (decisions.length === 0) {
405
+ console.log(pc3.yellow("\n\u26A0 No decisions found in"), pc3.dim(targetDir));
406
+ console.log(pc3.dim(" Decisions should import from '@criterionx/core' and use defineDecision()"));
407
+ console.log();
408
+ return;
409
+ }
410
+ console.log(pc3.cyan("\n\u{1F4CB} Criterion Decisions\n"));
411
+ const maxIdLen = Math.max(...decisions.map((d) => d.id.length), 2);
412
+ const maxVersionLen = Math.max(...decisions.map((d) => d.version.length), 7);
413
+ console.log(
414
+ pc3.dim(" ") + pc3.bold("ID".padEnd(maxIdLen + 2)) + pc3.bold("VERSION".padEnd(maxVersionLen + 2)) + pc3.bold("RULES".padEnd(7)) + pc3.bold("FILE")
415
+ );
416
+ console.log(pc3.dim(" " + "\u2500".repeat(60)));
417
+ for (const decision of decisions) {
418
+ const relativePath = path3.relative(targetDir, decision.file);
419
+ console.log(
420
+ pc3.dim(" ") + pc3.green(decision.id.padEnd(maxIdLen + 2)) + pc3.cyan(decision.version.padEnd(maxVersionLen + 2)) + String(decision.rulesCount).padEnd(7) + pc3.dim(relativePath)
421
+ );
422
+ }
423
+ console.log();
424
+ console.log(pc3.dim(` Found ${decisions.length} decision(s)`));
425
+ console.log();
426
+ }
427
+
428
+ // src/commands/validate.ts
429
+ import fs4 from "fs";
430
+ import path4 from "path";
431
+ import pc4 from "picocolors";
432
+ function findDecisionFiles2(dir) {
433
+ const files = [];
434
+ function walk(currentDir) {
435
+ if (!fs4.existsSync(currentDir)) return;
436
+ const entries = fs4.readdirSync(currentDir, { withFileTypes: true });
437
+ for (const entry of entries) {
438
+ const fullPath = path4.join(currentDir, entry.name);
439
+ if (entry.isDirectory()) {
440
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) {
441
+ continue;
442
+ }
443
+ walk(fullPath);
444
+ } else if (entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts")) {
445
+ files.push(fullPath);
446
+ }
447
+ }
448
+ }
449
+ walk(dir);
450
+ return files;
451
+ }
452
+ function isDecisionFile2(content) {
453
+ return content.includes("defineDecision") && content.includes("@criterionx/core");
454
+ }
455
+ function validateDecision(content, filePath) {
456
+ const errors = [];
457
+ const warnings = [];
458
+ const defineDecisionMatch = content.match(/defineDecision\s*\(\s*\{/);
459
+ if (!defineDecisionMatch) {
460
+ return null;
461
+ }
462
+ const startPos = defineDecisionMatch.index;
463
+ const blockEnd = findBlockEnd2(content, startPos);
464
+ const block = content.slice(startPos, blockEnd);
465
+ const requiredProps = [
466
+ { prop: "id:", message: "Missing required 'id' property" },
467
+ { prop: "version:", message: "Missing required 'version' property" },
468
+ { prop: "inputSchema:", message: "Missing required 'inputSchema' property" },
469
+ { prop: "outputSchema:", message: "Missing required 'outputSchema' property" },
470
+ { prop: "profileSchema:", message: "Missing required 'profileSchema' property" },
471
+ { prop: "rules:", message: "Missing required 'rules' property" }
472
+ ];
473
+ for (const { prop, message } of requiredProps) {
474
+ if (!block.includes(prop)) {
475
+ errors.push(message);
476
+ }
477
+ }
478
+ if (/rules\s*:\s*\[\s*\]/.test(block)) {
479
+ warnings.push("Decision has empty rules array");
480
+ }
481
+ const rulesMatch = block.match(/rules\s*:\s*\[/);
482
+ if (rulesMatch) {
483
+ const rulesStart = rulesMatch.index + rulesMatch[0].length;
484
+ const rulesEnd = findArrayEnd2(block, rulesMatch.index);
485
+ const rulesBlock = block.slice(rulesStart, rulesEnd);
486
+ const ruleRegex = /\{\s*id\s*:/g;
487
+ let ruleMatch;
488
+ while ((ruleMatch = ruleRegex.exec(rulesBlock)) !== null) {
489
+ const ruleStart = ruleMatch.index;
490
+ const ruleEnd = findBlockEnd2(rulesBlock, ruleStart);
491
+ const rule = rulesBlock.slice(ruleStart, ruleEnd);
492
+ const idMatch = rule.match(/id\s*:\s*["'`]([^"'`]+)["'`]/);
493
+ const ruleId = idMatch ? idMatch[1] : "unknown";
494
+ const requiredFunctions = ["when:", "emit:", "explain:"];
495
+ for (const func of requiredFunctions) {
496
+ if (!rule.includes(func)) {
497
+ errors.push(`Rule '${ruleId}' missing required '${func.replace(":", "")}' function`);
498
+ }
499
+ }
500
+ }
501
+ }
502
+ if (errors.length === 0 && warnings.length === 0) {
503
+ return null;
504
+ }
505
+ return { file: filePath, errors, warnings };
506
+ }
507
+ function findBlockEnd2(text, start) {
508
+ let depth = 0;
509
+ let inString = false;
510
+ let stringChar = "";
511
+ for (let i = start; i < text.length; i++) {
512
+ const char = text[i];
513
+ const prevChar = i > 0 ? text[i - 1] : "";
514
+ if (inString) {
515
+ if (char === stringChar && prevChar !== "\\") {
516
+ inString = false;
517
+ }
518
+ continue;
519
+ }
520
+ if (char === '"' || char === "'" || char === "`") {
521
+ inString = true;
522
+ stringChar = char;
523
+ continue;
524
+ }
525
+ if (char === "{") {
526
+ depth++;
527
+ } else if (char === "}") {
528
+ depth--;
529
+ if (depth === 0) {
530
+ return i + 1;
531
+ }
532
+ }
533
+ }
534
+ return text.length;
535
+ }
536
+ function findArrayEnd2(text, start) {
537
+ let depth = 0;
538
+ let inString = false;
539
+ let stringChar = "";
540
+ for (let i = start; i < text.length; i++) {
541
+ const char = text[i];
542
+ const prevChar = i > 0 ? text[i - 1] : "";
543
+ if (inString) {
544
+ if (char === stringChar && prevChar !== "\\") {
545
+ inString = false;
546
+ }
547
+ continue;
548
+ }
549
+ if (char === '"' || char === "'" || char === "`") {
550
+ inString = true;
551
+ stringChar = char;
552
+ continue;
553
+ }
554
+ if (char === "[") {
555
+ depth++;
556
+ } else if (char === "]") {
557
+ depth--;
558
+ if (depth === 0) {
559
+ return i;
560
+ }
561
+ }
562
+ }
563
+ return text.length;
564
+ }
565
+ async function validateCommand(options) {
566
+ const targetDir = path4.resolve(options.dir);
567
+ console.log(pc4.cyan("\n\u{1F50D} Validating Criterion decisions...\n"));
568
+ if (!fs4.existsSync(targetDir)) {
569
+ console.log(pc4.red(`
570
+ \u274C Directory not found: ${targetDir}
571
+ `));
572
+ process.exit(1);
573
+ }
574
+ const files = findDecisionFiles2(targetDir);
575
+ if (files.length === 0) {
576
+ console.log(pc4.yellow(" No TypeScript files found in"), pc4.dim(targetDir));
577
+ console.log();
578
+ return;
579
+ }
580
+ let decisionsFound = 0;
581
+ let decisionsValid = 0;
582
+ const validationErrors = [];
583
+ for (const file of files) {
584
+ const content = fs4.readFileSync(file, "utf-8");
585
+ if (!isDecisionFile2(content)) {
586
+ continue;
587
+ }
588
+ decisionsFound++;
589
+ const result = validateDecision(content, file);
590
+ if (result) {
591
+ validationErrors.push(result);
592
+ } else {
593
+ decisionsValid++;
594
+ const relativePath = path4.relative(targetDir, file);
595
+ console.log(pc4.green(" \u2713"), pc4.dim(relativePath));
596
+ }
597
+ }
598
+ if (validationErrors.length > 0) {
599
+ console.log();
600
+ for (const result of validationErrors) {
601
+ const relativePath = path4.relative(targetDir, result.file);
602
+ console.log(pc4.red(" \u2717"), pc4.dim(relativePath));
603
+ for (const error of result.errors) {
604
+ console.log(pc4.red(" \u2192"), error);
605
+ }
606
+ for (const warning of result.warnings) {
607
+ console.log(pc4.yellow(" \u2192"), warning);
608
+ }
609
+ }
610
+ }
611
+ console.log();
612
+ if (decisionsFound === 0) {
613
+ console.log(pc4.yellow("\u26A0 No decisions found in"), pc4.dim(targetDir));
614
+ console.log(pc4.dim(" Decisions should import from '@criterionx/core' and use defineDecision()"));
615
+ } else if (validationErrors.length === 0) {
616
+ console.log(pc4.green(`\u2705 All ${decisionsFound} decision(s) are valid!`));
617
+ } else {
618
+ const errorCount = validationErrors.reduce((sum, e) => sum + e.errors.length, 0);
619
+ const warningCount = validationErrors.reduce((sum, e) => sum + e.warnings.length, 0);
620
+ console.log(
621
+ pc4.red(`\u274C Found issues in ${validationErrors.length} of ${decisionsFound} decision(s)`)
622
+ );
623
+ if (errorCount > 0) {
624
+ console.log(pc4.red(` ${errorCount} error(s)`));
625
+ }
626
+ if (warningCount > 0) {
627
+ console.log(pc4.yellow(` ${warningCount} warning(s)`));
628
+ }
629
+ process.exit(1);
630
+ }
631
+ console.log();
632
+ }
633
+
634
+ // src/index.ts
635
+ var program = new Command();
636
+ program.name("criterion").description("CLI for scaffolding and managing Criterion decisions").version("0.3.1");
637
+ program.command("init").description("Initialize a new Criterion project").option("-d, --dir <directory>", "Target directory", ".").option("--no-install", "Skip npm install").action(initCommand);
638
+ program.command("new").description("Generate new Criterion components").argument("<type>", "Type to generate (decision, profile)").argument("<name>", "Name of the component").option("-d, --dir <directory>", "Target directory", "src/decisions").action(newCommand);
639
+ program.command("list").description("List all decisions in the project").option("-d, --dir <directory>", "Directory to search", ".").option("--json", "Output as JSON").action(listCommand);
640
+ program.command("validate").description("Validate all decisions in the project").option("-d, --dir <directory>", "Directory to validate", ".").action(validateCommand);
641
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@criterionx/cli",
3
+ "version": "0.3.1",
4
+ "description": "CLI for scaffolding Criterion decisions",
5
+ "type": "module",
6
+ "bin": {
7
+ "criterion": "./dist/index.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "types": "dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "templates",
14
+ "LICENSE",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "criterion",
19
+ "cli",
20
+ "decision-engine",
21
+ "scaffolding",
22
+ "generator"
23
+ ],
24
+ "author": {
25
+ "name": "Tomas Maritano",
26
+ "url": "https://github.com/tomymaritano"
27
+ },
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/tomymaritano/criterionx.git",
32
+ "directory": "packages/cli"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/tomymaritano/criterionx/issues"
36
+ },
37
+ "homepage": "https://github.com/tomymaritano/criterionx#readme",
38
+ "dependencies": {
39
+ "commander": "^12.0.0",
40
+ "picocolors": "^1.0.0"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^20.0.0",
44
+ "tsup": "^8.0.0",
45
+ "tsx": "^4.0.0",
46
+ "typescript": "^5.3.0",
47
+ "vitest": "^4.0.0"
48
+ },
49
+ "engines": {
50
+ "node": ">=18"
51
+ },
52
+ "scripts": {
53
+ "build": "tsup src/index.ts --format esm --dts --clean",
54
+ "dev": "tsx src/index.ts",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest",
57
+ "test:coverage": "vitest run --coverage",
58
+ "typecheck": "tsc --noEmit"
59
+ }
60
+ }