@ztffn/presentation-generator-plugin 1.4.0 → 1.4.2

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 (2) hide show
  1. package/bin/index.js +110 -19
  2. package/package.json +1 -1
package/bin/index.js CHANGED
@@ -5,7 +5,6 @@ const { execSync } = require("child_process");
5
5
  const fs = require("fs");
6
6
  const path = require("path");
7
7
  const os = require("os");
8
- const readline = require("readline");
9
8
 
10
9
  const NPM_PACKAGE = "@ztffn/presentation-generator-plugin";
11
10
  const PLUGIN_NAME = "presentation-generator";
@@ -104,23 +103,78 @@ async function downloadAndExtract(version, tarballUrl, installDir) {
104
103
 
105
104
  function promptChoice(title, choices) {
106
105
  return new Promise((resolve) => {
107
- console.log(`\n◆ ${title}`);
108
- choices.forEach((c, i) => {
109
- const bullet = i === 0 ? "●" : "○";
110
- console.log(`│ ${bullet} ${i + 1}) ${c.label}`);
111
- if (c.hint) console.log(`│ ${c.hint}`);
112
- });
113
- console.log("");
106
+ let selected = 0;
107
+
108
+ // Lines rendered per call to draw() used to reposition cursor on redraw
109
+ const lineCount = () => {
110
+ let n = 2; // blank line + title line
111
+ choices.forEach((c) => { n += c.hint ? 2 : 1; });
112
+ n += 1; // trailing
113
+ n += 1; // └ line (no trailing newline — cursor stays here)
114
+ return n;
115
+ };
114
116
 
115
- const rl = readline.createInterface({
116
- input: process.stdin,
117
- output: process.stdout,
118
- });
119
- rl.question(`└ Enter choice [1-${choices.length}]: `, (answer) => {
120
- rl.close();
121
- const idx = parseInt(answer.trim(), 10) - 1;
122
- resolve(idx >= 0 && idx < choices.length ? idx : 0);
123
- });
117
+ const C = {
118
+ cyan: (s) => `\x1B[36m${s}\x1B[0m`,
119
+ dim: (s) => `\x1B[2m${s}\x1B[0m`,
120
+ up: (n) => `\x1B[${n}A`,
121
+ clear: () => "\x1B[0J",
122
+ cursorHide: "\x1B[?25l",
123
+ cursorShow: "\x1B[?25h",
124
+ };
125
+
126
+ const draw = (first = false) => {
127
+ if (!first) {
128
+ // \r resets to col 0 before moving up, so C.clear() wipes from the true start of the line
129
+ process.stdout.write("\r" + C.up(lineCount() - 1) + C.clear());
130
+ }
131
+ process.stdout.write(`\n◆ ${title}\n`);
132
+ choices.forEach((c, i) => {
133
+ const active = i === selected;
134
+ const bullet = active ? C.cyan("●") : "○";
135
+ const label = active ? C.cyan(c.label) : c.label;
136
+ process.stdout.write(`│ ${bullet} ${label}\n`);
137
+ if (c.hint) process.stdout.write(`│ ${C.dim(c.hint)}\n`);
138
+ });
139
+ process.stdout.write("│\n");
140
+ process.stdout.write(C.dim("└ ↑↓ to move · Enter to confirm"));
141
+ };
142
+
143
+ process.stdout.write(C.cursorHide);
144
+ draw(true);
145
+
146
+ process.stdin.setRawMode(true);
147
+ process.stdin.resume();
148
+ process.stdin.setEncoding("utf8");
149
+
150
+ const onKey = (key) => {
151
+ if (key === "\x1B[A") { // ↑
152
+ selected = (selected - 1 + choices.length) % choices.length;
153
+ draw();
154
+ } else if (key === "\x1B[B") { // ↓
155
+ selected = (selected + 1) % choices.length;
156
+ draw();
157
+ } else if (key === "\r" || key === " ") { // Enter or Space
158
+ process.stdin.setRawMode(false);
159
+ process.stdin.pause();
160
+ process.stdin.removeListener("data", onKey);
161
+ process.stdout.write(C.cursorShow);
162
+
163
+ // Redraw with confirmed selection, then leave a clean line
164
+ process.stdout.write(C.up(lineCount() - 1) + C.clear());
165
+ process.stdout.write(`\n◆ ${title}\n`);
166
+ process.stdout.write(`│ ${C.cyan("●")} ${C.cyan(choices[selected].label)}\n`);
167
+ process.stdout.write("└\n\n");
168
+
169
+ resolve(selected);
170
+ } else if (key === "\x03") { // Ctrl-C
171
+ process.stdin.setRawMode(false);
172
+ process.stdout.write(C.cursorShow + "\n");
173
+ process.exit(0);
174
+ }
175
+ };
176
+
177
+ process.stdin.on("data", onKey);
124
178
  });
125
179
  }
126
180
 
@@ -190,16 +244,52 @@ function cleanOldCacheVersions(keepVersion) {
190
244
  }
191
245
  }
192
246
 
247
+ function purgeInstalledPluginsJson() {
248
+ // Directly remove all entries for this plugin from Claude's global registry
249
+ // so that a subsequent `claude plugin install` always writes a fresh entry.
250
+ const registryPath = path.join(
251
+ os.homedir(), ".claude", "plugins", "installed_plugins.json"
252
+ );
253
+ if (!fs.existsSync(registryPath)) return;
254
+ try {
255
+ const registry = JSON.parse(fs.readFileSync(registryPath, "utf8"));
256
+ const key = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
257
+ if (registry.plugins && key in registry.plugins) {
258
+ delete registry.plugins[key];
259
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2) + "\n");
260
+ }
261
+ } catch {
262
+ // registry unreadable/corrupt — leave it alone
263
+ }
264
+ }
265
+
193
266
  function registerWithClaude(installDir, scope) {
194
267
  const pluginsDir = path.dirname(installDir);
195
268
  ensureMarketplaceJson(pluginsDir);
196
269
 
270
+ // Remove any stale registration (e.g. from a previous install at a different path)
271
+ // before adding the current one. Errors are ignored — it may not exist yet.
197
272
  try {
198
- execSync(`claude plugin marketplace add "${pluginsDir}"`, { stdio: "pipe" });
273
+ execSync(`claude plugin marketplace remove "${MARKETPLACE_NAME}"`, { stdio: "pipe" });
199
274
  } catch {
200
- // marketplace may already be registeredcontinue
275
+ // didn't exist or not removableignore
201
276
  }
202
277
 
278
+ // Uninstall any stale plugin entries from both scopes before re-installing.
279
+ // This prevents Claude from reusing a cached entry pointing at an old version path.
280
+ try {
281
+ execSync(`claude plugin uninstall ${PLUGIN_NAME}@${MARKETPLACE_NAME} --scope project`, { stdio: "pipe" });
282
+ } catch {}
283
+ try {
284
+ execSync(`claude plugin uninstall ${PLUGIN_NAME}@${MARKETPLACE_NAME} --scope user`, { stdio: "pipe" });
285
+ } catch {}
286
+
287
+ // Belt-and-suspenders: also purge the registry JSON directly in case
288
+ // `claude plugin uninstall` doesn't clean installed_plugins.json reliably.
289
+ purgeInstalledPluginsJson();
290
+
291
+ execSync(`claude plugin marketplace add "${pluginsDir}"`, { stdio: "pipe" });
292
+
203
293
  const scopeFlag = scope === "project" ? "--scope project" : "--scope user";
204
294
  execSync(`claude plugin install ${PLUGIN_NAME}@${MARKETPLACE_NAME} ${scopeFlag}`, { stdio: "pipe" });
205
295
  }
@@ -263,6 +353,7 @@ async function install() {
263
353
 
264
354
  console.log("\nRegistering plugin with Claude Code...");
265
355
  registerWithClaude(installDir, scope);
356
+ cleanOldCacheVersions(meta.version);
266
357
 
267
358
  const settingsLabel = scope === "project" ? ".claude/settings.json" : "~/.claude/settings.json";
268
359
  console.log(`Plugin registered in ${settingsLabel} ✓`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ztffn/presentation-generator-plugin",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Claude Code plugin for generating graph-based presentations",
5
5
  "bin": {
6
6
  "presentation-generator-plugin": "bin/index.js"