@skills-store/rednote 0.1.0

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 ADDED
@@ -0,0 +1,35 @@
1
+ # rednote
2
+
3
+ A Xiaohongshu (RED) automation CLI.
4
+
5
+ ## Run with npx
6
+
7
+ ```bash
8
+ npx -y @skills-store/rednote browser list
9
+ npx -y @skills-store/rednote browser create --name seller-main --browser chrome
10
+ npx -y @skills-store/rednote browser connect --instance seller-main
11
+ npx -y @skills-store/rednote login --instance seller-main
12
+ npx -y @skills-store/rednote search --instance seller-main --keyword 护肤
13
+ npx -y @skills-store/rednote get-feed-detail --instance seller-main --url "https://www.xiaohongshu.com/explore/<id>?xsec_token=<token>"
14
+ npx -y @skills-store/rednote get-profile --instance seller-main --id USER_ID
15
+ ```
16
+
17
+ ## Install globally
18
+
19
+ ```bash
20
+ npm install -g @skills-store/rednote
21
+ ```
22
+
23
+ ## Storage
24
+
25
+ The CLI stores custom browser instances and metadata under:
26
+
27
+ ```text
28
+ ~/.skills-router/rednote/instances
29
+ ```
30
+
31
+ Run this to inspect the current environment and exact resolved paths:
32
+
33
+ ```bash
34
+ npx -y @skills-store/rednote env
35
+ ```
package/bin/rednote.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = path.dirname(__filename);
8
+ const entry = path.resolve(__dirname, '../scripts/index.ts');
9
+
10
+ const child = spawn(process.execPath, ['--experimental-strip-types', entry, ...process.argv.slice(2)], {
11
+ stdio: 'inherit',
12
+ env: process.env,
13
+ });
14
+
15
+ child.on('exit', (code, signal) => {
16
+ if (signal) {
17
+ process.kill(process.pid, signal);
18
+ return;
19
+ }
20
+
21
+ process.exit(code ?? 1);
22
+ });
23
+
24
+ child.on('error', (error) => {
25
+ console.error(error instanceof Error ? error.message : String(error));
26
+ process.exit(1);
27
+ });
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@skills-store/rednote",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "bin": {
7
+ "rednote": "./bin/rednote.js"
8
+ },
9
+ "scripts": {
10
+ "rednote": "node --experimental-strip-types ./scripts/index.ts",
11
+ "browser": "node --experimental-strip-types ./scripts/browser/index.ts",
12
+ "env": "node --experimental-strip-types ./scripts/rednote/env.ts",
13
+ "status": "node --experimental-strip-types ./scripts/rednote/status.ts",
14
+ "check-login": "node --experimental-strip-types ./scripts/rednote/checkLogin.ts",
15
+ "login": "node --experimental-strip-types ./scripts/rednote/login.ts",
16
+ "home": "node --experimental-strip-types ./scripts/rednote/home.ts",
17
+ "search": "node --experimental-strip-types ./scripts/rednote/search.ts",
18
+ "get-feed-detail": "node --experimental-strip-types ./scripts/rednote/getFeedDetail.ts",
19
+ "get-profile": "node --experimental-strip-types ./scripts/rednote/getProfile.ts",
20
+ "bun:rednote": "bun ./scripts/index.ts",
21
+ "bun:browser": "bun ./scripts/browser/index.ts",
22
+ "bun:env": "bun ./scripts/rednote/env.ts",
23
+ "bun:status": "bun ./scripts/rednote/status.ts",
24
+ "bun:check-login": "bun ./scripts/rednote/checkLogin.ts",
25
+ "bun:login": "bun ./scripts/rednote/login.ts",
26
+ "bun:home": "bun ./scripts/rednote/home.ts",
27
+ "bun:search": "bun ./scripts/rednote/search.ts",
28
+ "bun:get-feed-detail": "bun ./scripts/rednote/getFeedDetail.ts",
29
+ "bun:get-profile": "bun ./scripts/rednote/getProfile.ts"
30
+ },
31
+ "dependencies": {
32
+ "cheerio": "^1.2.0",
33
+ "chrome-paths": "^1.0.1",
34
+ "edge-paths": "^3.0.5",
35
+ "pid-port": "^1.0.2",
36
+ "playwright-core": "^1.52.0",
37
+ "ps-list": "^8.1.1"
38
+ },
39
+ "engines": {
40
+ "node": ">=22.6.0",
41
+ "bun": ">=1.0.0"
42
+ },
43
+ "description": "Xiaohongshu automation CLI for Skills Router",
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "files": [
48
+ "bin/rednote.js",
49
+ "scripts",
50
+ "README.md",
51
+ "package.json",
52
+ "tsconfig.json"
53
+ ]
54
+ }
@@ -0,0 +1,218 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import {
4
+ closeProcessesByPid,
5
+ findSpec,
6
+ getCdpWebSocketUrl,
7
+ getPidsListeningOnPort,
8
+ inspectBrowserInstance,
9
+ getRandomAvailablePort,
10
+ launchBrowser,
11
+ removeLockFiles,
12
+ readInstanceStore,
13
+ resolveExecutablePath,
14
+ resolveUserDataDir,
15
+ type ConnectBrowserOptions,
16
+ type ConnectBrowserResult,
17
+ type LastConnectInfo,
18
+ updateInstanceRemoteDebuggingPort,
19
+ updateLastConnect,
20
+ validateInstanceName,
21
+ waitForCdpReady,
22
+ waitForPortToClose,
23
+ } from '../utils/browser-core.ts';
24
+ import {
25
+ parseBrowserCliArgs,
26
+ printConnectBrowserHelp,
27
+ printJson,
28
+ runCli,
29
+ debugLog,
30
+ type BrowserCliValues,
31
+ } from '../utils/browser-cli.ts';
32
+
33
+
34
+ async function resolveBrowserPid(remoteDebuggingPort: number, detectedPid: number | null) {
35
+ if (detectedPid) {
36
+ return detectedPid;
37
+ }
38
+
39
+ const portPids = await getPidsListeningOnPort(remoteDebuggingPort);
40
+ return portPids[0] ?? null;
41
+ }
42
+
43
+ export async function resolveConnectOptions(options: ConnectBrowserOptions & { instanceName?: string }) {
44
+ if (!options.instanceName) {
45
+ return {
46
+ connectOptions: options,
47
+ lastConnect:
48
+ options.userDataDir
49
+ ? null
50
+ : {
51
+ scope: 'default',
52
+ name: options.browser ?? 'chrome',
53
+ browser: options.browser ?? 'chrome',
54
+ } satisfies LastConnectInfo,
55
+ };
56
+ }
57
+
58
+ if (options.userDataDir) {
59
+ throw new Error('Do not combine --instance with --user-data-dir');
60
+ }
61
+
62
+ if (options.browser) {
63
+ throw new Error('Do not combine --instance with --browser');
64
+ }
65
+
66
+ const store = readInstanceStore();
67
+ const instanceName = validateInstanceName(options.instanceName);
68
+ const persisted = store.instances.find((instance) => instance.name === instanceName);
69
+ if (!persisted) {
70
+ throw new Error(`Unknown instance: ${instanceName}`);
71
+ }
72
+
73
+ let remoteDebuggingPort = options.remoteDebuggingPort ?? persisted.remoteDebuggingPort;
74
+ if (!remoteDebuggingPort) {
75
+ remoteDebuggingPort = await getRandomAvailablePort();
76
+ updateInstanceRemoteDebuggingPort(instanceName, remoteDebuggingPort);
77
+ }
78
+
79
+ return {
80
+ connectOptions: {
81
+ ...options,
82
+ browser: persisted.browser,
83
+ userDataDir: persisted.userDataDir,
84
+ remoteDebuggingPort,
85
+ } satisfies ConnectBrowserOptions,
86
+ lastConnect: {
87
+ scope: 'custom',
88
+ name: persisted.name,
89
+ browser: persisted.browser,
90
+ } satisfies LastConnectInfo,
91
+ };
92
+ }
93
+
94
+ export async function initBrowser(options: ConnectBrowserOptions = {}): Promise<ConnectBrowserResult> {
95
+ debugLog('initBrowser', 'start', { options });
96
+ const browserType = options.browser ?? 'chrome';
97
+ const spec = findSpec(browserType);
98
+ const executablePath = options.executablePath || resolveExecutablePath(spec);
99
+ if (!executablePath) {
100
+ throw new Error(`No executable found for ${spec.displayName}`);
101
+ }
102
+
103
+ const userDataDir = resolveUserDataDir(spec, options);
104
+ const remoteDebuggingPort = options.remoteDebuggingPort ?? 9222;
105
+ const connectTimeoutMs = options.connectTimeoutMs ?? 15_000;
106
+ const killTimeoutMs = options.killTimeoutMs ?? 8_000;
107
+ const startupUrl = options.startupUrl ?? 'about:blank';
108
+
109
+ const detectedInstance = await inspectBrowserInstance(spec, executablePath, userDataDir);
110
+ debugLog('initBrowser', 'inspected instance', {
111
+ browserType,
112
+ executablePath,
113
+ userDataDir,
114
+ remoteDebuggingPort,
115
+ detectedInstance,
116
+ });
117
+ const closedPidSet = new Set<number>();
118
+
119
+ const existingWsUrl = await getCdpWebSocketUrl(remoteDebuggingPort);
120
+ debugLog('initBrowser', 'checked existing ws url', { remoteDebuggingPort, existingWsUrl });
121
+ if (existingWsUrl) {
122
+ debugLog('initBrowser', 'reusing existing ws url', { remoteDebuggingPort, existingWsUrl });
123
+ return {
124
+ ok: true,
125
+ type: spec.type,
126
+ executablePath,
127
+ userDataDir,
128
+ remoteDebuggingPort,
129
+ wsUrl: existingWsUrl,
130
+ pid: await resolveBrowserPid(remoteDebuggingPort, detectedInstance.pid),
131
+ };
132
+ }
133
+
134
+ if (detectedInstance.inUse && detectedInstance.remotePort === null) {
135
+ if (!options.force) {
136
+ throw new Error(
137
+ `Browser profile is already in use without a remote debugging port. Re-run with --force to safely close it first: ${detectedInstance.userDataDir}`,
138
+ );
139
+ }
140
+
141
+ if (detectedInstance.pids.length === 0) {
142
+ throw new Error(
143
+ `Browser profile is in use but no running browser PID was found for safe shutdown: ${detectedInstance.userDataDir}`,
144
+ );
145
+ }
146
+ }
147
+
148
+ if (detectedInstance.inUse && detectedInstance.pids.length > 0) {
149
+ debugLog('initBrowser', 'detected in-use profile, closing existing processes', {
150
+ pids: detectedInstance.pids,
151
+ killTimeoutMs,
152
+ remoteDebuggingPort,
153
+ });
154
+ for (const pid of await closeProcessesByPid(detectedInstance.pids, killTimeoutMs)) {
155
+ closedPidSet.add(pid);
156
+ }
157
+ await waitForPortToClose(remoteDebuggingPort, killTimeoutMs);
158
+ } else if (detectedInstance.staleLock) {
159
+ if (!options.force) {
160
+ throw new Error(`Profile is locked by stale files: ${detectedInstance.lockFiles.join(', ')}`);
161
+ }
162
+
163
+ removeLockFiles(detectedInstance.lockFiles);
164
+ }
165
+
166
+ const launched = await launchBrowser(spec, executablePath, userDataDir, remoteDebuggingPort, startupUrl);
167
+ debugLog('initBrowser', 'launched browser', { remoteDebuggingPort, pid: launched.pid ?? null, startupUrl });
168
+
169
+ const wsUrl = await waitForCdpReady(remoteDebuggingPort, connectTimeoutMs);
170
+ debugLog('initBrowser', 'waited for cdp ready', { remoteDebuggingPort, connectTimeoutMs, wsUrl });
171
+ if (!wsUrl) {
172
+ throw new Error(`CDP did not become available on port ${remoteDebuggingPort}`);
173
+ }
174
+
175
+ return {
176
+ ok: true,
177
+ type: spec.type,
178
+ executablePath,
179
+ userDataDir,
180
+ remoteDebuggingPort,
181
+ wsUrl,
182
+ pid: launched.pid ?? await resolveBrowserPid(remoteDebuggingPort, detectedInstance.pid),
183
+ };
184
+ }
185
+
186
+ export async function runConnectBrowserCommand(values: BrowserCliValues) {
187
+ const resolved = await resolveConnectOptions({
188
+ browser: values.browser,
189
+ instanceName: values.instance,
190
+ executablePath: values['executable-path'],
191
+ userDataDir: values['user-data-dir'],
192
+ force: values.force,
193
+ remoteDebuggingPort: values.port ? Number(values.port) : undefined,
194
+ connectTimeoutMs: values.timeout ? Number(values.timeout) : undefined,
195
+ killTimeoutMs: values['kill-timeout'] ? Number(values['kill-timeout']) : undefined,
196
+ startupUrl: values['startup-url'],
197
+ });
198
+ const result = await initBrowser(resolved.connectOptions);
199
+
200
+ if (resolved.lastConnect) {
201
+ updateLastConnect(resolved.lastConnect);
202
+ }
203
+
204
+ printJson(result);
205
+ }
206
+
207
+ async function main() {
208
+ const { values } = parseBrowserCliArgs(process.argv.slice(2));
209
+
210
+ if (values.help) {
211
+ printConnectBrowserHelp();
212
+ return;
213
+ }
214
+
215
+ await runConnectBrowserCommand(values);
216
+ }
217
+
218
+ runCli(import.meta.url, main);
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import {
4
+ customInstanceUserDataDir,
5
+ ensureDir,
6
+ isDefaultInstanceName,
7
+ readInstanceStore,
8
+ type CreateInstanceOptions,
9
+ type CreateInstanceResult,
10
+ type PersistedInstance,
11
+ validateInstanceName,
12
+ writeInstanceStore,
13
+ } from '../utils/browser-core.ts';
14
+ import {
15
+ parseBrowserCliArgs,
16
+ printCreateBrowserHelp,
17
+ printJson,
18
+ runCli,
19
+ type BrowserCliValues,
20
+ } from '../utils/browser-cli.ts';
21
+
22
+ export function createBrowserInstance(options: CreateInstanceOptions): CreateInstanceResult {
23
+ const store = readInstanceStore();
24
+ const name = validateInstanceName(options.name);
25
+ const browser = options.browser ?? 'chrome';
26
+
27
+ if (isDefaultInstanceName(name)) {
28
+ throw new Error(`Instance name is reserved for a default browser: ${name}`);
29
+ }
30
+
31
+ if (store.instances.some((instance) => instance.name === name)) {
32
+ throw new Error(`Instance already exists: ${name}`);
33
+ }
34
+
35
+ const instance: PersistedInstance = {
36
+ name,
37
+ browser,
38
+ userDataDir: ensureDir(customInstanceUserDataDir(name)),
39
+ createdAt: new Date().toISOString(),
40
+ remoteDebuggingPort:
41
+ typeof options.remoteDebuggingPort === 'number' && Number.isInteger(options.remoteDebuggingPort) && options.remoteDebuggingPort > 0
42
+ ? options.remoteDebuggingPort
43
+ : undefined,
44
+ };
45
+
46
+ writeInstanceStore({
47
+ ...store,
48
+ instances: [...store.instances, instance],
49
+ });
50
+
51
+ return {
52
+ ok: true,
53
+ instance,
54
+ };
55
+ }
56
+
57
+ export function runCreateBrowserCommand(values: BrowserCliValues) {
58
+ if (!values.name) {
59
+ throw new Error('Missing required option: --name');
60
+ }
61
+
62
+ const result = createBrowserInstance({
63
+ name: values.name,
64
+ browser: values.browser,
65
+ remoteDebuggingPort: values.port ? Number(values.port) : undefined,
66
+ });
67
+ printJson(result);
68
+ }
69
+
70
+ async function main() {
71
+ const { values } = parseBrowserCliArgs(process.argv.slice(2));
72
+
73
+ if (values.help) {
74
+ printCreateBrowserHelp();
75
+ return;
76
+ }
77
+
78
+ runCreateBrowserCommand(values);
79
+ }
80
+
81
+ runCli(import.meta.url, main);
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import {
4
+ parseBrowserCliArgs,
5
+ printInitBrowserHelp,
6
+ runCli,
7
+ } from '../utils/browser-cli.ts';
8
+
9
+ export async function runBrowserCli(argv: string[] = process.argv.slice(2)) {
10
+ const { values, positionals } = parseBrowserCliArgs(argv);
11
+ const command = positionals[0] || 'list';
12
+
13
+ if (values.help || command === 'help') {
14
+ printInitBrowserHelp();
15
+ return;
16
+ }
17
+
18
+ if (command === 'list') {
19
+ const { runListBrowserCommand } = await import('./list-browser.ts');
20
+ await runListBrowserCommand(values);
21
+ return;
22
+ }
23
+
24
+ if (command === 'create') {
25
+ const { runCreateBrowserCommand } = await import('./create-browser.ts');
26
+ runCreateBrowserCommand(values);
27
+ return;
28
+ }
29
+
30
+ if (command === 'remove') {
31
+ const { runRemoveBrowserCommand } = await import('./remove-browser.ts');
32
+ await runRemoveBrowserCommand(values);
33
+ return;
34
+ }
35
+
36
+ if (command === 'connect') {
37
+ const { runConnectBrowserCommand } = await import('./connect-browser.ts');
38
+ await runConnectBrowserCommand(values);
39
+ return;
40
+ }
41
+
42
+ throw new Error(`Unknown browser command: ${command}`);
43
+ }
44
+
45
+ async function main() {
46
+ await runBrowserCli();
47
+ }
48
+
49
+ runCli(import.meta.url, main);
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import {
4
+ browserSpecs,
5
+ findSpec,
6
+ inspectBrowserInstance,
7
+ isLastConnectMatch,
8
+ readInstanceStore,
9
+ toBrowserInstanceInfo,
10
+ type ListBrowserInstancesResult,
11
+ type ListedBrowserInstance,
12
+ } from '../utils/browser-core.ts';
13
+ import {
14
+ parseBrowserCliArgs,
15
+ printJson,
16
+ printListBrowserHelp,
17
+ runCli,
18
+ type BrowserCliValues,
19
+ } from '../utils/browser-cli.ts';
20
+
21
+ export async function listBrowserInstances(): Promise<ListBrowserInstancesResult> {
22
+ const specs = browserSpecs();
23
+ const store = readInstanceStore();
24
+ const defaultInstances = await Promise.all(
25
+ specs.map(async (spec) => {
26
+ const instance = await inspectBrowserInstance(spec);
27
+ return {
28
+ ...toBrowserInstanceInfo(instance),
29
+ scope: 'default',
30
+ instanceName: spec.type,
31
+ createdAt: null,
32
+ lastConnect: isLastConnectMatch(store.lastConnect, 'default', spec.type, spec.type),
33
+ } satisfies ListedBrowserInstance;
34
+ }),
35
+ );
36
+ const customInstances = await Promise.all(
37
+ store.instances.map(async (persisted) => {
38
+ const spec = findSpec(persisted.browser);
39
+ const instance = await inspectBrowserInstance(spec, undefined, persisted.userDataDir);
40
+ return {
41
+ ...toBrowserInstanceInfo(instance),
42
+ name: persisted.name,
43
+ userDataDir: persisted.userDataDir,
44
+ scope: 'custom',
45
+ instanceName: persisted.name,
46
+ createdAt: persisted.createdAt,
47
+ lastConnect: isLastConnectMatch(store.lastConnect, 'custom', persisted.name, persisted.browser),
48
+ } satisfies ListedBrowserInstance;
49
+ }),
50
+ );
51
+
52
+ return {
53
+ lastConnect: store.lastConnect,
54
+ instances: [...defaultInstances, ...customInstances],
55
+ };
56
+ }
57
+
58
+ export async function runListBrowserCommand(_values?: BrowserCliValues) {
59
+ const instances = await listBrowserInstances();
60
+ printJson(instances);
61
+ }
62
+
63
+ async function main() {
64
+ const { values } = parseBrowserCliArgs(process.argv.slice(2));
65
+
66
+ if (values.help) {
67
+ printListBrowserHelp();
68
+ return;
69
+ }
70
+
71
+ await runListBrowserCommand(values);
72
+ }
73
+
74
+ runCli(import.meta.url, main);
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env -S node --experimental-strip-types
2
+
3
+ import fs from 'node:fs';
4
+ import {
5
+ closeProcessesByPid,
6
+ customInstanceUserDataDir,
7
+ exists,
8
+ findSpec,
9
+ inspectBrowserInstance,
10
+ INSTANCES_DIR,
11
+ isDefaultInstanceName,
12
+ isSubPath,
13
+ normalizePath,
14
+ readInstanceStore,
15
+ type RemoveInstanceOptions,
16
+ type RemoveInstanceResult,
17
+ validateInstanceName,
18
+ waitForPortToClose,
19
+ writeInstanceStore,
20
+ } from '../utils/browser-core.ts';
21
+ import {
22
+ parseBrowserCliArgs,
23
+ printJson,
24
+ printRemoveBrowserHelp,
25
+ runCli,
26
+ type BrowserCliValues,
27
+ } from '../utils/browser-cli.ts';
28
+
29
+ export async function removeBrowserInstance(options: RemoveInstanceOptions): Promise<RemoveInstanceResult> {
30
+ const store = readInstanceStore();
31
+ const name = validateInstanceName(options.name);
32
+ if (isDefaultInstanceName(name)) {
33
+ throw new Error(`Default instance cannot be removed: ${name}`);
34
+ }
35
+
36
+ const instance = store.instances.find((item) => item.name === name);
37
+ if (!instance) {
38
+ throw new Error(`Instance not found: ${name}`);
39
+ }
40
+
41
+ const expectedUserDataDir = customInstanceUserDataDir(name);
42
+ const resolvedUserDataDir = normalizePath(instance.userDataDir);
43
+ if (!isSubPath(INSTANCES_DIR, resolvedUserDataDir) || normalizePath(expectedUserDataDir) !== resolvedUserDataDir) {
44
+ throw new Error(`Refusing to remove unexpected instance directory: ${instance.userDataDir}`);
45
+ }
46
+
47
+ const spec = findSpec(instance.browser);
48
+ const detectedInstance = await inspectBrowserInstance(spec, undefined, resolvedUserDataDir);
49
+ if (detectedInstance.inUse && !options.force) {
50
+ throw new Error(`Instance is currently in use. Re-run with --force: ${name}`);
51
+ }
52
+
53
+ const closedPids: number[] = [];
54
+ if (options.force && detectedInstance.pids.length > 0) {
55
+ for (const pid of await closeProcessesByPid(detectedInstance.pids, 8_000)) {
56
+ closedPids.push(pid);
57
+ }
58
+
59
+ if (detectedInstance.remotePort !== null) {
60
+ await waitForPortToClose(detectedInstance.remotePort, 8_000);
61
+ }
62
+ }
63
+
64
+ const removedDir = exists(resolvedUserDataDir);
65
+ fs.rmSync(resolvedUserDataDir, { recursive: true, force: true });
66
+
67
+ writeInstanceStore({
68
+ ...store,
69
+ lastConnect:
70
+ store.lastConnect?.scope === 'custom' &&
71
+ store.lastConnect.name === instance.name &&
72
+ store.lastConnect.browser === instance.browser
73
+ ? null
74
+ : store.lastConnect,
75
+ instances: store.instances.filter((item) => item.name !== instance.name),
76
+ });
77
+
78
+ return {
79
+ ok: true,
80
+ removedInstance: instance,
81
+ removedDir,
82
+ closedPids,
83
+ };
84
+ }
85
+
86
+ export async function runRemoveBrowserCommand(values: BrowserCliValues) {
87
+ if (!values.name) {
88
+ throw new Error('Missing required option: --name');
89
+ }
90
+
91
+ const result = await removeBrowserInstance({
92
+ name: values.name,
93
+ force: values.force,
94
+ });
95
+ printJson(result);
96
+ }
97
+
98
+ async function main() {
99
+ const { values } = parseBrowserCliArgs(process.argv.slice(2));
100
+
101
+ if (values.help) {
102
+ process.stdout.write(`remove-browser\n\nUsage:\n node --experimental-strip-types ./scripts/browser/remove-browser.ts --name NAME [--force]\n bun ./scripts/browser/remove-browser.ts --name NAME [--force]\n`);
103
+ return;
104
+ }
105
+
106
+ await runRemoveBrowserCommand(values);
107
+ }
108
+
109
+ runCli(import.meta.url, main);