fastbrowser_cli 1.0.22 → 1.0.28

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 (35) hide show
  1. package/README.md +55 -10
  2. package/dist/fastbrowser_cli/fastbrowser_cli.js +40 -70
  3. package/dist/fastbrowser_cli/fastbrowser_cli.js.map +1 -1
  4. package/dist/fastbrowser_cli/libs/query-builder.d.ts +17 -0
  5. package/dist/fastbrowser_cli/libs/query-builder.d.ts.map +1 -0
  6. package/dist/fastbrowser_cli/libs/query-builder.js +58 -0
  7. package/dist/fastbrowser_cli/libs/query-builder.js.map +1 -0
  8. package/dist/fastbrowser_cli/libs/server-manager.d.ts +8 -3
  9. package/dist/fastbrowser_cli/libs/server-manager.d.ts.map +1 -1
  10. package/dist/fastbrowser_cli/libs/server-manager.js +30 -15
  11. package/dist/fastbrowser_cli/libs/server-manager.js.map +1 -1
  12. package/dist/fastbrowser_httpd/fastbrowser_httpd.js +9 -6
  13. package/dist/fastbrowser_httpd/fastbrowser_httpd.js.map +1 -1
  14. package/dist/fastbrowser_httpd/libs/routes.d.ts +2 -1
  15. package/dist/fastbrowser_httpd/libs/routes.d.ts.map +1 -1
  16. package/dist/fastbrowser_httpd/libs/routes.js +2 -2
  17. package/dist/fastbrowser_httpd/libs/routes.js.map +1 -1
  18. package/docs/articles/ai_workflow_to_shell_scripts.md +74 -16
  19. package/docs/articles/ai_workflow_to_shell_scripts.notes.md +23 -0
  20. package/docs/articles/ai_workflow_to_shell_scripts.outline.md +64 -0
  21. package/docs/publishing_workflow.md +136 -0
  22. package/examples/post-to-x.sh +3 -0
  23. package/examples/welcometothejungle/fastbrowser_helper.ts +39 -0
  24. package/examples/welcometothejungle/wttj-job.ts +132 -153
  25. package/examples/welcometothejungle/wttj-search.ts +61 -84
  26. package/package.json +48 -47
  27. package/src/fastbrowser_cli/fastbrowser_cli.ts +50 -90
  28. package/src/fastbrowser_cli/libs/query-builder.ts +89 -0
  29. package/src/fastbrowser_cli/libs/server-manager.ts +45 -17
  30. package/src/fastbrowser_httpd/fastbrowser_httpd.ts +14 -6
  31. package/src/fastbrowser_httpd/libs/routes.ts +3 -2
  32. package/tests/http-client.test.ts +63 -0
  33. package/tests/query-builder.test.ts +204 -0
  34. package/tests/server-manager.test.ts +124 -0
  35. package/.playwright-mcp/.gitignore +0 -3
@@ -0,0 +1,204 @@
1
+ // node imports
2
+ import { describe, it } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+
5
+ // local imports
6
+ import { QueryBuilder } from '../src/fastbrowser_cli/libs/query-builder.js';
7
+
8
+ ///////////////////////////////////////////////////////////////////////////////
9
+ ///////////////////////////////////////////////////////////////////////////////
10
+ //
11
+ ///////////////////////////////////////////////////////////////////////////////
12
+ ///////////////////////////////////////////////////////////////////////////////
13
+
14
+ describe('QueryBuilder.buildQuerySelectorsBody', () => {
15
+ describe('--selector list', () => {
16
+ it('builds a single-selector body with defaults', () => {
17
+ const body = QueryBuilder.buildQuerySelectorsBody({
18
+ selector: ['button'],
19
+ });
20
+ assert.deepEqual(body, {
21
+ selectors: [{ selector: 'button', limit: 0, withAncestors: true }],
22
+ });
23
+ });
24
+
25
+ it('builds a multi-selector body', () => {
26
+ const body = QueryBuilder.buildQuerySelectorsBody({
27
+ selector: ['button', 'link'],
28
+ limit: '5',
29
+ });
30
+ assert.deepEqual(body, {
31
+ selectors: [
32
+ { selector: 'button', limit: 5, withAncestors: true },
33
+ { selector: 'link', limit: 5, withAncestors: true },
34
+ ],
35
+ });
36
+ });
37
+
38
+ it('honors --no-with-ancestors (withAncestors === false)', () => {
39
+ const body = QueryBuilder.buildQuerySelectorsBody({
40
+ selector: ['button'],
41
+ withAncestors: false,
42
+ });
43
+ assert.equal(body.selectors[0].withAncestors, false);
44
+ });
45
+
46
+ it('treats omitted withAncestors as true (default)', () => {
47
+ const body = QueryBuilder.buildQuerySelectorsBody({
48
+ selector: ['button'],
49
+ });
50
+ assert.equal(body.selectors[0].withAncestors, true);
51
+ });
52
+
53
+ it('parses --limit as an integer', () => {
54
+ const body = QueryBuilder.buildQuerySelectorsBody({
55
+ selector: ['button'],
56
+ limit: '42',
57
+ });
58
+ assert.equal(body.selectors[0].limit, 42);
59
+ });
60
+
61
+ it('defaults --limit to 0 when not provided', () => {
62
+ const body = QueryBuilder.buildQuerySelectorsBody({
63
+ selector: ['button'],
64
+ });
65
+ assert.equal(body.selectors[0].limit, 0);
66
+ });
67
+ });
68
+
69
+ describe('--selectors-json input', () => {
70
+ it('parses a JSON array and uses it verbatim', () => {
71
+ const body = QueryBuilder.buildQuerySelectorsBody({
72
+ selectorsJson: '[{"selector":"button","limit":3,"withAncestors":false}]',
73
+ });
74
+ assert.deepEqual(body, {
75
+ selectors: [{ selector: 'button', limit: 3, withAncestors: false }],
76
+ });
77
+ });
78
+
79
+ it('takes precedence over --selector when both are given', () => {
80
+ const body = QueryBuilder.buildQuerySelectorsBody({
81
+ selector: ['ignored'],
82
+ selectorsJson: '[{"selector":"button","limit":1,"withAncestors":true}]',
83
+ });
84
+ assert.deepEqual(body.selectors, [
85
+ { selector: 'button', limit: 1, withAncestors: true },
86
+ ]);
87
+ });
88
+
89
+ it('treats empty --selectors-json as not provided', () => {
90
+ const body = QueryBuilder.buildQuerySelectorsBody({
91
+ selector: ['button'],
92
+ selectorsJson: '',
93
+ });
94
+ assert.equal(body.selectors[0].selector, 'button');
95
+ });
96
+ });
97
+
98
+ describe('error cases', () => {
99
+ it('throws when --selectors-json is invalid JSON', () => {
100
+ assert.throws(
101
+ () => QueryBuilder.buildQuerySelectorsBody({ selectorsJson: 'not-json' }),
102
+ /--selectors-json is not valid JSON/,
103
+ );
104
+ });
105
+
106
+ it('throws when --selectors-json is not an array', () => {
107
+ assert.throws(
108
+ () => QueryBuilder.buildQuerySelectorsBody({ selectorsJson: '{"selector":"button"}' }),
109
+ /--selectors-json must be a JSON array/,
110
+ );
111
+ });
112
+
113
+ it('throws when no --selector and no --selectors-json provided', () => {
114
+ assert.throws(
115
+ () => QueryBuilder.buildQuerySelectorsBody({}),
116
+ /At least one --selector or --selectors-json is required/,
117
+ );
118
+ });
119
+
120
+ it('throws when --selector list is empty', () => {
121
+ assert.throws(
122
+ () => QueryBuilder.buildQuerySelectorsBody({ selector: [] }),
123
+ /At least one --selector or --selectors-json is required/,
124
+ );
125
+ });
126
+
127
+ it('throws when --limit is not a number', () => {
128
+ assert.throws(
129
+ () => QueryBuilder.buildQuerySelectorsBody({ selector: ['button'], limit: 'abc' }),
130
+ /Invalid --limit: abc/,
131
+ );
132
+ });
133
+ });
134
+ });
135
+
136
+ describe('QueryBuilder.buildQuerySelectorFirstBody', () => {
137
+ describe('--selector list', () => {
138
+ it('builds a body with default withAncestors=true', () => {
139
+ const body = QueryBuilder.buildQuerySelectorFirstBody({
140
+ selector: ['button'],
141
+ });
142
+ assert.deepEqual(body, {
143
+ selectors: [{ selector: 'button', withAncestors: true }],
144
+ });
145
+ });
146
+
147
+ it('honors --no-with-ancestors', () => {
148
+ const body = QueryBuilder.buildQuerySelectorFirstBody({
149
+ selector: ['button'],
150
+ withAncestors: false,
151
+ });
152
+ assert.equal(body.selectors[0].withAncestors, false);
153
+ });
154
+
155
+ it('builds a multi-selector body preserving order', () => {
156
+ const body = QueryBuilder.buildQuerySelectorFirstBody({
157
+ selector: ['a', 'b', 'c'],
158
+ });
159
+ assert.deepEqual(body.selectors.map((s) => s.selector), ['a', 'b', 'c']);
160
+ });
161
+ });
162
+
163
+ describe('--selectors-json input', () => {
164
+ it('parses a JSON array verbatim', () => {
165
+ const body = QueryBuilder.buildQuerySelectorFirstBody({
166
+ selectorsJson: '[{"selector":"button","withAncestors":false}]',
167
+ });
168
+ assert.deepEqual(body, {
169
+ selectors: [{ selector: 'button', withAncestors: false }],
170
+ });
171
+ });
172
+
173
+ it('takes precedence over --selector when both are given', () => {
174
+ const body = QueryBuilder.buildQuerySelectorFirstBody({
175
+ selector: ['ignored'],
176
+ selectorsJson: '[{"selector":"button","withAncestors":true}]',
177
+ });
178
+ assert.equal(body.selectors[0].selector, 'button');
179
+ });
180
+ });
181
+
182
+ describe('error cases', () => {
183
+ it('throws when --selectors-json is invalid JSON', () => {
184
+ assert.throws(
185
+ () => QueryBuilder.buildQuerySelectorFirstBody({ selectorsJson: '{' }),
186
+ /--selectors-json is not valid JSON/,
187
+ );
188
+ });
189
+
190
+ it('throws when --selectors-json is not an array', () => {
191
+ assert.throws(
192
+ () => QueryBuilder.buildQuerySelectorFirstBody({ selectorsJson: '"oops"' }),
193
+ /--selectors-json must be a JSON array/,
194
+ );
195
+ });
196
+
197
+ it('throws when no selector source is provided', () => {
198
+ assert.throws(
199
+ () => QueryBuilder.buildQuerySelectorFirstBody({}),
200
+ /At least one --selector or --selectors-json is required/,
201
+ );
202
+ });
203
+ });
204
+ });
@@ -0,0 +1,124 @@
1
+ // node imports
2
+ import { describe, it } from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import http from 'node:http';
5
+ import { AddressInfo } from 'node:net';
6
+
7
+ // local imports
8
+ import { ServerManager } from '../src/fastbrowser_cli/libs/server-manager.js';
9
+
10
+ ///////////////////////////////////////////////////////////////////////////////
11
+ ///////////////////////////////////////////////////////////////////////////////
12
+ //
13
+ ///////////////////////////////////////////////////////////////////////////////
14
+ ///////////////////////////////////////////////////////////////////////////////
15
+
16
+ type FakeServer = {
17
+ url: string;
18
+ close: () => Promise<void>;
19
+ };
20
+
21
+ async function startFakeServer(handler: (req: http.IncomingMessage, res: http.ServerResponse) => void): Promise<FakeServer> {
22
+ const server = http.createServer(handler);
23
+ await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
24
+ const address = server.address() as AddressInfo;
25
+ const url = `http://127.0.0.1:${address.port}`;
26
+ const close = (): Promise<void> => new Promise((resolve, reject) => {
27
+ server.close((err) => (err === undefined || err === null ? resolve() : reject(err)));
28
+ });
29
+ return { url, close };
30
+ }
31
+
32
+ describe('ServerManager.status', () => {
33
+ it("returns state 'stopped' when nothing is listening on the URL", async () => {
34
+ // Port 1 is reserved (tcpmux); a connection will be refused.
35
+ const result = await ServerManager.status('http://127.0.0.1:1');
36
+ assert.equal(result.state, 'stopped');
37
+ });
38
+
39
+ it("returns state 'stopped' for an unreachable host (DNS or routing failure)", async () => {
40
+ const result = await ServerManager.status('http://127.0.0.1:9');
41
+ assert.equal(result.state, 'stopped');
42
+ });
43
+
44
+ it("returns state 'running' when /health responds with { ok: true }", async () => {
45
+ const fake = await startFakeServer((req, res) => {
46
+ if (req.url === '/health') {
47
+ res.setHeader('content-type', 'application/json');
48
+ res.end(JSON.stringify({ ok: true }));
49
+ return;
50
+ }
51
+ res.statusCode = 404;
52
+ res.end();
53
+ });
54
+ try {
55
+ const result = await ServerManager.status(fake.url);
56
+ assert.equal(result.state, 'running');
57
+ assert.equal(result.mcpTarget, undefined);
58
+ } finally {
59
+ await fake.close();
60
+ }
61
+ });
62
+
63
+ it('reports mcpTarget from /health payload', async () => {
64
+ const fake = await startFakeServer((req, res) => {
65
+ if (req.url === '/health') {
66
+ res.setHeader('content-type', 'application/json');
67
+ res.end(JSON.stringify({ ok: true, mcpTarget: 'playwright' }));
68
+ return;
69
+ }
70
+ res.statusCode = 404;
71
+ res.end();
72
+ });
73
+ try {
74
+ const result = await ServerManager.status(fake.url);
75
+ assert.equal(result.state, 'running');
76
+ assert.equal(result.mcpTarget, 'playwright');
77
+ } finally {
78
+ await fake.close();
79
+ }
80
+ });
81
+
82
+ it("returns state 'stopped' when /health responds with { ok: false }", async () => {
83
+ const fake = await startFakeServer((_req, res) => {
84
+ res.setHeader('content-type', 'application/json');
85
+ res.end(JSON.stringify({ ok: false }));
86
+ });
87
+ try {
88
+ const result = await ServerManager.status(fake.url);
89
+ assert.equal(result.state, 'stopped');
90
+ } finally {
91
+ await fake.close();
92
+ }
93
+ });
94
+
95
+ it("returns state 'stopped' when /health returns non-2xx", async () => {
96
+ const fake = await startFakeServer((_req, res) => {
97
+ res.statusCode = 500;
98
+ res.end('boom');
99
+ });
100
+ try {
101
+ const result = await ServerManager.status(fake.url);
102
+ assert.equal(result.state, 'stopped');
103
+ } finally {
104
+ await fake.close();
105
+ }
106
+ });
107
+
108
+ it('strips trailing slashes from the URL before calling /health', async () => {
109
+ const seenUrls: string[] = [];
110
+ const fake = await startFakeServer((req, res) => {
111
+ seenUrls.push(req.url ?? '');
112
+ res.setHeader('content-type', 'application/json');
113
+ res.end(JSON.stringify({ ok: true }));
114
+ });
115
+ try {
116
+ const result = await ServerManager.status(`${fake.url}///`);
117
+ assert.equal(result.state, 'running');
118
+ assert.deepEqual(seenUrls, ['/health']);
119
+ } finally {
120
+ await fake.close();
121
+ }
122
+ });
123
+ });
124
+
@@ -1,3 +0,0 @@
1
- # ignore all but .gitignore
2
- *
3
- !.gitignore