@yancyyu/openhermit 1.6.29 → 1.6.31

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 (157) hide show
  1. package/dist-renderer/assets/{ProjectEditorOverlay-CQm6jUR1.js → ProjectEditorOverlay-DkXfi2pg.js} +1 -1
  2. package/dist-renderer/assets/{TeamGraphOverlay-h0WDfifv.js → TeamGraphOverlay-CHNNVraw.js} +1 -1
  3. package/dist-renderer/assets/{_basePickBy-CgG_tjgX.js → _basePickBy-Do-Ff83V.js} +1 -1
  4. package/dist-renderer/assets/{_baseUniq-DwPTU9lP.js → _baseUniq-nDLhSuJI.js} +1 -1
  5. package/dist-renderer/assets/{arc-7nIrGRzY.js → arc-Bp7fA6sx.js} +1 -1
  6. package/dist-renderer/assets/{architectureDiagram-VXUJARFQ-BYhA6Ev2.js → architectureDiagram-VXUJARFQ-CPC1HdGy.js} +1 -1
  7. package/dist-renderer/assets/{blockDiagram-VD42YOAC-BVpZUGDg.js → blockDiagram-VD42YOAC-DTVKyNTO.js} +1 -1
  8. package/dist-renderer/assets/{c4Diagram-YG6GDRKO-DsdreMQ9.js → c4Diagram-YG6GDRKO-XVu-AN00.js} +1 -1
  9. package/dist-renderer/assets/channel-CIwbNcUO.js +1 -0
  10. package/dist-renderer/assets/{chunk-4BX2VUAB-CcoAs7Jd.js → chunk-4BX2VUAB-BcWmVyA-.js} +1 -1
  11. package/dist-renderer/assets/{chunk-55IACEB6-CGGAOoXd.js → chunk-55IACEB6-Co4Z2jsE.js} +1 -1
  12. package/dist-renderer/assets/{chunk-B4BG7PRW-FhpTEPvD.js → chunk-B4BG7PRW-C8q9gfDT.js} +1 -1
  13. package/dist-renderer/assets/{chunk-DI55MBZ5-DoYySbm1.js → chunk-DI55MBZ5-qDgb1gxO.js} +1 -1
  14. package/dist-renderer/assets/{chunk-FMBD7UC4-e9l2tGHG.js → chunk-FMBD7UC4-Cm8Gu2gu.js} +1 -1
  15. package/dist-renderer/assets/{chunk-QN33PNHL-DeiXVTCy.js → chunk-QN33PNHL-DYji1BRS.js} +1 -1
  16. package/dist-renderer/assets/{chunk-QZHKN3VN-DC2UJLJM.js → chunk-QZHKN3VN-DWAS568H.js} +1 -1
  17. package/dist-renderer/assets/{chunk-TZMSLE5B-BHFD9eZI.js → chunk-TZMSLE5B-CLFzXLA8.js} +1 -1
  18. package/dist-renderer/assets/classDiagram-2ON5EDUG-04A-pvql.js +1 -0
  19. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-04A-pvql.js +1 -0
  20. package/dist-renderer/assets/clone-DQnvTIEM.js +1 -0
  21. package/dist-renderer/assets/{cose-bilkent-S5V4N54A-BdybQraU.js → cose-bilkent-S5V4N54A-CZdGhX_3.js} +1 -1
  22. package/dist-renderer/assets/{dagre-6UL2VRFP-DdF3pwM3.js → dagre-6UL2VRFP-BVY-G6nO.js} +1 -1
  23. package/dist-renderer/assets/{diagram-PSM6KHXK-B9Ldd3nh.js → diagram-PSM6KHXK-CUACvAwG.js} +1 -1
  24. package/dist-renderer/assets/{diagram-QEK2KX5R-XEqkrbpu.js → diagram-QEK2KX5R-3SfnesSG.js} +1 -1
  25. package/dist-renderer/assets/{diagram-S2PKOQOG-CipwtY59.js → diagram-S2PKOQOG-E3ksXClJ.js} +1 -1
  26. package/dist-renderer/assets/{erDiagram-Q2GNP2WA-BB-2ISGo.js → erDiagram-Q2GNP2WA-aYjGXss7.js} +1 -1
  27. package/dist-renderer/assets/{flowDiagram-NV44I4VS-B8XmJ0u2.js → flowDiagram-NV44I4VS-JMHrrTQs.js} +1 -1
  28. package/dist-renderer/assets/{ganttDiagram-JELNMOA3-D-8XglBb.js → ganttDiagram-JELNMOA3-CVQ-R5rN.js} +1 -1
  29. package/dist-renderer/assets/{gitGraphDiagram-V2S2FVAM-DL4ChakD.js → gitGraphDiagram-V2S2FVAM-OLn9jq61.js} +1 -1
  30. package/dist-renderer/assets/{graph-BiFNoBjP.js → graph-BAb2J0l8.js} +1 -1
  31. package/dist-renderer/assets/{index-qNBNjW4K.js → index-BSoCjBWn.js} +1 -1
  32. package/dist-renderer/assets/{index-6m1ZAymG.js → index-BtG3HbqP.js} +1 -1
  33. package/dist-renderer/assets/{index-BowUl0Jb.js → index-CH8e7g1f.js} +583 -573
  34. package/dist-renderer/assets/index-CSt8DTcn.css +1 -0
  35. package/dist-renderer/assets/{index-Dp3kJTEe.js → index-Ca4iNkRA.js} +1 -1
  36. package/dist-renderer/assets/{index-vAykq1H1.js → index-DU9PGgZJ.js} +1 -1
  37. package/dist-renderer/assets/{index-TOpt_T7A.js → index-DtMzIS9o.js} +1 -1
  38. package/dist-renderer/assets/{infoDiagram-HS3SLOUP-DRIBfHDi.js → infoDiagram-HS3SLOUP-CY_ptQNL.js} +1 -1
  39. package/dist-renderer/assets/{journeyDiagram-XKPGCS4Q-BOMiigU4.js → journeyDiagram-XKPGCS4Q-C2vuHEo_.js} +1 -1
  40. package/dist-renderer/assets/{kanban-definition-3W4ZIXB7-DDxeyjod.js → kanban-definition-3W4ZIXB7-mbdNfu8h.js} +1 -1
  41. package/dist-renderer/assets/{layout-DNANbrI4.js → layout-Do_ArEB1.js} +1 -1
  42. package/dist-renderer/assets/{linear-DxEJi1yT.js → linear-BMlMKyiq.js} +1 -1
  43. package/dist-renderer/assets/{mindmap-definition-VGOIOE7T-nBfGriW8.js → mindmap-definition-VGOIOE7T-Dfntn-o2.js} +1 -1
  44. package/dist-renderer/assets/{pieDiagram-ADFJNKIX-Din5j6sV.js → pieDiagram-ADFJNKIX-LiWHsGMV.js} +1 -1
  45. package/dist-renderer/assets/{quadrantDiagram-AYHSOK5B-DMVK2BEQ.js → quadrantDiagram-AYHSOK5B-D87St8AF.js} +1 -1
  46. package/dist-renderer/assets/{requirementDiagram-UZGBJVZJ-6SC94Gg_.js → requirementDiagram-UZGBJVZJ-DAa6lHBx.js} +1 -1
  47. package/dist-renderer/assets/{sankeyDiagram-TZEHDZUN-CD2gghhu.js → sankeyDiagram-TZEHDZUN-VOUngars.js} +1 -1
  48. package/dist-renderer/assets/{sequenceDiagram-WL72ISMW-BnhkN7nZ.js → sequenceDiagram-WL72ISMW-BzwzmFr2.js} +1 -1
  49. package/dist-renderer/assets/{stateDiagram-FKZM4ZOC-Bn8XdYX-.js → stateDiagram-FKZM4ZOC-BjAQEJ52.js} +1 -1
  50. package/dist-renderer/assets/{stateDiagram-v2-4FDKWEC3-1b6sI1_g.js → stateDiagram-v2-4FDKWEC3-BDwy4GJm.js} +1 -1
  51. package/dist-renderer/assets/{timeline-definition-IT6M3QCI-CNs3RPoa.js → timeline-definition-IT6M3QCI-Y5XBZt3W.js} +1 -1
  52. package/dist-renderer/assets/treemap-GDKQZRPO-DzkdUEow.js +162 -0
  53. package/dist-renderer/assets/{xychartDiagram-PRI3JC2R-B8o5J2f3.js → xychartDiagram-PRI3JC2R-D-zbvJOv.js} +1 -1
  54. package/dist-renderer/index.html +2 -2
  55. package/package.json +4 -1
  56. package/src/main/ipc/extensions.ts +353 -0
  57. package/src/main/server.ts +209 -6
  58. package/src/main/services/extensions/ExtensionFacadeService.ts +135 -0
  59. package/src/main/services/extensions/catalog/GlamaMcpEnrichmentService.ts +190 -0
  60. package/src/main/services/extensions/catalog/McpCatalogAggregator.ts +150 -0
  61. package/src/main/services/extensions/catalog/OfficialMcpRegistryService.ts +381 -0
  62. package/src/main/services/extensions/catalog/PluginCatalogService.ts +392 -0
  63. package/src/main/services/extensions/credentials/CredentialService.ts +343 -0
  64. package/src/main/services/extensions/install/McpInstallService.ts +407 -0
  65. package/src/main/services/extensions/install/PluginInstallService.ts +198 -0
  66. package/src/main/services/extensions/runtime/ClaudeCodeAdapter.ts +199 -0
  67. package/src/main/services/extensions/runtime/CodexAdapter.ts +100 -0
  68. package/src/main/services/extensions/runtime/CursorAdapter.ts +154 -0
  69. package/src/main/services/extensions/runtime/ExtensionsRuntimeAdapter.ts +172 -0
  70. package/src/main/services/extensions/runtime/GeminiAdapter.ts +91 -0
  71. package/src/main/services/extensions/runtime/HarnessInstallAdapter.ts +49 -0
  72. package/src/main/services/extensions/runtime/McpConfigStateReader.ts +209 -0
  73. package/src/main/services/extensions/runtime/OpenCodeAdapter.ts +91 -0
  74. package/src/main/services/extensions/runtime/adapterRegistry.ts +54 -0
  75. package/src/main/services/extensions/runtime/mcpDiagnosticsParser.ts +214 -0
  76. package/src/main/services/extensions/runtime/mcpRuntimeJson.ts +45 -0
  77. package/src/main/services/extensions/skills/SkillImportService.ts +155 -0
  78. package/src/main/services/extensions/skills/SkillMetadataParser.ts +323 -0
  79. package/src/main/services/extensions/skills/SkillPlanService.ts +411 -0
  80. package/src/main/services/extensions/skills/SkillReviewService.ts +73 -0
  81. package/src/main/services/extensions/skills/SkillRootsResolver.ts +49 -0
  82. package/src/main/services/extensions/skills/SkillScaffoldService.ts +89 -0
  83. package/src/main/services/extensions/skills/SkillScanner.ts +117 -0
  84. package/src/main/services/extensions/skills/SkillValidator.ts +69 -0
  85. package/src/main/services/extensions/skills/SkillsCatalogService.ts +92 -0
  86. package/src/main/services/extensions/skills/SkillsMutationService.ts +146 -0
  87. package/src/main/services/extensions/skills/SkillsWatcherService.ts +134 -0
  88. package/src/main/services/extensions/state/McpInstallationStateService.ts +42 -0
  89. package/src/main/services/extensions/state/PluginInstallationStateService.ts +281 -0
  90. package/src/main/services/identity/AgentTeamsIdentityStore.ts +218 -0
  91. package/src/main/services/runtime/providerAwareCliEnv.ts +60 -0
  92. package/src/main/services/team/ClaudeBinaryResolver.ts +469 -0
  93. package/src/main/services/team/ClaudeDoctorProbe.ts +0 -0
  94. package/src/main/services/team/cliFlavor.ts +54 -0
  95. package/src/main/services/teams-mvp/TaskDispatchService.ts +3 -0
  96. package/src/main/utils/atomicWrite.ts +72 -0
  97. package/src/main/utils/childProcess.ts +554 -0
  98. package/src/main/utils/cliEnv.ts +54 -0
  99. package/src/main/utils/cliPathMerge.ts +97 -0
  100. package/src/main/utils/pathDecoder.ts +664 -0
  101. package/src/main/utils/pathValidation.ts +432 -0
  102. package/src/main/utils/shellEnv.ts +331 -0
  103. package/src/renderer/api/httpClient.ts +61 -0
  104. package/src/renderer/components/extensions/ExtensionStoreView.tsx +63 -35
  105. package/src/renderer/components/extensions/ExtensionsSubTabTrigger.tsx +1 -1
  106. package/src/renderer/components/extensions/common/ExtensionToast.tsx +141 -0
  107. package/src/renderer/components/extensions/common/HarnessSelector.tsx +71 -0
  108. package/src/renderer/components/extensions/env/EnvVarPanel.tsx +335 -0
  109. package/src/renderer/components/extensions/env/ProjectEnvPanel.tsx +239 -0
  110. package/src/renderer/components/extensions/mcp/CustomMcpServerDialog.tsx +14 -223
  111. package/src/renderer/components/extensions/mcp/McpServerDetailDialog.tsx +111 -15
  112. package/src/renderer/components/extensions/mcp/McpServersPanel.tsx +51 -1
  113. package/src/renderer/components/extensions/skills/SkillsPanel.tsx +1 -126
  114. package/src/renderer/components/settings/sections/HarnessSection.tsx +2 -6
  115. package/src/renderer/components/settings/sections/TaskBusSection.tsx +17 -7
  116. package/src/renderer/components/sidebar/SidebarSessions.tsx +23 -0
  117. package/src/renderer/components/sidebar/WorkspaceBrowser.tsx +1 -7
  118. package/src/renderer/components/team/HarnessSelect.tsx +71 -0
  119. package/src/renderer/components/team/TeamDetailView.tsx +74 -123
  120. package/src/renderer/components/team/TeamListFilterPopover.tsx +0 -16
  121. package/src/renderer/components/team/TeamListView.tsx +7 -32
  122. package/src/renderer/components/team/dialogs/CreateTeamDialog.tsx +21 -12
  123. package/src/renderer/components/team/dialogs/EditTeamDialog.tsx +287 -418
  124. package/src/renderer/components/team/dialogs/useTeamEditForm.ts +283 -0
  125. package/src/renderer/components/team/kanban/KanbanBoard.tsx +26 -64
  126. package/src/renderer/components/team/messages/MessagesPanel.tsx +28 -24
  127. package/src/renderer/components/terminal/TerminalPanel.tsx +156 -0
  128. package/src/renderer/hooks/useExtensionsTabState.ts +2 -2
  129. package/src/renderer/store/slices/extensionsSlice.ts +42 -107
  130. package/src/renderer/store/slices/teamSlice.ts +8 -2
  131. package/src/renderer/utils/multimodelProviderVisibility.ts +17 -0
  132. package/src/renderer/utils/openCodeRuntimeDeliveryDiagnostics.ts +29 -9
  133. package/src/shared/types/api.ts +29 -0
  134. package/src/shared/types/extensions/index.ts +1 -0
  135. package/src/shared/types/extensions/mcp.ts +2 -0
  136. package/src/shared/types/extensions/plugin.ts +2 -1
  137. package/src/shared/types/extensions/skill.ts +7 -0
  138. package/src/shared/utils/providerExtensionCapabilities.ts +1 -1
  139. package/dist-renderer/assets/channel-C0SqeFU7.js +0 -1
  140. package/dist-renderer/assets/classDiagram-2ON5EDUG-DWew1HpM.js +0 -1
  141. package/dist-renderer/assets/classDiagram-v2-WZHVMYZB-DWew1HpM.js +0 -1
  142. package/dist-renderer/assets/clone-Dm-k63Yr.js +0 -1
  143. package/dist-renderer/assets/index-BhellmRb.css +0 -1
  144. package/dist-renderer/assets/treemap-GDKQZRPO-DU_yr827.js +0 -162
  145. package/src/features/recent-projects/main/adapters/input/http/registerRecentProjectsHttp.ts +0 -30
  146. package/src/features/recent-projects/main/adapters/output/presenters/DashboardRecentProjectsPresenter.ts +0 -27
  147. package/src/features/recent-projects/main/adapters/output/sources/ClaudeRecentProjectsSourceAdapter.ts +0 -91
  148. package/src/features/recent-projects/main/adapters/output/sources/CodexRecentProjectsSourceAdapter.ts +0 -326
  149. package/src/features/recent-projects/main/composition/createRecentProjectsFeature.ts +0 -43
  150. package/src/features/recent-projects/main/index.ts +0 -3
  151. package/src/features/recent-projects/main/infrastructure/cache/InMemoryRecentProjectsCache.ts +0 -34
  152. package/src/features/recent-projects/main/infrastructure/codex/CodexAppServerClient.ts +0 -116
  153. package/src/features/recent-projects/main/infrastructure/identity/RecentProjectIdentityResolver.ts +0 -20
  154. package/src/features/recent-projects/main/infrastructure/identity/normalizeIdentityPath.ts +0 -10
  155. package/src/renderer/components/extensions/apikeys/ApiKeyCard.tsx +0 -143
  156. package/src/renderer/components/extensions/apikeys/ApiKeyFormDialog.tsx +0 -282
  157. package/src/renderer/components/extensions/apikeys/ApiKeysPanel.tsx +0 -280
@@ -0,0 +1,554 @@
1
+ import {
2
+ type ChildProcess,
3
+ exec,
4
+ execFile,
5
+ type ExecFileOptions,
6
+ type ExecOptions,
7
+ spawn,
8
+ type SpawnOptions,
9
+ spawnSync,
10
+ } from 'child_process';
11
+ import { existsSync, readFileSync } from 'fs';
12
+ import path from 'path';
13
+
14
+ /**
15
+ * Promise wrapper for execFile that always returns { stdout, stderr }.
16
+ * Unlike promisify(execFile), this works correctly with mocked execFile
17
+ * (promisify relies on a custom symbol that mocks don't have).
18
+ */
19
+ function execFileAsync(
20
+ cmd: string,
21
+ args: string[],
22
+ options: ExecFileOptions = {}
23
+ ): Promise<{ stdout: string; stderr: string }> {
24
+ return new Promise((resolve, reject) => {
25
+ const { timeout, killSignal, ...execOptions } = options;
26
+ const timeoutMs = typeof timeout === 'number' && timeout > 0 ? timeout : 0;
27
+ const timeoutSignal = normalizeKillSignal(killSignal);
28
+ let child: ChildProcess | null = null;
29
+ let settled = false;
30
+ let stdoutText = '';
31
+ let stderrText = '';
32
+ let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
33
+ child = execFile(cmd, args, execOptions, (err, stdout, stderr) => {
34
+ if (settled) {
35
+ return;
36
+ }
37
+ settled = true;
38
+ timeoutHandle = cleanupTimedCliProcess(child, timeoutHandle);
39
+ if (err) {
40
+ const normalizedError =
41
+ err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'Unknown error');
42
+ Object.assign(normalizedError, {
43
+ stdout: String(stdout),
44
+ stderr: String(stderr),
45
+ });
46
+ reject(normalizedError);
47
+ } else resolve({ stdout: String(stdout), stderr: String(stderr) });
48
+ });
49
+ if (!settled) {
50
+ trackCliProcess(child);
51
+ if (timeoutMs > 0) {
52
+ child.stdout?.on('data', (chunk: Buffer | string) => {
53
+ stdoutText += chunk.toString();
54
+ });
55
+ child.stderr?.on('data', (chunk: Buffer | string) => {
56
+ stderrText += chunk.toString();
57
+ });
58
+ timeoutHandle = setTimeout(() => {
59
+ if (settled) {
60
+ return;
61
+ }
62
+ settled = true;
63
+ timeoutHandle = cleanupTimedCliProcess(child, timeoutHandle);
64
+ killProcessTree(child, timeoutSignal);
65
+ const error = new Error(
66
+ `Command timed out after ${timeoutMs}ms: ${cmd} ${args.join(' ')}`
67
+ );
68
+ Object.assign(error, {
69
+ killed: true,
70
+ signal: timeoutSignal,
71
+ stdout: stdoutText,
72
+ stderr: stderrText,
73
+ });
74
+ reject(error);
75
+ }, timeoutMs);
76
+ timeoutHandle.unref?.();
77
+ }
78
+ }
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Promise wrapper for exec. Used exclusively as a Windows shell fallback
84
+ * when execFile fails with EINVAL on non-ASCII binary paths. The command
85
+ * string is built from a known binary path + args, NOT from user input.
86
+ */
87
+ function execShellAsync(
88
+ cmd: string,
89
+ options: ExecOptions = {}
90
+ ): Promise<{ stdout: string; stderr: string }> {
91
+ return new Promise((resolve, reject) => {
92
+ const { timeout, killSignal, ...execOptions } = options;
93
+ const timeoutMs = typeof timeout === 'number' && timeout > 0 ? timeout : 0;
94
+ const timeoutSignal = normalizeKillSignal(killSignal);
95
+ let child: ChildProcess | null = null;
96
+ let settled = false;
97
+ let stdoutText = '';
98
+ let stderrText = '';
99
+ let timeoutHandle: ReturnType<typeof setTimeout> | null = null;
100
+ // eslint-disable-next-line sonarjs/os-command, security/detect-child-process -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
101
+ child = exec(cmd, execOptions, (err, stdout, stderr) => {
102
+ if (settled) {
103
+ return;
104
+ }
105
+ settled = true;
106
+ timeoutHandle = cleanupTimedCliProcess(child, timeoutHandle);
107
+ if (err)
108
+ reject(
109
+ err instanceof Error ? err : new Error(typeof err === 'string' ? err : 'Unknown error')
110
+ );
111
+ else resolve({ stdout: String(stdout), stderr: String(stderr) });
112
+ });
113
+ if (!settled) {
114
+ trackCliProcess(child);
115
+ if (timeoutMs > 0) {
116
+ child.stdout?.on('data', (chunk: Buffer | string) => {
117
+ stdoutText += chunk.toString();
118
+ });
119
+ child.stderr?.on('data', (chunk: Buffer | string) => {
120
+ stderrText += chunk.toString();
121
+ });
122
+ timeoutHandle = setTimeout(() => {
123
+ if (settled) {
124
+ return;
125
+ }
126
+ settled = true;
127
+ timeoutHandle = cleanupTimedCliProcess(child, timeoutHandle);
128
+ killProcessTree(child, timeoutSignal);
129
+ const error = new Error(`Command timed out after ${timeoutMs}ms: ${cmd}`);
130
+ Object.assign(error, {
131
+ killed: true,
132
+ signal: timeoutSignal,
133
+ stdout: stdoutText,
134
+ stderr: stderrText,
135
+ });
136
+ reject(error);
137
+ }, timeoutMs);
138
+ timeoutHandle.unref?.();
139
+ }
140
+ }
141
+ });
142
+ }
143
+
144
+ function cleanupTimedCliProcess(
145
+ child: ChildProcess | null,
146
+ timeoutHandle: ReturnType<typeof setTimeout> | null
147
+ ): null {
148
+ if (timeoutHandle) {
149
+ clearTimeout(timeoutHandle);
150
+ }
151
+ untrackCliProcess(child);
152
+ return null;
153
+ }
154
+
155
+ /**
156
+ * Returns true if the string contains any non-ASCII character.
157
+ */
158
+ function containsNonAscii(str: string): boolean {
159
+ return [...str].some((c) => c.charCodeAt(0) > 127);
160
+ }
161
+
162
+ /**
163
+ * On Windows, batch launchers need cmd.exe, and creating a process whose
164
+ * path contains non-ASCII characters will often fail with `spawn EINVAL`.
165
+ * Detect both cases so callers can launch through a shell when needed.
166
+ */
167
+ function needsShell(binaryPath: string): boolean {
168
+ if (process.platform !== 'win32') return false;
169
+ if (!binaryPath) return false;
170
+ const extension = path.extname(binaryPath).toLowerCase();
171
+ return extension === '.cmd' || extension === '.bat' || containsNonAscii(binaryPath);
172
+ }
173
+
174
+ interface DirectWindowsLauncher {
175
+ command: string;
176
+ argsPrefix: string[];
177
+ }
178
+
179
+ function isWindowsBatchLauncher(binaryPath: string): boolean {
180
+ const extension = path.extname(binaryPath).toLowerCase();
181
+ return extension === '.cmd' || extension === '.bat';
182
+ }
183
+
184
+ function resolveCmdPathTemplate(template: string, launcherDir: string): string {
185
+ const dirWithSep = launcherDir.endsWith(path.sep) ? launcherDir : `${launcherDir}${path.sep}`;
186
+ return path.resolve(
187
+ template
188
+ .replace(/%SCRIPT_DIR%/gi, dirWithSep)
189
+ .replace(/%~dp0/gi, dirWithSep)
190
+ .replace(/%dp0%/gi, dirWithSep)
191
+ .replace(/\\/g, path.sep)
192
+ );
193
+ }
194
+
195
+ function resolveGeneratedBunLauncher(
196
+ content: string,
197
+ launcherDir: string
198
+ ): DirectWindowsLauncher | null {
199
+ if (!/\bbun\s+"%TARGET%"\s+%\*/i.test(content)) {
200
+ return null;
201
+ }
202
+ const targetMatch = /set\s+"TARGET=([^"]+)"/i.exec(content);
203
+ const targetTemplate = targetMatch?.[1];
204
+ if (!targetTemplate) {
205
+ return null;
206
+ }
207
+
208
+ const target = resolveCmdPathTemplate(targetTemplate, launcherDir);
209
+ if (!existsSync(target)) {
210
+ return null;
211
+ }
212
+ return { command: 'bun', argsPrefix: [target] };
213
+ }
214
+
215
+ function resolveNpmNodeShim(content: string, launcherDir: string): DirectWindowsLauncher | null {
216
+ const scriptMatch = /"%_prog%"\s+"([^"]+(?:\.(?:cjs|mjs|js))?)"\s+%\*/i.exec(content);
217
+ const scriptTemplate = scriptMatch?.[1];
218
+ if (!scriptTemplate) {
219
+ return null;
220
+ }
221
+
222
+ const scriptPath = resolveCmdPathTemplate(scriptTemplate, launcherDir);
223
+ if (!existsSync(scriptPath)) {
224
+ return null;
225
+ }
226
+
227
+ const localNode = path.join(launcherDir, 'node.exe');
228
+ return {
229
+ command: existsSync(localNode) ? localNode : 'node',
230
+ argsPrefix: [scriptPath],
231
+ };
232
+ }
233
+
234
+ function resolveNpmNativeShim(content: string, launcherDir: string): DirectWindowsLauncher | null {
235
+ const nativeTarget = /(?:^|[&|])\s*"([^"]+\.(?:exe|com))"\s+%\*/im.exec(content)?.[1];
236
+ if (!nativeTarget) {
237
+ return null;
238
+ }
239
+
240
+ const target = resolveCmdPathTemplate(nativeTarget, launcherDir);
241
+ if (!existsSync(target)) {
242
+ return null;
243
+ }
244
+
245
+ return { command: target, argsPrefix: [] };
246
+ }
247
+
248
+ /**
249
+ * Some Windows launchers are thin wrappers around a real JS entrypoint.
250
+ * Running that entrypoint directly with an argv array avoids cmd.exe's
251
+ * percent expansion, which cannot safely represent args like `%PATH%`.
252
+ */
253
+ function resolveDirectWindowsLauncher(binaryPath: string): DirectWindowsLauncher | null {
254
+ if (process.platform !== 'win32' || !isWindowsBatchLauncher(binaryPath)) {
255
+ return null;
256
+ }
257
+
258
+ try {
259
+ const content = readFileSync(binaryPath, 'utf8');
260
+ const launcherDir = path.dirname(binaryPath);
261
+ return (
262
+ resolveGeneratedBunLauncher(content, launcherDir) ??
263
+ resolveNpmNodeShim(content, launcherDir) ??
264
+ resolveNpmNativeShim(content, launcherDir)
265
+ );
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Quote an argument for cmd.exe shell invocation on Windows.
273
+ *
274
+ * cmd.exe rules:
275
+ * - Double-quote args containing spaces or special characters
276
+ * - Inside double quotes, escape literal `"` as `\"` for the target argv parser
277
+ * - Double trailing backslashes so they do not escape the closing quote
278
+ * - `%` is expanded as env var even inside double quotes. Keep it outside
279
+ * quoted chunks and escape it as `^%`.
280
+ * - `^`, `&`, `|`, `<`, `>` are safe inside double quotes
281
+ *
282
+ * Our callers only pass controlled strings (binary paths, CLI flags),
283
+ * NOT arbitrary user input.
284
+ */
285
+ function quoteCmdChunk(chunk: string): string {
286
+ const escaped = chunk
287
+ .replace(/(\\*)"/g, (_match, backslashes: string) => `${backslashes}${backslashes}\\"`)
288
+ .replace(/(\\+)$/g, '$1$1');
289
+ return `"${escaped}"`;
290
+ }
291
+
292
+ export function quoteWindowsCmdArg(arg: string): string {
293
+ if (/[^A-Za-z0-9_\-/.]/.test(arg)) {
294
+ return arg.split('%').map(quoteCmdChunk).join('^%');
295
+ }
296
+ return arg;
297
+ }
298
+
299
+ function quoteArg(arg: string): string {
300
+ return quoteWindowsCmdArg(arg);
301
+ }
302
+
303
+ /** Env vars injected into every spawned Claude CLI process. */
304
+ const CLI_ENV_DEFAULTS: Record<string, string> = {
305
+ CLAUDE_HOOK_JUDGE_MODE: 'true',
306
+ };
307
+
308
+ const activeCliProcesses = new Set<ChildProcess>();
309
+
310
+ export function untrackCliProcess(child: ChildProcess | null): void {
311
+ if (child) {
312
+ activeCliProcesses.delete(child);
313
+ }
314
+ }
315
+
316
+ function trackCliProcess<T extends ChildProcess>(child: T): T {
317
+ activeCliProcesses.add(child);
318
+ const cleanup = (): void => {
319
+ activeCliProcesses.delete(child);
320
+ };
321
+ child.once?.('exit', cleanup);
322
+ child.once?.('close', cleanup);
323
+ child.once?.('error', cleanup);
324
+ return child;
325
+ }
326
+
327
+ export function killTrackedCliProcesses(signal: NodeJS.Signals = 'SIGKILL'): void {
328
+ for (const child of Array.from(activeCliProcesses)) {
329
+ try {
330
+ killProcessTree(child, signal);
331
+ } catch {
332
+ // Best effort during shutdown.
333
+ }
334
+ }
335
+ }
336
+
337
+ /** Apply shared CLI process defaults without overriding explicit caller choices. */
338
+ function withCliProcessDefaults<
339
+ T extends {
340
+ env?: NodeJS.ProcessEnv | Record<string, string | undefined>;
341
+ windowsHide?: boolean;
342
+ },
343
+ >(options: T): T & { windowsHide: boolean } {
344
+ return {
345
+ ...options,
346
+ windowsHide: options.windowsHide ?? true,
347
+ env: { ...(options.env ?? process.env), ...CLI_ENV_DEFAULTS },
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Execute a CLI binary, falling back to running the command through a
353
+ * shell on Windows if the normal path-based spawn fails.
354
+ *
355
+ * The return value matches the shape of Node's `execFile` promise: an
356
+ * object with `stdout` and `stderr` strings.
357
+ */
358
+ export interface ExecCliOptions extends ExecFileOptions {
359
+ /**
360
+ * Some generated Windows launchers are safe to run directly, but callers can
361
+ * force the .cmd/.bat path when they need the launcher environment exactly.
362
+ */
363
+ preferShellForWindowsBatch?: boolean;
364
+ }
365
+
366
+ export async function execCli(
367
+ binaryPath: string | null,
368
+ args: string[],
369
+ options: ExecCliOptions = {}
370
+ ): Promise<{ stdout: string; stderr: string }> {
371
+ if (!binaryPath) {
372
+ throw new Error(
373
+ 'Claude CLI binary path is null. Resolve the binary via ClaudeBinaryResolver before calling execCli.'
374
+ );
375
+ }
376
+ const target = binaryPath;
377
+ const { preferShellForWindowsBatch = false, ...execOptions } = options;
378
+ const opts = withCliProcessDefaults(execOptions);
379
+ const directLauncher =
380
+ preferShellForWindowsBatch && isWindowsBatchLauncher(target)
381
+ ? null
382
+ : resolveDirectWindowsLauncher(target);
383
+ if (directLauncher) {
384
+ const result = await execFileAsync(
385
+ directLauncher.command,
386
+ [...directLauncher.argsPrefix, ...args],
387
+ opts
388
+ );
389
+ return { stdout: String(result.stdout), stderr: String(result.stderr) };
390
+ }
391
+
392
+ // attempt the normal execFile path first
393
+ if (!needsShell(target)) {
394
+ try {
395
+ const result = await execFileAsync(target, args, opts);
396
+ return { stdout: String(result.stdout), stderr: String(result.stderr) };
397
+ } catch (err: unknown) {
398
+ // fall through to shell fallback only when the error matches the
399
+ // Windows "invalid argument" problem; otherwise rethrow.
400
+ const code =
401
+ err && typeof err === 'object' && 'code' in err
402
+ ? (err as { code?: string }).code
403
+ : undefined;
404
+ if (code !== 'EINVAL') {
405
+ throw err;
406
+ }
407
+ }
408
+ }
409
+
410
+ // shell fallback (Windows only; others shouldn't reach here)
411
+ const cmd = [target, ...args].map(quoteArg).join(' ');
412
+ const shellResult = await execShellAsync(cmd, opts as unknown as ExecOptions);
413
+ return { stdout: String(shellResult.stdout), stderr: String(shellResult.stderr) };
414
+ }
415
+
416
+ /**
417
+ * Spawn a child process. If the initial `spawn()` call throws
418
+ * synchronously with EINVAL on Windows, retry using a shell-based
419
+ * command string. The returned `ChildProcess` is whatever the
420
+ * underlying call returned; listeners may safely be attached to it.
421
+ */
422
+ export function spawnCli(
423
+ binaryPath: string,
424
+ args: string[],
425
+ options: SpawnOptions = {}
426
+ ): ReturnType<typeof spawn> {
427
+ const opts = withCliProcessDefaults(options);
428
+ const directLauncher = resolveDirectWindowsLauncher(binaryPath);
429
+ if (directLauncher) {
430
+ const directOpts = { ...opts };
431
+ delete directOpts.shell;
432
+ return trackCliProcess(
433
+ spawn(directLauncher.command, [...directLauncher.argsPrefix, ...args], directOpts)
434
+ );
435
+ }
436
+
437
+ if (process.platform === 'win32' && needsShell(binaryPath)) {
438
+ const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
439
+ // eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
440
+ return trackCliProcess(spawn(cmd, { ...opts, shell: true }));
441
+ }
442
+
443
+ try {
444
+ return trackCliProcess(spawn(binaryPath, args, opts));
445
+ } catch (err: unknown) {
446
+ const code =
447
+ err && typeof err === 'object' && 'code' in err ? (err as { code?: string }).code : undefined;
448
+ if (process.platform === 'win32' && code === 'EINVAL') {
449
+ const cmd = [binaryPath, ...args].map(quoteArg).join(' ');
450
+ // eslint-disable-next-line sonarjs/os-command -- cmd from known binaryPath+args, not user input (Windows EINVAL fallback)
451
+ return trackCliProcess(spawn(cmd, { ...opts, shell: true }));
452
+ }
453
+ throw err;
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Kill a child process and its entire process tree.
459
+ *
460
+ * On Windows with `shell: true`, `child.kill()` only kills the intermediate
461
+ * `cmd.exe` shell, leaving the actual process (e.g. `claude.cmd`) orphaned.
462
+ * `taskkill /T /F /PID` recursively kills the entire process tree.
463
+ *
464
+ * On macOS/Linux, kill the child and descendants by PID so shell wrappers
465
+ * and spawned grandchildren do not survive a timeout or team stop.
466
+ */
467
+ export function killProcessTree(
468
+ child: ChildProcess | null | undefined,
469
+ signal?: NodeJS.Signals
470
+ ): void {
471
+ if (!child?.pid) {
472
+ // Process is null, never started, or already exited
473
+ return;
474
+ }
475
+
476
+ if (process.platform === 'win32') {
477
+ try {
478
+ const taskkillPath = path.join(
479
+ process.env.SystemRoot ?? 'C:\\Windows',
480
+ 'System32',
481
+ 'taskkill.exe'
482
+ );
483
+ execFile(taskkillPath, ['/T', '/F', '/PID', String(child.pid)], { windowsHide: true }, () => {
484
+ // Best-effort - ignore errors (process may have already exited)
485
+ });
486
+ return;
487
+ } catch {
488
+ // taskkill failed, fall through to standard kill
489
+ }
490
+ }
491
+
492
+ const childPid = child.pid;
493
+ const descendants = getDescendantProcessIds(childPid);
494
+ const targetSignal = signal ?? 'SIGTERM';
495
+ for (const pid of [childPid, ...descendants.reverse()]) {
496
+ try {
497
+ process.kill(pid, targetSignal);
498
+ } catch {
499
+ // Best-effort - process may have already exited.
500
+ }
501
+ }
502
+ }
503
+
504
+ function normalizeKillSignal(signal: ExecFileOptions['killSignal']): NodeJS.Signals {
505
+ return typeof signal === 'string' ? signal : 'SIGTERM';
506
+ }
507
+
508
+ function getDescendantProcessIds(parentPid: number): number[] {
509
+ if (process.platform === 'win32') {
510
+ return [];
511
+ }
512
+
513
+ try {
514
+ const result = spawnSync('ps', ['-axo', 'pid=,ppid='], {
515
+ encoding: 'utf8',
516
+ windowsHide: true,
517
+ });
518
+ if (result.error || result.status !== 0 || typeof result.stdout !== 'string') {
519
+ return [];
520
+ }
521
+
522
+ const childrenByParent = new Map<number, number[]>();
523
+ for (const line of result.stdout.split('\n')) {
524
+ const match = /^(\d+)\s+(\d+)$/.exec(line.trim());
525
+ if (!match) {
526
+ continue;
527
+ }
528
+ const pid = Number(match[1]);
529
+ const ppid = Number(match[2]);
530
+ const children = childrenByParent.get(ppid);
531
+ if (children) {
532
+ children.push(pid);
533
+ } else {
534
+ childrenByParent.set(ppid, [pid]);
535
+ }
536
+ }
537
+
538
+ const descendants: number[] = [];
539
+ const stack = [...(childrenByParent.get(parentPid) ?? [])];
540
+ const seen = new Set<number>();
541
+ while (stack.length > 0) {
542
+ const pid = stack.pop();
543
+ if (!pid || seen.has(pid) || pid === process.pid) {
544
+ continue;
545
+ }
546
+ seen.add(pid);
547
+ descendants.push(pid);
548
+ stack.push(...(childrenByParent.get(pid) ?? []));
549
+ }
550
+ return descendants;
551
+ } catch {
552
+ return [];
553
+ }
554
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Builds an enriched environment for Claude CLI child processes.
3
+ *
4
+ * Packaged Electron apps on macOS receive a minimal PATH (often just /usr/bin:/bin)
5
+ * and may lack USER (needed for macOS Keychain credential lookup).
6
+ * This helper merges the user's interactive-shell env (cached during startup) with
7
+ * common install locations so that `claude` and its subprocesses (node, npx, etc.)
8
+ * can find the tools they need and authenticate properly.
9
+ */
10
+
11
+ import { applyAgentTeamsIdentityEnv } from '@main/services/identity/AgentTeamsIdentityStore';
12
+ import { buildMergedCliPath } from '@main/utils/cliPathMerge';
13
+ import { getAutoDetectedClaudeBasePath, getClaudeBasePath } from '@main/utils/pathDecoder';
14
+ import { getCachedShellEnv, getShellPreferredHome } from '@main/utils/shellEnv';
15
+ import { userInfo } from 'os';
16
+
17
+ export function buildEnrichedEnv(binaryPath?: string | null): NodeJS.ProcessEnv {
18
+ const shellEnv = getCachedShellEnv();
19
+ const home = getShellPreferredHome();
20
+ let osUsername = '';
21
+ try {
22
+ osUsername = userInfo().username;
23
+ } catch {
24
+ // userInfo() can throw in restricted environments (Docker, no passwd entry)
25
+ }
26
+ const user =
27
+ shellEnv?.USER?.trim() ||
28
+ process.env.USER?.trim() ||
29
+ process.env.USERNAME?.trim() ||
30
+ osUsername ||
31
+ '';
32
+
33
+ // Only set CLAUDE_CONFIG_DIR when the user has configured a custom path.
34
+ // Setting it to the default ~/.claude changes the macOS Keychain namespace
35
+ // that the CLI uses for OAuth credential lookup, causing "not logged in"
36
+ // even though `claude auth login` succeeded without the env var.
37
+ const configDir = getClaudeBasePath();
38
+ const isCustomConfigDir = configDir !== getAutoDetectedClaudeBasePath();
39
+
40
+ return applyAgentTeamsIdentityEnv({
41
+ ...process.env,
42
+ ...(shellEnv ?? {}),
43
+ HOME: home,
44
+ USERPROFILE: home,
45
+ PATH: buildMergedCliPath(binaryPath),
46
+ ...(isCustomConfigDir ? { CLAUDE_CONFIG_DIR: configDir } : {}),
47
+ ...(user
48
+ ? {
49
+ USER: user,
50
+ LOGNAME: shellEnv?.LOGNAME?.trim() || process.env.LOGNAME?.trim() || user,
51
+ }
52
+ : {}),
53
+ });
54
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Merged PATH for Claude CLI discovery and child processes.
3
+ * Packaged macOS apps get a minimal PATH; login-shell cache fixes that once warm.
4
+ */
5
+
6
+ import { getClaudeBasePath } from '@main/utils/pathDecoder';
7
+ import { getCachedShellEnv, getShellPreferredHome } from '@main/utils/shellEnv';
8
+ import { realpathSync } from 'fs';
9
+ import { posix as pathPosix, win32 as pathWin32 } from 'path';
10
+
11
+ /**
12
+ * Build a PATH string that prefers the CLI binary directory, then the user's
13
+ * interactive shell PATH (when cached), then common install locations, then the
14
+ * current process PATH.
15
+ */
16
+ export function buildMergedCliPath(binaryPath?: string | null): string {
17
+ const home = getShellPreferredHome();
18
+ const sep = process.platform === 'win32' ? pathWin32.delimiter : pathPosix.delimiter;
19
+ const pathForBin = process.platform === 'win32' ? pathWin32 : pathPosix;
20
+ const currentPath = process.env.PATH || '';
21
+ const extraDirs: string[] = [];
22
+ const vendorBinDir = pathForBin.join(getClaudeBasePath(), 'local', 'node_modules', '.bin');
23
+
24
+ if (binaryPath) {
25
+ const binDir = pathForBin.dirname(binaryPath);
26
+ extraDirs.push(binDir);
27
+ try {
28
+ const realBinDir = pathForBin.dirname(realpathSync(binaryPath));
29
+ if (realBinDir !== binDir) {
30
+ extraDirs.push(realBinDir);
31
+ }
32
+ } catch {
33
+ /* symlink resolution failed — ignore */
34
+ }
35
+ }
36
+
37
+ const cachedEnv = getCachedShellEnv();
38
+ if (cachedEnv?.PATH) {
39
+ extraDirs.push(...cachedEnv.PATH.split(sep).filter(Boolean));
40
+ }
41
+
42
+ if (process.platform === 'win32') {
43
+ extraDirs.push(
44
+ vendorBinDir,
45
+ pathWin32.join(home, 'AppData', 'Roaming', 'npm'),
46
+ pathWin32.join(home, 'scoop', 'shims'),
47
+ pathWin32.join(home, '.bun', 'bin'),
48
+ pathWin32.join(home, '.cargo', 'bin'),
49
+ pathWin32.join(home, '.volta', 'bin')
50
+ );
51
+ if (process.env.LOCALAPPDATA) {
52
+ extraDirs.push(
53
+ pathWin32.join(process.env.LOCALAPPDATA, 'Programs', 'claude'),
54
+ pathWin32.join(process.env.LOCALAPPDATA, 'pnpm')
55
+ );
56
+ }
57
+ if (process.env.ProgramFiles) {
58
+ extraDirs.push(
59
+ pathWin32.join(process.env.ProgramFiles, 'claude'),
60
+ pathWin32.join(process.env.ProgramFiles, 'nodejs')
61
+ );
62
+ }
63
+ } else {
64
+ extraDirs.push(
65
+ vendorBinDir,
66
+ pathPosix.join(home, '.bun', 'bin'),
67
+ pathPosix.join(home, '.local', 'bin'),
68
+ pathPosix.join(home, '.npm-global', 'bin'),
69
+ pathPosix.join(home, '.npm', 'bin'),
70
+ pathPosix.join(home, '.asdf', 'shims'),
71
+ pathPosix.join(home, '.local', 'share', 'mise', 'shims'),
72
+ pathPosix.join(home, '.volta', 'bin'),
73
+ pathPosix.join(home, 'Library', 'pnpm'),
74
+ pathPosix.join(home, '.local', 'share', 'pnpm'),
75
+ pathPosix.join(home, '.cargo', 'bin'),
76
+ pathPosix.join(home, '.nix-profile', 'bin'),
77
+ '/usr/local/bin',
78
+ '/opt/homebrew/bin',
79
+ '/opt/local/bin',
80
+ '/usr/bin',
81
+ '/bin',
82
+ '/usr/sbin',
83
+ '/sbin'
84
+ );
85
+ }
86
+
87
+ const seen = new Set<string>();
88
+ const merged: string[] = [];
89
+ for (const dir of [...extraDirs, ...currentPath.split(sep)]) {
90
+ if (dir && !seen.has(dir)) {
91
+ seen.add(dir);
92
+ merged.push(dir);
93
+ }
94
+ }
95
+
96
+ return merged.join(sep);
97
+ }