ai-kit-python 1.4.2__py3-none-any.whl
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.
- ai_kit_python-1.4.2.dist-info/METADATA +132 -0
- ai_kit_python-1.4.2.dist-info/RECORD +278 -0
- ai_kit_python-1.4.2.dist-info/WHEEL +5 -0
- ai_kit_python-1.4.2.dist-info/entry_points.txt +2 -0
- ai_kit_python-1.4.2.dist-info/licenses/LICENSE +21 -0
- ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/security-audit/LICENSE +21 -0
- ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/strix/LICENSE +201 -0
- ai_kit_python-1.4.2.dist-info/top_level.txt +1 -0
- aikit/.agents/skills/go-work/SKILL.md +8 -0
- aikit/.agents/skills/laravel-work/SKILL.md +8 -0
- aikit/.agents/skills/moodle-work/SKILL.md +8 -0
- aikit/.agents/skills/project-continuity/SKILL.md +34 -0
- aikit/.agents/skills/python-work/SKILL.md +8 -0
- aikit/.dockerignore +90 -0
- aikit/.gitattributes +3 -0
- aikit/.github/workflows/check-kit.yml +70 -0
- aikit/.github/workflows/publish-pypi.yml +108 -0
- aikit/.gitignore +60 -0
- aikit/BOOTSTRAP_PROMPT.md +18 -0
- aikit/CHANGELOG.md +85 -0
- aikit/CLAUDE.md +3 -0
- aikit/CONTRIBUTING.md +46 -0
- aikit/Dockerfile +44 -0
- aikit/KIMI.md +3 -0
- aikit/LICENSE +21 -0
- aikit/MANIFEST.in +22 -0
- aikit/MANUS.md +3 -0
- aikit/Makefile +70 -0
- aikit/README.md +1161 -0
- aikit/SECURITY.md +8 -0
- aikit/SKILL.md +105 -0
- aikit/VERSION +1 -0
- aikit/__init__.py +1 -0
- aikit/ai-kit/ADAPTERS.md +5 -0
- aikit/ai-kit/CONTAINERS.md +5 -0
- aikit/ai-kit/ENGINEERING.md +5 -0
- aikit/ai-kit/INTENT.md +5 -0
- aikit/ai-kit/MODELS.md +5 -0
- aikit/ai-kit/SKILLS.md +5 -0
- aikit/ai-kit/stacks/GO.md +5 -0
- aikit/ai-kit/stacks/LARAVEL.md +5 -0
- aikit/ai-kit/stacks/MOODLE.md +5 -0
- aikit/ai-kit/stacks/PYTHON.md +5 -0
- aikit/aikit_cli.py +59 -0
- aikit/deploy/k8s/install-preview-job.yaml +56 -0
- aikit/docker/build-requirements.txt +4 -0
- aikit/docker/check_version.py +29 -0
- aikit/docker/pypi-requirements.txt +3 -0
- aikit/docs/ADAPTER_PATH_FIX.md +23 -0
- aikit/docs/BENCHMARK.md +106 -0
- aikit/docs/CONTEXT7.md +33 -0
- aikit/docs/DECISIONS.md +22 -0
- aikit/docs/DEVELOPMENT_PLAN.md +16 -0
- aikit/docs/DIALOGUE_MEMORY.md +70 -0
- aikit/docs/DOCKER.md +74 -0
- aikit/docs/DOCKER_HUB_OVERVIEW.md +311 -0
- aikit/docs/EVALUATION.md +30 -0
- aikit/docs/IMPLEMENTATION.md +134 -0
- aikit/docs/MANUS_REVIEW.md +46 -0
- aikit/docs/PYPI.md +87 -0
- aikit/docs/PYPI_DESCRIPTION.md +103 -0
- aikit/docs/SECURITY_AUDIT.md +26 -0
- aikit/docs/STRIX.md +28 -0
- aikit/docs/V0_5_0.md +27 -0
- aikit/docs/V0_6_4.md +25 -0
- aikit/docs/V1_4_0.md +73 -0
- aikit/docs/V1_4_1.md +55 -0
- aikit/docs/V1_4_2.md +33 -0
- aikit/docs/WEB_DESIGN.md +72 -0
- aikit/docs/WINDOWS_CI_FIX.md +39 -0
- aikit/docs/adr/0000-template.md +24 -0
- aikit/docs/adr/0001-adaptive-model-routing.md +30 -0
- aikit/docs/adr/0002-template-separation.md +24 -0
- aikit/docs/adr/0003-agent-registry-and-router.md +24 -0
- aikit/docs/adr/0004-onboarding-and-project-facts.md +24 -0
- aikit/docs/adr/0005-client-settings-ownership.md +30 -0
- aikit/docs/adr/0006-routing-evidence-and-local-measurements.md +26 -0
- aikit/docs/adr/0007-generated-client-configuration-and-presets.md +34 -0
- aikit/docs/adr/0008-unified-cli-and-fact-drift.md +20 -0
- aikit/docs/adr/0009-provider-review-doctor-fix-and-ci-self-install.md +20 -0
- aikit/docs/adr/0010-metrics-analysis-and-gemini-verification.md +20 -0
- aikit/docs/adr/0011-evidence-lifecycle-and-evaluation.md +20 -0
- aikit/docs/adr/0012-measurement-led-routing.md +31 -0
- aikit/docs/adr/0013-docker-cli-image.md +24 -0
- aikit/docs/adr/0014-local-dialogue-memory.md +22 -0
- aikit/docs/history/original-bundle-checks.md +11 -0
- aikit/docs/history/v0.1-changelog.txt +37 -0
- aikit/docs/history/v0.10-changelog.md +12 -0
- aikit/docs/history/v0.2-changelog.md +35 -0
- aikit/docs/history/v0.3-changelog.md +25 -0
- aikit/docs/history/v0.4-changelog.md +31 -0
- aikit/docs/history/v0.5-changelog.md +14 -0
- aikit/docs/history/v0.6.4-changelog.md +12 -0
- aikit/docs/history/v0.6.5-v0.9-changelog.md +35 -0
- aikit/docs/history/v1.1.0-changelog.md +12 -0
- aikit/docs/history/v1.2.0-changelog.md +11 -0
- aikit/docs/history/verification-before-measurements.md +53 -0
- aikit/integrations/CONVENTIONS.md +3 -0
- aikit/integrations/README.md +7 -0
- aikit/integrations/agents.json +136 -0
- aikit/integrations/ai-kit-check.yml +66 -0
- aikit/integrations/ai-kit-cline.md +4 -0
- aikit/integrations/ai-kit-roo.md +4 -0
- aikit/integrations/ai-kit-windsurf.md +4 -0
- aikit/integrations/ai-kit.mdc +6 -0
- aikit/integrations/claude-settings.data-ask.json +23 -0
- aikit/integrations/claude-settings.git-ask.json +10 -0
- aikit/integrations/copilot-instructions.md +3 -0
- aikit/integrations/security-audit.json +135 -0
- aikit/integrations/strix.json +69 -0
- aikit/pyproject.toml +60 -0
- aikit/scripts/__init__.py +1 -0
- aikit/scripts/adapters.py +184 -0
- aikit/scripts/adaptive.py +167 -0
- aikit/scripts/adr.py +61 -0
- aikit/scripts/benchmark.py +543 -0
- aikit/scripts/changelog.py +55 -0
- aikit/scripts/check_distribution.py +178 -0
- aikit/scripts/check_kit.py +406 -0
- aikit/scripts/context.py +184 -0
- aikit/scripts/docker_test_usage.py +55 -0
- aikit/scripts/doctor.py +313 -0
- aikit/scripts/evaluate.py +125 -0
- aikit/scripts/install.py +1584 -0
- aikit/scripts/learning.py +260 -0
- aikit/scripts/memory.py +344 -0
- aikit/scripts/memory_import.py +177 -0
- aikit/scripts/memory_store.py +125 -0
- aikit/scripts/metrics.py +351 -0
- aikit/scripts/router.py +399 -0
- aikit/template/.agents/hooks/session-start.md +3 -0
- aikit/template/.agents/hooks/session-start.sh +21 -0
- aikit/template/.agents/skills/ai-kit-bootstrap/SKILL.md +14 -0
- aikit/template/.agents/skills/ai-kit-review/SKILL.md +11 -0
- aikit/template/.agents/skills/ai-kit-sync-context/SKILL.md +12 -0
- aikit/template/.agents/skills/context7-docs/SKILL.md +11 -0
- aikit/template/.agents/skills/design-extract/SKILL.md +9 -0
- aikit/template/.agents/skills/dotnet-work/SKILL.md +9 -0
- aikit/template/.agents/skills/filament-work/SKILL.md +9 -0
- aikit/template/.agents/skills/frontend-work/SKILL.md +9 -0
- aikit/template/.agents/skills/go-work/SKILL.md +9 -0
- aikit/template/.agents/skills/infra-work/SKILL.md +9 -0
- aikit/template/.agents/skills/java-work/SKILL.md +9 -0
- aikit/template/.agents/skills/laravel-blade-ui/SKILL.md +9 -0
- aikit/template/.agents/skills/laravel-work/SKILL.md +10 -0
- aikit/template/.agents/skills/library-work/SKILL.md +9 -0
- aikit/template/.agents/skills/moodle-work/SKILL.md +9 -0
- aikit/template/.agents/skills/php-work/SKILL.md +9 -0
- aikit/template/.agents/skills/project-continuity/SKILL.md +21 -0
- aikit/template/.agents/skills/python-work/SKILL.md +9 -0
- aikit/template/.agents/skills/rust-work/SKILL.md +9 -0
- aikit/template/.agents/skills/security-audit/SKILL.md +16 -0
- aikit/template/.agents/skills/security-audit/agents/openai.yaml +2 -0
- aikit/template/.agents/skills/strix/SKILL.md +16 -0
- aikit/template/.agents/skills/strix/agents/openai.yaml +2 -0
- aikit/template/.agents/skills/ui-review/SKILL.md +9 -0
- aikit/template/.agents/skills/web-design/SKILL.md +9 -0
- aikit/template/AGENTS.md +16 -0
- aikit/template/CHANGELOG.md +6 -0
- aikit/template/CLAUDE.md +5 -0
- aikit/template/GEMINI.md +4 -0
- aikit/template/KIMI.md +4 -0
- aikit/template/MANUS.md +4 -0
- aikit/template/PROJECT_CONTEXT.md +35 -0
- aikit/template/README.md +6 -0
- aikit/template/WIKI.md +37 -0
- aikit/template/ai-kit/ADAPTERS.md +100 -0
- aikit/template/ai-kit/BOOTSTRAP.md +15 -0
- aikit/template/ai-kit/CONTAINERS.md +63 -0
- aikit/template/ai-kit/CONTEXT7.md +96 -0
- aikit/template/ai-kit/CORE.md +23 -0
- aikit/template/ai-kit/ENGINEERING.md +34 -0
- aikit/template/ai-kit/MCP.md +17 -0
- aikit/template/ai-kit/METRICS.md +83 -0
- aikit/template/ai-kit/SECURITY-AUDIT.md +21 -0
- aikit/template/ai-kit/SECURITY.md +34 -0
- aikit/template/ai-kit/SKILLS.md +37 -0
- aikit/template/ai-kit/STRIX.md +44 -0
- aikit/template/ai-kit/UPSTREAM.md +38 -0
- aikit/template/ai-kit/WEB-DESIGN.md +37 -0
- aikit/template/ai-kit/profiles/OWNER.md +19 -0
- aikit/template/ai-kit/project.json +14 -0
- aikit/template/ai-kit/router/POLICY.md +79 -0
- aikit/template/ai-kit/router/providers/anthropic.json +44 -0
- aikit/template/ai-kit/router/providers/anthropic.md +14 -0
- aikit/template/ai-kit/router/providers/gemini.json +13 -0
- aikit/template/ai-kit/router/providers/gemini.md +10 -0
- aikit/template/ai-kit/router/providers/kimi.json +26 -0
- aikit/template/ai-kit/router/providers/kimi.md +13 -0
- aikit/template/ai-kit/router/providers/local.json +18 -0
- aikit/template/ai-kit/router/providers/local.md +10 -0
- aikit/template/ai-kit/router/providers/openai.json +48 -0
- aikit/template/ai-kit/router/providers/openai.md +14 -0
- aikit/template/ai-kit/router/routing.json +11 -0
- aikit/template/ai-kit/router/selection.json +14 -0
- aikit/template/ai-kit/settings.json +11 -0
- aikit/template/ai-kit/stacks/DOTNET.md +13 -0
- aikit/template/ai-kit/stacks/FILAMENT.md +18 -0
- aikit/template/ai-kit/stacks/FRONTEND.md +9 -0
- aikit/template/ai-kit/stacks/GO.md +19 -0
- aikit/template/ai-kit/stacks/INFRA.md +12 -0
- aikit/template/ai-kit/stacks/JAVA.md +13 -0
- aikit/template/ai-kit/stacks/LARAVEL.md +15 -0
- aikit/template/ai-kit/stacks/LIBRARY.md +9 -0
- aikit/template/ai-kit/stacks/MOODLE.md +12 -0
- aikit/template/ai-kit/stacks/PHP.md +11 -0
- aikit/template/ai-kit/stacks/PYTHON.md +11 -0
- aikit/template/ai-kit/stacks/RUST.md +18 -0
- aikit/template/ai-kit/vendor/security-audit/AI-AND-LLM.md +83 -0
- aikit/template/ai-kit/vendor/security-audit/ATTACK-CLASSES.md +130 -0
- aikit/template/ai-kit/vendor/security-audit/CLIENT-SIDE.md +83 -0
- aikit/template/ai-kit/vendor/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
- aikit/template/ai-kit/vendor/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
- aikit/template/ai-kit/vendor/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
- aikit/template/ai-kit/vendor/security-audit/HUNTING.md +251 -0
- aikit/template/ai-kit/vendor/security-audit/LICENSE +21 -0
- aikit/template/ai-kit/vendor/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
- aikit/template/ai-kit/vendor/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
- aikit/template/ai-kit/vendor/security-audit/RECONNAISSANCE.md +156 -0
- aikit/template/ai-kit/vendor/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
- aikit/template/ai-kit/vendor/security-audit/SKILL.md +192 -0
- aikit/template/ai-kit/vendor/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
- aikit/template/ai-kit/vendor/security-audit/VALIDATION-AND-REPORTING.md +186 -0
- aikit/template/ai-kit/vendor/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
- aikit/template/ai-kit/vendor/security-audit/report-schema.json +461 -0
- aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.cjs +872 -0
- aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.test.cjs +740 -0
- aikit/template/ai-kit/vendor/security-audit/validate-findings.cjs +773 -0
- aikit/template/ai-kit/vendor/security-audit/validate-findings.test.cjs +652 -0
- aikit/template/ai-kit/vendor/strix/LICENSE +201 -0
- aikit/template/ai-kit/vendor/strix/api-security-testing/SKILL.md +61 -0
- aikit/template/ai-kit/vendor/strix/application-security-testing/SKILL.md +66 -0
- aikit/template/ai-kit/vendor/strix/ci-security-scanning-with-strix/SKILL.md +149 -0
- aikit/template/ai-kit/vendor/strix/find-security-vulnerabilities-in-code/SKILL.md +62 -0
- aikit/template/ai-kit/vendor/strix/fix-security-vulnerabilities-with-strix/SKILL.md +77 -0
- aikit/template/ai-kit/vendor/strix/managed-pentesting-with-strix/SKILL.md +321 -0
- aikit/template/ai-kit/vendor/strix/owasp-top-10-testing/SKILL.md +64 -0
- aikit/template/ai-kit/vendor/strix/penetration-testing-with-strix/SKILL.md +162 -0
- aikit/template/ai-kit/vendor/strix/web-app-penetration-testing/SKILL.md +54 -0
- aikit/template/ai-kit/web-design/LARAVEL.md +51 -0
- aikit/template/ai-kit/web-design/LIBRARIES.md +39 -0
- aikit/template/ai-kit/web-design/REVIEW.md +37 -0
- aikit/template/ai-kit/web-design/TOOLS.md +43 -0
- aikit/template/docs/DECISIONS.md +7 -0
- aikit/template/docs/adr/0000-template.md +20 -0
- aikit/template/docs/design/ACCEPTANCE.md +24 -0
- aikit/template/docs/design/BRIEF.md +26 -0
- aikit/template/docs/design/SYSTEM.md +30 -0
- aikit/templates/agent-secrets.ignore +19 -0
- aikit/templates/gitignore.private +160 -0
- aikit/templates/gitignore.team +139 -0
- aikit/tests/build_distribution.py +73 -0
- aikit/tests/docker_smoke.py +172 -0
- aikit/tests/fixtures/create-junction.ps1 +18 -0
- aikit/tests/fixtures/golden/claude-settings-local.json +58 -0
- aikit/tests/test_adapters.py +105 -0
- aikit/tests/test_aikit.py +79 -0
- aikit/tests/test_benchmark.py +241 -0
- aikit/tests/test_check_kit.py +203 -0
- aikit/tests/test_context.py +76 -0
- aikit/tests/test_context7.py +153 -0
- aikit/tests/test_context_selection.py +70 -0
- aikit/tests/test_distribution.py +181 -0
- aikit/tests/test_docker_image.py +63 -0
- aikit/tests/test_docker_test_usage.py +54 -0
- aikit/tests/test_doctor.py +148 -0
- aikit/tests/test_evaluate.py +66 -0
- aikit/tests/test_install.py +939 -0
- aikit/tests/test_learning.py +247 -0
- aikit/tests/test_memory.py +371 -0
- aikit/tests/test_metrics.py +209 -0
- aikit/tests/test_native.py +342 -0
- aikit/tests/test_onboarding.py +301 -0
- aikit/tests/test_router.py +336 -0
- aikit/tests/test_security_audit.py +237 -0
- aikit/tests/test_strix.py +245 -0
- aikit/tests/test_web_design.py +219 -0
- aikit/tests/wheel_smoke.py +104 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Build release archives in an isolated copy with synthetic private-file probes."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
|
12
|
+
import check_distribution
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
15
|
+
PRIVATE_PROBES = ("AGENTS.md", "PROJECT_CONTEXT.md", "WIKI.md", "auth.json",
|
|
16
|
+
"ai-kit/.memory/session/private.json", "ai-kit/.metrics/private.json",
|
|
17
|
+
"ai-kit/.upstream-cache/private.md", "ai-kit/.design/private.md",
|
|
18
|
+
"docs/.private/private.md", "docs/auth.json", "docs/.env.release",
|
|
19
|
+
"template/.env", "docs/private.pem", "docs/id_ed25519")
|
|
20
|
+
MARKER = b"SYNTHETIC_PRIVATE_" + b"DISTRIBUTION_PROBE_MUST_NOT_SHIP"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def prepare(destination: Path) -> list[dict]:
|
|
24
|
+
destination = destination.resolve()
|
|
25
|
+
if destination.is_relative_to(ROOT):
|
|
26
|
+
raise ValueError("Distribution output must be outside the source checkout")
|
|
27
|
+
if list(destination.glob("*.whl")) or list(destination.glob("*.tar.gz")):
|
|
28
|
+
raise ValueError("Choose an output directory without existing distribution archives")
|
|
29
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
with tempfile.TemporaryDirectory(prefix="aikit-source-", dir=destination.parent) as temporary:
|
|
31
|
+
source = Path(temporary) / "source"
|
|
32
|
+
source.mkdir()
|
|
33
|
+
names = {p.name for pattern in ("*.md", "*.py") for p in ROOT.glob(pattern)} - check_distribution.LOCAL_FILES
|
|
34
|
+
names.update(("VERSION", "LICENSE", "MANIFEST.in", "pyproject.toml", "Makefile", "Dockerfile",
|
|
35
|
+
".dockerignore", ".gitignore", ".gitattributes"))
|
|
36
|
+
for name in sorted(names):
|
|
37
|
+
shutil.copy2(ROOT / name, source / name)
|
|
38
|
+
ignore = shutil.ignore_patterns(".git", "__pycache__", "*.pyc", "*.egg-info", ".memory", ".metrics",
|
|
39
|
+
".upstream-cache", ".design", ".private", ".env", ".env.*", "auth.json",
|
|
40
|
+
".aws", ".codex", ".claude", ".secrets", ".netrc", ".pypirc",
|
|
41
|
+
"*.pem", "*.p12", "*.pfx", "id_rsa*", "id_ed25519*")
|
|
42
|
+
for name in ("scripts", "tests", "docs", "template", "templates", "integrations", "ai-kit", ".agents", ".github", "docker", "deploy"):
|
|
43
|
+
shutil.copytree(ROOT / name, source / name, ignore=ignore)
|
|
44
|
+
for name in PRIVATE_PROBES:
|
|
45
|
+
path = source / name
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
path.write_bytes(MARKER + b"\n")
|
|
48
|
+
subprocess.run([sys.executable, "-m", "build", "--no-isolation", "--outdir", str(destination)],
|
|
49
|
+
cwd=source, check=True)
|
|
50
|
+
version = (ROOT / "VERSION").read_text(encoding="utf-8").strip()
|
|
51
|
+
result = check_distribution.check_archives(destination, version)
|
|
52
|
+
for item in result:
|
|
53
|
+
members = check_distribution._members(destination / item["file"])
|
|
54
|
+
if any(MARKER in data for data in members.values()):
|
|
55
|
+
raise ValueError("Synthetic private marker leaked into an archive")
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str] | None = None) -> int:
|
|
60
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
61
|
+
parser.add_argument("destination", type=Path)
|
|
62
|
+
args = parser.parse_args(argv)
|
|
63
|
+
try:
|
|
64
|
+
for item in prepare(args.destination):
|
|
65
|
+
print(item["file"] + ": metadata/resources/privacy passed; sha256=" + item["sha256"])
|
|
66
|
+
return 0
|
|
67
|
+
except (OSError, ValueError, subprocess.SubprocessError) as exc:
|
|
68
|
+
print("Distribution build refused: " + str(exc))
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Exercise a local CLI image with isolated fixtures and no runtime network access."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def docker(arguments: list[str], *, expected: int = 0, timeout: int = 180) -> str:
|
|
15
|
+
result = subprocess.run(["docker", *arguments], capture_output=True, text=True,
|
|
16
|
+
encoding="utf-8", timeout=timeout)
|
|
17
|
+
if result.returncode != expected:
|
|
18
|
+
raise RuntimeError(f"Docker exited {result.returncode}, expected {expected}: "
|
|
19
|
+
+ result.stdout + result.stderr)
|
|
20
|
+
return result.stdout
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def runtime_arguments(image: str, project: Path | None = None, *, readonly: bool = False,
|
|
24
|
+
entrypoint: str | None = None, create: bool = False,
|
|
25
|
+
name: str | None = None) -> list[str]:
|
|
26
|
+
arguments = ["create" if create else "run"]
|
|
27
|
+
if not create:
|
|
28
|
+
arguments.append("--rm")
|
|
29
|
+
arguments.extend(["--init", "--network", "none", "--read-only", "--cap-drop", "ALL",
|
|
30
|
+
"--security-opt", "no-new-privileges", "--tmpfs", "/tmp:rw,nosuid,nodev,size=256m",
|
|
31
|
+
"--label", "ai-kit.disposable=true"])
|
|
32
|
+
if hasattr(os, "getuid") and os.getuid() != 0:
|
|
33
|
+
arguments.extend(["--user", f"{os.getuid()}:{os.getgid()}"])
|
|
34
|
+
if project is not None:
|
|
35
|
+
source = project.resolve()
|
|
36
|
+
if not source.is_dir():
|
|
37
|
+
raise ValueError("Smoke project must be an existing directory")
|
|
38
|
+
mount = f"type=bind,source={source},target=/workspace" + (",readonly" if readonly else "")
|
|
39
|
+
arguments.extend(["--mount", mount])
|
|
40
|
+
if entrypoint:
|
|
41
|
+
arguments.extend(["--entrypoint", entrypoint])
|
|
42
|
+
if name:
|
|
43
|
+
arguments.extend(["--name", name, "--label", "ai-kit.test-run=" + name])
|
|
44
|
+
if create:
|
|
45
|
+
arguments.append("--interactive")
|
|
46
|
+
return [*arguments, image]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def context_smoke(source: Path, base: Path, builder: str | None) -> None:
|
|
50
|
+
"""Use Docker's actual ignore engine on synthetic sensitive files, not user secrets."""
|
|
51
|
+
context = base / "build context"
|
|
52
|
+
output = base / "context export"
|
|
53
|
+
context.mkdir()
|
|
54
|
+
for name in ("Dockerfile", ".dockerignore"):
|
|
55
|
+
(context / name).write_bytes((source / name).read_bytes())
|
|
56
|
+
included = ("VERSION", "scripts/install.py", "template/AGENTS.md", "template/PROJECT_CONTEXT.md",
|
|
57
|
+
"template/WIKI.md", "template/.agents/skills/web-design/SKILL.md",
|
|
58
|
+
"template/ai-kit/vendor/security-audit/LICENSE", "docker/check_version.py")
|
|
59
|
+
excluded = ("AGENTS.md", "PROJECT_CONTEXT.md", "WIKI.md", ".env", ".git/config",
|
|
60
|
+
".codex/auth.json", ".aws/credentials", "private-helper.py",
|
|
61
|
+
"docs/.private/brief.txt", "docs/auth.json", "template/.env", "ai-kit/.metrics/tasks.jsonl",
|
|
62
|
+
"ai-kit/.design/screenshots/private.png", "ai-kit/.memory/session/imports/private.json",
|
|
63
|
+
"scripts/__pycache__/private.pyc")
|
|
64
|
+
for name in included + excluded:
|
|
65
|
+
path = context / name
|
|
66
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
path.write_bytes(b"Synthetic build-context probe.\n")
|
|
68
|
+
arguments = ["buildx", "build"]
|
|
69
|
+
if builder:
|
|
70
|
+
arguments.extend(["--builder", builder])
|
|
71
|
+
arguments.extend(["--target", "context-audit", "--output", "type=local,dest=" + str(output), str(context)])
|
|
72
|
+
docker(arguments, timeout=300)
|
|
73
|
+
if not all((output / name).is_file() for name in included):
|
|
74
|
+
raise RuntimeError("Docker ignored required kit sources")
|
|
75
|
+
if any((output / name).exists() for name in excluded):
|
|
76
|
+
raise RuntimeError("Docker exposed a synthetic secret or private state in the build context")
|
|
77
|
+
print("Build context: required sources included; synthetic secrets/private state excluded.", flush=True)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def signal_smoke(image: str, project: Path) -> None:
|
|
81
|
+
name = "aikit-cli-signal-" + uuid.uuid4().hex[:12]
|
|
82
|
+
created = False
|
|
83
|
+
try:
|
|
84
|
+
docker([*runtime_arguments(image, project, readonly=True, create=True, name=name),
|
|
85
|
+
"install", "/workspace", "--interactive"])
|
|
86
|
+
created = True
|
|
87
|
+
docker(["start", name])
|
|
88
|
+
deadline = time.monotonic() + 15
|
|
89
|
+
while "AI-KIT interactive installer" not in docker(["logs", name]):
|
|
90
|
+
if time.monotonic() >= deadline:
|
|
91
|
+
raise RuntimeError("Interactive CLI did not reach its input prompt")
|
|
92
|
+
time.sleep(0.2)
|
|
93
|
+
docker(["stop", "--time", "5", name])
|
|
94
|
+
state = json.loads(docker(["inspect", name]))[0]
|
|
95
|
+
if state["State"]["Running"] or state["State"]["ExitCode"] != 143:
|
|
96
|
+
raise RuntimeError("SIGTERM did not terminate the CLI without a forced kill")
|
|
97
|
+
print("SIGTERM reached the foreground CLI; preview terminated with exit 143.", flush=True)
|
|
98
|
+
finally:
|
|
99
|
+
if created:
|
|
100
|
+
record = json.loads(docker(["inspect", name]))[0]
|
|
101
|
+
if record["Config"]["Labels"].get("ai-kit.test-run") != name:
|
|
102
|
+
raise RuntimeError("Refusing cleanup of an unrelated container")
|
|
103
|
+
docker(["rm", "--force", name])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def smoke(image: str, *, source: Path | None = None, builder: str | None = None) -> None:
|
|
107
|
+
inspection = json.loads(docker(["image", "inspect", image]))[0]
|
|
108
|
+
config = inspection["Config"]
|
|
109
|
+
if config["User"] != "10001:10001" or config["Entrypoint"] != ["aikit"]:
|
|
110
|
+
raise RuntimeError("Image must have a non-root user and direct aikit entry point")
|
|
111
|
+
if config.get("ExposedPorts") or config.get("Healthcheck") or config.get("Volumes"):
|
|
112
|
+
raise RuntimeError("A one-shot CLI must not introduce ports, a service probe or anonymous volumes")
|
|
113
|
+
identity_code = ("import json,os,aikit.aikit_cli; "
|
|
114
|
+
"print(json.dumps({'uid':os.getuid(),'version':aikit.aikit_cli.__version__}))")
|
|
115
|
+
identity = json.loads(docker([*runtime_arguments(image, entrypoint="python"), "-c", identity_code]))
|
|
116
|
+
if identity["uid"] == 0 or identity["version"] != config["Labels"]["org.opencontainers.image.version"]:
|
|
117
|
+
raise RuntimeError("Runtime UID/version does not match the image contract")
|
|
118
|
+
docker([*runtime_arguments(image), "--help"])
|
|
119
|
+
docker([*runtime_arguments(image), "route", "implement a feature"])
|
|
120
|
+
docker([*runtime_arguments(image), "check"])
|
|
121
|
+
|
|
122
|
+
with tempfile.TemporaryDirectory(prefix="aikit-container-") as folder:
|
|
123
|
+
base = Path(folder).resolve()
|
|
124
|
+
project = base / "project with spaces"
|
|
125
|
+
project.mkdir(mode=0o777)
|
|
126
|
+
project.chmod(0o777)
|
|
127
|
+
preserved = {"README.md": b"Existing project README.\r\n", ".mcp.json": b'{"mcpServers":{}}\r\n'}
|
|
128
|
+
for name, data in preserved.items():
|
|
129
|
+
(project / name).write_bytes(data)
|
|
130
|
+
preview = json.loads(docker([*runtime_arguments(image, project, readonly=True), "install", "/workspace",
|
|
131
|
+
"--with-web-design"]))
|
|
132
|
+
if not preview["preview"] or (project / "ai-kit").exists():
|
|
133
|
+
raise RuntimeError("Preview changed the mounted project")
|
|
134
|
+
docker([*runtime_arguments(image, project, readonly=True), "install", "/workspace", "--apply"], expected=1)
|
|
135
|
+
if (project / "ai-kit").exists():
|
|
136
|
+
raise RuntimeError("Read-only mount refusal left project writes")
|
|
137
|
+
docker([*runtime_arguments(image, project), "install", "/workspace", "--agent", "codex", "--agent", "claude",
|
|
138
|
+
"--with-web-design", "--with-context7", "--with-security-audit", "--with-strix", "--apply", "--check"])
|
|
139
|
+
if preserved != {name: (project / name).read_bytes() for name in preserved}:
|
|
140
|
+
raise RuntimeError("Installation replaced existing project/client documents")
|
|
141
|
+
report = json.loads(docker([*runtime_arguments(image, project, readonly=True), "doctor", "/workspace"]))
|
|
142
|
+
if report["errors"]:
|
|
143
|
+
raise RuntimeError("Installed project failed doctor: " + str(report["errors"]))
|
|
144
|
+
selection = json.loads(docker([*runtime_arguments(image, project, readonly=True), "context", "select",
|
|
145
|
+
"/workspace", "--workflow", "web-design"]))
|
|
146
|
+
if selection["workflow"] != "web-design":
|
|
147
|
+
raise RuntimeError("Installed design context was not selected")
|
|
148
|
+
repeated = json.loads(docker([*runtime_arguments(image, project), "install", "/workspace", "--apply"]))
|
|
149
|
+
if repeated["writes"]:
|
|
150
|
+
raise RuntimeError("Repeated installation was not stable")
|
|
151
|
+
signal_smoke(image, project)
|
|
152
|
+
if source:
|
|
153
|
+
context_smoke(source, base, builder)
|
|
154
|
+
print("CLI image smoke passed: offline/read-only runtime, preview, refusal, all extras, preservation and rerun.", flush=True)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv: list[str] | None = None) -> int:
|
|
158
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
159
|
+
parser.add_argument("image", help="Already built local image; this command does not publish images")
|
|
160
|
+
parser.add_argument("--context", type=Path, help="Reference checkout for an actual Docker ignore-engine check")
|
|
161
|
+
parser.add_argument("--builder", help="Optional dedicated builder for the context-export check")
|
|
162
|
+
args = parser.parse_args(argv)
|
|
163
|
+
try:
|
|
164
|
+
smoke(args.image, source=args.context, builder=args.builder)
|
|
165
|
+
return 0
|
|
166
|
+
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc:
|
|
167
|
+
print("Container verification failed: " + str(exc))
|
|
168
|
+
return 1
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)][string]$LinkPath,
|
|
3
|
+
[Parameter(Mandatory = $true)][string]$TargetPath
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
$ErrorActionPreference = 'Stop'
|
|
7
|
+
try {
|
|
8
|
+
New-Item -ItemType Junction -Path $LinkPath -Target $TargetPath | Out-Null
|
|
9
|
+
} catch {
|
|
10
|
+
$taskError = $_.Exception.GetBaseException()
|
|
11
|
+
if (($taskError -is [System.UnauthorizedAccessException]) -or
|
|
12
|
+
(($taskError -is [System.ComponentModel.Win32Exception]) -and
|
|
13
|
+
($taskError.NativeErrorCode -in @(5, 1314)))) {
|
|
14
|
+
Write-Error -Message 'Junction creation unavailable: access or privilege denied.' -ErrorAction Continue
|
|
15
|
+
exit 77
|
|
16
|
+
}
|
|
17
|
+
throw
|
|
18
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"ask": [
|
|
4
|
+
"Bash(git commit)",
|
|
5
|
+
"Bash(git commit *)",
|
|
6
|
+
"Bash(git push)",
|
|
7
|
+
"Bash(git push *)",
|
|
8
|
+
"Bash(rm -rf *)",
|
|
9
|
+
"Bash(rm -fr *)",
|
|
10
|
+
"Bash(git reset --hard*)",
|
|
11
|
+
"Bash(git clean *)",
|
|
12
|
+
"Bash(docker system prune*)",
|
|
13
|
+
"Bash(docker volume rm *)",
|
|
14
|
+
"Bash(docker volume prune*)",
|
|
15
|
+
"Bash(php artisan migrate*)",
|
|
16
|
+
"Bash(php artisan db:wipe*)",
|
|
17
|
+
"Bash(php artisan db:seed*)"
|
|
18
|
+
],
|
|
19
|
+
"allow": [
|
|
20
|
+
"Bash(go test ./... *)",
|
|
21
|
+
"Bash(go vet ./... *)",
|
|
22
|
+
"Bash(go build ./... *)",
|
|
23
|
+
"Bash(php artisan test *)"
|
|
24
|
+
],
|
|
25
|
+
"deny": [
|
|
26
|
+
"Read(.env)",
|
|
27
|
+
"Read(.env.*)",
|
|
28
|
+
"Read(!.env.example)",
|
|
29
|
+
"Read(!.env.*.example)",
|
|
30
|
+
"Read(!.env.dist)",
|
|
31
|
+
"Read(!.env.*.dist)",
|
|
32
|
+
"Read(.envrc)",
|
|
33
|
+
"Read(.netrc)",
|
|
34
|
+
"Read(*.pem)",
|
|
35
|
+
"Read(*.p12)",
|
|
36
|
+
"Read(*.pfx)",
|
|
37
|
+
"Read(*.private.key)",
|
|
38
|
+
"Read(id_rsa*)",
|
|
39
|
+
"Read(id_ed25519*)",
|
|
40
|
+
"Read(/secrets/**)",
|
|
41
|
+
"Read(/.secrets/**)",
|
|
42
|
+
"Read(auth.json)",
|
|
43
|
+
"Read(/web/storage/*.key)"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"hooks": {
|
|
47
|
+
"SessionStart": [
|
|
48
|
+
{
|
|
49
|
+
"hooks": [
|
|
50
|
+
{
|
|
51
|
+
"type": "command",
|
|
52
|
+
"command": "tr -d '\\r' < \"$CLAUDE_PROJECT_DIR/.agents/hooks/session-start.sh\" | sh"
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Exercise adapter documentation reporting and conservative retirement."""
|
|
2
|
+
import contextlib
|
|
3
|
+
import io
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
import unittest
|
|
10
|
+
from unittest import mock
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
|
13
|
+
import adapters
|
|
14
|
+
import install
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AdapterTests(unittest.TestCase):
|
|
18
|
+
def setUp(self):
|
|
19
|
+
self.temp = tempfile.TemporaryDirectory(prefix="ai-kit-adapters-")
|
|
20
|
+
self.addCleanup(self.temp.cleanup)
|
|
21
|
+
self.base = Path(self.temp.name).resolve()
|
|
22
|
+
self.target = self.base / "project"
|
|
23
|
+
self.source = self.base / "reference"
|
|
24
|
+
shutil.copytree(install.ROOT, self.source, ignore=shutil.ignore_patterns("__pycache__", ".git"))
|
|
25
|
+
|
|
26
|
+
def apply(self, **options):
|
|
27
|
+
plan = install.build_plan(self.target, root=self.source, **options)
|
|
28
|
+
self.assertTrue(install.apply_plan(plan))
|
|
29
|
+
return plan
|
|
30
|
+
|
|
31
|
+
def test_report_exposes_documentation_and_runtime_boundaries(self):
|
|
32
|
+
self.apply(agents=["codex", "cursor"])
|
|
33
|
+
report = adapters.report(root=self.source, project=self.target)
|
|
34
|
+
by_name = {item["client"]: item for item in report["clients"]}
|
|
35
|
+
self.assertEqual(by_name["cursor"]["documentation_status"], "verified")
|
|
36
|
+
self.assertEqual(by_name["cursor"]["activation"], "unverified")
|
|
37
|
+
self.assertTrue(by_name["cursor"]["selected"])
|
|
38
|
+
self.assertIn(".cursor/rules/ai-kit.mdc", by_name["cursor"]["managed_paths"])
|
|
39
|
+
self.assertEqual(by_name["kimi"]["documentation_status"], "unverified")
|
|
40
|
+
|
|
41
|
+
def test_retire_removes_only_unchanged_owned_files_and_updates_state(self):
|
|
42
|
+
self.apply(agents=["codex", "cursor"])
|
|
43
|
+
plan = adapters.retirement_plan(self.target, "cursor", root=self.source)
|
|
44
|
+
paths = {item["path"] for item in plan["removals"]}
|
|
45
|
+
self.assertIn(".cursor/rules/ai-kit.mdc", paths)
|
|
46
|
+
self.assertTrue(any("deselected" in warning for warning in plan["warnings"]))
|
|
47
|
+
self.assertTrue(adapters.apply_retirement(plan))
|
|
48
|
+
self.assertFalse((self.target / ".cursor/rules/ai-kit.mdc").exists())
|
|
49
|
+
backup = self.target / next(item["backup"] for item in plan["removals"]
|
|
50
|
+
if item["path"] == ".cursor/rules/ai-kit.mdc")
|
|
51
|
+
self.assertTrue(backup.is_file())
|
|
52
|
+
state = json.loads((self.target / "ai-kit/.install-state.json").read_text(encoding="utf-8"))
|
|
53
|
+
self.assertEqual(state["agents"], ["codex"])
|
|
54
|
+
rerun = install.build_plan(self.target, root=self.source, agents=["codex"])
|
|
55
|
+
self.assertFalse(rerun["adapter_conflicts"])
|
|
56
|
+
|
|
57
|
+
def test_retire_refuses_modified_adapter_and_preserves_state(self):
|
|
58
|
+
self.apply(agents=["codex", "cursor"])
|
|
59
|
+
path = self.target / ".cursor/rules/ai-kit.mdc"
|
|
60
|
+
path.write_text(path.read_text(encoding="utf-8") + "Local note.\n", encoding="utf-8")
|
|
61
|
+
plan = adapters.retirement_plan(self.target, "cursor", root=self.source)
|
|
62
|
+
self.assertIn(".cursor/rules/ai-kit.mdc", plan["conflicts"])
|
|
63
|
+
self.assertFalse(adapters.apply_retirement(plan))
|
|
64
|
+
self.assertTrue(path.is_file())
|
|
65
|
+
state = json.loads((self.target / "ai-kit/.install-state.json").read_text(encoding="utf-8"))
|
|
66
|
+
self.assertIn("cursor", state["agents"])
|
|
67
|
+
|
|
68
|
+
def test_report_and_retire_preserve_a_file_blocking_adapter_directory(self):
|
|
69
|
+
self.apply(agents=["codex", "cline"])
|
|
70
|
+
directory = self.target / ".clinerules"
|
|
71
|
+
(directory / "ai-kit.md").unlink()
|
|
72
|
+
directory.rmdir()
|
|
73
|
+
directory.write_text("native data\n", encoding="utf-8")
|
|
74
|
+
report = adapters.report(root=self.source, project=self.target)
|
|
75
|
+
cline = next(item for item in report["clients"] if item["client"] == "cline")
|
|
76
|
+
self.assertEqual(cline["blocked_paths"], [".clinerules/ai-kit.md"])
|
|
77
|
+
plan = adapters.retirement_plan(self.target, "cline", root=self.source)
|
|
78
|
+
self.assertEqual(plan["conflicts"], [".clinerules/ai-kit.md"])
|
|
79
|
+
self.assertEqual(directory.read_text(encoding="utf-8"), "native data\n")
|
|
80
|
+
|
|
81
|
+
def test_cli_report_and_retire_preview(self):
|
|
82
|
+
self.apply(agents=["codex", "cursor"])
|
|
83
|
+
with contextlib.redirect_stdout(io.StringIO()) as output:
|
|
84
|
+
self.assertEqual(adapters.main(["report", "--project", str(self.target)]), 0)
|
|
85
|
+
self.assertIn('"client": "cursor"', output.getvalue())
|
|
86
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
87
|
+
self.assertEqual(adapters.main(["retire", str(self.target), "--agent", "cursor"]), 0)
|
|
88
|
+
self.assertTrue((self.target / ".cursor/rules/ai-kit.mdc").is_file())
|
|
89
|
+
|
|
90
|
+
def test_audit_obstruction_does_not_hide_a_linked_ancestor(self):
|
|
91
|
+
self.apply(agents=["codex", "cursor"])
|
|
92
|
+
path = self.target / ".cursor/rules/ai-kit.mdc"
|
|
93
|
+
path.unlink()
|
|
94
|
+
path.parent.rmdir()
|
|
95
|
+
path.parent.write_text("native data\n", encoding="utf-8")
|
|
96
|
+
original = install.is_link
|
|
97
|
+
with mock.patch.object(install, "is_link", side_effect=lambda p: p == self.target / ".cursor" or original(p)):
|
|
98
|
+
with self.assertRaisesRegex(ValueError, "linked"):
|
|
99
|
+
adapters.report(root=self.source, project=self.target)
|
|
100
|
+
with self.assertRaisesRegex(ValueError, "linked"):
|
|
101
|
+
adapters.retirement_plan(self.target, "cursor", root=self.source)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
unittest.main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Exercise the unified aikit CLI dispatcher."""
|
|
2
|
+
import contextlib
|
|
3
|
+
import io
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import sys
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest import mock
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
10
|
+
import aikit_cli
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AikitCliTests(unittest.TestCase):
|
|
14
|
+
def test_no_command_prints_help(self):
|
|
15
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
16
|
+
self.assertEqual(aikit_cli.main([]), 2)
|
|
17
|
+
|
|
18
|
+
def test_route_dispatches_to_router(self):
|
|
19
|
+
output = io.StringIO()
|
|
20
|
+
with contextlib.redirect_stdout(output):
|
|
21
|
+
self.assertEqual(aikit_cli.main(["route", "implement a feature"]), 0)
|
|
22
|
+
self.assertIn('"level"', output.getvalue())
|
|
23
|
+
|
|
24
|
+
def test_install_help_forwards_arguments(self):
|
|
25
|
+
output = io.StringIO()
|
|
26
|
+
with contextlib.redirect_stdout(output), self.assertRaises(SystemExit) as context:
|
|
27
|
+
aikit_cli.main(["install", "--help"])
|
|
28
|
+
self.assertEqual(context.exception.code, 0)
|
|
29
|
+
self.assertIn("--preset", output.getvalue())
|
|
30
|
+
|
|
31
|
+
def test_benchmark_help_reaches_worker_commands(self):
|
|
32
|
+
output = io.StringIO()
|
|
33
|
+
with contextlib.redirect_stdout(output), self.assertRaises(SystemExit) as context:
|
|
34
|
+
aikit_cli.main(["benchmark", "--help"])
|
|
35
|
+
self.assertEqual(context.exception.code, 0)
|
|
36
|
+
self.assertIn("{run,report,review,export}", output.getvalue())
|
|
37
|
+
|
|
38
|
+
def test_version_reads_from_version_file(self):
|
|
39
|
+
self.assertEqual(aikit_cli.__version__, (Path(__file__).resolve().parents[1] / "VERSION")
|
|
40
|
+
.read_text(encoding="utf-8").strip())
|
|
41
|
+
|
|
42
|
+
def test_version_flag_reports_bundle_without_loading_or_running_a_command(self):
|
|
43
|
+
output = io.StringIO()
|
|
44
|
+
with mock.patch.object(aikit_cli, "_load") as load, contextlib.redirect_stdout(output), self.assertRaises(SystemExit) as context:
|
|
45
|
+
aikit_cli.main(["--version"])
|
|
46
|
+
self.assertEqual(context.exception.code, 0)
|
|
47
|
+
self.assertEqual(output.getvalue().strip(), "aikit " + aikit_cli.__version__)
|
|
48
|
+
load.assert_not_called()
|
|
49
|
+
|
|
50
|
+
def test_memory_help_reaches_offline_commands(self):
|
|
51
|
+
output = io.StringIO()
|
|
52
|
+
with contextlib.redirect_stdout(output), self.assertRaises(SystemExit) as context:
|
|
53
|
+
aikit_cli.main(["memory", "--help"])
|
|
54
|
+
self.assertEqual(context.exception.code, 0)
|
|
55
|
+
self.assertIn("{import,merge,export}", output.getvalue())
|
|
56
|
+
|
|
57
|
+
def test_providers_dispatches_to_router(self):
|
|
58
|
+
output = io.StringIO()
|
|
59
|
+
with contextlib.redirect_stdout(output):
|
|
60
|
+
self.assertEqual(aikit_cli.main(["providers"]), 0)
|
|
61
|
+
self.assertIn('"provider": "gemini"', output.getvalue())
|
|
62
|
+
|
|
63
|
+
def test_adapters_dispatches_to_client_report(self):
|
|
64
|
+
output = io.StringIO()
|
|
65
|
+
with contextlib.redirect_stdout(output):
|
|
66
|
+
self.assertEqual(aikit_cli.main(["adapters", "report"]), 0)
|
|
67
|
+
self.assertIn('"client": "codex"', output.getvalue())
|
|
68
|
+
|
|
69
|
+
def test_loader_prefers_installed_package_module(self):
|
|
70
|
+
import types
|
|
71
|
+
package_module = types.ModuleType("aikit.scripts.install")
|
|
72
|
+
package_module.main = lambda argv=None: 0
|
|
73
|
+
with mock.patch.dict(sys.modules, {"aikit.scripts.install": package_module}):
|
|
74
|
+
self.assertIs(aikit_cli._load("install"), package_module)
|
|
75
|
+
self.assertTrue(callable(aikit_cli._load("install").main))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
unittest.main()
|