@getlore/cli 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 +13 -0
- package/README.md +80 -0
- package/dist/cli/colors.d.ts +48 -0
- package/dist/cli/colors.js +48 -0
- package/dist/cli/commands/ask.d.ts +7 -0
- package/dist/cli/commands/ask.js +97 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.js +484 -0
- package/dist/cli/commands/daemon.d.ts +22 -0
- package/dist/cli/commands/daemon.js +244 -0
- package/dist/cli/commands/docs.d.ts +7 -0
- package/dist/cli/commands/docs.js +188 -0
- package/dist/cli/commands/extensions.d.ts +7 -0
- package/dist/cli/commands/extensions.js +204 -0
- package/dist/cli/commands/misc.d.ts +7 -0
- package/dist/cli/commands/misc.js +172 -0
- package/dist/cli/commands/pending.d.ts +7 -0
- package/dist/cli/commands/pending.js +63 -0
- package/dist/cli/commands/projects.d.ts +7 -0
- package/dist/cli/commands/projects.js +136 -0
- package/dist/cli/commands/search.d.ts +7 -0
- package/dist/cli/commands/search.js +102 -0
- package/dist/cli/commands/skills.d.ts +24 -0
- package/dist/cli/commands/skills.js +447 -0
- package/dist/cli/commands/sources.d.ts +7 -0
- package/dist/cli/commands/sources.js +121 -0
- package/dist/cli/commands/sync.d.ts +31 -0
- package/dist/cli/commands/sync.js +768 -0
- package/dist/cli/helpers.d.ts +30 -0
- package/dist/cli/helpers.js +119 -0
- package/dist/core/auth.d.ts +62 -0
- package/dist/core/auth.js +330 -0
- package/dist/core/config.d.ts +41 -0
- package/dist/core/config.js +96 -0
- package/dist/core/data-repo.d.ts +31 -0
- package/dist/core/data-repo.js +146 -0
- package/dist/core/embedder.d.ts +22 -0
- package/dist/core/embedder.js +104 -0
- package/dist/core/git.d.ts +37 -0
- package/dist/core/git.js +140 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +5 -0
- package/dist/core/insight-extractor.d.ts +26 -0
- package/dist/core/insight-extractor.js +114 -0
- package/dist/core/local-search.d.ts +43 -0
- package/dist/core/local-search.js +221 -0
- package/dist/core/themes.d.ts +15 -0
- package/dist/core/themes.js +77 -0
- package/dist/core/types.d.ts +177 -0
- package/dist/core/types.js +9 -0
- package/dist/core/user-settings.d.ts +15 -0
- package/dist/core/user-settings.js +42 -0
- package/dist/core/vector-store-lance.d.ts +98 -0
- package/dist/core/vector-store-lance.js +384 -0
- package/dist/core/vector-store-supabase.d.ts +89 -0
- package/dist/core/vector-store-supabase.js +295 -0
- package/dist/core/vector-store.d.ts +131 -0
- package/dist/core/vector-store.js +503 -0
- package/dist/daemon-runner.d.ts +8 -0
- package/dist/daemon-runner.js +246 -0
- package/dist/extensions/config.d.ts +22 -0
- package/dist/extensions/config.js +102 -0
- package/dist/extensions/proposals.d.ts +30 -0
- package/dist/extensions/proposals.js +178 -0
- package/dist/extensions/registry.d.ts +35 -0
- package/dist/extensions/registry.js +309 -0
- package/dist/extensions/sandbox.d.ts +16 -0
- package/dist/extensions/sandbox.js +17 -0
- package/dist/extensions/types.d.ts +114 -0
- package/dist/extensions/types.js +4 -0
- package/dist/extensions/worker.d.ts +1 -0
- package/dist/extensions/worker.js +49 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +105 -0
- package/dist/mcp/handlers/archive-project.d.ts +51 -0
- package/dist/mcp/handlers/archive-project.js +112 -0
- package/dist/mcp/handlers/get-quotes.d.ts +27 -0
- package/dist/mcp/handlers/get-quotes.js +61 -0
- package/dist/mcp/handlers/get-source.d.ts +9 -0
- package/dist/mcp/handlers/get-source.js +40 -0
- package/dist/mcp/handlers/ingest.d.ts +25 -0
- package/dist/mcp/handlers/ingest.js +305 -0
- package/dist/mcp/handlers/list-projects.d.ts +4 -0
- package/dist/mcp/handlers/list-projects.js +16 -0
- package/dist/mcp/handlers/list-sources.d.ts +11 -0
- package/dist/mcp/handlers/list-sources.js +20 -0
- package/dist/mcp/handlers/research-agent.d.ts +21 -0
- package/dist/mcp/handlers/research-agent.js +369 -0
- package/dist/mcp/handlers/research.d.ts +22 -0
- package/dist/mcp/handlers/research.js +225 -0
- package/dist/mcp/handlers/retain.d.ts +18 -0
- package/dist/mcp/handlers/retain.js +92 -0
- package/dist/mcp/handlers/search.d.ts +52 -0
- package/dist/mcp/handlers/search.js +145 -0
- package/dist/mcp/handlers/sync.d.ts +47 -0
- package/dist/mcp/handlers/sync.js +211 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +268 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +297 -0
- package/dist/sync/config.d.ts +26 -0
- package/dist/sync/config.js +140 -0
- package/dist/sync/discover.d.ts +51 -0
- package/dist/sync/discover.js +190 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.js +11 -0
- package/dist/sync/process.d.ts +50 -0
- package/dist/sync/process.js +285 -0
- package/dist/sync/processors.d.ts +24 -0
- package/dist/sync/processors.js +351 -0
- package/dist/tui/browse-handlers-ask.d.ts +30 -0
- package/dist/tui/browse-handlers-ask.js +372 -0
- package/dist/tui/browse-handlers-autocomplete.d.ts +49 -0
- package/dist/tui/browse-handlers-autocomplete.js +270 -0
- package/dist/tui/browse-handlers-extensions.d.ts +18 -0
- package/dist/tui/browse-handlers-extensions.js +107 -0
- package/dist/tui/browse-handlers-pending.d.ts +22 -0
- package/dist/tui/browse-handlers-pending.js +100 -0
- package/dist/tui/browse-handlers-research.d.ts +32 -0
- package/dist/tui/browse-handlers-research.js +363 -0
- package/dist/tui/browse-handlers-tools.d.ts +42 -0
- package/dist/tui/browse-handlers-tools.js +289 -0
- package/dist/tui/browse-handlers.d.ts +239 -0
- package/dist/tui/browse-handlers.js +1944 -0
- package/dist/tui/browse-render-extensions.d.ts +14 -0
- package/dist/tui/browse-render-extensions.js +114 -0
- package/dist/tui/browse-render-tools.d.ts +18 -0
- package/dist/tui/browse-render-tools.js +259 -0
- package/dist/tui/browse-render.d.ts +51 -0
- package/dist/tui/browse-render.js +599 -0
- package/dist/tui/browse-types.d.ts +142 -0
- package/dist/tui/browse-types.js +70 -0
- package/dist/tui/browse-ui.d.ts +10 -0
- package/dist/tui/browse-ui.js +432 -0
- package/dist/tui/browse.d.ts +17 -0
- package/dist/tui/browse.js +625 -0
- package/dist/tui/markdown.d.ts +22 -0
- package/dist/tui/markdown.js +223 -0
- package/package.json +71 -0
- package/plugins/claude-code/.claude-plugin/plugin.json +10 -0
- package/plugins/claude-code/.mcp.json +6 -0
- package/plugins/claude-code/skills/lore/SKILL.md +63 -0
- package/plugins/codex/SKILL.md +36 -0
- package/plugins/codex/agents/openai.yaml +10 -0
- package/plugins/gemini/GEMINI.md +31 -0
- package/plugins/gemini/gemini-extension.json +11 -0
- package/skills/generic-agent.md +99 -0
- package/skills/openclaw.md +67 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync Command
|
|
3
|
+
*
|
|
4
|
+
* All sync-related functionality: one-time sync, daemon, watch, sources.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn, spawnSync } from 'child_process';
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
8
|
+
import { mkdir } from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import os from 'os';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
import { colors, c } from '../colors.js';
|
|
13
|
+
// Get the directory of this module (for finding daemon-runner.js)
|
|
14
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
+
const __dirname = path.dirname(__filename);
|
|
16
|
+
// Config directory for daemon files
|
|
17
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'lore');
|
|
18
|
+
const PID_FILE = path.join(CONFIG_DIR, 'daemon.pid');
|
|
19
|
+
const STATUS_FILE = path.join(CONFIG_DIR, 'daemon.status.json');
|
|
20
|
+
const LOG_FILE = path.join(CONFIG_DIR, 'daemon.log');
|
|
21
|
+
// launchd (macOS) constants
|
|
22
|
+
const LAUNCHD_LABEL = 'com.lore.daemon';
|
|
23
|
+
const LAUNCHD_PLIST_PATH = path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
24
|
+
function isMacOS() {
|
|
25
|
+
return process.platform === 'darwin';
|
|
26
|
+
}
|
|
27
|
+
function generatePlist(dataDir) {
|
|
28
|
+
const nodePath = process.execPath;
|
|
29
|
+
const scriptPath = path.join(__dirname, '..', '..', 'daemon-runner.js');
|
|
30
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
31
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
32
|
+
<plist version="1.0">
|
|
33
|
+
<dict>
|
|
34
|
+
<key>Label</key>
|
|
35
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
36
|
+
<key>ProgramArguments</key>
|
|
37
|
+
<array>
|
|
38
|
+
<string>${nodePath}</string>
|
|
39
|
+
<string>${scriptPath}</string>
|
|
40
|
+
<string>${dataDir}</string>
|
|
41
|
+
</array>
|
|
42
|
+
<key>KeepAlive</key>
|
|
43
|
+
<true/>
|
|
44
|
+
<key>RunAtLoad</key>
|
|
45
|
+
<true/>
|
|
46
|
+
<key>StandardOutPath</key>
|
|
47
|
+
<string>${LOG_FILE}</string>
|
|
48
|
+
<key>StandardErrorPath</key>
|
|
49
|
+
<string>${LOG_FILE}</string>
|
|
50
|
+
</dict>
|
|
51
|
+
</plist>`;
|
|
52
|
+
}
|
|
53
|
+
function isLaunchdInstalled() {
|
|
54
|
+
return isMacOS() && existsSync(LAUNCHD_PLIST_PATH);
|
|
55
|
+
}
|
|
56
|
+
function installLaunchdAgent(dataDir) {
|
|
57
|
+
const plistDir = path.dirname(LAUNCHD_PLIST_PATH);
|
|
58
|
+
if (!existsSync(plistDir)) {
|
|
59
|
+
// LaunchAgents dir should exist, but just in case
|
|
60
|
+
spawnSync('mkdir', ['-p', plistDir]);
|
|
61
|
+
}
|
|
62
|
+
writeFileSync(LAUNCHD_PLIST_PATH, generatePlist(dataDir));
|
|
63
|
+
// Unload first in case an old version is loaded
|
|
64
|
+
spawnSync('launchctl', ['unload', LAUNCHD_PLIST_PATH], { stdio: 'ignore' });
|
|
65
|
+
const result = spawnSync('launchctl', ['load', LAUNCHD_PLIST_PATH], { stdio: 'pipe' });
|
|
66
|
+
if (result.status !== 0) {
|
|
67
|
+
const stderr = result.stderr?.toString().trim();
|
|
68
|
+
if (stderr)
|
|
69
|
+
console.error(`launchctl load error: ${stderr}`);
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// launchctl load starts the process (RunAtLoad=true). Wait for PID file.
|
|
73
|
+
// The daemon-runner writes PID_FILE on startup.
|
|
74
|
+
for (let i = 0; i < 20; i++) {
|
|
75
|
+
spawnSync('sleep', ['0.25']);
|
|
76
|
+
if (existsSync(PID_FILE)) {
|
|
77
|
+
try {
|
|
78
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
79
|
+
process.kill(pid, 0); // verify alive
|
|
80
|
+
return { pid };
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// PID file exists but process not ready yet, keep waiting
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function uninstallLaunchdAgent() {
|
|
90
|
+
if (!existsSync(LAUNCHD_PLIST_PATH))
|
|
91
|
+
return;
|
|
92
|
+
spawnSync('launchctl', ['unload', LAUNCHD_PLIST_PATH], { stdio: 'ignore' });
|
|
93
|
+
try {
|
|
94
|
+
unlinkSync(LAUNCHD_PLIST_PATH);
|
|
95
|
+
}
|
|
96
|
+
catch { }
|
|
97
|
+
}
|
|
98
|
+
async function ensureConfigDir() {
|
|
99
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
100
|
+
}
|
|
101
|
+
function getPid() {
|
|
102
|
+
if (!existsSync(PID_FILE))
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, 0);
|
|
108
|
+
return pid;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
unlinkSync(PID_FILE);
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function getStatus() {
|
|
120
|
+
if (!existsSync(STATUS_FILE))
|
|
121
|
+
return null;
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(readFileSync(STATUS_FILE, 'utf-8'));
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function formatUptime(ms) {
|
|
130
|
+
const seconds = Math.floor(ms / 1000);
|
|
131
|
+
const minutes = Math.floor(seconds / 60);
|
|
132
|
+
const hours = Math.floor(minutes / 60);
|
|
133
|
+
const days = Math.floor(hours / 24);
|
|
134
|
+
if (days > 0)
|
|
135
|
+
return `${days}d ${hours % 24}h`;
|
|
136
|
+
if (hours > 0)
|
|
137
|
+
return `${hours}h ${minutes % 60}m`;
|
|
138
|
+
if (minutes > 0)
|
|
139
|
+
return `${minutes}m ${seconds % 60}s`;
|
|
140
|
+
return `${seconds}s`;
|
|
141
|
+
}
|
|
142
|
+
function formatAgo(ms) {
|
|
143
|
+
const seconds = Math.floor(ms / 1000);
|
|
144
|
+
if (seconds < 60)
|
|
145
|
+
return 'just now';
|
|
146
|
+
const minutes = Math.floor(seconds / 60);
|
|
147
|
+
if (minutes < 60)
|
|
148
|
+
return `${minutes}m ago`;
|
|
149
|
+
const hours = Math.floor(minutes / 60);
|
|
150
|
+
if (hours < 24)
|
|
151
|
+
return `${hours}h ago`;
|
|
152
|
+
const days = Math.floor(hours / 24);
|
|
153
|
+
return `${days}d ago`;
|
|
154
|
+
}
|
|
155
|
+
export function registerSyncCommand(program, defaultDataDir) {
|
|
156
|
+
const syncCmd = program
|
|
157
|
+
.command('sync')
|
|
158
|
+
.description('Sync and manage the knowledge repository')
|
|
159
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
160
|
+
.option('--dry-run', 'Show what would be synced without processing')
|
|
161
|
+
.option('--legacy', 'Use legacy disk-based sync only')
|
|
162
|
+
.option('--no-git', 'Skip git operations')
|
|
163
|
+
.action(async (options) => {
|
|
164
|
+
// Default action: one-time sync
|
|
165
|
+
const { handleSync } = await import('../../mcp/handlers/sync.js');
|
|
166
|
+
const dataDir = options.dataDir;
|
|
167
|
+
const dbPath = path.join(dataDir, 'lore.lance');
|
|
168
|
+
console.log(`\nLore Sync`);
|
|
169
|
+
console.log(`=========`);
|
|
170
|
+
console.log(`Data dir: ${dataDir}`);
|
|
171
|
+
if (options.dryRun)
|
|
172
|
+
console.log(`Mode: DRY RUN`);
|
|
173
|
+
console.log('');
|
|
174
|
+
const result = await handleSync(dbPath, dataDir, {
|
|
175
|
+
git_pull: options.git !== false,
|
|
176
|
+
git_push: options.git !== false,
|
|
177
|
+
dry_run: options.dryRun,
|
|
178
|
+
use_legacy: options.legacy,
|
|
179
|
+
}, { hookContext: { mode: 'cli' } });
|
|
180
|
+
if (result.git_pulled) {
|
|
181
|
+
console.log('✓ Pulled latest changes from git');
|
|
182
|
+
}
|
|
183
|
+
if (result.git_error) {
|
|
184
|
+
console.log(`⚠ Git: ${result.git_error}`);
|
|
185
|
+
}
|
|
186
|
+
if (result.discovery) {
|
|
187
|
+
console.log(`\nDiscovery:`);
|
|
188
|
+
console.log(` Sources scanned: ${result.discovery.sources_scanned}`);
|
|
189
|
+
console.log(` Files found: ${result.discovery.total_files}`);
|
|
190
|
+
console.log(` New files: ${result.discovery.new_files}`);
|
|
191
|
+
if (result.discovery.edited_files && result.discovery.edited_files > 0) {
|
|
192
|
+
console.log(` Edited files: ${result.discovery.edited_files}`);
|
|
193
|
+
}
|
|
194
|
+
console.log(` Already indexed: ${result.discovery.existing_files}`);
|
|
195
|
+
if (result.discovery.errors > 0) {
|
|
196
|
+
console.log(` Errors: ${result.discovery.errors}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (result.processing) {
|
|
200
|
+
console.log(`\nProcessed ${result.processing.processed} new files:`);
|
|
201
|
+
for (const title of result.processing.titles.slice(0, 10)) {
|
|
202
|
+
console.log(` • ${title}`);
|
|
203
|
+
}
|
|
204
|
+
if (result.processing.titles.length > 10) {
|
|
205
|
+
console.log(` ... and ${result.processing.titles.length - 10} more`);
|
|
206
|
+
}
|
|
207
|
+
if (result.processing.errors > 0) {
|
|
208
|
+
console.log(` Errors: ${result.processing.errors}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (result.sources_found > 0 || result.sources_indexed > 0) {
|
|
212
|
+
console.log(`\nLegacy Sync:`);
|
|
213
|
+
console.log(` Sources on disk: ${result.sources_found}`);
|
|
214
|
+
console.log(` Newly indexed: ${result.sources_indexed}`);
|
|
215
|
+
console.log(` Already indexed: ${result.already_indexed}`);
|
|
216
|
+
}
|
|
217
|
+
if (result.git_pushed) {
|
|
218
|
+
console.log('\n✓ Pushed changes to git');
|
|
219
|
+
}
|
|
220
|
+
console.log('\nSync complete!');
|
|
221
|
+
});
|
|
222
|
+
// Start daemon
|
|
223
|
+
syncCmd
|
|
224
|
+
.command('start')
|
|
225
|
+
.description('Start background sync daemon')
|
|
226
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
227
|
+
.action(async (options) => {
|
|
228
|
+
const result = await startDaemonProcess(options.dataDir);
|
|
229
|
+
if (!result) {
|
|
230
|
+
console.error('Failed to start daemon - check logs with: lore sync logs');
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (result.alreadyRunning) {
|
|
234
|
+
console.log(`Daemon already running (PID: ${result.pid})`);
|
|
235
|
+
console.log(`Use "lore sync status" to check status`);
|
|
236
|
+
console.log(`Use "lore sync stop" to stop it`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
console.log(`Daemon started (PID: ${result.pid})`);
|
|
240
|
+
console.log(`Log file: ${LOG_FILE}`);
|
|
241
|
+
console.log(`Use "lore sync logs" to view activity`);
|
|
242
|
+
});
|
|
243
|
+
// Stop daemon
|
|
244
|
+
syncCmd
|
|
245
|
+
.command('stop')
|
|
246
|
+
.description('Stop background sync daemon')
|
|
247
|
+
.action(async () => {
|
|
248
|
+
// Uninstall launchd agent so the daemon doesn't restart on login
|
|
249
|
+
if (isLaunchdInstalled()) {
|
|
250
|
+
uninstallLaunchdAgent();
|
|
251
|
+
}
|
|
252
|
+
const pid = getPid();
|
|
253
|
+
if (!pid) {
|
|
254
|
+
console.log('Daemon is not running');
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
process.kill(pid, 'SIGTERM');
|
|
259
|
+
console.log(`Daemon stopped (PID: ${pid})`);
|
|
260
|
+
if (existsSync(PID_FILE))
|
|
261
|
+
unlinkSync(PID_FILE);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
console.error(`Failed to stop daemon: ${error}`);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// Restart daemon
|
|
268
|
+
syncCmd
|
|
269
|
+
.command('restart')
|
|
270
|
+
.description('Restart background sync daemon')
|
|
271
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
272
|
+
.action(async (options) => {
|
|
273
|
+
// Uninstall launchd agent so it doesn't auto-restart during our restart
|
|
274
|
+
if (isLaunchdInstalled()) {
|
|
275
|
+
uninstallLaunchdAgent();
|
|
276
|
+
}
|
|
277
|
+
const pid = getPid();
|
|
278
|
+
if (pid) {
|
|
279
|
+
try {
|
|
280
|
+
process.kill(pid, 'SIGTERM');
|
|
281
|
+
console.log(`Stopped existing daemon (PID: ${pid})`);
|
|
282
|
+
if (existsSync(PID_FILE))
|
|
283
|
+
unlinkSync(PID_FILE);
|
|
284
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// Process might already be dead
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// startDaemonProcess will reinstall launchd with fresh config on macOS
|
|
291
|
+
const result = await startDaemonProcess(options.dataDir);
|
|
292
|
+
if (!result) {
|
|
293
|
+
console.error('Failed to restart daemon - check logs with: lore sync logs');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
console.log(`Daemon restarted (PID: ${result.pid})`);
|
|
297
|
+
});
|
|
298
|
+
// Daemon status
|
|
299
|
+
syncCmd
|
|
300
|
+
.command('status')
|
|
301
|
+
.description('Check sync daemon status')
|
|
302
|
+
.action(async () => {
|
|
303
|
+
const pid = getPid();
|
|
304
|
+
const status = getStatus();
|
|
305
|
+
console.log('');
|
|
306
|
+
console.log('Lore Sync Status');
|
|
307
|
+
console.log('================');
|
|
308
|
+
if (!pid) {
|
|
309
|
+
console.log('Daemon: NOT RUNNING');
|
|
310
|
+
console.log(`Auto-start: ${isLaunchdInstalled() ? 'enabled (launchd)' : 'not configured'}`);
|
|
311
|
+
console.log('');
|
|
312
|
+
console.log('Start with: lore sync start');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
console.log(`Daemon: RUNNING (PID: ${pid})`);
|
|
316
|
+
console.log(`Auto-start: ${isLaunchdInstalled() ? 'enabled (launchd)' : 'not configured'}`);
|
|
317
|
+
if (status) {
|
|
318
|
+
const started = new Date(status.started_at);
|
|
319
|
+
const uptime = formatUptime(Date.now() - started.getTime());
|
|
320
|
+
console.log(`Uptime: ${uptime}`);
|
|
321
|
+
if (status.last_sync) {
|
|
322
|
+
const lastSync = new Date(status.last_sync);
|
|
323
|
+
const ago = formatAgo(Date.now() - lastSync.getTime());
|
|
324
|
+
console.log(`Last sync: ${ago}`);
|
|
325
|
+
if (status.last_sync_result) {
|
|
326
|
+
const r = status.last_sync_result;
|
|
327
|
+
console.log(` Files scanned: ${r.files_scanned}`);
|
|
328
|
+
console.log(` Files processed: ${r.files_processed}`);
|
|
329
|
+
if (r.errors > 0) {
|
|
330
|
+
console.log(` Errors: ${r.errors}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
console.log('Last sync: (not yet synced)');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
console.log('');
|
|
339
|
+
console.log(`Log file: ${LOG_FILE}`);
|
|
340
|
+
console.log('View logs: lore sync logs');
|
|
341
|
+
});
|
|
342
|
+
// Daemon logs
|
|
343
|
+
syncCmd
|
|
344
|
+
.command('logs')
|
|
345
|
+
.description('View sync daemon logs')
|
|
346
|
+
.option('-f, --follow', 'Follow log output (like tail -f)')
|
|
347
|
+
.option('-n, --lines <n>', 'Number of lines to show', '50')
|
|
348
|
+
.action(async (options) => {
|
|
349
|
+
if (!existsSync(LOG_FILE)) {
|
|
350
|
+
console.log('No log file found. Daemon may not have run yet.');
|
|
351
|
+
console.log(`Expected: ${LOG_FILE}`);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (options.follow) {
|
|
355
|
+
const tail = spawn('tail', ['-f', LOG_FILE], {
|
|
356
|
+
stdio: 'inherit',
|
|
357
|
+
});
|
|
358
|
+
process.on('SIGINT', () => {
|
|
359
|
+
tail.kill();
|
|
360
|
+
process.exit(0);
|
|
361
|
+
});
|
|
362
|
+
await new Promise(() => { });
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
const content = readFileSync(LOG_FILE, 'utf-8');
|
|
366
|
+
const lines = content.trim().split('\n');
|
|
367
|
+
const n = parseInt(options.lines, 10);
|
|
368
|
+
const lastLines = lines.slice(-n);
|
|
369
|
+
console.log(`Last ${Math.min(n, lastLines.length)} log entries:\n`);
|
|
370
|
+
console.log(lastLines.join('\n'));
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
// Watch (foreground)
|
|
374
|
+
syncCmd
|
|
375
|
+
.command('watch')
|
|
376
|
+
.description('Watch directories and sync in foreground (shows live output)')
|
|
377
|
+
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
378
|
+
.option('--interval <ms>', 'Debounce interval in ms', '2000')
|
|
379
|
+
.option('--no-initial', 'Skip initial sync on startup')
|
|
380
|
+
.action(async (options) => {
|
|
381
|
+
const chokidar = await import('chokidar');
|
|
382
|
+
const { loadSyncConfig, getEnabledSources, expandPath } = await import('../../sync/config.js');
|
|
383
|
+
const { handleSync } = await import('../../mcp/handlers/sync.js');
|
|
384
|
+
const { matchesGlob } = await import('../../sync/discover.js');
|
|
385
|
+
const dataDir = options.dataDir;
|
|
386
|
+
const dbPath = path.join(dataDir, 'lore.lance');
|
|
387
|
+
const debounceMs = parseInt(options.interval, 10);
|
|
388
|
+
// Header
|
|
389
|
+
console.log('');
|
|
390
|
+
console.log(c.title(' ╔══════════════════════════════════════╗'));
|
|
391
|
+
console.log(c.title(' ║ 🔍 LORE WATCH ║'));
|
|
392
|
+
console.log(c.title(' ╚══════════════════════════════════════╝'));
|
|
393
|
+
console.log('');
|
|
394
|
+
console.log(` ${c.dim('Data:')} ${dataDir}`);
|
|
395
|
+
console.log(` ${c.dim('Debounce:')} ${debounceMs}ms`);
|
|
396
|
+
console.log('');
|
|
397
|
+
const config = await loadSyncConfig();
|
|
398
|
+
const sources = getEnabledSources(config);
|
|
399
|
+
const watchPaths = [];
|
|
400
|
+
if (sources.length === 0) {
|
|
401
|
+
console.log(c.warning(' ⚠ No local sync sources configured'));
|
|
402
|
+
console.log(c.dim(' Will still pull from remote and process new files'));
|
|
403
|
+
console.log(c.dim(' Run "lore sync sources add" to watch local directories'));
|
|
404
|
+
console.log('');
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
console.log(c.info(' 📁 Watching:'));
|
|
408
|
+
for (const source of sources) {
|
|
409
|
+
const expanded = expandPath(source.path);
|
|
410
|
+
console.log(` ${c.file(source.name)}`);
|
|
411
|
+
console.log(` ${c.path(expanded)}`);
|
|
412
|
+
console.log(` ${c.dim(`glob: ${source.glob} → project: ${source.project}`)}`);
|
|
413
|
+
console.log('');
|
|
414
|
+
watchPaths.push(expanded);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Run initial sync
|
|
418
|
+
if (options.initial !== false) {
|
|
419
|
+
console.log(c.info(' ⚡ Initial sync...'));
|
|
420
|
+
try {
|
|
421
|
+
const result = await handleSync(dbPath, dataDir, {
|
|
422
|
+
git_pull: true,
|
|
423
|
+
git_push: true,
|
|
424
|
+
}, { hookContext: { mode: 'cli' } });
|
|
425
|
+
const totalFiles = result.discovery?.total_files || 0;
|
|
426
|
+
const newFiles = result.discovery?.new_files || 0;
|
|
427
|
+
const processed = result.processing?.processed || 0;
|
|
428
|
+
if (processed > 0) {
|
|
429
|
+
console.log(` ${c.success('✓')} Processed ${c.file(String(processed))} new file(s)`);
|
|
430
|
+
for (const title of result.processing?.titles || []) {
|
|
431
|
+
console.log(` ${c.dim('•')} ${title}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
console.log(` ${c.success('✓')} ${totalFiles} files indexed, ${newFiles} new`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
console.log(` ${c.error('✗')} Initial sync failed: ${error}`);
|
|
440
|
+
}
|
|
441
|
+
console.log('');
|
|
442
|
+
}
|
|
443
|
+
// Divider
|
|
444
|
+
console.log(c.dim(' ─────────────────────────────────────────'));
|
|
445
|
+
console.log(` ${c.success('●')} Watching for changes... ${c.dim('(Ctrl+C to stop)')}`);
|
|
446
|
+
console.log(c.dim(' ─────────────────────────────────────────'));
|
|
447
|
+
console.log('');
|
|
448
|
+
let pendingChanges = new Map();
|
|
449
|
+
let syncTimeout = null;
|
|
450
|
+
let isSyncing = false;
|
|
451
|
+
function getTimestamp() {
|
|
452
|
+
return new Date().toLocaleTimeString('en-US', {
|
|
453
|
+
hour12: false,
|
|
454
|
+
hour: '2-digit',
|
|
455
|
+
minute: '2-digit',
|
|
456
|
+
second: '2-digit'
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function fileMatchesAnySource(filePath) {
|
|
460
|
+
for (const source of sources) {
|
|
461
|
+
const expanded = expandPath(source.path);
|
|
462
|
+
if (filePath.startsWith(expanded)) {
|
|
463
|
+
const relativePath = path.relative(expanded, filePath);
|
|
464
|
+
if (matchesGlob(relativePath, source.glob)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
async function runSync() {
|
|
472
|
+
if (isSyncing)
|
|
473
|
+
return;
|
|
474
|
+
isSyncing = true;
|
|
475
|
+
const changes = Array.from(pendingChanges.values());
|
|
476
|
+
pendingChanges.clear();
|
|
477
|
+
const ts = getTimestamp();
|
|
478
|
+
console.log(` ${c.time(ts)} ${c.badge('SYNC', colors.bgBlue)} Processing ${changes.length} file(s)...`);
|
|
479
|
+
for (const change of changes) {
|
|
480
|
+
const icon = change.type === 'add' ? '+' : '~';
|
|
481
|
+
const relativePath = change.path.replace(process.env.HOME || '', '~');
|
|
482
|
+
console.log(` ${c.dim(icon)} ${c.file(path.basename(change.path))}`);
|
|
483
|
+
console.log(` ${c.path(relativePath)}`);
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
const result = await handleSync(dbPath, dataDir, {
|
|
487
|
+
git_pull: false,
|
|
488
|
+
git_push: true,
|
|
489
|
+
}, { hookContext: { mode: 'cli' } });
|
|
490
|
+
const processed = result.processing?.processed || 0;
|
|
491
|
+
const errors = result.processing?.errors || 0;
|
|
492
|
+
if (processed > 0) {
|
|
493
|
+
console.log(` ${c.time(ts)} ${c.badge('DONE', colors.bgGreen)} Indexed ${processed} file(s):`);
|
|
494
|
+
for (const title of result.processing?.titles || []) {
|
|
495
|
+
console.log(` ${c.success('✓')} ${title}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else if (result.discovery && result.discovery.new_files === 0) {
|
|
499
|
+
console.log(` ${c.time(ts)} ${c.badge('SKIP', colors.bgYellow)} Already indexed`);
|
|
500
|
+
}
|
|
501
|
+
if (errors > 0) {
|
|
502
|
+
console.log(` ${c.time(ts)} ${c.error(`${errors} ERROR(S)`)}`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch (error) {
|
|
506
|
+
console.log(` ${c.time(ts)} ${c.error('SYNC FAILED')} ${error}`);
|
|
507
|
+
}
|
|
508
|
+
isSyncing = false;
|
|
509
|
+
console.log('');
|
|
510
|
+
}
|
|
511
|
+
function scheduleSync(filePath, type) {
|
|
512
|
+
pendingChanges.set(filePath, { type, path: filePath });
|
|
513
|
+
if (syncTimeout) {
|
|
514
|
+
clearTimeout(syncTimeout);
|
|
515
|
+
}
|
|
516
|
+
syncTimeout = setTimeout(runSync, debounceMs);
|
|
517
|
+
}
|
|
518
|
+
// Set up file watcher
|
|
519
|
+
let watcher = null;
|
|
520
|
+
if (watchPaths.length > 0) {
|
|
521
|
+
watcher = chokidar.watch(watchPaths, {
|
|
522
|
+
ignored: [
|
|
523
|
+
/(^|[\\/])\../,
|
|
524
|
+
/node_modules/,
|
|
525
|
+
/__pycache__/,
|
|
526
|
+
/\.lance$/,
|
|
527
|
+
/vectors\.lance/,
|
|
528
|
+
/\.db$/,
|
|
529
|
+
/\.sqlite$/,
|
|
530
|
+
],
|
|
531
|
+
persistent: true,
|
|
532
|
+
ignoreInitial: true,
|
|
533
|
+
awaitWriteFinish: {
|
|
534
|
+
stabilityThreshold: 500,
|
|
535
|
+
pollInterval: 100,
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
watcher
|
|
539
|
+
.on('add', (filePath) => {
|
|
540
|
+
if (!fileMatchesAnySource(filePath))
|
|
541
|
+
return;
|
|
542
|
+
const ts = getTimestamp();
|
|
543
|
+
console.log(` ${c.time(ts)} ${c.success('+')} ${c.file(path.basename(filePath))} ${c.dim('added')}`);
|
|
544
|
+
scheduleSync(filePath, 'add');
|
|
545
|
+
})
|
|
546
|
+
.on('change', (filePath) => {
|
|
547
|
+
if (!fileMatchesAnySource(filePath))
|
|
548
|
+
return;
|
|
549
|
+
const ts = getTimestamp();
|
|
550
|
+
console.log(` ${c.time(ts)} ${c.warning('~')} ${c.file(path.basename(filePath))} ${c.dim('modified')}`);
|
|
551
|
+
scheduleSync(filePath, 'change');
|
|
552
|
+
})
|
|
553
|
+
.on('error', (error) => {
|
|
554
|
+
console.log(` ${c.error('WATCHER ERROR')} ${error}`);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
// Periodic sync
|
|
558
|
+
const PULL_INTERVAL_MS = 5 * 60 * 1000;
|
|
559
|
+
async function periodicSync() {
|
|
560
|
+
if (isSyncing)
|
|
561
|
+
return;
|
|
562
|
+
const ts = getTimestamp();
|
|
563
|
+
console.log(` ${c.time(ts)} ${c.badge('PULL', colors.bgBlue)} Checking for remote changes...`);
|
|
564
|
+
try {
|
|
565
|
+
const result = await handleSync(dbPath, dataDir, {
|
|
566
|
+
git_pull: true,
|
|
567
|
+
git_push: false,
|
|
568
|
+
}, { hookContext: { mode: 'cli' } });
|
|
569
|
+
if (result.git_pulled) {
|
|
570
|
+
console.log(` ${c.time(ts)} ${c.success('✓')} Pulled latest changes`);
|
|
571
|
+
}
|
|
572
|
+
const newFiles = result.discovery?.new_files || 0;
|
|
573
|
+
if (newFiles > 0) {
|
|
574
|
+
console.log(` ${c.time(ts)} ${c.info('→')} Found ${newFiles} new file(s) from remote`);
|
|
575
|
+
const processResult = await handleSync(dbPath, dataDir, {
|
|
576
|
+
git_pull: false,
|
|
577
|
+
git_push: true,
|
|
578
|
+
}, { hookContext: { mode: 'cli' } });
|
|
579
|
+
if (processResult.processing && processResult.processing.processed > 0) {
|
|
580
|
+
console.log(` ${c.time(ts)} ${c.badge('DONE', colors.bgGreen)} Indexed ${processResult.processing.processed} file(s):`);
|
|
581
|
+
for (const title of processResult.processing.titles) {
|
|
582
|
+
console.log(` ${c.success('✓')} ${title}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
console.log(` ${c.time(ts)} ${c.dim('✓ Up to date')}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
console.log(` ${c.time(ts)} ${c.warning('⚠')} Pull failed: ${error}`);
|
|
592
|
+
}
|
|
593
|
+
console.log('');
|
|
594
|
+
}
|
|
595
|
+
console.log(` ${c.dim(`Remote sync every ${PULL_INTERVAL_MS / 60000} minutes`)}`);
|
|
596
|
+
console.log('');
|
|
597
|
+
await periodicSync();
|
|
598
|
+
const pullInterval = setInterval(periodicSync, PULL_INTERVAL_MS);
|
|
599
|
+
// Handle shutdown
|
|
600
|
+
process.on('SIGINT', async () => {
|
|
601
|
+
console.log('\n\nShutting down...');
|
|
602
|
+
clearInterval(pullInterval);
|
|
603
|
+
if (watcher) {
|
|
604
|
+
await watcher.close();
|
|
605
|
+
}
|
|
606
|
+
console.log('Goodbye!');
|
|
607
|
+
process.exit(0);
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
// Source management (flat, not nested under "sources")
|
|
611
|
+
syncCmd
|
|
612
|
+
.command('list')
|
|
613
|
+
.description('List configured sync sources')
|
|
614
|
+
.action(async () => {
|
|
615
|
+
const { loadSyncConfig, getConfigPath } = await import('../../sync/config.js');
|
|
616
|
+
console.log(`\nSync Sources`);
|
|
617
|
+
console.log(`============`);
|
|
618
|
+
console.log(`Config: ${getConfigPath()}\n`);
|
|
619
|
+
const config = await loadSyncConfig();
|
|
620
|
+
if (config.sources.length === 0) {
|
|
621
|
+
console.log('No sources configured. Run "lore sync add" to add one.');
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
for (const source of config.sources) {
|
|
625
|
+
const status = source.enabled ? '✓' : '○';
|
|
626
|
+
console.log(`${status} ${source.name}`);
|
|
627
|
+
console.log(` Path: ${source.path}`);
|
|
628
|
+
console.log(` Glob: ${source.glob}`);
|
|
629
|
+
console.log(` Project: ${source.project}`);
|
|
630
|
+
console.log('');
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
syncCmd
|
|
634
|
+
.command('add')
|
|
635
|
+
.description('Add a new sync source directory')
|
|
636
|
+
.option('-n, --name <name>', 'Source name')
|
|
637
|
+
.option('-p, --path <path>', 'Directory path')
|
|
638
|
+
.option('-g, --glob <glob>', 'File glob pattern', '**/*.md')
|
|
639
|
+
.option('--project <project>', 'Default project')
|
|
640
|
+
.action(async (options) => {
|
|
641
|
+
const { addSyncSource } = await import('../../sync/config.js');
|
|
642
|
+
const readline = await import('readline');
|
|
643
|
+
const rl = readline.createInterface({
|
|
644
|
+
input: process.stdin,
|
|
645
|
+
output: process.stdout,
|
|
646
|
+
});
|
|
647
|
+
const ask = (question, defaultValue) => new Promise((resolve) => {
|
|
648
|
+
const prompt = defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `;
|
|
649
|
+
rl.question(prompt, (answer) => {
|
|
650
|
+
resolve(answer.trim() || defaultValue || '');
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
console.log(`\nAdd Sync Source`);
|
|
654
|
+
console.log(`===============\n`);
|
|
655
|
+
const name = options.name || await ask('Name (e.g., "Granola Meetings")');
|
|
656
|
+
const sourcePath = options.path || await ask('Path (e.g., ~/granola-extractor/output)');
|
|
657
|
+
const glob = options.glob || await ask('Glob pattern', '**/*.md');
|
|
658
|
+
const project = options.project || await ask('Default project');
|
|
659
|
+
rl.close();
|
|
660
|
+
if (!name || !sourcePath || !project) {
|
|
661
|
+
console.log('\nAll fields are required.');
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
await addSyncSource({
|
|
666
|
+
name,
|
|
667
|
+
path: sourcePath,
|
|
668
|
+
glob,
|
|
669
|
+
project,
|
|
670
|
+
enabled: true,
|
|
671
|
+
});
|
|
672
|
+
console.log(`\n✓ Added source "${name}"`);
|
|
673
|
+
console.log(`\nRun "lore sync" to process files from this source.`);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
console.error(`\nError: ${error}`);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
syncCmd
|
|
681
|
+
.command('enable <name>')
|
|
682
|
+
.description('Enable a sync source')
|
|
683
|
+
.action(async (name) => {
|
|
684
|
+
const { updateSyncSource } = await import('../../sync/config.js');
|
|
685
|
+
try {
|
|
686
|
+
await updateSyncSource(name, { enabled: true });
|
|
687
|
+
console.log(`✓ Enabled "${name}"`);
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
console.error(`Error: ${error}`);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
syncCmd
|
|
695
|
+
.command('disable <name>')
|
|
696
|
+
.description('Disable a sync source')
|
|
697
|
+
.action(async (name) => {
|
|
698
|
+
const { updateSyncSource } = await import('../../sync/config.js');
|
|
699
|
+
try {
|
|
700
|
+
await updateSyncSource(name, { enabled: false });
|
|
701
|
+
console.log(`✓ Disabled "${name}"`);
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
console.error(`Error: ${error}`);
|
|
705
|
+
process.exit(1);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
syncCmd
|
|
709
|
+
.command('remove <name>')
|
|
710
|
+
.description('Remove a sync source')
|
|
711
|
+
.action(async (name) => {
|
|
712
|
+
const { removeSyncSource } = await import('../../sync/config.js');
|
|
713
|
+
try {
|
|
714
|
+
await removeSyncSource(name);
|
|
715
|
+
console.log(`✓ Removed "${name}"`);
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
console.error(`Error: ${error}`);
|
|
719
|
+
process.exit(1);
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
// ============================================================================
|
|
724
|
+
// Exported helpers
|
|
725
|
+
// ============================================================================
|
|
726
|
+
/**
|
|
727
|
+
* Start the background sync daemon process.
|
|
728
|
+
* Returns { pid } on success, null on failure.
|
|
729
|
+
* If already running, returns the existing PID.
|
|
730
|
+
*/
|
|
731
|
+
export async function startDaemonProcess(dataDir) {
|
|
732
|
+
await ensureConfigDir();
|
|
733
|
+
const existingPid = getPid();
|
|
734
|
+
if (existingPid) {
|
|
735
|
+
return { pid: existingPid, alreadyRunning: true };
|
|
736
|
+
}
|
|
737
|
+
// macOS: use launchd for persistence across reboots
|
|
738
|
+
if (isMacOS()) {
|
|
739
|
+
const result = installLaunchdAgent(dataDir);
|
|
740
|
+
if (result) {
|
|
741
|
+
return { pid: result.pid, alreadyRunning: false };
|
|
742
|
+
}
|
|
743
|
+
return null;
|
|
744
|
+
}
|
|
745
|
+
// Non-macOS: use nohup fallback
|
|
746
|
+
const scriptPath = path.join(__dirname, '..', '..', 'daemon-runner.js');
|
|
747
|
+
const nodePath = process.execPath;
|
|
748
|
+
const tmpScript = path.join(os.tmpdir(), `lore-daemon-start-${Date.now()}.sh`);
|
|
749
|
+
const scriptContent = `#!/bin/bash\nnohup "${nodePath}" "${scriptPath}" "${dataDir}" > /dev/null 2>&1 &\n`;
|
|
750
|
+
writeFileSync(tmpScript, scriptContent, { mode: 0o755 });
|
|
751
|
+
spawnSync('/bin/bash', [tmpScript], { stdio: 'ignore' });
|
|
752
|
+
try {
|
|
753
|
+
unlinkSync(tmpScript);
|
|
754
|
+
}
|
|
755
|
+
catch { }
|
|
756
|
+
// Wait for daemon to start and write PID file
|
|
757
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
758
|
+
try {
|
|
759
|
+
const daemonPid = parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10);
|
|
760
|
+
process.kill(daemonPid, 0); // Verify running
|
|
761
|
+
return { pid: daemonPid, alreadyRunning: false };
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// Export for daemon-runner and browse
|
|
768
|
+
export { CONFIG_DIR, PID_FILE, STATUS_FILE, LOG_FILE };
|