Skip to content

Commit a386052

Browse files
committed
"feat: add @marionette-app/cli npm package"
1 parent f4733a7 commit a386052

5 files changed

Lines changed: 184 additions & 4 deletions

File tree

.github/workflows/release.yml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,31 @@ jobs:
7171
- name: Upload release artifact
7272
run: gh release upload "${{ github.ref_name }}" "${{ matrix.upload_file }}" --clobber
7373
env:
74-
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
75+
76+
publish-npm:
77+
name: Publish @marionette/cli to npm
78+
needs: create-release
79+
runs-on: ubuntu-latest
80+
steps:
81+
- name: Checkout
82+
uses: actions/checkout@v4
83+
84+
- name: Setup Node.js
85+
uses: actions/setup-node@v4
86+
with:
87+
node-version: 20
88+
registry-url: https://registry.npmjs.org
89+
90+
- name: Set version from tag
91+
run: |
92+
VERSION="${{ github.ref_name }}"
93+
VERSION="${VERSION#v}"
94+
npm version "$VERSION" --no-git-tag-version
95+
working-directory: packages/cli
96+
97+
- name: Publish to npm
98+
run: npm publish --access public
99+
working-directory: packages/cli
100+
env:
101+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,22 @@ A dashboard that captures every conversation, token count, and tool call across
1313

1414
## Quick Start (for users)
1515

16-
**macOS / Linux**
16+
**npm (any platform, Node.js 18+ required)**
17+
```bash
18+
npm install -g @marionette-app/cli && marionette setup
19+
```
20+
21+
**macOS / Linux** (alternative — no Node.js required)
1722
```bash
1823
curl -fsSL https://raw.githubusercontent.com/yarin-mag/Marionette/master/scripts/install.sh | bash
1924
```
2025

21-
**Windows** (PowerShell)
26+
**Windows** (alternative — PowerShell)
2227
```powershell
2328
irm https://raw.githubusercontent.com/yarin-mag/Marionette/master/scripts/install.ps1 | iex
2429
```
2530

26-
This downloads the latest release, installs it to `/usr/local/lib/marionette`, registers the MCP server with Claude Code, and configures auto-start.
31+
This downloads the latest release, installs it to `~/.marionette/app` (npm) or `/usr/local/lib/marionette` (curl/PowerShell), registers the MCP server with Claude Code, and configures auto-start.
2732

2833
After installing, open **http://localhost:8787**.
2934

packages/cli/bin/marionette.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
const path = require('path');
5+
const os = require('os');
6+
const fs = require('fs');
7+
const { spawn } = require('child_process');
8+
9+
const installDir = path.join(os.homedir(), '.marionette', 'app');
10+
const isWindows = process.platform === 'win32';
11+
const realBin = path.join(installDir, 'bin', isWindows ? 'marionette.cmd' : 'marionette');
12+
13+
const args = process.argv.slice(2);
14+
const command = args[0];
15+
16+
if (fs.existsSync(realBin)) {
17+
const child = spawn(realBin, args, { stdio: 'inherit', shell: isWindows });
18+
child.on('exit', (code) => process.exit(code ?? 0));
19+
} else if (!command || command === 'setup') {
20+
require('../scripts/bootstrap.js')();
21+
} else {
22+
console.error('Marionette is not installed yet. Run: marionette setup');
23+
process.exit(1);
24+
}

packages/cli/package.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"name": "@marionette-app/cli",
3+
"version": "1.3.0",
4+
"description": "Real-time monitoring for Claude Code sessions",
5+
"bin": {
6+
"marionette": "./bin/marionette.js"
7+
},
8+
"engines": {
9+
"node": ">=18"
10+
},
11+
"license": "MIT",
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com/yarin-mag/Marionette"
15+
},
16+
"keywords": [
17+
"claude",
18+
"claude-code",
19+
"ai",
20+
"monitoring",
21+
"mcp",
22+
"dashboard"
23+
],
24+
"files": [
25+
"bin/",
26+
"scripts/"
27+
]
28+
}

packages/cli/scripts/bootstrap.js

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
'use strict';
2+
3+
const https = require('https');
4+
const fs = require('fs');
5+
const path = require('path');
6+
const os = require('os');
7+
const { execSync, execFileSync } = require('child_process');
8+
9+
module.exports = function bootstrap() {
10+
const version = require('../package.json').version;
11+
const platform = process.platform;
12+
const arch = process.arch;
13+
const isWindows = platform === 'win32';
14+
15+
let artifact;
16+
if (platform === 'linux') {
17+
artifact = 'marionette-linux-x64.tar.gz';
18+
} else if (platform === 'darwin') {
19+
artifact = arch === 'arm64'
20+
? 'marionette-macos-arm64.tar.gz'
21+
: 'marionette-macos-x64.tar.gz';
22+
} else if (isWindows) {
23+
artifact = 'marionette-windows-x64.zip';
24+
} else {
25+
console.error(`Unsupported platform: ${platform}`);
26+
process.exit(1);
27+
}
28+
29+
const url = `https://github.com/yarin-mag/Marionette/releases/download/v${version}/${artifact}`;
30+
const installDir = path.join(os.homedir(), '.marionette', 'app');
31+
const tmpFile = path.join(os.tmpdir(), artifact);
32+
const realBin = path.join(installDir, 'bin', isWindows ? 'marionette.cmd' : 'marionette');
33+
34+
console.log(`Installing Marionette v${version}...`);
35+
console.log(`Downloading ${artifact}...`);
36+
37+
download(url, tmpFile, () => {
38+
console.log('Extracting...');
39+
fs.mkdirSync(installDir, { recursive: true });
40+
41+
if (isWindows) {
42+
execSync(`tar -xf "${tmpFile}" -C "${installDir}" --strip-components=1`, { stdio: 'inherit' });
43+
} else {
44+
execSync(`tar -xzf "${tmpFile}" -C "${installDir}" --strip-components=1`, { stdio: 'inherit' });
45+
fs.chmodSync(realBin, 0o755);
46+
}
47+
48+
try { fs.unlinkSync(tmpFile); } catch (_) {}
49+
50+
console.log('Running setup...');
51+
execFileSync(realBin, ['setup'], { stdio: 'inherit', shell: isWindows });
52+
});
53+
};
54+
55+
function download(url, dest, callback) {
56+
const file = fs.createWriteStream(dest);
57+
58+
function get(urlStr) {
59+
https.get(urlStr, (res) => {
60+
if (res.statusCode === 301 || res.statusCode === 302) {
61+
file.destroy();
62+
get(res.headers.location);
63+
return;
64+
}
65+
if (res.statusCode !== 200) {
66+
console.error(`Download failed: HTTP ${res.statusCode}\nURL: ${urlStr}`);
67+
process.exit(1);
68+
}
69+
70+
const total = parseInt(res.headers['content-length'] || '0', 10);
71+
let received = 0;
72+
73+
res.on('data', (chunk) => {
74+
received += chunk.length;
75+
if (total) {
76+
const pct = Math.round((received / total) * 100);
77+
process.stdout.write(`\r ${pct}%`);
78+
}
79+
});
80+
81+
res.pipe(file);
82+
file.on('finish', () => {
83+
file.close(() => {
84+
if (total) process.stdout.write('\n');
85+
callback();
86+
});
87+
});
88+
}).on('error', (err) => {
89+
fs.unlink(dest, () => {});
90+
console.error(`Download error: ${err.message}`);
91+
process.exit(1);
92+
});
93+
}
94+
95+
get(url);
96+
}

0 commit comments

Comments
 (0)