@peppermint-mcp/wizard 0.3.1 → 0.4.1
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/dist/cli.js +131 -32
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -197,6 +197,12 @@ function saveCredentials(creds) {
|
|
|
197
197
|
mode: 384
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
|
+
function clearCredentials() {
|
|
201
|
+
const path = getCredentialsPath();
|
|
202
|
+
if (existsSync3(path)) {
|
|
203
|
+
writeFileSync(path, "{}", { encoding: "utf-8", mode: 384 });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
200
206
|
|
|
201
207
|
// src/auth/localhost-oauth.ts
|
|
202
208
|
import { createServer } from "http";
|
|
@@ -461,6 +467,9 @@ Would create: ${filePath}`;
|
|
|
461
467
|
`Config merge produced invalid JSON: ${errors.map((e) => jsonc3.printParseErrorCode(e.error)).join(", ")}`
|
|
462
468
|
);
|
|
463
469
|
}
|
|
470
|
+
if (content === updated) {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
464
473
|
if (dryRun) {
|
|
465
474
|
return `Would write to ${filePath}:
|
|
466
475
|
${updated}`;
|
|
@@ -524,6 +533,9 @@ async function installClaudeDesktop(serverUrl, apiKey, dryRun) {
|
|
|
524
533
|
serverConfig,
|
|
525
534
|
dryRun
|
|
526
535
|
});
|
|
536
|
+
if (result === null) {
|
|
537
|
+
return { success: true, message: "Already up to date", needsRestart: false };
|
|
538
|
+
}
|
|
527
539
|
return {
|
|
528
540
|
success: true,
|
|
529
541
|
message: dryRun ? result : `Wrote config to ${configPath}`,
|
|
@@ -572,6 +584,9 @@ async function installCursor(serverUrl, apiKey, dryRun) {
|
|
|
572
584
|
serverConfig,
|
|
573
585
|
dryRun
|
|
574
586
|
});
|
|
587
|
+
if (result === null) {
|
|
588
|
+
return { success: true, message: "Already up to date", needsRestart: false };
|
|
589
|
+
}
|
|
575
590
|
return {
|
|
576
591
|
success: true,
|
|
577
592
|
message: dryRun ? result : `Wrote config to ${configPath}`,
|
|
@@ -736,13 +751,17 @@ function checkHostConfig(hostId, configPath) {
|
|
|
736
751
|
}
|
|
737
752
|
|
|
738
753
|
// src/skills/index.ts
|
|
754
|
+
import { createHash as createHash2 } from "crypto";
|
|
739
755
|
import {
|
|
740
756
|
copyFileSync as copyFileSync2,
|
|
741
757
|
existsSync as existsSync7,
|
|
742
758
|
mkdirSync as mkdirSync4,
|
|
759
|
+
readFileSync as readFileSync7,
|
|
743
760
|
readdirSync,
|
|
761
|
+
renameSync as renameSync2,
|
|
744
762
|
rmSync,
|
|
745
|
-
statSync
|
|
763
|
+
statSync,
|
|
764
|
+
writeFileSync as writeFileSync4
|
|
746
765
|
} from "fs";
|
|
747
766
|
import { homedir as homedir7 } from "os";
|
|
748
767
|
import { dirname as dirname3, join as join7, resolve } from "path";
|
|
@@ -753,12 +772,26 @@ var LEGACY_SKILLS = [
|
|
|
753
772
|
"peppermint-capture",
|
|
754
773
|
"peppermint-ask-twin"
|
|
755
774
|
];
|
|
775
|
+
var HASH_FILE = ".wizard-hash";
|
|
756
776
|
function getSkillsBundlePath() {
|
|
757
777
|
return resolve(__dirname, "..", "skills-bundle", "peppermint");
|
|
758
778
|
}
|
|
759
779
|
function getTargetPath() {
|
|
760
780
|
return join7(homedir7(), ".claude", "skills", "peppermint");
|
|
761
781
|
}
|
|
782
|
+
function hashDir(dir) {
|
|
783
|
+
const hash = createHash2("sha256");
|
|
784
|
+
const entries = readdirSync(dir).sort();
|
|
785
|
+
for (const entry of entries) {
|
|
786
|
+
const fullPath = join7(dir, entry);
|
|
787
|
+
if (statSync(fullPath).isDirectory()) {
|
|
788
|
+
hash.update(hashDir(fullPath));
|
|
789
|
+
} else {
|
|
790
|
+
hash.update(readFileSync7(fullPath));
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return hash.digest("hex");
|
|
794
|
+
}
|
|
762
795
|
function copyDirRecursive(src, dest) {
|
|
763
796
|
mkdirSync4(dest, { recursive: true });
|
|
764
797
|
for (const entry of readdirSync(src)) {
|
|
@@ -788,27 +821,41 @@ function removeLegacySkills(dryRun) {
|
|
|
788
821
|
function installSkills(dryRun) {
|
|
789
822
|
const bundlePath = getSkillsBundlePath();
|
|
790
823
|
const targetPath = getTargetPath();
|
|
824
|
+
const hashFile = join7(targetPath, HASH_FILE);
|
|
791
825
|
const alreadyExists = existsSync7(join7(targetPath, "SKILL.md"));
|
|
792
826
|
if (!existsSync7(bundlePath)) {
|
|
793
|
-
return { installed: false, updated: false, targetPath, error: "Skills bundle not found in package" };
|
|
827
|
+
return { installed: false, updated: false, skipped: false, backedUp: false, targetPath, error: "Skills bundle not found in package" };
|
|
828
|
+
}
|
|
829
|
+
const bundleHash = hashDir(bundlePath);
|
|
830
|
+
if (alreadyExists && existsSync7(hashFile)) {
|
|
831
|
+
const installedHash = readFileSync7(hashFile, "utf-8").trim();
|
|
832
|
+
if (installedHash === bundleHash) {
|
|
833
|
+
return { installed: true, updated: false, skipped: true, backedUp: false, targetPath };
|
|
834
|
+
}
|
|
794
835
|
}
|
|
795
836
|
if (dryRun) {
|
|
796
|
-
return { installed: true, updated: alreadyExists, targetPath };
|
|
837
|
+
return { installed: true, updated: alreadyExists, skipped: false, backedUp: false, targetPath };
|
|
838
|
+
}
|
|
839
|
+
let backedUp = false;
|
|
840
|
+
if (alreadyExists && !existsSync7(hashFile)) {
|
|
841
|
+
const backupPath = `${targetPath}.bak.${Date.now()}`;
|
|
842
|
+
renameSync2(targetPath, backupPath);
|
|
843
|
+
backedUp = true;
|
|
844
|
+
} else if (alreadyExists) {
|
|
845
|
+
rmSync(targetPath, { recursive: true, force: true });
|
|
797
846
|
}
|
|
798
847
|
try {
|
|
799
|
-
if (existsSync7(targetPath)) {
|
|
800
|
-
rmSync(targetPath, { recursive: true, force: true });
|
|
801
|
-
}
|
|
802
848
|
copyDirRecursive(bundlePath, targetPath);
|
|
803
|
-
|
|
849
|
+
writeFileSync4(join7(targetPath, HASH_FILE), bundleHash, "utf-8");
|
|
850
|
+
return { installed: true, updated: alreadyExists, skipped: false, backedUp, targetPath };
|
|
804
851
|
} catch (err) {
|
|
805
852
|
const message = err instanceof Error ? err.message : "Failed to install skills";
|
|
806
|
-
return { installed: false, updated: false, targetPath, error: message };
|
|
853
|
+
return { installed: false, updated: false, skipped: false, backedUp: false, targetPath, error: message };
|
|
807
854
|
}
|
|
808
855
|
}
|
|
809
856
|
|
|
810
857
|
// src/skills/permissions.ts
|
|
811
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as
|
|
858
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
812
859
|
import { homedir as homedir8 } from "os";
|
|
813
860
|
import { dirname as dirname4, join as join8 } from "path";
|
|
814
861
|
import * as jsonc5 from "jsonc-parser";
|
|
@@ -830,7 +877,7 @@ function installPermissions(dryRun) {
|
|
|
830
877
|
try {
|
|
831
878
|
let content = "";
|
|
832
879
|
if (existsSync8(settingsPath)) {
|
|
833
|
-
content =
|
|
880
|
+
content = readFileSync8(settingsPath, "utf-8");
|
|
834
881
|
}
|
|
835
882
|
const parsed = content ? jsonc5.parse(content) : {};
|
|
836
883
|
const existingAllow = parsed?.permissions?.allow || [];
|
|
@@ -850,7 +897,7 @@ function installPermissions(dryRun) {
|
|
|
850
897
|
if (!existsSync8(dir)) {
|
|
851
898
|
mkdirSync5(dir, { recursive: true });
|
|
852
899
|
}
|
|
853
|
-
|
|
900
|
+
writeFileSync5(settingsPath, updated, "utf-8");
|
|
854
901
|
return { added: toAdd };
|
|
855
902
|
} catch (err) {
|
|
856
903
|
const message = err instanceof Error ? err.message : "Failed to update permissions";
|
|
@@ -1009,9 +1056,12 @@ async function addCommand(options) {
|
|
|
1009
1056
|
p.log.info(` ${pc.green("\u2713")} Removed legacy skills: ${pc.dim(removed.join(", "))}`);
|
|
1010
1057
|
}
|
|
1011
1058
|
const skillResult = installSkills(options.dryRun);
|
|
1012
|
-
if (skillResult.
|
|
1059
|
+
if (skillResult.skipped) {
|
|
1060
|
+
p.log.info(` ${pc.green("\u2713")} Peppermint skill ${pc.dim("up to date")}`);
|
|
1061
|
+
} else if (skillResult.installed) {
|
|
1013
1062
|
const verb = skillResult.updated ? "Updated" : "Installed";
|
|
1014
|
-
|
|
1063
|
+
const backup = skillResult.backedUp ? pc.dim(" (previous version backed up)") : "";
|
|
1064
|
+
p.log.info(` ${pc.green("\u2713")} ${verb} Peppermint skill${backup} ${pc.dim(skillResult.targetPath)}`);
|
|
1015
1065
|
} else if (skillResult.error) {
|
|
1016
1066
|
p.log.info(` ${pc.red("\u2717")} Skill install failed: ${pc.dim(skillResult.error)}`);
|
|
1017
1067
|
}
|
|
@@ -1073,34 +1123,83 @@ async function doctorCommand(options) {
|
|
|
1073
1123
|
p.outro("Health check complete");
|
|
1074
1124
|
}
|
|
1075
1125
|
async function removeCommand(options) {
|
|
1076
|
-
|
|
1126
|
+
const nonInteractive = options.yes || !process.stdin.isTTY;
|
|
1127
|
+
if (!process.stdin.isTTY && !nonInteractive) {
|
|
1128
|
+
console.error("Error: peppermint-mcp-wizard requires an interactive terminal.");
|
|
1129
|
+
console.error("For non-interactive removes, use: --yes");
|
|
1130
|
+
process.exit(1);
|
|
1131
|
+
}
|
|
1132
|
+
if (!nonInteractive) {
|
|
1133
|
+
p.intro(pc.green("\u{1F33F} Peppermint MCP Wizard \u2014 Remove"));
|
|
1134
|
+
}
|
|
1077
1135
|
const hosts = await detectHosts();
|
|
1078
1136
|
const installed = hosts.filter((h) => h.alreadyInstalled);
|
|
1079
1137
|
if (installed.length === 0) {
|
|
1080
|
-
|
|
1138
|
+
const msg = "Peppermint is not installed in any detected hosts.";
|
|
1139
|
+
nonInteractive ? console.log(msg) : p.log.info(msg);
|
|
1081
1140
|
process.exit(0);
|
|
1082
1141
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1142
|
+
let selectedHosts;
|
|
1143
|
+
if (nonInteractive) {
|
|
1144
|
+
selectedHosts = installed;
|
|
1145
|
+
} else {
|
|
1146
|
+
const selected = await p.multiselect({
|
|
1147
|
+
message: "Remove Peppermint MCP from which hosts?",
|
|
1148
|
+
options: installed.map((h) => ({
|
|
1149
|
+
value: h.id,
|
|
1150
|
+
label: h.name
|
|
1151
|
+
})),
|
|
1152
|
+
initialValues: installed.map((h) => h.id)
|
|
1153
|
+
});
|
|
1154
|
+
if (p.isCancel(selected)) {
|
|
1155
|
+
p.cancel("Cancelled.");
|
|
1156
|
+
process.exit(0);
|
|
1157
|
+
}
|
|
1158
|
+
selectedHosts = hosts.filter(
|
|
1159
|
+
(h) => selected.includes(h.id)
|
|
1160
|
+
);
|
|
1094
1161
|
}
|
|
1095
|
-
const selectedHosts = hosts.filter(
|
|
1096
|
-
(h) => selected.includes(h.id)
|
|
1097
|
-
);
|
|
1098
1162
|
for (const host of selectedHosts) {
|
|
1099
1163
|
const result = await removeHost(host, options.dryRun);
|
|
1100
1164
|
const icon = result.success ? pc.green("\u2713") : pc.red("\u2717");
|
|
1101
|
-
p.log.info(`${icon} ${host.name} ${pc.dim(result.message)}`);
|
|
1165
|
+
nonInteractive ? console.log(`${result.success ? "\u2713" : "\u2717"} ${host.name} ${result.message}`) : p.log.info(`${icon} ${host.name} ${pc.dim(result.message)}`);
|
|
1166
|
+
}
|
|
1167
|
+
if (options.revokeToken) {
|
|
1168
|
+
const base = serverBase(options.server);
|
|
1169
|
+
const creds = loadCredentials(base);
|
|
1170
|
+
if (creds && !options.dryRun) {
|
|
1171
|
+
try {
|
|
1172
|
+
const keyPrefix = creds.api_key.slice(0, 8);
|
|
1173
|
+
const res = await fetch(`${base}/auth/api-keys`, {
|
|
1174
|
+
headers: { Authorization: `Bearer ${creds.api_key}` }
|
|
1175
|
+
});
|
|
1176
|
+
if (res.ok) {
|
|
1177
|
+
const keys = await res.json();
|
|
1178
|
+
for (const key of keys) {
|
|
1179
|
+
if (key.key_prefix === keyPrefix) {
|
|
1180
|
+
await fetch(`${base}/auth/api-keys/${key.id}`, {
|
|
1181
|
+
method: "DELETE",
|
|
1182
|
+
headers: { Authorization: `Bearer ${creds.api_key}` }
|
|
1183
|
+
});
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
clearCredentials();
|
|
1188
|
+
const msg = "Revoked API key and cleared credentials";
|
|
1189
|
+
nonInteractive ? console.log(`\u2713 ${msg}`) : p.log.info(`${pc.green("\u2713")} ${msg}`);
|
|
1190
|
+
} catch {
|
|
1191
|
+
const msg = "Failed to revoke API key (credentials cleared locally)";
|
|
1192
|
+
clearCredentials();
|
|
1193
|
+
nonInteractive ? console.log(`\u26A0 ${msg}`) : p.log.info(`${pc.yellow("\u26A0")} ${msg}`);
|
|
1194
|
+
}
|
|
1195
|
+
} else if (creds && options.dryRun) {
|
|
1196
|
+
const msg = "Would revoke API key and clear credentials";
|
|
1197
|
+
nonInteractive ? console.log(msg) : p.log.info(msg);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
if (!nonInteractive) {
|
|
1201
|
+
p.outro("Removal complete");
|
|
1102
1202
|
}
|
|
1103
|
-
p.outro("Removal complete");
|
|
1104
1203
|
}
|
|
1105
1204
|
var program = new Command().name("peppermint-mcp-wizard").description("One-command installer for Peppermint MCP").version("0.1.0");
|
|
1106
1205
|
program.command("add", { isDefault: true }).description("Detect hosts, authenticate, install MCP config").option("--server <url>", "MCP server URL", DEFAULT_SERVER).option("--dry-run", "Print changes without writing", false).option("--no-verify", "Skip post-install verification").option("--host <id...>", "Install to specific hosts (claude-code, claude-desktop, cursor, codex)").option("--yes", "Skip all prompts (non-interactive)", false).option("--auth-token <token>", "API key or token for auth (skips browser OAuth)").action((opts) => addCommand({
|
|
@@ -1113,5 +1212,5 @@ program.command("add", { isDefault: true }).description("Detect hosts, authentic
|
|
|
1113
1212
|
}));
|
|
1114
1213
|
program.command("list").description("List detected AI hosts and their Peppermint status").option("--server <url>", "MCP server URL", DEFAULT_SERVER).action((opts) => listCommand({ server: opts.server }));
|
|
1115
1214
|
program.command("doctor").description("Run health checks on existing installation").option("--server <url>", "MCP server URL", DEFAULT_SERVER).action((opts) => doctorCommand({ server: opts.server }));
|
|
1116
|
-
program.command("remove").description("Remove Peppermint MCP from selected hosts").option("--server <url>", "MCP server URL", DEFAULT_SERVER).option("--dry-run", "Print changes without writing", false).action((opts) => removeCommand({ server: opts.server, dryRun: opts.dryRun }));
|
|
1215
|
+
program.command("remove").description("Remove Peppermint MCP from selected hosts").option("--server <url>", "MCP server URL", DEFAULT_SERVER).option("--dry-run", "Print changes without writing", false).option("--yes", "Skip all prompts (non-interactive)", false).option("--revoke-token", "Revoke API key on the server", true).option("--no-revoke-token", "Keep API key for future re-installs").action((opts) => removeCommand({ server: opts.server, dryRun: opts.dryRun, yes: opts.yes, revokeToken: opts.revokeToken }));
|
|
1117
1216
|
program.parse();
|