@getlore/cli 0.3.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/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
|
@@ -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 { }
|