-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcodium-copilot_Version13(1).py
More file actions
1328 lines (1066 loc) · 48 KB
/
codium-copilot_Version13(1).py
File metadata and controls
1328 lines (1066 loc) · 48 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
VSCodium GitHub Copilot Auto-Installer v2.1
Automatically finds and installs compatible Copilot extensions for your VSCodium version
by checking which API proposals are actually available in your VSCodium installation.
Features:
- Smart API compatibility detection
- Automatic version finding
- User-level product.json configuration (survives updates)
- Model selector enablement
- Full settings configuration
Usage:
python3 codium-copilot.py
Requirements:
- Python 3.7+
- requests
- packaging
Author: GitHub Community
License: MIT
"""
import json
import os
import re
import subprocess
import sys
import time
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Set, Tuple
try:
import requests
from packaging import version
except ImportError as e:
print(f"Error: Missing required dependency: {e.name}")
print("\nPlease install required packages:")
print(" pip install requests packaging")
sys.exit(1)
# ============================================================================
# Constants
# ============================================================================
API_URL = "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery"
API_TIMEOUT = 30
DOWNLOAD_TIMEOUT = 120
DOWNLOAD_CHUNK_SIZE = 8192
MAX_VERSIONS_TO_CHECK = 200
INSTALL_RETRY_COUNT = 2
INSTALL_RETRY_DELAY = 3
# API Flags
API_FLAGS = 0x1 | 0x2 | 0x10
# User config directories
USER_CONFIG_DIRS = [
Path.home() / '.config' / 'VSCodium',
Path.home() / '.config' / 'Code - OSS',
Path(os.environ.get('XDG_CONFIG_HOME', Path.home() / '.config')) / 'VSCodium',
]
# System product.json locations
SYSTEM_PRODUCT_JSON_PATHS = [
Path('/usr/share/codium/resources/app/product.json'),
Path('/opt/vscodium-bin/resources/app/product.json'),
Path('/opt/VSCodium/resources/app/product.json'),
Path('/Applications/VSCodium.app/Contents/Resources/app/product.json'),
Path('/snap/codium/current/usr/share/codium/resources/app/product.json'),
]
# Runtime extensionApiProposals.js locations (authoritative list of implemented proposals)
_API_PROPOSALS_REL = 'resources/app/out/vs/workbench/api/common/extensionApiProposals.js'
RUNTIME_API_PROPOSALS_PATHS = [
Path('/usr/share/codium') / _API_PROPOSALS_REL,
Path('/opt/vscodium-bin') / _API_PROPOSALS_REL,
Path('/opt/VSCodium') / _API_PROPOSALS_REL,
Path('/snap/codium/current/usr/share/codium') / _API_PROPOSALS_REL,
# Flatpak — system-wide install
Path('/var/lib/flatpak/app/com.vscodium.codium/current/active/files/share/codium') / _API_PROPOSALS_REL,
# Flatpak — per-user install
Path.home() / '.local/share/flatpak/app/com.vscodium.codium/current/active/files/share/codium' / _API_PROPOSALS_REL,
]
# Workbench bundle files — contain the full allApiProposals table embedded.
# Used as a secondary fallback when extensionApiProposals.js cannot be found
# (e.g. some package layouts bundle JS files differently).
_WORKBENCH_BUNDLE_REL = 'resources/app/out/vs/workbench/workbench.desktop.main.js'
WORKBENCH_BUNDLE_PATHS = [
Path('/usr/share/codium') / _WORKBENCH_BUNDLE_REL,
Path('/opt/vscodium-bin') / _WORKBENCH_BUNDLE_REL,
Path('/opt/VSCodium') / _WORKBENCH_BUNDLE_REL,
Path('/snap/codium/current/usr/share/codium') / _WORKBENCH_BUNDLE_REL,
Path('/var/lib/flatpak/app/com.vscodium.codium/current/active/files/share/codium') / _WORKBENCH_BUNDLE_REL,
Path.home() / '.local/share/flatpak/app/com.vscodium.codium/current/active/files/share/codium' / _WORKBENCH_BUNDLE_REL,
]
# macOS config directory
if sys.platform == 'darwin':
_macos_base = Path('/Applications/VSCodium.app/Contents/Resources/app')
USER_CONFIG_DIRS.insert(0, Path.home() / 'Library' / 'Application Support' / 'VSCodium')
RUNTIME_API_PROPOSALS_PATHS.insert(0,
_macos_base / 'out/vs/workbench/api/common/extensionApiProposals.js')
WORKBENCH_BUNDLE_PATHS.insert(0,
_macos_base / 'out/vs/workbench/workbench.desktop.main.js')
# Windows config directory
if sys.platform == 'win32':
appdata = os.environ.get('APPDATA')
if appdata:
USER_CONFIG_DIRS.insert(0, Path(appdata) / 'VSCodium')
localappdata = os.environ.get('LOCALAPPDATA')
if localappdata:
_win_base = Path(localappdata) / 'Programs' / 'VSCodium' / 'resources' / 'app'
RUNTIME_API_PROPOSALS_PATHS.insert(0,
_win_base / 'out' / 'vs' / 'workbench' / 'api' / 'common' / 'extensionApiProposals.js')
WORKBENCH_BUNDLE_PATHS.insert(0,
_win_base / 'out' / 'vs' / 'workbench' / 'workbench.desktop.main.js')
# Required settings for full Copilot functionality
COPILOT_SETTINGS = {
"github.copilot.editor.enableAutoCompletions": True,
"github.copilot.chat.experimental.modelSelection": True,
"github.copilot.chat.modelPicker.enabled": True,
"github.copilot.advanced": {
"debug.enableModelSelection": True
}
}
# JS identifiers that can match the proposal pattern but are not API proposals
_JS_NOISE_WORDS = frozenset({'version', 'exports', 'module', 'define', 'require', 'default'})
@dataclass
class Extension:
"""Extension metadata."""
extension_id: str
name: str
install_order: int
# Only GitHub Copilot Chat is needed - includes inline completions
EXTENSIONS = [
Extension("GitHub.copilot-chat", "GitHub Copilot Chat", 1),
]
# ============================================================================
# Terminal Colors
# ============================================================================
class Colors:
"""ANSI color codes for terminal output."""
HEADER = '\033[95m'
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
@classmethod
def disable(cls) -> None:
"""Disable all colors (for non-TTY terminals)."""
cls.HEADER = ''
cls.OKBLUE = ''
cls.OKCYAN = ''
cls.OKGREEN = ''
cls.WARNING = ''
cls.FAIL = ''
cls.ENDC = ''
cls.BOLD = ''
cls.UNDERLINE = ''
if not sys.stdout.isatty():
Colors.disable()
# ============================================================================
# Output Functions
# ============================================================================
def print_banner() -> None:
"""Print application banner."""
banner = f"""
{Colors.OKCYAN}{Colors.BOLD}╔══════════════════════════════════════════════════════════════╗
║ ║
║ VSCodium GitHub Copilot Auto-Installer v2.1 ║
║ With Smart API Compatibility Detection ║
║ ║
╚══════════════════════════════════════════════════════════════╝{Colors.ENDC}
"""
print(banner)
def print_step(step_num: int, total_steps: int, message: str) -> None:
"""Print a numbered step header."""
print(f"\n{Colors.BOLD}{Colors.OKBLUE}[{step_num}/{total_steps}]{Colors.ENDC} "
f"{Colors.BOLD}{message}{Colors.ENDC}")
def print_success(message: str, indent: int = 2) -> None:
"""Print success message."""
print(f"{' ' * indent}{Colors.OKGREEN}✓{Colors.ENDC} {message}")
def print_error(message: str, indent: int = 2) -> None:
"""Print error message."""
print(f"{' ' * indent}{Colors.FAIL}✗{Colors.ENDC} {message}")
def print_warning(message: str, indent: int = 2) -> None:
"""Print warning message."""
print(f"{' ' * indent}{Colors.WARNING}⚠{Colors.ENDC} {message}")
def print_info(message: str, indent: int = 2) -> None:
"""Print info message."""
print(f"{' ' * indent}{Colors.OKCYAN}ℹ{Colors.ENDC} {message}")
# ============================================================================
# Safety Checks
# ============================================================================
def check_not_running_in_codium() -> None:
"""Verify script is not running inside VSCodium terminal."""
if os.environ.get('TERM_PROGRAM') == 'vscode':
print_error("This script is running inside VSCodium!", 0)
print_warning("VSCodium will be terminated during installation.", 0)
print_info("Please run this script from a regular terminal.", 0)
print(f"\n{Colors.WARNING}Exiting...{Colors.ENDC}\n")
sys.exit(1)
vscode_vars = [key for key in os.environ if key.startswith('VSCODE_')]
if vscode_vars:
print_warning("Detected VSCode-related environment variables:", 0)
for var in vscode_vars[:3]:
value = os.environ[var]
display_value = value[:50] + '...' if len(value) > 50 else value
print_info(f"{var} = {display_value}", 4)
try:
response = input(f"\n{Colors.WARNING}Are you running this inside "
f"VSCodium? (y/N): {Colors.ENDC}").strip().lower()
if response in ('y', 'yes'):
print_error("Please run this script from a regular terminal.", 0)
sys.exit(1)
except (EOFError, KeyboardInterrupt):
print("\n")
sys.exit(130)
def check_dependencies() -> None:
"""Check that required system commands are available."""
required_commands = ['codium', 'pgrep', 'pkill']
missing = []
for cmd in required_commands:
try:
subprocess.run([cmd, '--version'], capture_output=True, check=False, timeout=5)
except FileNotFoundError:
missing.append(cmd)
except subprocess.TimeoutExpired:
pass
if missing:
print_error("Missing required commands:", 0)
for cmd in missing:
print_info(cmd, 4)
sys.exit(1)
# ============================================================================
# API Proposal Detection
# ============================================================================
def get_runtime_api_proposals(proposals_js_path: Path) -> Optional[Set[str]]:
"""Extract implemented API proposal names from VSCodium's runtime JS file.
The extensionApiProposals.js file is the authoritative source for which
proposals VSCodium actually implements at runtime. Each entry has the form:
"proposalName": { version: N, proposal: "..." }
This function also works on workbench bundle files which embed the full
allApiProposals table in the same key:{ version: N } format.
"""
try:
content = proposals_js_path.read_text(encoding='utf-8')
# Match both quoted and unquoted keys followed by '{ version:'
# This is the exact structure used in allApiProposals.
names = re.findall(
r'["\']?([a-zA-Z][a-zA-Z0-9_]*)["\']?\s*:\s*\{\s*version\s*:\s*\d',
content
)
proposals = set(names)
# Exclude JS reserved/noise words that can match the pattern
proposals -= _JS_NOISE_WORDS
if proposals:
return proposals
except Exception:
pass
return None
def find_proposals_in_bundle_files(
install_roots: Optional[List[Path]] = None,
) -> Optional[Set[str]]:
"""Try to extract API proposals from the workbench desktop bundle.
Some VSCodium package layouts do not ship extensionApiProposals.js as a
separate file; instead, the allApiProposals table is inlined into the main
workbench bundle. This function checks both the hardcoded bundle paths and
any additional roots discovered from product.json locations.
A minimum threshold of 20 distinct proposal names is required to avoid
returning noise from large bundle files that may contain many objects with
a ``{ version: N }`` shape unrelated to API proposals.
"""
candidates: List[Path] = list(WORKBENCH_BUNDLE_PATHS)
if install_roots:
# install_roots are typically the resources/app directory.
# The bundle is at resources/app/out/vs/workbench/workbench.desktop.main.js
bundle_rel = Path('out') / 'vs' / 'workbench' / 'workbench.desktop.main.js'
for root in install_roots:
extra = root / bundle_rel
if extra not in candidates:
candidates.append(extra)
for bundle_path in candidates:
if bundle_path.exists():
proposals = get_runtime_api_proposals(bundle_path)
# Apply a minimum threshold to avoid noise from large bundle files
if proposals and len(proposals) >= 20:
return proposals
return None
def find_runtime_proposals_file_dynamically() -> Optional[Path]:
"""Search the filesystem for the runtime extensionApiProposals.js file.
Used as a last resort when none of the hardcoded paths exist (e.g. the
VSCodium binary is installed via Flatpak, AppImage, or a non-standard
package layout). The search is restricted to directories that are known
to host VSCodium installations so it completes quickly.
"""
# Resolve search roots from the known system product.json locations first
# (the runtime file lives in the same app tree as product.json).
search_roots: List[str] = []
for product_path in SYSTEM_PRODUCT_JSON_PATHS:
if product_path.exists():
# product.json is at <root>/resources/app/product.json
# runtime file is at <root>/resources/app/out/...
# product_path.parent is <root>/resources/app
candidate = (
product_path.parent
/ 'out' / 'vs' / 'workbench' / 'api' / 'common'
/ 'extensionApiProposals.js'
)
if candidate.exists():
return candidate
# Fall through: add resources/app so find can search it
search_roots.append(str(product_path.parent))
# Additional well-known base directories
for base in [
'/usr/share/codium',
'/opt/vscodium-bin',
'/opt/VSCodium',
'/var/lib/flatpak/app/com.vscodium.codium',
str(Path.home() / '.local/share/flatpak/app/com.vscodium.codium'),
]:
if Path(base).exists() and base not in search_roots:
search_roots.append(base)
if not search_roots:
return None
try:
result = subprocess.run(
['find'] + search_roots
+ ['-maxdepth', '20', '-name', 'extensionApiProposals.js', '-type', 'f'],
capture_output=True,
text=True,
timeout=15,
check=False,
)
for line in result.stdout.splitlines():
p = line.strip()
if p:
return Path(p)
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
pass
return None
def get_supported_api_proposals() -> Set[str]:
"""Get the list of API proposals that VSCodium actually supports.
Detection order (most → least authoritative):
1. extensionApiProposals.js — standalone runtime proposals file.
2. workbench.desktop.main.js — workbench bundle that embeds allApiProposals
(present in package layouts that do not ship the standalone file).
3. Empty set (permissive) — skip API compatibility checks entirely when
neither source can be found. The product.json extensionEnabledApiProposals
allowlist is intentionally NOT used here because it only covers proposals
explicitly approved for specific extensions and would incorrectly reject
extensions that use newer proposals (e.g. chatHooks) that VSCodium
implements but are absent from the allowlist.
"""
print_info("Detecting supported API proposals...")
# --- Primary: read from VSCodium's compiled runtime proposals file ---
# Check hardcoded paths first, then try a dynamic filesystem search.
runtime_path: Optional[Path] = None
for p in RUNTIME_API_PROPOSALS_PATHS:
if p.exists():
runtime_path = p
break
if runtime_path is None:
print_info("Runtime proposals file not found at known paths, searching...", 4)
runtime_path = find_runtime_proposals_file_dynamically()
if runtime_path is not None and runtime_path.exists():
print_success(f"Found runtime proposals file: {runtime_path}", 4)
proposals = get_runtime_api_proposals(runtime_path)
if proposals:
print_success(
f"Found {len(proposals)} runtime-implemented API proposals", 4
)
sample = sorted(proposals)[:5]
for prop in sample:
print_info(prop, 6)
if len(proposals) > 5:
print_info(f"... and {len(proposals) - 5} more", 6)
return proposals
print_warning("Could not parse runtime proposals file, trying bundle...", 4)
# --- Secondary: extract from the workbench desktop bundle ---
# Some VSCodium package layouts (e.g. certain Debian builds) do not ship
# extensionApiProposals.js as a standalone file; the allApiProposals table
# is inlined into the main workbench bundle instead.
install_roots: List[Path] = []
for product_path in SYSTEM_PRODUCT_JSON_PATHS:
if product_path.exists():
install_roots.append(product_path.parent) # resources/app dir
bundle_proposals = find_proposals_in_bundle_files(install_roots or None)
if bundle_proposals:
print_success(f"Found {len(bundle_proposals)} API proposals from workbench bundle", 4)
sample = sorted(bundle_proposals)[:5]
for prop in sample:
print_info(prop, 6)
if len(bundle_proposals) > 5:
print_info(f"... and {len(bundle_proposals) - 5} more", 6)
return bundle_proposals
# --- Fallback: skip API compatibility checking ---
# The product.json extensionEnabledApiProposals allowlist is NOT used as a
# substitute for the runtime proposals file. That list only reflects which
# proposals specific extensions are permitted to use — it does not enumerate
# everything VSCodium implements. Using it causes false negatives (e.g.
# chatHooks is implemented in VSCodium 1.100+ but absent from the allowlist).
# Returning an empty set makes check_api_compatibility accept all proposals,
# which is safer than incorrectly rejecting a compatible extension version.
system_product_json = None
for path in SYSTEM_PRODUCT_JSON_PATHS:
if path.exists():
system_product_json = path
break
if system_product_json:
print_success(f"Found system product.json: {system_product_json}", 4)
print_warning(
"Runtime proposals file not found; skipping API compatibility check", 4
)
print_info(
"All extension versions will be considered API-compatible", 4
)
print_info(
"Install the full VSCodium package to enable accurate API detection", 4
)
return set()
def normalize_api_proposal(proposal: str) -> str:
"""Normalize an API proposal name by removing version suffix."""
return proposal.split('@')[0]
def check_api_compatibility(
required_proposals: List[str],
supported_proposals: Set[str]
) -> Tuple[bool, List[str]]:
"""Check if required API proposals are supported."""
if not supported_proposals:
return True, []
unsupported = []
for proposal in required_proposals:
base_name = normalize_api_proposal(proposal)
if base_name not in supported_proposals:
unsupported.append(proposal)
is_compatible = len(unsupported) == 0
return is_compatible, unsupported
# ============================================================================
# VSIX Extraction
# ============================================================================
def extract_api_proposals_from_vsix(vsix_path: Path, extension_id: str) -> List[str]:
"""Extract enabledApiProposals from a VSIX package.json."""
try:
with zipfile.ZipFile(vsix_path, 'r') as zip_file:
try:
package_json_data = zip_file.read('extension/package.json')
except KeyError:
package_json_data = zip_file.read('package.json')
package_json = json.loads(package_json_data.decode('utf-8'))
api_proposals = package_json.get('enabledApiProposals', [])
return api_proposals
except zipfile.BadZipFile:
print_error(f"Invalid VSIX file: {vsix_path}", 4)
return []
except KeyError as e:
print_error(f"Could not find package.json in VSIX: {e}", 4)
return []
except json.JSONDecodeError as e:
print_error(f"Invalid JSON in package.json: {e}", 4)
return []
except Exception as e:
print_error(f"Failed to extract API proposals: {e}", 4)
return []
# ============================================================================
# VSCodium Configuration
# ============================================================================
def find_user_config_dir() -> Optional[Path]:
"""Find the VSCodium user configuration directory."""
for config_dir in USER_CONFIG_DIRS:
if config_dir.exists():
return config_dir
default_config = USER_CONFIG_DIRS[0]
try:
default_config.mkdir(parents=True, exist_ok=True)
return default_config
except OSError:
return None
def update_user_product_json(
extension_proposals: Dict[str, List[str]],
supported_apis: Set[str]
) -> bool:
"""Create or update user-level product.json with API proposals."""
print_info("Updating user-level product.json...")
config_dir = find_user_config_dir()
if not config_dir:
print_error("Could not find or create VSCodium config directory")
return False
product_json_path = config_dir / 'product.json'
try:
existing_data = {}
if product_json_path.exists():
try:
with product_json_path.open('r') as f:
existing_data = json.load(f)
except json.JSONDecodeError:
print_warning("Existing product.json is invalid, creating backup...", 4)
backup_path = product_json_path.with_suffix('.json.backup')
product_json_path.rename(backup_path)
if 'extensionEnabledApiProposals' not in existing_data:
existing_data['extensionEnabledApiProposals'] = {}
# Update with extracted API proposals.
# Normalize extension IDs to lowercase to match VSCodium's lookup convention.
for ext_id, proposals in extension_proposals.items():
if proposals:
norm_id = ext_id.lower()
existing_data['extensionEnabledApiProposals'][norm_id] = proposals
print_success(f"Configured {len(proposals)} API proposals for {norm_id}", 4)
# Add unversioned variants for all versioned proposals
for ext_id in list(extension_proposals.keys()):
norm_id = ext_id.lower()
proposals = existing_data['extensionEnabledApiProposals'].get(norm_id, [])
unversioned_added = []
for proposal in proposals[:]: # Copy to avoid modification during iteration
if '@' in proposal:
base_name = proposal.split('@')[0]
if base_name not in proposals:
proposals.append(base_name)
unversioned_added.append(base_name)
if unversioned_added:
print_info(f"Added {len(unversioned_added)} unversioned API variants", 4)
# Add critical proposals that might be missing — only if VSCodium actually
# implements them (present in supported_apis). Adding unimplemented proposals
# causes VSCodium to report them as incompatible at runtime.
critical_proposals = [
'languageModelPicker',
'chatParticipantPrivate',
'defaultChatParticipant',
'chatSessionsProvider',
'chatProvider',
'findFiles2',
]
for ext_id in list(extension_proposals.keys()):
norm_id = ext_id.lower()
proposals = existing_data['extensionEnabledApiProposals'].get(norm_id, [])
added_critical = []
for prop in critical_proposals:
# Only grant proposals that VSCodium can actually serve at runtime.
# When supported_apis is empty, detection failed (no runtime file or
# product.json found); fall back to allowing all critical proposals so
# the extension has the best chance of working.
if prop not in proposals and (not supported_apis or prop in supported_apis):
proposals.append(prop)
added_critical.append(prop)
if added_critical:
print_info(f"Added {len(added_critical)} critical proposals", 4)
with product_json_path.open('w') as f:
json.dump(existing_data, f, indent=2)
print_success(f"Updated user product.json: {product_json_path}")
return True
except PermissionError:
print_error(f"Permission denied when writing to {product_json_path}")
return False
except Exception as e:
print_error(f"Unexpected error configuring product.json: {e}")
return False
def update_user_settings() -> bool:
"""Update VSCodium settings to enable Copilot features."""
print_info("Updating user settings...")
config_dir = find_user_config_dir()
if not config_dir:
print_error("Could not find config directory")
return False
user_dir = config_dir / 'User'
user_dir.mkdir(parents=True, exist_ok=True)
settings_path = user_dir / 'settings.json'
try:
existing_settings = {}
if settings_path.exists():
try:
with settings_path.open('r') as f:
existing_settings = json.load(f)
except json.JSONDecodeError:
print_warning("Existing settings.json is invalid, creating backup...", 4)
backup_path = settings_path.with_suffix('.json.backup')
settings_path.rename(backup_path)
# Merge Copilot settings
updated_count = 0
for key, value in COPILOT_SETTINGS.items():
if key not in existing_settings or existing_settings[key] != value:
existing_settings[key] = value
updated_count += 1
with settings_path.open('w') as f:
json.dump(existing_settings, f, indent=2)
print_success(f"Updated settings.json: {settings_path}")
if updated_count > 0:
print_info(f"Added/updated {updated_count} Copilot settings", 4)
print_info("✓ Model selector enabled", 4)
print_info("✓ Auto-completions enabled", 4)
else:
print_info("All settings already configured", 4)
return True
except PermissionError:
print_error(f"Permission denied when writing to {settings_path}")
return False
except Exception as e:
print_error(f"Failed to update settings: {e}")
return False
# ============================================================================
# VSCodium Version Detection
# ============================================================================
def get_codium_version() -> str:
"""Get installed VSCodium version."""
try:
result = subprocess.run(
['codium', '--version'],
capture_output=True,
text=True,
check=True,
timeout=5
)
version_str = result.stdout.strip().split('\n')[0]
if not version_str or not version_str[0].isdigit():
raise ValueError(f"Invalid version format: {version_str}")
print_success(f"Detected VSCodium version: {Colors.BOLD}{version_str}{Colors.ENDC}")
return version_str
except subprocess.TimeoutExpired:
print_error("VSCodium version check timed out")
sys.exit(1)
except FileNotFoundError:
print_error("VSCodium not found in PATH")
print_info("Please install VSCodium: https://vscodium.com/", 4)
sys.exit(1)
except (subprocess.CalledProcessError, ValueError) as e:
print_error(f"Failed to get VSCodium version: {e}")
sys.exit(1)
# ============================================================================
# Marketplace API
# ============================================================================
@dataclass
class CompatibleVersion:
"""Information about a compatible extension version."""
version: str
engine_requirement: str
vsix_url: str
def query_marketplace(extension: Extension) -> Optional[Dict]:
"""Query Visual Studio Marketplace API for extension metadata."""
print_info(f"Querying marketplace for {extension.name}...")
payload = {
"filters": [{
"criteria": [{"filterType": 7, "value": extension.extension_id}],
"pageSize": 1000
}],
"flags": API_FLAGS
}
headers = {
"Content-Type": "application/json",
"Accept": "application/json;api-version=3.0-preview.1",
"User-Agent": "VSCodium-Copilot-Installer/2.1"
}
try:
response = requests.post(API_URL, json=payload, headers=headers, timeout=API_TIMEOUT)
response.raise_for_status()
data = response.json()
results = data.get('results', [])
if not results or not results[0].get('extensions'):
print_error(f"No data returned for {extension.name}")
return None
extension_data = results[0]['extensions'][0]
total_versions = len(extension_data.get('versions', []))
print_success(f"Found {total_versions} versions available")
return extension_data
except requests.exceptions.Timeout:
print_error(f"Request timed out for {extension.name}")
except requests.exceptions.RequestException as e:
print_error(f"Network error: {e}")
except (KeyError, IndexError, json.JSONDecodeError) as e:
print_error(f"Invalid API response: {e}")
return None
def parse_engine_requirement(engine_str: str) -> str:
"""Parse engine requirement string."""
return engine_str.lstrip('^>=~')
def is_version_compatible(vscode_version: str, engine_requirement: str) -> bool:
"""Check if VSCode version satisfies engine requirement."""
try:
vscode_ver = version.parse(vscode_version)
required_ver = version.parse(parse_engine_requirement(engine_requirement))
return vscode_ver >= required_ver
except version.InvalidVersion as e:
print_warning(f"Version comparison failed: {e}", 4)
return False
def find_compatible_version_with_api_check(
extension_data: Dict,
target_version: str,
extension_name: str,
extension_id: str,
supported_apis: Set[str]
) -> Optional[CompatibleVersion]:
"""Find highest compatible version with API compatibility checking."""
print_info(f"Searching for compatible version of {extension_name}...")
print_info("Will verify API proposal compatibility for each version", 4)
versions = extension_data.get('versions', [])
if not versions:
print_error("No versions available")
return None
total_to_check = min(len(versions), MAX_VERSIONS_TO_CHECK)
print_info(f"Checking up to {total_to_check} of {len(versions)} available versions...", 4)
checked_count = 0
skipped_prerelease = 0
skipped_incompatible_engine = 0
skipped_incompatible_api = 0
for ver in versions[:MAX_VERSIONS_TO_CHECK]:
checked_count += 1
ver_str = ver.get('version', 'unknown')
properties = {prop['key']: prop['value'] for prop in ver.get('properties', [])}
if properties.get('Microsoft.VisualStudio.Code.PreRelease') == 'true':
skipped_prerelease += 1
continue
engine = properties.get('Microsoft.VisualStudio.Code.Engine')
if not engine:
continue
if checked_count <= 10:
print_info(f"Checking v{ver_str} (requires {engine})...", 4)
if not is_version_compatible(target_version, engine):
skipped_incompatible_engine += 1
continue
files = ver.get('files', [])
vsix_file = next(
(f for f in files if f.get('assetType') == 'Microsoft.VisualStudio.Services.VSIXPackage'),
None
)
if not vsix_file or not vsix_file.get('source'):
continue
print_info(f"Downloading v{ver_str} to check API compatibility...", 4)
temp_vsix_path = Path(f"/tmp/{extension_id}-{ver_str}.vsix")
try:
response = requests.get(vsix_file['source'], stream=True, timeout=60)
response.raise_for_status()
with temp_vsix_path.open('wb') as f:
for chunk in response.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
f.write(chunk)
required_apis = extract_api_proposals_from_vsix(temp_vsix_path, extension_id)
if required_apis:
is_api_compatible, unsupported = check_api_compatibility(required_apis, supported_apis)
if not is_api_compatible:
print_warning(f"v{ver_str} requires unsupported APIs:", 4)
for api in unsupported[:3]:
print_info(api, 6)
if len(unsupported) > 3:
print_info(f"... and {len(unsupported) - 3} more", 6)
skipped_incompatible_api += 1
temp_vsix_path.unlink()
continue
print_success(f"Found compatible version: {Colors.BOLD}{ver_str}{Colors.ENDC}")
print_info(f"Engine requirement: {engine}", 4)
if required_apis:
print_info(f"All {len(required_apis)} API proposals are supported", 4)
print_info(f"Checked {checked_count} versions "
f"(skipped {skipped_prerelease} pre-release, "
f"{skipped_incompatible_engine} wrong engine, "
f"{skipped_incompatible_api} incompatible APIs)", 4)
temp_vsix_path.unlink()
return CompatibleVersion(
version=ver_str,
engine_requirement=engine,
vsix_url=vsix_file['source']
)
except Exception as e:
print_warning(f"Failed to check v{ver_str}: {e}", 4)
if temp_vsix_path.exists():
temp_vsix_path.unlink()
continue
print_error(f"No compatible version found for {extension_name}")
print_info(f"Checked {checked_count} versions:", 4)
print_info(f" - {skipped_prerelease} were pre-release", 4)
print_info(f" - {skipped_incompatible_engine} had incompatible engine requirements", 4)
print_info(f" - {skipped_incompatible_api} had unsupported API proposals", 4)
return None
# ============================================================================
# Download & Installation
# ============================================================================
def download_vsix(url: str, filename: str, extension_name: str) -> Optional[Path]:
"""Download VSIX file with progress indication."""
print_info(f"Downloading {extension_name}...")
filepath = Path(filename)
try:
response = requests.get(url, stream=True, timeout=DOWNLOAD_TIMEOUT)
response.raise_for_status()
total_size = int(response.headers.get('content-length', 0))
downloaded = 0
with filepath.open('wb') as f:
for chunk in response.iter_content(chunk_size=DOWNLOAD_CHUNK_SIZE):
f.write(chunk)
downloaded += len(chunk)
if total_size > 0:
percent = (downloaded / total_size) * 100
bar_length = 30
filled = int(bar_length * downloaded / total_size)
bar = '█' * filled + '░' * (bar_length - filled)
print(f"\r {Colors.OKCYAN}[{bar}] {percent:.1f}%{Colors.ENDC}",
end='', flush=True)
print()
file_size_mb = filepath.stat().st_size / (1024 * 1024)
print_success(f"Downloaded {filename} ({file_size_mb:.2f} MB)")
return filepath
except requests.exceptions.Timeout:
print_error("Download timed out")
except requests.exceptions.RequestException as e:
print_error(f"Download failed: {e}")
except OSError as e:
print_error(f"File write error: {e}")
if filepath.exists():
try:
filepath.unlink()
except OSError:
pass
return None
def cleanup_existing_extensions() -> None:
"""Remove existing Copilot extensions."""
print_info("Checking for existing Copilot extensions...")
try:
result = subprocess.run(
['codium', '--list-extensions'],
capture_output=True,
text=True,
check=True,
timeout=10
)