@agentrhq/webcmd 0.3.0 → 0.3.2
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 +228 -24
- 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/producthunt/hot.js +1 -2
- package/clis/spotify/spotify.js +11 -11
- 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/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/session-manager.d.ts +7 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -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 +58 -106
- package/dist/src/cli.test.js +71 -1
- 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/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 +7 -2
- 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 +2 -7
- package/dist/src/hosted/args.js +2 -99
- package/dist/src/hosted/args.test.js +15 -1
- 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 +191 -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 +31 -2
- package/dist/src/hosted/client.js +441 -53
- package/dist/src/hosted/client.test.js +651 -32
- package/dist/src/hosted/config.d.ts +7 -2
- package/dist/src/hosted/config.js +26 -4
- package/dist/src/hosted/config.test.js +34 -1
- 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 +5 -0
- package/dist/src/hosted/manifest.js +21 -66
- package/dist/src/hosted/manifest.test.js +120 -2
- 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 +7 -0
- package/dist/src/hosted/runner.js +554 -253
- package/dist/src/hosted/runner.test.js +1471 -53
- package/dist/src/hosted/setup.d.ts +3 -2
- package/dist/src/hosted/setup.js +45 -15
- package/dist/src/hosted/setup.test.js +133 -6
- package/dist/src/hosted/types.d.ts +101 -23
- package/dist/src/main.js +120 -108
- 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.js +12 -4
- package/dist/src/release-notes.test.js +27 -15
- 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 +36759 -0
- package/package.json +6 -1
- package/scripts/check-hosted-contract.mjs +108 -0
- package/scripts/docs-sync-review.ts +332 -0
- package/scripts/sync-community-plugins.ts +14 -0
- package/skills/webcmd-autofix/SKILL.md +26 -37
- package/skills/webcmd-usage/SKILL.md +7 -0
|
@@ -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
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { BrowserContext, Page as PlaywrightPage } from 'playwright-core';
|
|
2
2
|
import { launchPersistentContext as cloakLaunchPersistentContext } from 'cloakbrowser';
|
|
3
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>;
|
|
@@ -35,13 +36,19 @@ export interface CloakTabInfo {
|
|
|
35
36
|
export interface CloakSessionManagerOptions {
|
|
36
37
|
baseDir?: string;
|
|
37
38
|
launchPersistentContext?: LaunchPersistentContext;
|
|
39
|
+
launchBackgroundPersistentContext?: LaunchPersistentContext;
|
|
40
|
+
activateBackgroundContext?: typeof activateDarwinBackgroundContext;
|
|
38
41
|
recoverLockedProfile?: RecoverLockedProfile;
|
|
42
|
+
platform?: NodeJS.Platform;
|
|
39
43
|
}
|
|
40
44
|
export declare function resolveLeaseKey(input: SessionKeyInput): string;
|
|
41
45
|
export declare class CloakSessionManager {
|
|
42
46
|
private readonly opts;
|
|
43
47
|
readonly networkCapture: CloakNetworkCapture;
|
|
44
48
|
private readonly launchPersistentContext;
|
|
49
|
+
private readonly launchBackgroundPersistentContext;
|
|
50
|
+
private readonly activateBackgroundContext;
|
|
51
|
+
private readonly platform;
|
|
45
52
|
private readonly recoverLockedProfile;
|
|
46
53
|
private readonly profiles;
|
|
47
54
|
private readonly profileLaunches;
|
|
@@ -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,8 +159,10 @@ export class CloakSessionManager {
|
|
|
152
159
|
if (!match)
|
|
153
160
|
return null;
|
|
154
161
|
const [leaseKey, entry] = match;
|
|
155
|
-
if (input.windowMode !== 'background')
|
|
162
|
+
if (input.windowMode !== 'background') {
|
|
156
163
|
await entry.page.bringToFront?.().catch(() => { });
|
|
164
|
+
await this.activateBackgroundContext(runtime.context);
|
|
165
|
+
}
|
|
157
166
|
runtime.selectedPageId = entry.pageId;
|
|
158
167
|
runtime.lastSeenAt = Date.now();
|
|
159
168
|
return { profileId, leaseKey, context: runtime.context, page: entry.page, pageId: entry.pageId };
|
|
@@ -184,8 +193,10 @@ export class CloakSessionManager {
|
|
|
184
193
|
entry.siteSession = input.siteSession;
|
|
185
194
|
entry.idleTimeout = input.idleTimeout;
|
|
186
195
|
runtime.pages.set(canonicalKey, entry);
|
|
187
|
-
if (input.windowMode !== 'background')
|
|
196
|
+
if (input.windowMode !== 'background') {
|
|
188
197
|
await entry.page.bringToFront?.().catch(() => { });
|
|
198
|
+
await this.activateBackgroundContext(runtime.context);
|
|
199
|
+
}
|
|
189
200
|
this.refreshIdleTimer(runtime, canonicalKey, entry);
|
|
190
201
|
runtime.selectedPageId = entry.pageId;
|
|
191
202
|
runtime.lastSeenAt = Date.now();
|
|
@@ -237,14 +248,14 @@ export class CloakSessionManager {
|
|
|
237
248
|
}
|
|
238
249
|
this.profiles.clear();
|
|
239
250
|
}
|
|
240
|
-
async getProfileRuntime(profileId) {
|
|
251
|
+
async getProfileRuntime(profileId, windowMode) {
|
|
241
252
|
const existing = this.profiles.get(profileId);
|
|
242
253
|
if (existing)
|
|
243
254
|
return existing;
|
|
244
255
|
const pending = this.profileLaunches.get(profileId);
|
|
245
256
|
if (pending)
|
|
246
257
|
return pending;
|
|
247
|
-
const launch = this.launchProfileRuntime(profileId);
|
|
258
|
+
const launch = this.launchProfileRuntime(profileId, windowMode);
|
|
248
259
|
this.profileLaunches.set(profileId, launch);
|
|
249
260
|
try {
|
|
250
261
|
return await launch;
|
|
@@ -253,7 +264,7 @@ export class CloakSessionManager {
|
|
|
253
264
|
this.profileLaunches.delete(profileId);
|
|
254
265
|
}
|
|
255
266
|
}
|
|
256
|
-
async launchProfileRuntime(profileId) {
|
|
267
|
+
async launchProfileRuntime(profileId, windowMode) {
|
|
257
268
|
const userDataDir = resolveCloakProfileDir(profileId, { baseDir: this.opts.baseDir });
|
|
258
269
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
259
270
|
const launchOptions = {
|
|
@@ -261,14 +272,17 @@ export class CloakSessionManager {
|
|
|
261
272
|
headless: false,
|
|
262
273
|
humanize: true,
|
|
263
274
|
};
|
|
275
|
+
const launchPersistentContext = this.platform === 'darwin' && windowMode === 'background'
|
|
276
|
+
? this.launchBackgroundPersistentContext
|
|
277
|
+
: this.launchPersistentContext;
|
|
264
278
|
let context;
|
|
265
279
|
try {
|
|
266
|
-
context = await
|
|
280
|
+
context = await launchPersistentContext(launchOptions);
|
|
267
281
|
}
|
|
268
282
|
catch (err) {
|
|
269
283
|
if (!isProfileAlreadyInUseError(err) || !(await this.recoverLockedProfile(userDataDir)))
|
|
270
284
|
throw err;
|
|
271
|
-
context = await
|
|
285
|
+
context = await launchPersistentContext(launchOptions);
|
|
272
286
|
}
|
|
273
287
|
const runtime = { context, pages: new Map(), lastSeenAt: Date.now() };
|
|
274
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
|
}
|
|
@@ -1,5 +1,145 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { resolveTargetJs } from './target-resolver.js';
|
|
2
|
+
import { boundingRectResolvedJs, resolveTargetJs } from './target-resolver.js';
|
|
3
|
+
/**
|
|
4
|
+
* Build a fake element usable by the generated boundingRectResolvedJs. Rects are
|
|
5
|
+
* {left,top,width,height}; getBoundingClientRect derives right/bottom.
|
|
6
|
+
*/
|
|
7
|
+
function makeEl(opts) {
|
|
8
|
+
const attrs = opts.attrs ?? {};
|
|
9
|
+
const el = {
|
|
10
|
+
nodeType: 1,
|
|
11
|
+
tagName: opts.tag.toUpperCase(),
|
|
12
|
+
_cursor: opts.cursor ?? 'auto',
|
|
13
|
+
onclick: opts.onclick ? () => { } : null,
|
|
14
|
+
parentElement: null,
|
|
15
|
+
children: [],
|
|
16
|
+
scrollIntoView() { },
|
|
17
|
+
getBoundingClientRect() {
|
|
18
|
+
const r = opts.rect;
|
|
19
|
+
return { left: r.left, top: r.top, width: r.width, height: r.height, right: r.left + r.width, bottom: r.top + r.height };
|
|
20
|
+
},
|
|
21
|
+
getAttribute(k) { return k === 'role' ? (opts.role ?? null) : (attrs[k] ?? null); },
|
|
22
|
+
hasAttribute(k) { return k === 'role' ? opts.role != null : k in attrs; },
|
|
23
|
+
contains(other) {
|
|
24
|
+
if (other === el)
|
|
25
|
+
return true;
|
|
26
|
+
for (const c of el.children) {
|
|
27
|
+
if (c === other || (c.contains && c.contains(other)))
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return false;
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return el;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve computed cursor with CSS inheritance, so the fake DOM matches the
|
|
36
|
+
* browser: a child with no own cursor inherits its ancestors' pointer cursor. */
|
|
37
|
+
function inheritedCursor(node) {
|
|
38
|
+
let n = node;
|
|
39
|
+
while (n) {
|
|
40
|
+
if (n._cursor && n._cursor !== 'auto')
|
|
41
|
+
return n._cursor;
|
|
42
|
+
n = n.parentElement;
|
|
43
|
+
}
|
|
44
|
+
return 'auto';
|
|
45
|
+
}
|
|
46
|
+
/** Run the generated boundingRectResolvedJs against a fake DOM. */
|
|
47
|
+
function runRect(resolved, elementAt) {
|
|
48
|
+
const fakeWindow = { __resolved: resolved, getComputedStyle: (n) => ({ cursor: inheritedCursor(n) }) };
|
|
49
|
+
const fakeDoc = { elementFromPoint: (x, y) => elementAt(x, y) };
|
|
50
|
+
const js = boundingRectResolvedJs({ skipScroll: true, forClick: true });
|
|
51
|
+
// js is an IIFE expression `(() => {...})()`; arrow captures window/document
|
|
52
|
+
// params. Wrap in parens so ASI doesn't turn `return\n(` into `return;`.
|
|
53
|
+
return new Function('window', 'document', 'return (' + js + ')')(fakeWindow, fakeDoc);
|
|
54
|
+
}
|
|
55
|
+
describe('boundingRectResolvedJs runtime behavior', () => {
|
|
56
|
+
it('reports hit=target and no retarget when the centre lands on the element', () => {
|
|
57
|
+
const btn = makeEl({ tag: 'button', rect: { left: 0, top: 0, width: 100, height: 40 } });
|
|
58
|
+
const out = runRect(btn, () => btn);
|
|
59
|
+
expect(out.visible).toBe(true);
|
|
60
|
+
expect(out.hit).toBe('target');
|
|
61
|
+
expect(out.retargeted).toBe(false);
|
|
62
|
+
expect(out.x).toBe(50);
|
|
63
|
+
expect(out.y).toBe(20);
|
|
64
|
+
});
|
|
65
|
+
it('retargets an <svg> icon to its clickable <div> ancestor (#2071)', () => {
|
|
66
|
+
const div = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 80, height: 80 }, cursor: 'pointer' });
|
|
67
|
+
const svg = makeEl({ tag: 'svg', rect: { left: 20, top: 20, width: 40, height: 40 } });
|
|
68
|
+
svg.parentElement = div;
|
|
69
|
+
div.children = [svg];
|
|
70
|
+
// elementFromPoint at the div centre returns the svg (topmost paint), which
|
|
71
|
+
// is contained by div → hit=target on the retargeted element.
|
|
72
|
+
const out = runRect(svg, () => svg);
|
|
73
|
+
expect(out.retargeted).toBe(true);
|
|
74
|
+
expect(out.hit).toBe('target');
|
|
75
|
+
// measured rect is the div's (80x80 → centre 40,40), not the svg's
|
|
76
|
+
expect(out.w).toBe(80);
|
|
77
|
+
expect(out.x).toBe(40);
|
|
78
|
+
expect(out.y).toBe(40);
|
|
79
|
+
});
|
|
80
|
+
it('retargets to an ancestor that owns an onclick handler even without cursor:pointer (#2071)', () => {
|
|
81
|
+
const div = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 60, height: 60 }, onclick: true });
|
|
82
|
+
const svg = makeEl({ tag: 'svg', rect: { left: 10, top: 10, width: 40, height: 40 } });
|
|
83
|
+
svg.parentElement = div;
|
|
84
|
+
div.children = [svg];
|
|
85
|
+
const out = runRect(svg, () => svg);
|
|
86
|
+
expect(out.retargeted).toBe(true);
|
|
87
|
+
expect(out.w).toBe(60);
|
|
88
|
+
});
|
|
89
|
+
it('does not retarget when no ancestor is clickable', () => {
|
|
90
|
+
const wrap = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 60, height: 60 } });
|
|
91
|
+
const span = makeEl({ tag: 'span', rect: { left: 10, top: 10, width: 40, height: 40 } });
|
|
92
|
+
span.parentElement = wrap;
|
|
93
|
+
wrap.children = [span];
|
|
94
|
+
const out = runRect(span, () => span);
|
|
95
|
+
expect(out.retargeted).toBe(false);
|
|
96
|
+
expect(out.w).toBe(40);
|
|
97
|
+
});
|
|
98
|
+
it('treats an ancestor hit as trustworthy (open shadow-DOM host / own wrapper) and keeps the centre', () => {
|
|
99
|
+
// elementFromPoint returns a light-DOM ancestor (e.g. the shadow host) that
|
|
100
|
+
// contains the target — CDP pierces to the target, so this must NOT fall back.
|
|
101
|
+
const host = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 120, height: 60 } });
|
|
102
|
+
const btn = makeEl({ tag: 'button', rect: { left: 10, top: 10, width: 100, height: 40 } });
|
|
103
|
+
btn.parentElement = host;
|
|
104
|
+
host.children = [btn];
|
|
105
|
+
const out = runRect(btn, () => host); // topmost at every point is the host (ancestor)
|
|
106
|
+
expect(out.hit).toBe('ancestor');
|
|
107
|
+
expect(out.retargeted).toBe(false);
|
|
108
|
+
// centre kept — no wasted probing when the hit is already trustworthy
|
|
109
|
+
expect(out.x).toBe(60);
|
|
110
|
+
expect(out.y).toBe(30);
|
|
111
|
+
});
|
|
112
|
+
it('hover/dblClick mode (forClick omitted) returns the plain element centre with no hit-test', () => {
|
|
113
|
+
const div = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 80, height: 80 }, cursor: 'pointer' });
|
|
114
|
+
const svg = makeEl({ tag: 'svg', rect: { left: 20, top: 20, width: 40, height: 40 } });
|
|
115
|
+
svg.parentElement = div;
|
|
116
|
+
div.children = [svg];
|
|
117
|
+
const js = boundingRectResolvedJs({ skipScroll: true }); // no forClick
|
|
118
|
+
const out = new Function('window', 'document', 'return (' + js + ')')({ __resolved: svg, getComputedStyle: () => ({ cursor: 'pointer' }) }, { elementFromPoint: () => div });
|
|
119
|
+
// no retarget, no hit field — svg's own centre (40,40), unchanged behavior
|
|
120
|
+
expect(out.hit).toBeUndefined();
|
|
121
|
+
expect(out.retargeted).toBeUndefined();
|
|
122
|
+
expect(out.x).toBe(40);
|
|
123
|
+
expect(out.y).toBe(40);
|
|
124
|
+
expect(out.w).toBe(40);
|
|
125
|
+
});
|
|
126
|
+
it('reports hit=other when an overlay covers the element and no probe point lands (#2076)', () => {
|
|
127
|
+
const btn = makeEl({ tag: 'button', rect: { left: 0, top: 0, width: 100, height: 40 } });
|
|
128
|
+
const overlay = makeEl({ tag: 'div', rect: { left: 0, top: 0, width: 1000, height: 1000 } });
|
|
129
|
+
const out = runRect(btn, () => overlay); // every point hits the overlay
|
|
130
|
+
expect(out.hit).toBe('other');
|
|
131
|
+
expect(out.retargeted).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
it('recovers a hitting point by probing when the centre is occluded but a corner is clear (#2076)', () => {
|
|
134
|
+
const btn = makeEl({ tag: 'button', rect: { left: 0, top: 0, width: 100, height: 40 } });
|
|
135
|
+
const overlay = makeEl({ tag: 'div', rect: { left: 40, top: 15, width: 20, height: 10 } });
|
|
136
|
+
// Centre (50,20) is covered by the overlay; the top-left inset (3,3) is clear.
|
|
137
|
+
const out = runRect(btn, (x, y) => (x === 50 && y === 20 ? overlay : btn));
|
|
138
|
+
expect(out.hit).toBe('target');
|
|
139
|
+
// the returned point moved off the occluded centre (50,20)
|
|
140
|
+
expect(out.x === 50 && out.y === 20).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
3
143
|
/**
|
|
4
144
|
* Tests for the target resolver JS generator.
|
|
5
145
|
*
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* amount) to confirm an intentional removal.
|
|
23
23
|
*/
|
|
24
24
|
import type { ManifestEntry } from './manifest-types.js';
|
|
25
|
+
import { type HostedBrowserCommandContract } from './hosted/contract.js';
|
|
25
26
|
export type { ManifestEntry } from './manifest-types.js';
|
|
26
27
|
/**
|
|
27
28
|
* Thrown by `loadManifestEntries` when an adapter file looks like a CLI
|
|
@@ -66,6 +67,11 @@ export declare function loadManifestEntries(filePath: string, site: string, impo
|
|
|
66
67
|
export declare function scanClisDir(clisDir: string, importer?: (moduleHref: string) => Promise<unknown>): Promise<BuildManifestResult>;
|
|
67
68
|
export declare function buildManifest(): Promise<BuildManifestResult>;
|
|
68
69
|
export declare function serializeManifest(manifest: ManifestEntry[]): string;
|
|
70
|
+
export interface BuildManifestArtifacts {
|
|
71
|
+
manifestJson: string;
|
|
72
|
+
hostedContractJson: string;
|
|
73
|
+
}
|
|
74
|
+
export declare function buildManifestArtifacts(entries: ManifestEntry[], packageVersion: string, browserCatalog: readonly HostedBrowserCommandContract[]): BuildManifestArtifacts;
|
|
69
75
|
/**
|
|
70
76
|
* Metadata audit: every positional arg must carry a non-empty `help` string.
|
|
71
77
|
*
|
|
@@ -28,10 +28,12 @@ import { getErrorMessage } from './errors.js';
|
|
|
28
28
|
import { fullName, getRegistry } from './registry.js';
|
|
29
29
|
import { findPackageRoot, getCliManifestPath } from './package-paths.js';
|
|
30
30
|
import { isRecord } from './utils.js';
|
|
31
|
+
import { buildHostedContract, serializeHostedContract, } from './hosted/contract.js';
|
|
31
32
|
const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
|
|
32
33
|
const CLIS_DIR = path.join(PACKAGE_ROOT, 'clis');
|
|
33
34
|
// Write manifest next to clis/ so both dev and installed runtime can find it.
|
|
34
35
|
const OUTPUT = getCliManifestPath(CLIS_DIR);
|
|
36
|
+
const HOSTED_CONTRACT_OUTPUT = path.join(PACKAGE_ROOT, 'hosted-contract.json');
|
|
35
37
|
// Module is treated as a CLI command source if it either:
|
|
36
38
|
// 1. Calls `cli(...)` directly (the common case), or
|
|
37
39
|
// 2. Calls a factory `make<Pascal>Command(...)` from clis/_shared/ that
|
|
@@ -64,6 +66,7 @@ function toManifestArgs(args) {
|
|
|
64
66
|
positional: arg.positional || undefined,
|
|
65
67
|
help: arg.help ?? '',
|
|
66
68
|
choices: arg.choices,
|
|
69
|
+
file: arg.file,
|
|
67
70
|
}));
|
|
68
71
|
}
|
|
69
72
|
function toModulePath(filePath, site) {
|
|
@@ -211,6 +214,12 @@ export async function buildManifest() {
|
|
|
211
214
|
export function serializeManifest(manifest) {
|
|
212
215
|
return `${JSON.stringify(manifest, null, 2)}\n`;
|
|
213
216
|
}
|
|
217
|
+
export function buildManifestArtifacts(entries, packageVersion, browserCatalog) {
|
|
218
|
+
return {
|
|
219
|
+
manifestJson: serializeManifest(entries),
|
|
220
|
+
hostedContractJson: serializeHostedContract(buildHostedContract(entries, browserCatalog, packageVersion)),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
214
223
|
export function findManifestMetadataIssues(entries) {
|
|
215
224
|
const issues = [];
|
|
216
225
|
for (const entry of entries) {
|
|
@@ -330,9 +339,17 @@ async function main() {
|
|
|
330
339
|
process.exit(1);
|
|
331
340
|
}
|
|
332
341
|
}
|
|
342
|
+
const packagePath = path.resolve(PACKAGE_ROOT, 'package.json');
|
|
343
|
+
const packageMetadata = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
344
|
+
if (typeof packageMetadata.name !== 'string' || typeof packageMetadata.version !== 'string') {
|
|
345
|
+
throw new Error(`Package name and version are required in ${packagePath}`);
|
|
346
|
+
}
|
|
347
|
+
const artifacts = buildManifestArtifacts(entries, packageMetadata.version, []);
|
|
333
348
|
fs.mkdirSync(path.dirname(OUTPUT), { recursive: true });
|
|
334
|
-
fs.writeFileSync(OUTPUT,
|
|
349
|
+
fs.writeFileSync(OUTPUT, artifacts.manifestJson);
|
|
350
|
+
fs.writeFileSync(HOSTED_CONTRACT_OUTPUT, artifacts.hostedContractJson);
|
|
335
351
|
console.log(`✅ Manifest compiled: ${entries.length} entries → ${OUTPUT}`);
|
|
352
|
+
console.log(`✅ Hosted contract compiled: ${packageMetadata.name}@${packageMetadata.version} → ${HOSTED_CONTRACT_OUTPUT}`);
|
|
336
353
|
// Restore executable permissions on bin entries.
|
|
337
354
|
// tsc does not preserve the +x bit, so after a clean rebuild the CLI
|
|
338
355
|
// entry-point loses its executable permission, causing "Permission denied".
|