documentation-hub 5.7.2

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 (271) hide show
  1. package/.eslintrc.json +43 -0
  2. package/.github/workflows/build.yml +64 -0
  3. package/.github/workflows/ci.yml +39 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/Current.md +97 -0
  6. package/DocHub_Image.png +0 -0
  7. package/README.md +666 -0
  8. package/USER_GUIDE.md +1173 -0
  9. package/Updater.md +311 -0
  10. package/build/256x256.png +0 -0
  11. package/build/512x512.png +0 -0
  12. package/build/app-update.yml +4 -0
  13. package/build/create-icon.js +208 -0
  14. package/build/icon.ico +0 -0
  15. package/build/icon.png +0 -0
  16. package/build/icon_1024x1024.png +0 -0
  17. package/dist/assets/Analytics-BpsG9895.js +1 -0
  18. package/dist/assets/Card-IAZin8kp.js +1 -0
  19. package/dist/assets/CurrentSession-B-rFkHvf.js +12 -0
  20. package/dist/assets/Dashboard-C_5gMb0q.js +1 -0
  21. package/dist/assets/Documents-CqZ25axS.js +1 -0
  22. package/dist/assets/Input-l89xwXBi.js +1 -0
  23. package/dist/assets/Reporting-DqdHJY_a.js +1 -0
  24. package/dist/assets/Search-XNbu5z_3.js +1 -0
  25. package/dist/assets/SessionManager-lH9hZfzH.js +1 -0
  26. package/dist/assets/Sessions-ClZOPYNc.js +1 -0
  27. package/dist/assets/Settings-DUEHGURa.js +11 -0
  28. package/dist/assets/index-8xUe8ptc.js +24 -0
  29. package/dist/assets/index-RYyJqF7O.css +1 -0
  30. package/dist/assets/path-BkOl0AGO.js +1 -0
  31. package/dist/assets/promises-ID_B9S-h.js +1 -0
  32. package/dist/assets/urlHelpers-TvgahX0r.js +1 -0
  33. package/dist/assets/useToast-yRSO1dkm.js +1 -0
  34. package/dist/assets/vendor-charts-RkGK5ROP.js +36 -0
  35. package/dist/assets/vendor-db-l0sNRNKZ.js +1 -0
  36. package/dist/assets/vendor-react-BVZ_anCF.js +4 -0
  37. package/dist/assets/vendor-search-Dw8P0qyA.js +1 -0
  38. package/dist/assets/vendor-ui-BU7NfluV.js +53 -0
  39. package/dist/electron/PowerAutomateApiService-LfW09ZGr.js +147 -0
  40. package/dist/electron/main-CXkNtyv-.js +19789 -0
  41. package/dist/electron/main.js +5 -0
  42. package/dist/electron/preload.js +1 -0
  43. package/dist/icon.png +0 -0
  44. package/dist/index.html +27 -0
  45. package/docs/CODEBASE_ANALYSIS_REPORT.md +309 -0
  46. package/docs/DEBUG_LOGGING_GUIDE.md +244 -0
  47. package/docs/README.md +115 -0
  48. package/docs/TOC_WIRING_GUIDE.md +344 -0
  49. package/docs/analysis/Bullet_Symbol_Bug_Analysis.md +136 -0
  50. package/docs/analysis/DOCXMLATER_ANALYSIS_SUMMARY.txt +169 -0
  51. package/docs/analysis/Document_Processing_Issues_Analysis.md +704 -0
  52. package/docs/analysis/FIELD_PRESERVATION_ANALYSIS.md +1200 -0
  53. package/docs/analysis/INDENTATION_PRESERVE_ANALYSIS.md +181 -0
  54. package/docs/analysis/INDENTATION_PRESERVE_IMPLEMENTATION.md +207 -0
  55. package/docs/analysis/List_Implementation.md +206 -0
  56. package/docs/analysis/List_Implementation_Accuracy_Report.md +366 -0
  57. package/docs/analysis/PROCESSING_OPTIONS_UI_UPDATES.md +220 -0
  58. package/docs/analysis/RefactorStyles.md +852 -0
  59. package/docs/analysis/STYLE_PARAMETER_ENHANCEMENT.md +143 -0
  60. package/docs/analysis/docxmlater-comparison-todo-2025-11-13.md +636 -0
  61. package/docs/analysis/docxmlater-implementation-analysis-2025-11-13.md +340 -0
  62. package/docs/analysis/docxmlater-template_ui-integration-analysis.md +263 -0
  63. package/docs/analysis/github-issues-to-create.md +237 -0
  64. package/docs/api/API_README.md +538 -0
  65. package/docs/api/API_REFERENCE.md +751 -0
  66. package/docs/api/TYPE_DEFINITIONS.md +869 -0
  67. package/docs/architecture/FONT_EMBEDDING_GUIDE.md +318 -0
  68. package/docs/architecture/docxmlater-functions-and-structure.md +726 -0
  69. package/docs/docxmlater-readme.md +1341 -0
  70. package/docs/fixes/EXECUTION_LOG_TEST_BASE.md +573 -0
  71. package/docs/fixes/HYPERLINK_TEXT_SANITIZATION.md +253 -0
  72. package/docs/fixes/README.md +37 -0
  73. package/docs/github-issues/issue-1-body.md +125 -0
  74. package/docs/github-issues/issue-10-body.md +850 -0
  75. package/docs/github-issues/issue-2-body.md +200 -0
  76. package/docs/github-issues/issue-3-body.md +270 -0
  77. package/docs/github-issues/issue-4-body.md +169 -0
  78. package/docs/github-issues/issue-5-body.md +173 -0
  79. package/docs/github-issues/issue-6-body.md +158 -0
  80. package/docs/github-issues/issue-7-body.md +171 -0
  81. package/docs/github-issues/issue-8-body.md +407 -0
  82. package/docs/github-issues/issue-9-body.md +515 -0
  83. package/docs/github-issues/issue-tracker.md +274 -0
  84. package/docs/github-issues/predictive-analysis-2025-10-18.md +2131 -0
  85. package/docs/implementation/List_Framework_Refactor_Plan.md +336 -0
  86. package/docs/implementation/PRIMARY_TEXT_COLOR_FEATURE.md +217 -0
  87. package/docs/implementation/RELEASE_PLAN_v2.1.0.md +362 -0
  88. package/docs/implementation/RefactorStyles.md +588 -0
  89. package/docs/implementation/implement-plan.md +489 -0
  90. package/docs/implementation/missing-helpers-implementation.md +391 -0
  91. package/docs/implementation/refactor-plan.md +520 -0
  92. package/docs/implementation/session-implementation-complete.md +233 -0
  93. package/docs/implementation/session-management-plan.md +250 -0
  94. package/docs/setup-checklist.md +77 -0
  95. package/docs/versions/changelog.md +345 -0
  96. package/electron/customUpdater.ts +656 -0
  97. package/electron/main.ts +2441 -0
  98. package/electron/memoryConfig.ts +187 -0
  99. package/electron/preload.ts +394 -0
  100. package/electron/proxyConfig.ts +340 -0
  101. package/electron/services/BackupService.ts +452 -0
  102. package/electron/services/DictionaryService.ts +402 -0
  103. package/electron/services/LocalDictionaryLookupService.ts +147 -0
  104. package/electron/services/PowerAutomateApiService.ts +231 -0
  105. package/electron/services/SharePointSyncService.ts +474 -0
  106. package/electron/windowsCertStore.ts +427 -0
  107. package/electron/zscalerConfig.ts +381 -0
  108. package/eslint.config.js +92 -0
  109. package/jest.config.js +52 -0
  110. package/package.json +214 -0
  111. package/postcss.config.mjs +6 -0
  112. package/public/icon.png +0 -0
  113. package/publish-release.ps1 +5 -0
  114. package/renovate.json +30 -0
  115. package/src/App.tsx +216 -0
  116. package/src/__mocks__/p-limit.js +12 -0
  117. package/src/__mocks__/styleMock.js +1 -0
  118. package/src/components/common/BugReportButton.tsx +44 -0
  119. package/src/components/common/BugReportDialog.tsx +193 -0
  120. package/src/components/common/Button.tsx +153 -0
  121. package/src/components/common/Card.tsx +86 -0
  122. package/src/components/common/ColorPickerDialog.tsx +177 -0
  123. package/src/components/common/ConfirmDialog.tsx +96 -0
  124. package/src/components/common/DebugConsole.tsx +275 -0
  125. package/src/components/common/EmptyState.tsx +183 -0
  126. package/src/components/common/ErrorBoundary.tsx +98 -0
  127. package/src/components/common/ErrorDetailsDialog.tsx +153 -0
  128. package/src/components/common/ErrorFallback.tsx +218 -0
  129. package/src/components/common/Input.tsx +109 -0
  130. package/src/components/common/Skeleton.tsx +184 -0
  131. package/src/components/common/SplashScreen.tsx +81 -0
  132. package/src/components/common/Toast.tsx +155 -0
  133. package/src/components/common/Tooltip.tsx +79 -0
  134. package/src/components/common/UpdateNotification.tsx +320 -0
  135. package/src/components/comparison/ComparisonWindow.tsx +374 -0
  136. package/src/components/comparison/SideBySideDiff.tsx +486 -0
  137. package/src/components/comparison/index.ts +8 -0
  138. package/src/components/document/DocumentUploader.tsx +288 -0
  139. package/src/components/document/HyperlinkPreview.tsx +430 -0
  140. package/src/components/document/HyperlinkService.md +1484 -0
  141. package/src/components/document/Hyperlink_Technical_Documentation.md +496 -0
  142. package/src/components/document/InlineChangesView.tsx +707 -0
  143. package/src/components/document/ProcessingProgress.tsx +303 -0
  144. package/src/components/document/ProcessingResults.tsx +256 -0
  145. package/src/components/document/TrackedChangesDetail.tsx +530 -0
  146. package/src/components/document/TrackedChangesPanel.tsx +546 -0
  147. package/src/components/document/VirtualDocumentList.tsx +240 -0
  148. package/src/components/editor/DocumentEditor.tsx +723 -0
  149. package/src/components/editor/DocumentEditorModal.tsx +640 -0
  150. package/src/components/editor/EditorQuickActions.tsx +502 -0
  151. package/src/components/editor/EditorToolbar.tsx +312 -0
  152. package/src/components/editor/TableEditor.tsx +926 -0
  153. package/src/components/editor/index.ts +18 -0
  154. package/src/components/layout/Header.tsx +190 -0
  155. package/src/components/layout/Sidebar.tsx +313 -0
  156. package/src/components/layout/TitleBar.tsx +190 -0
  157. package/src/components/navigation/CommandPalette.tsx +233 -0
  158. package/src/components/navigation/KeyboardShortcutsModal.tsx +173 -0
  159. package/src/components/sessions/ChangeItem.tsx +408 -0
  160. package/src/components/sessions/ChangeViewer.tsx +1155 -0
  161. package/src/components/sessions/DocumentComparisonModal.tsx +314 -0
  162. package/src/components/sessions/ProcessingOptions.tsx +297 -0
  163. package/src/components/sessions/ReplacementsTab.tsx +438 -0
  164. package/src/components/sessions/RevisionHandlingOptions.tsx +87 -0
  165. package/src/components/sessions/SessionManager.tsx +188 -0
  166. package/src/components/sessions/StylesEditor.tsx +1335 -0
  167. package/src/components/sessions/TabContainer.tsx +151 -0
  168. package/src/components/sessions/VirtualSessionList.tsx +157 -0
  169. package/src/components/sessions/sessionToProcessorManager.tsx +420 -0
  170. package/src/components/settings/CertificateManager.tsx +410 -0
  171. package/src/components/settings/SegmentedControl.tsx +88 -0
  172. package/src/components/settings/SettingRow.tsx +52 -0
  173. package/src/contexts/GlobalStatsContext.tsx +396 -0
  174. package/src/contexts/SessionContext.tsx +2129 -0
  175. package/src/contexts/ThemeContext.tsx +428 -0
  176. package/src/contexts/UserSettingsContext.tsx +290 -0
  177. package/src/contexts/__tests__/GlobalStatsContext.test.tsx +390 -0
  178. package/src/global.d.ts +273 -0
  179. package/src/hooks/useDocumentQueue.tsx +210 -0
  180. package/src/hooks/useToast.tsx +55 -0
  181. package/src/main.tsx +10 -0
  182. package/src/pages/Analytics.tsx +386 -0
  183. package/src/pages/CurrentSession.tsx +1174 -0
  184. package/src/pages/Dashboard.tsx +319 -0
  185. package/src/pages/Documents.tsx +317 -0
  186. package/src/pages/Projects.tsx +250 -0
  187. package/src/pages/Reporting.tsx +386 -0
  188. package/src/pages/Search.tsx +349 -0
  189. package/src/pages/Sessions.tsx +285 -0
  190. package/src/pages/Settings.tsx +2662 -0
  191. package/src/services/HyperlinkService.ts +1085 -0
  192. package/src/services/document/DocXMLaterProcessor.ts +617 -0
  193. package/src/services/document/DocumentProcessingComparison.ts +856 -0
  194. package/src/services/document/DocumentSnapshotService.ts +575 -0
  195. package/src/services/document/WordDocumentProcessor.ts +10509 -0
  196. package/src/services/document/__tests__/DocXMLaterProcessor.hyperlinks.test.md +311 -0
  197. package/src/services/document/__tests__/WordDocumentProcessor.integration.test.ts +515 -0
  198. package/src/services/document/__tests__/WordDocumentProcessor.test.ts +812 -0
  199. package/src/services/document/blanklines/BlankLineManager.ts +658 -0
  200. package/src/services/document/blanklines/__tests__/paragraphChecks.test.ts +281 -0
  201. package/src/services/document/blanklines/helpers/blankLineInsertion.ts +87 -0
  202. package/src/services/document/blanklines/helpers/blankLineSnapshot.ts +251 -0
  203. package/src/services/document/blanklines/helpers/clearCustom.ts +121 -0
  204. package/src/services/document/blanklines/helpers/contextChecks.ts +117 -0
  205. package/src/services/document/blanklines/helpers/imageChecks.ts +51 -0
  206. package/src/services/document/blanklines/helpers/paragraphChecks.ts +236 -0
  207. package/src/services/document/blanklines/helpers/removeBlanksBetweenListItems.ts +91 -0
  208. package/src/services/document/blanklines/helpers/removeTrailingBlanks.ts +35 -0
  209. package/src/services/document/blanklines/helpers/tableGuards.ts +21 -0
  210. package/src/services/document/blanklines/index.ts +67 -0
  211. package/src/services/document/blanklines/rules/additionRules.ts +337 -0
  212. package/src/services/document/blanklines/rules/indentationRules.ts +317 -0
  213. package/src/services/document/blanklines/rules/removalRules.ts +362 -0
  214. package/src/services/document/blanklines/rules/ruleTypes.ts +92 -0
  215. package/src/services/document/blanklines/types.ts +29 -0
  216. package/src/services/document/helpers/ImageBorderCropper.ts +377 -0
  217. package/src/services/document/helpers/__tests__/whitespace.test.ts +272 -0
  218. package/src/services/document/helpers/whitespace.ts +117 -0
  219. package/src/services/document/list/ListNormalizer.ts +947 -0
  220. package/src/services/document/list/index.ts +45 -0
  221. package/src/services/document/list/list-detection.ts +275 -0
  222. package/src/services/document/list/list-types.ts +162 -0
  223. package/src/services/document/processors/HyperlinkProcessor.ts +370 -0
  224. package/src/services/document/processors/ListProcessor.ts +257 -0
  225. package/src/services/document/processors/StructureProcessor.ts +176 -0
  226. package/src/services/document/processors/StyleProcessor.ts +389 -0
  227. package/src/services/document/processors/TableProcessor.ts +2238 -0
  228. package/src/services/document/processors/__tests__/HyperlinkProcessor.test.ts +314 -0
  229. package/src/services/document/processors/__tests__/ListProcessor.test.ts +291 -0
  230. package/src/services/document/processors/__tests__/StructureProcessor.test.ts +257 -0
  231. package/src/services/document/processors/__tests__/TableProcessor.hlp-tips-bullets.test.ts +459 -0
  232. package/src/services/document/processors/__tests__/TableProcessor.test.ts +1604 -0
  233. package/src/services/document/processors/index.ts +28 -0
  234. package/src/services/document/types/docx-processing.ts +310 -0
  235. package/src/services/editor/EditorActionHandlers.ts +901 -0
  236. package/src/services/editor/index.ts +13 -0
  237. package/src/setupTests.ts +47 -0
  238. package/src/styles/global.css +782 -0
  239. package/src/types/backup.ts +132 -0
  240. package/src/types/dictionary.ts +125 -0
  241. package/src/types/document-processing.ts +331 -0
  242. package/src/types/docxmlater-augments.d.ts +142 -0
  243. package/src/types/editor.ts +280 -0
  244. package/src/types/electron.ts +340 -0
  245. package/src/types/globalStats.ts +155 -0
  246. package/src/types/hyperlink.ts +471 -0
  247. package/src/types/operations.ts +354 -0
  248. package/src/types/session.ts +427 -0
  249. package/src/types/settings.ts +112 -0
  250. package/src/utils/MemoryMonitor.ts +248 -0
  251. package/src/utils/cn.ts +6 -0
  252. package/src/utils/colorConvert.ts +306 -0
  253. package/src/utils/diffUtils.ts +347 -0
  254. package/src/utils/documentUtils.ts +202 -0
  255. package/src/utils/electronGuard.ts +62 -0
  256. package/src/utils/indexedDB.ts +915 -0
  257. package/src/utils/logger.ts +717 -0
  258. package/src/utils/pathSecurity.ts +232 -0
  259. package/src/utils/pathValidator.ts +236 -0
  260. package/src/utils/processingTimeEstimator.ts +153 -0
  261. package/src/utils/safeJsonParse.ts +62 -0
  262. package/src/utils/textSanitizer.ts +162 -0
  263. package/src/utils/urlHelpers.ts +304 -0
  264. package/src/utils/urlPatterns.ts +198 -0
  265. package/src/utils/urlSanitizer.ts +152 -0
  266. package/src/vite-env.d.ts +11 -0
  267. package/tsconfig.electron.json +19 -0
  268. package/tsconfig.json +36 -0
  269. package/tsconfig.node.json +12 -0
  270. package/typedoc.json +45 -0
  271. package/vite.config.ts +152 -0
@@ -0,0 +1,656 @@
1
+ import { autoUpdater, UpdateInfo } from 'electron-updater';
2
+ import { app, shell } from 'electron';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+ import { PublicClientApplication, Configuration, InteractiveRequest, AccountInfo } from '@azure/msal-node';
6
+ import { logger } from '../src/utils/logger';
7
+ import * as yaml from 'js-yaml';
8
+
9
+ const log = logger.namespace('CustomUpdater');
10
+
11
+ /**
12
+ * SharePoint update provider configuration
13
+ */
14
+ interface UpdateProviderConfig {
15
+ type: 'github' | 'sharepoint';
16
+ sharePointUrl?: string;
17
+ }
18
+
19
+ /**
20
+ * Custom updater with delta update support, silent install, and SharePoint source
21
+ */
22
+ export class CustomUpdater {
23
+ private mainWindow: Electron.BrowserWindow | null = null;
24
+ private updateInfo: UpdateInfo | null = null;
25
+ private isDev: boolean;
26
+ private forceUpdatesInDev: boolean;
27
+
28
+ // SharePoint update source state
29
+ private msalApp: PublicClientApplication | null = null;
30
+ private sharePointUrl: string | null = null;
31
+ private accessToken: string | null = null;
32
+ private currentProvider: 'github' | 'sharepoint' = 'github';
33
+ private accountInfo: AccountInfo | null = null;
34
+
35
+ constructor(mainWindow: Electron.BrowserWindow | null) {
36
+ this.mainWindow = mainWindow;
37
+ this.isDev = !app.isPackaged;
38
+ this.forceUpdatesInDev = process.env.FORCE_DEV_UPDATE_CONFIG === 'true';
39
+ this.configureDevUpdateServer();
40
+ this.setupAutoUpdater();
41
+ }
42
+
43
+ /** Returns true if updates should be skipped (dev mode without force flag) */
44
+ private shouldSkipUpdates(): boolean {
45
+ return this.isDev && !this.forceUpdatesInDev;
46
+ }
47
+
48
+ /**
49
+ * Configure dev update server for local testing
50
+ * Set FORCE_DEV_UPDATE_CONFIG=true env var to test against local server
51
+ */
52
+ private configureDevUpdateServer(): void {
53
+ const forceDevConfig = process.env.FORCE_DEV_UPDATE_CONFIG === 'true';
54
+
55
+ if (forceDevConfig || (this.isDev && process.env.TEST_UPDATES === 'true')) {
56
+ // When running from dist/electron/, go up two levels to project root
57
+ const devConfigPath = path.join(__dirname, '..', '..', 'dev-app-update.yml');
58
+
59
+ if (fs.existsSync(devConfigPath)) {
60
+ log.info('Using dev update config for local testing:', devConfigPath);
61
+ autoUpdater.forceDevUpdateConfig = true;
62
+ autoUpdater.updateConfigPath = devConfigPath;
63
+ } else {
64
+ log.warn('Dev update config not found at:', devConfigPath);
65
+ }
66
+ }
67
+ }
68
+
69
+ private setupAutoUpdater(): void {
70
+ // Configure auto-updater
71
+ autoUpdater.autoDownload = false;
72
+ autoUpdater.autoInstallOnAppQuit = true;
73
+ autoUpdater.autoRunAppAfterInstall = true; // Auto-restart after silent install
74
+ autoUpdater.disableDifferentialDownload = false; // Enable delta/blockmap updates
75
+
76
+ // Set up event handlers
77
+ autoUpdater.on('checking-for-update', () => {
78
+ this.sendToWindow('update-checking');
79
+ log.info('Checking for updates...');
80
+ });
81
+
82
+ autoUpdater.on('update-available', (info) => {
83
+ this.updateInfo = info;
84
+ this.sendToWindow('update-available', {
85
+ version: info.version,
86
+ releaseDate: info.releaseDate,
87
+ releaseNotes: info.releaseNotes,
88
+ });
89
+ log.info(`Update available: ${info.version}`);
90
+ });
91
+
92
+ autoUpdater.on('update-not-available', (info) => {
93
+ this.sendToWindow('update-not-available', {
94
+ version: info.version,
95
+ });
96
+ log.info('No updates available');
97
+ });
98
+
99
+ autoUpdater.on('error', (error) => {
100
+ this.sendToWindow('update-error', {
101
+ message: error.message,
102
+ });
103
+ log.error('Update error:', error);
104
+ });
105
+
106
+ autoUpdater.on('download-progress', (progressObj) => {
107
+ this.sendToWindow('update-download-progress', {
108
+ bytesPerSecond: progressObj.bytesPerSecond,
109
+ percent: progressObj.percent,
110
+ transferred: progressObj.transferred,
111
+ total: progressObj.total,
112
+ });
113
+ });
114
+
115
+ autoUpdater.on('update-downloaded', (info) => {
116
+ this.sendToWindow('update-downloaded', {
117
+ version: info.version,
118
+ releaseNotes: info.releaseNotes,
119
+ });
120
+ log.info(`Update downloaded: ${info.version}`);
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Send status to renderer process
126
+ */
127
+ private sendToWindow(channel: string, data?: any): void {
128
+ if (this.mainWindow && !this.mainWindow.isDestroyed()) {
129
+ this.mainWindow.webContents.send(channel, data);
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Check for updates (supports GitHub and SharePoint sources)
135
+ */
136
+ public async checkForUpdates(): Promise<any> {
137
+ if (this.shouldSkipUpdates()) {
138
+ return {
139
+ success: false,
140
+ message: 'Updates are not available in development mode',
141
+ };
142
+ }
143
+
144
+ // SharePoint: Only use if enabled AND URL provided AND authenticated
145
+ if (this.currentProvider === 'sharepoint' && this.sharePointUrl && this.accessToken) {
146
+ try {
147
+ const result = await this.checkSharePointUpdates();
148
+ if (result.success) return result;
149
+ // Fall through to GitHub on failure
150
+ log.warn('SharePoint update check failed, falling back to GitHub');
151
+ this.sendToWindow('update-status', { message: 'SharePoint unavailable, checking GitHub...' });
152
+ } catch (error) {
153
+ log.error('SharePoint update check error', { error });
154
+ this.sendToWindow('update-status', { message: 'SharePoint error, falling back to GitHub...' });
155
+ }
156
+ }
157
+
158
+ // Default: GitHub
159
+ try {
160
+ const result = await autoUpdater.checkForUpdates();
161
+ return {
162
+ success: true,
163
+ updateInfo: result?.updateInfo,
164
+ };
165
+ } catch (error) {
166
+ log.error('Failed to check for updates:', error);
167
+ return {
168
+ success: false,
169
+ message: error instanceof Error ? error.message : 'Failed to check for updates',
170
+ };
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Download update
176
+ */
177
+ public async downloadUpdate(): Promise<any> {
178
+ try {
179
+ await autoUpdater.downloadUpdate();
180
+ return {
181
+ success: true,
182
+ message: 'Download started',
183
+ };
184
+ } catch (error) {
185
+ log.error('Failed to download update:', error);
186
+ return {
187
+ success: false,
188
+ message: error instanceof Error ? error.message : 'Failed to download update',
189
+ };
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Install update and restart
195
+ * Shows installer UI for reliable MSI installation on Windows
196
+ */
197
+ public quitAndInstall(): void {
198
+ log.info('Installing update and restarting...');
199
+
200
+ // Remove listeners that might prevent quit
201
+ app.removeAllListeners('window-all-closed');
202
+
203
+ // quitAndInstall(isSilent, isForceRunAfter)
204
+ // isSilent=false: Show installer UI (required for MSI on Windows)
205
+ // isForceRunAfter=false: Don't auto-launch after install (avoids MSI error 2753)
206
+ // Note: User will need to manually start the app after update completes
207
+ // This is more reliable than auto-launch which can fail with path issues
208
+ autoUpdater.quitAndInstall(false, false);
209
+ }
210
+
211
+ /**
212
+ * Check for updates on startup
213
+ */
214
+ public async checkOnStartup(): Promise<void> {
215
+ if (this.shouldSkipUpdates()) {
216
+ log.info('Skipping update check in development mode');
217
+ return;
218
+ }
219
+
220
+ // Wait a bit for the window to load
221
+ setTimeout(async () => {
222
+ try {
223
+ log.info('Checking for updates on startup...');
224
+ await this.checkForUpdates();
225
+ } catch (error) {
226
+ log.error('Startup update check failed:', error);
227
+ }
228
+ }, 3000);
229
+ }
230
+
231
+ /**
232
+ * Start scheduled periodic update checks
233
+ * @param intervalMs Interval between checks in milliseconds (default: 4 hours)
234
+ */
235
+ public startScheduledChecks(intervalMs: number = 4 * 60 * 60 * 1000): void {
236
+ if (this.shouldSkipUpdates()) {
237
+ log.info('Skipping scheduled update checks in development mode');
238
+ return;
239
+ }
240
+
241
+ log.info(`Starting scheduled update checks every ${intervalMs / (60 * 60 * 1000)} hours`);
242
+
243
+ setInterval(async () => {
244
+ try {
245
+ log.info('Running scheduled update check...');
246
+ await this.checkForUpdates();
247
+ } catch (error) {
248
+ log.error('Scheduled update check failed:', error);
249
+ }
250
+ }, intervalMs);
251
+ }
252
+
253
+ // ============================================================================
254
+ // SharePoint Update Source Methods
255
+ // ============================================================================
256
+
257
+ /**
258
+ * Set the update provider (GitHub or SharePoint)
259
+ */
260
+ public async setProvider(config: UpdateProviderConfig): Promise<{ success: boolean; error?: string }> {
261
+ try {
262
+ this.currentProvider = config.type;
263
+
264
+ if (config.type === 'sharepoint' && config.sharePointUrl) {
265
+ this.sharePointUrl = config.sharePointUrl;
266
+ log.info('Update provider set to SharePoint', { url: config.sharePointUrl });
267
+ } else {
268
+ this.sharePointUrl = null;
269
+ this.accessToken = null;
270
+ this.currentProvider = 'github';
271
+ log.info('Update provider set to GitHub (default)');
272
+ }
273
+
274
+ return { success: true };
275
+ } catch (error) {
276
+ const message = error instanceof Error ? error.message : 'Failed to set provider';
277
+ log.error('Failed to set update provider', { error: message });
278
+ return { success: false, error: message };
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Interactive Microsoft login for SharePoint access
284
+ */
285
+ public async sharePointLogin(): Promise<{ success: boolean; error?: string }> {
286
+ try {
287
+ if (!this.msalApp) {
288
+ this.initializeMsal();
289
+ }
290
+
291
+ if (!this.msalApp) {
292
+ return { success: false, error: 'Failed to initialize authentication' };
293
+ }
294
+
295
+ // Try to acquire token silently first if we have cached account
296
+ if (this.accountInfo) {
297
+ try {
298
+ const silentResult = await this.msalApp.acquireTokenSilent({
299
+ account: this.accountInfo,
300
+ scopes: ['https://graph.microsoft.com/Files.Read.All'],
301
+ });
302
+ this.accessToken = silentResult.accessToken;
303
+ log.info('SharePoint token acquired silently');
304
+ return { success: true };
305
+ } catch {
306
+ // Silent acquisition failed, proceed with interactive
307
+ log.info('Silent token acquisition failed, proceeding with interactive login');
308
+ }
309
+ }
310
+
311
+ // Interactive login - open browser for user to sign in
312
+ const interactiveRequest: InteractiveRequest = {
313
+ scopes: ['https://graph.microsoft.com/Files.Read.All'],
314
+ openBrowser: async (url: string) => {
315
+ await shell.openExternal(url);
316
+ },
317
+ successTemplate: '<h1>Authentication Successful</h1><p>You can close this window and return to Documentation Hub.</p>',
318
+ errorTemplate: '<h1>Authentication Failed</h1><p>{{error}}</p>',
319
+ };
320
+
321
+ const authResult = await this.msalApp.acquireTokenInteractive(interactiveRequest);
322
+ this.accessToken = authResult.accessToken;
323
+ this.accountInfo = authResult.account;
324
+
325
+ log.info('SharePoint login successful');
326
+ return { success: true };
327
+ } catch (error) {
328
+ const message = error instanceof Error ? error.message : 'Login failed';
329
+ log.error('SharePoint login failed', { error: message });
330
+ return { success: false, error: message };
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Logout from SharePoint
336
+ */
337
+ public async sharePointLogout(): Promise<void> {
338
+ this.accessToken = null;
339
+ this.accountInfo = null;
340
+
341
+ if (this.msalApp) {
342
+ try {
343
+ const tokenCache = this.msalApp.getTokenCache();
344
+ const accounts = await tokenCache.getAllAccounts();
345
+ for (const account of accounts) {
346
+ await tokenCache.removeAccount(account);
347
+ }
348
+ } catch (error) {
349
+ log.warn('Error clearing token cache:', error);
350
+ }
351
+ }
352
+
353
+ log.info('SharePoint logout complete');
354
+ }
355
+
356
+ /**
357
+ * Test SharePoint connection
358
+ */
359
+ public async testSharePointConnection(url: string): Promise<{ success: boolean; message: string; authenticated?: boolean }> {
360
+ // Validate URL format
361
+ if (!this.isValidSharePointUrl(url)) {
362
+ return { success: false, message: 'Invalid SharePoint URL. Must be https://*.sharepoint.com/sites/...' };
363
+ }
364
+
365
+ // Check if authenticated
366
+ if (!this.accessToken) {
367
+ return { success: false, message: 'Not authenticated. Please sign in to Microsoft first.', authenticated: false };
368
+ }
369
+
370
+ try {
371
+ // Try to access latest.yml
372
+ const latestYmlUrl = this.buildGraphApiUrl(url, 'latest.yml');
373
+ log.info('Testing SharePoint connection', { url: latestYmlUrl });
374
+
375
+ const response = await this.fetchWithAuth(latestYmlUrl);
376
+
377
+ if (response.ok) {
378
+ return { success: true, message: 'Connected! Update manifest (latest.yml) found.', authenticated: true };
379
+ } else if (response.status === 404) {
380
+ return { success: true, message: 'Connected to SharePoint, but latest.yml not found. Please upload the update files.', authenticated: true };
381
+ } else if (response.status === 401 || response.status === 403) {
382
+ return { success: false, message: `Access denied (${response.status}). Please check folder permissions.`, authenticated: true };
383
+ } else {
384
+ return { success: false, message: `SharePoint returned status ${response.status}: ${response.statusText}`, authenticated: true };
385
+ }
386
+ } catch (error) {
387
+ const message = error instanceof Error ? error.message : 'Connection failed';
388
+ log.error('SharePoint connection test failed', { error: message });
389
+ return { success: false, message: `Connection failed: ${message}`, authenticated: true };
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Check for updates from SharePoint
395
+ */
396
+ private async checkSharePointUpdates(): Promise<any> {
397
+ if (!this.sharePointUrl || !this.accessToken) {
398
+ throw new Error('SharePoint not configured or not authenticated');
399
+ }
400
+
401
+ // Fetch latest.yml from SharePoint
402
+ const latestYmlUrl = this.buildGraphApiUrl(this.sharePointUrl, 'latest.yml');
403
+ log.info('Checking for updates from SharePoint', { url: latestYmlUrl });
404
+
405
+ const response = await this.fetchWithAuth(latestYmlUrl);
406
+
407
+ if (!response.ok) {
408
+ throw new Error(`Failed to fetch latest.yml: ${response.status} ${response.statusText}`);
409
+ }
410
+
411
+ const yamlContent = await response.text();
412
+ const updateManifest = yaml.load(yamlContent) as any;
413
+
414
+ if (!updateManifest || !updateManifest.version) {
415
+ throw new Error('Invalid latest.yml format - missing version');
416
+ }
417
+
418
+ const currentVersion = app.getVersion();
419
+ const availableVersion = updateManifest.version;
420
+
421
+ log.info('SharePoint update check', { currentVersion, availableVersion });
422
+
423
+ // Compare versions
424
+ if (this.isNewerVersion(availableVersion, currentVersion)) {
425
+ // Update available
426
+ this.updateInfo = {
427
+ version: availableVersion,
428
+ releaseDate: updateManifest.releaseDate || new Date().toISOString(),
429
+ releaseNotes: updateManifest.releaseNotes || '',
430
+ files: updateManifest.files || [],
431
+ path: updateManifest.path || '',
432
+ sha512: updateManifest.sha512 || '',
433
+ } as UpdateInfo;
434
+
435
+ this.sendToWindow('update-available', {
436
+ version: availableVersion,
437
+ releaseDate: updateManifest.releaseDate || '',
438
+ releaseNotes: updateManifest.releaseNotes || '',
439
+ });
440
+
441
+ log.info(`SharePoint update available: ${availableVersion}`);
442
+ return { success: true, updateInfo: this.updateInfo };
443
+ } else {
444
+ // No update available
445
+ this.sendToWindow('update-not-available', {
446
+ version: currentVersion,
447
+ });
448
+ log.info('No SharePoint updates available');
449
+ return { success: true, updateInfo: null };
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Validate SharePoint URL format
455
+ */
456
+ private isValidSharePointUrl(url: string): boolean {
457
+ try {
458
+ const parsed = new URL(url);
459
+ // Must be HTTPS and a sharepoint.com domain
460
+ if (parsed.protocol !== 'https:') return false;
461
+ if (!parsed.hostname.endsWith('.sharepoint.com')) return false;
462
+ // Should contain /sites/ in the path
463
+ if (!parsed.pathname.includes('/sites/')) return false;
464
+ return true;
465
+ } catch {
466
+ return false;
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Initialize MSAL for interactive login
472
+ * Uses the public Microsoft Office client ID which allows delegated user permissions
473
+ */
474
+ private initializeMsal(): void {
475
+ const config: Configuration = {
476
+ auth: {
477
+ // Microsoft Office public client ID - works for delegated user auth
478
+ clientId: 'd3590ed6-52b3-4102-aeff-aad2292ab01c',
479
+ authority: 'https://login.microsoftonline.com/common',
480
+ },
481
+ cache: {
482
+ // Use in-memory cache (not persisted between sessions)
483
+ },
484
+ };
485
+
486
+ this.msalApp = new PublicClientApplication(config);
487
+ log.info('MSAL client initialized for SharePoint updates');
488
+ }
489
+
490
+ /**
491
+ * Build Microsoft Graph API URL for SharePoint file access
492
+ *
493
+ * Converts a SharePoint URL like:
494
+ * https://company.sharepoint.com/sites/IT/Shared Documents/Updates
495
+ *
496
+ * To a Graph API URL like:
497
+ * https://graph.microsoft.com/v1.0/sites/company.sharepoint.com:/sites/IT:/drive/root:/Shared Documents/Updates/filename:/content
498
+ */
499
+ private buildGraphApiUrl(folderUrl: string, fileName: string): string {
500
+ const url = new URL(folderUrl);
501
+ const hostname = url.hostname;
502
+ const pathParts = url.pathname.split('/').filter(p => p);
503
+
504
+ // Find the site path (e.g., /sites/IT)
505
+ const siteIndex = pathParts.indexOf('sites');
506
+ if (siteIndex === -1) {
507
+ throw new Error('Invalid SharePoint URL - must contain /sites/');
508
+ }
509
+
510
+ const siteName = pathParts[siteIndex + 1];
511
+ if (!siteName) {
512
+ throw new Error('Invalid SharePoint URL - missing site name');
513
+ }
514
+
515
+ // Everything after the site name is the folder path
516
+ const folderPath = pathParts.slice(siteIndex + 2).join('/');
517
+
518
+ // Build the Graph API URL
519
+ // Format: /sites/{hostname}:/sites/{siteName}:/drive/root:/{folderPath}/{fileName}:/content
520
+ const graphUrl = `https://graph.microsoft.com/v1.0/sites/${hostname}:/sites/${siteName}:/drive/root:/${encodeURIComponent(folderPath)}/${encodeURIComponent(fileName)}:/content`;
521
+
522
+ return graphUrl;
523
+ }
524
+
525
+ /**
526
+ * Fetch with Bearer token authentication
527
+ */
528
+ private async fetchWithAuth(url: string): Promise<Response> {
529
+ if (!this.accessToken) {
530
+ throw new Error('No access token available');
531
+ }
532
+
533
+ return fetch(url, {
534
+ headers: {
535
+ 'Authorization': `Bearer ${this.accessToken}`,
536
+ 'User-Agent': `DocumentationHub/${app.getVersion()} (${process.platform})`,
537
+ },
538
+ });
539
+ }
540
+
541
+ /**
542
+ * Compare versions to determine if target is newer than current
543
+ */
544
+ private isNewerVersion(target: string, current: string): boolean {
545
+ const targetParts = target.split('.').map(Number);
546
+ const currentParts = current.split('.').map(Number);
547
+
548
+ for (let i = 0; i < Math.max(targetParts.length, currentParts.length); i++) {
549
+ const t = targetParts[i] || 0;
550
+ const c = currentParts[i] || 0;
551
+ if (t > c) return true;
552
+ if (t < c) return false;
553
+ }
554
+ return false;
555
+ }
556
+
557
+ /**
558
+ * Download a specific file from SharePoint using interactive authentication
559
+ *
560
+ * This method is used for downloading dictionary files. It requires the user
561
+ * to be authenticated via sharePointLogin() first.
562
+ *
563
+ * @param fileUrl - Direct SharePoint URL to the file (e.g., https://company.sharepoint.com/sites/IT/Shared Documents/Dictionary.xlsx)
564
+ * @returns Buffer containing the file content, or error
565
+ */
566
+ public async downloadSharePointFile(fileUrl: string): Promise<{ success: boolean; data?: Buffer; error?: string }> {
567
+ try {
568
+ // Validate URL format
569
+ if (!this.isValidSharePointUrl(fileUrl)) {
570
+ return { success: false, error: 'Invalid SharePoint URL. Must be https://*.sharepoint.com/sites/...' };
571
+ }
572
+
573
+ // Check authentication
574
+ if (!this.accessToken) {
575
+ // Try to acquire token silently first
576
+ const loginResult = await this.sharePointLogin();
577
+ if (!loginResult.success) {
578
+ return { success: false, error: loginResult.error || 'Authentication required. Please sign in first.' };
579
+ }
580
+ }
581
+
582
+ // Build Graph API URL for the specific file
583
+ const graphApiUrl = this.buildGraphApiUrlForFile(fileUrl);
584
+ log.info('Downloading SharePoint file via Graph API:', graphApiUrl);
585
+
586
+ // Download the file
587
+ const response = await this.fetchWithAuth(graphApiUrl);
588
+
589
+ if (!response.ok) {
590
+ const errorText = await response.text();
591
+ log.error('SharePoint file download failed:', {
592
+ status: response.status,
593
+ statusText: response.statusText,
594
+ error: errorText,
595
+ });
596
+ return {
597
+ success: false,
598
+ error: `Download failed: ${response.status} ${response.statusText}`,
599
+ };
600
+ }
601
+
602
+ // Convert response to Buffer
603
+ const arrayBuffer = await response.arrayBuffer();
604
+ const buffer = Buffer.from(arrayBuffer);
605
+
606
+ log.info('SharePoint file downloaded successfully', { size: buffer.length });
607
+ return { success: true, data: buffer };
608
+ } catch (error) {
609
+ const message = error instanceof Error ? error.message : 'Download failed';
610
+ log.error('SharePoint file download error:', { error: message });
611
+ return { success: false, error: message };
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Build Microsoft Graph API URL for a specific SharePoint file
617
+ *
618
+ * Converts a SharePoint file URL like:
619
+ * https://company.sharepoint.com/sites/IT/Shared Documents/Folder/Dictionary.xlsx
620
+ *
621
+ * To a Graph API URL like:
622
+ * https://graph.microsoft.com/v1.0/sites/company.sharepoint.com:/sites/IT:/drive/root:/Shared Documents/Folder/Dictionary.xlsx:/content
623
+ */
624
+ private buildGraphApiUrlForFile(fileUrl: string): string {
625
+ const url = new URL(fileUrl);
626
+ const hostname = url.hostname;
627
+ const pathParts = url.pathname.split('/').filter(p => p);
628
+
629
+ // Find the site path (e.g., /sites/IT)
630
+ const siteIndex = pathParts.indexOf('sites');
631
+ if (siteIndex === -1) {
632
+ throw new Error('Invalid SharePoint URL - must contain /sites/');
633
+ }
634
+
635
+ const siteName = pathParts[siteIndex + 1];
636
+ if (!siteName) {
637
+ throw new Error('Invalid SharePoint URL - missing site name');
638
+ }
639
+
640
+ // Everything after the site name is the file path (including filename)
641
+ const filePath = pathParts.slice(siteIndex + 2).join('/');
642
+
643
+ // Build the Graph API URL for the specific file
644
+ // Format: /sites/{hostname}:/sites/{siteName}:/drive/root:/{filePath}:/content
645
+ const graphUrl = `https://graph.microsoft.com/v1.0/sites/${hostname}:/sites/${siteName}:/drive/root:/${filePath}:/content`;
646
+
647
+ return graphUrl;
648
+ }
649
+
650
+ /**
651
+ * Check if authenticated for SharePoint access
652
+ */
653
+ public isSharePointAuthenticated(): boolean {
654
+ return this.accessToken !== null;
655
+ }
656
+ }