@askviraj/ai-plugins 1.0.1 → 1.1.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.
Files changed (3) hide show
  1. package/bin/installer.js +219 -7
  2. package/package.json +20 -1
  3. package/readme.md +44 -0
package/bin/installer.js CHANGED
@@ -49,6 +49,12 @@ const SUBAGENT_STATUS_LINE = {
49
49
  const MARKETPLACE_REF = "virajp/ai-plugins";
50
50
  const MARKETPLACE_NAME = "virajp-plugins";
51
51
 
52
+ // Sources for the latest versions (used by --version and --upgrade): the
53
+ // marketplace manifest on the repo's main branch, and the published CLI on npm.
54
+ const REMOTE_MARKETPLACE_URL =
55
+ "https://raw.githubusercontent.com/virajp/ai-plugins/main/.claude-plugin/marketplace.json";
56
+ const NPM_LATEST_URL = "https://registry.npmjs.org/@askviraj/ai-plugins/latest";
57
+
52
58
  // All plugins published by the virajp-plugins marketplace.
53
59
  const PLUGINS = ["vwf", "typescript-lsp", "dart-lsp", "context7", "mempalace"];
54
60
 
@@ -83,22 +89,122 @@ const PLUGIN_EXTRA_DEPS = {
83
89
  class Installer extends Command {
84
90
  async run() {
85
91
  const { flags } = await this.parse(Installer);
92
+
93
+ if (flags.version) {
94
+ await this.printVersions();
95
+ return;
96
+ }
97
+
86
98
  const plan = this.resolvePlan(flags);
99
+ const hasInstall = plan.plugins.length
100
+ || plan.statusLine
101
+ || plan.subagentStatusLine;
87
102
 
88
- if (!plan.plugins.length && !plan.statusLine && !plan.subagentStatusLine) {
103
+ if (!hasInstall && !flags.upgrade) {
89
104
  this.error(
90
- "Nothing to do. Pass --all, --plugins, --plugin <name>, --statusline, and/or --subagentstatusline.",
105
+ "Nothing to do. Pass --all, --plugins, --plugin <name>, --statusline, --subagentstatusline, --upgrade, or --version.",
91
106
  );
92
107
  }
93
108
 
94
- this.checkDeps(plan);
109
+ // Install first — so a fresh machine ends up with the requested plugins…
110
+ if (hasInstall) {
111
+ this.checkDeps(plan);
112
+ if (plan.plugins.length) {
113
+ this.installPlugins(plan.plugins);
114
+ }
115
+ if (plan.statusLine || plan.subagentStatusLine) {
116
+ await this.installStatusline(plan, flags.yes);
117
+ }
118
+ }
95
119
 
96
- if (plan.plugins.length) {
97
- this.installPlugins(plan.plugins);
120
+ // …then upgrade everything that is installed to the latest versions.
121
+ if (flags.upgrade) {
122
+ await this.upgrade();
98
123
  }
124
+ }
99
125
 
100
- if (plan.statusLine || plan.subagentStatusLine) {
101
- await this.installStatusline(plan, flags.yes);
126
+ // Print the CLI version (vs npm latest), the bundled statusline version, and
127
+ // each plugin's installed version (from `claude plugin list`) vs the latest in
128
+ // the remote marketplace, flagging updates. Errors out if the network or the
129
+ // claude CLI is unavailable.
130
+ async printVersions() {
131
+ const cli = this.config.version;
132
+ const cliLatest = (await fetchJson(NPM_LATEST_URL)).version;
133
+ const latest = await remoteLatest();
134
+ const installed = installedPlugins();
135
+
136
+ this.log(`${this.config.name} ${cli}${updateNote(cli, cliLatest)}`);
137
+ this.log(` ${"statusline".padEnd(16)} ${cli} (bundled with the CLI)`);
138
+
139
+ this.log(`\nPlugins (${MARKETPLACE_NAME}):`);
140
+ for (const name of PLUGINS) {
141
+ this.log(
142
+ ` ${name.padEnd(16)} ${
143
+ pluginVersionLine(installed[name], latest[name])
144
+ }`,
145
+ );
146
+ }
147
+ }
148
+
149
+ // Upgrade every installed virajp-plugins plugin to its latest version, refresh
150
+ // the statusline (if installed) to this CLI's bundled version, and note a newer
151
+ // CLI. Runs after the install phase, so newly installed plugins are already
152
+ // latest and only pre-existing ones get bumped. Installing missing plugins is
153
+ // the install flags' job — this only upgrades what is present. Errors out if
154
+ // the network or the claude CLI is unavailable.
155
+ async upgrade() {
156
+ if (!hasBin("claude")) {
157
+ this.error("claude CLI not found. Install it first, then re-run.");
158
+ }
159
+ // Fail fast on the remote reads before mutating anything.
160
+ const latest = await remoteLatest();
161
+ const cliLatest = (await fetchJson(NPM_LATEST_URL)).version;
162
+ const installed = installedPlugins();
163
+ const ours = PLUGINS.filter(name => installed[name]);
164
+
165
+ this.log(
166
+ `\nUpgrading installed plugins (marketplace ${MARKETPLACE_NAME})…`,
167
+ );
168
+ this.runClaude(["plugin", "marketplace", "update", MARKETPLACE_NAME]);
169
+
170
+ if (!ours.length) {
171
+ this.log("No virajp-plugins plugins are installed — nothing to upgrade.");
172
+ }
173
+ else {
174
+ let updated = 0;
175
+ for (const name of ours) {
176
+ const have = installed[name];
177
+ const want = latest[name];
178
+ if (want && cmpVer(want, have) > 0) {
179
+ this.log(`\nUpdating ${name} (${have} → ${want})…`);
180
+ this.runClaude(["plugin", "update", `${name}@${MARKETPLACE_NAME}`]);
181
+ updated++;
182
+ }
183
+ else {
184
+ this.log(
185
+ `${name} is up to date (${have}${want ? "" : ", external"}).`,
186
+ );
187
+ }
188
+ }
189
+ if (updated) {
190
+ this.log(
191
+ "\nPlugin updates applied — restart Claude Code to load them.",
192
+ );
193
+ }
194
+ }
195
+
196
+ // Refresh the installed statusline script + config (no settings.json change).
197
+ if (existsSync(INSTALLED_SCRIPT)) {
198
+ this.log("\nRefreshing statusline…");
199
+ await this.installScript();
200
+ await this.seedUserConfig();
201
+ }
202
+
203
+ if (cmpVer(cliLatest, this.config.version) > 0) {
204
+ this.log(
205
+ `\nA newer CLI is available: ${this.config.version} → ${cliLatest}`,
206
+ );
207
+ this.log("Re-run with: pnpx @askviraj/ai-plugins@latest --upgrade");
102
208
  }
103
209
  }
104
210
 
@@ -302,9 +408,20 @@ Installer.examples = [
302
408
  "pnpx @askviraj/ai-plugins --plugins",
303
409
  "pnpx @askviraj/ai-plugins --plugin vwf --plugin dart-lsp",
304
410
  "pnpx @askviraj/ai-plugins --statusline --subagentstatusline --yes",
411
+ "pnpx @askviraj/ai-plugins --all --upgrade",
412
+ "pnpx @askviraj/ai-plugins --version",
305
413
  ];
306
414
 
307
415
  Installer.flags = {
416
+ version: Flags.boolean({
417
+ char: "v",
418
+ description:
419
+ "Show CLI, statusline, and plugin versions (installed vs latest)",
420
+ }),
421
+ upgrade: Flags.boolean({
422
+ description:
423
+ "After any install, upgrade installed plugins to latest + refresh the statusline + check for a CLI update. Combine with --all for an idempotent install+upgrade (safe in setup scripts)",
424
+ }),
308
425
  all: Flags.boolean({
309
426
  description:
310
427
  "Install everything: every marketplace plugin plus both statusline keys",
@@ -332,6 +449,101 @@ Installer.flags = {
332
449
  }),
333
450
  };
334
451
 
452
+ // Fetch and parse JSON over HTTP with a bounded timeout. Throws on any failure
453
+ // (network down, non-2xx, bad JSON) so callers can hard-error per the offline
454
+ // policy.
455
+ async function fetchJson(url) {
456
+ const ctrl = new AbortController();
457
+ const timer = setTimeout(() => ctrl.abort(), 5000);
458
+ try {
459
+ const res = await fetch(url, {
460
+ signal: ctrl.signal,
461
+ headers: { "user-agent": "askviraj-ai-plugins" },
462
+ });
463
+ if (!res.ok) {
464
+ throw new Error(`HTTP ${res.status} for ${url}`);
465
+ }
466
+ return await res.json();
467
+ }
468
+ finally {
469
+ clearTimeout(timer);
470
+ }
471
+ }
472
+
473
+ // Map of plugin name → latest version from the remote marketplace manifest
474
+ // (null for url-sourced entries like mempalace, which pin no version here).
475
+ async function remoteLatest() {
476
+ const mp = await fetchJson(REMOTE_MARKETPLACE_URL);
477
+ const out = {};
478
+ for (const p of mp.plugins || []) {
479
+ out[p.name] = p.version || null;
480
+ }
481
+ return out;
482
+ }
483
+
484
+ // Map of plugin name → installed version, parsed from `claude plugin list`,
485
+ // restricted to the virajp-plugins marketplace. Throws if claude fails.
486
+ function installedPlugins() {
487
+ const res = spawnSync("claude", ["plugin", "list"], { encoding: "utf8" });
488
+ if (res.error || res.status !== 0) {
489
+ throw new Error(
490
+ "`claude plugin list` failed — is the claude CLI installed?",
491
+ );
492
+ }
493
+ const out = {};
494
+ let current = null;
495
+ for (const line of (res.stdout || "").split("\n")) {
496
+ const name = line.match(/([A-Za-z0-9_-]+)@virajp-plugins\b/);
497
+ if (name) {
498
+ current = name[1];
499
+ continue;
500
+ }
501
+ if (current) {
502
+ const ver = line.match(/Version:\s*(\S+)/);
503
+ if (ver) {
504
+ out[current] = ver[1];
505
+ current = null;
506
+ }
507
+ }
508
+ }
509
+ return out;
510
+ }
511
+
512
+ // Numeric semver compare of dotted versions: 1 if a>b, -1 if a<b, 0 if equal.
513
+ function cmpVer(a, b) {
514
+ const pa = String(a).split(".").map(n => parseInt(n, 10) || 0);
515
+ const pb = String(b).split(".").map(n => parseInt(n, 10) || 0);
516
+ for (let i = 0; i < 3; i++) {
517
+ if ((pa[i] || 0) > (pb[i] || 0)) {
518
+ return 1;
519
+ }
520
+ if ((pa[i] || 0) < (pb[i] || 0)) {
521
+ return -1;
522
+ }
523
+ }
524
+ return 0;
525
+ }
526
+
527
+ // " → X (update available)" when latest beats current, else " (latest)".
528
+ function updateNote(current, latest) {
529
+ return latest && cmpVer(latest, current) > 0
530
+ ? ` → ${latest} (update available)`
531
+ : " (latest)";
532
+ }
533
+
534
+ // The version column for one plugin given its installed and latest versions.
535
+ function pluginVersionLine(installed, latest) {
536
+ if (!latest) {
537
+ return installed
538
+ ? `${installed} (external; not tracked here)`
539
+ : "not installed (external)";
540
+ }
541
+ if (!installed) {
542
+ return `not installed (latest ${latest})`;
543
+ }
544
+ return `${installed}${updateNote(installed, latest)}`;
545
+ }
546
+
335
547
  // True if `bin` is an executable found on PATH.
336
548
  function hasBin(bin) {
337
549
  return (process.env.PATH || "")
package/package.json CHANGED
@@ -7,6 +7,25 @@
7
7
  "@oclif/core": "latest"
8
8
  },
9
9
  "description": "CLI to install Viraj Patel's Claude Code toolkit: marketplace plugins (via the claude CLI) and the powerline statusline",
10
+ "keywords": [
11
+ "claude",
12
+ "claude-code",
13
+ "claude-code-plugin",
14
+ "claude-code-plugins",
15
+ "marketplace",
16
+ "statusline",
17
+ "powerline",
18
+ "subagent",
19
+ "cli",
20
+ "mcp",
21
+ "lsp",
22
+ "vwf",
23
+ "context7",
24
+ "mempalace",
25
+ "ai",
26
+ "anthropic",
27
+ "oclif"
28
+ ],
10
29
  "engines": {
11
30
  "node": ">=18"
12
31
  },
@@ -28,5 +47,5 @@
28
47
  "url": "github:virajp/ai-plugins"
29
48
  },
30
49
  "type": "commonjs",
31
- "version": "1.0.1"
50
+ "version": "1.1.0"
32
51
  }
package/readme.md CHANGED
@@ -52,6 +52,19 @@ Available plugin names: `vwf`, `mempalace`, `context7`, `typescript-lsp`,
52
52
  > The **statusline** also installs through this CLI — see
53
53
  > [Statusline](#statusline) below.
54
54
 
55
+ **Check versions, and keep everything up to date:**
56
+
57
+ ```sh
58
+ # CLI, statusline, and each plugin's installed vs latest version
59
+ pnpx @askviraj/ai-plugins --version
60
+
61
+ # upgrade installed plugins + refresh the statusline (runs after any install)
62
+ pnpx @askviraj/ai-plugins --upgrade
63
+
64
+ # idempotent install + upgrade — safe to drop in a setup script
65
+ pnpx @askviraj/ai-plugins --all --upgrade
66
+ ```
67
+
55
68
  ## Plugins
56
69
 
57
70
  ### vwf
@@ -161,3 +174,34 @@ Dart language server.
161
174
  ```sh
162
175
  pnpx @askviraj/ai-plugins --plugin dart-lsp
163
176
  ```
177
+
178
+ ## Credits & acknowledgements
179
+
180
+ This project is a thin layer over a lot of excellent work. It would not exist —
181
+ or would be far poorer — without these. Thank you to their authors and
182
+ maintainers. 🙏
183
+
184
+ - **[Claude Code](https://claude.ai/code)** by
185
+ [Anthropic](https://anthropic.com) — the host these plugins, hooks, and
186
+ statusline plug into.
187
+ - **[MemPalace](https://github.com/MemPalace/mempalace)** — the AI memory system
188
+ that powers `vwf`'s cross-session recall (re-listed here as a dependency).
189
+ - **[Context7](https://github.com/upstash/context7)** by
190
+ [Upstash](https://upstash.com) — the MCP docs server behind the `context7`
191
+ plugin.
192
+ - **[mise](https://mise.jdx.dev/)** by Jeff Dickey — resolves the toolchain the
193
+ LSP plugins and hooks depend on.
194
+ - **[pnpm](https://pnpm.io/)** — the package manager the `npm→pnpm` hook and
195
+ `context7` rely on.
196
+ - **[typescript-language-server](https://github.com/typescript-language-server/typescript-language-server)**
197
+ and the **[Dart SDK](https://dart.dev/)** language server — the engines behind
198
+ the LSP plugins.
199
+ - **[rtk](https://github.com/rtk-ai/rtk) (Rust Token Killer)** — the
200
+ token-saving proxy `vwf`'s Bash hook shells out to (installed via
201
+ `brew install --formulae rtk`).
202
+ - **[graphify](https://github.com/safishamsi/graphify)** — the knowledge-graph
203
+ tool `vwf` integrates with.
204
+ - **[oclif](https://oclif.io/)** — the framework this installer CLI is built on.
205
+ - **[Nerd Fonts](https://www.nerdfonts.com/)** — the glyphs that make the
206
+ statusline render, and the **[Gruvbox](https://github.com/morhetz/gruvbox)**
207
+ palette it ships by default.