trove-cli 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +200 -2
- package/dist/adapters/file-source.d.ts +31 -0
- package/dist/adapters/file-source.js +97 -0
- package/dist/adapters/file-source.js.map +1 -0
- package/dist/adapters/index.d.ts +34 -0
- package/dist/adapters/index.js +146 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.d.ts +30 -0
- package/dist/adapters/opencode.js +135 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/cli.d.ts +58 -0
- package/dist/cli.js +972 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +81 -0
- package/dist/config.js +99 -0
- package/dist/config.js.map +1 -0
- package/dist/core/archive.d.ts +27 -0
- package/dist/core/archive.js +101 -0
- package/dist/core/archive.js.map +1 -0
- package/dist/core/chunker.d.ts +28 -0
- package/dist/core/chunker.js +67 -0
- package/dist/core/chunker.js.map +1 -0
- package/dist/core/clean.d.ts +36 -0
- package/dist/core/clean.js +182 -0
- package/dist/core/clean.js.map +1 -0
- package/dist/core/collect.d.ts +39 -0
- package/dist/core/collect.js +194 -0
- package/dist/core/collect.js.map +1 -0
- package/dist/core/concurrency.d.ts +2 -0
- package/dist/core/concurrency.js +16 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/cursor.d.ts +31 -0
- package/dist/core/cursor.js +31 -0
- package/dist/core/cursor.js.map +1 -0
- package/dist/core/events/claude.d.ts +7 -0
- package/dist/core/events/claude.js +287 -0
- package/dist/core/events/claude.js.map +1 -0
- package/dist/core/events/codex.d.ts +6 -0
- package/dist/core/events/codex.js +149 -0
- package/dist/core/events/codex.js.map +1 -0
- package/dist/core/events/doubao.d.ts +9 -0
- package/dist/core/events/doubao.js +151 -0
- package/dist/core/events/doubao.js.map +1 -0
- package/dist/core/events/index.d.ts +6 -0
- package/dist/core/events/index.js +20 -0
- package/dist/core/events/index.js.map +1 -0
- package/dist/core/events/opencode.d.ts +7 -0
- package/dist/core/events/opencode.js +189 -0
- package/dist/core/events/opencode.js.map +1 -0
- package/dist/core/events/schema.d.ts +83 -0
- package/dist/core/events/schema.js +35 -0
- package/dist/core/events/schema.js.map +1 -0
- package/dist/core/layout.d.ts +26 -0
- package/dist/core/layout.js +122 -0
- package/dist/core/layout.js.map +1 -0
- package/dist/core/lessons/analyze.d.ts +40 -0
- package/dist/core/lessons/analyze.js +107 -0
- package/dist/core/lessons/analyze.js.map +1 -0
- package/dist/core/lessons/audit.d.ts +51 -0
- package/dist/core/lessons/audit.js +57 -0
- package/dist/core/lessons/audit.js.map +1 -0
- package/dist/core/lessons/compaction.d.ts +5 -0
- package/dist/core/lessons/compaction.js +58 -0
- package/dist/core/lessons/compaction.js.map +1 -0
- package/dist/core/lessons/config.d.ts +15 -0
- package/dist/core/lessons/config.js +31 -0
- package/dist/core/lessons/config.js.map +1 -0
- package/dist/core/lessons/existing.d.ts +4 -0
- package/dist/core/lessons/existing.js +22 -0
- package/dist/core/lessons/existing.js.map +1 -0
- package/dist/core/lessons/extract.d.ts +21 -0
- package/dist/core/lessons/extract.js +106 -0
- package/dist/core/lessons/extract.js.map +1 -0
- package/dist/core/lessons/frontmatter.d.ts +29 -0
- package/dist/core/lessons/frontmatter.js +87 -0
- package/dist/core/lessons/frontmatter.js.map +1 -0
- package/dist/core/lessons/graph.d.ts +256 -0
- package/dist/core/lessons/graph.js +86 -0
- package/dist/core/lessons/graph.js.map +1 -0
- package/dist/core/lessons/jev.d.ts +36 -0
- package/dist/core/lessons/jev.js +100 -0
- package/dist/core/lessons/jev.js.map +1 -0
- package/dist/core/lessons/layout.d.ts +2 -0
- package/dist/core/lessons/layout.js +56 -0
- package/dist/core/lessons/layout.js.map +1 -0
- package/dist/core/lessons/learn.d.ts +38 -0
- package/dist/core/lessons/learn.js +190 -0
- package/dist/core/lessons/learn.js.map +1 -0
- package/dist/core/lessons/llm.d.ts +42 -0
- package/dist/core/lessons/llm.js +106 -0
- package/dist/core/lessons/llm.js.map +1 -0
- package/dist/core/lessons/memo.d.ts +19 -0
- package/dist/core/lessons/memo.js +170 -0
- package/dist/core/lessons/memo.js.map +1 -0
- package/dist/core/lessons/merge.d.ts +39 -0
- package/dist/core/lessons/merge.js +79 -0
- package/dist/core/lessons/merge.js.map +1 -0
- package/dist/core/lessons/queue.d.ts +6 -0
- package/dist/core/lessons/queue.js +52 -0
- package/dist/core/lessons/queue.js.map +1 -0
- package/dist/core/lessons/recent.d.ts +21 -0
- package/dist/core/lessons/recent.js +111 -0
- package/dist/core/lessons/recent.js.map +1 -0
- package/dist/core/lessons/schema.d.ts +26 -0
- package/dist/core/lessons/schema.js +64 -0
- package/dist/core/lessons/schema.js.map +1 -0
- package/dist/core/lessons/select.d.ts +19 -0
- package/dist/core/lessons/select.js +55 -0
- package/dist/core/lessons/select.js.map +1 -0
- package/dist/core/lessons/sidecar.d.ts +23 -0
- package/dist/core/lessons/sidecar.js +171 -0
- package/dist/core/lessons/sidecar.js.map +1 -0
- package/dist/core/lessons/storage.d.ts +21 -0
- package/dist/core/lessons/storage.js +65 -0
- package/dist/core/lessons/storage.js.map +1 -0
- package/dist/core/lessons/usage.d.ts +31 -0
- package/dist/core/lessons/usage.js +91 -0
- package/dist/core/lessons/usage.js.map +1 -0
- package/dist/core/lessons/write.d.ts +18 -0
- package/dist/core/lessons/write.js +131 -0
- package/dist/core/lessons/write.js.map +1 -0
- package/dist/core/library/build.d.ts +22 -0
- package/dist/core/library/build.js +163 -0
- package/dist/core/library/build.js.map +1 -0
- package/dist/core/library/group.d.ts +14 -0
- package/dist/core/library/group.js +129 -0
- package/dist/core/library/group.js.map +1 -0
- package/dist/core/library/index-file.d.ts +7 -0
- package/dist/core/library/index-file.js +250 -0
- package/dist/core/library/index-file.js.map +1 -0
- package/dist/core/library/schema.d.ts +78 -0
- package/dist/core/library/schema.js +37 -0
- package/dist/core/library/schema.js.map +1 -0
- package/dist/core/library/skills.d.ts +26 -0
- package/dist/core/library/skills.js +168 -0
- package/dist/core/library/skills.js.map +1 -0
- package/dist/core/library/sync.d.ts +48 -0
- package/dist/core/library/sync.js +271 -0
- package/dist/core/library/sync.js.map +1 -0
- package/dist/core/lock.d.ts +6 -0
- package/dist/core/lock.js +56 -0
- package/dist/core/lock.js.map +1 -0
- package/dist/core/pull.d.ts +23 -0
- package/dist/core/pull.js +147 -0
- package/dist/core/pull.js.map +1 -0
- package/dist/core/push.d.ts +29 -0
- package/dist/core/push.js +170 -0
- package/dist/core/push.js.map +1 -0
- package/dist/core/remote-layout.d.ts +67 -0
- package/dist/core/remote-layout.js +94 -0
- package/dist/core/remote-layout.js.map +1 -0
- package/dist/core/sessions.d.ts +29 -0
- package/dist/core/sessions.js +96 -0
- package/dist/core/sessions.js.map +1 -0
- package/dist/core/state.d.ts +94 -0
- package/dist/core/state.js +62 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/sync.d.ts +48 -0
- package/dist/core/sync.js +140 -0
- package/dist/core/sync.js.map +1 -0
- package/dist/core/time-window.d.ts +8 -0
- package/dist/core/time-window.js +36 -0
- package/dist/core/time-window.js.map +1 -0
- package/dist/daemon.d.ts +73 -0
- package/dist/daemon.js +272 -0
- package/dist/daemon.js.map +1 -0
- package/dist/format.d.ts +4 -0
- package/dist/format.js +28 -0
- package/dist/format.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/main.js.map +1 -0
- package/dist/paths.d.ts +19 -0
- package/dist/paths.js +19 -0
- package/dist/paths.js.map +1 -0
- package/dist/progress-console.d.ts +12 -0
- package/dist/progress-console.js +42 -0
- package/dist/progress-console.js.map +1 -0
- package/dist/progress.d.ts +7 -0
- package/dist/progress.js +6 -0
- package/dist/progress.js.map +1 -0
- package/dist/remote/create-remote.d.ts +20 -0
- package/dist/remote/create-remote.js +86 -0
- package/dist/remote/create-remote.js.map +1 -0
- package/dist/remote/prompt.d.ts +17 -0
- package/dist/remote/prompt.js +111 -0
- package/dist/remote/prompt.js.map +1 -0
- package/dist/remote/s3-sigv4.d.ts +28 -0
- package/dist/remote/s3-sigv4.js +100 -0
- package/dist/remote/s3-sigv4.js.map +1 -0
- package/dist/remote/s3-store.d.ts +75 -0
- package/dist/remote/s3-store.js +438 -0
- package/dist/remote/s3-store.js.map +1 -0
- package/dist/remote/s3-vendors.d.ts +31 -0
- package/dist/remote/s3-vendors.js +47 -0
- package/dist/remote/s3-vendors.js.map +1 -0
- package/dist/remote/store.d.ts +44 -0
- package/dist/remote/store.js +2 -0
- package/dist/remote/store.js.map +1 -0
- package/dist/remote/webdav-store.d.ts +41 -0
- package/dist/remote/webdav-store.js +285 -0
- package/dist/remote/webdav-store.js.map +1 -0
- package/dist/sqlite.d.ts +3 -0
- package/dist/sqlite.js +12 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/status.d.ts +82 -0
- package/dist/status.js +162 -0
- package/dist/status.js.map +1 -0
- package/package.json +43 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { hostname } from 'node:os';
|
|
3
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { Command, CommanderError } from 'commander';
|
|
6
|
+
import { confirm, text } from '@clack/prompts';
|
|
7
|
+
import pc from 'picocolors';
|
|
8
|
+
import { trovePaths } from './paths.js';
|
|
9
|
+
import { loadConfig, resolveRawSync, saveConfig } from './config.js';
|
|
10
|
+
import { loadState } from './core/state.js';
|
|
11
|
+
import { sync } from './core/sync.js';
|
|
12
|
+
import { deleteOrphans, scanOrphans } from './core/clean.js';
|
|
13
|
+
import { listSessions } from './core/sessions.js';
|
|
14
|
+
import { ensureLibraryLayout, cleanupLegacyEvents, migrateLegacyLayout, librarySkillsDir, libraryLessonsDir, safeSessionName } from './core/layout.js';
|
|
15
|
+
import { loadLibraryIndex, rebuildUnifiedIndex } from './core/library/index-file.js';
|
|
16
|
+
import { parseSince, formatSince } from './core/time-window.js';
|
|
17
|
+
import { computeLessonsSummary, computeLibraryPending, computeStatus, probeRemote, } from './status.js';
|
|
18
|
+
import { humanSize, humanSpan } from './format.js';
|
|
19
|
+
import { createRemoteFromConfig, describeRemote, isRemoteConfigured, remoteIdentity, } from './remote/create-remote.js';
|
|
20
|
+
import { promptRemoteConfig } from './remote/prompt.js';
|
|
21
|
+
import { createAdapters } from './adapters/index.js';
|
|
22
|
+
import { createConsoleProgress } from './progress-console.js';
|
|
23
|
+
import { silentProgress } from './progress.js';
|
|
24
|
+
import { formatDiff, readLatestDiff, readSessionDiff } from './core/lessons/audit.js';
|
|
25
|
+
import { LocalStorageAdapter } from './core/lessons/storage.js';
|
|
26
|
+
import { refreshSidecars } from './core/lessons/sidecar.js';
|
|
27
|
+
import { runLessonExtraction, learnPendingBatch, learnRecentBatch } from './core/lessons/learn.js';
|
|
28
|
+
import { formatUsageSummary, readUsageRecords, summarizeUsage } from './core/lessons/usage.js';
|
|
29
|
+
import { learnClaudeMemo } from './core/lessons/memo.js';
|
|
30
|
+
import { learnRecentMatters } from './core/lessons/recent.js';
|
|
31
|
+
import { enqueueLearn, readLearnQueue } from './core/lessons/queue.js';
|
|
32
|
+
import { daemonState, daemonLog, runDaemon, restartDaemon, startDaemon, stopDaemon, } from './daemon.js';
|
|
33
|
+
export const VERSION = '0.2.0';
|
|
34
|
+
function defaultCreateRemote(config) {
|
|
35
|
+
return createRemoteFromConfig(config);
|
|
36
|
+
}
|
|
37
|
+
function defaultResolveAdapters(config) {
|
|
38
|
+
return createAdapters().filter((a) => config.sources[a.id]?.enabled !== false);
|
|
39
|
+
}
|
|
40
|
+
function getPath(obj, key) {
|
|
41
|
+
return key.split('.').reduce((o, k) => {
|
|
42
|
+
if (o == null)
|
|
43
|
+
return undefined;
|
|
44
|
+
return o[k];
|
|
45
|
+
}, obj);
|
|
46
|
+
}
|
|
47
|
+
function setPath(obj, key, value) {
|
|
48
|
+
const parts = key.split('.');
|
|
49
|
+
let cur = obj;
|
|
50
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
51
|
+
const k = parts[i];
|
|
52
|
+
if (cur[k] == null || typeof cur[k] !== 'object')
|
|
53
|
+
cur[k] = {};
|
|
54
|
+
cur = cur[k];
|
|
55
|
+
}
|
|
56
|
+
cur[parts[parts.length - 1]] = value;
|
|
57
|
+
}
|
|
58
|
+
function parseValue(raw) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(raw);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return raw;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function formatStatus(r, opts) {
|
|
67
|
+
const lines = [pc.bold(`设备 ${r.deviceName} (${r.deviceId})`), '', pc.bold('本地')];
|
|
68
|
+
if (r.harnesses.every((h) => h.sessionCount === 0))
|
|
69
|
+
lines.push(' (还没有归档数据)');
|
|
70
|
+
for (const h of r.harnesses) {
|
|
71
|
+
lines.push(` ${h.displayName.padEnd(12)} ${pc.green(`${h.sessionCount} 个会话`)} · ${humanSize(h.bytes)}`);
|
|
72
|
+
}
|
|
73
|
+
lines.push('', pc.bold('技能'), opts.library
|
|
74
|
+
? ` ${pc.green(`${opts.library.skills} 个技能`)}`
|
|
75
|
+
: ' 尚未构建(跑一次 trove sync)');
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
const LESSON_LABELS = [
|
|
79
|
+
['preferences', '偏好'],
|
|
80
|
+
['entities', '实体'],
|
|
81
|
+
['experiences', '经验'],
|
|
82
|
+
['events', '事件'],
|
|
83
|
+
['resources', '资源'],
|
|
84
|
+
];
|
|
85
|
+
function formatLessons(lessons, learnQueue) {
|
|
86
|
+
const lines = ['', pc.bold('学习沉淀')];
|
|
87
|
+
if (lessons.total === 0) {
|
|
88
|
+
lines.push(' 暂无记忆(跑一次 trove learn)');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const parts = LESSON_LABELS.map(([k, label]) => `${label} ${lessons[k]}`).join(' · ');
|
|
92
|
+
lines.push(` ${pc.green(`${lessons.total} 条记忆`)} · ${parts}`);
|
|
93
|
+
}
|
|
94
|
+
lines.push(` 待学习 ${learnQueue.length > 0 ? pc.yellow(`${learnQueue.length} 个会话`) : '无'}`);
|
|
95
|
+
return lines;
|
|
96
|
+
}
|
|
97
|
+
function formatRemote(url, probe, selfId, now) {
|
|
98
|
+
const head = ['', pc.bold(`远端 ${url || '未配置'}`)];
|
|
99
|
+
if (!probe)
|
|
100
|
+
return [...head, ' 未配置(先 trove init)'];
|
|
101
|
+
if (!probe.ok)
|
|
102
|
+
return [...head, ` ${pc.yellow('不可达')} · ${probe.error ?? ''}`.trimEnd()];
|
|
103
|
+
const self = probe.devices.find((d) => d.id === selfId);
|
|
104
|
+
const detail = [`可达`, `${probe.devices.length} 台设备`];
|
|
105
|
+
if (self?.lastSeen)
|
|
106
|
+
detail.push(`本机 ${humanSpan(now - self.lastSeen)}前在线`);
|
|
107
|
+
const lines = [...head, ` ${pc.green(detail.join(' · '))}`];
|
|
108
|
+
for (const d of probe.devices) {
|
|
109
|
+
if (d.id === selfId)
|
|
110
|
+
continue;
|
|
111
|
+
const seen = d.lastSeen ? `${humanSpan(now - d.lastSeen)}前在线` : '在线时间未知';
|
|
112
|
+
lines.push(` ${d.name} (${d.id}) · ${seen}`);
|
|
113
|
+
}
|
|
114
|
+
return lines;
|
|
115
|
+
}
|
|
116
|
+
function formatDaemon(s, now) {
|
|
117
|
+
const head = ['', pc.bold('daemon')];
|
|
118
|
+
if (!s.running)
|
|
119
|
+
return [...head, ' 未运行 · 用 trove daemon start 启动'];
|
|
120
|
+
const st = s.status;
|
|
121
|
+
const lines = [
|
|
122
|
+
...head,
|
|
123
|
+
` ${pc.green('运行中')} (pid ${s.pid}) · 每 ${st?.intervalSeconds ?? '?'}s · 时间窗 ${st?.since ?? '?'}`,
|
|
124
|
+
];
|
|
125
|
+
if (st?.lastRunAt) {
|
|
126
|
+
lines.push(` 上次同步 ${humanSpan(now - st.lastRunAt)}前 · 耗时 ${((st.lastDurationMs ?? 0) / 1000).toFixed(1)}s` +
|
|
127
|
+
(st.nextRunAt ? ` · 下次约 ${humanSpan(st.nextRunAt - now)}后` : ''));
|
|
128
|
+
}
|
|
129
|
+
if (st?.lastResult) {
|
|
130
|
+
const c = st.lastResult;
|
|
131
|
+
lines.push(` 最近一轮 收集 ${c.collected.archived} · 拉取 ${c.pulled.downloadedItems} · ` +
|
|
132
|
+
`推送 ${c.pushed.uploadedItems}`);
|
|
133
|
+
}
|
|
134
|
+
if (st?.lastError)
|
|
135
|
+
lines.push(pc.yellow(` 最近错误: ${st.lastError}`));
|
|
136
|
+
return lines;
|
|
137
|
+
}
|
|
138
|
+
function formatPending(pending, library, rawSync) {
|
|
139
|
+
const lines = ['', pc.bold('待同步')];
|
|
140
|
+
const libFiles = library.sessions.files + library.skills.files + library.index.files;
|
|
141
|
+
if (rawSync && pending.files === 0 && libFiles === 0)
|
|
142
|
+
return [...lines, ' 无'];
|
|
143
|
+
if (!rawSync)
|
|
144
|
+
lines.push(' raw 同步已关闭(仅同步 library)');
|
|
145
|
+
else if (pending.files > 0) {
|
|
146
|
+
lines.push(` 会话 ${pc.yellow(`${pending.sessions} 个`)} · ${pending.files} 个文件 · ${humanSize(pending.bytes)}`);
|
|
147
|
+
lines.push(` ${pending.byHarness.map((h) => `${h.displayName} ${h.sessions}`).join(' · ')}`);
|
|
148
|
+
}
|
|
149
|
+
if (library.sessions.files > 0) {
|
|
150
|
+
lines.push(` 会话文件 ${library.sessions.count} 个会话 · ${library.sessions.files} 个文件 · ` +
|
|
151
|
+
`${humanSize(library.sessions.bytes)}`);
|
|
152
|
+
}
|
|
153
|
+
if (library.skills.files > 0) {
|
|
154
|
+
lines.push(` 技能 ${pc.yellow(`${library.skills.count} 个`)} · ${library.skills.files} 个文件 · ` +
|
|
155
|
+
`${humanSize(library.skills.bytes)}`);
|
|
156
|
+
}
|
|
157
|
+
if (library.index.files > 0) {
|
|
158
|
+
lines.push(` 索引 ${library.index.files} 个文件 · ${humanSize(library.index.bytes)}`);
|
|
159
|
+
}
|
|
160
|
+
return lines;
|
|
161
|
+
}
|
|
162
|
+
function looksLikeDateWindow(since) {
|
|
163
|
+
return since === 'all' ? 'all' : since;
|
|
164
|
+
}
|
|
165
|
+
async function loadRuntime(paths, createRemote, resolveAdapters) {
|
|
166
|
+
const config = await loadConfig(paths.config);
|
|
167
|
+
// 未配置远端时降级为本地模式,不报错
|
|
168
|
+
const remote = isRemoteConfigured(config.remote) ? createRemote(config.remote) : null;
|
|
169
|
+
return { config, remote, adapters: resolveAdapters(config) };
|
|
170
|
+
}
|
|
171
|
+
const ORPHAN_LABELS = {
|
|
172
|
+
'raw-chunk': 'raw 旧分块',
|
|
173
|
+
'raw-file': 'raw 整文件',
|
|
174
|
+
'library-session': 'library 会话',
|
|
175
|
+
'library-skill': 'library 技能',
|
|
176
|
+
};
|
|
177
|
+
function printOrphanScan(scan, out) {
|
|
178
|
+
out(pc.cyan('待清理对象(只含 24 小时前的对象):'));
|
|
179
|
+
const counts = new Map();
|
|
180
|
+
for (const o of scan.objects) {
|
|
181
|
+
const c = counts.get(o.category) ?? { n: 0, bytes: 0 };
|
|
182
|
+
c.n++;
|
|
183
|
+
c.bytes += o.size;
|
|
184
|
+
counts.set(o.category, c);
|
|
185
|
+
}
|
|
186
|
+
for (const cat of Object.keys(ORPHAN_LABELS)) {
|
|
187
|
+
const c = counts.get(cat);
|
|
188
|
+
if (c)
|
|
189
|
+
out(` ${ORPHAN_LABELS[cat]} ${c.n} 个 (${humanSize(c.bytes)})`);
|
|
190
|
+
}
|
|
191
|
+
out(scan.objects.length
|
|
192
|
+
? ` 合计 ${scan.objects.length} 个 (${humanSize(scan.bytes)})`
|
|
193
|
+
: ' 无');
|
|
194
|
+
if (scan.skippedDevices.length) {
|
|
195
|
+
out(pc.yellow(` 跳过设备(索引缺失,一个都不删): ${scan.skippedDevices.join(', ')}`));
|
|
196
|
+
}
|
|
197
|
+
if (scan.librarySkipped) {
|
|
198
|
+
out(pc.yellow(' library 分片不完整,本次跳过 library 清理'));
|
|
199
|
+
}
|
|
200
|
+
if (scan.skippedSkills.length) {
|
|
201
|
+
out(pc.yellow(` 保留整个技能目录(meta 缺失): ${[...new Set(scan.skippedSkills)].join(', ')}`));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function cmdClean(opts, paths, createRemote, out, interactive) {
|
|
205
|
+
const { remote } = await loadRuntime(paths, createRemote, () => []);
|
|
206
|
+
if (!remote) {
|
|
207
|
+
out('本地模式(未配置远端),没有可清理的对象');
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const scan = await scanOrphans(remote);
|
|
211
|
+
printOrphanScan(scan, out);
|
|
212
|
+
if (scan.objects.length === 0)
|
|
213
|
+
return 0;
|
|
214
|
+
if (opts.dryRun) {
|
|
215
|
+
out('dry-run:未删除任何对象');
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
let go = opts.yes === true;
|
|
219
|
+
if (!go && interactive) {
|
|
220
|
+
const ans = await confirm({
|
|
221
|
+
message: `确认删除这 ${scan.objects.length} 个对象?`,
|
|
222
|
+
initialValue: false,
|
|
223
|
+
});
|
|
224
|
+
go = ans === true;
|
|
225
|
+
}
|
|
226
|
+
if (!go) {
|
|
227
|
+
out('未删除。确认无误后运行 `trove clean --yes`。');
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
const r = await deleteOrphans(remote, scan);
|
|
231
|
+
out(`已删除 ${r.deleted} 个对象 (${humanSize(r.bytes)})${r.failed ? `, ${r.failed} 个失败` : ''}`);
|
|
232
|
+
return 0;
|
|
233
|
+
}
|
|
234
|
+
async function cmdSync(opts, paths, createRemote, resolveAdapters, out, rawOut, tty, interactive, now) {
|
|
235
|
+
const { config, remote, adapters } = await loadRuntime(paths, createRemote, resolveAdapters);
|
|
236
|
+
const since = opts.since ?? 'all';
|
|
237
|
+
const cutoff = parseSince(since, now());
|
|
238
|
+
const progress = opts.quiet
|
|
239
|
+
? silentProgress
|
|
240
|
+
: createConsoleProgress({ write: rawOut, tty, now });
|
|
241
|
+
const rawSync = resolveRawSync(config, opts.rawExplicit);
|
|
242
|
+
const report = await sync({
|
|
243
|
+
adapters,
|
|
244
|
+
deviceId: config.device.id,
|
|
245
|
+
deviceName: config.device.name,
|
|
246
|
+
vaultRoot: paths.vault,
|
|
247
|
+
statePath: paths.state,
|
|
248
|
+
tmpDir: paths.tmp,
|
|
249
|
+
remote,
|
|
250
|
+
remoteId: remoteIdentity(config.remote),
|
|
251
|
+
cutoff,
|
|
252
|
+
progress,
|
|
253
|
+
now,
|
|
254
|
+
pullOnly: opts.pullOnly,
|
|
255
|
+
pushOnly: opts.pushOnly,
|
|
256
|
+
rawSync,
|
|
257
|
+
});
|
|
258
|
+
const c = report.collected;
|
|
259
|
+
out(`发现 ${c.discovered} 项 · 会话 ${c.sessions} · 归档 ${c.archived} · 追加 ${c.appended} · ` +
|
|
260
|
+
`跳过 ${c.skipped} · 窗口外 ${c.outOfWindow}`);
|
|
261
|
+
if (!remote)
|
|
262
|
+
out(pc.yellow('本地模式(未配置远端,仅本地 .trove 归档)'));
|
|
263
|
+
out(`拉取 ${report.pulled.downloadedItems} 项 (${humanSize(report.pulled.downloadedBytes)}) · ` +
|
|
264
|
+
`推送 ${report.pushed.uploadedItems} 项 (${humanSize(report.pushed.uploadedBytes)}) · ` +
|
|
265
|
+
`跳过 ${report.pushed.skippedItems}`);
|
|
266
|
+
out(`库: 拉取 ${report.library.pulled.downloaded} 项 · ` +
|
|
267
|
+
`推送 ${report.library.pushed.uploaded} 项 (${humanSize(report.library.pushed.bytes)})`);
|
|
268
|
+
out(pc.green(`同步完成${rawSync ? '' : ' · 仅 library'} · ${formatSince(cutoff)} · ` +
|
|
269
|
+
`${(report.durationMs / 1000).toFixed(1)}s`));
|
|
270
|
+
// 切换远端后首次同步:询问是否清理旧对象,默认不清理
|
|
271
|
+
if (report.remoteChanged && remote && interactive) {
|
|
272
|
+
out(pc.yellow('检测到远端已切换。清理前建议其他设备停止同步。'));
|
|
273
|
+
const yes = await confirm({
|
|
274
|
+
message: '是否清理 24 小时前、且不再被索引引用的旧对象?',
|
|
275
|
+
initialValue: false,
|
|
276
|
+
});
|
|
277
|
+
if (yes === true) {
|
|
278
|
+
const scan = await scanOrphans(remote);
|
|
279
|
+
printOrphanScan(scan, out);
|
|
280
|
+
if (scan.objects.length > 0) {
|
|
281
|
+
const r = await deleteOrphans(remote, scan);
|
|
282
|
+
out(`已删除 ${r.deleted} 个对象 (${humanSize(r.bytes)})${r.failed ? `, ${r.failed} 个失败` : ''}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
out('未清理。之后可运行 `trove clean` 查看。');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function cmdSessions(opts, paths, out) {
|
|
291
|
+
const all = await listSessions(paths.vault, {
|
|
292
|
+
harness: opts.harness,
|
|
293
|
+
device: opts.device,
|
|
294
|
+
grep: opts.grep,
|
|
295
|
+
});
|
|
296
|
+
const limit = opts.limit === undefined ? 50 : Math.max(0, Number(opts.limit) || 0);
|
|
297
|
+
const list = limit > 0 ? all.slice(0, limit) : all;
|
|
298
|
+
if (opts.json) {
|
|
299
|
+
out(JSON.stringify(list, null, 2));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (!list.length) {
|
|
303
|
+
out('没有匹配的会话。');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
for (const s of list) {
|
|
307
|
+
const when = s.startedAt ? s.startedAt.replace('T', ' ').slice(0, 16) : '时间未知';
|
|
308
|
+
out(`${pc.dim(when)} ${s.harness.padEnd(12)} ${pc.green(s.session.slice(0, 12))} ` +
|
|
309
|
+
`${s.title ?? s.firstText ?? ''}`);
|
|
310
|
+
const sub = [s.cwd, s.model, humanSize(s.bytes)].filter(Boolean).join(' · ');
|
|
311
|
+
if (sub)
|
|
312
|
+
out(` ${pc.dim(sub)}`);
|
|
313
|
+
}
|
|
314
|
+
out('');
|
|
315
|
+
out(pc.dim(`${all.length} 个会话` +
|
|
316
|
+
(all.length > list.length ? `(显示前 ${list.length} 个,用 --limit 调整)` : '')));
|
|
317
|
+
}
|
|
318
|
+
async function cmdSkillsList(paths, out, json) {
|
|
319
|
+
const index = (await loadLibraryIndex(paths.vault)) ?? (await rebuildUnifiedIndex(paths.vault));
|
|
320
|
+
const list = Object.values(index.skills).sort((a, b) => a.name.localeCompare(b.name));
|
|
321
|
+
if (json) {
|
|
322
|
+
out(JSON.stringify(list, null, 2));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
if (!list.length) {
|
|
326
|
+
out('还没有汇总的技能。');
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
for (const skill of list) {
|
|
330
|
+
const latest = skill.versions.find((v) => v.hash === skill.latest);
|
|
331
|
+
const version = latest.version ? `v${latest.version}` : '未标版本';
|
|
332
|
+
out(`${skill.name.padEnd(28)} ${version.padEnd(12)} ${latest.source.padEnd(8)} ` +
|
|
333
|
+
`${skill.versions.length} 个版本 · ${latest.devices.join(',')}`);
|
|
334
|
+
if (latest.origin)
|
|
335
|
+
out(` ${pc.dim(latest.origin)}`);
|
|
336
|
+
}
|
|
337
|
+
out('');
|
|
338
|
+
out(pc.dim(`${list.length} 个技能`));
|
|
339
|
+
}
|
|
340
|
+
async function cmdSkillsShow(paths, name, out) {
|
|
341
|
+
const index = (await loadLibraryIndex(paths.vault)) ?? (await rebuildUnifiedIndex(paths.vault));
|
|
342
|
+
const skill = index.skills[name];
|
|
343
|
+
if (!skill) {
|
|
344
|
+
out(`没有找到技能: ${name}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
out(pc.bold(skill.name));
|
|
348
|
+
for (const v of skill.versions) {
|
|
349
|
+
const mark = v.hash === skill.latest ? pc.green('latest') : ' ';
|
|
350
|
+
out(` ${mark} ${v.hash.slice(0, 12)} ${(v.version ?? '-').padEnd(10)} ${v.source.padEnd(8)} ` +
|
|
351
|
+
`${v.devices.join(',')} ${v.updatedAt ?? ''}`);
|
|
352
|
+
if (v.origin)
|
|
353
|
+
out(` ${pc.dim(v.origin)}`);
|
|
354
|
+
}
|
|
355
|
+
const latest = skill.versions.find((v) => v.hash === skill.latest);
|
|
356
|
+
if (latest) {
|
|
357
|
+
out('');
|
|
358
|
+
out(pc.dim(join(librarySkillsDir(paths.vault), safeSessionName(skill.name), 'versions', latest.hash)));
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
async function cmdLibraryReindex(paths, out) {
|
|
362
|
+
await ensureLibraryLayout(paths.vault);
|
|
363
|
+
const index = await rebuildUnifiedIndex(paths.vault);
|
|
364
|
+
out(pc.green(`索引已重建: ${Object.keys(index.sessions).length} 个会话 · ` +
|
|
365
|
+
`${Object.keys(index.skills).length} 个技能 · ${index.hash.slice(0, 12)}`));
|
|
366
|
+
}
|
|
367
|
+
async function cmdLearnList(paths, sessionId, out) {
|
|
368
|
+
const storage = new LocalStorageAdapter(libraryLessonsDir(paths.vault));
|
|
369
|
+
const diff = sessionId ? await readSessionDiff(storage, sessionId) : await readLatestDiff(storage);
|
|
370
|
+
out(diff ? formatDiff(diff) : sessionId ? `无会话 ${sessionId} 的提取记录` : '尚无提取记录');
|
|
371
|
+
}
|
|
372
|
+
async function cmdLearnExtract(paths, sessionId, lessonDeps, out, force) {
|
|
373
|
+
const outcome = await runLessonExtraction(paths.vault, sessionId, { ...lessonDeps, usagePath: paths.usageLog }, { force });
|
|
374
|
+
if (outcome.skipReason === 'already_learned') {
|
|
375
|
+
out('该会话已提取过,跳过(用 --force 强制重跑)');
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (outcome.skipReason) {
|
|
379
|
+
out(`会话未达提取阈值,跳过: ${outcome.skipReason}`);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
out(formatDiff(outcome.diff));
|
|
383
|
+
}
|
|
384
|
+
async function cmdLearnMemo(paths, lessonDeps, out, force) {
|
|
385
|
+
const adapter = createAdapters().find((a) => a.id === 'claude-code');
|
|
386
|
+
if (!adapter || adapter.kind !== 'files') {
|
|
387
|
+
out('未找到 claude-code 适配器,无法读取其记忆目录');
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const config = await loadConfig(paths.config);
|
|
391
|
+
const outcome = await learnClaudeMemo(paths.vault, adapter.source.root, { ...lessonDeps, config }, { force });
|
|
392
|
+
if (outcome.skipReason === 'already_learned') {
|
|
393
|
+
out('Claude Code 记忆已学习过,跳过(用 --force 强制重跑)');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (outcome.skipReason) {
|
|
397
|
+
out(`未学习到 Claude Code 记忆,跳过: ${outcome.skipReason}`);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
out(formatDiff(outcome.diff));
|
|
401
|
+
}
|
|
402
|
+
async function cmdLearnRecent(paths, lessonDeps, out, force) {
|
|
403
|
+
const config = await loadConfig(paths.config);
|
|
404
|
+
const outcome = await learnRecentMatters(paths.vault, { ...lessonDeps, config }, { force });
|
|
405
|
+
if (outcome.skipReason === 'unchanged') {
|
|
406
|
+
out('近期方向无变化,跳过(用 --force 强制重跑)');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (outcome.skipReason) {
|
|
410
|
+
out(`无近期会话可聚合,跳过: ${outcome.skipReason}`);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
out(formatDiff(outcome.diff));
|
|
414
|
+
}
|
|
415
|
+
async function cmdStatus(paths, createRemote, resolveAdapters, out, now) {
|
|
416
|
+
const config = await loadConfig(paths.config);
|
|
417
|
+
const state = await loadState(paths.state);
|
|
418
|
+
const adapters = resolveAdapters(config);
|
|
419
|
+
const report = computeStatus({
|
|
420
|
+
state,
|
|
421
|
+
deviceId: config.device.id,
|
|
422
|
+
deviceName: config.device.name,
|
|
423
|
+
adapters,
|
|
424
|
+
});
|
|
425
|
+
const rawSync = resolveRawSync(config);
|
|
426
|
+
// 远端探测与本地统计并行;未配置就不发请求
|
|
427
|
+
const probe = isRemoteConfigured(config.remote)
|
|
428
|
+
? probeRemote(createRemote(config.remote), AbortSignal.timeout(5000))
|
|
429
|
+
: Promise.resolve(null);
|
|
430
|
+
const [index, libraryPending, daemon, remote, lessons, learnQueue] = await Promise.all([
|
|
431
|
+
loadLibraryIndex(paths.vault),
|
|
432
|
+
computeLibraryPending(paths.vault, state),
|
|
433
|
+
daemonState(paths),
|
|
434
|
+
probe,
|
|
435
|
+
computeLessonsSummary(paths.vault),
|
|
436
|
+
readLearnQueue(paths.learnQueue),
|
|
437
|
+
]);
|
|
438
|
+
const nowMs = now();
|
|
439
|
+
out([
|
|
440
|
+
...formatStatus(report, {
|
|
441
|
+
library: index ? { skills: Object.keys(index.skills).length } : null,
|
|
442
|
+
}),
|
|
443
|
+
...formatLessons(lessons, learnQueue),
|
|
444
|
+
...formatRemote(describeRemote(config.remote), remote, config.device.id, nowMs),
|
|
445
|
+
...formatDaemon(daemon, nowMs),
|
|
446
|
+
...formatPending(report.pending, libraryPending, rawSync),
|
|
447
|
+
].join('\n'));
|
|
448
|
+
}
|
|
449
|
+
function isCancel(v) {
|
|
450
|
+
return typeof v === 'symbol';
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* 命令行给了足够的 S3 参数就直接用(供脚本化配置);否则返回 undefined 走交互引导。
|
|
454
|
+
*/
|
|
455
|
+
export function buildS3FromFlags(opts, existing) {
|
|
456
|
+
const prev = existing.type === 's3' ? existing : undefined;
|
|
457
|
+
const bucket = opts.bucket ?? prev?.bucket;
|
|
458
|
+
const region = opts.region ?? prev?.region;
|
|
459
|
+
const accessKeyId = opts.accessKeyId ?? prev?.accessKeyId;
|
|
460
|
+
const secretAccessKey = opts.secretAccessKey ?? process.env.TROVE_PASSWORD ?? prev?.secretAccessKey;
|
|
461
|
+
const endpoint = opts.endpoint ?? prev?.endpoint;
|
|
462
|
+
if (!bucket || !region || !accessKeyId || !secretAccessKey)
|
|
463
|
+
return undefined;
|
|
464
|
+
return {
|
|
465
|
+
type: 's3',
|
|
466
|
+
provider: opts.provider ?? prev?.provider ?? 'aliyun',
|
|
467
|
+
region,
|
|
468
|
+
bucket,
|
|
469
|
+
accessKeyId,
|
|
470
|
+
secretAccessKey,
|
|
471
|
+
...(endpoint ? { endpoint } : {}),
|
|
472
|
+
forcePathStyle: opts.forcePathStyle ?? prev?.forcePathStyle ?? false,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
async function cmdConfig(paths, out, err, interactive) {
|
|
476
|
+
if (!interactive) {
|
|
477
|
+
throw new Error('非交互环境无法交互式配置,请用 `trove config set <key> <value>`。');
|
|
478
|
+
}
|
|
479
|
+
const existing = await loadConfig(paths.config);
|
|
480
|
+
const name = await text({ message: '设备名', initialValue: existing.device.name });
|
|
481
|
+
if (isCancel(name)) {
|
|
482
|
+
err('已取消。');
|
|
483
|
+
return 1;
|
|
484
|
+
}
|
|
485
|
+
// 远端:先问类型,再按类型问字段
|
|
486
|
+
const remote = await promptRemoteConfig(existing.remote, true);
|
|
487
|
+
if (remote === undefined) {
|
|
488
|
+
err('已取消。');
|
|
489
|
+
return 1;
|
|
490
|
+
}
|
|
491
|
+
const config = {
|
|
492
|
+
...existing,
|
|
493
|
+
device: { ...existing.device, name: name },
|
|
494
|
+
remote,
|
|
495
|
+
};
|
|
496
|
+
await saveConfig(paths.config, config);
|
|
497
|
+
out(pc.green('配置已保存'));
|
|
498
|
+
out(` 设备: ${config.device.name}`);
|
|
499
|
+
out(` 远端: ${describeRemote(config.remote) || '未配置(本地模式)'}`);
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
async function cmdRename(name, paths, out) {
|
|
503
|
+
const trimmed = name.trim();
|
|
504
|
+
if (!trimmed)
|
|
505
|
+
throw new Error('设备名不能为空');
|
|
506
|
+
const existing = await loadConfig(paths.config);
|
|
507
|
+
// 只改显示名;设备 id 保持稳定,已归档路径与远端索引不受影响
|
|
508
|
+
await saveConfig(paths.config, { ...existing, device: { ...existing.device, name: trimmed } });
|
|
509
|
+
out(pc.green(`设备已重命名为 ${trimmed} (id ${existing.device.id} 不变)`));
|
|
510
|
+
out(' 下次 trove sync 会把新名字写回远端设备 meta');
|
|
511
|
+
}
|
|
512
|
+
async function cmdInit(opts, paths, createRemote, start, out, err, interactive) {
|
|
513
|
+
const existing = await loadConfig(paths.config);
|
|
514
|
+
const name = opts.name ?? (interactive ? await text({ message: '设备名', initialValue: hostname() }) : hostname());
|
|
515
|
+
if (isCancel(name)) {
|
|
516
|
+
err('已取消。');
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
// 远端配置:命令行 flag 优先,其次交互引导(先选类型),都没有则进入本地模式
|
|
520
|
+
let remoteConfig;
|
|
521
|
+
if (opts.type === 's3') {
|
|
522
|
+
// S3 路径:命令行给了完整参数就用参数,否则交互引导
|
|
523
|
+
const fromFlags = buildS3FromFlags(opts, existing.remote);
|
|
524
|
+
if (fromFlags) {
|
|
525
|
+
remoteConfig = fromFlags;
|
|
526
|
+
}
|
|
527
|
+
else if (!interactive) {
|
|
528
|
+
err('S3 参数不完整:需要 --bucket --region --access-key-id --secret-access-key(或 TROVE_PASSWORD)。');
|
|
529
|
+
return 1;
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
const picked = await promptRemoteConfig(existing.remote, interactive, 's3');
|
|
533
|
+
if (!picked) {
|
|
534
|
+
err('已取消。');
|
|
535
|
+
return 1;
|
|
536
|
+
}
|
|
537
|
+
remoteConfig = picked;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else if (opts.url) {
|
|
541
|
+
// WebDAV:命令行给了地址就走 flag 路径;填了地址就必须有密码
|
|
542
|
+
const password = opts.password ?? process.env.TROVE_PASSWORD;
|
|
543
|
+
if (!password)
|
|
544
|
+
throw new Error('未提供密码(--password 或 TROVE_PASSWORD)。');
|
|
545
|
+
remoteConfig = {
|
|
546
|
+
type: 'webdav',
|
|
547
|
+
url: opts.url,
|
|
548
|
+
username: opts.username ?? 'webdav',
|
|
549
|
+
password,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
else if (interactive) {
|
|
553
|
+
// 交互且没给 flag:先选远端类型(WebDAV / S3),再按类型问字段
|
|
554
|
+
const picked = await promptRemoteConfig(existing.remote, true);
|
|
555
|
+
if (!picked) {
|
|
556
|
+
err('已取消。');
|
|
557
|
+
return 1;
|
|
558
|
+
}
|
|
559
|
+
remoteConfig = picked;
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
// 本次 init 没给远端:进入本地模式(清掉旧远端)
|
|
563
|
+
remoteConfig = { type: 'webdav', url: '', username: 'webdav', password: '' };
|
|
564
|
+
}
|
|
565
|
+
// 保留已有的 sources / sync / daemon 设置,只覆盖设备名与远端
|
|
566
|
+
const config = {
|
|
567
|
+
...existing,
|
|
568
|
+
device: { id: existing.device.id, name: name },
|
|
569
|
+
remote: remoteConfig,
|
|
570
|
+
};
|
|
571
|
+
if (isRemoteConfigured(remoteConfig)) {
|
|
572
|
+
const remote = createRemote(remoteConfig);
|
|
573
|
+
await mkdir(paths.tmp, { recursive: true });
|
|
574
|
+
const probePath = `.trove-probe/${randomUUID()}.json`;
|
|
575
|
+
const probeLocal = join(paths.tmp, `probe-${randomUUID()}.json`);
|
|
576
|
+
await writeFile(probeLocal, '{"probe":true}\n');
|
|
577
|
+
await remote.writeFile(probePath, probeLocal);
|
|
578
|
+
const echoed = await remote.readJson(probePath);
|
|
579
|
+
await remote.delete(probePath);
|
|
580
|
+
await rm(probeLocal, { force: true });
|
|
581
|
+
if (!echoed)
|
|
582
|
+
throw new Error('连通性探针失败:写入的文件读不回来。');
|
|
583
|
+
}
|
|
584
|
+
await saveConfig(paths.config, config);
|
|
585
|
+
out(pc.green('初始化完成'));
|
|
586
|
+
out(` 设备: ${name} (${config.device.id})`);
|
|
587
|
+
if (isRemoteConfigured(remoteConfig)) {
|
|
588
|
+
out(` 远端: ${describeRemote(remoteConfig)}`);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
out(` 远端: ${pc.yellow('未配置(本地模式,仅本地 .trove 归档)')}`);
|
|
592
|
+
}
|
|
593
|
+
if (opts.daemon === false || !config.daemon.startOnInit)
|
|
594
|
+
return 0;
|
|
595
|
+
try {
|
|
596
|
+
const since = config.daemon.since;
|
|
597
|
+
parseSince(since, Date.now()); // 配置可能被手改成非法值,启动前拦住
|
|
598
|
+
const interval = Math.max(5, config.daemon.intervalSeconds);
|
|
599
|
+
const pid = await start({
|
|
600
|
+
paths,
|
|
601
|
+
intervalSeconds: interval,
|
|
602
|
+
since,
|
|
603
|
+
env: { TROVE_HOME: paths.base },
|
|
604
|
+
});
|
|
605
|
+
out(` daemon: 已启动 (pid ${pid}) · 每 ${interval}s · 时间窗 ${since} · 首轮同步进行中`);
|
|
606
|
+
}
|
|
607
|
+
catch (e) {
|
|
608
|
+
const msg = e.message;
|
|
609
|
+
out(msg.includes('已在运行') ? ` daemon: ${msg}` : pc.yellow(` daemon: 启动失败 —— ${msg}`));
|
|
610
|
+
}
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
613
|
+
function daemonRuntime(paths, opts, createRemote, resolveAdapters, now, lessonDeps, learnIntervalSeconds, learnBatch) {
|
|
614
|
+
const intervalSeconds = Math.max(5, Number(opts.interval) || 300);
|
|
615
|
+
return {
|
|
616
|
+
paths,
|
|
617
|
+
intervalSeconds,
|
|
618
|
+
since: opts.since,
|
|
619
|
+
runOnce: async (progress, signal) => {
|
|
620
|
+
const { config, remote, adapters } = await loadRuntime(paths, createRemote, resolveAdapters);
|
|
621
|
+
return sync({
|
|
622
|
+
adapters,
|
|
623
|
+
deviceId: config.device.id,
|
|
624
|
+
deviceName: config.device.name,
|
|
625
|
+
vaultRoot: paths.vault,
|
|
626
|
+
statePath: paths.state,
|
|
627
|
+
tmpDir: paths.tmp,
|
|
628
|
+
remote,
|
|
629
|
+
remoteId: remoteIdentity(config.remote),
|
|
630
|
+
cutoff: parseSince(opts.since, now()),
|
|
631
|
+
progress,
|
|
632
|
+
now,
|
|
633
|
+
rawSync: resolveRawSync(config, opts.rawExplicit),
|
|
634
|
+
signal,
|
|
635
|
+
});
|
|
636
|
+
},
|
|
637
|
+
afterSync: async (report) => {
|
|
638
|
+
const ids = report.collected.changedSessions;
|
|
639
|
+
await enqueueLearn(paths.learnQueue, ids);
|
|
640
|
+
return ids.length ? `经验提取入队 ${ids.length} 个新会话` : undefined;
|
|
641
|
+
},
|
|
642
|
+
learnTick: async () => {
|
|
643
|
+
// 每 tick 重读配置,模型端点变更即时生效,无需重启 daemon。
|
|
644
|
+
const config = await loadConfig(paths.config);
|
|
645
|
+
const pending = await learnPendingBatch(paths.vault, paths.learnQueue, { ...lessonDeps, config, usagePath: paths.usageLog }, learnBatch, {
|
|
646
|
+
log: (l) => void daemonLog(paths, l),
|
|
647
|
+
});
|
|
648
|
+
// 近期方向聚合:hash 不变则 skip(不烧 token),有实际更新才记一行,避免每 tick 刷噪音。
|
|
649
|
+
const recent = await learnRecentMatters(paths.vault, { ...lessonDeps, config });
|
|
650
|
+
const parts = [pending, recent.diff ? '近期方向已刷新' : undefined].filter((x) => !!x);
|
|
651
|
+
return parts.length ? parts.join(' · ') : undefined;
|
|
652
|
+
},
|
|
653
|
+
learnIntervalSeconds,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
/** 主入口:用 commander 定义命令并分发。返回进程退出码。 */
|
|
657
|
+
export async function run(argv, deps = {}) {
|
|
658
|
+
const out = deps.out ?? ((l) => console.log(l));
|
|
659
|
+
const err = deps.err ?? ((l) => console.error(l));
|
|
660
|
+
const rawOut = deps.rawOut ?? ((s) => process.stdout.write(s));
|
|
661
|
+
const paths = trovePaths(deps.base);
|
|
662
|
+
const createRemote = deps.createRemote ?? defaultCreateRemote;
|
|
663
|
+
const resolveAdapters = deps.resolveAdapters ?? defaultResolveAdapters;
|
|
664
|
+
const now = deps.now ?? Date.now;
|
|
665
|
+
const start = deps.startDaemon ?? startDaemon;
|
|
666
|
+
const restart = deps.restartDaemon ?? restartDaemon;
|
|
667
|
+
const interactive = deps.isTTY ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
668
|
+
const tty = deps.isTTY ?? Boolean(process.stdout.isTTY);
|
|
669
|
+
const program = new Command();
|
|
670
|
+
program
|
|
671
|
+
.name('trove')
|
|
672
|
+
.description('AI 会话资产归档与多设备同步')
|
|
673
|
+
.version(VERSION);
|
|
674
|
+
program
|
|
675
|
+
.command('init')
|
|
676
|
+
.description('首次配置(设备名、远端后端、凭证)')
|
|
677
|
+
.option('--name <name>', '设备名')
|
|
678
|
+
.option('--type <type>', '远端类型:webdav / s3(不给则交互询问)')
|
|
679
|
+
.option('--url <url>', 'WebDAV 地址')
|
|
680
|
+
.option('--username <user>', 'WebDAV 用户名')
|
|
681
|
+
.option('--password <pass>', 'WebDAV 密码(也可用 TROVE_PASSWORD 环境变量)')
|
|
682
|
+
.option('--provider <vendor>', 'S3 厂商:aliyun / tencent')
|
|
683
|
+
.option('--region <region>', 'S3 地域(如 cn-hangzhou / ap-guangzhou)')
|
|
684
|
+
.option('--bucket <bucket>', 'S3 Bucket 名')
|
|
685
|
+
.option('--endpoint <url>', '自定义 S3 endpoint(覆盖厂商推导,MinIO 等私有部署用)')
|
|
686
|
+
.option('--force-path-style', 'S3 用 path-style 寻址(bucket 放路径里,私有部署常用)')
|
|
687
|
+
.option('--access-key-id <id>', 'S3 AccessKey ID')
|
|
688
|
+
.option('--secret-access-key <key>', 'S3 Secret AccessKey(也可用 TROVE_PASSWORD 环境变量)')
|
|
689
|
+
.option('--no-daemon', '初始化后不自动启动 daemon')
|
|
690
|
+
.action(async (opts) => {
|
|
691
|
+
const code = await cmdInit(opts, paths, createRemote, start, out, err, interactive);
|
|
692
|
+
if (code !== 0)
|
|
693
|
+
process.exitCode = code;
|
|
694
|
+
});
|
|
695
|
+
program
|
|
696
|
+
.command('rename <name>')
|
|
697
|
+
.description('重命名当前设备(只改显示名,设备 id 与已归档路径不变)')
|
|
698
|
+
.action(async (name) => {
|
|
699
|
+
await cmdRename(name, paths, out);
|
|
700
|
+
});
|
|
701
|
+
const configCmd = program.command('config').description('交互式配置(子命令:path/get/set)');
|
|
702
|
+
configCmd.action(async () => {
|
|
703
|
+
const code = await cmdConfig(paths, out, err, interactive);
|
|
704
|
+
if (code !== 0)
|
|
705
|
+
process.exitCode = code;
|
|
706
|
+
});
|
|
707
|
+
configCmd
|
|
708
|
+
.command('path')
|
|
709
|
+
.description('打印配置文件路径')
|
|
710
|
+
.action(() => out(paths.config));
|
|
711
|
+
configCmd
|
|
712
|
+
.command('get <key>')
|
|
713
|
+
.description('读取配置项(点路径,如 device.name)')
|
|
714
|
+
.action(async (key) => {
|
|
715
|
+
const config = await loadConfig(paths.config);
|
|
716
|
+
const v = getPath(config, key);
|
|
717
|
+
out(v === undefined ? '' : typeof v === 'string' ? v : JSON.stringify(v));
|
|
718
|
+
});
|
|
719
|
+
configCmd
|
|
720
|
+
.command('set <key> <value>')
|
|
721
|
+
.description('写入配置项(JSON 值)')
|
|
722
|
+
.action(async (key, value) => {
|
|
723
|
+
const config = await loadConfig(paths.config);
|
|
724
|
+
setPath(config, key, parseValue(value));
|
|
725
|
+
await saveConfig(paths.config, config);
|
|
726
|
+
});
|
|
727
|
+
program
|
|
728
|
+
.command('sync')
|
|
729
|
+
.description('收集本机会话并同步远端 (collect → pull → push)')
|
|
730
|
+
.option('--since <range>', '只收集最近变更的会话:7d / 24h / 2026-10-01 / all', 'all')
|
|
731
|
+
.option('--pull-only', '只拉取')
|
|
732
|
+
.option('--push-only', '只推送')
|
|
733
|
+
.option('--raw', '同步 raw(覆盖配置)')
|
|
734
|
+
.option('--no-raw', '只同步 library(raw 只归档到本地,不上传/不拉取)')
|
|
735
|
+
.option('--quiet', '不显示逐项进度')
|
|
736
|
+
.action(async function (opts) {
|
|
737
|
+
const rawExplicit = this.getOptionValueSource('raw') === 'cli' ? opts.raw === true : undefined;
|
|
738
|
+
await cmdSync({ ...opts, rawExplicit }, paths, createRemote, resolveAdapters, out, rawOut, tty, interactive, now);
|
|
739
|
+
});
|
|
740
|
+
program
|
|
741
|
+
.command('clean')
|
|
742
|
+
.description('清理远端中不再被索引引用的旧对象 (默认只列出)')
|
|
743
|
+
.option('--dry-run', '只列出,不删除')
|
|
744
|
+
.option('--yes', '不再确认,直接删除')
|
|
745
|
+
.action(async (opts) => {
|
|
746
|
+
const code = await cmdClean(opts, paths, createRemote, out, interactive);
|
|
747
|
+
if (code !== 0)
|
|
748
|
+
process.exitCode = code;
|
|
749
|
+
});
|
|
750
|
+
program
|
|
751
|
+
.command('sessions')
|
|
752
|
+
.description('列出/检索 library 会话')
|
|
753
|
+
.option('--harness <id>', '只看某个工具:claude-code / codex / opencode / doubao')
|
|
754
|
+
.option('--device <id>', '只看某台设备')
|
|
755
|
+
.option('--grep <text>', '按标题/摘要/目录/模型搜索')
|
|
756
|
+
.option('--limit <n>', '最多显示多少条(0 = 全部)', '50')
|
|
757
|
+
.option('--json', '输出 JSON')
|
|
758
|
+
.action(async (opts) => {
|
|
759
|
+
await cmdSessions(opts, paths, out);
|
|
760
|
+
});
|
|
761
|
+
program
|
|
762
|
+
.command('skills')
|
|
763
|
+
.description('查看汇总后的技能')
|
|
764
|
+
.argument('[action]', 'list(默认) / show', 'list')
|
|
765
|
+
.argument('[name]', '技能名(show 时必填)')
|
|
766
|
+
.option('--json', '输出 JSON')
|
|
767
|
+
.action(async (action, name, opts) => {
|
|
768
|
+
if (action === 'show' || action === 'list') {
|
|
769
|
+
if (action === 'show') {
|
|
770
|
+
if (!name) {
|
|
771
|
+
err('用法: trove skills show <name>');
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
await cmdSkillsShow(paths, name, out);
|
|
775
|
+
}
|
|
776
|
+
else {
|
|
777
|
+
await cmdSkillsList(paths, out, Boolean(opts.json));
|
|
778
|
+
}
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
err(`未知的 skills 子命令: ${action}`);
|
|
782
|
+
});
|
|
783
|
+
const libraryCmd = program.command('library').description('个人资产层管理');
|
|
784
|
+
libraryCmd
|
|
785
|
+
.command('reindex')
|
|
786
|
+
.description('从设备分片重建统一索引')
|
|
787
|
+
.action(async () => {
|
|
788
|
+
await cmdLibraryReindex(paths, out);
|
|
789
|
+
});
|
|
790
|
+
program
|
|
791
|
+
.command('learn')
|
|
792
|
+
.description('经验提取与审计查看')
|
|
793
|
+
.argument('[sessionId]', '对单会话跑提取流水线(缺省需配合 --list)')
|
|
794
|
+
.option('--list', '查看最近一次提取的审计 diff')
|
|
795
|
+
.option('--session <id>', '配合 --list 指定会话')
|
|
796
|
+
.option('--force', '即使已提取过也强制重跑')
|
|
797
|
+
.option('-n, --recent <n>', '按最近优先提取 n 个未学习会话')
|
|
798
|
+
.action(async (sessionId, opts) => {
|
|
799
|
+
if (opts.list) {
|
|
800
|
+
await cmdLearnList(paths, opts.session, out);
|
|
801
|
+
}
|
|
802
|
+
else if (opts.recent !== undefined) {
|
|
803
|
+
const n = Number(opts.recent);
|
|
804
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
805
|
+
err('用法: trove learn -n <正整数>');
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const config = await loadConfig(paths.config);
|
|
809
|
+
out(`准备提取最近 ${n} 个未学习会话...`);
|
|
810
|
+
const summary = await learnRecentBatch(paths.vault, n, { ...deps.lessonDeps, config }, { log: (l) => out(l) });
|
|
811
|
+
out(pc.green(summary));
|
|
812
|
+
}
|
|
813
|
+
else if (sessionId) {
|
|
814
|
+
const config = await loadConfig(paths.config);
|
|
815
|
+
await cmdLearnExtract(paths, sessionId, { ...deps.lessonDeps, config }, out, opts.force === true);
|
|
816
|
+
}
|
|
817
|
+
else {
|
|
818
|
+
err('用法: trove learn <session-id> | trove learn -n <数量> | trove learn --list [--session <id>]');
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
program
|
|
822
|
+
.command('learn-memo')
|
|
823
|
+
.description('学习 Claude Code 自带记忆系统(~/.claude/projects/*/memory)进入 lessons')
|
|
824
|
+
.option('--force', '即使已学习过也强制重跑')
|
|
825
|
+
.action(async (opts) => {
|
|
826
|
+
await cmdLearnMemo(paths, deps.lessonDeps, out, opts.force === true);
|
|
827
|
+
});
|
|
828
|
+
program
|
|
829
|
+
.command('learn-recent')
|
|
830
|
+
.description('从最近会话聚合用户近期工作方向(时间+项目+主题)写入 recent_matters')
|
|
831
|
+
.option('--force', '即使输入无变化也强制重跑')
|
|
832
|
+
.action(async (opts) => {
|
|
833
|
+
await cmdLearnRecent(paths, deps.lessonDeps, out, opts.force === true);
|
|
834
|
+
});
|
|
835
|
+
program
|
|
836
|
+
.command('usage')
|
|
837
|
+
.description('查看 trove 自身消耗的 LLM token 用量(learn 与 daemon 定时任务)')
|
|
838
|
+
.option('--since <range>', '时间范围(如 7d / 24h / 2026-10-01 / all)', 'all')
|
|
839
|
+
.option('--json', '输出 JSON')
|
|
840
|
+
.action(async (opts) => {
|
|
841
|
+
const since = opts.since ?? 'all';
|
|
842
|
+
const cutoff = parseSince(since, now()); // 非法值抛错,由 run 统一报错退出
|
|
843
|
+
const records = await readUsageRecords(paths.usageLog);
|
|
844
|
+
const filtered = cutoff ? records.filter((r) => Date.parse(r.at) >= cutoff) : records;
|
|
845
|
+
const summary = summarizeUsage(filtered);
|
|
846
|
+
if (opts.json) {
|
|
847
|
+
out(JSON.stringify({ since, ...summary }, null, 2));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
out(formatUsageSummary(summary, since).join('\n'));
|
|
851
|
+
});
|
|
852
|
+
program
|
|
853
|
+
.command('refresh')
|
|
854
|
+
.description('强制重建 lessons 分层概览(index/.abstract.md/.overview.md)')
|
|
855
|
+
.action(async () => {
|
|
856
|
+
const storage = new LocalStorageAdapter(libraryLessonsDir(paths.vault));
|
|
857
|
+
const written = await refreshSidecars(storage, { trigger: 'manual_refresh', now: () => new Date(now()) });
|
|
858
|
+
out(pc.green(`已重建 ${written.length} 个分层概览文件`));
|
|
859
|
+
});
|
|
860
|
+
program
|
|
861
|
+
.command('status')
|
|
862
|
+
.description('概览(本地归档、远端、daemon、待同步)')
|
|
863
|
+
.action(async () => {
|
|
864
|
+
await cmdStatus(paths, createRemote, resolveAdapters, out, now);
|
|
865
|
+
});
|
|
866
|
+
const daemonCmd = program.command('daemon').description('常驻后台同步服务');
|
|
867
|
+
/** start/restart 共用:解析 interval/since/raw,CLI 显式参数优先于 config。 */
|
|
868
|
+
async function daemonStartOptions(cmd, opts) {
|
|
869
|
+
const config = await loadConfig(paths.config);
|
|
870
|
+
const since = opts.since ?? config.daemon.since;
|
|
871
|
+
parseSince(since, now()); // 启动前校验,避免子进程每轮都失败
|
|
872
|
+
const interval = Math.max(5, Number(opts.interval ?? config.daemon.intervalSeconds) || 300);
|
|
873
|
+
const rawExplicit = cmd.getOptionValueSource('raw') === 'cli' ? opts.raw === true : undefined;
|
|
874
|
+
return { paths, intervalSeconds: interval, since, raw: rawExplicit };
|
|
875
|
+
}
|
|
876
|
+
daemonCmd
|
|
877
|
+
.command('start')
|
|
878
|
+
.description('后台启动 daemon')
|
|
879
|
+
.option('--interval <seconds>', '轮询间隔秒数(默认取 config.daemon.intervalSeconds)')
|
|
880
|
+
.option('--since <range>', '只收集最近变更的会话(默认取 config.daemon.since)')
|
|
881
|
+
.option('--raw', '同步 raw(覆盖配置)')
|
|
882
|
+
.option('--no-raw', '只同步 library(raw 只归档到本地)')
|
|
883
|
+
.action(async function (opts) {
|
|
884
|
+
const startOpts = await daemonStartOptions(this, opts);
|
|
885
|
+
const pid = await start(startOpts);
|
|
886
|
+
out(pc.green(`daemon 已启动 (pid ${pid}) · 每 ${startOpts.intervalSeconds}s · ` +
|
|
887
|
+
`时间窗 ${looksLikeDateWindow(startOpts.since)}`));
|
|
888
|
+
});
|
|
889
|
+
daemonCmd
|
|
890
|
+
.command('restart')
|
|
891
|
+
.description('重启 daemon(停止后按同样参数重新拉起)')
|
|
892
|
+
.option('--interval <seconds>', '轮询间隔秒数(默认取 config.daemon.intervalSeconds)')
|
|
893
|
+
.option('--since <range>', '只收集最近变更的会话(默认取 config.daemon.since)')
|
|
894
|
+
.option('--raw', '同步 raw(覆盖配置)')
|
|
895
|
+
.option('--no-raw', '只同步 library(raw 只归档到本地)')
|
|
896
|
+
.action(async function (opts) {
|
|
897
|
+
const startOpts = await daemonStartOptions(this, opts);
|
|
898
|
+
const { pid, wasRunning } = await restart(startOpts);
|
|
899
|
+
out(pc.green(`daemon 已重启 (pid ${pid}) · 每 ${startOpts.intervalSeconds}s · ` +
|
|
900
|
+
`时间窗 ${looksLikeDateWindow(startOpts.since)}`));
|
|
901
|
+
if (!wasRunning)
|
|
902
|
+
out(pc.yellow(' 原 daemon 未在运行,已直接启动'));
|
|
903
|
+
});
|
|
904
|
+
daemonCmd
|
|
905
|
+
.command('stop')
|
|
906
|
+
.description('优雅停止 daemon')
|
|
907
|
+
.action(async () => {
|
|
908
|
+
const stopped = await stopDaemon(paths);
|
|
909
|
+
out(stopped ? pc.green('daemon 已停止') : 'daemon 未运行');
|
|
910
|
+
});
|
|
911
|
+
daemonCmd
|
|
912
|
+
.command('status')
|
|
913
|
+
.description('查看 daemon 状态')
|
|
914
|
+
.action(async () => {
|
|
915
|
+
const s = await daemonState(paths);
|
|
916
|
+
if (!s.running) {
|
|
917
|
+
out('daemon 未运行');
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
out(pc.green(`daemon 运行中 (pid ${s.pid})`));
|
|
921
|
+
if (s.status?.lastRunAt) {
|
|
922
|
+
const ago = Math.round((now() - s.status.lastRunAt) / 1000);
|
|
923
|
+
out(` 上次同步: ${ago}s 前 · 耗时 ${s.status.lastDurationMs ?? 0}ms`);
|
|
924
|
+
}
|
|
925
|
+
if (s.status?.lastResult) {
|
|
926
|
+
const c = s.status.lastResult;
|
|
927
|
+
out(` 结果: 收集 ${c.collected.archived} · 拉取 ${c.pulled.downloadedItems} · ` +
|
|
928
|
+
`推送 ${c.pushed.uploadedItems}`);
|
|
929
|
+
}
|
|
930
|
+
if (s.status?.lastError)
|
|
931
|
+
out(pc.yellow(` 最近错误: ${s.status.lastError}`));
|
|
932
|
+
});
|
|
933
|
+
daemonCmd
|
|
934
|
+
.command('run', { hidden: true })
|
|
935
|
+
.description('前台运行(daemon start 内部使用)')
|
|
936
|
+
.option('--interval <seconds>', '轮询间隔秒数', '300')
|
|
937
|
+
.option('--since <range>', '只收集最近变更的会话', '7d')
|
|
938
|
+
.option('--raw', '同步 raw(覆盖配置)')
|
|
939
|
+
.option('--no-raw', '只同步 library(raw 只归档到本地)')
|
|
940
|
+
.action(async function (opts) {
|
|
941
|
+
parseSince(opts.since, now());
|
|
942
|
+
const config = await loadConfig(paths.config);
|
|
943
|
+
const controller = new AbortController();
|
|
944
|
+
const stop = () => controller.abort();
|
|
945
|
+
process.once('SIGTERM', stop);
|
|
946
|
+
process.once('SIGINT', stop);
|
|
947
|
+
const rawExplicit = this.getOptionValueSource('raw') === 'cli' ? opts.raw === true : undefined;
|
|
948
|
+
await runDaemon(daemonRuntime(paths, { ...opts, rawExplicit }, createRemote, resolveAdapters, now, deps.lessonDeps, config.daemon.learnIntervalSeconds, config.daemon.learnBatch), controller.signal);
|
|
949
|
+
});
|
|
950
|
+
program.configureOutput({
|
|
951
|
+
writeOut: (str) => out(str.replace(/\n+$/, '')),
|
|
952
|
+
writeErr: (str) => err(str.replace(/\n+$/, '')),
|
|
953
|
+
});
|
|
954
|
+
program.exitOverride();
|
|
955
|
+
try {
|
|
956
|
+
await migrateLegacyLayout(paths.vault);
|
|
957
|
+
await cleanupLegacyEvents(paths.vault);
|
|
958
|
+
await ensureLibraryLayout(paths.vault);
|
|
959
|
+
await program.parseAsync(['node', 'trove', ...argv]);
|
|
960
|
+
return 0;
|
|
961
|
+
}
|
|
962
|
+
catch (e) {
|
|
963
|
+
if (e instanceof CommanderError) {
|
|
964
|
+
if (e.code === 'commander.helpDisplayed' || e.code === 'commander.version')
|
|
965
|
+
return 0;
|
|
966
|
+
return 1;
|
|
967
|
+
}
|
|
968
|
+
err(pc.red(`错误: ${e.message}`));
|
|
969
|
+
return 1;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
//# sourceMappingURL=cli.js.map
|