@glxmart/boss-cli 1.0.0-beta.0
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/LICENSE +21 -0
- package/README.md +134 -0
- package/assets/claude-folder/agents/.gitkeep +0 -0
- package/assets/claude-folder/commands/boss-commands.md +138 -0
- package/assets/claude-folder/rules/boss-workflow.md +33 -0
- package/assets/claude-folder/rules/code-style.md +23 -0
- package/assets/claude-folder/rules/security.md +22 -0
- package/assets/claude-folder/rules/testing.md +32 -0
- package/assets/claude-folder/settings.local.json +88 -0
- package/assets/claude-folder/skills/.gitkeep +0 -0
- package/assets/claude-md/docs/container-use.md +140 -0
- package/assets/claude-md/docs/github-operations.md +238 -0
- package/assets/claude-md/docs/initialization.md +186 -0
- package/assets/claude-md/docs/quality-standards.md +15 -0
- package/assets/claude-md/docs/spec-kit.md +46 -0
- package/assets/claude-md/docs/workers.md +174 -0
- package/assets/claude-md/docs/workflow.md +140 -0
- package/assets/claude-md/template.md +812 -0
- package/assets/docker-compose/docker-compose.yml +52 -0
- package/assets/git-hooks/commit-msg.sh +102 -0
- package/assets/git-hooks/pre-commit-check.sh +21 -0
- package/assets/git-hooks/pre-commit.sh +6 -0
- package/assets/git-hooks/pre-push.sh +154 -0
- package/assets/git-hooks/security-check.sh +40 -0
- package/assets/git-hooks/test-changed.sh +84 -0
- package/assets/github-workflows/CODEOWNERS +3 -0
- package/assets/github-workflows/boss-ci.yml +40 -0
- package/assets/github-workflows/boss-gates.yml +28 -0
- package/assets/start-boss-sh/start-boss.sh +471 -0
- package/assets/template-docs/api-service-fastify.md +98 -0
- package/assets/template-docs/blank.md +91 -0
- package/assets/template-docs/nextjs-app-turbo.md +102 -0
- package/assets/template-docs/t3-app.md +106 -0
- package/assets/template-loader/README.md +28 -0
- package/assets/template-loader/eslint.config.node.js +28 -0
- package/assets/template-loader/eslint.config.react.js +47 -0
- package/assets/template-loader/gitignore +42 -0
- package/assets/template-loader/index.test.ts +10 -0
- package/assets/template-loader/index.ts +13 -0
- package/assets/template-loader/prettierignore +8 -0
- package/assets/template-loader/vitest.config.ts +19 -0
- package/assets/worker-configs/architect/.claude/commands/.gitkeep +0 -0
- package/assets/worker-configs/architect/.claude/skills/.gitkeep +0 -0
- package/assets/worker-configs/architect/CLAUDE.md +106 -0
- package/assets/worker-configs/clarifier/.claude/commands/.gitkeep +1 -0
- package/assets/worker-configs/clarifier/.claude/skills/.gitkeep +1 -0
- package/assets/worker-configs/clarifier/CLAUDE.md +112 -0
- package/assets/worker-configs/code-reviewer/CLAUDE.md +113 -0
- package/assets/worker-configs/consolidator/.claude/commands/.gitkeep +1 -0
- package/assets/worker-configs/consolidator/.claude/skills/.gitkeep +1 -0
- package/assets/worker-configs/consolidator/CLAUDE.md +114 -0
- package/assets/worker-configs/developer-backend/.claude/commands/.gitkeep +1 -0
- package/assets/worker-configs/developer-backend/.claude/skills/.gitkeep +1 -0
- package/assets/worker-configs/developer-backend/CLAUDE.md +116 -0
- package/assets/worker-configs/developer-frontend/.claude/commands/.gitkeep +1 -0
- package/assets/worker-configs/developer-frontend/.claude/skills/.gitkeep +1 -0
- package/assets/worker-configs/developer-frontend/CLAUDE.md +116 -0
- package/assets/worker-configs/developer-fullstack/.claude/commands/.gitkeep +1 -0
- package/assets/worker-configs/developer-fullstack/.claude/skills/.gitkeep +1 -0
- package/assets/worker-configs/developer-fullstack/CLAUDE.md +116 -0
- package/assets/worker-configs/devops-engineer/CLAUDE.md +111 -0
- package/assets/worker-configs/planner/.claude/commands/.gitkeep +1 -0
- package/assets/worker-configs/planner/.claude/skills/.gitkeep +1 -0
- package/assets/worker-configs/planner/CLAUDE.md +111 -0
- package/assets/worker-configs/product-owner/CLAUDE.md +108 -0
- package/assets/worker-configs/reviewer/.claude/commands/.gitkeep +1 -0
- package/assets/worker-configs/reviewer/.claude/skills/.gitkeep +1 -0
- package/assets/worker-configs/reviewer/CLAUDE.md +110 -0
- package/assets/worker-configs/security-engineer/CLAUDE.md +109 -0
- package/assets/worker-configs/spec-writer/.claude/commands/.gitkeep +1 -0
- package/assets/worker-configs/spec-writer/.claude/skills/.gitkeep +1 -0
- package/assets/worker-configs/spec-writer/CLAUDE.md +110 -0
- package/assets/worker-configs/technical-writer/CLAUDE.md +107 -0
- package/assets/worker-configs/tester/CLAUDE.md +110 -0
- package/dist/assets/claude-folder/agents/.gitkeep +0 -0
- package/dist/assets/claude-folder/commands/boss-commands.md +138 -0
- package/dist/assets/claude-folder/rules/boss-workflow.md +33 -0
- package/dist/assets/claude-folder/rules/code-style.md +23 -0
- package/dist/assets/claude-folder/rules/security.md +22 -0
- package/dist/assets/claude-folder/rules/testing.md +32 -0
- package/dist/assets/claude-folder/settings.local.json +88 -0
- package/dist/assets/claude-folder/skills/.gitkeep +0 -0
- package/dist/assets/claude-md/docs/container-use.md +140 -0
- package/dist/assets/claude-md/docs/github-operations.md +238 -0
- package/dist/assets/claude-md/docs/initialization.md +186 -0
- package/dist/assets/claude-md/docs/quality-standards.md +15 -0
- package/dist/assets/claude-md/docs/spec-kit.md +46 -0
- package/dist/assets/claude-md/docs/workers.md +174 -0
- package/dist/assets/claude-md/docs/workflow.md +140 -0
- package/dist/assets/claude-md/template.md +812 -0
- package/dist/assets/docker-compose/docker-compose.yml +52 -0
- package/dist/assets/git-hooks/commit-msg.sh +102 -0
- package/dist/assets/git-hooks/pre-commit-check.sh +21 -0
- package/dist/assets/git-hooks/pre-commit.sh +6 -0
- package/dist/assets/git-hooks/pre-push.sh +154 -0
- package/dist/assets/git-hooks/security-check.sh +40 -0
- package/dist/assets/git-hooks/test-changed.sh +84 -0
- package/dist/assets/github-workflows/CODEOWNERS +3 -0
- package/dist/assets/github-workflows/boss-ci.yml +40 -0
- package/dist/assets/github-workflows/boss-gates.yml +28 -0
- package/dist/assets/start-boss-sh/start-boss.sh +471 -0
- package/dist/assets/template-docs/api-service-fastify.md +98 -0
- package/dist/assets/template-docs/blank.md +91 -0
- package/dist/assets/template-docs/nextjs-app-turbo.md +102 -0
- package/dist/assets/template-docs/t3-app.md +106 -0
- package/dist/assets/template-loader/README.md +28 -0
- package/dist/assets/template-loader/eslint.config.node.js +28 -0
- package/dist/assets/template-loader/eslint.config.react.js +47 -0
- package/dist/assets/template-loader/gitignore +42 -0
- package/dist/assets/template-loader/index.test.ts +10 -0
- package/dist/assets/template-loader/index.ts +13 -0
- package/dist/assets/template-loader/prettierignore +8 -0
- package/dist/assets/template-loader/vitest.config.ts +19 -0
- package/dist/assets/worker-configs/architect/.claude/commands/.gitkeep +0 -0
- package/dist/assets/worker-configs/architect/.claude/skills/.gitkeep +0 -0
- package/dist/assets/worker-configs/architect/CLAUDE.md +106 -0
- package/dist/assets/worker-configs/clarifier/.claude/commands/.gitkeep +1 -0
- package/dist/assets/worker-configs/clarifier/.claude/skills/.gitkeep +1 -0
- package/dist/assets/worker-configs/clarifier/CLAUDE.md +112 -0
- package/dist/assets/worker-configs/code-reviewer/CLAUDE.md +113 -0
- package/dist/assets/worker-configs/consolidator/.claude/commands/.gitkeep +1 -0
- package/dist/assets/worker-configs/consolidator/.claude/skills/.gitkeep +1 -0
- package/dist/assets/worker-configs/consolidator/CLAUDE.md +114 -0
- package/dist/assets/worker-configs/developer-backend/.claude/commands/.gitkeep +1 -0
- package/dist/assets/worker-configs/developer-backend/.claude/skills/.gitkeep +1 -0
- package/dist/assets/worker-configs/developer-backend/CLAUDE.md +116 -0
- package/dist/assets/worker-configs/developer-frontend/.claude/commands/.gitkeep +1 -0
- package/dist/assets/worker-configs/developer-frontend/.claude/skills/.gitkeep +1 -0
- package/dist/assets/worker-configs/developer-frontend/CLAUDE.md +116 -0
- package/dist/assets/worker-configs/developer-fullstack/.claude/commands/.gitkeep +1 -0
- package/dist/assets/worker-configs/developer-fullstack/.claude/skills/.gitkeep +1 -0
- package/dist/assets/worker-configs/developer-fullstack/CLAUDE.md +116 -0
- package/dist/assets/worker-configs/devops-engineer/CLAUDE.md +111 -0
- package/dist/assets/worker-configs/planner/.claude/commands/.gitkeep +1 -0
- package/dist/assets/worker-configs/planner/.claude/skills/.gitkeep +1 -0
- package/dist/assets/worker-configs/planner/CLAUDE.md +111 -0
- package/dist/assets/worker-configs/product-owner/CLAUDE.md +108 -0
- package/dist/assets/worker-configs/reviewer/.claude/commands/.gitkeep +1 -0
- package/dist/assets/worker-configs/reviewer/.claude/skills/.gitkeep +1 -0
- package/dist/assets/worker-configs/reviewer/CLAUDE.md +110 -0
- package/dist/assets/worker-configs/security-engineer/CLAUDE.md +109 -0
- package/dist/assets/worker-configs/spec-writer/.claude/commands/.gitkeep +1 -0
- package/dist/assets/worker-configs/spec-writer/.claude/skills/.gitkeep +1 -0
- package/dist/assets/worker-configs/spec-writer/CLAUDE.md +110 -0
- package/dist/assets/worker-configs/technical-writer/CLAUDE.md +107 -0
- package/dist/assets/worker-configs/tester/CLAUDE.md +110 -0
- package/dist/commands/__tests__/bootstrap.test.d.ts +2 -0
- package/dist/commands/__tests__/bootstrap.test.d.ts.map +1 -0
- package/dist/commands/__tests__/bootstrap.test.js +379 -0
- package/dist/commands/__tests__/bootstrap.test.js.map +1 -0
- package/dist/commands/__tests__/doctor.test.d.ts +2 -0
- package/dist/commands/__tests__/doctor.test.d.ts.map +1 -0
- package/dist/commands/__tests__/doctor.test.js +13 -0
- package/dist/commands/__tests__/doctor.test.js.map +1 -0
- package/dist/commands/bootstrap.d.ts +3 -0
- package/dist/commands/bootstrap.d.ts.map +1 -0
- package/dist/commands/bootstrap.js +390 -0
- package/dist/commands/bootstrap.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +171 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/templates.d.ts +2 -0
- package/dist/commands/templates.d.ts.map +1 -0
- package/dist/commands/templates.js +12 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/constants.d.ts +23 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +23 -0
- package/dist/constants.js.map +1 -0
- package/dist/generators/__tests__/boss-config.test.d.ts +2 -0
- package/dist/generators/__tests__/boss-config.test.d.ts.map +1 -0
- package/dist/generators/__tests__/boss-config.test.js +61 -0
- package/dist/generators/__tests__/boss-config.test.js.map +1 -0
- package/dist/generators/__tests__/claude-folder.test.d.ts +2 -0
- package/dist/generators/__tests__/claude-folder.test.d.ts.map +1 -0
- package/dist/generators/__tests__/claude-folder.test.js +128 -0
- package/dist/generators/__tests__/claude-folder.test.js.map +1 -0
- package/dist/generators/__tests__/claude-md.test.d.ts +2 -0
- package/dist/generators/__tests__/claude-md.test.d.ts.map +1 -0
- package/dist/generators/__tests__/claude-md.test.js +55 -0
- package/dist/generators/__tests__/claude-md.test.js.map +1 -0
- package/dist/generators/__tests__/container-use-config.test.d.ts +2 -0
- package/dist/generators/__tests__/container-use-config.test.d.ts.map +1 -0
- package/dist/generators/__tests__/container-use-config.test.js +80 -0
- package/dist/generators/__tests__/container-use-config.test.js.map +1 -0
- package/dist/generators/__tests__/docker-compose.test.d.ts +2 -0
- package/dist/generators/__tests__/docker-compose.test.d.ts.map +1 -0
- package/dist/generators/__tests__/docker-compose.test.js +39 -0
- package/dist/generators/__tests__/docker-compose.test.js.map +1 -0
- package/dist/generators/__tests__/git-hooks.test.d.ts +2 -0
- package/dist/generators/__tests__/git-hooks.test.d.ts.map +1 -0
- package/dist/generators/__tests__/git-hooks.test.js +118 -0
- package/dist/generators/__tests__/git-hooks.test.js.map +1 -0
- package/dist/generators/__tests__/github-workflows.test.d.ts +2 -0
- package/dist/generators/__tests__/github-workflows.test.d.ts.map +1 -0
- package/dist/generators/__tests__/github-workflows.test.js +86 -0
- package/dist/generators/__tests__/github-workflows.test.js.map +1 -0
- package/dist/generators/__tests__/mcp-config.test.d.ts +2 -0
- package/dist/generators/__tests__/mcp-config.test.d.ts.map +1 -0
- package/dist/generators/__tests__/mcp-config.test.js +131 -0
- package/dist/generators/__tests__/mcp-config.test.js.map +1 -0
- package/dist/generators/__tests__/project-structure.test.d.ts +2 -0
- package/dist/generators/__tests__/project-structure.test.d.ts.map +1 -0
- package/dist/generators/__tests__/project-structure.test.js +40 -0
- package/dist/generators/__tests__/project-structure.test.js.map +1 -0
- package/dist/generators/__tests__/quality-gates.test.d.ts +2 -0
- package/dist/generators/__tests__/quality-gates.test.d.ts.map +1 -0
- package/dist/generators/__tests__/quality-gates.test.js +71 -0
- package/dist/generators/__tests__/quality-gates.test.js.map +1 -0
- package/dist/generators/__tests__/specify-structure.test.d.ts +2 -0
- package/dist/generators/__tests__/specify-structure.test.d.ts.map +1 -0
- package/dist/generators/__tests__/specify-structure.test.js +63 -0
- package/dist/generators/__tests__/specify-structure.test.js.map +1 -0
- package/dist/generators/__tests__/start-boss-sh.test.d.ts +2 -0
- package/dist/generators/__tests__/start-boss-sh.test.d.ts.map +1 -0
- package/dist/generators/__tests__/start-boss-sh.test.js +36 -0
- package/dist/generators/__tests__/start-boss-sh.test.js.map +1 -0
- package/dist/generators/__tests__/template-docs.test.d.ts +2 -0
- package/dist/generators/__tests__/template-docs.test.d.ts.map +1 -0
- package/dist/generators/__tests__/template-docs.test.js +92 -0
- package/dist/generators/__tests__/template-docs.test.js.map +1 -0
- package/dist/generators/__tests__/template-loader.test.d.ts +2 -0
- package/dist/generators/__tests__/template-loader.test.d.ts.map +1 -0
- package/dist/generators/__tests__/template-loader.test.js +215 -0
- package/dist/generators/__tests__/template-loader.test.js.map +1 -0
- package/dist/generators/__tests__/worker-configs.test.d.ts +2 -0
- package/dist/generators/__tests__/worker-configs.test.d.ts.map +1 -0
- package/dist/generators/__tests__/worker-configs.test.js +122 -0
- package/dist/generators/__tests__/worker-configs.test.js.map +1 -0
- package/dist/generators/boss-config.d.ts +3 -0
- package/dist/generators/boss-config.d.ts.map +1 -0
- package/dist/generators/boss-config.js +242 -0
- package/dist/generators/boss-config.js.map +1 -0
- package/dist/generators/claude-folder.d.ts +3 -0
- package/dist/generators/claude-folder.d.ts.map +1 -0
- package/dist/generators/claude-folder.js +94 -0
- package/dist/generators/claude-folder.js.map +1 -0
- package/dist/generators/claude-md.d.ts +3 -0
- package/dist/generators/claude-md.d.ts.map +1 -0
- package/dist/generators/claude-md.js +64 -0
- package/dist/generators/claude-md.js.map +1 -0
- package/dist/generators/container-use-config.d.ts +13 -0
- package/dist/generators/container-use-config.d.ts.map +1 -0
- package/dist/generators/container-use-config.js +60 -0
- package/dist/generators/container-use-config.js.map +1 -0
- package/dist/generators/docker-compose.d.ts +2 -0
- package/dist/generators/docker-compose.d.ts.map +1 -0
- package/dist/generators/docker-compose.js +8 -0
- package/dist/generators/docker-compose.js.map +1 -0
- package/dist/generators/git-hooks.d.ts +3 -0
- package/dist/generators/git-hooks.d.ts.map +1 -0
- package/dist/generators/git-hooks.js +58 -0
- package/dist/generators/git-hooks.js.map +1 -0
- package/dist/generators/github-workflows.d.ts +3 -0
- package/dist/generators/github-workflows.d.ts.map +1 -0
- package/dist/generators/github-workflows.js +27 -0
- package/dist/generators/github-workflows.js.map +1 -0
- package/dist/generators/mcp-config.d.ts +3 -0
- package/dist/generators/mcp-config.d.ts.map +1 -0
- package/dist/generators/mcp-config.js +183 -0
- package/dist/generators/mcp-config.js.map +1 -0
- package/dist/generators/project-structure.d.ts +3 -0
- package/dist/generators/project-structure.d.ts.map +1 -0
- package/dist/generators/project-structure.js +24 -0
- package/dist/generators/project-structure.js.map +1 -0
- package/dist/generators/quality-gates.d.ts +3 -0
- package/dist/generators/quality-gates.d.ts.map +1 -0
- package/dist/generators/quality-gates.js +32 -0
- package/dist/generators/quality-gates.js.map +1 -0
- package/dist/generators/specify-structure.d.ts +2 -0
- package/dist/generators/specify-structure.d.ts.map +1 -0
- package/dist/generators/specify-structure.js +43 -0
- package/dist/generators/specify-structure.js.map +1 -0
- package/dist/generators/start-boss-sh.d.ts +2 -0
- package/dist/generators/start-boss-sh.d.ts.map +1 -0
- package/dist/generators/start-boss-sh.js +10 -0
- package/dist/generators/start-boss-sh.js.map +1 -0
- package/dist/generators/template-docs.d.ts +3 -0
- package/dist/generators/template-docs.d.ts.map +1 -0
- package/dist/generators/template-docs.js +19 -0
- package/dist/generators/template-docs.js.map +1 -0
- package/dist/generators/template-loader.d.ts +3 -0
- package/dist/generators/template-loader.d.ts.map +1 -0
- package/dist/generators/template-loader.js +308 -0
- package/dist/generators/template-loader.js.map +1 -0
- package/dist/generators/worker-configs.d.ts +3 -0
- package/dist/generators/worker-configs.d.ts.map +1 -0
- package/dist/generators/worker-configs.js +119 -0
- package/dist/generators/worker-configs.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/presets/__tests__/quality-presets.test.d.ts +2 -0
- package/dist/presets/__tests__/quality-presets.test.d.ts.map +1 -0
- package/dist/presets/__tests__/quality-presets.test.js +152 -0
- package/dist/presets/__tests__/quality-presets.test.js.map +1 -0
- package/dist/presets/quality-presets.d.ts +3 -0
- package/dist/presets/quality-presets.d.ts.map +1 -0
- package/dist/presets/quality-presets.js +65 -0
- package/dist/presets/quality-presets.js.map +1 -0
- package/dist/types/index.d.ts +42 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/__tests__/file-system.test.d.ts +2 -0
- package/dist/utils/__tests__/file-system.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-system.test.js +83 -0
- package/dist/utils/__tests__/file-system.test.js.map +1 -0
- package/dist/utils/__tests__/git.test.d.ts +2 -0
- package/dist/utils/__tests__/git.test.d.ts.map +1 -0
- package/dist/utils/__tests__/git.test.js +79 -0
- package/dist/utils/__tests__/git.test.js.map +1 -0
- package/dist/utils/__tests__/template-loader.test.d.ts +2 -0
- package/dist/utils/__tests__/template-loader.test.d.ts.map +1 -0
- package/dist/utils/__tests__/template-loader.test.js +109 -0
- package/dist/utils/__tests__/template-loader.test.js.map +1 -0
- package/dist/utils/__tests__/validators.test.d.ts +2 -0
- package/dist/utils/__tests__/validators.test.d.ts.map +1 -0
- package/dist/utils/__tests__/validators.test.js +118 -0
- package/dist/utils/__tests__/validators.test.js.map +1 -0
- package/dist/utils/file-system.d.ts +8 -0
- package/dist/utils/file-system.d.ts.map +1 -0
- package/dist/utils/file-system.js +54 -0
- package/dist/utils/file-system.js.map +1 -0
- package/dist/utils/git.d.ts +7 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +98 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +42 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/prompts.d.ts +18 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +197 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/template-loader.d.ts +18 -0
- package/dist/utils/template-loader.d.ts.map +1 -0
- package/dist/utils/template-loader.js +92 -0
- package/dist/utils/template-loader.js.map +1 -0
- package/dist/utils/validators.d.ts +21 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +50 -0
- package/dist/utils/validators.js.map +1 -0
- package/package.json +79 -0
- package/templates/spec-kit/README.md +17 -0
- package/templates/spec-kit/memory/constitution.md +50 -0
- package/templates/spec-kit/scripts/bash/check-prerequisites.sh +166 -0
- package/templates/spec-kit/scripts/bash/common.sh +156 -0
- package/templates/spec-kit/scripts/bash/create-new-feature.sh +297 -0
- package/templates/spec-kit/scripts/bash/setup-plan.sh +61 -0
- package/templates/spec-kit/scripts/bash/update-agent-context.sh +799 -0
- package/templates/spec-kit/scripts/powershell/check-prerequisites.ps1 +148 -0
- package/templates/spec-kit/scripts/powershell/common.ps1 +137 -0
- package/templates/spec-kit/scripts/powershell/create-new-feature.ps1 +283 -0
- package/templates/spec-kit/scripts/powershell/setup-plan.ps1 +61 -0
- package/templates/spec-kit/scripts/powershell/update-agent-context.ps1 +448 -0
- package/templates/spec-kit/src/specify_cli/__init__.py +1369 -0
- package/templates/spec-kit/templates/agent-file-template.md +28 -0
- package/templates/spec-kit/templates/checklist-template.md +40 -0
- package/templates/spec-kit/templates/commands/analyze.md +187 -0
- package/templates/spec-kit/templates/commands/checklist.md +297 -0
- package/templates/spec-kit/templates/commands/clarify.md +184 -0
- package/templates/spec-kit/templates/commands/constitution.md +82 -0
- package/templates/spec-kit/templates/commands/implement.md +138 -0
- package/templates/spec-kit/templates/commands/plan.md +95 -0
- package/templates/spec-kit/templates/commands/specify.md +261 -0
- package/templates/spec-kit/templates/commands/tasks.md +140 -0
- package/templates/spec-kit/templates/commands/taskstoissues.md +33 -0
- package/templates/spec-kit/templates/plan-template.md +104 -0
- package/templates/spec-kit/templates/spec-template.md +115 -0
- package/templates/spec-kit/templates/tasks-template.md +251 -0
- package/templates/spec-kit/templates/vscode-settings.json +14 -0
- package/templates/t3-app/README.md +41 -0
- package/templates/t3-app/base/README.md +29 -0
- package/templates/t3-app/base/_gitignore +47 -0
- package/templates/t3-app/base/next-env.d.ts +5 -0
- package/templates/t3-app/base/next.config.js +22 -0
- package/templates/t3-app/base/package.json +26 -0
- package/templates/t3-app/base/public/favicon.ico +0 -0
- package/templates/t3-app/base/src/env.js +40 -0
- package/templates/t3-app/base/src/styles/globals.css +16 -0
- package/templates/t3-app/base/tsconfig.json +42 -0
- package/templates/t3-app/extras/config/_eslint.base.js +45 -0
- package/templates/t3-app/extras/config/_eslint.drizzle.js +58 -0
- package/templates/t3-app/extras/config/_prettier.config.js +2 -0
- package/templates/t3-app/extras/config/_tailwind.prettier.config.js +4 -0
- package/templates/t3-app/extras/config/biome.jsonc +69 -0
- package/templates/t3-app/extras/config/drizzle-config-mysql.ts +12 -0
- package/templates/t3-app/extras/config/drizzle-config-postgres.ts +12 -0
- package/templates/t3-app/extras/config/drizzle-config-sqlite.ts +12 -0
- package/templates/t3-app/extras/config/next-config-appdir.js +10 -0
- package/templates/t3-app/extras/config/postcss.config.js +5 -0
- package/templates/t3-app/extras/pnpm/_npmrc +2 -0
- package/templates/t3-app/extras/prisma/schema/base-planetscale.prisma +25 -0
- package/templates/t3-app/extras/prisma/schema/base.prisma +21 -0
- package/templates/t3-app/extras/prisma/schema/with-auth-planetscale.prisma +79 -0
- package/templates/t3-app/extras/prisma/schema/with-auth.prisma +75 -0
- package/templates/t3-app/extras/prisma/schema/with-better-auth-planetscale.prisma +90 -0
- package/templates/t3-app/extras/prisma/schema/with-better-auth.prisma +89 -0
- package/templates/t3-app/extras/src/app/_components/post-tw.tsx +50 -0
- package/templates/t3-app/extras/src/app/_components/post.tsx +54 -0
- package/templates/t3-app/extras/src/app/api/auth/[...all]/route.ts +5 -0
- package/templates/t3-app/extras/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/templates/t3-app/extras/src/app/api/trpc/[trpc]/route.ts +34 -0
- package/templates/t3-app/extras/src/app/layout/base.tsx +24 -0
- package/templates/t3-app/extras/src/app/layout/with-trpc-tw.tsx +29 -0
- package/templates/t3-app/extras/src/app/layout/with-trpc.tsx +28 -0
- package/templates/t3-app/extras/src/app/layout/with-tw.tsx +25 -0
- package/templates/t3-app/extras/src/app/page/base.tsx +39 -0
- package/templates/t3-app/extras/src/app/page/with-auth-trpc-tw.tsx +69 -0
- package/templates/t3-app/extras/src/app/page/with-auth-trpc.tsx +70 -0
- package/templates/t3-app/extras/src/app/page/with-better-auth-trpc-tw.tsx +105 -0
- package/templates/t3-app/extras/src/app/page/with-better-auth-trpc.tsx +103 -0
- package/templates/t3-app/extras/src/app/page/with-better-auth-tw.tsx +90 -0
- package/templates/t3-app/extras/src/app/page/with-better-auth.tsx +88 -0
- package/templates/t3-app/extras/src/app/page/with-trpc-tw.tsx +53 -0
- package/templates/t3-app/extras/src/app/page/with-trpc.tsx +54 -0
- package/templates/t3-app/extras/src/app/page/with-tw.tsx +37 -0
- package/templates/t3-app/extras/src/env/with-auth-db-planetscale.js +58 -0
- package/templates/t3-app/extras/src/env/with-auth-db.js +52 -0
- package/templates/t3-app/extras/src/env/with-auth.js +51 -0
- package/templates/t3-app/extras/src/env/with-better-auth-db-planetscale.js +58 -0
- package/templates/t3-app/extras/src/env/with-better-auth-db.js +50 -0
- package/templates/t3-app/extras/src/env/with-better-auth.js +49 -0
- package/templates/t3-app/extras/src/env/with-db-planetscale.js +50 -0
- package/templates/t3-app/extras/src/env/with-db.js +44 -0
- package/templates/t3-app/extras/src/index.module.css +177 -0
- package/templates/t3-app/extras/src/pages/_app/base.tsx +18 -0
- package/templates/t3-app/extras/src/pages/_app/with-auth-trpc-tw.tsx +27 -0
- package/templates/t3-app/extras/src/pages/_app/with-auth-trpc.tsx +27 -0
- package/templates/t3-app/extras/src/pages/_app/with-auth-tw.tsx +25 -0
- package/templates/t3-app/extras/src/pages/_app/with-auth.tsx +25 -0
- package/templates/t3-app/extras/src/pages/_app/with-better-auth-trpc-tw.tsx +20 -0
- package/templates/t3-app/extras/src/pages/_app/with-better-auth-trpc.tsx +20 -0
- package/templates/t3-app/extras/src/pages/_app/with-trpc-tw.tsx +20 -0
- package/templates/t3-app/extras/src/pages/_app/with-trpc.tsx +20 -0
- package/templates/t3-app/extras/src/pages/_app/with-tw.tsx +18 -0
- package/templates/t3-app/extras/src/pages/api/auth/[...all].ts +8 -0
- package/templates/t3-app/extras/src/pages/api/trpc/[trpc].ts +19 -0
- package/templates/t3-app/extras/src/pages/index/base.tsx +47 -0
- package/templates/t3-app/extras/src/pages/index/with-auth-trpc-tw.tsx +81 -0
- package/templates/t3-app/extras/src/pages/index/with-auth-trpc.tsx +82 -0
- package/templates/t3-app/extras/src/pages/index/with-better-auth-trpc-tw.tsx +99 -0
- package/templates/t3-app/extras/src/pages/index/with-better-auth-trpc.tsx +100 -0
- package/templates/t3-app/extras/src/pages/index/with-better-auth-tw.tsx +87 -0
- package/templates/t3-app/extras/src/pages/index/with-better-auth.tsx +88 -0
- package/templates/t3-app/extras/src/pages/index/with-trpc-tw.tsx +52 -0
- package/templates/t3-app/extras/src/pages/index/with-trpc.tsx +53 -0
- package/templates/t3-app/extras/src/pages/index/with-tw.tsx +45 -0
- package/templates/t3-app/extras/src/server/api/root.ts +23 -0
- package/templates/t3-app/extras/src/server/api/routers/post/base.ts +40 -0
- package/templates/t3-app/extras/src/server/api/routers/post/with-auth-drizzle.ts +39 -0
- package/templates/t3-app/extras/src/server/api/routers/post/with-auth-prisma.ts +41 -0
- package/templates/t3-app/extras/src/server/api/routers/post/with-auth.ts +37 -0
- package/templates/t3-app/extras/src/server/api/routers/post/with-drizzle.ts +30 -0
- package/templates/t3-app/extras/src/server/api/routers/post/with-prisma.ts +31 -0
- package/templates/t3-app/extras/src/server/api/trpc-app/base.ts +103 -0
- package/templates/t3-app/extras/src/server/api/trpc-app/with-auth-db.ts +133 -0
- package/templates/t3-app/extras/src/server/api/trpc-app/with-auth.ts +130 -0
- package/templates/t3-app/extras/src/server/api/trpc-app/with-better-auth-db.ts +134 -0
- package/templates/t3-app/extras/src/server/api/trpc-app/with-better-auth.ts +131 -0
- package/templates/t3-app/extras/src/server/api/trpc-app/with-db.ts +106 -0
- package/templates/t3-app/extras/src/server/api/trpc-pages/base.ts +122 -0
- package/templates/t3-app/extras/src/server/api/trpc-pages/with-auth-db.ts +160 -0
- package/templates/t3-app/extras/src/server/api/trpc-pages/with-auth.ts +158 -0
- package/templates/t3-app/extras/src/server/api/trpc-pages/with-better-auth-db.ts +168 -0
- package/templates/t3-app/extras/src/server/api/trpc-pages/with-better-auth.ts +166 -0
- package/templates/t3-app/extras/src/server/api/trpc-pages/with-db.ts +125 -0
- package/templates/t3-app/extras/src/server/auth/config/base.ts +52 -0
- package/templates/t3-app/extras/src/server/auth/config/with-drizzle.ts +67 -0
- package/templates/t3-app/extras/src/server/auth/config/with-prisma.ts +56 -0
- package/templates/t3-app/extras/src/server/auth/index.ts +10 -0
- package/templates/t3-app/extras/src/server/better-auth/client.ts +5 -0
- package/templates/t3-app/extras/src/server/better-auth/config/base.ts +12 -0
- package/templates/t3-app/extras/src/server/better-auth/config/with-drizzle.ts +26 -0
- package/templates/t3-app/extras/src/server/better-auth/config/with-prisma.ts +26 -0
- package/templates/t3-app/extras/src/server/better-auth/index.ts +1 -0
- package/templates/t3-app/extras/src/server/better-auth/server.ts +7 -0
- package/templates/t3-app/extras/src/server/db/db-prisma-planetscale.ts +19 -0
- package/templates/t3-app/extras/src/server/db/db-prisma.ts +16 -0
- package/templates/t3-app/extras/src/server/db/index-drizzle/with-mysql.ts +18 -0
- package/templates/t3-app/extras/src/server/db/index-drizzle/with-planetscale.ts +7 -0
- package/templates/t3-app/extras/src/server/db/index-drizzle/with-postgres.ts +18 -0
- package/templates/t3-app/extras/src/server/db/index-drizzle/with-sqlite.ts +19 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/base-mysql.ts +26 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/base-planetscale.ts +26 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/base-postgres.ts +26 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/base-sqlite.ts +27 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/with-auth-mysql.ts +111 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/with-auth-planetscale.ts +100 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/with-auth-postgres.ts +108 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/with-auth-sqlite.ts +105 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/with-better-auth-mysql.ts +106 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/with-better-auth-planetscale.ts +106 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/with-better-auth-postgres.ts +105 -0
- package/templates/t3-app/extras/src/server/db/schema-drizzle/with-better-auth-sqlite.ts +134 -0
- package/templates/t3-app/extras/src/styles/globals.css +6 -0
- package/templates/t3-app/extras/src/trpc/query-client.ts +25 -0
- package/templates/t3-app/extras/src/trpc/react.tsx +78 -0
- package/templates/t3-app/extras/src/trpc/server.ts +30 -0
- package/templates/t3-app/extras/src/utils/api.ts +68 -0
- package/templates/t3-app/extras/start-database/mysql.sh +86 -0
- package/templates/t3-app/extras/start-database/postgres.sh +88 -0
|
@@ -0,0 +1,1369 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.11"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "typer",
|
|
6
|
+
# "rich",
|
|
7
|
+
# "platformdirs",
|
|
8
|
+
# "readchar",
|
|
9
|
+
# "httpx",
|
|
10
|
+
# ]
|
|
11
|
+
# ///
|
|
12
|
+
"""
|
|
13
|
+
Specify CLI - Setup tool for Specify projects
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
uvx specify-cli.py init <project-name>
|
|
17
|
+
uvx specify-cli.py init .
|
|
18
|
+
uvx specify-cli.py init --here
|
|
19
|
+
|
|
20
|
+
Or install globally:
|
|
21
|
+
uv tool install --from specify-cli.py specify-cli
|
|
22
|
+
specify init <project-name>
|
|
23
|
+
specify init .
|
|
24
|
+
specify init --here
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import zipfile
|
|
31
|
+
import tempfile
|
|
32
|
+
import shutil
|
|
33
|
+
import shlex
|
|
34
|
+
import json
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Optional, Tuple
|
|
37
|
+
|
|
38
|
+
import typer
|
|
39
|
+
import httpx
|
|
40
|
+
from rich.console import Console
|
|
41
|
+
from rich.panel import Panel
|
|
42
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
43
|
+
from rich.text import Text
|
|
44
|
+
from rich.live import Live
|
|
45
|
+
from rich.align import Align
|
|
46
|
+
from rich.table import Table
|
|
47
|
+
from rich.tree import Tree
|
|
48
|
+
from typer.core import TyperGroup
|
|
49
|
+
|
|
50
|
+
# For cross-platform keyboard input
|
|
51
|
+
import readchar
|
|
52
|
+
import ssl
|
|
53
|
+
import truststore
|
|
54
|
+
from datetime import datetime, timezone
|
|
55
|
+
|
|
56
|
+
ssl_context = truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
57
|
+
client = httpx.Client(verify=ssl_context)
|
|
58
|
+
|
|
59
|
+
def _github_token(cli_token: str | None = None) -> str | None:
|
|
60
|
+
"""Return sanitized GitHub token (cli arg takes precedence) or None."""
|
|
61
|
+
return ((cli_token or os.getenv("GH_TOKEN") or os.getenv("GITHUB_TOKEN") or "").strip()) or None
|
|
62
|
+
|
|
63
|
+
def _github_auth_headers(cli_token: str | None = None) -> dict:
|
|
64
|
+
"""Return Authorization header dict only when a non-empty token exists."""
|
|
65
|
+
token = _github_token(cli_token)
|
|
66
|
+
return {"Authorization": f"Bearer {token}"} if token else {}
|
|
67
|
+
|
|
68
|
+
def _parse_rate_limit_headers(headers: httpx.Headers) -> dict:
|
|
69
|
+
"""Extract and parse GitHub rate-limit headers."""
|
|
70
|
+
info = {}
|
|
71
|
+
|
|
72
|
+
# Standard GitHub rate-limit headers
|
|
73
|
+
if "X-RateLimit-Limit" in headers:
|
|
74
|
+
info["limit"] = headers.get("X-RateLimit-Limit")
|
|
75
|
+
if "X-RateLimit-Remaining" in headers:
|
|
76
|
+
info["remaining"] = headers.get("X-RateLimit-Remaining")
|
|
77
|
+
if "X-RateLimit-Reset" in headers:
|
|
78
|
+
reset_epoch = int(headers.get("X-RateLimit-Reset", "0"))
|
|
79
|
+
if reset_epoch:
|
|
80
|
+
reset_time = datetime.fromtimestamp(reset_epoch, tz=timezone.utc)
|
|
81
|
+
info["reset_epoch"] = reset_epoch
|
|
82
|
+
info["reset_time"] = reset_time
|
|
83
|
+
info["reset_local"] = reset_time.astimezone()
|
|
84
|
+
|
|
85
|
+
# Retry-After header (seconds or HTTP-date)
|
|
86
|
+
if "Retry-After" in headers:
|
|
87
|
+
retry_after = headers.get("Retry-After")
|
|
88
|
+
try:
|
|
89
|
+
info["retry_after_seconds"] = int(retry_after)
|
|
90
|
+
except ValueError:
|
|
91
|
+
# HTTP-date format - not implemented, just store as string
|
|
92
|
+
info["retry_after"] = retry_after
|
|
93
|
+
|
|
94
|
+
return info
|
|
95
|
+
|
|
96
|
+
def _format_rate_limit_error(status_code: int, headers: httpx.Headers, url: str) -> str:
|
|
97
|
+
"""Format a user-friendly error message with rate-limit information."""
|
|
98
|
+
rate_info = _parse_rate_limit_headers(headers)
|
|
99
|
+
|
|
100
|
+
lines = [f"GitHub API returned status {status_code} for {url}"]
|
|
101
|
+
lines.append("")
|
|
102
|
+
|
|
103
|
+
if rate_info:
|
|
104
|
+
lines.append("[bold]Rate Limit Information:[/bold]")
|
|
105
|
+
if "limit" in rate_info:
|
|
106
|
+
lines.append(f" • Rate Limit: {rate_info['limit']} requests/hour")
|
|
107
|
+
if "remaining" in rate_info:
|
|
108
|
+
lines.append(f" • Remaining: {rate_info['remaining']}")
|
|
109
|
+
if "reset_local" in rate_info:
|
|
110
|
+
reset_str = rate_info["reset_local"].strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
111
|
+
lines.append(f" • Resets at: {reset_str}")
|
|
112
|
+
if "retry_after_seconds" in rate_info:
|
|
113
|
+
lines.append(f" • Retry after: {rate_info['retry_after_seconds']} seconds")
|
|
114
|
+
lines.append("")
|
|
115
|
+
|
|
116
|
+
# Add troubleshooting guidance
|
|
117
|
+
lines.append("[bold]Troubleshooting Tips:[/bold]")
|
|
118
|
+
lines.append(" • If you're on a shared CI or corporate environment, you may be rate-limited.")
|
|
119
|
+
lines.append(" • Consider using a GitHub token via --github-token or the GH_TOKEN/GITHUB_TOKEN")
|
|
120
|
+
lines.append(" environment variable to increase rate limits.")
|
|
121
|
+
lines.append(" • Authenticated requests have a limit of 5,000/hour vs 60/hour for unauthenticated.")
|
|
122
|
+
|
|
123
|
+
return "\n".join(lines)
|
|
124
|
+
|
|
125
|
+
# Agent configuration with name, folder, install URL, and CLI tool requirement
|
|
126
|
+
AGENT_CONFIG = {
|
|
127
|
+
"copilot": {
|
|
128
|
+
"name": "GitHub Copilot",
|
|
129
|
+
"folder": ".github/",
|
|
130
|
+
"install_url": None, # IDE-based, no CLI check needed
|
|
131
|
+
"requires_cli": False,
|
|
132
|
+
},
|
|
133
|
+
"claude": {
|
|
134
|
+
"name": "Claude Code",
|
|
135
|
+
"folder": ".claude/",
|
|
136
|
+
"install_url": "https://docs.anthropic.com/en/docs/claude-code/setup",
|
|
137
|
+
"requires_cli": True,
|
|
138
|
+
},
|
|
139
|
+
"gemini": {
|
|
140
|
+
"name": "Gemini CLI",
|
|
141
|
+
"folder": ".gemini/",
|
|
142
|
+
"install_url": "https://github.com/google-gemini/gemini-cli",
|
|
143
|
+
"requires_cli": True,
|
|
144
|
+
},
|
|
145
|
+
"cursor-agent": {
|
|
146
|
+
"name": "Cursor",
|
|
147
|
+
"folder": ".cursor/",
|
|
148
|
+
"install_url": None, # IDE-based
|
|
149
|
+
"requires_cli": False,
|
|
150
|
+
},
|
|
151
|
+
"qwen": {
|
|
152
|
+
"name": "Qwen Code",
|
|
153
|
+
"folder": ".qwen/",
|
|
154
|
+
"install_url": "https://github.com/QwenLM/qwen-code",
|
|
155
|
+
"requires_cli": True,
|
|
156
|
+
},
|
|
157
|
+
"opencode": {
|
|
158
|
+
"name": "opencode",
|
|
159
|
+
"folder": ".opencode/",
|
|
160
|
+
"install_url": "https://opencode.ai",
|
|
161
|
+
"requires_cli": True,
|
|
162
|
+
},
|
|
163
|
+
"codex": {
|
|
164
|
+
"name": "Codex CLI",
|
|
165
|
+
"folder": ".codex/",
|
|
166
|
+
"install_url": "https://github.com/openai/codex",
|
|
167
|
+
"requires_cli": True,
|
|
168
|
+
},
|
|
169
|
+
"windsurf": {
|
|
170
|
+
"name": "Windsurf",
|
|
171
|
+
"folder": ".windsurf/",
|
|
172
|
+
"install_url": None, # IDE-based
|
|
173
|
+
"requires_cli": False,
|
|
174
|
+
},
|
|
175
|
+
"kilocode": {
|
|
176
|
+
"name": "Kilo Code",
|
|
177
|
+
"folder": ".kilocode/",
|
|
178
|
+
"install_url": None, # IDE-based
|
|
179
|
+
"requires_cli": False,
|
|
180
|
+
},
|
|
181
|
+
"auggie": {
|
|
182
|
+
"name": "Auggie CLI",
|
|
183
|
+
"folder": ".augment/",
|
|
184
|
+
"install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli",
|
|
185
|
+
"requires_cli": True,
|
|
186
|
+
},
|
|
187
|
+
"codebuddy": {
|
|
188
|
+
"name": "CodeBuddy",
|
|
189
|
+
"folder": ".codebuddy/",
|
|
190
|
+
"install_url": "https://www.codebuddy.ai/cli",
|
|
191
|
+
"requires_cli": True,
|
|
192
|
+
},
|
|
193
|
+
"qoder": {
|
|
194
|
+
"name": "Qoder CLI",
|
|
195
|
+
"folder": ".qoder/",
|
|
196
|
+
"install_url": "https://qoder.com/cli",
|
|
197
|
+
"requires_cli": True,
|
|
198
|
+
},
|
|
199
|
+
"roo": {
|
|
200
|
+
"name": "Roo Code",
|
|
201
|
+
"folder": ".roo/",
|
|
202
|
+
"install_url": None, # IDE-based
|
|
203
|
+
"requires_cli": False,
|
|
204
|
+
},
|
|
205
|
+
"q": {
|
|
206
|
+
"name": "Amazon Q Developer CLI",
|
|
207
|
+
"folder": ".amazonq/",
|
|
208
|
+
"install_url": "https://aws.amazon.com/developer/learning/q-developer-cli/",
|
|
209
|
+
"requires_cli": True,
|
|
210
|
+
},
|
|
211
|
+
"amp": {
|
|
212
|
+
"name": "Amp",
|
|
213
|
+
"folder": ".agents/",
|
|
214
|
+
"install_url": "https://ampcode.com/manual#install",
|
|
215
|
+
"requires_cli": True,
|
|
216
|
+
},
|
|
217
|
+
"shai": {
|
|
218
|
+
"name": "SHAI",
|
|
219
|
+
"folder": ".shai/",
|
|
220
|
+
"install_url": "https://github.com/ovh/shai",
|
|
221
|
+
"requires_cli": True,
|
|
222
|
+
},
|
|
223
|
+
"bob": {
|
|
224
|
+
"name": "IBM Bob",
|
|
225
|
+
"folder": ".bob/",
|
|
226
|
+
"install_url": None, # IDE-based
|
|
227
|
+
"requires_cli": False,
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
|
|
232
|
+
|
|
233
|
+
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
|
|
234
|
+
|
|
235
|
+
BANNER = """
|
|
236
|
+
███████╗██████╗ ███████╗ ██████╗██╗███████╗██╗ ██╗
|
|
237
|
+
██╔════╝██╔══██╗██╔════╝██╔════╝██║██╔════╝╚██╗ ██╔╝
|
|
238
|
+
███████╗██████╔╝█████╗ ██║ ██║█████╗ ╚████╔╝
|
|
239
|
+
╚════██║██╔═══╝ ██╔══╝ ██║ ██║██╔══╝ ╚██╔╝
|
|
240
|
+
███████║██║ ███████╗╚██████╗██║██║ ██║
|
|
241
|
+
╚══════╝╚═╝ ╚══════╝ ╚═════╝╚═╝╚═╝ ╚═╝
|
|
242
|
+
"""
|
|
243
|
+
|
|
244
|
+
TAGLINE = "GitHub Spec Kit - Spec-Driven Development Toolkit"
|
|
245
|
+
class StepTracker:
|
|
246
|
+
"""Track and render hierarchical steps without emojis, similar to Claude Code tree output.
|
|
247
|
+
Supports live auto-refresh via an attached refresh callback.
|
|
248
|
+
"""
|
|
249
|
+
def __init__(self, title: str):
|
|
250
|
+
self.title = title
|
|
251
|
+
self.steps = [] # list of dicts: {key, label, status, detail}
|
|
252
|
+
self.status_order = {"pending": 0, "running": 1, "done": 2, "error": 3, "skipped": 4}
|
|
253
|
+
self._refresh_cb = None # callable to trigger UI refresh
|
|
254
|
+
|
|
255
|
+
def attach_refresh(self, cb):
|
|
256
|
+
self._refresh_cb = cb
|
|
257
|
+
|
|
258
|
+
def add(self, key: str, label: str):
|
|
259
|
+
if key not in [s["key"] for s in self.steps]:
|
|
260
|
+
self.steps.append({"key": key, "label": label, "status": "pending", "detail": ""})
|
|
261
|
+
self._maybe_refresh()
|
|
262
|
+
|
|
263
|
+
def start(self, key: str, detail: str = ""):
|
|
264
|
+
self._update(key, status="running", detail=detail)
|
|
265
|
+
|
|
266
|
+
def complete(self, key: str, detail: str = ""):
|
|
267
|
+
self._update(key, status="done", detail=detail)
|
|
268
|
+
|
|
269
|
+
def error(self, key: str, detail: str = ""):
|
|
270
|
+
self._update(key, status="error", detail=detail)
|
|
271
|
+
|
|
272
|
+
def skip(self, key: str, detail: str = ""):
|
|
273
|
+
self._update(key, status="skipped", detail=detail)
|
|
274
|
+
|
|
275
|
+
def _update(self, key: str, status: str, detail: str):
|
|
276
|
+
for s in self.steps:
|
|
277
|
+
if s["key"] == key:
|
|
278
|
+
s["status"] = status
|
|
279
|
+
if detail:
|
|
280
|
+
s["detail"] = detail
|
|
281
|
+
self._maybe_refresh()
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
self.steps.append({"key": key, "label": key, "status": status, "detail": detail})
|
|
285
|
+
self._maybe_refresh()
|
|
286
|
+
|
|
287
|
+
def _maybe_refresh(self):
|
|
288
|
+
if self._refresh_cb:
|
|
289
|
+
try:
|
|
290
|
+
self._refresh_cb()
|
|
291
|
+
except Exception:
|
|
292
|
+
pass
|
|
293
|
+
|
|
294
|
+
def render(self):
|
|
295
|
+
tree = Tree(f"[cyan]{self.title}[/cyan]", guide_style="grey50")
|
|
296
|
+
for step in self.steps:
|
|
297
|
+
label = step["label"]
|
|
298
|
+
detail_text = step["detail"].strip() if step["detail"] else ""
|
|
299
|
+
|
|
300
|
+
status = step["status"]
|
|
301
|
+
if status == "done":
|
|
302
|
+
symbol = "[green]●[/green]"
|
|
303
|
+
elif status == "pending":
|
|
304
|
+
symbol = "[green dim]○[/green dim]"
|
|
305
|
+
elif status == "running":
|
|
306
|
+
symbol = "[cyan]○[/cyan]"
|
|
307
|
+
elif status == "error":
|
|
308
|
+
symbol = "[red]●[/red]"
|
|
309
|
+
elif status == "skipped":
|
|
310
|
+
symbol = "[yellow]○[/yellow]"
|
|
311
|
+
else:
|
|
312
|
+
symbol = " "
|
|
313
|
+
|
|
314
|
+
if status == "pending":
|
|
315
|
+
# Entire line light gray (pending)
|
|
316
|
+
if detail_text:
|
|
317
|
+
line = f"{symbol} [bright_black]{label} ({detail_text})[/bright_black]"
|
|
318
|
+
else:
|
|
319
|
+
line = f"{symbol} [bright_black]{label}[/bright_black]"
|
|
320
|
+
else:
|
|
321
|
+
# Label white, detail (if any) light gray in parentheses
|
|
322
|
+
if detail_text:
|
|
323
|
+
line = f"{symbol} [white]{label}[/white] [bright_black]({detail_text})[/bright_black]"
|
|
324
|
+
else:
|
|
325
|
+
line = f"{symbol} [white]{label}[/white]"
|
|
326
|
+
|
|
327
|
+
tree.add(line)
|
|
328
|
+
return tree
|
|
329
|
+
|
|
330
|
+
def get_key():
|
|
331
|
+
"""Get a single keypress in a cross-platform way using readchar."""
|
|
332
|
+
key = readchar.readkey()
|
|
333
|
+
|
|
334
|
+
if key == readchar.key.UP or key == readchar.key.CTRL_P:
|
|
335
|
+
return 'up'
|
|
336
|
+
if key == readchar.key.DOWN or key == readchar.key.CTRL_N:
|
|
337
|
+
return 'down'
|
|
338
|
+
|
|
339
|
+
if key == readchar.key.ENTER:
|
|
340
|
+
return 'enter'
|
|
341
|
+
|
|
342
|
+
if key == readchar.key.ESC:
|
|
343
|
+
return 'escape'
|
|
344
|
+
|
|
345
|
+
if key == readchar.key.CTRL_C:
|
|
346
|
+
raise KeyboardInterrupt
|
|
347
|
+
|
|
348
|
+
return key
|
|
349
|
+
|
|
350
|
+
def select_with_arrows(options: dict, prompt_text: str = "Select an option", default_key: str = None) -> str:
|
|
351
|
+
"""
|
|
352
|
+
Interactive selection using arrow keys with Rich Live display.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
options: Dict with keys as option keys and values as descriptions
|
|
356
|
+
prompt_text: Text to show above the options
|
|
357
|
+
default_key: Default option key to start with
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
Selected option key
|
|
361
|
+
"""
|
|
362
|
+
option_keys = list(options.keys())
|
|
363
|
+
if default_key and default_key in option_keys:
|
|
364
|
+
selected_index = option_keys.index(default_key)
|
|
365
|
+
else:
|
|
366
|
+
selected_index = 0
|
|
367
|
+
|
|
368
|
+
selected_key = None
|
|
369
|
+
|
|
370
|
+
def create_selection_panel():
|
|
371
|
+
"""Create the selection panel with current selection highlighted."""
|
|
372
|
+
table = Table.grid(padding=(0, 2))
|
|
373
|
+
table.add_column(style="cyan", justify="left", width=3)
|
|
374
|
+
table.add_column(style="white", justify="left")
|
|
375
|
+
|
|
376
|
+
for i, key in enumerate(option_keys):
|
|
377
|
+
if i == selected_index:
|
|
378
|
+
table.add_row("▶", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
|
|
379
|
+
else:
|
|
380
|
+
table.add_row(" ", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
|
|
381
|
+
|
|
382
|
+
table.add_row("", "")
|
|
383
|
+
table.add_row("", "[dim]Use ↑/↓ to navigate, Enter to select, Esc to cancel[/dim]")
|
|
384
|
+
|
|
385
|
+
return Panel(
|
|
386
|
+
table,
|
|
387
|
+
title=f"[bold]{prompt_text}[/bold]",
|
|
388
|
+
border_style="cyan",
|
|
389
|
+
padding=(1, 2)
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
console.print()
|
|
393
|
+
|
|
394
|
+
def run_selection_loop():
|
|
395
|
+
nonlocal selected_key, selected_index
|
|
396
|
+
with Live(create_selection_panel(), console=console, transient=True, auto_refresh=False) as live:
|
|
397
|
+
while True:
|
|
398
|
+
try:
|
|
399
|
+
key = get_key()
|
|
400
|
+
if key == 'up':
|
|
401
|
+
selected_index = (selected_index - 1) % len(option_keys)
|
|
402
|
+
elif key == 'down':
|
|
403
|
+
selected_index = (selected_index + 1) % len(option_keys)
|
|
404
|
+
elif key == 'enter':
|
|
405
|
+
selected_key = option_keys[selected_index]
|
|
406
|
+
break
|
|
407
|
+
elif key == 'escape':
|
|
408
|
+
console.print("\n[yellow]Selection cancelled[/yellow]")
|
|
409
|
+
raise typer.Exit(1)
|
|
410
|
+
|
|
411
|
+
live.update(create_selection_panel(), refresh=True)
|
|
412
|
+
|
|
413
|
+
except KeyboardInterrupt:
|
|
414
|
+
console.print("\n[yellow]Selection cancelled[/yellow]")
|
|
415
|
+
raise typer.Exit(1)
|
|
416
|
+
|
|
417
|
+
run_selection_loop()
|
|
418
|
+
|
|
419
|
+
if selected_key is None:
|
|
420
|
+
console.print("\n[red]Selection failed.[/red]")
|
|
421
|
+
raise typer.Exit(1)
|
|
422
|
+
|
|
423
|
+
return selected_key
|
|
424
|
+
|
|
425
|
+
console = Console()
|
|
426
|
+
|
|
427
|
+
class BannerGroup(TyperGroup):
|
|
428
|
+
"""Custom group that shows banner before help."""
|
|
429
|
+
|
|
430
|
+
def format_help(self, ctx, formatter):
|
|
431
|
+
# Show banner before help
|
|
432
|
+
show_banner()
|
|
433
|
+
super().format_help(ctx, formatter)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
app = typer.Typer(
|
|
437
|
+
name="specify",
|
|
438
|
+
help="Setup tool for Specify spec-driven development projects",
|
|
439
|
+
add_completion=False,
|
|
440
|
+
invoke_without_command=True,
|
|
441
|
+
cls=BannerGroup,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
def show_banner():
|
|
445
|
+
"""Display the ASCII art banner."""
|
|
446
|
+
banner_lines = BANNER.strip().split('\n')
|
|
447
|
+
colors = ["bright_blue", "blue", "cyan", "bright_cyan", "white", "bright_white"]
|
|
448
|
+
|
|
449
|
+
styled_banner = Text()
|
|
450
|
+
for i, line in enumerate(banner_lines):
|
|
451
|
+
color = colors[i % len(colors)]
|
|
452
|
+
styled_banner.append(line + "\n", style=color)
|
|
453
|
+
|
|
454
|
+
console.print(Align.center(styled_banner))
|
|
455
|
+
console.print(Align.center(Text(TAGLINE, style="italic bright_yellow")))
|
|
456
|
+
console.print()
|
|
457
|
+
|
|
458
|
+
@app.callback()
|
|
459
|
+
def callback(ctx: typer.Context):
|
|
460
|
+
"""Show banner when no subcommand is provided."""
|
|
461
|
+
if ctx.invoked_subcommand is None and "--help" not in sys.argv and "-h" not in sys.argv:
|
|
462
|
+
show_banner()
|
|
463
|
+
console.print(Align.center("[dim]Run 'specify --help' for usage information[/dim]"))
|
|
464
|
+
console.print()
|
|
465
|
+
|
|
466
|
+
def run_command(cmd: list[str], check_return: bool = True, capture: bool = False, shell: bool = False) -> Optional[str]:
|
|
467
|
+
"""Run a shell command and optionally capture output."""
|
|
468
|
+
try:
|
|
469
|
+
if capture:
|
|
470
|
+
result = subprocess.run(cmd, check=check_return, capture_output=True, text=True, shell=shell)
|
|
471
|
+
return result.stdout.strip()
|
|
472
|
+
else:
|
|
473
|
+
subprocess.run(cmd, check=check_return, shell=shell)
|
|
474
|
+
return None
|
|
475
|
+
except subprocess.CalledProcessError as e:
|
|
476
|
+
if check_return:
|
|
477
|
+
console.print(f"[red]Error running command:[/red] {' '.join(cmd)}")
|
|
478
|
+
console.print(f"[red]Exit code:[/red] {e.returncode}")
|
|
479
|
+
if hasattr(e, 'stderr') and e.stderr:
|
|
480
|
+
console.print(f"[red]Error output:[/red] {e.stderr}")
|
|
481
|
+
raise
|
|
482
|
+
return None
|
|
483
|
+
|
|
484
|
+
def check_tool(tool: str, tracker: StepTracker = None) -> bool:
|
|
485
|
+
"""Check if a tool is installed. Optionally update tracker.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
tool: Name of the tool to check
|
|
489
|
+
tracker: Optional StepTracker to update with results
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
True if tool is found, False otherwise
|
|
493
|
+
"""
|
|
494
|
+
# Special handling for Claude CLI after `claude migrate-installer`
|
|
495
|
+
# See: https://github.com/github/spec-kit/issues/123
|
|
496
|
+
# The migrate-installer command REMOVES the original executable from PATH
|
|
497
|
+
# and creates an alias at ~/.claude/local/claude instead
|
|
498
|
+
# This path should be prioritized over other claude executables in PATH
|
|
499
|
+
if tool == "claude":
|
|
500
|
+
if CLAUDE_LOCAL_PATH.exists() and CLAUDE_LOCAL_PATH.is_file():
|
|
501
|
+
if tracker:
|
|
502
|
+
tracker.complete(tool, "available")
|
|
503
|
+
return True
|
|
504
|
+
|
|
505
|
+
found = shutil.which(tool) is not None
|
|
506
|
+
|
|
507
|
+
if tracker:
|
|
508
|
+
if found:
|
|
509
|
+
tracker.complete(tool, "available")
|
|
510
|
+
else:
|
|
511
|
+
tracker.error(tool, "not found")
|
|
512
|
+
|
|
513
|
+
return found
|
|
514
|
+
|
|
515
|
+
def is_git_repo(path: Path = None) -> bool:
|
|
516
|
+
"""Check if the specified path is inside a git repository."""
|
|
517
|
+
if path is None:
|
|
518
|
+
path = Path.cwd()
|
|
519
|
+
|
|
520
|
+
if not path.is_dir():
|
|
521
|
+
return False
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
# Use git command to check if inside a work tree
|
|
525
|
+
subprocess.run(
|
|
526
|
+
["git", "rev-parse", "--is-inside-work-tree"],
|
|
527
|
+
check=True,
|
|
528
|
+
capture_output=True,
|
|
529
|
+
cwd=path,
|
|
530
|
+
)
|
|
531
|
+
return True
|
|
532
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
533
|
+
return False
|
|
534
|
+
|
|
535
|
+
def init_git_repo(project_path: Path, quiet: bool = False) -> Tuple[bool, Optional[str]]:
|
|
536
|
+
"""Initialize a git repository in the specified path.
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
project_path: Path to initialize git repository in
|
|
540
|
+
quiet: if True suppress console output (tracker handles status)
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
Tuple of (success: bool, error_message: Optional[str])
|
|
544
|
+
"""
|
|
545
|
+
try:
|
|
546
|
+
original_cwd = Path.cwd()
|
|
547
|
+
os.chdir(project_path)
|
|
548
|
+
if not quiet:
|
|
549
|
+
console.print("[cyan]Initializing git repository...[/cyan]")
|
|
550
|
+
subprocess.run(["git", "init"], check=True, capture_output=True, text=True)
|
|
551
|
+
subprocess.run(["git", "add", "."], check=True, capture_output=True, text=True)
|
|
552
|
+
subprocess.run(["git", "commit", "-m", "Initial commit from Specify template"], check=True, capture_output=True, text=True)
|
|
553
|
+
if not quiet:
|
|
554
|
+
console.print("[green]✓[/green] Git repository initialized")
|
|
555
|
+
return True, None
|
|
556
|
+
|
|
557
|
+
except subprocess.CalledProcessError as e:
|
|
558
|
+
error_msg = f"Command: {' '.join(e.cmd)}\nExit code: {e.returncode}"
|
|
559
|
+
if e.stderr:
|
|
560
|
+
error_msg += f"\nError: {e.stderr.strip()}"
|
|
561
|
+
elif e.stdout:
|
|
562
|
+
error_msg += f"\nOutput: {e.stdout.strip()}"
|
|
563
|
+
|
|
564
|
+
if not quiet:
|
|
565
|
+
console.print(f"[red]Error initializing git repository:[/red] {e}")
|
|
566
|
+
return False, error_msg
|
|
567
|
+
finally:
|
|
568
|
+
os.chdir(original_cwd)
|
|
569
|
+
|
|
570
|
+
def handle_vscode_settings(sub_item, dest_file, rel_path, verbose=False, tracker=None) -> None:
|
|
571
|
+
"""Handle merging or copying of .vscode/settings.json files."""
|
|
572
|
+
def log(message, color="green"):
|
|
573
|
+
if verbose and not tracker:
|
|
574
|
+
console.print(f"[{color}]{message}[/] {rel_path}")
|
|
575
|
+
|
|
576
|
+
try:
|
|
577
|
+
with open(sub_item, 'r', encoding='utf-8') as f:
|
|
578
|
+
new_settings = json.load(f)
|
|
579
|
+
|
|
580
|
+
if dest_file.exists():
|
|
581
|
+
merged = merge_json_files(dest_file, new_settings, verbose=verbose and not tracker)
|
|
582
|
+
with open(dest_file, 'w', encoding='utf-8') as f:
|
|
583
|
+
json.dump(merged, f, indent=4)
|
|
584
|
+
f.write('\n')
|
|
585
|
+
log("Merged:", "green")
|
|
586
|
+
else:
|
|
587
|
+
shutil.copy2(sub_item, dest_file)
|
|
588
|
+
log("Copied (no existing settings.json):", "blue")
|
|
589
|
+
|
|
590
|
+
except Exception as e:
|
|
591
|
+
log(f"Warning: Could not merge, copying instead: {e}", "yellow")
|
|
592
|
+
shutil.copy2(sub_item, dest_file)
|
|
593
|
+
|
|
594
|
+
def merge_json_files(existing_path: Path, new_content: dict, verbose: bool = False) -> dict:
|
|
595
|
+
"""Merge new JSON content into existing JSON file.
|
|
596
|
+
|
|
597
|
+
Performs a deep merge where:
|
|
598
|
+
- New keys are added
|
|
599
|
+
- Existing keys are preserved unless overwritten by new content
|
|
600
|
+
- Nested dictionaries are merged recursively
|
|
601
|
+
- Lists and other values are replaced (not merged)
|
|
602
|
+
|
|
603
|
+
Args:
|
|
604
|
+
existing_path: Path to existing JSON file
|
|
605
|
+
new_content: New JSON content to merge in
|
|
606
|
+
verbose: Whether to print merge details
|
|
607
|
+
|
|
608
|
+
Returns:
|
|
609
|
+
Merged JSON content as dict
|
|
610
|
+
"""
|
|
611
|
+
try:
|
|
612
|
+
with open(existing_path, 'r', encoding='utf-8') as f:
|
|
613
|
+
existing_content = json.load(f)
|
|
614
|
+
except (FileNotFoundError, json.JSONDecodeError):
|
|
615
|
+
# If file doesn't exist or is invalid, just use new content
|
|
616
|
+
return new_content
|
|
617
|
+
|
|
618
|
+
def deep_merge(base: dict, update: dict) -> dict:
|
|
619
|
+
"""Recursively merge update dict into base dict."""
|
|
620
|
+
result = base.copy()
|
|
621
|
+
for key, value in update.items():
|
|
622
|
+
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
|
623
|
+
# Recursively merge nested dictionaries
|
|
624
|
+
result[key] = deep_merge(result[key], value)
|
|
625
|
+
else:
|
|
626
|
+
# Add new key or replace existing value
|
|
627
|
+
result[key] = value
|
|
628
|
+
return result
|
|
629
|
+
|
|
630
|
+
merged = deep_merge(existing_content, new_content)
|
|
631
|
+
|
|
632
|
+
if verbose:
|
|
633
|
+
console.print(f"[cyan]Merged JSON file:[/cyan] {existing_path.name}")
|
|
634
|
+
|
|
635
|
+
return merged
|
|
636
|
+
|
|
637
|
+
def download_template_from_github(ai_assistant: str, download_dir: Path, *, script_type: str = "sh", verbose: bool = True, show_progress: bool = True, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Tuple[Path, dict]:
|
|
638
|
+
repo_owner = "github"
|
|
639
|
+
repo_name = "spec-kit"
|
|
640
|
+
if client is None:
|
|
641
|
+
client = httpx.Client(verify=ssl_context)
|
|
642
|
+
|
|
643
|
+
if verbose:
|
|
644
|
+
console.print("[cyan]Fetching latest release information...[/cyan]")
|
|
645
|
+
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
|
|
646
|
+
|
|
647
|
+
try:
|
|
648
|
+
response = client.get(
|
|
649
|
+
api_url,
|
|
650
|
+
timeout=30,
|
|
651
|
+
follow_redirects=True,
|
|
652
|
+
headers=_github_auth_headers(github_token),
|
|
653
|
+
)
|
|
654
|
+
status = response.status_code
|
|
655
|
+
if status != 200:
|
|
656
|
+
# Format detailed error message with rate-limit info
|
|
657
|
+
error_msg = _format_rate_limit_error(status, response.headers, api_url)
|
|
658
|
+
if debug:
|
|
659
|
+
error_msg += f"\n\n[dim]Response body (truncated 500):[/dim]\n{response.text[:500]}"
|
|
660
|
+
raise RuntimeError(error_msg)
|
|
661
|
+
try:
|
|
662
|
+
release_data = response.json()
|
|
663
|
+
except ValueError as je:
|
|
664
|
+
raise RuntimeError(f"Failed to parse release JSON: {je}\nRaw (truncated 400): {response.text[:400]}")
|
|
665
|
+
except Exception as e:
|
|
666
|
+
console.print(f"[red]Error fetching release information[/red]")
|
|
667
|
+
console.print(Panel(str(e), title="Fetch Error", border_style="red"))
|
|
668
|
+
raise typer.Exit(1)
|
|
669
|
+
|
|
670
|
+
assets = release_data.get("assets", [])
|
|
671
|
+
pattern = f"spec-kit-template-{ai_assistant}-{script_type}"
|
|
672
|
+
matching_assets = [
|
|
673
|
+
asset for asset in assets
|
|
674
|
+
if pattern in asset["name"] and asset["name"].endswith(".zip")
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
asset = matching_assets[0] if matching_assets else None
|
|
678
|
+
|
|
679
|
+
if asset is None:
|
|
680
|
+
console.print(f"[red]No matching release asset found[/red] for [bold]{ai_assistant}[/bold] (expected pattern: [bold]{pattern}[/bold])")
|
|
681
|
+
asset_names = [a.get('name', '?') for a in assets]
|
|
682
|
+
console.print(Panel("\n".join(asset_names) or "(no assets)", title="Available Assets", border_style="yellow"))
|
|
683
|
+
raise typer.Exit(1)
|
|
684
|
+
|
|
685
|
+
download_url = asset["browser_download_url"]
|
|
686
|
+
filename = asset["name"]
|
|
687
|
+
file_size = asset["size"]
|
|
688
|
+
|
|
689
|
+
if verbose:
|
|
690
|
+
console.print(f"[cyan]Found template:[/cyan] {filename}")
|
|
691
|
+
console.print(f"[cyan]Size:[/cyan] {file_size:,} bytes")
|
|
692
|
+
console.print(f"[cyan]Release:[/cyan] {release_data['tag_name']}")
|
|
693
|
+
|
|
694
|
+
zip_path = download_dir / filename
|
|
695
|
+
if verbose:
|
|
696
|
+
console.print(f"[cyan]Downloading template...[/cyan]")
|
|
697
|
+
|
|
698
|
+
try:
|
|
699
|
+
with client.stream(
|
|
700
|
+
"GET",
|
|
701
|
+
download_url,
|
|
702
|
+
timeout=60,
|
|
703
|
+
follow_redirects=True,
|
|
704
|
+
headers=_github_auth_headers(github_token),
|
|
705
|
+
) as response:
|
|
706
|
+
if response.status_code != 200:
|
|
707
|
+
# Handle rate-limiting on download as well
|
|
708
|
+
error_msg = _format_rate_limit_error(response.status_code, response.headers, download_url)
|
|
709
|
+
if debug:
|
|
710
|
+
error_msg += f"\n\n[dim]Response body (truncated 400):[/dim]\n{response.text[:400]}"
|
|
711
|
+
raise RuntimeError(error_msg)
|
|
712
|
+
total_size = int(response.headers.get('content-length', 0))
|
|
713
|
+
with open(zip_path, 'wb') as f:
|
|
714
|
+
if total_size == 0:
|
|
715
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
|
716
|
+
f.write(chunk)
|
|
717
|
+
else:
|
|
718
|
+
if show_progress:
|
|
719
|
+
with Progress(
|
|
720
|
+
SpinnerColumn(),
|
|
721
|
+
TextColumn("[progress.description]{task.description}"),
|
|
722
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
723
|
+
console=console,
|
|
724
|
+
) as progress:
|
|
725
|
+
task = progress.add_task("Downloading...", total=total_size)
|
|
726
|
+
downloaded = 0
|
|
727
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
|
728
|
+
f.write(chunk)
|
|
729
|
+
downloaded += len(chunk)
|
|
730
|
+
progress.update(task, completed=downloaded)
|
|
731
|
+
else:
|
|
732
|
+
for chunk in response.iter_bytes(chunk_size=8192):
|
|
733
|
+
f.write(chunk)
|
|
734
|
+
except Exception as e:
|
|
735
|
+
console.print(f"[red]Error downloading template[/red]")
|
|
736
|
+
detail = str(e)
|
|
737
|
+
if zip_path.exists():
|
|
738
|
+
zip_path.unlink()
|
|
739
|
+
console.print(Panel(detail, title="Download Error", border_style="red"))
|
|
740
|
+
raise typer.Exit(1)
|
|
741
|
+
if verbose:
|
|
742
|
+
console.print(f"Downloaded: {filename}")
|
|
743
|
+
metadata = {
|
|
744
|
+
"filename": filename,
|
|
745
|
+
"size": file_size,
|
|
746
|
+
"release": release_data["tag_name"],
|
|
747
|
+
"asset_url": download_url
|
|
748
|
+
}
|
|
749
|
+
return zip_path, metadata
|
|
750
|
+
|
|
751
|
+
def download_and_extract_template(project_path: Path, ai_assistant: str, script_type: str, is_current_dir: bool = False, *, verbose: bool = True, tracker: StepTracker | None = None, client: httpx.Client = None, debug: bool = False, github_token: str = None) -> Path:
|
|
752
|
+
"""Download the latest release and extract it to create a new project.
|
|
753
|
+
Returns project_path. Uses tracker if provided (with keys: fetch, download, extract, cleanup)
|
|
754
|
+
"""
|
|
755
|
+
current_dir = Path.cwd()
|
|
756
|
+
|
|
757
|
+
if tracker:
|
|
758
|
+
tracker.start("fetch", "contacting GitHub API")
|
|
759
|
+
try:
|
|
760
|
+
zip_path, meta = download_template_from_github(
|
|
761
|
+
ai_assistant,
|
|
762
|
+
current_dir,
|
|
763
|
+
script_type=script_type,
|
|
764
|
+
verbose=verbose and tracker is None,
|
|
765
|
+
show_progress=(tracker is None),
|
|
766
|
+
client=client,
|
|
767
|
+
debug=debug,
|
|
768
|
+
github_token=github_token
|
|
769
|
+
)
|
|
770
|
+
if tracker:
|
|
771
|
+
tracker.complete("fetch", f"release {meta['release']} ({meta['size']:,} bytes)")
|
|
772
|
+
tracker.add("download", "Download template")
|
|
773
|
+
tracker.complete("download", meta['filename'])
|
|
774
|
+
except Exception as e:
|
|
775
|
+
if tracker:
|
|
776
|
+
tracker.error("fetch", str(e))
|
|
777
|
+
else:
|
|
778
|
+
if verbose:
|
|
779
|
+
console.print(f"[red]Error downloading template:[/red] {e}")
|
|
780
|
+
raise
|
|
781
|
+
|
|
782
|
+
if tracker:
|
|
783
|
+
tracker.add("extract", "Extract template")
|
|
784
|
+
tracker.start("extract")
|
|
785
|
+
elif verbose:
|
|
786
|
+
console.print("Extracting template...")
|
|
787
|
+
|
|
788
|
+
try:
|
|
789
|
+
if not is_current_dir:
|
|
790
|
+
project_path.mkdir(parents=True)
|
|
791
|
+
|
|
792
|
+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
|
793
|
+
zip_contents = zip_ref.namelist()
|
|
794
|
+
if tracker:
|
|
795
|
+
tracker.start("zip-list")
|
|
796
|
+
tracker.complete("zip-list", f"{len(zip_contents)} entries")
|
|
797
|
+
elif verbose:
|
|
798
|
+
console.print(f"[cyan]ZIP contains {len(zip_contents)} items[/cyan]")
|
|
799
|
+
|
|
800
|
+
if is_current_dir:
|
|
801
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
802
|
+
temp_path = Path(temp_dir)
|
|
803
|
+
zip_ref.extractall(temp_path)
|
|
804
|
+
|
|
805
|
+
extracted_items = list(temp_path.iterdir())
|
|
806
|
+
if tracker:
|
|
807
|
+
tracker.start("extracted-summary")
|
|
808
|
+
tracker.complete("extracted-summary", f"temp {len(extracted_items)} items")
|
|
809
|
+
elif verbose:
|
|
810
|
+
console.print(f"[cyan]Extracted {len(extracted_items)} items to temp location[/cyan]")
|
|
811
|
+
|
|
812
|
+
source_dir = temp_path
|
|
813
|
+
if len(extracted_items) == 1 and extracted_items[0].is_dir():
|
|
814
|
+
source_dir = extracted_items[0]
|
|
815
|
+
if tracker:
|
|
816
|
+
tracker.add("flatten", "Flatten nested directory")
|
|
817
|
+
tracker.complete("flatten")
|
|
818
|
+
elif verbose:
|
|
819
|
+
console.print(f"[cyan]Found nested directory structure[/cyan]")
|
|
820
|
+
|
|
821
|
+
for item in source_dir.iterdir():
|
|
822
|
+
dest_path = project_path / item.name
|
|
823
|
+
if item.is_dir():
|
|
824
|
+
if dest_path.exists():
|
|
825
|
+
if verbose and not tracker:
|
|
826
|
+
console.print(f"[yellow]Merging directory:[/yellow] {item.name}")
|
|
827
|
+
for sub_item in item.rglob('*'):
|
|
828
|
+
if sub_item.is_file():
|
|
829
|
+
rel_path = sub_item.relative_to(item)
|
|
830
|
+
dest_file = dest_path / rel_path
|
|
831
|
+
dest_file.parent.mkdir(parents=True, exist_ok=True)
|
|
832
|
+
# Special handling for .vscode/settings.json - merge instead of overwrite
|
|
833
|
+
if dest_file.name == "settings.json" and dest_file.parent.name == ".vscode":
|
|
834
|
+
handle_vscode_settings(sub_item, dest_file, rel_path, verbose, tracker)
|
|
835
|
+
else:
|
|
836
|
+
shutil.copy2(sub_item, dest_file)
|
|
837
|
+
else:
|
|
838
|
+
shutil.copytree(item, dest_path)
|
|
839
|
+
else:
|
|
840
|
+
if dest_path.exists() and verbose and not tracker:
|
|
841
|
+
console.print(f"[yellow]Overwriting file:[/yellow] {item.name}")
|
|
842
|
+
shutil.copy2(item, dest_path)
|
|
843
|
+
if verbose and not tracker:
|
|
844
|
+
console.print(f"[cyan]Template files merged into current directory[/cyan]")
|
|
845
|
+
else:
|
|
846
|
+
zip_ref.extractall(project_path)
|
|
847
|
+
|
|
848
|
+
extracted_items = list(project_path.iterdir())
|
|
849
|
+
if tracker:
|
|
850
|
+
tracker.start("extracted-summary")
|
|
851
|
+
tracker.complete("extracted-summary", f"{len(extracted_items)} top-level items")
|
|
852
|
+
elif verbose:
|
|
853
|
+
console.print(f"[cyan]Extracted {len(extracted_items)} items to {project_path}:[/cyan]")
|
|
854
|
+
for item in extracted_items:
|
|
855
|
+
console.print(f" - {item.name} ({'dir' if item.is_dir() else 'file'})")
|
|
856
|
+
|
|
857
|
+
if len(extracted_items) == 1 and extracted_items[0].is_dir():
|
|
858
|
+
nested_dir = extracted_items[0]
|
|
859
|
+
temp_move_dir = project_path.parent / f"{project_path.name}_temp"
|
|
860
|
+
|
|
861
|
+
shutil.move(str(nested_dir), str(temp_move_dir))
|
|
862
|
+
|
|
863
|
+
project_path.rmdir()
|
|
864
|
+
|
|
865
|
+
shutil.move(str(temp_move_dir), str(project_path))
|
|
866
|
+
if tracker:
|
|
867
|
+
tracker.add("flatten", "Flatten nested directory")
|
|
868
|
+
tracker.complete("flatten")
|
|
869
|
+
elif verbose:
|
|
870
|
+
console.print(f"[cyan]Flattened nested directory structure[/cyan]")
|
|
871
|
+
|
|
872
|
+
except Exception as e:
|
|
873
|
+
if tracker:
|
|
874
|
+
tracker.error("extract", str(e))
|
|
875
|
+
else:
|
|
876
|
+
if verbose:
|
|
877
|
+
console.print(f"[red]Error extracting template:[/red] {e}")
|
|
878
|
+
if debug:
|
|
879
|
+
console.print(Panel(str(e), title="Extraction Error", border_style="red"))
|
|
880
|
+
|
|
881
|
+
if not is_current_dir and project_path.exists():
|
|
882
|
+
shutil.rmtree(project_path)
|
|
883
|
+
raise typer.Exit(1)
|
|
884
|
+
else:
|
|
885
|
+
if tracker:
|
|
886
|
+
tracker.complete("extract")
|
|
887
|
+
finally:
|
|
888
|
+
if tracker:
|
|
889
|
+
tracker.add("cleanup", "Remove temporary archive")
|
|
890
|
+
|
|
891
|
+
if zip_path.exists():
|
|
892
|
+
zip_path.unlink()
|
|
893
|
+
if tracker:
|
|
894
|
+
tracker.complete("cleanup")
|
|
895
|
+
elif verbose:
|
|
896
|
+
console.print(f"Cleaned up: {zip_path.name}")
|
|
897
|
+
|
|
898
|
+
return project_path
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None:
|
|
902
|
+
"""Ensure POSIX .sh scripts under .specify/scripts (recursively) have execute bits (no-op on Windows)."""
|
|
903
|
+
if os.name == "nt":
|
|
904
|
+
return # Windows: skip silently
|
|
905
|
+
scripts_root = project_path / ".specify" / "scripts"
|
|
906
|
+
if not scripts_root.is_dir():
|
|
907
|
+
return
|
|
908
|
+
failures: list[str] = []
|
|
909
|
+
updated = 0
|
|
910
|
+
for script in scripts_root.rglob("*.sh"):
|
|
911
|
+
try:
|
|
912
|
+
if script.is_symlink() or not script.is_file():
|
|
913
|
+
continue
|
|
914
|
+
try:
|
|
915
|
+
with script.open("rb") as f:
|
|
916
|
+
if f.read(2) != b"#!":
|
|
917
|
+
continue
|
|
918
|
+
except Exception:
|
|
919
|
+
continue
|
|
920
|
+
st = script.stat(); mode = st.st_mode
|
|
921
|
+
if mode & 0o111:
|
|
922
|
+
continue
|
|
923
|
+
new_mode = mode
|
|
924
|
+
if mode & 0o400: new_mode |= 0o100
|
|
925
|
+
if mode & 0o040: new_mode |= 0o010
|
|
926
|
+
if mode & 0o004: new_mode |= 0o001
|
|
927
|
+
if not (new_mode & 0o100):
|
|
928
|
+
new_mode |= 0o100
|
|
929
|
+
os.chmod(script, new_mode)
|
|
930
|
+
updated += 1
|
|
931
|
+
except Exception as e:
|
|
932
|
+
failures.append(f"{script.relative_to(scripts_root)}: {e}")
|
|
933
|
+
if tracker:
|
|
934
|
+
detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "")
|
|
935
|
+
tracker.add("chmod", "Set script permissions recursively")
|
|
936
|
+
(tracker.error if failures else tracker.complete)("chmod", detail)
|
|
937
|
+
else:
|
|
938
|
+
if updated:
|
|
939
|
+
console.print(f"[cyan]Updated execute permissions on {updated} script(s) recursively[/cyan]")
|
|
940
|
+
if failures:
|
|
941
|
+
console.print("[yellow]Some scripts could not be updated:[/yellow]")
|
|
942
|
+
for f in failures:
|
|
943
|
+
console.print(f" - {f}")
|
|
944
|
+
|
|
945
|
+
@app.command()
|
|
946
|
+
def init(
|
|
947
|
+
project_name: str = typer.Argument(None, help="Name for your new project directory (optional if using --here, or use '.' for current directory)"),
|
|
948
|
+
ai_assistant: str = typer.Option(None, "--ai", help="AI assistant to use: claude, gemini, copilot, cursor-agent, qwen, opencode, codex, windsurf, kilocode, auggie, codebuddy, amp, shai, q, bob, or qoder "),
|
|
949
|
+
script_type: str = typer.Option(None, "--script", help="Script type to use: sh or ps"),
|
|
950
|
+
ignore_agent_tools: bool = typer.Option(False, "--ignore-agent-tools", help="Skip checks for AI agent tools like Claude Code"),
|
|
951
|
+
no_git: bool = typer.Option(False, "--no-git", help="Skip git repository initialization"),
|
|
952
|
+
here: bool = typer.Option(False, "--here", help="Initialize project in the current directory instead of creating a new one"),
|
|
953
|
+
force: bool = typer.Option(False, "--force", help="Force merge/overwrite when using --here (skip confirmation)"),
|
|
954
|
+
skip_tls: bool = typer.Option(False, "--skip-tls", help="Skip SSL/TLS verification (not recommended)"),
|
|
955
|
+
debug: bool = typer.Option(False, "--debug", help="Show verbose diagnostic output for network and extraction failures"),
|
|
956
|
+
github_token: str = typer.Option(None, "--github-token", help="GitHub token to use for API requests (or set GH_TOKEN or GITHUB_TOKEN environment variable)"),
|
|
957
|
+
):
|
|
958
|
+
"""
|
|
959
|
+
Initialize a new Specify project from the latest template.
|
|
960
|
+
|
|
961
|
+
This command will:
|
|
962
|
+
1. Check that required tools are installed (git is optional)
|
|
963
|
+
2. Let you choose your AI assistant
|
|
964
|
+
3. Download the appropriate template from GitHub
|
|
965
|
+
4. Extract the template to a new project directory or current directory
|
|
966
|
+
5. Initialize a fresh git repository (if not --no-git and no existing repo)
|
|
967
|
+
6. Optionally set up AI assistant commands
|
|
968
|
+
|
|
969
|
+
Examples:
|
|
970
|
+
specify init my-project
|
|
971
|
+
specify init my-project --ai claude
|
|
972
|
+
specify init my-project --ai copilot --no-git
|
|
973
|
+
specify init --ignore-agent-tools my-project
|
|
974
|
+
specify init . --ai claude # Initialize in current directory
|
|
975
|
+
specify init . # Initialize in current directory (interactive AI selection)
|
|
976
|
+
specify init --here --ai claude # Alternative syntax for current directory
|
|
977
|
+
specify init --here --ai codex
|
|
978
|
+
specify init --here --ai codebuddy
|
|
979
|
+
specify init --here
|
|
980
|
+
specify init --here --force # Skip confirmation when current directory not empty
|
|
981
|
+
"""
|
|
982
|
+
|
|
983
|
+
show_banner()
|
|
984
|
+
|
|
985
|
+
if project_name == ".":
|
|
986
|
+
here = True
|
|
987
|
+
project_name = None # Clear project_name to use existing validation logic
|
|
988
|
+
|
|
989
|
+
if here and project_name:
|
|
990
|
+
console.print("[red]Error:[/red] Cannot specify both project name and --here flag")
|
|
991
|
+
raise typer.Exit(1)
|
|
992
|
+
|
|
993
|
+
if not here and not project_name:
|
|
994
|
+
console.print("[red]Error:[/red] Must specify either a project name, use '.' for current directory, or use --here flag")
|
|
995
|
+
raise typer.Exit(1)
|
|
996
|
+
|
|
997
|
+
if here:
|
|
998
|
+
project_name = Path.cwd().name
|
|
999
|
+
project_path = Path.cwd()
|
|
1000
|
+
|
|
1001
|
+
existing_items = list(project_path.iterdir())
|
|
1002
|
+
if existing_items:
|
|
1003
|
+
console.print(f"[yellow]Warning:[/yellow] Current directory is not empty ({len(existing_items)} items)")
|
|
1004
|
+
console.print("[yellow]Template files will be merged with existing content and may overwrite existing files[/yellow]")
|
|
1005
|
+
if force:
|
|
1006
|
+
console.print("[cyan]--force supplied: skipping confirmation and proceeding with merge[/cyan]")
|
|
1007
|
+
else:
|
|
1008
|
+
response = typer.confirm("Do you want to continue?")
|
|
1009
|
+
if not response:
|
|
1010
|
+
console.print("[yellow]Operation cancelled[/yellow]")
|
|
1011
|
+
raise typer.Exit(0)
|
|
1012
|
+
else:
|
|
1013
|
+
project_path = Path(project_name).resolve()
|
|
1014
|
+
if project_path.exists():
|
|
1015
|
+
error_panel = Panel(
|
|
1016
|
+
f"Directory '[cyan]{project_name}[/cyan]' already exists\n"
|
|
1017
|
+
"Please choose a different project name or remove the existing directory.",
|
|
1018
|
+
title="[red]Directory Conflict[/red]",
|
|
1019
|
+
border_style="red",
|
|
1020
|
+
padding=(1, 2)
|
|
1021
|
+
)
|
|
1022
|
+
console.print()
|
|
1023
|
+
console.print(error_panel)
|
|
1024
|
+
raise typer.Exit(1)
|
|
1025
|
+
|
|
1026
|
+
current_dir = Path.cwd()
|
|
1027
|
+
|
|
1028
|
+
setup_lines = [
|
|
1029
|
+
"[cyan]Specify Project Setup[/cyan]",
|
|
1030
|
+
"",
|
|
1031
|
+
f"{'Project':<15} [green]{project_path.name}[/green]",
|
|
1032
|
+
f"{'Working Path':<15} [dim]{current_dir}[/dim]",
|
|
1033
|
+
]
|
|
1034
|
+
|
|
1035
|
+
if not here:
|
|
1036
|
+
setup_lines.append(f"{'Target Path':<15} [dim]{project_path}[/dim]")
|
|
1037
|
+
|
|
1038
|
+
console.print(Panel("\n".join(setup_lines), border_style="cyan", padding=(1, 2)))
|
|
1039
|
+
|
|
1040
|
+
should_init_git = False
|
|
1041
|
+
if not no_git:
|
|
1042
|
+
should_init_git = check_tool("git")
|
|
1043
|
+
if not should_init_git:
|
|
1044
|
+
console.print("[yellow]Git not found - will skip repository initialization[/yellow]")
|
|
1045
|
+
|
|
1046
|
+
if ai_assistant:
|
|
1047
|
+
if ai_assistant not in AGENT_CONFIG:
|
|
1048
|
+
console.print(f"[red]Error:[/red] Invalid AI assistant '{ai_assistant}'. Choose from: {', '.join(AGENT_CONFIG.keys())}")
|
|
1049
|
+
raise typer.Exit(1)
|
|
1050
|
+
selected_ai = ai_assistant
|
|
1051
|
+
else:
|
|
1052
|
+
# Create options dict for selection (agent_key: display_name)
|
|
1053
|
+
ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()}
|
|
1054
|
+
selected_ai = select_with_arrows(
|
|
1055
|
+
ai_choices,
|
|
1056
|
+
"Choose your AI assistant:",
|
|
1057
|
+
"copilot"
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
if not ignore_agent_tools:
|
|
1061
|
+
agent_config = AGENT_CONFIG.get(selected_ai)
|
|
1062
|
+
if agent_config and agent_config["requires_cli"]:
|
|
1063
|
+
install_url = agent_config["install_url"]
|
|
1064
|
+
if not check_tool(selected_ai):
|
|
1065
|
+
error_panel = Panel(
|
|
1066
|
+
f"[cyan]{selected_ai}[/cyan] not found\n"
|
|
1067
|
+
f"Install from: [cyan]{install_url}[/cyan]\n"
|
|
1068
|
+
f"{agent_config['name']} is required to continue with this project type.\n\n"
|
|
1069
|
+
"Tip: Use [cyan]--ignore-agent-tools[/cyan] to skip this check",
|
|
1070
|
+
title="[red]Agent Detection Error[/red]",
|
|
1071
|
+
border_style="red",
|
|
1072
|
+
padding=(1, 2)
|
|
1073
|
+
)
|
|
1074
|
+
console.print()
|
|
1075
|
+
console.print(error_panel)
|
|
1076
|
+
raise typer.Exit(1)
|
|
1077
|
+
|
|
1078
|
+
if script_type:
|
|
1079
|
+
if script_type not in SCRIPT_TYPE_CHOICES:
|
|
1080
|
+
console.print(f"[red]Error:[/red] Invalid script type '{script_type}'. Choose from: {', '.join(SCRIPT_TYPE_CHOICES.keys())}")
|
|
1081
|
+
raise typer.Exit(1)
|
|
1082
|
+
selected_script = script_type
|
|
1083
|
+
else:
|
|
1084
|
+
default_script = "ps" if os.name == "nt" else "sh"
|
|
1085
|
+
|
|
1086
|
+
if sys.stdin.isatty():
|
|
1087
|
+
selected_script = select_with_arrows(SCRIPT_TYPE_CHOICES, "Choose script type (or press Enter)", default_script)
|
|
1088
|
+
else:
|
|
1089
|
+
selected_script = default_script
|
|
1090
|
+
|
|
1091
|
+
console.print(f"[cyan]Selected AI assistant:[/cyan] {selected_ai}")
|
|
1092
|
+
console.print(f"[cyan]Selected script type:[/cyan] {selected_script}")
|
|
1093
|
+
|
|
1094
|
+
tracker = StepTracker("Initialize Specify Project")
|
|
1095
|
+
|
|
1096
|
+
sys._specify_tracker_active = True
|
|
1097
|
+
|
|
1098
|
+
tracker.add("precheck", "Check required tools")
|
|
1099
|
+
tracker.complete("precheck", "ok")
|
|
1100
|
+
tracker.add("ai-select", "Select AI assistant")
|
|
1101
|
+
tracker.complete("ai-select", f"{selected_ai}")
|
|
1102
|
+
tracker.add("script-select", "Select script type")
|
|
1103
|
+
tracker.complete("script-select", selected_script)
|
|
1104
|
+
for key, label in [
|
|
1105
|
+
("fetch", "Fetch latest release"),
|
|
1106
|
+
("download", "Download template"),
|
|
1107
|
+
("extract", "Extract template"),
|
|
1108
|
+
("zip-list", "Archive contents"),
|
|
1109
|
+
("extracted-summary", "Extraction summary"),
|
|
1110
|
+
("chmod", "Ensure scripts executable"),
|
|
1111
|
+
("cleanup", "Cleanup"),
|
|
1112
|
+
("git", "Initialize git repository"),
|
|
1113
|
+
("final", "Finalize")
|
|
1114
|
+
]:
|
|
1115
|
+
tracker.add(key, label)
|
|
1116
|
+
|
|
1117
|
+
# Track git error message outside Live context so it persists
|
|
1118
|
+
git_error_message = None
|
|
1119
|
+
|
|
1120
|
+
with Live(tracker.render(), console=console, refresh_per_second=8, transient=True) as live:
|
|
1121
|
+
tracker.attach_refresh(lambda: live.update(tracker.render()))
|
|
1122
|
+
try:
|
|
1123
|
+
verify = not skip_tls
|
|
1124
|
+
local_ssl_context = ssl_context if verify else False
|
|
1125
|
+
local_client = httpx.Client(verify=local_ssl_context)
|
|
1126
|
+
|
|
1127
|
+
download_and_extract_template(project_path, selected_ai, selected_script, here, verbose=False, tracker=tracker, client=local_client, debug=debug, github_token=github_token)
|
|
1128
|
+
|
|
1129
|
+
ensure_executable_scripts(project_path, tracker=tracker)
|
|
1130
|
+
|
|
1131
|
+
if not no_git:
|
|
1132
|
+
tracker.start("git")
|
|
1133
|
+
if is_git_repo(project_path):
|
|
1134
|
+
tracker.complete("git", "existing repo detected")
|
|
1135
|
+
elif should_init_git:
|
|
1136
|
+
success, error_msg = init_git_repo(project_path, quiet=True)
|
|
1137
|
+
if success:
|
|
1138
|
+
tracker.complete("git", "initialized")
|
|
1139
|
+
else:
|
|
1140
|
+
tracker.error("git", "init failed")
|
|
1141
|
+
git_error_message = error_msg
|
|
1142
|
+
else:
|
|
1143
|
+
tracker.skip("git", "git not available")
|
|
1144
|
+
else:
|
|
1145
|
+
tracker.skip("git", "--no-git flag")
|
|
1146
|
+
|
|
1147
|
+
tracker.complete("final", "project ready")
|
|
1148
|
+
except Exception as e:
|
|
1149
|
+
tracker.error("final", str(e))
|
|
1150
|
+
console.print(Panel(f"Initialization failed: {e}", title="Failure", border_style="red"))
|
|
1151
|
+
if debug:
|
|
1152
|
+
_env_pairs = [
|
|
1153
|
+
("Python", sys.version.split()[0]),
|
|
1154
|
+
("Platform", sys.platform),
|
|
1155
|
+
("CWD", str(Path.cwd())),
|
|
1156
|
+
]
|
|
1157
|
+
_label_width = max(len(k) for k, _ in _env_pairs)
|
|
1158
|
+
env_lines = [f"{k.ljust(_label_width)} → [bright_black]{v}[/bright_black]" for k, v in _env_pairs]
|
|
1159
|
+
console.print(Panel("\n".join(env_lines), title="Debug Environment", border_style="magenta"))
|
|
1160
|
+
if not here and project_path.exists():
|
|
1161
|
+
shutil.rmtree(project_path)
|
|
1162
|
+
raise typer.Exit(1)
|
|
1163
|
+
finally:
|
|
1164
|
+
pass
|
|
1165
|
+
|
|
1166
|
+
console.print(tracker.render())
|
|
1167
|
+
console.print("\n[bold green]Project ready.[/bold green]")
|
|
1168
|
+
|
|
1169
|
+
# Show git error details if initialization failed
|
|
1170
|
+
if git_error_message:
|
|
1171
|
+
console.print()
|
|
1172
|
+
git_error_panel = Panel(
|
|
1173
|
+
f"[yellow]Warning:[/yellow] Git repository initialization failed\n\n"
|
|
1174
|
+
f"{git_error_message}\n\n"
|
|
1175
|
+
f"[dim]You can initialize git manually later with:[/dim]\n"
|
|
1176
|
+
f"[cyan]cd {project_path if not here else '.'}[/cyan]\n"
|
|
1177
|
+
f"[cyan]git init[/cyan]\n"
|
|
1178
|
+
f"[cyan]git add .[/cyan]\n"
|
|
1179
|
+
f"[cyan]git commit -m \"Initial commit\"[/cyan]",
|
|
1180
|
+
title="[red]Git Initialization Failed[/red]",
|
|
1181
|
+
border_style="red",
|
|
1182
|
+
padding=(1, 2)
|
|
1183
|
+
)
|
|
1184
|
+
console.print(git_error_panel)
|
|
1185
|
+
|
|
1186
|
+
# Agent folder security notice
|
|
1187
|
+
agent_config = AGENT_CONFIG.get(selected_ai)
|
|
1188
|
+
if agent_config:
|
|
1189
|
+
agent_folder = agent_config["folder"]
|
|
1190
|
+
security_notice = Panel(
|
|
1191
|
+
f"Some agents may store credentials, auth tokens, or other identifying and private artifacts in the agent folder within your project.\n"
|
|
1192
|
+
f"Consider adding [cyan]{agent_folder}[/cyan] (or parts of it) to [cyan].gitignore[/cyan] to prevent accidental credential leakage.",
|
|
1193
|
+
title="[yellow]Agent Folder Security[/yellow]",
|
|
1194
|
+
border_style="yellow",
|
|
1195
|
+
padding=(1, 2)
|
|
1196
|
+
)
|
|
1197
|
+
console.print()
|
|
1198
|
+
console.print(security_notice)
|
|
1199
|
+
|
|
1200
|
+
steps_lines = []
|
|
1201
|
+
if not here:
|
|
1202
|
+
steps_lines.append(f"1. Go to the project folder: [cyan]cd {project_name}[/cyan]")
|
|
1203
|
+
step_num = 2
|
|
1204
|
+
else:
|
|
1205
|
+
steps_lines.append("1. You're already in the project directory!")
|
|
1206
|
+
step_num = 2
|
|
1207
|
+
|
|
1208
|
+
# Add Codex-specific setup step if needed
|
|
1209
|
+
if selected_ai == "codex":
|
|
1210
|
+
codex_path = project_path / ".codex"
|
|
1211
|
+
quoted_path = shlex.quote(str(codex_path))
|
|
1212
|
+
if os.name == "nt": # Windows
|
|
1213
|
+
cmd = f"setx CODEX_HOME {quoted_path}"
|
|
1214
|
+
else: # Unix-like systems
|
|
1215
|
+
cmd = f"export CODEX_HOME={quoted_path}"
|
|
1216
|
+
|
|
1217
|
+
steps_lines.append(f"{step_num}. Set [cyan]CODEX_HOME[/cyan] environment variable before running Codex: [cyan]{cmd}[/cyan]")
|
|
1218
|
+
step_num += 1
|
|
1219
|
+
|
|
1220
|
+
steps_lines.append(f"{step_num}. Start using slash commands with your AI agent:")
|
|
1221
|
+
|
|
1222
|
+
steps_lines.append(" 2.1 [cyan]/speckit.constitution[/] - Establish project principles")
|
|
1223
|
+
steps_lines.append(" 2.2 [cyan]/speckit.specify[/] - Create baseline specification")
|
|
1224
|
+
steps_lines.append(" 2.3 [cyan]/speckit.plan[/] - Create implementation plan")
|
|
1225
|
+
steps_lines.append(" 2.4 [cyan]/speckit.tasks[/] - Generate actionable tasks")
|
|
1226
|
+
steps_lines.append(" 2.5 [cyan]/speckit.implement[/] - Execute implementation")
|
|
1227
|
+
|
|
1228
|
+
steps_panel = Panel("\n".join(steps_lines), title="Next Steps", border_style="cyan", padding=(1,2))
|
|
1229
|
+
console.print()
|
|
1230
|
+
console.print(steps_panel)
|
|
1231
|
+
|
|
1232
|
+
enhancement_lines = [
|
|
1233
|
+
"Optional commands that you can use for your specs [bright_black](improve quality & confidence)[/bright_black]",
|
|
1234
|
+
"",
|
|
1235
|
+
f"○ [cyan]/speckit.clarify[/] [bright_black](optional)[/bright_black] - Ask structured questions to de-risk ambiguous areas before planning (run before [cyan]/speckit.plan[/] if used)",
|
|
1236
|
+
f"○ [cyan]/speckit.analyze[/] [bright_black](optional)[/bright_black] - Cross-artifact consistency & alignment report (after [cyan]/speckit.tasks[/], before [cyan]/speckit.implement[/])",
|
|
1237
|
+
f"○ [cyan]/speckit.checklist[/] [bright_black](optional)[/bright_black] - Generate quality checklists to validate requirements completeness, clarity, and consistency (after [cyan]/speckit.plan[/])"
|
|
1238
|
+
]
|
|
1239
|
+
enhancements_panel = Panel("\n".join(enhancement_lines), title="Enhancement Commands", border_style="cyan", padding=(1,2))
|
|
1240
|
+
console.print()
|
|
1241
|
+
console.print(enhancements_panel)
|
|
1242
|
+
|
|
1243
|
+
@app.command()
|
|
1244
|
+
def check():
|
|
1245
|
+
"""Check that all required tools are installed."""
|
|
1246
|
+
show_banner()
|
|
1247
|
+
console.print("[bold]Checking for installed tools...[/bold]\n")
|
|
1248
|
+
|
|
1249
|
+
tracker = StepTracker("Check Available Tools")
|
|
1250
|
+
|
|
1251
|
+
tracker.add("git", "Git version control")
|
|
1252
|
+
git_ok = check_tool("git", tracker=tracker)
|
|
1253
|
+
|
|
1254
|
+
agent_results = {}
|
|
1255
|
+
for agent_key, agent_config in AGENT_CONFIG.items():
|
|
1256
|
+
agent_name = agent_config["name"]
|
|
1257
|
+
requires_cli = agent_config["requires_cli"]
|
|
1258
|
+
|
|
1259
|
+
tracker.add(agent_key, agent_name)
|
|
1260
|
+
|
|
1261
|
+
if requires_cli:
|
|
1262
|
+
agent_results[agent_key] = check_tool(agent_key, tracker=tracker)
|
|
1263
|
+
else:
|
|
1264
|
+
# IDE-based agent - skip CLI check and mark as optional
|
|
1265
|
+
tracker.skip(agent_key, "IDE-based, no CLI check")
|
|
1266
|
+
agent_results[agent_key] = False # Don't count IDE agents as "found"
|
|
1267
|
+
|
|
1268
|
+
# Check VS Code variants (not in agent config)
|
|
1269
|
+
tracker.add("code", "Visual Studio Code")
|
|
1270
|
+
code_ok = check_tool("code", tracker=tracker)
|
|
1271
|
+
|
|
1272
|
+
tracker.add("code-insiders", "Visual Studio Code Insiders")
|
|
1273
|
+
code_insiders_ok = check_tool("code-insiders", tracker=tracker)
|
|
1274
|
+
|
|
1275
|
+
console.print(tracker.render())
|
|
1276
|
+
|
|
1277
|
+
console.print("\n[bold green]Specify CLI is ready to use![/bold green]")
|
|
1278
|
+
|
|
1279
|
+
if not git_ok:
|
|
1280
|
+
console.print("[dim]Tip: Install git for repository management[/dim]")
|
|
1281
|
+
|
|
1282
|
+
if not any(agent_results.values()):
|
|
1283
|
+
console.print("[dim]Tip: Install an AI assistant for the best experience[/dim]")
|
|
1284
|
+
|
|
1285
|
+
@app.command()
|
|
1286
|
+
def version():
|
|
1287
|
+
"""Display version and system information."""
|
|
1288
|
+
import platform
|
|
1289
|
+
import importlib.metadata
|
|
1290
|
+
|
|
1291
|
+
show_banner()
|
|
1292
|
+
|
|
1293
|
+
# Get CLI version from package metadata
|
|
1294
|
+
cli_version = "unknown"
|
|
1295
|
+
try:
|
|
1296
|
+
cli_version = importlib.metadata.version("specify-cli")
|
|
1297
|
+
except Exception:
|
|
1298
|
+
# Fallback: try reading from pyproject.toml if running from source
|
|
1299
|
+
try:
|
|
1300
|
+
import tomllib
|
|
1301
|
+
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
|
|
1302
|
+
if pyproject_path.exists():
|
|
1303
|
+
with open(pyproject_path, "rb") as f:
|
|
1304
|
+
data = tomllib.load(f)
|
|
1305
|
+
cli_version = data.get("project", {}).get("version", "unknown")
|
|
1306
|
+
except Exception:
|
|
1307
|
+
pass
|
|
1308
|
+
|
|
1309
|
+
# Fetch latest template release version
|
|
1310
|
+
repo_owner = "github"
|
|
1311
|
+
repo_name = "spec-kit"
|
|
1312
|
+
api_url = f"https://api.github.com/repos/{repo_owner}/{repo_name}/releases/latest"
|
|
1313
|
+
|
|
1314
|
+
template_version = "unknown"
|
|
1315
|
+
release_date = "unknown"
|
|
1316
|
+
|
|
1317
|
+
try:
|
|
1318
|
+
response = client.get(
|
|
1319
|
+
api_url,
|
|
1320
|
+
timeout=10,
|
|
1321
|
+
follow_redirects=True,
|
|
1322
|
+
headers=_github_auth_headers(),
|
|
1323
|
+
)
|
|
1324
|
+
if response.status_code == 200:
|
|
1325
|
+
release_data = response.json()
|
|
1326
|
+
template_version = release_data.get("tag_name", "unknown")
|
|
1327
|
+
# Remove 'v' prefix if present
|
|
1328
|
+
if template_version.startswith("v"):
|
|
1329
|
+
template_version = template_version[1:]
|
|
1330
|
+
release_date = release_data.get("published_at", "unknown")
|
|
1331
|
+
if release_date != "unknown":
|
|
1332
|
+
# Format the date nicely
|
|
1333
|
+
try:
|
|
1334
|
+
dt = datetime.fromisoformat(release_date.replace('Z', '+00:00'))
|
|
1335
|
+
release_date = dt.strftime("%Y-%m-%d")
|
|
1336
|
+
except Exception:
|
|
1337
|
+
pass
|
|
1338
|
+
except Exception:
|
|
1339
|
+
pass
|
|
1340
|
+
|
|
1341
|
+
info_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
1342
|
+
info_table.add_column("Key", style="cyan", justify="right")
|
|
1343
|
+
info_table.add_column("Value", style="white")
|
|
1344
|
+
|
|
1345
|
+
info_table.add_row("CLI Version", cli_version)
|
|
1346
|
+
info_table.add_row("Template Version", template_version)
|
|
1347
|
+
info_table.add_row("Released", release_date)
|
|
1348
|
+
info_table.add_row("", "")
|
|
1349
|
+
info_table.add_row("Python", platform.python_version())
|
|
1350
|
+
info_table.add_row("Platform", platform.system())
|
|
1351
|
+
info_table.add_row("Architecture", platform.machine())
|
|
1352
|
+
info_table.add_row("OS Version", platform.version())
|
|
1353
|
+
|
|
1354
|
+
panel = Panel(
|
|
1355
|
+
info_table,
|
|
1356
|
+
title="[bold cyan]Specify CLI Information[/bold cyan]",
|
|
1357
|
+
border_style="cyan",
|
|
1358
|
+
padding=(1, 2)
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
console.print(panel)
|
|
1362
|
+
console.print()
|
|
1363
|
+
|
|
1364
|
+
def main():
|
|
1365
|
+
app()
|
|
1366
|
+
|
|
1367
|
+
if __name__ == "__main__":
|
|
1368
|
+
main()
|
|
1369
|
+
|