@lesliechan721/aw 0.1.0-beta.5 → 0.1.0-beta.51

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 (569) hide show
  1. package/dist/authoring/assets/aw-config.mjs +100 -501
  2. package/dist/authoring/assets/aw-config.mjs.map +1 -1
  3. package/dist/authoring/assets/aw-runtime-config.d.mts +13 -0
  4. package/dist/authoring/assets/aw-runtime-config.mjs +199 -0
  5. package/dist/authoring/assets/aw-runtime-config.mjs.map +1 -0
  6. package/dist/authoring/scripts/add-skill.mjs +4 -4
  7. package/dist/authoring/scripts/add-skill.mjs.map +1 -1
  8. package/dist/authoring/scripts/init-plugin.mjs +67 -97
  9. package/dist/authoring/scripts/init-plugin.mjs.map +1 -1
  10. package/dist/authoring/scripts/lib/plugin-root.mjs +4 -4
  11. package/dist/authoring/scripts/lib/plugin-root.mjs.map +1 -1
  12. package/dist/authoring/scripts/lib/plugin-version-check.d.mts +1 -0
  13. package/dist/authoring/scripts/lib/plugin-version-check.mjs +229 -8
  14. package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -1
  15. package/dist/authoring/scripts/lib/skill-scaffold.d.mts +77 -92
  16. package/dist/authoring/scripts/lib/skill-scaffold.mjs +99 -124
  17. package/dist/authoring/scripts/lib/skill-scaffold.mjs.map +1 -1
  18. package/dist/authoring/scripts/plugin-status.mjs +8 -11
  19. package/dist/authoring/scripts/plugin-status.mjs.map +1 -1
  20. package/dist/authoring/scripts/remove-plugin.mjs +3 -2
  21. package/dist/authoring/scripts/remove-plugin.mjs.map +1 -1
  22. package/dist/authoring/scripts/validate-plugin.mjs +346 -1131
  23. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  24. package/dist/scripts/aw-skill-eval-materializer.d.ts +26 -0
  25. package/dist/scripts/aw-skill-eval-materializer.js +306 -0
  26. package/dist/scripts/aw-skill-eval-materializer.js.map +1 -0
  27. package/dist/scripts/bundle-plugins.d.ts +1 -0
  28. package/dist/scripts/bundle-plugins.js +20 -5
  29. package/dist/scripts/bundle-plugins.js.map +1 -1
  30. package/dist/scripts/plugin-staging.d.ts +7 -9
  31. package/dist/scripts/plugin-staging.js +18 -29
  32. package/dist/scripts/plugin-staging.js.map +1 -1
  33. package/dist/scripts/test-plugin-scripts.d.ts +2 -0
  34. package/dist/scripts/test-plugin-scripts.js +47 -76
  35. package/dist/scripts/test-plugin-scripts.js.map +1 -1
  36. package/dist/scripts/test-workers.d.mts +24 -0
  37. package/dist/scripts/test-workers.mjs +59 -0
  38. package/dist/scripts/test-workers.mjs.map +1 -0
  39. package/dist/src/adapters/helpers.js +2 -2
  40. package/dist/src/adapters/helpers.js.map +1 -1
  41. package/dist/src/authoring/workspace-commands.d.ts +17 -1
  42. package/dist/src/authoring/workspace-commands.js +21 -4
  43. package/dist/src/authoring/workspace-commands.js.map +1 -1
  44. package/dist/src/commands/aw-capabilities.d.ts +78 -0
  45. package/dist/src/commands/aw-capabilities.js +263 -0
  46. package/dist/src/commands/aw-capabilities.js.map +1 -0
  47. package/dist/src/commands/catalog-dev-source.d.ts +154 -0
  48. package/dist/src/commands/catalog-dev-source.js +655 -0
  49. package/dist/src/commands/catalog-dev-source.js.map +1 -0
  50. package/dist/src/commands/catalog-generation.d.ts +21 -0
  51. package/dist/src/commands/catalog-generation.js +9 -0
  52. package/dist/src/commands/catalog-generation.js.map +1 -0
  53. package/dist/src/commands/catalog-publication-inspection.d.ts +71 -30
  54. package/dist/src/commands/catalog-publication-inspection.js +475 -63
  55. package/dist/src/commands/catalog-publication-inspection.js.map +1 -1
  56. package/dist/src/commands/catalog-publication-journal.d.ts +68 -0
  57. package/dist/src/commands/catalog-publication-journal.js +292 -0
  58. package/dist/src/commands/catalog-publication-journal.js.map +1 -0
  59. package/dist/src/commands/catalog-publication-target.d.ts +18 -0
  60. package/dist/src/commands/catalog-publication-target.js +95 -0
  61. package/dist/src/commands/catalog-publication-target.js.map +1 -0
  62. package/dist/src/commands/catalog-publish.d.ts +73 -15
  63. package/dist/src/commands/catalog-publish.js +701 -212
  64. package/dist/src/commands/catalog-publish.js.map +1 -1
  65. package/dist/src/commands/catalog-source.d.ts +60 -22
  66. package/dist/src/commands/catalog-source.js +311 -91
  67. package/dist/src/commands/catalog-source.js.map +1 -1
  68. package/dist/src/commands/catalog-status.d.ts +18 -22
  69. package/dist/src/commands/catalog-status.js +81 -115
  70. package/dist/src/commands/catalog-status.js.map +1 -1
  71. package/dist/src/commands/catalog-store.d.ts +104 -0
  72. package/dist/src/commands/catalog-store.js +685 -0
  73. package/dist/src/commands/catalog-store.js.map +1 -0
  74. package/dist/src/commands/catalog-workspace-resolution.d.ts +13 -0
  75. package/dist/src/commands/catalog-workspace-resolution.js +42 -0
  76. package/dist/src/commands/catalog-workspace-resolution.js.map +1 -0
  77. package/dist/src/commands/console.js +28 -17
  78. package/dist/src/commands/console.js.map +1 -1
  79. package/dist/src/commands/dependency-operation-plan.d.ts +52 -15
  80. package/dist/src/commands/dependency-operation-plan.js +289 -7
  81. package/dist/src/commands/dependency-operation-plan.js.map +1 -1
  82. package/dist/src/commands/init.d.ts +5 -11
  83. package/dist/src/commands/init.js +30 -51
  84. package/dist/src/commands/init.js.map +1 -1
  85. package/dist/src/commands/offline-cleanup-evidence.d.ts +67 -0
  86. package/dist/src/commands/offline-cleanup-evidence.js +181 -0
  87. package/dist/src/commands/offline-cleanup-evidence.js.map +1 -0
  88. package/dist/src/commands/operation-plan.d.ts +146 -0
  89. package/dist/src/commands/operation-plan.js +71 -0
  90. package/dist/src/commands/operation-plan.js.map +1 -0
  91. package/dist/src/commands/operation-planner.d.ts +46 -0
  92. package/dist/src/commands/operation-planner.js +141 -0
  93. package/dist/src/commands/operation-planner.js.map +1 -0
  94. package/dist/src/commands/options.d.ts +9 -7
  95. package/dist/src/commands/options.js +83 -21
  96. package/dist/src/commands/options.js.map +1 -1
  97. package/dist/src/commands/plugin-capabilities.js +23 -3
  98. package/dist/src/commands/plugin-capabilities.js.map +1 -1
  99. package/dist/src/commands/rebuild-command.d.ts +77 -31
  100. package/dist/src/commands/rebuild-command.js +4862 -729
  101. package/dist/src/commands/rebuild-command.js.map +1 -1
  102. package/dist/src/commands/rebuild-integration.d.ts +131 -21
  103. package/dist/src/commands/rebuild-integration.js +2866 -1339
  104. package/dist/src/commands/rebuild-integration.js.map +1 -1
  105. package/dist/src/commands/rebuild-operations.d.ts +58 -30
  106. package/dist/src/commands/rebuild-operations.js +260 -319
  107. package/dist/src/commands/rebuild-operations.js.map +1 -1
  108. package/dist/src/commands/reconcile-operation-plan.d.ts +28 -0
  109. package/dist/src/commands/reconcile-operation-plan.js +47 -0
  110. package/dist/src/commands/reconcile-operation-plan.js.map +1 -0
  111. package/dist/src/commands/review-inspection.d.ts +60 -0
  112. package/dist/src/commands/review-inspection.js +422 -0
  113. package/dist/src/commands/review-inspection.js.map +1 -0
  114. package/dist/src/commands/review-preparation.d.ts +49 -0
  115. package/dist/src/commands/review-preparation.js +427 -0
  116. package/dist/src/commands/review-preparation.js.map +1 -0
  117. package/dist/src/commands/runtime.d.ts +1 -5
  118. package/dist/src/commands/runtime.js +24 -50
  119. package/dist/src/commands/runtime.js.map +1 -1
  120. package/dist/src/commands/skill-eval.d.ts +39 -0
  121. package/dist/src/commands/skill-eval.js +400 -0
  122. package/dist/src/commands/skill-eval.js.map +1 -0
  123. package/dist/src/commands/status-v6.d.ts +279 -0
  124. package/dist/src/commands/status-v6.js +2787 -0
  125. package/dist/src/commands/status-v6.js.map +1 -0
  126. package/dist/src/commands/sync-command.d.ts +40 -0
  127. package/dist/src/commands/sync-command.js +250 -0
  128. package/dist/src/commands/sync-command.js.map +1 -0
  129. package/dist/src/commands/wizard.d.ts +8 -5
  130. package/dist/src/commands/wizard.js +6 -4
  131. package/dist/src/commands/wizard.js.map +1 -1
  132. package/dist/src/commands/write-commands.d.ts +9 -3
  133. package/dist/src/commands/write-commands.js +127 -31
  134. package/dist/src/commands/write-commands.js.map +1 -1
  135. package/dist/src/constants.d.ts +7 -1
  136. package/dist/src/constants.js +7 -1
  137. package/dist/src/constants.js.map +1 -1
  138. package/dist/src/eval/manifest-v1.d.ts +68 -0
  139. package/dist/src/eval/manifest-v1.js +353 -0
  140. package/dist/src/eval/manifest-v1.js.map +1 -0
  141. package/dist/src/generators/local-plugin.d.ts +5 -1
  142. package/dist/src/generators/local-plugin.js +236 -109
  143. package/dist/src/generators/local-plugin.js.map +1 -1
  144. package/dist/src/index.d.ts +10 -4
  145. package/dist/src/index.js +1752 -608
  146. package/dist/src/index.js.map +1 -1
  147. package/dist/src/output/command-receipt.d.ts +43 -46
  148. package/dist/src/output/command-receipt.js +256 -80
  149. package/dist/src/output/command-receipt.js.map +1 -1
  150. package/dist/src/output/model.d.ts +3 -44
  151. package/dist/src/output/model.js +1 -144
  152. package/dist/src/output/model.js.map +1 -1
  153. package/dist/src/output/rebuild-progress.js +1 -13
  154. package/dist/src/output/rebuild-progress.js.map +1 -1
  155. package/dist/src/portable/commands.d.ts +46 -7
  156. package/dist/src/portable/commands.js +349 -37
  157. package/dist/src/portable/commands.js.map +1 -1
  158. package/dist/src/portable/materializer.d.ts +17 -4
  159. package/dist/src/portable/materializer.js +179 -41
  160. package/dist/src/portable/materializer.js.map +1 -1
  161. package/dist/src/portable/skills-cli.d.ts +15 -7
  162. package/dist/src/portable/skills-cli.js +74 -20
  163. package/dist/src/portable/skills-cli.js.map +1 -1
  164. package/dist/src/review/eval-source-input.d.ts +43 -0
  165. package/dist/src/review/eval-source-input.js +207 -0
  166. package/dist/src/review/eval-source-input.js.map +1 -0
  167. package/dist/src/review/review-profile.d.ts +17 -0
  168. package/dist/src/review/review-profile.js +234 -0
  169. package/dist/src/review/review-profile.js.map +1 -0
  170. package/dist/src/review/review-selection.d.ts +30 -0
  171. package/dist/src/review/review-selection.js +527 -0
  172. package/dist/src/review/review-selection.js.map +1 -0
  173. package/dist/src/review/review-types.d.ts +68 -0
  174. package/dist/src/review/review-types.js +16 -0
  175. package/dist/src/review/review-types.js.map +1 -0
  176. package/dist/src/review/skill-materialization.d.ts +13 -0
  177. package/dist/src/review/skill-materialization.js +75 -0
  178. package/dist/src/review/skill-materialization.js.map +1 -0
  179. package/dist/src/review/source-binding.d.ts +183 -0
  180. package/dist/src/review/source-binding.js +708 -0
  181. package/dist/src/review/source-binding.js.map +1 -0
  182. package/dist/src/tui/catalog-freshness.d.ts +1 -0
  183. package/dist/src/tui/catalog-freshness.js +2 -0
  184. package/dist/src/tui/catalog-freshness.js.map +1 -0
  185. package/dist/src/tui/catalog-generation.d.ts +21 -0
  186. package/dist/src/tui/catalog-generation.js +156 -0
  187. package/dist/src/tui/catalog-generation.js.map +1 -0
  188. package/dist/src/tui/catalog-sources/projection.d.ts +4 -0
  189. package/dist/src/tui/catalog-sources/projection.js +17 -0
  190. package/dist/src/tui/catalog-sources/projection.js.map +1 -0
  191. package/dist/src/tui/catalog-sources/review.d.ts +3 -0
  192. package/dist/src/tui/catalog-sources/review.js +13 -0
  193. package/dist/src/tui/catalog-sources/review.js.map +1 -0
  194. package/dist/src/tui/catalog-sources/screens.d.ts +12 -0
  195. package/dist/src/tui/catalog-sources/screens.js +40 -0
  196. package/dist/src/tui/catalog-sources/screens.js.map +1 -0
  197. package/dist/src/tui/catalog-sources/session.d.ts +32 -0
  198. package/dist/src/tui/catalog-sources/session.js +300 -0
  199. package/dist/src/tui/catalog-sources/session.js.map +1 -0
  200. package/dist/src/tui/catalog-sources/types.d.ts +54 -0
  201. package/dist/src/tui/catalog-sources/types.js +2 -0
  202. package/dist/src/tui/catalog-sources/types.js.map +1 -0
  203. package/dist/src/tui/catalog-store-task.d.ts +16 -0
  204. package/dist/src/tui/catalog-store-task.js +19 -0
  205. package/dist/src/tui/catalog-store-task.js.map +1 -0
  206. package/dist/src/tui/clipboard.d.ts +13 -0
  207. package/dist/src/tui/clipboard.js +95 -0
  208. package/dist/src/tui/clipboard.js.map +1 -0
  209. package/dist/src/tui/components.d.ts +110 -3
  210. package/dist/src/tui/components.js +183 -36
  211. package/dist/src/tui/components.js.map +1 -1
  212. package/dist/src/tui/config-array-draft.d.ts +29 -0
  213. package/dist/src/tui/config-array-draft.js +48 -0
  214. package/dist/src/tui/config-array-draft.js.map +1 -0
  215. package/dist/src/tui/config-leaf-editor.d.ts +16 -0
  216. package/dist/src/tui/config-leaf-editor.js +206 -0
  217. package/dist/src/tui/config-leaf-editor.js.map +1 -0
  218. package/dist/src/tui/config-value-presentation.d.ts +20 -0
  219. package/dist/src/tui/config-value-presentation.js +104 -0
  220. package/dist/src/tui/config-value-presentation.js.map +1 -0
  221. package/dist/src/tui/console-app-v4.d.ts +14 -4
  222. package/dist/src/tui/console-app-v4.js +8941 -1872
  223. package/dist/src/tui/console-app-v4.js.map +1 -1
  224. package/dist/src/tui/content-item-card.d.ts +13 -0
  225. package/dist/src/tui/content-item-card.js +51 -0
  226. package/dist/src/tui/content-item-card.js.map +1 -0
  227. package/dist/src/tui/controller-runtime.d.ts +10 -2
  228. package/dist/src/tui/controller-runtime.js +3211 -388
  229. package/dist/src/tui/controller-runtime.js.map +1 -1
  230. package/dist/src/tui/controller.d.ts +615 -45
  231. package/dist/src/tui/controller.js +3558 -768
  232. package/dist/src/tui/controller.js.map +1 -1
  233. package/dist/src/tui/dependency-graph-view.d.ts +39 -0
  234. package/dist/src/tui/dependency-graph-view.js +121 -0
  235. package/dist/src/tui/dependency-graph-view.js.map +1 -0
  236. package/dist/src/tui/dependency-graph.d.ts +81 -0
  237. package/dist/src/tui/dependency-graph.js +452 -0
  238. package/dist/src/tui/dependency-graph.js.map +1 -0
  239. package/dist/src/tui/diagnostic-projection.d.ts +23 -0
  240. package/dist/src/tui/diagnostic-projection.js +567 -0
  241. package/dist/src/tui/diagnostic-projection.js.map +1 -0
  242. package/dist/src/tui/diagnostics.d.ts +3 -3
  243. package/dist/src/tui/diagnostics.js +14 -2
  244. package/dist/src/tui/diagnostics.js.map +1 -1
  245. package/dist/src/tui/entity-cards.d.ts +55 -0
  246. package/dist/src/tui/entity-cards.js +220 -0
  247. package/dist/src/tui/entity-cards.js.map +1 -0
  248. package/dist/src/tui/glossary.d.ts +23 -0
  249. package/dist/src/tui/glossary.js +42 -0
  250. package/dist/src/tui/glossary.js.map +1 -0
  251. package/dist/src/tui/index.d.ts +6 -1
  252. package/dist/src/tui/index.js +5 -1
  253. package/dist/src/tui/index.js.map +1 -1
  254. package/dist/src/tui/input-display.d.ts +9 -0
  255. package/dist/src/tui/input-display.js +58 -0
  256. package/dist/src/tui/input-display.js.map +1 -0
  257. package/dist/src/tui/keys.d.ts +44 -0
  258. package/dist/src/tui/keys.js +43 -15
  259. package/dist/src/tui/keys.js.map +1 -1
  260. package/dist/src/tui/lifecycle-action-view.d.ts +6 -0
  261. package/dist/src/tui/lifecycle-action-view.js +36 -0
  262. package/dist/src/tui/lifecycle-action-view.js.map +1 -0
  263. package/dist/src/tui/model.d.ts +223 -31
  264. package/dist/src/tui/model.js +1510 -397
  265. package/dist/src/tui/model.js.map +1 -1
  266. package/dist/src/tui/operation-diagnostic-screens.d.ts +8 -0
  267. package/dist/src/tui/operation-diagnostic-screens.js +71 -0
  268. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -0
  269. package/dist/src/tui/operation-history.d.ts +81 -0
  270. package/dist/src/tui/operation-history.js +136 -0
  271. package/dist/src/tui/operation-history.js.map +1 -0
  272. package/dist/src/tui/operation-review-planner.d.ts +21 -0
  273. package/dist/src/tui/operation-review-planner.js +1295 -0
  274. package/dist/src/tui/operation-review-planner.js.map +1 -0
  275. package/dist/src/tui/plugin-content.d.ts +31 -0
  276. package/dist/src/tui/plugin-content.js +680 -0
  277. package/dist/src/tui/plugin-content.js.map +1 -0
  278. package/dist/src/tui/plugin-detail.d.ts +52 -0
  279. package/dist/src/tui/plugin-detail.js +165 -0
  280. package/dist/src/tui/plugin-detail.js.map +1 -0
  281. package/dist/src/tui/plugin-write-task.d.ts +69 -0
  282. package/dist/src/tui/plugin-write-task.js +286 -0
  283. package/dist/src/tui/plugin-write-task.js.map +1 -0
  284. package/dist/src/tui/project-inventory.d.ts +68 -0
  285. package/dist/src/tui/project-inventory.js +203 -0
  286. package/dist/src/tui/project-inventory.js.map +1 -0
  287. package/dist/src/tui/prompt.js +4 -15
  288. package/dist/src/tui/prompt.js.map +1 -1
  289. package/dist/src/tui/query-header.d.ts +30 -0
  290. package/dist/src/tui/query-header.js +40 -0
  291. package/dist/src/tui/query-header.js.map +1 -0
  292. package/dist/src/tui/reader.d.ts +37 -0
  293. package/dist/src/tui/reader.js +47 -0
  294. package/dist/src/tui/reader.js.map +1 -0
  295. package/dist/src/tui/router.d.ts +197 -10
  296. package/dist/src/tui/router.js +338 -50
  297. package/dist/src/tui/router.js.map +1 -1
  298. package/dist/src/tui/screen-registry.d.ts +701 -0
  299. package/dist/src/tui/screen-registry.js +230 -0
  300. package/dist/src/tui/screen-registry.js.map +1 -0
  301. package/dist/src/tui/session-console-app.d.ts +6 -0
  302. package/dist/src/tui/session-console-app.js +13 -0
  303. package/dist/src/tui/session-console-app.js.map +1 -0
  304. package/dist/src/tui/session-host.d.ts +45 -0
  305. package/dist/src/tui/session-host.js +209 -0
  306. package/dist/src/tui/session-host.js.map +1 -0
  307. package/dist/src/tui/source-detail-card.d.ts +29 -0
  308. package/dist/src/tui/source-detail-card.js +45 -0
  309. package/dist/src/tui/source-detail-card.js.map +1 -0
  310. package/dist/src/tui/source-ref-presets.d.ts +3 -0
  311. package/dist/src/tui/source-ref-presets.js +11 -0
  312. package/dist/src/tui/source-ref-presets.js.map +1 -0
  313. package/dist/src/tui/theme.d.ts +12 -0
  314. package/dist/src/tui/theme.js +12 -0
  315. package/dist/src/tui/theme.js.map +1 -1
  316. package/dist/src/tui/viewport.d.ts +1 -0
  317. package/dist/src/tui/viewport.js +23 -0
  318. package/dist/src/tui/viewport.js.map +1 -1
  319. package/dist/src/tui/views.js +31 -3
  320. package/dist/src/tui/views.js.map +1 -1
  321. package/dist/src/tui/wizard.js +37 -13
  322. package/dist/src/tui/wizard.js.map +1 -1
  323. package/dist/src/types.d.ts +244 -20
  324. package/dist/src/types.js +1 -0
  325. package/dist/src/types.js.map +1 -1
  326. package/dist/src/utils/agent-version.d.ts +7 -1
  327. package/dist/src/utils/agent-version.js +41 -8
  328. package/dist/src/utils/agent-version.js.map +1 -1
  329. package/dist/src/utils/artifact-plan.d.ts +5 -4
  330. package/dist/src/utils/artifact-plan.js +18 -7
  331. package/dist/src/utils/artifact-plan.js.map +1 -1
  332. package/dist/src/utils/artifact-renderer.d.ts +17 -7
  333. package/dist/src/utils/artifact-renderer.js +214 -216
  334. package/dist/src/utils/artifact-renderer.js.map +1 -1
  335. package/dist/src/utils/artifact-request.d.ts +21 -0
  336. package/dist/src/utils/artifact-request.js +78 -0
  337. package/dist/src/utils/artifact-request.js.map +1 -0
  338. package/dist/src/utils/aw-home-mutation-lock.d.ts +1 -0
  339. package/dist/src/utils/aw-home-mutation-lock.js +32 -4
  340. package/dist/src/utils/aw-home-mutation-lock.js.map +1 -1
  341. package/dist/src/utils/bundle-integrity.d.ts +0 -1
  342. package/dist/src/utils/bundle-integrity.js +0 -25
  343. package/dist/src/utils/bundle-integrity.js.map +1 -1
  344. package/dist/src/utils/bundle-layout.js +22 -29
  345. package/dist/src/utils/bundle-layout.js.map +1 -1
  346. package/dist/src/utils/catalog-capability-projection.d.ts +8 -0
  347. package/dist/src/utils/catalog-capability-projection.js +14 -0
  348. package/dist/src/utils/catalog-capability-projection.js.map +1 -0
  349. package/dist/src/utils/catalog-digests.d.ts +10 -0
  350. package/dist/src/utils/catalog-digests.js +57 -20
  351. package/dist/src/utils/catalog-digests.js.map +1 -1
  352. package/dist/src/utils/catalog-generation-control-snapshot.d.ts +11 -0
  353. package/dist/src/utils/catalog-generation-control-snapshot.js +45 -0
  354. package/dist/src/utils/catalog-generation-control-snapshot.js.map +1 -0
  355. package/dist/src/utils/catalog-generation-error.d.ts +16 -0
  356. package/dist/src/utils/catalog-generation-error.js +30 -0
  357. package/dist/src/utils/catalog-generation-error.js.map +1 -0
  358. package/dist/src/utils/catalog-generation-resolver.d.ts +44 -0
  359. package/dist/src/utils/catalog-generation-resolver.js +273 -0
  360. package/dist/src/utils/catalog-generation-resolver.js.map +1 -0
  361. package/dist/src/utils/catalog-generation-service.d.ts +172 -0
  362. package/dist/src/utils/catalog-generation-service.js +1271 -0
  363. package/dist/src/utils/catalog-generation-service.js.map +1 -0
  364. package/dist/src/utils/catalog-generation-state.d.ts +22 -0
  365. package/dist/src/utils/catalog-generation-state.js +192 -0
  366. package/dist/src/utils/catalog-generation-state.js.map +1 -0
  367. package/dist/src/utils/catalog-generation-store.d.ts +58 -0
  368. package/dist/src/utils/catalog-generation-store.js +452 -0
  369. package/dist/src/utils/catalog-generation-store.js.map +1 -0
  370. package/dist/src/utils/catalog-manifest-v7.d.ts +5 -0
  371. package/dist/src/utils/{catalog-manifest-v4.js → catalog-manifest-v7.js} +35 -43
  372. package/dist/src/utils/catalog-manifest-v7.js.map +1 -0
  373. package/dist/src/utils/catalog-operation-context.d.ts +20 -0
  374. package/dist/src/utils/catalog-operation-context.js +142 -0
  375. package/dist/src/utils/catalog-operation-context.js.map +1 -0
  376. package/dist/src/utils/catalog-public-error.d.ts +7 -3
  377. package/dist/src/utils/catalog-public-error.js +28 -8
  378. package/dist/src/utils/catalog-public-error.js.map +1 -1
  379. package/dist/src/utils/catalog-runtime-projection.d.ts +3 -0
  380. package/dist/src/utils/catalog-runtime-projection.js +5 -40
  381. package/dist/src/utils/catalog-runtime-projection.js.map +1 -1
  382. package/dist/src/utils/catalog-snapshot.d.ts +5 -3
  383. package/dist/src/utils/catalog-snapshot.js +11 -3
  384. package/dist/src/utils/catalog-snapshot.js.map +1 -1
  385. package/dist/src/utils/catalog-source-identity.d.ts +1 -0
  386. package/dist/src/utils/catalog-source-identity.js +21 -2
  387. package/dist/src/utils/catalog-source-identity.js.map +1 -1
  388. package/dist/src/utils/catalog-source-impact.d.ts +21 -0
  389. package/dist/src/utils/catalog-source-impact.js +344 -0
  390. package/dist/src/utils/catalog-source-impact.js.map +1 -0
  391. package/dist/src/utils/catalog-source-references.d.ts +3 -2
  392. package/dist/src/utils/catalog-source-references.js +195 -347
  393. package/dist/src/utils/catalog-source-references.js.map +1 -1
  394. package/dist/src/utils/catalog-sources.d.ts +15 -1
  395. package/dist/src/utils/catalog-sources.js +206 -27
  396. package/dist/src/utils/catalog-sources.js.map +1 -1
  397. package/dist/src/utils/codex-config.d.ts +2 -0
  398. package/dist/src/utils/codex-config.js +26 -5
  399. package/dist/src/utils/codex-config.js.map +1 -1
  400. package/dist/src/utils/codex-manager.d.ts +79 -0
  401. package/dist/src/utils/codex-manager.js +747 -0
  402. package/dist/src/utils/codex-manager.js.map +1 -0
  403. package/dist/src/utils/codex-marketplace-path.d.ts +5 -0
  404. package/dist/src/utils/codex-marketplace-path.js +45 -0
  405. package/dist/src/utils/codex-marketplace-path.js.map +1 -0
  406. package/dist/src/utils/codex-native-observation.d.ts +49 -0
  407. package/dist/src/utils/codex-native-observation.js +154 -0
  408. package/dist/src/utils/codex-native-observation.js.map +1 -0
  409. package/dist/src/utils/codex-native-render-receipt.d.ts +40 -0
  410. package/dist/src/utils/codex-native-render-receipt.js +205 -0
  411. package/dist/src/utils/codex-native-render-receipt.js.map +1 -0
  412. package/dist/src/utils/codex-reconciler.d.ts +126 -0
  413. package/dist/src/utils/codex-reconciler.js +298 -0
  414. package/dist/src/utils/codex-reconciler.js.map +1 -0
  415. package/dist/src/utils/codex-shared-native.d.ts +43 -0
  416. package/dist/src/utils/codex-shared-native.js +81 -0
  417. package/dist/src/utils/codex-shared-native.js.map +1 -0
  418. package/dist/src/utils/codex-skill-staging.d.ts +2 -0
  419. package/dist/src/utils/codex-skill-staging.js.map +1 -1
  420. package/dist/src/utils/codex-surface-analysis.d.ts +46 -0
  421. package/dist/src/utils/codex-surface-analysis.js +247 -0
  422. package/dist/src/utils/codex-surface-analysis.js.map +1 -0
  423. package/dist/src/utils/dynamic-skill-tree.d.ts +4 -0
  424. package/dist/src/utils/dynamic-skill-tree.js +35 -5
  425. package/dist/src/utils/dynamic-skill-tree.js.map +1 -1
  426. package/dist/src/utils/git-catalog-provider.d.ts +16 -0
  427. package/dist/src/utils/git-catalog-provider.js +177 -145
  428. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  429. package/dist/src/utils/local-marketplace.d.ts +18 -1
  430. package/dist/src/utils/local-marketplace.js +354 -84
  431. package/dist/src/utils/local-marketplace.js.map +1 -1
  432. package/dist/src/utils/manifest.d.ts +1 -3
  433. package/dist/src/utils/manifest.js +1 -3
  434. package/dist/src/utils/manifest.js.map +1 -1
  435. package/dist/src/utils/multi-source-catalog.d.ts +3 -9
  436. package/dist/src/utils/multi-source-catalog.js +54 -110
  437. package/dist/src/utils/multi-source-catalog.js.map +1 -1
  438. package/dist/src/utils/native-cli.d.ts +55 -7
  439. package/dist/src/utils/native-cli.js +400 -58
  440. package/dist/src/utils/native-cli.js.map +1 -1
  441. package/dist/src/utils/orphan-marketplace-cleanup.js +0 -4
  442. package/dist/src/utils/orphan-marketplace-cleanup.js.map +1 -1
  443. package/dist/src/utils/ownership.d.ts +84 -20
  444. package/dist/src/utils/ownership.js +179 -222
  445. package/dist/src/utils/ownership.js.map +1 -1
  446. package/dist/src/utils/path-catalog-provider.d.ts +1 -1
  447. package/dist/src/utils/path-catalog-provider.js +54 -65
  448. package/dist/src/utils/path-catalog-provider.js.map +1 -1
  449. package/dist/src/utils/paths.d.ts +0 -1
  450. package/dist/src/utils/paths.js +0 -1
  451. package/dist/src/utils/paths.js.map +1 -1
  452. package/dist/src/utils/plugin-config-impact.d.ts +33 -0
  453. package/dist/src/utils/plugin-config-impact.js +298 -0
  454. package/dist/src/utils/plugin-config-impact.js.map +1 -0
  455. package/dist/src/utils/plugin-config-write.d.ts +52 -27
  456. package/dist/src/utils/plugin-config-write.js +1000 -296
  457. package/dist/src/utils/plugin-config-write.js.map +1 -1
  458. package/dist/src/utils/plugin-config.d.ts +153 -93
  459. package/dist/src/utils/plugin-config.js +1116 -968
  460. package/dist/src/utils/plugin-config.js.map +1 -1
  461. package/dist/src/utils/plugin-intent-state.d.ts +49 -0
  462. package/dist/src/utils/plugin-intent-state.js +320 -0
  463. package/dist/src/utils/plugin-intent-state.js.map +1 -0
  464. package/dist/src/utils/plugin-scripts.d.ts +65 -125
  465. package/dist/src/utils/plugin-scripts.js +484 -524
  466. package/dist/src/utils/plugin-scripts.js.map +1 -1
  467. package/dist/src/utils/process-tree.d.ts +2 -2
  468. package/dist/src/utils/process-tree.js +1 -1
  469. package/dist/src/utils/process-tree.js.map +1 -1
  470. package/dist/src/utils/project-boundary.d.ts +11 -13
  471. package/dist/src/utils/project-boundary.js +28 -34
  472. package/dist/src/utils/project-boundary.js.map +1 -1
  473. package/dist/src/utils/runtime-config.d.ts +20 -0
  474. package/dist/src/utils/runtime-config.js +75 -0
  475. package/dist/src/utils/runtime-config.js.map +1 -0
  476. package/dist/src/utils/safe-fs.d.ts +21 -0
  477. package/dist/src/utils/safe-fs.js +111 -0
  478. package/dist/src/utils/safe-fs.js.map +1 -0
  479. package/dist/src/utils/safe-operation-diagnostic.d.ts +15 -0
  480. package/dist/src/utils/safe-operation-diagnostic.js +33 -0
  481. package/dist/src/utils/safe-operation-diagnostic.js.map +1 -0
  482. package/dist/src/utils/sensitive-content.d.ts +4 -0
  483. package/dist/src/utils/sensitive-content.js +36 -0
  484. package/dist/src/utils/sensitive-content.js.map +1 -0
  485. package/dist/src/utils/state-paths.js +7 -2
  486. package/dist/src/utils/state-paths.js.map +1 -1
  487. package/dist/src/utils/target-eligibility.d.ts +8 -2
  488. package/dist/src/utils/target-eligibility.js +17 -8
  489. package/dist/src/utils/target-eligibility.js.map +1 -1
  490. package/dist/src/utils/windows-claude-shim.d.ts +7 -0
  491. package/dist/src/utils/windows-claude-shim.js +10 -0
  492. package/dist/src/utils/windows-claude-shim.js.map +1 -0
  493. package/dist/src/utils/windows-npm-shim.d.ts +14 -0
  494. package/dist/src/utils/windows-npm-shim.js +140 -0
  495. package/dist/src/utils/windows-npm-shim.js.map +1 -0
  496. package/dist/vitest.config.js +92 -10
  497. package/dist/vitest.config.js.map +1 -1
  498. package/package.json +27 -18
  499. package/dist/authoring/scripts/check-plugin-versions.d.mts +0 -2
  500. package/dist/authoring/scripts/check-plugin-versions.mjs +0 -5
  501. package/dist/authoring/scripts/check-plugin-versions.mjs.map +0 -1
  502. package/dist/scripts/aw-skill-eval-engine.d.ts +0 -13
  503. package/dist/scripts/aw-skill-eval-engine.js +0 -31
  504. package/dist/scripts/aw-skill-eval-engine.js.map +0 -1
  505. package/dist/scripts/codex-eval-app-server.d.ts +0 -89
  506. package/dist/scripts/codex-eval-app-server.js +0 -1228
  507. package/dist/scripts/codex-eval-app-server.js.map +0 -1
  508. package/dist/scripts/codex-eval-isolation.d.ts +0 -61
  509. package/dist/scripts/codex-eval-isolation.js +0 -318
  510. package/dist/scripts/codex-eval-isolation.js.map +0 -1
  511. package/dist/scripts/codex-eval-process-management.d.ts +0 -8
  512. package/dist/scripts/codex-eval-process-management.js +0 -69
  513. package/dist/scripts/codex-eval-process-management.js.map +0 -1
  514. package/dist/scripts/codex-eval-process-tree.d.ts +0 -38
  515. package/dist/scripts/codex-eval-process-tree.js +0 -803
  516. package/dist/scripts/codex-eval-process-tree.js.map +0 -1
  517. package/dist/scripts/codex-eval-runtime-config.d.ts +0 -62
  518. package/dist/scripts/codex-eval-runtime-config.js +0 -843
  519. package/dist/scripts/codex-eval-runtime-config.js.map +0 -1
  520. package/dist/scripts/codex-trigger-eval.d.ts +0 -84
  521. package/dist/scripts/codex-trigger-eval.js +0 -968
  522. package/dist/scripts/codex-trigger-eval.js.map +0 -1
  523. package/dist/scripts/eval-plugin-skills-audited.d.ts +0 -6
  524. package/dist/scripts/eval-plugin-skills-audited.js +0 -261
  525. package/dist/scripts/eval-plugin-skills-audited.js.map +0 -1
  526. package/dist/scripts/eval-plugin-skills.d.ts +0 -45
  527. package/dist/scripts/eval-plugin-skills.js +0 -1586
  528. package/dist/scripts/eval-plugin-skills.js.map +0 -1
  529. package/dist/scripts/eval-run-layout.d.ts +0 -17
  530. package/dist/scripts/eval-run-layout.js +0 -26
  531. package/dist/scripts/eval-run-layout.js.map +0 -1
  532. package/dist/scripts/plugin-eval-contract.d.ts +0 -185
  533. package/dist/scripts/plugin-eval-contract.js +0 -695
  534. package/dist/scripts/plugin-eval-contract.js.map +0 -1
  535. package/dist/scripts/plugin-eval-materializer.d.ts +0 -13
  536. package/dist/scripts/plugin-eval-materializer.js +0 -421
  537. package/dist/scripts/plugin-eval-materializer.js.map +0 -1
  538. package/dist/scripts/skill-reporting.d.ts +0 -212
  539. package/dist/scripts/skill-reporting.js +0 -922
  540. package/dist/scripts/skill-reporting.js.map +0 -1
  541. package/dist/src/commands/review-publication.d.ts +0 -99
  542. package/dist/src/commands/review-publication.js +0 -904
  543. package/dist/src/commands/review-publication.js.map +0 -1
  544. package/dist/src/commands/status-v4.d.ts +0 -123
  545. package/dist/src/commands/status-v4.js +0 -905
  546. package/dist/src/commands/status-v4.js.map +0 -1
  547. package/dist/src/commands/status.d.ts +0 -93
  548. package/dist/src/commands/status.js +0 -442
  549. package/dist/src/commands/status.js.map +0 -1
  550. package/dist/src/utils/active-marketplace-runtime-lock.d.ts +0 -12
  551. package/dist/src/utils/active-marketplace-runtime-lock.js +0 -133
  552. package/dist/src/utils/active-marketplace-runtime-lock.js.map +0 -1
  553. package/dist/src/utils/catalog-manifest-v3.d.ts +0 -2
  554. package/dist/src/utils/catalog-manifest-v3.js +0 -3
  555. package/dist/src/utils/catalog-manifest-v3.js.map +0 -1
  556. package/dist/src/utils/catalog-manifest-v4.d.ts +0 -5
  557. package/dist/src/utils/catalog-manifest-v4.js.map +0 -1
  558. package/dist/src/utils/catalog-source-cache.d.ts +0 -62
  559. package/dist/src/utils/catalog-source-cache.js +0 -296
  560. package/dist/src/utils/catalog-source-cache.js.map +0 -1
  561. package/dist/src/utils/codex-project-surface.d.ts +0 -3
  562. package/dist/src/utils/codex-project-surface.js +0 -49
  563. package/dist/src/utils/codex-project-surface.js.map +0 -1
  564. package/dist/src/utils/lifecycle-active-store.d.ts +0 -175
  565. package/dist/src/utils/lifecycle-active-store.js +0 -1276
  566. package/dist/src/utils/lifecycle-active-store.js.map +0 -1
  567. package/dist/src/utils/plugin-release.d.ts +0 -2
  568. package/dist/src/utils/plugin-release.js +0 -2
  569. package/dist/src/utils/plugin-release.js.map +0 -1
@@ -1,34 +1,80 @@
1
1
  import crypto from 'node:crypto';
2
- import { statusCommandV4, } from '../commands/status-v4.js';
2
+ import path from 'node:path';
3
+ import { flattenStatusTargets, statusCommandV8, statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, } from '../commands/status-v6.js';
3
4
  import { addCatalogSourceCommand, listCatalogSources, preflightCatalogSourceMutation, removeCatalogSourceCommand, setPrimaryCatalogSourceCommand, updateCatalogSourceCommand, } from '../commands/catalog-source.js';
4
- import { cleanupWriteCommand, installWriteCommand, reinstallWriteCommand, uninstallWriteCommand, upgradeWriteCommand, } from '../commands/write-commands.js';
5
+ import { cleanupWriteCommand, failedWriteCommandResult, installWriteCommand, mergeWriteCommandResults, reinstallWriteCommand, syncWriteCommand, taskWriteCommand, uninstallWriteCommand, } from '../commands/write-commands.js';
6
+ import { resolveSyncIntentSelection } from '../commands/sync-command.js';
7
+ import { parseScriptTimeoutMs, SCRIPT_TIMEOUT_DEFAULT_MS, } from '../commands/options.js';
5
8
  import { readCliPackageInfo } from '../utils/local-marketplace.js';
6
- import { readCatalogSourcesConfigSnapshot, } from '../utils/catalog-sources.js';
9
+ import { createCodexNativeObservationSession, } from '../utils/codex-native-observation.js';
10
+ import { readCatalogSourcesConfigSnapshot, recoverInvalidCatalogSourcesConfig, } from '../utils/catalog-sources.js';
7
11
  import { withResolvedCatalogPlugin, withResolvedMultiSourceCatalog, formatPluginId, parsePluginId, } from '../utils/multi-source-catalog.js';
8
- import { coercePluginConfigUpdates, readNamespacedPluginConfigStoreIdentity, readPluginConfigStore, resolveConfigRead, } from '../utils/plugin-config.js';
9
- import { applyPluginConfigWrite, resetPluginConfigWithHelper, } from '../utils/plugin-config-write.js';
10
- import { operationEvidenceFingerprint, TuiController, TuiOperationBlockedError, } from './controller.js';
11
- import { buildConfigGroups, buildConfigPage, projectPlatformPlacement, } from './model.js';
12
+ import { coercePluginConfigUpdates, inspectPluginConfigResetTarget, listNamespacedPluginConfigStoreIdentities, readPluginConfigStore, resolveConfigRead, resolveEffectivePluginConfig, resolvePluginConfigStorePath, } from '../utils/plugin-config.js';
13
+ import { assertProjectSharedConfigPathVisible } from '../utils/project-boundary.js';
14
+ import { calculatePluginIntentDigest, readPluginIntentState, } from '../utils/plugin-intent-state.js';
15
+ import { applyPluginConfigWrite, replayConfigRuntimeWrites, resolveConfigPreApplyAuthorization, resetPluginConfigWithHelper, } from '../utils/plugin-config-write.js';
16
+ import { analyzePluginConfigMutationImpact } from '../utils/plugin-config-impact.js';
17
+ import { createBatchIntentCompatiblePlanFingerprint } from '../commands/operation-plan.js';
18
+ import { inventoryCatalogSourceReferences } from '../utils/catalog-source-references.js';
19
+ import { operationEvidenceMatches, operationEvidenceFingerprint, TuiConfigInvalidStoreError, TuiController, TuiOperationBlockedError, } from './controller.js';
20
+ import { buildConfigGroups, buildConfigPage, buildTuiViewModel, hasActualScopeEvidence, isDirectCodexVersionUpgrade, projectPlatformPlacement, } from './model.js';
21
+ import { buildPluginWriteTaskCandidates, buildPluginWriteTaskDraft, evaluatePluginWriteTaskRestoreVersion, pluginWriteTaskTargetKey, } from './plugin-write-task.js';
22
+ import { projectExecutionDiagnostics, projectThrownOperationDiagnostic } from './diagnostic-projection.js';
12
23
  import { projectCatalogSourceLocation } from '../utils/catalog-source-presentation.js';
13
- import { targetCanBeUninstalled } from '../utils/target-eligibility.js';
14
- import { calculateCatalogSourceIdentity } from '../utils/catalog-source-identity.js';
15
- import { resolvePluginsFromCatalog } from '../utils/manifest.js';
24
+ import { calculateCatalogSourceConfigFingerprint, calculateCatalogSourceIdentity, } from '../utils/catalog-source-identity.js';
25
+ import { checkAwCapabilitySupport, readPluginManifest } from '../utils/manifest.js';
16
26
  import { withAwHomeMutationLock } from '../utils/aw-home-mutation-lock.js';
17
- import { installPortableSkills, preparePortableInstall, PortableCommandError, } from '../portable/commands.js';
27
+ import { IMPLEMENTED_AW_CAPABILITIES } from '../commands/aw-capabilities.js';
28
+ import { calculateCatalogGenerationSetDigest, resolveActiveCatalogGeneration, } from '../utils/catalog-generation-resolver.js';
29
+ import { resolveCatalogOperationContext, resolvePartialCatalogOperationContext, selectCatalogOperationContext, } from '../utils/catalog-operation-context.js';
30
+ import { createCatalogStoreKey, readCatalogStoreEntry, } from '../utils/catalog-generation-store.js';
31
+ import { activateCatalogSourceGeneration, compareCatalogGenerationFreshness, probeCatalogSourceFreshness, probeCatalogSourceGeneration, probeCatalogSourceRepairGeneration, recoverCatalogGenerationStateAndActivate, repairCatalogSourceGeneration, replaceDamagedCatalogSourceGeneration, syncCatalogSourceGeneration, } from '../utils/catalog-generation-service.js';
32
+ import { calculateCatalogSourceImpactDigest, inspectCatalogSourceImpact, } from '../utils/catalog-source-impact.js';
33
+ import { readCatalogGenerationState } from '../utils/catalog-generation-state.js';
34
+ import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
35
+ import { CatalogProviderCompatibilityError } from '../utils/catalog-public-error.js';
36
+ import { loadCatalogGenerationViewModels } from './catalog-generation.js';
37
+ import { executePortableRetry, installPortableSkills, preparePortableInstall, preparePortableRetry, PortableCommandError, recheckPortableInstall, } from '../portable/commands.js';
38
+ import { MANAGED_CODEX_PACKAGE_NAME, MANAGED_CODEX_VERSION, } from '../utils/codex-manager.js';
39
+ import { projectPluginContentOverview, } from './plugin-content.js';
40
+ import { executeCatalogStoreTask, inspectCatalogStoreTask, prepareCatalogStoreTask, } from './catalog-store-task.js';
41
+ function reusableCandidateEvidenceIsComplete(candidate) {
42
+ const evidence = candidate.probe.candidate;
43
+ return [
44
+ evidence.sourceConfigFingerprint,
45
+ evidence.sourceIdentity,
46
+ evidence.logicalIdentity,
47
+ evidence.requestedRef,
48
+ evidence.resolvedCommit,
49
+ evidence.catalogVersion,
50
+ evidence.channel,
51
+ evidence.manifestDigest,
52
+ evidence.snapshotDigest,
53
+ ].every((value) => typeof value === 'string' && value.length > 0)
54
+ && typeof candidate.probe.manifest === 'object'
55
+ && candidate.probe.manifest !== null;
56
+ }
18
57
  async function loadPortablePluginIds(options) {
19
58
  return withResolvedMultiSourceCatalog({
20
59
  cliVersion: options.cliVersion,
21
- freshnessPolicy: 'cacheFirst',
22
60
  env: options.context.env,
23
61
  signal: options.signal,
62
+ ...(options.catalogOperationContext
63
+ ? { operationContext: options.catalogOperationContext }
64
+ : {}),
24
65
  consume: async (catalog) => (await Promise.all(catalog.sources.flatMap((item) => item.resolution
25
- ? [resolvePluginsFromCatalog(item.resolution.rootDir, undefined).then((plugins) => plugins
26
- .filter((plugin) => plugin.manifest.distributions?.agentSkills)
27
- .map((plugin) => formatPluginId({
28
- sourceId: item.resolution.sourceId,
29
- pluginName: plugin.manifest.name,
30
- })))]
31
- : []))).flat(),
66
+ ? item.resolution.manifest.entries.flatMap((entry) => {
67
+ const support = checkAwCapabilitySupport(entry.requires.aw.capabilities ?? [], IMPLEMENTED_AW_CAPABILITIES);
68
+ return support.supported
69
+ ? [readPluginManifest(path.join(item.resolution.rootDir, 'plugins', entry.name), { mode: 'bundle' }).then((plugin) => plugin.manifest.distributions?.agentSkills
70
+ ? formatPluginId({
71
+ sourceId: item.resolution.sourceId,
72
+ pluginName: entry.name,
73
+ })
74
+ : null)]
75
+ : [];
76
+ })
77
+ : []))).filter((pluginId) => pluginId !== null),
32
78
  });
33
79
  }
34
80
  let tuiControllerRuntimeTestHooks;
@@ -46,9 +92,14 @@ export function statusTargetFingerprint(targets, revisions = []) {
46
92
  platform: target.platform,
47
93
  scope: target.scope,
48
94
  state: target.state,
49
- errors: target.errors.map((error) => error.code).sort(),
95
+ errors: target.reasons.map((error) => error.code).sort(),
96
+ sharedReasons: [...(target.sharedReasons ?? [])]
97
+ .sort((left, right) => `${left.code}\0${left.message}`.localeCompare(`${right.code}\0${right.message}`, 'en')),
50
98
  components: target.components.map((component) => [component.key, component.state])
51
99
  .sort(([left], [right]) => left.localeCompare(right, 'en')),
100
+ sharedComponents: (target.sharedComponents ?? [])
101
+ .map((component) => [component.key, component.state, component.metadata])
102
+ .sort(([left], [right]) => left.localeCompare(right, 'en')),
52
103
  })).sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
53
104
  .localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en')),
54
105
  revisions: revisions.map((entry) => ({
@@ -60,24 +111,127 @@ export function statusTargetFingerprint(targets, revisions = []) {
60
111
  .localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en')),
61
112
  });
62
113
  }
63
- function operationReviewStale(message) {
64
- return new TuiOperationBlockedError('operationReviewStale', `operationReviewStale: ${message}`);
114
+ export function configAffectedStatusTargets(targets, pluginId, configScope) {
115
+ const affected = new Map();
116
+ for (const target of flattenStatusTargets(targets)) {
117
+ const intent = statusTargetIntent(target);
118
+ if (target.pluginId !== pluginId || !intent.enabled)
119
+ continue;
120
+ for (const scope of intent.scopes) {
121
+ if (configScope === 'project' && scope !== 'project')
122
+ continue;
123
+ affected.set(`${target.platform}\0${scope}`, { ...target, scope });
124
+ }
125
+ }
126
+ return [...affected.values()]
127
+ .sort((left, right) => `${left.platform}\0${left.scope}`
128
+ .localeCompare(`${right.platform}\0${right.scope}`, 'en'));
129
+ }
130
+ function configRetrySharedNativeRenderTargets(plugin) {
131
+ return (plugin.manifest.artifacts?.targets ?? []).flatMap((target) => target.platform === 'codex'
132
+ && target.target === 'codex.skill'
133
+ && target.delivery.mode === 'codex-native'
134
+ && target.skillKey
135
+ && target.refreshKeys.length > 0
136
+ ? [{ id: target.id, skillKey: target.skillKey, refreshKeys: [...target.refreshKeys] }]
137
+ : []);
138
+ }
139
+ function configImpactVisibleTargets(pluginName, targets) {
140
+ return targets.map((target) => ({
141
+ pluginName,
142
+ platform: target.platform,
143
+ scope: target.scope,
144
+ }));
145
+ }
146
+ function configIntentRequestsApply(intent) {
147
+ if (!intent.apply)
148
+ return false;
149
+ if (intent.retryApply === true)
150
+ return true;
151
+ if (intent.changes)
152
+ return intent.changes.length > 0;
153
+ return Object.keys(intent.updates ?? {}).length > 0
154
+ || (intent.removeKeys?.length ?? 0) > 0;
155
+ }
156
+ function configSaveBatchGroupIntent(intent, group) {
157
+ return {
158
+ kind: 'configSave',
159
+ pluginId: intent.pluginId,
160
+ scope: group.scope,
161
+ apply: intent.apply,
162
+ ...(intent.scriptTimeoutMs === undefined ? {} : { scriptTimeoutMs: intent.scriptTimeoutMs }),
163
+ ...(group.updates ? { updates: group.updates } : {}),
164
+ ...(group.removeKeys ? { removeKeys: group.removeKeys } : {}),
165
+ ...(group.sensitiveKeys ? { sensitiveKeys: group.sensitiveKeys } : {}),
166
+ ...(group.privateKeys ? { privateKeys: group.privateKeys } : {}),
167
+ ...(group.changes ? { changes: group.changes } : {}),
168
+ };
169
+ }
170
+ function assertConfigSaveBatchIntent(intent) {
171
+ const order = { project: 0, user: 1 };
172
+ if (intent.groups.length === 0 || intent.groups.length > 2
173
+ || new Set(intent.groups.map((group) => group.scope)).size !== intent.groups.length
174
+ || intent.groups.some((group, index) => index > 0
175
+ && order[intent.groups[index - 1].scope] >= order[group.scope])
176
+ || intent.groups.some((group) => Object.keys(group.updates ?? {}).length === 0
177
+ && (group.removeKeys?.length ?? 0) === 0)) {
178
+ throw new TuiOperationBlockedError('configBatchInvalid', '设置批次必须包含按当前项目、个人默认排序的唯一非空目标。');
179
+ }
180
+ }
181
+ function operationReviewStale(message, diagnostics) {
182
+ return new TuiOperationBlockedError('operationReviewStale', `operationReviewStale: ${message}`, diagnostics);
65
183
  }
66
184
  function sourceEvidenceForPlugin(source, pluginName) {
67
185
  const entry = source.manifest.entries.find((candidate) => candidate.name === pluginName);
68
186
  return {
69
187
  sourceId: source.sourceId,
70
188
  sourceIdentity: source.sourceIdentity,
189
+ logicalIdentity: source.logicalIdentity,
71
190
  requestedRef: source.requestedRef,
72
191
  resolvedCommit: source.resolvedCommit,
73
192
  manifestDigest: source.manifestDigest,
74
193
  snapshotDigest: source.snapshotDigest,
75
194
  contentDigest: entry.contentDigest,
195
+ pluginVersion: entry.version,
196
+ catalogVersion: source.manifest.catalogVersion,
197
+ channel: source.manifest.channel,
76
198
  };
77
199
  }
200
+ async function configLayerStoreDigest(options) {
201
+ const store = await readPluginConfigStore({
202
+ sourceId: options.source.sourceId,
203
+ sourceIdentity: options.source.sourceIdentity,
204
+ ...('logicalIdentity' in options.source
205
+ ? { logicalIdentity: options.source.logicalIdentity }
206
+ : {}),
207
+ pluginName: options.pluginName,
208
+ scope: options.scope,
209
+ cwd: options.cwd,
210
+ env: options.env,
211
+ });
212
+ return digest({ exists: store.exists, rawDigest: store.rawDigest });
213
+ }
214
+ async function inspectConfigShareability(options) {
215
+ if (options.scope !== 'project')
216
+ return undefined;
217
+ const targetPath = resolvePluginConfigStorePath({
218
+ sourceId: options.sourceId,
219
+ pluginName: options.pluginName,
220
+ scope: 'project',
221
+ cwd: options.cwd,
222
+ env: options.env,
223
+ });
224
+ const visibility = await assertProjectSharedConfigPathVisible({
225
+ cwd: options.cwd,
226
+ targetPath,
227
+ env: options.env,
228
+ });
229
+ return visibility.state === 'unverified' ? 'unverified' : 'verified';
230
+ }
78
231
  function assertSourceEvidenceMatches(expected, actual) {
79
232
  if (actual.sourceId !== expected.sourceId
80
233
  || actual.sourceIdentity !== expected.sourceIdentity
234
+ || actual.logicalIdentity !== expected.logicalIdentity
81
235
  || actual.resolvedCommit !== expected.resolvedCommit
82
236
  || actual.manifestDigest !== expected.manifestDigest
83
237
  || actual.snapshotDigest !== expected.snapshotDigest
@@ -85,142 +239,194 @@ function assertSourceEvidenceMatches(expected, actual) {
85
239
  throw operationReviewStale('确认后来源内容发生变化。');
86
240
  }
87
241
  }
242
+ function writeReviewIsStale(result) {
243
+ return result.targetResults.some((target) => target.errorCode === 'operationReviewStale'
244
+ || target.errorCode === 'operationPlanStale');
245
+ }
88
246
  function throwIfWriteReviewStale(result) {
89
- const stale = result.targetResults.find((target) => target.errorCode === 'operationReviewStale');
90
- if (stale) {
247
+ if (writeReviewIsStale(result)) {
91
248
  throw operationReviewStale('确认后实际写入计划发生变化。');
92
249
  }
93
250
  }
94
251
  const SOURCE_PROGRESS_PHASES = {
95
- refreshingCatalog: '刷新 Catalog 缓存',
96
- catalogReady: 'Catalog 已读取,索引已刷新',
97
252
  savingConfig: '保存来源配置',
98
253
  };
99
254
  function pluginIntent(intent) {
100
255
  return 'pluginId' in intent;
101
256
  }
102
- function assertIntentIsLegal(intent, targets, revisions = []) {
103
- const platforms = intent.platform === 'both'
104
- ? ['codex', 'claude']
105
- : [intent.platform];
106
- for (const platform of platforms) {
107
- const selected = targets.find((target) => target.pluginId === intent.pluginId
108
- && target.platform === platform && target.scope === intent.scope);
109
- if (!selected || selected.errors.some((error) => error.code === 'unsupportedTarget')) {
110
- throw new TuiOperationBlockedError('unsupportedTarget', '所选插件不支持这个平台和使用位置。');
111
- }
112
- const otherScopeInstalled = targets.some((target) => target.platform === platform
113
- && target.scope !== intent.scope
114
- && (target.state === 'present' || target.state === 'partial'));
115
- if (intent.action === 'install' && otherScopeInstalled) {
116
- throw new TuiOperationBlockedError('scopeConflict', '这个平台已安装在另一个使用位置。请先移除原位置。');
117
- }
118
- if (intent.action === 'install' && selected.state !== 'absent') {
119
- throw new TuiOperationBlockedError('targetAlreadyInstalled', '所选位置已经安装。');
120
- }
121
- const revision = revisions.find((entry) => entry.pluginId === intent.pluginId
122
- && entry.platform === platform && entry.scope === intent.scope)?.revision;
123
- if (intent.action === 'upgrade'
124
- && (selected.state !== 'present' || revision?.state !== 'upgradeAvailable')) {
125
- throw new TuiOperationBlockedError('targetStateChanged', '当前安装状态不允许执行这个操作。');
257
+ function reviewedScriptTimeoutMs(intent) {
258
+ if (intent.kind === 'pluginWriteTask') {
259
+ return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
260
+ }
261
+ if ((intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch')
262
+ && (intent.action === 'install' || intent.action === 'refreshAndSync'
263
+ || intent.action === 'reinstall' || intent.action === 'sync')) {
264
+ return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
265
+ }
266
+ if ((intent.kind === 'configSave' || intent.kind === 'configSaveBatch') && intent.apply) {
267
+ return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
268
+ }
269
+ return undefined;
270
+ }
271
+ function selectedBatchGroups(intent) {
272
+ if (intent.selection.kind !== 'selected' && intent.selection.kind !== 'targets') {
273
+ throw new TuiOperationBlockedError('syncSelectionRequired', '当前项目全部同步必须从项目 intent roots 解析。');
274
+ }
275
+ const rawTargets = intent.selection.kind === 'targets'
276
+ ? intent.selection.targets
277
+ : intent.selection.pluginIds.map((pluginId) => ({
278
+ pluginId, platform: intent.platform, scope: intent.scope,
279
+ }));
280
+ const normalizedTargets = rawTargets.map((target) => {
281
+ if ((target.platform !== 'codex' && target.platform !== 'claude' && target.platform !== 'both')
282
+ || (target.scope !== 'project' && target.scope !== 'user')
283
+ || (intent.selection.kind === 'targets' && target.platform === 'both')) {
284
+ throw new TuiOperationBlockedError('batchTargetInvalid', '批量精确目标必须使用有效 scope 和单个平台。');
126
285
  }
127
- if (intent.action === 'uninstall' && !targetCanBeUninstalled(selected.state)) {
128
- throw new TuiOperationBlockedError('targetStateChanged', '当前安装状态不允许执行这个操作。');
286
+ const pluginId = target.pluginId;
287
+ const parsed = parsePluginId(pluginId);
288
+ if (!parsed.sourceId) {
289
+ throw new TuiOperationBlockedError('invalidPluginId', '批量操作必须使用 canonical pluginId。');
129
290
  }
130
- if ((intent.action === 'reinstall' || intent.action === 'cleanup') && selected.state === 'absent') {
131
- throw new TuiOperationBlockedError('targetAbsent', '所选位置没有可处理的 AW 安装。');
291
+ const canonicalPluginId = formatPluginId({ sourceId: parsed.sourceId, pluginName: parsed.pluginName });
292
+ if (intent.selection.kind === 'targets' && canonicalPluginId !== pluginId) {
293
+ throw new TuiOperationBlockedError('invalidPluginId', '批量精确目标必须使用 canonical pluginId。');
132
294
  }
295
+ return {
296
+ pluginId: canonicalPluginId,
297
+ platform: target.platform,
298
+ scope: target.scope,
299
+ };
300
+ }).sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
301
+ .localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en'));
302
+ const uniqueTargets = [...new Map(normalizedTargets.map((target) => [
303
+ [target.pluginId, target.platform, target.scope].join('\0'), target,
304
+ ])).values()];
305
+ const requestedPluginIds = [...new Set(uniqueTargets.map((target) => target.pluginId))];
306
+ if (requestedPluginIds.length === 0) {
307
+ throw new TuiOperationBlockedError('syncSelectionRequired', '批量操作至少需要一个插件。');
308
+ }
309
+ const groups = new Map();
310
+ for (const target of uniqueTargets) {
311
+ const pluginId = target.pluginId;
312
+ const sourceId = parsePluginId(pluginId).sourceId;
313
+ const groupKey = intent.action === 'cleanup'
314
+ ? ['cleanup', target.platform, target.scope].join('\0')
315
+ : [sourceId, target.platform, target.scope].join('\0');
316
+ const group = groups.get(groupKey) ?? {
317
+ groupId: groupKey,
318
+ sourceId: intent.action === 'cleanup' ? 'offline' : sourceId,
319
+ platform: target.platform,
320
+ scope: target.scope,
321
+ requestedPluginIds: [],
322
+ };
323
+ group.requestedPluginIds.push(pluginId);
324
+ groups.set(groupKey, group);
133
325
  }
326
+ return { requestedPluginIds, groups: [...groups.values()] };
134
327
  }
135
- async function assertTypedIssueOrigin(intent, targets) {
136
- if (!intent.originIssue)
137
- return;
138
- const stale = (message) => {
139
- throw new TuiOperationBlockedError('operationReviewStale', `operationReviewStale: ${message}`);
140
- };
141
- const selected = targets.find((target) => target.pluginId === intent.pluginId
142
- && target.platform === intent.platform
143
- && target.scope === intent.scope);
144
- if (intent.originIssue.category === 'dependencyMissing') {
145
- if (intent.originIssue.actionId !== 'installDependency'
146
- || intent.action !== 'install' || !selected || selected.state !== 'absent') {
147
- stale('原缺失依赖问题已变化。');
148
- }
328
+ async function resolveBatchGroups(intent, env, cwd, frozenRequestedPluginIds) {
329
+ if (intent.selection.kind === 'selected' || intent.selection.kind === 'targets') {
330
+ return selectedBatchGroups(intent);
149
331
  }
150
- else if (intent.originIssue.category === 'dependencyNotReady') {
151
- if (intent.originIssue.actionId !== 'reinstallDependency'
152
- || intent.action !== 'reinstall' || !selected
153
- || (selected.state !== 'partial' && selected.state !== 'error')) {
154
- stale('原依赖安装问题已变化。');
155
- }
332
+ if (intent.action !== 'sync' || intent.scope !== 'project') {
333
+ throw new TuiOperationBlockedError('syncSelectionRequired', '当前项目全部操作只支持 project scope sync。');
156
334
  }
157
- else if (intent.originIssue.category === 'incompleteInstallation') {
158
- if (!selected || !targetCanBeUninstalled(selected.state) || selected.state !== 'partial'
159
- || !((intent.originIssue.actionId === 'reinstall' && intent.action === 'reinstall')
160
- || (intent.originIssue.actionId === 'remove' && intent.action === 'uninstall'))) {
161
- stale('原安装问题已变化。');
335
+ const state = await readPluginIntentState({ scope: 'project', cwd, env });
336
+ const projectIntentDigest = calculatePluginIntentDigest(state);
337
+ if (intent.selection.kind === 'currentProjectIntentTargets') {
338
+ if (intent.selection.targets.length === 0
339
+ || intent.selection.targets.some((target) => target.scope !== 'project')) {
340
+ throw new TuiOperationBlockedError('batchTargetInvalid', '当前项目精确同步目标必须是非空 project targets。');
162
341
  }
163
- return;
164
- }
165
- else if (intent.originIssue.category === 'sourceUnavailable') {
166
- if (intent.originIssue.actionId !== 'cleanup' || intent.action !== 'cleanup'
167
- || !selected || !selected.errors.some((error) => error.code === 'bundleUnavailable')) {
168
- stale('原 Catalog 缺失插件问题已变化。');
342
+ const exact = selectedBatchGroups({
343
+ ...intent,
344
+ selection: { kind: 'targets', targets: intent.selection.targets },
345
+ });
346
+ const declaredPlatforms = new Map(state.roots.map((root) => [
347
+ root.pluginId,
348
+ new Set(root.platforms),
349
+ ]));
350
+ if (intent.selection.targets.some((target) => !declaredPlatforms.get(target.pluginId)?.has(target.platform))) {
351
+ throw new TuiOperationBlockedError('batchTargetInvalid', '当前项目精确同步目标必须属于完整 project intent roots 声明。');
169
352
  }
170
- return;
353
+ return { ...exact, projectIntentDigest };
171
354
  }
172
- else if (intent.originIssue.category === 'scopeConflict') {
173
- const installedScopes = new Set(targets.filter((target) => target.pluginId === intent.pluginId
174
- && target.platform === intent.platform
175
- && (target.state === 'present' || target.state === 'partial')).map((target) => target.scope));
176
- const correctBranch = intent.action === 'uninstall'
177
- && ((intent.originIssue.actionId === 'keepUserScope' && intent.scope === 'project')
178
- || (intent.originIssue.actionId === 'keepProjectScope' && intent.scope === 'user'));
179
- if (!correctBranch || !installedScopes.has('user') || !installedScopes.has('project')) {
180
- stale('原位置冲突已变化。');
181
- }
182
- return;
355
+ const selection = resolveSyncIntentSelection({
356
+ roots: state.roots,
357
+ selectors: frozenRequestedPluginIds ?? [],
358
+ all: frozenRequestedPluginIds === undefined,
359
+ platform: intent.platform,
360
+ });
361
+ if (selection.requestedPluginIds.length === 0) {
362
+ throw new TuiOperationBlockedError('syncSelectionRequired', '当前项目没有可同步的 intent roots。');
183
363
  }
184
- else
185
- stale('这个问题没有安全的自动写入动作。');
186
- const origin = intent.originIssue;
187
- if (!origin.consumerPluginId || !origin.consumerSourceIdentity || !origin.consumerPlatform
188
- || !origin.consumerScope || !origin.dependencyPluginId || !origin.dependencyPlatform
189
- || !origin.dependencyScope || origin.dependencyProject === undefined
190
- || origin.dependencyPluginId !== intent.pluginId
191
- || origin.dependencyPlatform !== intent.platform || origin.dependencyScope !== intent.scope) {
192
- stale('依赖问题来源缺少稳定目标证据。');
364
+ return {
365
+ requestedPluginIds: selection.requestedPluginIds,
366
+ projectIntentDigest,
367
+ groups: selection.groups.map((group) => ({
368
+ groupId: `${group.sourceId}\0${group.platform}\0${intent.scope}`,
369
+ sourceId: group.sourceId,
370
+ platform: group.platform,
371
+ scope: intent.scope,
372
+ requestedPluginIds: group.pluginIds,
373
+ })),
374
+ };
375
+ }
376
+ async function runBatchWriteGroup(options) {
377
+ const dependencyScopeOverrides = options.intent.action === 'install'
378
+ ? options.intent.dependencyScopeOverrides?.filter((override) => parsePluginId(override.pluginId).sourceId === options.group.sourceId
379
+ && (options.group.platform === 'both' || override.platform === options.group.platform))
380
+ : undefined;
381
+ const commandOptions = {
382
+ cwd: options.cwd,
383
+ ...(dependencyScopeOverrides?.length ? { dependencyScopeOverrides } : {}),
384
+ platform: options.group.platform,
385
+ scope: options.group.scope,
386
+ ...(reviewedScriptTimeoutMs(options.intent) === undefined
387
+ ? {}
388
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(options.intent) }),
389
+ };
390
+ switch (options.intent.action) {
391
+ case 'install':
392
+ return installWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
393
+ case 'sync':
394
+ return syncWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
395
+ case 'reinstall':
396
+ return reinstallWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
397
+ case 'uninstall':
398
+ return uninstallWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
399
+ case 'cleanup':
400
+ return cleanupWriteCommand(options.group.requestedPluginIds, commandOptions, options.runtime);
193
401
  }
194
- const consumer = targets.find((target) => target.pluginId === origin.consumerPluginId
195
- && target.sourceIdentity === origin.consumerSourceIdentity
196
- && target.platform === origin.consumerPlatform
197
- && target.scope === origin.consumerScope);
198
- const consumerError = consumer?.errors.some((error) => error.code === origin.category && error.message.includes(origin.dependencyPluginId));
199
- if (!consumer || !consumerError)
200
- stale('原 consumer 依赖问题已消失或变化。');
201
402
  }
202
403
  async function loadDependencyOverviews(options) {
203
404
  const graph = await withResolvedMultiSourceCatalog({
204
405
  cliVersion: options.cliVersion,
205
- freshnessPolicy: 'cacheFirst',
206
406
  env: options.context.env,
207
407
  signal: options.signal,
408
+ ...(options.catalogOperationContext
409
+ ? { operationContext: options.catalogOperationContext }
410
+ : {}),
208
411
  consume: async (catalog) => ({
209
- nodes: (await Promise.all(catalog.sources.flatMap((item) => item.resolution
210
- ? [resolvePluginsFromCatalog(item.resolution.rootDir, undefined).then((plugins) => plugins.map((plugin) => ({
211
- pluginId: formatPluginId({
212
- sourceId: item.resolution.sourceId,
213
- pluginName: plugin.manifest.name,
214
- }),
215
- displayName: plugin.manifest.interface?.displayName ?? plugin.manifest.name,
216
- dependencies: (plugin.manifest.dependencies ?? []).map((pluginName) => formatPluginId({ sourceId: item.resolution.sourceId, pluginName })),
217
- })))] : []))).flat(),
412
+ nodes: catalog.sources.flatMap((item) => item.resolution
413
+ ? item.resolution.manifest.entries.map((entry) => ({
414
+ pluginId: formatPluginId({
415
+ sourceId: item.resolution.sourceId,
416
+ pluginName: entry.name,
417
+ }),
418
+ sourceIdentity: item.resolution.sourceIdentity,
419
+ displayName: entry.interface?.displayName ?? entry.name,
420
+ dependencies: entry.dependencies.map((pluginName) => formatPluginId({ sourceId: item.resolution.sourceId, pluginName })),
421
+ }))
422
+ : []),
218
423
  sourceErrors: catalog.sources.flatMap((item) => item.error
219
424
  ? [{ sourceId: item.sourceId, code: 'dependencyGraphInvalid' }]
220
425
  : []),
221
426
  }),
222
427
  });
223
428
  const nodes = new Map(graph.nodes.map((node) => [node.pluginId, node]));
429
+ const summaryById = new Map(options.status.pluginSummaries.map((summary) => [summary.pluginId, summary]));
224
430
  const displayName = (pluginId) => nodes.get(pluginId)?.displayName
225
431
  ?? parsePluginId(pluginId).pluginName;
226
432
  const reverse = new Map();
@@ -230,43 +436,61 @@ async function loadDependencyOverviews(options) {
230
436
  }
231
437
  }
232
438
  const targetsByPlugin = new Map();
233
- for (const target of options.status.payload.targets) {
439
+ for (const target of flattenStatusTargets(options.status.payload.targets)) {
234
440
  targetsByPlugin.set(target.pluginId, [...(targetsByPlugin.get(target.pluginId) ?? []), target]);
235
441
  }
236
- const placementReferences = new Map();
237
- for (const consumer of options.status.payload.targets.filter((target) => target.state !== 'absent')) {
238
- const consumerNode = nodes.get(consumer.pluginId);
239
- if (!consumerNode)
240
- continue;
241
- for (const dependencyPluginId of consumerNode.dependencies) {
242
- const dependencyTargets = (targetsByPlugin.get(dependencyPluginId) ?? [])
243
- .filter((target) => target.platform === consumer.platform);
244
- const projectTarget = dependencyTargets.find((target) => target.scope === 'project');
245
- const userTarget = dependencyTargets.find((target) => target.scope === 'user');
246
- const dependencyScope = consumer.scope === 'user'
247
- ? 'user'
248
- : userTarget && (!projectTarget
249
- || (userTarget.state !== 'absent' && projectTarget.state === 'absent'))
250
- ? 'user'
251
- : 'project';
252
- const reference = {
253
- consumer: {
254
- pluginId: consumer.pluginId,
255
- platform: consumer.platform,
256
- scope: consumer.scope,
257
- project: consumer.scope === 'project' ? 'current' : null,
258
- },
259
- dependency: {
260
- pluginId: dependencyPluginId,
261
- platform: consumer.platform,
262
- scope: dependencyScope,
263
- project: dependencyScope === 'project' ? 'current' : null,
264
- },
265
- };
266
- const key = `${consumer.pluginId}\0${dependencyPluginId}`;
267
- placementReferences.set(key, [...(placementReferences.get(key) ?? []), reference]);
268
- }
269
- }
442
+ const unsupportedPluginIds = new Set(options.status.pluginSummaries
443
+ .filter((summary) => (summary.missingCapabilities?.length ?? 0) > 0)
444
+ .map((summary) => summary.pluginId));
445
+ const availableReferencesFor = (consumerPluginId, dependencyPluginId) => {
446
+ const consumerSourceId = parsePluginId(consumerPluginId).sourceId;
447
+ const dependencySourceId = parsePluginId(dependencyPluginId).sourceId;
448
+ const consumerNode = nodes.get(consumerPluginId);
449
+ const dependencyNode = nodes.get(dependencyPluginId);
450
+ if (consumerSourceId !== dependencySourceId
451
+ || !nodes.get(consumerPluginId)?.dependencies.includes(dependencyPluginId))
452
+ return [];
453
+ return (targetsByPlugin.get(consumerPluginId) ?? []).flatMap((consumer) => {
454
+ if (!hasActualScopeEvidence(consumer)
455
+ || consumer.sourceId !== consumerSourceId
456
+ || consumer.sourceIdentity !== consumerNode?.sourceIdentity)
457
+ return [];
458
+ return [...consumer.components, ...(consumer.sharedComponents ?? [])].flatMap((component) => {
459
+ if (component.kind !== 'dependency'
460
+ || component.key !== dependencyPluginId
461
+ || component.state !== 'ready')
462
+ return [];
463
+ const platform = component.metadata?.platform;
464
+ const scope = component.metadata?.scope;
465
+ const project = component.metadata?.project;
466
+ if ((platform !== 'codex' && platform !== 'claude')
467
+ || (scope !== 'user' && scope !== 'project')
468
+ || project !== (scope === 'project' ? 'current' : 'none'))
469
+ return [];
470
+ const dependency = (targetsByPlugin.get(dependencyPluginId) ?? []).find((target) => target.platform === platform
471
+ && target.scope === scope
472
+ && target.sourceId === dependencySourceId
473
+ && target.sourceIdentity === dependencyNode?.sourceIdentity
474
+ && target.sourceIdentity === consumer.sourceIdentity);
475
+ if (!dependency)
476
+ return [];
477
+ return [{
478
+ consumer: {
479
+ pluginId: consumerPluginId,
480
+ platform: consumer.platform,
481
+ scope: consumer.scope,
482
+ project: consumer.scope === 'project' ? 'current' : null,
483
+ },
484
+ dependency: {
485
+ pluginId: dependencyPluginId,
486
+ platform,
487
+ scope,
488
+ project: scope === 'project' ? 'current' : null,
489
+ },
490
+ }];
491
+ });
492
+ });
493
+ };
270
494
  const placementsFor = (pluginId) => {
271
495
  const targets = targetsByPlugin.get(pluginId) ?? [];
272
496
  if (targets.length > 0) {
@@ -279,13 +503,15 @@ async function loadDependencyOverviews(options) {
279
503
  };
280
504
  const aggregateFor = (pluginId, placements) => {
281
505
  const sourceId = parsePluginId(pluginId).sourceId;
282
- if (graph.sourceErrors.some((error) => error.sourceId === sourceId))
506
+ if (graph.sourceErrors.some((error) => error.sourceId === sourceId)
507
+ || unsupportedPluginIds.has(pluginId))
283
508
  return 'unavailable';
284
509
  if (placements.length === 0 || placements.every((placement) => placement.state === 'notInstalled')) {
285
510
  return 'notInstalled';
286
511
  }
287
512
  return placements.some((placement) => placement.state === 'scopeConflict'
288
- || (placement.state === 'installed' && placement.health !== 'ready'))
513
+ || (placement.state === 'installed'
514
+ && (placement.health !== 'ready' || placement.blockingReason !== undefined)))
289
515
  ? 'needsAttention'
290
516
  : 'ready';
291
517
  };
@@ -336,7 +562,8 @@ async function loadDependencyOverviews(options) {
336
562
  })),
337
563
  placements,
338
564
  aggregateStatus: aggregateFor(dependency.pluginId, placements),
339
- placementReferences: placementReferences.get(`${pluginId}\0${dependency.pluginId}`) ?? [],
565
+ placementReferences: [],
566
+ availablePlacementReferences: availableReferencesFor(pluginId, dependency.pluginId),
340
567
  };
341
568
  });
342
569
  const dependentIds = [...new Set(reverse.get(pluginId) ?? [])].sort();
@@ -350,15 +577,23 @@ async function loadDependencyOverviews(options) {
350
577
  requiredBy: [],
351
578
  placements,
352
579
  aggregateStatus: aggregateFor(dependentId, placements),
353
- placementReferences: placementReferences.get(`${dependentId}\0${pluginId}`) ?? [],
580
+ placementReferences: [],
581
+ availablePlacementReferences: availableReferencesFor(dependentId, pluginId),
354
582
  };
355
583
  });
356
- const targetBlockers = (targetsByPlugin.get(pluginId) ?? []).flatMap((target) => target.errors.filter((error) => error.code.startsWith('dependency') || error.code === 'sourceChanged')
584
+ const targetBlockers = (targetsByPlugin.get(pluginId) ?? []).flatMap((target) => target.reasons.filter((error) => error.code.startsWith('dependency') || error.code === 'sourceChanged')
357
585
  .map((error) => ({
358
586
  code: error.code,
359
587
  title: '依赖关系需要处理',
360
588
  explanation: '当前目标的依赖证据不能安全通过检查。',
361
589
  })));
590
+ const consumerTargets = summaryById.get(pluginId)?.supportedTargets;
591
+ const unsupportedDependency = dependencyItems.find((dependency) => {
592
+ const dependencyTargets = summaryById.get(dependency.pluginId)?.supportedTargets;
593
+ if (!consumerTargets || !dependencyTargets)
594
+ return true;
595
+ return ['codex', 'claude'].some((platform) => consumerTargets[platform].length > 0 && dependencyTargets[platform].length === 0);
596
+ });
362
597
  const sourceId = parsePluginId(pluginId).sourceId;
363
598
  const blockers = [
364
599
  ...graph.sourceErrors.filter((error) => error.sourceId === sourceId).map((error) => ({
@@ -371,6 +606,11 @@ async function loadDependencyOverviews(options) {
371
606
  title: '依赖关系无效',
372
607
  explanation: '依赖关系形成环。',
373
608
  }] : []),
609
+ ...(unsupportedDependency ? [{
610
+ code: 'dependencyTargetUnsupported',
611
+ title: '依赖目标不兼容',
612
+ explanation: `${unsupportedDependency.displayName} 不支持根插件所需的平台。`,
613
+ }] : []),
374
614
  ...targetBlockers,
375
615
  ];
376
616
  const directCount = dependencyItems.filter((item) => item.relation === 'direct').length;
@@ -418,46 +658,206 @@ function createReviewStageReporter(options) {
418
658
  };
419
659
  }
420
660
  export function createTuiControllerDependencies(context, progressFeed, options = {}) {
661
+ const readInventory = options.inventoryCatalogSourceReferences
662
+ ?? inventoryCatalogSourceReferences;
663
+ const validatedStoreEntries = new Map();
664
+ const validatingStoreEntries = new Map();
665
+ const loadCatalogGenerations = async () => {
666
+ const generations = await loadCatalogGenerationViewModels({
667
+ env: context.env,
668
+ validatedStoreEntries,
669
+ validatingStoreEntries,
670
+ onStage: (event) => context.debugLogger.log({
671
+ type: 'performance:stage',
672
+ level: 'debug',
673
+ target: 'debugLog',
674
+ message: 'Catalog generation load stage completed.',
675
+ data: event,
676
+ }),
677
+ });
678
+ const retained = new Set(generations.flatMap((source) => [
679
+ source.active?.storeKey,
680
+ ].filter((value) => value !== undefined)));
681
+ for (const storeKey of validatedStoreEntries.keys()) {
682
+ if (!retained.has(storeKey))
683
+ validatedStoreEntries.delete(storeKey);
684
+ }
685
+ return generations;
686
+ };
687
+ const createOperationInventoryReader = () => {
688
+ let ordinaryInventory;
689
+ return (inventoryOptions) => {
690
+ ordinaryInventory ??= readInventory(inventoryOptions);
691
+ return ordinaryInventory;
692
+ };
693
+ };
694
+ const catalogOperationByEvidence = new WeakMap();
695
+ const taskCatalogOperationsByEvidence = new WeakMap();
696
+ const codexManagerByEvidence = new WeakMap();
697
+ let sessionCatalogOperationContext;
698
+ let sessionStatusResult;
699
+ const sessionContextForPlugin = (pluginId) => {
700
+ const sourceId = parsePluginId(pluginId).sourceId;
701
+ if (!sourceId || !sessionCatalogOperationContext)
702
+ return undefined;
703
+ return selectCatalogOperationContext(sessionCatalogOperationContext, [sourceId]);
704
+ };
705
+ const readConfigStoresForTui = async (config) => {
706
+ const scopes = config.scope === 'user' ? ['user'] : ['user', 'project'];
707
+ const stores = await Promise.all(scopes.map((scope) => readPluginConfigStore({
708
+ sourceId: config.sourceId,
709
+ sourceIdentity: config.sourceIdentity,
710
+ logicalIdentity: config.logicalIdentity,
711
+ pluginName: config.pluginName,
712
+ scope,
713
+ cwd: context.cwd,
714
+ env: context.env,
715
+ schema: config.schema,
716
+ })));
717
+ const user = stores[0];
718
+ const project = config.scope === 'user' ? undefined : stores[1];
719
+ config.signal.throwIfAborted();
720
+ const invalidStores = ([
721
+ { scope: 'user', store: user },
722
+ ...(project ? [{ scope: 'project', store: project }] : []),
723
+ ]).flatMap(({ scope, store }) => store.state === 'invalid' ? [{
724
+ scope,
725
+ code: store.invalidCode,
726
+ pathSummary: scope === 'user'
727
+ ? `AW_HOME/plugin-config/${config.sourceId ?? '<source>'}/${config.pluginName}/config.json`
728
+ : `<project>/.agent-workflows/plugin-config/${config.sourceId ?? '<source>'}/${config.pluginName}/config.json`,
729
+ }] : []);
730
+ if (invalidStores.length > 0)
731
+ throw new TuiConfigInvalidStoreError(invalidStores);
732
+ return { user, ...(project ? { project } : {}) };
733
+ };
421
734
  const loadStatus = async (signal) => {
422
735
  signal.throwIfAborted();
423
736
  const packageInfo = await readCliPackageInfo();
737
+ const catalogOperationContext = sessionCatalogOperationContext
738
+ ?? await resolvePartialCatalogOperationContext({
739
+ env: context.env,
740
+ signal,
741
+ validatedStoreEntries,
742
+ validatingStoreEntries,
743
+ }).catch(async (error) => {
744
+ if (!(error instanceof CatalogGenerationError)
745
+ || error.code !== 'catalogGenerationStateInvalid')
746
+ return undefined;
747
+ const snapshot = await readCatalogSourcesConfigSnapshot(context.env);
748
+ const primarySourceId = snapshot.config.sources.find((source) => source.primary).id;
749
+ const generationSetDigest = calculateCatalogGenerationSetDigest(snapshot.digest, primarySourceId, []);
750
+ return {
751
+ generationSet: {
752
+ generationSetDigest,
753
+ configDigest: snapshot.digest,
754
+ primarySourceId,
755
+ sources: [],
756
+ },
757
+ generationSetDigest,
758
+ catalog: {
759
+ sources: snapshot.config.sources.map((source) => ({
760
+ sourceId: source.id,
761
+ error,
762
+ })),
763
+ primarySourceId,
764
+ configDigest: snapshot.digest,
765
+ compositeDigest: generationSetDigest,
766
+ },
767
+ };
768
+ });
769
+ if (catalogOperationContext)
770
+ sessionCatalogOperationContext = catalogOperationContext;
424
771
  for (let attempt = 0; attempt < 3; attempt += 1) {
425
- const result = await statusCommandV4({
772
+ const result = await statusCommandV8({
426
773
  cwd: context.cwd,
427
774
  env: context.env,
775
+ nativeCliRunner: context.nativeCliRunner,
428
776
  cliVersion: packageInfo.version,
429
777
  verbose: context.diagnostics.verbose,
778
+ ...(catalogOperationContext ? { catalogOperationContext } : {}),
779
+ inventoryCatalogSourceReferences: readInventory,
430
780
  });
431
781
  signal.throwIfAborted();
432
782
  const configured = await listCatalogSources({ env: context.env });
433
783
  signal.throwIfAborted();
434
784
  if (result.payload.catalog.configDigest !== configured.configDigest)
435
785
  continue;
786
+ sessionStatusResult = result;
436
787
  const dependencyOverviews = await loadDependencyOverviews({
437
788
  context,
438
789
  cliVersion: packageInfo.version,
439
790
  status: result,
440
791
  signal,
792
+ ...(catalogOperationContext ? { catalogOperationContext } : {}),
441
793
  });
442
794
  const portablePluginIds = await loadPortablePluginIds({
443
795
  context,
444
796
  cliVersion: packageInfo.version,
445
797
  signal,
798
+ ...(catalogOperationContext ? { catalogOperationContext } : {}),
446
799
  });
447
800
  signal.throwIfAborted();
801
+ const catalogGenerations = await loadCatalogGenerations();
802
+ signal.throwIfAborted();
803
+ const configStoreInventory = (await Promise.all(['user', 'project'].map(async (scope) => {
804
+ try {
805
+ return (await listNamespacedPluginConfigStoreIdentities({
806
+ cwd: context.cwd,
807
+ env: context.env,
808
+ scope,
809
+ })).map(({ pluginId, sourceId, pluginName }) => ({
810
+ pluginId, sourceId, pluginName, scope,
811
+ }));
812
+ }
813
+ catch {
814
+ await context.debugLogger.log({
815
+ type: 'tui:config-inventory-error',
816
+ level: 'debug',
817
+ target: 'debugLog',
818
+ message: 'TUI saved config inventory could not be read.',
819
+ data: { scope },
820
+ });
821
+ return [];
822
+ }
823
+ }))).flat();
824
+ signal.throwIfAborted();
448
825
  const statusById = new Map(result.payload.catalog.sources.map((source) => [source.id, source]));
826
+ const projectIntentObservation = result.projectIntentObservation;
827
+ const projectIntent = projectIntentObservation.state === 'ready' ? {
828
+ state: 'ready',
829
+ roots: structuredClone(projectIntentObservation.value.roots),
830
+ } : projectIntentObservation.state === 'absent' ? {
831
+ state: 'absent',
832
+ } : {
833
+ state: projectIntentObservation.state,
834
+ code: projectIntentObservation.code,
835
+ message: projectIntentObservation.message,
836
+ };
449
837
  return {
838
+ cliChannel: packageInfo.version.includes('-') ? 'beta' : 'latest',
839
+ ...(result.projectRoot ? { projectRoot: result.projectRoot } : {}),
840
+ projectIntent,
841
+ ...(projectIntent.state === 'ready'
842
+ ? { projectIntentRoots: structuredClone(projectIntent.roots) }
843
+ : {}),
844
+ configuredSources: structuredClone(configured.sources),
450
845
  payload: result.payload,
451
846
  pluginSummaries: result.pluginSummaries,
452
847
  targetRevisions: result.targetRevisions,
453
848
  dependencyOverviews,
454
849
  portablePluginIds,
850
+ configStoreInventory,
851
+ catalogGenerations,
852
+ sessionGenerationKeys: Object.fromEntries((catalogOperationContext?.generationSet.sources ?? [])
853
+ .map((source) => [source.sourceId, source.storeKey])),
455
854
  sourceDetails: configured.sources.map((source) => {
456
855
  const status = statusById.get(source.id);
457
856
  return {
458
857
  id: source.id,
459
858
  type: source.type,
460
859
  primary: source.primary,
860
+ autoCheck: source.autoCheck,
461
861
  locationSummary: projectCatalogSourceLocation(source.type === 'git' ? source.url : source.path),
462
862
  ...(source.type === 'git' ? {
463
863
  refPolicy: source.refPolicy.type,
@@ -479,7 +879,475 @@ export function createTuiControllerDependencies(context, progressFeed, options =
479
879
  }
480
880
  throw new TuiOperationBlockedError('operationReviewStale', '状态读取期间来源配置持续变化。请重试。');
481
881
  };
482
- const captureEvidence = async (intent, signal, freshnessPolicy = 'cacheFirst', preparation) => {
882
+ const loadPluginContent = async (pluginId, signal) => {
883
+ signal.throwIfAborted();
884
+ try {
885
+ const packageInfo = await readCliPackageInfo();
886
+ const catalogOperation = sessionContextForPlugin(pluginId);
887
+ return await withResolvedCatalogPlugin({
888
+ selector: pluginId,
889
+ cliVersion: packageInfo.version,
890
+ env: context.env,
891
+ signal,
892
+ ...(catalogOperation ? { operationContext: catalogOperation } : {}),
893
+ consume: ({ plugin, source }) => projectPluginContentOverview({
894
+ pluginId,
895
+ catalogVersion: source.manifest.catalogVersion,
896
+ plugin,
897
+ }),
898
+ });
899
+ }
900
+ catch {
901
+ signal.throwIfAborted();
902
+ const summary = sessionStatusResult?.pluginSummaries.find((entry) => entry.pluginId === pluginId);
903
+ let sourceId;
904
+ try {
905
+ sourceId = parsePluginId(pluginId).sourceId;
906
+ }
907
+ catch {
908
+ // The fixed unavailable result below does not repeat an unsafe selector.
909
+ }
910
+ const catalogVersion = sessionStatusResult?.payload.catalog.sources
911
+ .find((source) => source.id === sourceId)?.catalogVersion;
912
+ return {
913
+ pluginId,
914
+ pluginVersion: summary?.version ?? '待确认',
915
+ catalogVersion: catalogVersion ?? '待确认',
916
+ evidence: 'unavailable',
917
+ items: [],
918
+ notices: ['当前无法安全读取这个插件的目录内容。请检查来源状态后重试。'],
919
+ coverage: 'unavailable',
920
+ coverageIssue: {
921
+ code: 'unreadable',
922
+ message: '当前无法安全读取这个插件的目录内容。请检查来源状态后重试。',
923
+ },
924
+ };
925
+ }
926
+ };
927
+ const captureEvidence = async (intent, signal, preparation, operationInventory = createOperationInventoryReader(), freshCatalog = false, reusableCandidate, codexObservationSession, frozenBatchEvidence) => {
928
+ reviewedScriptTimeoutMs(intent);
929
+ if (intent.kind === 'catalogStorePrune') {
930
+ signal?.throwIfAborted();
931
+ const config = await readCatalogSourcesConfigSnapshot(context.env);
932
+ const catalogStore = await prepareCatalogStoreTask({ env: context.env });
933
+ signal?.throwIfAborted();
934
+ return {
935
+ catalog: { configDigest: config.digest, compositeDigest: config.digest },
936
+ catalogStore,
937
+ };
938
+ }
939
+ if (intent.kind === 'configSaveBatch') {
940
+ assertConfigSaveBatchIntent(intent);
941
+ const groupEvidence = [];
942
+ let batchBase;
943
+ let batchCatalogOperation;
944
+ for (const group of intent.groups) {
945
+ const current = await captureEvidence(configSaveBatchGroupIntent(intent, group), signal, preparation, operationInventory, freshCatalog, reusableCandidate, codexObservationSession);
946
+ if (!current.config || !current.source || !current.targetFingerprint) {
947
+ throw operationReviewStale('设置批次缺少单层证据。');
948
+ }
949
+ if (batchBase && (JSON.stringify(batchBase.catalog) !== JSON.stringify(current.catalog)
950
+ || JSON.stringify(batchBase.source) !== JSON.stringify(current.source))) {
951
+ throw operationReviewStale('准备设置批次期间 Catalog 或来源发生变化。');
952
+ }
953
+ for (const expected of Object.values(group.editEvidence ?? {})) {
954
+ if (expected.sourceIdentity !== current.source.sourceIdentity
955
+ || expected.schemaIdentity !== current.config.schemaIdentity) {
956
+ throw operationReviewStale('编辑期间插件来源或设置定义已变化,请重新打开列表。');
957
+ }
958
+ for (const scope of ['user', 'project']) {
959
+ if (expected.storeDigests[scope] === undefined)
960
+ continue;
961
+ const currentDigest = await configLayerStoreDigest({
962
+ source: current.source, pluginName: parsePluginId(intent.pluginId).pluginName,
963
+ scope, cwd: context.cwd, env: context.env,
964
+ });
965
+ if (expected.storeDigests[scope] !== currentDigest) {
966
+ throw operationReviewStale('编辑期间设置已被外部修改,请重新打开列表。');
967
+ }
968
+ }
969
+ }
970
+ batchBase ??= current;
971
+ batchCatalogOperation ??= catalogOperationByEvidence.get(current);
972
+ groupEvidence.push({
973
+ scope: group.scope,
974
+ config: {
975
+ ...current.config,
976
+ storeDigest: await configLayerStoreDigest({
977
+ source: current.source,
978
+ pluginName: parsePluginId(intent.pluginId).pluginName,
979
+ scope: group.scope,
980
+ cwd: context.cwd,
981
+ env: context.env,
982
+ }),
983
+ },
984
+ targetFingerprint: current.targetFingerprint,
985
+ });
986
+ }
987
+ const evidence = {
988
+ catalog: batchBase.catalog,
989
+ source: batchBase.source,
990
+ configBatch: { groups: groupEvidence },
991
+ };
992
+ if (batchCatalogOperation)
993
+ catalogOperationByEvidence.set(evidence, batchCatalogOperation);
994
+ return evidence;
995
+ }
996
+ if ((intent.kind === 'pluginWrite'
997
+ && (intent.platform === 'codex' || intent.platform === 'both')
998
+ || intent.kind === 'pluginWriteTask'
999
+ && intent.targets.some((target) => target.platform === 'codex'))
1000
+ && !codexObservationSession) {
1001
+ const observationSession = await createCodexNativeObservationSession({
1002
+ exactCwd: context.cwd,
1003
+ env: context.env,
1004
+ runner: context.nativeCliRunner,
1005
+ });
1006
+ try {
1007
+ return await captureEvidence(intent, signal, preparation, operationInventory, freshCatalog, reusableCandidate, observationSession, frozenBatchEvidence);
1008
+ }
1009
+ finally {
1010
+ await observationSession.dispose();
1011
+ }
1012
+ }
1013
+ if (intent.kind === 'configReset') {
1014
+ const parsed = parsePluginId(intent.pluginId);
1015
+ if (!parsed.sourceId || !parsed.pluginName) {
1016
+ throw new TuiOperationBlockedError('invalidPluginId', '设置目标必须使用 canonical pluginId。');
1017
+ }
1018
+ await preparation?.reporter.emit('config');
1019
+ const target = await inspectPluginConfigResetTarget({
1020
+ sourceId: parsed.sourceId,
1021
+ pluginName: parsed.pluginName,
1022
+ scope: intent.scope,
1023
+ cwd: context.cwd,
1024
+ env: context.env,
1025
+ });
1026
+ signal?.throwIfAborted();
1027
+ if (target.kind === 'special') {
1028
+ throw new TuiOperationBlockedError('configResetTargetUnsupported', '设置目标是特殊文件系统节点,不能自动重置。请手动移动后重试。');
1029
+ }
1030
+ const relevantTargets = sessionStatusResult
1031
+ ? configAffectedStatusTargets(sessionStatusResult.payload.targets, intent.pluginId, intent.scope)
1032
+ : [];
1033
+ const relevantRevisions = (sessionStatusResult?.targetRevisions ?? []).filter((revision) => relevantTargets.some((statusTarget) => statusTarget.platform === revision.platform
1034
+ && statusTarget.scope === revision.scope
1035
+ && statusTarget.pluginId === revision.pluginId));
1036
+ let selected;
1037
+ try {
1038
+ const packageInfo = await readCliPackageInfo();
1039
+ selected = await withResolvedCatalogPlugin({
1040
+ selector: intent.pluginId,
1041
+ cliVersion: packageInfo.version,
1042
+ env: context.env,
1043
+ ...(preparation && sessionContextForPlugin(intent.pluginId)
1044
+ ? { operationContext: sessionContextForPlugin(intent.pluginId) }
1045
+ : {}),
1046
+ ...(signal ? { signal } : {}),
1047
+ consume: async ({ plugin, source: resolvedSource }) => ({
1048
+ source: sourceEvidenceForPlugin(resolvedSource, plugin.manifest.name),
1049
+ impact: await analyzePluginConfigMutationImpact({
1050
+ plugin,
1051
+ scope: intent.scope,
1052
+ cwd: context.cwd,
1053
+ env: context.env,
1054
+ visibleTargets: configImpactVisibleTargets(plugin.manifest.name, relevantTargets),
1055
+ mutation: { kind: 'reset' },
1056
+ }),
1057
+ }),
1058
+ });
1059
+ }
1060
+ catch {
1061
+ selected = undefined;
1062
+ }
1063
+ signal?.throwIfAborted();
1064
+ await preparation?.reporter.emit('evidence');
1065
+ return {
1066
+ catalog: sessionStatusResult ? {
1067
+ configDigest: sessionStatusResult.payload.catalog.configDigest,
1068
+ compositeDigest: sessionStatusResult.payload.catalog.compositeDigest,
1069
+ } : {
1070
+ configDigest: 'config-reset-offline',
1071
+ compositeDigest: 'config-reset-target',
1072
+ },
1073
+ ...(selected ? { source: selected.source } : {}),
1074
+ ...(sessionStatusResult ? {
1075
+ targetFingerprint: statusTargetFingerprint(relevantTargets, relevantRevisions),
1076
+ } : {}),
1077
+ config: {
1078
+ scope: intent.scope,
1079
+ storeDigest: selected?.impact.storeDigest ?? target.fingerprint,
1080
+ impactState: selected?.impact.impactState ?? 'unknown',
1081
+ ...(selected?.impact.noEffectiveChange ? { noEffectiveChange: true } : {}),
1082
+ affectedTargets: (selected?.impact.affectedTargets
1083
+ ?? relevantTargets).map(({ platform, scope }) => ({ platform, scope })),
1084
+ ...((selected?.impact.sharedNativeRenderTargets.length ?? 0) > 0
1085
+ ? { sharedNativeRenderTargets: selected.impact.sharedNativeRenderTargets }
1086
+ : {}),
1087
+ },
1088
+ resetTarget: {
1089
+ sourceId: parsed.sourceId,
1090
+ pluginName: parsed.pluginName,
1091
+ scope: intent.scope,
1092
+ kind: target.kind,
1093
+ fingerprint: target.fingerprint,
1094
+ },
1095
+ };
1096
+ }
1097
+ if (intent.kind === 'catalogSourcesRecovery') {
1098
+ signal?.throwIfAborted();
1099
+ await preparation?.reporter.emit('validate');
1100
+ await preparation?.reporter.emit('evidence');
1101
+ return {
1102
+ catalog: {
1103
+ configDigest: 'invalid-catalog-sources-config',
1104
+ compositeDigest: 'bootstrap-recovery',
1105
+ },
1106
+ };
1107
+ }
1108
+ if (intent.kind === 'catalogGeneration') {
1109
+ signal?.throwIfAborted();
1110
+ await preparation?.reporter.emit('validate');
1111
+ const packageInfo = await readCliPackageInfo();
1112
+ const config = await readCatalogSourcesConfigSnapshot(context.env);
1113
+ const configuredSource = config.config.sources.find((source) => source.id === intent.sourceId);
1114
+ let proposedSource;
1115
+ let proposedConfigDigest;
1116
+ let explicitSourceChange = false;
1117
+ let sourceIdentityChangeImpact;
1118
+ if (intent.sourceMutation) {
1119
+ const preview = await preflightCatalogSourceMutation({
1120
+ request: intent.sourceMutation.action === 'add'
1121
+ ? intent.sourceMutation
1122
+ : { action: 'update', sourceId: intent.sourceId, update: intent.sourceMutation.update },
1123
+ cwd: context.cwd,
1124
+ env: context.env,
1125
+ });
1126
+ proposedSource = preview.proposedSource;
1127
+ proposedConfigDigest = preview.proposedConfigDigest;
1128
+ if (preview.configDigest !== config.digest) {
1129
+ throw operationReviewStale('准备 Catalog 候选期间来源配置发生变化。');
1130
+ }
1131
+ const sourceFingerprintChanged = Boolean(proposedSource && (!configuredSource
1132
+ || calculateCatalogSourceConfigFingerprint(proposedSource)
1133
+ !== calculateCatalogSourceConfigFingerprint(configuredSource)));
1134
+ explicitSourceChange = preview.proposedConfigDigest !== preview.configDigest
1135
+ && (preview.identityChanged || sourceFingerprintChanged);
1136
+ if (preview.identityChanged)
1137
+ sourceIdentityChangeImpact = {
1138
+ impactSchemaVersion: 3,
1139
+ sourceId: preview.sourceId,
1140
+ primaryChange: preview.references.primaryChange,
1141
+ inventoryComplete: preview.references.inventoryComplete,
1142
+ references: preview.references.references,
1143
+ affectedReferences: preview.references.references,
1144
+ changes: {
1145
+ added: [], removed: [], changed: [], newlyIncompatible: [],
1146
+ dependencyChanged: [], platformSupportChanged: [],
1147
+ },
1148
+ errors: preview.references.errors,
1149
+ };
1150
+ if (!explicitSourceChange) {
1151
+ throw new TuiOperationBlockedError('sourceNoChanges', '来源没有变化。');
1152
+ }
1153
+ }
1154
+ let previousActive;
1155
+ let healthyActive;
1156
+ let previousActiveVersion;
1157
+ let previousStoreReady = false;
1158
+ try {
1159
+ previousActive = (await readCatalogGenerationState(context.env))
1160
+ .sources[intent.sourceId]?.active;
1161
+ }
1162
+ catch (error) {
1163
+ if (intent.action !== 'recoverState'
1164
+ || !(error instanceof CatalogGenerationError)
1165
+ || error.code !== 'catalogGenerationStateInvalid')
1166
+ throw error;
1167
+ }
1168
+ const activeStoreKey = previousActive
1169
+ ? createCatalogStoreKey(previousActive.logicalIdentity, previousActive.snapshotDigest)
1170
+ : null;
1171
+ const configuredFingerprint = configuredSource
1172
+ ? calculateCatalogSourceConfigFingerprint(configuredSource)
1173
+ : undefined;
1174
+ const canReuseCandidate = Boolean(reusableCandidate
1175
+ && !intent.sourceMutation
1176
+ && (intent.action === 'activate' || intent.action === 'update')
1177
+ && configuredFingerprint
1178
+ && reusableCandidate.sourceId === intent.sourceId
1179
+ && reusableCandidate.sourceConfigFingerprint === configuredFingerprint
1180
+ && reusableCandidate.probe.candidate.sourceConfigFingerprint === configuredFingerprint
1181
+ && reusableCandidate.activeStoreKey === activeStoreKey
1182
+ && reusableCandidate.probe.sourceId === intent.sourceId
1183
+ && reusableCandidateEvidenceIsComplete(reusableCandidate));
1184
+ const probe = intent.action === 'repair'
1185
+ ? await probeCatalogSourceRepairGeneration({
1186
+ sourceId: intent.sourceId,
1187
+ cliVersion: packageInfo.version,
1188
+ ...(previousActive ? { expectedActive: previousActive } : {}),
1189
+ expectedConfigDigest: config.digest,
1190
+ env: context.env,
1191
+ signal,
1192
+ runtime: options.catalogGenerationRuntime,
1193
+ })
1194
+ : canReuseCandidate
1195
+ ? structuredClone(reusableCandidate.probe)
1196
+ : await probeCatalogSourceGeneration({
1197
+ sourceId: intent.sourceId,
1198
+ ...(proposedSource ? { source: proposedSource } : {}),
1199
+ cliVersion: packageInfo.version,
1200
+ env: context.env,
1201
+ signal,
1202
+ runtime: options.catalogGenerationRuntime,
1203
+ });
1204
+ signal?.throwIfAborted();
1205
+ await preparation?.reporter.emit('evidence');
1206
+ if (intent.action === 'activate' || intent.action === 'update') {
1207
+ try {
1208
+ healthyActive = await resolveActiveCatalogGeneration({ sourceId: intent.sourceId, env: context.env });
1209
+ }
1210
+ catch (error) {
1211
+ if (!(error instanceof CatalogGenerationError)
1212
+ && !(error instanceof CatalogProviderCompatibilityError))
1213
+ throw error;
1214
+ }
1215
+ }
1216
+ const comparisonSource = proposedSource
1217
+ ?? configuredSource;
1218
+ if (previousActive) {
1219
+ const store = await readCatalogStoreEntry({
1220
+ logicalIdentity: previousActive.logicalIdentity,
1221
+ snapshotDigest: previousActive.snapshotDigest,
1222
+ env: context.env,
1223
+ }).catch(() => undefined);
1224
+ previousStoreReady = Boolean(store);
1225
+ previousActiveVersion = healthyActive?.catalogVersion
1226
+ ?? store?.metadata.catalogVersion;
1227
+ }
1228
+ if (intent.action === 'update' && !intent.sourceMutation && previousActive && comparisonSource) {
1229
+ explicitSourceChange = previousActive.sourceConfigFingerprint
1230
+ !== calculateCatalogSourceConfigFingerprint(comparisonSource)
1231
+ || previousActive.sourceIdentity !== await calculateCatalogSourceIdentity(comparisonSource);
1232
+ }
1233
+ const comparisonActive = healthyActive
1234
+ ? {
1235
+ logicalIdentity: healthyActive.logicalIdentity,
1236
+ catalogVersion: healthyActive.catalogVersion,
1237
+ snapshotDigest: healthyActive.snapshotDigest,
1238
+ resolvedCommit: healthyActive.resolvedCommit,
1239
+ }
1240
+ : previousActive && previousActiveVersion
1241
+ ? {
1242
+ logicalIdentity: previousActive.logicalIdentity,
1243
+ catalogVersion: previousActiveVersion,
1244
+ snapshotDigest: previousActive.snapshotDigest,
1245
+ resolvedCommit: previousActive.resolvedCommit,
1246
+ }
1247
+ : undefined;
1248
+ const candidateRelation = comparisonActive && comparisonSource
1249
+ ? compareCatalogGenerationFreshness({
1250
+ sourceType: comparisonSource.type,
1251
+ active: comparisonActive,
1252
+ candidate: probe.candidate,
1253
+ }).state
1254
+ : undefined;
1255
+ const exactStoreTarget = Boolean(previousActive)
1256
+ && createCatalogStoreKey(previousActive.logicalIdentity, previousActive.snapshotDigest)
1257
+ === createCatalogStoreKey(probe.candidate.logicalIdentity, probe.candidate.snapshotDigest);
1258
+ if (intent.action === 'repair' && !exactStoreTarget) {
1259
+ throw new CatalogGenerationError({
1260
+ code: 'catalogReplaceDamagedRequired', sourceId: intent.sourceId,
1261
+ message: `Catalog source "${intent.sourceId}" cannot reproduce the damaged Active entry. Use the current candidate replacement Review.`,
1262
+ });
1263
+ }
1264
+ const exactRepair = (intent.action === 'repair' || intent.action === 'update')
1265
+ && exactStoreTarget && !previousStoreReady;
1266
+ if (previousActive && !previousStoreReady && !exactStoreTarget
1267
+ && !intent.sourceMutation && intent.action !== 'replaceDamaged') {
1268
+ throw new CatalogGenerationError({
1269
+ code: 'catalogReplaceDamagedRequired',
1270
+ sourceId: intent.sourceId,
1271
+ message: `Catalog source "${intent.sourceId}" has a damaged Active entry whose version cannot be compared safely. The current Active remains unchanged. Edit the source explicitly, or remove and add it again.`,
1272
+ });
1273
+ }
1274
+ if (intent.action !== 'replaceDamaged' && !explicitSourceChange && candidateRelation === 'incompatible') {
1275
+ throw new CatalogGenerationError({
1276
+ code: 'catalogLogicalIdentityChanged', sourceId: intent.sourceId,
1277
+ message: `Catalog source "${intent.sourceId}" changed logical identity. Edit or remove and re-add the source.`,
1278
+ });
1279
+ }
1280
+ if (intent.action !== 'replaceDamaged' && !explicitSourceChange && candidateRelation === 'versionConflict') {
1281
+ throw new TuiOperationBlockedError('catalogVersionConflict', '候选 Catalog 与当前 Active 版本相同,但内容摘要不同。普通更新已阻止。');
1282
+ }
1283
+ if (intent.action !== 'replaceDamaged' && !explicitSourceChange && candidateRelation === 'rollbackDetected') {
1284
+ throw new TuiOperationBlockedError('catalogRollbackDetected', '候选 Catalog 版本低于当前 Active。普通更新已阻止。');
1285
+ }
1286
+ const impact = exactRepair ? undefined
1287
+ : sourceIdentityChangeImpact ?? await inspectCatalogSourceImpact({
1288
+ sourceId: intent.sourceId,
1289
+ cwd: context.cwd,
1290
+ env: context.env,
1291
+ sourcePrimary: comparisonSource?.primary === true,
1292
+ mode: 'generation',
1293
+ candidateManifest: probe.manifest,
1294
+ });
1295
+ const primaryPreview = intent.setPrimaryAfterActivation
1296
+ ? await preflightCatalogSourceMutation({
1297
+ request: { action: 'setPrimary', sourceId: intent.sourceId },
1298
+ cwd: context.cwd,
1299
+ env: context.env,
1300
+ })
1301
+ : undefined;
1302
+ if (primaryPreview && primaryPreview.configDigest !== config.digest) {
1303
+ throw operationReviewStale('准备主来源切换期间来源配置发生变化。');
1304
+ }
1305
+ const primaryImpact = primaryPreview ? {
1306
+ impactSchemaVersion: 3,
1307
+ sourceId: primaryPreview.sourceId,
1308
+ primaryChange: primaryPreview.references.primaryChange,
1309
+ inventoryComplete: primaryPreview.references.inventoryComplete,
1310
+ references: primaryPreview.references.references,
1311
+ affectedReferences: primaryPreview.references.references,
1312
+ changes: {
1313
+ added: [], removed: [], changed: [], newlyIncompatible: [],
1314
+ dependencyChanged: [], platformSupportChanged: [],
1315
+ },
1316
+ errors: primaryPreview.references.errors,
1317
+ } : undefined;
1318
+ return {
1319
+ catalog: {
1320
+ configDigest: config.digest,
1321
+ compositeDigest: probe.candidate.snapshotDigest,
1322
+ },
1323
+ catalogGeneration: {
1324
+ action: intent.action,
1325
+ ...(previousActive ? { previousActive } : {}),
1326
+ ...(previousActiveVersion ? { previousActiveVersion } : {}),
1327
+ candidate: probe.candidate,
1328
+ ...(candidateRelation ? { candidateRelation } : {}),
1329
+ ...(explicitSourceChange ? { explicitSourceChange: true } : {}),
1330
+ ...(exactRepair ? { exactRepair: true } : {}),
1331
+ activationMode: intent.action === 'replaceDamaged' ? 'replaceDamaged'
1332
+ : explicitSourceChange ? 'explicitSourceChange' : 'normal',
1333
+ ...(impact ? {
1334
+ impact,
1335
+ impactDigest: calculateCatalogSourceImpactDigest(impact),
1336
+ impactMode: sourceIdentityChangeImpact ? 'identityChange' : 'generation',
1337
+ } : {}),
1338
+ ...(primaryPreview && primaryImpact ? {
1339
+ primaryImpact,
1340
+ primaryImpactDigest: primaryPreview.impactDigest,
1341
+ primaryConfigDigest: primaryPreview.configDigest,
1342
+ } : {}),
1343
+ configDigest: config.digest,
1344
+ ...(proposedConfigDigest ? { proposedConfigDigest } : {}),
1345
+ ...(proposedSource ? { proposedSource } : {}),
1346
+ ...(intent.sourceMutation ? { sourceMutation: intent.sourceMutation } : {}),
1347
+ ...(intent.setPrimaryAfterActivation ? { setPrimaryAfterActivation: true } : {}),
1348
+ },
1349
+ };
1350
+ }
483
1351
  if (intent.kind === 'sourceMutation') {
484
1352
  signal?.throwIfAborted();
485
1353
  if (intent.action === 'add' && !intent.source
@@ -501,249 +1369,1329 @@ export function createTuiControllerDependencies(context, progressFeed, options =
501
1369
  await preparation?.reporter.emit('evidence');
502
1370
  signal?.throwIfAborted();
503
1371
  const evidence = {
504
- catalog: { configDigest: preview.configDigest, compositeDigest: preview.candidateDigest },
1372
+ catalog: { configDigest: preview.configDigest, compositeDigest: preview.proposedConfigDigest },
505
1373
  sourceWrite: {
506
1374
  configDigest: preview.configDigest,
507
- candidateDigest: preview.candidateDigest,
1375
+ proposedConfigDigest: preview.proposedConfigDigest,
508
1376
  ...(preview.sourceIdentity ? { sourceIdentity: preview.sourceIdentity } : {}),
509
1377
  inventoryDigest: digest({ references: preview.references.references, errors: preview.references.errors }),
1378
+ impactDigest: preview.impactDigest,
510
1379
  referenceCount: preview.references.references.length,
511
- inventoryComplete: preview.references.errors.length === 0,
1380
+ inventoryComplete: preview.references.inventoryComplete,
1381
+ impact: {
1382
+ impactSchemaVersion: 3,
1383
+ sourceId: preview.sourceId,
1384
+ primaryChange: preview.references.primaryChange,
1385
+ inventoryComplete: preview.references.inventoryComplete,
1386
+ references: preview.references.references,
1387
+ affectedReferences: preview.references.references,
1388
+ changes: {
1389
+ added: [], removed: [], changed: [], newlyIncompatible: [],
1390
+ dependencyChanged: [], platformSupportChanged: [],
1391
+ },
1392
+ errors: preview.references.errors,
1393
+ },
512
1394
  },
513
1395
  };
514
1396
  if (preparation?.validateIntent
515
- && evidence.sourceWrite?.configDigest === evidence.sourceWrite?.candidateDigest) {
1397
+ && evidence.sourceWrite?.configDigest === evidence.sourceWrite?.proposedConfigDigest) {
516
1398
  throw new TuiOperationBlockedError('sourceNoChanges', '来源没有变化。');
517
1399
  }
518
1400
  return evidence;
519
1401
  }
520
- if (!pluginIntent(intent)) {
1402
+ if (!pluginIntent(intent) && intent.kind !== 'pluginWriteBatch'
1403
+ && intent.kind !== 'pluginWriteTask') {
521
1404
  throw new TuiOperationBlockedError('operationNotImplemented', '这个写操作尚未接入新 Controller。');
522
1405
  }
523
1406
  signal?.throwIfAborted();
524
- const packageInfo = await readCliPackageInfo();
525
- if (intent.kind === 'portableInstall') {
526
- await preparation?.reporter.emit('portable');
527
- const portable = await preparePortableInstall({
528
- selector: intent.pluginId,
529
- scope: intent.scope,
530
- ...(intent.selectedSkills.length > 0 ? { requestedSkills: intent.selectedSkills } : {}),
531
- ...(intent.requestedAgents ? { requestedAgents: intent.requestedAgents } : {}),
532
- cwd: context.cwd,
533
- env: context.env,
534
- cliVersion: packageInfo.version,
535
- signal,
536
- });
537
- signal?.throwIfAborted();
538
- await preparation?.reporter.emit('evidence');
539
- signal?.throwIfAborted();
540
- return {
541
- catalog: {
542
- configDigest: portable.configDigest,
543
- compositeDigest: portable.evidenceDigest,
544
- },
545
- portable,
546
- };
1407
+ if (intent.kind === 'pluginWriteTask') {
1408
+ await preparation?.reporter.emit('status');
1409
+ const packageInfo = await readCliPackageInfo();
1410
+ const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
1411
+ const requestedBySource = new Map();
1412
+ for (const target of intent.targets) {
1413
+ const parsed = parsePluginId(target.pluginId);
1414
+ if (!parsed.sourceId || formatPluginId({
1415
+ sourceId: parsed.sourceId,
1416
+ pluginName: parsed.pluginName,
1417
+ }) !== target.pluginId) {
1418
+ throw new TuiOperationBlockedError('invalidPluginId', '任务目标必须使用 canonical pluginId。');
1419
+ }
1420
+ requestedBySource.set(parsed.sourceId, [
1421
+ ...(requestedBySource.get(parsed.sourceId) ?? []),
1422
+ target,
1423
+ ]);
1424
+ }
1425
+ const sources = [];
1426
+ const executableTargets = [];
1427
+ const causes = [];
1428
+ const blockedTargets = [];
1429
+ const observedTargets = [];
1430
+ const observedRevisions = [];
1431
+ const operationContexts = new Map();
1432
+ await preparation?.reporter.emit('catalog');
1433
+ for (const [sourceId, requestedTargets] of [...requestedBySource.entries()]
1434
+ .sort(([left], [right]) => left.localeCompare(right, 'en'))) {
1435
+ signal?.throwIfAborted();
1436
+ let catalogOperation;
1437
+ try {
1438
+ catalogOperation = !freshCatalog && sessionCatalogOperationContext
1439
+ ? selectCatalogOperationContext(sessionCatalogOperationContext, [sourceId])
1440
+ : await resolveCatalogOperationContext({
1441
+ sourceIds: [sourceId],
1442
+ env: context.env,
1443
+ ...(!freshCatalog ? { validatedStoreEntries, validatingStoreEntries } : {}),
1444
+ ...(signal ? { signal } : {}),
1445
+ });
1446
+ }
1447
+ catch (error) {
1448
+ if (error instanceof Error && error.name === 'AbortError')
1449
+ throw error;
1450
+ blockedTargets.push(...requestedTargets.map((target, index) => {
1451
+ const diagnostic = error instanceof CatalogGenerationError ? {
1452
+ id: `task-source:${sourceId}:${index}`,
1453
+ code: error.code,
1454
+ stage: 'source',
1455
+ sourceId,
1456
+ safeMessage: 'The Catalog source could not be prepared safely.',
1457
+ target: { pluginId: target.pluginId, platform: target.platform, scope: target.scope },
1458
+ causes: [],
1459
+ detailLines: [],
1460
+ } : projectThrownOperationDiagnostic({
1461
+ error,
1462
+ id: `task-source:${sourceId}:${index}`,
1463
+ stage: 'source',
1464
+ fallbackCode: 'catalogSourceResolutionFailed',
1465
+ sourceId,
1466
+ target,
1467
+ });
1468
+ return {
1469
+ target,
1470
+ reason: 'targetUnavailable',
1471
+ issueIds: [],
1472
+ errorCodes: [diagnostic.code],
1473
+ diagnostics: [diagnostic],
1474
+ };
1475
+ }));
1476
+ continue;
1477
+ }
1478
+ operationContexts.set(sourceId, catalogOperation);
1479
+ const status = await statusCommandV8({
1480
+ plugins: [...new Set(requestedTargets.map((target) => target.pluginId))],
1481
+ cwd: context.cwd,
1482
+ env: context.env,
1483
+ nativeCliRunner: context.nativeCliRunner,
1484
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1485
+ cliVersion: packageInfo.version,
1486
+ catalogOperationContext: catalogOperation,
1487
+ inventoryCatalogSourceReferences: operationInventory,
1488
+ });
1489
+ observedTargets.push(...flattenStatusTargets(status.payload.targets));
1490
+ observedRevisions.push(...status.targetRevisions);
1491
+ const model = buildTuiViewModel({
1492
+ status: status.payload,
1493
+ pluginSummaries: status.pluginSummaries,
1494
+ targetRevisions: status.targetRevisions,
1495
+ });
1496
+ const runtimeCandidates = intent.goal === 'restore'
1497
+ ? buildPluginWriteTaskCandidates(model, 'restore')
1498
+ : requestedTargets.map((target) => {
1499
+ const expected = target.expectedUpdate;
1500
+ const revision = status.targetRevisions.find((entry) => entry.pluginId === target.pluginId && entry.platform === target.platform
1501
+ && entry.scope === target.scope);
1502
+ const live = flattenStatusTargets(status.payload.targets).find((entry) => {
1503
+ if (entry.pluginId !== target.pluginId || entry.platform !== target.platform)
1504
+ return false;
1505
+ const desired = statusTargetIntent(entry);
1506
+ return entry.scope === target.scope || desired.scopes.includes(target.scope);
1507
+ });
1508
+ const generation = catalogOperation.generationSet.sources.find((entry) => entry.sourceId === sourceId);
1509
+ const eligible = Boolean(expected && live && revision
1510
+ && live.sourceIdentity === expected.sourceIdentity
1511
+ && generation?.storeKey === expected.generationKey
1512
+ && revision.revision.installedVersion === expected.installedVersion
1513
+ && revision.revision.availableVersion === expected.activeVersion
1514
+ && revision.revision.state === 'upgradeAvailable');
1515
+ const directVersionUpgrade = live !== undefined && revision !== undefined
1516
+ && isDirectCodexVersionUpgrade(live, revision.revision);
1517
+ return {
1518
+ target,
1519
+ displayName: model.plugins.find((plugin) => plugin.pluginId === target.pluginId)
1520
+ ?.displayName ?? parsePluginId(target.pluginId).pluginName,
1521
+ sourceId,
1522
+ eligible,
1523
+ ...(!eligible ? { blockReason: 'updateEvidenceIncomplete' } : {}),
1524
+ recommendedMethod: live?.state === 'ready' || directVersionUpgrade
1525
+ ? 'sync' : 'reinstall',
1526
+ issueIds: [],
1527
+ };
1528
+ });
1529
+ const draft = buildPluginWriteTaskDraft({ ...intent, targets: requestedTargets }, model, runtimeCandidates);
1530
+ blockedTargets.push(...draft.blockedTargets);
1531
+ if (draft.executableTargets.length === 0)
1532
+ continue;
1533
+ const methodsByTarget = new Map();
1534
+ for (const cause of draft.causes) {
1535
+ const key = pluginWriteTaskTargetKey(cause.repairTarget);
1536
+ const methods = methodsByTarget.get(key) ?? new Set();
1537
+ methods.add(cause.method);
1538
+ methodsByTarget.set(key, methods);
1539
+ }
1540
+ const conflicted = new Set([...methodsByTarget].filter(([, methods]) => methods.size > 1)
1541
+ .map(([key]) => key));
1542
+ if (conflicted.size > 0) {
1543
+ const blockedKeys = new Set(draft.causes.filter((cause) => conflicted.has(pluginWriteTaskTargetKey(cause.repairTarget)))
1544
+ .flatMap((cause) => cause.consumers.map(pluginWriteTaskTargetKey)));
1545
+ blockedTargets.push(...draft.executableTargets.filter((target) => blockedKeys.has(pluginWriteTaskTargetKey(target))).map((target) => ({
1546
+ target,
1547
+ reason: 'taskActionConflict',
1548
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1549
+ .map((cause) => cause.issueId),
1550
+ })));
1551
+ draft.executableTargets = draft.executableTargets.filter((target) => !blockedKeys.has(pluginWriteTaskTargetKey(target)));
1552
+ draft.causes = draft.causes.filter((cause) => cause.consumers.some((consumer) => !blockedKeys.has(pluginWriteTaskTargetKey(consumer))));
1553
+ }
1554
+ if (draft.executableTargets.length === 0)
1555
+ continue;
1556
+ const taskTargets = () => draft.executableTargets.map((target) => {
1557
+ const direct = draft.causes.filter((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === pluginWriteTaskTargetKey(target));
1558
+ return {
1559
+ pluginId: target.pluginId,
1560
+ platform: target.platform,
1561
+ scope: target.scope,
1562
+ method: direct.some((cause) => cause.method === 'reinstall')
1563
+ ? 'reinstall' : 'sync',
1564
+ };
1565
+ });
1566
+ const repairTargets = () => [...new Map(draft.causes.map((cause) => [
1567
+ pluginWriteTaskTargetKey(cause.repairTarget),
1568
+ { ...cause.repairTarget, method: cause.method },
1569
+ ])).values()];
1570
+ let preview;
1571
+ while (draft.executableTargets.length > 0) {
1572
+ preview = await taskWriteCommand(taskTargets(), {
1573
+ cwd: context.cwd,
1574
+ repairTargets: repairTargets(),
1575
+ ...(reviewedScriptTimeoutMs(intent) === undefined
1576
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
1577
+ }, {
1578
+ ...context,
1579
+ catalogOperationContext: catalogOperation,
1580
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
1581
+ expectedCatalogConfigDigest: configSnapshot.digest,
1582
+ inventoryCatalogSourceReferences: operationInventory,
1583
+ previewOperationPlan: true,
1584
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1585
+ ...(preparation ? {
1586
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1587
+ } : {}),
1588
+ });
1589
+ if (!preview.operationPlan) {
1590
+ const projected = projectExecutionDiagnostics(preview, `task-preview:${sourceId}`, [], 'pluginWrite');
1591
+ const sourceDiagnostics = projected.diagnostics.filter((diagnostic) => !diagnostic.target)
1592
+ .map((diagnostic) => ({ ...diagnostic, sourceId, target: undefined }));
1593
+ blockedTargets.push(...draft.executableTargets.map((target, targetIndex) => {
1594
+ const matchingDiagnostics = projected.diagnostics.filter((diagnostic) => diagnostic.target?.pluginId === target.pluginId
1595
+ && diagnostic.target.platform === target.platform
1596
+ && diagnostic.target.scope === target.scope);
1597
+ const preparedDiagnostics = [
1598
+ ...matchingDiagnostics,
1599
+ ...(targetIndex === 0 ? sourceDiagnostics : []),
1600
+ ];
1601
+ const targetDiagnostics = preparedDiagnostics.length > 0
1602
+ ? preparedDiagnostics
1603
+ : [projectThrownOperationDiagnostic({
1604
+ error: undefined,
1605
+ id: `task-preview:${sourceId}:${targetIndex}`,
1606
+ stage: 'dependencyPlan',
1607
+ fallbackCode: 'taskOperationPlanMissing',
1608
+ sourceId,
1609
+ target,
1610
+ })];
1611
+ const diagnostics = targetDiagnostics.map((diagnostic, diagnosticIndex) => ({
1612
+ ...structuredClone(diagnostic),
1613
+ id: `task-preview:${sourceId}:${targetIndex}:${diagnosticIndex}`,
1614
+ sourceId,
1615
+ }));
1616
+ return {
1617
+ target,
1618
+ reason: 'targetUnavailable',
1619
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1620
+ .map((cause) => cause.issueId),
1621
+ errorCodes: [...new Set(diagnostics.map((diagnostic) => diagnostic.code))],
1622
+ diagnostics,
1623
+ };
1624
+ }));
1625
+ draft.executableTargets = [];
1626
+ draft.causes = [];
1627
+ break;
1628
+ }
1629
+ if (intent.goal === 'restore') {
1630
+ const grouped = new Map();
1631
+ const blockedByVersion = new Map();
1632
+ for (const cause of draft.causes) {
1633
+ const placements = (preview.dependencyPlan?.targets ?? []).filter((target) => target.pluginId === cause.repairTarget.pluginId
1634
+ && target.platform === cause.repairTarget.platform);
1635
+ const placement = placements.find((target) => target.requestedScope === cause.repairTarget.scope)
1636
+ ?? (placements.length === 1 ? placements[0] : undefined);
1637
+ if (!placement) {
1638
+ throw new TuiOperationBlockedError('taskRepairPlacementMissing', '任务修复目标无法绑定到依赖位置。');
1639
+ }
1640
+ const repairTarget = {
1641
+ pluginId: cause.repairTarget.pluginId,
1642
+ platform: cause.repairTarget.platform,
1643
+ scope: placement.requestedScope ?? placement.resolvedScope,
1644
+ };
1645
+ const version = evaluatePluginWriteTaskRestoreVersion(repairTarget, cause.restoreVersionEvidence?.state === 'absent', model);
1646
+ if (!version.evidence || version.blockReason) {
1647
+ for (const consumer of cause.consumers) {
1648
+ const key = pluginWriteTaskTargetKey(consumer);
1649
+ const current = blockedByVersion.get(key) ?? {
1650
+ reason: version.blockReason ?? 'restoreVersionUnverified',
1651
+ issueIds: new Set(),
1652
+ };
1653
+ if (version.blockReason === 'restoreVersionChanged') {
1654
+ current.reason = version.blockReason;
1655
+ }
1656
+ current.issueIds.add(cause.issueId);
1657
+ blockedByVersion.set(key, current);
1658
+ }
1659
+ continue;
1660
+ }
1661
+ const causeKey = ['restore', cause.sourceIdentity,
1662
+ pluginWriteTaskTargetKey(repairTarget)].join('\0');
1663
+ const current = grouped.get(causeKey);
1664
+ grouped.set(causeKey, {
1665
+ cause: {
1666
+ ...cause,
1667
+ causeKey,
1668
+ issueId: [cause.issueId, ...(current?.issueIds ?? [])]
1669
+ .sort((left, right) => left.localeCompare(right, 'en'))[0],
1670
+ repairTarget,
1671
+ consumers: [...new Map([
1672
+ ...(current?.cause.consumers ?? []),
1673
+ ...cause.consumers,
1674
+ ].map((consumer) => [pluginWriteTaskTargetKey(consumer), consumer])).values()]
1675
+ .sort((left, right) => pluginWriteTaskTargetKey(left)
1676
+ .localeCompare(pluginWriteTaskTargetKey(right), 'en')),
1677
+ restoreVersionEvidence: version.evidence,
1678
+ },
1679
+ issueIds: new Set([...(current?.issueIds ?? []), cause.issueId]),
1680
+ methods: new Set([...(current?.methods ?? []), cause.method]),
1681
+ });
1682
+ }
1683
+ for (const group of grouped.values()) {
1684
+ if (group.methods.size < 2)
1685
+ continue;
1686
+ for (const consumer of group.cause.consumers) {
1687
+ const key = pluginWriteTaskTargetKey(consumer);
1688
+ const current = blockedByVersion.get(key) ?? {
1689
+ reason: 'taskActionConflict',
1690
+ issueIds: new Set(),
1691
+ };
1692
+ group.issueIds.forEach((issueId) => current.issueIds.add(issueId));
1693
+ blockedByVersion.set(key, current);
1694
+ }
1695
+ }
1696
+ const newlyBlockedKeys = new Set(blockedByVersion.keys());
1697
+ blockedTargets.push(...draft.executableTargets.flatMap((target) => {
1698
+ const blocked = blockedByVersion.get(pluginWriteTaskTargetKey(target));
1699
+ return blocked ? [{
1700
+ target,
1701
+ reason: blocked.reason,
1702
+ issueIds: [...blocked.issueIds].sort((left, right) => left.localeCompare(right, 'en')),
1703
+ }] : [];
1704
+ }));
1705
+ const normalizedCauses = [...grouped.values()]
1706
+ .filter((group) => group.methods.size === 1)
1707
+ .map((group) => ({
1708
+ ...group.cause,
1709
+ consumers: group.cause.consumers.filter((consumer) => !newlyBlockedKeys.has(pluginWriteTaskTargetKey(consumer))),
1710
+ }))
1711
+ .filter((cause) => cause.consumers.length > 0)
1712
+ .sort((left, right) => left.causeKey.localeCompare(right.causeKey, 'en'));
1713
+ const normalizedTargets = draft.executableTargets.filter((target) => !newlyBlockedKeys.has(pluginWriteTaskTargetKey(target)));
1714
+ const changed = JSON.stringify(normalizedCauses) !== JSON.stringify(draft.causes)
1715
+ || JSON.stringify(normalizedTargets) !== JSON.stringify(draft.executableTargets);
1716
+ draft.causes = normalizedCauses;
1717
+ draft.executableTargets = normalizedTargets;
1718
+ if (changed)
1719
+ continue;
1720
+ }
1721
+ const blockedPlanTargets = (preview.dependencyPlan?.targets ?? []).filter((target) => target.action === 'block' || target.errorCode !== undefined);
1722
+ if (blockedPlanTargets.length === 0)
1723
+ break;
1724
+ const requestedKeys = new Set(draft.executableTargets.map(pluginWriteTaskTargetKey));
1725
+ const blockedPlansByRoot = new Map();
1726
+ for (const blockedPlanTarget of blockedPlanTargets) {
1727
+ const pending = [blockedPlanTarget];
1728
+ const visited = new Set();
1729
+ const mappedRoots = new Set();
1730
+ const repairKey = pluginWriteTaskTargetKey({
1731
+ pluginId: blockedPlanTarget.pluginId,
1732
+ platform: blockedPlanTarget.platform,
1733
+ scope: blockedPlanTarget.requestedScope ?? blockedPlanTarget.resolvedScope,
1734
+ });
1735
+ for (const cause of draft.causes) {
1736
+ if (pluginWriteTaskTargetKey(cause.repairTarget) !== repairKey)
1737
+ continue;
1738
+ cause.consumers.forEach((consumer) => mappedRoots.add(pluginWriteTaskTargetKey(consumer)));
1739
+ }
1740
+ while (pending.length > 0) {
1741
+ const current = pending.shift();
1742
+ const currentKey = pluginWriteTaskTargetKey({
1743
+ pluginId: current.pluginId,
1744
+ platform: current.platform,
1745
+ scope: current.requestedScope ?? current.resolvedScope,
1746
+ });
1747
+ if (visited.has(currentKey))
1748
+ continue;
1749
+ visited.add(currentKey);
1750
+ if (current.relation === 'requested' && requestedKeys.has(currentKey)) {
1751
+ mappedRoots.add(currentKey);
1752
+ }
1753
+ for (const consumer of current.requiredByTargets ?? []) {
1754
+ const consumerKey = pluginWriteTaskTargetKey(consumer);
1755
+ if (requestedKeys.has(consumerKey)) {
1756
+ mappedRoots.add(consumerKey);
1757
+ continue;
1758
+ }
1759
+ const placement = preview.dependencyPlan?.targets.find((target) => target.pluginId === consumer.pluginId && target.platform === consumer.platform
1760
+ && (target.requestedScope ?? target.resolvedScope) === consumer.scope);
1761
+ if (placement)
1762
+ pending.push(placement);
1763
+ }
1764
+ }
1765
+ if (mappedRoots.size === 0) {
1766
+ throw new TuiOperationBlockedError('taskPlanConsumerMissing', '任务计划中的阻止项无法映射到请求目标。');
1767
+ }
1768
+ for (const key of mappedRoots) {
1769
+ const plans = blockedPlansByRoot.get(key) ?? [];
1770
+ plans.push(blockedPlanTarget);
1771
+ blockedPlansByRoot.set(key, plans);
1772
+ }
1773
+ }
1774
+ blockedTargets.push(...draft.executableTargets.flatMap((target) => {
1775
+ const plans = blockedPlansByRoot.get(pluginWriteTaskTargetKey(target));
1776
+ return plans ? [{
1777
+ target,
1778
+ reason: 'targetUnavailable',
1779
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1780
+ .map((cause) => cause.issueId),
1781
+ errorCodes: [...new Set(plans.map((plan) => plan.errorCode ?? 'writePlanBlocked'))]
1782
+ .sort((left, right) => left.localeCompare(right, 'en')),
1783
+ diagnostics: plans.map((plan, index) => ({
1784
+ id: `task-plan:${sourceId}:${target.pluginId}:${target.platform}:${target.scope}:${index}`,
1785
+ code: plan.errorCode ?? 'writePlanBlocked',
1786
+ stage: 'dependencyPlan',
1787
+ sourceId,
1788
+ target: {
1789
+ pluginId: plan.pluginId, platform: plan.platform,
1790
+ scope: plan.requestedScope ?? plan.resolvedScope,
1791
+ },
1792
+ causes: [],
1793
+ detailLines: [],
1794
+ safeMessage: plan.errorCode === 'artifactCollision'
1795
+ ? '输出位置存在外来内容,无法覆盖。'
1796
+ : plan.errorCode === 'exclusiveOwnerEvidenceInvalid'
1797
+ ? '独立扩展的归属标记缺失或损坏。'
1798
+ : plan.errorCode === 'dependencyNotReady'
1799
+ ? '依赖安装状态无法读取。'
1800
+ : '依赖计划阻止了此目标。',
1801
+ })),
1802
+ }] : [];
1803
+ }));
1804
+ draft.executableTargets = draft.executableTargets.filter((target) => !blockedPlansByRoot.has(pluginWriteTaskTargetKey(target)));
1805
+ draft.causes = draft.causes.map((cause) => ({
1806
+ ...cause,
1807
+ consumers: cause.consumers.filter((consumer) => !blockedPlansByRoot.has(pluginWriteTaskTargetKey(consumer))),
1808
+ })).filter((cause) => cause.consumers.length > 0);
1809
+ }
1810
+ if (!preview?.operationPlan || draft.executableTargets.length === 0)
1811
+ continue;
1812
+ const sourceIdentity = draft.causes[0]?.sourceIdentity
1813
+ ?? status.payload.catalog.sources.find((source) => source.id === sourceId)?.identity;
1814
+ if (!sourceIdentity) {
1815
+ throw new TuiOperationBlockedError('taskSourceIdentityMissing', '任务来源身份不可验证。');
1816
+ }
1817
+ const sourceEvidence = {
1818
+ sourceId,
1819
+ sourceIdentity,
1820
+ requestedTargets: draft.executableTargets,
1821
+ causes: draft.causes,
1822
+ preview,
1823
+ executable: true,
1824
+ };
1825
+ sources.push(sourceEvidence);
1826
+ executableTargets.push(...draft.executableTargets);
1827
+ causes.push(...draft.causes);
1828
+ }
1829
+ const canonicalBlocked = [...new Map(blockedTargets.map((target) => [
1830
+ pluginWriteTaskTargetKey(target.target), target,
1831
+ ])).values()].sort((left, right) => pluginWriteTaskTargetKey(left.target)
1832
+ .localeCompare(pluginWriteTaskTargetKey(right.target), 'en'));
1833
+ if (executableTargets.length === 0) {
1834
+ throw new TuiOperationBlockedError('taskNoExecutableTargets', '任务没有可安全执行的目标。', canonicalBlocked.flatMap((target) => target.diagnostics ?? []));
1835
+ }
1836
+ if (intent.executionPolicy === 'all' && canonicalBlocked.length > 0) {
1837
+ throw new TuiOperationBlockedError('taskHasBlockedTargets', '部分目标需要先处理。请调整选择。', canonicalBlocked.flatMap((target) => target.diagnostics ?? []));
1838
+ }
1839
+ await preparation?.reporter.emit('evidence');
1840
+ const evidence = {
1841
+ catalog: {
1842
+ configDigest: configSnapshot.digest,
1843
+ compositeDigest: digest(sources.map((source) => [
1844
+ source.sourceId,
1845
+ source.sourceIdentity,
1846
+ source.preview.operationPlan?.planFingerprint,
1847
+ ])),
1848
+ },
1849
+ taskPlan: {
1850
+ goal: intent.goal,
1851
+ requestedTargets: intent.targets,
1852
+ executableTargets: [...executableTargets].sort((left, right) => pluginWriteTaskTargetKey(left).localeCompare(pluginWriteTaskTargetKey(right), 'en')),
1853
+ causes: [...new Map(causes.map((cause) => [cause.causeKey, cause])).values()]
1854
+ .sort((left, right) => left.causeKey.localeCompare(right.causeKey, 'en')),
1855
+ blockedTargets: canonicalBlocked,
1856
+ sources,
1857
+ targetFingerprint: statusTargetFingerprint(observedTargets, observedRevisions),
1858
+ planFingerprint: '',
1859
+ },
1860
+ };
1861
+ evidence.taskPlan.planFingerprint = operationEvidenceFingerprint(evidence);
1862
+ taskCatalogOperationsByEvidence.set(evidence, operationContexts);
1863
+ return evidence;
547
1864
  }
548
- await preparation?.reporter.emit('status');
549
- const status = await statusCommandV4({
550
- plugins: [...new Set([intent.pluginId,
551
- ...(intent.kind === 'pluginWrite' && intent.originIssue?.consumerPluginId
552
- ? [intent.originIssue.consumerPluginId] : [])])],
553
- ...('platform' in intent ? { platform: intent.platform } : {}),
554
- ...(intent.kind !== 'pluginWrite' && intent.scope === 'project'
555
- ? { scope: intent.scope }
556
- : {}),
557
- cwd: context.cwd,
558
- env: context.env,
559
- cliVersion: packageInfo.version,
560
- });
561
- signal?.throwIfAborted();
562
- if (intent.kind === 'pluginWrite') {
563
- await assertTypedIssueOrigin(intent, status.payload.targets);
564
- if (preparation?.validateIntent) {
565
- assertIntentIsLegal(intent, status.payload.targets, status.targetRevisions);
1865
+ if (intent.kind === 'pluginWriteBatch') {
1866
+ await preparation?.reporter.emit('status');
1867
+ const selection = await resolveBatchGroups(intent, context.env, context.cwd, frozenBatchEvidence?.requestedPluginIds);
1868
+ signal?.throwIfAborted();
1869
+ if (intent.action === 'cleanup') {
1870
+ const groups = [];
1871
+ const reviewedRemovedPlugins = new Set();
1872
+ for (const group of selection.groups) {
1873
+ signal?.throwIfAborted();
1874
+ const requestedPluginIds = group.requestedPluginIds.filter((pluginId) => !reviewedRemovedPlugins.has(pluginId));
1875
+ if (requestedPluginIds.length === 0)
1876
+ continue;
1877
+ const preview = await runBatchWriteGroup({
1878
+ intent,
1879
+ group: { ...group, requestedPluginIds },
1880
+ cwd: context.cwd,
1881
+ runtime: {
1882
+ ...context,
1883
+ inventoryCatalogSourceReferences: operationInventory,
1884
+ previewOperationPlan: true,
1885
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1886
+ ...(preparation ? {
1887
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1888
+ } : {}),
1889
+ },
1890
+ });
1891
+ for (const pluginId of preview.removedPluginCleanupIds ?? []) {
1892
+ reviewedRemovedPlugins.add(pluginId);
1893
+ }
1894
+ groups.push({ ...group, requestedPluginIds, preview,
1895
+ executable: preview.operationPlan !== undefined });
1896
+ }
1897
+ await preparation?.reporter.emit('evidence');
1898
+ signal?.throwIfAborted();
1899
+ const evidence = {
1900
+ catalog: {
1901
+ configDigest: 'cleanup-offline',
1902
+ compositeDigest: digest(groups.map((group) => [
1903
+ group.groupId,
1904
+ group.preview.operationPlan?.planFingerprint,
1905
+ ])),
1906
+ },
1907
+ requestedPluginIds: selection.requestedPluginIds,
1908
+ groups,
1909
+ };
1910
+ return evidence;
566
1911
  }
1912
+ const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
1913
+ const groups = [];
1914
+ const ownsObservationSession = !codexObservationSession
1915
+ && selection.groups.some((group) => {
1916
+ const reviewed = frozenBatchEvidence?.groups.find((candidate) => candidate.groupId === group.groupId);
1917
+ return reviewed?.executable !== false
1918
+ && (group.platform === 'codex' || group.platform === 'both');
1919
+ });
1920
+ const observationSession = codexObservationSession ?? (ownsObservationSession
1921
+ ? await createCodexNativeObservationSession({
1922
+ exactCwd: context.cwd,
1923
+ env: context.env,
1924
+ runner: context.nativeCliRunner,
1925
+ })
1926
+ : undefined);
1927
+ try {
1928
+ await preparation?.reporter.emit('catalog');
1929
+ for (const group of selection.groups) {
1930
+ signal?.throwIfAborted();
1931
+ const reviewed = frozenBatchEvidence?.groups.find((candidate) => candidate.groupId === group.groupId);
1932
+ if (reviewed?.executable === false) {
1933
+ groups.push({ ...group, preview: reviewed.preview, executable: false });
1934
+ continue;
1935
+ }
1936
+ let catalogOperation;
1937
+ try {
1938
+ catalogOperation = !freshCatalog && sessionCatalogOperationContext
1939
+ ? selectCatalogOperationContext(sessionCatalogOperationContext, [group.sourceId])
1940
+ : await resolveCatalogOperationContext({
1941
+ sourceIds: [group.sourceId],
1942
+ env: context.env,
1943
+ ...(!freshCatalog ? { validatedStoreEntries, validatingStoreEntries } : {}),
1944
+ ...(signal ? { signal } : {}),
1945
+ });
1946
+ }
1947
+ catch {
1948
+ catalogOperation = undefined;
1949
+ }
1950
+ const preview = await runBatchWriteGroup({
1951
+ intent,
1952
+ group,
1953
+ cwd: context.cwd,
1954
+ runtime: {
1955
+ ...context,
1956
+ inventoryCatalogSourceReferences: operationInventory,
1957
+ expectedCatalogConfigDigest: configSnapshot.digest,
1958
+ ...(catalogOperation ? {
1959
+ catalogOperationContext: catalogOperation,
1960
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
1961
+ } : {}),
1962
+ previewOperationPlan: true,
1963
+ ...(observationSession ? { codexObservationSession: observationSession } : {}),
1964
+ ...(preparation ? {
1965
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1966
+ } : {}),
1967
+ },
1968
+ });
1969
+ groups.push({ ...group, preview, executable: preview.operationPlan !== undefined });
1970
+ }
1971
+ }
1972
+ finally {
1973
+ if (ownsObservationSession)
1974
+ await observationSession?.dispose();
1975
+ }
1976
+ await preparation?.reporter.emit('evidence');
1977
+ signal?.throwIfAborted();
1978
+ const merged = mergeWriteCommandResults(groups.map((group) => group.preview));
1979
+ const targetVersions = groups.flatMap((group) => {
1980
+ if (group.platform === 'both')
1981
+ return [];
1982
+ const platform = group.platform;
1983
+ return group.requestedPluginIds.map((pluginId) => {
1984
+ const live = flattenStatusTargets(sessionStatusResult?.payload.targets ?? []).find((target) => target.pluginId === pluginId && target.platform === platform
1985
+ && (target.scope === group.scope || statusTargetIntent(target).scopes.includes(group.scope)
1986
+ || platform === 'codex' && statusTargetActivation(target)?.[group.scope] === true));
1987
+ const revision = sessionStatusResult?.targetRevisions?.find((target) => target.pluginId === pluginId && target.platform === platform
1988
+ && target.scope === group.scope)?.revision;
1989
+ const activation = live ? statusTargetActivation(live) : undefined;
1990
+ const ownership = live && live.scope === group.scope
1991
+ ? statusTargetOwnershipState(live) : 'absent';
1992
+ const present = ownership === 'present' || ownership === 'partial'
1993
+ || platform === 'codex' && activation?.[group.scope] === true;
1994
+ const planned = live ? statusTargetIntent(live).scopes.includes(group.scope) : false;
1995
+ return {
1996
+ pluginId,
1997
+ platform,
1998
+ scope: group.scope,
1999
+ presence: present ? 'installed'
2000
+ : ownership === 'absent' && planned ? 'new' : 'unknown',
2001
+ ...(revision?.installedVersion ? { installedVersion: revision.installedVersion } : {}),
2002
+ ...(revision?.availableVersion ? { availableVersion: revision.availableVersion } : {}),
2003
+ };
2004
+ });
2005
+ });
2006
+ const evidence = {
2007
+ catalog: {
2008
+ configDigest: configSnapshot.digest,
2009
+ compositeDigest: sessionStatusResult?.payload.catalog.compositeDigest
2010
+ ?? digest(groups.map((group) => [group.groupId, group.preview.operationPlan?.planFingerprint])),
2011
+ },
2012
+ requestedPluginIds: selection.requestedPluginIds,
2013
+ ...(selection.projectIntentDigest
2014
+ ? { projectIntentDigest: selection.projectIntentDigest }
2015
+ : {}),
2016
+ groups,
2017
+ targetVersions: [...new Map(targetVersions.map((target) => [[
2018
+ target.pluginId, target.platform, target.scope,
2019
+ ].join('\0'), target])).values()].sort((left, right) => [
2020
+ left.pluginId, left.platform, left.scope,
2021
+ ].join('\0').localeCompare([
2022
+ right.pluginId, right.platform, right.scope,
2023
+ ].join('\0'), 'en')),
2024
+ ...(merged.dependencyPlan ? { dependencyPlan: merged.dependencyPlan } : {}),
2025
+ ...(merged.codexReconcilePlans ? { codexReconcilePlans: merged.codexReconcilePlans } : {}),
2026
+ ...(merged.operationPlan ? { operationPlan: merged.operationPlan } : {}),
2027
+ };
2028
+ return evidence;
567
2029
  }
568
- const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
569
- signal?.throwIfAborted();
570
- if (configSnapshot.digest !== status.payload.catalog.configDigest) {
571
- throw operationReviewStale('准备确认信息期间来源配置发生变化。');
2030
+ if (intent.kind === 'pluginWrite' && intent.action === 'cleanup') {
2031
+ const preview = await cleanupWriteCommand([intent.pluginId], {
2032
+ cwd: context.cwd,
2033
+ platform: intent.platform,
2034
+ scope: intent.scope,
2035
+ force: intent.force === true,
2036
+ }, {
2037
+ ...context,
2038
+ inventoryCatalogSourceReferences: operationInventory,
2039
+ previewOperationPlan: true,
2040
+ ...(codexObservationSession ? { codexObservationSession } : {}),
2041
+ ...(preparation ? {
2042
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
2043
+ } : {}),
2044
+ });
2045
+ signal?.throwIfAborted();
2046
+ if (!preview.operationPlan) {
2047
+ throw new TuiOperationBlockedError('cleanupPlanUnavailable', '无法建立可信的清理计划。');
2048
+ }
2049
+ const offlineCleanupPlans = preview.offlineCleanupPlans ?? [];
2050
+ await preparation?.reporter.emit('evidence');
2051
+ return {
2052
+ catalog: {
2053
+ configDigest: 'cleanup-offline',
2054
+ compositeDigest: preview.operationPlan.planFingerprint,
2055
+ },
2056
+ operationPlan: preview.operationPlan,
2057
+ offlineCleanupPlans,
2058
+ ...(preview.removedPluginCleanupIds
2059
+ ? { removedPluginCleanupIds: preview.removedPluginCleanupIds }
2060
+ : {}),
2061
+ ...(offlineCleanupPlans.length === 1
2062
+ ? { offlineCleanup: offlineCleanupPlans[0] }
2063
+ : {}),
2064
+ targetFingerprint: preview.operationPlan.planFingerprint,
2065
+ };
572
2066
  }
573
- let dependencyPlan;
574
- await preparation?.reporter.emit('catalog');
575
- const bundleUnavailableCleanupTarget = intent.kind === 'pluginWrite'
576
- && intent.action === 'cleanup'
577
- && intent.originIssue?.category === 'sourceUnavailable'
578
- && intent.originIssue.actionId === 'cleanup'
579
- ? status.payload.targets.find((target) => target.pluginId === intent.pluginId
580
- && target.platform === intent.platform
581
- && target.scope === intent.scope
582
- && target.errors.some((error) => error.code === 'bundleUnavailable'))
583
- : undefined;
584
- const catalogSource = bundleUnavailableCleanupTarget
585
- ? status.payload.catalog.sources.find((source) => source.id === bundleUnavailableCleanupTarget.sourceId)
586
- : undefined;
587
- const selected = bundleUnavailableCleanupTarget
588
- ? {
589
- pluginName: bundleUnavailableCleanupTarget.pluginName,
590
- source: {
591
- sourceId: bundleUnavailableCleanupTarget.sourceId,
592
- sourceIdentity: bundleUnavailableCleanupTarget.sourceIdentity,
593
- ...(catalogSource?.requestedRef ? { requestedRef: catalogSource.requestedRef } : {}),
594
- ...(catalogSource?.resolvedCommit ? { resolvedCommit: catalogSource.resolvedCommit } : {}),
2067
+ const packageInfo = await readCliPackageInfo();
2068
+ let catalogOperation;
2069
+ let catalogOperationError;
2070
+ if (intent.kind !== 'portableInstall') {
2071
+ try {
2072
+ const sourceIds = [parsePluginId(intent.pluginId).sourceId];
2073
+ catalogOperation = !freshCatalog && sessionCatalogOperationContext
2074
+ ? selectCatalogOperationContext(sessionCatalogOperationContext, sourceIds)
2075
+ : await resolveCatalogOperationContext({
2076
+ sourceIds,
2077
+ env: context.env,
2078
+ ...(!freshCatalog ? { validatedStoreEntries, validatingStoreEntries } : {}),
2079
+ ...(signal ? { signal } : {}),
2080
+ });
2081
+ }
2082
+ catch (error) {
2083
+ catalogOperationError = error;
2084
+ }
2085
+ }
2086
+ if (intent.kind === 'portableInstall') {
2087
+ await preparation?.reporter.emit('portable');
2088
+ if (intent.retry
2089
+ && (intent.retry.receipt.plugin.id !== intent.pluginId
2090
+ || intent.retry.receipt.scope !== intent.scope)) {
2091
+ throw operationReviewStale('Portable 重试结果与当前插件或范围不匹配。');
2092
+ }
2093
+ const portableSourceId = parsePluginId(intent.pluginId).sourceId;
2094
+ const portableCatalogOperation = sessionCatalogOperationContext
2095
+ ? selectCatalogOperationContext(sessionCatalogOperationContext, [portableSourceId])
2096
+ : undefined;
2097
+ const portable = intent.retry
2098
+ ? await preparePortableRetry({
2099
+ receipt: intent.retry.receipt,
2100
+ cwd: context.cwd,
2101
+ env: context.env,
2102
+ cliVersion: packageInfo.version,
2103
+ signal,
2104
+ ...(portableCatalogOperation
2105
+ ? { catalogOperationContext: portableCatalogOperation }
2106
+ : {}),
2107
+ })
2108
+ : await preparePortableInstall({
2109
+ selector: intent.pluginId,
2110
+ scope: intent.scope,
2111
+ ...(intent.selectedSkills.length > 0 ? { requestedSkills: intent.selectedSkills } : {}),
2112
+ ...(intent.requestedAgents ? { requestedAgents: intent.requestedAgents } : {}),
2113
+ cwd: context.cwd,
2114
+ env: context.env,
2115
+ cliVersion: packageInfo.version,
2116
+ signal,
2117
+ ...(portableCatalogOperation
2118
+ ? { catalogOperationContext: portableCatalogOperation }
2119
+ : {}),
2120
+ });
2121
+ signal?.throwIfAborted();
2122
+ await preparation?.reporter.emit('evidence');
2123
+ signal?.throwIfAborted();
2124
+ const evidence = {
2125
+ catalog: {
2126
+ configDigest: portable.configDigest,
2127
+ compositeDigest: portable.evidenceDigest,
2128
+ ...(portable.generationSetDigest
2129
+ ? { generationSetDigest: portable.generationSetDigest }
2130
+ : {}),
595
2131
  },
2132
+ portable,
2133
+ };
2134
+ if (catalogOperation)
2135
+ catalogOperationByEvidence.set(evidence, catalogOperation);
2136
+ return evidence;
2137
+ }
2138
+ if (preparation && intent.kind === 'configSave') {
2139
+ if (!sessionStatusResult || !catalogOperation) {
2140
+ throw operationReviewStale('当前 TUI session 没有可复用的设置证据。');
596
2141
  }
597
- : await withResolvedCatalogPlugin({
2142
+ await preparation.reporter.emit('config');
2143
+ const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
2144
+ signal?.throwIfAborted();
2145
+ if (configSnapshot.digest !== sessionStatusResult.payload.catalog.configDigest) {
2146
+ throw operationReviewStale('准备设置确认期间来源配置发生变化。');
2147
+ }
2148
+ const relevantTargets = configAffectedStatusTargets(sessionStatusResult.payload.targets, intent.pluginId, intent.scope);
2149
+ const preparedCatalogOperation = catalogOperation;
2150
+ const selected = await withResolvedCatalogPlugin({
598
2151
  selector: intent.pluginId,
599
2152
  cliVersion: packageInfo.version,
600
- freshnessPolicy,
601
2153
  configSnapshot,
602
2154
  env: context.env,
2155
+ operationContext: preparedCatalogOperation,
603
2156
  ...(signal ? { signal } : {}),
604
2157
  consume: async ({ plugin, source }) => {
605
- if (preparation?.validateIntent && intent.kind === 'configSave') {
606
- await preparation.reporter.emit('config');
607
- if (!plugin.manifest.config) {
608
- throw new TuiOperationBlockedError('configSchemaUnavailable', '这个插件没有设置 schema。');
609
- }
2158
+ if (!plugin.manifest.config) {
2159
+ throw new TuiOperationBlockedError('configSchemaUnavailable', '这个插件没有设置 schema。');
2160
+ }
2161
+ if (intent.kind === 'configSave') {
610
2162
  await coercePluginConfigUpdates({
611
- schema: plugin.manifest.config.schema,
2163
+ schema: plugin.manifest.config.fields,
612
2164
  updates: intent.updates ?? {},
613
2165
  cwd: context.cwd,
614
2166
  });
615
2167
  for (const key of intent.removeKeys ?? []) {
616
- if (!plugin.manifest.config.schema[key]) {
2168
+ if (!plugin.manifest.config.fields[key]) {
617
2169
  throw new TuiOperationBlockedError('unknownConfigKey', '设置字段已变化,请重新检查。');
618
2170
  }
619
2171
  }
620
2172
  }
2173
+ const impact = intent.retryApply ? undefined : await analyzePluginConfigMutationImpact({
2174
+ plugin,
2175
+ scope: intent.scope,
2176
+ cwd: context.cwd,
2177
+ env: context.env,
2178
+ visibleTargets: configImpactVisibleTargets(plugin.manifest.name, relevantTargets),
2179
+ mutation: {
2180
+ kind: 'patch',
2181
+ updates: intent.updates,
2182
+ removeKeys: intent.removeKeys,
2183
+ },
2184
+ });
2185
+ const affectedTargets = intent.retryApply
2186
+ ? configImpactVisibleTargets(plugin.manifest.name, relevantTargets)
2187
+ : impact.affectedTargets;
621
2188
  return {
622
2189
  pluginName: plugin.manifest.name,
623
2190
  source: sourceEvidenceForPlugin(source, plugin.manifest.name),
2191
+ impact,
2192
+ preApplyAuthorization: configIntentRequestsApply(intent)
2193
+ ? (await resolveConfigPreApplyAuthorization({
2194
+ plugin,
2195
+ scope: intent.scope,
2196
+ cwd: context.cwd,
2197
+ env: context.env,
2198
+ applyTargets: affectedTargets,
2199
+ nativeCliRunner: context.nativeCliRunner,
2200
+ rebuildRuntime: {
2201
+ catalogOperationContext: preparedCatalogOperation,
2202
+ expectedGenerationSetDigest: preparedCatalogOperation.generationSetDigest,
2203
+ inventoryCatalogSourceReferences: operationInventory,
2204
+ },
2205
+ })).targets
2206
+ : [],
2207
+ sharedNativeRenderTargets: intent.retryApply
2208
+ ? configRetrySharedNativeRenderTargets(plugin)
2209
+ : impact.sharedNativeRenderTargets,
624
2210
  };
625
2211
  },
626
2212
  });
2213
+ const retryStore = intent.retryApply ? await readPluginConfigStore({
2214
+ sourceId: selected.source.sourceId,
2215
+ sourceIdentity: selected.source.sourceIdentity,
2216
+ logicalIdentity: selected.source.logicalIdentity,
2217
+ pluginName: selected.pluginName,
2218
+ scope: intent.scope,
2219
+ cwd: context.cwd,
2220
+ env: context.env,
2221
+ }) : undefined;
2222
+ signal?.throwIfAborted();
2223
+ const relevantRevisions = (sessionStatusResult.targetRevisions ?? []).filter((revision) => relevantTargets.some((target) => target.platform === revision.platform
2224
+ && target.scope === revision.scope
2225
+ && target.pluginId === revision.pluginId));
2226
+ await preparation.reporter.emit('evidence');
2227
+ const evidence = {
2228
+ catalog: {
2229
+ configDigest: sessionStatusResult.payload.catalog.configDigest,
2230
+ compositeDigest: sessionStatusResult.payload.catalog.compositeDigest,
2231
+ ...(catalogOperation ? { generationSetDigest: catalogOperation.generationSetDigest } : {}),
2232
+ },
2233
+ ...(selected.source ? { source: selected.source } : {}),
2234
+ targetFingerprint: statusTargetFingerprint(relevantTargets, relevantRevisions),
2235
+ config: {
2236
+ scope: intent.scope,
2237
+ ...(selected.source?.manifestDigest ? { schemaIdentity: selected.source.manifestDigest } : {}),
2238
+ storeDigest: selected.impact?.storeDigest
2239
+ ?? digest({ exists: retryStore?.exists, rawDigest: retryStore?.rawDigest }),
2240
+ impactState: selected.impact?.impactState ?? 'known',
2241
+ ...(selected.impact?.noEffectiveChange ? { noEffectiveChange: true } : {}),
2242
+ affectedTargets: (selected.impact?.affectedTargets
2243
+ ?? relevantTargets).map(({ platform, scope }) => ({ platform, scope })),
2244
+ ...(selected.preApplyAuthorization.length > 0
2245
+ ? { preApplyAuthorization: selected.preApplyAuthorization }
2246
+ : {}),
2247
+ ...(selected.sharedNativeRenderTargets.length > 0
2248
+ ? { sharedNativeRenderTargets: selected.sharedNativeRenderTargets }
2249
+ : {}),
2250
+ },
2251
+ };
2252
+ if (catalogOperation)
2253
+ catalogOperationByEvidence.set(evidence, catalogOperation);
2254
+ return evidence;
2255
+ }
2256
+ await preparation?.reporter.emit('status');
2257
+ const statusCatalogOperation = catalogOperation
2258
+ ?? (!freshCatalog ? sessionCatalogOperationContext : undefined);
2259
+ const status = await statusCommandV8({
2260
+ plugins: [...new Set([intent.pluginId,
2261
+ ...(intent.kind === 'pluginWrite' && intent.originIssue?.consumerPluginId
2262
+ ? [intent.originIssue.consumerPluginId] : [])])],
2263
+ ...('platform' in intent ? { platform: intent.platform } : {}),
2264
+ ...(intent.kind !== 'pluginWrite' && intent.scope === 'project'
2265
+ ? { scope: intent.scope }
2266
+ : {}),
2267
+ cwd: context.cwd,
2268
+ env: context.env,
2269
+ nativeCliRunner: context.nativeCliRunner,
2270
+ ...(codexObservationSession ? { codexObservationSession } : {}),
2271
+ cliVersion: packageInfo.version,
2272
+ ...(statusCatalogOperation ? { catalogOperationContext: statusCatalogOperation } : {}),
2273
+ inventoryCatalogSourceReferences: operationInventory,
2274
+ });
2275
+ signal?.throwIfAborted();
2276
+ const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
2277
+ signal?.throwIfAborted();
2278
+ if (configSnapshot.digest !== status.payload.catalog.configDigest) {
2279
+ throw operationReviewStale('准备确认信息期间来源配置发生变化。');
2280
+ }
2281
+ if (catalogOperationError)
2282
+ throw catalogOperationError;
2283
+ if (!catalogOperation) {
2284
+ catalogOperation = await resolveCatalogOperationContext({
2285
+ sourceIds: [parsePluginId(intent.pluginId).sourceId],
2286
+ configSnapshot,
2287
+ env: context.env,
2288
+ ...(!freshCatalog ? { validatedStoreEntries, validatingStoreEntries } : {}),
2289
+ ...(signal ? { signal } : {}),
2290
+ });
2291
+ }
2292
+ let dependencyPlan;
2293
+ let codexReconcilePlans;
2294
+ let operationPlan;
2295
+ await preparation?.reporter.emit('catalog');
2296
+ const configVisibleTargets = intent.kind === 'configSave'
2297
+ ? configAffectedStatusTargets(status.payload.targets, intent.pluginId, intent.scope)
2298
+ : [];
2299
+ const selected = await withResolvedCatalogPlugin({
2300
+ selector: intent.pluginId,
2301
+ cliVersion: packageInfo.version,
2302
+ configSnapshot,
2303
+ env: context.env,
2304
+ ...(catalogOperation ? { operationContext: catalogOperation } : {}),
2305
+ ...(signal ? { signal } : {}),
2306
+ consume: async ({ plugin, source }) => {
2307
+ if (preparation?.validateIntent && intent.kind === 'configSave') {
2308
+ await preparation.reporter.emit('config');
2309
+ if (!plugin.manifest.config) {
2310
+ throw new TuiOperationBlockedError('configSchemaUnavailable', '这个插件没有设置 schema。');
2311
+ }
2312
+ await coercePluginConfigUpdates({
2313
+ schema: plugin.manifest.config.fields,
2314
+ updates: intent.updates ?? {},
2315
+ cwd: context.cwd,
2316
+ });
2317
+ for (const key of intent.removeKeys ?? []) {
2318
+ if (!plugin.manifest.config.fields[key]) {
2319
+ throw new TuiOperationBlockedError('unknownConfigKey', '设置字段已变化,请重新检查。');
2320
+ }
2321
+ }
2322
+ }
2323
+ const impact = intent.kind === 'configSave' && !intent.retryApply
2324
+ ? await analyzePluginConfigMutationImpact({
2325
+ plugin,
2326
+ scope: intent.scope,
2327
+ cwd: context.cwd,
2328
+ env: context.env,
2329
+ visibleTargets: configImpactVisibleTargets(plugin.manifest.name, configVisibleTargets),
2330
+ mutation: {
2331
+ kind: 'patch',
2332
+ updates: intent.updates,
2333
+ removeKeys: intent.removeKeys,
2334
+ },
2335
+ })
2336
+ : undefined;
2337
+ return {
2338
+ pluginName: plugin.manifest.name,
2339
+ source: sourceEvidenceForPlugin(source, plugin.manifest.name),
2340
+ impact,
2341
+ preApplyAuthorization: intent.kind === 'configSave'
2342
+ && configIntentRequestsApply(intent)
2343
+ ? (await resolveConfigPreApplyAuthorization({
2344
+ plugin,
2345
+ scope: intent.scope,
2346
+ cwd: context.cwd,
2347
+ env: context.env,
2348
+ applyTargets: intent.retryApply
2349
+ ? configImpactVisibleTargets(plugin.manifest.name, configVisibleTargets)
2350
+ : impact.affectedTargets,
2351
+ nativeCliRunner: context.nativeCliRunner,
2352
+ rebuildRuntime: {
2353
+ ...(catalogOperation ? { catalogOperationContext: catalogOperation } : {}),
2354
+ ...(catalogOperation?.generationSetDigest
2355
+ ? { expectedGenerationSetDigest: catalogOperation.generationSetDigest }
2356
+ : {}),
2357
+ inventoryCatalogSourceReferences: operationInventory,
2358
+ },
2359
+ })).targets
2360
+ : [],
2361
+ sharedNativeRenderTargets: intent.kind === 'configSave'
2362
+ ? intent.retryApply
2363
+ ? configRetrySharedNativeRenderTargets(plugin)
2364
+ : impact.sharedNativeRenderTargets
2365
+ : [],
2366
+ };
2367
+ },
2368
+ });
2369
+ if (intent.kind === 'pluginWrite' && intent.expectedUpdate) {
2370
+ const revision = status.targetRevisions.find((entry) => entry.pluginId === intent.pluginId && entry.platform === intent.platform
2371
+ && entry.scope === intent.scope);
2372
+ const generation = catalogOperation?.generationSet.sources.find((entry) => entry.sourceId === selected.source.sourceId);
2373
+ if (intent.action !== 'sync'
2374
+ || !('pluginVersion' in selected.source)
2375
+ || selected.source.sourceIdentity !== intent.expectedUpdate.sourceIdentity
2376
+ || selected.source.pluginVersion !== intent.expectedUpdate.activeVersion
2377
+ || generation?.storeKey !== intent.expectedUpdate.generationKey
2378
+ || revision?.revision.installedVersion !== intent.expectedUpdate.installedVersion
2379
+ || revision?.revision.availableVersion !== intent.expectedUpdate.activeVersion
2380
+ || revision.revision.state !== 'upgradeAvailable') {
2381
+ throw operationReviewStale('插件更新版本或 Active Catalog 证据已变化。');
2382
+ }
2383
+ }
2384
+ let codexManager;
2385
+ let resolvedCodexManager;
627
2386
  const hasDeclaredDependencies = (status.pluginSummaries.find((summary) => summary.pluginId === intent.pluginId)?.dependencyTotalCount ?? 0) > 0;
628
- if (intent.kind === 'pluginWrite'
629
- && hasDeclaredDependencies
630
- && (intent.action === 'install' || intent.action === 'upgrade' || intent.action === 'reinstall')) {
2387
+ if (intent.kind === 'pluginWrite') {
631
2388
  const command = {
632
2389
  install: installWriteCommand,
633
- upgrade: upgradeWriteCommand,
2390
+ refreshAndSync: syncWriteCommand,
634
2391
  reinstall: reinstallWriteCommand,
2392
+ uninstall: uninstallWriteCommand,
2393
+ cleanup: cleanupWriteCommand,
2394
+ sync: syncWriteCommand,
635
2395
  }[intent.action];
636
2396
  const preview = await command([intent.pluginId], {
637
2397
  cwd: context.cwd,
638
2398
  platform: intent.platform,
639
2399
  scope: intent.scope,
2400
+ ...(reviewedScriptTimeoutMs(intent) === undefined
2401
+ ? {}
2402
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
2403
+ ...(intent.action === 'install' && intent.dependencyScopeOverrides
2404
+ ? { dependencyScopeOverrides: intent.dependencyScopeOverrides }
2405
+ : {}),
2406
+ ...(intent.action === 'cleanup' ? { force: intent.force === true } : {}),
640
2407
  }, {
641
2408
  ...context,
2409
+ inventoryCatalogSourceReferences: operationInventory,
642
2410
  expectedCatalogConfigDigest: status.payload.catalog.configDigest,
643
- previewDependencyPlanOnly: true,
2411
+ ...(catalogOperation ? {
2412
+ catalogOperationContext: catalogOperation,
2413
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
2414
+ } : {}),
2415
+ previewOperationPlan: true,
2416
+ ...(codexObservationSession ? { codexObservationSession } : {}),
644
2417
  ...(preparation ? {
645
- previewCatalogFreshnessPolicy: 'cacheFirst',
646
2418
  onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
647
2419
  } : {}),
648
2420
  });
649
2421
  signal?.throwIfAborted();
650
- dependencyPlan = preview.dependencyPlan;
651
- if (!dependencyPlan) {
652
- throw new TuiOperationBlockedError('dependencyGraphInvalid', '无法建立可信的依赖安装计划。');
2422
+ codexReconcilePlans = preview?.codexReconcilePlans;
2423
+ operationPlan = preview?.operationPlan;
2424
+ if (hasDeclaredDependencies
2425
+ && (intent.action === 'install'
2426
+ || intent.action === 'refreshAndSync'
2427
+ || intent.action === 'reinstall'
2428
+ || intent.action === 'sync')) {
2429
+ dependencyPlan = preview.dependencyPlan;
2430
+ if (!dependencyPlan) {
2431
+ throw new TuiOperationBlockedError('dependencyGraphInvalid', '无法建立可信的依赖安装计划。');
2432
+ }
2433
+ const blocker = dependencyPlan.targets.find((target) => target.action === 'block');
2434
+ if (blocker) {
2435
+ const code = blocker.errorCode ?? 'dependencyNotReady';
2436
+ const reason = code === 'dependencyScopeConflict' || code === 'codexExtensionScopeConflict'
2437
+ ? '这个依赖的安装位置存在冲突,需要先选择保留的位置。'
2438
+ : code === 'artifactCollision' || code === 'exclusiveOwnerEvidenceInvalid'
2439
+ ? '这个依赖的安装位置被其他内容占用,需要先检查并处理冲突。'
2440
+ : '这个依赖尚不可用,需要先检查并恢复后再安装。';
2441
+ throw new TuiOperationBlockedError(code, `${blocker.pluginId}:${reason}`, [{
2442
+ id: `dependency:${blocker.pluginId}:${blocker.platform}:${blocker.resolvedScope}`,
2443
+ code, stage: 'dependencyPlan', safeMessage: reason,
2444
+ target: { pluginId: blocker.pluginId, platform: blocker.platform, scope: blocker.resolvedScope },
2445
+ causes: [], detailLines: [],
2446
+ }]);
2447
+ }
653
2448
  }
654
- const blocker = dependencyPlan.targets.find((target) => target.action === 'block');
655
- if (blocker) {
656
- throw new TuiOperationBlockedError(blocker.errorCode ?? 'dependencyNotReady', `${blocker.errorCode ?? 'dependencyNotReady'}: 依赖安装计划被阻止。`);
2449
+ const managerPreview = preview?.codexManagerPreview;
2450
+ if (managerPreview?.state === 'blocked') {
2451
+ const message = managerPreview.code === 'codexSystemCliUnsupported'
2452
+ ? '系统 Codex CLI 版本不受支持。请升级官方 Codex CLI 后重试。'
2453
+ : 'AW-managed Codex runtime 当前不可用。请重试、升级 AW,或安装官方 Codex CLI。';
2454
+ throw new TuiOperationBlockedError(managerPreview.code, `${managerPreview.code}: ${message}`);
2455
+ }
2456
+ if (managerPreview) {
2457
+ if (managerPreview.state === 'system' || managerPreview.state === 'cached') {
2458
+ resolvedCodexManager = managerPreview.manager;
2459
+ }
2460
+ codexManager = {
2461
+ state: managerPreview.state,
2462
+ packageName: MANAGED_CODEX_PACKAGE_NAME,
2463
+ version: managerPreview.state === 'requires-install'
2464
+ ? MANAGED_CODEX_VERSION
2465
+ : managerPreview.manager.version,
2466
+ };
657
2467
  }
658
2468
  }
659
2469
  await preparation?.reporter.emit('evidence');
660
2470
  signal?.throwIfAborted();
2471
+ const evidenceTargets = intent.kind === 'configSave'
2472
+ ? configVisibleTargets
2473
+ : flattenStatusTargets(status.payload.targets);
2474
+ const evidenceRevisions = intent.kind === 'configSave'
2475
+ ? (status.targetRevisions ?? []).filter((revision) => evidenceTargets.some((target) => target.platform === revision.platform
2476
+ && target.scope === revision.scope
2477
+ && target.pluginId === revision.pluginId))
2478
+ : (status.targetRevisions ?? []);
661
2479
  const evidence = {
662
2480
  catalog: {
663
2481
  configDigest: status.payload.catalog.configDigest,
664
2482
  compositeDigest: status.payload.catalog.compositeDigest,
2483
+ ...(catalogOperation
2484
+ ? { generationSetDigest: catalogOperation.generationSetDigest }
2485
+ : {}),
665
2486
  },
666
2487
  source: selected.source,
667
2488
  ...(dependencyPlan ? { dependencyPlan } : {}),
668
- targetFingerprint: statusTargetFingerprint(status.payload.targets, status.targetRevisions ?? []),
2489
+ ...(codexReconcilePlans ? { codexReconcilePlans } : {}),
2490
+ ...(operationPlan ? { operationPlan } : {}),
2491
+ ...(codexManager ? { codexManager } : {}),
2492
+ targetFingerprint: statusTargetFingerprint(evidenceTargets, evidenceRevisions),
669
2493
  };
670
- if (intent.kind === 'configSave' || intent.kind === 'configReset') {
671
- const store = await readPluginConfigStore({
2494
+ if (intent.kind === 'configSave') {
2495
+ const impact = 'impact' in selected ? selected.impact : undefined;
2496
+ const retryStore = intent.retryApply ? await readPluginConfigStore({
672
2497
  sourceId: selected.source.sourceId,
673
2498
  sourceIdentity: selected.source.sourceIdentity,
2499
+ ...('logicalIdentity' in selected.source
2500
+ ? { logicalIdentity: selected.source.logicalIdentity }
2501
+ : {}),
674
2502
  pluginName: selected.pluginName,
675
2503
  scope: intent.scope,
676
- projectLayer: intent.projectLayer,
677
2504
  cwd: context.cwd,
678
2505
  env: context.env,
679
- });
2506
+ }) : undefined;
680
2507
  signal?.throwIfAborted();
681
2508
  evidence.config = {
682
2509
  scope: intent.scope,
683
- projectLayer: intent.projectLayer,
684
- schemaIdentity: selected.source.manifestDigest,
685
- storeDigest: digest({ exists: store.exists, values: store.values }),
2510
+ ...('manifestDigest' in selected.source
2511
+ ? { schemaIdentity: selected.source.manifestDigest }
2512
+ : {}),
2513
+ storeDigest: impact?.storeDigest
2514
+ ?? digest({ exists: retryStore?.exists, rawDigest: retryStore?.rawDigest }),
2515
+ impactState: impact?.impactState ?? 'known',
2516
+ ...(impact?.noEffectiveChange ? { noEffectiveChange: true } : {}),
2517
+ affectedTargets: (impact?.affectedTargets
2518
+ ?? evidenceTargets).map(({ platform, scope }) => ({ platform, scope })),
2519
+ ...(intent.scope === 'project' ? {
2520
+ shareability: await inspectConfigShareability({
2521
+ sourceId: selected.source.sourceId,
2522
+ pluginName: selected.pluginName,
2523
+ scope: intent.scope,
2524
+ cwd: context.cwd,
2525
+ env: context.env,
2526
+ }),
2527
+ } : {}),
2528
+ ...('sharedNativeRenderTargets' in selected
2529
+ && selected.sharedNativeRenderTargets.length > 0
2530
+ ? { sharedNativeRenderTargets: selected.sharedNativeRenderTargets }
2531
+ : {}),
2532
+ ...('preApplyAuthorization' in selected
2533
+ && selected.preApplyAuthorization.length > 0
2534
+ ? { preApplyAuthorization: selected.preApplyAuthorization }
2535
+ : {}),
686
2536
  };
687
2537
  }
2538
+ if (catalogOperation)
2539
+ catalogOperationByEvidence.set(evidence, catalogOperation);
2540
+ if (resolvedCodexManager)
2541
+ codexManagerByEvidence.set(evidence, resolvedCodexManager);
688
2542
  return evidence;
689
2543
  };
690
2544
  const dependencies = {
2545
+ beginCatalogSession(sessionOptions) {
2546
+ sessionCatalogOperationContext = undefined;
2547
+ if (sessionOptions?.reuseValidatedGenerations !== true) {
2548
+ validatedStoreEntries.clear();
2549
+ validatingStoreEntries.clear();
2550
+ }
2551
+ },
691
2552
  loadStatus,
692
- async inspectSourceRemoval(_sourceId, signal) {
2553
+ loadPluginContent,
2554
+ async probeCatalogFreshness(sourceId, freshnessOptions, signal) {
693
2555
  signal.throwIfAborted();
2556
+ const packageInfo = await readCliPackageInfo();
2557
+ return probeCatalogSourceFreshness({
2558
+ sourceId,
2559
+ cliVersion: packageInfo.version,
2560
+ env: context.env,
2561
+ signal,
2562
+ fullValidation: freshnessOptions.fullValidation,
2563
+ runtime: {
2564
+ ...options.catalogGenerationRuntime,
2565
+ validatedStoreEntries,
2566
+ validatingStoreEntries,
2567
+ },
2568
+ });
2569
+ },
2570
+ async inspectSourceRemoval(sourceId, signal) {
2571
+ signal.throwIfAborted();
2572
+ const config = await readCatalogSourcesConfigSnapshot(context.env);
2573
+ const source = config.config.sources.find((entry) => entry.id === sourceId);
2574
+ if (!source)
2575
+ throw new TuiOperationBlockedError('catalogGenerationSourceMissing', '来源不存在。');
2576
+ const impact = await inspectCatalogSourceImpact({
2577
+ sourceId,
2578
+ cwd: context.cwd,
2579
+ env: context.env,
2580
+ sourcePrimary: source.primary,
2581
+ mode: 'remove',
2582
+ });
2583
+ const counts = (category) => impact.affectedReferences.filter((reference) => reference.category === category).length;
694
2584
  return {
695
- blocked: false,
696
- inventoryComplete: true,
697
- installationCount: 0,
698
- configCount: 0,
699
- lifecycleCount: 0,
700
- marketplaceCount: 0,
701
- dependencies: [],
2585
+ blocked: impact.affectedReferences.length > 0 || !impact.inventoryComplete,
2586
+ inventoryComplete: impact.inventoryComplete,
2587
+ installationCount: counts('installation'),
2588
+ configCount: counts('config'),
2589
+ lifecycleCount: counts('lifecycle'),
2590
+ marketplaceCount: counts('marketplace'),
2591
+ dependencies: impact.affectedReferences,
702
2592
  };
703
2593
  },
704
- async prepareReviewEvidence(intent, signal, onStage) {
2594
+ async prepareReviewEvidence(intent, signal, onStage, reusableCandidate) {
705
2595
  const reporter = createReviewStageReporter({
706
2596
  context,
707
2597
  onStage,
708
2598
  now: options.now ?? Date.now,
709
2599
  });
710
2600
  try {
711
- return await captureEvidence(intent, signal, 'cacheFirst', {
712
- reporter,
713
- validateIntent: true,
714
- });
2601
+ const preparationWarnings = [];
2602
+ if (intent.kind === 'pluginWrite' && intent.action === 'refreshAndSync') {
2603
+ await reporter.emit('refresh');
2604
+ const packageInfo = await readCliPackageInfo();
2605
+ const sourceId = parsePluginId(intent.pluginId).sourceId;
2606
+ await syncCatalogSourceGeneration({
2607
+ sourceId,
2608
+ cliVersion: packageInfo.version,
2609
+ cwd: context.cwd,
2610
+ env: context.env,
2611
+ signal,
2612
+ }).catch((error) => {
2613
+ preparationWarnings.push(`catalogSourceRefreshFailed: ${sourceId}: ${error instanceof Error ? error.message : String(error)}`);
2614
+ });
2615
+ sessionCatalogOperationContext = undefined;
2616
+ sessionStatusResult = undefined;
2617
+ validatedStoreEntries.clear();
2618
+ validatingStoreEntries.clear();
2619
+ }
2620
+ const taskObservationSession = intent.kind === 'pluginWriteTask'
2621
+ && intent.targets.some((target) => target.platform === 'codex')
2622
+ ? await createCodexNativeObservationSession({
2623
+ exactCwd: context.cwd,
2624
+ env: context.env,
2625
+ runner: context.nativeCliRunner,
2626
+ })
2627
+ : undefined;
2628
+ let evidence;
2629
+ try {
2630
+ evidence = await captureEvidence(intent, signal, {
2631
+ reporter,
2632
+ validateIntent: true,
2633
+ }, createOperationInventoryReader(), intent.kind === 'pluginWrite'
2634
+ && intent.action === 'refreshAndSync', reusableCandidate, taskObservationSession);
2635
+ }
2636
+ finally {
2637
+ await taskObservationSession?.dispose();
2638
+ }
2639
+ if (preparationWarnings.length > 0)
2640
+ evidence.preparationWarnings = preparationWarnings;
2641
+ return evidence;
715
2642
  }
716
2643
  finally {
717
2644
  await reporter.finish();
718
2645
  }
719
2646
  },
2647
+ async previewDependencyPlacement(intent, signal) {
2648
+ const evidence = await captureEvidence(intent, signal);
2649
+ return evidence.dependencyPlan;
2650
+ },
720
2651
  async validateIntent(intent, signal) {
2652
+ if (intent.kind === 'catalogSourcesRecovery') {
2653
+ signal.throwIfAborted();
2654
+ return;
2655
+ }
2656
+ if (intent.kind === 'catalogGeneration') {
2657
+ signal.throwIfAborted();
2658
+ return;
2659
+ }
721
2660
  if (intent.kind === 'sourceMutation') {
722
2661
  const evidence = await captureEvidence(intent, signal);
723
- if (evidence.sourceWrite?.configDigest === evidence.sourceWrite?.candidateDigest) {
2662
+ if (evidence.sourceWrite?.configDigest === evidence.sourceWrite?.proposedConfigDigest) {
724
2663
  throw new TuiOperationBlockedError('sourceNoChanges', '来源没有变化。');
725
2664
  }
726
2665
  return;
727
2666
  }
2667
+ if (intent.kind === 'configSaveBatch') {
2668
+ assertConfigSaveBatchIntent(intent);
2669
+ for (const group of intent.groups) {
2670
+ await dependencies.validateIntent(configSaveBatchGroupIntent(intent, group), signal);
2671
+ }
2672
+ return;
2673
+ }
728
2674
  if (intent.kind === 'configSave') {
729
2675
  const packageInfo = await readCliPackageInfo();
730
2676
  await withResolvedCatalogPlugin({
731
2677
  selector: intent.pluginId,
732
2678
  cliVersion: packageInfo.version,
733
- freshnessPolicy: 'cacheFirst',
734
2679
  env: context.env,
735
2680
  signal,
2681
+ ...(sessionContextForPlugin(intent.pluginId)
2682
+ ? { operationContext: sessionContextForPlugin(intent.pluginId) }
2683
+ : {}),
736
2684
  consume: async ({ plugin }) => {
737
2685
  if (!plugin.manifest.config) {
738
2686
  throw new TuiOperationBlockedError('configSchemaUnavailable', '这个插件没有设置 schema。');
739
2687
  }
740
2688
  await coercePluginConfigUpdates({
741
- schema: plugin.manifest.config.schema,
2689
+ schema: plugin.manifest.config.fields,
742
2690
  updates: intent.updates ?? {},
743
2691
  cwd: context.cwd,
744
2692
  });
745
2693
  for (const key of intent.removeKeys ?? []) {
746
- if (!plugin.manifest.config.schema[key]) {
2694
+ if (!plugin.manifest.config.fields[key]) {
747
2695
  throw new TuiOperationBlockedError('unknownConfigKey', '设置字段已变化,请重新检查。');
748
2696
  }
749
2697
  }
@@ -754,41 +2702,24 @@ export function createTuiControllerDependencies(context, progressFeed, options =
754
2702
  if (intent.kind !== 'pluginWrite')
755
2703
  return;
756
2704
  signal.throwIfAborted();
757
- const packageInfo = await readCliPackageInfo();
758
- const status = await statusCommandV4({
759
- plugins: [...new Set([intent.pluginId,
760
- ...(intent.originIssue?.consumerPluginId ? [intent.originIssue.consumerPluginId] : [])])],
761
- platform: intent.platform,
762
- cwd: context.cwd,
763
- env: context.env,
764
- cliVersion: packageInfo.version,
765
- });
2705
+ const parsed = parsePluginId(intent.pluginId);
2706
+ if (!parsed.sourceId || !parsed.pluginName) {
2707
+ throw new TuiOperationBlockedError('invalidPluginId', '操作必须使用 canonical pluginId。');
2708
+ }
2709
+ },
2710
+ captureEvidence,
2711
+ inspectCatalogStore(signal) {
766
2712
  signal.throwIfAborted();
767
- await assertTypedIssueOrigin(intent, status.payload.targets);
768
- assertIntentIsLegal(intent, status.payload.targets, status.targetRevisions);
2713
+ return inspectCatalogStoreTask({ env: context.env });
769
2714
  },
770
- async checkSource(sourceId, signal) {
771
- const packageInfo = await readCliPackageInfo();
772
- const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
773
- return withResolvedMultiSourceCatalog({
774
- cliVersion: packageInfo.version,
775
- freshnessPolicy: 'forceRemote',
776
- sourceIds: [sourceId],
777
- configSnapshot,
2715
+ recheckPortableInstall(receipt, signal) {
2716
+ return recheckPortableInstall({
2717
+ receipt,
2718
+ cwd: context.cwd,
778
2719
  env: context.env,
779
2720
  signal,
780
- consume: (catalog) => {
781
- const item = catalog.sources[0];
782
- if (item.error)
783
- throw item.error;
784
- return {
785
- sourceIdentity: item.resolution.sourceIdentity,
786
- requestedRef: item.resolution.requestedRef,
787
- };
788
- },
789
2721
  });
790
2722
  },
791
- captureEvidence,
792
2723
  logError(_error, code, logContext) {
793
2724
  return context.debugLogger.log({
794
2725
  type: 'tui:controller-error',
@@ -805,9 +2736,11 @@ export function createTuiControllerDependencies(context, progressFeed, options =
805
2736
  return withResolvedCatalogPlugin({
806
2737
  selector: pluginId,
807
2738
  cliVersion: packageInfo.version,
808
- freshnessPolicy: 'cacheFirst',
809
2739
  env: context.env,
810
2740
  signal,
2741
+ ...(sessionContextForPlugin(pluginId)
2742
+ ? { operationContext: sessionContextForPlugin(pluginId) }
2743
+ : {}),
811
2744
  consume: async ({ plugin }) => {
812
2745
  if (!plugin.manifest.config) {
813
2746
  throw new TuiOperationBlockedError('configSchemaUnavailable', '这个插件没有设置 schema。');
@@ -816,7 +2749,6 @@ export function createTuiControllerDependencies(context, progressFeed, options =
816
2749
  pluginName: plugin.manifest.name,
817
2750
  plugin,
818
2751
  scope: layer.scope,
819
- projectLayer: layer.projectLayer,
820
2752
  cwd: context.cwd,
821
2753
  env: context.env,
822
2754
  });
@@ -824,7 +2756,6 @@ export function createTuiControllerDependencies(context, progressFeed, options =
824
2756
  pluginName: plugin.manifest.name,
825
2757
  plugin,
826
2758
  scope: 'effective',
827
- projectLayer: layer.projectLayer,
828
2759
  cwd: context.cwd,
829
2760
  env: context.env,
830
2761
  });
@@ -837,18 +2768,30 @@ export function createTuiControllerDependencies(context, progressFeed, options =
837
2768
  return withResolvedCatalogPlugin({
838
2769
  selector: pluginId,
839
2770
  cliVersion: packageInfo.version,
840
- freshnessPolicy: 'cacheFirst',
841
2771
  env: context.env,
842
2772
  signal,
843
- consume: async ({ plugin }) => {
2773
+ ...(sessionContextForPlugin(pluginId)
2774
+ ? { operationContext: sessionContextForPlugin(pluginId) }
2775
+ : {}),
2776
+ consume: async ({ plugin, source }) => {
844
2777
  if (!plugin.manifest.config) {
845
2778
  throw new TuiOperationBlockedError('configSchemaUnavailable', '这个插件没有设置 schema。');
846
2779
  }
2780
+ const stores = await readConfigStoresForTui({
2781
+ sourceId: plugin.sourceId,
2782
+ sourceIdentity: plugin.sourceIdentity,
2783
+ logicalIdentity: plugin.catalogLogicalIdentity,
2784
+ pluginName: plugin.manifest.name,
2785
+ schema: plugin.manifest.config.fields,
2786
+ scope: layer.scope,
2787
+ signal,
2788
+ });
2789
+ const userStore = stores.user;
2790
+ const projectStore = stores.project;
847
2791
  const read = await resolveConfigRead({
848
2792
  pluginName: plugin.manifest.name,
849
2793
  plugin,
850
2794
  scope: layer.scope,
851
- projectLayer: layer.projectLayer,
852
2795
  cwd: context.cwd,
853
2796
  env: context.env,
854
2797
  });
@@ -856,60 +2799,332 @@ export function createTuiControllerDependencies(context, progressFeed, options =
856
2799
  pluginName: plugin.manifest.name,
857
2800
  plugin,
858
2801
  scope: 'effective',
859
- projectLayer: layer.projectLayer,
860
2802
  cwd: context.cwd,
861
2803
  env: context.env,
862
2804
  });
863
- return buildConfigPage({ pluginId, schema: plugin.manifest.config, read, effectiveRead });
2805
+ const layerReads = layer.scope === 'effective' ? undefined : {
2806
+ user: layer.scope === 'user' ? read : await resolveConfigRead({
2807
+ pluginName: plugin.manifest.name,
2808
+ plugin,
2809
+ scope: 'user',
2810
+ cwd: context.cwd,
2811
+ env: context.env,
2812
+ }),
2813
+ ...(layer.scope === 'user' ? {} : { project: read }),
2814
+ };
2815
+ const effectiveRaw = await resolveEffectivePluginConfig({
2816
+ sourceId: plugin.sourceId,
2817
+ sourceIdentity: plugin.sourceIdentity,
2818
+ logicalIdentity: plugin.catalogLogicalIdentity,
2819
+ pluginName: plugin.manifest.name,
2820
+ manifest: plugin.manifest,
2821
+ targetScope: layer.scope === 'user' ? 'user' : 'project',
2822
+ cwd: context.cwd,
2823
+ env: context.env,
2824
+ });
2825
+ const layerStore = layer.scope === 'effective'
2826
+ ? undefined
2827
+ : layer.scope === 'user' ? userStore : projectStore;
2828
+ const page = buildConfigPage({
2829
+ pluginId,
2830
+ schema: plugin.manifest.config,
2831
+ read,
2832
+ effectiveRead,
2833
+ ...(layerReads ? { layerReads } : {}),
2834
+ ...(layerStore ? { rawValues: layerStore.values } : {}),
2835
+ rawLayerValues: {
2836
+ user: userStore.values,
2837
+ ...(projectStore ? { project: projectStore.values } : {}),
2838
+ },
2839
+ effectiveRawValues: effectiveRaw.values,
2840
+ sharedNativeRefreshKeys: new Set((plugin.manifest.artifacts?.targets ?? []).flatMap((target) => target.target === 'codex.skill' && target.delivery.mode === 'codex-native'
2841
+ ? target.refreshKeys
2842
+ : [])),
2843
+ });
2844
+ return { ...page, editEvidence: {
2845
+ sourceIdentity: source.sourceIdentity,
2846
+ schemaIdentity: source.manifestDigest,
2847
+ storeDigests: {
2848
+ user: digest({ exists: userStore.exists, rawDigest: userStore.rawDigest }),
2849
+ ...(projectStore ? { project: digest({ exists: projectStore.exists, rawDigest: projectStore.rawDigest }) } : {}),
2850
+ },
2851
+ } };
864
2852
  },
865
2853
  });
866
2854
  },
867
2855
  async getConfigResetAvailability(pluginId, layer, signal) {
2856
+ const parsed = parsePluginId(pluginId);
2857
+ if (!parsed.sourceId || !parsed.pluginName) {
2858
+ throw new TuiOperationBlockedError('invalidPluginId', '设置目标必须使用 canonical pluginId。');
2859
+ }
2860
+ let sourceIdentity;
2861
+ let logicalIdentity;
2862
+ try {
2863
+ const packageInfo = await readCliPackageInfo();
2864
+ const source = await withResolvedCatalogPlugin({
2865
+ selector: pluginId,
2866
+ cliVersion: packageInfo.version,
2867
+ env: context.env,
2868
+ signal,
2869
+ ...(sessionContextForPlugin(pluginId)
2870
+ ? { operationContext: sessionContextForPlugin(pluginId) }
2871
+ : {}),
2872
+ consume: async ({ source }) => ({
2873
+ sourceIdentity: source.sourceIdentity,
2874
+ logicalIdentity: source.logicalIdentity,
2875
+ }),
2876
+ });
2877
+ sourceIdentity = source.sourceIdentity;
2878
+ logicalIdentity = source.logicalIdentity;
2879
+ }
2880
+ catch {
2881
+ sourceIdentity = undefined;
2882
+ logicalIdentity = undefined;
2883
+ }
868
2884
  signal.throwIfAborted();
869
- const { sourceId, pluginName } = parsePluginId(pluginId);
870
- if (!sourceId)
871
- return { exists: false, sourceIdentityPresent: false };
872
- const identity = await readNamespacedPluginConfigStoreIdentity({
873
- sourceId,
874
- pluginName,
2885
+ const target = await inspectPluginConfigResetTarget({
2886
+ sourceId: parsed.sourceId,
2887
+ pluginName: parsed.pluginName,
875
2888
  scope: layer.scope,
876
- projectLayer: layer.projectLayer,
877
2889
  cwd: context.cwd,
878
2890
  env: context.env,
879
2891
  });
880
2892
  signal.throwIfAborted();
881
- if (!identity)
882
- return { exists: false, sourceIdentityPresent: false };
883
- const configured = await readCatalogSourcesConfigSnapshot(context.env);
884
- const source = configured.config.sources.find((entry) => entry.id === sourceId);
885
- const configuredIdentity = source
886
- ? await calculateCatalogSourceIdentity(source)
887
- : undefined;
888
- if (identity.sourceIdentity !== configuredIdentity) {
2893
+ if (target.kind === 'regularFile') {
2894
+ const store = await readPluginConfigStore({
2895
+ sourceId: parsed.sourceId,
2896
+ ...(sourceIdentity ? { sourceIdentity } : {}),
2897
+ ...(logicalIdentity ? { logicalIdentity } : {}),
2898
+ pluginName: parsed.pluginName,
2899
+ scope: layer.scope,
2900
+ cwd: context.cwd,
2901
+ env: context.env,
2902
+ });
2903
+ signal.throwIfAborted();
889
2904
  return {
890
- exists: true,
891
- sourceIdentityPresent: false,
892
- sourceIdentity: identity.sourceIdentity,
2905
+ exists: store.exists,
2906
+ state: store.state,
2907
+ ...(store.invalidCode ? { invalidCode: store.invalidCode } : {}),
2908
+ sourceIdentityPresent: sourceIdentity !== undefined,
2909
+ ...(sourceIdentity ? { sourceIdentity } : {}),
2910
+ targetKind: target.kind,
2911
+ resettable: target.resettable,
2912
+ storeDigest: target.fingerprint,
893
2913
  };
894
2914
  }
895
2915
  return {
896
- exists: true,
897
- sourceIdentityPresent: true,
898
- sourceIdentity: identity.sourceIdentity,
899
- storeDigest: digest({
900
- exists: identity.store.exists,
901
- sourceIdentity: identity.sourceIdentity,
902
- values: identity.store.values,
903
- }),
2916
+ exists: target.exists,
2917
+ state: target.kind === 'absent' ? 'missing' : 'invalid',
2918
+ sourceIdentityPresent: sourceIdentity !== undefined,
2919
+ ...(sourceIdentity ? { sourceIdentity } : {}),
2920
+ targetKind: target.kind,
2921
+ resettable: target.resettable,
2922
+ storeDigest: target.fingerprint,
904
2923
  };
905
2924
  },
906
- async executeIntent(intent, expectedEvidence) {
2925
+ async executeIntent(intent, expectedEvidence, signal) {
2926
+ if (intent.kind === 'catalogStorePrune') {
2927
+ signal?.throwIfAborted();
2928
+ if (!expectedEvidence.catalogStore) {
2929
+ throw new TuiOperationBlockedError('catalogStoreReviewStale', '确认信息缺少精确清理目标。');
2930
+ }
2931
+ return executeCatalogStoreTask({
2932
+ env: context.env,
2933
+ authorizedItems: expectedEvidence.catalogStore.authorizedItems,
2934
+ });
2935
+ }
2936
+ if (intent.kind === 'catalogSourcesRecovery') {
2937
+ signal?.throwIfAborted();
2938
+ return recoverInvalidCatalogSourcesConfig({ env: context.env });
2939
+ }
2940
+ if (intent.kind === 'catalogGeneration') {
2941
+ signal?.throwIfAborted();
2942
+ const expected = expectedEvidence.catalogGeneration;
2943
+ if (!expected)
2944
+ throw new TuiOperationBlockedError('catalogGenerationReviewStale', '确认信息缺少 Catalog candidate。');
2945
+ const packageInfo = await readCliPackageInfo();
2946
+ let configSaved = false;
2947
+ if (intent.sourceMutation) {
2948
+ const common = {
2949
+ cwd: context.cwd,
2950
+ env: context.env,
2951
+ cliVersion: packageInfo.version,
2952
+ expectedConfigDigest: expected.configDigest,
2953
+ };
2954
+ if (intent.sourceMutation.action === 'add') {
2955
+ await addCatalogSourceCommand({
2956
+ ...common,
2957
+ source: intent.sourceMutation.source,
2958
+ expectedProposedConfigDigest: expected.proposedConfigDigest,
2959
+ expectedImpactDigest: expected.impactDigest,
2960
+ requireImpactConfirmation: true,
2961
+ confirmImpact: true,
2962
+ });
2963
+ }
2964
+ else {
2965
+ await updateCatalogSourceCommand({
2966
+ ...common,
2967
+ sourceId: intent.sourceId,
2968
+ update: intent.sourceMutation.update,
2969
+ expectedProposedConfigDigest: expected.proposedConfigDigest,
2970
+ expectedImpactDigest: expected.impactDigest,
2971
+ requireIdentityChangeConfirmation: true,
2972
+ confirmIdentityChange: true,
2973
+ });
2974
+ }
2975
+ configSaved = true;
2976
+ await tuiControllerRuntimeTestHooks?.afterCatalogSourceConfigCommit?.();
2977
+ }
2978
+ try {
2979
+ let generation;
2980
+ if (intent.action === 'recoverState') {
2981
+ generation = await recoverCatalogGenerationStateAndActivate({
2982
+ sourceId: intent.sourceId,
2983
+ cliVersion: packageInfo.version,
2984
+ cwd: context.cwd,
2985
+ impactConfirmed: true,
2986
+ expectedImpactDigest: expected.impactDigest,
2987
+ expectedCandidate: expected.candidate,
2988
+ env: context.env,
2989
+ signal,
2990
+ runtime: options.catalogGenerationRuntime,
2991
+ });
2992
+ }
2993
+ else if (intent.action === 'repair') {
2994
+ generation = await repairCatalogSourceGeneration({
2995
+ sourceId: intent.sourceId,
2996
+ cliVersion: packageInfo.version,
2997
+ cwd: context.cwd,
2998
+ expectedCandidate: expected.candidate,
2999
+ expectedActive: expected.previousActive ?? null,
3000
+ expectedConfigDigest: expected.proposedConfigDigest ?? expected.configDigest,
3001
+ env: context.env,
3002
+ signal,
3003
+ runtime: {
3004
+ ...options.catalogGenerationRuntime,
3005
+ afterCandidateResolved: tuiControllerRuntimeTestHooks
3006
+ ?.afterCatalogGenerationCandidateResolved,
3007
+ },
3008
+ });
3009
+ }
3010
+ else if (intent.action === 'update' && expected.exactRepair) {
3011
+ generation = await repairCatalogSourceGeneration({
3012
+ sourceId: intent.sourceId,
3013
+ cliVersion: packageInfo.version,
3014
+ cwd: context.cwd,
3015
+ expectedActive: expected.previousActive ?? null,
3016
+ expectedConfigDigest: expected.proposedConfigDigest ?? expected.configDigest,
3017
+ env: context.env,
3018
+ signal,
3019
+ runtime: options.catalogGenerationRuntime,
3020
+ });
3021
+ }
3022
+ else if (intent.action === 'replaceDamaged') {
3023
+ generation = await replaceDamagedCatalogSourceGeneration({
3024
+ sourceId: intent.sourceId,
3025
+ cliVersion: packageInfo.version,
3026
+ cwd: context.cwd,
3027
+ expectedCandidate: expected.candidate,
3028
+ expectedActive: expected.previousActive,
3029
+ expectedConfigDigest: expected.proposedConfigDigest ?? expected.configDigest,
3030
+ impactConfirmed: true,
3031
+ expectedImpactDigest: expected.impactDigest,
3032
+ env: context.env,
3033
+ signal,
3034
+ runtime: {
3035
+ ...options.catalogGenerationRuntime,
3036
+ afterCandidateResolved: tuiControllerRuntimeTestHooks
3037
+ ?.afterCatalogGenerationCandidateResolved,
3038
+ },
3039
+ });
3040
+ }
3041
+ else {
3042
+ generation = await activateCatalogSourceGeneration({
3043
+ sourceId: intent.sourceId,
3044
+ cliVersion: packageInfo.version,
3045
+ cwd: context.cwd,
3046
+ activationMode: expected.activationMode,
3047
+ impactConfirmed: true,
3048
+ expectedImpactDigest: expected.impactDigest,
3049
+ impactMode: expected.impactMode,
3050
+ expectedCandidate: expected.candidate,
3051
+ expectedActive: expected.previousActive ?? null,
3052
+ expectedConfigDigest: expected.proposedConfigDigest ?? expected.configDigest,
3053
+ env: context.env,
3054
+ signal,
3055
+ runtime: {
3056
+ ...options.catalogGenerationRuntime,
3057
+ afterCandidateResolved: tuiControllerRuntimeTestHooks
3058
+ ?.afterCatalogGenerationCandidateResolved,
3059
+ },
3060
+ });
3061
+ }
3062
+ let primaryChanged = false;
3063
+ if (intent.setPrimaryAfterActivation) {
3064
+ try {
3065
+ await setPrimaryCatalogSourceCommand({
3066
+ sourceId: intent.sourceId,
3067
+ cwd: context.cwd,
3068
+ env: context.env,
3069
+ expectedConfigDigest: expected.primaryConfigDigest,
3070
+ expectedImpactDigest: expected.primaryImpactDigest,
3071
+ requireImpactConfirmation: true,
3072
+ confirmImpact: true,
3073
+ });
3074
+ primaryChanged = true;
3075
+ }
3076
+ catch (error) {
3077
+ return {
3078
+ status: 'partial',
3079
+ partialReason: 'primary',
3080
+ configSaved,
3081
+ generation,
3082
+ activeAvailable: true,
3083
+ errorCode: typeof error === 'object' && error !== null
3084
+ && 'code' in error && typeof error.code === 'string'
3085
+ ? error.code
3086
+ : error instanceof Error ? error.name : 'catalogSourcePrimaryFailed',
3087
+ };
3088
+ }
3089
+ }
3090
+ return { status: 'succeeded', generation, configSaved, primaryChanged };
3091
+ }
3092
+ catch (error) {
3093
+ if (!configSaved)
3094
+ throw error;
3095
+ return {
3096
+ status: 'partial',
3097
+ partialReason: 'activation',
3098
+ configSaved: true,
3099
+ errorCode: error instanceof CatalogGenerationError ? error.code : 'catalogGenerationRefreshFailed',
3100
+ };
3101
+ }
3102
+ }
907
3103
  if (intent.kind === 'portableInstall') {
908
3104
  if (!expectedEvidence.portable) {
909
3105
  throw operationReviewStale('确认信息缺少 Portable evidence。');
910
3106
  }
911
3107
  const packageInfo = await readCliPackageInfo();
912
3108
  try {
3109
+ if (intent.retry) {
3110
+ if (!('retryPairs' in expectedEvidence.portable)) {
3111
+ throw operationReviewStale('确认信息缺少 Portable 重试目标。');
3112
+ }
3113
+ return await executePortableRetry({
3114
+ receipt: intent.retry.receipt,
3115
+ review: expectedEvidence.portable,
3116
+ expectedEvidenceDigest: expectedEvidence.portable.evidenceDigest,
3117
+ cwd: context.cwd,
3118
+ env: context.env,
3119
+ cliVersion: packageInfo.version,
3120
+ signal,
3121
+ onProgress(stage) {
3122
+ const phase = stage === 'prepare' ? '重新检查失败目标'
3123
+ : stage === 'add' ? '重试缺失的 Skills' : '验证重试结果';
3124
+ progressFeed?.setCurrent({ target: 'Skills CLI', phase }, `Portable Skills:${phase}`);
3125
+ },
3126
+ });
3127
+ }
913
3128
  return await installPortableSkills({
914
3129
  selector: intent.pluginId,
915
3130
  scope: intent.scope,
@@ -920,6 +3135,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
920
3135
  env: context.env,
921
3136
  cliVersion: packageInfo.version,
922
3137
  expectedEvidenceDigest: expectedEvidence.portable.evidenceDigest,
3138
+ expectedGenerationSetDigest: expectedEvidence.catalog.generationSetDigest,
923
3139
  onProgress(stage) {
924
3140
  const phase = stage === 'prepare' ? '检查插件依赖并生成候选'
925
3141
  : stage === 'add' ? '安装已选择的 Skills' : '验证安装结果';
@@ -934,73 +3150,644 @@ export function createTuiControllerDependencies(context, progressFeed, options =
934
3150
  throw error;
935
3151
  }
936
3152
  }
3153
+ if (intent.kind === 'sourceMutation') {
3154
+ const currentEvidence = await captureEvidence(intent);
3155
+ if (operationEvidenceFingerprint(currentEvidence) !== operationEvidenceFingerprint(expectedEvidence)) {
3156
+ throw new TuiOperationBlockedError('operationReviewStale', '确认后状态发生变化。');
3157
+ }
3158
+ await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
3159
+ const packageInfo = await readCliPackageInfo();
3160
+ const runtime = {
3161
+ onProgress: async (event) => {
3162
+ const phase = SOURCE_PROGRESS_PHASES[event.stage];
3163
+ progressFeed?.setCurrent({ target: 'Catalog 来源配置', phase }, `来源操作:${phase}`);
3164
+ await tuiControllerRuntimeTestHooks?.onCatalogSourceProgress?.(event);
3165
+ },
3166
+ };
3167
+ const common = {
3168
+ cwd: context.cwd,
3169
+ env: context.env,
3170
+ expectedConfigDigest: expectedEvidence.sourceWrite?.configDigest,
3171
+ expectedProposedConfigDigest: expectedEvidence.sourceWrite?.proposedConfigDigest,
3172
+ expectedImpactDigest: expectedEvidence.sourceWrite?.impactDigest,
3173
+ runtime,
3174
+ };
3175
+ if (intent.action === 'add' && intent.source) {
3176
+ return addCatalogSourceCommand({
3177
+ ...common,
3178
+ source: intent.source,
3179
+ cliVersion: packageInfo.version,
3180
+ requireImpactConfirmation: true,
3181
+ confirmImpact: true,
3182
+ });
3183
+ }
3184
+ if (intent.action === 'update' && intent.update) {
3185
+ return updateCatalogSourceCommand({
3186
+ ...common,
3187
+ sourceId: intent.sourceId,
3188
+ update: intent.update,
3189
+ cliVersion: packageInfo.version,
3190
+ requireIdentityChangeConfirmation: true,
3191
+ confirmIdentityChange: true,
3192
+ });
3193
+ }
3194
+ if (intent.action === 'setPrimary') {
3195
+ return setPrimaryCatalogSourceCommand({
3196
+ ...common,
3197
+ sourceId: intent.sourceId,
3198
+ requireImpactConfirmation: true,
3199
+ confirmImpact: true,
3200
+ });
3201
+ }
3202
+ if (intent.action === 'remove') {
3203
+ return removeCatalogSourceCommand({
3204
+ ...common,
3205
+ sourceId: intent.sourceId,
3206
+ requireImpactConfirmation: true,
3207
+ confirmImpact: true,
3208
+ });
3209
+ }
3210
+ throw new TuiOperationBlockedError('invalidSourceMutation', '来源操作缺少候选输入。');
3211
+ }
3212
+ const codexObservationSession = intent.kind === 'pluginWrite'
3213
+ || intent.kind === 'pluginWriteBatch'
3214
+ || intent.kind === 'pluginWriteTask'
3215
+ ? await createCodexNativeObservationSession({
3216
+ exactCwd: context.cwd,
3217
+ env: context.env,
3218
+ runner: context.nativeCliRunner,
3219
+ manager: codexManagerByEvidence.get(expectedEvidence),
3220
+ })
3221
+ : undefined;
937
3222
  return withAwHomeMutationLock({
938
3223
  env: context.env,
939
3224
  action: async (awHomeMutationToken) => {
940
- const currentEvidence = await captureEvidence(intent, undefined, 'requireRemote');
941
- if (operationEvidenceFingerprint(currentEvidence) !== operationEvidenceFingerprint(expectedEvidence)) {
942
- throw new TuiOperationBlockedError('operationReviewStale', '确认后状态发生变化。');
3225
+ await tuiControllerRuntimeTestHooks?.beforeMutationEvidenceCapture?.();
3226
+ const operationInventory = createOperationInventoryReader();
3227
+ let currentEvidence;
3228
+ try {
3229
+ currentEvidence = await captureEvidence(intent, undefined, undefined, operationInventory, true, undefined, codexObservationSession, intent.kind === 'pluginWriteBatch'
3230
+ ? expectedEvidence
3231
+ : undefined);
943
3232
  }
944
- await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
945
- if (intent.kind === 'sourceMutation') {
946
- const packageInfo = await readCliPackageInfo();
947
- const runtime = {
948
- awHomeMutationToken,
949
- onProgress: async (event) => {
950
- const phase = SOURCE_PROGRESS_PHASES[event.stage];
951
- progressFeed?.setCurrent({ target: 'Catalog 来源配置', phase }, `来源操作:${phase}`);
952
- await tuiControllerRuntimeTestHooks?.onCatalogSourceProgress?.(event);
953
- },
954
- };
955
- const common = {
956
- cwd: context.cwd,
957
- env: context.env,
958
- expectedConfigDigest: expectedEvidence.sourceWrite?.configDigest,
959
- runtime,
960
- };
961
- if (intent.action === 'add' && intent.source) {
962
- return addCatalogSourceCommand({ ...common, source: intent.source, cliVersion: packageInfo.version });
3233
+ catch (error) {
3234
+ const code = error instanceof TuiOperationBlockedError ? error.code : undefined;
3235
+ if (intent.kind === 'pluginWriteBatch'
3236
+ && (intent.selection.kind === 'currentProjectIntentRoots'
3237
+ || intent.selection.kind === 'currentProjectIntentTargets')
3238
+ && error instanceof TuiOperationBlockedError) {
3239
+ throw operationReviewStale('确认后完整项目 Intent 声明发生变化。');
963
3240
  }
964
- if (intent.action === 'update' && intent.update) {
965
- return updateCatalogSourceCommand({ ...common, sourceId: intent.sourceId, update: intent.update, cliVersion: packageInfo.version });
3241
+ if (intent.kind === 'pluginWriteTask'
3242
+ && (code === 'taskHasBlockedTargets' || code === 'taskNoExecutableTargets')) {
3243
+ throw operationReviewStale('确认后任务可执行目标发生变化。', error instanceof TuiOperationBlockedError ? error.diagnostics : undefined);
966
3244
  }
967
- if (intent.action === 'setPrimary') {
968
- return setPrimaryCatalogSourceCommand({ ...common, sourceId: intent.sourceId });
3245
+ throw error;
3246
+ }
3247
+ if (intent.kind === 'pluginWriteBatch') {
3248
+ const expectedBatch = expectedEvidence;
3249
+ const currentBatch = currentEvidence;
3250
+ if ((intent.selection.kind === 'currentProjectIntentRoots'
3251
+ || intent.selection.kind === 'currentProjectIntentTargets')
3252
+ && (!expectedBatch.projectIntentDigest
3253
+ || expectedBatch.projectIntentDigest !== currentBatch.projectIntentDigest)) {
3254
+ throw operationReviewStale('确认后完整项目 Intent 声明发生变化。');
3255
+ }
3256
+ const reviewedGroups = expectedBatch.groups.map((group) => ({
3257
+ groupId: group.groupId,
3258
+ sourceId: group.sourceId,
3259
+ platform: group.platform,
3260
+ scope: group.scope,
3261
+ requestedPluginIds: group.requestedPluginIds,
3262
+ executable: group.executable,
3263
+ planFingerprint: group.preview.operationPlan
3264
+ ? intent.action === 'cleanup'
3265
+ ? group.preview.operationPlan.planFingerprint
3266
+ : createBatchIntentCompatiblePlanFingerprint(group.preview.operationPlan)
3267
+ : undefined,
3268
+ }));
3269
+ const currentGroups = currentBatch.groups.map((group) => ({
3270
+ groupId: group.groupId,
3271
+ sourceId: group.sourceId,
3272
+ platform: group.platform,
3273
+ scope: group.scope,
3274
+ requestedPluginIds: group.requestedPluginIds,
3275
+ executable: expectedBatch.groups.find((reviewed) => reviewed.groupId === group.groupId)
3276
+ ?.executable === false ? false : group.executable,
3277
+ planFingerprint: expectedBatch.groups.find((reviewed) => reviewed.groupId === group.groupId)
3278
+ ?.executable === false
3279
+ ? undefined
3280
+ : group.preview.operationPlan
3281
+ ? intent.action === 'cleanup'
3282
+ ? group.preview.operationPlan.planFingerprint
3283
+ : createBatchIntentCompatiblePlanFingerprint(group.preview.operationPlan)
3284
+ : undefined,
3285
+ }));
3286
+ if (expectedBatch.catalog.configDigest !== currentBatch.catalog.configDigest
3287
+ || JSON.stringify(expectedBatch.requestedPluginIds)
3288
+ !== JSON.stringify(currentBatch.requestedPluginIds)
3289
+ || JSON.stringify(reviewedGroups) !== JSON.stringify(currentGroups)) {
3290
+ throw operationReviewStale('确认后批量目标或写入计划发生变化。');
969
3291
  }
970
- if (intent.action === 'remove') {
971
- return removeCatalogSourceCommand({ ...common, sourceId: intent.sourceId });
3292
+ await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
3293
+ const groupResults = [];
3294
+ const completedTargetKeys = new Set();
3295
+ const expectedIntentDigests = new Map();
3296
+ for (const group of expectedBatch.groups) {
3297
+ for (const intentDigest of group.preview.operationPlan?.evidence.intentDigests ?? []) {
3298
+ expectedIntentDigests.set(intentDigest.scope, intentDigest.digest);
3299
+ }
3300
+ }
3301
+ let executedGroupCount = 0;
3302
+ const appendStoppedGroups = (startIndex, staleMessage) => {
3303
+ for (const [offset, stopped] of expectedBatch.groups.slice(startIndex).entries()) {
3304
+ groupResults.push({
3305
+ groupId: stopped.groupId,
3306
+ sourceId: stopped.sourceId,
3307
+ platform: stopped.platform,
3308
+ scope: stopped.scope,
3309
+ requestedPluginIds: stopped.requestedPluginIds,
3310
+ result: !stopped.executable ? stopped.preview : failedWriteCommandResult(intent.action, stopped.requestedPluginIds, { cwd: context.cwd, platform: stopped.platform, scope: stopped.scope }, offset === 0
3311
+ ? new TuiOperationBlockedError('operationReviewStale', staleMessage)
3312
+ : new TuiOperationBlockedError('batchNotStarted', '前一个批量组的写入计划已变化,因此这个组没有开始。')),
3313
+ });
3314
+ }
3315
+ };
3316
+ for (const [groupIndex, reviewed] of expectedBatch.groups.entries()) {
3317
+ const current = currentBatch.groups.find((group) => group.groupId === reviewed.groupId);
3318
+ if (!reviewed.executable) {
3319
+ groupResults.push({
3320
+ groupId: reviewed.groupId,
3321
+ sourceId: reviewed.sourceId,
3322
+ platform: reviewed.platform,
3323
+ scope: reviewed.scope,
3324
+ requestedPluginIds: reviewed.requestedPluginIds,
3325
+ result: reviewed.preview,
3326
+ });
3327
+ continue;
3328
+ }
3329
+ const groupInventory = groupResults.length === 0
3330
+ ? operationInventory
3331
+ : createOperationInventoryReader();
3332
+ let executionPreview = current.preview;
3333
+ if (completedTargetKeys.size > 0
3334
+ || (intent.action === 'cleanup' && executedGroupCount > 0)) {
3335
+ executionPreview = await runBatchWriteGroup({
3336
+ intent,
3337
+ group: current,
3338
+ cwd: context.cwd,
3339
+ runtime: {
3340
+ ...context,
3341
+ awHomeMutationToken,
3342
+ inventoryCatalogSourceReferences: groupInventory,
3343
+ ...(intent.action === 'cleanup' ? {} : {
3344
+ expectedCatalogConfigDigest: expectedBatch.catalog.configDigest,
3345
+ }),
3346
+ previewOperationPlan: true,
3347
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3348
+ },
3349
+ });
3350
+ const cleanupIntentDigestChanged = intent.action === 'cleanup'
3351
+ && executionPreview.operationPlan?.evidence.intentDigests.find((entry) => entry.scope === current.scope)?.digest !== expectedIntentDigests.get(current.scope);
3352
+ if (cleanupIntentDigestChanged
3353
+ || !reviewed.preview.operationPlan || !executionPreview.operationPlan
3354
+ || createBatchIntentCompatiblePlanFingerprint(reviewed.preview.operationPlan, completedTargetKeys) !== createBatchIntentCompatiblePlanFingerprint(executionPreview.operationPlan, completedTargetKeys)) {
3355
+ appendStoppedGroups(groupIndex, '前序批量结果改变了这个组的实际写入计划。');
3356
+ break;
3357
+ }
3358
+ }
3359
+ const requiresManagedCodex = reviewed.preview.codexManagerPreview?.state === 'requires-install';
3360
+ const result = await runBatchWriteGroup({
3361
+ intent,
3362
+ group: current,
3363
+ cwd: context.cwd,
3364
+ runtime: {
3365
+ ...context,
3366
+ awHomeMutationToken,
3367
+ preApplyConfirmed: true,
3368
+ inventoryCatalogSourceReferences: groupInventory,
3369
+ ...(intent.action === 'cleanup' ? {} : {
3370
+ expectedCatalogConfigDigest: expectedBatch.catalog.configDigest,
3371
+ }),
3372
+ ...(executionPreview.operationPlan ? {
3373
+ expectedPlanFingerprint: executionPreview.operationPlan.planFingerprint,
3374
+ } : {}),
3375
+ ...(requiresManagedCodex ? {
3376
+ managedCodexAuthorizer: async () => true,
3377
+ } : {}),
3378
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3379
+ },
3380
+ });
3381
+ if (writeReviewIsStale(result)) {
3382
+ if (executedGroupCount === 0)
3383
+ throwIfWriteReviewStale(result);
3384
+ appendStoppedGroups(groupIndex, '这个组的实际写入计划发生变化。');
3385
+ break;
3386
+ }
3387
+ if (intent.action === 'cleanup') {
3388
+ for (const change of result.operationPlan?.intentChanges ?? []) {
3389
+ expectedIntentDigests.set(change.scope, change.afterDigest);
3390
+ }
3391
+ }
3392
+ executedGroupCount += 1;
3393
+ for (const target of result.targetResults) {
3394
+ if (target.state === 'completed') {
3395
+ completedTargetKeys.add([
3396
+ target.target.pluginId,
3397
+ target.target.platform,
3398
+ target.target.scope,
3399
+ ].join('\0'));
3400
+ }
3401
+ }
3402
+ groupResults.push({
3403
+ groupId: reviewed.groupId,
3404
+ sourceId: reviewed.sourceId,
3405
+ platform: reviewed.platform,
3406
+ scope: reviewed.scope,
3407
+ requestedPluginIds: reviewed.requestedPluginIds,
3408
+ result,
3409
+ });
3410
+ }
3411
+ const merged = mergeWriteCommandResults(groupResults.map((group) => group.result));
3412
+ return {
3413
+ ...merged,
3414
+ requestedPluginIds: expectedBatch.requestedPluginIds,
3415
+ groupResults,
3416
+ };
3417
+ }
3418
+ if (intent.kind === 'pluginWriteTask') {
3419
+ const expectedTask = expectedEvidence;
3420
+ const currentTask = currentEvidence;
3421
+ if (!operationEvidenceMatches(expectedTask, currentTask)) {
3422
+ throw operationReviewStale('确认后任务目标、根因、版本或写入计划发生变化。');
3423
+ }
3424
+ await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
3425
+ const operationContexts = taskCatalogOperationsByEvidence.get(currentEvidence)
3426
+ ?? new Map();
3427
+ const sourceResults = [];
3428
+ const completedTargetKeys = new Set();
3429
+ const preApplyExecutionCache = new Map();
3430
+ const commandTargets = (source) => source.requestedTargets.map((target) => {
3431
+ const direct = source.causes.filter((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === pluginWriteTaskTargetKey(target));
3432
+ return {
3433
+ pluginId: target.pluginId,
3434
+ platform: target.platform,
3435
+ scope: target.scope,
3436
+ method: direct.some((cause) => cause.method === 'reinstall')
3437
+ ? 'reinstall' : 'sync',
3438
+ };
3439
+ });
3440
+ const repairTargets = (source) => [...new Map(source.causes.map((cause) => [
3441
+ pluginWriteTaskTargetKey(cause.repairTarget),
3442
+ { ...cause.repairTarget, method: cause.method },
3443
+ ])).values()];
3444
+ const stoppedResult = (source, error) => mergeWriteCommandResults(source.requestedTargets.map((target) => failedWriteCommandResult('sync', [target.pluginId], {
3445
+ cwd: context.cwd,
3446
+ platform: target.platform,
3447
+ scope: target.scope,
3448
+ }, error)));
3449
+ const appendStoppedSources = (startIndex, message) => {
3450
+ for (const [offset, source] of expectedTask.taskPlan.sources.slice(startIndex).entries()) {
3451
+ sourceResults.push({
3452
+ sourceId: source.sourceId,
3453
+ sourceIdentity: source.sourceIdentity,
3454
+ requestedTargets: source.requestedTargets,
3455
+ result: stoppedResult(source, offset === 0
3456
+ ? new TuiOperationBlockedError('operationReviewStale', message)
3457
+ : new TuiOperationBlockedError('taskNotStarted', '前一个来源计划已变化,因此这个来源没有开始。')),
3458
+ });
3459
+ }
3460
+ };
3461
+ for (const [sourceIndex, reviewed] of expectedTask.taskPlan.sources.entries()) {
3462
+ const current = currentTask.taskPlan.sources.find((source) => source.sourceId === reviewed.sourceId);
3463
+ const catalogOperation = operationContexts.get(reviewed.sourceId);
3464
+ if (!current || !catalogOperation || current.sourceIdentity !== reviewed.sourceIdentity) {
3465
+ if (sourceResults.length === 0) {
3466
+ throw operationReviewStale('确认后任务来源身份发生变化。');
3467
+ }
3468
+ appendStoppedSources(sourceIndex, '确认后任务来源身份发生变化。');
3469
+ break;
3470
+ }
3471
+ const sourceInventory = sourceResults.length === 0
3472
+ ? operationInventory : createOperationInventoryReader();
3473
+ let executionPreview = current.preview;
3474
+ if (completedTargetKeys.size > 0) {
3475
+ executionPreview = await taskWriteCommand(commandTargets(current), {
3476
+ cwd: context.cwd,
3477
+ repairTargets: repairTargets(current),
3478
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3479
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3480
+ }, {
3481
+ ...context,
3482
+ awHomeMutationToken,
3483
+ catalogOperationContext: catalogOperation,
3484
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
3485
+ expectedCatalogConfigDigest: expectedTask.catalog.configDigest,
3486
+ inventoryCatalogSourceReferences: sourceInventory,
3487
+ previewOperationPlan: true,
3488
+ preApplyExecutionCache,
3489
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3490
+ });
3491
+ if (!reviewed.preview.operationPlan || !executionPreview.operationPlan
3492
+ || createBatchIntentCompatiblePlanFingerprint(reviewed.preview.operationPlan, completedTargetKeys) !== createBatchIntentCompatiblePlanFingerprint(executionPreview.operationPlan, completedTargetKeys)) {
3493
+ appendStoppedSources(sourceIndex, '前序结果改变了这个来源的实际写入计划。');
3494
+ break;
3495
+ }
3496
+ }
3497
+ const requiresManagedCodex = reviewed.preview.codexManagerPreview?.state === 'requires-install';
3498
+ const result = await taskWriteCommand(commandTargets(current), {
3499
+ cwd: context.cwd,
3500
+ repairTargets: repairTargets(current),
3501
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3502
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3503
+ }, {
3504
+ ...context,
3505
+ awHomeMutationToken,
3506
+ catalogOperationContext: catalogOperation,
3507
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
3508
+ expectedCatalogConfigDigest: expectedTask.catalog.configDigest,
3509
+ inventoryCatalogSourceReferences: sourceInventory,
3510
+ preApplyConfirmed: true,
3511
+ preApplyExecutionCache,
3512
+ ...(executionPreview.operationPlan
3513
+ ? { expectedPlanFingerprint: executionPreview.operationPlan.planFingerprint } : {}),
3514
+ ...(requiresManagedCodex ? { managedCodexAuthorizer: async () => true } : {}),
3515
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3516
+ });
3517
+ if (writeReviewIsStale(result)) {
3518
+ if (sourceResults.length === 0)
3519
+ throwIfWriteReviewStale(result);
3520
+ appendStoppedSources(sourceIndex, '这个来源的实际写入计划发生变化。');
3521
+ break;
3522
+ }
3523
+ for (const target of result.targetResults) {
3524
+ if (target.state === 'completed') {
3525
+ completedTargetKeys.add([
3526
+ target.target.pluginId,
3527
+ target.target.platform,
3528
+ target.target.scope,
3529
+ ].join('\0'));
3530
+ }
3531
+ }
3532
+ sourceResults.push({
3533
+ sourceId: reviewed.sourceId,
3534
+ sourceIdentity: reviewed.sourceIdentity,
3535
+ requestedTargets: reviewed.requestedTargets,
3536
+ result,
3537
+ });
972
3538
  }
973
- throw new TuiOperationBlockedError('invalidSourceMutation', '来源操作缺少候选输入。');
3539
+ const merged = mergeWriteCommandResults(sourceResults.map((source) => source.result));
3540
+ return {
3541
+ ...merged,
3542
+ taskPlan: expectedTask.taskPlan,
3543
+ sourceResults,
3544
+ };
974
3545
  }
3546
+ if (intent.kind !== 'pluginWrite'
3547
+ && !operationEvidenceMatches(expectedEvidence, currentEvidence)) {
3548
+ throw new TuiOperationBlockedError('operationReviewStale', '确认后状态发生变化。');
3549
+ }
3550
+ const catalogOperation = catalogOperationByEvidence.get(currentEvidence);
3551
+ if (expectedEvidence.catalog.generationSetDigest
3552
+ && catalogOperation?.generationSetDigest !== expectedEvidence.catalog.generationSetDigest) {
3553
+ throw operationReviewStale('确认后 Active Catalog generation 发生变化。');
3554
+ }
3555
+ await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
975
3556
  if (intent.kind === 'pluginWrite') {
976
3557
  const options = {
977
3558
  cwd: context.cwd,
978
3559
  platform: intent.platform,
979
3560
  scope: intent.scope,
3561
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3562
+ ? {}
3563
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3564
+ ...(intent.action === 'install' && intent.dependencyScopeOverrides
3565
+ ? { dependencyScopeOverrides: intent.dependencyScopeOverrides }
3566
+ : {}),
3567
+ ...(expectedEvidence.codexManager?.state === 'requires-install'
3568
+ ? { allowManagedCodexInstall: true }
3569
+ : {}),
980
3570
  ...(intent.action === 'cleanup' ? { force: intent.force === true } : {}),
981
3571
  };
982
3572
  const command = {
983
3573
  install: installWriteCommand,
984
- upgrade: upgradeWriteCommand,
3574
+ refreshAndSync: syncWriteCommand,
985
3575
  reinstall: reinstallWriteCommand,
986
3576
  uninstall: uninstallWriteCommand,
987
3577
  cleanup: cleanupWriteCommand,
3578
+ sync: syncWriteCommand,
988
3579
  }[intent.action];
3580
+ if (intent.action === 'cleanup') {
3581
+ if (!expectedEvidence.operationPlan || !currentEvidence.operationPlan
3582
+ || expectedEvidence.operationPlan.planFingerprint
3583
+ !== currentEvidence.operationPlan.planFingerprint) {
3584
+ throw operationReviewStale('确认后清理计划发生变化。');
3585
+ }
3586
+ const result = await command([intent.pluginId], options, {
3587
+ ...context,
3588
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3589
+ awHomeMutationToken,
3590
+ preApplyConfirmed: true,
3591
+ inventoryCatalogSourceReferences: operationInventory,
3592
+ expectedPlanFingerprint: expectedEvidence.operationPlan.planFingerprint,
3593
+ ...(expectedEvidence.codexManager?.state === 'requires-install'
3594
+ ? { managedCodexAuthorizer: async () => true }
3595
+ : {}),
3596
+ });
3597
+ throwIfWriteReviewStale(result);
3598
+ return result;
3599
+ }
989
3600
  const currentSource = currentEvidence.source;
990
3601
  if (!expectedEvidence.source || !currentSource) {
991
3602
  throw new TuiOperationBlockedError('operationReviewStale', '确认信息缺少来源身份。');
992
3603
  }
993
3604
  const result = await command([intent.pluginId], options, {
994
3605
  ...context,
3606
+ ...(codexObservationSession ? { codexObservationSession } : {}),
995
3607
  awHomeMutationToken,
3608
+ preApplyConfirmed: true,
3609
+ inventoryCatalogSourceReferences: operationInventory,
3610
+ ...(expectedEvidence.operationPlan
3611
+ ? { expectedPlanFingerprint: expectedEvidence.operationPlan.planFingerprint }
3612
+ : {}),
996
3613
  expectedCatalogConfigDigest: expectedEvidence.catalog.configDigest,
3614
+ ...(catalogOperation ? {
3615
+ catalogOperationContext: catalogOperation,
3616
+ expectedGenerationSetDigest: expectedEvidence.catalog.generationSetDigest,
3617
+ } : {}),
3618
+ ...(expectedEvidence.codexManager?.state === 'requires-install'
3619
+ ? { managedCodexAuthorizer: async () => true }
3620
+ : {}),
997
3621
  reviewPreflight: (actual) => assertSourceEvidenceMatches(currentSource, actual),
998
3622
  });
999
3623
  throwIfWriteReviewStale(result);
1000
3624
  return result;
1001
3625
  }
3626
+ if (intent.kind === 'configSaveBatch') {
3627
+ assertConfigSaveBatchIntent(intent);
3628
+ const reviewedBatch = expectedEvidence.configBatch;
3629
+ if (!reviewedBatch || !currentEvidence.configBatch || !currentEvidence.source) {
3630
+ throw operationReviewStale('确认信息缺少设置批次证据。');
3631
+ }
3632
+ const approvedApplyTargets = new Set(reviewedBatch.groups.flatMap((group) => group.config.affectedTargets.map((target) => `${target.platform}\0${target.scope}`)));
3633
+ const approvedPreApplyTargets = reviewedBatch.groups.flatMap((group) => group.config.preApplyAuthorization ?? []);
3634
+ const results = [];
3635
+ const packageInfo = await readCliPackageInfo();
3636
+ for (const group of intent.groups) {
3637
+ const groupIntent = configSaveBatchGroupIntent(intent, group);
3638
+ const reviewedGroup = reviewedBatch.groups.find((item) => item.scope === group.scope);
3639
+ if (!reviewedGroup) {
3640
+ throw operationReviewStale('确认信息缺少设置层基线。');
3641
+ }
3642
+ let configSaved = false;
3643
+ const operationRebuildResults = [];
3644
+ try {
3645
+ const freshEvidence = await captureEvidence(groupIntent, undefined, undefined, operationInventory, true);
3646
+ const freshSource = freshEvidence.source;
3647
+ const freshConfig = freshEvidence.config;
3648
+ if (!freshSource || !freshConfig) {
3649
+ throw operationReviewStale('设置目标缺少最新证据。');
3650
+ }
3651
+ assertSourceEvidenceMatches(currentEvidence.source, freshSource);
3652
+ const freshLayerStoreDigest = await configLayerStoreDigest({
3653
+ source: freshSource,
3654
+ pluginName: parsePluginId(intent.pluginId).pluginName,
3655
+ scope: group.scope,
3656
+ cwd: context.cwd,
3657
+ env: context.env,
3658
+ });
3659
+ if (freshLayerStoreDigest !== reviewedGroup.config.storeDigest) {
3660
+ throw operationReviewStale('保存设置期间此设置层的原始内容发生变化。');
3661
+ }
3662
+ if (freshConfig.affectedTargets.some((target) => !approvedApplyTargets.has(`${target.platform}\0${target.scope}`))) {
3663
+ throw operationReviewStale('同批前一层变更扩大了未经确认的应用目标。');
3664
+ }
3665
+ const groupCatalogOperation = catalogOperationByEvidence.get(freshEvidence);
3666
+ if (expectedEvidence.catalog.generationSetDigest
3667
+ && groupCatalogOperation?.generationSetDigest
3668
+ !== expectedEvidence.catalog.generationSetDigest) {
3669
+ throw operationReviewStale('保存设置期间 Active Catalog generation 发生变化。');
3670
+ }
3671
+ const result = await withResolvedCatalogPlugin({
3672
+ selector: intent.pluginId,
3673
+ cliVersion: packageInfo.version,
3674
+ env: context.env,
3675
+ ...(groupCatalogOperation ? { operationContext: groupCatalogOperation } : {}),
3676
+ consume: async ({ plugin, source }) => {
3677
+ assertSourceEvidenceMatches(freshSource, sourceEvidenceForPlugin(source, plugin.manifest.name));
3678
+ const applyTargets = intent.apply
3679
+ ? freshConfig.affectedTargets.map((target) => ({
3680
+ pluginName: plugin.manifest.name,
3681
+ platform: target.platform,
3682
+ scope: target.scope,
3683
+ }))
3684
+ : [];
3685
+ return applyPluginConfigWrite({
3686
+ plugin,
3687
+ pluginName: plugin.manifest.name,
3688
+ scope: group.scope,
3689
+ cwd: context.cwd,
3690
+ env: context.env,
3691
+ nativeCliRunner: context.nativeCliRunner,
3692
+ rebuildRuntime: {
3693
+ ...context,
3694
+ awHomeMutationToken,
3695
+ operationDiagnosticSink: (entries) => {
3696
+ operationRebuildResults.push(...structuredClone(entries));
3697
+ },
3698
+ ...(reviewedScriptTimeoutMs(intent) === undefined ? {} : {
3699
+ scriptTimeoutMs: reviewedScriptTimeoutMs(intent),
3700
+ }),
3701
+ ...(groupCatalogOperation ? {
3702
+ catalogOperationContext: groupCatalogOperation,
3703
+ expectedGenerationSetDigest: expectedEvidence.catalog.generationSetDigest,
3704
+ } : {}),
3705
+ },
3706
+ awHomeMutationToken,
3707
+ applyTargets,
3708
+ request: { updates: group.updates, removeKeys: group.removeKeys },
3709
+ sharedNativeApplyConfirmed: true,
3710
+ preApplyConfirmed: true,
3711
+ preApplyAuthorizedTargets: approvedPreApplyTargets,
3712
+ });
3713
+ },
3714
+ });
3715
+ configSaved = result.configWrite === 'succeeded';
3716
+ const applyStatus = !intent.apply
3717
+ ? 'notRequested'
3718
+ : result.apply.state === 'succeeded'
3719
+ ? 'succeeded'
3720
+ : result.apply.state === 'skipped'
3721
+ ? 'notNeeded'
3722
+ : 'failed';
3723
+ results.push({
3724
+ scope: group.scope,
3725
+ status: applyStatus === 'failed' ? 'failed' : 'succeeded',
3726
+ configSaved,
3727
+ applyRequested: intent.apply,
3728
+ applyStatus,
3729
+ execution: { ...result, operationRebuildResults },
3730
+ ...(applyStatus === 'failed' ? {
3731
+ errorCode: result.apply.state === 'unavailable'
3732
+ ? result.apply.error?.code ?? result.apply.reason
3733
+ : result.apply.targetResults.find((target) => target.errorCode)?.errorCode
3734
+ ?? 'runtimeApplyFailed',
3735
+ } : {}),
3736
+ });
3737
+ }
3738
+ catch (error) {
3739
+ configSaved = error?.configWrite === 'succeeded';
3740
+ const errorCode = typeof error === 'object' && error !== null
3741
+ && 'code' in error && typeof error.code === 'string'
3742
+ ? error.code
3743
+ : configSaved ? 'runtimeApplyFailed' : 'configWriteFailed';
3744
+ results.push({
3745
+ scope: group.scope,
3746
+ status: 'failed',
3747
+ configSaved,
3748
+ applyRequested: intent.apply,
3749
+ applyStatus: intent.apply ? 'failed' : 'notRequested',
3750
+ errorCode,
3751
+ ...(operationRebuildResults.length > 0
3752
+ ? { execution: { operationRebuildResults } }
3753
+ : {}),
3754
+ });
3755
+ }
3756
+ }
3757
+ const succeeded = results.filter((result) => result.status === 'succeeded').length;
3758
+ return {
3759
+ status: succeeded === results.length
3760
+ ? 'succeeded'
3761
+ : succeeded > 0 || results.some((result) => result.configSaved)
3762
+ ? 'partial'
3763
+ : 'failed',
3764
+ groups: results,
3765
+ };
3766
+ }
1002
3767
  if (intent.kind === 'configSave' || intent.kind === 'configReset') {
3768
+ const operationRebuildResults = [];
3769
+ const operationDiagnosticSink = (results) => {
3770
+ operationRebuildResults.push(...structuredClone(results));
3771
+ };
1003
3772
  const currentSource = currentEvidence.source;
3773
+ if (intent.kind === 'configReset') {
3774
+ const parsed = parsePluginId(intent.pluginId);
3775
+ if (!parsed.sourceId || !parsed.pluginName) {
3776
+ throw operationReviewStale('确认信息缺少 qualified 设置目标。');
3777
+ }
3778
+ return resetPluginConfigWithHelper({
3779
+ sourceId: parsed.sourceId,
3780
+ pluginName: parsed.pluginName,
3781
+ scope: intent.scope,
3782
+ cwd: context.cwd,
3783
+ env: context.env,
3784
+ nativeCliRunner: context.nativeCliRunner,
3785
+ rebuildRuntime: { ...context, awHomeMutationToken },
3786
+ awHomeMutationToken,
3787
+ apply: false,
3788
+ sharedNativeApplyConfirmed: false,
3789
+ });
3790
+ }
1004
3791
  if (!currentSource) {
1005
3792
  throw operationReviewStale('确认信息缺少来源身份。');
1006
3793
  }
@@ -1008,40 +3795,76 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1008
3795
  return withResolvedCatalogPlugin({
1009
3796
  selector: intent.pluginId,
1010
3797
  cliVersion: packageInfo.version,
1011
- freshnessPolicy: 'requireRemote',
1012
3798
  env: context.env,
3799
+ ...(catalogOperation ? { operationContext: catalogOperation } : {}),
1013
3800
  consume: async ({ plugin, source }) => {
1014
3801
  assertSourceEvidenceMatches(currentSource, sourceEvidenceForPlugin(source, plugin.manifest.name));
1015
- const result = intent.kind === 'configSave'
1016
- ? await applyPluginConfigWrite({
1017
- plugin,
3802
+ const applyTargets = intent.retryApply || intent.apply
3803
+ ? currentEvidence.config?.affectedTargets.map((target) => ({
1018
3804
  pluginName: plugin.manifest.name,
3805
+ platform: target.platform,
3806
+ scope: target.scope,
3807
+ })) ?? []
3808
+ : [];
3809
+ const result = intent.retryApply
3810
+ ? await replayConfigRuntimeWrites({
3811
+ plugin,
1019
3812
  scope: intent.scope,
1020
- projectLayer: intent.projectLayer,
1021
3813
  cwd: context.cwd,
1022
3814
  env: context.env,
1023
3815
  nativeCliRunner: context.nativeCliRunner,
1024
- rebuildRuntime: { ...context, awHomeMutationToken },
3816
+ rebuildRuntime: {
3817
+ ...context,
3818
+ awHomeMutationToken,
3819
+ operationDiagnosticSink,
3820
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3821
+ ? {}
3822
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3823
+ ...(catalogOperation ? {
3824
+ catalogOperationContext: catalogOperation,
3825
+ expectedGenerationSetDigest: expectedEvidence.catalog.generationSetDigest,
3826
+ } : {}),
3827
+ },
1025
3828
  awHomeMutationToken,
1026
- request: { updates: intent.updates, removeKeys: intent.removeKeys },
3829
+ applyTargets,
3830
+ sharedNativeApplyConfirmed: true,
3831
+ preApplyConfirmed: true,
3832
+ preApplyAuthorizedTargets: expectedEvidence.config?.preApplyAuthorization ?? [],
1027
3833
  })
1028
- : await resetPluginConfigWithHelper({
3834
+ : await applyPluginConfigWrite({
1029
3835
  plugin,
1030
3836
  pluginName: plugin.manifest.name,
1031
3837
  scope: intent.scope,
1032
- projectLayer: intent.projectLayer,
1033
3838
  cwd: context.cwd,
1034
3839
  env: context.env,
1035
3840
  nativeCliRunner: context.nativeCliRunner,
1036
- rebuildRuntime: { ...context, awHomeMutationToken },
3841
+ rebuildRuntime: {
3842
+ ...context,
3843
+ awHomeMutationToken,
3844
+ operationDiagnosticSink,
3845
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3846
+ ? {}
3847
+ : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3848
+ ...(catalogOperation ? {
3849
+ catalogOperationContext: catalogOperation,
3850
+ expectedGenerationSetDigest: expectedEvidence.catalog.generationSetDigest,
3851
+ } : {}),
3852
+ },
1037
3853
  awHomeMutationToken,
3854
+ applyTargets,
3855
+ request: { updates: intent.updates, removeKeys: intent.removeKeys },
3856
+ sharedNativeApplyConfirmed: true,
3857
+ preApplyConfirmed: true,
3858
+ preApplyAuthorizedTargets: expectedEvidence.config?.preApplyAuthorization ?? [],
1038
3859
  });
1039
- return result;
3860
+ return { ...result, operationRebuildResults };
1040
3861
  },
1041
3862
  });
1042
3863
  }
1043
3864
  throw new TuiOperationBlockedError('operationNotImplemented', '这个写操作尚未接入新 Controller。');
1044
3865
  },
3866
+ }).finally(async () => {
3867
+ await codexObservationSession?.dispose();
1045
3868
  });
1046
3869
  },
1047
3870
  };