@lesliechan721/aw 0.1.0-beta.35 → 0.1.0-beta.37
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/operation-plan.d.ts +19 -0
- package/dist/src/commands/operation-plan.js.map +1 -1
- package/dist/src/commands/rebuild-command.d.ts +1 -0
- package/dist/src/commands/rebuild-command.js +890 -8
- package/dist/src/commands/rebuild-command.js.map +1 -1
- package/dist/src/commands/rebuild-integration.d.ts +12 -0
- package/dist/src/commands/rebuild-integration.js +88 -2
- package/dist/src/commands/rebuild-integration.js.map +1 -1
- package/dist/src/commands/status-v6.js +109 -19
- package/dist/src/commands/status-v6.js.map +1 -1
- package/dist/src/commands/wizard.d.ts +5 -1
- package/dist/src/commands/wizard.js +4 -2
- package/dist/src/commands/wizard.js.map +1 -1
- package/dist/src/commands/write-commands.js +3 -0
- package/dist/src/commands/write-commands.js.map +1 -1
- package/dist/src/index.js +57 -4
- package/dist/src/index.js.map +1 -1
- package/dist/src/tui/components.d.ts +20 -1
- package/dist/src/tui/components.js +41 -20
- package/dist/src/tui/components.js.map +1 -1
- package/dist/src/tui/console-app-v4.js +1323 -1165
- package/dist/src/tui/console-app-v4.js.map +1 -1
- package/dist/src/tui/content-item-card.d.ts +13 -0
- package/dist/src/tui/content-item-card.js +51 -0
- package/dist/src/tui/content-item-card.js.map +1 -0
- package/dist/src/tui/controller-runtime.js +18 -2
- package/dist/src/tui/controller-runtime.js.map +1 -1
- package/dist/src/tui/controller.d.ts +3 -0
- package/dist/src/tui/controller.js +57 -8
- package/dist/src/tui/controller.js.map +1 -1
- package/dist/src/tui/dependency-graph-view.d.ts +39 -0
- package/dist/src/tui/dependency-graph-view.js +121 -0
- package/dist/src/tui/dependency-graph-view.js.map +1 -0
- package/dist/src/tui/dependency-graph.js +16 -19
- package/dist/src/tui/dependency-graph.js.map +1 -1
- package/dist/src/tui/entity-cards.d.ts +55 -0
- package/dist/src/tui/entity-cards.js +220 -0
- package/dist/src/tui/entity-cards.js.map +1 -0
- package/dist/src/tui/input-display.js +10 -1
- package/dist/src/tui/input-display.js.map +1 -1
- package/dist/src/tui/model.d.ts +2 -0
- package/dist/src/tui/model.js +10 -2
- package/dist/src/tui/model.js.map +1 -1
- package/dist/src/tui/operation-diagnostic-screens.d.ts +2 -18
- package/dist/src/tui/operation-diagnostic-screens.js +16 -69
- package/dist/src/tui/operation-diagnostic-screens.js.map +1 -1
- package/dist/src/tui/operation-review-planner.js +44 -3
- package/dist/src/tui/operation-review-planner.js.map +1 -1
- package/dist/src/tui/plugin-content.d.ts +8 -0
- package/dist/src/tui/plugin-content.js +409 -65
- package/dist/src/tui/plugin-content.js.map +1 -1
- package/dist/src/tui/plugin-detail.d.ts +52 -0
- package/dist/src/tui/plugin-detail.js +165 -0
- package/dist/src/tui/plugin-detail.js.map +1 -0
- package/dist/src/tui/query-header.d.ts +30 -0
- package/dist/src/tui/query-header.js +40 -0
- package/dist/src/tui/query-header.js.map +1 -0
- package/dist/src/tui/reader.d.ts +37 -0
- package/dist/src/tui/reader.js +47 -0
- package/dist/src/tui/reader.js.map +1 -0
- package/dist/src/tui/router.d.ts +10 -0
- package/dist/src/tui/router.js.map +1 -1
- package/dist/src/tui/screen-registry.d.ts +2 -0
- package/dist/src/tui/source-detail-card.d.ts +29 -0
- package/dist/src/tui/source-detail-card.js +45 -0
- package/dist/src/tui/source-detail-card.js.map +1 -0
- package/dist/src/tui/wizard.js +26 -4
- package/dist/src/tui/wizard.js.map +1 -1
- package/dist/src/utils/local-marketplace.d.ts +1 -0
- package/dist/src/utils/local-marketplace.js +22 -0
- package/dist/src/utils/local-marketplace.js.map +1 -1
- 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 {
|