@agentrhq/webcmd 0.2.1 → 0.2.3
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 +2308 -650
- package/clis/_shared/site-auth.js +6 -1
- package/clis/bigbasket/add-to-cart.js +82 -0
- package/clis/bigbasket/bigbasket.test.js +255 -0
- package/clis/bigbasket/cart.js +81 -0
- package/clis/bigbasket/category.js +30 -0
- package/clis/bigbasket/checkout.js +71 -0
- package/clis/bigbasket/location.js +30 -0
- package/clis/bigbasket/product.js +79 -0
- package/clis/bigbasket/search.js +30 -0
- package/clis/bigbasket/utils.js +207 -0
- package/clis/blinkit/add-to-cart.js +123 -0
- package/clis/blinkit/auth.js +99 -0
- package/clis/blinkit/blinkit.test.js +168 -0
- package/clis/blinkit/cart.js +34 -0
- package/clis/blinkit/checkout.js +32 -0
- package/clis/blinkit/location.js +29 -0
- package/clis/blinkit/place-order.js +78 -0
- package/clis/blinkit/product.js +63 -0
- package/clis/blinkit/search.js +89 -0
- package/clis/blinkit/utils.js +223 -0
- package/clis/district/_lib.js +566 -0
- package/clis/district/auth.js +49 -0
- package/clis/district/checkout.js +348 -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/practo/appointment.js +21 -0
- package/clis/practo/appointments.js +27 -0
- package/clis/practo/book-confirm.js +35 -0
- package/clis/practo/book-preview.js +24 -0
- package/clis/practo/booking-link.js +24 -0
- package/clis/practo/cancel.js +29 -0
- package/clis/practo/contact.js +21 -0
- package/clis/practo/login.js +21 -0
- package/clis/practo/practo.test.js +136 -0
- package/clis/practo/profile.js +31 -0
- package/clis/practo/search.js +30 -0
- package/clis/practo/slots.js +19 -0
- package/clis/practo/utils.js +374 -0
- package/clis/practo/whoami.js +28 -0
- package/clis/reddit/popular.js +12 -1
- package/clis/reddit/popular.test.js +12 -3
- package/clis/zepto/add-to-cart.js +53 -0
- package/clis/zepto/auth.js +59 -0
- package/clis/zepto/cart.js +23 -0
- package/clis/zepto/checkout.js +60 -0
- package/clis/zepto/location.js +20 -0
- package/clis/zepto/place-order.js +47 -0
- package/clis/zepto/product.js +52 -0
- package/clis/zepto/search.js +30 -0
- package/clis/zepto/utils.js +228 -0
- package/clis/zepto/zepto.test.js +238 -0
- 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/generate-release-notes-cli.test.js +55 -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/release-notes.d.ts +3 -1
- package/dist/src/release-notes.js +44 -1
- package/dist/src/release-notes.test.js +39 -1
- 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/scripts/generate-release-notes.ts +31 -0
- 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 +10 -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/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
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { fileURLToPath } from 'node:url';
|
|
3
5
|
import { describe, expect, it, vi } from 'vitest';
|
|
4
6
|
import { loadReleaseContext, runGenerateReleaseNotes } from '../scripts/generate-release-notes.js';
|
|
@@ -72,6 +74,9 @@ describe('runGenerateReleaseNotes', () => {
|
|
|
72
74
|
'## Fixes',
|
|
73
75
|
'None.',
|
|
74
76
|
'',
|
|
77
|
+
'## Adapters',
|
|
78
|
+
'None.',
|
|
79
|
+
'',
|
|
75
80
|
'## Contributors',
|
|
76
81
|
'- @alice',
|
|
77
82
|
'',
|
|
@@ -94,6 +99,52 @@ describe('runGenerateReleaseNotes', () => {
|
|
|
94
99
|
stderr: 'Gemini release notes failed: gh timed out\n',
|
|
95
100
|
});
|
|
96
101
|
});
|
|
102
|
+
it('updates the matching changelog entry from an existing notes file', async () => {
|
|
103
|
+
const { io, read } = createIo();
|
|
104
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'webcmd-release-notes-'));
|
|
105
|
+
const notesPath = join(tempDir, 'release-notes.md');
|
|
106
|
+
const changelogPath = join(tempDir, 'CHANGELOG.md');
|
|
107
|
+
try {
|
|
108
|
+
writeFileSync(notesPath, [
|
|
109
|
+
'## Highlights',
|
|
110
|
+
'- Better generated notes.',
|
|
111
|
+
'',
|
|
112
|
+
'## Adapters',
|
|
113
|
+
'- Added checkout adapter polish.',
|
|
114
|
+
'',
|
|
115
|
+
].join('\n'));
|
|
116
|
+
writeFileSync(changelogPath, [
|
|
117
|
+
'# Changelog',
|
|
118
|
+
'',
|
|
119
|
+
'## [0.2.3](https://github.com/agentrhq/webcmd/compare/webcmd-v0.2.2...webcmd-v0.2.3) (2026-07-09)',
|
|
120
|
+
'',
|
|
121
|
+
'### Features',
|
|
122
|
+
'',
|
|
123
|
+
'* release-please generated note',
|
|
124
|
+
'',
|
|
125
|
+
'## [0.2.2](https://github.com/agentrhq/webcmd/compare/webcmd-v0.2.1...webcmd-v0.2.2) (2026-07-08)',
|
|
126
|
+
'',
|
|
127
|
+
'### Bug Fixes',
|
|
128
|
+
'',
|
|
129
|
+
'* older note',
|
|
130
|
+
'',
|
|
131
|
+
].join('\n'));
|
|
132
|
+
const exitCode = await runGenerateReleaseNotes(['node', 'script', '--update-changelog', 'webcmd-v0.2.3', notesPath, changelogPath], {}, {}, io);
|
|
133
|
+
expect(exitCode).toBe(0);
|
|
134
|
+
expect(read()).toEqual({
|
|
135
|
+
stdout: `Updated ${changelogPath} for webcmd-v0.2.3\n`,
|
|
136
|
+
stderr: '',
|
|
137
|
+
});
|
|
138
|
+
const changelog = readFileSync(changelogPath, 'utf8');
|
|
139
|
+
expect(changelog).toContain('### Highlights\n- Better generated notes.');
|
|
140
|
+
expect(changelog).toContain('### Adapters\n- Added checkout adapter polish.');
|
|
141
|
+
expect(changelog).not.toContain('release-please generated note');
|
|
142
|
+
expect(changelog).toContain('* older note');
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
97
148
|
it('loads bounded PR diffs into release context', async () => {
|
|
98
149
|
const gh = vi.fn((args) => {
|
|
99
150
|
const key = args.join(' ');
|
|
@@ -134,6 +185,9 @@ describe('runGenerateReleaseNotes', () => {
|
|
|
134
185
|
it('keeps npm publish unblocked when enhanced release-note editing fails', () => {
|
|
135
186
|
const workflowPath = fileURLToPath(new URL('../.github/workflows/release.yml', import.meta.url));
|
|
136
187
|
const workflow = readFileSync(workflowPath, 'utf8');
|
|
188
|
+
expect(workflow.indexOf('- name: Publish to npm')).toBeLessThan(workflow.indexOf('- name: Update changelog with enhanced release notes'));
|
|
189
|
+
expect(workflow).toContain('npm --silent run generate-release-notes -- --update-changelog');
|
|
190
|
+
expect(workflow).toContain('git push origin "HEAD:${{ github.ref_name }}"');
|
|
137
191
|
expect(workflow).toMatch(/if gh release edit "\$\{\{ steps\.release\.outputs\.tag_name \}\}" --notes-file "\$RUNNER_TEMP\/release-notes\.md"; then/);
|
|
138
192
|
expect(workflow).toMatch(/Enhanced release notes could not be applied; keeping release-please notes\./);
|
|
139
193
|
});
|
|
@@ -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',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const RELEASE_NOTE_SECTIONS: readonly ["Highlights", "Improvements", "Fixes", "Contributors", "Reverts"];
|
|
1
|
+
export declare const RELEASE_NOTE_SECTIONS: readonly ["Highlights", "Improvements", "Fixes", "Adapters", "Contributors", "Reverts"];
|
|
2
2
|
export type ReleaseNoteSection = typeof RELEASE_NOTE_SECTIONS[number];
|
|
3
3
|
export interface PullRequestLabel {
|
|
4
4
|
name: string;
|
|
@@ -32,6 +32,8 @@ export interface CompareCommit {
|
|
|
32
32
|
author?: string | null;
|
|
33
33
|
}
|
|
34
34
|
export type GitRunner = (args: readonly string[]) => Promise<string>;
|
|
35
|
+
export declare function releaseVersionFromTag(tag: string): string;
|
|
36
|
+
export declare function replaceChangelogReleaseNotes(changelog: string, tag: string, notes: string): string;
|
|
35
37
|
export declare function extractPullRequestNumber(message: string): number | null;
|
|
36
38
|
export declare function filterReleasePullRequests(prs: PullRequestDetails[]): PullRequestDetails[];
|
|
37
39
|
export declare function normalizeReleaseNotes(raw: string, contributors: string[]): string;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export const RELEASE_NOTE_SECTIONS = [
|
|
1
|
+
export const RELEASE_NOTE_SECTIONS = [
|
|
2
|
+
'Highlights',
|
|
3
|
+
'Improvements',
|
|
4
|
+
'Fixes',
|
|
5
|
+
'Adapters',
|
|
6
|
+
'Contributors',
|
|
7
|
+
'Reverts',
|
|
8
|
+
];
|
|
2
9
|
const SQUASH_MERGE_PR_NUMBER_PATTERN = /\(#(?<number>\d+)\)\s*$/;
|
|
3
10
|
const MERGE_COMMIT_PR_NUMBER_PATTERN = /^Merge pull request #(?<number>\d+) /;
|
|
4
11
|
const RELEASE_PLEASE_TITLE_PATTERN = /^chore(?:\([^)]+\))?: release(?:\s|$)/;
|
|
@@ -13,6 +20,39 @@ function formatSectionContent(content) {
|
|
|
13
20
|
const trimmed = content?.trim();
|
|
14
21
|
return trimmed && trimmed.length > 0 ? trimmed : 'None.';
|
|
15
22
|
}
|
|
23
|
+
function escapeRegExp(value) {
|
|
24
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
25
|
+
}
|
|
26
|
+
function formatReleaseNotesForChangelog(notes) {
|
|
27
|
+
const trimmed = notes.trim();
|
|
28
|
+
if (!trimmed)
|
|
29
|
+
return '### Highlights\nNone.';
|
|
30
|
+
return trimmed.replace(/^##\s+/gm, '### ');
|
|
31
|
+
}
|
|
32
|
+
export function releaseVersionFromTag(tag) {
|
|
33
|
+
const value = tag.trim();
|
|
34
|
+
if (value.startsWith('webcmd-v'))
|
|
35
|
+
return value.slice('webcmd-v'.length);
|
|
36
|
+
if (value.startsWith('v'))
|
|
37
|
+
return value.slice(1);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
export function replaceChangelogReleaseNotes(changelog, tag, notes) {
|
|
41
|
+
const version = releaseVersionFromTag(tag);
|
|
42
|
+
const headingPattern = new RegExp(`^## \\[${escapeRegExp(version)}\\]\\([^\\n]+\\) \\([^\\n]+\\)\\s*$`, 'm');
|
|
43
|
+
const headingMatch = headingPattern.exec(changelog);
|
|
44
|
+
if (!headingMatch) {
|
|
45
|
+
throw new Error(`Could not find CHANGELOG.md entry for ${version}`);
|
|
46
|
+
}
|
|
47
|
+
const headingEnd = headingMatch.index + headingMatch[0].length;
|
|
48
|
+
const remaining = changelog.slice(headingEnd);
|
|
49
|
+
const nextReleaseMatch = /\n## \[/.exec(remaining);
|
|
50
|
+
const releaseEnd = nextReleaseMatch ? headingEnd + nextReleaseMatch.index : changelog.length;
|
|
51
|
+
const before = changelog.slice(0, headingEnd).trimEnd();
|
|
52
|
+
const after = changelog.slice(releaseEnd);
|
|
53
|
+
const suffix = after ? after.replace(/^\n+/, '\n\n') : '\n';
|
|
54
|
+
return `${before}\n\n${formatReleaseNotesForChangelog(notes)}${suffix}`;
|
|
55
|
+
}
|
|
16
56
|
export function extractPullRequestNumber(message) {
|
|
17
57
|
const firstLine = message.split(/\r?\n/, 1)[0] ?? message;
|
|
18
58
|
const squashMatch = firstLine.match(SQUASH_MERGE_PR_NUMBER_PATTERN);
|
|
@@ -92,6 +132,9 @@ export function buildReleaseNotesPrompt(context) {
|
|
|
92
132
|
'Use only the supplied pull requests below. Do not invent changes or pull in information from elsewhere.',
|
|
93
133
|
`Required sections: ${RELEASE_NOTE_SECTIONS.map((section) => `## ${section}`).join(', ')}.`,
|
|
94
134
|
'Each section must be present in the final notes.',
|
|
135
|
+
'In this project, CLI commands and adapters are the same thing. Treat any PR that adds, removes, or changes files under clis/** as an adapter change, even if the PR title says "CLI" instead of "adapter".',
|
|
136
|
+
'Put new site adapters/CLIs, adapter promotions, adapter hardening, adapter output changes, selector/API updates, and site-specific workflow improvements in ## Adapters.',
|
|
137
|
+
'Use ## Improvements for non-adapter product, runtime, CLI, docs, or workflow improvements.',
|
|
95
138
|
'',
|
|
96
139
|
`Pull requests included for this release:`,
|
|
97
140
|
prSummaries,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { RELEASE_NOTE_SECTIONS, buildReleaseNotesPrompt, extractPullRequestNumber, filterReleasePullRequests, normalizeReleaseNotes, } from './release-notes.js';
|
|
2
|
+
import { RELEASE_NOTE_SECTIONS, buildReleaseNotesPrompt, extractPullRequestNumber, filterReleasePullRequests, normalizeReleaseNotes, replaceChangelogReleaseNotes, } from './release-notes.js';
|
|
3
3
|
describe('release notes helpers', () => {
|
|
4
4
|
it('extracts PR numbers from squash and merge commit messages', () => {
|
|
5
5
|
expect(extractPullRequestNumber('feat: add release notes (#123)')).toBe(123);
|
|
@@ -29,10 +29,44 @@ describe('release notes helpers', () => {
|
|
|
29
29
|
for (const section of RELEASE_NOTE_SECTIONS) {
|
|
30
30
|
expect(normalized).toContain(`## ${section}`);
|
|
31
31
|
}
|
|
32
|
+
expect(normalized).toContain('## Adapters\nNone.');
|
|
32
33
|
expect(normalized).toContain('## Improvements\nNone.');
|
|
33
34
|
expect(normalized).toContain('## Contributors\n- @alice\n- @bob');
|
|
34
35
|
expect(normalized).toContain('## Reverts\nNone.');
|
|
35
36
|
});
|
|
37
|
+
it('replaces a matching changelog release entry with generated notes', () => {
|
|
38
|
+
const changelog = [
|
|
39
|
+
'# Changelog',
|
|
40
|
+
'',
|
|
41
|
+
'## [0.2.3](https://github.com/agentrhq/webcmd/compare/webcmd-v0.2.2...webcmd-v0.2.3) (2026-07-09)',
|
|
42
|
+
'',
|
|
43
|
+
'',
|
|
44
|
+
'### Features',
|
|
45
|
+
'',
|
|
46
|
+
'* release-please generated note',
|
|
47
|
+
'',
|
|
48
|
+
'## [0.2.2](https://github.com/agentrhq/webcmd/compare/webcmd-v0.2.1...webcmd-v0.2.2) (2026-07-08)',
|
|
49
|
+
'',
|
|
50
|
+
'### Bug Fixes',
|
|
51
|
+
'',
|
|
52
|
+
'* older note',
|
|
53
|
+
'',
|
|
54
|
+
].join('\n');
|
|
55
|
+
const notes = [
|
|
56
|
+
'## Highlights',
|
|
57
|
+
'- Better release notes.',
|
|
58
|
+
'',
|
|
59
|
+
'## Adapters',
|
|
60
|
+
'- Improved district checkout.',
|
|
61
|
+
].join('\n');
|
|
62
|
+
const updated = replaceChangelogReleaseNotes(changelog, 'webcmd-v0.2.3', notes);
|
|
63
|
+
expect(updated).toContain('## [0.2.3]');
|
|
64
|
+
expect(updated).toContain('### Highlights\n- Better release notes.');
|
|
65
|
+
expect(updated).toContain('### Adapters\n- Improved district checkout.');
|
|
66
|
+
expect(updated).not.toContain('release-please generated note');
|
|
67
|
+
expect(updated).toContain('## [0.2.2]');
|
|
68
|
+
expect(updated).toContain('* older note');
|
|
69
|
+
});
|
|
36
70
|
it('builds a prompt grounded in the exact release range and PR list', () => {
|
|
37
71
|
const context = {
|
|
38
72
|
tag: 'v0.2.0',
|
|
@@ -56,6 +90,10 @@ describe('release notes helpers', () => {
|
|
|
56
90
|
expect(prompt).toContain('PR #42: feat: add docs scaffold');
|
|
57
91
|
expect(prompt).toContain('docs/docs.json');
|
|
58
92
|
expect(prompt).toContain('## Highlights');
|
|
93
|
+
expect(prompt).toContain('## Adapters');
|
|
94
|
+
expect(prompt).toContain('CLI commands and adapters are the same thing');
|
|
95
|
+
expect(prompt).toContain('files under clis/** as an adapter change');
|
|
96
|
+
expect(prompt).toContain('Put new site adapters/CLIs, adapter promotions, adapter hardening');
|
|
59
97
|
expect(prompt).toContain('## Reverts');
|
|
60
98
|
});
|
|
61
99
|
});
|
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 {};
|
package/dist/src/skills.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
2
3
|
import * as path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import yaml from 'js-yaml';
|
|
@@ -13,35 +14,100 @@ export function listWebcmdSkills(packageRoot) {
|
|
|
13
14
|
if (!fs.existsSync(skillsRoot))
|
|
14
15
|
return [];
|
|
15
16
|
return fs.readdirSync(skillsRoot, { withFileTypes: true })
|
|
16
|
-
.filter((entry) => entry.isDirectory()
|
|
17
|
+
.filter((entry) => entry.isDirectory())
|
|
17
18
|
.map((entry) => readSkillInfo(skillsRoot, entry.name))
|
|
18
19
|
.filter((entry) => entry !== null)
|
|
19
20
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
20
21
|
}
|
|
21
|
-
export function
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
export function installWebcmdSkill(options = {}) {
|
|
23
|
+
const provider = options.customPath === undefined ? normalizeProvider(options.provider) : undefined;
|
|
24
|
+
const scope = normalizeScope(options.scope);
|
|
25
|
+
const skills = updateStableSkillLinks(options)
|
|
26
|
+
.map((skill) => {
|
|
27
|
+
const destination = destinationFor(skill.name, provider, scope, options);
|
|
28
|
+
replaceDirectorySymlink(skill.stableLink, destination);
|
|
29
|
+
return { ...skill, destination };
|
|
30
|
+
});
|
|
31
|
+
return { provider, scope, skills };
|
|
32
|
+
}
|
|
33
|
+
export function updateWebcmdSkill(options = {}) {
|
|
34
|
+
const skills = updateStableSkillLinks(options);
|
|
35
|
+
if (options.provider === undefined && options.scope === undefined && options.customPath === undefined)
|
|
36
|
+
return { skills };
|
|
37
|
+
const provider = options.customPath === undefined ? normalizeProvider(options.provider) : undefined;
|
|
38
|
+
const scope = normalizeScope(options.scope);
|
|
39
|
+
return {
|
|
40
|
+
provider,
|
|
41
|
+
scope,
|
|
42
|
+
skills: skills.map((skill) => {
|
|
43
|
+
const destination = destinationFor(skill.name, provider, scope, options);
|
|
44
|
+
replaceDirectorySymlink(skill.stableLink, destination);
|
|
45
|
+
return { ...skill, destination };
|
|
46
|
+
}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function updateStableSkillLinks(options) {
|
|
50
|
+
const skillsRoot = getSkillsRoot(options.packageRoot);
|
|
51
|
+
const skills = listWebcmdSkills(options.packageRoot);
|
|
52
|
+
if (skills.length === 0) {
|
|
53
|
+
throw new ArgumentError(`No Webcmd skills found: ${skillsRoot}`, 'Install a package that includes skills/*/SKILL.md.');
|
|
25
54
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
55
|
+
return skills.map((skill) => {
|
|
56
|
+
const source = path.join(skillsRoot, skill.name);
|
|
57
|
+
const stableLink = path.join(options.homeDir ?? os.homedir(), '.webcmd', 'skills', skill.name);
|
|
58
|
+
replaceDirectorySymlink(source, stableLink);
|
|
59
|
+
return { name: skill.name, source, stableLink };
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function destinationFor(name, provider, scope, options) {
|
|
63
|
+
if (options.customPath !== undefined)
|
|
64
|
+
return path.join(expandHomePath(options.customPath), name);
|
|
65
|
+
const base = scope === 'project' ? options.cwd ?? process.cwd() : options.homeDir ?? os.homedir();
|
|
66
|
+
const agentDir = provider === 'claude' ? '.claude' : provider === 'codex' ? '.codex' : '.agents';
|
|
67
|
+
return path.join(base, agentDir, 'skills', name);
|
|
68
|
+
}
|
|
69
|
+
function expandHomePath(raw) {
|
|
70
|
+
const value = raw.trim();
|
|
71
|
+
if (!value)
|
|
72
|
+
throw new ArgumentError('Custom skills path must be non-empty.');
|
|
73
|
+
return path.resolve(value === '~' ? os.homedir() : value.startsWith('~/') ? path.join(os.homedir(), value.slice(2)) : value);
|
|
74
|
+
}
|
|
75
|
+
function normalizeProvider(raw = 'agents') {
|
|
76
|
+
const value = raw.trim().toLowerCase();
|
|
77
|
+
if (value === 'agents' || value === 'codex')
|
|
78
|
+
return value;
|
|
79
|
+
if (value === 'claude' || value === 'claude-code' || value === 'claude_code')
|
|
80
|
+
return 'claude';
|
|
81
|
+
throw new ArgumentError(`Unsupported skill provider: ${raw}`, 'Use one of: agents, codex, claude.');
|
|
82
|
+
}
|
|
83
|
+
function normalizeScope(raw = 'user') {
|
|
84
|
+
const value = raw.trim().toLowerCase();
|
|
85
|
+
if (value === 'user' || value === 'global')
|
|
86
|
+
return 'user';
|
|
87
|
+
if (value === 'project' || value === 'local')
|
|
88
|
+
return 'project';
|
|
89
|
+
throw new ArgumentError(`Unsupported skill scope: ${raw}`, 'Use one of: user, global, project, local.');
|
|
90
|
+
}
|
|
91
|
+
function replaceDirectorySymlink(target, linkPath) {
|
|
92
|
+
const current = safeLstat(linkPath);
|
|
93
|
+
if (current) {
|
|
94
|
+
if (!current.isSymbolicLink()) {
|
|
95
|
+
throw new ArgumentError(`Refusing to replace non-symlink path: ${linkPath}`, 'Remove it manually or choose a different scope/provider.');
|
|
96
|
+
}
|
|
97
|
+
fs.unlinkSync(linkPath);
|
|
30
98
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
99
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
100
|
+
fs.symlinkSync(target, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
|
|
101
|
+
}
|
|
102
|
+
function safeLstat(filePath) {
|
|
103
|
+
try {
|
|
104
|
+
return fs.lstatSync(filePath);
|
|
36
105
|
}
|
|
37
|
-
|
|
38
|
-
|
|
106
|
+
catch (err) {
|
|
107
|
+
if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT')
|
|
108
|
+
return null;
|
|
109
|
+
throw err;
|
|
39
110
|
}
|
|
40
|
-
return {
|
|
41
|
-
skill: name,
|
|
42
|
-
path: relativePath,
|
|
43
|
-
content: fs.readFileSync(absolutePath, 'utf8'),
|
|
44
|
-
};
|
|
45
111
|
}
|
|
46
112
|
function readSkillInfo(skillsRoot, name) {
|
|
47
113
|
const skillMdPath = path.join(skillsRoot, name, 'SKILL.md');
|
|
@@ -56,30 +122,6 @@ function readSkillInfo(skillsRoot, name) {
|
|
|
56
122
|
path: `${name}/SKILL.md`,
|
|
57
123
|
};
|
|
58
124
|
}
|
|
59
|
-
function parseSkillTarget(target, relpath) {
|
|
60
|
-
const normalizedTarget = normalizeSkillPath(target);
|
|
61
|
-
if (relpath) {
|
|
62
|
-
return { name: normalizedTarget, pathInSkill: relpath };
|
|
63
|
-
}
|
|
64
|
-
const slash = normalizedTarget.indexOf('/');
|
|
65
|
-
if (slash === -1) {
|
|
66
|
-
return { name: normalizedTarget, pathInSkill: '' };
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
name: normalizedTarget.slice(0, slash),
|
|
70
|
-
pathInSkill: normalizedTarget.slice(slash + 1),
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
function normalizeSkillPath(raw) {
|
|
74
|
-
const normalized = raw.trim().replace(/\\/g, '/');
|
|
75
|
-
if (!normalized || normalized.includes('\0')) {
|
|
76
|
-
throw new ArgumentError('Skill path must be non-empty.');
|
|
77
|
-
}
|
|
78
|
-
if (normalized.startsWith('/') || normalized.split('/').some((part) => part === '..')) {
|
|
79
|
-
throw new ArgumentError(`Invalid skill path: ${raw}`, 'Use a path relative to a Webcmd skill directory.');
|
|
80
|
-
}
|
|
81
|
-
return path.posix.normalize(normalized);
|
|
82
|
-
}
|
|
83
125
|
function parseFrontmatter(content) {
|
|
84
126
|
if (!content.startsWith('---\n'))
|
|
85
127
|
return {};
|