@einsia/agent-git 0.2.11 → 0.2.12

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/CHANGELOG.md CHANGED
@@ -8,6 +8,15 @@ its [GitHub Release](https://github.com/Einsia/agent-git/releases), and the
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.2.12] - 2026-09-28
12
+
13
+ ### Fixed
14
+
15
+ - Retry RC daemon reconciliation when a global npm installation's lifecycle
16
+ scripts are disabled. The next CLI invocation schedules the guarded handoff
17
+ in the background, while project-local and temporary npm packages cannot
18
+ replace a persistent daemon.
19
+
11
20
  ## [0.2.11] - 2026-09-28
12
21
 
13
22
  ### Fixed
package/npm/README.md CHANGED
@@ -65,6 +65,9 @@ Unsupported operating system and architecture pairs get no prebuilt binary: runn
65
65
  recipe.
66
66
  - `agit setup` failing in postinstall never blocks the install — the CLI is on
67
67
  disk; re-run `agit setup` any time.
68
+ - If npm suppresses postinstall, the next invocation of a globally installed
69
+ `agit` schedules a guarded RC daemon reconciliation in the background.
70
+ npm cannot run the daemon handoff at install time when it suppresses scripts.
68
71
 
69
72
  ## Environment variables
70
73
 
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ const { spawnSync } = require('child_process');
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+
7
+ const [bin, pkgRoot, launcher] = process.argv.slice(2);
8
+ if (!bin || !pkgRoot) process.exit(0);
9
+
10
+ // A transient npx or project-local binary cannot own a persistent daemon.
11
+ // npm's global root or its verified bin symlink identifies a durable install.
12
+ const root = spawnSync('npm', ['root', '-g'], { encoding: 'utf8', timeout: 5000, windowsHide: true });
13
+ let global = false;
14
+ try {
15
+ const installed = fs.realpathSync(pkgRoot);
16
+ const same = other => process.platform === 'win32'
17
+ ? installed.toLowerCase() === other.toLowerCase()
18
+ : installed === other;
19
+ if (!root.error && root.status === 0) {
20
+ try {
21
+ global = same(fs.realpathSync(path.join(root.stdout.trim(), '@einsia', 'agent-git', 'npm')));
22
+ } catch { /* A missing current npm root may leave a still-valid installed launcher. */ }
23
+ }
24
+ if (!global && launcher && fs.lstatSync(launcher).isSymbolicLink()
25
+ && path.basename(path.dirname(launcher)) === 'bin'
26
+ && same(fs.realpathSync(path.join(path.dirname(launcher), '..', 'lib', 'node_modules', '@einsia', 'agent-git', 'npm')))
27
+ && fs.realpathSync(launcher) === fs.realpathSync(path.join(pkgRoot, 'shim.js'))) {
28
+ global = true;
29
+ }
30
+ } catch {
31
+ process.exit(0);
32
+ }
33
+ if (!global) process.exit(0);
34
+
35
+ spawnSync(bin, ['rc', 'local', 'reconcile-installed', '--allow-other-installation'], {
36
+ stdio: 'ignore',
37
+ timeout: 35000,
38
+ env: { ...process.env, AGIT_INSTALL_CHANNEL: 'npm_global' },
39
+ windowsHide: true,
40
+ });
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { spawn } = require('child_process');
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+
8
+ const RETRY_MS = 5 * 60 * 1000;
9
+
10
+ // An npm policy may suppress postinstall. The next CLI use retries the guarded
11
+ // daemon handoff without changing the command's output or exit status.
12
+ function scheduleReconciliation(bin, pkgRoot) {
13
+ if (process.env.AGIT_BINARY || process.env.AGIT_INTERNAL_UPDATE_RESTART) return;
14
+ const home = process.env.AGIT_HOME?.trim() || path.join(os.homedir(), '.agit');
15
+ const dir = path.join(home, 'desktop-rc');
16
+ let fingerprint;
17
+ try {
18
+ const daemon = fs.statSync(path.join(dir, 'agitd.pid'));
19
+ const installed = fs.statSync(bin);
20
+ fingerprint = `${path.resolve(bin)}:${installed.size}:${installed.mtimeMs}:${daemon.size}:${daemon.mtimeMs}`;
21
+ const stamp = path.join(dir, 'npm-reconcile.json');
22
+ try {
23
+ const previous = JSON.parse(fs.readFileSync(stamp, 'utf8'));
24
+ if (previous.fingerprint === fingerprint && Date.now() - previous.at < RETRY_MS) return;
25
+ } catch { /* An absent or invalid retry receipt cannot suppress reconciliation. */ }
26
+ const temporary = `${stamp}.${process.pid}.${Math.random().toString(16).slice(2)}`;
27
+ try {
28
+ fs.writeFileSync(temporary, JSON.stringify({ fingerprint, at: Date.now() }), { mode: 0o600, flag: 'wx' });
29
+ fs.renameSync(temporary, stamp);
30
+ } catch {
31
+ try { fs.unlinkSync(temporary); } catch { /* The best-effort receipt is optional. */ }
32
+ }
33
+ } catch {
34
+ return; // No running daemon is indicated in this home.
35
+ }
36
+
37
+ try {
38
+ const child = spawn(process.execPath, [path.join(__dirname, 'reconcile-worker.js'), bin, pkgRoot, process.argv[1] || ''], {
39
+ detached: true,
40
+ stdio: 'ignore',
41
+ env: { ...process.env, AGIT_TELEMETRY_DEFER: '1', AGIT_INTERNAL_UPDATE_RESTART: '1' },
42
+ windowsHide: true,
43
+ });
44
+ child.on('error', () => {});
45
+ child.unref();
46
+ } catch {
47
+ // Reconciliation never changes the forwarded command's result.
48
+ }
49
+ }
50
+
51
+ module.exports = { scheduleReconciliation };
package/npm/lib/run.js CHANGED
@@ -5,10 +5,10 @@ const path = require('path');
5
5
  const platform = require('./platform');
6
6
  const { resolveBinary } = require('./resolve');
7
7
  const log = require('./log');
8
+ const { scheduleReconciliation } = require('./reconcile');
8
9
 
9
- // Entry point for the `agit` command. Thin is deliberate: forward argv / stdio /
10
- // exit code, nothing else. agit gets written into scripts and hooks, so exit
11
- // codes and stdio pass-through are load-bearing semantics and must not change.
10
+ // Forward argv, stdio, and exit status unchanged. Hooks rely on those semantics;
11
+ // daemon reconciliation therefore runs independently of the forwarded command.
12
12
  function run() {
13
13
  const pkgRoot = path.join(__dirname, '..');
14
14
  let bin = null;
@@ -37,7 +37,13 @@ function run() {
37
37
  process.exit(127);
38
38
  }
39
39
 
40
- const r = spawnSync(bin, process.argv.slice(2), {
40
+ const args = process.argv.slice(2);
41
+ // A bridge may stay open indefinitely, so queue its install check before forwarding.
42
+ // Other commands finish first to avoid racing their own daemon operation.
43
+ if (args[0] === 'rc' && args[1] === 'local' && args[2] === 'bridge') {
44
+ scheduleReconciliation(bin, pkgRoot);
45
+ }
46
+ const r = spawnSync(bin, args, {
41
47
  stdio: 'inherit',
42
48
  env: { ...process.env, AGIT_INSTALL_CHANNEL: 'npm_global' },
43
49
  });
@@ -45,6 +51,9 @@ function run() {
45
51
  log.error(`failed to start ${bin}: ${r.error.message}`);
46
52
  process.exit(1);
47
53
  }
54
+ if (!(args[0] === 'rc' && args[1] === 'local' && args[2] === 'bridge')) {
55
+ scheduleReconciliation(bin, pkgRoot);
56
+ }
48
57
  process.exit(r.status === null ? 1 : r.status);
49
58
  }
50
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@einsia/agent-git",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "Version control for agent sessions — the agit CLI plus skills/hooks/MCP wiring for Claude Code, Codex, OpenCode and Cursor. The AgentGit hub deploys separately.",
5
5
  "keywords": [
6
6
  "agit",
@@ -49,10 +49,10 @@
49
49
  "access": "public"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@einsia/agent-git-linux-x64": "0.2.11",
53
- "@einsia/agent-git-linux-arm64": "0.2.11",
54
- "@einsia/agent-git-darwin-x64": "0.2.11",
55
- "@einsia/agent-git-darwin-arm64": "0.2.11",
56
- "@einsia/agent-git-win32-x64": "0.2.11"
52
+ "@einsia/agent-git-linux-x64": "0.2.12",
53
+ "@einsia/agent-git-linux-arm64": "0.2.12",
54
+ "@einsia/agent-git-darwin-x64": "0.2.12",
55
+ "@einsia/agent-git-darwin-arm64": "0.2.12",
56
+ "@einsia/agent-git-win32-x64": "0.2.12"
57
57
  }
58
58
  }