@phnx-labs/agents-cli 1.15.0 → 1.16.0
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/CHANGELOG.md +78 -39
- package/README.md +6 -6
- package/dist/commands/alias.js +2 -2
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +546 -75
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/fork.js +2 -2
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +81 -39
- package/dist/commands/plugins.js +48 -15
- package/dist/commands/prune.js +23 -1
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +37 -1
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +22 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/view.js +61 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -20
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +29 -1
- package/dist/lib/browser/chrome.js +5 -2
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +9 -4
- package/dist/lib/browser/drivers/ssh.js +9 -2
- package/dist/lib/browser/ipc.js +144 -23
- package/dist/lib/browser/profiles.d.ts +5 -2
- package/dist/lib/browser/profiles.js +77 -37
- package/dist/lib/browser/service.d.ts +81 -13
- package/dist/lib/browser/service.js +738 -131
- package/dist/lib/browser/types.d.ts +81 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -0
- package/dist/lib/commands.js +6 -2
- package/dist/lib/daemon.js +2 -3
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.js +2 -2
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +125 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1178 -21
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +8 -8
- package/dist/lib/permissions.js +8 -8
- package/dist/lib/plugins.d.ts +30 -1
- package/dist/lib/plugins.js +75 -3
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.js +8 -3
- package/dist/lib/session/discover.js +30 -15
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +6 -6
- package/dist/lib/skills.js +6 -2
- package/dist/lib/state.d.ts +86 -14
- package/dist/lib/state.js +150 -23
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +32 -3
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.js +2 -2
- package/dist/lib/versions.js +20 -21
- package/package.json +1 -1
- package/scripts/postinstall.js +1 -1
package/dist/commands/view.js
CHANGED
|
@@ -2,16 +2,16 @@ import chalk from 'chalk';
|
|
|
2
2
|
import ora from 'ora';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import * as yaml from 'yaml';
|
|
6
5
|
import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
7
6
|
import { formatUsageSection, formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
8
7
|
import { readManifest } from '../lib/manifest.js';
|
|
9
8
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, } from '../lib/versions.js';
|
|
10
9
|
import { getShimsDir, isShimsInPath, ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
11
10
|
import { getAgentResources } from '../lib/resources.js';
|
|
12
|
-
import { getAgentsDir, getUserAgentsDir,
|
|
11
|
+
import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts } from '../lib/state.js';
|
|
13
12
|
import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
|
|
14
13
|
import { getCentralRulesFileName } from '../lib/rules/rules.js';
|
|
14
|
+
import { composeRulesFromState } from '../lib/rules/compose.js';
|
|
15
15
|
import { getConfiguredRunStrategy } from '../lib/rotate.js';
|
|
16
16
|
import { confirm } from '@inquirer/prompts';
|
|
17
17
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -512,25 +512,19 @@ async function showAgentResources(agentId, requestedVersion) {
|
|
|
512
512
|
console.log(` ${display.padEnd(38)} ${pathStr}${syncStr}`);
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
|
-
// Render
|
|
516
|
-
//
|
|
515
|
+
// Render promptcuts (cross-agent, not per-version). Shortcuts are layered
|
|
516
|
+
// across system + user files with user precedence; the displayed file path
|
|
517
|
+
// is whichever is "live" — user if it exists, else system.
|
|
517
518
|
function renderPromptcuts() {
|
|
518
519
|
console.log(chalk.bold(`\nPromptcuts\n`));
|
|
519
|
-
const
|
|
520
|
-
|
|
520
|
+
const merged = readMergedPromptcuts();
|
|
521
|
+
const count = Object.keys(merged).length;
|
|
522
|
+
if (count === 0) {
|
|
521
523
|
console.log(` ${chalk.gray('none')}`);
|
|
522
524
|
return;
|
|
523
525
|
}
|
|
524
|
-
let count = 0;
|
|
525
|
-
try {
|
|
526
|
-
const parsed = yaml.parse(fs.readFileSync(promptcutsPath, 'utf-8'));
|
|
527
|
-
count = parsed?.shortcuts ? Object.keys(parsed.shortcuts).length : 0;
|
|
528
|
-
}
|
|
529
|
-
catch {
|
|
530
|
-
count = 0;
|
|
531
|
-
}
|
|
532
526
|
const label = `${count} shortcut${count === 1 ? '' : 's'}`;
|
|
533
|
-
console.log(` ${chalk.green(label).padEnd(24)} ${chalk.gray(formatPath(
|
|
527
|
+
console.log(` ${chalk.green(label).padEnd(24)} ${chalk.gray(formatPath(getEffectivePromptcutsPath(), cwd))}`);
|
|
534
528
|
}
|
|
535
529
|
// 1. Agent CLI info
|
|
536
530
|
console.log(chalk.bold('Agent CLIs\n'));
|
|
@@ -565,7 +559,58 @@ async function showAgentResources(agentId, requestedVersion) {
|
|
|
565
559
|
}
|
|
566
560
|
}
|
|
567
561
|
renderSection('MCP Servers', agentData.mcp);
|
|
568
|
-
|
|
562
|
+
// Rules section with subrules breakdown
|
|
563
|
+
function renderRulesSection() {
|
|
564
|
+
console.log(chalk.bold('\nRules\n'));
|
|
565
|
+
const items = agentData.memory;
|
|
566
|
+
if (items.length === 0) {
|
|
567
|
+
console.log(` ${chalk.gray('none')}`);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const versionStr = agentData.version ? ` (${agentData.version})` : '';
|
|
571
|
+
console.log(` ${chalk.bold(agentData.agentName)}${chalk.gray(versionStr)}:`);
|
|
572
|
+
// Get composed subrules for the user scope
|
|
573
|
+
let composedSubrules = [];
|
|
574
|
+
try {
|
|
575
|
+
const composed = composeRulesFromState({ cwd });
|
|
576
|
+
composedSubrules = composed.subrules;
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
// No preset configured or rules.yaml missing — show rules without subrule breakdown
|
|
580
|
+
}
|
|
581
|
+
for (const r of items) {
|
|
582
|
+
let nameColor = chalk.cyan;
|
|
583
|
+
if (r.syncState === 'synced')
|
|
584
|
+
nameColor = chalk.green;
|
|
585
|
+
else if (r.syncState === 'new')
|
|
586
|
+
nameColor = chalk.blue;
|
|
587
|
+
else if (r.syncState === 'modified')
|
|
588
|
+
nameColor = chalk.yellow;
|
|
589
|
+
else if (r.syncState === 'deleted')
|
|
590
|
+
nameColor = chalk.red;
|
|
591
|
+
let display = nameColor(r.name);
|
|
592
|
+
if (r.ruleCount !== undefined)
|
|
593
|
+
display += chalk.gray(` (${r.ruleCount} rules)`);
|
|
594
|
+
const sourceTag = r.scope === 'project' ? chalk.blue('[project]')
|
|
595
|
+
: r.scope === 'user' ? chalk.cyan('[user]')
|
|
596
|
+
: chalk.gray('[system]');
|
|
597
|
+
display += ` ${sourceTag}`;
|
|
598
|
+
const pathStr = r.path ? chalk.gray(formatPath(r.path, cwd)) : '';
|
|
599
|
+
const syncStr = r.syncState ? chalk.gray(` [${r.syncState}]`) : '';
|
|
600
|
+
console.log(` ${display.padEnd(38)} ${pathStr}${syncStr}`);
|
|
601
|
+
// Show subrules for user-scope rules (the compiled CLAUDE.md)
|
|
602
|
+
if (r.scope === 'user' && composedSubrules.length > 0) {
|
|
603
|
+
for (const sub of composedSubrules) {
|
|
604
|
+
const scopeLabel = sub.layerScope === 'project' ? chalk.blue('[project]')
|
|
605
|
+
: sub.layerScope === 'user' ? chalk.cyan('[user]')
|
|
606
|
+
: sub.layerScope === 'extra' ? chalk.magenta(`[${sub.layerAlias || 'extra'}]`)
|
|
607
|
+
: chalk.gray('[system]');
|
|
608
|
+
console.log(` ${chalk.gray('-')} ${sub.name} ${scopeLabel}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
renderRulesSection();
|
|
569
614
|
renderSection('Hooks', agentData.hooks);
|
|
570
615
|
renderPromptcuts();
|
|
571
616
|
// Show legend at the end if git repo exists
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* CLI entry point for agents-cli.
|
|
4
4
|
*
|
|
5
5
|
* Registers all commands, handles update checks, auto-corrects typos,
|
|
6
|
-
* and launches the first-run interactive
|
|
6
|
+
* and launches the first-run interactive setup when appropriate.
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from 'commander';
|
|
9
9
|
import chalk from 'chalk';
|
|
@@ -26,7 +26,7 @@ const VERSION = packageJson.version;
|
|
|
26
26
|
// Import command registrations
|
|
27
27
|
import { registerPullCommand } from './commands/pull.js';
|
|
28
28
|
import { registerRepoCommands } from './commands/repo.js';
|
|
29
|
-
import {
|
|
29
|
+
import { registerSetupCommand, runSetup } from './commands/setup.js';
|
|
30
30
|
import { registerStatusCommand } from './commands/status.js';
|
|
31
31
|
import { registerViewCommand } from './commands/view.js';
|
|
32
32
|
import { registerCommandsCommands } from './commands/commands.js';
|
|
@@ -59,7 +59,6 @@ import { registerAliasCommand } from './commands/alias.js';
|
|
|
59
59
|
import { registerBetaCommands } from './commands/beta.js';
|
|
60
60
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
61
61
|
import { isPromptCancelled } from './commands/utils.js';
|
|
62
|
-
import { getAgentsDir } from './lib/state.js';
|
|
63
62
|
import { AGENTS } from './lib/agents.js';
|
|
64
63
|
import { getGlobalDefault } from './lib/versions.js';
|
|
65
64
|
import { addShimsToPath, ensureShimCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
|
|
@@ -80,7 +79,7 @@ Install, configure, run, and dispatch AI coding agents from one place.
|
|
|
80
79
|
Works with Claude, Codex, Gemini, Cursor, OpenCode, OpenClaw, and Droid.
|
|
81
80
|
|
|
82
81
|
Quick start:
|
|
83
|
-
agents
|
|
82
|
+
agents setup First-time setup (interactive)
|
|
84
83
|
agents view See what's installed
|
|
85
84
|
agents run <agent> ["prompt"] Run an agent (interactive without prompt, headless with)
|
|
86
85
|
agents sessions Browse past sessions across all agents
|
|
@@ -199,7 +198,8 @@ async function showWhatsNew(fromVersion, toVersion) {
|
|
|
199
198
|
}
|
|
200
199
|
}
|
|
201
200
|
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
202
|
-
|
|
201
|
+
import { getUpdateCheckPath, getMigratedSentinelPath, getUserAgentsDir } from './lib/state.js';
|
|
202
|
+
const UPDATE_CHECK_FILE = getUpdateCheckPath();
|
|
203
203
|
/** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
|
|
204
204
|
function readUpdateCache() {
|
|
205
205
|
try {
|
|
@@ -526,7 +526,7 @@ program
|
|
|
526
526
|
});
|
|
527
527
|
registerPullCommand(program);
|
|
528
528
|
registerRepoCommands(program);
|
|
529
|
-
|
|
529
|
+
registerSetupCommand(program);
|
|
530
530
|
applyGlobalHelpConventions(program);
|
|
531
531
|
/** Calculate the Levenshtein edit distance between two strings. */
|
|
532
532
|
function levenshtein(a, b) {
|
|
@@ -582,7 +582,7 @@ await checkForUpdates();
|
|
|
582
582
|
// status marker that we'll print on the *next* invocation.
|
|
583
583
|
const { spawnDetachedSync } = await import('./lib/auto-pull.js');
|
|
584
584
|
spawnDetachedSync();
|
|
585
|
-
// First-run experience: no args + no config yet + TTY -> launch interactive
|
|
585
|
+
// First-run experience: no args + no config yet + TTY -> launch interactive setup.
|
|
586
586
|
// Skipped when stdin/stdout isn't a terminal (CI, pipes) or when user passes any args.
|
|
587
587
|
const passedArgs = process.argv.slice(2);
|
|
588
588
|
const requestedCommand = passedArgs.find((arg) => !arg.startsWith('-'));
|
|
@@ -613,14 +613,14 @@ async function registerLazyCommands() {
|
|
|
613
613
|
}
|
|
614
614
|
}
|
|
615
615
|
await registerLazyCommands();
|
|
616
|
-
const metaFilePath = path.join(
|
|
616
|
+
const metaFilePath = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
617
617
|
const firstRun = passedArgs.length === 0 &&
|
|
618
618
|
!fs.existsSync(metaFilePath) &&
|
|
619
619
|
process.stdin.isTTY &&
|
|
620
620
|
process.stdout.isTTY;
|
|
621
621
|
if (firstRun) {
|
|
622
622
|
try {
|
|
623
|
-
await
|
|
623
|
+
await runSetup(program);
|
|
624
624
|
}
|
|
625
625
|
catch (err) {
|
|
626
626
|
if (!(err instanceof Error && err.name === 'ExitPromptError')) {
|
|
@@ -629,14 +629,11 @@ if (firstRun) {
|
|
|
629
629
|
}
|
|
630
630
|
process.exit(0);
|
|
631
631
|
}
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
]);
|
|
638
|
-
if (!firstRun && requestedCommand && SYSTEM_REPO_COMMANDS.has(requestedCommand)) {
|
|
639
|
-
const { ensureInitialized } = await import('./commands/init.js');
|
|
632
|
+
// Every command requires the system repo to be cloned first. `setup` is the
|
|
633
|
+
// only exemption — it's the command that does the cloning.
|
|
634
|
+
const SETUP_EXEMPT_COMMANDS = new Set(['setup', 'help']);
|
|
635
|
+
if (!firstRun && requestedCommand && !SETUP_EXEMPT_COMMANDS.has(requestedCommand)) {
|
|
636
|
+
const { ensureInitialized } = await import('./commands/setup.js');
|
|
640
637
|
await ensureInitialized(program);
|
|
641
638
|
}
|
|
642
639
|
// One-shot idempotent migrations (split-layout, legacy file moves).
|
|
@@ -648,8 +645,8 @@ if (!firstRun && requestedCommand && SYSTEM_REPO_COMMANDS.has(requestedCommand))
|
|
|
648
645
|
if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
649
646
|
try {
|
|
650
647
|
const { runMigration } = await import('./lib/migrate.js');
|
|
651
|
-
const sentinel =
|
|
652
|
-
const sentinelValue = `${VERSION}-
|
|
648
|
+
const sentinel = getMigratedSentinelPath();
|
|
649
|
+
const sentinelValue = `${VERSION}-v8`;
|
|
653
650
|
let needRun = true;
|
|
654
651
|
try {
|
|
655
652
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
|
@@ -658,7 +655,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
658
655
|
}
|
|
659
656
|
catch { /* best-effort — fall through to run */ }
|
|
660
657
|
if (needRun) {
|
|
661
|
-
runMigration();
|
|
658
|
+
await runMigration();
|
|
662
659
|
try {
|
|
663
660
|
fs.mkdirSync(path.dirname(sentinel), { recursive: true });
|
|
664
661
|
fs.writeFileSync(sentinel, sentinelValue);
|
package/dist/lib/agents.js
CHANGED
|
@@ -16,7 +16,7 @@ import * as os from 'os';
|
|
|
16
16
|
import * as TOML from 'smol-toml';
|
|
17
17
|
import chalk from 'chalk';
|
|
18
18
|
import { walkForFiles } from './fs-walk.js';
|
|
19
|
-
import { getVersionsDir, getShimsDir,
|
|
19
|
+
import { getVersionsDir, getShimsDir, getCliVersionCachePath } from './state.js';
|
|
20
20
|
import { resolveVersion, getVersionHomePath, getBinaryPath } from './versions.js';
|
|
21
21
|
import { loadClaudeOauth } from './usage.js';
|
|
22
22
|
const execFileAsync = promisify(execFile);
|
|
@@ -29,7 +29,7 @@ const HOME = os.homedir();
|
|
|
29
29
|
export const CODEX_HOOKS_MIN_VERSION = '0.116.0';
|
|
30
30
|
/** Minimum Gemini CLI version that supports the hooks system (v0.26.0, Jan 2026). */
|
|
31
31
|
export const GEMINI_HOOKS_MIN_VERSION = '0.26.0';
|
|
32
|
-
const CLI_VERSION_CACHE_PATH =
|
|
32
|
+
const CLI_VERSION_CACHE_PATH = getCliVersionCachePath();
|
|
33
33
|
let cliVersionCache = null;
|
|
34
34
|
function loadCliVersionCache() {
|
|
35
35
|
if (cliVersionCache)
|
|
@@ -9,14 +9,13 @@
|
|
|
9
9
|
* Lock mtime under 5 min => skip (another invocation already in flight).
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
|
-
import * as path from 'path';
|
|
13
12
|
import { simpleGit } from 'simple-git';
|
|
14
13
|
import { tryAutoPull, isGitRepo } from './git.js';
|
|
15
|
-
import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos, } from './state.js';
|
|
14
|
+
import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos, getFetchCacheDir, } from './state.js';
|
|
16
15
|
import { lockFilePath, statusFilePath } from './auto-pull.js';
|
|
17
16
|
const LOCK_TTL_MS = 5 * 60 * 1000;
|
|
18
17
|
function ensureFetchDir() {
|
|
19
|
-
const dir =
|
|
18
|
+
const dir = getFetchCacheDir();
|
|
20
19
|
if (!fs.existsSync(dir)) {
|
|
21
20
|
try {
|
|
22
21
|
fs.mkdirSync(dir, { recursive: true });
|
package/dist/lib/auto-pull.js
CHANGED
|
@@ -13,10 +13,10 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as path from 'path';
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
|
-
import {
|
|
16
|
+
import { getFetchCacheDir } from './state.js';
|
|
17
17
|
/** Where lock files and per-repo status markers live. */
|
|
18
18
|
function fetchStateDir() {
|
|
19
|
-
return
|
|
19
|
+
return getFetchCacheDir();
|
|
20
20
|
}
|
|
21
21
|
/** Per-repo lock file path. mtime acts as a recency check. */
|
|
22
22
|
export function lockFilePath(alias) {
|
|
@@ -14,7 +14,13 @@ export declare class CDPClient {
|
|
|
14
14
|
private handleMessage;
|
|
15
15
|
private handleClose;
|
|
16
16
|
}
|
|
17
|
-
export
|
|
17
|
+
export interface BrowserDiscovery {
|
|
18
|
+
wsUrl: string;
|
|
19
|
+
browser: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function discoverBrowserWsUrl(port: number, host?: string): Promise<BrowserDiscovery>;
|
|
22
|
+
export declare function normalizeBrowserName(s: string): string;
|
|
23
|
+
export declare function verifyBrowserIdentity(reported: string, expected: string, port: number, host?: string): void;
|
|
18
24
|
export declare function listTargets(port: number, host?: string): Promise<Array<{
|
|
19
25
|
id: string;
|
|
20
26
|
type: string;
|
package/dist/lib/browser/cdp.js
CHANGED
|
@@ -83,7 +83,35 @@ export async function discoverBrowserWsUrl(port, host = 'localhost') {
|
|
|
83
83
|
throw new Error(`Failed to discover browser: ${response.status}`);
|
|
84
84
|
}
|
|
85
85
|
const data = (await response.json());
|
|
86
|
-
|
|
86
|
+
const browserField = data.Browser || data.Product || '';
|
|
87
|
+
return {
|
|
88
|
+
wsUrl: data.webSocketDebuggerUrl,
|
|
89
|
+
browser: normalizeBrowserName(browserField),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
export function normalizeBrowserName(s) {
|
|
93
|
+
if (!s)
|
|
94
|
+
return 'unknown';
|
|
95
|
+
return s.split('/')[0].trim().toLowerCase().replace(/\s+/g, '-');
|
|
96
|
+
}
|
|
97
|
+
export function verifyBrowserIdentity(reported, expected, port, host = 'localhost') {
|
|
98
|
+
if (expected === 'custom')
|
|
99
|
+
return;
|
|
100
|
+
if (reported === 'unknown')
|
|
101
|
+
return;
|
|
102
|
+
const matches = {
|
|
103
|
+
chrome: ['chrome', 'google-chrome', 'headlesschrome'],
|
|
104
|
+
chromium: ['chromium', 'headlesschrome'],
|
|
105
|
+
comet: ['comet'],
|
|
106
|
+
brave: ['brave', 'brave-browser'],
|
|
107
|
+
edge: ['edge', 'microsoft-edge', 'msedge'],
|
|
108
|
+
};
|
|
109
|
+
const accepted = matches[expected] || [expected];
|
|
110
|
+
if (accepted.includes(reported))
|
|
111
|
+
return;
|
|
112
|
+
const target = host === 'localhost' || host === '127.0.0.1' ? `port ${port}` : `${host}:${port}`;
|
|
113
|
+
throw new Error(`Browser identity mismatch: profile expects "${expected}" but ${target} is serving "${reported}". ` +
|
|
114
|
+
`Stop the running browser (e.g. \`pkill -f ${reported}\`) or update the profile to browser=${reported}, then retry.`);
|
|
87
115
|
}
|
|
88
116
|
export async function listTargets(port, host = 'localhost') {
|
|
89
117
|
const response = await fetch(`http://${host}:${port}/json`);
|
|
@@ -77,6 +77,7 @@ export async function launchBrowser(profileName, browserType, port, options = {}
|
|
|
77
77
|
'--disable-backgrounding-occluded-windows',
|
|
78
78
|
'--disable-renderer-backgrounding',
|
|
79
79
|
...(options.headless ? ['--headless=new'] : []),
|
|
80
|
+
...(options.viewport ? [`--window-size=${options.viewport.width},${options.viewport.height}`] : []),
|
|
80
81
|
...(options.args || []),
|
|
81
82
|
];
|
|
82
83
|
let env = { ...process.env };
|
|
@@ -103,7 +104,8 @@ export async function launchBrowser(profileName, browserType, port, options = {}
|
|
|
103
104
|
for (let i = 0; i < 30; i++) {
|
|
104
105
|
await sleep(200);
|
|
105
106
|
try {
|
|
106
|
-
|
|
107
|
+
const result = await discoverBrowserWsUrl(port);
|
|
108
|
+
wsUrl = result.wsUrl;
|
|
107
109
|
break;
|
|
108
110
|
}
|
|
109
111
|
catch {
|
|
@@ -116,7 +118,8 @@ export async function launchBrowser(profileName, browserType, port, options = {}
|
|
|
116
118
|
return { pid, port, wsUrl };
|
|
117
119
|
}
|
|
118
120
|
export async function attachToChrome(port) {
|
|
119
|
-
|
|
121
|
+
const { wsUrl } = await discoverBrowserWsUrl(port);
|
|
122
|
+
return wsUrl;
|
|
120
123
|
}
|
|
121
124
|
export function killChrome(pid) {
|
|
122
125
|
try {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const DEVICES = {
|
|
2
|
+
'iPhone 14': {
|
|
3
|
+
width: 390,
|
|
4
|
+
height: 844,
|
|
5
|
+
deviceScaleFactor: 3,
|
|
6
|
+
mobile: true,
|
|
7
|
+
},
|
|
8
|
+
'iPad': {
|
|
9
|
+
width: 768,
|
|
10
|
+
height: 1024,
|
|
11
|
+
deviceScaleFactor: 2,
|
|
12
|
+
mobile: true,
|
|
13
|
+
},
|
|
14
|
+
'MacBook Pro': {
|
|
15
|
+
width: 1440,
|
|
16
|
+
height: 900,
|
|
17
|
+
deviceScaleFactor: 2,
|
|
18
|
+
mobile: false,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
export function getDevice(name) {
|
|
22
|
+
const key = Object.keys(DEVICES).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
23
|
+
return key ? DEVICES[key] : undefined;
|
|
24
|
+
}
|
|
25
|
+
export function listDevices() {
|
|
26
|
+
return Object.keys(DEVICES);
|
|
27
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CDPClient, discoverBrowserWsUrl } from '../cdp.js';
|
|
1
|
+
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
|
|
2
2
|
import { launchBrowser, allocatePort } from '../chrome.js';
|
|
3
3
|
export async function connectLocal(endpoint, profile) {
|
|
4
4
|
const url = new URL(endpoint);
|
|
@@ -7,14 +7,19 @@ export async function connectLocal(endpoint, profile) {
|
|
|
7
7
|
}
|
|
8
8
|
const port = parseInt(url.port, 10) || 9222;
|
|
9
9
|
try {
|
|
10
|
-
const wsUrl = await discoverBrowserWsUrl(port);
|
|
10
|
+
const { wsUrl, browser } = await discoverBrowserWsUrl(port);
|
|
11
|
+
verifyBrowserIdentity(browser, profile.browser, port);
|
|
11
12
|
const cdp = new CDPClient();
|
|
12
13
|
await cdp.connect(wsUrl);
|
|
13
14
|
return { cdp, port, pid: 0 };
|
|
14
15
|
}
|
|
15
|
-
catch {
|
|
16
|
+
catch (err) {
|
|
17
|
+
if (err instanceof Error && err.message.startsWith('Browser identity mismatch')) {
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
16
20
|
const newPort = allocatePort();
|
|
17
|
-
const
|
|
21
|
+
const chromeOpts = { ...profile.chrome, viewport: profile.viewport };
|
|
22
|
+
const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary);
|
|
18
23
|
const cdp = new CDPClient();
|
|
19
24
|
await cdp.connect(wsUrl);
|
|
20
25
|
return { cdp, port: newPort, pid };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
2
|
import * as net from 'net';
|
|
3
|
-
import { CDPClient, discoverBrowserWsUrl } from '../cdp.js';
|
|
3
|
+
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
|
|
4
4
|
import { allocatePort } from '../chrome.js';
|
|
5
5
|
export async function connectSSH(endpoint, profile) {
|
|
6
6
|
const url = new URL(endpoint);
|
|
@@ -25,7 +25,14 @@ export async function connectSSH(endpoint, profile) {
|
|
|
25
25
|
tunnel.kill();
|
|
26
26
|
throw new Error(`SSH tunnel failed to establish to ${host}`);
|
|
27
27
|
}
|
|
28
|
-
const wsUrl = await discoverBrowserWsUrl(localPort);
|
|
28
|
+
const { wsUrl, browser } = await discoverBrowserWsUrl(localPort);
|
|
29
|
+
try {
|
|
30
|
+
verifyBrowserIdentity(browser, profile.browser, remotePort, host);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
tunnel.kill();
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
29
36
|
const cdp = new CDPClient();
|
|
30
37
|
await cdp.connect(wsUrl);
|
|
31
38
|
return {
|