@agentrhq/webcmd 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/README.md +13 -2
  2. package/cli-manifest.json +228 -24
  3. package/clis/chatgpt/ask.js +1 -1
  4. package/clis/chatgpt/ask.test.js +11 -0
  5. package/clis/chatgpt/commands.test.js +102 -1
  6. package/clis/chatgpt/deep-research-result.js +45 -3
  7. package/clis/chatgpt/image.js +22 -2
  8. package/clis/chatgpt/project-file-add.js +24 -1
  9. package/clis/chatgpt/utils.js +258 -52
  10. package/clis/chatgpt/utils.test.js +259 -1
  11. package/clis/claude/ask.js +21 -1
  12. package/clis/facebook/feed.js +110 -24
  13. package/clis/facebook/feed.test.js +62 -0
  14. package/clis/facebook/search.js +185 -38
  15. package/clis/facebook/search.test.js +95 -50
  16. package/clis/instagram/explore.js +30 -12
  17. package/clis/instagram/explore.test.js +77 -0
  18. package/clis/instagram/post.js +14 -1
  19. package/clis/instagram/reel.js +13 -1
  20. package/clis/linkedin/company.js +153 -0
  21. package/clis/linkedin/company.test.js +111 -0
  22. package/clis/linkedin/connections.js +135 -0
  23. package/clis/linkedin/connections.test.js +141 -0
  24. package/clis/mercury/reimbursement-draft.js +12 -1
  25. package/clis/producthunt/hot.js +1 -2
  26. package/clis/spotify/spotify.js +11 -11
  27. package/clis/twitter/article-evaluate.test.js +27 -0
  28. package/clis/twitter/article.js +73 -16
  29. package/clis/twitter/article.test.js +209 -0
  30. package/clis/twitter/download.js +6 -1
  31. package/clis/twitter/post.js +14 -1
  32. package/clis/twitter/profile.js +1 -1
  33. package/clis/twitter/profile.test.js +8 -0
  34. package/clis/twitter/quote.js +11 -1
  35. package/clis/twitter/reply.js +11 -1
  36. package/dist/src/browser/base-page.d.ts +26 -0
  37. package/dist/src/browser/base-page.js +23 -8
  38. package/dist/src/browser/base-page.test.js +45 -12
  39. package/dist/src/browser/command-catalog.d.ts +6 -0
  40. package/dist/src/browser/command-catalog.js +259 -0
  41. package/dist/src/browser/command-catalog.test.d.ts +1 -0
  42. package/dist/src/browser/command-catalog.test.js +79 -0
  43. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
  44. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
  45. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
  46. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
  47. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
  48. package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
  49. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
  50. package/dist/src/browser/target-resolver.d.ts +1 -0
  51. package/dist/src/browser/target-resolver.js +99 -4
  52. package/dist/src/browser/target-resolver.test.js +141 -1
  53. package/dist/src/build-manifest.d.ts +6 -0
  54. package/dist/src/build-manifest.js +18 -1
  55. package/dist/src/build-manifest.test.js +57 -1
  56. package/dist/src/builtin-command-surface.d.ts +9 -0
  57. package/dist/src/builtin-command-surface.js +16 -0
  58. package/dist/src/check-hosted-contract.test.d.ts +1 -0
  59. package/dist/src/check-hosted-contract.test.js +56 -0
  60. package/dist/src/cli.js +58 -106
  61. package/dist/src/cli.test.js +71 -1
  62. package/dist/src/command-presentation.d.ts +95 -0
  63. package/dist/src/command-presentation.js +486 -0
  64. package/dist/src/command-presentation.test.d.ts +1 -0
  65. package/dist/src/command-presentation.test.js +97 -0
  66. package/dist/src/command-surface.d.ts +43 -0
  67. package/dist/src/command-surface.js +205 -0
  68. package/dist/src/command-surface.test.d.ts +1 -0
  69. package/dist/src/command-surface.test.js +406 -0
  70. package/dist/src/commanderAdapter.d.ts +5 -1
  71. package/dist/src/commanderAdapter.js +12 -55
  72. package/dist/src/commanderAdapter.test.js +15 -3
  73. package/dist/src/community-plugin-sync.d.ts +11 -0
  74. package/dist/src/community-plugin-sync.js +138 -0
  75. package/dist/src/community-plugin-sync.test.d.ts +1 -0
  76. package/dist/src/community-plugin-sync.test.js +123 -0
  77. package/dist/src/completion-fast.d.ts +4 -4
  78. package/dist/src/completion-fast.js +15 -34
  79. package/dist/src/completion-shared.d.ts +4 -0
  80. package/dist/src/completion-shared.js +38 -0
  81. package/dist/src/completion.js +3 -27
  82. package/dist/src/completion.test.js +31 -2
  83. package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
  84. package/dist/src/docs-sync-review-cli.test.js +322 -0
  85. package/dist/src/docs-sync-review.d.ts +117 -0
  86. package/dist/src/docs-sync-review.js +475 -0
  87. package/dist/src/docs-sync-review.test.d.ts +1 -0
  88. package/dist/src/docs-sync-review.test.js +324 -0
  89. package/dist/src/errors.d.ts +12 -3
  90. package/dist/src/errors.js +11 -7
  91. package/dist/src/errors.test.js +14 -1
  92. package/dist/src/execution.d.ts +1 -2
  93. package/dist/src/execution.js +4 -45
  94. package/dist/src/generate-release-notes-cli.test.js +7 -2
  95. package/dist/src/help.d.ts +4 -0
  96. package/dist/src/help.js +50 -255
  97. package/dist/src/help.test.js +27 -1
  98. package/dist/src/hosted/args.d.ts +2 -7
  99. package/dist/src/hosted/args.js +2 -99
  100. package/dist/src/hosted/args.test.js +15 -1
  101. package/dist/src/hosted/availability.d.ts +13 -0
  102. package/dist/src/hosted/availability.js +16 -0
  103. package/dist/src/hosted/availability.test.d.ts +1 -0
  104. package/dist/src/hosted/availability.test.js +191 -0
  105. package/dist/src/hosted/browser-args.d.ts +18 -0
  106. package/dist/src/hosted/browser-args.js +152 -0
  107. package/dist/src/hosted/browser-args.test.d.ts +1 -0
  108. package/dist/src/hosted/browser-args.test.js +182 -0
  109. package/dist/src/hosted/client.d.ts +31 -2
  110. package/dist/src/hosted/client.js +441 -53
  111. package/dist/src/hosted/client.test.js +651 -32
  112. package/dist/src/hosted/config.d.ts +7 -2
  113. package/dist/src/hosted/config.js +26 -4
  114. package/dist/src/hosted/config.test.js +34 -1
  115. package/dist/src/hosted/contract.d.ts +94 -0
  116. package/dist/src/hosted/contract.js +208 -0
  117. package/dist/src/hosted/contract.test.d.ts +1 -0
  118. package/dist/src/hosted/contract.test.js +361 -0
  119. package/dist/src/hosted/credentials.d.ts +38 -0
  120. package/dist/src/hosted/credentials.js +248 -0
  121. package/dist/src/hosted/credentials.test.d.ts +1 -0
  122. package/dist/src/hosted/credentials.test.js +93 -0
  123. package/dist/src/hosted/file-contract.test.d.ts +1 -0
  124. package/dist/src/hosted/file-contract.test.js +174 -0
  125. package/dist/src/hosted/files.d.ts +29 -0
  126. package/dist/src/hosted/files.js +296 -0
  127. package/dist/src/hosted/files.test.d.ts +1 -0
  128. package/dist/src/hosted/files.test.js +231 -0
  129. package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
  130. package/dist/src/hosted/main-lifecycle.test.js +213 -0
  131. package/dist/src/hosted/manifest.d.ts +5 -0
  132. package/dist/src/hosted/manifest.js +21 -66
  133. package/dist/src/hosted/manifest.test.js +120 -2
  134. package/dist/src/hosted/output-parity.test.d.ts +1 -0
  135. package/dist/src/hosted/output-parity.test.js +108 -0
  136. package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
  137. package/dist/src/hosted/root-command-surface.test.js +646 -0
  138. package/dist/src/hosted/runner.d.ts +7 -0
  139. package/dist/src/hosted/runner.js +554 -253
  140. package/dist/src/hosted/runner.test.js +1471 -53
  141. package/dist/src/hosted/setup.d.ts +3 -2
  142. package/dist/src/hosted/setup.js +45 -15
  143. package/dist/src/hosted/setup.test.js +133 -6
  144. package/dist/src/hosted/types.d.ts +101 -23
  145. package/dist/src/main.js +120 -108
  146. package/dist/src/manifest-types.d.ts +2 -0
  147. package/dist/src/output.d.ts +17 -2
  148. package/dist/src/output.js +88 -81
  149. package/dist/src/output.test.js +141 -51
  150. package/dist/src/pipeline/executor.test.js +1 -0
  151. package/dist/src/pipeline/steps/download.test.js +1 -0
  152. package/dist/src/plugin-create-cli.test.d.ts +1 -0
  153. package/dist/src/plugin-create-cli.test.js +37 -0
  154. package/dist/src/plugin-manifest.d.ts +12 -0
  155. package/dist/src/plugin-manifest.js +16 -0
  156. package/dist/src/plugin-manifest.test.js +19 -1
  157. package/dist/src/plugin-scaffold.d.ts +4 -1
  158. package/dist/src/plugin-scaffold.js +4 -1
  159. package/dist/src/plugin-scaffold.test.js +23 -8
  160. package/dist/src/plugin.js +4 -1
  161. package/dist/src/plugin.test.js +13 -0
  162. package/dist/src/registry.d.ts +2 -0
  163. package/dist/src/release-notes.js +12 -4
  164. package/dist/src/release-notes.test.js +27 -15
  165. package/dist/src/root-command-surface.d.ts +31 -0
  166. package/dist/src/root-command-surface.js +106 -0
  167. package/dist/src/serialization.d.ts +1 -16
  168. package/dist/src/serialization.js +5 -55
  169. package/dist/src/stream-write.d.ts +12 -0
  170. package/dist/src/stream-write.js +91 -0
  171. package/dist/src/stream-write.test.d.ts +1 -0
  172. package/dist/src/stream-write.test.js +186 -0
  173. package/dist/src/types.d.ts +5 -0
  174. package/dist/src/utils.d.ts +1 -1
  175. package/dist/src/utils.js +2 -8
  176. package/dist/src/utils.test.js +50 -0
  177. package/hosted-contract.json +36759 -0
  178. package/package.json +6 -1
  179. package/scripts/check-hosted-contract.mjs +108 -0
  180. package/scripts/docs-sync-review.ts +332 -0
  181. package/scripts/sync-community-plugins.ts +14 -0
  182. package/skills/webcmd-autofix/SKILL.md +26 -37
  183. package/skills/webcmd-usage/SKILL.md +7 -0
@@ -20,8 +20,11 @@ describe('release notes helpers', () => {
20
20
  it('detects major releases from semver tag ranges', () => {
21
21
  expect(isMajorRelease({ tag: 'webcmd-v2.0.0', previousTag: 'webcmd-v1.9.9' })).toBe(true);
22
22
  expect(isMajorRelease({ tag: 'v3.1.0', previousTag: 'v2.9.9' })).toBe(true);
23
+ expect(isMajorRelease({ tag: 'webcmd-v0.3.0', previousTag: 'webcmd-v0.2.5' })).toBe(true);
24
+ expect(isMajorRelease({ tag: 'webcmd-v0.1.0', previousTag: 'webcmd-v0.0.9' })).toBe(true);
23
25
  expect(isMajorRelease({ tag: 'webcmd-v2.1.0', previousTag: 'webcmd-v2.0.0' })).toBe(false);
24
- expect(isMajorRelease({ tag: 'webcmd-v0.3.0', previousTag: 'webcmd-v0.2.5' })).toBe(false);
26
+ expect(isMajorRelease({ tag: 'webcmd-v0.3.1', previousTag: 'webcmd-v0.3.0' })).toBe(false);
27
+ expect(isMajorRelease({ tag: 'webcmd-v0.0.1', previousTag: 'webcmd-v0.0.0' })).toBe(false);
25
28
  });
26
29
  it('deduplicates PR author contributors and excludes service accounts', () => {
27
30
  const prs = [
@@ -83,11 +86,19 @@ describe('release notes helpers', () => {
83
86
  },
84
87
  {
85
88
  number: 43,
89
+ title: 'feat: improve release presentation',
90
+ author: { login: 'bob' },
91
+ labels: [{ name: 'feature' }],
92
+ files: [{ path: 'src/release-notes.ts' }],
93
+ url: 'https://github.com/agentrhq/webcmd/pull/43',
94
+ },
95
+ {
96
+ number: 44,
86
97
  title: 'chore: release automation',
87
98
  author: { login: 'github-actions[bot]' },
88
99
  labels: [],
89
100
  files: [{ path: '.github/workflows/release.yml' }],
90
- url: 'https://github.com/agentrhq/webcmd/pull/43',
101
+ url: 'https://github.com/agentrhq/webcmd/pull/44',
91
102
  },
92
103
  ],
93
104
  };
@@ -100,15 +111,16 @@ describe('release notes helpers', () => {
100
111
  const normalized = normalizeReleaseNotes(raw, { context });
101
112
  expect(normalized).toContain('# webcmd v2.0.0: The Command Surface Opens');
102
113
  expect(normalized).toContain('## Contributors');
103
- expect(normalized).toContain('<img src="https://github.com/alice.png?size=64" width="64" height="64" alt="@alice" />');
114
+ expect(normalized).toContain('<img src="https://github.com/alice.png?size=40" width="40" height="40" alt="@alice" /></a> <a href="https://github.com/bob"');
104
115
  expect(normalized).toContain('[@alice](https://github.com/alice)');
116
+ expect(normalized).toContain('[@alice](https://github.com/alice) | [@bob](https://github.com/bob)');
105
117
  expect(normalized).not.toContain('github-actions');
106
118
  });
107
119
  it('uses a deterministic major release title fallback when the model omits one', () => {
108
120
  const context = {
109
- tag: 'webcmd-v2.0.0',
110
- previousTag: 'webcmd-v1.9.9',
111
- currentRef: 'webcmd-v2.0.0',
121
+ tag: 'webcmd-v0.3.0',
122
+ previousTag: 'webcmd-v0.2.5',
123
+ currentRef: 'webcmd-v0.3.0',
112
124
  pullRequests: [
113
125
  {
114
126
  number: 42,
@@ -121,7 +133,7 @@ describe('release notes helpers', () => {
121
133
  ],
122
134
  };
123
135
  const normalized = normalizeReleaseNotes('## Highlights\n- Added a GitHub adapter.', { context });
124
- expect(normalized).toContain('# webcmd v2.0.0: The Command Surface Expands');
136
+ expect(normalized).toContain('# webcmd v0.3.0: The Command Surface Expands');
125
137
  });
126
138
  it('does not add major release title treatment to minor releases', () => {
127
139
  const context = {
@@ -172,7 +184,7 @@ describe('release notes helpers', () => {
172
184
  '- Improved district checkout.',
173
185
  '',
174
186
  '## Contributors',
175
- '<a href="https://github.com/alice" title="@alice"><img src="https://github.com/alice.png?size=64" width="64" height="64" alt="@alice" /></a>',
187
+ '<a href="https://github.com/alice" title="@alice"><img src="https://github.com/alice.png?size=40" width="40" height="40" alt="@alice" /></a>',
176
188
  '',
177
189
  '[@alice](https://github.com/alice)',
178
190
  ].join('\n');
@@ -189,8 +201,8 @@ describe('release notes helpers', () => {
189
201
  });
190
202
  it('builds a prompt grounded in the exact release range and PR list', () => {
191
203
  const context = {
192
- tag: 'v0.2.0',
193
- previousTag: 'v0.1.1',
204
+ tag: 'v0.2.1',
205
+ previousTag: 'v0.2.0',
194
206
  currentRef: 'abc123',
195
207
  pullRequests: [
196
208
  {
@@ -206,7 +218,7 @@ describe('release notes helpers', () => {
206
218
  ],
207
219
  };
208
220
  const prompt = buildReleaseNotesPrompt(context);
209
- expect(prompt).toContain('v0.1.1...abc123');
221
+ expect(prompt).toContain('v0.2.0...abc123');
210
222
  expect(prompt).toContain('PR #42: feat: add docs scaffold');
211
223
  expect(prompt).toContain('docs/docs.json');
212
224
  expect(prompt).toContain('## Highlights');
@@ -222,9 +234,9 @@ describe('release notes helpers', () => {
222
234
  });
223
235
  it('asks for a tasteful H1 title only when building major release notes', () => {
224
236
  const context = {
225
- tag: 'webcmd-v2.0.0',
226
- previousTag: 'webcmd-v1.9.9',
227
- currentRef: 'webcmd-v2.0.0',
237
+ tag: 'webcmd-v0.3.0',
238
+ previousTag: 'webcmd-v0.2.5',
239
+ currentRef: 'webcmd-v0.3.0',
228
240
  pullRequests: [
229
241
  {
230
242
  number: 42,
@@ -240,7 +252,7 @@ describe('release notes helpers', () => {
240
252
  };
241
253
  const prompt = buildReleaseNotesPrompt(context);
242
254
  expect(prompt).toContain('This is a major release');
243
- expect(prompt).toContain('# webcmd v2.0.0: <Elegant Release Title>');
255
+ expect(prompt).toContain('# webcmd v0.3.0: <Elegant Release Title>');
244
256
  expect(prompt).toContain('grand enough to feel memorable, but polished rather than loud');
245
257
  });
246
258
  });
@@ -0,0 +1,31 @@
1
+ import { Command } from 'commander';
2
+ export declare const ROOT_PROFILE_FLAGS = "--profile <name>";
3
+ export declare const ROOT_PROFILE_DESCRIPTION = "Chrome profile/context alias for browser runtime commands";
4
+ export declare const COMPLETION_SENTINEL = "--get-completions";
5
+ /**
6
+ * Configure the structural root options shared by the local and hosted CLI.
7
+ * Keeping this in one place makes Commander the source of truth for attached
8
+ * values, short-option clusters, missing values, and help/version precedence.
9
+ */
10
+ export declare function configureRootCommandSurface(program: Command): Command;
11
+ export type HostedRootCommandSurface = {
12
+ kind: 'help';
13
+ exitCode: number;
14
+ } | {
15
+ kind: 'version';
16
+ output: string;
17
+ } | {
18
+ kind: 'completion';
19
+ argv: string[];
20
+ } | {
21
+ kind: 'dispatch';
22
+ argv: string[];
23
+ profile?: string;
24
+ literal: boolean;
25
+ };
26
+ /**
27
+ * Parse only the root command surface without registering or discovering local
28
+ * commands. The completion/version checks reproduce the entry-point fast paths
29
+ * in main.ts; all remaining root grammar is delegated to Commander itself.
30
+ */
31
+ export declare function parseHostedRootCommandSurface(argv: readonly string[]): HostedRootCommandSurface;
@@ -0,0 +1,106 @@
1
+ import { Command, CommanderError } from 'commander';
2
+ import { CommanderStructuralError } from './command-surface.js';
3
+ import { PKG_VERSION } from './version.js';
4
+ export const ROOT_PROFILE_FLAGS = '--profile <name>';
5
+ export const ROOT_PROFILE_DESCRIPTION = 'Chrome profile/context alias for browser runtime commands';
6
+ export const COMPLETION_SENTINEL = '--get-completions';
7
+ /**
8
+ * Configure the structural root options shared by the local and hosted CLI.
9
+ * Keeping this in one place makes Commander the source of truth for attached
10
+ * values, short-option clusters, missing values, and help/version precedence.
11
+ */
12
+ export function configureRootCommandSurface(program) {
13
+ return program
14
+ .version(PKG_VERSION)
15
+ .option(ROOT_PROFILE_FLAGS, ROOT_PROFILE_DESCRIPTION)
16
+ .enablePositionalOptions();
17
+ }
18
+ /**
19
+ * Parse only the root command surface without registering or discovering local
20
+ * commands. The completion/version checks reproduce the entry-point fast paths
21
+ * in main.ts; all remaining root grammar is delegated to Commander itself.
22
+ */
23
+ export function parseHostedRootCommandSurface(argv) {
24
+ const input = [...argv];
25
+ // main.ts checks an exact first-token version before its completion scan.
26
+ if (input[0] === '--version' || input[0] === '-V') {
27
+ return { kind: 'version', output: `${PKG_VERSION}\n` };
28
+ }
29
+ // The local completion path scans the complete raw argv before discovery or
30
+ // Commander parsing, including after `--` and malformed root options.
31
+ if (input.includes(COMPLETION_SENTINEL)) {
32
+ return { kind: 'completion', argv: input };
33
+ }
34
+ let stdout = '';
35
+ let stderr = '';
36
+ const boundary = findRootCommandBoundary(input);
37
+ const root = configureRootCommandSurface(new Command('webcmd'))
38
+ .exitOverride()
39
+ .configureOutput({
40
+ writeOut: value => { stdout += value; },
41
+ writeErr: value => { stderr += value; },
42
+ });
43
+ if (boundary.commandIndex !== undefined) {
44
+ // Register only the token Commander needs in order to stop root option
45
+ // parsing. This is structural scaffolding, not adapter discovery.
46
+ root.command(input[boundary.commandIndex])
47
+ .helpOption(false)
48
+ .allowUnknownOption(true)
49
+ .allowExcessArguments(true)
50
+ .argument('[commandArgs...]')
51
+ .exitOverride()
52
+ .configureOutput({ writeOut: () => undefined, writeErr: () => undefined });
53
+ }
54
+ const structuralArgv = boundary.commandIndex !== undefined
55
+ ? input
56
+ : boundary.separatorIndex !== undefined
57
+ ? input.slice(0, boundary.separatorIndex + 1)
58
+ : input;
59
+ try {
60
+ root.parse(structuralArgv, { from: 'user' });
61
+ }
62
+ catch (error) {
63
+ if (!(error instanceof CommanderError))
64
+ throw error;
65
+ if (error.code === 'commander.helpDisplayed')
66
+ return { kind: 'help', exitCode: error.exitCode };
67
+ if (error.code === 'commander.version') {
68
+ return { kind: 'version', output: stdout || `${PKG_VERSION}\n` };
69
+ }
70
+ throw new CommanderStructuralError(stderr || `${error.message}\n`, error.exitCode);
71
+ }
72
+ const profile = root.opts().profile;
73
+ if (boundary.commandIndex === undefined && boundary.separatorIndex === undefined)
74
+ return { kind: 'help', exitCode: 1 };
75
+ const literal = boundary.separatorIndex !== undefined;
76
+ const parsedArgv = boundary.commandIndex !== undefined
77
+ ? input.slice(boundary.commandIndex)
78
+ : input.slice(boundary.separatorIndex + 1);
79
+ if (parsedArgv.length === 0)
80
+ return { kind: 'help', exitCode: 1 };
81
+ return {
82
+ kind: 'dispatch',
83
+ argv: parsedArgv,
84
+ ...(profile !== undefined ? { profile } : {}),
85
+ literal,
86
+ };
87
+ }
88
+ /** Locates the undiscovered command token while respecting root value options. */
89
+ function findRootCommandBoundary(argv) {
90
+ for (let index = 0; index < argv.length; index += 1) {
91
+ const token = argv[index];
92
+ if (token === '--profile') {
93
+ // Commander requires and consumes the next token even when it is `--` or
94
+ // starts with a dash. Structural failures have already been reported.
95
+ index += 1;
96
+ continue;
97
+ }
98
+ if (token.startsWith('--profile='))
99
+ continue;
100
+ if (token === '--')
101
+ return { separatorIndex: index };
102
+ if (!token.startsWith('-') || token === '-')
103
+ return { commandIndex: index };
104
+ }
105
+ return {};
106
+ }
@@ -18,22 +18,7 @@ export type SerializedArg = {
18
18
  /** Stable arg schema — every field is always present (no sparse objects). */
19
19
  export declare function serializeArg(a: Arg): SerializedArg;
20
20
  /** Full command metadata for structured output (json/yaml). */
21
- export declare function serializeCommand(cmd: CliCommand): {
22
- command: string;
23
- site: string;
24
- name: string;
25
- aliases: string[];
26
- description: string;
27
- access: import("./registry.js").CommandAccess;
28
- strategy: string;
29
- browser: boolean;
30
- args: SerializedArg[];
31
- columns: string[];
32
- domain: string | null;
33
- example: string;
34
- defaultFormat: "table" | "plain" | "json" | "yaml" | "yml" | "md" | "markdown" | "csv" | null;
35
- siteSession: import("./registry.js").SiteSessionMode | null;
36
- };
21
+ export declare function serializeCommand(cmd: CliCommand): Record<string, unknown>;
37
22
  /** Human-readable arg summary: `<required> [optional]` style. */
38
23
  export declare function formatArgSummary(args: Arg[]): string;
39
24
  /** Agent-facing canonical invocation. Adapter authors may override with `example`. */
@@ -4,78 +4,28 @@
4
4
  * Used by the `list` command, Commander --help, and build-manifest.
5
5
  * Separated from registry.ts to keep the registry focused on types + registration.
6
6
  */
7
- import { fullName, strategyLabel } from './registry.js';
8
- import { CLI_COMMAND } from './brand.js';
7
+ import { commandListRows, formatArgumentSummary, formatPresentableCommandExample, serializePresentableArg, toPresentableCommand, } from './command-presentation.js';
9
8
  /** Stable arg schema — every field is always present (no sparse objects). */
10
9
  export function serializeArg(a) {
11
- return {
12
- name: a.name,
13
- type: a.type ?? 'string',
14
- required: !!a.required,
15
- valueRequired: !!a.valueRequired,
16
- positional: !!a.positional,
17
- choices: a.choices ?? [],
18
- default: a.default ?? null,
19
- help: a.help ?? '',
20
- };
10
+ return serializePresentableArg(a);
21
11
  }
22
12
  /** Full command metadata for structured output (json/yaml). */
23
13
  export function serializeCommand(cmd) {
24
- return {
25
- command: fullName(cmd),
26
- site: cmd.site,
27
- name: cmd.name,
28
- aliases: cmd.aliases ?? [],
29
- description: cmd.description,
30
- access: cmd.access,
31
- strategy: strategyLabel(cmd),
32
- browser: !!cmd.browser,
33
- args: cmd.args.map(serializeArg),
34
- columns: cmd.columns ?? [],
35
- domain: cmd.domain ?? null,
36
- example: formatCommandExample(cmd),
37
- defaultFormat: cmd.defaultFormat ?? null,
38
- siteSession: cmd.siteSession ?? null,
39
- };
14
+ return commandListRows([toPresentableCommand(cmd)], true)[0];
40
15
  }
41
16
  // ── Formatting ──────────────────────────────────────────────────────────────
42
17
  /** Human-readable arg summary: `<required> [optional]` style. */
43
18
  export function formatArgSummary(args) {
44
- return args
45
- .map(a => {
46
- if (a.positional)
47
- return a.required ? `<${a.name}>` : `[${a.name}]`;
48
- return a.required ? `--${a.name}` : `[--${a.name}]`;
49
- })
50
- .join(' ');
19
+ return formatArgumentSummary(args);
51
20
  }
52
21
  function summarizeChoices(choices) {
53
22
  if (choices.length <= 4)
54
23
  return choices.join(', ');
55
24
  return `${choices.slice(0, 4).join(', ')}, ... (+${choices.length - 4} more)`;
56
25
  }
57
- function formatValuePlaceholder(name) {
58
- return `<${name}>`;
59
- }
60
26
  /** Agent-facing canonical invocation. Adapter authors may override with `example`. */
61
27
  export function formatCommandExample(cmd) {
62
- if (cmd.example?.trim())
63
- return cmd.example.trim();
64
- const parts = [CLI_COMMAND, cmd.site, cmd.name];
65
- for (const arg of cmd.args) {
66
- if (arg.positional && arg.required) {
67
- parts.push(formatValuePlaceholder(arg.name));
68
- }
69
- }
70
- for (const arg of cmd.args) {
71
- if (arg.positional || !arg.required)
72
- continue;
73
- parts.push(`--${arg.name}`);
74
- if (arg.type !== 'bool' && arg.type !== 'boolean')
75
- parts.push(formatValuePlaceholder(arg.name));
76
- }
77
- parts.push('-f', 'yaml');
78
- return parts.join(' ');
28
+ return formatPresentableCommandExample(toPresentableCommand(cmd));
79
29
  }
80
30
  /** Generate the --help appendix showing registry metadata not exposed by Commander. */
81
31
  export function formatRegistryHelpText(cmd) {
@@ -0,0 +1,12 @@
1
+ /** A write failure that must not be mistaken for a command/runtime failure. */
2
+ export declare class StreamWriteError extends Error {
3
+ readonly cause: unknown;
4
+ constructor(cause: unknown);
5
+ }
6
+ /**
7
+ * Write one complete chunk without taking ownership of the stream.
8
+ *
9
+ * Resolution requires both the write callback and, when write() reports
10
+ * backpressure, the matching drain event. The stream is never ended.
11
+ */
12
+ export declare function writeToStream(stream: NodeJS.WritableStream, chunk: string | Uint8Array): Promise<void>;
@@ -0,0 +1,91 @@
1
+ /** A write failure that must not be mistaken for a command/runtime failure. */
2
+ export class StreamWriteError extends Error {
3
+ cause;
4
+ constructor(cause) {
5
+ super(cause instanceof Error ? cause.message : String(cause));
6
+ this.name = 'StreamWriteError';
7
+ this.cause = cause;
8
+ }
9
+ }
10
+ /**
11
+ * Write one complete chunk without taking ownership of the stream.
12
+ *
13
+ * Resolution requires both the write callback and, when write() reports
14
+ * backpressure, the matching drain event. The stream is never ended.
15
+ */
16
+ export function writeToStream(stream, chunk) {
17
+ return new Promise((resolve, reject) => {
18
+ const prematureCloseError = new Error('Writable stream closed before the write completed');
19
+ const streamState = stream;
20
+ if (streamState.closed || streamState.destroyed) {
21
+ reject(new StreamWriteError(prematureCloseError));
22
+ return;
23
+ }
24
+ let writeReturned = false;
25
+ let callbackComplete = false;
26
+ let requiresDrain = false;
27
+ let drainSeen = false;
28
+ let settled = false;
29
+ let pendingCallbackError;
30
+ let pendingCallbackFailure;
31
+ const cleanup = () => {
32
+ stream.removeListener('error', onError);
33
+ stream.removeListener('drain', onDrain);
34
+ stream.removeListener('close', onClose);
35
+ if (pendingCallbackFailure) {
36
+ clearImmediate(pendingCallbackFailure);
37
+ pendingCallbackFailure = undefined;
38
+ }
39
+ };
40
+ const fail = (error) => {
41
+ if (settled)
42
+ return;
43
+ settled = true;
44
+ cleanup();
45
+ reject(error instanceof StreamWriteError ? error : new StreamWriteError(error));
46
+ };
47
+ const failFromCallback = (error) => {
48
+ if (settled)
49
+ return;
50
+ pendingCallbackError ??= error;
51
+ // A real Node Writable reports an _write callback failure to the public
52
+ // write callback and then emits `error`. Keep the error listener until
53
+ // that paired event has had a chance to arrive, while still bounding
54
+ // non-standard streams that only invoke the callback.
55
+ pendingCallbackFailure ??= setImmediate(() => fail(pendingCallbackError));
56
+ };
57
+ const finish = () => {
58
+ if (settled || !writeReturned || !callbackComplete || (requiresDrain && !drainSeen))
59
+ return;
60
+ settled = true;
61
+ cleanup();
62
+ resolve();
63
+ };
64
+ const onError = (error) => fail(error);
65
+ const onClose = () => fail(pendingCallbackError ?? prematureCloseError);
66
+ const onDrain = () => {
67
+ drainSeen = true;
68
+ finish();
69
+ };
70
+ stream.once('error', onError);
71
+ stream.once('drain', onDrain);
72
+ stream.once('close', onClose);
73
+ try {
74
+ requiresDrain = stream.write(chunk, (error) => {
75
+ if (error) {
76
+ failFromCallback(error);
77
+ return;
78
+ }
79
+ callbackComplete = true;
80
+ finish();
81
+ }) === false;
82
+ writeReturned = true;
83
+ if (!requiresDrain)
84
+ stream.removeListener('drain', onDrain);
85
+ finish();
86
+ }
87
+ catch (error) {
88
+ fail(error);
89
+ }
90
+ });
91
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,186 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Writable } from 'node:stream';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { writeToStream } from './stream-write.js';
5
+ class ManualWritable extends Writable {
6
+ chunks = [];
7
+ callbacks = [];
8
+ constructor() {
9
+ super({ highWaterMark: 1 });
10
+ }
11
+ _write(chunk, _encoding, callback) {
12
+ this.chunks.push(Buffer.from(chunk));
13
+ this.callbacks.push(callback);
14
+ }
15
+ release(error) {
16
+ const callback = this.callbacks.shift();
17
+ if (!callback)
18
+ throw new Error('No manual write callback is pending');
19
+ callback(error);
20
+ }
21
+ }
22
+ class SyntheticBackpressureStream extends EventEmitter {
23
+ callback;
24
+ end = vi.fn();
25
+ write(_chunk, callback) {
26
+ this.callback = callback;
27
+ return false;
28
+ }
29
+ }
30
+ class SyntheticBackpressureWritable extends Writable {
31
+ callback;
32
+ write(_chunk, encodingOrCallback, callback) {
33
+ this.callback = typeof encodingOrCallback === 'function' ? encodingOrCallback : callback;
34
+ return false;
35
+ }
36
+ _write(_chunk, _encoding, callback) {
37
+ callback();
38
+ }
39
+ }
40
+ async function within(promise, milliseconds = 500) {
41
+ let timer;
42
+ try {
43
+ return await Promise.race([
44
+ promise,
45
+ new Promise((_resolve, reject) => {
46
+ timer = setTimeout(() => reject(new Error(`promise did not settle within ${milliseconds}ms`)), milliseconds);
47
+ }),
48
+ ]);
49
+ }
50
+ finally {
51
+ if (timer)
52
+ clearTimeout(timer);
53
+ }
54
+ }
55
+ describe('writeToStream', () => {
56
+ it('waits for both the write callback and backpressure drain', async () => {
57
+ const stream = new SyntheticBackpressureStream();
58
+ let settled = false;
59
+ const write = writeToStream(stream, 'payload').then(() => {
60
+ settled = true;
61
+ });
62
+ stream.callback?.();
63
+ await Promise.resolve();
64
+ expect(settled).toBe(false);
65
+ stream.emit('drain');
66
+ await write;
67
+ expect(settled).toBe(true);
68
+ expect(stream.end).not.toHaveBeenCalled();
69
+ });
70
+ it('preserves ordering across multiple awaited writes', async () => {
71
+ const stream = new ManualWritable();
72
+ let settled = false;
73
+ const writes = (async () => {
74
+ await writeToStream(stream, 'first');
75
+ await writeToStream(stream, 'second');
76
+ settled = true;
77
+ })();
78
+ expect(stream.callbacks).toHaveLength(1);
79
+ stream.release();
80
+ await new Promise(resolve => setImmediate(resolve));
81
+ expect(stream.callbacks).toHaveLength(1);
82
+ expect(Buffer.concat(stream.chunks).toString('utf8')).toBe('firstsecond');
83
+ expect(settled).toBe(false);
84
+ stream.release();
85
+ await writes;
86
+ expect(settled).toBe(true);
87
+ });
88
+ it('writes and drains a value larger than one MiB', async () => {
89
+ const stream = new ManualWritable();
90
+ const payload = Buffer.alloc(1024 * 1024 + 17, 0x61);
91
+ const write = writeToStream(stream, payload);
92
+ expect(stream.chunks[0]).toHaveLength(payload.length);
93
+ stream.release();
94
+ await write;
95
+ expect(Buffer.concat(stream.chunks).equals(payload)).toBe(true);
96
+ });
97
+ it('rejects an error emitted before drain without hanging', async () => {
98
+ const stream = new SyntheticBackpressureStream();
99
+ const write = writeToStream(stream, 'payload');
100
+ stream.emit('error', new Error('stream failed before drain'));
101
+ await expect(write).rejects.toThrow('stream failed before drain');
102
+ });
103
+ it('rejects a real Writable callback error without an uncaught error event', async () => {
104
+ const stream = new ManualWritable();
105
+ const write = writeToStream(stream, 'payload');
106
+ stream.release(new Error('real writable callback failed'));
107
+ await expect(write).rejects.toThrow('real writable callback failed');
108
+ await new Promise(resolve => setImmediate(resolve));
109
+ });
110
+ it('rejects a callback error delivered after drain without double resolution', async () => {
111
+ const stream = new SyntheticBackpressureStream();
112
+ const write = writeToStream(stream, 'payload');
113
+ stream.emit('drain');
114
+ stream.callback?.(new Error('stream failed after drain'));
115
+ await expect(write).rejects.toThrow('stream failed after drain');
116
+ });
117
+ it('rejects when a real Writable is destroyed before its write callback', async () => {
118
+ const stream = new ManualWritable();
119
+ const write = writeToStream(stream, 'payload');
120
+ stream.destroy();
121
+ await expect(within(write)).rejects.toThrow('closed before the write completed');
122
+ expect(stream.listenerCount('close')).toBe(0);
123
+ expect(stream.listenerCount('drain')).toBe(0);
124
+ expect(stream.listenerCount('error')).toBe(0);
125
+ });
126
+ it('rejects when close occurs after callback but before required drain', async () => {
127
+ const stream = new SyntheticBackpressureWritable();
128
+ const write = writeToStream(stream, 'payload');
129
+ stream.callback?.();
130
+ stream.destroy();
131
+ await expect(within(write)).rejects.toThrow('closed before the write completed');
132
+ expect(stream.listenerCount('close')).toBe(0);
133
+ expect(stream.listenerCount('drain')).toBe(0);
134
+ expect(stream.listenerCount('error')).toBe(0);
135
+ });
136
+ it('rejects an already-destroyed Writable without attempting a write', async () => {
137
+ const stream = new ManualWritable();
138
+ stream.destroy();
139
+ await new Promise(resolve => stream.once('close', resolve));
140
+ await expect(within(writeToStream(stream, 'payload'))).rejects.toThrow('closed before the write completed');
141
+ expect(stream.chunks).toHaveLength(0);
142
+ expect(stream.listenerCount('close')).toBe(0);
143
+ expect(stream.listenerCount('drain')).toBe(0);
144
+ expect(stream.listenerCount('error')).toBe(0);
145
+ });
146
+ it('ignores a late callback after close has already rejected the write', async () => {
147
+ const stream = new ManualWritable();
148
+ const write = writeToStream(stream, 'payload');
149
+ stream.destroy();
150
+ await expect(within(write)).rejects.toThrow('closed before the write completed');
151
+ expect(() => {
152
+ stream.emit('drain');
153
+ stream.release(new Error('late callback failure'));
154
+ }).not.toThrow();
155
+ await new Promise(resolve => setImmediate(resolve));
156
+ expect(stream.listenerCount('close')).toBe(0);
157
+ expect(stream.listenerCount('drain')).toBe(0);
158
+ expect(stream.listenerCount('error')).toBe(0);
159
+ });
160
+ it('settles once with the original error when error and close race', async () => {
161
+ const stream = new ManualWritable();
162
+ const write = writeToStream(stream, 'payload');
163
+ stream.destroy(new Error('destroy race failed'));
164
+ await expect(within(write)).rejects.toThrow('destroy race failed');
165
+ await new Promise(resolve => setImmediate(resolve));
166
+ expect(stream.listenerCount('close')).toBe(0);
167
+ expect(stream.listenerCount('drain')).toBe(0);
168
+ expect(stream.listenerCount('error')).toBe(0);
169
+ });
170
+ it('does not let close after success change the resolved result', async () => {
171
+ const stream = new SyntheticBackpressureWritable();
172
+ const end = vi.spyOn(stream, 'end');
173
+ const destroy = vi.spyOn(stream, 'destroy');
174
+ const write = writeToStream(stream, 'payload');
175
+ stream.emit('drain');
176
+ stream.callback?.();
177
+ await expect(within(write)).resolves.toBeUndefined();
178
+ expect(end).not.toHaveBeenCalled();
179
+ expect(destroy).not.toHaveBeenCalled();
180
+ stream.destroy();
181
+ await new Promise(resolve => setImmediate(resolve));
182
+ expect(stream.listenerCount('close')).toBe(0);
183
+ expect(stream.listenerCount('drain')).toBe(0);
184
+ expect(stream.listenerCount('error')).toBe(0);
185
+ });
186
+ });
@@ -87,6 +87,9 @@ export interface IPage {
87
87
  }): Promise<{
88
88
  matches_n: number;
89
89
  match_level: 'exact' | 'stable' | 'reidentified';
90
+ click_method?: 'cdp' | 'js' | 'ax';
91
+ hit?: 'target' | 'ancestor' | 'other' | 'none';
92
+ retargeted?: boolean;
90
93
  }>;
91
94
  dblClick?(ref: string, opts?: {
92
95
  nth?: number;
@@ -178,6 +181,8 @@ export interface IPage {
178
181
  }): Promise<any>;
179
182
  getFormState(): Promise<any>;
180
183
  wait(options: number | WaitOptions): Promise<void>;
184
+ /** Pure client-side sleep (bare setTimeout, no page evaluation). */
185
+ sleep(seconds: number): Promise<void>;
181
186
  waitForDownload?(pattern?: string, timeoutMs?: number): Promise<BrowserDownloadWaitResult>;
182
187
  tabs(): Promise<any>;
183
188
  closeTab?(target?: number | string): Promise<void>;