@agentrhq/webcmd 0.2.1 → 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 +39 -24
- package/cli-manifest.json +504 -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/dist/src/browser/bridge.d.ts +1 -0
- package/dist/src/browser/bridge.js +1 -1
- package/dist/src/browser/page.d.ts +4 -1
- package/dist/src/browser/page.js +12 -1
- package/dist/src/browser/protocol.d.ts +2 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +1 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +2 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +12 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +58 -0
- package/dist/src/build-manifest.js +1 -0
- package/dist/src/build-manifest.test.js +34 -0
- package/dist/src/cli.js +111 -28
- package/dist/src/discovery.js +1 -0
- package/dist/src/engine.test.js +62 -0
- package/dist/src/execution.js +1 -1
- package/dist/src/manifest-types.d.ts +2 -0
- 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.d.ts +2 -0
- package/dist/src/runtime.js +1 -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 +2 -2
- 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
|
@@ -42,6 +42,64 @@ describe('CloakSessionManager', () => {
|
|
|
42
42
|
expect(launchPersistentContext).toHaveBeenCalledTimes(1);
|
|
43
43
|
expect(launchPersistentContext.mock.calls[0][0]).toMatchObject({ headless: false });
|
|
44
44
|
});
|
|
45
|
+
it('freshPage closes the existing persistent lease page and creates a new one', async () => {
|
|
46
|
+
const makePage = () => ({
|
|
47
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
evaluate: vi.fn().mockResolvedValue('ok'),
|
|
49
|
+
title: vi.fn().mockResolvedValue('Title'),
|
|
50
|
+
url: vi.fn().mockReturnValue('about:blank'),
|
|
51
|
+
isClosed: vi.fn().mockReturnValue(false),
|
|
52
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
53
|
+
});
|
|
54
|
+
const openPages = [];
|
|
55
|
+
const context = {
|
|
56
|
+
pages: vi.fn(() => openPages),
|
|
57
|
+
newPage: vi.fn(async () => {
|
|
58
|
+
const page = makePage();
|
|
59
|
+
openPages.push(page);
|
|
60
|
+
return page;
|
|
61
|
+
}),
|
|
62
|
+
cookies: vi.fn().mockResolvedValue([]),
|
|
63
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
64
|
+
};
|
|
65
|
+
const manager = new CloakSessionManager({
|
|
66
|
+
baseDir: '/tmp/webcmd-test',
|
|
67
|
+
launchPersistentContext: vi.fn().mockResolvedValue(context),
|
|
68
|
+
});
|
|
69
|
+
const key = { profileId: 'default', session: 'site:district', surface: 'adapter', siteSession: 'persistent' };
|
|
70
|
+
const first = await manager.getPage(key);
|
|
71
|
+
expect((await manager.getPage(key)).page).toBe(first.page);
|
|
72
|
+
const fresh = await manager.getPage({ ...key, freshPage: true });
|
|
73
|
+
expect(first.page.close).toHaveBeenCalled();
|
|
74
|
+
expect(fresh.page).not.toBe(first.page);
|
|
75
|
+
const reused = await manager.getPage(key);
|
|
76
|
+
expect(reused.page).toBe(fresh.page);
|
|
77
|
+
});
|
|
78
|
+
it('freshPage never adopts a leftover context tab', async () => {
|
|
79
|
+
const leftover = {
|
|
80
|
+
goto: vi.fn(),
|
|
81
|
+
isClosed: vi.fn().mockReturnValue(false),
|
|
82
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
83
|
+
};
|
|
84
|
+
const created = {
|
|
85
|
+
goto: vi.fn(),
|
|
86
|
+
isClosed: vi.fn().mockReturnValue(false),
|
|
87
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
88
|
+
};
|
|
89
|
+
const context = {
|
|
90
|
+
pages: vi.fn().mockReturnValue([leftover]),
|
|
91
|
+
newPage: vi.fn().mockResolvedValue(created),
|
|
92
|
+
cookies: vi.fn().mockResolvedValue([]),
|
|
93
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
94
|
+
};
|
|
95
|
+
const manager = new CloakSessionManager({
|
|
96
|
+
baseDir: '/tmp/webcmd-test',
|
|
97
|
+
launchPersistentContext: vi.fn().mockResolvedValue(context),
|
|
98
|
+
});
|
|
99
|
+
const lease = await manager.getPage({ profileId: 'default', session: 'site:district', surface: 'adapter', siteSession: 'persistent', freshPage: true });
|
|
100
|
+
expect(lease.page).toBe(created);
|
|
101
|
+
expect(context.newPage).toHaveBeenCalled();
|
|
102
|
+
});
|
|
45
103
|
it('closes ephemeral adapter sessions when released', async () => {
|
|
46
104
|
const launched = fakeContext();
|
|
47
105
|
const manager = new CloakSessionManager({
|
|
@@ -81,6 +81,40 @@ describe('manifest helper rules', () => {
|
|
|
81
81
|
expect(entries[0].sourceFile).not.toContain('\\');
|
|
82
82
|
getRegistry().delete(key);
|
|
83
83
|
});
|
|
84
|
+
it('serializes freshPage for persistent browser commands', async () => {
|
|
85
|
+
const site = `manifest-fresh-page-${Date.now()}`;
|
|
86
|
+
const key = `${site}/checkout`;
|
|
87
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-manifest-'));
|
|
88
|
+
tempDirs.push(dir);
|
|
89
|
+
const file = path.join(dir, `${site}.ts`);
|
|
90
|
+
fs.writeFileSync(file, `export const command = cli({ site: '${site}', name: 'checkout', access: 'write' });`);
|
|
91
|
+
const entries = await loadManifestEntries(file, site, async () => ({
|
|
92
|
+
command: cli({
|
|
93
|
+
site,
|
|
94
|
+
name: 'checkout',
|
|
95
|
+
access: 'write',
|
|
96
|
+
description: 'checkout command',
|
|
97
|
+
strategy: Strategy.COOKIE,
|
|
98
|
+
browser: true,
|
|
99
|
+
siteSession: 'persistent',
|
|
100
|
+
freshPage: true,
|
|
101
|
+
}),
|
|
102
|
+
}));
|
|
103
|
+
expect(entries).toMatchObject([
|
|
104
|
+
{
|
|
105
|
+
site,
|
|
106
|
+
name: 'checkout',
|
|
107
|
+
access: 'write',
|
|
108
|
+
strategy: 'cookie',
|
|
109
|
+
browser: true,
|
|
110
|
+
siteSession: 'persistent',
|
|
111
|
+
freshPage: true,
|
|
112
|
+
type: 'js',
|
|
113
|
+
modulePath: `${site}/${site}.js`,
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
getRegistry().delete(key);
|
|
117
|
+
});
|
|
84
118
|
it('falls back to registry delta for side-effect-only cli modules', async () => {
|
|
85
119
|
const site = `manifest-side-effect-${Date.now()}`;
|
|
86
120
|
const key = `${site}/legacy`;
|
package/dist/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
+
import * as readline from 'node:readline/promises';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import { Command, InvalidArgumentError, Option } from 'commander';
|
|
12
13
|
import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
|
|
@@ -16,10 +17,10 @@ import { render as renderOutput } from './output.js';
|
|
|
16
17
|
import { PKG_VERSION } from './version.js';
|
|
17
18
|
import { printCompletionScript } from './completion.js';
|
|
18
19
|
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled, formatExternalCliLabel } from './external.js';
|
|
19
|
-
import { listWebcmdSkills,
|
|
20
|
+
import { installWebcmdSkill, listWebcmdSkills, updateWebcmdSkill } from './skills.js';
|
|
20
21
|
import { registerAllCommands } from './commanderAdapter.js';
|
|
21
22
|
import { classifyAdapter, formatRootAdapterHelpText, installCommanderNamespaceStructuredHelp, installStructuredHelp, leadingPositionalFromUsage, rootHelpData } from './help.js';
|
|
22
|
-
import { EXIT_CODES, getErrorMessage, BrowserConnectError, CliError } from './errors.js';
|
|
23
|
+
import { EXIT_CODES, getErrorMessage, BrowserConnectError, CliError, ArgumentError } from './errors.js';
|
|
23
24
|
import { TargetError } from './browser/target-errors.js';
|
|
24
25
|
import { resolveTargetJs, getTextResolvedJs, getValueResolvedJs, getAttributesResolvedJs, selectResolvedJs, isAutocompleteResolvedJs } from './browser/target-resolver.js';
|
|
25
26
|
import { buildFindJs, buildSemanticFindJs, isFindError } from './browser/find.js';
|
|
@@ -58,6 +59,70 @@ function parseDurationMs(raw, flagName) {
|
|
|
58
59
|
function timestampFromRaw(value) {
|
|
59
60
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : Date.now();
|
|
60
61
|
}
|
|
62
|
+
function isInteractiveInstall(opts) {
|
|
63
|
+
return !opts.json && process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
64
|
+
}
|
|
65
|
+
async function resolveSkillInstallOptions(opts) {
|
|
66
|
+
if (!isInteractiveInstall(opts))
|
|
67
|
+
return opts;
|
|
68
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
69
|
+
try {
|
|
70
|
+
const scope = opts.scope ?? await choosePrompt(rl, 'Where should Webcmd install skills?', [
|
|
71
|
+
{ key: '1', label: 'Global', value: 'user', aliases: ['global', 'user', 'g'] },
|
|
72
|
+
{ key: '2', label: 'Local project', value: 'project', aliases: ['local', 'project', 'l'] },
|
|
73
|
+
], '1');
|
|
74
|
+
const provider = opts.provider ?? (opts.path ? undefined : await choosePrompt(rl, 'Which coding agent should use them?', [
|
|
75
|
+
{ key: '1', label: 'Agents', value: 'agents', aliases: ['agents', 'agent', 'a'] },
|
|
76
|
+
{ key: '2', label: 'Codex', value: 'codex', aliases: ['codex', 'c'] },
|
|
77
|
+
{ key: '3', label: 'Claude', value: 'claude', aliases: ['claude', 'claude-code'] },
|
|
78
|
+
{ key: '4', label: 'Custom path', value: 'custom', aliases: ['custom', 'path'] },
|
|
79
|
+
], '1'));
|
|
80
|
+
const customPath = opts.path ?? (provider === 'custom' ? await nonEmptyPrompt(rl, 'Skills directory path: ') : undefined);
|
|
81
|
+
return { ...opts, scope, provider: provider === 'custom' ? undefined : provider, path: customPath };
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
rl.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function choosePrompt(rl, question, choices, defaultKey) {
|
|
88
|
+
console.log(question);
|
|
89
|
+
for (const choice of choices)
|
|
90
|
+
console.log(` ${choice.key}) ${choice.label}`);
|
|
91
|
+
while (true) {
|
|
92
|
+
const answer = (await rl.question(`Choose [${defaultKey}]: `)).trim().toLowerCase() || defaultKey;
|
|
93
|
+
const choice = choices.find((item) => item.key === answer || item.aliases.includes(answer));
|
|
94
|
+
if (choice)
|
|
95
|
+
return choice.value;
|
|
96
|
+
console.log(`Choose one of: ${choices.map((item) => item.key).join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function nonEmptyPrompt(rl, question) {
|
|
100
|
+
while (true) {
|
|
101
|
+
const answer = (await rl.question(question)).trim();
|
|
102
|
+
if (answer)
|
|
103
|
+
return answer;
|
|
104
|
+
console.log('Path is required.');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function handleSkillLinkCommand(action, json, verb) {
|
|
108
|
+
try {
|
|
109
|
+
const result = await action();
|
|
110
|
+
if (json) {
|
|
111
|
+
console.log(JSON.stringify(result, null, 2));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log(`Webcmd skills ${verb}: ${result.skills.length}`);
|
|
115
|
+
for (const skill of result.skills) {
|
|
116
|
+
console.log(`${skill.name}: ${skill.destination ? `${skill.destination} -> ` : ''}${skill.stableLink}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
121
|
+
if (err instanceof CliError && err.hint)
|
|
122
|
+
console.error(`Hint: ${err.hint}`);
|
|
123
|
+
process.exitCode = err instanceof CliError ? err.exitCode : EXIT_CODES.GENERIC_ERROR;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
61
126
|
function toIsoTimestamp(timestamp) {
|
|
62
127
|
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp <= 0)
|
|
63
128
|
return undefined;
|
|
@@ -711,10 +776,20 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
711
776
|
});
|
|
712
777
|
const skillsCmd = program
|
|
713
778
|
.command('skills')
|
|
714
|
-
.description('
|
|
779
|
+
.description('List, install, and update bundled Webcmd skills')
|
|
780
|
+
.action(() => {
|
|
781
|
+
const rows = listWebcmdSkills();
|
|
782
|
+
renderOutput(rows, {
|
|
783
|
+
fmt: 'table',
|
|
784
|
+
fmtExplicit: false,
|
|
785
|
+
columns: ['name', 'description', 'version', 'path'],
|
|
786
|
+
title: 'webcmd/skills/list',
|
|
787
|
+
source: 'webcmd skills',
|
|
788
|
+
});
|
|
789
|
+
});
|
|
715
790
|
skillsCmd
|
|
716
791
|
.command('list')
|
|
717
|
-
.description('List bundled
|
|
792
|
+
.description('List bundled Webcmd skills')
|
|
718
793
|
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
719
794
|
.action((opts) => {
|
|
720
795
|
const rows = listWebcmdSkills();
|
|
@@ -727,30 +802,38 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
727
802
|
});
|
|
728
803
|
});
|
|
729
804
|
skillsCmd
|
|
730
|
-
.command('
|
|
731
|
-
.description(
|
|
732
|
-
.
|
|
733
|
-
.
|
|
734
|
-
.option('--
|
|
735
|
-
.
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
805
|
+
.command('install')
|
|
806
|
+
.description('Install bundled Webcmd skills into an agent skills folder')
|
|
807
|
+
.option('-p, --provider <provider>', 'Agent provider: agents, codex, claude')
|
|
808
|
+
.option('-s, --scope <scope>', 'Install scope: user/global or project/local')
|
|
809
|
+
.option('--path <path>', 'Custom agent skills directory')
|
|
810
|
+
.option('--json', 'Output a JSON envelope', false)
|
|
811
|
+
.action(async (opts) => {
|
|
812
|
+
await handleSkillLinkCommand(async () => {
|
|
813
|
+
const resolved = await resolveSkillInstallOptions(opts);
|
|
814
|
+
if (resolved.provider === 'custom' && !resolved.path) {
|
|
815
|
+
throw new ArgumentError('Custom skill provider requires --path.', 'Pass --path <skills-dir> or run interactively.');
|
|
816
|
+
}
|
|
817
|
+
return installWebcmdSkill({
|
|
818
|
+
provider: resolved.provider,
|
|
819
|
+
scope: resolved.scope,
|
|
820
|
+
customPath: resolved.path,
|
|
821
|
+
});
|
|
822
|
+
}, opts.json, 'installed');
|
|
823
|
+
});
|
|
824
|
+
skillsCmd
|
|
825
|
+
.command('update')
|
|
826
|
+
.description('Refresh bundled Webcmd skill symlinks after updating the package')
|
|
827
|
+
.option('-p, --provider <provider>', 'Also repair provider link: agents, codex, claude')
|
|
828
|
+
.option('-s, --scope <scope>', 'Also repair scoped link: user/global or project/local')
|
|
829
|
+
.option('--path <path>', 'Also repair a custom agent skills directory')
|
|
830
|
+
.option('--json', 'Output a JSON envelope', false)
|
|
831
|
+
.action(async (opts) => {
|
|
832
|
+
await handleSkillLinkCommand(() => updateWebcmdSkill({
|
|
833
|
+
provider: opts.provider,
|
|
834
|
+
scope: opts.scope,
|
|
835
|
+
customPath: opts.path,
|
|
836
|
+
}), opts.json, 'updated');
|
|
754
837
|
});
|
|
755
838
|
const authCmd = registerAuthCommands(program);
|
|
756
839
|
program
|
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/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
|
@@ -334,7 +334,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
|
|
|
334
334
|
await page.closeWindow?.().catch(() => { });
|
|
335
335
|
throw err;
|
|
336
336
|
}
|
|
337
|
-
}, { session, cdpEndpoint, ...profileRouting, windowMode, surface: 'adapter', siteSession });
|
|
337
|
+
}, { session, cdpEndpoint, ...profileRouting, windowMode, surface: 'adapter', siteSession, freshPage: cmd.freshPage === true && siteSession === 'persistent' });
|
|
338
338
|
}
|
|
339
339
|
else {
|
|
340
340
|
// Non-browser commands: enforce a timeout only when the command exposes
|
|
@@ -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
|
}
|
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',
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface IBrowserFactory {
|
|
|
34
34
|
windowMode?: BrowserWindowMode;
|
|
35
35
|
surface?: BrowserSurface;
|
|
36
36
|
siteSession?: 'ephemeral' | 'persistent';
|
|
37
|
+
freshPage?: boolean;
|
|
37
38
|
}): Promise<IPage>;
|
|
38
39
|
close(): Promise<void>;
|
|
39
40
|
}
|
|
@@ -46,4 +47,5 @@ export declare function browserSession<T>(BrowserFactory: new () => IBrowserFact
|
|
|
46
47
|
windowMode?: BrowserWindowMode;
|
|
47
48
|
surface?: BrowserSurface;
|
|
48
49
|
siteSession?: 'ephemeral' | 'persistent';
|
|
50
|
+
freshPage?: boolean;
|
|
49
51
|
}): Promise<T>;
|
package/dist/src/runtime.js
CHANGED
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 {};
|