@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 +30 -0
- package/npm/README.md +3 -0
- package/npm/lib/reconcile-worker.js +40 -0
- package/npm/lib/reconcile.js +51 -0
- package/npm/lib/run.js +13 -4
- package/npm/postinstall.js +19 -9
- package/package.json +6 -6
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
|
-
//
|
|
10
|
-
//
|
|
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
|
|
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/npm/postinstall.js
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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.
|
|
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.
|
|
53
|
-
"@einsia/agent-git-linux-arm64": "0.2.
|
|
54
|
-
"@einsia/agent-git-darwin-x64": "0.2.
|
|
55
|
-
"@einsia/agent-git-darwin-arm64": "0.2.
|
|
56
|
-
"@einsia/agent-git-win32-x64": "0.2.
|
|
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
|
}
|