Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/codra-npm-cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/native/
60 changes: 60 additions & 0 deletions packages/codra-npm-cli/README.md
Original file line number Diff line number Diff line change
@@ -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/<platform>-<arch>/`.

## 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).
66 changes: 66 additions & 0 deletions packages/codra-npm-cli/bin/codra.js
Original file line number Diff line number Diff line change
@@ -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();
31 changes: 31 additions & 0 deletions packages/codra-npm-cli/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
53 changes: 53 additions & 0 deletions packages/codra-npm-cli/scripts/build.js
Original file line number Diff line number Diff line change
@@ -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);
Comment on lines +32 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Package binaries for every install target

When this single @codra/cli package is packed after npm run build on one machine, only that machine's bin/native/<platform>-<arch>/codra is copied into the tarball; I checked npm pack --dry-run and it only includes files present under bin/. On any other supported platform, the wrapper computes a different key and exits with “native binary not found”, so a package published from a Linux CI runner would not work for macOS or Windows global installs unless the release process adds all platform binaries or uses per-platform optional packages/os/cpu constraints.

Useful? React with 👍 / 👎.

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();
71 changes: 71 additions & 0 deletions packages/codra-npm-cli/scripts/test.js
Original file line number Diff line number Diff line change
@@ -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();
Loading