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 CHANGED
@@ -29,6 +29,18 @@ riciplay chat # start an agent session
29
29
 
30
30
  ## The npm wrapper
31
31
 
32
- `bin/riciplay` prefers the pip-installed console script on PATH and falls back to invoking `riciplay_cli.main:app` through `python3`. `scripts/postinstall.js` pip-installs `riciplay-cli` if the module isn't importable — and degrades to a printed manual instruction when it can't (no network, `--ignore-scripts`, managed Python).
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). When the pip console script exists on PATH, exec it
9
- // directly (identical behavior to a native install); otherwise invoke
10
- // the app entry through python3 with argv[0] normalized so the CLI's
11
- // own help/version output stays correct.
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 { existsSync } = require('fs');
18
+ const fs = require('fs');
15
19
  const path = require('path');
16
20
  const { EOL } = require('os');
17
21
 
18
- const args = process.argv.slice(2);
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
- // Prefer an installed console script (same PATH resolution pip used).
34
- const isWin = process.platform === 'win32';
35
- const exe = isWin ? 'riciplay.exe' : 'riciplay';
36
- for (const dir of ['/usr/local/bin', '/usr/bin', path.join(process.env.HOME || '', '.local', 'bin')]) {
37
- const candidate = path.join(dir, exe);
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
- // Module fallback — app() reads sys.argv; normalize argv[0] so usage
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.8.95",
4
- "description": "AI-powered bug bounty reconnaissance and penetration testing agent — browser, proxy interception, recon, and exploit workflows",
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": "echo \"Error: no test specified\" && exit 1",
11
- "prepublishOnly": "echo \"Ensure riciplay-cli is published to PyPI first\"",
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": {
@@ -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
- // package `riciplay-cli` (PyPI). If it is not importable, pip install
6
- // it. Failures are warnings, not errors: the user may be installing
7
- // with --ignore-scripts, without network, or managing Python manually —
8
- // print the manual command instead of failing the install.
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
- console.log('\n🕷️ Installing Riciplay (bug bounty AI agent)...\n');
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
- function probe(module) {
20
- const r = spawnSync(python, ['-c', `import ${module}`], { stdio: 'ignore' });
21
- return r.status === 0;
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
- try {
25
- if (probe('riciplay_cli')) {
26
- console.log('✓ Python package `riciplay_cli` already importable.');
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
- console.log('Installing riciplay-cli from PyPI (this also pulls Chrome/mitmproxy deps)...');
29
- execSync(`${pip} install --upgrade riciplay-cli`, { stdio: 'inherit' });
30
- console.log('✓ Installed riciplay-cli.');
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
- } catch (err) {
33
- console.warn(
34
- `\n⚠ Could not install the Python package automatically (${err.message}).\n` +
35
- ' Riciplay needs Python 3.10+. Install manually with:\n' +
36
- ' pip install riciplay-cli\n'
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
- console.log('\nUsage: riciplay chat # start an agent session\n');
101
+ module.exports = { npmVersion, installedVersion, needsInstall, pipSpec, main };