baro-ai 0.118.4 → 0.118.7

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/bin/baro CHANGED
@@ -3,9 +3,21 @@
3
3
  // shebang, and a #!/bin/sh bin makes the Windows shim invoke sh.exe (absent on
4
4
  // Windows). A node shebang yields correct cmd/ps1/sh shims on every platform.
5
5
  import { spawnSync } from "node:child_process"
6
- import { existsSync } from "node:fs"
6
+ import { existsSync, readFileSync } from "node:fs"
7
7
  import { homedir } from "node:os"
8
8
  import path from "node:path"
9
+ import { fileURLToPath } from "node:url"
10
+
11
+ import { selfUpdate } from "./self-update.mjs"
12
+
13
+ const argv = process.argv.slice(2)
14
+ try {
15
+ const packageJson = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json")
16
+ const packageVersion = JSON.parse(readFileSync(packageJson, "utf8")).version
17
+ await selfUpdate({ packageVersion, argv })
18
+ } catch {
19
+ // An update that cannot be attempted never stops baro from starting.
20
+ }
9
21
 
10
22
  const bin = path.join(
11
23
  homedir(),
@@ -19,7 +31,7 @@ if (!existsSync(bin)) {
19
31
  process.exit(1)
20
32
  }
21
33
 
22
- const result = spawnSync(bin, process.argv.slice(2), { stdio: "inherit" })
34
+ const result = spawnSync(bin, argv, { stdio: "inherit" })
23
35
  if (result.error) {
24
36
  console.error(`baro: failed to launch binary: ${result.error.message}`)
25
37
  process.exit(1)
@@ -0,0 +1,122 @@
1
+ // A hand-started baro brings itself up to date before it runs. The runner
2
+ // already did this for services; a person running `baro` stayed on whatever
3
+ // they had installed until they noticed the banner.
4
+ import { spawnSync } from "node:child_process"
5
+ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs"
6
+ import { homedir } from "node:os"
7
+ import path from "node:path"
8
+
9
+ const CACHE_TTL_MS = 3 * 3600_000
10
+ const FETCH_TIMEOUT_MS = 2500
11
+ const QUICK_COMMANDS = new Set(["--version", "-V", "--help", "-h", "connect"])
12
+
13
+ export function semverLt(a, b) {
14
+ const parts = (v) => String(v).split(".").map((x) => Number.parseInt(x, 10) || 0)
15
+ const [pa, pb] = [parts(a), parts(b)]
16
+ for (let i = 0; i < 3; i++) {
17
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) < (pb[i] ?? 0)
18
+ }
19
+ return false
20
+ }
21
+
22
+ /**
23
+ * Why this invocation must not update, or null when it may. Automation keeps
24
+ * the version it was given; the runner updates itself between runs; and the
25
+ * staged bundles are shared, so replacing them under a live run breaks it.
26
+ */
27
+ export function skipReason({ argv, env, isTTY, liveRuns }) {
28
+ if (env.BARO_NO_SELF_UPDATE === "1") return "BARO_NO_SELF_UPDATE=1"
29
+ if (env.CI) return "CI"
30
+ if (!isTTY) return "not a terminal"
31
+ if (argv.includes("--headless")) return "headless"
32
+ if (argv.some((arg) => QUICK_COMMANDS.has(arg))) return "quick command"
33
+ if (liveRuns > 0) return "another baro run is active"
34
+ return null
35
+ }
36
+
37
+ function baroHome() {
38
+ return path.join(homedir(), ".baro")
39
+ }
40
+
41
+ function readJson(file) {
42
+ try {
43
+ return JSON.parse(readFileSync(file, "utf8"))
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+
49
+ /** The version the binary and its bundles are actually staged at. */
50
+ export function installedVersion(packageVersion) {
51
+ return readJson(path.join(baroHome(), "bin", "bundle-version.json"))?.version ?? packageVersion
52
+ }
53
+
54
+ export function liveRunCount() {
55
+ try {
56
+ const dir = path.join(baroHome(), "live")
57
+ return readdirSync(dir).filter((name) => {
58
+ const pid = readJson(path.join(dir, name))?.pid
59
+ if (!Number.isInteger(pid)) return false
60
+ try {
61
+ process.kill(pid, 0)
62
+ return true
63
+ } catch (error) {
64
+ return error.code === "EPERM"
65
+ }
66
+ }).length
67
+ } catch {
68
+ return 0
69
+ }
70
+ }
71
+
72
+ export async function latestVersion(now = Date.now()) {
73
+ const cachePath = path.join(baroHome(), "update-check.json")
74
+ const cached = readJson(cachePath)
75
+ if (cached?.latest && cached.checkedAt && now - cached.checkedAt < CACHE_TTL_MS) {
76
+ return cached.latest
77
+ }
78
+ try {
79
+ const response = await fetch("https://registry.npmjs.org/baro-ai/latest", {
80
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
81
+ })
82
+ const latest = (await response.json())?.version
83
+ if (typeof latest !== "string") return null
84
+ try {
85
+ mkdirSync(baroHome(), { recursive: true })
86
+ writeFileSync(cachePath, JSON.stringify({ latest, checkedAt: now }))
87
+ } catch {
88
+ // A read-only home only costs the next start another lookup.
89
+ }
90
+ return latest
91
+ } catch {
92
+ return null
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Update when a newer baro is published. Never throws and never blocks a run
98
+ * for long: offline, a failed install or a root-owned global prefix all fall
99
+ * through to the version already installed.
100
+ */
101
+ export async function selfUpdate({ packageVersion, argv, env = process.env, isTTY = process.stdin.isTTY }) {
102
+ const reason = skipReason({ argv, env, isTTY: Boolean(isTTY), liveRuns: 0 })
103
+ if (reason) return { updated: false, reason }
104
+ const current = installedVersion(packageVersion)
105
+ const latest = await latestVersion()
106
+ if (!latest || !semverLt(current, latest)) return { updated: false, reason: "up to date" }
107
+ if (liveRunCount() > 0) {
108
+ console.error(`baro ${latest} is available; not updating while another baro run is active.`)
109
+ return { updated: false, reason: "another baro run is active" }
110
+ }
111
+ console.error(`Updating baro ${current} → ${latest}…`)
112
+ const result = spawnSync("npm", ["install", "-g", `baro-ai@${latest}`], {
113
+ stdio: ["ignore", "ignore", "inherit"],
114
+ shell: process.platform === "win32",
115
+ })
116
+ if (result.status !== 0 || installedVersion(packageVersion) !== latest) {
117
+ console.error(`Could not update baro automatically. Update with: npm i -g baro-ai@latest`)
118
+ return { updated: false, reason: "install failed" }
119
+ }
120
+ console.error(`baro ${latest} installed.`)
121
+ return { updated: true, reason: null }
122
+ }
package/dist/cli.mjs CHANGED
@@ -44719,6 +44719,18 @@ function javaRuns(env) {
44719
44719
  return false;
44720
44720
  }
44721
44721
  }
44722
+ function resolveJvmBuildTool(cwd, kind, probe2 = {}) {
44723
+ const env = probe2.env ?? process.env;
44724
+ const windows = (probe2.platform ?? process.platform) === "win32";
44725
+ if (!(probe2.javaRuns ?? javaRuns)(env)) return null;
44726
+ return kind === "maven" ? wrapperOrTool(cwd, windows ? "mvnw.cmd" : "mvnw", "mvn", env, windows) : wrapperOrTool(cwd, windows ? "gradlew.bat" : "gradlew", "gradle", env, windows);
44727
+ }
44728
+ function isMavenRepository(cwd) {
44729
+ return MAVEN_FILES.some((name) => existsSync4(join7(cwd, name)));
44730
+ }
44731
+ function isGradleRepository(cwd) {
44732
+ return GRADLE_FILES.some((name) => existsSync4(join7(cwd, name)));
44733
+ }
44722
44734
  function wrapperOrTool(cwd, wrapper, tool, env, windows) {
44723
44735
  if (isFile(join7(cwd, wrapper))) return windows ? `.\\${wrapper}` : `./${wrapper}`;
44724
44736
  return onPath(tool, env, windows) ? tool : null;
@@ -45011,6 +45023,10 @@ function dispatchDeclared(cwd, requirement, parsed, packageManagers, insideDdev)
45011
45023
  args: ["diff", "--no-ext-diff", "--no-textconv", "--check"]
45012
45024
  };
45013
45025
  }
45026
+ if (tool === "mvn" || tool === "./mvnw") return translateMaven(cwd, requirement, parsed);
45027
+ if (tool === "gradle" || tool === "./gradlew") {
45028
+ return translateGradle(cwd, requirement, parsed);
45029
+ }
45014
45030
  if (tool === "composer") return translateComposer(cwd, requirement, parsed);
45015
45031
  if (tool === "vendor/bin/phpunit") {
45016
45032
  return translatePhpunit(cwd, requirement, parsed);
@@ -45026,7 +45042,7 @@ function dispatchDeclared(cwd, requirement, parsed, packageManagers, insideDdev)
45026
45042
  }
45027
45043
  return incomplete(
45028
45044
  requirement,
45029
- "unsupported declared test; allowed tools are npm/pnpm/yarn, exact npx rstest run paths, cargo, node, git diff --check, composer, vendor/bin/phpunit, and ddev exec"
45045
+ "unsupported declared test; allowed tools are npm/pnpm/yarn, exact npx rstest run paths, cargo, node, mvn, gradle, git diff --check, composer, vendor/bin/phpunit, and ddev exec"
45030
45046
  );
45031
45047
  }
45032
45048
  var CD_PREFIX = /^cd\s+([^\s&]+)\s*&&\s*([^]*)$/;
@@ -45639,6 +45655,95 @@ function translateCargo(cwd, requirement, parsed) {
45639
45655
  args
45640
45656
  };
45641
45657
  }
45658
+ var MAVEN_GOALS = /* @__PURE__ */ new Set(["test", "verify", "package", "compile", "test-compile"]);
45659
+ var MAVEN_FLAGS = /* @__PURE__ */ new Set([
45660
+ "-B",
45661
+ "--batch-mode",
45662
+ "-ntp",
45663
+ "--no-transfer-progress",
45664
+ "-q",
45665
+ "--quiet",
45666
+ "-o",
45667
+ "--offline",
45668
+ "-e",
45669
+ "-am",
45670
+ "--also-make"
45671
+ ]);
45672
+ var MAVEN_PROPERTY = /^-D(?:test|failIfNoTests|surefire\.failIfNoSpecifiedTests|skipITs)=[A-Za-z0-9_.,*#+!$-]+$/;
45673
+ var JVM_MODULE = /^[A-Za-z0-9_./:-]+$/;
45674
+ function translateMaven(cwd, requirement, parsed) {
45675
+ if (!isMavenRepository(cwd)) {
45676
+ return incomplete(requirement, "declared mvn test requires pom.xml at the repository root");
45677
+ }
45678
+ const args = parsed.tokens.slice(1);
45679
+ let goals = 0;
45680
+ for (let index = 0; index < args.length; index++) {
45681
+ const token = args[index];
45682
+ if (MAVEN_GOALS.has(token)) {
45683
+ goals++;
45684
+ } else if (token === "-pl" || token === "--projects") {
45685
+ const module = args[++index];
45686
+ if (!module || !JVM_MODULE.test(module) || hasParentTraversal(module)) {
45687
+ return incomplete(requirement, "mvn -pl needs a module path inside the repository");
45688
+ }
45689
+ } else if (!MAVEN_FLAGS.has(token) && !MAVEN_PROPERTY.test(token)) {
45690
+ return incomplete(
45691
+ requirement,
45692
+ `mvn declarations are limited to test, verify, package, compile and test-compile with batch/offline flags, -pl and -Dtest; unsupported '${token}'`
45693
+ );
45694
+ }
45695
+ }
45696
+ if (goals !== 1) {
45697
+ return incomplete(requirement, "mvn declaration must name exactly one goal");
45698
+ }
45699
+ const tool = resolveJvmBuildTool(cwd, "maven");
45700
+ if (!tool) {
45701
+ return incomplete(requirement, "this host has no working JDK and Maven (or ./mvnw) to run the declared test");
45702
+ }
45703
+ return { label: ["mvn", ...args].join(" "), tool, args };
45704
+ }
45705
+ var GRADLE_TASK = /^(?::[A-Za-z0-9_-]+)*:?(?:test|check|build|assemble)$/;
45706
+ var GRADLE_FLAGS = /* @__PURE__ */ new Set([
45707
+ "--console=plain",
45708
+ "--offline",
45709
+ "--no-daemon",
45710
+ "-q",
45711
+ "--quiet",
45712
+ "--stacktrace",
45713
+ "--continue"
45714
+ ]);
45715
+ var GRADLE_TEST_FILTER = /^[A-Za-z0-9_.*$]+$/;
45716
+ function translateGradle(cwd, requirement, parsed) {
45717
+ if (!isGradleRepository(cwd)) {
45718
+ return incomplete(requirement, "declared gradle test requires a Gradle build at the repository root");
45719
+ }
45720
+ const args = parsed.tokens.slice(1);
45721
+ let tasks = 0;
45722
+ for (let index = 0; index < args.length; index++) {
45723
+ const token = args[index];
45724
+ if (GRADLE_TASK.test(token)) {
45725
+ tasks++;
45726
+ } else if (token === "--tests") {
45727
+ const filter = args[++index];
45728
+ if (!filter || !GRADLE_TEST_FILTER.test(filter)) {
45729
+ return incomplete(requirement, "gradle --tests needs a class or method filter");
45730
+ }
45731
+ } else if (!GRADLE_FLAGS.has(token)) {
45732
+ return incomplete(
45733
+ requirement,
45734
+ `gradle declarations are limited to test, check, build and assemble tasks with --tests; unsupported '${token}'`
45735
+ );
45736
+ }
45737
+ }
45738
+ if (tasks === 0) {
45739
+ return incomplete(requirement, "gradle declaration must name a test, check, build or assemble task");
45740
+ }
45741
+ const tool = resolveJvmBuildTool(cwd, "gradle");
45742
+ if (!tool) {
45743
+ return incomplete(requirement, "this host has no working JDK and Gradle (or ./gradlew) to run the declared test");
45744
+ }
45745
+ return { label: ["gradle", ...args].join(" "), tool, args };
45746
+ }
45642
45747
  function containedPath(cwd, candidate, requireFile) {
45643
45748
  if (candidate === "" || candidate.startsWith("-") || candidate.startsWith("@") || isAbsolute3(candidate) || /^[A-Za-z]:\//.test(candidate) || hasParentTraversal(candidate)) return { reason: `unsafe or escaping path '${candidate}'` };
45644
45749
  const root = resolve5(cwd);
@@ -70765,8 +70870,13 @@ var Finalizer = class extends BaseObserver {
70765
70870
  hostRepoRoot: this.opts.hostRepoRoot
70766
70871
  });
70767
70872
  }
70768
- const verificationIncomplete = !verify.ran || verify.commands.some((command) => command.status === "skipped");
70769
- if (!verify.ok) {
70873
+ const hygieneOnly = verify.ok && provesOnlyHygiene(
70874
+ verify.commands.filter((command) => command.command !== VERDICT_COMMAND)
70875
+ );
70876
+ const verificationIncomplete = !hygieneOnly && (!verify.ran || verify.commands.some((command) => command.status === "skipped"));
70877
+ if (hygieneOnly) {
70878
+ this.log("[finalizer] not test-verified: only git diff --check ran on the merged result");
70879
+ } else if (!verify.ok) {
70770
70880
  this.log(`[finalizer] \u26A0 verification failed: ${verify.failures[0]?.cmd ?? "build/test"}`);
70771
70881
  } else if (!verify.ran) {
70772
70882
  this.log("[finalizer] nothing to verify (no build/test)");
@@ -70783,6 +70893,7 @@ var Finalizer = class extends BaseObserver {
70783
70893
  prd,
70784
70894
  run,
70785
70895
  checkpoint,
70896
+ hygieneOnly,
70786
70897
  verify,
70787
70898
  orderedStories,
70788
70899
  passed,
@@ -71075,7 +71186,14 @@ ${BARO_COAUTHOR_TRAILER}`,
71075
71186
  const skippedVerification = args.verify.commands.filter(
71076
71187
  (command) => command.status === "skipped"
71077
71188
  );
71078
- if (!args.verify.ran || skippedVerification.length > 0) {
71189
+ if (args.hygieneOnly) {
71190
+ lines.push("## Not test-verified");
71191
+ lines.push("");
71192
+ lines.push(
71193
+ "The only check run on the merged branch was `git diff --check`. No build or test command verified it; run the project's tests before merging."
71194
+ );
71195
+ lines.push("");
71196
+ } else if (!args.verify.ran || skippedVerification.length > 0) {
71079
71197
  lines.push("## \u26A0 Build/test verification incomplete");
71080
71198
  lines.push("");
71081
71199
  lines.push(
@@ -71207,11 +71325,12 @@ function bindAuthority2(current, authority, label) {
71207
71325
  }
71208
71326
  return authority;
71209
71327
  }
71328
+ var VERDICT_COMMAND = "objective verification verdict";
71210
71329
  function verificationResult(evidence) {
71211
71330
  const commands = evidence.commands.map((command) => ({ ...command }));
71212
71331
  if (evidence.status === "skipped" && !commands.some((command) => command.status === "skipped")) {
71213
71332
  commands.push({
71214
- command: "objective verification verdict",
71333
+ command: VERDICT_COMMAND,
71215
71334
  status: "skipped",
71216
71335
  durationMs: 0,
71217
71336
  tail: "verifier reported an incomplete/skipped objective gate"