@sanqianx/ai-coding-event-bridge-console 0.7.1 → 0.7.3
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/LICENSE +21 -21
- package/README.md +81 -74
- package/console/index.html +953 -948
- package/package.json +26 -26
- package/src/agent-detect.js +128 -128
- package/src/bin.js +80 -80
- package/src/bridge/bin/commit-boundary.js +64 -64
- package/src/bridge/connectors/capture-guard.js +24 -24
- package/src/bridge/connectors/claude-code/hook-entry.js +45 -45
- package/src/bridge/connectors/claude-code/notify.js +57 -57
- package/src/bridge/connectors/codex/cursor-store.js +83 -83
- package/src/bridge/connectors/codex/hook-entry.js +215 -215
- package/src/bridge/connectors/codex/session-parser.js +100 -100
- package/src/bridge/connectors/opencode/hook-entry.js +55 -55
- package/src/bridge/core/bridge.js +249 -246
- package/src/bridge/core/commit-projection.js +288 -288
- package/src/bridge/core/compaction.js +23 -23
- package/src/bridge/core/consumer-registry.js +111 -111
- package/src/bridge/core/event-schema.js +127 -127
- package/src/bridge/core/fs-utils.js +81 -81
- package/src/bridge/core/journal-router.js +39 -39
- package/src/bridge/core/journal.js +507 -507
- package/src/bridge/core/lock.js +96 -96
- package/src/bridge/core/normalizer.js +114 -114
- package/src/bridge/core/paths.js +12 -12
- package/src/bridge/core/project-registry.js +176 -176
- package/src/bridge/core/repo-context.js +191 -191
- package/src/bridge/core/runtime-home.js +189 -188
- package/src/bridge/core/semver.js +23 -23
- package/src/bridge/core/turn-identity.js +127 -127
- package/src/bridge/index.js +86 -86
- package/src/bridge/installers/claude-code/installer.js +181 -181
- package/src/bridge/installers/codex/installer.js +114 -114
- package/src/bridge/installers/opencode/installer.js +96 -96
- package/src/bridge/installers/zcode/installer.js +200 -188
- package/src/bridge/query/conversation-query.js +194 -194
- package/src/server.js +825 -825
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const crypto = require('crypto');
|
|
6
|
-
const { execFile } = require('child_process');
|
|
7
|
-
|
|
8
|
-
const REPO_IDENTITY_SCHEMA = 'repo-identity/v1';
|
|
9
|
-
|
|
10
|
-
function normalizeGitUrl(url) {
|
|
11
|
-
if (typeof url !== 'string') return null;
|
|
12
|
-
let u = url.trim();
|
|
13
|
-
if (!u) return null;
|
|
14
|
-
let match = /^git@([^:]+):(.+?)(?:\.git)?$/i.exec(u);
|
|
15
|
-
if (match) return `${match[1].toLowerCase()}/${match[2].replace(/\.git$/, '')}`;
|
|
16
|
-
match = /^(?:https?|ssh|git):\/\/(?:[^@]+@)?([^/]+)\/(.+?)(?:\.git)?$/i.exec(u);
|
|
17
|
-
if (match) return `${match[1].toLowerCase()}/${match[2].replace(/\.git$/, '')}`;
|
|
18
|
-
return u.replace(/\.git$/, '');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function runGitDefault(args, cwd) {
|
|
22
|
-
return new Promise((resolve, reject) => {
|
|
23
|
-
execFile('git', args, { cwd, windowsHide: true }, (err, stdout) => {
|
|
24
|
-
if (err) return reject(err);
|
|
25
|
-
resolve(String(stdout).trim());
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function realpathBestEffort(target) {
|
|
31
|
-
try {
|
|
32
|
-
return fs.realpathSync.native ? fs.realpathSync.native(target) : fs.realpathSync(target);
|
|
33
|
-
} catch (_) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Comparison form: identity comparisons are case-insensitive on Windows and
|
|
39
|
-
// separator-insensitive everywhere. The display workspaceRoot keeps its
|
|
40
|
-
// on-disk casing; only the hash/comparison form is normalized.
|
|
41
|
-
function comparisonForm(target) {
|
|
42
|
-
let value = String(target).replace(/\\/g, '/');
|
|
43
|
-
while (value.length > 1 && value.endsWith('/')) value = value.slice(0, -1);
|
|
44
|
-
if (process.platform === 'win32') value = value.toLowerCase();
|
|
45
|
-
return value;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function workspaceIdFor(workspaceRoot) {
|
|
49
|
-
const digest = crypto.createHash('sha256').update(comparisonForm(workspaceRoot)).digest('hex');
|
|
50
|
-
return `sha256:${digest}`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function isValidRepoIdentityV1(identity) {
|
|
54
|
-
return Boolean(
|
|
55
|
-
identity &&
|
|
56
|
-
typeof identity === 'object' &&
|
|
57
|
-
identity.schema === REPO_IDENTITY_SCHEMA &&
|
|
58
|
-
typeof identity.workspaceId === 'string' &&
|
|
59
|
-
/^sha256:[0-9a-f]{64}$/.test(identity.workspaceId) &&
|
|
60
|
-
typeof identity.workspaceRoot === 'string' &&
|
|
61
|
-
identity.workspaceRoot.length > 0 &&
|
|
62
|
-
(identity.commonDir === null || identity.commonDir === undefined || typeof identity.commonDir === 'string') &&
|
|
63
|
-
(identity.remote === null || identity.remote === undefined || typeof identity.remote === 'string')
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Journal/projection key. v1 identities key by workspaceId; plain strings are
|
|
68
|
-
// legacy journal records written before repo-identity/v1 and stay readable.
|
|
69
|
-
function repoIdentityKey(identity) {
|
|
70
|
-
if (identity === null || identity === undefined) return null;
|
|
71
|
-
if (typeof identity === 'string') return identity;
|
|
72
|
-
if (isValidRepoIdentityV1(identity)) return identity.workspaceId;
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Turn-namespace key: events with no repo identity at all share the 'no-repo'
|
|
77
|
-
// namespace so same-session binding still works outside Git repos, while
|
|
78
|
-
// malformed identity objects get a unique namespace and never cross-bind.
|
|
79
|
-
function turnScopeKey(identity) {
|
|
80
|
-
if (identity === null || identity === undefined) return 'no-repo';
|
|
81
|
-
const key = repoIdentityKey(identity);
|
|
82
|
-
if (key !== null) return key;
|
|
83
|
-
try {
|
|
84
|
-
return `invalid:${JSON.stringify(identity).slice(0, 64)}`;
|
|
85
|
-
} catch (_) {
|
|
86
|
-
return 'invalid:unserializable';
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function sameRepoIdentity(a, b) {
|
|
91
|
-
const keyA = repoIdentityKey(a);
|
|
92
|
-
const keyB = repoIdentityKey(b);
|
|
93
|
-
if (keyA === null || keyB === null) return false;
|
|
94
|
-
return keyA === keyB;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function buildRepoIdentityV1({ workspaceRoot, commonDir = null, remote = null }) {
|
|
98
|
-
return {
|
|
99
|
-
schema: REPO_IDENTITY_SCHEMA,
|
|
100
|
-
workspaceId: workspaceIdFor(workspaceRoot),
|
|
101
|
-
workspaceRoot,
|
|
102
|
-
commonDir: commonDir === undefined ? null : commonDir,
|
|
103
|
-
remote: remote === undefined ? null : remote
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Canonical workspace identity for an authoritative development cwd.
|
|
109
|
-
*
|
|
110
|
-
* 1. `git rev-parse --show-toplevel` (authoritative Git working tree root)
|
|
111
|
-
* 2. realpath + canonical separators for the display root
|
|
112
|
-
* 3. workspaceId = sha256 over the comparison form (case-insensitive on win32)
|
|
113
|
-
* 4. commonDir from `git rev-parse --git-common-dir`, resolved against cwd
|
|
114
|
-
* (metadata: worktrees share it, but their workspaceIds differ)
|
|
115
|
-
* 5. normalized origin URL as metadata only — never the primary identity
|
|
116
|
-
*
|
|
117
|
-
* Non-Git cwd => repoIdentity null + identityConfidence 'unavailable'.
|
|
118
|
-
* No fallback to process.cwd, no fake workspaceId, remote never overrides.
|
|
119
|
-
*/
|
|
120
|
-
async function resolveRepoContext(cwd, { runGit = runGitDefault } = {}) {
|
|
121
|
-
const projectPath = realpathBestEffort(cwd) || path.resolve(cwd);
|
|
122
|
-
let toplevel = null;
|
|
123
|
-
try {
|
|
124
|
-
toplevel = await runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
125
|
-
} catch (_) {
|
|
126
|
-
return {
|
|
127
|
-
repoIdentity: null,
|
|
128
|
-
projectPath,
|
|
129
|
-
branch: null,
|
|
130
|
-
headAtCapture: null,
|
|
131
|
-
identityConfidence: 'unavailable'
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
const realToplevel = path.resolve(realpathBestEffort(toplevel) || toplevel);
|
|
135
|
-
|
|
136
|
-
let commonDir = null;
|
|
137
|
-
try {
|
|
138
|
-
const rawCommonDir = String(await runGit(['rev-parse', '--git-common-dir'], cwd)).trim();
|
|
139
|
-
if (rawCommonDir && !/[\r\n\0]/.test(rawCommonDir) && !rawCommonDir.startsWith('--')) {
|
|
140
|
-
const absoluteCommonDir = path.resolve(cwd, rawCommonDir);
|
|
141
|
-
commonDir = realpathBestEffort(absoluteCommonDir) || absoluteCommonDir;
|
|
142
|
-
}
|
|
143
|
-
} catch (_) {
|
|
144
|
-
commonDir = null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let origin = null;
|
|
148
|
-
try {
|
|
149
|
-
origin = await runGit(['config', '--get', 'remote.origin.url'], cwd);
|
|
150
|
-
} catch (_) {
|
|
151
|
-
origin = null;
|
|
152
|
-
}
|
|
153
|
-
let branch = null;
|
|
154
|
-
try {
|
|
155
|
-
branch = await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
156
|
-
} catch (_) {
|
|
157
|
-
branch = null;
|
|
158
|
-
}
|
|
159
|
-
let head = null;
|
|
160
|
-
try {
|
|
161
|
-
head = await runGit(['rev-parse', 'HEAD'], cwd);
|
|
162
|
-
} catch (_) {
|
|
163
|
-
head = null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
repoIdentity: buildRepoIdentityV1({
|
|
168
|
-
workspaceRoot: realToplevel,
|
|
169
|
-
commonDir,
|
|
170
|
-
remote: normalizeGitUrl(origin)
|
|
171
|
-
}),
|
|
172
|
-
projectPath: realToplevel,
|
|
173
|
-
branch: branch === 'HEAD' ? null : branch,
|
|
174
|
-
headAtCapture: head,
|
|
175
|
-
identityConfidence: 'exact'
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
module.exports = {
|
|
180
|
-
REPO_IDENTITY_SCHEMA,
|
|
181
|
-
normalizeGitUrl,
|
|
182
|
-
resolveRepoContext,
|
|
183
|
-
realpathBestEffort,
|
|
184
|
-
comparisonForm,
|
|
185
|
-
workspaceIdFor,
|
|
186
|
-
buildRepoIdentityV1,
|
|
187
|
-
isValidRepoIdentityV1,
|
|
188
|
-
repoIdentityKey,
|
|
189
|
-
turnScopeKey,
|
|
190
|
-
sameRepoIdentity
|
|
191
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const { execFile } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const REPO_IDENTITY_SCHEMA = 'repo-identity/v1';
|
|
9
|
+
|
|
10
|
+
function normalizeGitUrl(url) {
|
|
11
|
+
if (typeof url !== 'string') return null;
|
|
12
|
+
let u = url.trim();
|
|
13
|
+
if (!u) return null;
|
|
14
|
+
let match = /^git@([^:]+):(.+?)(?:\.git)?$/i.exec(u);
|
|
15
|
+
if (match) return `${match[1].toLowerCase()}/${match[2].replace(/\.git$/, '')}`;
|
|
16
|
+
match = /^(?:https?|ssh|git):\/\/(?:[^@]+@)?([^/]+)\/(.+?)(?:\.git)?$/i.exec(u);
|
|
17
|
+
if (match) return `${match[1].toLowerCase()}/${match[2].replace(/\.git$/, '')}`;
|
|
18
|
+
return u.replace(/\.git$/, '');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function runGitDefault(args, cwd) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
execFile('git', args, { cwd, windowsHide: true }, (err, stdout) => {
|
|
24
|
+
if (err) return reject(err);
|
|
25
|
+
resolve(String(stdout).trim());
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function realpathBestEffort(target) {
|
|
31
|
+
try {
|
|
32
|
+
return fs.realpathSync.native ? fs.realpathSync.native(target) : fs.realpathSync(target);
|
|
33
|
+
} catch (_) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Comparison form: identity comparisons are case-insensitive on Windows and
|
|
39
|
+
// separator-insensitive everywhere. The display workspaceRoot keeps its
|
|
40
|
+
// on-disk casing; only the hash/comparison form is normalized.
|
|
41
|
+
function comparisonForm(target) {
|
|
42
|
+
let value = String(target).replace(/\\/g, '/');
|
|
43
|
+
while (value.length > 1 && value.endsWith('/')) value = value.slice(0, -1);
|
|
44
|
+
if (process.platform === 'win32') value = value.toLowerCase();
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function workspaceIdFor(workspaceRoot) {
|
|
49
|
+
const digest = crypto.createHash('sha256').update(comparisonForm(workspaceRoot)).digest('hex');
|
|
50
|
+
return `sha256:${digest}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isValidRepoIdentityV1(identity) {
|
|
54
|
+
return Boolean(
|
|
55
|
+
identity &&
|
|
56
|
+
typeof identity === 'object' &&
|
|
57
|
+
identity.schema === REPO_IDENTITY_SCHEMA &&
|
|
58
|
+
typeof identity.workspaceId === 'string' &&
|
|
59
|
+
/^sha256:[0-9a-f]{64}$/.test(identity.workspaceId) &&
|
|
60
|
+
typeof identity.workspaceRoot === 'string' &&
|
|
61
|
+
identity.workspaceRoot.length > 0 &&
|
|
62
|
+
(identity.commonDir === null || identity.commonDir === undefined || typeof identity.commonDir === 'string') &&
|
|
63
|
+
(identity.remote === null || identity.remote === undefined || typeof identity.remote === 'string')
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Journal/projection key. v1 identities key by workspaceId; plain strings are
|
|
68
|
+
// legacy journal records written before repo-identity/v1 and stay readable.
|
|
69
|
+
function repoIdentityKey(identity) {
|
|
70
|
+
if (identity === null || identity === undefined) return null;
|
|
71
|
+
if (typeof identity === 'string') return identity;
|
|
72
|
+
if (isValidRepoIdentityV1(identity)) return identity.workspaceId;
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Turn-namespace key: events with no repo identity at all share the 'no-repo'
|
|
77
|
+
// namespace so same-session binding still works outside Git repos, while
|
|
78
|
+
// malformed identity objects get a unique namespace and never cross-bind.
|
|
79
|
+
function turnScopeKey(identity) {
|
|
80
|
+
if (identity === null || identity === undefined) return 'no-repo';
|
|
81
|
+
const key = repoIdentityKey(identity);
|
|
82
|
+
if (key !== null) return key;
|
|
83
|
+
try {
|
|
84
|
+
return `invalid:${JSON.stringify(identity).slice(0, 64)}`;
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return 'invalid:unserializable';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sameRepoIdentity(a, b) {
|
|
91
|
+
const keyA = repoIdentityKey(a);
|
|
92
|
+
const keyB = repoIdentityKey(b);
|
|
93
|
+
if (keyA === null || keyB === null) return false;
|
|
94
|
+
return keyA === keyB;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildRepoIdentityV1({ workspaceRoot, commonDir = null, remote = null }) {
|
|
98
|
+
return {
|
|
99
|
+
schema: REPO_IDENTITY_SCHEMA,
|
|
100
|
+
workspaceId: workspaceIdFor(workspaceRoot),
|
|
101
|
+
workspaceRoot,
|
|
102
|
+
commonDir: commonDir === undefined ? null : commonDir,
|
|
103
|
+
remote: remote === undefined ? null : remote
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Canonical workspace identity for an authoritative development cwd.
|
|
109
|
+
*
|
|
110
|
+
* 1. `git rev-parse --show-toplevel` (authoritative Git working tree root)
|
|
111
|
+
* 2. realpath + canonical separators for the display root
|
|
112
|
+
* 3. workspaceId = sha256 over the comparison form (case-insensitive on win32)
|
|
113
|
+
* 4. commonDir from `git rev-parse --git-common-dir`, resolved against cwd
|
|
114
|
+
* (metadata: worktrees share it, but their workspaceIds differ)
|
|
115
|
+
* 5. normalized origin URL as metadata only — never the primary identity
|
|
116
|
+
*
|
|
117
|
+
* Non-Git cwd => repoIdentity null + identityConfidence 'unavailable'.
|
|
118
|
+
* No fallback to process.cwd, no fake workspaceId, remote never overrides.
|
|
119
|
+
*/
|
|
120
|
+
async function resolveRepoContext(cwd, { runGit = runGitDefault } = {}) {
|
|
121
|
+
const projectPath = realpathBestEffort(cwd) || path.resolve(cwd);
|
|
122
|
+
let toplevel = null;
|
|
123
|
+
try {
|
|
124
|
+
toplevel = await runGit(['rev-parse', '--show-toplevel'], cwd);
|
|
125
|
+
} catch (_) {
|
|
126
|
+
return {
|
|
127
|
+
repoIdentity: null,
|
|
128
|
+
projectPath,
|
|
129
|
+
branch: null,
|
|
130
|
+
headAtCapture: null,
|
|
131
|
+
identityConfidence: 'unavailable'
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const realToplevel = path.resolve(realpathBestEffort(toplevel) || toplevel);
|
|
135
|
+
|
|
136
|
+
let commonDir = null;
|
|
137
|
+
try {
|
|
138
|
+
const rawCommonDir = String(await runGit(['rev-parse', '--git-common-dir'], cwd)).trim();
|
|
139
|
+
if (rawCommonDir && !/[\r\n\0]/.test(rawCommonDir) && !rawCommonDir.startsWith('--')) {
|
|
140
|
+
const absoluteCommonDir = path.resolve(cwd, rawCommonDir);
|
|
141
|
+
commonDir = realpathBestEffort(absoluteCommonDir) || absoluteCommonDir;
|
|
142
|
+
}
|
|
143
|
+
} catch (_) {
|
|
144
|
+
commonDir = null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let origin = null;
|
|
148
|
+
try {
|
|
149
|
+
origin = await runGit(['config', '--get', 'remote.origin.url'], cwd);
|
|
150
|
+
} catch (_) {
|
|
151
|
+
origin = null;
|
|
152
|
+
}
|
|
153
|
+
let branch = null;
|
|
154
|
+
try {
|
|
155
|
+
branch = await runGit(['rev-parse', '--abbrev-ref', 'HEAD'], cwd);
|
|
156
|
+
} catch (_) {
|
|
157
|
+
branch = null;
|
|
158
|
+
}
|
|
159
|
+
let head = null;
|
|
160
|
+
try {
|
|
161
|
+
head = await runGit(['rev-parse', 'HEAD'], cwd);
|
|
162
|
+
} catch (_) {
|
|
163
|
+
head = null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
repoIdentity: buildRepoIdentityV1({
|
|
168
|
+
workspaceRoot: realToplevel,
|
|
169
|
+
commonDir,
|
|
170
|
+
remote: normalizeGitUrl(origin)
|
|
171
|
+
}),
|
|
172
|
+
projectPath: realToplevel,
|
|
173
|
+
branch: branch === 'HEAD' ? null : branch,
|
|
174
|
+
headAtCapture: head,
|
|
175
|
+
identityConfidence: 'exact'
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
REPO_IDENTITY_SCHEMA,
|
|
181
|
+
normalizeGitUrl,
|
|
182
|
+
resolveRepoContext,
|
|
183
|
+
realpathBestEffort,
|
|
184
|
+
comparisonForm,
|
|
185
|
+
workspaceIdFor,
|
|
186
|
+
buildRepoIdentityV1,
|
|
187
|
+
isValidRepoIdentityV1,
|
|
188
|
+
repoIdentityKey,
|
|
189
|
+
turnScopeKey,
|
|
190
|
+
sameRepoIdentity
|
|
191
|
+
};
|