fix(extensions): share global packages by client
CI / Capabilities and shell quality (push) Failing after 27s
CI / Python tests and compilation (push) Failing after 32s
CI / Build and inspect generated packages (push) Skipped
CI / Bash integration and installation guards (push) Skipped
CI / Installed client configuration smoke tests (push) Skipped
CI / Qwen retrieval integration (push) Skipped
CI / PowerShell tests on Ubuntu (push) Failing after 33s
CI / CI configuration security checks (push) Successful in 5s
CI / Capabilities and shell quality (push) Failing after 27s
CI / Python tests and compilation (push) Failing after 32s
CI / Build and inspect generated packages (push) Skipped
CI / Bash integration and installation guards (push) Skipped
CI / Installed client configuration smoke tests (push) Skipped
CI / Qwen retrieval integration (push) Skipped
CI / PowerShell tests on Ubuntu (push) Failing after 33s
CI / CI configuration security checks (push) Successful in 5s
This commit is contained in:
1 parent
5c1b8fedba
commit
0befbbc07c
2 files changed
+44
-20
No files matched your search
@@ -15,7 +15,7 @@
|
||||
|
||||
- [x] Build update-menu checks per client when `Both` is selected. Both menus inspect Claude's installed plugins for every entry, so a Codex resource can start unchecked and the combined sync can remove it. (`scripts/lib/plugins.ps1:98`, `scripts/lib/plugins.sh:108`)
|
||||
- [x] Keep shared MCPorter servers until neither client needs them. Reconciliation processes `shared` records even when only one client is selected; uninstalling that client with an empty selection removes servers still used by the other. (`scripts/lib/managed-extensions.py:463`, `scripts/commands/uninstall.ps1:59`, `scripts/commands/uninstall.sh:77`)
|
||||
- [ ] Manage global packages once across clients. With `--client both`, MCPorter is installed and recorded twice; QMD's global package can be removed during a Codex-only deselection while Claude still uses its plugin. (`scripts/lib/managed-extensions.py:342`, `scripts/lib/managed-extensions.py:378`, `scripts/lib/managed-extensions.py:463`)
|
||||
- [x] Manage global packages once across clients. With `--client both`, MCPorter is installed and recorded twice; QMD's global package can be removed during a Codex-only deselection while Claude still uses its plugin. (`scripts/lib/managed-extensions.py:342`, `scripts/lib/managed-extensions.py:378`, `scripts/lib/managed-extensions.py:463`)
|
||||
- [x] Fix Bash `auto` semantic retrieval selection. Both `test_semantic_retrieval_device()` and its caller read `$?` after a completed `if`, losing the benchmark's exit code; an unsuitable device produces an empty choice instead of `false`. (`scripts/lib/semantic-retrieval.sh:13`, `scripts/lib/install-options.sh:52`)
|
||||
- [x] Make ordinary update dry-runs use the saved/current selection. Both menus bypass selection in dry-run and select every manifest entry, so the preview can diverge from the real update. (`scripts/lib/plugins.ps1:91`, `scripts/lib/plugins.sh:97`)
|
||||
- [x] Reconcile resources whose ownership record remains but whose installation was removed manually. `ensure_cli()` and `ensure_mcporter()` return early from a ledger record without checking the actual CLI or server; normal sync cannot restore the missing resource. (`scripts/lib/managed-extensions.py:342`, `scripts/lib/managed-extensions.py:395`)
|
||||
|
||||
@@ -163,7 +163,7 @@ class Manager:
|
||||
self.state = json.loads(self.path.read_text(encoding='utf-8'))
|
||||
self.validate()
|
||||
self.cache = {}
|
||||
self.new_packages = set()
|
||||
self.updated_packages = set()
|
||||
|
||||
def validate(self):
|
||||
if not isinstance(self.state, dict) or self.state.get('version') != 1 or not isinstance(self.state.get('resources'), list):
|
||||
@@ -196,7 +196,7 @@ class Manager:
|
||||
if not relative.startswith(directory + '/') or not re.fullmatch(r'[a-f0-9]{64}', value):
|
||||
raise ValueError('Invalid owned skill file record.')
|
||||
safe_path(self.home, relative)
|
||||
elif resource['kind'] == 'qmd' and (resource.get('package') != '@tobilu/qmd' or resource['client'] != 'codex'):
|
||||
elif resource['kind'] == 'qmd' and (resource.get('package') != '@tobilu/qmd' or resource['client'] not in ('codex', 'shared')):
|
||||
raise ValueError('Invalid owned QMD record.')
|
||||
elif resource['kind'] == 'cli' and not re.fullmatch(r'[a-z0-9@/_.-]+', resource.get('package', '')):
|
||||
raise ValueError('Invalid owned CLI record.')
|
||||
@@ -223,6 +223,24 @@ class Manager:
|
||||
self.cache['global_packages'] = set(result['dependencies'])
|
||||
return self.cache['global_packages']
|
||||
|
||||
def normalize_global_records(self):
|
||||
for name in {record['name'] for record in self.state['resources'] if record['kind'] in ('cli', 'qmd')}:
|
||||
records = [record for record in self.state['resources'] if record['name'] == name and record['kind'] in ('cli', 'qmd')]
|
||||
legacy = [record for record in records if record['client'] != 'shared']
|
||||
if not legacy:
|
||||
continue
|
||||
shared = next((record for record in records if record['client'] == 'shared'), None)
|
||||
if shared is None:
|
||||
shared = dict(legacy[0], client='shared')
|
||||
self.state['resources'].append(shared)
|
||||
users = set(shared.get('clients', [])) | {record['client'] for record in legacy}
|
||||
if shared['kind'] == 'qmd' and any(record['client'] == 'claude' and record['name'] == name and record['kind'] == 'plugin' for record in self.state['resources']):
|
||||
users.add('claude')
|
||||
shared['clients'] = sorted(users)
|
||||
for record in legacy:
|
||||
self.state['resources'].remove(record)
|
||||
self.save()
|
||||
|
||||
def command(self, arguments):
|
||||
if self.dry_run:
|
||||
if not self.summary:
|
||||
@@ -357,19 +375,22 @@ class Manager:
|
||||
package = entry['codex_source']
|
||||
if not re.fullmatch(r'[a-z0-9@/_.-]+', package):
|
||||
raise ValueError('Invalid CLI package.')
|
||||
record = next((r for r in self.state['resources'] if r['client'] == client and r['name'] == entry['name']), None)
|
||||
record = next((r for r in self.state['resources'] if r['client'] == 'shared' and r['name'] == entry['name']), None)
|
||||
installed = package in self.global_packages()
|
||||
if record is None and installed:
|
||||
if not self.summary:
|
||||
print(f'PASS Preserving pre-existing global package: {package}')
|
||||
return
|
||||
if record is not None and installed and not self.update:
|
||||
if record is not None:
|
||||
record['clients'] = sorted(set(record.get('clients', ['claude', 'codex'])) | {client})
|
||||
self.save()
|
||||
if installed and (not self.update or package in self.updated_packages):
|
||||
return
|
||||
self.command(['npm', 'update' if record is not None and installed else 'install', '--global', package])
|
||||
self.global_packages().add(package)
|
||||
self.new_packages.add(package)
|
||||
if not self.dry_run and record is None:
|
||||
self.state['resources'].append(dict(client=client, name=entry['name'], kind='cli', package=package))
|
||||
self.updated_packages.add(package)
|
||||
if record is None:
|
||||
self.state['resources'].append(dict(client='shared', name=entry['name'], kind='cli', package=package, clients=[client]))
|
||||
self.save()
|
||||
|
||||
def repair_caveman_windows_hooks(self):
|
||||
@@ -400,21 +421,23 @@ class Manager:
|
||||
package = entry['codex_source']
|
||||
if package != '@tobilu/qmd':
|
||||
raise ValueError('Invalid QMD package.')
|
||||
record = next((r for r in self.state['resources'] if r['client'] == 'codex' and r['name'] == entry['name']), None)
|
||||
pre_existing = record is None and package in self.global_packages() and package not in self.new_packages
|
||||
if not getattr(self, 'qmd_ready', False) and not pre_existing and package not in self.global_packages():
|
||||
record = next((r for r in self.state['resources'] if r['client'] == 'shared' and r['name'] == entry['name']), None)
|
||||
installed = package in self.global_packages()
|
||||
if record is not None:
|
||||
record['clients'] = sorted(set(record.get('clients', ['claude', 'codex'])) | {client})
|
||||
self.save()
|
||||
if record is None and installed and not self.summary:
|
||||
print(f'PASS Preserving pre-existing global package: {package}')
|
||||
if (record is not None or not installed) and (not installed or (self.update and package not in self.updated_packages)):
|
||||
self.command(['npm', 'install', '--global', package])
|
||||
self.global_packages().add(package)
|
||||
self.new_packages.add(package)
|
||||
self.qmd_ready = True
|
||||
self.updated_packages.add(package)
|
||||
if record is None:
|
||||
self.state['resources'].append(dict(client='shared', name=entry['name'], kind='qmd', package=package, clients=[client]))
|
||||
self.save()
|
||||
if client == 'claude':
|
||||
entry = dict(entry, codex_method='plugin')
|
||||
self.ensure(client, entry)
|
||||
return
|
||||
if record is not None or pre_existing:
|
||||
return
|
||||
self.state['resources'].append(dict(client='codex', name=entry['name'], kind='qmd', package=package))
|
||||
self.save()
|
||||
|
||||
def ensure_mcporter(self, entry, clients):
|
||||
server, url = entry['mcporter_name'], entry['mcporter_url']
|
||||
@@ -503,6 +526,7 @@ class Manager:
|
||||
self.save()
|
||||
|
||||
def sync(self, entries, clients, selected, action='sync'):
|
||||
self.normalize_global_records()
|
||||
desired = {entry['name']: entry for entry in entries if entry['name'] in selected}
|
||||
use_mcporter = 'MCPorter' in desired
|
||||
mcporter_entries = {name: entry for name, entry in desired.items() if use_mcporter and entry.get('mcporter_name') and entry.get('mcporter_url') not in ('', '-')}
|
||||
@@ -510,9 +534,9 @@ class Manager:
|
||||
for record in sorted(self.state['resources'], key=lambda record: record['kind'] != 'mcp'):
|
||||
if record['client'] != 'shared' and record['client'] not in clients:
|
||||
continue
|
||||
selected_entries = mcporter_entries if record['client'] == 'shared' else direct_entries
|
||||
selected_entries = mcporter_entries if record['kind'] == 'mcp' else direct_entries
|
||||
if (action == 'sync' and record['name'] not in selected_entries) or (action == 'remove' and record['name'] in selected):
|
||||
if record['kind'] == 'mcp':
|
||||
if record['client'] == 'shared':
|
||||
remaining = [client for client in record.get('clients', ['claude', 'codex']) if client not in clients]
|
||||
if remaining:
|
||||
record['clients'] = remaining
|
||||
|
||||
Reference in new issue
Block a user