@lesliechan721/aw 0.1.0-beta.11 → 0.1.0-beta.12

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 (52) hide show
  1. package/dist/authoring/scripts/validate-plugin.mjs +33 -42
  2. package/dist/authoring/scripts/validate-plugin.mjs.map +1 -1
  3. package/dist/src/commands/operation-planner.js +11 -1
  4. package/dist/src/commands/operation-planner.js.map +1 -1
  5. package/dist/src/commands/rebuild-command.d.ts +12 -0
  6. package/dist/src/commands/rebuild-command.js +72 -17
  7. package/dist/src/commands/rebuild-command.js.map +1 -1
  8. package/dist/src/commands/rebuild-integration.d.ts +1 -0
  9. package/dist/src/commands/rebuild-integration.js +138 -41
  10. package/dist/src/commands/rebuild-integration.js.map +1 -1
  11. package/dist/src/commands/status-v6.js +6 -6
  12. package/dist/src/commands/status-v6.js.map +1 -1
  13. package/dist/src/index.js +52 -14
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/portable/materializer.js +5 -3
  16. package/dist/src/portable/materializer.js.map +1 -1
  17. package/dist/src/tui/console-app-v4.js +1 -21
  18. package/dist/src/tui/console-app-v4.js.map +1 -1
  19. package/dist/src/tui/controller-runtime.js +165 -48
  20. package/dist/src/tui/controller-runtime.js.map +1 -1
  21. package/dist/src/tui/controller.d.ts +4 -4
  22. package/dist/src/tui/controller.js +19 -7
  23. package/dist/src/tui/controller.js.map +1 -1
  24. package/dist/src/tui/diagnostics.js +6 -6
  25. package/dist/src/tui/diagnostics.js.map +1 -1
  26. package/dist/src/tui/operation-review-planner.js +66 -19
  27. package/dist/src/tui/operation-review-planner.js.map +1 -1
  28. package/dist/src/utils/artifact-plan.d.ts +2 -2
  29. package/dist/src/utils/artifact-plan.js +8 -6
  30. package/dist/src/utils/artifact-plan.js.map +1 -1
  31. package/dist/src/utils/artifact-renderer.js +96 -77
  32. package/dist/src/utils/artifact-renderer.js.map +1 -1
  33. package/dist/src/utils/artifact-request.d.ts +5 -0
  34. package/dist/src/utils/artifact-request.js +40 -0
  35. package/dist/src/utils/artifact-request.js.map +1 -1
  36. package/dist/src/utils/plugin-config-impact.d.ts +33 -0
  37. package/dist/src/utils/plugin-config-impact.js +298 -0
  38. package/dist/src/utils/plugin-config-impact.js.map +1 -0
  39. package/dist/src/utils/plugin-config-write.d.ts +22 -2
  40. package/dist/src/utils/plugin-config-write.js +368 -104
  41. package/dist/src/utils/plugin-config-write.js.map +1 -1
  42. package/dist/src/utils/plugin-config.d.ts +10 -0
  43. package/dist/src/utils/plugin-config.js +39 -22
  44. package/dist/src/utils/plugin-config.js.map +1 -1
  45. package/dist/src/utils/plugin-scripts.js +47 -39
  46. package/dist/src/utils/plugin-scripts.js.map +1 -1
  47. package/dist/src/utils/project-boundary.js +22 -9
  48. package/dist/src/utils/project-boundary.js.map +1 -1
  49. package/dist/src/utils/sensitive-content.d.ts +3 -0
  50. package/dist/src/utils/sensitive-content.js +28 -7
  51. package/dist/src/utils/sensitive-content.js.map +1 -1
  52. package/package.json +3 -3
@@ -1,9 +1,9 @@
1
- import { stablePluginConfigStringify } from '@lesliechan721/aw-plugin-core/manifest';
2
1
  import { statusCommandV7, statusTargetIntent, } from '../commands/status-v6.js';
3
2
  import { syncWriteCommand } from '../commands/write-commands.js';
4
3
  import { CONFIG_MUTATION_RECEIPT_SCHEMA_VERSION, } from '../types.js';
5
4
  import { getDefaultNativeCliRunner } from './native-cli.js';
6
5
  import { assertReadyPluginConfigStore, assertSafePluginConfigSegment, codexNativeRefreshKeys, coercePluginConfigUpdates, patchPluginConfigStore, readPluginConfigStore, readNamespacedPluginConfigStoreIdentity, resetPluginConfigStore, resolveConfigRead, resolveEffectivePluginConfig, resolvePluginConfigStorePath, resolvePluginConfigSchemaAccess, summarizeEffectivePluginConfig, } from './plugin-config.js';
6
+ import { analyzePluginConfigMutationImpact } from './plugin-config-impact.js';
7
7
  import { collectSensitivePluginConfigValues, redactPluginScriptOutput, } from './plugin-scripts.js';
8
8
  import { resolveClaudeProjectKey, resolveCodexProjectKey } from './paths.js';
9
9
  import { withAwHomeMutationLock, } from './aw-home-mutation-lock.js';
@@ -213,6 +213,7 @@ async function reinstallVisibleTargets(options) {
213
213
  : Array.isArray(entry) && /^(?:messages|warnings)$/u.test(key)),
214
214
  ])));
215
215
  };
216
+ const preApplyExecutionCache = new Map();
216
217
  for (const target of discovered.targets) {
217
218
  try {
218
219
  const effective = await resolveEffectivePluginConfig({
@@ -263,7 +264,6 @@ async function reinstallVisibleTargets(options) {
263
264
  cwd: options.cwd,
264
265
  platform: target.platform,
265
266
  scope: target.scope,
266
- yes: true,
267
267
  ...(options.rebuildRuntime?.scriptTimeoutMs === undefined
268
268
  ? {}
269
269
  : { scriptTimeoutMs: options.rebuildRuntime.scriptTimeoutMs }),
@@ -271,6 +271,8 @@ async function reinstallVisibleTargets(options) {
271
271
  ...options.rebuildRuntime,
272
272
  env: options.env,
273
273
  nativeCliRunner,
274
+ preApplyConfirmed: options.preApplyConfirmed === true,
275
+ preApplyExecutionCache,
274
276
  ...(debugLogger ? {
275
277
  debugLogger: {
276
278
  ...debugLogger,
@@ -368,11 +370,16 @@ async function configApplyHasValidCodexTarget(options) {
368
370
  }
369
371
  async function applyConfigToVisibleTargets(options) {
370
372
  let discovered;
371
- try {
372
- discovered = await resolveConfigApplyTargets(options);
373
+ if (options.discovered) {
374
+ discovered = options.discovered;
373
375
  }
374
- catch {
375
- return unavailableApply('targetDiscoveryFailed', 'targetDiscovery');
376
+ else {
377
+ try {
378
+ discovered = await resolveConfigApplyTargets(options);
379
+ }
380
+ catch {
381
+ return unavailableApply('targetDiscoveryFailed', 'targetDiscovery');
382
+ }
376
383
  }
377
384
  if (discovered.targets.length === 0
378
385
  && discovered.failures.length === 0
@@ -381,6 +388,113 @@ async function applyConfigToVisibleTargets(options) {
381
388
  }
382
389
  return reinstallVisibleTargets({ ...options, discovered });
383
390
  }
391
+ export async function resolveConfigPreApplyAuthorization(options) {
392
+ const applyTargets = dedupeTargets(options.applyTargets);
393
+ if (applyTargets.length === 0 || !pluginMayRunPreApply(options.plugin)) {
394
+ return { targets: [], availableApplyTargets: applyTargets, unavailableTargetResults: [] };
395
+ }
396
+ if (options.plugin.sourceId && !options.plugin.pluginId) {
397
+ return {
398
+ targets: [],
399
+ availableApplyTargets: [],
400
+ unavailableTargetResults: applyTargets.map((target) => ({
401
+ target,
402
+ action: 'reinstall',
403
+ state: 'failed',
404
+ errorCode: 'sourceUnavailable',
405
+ message: 'The preApply authorization scope cannot be resolved for this target.',
406
+ })),
407
+ };
408
+ }
409
+ const selector = options.plugin.sourceId
410
+ ? options.plugin.pluginId
411
+ : options.plugin.manifest.name;
412
+ const groups = new Map();
413
+ for (const target of applyTargets) {
414
+ const targets = groups.get(target.scope) ?? [];
415
+ targets.push(target);
416
+ groups.set(target.scope, targets);
417
+ }
418
+ const authorization = [];
419
+ const availableApplyTargets = [];
420
+ const unavailableTargetResults = [];
421
+ const resolveGroup = async (targets) => {
422
+ const platforms = new Set(targets.map((target) => target.platform));
423
+ const platform = platforms.size === 2 ? 'both' : [...platforms][0];
424
+ try {
425
+ const preview = await (options.reinstallCommandImpl ?? syncWriteCommand)([selector], {
426
+ cwd: options.cwd,
427
+ platform,
428
+ scope: targets[0].scope,
429
+ }, {
430
+ ...options.rebuildRuntime,
431
+ env: options.env ?? process.env,
432
+ nativeCliRunner: options.nativeCliRunner,
433
+ resolvePreApplyAuthorization: true,
434
+ });
435
+ if (preview.overallState !== 'succeeded' || !preview.preApplyAuthorization)
436
+ return false;
437
+ authorization.push(...preview.preApplyAuthorization);
438
+ availableApplyTargets.push(...targets);
439
+ return true;
440
+ }
441
+ catch {
442
+ return false;
443
+ }
444
+ };
445
+ for (const targets of groups.values()) {
446
+ if (await resolveGroup(targets))
447
+ continue;
448
+ for (const target of targets) {
449
+ if (targets.length > 1 && await resolveGroup([target]))
450
+ continue;
451
+ unavailableTargetResults.push({
452
+ target,
453
+ action: 'reinstall',
454
+ state: 'failed',
455
+ errorCode: 'sourceUnavailable',
456
+ message: 'The preApply authorization scope cannot be resolved for this target.',
457
+ });
458
+ }
459
+ }
460
+ return {
461
+ targets: [...new Map(authorization.map((target) => [[
462
+ target.pluginId,
463
+ target.platform,
464
+ target.scope,
465
+ ].join('\0'), target])).values()],
466
+ availableApplyTargets: dedupeTargets(availableApplyTargets),
467
+ unavailableTargetResults,
468
+ };
469
+ }
470
+ function pluginMayRunPreApply(plugin) {
471
+ return plugin.manifest.scripts?.preApply !== undefined
472
+ || (plugin.manifest.dependencies?.length ?? 0) > 0;
473
+ }
474
+ function throwPreApplyAuthorizationRequired(targets) {
475
+ const plugins = [...new Set(targets.map((target) => target.pluginId))].join(', ');
476
+ const error = new Error(`preApplyAuthorizationRequired: Applying this config runs plugin preApply for ${plugins}. Re-run with --yes.`);
477
+ error.code = 'preApplyAuthorizationRequired';
478
+ throw error;
479
+ }
480
+ function preApplyAuthorizationIsConfirmed(options) {
481
+ if (options.confirmed !== true)
482
+ return false;
483
+ if (options.authorizedTargets === undefined)
484
+ return true;
485
+ const authorized = new Set(options.authorizedTargets.map((target) => [
486
+ target.pluginId,
487
+ target.sourceIdentity,
488
+ target.platform,
489
+ target.scope,
490
+ ].join('\0')));
491
+ return options.required.every((target) => authorized.has([
492
+ target.pluginId,
493
+ target.sourceIdentity,
494
+ target.platform,
495
+ target.scope,
496
+ ].join('\0')));
497
+ }
384
498
  function otherProjectsWarning(scope) {
385
499
  return scope === 'user'
386
500
  ? ['其它项目不会由本次命令扫描或更新,可能需要在对应项目目录中运行 aw sync。']
@@ -417,10 +531,62 @@ export async function replayConfigRuntimeWrites(options) {
417
531
  });
418
532
  }
419
533
  catch {
534
+ const apply = unavailableApply('targetDiscoveryFailed', 'targetDiscovery');
420
535
  return {
421
536
  configWrite: 'succeeded',
422
537
  overallState: 'partial',
423
- apply: publicConfigApplyResult(unavailableApply('targetDiscoveryFailed', 'targetDiscovery')),
538
+ apply: publicConfigApplyResult(apply),
539
+ otherProjectsMayRequireSync: false,
540
+ };
541
+ }
542
+ const preApplyAuthorization = await resolveConfigPreApplyAuthorization({
543
+ plugin: options.plugin,
544
+ scope: options.scope,
545
+ cwd: options.cwd,
546
+ env,
547
+ applyTargets: discovered.targets,
548
+ nativeCliRunner: options.nativeCliRunner,
549
+ rebuildRuntime,
550
+ reinstallCommandImpl: options.reinstallCommandImpl,
551
+ });
552
+ if (preApplyAuthorization.targets.length > 0 && !preApplyAuthorizationIsConfirmed({
553
+ required: preApplyAuthorization.targets,
554
+ confirmed: options.preApplyConfirmed,
555
+ authorizedTargets: options.preApplyAuthorizedTargets,
556
+ })) {
557
+ const apply = {
558
+ state: 'partial',
559
+ targetResults: preApplyAuthorization.targets.map((target) => ({
560
+ target: {
561
+ pluginName: target.pluginName,
562
+ pluginId: target.pluginId,
563
+ platform: target.platform,
564
+ scope: target.scope,
565
+ },
566
+ action: 'reinstall',
567
+ state: 'failed',
568
+ errorCode: 'preApplyAuthorizationRequired',
569
+ message: 'Running plugin preApply requires explicit authorization.',
570
+ })),
571
+ };
572
+ return {
573
+ configWrite: 'succeeded',
574
+ overallState: 'partial',
575
+ apply: publicConfigApplyResult(apply),
576
+ otherProjectsMayRequireSync: false,
577
+ };
578
+ }
579
+ discovered = {
580
+ targets: preApplyAuthorization.availableApplyTargets,
581
+ failures: [...discovered.failures, ...preApplyAuthorization.unavailableTargetResults],
582
+ };
583
+ if (discovered.targets.length === 0
584
+ && preApplyAuthorization.unavailableTargetResults.length > 0) {
585
+ const apply = unavailableApply('sourceUnavailable', 'source', preApplyAuthorization.unavailableTargetResults);
586
+ return {
587
+ configWrite: 'succeeded',
588
+ overallState: 'partial',
589
+ apply: publicConfigApplyResult(apply),
424
590
  otherProjectsMayRequireSync: false,
425
591
  };
426
592
  }
@@ -445,6 +611,7 @@ export async function replayConfigRuntimeWrites(options) {
445
611
  reinstallCommandImpl: options.reinstallCommandImpl,
446
612
  rebuildRuntime,
447
613
  discovered,
614
+ preApplyConfirmed: options.preApplyConfirmed,
448
615
  });
449
616
  return {
450
617
  configWrite: 'succeeded',
@@ -455,13 +622,6 @@ export async function replayConfigRuntimeWrites(options) {
455
622
  },
456
623
  });
457
624
  }
458
- function changedKeysAffectSharedNativeTarget(options) {
459
- const changedKeys = new Set(options.changedKeys);
460
- return (options.plugin.manifest.artifacts?.targets ?? []).some((target) => target.platform === 'codex'
461
- && target.target === 'codex.skill'
462
- && target.delivery?.mode === 'codex-native'
463
- && target.refreshKeys.some((key) => changedKeys.has(key)));
464
- }
465
625
  function pluginHasSharedNativeRefreshKeys(plugin) {
466
626
  return (plugin.manifest.artifacts?.targets ?? []).some((target) => target.platform === 'codex'
467
627
  && target.target === 'codex.skill'
@@ -529,60 +689,82 @@ async function applyPluginConfigWriteUnlocked(options) {
529
689
  throw error;
530
690
  }
531
691
  }
532
- const updates = await coercePluginConfigUpdates({ schema, updates: requestedUpdates, cwd: options.cwd });
533
- const store = await readPluginConfigStore({
534
- ...configIdentity,
535
- pluginName,
692
+ let discovered;
693
+ let targetDiscoveryFailed = false;
694
+ if (options.apply !== false) {
695
+ try {
696
+ discovered = await resolveConfigApplyTargets({
697
+ plugin: schemaPlugin,
698
+ scope: options.scope,
699
+ cwd: options.cwd,
700
+ env: options.env ?? process.env,
701
+ applyTargets: options.applyTargets,
702
+ nativeCliRunner: options.nativeCliRunner,
703
+ rebuildRuntime: options.rebuildRuntime,
704
+ });
705
+ }
706
+ catch {
707
+ targetDiscoveryFailed = true;
708
+ }
709
+ }
710
+ const impact = await analyzePluginConfigMutationImpact({
711
+ plugin: schemaPlugin,
536
712
  scope: options.scope,
537
713
  cwd: options.cwd,
538
714
  env: options.env,
539
- schema,
715
+ visibleTargets: discovered?.targets ?? [],
716
+ mutation: {
717
+ kind: 'patch',
718
+ updates: requestedUpdates,
719
+ removeKeys: requestedRemoveKeys,
720
+ },
540
721
  });
541
- assertReadyPluginConfigStore(store);
542
- for (const key of requestedRemoveKeys) {
543
- if (!schema[key] && !Object.hasOwn(store.values, key)) {
544
- throw new Error(`Unknown config key "${key}".`);
545
- }
546
- }
722
+ const updates = await coercePluginConfigUpdates({ schema, updates: requestedUpdates, cwd: options.cwd });
547
723
  const warnings = [];
548
- const removedKeys = requestedRemoveKeys.filter((key) => Object.hasOwn(store.values, key));
549
- const staleRemovedKeys = removedKeys.filter((key) => !schema[key]);
550
- const updatedKeys = Object.entries(updates)
551
- .filter(([key, value]) => !Object.hasOwn(store.values, key)
552
- || stablePluginConfigStringify(store.values[key]) !== stablePluginConfigStringify(value))
553
- .map(([key]) => key);
554
- const changedKeys = [...updatedKeys, ...removedKeys];
555
- let applyTargets = options.applyTargets;
556
- const sharedNativeAffected = changedKeysAffectSharedNativeTarget({
557
- plugin: schemaPlugin,
558
- changedKeys,
559
- });
560
- if (options.apply !== false
561
- && options.scope === 'user'
562
- && options.sharedNativeApplyConfirmed !== true
563
- && sharedNativeAffected) {
564
- applyTargets ??= (await discoverVisibleTargets({
565
- plugin: schemaPlugin,
566
- pluginName,
567
- scope: options.scope,
568
- cwd: options.cwd,
569
- env: options.env ?? process.env,
570
- nativeCliRunner: options.nativeCliRunner ?? getDefaultNativeCliRunner(),
571
- rebuildRuntime: options.rebuildRuntime,
572
- })).targets;
573
- if (applyTargets.some((target) => target.platform === 'codex')) {
574
- throwSharedNativeApplyConfirmationRequired();
724
+ const { updatedKeys, removedKeys, staleRemovedKeys } = impact;
725
+ let preApplyUnavailableReason;
726
+ let preApplyUnavailableTargetResults = [];
727
+ let applyDiscovery = discovered
728
+ ? { targets: impact.affectedTargets, failures: discovered.failures }
729
+ : undefined;
730
+ const shouldApply = options.apply !== false && impact.storeChanged && !impact.noEffectiveChange;
731
+ if (shouldApply && pluginMayRunPreApply(schemaPlugin)) {
732
+ if (applyDiscovery) {
733
+ const preApplyAuthorization = await resolveConfigPreApplyAuthorization({
734
+ plugin: schemaPlugin,
735
+ scope: options.scope,
736
+ cwd: options.cwd,
737
+ env: options.env,
738
+ applyTargets: applyDiscovery.targets,
739
+ nativeCliRunner: options.nativeCliRunner,
740
+ rebuildRuntime: options.rebuildRuntime,
741
+ reinstallCommandImpl: options.reinstallCommandImpl,
742
+ });
743
+ if (preApplyAuthorization.targets.length > 0 && !preApplyAuthorizationIsConfirmed({
744
+ required: preApplyAuthorization.targets,
745
+ confirmed: options.preApplyConfirmed,
746
+ authorizedTargets: options.preApplyAuthorizedTargets,
747
+ })) {
748
+ throwPreApplyAuthorizationRequired(preApplyAuthorization.targets);
749
+ }
750
+ preApplyUnavailableTargetResults = preApplyAuthorization.unavailableTargetResults;
751
+ applyDiscovery = {
752
+ targets: preApplyAuthorization.availableApplyTargets,
753
+ failures: [...applyDiscovery.failures, ...preApplyUnavailableTargetResults],
754
+ };
755
+ if (applyDiscovery.targets.length === 0 && preApplyUnavailableTargetResults.length > 0) {
756
+ preApplyUnavailableReason = 'sourceUnavailable';
757
+ }
575
758
  }
576
759
  }
577
760
  if (options.apply !== false
761
+ && options.scope === 'user'
578
762
  && options.sharedNativeApplyConfirmed !== true
579
- && changedKeys.length > 0
580
- && schemaPlugin.manifest.scripts?.preApply) {
581
- const error = new Error('preApplyAuthorizationRequired: Applying this config runs plugin preApply. Re-run with --yes.');
582
- error.code = 'preApplyAuthorizationRequired';
583
- throw error;
763
+ && impact.sharedNativeRenderTargets.length > 0
764
+ && impact.affectedTargets.some((target) => target.platform === 'codex')) {
765
+ throwSharedNativeApplyConfirmationRequired();
584
766
  }
585
- if (changedKeys.length > 0) {
767
+ if (impact.storeChanged) {
586
768
  try {
587
769
  const write = await patchPluginConfigStore({
588
770
  ...configIdentity,
@@ -613,7 +795,7 @@ async function applyPluginConfigWriteUnlocked(options) {
613
795
  }
614
796
  catch {
615
797
  const apply = unavailableApply('effectiveConfigUnavailable', 'effectiveConfig');
616
- const otherProjectsMayRequireSync = options.scope === 'user' && changedKeys.length > 0;
798
+ const otherProjectsMayRequireSync = options.scope === 'user' && impact.storeChanged;
617
799
  if (otherProjectsMayRequireSync)
618
800
  warnings.push(...otherProjectsWarning(options.scope));
619
801
  return {
@@ -632,29 +814,38 @@ async function applyPluginConfigWriteUnlocked(options) {
632
814
  }
633
815
  let apply;
634
816
  try {
635
- apply = changedKeys.length === 0
817
+ apply = !impact.storeChanged
636
818
  ? skippedApply('noConfigChange')
637
- : staleRemovedKeys.length === changedKeys.length
638
- ? skippedApply('noEffectiveConfigChange')
639
- : options.apply === false
640
- ? skippedApply('notRequested')
641
- : await applyConfigToVisibleTargets({
642
- plugin: schemaPlugin,
643
- scope: options.scope,
644
- cwd: options.cwd,
645
- env: options.env ?? process.env,
646
- applyTargets,
647
- nativeCliRunner: options.nativeCliRunner,
648
- reinstallCommandImpl: options.reinstallCommandImpl,
649
- rebuildRuntime: options.rebuildRuntime,
650
- noVisibleTargetHasInvalidConfig: effective.issues.length > 0,
651
- });
819
+ : options.apply === false
820
+ ? skippedApply('notRequested')
821
+ : targetDiscoveryFailed
822
+ ? unavailableApply('targetDiscoveryFailed', 'targetDiscovery')
823
+ : impact.noEffectiveChange
824
+ && ((discovered?.targets.length ?? 0) > 0
825
+ || staleRemovedKeys.length === impact.changedKeys.length)
826
+ ? skippedApply('noEffectiveConfigChange')
827
+ : preApplyUnavailableReason
828
+ ? unavailableApply(preApplyUnavailableReason, preApplyUnavailableReason === 'sourceUnavailable' ? 'source' : 'targetDiscovery', preApplyUnavailableTargetResults)
829
+ : await applyConfigToVisibleTargets({
830
+ plugin: schemaPlugin,
831
+ scope: options.scope,
832
+ cwd: options.cwd,
833
+ env: options.env ?? process.env,
834
+ applyTargets: applyDiscovery?.targets ?? impact.affectedTargets,
835
+ nativeCliRunner: options.nativeCliRunner,
836
+ reinstallCommandImpl: options.reinstallCommandImpl,
837
+ rebuildRuntime: options.rebuildRuntime,
838
+ noVisibleTargetHasInvalidConfig: (discovered?.targets.length ?? 0) > 0
839
+ && effective.issues.length > 0,
840
+ preApplyConfirmed: options.preApplyConfirmed,
841
+ discovered: applyDiscovery,
842
+ });
652
843
  }
653
844
  catch (error) {
654
845
  const failure = error instanceof Error ? error : new Error(String(error));
655
846
  throw Object.assign(failure, { configWrite: 'succeeded' });
656
847
  }
657
- const otherProjectsMayRequireSync = options.scope === 'user' && changedKeys.length > 0;
848
+ const otherProjectsMayRequireSync = options.scope === 'user' && impact.storeChanged;
658
849
  if (otherProjectsMayRequireSync)
659
850
  warnings.push(...otherProjectsWarning(options.scope));
660
851
  return {
@@ -797,7 +988,6 @@ async function resetPluginConfigWithHelperUnlocked(options) {
797
988
  }
798
989
  }
799
990
  let removedKeys = [];
800
- let removedKeysKnown = false;
801
991
  try {
802
992
  const store = await readPluginConfigStore({
803
993
  ...configIdentity,
@@ -807,37 +997,94 @@ async function resetPluginConfigWithHelperUnlocked(options) {
807
997
  env: options.env,
808
998
  });
809
999
  removedKeys = collectRemovedConfigKeys(store.values);
810
- removedKeysKnown = store.state !== 'invalid';
811
1000
  }
812
1001
  catch {
813
1002
  // Reset is the final recovery boundary and does not require a readable store.
814
1003
  }
815
1004
  const warnings = [];
816
- const sharedNativeAffected = options.plugin
817
- && (removedKeysKnown
818
- ? changedKeysAffectSharedNativeTarget({
1005
+ let impact;
1006
+ let targetDiscoveryFailed = false;
1007
+ let visibleTargets = [];
1008
+ if (options.plugin) {
1009
+ if (options.apply !== false) {
1010
+ try {
1011
+ visibleTargets = (await resolveConfigApplyTargets({
1012
+ plugin: options.plugin,
1013
+ scope: options.scope,
1014
+ cwd: options.cwd,
1015
+ env: options.env ?? process.env,
1016
+ applyTargets: options.applyTargets,
1017
+ nativeCliRunner: options.nativeCliRunner,
1018
+ rebuildRuntime: options.rebuildRuntime,
1019
+ })).targets;
1020
+ }
1021
+ catch {
1022
+ targetDiscoveryFailed = true;
1023
+ }
1024
+ }
1025
+ try {
1026
+ impact = await analyzePluginConfigMutationImpact({
819
1027
  plugin: options.plugin,
820
- changedKeys: removedKeys,
821
- })
822
- : pluginHasSharedNativeRefreshKeys(options.plugin));
1028
+ scope: options.scope,
1029
+ cwd: options.cwd,
1030
+ env: options.env,
1031
+ visibleTargets,
1032
+ mutation: { kind: 'reset' },
1033
+ });
1034
+ removedKeys = impact.removedKeys;
1035
+ }
1036
+ catch {
1037
+ // Reset remains available when its before state cannot be read.
1038
+ }
1039
+ }
1040
+ let preApplyUnavailableReason;
1041
+ let preApplyUnavailableTargetResults = [];
1042
+ let applyDiscovery = options.plugin && !targetDiscoveryFailed
1043
+ ? { targets: impact?.affectedTargets ?? visibleTargets, failures: [] }
1044
+ : undefined;
823
1045
  if (options.plugin
824
1046
  && options.apply !== false
825
- && options.scope === 'user'
826
- && options.sharedNativeApplyConfirmed !== true
827
- && sharedNativeAffected) {
828
- const discovered = await resolveConfigApplyTargets({
1047
+ && impact?.storeChanged
1048
+ && !impact.noEffectiveChange
1049
+ && applyDiscovery
1050
+ && pluginMayRunPreApply(options.plugin)) {
1051
+ const preApplyAuthorization = await resolveConfigPreApplyAuthorization({
829
1052
  plugin: options.plugin,
830
1053
  scope: options.scope,
831
1054
  cwd: options.cwd,
832
- env: options.env ?? process.env,
833
- applyTargets: options.applyTargets,
1055
+ env: options.env,
1056
+ applyTargets: applyDiscovery.targets,
834
1057
  nativeCliRunner: options.nativeCliRunner,
835
1058
  rebuildRuntime: options.rebuildRuntime,
1059
+ reinstallCommandImpl: options.reinstallCommandImpl,
836
1060
  });
837
- if (discovered.targets.some((target) => target.platform === 'codex')) {
838
- throwSharedNativeApplyConfirmationRequired();
1061
+ if (preApplyAuthorization.targets.length > 0 && !preApplyAuthorizationIsConfirmed({
1062
+ required: preApplyAuthorization.targets,
1063
+ confirmed: options.preApplyConfirmed,
1064
+ authorizedTargets: options.preApplyAuthorizedTargets,
1065
+ })) {
1066
+ throwPreApplyAuthorizationRequired(preApplyAuthorization.targets);
1067
+ }
1068
+ preApplyUnavailableTargetResults = preApplyAuthorization.unavailableTargetResults;
1069
+ applyDiscovery = {
1070
+ targets: preApplyAuthorization.availableApplyTargets,
1071
+ failures: preApplyUnavailableTargetResults,
1072
+ };
1073
+ if (applyDiscovery.targets.length === 0 && preApplyUnavailableTargetResults.length > 0) {
1074
+ preApplyUnavailableReason = 'sourceUnavailable';
839
1075
  }
840
1076
  }
1077
+ if (options.plugin
1078
+ && options.apply !== false
1079
+ && options.scope === 'user'
1080
+ && options.sharedNativeApplyConfirmed !== true
1081
+ && (impact
1082
+ ? impact.sharedNativeRenderTargets.length > 0
1083
+ && impact.affectedTargets.some((target) => target.platform === 'codex')
1084
+ : pluginHasSharedNativeRefreshKeys(options.plugin))
1085
+ && visibleTargets.some((target) => target.platform === 'codex')) {
1086
+ throwSharedNativeApplyConfirmationRequired();
1087
+ }
841
1088
  let reset;
842
1089
  try {
843
1090
  reset = await resetPluginConfigStore({
@@ -886,18 +1133,35 @@ async function resetPluginConfigWithHelperUnlocked(options) {
886
1133
  apply = unavailableApply('effectiveConfigUnavailable', 'effectiveConfig');
887
1134
  }
888
1135
  }
889
- if (reset.deleted && options.apply !== false && apply.state === 'skipped') {
890
- apply = await applyConfigToVisibleTargets({
891
- plugin: schemaPlugin,
892
- scope: options.scope,
893
- cwd: options.cwd,
894
- env: options.env ?? process.env,
895
- applyTargets: options.applyTargets,
896
- nativeCliRunner: options.nativeCliRunner,
897
- reinstallCommandImpl: options.reinstallCommandImpl,
898
- rebuildRuntime: options.rebuildRuntime,
899
- noVisibleTargetHasInvalidConfig: (effective?.issues.length ?? 0) > 0,
900
- });
1136
+ if (reset.deleted
1137
+ && options.apply !== false
1138
+ && apply.state === 'skipped'
1139
+ && impact?.noEffectiveChange
1140
+ && visibleTargets.length > 0) {
1141
+ apply = skippedApply('noEffectiveConfigChange');
1142
+ }
1143
+ else if (reset.deleted
1144
+ && options.apply !== false
1145
+ && apply.state === 'skipped'
1146
+ && targetDiscoveryFailed) {
1147
+ apply = unavailableApply('targetDiscoveryFailed', 'targetDiscovery');
1148
+ }
1149
+ else if (reset.deleted && options.apply !== false && apply.state === 'skipped') {
1150
+ apply = preApplyUnavailableReason
1151
+ ? unavailableApply(preApplyUnavailableReason, 'source', preApplyUnavailableTargetResults)
1152
+ : await applyConfigToVisibleTargets({
1153
+ plugin: schemaPlugin,
1154
+ scope: options.scope,
1155
+ cwd: options.cwd,
1156
+ env: options.env ?? process.env,
1157
+ applyTargets: applyDiscovery?.targets ?? impact?.affectedTargets ?? options.applyTargets,
1158
+ nativeCliRunner: options.nativeCliRunner,
1159
+ reinstallCommandImpl: options.reinstallCommandImpl,
1160
+ rebuildRuntime: options.rebuildRuntime,
1161
+ noVisibleTargetHasInvalidConfig: (effective?.issues.length ?? 0) > 0,
1162
+ preApplyConfirmed: options.preApplyConfirmed,
1163
+ discovered: applyDiscovery,
1164
+ });
901
1165
  }
902
1166
  }
903
1167
  else if (reset.deleted && options.apply !== false && apply.state === 'skipped') {