@shnitzel/plugscout 0.3.11 → 0.3.13

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.
@@ -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',
@@ -891,12 +891,34 @@ async function handleClient(args) {
891
891
  }
892
892
  async function handleUpgrade(args) {
893
893
  const subcommand = args[0] ?? 'check';
894
+ if (subcommand === 'apply') {
895
+ const result = await applyUpdate();
896
+ renderApplyResult(result);
897
+ return;
898
+ }
894
899
  if (subcommand !== 'check') {
895
- throw new Error('Usage: upgrade check');
900
+ throw new Error('Usage: upgrade check | upgrade apply');
896
901
  }
897
902
  const result = await checkForUpdateNow();
898
903
  renderUpgradeResult(result);
899
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
+ }
900
922
  function renderUpgradeResult(result) {
901
923
  if (result.status === 'no-release') {
902
924
  console.log('No published release found yet.');
@@ -915,7 +937,7 @@ function renderUpgradeResult(result) {
915
937
  return;
916
938
  }
917
939
  console.log(`New PlugScout version available: v${result.currentVersion} -> v${result.latestVersion}`);
918
- console.log(`Download: ${RELEASE_DOWNLOAD_URL}`);
940
+ console.log(`Upgrade: plugscout upgrade apply | Download: ${RELEASE_DOWNLOAD_URL}`);
919
941
  }
920
942
  function sortRecommendations(recommendations, sort) {
921
943
  const sorted = [...recommendations];
@@ -1043,6 +1065,7 @@ function printHelp() {
1043
1065
  console.log(' web [--out .plugscout/report.html] [--kind ...] [--limit n] [--open]');
1044
1066
  console.log(' client setup --client cursor|gemini|claude-desktop|windsurf|opencode|zed [--scope user|project] [--force]');
1045
1067
  console.log(' upgrade check');
1068
+ console.log(' upgrade apply auto-install latest version via npm');
1046
1069
  console.log(' help');
1047
1070
  console.log('');
1048
1071
  console.log('Kind aliases');
@@ -192,24 +192,27 @@ export async function renderInteractiveHome() {
192
192
  const CTRL_C = '\u0003';
193
193
  function render(firstRender) {
194
194
  if (!firstRender) {
195
- process.stdout.write(`\x1b[${menuItems.length * 2}A\r`);
195
+ // Restore saved cursor position and clear everything below it.
196
+ // This avoids line-count arithmetic that breaks when descriptions wrap.
197
+ process.stdout.write('\x1b[u\x1b[0J');
196
198
  }
197
199
  for (let i = 0; i < menuItems.length; i++) {
198
200
  const item = menuItems[i];
199
201
  const prefix = i === selected ? ' \u276f ' : ' ';
200
- process.stdout.write(`\x1b[2K${prefix}${item.label}\n`);
202
+ process.stdout.write(`${prefix}${item.label}\n`);
201
203
  if (item.description) {
202
204
  const firstLine = item.description.split('\n')[0];
203
- process.stdout.write(`\x1b[2K \x1b[2m${firstLine}\x1b[0m\n`);
205
+ process.stdout.write(` \x1b[2m${firstLine}\x1b[0m\n`);
204
206
  }
205
207
  else {
206
- process.stdout.write(`\x1b[2K\n`);
208
+ process.stdout.write('\n');
207
209
  }
208
210
  }
209
211
  }
210
212
  let running = true;
211
213
  while (running) {
212
214
  process.stdout.write('\n');
215
+ process.stdout.write('\x1b[s'); // save cursor — used by render(false) to redraw in-place
213
216
  process.stdin.setRawMode(true);
214
217
  process.stdin.resume();
215
218
  process.stdin.setEncoding('utf8');
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shnitzel/plugscout",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
4
4
  "description": "Claude plugins + Claude connectors + Copilot extensions + Skills + MCP security intelligence framework",
5
5
  "private": false,
6
6
  "type": "module",