@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.
- package/dist/src/commands/catalog-dev-source.d.ts +31 -0
- package/dist/src/commands/catalog-dev-source.js +165 -5
- package/dist/src/commands/catalog-dev-source.js.map +1 -1
- package/dist/src/commands/catalog-generation.d.ts +5 -0
- package/dist/src/commands/catalog-generation.js.map +1 -1
- package/dist/src/commands/catalog-publication-journal.d.ts +12 -0
- package/dist/src/commands/catalog-publication-journal.js +77 -0
- package/dist/src/commands/catalog-publication-journal.js.map +1 -1
- package/dist/src/commands/catalog-store.d.ts +5 -0
- package/dist/src/commands/catalog-store.js +19 -1
- package/dist/src/commands/catalog-store.js.map +1 -1
- package/dist/src/commands/dependency-operation-plan.d.ts +7 -0
- package/dist/src/commands/dependency-operation-plan.js +63 -27
- package/dist/src/commands/dependency-operation-plan.js.map +1 -1
- package/dist/src/commands/rebuild-command.d.ts +22 -8
- package/dist/src/commands/rebuild-command.js +562 -250
- package/dist/src/commands/rebuild-command.js.map +1 -1
- package/dist/src/commands/reconcile-operation-plan.d.ts +11 -9
- package/dist/src/commands/reconcile-operation-plan.js +25 -6
- package/dist/src/commands/reconcile-operation-plan.js.map +1 -1
- package/dist/src/commands/status-v6.d.ts +2 -1
- package/dist/src/commands/status-v6.js +7 -1
- package/dist/src/commands/status-v6.js.map +1 -1
- package/dist/src/commands/write-commands.d.ts +2 -1
- package/dist/src/commands/write-commands.js +24 -1
- package/dist/src/commands/write-commands.js.map +1 -1
- package/dist/src/tui/catalog-management-screens.d.ts +45 -0
- package/dist/src/tui/catalog-management-screens.js +25 -0
- package/dist/src/tui/catalog-management-screens.js.map +1 -0
- package/dist/src/tui/catalog-management.d.ts +240 -0
- package/dist/src/tui/catalog-management.js +856 -0
- package/dist/src/tui/catalog-management.js.map +1 -0
- package/dist/src/tui/catalog-sources/session.d.ts +5 -0
- package/dist/src/tui/catalog-sources/session.js +49 -1
- package/dist/src/tui/catalog-sources/session.js.map +1 -1
- package/dist/src/tui/catalog-sources/types.d.ts +11 -0
- package/dist/src/tui/console-app-v4.d.ts +1 -1
- package/dist/src/tui/console-app-v4.js +2597 -587
- package/dist/src/tui/console-app-v4.js.map +1 -1
- package/dist/src/tui/controller-runtime.js +668 -50
- package/dist/src/tui/controller-runtime.js.map +1 -1
- package/dist/src/tui/controller.d.ts +136 -7
- package/dist/src/tui/controller.js +752 -19
- package/dist/src/tui/controller.js.map +1 -1
- package/dist/src/tui/dependency-graph.d.ts +67 -0
- package/dist/src/tui/dependency-graph.js +399 -0
- package/dist/src/tui/dependency-graph.js.map +1 -0
- package/dist/src/tui/lifecycle-action-view.js +4 -2
- package/dist/src/tui/lifecycle-action-view.js.map +1 -1
- package/dist/src/tui/model.d.ts +37 -0
- package/dist/src/tui/model.js +163 -7
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/operation-review-planner.js +266 -9
- package/dist/src/tui/operation-review-planner.js.map +1 -1
- package/dist/src/tui/plugin-write-task.d.ts +67 -0
- package/dist/src/tui/plugin-write-task.js +282 -0
- package/dist/src/tui/plugin-write-task.js.map +1 -0
- package/dist/src/tui/router.d.ts +39 -2
- package/dist/src/tui/router.js +13 -5
- package/dist/src/tui/router.js.map +1 -1
- package/dist/src/tui/screen-registry.d.ts +125 -6
- package/dist/src/tui/screen-registry.js +35 -4
- package/dist/src/tui/screen-registry.js.map +1 -1
- package/dist/src/utils/catalog-generation-service.d.ts +4 -0
- package/dist/src/utils/catalog-generation-service.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { statusCommandV7, statusTargetIntent, } from '../commands/status-v6.js';
|
|
3
|
+
import { statusCommandV7, statusTargetActivation, statusTargetIntent, statusTargetOwnershipState, } 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 !==
|
|
237
|
+
|| (target.scope !== 'project' && target.scope !== 'user')
|
|
233
238
|
|| (intent.selection.kind === 'targets' && target.platform === 'both')) {
|
|
234
|
-
throw new TuiOperationBlockedError('batchTargetInvalid', '
|
|
239
|
+
throw new TuiOperationBlockedError('batchTargetInvalid', '批量精确目标必须使用有效 scope 和单个平台。');
|
|
235
240
|
}
|
|
236
241
|
const pluginId = target.pluginId;
|
|
237
242
|
const parsed = parsePluginId(pluginId);
|
|
@@ -364,40 +369,6 @@ async function loadDependencyOverviews(options) {
|
|
|
364
369
|
const unsupportedPluginIds = new Set(options.status.pluginSummaries
|
|
365
370
|
.filter((summary) => (summary.missingCapabilities?.length ?? 0) > 0)
|
|
366
371
|
.map((summary) => summary.pluginId));
|
|
367
|
-
const placementReferences = new Map();
|
|
368
|
-
for (const consumer of options.status.payload.targets.filter((target) => target.state !== 'absent')) {
|
|
369
|
-
const consumerNode = nodes.get(consumer.pluginId);
|
|
370
|
-
if (!consumerNode)
|
|
371
|
-
continue;
|
|
372
|
-
for (const dependencyPluginId of consumerNode.dependencies) {
|
|
373
|
-
const dependencyTargets = (targetsByPlugin.get(dependencyPluginId) ?? [])
|
|
374
|
-
.filter((target) => target.platform === consumer.platform);
|
|
375
|
-
const projectTarget = dependencyTargets.find((target) => target.scope === 'project');
|
|
376
|
-
const userTarget = dependencyTargets.find((target) => target.scope === 'user');
|
|
377
|
-
const dependencyScope = consumer.scope === 'user'
|
|
378
|
-
? 'user'
|
|
379
|
-
: userTarget && (!projectTarget
|
|
380
|
-
|| (userTarget.state !== 'absent' && projectTarget.state === 'absent'))
|
|
381
|
-
? 'user'
|
|
382
|
-
: 'project';
|
|
383
|
-
const reference = {
|
|
384
|
-
consumer: {
|
|
385
|
-
pluginId: consumer.pluginId,
|
|
386
|
-
platform: consumer.platform,
|
|
387
|
-
scope: consumer.scope,
|
|
388
|
-
project: consumer.scope === 'project' ? 'current' : null,
|
|
389
|
-
},
|
|
390
|
-
dependency: {
|
|
391
|
-
pluginId: dependencyPluginId,
|
|
392
|
-
platform: consumer.platform,
|
|
393
|
-
scope: dependencyScope,
|
|
394
|
-
project: dependencyScope === 'project' ? 'current' : null,
|
|
395
|
-
},
|
|
396
|
-
};
|
|
397
|
-
const key = `${consumer.pluginId}\0${dependencyPluginId}`;
|
|
398
|
-
placementReferences.set(key, [...(placementReferences.get(key) ?? []), reference]);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
372
|
const placementsFor = (pluginId) => {
|
|
402
373
|
const targets = targetsByPlugin.get(pluginId) ?? [];
|
|
403
374
|
if (targets.length > 0) {
|
|
@@ -469,7 +440,7 @@ async function loadDependencyOverviews(options) {
|
|
|
469
440
|
})),
|
|
470
441
|
placements,
|
|
471
442
|
aggregateStatus: aggregateFor(dependency.pluginId, placements),
|
|
472
|
-
placementReferences:
|
|
443
|
+
placementReferences: [],
|
|
473
444
|
};
|
|
474
445
|
});
|
|
475
446
|
const dependentIds = [...new Set(reverse.get(pluginId) ?? [])].sort();
|
|
@@ -483,7 +454,7 @@ async function loadDependencyOverviews(options) {
|
|
|
483
454
|
requiredBy: [],
|
|
484
455
|
placements,
|
|
485
456
|
aggregateStatus: aggregateFor(dependentId, placements),
|
|
486
|
-
placementReferences:
|
|
457
|
+
placementReferences: [],
|
|
487
458
|
};
|
|
488
459
|
});
|
|
489
460
|
const targetBlockers = (targetsByPlugin.get(pluginId) ?? []).flatMap((target) => target.reasons.filter((error) => error.code.startsWith('dependency') || error.code === 'sourceChanged')
|
|
@@ -563,6 +534,7 @@ function createReviewStageReporter(options) {
|
|
|
563
534
|
};
|
|
564
535
|
}
|
|
565
536
|
export function createTuiControllerDependencies(context, progressFeed, options = {}) {
|
|
537
|
+
const catalogManagement = createCatalogManagementService(context);
|
|
566
538
|
const readInventory = options.inventoryCatalogSourceReferences
|
|
567
539
|
?? inventoryCatalogSourceReferences;
|
|
568
540
|
const validatedStoreEntries = new Map();
|
|
@@ -597,6 +569,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
597
569
|
};
|
|
598
570
|
};
|
|
599
571
|
const catalogOperationByEvidence = new WeakMap();
|
|
572
|
+
const taskCatalogOperationsByEvidence = new WeakMap();
|
|
600
573
|
const codexManagerByEvidence = new WeakMap();
|
|
601
574
|
let sessionCatalogOperationContext;
|
|
602
575
|
let sessionStatusResult;
|
|
@@ -676,6 +649,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
676
649
|
const result = await statusCommandV7({
|
|
677
650
|
cwd: context.cwd,
|
|
678
651
|
env: context.env,
|
|
652
|
+
nativeCliRunner: context.nativeCliRunner,
|
|
679
653
|
cliVersion: packageInfo.version,
|
|
680
654
|
verbose: context.diagnostics.verbose,
|
|
681
655
|
...(catalogOperationContext ? { catalogOperationContext } : {}),
|
|
@@ -768,6 +742,37 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
768
742
|
};
|
|
769
743
|
const captureEvidence = async (intent, signal, preparation, operationInventory = createOperationInventoryReader(), freshCatalog = false, reusableCandidate, codexObservationSession, frozenBatchEvidence) => {
|
|
770
744
|
reviewedScriptTimeoutMs(intent);
|
|
745
|
+
if ((intent.kind === 'pluginWrite'
|
|
746
|
+
&& (intent.platform === 'codex' || intent.platform === 'both')
|
|
747
|
+
|| intent.kind === 'pluginWriteTask'
|
|
748
|
+
&& intent.targets.some((target) => target.platform === 'codex'))
|
|
749
|
+
&& !codexObservationSession) {
|
|
750
|
+
const observationSession = await createCodexNativeObservationSession({
|
|
751
|
+
exactCwd: context.cwd,
|
|
752
|
+
env: context.env,
|
|
753
|
+
runner: context.nativeCliRunner,
|
|
754
|
+
});
|
|
755
|
+
try {
|
|
756
|
+
return await captureEvidence(intent, signal, preparation, operationInventory, freshCatalog, reusableCandidate, observationSession, frozenBatchEvidence);
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
await observationSession.dispose();
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
if (intent.kind === 'catalogManagement') {
|
|
763
|
+
signal?.throwIfAborted();
|
|
764
|
+
await preparation?.reporter.emit('validate');
|
|
765
|
+
const managementEvidence = await catalogManagement.captureEvidence(intent);
|
|
766
|
+
signal?.throwIfAborted();
|
|
767
|
+
await preparation?.reporter.emit('evidence');
|
|
768
|
+
return {
|
|
769
|
+
catalog: {
|
|
770
|
+
configDigest: managementEvidence.workspaceBinding ?? 'catalog-store',
|
|
771
|
+
compositeDigest: managementEvidence.evidenceDigest,
|
|
772
|
+
},
|
|
773
|
+
catalogManagement: managementEvidence,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
771
776
|
if (intent.kind === 'configReset') {
|
|
772
777
|
const parsed = parsePluginId(intent.pluginId);
|
|
773
778
|
if (!parsed.sourceId || !parsed.pluginName) {
|
|
@@ -1157,10 +1162,394 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
1157
1162
|
}
|
|
1158
1163
|
return evidence;
|
|
1159
1164
|
}
|
|
1160
|
-
if (!pluginIntent(intent) && intent.kind !== 'pluginWriteBatch'
|
|
1165
|
+
if (!pluginIntent(intent) && intent.kind !== 'pluginWriteBatch'
|
|
1166
|
+
&& intent.kind !== 'pluginWriteTask') {
|
|
1161
1167
|
throw new TuiOperationBlockedError('operationNotImplemented', '这个写操作尚未接入新 Controller。');
|
|
1162
1168
|
}
|
|
1163
1169
|
signal?.throwIfAborted();
|
|
1170
|
+
if (intent.kind === 'pluginWriteTask') {
|
|
1171
|
+
await preparation?.reporter.emit('status');
|
|
1172
|
+
const packageInfo = await readCliPackageInfo();
|
|
1173
|
+
const configSnapshot = await readCatalogSourcesConfigSnapshot(context.env);
|
|
1174
|
+
const requestedBySource = new Map();
|
|
1175
|
+
for (const target of intent.targets) {
|
|
1176
|
+
const parsed = parsePluginId(target.pluginId);
|
|
1177
|
+
if (!parsed.sourceId || formatPluginId({
|
|
1178
|
+
sourceId: parsed.sourceId,
|
|
1179
|
+
pluginName: parsed.pluginName,
|
|
1180
|
+
}) !== target.pluginId) {
|
|
1181
|
+
throw new TuiOperationBlockedError('invalidPluginId', '任务目标必须使用 canonical pluginId。');
|
|
1182
|
+
}
|
|
1183
|
+
requestedBySource.set(parsed.sourceId, [
|
|
1184
|
+
...(requestedBySource.get(parsed.sourceId) ?? []),
|
|
1185
|
+
target,
|
|
1186
|
+
]);
|
|
1187
|
+
}
|
|
1188
|
+
const sources = [];
|
|
1189
|
+
const executableTargets = [];
|
|
1190
|
+
const causes = [];
|
|
1191
|
+
const blockedTargets = [];
|
|
1192
|
+
const observedTargets = [];
|
|
1193
|
+
const observedRevisions = [];
|
|
1194
|
+
const operationContexts = new Map();
|
|
1195
|
+
await preparation?.reporter.emit('catalog');
|
|
1196
|
+
for (const [sourceId, requestedTargets] of [...requestedBySource.entries()]
|
|
1197
|
+
.sort(([left], [right]) => left.localeCompare(right, 'en'))) {
|
|
1198
|
+
signal?.throwIfAborted();
|
|
1199
|
+
let catalogOperation;
|
|
1200
|
+
try {
|
|
1201
|
+
catalogOperation = !freshCatalog && sessionCatalogOperationContext
|
|
1202
|
+
? selectCatalogOperationContext(sessionCatalogOperationContext, [sourceId])
|
|
1203
|
+
: await resolveCatalogOperationContext({
|
|
1204
|
+
sourceIds: [sourceId],
|
|
1205
|
+
env: context.env,
|
|
1206
|
+
...(!freshCatalog ? { validatedStoreEntries, validatingStoreEntries } : {}),
|
|
1207
|
+
...(signal ? { signal } : {}),
|
|
1208
|
+
});
|
|
1209
|
+
}
|
|
1210
|
+
catch {
|
|
1211
|
+
blockedTargets.push(...requestedTargets.map((target) => ({
|
|
1212
|
+
target,
|
|
1213
|
+
reason: 'targetUnavailable',
|
|
1214
|
+
issueIds: [],
|
|
1215
|
+
})));
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
operationContexts.set(sourceId, catalogOperation);
|
|
1219
|
+
const status = await statusCommandV7({
|
|
1220
|
+
plugins: [...new Set(requestedTargets.map((target) => target.pluginId))],
|
|
1221
|
+
cwd: context.cwd,
|
|
1222
|
+
env: context.env,
|
|
1223
|
+
nativeCliRunner: context.nativeCliRunner,
|
|
1224
|
+
...(codexObservationSession ? { codexObservationSession } : {}),
|
|
1225
|
+
cliVersion: packageInfo.version,
|
|
1226
|
+
catalogOperationContext: catalogOperation,
|
|
1227
|
+
inventoryCatalogSourceReferences: operationInventory,
|
|
1228
|
+
});
|
|
1229
|
+
observedTargets.push(...status.payload.targets);
|
|
1230
|
+
observedRevisions.push(...status.targetRevisions);
|
|
1231
|
+
const model = buildTuiViewModel({
|
|
1232
|
+
status: status.payload,
|
|
1233
|
+
pluginSummaries: status.pluginSummaries,
|
|
1234
|
+
targetRevisions: status.targetRevisions,
|
|
1235
|
+
});
|
|
1236
|
+
const runtimeCandidates = intent.goal === 'restore'
|
|
1237
|
+
? buildPluginWriteTaskCandidates(model, 'restore')
|
|
1238
|
+
: requestedTargets.map((target) => {
|
|
1239
|
+
const expected = target.expectedUpdate;
|
|
1240
|
+
const revision = status.targetRevisions.find((entry) => entry.pluginId === target.pluginId && entry.platform === target.platform
|
|
1241
|
+
&& entry.scope === target.scope);
|
|
1242
|
+
const live = status.payload.targets.find((entry) => {
|
|
1243
|
+
if (entry.pluginId !== target.pluginId || entry.platform !== target.platform)
|
|
1244
|
+
return false;
|
|
1245
|
+
const desired = statusTargetIntent(entry);
|
|
1246
|
+
return entry.scope === target.scope || desired.scopes.includes(target.scope);
|
|
1247
|
+
});
|
|
1248
|
+
const generation = catalogOperation.generationSet.sources.find((entry) => entry.sourceId === sourceId);
|
|
1249
|
+
const eligible = Boolean(expected && live && revision
|
|
1250
|
+
&& live.sourceIdentity === expected.sourceIdentity
|
|
1251
|
+
&& generation?.storeKey === expected.generationKey
|
|
1252
|
+
&& revision.revision.installedVersion === expected.installedVersion
|
|
1253
|
+
&& revision.revision.availableVersion === expected.activeVersion
|
|
1254
|
+
&& revision.revision.state === 'upgradeAvailable');
|
|
1255
|
+
return {
|
|
1256
|
+
target,
|
|
1257
|
+
displayName: model.plugins.find((plugin) => plugin.pluginId === target.pluginId)
|
|
1258
|
+
?.displayName ?? parsePluginId(target.pluginId).pluginName,
|
|
1259
|
+
sourceId,
|
|
1260
|
+
eligible,
|
|
1261
|
+
...(!eligible ? { blockReason: 'updateEvidenceIncomplete' } : {}),
|
|
1262
|
+
recommendedMethod: live?.state === 'ready' ? 'sync' : 'reinstall',
|
|
1263
|
+
issueIds: [],
|
|
1264
|
+
};
|
|
1265
|
+
});
|
|
1266
|
+
const draft = buildPluginWriteTaskDraft({ ...intent, targets: requestedTargets }, model, runtimeCandidates);
|
|
1267
|
+
blockedTargets.push(...draft.blockedTargets);
|
|
1268
|
+
if (draft.executableTargets.length === 0)
|
|
1269
|
+
continue;
|
|
1270
|
+
const methodsByTarget = new Map();
|
|
1271
|
+
for (const cause of draft.causes) {
|
|
1272
|
+
const key = pluginWriteTaskTargetKey(cause.repairTarget);
|
|
1273
|
+
const methods = methodsByTarget.get(key) ?? new Set();
|
|
1274
|
+
methods.add(cause.method);
|
|
1275
|
+
methodsByTarget.set(key, methods);
|
|
1276
|
+
}
|
|
1277
|
+
const conflicted = new Set([...methodsByTarget].filter(([, methods]) => methods.size > 1)
|
|
1278
|
+
.map(([key]) => key));
|
|
1279
|
+
if (conflicted.size > 0) {
|
|
1280
|
+
const blockedKeys = new Set(draft.causes.filter((cause) => conflicted.has(pluginWriteTaskTargetKey(cause.repairTarget)))
|
|
1281
|
+
.flatMap((cause) => cause.consumers.map(pluginWriteTaskTargetKey)));
|
|
1282
|
+
blockedTargets.push(...draft.executableTargets.filter((target) => blockedKeys.has(pluginWriteTaskTargetKey(target))).map((target) => ({
|
|
1283
|
+
target,
|
|
1284
|
+
reason: 'taskActionConflict',
|
|
1285
|
+
issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
|
|
1286
|
+
.map((cause) => cause.issueId),
|
|
1287
|
+
})));
|
|
1288
|
+
draft.executableTargets = draft.executableTargets.filter((target) => !blockedKeys.has(pluginWriteTaskTargetKey(target)));
|
|
1289
|
+
draft.causes = draft.causes.filter((cause) => cause.consumers.some((consumer) => !blockedKeys.has(pluginWriteTaskTargetKey(consumer))));
|
|
1290
|
+
}
|
|
1291
|
+
if (draft.executableTargets.length === 0)
|
|
1292
|
+
continue;
|
|
1293
|
+
const taskTargets = () => draft.executableTargets.map((target) => {
|
|
1294
|
+
const direct = draft.causes.filter((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === pluginWriteTaskTargetKey(target));
|
|
1295
|
+
return {
|
|
1296
|
+
pluginId: target.pluginId,
|
|
1297
|
+
platform: target.platform,
|
|
1298
|
+
scope: target.scope,
|
|
1299
|
+
method: direct.some((cause) => cause.method === 'reinstall')
|
|
1300
|
+
? 'reinstall' : 'sync',
|
|
1301
|
+
};
|
|
1302
|
+
});
|
|
1303
|
+
const repairTargets = () => [...new Map(draft.causes.map((cause) => [
|
|
1304
|
+
pluginWriteTaskTargetKey(cause.repairTarget),
|
|
1305
|
+
{ ...cause.repairTarget, method: cause.method },
|
|
1306
|
+
])).values()];
|
|
1307
|
+
let preview;
|
|
1308
|
+
while (draft.executableTargets.length > 0) {
|
|
1309
|
+
preview = await taskWriteCommand(taskTargets(), {
|
|
1310
|
+
cwd: context.cwd,
|
|
1311
|
+
repairTargets: repairTargets(),
|
|
1312
|
+
...(reviewedScriptTimeoutMs(intent) === undefined
|
|
1313
|
+
? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
|
|
1314
|
+
}, {
|
|
1315
|
+
...context,
|
|
1316
|
+
catalogOperationContext: catalogOperation,
|
|
1317
|
+
expectedGenerationSetDigest: catalogOperation.generationSetDigest,
|
|
1318
|
+
expectedCatalogConfigDigest: configSnapshot.digest,
|
|
1319
|
+
inventoryCatalogSourceReferences: operationInventory,
|
|
1320
|
+
previewOperationPlan: true,
|
|
1321
|
+
...(codexObservationSession ? { codexObservationSession } : {}),
|
|
1322
|
+
...(preparation ? {
|
|
1323
|
+
onPreviewStage: (stage) => stage === 'catalog' ? undefined : preparation.reporter.emit(stage),
|
|
1324
|
+
} : {}),
|
|
1325
|
+
});
|
|
1326
|
+
if (!preview.operationPlan) {
|
|
1327
|
+
blockedTargets.push(...draft.executableTargets.map((target) => ({
|
|
1328
|
+
target,
|
|
1329
|
+
reason: 'targetUnavailable',
|
|
1330
|
+
issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
|
|
1331
|
+
.map((cause) => cause.issueId),
|
|
1332
|
+
})));
|
|
1333
|
+
draft.executableTargets = [];
|
|
1334
|
+
draft.causes = [];
|
|
1335
|
+
break;
|
|
1336
|
+
}
|
|
1337
|
+
if (intent.goal === 'restore') {
|
|
1338
|
+
const grouped = new Map();
|
|
1339
|
+
const blockedByVersion = new Map();
|
|
1340
|
+
for (const cause of draft.causes) {
|
|
1341
|
+
const placements = (preview.dependencyPlan?.targets ?? []).filter((target) => target.pluginId === cause.repairTarget.pluginId
|
|
1342
|
+
&& target.platform === cause.repairTarget.platform);
|
|
1343
|
+
const placement = placements.find((target) => target.requestedScope === cause.repairTarget.scope)
|
|
1344
|
+
?? (placements.length === 1 ? placements[0] : undefined);
|
|
1345
|
+
if (!placement) {
|
|
1346
|
+
throw new TuiOperationBlockedError('taskRepairPlacementMissing', '任务修复目标无法绑定到依赖位置。');
|
|
1347
|
+
}
|
|
1348
|
+
const repairTarget = {
|
|
1349
|
+
pluginId: cause.repairTarget.pluginId,
|
|
1350
|
+
platform: cause.repairTarget.platform,
|
|
1351
|
+
scope: placement.requestedScope ?? placement.resolvedScope,
|
|
1352
|
+
};
|
|
1353
|
+
const version = evaluatePluginWriteTaskRestoreVersion(repairTarget, cause.restoreVersionEvidence?.state === 'absent', model);
|
|
1354
|
+
if (!version.evidence || version.blockReason) {
|
|
1355
|
+
for (const consumer of cause.consumers) {
|
|
1356
|
+
const key = pluginWriteTaskTargetKey(consumer);
|
|
1357
|
+
const current = blockedByVersion.get(key) ?? {
|
|
1358
|
+
reason: version.blockReason ?? 'restoreVersionUnverified',
|
|
1359
|
+
issueIds: new Set(),
|
|
1360
|
+
};
|
|
1361
|
+
if (version.blockReason === 'restoreVersionChanged') {
|
|
1362
|
+
current.reason = version.blockReason;
|
|
1363
|
+
}
|
|
1364
|
+
current.issueIds.add(cause.issueId);
|
|
1365
|
+
blockedByVersion.set(key, current);
|
|
1366
|
+
}
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
const causeKey = ['restore', cause.sourceIdentity,
|
|
1370
|
+
pluginWriteTaskTargetKey(repairTarget)].join('\0');
|
|
1371
|
+
const current = grouped.get(causeKey);
|
|
1372
|
+
grouped.set(causeKey, {
|
|
1373
|
+
cause: {
|
|
1374
|
+
...cause,
|
|
1375
|
+
causeKey,
|
|
1376
|
+
issueId: [cause.issueId, ...(current?.issueIds ?? [])]
|
|
1377
|
+
.sort((left, right) => left.localeCompare(right, 'en'))[0],
|
|
1378
|
+
repairTarget,
|
|
1379
|
+
consumers: [...new Map([
|
|
1380
|
+
...(current?.cause.consumers ?? []),
|
|
1381
|
+
...cause.consumers,
|
|
1382
|
+
].map((consumer) => [pluginWriteTaskTargetKey(consumer), consumer])).values()]
|
|
1383
|
+
.sort((left, right) => pluginWriteTaskTargetKey(left)
|
|
1384
|
+
.localeCompare(pluginWriteTaskTargetKey(right), 'en')),
|
|
1385
|
+
restoreVersionEvidence: version.evidence,
|
|
1386
|
+
},
|
|
1387
|
+
issueIds: new Set([...(current?.issueIds ?? []), cause.issueId]),
|
|
1388
|
+
methods: new Set([...(current?.methods ?? []), cause.method]),
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
for (const group of grouped.values()) {
|
|
1392
|
+
if (group.methods.size < 2)
|
|
1393
|
+
continue;
|
|
1394
|
+
for (const consumer of group.cause.consumers) {
|
|
1395
|
+
const key = pluginWriteTaskTargetKey(consumer);
|
|
1396
|
+
const current = blockedByVersion.get(key) ?? {
|
|
1397
|
+
reason: 'taskActionConflict',
|
|
1398
|
+
issueIds: new Set(),
|
|
1399
|
+
};
|
|
1400
|
+
group.issueIds.forEach((issueId) => current.issueIds.add(issueId));
|
|
1401
|
+
blockedByVersion.set(key, current);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
const newlyBlockedKeys = new Set(blockedByVersion.keys());
|
|
1405
|
+
blockedTargets.push(...draft.executableTargets.flatMap((target) => {
|
|
1406
|
+
const blocked = blockedByVersion.get(pluginWriteTaskTargetKey(target));
|
|
1407
|
+
return blocked ? [{
|
|
1408
|
+
target,
|
|
1409
|
+
reason: blocked.reason,
|
|
1410
|
+
issueIds: [...blocked.issueIds].sort((left, right) => left.localeCompare(right, 'en')),
|
|
1411
|
+
}] : [];
|
|
1412
|
+
}));
|
|
1413
|
+
const normalizedCauses = [...grouped.values()]
|
|
1414
|
+
.filter((group) => group.methods.size === 1)
|
|
1415
|
+
.map((group) => ({
|
|
1416
|
+
...group.cause,
|
|
1417
|
+
consumers: group.cause.consumers.filter((consumer) => !newlyBlockedKeys.has(pluginWriteTaskTargetKey(consumer))),
|
|
1418
|
+
}))
|
|
1419
|
+
.filter((cause) => cause.consumers.length > 0)
|
|
1420
|
+
.sort((left, right) => left.causeKey.localeCompare(right.causeKey, 'en'));
|
|
1421
|
+
const normalizedTargets = draft.executableTargets.filter((target) => !newlyBlockedKeys.has(pluginWriteTaskTargetKey(target)));
|
|
1422
|
+
const changed = JSON.stringify(normalizedCauses) !== JSON.stringify(draft.causes)
|
|
1423
|
+
|| JSON.stringify(normalizedTargets) !== JSON.stringify(draft.executableTargets);
|
|
1424
|
+
draft.causes = normalizedCauses;
|
|
1425
|
+
draft.executableTargets = normalizedTargets;
|
|
1426
|
+
if (changed)
|
|
1427
|
+
continue;
|
|
1428
|
+
}
|
|
1429
|
+
const blockedPlanTargets = (preview.dependencyPlan?.targets ?? []).filter((target) => target.action === 'block' || target.errorCode !== undefined);
|
|
1430
|
+
if (blockedPlanTargets.length === 0)
|
|
1431
|
+
break;
|
|
1432
|
+
const requestedKeys = new Set(draft.executableTargets.map(pluginWriteTaskTargetKey));
|
|
1433
|
+
const errorCodesByRoot = new Map();
|
|
1434
|
+
for (const blockedPlanTarget of blockedPlanTargets) {
|
|
1435
|
+
const pending = [blockedPlanTarget];
|
|
1436
|
+
const visited = new Set();
|
|
1437
|
+
const mappedRoots = new Set();
|
|
1438
|
+
const repairKey = pluginWriteTaskTargetKey({
|
|
1439
|
+
pluginId: blockedPlanTarget.pluginId,
|
|
1440
|
+
platform: blockedPlanTarget.platform,
|
|
1441
|
+
scope: blockedPlanTarget.requestedScope ?? blockedPlanTarget.resolvedScope,
|
|
1442
|
+
});
|
|
1443
|
+
for (const cause of draft.causes) {
|
|
1444
|
+
if (pluginWriteTaskTargetKey(cause.repairTarget) !== repairKey)
|
|
1445
|
+
continue;
|
|
1446
|
+
cause.consumers.forEach((consumer) => mappedRoots.add(pluginWriteTaskTargetKey(consumer)));
|
|
1447
|
+
}
|
|
1448
|
+
while (pending.length > 0) {
|
|
1449
|
+
const current = pending.shift();
|
|
1450
|
+
const currentKey = pluginWriteTaskTargetKey({
|
|
1451
|
+
pluginId: current.pluginId,
|
|
1452
|
+
platform: current.platform,
|
|
1453
|
+
scope: current.requestedScope ?? current.resolvedScope,
|
|
1454
|
+
});
|
|
1455
|
+
if (visited.has(currentKey))
|
|
1456
|
+
continue;
|
|
1457
|
+
visited.add(currentKey);
|
|
1458
|
+
if (current.relation === 'requested' && requestedKeys.has(currentKey)) {
|
|
1459
|
+
mappedRoots.add(currentKey);
|
|
1460
|
+
}
|
|
1461
|
+
for (const consumer of current.requiredByTargets ?? []) {
|
|
1462
|
+
const consumerKey = pluginWriteTaskTargetKey(consumer);
|
|
1463
|
+
if (requestedKeys.has(consumerKey)) {
|
|
1464
|
+
mappedRoots.add(consumerKey);
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
const placement = preview.dependencyPlan?.targets.find((target) => target.pluginId === consumer.pluginId && target.platform === consumer.platform
|
|
1468
|
+
&& (target.requestedScope ?? target.resolvedScope) === consumer.scope);
|
|
1469
|
+
if (placement)
|
|
1470
|
+
pending.push(placement);
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
if (mappedRoots.size === 0) {
|
|
1474
|
+
throw new TuiOperationBlockedError('taskPlanConsumerMissing', '任务计划中的阻止项无法映射到请求目标。');
|
|
1475
|
+
}
|
|
1476
|
+
for (const key of mappedRoots) {
|
|
1477
|
+
const codes = errorCodesByRoot.get(key) ?? new Set();
|
|
1478
|
+
codes.add(blockedPlanTarget.errorCode ?? 'writePlanBlocked');
|
|
1479
|
+
errorCodesByRoot.set(key, codes);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
blockedTargets.push(...draft.executableTargets.flatMap((target) => {
|
|
1483
|
+
const codes = errorCodesByRoot.get(pluginWriteTaskTargetKey(target));
|
|
1484
|
+
return codes ? [{
|
|
1485
|
+
target,
|
|
1486
|
+
reason: 'targetUnavailable',
|
|
1487
|
+
issueIds: draft.causes.filter((cause) => cause.consumers.some((consumer) => pluginWriteTaskTargetKey(consumer) === pluginWriteTaskTargetKey(target)))
|
|
1488
|
+
.map((cause) => cause.issueId),
|
|
1489
|
+
errorCodes: [...codes].sort((left, right) => left.localeCompare(right, 'en')),
|
|
1490
|
+
}] : [];
|
|
1491
|
+
}));
|
|
1492
|
+
draft.executableTargets = draft.executableTargets.filter((target) => !errorCodesByRoot.has(pluginWriteTaskTargetKey(target)));
|
|
1493
|
+
draft.causes = draft.causes.map((cause) => ({
|
|
1494
|
+
...cause,
|
|
1495
|
+
consumers: cause.consumers.filter((consumer) => !errorCodesByRoot.has(pluginWriteTaskTargetKey(consumer))),
|
|
1496
|
+
})).filter((cause) => cause.consumers.length > 0);
|
|
1497
|
+
}
|
|
1498
|
+
if (!preview?.operationPlan || draft.executableTargets.length === 0)
|
|
1499
|
+
continue;
|
|
1500
|
+
const sourceIdentity = draft.causes[0]?.sourceIdentity
|
|
1501
|
+
?? status.payload.catalog.sources.find((source) => source.id === sourceId)?.identity;
|
|
1502
|
+
if (!sourceIdentity) {
|
|
1503
|
+
throw new TuiOperationBlockedError('taskSourceIdentityMissing', '任务来源身份不可验证。');
|
|
1504
|
+
}
|
|
1505
|
+
const sourceEvidence = {
|
|
1506
|
+
sourceId,
|
|
1507
|
+
sourceIdentity,
|
|
1508
|
+
requestedTargets: draft.executableTargets,
|
|
1509
|
+
causes: draft.causes,
|
|
1510
|
+
preview,
|
|
1511
|
+
executable: true,
|
|
1512
|
+
};
|
|
1513
|
+
sources.push(sourceEvidence);
|
|
1514
|
+
executableTargets.push(...draft.executableTargets);
|
|
1515
|
+
causes.push(...draft.causes);
|
|
1516
|
+
}
|
|
1517
|
+
const canonicalBlocked = [...new Map(blockedTargets.map((target) => [
|
|
1518
|
+
pluginWriteTaskTargetKey(target.target), target,
|
|
1519
|
+
])).values()].sort((left, right) => pluginWriteTaskTargetKey(left.target)
|
|
1520
|
+
.localeCompare(pluginWriteTaskTargetKey(right.target), 'en'));
|
|
1521
|
+
if (executableTargets.length === 0) {
|
|
1522
|
+
throw new TuiOperationBlockedError('taskNoExecutableTargets', '任务没有可安全执行的目标。');
|
|
1523
|
+
}
|
|
1524
|
+
if (intent.executionPolicy === 'all' && canonicalBlocked.length > 0) {
|
|
1525
|
+
throw new TuiOperationBlockedError('taskHasBlockedTargets', '部分目标需要先处理。请调整选择。');
|
|
1526
|
+
}
|
|
1527
|
+
await preparation?.reporter.emit('evidence');
|
|
1528
|
+
const evidence = {
|
|
1529
|
+
catalog: {
|
|
1530
|
+
configDigest: configSnapshot.digest,
|
|
1531
|
+
compositeDigest: digest(sources.map((source) => [
|
|
1532
|
+
source.sourceId,
|
|
1533
|
+
source.sourceIdentity,
|
|
1534
|
+
source.preview.operationPlan?.planFingerprint,
|
|
1535
|
+
])),
|
|
1536
|
+
},
|
|
1537
|
+
taskPlan: {
|
|
1538
|
+
goal: intent.goal,
|
|
1539
|
+
requestedTargets: intent.targets,
|
|
1540
|
+
executableTargets: [...executableTargets].sort((left, right) => pluginWriteTaskTargetKey(left).localeCompare(pluginWriteTaskTargetKey(right), 'en')),
|
|
1541
|
+
causes: [...new Map(causes.map((cause) => [cause.causeKey, cause])).values()]
|
|
1542
|
+
.sort((left, right) => left.causeKey.localeCompare(right.causeKey, 'en')),
|
|
1543
|
+
blockedTargets: canonicalBlocked,
|
|
1544
|
+
sources,
|
|
1545
|
+
targetFingerprint: statusTargetFingerprint(observedTargets, observedRevisions),
|
|
1546
|
+
planFingerprint: '',
|
|
1547
|
+
},
|
|
1548
|
+
};
|
|
1549
|
+
evidence.taskPlan.planFingerprint = operationEvidenceFingerprint(evidence);
|
|
1550
|
+
taskCatalogOperationsByEvidence.set(evidence, operationContexts);
|
|
1551
|
+
return evidence;
|
|
1552
|
+
}
|
|
1164
1553
|
if (intent.kind === 'pluginWriteBatch') {
|
|
1165
1554
|
await preparation?.reporter.emit('status');
|
|
1166
1555
|
const selection = await resolveBatchGroups(intent, context.env, context.cwd, frozenBatchEvidence?.requestedPluginIds);
|
|
@@ -1232,6 +1621,33 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
1232
1621
|
await preparation?.reporter.emit('evidence');
|
|
1233
1622
|
signal?.throwIfAborted();
|
|
1234
1623
|
const merged = mergeWriteCommandResults(groups.map((group) => group.preview));
|
|
1624
|
+
const targetVersions = groups.flatMap((group) => {
|
|
1625
|
+
if (group.platform === 'both')
|
|
1626
|
+
return [];
|
|
1627
|
+
const platform = group.platform;
|
|
1628
|
+
return group.requestedPluginIds.map((pluginId) => {
|
|
1629
|
+
const live = sessionStatusResult?.payload.targets.find((target) => target.pluginId === pluginId && target.platform === platform
|
|
1630
|
+
&& (target.scope === group.scope || statusTargetIntent(target).scopes.includes(group.scope)
|
|
1631
|
+
|| platform === 'codex' && statusTargetActivation(target)?.[group.scope] === true));
|
|
1632
|
+
const revision = sessionStatusResult?.targetRevisions?.find((target) => target.pluginId === pluginId && target.platform === platform
|
|
1633
|
+
&& target.scope === group.scope)?.revision;
|
|
1634
|
+
const activation = live ? statusTargetActivation(live) : undefined;
|
|
1635
|
+
const ownership = live && live.scope === group.scope
|
|
1636
|
+
? statusTargetOwnershipState(live) : 'absent';
|
|
1637
|
+
const present = ownership === 'present' || ownership === 'partial'
|
|
1638
|
+
|| platform === 'codex' && activation?.[group.scope] === true;
|
|
1639
|
+
const planned = live ? statusTargetIntent(live).scopes.includes(group.scope) : false;
|
|
1640
|
+
return {
|
|
1641
|
+
pluginId,
|
|
1642
|
+
platform,
|
|
1643
|
+
scope: group.scope,
|
|
1644
|
+
presence: present ? 'installed'
|
|
1645
|
+
: ownership === 'absent' && planned ? 'new' : 'unknown',
|
|
1646
|
+
...(revision?.installedVersion ? { installedVersion: revision.installedVersion } : {}),
|
|
1647
|
+
...(revision?.availableVersion ? { availableVersion: revision.availableVersion } : {}),
|
|
1648
|
+
};
|
|
1649
|
+
});
|
|
1650
|
+
});
|
|
1235
1651
|
const evidence = {
|
|
1236
1652
|
catalog: {
|
|
1237
1653
|
configDigest: configSnapshot.digest,
|
|
@@ -1240,6 +1656,13 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
1240
1656
|
},
|
|
1241
1657
|
requestedPluginIds: selection.requestedPluginIds,
|
|
1242
1658
|
groups,
|
|
1659
|
+
targetVersions: [...new Map(targetVersions.map((target) => [[
|
|
1660
|
+
target.pluginId, target.platform, target.scope,
|
|
1661
|
+
].join('\0'), target])).values()].sort((left, right) => [
|
|
1662
|
+
left.pluginId, left.platform, left.scope,
|
|
1663
|
+
].join('\0').localeCompare([
|
|
1664
|
+
right.pluginId, right.platform, right.scope,
|
|
1665
|
+
].join('\0'), 'en')),
|
|
1243
1666
|
...(merged.dependencyPlan ? { dependencyPlan: merged.dependencyPlan } : {}),
|
|
1244
1667
|
...(merged.codexReconcilePlans ? { codexReconcilePlans: merged.codexReconcilePlans } : {}),
|
|
1245
1668
|
...(merged.operationPlan ? { operationPlan: merged.operationPlan } : {}),
|
|
@@ -1440,6 +1863,8 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
1440
1863
|
: {}),
|
|
1441
1864
|
cwd: context.cwd,
|
|
1442
1865
|
env: context.env,
|
|
1866
|
+
nativeCliRunner: context.nativeCliRunner,
|
|
1867
|
+
...(codexObservationSession ? { codexObservationSession } : {}),
|
|
1443
1868
|
cliVersion: packageInfo.version,
|
|
1444
1869
|
...(statusCatalogOperation ? { catalogOperationContext: statusCatalogOperation } : {}),
|
|
1445
1870
|
inventoryCatalogSourceReferences: operationInventory,
|
|
@@ -1580,6 +2005,21 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
1580
2005
|
};
|
|
1581
2006
|
},
|
|
1582
2007
|
});
|
|
2008
|
+
if (intent.kind === 'pluginWrite' && intent.expectedUpdate) {
|
|
2009
|
+
const revision = status.targetRevisions.find((entry) => entry.pluginId === intent.pluginId && entry.platform === intent.platform
|
|
2010
|
+
&& entry.scope === intent.scope);
|
|
2011
|
+
const generation = catalogOperation?.generationSet.sources.find((entry) => entry.sourceId === selected.source.sourceId);
|
|
2012
|
+
if (intent.action !== 'sync'
|
|
2013
|
+
|| !('pluginVersion' in selected.source)
|
|
2014
|
+
|| selected.source.sourceIdentity !== intent.expectedUpdate.sourceIdentity
|
|
2015
|
+
|| selected.source.pluginVersion !== intent.expectedUpdate.activeVersion
|
|
2016
|
+
|| generation?.storeKey !== intent.expectedUpdate.generationKey
|
|
2017
|
+
|| revision?.revision.installedVersion !== intent.expectedUpdate.installedVersion
|
|
2018
|
+
|| revision?.revision.availableVersion !== intent.expectedUpdate.activeVersion
|
|
2019
|
+
|| revision.revision.state !== 'upgradeAvailable') {
|
|
2020
|
+
throw operationReviewStale('插件更新版本或 Active Catalog 证据已变化。');
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
1583
2023
|
let codexManager;
|
|
1584
2024
|
let resolvedCodexManager;
|
|
1585
2025
|
const hasDeclaredDependencies = (status.pluginSummaries.find((summary) => summary.pluginId === intent.pluginId)?.dependencyTotalCount ?? 0) > 0;
|
|
@@ -1743,6 +2183,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
1743
2183
|
return evidence;
|
|
1744
2184
|
};
|
|
1745
2185
|
const dependencies = {
|
|
2186
|
+
catalogManagement,
|
|
1746
2187
|
beginCatalogSession(sessionOptions) {
|
|
1747
2188
|
sessionCatalogOperationContext = undefined;
|
|
1748
2189
|
if (sessionOptions?.reuseValidatedGenerations !== true) {
|
|
@@ -1817,11 +2258,25 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
1817
2258
|
validatedStoreEntries.clear();
|
|
1818
2259
|
validatingStoreEntries.clear();
|
|
1819
2260
|
}
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
2261
|
+
const taskObservationSession = intent.kind === 'pluginWriteTask'
|
|
2262
|
+
&& intent.targets.some((target) => target.platform === 'codex')
|
|
2263
|
+
? await createCodexNativeObservationSession({
|
|
2264
|
+
exactCwd: context.cwd,
|
|
2265
|
+
env: context.env,
|
|
2266
|
+
runner: context.nativeCliRunner,
|
|
2267
|
+
})
|
|
2268
|
+
: undefined;
|
|
2269
|
+
let evidence;
|
|
2270
|
+
try {
|
|
2271
|
+
evidence = await captureEvidence(intent, signal, {
|
|
2272
|
+
reporter,
|
|
2273
|
+
validateIntent: true,
|
|
2274
|
+
}, createOperationInventoryReader(), intent.kind === 'pluginWrite'
|
|
2275
|
+
&& intent.action === 'refreshAndSync', reusableCandidate, taskObservationSession);
|
|
2276
|
+
}
|
|
2277
|
+
finally {
|
|
2278
|
+
await taskObservationSession?.dispose();
|
|
2279
|
+
}
|
|
1825
2280
|
if (preparationWarnings.length > 0)
|
|
1826
2281
|
evidence.preparationWarnings = preparationWarnings;
|
|
1827
2282
|
return evidence;
|
|
@@ -1835,6 +2290,10 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
1835
2290
|
return evidence.dependencyPlan;
|
|
1836
2291
|
},
|
|
1837
2292
|
async validateIntent(intent, signal) {
|
|
2293
|
+
if (intent.kind === 'catalogManagement') {
|
|
2294
|
+
signal.throwIfAborted();
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
1838
2297
|
if (intent.kind === 'catalogSourcesRecovery') {
|
|
1839
2298
|
signal.throwIfAborted();
|
|
1840
2299
|
return;
|
|
@@ -2082,6 +2541,25 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
2082
2541
|
};
|
|
2083
2542
|
},
|
|
2084
2543
|
async executeIntent(intent, expectedEvidence, signal) {
|
|
2544
|
+
if (intent.kind === 'catalogManagement') {
|
|
2545
|
+
signal?.throwIfAborted();
|
|
2546
|
+
if (!expectedEvidence.catalogManagement) {
|
|
2547
|
+
throw new TuiOperationBlockedError('operationReviewStale', 'Catalog management Review evidence is missing.');
|
|
2548
|
+
}
|
|
2549
|
+
const progressLabels = {
|
|
2550
|
+
validate: '验证 Catalog 状态',
|
|
2551
|
+
build: '构建 Catalog snapshot',
|
|
2552
|
+
source: '更新开发来源',
|
|
2553
|
+
sync: '同步开发来源',
|
|
2554
|
+
prune: '清理 Catalog store',
|
|
2555
|
+
prepare: '准备发布计划',
|
|
2556
|
+
remote: '写入 Catalog 远端',
|
|
2557
|
+
verify: '验证操作结果',
|
|
2558
|
+
};
|
|
2559
|
+
return catalogManagement.execute(intent, expectedEvidence.catalogManagement, (stage) => {
|
|
2560
|
+
progressFeed?.setCurrent({ target: 'Catalog 管理', phase: progressLabels[stage] }, `Catalog 管理:${progressLabels[stage]}`);
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2085
2563
|
if (intent.kind === 'catalogSourcesRecovery') {
|
|
2086
2564
|
signal?.throwIfAborted();
|
|
2087
2565
|
return recoverInvalidCatalogSourcesConfig({ env: context.env });
|
|
@@ -2341,6 +2819,7 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
2341
2819
|
}
|
|
2342
2820
|
const codexObservationSession = intent.kind === 'pluginWrite'
|
|
2343
2821
|
|| intent.kind === 'pluginWriteBatch'
|
|
2822
|
+
|| intent.kind === 'pluginWriteTask'
|
|
2344
2823
|
? await createCodexNativeObservationSession({
|
|
2345
2824
|
exactCwd: context.cwd,
|
|
2346
2825
|
env: context.env,
|
|
@@ -2353,9 +2832,20 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
2353
2832
|
action: async (awHomeMutationToken) => {
|
|
2354
2833
|
await tuiControllerRuntimeTestHooks?.beforeMutationEvidenceCapture?.();
|
|
2355
2834
|
const operationInventory = createOperationInventoryReader();
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2835
|
+
let currentEvidence;
|
|
2836
|
+
try {
|
|
2837
|
+
currentEvidence = await captureEvidence(intent, undefined, undefined, operationInventory, true, undefined, codexObservationSession, intent.kind === 'pluginWriteBatch'
|
|
2838
|
+
? expectedEvidence
|
|
2839
|
+
: undefined);
|
|
2840
|
+
}
|
|
2841
|
+
catch (error) {
|
|
2842
|
+
const code = error instanceof TuiOperationBlockedError ? error.code : undefined;
|
|
2843
|
+
if (intent.kind === 'pluginWriteTask'
|
|
2844
|
+
&& (code === 'taskHasBlockedTargets' || code === 'taskNoExecutableTargets')) {
|
|
2845
|
+
throw operationReviewStale('确认后任务可执行目标发生变化。');
|
|
2846
|
+
}
|
|
2847
|
+
throw error;
|
|
2848
|
+
}
|
|
2359
2849
|
if (intent.kind === 'pluginWriteBatch') {
|
|
2360
2850
|
const expectedBatch = expectedEvidence;
|
|
2361
2851
|
const currentBatch = currentEvidence;
|
|
@@ -2498,6 +2988,134 @@ export function createTuiControllerDependencies(context, progressFeed, options =
|
|
|
2498
2988
|
groupResults,
|
|
2499
2989
|
};
|
|
2500
2990
|
}
|
|
2991
|
+
if (intent.kind === 'pluginWriteTask') {
|
|
2992
|
+
const expectedTask = expectedEvidence;
|
|
2993
|
+
const currentTask = currentEvidence;
|
|
2994
|
+
if (!operationEvidenceMatches(expectedTask, currentTask)) {
|
|
2995
|
+
throw operationReviewStale('确认后任务目标、根因、版本或写入计划发生变化。');
|
|
2996
|
+
}
|
|
2997
|
+
await tuiControllerRuntimeTestHooks?.afterMutationEvidenceCapture?.();
|
|
2998
|
+
const operationContexts = taskCatalogOperationsByEvidence.get(currentEvidence)
|
|
2999
|
+
?? new Map();
|
|
3000
|
+
const sourceResults = [];
|
|
3001
|
+
const completedTargetKeys = new Set();
|
|
3002
|
+
const preApplyExecutionCache = new Map();
|
|
3003
|
+
const commandTargets = (source) => source.requestedTargets.map((target) => {
|
|
3004
|
+
const direct = source.causes.filter((cause) => pluginWriteTaskTargetKey(cause.repairTarget) === pluginWriteTaskTargetKey(target));
|
|
3005
|
+
return {
|
|
3006
|
+
pluginId: target.pluginId,
|
|
3007
|
+
platform: target.platform,
|
|
3008
|
+
scope: target.scope,
|
|
3009
|
+
method: direct.some((cause) => cause.method === 'reinstall')
|
|
3010
|
+
? 'reinstall' : 'sync',
|
|
3011
|
+
};
|
|
3012
|
+
});
|
|
3013
|
+
const repairTargets = (source) => [...new Map(source.causes.map((cause) => [
|
|
3014
|
+
pluginWriteTaskTargetKey(cause.repairTarget),
|
|
3015
|
+
{ ...cause.repairTarget, method: cause.method },
|
|
3016
|
+
])).values()];
|
|
3017
|
+
const stoppedResult = (source, error) => mergeWriteCommandResults(source.requestedTargets.map((target) => failedWriteCommandResult('sync', [target.pluginId], {
|
|
3018
|
+
cwd: context.cwd,
|
|
3019
|
+
platform: target.platform,
|
|
3020
|
+
scope: target.scope,
|
|
3021
|
+
}, error)));
|
|
3022
|
+
const appendStoppedSources = (startIndex, message) => {
|
|
3023
|
+
for (const [offset, source] of expectedTask.taskPlan.sources.slice(startIndex).entries()) {
|
|
3024
|
+
sourceResults.push({
|
|
3025
|
+
sourceId: source.sourceId,
|
|
3026
|
+
sourceIdentity: source.sourceIdentity,
|
|
3027
|
+
requestedTargets: source.requestedTargets,
|
|
3028
|
+
result: stoppedResult(source, offset === 0
|
|
3029
|
+
? new TuiOperationBlockedError('operationReviewStale', message)
|
|
3030
|
+
: new TuiOperationBlockedError('taskNotStarted', '前一个来源计划已变化,因此这个来源没有开始。')),
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
3034
|
+
for (const [sourceIndex, reviewed] of expectedTask.taskPlan.sources.entries()) {
|
|
3035
|
+
const current = currentTask.taskPlan.sources.find((source) => source.sourceId === reviewed.sourceId);
|
|
3036
|
+
const catalogOperation = operationContexts.get(reviewed.sourceId);
|
|
3037
|
+
if (!current || !catalogOperation || current.sourceIdentity !== reviewed.sourceIdentity) {
|
|
3038
|
+
if (sourceResults.length === 0) {
|
|
3039
|
+
throw operationReviewStale('确认后任务来源身份发生变化。');
|
|
3040
|
+
}
|
|
3041
|
+
appendStoppedSources(sourceIndex, '确认后任务来源身份发生变化。');
|
|
3042
|
+
break;
|
|
3043
|
+
}
|
|
3044
|
+
const sourceInventory = sourceResults.length === 0
|
|
3045
|
+
? operationInventory : createOperationInventoryReader();
|
|
3046
|
+
let executionPreview = current.preview;
|
|
3047
|
+
if (completedTargetKeys.size > 0) {
|
|
3048
|
+
executionPreview = await taskWriteCommand(commandTargets(current), {
|
|
3049
|
+
cwd: context.cwd,
|
|
3050
|
+
repairTargets: repairTargets(current),
|
|
3051
|
+
...(reviewedScriptTimeoutMs(intent) === undefined
|
|
3052
|
+
? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
|
|
3053
|
+
}, {
|
|
3054
|
+
...context,
|
|
3055
|
+
awHomeMutationToken,
|
|
3056
|
+
catalogOperationContext: catalogOperation,
|
|
3057
|
+
expectedGenerationSetDigest: catalogOperation.generationSetDigest,
|
|
3058
|
+
expectedCatalogConfigDigest: expectedTask.catalog.configDigest,
|
|
3059
|
+
inventoryCatalogSourceReferences: sourceInventory,
|
|
3060
|
+
previewOperationPlan: true,
|
|
3061
|
+
preApplyExecutionCache,
|
|
3062
|
+
...(codexObservationSession ? { codexObservationSession } : {}),
|
|
3063
|
+
});
|
|
3064
|
+
if (!reviewed.preview.operationPlan || !executionPreview.operationPlan
|
|
3065
|
+
|| createBatchIntentCompatiblePlanFingerprint(reviewed.preview.operationPlan, completedTargetKeys) !== createBatchIntentCompatiblePlanFingerprint(executionPreview.operationPlan, completedTargetKeys)) {
|
|
3066
|
+
appendStoppedSources(sourceIndex, '前序结果改变了这个来源的实际写入计划。');
|
|
3067
|
+
break;
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
const requiresManagedCodex = reviewed.preview.codexManagerPreview?.state === 'requires-install';
|
|
3071
|
+
const result = await taskWriteCommand(commandTargets(current), {
|
|
3072
|
+
cwd: context.cwd,
|
|
3073
|
+
repairTargets: repairTargets(current),
|
|
3074
|
+
...(reviewedScriptTimeoutMs(intent) === undefined
|
|
3075
|
+
? {} : { scriptTimeoutMs: reviewedScriptTimeoutMs(intent) }),
|
|
3076
|
+
}, {
|
|
3077
|
+
...context,
|
|
3078
|
+
awHomeMutationToken,
|
|
3079
|
+
catalogOperationContext: catalogOperation,
|
|
3080
|
+
expectedGenerationSetDigest: catalogOperation.generationSetDigest,
|
|
3081
|
+
expectedCatalogConfigDigest: expectedTask.catalog.configDigest,
|
|
3082
|
+
inventoryCatalogSourceReferences: sourceInventory,
|
|
3083
|
+
preApplyConfirmed: true,
|
|
3084
|
+
preApplyExecutionCache,
|
|
3085
|
+
...(executionPreview.operationPlan
|
|
3086
|
+
? { expectedPlanFingerprint: executionPreview.operationPlan.planFingerprint } : {}),
|
|
3087
|
+
...(requiresManagedCodex ? { managedCodexAuthorizer: async () => true } : {}),
|
|
3088
|
+
...(codexObservationSession ? { codexObservationSession } : {}),
|
|
3089
|
+
});
|
|
3090
|
+
if (writeReviewIsStale(result)) {
|
|
3091
|
+
if (sourceResults.length === 0)
|
|
3092
|
+
throwIfWriteReviewStale(result);
|
|
3093
|
+
appendStoppedSources(sourceIndex, '这个来源的实际写入计划发生变化。');
|
|
3094
|
+
break;
|
|
3095
|
+
}
|
|
3096
|
+
for (const target of result.targetResults) {
|
|
3097
|
+
if (target.state === 'completed') {
|
|
3098
|
+
completedTargetKeys.add([
|
|
3099
|
+
target.target.pluginId,
|
|
3100
|
+
target.target.platform,
|
|
3101
|
+
target.target.scope,
|
|
3102
|
+
].join('\0'));
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
sourceResults.push({
|
|
3106
|
+
sourceId: reviewed.sourceId,
|
|
3107
|
+
sourceIdentity: reviewed.sourceIdentity,
|
|
3108
|
+
requestedTargets: reviewed.requestedTargets,
|
|
3109
|
+
result,
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3112
|
+
const merged = mergeWriteCommandResults(sourceResults.map((source) => source.result));
|
|
3113
|
+
return {
|
|
3114
|
+
...merged,
|
|
3115
|
+
taskPlan: expectedTask.taskPlan,
|
|
3116
|
+
sourceResults,
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
2501
3119
|
if (intent.kind !== 'pluginWrite'
|
|
2502
3120
|
&& !operationEvidenceMatches(expectedEvidence, currentEvidence)) {
|
|
2503
3121
|
throw new TuiOperationBlockedError('operationReviewStale', '确认后状态发生变化。');
|