@lesliechan721/aw 0.1.0-beta.4 → 0.1.0-beta.6

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 (99) hide show
  1. package/dist/authoring/scripts/lib/plugin-version-check.d.mts +14 -0
  2. package/dist/authoring/scripts/lib/plugin-version-check.mjs +10 -224
  3. package/dist/authoring/scripts/lib/plugin-version-check.mjs.map +1 -1
  4. package/dist/authoring/scripts/remove-plugin.d.mts +2 -0
  5. package/dist/authoring/scripts/remove-plugin.mjs +366 -0
  6. package/dist/authoring/scripts/remove-plugin.mjs.map +1 -0
  7. package/dist/authoring/scripts/validate-plugin.mjs +9 -46
  8. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  9. package/dist/scripts/bundle-plugins.d.ts +0 -2
  10. package/dist/scripts/bundle-plugins.js +4 -2
  11. package/dist/scripts/bundle-plugins.js.map +1 -1
  12. package/dist/scripts/plugin-staging.d.ts +1 -16
  13. package/dist/scripts/plugin-staging.js +8 -115
  14. package/dist/scripts/plugin-staging.js.map +1 -1
  15. package/dist/src/authoring/workspace-commands.d.ts +9 -1
  16. package/dist/src/authoring/workspace-commands.js +24 -16
  17. package/dist/src/authoring/workspace-commands.js.map +1 -1
  18. package/dist/src/commands/catalog-publication-inspection.d.ts +106 -0
  19. package/dist/src/commands/catalog-publication-inspection.js +444 -0
  20. package/dist/src/commands/catalog-publication-inspection.js.map +1 -0
  21. package/dist/src/commands/catalog-publish.d.ts +18 -2
  22. package/dist/src/commands/catalog-publish.js +261 -316
  23. package/dist/src/commands/catalog-publish.js.map +1 -1
  24. package/dist/src/commands/catalog-status.d.ts +60 -0
  25. package/dist/src/commands/catalog-status.js +251 -0
  26. package/dist/src/commands/catalog-status.js.map +1 -0
  27. package/dist/src/commands/plugin-capabilities.d.ts +22 -0
  28. package/dist/src/commands/plugin-capabilities.js +143 -0
  29. package/dist/src/commands/plugin-capabilities.js.map +1 -0
  30. package/dist/src/commands/rebuild-command.d.ts +2 -0
  31. package/dist/src/commands/rebuild-command.js +37 -6
  32. package/dist/src/commands/rebuild-command.js.map +1 -1
  33. package/dist/src/commands/rebuild-integration.d.ts +1 -0
  34. package/dist/src/commands/rebuild-integration.js +55 -57
  35. package/dist/src/commands/rebuild-integration.js.map +1 -1
  36. package/dist/src/commands/status-v4.d.ts +21 -0
  37. package/dist/src/commands/status-v4.js +63 -2
  38. package/dist/src/commands/status-v4.js.map +1 -1
  39. package/dist/src/index.js +79 -18
  40. package/dist/src/index.js.map +1 -1
  41. package/dist/src/tui/components.d.ts +70 -0
  42. package/dist/src/tui/components.js +149 -6
  43. package/dist/src/tui/components.js.map +1 -1
  44. package/dist/src/tui/console-app-v4.js +1660 -628
  45. package/dist/src/tui/console-app-v4.js.map +1 -1
  46. package/dist/src/tui/controller-runtime.d.ts +5 -1
  47. package/dist/src/tui/controller-runtime.js +184 -33
  48. package/dist/src/tui/controller-runtime.js.map +1 -1
  49. package/dist/src/tui/controller.d.ts +35 -2
  50. package/dist/src/tui/controller.js +300 -39
  51. package/dist/src/tui/controller.js.map +1 -1
  52. package/dist/src/tui/diagnostics.d.ts +31 -0
  53. package/dist/src/tui/diagnostics.js +71 -0
  54. package/dist/src/tui/diagnostics.js.map +1 -0
  55. package/dist/src/tui/index.d.ts +5 -3
  56. package/dist/src/tui/index.js +3 -1
  57. package/dist/src/tui/index.js.map +1 -1
  58. package/dist/src/tui/model.d.ts +33 -4
  59. package/dist/src/tui/model.js +252 -17
  60. package/dist/src/tui/model.js.map +1 -1
  61. package/dist/src/tui/router.d.ts +15 -9
  62. package/dist/src/tui/router.js +35 -0
  63. package/dist/src/tui/router.js.map +1 -1
  64. package/dist/src/tui/theme.d.ts +18 -10
  65. package/dist/src/tui/theme.js +18 -10
  66. package/dist/src/tui/theme.js.map +1 -1
  67. package/dist/src/tui/viewport.d.ts +20 -0
  68. package/dist/src/tui/viewport.js +29 -0
  69. package/dist/src/tui/viewport.js.map +1 -0
  70. package/dist/src/utils/catalog-runtime-projection.d.ts +1 -7
  71. package/dist/src/utils/catalog-runtime-projection.js +2 -62
  72. package/dist/src/utils/catalog-runtime-projection.js.map +1 -1
  73. package/dist/src/utils/catalog-source-cache.d.ts +12 -11
  74. package/dist/src/utils/catalog-source-cache.js +20 -0
  75. package/dist/src/utils/catalog-source-cache.js.map +1 -1
  76. package/dist/src/utils/dynamic-skill-tree.js +1 -1
  77. package/dist/src/utils/dynamic-skill-tree.js.map +1 -1
  78. package/dist/src/utils/git-catalog-provider.js +1 -1
  79. package/dist/src/utils/git-catalog-provider.js.map +1 -1
  80. package/dist/src/utils/manifest.d.ts +0 -1
  81. package/dist/src/utils/manifest.js +0 -1
  82. package/dist/src/utils/manifest.js.map +1 -1
  83. package/dist/src/utils/plugin-config.js +28 -30
  84. package/dist/src/utils/plugin-config.js.map +1 -1
  85. package/dist/src/utils/skill-validation.d.ts +1 -1
  86. package/dist/src/utils/skill-validation.js +1 -1
  87. package/dist/src/utils/skill-validation.js.map +1 -1
  88. package/dist/src/utils/state-paths.d.ts +1 -0
  89. package/dist/src/utils/state-paths.js +5 -0
  90. package/dist/src/utils/state-paths.js.map +1 -1
  91. package/package.json +3 -4
  92. package/dist/src/utils/plugin-authoring-capabilities.d.ts +0 -23
  93. package/dist/src/utils/plugin-authoring-capabilities.js +0 -37
  94. package/dist/src/utils/plugin-authoring-capabilities.js.map +0 -1
  95. package/dist/src/utils/plugin-authoring-capabilities.json +0 -92
  96. package/dist/src/utils/skill-frontmatter-capabilities.d.ts +0 -201
  97. package/dist/src/utils/skill-frontmatter-capabilities.js +0 -5
  98. package/dist/src/utils/skill-frontmatter-capabilities.js.map +0 -1
  99. package/dist/src/utils/skill-frontmatter-capabilities.json +0 -200
@@ -1,6 +1,19 @@
1
1
  import crypto from 'node:crypto';
2
2
  import { buildTuiViewModel, hasInstalledPlacement, } from './model.js';
3
+ import { safeDiagnosticCode } from './diagnostics.js';
3
4
  import { TuiRouter, } from './router.js';
5
+ function reviewPreparationStageLabel(stage) {
6
+ return {
7
+ validate: '验证当前状态',
8
+ status: '读取安装状态',
9
+ catalog: '解析 Catalog',
10
+ inventory: '检查本地现场',
11
+ dependencyPlan: '准备依赖计划',
12
+ config: '验证设置',
13
+ portable: '准备 Portable Skills',
14
+ evidence: '准备确认信息',
15
+ }[stage];
16
+ }
4
17
  export class TuiOperationBlockedError extends Error {
5
18
  code;
6
19
  constructor(code, message) {
@@ -58,6 +71,14 @@ function operationLabels(intent) {
58
71
  }
59
72
  if (intent.kind === 'pluginWrite') {
60
73
  if (intent.originIssue) {
74
+ if (intent.action === 'cleanup') {
75
+ return {
76
+ title: '清理已安装内容',
77
+ description: '只清理可证明属于 AW 的现场内容;保留插件设置;不运行 uninstall lifecycle。',
78
+ primaryLabel: '开始清理',
79
+ destructive: true,
80
+ };
81
+ }
61
82
  return {
62
83
  title: '处理问题',
63
84
  description: '只执行所选问题处理动作;插件设置保持不变。',
@@ -310,7 +331,7 @@ export function extractPortableError(error) {
310
331
  : undefined;
311
332
  const message = error instanceof Error ? error.message : String(error);
312
333
  const fallback = message.match(/^([A-Za-z][A-Za-z0-9]*):/u)?.[1];
313
- const diagnosticCode = typedCode ?? fallback ?? 'portableValidationFailed';
334
+ const diagnosticCode = safeDiagnosticCode(typedCode ?? fallback, 'portableValidationFailed');
314
335
  if (diagnosticCode === 'requiredConfigMissing')
315
336
  return { code: 'requiredConfigMissing', diagnosticCode };
316
337
  if (diagnosticCode === 'portableSkillsCliMissing')
@@ -340,6 +361,8 @@ export class TuiController {
340
361
  #progress = { messages: [] };
341
362
  #progressTargets = [];
342
363
  #activeReview;
364
+ #reviewPreparation;
365
+ #reviewPreparationToken = 0;
343
366
  #result;
344
367
  #loadError;
345
368
  #portableDraft;
@@ -390,6 +413,7 @@ export class TuiController {
390
413
  progress: this.#progress,
391
414
  progressTargets: this.#progressTargets,
392
415
  ...(this.#activeReview ? { activeReview: this.#activeReview } : {}),
416
+ ...(this.#reviewPreparation ? { reviewPreparation: this.#reviewPreparation.snapshot } : {}),
393
417
  ...(this.#result ? { result: this.#result } : {}),
394
418
  ...(this.#loadError ? { loadError: this.#loadError } : {}),
395
419
  ...(this.#portableDraft ? { portableDraft: this.#portableDraft } : {}),
@@ -421,6 +445,7 @@ export class TuiController {
421
445
  this.#viewModel = buildTuiViewModel({
422
446
  status: this.#status.payload,
423
447
  pluginSummaries: this.#status.pluginSummaries,
448
+ targetRevisions: this.#status.targetRevisions,
424
449
  sourceHealth: this.#health,
425
450
  sourceDetails: this.#status.sourceDetails,
426
451
  dependencyOverviews: this.#status.dependencyOverviews,
@@ -441,7 +466,7 @@ export class TuiController {
441
466
  || route.name === 'portableBlocker'
442
467
  || route.name === 'pluginDependencies'
443
468
  || route.name === 'installPlacement'
444
- || route.name === 'installTools'
469
+ || route.name === 'installTargets'
445
470
  || route.name === 'pluginDetail'
446
471
  || route.name === 'platform'
447
472
  || route.name === 'pluginAbout'
@@ -449,13 +474,12 @@ export class TuiController {
449
474
  || route.name === 'configLayer'
450
475
  || route.name === 'configFields'
451
476
  || route.name === 'configFieldEditor'
452
- || route.name === 'configPresets'
453
477
  || route.name === 'configUnavailable';
454
478
  if (pluginRoute && !this.#viewModel?.plugins.some((plugin) => plugin.pluginId === route.pluginId)) {
455
479
  return {
456
480
  name: 'pluginList',
457
481
  mode: route.name === 'pluginPreview' || route.name === 'pluginDependencies'
458
- || route.name === 'installPlacement' || route.name === 'installTools'
482
+ || route.name === 'installPlacement' || route.name === 'installTargets'
459
483
  ? 'browse'
460
484
  : 'installed',
461
485
  };
@@ -685,6 +709,7 @@ export class TuiController {
685
709
  this.#publish();
686
710
  }
687
711
  push(route, context) {
712
+ this.#cancelReviewPreparation();
688
713
  this.#cancelPortableRequest();
689
714
  this.#discardPortableDraftForRoute(route);
690
715
  this.#router.push(route, context);
@@ -818,6 +843,16 @@ export class TuiController {
818
843
  this.#publish();
819
844
  }
820
845
  async buildPortableInstallReview() {
846
+ const intent = this.#portableInstallIntent();
847
+ const draft = this.#portableDraft;
848
+ const expectedRoute = this.#router.snapshot.current.route;
849
+ return this.#runPortableRequest(`review:${JSON.stringify(intent)}`, async (signal, token) => {
850
+ const evidence = await this.dependencies.captureEvidence(structuredClone(intent), signal);
851
+ this.#assertPortableRequestCurrent(token, expectedRoute);
852
+ return this.#storeOperationReview(intent, evidence, 'push', this.#router.portableOrigin(draft.pluginId));
853
+ });
854
+ }
855
+ #portableInstallIntent() {
821
856
  const draft = this.#portableDraft;
822
857
  if (!draft?.canContinue) {
823
858
  throw new TuiOperationBlockedError('portableNoSkillsSelected', '至少选择一个 Skill。');
@@ -826,19 +861,17 @@ export class TuiController {
826
861
  if (unavailable.length > 0) {
827
862
  throw new TuiOperationBlockedError('portableScopeUnavailable', `所选范围不提供这些 Skill:${unavailable.map((candidate) => candidate.name).join(', ')}。请返回 Skill 选择或更改范围。`);
828
863
  }
829
- const intent = {
864
+ return {
830
865
  kind: 'portableInstall',
831
866
  pluginId: draft.pluginId,
832
867
  scope: draft.scope,
833
868
  selectedSkills: draft.candidates.filter((candidate) => candidate.selected).map((candidate) => candidate.name),
834
869
  ...(draft.agentMode === 'explicit' ? { requestedAgents: draft.requestedAgents } : {}),
835
870
  };
836
- const expectedRoute = this.#router.snapshot.current.route;
837
- return this.#runPortableRequest(`review:${JSON.stringify(intent)}`, async (signal, token) => {
838
- const evidence = await this.dependencies.captureEvidence(structuredClone(intent), signal);
839
- this.#assertPortableRequestCurrent(token, expectedRoute);
840
- return this.#storeOperationReview(intent, evidence, 'push', this.#router.portableOrigin(draft.pluginId));
841
- });
871
+ }
872
+ startPortableInstallReviewPreparation() {
873
+ const intent = this.#portableInstallIntent();
874
+ return this.startReviewPreparation(intent, this.#router.portableOrigin(intent.pluginId));
842
875
  }
843
876
  discardPortableDraft() {
844
877
  this.#cancelPortableRequest();
@@ -846,6 +879,7 @@ export class TuiController {
846
879
  this.#publish();
847
880
  }
848
881
  replace(route, context) {
882
+ this.#cancelReviewPreparation();
849
883
  this.#cancelPortableRequest();
850
884
  const current = this.#router.snapshot.current.route;
851
885
  if (current.name === 'portableBlocker' && current.code === 'requiredConfigMissing'
@@ -856,11 +890,24 @@ export class TuiController {
856
890
  this.#router.replace(route, context);
857
891
  this.#publish();
858
892
  }
893
+ beginConfigFlow(pluginId) {
894
+ this.#router.beginConfigFlow(pluginId);
895
+ this.#router.push({
896
+ name: 'configFields',
897
+ pluginId,
898
+ layer: { scope: 'effective', projectLayer: 'shared' },
899
+ });
900
+ this.#publish();
901
+ }
902
+ configOrigin(pluginId) {
903
+ return this.#router.configOrigin(pluginId);
904
+ }
859
905
  updatePageContext(context) {
860
906
  this.#router.updateContext(context);
861
907
  this.#publish();
862
908
  }
863
909
  back() {
910
+ this.#cancelReviewPreparation();
864
911
  this.#cancelPortableRequest();
865
912
  const route = this.#router.snapshot.current.route;
866
913
  if (route.name === 'review' && this.#activeReview?.operationId === route.operationId) {
@@ -892,6 +939,14 @@ export class TuiController {
892
939
  this.resetPortableFlowToPreview(this.#result.pluginId, target, preserveDraft);
893
940
  return;
894
941
  }
942
+ if (this.#result?.state === 'succeeded'
943
+ && (this.#result.operationKind === 'configSave' || this.#result.operationKind === 'configReset')
944
+ && this.#result.pluginId
945
+ && this.#router.resetConfigFlow(this.#result.pluginId, target)) {
946
+ this.#result = undefined;
947
+ this.#publish();
948
+ return;
949
+ }
895
950
  const route = target ?? (this.#result && 'returnRoute' in this.#result
896
951
  ? this.#result.returnRoute
897
952
  : undefined);
@@ -911,8 +966,10 @@ export class TuiController {
911
966
  || route.name === 'portableAgentInput'
912
967
  || route.name === 'portableScope'
913
968
  || route.name === 'portableBlocker';
914
- const portableOperationRoute = (route.name === 'review' || route.name === 'progress' || route.name === 'result')
915
- && (this.#activeReview?.operationKind === 'portableInstall'
969
+ const portableOperationRoute = (route.name === 'reviewPreparing' || route.name === 'review'
970
+ || route.name === 'progress' || route.name === 'result')
971
+ && (this.#reviewPreparation?.snapshot.intent.kind === 'portableInstall'
972
+ || this.#activeReview?.operationKind === 'portableInstall'
916
973
  || this.#result?.operationKind === 'portableInstall');
917
974
  if (!portableRoute && !portableOperationRoute) {
918
975
  this.#cancelPortableRequest();
@@ -970,6 +1027,7 @@ export class TuiController {
970
1027
  const query = options.query?.trim().toLocaleLowerCase('en');
971
1028
  return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
972
1029
  && (options.mode === 'browse' || hasInstalledPlacement(plugin))
1030
+ && (options.installFilter !== 'uninstalled' || !hasInstalledPlacement(plugin))
973
1031
  && (!query || `${plugin.displayName}\n${plugin.description}\n${plugin.sourceId}`.toLocaleLowerCase('en').includes(query))));
974
1032
  }
975
1033
  getPluginOverview(pluginId) {
@@ -997,6 +1055,64 @@ export class TuiController {
997
1055
  getPlatformPlacement(pluginId, platform) {
998
1056
  return this.getPluginOverview(pluginId)?.installedPlacements.find((placement) => placement.platform === platform);
999
1057
  }
1058
+ listInstallTargets(pluginId, scope) {
1059
+ const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
1060
+ if (!plugin)
1061
+ return [];
1062
+ if (plugin.aggregateStatus === 'unavailable') {
1063
+ return plugin.supportedPlatforms.map((platform) => ({
1064
+ platform,
1065
+ compatible: false,
1066
+ disabled: '插件来源不可用',
1067
+ }));
1068
+ }
1069
+ return plugin.supportedPlatforms.map((platform) => {
1070
+ const placement = plugin.installedPlacements.find((entry) => entry.platform === platform);
1071
+ if (!placement)
1072
+ return { platform, compatible: false, disabled: '当前没有可信的平台能力信息' };
1073
+ if (placement.state === 'scopeConflict') {
1074
+ return { platform, compatible: false, disabled: '安装位置冲突;请先处理问题' };
1075
+ }
1076
+ if (placement.state === 'installed') {
1077
+ return {
1078
+ platform,
1079
+ compatible: false,
1080
+ disabled: `已安装到${placement.scope === 'user' ? '所有项目' : '当前项目'};请先移除现有安装`,
1081
+ };
1082
+ }
1083
+ if (!placement.supportedScopes.includes(scope)) {
1084
+ return { platform, compatible: false, disabled: '不支持所选位置' };
1085
+ }
1086
+ const overview = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId);
1087
+ for (const dependency of overview?.dependencies ?? []) {
1088
+ const dependentPlugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === dependency.pluginId);
1089
+ if (!dependentPlugin || !dependentPlugin.supportedPlatforms.includes(platform)) {
1090
+ return {
1091
+ platform,
1092
+ compatible: false,
1093
+ disabled: `${dependency.displayName} 不支持${platform === 'codex' ? 'Codex' : 'Claude Code'}`,
1094
+ };
1095
+ }
1096
+ const dependencyPlacement = dependentPlugin.installedPlacements.find((entry) => entry.platform === platform);
1097
+ if (dependencyPlacement?.state === 'scopeConflict') {
1098
+ return { platform, compatible: false, disabled: `${dependency.displayName} 的安装位置冲突` };
1099
+ }
1100
+ if (dependencyPlacement?.state === 'installed') {
1101
+ if (dependencyPlacement.health !== 'ready') {
1102
+ return { platform, compatible: false, disabled: `${dependency.displayName} 需要先处理` };
1103
+ }
1104
+ if (scope === 'user' && dependencyPlacement.scope === 'project') {
1105
+ return { platform, compatible: false, disabled: `${dependency.displayName} 只在当前项目可用` };
1106
+ }
1107
+ }
1108
+ if (dependencyPlacement?.state === 'notInstalled'
1109
+ && !dependencyPlacement.supportedScopes.includes(scope)) {
1110
+ return { platform, compatible: false, disabled: `${dependency.displayName} 不支持所选位置` };
1111
+ }
1112
+ }
1113
+ return { platform, compatible: true };
1114
+ });
1115
+ }
1000
1116
  getTargetState(pluginId, platform, scope) {
1001
1117
  return this.#status?.payload.targets.find((target) => target.pluginId === pluginId
1002
1118
  && target.platform === platform
@@ -1044,16 +1160,8 @@ export class TuiController {
1044
1160
  }
1045
1161
  async buildOperationReview(intent, returnRoute) {
1046
1162
  try {
1047
- this.#assertIssueOrigin(intent);
1048
- if (intent.kind === 'pluginWrite'
1049
- && (intent.action === 'uninstall' || intent.action === 'cleanup')
1050
- && intent.force !== true
1051
- && (intent.platform === 'both' ? ['codex', 'claude'] : [intent.platform])
1052
- .some((platform) => this.getBlockingDependents(intent.pluginId, platform, intent.scope).length > 0)) {
1053
- throw new TuiOperationBlockedError('dependencyStillRequired', '仍有已安装插件依赖这个插件。请先查看完整依赖关系。');
1054
- }
1055
- await this.dependencies.validateIntent?.(structuredClone(intent), this.#abort.signal);
1056
- const evidence = await this.dependencies.captureEvidence(structuredClone(intent), this.#abort.signal);
1163
+ this.#validateReviewIntent(intent);
1164
+ const evidence = await this.#prepareReviewEvidence(intent, this.#abort.signal);
1057
1165
  return this.#storeOperationReview(intent, evidence, 'push', returnRoute);
1058
1166
  }
1059
1167
  catch (error) {
@@ -1062,6 +1170,142 @@ export class TuiController {
1062
1170
  throw new TuiOperationBlockedError(code, '操作未建立。请检查当前设置后重试。');
1063
1171
  }
1064
1172
  }
1173
+ startReviewPreparation(intent, returnRoute) {
1174
+ const inheritedReturnRoute = this.#reviewPreparation?.returnRoute;
1175
+ const current = this.#router.snapshot.current.route;
1176
+ const navigation = current.name === 'result' || current.name === 'reviewPreparing'
1177
+ ? 'replace' : 'push';
1178
+ const resolvedReturnRoute = returnRoute
1179
+ ?? (current.name === 'reviewPreparing' ? inheritedReturnRoute : undefined)
1180
+ ?? (current.name === 'result' && this.#result
1181
+ && 'returnRoute' in this.#result && this.#result.returnRoute
1182
+ ? this.#result.returnRoute
1183
+ : current);
1184
+ const normalizedReturnRoute = this.#normalizeRoute(resolvedReturnRoute);
1185
+ const key = JSON.stringify(stableValue({ intent, returnRoute: normalizedReturnRoute }));
1186
+ if (this.#reviewPreparation?.snapshot.state === 'running'
1187
+ && this.#reviewPreparation.key === key) {
1188
+ return cloneFrozen(this.#reviewPreparation.snapshot);
1189
+ }
1190
+ this.#cancelReviewPreparation();
1191
+ const id = crypto.randomUUID();
1192
+ const abort = new AbortController();
1193
+ const token = ++this.#reviewPreparationToken;
1194
+ const snapshot = {
1195
+ id,
1196
+ intent: structuredClone(intent),
1197
+ state: 'running',
1198
+ stages: this.dependencies.prepareReviewEvidence ? [] : [
1199
+ { id: 'validate', label: '验证当前状态', state: 'running' },
1200
+ { id: 'evidence', label: '准备确认信息', state: 'pending' },
1201
+ ],
1202
+ currentLabel: this.#reviewPreparationLabel(intent),
1203
+ };
1204
+ this.#reviewPreparation = {
1205
+ snapshot,
1206
+ key,
1207
+ returnRoute: structuredClone(normalizedReturnRoute),
1208
+ abort,
1209
+ token,
1210
+ };
1211
+ this.#result = undefined;
1212
+ this.#router[navigation]({ name: 'reviewPreparing', preparationId: id });
1213
+ this.#publish();
1214
+ queueMicrotask(() => void this.#runReviewPreparation(token));
1215
+ return cloneFrozen(snapshot);
1216
+ }
1217
+ retryReviewPreparation() {
1218
+ const preparation = this.#reviewPreparation;
1219
+ if (!preparation || preparation.snapshot.state !== 'failed')
1220
+ return undefined;
1221
+ return this.startReviewPreparation(preparation.snapshot.intent, preparation.returnRoute);
1222
+ }
1223
+ #validateReviewIntent(intent) {
1224
+ this.#assertIssueOrigin(intent);
1225
+ if (intent.kind === 'pluginWrite'
1226
+ && (intent.action === 'uninstall' || intent.action === 'cleanup')
1227
+ && intent.force !== true
1228
+ && (intent.platform === 'both' ? ['codex', 'claude'] : [intent.platform])
1229
+ .some((platform) => this.getBlockingDependents(intent.pluginId, platform, intent.scope).length > 0)) {
1230
+ throw new TuiOperationBlockedError('dependencyStillRequired', '仍有已安装插件依赖这个插件。请先查看完整依赖关系。');
1231
+ }
1232
+ }
1233
+ #reviewPreparationLabel(intent) {
1234
+ if ('pluginId' in intent) {
1235
+ return this.#viewModel?.plugins.find((plugin) => plugin.pluginId === intent.pluginId)?.displayName
1236
+ ?? pluginNameFromId(intent.pluginId);
1237
+ }
1238
+ return intent.sourceId;
1239
+ }
1240
+ #reviewPreparationIsCurrent(token) {
1241
+ const preparation = this.#reviewPreparation;
1242
+ return Boolean(preparation && preparation.token === token
1243
+ && !preparation.abort.signal.aborted
1244
+ && !this.#disposed
1245
+ && this.#router.snapshot.current.route.name === 'reviewPreparing'
1246
+ && this.#router.snapshot.current.route.preparationId === preparation.snapshot.id);
1247
+ }
1248
+ async #runReviewPreparation(token) {
1249
+ const preparation = this.#reviewPreparation;
1250
+ if (!preparation || preparation.token !== token)
1251
+ return;
1252
+ const intent = structuredClone(preparation.snapshot.intent);
1253
+ const signal = AbortSignal.any([this.#abort.signal, preparation.abort.signal]);
1254
+ try {
1255
+ this.#validateReviewIntent(intent);
1256
+ const evidence = await this.#prepareReviewEvidence(intent, signal, (stage) => {
1257
+ if (!this.#reviewPreparationIsCurrent(token))
1258
+ return;
1259
+ const existing = preparation.snapshot.stages.find((entry) => entry.id === stage);
1260
+ preparation.snapshot.stages = preparation.snapshot.stages.map((entry) => entry.state === 'running' ? { ...entry, state: 'completed' } : entry);
1261
+ if (existing) {
1262
+ preparation.snapshot.stages = preparation.snapshot.stages.map((entry) => entry.id === stage ? { ...entry, state: 'running' } : entry);
1263
+ }
1264
+ else {
1265
+ preparation.snapshot.stages.push({
1266
+ id: stage,
1267
+ label: reviewPreparationStageLabel(stage),
1268
+ state: 'running',
1269
+ });
1270
+ }
1271
+ this.#publish();
1272
+ });
1273
+ if (!this.#reviewPreparationIsCurrent(token))
1274
+ return;
1275
+ preparation.snapshot.stages = preparation.snapshot.stages.map((stage) => stage.state === 'running' ? { ...stage, state: 'completed' } : stage);
1276
+ this.#publish();
1277
+ const returnRoute = structuredClone(preparation.returnRoute);
1278
+ this.#reviewPreparation = undefined;
1279
+ this.#storeOperationReview(intent, evidence, 'replace', returnRoute);
1280
+ }
1281
+ catch (error) {
1282
+ if (!this.#reviewPreparationIsCurrent(token))
1283
+ return;
1284
+ const active = this.#reviewPreparation;
1285
+ active.snapshot.state = 'failed';
1286
+ active.snapshot.stages = active.snapshot.stages.map((stage) => stage.state === 'running' ? { ...stage, state: 'failed' } : stage);
1287
+ active.snapshot.code = 'reviewPreparationFailed';
1288
+ active.snapshot.message = '无法准备确认信息。请返回检查选择,或重试。';
1289
+ this.#publish();
1290
+ await this.#logOperationError(error, issueCode(error), intent);
1291
+ }
1292
+ }
1293
+ async #prepareReviewEvidence(intent, signal, onStage = () => undefined) {
1294
+ if (this.dependencies.prepareReviewEvidence) {
1295
+ return this.dependencies.prepareReviewEvidence(structuredClone(intent), signal, onStage);
1296
+ }
1297
+ await onStage('validate');
1298
+ await this.dependencies.validateIntent?.(structuredClone(intent), signal);
1299
+ await onStage('evidence');
1300
+ return this.dependencies.captureEvidence(structuredClone(intent), signal);
1301
+ }
1302
+ #cancelReviewPreparation() {
1303
+ if (!this.#reviewPreparation)
1304
+ return;
1305
+ this.#reviewPreparationToken += 1;
1306
+ this.#reviewPreparation.abort.abort();
1307
+ this.#reviewPreparation = undefined;
1308
+ }
1065
1309
  async #logOperationError(error, code, intent) {
1066
1310
  await this.#logErrorSafe(error, code, {
1067
1311
  privateConfigOperation: intent.kind === 'configSave' || intent.kind === 'configReset'
@@ -1119,8 +1363,8 @@ export class TuiController {
1119
1363
  },
1120
1364
  changes: reviewChanges(intent, evidence, 'pluginId' in intent ? this.getPluginDependencyOverview(intent.pluginId) : undefined),
1121
1365
  technicalDetails: technicalDetails(evidence),
1122
- ...(intent.kind === 'pluginWrite' && intent.action === 'cleanup' && intent.force === true
1123
- ? { equivalentCommand: `aw cleanup ${intent.pluginId} --platform ${intent.platform} --scope ${intent.scope} --force` }
1366
+ ...(intent.kind === 'pluginWrite' && intent.action === 'cleanup'
1367
+ ? { equivalentCommand: `aw cleanup ${intent.pluginId} --platform ${intent.platform} --scope ${intent.scope}${intent.force === true ? ' --force' : ''}` }
1124
1368
  : {}),
1125
1369
  primaryLabel: intent.kind === 'pluginWrite' && evidence.dependencyPlan
1126
1370
  ? `${labels.primaryLabel} ${evidence.dependencyPlan.resolvedPluginIds.length} 个插件`
@@ -1293,19 +1537,26 @@ export class TuiController {
1293
1537
  const pluginSummary = ticket.intent.kind === 'pluginWrite'
1294
1538
  ? operationResultSummary(ticket.intent, execution)
1295
1539
  : undefined;
1540
+ const hasFailedDependencyTarget = pluginSummary?.targets?.some((target) => target.relation !== undefined && (target.state === 'failed' || target.state === 'skipped'));
1541
+ const hasPreservedTarget = pluginSummary?.targets?.some((target) => target.state === 'completed' || target.state === 'kept');
1542
+ const dependencyPartial = pluginSummary?.code === 'operationPartial'
1543
+ && Boolean(hasFailedDependencyTarget && hasPreservedTarget);
1296
1544
  const dependencyFailure = pluginSummary?.code === 'operationFailed'
1297
- && pluginSummary.targets?.some((target) => target.relation !== undefined
1298
- && (target.state === 'failed' || target.state === 'skipped'));
1299
- if (pluginSummary?.code === 'operationFailed' && !dependencyFailure) {
1300
- return this.#setBlocked(operationId, 'operationFailed', {
1545
+ && Boolean(hasFailedDependencyTarget);
1546
+ if (pluginSummary?.code === 'operationFailed') {
1547
+ const unsuccessfulTargets = pluginSummary.targets?.filter((target) => target.state === 'failed' || target.state === 'skipped') ?? [];
1548
+ const dependencyStillRequired = unsuccessfulTargets.length > 0
1549
+ && unsuccessfulTargets.every((target) => target.errorCode === 'dependencyStillRequired');
1550
+ return this.#setBlocked(operationId, dependencyStillRequired ? 'dependencyStillRequired' : 'operationFailed', {
1301
1551
  intent: ticket.intent,
1302
1552
  navigation: 'result',
1303
1553
  returnRoute: ticket.returnRoute,
1304
- writeMayHaveCompleted: true,
1554
+ ...(dependencyFailure || dependencyStillRequired ? { summary: pluginSummary } : {}),
1555
+ ...(!dependencyFailure && !dependencyStillRequired ? { writeMayHaveCompleted: true } : {}),
1305
1556
  });
1306
1557
  }
1307
1558
  const originIssue = ticket.intent.kind === 'pluginWrite' ? ticket.intent.originIssue : undefined;
1308
- if (originIssue && !dependencyFailure
1559
+ if (originIssue && !dependencyPartial
1309
1560
  && this.#viewModel?.issues.some((issue) => userIssueKey(issue) === originIssue.issueKey)) {
1310
1561
  return this.#setBlocked(operationId, 'issueStillPresent', {
1311
1562
  intent: ticket.intent,
@@ -1326,7 +1577,7 @@ export class TuiController {
1326
1577
  });
1327
1578
  }
1328
1579
  }
1329
- const baseSummary = dependencyFailure && pluginSummary
1580
+ const baseSummary = dependencyPartial && pluginSummary
1330
1581
  ? { ...pluginSummary, code: 'operationPartial', message: '依赖安装没有全部完成。' }
1331
1582
  : pluginSummary ?? operationResultSummary(ticket.intent, execution);
1332
1583
  const summary = {
@@ -1538,6 +1789,7 @@ export class TuiController {
1538
1789
  ...(options?.returnRoute ? { returnRoute: structuredClone(this.#normalizeRoute(options.returnRoute)) } : {}),
1539
1790
  ...(options?.writeMayHaveCompleted ? { writeMayHaveCompleted: true } : {}),
1540
1791
  ...(options?.verification ? { verification: options.verification } : {}),
1792
+ ...(options?.summary ? { summary: structuredClone(options.summary) } : {}),
1541
1793
  };
1542
1794
  this.#result = structuredClone(result);
1543
1795
  this.#activeReview = undefined;
@@ -1587,13 +1839,17 @@ export class TuiController {
1587
1839
  : origin.actionId === 'remove'
1588
1840
  ? { action: 'uninstall', scope: issue.scope }
1589
1841
  : undefined
1590
- : issue.category === 'scopeConflict'
1591
- ? origin.actionId === 'keepUserScope'
1592
- ? { action: 'uninstall', scope: 'project' }
1593
- : origin.actionId === 'keepProjectScope'
1594
- ? { action: 'uninstall', scope: 'user' }
1595
- : undefined
1596
- : undefined;
1842
+ : issue.category === 'sourceUnavailable'
1843
+ ? origin.actionId === 'cleanup' && action.writeAction === 'cleanup'
1844
+ ? { action: 'cleanup', scope: issue.scope }
1845
+ : undefined
1846
+ : issue.category === 'scopeConflict'
1847
+ ? origin.actionId === 'keepUserScope'
1848
+ ? { action: 'uninstall', scope: 'project' }
1849
+ : origin.actionId === 'keepProjectScope'
1850
+ ? { action: 'uninstall', scope: 'user' }
1851
+ : undefined
1852
+ : undefined;
1597
1853
  const dependencyOriginValid = issue.category !== 'dependencyMissing' && issue.category !== 'dependencyNotReady'
1598
1854
  || (origin.consumerPluginId === issue.pluginId
1599
1855
  && origin.consumerSourceIdentity === issue.sourceIdentity
@@ -1612,6 +1868,7 @@ export class TuiController {
1612
1868
  if (this.#disposed)
1613
1869
  return;
1614
1870
  this.#disposed = true;
1871
+ this.#cancelReviewPreparation();
1615
1872
  this.#cancelPortableRequest();
1616
1873
  this.#abort.abort();
1617
1874
  this.#cancelSourceRefreshes();
@@ -1644,6 +1901,7 @@ function operationResultSummary(intent, execution) {
1644
1901
  requestedAgents: receipt.requestedAgents,
1645
1902
  scope: receipt.scope,
1646
1903
  materializationDigest: receipt.materializationDigest,
1904
+ skillsCliVersion: receipt.skillsCliVersion,
1647
1905
  missing: receipt.verification.missing,
1648
1906
  verificationUnknown,
1649
1907
  ...(receipt.failures[0]?.recovery ? { recovery: receipt.failures[0].recovery } : {}),
@@ -1691,6 +1949,9 @@ function operationResultSummary(intent, execution) {
1691
1949
  relation: dependencyRelation(executionPlan, planned.pluginId),
1692
1950
  requiredBy: planned.requiredBy,
1693
1951
  } : {}),
1952
+ ...(entry.errorCode === 'dependencyStillRequired'
1953
+ ? { errorCode: 'dependencyStillRequired' }
1954
+ : {}),
1694
1955
  }];
1695
1956
  });
1696
1957
  const inferredTargets = intent.kind === 'pluginWrite' && targets.length === 0