@getlore/cli 0.2.0 → 0.4.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/dist/cli/commands/auth.js +120 -63
- package/dist/cli/commands/sync.d.ts +12 -0
- package/dist/cli/commands/sync.js +35 -19
- package/dist/cli/commands/update.d.ts +8 -0
- package/dist/cli/commands/update.js +71 -0
- package/dist/cli/update-notifier.d.ts +12 -0
- package/dist/cli/update-notifier.js +95 -0
- package/dist/index.js +10 -2
- package/package.json +1 -1
|
@@ -37,6 +37,8 @@ export function registerAuthCommands(program) {
|
|
|
37
37
|
.command('login')
|
|
38
38
|
.description('Sign in with email (OTP)')
|
|
39
39
|
.option('-e, --email <email>', 'Email address')
|
|
40
|
+
.option('--code <code>', 'OTP code (skip interactive prompt — use after --send-only)')
|
|
41
|
+
.option('--send-only', 'Send OTP and exit without waiting for code')
|
|
40
42
|
.action(async (options) => {
|
|
41
43
|
const { sendOTP, verifyOTP, sessionFromMagicLink, waitForMagicLinkCallback, isAuthenticated } = await import('../../core/auth.js');
|
|
42
44
|
// Check if already logged in
|
|
@@ -52,25 +54,37 @@ export function registerAuthCommands(program) {
|
|
|
52
54
|
console.error(c.error('Email is required'));
|
|
53
55
|
process.exit(1);
|
|
54
56
|
}
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
// Non-interactive: --code provided → verify directly (OTP must have been sent already)
|
|
58
|
+
if (options.code) {
|
|
59
|
+
const { session, error } = await verifyOTP(email, options.code);
|
|
60
|
+
if (error || !session) {
|
|
61
|
+
console.error(c.error(`Verification failed: ${error || 'Unknown error'}`));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
console.log(c.success(`Logged in as ${session.user.email}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Send OTP
|
|
62
68
|
console.log(c.dim(`Sending code to ${email}...`));
|
|
63
69
|
const { error: sendError } = await sendOTP(email);
|
|
64
70
|
if (sendError) {
|
|
65
|
-
callback.abort();
|
|
66
71
|
console.error(c.error(`Failed to send code: ${sendError}`));
|
|
67
72
|
process.exit(1);
|
|
68
73
|
}
|
|
69
|
-
console.log(c.success(
|
|
74
|
+
console.log(c.success(`OTP sent to ${email}`));
|
|
75
|
+
// --send-only: exit after sending
|
|
76
|
+
if (options.sendOnly) {
|
|
77
|
+
console.log(c.dim('Re-run with --code <code> to complete login.'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
70
80
|
console.log(c.dim('Click the magic link in the email — it will sign you in automatically.'));
|
|
71
81
|
console.log(c.dim('Or paste a 6-digit code if your email shows one.'));
|
|
72
82
|
console.log('');
|
|
73
83
|
console.log(c.dim('Waiting for magic link click...'));
|
|
84
|
+
// Start the localhost callback server
|
|
85
|
+
const callback = waitForMagicLinkCallback({
|
|
86
|
+
onListening: () => { },
|
|
87
|
+
});
|
|
74
88
|
// Race: callback server catches the magic link, or user pastes a code/URL
|
|
75
89
|
const readline = await import('readline');
|
|
76
90
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -158,19 +172,27 @@ export function registerAuthCommands(program) {
|
|
|
158
172
|
program
|
|
159
173
|
.command('setup')
|
|
160
174
|
.description('Guided setup wizard (config, login, init)')
|
|
161
|
-
.
|
|
175
|
+
.option('--openai-key <key>', 'OpenAI API key (skip prompt)')
|
|
176
|
+
.option('--anthropic-key <key>', 'Anthropic API key (skip prompt)')
|
|
177
|
+
.option('-e, --email <email>', 'Email address (skip prompt)')
|
|
178
|
+
.option('--data-dir <dir>', 'Data directory path (skip prompt)')
|
|
179
|
+
.option('--code <code>', 'OTP code (for non-interactive login)')
|
|
180
|
+
.option('--skip-login', 'Skip the login step (use if already authenticated)')
|
|
181
|
+
.action(async (options) => {
|
|
162
182
|
const { saveLoreConfig, getLoreConfigPath } = await import('../../core/config.js');
|
|
163
183
|
const { sendOTP, verifyOTP, sessionFromMagicLink, isAuthenticated, loadAuthSession } = await import('../../core/auth.js');
|
|
164
184
|
const { bridgeConfigToEnv } = await import('../../core/config.js');
|
|
185
|
+
// Non-interactive mode: skip prompts when all key flags are provided
|
|
186
|
+
const nonInteractive = !!(options.openaiKey && options.anthropicKey && options.email);
|
|
165
187
|
console.log(`\n${c.title('Lore Setup Wizard')}`);
|
|
166
188
|
console.log(`${c.dim('=')}`.repeat(40) + '\n');
|
|
167
189
|
// ── Step 1: Configuration ───────────────────────────────────────
|
|
168
190
|
console.log(c.bold('Step 1: Configuration\n'));
|
|
169
191
|
const { expandPath } = await import('../../sync/config.js');
|
|
170
192
|
const defaultDataDir = process.env.LORE_DATA_DIR || '~/.lore';
|
|
171
|
-
const dataDir = await prompt('Data directory', defaultDataDir);
|
|
172
|
-
const openaiApiKey = await prompt('OpenAI API Key (for embeddings)');
|
|
173
|
-
const anthropicApiKey = await prompt('Anthropic API Key (for sync & research)');
|
|
193
|
+
const dataDir = options.dataDir || await prompt('Data directory', defaultDataDir);
|
|
194
|
+
const openaiApiKey = options.openaiKey || process.env.OPENAI_API_KEY || await prompt('OpenAI API Key (for embeddings)');
|
|
195
|
+
const anthropicApiKey = options.anthropicKey || process.env.ANTHROPIC_API_KEY || await prompt('Anthropic API Key (for sync & research)');
|
|
174
196
|
await saveLoreConfig({
|
|
175
197
|
...(dataDir ? { data_dir: expandPath(dataDir) } : {}),
|
|
176
198
|
...(openaiApiKey ? { openai_api_key: openaiApiKey } : {}),
|
|
@@ -181,46 +203,75 @@ export function registerAuthCommands(program) {
|
|
|
181
203
|
await bridgeConfigToEnv();
|
|
182
204
|
// ── Step 2: Login ──────────────────────────────────────────────────
|
|
183
205
|
console.log(c.bold('Step 2: Login\n'));
|
|
184
|
-
if (
|
|
206
|
+
if (options.skipLogin) {
|
|
207
|
+
console.log(c.dim('Skipped (--skip-login)\n'));
|
|
208
|
+
}
|
|
209
|
+
else if (await isAuthenticated()) {
|
|
185
210
|
const session = await loadAuthSession();
|
|
186
211
|
console.log(c.success(`Already logged in as ${session?.user.email}\n`));
|
|
187
212
|
}
|
|
188
213
|
else {
|
|
189
|
-
const email = await prompt('Email');
|
|
214
|
+
const email = options.email || await prompt('Email');
|
|
190
215
|
if (!email) {
|
|
191
216
|
console.error(c.error('Email is required'));
|
|
192
217
|
process.exit(1);
|
|
193
218
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const response = await prompt('Paste the code or the full magic link URL');
|
|
203
|
-
if (!response) {
|
|
204
|
-
console.error(c.error('Code or magic link is required'));
|
|
205
|
-
process.exit(1);
|
|
219
|
+
if (options.code) {
|
|
220
|
+
// Non-interactive: --code provided → verify directly (OTP must have been sent already)
|
|
221
|
+
const result = await verifyOTP(email, options.code);
|
|
222
|
+
if (result.error || !result.session) {
|
|
223
|
+
console.error(c.error(`Verification failed: ${result.error || 'Unknown error'}`));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
console.log(c.success(`Logged in as ${result.session.user.email}\n`));
|
|
206
227
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
228
|
+
else if (nonInteractive) {
|
|
229
|
+
// Non-interactive without --code: send OTP and exit so agent can retrieve code
|
|
230
|
+
console.log(c.dim(`Sending code to ${email}...`));
|
|
231
|
+
const { error: sendError } = await sendOTP(email);
|
|
232
|
+
if (sendError) {
|
|
233
|
+
console.error(c.error(`Failed to send code: ${sendError}`));
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
console.log(c.success(`OTP sent to ${email}`));
|
|
237
|
+
console.log(c.dim('Re-run with --code <code> to complete setup.\n'));
|
|
238
|
+
console.log(c.dim('Example:'));
|
|
239
|
+
console.log(c.dim(` lore setup --openai-key ... --anthropic-key ... --email ${email} --code <code>\n`));
|
|
240
|
+
return; // Exit — config is saved, agent re-runs with --code
|
|
213
241
|
}
|
|
214
242
|
else {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
// Interactive: send OTP and prompt
|
|
244
|
+
console.log(c.dim(`Sending code to ${email}...`));
|
|
245
|
+
const { error: sendError } = await sendOTP(email);
|
|
246
|
+
if (sendError) {
|
|
247
|
+
console.error(c.error(`Failed to send code: ${sendError}`));
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
console.log(c.success('Check your email!'));
|
|
251
|
+
console.log(c.dim('You may receive a 6-digit code or a magic link URL.'));
|
|
252
|
+
const response = await prompt('Paste the code or the full magic link URL');
|
|
253
|
+
if (!response) {
|
|
254
|
+
console.error(c.error('Code or magic link is required'));
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
let session;
|
|
258
|
+
let verifyError;
|
|
259
|
+
if (response.startsWith('http')) {
|
|
260
|
+
const result = await sessionFromMagicLink(response);
|
|
261
|
+
session = result.session;
|
|
262
|
+
verifyError = result.error;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
const result = await verifyOTP(email, response);
|
|
266
|
+
session = result.session;
|
|
267
|
+
verifyError = result.error;
|
|
268
|
+
}
|
|
269
|
+
if (verifyError || !session) {
|
|
270
|
+
console.error(c.error(`Verification failed: ${verifyError || 'Unknown error'}`));
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
console.log(c.success(`Logged in as ${session.user.email}\n`));
|
|
222
274
|
}
|
|
223
|
-
console.log(c.success(`Logged in as ${session.user.email}\n`));
|
|
224
275
|
}
|
|
225
276
|
// ── Step 3: Data Repository ────────────────────────────────────────
|
|
226
277
|
console.log(c.bold('Step 3: Data Repository\n'));
|
|
@@ -262,7 +313,7 @@ export function registerAuthCommands(program) {
|
|
|
262
313
|
if (savedUrl) {
|
|
263
314
|
// Machine B: clone existing repo
|
|
264
315
|
console.log(c.success(`Found your data repo URL: ${savedUrl}`));
|
|
265
|
-
const cloneIt = await prompt('Clone it to ' + resolvedDataDir + '? (y/n)', 'y');
|
|
316
|
+
const cloneIt = nonInteractive ? 'y' : await prompt('Clone it to ' + resolvedDataDir + '? (y/n)', 'y');
|
|
266
317
|
if (cloneIt.toLowerCase() === 'y') {
|
|
267
318
|
try {
|
|
268
319
|
const { execSync } = await import('child_process');
|
|
@@ -284,9 +335,9 @@ export function registerAuthCommands(program) {
|
|
|
284
335
|
console.log(c.success('Created data repository.\n'));
|
|
285
336
|
// Try to set up GitHub remote
|
|
286
337
|
if (await isGhAvailable()) {
|
|
287
|
-
const createRepo = await prompt('Create a private GitHub repo for cross-machine sync? (y/n)', 'y');
|
|
338
|
+
const createRepo = nonInteractive ? 'y' : await prompt('Create a private GitHub repo for cross-machine sync? (y/n)', 'y');
|
|
288
339
|
if (createRepo.toLowerCase() === 'y') {
|
|
289
|
-
const repoName = await prompt('Repository name', 'lore-data');
|
|
340
|
+
const repoName = nonInteractive ? 'lore-data' : await prompt('Repository name', 'lore-data');
|
|
290
341
|
const url = await createGithubRepo(resolvedDataDir, repoName);
|
|
291
342
|
if (url) {
|
|
292
343
|
console.log(c.success(`Created and pushed to ${url}\n`));
|
|
@@ -303,7 +354,7 @@ export function registerAuthCommands(program) {
|
|
|
303
354
|
}
|
|
304
355
|
}
|
|
305
356
|
}
|
|
306
|
-
else {
|
|
357
|
+
else if (!nonInteractive) {
|
|
307
358
|
const remoteUrl = await prompt('Git remote URL for cross-machine sync (or press Enter to skip)');
|
|
308
359
|
if (remoteUrl) {
|
|
309
360
|
try {
|
|
@@ -344,7 +395,7 @@ export function registerAuthCommands(program) {
|
|
|
344
395
|
if (savedUrl) {
|
|
345
396
|
// Found existing repo — add as remote and pull
|
|
346
397
|
console.log(c.success(`Found your data repo: ${savedUrl}`));
|
|
347
|
-
const useIt = await prompt('Add as remote and pull? (y/n)', 'y');
|
|
398
|
+
const useIt = nonInteractive ? 'y' : await prompt('Add as remote and pull? (y/n)', 'y');
|
|
348
399
|
if (useIt.toLowerCase() === 'y') {
|
|
349
400
|
try {
|
|
350
401
|
const { execSync } = await import('child_process');
|
|
@@ -359,9 +410,9 @@ export function registerAuthCommands(program) {
|
|
|
359
410
|
}
|
|
360
411
|
}
|
|
361
412
|
else if (await isGhAvailable()) {
|
|
362
|
-
const createRepo = await prompt('Create a private GitHub repo for cross-machine sync? (y/n)', 'y');
|
|
413
|
+
const createRepo = nonInteractive ? 'y' : await prompt('Create a private GitHub repo for cross-machine sync? (y/n)', 'y');
|
|
363
414
|
if (createRepo.toLowerCase() === 'y') {
|
|
364
|
-
const repoName = await prompt('Repository name', 'lore-data');
|
|
415
|
+
const repoName = nonInteractive ? 'lore-data' : await prompt('Repository name', 'lore-data');
|
|
365
416
|
const url = await createGithubRepo(resolvedDataDir, repoName);
|
|
366
417
|
if (url) {
|
|
367
418
|
console.log(c.success(`Created and pushed to ${url}\n`));
|
|
@@ -377,7 +428,7 @@ export function registerAuthCommands(program) {
|
|
|
377
428
|
}
|
|
378
429
|
}
|
|
379
430
|
}
|
|
380
|
-
else {
|
|
431
|
+
else if (!nonInteractive) {
|
|
381
432
|
const remoteUrl = await prompt('Git remote URL for cross-machine sync (or press Enter to skip)');
|
|
382
433
|
if (remoteUrl) {
|
|
383
434
|
try {
|
|
@@ -430,7 +481,7 @@ export function registerAuthCommands(program) {
|
|
|
430
481
|
}
|
|
431
482
|
// ── Step 5: Background Daemon ──────────────────────────────────────
|
|
432
483
|
console.log(c.bold('Step 5: Background Daemon\n'));
|
|
433
|
-
const startDaemon = await prompt('Start background sync daemon? (y/n)', 'y');
|
|
484
|
+
const startDaemon = nonInteractive ? 'y' : await prompt('Start background sync daemon? (y/n)', 'y');
|
|
434
485
|
if (startDaemon.toLowerCase() === 'y') {
|
|
435
486
|
try {
|
|
436
487
|
const { startDaemonProcess } = await import('./sync.js');
|
|
@@ -455,22 +506,28 @@ export function registerAuthCommands(program) {
|
|
|
455
506
|
}
|
|
456
507
|
// ── Step 6: Agent Skills ──────────────────────────────────────────
|
|
457
508
|
console.log(c.bold('Step 6: Agent Skills\n'));
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
509
|
+
if (nonInteractive) {
|
|
510
|
+
console.log(c.dim('Skipped in non-interactive mode.'));
|
|
511
|
+
console.log(c.dim('Install later with: lore skills install <name>\n'));
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
console.log(c.dim('Lore works best when your AI agents know how to use it.'));
|
|
515
|
+
console.log(c.dim('Install instruction files so agents automatically search and ingest into Lore.\n'));
|
|
516
|
+
try {
|
|
517
|
+
const { interactiveSkillInstall } = await import('./skills.js');
|
|
518
|
+
const installed = await interactiveSkillInstall();
|
|
519
|
+
if (installed.length > 0) {
|
|
520
|
+
console.log(c.success(`\nInstalled skills: ${installed.join(', ')}\n`));
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
console.log(c.dim('\nSkipped. You can install later with: lore skills install <name>\n'));
|
|
524
|
+
}
|
|
465
525
|
}
|
|
466
|
-
|
|
467
|
-
console.log(c.
|
|
526
|
+
catch (err) {
|
|
527
|
+
console.log(c.warning(`Could not install skills: ${err instanceof Error ? err.message : err}`));
|
|
528
|
+
console.log(c.dim('You can install later with: lore skills install <name>\n'));
|
|
468
529
|
}
|
|
469
530
|
}
|
|
470
|
-
catch (err) {
|
|
471
|
-
console.log(c.warning(`Could not install skills: ${err instanceof Error ? err.message : err}`));
|
|
472
|
-
console.log(c.dim('You can install later with: lore skills install <name>\n'));
|
|
473
|
-
}
|
|
474
531
|
// ── Done ───────────────────────────────────────────────────────────
|
|
475
532
|
console.log(c.title('Setup complete!\n'));
|
|
476
533
|
console.log('Try these commands:');
|
|
@@ -19,6 +19,14 @@ export interface DaemonStatus {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
export declare function registerSyncCommand(program: Command, defaultDataDir: string): void;
|
|
22
|
+
/**
|
|
23
|
+
* Restart the background sync daemon.
|
|
24
|
+
* Stops the existing daemon (if running), then starts a fresh one.
|
|
25
|
+
* Returns { pid } on success, null on failure.
|
|
26
|
+
*/
|
|
27
|
+
export declare function restartDaemon(dataDir: string): Promise<{
|
|
28
|
+
pid: number;
|
|
29
|
+
} | null>;
|
|
22
30
|
/**
|
|
23
31
|
* Start the background sync daemon process.
|
|
24
32
|
* Returns { pid } on success, null on failure.
|
|
@@ -28,4 +36,8 @@ export declare function startDaemonProcess(dataDir: string): Promise<{
|
|
|
28
36
|
pid: number;
|
|
29
37
|
alreadyRunning: boolean;
|
|
30
38
|
} | null>;
|
|
39
|
+
/**
|
|
40
|
+
* Check if the sync daemon is currently running.
|
|
41
|
+
*/
|
|
42
|
+
export declare function isDaemonRunning(): boolean;
|
|
31
43
|
export { CONFIG_DIR, PID_FILE, STATUS_FILE, LOG_FILE };
|
|
@@ -270,25 +270,7 @@ export function registerSyncCommand(program, defaultDataDir) {
|
|
|
270
270
|
.description('Restart background sync daemon')
|
|
271
271
|
.option('-d, --data-dir <dir>', 'Data directory', defaultDataDir)
|
|
272
272
|
.action(async (options) => {
|
|
273
|
-
|
|
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);
|
|
273
|
+
const result = await restartDaemon(options.dataDir);
|
|
292
274
|
if (!result) {
|
|
293
275
|
console.error('Failed to restart daemon - check logs with: lore sync logs');
|
|
294
276
|
return;
|
|
@@ -723,6 +705,34 @@ export function registerSyncCommand(program, defaultDataDir) {
|
|
|
723
705
|
// ============================================================================
|
|
724
706
|
// Exported helpers
|
|
725
707
|
// ============================================================================
|
|
708
|
+
/**
|
|
709
|
+
* Restart the background sync daemon.
|
|
710
|
+
* Stops the existing daemon (if running), then starts a fresh one.
|
|
711
|
+
* Returns { pid } on success, null on failure.
|
|
712
|
+
*/
|
|
713
|
+
export async function restartDaemon(dataDir) {
|
|
714
|
+
// Uninstall launchd agent so it doesn't auto-restart during our restart
|
|
715
|
+
if (isLaunchdInstalled()) {
|
|
716
|
+
uninstallLaunchdAgent();
|
|
717
|
+
}
|
|
718
|
+
const pid = getPid();
|
|
719
|
+
if (pid) {
|
|
720
|
+
try {
|
|
721
|
+
process.kill(pid, 'SIGTERM');
|
|
722
|
+
if (existsSync(PID_FILE))
|
|
723
|
+
unlinkSync(PID_FILE);
|
|
724
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
// Process might already be dead
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
// startDaemonProcess will reinstall launchd with fresh config on macOS
|
|
731
|
+
const result = await startDaemonProcess(dataDir);
|
|
732
|
+
if (!result)
|
|
733
|
+
return null;
|
|
734
|
+
return { pid: result.pid };
|
|
735
|
+
}
|
|
726
736
|
/**
|
|
727
737
|
* Start the background sync daemon process.
|
|
728
738
|
* Returns { pid } on success, null on failure.
|
|
@@ -764,5 +774,11 @@ export async function startDaemonProcess(dataDir) {
|
|
|
764
774
|
return null;
|
|
765
775
|
}
|
|
766
776
|
}
|
|
777
|
+
/**
|
|
778
|
+
* Check if the sync daemon is currently running.
|
|
779
|
+
*/
|
|
780
|
+
export function isDaemonRunning() {
|
|
781
|
+
return getPid() !== null;
|
|
782
|
+
}
|
|
767
783
|
// Export for daemon-runner and browse
|
|
768
784
|
export { CONFIG_DIR, PID_FILE, STATUS_FILE, LOG_FILE };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command
|
|
3
|
+
*
|
|
4
|
+
* Check for and install updates to @getlore/cli.
|
|
5
|
+
* Restarts the background daemon after upgrading so it picks up new code.
|
|
6
|
+
*/
|
|
7
|
+
import type { Command } from 'commander';
|
|
8
|
+
export declare function registerUpdateCommand(program: Command, defaultDataDir: string): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update Command
|
|
3
|
+
*
|
|
4
|
+
* Check for and install updates to @getlore/cli.
|
|
5
|
+
* Restarts the background daemon after upgrading so it picks up new code.
|
|
6
|
+
*/
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
8
|
+
import { c } from '../colors.js';
|
|
9
|
+
import { getLoreVersionString } from '../../extensions/registry.js';
|
|
10
|
+
import { restartDaemon, isDaemonRunning } from './sync.js';
|
|
11
|
+
const NPM_PACKAGE = '@getlore/cli';
|
|
12
|
+
function getLatestVersion() {
|
|
13
|
+
const result = spawnSync('npm', ['view', NPM_PACKAGE, 'version'], {
|
|
14
|
+
encoding: 'utf-8',
|
|
15
|
+
timeout: 10_000,
|
|
16
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
17
|
+
});
|
|
18
|
+
if (result.status !== 0 || !result.stdout)
|
|
19
|
+
return null;
|
|
20
|
+
return result.stdout.trim();
|
|
21
|
+
}
|
|
22
|
+
export function registerUpdateCommand(program, defaultDataDir) {
|
|
23
|
+
program
|
|
24
|
+
.command('update')
|
|
25
|
+
.description('Check for and install updates')
|
|
26
|
+
.option('--check', 'Check for updates without installing')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
const currentVersion = (await getLoreVersionString()) || 'unknown';
|
|
29
|
+
console.log(`\nCurrent version: ${c.bold(currentVersion)}`);
|
|
30
|
+
console.log('Checking npm for latest version...');
|
|
31
|
+
const latestVersion = getLatestVersion();
|
|
32
|
+
if (!latestVersion) {
|
|
33
|
+
console.error('Could not check npm registry. Are you online?');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
console.log(`Latest version: ${c.bold(latestVersion)}`);
|
|
37
|
+
if (currentVersion === latestVersion) {
|
|
38
|
+
console.log(c.success('\nAlready up to date!'));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(`\nUpdate available: ${c.dim(currentVersion)} → ${c.success(latestVersion)}`);
|
|
42
|
+
if (options.check) {
|
|
43
|
+
console.log(`\nRun ${c.bold('lore update')} to install.`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Install
|
|
47
|
+
console.log(`\nInstalling ${NPM_PACKAGE}@${latestVersion}...`);
|
|
48
|
+
const installResult = spawnSync('npm', ['install', '-g', `${NPM_PACKAGE}@latest`], {
|
|
49
|
+
stdio: 'inherit',
|
|
50
|
+
timeout: 120_000,
|
|
51
|
+
});
|
|
52
|
+
if (installResult.status !== 0) {
|
|
53
|
+
console.error('\nInstallation failed. You may need to run with sudo:');
|
|
54
|
+
console.error(` sudo npm install -g ${NPM_PACKAGE}@latest`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
console.log(c.success(`\nUpdated to ${latestVersion}!`));
|
|
58
|
+
// Restart daemon if running
|
|
59
|
+
if (isDaemonRunning()) {
|
|
60
|
+
console.log('\nRestarting background daemon...');
|
|
61
|
+
const result = await restartDaemon(defaultDataDir);
|
|
62
|
+
if (result) {
|
|
63
|
+
console.log(c.success(`Daemon restarted (PID: ${result.pid})`));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
console.log(c.warning('Could not restart daemon. Run: lore sync restart'));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log('');
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passive Update Notifier
|
|
3
|
+
*
|
|
4
|
+
* Checks npm for a newer version of @getlore/cli once every 24 hours.
|
|
5
|
+
* Prints a subtle notification after command output if an update is available.
|
|
6
|
+
* Never blocks or throws — all errors are silently swallowed.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Check for updates and print a notification if one is available.
|
|
10
|
+
* Safe to call fire-and-forget — never throws, never blocks meaningfully.
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkForUpdates(): Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passive Update Notifier
|
|
3
|
+
*
|
|
4
|
+
* Checks npm for a newer version of @getlore/cli once every 24 hours.
|
|
5
|
+
* Prints a subtle notification after command output if an update is available.
|
|
6
|
+
* Never blocks or throws — all errors are silently swallowed.
|
|
7
|
+
*/
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
|
+
import { spawnSync } from 'child_process';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
import { colors } from './colors.js';
|
|
13
|
+
import { getLoreVersionString } from '../extensions/registry.js';
|
|
14
|
+
const NPM_PACKAGE = '@getlore/cli';
|
|
15
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'lore');
|
|
16
|
+
const CACHE_FILE = path.join(CONFIG_DIR, 'update-check.json');
|
|
17
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
18
|
+
function readCache() {
|
|
19
|
+
try {
|
|
20
|
+
if (!existsSync(CACHE_FILE))
|
|
21
|
+
return null;
|
|
22
|
+
return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function writeCache(cache) {
|
|
29
|
+
try {
|
|
30
|
+
if (!existsSync(CONFIG_DIR))
|
|
31
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
32
|
+
writeFileSync(CACHE_FILE, JSON.stringify(cache, null, 2));
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Silently ignore
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check for updates and print a notification if one is available.
|
|
40
|
+
* Safe to call fire-and-forget — never throws, never blocks meaningfully.
|
|
41
|
+
*/
|
|
42
|
+
export async function checkForUpdates() {
|
|
43
|
+
// Don't notify in non-interactive contexts
|
|
44
|
+
if (!process.stdout.isTTY)
|
|
45
|
+
return;
|
|
46
|
+
const cache = readCache();
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
let latestVersion;
|
|
49
|
+
if (cache && (now - cache.last_check) < CHECK_INTERVAL_MS) {
|
|
50
|
+
// Use cached value
|
|
51
|
+
latestVersion = cache.latest_version;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Fetch from npm with short timeout
|
|
55
|
+
const result = spawnSync('npm', ['view', NPM_PACKAGE, 'version'], {
|
|
56
|
+
encoding: 'utf-8',
|
|
57
|
+
timeout: 5_000,
|
|
58
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
59
|
+
});
|
|
60
|
+
if (result.status !== 0 || !result.stdout)
|
|
61
|
+
return;
|
|
62
|
+
latestVersion = result.stdout.trim();
|
|
63
|
+
writeCache({
|
|
64
|
+
last_check: now,
|
|
65
|
+
latest_version: latestVersion,
|
|
66
|
+
last_notified_version: cache?.last_notified_version,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const currentVersion = await getLoreVersionString();
|
|
70
|
+
if (!currentVersion || currentVersion === latestVersion)
|
|
71
|
+
return;
|
|
72
|
+
// Don't re-notify for the same version
|
|
73
|
+
if (cache?.last_notified_version === latestVersion)
|
|
74
|
+
return;
|
|
75
|
+
// Print notification
|
|
76
|
+
const border = `${colors.dim}╭────────────────────────────────────────╮${colors.reset}`;
|
|
77
|
+
const bottom = `${colors.dim}╰────────────────────────────────────────╯${colors.reset}`;
|
|
78
|
+
const pad = (s, width) => {
|
|
79
|
+
// Strip ANSI for length calculation
|
|
80
|
+
const stripped = s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
81
|
+
const padding = Math.max(0, width - stripped.length);
|
|
82
|
+
return s + ' '.repeat(padding);
|
|
83
|
+
};
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(border);
|
|
86
|
+
console.log(`${colors.dim}│${colors.reset} ${pad(`Update available: ${colors.dim}${currentVersion}${colors.reset} → ${colors.green}${latestVersion}${colors.reset}`, 39)}${colors.dim}│${colors.reset}`);
|
|
87
|
+
console.log(`${colors.dim}│${colors.reset} ${pad(`Run ${colors.bold}lore update${colors.reset} to upgrade`, 39)}${colors.dim}│${colors.reset}`);
|
|
88
|
+
console.log(bottom);
|
|
89
|
+
// Mark as notified
|
|
90
|
+
writeCache({
|
|
91
|
+
last_check: cache?.last_check || now,
|
|
92
|
+
latest_version: latestVersion,
|
|
93
|
+
last_notified_version: latestVersion,
|
|
94
|
+
});
|
|
95
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,7 @@ import { registerPendingCommand } from './cli/commands/pending.js';
|
|
|
29
29
|
import { registerAskCommand } from './cli/commands/ask.js';
|
|
30
30
|
import { registerAuthCommands } from './cli/commands/auth.js';
|
|
31
31
|
import { registerSkillsCommand } from './cli/commands/skills.js';
|
|
32
|
+
import { registerUpdateCommand } from './cli/commands/update.js';
|
|
32
33
|
import { getExtensionRegistry, getLoreVersionString } from './extensions/registry.js';
|
|
33
34
|
import { bridgeConfigToEnv } from './core/config.js';
|
|
34
35
|
import { expandPath } from './sync/config.js';
|
|
@@ -76,6 +77,7 @@ registerMiscCommands(program, DEFAULT_DATA_DIR);
|
|
|
76
77
|
registerAskCommand(program, DEFAULT_DATA_DIR);
|
|
77
78
|
registerAuthCommands(program);
|
|
78
79
|
registerSkillsCommand(program);
|
|
80
|
+
registerUpdateCommand(program, DEFAULT_DATA_DIR);
|
|
79
81
|
// Extension system — hidden from top-level help for now
|
|
80
82
|
const extensionCmd = registerExtensionCommands(program);
|
|
81
83
|
extensionCmd._hidden = true;
|
|
@@ -101,5 +103,11 @@ process.on('unhandledRejection', (reason) => {
|
|
|
101
103
|
console.error(`\nError: ${message}`);
|
|
102
104
|
process.exit(1);
|
|
103
105
|
});
|
|
104
|
-
// Parse and run
|
|
105
|
-
program.
|
|
106
|
+
// Parse and run, then show update notification after command output
|
|
107
|
+
await program.parseAsync();
|
|
108
|
+
// Passive update notification (non-blocking, silent on errors)
|
|
109
|
+
try {
|
|
110
|
+
const { checkForUpdates } = await import('./cli/update-notifier.js');
|
|
111
|
+
await checkForUpdates();
|
|
112
|
+
}
|
|
113
|
+
catch { }
|