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
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

<h3 align="center">The most agent-ergonomic browser automation</h3>

> [!IMPORTANT]
> **This package has moved to [`opera-devtools-mcp`](https://github.com/operasoftware/opera-devtools-mcp).**
> `opera-browser-cli` is no longer developed here; the CLI now ships inside `opera-devtools-mcp`,
> still as the `opera-browser-cli` command.
>
> ```sh
> npm install -g opera-browser-cli # or: npm update -g opera-browser-cli
> ```
>
> That installs the **compatibility launcher** (`0.1.55`): the same package name and the same
> command, delegating to the new implementation inside `opera-devtools-mcp`. No uninstall, no
> `--force`, no changes to your scripts, and your `~/.opera-browser-cli/config` is reused.
> Add the MCP server as its own global install if you want it too:
> `npm install -g opera-devtools-mcp`.
>
> When you want to drop the launcher and keep a single global package:
>
> ```sh
> npm install -g opera-devtools-mcp@0.9.0 opera-browser-cli@0.2.0
> npm rm -g opera-browser-cli # optional: drops the inert tombstone
> ```
>
> Existing `0.1.x` installs keep working; they are simply no longer maintained.
> Run `opera-browser-cli doctor` for the same instructions on the installed CLI.

`opera-browser-cli` is a fork of [chrome-devtools-axi](https://github.com/kunchenguid/chrome-devtools-axi).
It wraps [opera-devtools-mcp](https://github.com/operasoftware/opera-devtools-mcp) with an [AXI](https://axi.md)-compliant CLI.

Expand All @@ -12,7 +37,7 @@ It wraps [opera-devtools-mcp](https://github.com/operasoftware/opera-devtools-mc
## Quick Start

```sh
npm install -g opera-browser-cli
npm install -g opera-browser-cli # installs the launcher that delivers the new implementation
opera-browser-cli open https://example.com
```

Expand Down Expand Up @@ -69,6 +94,10 @@ Prerequisites: **Node.js >= 20**, **Opera** browser ([Opera Neon](https://www.op
npm install -g opera-browser-cli
```

Development has moved to `opera-devtools-mcp`, which ships this CLI as the
`opera-browser-cli` command; this package name installs the compatibility
launcher that delivers it. See the notice at the top of this file.

No setup step is required. The first command you run detects your Opera
installation, writes `~/.opera-browser-cli/config`, and continues:

Expand Down
37 changes: 37 additions & 0 deletions launcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# opera-browser-cli (compatibility launcher)

`opera-browser-cli` has moved into
[`opera-devtools-mcp`](https://github.com/operasoftware/opera-devtools-mcp),
which now ships both the MCP server and the `opera-browser-cli` command.

This version is a **launcher**: it keeps the package name and the bin name so
that the command you already run installs the new implementation —

```sh
npm i -g opera-browser-cli # or: npm update -g opera-browser-cli
opera-browser-cli --version # the new implementation's version
```

— and delegates every invocation to `opera-devtools-mcp`, installed as its
dependency. Nothing about the command line changes.

When you are ready to drop the launcher and keep only one global package:

```sh
npm i -g opera-devtools-mcp@0.9.0 opera-browser-cli@0.2.0
npm rm -g opera-browser-cli # optional: drops the inert tombstone
```

or, in two steps:

```sh
npm rm -g opera-browser-cli
npm i -g opera-devtools-mcp@0.9.0
```

`opera-browser-cli --version` and `opera-browser-cli doctor` print the same
recipes whenever the CLI notices it was launched from here.

See
[`docs/npm-package-transition.md`](https://github.com/operasoftware/opera-devtools-mcp/blob/main/docs/npm-package-transition.md)
for the full transition, including what each release train publishes.
157 changes: 157 additions & 0 deletions launcher/bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#!/usr/bin/env node

/**
* `opera-browser-cli` compatibility launcher.
*
* The CLI moved into `opera-devtools-mcp`, but npm has no way to remove another
* package's global binstub except a **same-name upgrade**: it retires the
* previous version of the incoming package's name — unlinking its binstubs —
* before the new package's bin-conflict check runs. So this package keeps the
* old name and the old bin name and depends on the real implementation, which
* makes `npm i -g opera-browser-cli` / `npm update -g opera-browser-cli` — the
* command users already run — install the new CLI without `--force` and
* without a manual uninstall.
*
* The launcher only resolves and spawns; it never reimplements the CLI. The
* implementation entry is ESM (so it is spawned, not `require`d), and stdio is
* inherited for a zero-copy pass-through of TTYs and pipes.
*
* Marker it hands the child (read by `--version` and `doctor` in
* `opera-devtools-mcp`, `src/opera/launcherNotice.ts`):
* OPERA_CLI_LAUNCHER=1 — this CLI came from the launcher
* OPERA_CLI_LAUNCHER_PREFIX=<prefix> — the global prefix it resolved
*
* Retire the launcher when `opera-browser-cli@0.2.0` (a bin-less tombstone) and
* `opera-devtools-mcp@0.9.0` (dual bin) exist:
* npm i -g opera-devtools-mcp@0.9.0 opera-browser-cli@0.2.0
* npm rm -g opera-browser-cli # optional: drops the inert tombstone
*/

'use strict';

const {execSync, spawnSync} = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');

/** Where the implementation's CLI entry lives inside its package. */
const CLI_ENTRY = path.join('build', 'src', 'bin', 'opera-browser-cli.js');

/** What npm sets while it runs an install script, when it is set at all. */
function envPrefix() {
const prefix = process.env.npm_config_prefix ?? process.env.NPM_CONFIG_PREFIX;
return prefix ? prefix : null;
}

/** The prefix npm installed this launcher under, asked of npm as a last resort. */
function npmPrefix() {
try {
return (
execSync('npm config get prefix', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5_000,
}).trim() || null
);
} catch {
return null;
}
}

/**
* The prefix the launcher itself sits under, read from its own path: npm links
* `<prefix>/bin/opera-browser-cli` to `<prefix>/lib/node_modules/<pkg>/bin/cli.js`
* on POSIX and `<prefix>/node_modules/<pkg>/bin/cli.js` on Windows. Deriving it
* costs nothing; asking npm would spawn a process on every single command.
*/
function ownPathPrefix() {
const packageDir = path.dirname(__dirname);
if (path.basename(packageDir) !== 'opera-browser-cli') {
return null;
}
const nodeModules = path.dirname(packageDir);
if (path.basename(nodeModules) !== 'node_modules') {
return null;
}
const parent = path.dirname(nodeModules);
return path.basename(parent) === 'lib' ? path.dirname(parent) : parent;
}

function globalPrefix() {
return ownPathPrefix() ?? envPrefix() ?? npmPrefix();
}

/**
* Where the implementation may be, best first: a sibling root is a package the
* user installed directly and is therefore the one they upgrade, while the
* nested copy is whatever `^0.8.0` resolved when *this* launcher was installed.
*/
function implementationCandidates() {
const candidates = [];
const prefix = globalPrefix();
if (prefix) {
candidates.push(
path.join(prefix, 'lib', 'node_modules', 'opera-devtools-mcp'),
path.join(prefix, 'node_modules', 'opera-devtools-mcp'),
);
}
try {
candidates.push(path.dirname(require.resolve('opera-devtools-mcp/package.json')));
} catch {
// No nested copy either: the candidates tried so far are what the error lists.
}
return candidates;
}

function resolveImplementation() {
const candidates = implementationCandidates();
const usable = candidates.find(dir => fs.existsSync(path.join(dir, CLI_ENTRY)));
if (usable) {
return usable;
}
process.stderr.write(
[
'opera-browser-cli: the compatibility launcher cannot find the new implementation.',
...candidates.map(dir => ` tried ${dir}`),
'Install it directly, then retire this launcher:',
' npm i -g opera-devtools-mcp@latest',
' npm rm -g opera-browser-cli',
'',
].join('\n'),
);
process.exit(1);
}

const prefix = globalPrefix();
const result = spawnSync(
process.execPath,
[path.join(resolveImplementation(), CLI_ENTRY), ...process.argv.slice(2)],
{
stdio: 'inherit',
env: {
...process.env,
OPERA_CLI_LAUNCHER: '1',
OPERA_CLI_LAUNCHER_PREFIX: prefix ?? '',
},
},
);

if (result.error) {
process.stderr.write(
`opera-browser-cli: could not start the new implementation: ${result.error.message}\n`,
);
process.exit(1);
}

if (result.signal) {
// A child killed by a signal (`Ctrl-C` sends SIGINT to the whole foreground
// process group) has no exit status, and `?? 1` would report the user's
// cancellation as a failure — visible to `set -e` and to CI runners. Re-raise
// it instead; the ones Windows cannot raise on itself fall through to 1.
try {
process.kill(process.pid, result.signal);
} catch {
// Unsupported signal: the exit code below is all that is left.
}
}

process.exit(result.status ?? 1);
18 changes: 18 additions & 0 deletions launcher/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "opera-browser-cli",
"version": "0.1.55",
"description": "Compatibility launcher — delegates to opera-devtools-mcp",
"license": "MIT",
"bin": {
"opera-browser-cli": "./bin/cli.js"
},
"dependencies": {
"opera-devtools-mcp": "^0.8.0"
},
"files": [
"bin/"
],
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
}
29 changes: 29 additions & 0 deletions tombstone/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# opera-browser-cli is retired

The CLI lives in
[`opera-devtools-mcp`](https://github.com/operasoftware/opera-devtools-mcp)
now — one package provides both the MCP server and the `opera-browser-cli`
command.

```sh
npm rm -g opera-browser-cli
npm i -g opera-devtools-mcp@latest
```

If you installed `opera-browser-cli@0.1.55`, that version was a **launcher**: it
kept the package name so the command you already run delivered the new
implementation, and it delegated every invocation to `opera-devtools-mcp`.

This version (`0.2.0`) is the tombstone that retires the launcher. It has no
bin and no dependencies; installing it is what lets npm unlink the launcher's
global binstub on a same-name upgrade, which is the only way npm removes a
foreign binstub without `--force`. Installing it *together with* the new MCP
package does the whole move in one command:

```sh
npm i -g opera-devtools-mcp@0.9.0 opera-browser-cli@0.2.0
npm rm -g opera-browser-cli # optional: drops this inert package
```

`opera-browser-cli --version` and `opera-browser-cli doctor` print both recipes
whenever the CLI notices it was launched by the launcher.
12 changes: 12 additions & 0 deletions tombstone/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "opera-browser-cli",
"version": "0.2.0",
"description": "Retired. Use opera-devtools-mcp.",
"license": "MIT",
"files": [
"README.md"
],
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23"
}
}
Loading