@shnitzel/plugscout 0.3.10 → 0.3.12
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.
|
@@ -1,24 +1,111 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
const
|
|
4
|
+
const PLUGSCOUT_MCP_STDIO = { command: 'npx', args: ['plugscout', 'mcp'] };
|
|
5
|
+
const PLUGSCOUT_MCP_ZED = { command: { path: 'npx', args: ['plugscout', 'mcp'] } };
|
|
6
|
+
function claudeDesktopConfigPath() {
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
return path.join(process.env['APPDATA'] ?? os.homedir(), 'Claude', 'claude_desktop_config.json');
|
|
9
|
+
}
|
|
10
|
+
if (process.platform === 'darwin') {
|
|
11
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
|
12
|
+
}
|
|
13
|
+
return path.join(os.homedir(), '.config', 'claude-desktop', 'claude_desktop_config.json');
|
|
14
|
+
}
|
|
15
|
+
function openCodeConfigPath() {
|
|
16
|
+
if (process.platform === 'win32') {
|
|
17
|
+
return path.join(process.env['APPDATA'] ?? os.homedir(), 'opencode', 'config.json');
|
|
18
|
+
}
|
|
19
|
+
return path.join(os.homedir(), '.config', 'opencode', 'config.json');
|
|
20
|
+
}
|
|
21
|
+
export const CLIENT_DEFS = {
|
|
22
|
+
cursor: {
|
|
23
|
+
label: 'Cursor IDE',
|
|
24
|
+
supportsProjectScope: true,
|
|
25
|
+
getConfigPath(scope) {
|
|
26
|
+
return scope === 'project'
|
|
27
|
+
? path.join(process.cwd(), '.cursor', 'mcp.json')
|
|
28
|
+
: path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
29
|
+
},
|
|
30
|
+
containerPath: ['mcpServers'],
|
|
31
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
32
|
+
},
|
|
33
|
+
gemini: {
|
|
34
|
+
label: 'Gemini CLI',
|
|
35
|
+
supportsProjectScope: false,
|
|
36
|
+
getConfigPath() {
|
|
37
|
+
return path.join(os.homedir(), '.gemini', 'settings.json');
|
|
38
|
+
},
|
|
39
|
+
containerPath: ['mcpServers'],
|
|
40
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
41
|
+
},
|
|
42
|
+
'claude-desktop': {
|
|
43
|
+
label: 'Claude Desktop',
|
|
44
|
+
supportsProjectScope: false,
|
|
45
|
+
getConfigPath() {
|
|
46
|
+
return claudeDesktopConfigPath();
|
|
47
|
+
},
|
|
48
|
+
containerPath: ['mcpServers'],
|
|
49
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
50
|
+
},
|
|
51
|
+
windsurf: {
|
|
52
|
+
label: 'Windsurf',
|
|
53
|
+
supportsProjectScope: false,
|
|
54
|
+
getConfigPath() {
|
|
55
|
+
return path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json');
|
|
56
|
+
},
|
|
57
|
+
containerPath: ['mcpServers'],
|
|
58
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
59
|
+
},
|
|
60
|
+
opencode: {
|
|
61
|
+
label: 'OpenCode',
|
|
62
|
+
supportsProjectScope: false,
|
|
63
|
+
getConfigPath() {
|
|
64
|
+
return openCodeConfigPath();
|
|
65
|
+
},
|
|
66
|
+
containerPath: ['mcp'],
|
|
67
|
+
entryValue: PLUGSCOUT_MCP_STDIO,
|
|
68
|
+
},
|
|
69
|
+
zed: {
|
|
70
|
+
label: 'Zed',
|
|
71
|
+
supportsProjectScope: false,
|
|
72
|
+
getConfigPath() {
|
|
73
|
+
return path.join(os.homedir(), '.config', 'zed', 'settings.json');
|
|
74
|
+
},
|
|
75
|
+
containerPath: ['context_servers'],
|
|
76
|
+
entryValue: PLUGSCOUT_MCP_ZED,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
export const VALID_CLIENT_KINDS = Object.keys(CLIENT_DEFS);
|
|
5
80
|
export function getConfigPath(client, scope) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
81
|
+
return CLIENT_DEFS[client].getConfigPath(scope);
|
|
82
|
+
}
|
|
83
|
+
function navigateContainer(obj, keys) {
|
|
84
|
+
let current = obj;
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
const next = current[key];
|
|
87
|
+
if (next === undefined || next === null || typeof next !== 'object' || Array.isArray(next)) {
|
|
88
|
+
current[key] = {};
|
|
9
89
|
}
|
|
10
|
-
|
|
90
|
+
current = current[key];
|
|
11
91
|
}
|
|
12
|
-
return
|
|
92
|
+
return current;
|
|
13
93
|
}
|
|
14
94
|
export async function getClientMcpConfigStatus(client, scope) {
|
|
15
|
-
const
|
|
95
|
+
const def = CLIENT_DEFS[client];
|
|
96
|
+
const configPath = def.getConfigPath(scope);
|
|
16
97
|
try {
|
|
17
98
|
const raw = await fs.readFile(configPath, 'utf8');
|
|
18
99
|
const config = JSON.parse(raw);
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
100
|
+
let container = config;
|
|
101
|
+
for (const key of def.containerPath) {
|
|
102
|
+
const next = container[key];
|
|
103
|
+
if (next === undefined || typeof next !== 'object' || next === null || Array.isArray(next)) {
|
|
104
|
+
return { configured: false, configPath };
|
|
105
|
+
}
|
|
106
|
+
container = next;
|
|
107
|
+
}
|
|
108
|
+
return { configured: !!container['plugscout'], configPath };
|
|
22
109
|
}
|
|
23
110
|
catch {
|
|
24
111
|
return { configured: false, configPath };
|
|
@@ -26,8 +113,9 @@ export async function getClientMcpConfigStatus(client, scope) {
|
|
|
26
113
|
}
|
|
27
114
|
export async function writeClientMcpConfig(options) {
|
|
28
115
|
const { client, force = false } = options;
|
|
29
|
-
const
|
|
30
|
-
const
|
|
116
|
+
const def = CLIENT_DEFS[client];
|
|
117
|
+
const scope = def.supportsProjectScope ? options.scope : 'user';
|
|
118
|
+
const configPath = def.getConfigPath(scope);
|
|
31
119
|
let existing = {};
|
|
32
120
|
try {
|
|
33
121
|
const raw = await fs.readFile(configPath, 'utf8');
|
|
@@ -36,25 +124,21 @@ export async function writeClientMcpConfig(options) {
|
|
|
36
124
|
catch {
|
|
37
125
|
// file doesn't exist yet — start empty
|
|
38
126
|
}
|
|
39
|
-
const
|
|
40
|
-
const current =
|
|
127
|
+
const container = navigateContainer(existing, def.containerPath);
|
|
128
|
+
const current = container['plugscout'];
|
|
41
129
|
if (current !== undefined) {
|
|
42
|
-
const isSame = JSON.stringify(current) === JSON.stringify(
|
|
43
|
-
if (isSame)
|
|
130
|
+
const isSame = JSON.stringify(current) === JSON.stringify(def.entryValue);
|
|
131
|
+
if (isSame)
|
|
44
132
|
return { status: 'already-configured', configPath };
|
|
45
|
-
}
|
|
46
133
|
if (!force) {
|
|
47
134
|
throw new Error(`plugscout already exists in ${configPath} with a different value. Use --force to overwrite.`);
|
|
48
135
|
}
|
|
49
136
|
}
|
|
50
|
-
|
|
51
|
-
...existing,
|
|
52
|
-
mcpServers: { ...mcpServers, plugscout: PLUGSCOUT_MCP_VALUE }
|
|
53
|
-
};
|
|
137
|
+
container['plugscout'] = def.entryValue;
|
|
54
138
|
const dir = path.dirname(configPath);
|
|
55
139
|
await fs.mkdir(dir, { recursive: true });
|
|
56
140
|
const tmpPath = `${configPath}.plugscout.tmp`;
|
|
57
|
-
await fs.writeFile(tmpPath, `${JSON.stringify(
|
|
141
|
+
await fs.writeFile(tmpPath, `${JSON.stringify(existing, null, 2)}\n`, 'utf8');
|
|
58
142
|
await fs.rename(tmpPath, configPath);
|
|
59
143
|
return { status: 'written', configPath };
|
|
60
144
|
}
|
|
@@ -5,7 +5,7 @@ import fs from 'node:fs/promises';
|
|
|
5
5
|
import { getStaleRegistries, loadSyncState } from '../../catalog/sync-state.js';
|
|
6
6
|
import { loadCatalogItems } from '../../catalog/repository.js';
|
|
7
7
|
import { hasLegacySkillSh, resolveSkillsRuntime } from '../../install/dependencies.js';
|
|
8
|
-
import { getClientMcpConfigStatus } from './client-setup.js';
|
|
8
|
+
import { getClientMcpConfigStatus, CLIENT_DEFS } from './client-setup.js';
|
|
9
9
|
export async function runDoctorChecks(projectPath = '.') {
|
|
10
10
|
const checks = [];
|
|
11
11
|
checks.push(checkSkillsRuntime());
|
|
@@ -88,6 +88,64 @@ export async function runDoctorChecks(projectPath = '.') {
|
|
|
88
88
|
catch {
|
|
89
89
|
checks.push({ name: 'Gemini MCP config', status: 'warn', message: 'Could not read Gemini MCP config', suggestion: 'Run: plugscout client setup --client gemini' });
|
|
90
90
|
}
|
|
91
|
+
// Claude Desktop check
|
|
92
|
+
const claudeDesktopConfigPath = CLIENT_DEFS['claude-desktop'].getConfigPath('user');
|
|
93
|
+
const claudeDesktopPresent = spawnSync('which', ['claude'], { encoding: 'utf8' }).status === 0 ||
|
|
94
|
+
await fs.access(path.dirname(claudeDesktopConfigPath)).then(() => true).catch(() => false) ||
|
|
95
|
+
await fs.access(path.join('/', 'Applications', 'Claude.app')).then(() => true).catch(() => false);
|
|
96
|
+
checks.push(claudeDesktopPresent
|
|
97
|
+
? { name: 'Claude Desktop', status: 'pass', message: 'Claude Desktop detected' }
|
|
98
|
+
: { name: 'Claude Desktop', status: 'warn', message: 'Claude Desktop not detected', suggestion: 'Install from https://claude.ai/download' });
|
|
99
|
+
// Claude Desktop MCP config check
|
|
100
|
+
try {
|
|
101
|
+
const claudeStatus = await getClientMcpConfigStatus('claude-desktop', 'user');
|
|
102
|
+
checks.push(claudeStatus.configured
|
|
103
|
+
? { name: 'Claude Desktop MCP', status: 'pass', message: `plugscout wired in ${claudeStatus.configPath}` }
|
|
104
|
+
: { name: 'Claude Desktop MCP', status: 'warn', message: 'plugscout not in Claude Desktop config', suggestion: 'Run: plugscout client setup --client claude-desktop' });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
checks.push({ name: 'Claude Desktop MCP', status: 'warn', message: 'Could not read Claude Desktop config', suggestion: 'Run: plugscout client setup --client claude-desktop' });
|
|
108
|
+
}
|
|
109
|
+
// Windsurf check
|
|
110
|
+
checks.push(checkBinary('windsurf', { suggestion: 'Install Windsurf from https://windsurf.ai' }));
|
|
111
|
+
// Windsurf MCP config check
|
|
112
|
+
try {
|
|
113
|
+
const windsurfStatus = await getClientMcpConfigStatus('windsurf', 'user');
|
|
114
|
+
checks.push(windsurfStatus.configured
|
|
115
|
+
? { name: 'Windsurf MCP config', status: 'pass', message: `plugscout wired in ${windsurfStatus.configPath}` }
|
|
116
|
+
: { name: 'Windsurf MCP config', status: 'warn', message: 'plugscout not in Windsurf MCP config', suggestion: 'Run: plugscout client setup --client windsurf' });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
checks.push({ name: 'Windsurf MCP config', status: 'warn', message: 'Could not read Windsurf MCP config', suggestion: 'Run: plugscout client setup --client windsurf' });
|
|
120
|
+
}
|
|
121
|
+
// OpenCode check
|
|
122
|
+
checks.push(checkBinary('opencode', { suggestion: 'Install OpenCode: npm install -g opencode-ai' }));
|
|
123
|
+
// OpenCode MCP config check
|
|
124
|
+
try {
|
|
125
|
+
const opencodeStatus = await getClientMcpConfigStatus('opencode', 'user');
|
|
126
|
+
checks.push(opencodeStatus.configured
|
|
127
|
+
? { name: 'OpenCode MCP config', status: 'pass', message: `plugscout wired in ${opencodeStatus.configPath}` }
|
|
128
|
+
: { name: 'OpenCode MCP config', status: 'warn', message: 'plugscout not in OpenCode config', suggestion: 'Run: plugscout client setup --client opencode' });
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
checks.push({ name: 'OpenCode MCP config', status: 'warn', message: 'Could not read OpenCode config', suggestion: 'Run: plugscout client setup --client opencode' });
|
|
132
|
+
}
|
|
133
|
+
// Zed check
|
|
134
|
+
const zedInstalled = spawnSync('which', ['zed'], { encoding: 'utf8' }).status === 0 ||
|
|
135
|
+
await fs.access(path.join('/', 'Applications', 'Zed.app')).then(() => true).catch(() => false);
|
|
136
|
+
checks.push(zedInstalled
|
|
137
|
+
? { name: 'Zed', status: 'pass', message: 'Zed detected' }
|
|
138
|
+
: { name: 'Zed', status: 'warn', message: 'Zed not detected', suggestion: 'Install Zed from https://zed.dev' });
|
|
139
|
+
// Zed MCP config check
|
|
140
|
+
try {
|
|
141
|
+
const zedStatus = await getClientMcpConfigStatus('zed', 'user');
|
|
142
|
+
checks.push(zedStatus.configured
|
|
143
|
+
? { name: 'Zed MCP config', status: 'pass', message: `plugscout wired in ${zedStatus.configPath}` }
|
|
144
|
+
: { name: 'Zed MCP config', status: 'warn', message: 'plugscout not in Zed settings', suggestion: 'Run: plugscout client setup --client zed' });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
checks.push({ name: 'Zed MCP config', status: 'warn', message: 'Could not read Zed settings', suggestion: 'Run: plugscout client setup --client zed' });
|
|
148
|
+
}
|
|
91
149
|
return checks;
|
|
92
150
|
}
|
|
93
151
|
function checkSkillsRuntime() {
|
|
@@ -30,7 +30,7 @@ import { hasFlag, readCsvList, readFlag, readKinds, readLimit, readSort } from '
|
|
|
30
30
|
import { renderHomeScreen, renderInteractiveHome } from './ui/home.js';
|
|
31
31
|
import { handleMcp } from './mcp.js';
|
|
32
32
|
import { writeWebReport } from './ui/web-report.js';
|
|
33
|
-
import { checkForUpdateNow, maybeNotifyAboutUpdate, RELEASE_DOWNLOAD_URL } from './update-check.js';
|
|
33
|
+
import { applyUpdate, checkForUpdateNow, maybeNotifyAboutUpdate, RELEASE_DOWNLOAD_URL } from './update-check.js';
|
|
34
34
|
const COMMAND_ALIASES = {
|
|
35
35
|
home: 'home',
|
|
36
36
|
about: 'about',
|
|
@@ -860,38 +860,65 @@ async function handleQuarantine(args) {
|
|
|
860
860
|
async function handleClient(args) {
|
|
861
861
|
const subcommand = args[0];
|
|
862
862
|
if (subcommand !== 'setup') {
|
|
863
|
-
throw new Error('Usage: client setup --client cursor|gemini [--scope user|project] [--force]');
|
|
863
|
+
throw new Error('Usage: client setup --client cursor|gemini|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
|
|
864
864
|
}
|
|
865
|
+
const { writeClientMcpConfig, CLIENT_DEFS, VALID_CLIENT_KINDS } = await import('./client-setup.js');
|
|
865
866
|
const clientFlag = readFlag(args, '--client');
|
|
866
|
-
if (clientFlag
|
|
867
|
-
throw new Error(
|
|
867
|
+
if (!clientFlag || !VALID_CLIENT_KINDS.includes(clientFlag)) {
|
|
868
|
+
throw new Error(`Usage: client setup --client ${VALID_CLIENT_KINDS.join('|')}`);
|
|
868
869
|
}
|
|
869
870
|
const scopeFlag = readFlag(args, '--scope') ?? 'user';
|
|
870
871
|
if (scopeFlag !== 'user' && scopeFlag !== 'project') {
|
|
871
872
|
throw new Error('--scope must be user or project');
|
|
872
873
|
}
|
|
873
|
-
|
|
874
|
-
|
|
874
|
+
const def = CLIENT_DEFS[clientFlag];
|
|
875
|
+
if (!def.supportsProjectScope && scopeFlag === 'project') {
|
|
876
|
+
logger.warn(`${def.label} only supports user scope; falling back to user scope.`);
|
|
875
877
|
}
|
|
876
878
|
const force = hasFlag(args, '--force');
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
+
const result = await writeClientMcpConfig({
|
|
880
|
+
client: clientFlag,
|
|
881
|
+
scope: scopeFlag,
|
|
882
|
+
force
|
|
883
|
+
});
|
|
879
884
|
if (result.status === 'already-configured') {
|
|
880
885
|
console.log(`plugscout already configured in ${result.configPath}`);
|
|
881
886
|
}
|
|
882
887
|
else {
|
|
883
888
|
console.log(`plugscout MCP config written: ${result.configPath}`);
|
|
884
|
-
printHint(`Restart ${
|
|
889
|
+
printHint(`Restart ${def.label} for the change to take effect.`);
|
|
885
890
|
}
|
|
886
891
|
}
|
|
887
892
|
async function handleUpgrade(args) {
|
|
888
893
|
const subcommand = args[0] ?? 'check';
|
|
894
|
+
if (subcommand === 'apply') {
|
|
895
|
+
const result = await applyUpdate();
|
|
896
|
+
renderApplyResult(result);
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
889
899
|
if (subcommand !== 'check') {
|
|
890
|
-
throw new Error('Usage: upgrade check');
|
|
900
|
+
throw new Error('Usage: upgrade check | upgrade apply');
|
|
891
901
|
}
|
|
892
902
|
const result = await checkForUpdateNow();
|
|
893
903
|
renderUpgradeResult(result);
|
|
894
904
|
}
|
|
905
|
+
function renderApplyResult(result) {
|
|
906
|
+
if (result.status === 'upgraded') {
|
|
907
|
+
console.log(`PlugScout upgraded: v${result.fromVersion} -> v${result.toVersion}`);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (result.status === 'already-latest') {
|
|
911
|
+
console.log(`PlugScout is already up to date (v${result.currentVersion}).`);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (result.status === 'no-release') {
|
|
915
|
+
console.log('No published release found yet.');
|
|
916
|
+
console.log(`Releases: ${RELEASE_DOWNLOAD_URL}`);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
console.log(`Upgrade failed: ${result.detail}`);
|
|
920
|
+
console.log(`Manual install: npm install -g @shnitzel/plugscout@latest`);
|
|
921
|
+
}
|
|
895
922
|
function renderUpgradeResult(result) {
|
|
896
923
|
if (result.status === 'no-release') {
|
|
897
924
|
console.log('No published release found yet.');
|
|
@@ -910,7 +937,7 @@ function renderUpgradeResult(result) {
|
|
|
910
937
|
return;
|
|
911
938
|
}
|
|
912
939
|
console.log(`New PlugScout version available: v${result.currentVersion} -> v${result.latestVersion}`);
|
|
913
|
-
console.log(`Download: ${RELEASE_DOWNLOAD_URL}`);
|
|
940
|
+
console.log(`Upgrade: plugscout upgrade apply | Download: ${RELEASE_DOWNLOAD_URL}`);
|
|
914
941
|
}
|
|
915
942
|
function sortRecommendations(recommendations, sort) {
|
|
916
943
|
const sorted = [...recommendations];
|
|
@@ -1036,8 +1063,9 @@ function printHelp() {
|
|
|
1036
1063
|
console.log('Other');
|
|
1037
1064
|
console.log(' about');
|
|
1038
1065
|
console.log(' web [--out .plugscout/report.html] [--kind ...] [--limit n] [--open]');
|
|
1039
|
-
console.log(' client setup --client cursor|gemini [--scope user|project] [--force]');
|
|
1066
|
+
console.log(' client setup --client cursor|gemini|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
|
|
1040
1067
|
console.log(' upgrade check');
|
|
1068
|
+
console.log(' upgrade apply auto-install latest version via npm');
|
|
1041
1069
|
console.log(' help');
|
|
1042
1070
|
console.log('');
|
|
1043
1071
|
console.log('Kind aliases');
|
|
@@ -1057,6 +1085,10 @@ function printHelp() {
|
|
|
1057
1085
|
console.log(' plugscout show --id claude-connector:asana');
|
|
1058
1086
|
console.log(' plugscout client setup --client cursor');
|
|
1059
1087
|
console.log(' plugscout client setup --client gemini');
|
|
1088
|
+
console.log(' plugscout client setup --client claude-desktop');
|
|
1089
|
+
console.log(' plugscout client setup --client windsurf');
|
|
1090
|
+
console.log(' plugscout client setup --client opencode');
|
|
1091
|
+
console.log(' plugscout client setup --client zed');
|
|
1060
1092
|
console.log(' plugscout sync --kind cursor-extension,gemini-extension');
|
|
1061
1093
|
console.log('');
|
|
1062
1094
|
console.log('Global options');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
2
3
|
import semver from 'semver';
|
|
3
4
|
import { readJsonFile, writeJsonFile } from '../../lib/json.js';
|
|
4
5
|
import { getPackagePath, getStatePath } from '../../lib/paths.js';
|
|
@@ -80,7 +81,7 @@ export async function maybeNotifyAboutUpdate(options = {}) {
|
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
83
|
console.log(`New PlugScout version available: v${currentVersion} -> v${state.latestVersion}`);
|
|
83
|
-
console.log(`Download: ${RELEASE_DOWNLOAD_URL}`);
|
|
84
|
+
console.log(`Upgrade: plugscout upgrade apply | Download: ${RELEASE_DOWNLOAD_URL}`);
|
|
84
85
|
await saveUpdateCheckState({
|
|
85
86
|
...state,
|
|
86
87
|
source: 'github-releases',
|
|
@@ -113,6 +114,32 @@ export async function checkForUpdateNow() {
|
|
|
113
114
|
}
|
|
114
115
|
return { status: 'up-to-date', currentVersion, latestVersion: result.latestVersion };
|
|
115
116
|
}
|
|
117
|
+
export async function applyUpdate() {
|
|
118
|
+
const currentVersion = await loadCurrentVersion();
|
|
119
|
+
const release = await lookupLatestReleaseVersion();
|
|
120
|
+
if (release.status === 'error') {
|
|
121
|
+
return { status: 'error', currentVersion, detail: 'Could not reach GitHub releases API.' };
|
|
122
|
+
}
|
|
123
|
+
if (release.status === 'no-release') {
|
|
124
|
+
return { status: 'no-release', currentVersion };
|
|
125
|
+
}
|
|
126
|
+
if (!isVersionNewer(release.latestVersion, currentVersion)) {
|
|
127
|
+
return { status: 'already-latest', currentVersion, latestVersion: release.latestVersion };
|
|
128
|
+
}
|
|
129
|
+
const pkg = '@shnitzel/plugscout@latest';
|
|
130
|
+
const result = spawnSync('npm', ['install', '-g', pkg], { stdio: 'inherit' });
|
|
131
|
+
if (result.status !== 0) {
|
|
132
|
+
return { status: 'error', currentVersion, detail: `npm install -g ${pkg} exited with ${result.status ?? 'signal'}` };
|
|
133
|
+
}
|
|
134
|
+
await saveUpdateCheckState({
|
|
135
|
+
...(await loadUpdateCheckState()),
|
|
136
|
+
source: 'github-releases',
|
|
137
|
+
lastCheckedAt: new Date().toISOString(),
|
|
138
|
+
latestVersion: release.latestVersion,
|
|
139
|
+
lastNotifiedVersion: release.latestVersion
|
|
140
|
+
});
|
|
141
|
+
return { status: 'upgraded', fromVersion: currentVersion, toVersion: release.latestVersion };
|
|
142
|
+
}
|
|
116
143
|
async function lookupLatestReleaseVersion() {
|
|
117
144
|
const controller = new AbortController();
|
|
118
145
|
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
package/package.json
CHANGED