riciplay 1.8.95 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/bin/riciplay +92 -20
- package/package.json +4 -4
- package/scripts/postinstall.js +84 -23
package/README.md
CHANGED
|
@@ -29,6 +29,18 @@ riciplay chat # start an agent session
|
|
|
29
29
|
|
|
30
30
|
## The npm wrapper
|
|
31
31
|
|
|
32
|
-
`bin/riciplay`
|
|
32
|
+
`bin/riciplay` resolves the pip-installed console script via `PATH` and falls back to invoking `riciplay_cli.main:app` through `python3`. It explicitly excludes its own path as a candidate, so an npm global install (which symlinks this wrapper onto `PATH`) can never re-exec itself.
|
|
33
|
+
|
|
34
|
+
`scripts/postinstall.js` pip-installs `riciplay-cli` **pinned to this launcher's exact version** (`riciplay-cli==<package.json version>`), and compares the *installed* version rather than merely checking that the module imports — so an out-of-date install is upgraded instead of being silently kept. It degrades to a printed manual command when it can't (no network, `--ignore-scripts`, managed Python).
|
|
35
|
+
|
|
36
|
+
### The two versions are locked together
|
|
37
|
+
|
|
38
|
+
The launcher and the Python package must be released at the same version, because postinstall pins the pair. That used to be an unenforced convention and it drifted (npm `1.8.95` against PyPI `1.8.96`). Three things now enforce it:
|
|
39
|
+
|
|
40
|
+
- `npm test` / `npm publish` run `scripts/check-version-sync.js` — `prepublishOnly` **refuses to publish** a mismatched pair
|
|
41
|
+
- `pytest tests/test_version_sync.py` fails the suite on drift, catching it at PR time
|
|
42
|
+
- `python scripts/bump_version.py <version>` moves both files together, so the correct action is also the easy one (`--check` to verify, `--show` to inspect)
|
|
43
|
+
|
|
44
|
+
Release order: **PyPI first, then npm** — the pinned postinstall needs the matching version to already exist on PyPI.
|
|
33
45
|
|
|
34
46
|
Repository: [Zaidux/Riciplay](https://github.com/Zaidux/Riciplay)
|
package/bin/riciplay
CHANGED
|
@@ -5,17 +5,85 @@
|
|
|
5
5
|
// python3 -c "from riciplay_cli.main import app; app()"
|
|
6
6
|
//
|
|
7
7
|
// The Python package is installed by scripts/postinstall.js (pip install
|
|
8
|
-
// riciplay-cli).
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// own
|
|
8
|
+
// riciplay-cli). The real console script is resolved through PATH — the same
|
|
9
|
+
// resolution the OS/shell performs — so a venv/pipx install
|
|
10
|
+
// (<venv>/bin/riciplay) is found instead of silently falling back to the
|
|
11
|
+
// system python3. Crucially, this wrapper's own file is explicitly excluded
|
|
12
|
+
// as a candidate: an npm global install puts a symlink to this very script on
|
|
13
|
+
// PATH (/usr/local/bin/riciplay), and matching it re-executed the wrapper
|
|
14
|
+
// forever. Only when no other candidate exists do we fall back to
|
|
15
|
+
// `python3 -m`, which is exactly the case the wrapper must never feed itself.
|
|
12
16
|
|
|
13
17
|
const { spawn } = require('child_process');
|
|
14
|
-
const
|
|
18
|
+
const fs = require('fs');
|
|
15
19
|
const path = require('path');
|
|
16
20
|
const { EOL } = require('os');
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
function realpathSafe(p) {
|
|
23
|
+
try {
|
|
24
|
+
return fs.realpathSync(p);
|
|
25
|
+
} catch {
|
|
26
|
+
return path.resolve(p);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Filesystem identities of this wrapper, so it can never be selected as its
|
|
32
|
+
* own target. Both the symlinked invoke path (process.argv[1]) and the real
|
|
33
|
+
* module path (__filename) are registered: npm's global bin is a symlink to
|
|
34
|
+
* the latter, while a plain copy would match the former.
|
|
35
|
+
*/
|
|
36
|
+
function selfIdentities({ filename, argv1 }) {
|
|
37
|
+
const ids = new Set();
|
|
38
|
+
if (filename) {
|
|
39
|
+
ids.add(realpathSafe(filename));
|
|
40
|
+
ids.add(path.resolve(filename));
|
|
41
|
+
}
|
|
42
|
+
if (argv1) {
|
|
43
|
+
ids.add(realpathSafe(argv1));
|
|
44
|
+
ids.add(path.resolve(argv1));
|
|
45
|
+
}
|
|
46
|
+
return ids;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the installed `riciplay` console script via PATH.
|
|
51
|
+
*
|
|
52
|
+
* Returns the candidate path, or null when the only match on PATH is this
|
|
53
|
+
* wrapper itself (in which case the caller falls back to `python3 -m`).
|
|
54
|
+
*/
|
|
55
|
+
function resolveConsoleScript(options = {}) {
|
|
56
|
+
const platform = options.platform || process.platform;
|
|
57
|
+
const env = options.env || process.env;
|
|
58
|
+
const filename = options.filename || __filename;
|
|
59
|
+
const argv1 = 'argv1' in options ? options.argv1 : process.argv[1];
|
|
60
|
+
const isWin = platform === 'win32';
|
|
61
|
+
|
|
62
|
+
// Windows: pip installs `riciplay.exe`. npm's own shim is a .cmd/.ps1, so
|
|
63
|
+
// searching for the .exe cannot accidentally match the npm shim.
|
|
64
|
+
const name = isWin ? 'riciplay.exe' : 'riciplay';
|
|
65
|
+
const pathEnv = env.PATH || env.Path || '';
|
|
66
|
+
const sep = isWin ? ';' : ':';
|
|
67
|
+
const own = selfIdentities({ filename, argv1 });
|
|
68
|
+
|
|
69
|
+
for (const dir of pathEnv.split(sep)) {
|
|
70
|
+
if (!dir) continue;
|
|
71
|
+
const candidate = path.join(dir, name);
|
|
72
|
+
let st;
|
|
73
|
+
try {
|
|
74
|
+
st = fs.statSync(candidate); // follows symlinks
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!st.isFile()) continue;
|
|
79
|
+
// Never re-exec this wrapper — by realpath (npm symlink) or by literal
|
|
80
|
+
// path (copied install).
|
|
81
|
+
if (own.has(realpathSafe(candidate)) || own.has(path.resolve(candidate))) continue;
|
|
82
|
+
if (!isWin && (st.mode & 0o111) === 0) continue; // not executable
|
|
83
|
+
return candidate;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
19
87
|
|
|
20
88
|
function fail(msg) {
|
|
21
89
|
process.stderr.write(`Error: ${msg}${EOL}`);
|
|
@@ -30,21 +98,25 @@ function run(cmd, argv) {
|
|
|
30
98
|
proc.on('close', (code) => process.exit(code ?? 1));
|
|
31
99
|
}
|
|
32
100
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (existsSync(candidate)) {
|
|
39
|
-
run(candidate, args);
|
|
101
|
+
function main(argv = process.argv.slice(2)) {
|
|
102
|
+
// Prefer an installed console script (same PATH resolution the OS used).
|
|
103
|
+
const candidate = resolveConsoleScript();
|
|
104
|
+
if (candidate) {
|
|
105
|
+
run(candidate, argv);
|
|
40
106
|
return;
|
|
41
107
|
}
|
|
108
|
+
|
|
109
|
+
// Module fallback — app() reads sys.argv; normalize argv[0] so usage
|
|
110
|
+
// strings and version output match the real console script.
|
|
111
|
+
run('python3', [
|
|
112
|
+
'-c',
|
|
113
|
+
"import sys; sys.argv[0] = 'riciplay'; from riciplay_cli.main import app; app()",
|
|
114
|
+
...argv,
|
|
115
|
+
]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (require.main === module) {
|
|
119
|
+
main();
|
|
42
120
|
}
|
|
43
121
|
|
|
44
|
-
|
|
45
|
-
// strings and version output match the real console script.
|
|
46
|
-
run('python3', [
|
|
47
|
-
'-c',
|
|
48
|
-
"import sys; sys.argv[0] = 'riciplay'; from riciplay_cli.main import app; app()",
|
|
49
|
-
...args,
|
|
50
|
-
]);
|
|
122
|
+
module.exports = { resolveConsoleScript, selfIdentities, main };
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riciplay",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI-powered bug bounty reconnaissance and penetration testing agent
|
|
3
|
+
"version": "1.9.1",
|
|
4
|
+
"description": "AI-powered bug bounty reconnaissance and penetration testing agent \u2014 browser, proxy interception, recon, and exploit workflows",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"riciplay": "./bin/riciplay"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
11
|
-
"prepublishOnly": "
|
|
10
|
+
"test": "node scripts/check-version-sync.js",
|
|
11
|
+
"prepublishOnly": "node scripts/check-version-sync.js || (echo 'Refusing to publish: npm and PyPI versions disagree.' && exit 1)",
|
|
12
12
|
"postinstall": "node scripts/postinstall.js"
|
|
13
13
|
},
|
|
14
14
|
"repository": {
|
package/scripts/postinstall.js
CHANGED
|
@@ -1,40 +1,101 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Riciplay npm postinstall — installs the Python package from PyPI.
|
|
2
|
+
// Riciplay npm postinstall — installs the matching Python package from PyPI.
|
|
3
3
|
//
|
|
4
|
-
// This npm package is a launcher; the implementation is the Python
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// This npm package is a launcher; the implementation is the Python package
|
|
5
|
+
// `riciplay-cli` (PyPI). The two versions are meant to be identical, and this
|
|
6
|
+
// script enforces that:
|
|
7
|
+
//
|
|
8
|
+
// * it installs the EXACT version matching this npm release
|
|
9
|
+
// (`riciplay-cli==<package.json version>`), never just "latest", so a
|
|
10
|
+
// launcher can never end up driving a different implementation than the
|
|
11
|
+
// one it was released with;
|
|
12
|
+
// * it compares the INSTALLED version, not merely whether the module
|
|
13
|
+
// imports. The previous check was `python -c "import riciplay_cli"`, which
|
|
14
|
+
// passed for any old version already present — so a fresh npm install
|
|
15
|
+
// silently kept a stale CLI and reported "already importable".
|
|
16
|
+
//
|
|
17
|
+
// Failures are warnings, not errors: the user may be installing with
|
|
18
|
+
// --ignore-scripts, without network, or managing Python manually — print the
|
|
19
|
+
// manual command instead of failing the install.
|
|
9
20
|
|
|
10
21
|
const { execSync, spawnSync } = require('child_process');
|
|
11
22
|
const os = require('os');
|
|
23
|
+
const path = require('path');
|
|
12
24
|
|
|
13
|
-
|
|
25
|
+
const npmVersion = require(path.join(__dirname, '..', 'package.json')).version;
|
|
14
26
|
|
|
15
27
|
const isWin = os.platform() === 'win32';
|
|
16
28
|
const python = isWin ? 'python' : 'python3';
|
|
17
29
|
const pip = isWin ? 'pip' : 'pip3';
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
31
|
+
/**
|
|
32
|
+
* The version of `riciplay_cli` importable right now, or null if it is not
|
|
33
|
+
* installed / not importable.
|
|
34
|
+
*
|
|
35
|
+
* Read via `riciplay_cli.__version__`, which falls back to parsing
|
|
36
|
+
* pyproject.toml when dist metadata is unavailable, so this works for both a
|
|
37
|
+
* pip install and a source checkout.
|
|
38
|
+
*/
|
|
39
|
+
function installedVersion(py = python, run = spawnSync) {
|
|
40
|
+
const r = run(py, ['-c', 'import riciplay_cli; print(riciplay_cli.__version__)'], {
|
|
41
|
+
encoding: 'utf8',
|
|
42
|
+
});
|
|
43
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
44
|
+
const v = String(r.stdout).trim();
|
|
45
|
+
return v && v !== '0.0.0' ? v : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when the installed CLI is not the version this launcher ships with. */
|
|
49
|
+
function needsInstall(installed, target = npmVersion) {
|
|
50
|
+
return installed !== target;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The exact pip requirement pinning the Python package to this npm release. */
|
|
54
|
+
function pipSpec(target = npmVersion) {
|
|
55
|
+
return `riciplay-cli==${target}`;
|
|
22
56
|
}
|
|
23
57
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
58
|
+
function main() {
|
|
59
|
+
console.log('\n🕷️ Installing Riciplay (bug bounty AI agent)...\n');
|
|
60
|
+
|
|
61
|
+
let current = null;
|
|
62
|
+
try {
|
|
63
|
+
current = installedVersion();
|
|
64
|
+
} catch {
|
|
65
|
+
current = null; // treated as "not installed" below
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!needsInstall(current)) {
|
|
69
|
+
console.log(`✓ riciplay-cli ${npmVersion} already installed.`);
|
|
27
70
|
} else {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
71
|
+
if (current) {
|
|
72
|
+
console.log(
|
|
73
|
+
`Updating riciplay-cli ${current} → ${npmVersion} ` +
|
|
74
|
+
'(the launcher and the Python package are version-locked)...'
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
console.log(
|
|
78
|
+
`Installing riciplay-cli ${npmVersion} from PyPI ` +
|
|
79
|
+
'(this also pulls Chrome/mitmproxy deps)...'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
execSync(`${pip} install --upgrade ${pipSpec()}`, { stdio: 'inherit' });
|
|
84
|
+
console.log(`✓ Installed riciplay-cli ${npmVersion}.`);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
console.warn(
|
|
87
|
+
`\n⚠ Could not install the Python package automatically (${err.message}).\n` +
|
|
88
|
+
' Riciplay needs Python 3.10+. Install the matching version manually with:\n' +
|
|
89
|
+
` pip install ${pipSpec()}\n`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
31
92
|
}
|
|
32
|
-
|
|
33
|
-
console.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
);
|
|
93
|
+
|
|
94
|
+
console.log('\nUsage: riciplay chat # start an agent session\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (require.main === module) {
|
|
98
|
+
main();
|
|
38
99
|
}
|
|
39
100
|
|
|
40
|
-
|
|
101
|
+
module.exports = { npmVersion, installedVersion, needsInstall, pipSpec, main };
|