tt-help-cli-ycl 1.4.27 → 1.4.29
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/package.json +1 -1
- package/scripts/search-authors.mjs +21 -0
- package/scripts/sync-raw-jobs.mjs +450 -181
- package/src/cli/comments.js +22 -0
- package/src/cli/raw-sync.js +2 -5
- package/src/cli/search.js +583 -0
- package/src/lib/args.js +85 -8
- package/src/lib/constants.js +25 -5
- package/src/lib/search-collector.js +203 -0
- package/src/lib/target-locations.js +1 -0
- package/src/main.js +3 -0
- package/src/npm-main.js +3 -0
- package/src/watch/data-store.js +266 -577
- package/src/watch/db-columns.js +1 -0
- package/src/watch/db-crud.js +27 -3
- package/src/watch/db-raw-jobs.js +3 -3
- package/src/watch/db-schema.js +100 -0
- package/src/watch/db-stats.js +46 -23
- package/src/watch/db-tags.js +100 -1
- package/src/watch/llm-config.js +145 -1
- package/src/watch/llm-scoring.js +60 -117
- package/src/watch/public/app.js +5 -5
- package/src/watch/public/index.html +4 -5
- package/src/watch/public/style.css +16 -0
- package/src/watch/server.js +170 -59
- package/src/watch/tag-service.js +11 -67
package/src/lib/args.js
CHANGED
|
@@ -556,6 +556,41 @@ function parseRefreshArgs(args) {
|
|
|
556
556
|
};
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
function parseSearchArgs(args) {
|
|
560
|
+
let serverUrl = null;
|
|
561
|
+
let basePort = 3333;
|
|
562
|
+
let portCount = 10;
|
|
563
|
+
let proxy = null;
|
|
564
|
+
|
|
565
|
+
for (let i = 0; i < args.length; i++) {
|
|
566
|
+
const arg = args[i];
|
|
567
|
+
if (arg === "-s" || arg === "--server") {
|
|
568
|
+
serverUrl = args[++i];
|
|
569
|
+
} else if (arg === "--base-port") {
|
|
570
|
+
basePort = parseInt(args[++i]) || 3333;
|
|
571
|
+
} else if (arg === "--port-count") {
|
|
572
|
+
portCount = parseInt(args[++i]) || 10;
|
|
573
|
+
} else if (arg === "--proxy") {
|
|
574
|
+
proxy = args[++i];
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return {
|
|
579
|
+
subcommand: "search",
|
|
580
|
+
search: { serverUrl, basePort, portCount, proxy },
|
|
581
|
+
urls: [],
|
|
582
|
+
outputFormat: "json",
|
|
583
|
+
exploreCount: 0,
|
|
584
|
+
showConfig: false,
|
|
585
|
+
showHelp: false,
|
|
586
|
+
customProxy: null,
|
|
587
|
+
configAction: null,
|
|
588
|
+
configValue: null,
|
|
589
|
+
pipeMode: false,
|
|
590
|
+
filterStr: null,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
559
594
|
function parseAttachArgs(args) {
|
|
560
595
|
let parallel = 1;
|
|
561
596
|
let interval = 10;
|
|
@@ -690,8 +725,10 @@ function parseRawSyncArgs(args) {
|
|
|
690
725
|
let rawSyncDb = null;
|
|
691
726
|
let rawSyncArchive = null;
|
|
692
727
|
let rawSyncInterval = null;
|
|
693
|
-
let
|
|
694
|
-
let
|
|
728
|
+
let rawSyncThreshold = null;
|
|
729
|
+
let rawSyncLlm = false;
|
|
730
|
+
let rawSyncLlmMin = null;
|
|
731
|
+
let rawSyncLlmSample = null;
|
|
695
732
|
let rawSyncOnce = false;
|
|
696
733
|
|
|
697
734
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -702,10 +739,17 @@ function parseRawSyncArgs(args) {
|
|
|
702
739
|
rawSyncArchive = args[++i];
|
|
703
740
|
} else if (arg === "--interval" && i + 1 < args.length) {
|
|
704
741
|
rawSyncInterval = parseInt(args[++i]) || null;
|
|
705
|
-
} else if (
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
742
|
+
} else if (
|
|
743
|
+
(arg === "--threshold" || arg === "--max") &&
|
|
744
|
+
i + 1 < args.length
|
|
745
|
+
) {
|
|
746
|
+
rawSyncThreshold = parseInt(args[++i]) || null;
|
|
747
|
+
} else if (arg === "--llm") {
|
|
748
|
+
rawSyncLlm = true;
|
|
749
|
+
} else if (arg === "--llm-min" && i + 1 < args.length) {
|
|
750
|
+
rawSyncLlmMin = parseInt(args[++i]) || null;
|
|
751
|
+
} else if (arg === "--llm-sample" && i + 1 < args.length) {
|
|
752
|
+
rawSyncLlmSample = parseInt(args[++i]) || null;
|
|
709
753
|
} else if (arg === "--once") {
|
|
710
754
|
rawSyncOnce = true;
|
|
711
755
|
}
|
|
@@ -716,8 +760,10 @@ function parseRawSyncArgs(args) {
|
|
|
716
760
|
rawSyncDb,
|
|
717
761
|
rawSyncArchive,
|
|
718
762
|
rawSyncInterval,
|
|
719
|
-
|
|
720
|
-
|
|
763
|
+
rawSyncThreshold,
|
|
764
|
+
rawSyncLlm,
|
|
765
|
+
rawSyncLlmMin,
|
|
766
|
+
rawSyncLlmSample,
|
|
721
767
|
rawSyncOnce,
|
|
722
768
|
urls: [],
|
|
723
769
|
outputFormat: "json",
|
|
@@ -855,8 +901,10 @@ function parseTagArgs(args) {
|
|
|
855
901
|
let isScore = false;
|
|
856
902
|
let isScoreAll = false;
|
|
857
903
|
let isRefresh = false;
|
|
904
|
+
let isSearch = false;
|
|
858
905
|
let scoreAllPort = 7222;
|
|
859
906
|
let refreshPort = 4222;
|
|
907
|
+
let searchPort = 3333;
|
|
860
908
|
let scoreProxy = null;
|
|
861
909
|
let scoreTag = null;
|
|
862
910
|
let scoreCountries = null;
|
|
@@ -938,6 +986,8 @@ function parseTagArgs(args) {
|
|
|
938
986
|
isScoreAll = true;
|
|
939
987
|
} else if (cleaned.toLowerCase() === "refresh") {
|
|
940
988
|
isRefresh = true;
|
|
989
|
+
} else if (cleaned.toLowerCase() === "search") {
|
|
990
|
+
isSearch = true;
|
|
941
991
|
} else {
|
|
942
992
|
tags.push(cleaned.toLowerCase());
|
|
943
993
|
}
|
|
@@ -948,6 +998,7 @@ function parseTagArgs(args) {
|
|
|
948
998
|
if (rawTagPort !== null && !isNaN(rawTagPort)) {
|
|
949
999
|
if (isScoreAll) scoreAllPort = rawTagPort;
|
|
950
1000
|
else if (isRefresh) refreshPort = rawTagPort;
|
|
1001
|
+
else if (isSearch) searchPort = rawTagPort;
|
|
951
1002
|
else scorePort = rawTagPort;
|
|
952
1003
|
}
|
|
953
1004
|
|
|
@@ -1039,6 +1090,28 @@ function parseTagArgs(args) {
|
|
|
1039
1090
|
};
|
|
1040
1091
|
}
|
|
1041
1092
|
|
|
1093
|
+
if (isSearch) {
|
|
1094
|
+
return {
|
|
1095
|
+
subcommand: "search",
|
|
1096
|
+
search: {
|
|
1097
|
+
serverUrl,
|
|
1098
|
+
basePort: searchPort,
|
|
1099
|
+
portCount: 10,
|
|
1100
|
+
proxy: scoreProxy,
|
|
1101
|
+
},
|
|
1102
|
+
urls: [],
|
|
1103
|
+
outputFormat: "json",
|
|
1104
|
+
exploreCount: 0,
|
|
1105
|
+
showConfig: false,
|
|
1106
|
+
showHelp: false,
|
|
1107
|
+
customProxy: null,
|
|
1108
|
+
configAction: null,
|
|
1109
|
+
configValue: null,
|
|
1110
|
+
pipeMode: false,
|
|
1111
|
+
filterStr: null,
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1042
1115
|
return {
|
|
1043
1116
|
subcommand: "tag",
|
|
1044
1117
|
tagTags: {
|
|
@@ -1143,6 +1216,10 @@ export function parseArgs() {
|
|
|
1143
1216
|
return parseRefreshArgs(args.slice(1));
|
|
1144
1217
|
}
|
|
1145
1218
|
|
|
1219
|
+
if (args.length > 0 && args[0] === "search") {
|
|
1220
|
+
return parseSearchArgs(args.slice(1));
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1146
1223
|
if (args.length > 0 && args[0] === "raw-sync") {
|
|
1147
1224
|
return parseRawSyncArgs(args.slice(1));
|
|
1148
1225
|
}
|
package/src/lib/constants.js
CHANGED
|
@@ -314,18 +314,38 @@ const HELP_TEXT = [
|
|
|
314
314
|
" tt-help tag refresh -s http://127.0.0.1:3001",
|
|
315
315
|
" tt-help tag refresh --port 8222 -s http://127.0.0.1:3001",
|
|
316
316
|
"",
|
|
317
|
+
" search [选项]",
|
|
318
|
+
" 基于 productive 标签搜索 TikTok 作者(score>70 的标签作为搜索词)",
|
|
319
|
+
" 5 个搜索 Tab 并行采集(综合/视频/用户/直播/照片),去重后提交到 job_base",
|
|
320
|
+
" 选项:",
|
|
321
|
+
" -s, --server <URL> 服务端地址(默认 http://127.0.0.1:3000)",
|
|
322
|
+
" --base-port <端口号> 起始 CDP 端口,默认 3333",
|
|
323
|
+
" --port-count <数量> 端口数量(账户数),默认 10",
|
|
324
|
+
" --proxy <地址> 浏览器代理",
|
|
325
|
+
" 行为:",
|
|
326
|
+
" - 从 tags 表取 score>70 的 productive 标签",
|
|
327
|
+
" - 去掉 # 号后作为搜索关键词",
|
|
328
|
+
" - 并行搜索 5 个 tab,去重后提交到 job_base(来源=search)",
|
|
329
|
+
" - 猜测国家取标签 countries 数组的第一个",
|
|
330
|
+
" 示例: tt-help search",
|
|
331
|
+
" tt-help search -s http://127.0.0.1:3001",
|
|
332
|
+
" tt-help search --base-port 3333 -s http://127.0.0.1:3001",
|
|
333
|
+
" tt-help search --proxy http://127.0.0.1:7890",
|
|
334
|
+
"",
|
|
317
335
|
" raw-sync --db <主库路径> --archive <归档库路径> [选项]",
|
|
318
|
-
"
|
|
336
|
+
" 归档新用户 + 按需补充任务(从归档库向主库 jobs 移入)",
|
|
319
337
|
" 选项:",
|
|
320
338
|
" --db <路径> 主库路径(必填)",
|
|
321
339
|
" --archive <路径> 归档库路径(必填)",
|
|
322
|
-
"
|
|
323
|
-
"
|
|
324
|
-
" --
|
|
340
|
+
" --interval <秒> 检测间隔,默认 120(2分钟)",
|
|
341
|
+
" --threshold <条> pending 低于此值时触发补任务,默认 50",
|
|
342
|
+
" --llm 启用 LLM 打分模式",
|
|
343
|
+
" --llm-min <分> LLM 最低分阈值,默认 60",
|
|
344
|
+
" --llm-sample <条> LLM 每批采样数,默认 100",
|
|
325
345
|
" --once 只执行一次后退出",
|
|
326
346
|
" 示例: tt-help raw-sync --db data/result.db --archive data/result-archive.db",
|
|
327
347
|
" tt-help raw-sync --db data/result.db --archive data/result-archive.db --once",
|
|
328
|
-
" tt-help raw-sync --db data/result.db --archive data/result-archive.db --
|
|
348
|
+
" tt-help raw-sync --db data/result.db --archive data/result-archive.db --llm --threshold 300",
|
|
329
349
|
"",
|
|
330
350
|
" config [show|set|unset|reset]",
|
|
331
351
|
" config 查看当前配置",
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TikTok 搜索页作者采集 — DOM 滚动版
|
|
3
|
+
*
|
|
4
|
+
* 用法:
|
|
5
|
+
* import { collectSearchAuthors } from "../lib/search-collector.js";
|
|
6
|
+
* const ids = await collectSearchAuthors(browser, "tienda");
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const TAB_URLS = {
|
|
10
|
+
综合: (q) => `https://www.tiktok.com/search?q=${encodeURIComponent(q)}`,
|
|
11
|
+
视频: (q) => `https://www.tiktok.com/search/video?q=${encodeURIComponent(q)}`,
|
|
12
|
+
用户: (q) => `https://www.tiktok.com/search/user?q=${encodeURIComponent(q)}`,
|
|
13
|
+
直播: (q) => `https://www.tiktok.com/search/live?q=${encodeURIComponent(q)}`,
|
|
14
|
+
照片: (q) => `https://www.tiktok.com/search/photo?q=${encodeURIComponent(q)}`,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const DEFAULT_TABS = ["综合", "视频", "用户", "直播", "照片"];
|
|
18
|
+
|
|
19
|
+
const SCROLL = {
|
|
20
|
+
maxRounds: 60,
|
|
21
|
+
staleThreshold: 3,
|
|
22
|
+
delayMin: 800,
|
|
23
|
+
delayMax: 1500,
|
|
24
|
+
waitAfterScroll: 4000,
|
|
25
|
+
};
|
|
26
|
+
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
27
|
+
const randDelay = () =>
|
|
28
|
+
delay(
|
|
29
|
+
Math.floor(Math.random() * (SCROLL.delayMax - SCROLL.delayMin + 1)) +
|
|
30
|
+
SCROLL.delayMin,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
async function extractAuthors(page, excludeSet) {
|
|
34
|
+
return page.evaluate(
|
|
35
|
+
(exclude) => {
|
|
36
|
+
const ids = new Set();
|
|
37
|
+
const skip = new Set(exclude || []);
|
|
38
|
+
for (const a of document.querySelectorAll('a[href*="/@"]')) {
|
|
39
|
+
const m = (a.getAttribute("href") || "").match(/\/@([^/?]+)/);
|
|
40
|
+
if (m && m[1] !== "undefined" && m[1].length <= 30 && !skip.has(m[1])) {
|
|
41
|
+
ids.add(m[1]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return [...ids];
|
|
45
|
+
},
|
|
46
|
+
[...excludeSet],
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function scrollPage(page) {
|
|
51
|
+
await page.evaluate(() => {
|
|
52
|
+
const main = document.querySelector("main");
|
|
53
|
+
if (main && main.scrollHeight > main.clientHeight + 10) {
|
|
54
|
+
main.scrollBy(0, main.clientHeight * 0.8);
|
|
55
|
+
} else {
|
|
56
|
+
window.scrollBy(0, window.innerHeight * 0.8);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function processOneTab(page, tabName, keyword, allIds, excludeSet, log) {
|
|
62
|
+
const urlFn = TAB_URLS[tabName];
|
|
63
|
+
if (!urlFn) {
|
|
64
|
+
log(`[${tabName}] 不支持`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const tag = `[${tabName}]`;
|
|
69
|
+
const url = urlFn(keyword);
|
|
70
|
+
|
|
71
|
+
// 首屏正常加载
|
|
72
|
+
log(`${tag} 加载...`);
|
|
73
|
+
try {
|
|
74
|
+
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
75
|
+
} catch (e) {
|
|
76
|
+
log(`${tag} 导航失败: ${e.message}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await delay(4000);
|
|
80
|
+
|
|
81
|
+
// 首屏渲染完成后屏蔽图片/字体/媒体,加速后续滚动
|
|
82
|
+
await page.route("**/*", (route) => {
|
|
83
|
+
const t = route.request().resourceType();
|
|
84
|
+
if (t === "image" || t === "font" || t === "media") route.abort();
|
|
85
|
+
else route.continue();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const tabSet = new Set();
|
|
89
|
+
let stale = 0;
|
|
90
|
+
|
|
91
|
+
for (let round = 0; round <= SCROLL.maxRounds; round++) {
|
|
92
|
+
if (round > 0) {
|
|
93
|
+
await scrollPage(page);
|
|
94
|
+
await randDelay();
|
|
95
|
+
await delay(SCROLL.waitAfterScroll);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const ids = await extractAuthors(page, excludeSet);
|
|
99
|
+
let added = 0;
|
|
100
|
+
for (const id of ids) {
|
|
101
|
+
if (!tabSet.has(id)) {
|
|
102
|
+
tabSet.add(id);
|
|
103
|
+
added++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (round === 0) {
|
|
108
|
+
log(`${tag} 初始: ${tabSet.size}`);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (added === 0) {
|
|
113
|
+
stale++;
|
|
114
|
+
if (stale >= SCROLL.staleThreshold) {
|
|
115
|
+
log(`${tag} ${SCROLL.staleThreshold}轮无新`);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
stale = 0;
|
|
120
|
+
if (round % 5 === 0 || added >= 5)
|
|
121
|
+
log(`${tag} R${round}: +${added}, 计${tabSet.size}`);
|
|
122
|
+
}
|
|
123
|
+
if (tabSet.size >= 500) break;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let added = 0;
|
|
127
|
+
for (const id of tabSet) {
|
|
128
|
+
if (!allIds.has(id)) {
|
|
129
|
+
allIds.add(id);
|
|
130
|
+
added++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
log(`${tag} 完成: ${tabSet.size}个, 新增${added} (累计${allIds.size})`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {import("playwright").Browser} browser
|
|
138
|
+
* @param {string} keyword
|
|
139
|
+
* @param {object} [opts]
|
|
140
|
+
* @param {string[]} [opts.tabs]
|
|
141
|
+
* @param {function} [opts.log] - 日志函数,默认 console.error
|
|
142
|
+
* @returns {Promise<string[]>}
|
|
143
|
+
*/
|
|
144
|
+
export async function collectSearchAuthors(browser, keyword, opts = {}) {
|
|
145
|
+
const tabs = opts.tabs || DEFAULT_TABS;
|
|
146
|
+
const log = opts.log || ((...args) => console.error(...args));
|
|
147
|
+
const excludeSet = new Set(opts.exclude || []);
|
|
148
|
+
|
|
149
|
+
log(`[collect] "${keyword}" Tab: [${tabs.join(", ")}]`);
|
|
150
|
+
if (excludeSet.size > 0)
|
|
151
|
+
log(`[collect] 排除: [${[...excludeSet].join(", ")}]`);
|
|
152
|
+
|
|
153
|
+
const context = browser.contexts()[0];
|
|
154
|
+
const existingPage = context.pages()[0];
|
|
155
|
+
const allIds = new Set();
|
|
156
|
+
|
|
157
|
+
// 并行:每个 tab 独立页面同时跑
|
|
158
|
+
const newPages = []; // 追踪所有新建页面,确保清理
|
|
159
|
+
const tasks = tabs.map(async (tabName, i) => {
|
|
160
|
+
let page = null;
|
|
161
|
+
try {
|
|
162
|
+
page = i === 0 ? existingPage : await context.newPage();
|
|
163
|
+
if (i > 0) newPages.push(page);
|
|
164
|
+
await processOneTab(page, tabName, keyword, allIds, excludeSet, log);
|
|
165
|
+
} catch (e) {
|
|
166
|
+
log(`[${tabName}] 异常: ${e.message}`);
|
|
167
|
+
} finally {
|
|
168
|
+
if (i > 0 && page) {
|
|
169
|
+
try {
|
|
170
|
+
await page.close();
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
await Promise.all(tasks);
|
|
177
|
+
|
|
178
|
+
// 兜底:确保所有新建页面都已关闭
|
|
179
|
+
for (const p of newPages) {
|
|
180
|
+
try {
|
|
181
|
+
if (!p.isClosed()) await p.close();
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return [...allIds].sort();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 采集并保存到文件(便捷函数)
|
|
190
|
+
* @param {import("playwright").Browser} browser
|
|
191
|
+
* @param {string} keyword
|
|
192
|
+
* @param {string} outputFile - 输出文件路径
|
|
193
|
+
* @param {object} [opts] - 同 collectSearchAuthors
|
|
194
|
+
* @returns {Promise<string[]>}
|
|
195
|
+
*/
|
|
196
|
+
export async function searchAndSave(browser, keyword, outputFile, opts = {}) {
|
|
197
|
+
const fs = await import("fs");
|
|
198
|
+
const path = await import("path");
|
|
199
|
+
const ids = await collectSearchAuthors(browser, keyword, opts);
|
|
200
|
+
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
201
|
+
fs.writeFileSync(outputFile, ids.join("\n") + "\n", "utf-8");
|
|
202
|
+
return ids;
|
|
203
|
+
}
|
package/src/main.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
handleTagRefresh,
|
|
21
21
|
} from "./cli/tag.js";
|
|
22
22
|
import { handleRawSync } from "./cli/raw-sync.js";
|
|
23
|
+
import { handleSearch } from "./cli/search.js";
|
|
23
24
|
|
|
24
25
|
async function main() {
|
|
25
26
|
const parsed = parseArgs();
|
|
@@ -59,6 +60,8 @@ async function main() {
|
|
|
59
60
|
return handleScoreAll(parsed);
|
|
60
61
|
case "tag-refresh":
|
|
61
62
|
return handleTagRefresh(parsed);
|
|
63
|
+
case "search":
|
|
64
|
+
return handleSearch(parsed);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
const {
|
package/src/npm-main.js
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
handleTagRefresh,
|
|
17
17
|
} from "./cli/tag.js";
|
|
18
18
|
import { handleRawSync } from "./cli/raw-sync.js";
|
|
19
|
+
import { handleSearch } from "./cli/search.js";
|
|
19
20
|
|
|
20
21
|
function exitUnsupportedCommand(command) {
|
|
21
22
|
console.error(
|
|
@@ -59,6 +60,8 @@ async function main() {
|
|
|
59
60
|
return handleTagRefresh(parsed);
|
|
60
61
|
case "raw-sync":
|
|
61
62
|
return handleRawSync(parsed);
|
|
63
|
+
case "search":
|
|
64
|
+
return handleSearch(parsed);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
const {
|