solokit 0.1.1__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.
- solokit/__init__.py +10 -0
- solokit/__version__.py +3 -0
- solokit/cli.py +374 -0
- solokit/core/__init__.py +1 -0
- solokit/core/cache.py +102 -0
- solokit/core/command_runner.py +278 -0
- solokit/core/config.py +453 -0
- solokit/core/config_validator.py +204 -0
- solokit/core/constants.py +291 -0
- solokit/core/error_formatter.py +279 -0
- solokit/core/error_handlers.py +346 -0
- solokit/core/exceptions.py +1567 -0
- solokit/core/file_ops.py +309 -0
- solokit/core/logging_config.py +166 -0
- solokit/core/output.py +99 -0
- solokit/core/performance.py +57 -0
- solokit/core/protocols.py +141 -0
- solokit/core/types.py +312 -0
- solokit/deployment/__init__.py +1 -0
- solokit/deployment/executor.py +411 -0
- solokit/git/__init__.py +1 -0
- solokit/git/integration.py +619 -0
- solokit/init/__init__.py +41 -0
- solokit/init/claude_commands_installer.py +87 -0
- solokit/init/dependency_installer.py +313 -0
- solokit/init/docs_structure.py +90 -0
- solokit/init/env_generator.py +160 -0
- solokit/init/environment_validator.py +334 -0
- solokit/init/git_hooks_installer.py +71 -0
- solokit/init/git_setup.py +188 -0
- solokit/init/gitignore_updater.py +195 -0
- solokit/init/initial_commit.py +145 -0
- solokit/init/initial_scans.py +109 -0
- solokit/init/orchestrator.py +246 -0
- solokit/init/readme_generator.py +207 -0
- solokit/init/session_structure.py +239 -0
- solokit/init/template_installer.py +424 -0
- solokit/learning/__init__.py +1 -0
- solokit/learning/archiver.py +115 -0
- solokit/learning/categorizer.py +126 -0
- solokit/learning/curator.py +428 -0
- solokit/learning/extractor.py +352 -0
- solokit/learning/reporter.py +351 -0
- solokit/learning/repository.py +254 -0
- solokit/learning/similarity.py +342 -0
- solokit/learning/validator.py +144 -0
- solokit/project/__init__.py +1 -0
- solokit/project/init.py +1162 -0
- solokit/project/stack.py +436 -0
- solokit/project/sync_plugin.py +438 -0
- solokit/project/tree.py +375 -0
- solokit/quality/__init__.py +1 -0
- solokit/quality/api_validator.py +424 -0
- solokit/quality/checkers/__init__.py +25 -0
- solokit/quality/checkers/base.py +114 -0
- solokit/quality/checkers/context7.py +221 -0
- solokit/quality/checkers/custom.py +162 -0
- solokit/quality/checkers/deployment.py +323 -0
- solokit/quality/checkers/documentation.py +179 -0
- solokit/quality/checkers/formatting.py +161 -0
- solokit/quality/checkers/integration.py +394 -0
- solokit/quality/checkers/linting.py +159 -0
- solokit/quality/checkers/security.py +261 -0
- solokit/quality/checkers/spec_completeness.py +127 -0
- solokit/quality/checkers/tests.py +184 -0
- solokit/quality/env_validator.py +306 -0
- solokit/quality/gates.py +655 -0
- solokit/quality/reporters/__init__.py +10 -0
- solokit/quality/reporters/base.py +25 -0
- solokit/quality/reporters/console.py +98 -0
- solokit/quality/reporters/json_reporter.py +34 -0
- solokit/quality/results.py +98 -0
- solokit/session/__init__.py +1 -0
- solokit/session/briefing/__init__.py +245 -0
- solokit/session/briefing/documentation_loader.py +53 -0
- solokit/session/briefing/formatter.py +476 -0
- solokit/session/briefing/git_context.py +282 -0
- solokit/session/briefing/learning_loader.py +212 -0
- solokit/session/briefing/milestone_builder.py +78 -0
- solokit/session/briefing/orchestrator.py +137 -0
- solokit/session/briefing/stack_detector.py +51 -0
- solokit/session/briefing/tree_generator.py +52 -0
- solokit/session/briefing/work_item_loader.py +209 -0
- solokit/session/briefing.py +353 -0
- solokit/session/complete.py +1188 -0
- solokit/session/status.py +246 -0
- solokit/session/validate.py +452 -0
- solokit/templates/.claude/commands/end.md +109 -0
- solokit/templates/.claude/commands/init.md +159 -0
- solokit/templates/.claude/commands/learn-curate.md +88 -0
- solokit/templates/.claude/commands/learn-search.md +62 -0
- solokit/templates/.claude/commands/learn-show.md +69 -0
- solokit/templates/.claude/commands/learn.md +136 -0
- solokit/templates/.claude/commands/start.md +114 -0
- solokit/templates/.claude/commands/status.md +22 -0
- solokit/templates/.claude/commands/validate.md +27 -0
- solokit/templates/.claude/commands/work-delete.md +119 -0
- solokit/templates/.claude/commands/work-graph.md +139 -0
- solokit/templates/.claude/commands/work-list.md +26 -0
- solokit/templates/.claude/commands/work-new.md +114 -0
- solokit/templates/.claude/commands/work-next.md +25 -0
- solokit/templates/.claude/commands/work-show.md +24 -0
- solokit/templates/.claude/commands/work-update.md +141 -0
- solokit/templates/CHANGELOG.md +17 -0
- solokit/templates/WORK_ITEM_TYPES.md +141 -0
- solokit/templates/__init__.py +1 -0
- solokit/templates/bug_spec.md +217 -0
- solokit/templates/config.schema.json +150 -0
- solokit/templates/dashboard_refine/base/.gitignore +36 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/layout.tsx +22 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/page.tsx +68 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/users/page.tsx +77 -0
- solokit/templates/dashboard_refine/base/app/globals.css +60 -0
- solokit/templates/dashboard_refine/base/app/layout.tsx +23 -0
- solokit/templates/dashboard_refine/base/app/page.tsx +9 -0
- solokit/templates/dashboard_refine/base/components/client-refine-wrapper.tsx +21 -0
- solokit/templates/dashboard_refine/base/components/layout/header.tsx +44 -0
- solokit/templates/dashboard_refine/base/components/layout/sidebar.tsx +82 -0
- solokit/templates/dashboard_refine/base/components/ui/button.tsx +53 -0
- solokit/templates/dashboard_refine/base/components/ui/card.tsx +78 -0
- solokit/templates/dashboard_refine/base/components/ui/table.tsx +116 -0
- solokit/templates/dashboard_refine/base/components.json +16 -0
- solokit/templates/dashboard_refine/base/lib/refine.tsx +65 -0
- solokit/templates/dashboard_refine/base/lib/utils.ts +13 -0
- solokit/templates/dashboard_refine/base/next.config.ts +10 -0
- solokit/templates/dashboard_refine/base/package.json.template +40 -0
- solokit/templates/dashboard_refine/base/postcss.config.mjs +8 -0
- solokit/templates/dashboard_refine/base/providers/refine-provider.tsx +26 -0
- solokit/templates/dashboard_refine/base/tailwind.config.ts +57 -0
- solokit/templates/dashboard_refine/base/tsconfig.json +27 -0
- solokit/templates/dashboard_refine/docker/Dockerfile +57 -0
- solokit/templates/dashboard_refine/docker/docker-compose.prod.yml +31 -0
- solokit/templates/dashboard_refine/docker/docker-compose.yml +21 -0
- solokit/templates/dashboard_refine/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/dashboard_refine/tier-1-essential/package.json.tier1.template +57 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/setup.ts +26 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/unit/example.test.tsx +73 -0
- solokit/templates/dashboard_refine/tier-2-standard/package.json.tier2.template +62 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/eslint.config.mjs +22 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/package.json.tier3.template +79 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/stryker.conf.json +38 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/dashboard.spec.ts +88 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/user-management.spec.ts +102 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/integration/dashboard.test.tsx +90 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/type-coverage.json +16 -0
- solokit/templates/dashboard_refine/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/dashboard_refine/tier-4-production/k6/dashboard-load-test.js +70 -0
- solokit/templates/dashboard_refine/tier-4-production/next.config.ts +46 -0
- solokit/templates/dashboard_refine/tier-4-production/package.json.tier4.template +89 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/deployment_spec.md +500 -0
- solokit/templates/feature_spec.md +248 -0
- solokit/templates/fullstack_nextjs/base/.gitignore +36 -0
- solokit/templates/fullstack_nextjs/base/app/api/example/route.ts +65 -0
- solokit/templates/fullstack_nextjs/base/app/globals.css +27 -0
- solokit/templates/fullstack_nextjs/base/app/layout.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/app/page.tsx +32 -0
- solokit/templates/fullstack_nextjs/base/components/example-component.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/lib/prisma.ts +17 -0
- solokit/templates/fullstack_nextjs/base/lib/utils.ts +13 -0
- solokit/templates/fullstack_nextjs/base/lib/validations.ts +20 -0
- solokit/templates/fullstack_nextjs/base/next.config.ts +7 -0
- solokit/templates/fullstack_nextjs/base/package.json.template +32 -0
- solokit/templates/fullstack_nextjs/base/postcss.config.mjs +8 -0
- solokit/templates/fullstack_nextjs/base/prisma/schema.prisma +21 -0
- solokit/templates/fullstack_nextjs/base/tailwind.config.ts +19 -0
- solokit/templates/fullstack_nextjs/base/tsconfig.json +27 -0
- solokit/templates/fullstack_nextjs/docker/Dockerfile +60 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.prod.yml +57 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.yml +47 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/package.json.tier1.template +48 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/api/example.test.ts +88 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/unit/example.test.tsx +22 -0
- solokit/templates/fullstack_nextjs/tier-2-standard/package.json.tier2.template +52 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/package.json.tier3.template +68 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/stryker.conf.json +33 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/e2e/flow.spec.ts +59 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/integration/api.test.ts +165 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/fullstack_nextjs/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/fullstack_nextjs/tier-4-production/k6/load-test.js +45 -0
- solokit/templates/fullstack_nextjs/tier-4-production/next.config.ts +46 -0
- solokit/templates/fullstack_nextjs/tier-4-production/package.json.tier4.template +77 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/git-hooks/prepare-commit-msg +24 -0
- solokit/templates/integration_test_spec.md +363 -0
- solokit/templates/learnings.json +15 -0
- solokit/templates/ml_ai_fastapi/base/.gitignore +104 -0
- solokit/templates/ml_ai_fastapi/base/alembic/env.py +96 -0
- solokit/templates/ml_ai_fastapi/base/alembic.ini +114 -0
- solokit/templates/ml_ai_fastapi/base/pyproject.toml.template +91 -0
- solokit/templates/ml_ai_fastapi/base/requirements.txt.template +28 -0
- solokit/templates/ml_ai_fastapi/base/src/__init__.py +5 -0
- solokit/templates/ml_ai_fastapi/base/src/api/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/dependencies.py +20 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/example.py +134 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/health.py +66 -0
- solokit/templates/ml_ai_fastapi/base/src/core/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/core/config.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/core/database.py +50 -0
- solokit/templates/ml_ai_fastapi/base/src/main.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/models/__init__.py +7 -0
- solokit/templates/ml_ai_fastapi/base/src/models/example.py +61 -0
- solokit/templates/ml_ai_fastapi/base/src/services/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/services/example.py +115 -0
- solokit/templates/ml_ai_fastapi/docker/Dockerfile +59 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.prod.yml +112 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.yml +77 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyproject.toml.tier1.template +112 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyrightconfig.json +41 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pytest.ini +69 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/requirements-dev.txt +17 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/ruff.toml +81 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/conftest.py +72 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/test_main.py +49 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/test_example.py +113 -0
- solokit/templates/ml_ai_fastapi/tier-2-standard/pyproject.toml.tier2.template +130 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/locustfile.py +99 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/mutmut_config.py +53 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/pyproject.toml.tier3.template +150 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/conftest.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/test_api.py +131 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/pyproject.toml.tier4.template +162 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/requirements-prod.txt +25 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/api/routes/metrics.py +19 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/logging.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/monitoring.py +68 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/sentry.py +66 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/logging.py +79 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/tracing.py +60 -0
- solokit/templates/refactor_spec.md +287 -0
- solokit/templates/saas_t3/base/.gitignore +36 -0
- solokit/templates/saas_t3/base/app/api/trpc/[trpc]/route.ts +33 -0
- solokit/templates/saas_t3/base/app/globals.css +27 -0
- solokit/templates/saas_t3/base/app/layout.tsx +23 -0
- solokit/templates/saas_t3/base/app/page.tsx +31 -0
- solokit/templates/saas_t3/base/lib/api.tsx +77 -0
- solokit/templates/saas_t3/base/lib/utils.ts +13 -0
- solokit/templates/saas_t3/base/next.config.ts +7 -0
- solokit/templates/saas_t3/base/package.json.template +38 -0
- solokit/templates/saas_t3/base/postcss.config.mjs +8 -0
- solokit/templates/saas_t3/base/prisma/schema.prisma +20 -0
- solokit/templates/saas_t3/base/server/api/root.ts +19 -0
- solokit/templates/saas_t3/base/server/api/routers/example.ts +28 -0
- solokit/templates/saas_t3/base/server/api/trpc.ts +52 -0
- solokit/templates/saas_t3/base/server/db.ts +17 -0
- solokit/templates/saas_t3/base/tailwind.config.ts +19 -0
- solokit/templates/saas_t3/base/tsconfig.json +27 -0
- solokit/templates/saas_t3/docker/Dockerfile +60 -0
- solokit/templates/saas_t3/docker/docker-compose.prod.yml +59 -0
- solokit/templates/saas_t3/docker/docker-compose.yml +49 -0
- solokit/templates/saas_t3/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/saas_t3/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/saas_t3/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/saas_t3/tier-1-essential/package.json.tier1.template +54 -0
- solokit/templates/saas_t3/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/saas_t3/tier-1-essential/tests/unit/example.test.tsx +24 -0
- solokit/templates/saas_t3/tier-2-standard/package.json.tier2.template +58 -0
- solokit/templates/saas_t3/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/saas_t3/tier-3-comprehensive/package.json.tier3.template +74 -0
- solokit/templates/saas_t3/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/saas_t3/tier-3-comprehensive/stryker.conf.json +34 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/e2e/home.spec.ts +41 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/integration/api.test.ts +44 -0
- solokit/templates/saas_t3/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/saas_t3/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/saas_t3/tier-4-production/k6/load-test.js +51 -0
- solokit/templates/saas_t3/tier-4-production/next.config.ts +46 -0
- solokit/templates/saas_t3/tier-4-production/package.json.tier4.template +83 -0
- solokit/templates/saas_t3/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/saas_t3/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/vercel.json +37 -0
- solokit/templates/security_spec.md +287 -0
- solokit/templates/stack-versions.yaml +617 -0
- solokit/templates/status_update.json +6 -0
- solokit/templates/template-registry.json +257 -0
- solokit/templates/work_items.json +11 -0
- solokit/testing/__init__.py +1 -0
- solokit/testing/integration_runner.py +550 -0
- solokit/testing/performance.py +637 -0
- solokit/visualization/__init__.py +1 -0
- solokit/visualization/dependency_graph.py +788 -0
- solokit/work_items/__init__.py +1 -0
- solokit/work_items/creator.py +217 -0
- solokit/work_items/delete.py +264 -0
- solokit/work_items/get_dependencies.py +185 -0
- solokit/work_items/get_dependents.py +113 -0
- solokit/work_items/get_metadata.py +121 -0
- solokit/work_items/get_next_recommendations.py +133 -0
- solokit/work_items/manager.py +235 -0
- solokit/work_items/milestones.py +137 -0
- solokit/work_items/query.py +376 -0
- solokit/work_items/repository.py +267 -0
- solokit/work_items/scheduler.py +184 -0
- solokit/work_items/spec_parser.py +838 -0
- solokit/work_items/spec_validator.py +493 -0
- solokit/work_items/updater.py +157 -0
- solokit/work_items/validator.py +205 -0
- solokit-0.1.1.dist-info/METADATA +640 -0
- solokit-0.1.1.dist-info/RECORD +323 -0
- solokit-0.1.1.dist-info/WHEEL +5 -0
- solokit-0.1.1.dist-info/entry_points.txt +2 -0
- solokit-0.1.1.dist-info/licenses/LICENSE +21 -0
- solokit-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Environment Validator Module
|
|
3
|
+
|
|
4
|
+
Validates and optionally auto-updates environment (Python, Node.js) for template installation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import shutil
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from solokit.core.command_runner import CommandRunner
|
|
15
|
+
from solokit.core.exceptions import ErrorCode, ValidationError
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
MIN_NODE_VERSION = (18, 0, 0)
|
|
20
|
+
MIN_PYTHON_VERSION = (3, 11, 0)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_version(version_str: str) -> tuple[int, int, int]:
|
|
24
|
+
"""
|
|
25
|
+
Parse version string into tuple of (major, minor, patch).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
version_str: Version string like "18.0.0" or "v18.0.0" or "3.11.7"
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Tuple of (major, minor, patch) as integers
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ValueError: If version string is malformed
|
|
35
|
+
"""
|
|
36
|
+
# Remove 'v' prefix if present
|
|
37
|
+
clean_version = version_str.strip().lstrip("v")
|
|
38
|
+
|
|
39
|
+
# Split by '.' and take first 3 parts
|
|
40
|
+
parts = clean_version.split(".")
|
|
41
|
+
if len(parts) < 2:
|
|
42
|
+
raise ValueError(f"Invalid version format: {version_str}")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
major = int(parts[0])
|
|
46
|
+
minor = int(parts[1])
|
|
47
|
+
patch = int(parts[2]) if len(parts) > 2 else 0
|
|
48
|
+
return (major, minor, patch)
|
|
49
|
+
except (ValueError, IndexError) as e:
|
|
50
|
+
raise ValueError(f"Invalid version format: {version_str}") from e
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_node_version() -> tuple[bool, str | None]:
|
|
54
|
+
"""
|
|
55
|
+
Check if Node.js is installed and meets minimum version requirement.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Tuple of (meets_requirement: bool, current_version: str | None)
|
|
59
|
+
- meets_requirement: True if Node.js >= 18.0.0
|
|
60
|
+
- current_version: Version string or None if not installed
|
|
61
|
+
"""
|
|
62
|
+
# Check if node is available
|
|
63
|
+
if not shutil.which("node"):
|
|
64
|
+
return False, None
|
|
65
|
+
|
|
66
|
+
runner = CommandRunner(default_timeout=5)
|
|
67
|
+
result = runner.run(["node", "--version"], check=False)
|
|
68
|
+
|
|
69
|
+
if not result.success:
|
|
70
|
+
return False, None
|
|
71
|
+
|
|
72
|
+
version_str = result.stdout.strip()
|
|
73
|
+
try:
|
|
74
|
+
version = parse_version(version_str)
|
|
75
|
+
meets_req = version >= MIN_NODE_VERSION
|
|
76
|
+
return meets_req, version_str
|
|
77
|
+
except ValueError:
|
|
78
|
+
logger.warning(f"Could not parse Node.js version: {version_str}")
|
|
79
|
+
return False, version_str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def check_python_version(
|
|
83
|
+
specific_binary: str | None = None,
|
|
84
|
+
) -> tuple[bool, str | None, str | None]:
|
|
85
|
+
"""
|
|
86
|
+
Check if Python is installed and meets minimum version requirement.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
specific_binary: Specific Python binary to check (e.g., "python3.11").
|
|
90
|
+
If None, checks "python3" and "python".
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Tuple of (meets_requirement: bool, current_version: str | None, binary_path: str | None)
|
|
94
|
+
- meets_requirement: True if Python >= 3.11.0
|
|
95
|
+
- current_version: Version string or None if not installed
|
|
96
|
+
- binary_path: Path to the Python binary or None
|
|
97
|
+
"""
|
|
98
|
+
binaries_to_check = (
|
|
99
|
+
[specific_binary] if specific_binary else ["python3", "python", "python3.11"]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
for binary in binaries_to_check:
|
|
103
|
+
binary_path = shutil.which(binary)
|
|
104
|
+
if not binary_path:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
runner = CommandRunner(default_timeout=5)
|
|
108
|
+
result = runner.run([binary, "--version"], check=False)
|
|
109
|
+
|
|
110
|
+
if not result.success:
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# Python --version outputs to stdout (Python 3.x) or stderr (Python 2.x)
|
|
114
|
+
version_output = result.stdout or result.stderr
|
|
115
|
+
version_str = version_output.strip().replace("Python ", "")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
version = parse_version(version_str)
|
|
119
|
+
meets_req = version >= MIN_PYTHON_VERSION
|
|
120
|
+
return meets_req, version_str, binary_path
|
|
121
|
+
except ValueError:
|
|
122
|
+
logger.warning(f"Could not parse Python version from {binary}: {version_str}")
|
|
123
|
+
continue
|
|
124
|
+
|
|
125
|
+
return False, None, None
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def attempt_node_install_with_nvm() -> tuple[bool, str]:
|
|
129
|
+
"""
|
|
130
|
+
Attempt to install Node.js >= 18.0.0 using nvm if available.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Tuple of (success: bool, message: str)
|
|
134
|
+
"""
|
|
135
|
+
# Check if nvm is available
|
|
136
|
+
nvm_dir = Path.home() / ".nvm"
|
|
137
|
+
if not nvm_dir.exists():
|
|
138
|
+
return (
|
|
139
|
+
False,
|
|
140
|
+
"nvm not found. Please install Node.js 18+ manually:\n"
|
|
141
|
+
" macOS: brew install node@18\n"
|
|
142
|
+
" Ubuntu: curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -\n"
|
|
143
|
+
" sudo apt-get install -y nodejs",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Try to source nvm and install Node.js 18
|
|
147
|
+
runner = CommandRunner(default_timeout=300)
|
|
148
|
+
|
|
149
|
+
# nvm install command needs to be run in a shell environment
|
|
150
|
+
nvm_script = f"""
|
|
151
|
+
export NVM_DIR="{nvm_dir}"
|
|
152
|
+
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
|
|
153
|
+
nvm install 18
|
|
154
|
+
nvm use 18
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
result = runner.run(["bash", "-c", nvm_script], check=False)
|
|
158
|
+
|
|
159
|
+
if result.success:
|
|
160
|
+
logger.info("Successfully installed Node.js 18 via nvm")
|
|
161
|
+
return True, "Node.js 18 installed successfully via nvm"
|
|
162
|
+
else:
|
|
163
|
+
return (
|
|
164
|
+
False,
|
|
165
|
+
f"nvm installation failed: {result.stderr}\n\n"
|
|
166
|
+
"Please install Node.js 18+ manually:\n"
|
|
167
|
+
" macOS: brew install node@18\n"
|
|
168
|
+
" Ubuntu: curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -\n"
|
|
169
|
+
" sudo apt-get install -y nodejs",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def attempt_python_install_with_pyenv(version: str = "3.11") -> tuple[bool, str]:
|
|
174
|
+
"""
|
|
175
|
+
Attempt to install Python using pyenv if available.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
version: Python version to install (e.g., "3.11")
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Tuple of (success: bool, message: str)
|
|
182
|
+
"""
|
|
183
|
+
# Check if pyenv is available
|
|
184
|
+
if not shutil.which("pyenv"):
|
|
185
|
+
return (
|
|
186
|
+
False,
|
|
187
|
+
f"pyenv not found. Please install Python {version}+ manually:\n"
|
|
188
|
+
f" macOS: brew install python@{version}\n"
|
|
189
|
+
f" Ubuntu: sudo apt install python{version} python{version}-venv",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Try to install Python via pyenv
|
|
193
|
+
runner = CommandRunner(default_timeout=600) # Python builds can take time
|
|
194
|
+
|
|
195
|
+
# Install latest 3.11.x
|
|
196
|
+
result = runner.run(["pyenv", "install", "-s", version], check=False)
|
|
197
|
+
|
|
198
|
+
if result.success:
|
|
199
|
+
logger.info(f"Successfully installed Python {version} via pyenv")
|
|
200
|
+
return True, f"Python {version} installed successfully via pyenv"
|
|
201
|
+
else:
|
|
202
|
+
return (
|
|
203
|
+
False,
|
|
204
|
+
f"pyenv installation failed: {result.stderr}\n\n"
|
|
205
|
+
f"Please install Python {version}+ manually:\n"
|
|
206
|
+
f" macOS: brew install python@{version}\n"
|
|
207
|
+
f" Ubuntu: sudo apt install python{version} python{version}-venv",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def validate_environment(
|
|
212
|
+
stack_type: Literal["saas_t3", "ml_ai_fastapi", "dashboard_refine", "fullstack_nextjs"],
|
|
213
|
+
auto_update: bool = True,
|
|
214
|
+
) -> dict[str, bool | str | None | list[str]]:
|
|
215
|
+
"""
|
|
216
|
+
Validate environment for a specific stack and optionally auto-update.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
stack_type: Type of stack to validate for
|
|
220
|
+
auto_update: If True, attempt to auto-install missing requirements
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Dictionary with validation results:
|
|
224
|
+
{
|
|
225
|
+
"node_ok": bool,
|
|
226
|
+
"node_version": str | None,
|
|
227
|
+
"python_ok": bool,
|
|
228
|
+
"python_version": str | None,
|
|
229
|
+
"python_binary": str | None,
|
|
230
|
+
"errors": list[str],
|
|
231
|
+
"warnings": list[str]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
ValidationError: If required environment cannot be satisfied
|
|
236
|
+
"""
|
|
237
|
+
errors: list[str] = []
|
|
238
|
+
warnings: list[str] = []
|
|
239
|
+
|
|
240
|
+
result: dict[str, bool | str | None | list[str]] = {
|
|
241
|
+
"node_ok": True,
|
|
242
|
+
"node_version": None,
|
|
243
|
+
"python_ok": True,
|
|
244
|
+
"python_version": None,
|
|
245
|
+
"python_binary": None,
|
|
246
|
+
"errors": errors,
|
|
247
|
+
"warnings": warnings,
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Check Node.js for Next.js-based stacks
|
|
251
|
+
if stack_type in ["saas_t3", "dashboard_refine", "fullstack_nextjs"]:
|
|
252
|
+
node_ok, node_version = check_node_version()
|
|
253
|
+
result["node_ok"] = node_ok
|
|
254
|
+
result["node_version"] = node_version
|
|
255
|
+
|
|
256
|
+
if not node_ok:
|
|
257
|
+
if auto_update:
|
|
258
|
+
logger.info("Node.js 18+ not found, attempting to install via nvm...")
|
|
259
|
+
success, message = attempt_node_install_with_nvm()
|
|
260
|
+
if success:
|
|
261
|
+
# Re-check after installation
|
|
262
|
+
node_ok, node_version = check_node_version()
|
|
263
|
+
result["node_ok"] = node_ok
|
|
264
|
+
result["node_version"] = node_version
|
|
265
|
+
if node_ok:
|
|
266
|
+
logger.info(f"Node.js {node_version} is now available")
|
|
267
|
+
else:
|
|
268
|
+
errors.append(
|
|
269
|
+
f"Auto-installation succeeded but Node.js still not detected\n{message}"
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
errors.append(message)
|
|
273
|
+
else:
|
|
274
|
+
errors.append(
|
|
275
|
+
f"Node.js 18+ required but {'not installed' if node_version is None else f'found {node_version}'}\n"
|
|
276
|
+
"Install Node.js 18+ and try again"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Check Python for ML/AI stack
|
|
280
|
+
if stack_type == "ml_ai_fastapi":
|
|
281
|
+
python_ok, python_version, python_binary = check_python_version()
|
|
282
|
+
result["python_ok"] = python_ok
|
|
283
|
+
result["python_version"] = python_version
|
|
284
|
+
result["python_binary"] = python_binary
|
|
285
|
+
|
|
286
|
+
if not python_ok:
|
|
287
|
+
# Try to find python3.11 specifically
|
|
288
|
+
python_ok_311, python_version_311, python_binary_311 = check_python_version(
|
|
289
|
+
"python3.11"
|
|
290
|
+
)
|
|
291
|
+
if python_ok_311:
|
|
292
|
+
result["python_ok"] = True
|
|
293
|
+
result["python_version"] = python_version_311
|
|
294
|
+
result["python_binary"] = python_binary_311
|
|
295
|
+
logger.info(f"Found Python {python_version_311} at {python_binary_311}")
|
|
296
|
+
elif auto_update:
|
|
297
|
+
logger.info("Python 3.11+ not found, attempting to install via pyenv...")
|
|
298
|
+
success, message = attempt_python_install_with_pyenv()
|
|
299
|
+
if success:
|
|
300
|
+
# Re-check after installation
|
|
301
|
+
python_ok, python_version, python_binary = check_python_version("python3.11")
|
|
302
|
+
result["python_ok"] = python_ok
|
|
303
|
+
result["python_version"] = python_version
|
|
304
|
+
result["python_binary"] = python_binary
|
|
305
|
+
if python_ok:
|
|
306
|
+
logger.info(f"Python {python_version} is now available")
|
|
307
|
+
else:
|
|
308
|
+
errors.append(
|
|
309
|
+
f"Auto-installation succeeded but Python 3.11+ still not detected\n{message}"
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
errors.append(message)
|
|
313
|
+
else:
|
|
314
|
+
errors.append(
|
|
315
|
+
f"Python 3.11+ required but {'not installed' if python_version is None else f'found {python_version}'}\n"
|
|
316
|
+
"Install Python 3.11+ and try again"
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
# Raise validation error if there are blocking errors
|
|
320
|
+
if errors:
|
|
321
|
+
error_msg = "Environment validation failed:\n\n" + "\n\n".join(errors)
|
|
322
|
+
|
|
323
|
+
raise ValidationError(
|
|
324
|
+
message=error_msg,
|
|
325
|
+
code=ErrorCode.INVALID_CONFIGURATION,
|
|
326
|
+
context={
|
|
327
|
+
"stack_type": stack_type,
|
|
328
|
+
"node_ok": result["node_ok"],
|
|
329
|
+
"python_ok": result["python_ok"],
|
|
330
|
+
},
|
|
331
|
+
remediation="Install required runtime versions and try again",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
return result
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Hooks Installer Module
|
|
3
|
+
|
|
4
|
+
Installs git hooks from templates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import shutil
|
|
11
|
+
import stat
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from solokit.core.exceptions import FileOperationError, NotAGitRepoError, TemplateNotFoundError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def install_git_hooks(project_root: Path | None = None) -> list[Path]:
|
|
20
|
+
"""
|
|
21
|
+
Install git hooks from templates.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
project_root: Root directory of the project. Defaults to current working directory.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
List of installed hook paths
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
NotAGitRepoError: If .git/hooks directory doesn't exist (git not initialized).
|
|
31
|
+
TemplateNotFoundError: If hook template file is not found.
|
|
32
|
+
FileOperationError: If hook installation or permission setting fails.
|
|
33
|
+
"""
|
|
34
|
+
if project_root is None:
|
|
35
|
+
project_root = Path.cwd()
|
|
36
|
+
|
|
37
|
+
git_hooks_dir = project_root / ".git" / "hooks"
|
|
38
|
+
|
|
39
|
+
# Check if .git/hooks exists
|
|
40
|
+
if not git_hooks_dir.exists():
|
|
41
|
+
raise NotAGitRepoError(str(project_root))
|
|
42
|
+
|
|
43
|
+
# Get template directory
|
|
44
|
+
template_dir = Path(__file__).parent.parent / "templates" / "git-hooks"
|
|
45
|
+
|
|
46
|
+
installed_hooks = []
|
|
47
|
+
|
|
48
|
+
# Install prepare-commit-msg hook
|
|
49
|
+
hook_template = template_dir / "prepare-commit-msg"
|
|
50
|
+
hook_dest = git_hooks_dir / "prepare-commit-msg"
|
|
51
|
+
|
|
52
|
+
if not hook_template.exists():
|
|
53
|
+
raise TemplateNotFoundError(
|
|
54
|
+
template_name="prepare-commit-msg", template_path=str(template_dir)
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
shutil.copy(hook_template, hook_dest)
|
|
59
|
+
# Make executable (chmod +x)
|
|
60
|
+
hook_dest.chmod(hook_dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
61
|
+
installed_hooks.append(hook_dest)
|
|
62
|
+
logger.info("Installed git prepare-commit-msg hook")
|
|
63
|
+
except Exception as e:
|
|
64
|
+
raise FileOperationError(
|
|
65
|
+
operation="install",
|
|
66
|
+
file_path=str(hook_dest),
|
|
67
|
+
details=f"Failed to copy or set permissions: {str(e)}",
|
|
68
|
+
cause=e,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return installed_hooks
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Git Setup Module
|
|
3
|
+
|
|
4
|
+
Handles git initialization and pre-flight validation checks for template-based init.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from solokit.core.command_runner import CommandRunner
|
|
13
|
+
from solokit.core.constants import GIT_QUICK_TIMEOUT
|
|
14
|
+
from solokit.core.exceptions import ErrorCode, GitError, ValidationError
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_blank_project(project_root: Path | None = None) -> tuple[bool, list[str]]:
|
|
20
|
+
"""
|
|
21
|
+
Check if the current directory is blank enough for initialization.
|
|
22
|
+
|
|
23
|
+
A blank project can have:
|
|
24
|
+
- .git directory
|
|
25
|
+
- .gitignore, README.md, LICENSE, .gitattributes
|
|
26
|
+
- docs/ directory
|
|
27
|
+
- Empty directories with .gitkeep
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
project_root: Root directory to check. Defaults to current working directory.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tuple of (is_blank: bool, blocking_files: list[str])
|
|
34
|
+
- is_blank: True if project is blank/safe to initialize
|
|
35
|
+
- blocking_files: List of files/directories that block initialization
|
|
36
|
+
"""
|
|
37
|
+
if project_root is None:
|
|
38
|
+
project_root = Path.cwd()
|
|
39
|
+
|
|
40
|
+
blocking_files: list[str] = []
|
|
41
|
+
|
|
42
|
+
# Check for existing project files that indicate non-blank project
|
|
43
|
+
blocking_file_patterns = [
|
|
44
|
+
"package.json",
|
|
45
|
+
"package-lock.json",
|
|
46
|
+
"yarn.lock",
|
|
47
|
+
"pnpm-lock.yaml",
|
|
48
|
+
"pyproject.toml",
|
|
49
|
+
"setup.py",
|
|
50
|
+
"requirements.txt",
|
|
51
|
+
"Pipfile",
|
|
52
|
+
"poetry.lock",
|
|
53
|
+
"tsconfig.json",
|
|
54
|
+
".eslintrc.js",
|
|
55
|
+
".eslintrc.json",
|
|
56
|
+
".prettierrc",
|
|
57
|
+
"jest.config.js",
|
|
58
|
+
"vitest.config.ts",
|
|
59
|
+
".session",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Check for blocking files
|
|
63
|
+
for file_pattern in blocking_file_patterns:
|
|
64
|
+
file_path = project_root / file_pattern
|
|
65
|
+
if file_path.exists():
|
|
66
|
+
# Add description for better error messages
|
|
67
|
+
if file_pattern == "package.json":
|
|
68
|
+
blocking_files.append("package.json (Node.js project detected)")
|
|
69
|
+
elif file_pattern == "pyproject.toml":
|
|
70
|
+
blocking_files.append("pyproject.toml (Python project detected)")
|
|
71
|
+
elif file_pattern == ".session":
|
|
72
|
+
blocking_files.append(".session/ (Solokit already initialized)")
|
|
73
|
+
else:
|
|
74
|
+
blocking_files.append(file_pattern)
|
|
75
|
+
|
|
76
|
+
# Check for source directories (strong signal of existing project)
|
|
77
|
+
blocking_dir_patterns = [
|
|
78
|
+
"src",
|
|
79
|
+
"app",
|
|
80
|
+
"pages",
|
|
81
|
+
"components",
|
|
82
|
+
"lib",
|
|
83
|
+
"utils",
|
|
84
|
+
"node_modules",
|
|
85
|
+
"venv",
|
|
86
|
+
".venv",
|
|
87
|
+
"__pycache__",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
for dir_pattern in blocking_dir_patterns:
|
|
91
|
+
dir_path = project_root / dir_pattern
|
|
92
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
93
|
+
# Check if directory has actual content (not just .gitkeep)
|
|
94
|
+
try:
|
|
95
|
+
contents = list(dir_path.iterdir())
|
|
96
|
+
if contents and not (len(contents) == 1 and contents[0].name == ".gitkeep"):
|
|
97
|
+
blocking_files.append(f"{dir_pattern}/ directory")
|
|
98
|
+
except PermissionError:
|
|
99
|
+
# Can't read directory - treat as blocking
|
|
100
|
+
blocking_files.append(f"{dir_pattern}/ directory (permission denied)")
|
|
101
|
+
|
|
102
|
+
is_blank = len(blocking_files) == 0
|
|
103
|
+
return is_blank, blocking_files
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def check_blank_project_or_exit(project_root: Path | None = None) -> None:
|
|
107
|
+
"""
|
|
108
|
+
Check if project is blank, raise exception with helpful error message if not.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
project_root: Root directory to check. Defaults to current working directory.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
ValidationError: If project is not blank with helpful error message and remediation steps.
|
|
115
|
+
"""
|
|
116
|
+
is_blank, blocking_files = is_blank_project(project_root)
|
|
117
|
+
|
|
118
|
+
if not is_blank:
|
|
119
|
+
error_msg = (
|
|
120
|
+
"Cannot initialize: Project directory is not blank.\n\n"
|
|
121
|
+
"Found existing project files:\n"
|
|
122
|
+
+ "\n".join(f" - {f}" for f in blocking_files)
|
|
123
|
+
+ "\n\n"
|
|
124
|
+
"Solokit initialization must be run in a blank project directory to avoid conflicts.\n\n"
|
|
125
|
+
"Solutions:\n"
|
|
126
|
+
" 1. Create a new directory: mkdir my-project && cd my-project\n"
|
|
127
|
+
" 2. Clone an empty repo: git clone <repo-url> && cd <repo>\n"
|
|
128
|
+
" 3. Clear existing project (CAUTION): Remove conflicting files manually\n"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
raise ValidationError(
|
|
132
|
+
message=error_msg,
|
|
133
|
+
code=ErrorCode.PROJECT_NOT_BLANK,
|
|
134
|
+
context={"existing_files": blocking_files},
|
|
135
|
+
remediation="Use a blank directory for initialization",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def check_or_init_git(project_root: Path | None = None) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Check if git is initialized, if not initialize it.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
project_root: Root directory of the project. Defaults to current working directory.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
True if git repository exists or was successfully initialized.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
GitError: If git initialization or branch configuration fails.
|
|
151
|
+
|
|
152
|
+
Note:
|
|
153
|
+
This function logs success messages but raises exceptions on errors.
|
|
154
|
+
"""
|
|
155
|
+
if project_root is None:
|
|
156
|
+
project_root = Path.cwd()
|
|
157
|
+
|
|
158
|
+
git_dir = project_root / ".git"
|
|
159
|
+
|
|
160
|
+
if git_dir.exists():
|
|
161
|
+
logger.info("Git repository already initialized")
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
runner = CommandRunner(default_timeout=GIT_QUICK_TIMEOUT, working_dir=project_root)
|
|
165
|
+
|
|
166
|
+
# Initialize git
|
|
167
|
+
result = runner.run(["git", "init"], check=True)
|
|
168
|
+
if not result.success:
|
|
169
|
+
raise GitError(
|
|
170
|
+
message="Failed to initialize git repository",
|
|
171
|
+
code=ErrorCode.GIT_COMMAND_FAILED,
|
|
172
|
+
context={"stderr": result.stderr, "command": "git init"},
|
|
173
|
+
remediation="Ensure git is installed and you have write permissions in the directory",
|
|
174
|
+
)
|
|
175
|
+
logger.info("Initialized git repository")
|
|
176
|
+
|
|
177
|
+
# Set default branch to main (modern convention)
|
|
178
|
+
result = runner.run(["git", "branch", "-m", "main"], check=True)
|
|
179
|
+
if not result.success:
|
|
180
|
+
raise GitError(
|
|
181
|
+
message="Failed to set default branch to 'main'",
|
|
182
|
+
code=ErrorCode.GIT_COMMAND_FAILED,
|
|
183
|
+
context={"stderr": result.stderr, "command": "git branch -m main"},
|
|
184
|
+
remediation="Manually run 'git branch -m main' in the repository",
|
|
185
|
+
)
|
|
186
|
+
logger.info("Set default branch to 'main'")
|
|
187
|
+
|
|
188
|
+
return True
|