@harness.farm/social-cli 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/README.md +213 -0
- package/dist/adapters/base.js +2 -0
- package/dist/adapters/index.js +8 -0
- package/dist/adapters/xiaohongshu.js +314 -0
- package/dist/browser/cdp.js +106 -0
- package/dist/browser/runner.js +75 -0
- package/dist/browser/session.js +38 -0
- package/dist/cli.js +99 -0
- package/dist/output/table.js +43 -0
- package/dist/runner/step-executor.js +142 -0
- package/dist/runner/yaml-runner.js +368 -0
- package/dist/scripts/explore-bili.js +37 -0
- package/dist/scripts/explore-douyin.js +30 -0
- package/dist/scripts/explore-x.js +31 -0
- package/package.json +2 -1
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runner โ orchestrates login flow + command execution for any adapter.
|
|
3
|
+
*
|
|
4
|
+
* Flow:
|
|
5
|
+
* 1. Connect to Chrome tab
|
|
6
|
+
* 2. Check if already logged in (via saved session or live cookies)
|
|
7
|
+
* 3. If not, navigate to loginUrl and wait for user to log in
|
|
8
|
+
* 4. Save session cookies
|
|
9
|
+
* 5. Run the requested command
|
|
10
|
+
* 6. Render output
|
|
11
|
+
*/
|
|
12
|
+
import { newTab, sleep } from './cdp.js';
|
|
13
|
+
import { captureSession, restoreSession, hasSession } from './session.js';
|
|
14
|
+
import { renderTable } from '../output/table.js';
|
|
15
|
+
export async function run(adapter, opts) {
|
|
16
|
+
const port = opts.cdpPort ?? 9222;
|
|
17
|
+
console.log(`\n๐ ่ฟๆฅ Chrome CDP :${port}...`);
|
|
18
|
+
const client = await newTab(port);
|
|
19
|
+
// --- ็ปๅฝๆต็จ ---
|
|
20
|
+
let loggedIn = false;
|
|
21
|
+
// ๅ
ๅฐ่ฏๆขๅคๅทฒไฟๅญ็ session
|
|
22
|
+
if (hasSession(adapter.platform)) {
|
|
23
|
+
await restoreSession(client, adapter.platform);
|
|
24
|
+
// ๅฏผ่ชๅฐ็ฎๆ ็ซ้ช่ฏ session ๆฏๅฆ่ฟๆๆ
|
|
25
|
+
await client.navigate(adapter.loginUrl, 3000);
|
|
26
|
+
loggedIn = await adapter.isLoggedIn(client);
|
|
27
|
+
if (!loggedIn) {
|
|
28
|
+
console.log('โ ๏ธ ๅทฒไฟๅญ็ session ๅทฒๅคฑๆ๏ผ้่ฆ้ๆฐ็ปๅฝ');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// session ๆ ๆๆๆฒกๆ session โ ๅผๅฏผ็จๆท็ปๅฝ
|
|
32
|
+
if (!loggedIn) {
|
|
33
|
+
await client.navigate(adapter.loginUrl, 2000);
|
|
34
|
+
console.log(`\n๐ ่ฏทๅจ Chrome ไธญ็ปๅฝ ${adapter.platform}๏ผๅฎๆๅๆ Enter ็ปง็ปญ...`);
|
|
35
|
+
await waitForEnter();
|
|
36
|
+
// ็ญๅพ
ๅนถ่ฝฎ่ฏข็ปๅฝ็ถๆ
|
|
37
|
+
for (let i = 0; i < 30; i++) {
|
|
38
|
+
loggedIn = await adapter.isLoggedIn(client);
|
|
39
|
+
if (loggedIn)
|
|
40
|
+
break;
|
|
41
|
+
process.stdout.write(`\r็ญๅพ
็ปๅฝ... ${(i + 1) * 2}s`);
|
|
42
|
+
await sleep(2000);
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write('\n');
|
|
45
|
+
if (!loggedIn) {
|
|
46
|
+
client.close();
|
|
47
|
+
throw new Error('็ปๅฝ่ถ
ๆถ๏ผ่ฏท้่ฏ');
|
|
48
|
+
}
|
|
49
|
+
// ไฟๅญ session
|
|
50
|
+
await captureSession(client, adapter.platform);
|
|
51
|
+
}
|
|
52
|
+
console.log(`โ
ๅทฒ็ปๅฝ ${adapter.platform}`);
|
|
53
|
+
// --- ๆง่กๅฝไปค ---
|
|
54
|
+
const handler = adapter.commands[opts.command];
|
|
55
|
+
if (!handler) {
|
|
56
|
+
const available = Object.keys(adapter.commands).join(', ');
|
|
57
|
+
client.close();
|
|
58
|
+
throw new Error(`ๆช็ฅๅฝไปค "${opts.command}"๏ผๅฏ็จ: ${available}`);
|
|
59
|
+
}
|
|
60
|
+
console.log(`\n๐ ๆง่ก: ${adapter.platform} ${opts.command} ${(opts.args ?? []).join(' ')}\n`);
|
|
61
|
+
const result = await handler(client, opts.args ?? []);
|
|
62
|
+
client.close();
|
|
63
|
+
// --- ๆธฒๆ ---
|
|
64
|
+
renderTable(result.columns, result.rows);
|
|
65
|
+
}
|
|
66
|
+
function waitForEnter() {
|
|
67
|
+
return new Promise((resolve) => {
|
|
68
|
+
process.stdin.setRawMode?.(false);
|
|
69
|
+
process.stdin.resume();
|
|
70
|
+
process.stdin.once('data', () => {
|
|
71
|
+
process.stdin.pause();
|
|
72
|
+
resolve();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session manager โ save/load cookies to disk per platform.
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
const SESSION_DIR = path.join(process.env.HOME ?? '.', '.cdp-scraper', 'sessions');
|
|
7
|
+
function sessionPath(platform) {
|
|
8
|
+
return path.join(SESSION_DIR, `${platform}.json`);
|
|
9
|
+
}
|
|
10
|
+
export function saveSession(platform, cookies) {
|
|
11
|
+
fs.mkdirSync(SESSION_DIR, { recursive: true });
|
|
12
|
+
fs.writeFileSync(sessionPath(platform), JSON.stringify(cookies, null, 2));
|
|
13
|
+
console.log(`โ
Session saved โ ${sessionPath(platform)} (${cookies.length} cookies)`);
|
|
14
|
+
}
|
|
15
|
+
export function loadSession(platform) {
|
|
16
|
+
const p = sessionPath(platform);
|
|
17
|
+
if (!fs.existsSync(p))
|
|
18
|
+
return null;
|
|
19
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
export function hasSession(platform) {
|
|
22
|
+
return fs.existsSync(sessionPath(platform));
|
|
23
|
+
}
|
|
24
|
+
/** ไป browser ่ฏปๅ cookies ๅนถไฟๅญ */
|
|
25
|
+
export async function captureSession(client, platform) {
|
|
26
|
+
const cookies = await client.getAllCookies();
|
|
27
|
+
saveSession(platform, cookies);
|
|
28
|
+
return cookies;
|
|
29
|
+
}
|
|
30
|
+
/** ๆขๅคๅทฒไฟๅญ็ session ๅฐ browser */
|
|
31
|
+
export async function restoreSession(client, platform) {
|
|
32
|
+
const cookies = loadSession(platform);
|
|
33
|
+
if (!cookies)
|
|
34
|
+
return false;
|
|
35
|
+
await client.setCookies(cookies);
|
|
36
|
+
console.log(`โ
Session restored โ ${platform} (${cookies.length} cookies)`);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI entry โ supports both YAML adapters and TypeScript adapters.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* tsx src/cli.ts <platform> <command> [args...]
|
|
7
|
+
*
|
|
8
|
+
* Adapter resolution order:
|
|
9
|
+
* 1. adapters/<platform>.yaml โ YAML-first
|
|
10
|
+
* 2. src/adapters/<platform>.ts โ TypeScript fallback
|
|
11
|
+
*
|
|
12
|
+
* Examples:
|
|
13
|
+
* tsx src/cli.ts xhs search ๆณๅพai
|
|
14
|
+
* tsx src/cli.ts xhs like "https://..."
|
|
15
|
+
* tsx src/cli.ts xhs comment "https://..." "ๅคชๆฃไบ๏ผ"
|
|
16
|
+
* tsx src/cli.ts xhs post --title "ๆ ้ข" --content "ๅ
ๅฎน"
|
|
17
|
+
*/
|
|
18
|
+
import path from 'path';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import { fileURLToPath } from 'url';
|
|
21
|
+
import { runYamlCommand } from './runner/yaml-runner.js';
|
|
22
|
+
import { run } from './browser/runner.js';
|
|
23
|
+
import { adapters } from './adapters/index.js';
|
|
24
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
26
|
+
const [, , platform, command, ...rest] = process.argv;
|
|
27
|
+
if (!platform || !command) {
|
|
28
|
+
printHelp();
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// โโ Resolve adapter โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
32
|
+
const yamlPath = path.join(ROOT, 'adapters', `${platform}.yaml`);
|
|
33
|
+
const hasYaml = fs.existsSync(yamlPath);
|
|
34
|
+
const tsAdapter = adapters[platform];
|
|
35
|
+
if (!hasYaml && !tsAdapter) {
|
|
36
|
+
console.error(`โ Unknown platform "${platform}"`);
|
|
37
|
+
console.error(` YAML adapters: ${listYamlAdapters().join(', ') || '(none)'}`);
|
|
38
|
+
console.error(` TS adapters: ${Object.keys(adapters).join(', ')}`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
// โโ Parse args โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
42
|
+
// Support both positional args and --key value flags
|
|
43
|
+
const args = parseArgs(rest);
|
|
44
|
+
// โโ Run โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
45
|
+
if (hasYaml) {
|
|
46
|
+
// YAML adapter: pass positional args in order
|
|
47
|
+
runYamlCommand(yamlPath, command, args.positional).catch(err => {
|
|
48
|
+
console.error('โ', err.message);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
// TypeScript adapter: pass flags as array (legacy)
|
|
54
|
+
run(tsAdapter, { command, args: rest }).catch(err => {
|
|
55
|
+
console.error('โ', err.message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
// โโ Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
60
|
+
function parseArgs(argv) {
|
|
61
|
+
const positional = [];
|
|
62
|
+
const flags = {};
|
|
63
|
+
for (let i = 0; i < argv.length; i++) {
|
|
64
|
+
if (argv[i].startsWith('--')) {
|
|
65
|
+
flags[argv[i].slice(2)] = argv[++i] ?? '';
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
positional.push(argv[i]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { positional, flags };
|
|
72
|
+
}
|
|
73
|
+
function listYamlAdapters() {
|
|
74
|
+
const dir = path.join(ROOT, 'adapters');
|
|
75
|
+
if (!fs.existsSync(dir))
|
|
76
|
+
return [];
|
|
77
|
+
return fs.readdirSync(dir)
|
|
78
|
+
.filter(f => f.endsWith('.yaml'))
|
|
79
|
+
.map(f => f.replace('.yaml', ''));
|
|
80
|
+
}
|
|
81
|
+
function printHelp() {
|
|
82
|
+
const yaml = listYamlAdapters();
|
|
83
|
+
const ts = Object.keys(adapters);
|
|
84
|
+
console.log('็จๆณ: tsx src/cli.ts <platform> <command> [args...]');
|
|
85
|
+
console.log('');
|
|
86
|
+
if (yaml.length) {
|
|
87
|
+
console.log('YAML ๅนณๅฐ (ๆจ่):');
|
|
88
|
+
yaml.forEach(p => console.log(` ${p}`));
|
|
89
|
+
}
|
|
90
|
+
if (ts.length) {
|
|
91
|
+
console.log('TS ๅนณๅฐ:');
|
|
92
|
+
ts.forEach(p => console.log(` ${p}`));
|
|
93
|
+
}
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log('็คบไพ:');
|
|
96
|
+
console.log(' tsx src/cli.ts xhs search ๆณๅพai');
|
|
97
|
+
console.log(' tsx src/cli.ts xhs like "https://www.xiaohongshu.com/explore/..."');
|
|
98
|
+
console.log(' tsx src/cli.ts xhs comment "https://..." "ๅคชๆฃไบ๏ผ"');
|
|
99
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI table renderer with CJK character width support.
|
|
3
|
+
*/
|
|
4
|
+
function dispWidth(s) {
|
|
5
|
+
let w = 0;
|
|
6
|
+
for (const c of s)
|
|
7
|
+
w += c.codePointAt(0) > 127 ? 2 : 1;
|
|
8
|
+
return w;
|
|
9
|
+
}
|
|
10
|
+
function truncate(s, maxW) {
|
|
11
|
+
let out = '', cur = 0;
|
|
12
|
+
for (const c of s) {
|
|
13
|
+
const cw = c.codePointAt(0) > 127 ? 2 : 1;
|
|
14
|
+
if (cur + cw > maxW - 1)
|
|
15
|
+
return out + 'โฆ';
|
|
16
|
+
out += c;
|
|
17
|
+
cur += cw;
|
|
18
|
+
}
|
|
19
|
+
return out;
|
|
20
|
+
}
|
|
21
|
+
function pad(s, width) {
|
|
22
|
+
return s + ' '.repeat(Math.max(0, width - dispWidth(s)));
|
|
23
|
+
}
|
|
24
|
+
function sep(cols, char = '-') {
|
|
25
|
+
return '+' + cols.map((c) => char.repeat(c.width + 2)).join('+') + '+';
|
|
26
|
+
}
|
|
27
|
+
function row(cols, values) {
|
|
28
|
+
return '|' + cols.map((c, i) => ` ${pad(truncate(String(values[i] ?? ''), c.width), c.width)} `).join('|') + '|';
|
|
29
|
+
}
|
|
30
|
+
export function renderTable(cols, data) {
|
|
31
|
+
const divider = sep(cols);
|
|
32
|
+
const doubleSep = sep(cols, '=');
|
|
33
|
+
console.log(divider);
|
|
34
|
+
console.log(row(cols, cols.map((c) => c.header)));
|
|
35
|
+
console.log(doubleSep);
|
|
36
|
+
data.forEach((item, i) => {
|
|
37
|
+
console.log(row(cols, cols.map((c) => String(item[c.key] ?? ''))));
|
|
38
|
+
if (i < data.length - 1)
|
|
39
|
+
console.log(divider);
|
|
40
|
+
});
|
|
41
|
+
console.log(divider);
|
|
42
|
+
console.log(`\nๅ
ฑ ${data.length} ๆก็ปๆ`);
|
|
43
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step executor โ maps YAML step types to agent-browser CLI calls.
|
|
3
|
+
*
|
|
4
|
+
* Each method runs `agent-browser <args>` and returns parsed JSON result.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
export class StepExecutor {
|
|
8
|
+
wsUrl;
|
|
9
|
+
connected = false;
|
|
10
|
+
constructor(wsUrl) {
|
|
11
|
+
this.wsUrl = wsUrl;
|
|
12
|
+
}
|
|
13
|
+
/** Connect to Chrome tab (once per session) */
|
|
14
|
+
connect() {
|
|
15
|
+
if (this.connected)
|
|
16
|
+
return;
|
|
17
|
+
this.run(['connect', this.wsUrl]);
|
|
18
|
+
this.connected = true;
|
|
19
|
+
}
|
|
20
|
+
// โโ Core step methods โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
21
|
+
open(url) {
|
|
22
|
+
return this.run(['open', url]);
|
|
23
|
+
}
|
|
24
|
+
click(selector) {
|
|
25
|
+
return this.run(['click', selector]);
|
|
26
|
+
}
|
|
27
|
+
/** Click an element by its visible text (uses eval under the hood) */
|
|
28
|
+
clickText(text) {
|
|
29
|
+
const js = `(function(){
|
|
30
|
+
var el = [...document.querySelectorAll('*')].find(function(e){
|
|
31
|
+
return e.textContent.trim() === ${JSON.stringify(text)} && e.children.length === 0;
|
|
32
|
+
});
|
|
33
|
+
if(el){ el.click(); return true; }
|
|
34
|
+
return false;
|
|
35
|
+
})()`;
|
|
36
|
+
const r = this.eval(js);
|
|
37
|
+
if (!r.value)
|
|
38
|
+
return { ok: false, error: `Text not found: "${text}"` };
|
|
39
|
+
return { ok: true };
|
|
40
|
+
}
|
|
41
|
+
fill(selector, value) {
|
|
42
|
+
// Use eval to avoid shell-quoting issues with complex selectors
|
|
43
|
+
const js = `(function(){
|
|
44
|
+
var el = document.querySelector(${JSON.stringify(selector)});
|
|
45
|
+
if (!el) return false;
|
|
46
|
+
el.focus();
|
|
47
|
+
var nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
|
|
48
|
+
|| Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value');
|
|
49
|
+
if (nativeSet && nativeSet.set) nativeSet.set.call(el, ${JSON.stringify(value)});
|
|
50
|
+
else el.value = ${JSON.stringify(value)};
|
|
51
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
52
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
53
|
+
return true;
|
|
54
|
+
})()`;
|
|
55
|
+
return this.eval(js);
|
|
56
|
+
}
|
|
57
|
+
type(selector, value) {
|
|
58
|
+
return this.run(['type', selector, value]);
|
|
59
|
+
}
|
|
60
|
+
/** Type into a contenteditable element via execCommand */
|
|
61
|
+
typeContentEditable(selector, value) {
|
|
62
|
+
const js = `(function(){
|
|
63
|
+
var el = document.querySelector(${JSON.stringify(selector)});
|
|
64
|
+
if(!el) return false;
|
|
65
|
+
el.focus();
|
|
66
|
+
document.execCommand('selectAll', false, null);
|
|
67
|
+
document.execCommand('insertText', false, ${JSON.stringify(value)});
|
|
68
|
+
return el.textContent || el.value || true;
|
|
69
|
+
})()`;
|
|
70
|
+
return this.eval(js);
|
|
71
|
+
}
|
|
72
|
+
/** Type via agent-browser's real keystroke simulation (works with Draft.js / React) */
|
|
73
|
+
typeKeys(selector, value) {
|
|
74
|
+
return this.run(['type', selector, value]);
|
|
75
|
+
}
|
|
76
|
+
wait(msOrSelector) {
|
|
77
|
+
if (typeof msOrSelector === 'number') {
|
|
78
|
+
return this.run(['wait', String(msOrSelector)]);
|
|
79
|
+
}
|
|
80
|
+
return this.run(['wait', msOrSelector]);
|
|
81
|
+
}
|
|
82
|
+
/** Press a key via agent-browser press (real key event, e.g. z, x, Enter, Control+Enter) */
|
|
83
|
+
pressKey(key) {
|
|
84
|
+
return this.run(['press', key]);
|
|
85
|
+
}
|
|
86
|
+
/** Insert text into the currently focused element via agent-browser keyboard type */
|
|
87
|
+
keyboardInsertText(text) {
|
|
88
|
+
// 'keyboard type' sends real key events char-by-char โ works with Draft.js
|
|
89
|
+
return this.run(['keyboard', 'type', text]);
|
|
90
|
+
}
|
|
91
|
+
eval(js) {
|
|
92
|
+
const r = this.run(['eval', js]);
|
|
93
|
+
if (r.ok && r.value !== undefined) {
|
|
94
|
+
// agent-browser wraps eval result in data.result
|
|
95
|
+
const data = r.value;
|
|
96
|
+
return { ok: true, value: data.result ?? r.value };
|
|
97
|
+
}
|
|
98
|
+
return r;
|
|
99
|
+
}
|
|
100
|
+
screenshot(path) {
|
|
101
|
+
return this.run(path ? ['screenshot', path] : ['screenshot']);
|
|
102
|
+
}
|
|
103
|
+
upload(selector, filePath) {
|
|
104
|
+
return this.run(['upload', selector, filePath]);
|
|
105
|
+
}
|
|
106
|
+
getUrl() {
|
|
107
|
+
const r = this.run(['get', 'url']);
|
|
108
|
+
const data = r.value;
|
|
109
|
+
return data?.url ?? '';
|
|
110
|
+
}
|
|
111
|
+
snapshot() {
|
|
112
|
+
const r = this.run(['snapshot']);
|
|
113
|
+
return String(r.value ?? '');
|
|
114
|
+
}
|
|
115
|
+
// โโ Internal runner โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
116
|
+
run(args) {
|
|
117
|
+
try {
|
|
118
|
+
const cmd = `agent-browser ${args.map(a => this.shellQuote(a)).join(' ')}`;
|
|
119
|
+
const out = execSync(cmd, {
|
|
120
|
+
env: { ...process.env, AGENT_BROWSER_JSON: '1' },
|
|
121
|
+
timeout: 30000,
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
});
|
|
124
|
+
// Find last JSON line (agent-browser may emit warnings before JSON)
|
|
125
|
+
const jsonLine = out.trim().split('\n').reverse().find(l => l.startsWith('{'));
|
|
126
|
+
if (!jsonLine)
|
|
127
|
+
return { ok: true };
|
|
128
|
+
const parsed = JSON.parse(jsonLine);
|
|
129
|
+
if (!parsed.success)
|
|
130
|
+
return { ok: false, error: parsed.error ?? 'unknown error' };
|
|
131
|
+
return { ok: true, value: parsed.data };
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
135
|
+
return { ok: false, error: msg };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
shellQuote(s) {
|
|
139
|
+
// Wrap in single quotes, escape internal single quotes
|
|
140
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
141
|
+
}
|
|
142
|
+
}
|