@lesliechan721/aw 0.1.0-beta.13 → 0.1.0-beta.15

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 (66) hide show
  1. package/dist/src/commands/catalog-dev-source.d.ts +31 -0
  2. package/dist/src/commands/catalog-dev-source.js +165 -5
  3. package/dist/src/commands/catalog-dev-source.js.map +1 -1
  4. package/dist/src/commands/catalog-generation.d.ts +5 -0
  5. package/dist/src/commands/catalog-generation.js.map +1 -1
  6. package/dist/src/commands/catalog-publication-journal.d.ts +12 -0
  7. package/dist/src/commands/catalog-publication-journal.js +77 -0
  8. package/dist/src/commands/catalog-publication-journal.js.map +1 -1
  9. package/dist/src/commands/catalog-store.d.ts +5 -0
  10. package/dist/src/commands/catalog-store.js +19 -1
  11. package/dist/src/commands/catalog-store.js.map +1 -1
  12. package/dist/src/commands/dependency-operation-plan.d.ts +7 -0
  13. package/dist/src/commands/dependency-operation-plan.js +63 -27
  14. package/dist/src/commands/dependency-operation-plan.js.map +1 -1
  15. package/dist/src/commands/rebuild-command.d.ts +22 -8
  16. package/dist/src/commands/rebuild-command.js +562 -250
  17. package/dist/src/commands/rebuild-command.js.map +1 -1
  18. package/dist/src/commands/reconcile-operation-plan.d.ts +11 -9
  19. package/dist/src/commands/reconcile-operation-plan.js +25 -6
  20. package/dist/src/commands/reconcile-operation-plan.js.map +1 -1
  21. package/dist/src/commands/status-v6.d.ts +2 -1
  22. package/dist/src/commands/status-v6.js +7 -1
  23. package/dist/src/commands/status-v6.js.map +1 -1
  24. package/dist/src/commands/write-commands.d.ts +2 -1
  25. package/dist/src/commands/write-commands.js +24 -1
  26. package/dist/src/commands/write-commands.js.map +1 -1
  27. package/dist/src/tui/catalog-management-screens.d.ts +45 -0
  28. package/dist/src/tui/catalog-management-screens.js +25 -0
  29. package/dist/src/tui/catalog-management-screens.js.map +1 -0
  30. package/dist/src/tui/catalog-management.d.ts +240 -0
  31. package/dist/src/tui/catalog-management.js +856 -0
  32. package/dist/src/tui/catalog-management.js.map +1 -0
  33. package/dist/src/tui/catalog-sources/session.d.ts +5 -0
  34. package/dist/src/tui/catalog-sources/session.js +49 -1
  35. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  36. package/dist/src/tui/catalog-sources/types.d.ts +11 -0
  37. package/dist/src/tui/console-app-v4.d.ts +1 -1
  38. package/dist/src/tui/console-app-v4.js +2597 -587
  39. package/dist/src/tui/console-app-v4.js.map +1 -1
  40. package/dist/src/tui/controller-runtime.js +668 -50
  41. package/dist/src/tui/controller-runtime.js.map +1 -1
  42. package/dist/src/tui/controller.d.ts +136 -7
  43. package/dist/src/tui/controller.js +752 -19
  44. package/dist/src/tui/controller.js.map +1 -1
  45. package/dist/src/tui/dependency-graph.d.ts +67 -0
  46. package/dist/src/tui/dependency-graph.js +399 -0
  47. package/dist/src/tui/dependency-graph.js.map +1 -0
  48. package/dist/src/tui/lifecycle-action-view.js +4 -2
  49. package/dist/src/tui/lifecycle-action-view.js.map +1 -1
  50. package/dist/src/tui/model.d.ts +37 -0
  51. package/dist/src/tui/model.js +163 -7
  52. package/dist/src/tui/model.js.map +1 -1
  53. package/dist/src/tui/operation-review-planner.js +266 -9
  54. package/dist/src/tui/operation-review-planner.js.map +1 -1
  55. package/dist/src/tui/plugin-write-task.d.ts +67 -0
  56. package/dist/src/tui/plugin-write-task.js +282 -0
  57. package/dist/src/tui/plugin-write-task.js.map +1 -0
  58. package/dist/src/tui/router.d.ts +39 -2
  59. package/dist/src/tui/router.js +13 -5
  60. package/dist/src/tui/router.js.map +1 -1
  61. package/dist/src/tui/screen-registry.d.ts +125 -6
  62. package/dist/src/tui/screen-registry.js +35 -4
  63. package/dist/src/tui/screen-registry.js.map +1 -1
  64. package/dist/src/utils/catalog-generation-service.d.ts +4 -0
  65. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  66. package/package.json +3 -3
@@ -1,6 +1,8 @@
1
1
  import crypto from 'node:crypto';
2
+ import { CatalogManagementError, } from './catalog-management.js';
2
3
  import { CatalogGenerationError } from '../utils/catalog-generation-error.js';
3
4
  import { readSafeOperationError } from '../utils/safe-operation-diagnostic.js';
5
+ import { statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, } from '../commands/status-v6.js';
4
6
  import { buildRebuildReceiptJson, requestedIntentAction } from '../output/command-receipt.js';
5
7
  import { buildOperationResultModel } from '../commands/operation-planner.js';
6
8
  import { buildOperationReviewProjection } from './operation-review-planner.js';
@@ -13,6 +15,7 @@ import { TuiRouter, } from './router.js';
13
15
  import { normalizeTuiRoute } from './screen-registry.js';
14
16
  import { CatalogSourcesSession } from './catalog-sources/session.js';
15
17
  import { catalogSourceAutoCheckOnlyValue, projectCatalogSourceMutationWarnings, } from './catalog-sources/review.js';
18
+ import { buildPluginWriteTaskCandidates, pluginWriteTaskTargetKey, } from './plugin-write-task.js';
16
19
  export const PLUGIN_WRITE_BATCH_ACTIONS = [
17
20
  'install',
18
21
  'sync',
@@ -20,6 +23,57 @@ export const PLUGIN_WRITE_BATCH_ACTIONS = [
20
23
  'uninstall',
21
24
  'cleanup',
22
25
  ];
26
+ export function pluginWriteBatchTargets(action, pluginIds, statusTargets) {
27
+ const selected = new Set(pluginIds);
28
+ const targets = [];
29
+ const add = (target, scope) => {
30
+ targets.push({ pluginId: target.pluginId, platform: target.platform, scope });
31
+ };
32
+ for (const target of statusTargets) {
33
+ if (!selected.has(target.pluginId))
34
+ continue;
35
+ const intent = statusTargetIntent(target);
36
+ const ownership = statusTargetOwnershipState(target);
37
+ const activation = statusTargetActivation(target);
38
+ const hasManagedOutput = ownership !== 'absent';
39
+ const hasCleanupEvidence = ownership === 'present' || ownership === 'partial'
40
+ || intent.relation === 'managed';
41
+ if (action === 'sync' || action === 'reinstall') {
42
+ intent.scopes.forEach((scope) => add(target, scope));
43
+ if (hasManagedOutput)
44
+ add(target, target.scope);
45
+ if (target.platform === 'codex') {
46
+ if (activation?.user === true)
47
+ add(target, 'user');
48
+ if (activation?.project === true)
49
+ add(target, 'project');
50
+ }
51
+ }
52
+ else if (action === 'uninstall') {
53
+ if (hasManagedOutput)
54
+ add(target, target.scope);
55
+ if (target.platform === 'codex') {
56
+ if (activation?.user === true)
57
+ add(target, 'user');
58
+ if (activation?.project === true)
59
+ add(target, 'project');
60
+ }
61
+ }
62
+ else if (hasCleanupEvidence) {
63
+ add(target, target.scope);
64
+ if (target.platform === 'codex') {
65
+ if (activation?.user === true)
66
+ add(target, 'user');
67
+ if (activation?.project === true)
68
+ add(target, 'project');
69
+ }
70
+ }
71
+ }
72
+ return [...new Map(targets.map((target) => [
73
+ [target.pluginId, target.platform, target.scope].join('\0'), target,
74
+ ])).values()].sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
75
+ .localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en'));
76
+ }
23
77
  function reviewPreparationStageLabel(stage) {
24
78
  return {
25
79
  refresh: '刷新相关来源',
@@ -70,10 +124,25 @@ function canonicalBatchTargets(targets) {
70
124
  return [...byKey.values()].sort((left, right) => [left.pluginId, left.platform, left.scope].join('\0')
71
125
  .localeCompare([right.pluginId, right.platform, right.scope].join('\0'), 'en'));
72
126
  }
127
+ function canonicalPluginWriteTaskTargets(targets) {
128
+ return [...new Map(targets.map((target) => [pluginWriteTaskTargetKey(target), target])).values()]
129
+ .sort((left, right) => pluginWriteTaskTargetKey(left)
130
+ .localeCompare(pluginWriteTaskTargetKey(right), 'en'));
131
+ }
73
132
  function normalizeOperationIntent(intent) {
74
133
  if (intent.kind === 'configSave' && intent.retryApply && !intent.apply) {
75
134
  return { ...intent, apply: true };
76
135
  }
136
+ if (intent.kind === 'pluginWriteTask') {
137
+ return { ...intent, targets: canonicalPluginWriteTaskTargets(intent.targets) };
138
+ }
139
+ if (intent.kind === 'catalogManagement' && intent.action === 'storePrune') {
140
+ const items = [...new Map(intent.items.map((item) => [
141
+ `${item.category}\0${item.storeKey}`,
142
+ item,
143
+ ])).values()].sort((left, right) => `${left.category}\0${left.storeKey}`.localeCompare(`${right.category}\0${right.storeKey}`, 'en'));
144
+ return { ...intent, items };
145
+ }
77
146
  if (intent.kind !== 'pluginWriteBatch')
78
147
  return intent;
79
148
  if (intent.selection.kind === 'targets') {
@@ -92,6 +161,49 @@ function normalizeOperationIntent(intent) {
92
161
  },
93
162
  };
94
163
  }
164
+ function assertPluginWriteTaskEvidence(intent, evidence) {
165
+ const candidate = evidence;
166
+ const plan = candidate.taskPlan;
167
+ if (!plan || plan.goal !== intent.goal || !Array.isArray(plan.requestedTargets)
168
+ || !Array.isArray(plan.executableTargets) || !Array.isArray(plan.blockedTargets)
169
+ || !Array.isArray(plan.causes) || !Array.isArray(plan.sources)
170
+ || typeof plan.targetFingerprint !== 'string' || typeof plan.planFingerprint !== 'string'
171
+ || JSON.stringify(plan.requestedTargets) !== JSON.stringify(intent.targets)
172
+ || plan.executableTargets.length === 0) {
173
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务确认信息不完整。');
174
+ }
175
+ if (plan.planFingerprint !== operationEvidenceFingerprint(evidence)) {
176
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务确认指纹无效。');
177
+ }
178
+ if (intent.executionPolicy === 'all' && plan.blockedTargets.length > 0) {
179
+ throw new TuiOperationBlockedError('taskHasBlockedTargets', '部分目标需要先处理,不能作为完整任务执行。');
180
+ }
181
+ const requested = new Set(plan.requestedTargets.map(pluginWriteTaskTargetKey));
182
+ const executable = new Set(plan.executableTargets.map(pluginWriteTaskTargetKey));
183
+ const blocked = new Set(plan.blockedTargets.map((target) => pluginWriteTaskTargetKey(target.target)));
184
+ if (executable.size !== plan.executableTargets.length || blocked.size !== plan.blockedTargets.length
185
+ || [...executable].some((key) => !requested.has(key) || blocked.has(key))
186
+ || [...blocked].some((key) => !requested.has(key))
187
+ || [...requested].some((key) => !executable.has(key) && !blocked.has(key))) {
188
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务目标分区无效。');
189
+ }
190
+ const grouped = new Set();
191
+ for (const source of plan.sources) {
192
+ if (!source.sourceId || !source.sourceIdentity || source.requestedTargets.length === 0
193
+ || source.executable !== Boolean(source.preview.operationPlan)
194
+ || source.requestedTargets.some((target) => !executable.has(pluginWriteTaskTargetKey(target))
195
+ || sourceIdFromPluginId(target.pluginId) !== source.sourceId)
196
+ || source.causes.some((cause) => cause.sourceId !== source.sourceId
197
+ || cause.sourceIdentity !== source.sourceIdentity
198
+ || cause.consumers.some((consumer) => !executable.has(pluginWriteTaskTargetKey(consumer))))) {
199
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务来源计划无效。');
200
+ }
201
+ source.requestedTargets.forEach((target) => grouped.add(pluginWriteTaskTargetKey(target)));
202
+ }
203
+ if ([...executable].some((key) => !grouped.has(key))) {
204
+ throw new TuiOperationBlockedError('taskEvidenceInvalid', '任务来源计划缺少可执行目标。');
205
+ }
206
+ }
95
207
  function assertPluginWriteBatchEvidence(evidence) {
96
208
  const candidate = evidence;
97
209
  if (!Array.isArray(candidate.requestedPluginIds) || !Array.isArray(candidate.groups)) {
@@ -195,7 +307,62 @@ export function pluginWriteBatchFailedRequestedPluginIds(execution) {
195
307
  return canonicalPluginIds(pluginWriteBatchFailedRequestedTargets(execution)
196
308
  .map((target) => target.pluginId));
197
309
  }
310
+ export function pluginWriteTaskFailedRequestedTargets(execution) {
311
+ const requested = new Map(execution.taskPlan.requestedTargets.map((target) => [
312
+ pluginWriteTaskTargetKey(target), target,
313
+ ]));
314
+ const failed = new Set();
315
+ const succeeded = new Set();
316
+ for (const source of execution.sourceResults) {
317
+ const sourceRequested = new Set(source.requestedTargets.map(pluginWriteTaskTargetKey));
318
+ if (source.result.targetResults.length === 0 && source.result.overallState !== 'succeeded') {
319
+ sourceRequested.forEach((key) => failed.add(key));
320
+ continue;
321
+ }
322
+ for (const result of source.result.targetResults) {
323
+ const pluginId = result.target.pluginId;
324
+ if (!pluginId)
325
+ continue;
326
+ const exactKey = pluginWriteTaskTargetKey({
327
+ pluginId,
328
+ platform: result.target.platform,
329
+ scope: result.target.scope,
330
+ });
331
+ if (sourceRequested.has(exactKey)) {
332
+ if (result.state === 'failed' || result.action === 'skipped')
333
+ failed.add(exactKey);
334
+ else
335
+ succeeded.add(exactKey);
336
+ }
337
+ if (result.state !== 'failed' && result.action !== 'skipped')
338
+ continue;
339
+ for (const cause of source.requestedTargets.length > 0
340
+ ? execution.taskPlan.causes.filter((cause) => cause.sourceId === source.sourceId) : []) {
341
+ if (pluginWriteTaskTargetKey(cause.repairTarget) !== exactKey)
342
+ continue;
343
+ cause.consumers.forEach((consumer) => failed.add(pluginWriteTaskTargetKey(consumer)));
344
+ }
345
+ const placement = source.result.dependencyPlan?.targets.find((target) => target.pluginId === pluginId && target.platform === result.target.platform
346
+ && (target.requestedScope ?? target.resolvedScope) === result.target.scope);
347
+ for (const consumer of placement?.requiredByTargets ?? []) {
348
+ failed.add(pluginWriteTaskTargetKey(consumer));
349
+ }
350
+ }
351
+ }
352
+ for (const key of succeeded)
353
+ failed.delete(key);
354
+ return canonicalPluginWriteTaskTargets([...failed].flatMap((key) => {
355
+ const target = requested.get(key);
356
+ return target ? [target] : [];
357
+ }));
358
+ }
198
359
  function collectOperationSensitiveValues(intent) {
360
+ if (intent.kind === 'catalogManagement') {
361
+ return [
362
+ ...('ownerUrl' in intent && intent.ownerUrl ? [intent.ownerUrl] : []),
363
+ ...('remoteUrl' in intent && intent.remoteUrl ? [intent.remoteUrl] : []),
364
+ ];
365
+ }
199
366
  if (intent.kind !== 'configSave')
200
367
  return [];
201
368
  const sensitiveKeys = new Set([...(intent.sensitiveKeys ?? []), ...(intent.privateKeys ?? [])]);
@@ -243,6 +410,53 @@ function cloneFrozen(value) {
243
410
  return deepFreeze(structuredClone(value));
244
411
  }
245
412
  export function operationEvidenceFingerprint(evidence) {
413
+ if (evidence.catalogManagement) {
414
+ const management = evidence.catalogManagement;
415
+ return crypto.createHash('sha256')
416
+ .update(JSON.stringify(stableValue(management.action === 'storePrune'
417
+ ? {
418
+ action: management.action,
419
+ workspaceBinding: management.workspaceBinding,
420
+ authorizedStoreItems: management.authorizedStoreItems,
421
+ }
422
+ : {
423
+ action: management.action,
424
+ workspaceBinding: management.workspaceBinding,
425
+ evidenceDigest: management.evidenceDigest,
426
+ })))
427
+ .digest('hex');
428
+ }
429
+ if ('taskPlan' in evidence) {
430
+ const task = evidence.taskPlan;
431
+ return crypto.createHash('sha256')
432
+ .update(JSON.stringify(stableValue({
433
+ catalog: evidence.catalog,
434
+ goal: task.goal,
435
+ requestedTargets: task.requestedTargets,
436
+ executableTargets: task.executableTargets,
437
+ causes: task.causes,
438
+ blockedTargets: task.blockedTargets,
439
+ targetFingerprint: task.targetFingerprint,
440
+ sources: task.sources.map((source) => ({
441
+ sourceId: source.sourceId,
442
+ sourceIdentity: source.sourceIdentity,
443
+ requestedTargets: source.requestedTargets,
444
+ causes: source.causes,
445
+ executable: source.executable,
446
+ planFingerprint: source.preview.operationPlan?.planFingerprint,
447
+ targetResults: source.preview.targetResults.map((target) => ({
448
+ pluginId: target.target.pluginId,
449
+ platform: target.target.platform,
450
+ scope: target.target.scope,
451
+ state: target.state,
452
+ action: target.action,
453
+ errorCode: target.errorCode,
454
+ requiredBy: target.requiredBy,
455
+ })),
456
+ })),
457
+ })))
458
+ .digest('hex');
459
+ }
246
460
  if ('groups' in evidence && 'requestedPluginIds' in evidence) {
247
461
  const batch = evidence;
248
462
  return crypto.createHash('sha256')
@@ -337,6 +551,8 @@ function issueCode(error) {
337
551
  return safeDiagnosticCode(safeOperationError.code, 'operationBlocked');
338
552
  if (error instanceof TuiOperationBlockedError)
339
553
  return error.code;
554
+ if (error instanceof CatalogManagementError)
555
+ return error.code;
340
556
  if (error instanceof CatalogGenerationError)
341
557
  return error.code;
342
558
  if (error instanceof Error && error.name === 'CatalogSourcesConfigConflictError') {
@@ -408,6 +624,7 @@ export class TuiController {
408
624
  #retiring = false;
409
625
  #externalDrafts = [];
410
626
  #batchRetryIntent;
627
+ #taskRetryIntent;
411
628
  #operationHistory = new OperationHistoryStore();
412
629
  #sessionEpoch = 0;
413
630
  #lastRecordedResultId;
@@ -431,6 +648,7 @@ export class TuiController {
431
648
  onError: (error, code) => {
432
649
  void this.#logErrorSafe(error, code);
433
650
  },
651
+ now: this.dependencies.now,
434
652
  });
435
653
  if (progressFeed) {
436
654
  this.#progressUnsubscribe = progressFeed.subscribe((progress) => {
@@ -622,6 +840,7 @@ export class TuiController {
622
840
  this.#installPlacementDraft = undefined;
623
841
  this.#recoverableInstallPlacementDraft = undefined;
624
842
  this.#batchRetryIntent = undefined;
843
+ this.#taskRetryIntent = undefined;
625
844
  this.#externalDrafts = [];
626
845
  this.#result = undefined;
627
846
  this.#resultAnchor = undefined;
@@ -774,6 +993,18 @@ export class TuiController {
774
993
  }
775
994
  #ensureOperationFlow(intent) {
776
995
  const active = this.#router.activeFlow();
996
+ if (intent.kind === 'catalogManagement') {
997
+ if (active?.kind === 'catalogManagement')
998
+ return;
999
+ this.#router.beginFlow('catalogManagement', this.#router.snapshot.current.route);
1000
+ return;
1001
+ }
1002
+ if (intent.kind === 'pluginWriteTask') {
1003
+ if (active?.kind === 'pluginWriteTask')
1004
+ return;
1005
+ this.#router.beginFlow('pluginWriteTask', this.#router.snapshot.current.route);
1006
+ return;
1007
+ }
777
1008
  if (intent.kind === 'pluginWriteBatch') {
778
1009
  if (active?.kind === 'batchWrite')
779
1010
  return;
@@ -818,7 +1049,9 @@ export class TuiController {
818
1049
  : intent.kind === 'sourceMutation' && intent.action !== 'remove'
819
1050
  ? { name: 'sourceDetail', sourceId: intent.sourceId }
820
1051
  : intent.kind === 'catalogGeneration'
821
- ? { name: 'sourceDetail', sourceId: intent.sourceId }
1052
+ ? this.#router.snapshot.current.route.name === 'pluginWriteTask'
1053
+ ? this.#router.snapshot.current.route
1054
+ : { name: 'sourceDetail', sourceId: intent.sourceId }
822
1055
  : { name: 'sourceList' });
823
1056
  }
824
1057
  #stableResultRoute(intent, result) {
@@ -836,6 +1069,8 @@ export class TuiController {
836
1069
  }
837
1070
  if (intent.kind === 'pluginWriteBatch')
838
1071
  return { name: 'pluginList', mode: 'installed' };
1072
+ if (intent.kind === 'pluginWriteTask')
1073
+ return { name: 'pluginList', mode: 'installed' };
839
1074
  if (intent.kind === 'portableInstall' || intent.kind === 'configSave' || intent.kind === 'configReset') {
840
1075
  return this.#normalizeRoute({ name: 'pluginDetail', pluginId: intent.pluginId });
841
1076
  }
@@ -843,8 +1078,22 @@ export class TuiController {
843
1078
  return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
844
1079
  }
845
1080
  if (intent.kind === 'catalogGeneration') {
1081
+ if ('returnRoute' in result && result.returnRoute?.name === 'pluginWriteTask') {
1082
+ return this.#normalizeRoute(result.returnRoute);
1083
+ }
846
1084
  return this.#normalizeRoute({ name: 'sourceDetail', sourceId: intent.sourceId });
847
1085
  }
1086
+ if (intent.kind === 'catalogManagement') {
1087
+ if (intent.action === 'devActivate' || intent.action === 'devDeactivate') {
1088
+ return { name: 'catalogDev', step: 'overview' };
1089
+ }
1090
+ if (intent.action === 'storePrune')
1091
+ return { name: 'catalogStore' };
1092
+ if (intent.action === 'publishPlan' || intent.action === 'publishApply') {
1093
+ return { name: 'catalogPublish', step: 'overview' };
1094
+ }
1095
+ return { name: 'catalogWorkspace', view: 'overview' };
1096
+ }
848
1097
  return { name: 'sourceList' };
849
1098
  }
850
1099
  #showResult(result, intent) {
@@ -883,6 +1132,19 @@ export class TuiController {
883
1132
  },
884
1133
  }
885
1134
  : undefined;
1135
+ this.#taskRetryIntent = intent.kind === 'pluginWriteTask'
1136
+ && 'summary' in result
1137
+ && result.summary?.task
1138
+ && (result.summary.task.retryTargets?.length ?? 0) > 0
1139
+ ? {
1140
+ ...structuredClone(intent),
1141
+ targets: result.summary.task.retryTargets.map((target) => {
1142
+ const current = this.listPluginWriteTaskCandidates(intent.goal).find((candidate) => pluginWriteTaskTargetKey(candidate.target) === pluginWriteTaskTargetKey(target));
1143
+ return structuredClone(current?.eligible ? current.target : target);
1144
+ }),
1145
+ executionPolicy: 'all',
1146
+ }
1147
+ : undefined;
886
1148
  if (!this.#router.finishFlow(target, terminal))
887
1149
  this.#router.replace(terminal);
888
1150
  }
@@ -923,14 +1185,18 @@ export class TuiController {
923
1185
  this.#catalogSourcesSession.configure(status.payload.catalog.sources.map((source) => {
924
1186
  const generation = this.#catalogGenerations.get(source.id);
925
1187
  const detail = details.get(source.id);
1188
+ const probeFingerprint = JSON.stringify({
1189
+ sourceIdentity: source.identity,
1190
+ requestedRef: source.requestedRef,
1191
+ });
926
1192
  return {
927
1193
  sourceId: source.id,
928
1194
  primary: source.primary,
929
1195
  autoCheck: this.dependencies.probeCatalogFreshness !== undefined
930
1196
  && (detail?.autoCheck ?? true),
1197
+ probeFingerprint,
931
1198
  fingerprint: JSON.stringify({
932
- sourceIdentity: source.identity,
933
- requestedRef: source.requestedRef,
1199
+ probeFingerprint,
934
1200
  activeStoreKey: generation?.active?.storeKey,
935
1201
  generationState: generation?.state,
936
1202
  generationErrorCode: generation?.errorCode,
@@ -1252,8 +1518,11 @@ export class TuiController {
1252
1518
  return origin;
1253
1519
  if (this.#router.snapshot.current.route.name === 'result'
1254
1520
  && this.#resultAnchor
1255
- && (this.#resultAnchor.name === 'pluginPreview' || this.#resultAnchor.name === 'pluginDetail')
1256
- && this.#resultAnchor.pluginId === pluginId) {
1521
+ && (this.#resultAnchor.name === 'pluginPreview' || this.#resultAnchor.name === 'pluginDetail'
1522
+ || this.#resultAnchor.name === 'pluginWriteTask')
1523
+ && (this.#resultAnchor.pluginId === pluginId
1524
+ || this.#resultAnchor.name === 'pluginWriteTask'
1525
+ && this.#resultAnchor.targetKeys?.some((key) => key.startsWith(`${pluginId}\0`)))) {
1257
1526
  return structuredClone(this.#resultAnchor);
1258
1527
  }
1259
1528
  return undefined;
@@ -1621,16 +1890,30 @@ export class TuiController {
1621
1890
  return cloneFrozen((this.#viewModel?.plugins ?? []).filter((plugin) => (!options.sourceId || plugin.sourceId === options.sourceId)
1622
1891
  && (options.mode === 'browse' || hasInstalledPlacement(plugin) || plugin.savedConfigScopes.length > 0)
1623
1892
  && (options.installFilter !== 'uninstalled' || !hasInstalledPlacement(plugin))
1893
+ && (options.installFilter !== 'updateAvailable' || plugin.update?.state === 'available')
1624
1894
  && (!query || `${plugin.displayName}\n${plugin.description}\n${plugin.sourceId}`.toLocaleLowerCase('en').includes(query))));
1625
1895
  }
1626
1896
  getPluginOverview(pluginId) {
1627
1897
  const plugin = this.#viewModel?.plugins.find((entry) => entry.pluginId === pluginId);
1628
1898
  return plugin ? cloneFrozen(plugin) : undefined;
1629
1899
  }
1900
+ listCurrentProjectIntentRoots() {
1901
+ return cloneFrozen([...new Set((this.#status?.payload.targets ?? []).flatMap((target) => {
1902
+ const intent = statusTargetIntent(target);
1903
+ return intent.relation === 'root' && intent.scopes.includes('project')
1904
+ ? [target.pluginId] : [];
1905
+ }))].sort((left, right) => left.localeCompare(right, 'en')));
1906
+ }
1907
+ listPluginWriteBatchTargets(action, pluginIds) {
1908
+ return cloneFrozen(pluginWriteBatchTargets(action, pluginIds, this.#status?.payload.targets ?? []));
1909
+ }
1630
1910
  getPluginDependencyOverview(pluginId) {
1631
1911
  const overview = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId);
1632
1912
  return overview ? cloneFrozen(overview) : undefined;
1633
1913
  }
1914
+ listPluginDependencyOverviews() {
1915
+ return cloneFrozen(this.#viewModel?.dependencyOverviews ?? []);
1916
+ }
1634
1917
  getBlockingDependents(pluginId, platform, scope) {
1635
1918
  const dependents = this.#viewModel?.dependencyOverviews.find((entry) => entry.pluginId === pluginId)?.dependents ?? [];
1636
1919
  return cloneFrozen(dependents.flatMap((dependent) => {
@@ -1756,7 +2039,7 @@ export class TuiController {
1756
2039
  return this.#catalogSourcesSession.fullValidate(sourceId);
1757
2040
  }
1758
2041
  setExternalDraftState(states = []) {
1759
- const priority = { sourceDraft: 0, batchDraft: 1, configDraft: 2 };
2042
+ const priority = { sourceDraft: 0, batchDraft: 1, catalogDraft: 2, configDraft: 3 };
1760
2043
  this.#externalDrafts = structuredClone(states).sort((left, right) => priority[left.kind] - priority[right.kind] || left.owner.localeCompare(right.owner, 'en'));
1761
2044
  this.#publish();
1762
2045
  }
@@ -1769,6 +2052,26 @@ export class TuiController {
1769
2052
  startPluginWriteBatchReview(intent) {
1770
2053
  return this.startReviewPreparation(intent, { name: 'batchPlacement' });
1771
2054
  }
2055
+ startPluginWriteTaskReview(intent, returnRoute) {
2056
+ return this.startReviewPreparation(intent, returnRoute);
2057
+ }
2058
+ beginNestedPluginWriteTask(route) {
2059
+ this.#assertAcceptingActions();
2060
+ const current = this.#router.snapshot.current.route;
2061
+ if (current.name !== 'pluginWriteTask') {
2062
+ throw new TuiOperationBlockedError('taskOriginUnavailable', '嵌套任务缺少父任务。');
2063
+ }
2064
+ this.#router.beginFlow('pluginWriteTask', current);
2065
+ this.#router.push(route);
2066
+ this.#publish();
2067
+ }
2068
+ pluginWriteTaskReturnRoute(route) {
2069
+ const active = this.#router.activeFlow();
2070
+ return structuredClone(active?.kind === 'pluginWriteTask'
2071
+ && active.returnTarget.name === 'pluginWriteTask'
2072
+ ? active.returnTarget
2073
+ : route);
2074
+ }
1772
2075
  retryFailedPluginWriteBatch() {
1773
2076
  if (!this.#batchRetryIntent)
1774
2077
  return undefined;
@@ -1776,6 +2079,13 @@ export class TuiController {
1776
2079
  this.#batchRetryIntent = undefined;
1777
2080
  return this.startReviewPreparation(intent, { name: 'pluginList', mode: 'installed' });
1778
2081
  }
2082
+ retryFailedPluginWriteTask() {
2083
+ if (!this.#taskRetryIntent)
2084
+ return undefined;
2085
+ const intent = structuredClone(this.#taskRetryIntent);
2086
+ this.#taskRetryIntent = undefined;
2087
+ return this.startReviewPreparation(intent, { name: 'pluginList', mode: 'installed' });
2088
+ }
1779
2089
  catalogActivationBlockReason() {
1780
2090
  if (this.#externalDrafts[0])
1781
2091
  return this.#externalDrafts[0].kind;
@@ -1790,11 +2100,26 @@ export class TuiController {
1790
2100
  return 'writeProgress';
1791
2101
  return undefined;
1792
2102
  }
1793
- catalogSourceRecoveryBlockReason() {
2103
+ catalogSourceRecoveryBlockReason(intent) {
1794
2104
  const activeFlow = this.#router.activeFlow();
2105
+ const route = this.#router.snapshot.current.route;
2106
+ const ownedFrozenBatchTask = intent !== undefined
2107
+ && route.name === 'pluginWriteTask'
2108
+ && route.targetKeys !== undefined
2109
+ && route.targetKeys.length > 0
2110
+ && route.batchOrigin !== undefined
2111
+ && (activeFlow?.id === route.batchOrigin.flowId
2112
+ || activeFlow?.parentFlowId === route.batchOrigin.flowId)
2113
+ && this.listPluginWriteTaskCandidates(route.goal).some((candidate) => candidate.sourceId === intent.sourceId
2114
+ && candidate.blockReason === 'catalogActivationRequired'
2115
+ && route.targetKeys.includes(pluginWriteTaskTargetKey(candidate.target)));
1795
2116
  const blockingExternalDraft = this.#externalDrafts.find((draft) => !(draft.kind === 'configDraft'
1796
2117
  && activeFlow?.kind === 'sourceRecovery'
1797
- && activeFlow.parentFlowId !== undefined));
2118
+ && activeFlow.parentFlowId !== undefined)
2119
+ && !(ownedFrozenBatchTask
2120
+ && draft.kind === 'batchDraft'
2121
+ && draft.owner === route.batchOrigin?.owner
2122
+ && draft.flowId === route.batchOrigin.flowId));
1798
2123
  if (blockingExternalDraft)
1799
2124
  return blockingExternalDraft.kind;
1800
2125
  if (this.#portableDraft)
@@ -1865,6 +2190,61 @@ export class TuiController {
1865
2190
  : this.#abort.signal;
1866
2191
  return cloneFrozen(await this.#trackRead(this.dependencies.getConfigPage(pluginId, structuredClone(layer), combined)));
1867
2192
  }
2193
+ async selectCatalogWorkspace(workspaceRoot) {
2194
+ this.#assertAcceptingActions();
2195
+ if (!this.dependencies.catalogManagement) {
2196
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2197
+ }
2198
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.selectWorkspace(workspaceRoot)));
2199
+ }
2200
+ async getCatalogWorkspaceManagement() {
2201
+ this.#assertAcceptingActions();
2202
+ if (!this.dependencies.catalogManagement) {
2203
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2204
+ }
2205
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getWorkspace()));
2206
+ }
2207
+ async validateCatalogWorkspace() {
2208
+ this.#assertAcceptingActions();
2209
+ if (!this.dependencies.catalogManagement) {
2210
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2211
+ }
2212
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.validateWorkspace()));
2213
+ }
2214
+ async getCatalogDevManagement() {
2215
+ this.#assertAcceptingActions();
2216
+ if (!this.dependencies.catalogManagement) {
2217
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2218
+ }
2219
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getDev()));
2220
+ }
2221
+ async getCatalogStoreManagement() {
2222
+ this.#assertAcceptingActions();
2223
+ if (!this.dependencies.catalogManagement) {
2224
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2225
+ }
2226
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getStore()));
2227
+ }
2228
+ async getCatalogPublicationManagement() {
2229
+ this.#assertAcceptingActions();
2230
+ if (!this.dependencies.catalogManagement) {
2231
+ throw new TuiOperationBlockedError('catalogManagementUnavailable', 'Catalog management is unavailable.');
2232
+ }
2233
+ return cloneFrozen(await this.#trackRead(this.dependencies.catalogManagement.getPublication()));
2234
+ }
2235
+ async previewConfigSaveImpact(intent, signal) {
2236
+ this.#assertAcceptingActions();
2237
+ const normalized = normalizeOperationIntent(intent);
2238
+ this.#validateReviewIntent(normalized);
2239
+ const combined = signal
2240
+ ? AbortSignal.any([this.#abort.signal, signal])
2241
+ : this.#abort.signal;
2242
+ const evidence = await this.#trackRead(this.#prepareReviewEvidence(normalized, combined));
2243
+ if (!evidence.config) {
2244
+ throw new TuiOperationBlockedError('configImpactUnavailable', '无法确定设置修改的应用影响。');
2245
+ }
2246
+ return cloneFrozen(evidence.config);
2247
+ }
1868
2248
  async getConfigResetAvailability(pluginId, layer) {
1869
2249
  this.#assertAcceptingActions();
1870
2250
  if (!this.dependencies.getConfigResetAvailability) {
@@ -1875,6 +2255,11 @@ export class TuiController {
1875
2255
  listUserIssues() {
1876
2256
  return cloneFrozen(this.#viewModel?.issues ?? []);
1877
2257
  }
2258
+ listPluginWriteTaskCandidates(goal) {
2259
+ return cloneFrozen(this.#viewModel
2260
+ ? buildPluginWriteTaskCandidates(this.#viewModel, goal)
2261
+ : []);
2262
+ }
1878
2263
  async buildOperationReview(intent, returnRoute) {
1879
2264
  this.#assertAcceptingActions();
1880
2265
  intent = normalizeOperationIntent(intent);
@@ -1895,7 +2280,7 @@ export class TuiController {
1895
2280
  this.#assertAcceptingActions();
1896
2281
  intent = normalizeOperationIntent(intent);
1897
2282
  if (intent.kind === 'catalogGeneration' && !intent.sourceMutation) {
1898
- const blockReason = this.catalogSourceRecoveryBlockReason();
2283
+ const blockReason = this.catalogSourceRecoveryBlockReason(intent);
1899
2284
  if (blockReason) {
1900
2285
  throw new TuiOperationBlockedError('catalogActivationBlocked', `Catalog Active 切换已阻止:${blockReason}。请先完成或放弃当前草稿或操作。`);
1901
2286
  }
@@ -1958,6 +2343,37 @@ export class TuiController {
1958
2343
  return this.startReviewPreparation(structuredClone(intent), preparation.returnRoute);
1959
2344
  }
1960
2345
  #validateReviewIntent(intent) {
2346
+ if (intent.kind === 'catalogManagement') {
2347
+ if (intent.action === 'storePrune' && intent.items.length === 0) {
2348
+ throw new TuiOperationBlockedError('catalogStoreSelectionInvalid', 'Select at least one reclaimable Catalog store item.');
2349
+ }
2350
+ if (intent.action === 'workspaceVersion'
2351
+ && (intent.version === undefined) === (intent.bump === undefined)) {
2352
+ throw new TuiOperationBlockedError('catalogVersionSelectionInvalid', 'Select exactly one Catalog version or bump level.');
2353
+ }
2354
+ if ((intent.action === 'publishPlan' || intent.action === 'publishApply')
2355
+ && intent.remote !== undefined && intent.remoteUrl !== undefined) {
2356
+ throw new TuiOperationBlockedError('catalogPublicationTargetInvalid', 'Select a Git remote name or one direct remote URL.');
2357
+ }
2358
+ if (intent.action === 'publishApply'
2359
+ && !/^sha256:[0-9a-f]{64}$/u.test(intent.expectedPlanDigest)) {
2360
+ throw new TuiOperationBlockedError('catalogPublicationPlanInvalid', 'Select an exact prepared Catalog publication plan.');
2361
+ }
2362
+ }
2363
+ if (intent.kind === 'pluginWriteTask') {
2364
+ if (intent.targets.length === 0
2365
+ || JSON.stringify(intent.targets) !== JSON.stringify(canonicalPluginWriteTaskTargets(intent.targets))) {
2366
+ throw new TuiOperationBlockedError('taskSelectionInvalid', '任务目标必须是非空精确目标集合。');
2367
+ }
2368
+ for (const target of intent.targets) {
2369
+ sourceIdFromPluginId(target.pluginId);
2370
+ if ((target.platform !== 'codex' && target.platform !== 'claude')
2371
+ || (target.scope !== 'user' && target.scope !== 'project')
2372
+ || (intent.goal === 'update') !== Boolean(target.expectedUpdate)) {
2373
+ throw new TuiOperationBlockedError('taskSelectionInvalid', '任务目标或版本证据无效。');
2374
+ }
2375
+ }
2376
+ }
1961
2377
  if (intent.kind === 'pluginWriteBatch') {
1962
2378
  if (intent.selection.kind === 'currentProjectIntentRoots'
1963
2379
  && (intent.action !== 'sync' || intent.scope !== 'project')) {
@@ -1978,8 +2394,12 @@ export class TuiController {
1978
2394
  }
1979
2395
  }
1980
2396
  #reviewPreparationLabel(intent) {
2397
+ if (intent.kind === 'catalogManagement')
2398
+ return 'Catalog 管理';
1981
2399
  if (intent.kind === 'catalogSourcesRecovery')
1982
2400
  return 'Catalog 来源配置';
2401
+ if (intent.kind === 'pluginWriteTask')
2402
+ return `${intent.targets.length} 个安装位置`;
1983
2403
  if (intent.kind === 'pluginWriteBatch') {
1984
2404
  return intent.selection.kind === 'selected'
1985
2405
  ? `${intent.selection.pluginIds.length} 个插件`
@@ -2033,9 +2453,10 @@ export class TuiController {
2033
2453
  preparation.snapshot.stages = preparation.snapshot.stages.map((stage) => stage.state === 'running' ? { ...stage, state: 'completed' } : stage);
2034
2454
  this.#publish();
2035
2455
  const returnRoute = structuredClone(preparation.returnRoute);
2456
+ this.#storeOperationReview(intent, evidence, 'replace', returnRoute);
2036
2457
  this.#reviewPreparationIntents.delete(preparation.snapshot.id);
2037
2458
  this.#reviewPreparation = undefined;
2038
- this.#storeOperationReview(intent, evidence, 'replace', returnRoute);
2459
+ this.#publish();
2039
2460
  }
2040
2461
  catch (error) {
2041
2462
  if (!this.#reviewPreparationIsCurrent(token))
@@ -2148,6 +2569,8 @@ export class TuiController {
2148
2569
  this.#ensureOperationFlow(intent);
2149
2570
  if (intent.kind === 'pluginWriteBatch')
2150
2571
  assertPluginWriteBatchEvidence(evidence);
2572
+ if (intent.kind === 'pluginWriteTask')
2573
+ assertPluginWriteTaskEvidence(intent, evidence);
2151
2574
  if (this.#router.snapshot.current.route.name === 'result')
2152
2575
  navigation = 'replace';
2153
2576
  const projection = buildOperationReviewProjection({
@@ -2164,10 +2587,12 @@ export class TuiController {
2164
2587
  const expiresAt = now
2165
2588
  + (this.dependencies.reviewTtlMs ?? 2 * 60_000);
2166
2589
  const pluginId = 'pluginId' in intent ? intent.pluginId
2167
- : 'sourceId' in intent ? intent.sourceId
2590
+ : 'sourceId' in intent ? intent.sourceId ?? 'Catalog 管理'
2168
2591
  : intent.kind === 'pluginWriteBatch'
2169
2592
  ? `${evidence.requestedPluginIds.length} 个插件`
2170
- : 'Catalog 来源配置';
2593
+ : intent.kind === 'pluginWriteTask'
2594
+ ? `${evidence.taskPlan.requestedTargets.length} 个安装位置`
2595
+ : 'Catalog 来源配置';
2171
2596
  if ('pluginId' in intent)
2172
2597
  sourceIdFromPluginId(intent.pluginId);
2173
2598
  const pluginVersion = 'pluginId' in intent
@@ -2184,6 +2609,7 @@ export class TuiController {
2184
2609
  ? { sourceAction: intent.sourceMutation.action } : {}),
2185
2610
  ...(intent.kind === 'pluginWrite' ? { pluginAction: intent.action } : {}),
2186
2611
  ...(intent.kind === 'pluginWriteBatch' ? { pluginAction: intent.action } : {}),
2612
+ ...(intent.kind === 'pluginWriteTask' ? { taskGoal: intent.goal } : {}),
2187
2613
  ...(intent.kind === 'pluginWrite' && intent.originIssue
2188
2614
  ? { originIssue: intent.originIssue }
2189
2615
  : {}),
@@ -2204,6 +2630,15 @@ export class TuiController {
2204
2630
  .filter((group) => group.executable).length,
2205
2631
  },
2206
2632
  } : {}),
2633
+ ...(intent.kind === 'pluginWriteTask' ? {
2634
+ task: {
2635
+ goal: intent.goal,
2636
+ requestedTargetCount: evidence.taskPlan.requestedTargets.length,
2637
+ executableTargetCount: evidence.taskPlan.executableTargets.length,
2638
+ blockedTargetCount: evidence.taskPlan.blockedTargets.length,
2639
+ causeCount: evidence.taskPlan.causes.length,
2640
+ },
2641
+ } : {}),
2207
2642
  ...(evidence.source && 'catalogVersion' in evidence.source
2208
2643
  && typeof evidence.source.catalogVersion === 'string'
2209
2644
  && evidence.source.snapshotDigest
@@ -2267,7 +2702,9 @@ export class TuiController {
2267
2702
  this.#progressFeed?.clearCurrent?.();
2268
2703
  const dependencyTargets = ticket.intent.kind === 'pluginWriteBatch'
2269
2704
  ? ticket.evidence.groups.flatMap((group) => group.preview.dependencyPlan?.targets ?? [])
2270
- : ticket.evidence.dependencyPlan?.targets ?? [];
2705
+ : ticket.intent.kind === 'pluginWriteTask'
2706
+ ? ticket.evidence.taskPlan.sources.flatMap((source) => source.preview.dependencyPlan?.targets ?? [])
2707
+ : ticket.evidence.dependencyPlan?.targets ?? [];
2271
2708
  this.#progressTargets = dependencyTargets.map((target) => ({
2272
2709
  pluginId: target.pluginId,
2273
2710
  platform: target.platform,
@@ -2285,15 +2722,18 @@ export class TuiController {
2285
2722
  current: {
2286
2723
  target: ticket.intent.kind === 'pluginWriteBatch'
2287
2724
  ? `${ticket.evidence.requestedPluginIds.length} 个插件`
2288
- : 'pluginId' in ticket.intent ? ticket.intent.pluginId
2289
- : 'sourceId' in ticket.intent ? ticket.intent.sourceId : 'Catalog 来源配置',
2725
+ : ticket.intent.kind === 'pluginWriteTask'
2726
+ ? `${ticket.evidence.taskPlan.executableTargets.length} 个安装位置`
2727
+ : 'pluginId' in ticket.intent ? ticket.intent.pluginId
2728
+ : 'sourceId' in ticket.intent ? ticket.intent.sourceId ?? 'Catalog 管理' : 'Catalog 来源配置',
2290
2729
  phase: '验证当前状态',
2291
2730
  },
2292
2731
  };
2293
2732
  this.#router.replace({ name: 'progress', operationId });
2294
2733
  this.#publish();
2295
2734
  try {
2296
- if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch') {
2735
+ if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
2736
+ && ticket.intent.kind !== 'pluginWriteTask') {
2297
2737
  await this.dependencies.validateIntent?.(structuredClone(ticket.intent), this.#abort.signal);
2298
2738
  }
2299
2739
  }
@@ -2325,6 +2765,7 @@ export class TuiController {
2325
2765
  if (!this.#operationIsCurrent(generation))
2326
2766
  return this.#detachedBlocked(operationId, ticket.intent);
2327
2767
  if (ticket.intent.kind !== 'pluginWrite' && ticket.intent.kind !== 'pluginWriteBatch'
2768
+ && ticket.intent.kind !== 'pluginWriteTask'
2328
2769
  && ticket.intent.kind !== 'catalogGeneration') {
2329
2770
  let current;
2330
2771
  try {
@@ -2358,6 +2799,63 @@ export class TuiController {
2358
2799
  ...executionProjection,
2359
2800
  sensitiveValues: collectOperationSensitiveValues(ticket.intent),
2360
2801
  });
2802
+ if (ticket.intent.kind === 'catalogManagement') {
2803
+ if (ticket.intent.action === 'devActivate' || ticket.intent.action === 'devDeactivate') {
2804
+ this.dependencies.beginCatalogSession?.();
2805
+ generation = ++this.#generation;
2806
+ try {
2807
+ const refreshed = await this.dependencies.loadStatus(this.#abort.signal);
2808
+ if (!this.#operationIsCurrent(generation)) {
2809
+ return this.#detachedBlocked(operationId, ticket.intent);
2810
+ }
2811
+ this.#applyStatus(refreshed);
2812
+ }
2813
+ catch (reloadError) {
2814
+ if (!this.#operationIsCurrent(generation)) {
2815
+ return this.#detachedBlocked(operationId, ticket.intent);
2816
+ }
2817
+ await this.#logErrorSafe(reloadError, 'tuiStateReloadFailed', {
2818
+ privateConfigOperation: true,
2819
+ });
2820
+ this.#status = undefined;
2821
+ this.#viewModel = undefined;
2822
+ this.#phase = 'error';
2823
+ this.#loadError = {
2824
+ code: 'tuiStateReloadFailed',
2825
+ message: 'Catalog 来源已变更,但插件状态刷新失败。请重新加载后再继续操作。',
2826
+ };
2827
+ return this.#setBlocked(operationId, 'tuiStateReloadFailed', {
2828
+ intent: ticket.intent,
2829
+ navigation: 'result',
2830
+ returnRoute: ticket.returnRoute,
2831
+ writeMayHaveCompleted: true,
2832
+ });
2833
+ }
2834
+ }
2835
+ const baseSummary = operationResultSummary(ticket.intent, execution);
2836
+ const summary = {
2837
+ ...baseSummary,
2838
+ elapsedMs: Math.max(0, (this.dependencies.now?.() ?? Date.now()) - startedAtMs),
2839
+ messages: [...this.#progress.messages, ...(baseSummary.messages ?? [])].slice(-8)
2840
+ .map((message) => sanitizeDiagnosticText(message, [], collectOperationSensitiveValues(ticket.intent))),
2841
+ };
2842
+ const projection = this.#operationDiagnostics.get(operationId);
2843
+ if (projection)
2844
+ projection.effects.finalVerification = 'completed';
2845
+ this.#progress = { ...this.#progress, elapsedMs: summary.elapsedMs };
2846
+ const result = {
2847
+ state: summary.code === 'operationPartial' ? 'partial' : 'succeeded',
2848
+ operationId,
2849
+ summary,
2850
+ operationKind: ticket.intent.kind,
2851
+ returnRoute: structuredClone(this.#normalizeRoute(ticket.returnRoute)),
2852
+ };
2853
+ this.#result = result;
2854
+ this.#activeReview = undefined;
2855
+ this.#showResult(result, ticket.intent);
2856
+ this.#publish();
2857
+ return cloneFrozen(result);
2858
+ }
2361
2859
  if (ticket.intent.kind === 'sourceMutation' && ticket.intent.action === 'update') {
2362
2860
  const confirmedAutoCheck = catalogSourceAutoCheckOnlyValue(ticket.intent.update);
2363
2861
  if (confirmedAutoCheck !== undefined) {
@@ -2453,6 +2951,17 @@ export class TuiController {
2453
2951
  if (!this.#operationIsCurrent(generation))
2454
2952
  return this.#detachedBlocked(operationId, ticket.intent);
2455
2953
  this.#applyStatus(refreshed);
2954
+ if (ticket.intent.kind === 'catalogGeneration') {
2955
+ const completed = typeof execution === 'object' && execution !== null
2956
+ ? execution.generation
2957
+ : undefined;
2958
+ const active = this.#catalogGenerations.get(ticket.intent.sourceId)?.active;
2959
+ if (completed && active
2960
+ && active.catalogVersion === completed.resolution.catalogVersion
2961
+ && active.resolvedCommit === completed.resolution.resolvedCommit) {
2962
+ this.#catalogSourcesSession.recordVerifiedGeneration(ticket.intent.sourceId, active);
2963
+ }
2964
+ }
2456
2965
  const verifiedProjection = this.#operationDiagnostics.get(operationId);
2457
2966
  if (verifiedProjection && verifiedProjection.effects.finalVerification !== 'failed') {
2458
2967
  verifiedProjection.effects.finalVerification = 'completed';
@@ -2463,6 +2972,7 @@ export class TuiController {
2463
2972
  }
2464
2973
  const pluginSummary = ticket.intent.kind === 'pluginWrite'
2465
2974
  || ticket.intent.kind === 'pluginWriteBatch'
2975
+ || ticket.intent.kind === 'pluginWriteTask'
2466
2976
  ? operationResultSummary(ticket.intent, execution)
2467
2977
  : undefined;
2468
2978
  const hasFailedDependencyTarget = pluginSummary?.targets?.some((target) => target.relation !== undefined && target.relation !== 'requested'
@@ -2503,9 +3013,10 @@ export class TuiController {
2503
3013
  intent: ticket.intent,
2504
3014
  navigation: 'result',
2505
3015
  returnRoute: ticket.returnRoute,
2506
- ...(ticket.intent.kind === 'pluginWriteBatch' || dependencyFailure || dependencyStillRequired
3016
+ ...(ticket.intent.kind === 'pluginWriteBatch' || ticket.intent.kind === 'pluginWriteTask'
3017
+ || dependencyFailure || dependencyStillRequired
2507
3018
  ? { summary: pluginSummary } : {}),
2508
- ...(ticket.intent.kind !== 'pluginWriteBatch'
3019
+ ...(ticket.intent.kind !== 'pluginWriteBatch' && ticket.intent.kind !== 'pluginWriteTask'
2509
3020
  && !dependencyFailure && !dependencyStillRequired && ['started', 'partial', 'completed', 'failed', 'unknown'].includes(this.#operationDiagnostics.get(operationId)?.effects.awWrite ?? 'unknown') ? { writeMayHaveCompleted: true } : {}),
2510
3021
  });
2511
3022
  }
@@ -2579,7 +3090,9 @@ export class TuiController {
2579
3090
  : ticket.intent.kind === 'sourceMutation' && ticket.intent.action === 'update'
2580
3091
  ? { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
2581
3092
  : ticket.intent.kind === 'catalogGeneration'
2582
- ? { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
3093
+ ? ticket.returnRoute.name === 'pluginWriteTask'
3094
+ ? ticket.returnRoute
3095
+ : { name: 'sourceDetail', sourceId: ticket.intent.sourceId }
2583
3096
  : ticket.returnRoute)),
2584
3097
  };
2585
3098
  if ((ticket.intent.kind === 'configSave' || ticket.intent.kind === 'configReset')
@@ -2620,6 +3133,14 @@ export class TuiController {
2620
3133
  this.#operationDiagnostics.delete(operationId);
2621
3134
  return this.#rebuildStaleReview(ticket, operationId, undefined, generation);
2622
3135
  }
3136
+ if (ticket.intent.kind === 'catalogManagement') {
3137
+ return this.#setBlocked(operationId, code, {
3138
+ intent: ticket.intent,
3139
+ navigation: 'result',
3140
+ returnRoute: ticket.returnRoute,
3141
+ writeMayHaveCompleted: true,
3142
+ });
3143
+ }
2623
3144
  generation = ++this.#generation;
2624
3145
  let finalVerification = 'completed';
2625
3146
  try {
@@ -2723,6 +3244,30 @@ export class TuiController {
2723
3244
  });
2724
3245
  }
2725
3246
  async #rebuildStaleReview(ticket, operationId, currentEvidence, generation = this.#generation) {
3247
+ if (ticket.intent.kind === 'catalogManagement') {
3248
+ let evidence = currentEvidence;
3249
+ try {
3250
+ evidence ??= await this.dependencies.captureEvidence(structuredClone(ticket.intent), this.#abort.signal);
3251
+ }
3252
+ catch (error) {
3253
+ const code = issueCode(error);
3254
+ await this.#logOperationError(error, code, ticket.intent);
3255
+ return this.#setBlocked(operationId, code, {
3256
+ intent: ticket.intent,
3257
+ returnRoute: ticket.returnRoute,
3258
+ });
3259
+ }
3260
+ const review = this.#storeOperationReview(ticket.intent, evidence, 'replace', ticket.returnRoute);
3261
+ const result = {
3262
+ state: 'stale',
3263
+ operationId,
3264
+ review,
3265
+ ...this.#operationIdentity(ticket.intent),
3266
+ };
3267
+ this.#result = result;
3268
+ this.#publish();
3269
+ return cloneFrozen(result);
3270
+ }
2726
3271
  this.dependencies.beginCatalogSession?.();
2727
3272
  generation = ++this.#generation;
2728
3273
  try {
@@ -2874,6 +3419,22 @@ export class TuiController {
2874
3419
  }
2875
3420
  }
2876
3421
  function operationResultSummaryUnsafe(intent, execution) {
3422
+ if (intent.kind === 'catalogManagement') {
3423
+ const result = execution;
3424
+ return {
3425
+ code: result.state === 'partial' ? 'operationPartial' : 'operationSucceeded',
3426
+ message: result.message,
3427
+ messages: result.messages,
3428
+ catalogManagement: {
3429
+ action: result.action,
3430
+ state: result.state,
3431
+ ...(result.catalogVersion ? { catalogVersion: result.catalogVersion } : {}),
3432
+ ...(result.planDigest ? { planDigest: result.planDigest } : {}),
3433
+ ...(result.store ? { store: structuredClone(result.store) } : {}),
3434
+ ...(result.publication ? { publication: structuredClone(result.publication) } : {}),
3435
+ },
3436
+ };
3437
+ }
2877
3438
  if (intent.kind === 'catalogSourcesRecovery') {
2878
3439
  const result = execution;
2879
3440
  return {
@@ -2950,6 +3511,131 @@ function operationResultSummaryUnsafe(intent, execution) {
2950
3511
  },
2951
3512
  };
2952
3513
  }
3514
+ if (intent.kind === 'pluginWriteTask') {
3515
+ const taskExecution = execution;
3516
+ const failedTargets = pluginWriteTaskFailedRequestedTargets(taskExecution);
3517
+ const failedKeys = new Set(failedTargets.map(pluginWriteTaskTargetKey));
3518
+ const technicalTargets = taskExecution.targetResults.map((entry) => {
3519
+ const placement = taskExecution.dependencyPlan?.targets.find((target) => target.pluginId === entry.target.pluginId && target.platform === entry.target.platform
3520
+ && (target.requestedScope ?? target.resolvedScope) === entry.target.scope);
3521
+ return {
3522
+ pluginId: entry.target.pluginId,
3523
+ platform: entry.target.platform,
3524
+ scope: entry.target.scope,
3525
+ state: entry.action === 'skipped' ? 'skipped'
3526
+ : entry.state === 'failed' ? 'failed'
3527
+ : entry.action === 'keep' ? 'kept' : 'completed',
3528
+ relation: entry.relation === 'requested' ? 'requested'
3529
+ : placement?.depth === 1 ? 'direct' : 'transitive',
3530
+ requiredBy: entry.requiredBy,
3531
+ ...(entry.errorCode ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') } : {}),
3532
+ };
3533
+ });
3534
+ const blocked = new Map(taskExecution.taskPlan.blockedTargets.map((entry) => [
3535
+ pluginWriteTaskTargetKey(entry.target), entry,
3536
+ ]));
3537
+ const relatedResults = (target) => {
3538
+ const key = pluginWriteTaskTargetKey(target);
3539
+ const source = taskExecution.sourceResults.find((candidate) => candidate.requestedTargets.some((requested) => pluginWriteTaskTargetKey(requested) === key));
3540
+ const related = source?.result.targetResults.filter((entry) => {
3541
+ const pluginId = entry.target.pluginId;
3542
+ if (!pluginId)
3543
+ return false;
3544
+ const executionKey = pluginWriteTaskTargetKey({
3545
+ pluginId,
3546
+ platform: entry.target.platform,
3547
+ scope: entry.target.scope,
3548
+ });
3549
+ if (executionKey === key)
3550
+ return true;
3551
+ if (taskExecution.taskPlan.causes.some((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === executionKey
3552
+ && cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === key)))
3553
+ return true;
3554
+ const placement = source.result.dependencyPlan?.targets.find((candidate) => candidate.pluginId === pluginId && candidate.platform === entry.target.platform
3555
+ && (candidate.requestedScope ?? candidate.resolvedScope) === entry.target.scope);
3556
+ return placement?.requiredByTargets?.some((consumer) => pluginWriteTaskTargetKey(consumer) === key) ?? false;
3557
+ }) ?? [];
3558
+ const targetResults = related.map((entry) => {
3559
+ const placement = source?.result.dependencyPlan?.targets.find((candidate) => candidate.pluginId === entry.target.pluginId
3560
+ && candidate.platform === entry.target.platform
3561
+ && (candidate.requestedScope ?? candidate.resolvedScope) === entry.target.scope);
3562
+ return {
3563
+ pluginId: entry.target.pluginId,
3564
+ platform: entry.target.platform,
3565
+ scope: entry.target.scope,
3566
+ state: entry.action === 'skipped' ? 'skipped'
3567
+ : entry.state === 'failed' ? 'failed'
3568
+ : entry.action === 'keep' ? 'kept' : 'completed',
3569
+ relation: entry.relation === 'requested' ? 'requested'
3570
+ : placement?.depth === 1 ? 'direct' : 'transitive',
3571
+ ...(entry.errorCode ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') } : {}),
3572
+ };
3573
+ });
3574
+ if (blocked.has(key) && !targetResults.some((entry) => entry.pluginId === target.pluginId && entry.platform === target.platform
3575
+ && entry.scope === target.scope)) {
3576
+ targetResults.unshift({
3577
+ pluginId: target.pluginId,
3578
+ platform: target.platform,
3579
+ scope: target.scope,
3580
+ state: 'skipped',
3581
+ relation: 'requested',
3582
+ });
3583
+ }
3584
+ const hasFailed = targetResults.some((entry) => entry.state === 'failed' || entry.state === 'skipped');
3585
+ const hasCompleted = targetResults.some((entry) => entry.state === 'completed' || entry.state === 'kept');
3586
+ const state = blocked.has(key) ? 'blocked'
3587
+ : !source ? 'notStarted'
3588
+ : hasFailed && hasCompleted ? 'partial'
3589
+ : hasFailed || failedKeys.has(key) ? 'failed'
3590
+ : targetResults.length > 0 && targetResults.every((entry) => entry.state === 'kept')
3591
+ ? 'kept' : targetResults.length > 0 ? 'completed' : 'notStarted';
3592
+ return { state, targets: targetResults };
3593
+ };
3594
+ const roots = [...new Set(taskExecution.taskPlan.requestedTargets
3595
+ .map((target) => target.pluginId))].map((pluginId) => {
3596
+ const requestedTargets = taskExecution.taskPlan.requestedTargets.filter((target) => target.pluginId === pluginId);
3597
+ const positions = requestedTargets.map((target) => relatedResults(target));
3598
+ const successfulPositions = positions.filter((position) => position.state === 'completed' || position.state === 'kept');
3599
+ const state = successfulPositions.length > 0
3600
+ ? successfulPositions.length === positions.length
3601
+ ? positions.every((position) => position.state === 'kept') ? 'kept' : 'completed'
3602
+ : 'partial'
3603
+ : positions.some((position) => position.state === 'failed' || position.state === 'partial')
3604
+ ? 'failed'
3605
+ : positions.some((position) => position.state === 'notStarted') ? 'notStarted' : 'blocked';
3606
+ const targets = [...new Map(positions.flatMap((position) => position.targets).map((target) => [[
3607
+ target.pluginId ?? '', target.platform, target.scope,
3608
+ ].join('\0'), target])).values()];
3609
+ const causeKeys = [...new Set(taskExecution.taskPlan.causes.filter((cause) => cause.consumers.some((consumer) => consumer.pluginId === pluginId))
3610
+ .map((cause) => cause.causeKey))];
3611
+ return { pluginId, state, causeKeys, targets };
3612
+ });
3613
+ const retryTargets = canonicalPluginWriteTaskTargets(roots.flatMap((root) => root.state === 'failed' || root.state === 'partial' || root.state === 'notStarted'
3614
+ ? failedTargets.filter((target) => target.pluginId === root.pluginId) : []));
3615
+ const completedCount = roots.filter((root) => root.state === 'completed'
3616
+ || root.state === 'kept' || root.state === 'partial').length;
3617
+ const failedCount = roots.filter((root) => root.state === 'failed' || root.state === 'partial'
3618
+ || root.state === 'notStarted' || root.state === 'blocked').length;
3619
+ const code = failedCount === 0 ? 'operationSucceeded'
3620
+ : completedCount > 0 ? 'operationPartial' : 'operationFailed';
3621
+ return {
3622
+ code,
3623
+ message: code === 'operationSucceeded' ? '任务已完成。'
3624
+ : code === 'operationPartial' ? '任务只完成了一部分。' : '任务没有完成。',
3625
+ warnings: taskExecution.warnings,
3626
+ recoveryActions: [...new Set(taskExecution.targetResults.flatMap((entry) => entry.recoveryActions ?? []))],
3627
+ ...(taskExecution.operationPlan?.planFingerprint
3628
+ ? { planFingerprint: taskExecution.operationPlan.planFingerprint } : {}),
3629
+ task: {
3630
+ goal: intent.goal,
3631
+ requestedTargets: taskExecution.taskPlan.requestedTargets,
3632
+ rootResults: roots,
3633
+ ...(retryTargets.length > 0 ? { retryTargets } : {}),
3634
+ blockedTargets: taskExecution.taskPlan.blockedTargets,
3635
+ },
3636
+ targets: technicalTargets,
3637
+ };
3638
+ }
2953
3639
  if (intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch') {
2954
3640
  const command = intent.action === 'refreshAndSync' ? 'sync' : intent.action;
2955
3641
  const rawExecution = typeof execution === 'object' && execution !== null
@@ -2995,6 +3681,52 @@ function operationResultSummaryUnsafe(intent, execution) {
2995
3681
  ? pluginWriteBatchFailedRequestedTargets(execution)
2996
3682
  : [];
2997
3683
  const failedRequestedPluginIds = canonicalPluginIds(retryTargets.map((target) => target.pluginId));
3684
+ const requestedTargetKeys = intent.kind === 'pluginWriteBatch'
3685
+ ? new Set(execution.groupResults.flatMap((group) => {
3686
+ const roots = new Set(group.requestedPluginIds);
3687
+ return group.result.targetResults.flatMap((entry) => entry.target.pluginId
3688
+ && roots.has(entry.target.pluginId)
3689
+ && entry.target.scope === group.scope
3690
+ && (group.platform === 'both' || entry.target.platform === group.platform)
3691
+ ? [[entry.target.pluginId, entry.target.platform, entry.target.scope].join('\0')]
3692
+ : []);
3693
+ }))
3694
+ : new Set();
3695
+ const rootResults = intent.kind === 'pluginWriteBatch'
3696
+ ? requestedPluginIds.map((pluginId) => {
3697
+ const observed = (result.targetResults ?? []).flatMap((entry) => entry.target.pluginId === pluginId
3698
+ && requestedTargetKeys.has([
3699
+ pluginId, entry.target.platform, entry.target.scope,
3700
+ ].join('\0')) ? [{
3701
+ pluginId,
3702
+ platform: entry.target.platform,
3703
+ scope: entry.target.scope,
3704
+ state: entry.action === 'skipped' ? 'skipped'
3705
+ : entry.state === 'failed' ? 'failed'
3706
+ : entry.action === 'keep' ? 'kept' : 'completed',
3707
+ relation: 'requested',
3708
+ ...(entry.errorCode
3709
+ ? { errorCode: safeDiagnosticCode(entry.errorCode, 'writeFailed') }
3710
+ : {}),
3711
+ }] : []);
3712
+ const targets = [...new Map([
3713
+ ...retryTargets.filter((target) => target.pluginId === pluginId).map((target) => ({
3714
+ ...target,
3715
+ state: 'skipped',
3716
+ relation: 'requested',
3717
+ })),
3718
+ ...observed,
3719
+ ].map((target) => [[target.pluginId, target.platform, target.scope].join('\0'), target]))
3720
+ .values()];
3721
+ const completed = targets.filter((target) => target.state === 'completed' || target.state === 'kept').length;
3722
+ const failed = targets.length - completed;
3723
+ const state = completed > 0
3724
+ ? failed > 0 ? 'partial'
3725
+ : targets.every((target) => target.state === 'kept') ? 'kept' : 'completed'
3726
+ : failed > 0 ? 'failed' : 'notStarted';
3727
+ return { pluginId, state, targets };
3728
+ })
3729
+ : [];
2998
3730
  return {
2999
3731
  code: result.state === 'succeeded' ? 'operationSucceeded'
3000
3732
  : result.state === 'partial' ? 'operationPartial' : 'operationFailed',
@@ -3017,6 +3749,7 @@ function operationResultSummaryUnsafe(intent, execution) {
3017
3749
  batch: {
3018
3750
  requestedPluginIds,
3019
3751
  failedRequestedPluginIds,
3752
+ rootResults,
3020
3753
  retryTargets,
3021
3754
  groupCount: execution.groupResults.length,
3022
3755
  },