nearly-cli 0.1.2 → 0.1.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.
package/README.md CHANGED
@@ -24,7 +24,27 @@ Watch the first thirty seconds. The cover says what the diff cannot: an action t
24
24
 
25
25
  Node 18 or newer, Claude Code signed in, and git. No dependencies and no API key: agents run on your existing Claude subscription.
26
26
 
27
- Gating, recording and the record itself work on macOS, Linux and Windows. **Spoken narration is macOS only**, because it uses the built-in `say`. Elsewhere the record is built the same way and reads from its captions, or you can supply your own recordings with `--voice-dir`.
27
+ ### Platforms
28
+
29
+ The suite runs on every push against macOS, Linux and Windows, on Node 18 and 22.
30
+
31
+ | | macOS | Linux | Windows |
32
+ |---|---|---|---|
33
+ | Gate, recording, records | tested in CI | tested in CI | tested in CI |
34
+ | Turning it on and off | tested in CI | tested in CI | tested in CI |
35
+ | Updating itself | tested in CI | tested in CI | tested in CI |
36
+ | Driving a real agent end to end | **run by hand** | not yet | not yet |
37
+ | Handing the record over at `git push` | **run by hand** | not yet | not yet |
38
+ | Spoken narration | yes | no | no |
39
+
40
+ Narration uses the built-in `say`, so it is macOS only. Everywhere else the
41
+ record is built identically and reads from its captions, or you can record the
42
+ lines yourself with `--voice-dir`.
43
+
44
+ The bottom two rows are the honest gap: CI proves the pieces work on all three,
45
+ but a whole session with a real agent, and a real push through the hook, have
46
+ only been done on macOS. If you are the first to try either on Windows or Linux,
47
+ an issue with what broke would be genuinely useful.
28
48
 
29
49
  ## Use it on your own repo
30
50
 
@@ -34,6 +54,12 @@ One command, in the repo you want recorded.
34
54
  npx nearly-cli
35
55
  ```
36
56
 
57
+ > **`npx` not recognised?** It comes with Node.js, so that error means Node is
58
+ > not installed. Get it from [nodejs.org](https://nodejs.org) or, on Windows,
59
+ > `winget install OpenJS.NodeJS.LTS`. Then **open a new terminal** so it picks up
60
+ > the change, and check with `node --version`. You also need Claude Code signed
61
+ > in: Nearly gates Claude Code sessions and does nothing without one.
62
+
37
63
  ```
38
64
  ✓ Nearly is on for my-app
39
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nearly-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "A pull request tells you what changed. Nearly tells you what nearly happened: the commands a human refused, the pushes policy blocked, the turns rolled back.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -34,6 +34,6 @@
34
34
  },
35
35
  "homepage": "https://anujpatel06.github.io/nearly/",
36
36
  "scripts": {
37
- "test": "node --test --test-concurrency=1 test/"
37
+ "test": "node --test --test-concurrency=1 test/policy.test.mjs test/server.test.mjs test/record.test.mjs test/resilience.test.mjs"
38
38
  }
39
39
  }
@@ -16,7 +16,7 @@
16
16
  // it, and if it cannot start, Claude Code falls back to its own prompts and
17
17
  // nothing breaks.
18
18
 
19
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
19
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, realpathSync } from 'node:fs';
20
20
  import { join, resolve, dirname, basename } from 'node:path';
21
21
  import { fileURLToPath } from 'node:url';
22
22
  import { execFileSync, spawnSync } from 'node:child_process';
@@ -40,15 +40,31 @@ function pkgVersion() {
40
40
  catch { return 'latest'; }
41
41
  }
42
42
 
43
- // Only trust a `nearly` on PATH if it really is this tool.
43
+ // Only trust a `nearly` on PATH if it really is this tool and it will still be
44
+ // there tomorrow.
45
+ //
46
+ // npx puts its own temporary bin first on PATH for the life of the process, so
47
+ // during `npx nearly-cli` a naive lookup finds a `nearly` that ceases to exist
48
+ // the moment npx exits. Believing it meant writing hooks that call a command
49
+ // nobody has, which fail on every tool call, which means no gate at all.
44
50
  function onPath() {
51
+ const args = process.platform === 'win32' ? ['nearly'] : ['-a', 'nearly'];
52
+ let found = [];
45
53
  try {
46
- const p = execFileSync(process.platform === 'win32' ? 'where' : 'which', ['nearly'],
47
- { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split('\n')[0];
48
- if (!p) return null;
49
- const out = execFileSync(p, ['--which'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
50
- return out ? p : null;
54
+ found = execFileSync(process.platform === 'win32' ? 'where' : 'which', args,
55
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split('\n').filter(Boolean);
51
56
  } catch { return null; }
57
+
58
+ for (const p of found) {
59
+ let resolved = p;
60
+ try { resolved = realpathSync(p); } catch { /* keep the literal path */ }
61
+ if (/[\\/]_npx[\\/]/.test(p) || /[\\/]_npx[\\/]/.test(resolved)) continue; // vanishes when npx exits
62
+ try {
63
+ const out = execFileSync(p, ['--which'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
64
+ if (out) return p;
65
+ } catch { /* not our command; try the next one */ }
66
+ }
67
+ return null;
52
68
  }
53
69
 
54
70
  const fromPackage = /[\\/]node_modules[\\/]/.test(root) || /[\\/]_npx[\\/]/.test(root);
@@ -62,7 +78,7 @@ const viaNpx = /[\\/]_npx[\\/]/.test(root);
62
78
  function installGlobally() {
63
79
  if (!viaNpx || onPath() || argv.includes('--no-install') || process.env.NEARLY_NO_INSTALL === '1') return false;
64
80
  process.stdout.write(dim(` Installing nearly so upgrades reach you… `));
65
- const r = spawnSync('npm', ['install', '-g', `nearly-cli@${pkgVersion()}`, '--silent', '--no-fund', '--no-audit'],
81
+ const r = spawnSync(NPM, ['install', '-g', `nearly-cli@${pkgVersion()}`, '--silent', '--no-fund', '--no-audit'],
66
82
  { encoding: 'utf8', timeout: 180_000 });
67
83
  if (r.status === 0 && onPath()) { console.log('done'); return true; }
68
84
  console.log(dim('skipped'));
@@ -72,11 +88,13 @@ function installGlobally() {
72
88
  }
73
89
  let installed = onPath();
74
90
  const hookCmd = (ev) => installed
75
- ? `nearly hook ${ev}`
91
+ ? `nearly hook ${ev}` // verified above to survive this process
76
92
  : fromPackage
77
93
  ? `npx -y nearly-cli@${pkgVersion()} hook ${ev}`
78
94
  : `node ${JSON.stringify(HOOK)} ${ev}`;
79
- const updateNote = installed
95
+ // A function, not a value: the install below changes the answer, and computing
96
+ // it early told a fresh install it was pinned when it was not.
97
+ const updateNote = () => installed
80
98
  ? 'upgrades reach this repo automatically'
81
99
  : fromPackage
82
100
  ? `pinned to v${pkgVersion()} — run nearly again here after upgrading`
@@ -89,6 +107,9 @@ const nameIdx = argv.indexOf('--name');
89
107
  const name = (nameIdx !== -1 ? argv[nameIdx + 1] : basename(repo))
90
108
  .replace(/[^a-z0-9-]/gi, '-').toLowerCase().slice(0, 24) || 'repo';
91
109
 
110
+ // npm is a .cmd shim on Windows and Node will not run one through spawn unless
111
+ // it is named exactly. Without this, installing and upgrading both fail there.
112
+ const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
92
113
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
93
114
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
94
115
  const ok = (s) => `\x1b[32m${s}\x1b[0m`;
@@ -201,7 +222,7 @@ if (off) {
201
222
  console.log(`${ok('✓')} ${bold('Nearly is on')} for ${bold(name)} ${dim(repo)}`);
202
223
  console.log('');
203
224
  console.log(` ${ok('·')} every Claude Code session here is gated and recorded`);
204
- console.log(` ${ok('·')} ${dim(updateNote)}`);
225
+ console.log(` ${ok('·')} ${dim(updateNote())}`);
205
226
  console.log(` ${ok('·')} ${push.status === 0 ? 'the record is offered when you push' : dim('pre-push hook skipped: ' + (push.stderr || '').trim().split('\n')[0])}`);
206
227
  if (base) {
207
228
  console.log(` ${ok('·')} records publish to ${base}`);
@@ -24,6 +24,9 @@ import { spawnSync, execFileSync } from 'node:child_process';
24
24
  const root = join(dirname(fileURLToPath(import.meta.url)), '..');
25
25
  const DAY = 24 * 60 * 60 * 1000;
26
26
 
27
+ // npm is a .cmd shim on Windows and Node will not run one through spawn unless
28
+ // it is named exactly. Without this, installing and upgrading both fail there.
29
+ const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm';
27
30
  const dim = (s) => `\x1b[2m${s}\x1b[0m`;
28
31
  const bold = (s) => `\x1b[1m${s}\x1b[0m`;
29
32
 
@@ -100,7 +103,7 @@ export function applyUpdate(u) {
100
103
  }
101
104
 
102
105
  process.stdout.write(dim(` Updating Nearly ${u.from} → ${u.to}… `));
103
- const r = spawnSync('npm', ['install', '-g', `${u.name}@${u.to}`, '--silent', '--no-fund', '--no-audit'],
106
+ const r = spawnSync(NPM, ['install', '-g', `${u.name}@${u.to}`, '--silent', '--no-fund', '--no-audit'],
104
107
  { encoding: 'utf8', timeout: 120_000 });
105
108
 
106
109
  if (r.status === 0) {