@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,411 @@
1
+ import { createHash } from 'node:crypto';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+
6
+ import { SkillScanner } from './SkillScanner';
7
+
8
+ import type { ImportedSkillSourceFile } from './SkillImportService';
9
+ import type {
10
+ SkillDraftFile,
11
+ SkillReviewFileChange,
12
+ SkillReviewPreview,
13
+ SkillReviewSummary,
14
+ } from '@shared/types/extensions';
15
+
16
+ type SkillPlanInputFile =
17
+ | { relativePath: string; isBinary: false; content: string }
18
+ | { relativePath: string; isBinary: true; sourceAbsolutePath: string };
19
+
20
+ interface ManagedCurrentFile {
21
+ relativePath: string;
22
+ absolutePath: string;
23
+ }
24
+
25
+ interface SkillExecutionChange extends SkillReviewFileChange {
26
+ sourceAbsolutePath?: string;
27
+ }
28
+
29
+ export interface SkillExecutionPlan {
30
+ preview: SkillReviewPreview;
31
+ changes: SkillExecutionChange[];
32
+ }
33
+
34
+ const MANAGED_SUBDIRECTORIES = ['scripts', 'references', 'assets'] as const;
35
+ export class SkillPlanService {
36
+ constructor(private readonly scanner = new SkillScanner()) {}
37
+
38
+ async buildUpsertPlan(
39
+ targetSkillDir: string,
40
+ files: SkillDraftFile[]
41
+ ): Promise<SkillExecutionPlan> {
42
+ const desiredFiles: SkillPlanInputFile[] = files.map((file) => ({
43
+ relativePath: file.relativePath,
44
+ isBinary: false,
45
+ content: file.content,
46
+ }));
47
+
48
+ return this.buildPlan(targetSkillDir, desiredFiles, 'upsert');
49
+ }
50
+
51
+ async buildImportPlan(
52
+ targetSkillDir: string,
53
+ files: ImportedSkillSourceFile[]
54
+ ): Promise<SkillExecutionPlan> {
55
+ const desiredFiles: SkillPlanInputFile[] = files.map((file) =>
56
+ file.isBinary
57
+ ? {
58
+ relativePath: file.relativePath,
59
+ isBinary: true,
60
+ sourceAbsolutePath: file.absolutePath,
61
+ }
62
+ : {
63
+ relativePath: file.relativePath,
64
+ isBinary: false,
65
+ content: file.content ?? '',
66
+ }
67
+ );
68
+
69
+ return this.buildPlan(targetSkillDir, desiredFiles, 'import');
70
+ }
71
+
72
+ async applyPlan(plan: SkillExecutionPlan): Promise<void> {
73
+ const backupRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'skill-plan-backup-'));
74
+ const createdPaths: string[] = [];
75
+ const backups: { absolutePath: string; backupPath: string }[] = [];
76
+
77
+ try {
78
+ for (const [index, change] of plan.changes.entries()) {
79
+ if (change.action !== 'create' && (await this.pathExists(change.absolutePath))) {
80
+ const backupPath = path.join(backupRoot, String(index));
81
+ await fs.mkdir(path.dirname(backupPath), { recursive: true });
82
+ await fs.copyFile(change.absolutePath, backupPath);
83
+ backups.push({ absolutePath: change.absolutePath, backupPath });
84
+ }
85
+
86
+ if (change.action === 'delete') {
87
+ await fs.rm(change.absolutePath, { force: true });
88
+ await this.cleanupManagedParents(
89
+ path.dirname(change.absolutePath),
90
+ plan.preview.targetSkillDir
91
+ );
92
+ continue;
93
+ }
94
+
95
+ await fs.mkdir(path.dirname(change.absolutePath), { recursive: true });
96
+ if (change.isBinary) {
97
+ if (!change.sourceAbsolutePath) {
98
+ throw new Error(`Missing binary source for ${change.relativePath}`);
99
+ }
100
+ await fs.copyFile(change.sourceAbsolutePath, change.absolutePath);
101
+ } else {
102
+ await fs.writeFile(change.absolutePath, change.newContent ?? '', 'utf8');
103
+ }
104
+
105
+ if (change.action === 'create') {
106
+ createdPaths.push(change.absolutePath);
107
+ }
108
+ }
109
+
110
+ await this.cleanupManagedDirectories(plan.preview.targetSkillDir);
111
+ } catch (error) {
112
+ await Promise.all(
113
+ createdPaths
114
+ .slice()
115
+ .reverse()
116
+ .map(async (absolutePath) => {
117
+ await fs.rm(absolutePath, { force: true });
118
+ await this.cleanupManagedParents(
119
+ path.dirname(absolutePath),
120
+ plan.preview.targetSkillDir
121
+ );
122
+ })
123
+ );
124
+
125
+ await Promise.all(
126
+ backups
127
+ .slice()
128
+ .reverse()
129
+ .map(async ({ absolutePath, backupPath }) => {
130
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
131
+ await fs.copyFile(backupPath, absolutePath);
132
+ })
133
+ );
134
+
135
+ throw error;
136
+ } finally {
137
+ await fs.rm(backupRoot, { recursive: true, force: true });
138
+ }
139
+ }
140
+
141
+ private async buildPlan(
142
+ targetSkillDir: string,
143
+ desiredFiles: SkillPlanInputFile[],
144
+ mode: 'upsert' | 'import'
145
+ ): Promise<SkillExecutionPlan> {
146
+ const normalizedDesired = this.normalizeDesiredFiles(desiredFiles);
147
+ const [currentManagedFiles, allExistingFiles] = await Promise.all([
148
+ this.readCurrentManagedFiles(targetSkillDir),
149
+ this.listAllRelativeFiles(targetSkillDir),
150
+ ]);
151
+
152
+ const changesByRelativePath = new Map<string, SkillExecutionChange>();
153
+
154
+ await Promise.all(
155
+ normalizedDesired.map(async (file) => {
156
+ const absolutePath = path.join(targetSkillDir, file.relativePath);
157
+ const existingTextContent = file.isBinary
158
+ ? null
159
+ : await this.readUtf8IfExists(absolutePath);
160
+ const action = (await this.pathExists(absolutePath)) ? 'update' : 'create';
161
+ changesByRelativePath.set(file.relativePath, {
162
+ relativePath: file.relativePath,
163
+ absolutePath,
164
+ action,
165
+ oldContent: existingTextContent,
166
+ newContent: file.isBinary ? null : file.content,
167
+ isBinary: file.isBinary,
168
+ sourceAbsolutePath: file.isBinary ? file.sourceAbsolutePath : undefined,
169
+ });
170
+ })
171
+ );
172
+
173
+ for (const currentFile of currentManagedFiles.values()) {
174
+ if (changesByRelativePath.has(currentFile.relativePath)) {
175
+ continue;
176
+ }
177
+
178
+ const existingTextContent = await this.readUtf8IfExists(currentFile.absolutePath);
179
+ changesByRelativePath.set(currentFile.relativePath, {
180
+ relativePath: currentFile.relativePath,
181
+ absolutePath: currentFile.absolutePath,
182
+ action: 'delete',
183
+ oldContent: existingTextContent,
184
+ newContent: null,
185
+ isBinary: false,
186
+ });
187
+ }
188
+
189
+ const changes = [...changesByRelativePath.values()].sort((a, b) =>
190
+ a.relativePath.localeCompare(b.relativePath)
191
+ );
192
+ const warnings = this.buildWarnings({
193
+ changes,
194
+ currentManagedFiles,
195
+ allExistingFiles,
196
+ desiredFiles: new Set(normalizedDesired.map((file) => file.relativePath)),
197
+ mode,
198
+ });
199
+
200
+ const summary = changes.reduce<SkillReviewSummary>(
201
+ (acc, change) => {
202
+ acc[`${change.action}d`] += 1;
203
+ if (change.isBinary) {
204
+ acc.binary += 1;
205
+ }
206
+ return acc;
207
+ },
208
+ { created: 0, updated: 0, deleted: 0, binary: 0 }
209
+ );
210
+
211
+ const preview: SkillReviewPreview = {
212
+ planId: this.buildPlanId(targetSkillDir, changes, warnings),
213
+ targetSkillDir,
214
+ changes: changes.map(({ sourceAbsolutePath: _sourceAbsolutePath, ...change }) => change),
215
+ warnings,
216
+ summary,
217
+ };
218
+
219
+ return { preview, changes };
220
+ }
221
+
222
+ private normalizeDesiredFiles(files: SkillPlanInputFile[]): SkillPlanInputFile[] {
223
+ const map = new Map<string, SkillPlanInputFile>();
224
+ for (const file of files) {
225
+ const normalizedPath = path.normalize(file.relativePath).replace(/\\/g, '/');
226
+ map.set(normalizedPath, { ...file, relativePath: normalizedPath });
227
+ }
228
+ return [...map.values()].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
229
+ }
230
+
231
+ private async readCurrentManagedFiles(
232
+ targetSkillDir: string
233
+ ): Promise<Map<string, ManagedCurrentFile>> {
234
+ const files = new Map<string, ManagedCurrentFile>();
235
+ const detectedSkillFile = await this.scanner.detectSkillFile(targetSkillDir);
236
+ if (detectedSkillFile) {
237
+ files.set(path.basename(detectedSkillFile), {
238
+ relativePath: path.basename(detectedSkillFile),
239
+ absolutePath: detectedSkillFile,
240
+ });
241
+ }
242
+
243
+ for (const directory of MANAGED_SUBDIRECTORIES) {
244
+ const fullDirectoryPath = path.join(targetSkillDir, directory);
245
+ const relativeFiles = await this.listAllRelativeFiles(fullDirectoryPath);
246
+ for (const relativePath of relativeFiles) {
247
+ const managedRelativePath = `${directory}/${relativePath}`;
248
+ files.set(managedRelativePath, {
249
+ relativePath: managedRelativePath,
250
+ absolutePath: path.join(fullDirectoryPath, relativePath),
251
+ });
252
+ }
253
+ }
254
+
255
+ return files;
256
+ }
257
+
258
+ private async listAllRelativeFiles(rootDir: string): Promise<string[]> {
259
+ try {
260
+ const rootStat = await fs.stat(rootDir);
261
+ if (!rootStat.isDirectory()) {
262
+ return [];
263
+ }
264
+ } catch {
265
+ return [];
266
+ }
267
+
268
+ const dirEntries = await fs.readdir(rootDir, { withFileTypes: true });
269
+ const entries = await Promise.all(
270
+ dirEntries.map(async (entry) => {
271
+ const fullPath = path.join(rootDir, entry.name);
272
+ if (entry.isDirectory()) {
273
+ const children = await this.listAllRelativeFiles(fullPath);
274
+ return children.map((child) => path.join(entry.name, child).replace(/\\/g, '/'));
275
+ }
276
+ return [entry.name];
277
+ })
278
+ );
279
+
280
+ return entries.flat().sort((a, b) => a.localeCompare(b));
281
+ }
282
+
283
+ private buildWarnings({
284
+ changes,
285
+ currentManagedFiles,
286
+ allExistingFiles,
287
+ desiredFiles,
288
+ mode,
289
+ }: {
290
+ changes: SkillExecutionChange[];
291
+ currentManagedFiles: Map<string, ManagedCurrentFile>;
292
+ allExistingFiles: string[];
293
+ desiredFiles: Set<string>;
294
+ mode: 'upsert' | 'import';
295
+ }): string[] {
296
+ const warnings: string[] = [];
297
+ const deleteCount = changes.filter((change) => change.action === 'delete').length;
298
+ const updateCount = changes.filter((change) => change.action === 'update').length;
299
+ const binaryCount = changes.filter((change) => change.isBinary).length;
300
+
301
+ if (deleteCount > 0) {
302
+ warnings.push(
303
+ deleteCount === 1
304
+ ? '1 managed file will be removed to match this reviewed plan.'
305
+ : `${deleteCount} managed files will be removed to match this reviewed plan.`
306
+ );
307
+ }
308
+
309
+ if (updateCount > 0) {
310
+ warnings.push(
311
+ updateCount === 1
312
+ ? '1 existing file will be overwritten.'
313
+ : `${updateCount} existing files will be overwritten.`
314
+ );
315
+ }
316
+
317
+ if (binaryCount > 0) {
318
+ warnings.push(
319
+ binaryCount === 1
320
+ ? '1 binary file will be copied as-is.'
321
+ : `${binaryCount} binary files will be copied as-is.`
322
+ );
323
+ }
324
+
325
+ const managedPaths = new Set(currentManagedFiles.keys());
326
+ const unmanagedFiles = allExistingFiles.filter(
327
+ (relativePath) => !managedPaths.has(relativePath) && !desiredFiles.has(relativePath)
328
+ );
329
+ if (unmanagedFiles.length > 0) {
330
+ warnings.push(
331
+ mode === 'import'
332
+ ? 'Existing files outside the imported plan will be kept as-is.'
333
+ : 'Existing files outside the managed skill set will be kept as-is.'
334
+ );
335
+ }
336
+
337
+ return warnings;
338
+ }
339
+
340
+ private buildPlanId(
341
+ targetSkillDir: string,
342
+ changes: SkillExecutionChange[],
343
+ warnings: string[]
344
+ ): string {
345
+ const hash = createHash('sha256');
346
+ hash.update(targetSkillDir);
347
+ hash.update('\n');
348
+ for (const change of changes) {
349
+ hash.update(
350
+ JSON.stringify({
351
+ relativePath: change.relativePath,
352
+ action: change.action,
353
+ oldContent: change.oldContent,
354
+ newContent: change.newContent,
355
+ isBinary: change.isBinary,
356
+ sourceAbsolutePath: change.sourceAbsolutePath ?? null,
357
+ })
358
+ );
359
+ hash.update('\n');
360
+ }
361
+ for (const warning of warnings) {
362
+ hash.update(warning);
363
+ hash.update('\n');
364
+ }
365
+ return hash.digest('hex');
366
+ }
367
+
368
+ private async readUtf8IfExists(filePath: string): Promise<string | null> {
369
+ try {
370
+ return await fs.readFile(filePath, 'utf8');
371
+ } catch (error) {
372
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
373
+ return null;
374
+ }
375
+ return null;
376
+ }
377
+ }
378
+
379
+ private async pathExists(targetPath: string): Promise<boolean> {
380
+ try {
381
+ await fs.stat(targetPath);
382
+ return true;
383
+ } catch {
384
+ return false;
385
+ }
386
+ }
387
+
388
+ private async cleanupManagedDirectories(targetSkillDir: string): Promise<void> {
389
+ await Promise.all(
390
+ MANAGED_SUBDIRECTORIES.map((directory) =>
391
+ this.cleanupManagedParents(path.join(targetSkillDir, directory), targetSkillDir)
392
+ )
393
+ );
394
+ }
395
+
396
+ private async cleanupManagedParents(currentDir: string, targetSkillDir: string): Promise<void> {
397
+ let nextDir = currentDir;
398
+ while (nextDir.startsWith(targetSkillDir) && nextDir !== targetSkillDir) {
399
+ try {
400
+ const entries = await fs.readdir(nextDir);
401
+ if (entries.length > 0) {
402
+ return;
403
+ }
404
+ await fs.rm(nextDir, { recursive: true });
405
+ } catch {
406
+ return;
407
+ }
408
+ nextDir = path.dirname(nextDir);
409
+ }
410
+ }
411
+ }
@@ -0,0 +1,73 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+
4
+ import { createLogger } from '@shared/utils/logger';
5
+
6
+ import type { ImportedSkillSourceFile } from './SkillImportService';
7
+ import type { SkillDraftFile, SkillReviewFileChange } from '@shared/types/extensions';
8
+
9
+ const logger = createLogger('Extensions:SkillReview');
10
+
11
+ export class SkillReviewService {
12
+ async buildTextChanges(
13
+ targetSkillDir: string,
14
+ files: SkillDraftFile[]
15
+ ): Promise<SkillReviewFileChange[]> {
16
+ return Promise.all(
17
+ files.map(async (file) => {
18
+ const absolutePath = path.join(targetSkillDir, file.relativePath);
19
+ const oldContent = await this.readUtf8IfExists(absolutePath);
20
+ return {
21
+ relativePath: file.relativePath,
22
+ absolutePath,
23
+ action: oldContent === null ? 'create' : 'update',
24
+ oldContent,
25
+ newContent: file.content,
26
+ isBinary: false,
27
+ } satisfies SkillReviewFileChange;
28
+ })
29
+ );
30
+ }
31
+
32
+ async buildImportChanges(
33
+ targetSkillDir: string,
34
+ files: ImportedSkillSourceFile[]
35
+ ): Promise<SkillReviewFileChange[]> {
36
+ return Promise.all(
37
+ files.map(async (file) => {
38
+ const destPath = path.join(targetSkillDir, file.relativePath);
39
+ const exists = await this.pathExists(destPath);
40
+ const oldContent = file.isBinary ? null : await this.readUtf8IfExists(destPath);
41
+ return {
42
+ relativePath: file.relativePath,
43
+ absolutePath: destPath,
44
+ action: exists ? 'update' : 'create',
45
+ oldContent,
46
+ newContent: file.isBinary ? null : file.content,
47
+ isBinary: file.isBinary,
48
+ } satisfies SkillReviewFileChange;
49
+ })
50
+ );
51
+ }
52
+
53
+ private async readUtf8IfExists(filePath: string): Promise<string | null> {
54
+ try {
55
+ return await fs.readFile(filePath, 'utf8');
56
+ } catch (error) {
57
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
58
+ return null;
59
+ }
60
+ logger.warn(`Failed to read existing file ${filePath}`, error);
61
+ return null;
62
+ }
63
+ }
64
+
65
+ private async pathExists(targetPath: string): Promise<boolean> {
66
+ try {
67
+ await fs.stat(targetPath);
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,49 @@
1
+ import * as path from 'node:path';
2
+
3
+ import { getHomeDir } from '@main/utils/pathDecoder';
4
+ import { SKILL_ROOT_DEFINITIONS } from '@shared/utils/skillRoots';
5
+
6
+ import type { SkillRootKind, SkillScope } from '@shared/types/extensions';
7
+
8
+ export interface ResolvedSkillRoot {
9
+ scope: SkillScope;
10
+ rootKind: SkillRootKind;
11
+ projectRoot: string | null;
12
+ rootPath: string;
13
+ }
14
+
15
+ const USER_ROOTS: { rootKind: SkillRootKind; segments: string[] }[] = SKILL_ROOT_DEFINITIONS.map(
16
+ (definition) => ({
17
+ rootKind: definition.rootKind,
18
+ segments: [...definition.segments],
19
+ })
20
+ );
21
+
22
+ export class SkillRootsResolver {
23
+ resolve(projectPath?: string): ResolvedSkillRoot[] {
24
+ const roots: ResolvedSkillRoot[] = [];
25
+ const homeDir = getHomeDir();
26
+
27
+ for (const def of USER_ROOTS) {
28
+ roots.push({
29
+ scope: 'user',
30
+ rootKind: def.rootKind,
31
+ projectRoot: null,
32
+ rootPath: path.join(homeDir, ...def.segments),
33
+ });
34
+ }
35
+
36
+ if (projectPath) {
37
+ for (const def of USER_ROOTS) {
38
+ roots.push({
39
+ scope: 'project',
40
+ rootKind: def.rootKind,
41
+ projectRoot: projectPath,
42
+ rootPath: path.join(projectPath, ...def.segments),
43
+ });
44
+ }
45
+ }
46
+
47
+ return roots;
48
+ }
49
+ }
@@ -0,0 +1,89 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+
4
+ import { isPathWithinRoot, validateFileName } from '@main/utils/pathValidation';
5
+
6
+ import { SkillRootsResolver } from './SkillRootsResolver';
7
+
8
+ import type { SkillDraftFile, SkillRootKind, SkillScope } from '@shared/types/extensions';
9
+
10
+ export class SkillScaffoldService {
11
+ constructor(private readonly rootsResolver = new SkillRootsResolver()) {}
12
+
13
+ async resolveUpsertTarget(
14
+ scope: SkillScope,
15
+ rootKind: SkillRootKind,
16
+ projectPath: string | undefined,
17
+ folderName: string,
18
+ existingSkillId?: string
19
+ ): Promise<string> {
20
+ const root = this.resolveWritableRoot(scope, rootKind, projectPath);
21
+ await fs.mkdir(root.rootPath, { recursive: true });
22
+
23
+ const folderValidation = validateFileName(folderName);
24
+ if (!folderValidation.valid) {
25
+ throw new Error(folderValidation.error ?? 'Invalid folder name');
26
+ }
27
+
28
+ const targetSkillDir = existingSkillId
29
+ ? path.resolve(existingSkillId)
30
+ : path.join(root.rootPath, folderName);
31
+ if (!isPathWithinRoot(targetSkillDir, root.rootPath)) {
32
+ throw new Error('Target skill directory is outside the allowed root');
33
+ }
34
+
35
+ return targetSkillDir;
36
+ }
37
+
38
+ normalizeDraftFiles(files: SkillDraftFile[]): SkillDraftFile[] {
39
+ return files.map((file) => ({
40
+ ...file,
41
+ relativePath: this.normalizeRelativePath(file.relativePath),
42
+ }));
43
+ }
44
+
45
+ async writeTextFiles(targetSkillDir: string, files: SkillDraftFile[]): Promise<void> {
46
+ for (const file of files) {
47
+ const absolutePath = path.join(targetSkillDir, file.relativePath);
48
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
49
+ await fs.writeFile(absolutePath, file.content, 'utf8');
50
+ }
51
+ }
52
+
53
+ private resolveWritableRoot(scope: SkillScope, rootKind: SkillRootKind, projectPath?: string) {
54
+ const roots = this.rootsResolver.resolve(projectPath);
55
+ const match = roots.find((root) => root.scope === scope && root.rootKind === rootKind);
56
+ if (!match) {
57
+ throw new Error('Requested skill root is unavailable');
58
+ }
59
+ if (scope === 'project' && !projectPath) {
60
+ throw new Error('projectPath is required for project-scoped skills');
61
+ }
62
+ return match;
63
+ }
64
+
65
+ private normalizeRelativePath(relativePath: string): string {
66
+ if (!relativePath || typeof relativePath !== 'string') {
67
+ throw new Error('relativePath is required');
68
+ }
69
+
70
+ const normalized = path.normalize(relativePath).replace(/\\/g, '/');
71
+ if (normalized.startsWith('../') || normalized === '..' || path.isAbsolute(normalized)) {
72
+ throw new Error(`Invalid relative path: ${relativePath}`);
73
+ }
74
+
75
+ const parts = normalized.split('/').filter(Boolean);
76
+ if (parts.length === 0) {
77
+ throw new Error(`Invalid relative path: ${relativePath}`);
78
+ }
79
+
80
+ for (const part of parts) {
81
+ const validation = validateFileName(part);
82
+ if (!validation.valid) {
83
+ throw new Error(validation.error ?? `Invalid path segment: ${part}`);
84
+ }
85
+ }
86
+
87
+ return parts.join('/');
88
+ }
89
+ }