@sanqianx/ai-coding-event-bridge-console 0.7.2 → 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 -81
- package/console/index.html +953 -953
- 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 -189
- 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 -200
- package/src/bridge/query/conversation-query.js +194 -194
- package/src/server.js +825 -825
|
@@ -1,181 +1,181 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { ensureRuntimeHome } = require('../../core/runtime-home');
|
|
7
|
-
const { ConsumerRegistry } = require('../../core/consumer-registry');
|
|
8
|
-
const { readJsonIfExists, writeJsonAtomicSync } = require('../../core/fs-utils');
|
|
9
|
-
|
|
10
|
-
const HOOK_EVENTS = ['UserPromptSubmit', 'Stop'];
|
|
11
|
-
|
|
12
|
-
class InstallerError extends Error {
|
|
13
|
-
constructor(message) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.name = 'InstallerError';
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function shimPathFor(homeDir) {
|
|
20
|
-
return path.join(homeDir, 'bin', 'bridge-hook.cjs');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function managedCommand(shimPath) {
|
|
24
|
-
return `node "${shimPath}"`;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isManagedCommand(command, shimPath) {
|
|
28
|
-
return typeof command === 'string' && command.includes(shimPath);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function readSettings(settingsFile) {
|
|
32
|
-
const settings = readJsonIfExists(settingsFile);
|
|
33
|
-
if (settings === null) {
|
|
34
|
-
if (fs.existsSync(settingsFile)) {
|
|
35
|
-
throw new InstallerError(`settings file is not valid JSON: ${settingsFile}`);
|
|
36
|
-
}
|
|
37
|
-
return { hooks: {} };
|
|
38
|
-
}
|
|
39
|
-
if (typeof settings !== 'object' || settings === null) {
|
|
40
|
-
throw new InstallerError(`settings file must contain a JSON object: ${settingsFile}`);
|
|
41
|
-
}
|
|
42
|
-
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
|
43
|
-
settings.hooks = {};
|
|
44
|
-
}
|
|
45
|
-
return settings;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function upsertManagedEntries(settings, shimPath) {
|
|
49
|
-
const command = managedCommand(shimPath);
|
|
50
|
-
const changed = [];
|
|
51
|
-
for (const eventName of HOOK_EVENTS) {
|
|
52
|
-
if (!Array.isArray(settings.hooks[eventName])) {
|
|
53
|
-
settings.hooks[eventName] = [];
|
|
54
|
-
}
|
|
55
|
-
const alreadyManaged = settings.hooks[eventName].some(
|
|
56
|
-
(group) =>
|
|
57
|
-
group &&
|
|
58
|
-
Array.isArray(group.hooks) &&
|
|
59
|
-
group.hooks.some((hook) => hook && hook.type === 'command' && isManagedCommand(hook.command, shimPath))
|
|
60
|
-
);
|
|
61
|
-
if (alreadyManaged) continue;
|
|
62
|
-
settings.hooks[eventName].push({
|
|
63
|
-
matcher: '*',
|
|
64
|
-
hooks: [{ type: 'command', command }]
|
|
65
|
-
});
|
|
66
|
-
changed.push(eventName);
|
|
67
|
-
}
|
|
68
|
-
return changed;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function removeManagedEntries(settings, shimPath) {
|
|
72
|
-
const removed = [];
|
|
73
|
-
for (const eventName of Object.keys(settings.hooks)) {
|
|
74
|
-
const groups = settings.hooks[eventName];
|
|
75
|
-
if (!Array.isArray(groups)) continue;
|
|
76
|
-
const kept = groups.filter(
|
|
77
|
-
(group) =>
|
|
78
|
-
!(
|
|
79
|
-
group &&
|
|
80
|
-
Array.isArray(group.hooks) &&
|
|
81
|
-
group.hooks.some((hook) => hook && hook.type === 'command' && isManagedCommand(hook.command, shimPath))
|
|
82
|
-
)
|
|
83
|
-
);
|
|
84
|
-
if (kept.length !== groups.length) removed.push(eventName);
|
|
85
|
-
if (kept.length === 0) {
|
|
86
|
-
delete settings.hooks[eventName];
|
|
87
|
-
} else {
|
|
88
|
-
settings.hooks[eventName] = kept;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return removed;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Host-level consumer registration is owned by the host (Project-Knowledge
|
|
95
|
-
// registers "project-knowledge" once via createBridge()). Connector
|
|
96
|
-
// installers mutate the registry only on explicit opt-in:
|
|
97
|
-
// registerConsumer: true -> register consumerName (legacy convenience)
|
|
98
|
-
// unregisterConsumer: true -> unregister consumerName during uninstall
|
|
99
|
-
// Uninstalling a connector never deletes the shared runtime home.
|
|
100
|
-
async function installClaudeCodeHook({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, settingsFile, version } = {}) {
|
|
101
|
-
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
102
|
-
const target = settingsFile || path.join(os.homedir(), '.claude', 'settings.json');
|
|
103
|
-
const runtime = await ensureRuntimeHome({ homeDir: home, version });
|
|
104
|
-
const registry = new ConsumerRegistry(home);
|
|
105
|
-
if (consumerName && registerConsumer) {
|
|
106
|
-
await registry.registerConsumer(consumerName, consumerMeta);
|
|
107
|
-
}
|
|
108
|
-
const settings = readSettings(target);
|
|
109
|
-
const added = upsertManagedEntries(settings, shimPathFor(home));
|
|
110
|
-
writeJsonAtomicSync(target, settings);
|
|
111
|
-
return { added, settingsFile: target, runtime, consumers: (await registry.getConsumers()).map((c) => c.name) };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function statusClaudeCodeHook({ homeDir, settingsFile } = {}) {
|
|
115
|
-
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
116
|
-
const target = settingsFile || path.join(os.homedir(), '.claude', 'settings.json');
|
|
117
|
-
const settings = readSettings(target);
|
|
118
|
-
const shimPath = shimPathFor(home);
|
|
119
|
-
const managedEvents = [];
|
|
120
|
-
let thirdPartyCount = 0;
|
|
121
|
-
for (const eventName of Object.keys(settings.hooks || {})) {
|
|
122
|
-
const groups = settings.hooks[eventName];
|
|
123
|
-
if (!Array.isArray(groups)) continue;
|
|
124
|
-
let hasManaged = false;
|
|
125
|
-
for (const group of groups) {
|
|
126
|
-
for (const hook of (group && group.hooks) || []) {
|
|
127
|
-
if (hook && hook.type === 'command') {
|
|
128
|
-
if (isManagedCommand(hook.command, shimPath)) {
|
|
129
|
-
hasManaged = true;
|
|
130
|
-
} else {
|
|
131
|
-
thirdPartyCount++;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
if (hasManaged) managedEvents.push(eventName);
|
|
137
|
-
}
|
|
138
|
-
const registry = new ConsumerRegistry(home);
|
|
139
|
-
return {
|
|
140
|
-
installed: HOOK_EVENTS.every((eventName) => managedEvents.includes(eventName)),
|
|
141
|
-
managedEvents,
|
|
142
|
-
thirdPartyCount,
|
|
143
|
-
consumers: (await registry.getConsumers()).map((c) => c.name)
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function repairClaudeCodeHook({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, settingsFile, version } = {}) {
|
|
148
|
-
return installClaudeCodeHook({ homeDir, consumerName, consumerMeta, registerConsumer, settingsFile, version });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
async function uninstallClaudeCodeHook({ homeDir, consumerName, unregisterConsumer = false, settingsFile } = {}) {
|
|
152
|
-
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
153
|
-
const target = settingsFile || path.join(os.homedir(), '.claude', 'settings.json');
|
|
154
|
-
const registry = new ConsumerRegistry(home);
|
|
155
|
-
const settings = readSettings(target);
|
|
156
|
-
const removed = removeManagedEntries(settings, shimPathFor(home));
|
|
157
|
-
writeJsonAtomicSync(target, settings);
|
|
158
|
-
let consumerUnregistered = false;
|
|
159
|
-
if (consumerName && unregisterConsumer) {
|
|
160
|
-
await registry.unregisterConsumer(consumerName);
|
|
161
|
-
consumerUnregistered = true;
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
removed: true,
|
|
165
|
-
removedEvents: removed,
|
|
166
|
-
settingsFile: target,
|
|
167
|
-
consumerUnregistered,
|
|
168
|
-
consumers: (await registry.getConsumers()).map((c) => c.name)
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
module.exports = {
|
|
173
|
-
HOOK_EVENTS,
|
|
174
|
-
InstallerError,
|
|
175
|
-
installClaudeCodeHook,
|
|
176
|
-
statusClaudeCodeHook,
|
|
177
|
-
repairClaudeCodeHook,
|
|
178
|
-
uninstallClaudeCodeHook,
|
|
179
|
-
managedCommand,
|
|
180
|
-
isManagedCommand
|
|
181
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { ensureRuntimeHome } = require('../../core/runtime-home');
|
|
7
|
+
const { ConsumerRegistry } = require('../../core/consumer-registry');
|
|
8
|
+
const { readJsonIfExists, writeJsonAtomicSync } = require('../../core/fs-utils');
|
|
9
|
+
|
|
10
|
+
const HOOK_EVENTS = ['UserPromptSubmit', 'Stop'];
|
|
11
|
+
|
|
12
|
+
class InstallerError extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'InstallerError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function shimPathFor(homeDir) {
|
|
20
|
+
return path.join(homeDir, 'bin', 'bridge-hook.cjs');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function managedCommand(shimPath) {
|
|
24
|
+
return `node "${shimPath}"`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isManagedCommand(command, shimPath) {
|
|
28
|
+
return typeof command === 'string' && command.includes(shimPath);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function readSettings(settingsFile) {
|
|
32
|
+
const settings = readJsonIfExists(settingsFile);
|
|
33
|
+
if (settings === null) {
|
|
34
|
+
if (fs.existsSync(settingsFile)) {
|
|
35
|
+
throw new InstallerError(`settings file is not valid JSON: ${settingsFile}`);
|
|
36
|
+
}
|
|
37
|
+
return { hooks: {} };
|
|
38
|
+
}
|
|
39
|
+
if (typeof settings !== 'object' || settings === null) {
|
|
40
|
+
throw new InstallerError(`settings file must contain a JSON object: ${settingsFile}`);
|
|
41
|
+
}
|
|
42
|
+
if (!settings.hooks || typeof settings.hooks !== 'object') {
|
|
43
|
+
settings.hooks = {};
|
|
44
|
+
}
|
|
45
|
+
return settings;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function upsertManagedEntries(settings, shimPath) {
|
|
49
|
+
const command = managedCommand(shimPath);
|
|
50
|
+
const changed = [];
|
|
51
|
+
for (const eventName of HOOK_EVENTS) {
|
|
52
|
+
if (!Array.isArray(settings.hooks[eventName])) {
|
|
53
|
+
settings.hooks[eventName] = [];
|
|
54
|
+
}
|
|
55
|
+
const alreadyManaged = settings.hooks[eventName].some(
|
|
56
|
+
(group) =>
|
|
57
|
+
group &&
|
|
58
|
+
Array.isArray(group.hooks) &&
|
|
59
|
+
group.hooks.some((hook) => hook && hook.type === 'command' && isManagedCommand(hook.command, shimPath))
|
|
60
|
+
);
|
|
61
|
+
if (alreadyManaged) continue;
|
|
62
|
+
settings.hooks[eventName].push({
|
|
63
|
+
matcher: '*',
|
|
64
|
+
hooks: [{ type: 'command', command }]
|
|
65
|
+
});
|
|
66
|
+
changed.push(eventName);
|
|
67
|
+
}
|
|
68
|
+
return changed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function removeManagedEntries(settings, shimPath) {
|
|
72
|
+
const removed = [];
|
|
73
|
+
for (const eventName of Object.keys(settings.hooks)) {
|
|
74
|
+
const groups = settings.hooks[eventName];
|
|
75
|
+
if (!Array.isArray(groups)) continue;
|
|
76
|
+
const kept = groups.filter(
|
|
77
|
+
(group) =>
|
|
78
|
+
!(
|
|
79
|
+
group &&
|
|
80
|
+
Array.isArray(group.hooks) &&
|
|
81
|
+
group.hooks.some((hook) => hook && hook.type === 'command' && isManagedCommand(hook.command, shimPath))
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
if (kept.length !== groups.length) removed.push(eventName);
|
|
85
|
+
if (kept.length === 0) {
|
|
86
|
+
delete settings.hooks[eventName];
|
|
87
|
+
} else {
|
|
88
|
+
settings.hooks[eventName] = kept;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return removed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Host-level consumer registration is owned by the host (Project-Knowledge
|
|
95
|
+
// registers "project-knowledge" once via createBridge()). Connector
|
|
96
|
+
// installers mutate the registry only on explicit opt-in:
|
|
97
|
+
// registerConsumer: true -> register consumerName (legacy convenience)
|
|
98
|
+
// unregisterConsumer: true -> unregister consumerName during uninstall
|
|
99
|
+
// Uninstalling a connector never deletes the shared runtime home.
|
|
100
|
+
async function installClaudeCodeHook({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, settingsFile, version } = {}) {
|
|
101
|
+
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
102
|
+
const target = settingsFile || path.join(os.homedir(), '.claude', 'settings.json');
|
|
103
|
+
const runtime = await ensureRuntimeHome({ homeDir: home, version });
|
|
104
|
+
const registry = new ConsumerRegistry(home);
|
|
105
|
+
if (consumerName && registerConsumer) {
|
|
106
|
+
await registry.registerConsumer(consumerName, consumerMeta);
|
|
107
|
+
}
|
|
108
|
+
const settings = readSettings(target);
|
|
109
|
+
const added = upsertManagedEntries(settings, shimPathFor(home));
|
|
110
|
+
writeJsonAtomicSync(target, settings);
|
|
111
|
+
return { added, settingsFile: target, runtime, consumers: (await registry.getConsumers()).map((c) => c.name) };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function statusClaudeCodeHook({ homeDir, settingsFile } = {}) {
|
|
115
|
+
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
116
|
+
const target = settingsFile || path.join(os.homedir(), '.claude', 'settings.json');
|
|
117
|
+
const settings = readSettings(target);
|
|
118
|
+
const shimPath = shimPathFor(home);
|
|
119
|
+
const managedEvents = [];
|
|
120
|
+
let thirdPartyCount = 0;
|
|
121
|
+
for (const eventName of Object.keys(settings.hooks || {})) {
|
|
122
|
+
const groups = settings.hooks[eventName];
|
|
123
|
+
if (!Array.isArray(groups)) continue;
|
|
124
|
+
let hasManaged = false;
|
|
125
|
+
for (const group of groups) {
|
|
126
|
+
for (const hook of (group && group.hooks) || []) {
|
|
127
|
+
if (hook && hook.type === 'command') {
|
|
128
|
+
if (isManagedCommand(hook.command, shimPath)) {
|
|
129
|
+
hasManaged = true;
|
|
130
|
+
} else {
|
|
131
|
+
thirdPartyCount++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (hasManaged) managedEvents.push(eventName);
|
|
137
|
+
}
|
|
138
|
+
const registry = new ConsumerRegistry(home);
|
|
139
|
+
return {
|
|
140
|
+
installed: HOOK_EVENTS.every((eventName) => managedEvents.includes(eventName)),
|
|
141
|
+
managedEvents,
|
|
142
|
+
thirdPartyCount,
|
|
143
|
+
consumers: (await registry.getConsumers()).map((c) => c.name)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function repairClaudeCodeHook({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, settingsFile, version } = {}) {
|
|
148
|
+
return installClaudeCodeHook({ homeDir, consumerName, consumerMeta, registerConsumer, settingsFile, version });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function uninstallClaudeCodeHook({ homeDir, consumerName, unregisterConsumer = false, settingsFile } = {}) {
|
|
152
|
+
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
153
|
+
const target = settingsFile || path.join(os.homedir(), '.claude', 'settings.json');
|
|
154
|
+
const registry = new ConsumerRegistry(home);
|
|
155
|
+
const settings = readSettings(target);
|
|
156
|
+
const removed = removeManagedEntries(settings, shimPathFor(home));
|
|
157
|
+
writeJsonAtomicSync(target, settings);
|
|
158
|
+
let consumerUnregistered = false;
|
|
159
|
+
if (consumerName && unregisterConsumer) {
|
|
160
|
+
await registry.unregisterConsumer(consumerName);
|
|
161
|
+
consumerUnregistered = true;
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
removed: true,
|
|
165
|
+
removedEvents: removed,
|
|
166
|
+
settingsFile: target,
|
|
167
|
+
consumerUnregistered,
|
|
168
|
+
consumers: (await registry.getConsumers()).map((c) => c.name)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
HOOK_EVENTS,
|
|
174
|
+
InstallerError,
|
|
175
|
+
installClaudeCodeHook,
|
|
176
|
+
statusClaudeCodeHook,
|
|
177
|
+
repairClaudeCodeHook,
|
|
178
|
+
uninstallClaudeCodeHook,
|
|
179
|
+
managedCommand,
|
|
180
|
+
isManagedCommand
|
|
181
|
+
};
|
|
@@ -1,114 +1,114 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const fs = require('fs');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const { ensureRuntimeHome } = require('../../core/runtime-home');
|
|
7
|
-
const { ConsumerRegistry } = require('../../core/consumer-registry');
|
|
8
|
-
|
|
9
|
-
class InstallerError extends Error {
|
|
10
|
-
constructor(message) {
|
|
11
|
-
super(message);
|
|
12
|
-
this.name = 'InstallerError';
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function shimPathFor(homeDir) {
|
|
17
|
-
return path.join(homeDir, 'bin', 'bridge-hook.cjs');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function managedNotifyCommand(shimPath) {
|
|
21
|
-
return `["node", "${shimPath}"]`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function isManagedNotifyLine(line, shimPath) {
|
|
25
|
-
return /^\s*notify\s*=/.test(line) && line.includes(shimPath);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Codex config.toml merge. The notify entry is a single TOML array; we install
|
|
30
|
-
* ours only when the key is absent, and refuse to displace a third-party
|
|
31
|
-
* notifier (report conflict instead). Uninstall removes the entry only when it
|
|
32
|
-
* is Bridge-managed.
|
|
33
|
-
*/
|
|
34
|
-
// Registry mutations are host-owned; registerConsumer/unregisterConsumer are
|
|
35
|
-
// explicit opt-ins. Uninstall never deletes the shared runtime home.
|
|
36
|
-
async function installCodexNotify({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, configFile, version } = {}) {
|
|
37
|
-
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
38
|
-
const target = configFile || path.join(os.homedir(), '.codex', 'config.toml');
|
|
39
|
-
const runtime = await ensureRuntimeHome({ homeDir: home, version });
|
|
40
|
-
const registry = new ConsumerRegistry(home);
|
|
41
|
-
if (consumerName && registerConsumer) await registry.registerConsumer(consumerName, consumerMeta);
|
|
42
|
-
|
|
43
|
-
const existing = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : '';
|
|
44
|
-
const lines = existing.split(/\r?\n/);
|
|
45
|
-
let notifyIndex = -1;
|
|
46
|
-
let alreadyManaged = false;
|
|
47
|
-
let thirdParty = false;
|
|
48
|
-
for (let i = 0; i < lines.length; i++) {
|
|
49
|
-
if (/^\s*notify\s*=/.test(lines[i])) {
|
|
50
|
-
notifyIndex = i;
|
|
51
|
-
if (isManagedNotifyLine(lines[i], shimPathFor(home))) alreadyManaged = true;
|
|
52
|
-
else thirdParty = true;
|
|
53
|
-
break;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (thirdParty && !alreadyManaged) {
|
|
57
|
-
return {
|
|
58
|
-
installed: false,
|
|
59
|
-
conflict: true,
|
|
60
|
-
reason: 'third-party-notify-present',
|
|
61
|
-
settingsFile: target,
|
|
62
|
-
runtime
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
if (!alreadyManaged) {
|
|
66
|
-
const entry = `notify = ${managedNotifyCommand(shimPathFor(home))}`;
|
|
67
|
-
if (notifyIndex === -1) {
|
|
68
|
-
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
69
|
-
lines.push('', entry);
|
|
70
|
-
} else {
|
|
71
|
-
lines[notifyIndex] = entry;
|
|
72
|
-
}
|
|
73
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
74
|
-
fs.writeFileSync(target, lines.join('\n').replace(/^\n/, '') + '\n');
|
|
75
|
-
}
|
|
76
|
-
return { installed: true, conflict: false, settingsFile: target, runtime };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async function statusCodexNotify({ homeDir, configFile } = {}) {
|
|
80
|
-
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
81
|
-
const target = configFile || path.join(os.homedir(), '.codex', 'config.toml');
|
|
82
|
-
if (!fs.existsSync(target)) return { installed: false, thirdParty: false };
|
|
83
|
-
const line = fs.readFileSync(target, 'utf8').split(/\r?\n/).find(l => /^\s*notify\s*=/.test(l));
|
|
84
|
-
if (!line) return { installed: false, thirdParty: false };
|
|
85
|
-
return { installed: isManagedNotifyLine(line, shimPathFor(home)), thirdParty: !isManagedNotifyLine(line, shimPathFor(home)) };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function uninstallCodexNotify({ homeDir, consumerName, unregisterConsumer = false, configFile } = {}) {
|
|
89
|
-
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
90
|
-
const target = configFile || path.join(os.homedir(), '.codex', 'config.toml');
|
|
91
|
-
const registry = new ConsumerRegistry(home);
|
|
92
|
-
if (fs.existsSync(target)) {
|
|
93
|
-
const lines = fs.readFileSync(target, 'utf8').split(/\r?\n/);
|
|
94
|
-
const kept = lines.filter(line => !(isManagedNotifyLine(line, shimPathFor(home))));
|
|
95
|
-
if (kept.length !== lines.length) {
|
|
96
|
-
fs.writeFileSync(target, kept.join('\n').replace(/\n{3,}/g, '\n\n'));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
let consumerUnregistered = false;
|
|
100
|
-
if (consumerName && unregisterConsumer) {
|
|
101
|
-
await registry.unregisterConsumer(consumerName);
|
|
102
|
-
consumerUnregistered = true;
|
|
103
|
-
}
|
|
104
|
-
return { removed: true, consumerUnregistered, consumers: (await registry.getConsumers()).map(c => c.name) };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
module.exports = {
|
|
108
|
-
InstallerError,
|
|
109
|
-
installCodexNotify,
|
|
110
|
-
statusCodexNotify,
|
|
111
|
-
uninstallCodexNotify,
|
|
112
|
-
managedNotifyCommand,
|
|
113
|
-
isManagedNotifyLine
|
|
114
|
-
};
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { ensureRuntimeHome } = require('../../core/runtime-home');
|
|
7
|
+
const { ConsumerRegistry } = require('../../core/consumer-registry');
|
|
8
|
+
|
|
9
|
+
class InstallerError extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'InstallerError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function shimPathFor(homeDir) {
|
|
17
|
+
return path.join(homeDir, 'bin', 'bridge-hook.cjs');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function managedNotifyCommand(shimPath) {
|
|
21
|
+
return `["node", "${shimPath}"]`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isManagedNotifyLine(line, shimPath) {
|
|
25
|
+
return /^\s*notify\s*=/.test(line) && line.includes(shimPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Codex config.toml merge. The notify entry is a single TOML array; we install
|
|
30
|
+
* ours only when the key is absent, and refuse to displace a third-party
|
|
31
|
+
* notifier (report conflict instead). Uninstall removes the entry only when it
|
|
32
|
+
* is Bridge-managed.
|
|
33
|
+
*/
|
|
34
|
+
// Registry mutations are host-owned; registerConsumer/unregisterConsumer are
|
|
35
|
+
// explicit opt-ins. Uninstall never deletes the shared runtime home.
|
|
36
|
+
async function installCodexNotify({ homeDir, consumerName, consumerMeta = {}, registerConsumer = false, configFile, version } = {}) {
|
|
37
|
+
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
38
|
+
const target = configFile || path.join(os.homedir(), '.codex', 'config.toml');
|
|
39
|
+
const runtime = await ensureRuntimeHome({ homeDir: home, version });
|
|
40
|
+
const registry = new ConsumerRegistry(home);
|
|
41
|
+
if (consumerName && registerConsumer) await registry.registerConsumer(consumerName, consumerMeta);
|
|
42
|
+
|
|
43
|
+
const existing = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : '';
|
|
44
|
+
const lines = existing.split(/\r?\n/);
|
|
45
|
+
let notifyIndex = -1;
|
|
46
|
+
let alreadyManaged = false;
|
|
47
|
+
let thirdParty = false;
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
if (/^\s*notify\s*=/.test(lines[i])) {
|
|
50
|
+
notifyIndex = i;
|
|
51
|
+
if (isManagedNotifyLine(lines[i], shimPathFor(home))) alreadyManaged = true;
|
|
52
|
+
else thirdParty = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (thirdParty && !alreadyManaged) {
|
|
57
|
+
return {
|
|
58
|
+
installed: false,
|
|
59
|
+
conflict: true,
|
|
60
|
+
reason: 'third-party-notify-present',
|
|
61
|
+
settingsFile: target,
|
|
62
|
+
runtime
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (!alreadyManaged) {
|
|
66
|
+
const entry = `notify = ${managedNotifyCommand(shimPathFor(home))}`;
|
|
67
|
+
if (notifyIndex === -1) {
|
|
68
|
+
while (lines.length && lines[lines.length - 1].trim() === '') lines.pop();
|
|
69
|
+
lines.push('', entry);
|
|
70
|
+
} else {
|
|
71
|
+
lines[notifyIndex] = entry;
|
|
72
|
+
}
|
|
73
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
74
|
+
fs.writeFileSync(target, lines.join('\n').replace(/^\n/, '') + '\n');
|
|
75
|
+
}
|
|
76
|
+
return { installed: true, conflict: false, settingsFile: target, runtime };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function statusCodexNotify({ homeDir, configFile } = {}) {
|
|
80
|
+
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
81
|
+
const target = configFile || path.join(os.homedir(), '.codex', 'config.toml');
|
|
82
|
+
if (!fs.existsSync(target)) return { installed: false, thirdParty: false };
|
|
83
|
+
const line = fs.readFileSync(target, 'utf8').split(/\r?\n/).find(l => /^\s*notify\s*=/.test(l));
|
|
84
|
+
if (!line) return { installed: false, thirdParty: false };
|
|
85
|
+
return { installed: isManagedNotifyLine(line, shimPathFor(home)), thirdParty: !isManagedNotifyLine(line, shimPathFor(home)) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function uninstallCodexNotify({ homeDir, consumerName, unregisterConsumer = false, configFile } = {}) {
|
|
89
|
+
const home = homeDir || path.join(os.homedir(), '.ai-coding-event-bridge');
|
|
90
|
+
const target = configFile || path.join(os.homedir(), '.codex', 'config.toml');
|
|
91
|
+
const registry = new ConsumerRegistry(home);
|
|
92
|
+
if (fs.existsSync(target)) {
|
|
93
|
+
const lines = fs.readFileSync(target, 'utf8').split(/\r?\n/);
|
|
94
|
+
const kept = lines.filter(line => !(isManagedNotifyLine(line, shimPathFor(home))));
|
|
95
|
+
if (kept.length !== lines.length) {
|
|
96
|
+
fs.writeFileSync(target, kept.join('\n').replace(/\n{3,}/g, '\n\n'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
let consumerUnregistered = false;
|
|
100
|
+
if (consumerName && unregisterConsumer) {
|
|
101
|
+
await registry.unregisterConsumer(consumerName);
|
|
102
|
+
consumerUnregistered = true;
|
|
103
|
+
}
|
|
104
|
+
return { removed: true, consumerUnregistered, consumers: (await registry.getConsumers()).map(c => c.name) };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
InstallerError,
|
|
109
|
+
installCodexNotify,
|
|
110
|
+
statusCodexNotify,
|
|
111
|
+
uninstallCodexNotify,
|
|
112
|
+
managedNotifyCommand,
|
|
113
|
+
isManagedNotifyLine
|
|
114
|
+
};
|