@agentrhq/webcmd 0.2.0 → 0.2.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 +23 -11
- package/cli-manifest.json +2 -0
- package/clis/twitter/delete.js +14 -6
- package/clis/twitter/delete.test.js +74 -1
- package/clis/twitter/timeline.js +3 -1
- package/clis/twitter/timeline.test.js +4 -0
- package/dist/src/browser/bridge-readiness.test.js +1 -1
- package/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +6 -4
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/browser/daemon-client.js +7 -2
- package/dist/src/browser/daemon-client.test.js +33 -30
- package/dist/src/browser/daemon-transport.js +3 -19
- package/dist/src/browser/page.d.ts +2 -1
- package/dist/src/browser/page.js +5 -1
- package/dist/src/browser/profile.d.ts +9 -0
- package/dist/src/browser/profile.js +18 -7
- package/dist/src/browser/profile.test.d.ts +1 -0
- package/dist/src/browser/profile.test.js +44 -0
- package/dist/src/browser/protocol.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +24 -10
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +3 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +87 -0
- package/dist/src/cli.js +22 -17
- package/dist/src/cli.test.js +2 -2
- package/dist/src/commands/daemon.test.js +13 -13
- package/dist/src/constants.d.ts +2 -3
- package/dist/src/constants.js +3 -10
- package/dist/src/daemon.js +1 -5
- package/dist/src/doctor.js +12 -0
- package/dist/src/doctor.test.js +30 -3
- package/dist/src/execution.js +5 -3
- package/dist/src/external.js +19 -1
- package/dist/src/external.test.js +37 -1
- package/dist/src/main.js +0 -5
- package/dist/src/node-network.test.js +3 -3
- package/dist/src/runtime-identity.test.js +0 -8
- package/dist/src/runtime.d.ts +2 -0
- package/dist/src/runtime.js +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { ENV_PREFIX } from '../brand.js';
|
|
6
|
+
import { profileRouteParams, resolveProfileSelection } from './profile.js';
|
|
7
|
+
describe('profile selection', () => {
|
|
8
|
+
let configDir;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-profile-test-'));
|
|
11
|
+
vi.stubEnv(`${ENV_PREFIX}_CONFIG_DIR`, configDir);
|
|
12
|
+
vi.stubEnv(`${ENV_PREFIX}_PROFILE`, '');
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vi.unstubAllEnvs();
|
|
16
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
function writeConfig(config) {
|
|
19
|
+
fs.writeFileSync(path.join(configDir, 'browser-profiles.json'), JSON.stringify(config));
|
|
20
|
+
}
|
|
21
|
+
it('tags an explicit profile argument as explicit and resolves aliases', () => {
|
|
22
|
+
writeConfig({ version: 1, aliases: { work: 'profile-work' } });
|
|
23
|
+
expect(resolveProfileSelection('work')).toEqual({ contextId: 'profile-work', source: 'explicit' });
|
|
24
|
+
});
|
|
25
|
+
it('tags WEBCMD_PROFILE as explicit', () => {
|
|
26
|
+
vi.stubEnv(`${ENV_PREFIX}_PROFILE`, 'profile-env');
|
|
27
|
+
expect(resolveProfileSelection()).toEqual({ contextId: 'profile-env', source: 'explicit' });
|
|
28
|
+
});
|
|
29
|
+
it('tags the persisted config default as preferred', () => {
|
|
30
|
+
writeConfig({ version: 1, aliases: {}, defaultContextId: 'profile-default' });
|
|
31
|
+
expect(resolveProfileSelection()).toEqual({ contextId: 'profile-default', source: 'preferred' });
|
|
32
|
+
});
|
|
33
|
+
it('explicit argument beats env, and env beats config default', () => {
|
|
34
|
+
vi.stubEnv(`${ENV_PREFIX}_PROFILE`, 'from-env');
|
|
35
|
+
writeConfig({ version: 1, aliases: {}, defaultContextId: 'from-config' });
|
|
36
|
+
expect(resolveProfileSelection('from-arg')).toEqual({ contextId: 'from-arg', source: 'explicit' });
|
|
37
|
+
expect(resolveProfileSelection()).toEqual({ contextId: 'from-env', source: 'explicit' });
|
|
38
|
+
});
|
|
39
|
+
it('maps explicit routes to contextId and preferred routes to preferredContextId', () => {
|
|
40
|
+
expect(profileRouteParams({ contextId: 'a', source: 'explicit' })).toEqual({ contextId: 'a' });
|
|
41
|
+
expect(profileRouteParams({ contextId: 'b', source: 'preferred' })).toEqual({ preferredContextId: 'b' });
|
|
42
|
+
expect(profileRouteParams(undefined)).toEqual({});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -10,8 +10,22 @@ class CloakActionError extends Error {
|
|
|
10
10
|
this.errorHint = errorHint;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
function commandProfileId(command) {
|
|
14
|
-
|
|
13
|
+
function commandProfileId(manager, command) {
|
|
14
|
+
const requested = command.profileId ?? command.contextId;
|
|
15
|
+
if (requested?.trim())
|
|
16
|
+
return requested.trim();
|
|
17
|
+
const preferred = command.preferredContextId?.trim();
|
|
18
|
+
if (!preferred)
|
|
19
|
+
return 'default';
|
|
20
|
+
const active = manager.activeProfileIds();
|
|
21
|
+
if (active.includes(preferred))
|
|
22
|
+
return preferred;
|
|
23
|
+
if (active.length === 1)
|
|
24
|
+
return active[0];
|
|
25
|
+
if (active.length > 1) {
|
|
26
|
+
throw new CloakActionError('profile_required', `Default Cloak profile "${preferred}" is not active and multiple profiles are running; choose one with --profile.`, undefined, 'Run webcmd profile list, then update the default with webcmd profile use <name> or pass --profile <name>.');
|
|
27
|
+
}
|
|
28
|
+
return preferred;
|
|
15
29
|
}
|
|
16
30
|
function invalidRequest(command, error) {
|
|
17
31
|
return { id: command.id, ok: false, errorCode: 'invalid_request', error };
|
|
@@ -24,7 +38,7 @@ async function resolveLease(manager, command) {
|
|
|
24
38
|
throw new CloakActionError('stale_page_identity', `Page not found: ${command.page} — stale page identity`);
|
|
25
39
|
}
|
|
26
40
|
return manager.getPage({
|
|
27
|
-
profileId: commandProfileId(command),
|
|
41
|
+
profileId: commandProfileId(manager, command),
|
|
28
42
|
session: command.session,
|
|
29
43
|
surface: command.surface,
|
|
30
44
|
siteSession: command.siteSession,
|
|
@@ -93,12 +107,12 @@ export async function dispatchCloakAction(manager, command) {
|
|
|
93
107
|
}
|
|
94
108
|
case 'close-window': {
|
|
95
109
|
if (command.page) {
|
|
96
|
-
const closed = await manager.closePage({ profileId: commandProfileId(command), pageId: command.page });
|
|
110
|
+
const closed = await manager.closePage({ profileId: commandProfileId(manager, command), pageId: command.page });
|
|
97
111
|
return { id: command.id, ok: true, data: { closed: Boolean(closed), page: closed ?? command.page, session: command.session } };
|
|
98
112
|
}
|
|
99
113
|
else {
|
|
100
114
|
await manager.release({
|
|
101
|
-
profileId: commandProfileId(command),
|
|
115
|
+
profileId: commandProfileId(manager, command),
|
|
102
116
|
session: command.session,
|
|
103
117
|
surface: command.surface,
|
|
104
118
|
});
|
|
@@ -108,12 +122,12 @@ export async function dispatchCloakAction(manager, command) {
|
|
|
108
122
|
case 'tabs': {
|
|
109
123
|
switch (command.op ?? 'list') {
|
|
110
124
|
case 'list': {
|
|
111
|
-
const tabs = await manager.listPages({ profileId: commandProfileId(command) });
|
|
125
|
+
const tabs = await manager.listPages({ profileId: commandProfileId(manager, command) });
|
|
112
126
|
return { id: command.id, ok: true, data: tabs };
|
|
113
127
|
}
|
|
114
128
|
case 'new': {
|
|
115
129
|
const lease = await manager.newPage({
|
|
116
|
-
profileId: commandProfileId(command),
|
|
130
|
+
profileId: commandProfileId(manager, command),
|
|
117
131
|
session: command.session,
|
|
118
132
|
surface: command.surface,
|
|
119
133
|
siteSession: command.siteSession,
|
|
@@ -123,13 +137,13 @@ export async function dispatchCloakAction(manager, command) {
|
|
|
123
137
|
return { id: command.id, ok: true, data: { title: await lease.page.title(), url: lease.page.url() }, page: lease.pageId };
|
|
124
138
|
}
|
|
125
139
|
case 'select': {
|
|
126
|
-
const lease = await manager.selectPage({ profileId: commandProfileId(command), pageId: command.page, index: command.index });
|
|
140
|
+
const lease = await manager.selectPage({ profileId: commandProfileId(manager, command), pageId: command.page, index: command.index });
|
|
127
141
|
if (!lease)
|
|
128
142
|
return { id: command.id, ok: false, errorCode: 'runtime_command_failed', error: 'Tab not found' };
|
|
129
143
|
return { id: command.id, ok: true, data: { selected: true, url: lease.page.url() }, page: lease.pageId };
|
|
130
144
|
}
|
|
131
145
|
case 'close': {
|
|
132
|
-
const closed = await manager.closePage({ profileId: commandProfileId(command), pageId: command.page, index: command.index });
|
|
146
|
+
const closed = await manager.closePage({ profileId: commandProfileId(manager, command), pageId: command.page, index: command.index });
|
|
133
147
|
if (!closed)
|
|
134
148
|
return { id: command.id, ok: false, errorCode: 'runtime_command_failed', error: 'Tab not found' };
|
|
135
149
|
return { id: command.id, ok: true, data: { closed } };
|
|
@@ -197,7 +211,7 @@ export async function dispatchCloakAction(manager, command) {
|
|
|
197
211
|
}
|
|
198
212
|
{
|
|
199
213
|
const lease = await manager.bindPage({
|
|
200
|
-
profileId: commandProfileId(command),
|
|
214
|
+
profileId: commandProfileId(manager, command),
|
|
201
215
|
session: command.session,
|
|
202
216
|
surface: command.surface,
|
|
203
217
|
siteSession: command.siteSession,
|
|
@@ -46,6 +46,7 @@ export declare class CloakSessionManager {
|
|
|
46
46
|
pending: number;
|
|
47
47
|
lastSeenAt: number;
|
|
48
48
|
}[];
|
|
49
|
+
activeProfileIds(): string[];
|
|
49
50
|
getPage(input: SessionKeyInput): Promise<CloakPageLease>;
|
|
50
51
|
findPageById(pageId: string, opts?: Pick<SessionKeyInput, 'idleTimeout'>): CloakPageLease | null;
|
|
51
52
|
pageIdFor(page: PlaywrightPage): string | undefined;
|
|
@@ -31,6 +31,9 @@ export class CloakSessionManager {
|
|
|
31
31
|
lastSeenAt: runtime.lastSeenAt,
|
|
32
32
|
}));
|
|
33
33
|
}
|
|
34
|
+
activeProfileIds() {
|
|
35
|
+
return [...this.profiles.keys()];
|
|
36
|
+
}
|
|
34
37
|
async getPage(input) {
|
|
35
38
|
const profileId = normalizeProfileId(input.profileId);
|
|
36
39
|
const session = requireSession(input.session);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { CloakSessionManager } from './session-manager.js';
|
|
4
|
+
import { dispatchCloakAction } from './actions.js';
|
|
3
5
|
function fakeContext() {
|
|
4
6
|
const page = {
|
|
5
7
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
@@ -20,6 +22,9 @@ function fakeContext() {
|
|
|
20
22
|
page,
|
|
21
23
|
};
|
|
22
24
|
}
|
|
25
|
+
function expectedProfileDir(profileId) {
|
|
26
|
+
return path.join('/tmp/webcmd-test', 'cloak', 'profiles', profileId);
|
|
27
|
+
}
|
|
23
28
|
describe('CloakSessionManager', () => {
|
|
24
29
|
afterEach(() => {
|
|
25
30
|
vi.useRealTimers();
|
|
@@ -89,4 +94,86 @@ describe('CloakSessionManager', () => {
|
|
|
89
94
|
expect(lease.page.close).not.toHaveBeenCalled();
|
|
90
95
|
expect(await manager.listPages({ profileId: 'default' })).toHaveLength(1);
|
|
91
96
|
});
|
|
97
|
+
it('launches a preferred profile when no Cloak profile is active', async () => {
|
|
98
|
+
const launched = fakeContext();
|
|
99
|
+
const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
|
|
100
|
+
const manager = new CloakSessionManager({
|
|
101
|
+
baseDir: '/tmp/webcmd-test',
|
|
102
|
+
launchPersistentContext,
|
|
103
|
+
});
|
|
104
|
+
await dispatchCloakAction(manager, {
|
|
105
|
+
id: 'cmd-preferred',
|
|
106
|
+
action: 'navigate',
|
|
107
|
+
session: 'work',
|
|
108
|
+
surface: 'browser',
|
|
109
|
+
url: 'https://example.com/',
|
|
110
|
+
preferredContextId: 'profile-default',
|
|
111
|
+
});
|
|
112
|
+
expect(launchPersistentContext).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(launchPersistentContext.mock.calls[0][0].userDataDir).toBe(expectedProfileDir('profile-default'));
|
|
114
|
+
});
|
|
115
|
+
it('falls back to the only active profile when the preferred profile is stale', async () => {
|
|
116
|
+
const launched = fakeContext();
|
|
117
|
+
const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
|
|
118
|
+
const manager = new CloakSessionManager({
|
|
119
|
+
baseDir: '/tmp/webcmd-test',
|
|
120
|
+
launchPersistentContext,
|
|
121
|
+
});
|
|
122
|
+
await dispatchCloakAction(manager, {
|
|
123
|
+
id: 'cmd-active',
|
|
124
|
+
action: 'navigate',
|
|
125
|
+
session: 'work',
|
|
126
|
+
surface: 'browser',
|
|
127
|
+
url: 'https://example.com/',
|
|
128
|
+
contextId: 'active',
|
|
129
|
+
});
|
|
130
|
+
await dispatchCloakAction(manager, {
|
|
131
|
+
id: 'cmd-stale-default',
|
|
132
|
+
action: 'navigate',
|
|
133
|
+
session: 'work',
|
|
134
|
+
surface: 'browser',
|
|
135
|
+
url: 'https://example.com/next',
|
|
136
|
+
preferredContextId: 'stale-default',
|
|
137
|
+
});
|
|
138
|
+
expect(launchPersistentContext).toHaveBeenCalledTimes(1);
|
|
139
|
+
expect(launchPersistentContext.mock.calls[0][0].userDataDir).toBe(expectedProfileDir('active'));
|
|
140
|
+
});
|
|
141
|
+
it('asks for an explicit profile when a stale preferred profile meets multiple active profiles', async () => {
|
|
142
|
+
const launched = fakeContext();
|
|
143
|
+
const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
|
|
144
|
+
const manager = new CloakSessionManager({
|
|
145
|
+
baseDir: '/tmp/webcmd-test',
|
|
146
|
+
launchPersistentContext,
|
|
147
|
+
});
|
|
148
|
+
await dispatchCloakAction(manager, {
|
|
149
|
+
id: 'cmd-a',
|
|
150
|
+
action: 'navigate',
|
|
151
|
+
session: 'work-a',
|
|
152
|
+
surface: 'browser',
|
|
153
|
+
url: 'https://example.com/a',
|
|
154
|
+
contextId: 'profile-a',
|
|
155
|
+
});
|
|
156
|
+
await dispatchCloakAction(manager, {
|
|
157
|
+
id: 'cmd-b',
|
|
158
|
+
action: 'navigate',
|
|
159
|
+
session: 'work-b',
|
|
160
|
+
surface: 'browser',
|
|
161
|
+
url: 'https://example.com/b',
|
|
162
|
+
contextId: 'profile-b',
|
|
163
|
+
});
|
|
164
|
+
const result = await dispatchCloakAction(manager, {
|
|
165
|
+
id: 'cmd-stale',
|
|
166
|
+
action: 'navigate',
|
|
167
|
+
session: 'work',
|
|
168
|
+
surface: 'browser',
|
|
169
|
+
url: 'https://example.com/',
|
|
170
|
+
preferredContextId: 'stale-default',
|
|
171
|
+
});
|
|
172
|
+
expect(result).toMatchObject({
|
|
173
|
+
id: 'cmd-stale',
|
|
174
|
+
ok: false,
|
|
175
|
+
errorCode: 'profile_required',
|
|
176
|
+
});
|
|
177
|
+
expect(launchPersistentContext).toHaveBeenCalledTimes(2);
|
|
178
|
+
});
|
|
92
179
|
});
|
package/dist/src/cli.js
CHANGED
|
@@ -36,7 +36,7 @@ import { daemonRestart, daemonStatus, daemonStop } from './commands/daemon.js';
|
|
|
36
36
|
import { log } from './logger.js';
|
|
37
37
|
import { bindTab, BrowserCommandError, sendCommand } from './browser/daemon-client.js';
|
|
38
38
|
import { fetchDaemonStatus } from './browser/daemon-transport.js';
|
|
39
|
-
import { aliasForContextId, loadProfileConfig, renameProfile,
|
|
39
|
+
import { aliasForContextId, loadProfileConfig, profileRouteParams, renameProfile, resolveProfileSelection, setDefaultProfile } from './browser/profile.js';
|
|
40
40
|
import { formatDaemonVersion, isDaemonStale } from './browser/daemon-version.js';
|
|
41
41
|
import { DEFAULT_BROWSER_CONNECT_TIMEOUT } from './browser/config.js';
|
|
42
42
|
import { CLI_COMMAND } from './brand.js';
|
|
@@ -428,7 +428,7 @@ async function resolveStoredBrowserTarget(page, scope) {
|
|
|
428
428
|
return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
|
|
429
429
|
}
|
|
430
430
|
/** Create a browser page for browser commands. Uses a named browser session for continuity. */
|
|
431
|
-
async function getBrowserPage(session, targetPage,
|
|
431
|
+
async function getBrowserPage(session, targetPage, profileSelection, opts = {}) {
|
|
432
432
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
433
433
|
const bridge = new BrowserBridge();
|
|
434
434
|
// Internal GC timeout for browser sessions. Not the per-command runtime timeout.
|
|
@@ -438,11 +438,11 @@ async function getBrowserPage(session, targetPage, contextId, opts = {}) {
|
|
|
438
438
|
timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT,
|
|
439
439
|
session,
|
|
440
440
|
surface: 'browser',
|
|
441
|
-
...(
|
|
441
|
+
...profileRouteParams(profileSelection),
|
|
442
442
|
...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
|
|
443
443
|
windowMode: opts.windowMode ?? getBrowserWindowMode(undefined, 'foreground'),
|
|
444
444
|
});
|
|
445
|
-
const targetScope = getBrowserScope(session, contextId);
|
|
445
|
+
const targetScope = getBrowserScope(session, profileSelection?.contextId);
|
|
446
446
|
const resolvedTargetPage = targetPage
|
|
447
447
|
? await resolveBrowserTargetInSession(page, targetPage, { scope: targetScope, source: 'explicit' })
|
|
448
448
|
: await resolveStoredBrowserTarget(page, targetScope);
|
|
@@ -497,9 +497,9 @@ function getBrowserSession(command) {
|
|
|
497
497
|
return raw.trim();
|
|
498
498
|
throw new Error('<session> is a required positional argument: webcmd browser <session> <command>');
|
|
499
499
|
}
|
|
500
|
-
function
|
|
500
|
+
function getBrowserProfileSelection(command) {
|
|
501
501
|
const raw = getCommandOption(command, 'profile');
|
|
502
|
-
return
|
|
502
|
+
return resolveProfileSelection(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
|
|
503
503
|
}
|
|
504
504
|
function getPageSession(page) {
|
|
505
505
|
const session = page.session;
|
|
@@ -508,8 +508,11 @@ function getPageSession(page) {
|
|
|
508
508
|
throw new Error('Browser page is missing a session');
|
|
509
509
|
}
|
|
510
510
|
function getPageScope(page) {
|
|
511
|
-
const contextId = page
|
|
512
|
-
|
|
511
|
+
const { contextId, preferredContextId } = page;
|
|
512
|
+
const selected = typeof contextId === 'string' && contextId.trim()
|
|
513
|
+
? contextId.trim()
|
|
514
|
+
: (typeof preferredContextId === 'string' && preferredContextId.trim() ? preferredContextId.trim() : undefined);
|
|
515
|
+
return getBrowserScope(getPageSession(page), selected);
|
|
513
516
|
}
|
|
514
517
|
function snapshotMetricText(snapshot) {
|
|
515
518
|
return typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2);
|
|
@@ -882,9 +885,9 @@ Examples:
|
|
|
882
885
|
const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
|
|
883
886
|
const targetPage = getBrowserTargetId(command);
|
|
884
887
|
const session = getBrowserSession(command);
|
|
885
|
-
const
|
|
888
|
+
const profileSelection = getBrowserProfileSelection(command);
|
|
886
889
|
const windowMode = getBrowserWindowMode(command, 'foreground');
|
|
887
|
-
page = await getBrowserPage(session, targetPage,
|
|
890
|
+
page = await getBrowserPage(session, targetPage, profileSelection, { windowMode });
|
|
888
891
|
await fn(page, ...args);
|
|
889
892
|
}
|
|
890
893
|
catch (err) {
|
|
@@ -933,12 +936,14 @@ Examples:
|
|
|
933
936
|
? optsOrCommand
|
|
934
937
|
: {};
|
|
935
938
|
const session = getBrowserSession(command);
|
|
936
|
-
const
|
|
939
|
+
const profileSelection = getBrowserProfileSelection(command);
|
|
940
|
+
const contextId = profileSelection?.contextId;
|
|
941
|
+
const routing = profileRouteParams(profileSelection);
|
|
937
942
|
try {
|
|
938
943
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
939
944
|
const bridge = new BrowserBridge();
|
|
940
|
-
await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...
|
|
941
|
-
await fn({ session, contextId }, opts);
|
|
945
|
+
await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...routing });
|
|
946
|
+
await fn({ session, contextId, routing }, opts);
|
|
942
947
|
}
|
|
943
948
|
catch (err) {
|
|
944
949
|
if (err instanceof BrowserCommandError) {
|
|
@@ -956,7 +961,7 @@ Examples:
|
|
|
956
961
|
.description('Bind an existing Cloak runtime tab to the browser session named by <session>')
|
|
957
962
|
.option('--page <id>', 'Cloak tab page id from `webcmd browser <session> tab list`')
|
|
958
963
|
.option('--index <n>', 'Cloak tab index from `webcmd browser <session> tab list`')
|
|
959
|
-
.action(browserSessionCommandAction(async ({ session, contextId }, opts) => {
|
|
964
|
+
.action(browserSessionCommandAction(async ({ session, contextId, routing }, opts) => {
|
|
960
965
|
const page = typeof opts.page === 'string' && opts.page.trim() ? opts.page.trim() : undefined;
|
|
961
966
|
const rawIndex = typeof opts.index === 'string' && opts.index.trim() ? opts.index.trim() : undefined;
|
|
962
967
|
if ((page && rawIndex) || (!page && !rawIndex)) {
|
|
@@ -966,14 +971,14 @@ Examples:
|
|
|
966
971
|
throw new BrowserCommandError('--index must be a non-negative integer.', 'invalid_request');
|
|
967
972
|
}
|
|
968
973
|
const index = rawIndex === undefined ? undefined : Number.parseInt(rawIndex, 10);
|
|
969
|
-
const data = await bindTab(session, { ...
|
|
974
|
+
const data = await bindTab(session, { ...routing, ...(page && { page }), ...(index !== undefined && { index }) });
|
|
970
975
|
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
971
976
|
console.log(JSON.stringify({ session, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
|
|
972
977
|
}));
|
|
973
978
|
browser.command('unbind')
|
|
974
979
|
.description('Compatibility command; release the Cloak browser session named by <session>')
|
|
975
|
-
.action(browserSessionCommandAction(async ({ session, contextId }) => {
|
|
976
|
-
await sendCommand('close-window', { session, surface: 'browser', ...
|
|
980
|
+
.action(browserSessionCommandAction(async ({ session, contextId, routing }) => {
|
|
981
|
+
await sendCommand('close-window', { session, surface: 'browser', ...routing });
|
|
977
982
|
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
978
983
|
console.log(JSON.stringify({ unbound: true, session }, null, 2));
|
|
979
984
|
}));
|
package/dist/src/cli.test.js
CHANGED
|
@@ -1004,7 +1004,7 @@ describe('profile list', () => {
|
|
|
1004
1004
|
runtimeVersion: '1.0.3',
|
|
1005
1005
|
pending: 0,
|
|
1006
1006
|
memoryMB: 20,
|
|
1007
|
-
port:
|
|
1007
|
+
port: 9777,
|
|
1008
1008
|
}),
|
|
1009
1009
|
});
|
|
1010
1010
|
const program = createProgram('', '');
|
|
@@ -1027,7 +1027,7 @@ describe('profile list', () => {
|
|
|
1027
1027
|
profiles: [],
|
|
1028
1028
|
pending: 0,
|
|
1029
1029
|
memoryMB: 20,
|
|
1030
|
-
port:
|
|
1030
|
+
port: 9777,
|
|
1031
1031
|
}),
|
|
1032
1032
|
});
|
|
1033
1033
|
const program = createProgram('', '');
|
|
@@ -39,7 +39,7 @@ describe('daemonStatus', () => {
|
|
|
39
39
|
runtimeVersion: '1.6.8',
|
|
40
40
|
pending: 0,
|
|
41
41
|
memoryMB: 64,
|
|
42
|
-
port:
|
|
42
|
+
port: 9777,
|
|
43
43
|
});
|
|
44
44
|
await daemonStatus();
|
|
45
45
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
|
|
@@ -49,7 +49,7 @@ describe('daemonStatus', () => {
|
|
|
49
49
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
|
|
50
50
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('v1.6.8'));
|
|
51
51
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('64 MB'));
|
|
52
|
-
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('
|
|
52
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('9777'));
|
|
53
53
|
});
|
|
54
54
|
it('shows disconnected when runtime is not connected', async () => {
|
|
55
55
|
fetchDaemonStatusMock.mockResolvedValue({
|
|
@@ -61,7 +61,7 @@ describe('daemonStatus', () => {
|
|
|
61
61
|
runtimeName: 'fake',
|
|
62
62
|
pending: 0,
|
|
63
63
|
memoryMB: 32,
|
|
64
|
-
port:
|
|
64
|
+
port: 9777,
|
|
65
65
|
});
|
|
66
66
|
await daemonStatus();
|
|
67
67
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('disconnected'));
|
|
@@ -77,7 +77,7 @@ describe('daemonStatus', () => {
|
|
|
77
77
|
runtimeVersion: undefined,
|
|
78
78
|
pending: 0,
|
|
79
79
|
memoryMB: 32,
|
|
80
|
-
port:
|
|
80
|
+
port: 9777,
|
|
81
81
|
});
|
|
82
82
|
await daemonStatus();
|
|
83
83
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('fake connected'));
|
|
@@ -105,7 +105,7 @@ describe('daemonStatus runtime label states (#1575)', () => {
|
|
|
105
105
|
daemonVersion: PKG_VERSION,
|
|
106
106
|
pending: 0,
|
|
107
107
|
memoryMB: 32,
|
|
108
|
-
port:
|
|
108
|
+
port: 9777,
|
|
109
109
|
runtimeName: 'fake',
|
|
110
110
|
...extra,
|
|
111
111
|
});
|
|
@@ -173,7 +173,7 @@ describe('daemonStop', () => {
|
|
|
173
173
|
runtimeName: 'fake',
|
|
174
174
|
pending: 0,
|
|
175
175
|
memoryMB: 50,
|
|
176
|
-
port:
|
|
176
|
+
port: 9777,
|
|
177
177
|
});
|
|
178
178
|
requestDaemonShutdownMock.mockResolvedValue(true);
|
|
179
179
|
await daemonStop();
|
|
@@ -190,7 +190,7 @@ describe('daemonStop', () => {
|
|
|
190
190
|
runtimeName: 'fake',
|
|
191
191
|
pending: 0,
|
|
192
192
|
memoryMB: 50,
|
|
193
|
-
port:
|
|
193
|
+
port: 9777,
|
|
194
194
|
});
|
|
195
195
|
requestDaemonShutdownMock.mockResolvedValue(false);
|
|
196
196
|
await daemonStop();
|
|
@@ -221,7 +221,7 @@ describe('daemonRestart', () => {
|
|
|
221
221
|
profiles: [{ contextId: 'work', runtimeConnected: true, pending: 0 }],
|
|
222
222
|
pending: 0,
|
|
223
223
|
memoryMB: 50,
|
|
224
|
-
port:
|
|
224
|
+
port: 9777,
|
|
225
225
|
});
|
|
226
226
|
restartDaemonMock.mockResolvedValue({
|
|
227
227
|
previousStatus: { daemonVersion: '1.7.6' },
|
|
@@ -237,13 +237,13 @@ describe('daemonRestart', () => {
|
|
|
237
237
|
profiles: [{ contextId: 'work', runtimeConnected: true, pending: 0 }],
|
|
238
238
|
pending: 0,
|
|
239
239
|
memoryMB: 51,
|
|
240
|
-
port:
|
|
240
|
+
port: 9777,
|
|
241
241
|
},
|
|
242
242
|
});
|
|
243
243
|
await daemonRestart();
|
|
244
244
|
expect(restartDaemonMock).toHaveBeenCalledTimes(1);
|
|
245
245
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('will disconnect 1 browser profile'));
|
|
246
|
-
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon restarted on port
|
|
246
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon restarted on port 9777 (v${PKG_VERSION})`));
|
|
247
247
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Runtime connected; 1 profile connected'));
|
|
248
248
|
});
|
|
249
249
|
it('starts a new daemon when none was running', async () => {
|
|
@@ -261,11 +261,11 @@ describe('daemonRestart', () => {
|
|
|
261
261
|
runtimeName: 'fake',
|
|
262
262
|
pending: 0,
|
|
263
263
|
memoryMB: 51,
|
|
264
|
-
port:
|
|
264
|
+
port: 9777,
|
|
265
265
|
},
|
|
266
266
|
});
|
|
267
267
|
await daemonRestart();
|
|
268
|
-
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port
|
|
268
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port 9777 (v${PKG_VERSION})`));
|
|
269
269
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Cloak runtime has not connected yet'));
|
|
270
270
|
});
|
|
271
271
|
it('reports failure when the daemon cannot stop', async () => {
|
|
@@ -278,7 +278,7 @@ describe('daemonRestart', () => {
|
|
|
278
278
|
runtimeName: 'fake',
|
|
279
279
|
pending: 0,
|
|
280
280
|
memoryMB: 50,
|
|
281
|
-
port:
|
|
281
|
+
port: 9777,
|
|
282
282
|
});
|
|
283
283
|
restartDaemonMock.mockResolvedValue({
|
|
284
284
|
previousStatus: { daemonVersion: '1.7.6' },
|
package/dist/src/constants.d.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Shared constants used across explore, synthesize, and pipeline modules.
|
|
3
3
|
*/
|
|
4
4
|
import { DAEMON_HEADER_NAME } from './brand.js';
|
|
5
|
-
/** Default daemon port for HTTP
|
|
6
|
-
export declare const DEFAULT_DAEMON_PORT =
|
|
7
|
-
export declare function unsupportedDaemonPortEnvMessage(value?: string): string;
|
|
5
|
+
/** Default daemon port for HTTP communication with the browser runtime. */
|
|
6
|
+
export declare const DEFAULT_DAEMON_PORT = 9777;
|
|
8
7
|
export { DAEMON_HEADER_NAME };
|
|
9
8
|
/** URL query params that are volatile/ephemeral and should be stripped from patterns */
|
|
10
9
|
export declare const VOLATILE_PARAMS: Set<string>;
|
package/dist/src/constants.js
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared constants used across explore, synthesize, and pipeline modules.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
/** Default daemon port for HTTP
|
|
6
|
-
export const DEFAULT_DAEMON_PORT =
|
|
7
|
-
export function unsupportedDaemonPortEnvMessage(value) {
|
|
8
|
-
const envName = `${ENV_PREFIX}_DAEMON_PORT`;
|
|
9
|
-
const suffix = value ? ` (received ${value})` : '';
|
|
10
|
-
return `${envName} is no longer supported${suffix}. ` +
|
|
11
|
-
`The ${PRODUCT_DISPLAY_NAME} Chrome extension can only connect to localhost:${DEFAULT_DAEMON_PORT}. ` +
|
|
12
|
-
`Unset ${envName} and rerun ${CLI_COMMAND}.`;
|
|
13
|
-
}
|
|
4
|
+
import { DAEMON_HEADER_NAME } from './brand.js';
|
|
5
|
+
/** Default daemon port for HTTP communication with the browser runtime. */
|
|
6
|
+
export const DEFAULT_DAEMON_PORT = 9777;
|
|
14
7
|
export { DAEMON_HEADER_NAME };
|
|
15
8
|
/** URL query params that are volatile/ephemeral and should be stripped from patterns */
|
|
16
9
|
export const VOLATILE_PARAMS = new Set([
|
package/dist/src/daemon.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import { DEFAULT_DAEMON_PORT
|
|
1
|
+
import { DEFAULT_DAEMON_PORT } from './constants.js';
|
|
2
2
|
import { EXIT_CODES } from './errors.js';
|
|
3
3
|
import { log } from './logger.js';
|
|
4
4
|
import { PKG_VERSION } from './version.js';
|
|
5
5
|
import { createDaemonServer } from './daemon/server.js';
|
|
6
6
|
import { LocalCloakRuntimeProvider } from './browser/runtime/local-cloak/provider.js';
|
|
7
|
-
if (process.env.WEBCMD_DAEMON_PORT) {
|
|
8
|
-
log.error(unsupportedDaemonPortEnvMessage(process.env.WEBCMD_DAEMON_PORT));
|
|
9
|
-
process.exit(EXIT_CODES.USAGE_ERROR);
|
|
10
|
-
}
|
|
11
7
|
const provider = new LocalCloakRuntimeProvider();
|
|
12
8
|
const daemon = createDaemonServer(provider, { port: DEFAULT_DAEMON_PORT, host: '127.0.0.1', version: PKG_VERSION });
|
|
13
9
|
daemon.listen().then(() => {
|
package/dist/src/doctor.js
CHANGED
|
@@ -93,6 +93,18 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
93
93
|
if (!connectivity.ok) {
|
|
94
94
|
issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
|
|
95
95
|
}
|
|
96
|
+
const profileConfig = loadProfileConfig();
|
|
97
|
+
const staleDefault = profileConfig.defaultContextId;
|
|
98
|
+
if (staleDefault && profiles?.length && !profiles.some((p) => p.contextId === staleDefault)) {
|
|
99
|
+
const alias = aliasForContextId(profileConfig, staleDefault);
|
|
100
|
+
const label = alias ? `${alias} (${staleDefault})` : staleDefault;
|
|
101
|
+
const fallbackNote = profiles.length === 1
|
|
102
|
+
? `Commands currently fall back to the only active profile: ${profiles[0].contextId}.`
|
|
103
|
+
: 'Multiple profiles are active, so commands will ask you to choose.';
|
|
104
|
+
issues.push(`Default Cloak profile is not active: ${label}.\n` +
|
|
105
|
+
` ${fallbackNote}\n` +
|
|
106
|
+
' Refresh it with: webcmd profile list, then webcmd profile use <name>.');
|
|
107
|
+
}
|
|
96
108
|
if (adapterShadows.length > 0) {
|
|
97
109
|
issues.push(formatAdapterShadowIssue(adapterShadows));
|
|
98
110
|
}
|
package/dist/src/doctor.test.js
CHANGED
|
@@ -49,7 +49,7 @@ describe('doctor report rendering', () => {
|
|
|
49
49
|
runtimeVersion: '1.6.8',
|
|
50
50
|
issues: [],
|
|
51
51
|
}));
|
|
52
|
-
expect(text).toContain('[OK] Daemon: running on port
|
|
52
|
+
expect(text).toContain('[OK] Daemon: running on port 9777');
|
|
53
53
|
expect(text).toContain('(v1.7.9)');
|
|
54
54
|
expect(text).toContain('[OK] Runtime: Cloak connected (v1.6.8)');
|
|
55
55
|
expect(text).toContain('Everything looks good!');
|
|
@@ -66,7 +66,7 @@ describe('doctor report rendering', () => {
|
|
|
66
66
|
runtimeVersion: '1.0.3',
|
|
67
67
|
issues: ['Stale daemon detected: daemon v1.7.6 != CLI v1.7.9.\n Run: webcmd daemon restart'],
|
|
68
68
|
}));
|
|
69
|
-
expect(text).toContain('[WARN] Daemon: running on port
|
|
69
|
+
expect(text).toContain('[WARN] Daemon: running on port 9777 (v1.7.6, stale; CLI v1.7.9)');
|
|
70
70
|
expect(text).toContain('Run: webcmd daemon restart');
|
|
71
71
|
expect(text).not.toContain('Everything looks good!');
|
|
72
72
|
});
|
|
@@ -86,7 +86,7 @@ describe('doctor report rendering', () => {
|
|
|
86
86
|
runtimeConnected: false,
|
|
87
87
|
issues: ['Daemon is running but the Cloak runtime is not connected.'],
|
|
88
88
|
}));
|
|
89
|
-
expect(text).toContain('[OK] Daemon: running on port
|
|
89
|
+
expect(text).toContain('[OK] Daemon: running on port 9777');
|
|
90
90
|
expect(text).toContain('[MISSING] Runtime: Cloak not connected');
|
|
91
91
|
});
|
|
92
92
|
it('renders OK when the connected Cloak runtime version is unknown', () => {
|
|
@@ -157,6 +157,33 @@ describe('doctor report rendering', () => {
|
|
|
157
157
|
expect.stringContaining('Daemon is not running'),
|
|
158
158
|
]));
|
|
159
159
|
});
|
|
160
|
+
it('reports a stale default Cloak profile when it is not active', async () => {
|
|
161
|
+
const fs = await import('node:fs');
|
|
162
|
+
const os = await import('node:os');
|
|
163
|
+
const path = await import('node:path');
|
|
164
|
+
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-doctor-profile-'));
|
|
165
|
+
fs.writeFileSync(path.join(configDir, 'browser-profiles.json'), JSON.stringify({ version: 1, aliases: { work: 'profile-default' }, defaultContextId: 'profile-default' }));
|
|
166
|
+
vi.stubEnv('WEBCMD_CONFIG_DIR', configDir);
|
|
167
|
+
try {
|
|
168
|
+
mockGetDaemonHealth.mockResolvedValueOnce({
|
|
169
|
+
state: 'ready',
|
|
170
|
+
status: {
|
|
171
|
+
runtimeConnected: true,
|
|
172
|
+
runtimeName: 'Cloak',
|
|
173
|
+
profiles: [{ contextId: 'active-profile', runtimeConnected: true, pending: 0 }],
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
const report = await runBrowserDoctor();
|
|
177
|
+
expect(report.issues).toEqual(expect.arrayContaining([
|
|
178
|
+
expect.stringContaining('Default Cloak profile is not active: work (profile-default)'),
|
|
179
|
+
]));
|
|
180
|
+
expect(report.issues.join('\n')).toContain('fall back to the only active profile: active-profile');
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
vi.unstubAllEnvs();
|
|
184
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
185
|
+
}
|
|
186
|
+
});
|
|
160
187
|
it('reports flapping when live check succeeds but final status shows runtime disconnected', async () => {
|
|
161
188
|
mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-runtime', status: { runtimeConnected: false, runtimeName: 'Cloak' } });
|
|
162
189
|
const report = await runBrowserDoctor();
|