agrep-cli 0.4.1 → 0.4.5

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.
Files changed (3) hide show
  1. package/bin.js +33 -13
  2. package/package.json +1 -1
  3. package/postinstall.js +87 -32
package/bin.js CHANGED
@@ -13,13 +13,38 @@ const { version } = require("./package.json");
13
13
  const SPEC = process.env.AGREP_PYPI_SPEC || `agrep==${version}`;
14
14
  const args = process.argv.slice(2);
15
15
 
16
- function has(cmd) {
17
- const probe = spawnSync(cmd, ["--version"], { stdio: "ignore", shell: false });
18
- return probe.status === 0;
16
+ // uv/pipx run agrep out of a cache that a prune wipes while this package
17
+ // stays installed; the sentinel tracks this file instead (see teach.install_anchor).
18
+ const ANCHOR_ENV = "AGREP_INSTALL_ANCHOR";
19
+
20
+ function probe(cmd) {
21
+ const r = spawnSync(cmd, ["--version"], { encoding: "utf8", shell: false });
22
+ return !r.error && r.status === 0 ? String(r.stdout || "") : null;
23
+ }
24
+
25
+ function uvVersion(text) {
26
+ const m = /\buv\s+(\d+)\.(\d+)\.(\d+)/.exec(text || "");
27
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
28
+ }
29
+
30
+ // `--exclude-newer-package agrep=false` opts agrep out of a user's global
31
+ // exclude-newer pin; only uv >= 0.9.25 parses the `=false` form, older uv
32
+ // rejects the whole invocation, so the flag rides along only where it parses.
33
+ function excludeNewerOptOut(v) {
34
+ return v !== null && (v[0] > 0 || v[1] > 9 || (v[1] === 9 && v[2] >= 25));
35
+ }
36
+
37
+ function uvArgs(uvVersionText, rest) {
38
+ const flag = excludeNewerOptOut(uvVersion(uvVersionText))
39
+ ? ["--exclude-newer-package", "agrep=false"] : [];
40
+ return ["tool", "run", ...flag, "--from", SPEC, "agrep", ...rest];
19
41
  }
20
42
 
21
43
  function run(cmd, cmdArgs) {
22
- const r = spawnSync(cmd, cmdArgs, { stdio: "inherit", shell: false });
44
+ const r = spawnSync(cmd, cmdArgs, {
45
+ stdio: "inherit", shell: false,
46
+ env: { ...process.env, [ANCHOR_ENV]: __filename },
47
+ });
23
48
  if (r.error) {
24
49
  console.error(`agrep: failed to launch ${cmd}: ${r.error.message}`);
25
50
  process.exit(1);
@@ -27,16 +52,11 @@ function run(cmd, cmdArgs) {
27
52
  process.exit(r.status === null ? 1 : r.status);
28
53
  }
29
54
 
30
- if (has("uv")) {
55
+ const uv = probe("uv");
56
+ if (uv !== null) {
31
57
  // `uvx agrep` == `uv tool run agrep`; --from lets AGREP_PYPI_SPEC point anywhere
32
- run("uv", [
33
- "tool", "run",
34
- "--exclude-newer-package", "agrep=false",
35
- "--from", SPEC,
36
- "agrep",
37
- ...args,
38
- ]);
39
- } else if (has("pipx")) {
58
+ run("uv", uvArgs(uv, args));
59
+ } else if (probe("pipx") !== null) {
40
60
  run("pipx", ["run", "--spec", SPEC, "agrep", ...args]);
41
61
  } else {
42
62
  console.error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agrep-cli",
3
- "version": "0.4.1",
3
+ "version": "0.4.5",
4
4
  "description": "unscoped npm alias for @mundy/agrep, local search across AI coding-agent history",
5
5
  "bin": {
6
6
  "agrep": "bin.js"
package/postinstall.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- // Global installs finish enrolled, not merely downloaded: after warming uv's
3
- // ephemeral tool cache, run the pinned `agrep setup --yes` so the box is
4
- // indexed and the detected agents are taught the moment `npm install -g`
2
+ // Global installs finish enrolled, not merely downloaded: after warming the
3
+ // runner's ephemeral tool cache, run the pinned `agrep setup --yes` so the box
4
+ // is indexed and the detected agents are taught the moment `npm install -g`
5
5
  // returns. Everything setup writes is disclosed in its output, owned by
6
6
  // agrep, and undone by `agrep remove`; --no-semantic defers the ~52MiB model
7
7
  // fetch to the first semantic use, and --no-archive leaves source-store
@@ -13,6 +13,7 @@
13
13
 
14
14
  "use strict";
15
15
 
16
+ const path = require("path");
16
17
  const { spawnSync } = require("child_process");
17
18
  const { version } = require("./package.json");
18
19
 
@@ -22,6 +23,12 @@ const forced = Boolean(process.env.AGREP_POSTINSTALL_FORCE);
22
23
  const strict = Boolean(process.env.AGREP_POSTINSTALL_STRICT);
23
24
  const autoSetup = !process.env.AGREP_NO_AUTO_SETUP;
24
25
  const SETUP_TIMEOUT_MS = 15 * 60 * 1000;
26
+ // `agrep setup` exit 3 (docs/OUTPUT_CONTRACTS.md): declined without changing
27
+ // anything; the tool is provisioned, so this is not a failed install
28
+ const SETUP_DECLINED = 3;
29
+ // the sentinel must track this package, not the runner's prunable cache copy
30
+ const ANCHOR_ENV = "AGREP_INSTALL_ANCHOR";
31
+ const anchor = path.join(__dirname, "bin.js");
25
32
 
26
33
  if (process.env.AGREP_SKIP_POSTINSTALL) {
27
34
  process.exit(0);
@@ -31,48 +38,96 @@ if (!npmGlobal && !forced) {
31
38
  process.exit(0);
32
39
  }
33
40
 
34
- function has(cmd) {
35
- const probe = spawnSync(cmd, ["--version"], { stdio: "ignore", shell: false });
36
- return probe.status === 0;
41
+ function probe(cmd) {
42
+ const r = spawnSync(cmd, ["--version"], { encoding: "utf8", shell: false });
43
+ return !r.error && r.status === 0 ? String(r.stdout || "") : null;
44
+ }
45
+
46
+ function uvVersion(text) {
47
+ const m = /\buv\s+(\d+)\.(\d+)\.(\d+)/.exec(text || "");
48
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
49
+ }
50
+
51
+ // same rule as bin.js: the `=false` opt-out parses only on uv >= 0.9.25
52
+ function excludeNewerOptOut(v) {
53
+ return v !== null && (v[0] > 0 || v[1] > 9 || (v[1] === 9 && v[2] >= 25));
37
54
  }
38
55
 
39
56
  function tryRun(cmd, args, options) {
40
- const result = spawnSync(cmd, args, { shell: false, ...options });
41
- return !result.error && result.status === 0;
57
+ const result = spawnSync(cmd, args, {
58
+ shell: false, ...options, env: { ...process.env, [ANCHOR_ENV]: anchor },
59
+ });
60
+ return result.error ? null : result.status;
42
61
  }
43
62
 
44
- function agrepArgs(...rest) {
45
- return [
46
- "tool", "run",
47
- "--exclude-newer-package", "agrep=false",
48
- "--from", spec,
49
- "agrep", ...rest,
50
- ];
63
+ // Resolve and cache the pinned agrep without running it for real; a failure here
64
+ // is the runner's (network, registry, an unsupported python), not setup's.
65
+ function provision(runner) {
66
+ const result = spawnSync(runner.cmd, runner.args("--version"), {
67
+ shell: false, encoding: "utf8", stdio: ["ignore", "ignore", "pipe"],
68
+ env: { ...process.env, [ANCHOR_ENV]: anchor },
69
+ });
70
+ if (result.error) return { ok: false, detail: result.error.message };
71
+ const lines = String(result.stderr || "").trim().split("\n").filter(Boolean);
72
+ return { ok: result.status === 0, detail: lines.length ? lines[lines.length - 1] : `exit ${result.status}` };
73
+ }
74
+
75
+ // bin.js's runner choice, in the same order: uv, else pipx
76
+ function findRunner() {
77
+ const uv = probe("uv");
78
+ if (uv !== null) {
79
+ const flag = excludeNewerOptOut(uvVersion(uv))
80
+ ? ["--exclude-newer-package", "agrep=false"] : [];
81
+ return {
82
+ cmd: "uv",
83
+ args: (...rest) => ["tool", "run", ...flag, "--from", spec, "agrep", ...rest],
84
+ };
85
+ }
86
+ if (probe("pipx") !== null) {
87
+ return { cmd: "pipx", args: (...rest) => ["run", "--spec", spec, "agrep", ...rest] };
88
+ }
89
+ return null;
51
90
  }
52
91
 
53
92
  let done = false;
54
- const uvAvailable = has("uv");
55
- if (uvAvailable && autoSetup) {
56
- // Inherited stdio: npm surfaces this with --foreground-scripts (and on
57
- // failure), so every file setup touches is disclosed, never silent.
58
- console.log(
59
- "agrep: running one-time setup (agrep setup --yes) - writes agent instruction blocks"
60
- + " and an uninstall-cleanup sentinel, all undone by `agrep remove`;"
61
- + " AGREP_NO_AUTO_SETUP=1 skips this.");
62
- done = tryRun(
63
- "uv", agrepArgs("setup", "--yes", "--no-semantic", "--no-archive"),
64
- { stdio: "inherit", timeout: SETUP_TIMEOUT_MS });
65
- if (!done) {
66
- console.warn("agrep: automatic setup did not finish; `agrep setup` completes it any time.");
93
+ const runner = findRunner();
94
+ if (runner && autoSetup) {
95
+ const fetched = provision(runner);
96
+ if (!fetched.ok) {
97
+ console.warn(
98
+ `agrep: ${runner.cmd} could not fetch ${spec} (${fetched.detail}); nothing was set up.`
99
+ + " once the registry is reachable, `agrep setup` runs the one-time setup.");
100
+ } else {
101
+ // Inherited stdio: npm surfaces this with --foreground-scripts (and on
102
+ // failure), so every file setup touches is disclosed, never silent.
103
+ console.log(
104
+ "agrep: running one-time setup (agrep setup --yes) - writes agent instruction blocks"
105
+ + " and an uninstall-cleanup sentinel, all undone by `agrep remove`;"
106
+ + " AGREP_NO_AUTO_SETUP=1 skips this.");
107
+ const status = tryRun(
108
+ runner.cmd, runner.args("setup", "--yes", "--no-semantic", "--no-archive"),
109
+ { stdio: "inherit", timeout: SETUP_TIMEOUT_MS });
110
+ done = status === 0 || status === SETUP_DECLINED;
111
+ if (status === null) {
112
+ console.warn(`agrep: could not launch ${runner.cmd} for setup; \`agrep setup\` runs it any time.`);
113
+ } else if (status === SETUP_DECLINED) {
114
+ console.warn(
115
+ "agrep: setup declined and changed nothing; its output above names the cause"
116
+ + " and the fix. `agrep setup` declines the same way until that is resolved.");
117
+ } else if (!done) {
118
+ console.warn(
119
+ `agrep: setup exited ${status}; its output above names each step that did not complete`
120
+ + " and the fix. the rest of setup still ran; `agrep setup` retries any time.");
121
+ }
67
122
  }
68
- } else if (uvAvailable) {
123
+ } else if (runner) {
69
124
  // Warm-only: keep npm output clean; the shim provisions on first run.
70
- done = tryRun("uv", agrepArgs("--version"), { stdio: "ignore" });
125
+ done = tryRun(runner.cmd, runner.args("--version"), { stdio: "ignore" }) === 0;
71
126
  if (!done) {
72
- console.warn("agrep: uv cache warm failed; the first agrep run will retry");
127
+ console.warn(`agrep: ${runner.cmd} cache warm failed; the first agrep run will retry`);
73
128
  }
74
129
  } else {
75
- console.warn("agrep: install uv (https://docs.astral.sh/uv) - agrep needs it on first run.");
130
+ console.warn("agrep: install uv (https://docs.astral.sh/uv) or pipx - agrep needs one of them on first run.");
76
131
  }
77
132
  if (!done && strict) {
78
133
  console.error("agrep: postinstall failed to provision the pinned PyPI tool");