From c08e0eb292d57f5e9202e1e97737d67fdeabf897 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 2 Jun 2026 13:31:34 +0000 Subject: [PATCH] feat(cli): add npm package wrapper for Codra CLI --- README.md | 11 ++++ packages/codra-npm-cli/.gitignore | 1 + packages/codra-npm-cli/README.md | 60 +++++++++++++++++++++ packages/codra-npm-cli/bin/codra.js | 66 +++++++++++++++++++++++ packages/codra-npm-cli/package.json | 31 +++++++++++ packages/codra-npm-cli/scripts/build.js | 53 ++++++++++++++++++ packages/codra-npm-cli/scripts/test.js | 71 +++++++++++++++++++++++++ 7 files changed, 293 insertions(+) create mode 100644 packages/codra-npm-cli/.gitignore create mode 100644 packages/codra-npm-cli/README.md create mode 100755 packages/codra-npm-cli/bin/codra.js create mode 100644 packages/codra-npm-cli/package.json create mode 100644 packages/codra-npm-cli/scripts/build.js create mode 100644 packages/codra-npm-cli/scripts/test.js diff --git a/README.md b/README.md index a1ec90c..5147fbf 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,17 @@ codra run --task summarize-context See [crates/codra-cli/README.md](crates/codra-cli/README.md). +## Installable CLI roadmap + +The Rust CLI will be installable globally via npm (not published yet): + +```bash +npm install -g @codra/cli # coming soon +codra run --task summarize-context --jsonl +``` + +The [`@codra/cli`](packages/codra-npm-cli/) package is a thin Node wrapper that spawns the native `codra` binary built from `codra-cli`. For local development, see [packages/codra-npm-cli/README.md](packages/codra-npm-cli/README.md). + ## Roadmap See [docs/ROADMAP.md](docs/ROADMAP.md) for the full phased roadmap. diff --git a/packages/codra-npm-cli/.gitignore b/packages/codra-npm-cli/.gitignore new file mode 100644 index 0000000..245139d --- /dev/null +++ b/packages/codra-npm-cli/.gitignore @@ -0,0 +1 @@ +bin/native/ \ No newline at end of file diff --git a/packages/codra-npm-cli/README.md b/packages/codra-npm-cli/README.md new file mode 100644 index 0000000..08c3cff --- /dev/null +++ b/packages/codra-npm-cli/README.md @@ -0,0 +1,60 @@ +# @codra/cli + +npm package wrapper for the [Codra](https://github.com/talocode/codra) Rust CLI (`codra-cli` crate). Installs a global `codra` command that forwards to the native binary for your platform. + +## Installation (coming soon) + +This package is **not published to npm yet**. When it is: + +```bash +npm install -g @codra/cli +codra --help +``` + +## Local development + +From the monorepo root (or this package directory): + +```bash +cd packages/codra-npm-cli +npm run build +node bin/codra.js --help +node bin/codra.js run --task summarize-context --jsonl +npm test +``` + +`npm run build` runs `cargo build -p codra-cli --release` and copies the release binary into `bin/native/-/`. + +## Supported commands + +Same as the Rust CLI: + +```bash +codra run --task review-pr --jsonl +codra run --task explain-issue --jsonl +codra run --task summarize-context --jsonl +``` + +Invalid tasks exit non-zero. With `--jsonl`, failures emit `codra.run.failed`. + +## GitHub context (optional) + +When running in GitHub Actions or with fixtures: + +| Variable | Purpose | +|----------|---------| +| `GITHUB_ACTIONS` | Detect Actions runtime | +| `GITHUB_REPOSITORY` | Repository slug | +| `GITHUB_EVENT_NAME` | Workflow event name | +| `GITHUB_EVENT_PATH` | Path to event JSON payload | +| `GITHUB_TOKEN` | Optional API enrichment (never printed) | + +## Security + +- No AI provider API calls in this CLI layer yet. +- Does not print `GITHUB_TOKEN` or other secrets in output. +- Local-first CLI foundation; wraps the existing Rust binary unchanged. + +## License + +MIT — see repository [LICENSE](https://github.com/talocode/codra/blob/main/LICENSE). \ No newline at end of file diff --git a/packages/codra-npm-cli/bin/codra.js b/packages/codra-npm-cli/bin/codra.js new file mode 100755 index 0000000..1185661 --- /dev/null +++ b/packages/codra-npm-cli/bin/codra.js @@ -0,0 +1,66 @@ +#!/usr/bin/env node +'use strict'; + +const { spawnSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const SUPPORTED_PLATFORMS = new Set([ + 'linux-arm64', + 'linux-x64', + 'darwin-arm64', + 'darwin-x64', + 'win32-x64', +]); + +function platformArchKey() { + const platform = process.platform; + const arch = process.arch; + return `${platform}-${arch}`; +} + +function binaryFileName() { + return process.platform === 'win32' ? 'codra.exe' : 'codra'; +} + +function resolveNativeBinary() { + const key = platformArchKey(); + const name = binaryFileName(); + const nativePath = path.join(__dirname, 'native', key, name); + + if (!fs.existsSync(nativePath)) { + const supported = [...SUPPORTED_PLATFORMS].sort().join(', '); + process.stderr.write( + `codra: native binary not found for ${key}.\n` + + `Expected: ${nativePath}\n` + + `Supported platform keys (when built): ${supported}\n` + + `Build the package for this machine: npm run build (from packages/codra-npm-cli)\n`, + ); + process.exit(1); + } + + return nativePath; +} + +function main() { + const binary = resolveNativeBinary(); + const args = process.argv.slice(2); + + const result = spawnSync(binary, args, { + stdio: 'inherit', + env: process.env, + }); + + if (result.error) { + process.stderr.write(`codra: failed to run native binary: ${result.error.message}\n`); + process.exit(1); + } + + if (result.signal) { + process.exit(1); + } + + process.exit(result.status === null ? 1 : result.status); +} + +main(); \ No newline at end of file diff --git a/packages/codra-npm-cli/package.json b/packages/codra-npm-cli/package.json new file mode 100644 index 0000000..36fa6bb --- /dev/null +++ b/packages/codra-npm-cli/package.json @@ -0,0 +1,31 @@ +{ + "name": "@codra/cli", + "version": "0.1.0", + "description": "Codra CLI for local-first AI coding agent workflows", + "license": "MIT", + "author": "Talocode", + "repository": { + "type": "git", + "url": "https://github.com/talocode/codra", + "directory": "packages/codra-npm-cli" + }, + "bin": { + "codra": "./bin/codra.js" + }, + "files": [ + "bin/", + "README.md", + "package.json" + ], + "scripts": { + "build": "node scripts/build.js", + "test": "node scripts/test.js", + "pack:dry": "npm pack --dry-run" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=18" + } +} \ No newline at end of file diff --git a/packages/codra-npm-cli/scripts/build.js b/packages/codra-npm-cli/scripts/build.js new file mode 100644 index 0000000..c7f8ae3 --- /dev/null +++ b/packages/codra-npm-cli/scripts/build.js @@ -0,0 +1,53 @@ +'use strict'; + +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +const packageRoot = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(packageRoot, '../..'); + +function platformArchKey() { + return `${process.platform}-${process.arch}`; +} + +function binaryFileName() { + return process.platform === 'win32' ? 'codra.exe' : 'codra'; +} + +function main() { + console.log(`[build] repo root: ${repoRoot}`); + console.log('[build] cargo build -p codra-cli --release'); + + try { + execSync('cargo build -p codra-cli --release', { + cwd: repoRoot, + stdio: 'inherit', + }); + } catch { + console.error('[build] cargo build failed'); + process.exit(1); + } + + const key = platformArchKey(); + const name = binaryFileName(); + const src = path.join(repoRoot, 'target', 'release', name); + const destDir = path.join(packageRoot, 'bin', 'native', key); + const dest = path.join(destDir, name); + + if (!fs.existsSync(src)) { + console.error(`[build] release binary missing: ${src}`); + process.exit(1); + } + + fs.mkdirSync(destDir, { recursive: true }); + fs.copyFileSync(src, dest); + + if (process.platform !== 'win32') { + fs.chmodSync(dest, 0o755); + } + + console.log(`[build] packaged ${key}: ${dest}`); +} + +main(); \ No newline at end of file diff --git a/packages/codra-npm-cli/scripts/test.js b/packages/codra-npm-cli/scripts/test.js new file mode 100644 index 0000000..42733ac --- /dev/null +++ b/packages/codra-npm-cli/scripts/test.js @@ -0,0 +1,71 @@ +'use strict'; + +const { spawnSync } = require('child_process'); +const path = require('path'); + +const packageRoot = path.resolve(__dirname, '..'); +const wrapper = path.join(packageRoot, 'bin', 'codra.js'); + +function run(args, { expectFail = false } = {}) { + const label = ['node', 'bin/codra.js', ...args].join(' '); + console.log(`[test] ${label}`); + + const result = spawnSync(process.execPath, [wrapper, ...args], { + cwd: packageRoot, + encoding: 'utf8', + env: process.env, + }); + + const stdout = result.stdout || ''; + const stderr = result.stderr || ''; + const exitCode = result.status ?? 1; + + if (result.error) { + console.error(`[test] spawn error: ${result.error.message}`); + process.exit(1); + } + + if (expectFail) { + if (exitCode === 0) { + console.error('[test] expected non-zero exit'); + process.exit(1); + } + } else if (exitCode !== 0) { + console.error(`[test] unexpected exit ${exitCode}`); + if (stderr) console.error(stderr); + process.exit(1); + } + + return { stdout, stderr, exitCode }; +} + +function assertIncludes(haystack, needle, label) { + if (!haystack.includes(needle)) { + console.error(`[test] missing ${label}: ${needle}`); + process.exit(1); + } +} + +function main() { + const help = run(['--help']); + assertIncludes(help.stdout + help.stderr, 'codra', 'help output'); + + const valid = run(['run', '--task', 'summarize-context', '--jsonl']); + assertIncludes(valid.stdout, 'codra.run.started', 'run started event'); + assertIncludes(valid.stdout, 'codra.run.completed', 'run completed event'); + + const invalid = run(['run', '--task', 'not-a-real-task', '--jsonl'], { + expectFail: true, + }); + assertIncludes(invalid.stdout, 'codra.run.failed', 'run failed event'); + + const combined = invalid.stdout + invalid.stderr; + if (/ghp_[A-Za-z0-9]+/i.test(combined) || /github_pat_/i.test(combined)) { + console.error('[test] output appears to contain a token pattern'); + process.exit(1); + } + + console.log('[test] all checks passed'); +} + +main(); \ No newline at end of file