@agentrhq/webcmd 0.2.0 → 0.2.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 +58 -31
- package/cli-manifest.json +506 -0
- package/clis/_shared/site-auth.js +6 -1
- package/clis/district/_lib.js +566 -0
- package/clis/district/auth.js +49 -0
- package/clis/district/checkout.js +278 -0
- package/clis/district/listings.js +158 -0
- package/clis/district/locations.js +211 -0
- package/clis/district/search.js +218 -0
- package/clis/district/seats.js +233 -0
- package/clis/district/set-location.js +82 -0
- package/clis/district/showtimes.js +433 -0
- package/clis/reddit/popular.js +12 -1
- package/clis/reddit/popular.test.js +12 -3
- 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 +2 -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 +5 -1
- package/dist/src/browser/page.js +16 -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 +3 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +25 -10
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +15 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +145 -0
- package/dist/src/build-manifest.js +1 -0
- package/dist/src/build-manifest.test.js +34 -0
- package/dist/src/cli.js +133 -45
- 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/discovery.js +1 -0
- package/dist/src/doctor.js +12 -0
- package/dist/src/doctor.test.js +30 -3
- package/dist/src/engine.test.js +62 -0
- 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/manifest-types.d.ts +2 -0
- package/dist/src/node-network.test.js +3 -3
- package/dist/src/registry.d.ts +10 -0
- package/dist/src/registry.js +5 -3
- package/dist/src/registry.test.js +25 -0
- package/dist/src/runtime-identity.test.js +0 -8
- package/dist/src/runtime.d.ts +4 -0
- package/dist/src/runtime.js +2 -0
- package/dist/src/skills.d.ts +23 -5
- package/dist/src/skills.js +87 -45
- package/dist/src/skills.test.js +80 -23
- package/package.json +3 -3
- package/skills/smart-search/SKILL.md +156 -0
- package/skills/smart-search/references/sources-ai.md +74 -0
- package/skills/smart-search/references/sources-info.md +43 -0
- package/skills/smart-search/references/sources-media.md +40 -0
- package/skills/smart-search/references/sources-other.md +32 -0
- package/skills/smart-search/references/sources-shopping.md +21 -0
- package/skills/smart-search/references/sources-social.md +36 -0
- package/skills/smart-search/references/sources-tech.md +38 -0
- package/skills/smart-search/references/sources-travel.md +26 -0
- package/skills/webcmd-adapter-author/SKILL.md +1 -0
- package/skills/webcmd-adapter-author/references/adapter-template.md +2 -0
- package/skills/webcmd-autofix/SKILL.md +8 -0
- package/skills/webcmd-sitemap-author/SKILL.md +1 -1
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/discovery.js
CHANGED
|
@@ -211,6 +211,7 @@ async function loadFromManifest(manifestPath, clisDir) {
|
|
|
211
211
|
source: entry.sourceFile ? path.resolve(clisDir, entry.sourceFile) : modulePath,
|
|
212
212
|
navigateBefore: entry.navigateBefore,
|
|
213
213
|
siteSession: entry.siteSession,
|
|
214
|
+
freshPage: entry.freshPage,
|
|
214
215
|
defaultWindowMode: entry.defaultWindowMode,
|
|
215
216
|
_lazy: true,
|
|
216
217
|
_modulePath: modulePath,
|
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();
|
package/dist/src/engine.test.js
CHANGED
|
@@ -3,6 +3,7 @@ import { discoverClis, discoverPlugins, ensureUserCliCompatShims, ensureUserAdap
|
|
|
3
3
|
import { executeCommand } from './execution.js';
|
|
4
4
|
import { getRegistry, cli, Strategy } from './registry.js';
|
|
5
5
|
import { clearAllHooks, onAfterExecute } from './hooks.js';
|
|
6
|
+
import * as runtime from './runtime.js';
|
|
6
7
|
import * as fs from 'node:fs';
|
|
7
8
|
import * as os from 'node:os';
|
|
8
9
|
import * as path from 'node:path';
|
|
@@ -71,6 +72,67 @@ cli({
|
|
|
71
72
|
await fs.promises.rm(tempBuildRoot, { recursive: true, force: true });
|
|
72
73
|
}
|
|
73
74
|
});
|
|
75
|
+
it('preserves freshPage from manifest before lazy command import', async () => {
|
|
76
|
+
const site = `manifest-fresh-${Date.now()}`;
|
|
77
|
+
const tempBuildRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'webcmd-manifest-fresh-'));
|
|
78
|
+
const distDir = path.join(tempBuildRoot, 'dist');
|
|
79
|
+
const siteDir = path.join(distDir, site);
|
|
80
|
+
const commandPath = path.join(siteDir, 'checkout.js');
|
|
81
|
+
const manifestPath = path.join(tempBuildRoot, 'cli-manifest.json');
|
|
82
|
+
const sessionOpts = [];
|
|
83
|
+
const mockPage = { closeWindow: vi.fn().mockResolvedValue(undefined) };
|
|
84
|
+
try {
|
|
85
|
+
await fs.promises.mkdir(siteDir, { recursive: true });
|
|
86
|
+
await fs.promises.writeFile(manifestPath, `${JSON.stringify([
|
|
87
|
+
{
|
|
88
|
+
site,
|
|
89
|
+
name: 'checkout',
|
|
90
|
+
description: 'fresh checkout',
|
|
91
|
+
access: 'write',
|
|
92
|
+
strategy: 'public',
|
|
93
|
+
browser: true,
|
|
94
|
+
args: [],
|
|
95
|
+
type: 'js',
|
|
96
|
+
modulePath: `${site}/checkout.js`,
|
|
97
|
+
sourceFile: `${site}/checkout.js`,
|
|
98
|
+
siteSession: 'persistent',
|
|
99
|
+
freshPage: true,
|
|
100
|
+
},
|
|
101
|
+
], null, 2)}\n`);
|
|
102
|
+
await fs.promises.writeFile(commandPath, `
|
|
103
|
+
import { cli, Strategy } from '${pathToFileURL(path.join(process.cwd(), 'src', 'registry.ts')).href}';
|
|
104
|
+
cli({
|
|
105
|
+
site: '${site}',
|
|
106
|
+
name: 'checkout', access: 'write',
|
|
107
|
+
description: 'fresh checkout',
|
|
108
|
+
browser: true,
|
|
109
|
+
strategy: Strategy.PUBLIC,
|
|
110
|
+
siteSession: 'persistent',
|
|
111
|
+
freshPage: true,
|
|
112
|
+
func: async () => [{ ok: true }],
|
|
113
|
+
});
|
|
114
|
+
`);
|
|
115
|
+
await discoverClis(distDir);
|
|
116
|
+
const cmd = getRegistry().get(`${site}/checkout`);
|
|
117
|
+
expect(cmd?.freshPage).toBe(true);
|
|
118
|
+
vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
|
|
119
|
+
sessionOpts.push(opts ?? {});
|
|
120
|
+
return fn(mockPage);
|
|
121
|
+
});
|
|
122
|
+
await expect(executeCommand(cmd, {})).resolves.toEqual([{ ok: true }]);
|
|
123
|
+
expect(sessionOpts).toHaveLength(1);
|
|
124
|
+
expect(sessionOpts[0]).toMatchObject({
|
|
125
|
+
session: `site:${site}`,
|
|
126
|
+
siteSession: 'persistent',
|
|
127
|
+
freshPage: true,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
vi.restoreAllMocks();
|
|
132
|
+
getRegistry().delete(`${site}/checkout`);
|
|
133
|
+
await fs.promises.rm(tempBuildRoot, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
74
136
|
it('loads user CLI modules via package exports symlink', async () => {
|
|
75
137
|
const tempWebcmdRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'webcmd-user-clis-'));
|
|
76
138
|
const userClisDir = path.join(tempWebcmdRoot, 'clis');
|
package/dist/src/execution.js
CHANGED
|
@@ -18,7 +18,7 @@ import { executePipeline } from './pipeline/index.js';
|
|
|
18
18
|
import { adapterLoadError, ArgumentError, CommandExecutionError, attachTraceReceipt, getErrorMessage } from './errors.js';
|
|
19
19
|
import { shouldUseBrowserSession } from './capabilityRouting.js';
|
|
20
20
|
import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
|
|
21
|
-
import {
|
|
21
|
+
import { profileRouteParams, resolveProfileSelection } from './browser/profile.js';
|
|
22
22
|
import { setDaemonCommandTimeoutSeconds } from './browser/daemon-client.js';
|
|
23
23
|
import { emitHook } from './hooks.js';
|
|
24
24
|
import { log } from './logger.js';
|
|
@@ -211,7 +211,9 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
const BrowserFactory = getBrowserFactory(cmd.site);
|
|
214
|
-
const
|
|
214
|
+
const profileSelection = resolveProfileSelection(opts.profile);
|
|
215
|
+
const profileRouting = profileRouteParams(profileSelection);
|
|
216
|
+
const contextId = profileSelection?.contextId;
|
|
215
217
|
const internal = cmd;
|
|
216
218
|
const siteSession = resolveSiteSession(cmd, opts.siteSession);
|
|
217
219
|
const session = resolveAdapterBrowserSession(cmd, siteSession);
|
|
@@ -332,7 +334,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
332
334
|
await page.closeWindow?.().catch(() => { });
|
|
333
335
|
throw err;
|
|
334
336
|
}
|
|
335
|
-
}, { session, cdpEndpoint,
|
|
337
|
+
}, { session, cdpEndpoint, ...profileRouting, windowMode, surface: 'adapter', siteSession, freshPage: cmd.freshPage === true && siteSession === 'persistent' });
|
|
336
338
|
}
|
|
337
339
|
else {
|
|
338
340
|
// Non-browser commands: enforce a timeout only when the command exposes
|
package/dist/src/external.js
CHANGED
|
@@ -161,16 +161,34 @@ export function executeExternalCli(name, args, preloaded) {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
// 3. Passthrough execution with stdio inherited
|
|
164
|
-
const result =
|
|
164
|
+
const result = spawnPassthrough(cli.binary, args);
|
|
165
165
|
if (result.error) {
|
|
166
166
|
log.error(`Failed to execute '${cli.binary}': ${result.error.message}`);
|
|
167
167
|
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
168
168
|
return;
|
|
169
169
|
}
|
|
170
|
+
if (result.signal) {
|
|
171
|
+
process.exitCode = EXIT_CODES.GENERIC_ERROR;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
170
174
|
if (result.status !== null) {
|
|
171
175
|
process.exitCode = result.status;
|
|
172
176
|
}
|
|
173
177
|
}
|
|
178
|
+
function quoteForCmdShell(token) {
|
|
179
|
+
if (token !== '' && !/[\s"^&|<>%()]/.test(token))
|
|
180
|
+
return token;
|
|
181
|
+
return `"${token.replace(/"/g, '""')}"`;
|
|
182
|
+
}
|
|
183
|
+
function spawnPassthrough(binary, args) {
|
|
184
|
+
const direct = spawnSync(binary, args, { stdio: 'inherit' });
|
|
185
|
+
const errorCode = direct.error?.code;
|
|
186
|
+
if (os.platform() === 'win32' && (errorCode === 'EINVAL' || errorCode === 'ENOENT')) {
|
|
187
|
+
const command = [binary, ...args].map(quoteForCmdShell).join(' ');
|
|
188
|
+
return spawnSync(command, { stdio: 'inherit', shell: true });
|
|
189
|
+
}
|
|
190
|
+
return direct;
|
|
191
|
+
}
|
|
174
192
|
export function registerExternalCli(name, opts) {
|
|
175
193
|
const userPath = getUserRegistryPath();
|
|
176
194
|
const configDir = path.dirname(userPath);
|
|
@@ -18,7 +18,8 @@ vi.mock('node:os', async () => {
|
|
|
18
18
|
platform: mockPlatform,
|
|
19
19
|
};
|
|
20
20
|
});
|
|
21
|
-
import {
|
|
21
|
+
import { spawnSync } from 'node:child_process';
|
|
22
|
+
import { executeExternalCli, formatExternalCliLabel, installExternalCli, parseCommand } from './external.js';
|
|
22
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
describe('parseCommand', () => {
|
|
24
25
|
it('splits binaries and quoted arguments without invoking a shell', () => {
|
|
@@ -93,3 +94,38 @@ describe('installExternalCli', () => {
|
|
|
93
94
|
expect(mockExecFileSync).toHaveBeenCalledTimes(1);
|
|
94
95
|
});
|
|
95
96
|
});
|
|
97
|
+
describe('executeExternalCli passthrough', () => {
|
|
98
|
+
const spawnMock = vi.mocked(spawnSync);
|
|
99
|
+
const cli = { name: 'tg', binary: 'tg', description: '' };
|
|
100
|
+
beforeEach(() => {
|
|
101
|
+
spawnMock.mockReset();
|
|
102
|
+
mockExecFileSync.mockReset();
|
|
103
|
+
mockExecFileSync.mockReturnValue(Buffer.from(''));
|
|
104
|
+
mockPlatform.mockReturnValue('darwin');
|
|
105
|
+
process.exitCode = undefined;
|
|
106
|
+
});
|
|
107
|
+
it('retries through the shell on Windows when the binary is a .cmd shim', () => {
|
|
108
|
+
mockPlatform.mockReturnValue('win32');
|
|
109
|
+
const einval = Object.assign(new Error('spawnSync tg EINVAL'), { code: 'EINVAL' });
|
|
110
|
+
spawnMock
|
|
111
|
+
.mockReturnValueOnce({ error: einval, status: null, signal: null })
|
|
112
|
+
.mockReturnValueOnce({ status: 0, signal: null });
|
|
113
|
+
executeExternalCli('tg', ['send', 'hello world', '--to', 'a"b'], [cli]);
|
|
114
|
+
expect(spawnMock).toHaveBeenCalledTimes(2);
|
|
115
|
+
expect(spawnMock).toHaveBeenNthCalledWith(1, 'tg', ['send', 'hello world', '--to', 'a"b'], { stdio: 'inherit' });
|
|
116
|
+
expect(spawnMock).toHaveBeenNthCalledWith(2, 'tg send "hello world" --to "a""b"', { stdio: 'inherit', shell: true });
|
|
117
|
+
expect(process.exitCode).toBe(0);
|
|
118
|
+
});
|
|
119
|
+
it('does not retry through the shell on non-Windows platforms', () => {
|
|
120
|
+
const einval = Object.assign(new Error('spawnSync tg EINVAL'), { code: 'EINVAL' });
|
|
121
|
+
spawnMock.mockReturnValueOnce({ error: einval, status: null, signal: null });
|
|
122
|
+
executeExternalCli('tg', [], [cli]);
|
|
123
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
124
|
+
expect(process.exitCode).toBe(1);
|
|
125
|
+
});
|
|
126
|
+
it('reports a non-zero exit code when the child dies from a signal', () => {
|
|
127
|
+
spawnMock.mockReturnValueOnce({ status: null, signal: 'SIGKILL' });
|
|
128
|
+
executeExternalCli('tg', [], [cli]);
|
|
129
|
+
expect(process.exitCode).toBe(1);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/dist/src/main.js
CHANGED
|
@@ -21,7 +21,6 @@ import { findPackageRoot, getCliManifestPath } from './package-paths.js';
|
|
|
21
21
|
import { PKG_VERSION } from './version.js';
|
|
22
22
|
import { EXIT_CODES } from './errors.js';
|
|
23
23
|
import { isSupportedNodeVersion, MIN_SUPPORTED_NODE_MAJOR } from './runtime-detect.js';
|
|
24
|
-
import { unsupportedDaemonPortEnvMessage } from './constants.js';
|
|
25
24
|
import { CONFIG_DIR_NAME } from './brand.js';
|
|
26
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
27
26
|
const __dirname = path.dirname(__filename);
|
|
@@ -41,10 +40,6 @@ if (typeof globalThis.Bun === 'undefined' && !isSupportedNodeVersion(process.ver
|
|
|
41
40
|
].join('\n'));
|
|
42
41
|
process.exit(EXIT_CODES.CONFIG_ERROR);
|
|
43
42
|
}
|
|
44
|
-
if (process.env.WEBCMD_DAEMON_PORT) {
|
|
45
|
-
process.stderr.write(`error: ${unsupportedDaemonPortEnvMessage(process.env.WEBCMD_DAEMON_PORT)}\n`);
|
|
46
|
-
process.exit(EXIT_CODES.CONFIG_ERROR);
|
|
47
|
-
}
|
|
48
43
|
// Fast path: --version (only when it's the top-level intent, not passed to a subcommand)
|
|
49
44
|
// e.g. `webcmd --version` or `webcmd -V`, but NOT `webcmd gh --version`
|
|
50
45
|
if (argv[0] === '--version' || argv[0] === '-V') {
|
|
@@ -38,6 +38,8 @@ export interface ManifestEntry {
|
|
|
38
38
|
navigateBefore?: boolean | string;
|
|
39
39
|
/** Site session lifecycle defaults — see CliCommand.siteSession */
|
|
40
40
|
siteSession?: 'ephemeral' | 'persistent';
|
|
41
|
+
/** Fresh page behavior for persistent site sessions — see CliCommand.freshPage */
|
|
42
|
+
freshPage?: boolean;
|
|
41
43
|
/** Default browser window visibility — see CliCommand.defaultWindowMode */
|
|
42
44
|
defaultWindowMode?: 'foreground' | 'background';
|
|
43
45
|
}
|
|
@@ -22,9 +22,9 @@ describe('node network proxy decisions', () => {
|
|
|
22
22
|
});
|
|
23
23
|
it('bypasses proxies for loopback addresses', () => {
|
|
24
24
|
const env = { https_proxy: 'http://127.0.0.1:7897', http_proxy: 'http://127.0.0.1:7897' };
|
|
25
|
-
expect(decideProxy(new URL('http://127.0.0.1:
|
|
26
|
-
expect(decideProxy(new URL('http://localhost:
|
|
27
|
-
expect(decideProxy(new URL('http://[::1]:
|
|
25
|
+
expect(decideProxy(new URL('http://127.0.0.1:9777/status'), env)).toEqual({ mode: 'direct' });
|
|
26
|
+
expect(decideProxy(new URL('http://localhost:9777/status'), env)).toEqual({ mode: 'direct' });
|
|
27
|
+
expect(decideProxy(new URL('http://[::1]:9777/status'), env)).toEqual({ mode: 'direct' });
|
|
28
28
|
});
|
|
29
29
|
it('honors NO_PROXY domain matches', () => {
|
|
30
30
|
const decision = decideProxy(new URL('https://api.example.com/v1/items'), {
|
package/dist/src/registry.d.ts
CHANGED
|
@@ -63,6 +63,16 @@ interface BaseCliCommand {
|
|
|
63
63
|
navigateBefore?: boolean | string;
|
|
64
64
|
/** Site session lifecycle for adapter commands. */
|
|
65
65
|
siteSession?: SiteSessionMode;
|
|
66
|
+
/**
|
|
67
|
+
* Start this command on a freshly created page even when the persistent
|
|
68
|
+
* site session already has a leased tab. The old tab is closed and a new
|
|
69
|
+
* one opened under the same lease, so profile state (cookies, localStorage,
|
|
70
|
+
* login, location) survives while stale DOM (leftover modals, drawers,
|
|
71
|
+
* half-finished flows from earlier commands) is discarded. Requires
|
|
72
|
+
* `siteSession: 'persistent'` — ephemeral sessions always start fresh, so
|
|
73
|
+
* combining them is a contradiction and fails at registration.
|
|
74
|
+
*/
|
|
75
|
+
freshPage?: boolean;
|
|
66
76
|
/** Default browser window mode for commands whose UX requires visibility. */
|
|
67
77
|
defaultWindowMode?: 'foreground' | 'background';
|
|
68
78
|
/** Override the default CLI output format when the user does not pass -f/--format. */
|
package/dist/src/registry.js
CHANGED
|
@@ -29,6 +29,7 @@ export function cli(opts) {
|
|
|
29
29
|
validateArgs: opts.validateArgs,
|
|
30
30
|
navigateBefore: opts.navigateBefore,
|
|
31
31
|
siteSession: opts.siteSession,
|
|
32
|
+
freshPage: opts.freshPage,
|
|
32
33
|
defaultWindowMode: opts.defaultWindowMode,
|
|
33
34
|
defaultFormat: opts.defaultFormat,
|
|
34
35
|
authStatus: opts.authStatus,
|
|
@@ -86,12 +87,13 @@ function assertCommandAccess(cmd) {
|
|
|
86
87
|
throw new Error(`Command ${key} must declare access: 'read' | 'write'`);
|
|
87
88
|
}
|
|
88
89
|
function assertSiteSession(cmd) {
|
|
89
|
-
if (cmd.siteSession === undefined)
|
|
90
|
-
return;
|
|
91
90
|
const key = `${cmd.site}/${cmd.name}`;
|
|
92
|
-
if (cmd.siteSession !== 'ephemeral' && cmd.siteSession !== 'persistent') {
|
|
91
|
+
if (cmd.siteSession !== undefined && cmd.siteSession !== 'ephemeral' && cmd.siteSession !== 'persistent') {
|
|
93
92
|
throw new Error(`Command ${key} siteSession must be one of: ephemeral, persistent`);
|
|
94
93
|
}
|
|
94
|
+
if (cmd.freshPage === true && cmd.siteSession !== 'persistent') {
|
|
95
|
+
throw new Error(`Command ${key} freshPage requires siteSession: 'persistent' — ephemeral sessions already start on a fresh page`);
|
|
96
|
+
}
|
|
95
97
|
}
|
|
96
98
|
export function registerCommand(cmd) {
|
|
97
99
|
const normalized = normalizeCommand(cmd);
|
|
@@ -19,6 +19,31 @@ describe('cli() registration', () => {
|
|
|
19
19
|
expect(cmd.browser).toBe(false);
|
|
20
20
|
expect(cmd.args).toEqual([]);
|
|
21
21
|
});
|
|
22
|
+
it('accepts freshPage with a persistent site session', () => {
|
|
23
|
+
const cmd = cli({
|
|
24
|
+
site: 'test-registry',
|
|
25
|
+
name: 'fresh-ok', access: 'write',
|
|
26
|
+
description: 'test',
|
|
27
|
+
siteSession: 'persistent',
|
|
28
|
+
freshPage: true,
|
|
29
|
+
});
|
|
30
|
+
expect(cmd.freshPage).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
it('rejects freshPage without a persistent site session', () => {
|
|
33
|
+
expect(() => cli({
|
|
34
|
+
site: 'test-registry',
|
|
35
|
+
name: 'fresh-bad', access: 'write',
|
|
36
|
+
description: 'test',
|
|
37
|
+
freshPage: true,
|
|
38
|
+
})).toThrow(/freshPage requires siteSession: 'persistent'/);
|
|
39
|
+
expect(() => cli({
|
|
40
|
+
site: 'test-registry',
|
|
41
|
+
name: 'fresh-bad-ephemeral', access: 'write',
|
|
42
|
+
description: 'test',
|
|
43
|
+
siteSession: 'ephemeral',
|
|
44
|
+
freshPage: true,
|
|
45
|
+
})).toThrow(/freshPage requires siteSession: 'persistent'/);
|
|
46
|
+
});
|
|
22
47
|
it('puts registered command in the registry', () => {
|
|
23
48
|
cli({
|
|
24
49
|
site: 'test-registry',
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
|
-
import { unsupportedDaemonPortEnvMessage } from './constants.js';
|
|
4
3
|
import { getUserWebcmdDir, getUserClisDir, getPluginsDir } from './discovery.js';
|
|
5
4
|
describe('webcmd runtime identity', () => {
|
|
6
5
|
it('uses webcmd runtime directories', async () => {
|
|
@@ -8,11 +7,4 @@ describe('webcmd runtime identity', () => {
|
|
|
8
7
|
expect(getUserClisDir('/home/tester')).toBe(path.join('/home/tester', '.webcmd', 'clis'));
|
|
9
8
|
expect(getPluginsDir('/home/tester')).toBe(path.join('/home/tester', '.webcmd', 'plugins'));
|
|
10
9
|
});
|
|
11
|
-
it('reports unsupported daemon port with WEBCMD env names', () => {
|
|
12
|
-
const legacyEnvName = ['OPEN', 'CLI_DAEMON_PORT'].join('');
|
|
13
|
-
expect(unsupportedDaemonPortEnvMessage('1234')).toContain('WEBCMD_DAEMON_PORT');
|
|
14
|
-
expect(unsupportedDaemonPortEnvMessage('1234')).toContain('Webcmd');
|
|
15
|
-
expect(unsupportedDaemonPortEnvMessage('1234')).toContain('rerun webcmd');
|
|
16
|
-
expect(unsupportedDaemonPortEnvMessage('1234')).not.toContain(legacyEnvName);
|
|
17
|
-
});
|
|
18
10
|
});
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -29,10 +29,12 @@ export interface IBrowserFactory {
|
|
|
29
29
|
session?: string;
|
|
30
30
|
cdpEndpoint?: string;
|
|
31
31
|
contextId?: string;
|
|
32
|
+
preferredContextId?: string;
|
|
32
33
|
idleTimeout?: number;
|
|
33
34
|
windowMode?: BrowserWindowMode;
|
|
34
35
|
surface?: BrowserSurface;
|
|
35
36
|
siteSession?: 'ephemeral' | 'persistent';
|
|
37
|
+
freshPage?: boolean;
|
|
36
38
|
}): Promise<IPage>;
|
|
37
39
|
close(): Promise<void>;
|
|
38
40
|
}
|
|
@@ -40,8 +42,10 @@ export declare function browserSession<T>(BrowserFactory: new () => IBrowserFact
|
|
|
40
42
|
session?: string;
|
|
41
43
|
cdpEndpoint?: string;
|
|
42
44
|
contextId?: string;
|
|
45
|
+
preferredContextId?: string;
|
|
43
46
|
idleTimeout?: number;
|
|
44
47
|
windowMode?: BrowserWindowMode;
|
|
45
48
|
surface?: BrowserSurface;
|
|
46
49
|
siteSession?: 'ephemeral' | 'persistent';
|
|
50
|
+
freshPage?: boolean;
|
|
47
51
|
}): Promise<T>;
|
package/dist/src/runtime.js
CHANGED
|
@@ -41,10 +41,12 @@ export async function browserSession(BrowserFactory, fn, opts = {}) {
|
|
|
41
41
|
session: opts.session,
|
|
42
42
|
cdpEndpoint: opts.cdpEndpoint,
|
|
43
43
|
contextId: opts.contextId,
|
|
44
|
+
preferredContextId: opts.preferredContextId,
|
|
44
45
|
idleTimeout: opts.idleTimeout,
|
|
45
46
|
windowMode: opts.windowMode,
|
|
46
47
|
surface: opts.surface,
|
|
47
48
|
siteSession: opts.siteSession,
|
|
49
|
+
freshPage: opts.freshPage,
|
|
48
50
|
});
|
|
49
51
|
return await fn(page);
|
|
50
52
|
}
|
package/dist/src/skills.d.ts
CHANGED
|
@@ -4,11 +4,29 @@ export interface WebcmdSkillInfo {
|
|
|
4
4
|
version: string;
|
|
5
5
|
path: string;
|
|
6
6
|
}
|
|
7
|
-
export interface
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export interface WebcmdSkillInstallOptions {
|
|
8
|
+
provider?: string;
|
|
9
|
+
scope?: string;
|
|
10
|
+
customPath?: string;
|
|
11
|
+
packageRoot?: string;
|
|
12
|
+
homeDir?: string;
|
|
13
|
+
cwd?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface WebcmdSkillLink {
|
|
16
|
+
name: string;
|
|
17
|
+
source: string;
|
|
18
|
+
stableLink: string;
|
|
19
|
+
destination?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WebcmdSkillInstallResult {
|
|
22
|
+
provider?: SkillProvider;
|
|
23
|
+
scope?: SkillScope;
|
|
24
|
+
skills: WebcmdSkillLink[];
|
|
11
25
|
}
|
|
26
|
+
type SkillProvider = 'agents' | 'codex' | 'claude';
|
|
27
|
+
type SkillScope = 'user' | 'project';
|
|
12
28
|
export declare function getSkillsRoot(packageRoot?: string): string;
|
|
13
29
|
export declare function listWebcmdSkills(packageRoot?: string): WebcmdSkillInfo[];
|
|
14
|
-
export declare function
|
|
30
|
+
export declare function installWebcmdSkill(options?: WebcmdSkillInstallOptions): WebcmdSkillInstallResult;
|
|
31
|
+
export declare function updateWebcmdSkill(options?: WebcmdSkillInstallOptions): WebcmdSkillInstallResult;
|
|
32
|
+
export {};
|