@agentbridge1/cli 0.0.10 → 0.0.11

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.
@@ -6,15 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.parseProofOutput = parseProofOutput;
8
8
  exports.proofEvidenceString = proofEvidenceString;
9
- const DOMAIN_TEST_KEYWORDS = {
10
- auth: ["auth", "login", "logout", "session", "token", "password", "oauth", "signin", "signup"],
11
- database: ["db", "database", "prisma", "migration", "schema", "query", "model"],
12
- api: ["api", "route", "endpoint", "handler", "request", "response"],
13
- payments: ["payment", "billing", "stripe", "invoice", "webhook", "subscription"],
14
- mcp: ["mcp", "setup", "rules", "config", "agentbridge"],
15
- ui: ["component", "render", "view", "style", "page", "layout"],
16
- tests: ["test", "spec"],
17
- };
9
+ const domain_keywords_1 = require("./domain-keywords");
18
10
  // Vitest / Jest summary: "12 passed", "Tests: 5 passed", "5 failed"
19
11
  const PASSED_RE = /(\d+)\s+passed/i;
20
12
  const FAILED_RE = /(\d+)\s+failed/i;
@@ -41,24 +33,30 @@ function detectRunner(stdout) {
41
33
  }
42
34
  function extractDomainKeywords(testNames) {
43
35
  const found = new Set();
44
- const lower = testNames.join(" ").toLowerCase();
45
- for (const [domain, keywords] of Object.entries(DOMAIN_TEST_KEYWORDS)) {
46
- if (keywords.some((kw) => lower.includes(kw)))
36
+ const text = testNames.join(" ");
37
+ for (const domain of Object.keys(domain_keywords_1.DOMAIN_KEYWORDS)) {
38
+ if ((0, domain_keywords_1.matchesDomainKeywords)(text, domain))
47
39
  found.add(domain);
48
40
  }
49
41
  return [...found];
50
42
  }
43
+ function normalizeCoverageDomain(domain) {
44
+ const lower = domain.trim().toLowerCase();
45
+ if (lower in domain_keywords_1.DOMAIN_KEYWORDS)
46
+ return lower;
47
+ if (lower === "db")
48
+ return "database";
49
+ if (lower === "payment")
50
+ return "payments";
51
+ if (lower === "test")
52
+ return "tests";
53
+ return null;
54
+ }
51
55
  function hasDomainCoverage(domain, domainKeywordsInPassed) {
52
- const aliases = {
53
- auth: ["auth"],
54
- database: ["database"],
55
- api: ["api"],
56
- payments: ["payments"],
57
- mcp: ["mcp"],
58
- ui: ["ui"],
59
- };
60
- const check = aliases[domain] ?? [domain];
61
- return check.some((alias) => domainKeywordsInPassed.includes(alias));
56
+ const canonical = normalizeCoverageDomain(domain);
57
+ if (!canonical)
58
+ return false;
59
+ return domainKeywordsInPassed.includes(canonical);
62
60
  }
63
61
  function parseProofOutput(proofRun) {
64
62
  const stdout = proofRun.stdoutExcerpt ?? "";
@@ -106,12 +104,13 @@ function parseProofOutput(proofRun) {
106
104
  function proofEvidenceString(parsed, domain) {
107
105
  const total = parsed.passed + parsed.failed + parsed.skipped;
108
106
  const base = `Proof run ${parsed.failed > 0 ? "failed" : "passed"} (${parsed.passed}/${total} tests passed).`;
109
- if (domain && parsed.passed > 0) {
110
- if (parsed.has_domain_coverage(domain)) {
107
+ const canonicalDomain = domain ? normalizeCoverageDomain(domain) : null;
108
+ if (canonicalDomain && parsed.passed > 0) {
109
+ if (parsed.has_domain_coverage(canonicalDomain)) {
111
110
  const covering = parsed.domain_keywords_in_passed.join(", ");
112
111
  return `${base} Tests covering: ${covering}.`;
113
112
  }
114
- const keywords = DOMAIN_TEST_KEYWORDS[domain]?.slice(0, 3).join(", ") ?? domain;
113
+ const keywords = domain_keywords_1.DOMAIN_KEYWORDS[canonicalDomain]?.slice(0, 3).join(", ") ?? canonicalDomain;
115
114
  return `${base} No passed tests mention ${keywords} — domain coverage unconfirmed.`;
116
115
  }
117
116
  return base;
@@ -10,6 +10,96 @@ exports.analyzeCoherence = analyzeCoherence;
10
10
  const node_fs_1 = require("node:fs");
11
11
  const node_path_1 = require("node:path");
12
12
  const node_child_process_1 = require("node:child_process");
13
+ function detectRunnerFromCommand(command) {
14
+ const lower = command.toLowerCase();
15
+ if (/\bvitest\b/.test(lower))
16
+ return "vitest";
17
+ if (/\bjest\b/.test(lower))
18
+ return "jest";
19
+ if (/\bpytest\b/.test(lower))
20
+ return "pytest";
21
+ return "unknown";
22
+ }
23
+ function quoteArg(value) {
24
+ return `"${value.replaceAll(`"`, `\\"`)}"`;
25
+ }
26
+ function classifyFailure(stdout, stderr, timedOut) {
27
+ if (timedOut)
28
+ return "timeout";
29
+ const combined = `${stdout}\n${stderr}`.toLowerCase();
30
+ if (combined.includes("unknown option") ||
31
+ combined.includes("unknown argument") ||
32
+ combined.includes("unrecognized option") ||
33
+ combined.includes("cacerror")) {
34
+ return "command_invalid";
35
+ }
36
+ if (combined.includes("command not found") ||
37
+ combined.includes("enoent") ||
38
+ combined.includes("cannot find module") ||
39
+ combined.includes("is not recognized as an internal or external command")) {
40
+ return "tooling_missing";
41
+ }
42
+ if (extractFailedTestNames(`${stdout}\n${stderr}`).length > 0) {
43
+ return "test_failures";
44
+ }
45
+ if (combined.includes("failed to load config") ||
46
+ combined.includes("syntaxerror") ||
47
+ combined.includes("typeerror") ||
48
+ combined.includes("referenceerror")) {
49
+ return "runtime_error";
50
+ }
51
+ return "test_failures";
52
+ }
53
+ async function runSingleCommand(command, options) {
54
+ const started = Date.now();
55
+ return new Promise((resolveResult) => {
56
+ const child = (0, node_child_process_1.spawn)(command, {
57
+ shell: true,
58
+ cwd: options.cwd,
59
+ env: process.env,
60
+ stdio: ["ignore", "pipe", "pipe"],
61
+ });
62
+ let stdout = "";
63
+ let stderr = "";
64
+ let didTimeout = false;
65
+ child.stdout?.on("data", (chunk) => {
66
+ stdout += chunk.toString();
67
+ });
68
+ child.stderr?.on("data", (chunk) => {
69
+ stderr += chunk.toString();
70
+ });
71
+ const timer = setTimeout(() => {
72
+ didTimeout = true;
73
+ child.kill("SIGTERM");
74
+ const durationMs = Date.now() - started;
75
+ resolveResult({
76
+ command,
77
+ passed: false,
78
+ stdout,
79
+ stderr: `${stderr}\nTimed out after ${options.timeoutMs}ms`,
80
+ durationMs,
81
+ timedOut: true,
82
+ failureType: "timeout",
83
+ });
84
+ }, options.timeoutMs);
85
+ child.on("close", (code) => {
86
+ clearTimeout(timer);
87
+ if (didTimeout)
88
+ return; // already resolved
89
+ const durationMs = Date.now() - started;
90
+ const passed = code === 0;
91
+ resolveResult({
92
+ command,
93
+ passed,
94
+ stdout,
95
+ stderr,
96
+ durationMs,
97
+ timedOut: false,
98
+ failureType: passed ? undefined : classifyFailure(stdout, stderr, false),
99
+ });
100
+ });
101
+ });
102
+ }
13
103
  /**
14
104
  * Detect the appropriate test command for the project rooted at `cwd`.
15
105
  * Checks config override first, then language-specific conventions.
@@ -75,16 +165,25 @@ function detectTestCommand(cwd = process.cwd()) {
75
165
  function buildScopedCommand(base, relatedFiles) {
76
166
  if (!relatedFiles.length)
77
167
  return base;
78
- // vitest / jest both support --testPathPattern (regex)
79
- if (/vitest|jest/.test(base)) {
168
+ const runner = detectRunnerFromCommand(base);
169
+ // Jest supports --testPathPattern (regex)
170
+ if (runner === "jest") {
80
171
  // Build a pattern that matches the test files corresponding to the changed source files.
81
172
  // e.g. "src/foo.ts" → look for "src/foo" anywhere in test paths.
82
173
  const stems = relatedFiles.map((f) => f.replace(/\.[^/.]+$/, "").replace(/\//g, "\\/"));
83
174
  const pattern = stems.join("|");
84
175
  return `${base} --testPathPattern="${pattern}"`;
85
176
  }
177
+ // Vitest supports positional filters; avoid jest-only --testPathPattern.
178
+ if (runner === "vitest") {
179
+ const stems = relatedFiles.map((f) => f.replace(/\.[^/.]+$/, "").replaceAll("\\", "/"));
180
+ const unique = [...new Set(stems)].filter(Boolean).slice(0, 10);
181
+ if (!unique.length)
182
+ return base;
183
+ return `${base} ${unique.map(quoteArg).join(" ")}`;
184
+ }
86
185
  // pytest supports positional file/dir args (best effort)
87
- if (base.startsWith("pytest")) {
186
+ if (runner === "pytest") {
88
187
  const pyFiles = relatedFiles
89
188
  .map((f) => f.replace(/\.ts$/, ".py"))
90
189
  .filter((f) => (0, node_fs_1.existsSync)(f));
@@ -125,52 +224,41 @@ async function runDetectedTests(optionsOrTimeout = {}) {
125
224
  const baseCommand = detectTestCommand(cwd);
126
225
  if (!baseCommand)
127
226
  return null;
128
- const command = options.relatedFiles?.length
227
+ const scopedCommand = options.relatedFiles?.length
129
228
  ? buildScopedCommand(baseCommand, options.relatedFiles)
130
229
  : baseCommand;
131
- const started = Date.now();
132
- return new Promise((resolveResult) => {
133
- const child = (0, node_child_process_1.spawn)(command, {
134
- shell: true,
135
- cwd,
136
- env: process.env,
137
- stdio: ["ignore", "pipe", "pipe"],
138
- });
139
- let stdout = "";
140
- let stderr = "";
141
- let didTimeout = false;
142
- child.stdout?.on("data", (chunk) => {
143
- stdout += chunk.toString();
230
+ const scopedResult = await runSingleCommand(scopedCommand, { cwd, timeoutMs });
231
+ const attempts = [
232
+ {
233
+ command: scopedResult.command,
234
+ passed: scopedResult.passed,
235
+ durationMs: scopedResult.durationMs,
236
+ timedOut: Boolean(scopedResult.timedOut),
237
+ failureType: scopedResult.failureType,
238
+ },
239
+ ];
240
+ if (scopedCommand !== baseCommand &&
241
+ !scopedResult.passed &&
242
+ (scopedResult.failureType === "command_invalid" || scopedResult.failureType === "tooling_missing")) {
243
+ const fallbackResult = await runSingleCommand(baseCommand, { cwd, timeoutMs });
244
+ attempts.push({
245
+ command: fallbackResult.command,
246
+ passed: fallbackResult.passed,
247
+ durationMs: fallbackResult.durationMs,
248
+ timedOut: Boolean(fallbackResult.timedOut),
249
+ failureType: fallbackResult.failureType,
144
250
  });
145
- child.stderr?.on("data", (chunk) => {
146
- stderr += chunk.toString();
147
- });
148
- const timer = setTimeout(() => {
149
- didTimeout = true;
150
- child.kill("SIGTERM");
151
- resolveResult({
152
- command,
153
- passed: false,
154
- stdout,
155
- stderr: `${stderr}\nTimed out after ${timeoutMs}ms`,
156
- durationMs: Date.now() - started,
157
- timedOut: true,
158
- });
159
- }, timeoutMs);
160
- child.on("close", (code) => {
161
- clearTimeout(timer);
162
- if (didTimeout)
163
- return; // already resolved
164
- resolveResult({
165
- command,
166
- passed: code === 0,
167
- stdout,
168
- stderr,
169
- durationMs: Date.now() - started,
170
- timedOut: false,
171
- });
172
- });
173
- });
251
+ return {
252
+ ...fallbackResult,
253
+ attempts,
254
+ fallbackUsed: true,
255
+ };
256
+ }
257
+ return {
258
+ ...scopedResult,
259
+ attempts,
260
+ fallbackUsed: false,
261
+ };
174
262
  }
175
263
  /** Common English stop words to strip from intent before keyword extraction. */
176
264
  const STOP_WORDS = new Set([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentbridge1/cli",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "CLI for AgentBridge — structured AI agent work sessions with domain authority and approval gates",
5
5
  "type": "commonjs",
6
6
  "bin": {