@lesliechan721/aw 0.1.0-beta.34 → 0.1.0-beta.36

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 (102) hide show
  1. package/dist/scripts/test-plugin-scripts.js +1 -1
  2. package/dist/src/commands/aw-capabilities.d.ts +1 -1
  3. package/dist/src/commands/aw-capabilities.js +1 -0
  4. package/dist/src/commands/aw-capabilities.js.map +1 -1
  5. package/dist/src/commands/operation-plan.d.ts +19 -0
  6. package/dist/src/commands/operation-plan.js.map +1 -1
  7. package/dist/src/commands/rebuild-command.d.ts +1 -0
  8. package/dist/src/commands/rebuild-command.js +890 -8
  9. package/dist/src/commands/rebuild-command.js.map +1 -1
  10. package/dist/src/commands/rebuild-integration.d.ts +12 -0
  11. package/dist/src/commands/rebuild-integration.js +88 -2
  12. package/dist/src/commands/rebuild-integration.js.map +1 -1
  13. package/dist/src/commands/status-v6.js +109 -19
  14. package/dist/src/commands/status-v6.js.map +1 -1
  15. package/dist/src/commands/wizard.d.ts +5 -1
  16. package/dist/src/commands/wizard.js +4 -2
  17. package/dist/src/commands/wizard.js.map +1 -1
  18. package/dist/src/commands/write-commands.js +3 -0
  19. package/dist/src/commands/write-commands.js.map +1 -1
  20. package/dist/src/index.js +57 -4
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/portable/materializer.js +19 -3
  23. package/dist/src/portable/materializer.js.map +1 -1
  24. package/dist/src/review/review-profile.js +19 -2
  25. package/dist/src/review/review-profile.js.map +1 -1
  26. package/dist/src/tui/components.d.ts +39 -0
  27. package/dist/src/tui/components.js +51 -18
  28. package/dist/src/tui/components.js.map +1 -1
  29. package/dist/src/tui/config-array-draft.d.ts +29 -0
  30. package/dist/src/tui/config-array-draft.js +48 -0
  31. package/dist/src/tui/config-array-draft.js.map +1 -0
  32. package/dist/src/tui/config-value-presentation.d.ts +1 -0
  33. package/dist/src/tui/config-value-presentation.js +17 -1
  34. package/dist/src/tui/config-value-presentation.js.map +1 -1
  35. package/dist/src/tui/console-app-v4.js +1742 -1368
  36. package/dist/src/tui/console-app-v4.js.map +1 -1
  37. package/dist/src/tui/content-item-card.d.ts +13 -0
  38. package/dist/src/tui/content-item-card.js +51 -0
  39. package/dist/src/tui/content-item-card.js.map +1 -0
  40. package/dist/src/tui/controller-runtime.js +45 -4
  41. package/dist/src/tui/controller-runtime.js.map +1 -1
  42. package/dist/src/tui/controller.d.ts +18 -0
  43. package/dist/src/tui/controller.js +168 -12
  44. package/dist/src/tui/controller.js.map +1 -1
  45. package/dist/src/tui/dependency-graph-view.d.ts +39 -0
  46. package/dist/src/tui/dependency-graph-view.js +121 -0
  47. package/dist/src/tui/dependency-graph-view.js.map +1 -0
  48. package/dist/src/tui/dependency-graph.js +16 -19
  49. package/dist/src/tui/dependency-graph.js.map +1 -1
  50. package/dist/src/tui/entity-cards.d.ts +55 -0
  51. package/dist/src/tui/entity-cards.js +220 -0
  52. package/dist/src/tui/entity-cards.js.map +1 -0
  53. package/dist/src/tui/input-display.js +10 -1
  54. package/dist/src/tui/input-display.js.map +1 -1
  55. package/dist/src/tui/model.d.ts +11 -0
  56. package/dist/src/tui/model.js +20 -4
  57. package/dist/src/tui/model.js.map +1 -1
  58. package/dist/src/tui/operation-diagnostic-screens.d.ts +2 -18
  59. package/dist/src/tui/operation-diagnostic-screens.js +16 -69
  60. package/dist/src/tui/operation-diagnostic-screens.js.map +1 -1
  61. package/dist/src/tui/operation-review-planner.js +44 -3
  62. package/dist/src/tui/operation-review-planner.js.map +1 -1
  63. package/dist/src/tui/plugin-content.d.ts +8 -0
  64. package/dist/src/tui/plugin-content.js +409 -65
  65. package/dist/src/tui/plugin-content.js.map +1 -1
  66. package/dist/src/tui/plugin-detail.d.ts +52 -0
  67. package/dist/src/tui/plugin-detail.js +165 -0
  68. package/dist/src/tui/plugin-detail.js.map +1 -0
  69. package/dist/src/tui/project-inventory.d.ts +68 -0
  70. package/dist/src/tui/project-inventory.js +203 -0
  71. package/dist/src/tui/project-inventory.js.map +1 -0
  72. package/dist/src/tui/query-header.d.ts +30 -0
  73. package/dist/src/tui/query-header.js +40 -0
  74. package/dist/src/tui/query-header.js.map +1 -0
  75. package/dist/src/tui/reader.d.ts +37 -0
  76. package/dist/src/tui/reader.js +47 -0
  77. package/dist/src/tui/reader.js.map +1 -0
  78. package/dist/src/tui/router.d.ts +26 -3
  79. package/dist/src/tui/router.js.map +1 -1
  80. package/dist/src/tui/screen-registry.d.ts +9 -1
  81. package/dist/src/tui/screen-registry.js +1 -1
  82. package/dist/src/tui/screen-registry.js.map +1 -1
  83. package/dist/src/tui/source-detail-card.d.ts +29 -0
  84. package/dist/src/tui/source-detail-card.js +45 -0
  85. package/dist/src/tui/source-detail-card.js.map +1 -0
  86. package/dist/src/tui/theme.d.ts +12 -0
  87. package/dist/src/tui/theme.js +12 -0
  88. package/dist/src/tui/theme.js.map +1 -1
  89. package/dist/src/tui/wizard.js +26 -4
  90. package/dist/src/tui/wizard.js.map +1 -1
  91. package/dist/src/types.d.ts +2 -0
  92. package/dist/src/types.js.map +1 -1
  93. package/dist/src/utils/catalog-source-references.js +2 -2
  94. package/dist/src/utils/local-marketplace.d.ts +1 -0
  95. package/dist/src/utils/local-marketplace.js +22 -0
  96. package/dist/src/utils/local-marketplace.js.map +1 -1
  97. package/dist/src/utils/plugin-config-write.js +3 -1
  98. package/dist/src/utils/plugin-config-write.js.map +1 -1
  99. package/dist/src/utils/plugin-config.d.ts +5 -5
  100. package/dist/src/utils/plugin-config.js +47 -23
  101. package/dist/src/utils/plugin-config.js.map +1 -1
  102. package/package.json +4 -4
@@ -16,18 +16,18 @@ import { buildGeneratedName } from '../generators/shared.js';
16
16
  import { createCodexReconcilePlan, } from '../utils/codex-reconciler.js';
17
17
  import { readCodexConfigFile } from '../utils/codex-config.js';
18
18
  import { executeRebuildComponentCleanup, executeRebuildOperation, prepareRebuildOperation, } from './rebuild-operations.js';
19
- import { assertNoClaudeUnboundNativeNameCollision, CodexActivationPostcheckError, createRebuildIntegration, executeCodexReconcileActivations, inspectCodexPluginRegistrationLive, inspectCodexSharedNativeLive, inspectNativeMarketplaceRegistrations, postcheckCodexReconcileActivations, } from './rebuild-integration.js';
20
- import { getNamespacedMarketplaceName } from '../utils/local-marketplace.js';
19
+ import { assertNoClaudeUnboundNativeNameCollision, CodexActivationPostcheckError, createRebuildIntegration, executeCodexReconcileActivations, inspectCodexPluginRegistrationLive, inspectCodexSharedNativeLive, inspectNativeMarketplaceRegistrations, removeCodexPluginActivation, removeNamespacedMarketplacePluginProjection, postcheckCodexReconcileActivations, } from './rebuild-integration.js';
20
+ import { assertSafeNamespacedMarketplacePluginRoot, getNamespacedMarketplaceName, readStrictNamespacedMarketplaceRoot, } from '../utils/local-marketplace.js';
21
21
  import { formatPluginId, parsePluginId, planPluginSelectorSourceIds, preflightDesiredArtifactCollisions, withResolvedMultiSourceCatalog, } from '../utils/multi-source-catalog.js';
22
22
  import { calculateCodexNativeRenderPayloadDigest, calculateCodexSharedNativePayloadDigest, } from '../utils/catalog-digests.js';
23
23
  import { resolveCatalogOperationContext, } from '../utils/catalog-operation-context.js';
24
24
  import { assertStrictSemVer } from '../utils/manifest.js';
25
25
  import { assertCatalogSourceInventoryComplete, canonicalMarketplaceRegistrationNames, inventoryCatalogSourceReferences, } from '../utils/catalog-source-references.js';
26
26
  import { sanitizeCatalogSourceErrorMessage } from '../utils/catalog-public-error.js';
27
- import { resolveEffectivePluginConfig } from '../utils/plugin-config.js';
27
+ import { inspectPluginConfigResetTarget, readNamespacedPluginConfigStoreIdentity, resetPluginConfigStore, resolvePluginConfigStorePath, resolveEffectivePluginConfig, } from '../utils/plugin-config.js';
28
28
  import { resolveArtifactRenderConfig } from '../utils/artifact-renderer.js';
29
29
  import { inspectOwnedComponents, parseOwnedTextHeader, readSkillOwnerMarker, SKILL_OWNER_MARKER, } from '../utils/ownership.js';
30
- import { resolveActiveCatalogPluginShallow } from '../utils/catalog-generation-resolver.js';
30
+ import { resolveActiveCatalogGeneration, resolveActiveCatalogPluginShallow } from '../utils/catalog-generation-resolver.js';
31
31
  import { canonicalizePathThroughExistingAncestorSync, codexProjectConfigAliasesUserHome, getClaudeHome, getCodexHome, resolveCanonicalRepoProjectKey, resolveRepoRootSync, } from '../utils/state-paths.js';
32
32
  import { resolvePluginMetaDependencyClosure } from '../utils/plugin-dependencies.js';
33
33
  import { readCatalogSourcesConfigSnapshot } from '../utils/catalog-sources.js';
@@ -402,6 +402,18 @@ function toTargetResult(result) {
402
402
  } : {}),
403
403
  };
404
404
  }
405
+ function toRemovedCleanupTargetResult(result) {
406
+ const target = toTargetResult(result);
407
+ if (result.status === 'succeeded' || !result.target.pluginId)
408
+ return target;
409
+ return {
410
+ ...target,
411
+ recoveryActions: [
412
+ `repair the reported ownership, native CLI, or config path evidence, then run aw cleanup ${result.target.pluginId} --platform both --scope user --yes`,
413
+ 'use cleanup --force only for exclusively owned damaged output, then retry ordinary cleanup',
414
+ ],
415
+ };
416
+ }
405
417
  async function assertClaudeVersion(options) {
406
418
  if (!options.platforms.includes('claude'))
407
419
  return;
@@ -659,6 +671,9 @@ function mergeRebuildCommandResults(results) {
659
671
  ...(results.some((result) => result.offlineCleanupPlans !== undefined)
660
672
  ? { offlineCleanupPlans: results.flatMap((result) => result.offlineCleanupPlans ?? []) }
661
673
  : {}),
674
+ ...(results.some((result) => result.removedPluginCleanupIds !== undefined)
675
+ ? { removedPluginCleanupIds: [...new Set(results.flatMap((result) => result.removedPluginCleanupIds ?? []))] }
676
+ : {}),
662
677
  };
663
678
  }
664
679
  function reconcileOverallState(targetResults, plan, rebuildResults = []) {
@@ -1092,7 +1107,7 @@ async function cleanupOfflineOwnedComponents(options) {
1092
1107
  && candidate.sourceIdentity === target.sourceIdentity
1093
1108
  && candidate.platform === target.platform
1094
1109
  && candidate.scope === target.scope);
1095
- const activationAction = target.platform === 'codex'
1110
+ const activationAction = target.platform === 'codex' && !options.removedPluginWhole
1096
1111
  ? evidence?.plan.actions.find((action) => action.action === 'removeNativeRegistration'
1097
1112
  && action.activationScope === target.scope)
1098
1113
  : undefined;
@@ -1124,6 +1139,7 @@ async function cleanupOfflineOwnedComponents(options) {
1124
1139
  channel: 'beta',
1125
1140
  },
1126
1141
  includeNativeCleanupCandidate: target.nativeRegistration === true,
1142
+ ...(options.removedPluginWhole ? { removedPluginWhole: true } : {}),
1127
1143
  ...(activationAction
1128
1144
  ? {
1129
1145
  codexActivationCleanup: [{
@@ -1536,6 +1552,750 @@ async function mergeForceCleanupPlans(...plans) {
1536
1552
  }))),
1537
1553
  };
1538
1554
  }
1555
+ function removedCleanupDigest(value) {
1556
+ return `sha256:${crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex')}`;
1557
+ }
1558
+ async function removedProjectionTreeFingerprint(root) {
1559
+ const visit = async (location) => {
1560
+ const stats = await fs.lstat(location).catch((error) => {
1561
+ if (error.code === 'ENOENT')
1562
+ return null;
1563
+ throw error;
1564
+ });
1565
+ if (!stats)
1566
+ return null;
1567
+ if (stats.isSymbolicLink() || (!stats.isDirectory() && !stats.isFile())) {
1568
+ return { kind: 'unsafe', mode: stats.mode, size: stats.size, mtimeMs: stats.mtimeMs };
1569
+ }
1570
+ if (stats.isFile()) {
1571
+ return { kind: 'file', digest: `sha256:${crypto.createHash('sha256').update(await fs.readFile(location)).digest('hex')}` };
1572
+ }
1573
+ const entries = await fs.readdir(location);
1574
+ return { kind: 'directory', entries: await Promise.all(entries.sort().map(async (name) => [name, await visit(path.join(location, name))])) };
1575
+ };
1576
+ return removedCleanupDigest(await visit(root));
1577
+ }
1578
+ function removeRemovedPluginIntent(state, pluginIds) {
1579
+ return canonicalizePluginIntentState({
1580
+ ...state,
1581
+ roots: state.roots.filter((root) => !pluginIds.has(root.pluginId)),
1582
+ dependencyScopeOverrides: state.dependencyScopeOverrides.filter((override) => !pluginIds.has(override.pluginId)),
1583
+ });
1584
+ }
1585
+ async function inspectRemovedPluginConfig(options) {
1586
+ const inspection = await inspectPluginConfigResetTarget(options);
1587
+ if (inspection.kind !== 'regularFile')
1588
+ return inspection;
1589
+ try {
1590
+ const identity = await readNamespacedPluginConfigStoreIdentity(options);
1591
+ if (identity && (options.scope === 'user'
1592
+ ? identity.binding.kind !== 'source'
1593
+ || identity.binding.sourceIdentity !== options.sourceIdentity
1594
+ : identity.binding.kind !== 'catalog'
1595
+ || identity.binding.logicalIdentity !== options.logicalIdentity)) {
1596
+ return { ...inspection, conflict: 'removedPluginConfigIdentityConflict: saved config binding differs from the Active source.' };
1597
+ }
1598
+ }
1599
+ catch {
1600
+ const filePath = resolvePluginConfigStorePath(options);
1601
+ const raw = await fs.readJson(filePath).catch(() => null);
1602
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
1603
+ const binding = raw.binding;
1604
+ if ((typeof raw.sourceId === 'string' && raw.sourceId !== options.sourceId)
1605
+ || (typeof raw.pluginName === 'string' && raw.pluginName !== options.pluginName)
1606
+ || (binding?.kind === 'source' && options.scope !== 'user')
1607
+ || (binding?.kind === 'catalog' && options.scope !== 'project')
1608
+ || (binding?.kind === 'source' && options.scope === 'user'
1609
+ && typeof binding.sourceIdentity === 'string'
1610
+ && binding.sourceIdentity !== options.sourceIdentity)
1611
+ || (binding?.kind === 'catalog' && options.scope === 'project'
1612
+ && typeof binding.logicalIdentity === 'string'
1613
+ && binding.logicalIdentity !== options.logicalIdentity)) {
1614
+ return { ...inspection, conflict: 'removedPluginConfigIdentityConflict: saved config identifies another source or plugin.' };
1615
+ }
1616
+ }
1617
+ }
1618
+ return inspection;
1619
+ }
1620
+ async function inspectRemovedPluginCleanup(options) {
1621
+ const parsed = parsePluginId(options.pluginId);
1622
+ if (!parsed.sourceId)
1623
+ return null;
1624
+ let generation;
1625
+ try {
1626
+ generation = await resolveActiveCatalogGeneration({ sourceId: parsed.sourceId, env: options.env });
1627
+ }
1628
+ catch {
1629
+ return null;
1630
+ }
1631
+ if (generation.manifest.entries.some((entry) => entry.name === parsed.pluginName))
1632
+ return null;
1633
+ const pluginId = formatPluginId(parsed);
1634
+ const blockers = [];
1635
+ const marketplaceRoot = path.join(resolveAwHome(options.env), 'marketplaces', parsed.sourceId);
1636
+ const relevant = options.inventory.references.filter((reference) => reference.sourceId === parsed.sourceId && reference.pluginName === parsed.pluginName);
1637
+ const ownershipConflict = relevant.some((reference) => reference.sourceIdentity !== undefined
1638
+ && reference.sourceIdentity !== generation.sourceIdentity);
1639
+ if (ownershipConflict) {
1640
+ blockers.push('removedPluginOwnershipConflict: live source identity differs from Active. Repair ownership evidence, then retry cleanup.');
1641
+ }
1642
+ let root = null;
1643
+ try {
1644
+ root = await readStrictNamespacedMarketplaceRoot({
1645
+ rootDir: marketplaceRoot,
1646
+ sourceId: parsed.sourceId,
1647
+ sourceIdentity: generation.sourceIdentity,
1648
+ });
1649
+ if (root?.config.logicalIdentity && root.config.logicalIdentity !== generation.logicalIdentity) {
1650
+ blockers.push('removedPluginMarketplaceIdentityConflict: marketplace logical identity differs from Active.');
1651
+ }
1652
+ }
1653
+ catch (error) {
1654
+ blockers.push(`removedPluginMarketplaceEvidenceUnavailable: ${error instanceof Error ? error.message : String(error)}`);
1655
+ }
1656
+ const registeredNames = canonicalMarketplaceRegistrationNames(options.inventory.references, parsed.sourceId);
1657
+ const marketplaceName = root?.config.marketplaceName
1658
+ ?? (registeredNames.length === 1 ? registeredNames[0] : undefined);
1659
+ let registrationReadFailed = false;
1660
+ const registration = root ? await inspectNativeMarketplaceRegistrations({
1661
+ marketplaceName: root.config.marketplaceName,
1662
+ marketplaceRoot,
1663
+ cwd: options.cwd,
1664
+ env: options.env,
1665
+ }).catch((error) => {
1666
+ registrationReadFailed = true;
1667
+ blockers.push(`removedPluginMarketplaceRegistrationUnknown: ${error instanceof Error ? error.message : String(error)}`);
1668
+ return null;
1669
+ }) : null;
1670
+ const projectionOwnershipVerified = Boolean(root
1671
+ && !ownershipConflict
1672
+ && (!root.config.logicalIdentity || root.config.logicalIdentity === generation.logicalIdentity));
1673
+ const nativeOwnershipVerified = projectionOwnershipVerified
1674
+ && registration?.codex === 'canonical';
1675
+ let claudeNativeCleanupBlocked = registrationReadFailed;
1676
+ if (registration?.claude === 'foreign') {
1677
+ blockers.push('removedPluginClaudeOwnershipConflict: Claude marketplace registration is foreign.');
1678
+ claudeNativeCleanupBlocked = true;
1679
+ }
1680
+ if (registration?.claude !== 'canonical' && relevant.some((reference) => reference.platform === 'claude' && reference.kind === 'namespacedMarketplace')) {
1681
+ blockers.push('removedPluginClaudeOwnershipUnavailable: Claude native evidence exists without a canonical marketplace registration.');
1682
+ claudeNativeCleanupBlocked = true;
1683
+ }
1684
+ const activationScopes = [];
1685
+ if (marketplaceName) {
1686
+ const inspectedPaths = new Set();
1687
+ for (const scope of ['user', 'project']) {
1688
+ const configPath = resolveCodexPaths(options.cwd, scope, options.env).configTomlPath;
1689
+ if (inspectedPaths.has(path.resolve(configPath)))
1690
+ continue;
1691
+ inspectedPaths.add(path.resolve(configPath));
1692
+ try {
1693
+ const config = await readCodexConfigFile(configPath);
1694
+ if (Object.hasOwn(config.snapshot.plugins, `${parsed.pluginName}@${marketplaceName}`)) {
1695
+ activationScopes.push({
1696
+ scope,
1697
+ enabled: config.snapshot.plugins[`${parsed.pluginName}@${marketplaceName}`]?.enabled,
1698
+ });
1699
+ }
1700
+ }
1701
+ catch (error) {
1702
+ blockers.push(`removedPluginActivationReadFailed: ${scope}: ${error instanceof Error ? error.message : String(error)}`);
1703
+ }
1704
+ }
1705
+ }
1706
+ let nativeState = 'unbound';
1707
+ let nativeObservationFingerprint = removedCleanupDigest('unbound');
1708
+ if (root) {
1709
+ if (registration?.codex === 'foreign') {
1710
+ blockers.push('removedPluginNativeOwnershipConflict: Codex marketplace registration is foreign.');
1711
+ }
1712
+ else {
1713
+ const observed = await inspectCodexPluginRegistrationLive({
1714
+ pluginId: `${parsed.pluginName}@${marketplaceName}`,
1715
+ cwd: options.cwd,
1716
+ env: options.env,
1717
+ nativeCliRunner: options.nativeCliRunner,
1718
+ manager: options.codexManager,
1719
+ scope: 'both',
1720
+ });
1721
+ nativeState = observed.observation.state === 'observed'
1722
+ ? observed.observation.value === 'installed' ? 'installed' : 'absent'
1723
+ : 'unknown';
1724
+ nativeObservationFingerprint = removedCleanupDigest(observed.observation);
1725
+ if (nativeState === 'unknown') {
1726
+ blockers.push('removedPluginNativeObservationUnknown: official Codex list did not confirm installed or absent. Retry cleanup with a compatible Codex CLI.');
1727
+ }
1728
+ if (registration?.codex !== 'canonical'
1729
+ && (nativeState === 'installed' || activationScopes.length > 0)) {
1730
+ blockers.push('removedPluginNativeOwnershipUnavailable: native or activation evidence exists but canonical marketplace registration is missing.');
1731
+ }
1732
+ }
1733
+ }
1734
+ else if (options.inventory.references.some((reference) => reference.pluginName === parsed.pluginName
1735
+ && reference.platform === 'codex'
1736
+ && reference.marketplaceName)) {
1737
+ blockers.push('removedPluginNativeOwnershipUnavailable: native evidence remains without a valid AW marketplace root.');
1738
+ }
1739
+ const configTargets = [];
1740
+ for (const scope of ['user', 'project']) {
1741
+ try {
1742
+ const inspected = await inspectRemovedPluginConfig({
1743
+ sourceId: parsed.sourceId,
1744
+ sourceIdentity: generation.sourceIdentity,
1745
+ logicalIdentity: generation.logicalIdentity,
1746
+ pluginName: parsed.pluginName,
1747
+ scope,
1748
+ cwd: options.cwd,
1749
+ env: options.env,
1750
+ });
1751
+ configTargets.push({ scope, kind: inspected.kind, fingerprint: inspected.fingerprint });
1752
+ if (inspected.conflict)
1753
+ blockers.push(`${scope}: ${inspected.conflict}`);
1754
+ if (inspected.kind === 'directory' || inspected.kind === 'special') {
1755
+ blockers.push(`removedPluginConfigTargetUnsupported: ${scope} saved config is a ${inspected.kind}. Repair the node or run explicit config reset, then retry cleanup.`);
1756
+ }
1757
+ }
1758
+ catch (error) {
1759
+ blockers.push(`removedPluginConfigTargetUnsafe: ${scope}: ${error instanceof Error ? error.message : String(error)}`);
1760
+ }
1761
+ }
1762
+ const projectionRoot = path.join(marketplaceRoot, 'plugins', parsed.pluginName);
1763
+ let projectionPathSafe = true;
1764
+ if (root) {
1765
+ try {
1766
+ await assertSafeNamespacedMarketplacePluginRoot(marketplaceRoot, parsed.pluginName);
1767
+ }
1768
+ catch (error) {
1769
+ projectionPathSafe = false;
1770
+ blockers.push(`removedPluginMarketplaceProjectionUnsafe: ${error instanceof Error ? error.message : String(error)}`);
1771
+ }
1772
+ }
1773
+ const projectionStats = root && projectionPathSafe ? await fs.lstat(projectionRoot).catch((error) => {
1774
+ if (error.code === 'ENOENT')
1775
+ return null;
1776
+ throw error;
1777
+ }) : null;
1778
+ const projectionPresent = Boolean(root && (projectionStats
1779
+ || [root.agentsManifest, root.claudeManifest].some((manifest) => manifest && Array.isArray(manifest.plugins) && manifest.plugins.some((entry) => entry !== null && typeof entry === 'object'
1780
+ && entry.name === parsed.pluginName))));
1781
+ const projectionFingerprint = removedCleanupDigest({
1782
+ tree: root && projectionPathSafe ? await removedProjectionTreeFingerprint(projectionRoot) : 'unsafe',
1783
+ agentsEntries: root && Array.isArray(root.agentsManifest.plugins)
1784
+ ? root.agentsManifest.plugins.filter((entry) => entry !== null && typeof entry === 'object'
1785
+ && entry.name === parsed.pluginName)
1786
+ : null,
1787
+ claudeEntries: root?.claudeManifest && Array.isArray(root.claudeManifest.plugins)
1788
+ ? root.claudeManifest.plugins.filter((entry) => entry !== null && typeof entry === 'object'
1789
+ && entry.name === parsed.pluginName)
1790
+ : null,
1791
+ });
1792
+ const marketplaceEvidenceFingerprint = removedCleanupDigest({
1793
+ config: root?.config ?? null,
1794
+ registration,
1795
+ marketplaceName,
1796
+ });
1797
+ const evidence = [];
1798
+ for (const platform of ['codex', 'claude']) {
1799
+ for (const scope of ['user', 'project']) {
1800
+ const base = offlineCleanupEvidenceFromInventory({
1801
+ reason: 'bundleUnavailable',
1802
+ pluginId,
1803
+ pluginName: parsed.pluginName,
1804
+ sourceId: parsed.sourceId,
1805
+ sourceIdentity: generation.sourceIdentity,
1806
+ platform,
1807
+ scope,
1808
+ inventory: options.inventory,
1809
+ }) ?? intentOnlyOfflineCleanupEvidence({
1810
+ pluginId,
1811
+ pluginName: parsed.pluginName,
1812
+ sourceId: parsed.sourceId,
1813
+ sourceIdentity: generation.sourceIdentity,
1814
+ platform,
1815
+ scope,
1816
+ });
1817
+ const actions = base.plan.actions.flatMap((action) => {
1818
+ if (platform === 'codex' && action.action === 'removeNativeRegistration')
1819
+ return [];
1820
+ if (platform === 'claude' && action.action === 'removeNativeRegistration') {
1821
+ return root && projectionOwnershipVerified && !claudeNativeCleanupBlocked
1822
+ && registration?.claude === 'canonical'
1823
+ && marketplaceName && action.kind === 'namespacedMarketplace'
1824
+ ? [{ ...action, target: `${parsed.pluginName}@${marketplaceName}` }]
1825
+ : [];
1826
+ }
1827
+ return [action];
1828
+ });
1829
+ if (platform === 'codex' && scope === 'user' && nativeState === 'installed'
1830
+ && marketplaceName && nativeOwnershipVerified && projectionPathSafe) {
1831
+ actions.push({
1832
+ action: 'removeNativeRegistration',
1833
+ kind: 'namespacedMarketplace',
1834
+ target: `${parsed.pluginName}@${marketplaceName}`,
1835
+ marketplaceName,
1836
+ });
1837
+ }
1838
+ const safeActions = ownershipConflict ? [] : actions;
1839
+ const enrichedActions = await Promise.all(safeActions.map(async (action) => action.action === 'removeOwnedOutput'
1840
+ ? { ...action, pathEvidence: await removedProjectionTreeFingerprint(action.target.split('#', 1)[0]) }
1841
+ : action));
1842
+ const plan = { ...base.plan, actions: enrichedActions };
1843
+ evidence.push({ ...base, plan, planDigest: removedCleanupDigest(plan), targetFingerprint: removedCleanupDigest(plan) });
1844
+ }
1845
+ }
1846
+ return {
1847
+ pluginId,
1848
+ pluginName: parsed.pluginName,
1849
+ sourceId: parsed.sourceId,
1850
+ sourceIdentity: generation.sourceIdentity,
1851
+ logicalIdentity: generation.logicalIdentity,
1852
+ snapshotDigest: generation.snapshotDigest,
1853
+ marketplaceRoot,
1854
+ ...(marketplaceName ? { marketplaceName } : {}),
1855
+ nativeOwnershipVerified: nativeOwnershipVerified && projectionPathSafe,
1856
+ projectionOwnershipVerified: projectionOwnershipVerified && projectionPathSafe,
1857
+ claudeNativeCleanupBlocked,
1858
+ nativeState,
1859
+ nativeObservationFingerprint,
1860
+ marketplaceEvidenceFingerprint,
1861
+ projectionFingerprint,
1862
+ projectionPresent,
1863
+ activationScopes,
1864
+ configTargets,
1865
+ evidence,
1866
+ blockers,
1867
+ };
1868
+ }
1869
+ function createRemovedPluginCleanupOperationPlan(options) {
1870
+ const actions = [];
1871
+ const changes = [];
1872
+ for (const target of options.targets) {
1873
+ for (const scope of ['user', 'project']) {
1874
+ const before = options.intents[scope].before;
1875
+ const after = options.intents[scope].after;
1876
+ if (calculatePluginIntentDigest(before) !== calculatePluginIntentDigest(after)
1877
+ && (before.roots.some((root) => root.pluginId === target.pluginId)
1878
+ || before.dependencyScopeOverrides.some((override) => override.pluginId === target.pluginId))) {
1879
+ changes.push(`${target.pluginId}:${scope}:removeIntentAndOverrides`);
1880
+ }
1881
+ }
1882
+ for (const evidence of target.evidence) {
1883
+ for (const action of evidence.plan.actions) {
1884
+ actions.push(action.action === 'removeNativeRegistration'
1885
+ ? {
1886
+ kind: 'removeNativeRegistration',
1887
+ pluginId: target.pluginId,
1888
+ platform: evidence.platform,
1889
+ scope: evidence.scope,
1890
+ registration: action.target,
1891
+ }
1892
+ : {
1893
+ kind: 'removeOwnedArtifact',
1894
+ pluginId: target.pluginId,
1895
+ platform: evidence.platform,
1896
+ scope: evidence.scope,
1897
+ path: action.target,
1898
+ ownerFingerprint: evidence.targetFingerprint,
1899
+ });
1900
+ changes.push(`${target.pluginId}:${evidence.platform}:${evidence.scope}:${action.action}:${action.target}`);
1901
+ }
1902
+ }
1903
+ for (const config of target.configTargets.filter((entry) => entry.kind !== 'absent')) {
1904
+ if (config.kind === 'directory' || config.kind === 'special'
1905
+ || target.blockers.some((blocker) => blocker.startsWith(`${config.scope}: removedPluginConfigIdentityConflict`))) {
1906
+ continue;
1907
+ }
1908
+ actions.push({
1909
+ kind: 'removePluginConfig',
1910
+ pluginId: target.pluginId,
1911
+ platform: 'codex',
1912
+ scope: config.scope,
1913
+ path: resolvePluginConfigStorePath({
1914
+ sourceId: target.sourceId,
1915
+ pluginName: target.pluginName,
1916
+ scope: config.scope,
1917
+ cwd: options.cwd,
1918
+ env: options.env,
1919
+ }),
1920
+ fingerprint: config.fingerprint,
1921
+ });
1922
+ changes.push(`${target.pluginId}:${config.scope}:removeSavedConfig:${config.kind}:${config.fingerprint}`);
1923
+ }
1924
+ for (const activation of target.activationScopes) {
1925
+ changes.push(`${target.pluginId}:codex:${activation.scope}:activationEvidence:${activation.enabled}`);
1926
+ if (!target.nativeOwnershipVerified || target.nativeState === 'unknown')
1927
+ continue;
1928
+ actions.push({
1929
+ kind: 'removeActivation',
1930
+ pluginId: target.pluginId,
1931
+ platform: 'codex',
1932
+ scope: activation.scope,
1933
+ registration: `${target.pluginName}@${target.marketplaceName}`,
1934
+ });
1935
+ changes.push(`${target.pluginId}:codex:${activation.scope}:removeActivation`);
1936
+ }
1937
+ changes.push(`${target.pluginId}:projectionEvidence:${target.projectionFingerprint}`);
1938
+ if (target.projectionPresent && target.projectionOwnershipVerified
1939
+ && target.nativeState !== 'unknown'
1940
+ && (target.nativeOwnershipVerified || target.activationScopes.length === 0)) {
1941
+ actions.push({
1942
+ kind: 'removeMarketplaceProjection',
1943
+ pluginId: target.pluginId,
1944
+ platform: 'codex',
1945
+ scope: 'user',
1946
+ marketplaceRoot: target.marketplaceRoot,
1947
+ });
1948
+ changes.push(`${target.pluginId}:removeMarketplaceProjection`);
1949
+ }
1950
+ changes.push(`${target.pluginId}:nativeObservation:${target.nativeState}:${target.nativeObservationFingerprint}:${target.marketplaceEvidenceFingerprint}`);
1951
+ }
1952
+ const warnings = options.targets.flatMap((target) => [
1953
+ `removedPluginCleanupScope: ${target.pluginId} 清理范围为 Codex 和 Claude 的用户及当前项目,包含两处保存设置。`,
1954
+ `removedPluginCleanupSharedNativeImpact: ${target.pluginId} 的 Codex 共享原生包卸载后,其他项目可能无法继续使用;其他项目的本地记录不会修改。`,
1955
+ ...target.blockers,
1956
+ ]);
1957
+ return createOperationPlan({
1958
+ intentChanges: ['user', 'project'].flatMap((scope) => {
1959
+ const beforeDigest = calculatePluginIntentDigest(options.intents[scope].before);
1960
+ const afterDigest = calculatePluginIntentDigest(options.intents[scope].after);
1961
+ return beforeDigest === afterDigest ? [] : [{ scope, beforeDigest, afterDigest }];
1962
+ }),
1963
+ actions,
1964
+ changes,
1965
+ warnings,
1966
+ evidence: {
1967
+ catalogDigests: options.targets.map((target) => ({
1968
+ sourceId: target.sourceId,
1969
+ digest: removedCleanupDigest({
1970
+ sourceIdentity: target.sourceIdentity,
1971
+ logicalIdentity: target.logicalIdentity,
1972
+ snapshotDigest: target.snapshotDigest,
1973
+ }),
1974
+ })),
1975
+ intentDigests: ['user', 'project'].map((scope) => ({
1976
+ scope,
1977
+ digest: calculatePluginIntentDigest(options.intents[scope].before),
1978
+ })),
1979
+ configDigests: options.targets.flatMap((target) => target.configTargets.map((config) => ({
1980
+ pluginId: target.pluginId,
1981
+ scope: config.scope,
1982
+ digest: config.fingerprint,
1983
+ }))),
1984
+ targetOwnerFingerprints: options.targets.flatMap((target) => target.evidence.map((evidence) => ({
1985
+ pluginId: target.pluginId,
1986
+ platform: evidence.platform,
1987
+ scope: evidence.scope,
1988
+ fingerprint: evidence.targetFingerprint,
1989
+ }))),
1990
+ },
1991
+ });
1992
+ }
1993
+ function combineCleanupOperationPlans(plans) {
1994
+ return createOperationPlan({
1995
+ intentChanges: plans.flatMap((plan) => plan.intentChanges),
1996
+ actions: plans.flatMap((plan) => plan.actions),
1997
+ changes: plans.flatMap((plan) => plan.changes),
1998
+ warnings: plans.flatMap((plan) => plan.warnings),
1999
+ evidence: {
2000
+ catalogDigests: plans.flatMap((plan) => plan.evidence.catalogDigests),
2001
+ intentDigests: plans.flatMap((plan) => plan.evidence.intentDigests),
2002
+ configDigests: plans.flatMap((plan) => plan.evidence.configDigests),
2003
+ targetOwnerFingerprints: plans.flatMap((plan) => plan.evidence.targetOwnerFingerprints),
2004
+ },
2005
+ });
2006
+ }
2007
+ async function unavailableRemovedCleanupWarnings(options) {
2008
+ const warnings = [];
2009
+ for (const pluginId of options.pluginIds) {
2010
+ const parsed = parsePluginId(pluginId);
2011
+ if (!parsed.sourceId)
2012
+ continue;
2013
+ try {
2014
+ await resolveActiveCatalogGeneration({ sourceId: parsed.sourceId, env: options.env });
2015
+ continue;
2016
+ }
2017
+ catch {
2018
+ // Only a healthy Active generation can prove that a plugin entry was deleted.
2019
+ }
2020
+ let hasConfig = false;
2021
+ for (const scope of ['user', 'project']) {
2022
+ try {
2023
+ hasConfig ||= (await inspectPluginConfigResetTarget({
2024
+ sourceId: parsed.sourceId,
2025
+ pluginName: parsed.pluginName,
2026
+ scope,
2027
+ cwd: options.cwd,
2028
+ env: options.env,
2029
+ })).exists;
2030
+ }
2031
+ catch {
2032
+ hasConfig = true;
2033
+ }
2034
+ }
2035
+ if (!hasConfig && !options.inventory.references.some((reference) => reference.sourceId === parsed.sourceId && reference.pluginName === parsed.pluginName)) {
2036
+ continue;
2037
+ }
2038
+ warnings.push(`removedPluginCleanupNotPerformed: ${pluginId} has no healthy local Active source, so whole-plugin cleanup was not performed and saved settings may remain. Restore the source, run aw catalog source sync ${parsed.sourceId}, then retry ordinary cleanup.`);
2039
+ }
2040
+ return warnings;
2041
+ }
2042
+ async function executeRemovedPluginCleanup(options) {
2043
+ const results = [];
2044
+ const warnings = [...options.operationPlan.warnings];
2045
+ const targetIdentity = (target, scope = 'user') => ({
2046
+ pluginId: target.pluginId,
2047
+ pluginName: target.pluginName,
2048
+ sourceId: target.sourceId,
2049
+ sourceIdentity: target.sourceIdentity,
2050
+ pluginVersion: '0.0.0',
2051
+ platform: 'codex',
2052
+ scope,
2053
+ cwd: options.cwd,
2054
+ });
2055
+ let intentChanged = false;
2056
+ for (const scope of ['user', 'project']) {
2057
+ if (calculatePluginIntentDigest(options.intents[scope].before)
2058
+ === calculatePluginIntentDigest(options.intents[scope].after))
2059
+ continue;
2060
+ try {
2061
+ await (options.runtime?.writePluginIntentState ?? writePluginIntentState)({
2062
+ scope, cwd: options.cwd, env: options.env,
2063
+ }, options.intents[scope].after);
2064
+ intentChanged = true;
2065
+ for (const target of options.targets) {
2066
+ if (!options.intents[scope].before.roots.some((root) => root.pluginId === target.pluginId)
2067
+ && !options.intents[scope].before.dependencyScopeOverrides.some((override) => override.pluginId === target.pluginId))
2068
+ continue;
2069
+ const result = cleanupIntentResult({
2070
+ pluginId: target.pluginId,
2071
+ pluginName: target.pluginName,
2072
+ sourceId: target.sourceId,
2073
+ sourceIdentity: target.sourceIdentity,
2074
+ platform: 'codex',
2075
+ scope,
2076
+ });
2077
+ result.removed.push(`pluginIntent:${scope}`);
2078
+ results.push(result);
2079
+ }
2080
+ }
2081
+ catch (error) {
2082
+ const message = `removedPluginIntentWriteFailed: ${scope}: ${error instanceof Error ? error.message : String(error)}. Retry cleanup.`;
2083
+ results.push(...options.targets.map((target) => failedResult('cleanup', targetIdentity(target, scope), message)));
2084
+ return {
2085
+ platforms: ['codex', 'claude'],
2086
+ plugins: [],
2087
+ warnings: [...warnings, message],
2088
+ rebuildResults: results,
2089
+ targetResults: results.map(toRemovedCleanupTargetResult),
2090
+ overallState: intentChanged ? 'partial' : 'failed',
2091
+ receiptTargets: results.map((result) => result.target),
2092
+ removedPluginCleanupIds: options.targets.map((target) => target.pluginId),
2093
+ operationPlan: options.operationPlan,
2094
+ };
2095
+ }
2096
+ }
2097
+ let codexManager = options.runtime?.codexManager;
2098
+ if (!codexManager && options.targets.some((target) => target.nativeState === 'installed')) {
2099
+ try {
2100
+ if (options.runtime?.codexManagerResolver) {
2101
+ codexManager = await options.runtime.codexManagerResolver({
2102
+ cwd: options.cwd,
2103
+ env: options.env,
2104
+ runner: options.runtime.nativeCliRunner ?? getDefaultNativeCliRunner(),
2105
+ authorizer: options.runtime.managedCodexAuthorizer,
2106
+ operationRequirement: getCodexOperationRequirement('project'),
2107
+ });
2108
+ }
2109
+ else {
2110
+ const consumer = await (options.runtime?.codexOperationConsumerResolver
2111
+ ?? resolveCodexOperationConsumer)({
2112
+ cwd: options.cwd,
2113
+ env: options.env,
2114
+ runner: options.runtime?.nativeCliRunner ?? getDefaultNativeCliRunner(),
2115
+ authorizer: options.runtime?.managedCodexAuthorizer,
2116
+ operationRequirement: getCodexOperationRequirement('project'),
2117
+ requiresNativeMaterialization: true,
2118
+ });
2119
+ if (consumer.support !== 'observed') {
2120
+ throw new Error('Codex native removal requires an observed official CLI consumer.');
2121
+ }
2122
+ codexManager = consumer.manager;
2123
+ }
2124
+ }
2125
+ catch (error) {
2126
+ warnings.push(`removedPluginCodexManagerUnavailable: ${error instanceof Error ? error.message : String(error)}`);
2127
+ }
2128
+ }
2129
+ const ownedTargets = options.targets.flatMap((target) => target.evidence
2130
+ .filter((entry) => entry.plan.actions.length > 0)
2131
+ .map((entry) => ({
2132
+ pluginId: target.pluginId,
2133
+ pluginName: target.pluginName,
2134
+ sourceId: target.sourceId,
2135
+ sourceIdentity: target.sourceIdentity,
2136
+ platform: entry.platform,
2137
+ scope: entry.scope,
2138
+ ...(target.marketplaceName ? { marketplaceName: target.marketplaceName } : {}),
2139
+ nativeRegistration: entry.plan.actions.some((action) => action.action === 'removeNativeRegistration'),
2140
+ })));
2141
+ if (ownedTargets.length > 0) {
2142
+ const owned = await cleanupOfflineOwnedComponents({
2143
+ targets: ownedTargets,
2144
+ evidence: options.targets.flatMap((target) => target.evidence),
2145
+ inventoryErrors: options.inventory.errors.filter((error) => !error.kind.startsWith('config')),
2146
+ cwd: options.cwd,
2147
+ env: options.env,
2148
+ nativeCliRunner: options.runtime?.nativeCliRunner,
2149
+ ...(codexManager ? { codexManager } : {}),
2150
+ executeCleanupOperation: options.runtime?.executeCleanupOperation,
2151
+ removeObsoleteInstallationContracts: async () => undefined,
2152
+ removedPluginWhole: true,
2153
+ });
2154
+ results.push(...owned.rebuildResults);
2155
+ warnings.push(...owned.warnings);
2156
+ }
2157
+ for (const target of options.targets) {
2158
+ for (const config of target.configTargets.filter((entry) => entry.kind !== 'absent')) {
2159
+ if (config.kind === 'directory' || config.kind === 'special'
2160
+ || target.blockers.some((blocker) => blocker.startsWith(`${config.scope}: removedPluginConfigIdentityConflict`))) {
2161
+ continue;
2162
+ }
2163
+ try {
2164
+ const receipt = await resetPluginConfigStore({
2165
+ sourceId: target.sourceId,
2166
+ sourceIdentity: target.sourceIdentity,
2167
+ pluginName: target.pluginName,
2168
+ scope: config.scope,
2169
+ cwd: options.cwd,
2170
+ env: options.env,
2171
+ });
2172
+ const result = cleanupIntentResult({
2173
+ pluginId: target.pluginId,
2174
+ pluginName: target.pluginName,
2175
+ sourceId: target.sourceId,
2176
+ sourceIdentity: target.sourceIdentity,
2177
+ platform: 'codex',
2178
+ scope: config.scope,
2179
+ });
2180
+ if (receipt.deleted)
2181
+ result.removed.push(`${config.scope}/plugin-config/${target.sourceId}/${target.pluginName}`);
2182
+ results.push(result);
2183
+ }
2184
+ catch (error) {
2185
+ results.push(failedResult('cleanup', targetIdentity(target, config.scope), `removedPluginConfigResetFailed: ${error instanceof Error ? error.message : String(error)}. Retry cleanup.`, 'cleanup.owned'));
2186
+ }
2187
+ }
2188
+ let nativeAbsent = target.nativeState === 'absent';
2189
+ if (target.marketplaceName && target.nativeState !== 'unbound') {
2190
+ try {
2191
+ const observed = await inspectCodexPluginRegistrationLive({
2192
+ pluginId: `${target.pluginName}@${target.marketplaceName}`,
2193
+ cwd: options.cwd,
2194
+ env: options.env,
2195
+ nativeCliRunner: options.runtime?.nativeCliRunner ?? getDefaultNativeCliRunner(),
2196
+ ...(codexManager ? { manager: codexManager } : {}),
2197
+ scope: 'both',
2198
+ });
2199
+ nativeAbsent = observed.observation.state === 'observed'
2200
+ && observed.observation.value === 'absent';
2201
+ }
2202
+ catch {
2203
+ nativeAbsent = false;
2204
+ }
2205
+ }
2206
+ if (!nativeAbsent && (target.nativeState === 'installed' || target.nativeState === 'unknown')) {
2207
+ results.push(failedResult('cleanup', targetIdentity(target), 'removedPluginNativeResidual: official Codex list did not confirm absent. Retry ordinary cleanup.', 'cleanup.owned'));
2208
+ }
2209
+ if (nativeAbsent && target.marketplaceName && target.nativeOwnershipVerified) {
2210
+ for (const activation of target.activationScopes) {
2211
+ try {
2212
+ await removeCodexPluginActivation({
2213
+ configPath: resolveCodexPaths(options.cwd, activation.scope, options.env).configTomlPath,
2214
+ codexPluginId: `${target.pluginName}@${target.marketplaceName}`,
2215
+ });
2216
+ const result = cleanupIntentResult({
2217
+ pluginId: target.pluginId,
2218
+ pluginName: target.pluginName,
2219
+ sourceId: target.sourceId,
2220
+ sourceIdentity: target.sourceIdentity,
2221
+ platform: 'codex',
2222
+ scope: activation.scope,
2223
+ });
2224
+ result.removed.push(`codexActivation:${activation.scope}`);
2225
+ results.push(result);
2226
+ }
2227
+ catch (error) {
2228
+ results.push(failedResult('cleanup', targetIdentity(target, activation.scope), `removedPluginActivationFailed: ${error instanceof Error ? error.message : String(error)}`, 'cleanup.owned'));
2229
+ }
2230
+ }
2231
+ }
2232
+ const claudeNativeFailed = results.some((result) => result.target.pluginId === target.pluginId
2233
+ && result.target.platform === 'claude' && result.status === 'failed');
2234
+ if (target.projectionPresent && target.projectionOwnershipVerified
2235
+ && nativeAbsent && !claudeNativeFailed && !target.claudeNativeCleanupBlocked
2236
+ && (target.nativeOwnershipVerified || target.activationScopes.length === 0)) {
2237
+ try {
2238
+ await removeNamespacedMarketplacePluginProjection({
2239
+ rootDir: target.marketplaceRoot,
2240
+ sourceId: target.sourceId,
2241
+ sourceIdentity: target.sourceIdentity,
2242
+ pluginName: target.pluginName,
2243
+ env: options.env,
2244
+ });
2245
+ const result = cleanupIntentResult({
2246
+ pluginId: target.pluginId,
2247
+ pluginName: target.pluginName,
2248
+ sourceId: target.sourceId,
2249
+ sourceIdentity: target.sourceIdentity,
2250
+ platform: 'codex',
2251
+ scope: 'user',
2252
+ });
2253
+ result.removed.push('marketplaceProjection');
2254
+ results.push(result);
2255
+ }
2256
+ catch (error) {
2257
+ results.push(failedResult('cleanup', targetIdentity(target), `removedPluginMarketplaceProjectionFailed: ${error instanceof Error ? error.message : String(error)}`, 'cleanup.owned'));
2258
+ }
2259
+ }
2260
+ for (const blocker of target.blockers) {
2261
+ results.push(failedResult('cleanup', targetIdentity(target), blocker));
2262
+ }
2263
+ }
2264
+ const afterInventory = await options.readInventory({ cwd: options.cwd, env: options.env });
2265
+ for (const target of options.targets) {
2266
+ const after = await inspectRemovedPluginCleanup({
2267
+ pluginId: target.pluginId,
2268
+ inventory: afterInventory,
2269
+ cwd: options.cwd,
2270
+ env: options.env,
2271
+ nativeCliRunner: options.runtime?.nativeCliRunner ?? getDefaultNativeCliRunner(),
2272
+ ...(codexManager ? { codexManager } : {}),
2273
+ });
2274
+ if (after && (after.configTargets.some((config) => config.kind !== 'absent')
2275
+ || after.activationScopes.length > 0
2276
+ || after.projectionPresent
2277
+ || after.nativeState === 'installed'
2278
+ || after.nativeState === 'unknown'
2279
+ || after.evidence.some((entry) => entry.plan.actions.length > 0)
2280
+ || after.blockers.length > 0)) {
2281
+ results.push(failedResult('cleanup', targetIdentity(target), 'removedPluginCleanupResidual: some exact targets remain. Retry ordinary cleanup or repair the reported evidence.', 'cleanup.owned'));
2282
+ }
2283
+ }
2284
+ const hasFailure = results.some((result) => result.status === 'failed');
2285
+ const hasSuccess = intentChanged || results.some((result) => result.status === 'succeeded'
2286
+ && result.removed.length > 0);
2287
+ return {
2288
+ platforms: ['codex', 'claude'],
2289
+ plugins: hasFailure ? [] : options.targets.map((target) => target.pluginName),
2290
+ warnings: [...new Set(warnings)],
2291
+ rebuildResults: results,
2292
+ targetResults: results.map(toRemovedCleanupTargetResult),
2293
+ overallState: hasFailure ? hasSuccess ? 'partial' : 'failed' : 'succeeded',
2294
+ receiptTargets: results.map((result) => result.target),
2295
+ removedPluginCleanupIds: options.targets.map((target) => target.pluginId),
2296
+ operationPlan: options.operationPlan,
2297
+ };
2298
+ }
1539
2299
  async function runRebuildCommandInSession(options) {
1540
2300
  const runtimeEnv = options.runtime?.env ?? process.env;
1541
2301
  const readInventory = options.runtime?.inventoryCatalogSourceReferences
@@ -1681,6 +2441,7 @@ async function runRebuildCommandInSession(options) {
1681
2441
  requestedPluginNames = [...identities];
1682
2442
  }
1683
2443
  let cleanupTargets = [];
2444
+ let cleanupUnavailableWarnings = [];
1684
2445
  let cleanupIntentAfter;
1685
2446
  let cleanupSelectionErrors = [];
1686
2447
  if (options.command === 'cleanup') {
@@ -1748,6 +2509,124 @@ async function runRebuildCommandInSession(options) {
1748
2509
  return mergeRebuildCommandResults([]);
1749
2510
  }
1750
2511
  if (options.command === 'cleanup') {
2512
+ if (!cleanupForce) {
2513
+ const exactIds = [...new Set(requestedPluginNames.flatMap((selector) => {
2514
+ const parsed = parsePluginId(selector);
2515
+ return parsed.sourceId ? [formatPluginId(parsed)] : [];
2516
+ }))];
2517
+ const projectSkills = await selectedProjectCodexSkillReferences({
2518
+ cwd: options.commandOptions.cwd,
2519
+ });
2520
+ const fullInventory = {
2521
+ ...cleanupInventory,
2522
+ references: [...new Map([...cleanupInventory.references, ...projectSkills].map((reference) => [[
2523
+ reference.kind,
2524
+ reference.location,
2525
+ reference.sourceId ?? '',
2526
+ reference.pluginName ?? '',
2527
+ reference.platform ?? '',
2528
+ reference.scope ?? '',
2529
+ ].join('\0'), reference])).values()],
2530
+ };
2531
+ const removed = (await Promise.all(exactIds.map((pluginId) => inspectRemovedPluginCleanup({
2532
+ pluginId,
2533
+ inventory: fullInventory,
2534
+ cwd: options.commandOptions.cwd,
2535
+ env: runtimeEnv,
2536
+ nativeCliRunner: options.runtime?.nativeCliRunner ?? getDefaultNativeCliRunner(),
2537
+ codexManager: options.runtime?.codexManager,
2538
+ })))).filter((target) => target !== null);
2539
+ cleanupUnavailableWarnings = await unavailableRemovedCleanupWarnings({
2540
+ pluginIds: exactIds.filter((pluginId) => !removed.some((target) => target.pluginId === pluginId)),
2541
+ inventory: fullInventory,
2542
+ cwd: options.commandOptions.cwd,
2543
+ env: runtimeEnv,
2544
+ });
2545
+ if (removed.length > 0) {
2546
+ const removedIds = new Set(removed.map((target) => target.pluginId));
2547
+ const intents = {
2548
+ user: {
2549
+ before: await readPluginIntentState({
2550
+ scope: 'user', cwd: options.commandOptions.cwd, env: runtimeEnv,
2551
+ }),
2552
+ after: createEmptyPluginIntentState(),
2553
+ },
2554
+ project: {
2555
+ before: await readPluginIntentState({
2556
+ scope: 'project', cwd: options.commandOptions.cwd, env: runtimeEnv,
2557
+ }),
2558
+ after: createEmptyPluginIntentState(),
2559
+ },
2560
+ };
2561
+ intents.user.after = removeRemovedPluginIntent(intents.user.before, removedIds);
2562
+ intents.project.after = removeRemovedPluginIntent(intents.project.before, removedIds);
2563
+ const removedPlan = createRemovedPluginCleanupOperationPlan({
2564
+ targets: removed,
2565
+ intents,
2566
+ cwd: options.commandOptions.cwd,
2567
+ env: runtimeEnv,
2568
+ });
2569
+ const remaining = exactIds.filter((pluginId) => !removedIds.has(pluginId));
2570
+ const normalOptions = {
2571
+ ...options,
2572
+ all: false,
2573
+ pluginNames: remaining,
2574
+ runtime: {
2575
+ ...options.runtime,
2576
+ expectedPlanFingerprint: undefined,
2577
+ expectedOfflineCleanupEvidence: undefined,
2578
+ },
2579
+ };
2580
+ const normalPreview = remaining.length > 0
2581
+ ? await runRebuildCommandInSession({
2582
+ ...normalOptions,
2583
+ runtime: { ...normalOptions.runtime, previewOperationPlan: true },
2584
+ })
2585
+ : undefined;
2586
+ const operationPlan = normalPreview?.operationPlan
2587
+ ? combineCleanupOperationPlans([removedPlan, normalPreview.operationPlan])
2588
+ : removedPlan;
2589
+ if (options.runtime?.expectedPlanFingerprint) {
2590
+ assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, operationPlan);
2591
+ }
2592
+ if (options.runtime?.previewOperationPlan) {
2593
+ const preview = normalPreview ?? mergeRebuildCommandResults([]);
2594
+ preview.removedPluginCleanupIds = [...removedIds];
2595
+ preview.operationPlan = operationPlan;
2596
+ preview.warnings = [...new Set([...preview.warnings, ...removedPlan.warnings])];
2597
+ return preview;
2598
+ }
2599
+ if (options.commandOptions.yes !== true
2600
+ && !(options.runtime?.preApplyConfirmed === true
2601
+ && options.runtime.expectedPlanFingerprint !== undefined)) {
2602
+ throw new Error('removedPluginCleanupAuthorizationRequired: deleted-plugin cleanup covers Codex and Claude in user and current-project scopes, removes saved settings and the shared Codex native package, and may affect other projects. Use --yes or accept the full Review.');
2603
+ }
2604
+ const removedResult = await executeRemovedPluginCleanup({
2605
+ targets: removed,
2606
+ intents,
2607
+ inventory: fullInventory,
2608
+ cwd: options.commandOptions.cwd,
2609
+ env: runtimeEnv,
2610
+ runtime: options.runtime,
2611
+ readInventory,
2612
+ operationPlan: removedPlan,
2613
+ });
2614
+ const normalResult = remaining.length > 0
2615
+ ? await runRebuildCommandInSession(normalOptions)
2616
+ : undefined;
2617
+ const combined = normalResult
2618
+ ? mergeRebuildCommandResults([removedResult, normalResult])
2619
+ : removedResult;
2620
+ combined.operationPlan = operationPlan;
2621
+ combined.removedPluginCleanupIds = [...removedIds];
2622
+ combined.warnings = [...new Set([...combined.warnings, ...operationPlan.warnings])];
2623
+ combined.overallState = [removedResult, normalResult].filter(Boolean).every((result) => result?.overallState === 'succeeded')
2624
+ ? 'succeeded'
2625
+ : [removedResult, normalResult].filter(Boolean).some((result) => result?.overallState === 'succeeded' || result?.overallState === 'partial')
2626
+ ? 'partial' : 'failed';
2627
+ return combined;
2628
+ }
2629
+ }
1751
2630
  if (!cleanupForce
1752
2631
  && cleanupTargets.length === 0
1753
2632
  && cleanupSelectionErrors.length === 0
@@ -1757,11 +2636,12 @@ async function runRebuildCommandInSession(options) {
1757
2636
  scope: options.commandOptions.scope,
1758
2637
  before: cleanupIntentBefore,
1759
2638
  after: cleanupIntentAfter,
1760
- }, cleanupInventoryWarnings(cleanupInventory, []));
2639
+ }, [...cleanupInventoryWarnings(cleanupInventory, []), ...cleanupUnavailableWarnings]);
1761
2640
  if (options.runtime?.expectedPlanFingerprint) {
1762
2641
  assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, operationPlan);
1763
2642
  }
1764
2643
  result.operationPlan = operationPlan;
2644
+ result.warnings.push(...cleanupUnavailableWarnings);
1765
2645
  return result;
1766
2646
  }
1767
2647
  if (cleanupForce) {
@@ -1954,7 +2834,7 @@ async function runRebuildCommandInSession(options) {
1954
2834
  scope: options.commandOptions.scope,
1955
2835
  before: cleanupIntentBefore,
1956
2836
  after: cleanupIntentAfter,
1957
- }, cleanupInventoryWarnings(cleanupInventory, []));
2837
+ }, [...cleanupInventoryWarnings(cleanupInventory, []), ...cleanupUnavailableWarnings]);
1958
2838
  if (options.runtime?.expectedPlanFingerprint) {
1959
2839
  assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, operationPlan);
1960
2840
  }
@@ -1964,6 +2844,7 @@ async function runRebuildCommandInSession(options) {
1964
2844
  : 'succeeded';
1965
2845
  result.warnings.push(...cleanupSelectionErrors.map((error) => `stateInventoryResidual: ${error.kind}: ${error.message}`));
1966
2846
  result.warnings.push(...legacyLiveTargets.map((target) => `legacyStateUnsupported: legacy live runtime state exists for ${target.pluginName}:${target.platform}/${target.scope}.`));
2847
+ result.warnings.push(...cleanupUnavailableWarnings);
1967
2848
  if (malformedSelectedConfigPreserved) {
1968
2849
  result.warnings.push(MALFORMED_CONFIG_PRESERVED_WARNING);
1969
2850
  }
@@ -1994,7 +2875,7 @@ async function runRebuildCommandInSession(options) {
1994
2875
  scope: options.commandOptions.scope,
1995
2876
  before: cleanupIntentBefore,
1996
2877
  after: cleanupIntentAfter,
1997
- }, cleanupInventoryWarnings(cleanupInventory, cleanupTargets));
2878
+ }, [...cleanupInventoryWarnings(cleanupInventory, cleanupTargets), ...cleanupUnavailableWarnings]);
1998
2879
  if (options.runtime?.expectedPlanFingerprint) {
1999
2880
  assertOperationPlanFingerprint(options.runtime.expectedPlanFingerprint, operationPlan);
2000
2881
  }
@@ -2055,6 +2936,7 @@ async function runRebuildCommandInSession(options) {
2055
2936
  if (malformedSelectedConfigPreserved) {
2056
2937
  cleanupResult.warnings.push(MALFORMED_CONFIG_PRESERVED_WARNING);
2057
2938
  }
2939
+ cleanupResult.warnings.push(...cleanupUnavailableWarnings);
2058
2940
  return cleanupResult;
2059
2941
  }
2060
2942
  try {