@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 +228 -12
- package/package.json +20 -1
- package/readme.md +44 -0
- package/tools/statusline/statusline +16 -0
- package/tools/statusline/statusline.json +1 -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 (!
|
|
103
|
+
if (!hasInstall && !flags.upgrade) {
|
|
89
104
|
this.error(
|
|
90
|
-
"Nothing to do. Pass --all, --plugins, --plugin <name>, --statusline,
|
|
105
|
+
"Nothing to do. Pass --all, --plugins, --plugin <name>, --statusline, --subagentstatusline, --upgrade, or --version.",
|
|
91
106
|
);
|
|
92
107
|
}
|
|
93
108
|
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
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.
|
|
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
|
|