fraim-hub 2.0.247 → 2.0.249
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/dist/src/ai-hub/cli.js +43 -6
- package/dist/src/ai-hub/custom-employees.js +31 -19
- package/dist/src/ai-hub/host-session-state.js +138 -0
- package/dist/src/ai-hub/hosts.js +4 -1
- package/dist/src/ai-hub/office-sideload.js +45 -16
- package/dist/src/ai-hub/preferences.js +6 -7
- package/dist/src/ai-hub/server.js +75 -20
- package/dist/src/cli/utils/local-folder-sync.js +100 -48
- package/dist/src/cli/utils/org-publish.js +37 -40
- package/dist/src/cli/utils/pack-git-publish.js +248 -0
- package/dist/src/cli/utils/pack-home.js +279 -0
- package/dist/src/cli/utils/user-config.js +10 -2
- package/dist/src/core/capability-pack.js +23 -1
- package/dist/src/local-mcp-server/learning-context-builder.js +29 -8
- package/extensions/office-word/manifest.xml +3 -3
- package/package.json +4 -2
- package/public/ai-hub/script.js +503 -60
- package/public/ai-hub/styles.css +141 -0
package/dist/src/ai-hub/cli.js
CHANGED
|
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.waitForDesktopHubReady = waitForDesktopHubReady;
|
|
39
40
|
exports.runHub = runHub;
|
|
40
41
|
// Hub-owned launcher used by the fraim-hub package. Keep this outside
|
|
41
42
|
// src/cli/commands so the core fraim package has no Hub command implementation.
|
|
@@ -79,7 +80,7 @@ function openDesktopWindow(projectPath, preferredPort, runtimeId) {
|
|
|
79
80
|
const electronBinary = resolveElectronBinary();
|
|
80
81
|
const desktopEntry = resolveDesktopEntry();
|
|
81
82
|
if (!electronBinary || !desktopEntry) {
|
|
82
|
-
return
|
|
83
|
+
return null;
|
|
83
84
|
}
|
|
84
85
|
const args = projectPath
|
|
85
86
|
? [desktopEntry, '--project-path', projectPath, '--port', String(preferredPort)]
|
|
@@ -91,8 +92,7 @@ function openDesktopWindow(projectPath, preferredPort, runtimeId) {
|
|
|
91
92
|
detached: true,
|
|
92
93
|
stdio: 'ignore',
|
|
93
94
|
});
|
|
94
|
-
child
|
|
95
|
-
return true;
|
|
95
|
+
return child;
|
|
96
96
|
}
|
|
97
97
|
function openBrowser(url) {
|
|
98
98
|
if (process.platform === 'win32') {
|
|
@@ -207,6 +207,41 @@ function fetchRunningHubVersion(port) {
|
|
|
207
207
|
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
|
+
async function waitForDesktopHubReady(child, preferredPort, options = {}) {
|
|
211
|
+
const timeoutMs = options.timeoutMs ?? 15000;
|
|
212
|
+
const pollMs = options.pollMs ?? 250;
|
|
213
|
+
const runtimeId = options.runtimeId || 'hub';
|
|
214
|
+
const fraimDir = options.fraimDir || (0, project_fraim_paths_1.getUserFraimDirPath)();
|
|
215
|
+
const start = Date.now();
|
|
216
|
+
const childState = {};
|
|
217
|
+
child.once('exit', (code, signal) => {
|
|
218
|
+
childState.exit = { code, signal };
|
|
219
|
+
});
|
|
220
|
+
child.once('error', (error) => {
|
|
221
|
+
childState.error = error;
|
|
222
|
+
});
|
|
223
|
+
while (Date.now() - start < timeoutMs) {
|
|
224
|
+
if (childState.error) {
|
|
225
|
+
throw new Error(`FRAIM Hub desktop shell failed to launch: ${childState.error.message}`);
|
|
226
|
+
}
|
|
227
|
+
if (childState.exit) {
|
|
228
|
+
const detail = childState.exit.signal ? `signal ${childState.exit.signal}` : `exit code ${childState.exit.code ?? 'unknown'}`;
|
|
229
|
+
throw new Error(`FRAIM Hub desktop shell exited before the Hub became ready (${detail}).`);
|
|
230
|
+
}
|
|
231
|
+
const runtime = (0, hub_runtime_file_1.readHubRuntimeFile)(fraimDir, runtimeId);
|
|
232
|
+
const ports = runtime?.port && runtime.port !== preferredPort
|
|
233
|
+
? [runtime.port, preferredPort]
|
|
234
|
+
: [preferredPort];
|
|
235
|
+
for (const port of ports) {
|
|
236
|
+
const version = await fetchRunningHubVersion(port);
|
|
237
|
+
if (version) {
|
|
238
|
+
return { port, version };
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
242
|
+
}
|
|
243
|
+
throw new Error(`Timed out waiting for FRAIM Hub desktop shell to become ready on port ${preferredPort}.`);
|
|
244
|
+
}
|
|
210
245
|
async function reconcileRunningHub(flags, runtimeId = 'hub') {
|
|
211
246
|
const running = (0, hub_runtime_file_1.readHubRuntimeFile)((0, project_fraim_paths_1.getUserFraimDirPath)(), runtimeId);
|
|
212
247
|
const confirmedVersion = running ? await fetchRunningHubVersion(running.port) : null;
|
|
@@ -243,8 +278,8 @@ async function runHub(options) {
|
|
|
243
278
|
if (wantDesktop) {
|
|
244
279
|
await reconcileRunningHub({ restart: !!options.restart, keepRunning: !!options.keepRunning }, runtimeId);
|
|
245
280
|
}
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
281
|
+
const desktopChild = wantDesktop ? openDesktopWindow(projectPath, preferredPort, runtimeId) : null;
|
|
282
|
+
if (!desktopChild) {
|
|
248
283
|
const port = await findAvailablePort(preferredPort);
|
|
249
284
|
const server = new AiHubServer(projectPath ? { projectPath } : {});
|
|
250
285
|
await server.start(port);
|
|
@@ -254,7 +289,9 @@ async function runHub(options) {
|
|
|
254
289
|
openBrowser(url);
|
|
255
290
|
return;
|
|
256
291
|
}
|
|
257
|
-
|
|
292
|
+
const ready = await waitForDesktopHubReady(desktopChild, preferredPort, { runtimeId });
|
|
293
|
+
desktopChild.unref();
|
|
294
|
+
console.log(`AI Hub desktop shell launched at http://127.0.0.1:${ready.port}/ai-hub/ (v${ready.version}).`);
|
|
258
295
|
if (projectPath)
|
|
259
296
|
console.log(`Project path: ${projectPath}`);
|
|
260
297
|
else
|
|
@@ -17,21 +17,22 @@ exports.buildCustomEmployeePersona = buildCustomEmployeePersona;
|
|
|
17
17
|
// evaluatePersonaAccess) must never call resolveEmployee.
|
|
18
18
|
const fs_1 = __importDefault(require("fs"));
|
|
19
19
|
const path_1 = __importDefault(require("path"));
|
|
20
|
-
const
|
|
20
|
+
const pack_home_1 = require("../cli/utils/pack-home");
|
|
21
21
|
const persona_hiring_1 = require("../config/persona-hiring");
|
|
22
22
|
const EMPLOYEES_DIR_REL = path_1.default.join('fraim', 'personalized-employee', 'employees');
|
|
23
23
|
function employeesDir(projectDir) {
|
|
24
24
|
return path_1.default.join(projectDir, EMPLOYEES_DIR_REL);
|
|
25
25
|
}
|
|
26
|
-
// Issue #
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
function
|
|
31
|
-
return
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
// Issue #1043 (review round 2): every layer has exactly one home, named by
|
|
27
|
+
// config, and that home holds the whole layer. There are no caches, so an
|
|
28
|
+
// employee has exactly one authoritative location per level.
|
|
29
|
+
const EMPLOYEES_SUBDIR = 'employees';
|
|
30
|
+
function layerEmployeeReadDirs(layer) {
|
|
31
|
+
return (0, pack_home_1.packReadRoots)(layer).map((root) => path_1.default.join(root, EMPLOYEES_SUBDIR));
|
|
32
|
+
}
|
|
33
|
+
/** The single writable employees directory for a layer. */
|
|
34
|
+
function layerEmployeeWriteDir(layer) {
|
|
35
|
+
return path_1.default.join((0, pack_home_1.resolvePackHome)(layer).root, EMPLOYEES_SUBDIR);
|
|
35
36
|
}
|
|
36
37
|
function readEmployeesFromDir(dir, scope) {
|
|
37
38
|
if (!fs_1.default.existsSync(dir))
|
|
@@ -82,15 +83,23 @@ function safeSlug(key) {
|
|
|
82
83
|
* cache. Reading the manager level here is what makes the manager-level default
|
|
83
84
|
* usable: without it an employee created at the manager level would exist on
|
|
84
85
|
* disk and appear in no roster.
|
|
86
|
+
*
|
|
87
|
+
* Issue #1043 adds the org level at the bottom of that order, so an employee
|
|
88
|
+
* shared company-wide is visible to everyone, and a manager or project copy of
|
|
89
|
+
* the same key still takes precedence over it.
|
|
85
90
|
*/
|
|
86
91
|
function readCustomEmployees(projectDir) {
|
|
87
|
-
const dirs = managerEmployeesDirs();
|
|
88
92
|
const byKey = new Map();
|
|
89
93
|
// Lowest precedence first, so a later write overwrites on key collision.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
// packReadRoots is highest-precedence-first, so it is walked in reverse.
|
|
95
|
+
for (const dir of [...layerEmployeeReadDirs('org')].reverse()) {
|
|
96
|
+
for (const emp of readEmployeesFromDir(dir, 'org'))
|
|
97
|
+
byKey.set(emp.key, emp);
|
|
98
|
+
}
|
|
99
|
+
for (const dir of [...layerEmployeeReadDirs('manager')].reverse()) {
|
|
100
|
+
for (const emp of readEmployeesFromDir(dir, 'manager'))
|
|
101
|
+
byKey.set(emp.key, emp);
|
|
102
|
+
}
|
|
94
103
|
for (const emp of readEmployeesFromDir(employeesDir(projectDir), 'project'))
|
|
95
104
|
byKey.set(emp.key, emp);
|
|
96
105
|
return [...byKey.values()];
|
|
@@ -108,7 +117,9 @@ function writeCustomEmployee(projectDir, employee) {
|
|
|
108
117
|
const slug = safeSlug(record.key);
|
|
109
118
|
if (!slug)
|
|
110
119
|
throw new Error(`Invalid employee key: ${record.key}`);
|
|
111
|
-
|
|
120
|
+
// Issue #1043 (review round 1): the org level now has a real writable home,
|
|
121
|
+
// resolved from config, so an org-scope write no longer has to be refused.
|
|
122
|
+
const dir = scope === 'project' ? employeesDir(projectDir) : layerEmployeeWriteDir(scope);
|
|
112
123
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
113
124
|
fs_1.default.writeFileSync(path_1.default.join(dir, `${slug}.json`), JSON.stringify(record, null, 2), 'utf8');
|
|
114
125
|
}
|
|
@@ -121,11 +132,12 @@ function deleteCustomEmployee(projectDir, key) {
|
|
|
121
132
|
const slug = safeSlug(key);
|
|
122
133
|
if (!slug)
|
|
123
134
|
return false;
|
|
124
|
-
|
|
135
|
+
// Every place a copy could live, at every level. A delete that misses one
|
|
136
|
+
// reports success while the record is still listed from that level.
|
|
125
137
|
const candidates = [
|
|
126
138
|
path_1.default.join(employeesDir(projectDir), `${slug}.json`),
|
|
127
|
-
path_1.default.join(
|
|
128
|
-
path_1.default.join(
|
|
139
|
+
...layerEmployeeReadDirs('manager').map((d) => path_1.default.join(d, `${slug}.json`)),
|
|
140
|
+
...layerEmployeeReadDirs('org').map((d) => path_1.default.join(d, `${slug}.json`)),
|
|
129
141
|
];
|
|
130
142
|
let removed = false;
|
|
131
143
|
for (const fp of candidates) {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hostSessionState = exports.HostSessionState = void 0;
|
|
4
|
+
class HostSessionState {
|
|
5
|
+
keyFor(owner) {
|
|
6
|
+
const configuredAgentId = typeof owner.configuredAgentId === 'string' ? owner.configuredAgentId.trim() : '';
|
|
7
|
+
return configuredAgentId || `${owner.baseHostId}-default`;
|
|
8
|
+
}
|
|
9
|
+
applySession(target, owner, sessionId, options = {}) {
|
|
10
|
+
const normalizedSessionId = sessionId.trim();
|
|
11
|
+
if (!normalizedSessionId)
|
|
12
|
+
return;
|
|
13
|
+
const at = options.at || new Date().toISOString();
|
|
14
|
+
const key = this.keyFor(owner);
|
|
15
|
+
target.hostSessions = {
|
|
16
|
+
...(target.hostSessions || {}),
|
|
17
|
+
[key]: {
|
|
18
|
+
configuredAgentId: owner.configuredAgentId || null,
|
|
19
|
+
baseHostId: owner.baseHostId,
|
|
20
|
+
sessionId: normalizedSessionId,
|
|
21
|
+
status: options.status || 'valid',
|
|
22
|
+
sourceRunId: options.sourceRunId ?? target.id ?? null,
|
|
23
|
+
updatedAt: at,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
target.sessionId = normalizedSessionId;
|
|
27
|
+
}
|
|
28
|
+
mergeFromRun(record, run) {
|
|
29
|
+
const owner = {
|
|
30
|
+
configuredAgentId: run.configuredAgentId || record.configuredAgentId || null,
|
|
31
|
+
baseHostId: (run.baseHostId || run.hostId || record.baseHostId || record.agentName),
|
|
32
|
+
};
|
|
33
|
+
record.hostSessions = { ...(record.hostSessions || {}) };
|
|
34
|
+
for (const [key, session] of Object.entries(run.hostSessions || {})) {
|
|
35
|
+
record.hostSessions[key] = { ...session };
|
|
36
|
+
}
|
|
37
|
+
if (run.sessionId) {
|
|
38
|
+
const projectedRun = {
|
|
39
|
+
id: run.id,
|
|
40
|
+
sessionId: record.sessionId || undefined,
|
|
41
|
+
hostSessions: record.hostSessions,
|
|
42
|
+
};
|
|
43
|
+
this.applySession(projectedRun, owner, run.sessionId, { sourceRunId: run.id });
|
|
44
|
+
record.hostSessions = projectedRun.hostSessions;
|
|
45
|
+
}
|
|
46
|
+
const active = this.resolve(record, owner);
|
|
47
|
+
record.sessionId = active?.sessionId || run.sessionId || record.sessionId || null;
|
|
48
|
+
}
|
|
49
|
+
resolve(conversation, owner) {
|
|
50
|
+
if (!conversation)
|
|
51
|
+
return null;
|
|
52
|
+
const sessions = conversation.hostSessions || {};
|
|
53
|
+
const key = this.keyFor(owner);
|
|
54
|
+
const exact = sessions[key];
|
|
55
|
+
if (this.isResumableForOwner(exact, owner))
|
|
56
|
+
return exact;
|
|
57
|
+
const fallback = Object.values(sessions).find((session) => this.isResumableForOwner(session, owner));
|
|
58
|
+
if (fallback)
|
|
59
|
+
return fallback;
|
|
60
|
+
const legacySessionId = typeof conversation.sessionId === 'string' ? conversation.sessionId.trim() : '';
|
|
61
|
+
if (!legacySessionId)
|
|
62
|
+
return null;
|
|
63
|
+
return {
|
|
64
|
+
configuredAgentId: owner.configuredAgentId || null,
|
|
65
|
+
baseHostId: owner.baseHostId,
|
|
66
|
+
sessionId: legacySessionId,
|
|
67
|
+
status: 'suspect',
|
|
68
|
+
sourceRunId: typeof conversation.runId === 'string' ? conversation.runId : null,
|
|
69
|
+
updatedAt: typeof conversation.lastUpdatedAt === 'string' ? conversation.lastUpdatedAt : new Date().toISOString(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
markInvalid(conversation, owner, sessionId, reason, at = new Date().toISOString()) {
|
|
73
|
+
const normalizedSessionId = sessionId.trim();
|
|
74
|
+
if (!normalizedSessionId)
|
|
75
|
+
return;
|
|
76
|
+
const key = this.keyFor(owner);
|
|
77
|
+
const existing = conversation.hostSessions?.[key];
|
|
78
|
+
conversation.hostSessions = { ...(conversation.hostSessions || {}) };
|
|
79
|
+
conversation.hostSessions[key] = {
|
|
80
|
+
configuredAgentId: owner.configuredAgentId || existing?.configuredAgentId || null,
|
|
81
|
+
baseHostId: owner.baseHostId,
|
|
82
|
+
sessionId: normalizedSessionId,
|
|
83
|
+
status: 'invalid',
|
|
84
|
+
sourceRunId: existing?.sourceRunId || (typeof conversation.runId === 'string' ? conversation.runId : null),
|
|
85
|
+
updatedAt: existing?.updatedAt || at,
|
|
86
|
+
invalidatedAt: at,
|
|
87
|
+
invalidationReason: reason,
|
|
88
|
+
};
|
|
89
|
+
if (conversation.sessionId === normalizedSessionId) {
|
|
90
|
+
const replacement = this.resolve({ ...conversation, sessionId: null }, owner);
|
|
91
|
+
conversation.sessionId = replacement?.sessionId || null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
markInvalidRun(run, owner, sessionId, reason, at = new Date().toISOString()) {
|
|
95
|
+
const normalizedSessionId = sessionId.trim();
|
|
96
|
+
if (!normalizedSessionId)
|
|
97
|
+
return;
|
|
98
|
+
const key = this.keyFor(owner);
|
|
99
|
+
const existing = run.hostSessions?.[key];
|
|
100
|
+
run.hostSessions = { ...(run.hostSessions || {}) };
|
|
101
|
+
run.hostSessions[key] = {
|
|
102
|
+
configuredAgentId: owner.configuredAgentId || existing?.configuredAgentId || null,
|
|
103
|
+
baseHostId: owner.baseHostId,
|
|
104
|
+
sessionId: normalizedSessionId,
|
|
105
|
+
status: 'invalid',
|
|
106
|
+
sourceRunId: existing?.sourceRunId || run.id || null,
|
|
107
|
+
updatedAt: existing?.updatedAt || at,
|
|
108
|
+
invalidatedAt: at,
|
|
109
|
+
invalidationReason: reason,
|
|
110
|
+
};
|
|
111
|
+
if (run.sessionId === normalizedSessionId) {
|
|
112
|
+
run.sessionId = undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
hasInvalidSession(conversation, owner, sessionId) {
|
|
116
|
+
const normalizedSessionId = sessionId.trim();
|
|
117
|
+
if (!conversation || !normalizedSessionId)
|
|
118
|
+
return false;
|
|
119
|
+
const key = this.keyFor(owner);
|
|
120
|
+
const exact = conversation.hostSessions?.[key];
|
|
121
|
+
if (exact?.status === 'invalid' && exact.sessionId === normalizedSessionId)
|
|
122
|
+
return true;
|
|
123
|
+
return Object.values(conversation.hostSessions || {}).some((session) => (session.status === 'invalid'
|
|
124
|
+
&& session.sessionId === normalizedSessionId
|
|
125
|
+
&& session.baseHostId === owner.baseHostId));
|
|
126
|
+
}
|
|
127
|
+
isResumableForOwner(session, owner) {
|
|
128
|
+
if (!session || !session.sessionId || session.status === 'invalid')
|
|
129
|
+
return false;
|
|
130
|
+
if (session.baseHostId !== owner.baseHostId)
|
|
131
|
+
return false;
|
|
132
|
+
const requestedConfiguredAgentId = typeof owner.configuredAgentId === 'string' ? owner.configuredAgentId.trim() : '';
|
|
133
|
+
const sessionConfiguredAgentId = typeof session.configuredAgentId === 'string' ? session.configuredAgentId.trim() : '';
|
|
134
|
+
return !requestedConfiguredAgentId || !sessionConfiguredAgentId || requestedConfiguredAgentId === sessionConfiguredAgentId;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.HostSessionState = HostSessionState;
|
|
138
|
+
exports.hostSessionState = new HostSessionState();
|
package/dist/src/ai-hub/hosts.js
CHANGED
|
@@ -1578,7 +1578,10 @@ function parseHostLine(hostId, line) {
|
|
|
1578
1578
|
if (parsed.type === 'result') {
|
|
1579
1579
|
// Don't emit message — the 'assistant' event already captured the turn text.
|
|
1580
1580
|
// result carries usage data (parsed by parseUsageSignal above via withSignal).
|
|
1581
|
-
|
|
1581
|
+
// Claude result events can echo the requested resume id even when resume failed
|
|
1582
|
+
// with "No conversation found with session ID". Treat system events as the
|
|
1583
|
+
// authoritative session source instead of reinforcing a stale pointer.
|
|
1584
|
+
return withSignal({ raw: trimmed });
|
|
1582
1585
|
}
|
|
1583
1586
|
return withSignal({ raw: trimmed });
|
|
1584
1587
|
}
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Sideloads Office add-in manifests (Word + PowerPoint) so they appear under
|
|
4
4
|
* Insert > My Add-ins > Developer Add-ins without admin rights or AppSource.
|
|
5
5
|
*
|
|
6
|
-
* Windows: writes HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer
|
|
7
|
-
* ABSOLUTE FILE PATH to manifest.xml. This is
|
|
8
|
-
*
|
|
6
|
+
* Windows: writes HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer\{<guid>}
|
|
7
|
+
* (Default) = ABSOLUTE FILE PATH to manifest.xml. This is the registry
|
|
8
|
+
* shape Word resolves for Developer Add-ins. A URL value is for
|
|
9
9
|
* SharePoint/network-share catalogs and yields "catalog access denied"
|
|
10
10
|
* for a developer sideload — do NOT use a URL here.
|
|
11
11
|
* macOS: copies the manifest into each app's
|
|
@@ -23,6 +23,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
23
23
|
};
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
25
|
exports.manifestXmlForPort = manifestXmlForPort;
|
|
26
|
+
exports.winDeveloperManifestSubkey = winDeveloperManifestSubkey;
|
|
26
27
|
exports.isSideloaded = isSideloaded;
|
|
27
28
|
exports.sideloadManifest = sideloadManifest;
|
|
28
29
|
exports.removeSideload = removeSideload;
|
|
@@ -61,7 +62,7 @@ function generatedManifestPath(entry, userDataDir) {
|
|
|
61
62
|
return path_1.default.join(userDataDir, 'office-manifests', entry.guid, 'manifest.xml');
|
|
62
63
|
}
|
|
63
64
|
function manifestXmlForPort(xml, httpPort) {
|
|
64
|
-
return xml.replace(/
|
|
65
|
+
return xml.replace(/https?:\/\/(?:localhost|127\.0\.0\.1):43091/g, `http://127.0.0.1:${httpPort}`);
|
|
65
66
|
}
|
|
66
67
|
function prepareManifestForSideload(entry, sourcePath, options) {
|
|
67
68
|
if (!options.httpPort)
|
|
@@ -76,16 +77,46 @@ function prepareManifestForSideload(entry, sourcePath, options) {
|
|
|
76
77
|
// ---------------------------------------------------------------------------
|
|
77
78
|
// Windows registry helpers
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
|
-
function
|
|
80
|
+
function winDeveloperManifestSubkey(guid) {
|
|
81
|
+
return `${WEF_DEVELOPER_KEY}\\{${guid}}`;
|
|
82
|
+
}
|
|
83
|
+
function parseRegSzValue(stdout) {
|
|
84
|
+
const line = stdout.split(/\r?\n/).find(l => l.includes('REG_SZ'));
|
|
85
|
+
if (!line)
|
|
86
|
+
return null;
|
|
87
|
+
const idx = line.indexOf('REG_SZ');
|
|
88
|
+
return line.slice(idx + 'REG_SZ'.length).trim() || null;
|
|
89
|
+
}
|
|
90
|
+
function winRegisteredRootValue(guid) {
|
|
80
91
|
const r = (0, child_process_1.spawnSync)('reg', ['query', WEF_DEVELOPER_KEY, '/v', guid], { encoding: 'utf8' });
|
|
81
92
|
if (r.status !== 0 || !r.stdout.includes('REG_SZ'))
|
|
82
93
|
return null;
|
|
83
94
|
// Output line looks like: " <guid> REG_SZ C:\path\to\manifest.xml"
|
|
84
|
-
|
|
85
|
-
|
|
95
|
+
return parseRegSzValue(r.stdout);
|
|
96
|
+
}
|
|
97
|
+
function winRegisteredDefaultValue(guid) {
|
|
98
|
+
const r = (0, child_process_1.spawnSync)('reg', ['query', winDeveloperManifestSubkey(guid), '/ve'], { encoding: 'utf8' });
|
|
99
|
+
if (r.status !== 0 || !r.stdout.includes('REG_SZ'))
|
|
86
100
|
return null;
|
|
87
|
-
|
|
88
|
-
return
|
|
101
|
+
// Output line looks like: " (Default) REG_SZ C:\path\to\manifest.xml"
|
|
102
|
+
return parseRegSzValue(r.stdout);
|
|
103
|
+
}
|
|
104
|
+
function winRegisteredValue(guid) {
|
|
105
|
+
// Word resolves the braced GUID subkey. Prefer it even when a legacy root
|
|
106
|
+
// value exists, so a stale effective registration cannot be hidden.
|
|
107
|
+
return winRegisteredDefaultValue(guid) ?? winRegisteredRootValue(guid);
|
|
108
|
+
}
|
|
109
|
+
function winWriteRegisteredValue(guid, manifestPath) {
|
|
110
|
+
const r = (0, child_process_1.spawnSync)('reg', [
|
|
111
|
+
'add', winDeveloperManifestSubkey(guid),
|
|
112
|
+
'/ve', '/t', 'REG_SZ', '/d', manifestPath, '/f',
|
|
113
|
+
], { encoding: 'utf8' });
|
|
114
|
+
if (r.status !== 0)
|
|
115
|
+
return { ok: false, reason: r.stderr || `reg add failed for ${guid}` };
|
|
116
|
+
// Remove the older root-value registration if present. Office does not need
|
|
117
|
+
// it, and keeping two registration shapes makes stale-state diagnosis harder.
|
|
118
|
+
(0, child_process_1.spawnSync)('reg', ['delete', WEF_DEVELOPER_KEY, '/v', guid, '/f'], { encoding: 'utf8' });
|
|
119
|
+
return { ok: true };
|
|
89
120
|
}
|
|
90
121
|
// ---------------------------------------------------------------------------
|
|
91
122
|
// Public API
|
|
@@ -117,13 +148,10 @@ function sideloadManifest(projectPath, options = {}) {
|
|
|
117
148
|
}
|
|
118
149
|
const sideloadPath = prepareManifestForSideload(entry, manifestPath, options);
|
|
119
150
|
if (process.platform === 'win32') {
|
|
120
|
-
// Developer-
|
|
121
|
-
const r = (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
], { encoding: 'utf8' });
|
|
125
|
-
if (r.status !== 0)
|
|
126
|
-
return { ok: false, reason: r.stderr || `reg add failed for ${entry.guid}` };
|
|
151
|
+
// Developer add-in subkey default value = absolute file path to manifest.xml (NOT a URL).
|
|
152
|
+
const r = winWriteRegisteredValue(entry.guid, sideloadPath);
|
|
153
|
+
if (!r.ok)
|
|
154
|
+
return r;
|
|
127
155
|
continue;
|
|
128
156
|
}
|
|
129
157
|
if (process.platform === 'darwin') {
|
|
@@ -146,6 +174,7 @@ function removeSideload() {
|
|
|
146
174
|
for (const entry of MANIFESTS) {
|
|
147
175
|
if (process.platform === 'win32') {
|
|
148
176
|
(0, child_process_1.spawnSync)('reg', ['delete', WEF_DEVELOPER_KEY, '/v', entry.guid, '/f'], { encoding: 'utf8' });
|
|
177
|
+
(0, child_process_1.spawnSync)('reg', ['delete', winDeveloperManifestSubkey(entry.guid), '/f'], { encoding: 'utf8' });
|
|
149
178
|
}
|
|
150
179
|
else if (process.platform === 'darwin') {
|
|
151
180
|
const target = macWefPath(entry.macContainer, entry.guid);
|
|
@@ -112,16 +112,15 @@ function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
|
|
|
112
112
|
const byPath = new Map();
|
|
113
113
|
const currentCanonical = currentProjectPath ? canonicalProjectPath(currentProjectPath) : null;
|
|
114
114
|
const removedKeys = new Set(normalizeRemovedProjectPaths(options.removedProjectPaths));
|
|
115
|
-
const add = (entry
|
|
115
|
+
const add = (entry) => {
|
|
116
116
|
if (!entry)
|
|
117
117
|
return;
|
|
118
118
|
const displayPath = normalizeProjectPath(entry.folderPath);
|
|
119
119
|
const dedupKey = canonicalProjectPath(displayPath);
|
|
120
|
-
// Issue #
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
if (!isCurrent && removedKeys.has(dedupKey))
|
|
120
|
+
// Issue #1044: tombstoned paths are never shown, even when the path is the active
|
|
121
|
+
// project. The caller must switch to a different project (or clear projectPath) before
|
|
122
|
+
// deleting — the server now enforces this only when other projects remain (#719 reverted).
|
|
123
|
+
if (removedKeys.has(dedupKey))
|
|
125
124
|
return;
|
|
126
125
|
if (!options.includeMissing && dedupKey !== currentCanonical && !projectPathExists(displayPath))
|
|
127
126
|
return;
|
|
@@ -129,7 +128,7 @@ function normalizeAiHubProjectList(projects, currentProjectPath, options = {}) {
|
|
|
129
128
|
byPath.set(dedupKey, existing ? { ...existing, ...entry, folderPath: existing.folderPath } : { ...entry, folderPath: displayPath });
|
|
130
129
|
};
|
|
131
130
|
if (currentProjectPath)
|
|
132
|
-
add(normalizeProjectEntry({ folderPath: currentProjectPath }, currentProjectPath)
|
|
131
|
+
add(normalizeProjectEntry({ folderPath: currentProjectPath }, currentProjectPath));
|
|
133
132
|
for (const project of projects)
|
|
134
133
|
add(normalizeProjectEntry(project));
|
|
135
134
|
return withUniqueProjectIds(Array.from(byPath.values()));
|