diff --git a/gitnexus/src/cli/ci-setup.ts b/gitnexus/src/cli/ci-setup.ts index a1d54a23b..2f632061a 100644 --- a/gitnexus/src/cli/ci-setup.ts +++ b/gitnexus/src/cli/ci-setup.ts @@ -35,9 +35,7 @@ export const ciSetupCommand = async (options?: { const detect = await detectEnvironment(cwd); if (!detect.gitRoot) { - console.error( - '✗ Not a git repository. Run `gitnexus ci-setup` from inside a git repo root.', - ); + console.log('✗ Not a git repository. Run `gitnexus ci-setup` from inside a git repo root.'); process.exit(1); } @@ -51,14 +49,23 @@ export const ciSetupCommand = async (options?: { if (detect.hasDocker) { console.log(' ✓ Docker: docker-compose or Dockerfile found'); } - console.log(` ${detect.portAvailable ? '✓' : '⚠'} Port 4747: ${detect.portAvailable ? 'available' : 'in use (serve may already be running)'}`); + console.log( + ` ${detect.portAvailable ? '✓' : '⚠'} Port 4747: ${detect.portAvailable ? 'available' : 'in use (serve may already be running)'}`, + ); console.log(' ⚠ License: PolyForm-Noncommercial — confirm non-commercial use\n'); // Parse and validate options from commander flags const partial: Partial = {}; if (options?.ci) partial.ci = options.ci as CiSystem; if (options?.deploy) partial.deploy = options.deploy as DeployTarget; - if (options?.port) partial.port = parseInt(options.port, 10); + if (options?.port) { + const portNum = parseInt(options.port as string, 10); + if (isNaN(portNum) || portNum < 1 || portNum > 65534) { + console.log(`✗ Invalid port: "${options.port}". Must be an integer between 1 and 65534.`); + process.exit(1); + } + partial.port = portNum; + } if (options?.auth) partial.auth = options.auth as AuthMode; if (options?.branchStrategy) partial.branchStrategy = options.branchStrategy as BranchStrategy; if (options?.dryRun !== undefined) partial.dryRun = options.dryRun; @@ -120,13 +127,15 @@ async function applyFiles( if (existingContent !== null && !opts.yes) { console.log(`\n⚠ ${file.relativePath} already exists and differs.`); - if (!opts.yes) { - const { confirm } = await import('@inquirer/prompts'); - const ok = await confirm({ message: `Overwrite ${file.relativePath}?`, default: false }); - if (!ok) { - result.skipped.push(`${file.relativePath} (skipped by user)`); - continue; - } + if (!process.stdin.isTTY) { + result.skipped.push(`${file.relativePath} (differs, non-TTY — use --yes to overwrite)`); + continue; + } + const { confirm } = await import('@inquirer/prompts'); + const ok = await confirm({ message: `Overwrite ${file.relativePath}?`, default: false }); + if (!ok) { + result.skipped.push(`${file.relativePath} (skipped by user)`); + continue; } } @@ -175,12 +184,16 @@ function printResult(result: CiSetupResult, opts: CiSetupOptions): void { } if (opts.ci === 'github-actions' || opts.ci === 'both') { - console.log(` ${step++}. Commit .github/workflows/gitnexus-ci.yml and push to trigger the first index.`); + console.log( + ` ${step++}. Commit .github/workflows/gitnexus-ci.yml and push to trigger the first index.`, + ); } if (opts.ci === 'azure-devops' || opts.ci === 'both') { console.log(` ${step++}. Import azure-pipelines-gitnexus.yml into Azure DevOps and run it.`); } - console.log(` ${step++}. Follow GITNEXUS.md to connect Claude Code / Cursor to the shared server.`); + console.log( + ` ${step++}. Follow GITNEXUS.md to connect Claude Code / Cursor to the shared server.`, + ); console.log(''); } diff --git a/gitnexus/src/cli/ci-setup/templates.ts b/gitnexus/src/cli/ci-setup/templates.ts index b6390a46a..24aa3e247 100644 --- a/gitnexus/src/cli/ci-setup/templates.ts +++ b/gitnexus/src/cli/ci-setup/templates.ts @@ -24,10 +24,18 @@ name: GitNexus Index ${onBlock} +permissions: + contents: read + +concurrency: + group: gitnexus-$\{{ github.ref }} + cancel-in-progress: true + jobs: gitnexus-index: name: Index repository runs-on: ubuntu-latest + timeout-minutes: 15 steps: - uses: actions/checkout@v4 @@ -136,7 +144,7 @@ services: GITNEXUS_HOME: /data/gitnexus restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:${opts.port}/api/health"] + test: ["CMD", "curl", "-f", "http://localhost:4747/api/health"] interval: 30s timeout: 5s retries: 3 @@ -172,7 +180,7 @@ services: gitnexus: image: ghcr.io/abhigyanpatwari/gitnexus:latest ports: - - "\${GITNEXUS_PORT:-${opts.port}}:${opts.port}" + - "\${GITNEXUS_PORT:-${opts.port}}:4747" volumes: - gitnexus-data:/data/gitnexus - \${WORKSPACE_DIR:-./workspace}:/workspace:ro @@ -180,7 +188,7 @@ services: GITNEXUS_HOME: /data/gitnexus restart: unless-stopped healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:${opts.port}/api/health"] + test: ["CMD", "curl", "-f", "http://localhost:4747/api/health"] interval: 30s timeout: 5s retries: 3 @@ -201,7 +209,7 @@ function buildCaddyfile(opts: CiSetupOptions): string { @authorized header Authorization "Bearer {env.GITNEXUS_TOKEN}" handle @authorized { - reverse_proxy gitnexus:${opts.port} + reverse_proxy gitnexus:4747 } respond "Unauthorized" 401 @@ -277,7 +285,7 @@ az containerapp create \\ --resource-group "\$RESOURCE_GROUP" \\ --environment "\$ENVIRONMENT" \\ --image ghcr.io/abhigyanpatwari/gitnexus:latest \\ - --target-port ${opts.port} \\ + --target-port 4747 \\ --ingress internal \\ --env-vars "GITNEXUS_HOME=/data/gitnexus" \\ --volume-name gitnexus-data \\ @@ -308,15 +316,10 @@ function buildMcpSnippet(opts: CiSetupOptions): string { }` : ''; + const commentLine = urlComment.replace(/^\s*\/\/\s*/, '').trim(); return `{ - // GitNexus shared MCP server snippet — merge into ~/.claude/settings.json mcpServers block. - // Do NOT auto-apply: use 'gitnexus setup' for personal stdio MCP; this snippet is for the - // shared HTTP server variant. - // - // NOTE: Verify the exact Claude Code HTTP MCP entry format against current docs - // (https://github.com/anthropics/claude-code) before applying — field names may evolve. - // -${urlComment} + "_comment": "${commentLine}", + "_note": "Merge into ~/.claude/settings.json mcpServers block. Verify the exact Claude Code HTTP MCP entry format against current docs before applying.", "mcpServers": { "gitnexus": { "type": "http", @@ -449,7 +452,7 @@ When the CI workflow runs \`analyze --skills\`, GitNexus generates repo-specific - \`.claude/skills/gitnexus/\` — standard GitNexus MCP skills (query, impact, context, detect-changes) - \`.claude/skills/generated/\` — community-detected area skills (one file per functional cluster) -These are committed to the repository so every developer's Claude Code session has them available automatically. +These are written into the repository by the CI workflow. To share them with your team, add a \`git add .claude/skills/ && git commit\` step after \`analyze --skills\` in your workflow, or commit them manually after the first run. --- diff --git a/gitnexus/src/cli/help-i18n.ts b/gitnexus/src/cli/help-i18n.ts index 5fb42dbe1..f342e3430 100644 --- a/gitnexus/src/cli/help-i18n.ts +++ b/gitnexus/src/cli/help-i18n.ts @@ -12,6 +12,7 @@ const TITLE_KEYS = { const COMMAND_DESCRIPTION_KEYS = { '': 'help.description.root', setup: 'help.command.setup.description', + 'ci-setup': 'help.command.ciSetup.description', analyze: 'help.command.analyze.description', index: 'help.command.index.description', serve: 'help.command.serve.description', @@ -44,6 +45,15 @@ const COMMAND_DESCRIPTION_KEYS = { const OPTION_DESCRIPTION_KEYS = { '|-V, --version': 'help.option.version', + 'ci-setup|--ci ': 'help.option.ciSetup.ci', + 'ci-setup|--deploy ': 'help.option.ciSetup.deploy', + 'ci-setup|--port ': 'help.option.ciSetup.port', + 'ci-setup|--auth ': 'help.option.ciSetup.auth', + 'ci-setup|--branch-strategy ': 'help.option.ciSetup.branchStrategy', + 'ci-setup|--dry-run': 'help.option.ciSetup.dryRun', + 'ci-setup|--apply': 'help.option.ciSetup.apply', + 'ci-setup|--yes': 'help.option.ciSetup.yes', + 'ci-setup|--output-dir ': 'help.option.ciSetup.outputDir', 'analyze|-f, --force': 'help.option.analyze.force', 'analyze|--repair-fts': 'help.option.analyze.repairFts', 'analyze|--embeddings [limit]': 'help.option.analyze.embeddings', diff --git a/gitnexus/src/cli/i18n/en.ts b/gitnexus/src/cli/i18n/en.ts index 040008570..78963da3f 100644 --- a/gitnexus/src/cli/i18n/en.ts +++ b/gitnexus/src/cli/i18n/en.ts @@ -106,6 +106,8 @@ export const en = { 'help.option.version': 'output the version number', 'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, OpenCode, Codex', + 'help.command.ciSetup.description': + 'Generate CI/CD workflows, Docker Compose, and MCP config for a shared team GitNexus server', 'help.command.analyze.description': 'Index a repository (full analysis)', 'help.command.index.description': 'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)', @@ -146,6 +148,16 @@ export const en = { 'Cross-repo impact for a symbol in one member repo of a group', 'help.command.group.query.description': 'Search execution flows across all repos in a group', 'help.command.group.contracts.description': 'Inspect Contract Registry', + 'help.option.ciSetup.ci': 'CI/CD system: github-actions, azure-devops, or both', + 'help.option.ciSetup.deploy': 'Deploy target: docker, azure-container-app, or both', + 'help.option.ciSetup.port': 'Host port to bind (container always runs on 4747)', + 'help.option.ciSetup.auth': 'Auth mode: token (Caddy proxy) or none', + 'help.option.ciSetup.branchStrategy': 'Index strategy: pr-scoped or main-only', + 'help.option.ciSetup.dryRun': + 'Print generated files without writing (default when no mode flag given)', + 'help.option.ciSetup.apply': 'Write files with per-file confirmation gates', + 'help.option.ciSetup.yes': 'Skip per-file confirmation prompts (use with --apply)', + 'help.option.ciSetup.outputDir': 'Directory to write generated files (default: git root)', 'help.option.analyze.force': 'Force full re-index even if up to date', 'help.option.analyze.repairFts': 'Repair/rebuild search FTS indexes without full re-analysis', 'help.option.analyze.embeddings': diff --git a/gitnexus/src/cli/i18n/zh-CN.ts b/gitnexus/src/cli/i18n/zh-CN.ts index 6d1efb77a..333fc4824 100644 --- a/gitnexus/src/cli/i18n/zh-CN.ts +++ b/gitnexus/src/cli/i18n/zh-CN.ts @@ -108,6 +108,8 @@ export const zhCN = { 'help.option.help': '显示命令帮助', 'help.option.version': '输出版本号', 'help.command.setup.description': '一次性设置:为 Cursor、Claude Code、OpenCode、Codex 配置 MCP', + 'help.command.ciSetup.description': + '为团队共享 GitNexus 服务器生成 CI/CD 工作流、Docker Compose 和 MCP 配置', 'help.command.analyze.description': '索引仓库(完整分析)', 'help.command.index.description': '将现有 .gitnexus/ 文件夹注册到全局注册表(无需重新分析)', 'help.command.serve.description': '启动供 Web UI 连接的本地 HTTP 服务器', @@ -139,6 +141,15 @@ export const zhCN = { 'help.command.group.impact.description': '分析仓库组中某个成员仓库符号的跨仓库影响', 'help.command.group.query.description': '跨仓库组所有仓库搜索执行流程', 'help.command.group.contracts.description': '查看 Contract Registry', + 'help.option.ciSetup.ci': 'CI/CD 系统:github-actions、azure-devops 或 both', + 'help.option.ciSetup.deploy': '部署目标:docker、azure-container-app 或 both', + 'help.option.ciSetup.port': '绑定的主机端口(容器始终运行在 4747)', + 'help.option.ciSetup.auth': '认证模式:token(Caddy 代理)或 none', + 'help.option.ciSetup.branchStrategy': '索引策略:pr-scoped 或 main-only', + 'help.option.ciSetup.dryRun': '打印生成的文件而不写入(未指定模式标志时的默认行为)', + 'help.option.ciSetup.apply': '写入文件,每个文件有独立确认步骤', + 'help.option.ciSetup.yes': '跳过逐文件确认提示(与 --apply 配合使用)', + 'help.option.ciSetup.outputDir': '生成文件的写入目录(默认:git 根目录)', 'help.option.analyze.force': '即使已是最新也强制完整重建索引', 'help.option.analyze.repairFts': '修复/重建搜索 FTS 索引,不执行完整重新分析', 'help.option.analyze.embeddings': diff --git a/gitnexus/test/unit/cli-index-help.test.ts b/gitnexus/test/unit/cli-index-help.test.ts index 3beaeff9a..3267e7fea 100644 --- a/gitnexus/test/unit/cli-index-help.test.ts +++ b/gitnexus/test/unit/cli-index-help.test.ts @@ -31,6 +31,7 @@ function runRootHelp(env: NodeJS.ProcessEnv = {}) { const allHelpCommands = [ [], ['setup'], + ['ci-setup'], ['analyze'], ['index'], ['serve'],