@softspark/ai-toolkit 4.15.0 → 4.15.1
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/AGENTS.md +117 -0
- package/CHANGELOG.md +9 -0
- package/README.md +7 -9
- package/app/.claude-plugin/plugin.json +1 -1
- package/kb/reference/global-install-model.md +5 -2
- package/kb/reference/windows-support.md +4 -3
- package/llms-full.txt +9 -5
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/scripts/copilot_legacy_hashes.json +338 -0
- package/scripts/generate_copilot.py +464 -71
- package/scripts/generate_copilot_hooks.py +124 -7
- package/scripts/install_steps/ai_tools.py +14 -1
|
@@ -18,6 +18,9 @@ import tempfile
|
|
|
18
18
|
from pathlib import Path
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
|
+
import secure_fs
|
|
22
|
+
from secure_fs import SecureDestination, run_secure_transaction
|
|
23
|
+
|
|
21
24
|
|
|
22
25
|
OWNER_KEY = "AI_TOOLKIT_HOOK_OWNER"
|
|
23
26
|
OWNER_VALUE = "ai-toolkit"
|
|
@@ -433,13 +436,11 @@ def _validate_entry(event: str, entry: Any) -> None:
|
|
|
433
436
|
raise ValueError(f"Copilot {event} {key} must be positive")
|
|
434
437
|
|
|
435
438
|
|
|
436
|
-
def
|
|
437
|
-
if path.is_symlink() or not path.is_file():
|
|
438
|
-
return False
|
|
439
|
+
def _is_managed_config_content(content: bytes) -> bool:
|
|
439
440
|
try:
|
|
440
|
-
data = json.loads(
|
|
441
|
+
data = json.loads(content.decode("utf-8"))
|
|
441
442
|
_validate_document(data)
|
|
442
|
-
except (
|
|
443
|
+
except (UnicodeError, json.JSONDecodeError, ValueError):
|
|
443
444
|
return False
|
|
444
445
|
entries = [entry for values in data["hooks"].values() for entry in values]
|
|
445
446
|
return bool(entries) and all(
|
|
@@ -448,12 +449,28 @@ def _is_managed_config(path: Path) -> bool:
|
|
|
448
449
|
)
|
|
449
450
|
|
|
450
451
|
|
|
452
|
+
def _is_managed_config(path: Path) -> bool:
|
|
453
|
+
if path.is_symlink() or not path.is_file():
|
|
454
|
+
return False
|
|
455
|
+
try:
|
|
456
|
+
return _is_managed_config_content(path.read_bytes())
|
|
457
|
+
except OSError:
|
|
458
|
+
return False
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _is_managed_script_content(content: bytes) -> bool:
|
|
462
|
+
try:
|
|
463
|
+
return SCRIPT_MARKER in content[:256].decode("utf-8")
|
|
464
|
+
except UnicodeError:
|
|
465
|
+
return False
|
|
466
|
+
|
|
467
|
+
|
|
451
468
|
def _is_managed_script(path: Path) -> bool:
|
|
452
469
|
if path.is_symlink() or not path.is_file():
|
|
453
470
|
return False
|
|
454
471
|
try:
|
|
455
|
-
return
|
|
456
|
-
except
|
|
472
|
+
return _is_managed_script_content(path.read_bytes())
|
|
473
|
+
except OSError:
|
|
457
474
|
return False
|
|
458
475
|
|
|
459
476
|
|
|
@@ -553,6 +570,106 @@ def copilot_home(home: Path | None = None) -> Path:
|
|
|
553
570
|
return base / ".copilot"
|
|
554
571
|
|
|
555
572
|
|
|
573
|
+
def _cleanup_targets(
|
|
574
|
+
target_dir: Path,
|
|
575
|
+
config_root: Path | None,
|
|
576
|
+
) -> tuple[Path, Path, Path, list[SecureDestination]]:
|
|
577
|
+
"""Resolve and validate every hook-cleanup destination without mutation."""
|
|
578
|
+
target_dir = Path(target_dir).expanduser().absolute()
|
|
579
|
+
project_install = config_root is None
|
|
580
|
+
customization_root = (
|
|
581
|
+
target_dir / ".github"
|
|
582
|
+
if project_install
|
|
583
|
+
else Path(config_root).expanduser().absolute()
|
|
584
|
+
)
|
|
585
|
+
hooks_dir = customization_root / "hooks"
|
|
586
|
+
assets_dir = hooks_dir / "ai-toolkit"
|
|
587
|
+
config_path = hooks_dir / CONFIG_NAME
|
|
588
|
+
script_path = assets_dir / SCRIPT_NAME
|
|
589
|
+
_assert_safe_paths([
|
|
590
|
+
(customization_root, "customization root"),
|
|
591
|
+
(hooks_dir, "hooks directory"),
|
|
592
|
+
(assets_dir, "hook assets directory"),
|
|
593
|
+
(config_path, "hook config"),
|
|
594
|
+
(script_path, "hook runtime"),
|
|
595
|
+
])
|
|
596
|
+
existing_paths: list[tuple[Path, str]] = []
|
|
597
|
+
if config_path.exists():
|
|
598
|
+
existing_paths.append((config_path, "hook config"))
|
|
599
|
+
if script_path.exists():
|
|
600
|
+
existing_paths.append((script_path, "hook runtime"))
|
|
601
|
+
trusted_root = target_dir if project_install else customization_root
|
|
602
|
+
destinations = [
|
|
603
|
+
SecureDestination(path, trusted_root, f"Copilot {label}")
|
|
604
|
+
for path, label in existing_paths
|
|
605
|
+
]
|
|
606
|
+
return hooks_dir, config_path, script_path, destinations
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _require_secure_cleanup() -> None:
|
|
610
|
+
if secure_fs.SECURE_DIR_FD:
|
|
611
|
+
return
|
|
612
|
+
raise RuntimeError(
|
|
613
|
+
"Copilot hook cleanup requires POSIX dir_fd and O_NOFOLLOW; "
|
|
614
|
+
"No files were changed"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def preflight_cleanup(
|
|
619
|
+
target_dir: Path,
|
|
620
|
+
*,
|
|
621
|
+
config_root: Path | None = None,
|
|
622
|
+
) -> None:
|
|
623
|
+
"""Fail before installer mutations when hook cleanup cannot run safely."""
|
|
624
|
+
_, _, _, destinations = _cleanup_targets(target_dir, config_root)
|
|
625
|
+
if not destinations:
|
|
626
|
+
return
|
|
627
|
+
_require_secure_cleanup()
|
|
628
|
+
run_secure_transaction(destinations, lambda _transaction: None)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def cleanup(target_dir: Path, *, config_root: Path | None = None) -> None:
|
|
632
|
+
"""Remove only the managed Copilot hook bundle for a profile downgrade."""
|
|
633
|
+
hooks_dir, config_path, script_path, destinations = _cleanup_targets(
|
|
634
|
+
target_dir,
|
|
635
|
+
config_root,
|
|
636
|
+
)
|
|
637
|
+
if not destinations:
|
|
638
|
+
return
|
|
639
|
+
_require_secure_cleanup()
|
|
640
|
+
|
|
641
|
+
def remove_managed(transaction) -> bool:
|
|
642
|
+
contents = {
|
|
643
|
+
destination.path: transaction.initial_content(destination)
|
|
644
|
+
for destination in destinations
|
|
645
|
+
}
|
|
646
|
+
config_content = contents.get(config_path)
|
|
647
|
+
script_content = contents.get(script_path)
|
|
648
|
+
config_owned = (
|
|
649
|
+
config_content is None or _is_managed_config_content(config_content)
|
|
650
|
+
)
|
|
651
|
+
script_owned = (
|
|
652
|
+
script_content is None or _is_managed_script_content(script_content)
|
|
653
|
+
)
|
|
654
|
+
if not config_owned or not script_owned:
|
|
655
|
+
print(
|
|
656
|
+
f"Warning: preserving user-owned Copilot hook bundle at '{hooks_dir}'",
|
|
657
|
+
file=sys.stderr,
|
|
658
|
+
)
|
|
659
|
+
return False
|
|
660
|
+
for destination in destinations:
|
|
661
|
+
transaction.unlink(destination)
|
|
662
|
+
return True
|
|
663
|
+
|
|
664
|
+
if run_secure_transaction(destinations, remove_managed):
|
|
665
|
+
label = (
|
|
666
|
+
".github/hooks"
|
|
667
|
+
if config_root is None
|
|
668
|
+
else str(hooks_dir)
|
|
669
|
+
)
|
|
670
|
+
print(f" Removed managed: {label}/{CONFIG_NAME}")
|
|
671
|
+
|
|
672
|
+
|
|
556
673
|
def generate(target_dir: Path, *, config_root: Path | None = None) -> None:
|
|
557
674
|
"""Generate repository hooks, or user hooks when ``config_root`` is set."""
|
|
558
675
|
target_dir = Path(target_dir).expanduser().absolute()
|
|
@@ -1267,6 +1267,16 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1267
1267
|
emit_pointer = not _claude_skills_discoverable(cwd)
|
|
1268
1268
|
|
|
1269
1269
|
if "copilot" in eds:
|
|
1270
|
+
from generate_copilot import (
|
|
1271
|
+
generate as gen_copilot_dir,
|
|
1272
|
+
preflight_cleanup as preflight_copilot_cleanup,
|
|
1273
|
+
)
|
|
1274
|
+
if not add_copilot_dir:
|
|
1275
|
+
from generate_copilot_hooks import (
|
|
1276
|
+
preflight_cleanup as preflight_copilot_hook_cleanup,
|
|
1277
|
+
)
|
|
1278
|
+
preflight_copilot_cleanup(cwd)
|
|
1279
|
+
preflight_copilot_hook_cleanup(cwd)
|
|
1270
1280
|
inject_with_rules(
|
|
1271
1281
|
"generate-copilot.sh",
|
|
1272
1282
|
cwd / ".github" / "copilot-instructions.md",
|
|
@@ -1275,16 +1285,19 @@ def _create_local_ai_tool_configs(cwd: Path, rules_dir: Path,
|
|
|
1275
1285
|
_install_copilot_agents_md(cwd, rules_dir)
|
|
1276
1286
|
# Agents and skills are the minimal Copilot surface. Standard and above
|
|
1277
1287
|
# add path instructions, prompts, and native lifecycle hooks.
|
|
1278
|
-
from generate_copilot import generate as gen_copilot_dir
|
|
1279
1288
|
gen_copilot_dir(
|
|
1280
1289
|
cwd,
|
|
1281
1290
|
language_modules=language_modules,
|
|
1282
1291
|
rules_dir=rules_dir,
|
|
1283
1292
|
emit_prompts=add_copilot_dir,
|
|
1284
1293
|
emit_instructions=add_copilot_dir,
|
|
1294
|
+
cleanup_disabled=not add_copilot_dir,
|
|
1285
1295
|
)
|
|
1286
1296
|
if add_copilot_dir:
|
|
1287
1297
|
_try_generator("generate_copilot_hooks", cwd)
|
|
1298
|
+
else:
|
|
1299
|
+
from generate_copilot_hooks import cleanup as cleanup_copilot_hooks
|
|
1300
|
+
cleanup_copilot_hooks(cwd)
|
|
1288
1301
|
|
|
1289
1302
|
if "cursor" in eds:
|
|
1290
1303
|
inject_with_rules(
|