@skills-store/rednote 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/rednote.js +16 -24
- package/dist/browser/connect-browser.js +172 -0
- package/dist/browser/create-browser.js +52 -0
- package/dist/browser/index.js +35 -0
- package/dist/browser/list-browser.js +50 -0
- package/dist/browser/remove-browser.js +69 -0
- package/{scripts/index.ts → dist/index.js} +19 -25
- package/dist/rednote/checkLogin.js +139 -0
- package/dist/rednote/env.js +69 -0
- package/dist/rednote/getFeedDetail.js +268 -0
- package/dist/rednote/getProfile.js +327 -0
- package/dist/rednote/home.js +210 -0
- package/dist/rednote/index.js +130 -0
- package/dist/rednote/login.js +109 -0
- package/dist/rednote/output-format.js +116 -0
- package/dist/rednote/publish.js +376 -0
- package/dist/rednote/search.js +207 -0
- package/dist/rednote/status.js +201 -0
- package/dist/utils/browser-cli.js +155 -0
- package/dist/utils/browser-core.js +705 -0
- package/package.json +7 -4
- package/scripts/browser/connect-browser.ts +0 -218
- package/scripts/browser/create-browser.ts +0 -81
- package/scripts/browser/index.ts +0 -49
- package/scripts/browser/list-browser.ts +0 -74
- package/scripts/browser/remove-browser.ts +0 -109
- package/scripts/rednote/checkLogin.ts +0 -171
- package/scripts/rednote/env.ts +0 -79
- package/scripts/rednote/getFeedDetail.ts +0 -351
- package/scripts/rednote/getProfile.ts +0 -420
- package/scripts/rednote/home.ts +0 -316
- package/scripts/rednote/index.ts +0 -122
- package/scripts/rednote/login.ts +0 -142
- package/scripts/rednote/output-format.ts +0 -156
- package/scripts/rednote/post-types.ts +0 -51
- package/scripts/rednote/search.ts +0 -316
- package/scripts/rednote/status.ts +0 -280
- package/scripts/utils/browser-cli.ts +0 -176
- package/scripts/utils/browser-core.ts +0 -906
- package/tsconfig.json +0 -13
- /package/{scripts/rednote/collect.ts → dist/rednote/collect.js} +0 -0
- /package/{scripts/rednote/publish.ts → dist/rednote/post-types.js} +0 -0
package/bin/rednote.js
CHANGED
|
@@ -1,27 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import { runRootCli } from '../dist/index.js';
|
|
3
|
+
import { stringifyError } from '../dist/utils/browser-cli.js';
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
process.exit(code ?? 1);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
child.on('error', (error) => {
|
|
25
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
5
|
+
try {
|
|
6
|
+
await runRootCli(process.argv.slice(2));
|
|
7
|
+
} catch (error) {
|
|
8
|
+
process.stderr.write(
|
|
9
|
+
`${JSON.stringify(
|
|
10
|
+
{
|
|
11
|
+
ok: false,
|
|
12
|
+
error: stringifyError(error),
|
|
13
|
+
},
|
|
14
|
+
null,
|
|
15
|
+
2,
|
|
16
|
+
)}\n`,
|
|
17
|
+
);
|
|
26
18
|
process.exit(1);
|
|
27
|
-
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { closeProcessesByPid, findSpec, getCdpWebSocketUrl, getPidsListeningOnPort, inspectBrowserInstance, getRandomAvailablePort, launchBrowser, removeLockFiles, readInstanceStore, resolveExecutablePath, resolveUserDataDir, updateInstanceRemoteDebuggingPort, updateLastConnect, validateInstanceName, waitForCdpReady, waitForPortToClose } from '../utils/browser-core.js';
|
|
3
|
+
import { parseBrowserCliArgs, printConnectBrowserHelp, printJson, runCli, debugLog } from '../utils/browser-cli.js';
|
|
4
|
+
async function resolveBrowserPid(remoteDebuggingPort, detectedPid) {
|
|
5
|
+
if (detectedPid) {
|
|
6
|
+
return detectedPid;
|
|
7
|
+
}
|
|
8
|
+
const portPids = await getPidsListeningOnPort(remoteDebuggingPort);
|
|
9
|
+
return portPids[0] ?? null;
|
|
10
|
+
}
|
|
11
|
+
export async function resolveConnectOptions(options) {
|
|
12
|
+
if (!options.instanceName) {
|
|
13
|
+
return {
|
|
14
|
+
connectOptions: options,
|
|
15
|
+
lastConnect: options.userDataDir ? null : {
|
|
16
|
+
scope: 'default',
|
|
17
|
+
name: options.browser ?? 'chrome',
|
|
18
|
+
browser: options.browser ?? 'chrome'
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (options.userDataDir) {
|
|
23
|
+
throw new Error('Do not combine --instance with --user-data-dir');
|
|
24
|
+
}
|
|
25
|
+
if (options.browser) {
|
|
26
|
+
throw new Error('Do not combine --instance with --browser');
|
|
27
|
+
}
|
|
28
|
+
const store = readInstanceStore();
|
|
29
|
+
const instanceName = validateInstanceName(options.instanceName);
|
|
30
|
+
const persisted = store.instances.find((instance)=>instance.name === instanceName);
|
|
31
|
+
if (!persisted) {
|
|
32
|
+
throw new Error(`Unknown instance: ${instanceName}`);
|
|
33
|
+
}
|
|
34
|
+
let remoteDebuggingPort = options.remoteDebuggingPort ?? persisted.remoteDebuggingPort;
|
|
35
|
+
if (!remoteDebuggingPort) {
|
|
36
|
+
remoteDebuggingPort = await getRandomAvailablePort();
|
|
37
|
+
updateInstanceRemoteDebuggingPort(instanceName, remoteDebuggingPort);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
connectOptions: {
|
|
41
|
+
...options,
|
|
42
|
+
browser: persisted.browser,
|
|
43
|
+
userDataDir: persisted.userDataDir,
|
|
44
|
+
remoteDebuggingPort
|
|
45
|
+
},
|
|
46
|
+
lastConnect: {
|
|
47
|
+
scope: 'custom',
|
|
48
|
+
name: persisted.name,
|
|
49
|
+
browser: persisted.browser
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export async function initBrowser(options = {}) {
|
|
54
|
+
debugLog('initBrowser', 'start', {
|
|
55
|
+
options
|
|
56
|
+
});
|
|
57
|
+
const browserType = options.browser ?? 'chrome';
|
|
58
|
+
const spec = findSpec(browserType);
|
|
59
|
+
const executablePath = options.executablePath || resolveExecutablePath(spec);
|
|
60
|
+
if (!executablePath) {
|
|
61
|
+
throw new Error(`No executable found for ${spec.displayName}`);
|
|
62
|
+
}
|
|
63
|
+
const userDataDir = resolveUserDataDir(spec, options);
|
|
64
|
+
const remoteDebuggingPort = options.remoteDebuggingPort ?? 9222;
|
|
65
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? 15_000;
|
|
66
|
+
const killTimeoutMs = options.killTimeoutMs ?? 8_000;
|
|
67
|
+
const startupUrl = options.startupUrl ?? 'about:blank';
|
|
68
|
+
const detectedInstance = await inspectBrowserInstance(spec, executablePath, userDataDir);
|
|
69
|
+
debugLog('initBrowser', 'inspected instance', {
|
|
70
|
+
browserType,
|
|
71
|
+
executablePath,
|
|
72
|
+
userDataDir,
|
|
73
|
+
remoteDebuggingPort,
|
|
74
|
+
detectedInstance
|
|
75
|
+
});
|
|
76
|
+
const closedPidSet = new Set();
|
|
77
|
+
const existingWsUrl = await getCdpWebSocketUrl(remoteDebuggingPort);
|
|
78
|
+
debugLog('initBrowser', 'checked existing ws url', {
|
|
79
|
+
remoteDebuggingPort,
|
|
80
|
+
existingWsUrl
|
|
81
|
+
});
|
|
82
|
+
if (existingWsUrl) {
|
|
83
|
+
debugLog('initBrowser', 'reusing existing ws url', {
|
|
84
|
+
remoteDebuggingPort,
|
|
85
|
+
existingWsUrl
|
|
86
|
+
});
|
|
87
|
+
return {
|
|
88
|
+
ok: true,
|
|
89
|
+
type: spec.type,
|
|
90
|
+
executablePath,
|
|
91
|
+
userDataDir,
|
|
92
|
+
remoteDebuggingPort,
|
|
93
|
+
wsUrl: existingWsUrl,
|
|
94
|
+
pid: await resolveBrowserPid(remoteDebuggingPort, detectedInstance.pid)
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (detectedInstance.inUse && detectedInstance.remotePort === null) {
|
|
98
|
+
if (!options.force) {
|
|
99
|
+
throw new Error(`Browser profile is already in use without a remote debugging port. Re-run with --force to safely close it first: ${detectedInstance.userDataDir}`);
|
|
100
|
+
}
|
|
101
|
+
if (detectedInstance.pids.length === 0) {
|
|
102
|
+
throw new Error(`Browser profile is in use but no running browser PID was found for safe shutdown: ${detectedInstance.userDataDir}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (detectedInstance.inUse && detectedInstance.pids.length > 0) {
|
|
106
|
+
debugLog('initBrowser', 'detected in-use profile, closing existing processes', {
|
|
107
|
+
pids: detectedInstance.pids,
|
|
108
|
+
killTimeoutMs,
|
|
109
|
+
remoteDebuggingPort
|
|
110
|
+
});
|
|
111
|
+
for (const pid of (await closeProcessesByPid(detectedInstance.pids, killTimeoutMs))){
|
|
112
|
+
closedPidSet.add(pid);
|
|
113
|
+
}
|
|
114
|
+
await waitForPortToClose(remoteDebuggingPort, killTimeoutMs);
|
|
115
|
+
} else if (detectedInstance.staleLock) {
|
|
116
|
+
if (!options.force) {
|
|
117
|
+
throw new Error(`Profile is locked by stale files: ${detectedInstance.lockFiles.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
removeLockFiles(detectedInstance.lockFiles);
|
|
120
|
+
}
|
|
121
|
+
const launched = await launchBrowser(spec, executablePath, userDataDir, remoteDebuggingPort, startupUrl);
|
|
122
|
+
debugLog('initBrowser', 'launched browser', {
|
|
123
|
+
remoteDebuggingPort,
|
|
124
|
+
pid: launched.pid ?? null,
|
|
125
|
+
startupUrl
|
|
126
|
+
});
|
|
127
|
+
const wsUrl = await waitForCdpReady(remoteDebuggingPort, connectTimeoutMs);
|
|
128
|
+
debugLog('initBrowser', 'waited for cdp ready', {
|
|
129
|
+
remoteDebuggingPort,
|
|
130
|
+
connectTimeoutMs,
|
|
131
|
+
wsUrl
|
|
132
|
+
});
|
|
133
|
+
if (!wsUrl) {
|
|
134
|
+
throw new Error(`CDP did not become available on port ${remoteDebuggingPort}`);
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
ok: true,
|
|
138
|
+
type: spec.type,
|
|
139
|
+
executablePath,
|
|
140
|
+
userDataDir,
|
|
141
|
+
remoteDebuggingPort,
|
|
142
|
+
wsUrl,
|
|
143
|
+
pid: launched.pid ?? await resolveBrowserPid(remoteDebuggingPort, detectedInstance.pid)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export async function runConnectBrowserCommand(values) {
|
|
147
|
+
const resolved = await resolveConnectOptions({
|
|
148
|
+
browser: values.browser,
|
|
149
|
+
instanceName: values.instance,
|
|
150
|
+
executablePath: values['executable-path'],
|
|
151
|
+
userDataDir: values['user-data-dir'],
|
|
152
|
+
force: values.force,
|
|
153
|
+
remoteDebuggingPort: values.port ? Number(values.port) : undefined,
|
|
154
|
+
connectTimeoutMs: values.timeout ? Number(values.timeout) : undefined,
|
|
155
|
+
killTimeoutMs: values['kill-timeout'] ? Number(values['kill-timeout']) : undefined,
|
|
156
|
+
startupUrl: values['startup-url']
|
|
157
|
+
});
|
|
158
|
+
const result = await initBrowser(resolved.connectOptions);
|
|
159
|
+
if (resolved.lastConnect) {
|
|
160
|
+
updateLastConnect(resolved.lastConnect);
|
|
161
|
+
}
|
|
162
|
+
printJson(result);
|
|
163
|
+
}
|
|
164
|
+
async function main() {
|
|
165
|
+
const { values } = parseBrowserCliArgs(process.argv.slice(2));
|
|
166
|
+
if (values.help) {
|
|
167
|
+
printConnectBrowserHelp();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
await runConnectBrowserCommand(values);
|
|
171
|
+
}
|
|
172
|
+
runCli(import.meta.url, main);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { customInstanceUserDataDir, ensureDir, isDefaultInstanceName, readInstanceStore, validateInstanceName, writeInstanceStore } from '../utils/browser-core.js';
|
|
3
|
+
import { parseBrowserCliArgs, printCreateBrowserHelp, printJson, runCli } from '../utils/browser-cli.js';
|
|
4
|
+
export function createBrowserInstance(options) {
|
|
5
|
+
const store = readInstanceStore();
|
|
6
|
+
const name = validateInstanceName(options.name);
|
|
7
|
+
const browser = options.browser ?? 'chrome';
|
|
8
|
+
if (isDefaultInstanceName(name)) {
|
|
9
|
+
throw new Error(`Instance name is reserved for a default browser: ${name}`);
|
|
10
|
+
}
|
|
11
|
+
if (store.instances.some((instance)=>instance.name === name)) {
|
|
12
|
+
throw new Error(`Instance already exists: ${name}`);
|
|
13
|
+
}
|
|
14
|
+
const instance = {
|
|
15
|
+
name,
|
|
16
|
+
browser,
|
|
17
|
+
userDataDir: ensureDir(customInstanceUserDataDir(name)),
|
|
18
|
+
createdAt: new Date().toISOString(),
|
|
19
|
+
remoteDebuggingPort: typeof options.remoteDebuggingPort === 'number' && Number.isInteger(options.remoteDebuggingPort) && options.remoteDebuggingPort > 0 ? options.remoteDebuggingPort : undefined
|
|
20
|
+
};
|
|
21
|
+
writeInstanceStore({
|
|
22
|
+
...store,
|
|
23
|
+
instances: [
|
|
24
|
+
...store.instances,
|
|
25
|
+
instance
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
instance
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function runCreateBrowserCommand(values) {
|
|
34
|
+
if (!values.name) {
|
|
35
|
+
throw new Error('Missing required option: --name');
|
|
36
|
+
}
|
|
37
|
+
const result = createBrowserInstance({
|
|
38
|
+
name: values.name,
|
|
39
|
+
browser: values.browser,
|
|
40
|
+
remoteDebuggingPort: values.port ? Number(values.port) : undefined
|
|
41
|
+
});
|
|
42
|
+
printJson(result);
|
|
43
|
+
}
|
|
44
|
+
async function main() {
|
|
45
|
+
const { values } = parseBrowserCliArgs(process.argv.slice(2));
|
|
46
|
+
if (values.help) {
|
|
47
|
+
printCreateBrowserHelp();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
runCreateBrowserCommand(values);
|
|
51
|
+
}
|
|
52
|
+
runCli(import.meta.url, main);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseBrowserCliArgs, printInitBrowserHelp, runCli } from '../utils/browser-cli.js';
|
|
3
|
+
export async function runBrowserCli(argv = process.argv.slice(2)) {
|
|
4
|
+
const { values, positionals } = parseBrowserCliArgs(argv);
|
|
5
|
+
const command = positionals[0] || 'list';
|
|
6
|
+
if (values.help || command === 'help') {
|
|
7
|
+
printInitBrowserHelp();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (command === 'list') {
|
|
11
|
+
const { runListBrowserCommand } = await import('./list-browser.js');
|
|
12
|
+
await runListBrowserCommand(values);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (command === 'create') {
|
|
16
|
+
const { runCreateBrowserCommand } = await import('./create-browser.js');
|
|
17
|
+
runCreateBrowserCommand(values);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (command === 'remove') {
|
|
21
|
+
const { runRemoveBrowserCommand } = await import('./remove-browser.js');
|
|
22
|
+
await runRemoveBrowserCommand(values);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (command === 'connect') {
|
|
26
|
+
const { runConnectBrowserCommand } = await import('./connect-browser.js');
|
|
27
|
+
await runConnectBrowserCommand(values);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Unknown browser command: ${command}`);
|
|
31
|
+
}
|
|
32
|
+
async function main() {
|
|
33
|
+
await runBrowserCli();
|
|
34
|
+
}
|
|
35
|
+
runCli(import.meta.url, main);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { browserSpecs, findSpec, inspectBrowserInstance, isLastConnectMatch, readInstanceStore, toBrowserInstanceInfo } from '../utils/browser-core.js';
|
|
3
|
+
import { parseBrowserCliArgs, printJson, printListBrowserHelp, runCli } from '../utils/browser-cli.js';
|
|
4
|
+
export async function listBrowserInstances() {
|
|
5
|
+
const specs = browserSpecs();
|
|
6
|
+
const store = readInstanceStore();
|
|
7
|
+
const defaultInstances = await Promise.all(specs.map(async (spec)=>{
|
|
8
|
+
const instance = await inspectBrowserInstance(spec);
|
|
9
|
+
return {
|
|
10
|
+
...toBrowserInstanceInfo(instance),
|
|
11
|
+
scope: 'default',
|
|
12
|
+
instanceName: spec.type,
|
|
13
|
+
createdAt: null,
|
|
14
|
+
lastConnect: isLastConnectMatch(store.lastConnect, 'default', spec.type, spec.type)
|
|
15
|
+
};
|
|
16
|
+
}));
|
|
17
|
+
const customInstances = await Promise.all(store.instances.map(async (persisted)=>{
|
|
18
|
+
const spec = findSpec(persisted.browser);
|
|
19
|
+
const instance = await inspectBrowserInstance(spec, undefined, persisted.userDataDir);
|
|
20
|
+
return {
|
|
21
|
+
...toBrowserInstanceInfo(instance),
|
|
22
|
+
name: persisted.name,
|
|
23
|
+
userDataDir: persisted.userDataDir,
|
|
24
|
+
scope: 'custom',
|
|
25
|
+
instanceName: persisted.name,
|
|
26
|
+
createdAt: persisted.createdAt,
|
|
27
|
+
lastConnect: isLastConnectMatch(store.lastConnect, 'custom', persisted.name, persisted.browser)
|
|
28
|
+
};
|
|
29
|
+
}));
|
|
30
|
+
return {
|
|
31
|
+
lastConnect: store.lastConnect,
|
|
32
|
+
instances: [
|
|
33
|
+
...defaultInstances,
|
|
34
|
+
...customInstances
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export async function runListBrowserCommand(_values) {
|
|
39
|
+
const instances = await listBrowserInstances();
|
|
40
|
+
printJson(instances);
|
|
41
|
+
}
|
|
42
|
+
async function main() {
|
|
43
|
+
const { values } = parseBrowserCliArgs(process.argv.slice(2));
|
|
44
|
+
if (values.help) {
|
|
45
|
+
printListBrowserHelp();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
await runListBrowserCommand(values);
|
|
49
|
+
}
|
|
50
|
+
runCli(import.meta.url, main);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { closeProcessesByPid, customInstanceUserDataDir, exists, findSpec, inspectBrowserInstance, INSTANCES_DIR, isDefaultInstanceName, isSubPath, normalizePath, readInstanceStore, validateInstanceName, waitForPortToClose, writeInstanceStore } from '../utils/browser-core.js';
|
|
4
|
+
import { parseBrowserCliArgs, printJson, printRemoveBrowserHelp, runCli } from '../utils/browser-cli.js';
|
|
5
|
+
export async function removeBrowserInstance(options) {
|
|
6
|
+
const store = readInstanceStore();
|
|
7
|
+
const name = validateInstanceName(options.name);
|
|
8
|
+
if (isDefaultInstanceName(name)) {
|
|
9
|
+
throw new Error(`Default instance cannot be removed: ${name}`);
|
|
10
|
+
}
|
|
11
|
+
const instance = store.instances.find((item)=>item.name === name);
|
|
12
|
+
if (!instance) {
|
|
13
|
+
throw new Error(`Instance not found: ${name}`);
|
|
14
|
+
}
|
|
15
|
+
const expectedUserDataDir = customInstanceUserDataDir(name);
|
|
16
|
+
const resolvedUserDataDir = normalizePath(instance.userDataDir);
|
|
17
|
+
if (!isSubPath(INSTANCES_DIR, resolvedUserDataDir) || normalizePath(expectedUserDataDir) !== resolvedUserDataDir) {
|
|
18
|
+
throw new Error(`Refusing to remove unexpected instance directory: ${instance.userDataDir}`);
|
|
19
|
+
}
|
|
20
|
+
const spec = findSpec(instance.browser);
|
|
21
|
+
const detectedInstance = await inspectBrowserInstance(spec, undefined, resolvedUserDataDir);
|
|
22
|
+
if (detectedInstance.inUse && !options.force) {
|
|
23
|
+
throw new Error(`Instance is currently in use. Re-run with --force: ${name}`);
|
|
24
|
+
}
|
|
25
|
+
const closedPids = [];
|
|
26
|
+
if (options.force && detectedInstance.pids.length > 0) {
|
|
27
|
+
for (const pid of (await closeProcessesByPid(detectedInstance.pids, 8_000))){
|
|
28
|
+
closedPids.push(pid);
|
|
29
|
+
}
|
|
30
|
+
if (detectedInstance.remotePort !== null) {
|
|
31
|
+
await waitForPortToClose(detectedInstance.remotePort, 8_000);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const removedDir = exists(resolvedUserDataDir);
|
|
35
|
+
fs.rmSync(resolvedUserDataDir, {
|
|
36
|
+
recursive: true,
|
|
37
|
+
force: true
|
|
38
|
+
});
|
|
39
|
+
writeInstanceStore({
|
|
40
|
+
...store,
|
|
41
|
+
lastConnect: store.lastConnect?.scope === 'custom' && store.lastConnect.name === instance.name && store.lastConnect.browser === instance.browser ? null : store.lastConnect,
|
|
42
|
+
instances: store.instances.filter((item)=>item.name !== instance.name)
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
removedInstance: instance,
|
|
47
|
+
removedDir,
|
|
48
|
+
closedPids
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export async function runRemoveBrowserCommand(values) {
|
|
52
|
+
if (!values.name) {
|
|
53
|
+
throw new Error('Missing required option: --name');
|
|
54
|
+
}
|
|
55
|
+
const result = await removeBrowserInstance({
|
|
56
|
+
name: values.name,
|
|
57
|
+
force: values.force
|
|
58
|
+
});
|
|
59
|
+
printJson(result);
|
|
60
|
+
}
|
|
61
|
+
async function main() {
|
|
62
|
+
const { values } = parseBrowserCliArgs(process.argv.slice(2));
|
|
63
|
+
if (values.help) {
|
|
64
|
+
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`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await runRemoveBrowserCommand(values);
|
|
68
|
+
}
|
|
69
|
+
runCli(import.meta.url, main);
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
#!/usr/bin/env
|
|
2
|
-
|
|
3
|
-
import { runCli } from './utils/browser-cli.ts';
|
|
4
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runCli } from './utils/browser-cli.js';
|
|
5
3
|
function printRootHelp() {
|
|
6
|
-
|
|
4
|
+
process.stdout.write(`rednote
|
|
7
5
|
|
|
8
6
|
Usage:
|
|
9
7
|
npx -y @skills-store/rednote browser <command> [...args]
|
|
@@ -15,6 +13,7 @@ Commands:
|
|
|
15
13
|
status [--instance NAME]
|
|
16
14
|
check-login [--instance NAME]
|
|
17
15
|
login [--instance NAME]
|
|
16
|
+
publish [--instance NAME]
|
|
18
17
|
home [--instance NAME] [--format md|json] [--save [PATH]]
|
|
19
18
|
search [--instance NAME] --keyword TEXT [--format md|json] [--save [PATH]]
|
|
20
19
|
get-feed-detail [--instance NAME] --url URL [--format md|json]
|
|
@@ -25,30 +24,25 @@ Examples:
|
|
|
25
24
|
npx -y @skills-store/rednote browser create --name seller-main --browser chrome
|
|
26
25
|
npx -y @skills-store/rednote browser connect --instance seller-main
|
|
27
26
|
npx -y @skills-store/rednote env
|
|
27
|
+
npx -y @skills-store/rednote publish --instance seller-main --type video --video ./note.mp4 --title 标题 --content 描述
|
|
28
28
|
npx -y @skills-store/rednote search --instance seller-main --keyword 护肤
|
|
29
29
|
`);
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const { runRednoteCli } = await import('./rednote/index.ts');
|
|
47
|
-
await runRednoteCli(argv);
|
|
31
|
+
export async function runRootCli(argv = process.argv.slice(2)) {
|
|
32
|
+
const [command, ...restArgv] = argv;
|
|
33
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
34
|
+
printRootHelp();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (command === 'browser') {
|
|
38
|
+
const { runBrowserCli } = await import('./browser/index.js');
|
|
39
|
+
await runBrowserCli(restArgv);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const { runRednoteCli } = await import('./rednote/index.js');
|
|
43
|
+
await runRednoteCli(argv);
|
|
48
44
|
}
|
|
49
|
-
|
|
50
45
|
async function main() {
|
|
51
|
-
|
|
46
|
+
await runRootCli();
|
|
52
47
|
}
|
|
53
|
-
|
|
54
48
|
runCli(import.meta.url, main);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from 'node:util';
|
|
3
|
+
import { initBrowser, resolveConnectOptions } from '../browser/connect-browser.js';
|
|
4
|
+
import { connectOverCdp, updateLastConnect } from '../utils/browser-core.js';
|
|
5
|
+
import { debugLog, printJson, runCli } from '../utils/browser-cli.js';
|
|
6
|
+
import { resolveStatusTarget } from './status.js';
|
|
7
|
+
function printCheckLoginHelp() {
|
|
8
|
+
process.stdout.write(`rednote check-login
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
npx -y @skills-store/rednote check-login [--instance NAME]
|
|
12
|
+
node --experimental-strip-types ./scripts/rednote/checkLogin.ts --instance NAME
|
|
13
|
+
bun ./scripts/rednote/checkLogin.ts --instance NAME
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--instance NAME Optional. Defaults to the saved lastConnect instance
|
|
17
|
+
-h, --help Show this help
|
|
18
|
+
`);
|
|
19
|
+
}
|
|
20
|
+
export async function createRednoteSession(target) {
|
|
21
|
+
debugLog('checkLogin', 'create session start', {
|
|
22
|
+
target
|
|
23
|
+
});
|
|
24
|
+
const resolved = await resolveConnectOptions(target.scope === 'custom' ? {
|
|
25
|
+
instanceName: target.instanceName
|
|
26
|
+
} : {
|
|
27
|
+
browser: target.browser
|
|
28
|
+
});
|
|
29
|
+
const launched = await initBrowser(resolved.connectOptions);
|
|
30
|
+
debugLog('checkLogin', 'initBrowser resolved', {
|
|
31
|
+
target,
|
|
32
|
+
connectOptions: resolved.connectOptions,
|
|
33
|
+
launched
|
|
34
|
+
});
|
|
35
|
+
if (resolved.lastConnect) {
|
|
36
|
+
updateLastConnect(resolved.lastConnect);
|
|
37
|
+
}
|
|
38
|
+
const browser = await connectOverCdp(launched.wsUrl);
|
|
39
|
+
debugLog('checkLogin', 'connected over cdp', {
|
|
40
|
+
wsUrl: launched.wsUrl,
|
|
41
|
+
remoteDebuggingPort: launched.remoteDebuggingPort
|
|
42
|
+
});
|
|
43
|
+
const browserContext = browser.contexts()[0];
|
|
44
|
+
if (!browserContext) {
|
|
45
|
+
throw new Error(`No browser context found for instance: ${target.instanceName}`);
|
|
46
|
+
}
|
|
47
|
+
const page = browserContext.pages().find((candidate)=>candidate.url().startsWith('https://www.xiaohongshu.com/')) ?? await browserContext.newPage();
|
|
48
|
+
debugLog('checkLogin', 'session page resolved', {
|
|
49
|
+
pageUrl: page.url(),
|
|
50
|
+
totalPages: browserContext.pages().length
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
browser,
|
|
54
|
+
browserContext,
|
|
55
|
+
page
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function disconnectRednoteSession(session) {
|
|
59
|
+
try {
|
|
60
|
+
session.browser._connection?.close();
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
export async function checkRednoteLogin(target, session) {
|
|
64
|
+
const ownsSession = !session;
|
|
65
|
+
const activeSession = session ?? await createRednoteSession(target);
|
|
66
|
+
try {
|
|
67
|
+
debugLog('checkLogin', 'check login start', {
|
|
68
|
+
target,
|
|
69
|
+
reusedSession: Boolean(session)
|
|
70
|
+
});
|
|
71
|
+
const page = activeSession.page;
|
|
72
|
+
if (!page.url().startsWith('https://www.xiaohongshu.com/')) {
|
|
73
|
+
debugLog('checkLogin', 'page is not on xiaohongshu, navigating', {
|
|
74
|
+
currentUrl: page.url()
|
|
75
|
+
});
|
|
76
|
+
await page.goto('https://www.xiaohongshu.com/explore', {
|
|
77
|
+
waitUntil: 'domcontentloaded'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
await page.waitForTimeout(2_000);
|
|
81
|
+
const needLogin = await page.locator('#login-btn').count() > 0;
|
|
82
|
+
debugLog('checkLogin', 'login state checked', {
|
|
83
|
+
pageUrl: page.url(),
|
|
84
|
+
needLogin
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
loginStatus: needLogin ? 'logged-out' : 'logged-in',
|
|
88
|
+
lastLoginAt: null,
|
|
89
|
+
needLogin,
|
|
90
|
+
checkedAt: new Date().toISOString()
|
|
91
|
+
};
|
|
92
|
+
} finally{
|
|
93
|
+
if (ownsSession) {
|
|
94
|
+
disconnectRednoteSession(activeSession);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export async function ensureRednoteLoggedIn(target, action = 'continue', session) {
|
|
99
|
+
const rednote = await checkRednoteLogin(target, session);
|
|
100
|
+
if (rednote.needLogin) {
|
|
101
|
+
throw new Error(`Xiaohongshu login is required before ${action}. Run \`rednote login --instance ${target.instanceName}\` first.`);
|
|
102
|
+
}
|
|
103
|
+
return rednote;
|
|
104
|
+
}
|
|
105
|
+
export async function runCheckLoginCommand(values = {}) {
|
|
106
|
+
if (values.help) {
|
|
107
|
+
printCheckLoginHelp();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const target = resolveStatusTarget(values.instance);
|
|
111
|
+
const rednote = await checkRednoteLogin(target);
|
|
112
|
+
const result = {
|
|
113
|
+
ok: true,
|
|
114
|
+
rednote
|
|
115
|
+
};
|
|
116
|
+
printJson(result);
|
|
117
|
+
}
|
|
118
|
+
async function main() {
|
|
119
|
+
const { values } = parseArgs({
|
|
120
|
+
args: process.argv.slice(2),
|
|
121
|
+
allowPositionals: true,
|
|
122
|
+
strict: false,
|
|
123
|
+
options: {
|
|
124
|
+
instance: {
|
|
125
|
+
type: 'string'
|
|
126
|
+
},
|
|
127
|
+
help: {
|
|
128
|
+
type: 'boolean',
|
|
129
|
+
short: 'h'
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
if (values.help) {
|
|
134
|
+
printCheckLoginHelp();
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
await runCheckLoginCommand(values);
|
|
138
|
+
}
|
|
139
|
+
runCli(import.meta.url, main);
|