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

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 (62) 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/write-commands.d.ts +2 -1
  22. package/dist/src/commands/write-commands.js +24 -1
  23. package/dist/src/commands/write-commands.js.map +1 -1
  24. package/dist/src/tui/catalog-management-screens.d.ts +45 -0
  25. package/dist/src/tui/catalog-management-screens.js +25 -0
  26. package/dist/src/tui/catalog-management-screens.js.map +1 -0
  27. package/dist/src/tui/catalog-management.d.ts +240 -0
  28. package/dist/src/tui/catalog-management.js +856 -0
  29. package/dist/src/tui/catalog-management.js.map +1 -0
  30. package/dist/src/tui/catalog-sources/session.d.ts +5 -0
  31. package/dist/src/tui/catalog-sources/session.js +49 -1
  32. package/dist/src/tui/catalog-sources/session.js.map +1 -1
  33. package/dist/src/tui/catalog-sources/types.d.ts +11 -0
  34. package/dist/src/tui/console-app-v4.d.ts +1 -1
  35. package/dist/src/tui/console-app-v4.js +2184 -564
  36. package/dist/src/tui/console-app-v4.js.map +1 -1
  37. package/dist/src/tui/controller-runtime.js +609 -13
  38. package/dist/src/tui/controller-runtime.js.map +1 -1
  39. package/dist/src/tui/controller.d.ts +121 -6
  40. package/dist/src/tui/controller.js +667 -17
  41. package/dist/src/tui/controller.js.map +1 -1
  42. package/dist/src/tui/dependency-graph.d.ts +67 -0
  43. package/dist/src/tui/dependency-graph.js +399 -0
  44. package/dist/src/tui/dependency-graph.js.map +1 -0
  45. package/dist/src/tui/lifecycle-action-view.js +4 -2
  46. package/dist/src/tui/lifecycle-action-view.js.map +1 -1
  47. package/dist/src/tui/model.d.ts +37 -0
  48. package/dist/src/tui/model.js +164 -7
  49. package/dist/src/tui/model.js.map +1 -1
  50. package/dist/src/tui/operation-review-planner.js +244 -8
  51. package/dist/src/tui/operation-review-planner.js.map +1 -1
  52. package/dist/src/tui/plugin-write-task.d.ts +66 -0
  53. package/dist/src/tui/plugin-write-task.js +265 -0
  54. package/dist/src/tui/plugin-write-task.js.map +1 -0
  55. package/dist/src/tui/router.d.ts +38 -2
  56. package/dist/src/tui/router.js.map +1 -1
  57. package/dist/src/tui/screen-registry.d.ts +105 -6
  58. package/dist/src/tui/screen-registry.js +31 -4
  59. package/dist/src/tui/screen-registry.js.map +1 -1
  60. package/dist/src/utils/catalog-generation-service.d.ts +4 -0
  61. package/dist/src/utils/catalog-generation-service.js.map +1 -1
  62. package/package.json +3 -3
@@ -2,7 +2,7 @@ import crypto from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import { statusCommandV7, statusTargetIntent, } from '../commands/status-v6.js';
4
4
  import { addCatalogSourceCommand, listCatalogSources, preflightCatalogSourceMutation, removeCatalogSourceCommand, setPrimaryCatalogSourceCommand, updateCatalogSourceCommand, } from '../commands/catalog-source.js';
5
- import { cleanupWriteCommand, failedWriteCommandResult, installWriteCommand, mergeWriteCommandResults, reinstallWriteCommand, syncWriteCommand, uninstallWriteCommand, } from '../commands/write-commands.js';
5
+ import { cleanupWriteCommand, failedWriteCommandResult, installWriteCommand, mergeWriteCommandResults, reinstallWriteCommand, syncWriteCommand, taskWriteCommand, uninstallWriteCommand, } from '../commands/write-commands.js';
6
6
  import { resolveSyncIntentSelection } from '../commands/sync-command.js';
7
7
  import { parseScriptTimeoutMs, SCRIPT_TIMEOUT_DEFAULT_MS, } from '../commands/options.js';
8
8
  import { readCliPackageInfo } from '../utils/local-marketplace.js';
@@ -18,7 +18,9 @@ import { createBatchIntentCompatiblePlanFingerprint } from '../commands/operatio
18
18
  import { offlineCleanupEvidenceFromInventory, offlineCleanupEvidenceMatches, } from '../commands/offline-cleanup-evidence.js';
19
19
  import { inventoryCatalogSourceReferences } from '../utils/catalog-source-references.js';
20
20
  import { operationEvidenceMatches, operationEvidenceFingerprint, TuiConfigInvalidStoreError, TuiController, TuiOperationBlockedError, } from './controller.js';
21
- import { buildConfigGroups, buildConfigPage, projectPlatformPlacement, } from './model.js';
21
+ import { buildConfigGroups, buildConfigPage, buildTuiViewModel, projectPlatformPlacement, } from './model.js';
22
+ import { buildPluginWriteTaskCandidates, buildPluginWriteTaskDraft, evaluatePluginWriteTaskRestoreVersion, pluginWriteTaskTargetKey, } from './plugin-write-task.js';
23
+ import { createCatalogManagementService, } from './catalog-management.js';
22
24
  import { projectCatalogSourceLocation } from '../utils/catalog-source-presentation.js';
23
25
  import { calculateCatalogSourceConfigFingerprint, calculateCatalogSourceIdentity, } from '../utils/catalog-source-identity.js';
24
26
  import { checkAwCapabilitySupport, readPluginManifest } from '../utils/manifest.js';
@@ -208,6 +210,9 @@ function pluginIntent(intent) {
208
210
  return 'pluginId' in intent;
209
211
  }
210
212
  function reviewedScriptTimeoutMs(intent) {
213
+ if (intent.kind === 'pluginWriteTask') {
214
+ return parseScriptTimeoutMs(intent.scriptTimeoutMs ?? SCRIPT_TIMEOUT_DEFAULT_MS);
215
+ }
211
216
  if ((intent.kind === 'pluginWrite' || intent.kind === 'pluginWriteBatch')
212
217
  && (intent.action === 'install' || intent.action === 'refreshAndSync'
213
218
  || intent.action === 'reinstall' || intent.action === 'sync')) {
@@ -229,9 +234,9 @@ function selectedBatchGroups(intent) {
229
234
  }));
230
235
  const normalizedTargets = rawTargets.map((target) => {
231
236
  if ((target.platform !== 'codex' && target.platform !== 'claude' && target.platform !== 'both')
232
- || target.scope !== intent.scope
237
+ || (target.scope !== 'project' && target.scope !== 'user')
233
238
  || (intent.selection.kind === 'targets' && target.platform === 'both')) {
234
- throw new TuiOperationBlockedError('batchTargetInvalid', '批量精确目标必须使用共同 scope 和单个平台。');
239
+ throw new TuiOperationBlockedError('batchTargetInvalid', '批量精确目标必须使用有效 scope 和单个平台。');
235
240
  }
236
241
  const pluginId = target.pluginId;
237
242
  const parsed = parsePluginId(pluginId);
@@ -563,6 +568,7 @@ function createReviewStageReporter(options) {
563
568
  };
564
569
  }
565
570
  export function createTuiControllerDependencies(context, progressFeed, options = {}) {
571
+ const catalogManagement = createCatalogManagementService(context);
566
572
  const readInventory = options.inventoryCatalogSourceReferences
567
573
  ?? inventoryCatalogSourceReferences;
568
574
  const validatedStoreEntries = new Map();
@@ -597,6 +603,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
597
603
  };
598
604
  };
599
605
  const catalogOperationByEvidence = new WeakMap();
606
+ const taskCatalogOperationsByEvidence = new WeakMap();
600
607
  const codexManagerByEvidence = new WeakMap();
601
608
  let sessionCatalogOperationContext;
602
609
  let sessionStatusResult;
@@ -768,6 +775,20 @@ export function createTuiControllerDependencies(context, progressFeed, options =
768
775
  };
769
776
  const captureEvidence = async (intent, signal, preparation, operationInventory = createOperationInventoryReader(), freshCatalog = false, reusableCandidate, codexObservationSession, frozenBatchEvidence) => {
770
777
  reviewedScriptTimeoutMs(intent);
778
+ if (intent.kind === 'catalogManagement') {
779
+ signal?.throwIfAborted();
780
+ await preparation?.reporter.emit('validate');
781
+ const managementEvidence = await catalogManagement.captureEvidence(intent);
782
+ signal?.throwIfAborted();
783
+ await preparation?.reporter.emit('evidence');
784
+ return {
785
+ catalog: {
786
+ configDigest: managementEvidence.workspaceBinding ?? 'catalog-store',
787
+ compositeDigest: managementEvidence.evidenceDigest,
788
+ },
789
+ catalogManagement: managementEvidence,
790
+ };
791
+ }
771
792
  if (intent.kind === 'configReset') {
772
793
  const parsed = parsePluginId(intent.pluginId);
773
794
  if (!parsed.sourceId || !parsed.pluginName) {
@@ -1157,10 +1178,392 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1157
1178
  }
1158
1179
  return evidence;
1159
1180
  }
1160
- if (!pluginIntent(intent) && intent.kind !== 'pluginWriteBatch') {
1181
+ if (!pluginIntent(intent) && intent.kind !== 'pluginWriteBatch'
1182
+ && intent.kind !== 'pluginWriteTask') {
1161
1183
  throw new TuiOperationBlockedError('operationNotImplemented', '这个写操作尚未接入新 Controller。');
1162
1184
  }
1163
1185
  signal?.throwIfAborted();
1186
+ if (intent.kind === 'pluginWriteTask') {
1187
+ await preparation?.reporter.emit('status');
1188
+ const packageInfo = await readCliPackageInfo();
1189
+ const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
1190
+ const requestedBySource = new Map();
1191
+ for (const target of intent.targets) {
1192
+ const parsed = parsePluginId(target.pluginId);
1193
+ if (!parsed.sourceId || formatPluginId({
1194
+ sourceId: parsed.sourceId,
1195
+ pluginName: parsed.pluginName,
1196
+ }) !== target.pluginId) {
1197
+ throw new TuiOperationBlockedError('invalidPluginId', '任务目标必须使用 canonical pluginId。');
1198
+ }
1199
+ requestedBySource.set(parsed.sourceId, [
1200
+ ...(requestedBySource.get(parsed.sourceId) ?? []),
1201
+ target,
1202
+ ]);
1203
+ }
1204
+ const sources = [];
1205
+ const executableTargets = [];
1206
+ const causes = [];
1207
+ const blockedTargets = [];
1208
+ const observedTargets = [];
1209
+ const observedRevisions = [];
1210
+ const operationContexts = new Map();
1211
+ await preparation?.reporter.emit('catalog');
1212
+ for (const [sourceId, requestedTargets] of [...requestedBySource.entries()]
1213
+ .sort(([left], [right]) => left.localeCompare(right, 'en'))) {
1214
+ signal?.throwIfAborted();
1215
+ let catalogOperation;
1216
+ try {
1217
+ catalogOperation = !freshCatalog && sessionCatalogOperationContext
1218
+ ? selectCatalogOperationContext(sessionCatalogOperationContext, [sourceId])
1219
+ : await resolveCatalogOperationContext({
1220
+ sourceIds: [sourceId],
1221
+ env: context.env,
1222
+ ...(!freshCatalog ? { validatedStoreEntries, validatingStoreEntries } : {}),
1223
+ ...(signal ? { signal } : {}),
1224
+ });
1225
+ }
1226
+ catch {
1227
+ blockedTargets.push(...requestedTargets.map((target) => ({
1228
+ target,
1229
+ reason: 'targetUnavailable',
1230
+ issueIds: [],
1231
+ })));
1232
+ continue;
1233
+ }
1234
+ operationContexts.set(sourceId, catalogOperation);
1235
+ const status = await statusCommandV7({
1236
+ plugins: [...new Set(requestedTargets.map((target) => target.pluginId))],
1237
+ cwd: context.cwd,
1238
+ env: context.env,
1239
+ cliVersion: packageInfo.version,
1240
+ catalogOperationContext: catalogOperation,
1241
+ inventoryCatalogSourceReferences: operationInventory,
1242
+ });
1243
+ observedTargets.push(...status.payload.targets);
1244
+ observedRevisions.push(...status.targetRevisions);
1245
+ const model = buildTuiViewModel({
1246
+ status: status.payload,
1247
+ pluginSummaries: status.pluginSummaries,
1248
+ targetRevisions: status.targetRevisions,
1249
+ });
1250
+ const runtimeCandidates = intent.goal === 'restore'
1251
+ ? buildPluginWriteTaskCandidates(model, 'restore')
1252
+ : requestedTargets.map((target) => {
1253
+ const expected = target.expectedUpdate;
1254
+ const revision = status.targetRevisions.find((entry) => entry.pluginId === target.pluginId && entry.platform === target.platform
1255
+ && entry.scope === target.scope);
1256
+ const live = status.payload.targets.find((entry) => {
1257
+ if (entry.pluginId !== target.pluginId || entry.platform !== target.platform)
1258
+ return false;
1259
+ const desired = statusTargetIntent(entry);
1260
+ return entry.scope === target.scope || desired.scopes.includes(target.scope);
1261
+ });
1262
+ const generation = catalogOperation.generationSet.sources.find((entry) => entry.sourceId === sourceId);
1263
+ const eligible = Boolean(expected && live && revision
1264
+ && live.sourceIdentity === expected.sourceIdentity
1265
+ && generation?.storeKey === expected.generationKey
1266
+ && revision.revision.installedVersion === expected.installedVersion
1267
+ && revision.revision.availableVersion === expected.activeVersion
1268
+ && revision.revision.state === 'upgradeAvailable');
1269
+ return {
1270
+ target,
1271
+ displayName: model.plugins.find((plugin) => plugin.pluginId === target.pluginId)
1272
+ ?.displayName ?? parsePluginId(target.pluginId).pluginName,
1273
+ sourceId,
1274
+ eligible,
1275
+ ...(!eligible ? { blockReason: 'updateEvidenceIncomplete' } : {}),
1276
+ recommendedMethod: live?.state === 'ready' ? 'sync' : 'reinstall',
1277
+ issueIds: [],
1278
+ };
1279
+ });
1280
+ const draft = buildPluginWriteTaskDraft({ ...intent, targets: requestedTargets }, model, runtimeCandidates);
1281
+ blockedTargets.push(...draft.blockedTargets);
1282
+ if (draft.executableTargets.length === 0)
1283
+ continue;
1284
+ const methodsByTarget = new Map();
1285
+ for (const cause of draft.causes) {
1286
+ const key = pluginWriteTaskTargetKey(cause.repairTarget);
1287
+ const methods = methodsByTarget.get(key) ?? new Set();
1288
+ methods.add(cause.method);
1289
+ methodsByTarget.set(key, methods);
1290
+ }
1291
+ const conflicted = new Set([...methodsByTarget].filter(([, methods]) => methods.size > 1)
1292
+ .map(([key]) => key));
1293
+ if (conflicted.size > 0) {
1294
+ const blockedKeys = new Set(draft.causes.filter((cause) => conflicted.has(pluginWriteTaskTargetKey(cause.repairTarget)))
1295
+ .flatMap((cause) => cause.consumers.map(pluginWriteTaskTargetKey)));
1296
+ blockedTargets.push(...draft.executableTargets.filter((target) => blockedKeys.has(pluginWriteTaskTargetKey(target))).map((target) => ({
1297
+ target,
1298
+ reason: 'taskActionConflict',
1299
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1300
+ .map((cause) => cause.issueId),
1301
+ })));
1302
+ draft.executableTargets = draft.executableTargets.filter((target) => !blockedKeys.has(pluginWriteTaskTargetKey(target)));
1303
+ draft.causes = draft.causes.filter((cause) => cause.consumers.some((consumer) => !blockedKeys.has(pluginWriteTaskTargetKey(consumer))));
1304
+ }
1305
+ if (draft.executableTargets.length === 0)
1306
+ continue;
1307
+ const taskTargets = () => draft.executableTargets.map((target) => {
1308
+ const direct = draft.causes.filter((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === pluginWriteTaskTargetKey(target));
1309
+ return {
1310
+ pluginId: target.pluginId,
1311
+ platform: target.platform,
1312
+ scope: target.scope,
1313
+ method: direct.some((cause) => cause.method === 'reinstall')
1314
+ ? 'reinstall' : 'sync',
1315
+ };
1316
+ });
1317
+ const repairTargets = () => [...new Map(draft.causes.map((cause) => [
1318
+ pluginWriteTaskTargetKey(cause.repairTarget),
1319
+ { ...cause.repairTarget, method: cause.method },
1320
+ ])).values()];
1321
+ let preview;
1322
+ while (draft.executableTargets.length > 0) {
1323
+ preview = await taskWriteCommand(taskTargets(), {
1324
+ cwd: context.cwd,
1325
+ repairTargets: repairTargets(),
1326
+ ...(reviewedScriptTimeoutMs(intent) === undefined
1327
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
1328
+ }, {
1329
+ ...context,
1330
+ catalogOperationContext: catalogOperation,
1331
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
1332
+ expectedCatalogConfigDigest: configSnapshot.digest,
1333
+ inventoryCatalogSourceReferences: operationInventory,
1334
+ previewOperationPlan: true,
1335
+ ...(codexObservationSession ? { codexObservationSession } : {}),
1336
+ ...(preparation ? {
1337
+ onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
1338
+ } : {}),
1339
+ });
1340
+ if (!preview.operationPlan) {
1341
+ blockedTargets.push(...draft.executableTargets.map((target) => ({
1342
+ target,
1343
+ reason: 'targetUnavailable',
1344
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1345
+ .map((cause) => cause.issueId),
1346
+ })));
1347
+ draft.executableTargets = [];
1348
+ draft.causes = [];
1349
+ break;
1350
+ }
1351
+ if (intent.goal === 'restore') {
1352
+ const grouped = new Map();
1353
+ const blockedByVersion = new Map();
1354
+ for (const cause of draft.causes) {
1355
+ const placements = (preview.dependencyPlan?.targets ?? []).filter((target) => target.pluginId === cause.repairTarget.pluginId
1356
+ && target.platform === cause.repairTarget.platform);
1357
+ const placement = placements.find((target) => target.requestedScope === cause.repairTarget.scope)
1358
+ ?? (placements.length === 1 ? placements[0] : undefined);
1359
+ if (!placement) {
1360
+ throw new TuiOperationBlockedError('taskRepairPlacementMissing', '任务修复目标无法绑定到依赖位置。');
1361
+ }
1362
+ const repairTarget = {
1363
+ pluginId: cause.repairTarget.pluginId,
1364
+ platform: cause.repairTarget.platform,
1365
+ scope: placement.requestedScope ?? placement.resolvedScope,
1366
+ };
1367
+ const version = evaluatePluginWriteTaskRestoreVersion(repairTarget, cause.restoreVersionEvidence?.state === 'absent', model);
1368
+ if (!version.evidence || version.blockReason) {
1369
+ for (const consumer of cause.consumers) {
1370
+ const key = pluginWriteTaskTargetKey(consumer);
1371
+ const current = blockedByVersion.get(key) ?? {
1372
+ reason: version.blockReason ?? 'restoreVersionUnverified',
1373
+ issueIds: new Set(),
1374
+ };
1375
+ if (version.blockReason === 'restoreVersionChanged') {
1376
+ current.reason = version.blockReason;
1377
+ }
1378
+ current.issueIds.add(cause.issueId);
1379
+ blockedByVersion.set(key, current);
1380
+ }
1381
+ continue;
1382
+ }
1383
+ const causeKey = ['restore', cause.sourceIdentity,
1384
+ pluginWriteTaskTargetKey(repairTarget)].join('\0');
1385
+ const current = grouped.get(causeKey);
1386
+ grouped.set(causeKey, {
1387
+ cause: {
1388
+ ...cause,
1389
+ causeKey,
1390
+ issueId: [cause.issueId, ...(current?.issueIds ?? [])]
1391
+ .sort((left, right) => left.localeCompare(right, 'en'))[0],
1392
+ repairTarget,
1393
+ consumers: [...new Map([
1394
+ ...(current?.cause.consumers ?? []),
1395
+ ...cause.consumers,
1396
+ ].map((consumer) => [pluginWriteTaskTargetKey(consumer), consumer])).values()]
1397
+ .sort((left, right) => pluginWriteTaskTargetKey(left)
1398
+ .localeCompare(pluginWriteTaskTargetKey(right), 'en')),
1399
+ restoreVersionEvidence: version.evidence,
1400
+ },
1401
+ issueIds: new Set([...(current?.issueIds ?? []), cause.issueId]),
1402
+ methods: new Set([...(current?.methods ?? []), cause.method]),
1403
+ });
1404
+ }
1405
+ for (const group of grouped.values()) {
1406
+ if (group.methods.size < 2)
1407
+ continue;
1408
+ for (const consumer of group.cause.consumers) {
1409
+ const key = pluginWriteTaskTargetKey(consumer);
1410
+ const current = blockedByVersion.get(key) ?? {
1411
+ reason: 'taskActionConflict',
1412
+ issueIds: new Set(),
1413
+ };
1414
+ group.issueIds.forEach((issueId) => current.issueIds.add(issueId));
1415
+ blockedByVersion.set(key, current);
1416
+ }
1417
+ }
1418
+ const newlyBlockedKeys = new Set(blockedByVersion.keys());
1419
+ blockedTargets.push(...draft.executableTargets.flatMap((target) => {
1420
+ const blocked = blockedByVersion.get(pluginWriteTaskTargetKey(target));
1421
+ return blocked ? [{
1422
+ target,
1423
+ reason: blocked.reason,
1424
+ issueIds: [...blocked.issueIds].sort((left, right) => left.localeCompare(right, 'en')),
1425
+ }] : [];
1426
+ }));
1427
+ const normalizedCauses = [...grouped.values()]
1428
+ .filter((group) => group.methods.size === 1)
1429
+ .map((group) => ({
1430
+ ...group.cause,
1431
+ consumers: group.cause.consumers.filter((consumer) => !newlyBlockedKeys.has(pluginWriteTaskTargetKey(consumer))),
1432
+ }))
1433
+ .filter((cause) => cause.consumers.length > 0)
1434
+ .sort((left, right) => left.causeKey.localeCompare(right.causeKey, 'en'));
1435
+ const normalizedTargets = draft.executableTargets.filter((target) => !newlyBlockedKeys.has(pluginWriteTaskTargetKey(target)));
1436
+ const changed = JSON.stringify(normalizedCauses) !== JSON.stringify(draft.causes)
1437
+ || JSON.stringify(normalizedTargets) !== JSON.stringify(draft.executableTargets);
1438
+ draft.causes = normalizedCauses;
1439
+ draft.executableTargets = normalizedTargets;
1440
+ if (changed)
1441
+ continue;
1442
+ }
1443
+ const blockedPlanTargets = (preview.dependencyPlan?.targets ?? []).filter((target) => target.action === 'block' || target.errorCode !== undefined);
1444
+ if (blockedPlanTargets.length === 0)
1445
+ break;
1446
+ const requestedKeys = new Set(draft.executableTargets.map(pluginWriteTaskTargetKey));
1447
+ const errorCodesByRoot = new Map();
1448
+ for (const blockedPlanTarget of blockedPlanTargets) {
1449
+ const pending = [blockedPlanTarget];
1450
+ const visited = new Set();
1451
+ const mappedRoots = new Set();
1452
+ const repairKey = pluginWriteTaskTargetKey({
1453
+ pluginId: blockedPlanTarget.pluginId,
1454
+ platform: blockedPlanTarget.platform,
1455
+ scope: blockedPlanTarget.requestedScope ?? blockedPlanTarget.resolvedScope,
1456
+ });
1457
+ for (const cause of draft.causes) {
1458
+ if (pluginWriteTaskTargetKey(cause.repairTarget) !== repairKey)
1459
+ continue;
1460
+ cause.consumers.forEach((consumer) => mappedRoots.add(pluginWriteTaskTargetKey(consumer)));
1461
+ }
1462
+ while (pending.length > 0) {
1463
+ const current = pending.shift();
1464
+ const currentKey = pluginWriteTaskTargetKey({
1465
+ pluginId: current.pluginId,
1466
+ platform: current.platform,
1467
+ scope: current.requestedScope ?? current.resolvedScope,
1468
+ });
1469
+ if (visited.has(currentKey))
1470
+ continue;
1471
+ visited.add(currentKey);
1472
+ if (current.relation === 'requested' && requestedKeys.has(currentKey)) {
1473
+ mappedRoots.add(currentKey);
1474
+ }
1475
+ for (const consumer of current.requiredByTargets ?? []) {
1476
+ const consumerKey = pluginWriteTaskTargetKey(consumer);
1477
+ if (requestedKeys.has(consumerKey)) {
1478
+ mappedRoots.add(consumerKey);
1479
+ continue;
1480
+ }
1481
+ const placement = preview.dependencyPlan?.targets.find((target) => target.pluginId === consumer.pluginId && target.platform === consumer.platform
1482
+ && (target.requestedScope ?? target.resolvedScope) === consumer.scope);
1483
+ if (placement)
1484
+ pending.push(placement);
1485
+ }
1486
+ }
1487
+ if (mappedRoots.size === 0) {
1488
+ throw new TuiOperationBlockedError('taskPlanConsumerMissing', '任务计划中的阻止项无法映射到请求目标。');
1489
+ }
1490
+ for (const key of mappedRoots) {
1491
+ const codes = errorCodesByRoot.get(key) ?? new Set();
1492
+ codes.add(blockedPlanTarget.errorCode ?? 'writePlanBlocked');
1493
+ errorCodesByRoot.set(key, codes);
1494
+ }
1495
+ }
1496
+ blockedTargets.push(...draft.executableTargets.flatMap((target) => {
1497
+ const codes = errorCodesByRoot.get(pluginWriteTaskTargetKey(target));
1498
+ return codes ? [{
1499
+ target,
1500
+ reason: 'targetUnavailable',
1501
+ issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
1502
+ .map((cause) => cause.issueId),
1503
+ errorCodes: [...codes].sort((left, right) => left.localeCompare(right, 'en')),
1504
+ }] : [];
1505
+ }));
1506
+ draft.executableTargets = draft.executableTargets.filter((target) => !errorCodesByRoot.has(pluginWriteTaskTargetKey(target)));
1507
+ draft.causes = draft.causes.map((cause) => ({
1508
+ ...cause,
1509
+ consumers: cause.consumers.filter((consumer) => !errorCodesByRoot.has(pluginWriteTaskTargetKey(consumer))),
1510
+ })).filter((cause) => cause.consumers.length > 0);
1511
+ }
1512
+ if (!preview?.operationPlan || draft.executableTargets.length === 0)
1513
+ continue;
1514
+ const sourceIdentity = draft.causes[0]?.sourceIdentity
1515
+ ?? status.payload.catalog.sources.find((source) => source.id === sourceId)?.identity;
1516
+ if (!sourceIdentity) {
1517
+ throw new TuiOperationBlockedError('taskSourceIdentityMissing', '任务来源身份不可验证。');
1518
+ }
1519
+ const sourceEvidence = {
1520
+ sourceId,
1521
+ sourceIdentity,
1522
+ requestedTargets: draft.executableTargets,
1523
+ causes: draft.causes,
1524
+ preview,
1525
+ executable: true,
1526
+ };
1527
+ sources.push(sourceEvidence);
1528
+ executableTargets.push(...draft.executableTargets);
1529
+ causes.push(...draft.causes);
1530
+ }
1531
+ const canonicalBlocked = [...new Map(blockedTargets.map((target) => [
1532
+ pluginWriteTaskTargetKey(target.target), target,
1533
+ ])).values()].sort((left, right) => pluginWriteTaskTargetKey(left.target)
1534
+ .localeCompare(pluginWriteTaskTargetKey(right.target), 'en'));
1535
+ if (executableTargets.length === 0) {
1536
+ throw new TuiOperationBlockedError('taskNoExecutableTargets', '任务没有可安全执行的目标。');
1537
+ }
1538
+ if (intent.executionPolicy === 'all' && canonicalBlocked.length > 0) {
1539
+ throw new TuiOperationBlockedError('taskHasBlockedTargets', '部分目标需要先处理。请调整选择。');
1540
+ }
1541
+ await preparation?.reporter.emit('evidence');
1542
+ const evidence = {
1543
+ catalog: {
1544
+ configDigest: configSnapshot.digest,
1545
+ compositeDigest: digest(sources.map((source) => [
1546
+ source.sourceId,
1547
+ source.sourceIdentity,
1548
+ source.preview.operationPlan?.planFingerprint,
1549
+ ])),
1550
+ },
1551
+ taskPlan: {
1552
+ goal: intent.goal,
1553
+ requestedTargets: intent.targets,
1554
+ executableTargets: [...executableTargets].sort((left, right) => pluginWriteTaskTargetKey(left).localeCompare(pluginWriteTaskTargetKey(right), 'en')),
1555
+ causes: [...new Map(causes.map((cause) => [cause.causeKey, cause])).values()]
1556
+ .sort((left, right) => left.causeKey.localeCompare(right.causeKey, 'en')),
1557
+ blockedTargets: canonicalBlocked,
1558
+ sources,
1559
+ targetFingerprint: statusTargetFingerprint(observedTargets, observedRevisions),
1560
+ planFingerprint: '',
1561
+ },
1562
+ };
1563
+ evidence.taskPlan.planFingerprint = operationEvidenceFingerprint(evidence);
1564
+ taskCatalogOperationsByEvidence.set(evidence, operationContexts);
1565
+ return evidence;
1566
+ }
1164
1567
  if (intent.kind === 'pluginWriteBatch') {
1165
1568
  await preparation?.reporter.emit('status');
1166
1569
  const selection = await resolveBatchGroups(intent, context.env, context.cwd, frozenBatchEvidence?.requestedPluginIds);
@@ -1580,6 +1983,21 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1580
1983
  };
1581
1984
  },
1582
1985
  });
1986
+ if (intent.kind === 'pluginWrite' && intent.expectedUpdate) {
1987
+ const revision = status.targetRevisions.find((entry) => entry.pluginId === intent.pluginId && entry.platform === intent.platform
1988
+ && entry.scope === intent.scope);
1989
+ const generation = catalogOperation?.generationSet.sources.find((entry) => entry.sourceId === selected.source.sourceId);
1990
+ if (intent.action !== 'sync'
1991
+ || !('pluginVersion' in selected.source)
1992
+ || selected.source.sourceIdentity !== intent.expectedUpdate.sourceIdentity
1993
+ || selected.source.pluginVersion !== intent.expectedUpdate.activeVersion
1994
+ || generation?.storeKey !== intent.expectedUpdate.generationKey
1995
+ || revision?.revision.installedVersion !== intent.expectedUpdate.installedVersion
1996
+ || revision?.revision.availableVersion !== intent.expectedUpdate.activeVersion
1997
+ || revision.revision.state !== 'upgradeAvailable') {
1998
+ throw operationReviewStale('插件更新版本或 Active Catalog 证据已变化。');
1999
+ }
2000
+ }
1583
2001
  let codexManager;
1584
2002
  let resolvedCodexManager;
1585
2003
  const hasDeclaredDependencies = (status.pluginSummaries.find((summary) => summary.pluginId === intent.pluginId)?.dependencyTotalCount ?? 0) > 0;
@@ -1743,6 +2161,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1743
2161
  return evidence;
1744
2162
  };
1745
2163
  const dependencies = {
2164
+ catalogManagement,
1746
2165
  beginCatalogSession(sessionOptions) {
1747
2166
  sessionCatalogOperationContext = undefined;
1748
2167
  if (sessionOptions?.reuseValidatedGenerations !== true) {
@@ -1817,11 +2236,25 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1817
2236
  validatedStoreEntries.clear();
1818
2237
  validatingStoreEntries.clear();
1819
2238
  }
1820
- const evidence = await captureEvidence(intent, signal, {
1821
- reporter,
1822
- validateIntent: true,
1823
- }, createOperationInventoryReader(), intent.kind === 'pluginWrite'
1824
- && intent.action === 'refreshAndSync', reusableCandidate);
2239
+ const taskObservationSession = intent.kind === 'pluginWriteTask'
2240
+ && intent.targets.some((target) => target.platform === 'codex')
2241
+ ? await createCodexNativeObservationSession({
2242
+ exactCwd: context.cwd,
2243
+ env: context.env,
2244
+ runner: context.nativeCliRunner,
2245
+ })
2246
+ : undefined;
2247
+ let evidence;
2248
+ try {
2249
+ evidence = await captureEvidence(intent, signal, {
2250
+ reporter,
2251
+ validateIntent: true,
2252
+ }, createOperationInventoryReader(), intent.kind === 'pluginWrite'
2253
+ && intent.action === 'refreshAndSync', reusableCandidate, taskObservationSession);
2254
+ }
2255
+ finally {
2256
+ await taskObservationSession?.dispose();
2257
+ }
1825
2258
  if (preparationWarnings.length > 0)
1826
2259
  evidence.preparationWarnings = preparationWarnings;
1827
2260
  return evidence;
@@ -1835,6 +2268,10 @@ export function createTuiControllerDependencies(context, progressFeed, options =
1835
2268
  return evidence.dependencyPlan;
1836
2269
  },
1837
2270
  async validateIntent(intent, signal) {
2271
+ if (intent.kind === 'catalogManagement') {
2272
+ signal.throwIfAborted();
2273
+ return;
2274
+ }
1838
2275
  if (intent.kind === 'catalogSourcesRecovery') {
1839
2276
  signal.throwIfAborted();
1840
2277
  return;
@@ -2082,6 +2519,25 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2082
2519
  };
2083
2520
  },
2084
2521
  async executeIntent(intent, expectedEvidence, signal) {
2522
+ if (intent.kind === 'catalogManagement') {
2523
+ signal?.throwIfAborted();
2524
+ if (!expectedEvidence.catalogManagement) {
2525
+ throw new TuiOperationBlockedError('operationReviewStale', 'Catalog management Review evidence is missing.');
2526
+ }
2527
+ const progressLabels = {
2528
+ validate: '验证 Catalog 状态',
2529
+ build: '构建 Catalog snapshot',
2530
+ source: '更新开发来源',
2531
+ sync: '同步开发来源',
2532
+ prune: '清理 Catalog store',
2533
+ prepare: '准备发布计划',
2534
+ remote: '写入 Catalog 远端',
2535
+ verify: '验证操作结果',
2536
+ };
2537
+ return catalogManagement.execute(intent, expectedEvidence.catalogManagement, (stage) => {
2538
+ progressFeed?.setCurrent({ target: 'Catalog 管理', phase: progressLabels[stage] }, `Catalog 管理:${progressLabels[stage]}`);
2539
+ });
2540
+ }
2085
2541
  if (intent.kind === 'catalogSourcesRecovery') {
2086
2542
  signal?.throwIfAborted();
2087
2543
  return recoverInvalidCatalogSourcesConfig({ env: context.env });
@@ -2341,6 +2797,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2341
2797
  }
2342
2798
  const codexObservationSession = intent.kind === 'pluginWrite'
2343
2799
  || intent.kind === 'pluginWriteBatch'
2800
+ || intent.kind === 'pluginWriteTask'
2344
2801
  ? await createCodexNativeObservationSession({
2345
2802
  exactCwd: context.cwd,
2346
2803
  env: context.env,
@@ -2353,9 +2810,20 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2353
2810
  action: async (awHomeMutationToken) => {
2354
2811
  await tuiControllerRuntimeTestHooks?.beforeMutationEvidenceCapture?.();
2355
2812
  const operationInventory = createOperationInventoryReader();
2356
- const currentEvidence = await captureEvidence(intent, undefined, undefined, operationInventory, true, undefined, codexObservationSession, intent.kind === 'pluginWriteBatch'
2357
- ? expectedEvidence
2358
- : undefined);
2813
+ let currentEvidence;
2814
+ try {
2815
+ currentEvidence = await captureEvidence(intent, undefined, undefined, operationInventory, true, undefined, codexObservationSession, intent.kind === 'pluginWriteBatch'
2816
+ ? expectedEvidence
2817
+ : undefined);
2818
+ }
2819
+ catch (error) {
2820
+ const code = error instanceof TuiOperationBlockedError ? error.code : undefined;
2821
+ if (intent.kind === 'pluginWriteTask'
2822
+ && (code === 'taskHasBlockedTargets' || code === 'taskNoExecutableTargets')) {
2823
+ throw operationReviewStale('确认后任务可执行目标发生变化。');
2824
+ }
2825
+ throw error;
2826
+ }
2359
2827
  if (intent.kind === 'pluginWriteBatch') {
2360
2828
  const expectedBatch = expectedEvidence;
2361
2829
  const currentBatch = currentEvidence;
@@ -2498,6 +2966,134 @@ export function createTuiControllerDependencies(context, progressFeed, options =
2498
2966
  groupResults,
2499
2967
  };
2500
2968
  }
2969
+ if (intent.kind === 'pluginWriteTask') {
2970
+ const expectedTask = expectedEvidence;
2971
+ const currentTask = currentEvidence;
2972
+ if (!operationEvidenceMatches(expectedTask, currentTask)) {
2973
+ throw operationReviewStale('确认后任务目标、根因、版本或写入计划发生变化。');
2974
+ }
2975
+ await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
2976
+ const operationContexts = taskCatalogOperationsByEvidence.get(currentEvidence)
2977
+ ?? new Map();
2978
+ const sourceResults = [];
2979
+ const completedTargetKeys = new Set();
2980
+ const preApplyExecutionCache = new Map();
2981
+ const commandTargets = (source) => source.requestedTargets.map((target) => {
2982
+ const direct = source.causes.filter((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === pluginWriteTaskTargetKey(target));
2983
+ return {
2984
+ pluginId: target.pluginId,
2985
+ platform: target.platform,
2986
+ scope: target.scope,
2987
+ method: direct.some((cause) => cause.method === 'reinstall')
2988
+ ? 'reinstall' : 'sync',
2989
+ };
2990
+ });
2991
+ const repairTargets = (source) => [...new Map(source.causes.map((cause) => [
2992
+ pluginWriteTaskTargetKey(cause.repairTarget),
2993
+ { ...cause.repairTarget, method: cause.method },
2994
+ ])).values()];
2995
+ const stoppedResult = (source, error) => mergeWriteCommandResults(source.requestedTargets.map((target) => failedWriteCommandResult('sync', [target.pluginId], {
2996
+ cwd: context.cwd,
2997
+ platform: target.platform,
2998
+ scope: target.scope,
2999
+ }, error)));
3000
+ const appendStoppedSources = (startIndex, message) => {
3001
+ for (const [offset, source] of expectedTask.taskPlan.sources.slice(startIndex).entries()) {
3002
+ sourceResults.push({
3003
+ sourceId: source.sourceId,
3004
+ sourceIdentity: source.sourceIdentity,
3005
+ requestedTargets: source.requestedTargets,
3006
+ result: stoppedResult(source, offset === 0
3007
+ ? new TuiOperationBlockedError('operationReviewStale', message)
3008
+ : new TuiOperationBlockedError('taskNotStarted', '前一个来源计划已变化,因此这个来源没有开始。')),
3009
+ });
3010
+ }
3011
+ };
3012
+ for (const [sourceIndex, reviewed] of expectedTask.taskPlan.sources.entries()) {
3013
+ const current = currentTask.taskPlan.sources.find((source) => source.sourceId === reviewed.sourceId);
3014
+ const catalogOperation = operationContexts.get(reviewed.sourceId);
3015
+ if (!current || !catalogOperation || current.sourceIdentity !== reviewed.sourceIdentity) {
3016
+ if (sourceResults.length === 0) {
3017
+ throw operationReviewStale('确认后任务来源身份发生变化。');
3018
+ }
3019
+ appendStoppedSources(sourceIndex, '确认后任务来源身份发生变化。');
3020
+ break;
3021
+ }
3022
+ const sourceInventory = sourceResults.length === 0
3023
+ ? operationInventory : createOperationInventoryReader();
3024
+ let executionPreview = current.preview;
3025
+ if (completedTargetKeys.size > 0) {
3026
+ executionPreview = await taskWriteCommand(commandTargets(current), {
3027
+ cwd: context.cwd,
3028
+ repairTargets: repairTargets(current),
3029
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3030
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3031
+ }, {
3032
+ ...context,
3033
+ awHomeMutationToken,
3034
+ catalogOperationContext: catalogOperation,
3035
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
3036
+ expectedCatalogConfigDigest: expectedTask.catalog.configDigest,
3037
+ inventoryCatalogSourceReferences: sourceInventory,
3038
+ previewOperationPlan: true,
3039
+ preApplyExecutionCache,
3040
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3041
+ });
3042
+ if (!reviewed.preview.operationPlan || !executionPreview.operationPlan
3043
+ || createBatchIntentCompatiblePlanFingerprint(reviewed.preview.operationPlan, completedTargetKeys) !== createBatchIntentCompatiblePlanFingerprint(executionPreview.operationPlan, completedTargetKeys)) {
3044
+ appendStoppedSources(sourceIndex, '前序结果改变了这个来源的实际写入计划。');
3045
+ break;
3046
+ }
3047
+ }
3048
+ const requiresManagedCodex = reviewed.preview.codexManagerPreview?.state === 'requires-install';
3049
+ const result = await taskWriteCommand(commandTargets(current), {
3050
+ cwd: context.cwd,
3051
+ repairTargets: repairTargets(current),
3052
+ ...(reviewedScriptTimeoutMs(intent) === undefined
3053
+ ? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
3054
+ }, {
3055
+ ...context,
3056
+ awHomeMutationToken,
3057
+ catalogOperationContext: catalogOperation,
3058
+ expectedGenerationSetDigest: catalogOperation.generationSetDigest,
3059
+ expectedCatalogConfigDigest: expectedTask.catalog.configDigest,
3060
+ inventoryCatalogSourceReferences: sourceInventory,
3061
+ preApplyConfirmed: true,
3062
+ preApplyExecutionCache,
3063
+ ...(executionPreview.operationPlan
3064
+ ? { expectedPlanFingerprint: executionPreview.operationPlan.planFingerprint } : {}),
3065
+ ...(requiresManagedCodex ? { managedCodexAuthorizer: async () => true } : {}),
3066
+ ...(codexObservationSession ? { codexObservationSession } : {}),
3067
+ });
3068
+ if (writeReviewIsStale(result)) {
3069
+ if (sourceResults.length === 0)
3070
+ throwIfWriteReviewStale(result);
3071
+ appendStoppedSources(sourceIndex, '这个来源的实际写入计划发生变化。');
3072
+ break;
3073
+ }
3074
+ for (const target of result.targetResults) {
3075
+ if (target.state === 'completed') {
3076
+ completedTargetKeys.add([
3077
+ target.target.pluginId,
3078
+ target.target.platform,
3079
+ target.target.scope,
3080
+ ].join('\0'));
3081
+ }
3082
+ }
3083
+ sourceResults.push({
3084
+ sourceId: reviewed.sourceId,
3085
+ sourceIdentity: reviewed.sourceIdentity,
3086
+ requestedTargets: reviewed.requestedTargets,
3087
+ result,
3088
+ });
3089
+ }
3090
+ const merged = mergeWriteCommandResults(sourceResults.map((source) => source.result));
3091
+ return {
3092
+ ...merged,
3093
+ taskPlan: expectedTask.taskPlan,
3094
+ sourceResults,
3095
+ };
3096
+ }
2501
3097
  if (intent.kind !== 'pluginWrite'
2502
3098
  && !operationEvidenceMatches(expectedEvidence, currentEvidence)) {
2503
3099
  throw new TuiOperationBlockedError('operationReviewStale', '确认后状态发生变化。');