Skip to content

Commit c9217d3

Browse files
committed
cli install
1 parent 0ffc283 commit c9217d3

File tree

8 files changed

+208
-6
lines changed

8 files changed

+208
-6
lines changed

jacs/src/bin/cli.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,12 +139,26 @@ fn wrap_quickstart_error_with_password_help(
139139
)))
140140
}
141141

142-
fn install_jacs_mcp_via_cargo(version: Option<&str>, force: bool) -> Result<(), Box<dyn Error>> {
142+
fn install_jacs_mcp_via_cargo(
143+
version: Option<&str>,
144+
force: bool,
145+
dry_run: bool,
146+
) -> Result<(), Box<dyn Error>> {
143147
let resolved_version = version
144148
.map(str::trim)
145149
.filter(|v| !v.is_empty())
146150
.unwrap_or(env!("CARGO_PKG_VERSION"));
147151

152+
if dry_run {
153+
let force_suffix = if force { " --force" } else { "" };
154+
println!("Dry run: MCP cargo install plan");
155+
println!(
156+
" Command: cargo install jacs-mcp --locked --version {}{}",
157+
resolved_version, force_suffix
158+
);
159+
return Ok(());
160+
}
161+
148162
let mut command = std::process::Command::new("cargo");
149163
command
150164
.arg("install")
@@ -1031,7 +1045,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
10311045
.arg(
10321046
Arg::new("dry-run")
10331047
.long("dry-run")
1034-
.help("Print prebuilt install plan without downloading")
1048+
.help("Print install plan without downloading/installing")
10351049
.action(ArgAction::SetTrue),
10361050
),
10371051
)
@@ -1709,7 +1723,7 @@ pub fn main() -> Result<(), Box<dyn Error>> {
17091723
let dry_run = *install_matches.get_one::<bool>("dry-run").unwrap_or(&false);
17101724

17111725
if from_cargo {
1712-
install_jacs_mcp_via_cargo(version, force)?;
1726+
install_jacs_mcp_via_cargo(version, force, dry_run)?;
17131727
} else {
17141728
install_jacs_mcp_prebuilt(version, force, bin_dir, url_override, dry_run)?;
17151729
}

jacs/tests/cli_tests.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1130,6 +1130,22 @@ fn test_mcp_install_dry_run_custom_url() -> Result<(), Box<dyn Error>> {
11301130
Ok(())
11311131
}
11321132

1133+
#[test]
1134+
fn test_mcp_install_from_cargo_dry_run_shows_cargo_plan() -> Result<(), Box<dyn Error>> {
1135+
let mut cmd = Command::cargo_bin("jacs")?;
1136+
cmd.arg("mcp")
1137+
.arg("install")
1138+
.arg("--from-cargo")
1139+
.arg("--dry-run")
1140+
.arg("--version")
1141+
.arg("0.8.0");
1142+
cmd.assert()
1143+
.success()
1144+
.stdout(predicate::str::contains("Dry run: MCP cargo install plan"))
1145+
.stdout(predicate::str::contains("cargo install jacs-mcp --locked --version 0.8.0"));
1146+
Ok(())
1147+
}
1148+
11331149
#[test]
11341150
fn test_mcp_run_missing_binary_shows_install_hint() -> Result<(), Box<dyn Error>> {
11351151
let mut cmd = Command::cargo_bin("jacs")?;

jacsnpm/bin/jacs-cli.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
const fs = require('fs');
5+
const os = require('os');
6+
const path = require('path');
7+
const { spawnSync } = require('child_process');
8+
9+
function binaryPath() {
10+
const binaryName = os.platform() === 'win32' ? 'jacs-cli.exe' : 'jacs-cli';
11+
return path.join(__dirname, binaryName);
12+
}
13+
14+
function runBinary(target, forwardedArgs) {
15+
if (!fs.existsSync(target)) {
16+
return false;
17+
}
18+
19+
const result = spawnSync(target, forwardedArgs, { stdio: 'inherit' });
20+
if (result.error) {
21+
if (result.error.code === 'ENOENT') {
22+
return false;
23+
}
24+
console.error(`[jacs] Failed to launch CLI binary: ${result.error.message}`);
25+
process.exit(1);
26+
}
27+
28+
if (typeof result.status === 'number') {
29+
process.exit(result.status);
30+
}
31+
32+
process.exit(1);
33+
}
34+
35+
function runInstaller() {
36+
const installer = path.join(__dirname, '..', 'scripts', 'install-cli.js');
37+
spawnSync(process.execPath, [installer], { stdio: 'inherit' });
38+
}
39+
40+
function main() {
41+
const args = process.argv.slice(2);
42+
const target = binaryPath();
43+
44+
if (runBinary(target, args)) {
45+
return;
46+
}
47+
48+
runInstaller();
49+
50+
if (runBinary(target, args)) {
51+
return;
52+
}
53+
54+
console.error('[jacs] CLI binary is not available for this platform/environment.');
55+
console.error('[jacs] The @hai.ai/jacs library APIs are still available.');
56+
process.exit(1);
57+
}
58+
59+
main();

jacsnpm/package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

jacsnpm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
}
7777
},
7878
"bin": {
79-
"jacs-cli": "./bin/jacs-cli"
79+
"jacs-cli": "./bin/jacs-cli.js"
8080
},
8181
"scripts": {
8282
"postinstall": "node scripts/install-cli.js || true",

jacsnpm/scripts/install-cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ async function main() {
116116
console.log(`[jacs] cargo install jacs --features cli`);
117117
console.log(`[jacs] OR download from https://github.com/${REPO}/releases`);
118118
// Clean up partial install
119-
try { fs.rmSync(binDir, { recursive: true, force: true }); } catch (_) {}
119+
try { fs.rmSync(binPath, { force: true }); } catch (_) {}
120120
} finally {
121121
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (_) {}
122122
}

jacsnpm/test/install-cli.test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Tests for npm CLI install helpers.
3+
*/
4+
5+
const { expect } = require('chai');
6+
const fs = require('fs');
7+
const os = require('os');
8+
const path = require('path');
9+
const { spawnSync } = require('child_process');
10+
11+
const ROOT = path.join(__dirname, '..');
12+
13+
function runNodeInline(jsCode) {
14+
return spawnSync(process.execPath, ['-e', jsCode], {
15+
cwd: ROOT,
16+
encoding: 'utf8',
17+
});
18+
}
19+
20+
describe('CLI installer scripts', function () {
21+
this.timeout(15000);
22+
23+
it('install-cli exits successfully on unsupported platforms', () => {
24+
const result = runNodeInline(
25+
"const os=require('os'); os.platform=()=> 'freebsd'; os.arch=()=> 'x64'; require('./scripts/install-cli.js');"
26+
);
27+
28+
expect(result.status).to.equal(0);
29+
expect(result.stdout).to.include('No prebuilt CLI binary for freebsd-x64');
30+
});
31+
32+
it('install-cli exits successfully when download fails', () => {
33+
const result = runNodeInline(
34+
"const os=require('os'); os.platform=()=> 'darwin'; os.arch=()=> 'arm64'; const https=require('https'); const {EventEmitter}=require('events'); https.get=()=>{const req=new EventEmitter(); process.nextTick(()=>req.emit('error', new Error('simulated-download-failure'))); return req;}; require('./scripts/install-cli.js');"
35+
);
36+
37+
expect(result.status).to.equal(0);
38+
expect(result.stdout).to.include('Could not install CLI binary: simulated-download-failure');
39+
});
40+
41+
it('bin shim forwards arguments to a local binary when present', () => {
42+
if (process.platform === 'win32') {
43+
this.skip();
44+
}
45+
46+
const binName = process.platform === 'win32' ? 'jacs-cli.exe' : 'jacs-cli';
47+
const binPath = path.join(ROOT, 'bin', binName);
48+
49+
fs.mkdirSync(path.dirname(binPath), { recursive: true });
50+
fs.writeFileSync(binPath, '#!/usr/bin/env bash\necho shim-ok \"$@\"\n', { mode: 0o755 });
51+
52+
try {
53+
const result = spawnSync(process.execPath, ['bin/jacs-cli.js', 'hello', 'world'], {
54+
cwd: ROOT,
55+
encoding: 'utf8',
56+
});
57+
58+
expect(result.status).to.equal(0);
59+
expect(result.stdout).to.include('shim-ok hello world');
60+
} finally {
61+
try {
62+
fs.unlinkSync(binPath);
63+
} catch (_) {
64+
// no-op cleanup
65+
}
66+
}
67+
});
68+
});

jacspy/tests/test_cli_runner.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Tests for the Python CLI launcher wrapper."""
2+
3+
import types
4+
5+
import pytest
6+
7+
import jacs.cli_runner as cli_runner
8+
9+
10+
def test_platform_key_linux_x64(monkeypatch):
11+
monkeypatch.setattr(cli_runner.platform, "system", lambda: "Linux")
12+
monkeypatch.setattr(cli_runner.platform, "machine", lambda: "x86_64")
13+
assert cli_runner._platform_key() == "linux-x64"
14+
15+
16+
def test_ensure_cli_returns_none_for_unsupported_platform(monkeypatch):
17+
monkeypatch.setattr(cli_runner, "_platform_key", lambda: None)
18+
assert cli_runner.ensure_cli() is None
19+
20+
21+
def test_main_exits_one_when_cli_unavailable(monkeypatch):
22+
monkeypatch.setattr(cli_runner, "ensure_cli", lambda: None)
23+
24+
with pytest.raises(SystemExit) as exc:
25+
cli_runner.main()
26+
27+
assert exc.value.code == 1
28+
29+
30+
def test_main_forwards_args_to_downloaded_cli(monkeypatch):
31+
captured = {}
32+
monkeypatch.setattr(cli_runner, "ensure_cli", lambda: "/tmp/fake-jacs-cli")
33+
34+
def fake_run(args):
35+
captured["args"] = args
36+
return types.SimpleNamespace(returncode=0)
37+
38+
monkeypatch.setattr(cli_runner.subprocess, "run", fake_run)
39+
monkeypatch.setattr(cli_runner.sys, "argv", ["jacs", "mcp", "install", "--dry-run"])
40+
41+
with pytest.raises(SystemExit) as exc:
42+
cli_runner.main()
43+
44+
assert exc.value.code == 0
45+
assert captured["args"] == ["/tmp/fake-jacs-cli", "mcp", "install", "--dry-run"]

0 commit comments

Comments
 (0)