@yasserkhanorg/e2e-agents 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -9
- package/dist/agent/feedback.d.ts +16 -0
- package/dist/agent/feedback.d.ts.map +1 -1
- package/dist/agent/feedback.js +62 -0
- package/dist/agent/process_runner.d.ts +1 -1
- package/dist/agent/process_runner.d.ts.map +1 -1
- package/dist/agent/process_runner.js +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +5 -2
- package/dist/cli/commands/train.d.ts +3 -0
- package/dist/cli/commands/train.d.ts.map +1 -0
- package/dist/cli/commands/train.js +307 -0
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +7 -1
- package/dist/cli/types.d.ts +6 -1
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/usage.d.ts.map +1 -1
- package/dist/cli/usage.js +7 -1
- package/dist/cli.js +5 -0
- package/dist/engine/plan_builder.d.ts +2 -1
- package/dist/engine/plan_builder.d.ts.map +1 -1
- package/dist/engine/plan_builder.js +22 -9
- package/dist/esm/agent/feedback.js +61 -0
- package/dist/esm/agent/process_runner.js +3 -3
- package/dist/esm/api.js +5 -2
- package/dist/esm/cli/commands/train.js +271 -0
- package/dist/esm/cli/parse_args.js +7 -1
- package/dist/esm/cli/usage.js +7 -1
- package/dist/esm/cli.js +5 -0
- package/dist/esm/engine/plan_builder.js +22 -9
- package/dist/esm/index.js +6 -1
- package/dist/esm/knowledge/route_families.js +2 -2
- package/dist/esm/pipeline/spec_verifier.js +75 -0
- package/dist/esm/pipeline/stage3_generation.js +122 -4
- package/dist/esm/pipeline/stage4_heal.js +146 -3
- package/dist/esm/prompts/heal.js +4 -0
- package/dist/esm/qa-agent/phase2/agent_loop.js +60 -24
- package/dist/esm/qa-agent/phase2/exploration_state.js +21 -0
- package/dist/esm/qa-agent/phase2/tools.js +99 -1
- package/dist/esm/qa-agent/phase3/reporter.js +31 -4
- package/dist/esm/training/enricher.js +273 -0
- package/dist/esm/training/merger.js +137 -0
- package/dist/esm/training/scanner.js +386 -0
- package/dist/esm/training/types.js +6 -0
- package/dist/esm/training/validator.js +153 -0
- package/dist/esm/validation/guardrails.js +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -1
- package/dist/knowledge/route_families.d.ts +2 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +2 -0
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/spec_verifier.d.ts +20 -0
- package/dist/pipeline/spec_verifier.d.ts.map +1 -0
- package/dist/pipeline/spec_verifier.js +79 -0
- package/dist/pipeline/stage3_generation.d.ts +10 -0
- package/dist/pipeline/stage3_generation.d.ts.map +1 -1
- package/dist/pipeline/stage3_generation.js +120 -2
- package/dist/pipeline/stage4_heal.d.ts +4 -0
- package/dist/pipeline/stage4_heal.d.ts.map +1 -1
- package/dist/pipeline/stage4_heal.js +145 -2
- package/dist/prompts/heal.d.ts +2 -0
- package/dist/prompts/heal.d.ts.map +1 -1
- package/dist/prompts/heal.js +4 -0
- package/dist/qa-agent/phase2/agent_loop.d.ts.map +1 -1
- package/dist/qa-agent/phase2/agent_loop.js +60 -24
- package/dist/qa-agent/phase2/exploration_state.d.ts.map +1 -1
- package/dist/qa-agent/phase2/exploration_state.js +21 -0
- package/dist/qa-agent/phase2/tools.d.ts.map +1 -1
- package/dist/qa-agent/phase2/tools.js +99 -1
- package/dist/qa-agent/phase3/reporter.js +31 -4
- package/dist/qa-agent/types.d.ts +9 -1
- package/dist/qa-agent/types.d.ts.map +1 -1
- package/dist/training/enricher.d.ts +15 -0
- package/dist/training/enricher.d.ts.map +1 -0
- package/dist/training/enricher.js +278 -0
- package/dist/training/merger.d.ts +5 -0
- package/dist/training/merger.d.ts.map +1 -0
- package/dist/training/merger.js +141 -0
- package/dist/training/scanner.d.ts +5 -0
- package/dist/training/scanner.d.ts.map +1 -0
- package/dist/training/scanner.js +391 -0
- package/dist/training/types.d.ts +109 -0
- package/dist/training/types.d.ts.map +1 -0
- package/dist/training/types.js +9 -0
- package/dist/training/validator.d.ts +16 -0
- package/dist/training/validator.d.ts.map +1 -0
- package/dist/training/validator.js +160 -0
- package/dist/validation/guardrails.d.ts +2 -0
- package/dist/validation/guardrails.d.ts.map +1 -1
- package/dist/validation/guardrails.js +4 -1
- package/package.json +1 -1
package/dist/cli/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,eAAe,EAAE,aAAa,EAAC,MAAM,oBAAoB,CAAC;AAEvE,MAAM,MAAM,OAAO,GACf,MAAM,GACJ,QAAQ,GACR,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,0BAA0B,GAC1B,UAAU,GACV,sBAAsB,GACtB,qBAAqB,GACrB,SAAS,GACT,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/usage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/cli/usage.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,IAAI,IAAI,
|
|
1
|
+
{"version":3,"file":"usage.d.ts","sourceRoot":"","sources":["../../src/cli/usage.ts"],"names":[],"mappings":"AAGA,wBAAgB,UAAU,IAAI,IAAI,CAyFjC"}
|
package/dist/cli/usage.js
CHANGED
|
@@ -17,11 +17,12 @@ function printUsage() {
|
|
|
17
17
|
' e2e-ai-agents traceability-ingest --path <app-root> --traceability-input <json>',
|
|
18
18
|
' e2e-ai-agents generate [--scenarios <path|json>] [--max-attempts <n>] [--dry-run]',
|
|
19
19
|
' e2e-ai-agents analyze --path <app-root> [--tests-root <path>] [--since <ref>] [--generate] [--generate-output <dir>] [--heal] [--heal-report <json>]',
|
|
20
|
+
' e2e-ai-agents train --path <project-root> [--no-enrich] [--validate] [--since <ref>] [--pr <num>]',
|
|
20
21
|
' e2e-ai-agents llm-health',
|
|
21
22
|
'',
|
|
22
23
|
'Options:',
|
|
23
24
|
' --config <path> Path to e2e-ai-agents.config.json (auto-discovered if present)',
|
|
24
|
-
' --path <
|
|
25
|
+
' --path <project-root> Path to the project root (scans both frontend and backend)',
|
|
25
26
|
' --profile <name> default | mattermost',
|
|
26
27
|
' --mattermost Shortcut for --profile mattermost',
|
|
27
28
|
' --tests-root <path> Path to tests root (optional)',
|
|
@@ -82,6 +83,11 @@ function printUsage() {
|
|
|
82
83
|
' --scenarios <path|json> Scenarios file/JSON for generate command',
|
|
83
84
|
' --apply Apply data-testid patches and generate tests',
|
|
84
85
|
' (legacy shortcut; prefer approve-and-generate)',
|
|
86
|
+
' --no-enrich Disable LLM enrichment (offline mode, train command)',
|
|
87
|
+
' --validate Validate manifest against git history (train command)',
|
|
88
|
+
' --pr <number> GitHub PR number for validation (requires gh CLI)',
|
|
89
|
+
' --yes, -y Non-interactive mode (train command)',
|
|
90
|
+
' --output <path> Output path for route-families.json (train command)',
|
|
85
91
|
' --help Show help',
|
|
86
92
|
].join('\n'));
|
|
87
93
|
}
|
package/dist/cli.js
CHANGED
|
@@ -16,6 +16,7 @@ const impact_js_1 = require("./cli/commands/impact.js");
|
|
|
16
16
|
const plan_js_1 = require("./cli/commands/plan.js");
|
|
17
17
|
const generate_js_1 = require("./cli/commands/generate.js");
|
|
18
18
|
const init_js_1 = require("./cli/commands/init.js");
|
|
19
|
+
const train_js_1 = require("./cli/commands/train.js");
|
|
19
20
|
async function main() {
|
|
20
21
|
const args = (0, parse_args_js_1.parseArgs)(process.argv.slice(2));
|
|
21
22
|
const autoConfig = (0, parse_args_js_1.resolveAutoConfig)(args);
|
|
@@ -24,6 +25,10 @@ async function main() {
|
|
|
24
25
|
await (0, init_js_1.runInitCommand)(hasYes);
|
|
25
26
|
return;
|
|
26
27
|
}
|
|
28
|
+
if (args.command === 'train') {
|
|
29
|
+
await (0, train_js_1.runTrainCommand)(args, autoConfig);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
27
32
|
if (args.help || !args.command) {
|
|
28
33
|
(0, usage_js_1.printUsage)();
|
|
29
34
|
process.exit(args.command ? 0 : 1);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { PolicyConfig } from '../agent/config.js';
|
|
2
2
|
import type { ImpactResult } from './impact_engine.js';
|
|
3
3
|
import type { AIEnrichmentResult } from './ai_enrichment.js';
|
|
4
|
+
import type { AdaptiveThresholds } from '../agent/feedback.js';
|
|
4
5
|
import type { PlanReport, GapDetail, CoveredFlowSummary } from '../agent/plan.js';
|
|
5
6
|
export type { PlanReport, GapDetail, CoveredFlowSummary };
|
|
6
|
-
export declare function buildPlanFromImpact(impact: ImpactResult, policyOverride?: Partial<PolicyConfig>, aiEnrichment?: AIEnrichmentResult): PlanReport;
|
|
7
|
+
export declare function buildPlanFromImpact(impact: ImpactResult, policyOverride?: Partial<PolicyConfig>, aiEnrichment?: AIEnrichmentResult, adaptiveThresholds?: AdaptiveThresholds): PlanReport;
|
|
7
8
|
export declare function writePlanReport(appRoot: string, plan: PlanReport): string;
|
|
8
9
|
export declare function renderCiSummaryMarkdown(plan: PlanReport): string;
|
|
9
10
|
export declare function writeCiSummary(appRoot: string, markdown: string, relativePath?: string): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plan_builder.d.ts","sourceRoot":"","sources":["../../src/engine/plan_builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"plan_builder.d.ts","sourceRoot":"","sources":["../../src/engine/plan_builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAErD,OAAO,KAAK,EAAC,YAAY,EAAkB,MAAM,oBAAoB,CAAC;AAEtE,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAC3D,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AAG7D,OAAO,KAAK,EACR,UAAU,EACV,SAAS,EACT,kBAAkB,EAIrB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAC,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAC,CAAC;AAoPxD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,YAAY,EACpB,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EACtC,YAAY,CAAC,EAAE,kBAAkB,EACjC,kBAAkB,CAAC,EAAE,kBAAkB,GACxC,UAAU,CAsJZ;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAMzE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAwHhE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,SAAiC,GAAG,MAAM,CAMvH"}
|
|
@@ -9,6 +9,7 @@ exports.writeCiSummary = writeCiSummary;
|
|
|
9
9
|
const fs_1 = require("fs");
|
|
10
10
|
const path_1 = require("path");
|
|
11
11
|
const minimatch_1 = require("minimatch");
|
|
12
|
+
const test_path_js_1 = require("../agent/test_path.js");
|
|
12
13
|
const impact_engine_js_1 = require("./impact_engine.js");
|
|
13
14
|
const DEFAULT_POLICY = {
|
|
14
15
|
minConfidenceForTargeted: 60,
|
|
@@ -196,22 +197,34 @@ function evaluateEnforcement(decision, policy) {
|
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
198
199
|
* Build recommended test list from impacted features' Playwright specs.
|
|
200
|
+
* When alwaysIncludeSubsystems is provided, specs from those subsystems are
|
|
201
|
+
* included regardless of their coverage status (blind-spot protection).
|
|
199
202
|
*/
|
|
200
|
-
function buildRecommendedTests(impact) {
|
|
201
|
-
const tests =
|
|
203
|
+
function buildRecommendedTests(impact, alwaysIncludeSubsystems = []) {
|
|
204
|
+
const tests = new Set();
|
|
205
|
+
const alwaysSet = new Set(alwaysIncludeSubsystems);
|
|
202
206
|
for (const feature of impact.impactedFeatures) {
|
|
203
|
-
|
|
207
|
+
const shouldInclude = feature.coverageStatus !== 'uncovered' ||
|
|
208
|
+
feature.playwrightSpecs.some((spec) => alwaysSet.has((0, test_path_js_1.inferSubsystemFromTestPath)(spec)));
|
|
209
|
+
if (shouldInclude) {
|
|
204
210
|
for (const spec of feature.playwrightSpecs) {
|
|
205
|
-
|
|
206
|
-
tests.push(spec);
|
|
207
|
-
}
|
|
211
|
+
tests.add(spec);
|
|
208
212
|
}
|
|
209
213
|
}
|
|
210
214
|
}
|
|
211
|
-
return tests;
|
|
215
|
+
return [...tests];
|
|
212
216
|
}
|
|
213
|
-
function buildPlanFromImpact(impact, policyOverride, aiEnrichment) {
|
|
217
|
+
function buildPlanFromImpact(impact, policyOverride, aiEnrichment, adaptiveThresholds) {
|
|
214
218
|
const policy = { ...DEFAULT_POLICY, ...(policyOverride || {}) };
|
|
219
|
+
// Apply adaptive calibration overrides (if available and not explicitly overridden)
|
|
220
|
+
if (adaptiveThresholds && policyOverride?.minConfidenceForTargeted === undefined) {
|
|
221
|
+
policy.minConfidenceForTargeted = adaptiveThresholds.minConfidenceForTargeted;
|
|
222
|
+
}
|
|
223
|
+
if (adaptiveThresholds && policyOverride?.safeMergeMinConfidence === undefined) {
|
|
224
|
+
policy.safeMergeMinConfidence = adaptiveThresholds.safeMergeMinConfidence;
|
|
225
|
+
}
|
|
226
|
+
// Apply alwaysIncludeSubsystems: force their tests into the recommended set
|
|
227
|
+
const alwaysIncludeSubsystems = adaptiveThresholds?.alwaysIncludeSubsystems ?? [];
|
|
215
228
|
const confidence = computeConfidence(impact);
|
|
216
229
|
const runSetResult = pickRunSet(impact, confidence, policy);
|
|
217
230
|
const decision = buildDecision(impact, runSetResult.runSet, confidence, policy);
|
|
@@ -294,7 +307,7 @@ function buildPlanFromImpact(impact, policyOverride, aiEnrichment) {
|
|
|
294
307
|
advisoryScenarios,
|
|
295
308
|
};
|
|
296
309
|
});
|
|
297
|
-
const recommendedTests = buildRecommendedTests(impact);
|
|
310
|
+
const recommendedTests = buildRecommendedTests(impact, alwaysIncludeSubsystems);
|
|
298
311
|
const requiredNewTests = gaps.map((f) => `${featureLabel(f)}: Add E2E tests`);
|
|
299
312
|
const p0 = impact.impactedFeatures.filter((f) => f.priority === 'P0').length;
|
|
300
313
|
const p1 = impact.impactedFeatures.filter((f) => f.priority === 'P1').length;
|
|
@@ -251,6 +251,67 @@ export function appendFeedbackAndRecompute(appRoot, input) {
|
|
|
251
251
|
export function readCalibration(appRoot) {
|
|
252
252
|
return readJson(join(appRoot, '.e2e-ai-agents', 'calibration.json'));
|
|
253
253
|
}
|
|
254
|
+
const DEFAULT_MIN_CONFIDENCE = 60;
|
|
255
|
+
const DEFAULT_SAFE_MERGE = 85;
|
|
256
|
+
const MIN_CONFIDENCE_FLOOR = 40;
|
|
257
|
+
const MIN_CONFIDENCE_CEILING = 80;
|
|
258
|
+
/**
|
|
259
|
+
* Compute adaptive thresholds based on calibration data.
|
|
260
|
+
* - If recent recall < 0.8: lower minConfidence (catch more escapes)
|
|
261
|
+
* - If recent precision > 0.9: raise minConfidence (fewer unnecessary tests)
|
|
262
|
+
* - Per-subsystem: if falseNegativeRate > 0.3 in 30d, always include tests
|
|
263
|
+
* Returns defaults if no calibration data exists.
|
|
264
|
+
*/
|
|
265
|
+
export function getAdaptiveThresholds(appRoot) {
|
|
266
|
+
const calibration = readCalibration(appRoot);
|
|
267
|
+
const reasons = [];
|
|
268
|
+
const alwaysInclude = [];
|
|
269
|
+
if (!calibration || calibration.samples === 0) {
|
|
270
|
+
return {
|
|
271
|
+
minConfidenceForTargeted: DEFAULT_MIN_CONFIDENCE,
|
|
272
|
+
safeMergeMinConfidence: DEFAULT_SAFE_MERGE,
|
|
273
|
+
alwaysIncludeSubsystems: [],
|
|
274
|
+
adjustmentReasons: ['No calibration data — using defaults'],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
let minConfidence = DEFAULT_MIN_CONFIDENCE;
|
|
278
|
+
let safeMerge = DEFAULT_SAFE_MERGE;
|
|
279
|
+
// Adjust based on 7-day recall
|
|
280
|
+
if (calibration.recent7d.samples >= 3) {
|
|
281
|
+
if (calibration.recent7d.recall < 0.8) {
|
|
282
|
+
const adjustment = 10;
|
|
283
|
+
minConfidence -= adjustment;
|
|
284
|
+
safeMerge -= adjustment;
|
|
285
|
+
reasons.push(`Lowering confidence threshold by ${adjustment} (7d recall: ${calibration.recent7d.recall.toFixed(2)})`);
|
|
286
|
+
}
|
|
287
|
+
else if (calibration.recent7d.precision > 0.9) {
|
|
288
|
+
const adjustment = 5;
|
|
289
|
+
minConfidence += adjustment;
|
|
290
|
+
safeMerge += adjustment;
|
|
291
|
+
reasons.push(`Raising confidence threshold by ${adjustment} (7d precision: ${calibration.recent7d.precision.toFixed(2)})`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Clamp to safe ranges
|
|
295
|
+
minConfidence = Math.max(MIN_CONFIDENCE_FLOOR, Math.min(MIN_CONFIDENCE_CEILING, minConfidence));
|
|
296
|
+
safeMerge = Math.max(70, Math.min(95, safeMerge));
|
|
297
|
+
// Per-subsystem blind spot detection (30-day window)
|
|
298
|
+
for (const [subsystem, metrics] of Object.entries(calibration.bySubsystem)) {
|
|
299
|
+
const recent = metrics.recent30d;
|
|
300
|
+
if (recent.samples >= 3 && recent.falseNegativeRate > 0.3) {
|
|
301
|
+
alwaysInclude.push(subsystem);
|
|
302
|
+
reasons.push(`Always including ${subsystem} tests (30d false-negative rate: ${recent.falseNegativeRate.toFixed(2)})`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (reasons.length === 0) {
|
|
306
|
+
reasons.push('Calibration data within normal range — using defaults');
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
minConfidenceForTargeted: minConfidence,
|
|
310
|
+
safeMergeMinConfidence: safeMerge,
|
|
311
|
+
alwaysIncludeSubsystems: alwaysInclude,
|
|
312
|
+
adjustmentReasons: reasons,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
254
315
|
export function readFlakyTests(appRoot) {
|
|
255
316
|
return readJson(join(appRoot, '.e2e-ai-agents', 'flaky-tests.json'));
|
|
256
317
|
}
|
|
@@ -22,12 +22,12 @@ export function summarizeCommandOutput(stdout, stderr) {
|
|
|
22
22
|
const lines = combined.split('\n').slice(-20);
|
|
23
23
|
return lines.join('\n').slice(0, 2000);
|
|
24
24
|
}
|
|
25
|
-
export function runCommand(command, args, cwd, timeoutMs = 60 * 60 * 1000) {
|
|
25
|
+
export function runCommand(command, args, cwd, timeoutMs = 60 * 60 * 1000, envOverride) {
|
|
26
26
|
// When spawning `claude`, unset CLAUDECODE so nested invocations are allowed.
|
|
27
27
|
// Claude Code sets this variable to block nested sessions; child processes
|
|
28
28
|
// that spawn their own claude instance must run without it.
|
|
29
|
-
let env;
|
|
30
|
-
if (command === 'claude') {
|
|
29
|
+
let env = envOverride;
|
|
30
|
+
if (!env && command === 'claude') {
|
|
31
31
|
const { CLAUDECODE: _, ...rest } = process.env;
|
|
32
32
|
env = rest;
|
|
33
33
|
}
|
package/dist/esm/api.js
CHANGED
|
@@ -5,6 +5,7 @@ import { appendPlanMetrics, } from './agent/plan.js';
|
|
|
5
5
|
import { analyzeImpact as analyzeImpactV2 } from './engine/impact_engine.js';
|
|
6
6
|
import { buildPlanFromImpact, renderCiSummaryMarkdown, writeCiSummary, writePlanReport, } from './engine/plan_builder.js';
|
|
7
7
|
import { getChangedFiles } from './agent/git.js';
|
|
8
|
+
import { getAdaptiveThresholds } from './agent/feedback.js';
|
|
8
9
|
import { loadDiffs } from './engine/diff_loader.js';
|
|
9
10
|
import { enrichImpactWithAI } from './engine/ai_enrichment.js';
|
|
10
11
|
import { AnthropicProvider } from './anthropic_provider.js';
|
|
@@ -52,7 +53,8 @@ export function recommendTestsDeterministic(options = {}) {
|
|
|
52
53
|
testsRoot: reportRoot,
|
|
53
54
|
routeFamilies: config.routeFamilies,
|
|
54
55
|
});
|
|
55
|
-
const
|
|
56
|
+
const adaptive = getAdaptiveThresholds(reportRoot);
|
|
57
|
+
const plan = buildPlanFromImpact(impact, config.policy, undefined, adaptive);
|
|
56
58
|
const planPath = writePlanReport(reportRoot, plan);
|
|
57
59
|
const ciSummaryMarkdown = renderCiSummaryMarkdown(plan);
|
|
58
60
|
const ciSummaryPath = writeCiSummary(reportRoot, ciSummaryMarkdown);
|
|
@@ -98,7 +100,8 @@ export async function recommendTestsAI(options = {}) {
|
|
|
98
100
|
specDetails: [...specDetailsMap.values()],
|
|
99
101
|
});
|
|
100
102
|
}
|
|
101
|
-
const
|
|
103
|
+
const adaptive = getAdaptiveThresholds(reportRoot);
|
|
104
|
+
const plan = buildPlanFromImpact(impact, config.policy, aiEnrichment, adaptive);
|
|
102
105
|
const planPath = writePlanReport(reportRoot, plan);
|
|
103
106
|
const ciSummaryMarkdown = renderCiSummaryMarkdown(plan);
|
|
104
107
|
const ciSummaryPath = writeCiSummary(reportRoot, ciSummaryMarkdown);
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { existsSync, mkdirSync, renameSync, writeFileSync } from 'fs';
|
|
4
|
+
import { dirname, join, resolve } from 'path';
|
|
5
|
+
import * as readline from 'readline';
|
|
6
|
+
import { resolveConfig } from '../../agent/config.js';
|
|
7
|
+
import { loadRouteFamilyManifest } from '../../knowledge/route_families.js';
|
|
8
|
+
import { LLMProviderFactory } from '../../provider_factory.js';
|
|
9
|
+
import { scanProject } from '../../training/scanner.js';
|
|
10
|
+
import { mergeFamilies, detectStaleFamilies } from '../../training/merger.js';
|
|
11
|
+
import { enrichFamilies } from '../../training/enricher.js';
|
|
12
|
+
import { getCommitFiles, validateCommit, buildValidationReport, formatValidationReport } from '../../training/validator.js';
|
|
13
|
+
class TrainError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'TrainError';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const MAX_BUDGET_USD = 10;
|
|
20
|
+
/**
|
|
21
|
+
* Resolves train-specific options from CLI args.
|
|
22
|
+
* Unlike other commands (analyze, plan, heal) that use the shared resolveConfig()
|
|
23
|
+
* for full pipeline configuration, train only needs appPath and testsRoot.
|
|
24
|
+
* We call resolveConfig() solely to extract testsRoot from the config file.
|
|
25
|
+
*/
|
|
26
|
+
function resolveTrainOptions(args, autoConfig) {
|
|
27
|
+
const appPath = args.path || '.';
|
|
28
|
+
let testsRoot = args.testsRoot || appPath;
|
|
29
|
+
// Try to resolve testsRoot from config
|
|
30
|
+
if (autoConfig) {
|
|
31
|
+
try {
|
|
32
|
+
const { config } = resolveConfig(process.cwd(), autoConfig, {
|
|
33
|
+
path: appPath,
|
|
34
|
+
testsRoot: args.testsRoot,
|
|
35
|
+
});
|
|
36
|
+
testsRoot = config.testsRoot || config.path || appPath;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// use defaults
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const outputPath = args.trainOutput ||
|
|
43
|
+
join(testsRoot, '.e2e-ai-agents', 'route-families.json');
|
|
44
|
+
// Validate --pr is a positive integer
|
|
45
|
+
if (args.trainPr !== undefined && (!Number.isInteger(args.trainPr) || args.trainPr <= 0)) {
|
|
46
|
+
throw new TrainError('--pr must be a positive integer');
|
|
47
|
+
}
|
|
48
|
+
// Validate --pr and --since are mutually exclusive
|
|
49
|
+
if (args.trainPr && args.gitSince) {
|
|
50
|
+
throw new TrainError('--pr and --since are mutually exclusive.');
|
|
51
|
+
}
|
|
52
|
+
// Validate --since format (reject leading '-' to prevent git flag injection)
|
|
53
|
+
const since = args.gitSince || 'HEAD~20';
|
|
54
|
+
if (/^-/.test(since) || !/^[a-zA-Z0-9_.~^@\/-]+$/.test(since)) {
|
|
55
|
+
throw new TrainError(`Invalid git ref: ${since}`);
|
|
56
|
+
}
|
|
57
|
+
// Validate budget bounds
|
|
58
|
+
const budget = args.budgetUSD || 0.50;
|
|
59
|
+
if (budget <= 0) {
|
|
60
|
+
throw new TrainError('--budget-usd must be a positive number');
|
|
61
|
+
}
|
|
62
|
+
if (budget > MAX_BUDGET_USD) {
|
|
63
|
+
throw new TrainError(`Budget exceeds maximum of $${MAX_BUDGET_USD}. Use a lower --budget-usd value.`);
|
|
64
|
+
}
|
|
65
|
+
const resolvedAppPath = resolve(appPath);
|
|
66
|
+
const resolvedTestsRoot = resolve(testsRoot);
|
|
67
|
+
const resolvedOutputPath = resolve(outputPath);
|
|
68
|
+
// Validate --path is a real project
|
|
69
|
+
if (!existsSync(resolvedAppPath)) {
|
|
70
|
+
throw new TrainError(`Project root not found: ${resolvedAppPath}`);
|
|
71
|
+
}
|
|
72
|
+
// Validate --output is within project boundary (append separator to prevent prefix attacks)
|
|
73
|
+
const inApp = resolvedOutputPath === resolvedAppPath || resolvedOutputPath.startsWith(resolvedAppPath + '/');
|
|
74
|
+
const inTests = resolvedOutputPath === resolvedTestsRoot || resolvedOutputPath.startsWith(resolvedTestsRoot + '/');
|
|
75
|
+
if (!inApp && !inTests) {
|
|
76
|
+
throw new TrainError(`Output path must be within the project root or tests root: ${resolvedOutputPath}`);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
appPath: resolvedAppPath,
|
|
80
|
+
testsRoot: resolvedTestsRoot,
|
|
81
|
+
enrich: args.trainEnrich !== false,
|
|
82
|
+
validate: args.trainValidate || false,
|
|
83
|
+
since,
|
|
84
|
+
pr: args.trainPr,
|
|
85
|
+
outputPath: resolvedOutputPath,
|
|
86
|
+
dryRun: args.dryRun || false,
|
|
87
|
+
yes: args.trainYes || false,
|
|
88
|
+
budgetUSD: budget,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function ask(rl, question, defaultValue) {
|
|
92
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
93
|
+
return new Promise((res) => {
|
|
94
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
95
|
+
res(answer.trim() || defaultValue || '');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function serializeManifest(manifest) {
|
|
100
|
+
const output = {
|
|
101
|
+
families: manifest.families.map((f) => {
|
|
102
|
+
// Remove undefined/empty optional fields for clean JSON
|
|
103
|
+
const cleaned = { ...f };
|
|
104
|
+
const optionalArrays = ['pageObjects', 'components', 'webappPaths', 'serverPaths', 'specDirs', 'cypressSpecDirs', 'tags', 'userFlows', 'features'];
|
|
105
|
+
for (const key of optionalArrays) {
|
|
106
|
+
if (!cleaned[key] || (Array.isArray(cleaned[key]) && cleaned[key].length === 0)) {
|
|
107
|
+
delete cleaned[key];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!cleaned.priority)
|
|
111
|
+
delete cleaned.priority;
|
|
112
|
+
return cleaned;
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
return JSON.stringify(output, null, 2) + '\n';
|
|
116
|
+
}
|
|
117
|
+
export async function runTrainCommand(args, autoConfig) {
|
|
118
|
+
const opts = resolveTrainOptions(args, autoConfig);
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(' e2e-ai-agents train');
|
|
121
|
+
console.log(' ===================');
|
|
122
|
+
console.log('');
|
|
123
|
+
// ---------- Phase 1: Deterministic scan ----------
|
|
124
|
+
console.log(' Scanning project structure...');
|
|
125
|
+
const scanResult = scanProject(opts.appPath);
|
|
126
|
+
console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
|
|
127
|
+
console.log(` Discovered ${scanResult.families.length} candidate families`);
|
|
128
|
+
if (scanResult.families.length === 0) {
|
|
129
|
+
console.log('');
|
|
130
|
+
console.log(' No families discovered. Make sure your project has recognizable');
|
|
131
|
+
console.log(' source directories (src/, server/, app/) and test directories');
|
|
132
|
+
console.log(' (tests/, e2e/, specs/) with matching names.');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// ---------- Phase 2: Merge with existing ----------
|
|
136
|
+
const existing = loadRouteFamilyManifest(opts.testsRoot);
|
|
137
|
+
if (existing) {
|
|
138
|
+
console.log(` Found existing manifest with ${existing.families.length} families`);
|
|
139
|
+
}
|
|
140
|
+
let mergeResult = mergeFamilies(existing, scanResult.families);
|
|
141
|
+
console.log(` Merge: ${mergeResult.summary}`);
|
|
142
|
+
// ---------- Phase 3: Stale detection ----------
|
|
143
|
+
if (mergeResult.manifest.families.length > 0) {
|
|
144
|
+
const stale = detectStaleFamilies(mergeResult.manifest, opts.appPath);
|
|
145
|
+
if (stale.length > 0) {
|
|
146
|
+
console.log('');
|
|
147
|
+
console.log(` Stale families detected (${stale.length}):`);
|
|
148
|
+
for (const id of stale) {
|
|
149
|
+
console.log(` ${id} — paths no longer exist`);
|
|
150
|
+
}
|
|
151
|
+
if (!opts.yes && !opts.dryRun && process.stdin.isTTY) {
|
|
152
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
153
|
+
try {
|
|
154
|
+
const answer = await ask(rl, ' Remove stale families? [y/N]', 'N');
|
|
155
|
+
if (answer.toLowerCase() === 'y') {
|
|
156
|
+
const staleSet = new Set(stale);
|
|
157
|
+
mergeResult.manifest.families = mergeResult.manifest.families.filter((f) => !staleSet.has(f.id));
|
|
158
|
+
mergeResult.staleFamilies = stale;
|
|
159
|
+
console.log(` Removed ${stale.length} stale families`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
rl.close();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ---------- Phase 4: LLM Enrichment ----------
|
|
169
|
+
if (opts.enrich) {
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(' Enriching with LLM...');
|
|
172
|
+
try {
|
|
173
|
+
const provider = await LLMProviderFactory.createFromEnv();
|
|
174
|
+
const enrichResult = await enrichFamilies(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD);
|
|
175
|
+
mergeResult.manifest.families = enrichResult.enrichedFamilies;
|
|
176
|
+
console.log(` Enriched ${enrichResult.enrichedFamilies.length} families (${enrichResult.tokensUsed} tokens, ~$${enrichResult.costUSD})`);
|
|
177
|
+
if (enrichResult.skippedFamilies.length > 0) {
|
|
178
|
+
console.log(` Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
console.warn(` LLM enrichment failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
183
|
+
console.warn(' Continuing with deterministic results. Use --no-enrich to skip LLM.');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// ---------- Phase 5: Write manifest ----------
|
|
187
|
+
console.log('');
|
|
188
|
+
const json = serializeManifest(mergeResult.manifest);
|
|
189
|
+
if (opts.dryRun) {
|
|
190
|
+
console.log(' Dry run — proposed manifest:');
|
|
191
|
+
console.log('');
|
|
192
|
+
console.log(json);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const dir = dirname(opts.outputPath);
|
|
196
|
+
if (!existsSync(dir)) {
|
|
197
|
+
mkdirSync(dir, { recursive: true });
|
|
198
|
+
}
|
|
199
|
+
const tmpPath = `${opts.outputPath}.tmp`;
|
|
200
|
+
writeFileSync(tmpPath, json, 'utf-8');
|
|
201
|
+
renameSync(tmpPath, opts.outputPath);
|
|
202
|
+
console.log(` Wrote ${opts.outputPath}`);
|
|
203
|
+
console.log(` ${mergeResult.manifest.families.length} families`);
|
|
204
|
+
}
|
|
205
|
+
// ---------- Phase 6: Report unmatched ----------
|
|
206
|
+
if (scanResult.unmatchedSourceDirs.length > 0 || scanResult.unmatchedTestDirs.length > 0) {
|
|
207
|
+
console.log('');
|
|
208
|
+
console.log(' Unmatched (review manually):');
|
|
209
|
+
for (const dir of scanResult.unmatchedSourceDirs.slice(0, 10)) {
|
|
210
|
+
console.log(` source: ${dir.relativePath}`);
|
|
211
|
+
}
|
|
212
|
+
for (const dir of scanResult.unmatchedTestDirs.slice(0, 10)) {
|
|
213
|
+
console.log(` test: ${dir.relativePath}`);
|
|
214
|
+
}
|
|
215
|
+
if (scanResult.unmatchedSourceDirs.length + scanResult.unmatchedTestDirs.length > 20) {
|
|
216
|
+
console.log(' ... and more');
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// ---------- Phase 7: Validation (optional) ----------
|
|
220
|
+
if (opts.validate) {
|
|
221
|
+
if (opts.pr) {
|
|
222
|
+
console.log('');
|
|
223
|
+
console.log(` Validating against PR #${opts.pr}...`);
|
|
224
|
+
// Check for gh CLI
|
|
225
|
+
const { execFileSync } = await import('child_process');
|
|
226
|
+
try {
|
|
227
|
+
execFileSync('gh', ['--version'], { stdio: 'pipe' });
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
throw new TrainError('--pr requires the GitHub CLI (gh). Install: https://cli.github.com/');
|
|
231
|
+
}
|
|
232
|
+
// Fetch PR changed files via gh CLI
|
|
233
|
+
let prFiles;
|
|
234
|
+
try {
|
|
235
|
+
const output = execFileSync('gh', ['pr', 'view', String(opts.pr), '--json', 'files', '-q', '.files[].path'], {
|
|
236
|
+
cwd: opts.appPath,
|
|
237
|
+
encoding: 'utf-8',
|
|
238
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
239
|
+
});
|
|
240
|
+
prFiles = output.trim().split('\n').filter(Boolean);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
throw new TrainError(`Error fetching PR #${opts.pr}: ${error instanceof Error ? error.message : String(error)}`);
|
|
244
|
+
}
|
|
245
|
+
if (prFiles.length === 0) {
|
|
246
|
+
console.log(' No files found in PR.');
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const validation = validateCommit(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`);
|
|
250
|
+
const report = buildValidationReport([validation], mergeResult.manifest);
|
|
251
|
+
console.log('');
|
|
252
|
+
console.log(formatValidationReport(report));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
console.log('');
|
|
257
|
+
console.log(` Validating against git history (${opts.since})...`);
|
|
258
|
+
const commits = getCommitFiles(opts.appPath, opts.since);
|
|
259
|
+
if (commits.length === 0) {
|
|
260
|
+
console.log(' No commits found in range.');
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
const validations = commits.map((c) => validateCommit(mergeResult.manifest, c.files, c.hash, c.message));
|
|
264
|
+
const report = buildValidationReport(validations, mergeResult.manifest);
|
|
265
|
+
console.log('');
|
|
266
|
+
console.log(formatValidationReport(report));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
console.log('');
|
|
271
|
+
}
|
|
@@ -61,6 +61,10 @@ const FLAGS = {
|
|
|
61
61
|
'--generate': { key: 'analyzeGenerate', type: 'boolean' },
|
|
62
62
|
'--heal': { key: 'analyzeHeal', type: 'boolean' },
|
|
63
63
|
'--no-ai': { key: 'noAi', type: 'boolean' },
|
|
64
|
+
'--enrich': { key: 'trainEnrich', type: 'boolean' },
|
|
65
|
+
'--no-enrich': { key: 'trainEnrich', type: 'boolean-false' },
|
|
66
|
+
'--validate': { key: 'trainValidate', type: 'boolean' },
|
|
67
|
+
'--yes': { key: 'trainYes', type: 'boolean', aliases: ['-y'] },
|
|
64
68
|
'--mattermost': { key: 'profile', type: 'boolean', transform: () => 'mattermost' },
|
|
65
69
|
// -- string flags --
|
|
66
70
|
'--config': { key: 'configPath', type: 'string' },
|
|
@@ -90,6 +94,7 @@ const FLAGS = {
|
|
|
90
94
|
'--generate-output': { key: 'analyzeGenerateOutputDir', type: 'string' },
|
|
91
95
|
'--heal-report': { key: 'analyzeHealReport', type: 'string' },
|
|
92
96
|
'--flow-catalog': { key: 'flowCatalogPath', type: 'string' },
|
|
97
|
+
'--output': { key: 'trainOutput', type: 'string' },
|
|
93
98
|
// -- number flags (with isFinite guard) --
|
|
94
99
|
'--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
|
|
95
100
|
'--time': { key: 'timeLimitMinutes', type: 'number' },
|
|
@@ -101,6 +106,7 @@ const FLAGS = {
|
|
|
101
106
|
'--traceability-min-hits': { key: 'traceabilityMinHits', type: 'number' },
|
|
102
107
|
'--traceability-max-files-per-test': { key: 'traceabilityMaxFilesPerTest', type: 'number' },
|
|
103
108
|
'--traceability-max-age-days': { key: 'traceabilityMaxAgeDays', type: 'number' },
|
|
109
|
+
'--pr': { key: 'trainPr', type: 'number' },
|
|
104
110
|
// -- number-raw flags (no isFinite guard, assigned directly via Number()) --
|
|
105
111
|
'--max-attempts': { key: 'maxAttempts', type: 'number-raw', transform: (v) => parseInt(v, 10) },
|
|
106
112
|
'--pipeline-mcp-timeout-ms': { key: 'pipelineMcpTimeoutMs', type: 'number-raw' },
|
|
@@ -134,7 +140,7 @@ const COMMANDS = new Set([
|
|
|
134
140
|
'init', 'impact', 'plan', 'heal', 'suggest', 'generate',
|
|
135
141
|
'finalize-generated-tests', 'feedback',
|
|
136
142
|
'traceability-capture', 'traceability-ingest',
|
|
137
|
-
'analyze', 'llm-health',
|
|
143
|
+
'analyze', 'llm-health', 'train',
|
|
138
144
|
]);
|
|
139
145
|
// ---------------------------------------------------------------------------
|
|
140
146
|
// Parser
|
package/dist/esm/cli/usage.js
CHANGED
|
@@ -14,11 +14,12 @@ export function printUsage() {
|
|
|
14
14
|
' e2e-ai-agents traceability-ingest --path <app-root> --traceability-input <json>',
|
|
15
15
|
' e2e-ai-agents generate [--scenarios <path|json>] [--max-attempts <n>] [--dry-run]',
|
|
16
16
|
' e2e-ai-agents analyze --path <app-root> [--tests-root <path>] [--since <ref>] [--generate] [--generate-output <dir>] [--heal] [--heal-report <json>]',
|
|
17
|
+
' e2e-ai-agents train --path <project-root> [--no-enrich] [--validate] [--since <ref>] [--pr <num>]',
|
|
17
18
|
' e2e-ai-agents llm-health',
|
|
18
19
|
'',
|
|
19
20
|
'Options:',
|
|
20
21
|
' --config <path> Path to e2e-ai-agents.config.json (auto-discovered if present)',
|
|
21
|
-
' --path <
|
|
22
|
+
' --path <project-root> Path to the project root (scans both frontend and backend)',
|
|
22
23
|
' --profile <name> default | mattermost',
|
|
23
24
|
' --mattermost Shortcut for --profile mattermost',
|
|
24
25
|
' --tests-root <path> Path to tests root (optional)',
|
|
@@ -79,6 +80,11 @@ export function printUsage() {
|
|
|
79
80
|
' --scenarios <path|json> Scenarios file/JSON for generate command',
|
|
80
81
|
' --apply Apply data-testid patches and generate tests',
|
|
81
82
|
' (legacy shortcut; prefer approve-and-generate)',
|
|
83
|
+
' --no-enrich Disable LLM enrichment (offline mode, train command)',
|
|
84
|
+
' --validate Validate manifest against git history (train command)',
|
|
85
|
+
' --pr <number> GitHub PR number for validation (requires gh CLI)',
|
|
86
|
+
' --yes, -y Non-interactive mode (train command)',
|
|
87
|
+
' --output <path> Output path for route-families.json (train command)',
|
|
82
88
|
' --help Show help',
|
|
83
89
|
].join('\n'));
|
|
84
90
|
}
|
package/dist/esm/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ import { runImpactCommand } from './cli/commands/impact.js';
|
|
|
14
14
|
import { runPlanCommand } from './cli/commands/plan.js';
|
|
15
15
|
import { runGenerateCommand } from './cli/commands/generate.js';
|
|
16
16
|
import { runInitCommand } from './cli/commands/init.js';
|
|
17
|
+
import { runTrainCommand } from './cli/commands/train.js';
|
|
17
18
|
async function main() {
|
|
18
19
|
const args = parseArgs(process.argv.slice(2));
|
|
19
20
|
const autoConfig = resolveAutoConfig(args);
|
|
@@ -22,6 +23,10 @@ async function main() {
|
|
|
22
23
|
await runInitCommand(hasYes);
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
26
|
+
if (args.command === 'train') {
|
|
27
|
+
await runTrainCommand(args, autoConfig);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
25
30
|
if (args.help || !args.command) {
|
|
26
31
|
printUsage();
|
|
27
32
|
process.exit(args.command ? 0 : 1);
|