@einsia/agent-git 0.2.10 → 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
@@ -6,6 +6,36 @@ Every notable change to agit, the AgentGit CLI, by release. The format follows
6
6
  its [GitHub Release](https://github.com/Einsia/agent-git/releases), and the
7
7
  `@einsia/agent-git` npm package ships this file.
8
8
 
9
+ ## [Unreleased]
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
+
20
+ ## [0.2.11] - 2026-09-28
21
+
22
+ ### Fixed
23
+
24
+ - Reconcile the local RC daemon after self-upgrade, global npm installation, or
25
+ `npx create-agit`, including installations at a different executable path.
26
+ Idle sessions attached to a persistent Codex server can detach for a safe
27
+ restart; busy daemons retry after their work clears.
28
+ - Stage the `create-agit` executable before replacing it, so an installation
29
+ failure preserves the previous CLI. Report when another `agit` on `PATH`
30
+ shadows the installed copy.
31
+ - Reach the Hub without a fixed stall when one of its addresses is unreachable
32
+ from your network. agit dialed the resolved addresses one after another and
33
+ gave the first one most of the connection budget, so every command that talked
34
+ to the Hub could wait about 20 seconds before trying the next address. The
35
+ addresses now race: the next one starts after 250 ms or as soon as the
36
+ previous one fails. Hub API requests, SOCKS proxy connections, LFS
37
+ availability checks and `agit upgrade` downloads all use it.
38
+
9
39
  ## [0.2.10] - 2026-09-28
10
40
 
11
41
  ### 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
 
@@ -1,14 +1,8 @@
1
1
  'use strict';
2
2
 
3
- // postinstall: the binary lands with the optional dep, so there is nothing to download. It
4
- // does two things in passing, each idempotent and neither able to block the install when it
5
- // fails:
6
- //
7
- // 1. Self-check that the binary really runs (an architecture mismatch, Rosetta and the like
8
- // surface here rather than the first time the user types agit).
9
- // 2. Run `agit setup` — the product promise of "installed by default at download time":
10
- // skills, hooks, MCP and AGENTS.md, all in one go. AGIT_SKIP_SETUP=1 turns it off
11
- // (CI / hosted environments).
3
+ // The binary lands with the optional dependency, so installation needs no download. Verify it
4
+ // before setup or daemon reconciliation; neither follow-up can invalidate the installed CLI.
5
+ // AGIT_SKIP_SETUP=1 leaves the installed binary available without configuring user integrations.
12
6
  //
13
7
  // npm displays postinstall output badly, so stay quiet: speak only when something is wrong.
14
8
 
@@ -59,6 +53,22 @@ function main() {
59
53
  env: { ...process.env, AGIT_INSTALL_CHANNEL: 'npm_global' },
60
54
  });
61
55
  }
56
+ if (truthy(process.env.npm_config_global) && process.env.npm_command !== 'exec') {
57
+ const reconciled = spawnSync(bin, ['rc', 'local', 'reconcile-installed', '--allow-other-installation'], {
58
+ encoding: 'utf8', timeout: 35000,
59
+ env: { ...env, AGIT_INTERNAL_UPDATE_RESTART: '1' },
60
+ });
61
+ if (reconciled.error || reconciled.status !== 0) {
62
+ log.warn(`RC daemon reconciliation failed: ${(reconciled.stderr || reconciled.error?.message || `exit ${reconciled.status}`).trim()}`);
63
+ } else {
64
+ try {
65
+ const outcome = JSON.parse(reconciled.stdout);
66
+ if (outcome.status === 'deferred') log.warn(`RC daemon upgrade is pending: ${outcome.message}`);
67
+ } catch {
68
+ log.warn('RC daemon returned an unreadable reconciliation result.');
69
+ }
70
+ }
71
+ }
62
72
  if (truthy(process.env.AGIT_SKIP_SETUP)) return;
63
73
  const setup = spawnSync(bin, ['setup'], { stdio: 'inherit', env });
64
74
  if (setup.status !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@einsia/agent-git",
3
- "version": "0.2.10",
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.10",
53
- "@einsia/agent-git-linux-arm64": "0.2.10",
54
- "@einsia/agent-git-darwin-x64": "0.2.10",
55
- "@einsia/agent-git-darwin-arm64": "0.2.10",
56
- "@einsia/agent-git-win32-x64": "0.2.10"
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
  }