@agentrhq/webcmd 0.2.5 → 0.3.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 -2
- package/cli-manifest.json +217 -13
- package/clis/_shared/site-auth.js +3 -3
- package/clis/_shared/site-auth.test.js +4 -4
- package/clis/chatgpt/ask.js +1 -1
- package/clis/chatgpt/ask.test.js +11 -0
- package/clis/chatgpt/commands.test.js +102 -1
- package/clis/chatgpt/deep-research-result.js +45 -3
- package/clis/chatgpt/image.js +22 -2
- package/clis/chatgpt/project-file-add.js +24 -1
- package/clis/chatgpt/utils.js +258 -52
- package/clis/chatgpt/utils.test.js +259 -1
- package/clis/claude/ask.js +21 -1
- package/clis/facebook/feed.js +110 -24
- package/clis/facebook/feed.test.js +62 -0
- package/clis/facebook/search.js +185 -38
- package/clis/facebook/search.test.js +95 -50
- package/clis/instagram/explore.js +30 -12
- package/clis/instagram/explore.test.js +77 -0
- package/clis/instagram/post.js +14 -1
- package/clis/instagram/reel.js +13 -1
- package/clis/linkedin/company.js +153 -0
- package/clis/linkedin/company.test.js +111 -0
- package/clis/linkedin/connections.js +135 -0
- package/clis/linkedin/connections.test.js +141 -0
- package/clis/mercury/reimbursement-draft.js +12 -1
- package/clis/slock/whoami.test.js +2 -2
- package/clis/twitter/article-evaluate.test.js +27 -0
- package/clis/twitter/article.js +73 -16
- package/clis/twitter/article.test.js +209 -0
- package/clis/twitter/download.js +6 -1
- package/clis/twitter/post.js +14 -1
- package/clis/twitter/profile.js +1 -1
- package/clis/twitter/profile.test.js +8 -0
- package/clis/twitter/quote.js +11 -1
- package/clis/twitter/reply.js +11 -1
- package/dist/src/browser/base-page.d.ts +26 -0
- package/dist/src/browser/base-page.js +23 -8
- package/dist/src/browser/base-page.test.js +45 -12
- package/dist/src/browser/command-catalog.d.ts +6 -0
- package/dist/src/browser/command-catalog.js +259 -0
- package/dist/src/browser/command-catalog.test.d.ts +1 -0
- package/dist/src/browser/command-catalog.test.js +79 -0
- package/dist/src/browser/daemon-client.d.ts +2 -1
- package/dist/src/browser/runtime/local-cloak/actions.js +4 -1
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
- package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
- package/dist/src/browser/runtime/local-cloak/provider.test.js +36 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +10 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.js +25 -9
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
- package/dist/src/browser/target-resolver.d.ts +1 -0
- package/dist/src/browser/target-resolver.js +99 -4
- package/dist/src/browser/target-resolver.test.js +141 -1
- package/dist/src/build-manifest.d.ts +6 -0
- package/dist/src/build-manifest.js +18 -1
- package/dist/src/build-manifest.test.js +57 -1
- package/dist/src/builtin-command-surface.d.ts +9 -0
- package/dist/src/builtin-command-surface.js +16 -0
- package/dist/src/check-hosted-contract.test.d.ts +1 -0
- package/dist/src/check-hosted-contract.test.js +56 -0
- package/dist/src/cli.js +63 -110
- package/dist/src/cli.test.js +82 -6
- package/dist/src/command-presentation.d.ts +95 -0
- package/dist/src/command-presentation.js +486 -0
- package/dist/src/command-presentation.test.d.ts +1 -0
- package/dist/src/command-presentation.test.js +97 -0
- package/dist/src/command-surface.d.ts +43 -0
- package/dist/src/command-surface.js +205 -0
- package/dist/src/command-surface.test.d.ts +1 -0
- package/dist/src/command-surface.test.js +406 -0
- package/dist/src/commanderAdapter.d.ts +5 -1
- package/dist/src/commanderAdapter.js +12 -55
- package/dist/src/commanderAdapter.test.js +15 -3
- package/dist/src/commands/auth.js +3 -2
- package/dist/src/commands/auth.test.js +6 -6
- package/dist/src/community-plugin-sync.d.ts +11 -0
- package/dist/src/community-plugin-sync.js +138 -0
- package/dist/src/community-plugin-sync.test.d.ts +1 -0
- package/dist/src/community-plugin-sync.test.js +123 -0
- package/dist/src/completion-fast.d.ts +4 -4
- package/dist/src/completion-fast.js +15 -34
- package/dist/src/completion-shared.d.ts +4 -0
- package/dist/src/completion-shared.js +38 -0
- package/dist/src/completion.js +3 -27
- package/dist/src/completion.test.js +31 -2
- package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
- package/dist/src/docs-sync-review-cli.test.js +322 -0
- package/dist/src/docs-sync-review.d.ts +117 -0
- package/dist/src/docs-sync-review.js +475 -0
- package/dist/src/docs-sync-review.test.d.ts +1 -0
- package/dist/src/docs-sync-review.test.js +324 -0
- package/dist/src/errors.d.ts +12 -3
- package/dist/src/errors.js +11 -7
- package/dist/src/errors.test.js +14 -1
- package/dist/src/execution.d.ts +1 -2
- package/dist/src/execution.js +4 -45
- package/dist/src/generate-release-notes-cli.test.js +11 -1
- package/dist/src/help.d.ts +4 -0
- package/dist/src/help.js +50 -255
- package/dist/src/help.test.js +27 -1
- package/dist/src/hosted/args.d.ts +4 -0
- package/dist/src/hosted/args.js +4 -0
- package/dist/src/hosted/args.test.d.ts +1 -0
- package/dist/src/hosted/args.test.js +49 -0
- package/dist/src/hosted/availability.d.ts +13 -0
- package/dist/src/hosted/availability.js +16 -0
- package/dist/src/hosted/availability.test.d.ts +1 -0
- package/dist/src/hosted/availability.test.js +180 -0
- package/dist/src/hosted/browser-args.d.ts +18 -0
- package/dist/src/hosted/browser-args.js +152 -0
- package/dist/src/hosted/browser-args.test.d.ts +1 -0
- package/dist/src/hosted/browser-args.test.js +182 -0
- package/dist/src/hosted/client.d.ts +59 -0
- package/dist/src/hosted/client.js +510 -0
- package/dist/src/hosted/client.test.d.ts +1 -0
- package/dist/src/hosted/client.test.js +738 -0
- package/dist/src/hosted/config.d.ts +55 -0
- package/dist/src/hosted/config.js +112 -0
- package/dist/src/hosted/config.test.d.ts +1 -0
- package/dist/src/hosted/config.test.js +81 -0
- package/dist/src/hosted/contract.d.ts +94 -0
- package/dist/src/hosted/contract.js +208 -0
- package/dist/src/hosted/contract.test.d.ts +1 -0
- package/dist/src/hosted/contract.test.js +361 -0
- package/dist/src/hosted/credentials.d.ts +38 -0
- package/dist/src/hosted/credentials.js +248 -0
- package/dist/src/hosted/credentials.test.d.ts +1 -0
- package/dist/src/hosted/credentials.test.js +93 -0
- package/dist/src/hosted/file-contract.test.d.ts +1 -0
- package/dist/src/hosted/file-contract.test.js +174 -0
- package/dist/src/hosted/files.d.ts +29 -0
- package/dist/src/hosted/files.js +296 -0
- package/dist/src/hosted/files.test.d.ts +1 -0
- package/dist/src/hosted/files.test.js +231 -0
- package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
- package/dist/src/hosted/main-lifecycle.test.js +213 -0
- package/dist/src/hosted/manifest.d.ts +14 -0
- package/dist/src/hosted/manifest.js +47 -0
- package/dist/src/hosted/manifest.test.d.ts +1 -0
- package/dist/src/hosted/manifest.test.js +164 -0
- package/dist/src/hosted/output-parity.test.d.ts +1 -0
- package/dist/src/hosted/output-parity.test.js +108 -0
- package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
- package/dist/src/hosted/root-command-surface.test.js +646 -0
- package/dist/src/hosted/runner.d.ts +19 -0
- package/dist/src/hosted/runner.js +705 -0
- package/dist/src/hosted/runner.test.d.ts +1 -0
- package/dist/src/hosted/runner.test.js +1607 -0
- package/dist/src/hosted/setup.d.ts +10 -0
- package/dist/src/hosted/setup.js +79 -0
- package/dist/src/hosted/setup.test.d.ts +1 -0
- package/dist/src/hosted/setup.test.js +195 -0
- package/dist/src/hosted/types.d.ts +175 -0
- package/dist/src/hosted/types.js +1 -0
- package/dist/src/main.js +123 -97
- package/dist/src/manifest-types.d.ts +2 -0
- package/dist/src/output.d.ts +17 -2
- package/dist/src/output.js +88 -81
- package/dist/src/output.test.js +141 -51
- package/dist/src/pipeline/executor.test.js +1 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/plugin-create-cli.test.d.ts +1 -0
- package/dist/src/plugin-create-cli.test.js +37 -0
- package/dist/src/plugin-manifest.d.ts +12 -0
- package/dist/src/plugin-manifest.js +16 -0
- package/dist/src/plugin-manifest.test.js +19 -1
- package/dist/src/plugin-scaffold.d.ts +4 -1
- package/dist/src/plugin-scaffold.js +4 -1
- package/dist/src/plugin-scaffold.test.js +23 -8
- package/dist/src/plugin.js +4 -1
- package/dist/src/plugin.test.js +13 -0
- package/dist/src/registry.d.ts +2 -0
- package/dist/src/release-notes.d.ts +6 -1
- package/dist/src/release-notes.js +192 -4
- package/dist/src/release-notes.test.js +143 -4
- package/dist/src/root-command-surface.d.ts +31 -0
- package/dist/src/root-command-surface.js +106 -0
- package/dist/src/serialization.d.ts +1 -16
- package/dist/src/serialization.js +5 -55
- package/dist/src/stream-write.d.ts +12 -0
- package/dist/src/stream-write.js +91 -0
- package/dist/src/stream-write.test.d.ts +1 -0
- package/dist/src/stream-write.test.js +186 -0
- package/dist/src/types.d.ts +5 -0
- package/dist/src/utils.d.ts +1 -1
- package/dist/src/utils.js +2 -8
- package/dist/src/utils.test.js +50 -0
- package/hosted-contract.json +36748 -0
- package/package.json +6 -1
- package/scripts/check-hosted-contract.mjs +108 -0
- package/scripts/docs-sync-review.ts +332 -0
- package/scripts/generate-release-notes.ts +1 -1
- package/scripts/sync-community-plugins.ts +14 -0
- package/skills/smart-search/SKILL.md +14 -3
- package/skills/webcmd-autofix/SKILL.md +26 -37
- package/skills/webcmd-usage/SKILL.md +21 -7
- package/clis/antigravity/SKILL.md +0 -38
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { posix as path } from 'node:path';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { buildLaunchOptions, humanizeBrowser } from 'cloakbrowser';
|
|
7
|
+
import { chromium } from 'playwright-core';
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
async function openApplication(appPath, args) {
|
|
10
|
+
await execFileAsync('/usr/bin/open', ['-g', '-n', appPath, '--args', ...args]);
|
|
11
|
+
}
|
|
12
|
+
export async function waitForDevToolsPort(portFile, timeoutMs = 10_000) {
|
|
13
|
+
const deadline = Date.now() + timeoutMs;
|
|
14
|
+
while (Date.now() < deadline) {
|
|
15
|
+
try {
|
|
16
|
+
const port = Number.parseInt((await readFile(portFile, 'utf8')).split('\n')[0], 10);
|
|
17
|
+
if (Number.isInteger(port) && port > 0)
|
|
18
|
+
return port;
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code !== 'ENOENT')
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
await delay(50);
|
|
25
|
+
}
|
|
26
|
+
throw new Error('Timed out waiting for background Chromium CDP endpoint');
|
|
27
|
+
}
|
|
28
|
+
async function terminateProfile(userDataDir) {
|
|
29
|
+
const { stdout } = await execFileAsync('/bin/ps', ['-axo', 'pid=,command=']);
|
|
30
|
+
const needle = `--user-data-dir=${userDataDir}`;
|
|
31
|
+
for (const line of stdout.split('\n')) {
|
|
32
|
+
if (!line.includes(needle))
|
|
33
|
+
continue;
|
|
34
|
+
const pid = Number.parseInt(line.trim().split(/\s+/, 1)[0], 10);
|
|
35
|
+
if (Number.isInteger(pid) && pid !== process.pid)
|
|
36
|
+
process.kill(pid, 'SIGTERM');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const defaultDependencies = {
|
|
40
|
+
buildLaunchOptions,
|
|
41
|
+
humanizeBrowser,
|
|
42
|
+
openApplication,
|
|
43
|
+
activateApplication: async (appPath) => {
|
|
44
|
+
await execFileAsync('/usr/bin/open', [appPath]);
|
|
45
|
+
},
|
|
46
|
+
readPort: waitForDevToolsPort,
|
|
47
|
+
connectOverCDP: endpoint => chromium.connectOverCDP(endpoint),
|
|
48
|
+
terminateProfile,
|
|
49
|
+
removePortFile: portFile => rm(portFile, { force: true }),
|
|
50
|
+
};
|
|
51
|
+
const contextActivators = new WeakMap();
|
|
52
|
+
export async function activateDarwinBackgroundContext(context) {
|
|
53
|
+
await contextActivators.get(context)?.();
|
|
54
|
+
}
|
|
55
|
+
function appPathFor(executablePath) {
|
|
56
|
+
const marker = `${path.sep}Contents${path.sep}MacOS${path.sep}`;
|
|
57
|
+
const index = executablePath.lastIndexOf(marker);
|
|
58
|
+
if (index < 0)
|
|
59
|
+
throw new Error(`Cloak Chromium executable is not inside a macOS app bundle: ${executablePath}`);
|
|
60
|
+
return executablePath.slice(0, index);
|
|
61
|
+
}
|
|
62
|
+
export async function launchDarwinBackgroundPersistentContext(options, deps = defaultDependencies) {
|
|
63
|
+
const portFile = path.join(options.userDataDir, 'DevToolsActivePort');
|
|
64
|
+
await deps.removePortFile(portFile);
|
|
65
|
+
const launchOptions = await deps.buildLaunchOptions(options);
|
|
66
|
+
if (!launchOptions.executablePath)
|
|
67
|
+
throw new Error('Cloak Chromium executable path is missing');
|
|
68
|
+
const appPath = appPathFor(launchOptions.executablePath);
|
|
69
|
+
let browser;
|
|
70
|
+
let launched = false;
|
|
71
|
+
try {
|
|
72
|
+
await deps.openApplication(appPath, [
|
|
73
|
+
...(launchOptions.args ?? []),
|
|
74
|
+
'--password-store=basic',
|
|
75
|
+
'--use-mock-keychain',
|
|
76
|
+
`--user-data-dir=${options.userDataDir}`,
|
|
77
|
+
'--remote-debugging-address=127.0.0.1',
|
|
78
|
+
'--remote-debugging-port=0',
|
|
79
|
+
'about:blank',
|
|
80
|
+
]);
|
|
81
|
+
launched = true;
|
|
82
|
+
const port = await deps.readPort(portFile);
|
|
83
|
+
browser = await deps.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
84
|
+
await deps.humanizeBrowser(browser, options);
|
|
85
|
+
const context = browser.contexts()[0];
|
|
86
|
+
if (!context)
|
|
87
|
+
throw new Error('Background Chromium did not expose a persistent context');
|
|
88
|
+
contextActivators.set(context, () => deps.activateApplication(appPath));
|
|
89
|
+
context.close = async () => {
|
|
90
|
+
try {
|
|
91
|
+
await browser.close();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
contextActivators.delete(context);
|
|
95
|
+
await deps.terminateProfile(options.userDataDir);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
return context;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
await browser?.close().catch(() => { });
|
|
102
|
+
if (launched)
|
|
103
|
+
await deps.terminateProfile(options.userDataDir).catch(() => { });
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { activateDarwinBackgroundContext, launchDarwinBackgroundPersistentContext, waitForDevToolsPort } from './darwin-background-launch.js';
|
|
3
|
+
const options = {
|
|
4
|
+
userDataDir: '/tmp/cloak profile',
|
|
5
|
+
headless: false,
|
|
6
|
+
humanize: true,
|
|
7
|
+
};
|
|
8
|
+
function fakeRuntime() {
|
|
9
|
+
const context = { close: vi.fn() };
|
|
10
|
+
const browser = {
|
|
11
|
+
contexts: vi.fn(() => [context]),
|
|
12
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
};
|
|
14
|
+
return { browser, context };
|
|
15
|
+
}
|
|
16
|
+
function fakeDependencies(browser) {
|
|
17
|
+
return {
|
|
18
|
+
buildLaunchOptions: vi.fn().mockResolvedValue({
|
|
19
|
+
executablePath: '/Applications/Cloak Chromium.app/Contents/MacOS/Chromium',
|
|
20
|
+
args: ['--fingerprint=123'],
|
|
21
|
+
}),
|
|
22
|
+
humanizeBrowser: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
openApplication: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
activateApplication: vi.fn().mockResolvedValue(undefined),
|
|
25
|
+
readPort: vi.fn().mockResolvedValue(43123),
|
|
26
|
+
connectOverCDP: vi.fn().mockResolvedValue(browser),
|
|
27
|
+
terminateProfile: vi.fn().mockResolvedValue(undefined),
|
|
28
|
+
removePortFile: vi.fn().mockResolvedValue(undefined),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
describe('launchDarwinBackgroundPersistentContext', () => {
|
|
32
|
+
it('launches the Chromium app without activation and connects through loopback CDP', async () => {
|
|
33
|
+
const { browser, context } = fakeRuntime();
|
|
34
|
+
const deps = fakeDependencies(browser);
|
|
35
|
+
const result = await launchDarwinBackgroundPersistentContext(options, deps);
|
|
36
|
+
expect(deps.removePortFile).toHaveBeenCalledWith('/tmp/cloak profile/DevToolsActivePort');
|
|
37
|
+
expect(deps.openApplication).toHaveBeenCalledWith('/Applications/Cloak Chromium.app', [
|
|
38
|
+
'--fingerprint=123',
|
|
39
|
+
'--password-store=basic',
|
|
40
|
+
'--use-mock-keychain',
|
|
41
|
+
'--user-data-dir=/tmp/cloak profile',
|
|
42
|
+
'--remote-debugging-address=127.0.0.1',
|
|
43
|
+
'--remote-debugging-port=0',
|
|
44
|
+
'about:blank',
|
|
45
|
+
]);
|
|
46
|
+
expect(deps.connectOverCDP).toHaveBeenCalledWith('http://127.0.0.1:43123');
|
|
47
|
+
expect(deps.humanizeBrowser).toHaveBeenCalledWith(browser, expect.objectContaining({ humanize: true }));
|
|
48
|
+
expect(result).toBe(context);
|
|
49
|
+
await activateDarwinBackgroundContext(result);
|
|
50
|
+
expect(deps.activateApplication).toHaveBeenCalledWith('/Applications/Cloak Chromium.app');
|
|
51
|
+
await result.close();
|
|
52
|
+
expect(browser.close).toHaveBeenCalledOnce();
|
|
53
|
+
expect(deps.terminateProfile).toHaveBeenCalledWith(options.userDataDir);
|
|
54
|
+
});
|
|
55
|
+
it('fails immediately when the CDP port file misses its deadline', async () => {
|
|
56
|
+
await expect(waitForDevToolsPort('/missing/DevToolsActivePort', 0)).rejects.toThrow('Timed out waiting for background Chromium CDP endpoint');
|
|
57
|
+
});
|
|
58
|
+
it('terminates the launched profile when CDP connection fails', async () => {
|
|
59
|
+
const { browser } = fakeRuntime();
|
|
60
|
+
const deps = fakeDependencies(browser);
|
|
61
|
+
deps.connectOverCDP.mockRejectedValueOnce(new Error('connect failed'));
|
|
62
|
+
await expect(launchDarwinBackgroundPersistentContext(options, deps)).rejects.toThrow('connect failed');
|
|
63
|
+
expect(deps.terminateProfile).toHaveBeenCalledWith(options.userDataDir);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -231,6 +231,42 @@ describe('LocalCloakRuntimeProvider', () => {
|
|
|
231
231
|
.resolves.toMatchObject({ id: 'close', ok: true, data: { closed: created.page } });
|
|
232
232
|
expect(pages[1].close).toHaveBeenCalled();
|
|
233
233
|
});
|
|
234
|
+
it('does not bring selected tabs to front in background window mode', async () => {
|
|
235
|
+
const { provider, pages } = makeProviderWithFakePage();
|
|
236
|
+
const created = await provider.dispatch({ id: 'new', action: 'tabs', op: 'new', session: 'work', surface: 'browser', url: 'https://second.example/', profileId: 'default' });
|
|
237
|
+
await expect(provider.dispatch({
|
|
238
|
+
id: 'select',
|
|
239
|
+
action: 'tabs',
|
|
240
|
+
op: 'select',
|
|
241
|
+
session: 'work',
|
|
242
|
+
surface: 'browser',
|
|
243
|
+
page: created.page,
|
|
244
|
+
profileId: 'default',
|
|
245
|
+
windowMode: 'background',
|
|
246
|
+
})).resolves.toMatchObject({ id: 'select', ok: true });
|
|
247
|
+
expect(pages[1].bringToFront).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
it('does not bring bound tabs to front in background window mode', async () => {
|
|
250
|
+
const { provider, pages } = makeProviderWithFakePage();
|
|
251
|
+
const created = await provider.dispatch({ id: 'new', action: 'tabs', op: 'new', session: 'source', surface: 'browser', url: 'https://second.example/', profileId: 'default' });
|
|
252
|
+
await expect(provider.dispatch({
|
|
253
|
+
id: 'bind',
|
|
254
|
+
action: 'bind',
|
|
255
|
+
session: 'target',
|
|
256
|
+
surface: 'browser',
|
|
257
|
+
page: created.page,
|
|
258
|
+
profileId: 'default',
|
|
259
|
+
windowMode: 'background',
|
|
260
|
+
})).resolves.toMatchObject({ id: 'bind', ok: true });
|
|
261
|
+
expect(pages[1].bringToFront).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
it('brings bound tabs to front by default', async () => {
|
|
264
|
+
const { provider, pages } = makeProviderWithFakePage();
|
|
265
|
+
const created = await provider.dispatch({ id: 'new', action: 'tabs', op: 'new', session: 'source', surface: 'browser', url: 'https://second.example/', profileId: 'default' });
|
|
266
|
+
await expect(provider.dispatch({ id: 'bind', action: 'bind', session: 'target', surface: 'browser', page: created.page, profileId: 'default' }))
|
|
267
|
+
.resolves.toMatchObject({ id: 'bind', ok: true });
|
|
268
|
+
expect(pages[1].bringToFront).toHaveBeenCalledOnce();
|
|
269
|
+
});
|
|
234
270
|
it('closes a window by page identity when command.page is provided', async () => {
|
|
235
271
|
const { provider, pages } = makeProviderWithFakePage();
|
|
236
272
|
const first = await provider.dispatch({ id: 'first', action: 'navigate', session: 'first', surface: 'browser', url: 'https://first.example/', profileId: 'default' });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BrowserContext, Page as PlaywrightPage } from 'playwright-core';
|
|
2
2
|
import { launchPersistentContext as cloakLaunchPersistentContext } from 'cloakbrowser';
|
|
3
|
-
import type { BrowserSurface, SiteSessionMode } from '../../protocol.js';
|
|
3
|
+
import type { BrowserSurface, BrowserWindowMode, SiteSessionMode } from '../../protocol.js';
|
|
4
|
+
import { activateDarwinBackgroundContext } from './darwin-background-launch.js';
|
|
4
5
|
import { CloakNetworkCapture } from './network.js';
|
|
5
6
|
export type LaunchPersistentContext = typeof cloakLaunchPersistentContext;
|
|
6
7
|
export type RecoverLockedProfile = (userDataDir: string) => Promise<boolean>;
|
|
@@ -10,6 +11,7 @@ export interface SessionKeyInput {
|
|
|
10
11
|
surface?: BrowserSurface;
|
|
11
12
|
siteSession?: SiteSessionMode;
|
|
12
13
|
idleTimeout?: number;
|
|
14
|
+
windowMode?: BrowserWindowMode;
|
|
13
15
|
/** Discard the existing leased page (if any) and create a new one under the same lease. */
|
|
14
16
|
freshPage?: boolean;
|
|
15
17
|
}
|
|
@@ -34,13 +36,19 @@ export interface CloakTabInfo {
|
|
|
34
36
|
export interface CloakSessionManagerOptions {
|
|
35
37
|
baseDir?: string;
|
|
36
38
|
launchPersistentContext?: LaunchPersistentContext;
|
|
39
|
+
launchBackgroundPersistentContext?: LaunchPersistentContext;
|
|
40
|
+
activateBackgroundContext?: typeof activateDarwinBackgroundContext;
|
|
37
41
|
recoverLockedProfile?: RecoverLockedProfile;
|
|
42
|
+
platform?: NodeJS.Platform;
|
|
38
43
|
}
|
|
39
44
|
export declare function resolveLeaseKey(input: SessionKeyInput): string;
|
|
40
45
|
export declare class CloakSessionManager {
|
|
41
46
|
private readonly opts;
|
|
42
47
|
readonly networkCapture: CloakNetworkCapture;
|
|
43
48
|
private readonly launchPersistentContext;
|
|
49
|
+
private readonly launchBackgroundPersistentContext;
|
|
50
|
+
private readonly activateBackgroundContext;
|
|
51
|
+
private readonly platform;
|
|
44
52
|
private readonly recoverLockedProfile;
|
|
45
53
|
private readonly profiles;
|
|
46
54
|
private readonly profileLaunches;
|
|
@@ -61,7 +69,7 @@ export declare class CloakSessionManager {
|
|
|
61
69
|
newPage(input: SessionKeyInput & {
|
|
62
70
|
url?: string;
|
|
63
71
|
}): Promise<CloakPageLease>;
|
|
64
|
-
selectPage(input: Pick<SessionKeyInput, 'profileId'> & {
|
|
72
|
+
selectPage(input: Pick<SessionKeyInput, 'profileId' | 'windowMode'> & {
|
|
65
73
|
pageId?: string;
|
|
66
74
|
index?: number;
|
|
67
75
|
}): Promise<CloakPageLease | null>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { execFile } from 'node:child_process';
|
|
3
3
|
import { launchPersistentContext as cloakLaunchPersistentContext } from 'cloakbrowser';
|
|
4
|
+
import { activateDarwinBackgroundContext, launchDarwinBackgroundPersistentContext } from './darwin-background-launch.js';
|
|
4
5
|
import { normalizeProfileId, resolveCloakProfileDir } from './profiles.js';
|
|
5
6
|
import { CloakNetworkCapture } from './network.js';
|
|
6
7
|
let pageCounter = 0;
|
|
@@ -18,12 +19,18 @@ export class CloakSessionManager {
|
|
|
18
19
|
opts;
|
|
19
20
|
networkCapture = new CloakNetworkCapture();
|
|
20
21
|
launchPersistentContext;
|
|
22
|
+
launchBackgroundPersistentContext;
|
|
23
|
+
activateBackgroundContext;
|
|
24
|
+
platform;
|
|
21
25
|
recoverLockedProfile;
|
|
22
26
|
profiles = new Map();
|
|
23
27
|
profileLaunches = new Map();
|
|
24
28
|
constructor(opts = {}) {
|
|
25
29
|
this.opts = opts;
|
|
26
30
|
this.launchPersistentContext = opts.launchPersistentContext ?? cloakLaunchPersistentContext;
|
|
31
|
+
this.launchBackgroundPersistentContext = opts.launchBackgroundPersistentContext ?? launchDarwinBackgroundPersistentContext;
|
|
32
|
+
this.activateBackgroundContext = opts.activateBackgroundContext ?? activateDarwinBackgroundContext;
|
|
33
|
+
this.platform = opts.platform ?? process.platform;
|
|
27
34
|
this.recoverLockedProfile = opts.recoverLockedProfile ?? recoverLockedCloakProfile;
|
|
28
35
|
}
|
|
29
36
|
profileStatuses() {
|
|
@@ -43,7 +50,7 @@ export class CloakSessionManager {
|
|
|
43
50
|
const session = requireSession(input.session);
|
|
44
51
|
const surface = normalizeSurface(input.surface);
|
|
45
52
|
const leaseKey = resolveLeaseKey(input);
|
|
46
|
-
const runtime = await this.getProfileRuntime(profileId);
|
|
53
|
+
const runtime = await this.getProfileRuntime(profileId, input.windowMode);
|
|
47
54
|
const freshPage = input.freshPage === true;
|
|
48
55
|
const existing = runtime.pages.get(leaseKey);
|
|
49
56
|
if (existing && !pageIsClosed(existing.page) && !freshPage) {
|
|
@@ -130,7 +137,7 @@ export class CloakSessionManager {
|
|
|
130
137
|
const profileId = normalizeProfileId(input.profileId);
|
|
131
138
|
const session = requireSession(input.session);
|
|
132
139
|
const surface = normalizeSurface(input.surface);
|
|
133
|
-
const runtime = await this.getProfileRuntime(profileId);
|
|
140
|
+
const runtime = await this.getProfileRuntime(profileId, input.windowMode);
|
|
134
141
|
const page = await runtime.context.newPage();
|
|
135
142
|
if (input.url) {
|
|
136
143
|
await page.goto(input.url, { waitUntil: 'load' });
|
|
@@ -152,7 +159,10 @@ export class CloakSessionManager {
|
|
|
152
159
|
if (!match)
|
|
153
160
|
return null;
|
|
154
161
|
const [leaseKey, entry] = match;
|
|
155
|
-
|
|
162
|
+
if (input.windowMode !== 'background') {
|
|
163
|
+
await entry.page.bringToFront?.().catch(() => { });
|
|
164
|
+
await this.activateBackgroundContext(runtime.context);
|
|
165
|
+
}
|
|
156
166
|
runtime.selectedPageId = entry.pageId;
|
|
157
167
|
runtime.lastSeenAt = Date.now();
|
|
158
168
|
return { profileId, leaseKey, context: runtime.context, page: entry.page, pageId: entry.pageId };
|
|
@@ -183,7 +193,10 @@ export class CloakSessionManager {
|
|
|
183
193
|
entry.siteSession = input.siteSession;
|
|
184
194
|
entry.idleTimeout = input.idleTimeout;
|
|
185
195
|
runtime.pages.set(canonicalKey, entry);
|
|
186
|
-
|
|
196
|
+
if (input.windowMode !== 'background') {
|
|
197
|
+
await entry.page.bringToFront?.().catch(() => { });
|
|
198
|
+
await this.activateBackgroundContext(runtime.context);
|
|
199
|
+
}
|
|
187
200
|
this.refreshIdleTimer(runtime, canonicalKey, entry);
|
|
188
201
|
runtime.selectedPageId = entry.pageId;
|
|
189
202
|
runtime.lastSeenAt = Date.now();
|
|
@@ -235,14 +248,14 @@ export class CloakSessionManager {
|
|
|
235
248
|
}
|
|
236
249
|
this.profiles.clear();
|
|
237
250
|
}
|
|
238
|
-
async getProfileRuntime(profileId) {
|
|
251
|
+
async getProfileRuntime(profileId, windowMode) {
|
|
239
252
|
const existing = this.profiles.get(profileId);
|
|
240
253
|
if (existing)
|
|
241
254
|
return existing;
|
|
242
255
|
const pending = this.profileLaunches.get(profileId);
|
|
243
256
|
if (pending)
|
|
244
257
|
return pending;
|
|
245
|
-
const launch = this.launchProfileRuntime(profileId);
|
|
258
|
+
const launch = this.launchProfileRuntime(profileId, windowMode);
|
|
246
259
|
this.profileLaunches.set(profileId, launch);
|
|
247
260
|
try {
|
|
248
261
|
return await launch;
|
|
@@ -251,7 +264,7 @@ export class CloakSessionManager {
|
|
|
251
264
|
this.profileLaunches.delete(profileId);
|
|
252
265
|
}
|
|
253
266
|
}
|
|
254
|
-
async launchProfileRuntime(profileId) {
|
|
267
|
+
async launchProfileRuntime(profileId, windowMode) {
|
|
255
268
|
const userDataDir = resolveCloakProfileDir(profileId, { baseDir: this.opts.baseDir });
|
|
256
269
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
257
270
|
const launchOptions = {
|
|
@@ -259,14 +272,17 @@ export class CloakSessionManager {
|
|
|
259
272
|
headless: false,
|
|
260
273
|
humanize: true,
|
|
261
274
|
};
|
|
275
|
+
const launchPersistentContext = this.platform === 'darwin' && windowMode === 'background'
|
|
276
|
+
? this.launchBackgroundPersistentContext
|
|
277
|
+
: this.launchPersistentContext;
|
|
262
278
|
let context;
|
|
263
279
|
try {
|
|
264
|
-
context = await
|
|
280
|
+
context = await launchPersistentContext(launchOptions);
|
|
265
281
|
}
|
|
266
282
|
catch (err) {
|
|
267
283
|
if (!isProfileAlreadyInUseError(err) || !(await this.recoverLockedProfile(userDataDir)))
|
|
268
284
|
throw err;
|
|
269
|
-
context = await
|
|
285
|
+
context = await launchPersistentContext(launchOptions);
|
|
270
286
|
}
|
|
271
287
|
const runtime = { context, pages: new Map(), lastSeenAt: Date.now() };
|
|
272
288
|
this.profiles.set(profileId, runtime);
|
|
@@ -42,6 +42,42 @@ describe('CloakSessionManager', () => {
|
|
|
42
42
|
expect(launchPersistentContext).toHaveBeenCalledTimes(1);
|
|
43
43
|
expect(launchPersistentContext.mock.calls[0][0]).toMatchObject({ headless: false });
|
|
44
44
|
});
|
|
45
|
+
it.each([
|
|
46
|
+
{ platform: 'darwin', windowMode: 'background', backgroundCalls: 1, normalCalls: 0 },
|
|
47
|
+
{ platform: 'darwin', windowMode: 'foreground', backgroundCalls: 0, normalCalls: 1 },
|
|
48
|
+
{ platform: 'linux', windowMode: 'background', backgroundCalls: 0, normalCalls: 1 },
|
|
49
|
+
])('routes a cold $platform $windowMode launch', async ({ platform, windowMode, backgroundCalls, normalCalls }) => {
|
|
50
|
+
const launched = fakeContext();
|
|
51
|
+
const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
|
|
52
|
+
const launchBackgroundPersistentContext = vi.fn().mockResolvedValue(launched.context);
|
|
53
|
+
const manager = new CloakSessionManager({
|
|
54
|
+
baseDir: '/tmp/webcmd-test',
|
|
55
|
+
platform,
|
|
56
|
+
launchPersistentContext,
|
|
57
|
+
launchBackgroundPersistentContext,
|
|
58
|
+
});
|
|
59
|
+
await manager.getPage({ profileId: 'default', session: 'work', surface: 'browser', windowMode });
|
|
60
|
+
expect(launchBackgroundPersistentContext).toHaveBeenCalledTimes(backgroundCalls);
|
|
61
|
+
expect(launchPersistentContext).toHaveBeenCalledTimes(normalCalls);
|
|
62
|
+
});
|
|
63
|
+
it('reactivates a background-launched context for foreground tab selection', async () => {
|
|
64
|
+
const launched = fakeContext();
|
|
65
|
+
const activateBackgroundContext = vi.fn().mockResolvedValue(undefined);
|
|
66
|
+
const manager = new CloakSessionManager({
|
|
67
|
+
baseDir: '/tmp/webcmd-test',
|
|
68
|
+
platform: 'darwin',
|
|
69
|
+
launchBackgroundPersistentContext: vi.fn().mockResolvedValue(launched.context),
|
|
70
|
+
activateBackgroundContext,
|
|
71
|
+
});
|
|
72
|
+
const lease = await manager.getPage({
|
|
73
|
+
profileId: 'default',
|
|
74
|
+
session: 'work',
|
|
75
|
+
surface: 'browser',
|
|
76
|
+
windowMode: 'background',
|
|
77
|
+
});
|
|
78
|
+
await manager.selectPage({ profileId: 'default', pageId: lease.pageId, windowMode: 'foreground' });
|
|
79
|
+
expect(activateBackgroundContext).toHaveBeenCalledWith(launched.context);
|
|
80
|
+
});
|
|
45
81
|
it('coalesces concurrent persistent context launches for the same profile', async () => {
|
|
46
82
|
const launched = fakeContext();
|
|
47
83
|
let resolveLaunch;
|
|
@@ -77,6 +77,7 @@ export declare function resolveTargetJs(ref: string, opts?: ResolveOptions): str
|
|
|
77
77
|
*/
|
|
78
78
|
export declare function boundingRectResolvedJs(opts?: {
|
|
79
79
|
skipScroll?: boolean;
|
|
80
|
+
forClick?: boolean;
|
|
80
81
|
}): string;
|
|
81
82
|
/**
|
|
82
83
|
* Generate JS for click that uses the unified resolver.
|
|
@@ -282,18 +282,113 @@ export function resolveTargetJs(ref, opts = {}) {
|
|
|
282
282
|
*/
|
|
283
283
|
export function boundingRectResolvedJs(opts = {}) {
|
|
284
284
|
const shouldScroll = opts.skipScroll ? 'false' : 'true';
|
|
285
|
+
const forClick = opts.forClick ? 'true' : 'false';
|
|
285
286
|
return `
|
|
286
287
|
(() => {
|
|
287
288
|
const el = window.__resolved;
|
|
288
289
|
if (!el) throw new Error('No resolved element');
|
|
289
290
|
if (${shouldScroll}) el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
290
|
-
|
|
291
|
+
|
|
292
|
+
const FOR_CLICK = ${forClick};
|
|
293
|
+
// hover()/dblClick() want the plain element centre — the retarget + hit-test
|
|
294
|
+
// below are click-only so those actions keep their original behaviour.
|
|
295
|
+
if (!FOR_CLICK) {
|
|
296
|
+
const r0 = el.getBoundingClientRect();
|
|
297
|
+
return {
|
|
298
|
+
x: Math.round(r0.left + r0.width / 2),
|
|
299
|
+
y: Math.round(r0.top + r0.height / 2),
|
|
300
|
+
w: Math.round(r0.width),
|
|
301
|
+
h: Math.round(r0.height),
|
|
302
|
+
visible: Math.round(r0.width) > 0 && Math.round(r0.height) > 0,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Does this node OWN a click handler? Deliberately excludes cursor:pointer,
|
|
307
|
+
// which is an *inherited* CSS property — an <svg> icon inside a clickable
|
|
308
|
+
// <div> inherits the pointer cursor but owns no handler, so cursor can't
|
|
309
|
+
// decide whether a node is the real click target. See issue #2071.
|
|
310
|
+
const ownsClickHandler = (node) => {
|
|
311
|
+
if (!node || node.nodeType !== 1) return false;
|
|
312
|
+
const tag = node.tagName.toLowerCase();
|
|
313
|
+
if (['a','button','input','select','textarea','label','summary'].includes(tag)) return true;
|
|
314
|
+
const role = node.getAttribute && node.getAttribute('role');
|
|
315
|
+
if (role && ['button','link','menuitem','menuitemcheckbox','menuitemradio','tab','option','checkbox','radio','switch'].includes(role)) return true;
|
|
316
|
+
if (typeof node.onclick === 'function') return true;
|
|
317
|
+
if (node.hasAttribute && (node.hasAttribute('onclick') || node.hasAttribute('jsaction'))) return true;
|
|
318
|
+
try {
|
|
319
|
+
for (const k in node) {
|
|
320
|
+
if (k.charCodeAt(0) === 95 && (k.indexOf('__reactProps$') === 0 || k.indexOf('__reactEventHandlers$') === 0)) {
|
|
321
|
+
const p = node[k];
|
|
322
|
+
if (p && (p.onClick || p.onMouseDown || p.onMouseUp)) return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch (e) {}
|
|
326
|
+
return false;
|
|
327
|
+
};
|
|
328
|
+
// A retarget destination just needs to look clickable; here cursor:pointer
|
|
329
|
+
// IS a useful signal (the ancestor is where it was set).
|
|
330
|
+
const isClickableAncestor = (node) => {
|
|
331
|
+
if (ownsClickHandler(node)) return true;
|
|
332
|
+
try { return window.getComputedStyle(node).cursor === 'pointer'; } catch (e) { return false; }
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// #2071: if the resolved node owns no click handler but a nearby ancestor
|
|
336
|
+
// is clickable, aim at that ancestor so the handler fires.
|
|
337
|
+
let target = el;
|
|
338
|
+
let retargeted = false;
|
|
339
|
+
if (!ownsClickHandler(el)) {
|
|
340
|
+
let a = el.parentElement, hops = 0;
|
|
341
|
+
while (a && hops < 4) {
|
|
342
|
+
if (isClickableAncestor(a)) { target = a; retargeted = true; break; }
|
|
343
|
+
a = a.parentElement; hops++;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const rect = target.getBoundingClientRect();
|
|
291
348
|
const w = Math.round(rect.width);
|
|
292
349
|
const h = Math.round(rect.height);
|
|
293
|
-
|
|
294
|
-
|
|
350
|
+
let x = Math.round(rect.left + rect.width / 2);
|
|
351
|
+
let y = Math.round(rect.top + rect.height / 2);
|
|
295
352
|
const visible = w > 0 && h > 0;
|
|
296
|
-
|
|
353
|
+
|
|
354
|
+
// #2076: verify the click point actually lands on the target. A trusted
|
|
355
|
+
// CDP click hit-tests at (x,y) and delivers the event to whatever is
|
|
356
|
+
// topmost there — an overlay/sibling can silently swallow it. Classify:
|
|
357
|
+
// 'target' — the point is the target or a light-DOM descendant.
|
|
358
|
+
// 'ancestor' — the point is an ANCESTOR of the target. This is the
|
|
359
|
+
// open-shadow-DOM case (elementFromPoint returns the shadow
|
|
360
|
+
// *host*, not the shadow content) and the "point sits over
|
|
361
|
+
// the target's own wrapper background" case. In both a CDP
|
|
362
|
+
// click at (x,y) still reaches the target — CDP hit-testing
|
|
363
|
+
// pierces shadow roots, and light-DOM clicks bubble up — so
|
|
364
|
+
// it is trustworthy, unlike an unrelated overlay.
|
|
365
|
+
// 'other' — an unrelated element covers the point (the real overlay
|
|
366
|
+
// bug); the caller then dispatches a direct DOM click.
|
|
367
|
+
const hitClass = (px, py) => {
|
|
368
|
+
let at = null;
|
|
369
|
+
try { at = document.elementFromPoint(px, py); } catch (e) { return 'none'; }
|
|
370
|
+
if (!at) return 'none';
|
|
371
|
+
if (at === target || target.contains(at)) return 'target';
|
|
372
|
+
if (at.contains && at.contains(target)) return 'ancestor';
|
|
373
|
+
return 'other';
|
|
374
|
+
};
|
|
375
|
+
let hit = visible ? hitClass(x, y) : 'none';
|
|
376
|
+
if (visible && hit !== 'target' && hit !== 'ancestor') {
|
|
377
|
+
const cands = [
|
|
378
|
+
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.25],
|
|
379
|
+
[rect.left + rect.width * 0.25, rect.top + rect.height * 0.5],
|
|
380
|
+
[rect.left + rect.width * 0.75, rect.top + rect.height * 0.5],
|
|
381
|
+
[rect.left + rect.width * 0.5, rect.top + rect.height * 0.75],
|
|
382
|
+
[rect.left + 3, rect.top + 3],
|
|
383
|
+
[rect.right - 3, rect.bottom - 3],
|
|
384
|
+
];
|
|
385
|
+
for (const c of cands) {
|
|
386
|
+
const px = Math.round(c[0]), py = Math.round(c[1]);
|
|
387
|
+
const hc = hitClass(px, py);
|
|
388
|
+
if (hc === 'target' || hc === 'ancestor') { x = px; y = py; hit = hc; break; }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { x, y, w, h, visible, hit, retargeted };
|
|
297
392
|
})()
|
|
298
393
|
`;
|
|
299
394
|
}
|