fraim 2.0.159 → 2.0.160

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.
Files changed (30) hide show
  1. package/README.md +1 -1
  2. package/dist/src/ai-hub/cert-store.js +70 -0
  3. package/dist/src/ai-hub/desktop-main.js +225 -50
  4. package/dist/src/ai-hub/manager-turns.js +38 -0
  5. package/dist/src/ai-hub/office-sideload.js +138 -0
  6. package/dist/src/ai-hub/openclaw-bridge.js +239 -0
  7. package/dist/src/ai-hub/server.js +346 -115
  8. package/dist/src/ai-hub/word-sideload.js +95 -0
  9. package/dist/src/cli/commands/add-ide.js +9 -0
  10. package/dist/src/cli/commands/login.js +1 -2
  11. package/dist/src/cli/commands/setup.js +0 -2
  12. package/dist/src/cli/commands/sync.js +19 -10
  13. package/dist/src/cli/doctor/checks/mcp-connectivity-checks.js +66 -2
  14. package/dist/src/cli/doctor/checks/workflow-checks.js +1 -65
  15. package/dist/src/cli/mcp/fraim-mcp-latest-launcher.js +136 -0
  16. package/dist/src/cli/mcp/mcp-server-registry.js +14 -10
  17. package/dist/src/cli/setup/auto-mcp-setup.js +1 -1
  18. package/dist/src/cli/utils/fraim-gitignore.js +11 -0
  19. package/dist/src/cli/utils/remote-sync.js +1 -1
  20. package/dist/src/core/config-loader.js +1 -2
  21. package/dist/src/core/fraim-config-schema.generated.js +0 -5
  22. package/dist/src/core/types.js +0 -1
  23. package/dist/src/first-run/session-service.js +3 -3
  24. package/package.json +2 -1
  25. package/public/ai-hub/index.html +20 -2
  26. package/public/ai-hub/powerpoint-taskpane/icon-64.png +0 -0
  27. package/public/ai-hub/powerpoint-taskpane/index.html +235 -0
  28. package/public/ai-hub/powerpoint-taskpane/manifest.xml +30 -0
  29. package/public/ai-hub/script.js +337 -120
  30. package/public/ai-hub/styles.css +456 -135
package/README.md CHANGED
@@ -231,7 +231,7 @@ R - Retrospectives: Continuous learning from experience
231
231
 
232
232
  **Recommended: Use npx (no installation needed)**
233
233
  ```bash
234
- npx fraim-framework@latest setup --key=<your-fraim-key>
234
+ npx fraim@latest setup --key=<your-fraim-key>
235
235
 
236
236
  # Optional: Create alias for convenience
237
237
  echo 'alias fraim="npx fraim-framework"' >> ~/.bashrc
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadOrCreateCert = loadOrCreateCert;
7
+ exports.trustCert = trustCert;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const os_1 = __importDefault(require("os"));
11
+ const child_process_1 = require("child_process");
12
+ function certDir() {
13
+ const base = process.env.APPDATA ||
14
+ (process.platform === 'darwin'
15
+ ? path_1.default.join(os_1.default.homedir(), 'Library', 'Application Support')
16
+ : path_1.default.join(os_1.default.homedir(), '.config'));
17
+ return path_1.default.join(base, 'FRAIM', 'certs');
18
+ }
19
+ function certPaths() {
20
+ const dir = certDir();
21
+ return { keyPath: path_1.default.join(dir, 'localhost.key'), certPath: path_1.default.join(dir, 'localhost.crt') };
22
+ }
23
+ async function loadOrCreateCert() {
24
+ const { keyPath, certPath } = certPaths();
25
+ if (fs_1.default.existsSync(keyPath) && fs_1.default.existsSync(certPath)) {
26
+ return { key: fs_1.default.readFileSync(keyPath, 'utf8'), cert: fs_1.default.readFileSync(certPath, 'utf8') };
27
+ }
28
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
29
+ const selfsigned = require('selfsigned');
30
+ const pem = await selfsigned.generate([{ name: 'commonName', value: 'localhost' }], {
31
+ keySize: 2048,
32
+ days: 3650,
33
+ algorithm: 'sha256',
34
+ extensions: [
35
+ { name: 'subjectAltName', altNames: [{ type: 2, value: 'localhost' }, { type: 7, ip: '127.0.0.1' }] },
36
+ { name: 'keyUsage', keyCertSign: true, digitalSignature: true, keyEncipherment: true },
37
+ { name: 'extKeyUsage', serverAuth: true },
38
+ ],
39
+ });
40
+ fs_1.default.mkdirSync(path_1.default.dirname(keyPath), { recursive: true });
41
+ fs_1.default.writeFileSync(keyPath, pem.private, { mode: 0o600 });
42
+ fs_1.default.writeFileSync(certPath, pem.cert, { mode: 0o644 });
43
+ // NOTE: we deliberately do NOT install this into the OS trusted-root store here.
44
+ // Desktop Word/PowerPoint reach the pane over plain HTTP loopback and need no cert.
45
+ // Trusting a root CA triggers a Windows security popup on every add/remove and is
46
+ // invasive, so it is reserved for explicit Word-Online opt-in via trustCert() below.
47
+ return { key: pem.private, cert: pem.cert };
48
+ }
49
+ /**
50
+ * Installs the cert as a trusted root CA in the user's OS certificate store.
51
+ * ONLY call this for explicit Word-Online support — it prompts a Windows security
52
+ * dialog. It is idempotent: it no-ops if a matching localhost cert is already
53
+ * trusted, so it never produces the delete/add popup churn.
54
+ * Windows: certutil -addstore -user Root (skipped if already present)
55
+ * macOS: security add-trusted-cert -r trustRoot -k login.keychain
56
+ */
57
+ function trustCert(certPath) {
58
+ if (process.platform === 'win32') {
59
+ // Skip if a localhost cert is already trusted — avoids the confirmation popup.
60
+ const check = (0, child_process_1.spawnSync)('certutil', ['-user', '-store', 'Root', 'localhost'], { encoding: 'utf8' });
61
+ if (check.status === 0 && /localhost/i.test(check.stdout))
62
+ return;
63
+ (0, child_process_1.spawnSync)('certutil', ['-addstore', '-user', 'Root', certPath], { stdio: 'ignore' });
64
+ return;
65
+ }
66
+ if (process.platform === 'darwin') {
67
+ const keychain = path_1.default.join(os_1.default.homedir(), 'Library', 'Keychains', 'login.keychain-db');
68
+ (0, child_process_1.spawnSync)('security', ['add-trusted-cert', '-r', 'trustRoot', '-k', keychain, certPath], { stdio: 'ignore' });
69
+ }
70
+ }
@@ -1,9 +1,27 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.launchDesktopShell = launchDesktopShell;
4
7
  const electron_1 = require("electron");
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
5
10
  const server_1 = require("./server");
6
11
  const db_service_1 = require("../fraim/db-service");
12
+ const cert_store_1 = require("./cert-store");
13
+ const office_sideload_1 = require("./office-sideload");
14
+ // ---------------------------------------------------------------------------
15
+ // State
16
+ // ---------------------------------------------------------------------------
17
+ let server = null;
18
+ let mainWindow = null;
19
+ let tray = null;
20
+ let stopping = null;
21
+ let isQuitting = false; // distinguishes window-close (→ tray) from app-quit
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
7
25
  function tryCreateDbService() {
8
26
  try {
9
27
  return new db_service_1.FraimDbService();
@@ -12,102 +30,259 @@ function tryCreateDbService() {
12
30
  return undefined;
13
31
  }
14
32
  }
15
- let server = null;
16
- let mainWindow = null;
17
- let stopping = null;
18
33
  function preferredWindowSize() {
19
- const primaryDisplay = electron_1.screen.getPrimaryDisplay();
20
- const workArea = primaryDisplay.workAreaSize;
34
+ const { workAreaSize } = electron_1.screen.getPrimaryDisplay();
21
35
  return {
22
- width: Math.max(1440, Math.min(1680, workArea.width)),
23
- height: Math.max(980, Math.min(1180, workArea.height)),
36
+ width: Math.max(1440, Math.min(1680, workAreaSize.width)),
37
+ height: Math.max(980, Math.min(1180, workAreaSize.height)),
24
38
  };
25
39
  }
26
40
  function parseArgs(argv) {
27
41
  let projectPath = process.cwd();
28
42
  let preferredPort = 43091;
29
- for (let index = 0; index < argv.length; index += 1) {
30
- const value = argv[index];
31
- if (value === '--project-path' && argv[index + 1]) {
32
- projectPath = argv[index + 1];
33
- index += 1;
34
- continue;
43
+ for (let i = 0; i < argv.length; i += 1) {
44
+ if (argv[i] === '--project-path' && argv[i + 1]) {
45
+ projectPath = argv[i + 1];
46
+ i += 1;
35
47
  }
36
- if (value === '--port' && argv[index + 1]) {
37
- preferredPort = Number(argv[index + 1]) || preferredPort;
38
- index += 1;
48
+ if (argv[i] === '--port' && argv[i + 1]) {
49
+ preferredPort = Number(argv[i + 1]) || preferredPort;
50
+ i += 1;
39
51
  }
40
52
  }
41
53
  return { projectPath, preferredPort };
42
54
  }
43
- async function stopServer() {
44
- if (!server)
55
+ // ---------------------------------------------------------------------------
56
+ // Tray icon resolution — prefers bundled icon, falls back to a 1×1 empty image
57
+ // so the app never crashes if assets aren't present.
58
+ // ---------------------------------------------------------------------------
59
+ function resolveTrayIcon() {
60
+ const candidates = [
61
+ path_1.default.resolve(process.cwd(), 'extensions/office-word/icon-64.png'),
62
+ path_1.default.resolve(__dirname, '..', '..', 'extensions/office-word/icon-64.png'),
63
+ path_1.default.resolve(__dirname, '..', '..', '..', 'extensions/office-word/icon-64.png'),
64
+ ];
65
+ for (const c of candidates) {
66
+ if (fs_1.default.existsSync(c))
67
+ return electron_1.nativeImage.createFromPath(c).resize({ width: 16, height: 16 });
68
+ }
69
+ return electron_1.nativeImage.createEmpty();
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Login-item (auto-start on boot)
73
+ // ---------------------------------------------------------------------------
74
+ function ensureLoginItem() {
75
+ const flagPath = path_1.default.join(electron_1.app.getPath('userData'), 'login-item-set.flag');
76
+ if (fs_1.default.existsSync(flagPath))
45
77
  return;
46
- await server.stop();
47
- server = null;
78
+ electron_1.app.setLoginItemSettings({ openAtLogin: true, openAsHidden: true });
79
+ fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
80
+ fs_1.default.writeFileSync(flagPath, '1');
48
81
  }
49
- function stopServerOnce() {
50
- if (!stopping) {
51
- stopping = stopServer().finally(() => {
52
- stopping = null;
53
- });
82
+ // ---------------------------------------------------------------------------
83
+ // Word manifest sideload (runs once on first launch)
84
+ // ---------------------------------------------------------------------------
85
+ function ensureWordSideload(projectPath) {
86
+ // Flag version bump: bump this string when new manifests are added so all
87
+ // users get re-sideloaded on their next launch.
88
+ const FLAG_VERSION = 'v3-filepath';
89
+ const flagPath = path_1.default.join(electron_1.app.getPath('userData'), 'word-sideloaded.flag');
90
+ if (fs_1.default.existsSync(flagPath) && fs_1.default.readFileSync(flagPath, 'utf8').trim() === FLAG_VERSION)
91
+ return;
92
+ if (!(0, office_sideload_1.isSideloaded)()) {
93
+ const result = (0, office_sideload_1.sideloadManifest)(projectPath);
94
+ if (!result.ok) {
95
+ console.warn('[fraim] Office sideload skipped:', result.reason);
96
+ return; // don't write flag — retry next launch
97
+ }
54
98
  }
55
- return stopping;
99
+ fs_1.default.mkdirSync(path_1.default.dirname(flagPath), { recursive: true });
100
+ fs_1.default.writeFileSync(flagPath, FLAG_VERSION);
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Tray setup
104
+ // ---------------------------------------------------------------------------
105
+ function buildTrayMenu(hubUrl) {
106
+ return electron_1.Menu.buildFromTemplate([
107
+ {
108
+ label: 'Open FRAIM Hub',
109
+ click: () => {
110
+ if (mainWindow) {
111
+ mainWindow.show();
112
+ mainWindow.focus();
113
+ }
114
+ else {
115
+ void createWindow(hubUrl);
116
+ }
117
+ },
118
+ },
119
+ { type: 'separator' },
120
+ {
121
+ label: 'Word Add-in',
122
+ enabled: false,
123
+ sublabel: (0, office_sideload_1.isSideloaded)() ? 'Active in Word' : 'Not sideloaded',
124
+ },
125
+ { type: 'separator' },
126
+ {
127
+ label: 'Start at Login',
128
+ type: 'checkbox',
129
+ checked: electron_1.app.getLoginItemSettings().openAtLogin,
130
+ click: (item) => {
131
+ electron_1.app.setLoginItemSettings({ openAtLogin: item.checked, openAsHidden: true });
132
+ },
133
+ },
134
+ { type: 'separator' },
135
+ {
136
+ label: 'Quit FRAIM',
137
+ click: () => {
138
+ isQuitting = true;
139
+ electron_1.app.quit();
140
+ },
141
+ },
142
+ ]);
56
143
  }
144
+ function createTray(hubUrl) {
145
+ tray = new electron_1.Tray(resolveTrayIcon());
146
+ tray.setToolTip('FRAIM AI Hub');
147
+ tray.setContextMenu(buildTrayMenu(hubUrl));
148
+ tray.on('double-click', () => {
149
+ if (mainWindow) {
150
+ mainWindow.show();
151
+ mainWindow.focus();
152
+ }
153
+ else {
154
+ void createWindow(hubUrl);
155
+ }
156
+ });
157
+ }
158
+ // ---------------------------------------------------------------------------
159
+ // BrowserWindow
160
+ // ---------------------------------------------------------------------------
57
161
  async function createWindow(url) {
58
162
  const { width, height } = preferredWindowSize();
163
+ const isMac = process.platform === 'darwin';
164
+ const isWin = process.platform === 'win32';
59
165
  mainWindow = new electron_1.BrowserWindow({
60
- title: 'AI Hub',
166
+ title: 'FRAIM AI Hub',
61
167
  width,
62
168
  height,
63
- minWidth: 1400,
64
- minHeight: 960,
169
+ minWidth: 1200,
170
+ minHeight: 800,
65
171
  useContentSize: true,
66
172
  autoHideMenuBar: true,
67
- backgroundColor: '#f2f5fb',
173
+ backgroundColor: (isMac || isWin) ? '#00000000' : '#ECECEC',
174
+ titleBarStyle: isMac ? 'hiddenInset' : 'hidden',
175
+ ...(isWin && {
176
+ titleBarOverlay: { color: 'rgba(0,0,0,0)', symbolColor: '#1A1A1A', height: 36 },
177
+ }),
178
+ ...(isWin && { backgroundMaterial: 'mica' }),
179
+ ...(isMac && { vibrancy: 'sidebar' }),
68
180
  show: false,
69
- webPreferences: {
70
- contextIsolation: true,
71
- nodeIntegration: false,
72
- sandbox: true,
73
- },
181
+ webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
74
182
  });
75
183
  electron_1.Menu.setApplicationMenu(null);
76
184
  mainWindow.webContents.setWindowOpenHandler(({ url: targetUrl }) => {
77
185
  void electron_1.shell.openExternal(targetUrl);
78
186
  return { action: 'deny' };
79
187
  });
80
- mainWindow.once('ready-to-show', () => {
81
- mainWindow?.show();
82
- });
83
- mainWindow.on('closed', () => {
84
- mainWindow = null;
188
+ mainWindow.once('ready-to-show', () => mainWindow?.show());
189
+ // Closing the window hides it to the tray rather than quitting the app.
190
+ // The server keeps running so Word can still reach the task pane.
191
+ mainWindow.on('close', (event) => {
192
+ if (!isQuitting) {
193
+ event.preventDefault();
194
+ mainWindow?.hide();
195
+ }
85
196
  });
197
+ mainWindow.on('closed', () => { mainWindow = null; });
86
198
  await mainWindow.loadURL(url);
87
199
  mainWindow.webContents.setZoomFactor(1);
88
200
  }
201
+ // ---------------------------------------------------------------------------
202
+ // Server lifecycle
203
+ // ---------------------------------------------------------------------------
204
+ async function stopServer() {
205
+ if (!server)
206
+ return;
207
+ await server.stop();
208
+ server = null;
209
+ }
210
+ function stopServerOnce() {
211
+ if (!stopping) {
212
+ stopping = stopServer().finally(() => { stopping = null; });
213
+ }
214
+ return stopping;
215
+ }
216
+ // ---------------------------------------------------------------------------
217
+ // Launch
218
+ // ---------------------------------------------------------------------------
89
219
  async function launchDesktopShell(options) {
90
- const port = await (0, server_1.findAvailablePort)(options.preferredPort);
91
- server = new server_1.AiHubServer({ projectPath: options.projectPath, dbService: tryCreateDbService() });
92
- await server.start(port);
93
- await createWindow(`http://127.0.0.1:${port}/ai-hub/`);
220
+ const [httpPort, httpsPort] = await Promise.all([
221
+ (0, server_1.findAvailablePort)(options.preferredPort),
222
+ (0, server_1.findAvailablePort)(43092),
223
+ ]);
224
+ // Generate (or load cached) self-signed cert for HTTPS.
225
+ // Fast on subsequent launches (file read); ~200ms on first launch (key gen).
226
+ const certBundle = await (0, cert_store_1.loadOrCreateCert)();
227
+ server = new server_1.AiHubServer({
228
+ projectPath: options.projectPath,
229
+ dbService: tryCreateDbService(),
230
+ httpsPort,
231
+ certBundle,
232
+ folderPicker: async () => {
233
+ const result = await electron_1.dialog.showOpenDialog(mainWindow, {
234
+ title: 'Select a FRAIM project folder',
235
+ properties: ['openDirectory', 'createDirectory'],
236
+ buttonLabel: 'Select Folder',
237
+ });
238
+ return result.canceled || result.filePaths.length === 0 ? null : result.filePaths[0];
239
+ },
240
+ });
241
+ await server.start(httpPort);
242
+ ensureWordSideload(options.projectPath);
243
+ const hubUrl = `http://127.0.0.1:${httpPort}/ai-hub/`;
244
+ createTray(hubUrl);
245
+ await createWindow(hubUrl);
94
246
  }
247
+ // ---------------------------------------------------------------------------
248
+ // Bootstrap
249
+ // ---------------------------------------------------------------------------
95
250
  async function bootstrap() {
96
251
  const options = parseArgs(process.argv.slice(2));
252
+ // Single-instance lock — if another instance is already running, focus it
253
+ // and exit rather than spawning a second server + window.
254
+ const gotLock = electron_1.app.requestSingleInstanceLock();
255
+ if (!gotLock) {
256
+ electron_1.app.quit();
257
+ return;
258
+ }
259
+ electron_1.app.on('second-instance', () => {
260
+ if (mainWindow) {
261
+ mainWindow.show();
262
+ mainWindow.focus();
263
+ }
264
+ });
97
265
  await electron_1.app.whenReady();
98
- electron_1.app.setName('AI Hub');
266
+ electron_1.app.setName('FRAIM Hub');
267
+ // First-launch housekeeping (idempotent, fast on subsequent runs)
268
+ ensureLoginItem();
99
269
  electron_1.app.on('activate', () => {
100
- if (electron_1.BrowserWindow.getAllWindows().length === 0) {
101
- void launchDesktopShell(options);
270
+ // macOS: clicking dock icon re-shows the window
271
+ if (mainWindow) {
272
+ mainWindow.show();
273
+ mainWindow.focus();
102
274
  }
103
275
  });
104
276
  electron_1.app.on('before-quit', () => {
277
+ isQuitting = true;
105
278
  void stopServerOnce();
106
279
  });
280
+ // Keep app alive when all windows are closed — server must keep serving
281
+ // for Word to reach the task pane. Only quit on explicit tray → Quit.
107
282
  electron_1.app.on('window-all-closed', () => {
108
- void stopServerOnce().finally(() => {
109
- electron_1.app.quit();
110
- });
283
+ if (process.platform !== 'darwin' && isQuitting) {
284
+ void stopServerOnce().finally(() => electron_1.app.quit());
285
+ }
111
286
  });
112
287
  await launchDesktopShell(options);
113
288
  }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractExplicitFraimInvocation = extractExplicitFraimInvocation;
4
+ exports.fraimInvocationFor = fraimInvocationFor;
5
+ exports.buildManagerMessage = buildManagerMessage;
6
+ function extractExplicitFraimInvocation(text) {
7
+ const raw = String(text || '');
8
+ const match = raw.match(/(?:^|\n|\s)([$/]fraim)\s+([a-z0-9][a-z0-9-]*)(?=\s|$)/i);
9
+ if (!match || match.index == null)
10
+ return null;
11
+ const before = raw.slice(0, match.index).trim();
12
+ const after = raw.slice(match.index + match[0].length).trim();
13
+ const remainder = [before, after].filter(Boolean).join('\n\n').trim();
14
+ return {
15
+ symbol: match[1].toLowerCase() === '$fraim' ? '$fraim' : '/fraim',
16
+ jobId: match[2].toLowerCase(),
17
+ remainder,
18
+ };
19
+ }
20
+ function fraimInvocationFor(employeeId, jobId) {
21
+ if (jobId === '__freeform__')
22
+ return null;
23
+ const symbol = employeeId === 'codex' ? '$fraim' : '/fraim';
24
+ return `${symbol} ${jobId}`;
25
+ }
26
+ function buildManagerMessage(employeeId, jobId, kind, instructions, stubPath) {
27
+ const trimmed = String(instructions || '').trim();
28
+ const explicit = extractExplicitFraimInvocation(trimmed);
29
+ const effectiveJobId = explicit?.jobId || jobId;
30
+ const invocation = fraimInvocationFor(employeeId, effectiveJobId);
31
+ if (!invocation)
32
+ return explicit?.remainder || trimmed;
33
+ const remainder = explicit ? explicit.remainder : trimmed;
34
+ const stub = (kind === 'start' && stubPath) ? `\n[Job stub: ${stubPath}]` : '';
35
+ if (!remainder)
36
+ return `${invocation}${stub}`;
37
+ return `${invocation}${stub}\n\n${remainder}`;
38
+ }
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ /**
3
+ * Sideloads Office add-in manifests (Word + PowerPoint) so they appear under
4
+ * Insert > My Add-ins > Developer Add-ins without admin rights or AppSource.
5
+ *
6
+ * Windows: writes HKCU\SOFTWARE\Microsoft\Office\16.0\WEF\Developer\<guid> =
7
+ * ABSOLUTE FILE PATH to manifest.xml. This is exactly what Microsoft's
8
+ * `office-addin-dev-settings register` writes. A URL value is for
9
+ * SharePoint/network-share catalogs and yields "catalog access denied"
10
+ * for a developer sideload — do NOT use a URL here.
11
+ * macOS: copies the manifest into each app's
12
+ * ~/Library/Containers/<bundle>/Data/Documents/wef/<guid>.xml
13
+ *
14
+ * NOTE: the manifest XML itself must be ASCII-clean (no smart quotes, em/en
15
+ * dashes, or BOM) or Office's parser hard-fails with -1072894428 and the pane
16
+ * silently never appears. See project_rules.md.
17
+ *
18
+ * Both paths are non-admin and survive app updates (keyed by manifest GUID).
19
+ * Safe to call multiple times — idempotent.
20
+ */
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.isSideloaded = isSideloaded;
26
+ exports.sideloadManifest = sideloadManifest;
27
+ exports.removeSideload = removeSideload;
28
+ const fs_1 = __importDefault(require("fs"));
29
+ const path_1 = __importDefault(require("path"));
30
+ const os_1 = __importDefault(require("os"));
31
+ const child_process_1 = require("child_process");
32
+ const WEF_DEVELOPER_KEY = 'HKCU\\SOFTWARE\\Microsoft\\Office\\16.0\\WEF\\Developer';
33
+ const MANIFESTS = [
34
+ {
35
+ guid: 'd1090951-50cf-4cf2-9d12-b0f8541d265c',
36
+ candidates: (base) => [
37
+ path_1.default.resolve(base, 'extensions/office-word/manifest.xml'),
38
+ path_1.default.resolve(__dirname, '..', '..', 'extensions/office-word/manifest.xml'),
39
+ path_1.default.resolve(__dirname, '..', '..', '..', 'extensions/office-word/manifest.xml'),
40
+ ],
41
+ macContainer: 'com.microsoft.Word',
42
+ },
43
+ {
44
+ guid: 'e7a3c812-91fd-4b2e-8c15-d4f6a903b71e',
45
+ candidates: (base) => [
46
+ path_1.default.resolve(base, 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
47
+ path_1.default.resolve(__dirname, '..', '..', 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
48
+ path_1.default.resolve(__dirname, '..', '..', '..', 'public/ai-hub/powerpoint-taskpane/manifest.xml'),
49
+ ],
50
+ macContainer: 'com.microsoft.Powerpoint',
51
+ },
52
+ ];
53
+ function resolveManifestPath(entry, projectPath) {
54
+ return entry.candidates(projectPath).find(c => fs_1.default.existsSync(c)) ?? null;
55
+ }
56
+ function macWefPath(container, guid) {
57
+ return path_1.default.join(os_1.default.homedir(), 'Library', 'Containers', container, 'Data', 'Documents', 'wef', `${guid}.xml`);
58
+ }
59
+ // ---------------------------------------------------------------------------
60
+ // Windows registry helpers
61
+ // ---------------------------------------------------------------------------
62
+ function winRegisteredValue(guid) {
63
+ const r = (0, child_process_1.spawnSync)('reg', ['query', WEF_DEVELOPER_KEY, '/v', guid], { encoding: 'utf8' });
64
+ if (r.status !== 0 || !r.stdout.includes('REG_SZ'))
65
+ return null;
66
+ // Output line looks like: " <guid> REG_SZ C:\path\to\manifest.xml"
67
+ const line = r.stdout.split(/\r?\n/).find(l => l.includes('REG_SZ'));
68
+ if (!line)
69
+ return null;
70
+ const idx = line.indexOf('REG_SZ');
71
+ return line.slice(idx + 'REG_SZ'.length).trim() || null;
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Public API
75
+ // ---------------------------------------------------------------------------
76
+ /** Returns true when ALL manifests are sideloaded with the correct value. */
77
+ function isSideloaded() {
78
+ return MANIFESTS.every(m => isManifestSideloaded(m));
79
+ }
80
+ function isManifestSideloaded(m) {
81
+ if (process.platform === 'win32') {
82
+ const value = winRegisteredValue(m.guid);
83
+ // Must be a real file path that exists — a URL or a stale/missing path
84
+ // means we have NOT correctly sideloaded and should re-register.
85
+ return !!value && !/^https?:\/\//i.test(value) && fs_1.default.existsSync(value);
86
+ }
87
+ if (process.platform === 'darwin')
88
+ return fs_1.default.existsSync(macWefPath(m.macContainer, m.guid));
89
+ return false;
90
+ }
91
+ /**
92
+ * Sideloads all manifests. Returns ok:true if every manifest was registered.
93
+ * Returns ok:false with the first failure reason.
94
+ */
95
+ function sideloadManifest(projectPath) {
96
+ for (const entry of MANIFESTS) {
97
+ const manifestPath = resolveManifestPath(entry, projectPath);
98
+ if (!manifestPath) {
99
+ return { ok: false, reason: `Manifest not found for GUID ${entry.guid} — check extensions/office-word/ and public/ai-hub/powerpoint-taskpane/` };
100
+ }
101
+ if (process.platform === 'win32') {
102
+ // Developer-key value = absolute file path to manifest.xml (NOT a URL).
103
+ const r = (0, child_process_1.spawnSync)('reg', [
104
+ 'add', WEF_DEVELOPER_KEY,
105
+ '/v', entry.guid, '/t', 'REG_SZ', '/d', manifestPath, '/f',
106
+ ], { encoding: 'utf8' });
107
+ if (r.status !== 0)
108
+ return { ok: false, reason: r.stderr || `reg add failed for ${entry.guid}` };
109
+ continue;
110
+ }
111
+ if (process.platform === 'darwin') {
112
+ const dest = macWefPath(entry.macContainer, entry.guid);
113
+ try {
114
+ fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true });
115
+ fs_1.default.copyFileSync(manifestPath, dest);
116
+ }
117
+ catch (err) {
118
+ return { ok: false, reason: String(err) };
119
+ }
120
+ continue;
121
+ }
122
+ return { ok: false, reason: `Unsupported platform: ${process.platform}` };
123
+ }
124
+ return { ok: true };
125
+ }
126
+ /** Removes all sideloaded manifests. */
127
+ function removeSideload() {
128
+ for (const entry of MANIFESTS) {
129
+ if (process.platform === 'win32') {
130
+ (0, child_process_1.spawnSync)('reg', ['delete', WEF_DEVELOPER_KEY, '/v', entry.guid, '/f'], { encoding: 'utf8' });
131
+ }
132
+ else if (process.platform === 'darwin') {
133
+ const target = macWefPath(entry.macContainer, entry.guid);
134
+ if (fs_1.default.existsSync(target))
135
+ fs_1.default.unlinkSync(target);
136
+ }
137
+ }
138
+ }