@empirical-gauzza/cli 1.0.16 → 1.0.17
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/lib/pluginUpdateCheck.js +148 -143
- package/package.json +1 -1
package/lib/pluginUpdateCheck.js
CHANGED
|
@@ -1,143 +1,148 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { askYesNo, ask, log, warn, uiHeader, parseSelection } = require('./ui');
|
|
4
|
-
const { runResolvedCommand, commandExists, summarizeProcessOutput } = require('./fsUtils');
|
|
5
|
-
const { readDeviceConfig, writeDeviceConfig } = require('./deviceConfig');
|
|
6
|
-
const { installNativePlugin } = require('./installers/nativePlugin');
|
|
7
|
-
|
|
8
|
-
const PLUGIN_METADATA_URL = 'https://raw.githubusercontent.com/mattgauzza/empirical-memory-plugin/main/.claude-plugin/marketplace.json';
|
|
9
|
-
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
-
const SUPPORTED_TARGETS = ['codex', 'claude', 'copilot'];
|
|
11
|
-
|
|
12
|
-
function parseVersion(value) {
|
|
13
|
-
const match = String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/i);
|
|
14
|
-
return match ? match.slice(1).map(Number) : null;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function compareVersions(left, right) {
|
|
18
|
-
const a = parseVersion(left);
|
|
19
|
-
const b = parseVersion(right);
|
|
20
|
-
if (!a || !b) return null;
|
|
21
|
-
for (let i = 0; i < 3; i += 1) {
|
|
22
|
-
if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;
|
|
23
|
-
}
|
|
24
|
-
return 0;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function extractVersion(output, pluginName = 'empirical-memory') {
|
|
28
|
-
const text = String(output || '');
|
|
29
|
-
const jsonCandidates = [text, ...text.split(/\r?\n/).map((line) => line.trim())];
|
|
30
|
-
for (const candidate of jsonCandidates) {
|
|
31
|
-
try {
|
|
32
|
-
const value = JSON.parse(candidate);
|
|
33
|
-
const rows = Array.isArray(value) ? value : [value, ...(Array.isArray(value?.plugins) ? value.plugins : [])];
|
|
34
|
-
const match = rows.find((row) => row && (row.name === pluginName || row.plugin === pluginName));
|
|
35
|
-
if (match?.version) return String(match.version);
|
|
36
|
-
} catch (_error) {
|
|
37
|
-
// Native CLIs may print a table instead of JSON.
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
const line = text.split(/\r?\n/).find((value) => value.toLowerCase().includes(pluginName.toLowerCase()));
|
|
41
|
-
const match = line && line.match(new RegExp(`${pluginName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\d]*(\\d+\\.\\d+\\.\\d+)`, 'i'));
|
|
42
|
-
return match ? match[1] : null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function readInstalledVersions(target, pluginNames) {
|
|
46
|
-
const commands = {
|
|
47
|
-
codex: ['codex', ['plugin', 'list']],
|
|
48
|
-
claude: ['claude', ['plugin', 'list']],
|
|
49
|
-
copilot: ['copilot', ['plugin', 'list']]
|
|
50
|
-
};
|
|
51
|
-
const [command, args] = commands[target];
|
|
52
|
-
if (!commandExists(command)) return null;
|
|
53
|
-
const result = runResolvedCommand(command, args, { stdio: 'pipe' });
|
|
54
|
-
if (!result || result.status !== 0) return null;
|
|
55
|
-
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
56
|
-
return Object.fromEntries(pluginNames.map((name) => [name, extractVersion(output, name)]));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function fetchLatestVersions(fetchImpl = globalThis.fetch) {
|
|
60
|
-
if (typeof fetchImpl !== 'function') return null;
|
|
61
|
-
const response = await fetchImpl(PLUGIN_METADATA_URL, { signal: AbortSignal.timeout(5000) });
|
|
62
|
-
if (!response.ok) throw new Error(`marketplace metadata returned HTTP ${response.status}`);
|
|
63
|
-
const data = await response.json();
|
|
64
|
-
if (Array.isArray(data?.plugins)) {
|
|
65
|
-
return Object.fromEntries(data.plugins.map((plugin) => [plugin.name, plugin.version]).filter(([, version]) => version));
|
|
66
|
-
}
|
|
67
|
-
return data?.version ? { 'empirical-memory': data.version } : {};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function shouldCheck(cache, now = Date.now(), force = false) {
|
|
71
|
-
return force || !cache?.checkedAt || now - Number(cache.checkedAt) >= CHECK_INTERVAL_MS;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function checkNativePluginUpdates({ force = false, interactive = process.stdin.isTTY, fetchImpl } = {}) {
|
|
75
|
-
if (!interactive && !force) return { skipped: true, reason: 'noninteractive' };
|
|
76
|
-
const config = readDeviceConfig();
|
|
77
|
-
const cache = config.pluginUpdates || {};
|
|
78
|
-
if (!shouldCheck(cache, Date.now(), force)) return { skipped: true, reason: 'rate_limited' };
|
|
79
|
-
|
|
80
|
-
let latestVersion;
|
|
81
|
-
try {
|
|
82
|
-
latestVersion = await fetchLatestVersions(fetchImpl);
|
|
83
|
-
} catch (error) {
|
|
84
|
-
config.pluginUpdates = { ...cache, checkedAt: Date.now(), lastError: error.message };
|
|
85
|
-
writeDeviceConfig(config);
|
|
86
|
-
return { skipped: true, reason: 'marketplace_unavailable' };
|
|
87
|
-
}
|
|
88
|
-
const pluginNames = Object.keys(latestVersion);
|
|
89
|
-
const installed = {};
|
|
90
|
-
for (const target of SUPPORTED_TARGETS) installed[target] = readInstalledVersions(target, pluginNames);
|
|
91
|
-
config.pluginUpdates = { checkedAt: Date.now(), latestVersions: latestVersion, installed };
|
|
92
|
-
writeDeviceConfig(config);
|
|
93
|
-
|
|
94
|
-
const outdated = SUPPORTED_TARGETS.flatMap((target) => pluginNames
|
|
95
|
-
.filter((name) => compareVersions(latestVersion[name], installed[target][name]) === 1)
|
|
96
|
-
.map((name) => ({ target, name, installed: installed[target][name], latest: latestVersion[name] })));
|
|
97
|
-
const latestSignature = JSON.stringify(latestVersion);
|
|
98
|
-
if (outdated.length === 0 || cache.offeredVersion === latestSignature) {
|
|
99
|
-
return { latestVersion, installed, outdated };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (!interactive) {
|
|
103
|
-
return { latestVersion, installed, outdated, promptSuppressed: true };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { askYesNo, ask, log, warn, uiHeader, parseSelection } = require('./ui');
|
|
4
|
+
const { runResolvedCommand, commandExists, summarizeProcessOutput } = require('./fsUtils');
|
|
5
|
+
const { readDeviceConfig, writeDeviceConfig } = require('./deviceConfig');
|
|
6
|
+
const { installNativePlugin } = require('./installers/nativePlugin');
|
|
7
|
+
|
|
8
|
+
const PLUGIN_METADATA_URL = 'https://raw.githubusercontent.com/mattgauzza/empirical-memory-plugin/main/.claude-plugin/marketplace.json';
|
|
9
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
10
|
+
const SUPPORTED_TARGETS = ['codex', 'claude', 'copilot'];
|
|
11
|
+
|
|
12
|
+
function parseVersion(value) {
|
|
13
|
+
const match = String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)/i);
|
|
14
|
+
return match ? match.slice(1).map(Number) : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function compareVersions(left, right) {
|
|
18
|
+
const a = parseVersion(left);
|
|
19
|
+
const b = parseVersion(right);
|
|
20
|
+
if (!a || !b) return null;
|
|
21
|
+
for (let i = 0; i < 3; i += 1) {
|
|
22
|
+
if (a[i] !== b[i]) return a[i] > b[i] ? 1 : -1;
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function extractVersion(output, pluginName = 'empirical-memory') {
|
|
28
|
+
const text = String(output || '');
|
|
29
|
+
const jsonCandidates = [text, ...text.split(/\r?\n/).map((line) => line.trim())];
|
|
30
|
+
for (const candidate of jsonCandidates) {
|
|
31
|
+
try {
|
|
32
|
+
const value = JSON.parse(candidate);
|
|
33
|
+
const rows = Array.isArray(value) ? value : [value, ...(Array.isArray(value?.plugins) ? value.plugins : [])];
|
|
34
|
+
const match = rows.find((row) => row && (row.name === pluginName || row.plugin === pluginName));
|
|
35
|
+
if (match?.version) return String(match.version);
|
|
36
|
+
} catch (_error) {
|
|
37
|
+
// Native CLIs may print a table instead of JSON.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const line = text.split(/\r?\n/).find((value) => value.toLowerCase().includes(pluginName.toLowerCase()));
|
|
41
|
+
const match = line && line.match(new RegExp(`${pluginName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[^\\d]*(\\d+\\.\\d+\\.\\d+)`, 'i'));
|
|
42
|
+
return match ? match[1] : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readInstalledVersions(target, pluginNames) {
|
|
46
|
+
const commands = {
|
|
47
|
+
codex: ['codex', ['plugin', 'list']],
|
|
48
|
+
claude: ['claude', ['plugin', 'list']],
|
|
49
|
+
copilot: ['copilot', ['plugin', 'list']]
|
|
50
|
+
};
|
|
51
|
+
const [command, args] = commands[target];
|
|
52
|
+
if (!commandExists(command)) return null;
|
|
53
|
+
const result = runResolvedCommand(command, args, { stdio: 'pipe' });
|
|
54
|
+
if (!result || result.status !== 0) return null;
|
|
55
|
+
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
56
|
+
return Object.fromEntries(pluginNames.map((name) => [name, extractVersion(output, name)]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchLatestVersions(fetchImpl = globalThis.fetch) {
|
|
60
|
+
if (typeof fetchImpl !== 'function') return null;
|
|
61
|
+
const response = await fetchImpl(PLUGIN_METADATA_URL, { signal: AbortSignal.timeout(5000) });
|
|
62
|
+
if (!response.ok) throw new Error(`marketplace metadata returned HTTP ${response.status}`);
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
if (Array.isArray(data?.plugins)) {
|
|
65
|
+
return Object.fromEntries(data.plugins.map((plugin) => [plugin.name, plugin.version]).filter(([, version]) => version));
|
|
66
|
+
}
|
|
67
|
+
return data?.version ? { 'empirical-memory': data.version } : {};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function shouldCheck(cache, now = Date.now(), force = false) {
|
|
71
|
+
return force || !cache?.checkedAt || now - Number(cache.checkedAt) >= CHECK_INTERVAL_MS;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function checkNativePluginUpdates({ force = false, interactive = process.stdin.isTTY, fetchImpl } = {}) {
|
|
75
|
+
if (!interactive && !force) return { skipped: true, reason: 'noninteractive' };
|
|
76
|
+
const config = readDeviceConfig();
|
|
77
|
+
const cache = config.pluginUpdates || {};
|
|
78
|
+
if (!shouldCheck(cache, Date.now(), force)) return { skipped: true, reason: 'rate_limited' };
|
|
79
|
+
|
|
80
|
+
let latestVersion;
|
|
81
|
+
try {
|
|
82
|
+
latestVersion = await fetchLatestVersions(fetchImpl);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
config.pluginUpdates = { ...cache, checkedAt: Date.now(), lastError: error.message };
|
|
85
|
+
writeDeviceConfig(config);
|
|
86
|
+
return { skipped: true, reason: 'marketplace_unavailable' };
|
|
87
|
+
}
|
|
88
|
+
const pluginNames = Object.keys(latestVersion);
|
|
89
|
+
const installed = {};
|
|
90
|
+
for (const target of SUPPORTED_TARGETS) installed[target] = readInstalledVersions(target, pluginNames);
|
|
91
|
+
config.pluginUpdates = { checkedAt: Date.now(), latestVersions: latestVersion, installed };
|
|
92
|
+
writeDeviceConfig(config);
|
|
93
|
+
|
|
94
|
+
const outdated = SUPPORTED_TARGETS.flatMap((target) => pluginNames
|
|
95
|
+
.filter((name) => compareVersions(latestVersion[name], installed[target][name]) === 1)
|
|
96
|
+
.map((name) => ({ target, name, installed: installed[target][name], latest: latestVersion[name] })));
|
|
97
|
+
const latestSignature = JSON.stringify(latestVersion);
|
|
98
|
+
if (outdated.length === 0 || cache.offeredVersion === latestSignature) {
|
|
99
|
+
return { latestVersion, installed, outdated };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!interactive) {
|
|
103
|
+
return { latestVersion, installed, outdated, promptSuppressed: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// latestVersion is a map of pluginName -> version (e.g. {'empirical-memory': '1.1.2', ...}),
|
|
107
|
+
// not a single scalar -- interpolating it directly stringified to "[object Object]". Prefer
|
|
108
|
+
// the core plugin's version since that's the one most users recognize; the per-target
|
|
109
|
+
// breakdown line right below already lists every outdated plugin/version precisely.
|
|
110
|
+
const headlineVersion = latestVersion['empirical-memory'] || Object.values(latestVersion)[0] || 'update available';
|
|
111
|
+
log(uiHeader(`A newer empirical-memory plugin is available (${headlineVersion}).`));
|
|
112
|
+
log(`Installed versions: ${outdated.map((item) => `${item.target} ${item.name} ${item.installed} -> ${item.latest}`).join(', ')}`);
|
|
113
|
+
const update = await askYesNo('Update the native plugin now?', true);
|
|
114
|
+
if (!update) {
|
|
115
|
+
config.pluginUpdates = { ...config.pluginUpdates, offeredVersion: latestSignature, outcome: 'declined' };
|
|
116
|
+
writeDeviceConfig(config);
|
|
117
|
+
return { latestVersion, installed, outdated, declined: true };
|
|
118
|
+
}
|
|
119
|
+
const selection = (await ask('Which agents should be updated? (all, or numbers 1-' + outdated.length + ') ')).trim().toLowerCase();
|
|
120
|
+
const outdatedTargets = [...new Set(outdated.map((item) => item.target))];
|
|
121
|
+
const selectedTargets = selection === 'all' || !selection
|
|
122
|
+
? outdatedTargets
|
|
123
|
+
: parseSelection(selection, outdatedTargets.length).map((index) => outdatedTargets[index]);
|
|
124
|
+
const results = {};
|
|
125
|
+
for (const target of selectedTargets) {
|
|
126
|
+
try {
|
|
127
|
+
installNativePlugin(target, [...new Set(outdated.filter((item) => item.target === target).map((item) => item.name))]);
|
|
128
|
+
results[target] = 'updated';
|
|
129
|
+
} catch (error) {
|
|
130
|
+
results[target] = `failed: ${summarizeProcessOutput({ stderr: error.message })}`;
|
|
131
|
+
warn(`Could not update ${target}: ${error.message}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
config.pluginUpdates = { ...config.pluginUpdates, offeredVersion: latestSignature, outcome: results };
|
|
135
|
+
writeDeviceConfig(config);
|
|
136
|
+
return { latestVersion, installed, outdated, results };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
CHECK_INTERVAL_MS,
|
|
141
|
+
SUPPORTED_TARGETS,
|
|
142
|
+
parseVersion,
|
|
143
|
+
compareVersions,
|
|
144
|
+
extractVersion,
|
|
145
|
+
shouldCheck,
|
|
146
|
+
fetchLatestVersions,
|
|
147
|
+
checkNativePluginUpdates
|
|
148
|
+
};
|