@askviraj/ai-plugins 1.0.0 → 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.
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
 
@@ -290,17 +396,32 @@ class Installer extends Command {
290
396
  }
291
397
  }
292
398
 
293
- Installer.description =
399
+ // Use `summary` (not `description`) so oclif prints this once at the top of
400
+ // --help; setting `description` would also render a duplicate DESCRIPTION block.
401
+ Installer.summary =
294
402
  "Install Viraj Patel's Claude Code toolkit: marketplace plugins (via the `claude` CLI) and the powerline statusline. Checks required tools (brew/mise/claude/rtk/pnpm/…) first and prints install hints for any that are missing.";
295
403
 
404
+ // Users invoke this via pnpx (npx works too), never the bare `ai-plugins` bin,
405
+ // so spell the runnable command out rather than using <%= config.bin %>.
296
406
  Installer.examples = [
297
- "<%= config.bin %> --all",
298
- "<%= config.bin %> --plugins",
299
- "<%= config.bin %> --plugin vwf --plugin dart-lsp",
300
- "<%= config.bin %> --statusline --subagentstatusline --yes",
407
+ "pnpx @askviraj/ai-plugins --all",
408
+ "pnpx @askviraj/ai-plugins --plugins",
409
+ "pnpx @askviraj/ai-plugins --plugin vwf --plugin dart-lsp",
410
+ "pnpx @askviraj/ai-plugins --statusline --subagentstatusline --yes",
411
+ "pnpx @askviraj/ai-plugins --all --upgrade",
412
+ "pnpx @askviraj/ai-plugins --version",
301
413
  ];
302
414
 
303
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
+ }),
304
425
  all: Flags.boolean({
305
426
  description:
306
427
  "Install everything: every marketplace plugin plus both statusline keys",
@@ -328,6 +449,101 @@ Installer.flags = {
328
449
  }),
329
450
  };
330
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
+
331
547
  // True if `bin` is an executable found on PATH.
332
548
  function hasBin(bin) {
333
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.0"
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.
@@ -300,6 +300,20 @@ function gitDirtyMark(cwd) {
300
300
  return "";
301
301
  }
302
302
 
303
+ // Up-arrow marker when the branch is ahead of its upstream (local commits not
304
+ // pushed). Empty when in sync, when there is no upstream, or on any git error.
305
+ // One bounded git call (250ms).
306
+ function gitAheadMark(cwd) {
307
+ const { execFileSync } = require("child_process");
308
+ const opts = { cwd, encoding: "utf8", timeout: 250, stdio: ["ignore", "pipe", "ignore"] };
309
+ try {
310
+ const out = execFileSync("git", ["rev-list", "--count", "@{upstream}..HEAD"], opts).trim();
311
+ return parseInt(out, 10) > 0 ? SYM.ahead : "";
312
+ } catch (_) {
313
+ return "";
314
+ }
315
+ }
316
+
303
317
  // ─────────────────────────────────────────────────────────────────────────────
304
318
  // Segment registry — each builder takes the resolved context `c` and returns the
305
319
  // segment TEXT (a string), or null to omit (when its data is absent). All styling
@@ -323,6 +337,7 @@ const SEGMENTS = {
323
337
  branch: (c) => {
324
338
  if (!c.branch) return null;
325
339
  let t = c.wt ? `${SYM.worktree} ${SYM.branch} ${c.branch}` : `${SYM.branch} ${c.branch}`;
340
+ if (c.ahead) t += ` ${c.ahead}`;
326
341
  if (c.dirty) t += ` ${c.dirty}`;
327
342
  return t;
328
343
  },
@@ -384,6 +399,7 @@ function renderMain(d) {
384
399
  projectSym: SYM.project,
385
400
  wt: worktreeSubpath(cwd),
386
401
  branch: git.branch,
402
+ ahead: git.branch ? gitAheadMark(cwd) : "",
387
403
  dirty: git.branch ? gitDirtyMark(cwd) : "",
388
404
  };
389
405
 
@@ -166,6 +166,7 @@
166
166
  },
167
167
  "symbols": {
168
168
  "agent": "",
169
+ "ahead": "↑",
169
170
  "branch": "",
170
171
  "context": "",
171
172
  "cost": "",