marvelous-ui-library 1.0.2 → 1.0.4

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/README.md CHANGED
@@ -7,7 +7,9 @@ npx marvelous-ui-library@latest init
7
7
  npx marvelous-ui-library@latest add dialog tabs toast
8
8
  ```
9
9
 
10
- `init` detects the project and supported agent configuration. In an interactive terminal it asks whether to install the Pack in this project or in your user profile. Enter a Polar license key in the hidden prompt for Pro, or press Enter for Free. User setup then asks which agents to connect and registers Codex and Claude Code directly. For automation, set `MARVELOUS_LICENSE_KEY` for Pro and use `--scope user --clients codex,claude` to choose agents; `--clients none` installs only the shared Pack. Rerunning user setup removes a previously selected plugin when you deselect that agent. A noninteractive invocation without a key defaults to Free in the current project. `--edition free|pro` remains available for existing scripts. Other agents can use project setup or the manual instructions in the Pack's `docs/MCP.md`.
10
+ `init` detects the project and supported agent configuration. In an interactive terminal it asks whether to install the Pack in this project or in your user profile. Enter a Polar license key in the hidden prompt for Pro, or press Enter for Free. A noninteractive invocation without a key defaults to Free in the current project. `--edition free|pro` remains available for existing scripts.
11
+
12
+ User setup detects Codex, Claude Code, Cursor and OpenCode where available. Choose several by entering comma-separated numbers or names; `all` selects every detected supported agent, and `none` installs only the shared Pack. A later interactive run remembers your choice. For automation, set `MARVELOUS_LICENSE_KEY` for Pro and use `--scope user --clients codex,cursor,opencode`, `--clients all`, or `--clients none`. The first noninteractive user setup connects no agent unless `--clients` is supplied. Codex and Claude Code require their command-line tools for automatic registration. Cursor receives a local plugin copy; OpenCode receives a user skill and MCP entry. Rerunning user setup disconnects agents you deselect. Restart an open agent session after setup, or reload Cursor. Other agents can use project setup or the manual instructions in the Pack's `docs/MCP.md`.
11
13
 
12
14
  The Pro Pack requires a Polar license key for the first download and for newer releases. A supplied invalid key causes an error. The key is not written to a project file. A project Pack remains at its installed version until you run:
13
15
 
@@ -25,4 +27,14 @@ npx marvelous-ui-library@latest restore --version 1.0.0
25
27
 
26
28
  Run `restore` without `--version` in an interactive terminal to choose from the saved releases. Use `--scope project|user` to choose the target. Restoration compares managed component hashes and preserves customized files. A saved release remains usable after the key expires; fetching a release that was never saved still requires an active key.
27
29
 
30
+ To remove an installation while keeping your downloaded Pro ZIPs:
31
+
32
+ ```bash
33
+ npx marvelous-ui-library@latest uninstall --scope user
34
+ # Or, from the project you initialized:
35
+ npx marvelous-ui-library@latest uninstall --scope project
36
+ ```
37
+
38
+ User uninstall disconnects the agents registered by the CLI and removes its user Pack. Project uninstall removes that project's Pack. Both leave copied project components and existing project agent configuration files for you to review. Existing agent entries that the CLI does not manage are left alone. Pro ZIPs remain under `~/.marvelous-ui/archives`; add `--purge-archives` to `uninstall --scope user` only when you also want to delete those saved ZIPs. An expired key cannot download them again.
39
+
28
40
  The Free Pack is MIT licensed. The Pro Pack is subject to the commercial license shipped in its `LICENSE.md`.
package/bin.mjs CHANGED
@@ -4,8 +4,8 @@ import { existsSync } from "node:fs";
4
4
  import { join, resolve } from "node:path";
5
5
  import { createInterface } from "node:readline/promises";
6
6
  import { Writable } from "node:stream";
7
- import { locations, readState, latestRelease, downloadRelease, installPack, addComponents, detectFramework, listProArchives, readProArchive } from "./lib/installer.mjs";
8
- import { checkUserAgents, configureUserAgents, parseUserAgents, readUserAgents } from "./lib/agents.mjs";
7
+ import { locations, readState, latestRelease, downloadRelease, installPack, uninstallPack, addComponents, detectFramework, listProArchives, readProArchive } from "./lib/installer.mjs";
8
+ import { SUPPORTED, LABELS, checkUserAgents, configureUserAgents, detectUserAgents, disconnectUserAgents, parseUserAgents, readUserAgents } from "./lib/agents.mjs";
9
9
 
10
10
  const HELP = `Marvelous UI
11
11
 
@@ -13,8 +13,9 @@ const HELP = `Marvelous UI
13
13
  npx marvelous-ui-library@latest add <component...> [--scope project|user] [--out path]
14
14
  npx marvelous-ui-library@latest update [--scope project|user]
15
15
  npx marvelous-ui-library@latest restore [--scope project|user] [--version 1.1.0]
16
+ npx marvelous-ui-library@latest uninstall [--scope project|user] [--purge-archives]
16
17
 
17
- Options: --project path, --clients names. User setup supports codex,claude,none. At init, press Enter for Free or enter a Polar key for Pro. In automation, set MARVELOUS_LICENSE_KEY for Pro.
18
+ Options: --project path, --clients codex,claude,cursor,opencode|all|none. At init, press Enter for Free or enter a Polar key for Pro. In automation, set MARVELOUS_LICENSE_KEY for Pro. Uninstall keeps saved Pro ZIPs unless --purge-archives is passed.
18
19
  `;
19
20
 
20
21
  function options(args) {
@@ -23,6 +24,7 @@ function options(args) {
23
24
  for (let i = 0; i < args.length; i++) {
24
25
  const arg = args[i];
25
26
  if (arg === "--help" || arg === "-h") parsed.help = true;
27
+ else if (arg === "--purge-archives") parsed.purgeArchives = true;
26
28
  else if (arg.startsWith("--")) {
27
29
  const name = arg.slice(2);
28
30
  if (!values.has(name) || !args[i + 1] || args[i + 1].startsWith("--")) throw new Error(`Invalid option: ${arg}`);
@@ -81,11 +83,7 @@ function pluginRefresh(place) {
81
83
  function defaultUserAgents(place) {
82
84
  const previous = readUserAgents(place);
83
85
  if (existsSync(join(place.base, "agents.json"))) return previous.length ? previous.join(",") : "none";
84
- for (const name of ["codex", "claude"]) {
85
- try { execFileSync(name, ["--version"], { stdio: "ignore" }); return name; }
86
- catch {}
87
- }
88
- return "none";
86
+ return detectUserAgents(place).join(",") || "none";
89
87
  }
90
88
 
91
89
  async function init(opts, project) {
@@ -99,10 +97,15 @@ async function init(opts, project) {
99
97
  if (opts.edition === "free" && key) throw new Error("A Polar key was provided with --edition free. Remove the key or choose Pro.");
100
98
  let selectedAgents = null;
101
99
  if (scope === "user") {
100
+ const available = detectUserAgents(place);
101
+ if (!opts.clients && process.stdin.isTTY) {
102
+ console.log("Connect Marvelous UI to your coding agents:");
103
+ SUPPORTED.forEach((name, index) => console.log(` ${index + 1}. ${LABELS[name]} (${available.includes(name) ? "detected" : "not detected"})`));
104
+ }
102
105
  const choice = opts.clients ?? (process.stdin.isTTY
103
- ? await ask("Which agents should be connected? [codex,claude,none] (comma-separated) ", defaultUserAgents(place))
106
+ ? await ask("Choose numbers or names, 'all' for detected, 'none' for Pack only: ", defaultUserAgents(place))
104
107
  : existsSync(join(place.base, "agents.json")) ? defaultUserAgents(place) : "none");
105
- selectedAgents = parseUserAgents(choice);
108
+ selectedAgents = parseUserAgents(choice, available);
106
109
  }
107
110
  if (selectedAgents) checkUserAgents(selectedAgents);
108
111
  const componentState = readState(place.components);
@@ -134,10 +137,24 @@ async function init(opts, project) {
134
137
  } else {
135
138
  const names = configureUserAgents(place, selectedAgents);
136
139
  console.log(names.length ? `Connected for all projects: ${names.join(", ")}.` : "No agent connected by choice.");
140
+ if (selectedAgents.includes("cursor")) console.log("Reload Cursor to activate its local plugin.");
137
141
  }
138
142
  console.log(scope === "user" && !selectedAgents.length ? "Pack installed." : "Ready. Restart your agent to load Marvelous UI.");
139
143
  }
140
144
 
145
+ function uninstall(opts, project) {
146
+ const scope = opts.scope ?? existingScope(project) ?? "user";
147
+ if (!["project", "user"].includes(scope)) throw new Error("Choose --scope project|user.");
148
+ if (opts.purgeArchives && scope !== "user") throw new Error("--purge-archives is available only with --scope user.");
149
+ const place = locations(scope, project);
150
+ if (scope === "user") disconnectUserAgents(place);
151
+ const result = uninstallPack(place, { purgeArchives: opts.purgeArchives });
152
+ console.log(`Marvelous UI ${scope} Pack uninstalled.`);
153
+ if (result.archivesKept) console.log(`Saved Pro ZIPs kept in ${place.archives}. Use --purge-archives to remove them.`);
154
+ if (result.projectFilesKept) console.log("Copied project components were kept. Remove them separately after reviewing local changes.");
155
+ if (scope === "project") console.log("Project agent configuration files were kept. Review their Marvelous UI entries before deleting them.");
156
+ }
157
+
141
158
  async function restore(opts, project) {
142
159
  const scope = opts.scope ?? existingScope(project) ?? "project";
143
160
  const place = locations(scope, project);
@@ -231,12 +248,13 @@ async function run(args = process.argv.slice(2)) {
231
248
  if (command === "--help" || command === "-h") return console.log(HELP);
232
249
  const opts = options(args.slice(1));
233
250
  if (!command || opts.help || command === "help") return console.log(HELP);
234
- if (!["init", "add", "update", "restore"].includes(command)) throw new Error(`Unknown command: ${command}\n${HELP}`);
251
+ if (!["init", "add", "update", "restore", "uninstall"].includes(command)) throw new Error(`Unknown command: ${command}\n${HELP}`);
235
252
  const project = resolve(opts.project ?? process.cwd());
236
253
  if (!existsSync(project)) throw new Error(`Project does not exist: ${project}`);
237
254
  if (command === "init") return init(opts, project);
238
255
  if (command === "add") return add(opts, project);
239
256
  if (command === "restore") return restore(opts, project);
257
+ if (command === "uninstall") return uninstall(opts, project);
240
258
  return update(opts, project);
241
259
  }
242
260
 
@@ -0,0 +1,161 @@
1
+ import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import { dirname, join, resolve, sep } from "node:path";
4
+ import jsonc from "jsonc-parser";
5
+
6
+ const { applyEdits, modify, parse } = jsonc;
7
+ const MARKER = ".marvelous-ui-managed.json";
8
+
9
+ function homeOf(place) { return dirname(place.base); }
10
+
11
+ function safePath(home, target) {
12
+ const root = resolve(home);
13
+ const path = resolve(target);
14
+ if (!path.startsWith(root + sep)) throw new Error(`Agent target is outside the user profile: ${path}`);
15
+ let cursor = root;
16
+ for (const part of path.slice(root.length + 1).split(sep)) {
17
+ cursor = join(cursor, part);
18
+ if (existsSync(cursor) && lstatSync(cursor).isSymbolicLink()) throw new Error(`Agent target contains a symbolic link: ${cursor}`);
19
+ }
20
+ return path;
21
+ }
22
+
23
+ function markerAt(target) { return join(target, MARKER); }
24
+ function digest(content) { return createHash("sha256").update(content).digest("hex"); }
25
+
26
+ function managed(target, place) {
27
+ const file = markerAt(target);
28
+ if (!existsSync(file)) return false;
29
+ const state = JSON.parse(readFileSync(file, "utf8"));
30
+ if (state.schema !== 1 || state.pack !== place.pack) throw new Error(`Marvelous UI does not own ${target}`);
31
+ if (state.pluginHash && digest(readFileSync(join(target, "plugin.json"))) !== state.pluginHash) throw new Error(`The Cursor plugin at ${target} was customized. It was left unchanged.`);
32
+ if (state.skillHash && digest(readFileSync(join(target, "SKILL.md"))) !== state.skillHash) throw new Error(`The OpenCode skill at ${target} was customized. It was left unchanged.`);
33
+ return true;
34
+ }
35
+
36
+ function writeMarker(target, place, extra = {}) {
37
+ writeFileSync(markerAt(target), JSON.stringify({ schema: 1, pack: place.pack, ...extra }, null, 2) + "\n");
38
+ }
39
+
40
+ export function cursorTarget(place) { return join(homeOf(place), ".cursor", "plugins", "local", "marvelous-ui"); }
41
+ export function opencodeTarget(place) { return join(homeOf(place), ".config", "opencode", "skills", "marvelous-ui"); }
42
+
43
+ export function cursorPresent(place) {
44
+ const home = homeOf(place);
45
+ return existsSync(join(home, ".cursor")) ||
46
+ process.platform === "win32" && existsSync(join(process.env.LOCALAPPDATA ?? "", "Programs", "cursor", "Cursor.exe")) ||
47
+ process.platform === "darwin" && existsSync("/Applications/Cursor.app");
48
+ }
49
+
50
+ export function opencodePresent(place) { return existsSync(join(homeOf(place), ".config", "opencode")); }
51
+
52
+ export function installCursor(place) {
53
+ const home = homeOf(place);
54
+ const target = safePath(home, cursorTarget(place));
55
+ if (!existsSync(place.pack)) throw new Error("Install the Pack before connecting Cursor.");
56
+ if (existsSync(target) && !managed(target, place)) throw new Error(`A Cursor plugin already exists at ${target}. It was not installed by Marvelous UI.`);
57
+ const parent = dirname(target);
58
+ mkdirSync(parent, { recursive: true });
59
+ const stage = safePath(home, join(parent, `.marvelous-ui-stage-${process.pid}-${Math.random().toString(16).slice(2)}`));
60
+ const backup = safePath(home, join(parent, `.marvelous-ui-backup-${process.pid}`));
61
+ if (existsSync(backup)) throw new Error("A previous Cursor plugin update is still being recovered.");
62
+ try {
63
+ cpSync(place.pack, stage, { recursive: true });
64
+ writeMarker(stage, place, { pluginHash: digest(readFileSync(join(stage, "plugin.json"))) });
65
+ if (existsSync(target)) renameSync(target, backup);
66
+ try { renameSync(stage, target); }
67
+ catch (error) { if (existsSync(backup)) renameSync(backup, target); throw error; }
68
+ if (existsSync(backup)) rmSync(backup, { recursive: true, force: true });
69
+ } finally { if (existsSync(stage)) rmSync(stage, { recursive: true, force: true }); }
70
+ }
71
+
72
+ export function removeCursor(place) {
73
+ const target = safePath(homeOf(place), cursorTarget(place));
74
+ if (!existsSync(target)) return;
75
+ if (!managed(target, place)) throw new Error(`Refusing to remove an unmanaged Cursor plugin: ${target}`);
76
+ rmSync(target, { recursive: true, force: true });
77
+ }
78
+
79
+ function configPath(place) {
80
+ const base = join(homeOf(place), ".config", "opencode");
81
+ const json = join(base, "opencode.json");
82
+ const jsoncPath = join(base, "opencode.jsonc");
83
+ if (existsSync(json) && existsSync(jsoncPath)) throw new Error("Both OpenCode config files exist. Choose one before connecting Marvelous UI.");
84
+ return existsSync(jsoncPath) ? jsoncPath : json;
85
+ }
86
+
87
+ function readConfig(file) {
88
+ const source = existsSync(file) ? readFileSync(file, "utf8") : "{}\n";
89
+ const errors = [];
90
+ const value = parse(source, errors, { allowTrailingComma: true });
91
+ if (errors.length || !value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Invalid OpenCode config: ${file}`);
92
+ return { source, value };
93
+ }
94
+
95
+ function entryPath(config) {
96
+ return config.mcp?.servers && typeof config.mcp.servers === "object" && !Array.isArray(config.mcp.servers)
97
+ ? ["mcp", "servers", "marvelous-ui"] : ["mcp", "marvelous-ui"];
98
+ }
99
+
100
+ function atPath(config, path) { return path.reduce((value, part) => value?.[part], config); }
101
+
102
+ function writeConfig(file, source, path, value) {
103
+ const updated = applyEdits(source, modify(source, path, value, { formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" } }));
104
+ const temporary = `${file}.${process.pid}.tmp`;
105
+ try { writeFileSync(temporary, updated); renameSync(temporary, file); }
106
+ finally { if (existsSync(temporary)) rmSync(temporary, { force: true }); }
107
+ }
108
+
109
+ export function installOpenCode(place) {
110
+ const home = homeOf(place);
111
+ const target = safePath(home, opencodeTarget(place));
112
+ const file = safePath(home, configPath(place));
113
+ if (!existsSync(place.pack)) throw new Error("Install the Pack before connecting OpenCode.");
114
+ const priorManaged = existsSync(target) && managed(target, place);
115
+ if (existsSync(target) && !priorManaged) throw new Error(`An unmanaged OpenCode skill already exists at ${target}.`);
116
+ const { source, value } = readConfig(file);
117
+ const path = entryPath(value);
118
+ const existing = atPath(value, path);
119
+ if (existing && !priorManaged) throw new Error("An unmanaged OpenCode MCP entry named marvelous-ui already exists.");
120
+ if (existing && existing.command?.[1] !== join(place.pack, "mcp", "server.mjs")) throw new Error("The OpenCode MCP entry was customized. It was left unchanged.");
121
+ const entry = path.length === 3
122
+ ? { type: "local", command: ["node", join(place.pack, "mcp", "server.mjs")], disabled: false }
123
+ : { type: "local", command: ["node", join(place.pack, "mcp", "server.mjs")], enabled: true };
124
+ const sourceSkill = join(place.pack, "skills", "marvelous-ui", "SKILL.md");
125
+ const original = readFileSync(sourceSkill, "utf8");
126
+ const replacement = `**Pack root**: \`${place.pack.replaceAll("\\", "/")}\`.`;
127
+ const skill = original.replace("**Pack root**: the folder two levels above this file.", replacement);
128
+ if (skill === original) throw new Error("The OpenCode skill could not locate its Pack root instruction.");
129
+ mkdirSync(target, { recursive: true });
130
+ const priorSkill = priorManaged ? readFileSync(join(target, "SKILL.md")) : null;
131
+ const priorMarker = priorManaged ? readFileSync(markerAt(target)) : null;
132
+ try {
133
+ writeFileSync(join(target, "SKILL.md"), skill);
134
+ writeMarker(target, place, { config: file, skillHash: digest(skill) });
135
+ mkdirSync(dirname(file), { recursive: true });
136
+ writeConfig(file, source, path, entry);
137
+ } catch (error) {
138
+ if (priorSkill) {
139
+ writeFileSync(join(target, "SKILL.md"), priorSkill);
140
+ writeFileSync(markerAt(target), priorMarker);
141
+ } else rmSync(target, { recursive: true, force: true });
142
+ throw error;
143
+ }
144
+ }
145
+
146
+ export function removeOpenCode(place) {
147
+ const home = homeOf(place);
148
+ const target = safePath(home, opencodeTarget(place));
149
+ if (!existsSync(target)) return;
150
+ if (!managed(target, place)) throw new Error(`Refusing to remove an unmanaged OpenCode skill: ${target}`);
151
+ const marker = JSON.parse(readFileSync(markerAt(target), "utf8"));
152
+ const file = safePath(home, marker.config);
153
+ if (existsSync(file)) {
154
+ const { source, value } = readConfig(file);
155
+ const path = entryPath(value);
156
+ const existing = atPath(value, path);
157
+ if (existing && existing.command?.[1] !== join(place.pack, "mcp", "server.mjs")) throw new Error("The OpenCode MCP entry was customized. It was left unchanged.");
158
+ if (existing) writeConfig(file, source, path, undefined);
159
+ }
160
+ rmSync(target, { recursive: true, force: true });
161
+ }
package/lib/agents.mjs CHANGED
@@ -1,17 +1,20 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
2
  import { join } from "node:path";
3
+ import { cursorPresent, installCursor, installOpenCode, opencodePresent, removeCursor, removeOpenCode } from "./agent-targets.mjs";
4
+ import { runCommand } from "./command.mjs";
4
5
 
5
- const SUPPORTED = ["codex", "claude"];
6
- const LABELS = { codex: "Codex", claude: "Claude Code" };
6
+ export const SUPPORTED = ["codex", "claude", "cursor", "opencode"];
7
+ export const LABELS = { codex: "Codex", claude: "Claude Code", cursor: "Cursor", opencode: "OpenCode" };
7
8
 
8
- export function parseUserAgents(value) {
9
+ export function parseUserAgents(value, available = []) {
9
10
  const names = [...new Set(value.toLowerCase().split(",").map((name) => name.trim()).filter(Boolean))];
10
11
  if (names.length === 1 && names[0] === "none") return [];
11
- const unsupported = names.filter((name) => !SUPPORTED.includes(name));
12
- if (unsupported.length) throw new Error(`User setup supports Codex and Claude Code. Unsupported choice: ${unsupported.join(", ")}. Other agents can use project setup or the manual guide.`);
13
- if (!names.length) throw new Error("Choose codex, claude, both, or none.");
14
- return names;
12
+ if (names.length === 1 && names[0] === "all") return [...available];
13
+ const chosen = names.map((name) => /^\d+$/.test(name) ? SUPPORTED[Number(name) - 1] ?? name : name);
14
+ const unsupported = chosen.filter((name) => !SUPPORTED.includes(name));
15
+ if (unsupported.length) throw new Error(`Unsupported agent choice: ${unsupported.join(", ")}. Choose codex, claude, cursor, opencode, all, or none.`);
16
+ if (!chosen.length) throw new Error("Choose an agent, all, or none.");
17
+ return [...new Set(chosen)];
15
18
  }
16
19
 
17
20
  export function readUserAgents(place) {
@@ -24,42 +27,77 @@ export function readUserAgents(place) {
24
27
  return state.clients;
25
28
  }
26
29
 
27
- function runAgent(command, args, runner) {
28
- try { runner(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); }
30
+ function runAgent(command, args, runner, phase = "setup") {
31
+ try { return runner(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); }
29
32
  catch (error) {
33
+ if (phase === "check" && error.code === "ENOENT") throw new Error(`${LABELS[command]} CLI was not found. Install its command-line tool or choose another agent.`);
30
34
  const detail = String(error.stderr ?? error.message ?? "").trim().split(/\r?\n/).at(-1);
31
- throw new Error(`${LABELS[command]} setup failed${detail ? `: ${detail}` : "."} The Pack remains installed. Rerun init to retry agent setup.`);
35
+ throw new Error(`${LABELS[command]} ${phase} failed${detail ? `: ${detail}` : "."}${phase === "check" ? "" : " The Pack remains installed. Rerun init to retry agent setup."}`);
32
36
  }
33
37
  }
34
38
 
35
- export function checkUserAgents(clients, runner = execFileSync) {
36
- for (const client of clients) runAgent(client, ["--version"], runner);
39
+ export function detectUserAgents(place, runner = runCommand) {
40
+ return SUPPORTED.filter((client) => {
41
+ if (client === "cursor") return cursorPresent(place);
42
+ if (client === "opencode" && opencodePresent(place)) return true;
43
+ try { runAgent(client, ["--version"], runner, "check"); return true; }
44
+ catch { return false; }
45
+ });
37
46
  }
38
47
 
39
- export function configureUserAgents(place, clients, runner = execFileSync) {
40
- const prior = readUserAgents(place);
41
- for (const client of clients) {
42
- if (client === "codex") {
43
- runAgent("codex", ["plugin", "marketplace", "add", place.pack], runner);
44
- runAgent("codex", ["plugin", "add", "marvelous-ui@marvelous-ui"], runner);
48
+ export function checkUserAgents(clients, runner = runCommand) {
49
+ for (const client of clients.filter((name) => name === "codex" || name === "claude")) runAgent(client, ["--version"], runner, "check");
50
+ }
51
+
52
+ function register(place, client, prior, runner) {
53
+ if (client === "codex") {
54
+ runAgent("codex", ["plugin", "marketplace", "add", place.pack], runner);
55
+ runAgent("codex", ["plugin", "add", "marvelous-ui@marvelous-ui"], runner);
56
+ } else if (client === "claude") {
57
+ if (prior.includes("claude")) {
58
+ runAgent("claude", ["plugin", "marketplace", "update", "marvelous-ui"], runner);
59
+ runAgent("claude", ["plugin", "update", "marvelous-ui@marvelous-ui"], runner);
45
60
  } else {
46
- if (prior.includes("claude")) {
47
- runAgent("claude", ["plugin", "marketplace", "update", "marvelous-ui"], runner);
48
- runAgent("claude", ["plugin", "update", "marvelous-ui@marvelous-ui"], runner);
49
- } else {
50
- runAgent("claude", ["plugin", "marketplace", "add", place.pack], runner);
51
- runAgent("claude", ["plugin", "install", "marvelous-ui@marvelous-ui", "--scope", "user"], runner);
52
- }
61
+ runAgent("claude", ["plugin", "marketplace", "add", place.pack], runner);
62
+ runAgent("claude", ["plugin", "install", "marvelous-ui@marvelous-ui", "--scope", "user"], runner);
63
+ }
64
+ } else if (client === "cursor") installCursor(place);
65
+ else installOpenCode(place);
66
+ }
67
+
68
+ function unregister(place, client, runner) {
69
+ if (client === "codex") {
70
+ runAgent("codex", ["plugin", "remove", "marvelous-ui@marvelous-ui"], runner);
71
+ try { runAgent("codex", ["plugin", "marketplace", "remove", "marvelous-ui"], runner); }
72
+ catch (error) {
73
+ if (!/marketplace `?marvelous-ui`? is not configured or installed/i.test(error.message)) throw error;
74
+ }
75
+ } else if (client === "claude") {
76
+ runAgent("claude", ["plugin", "uninstall", "marvelous-ui@marvelous-ui", "--scope", "user"], runner);
77
+ runAgent("claude", ["plugin", "marketplace", "remove", "marvelous-ui"], runner);
78
+ } else if (client === "cursor") removeCursor(place);
79
+ else removeOpenCode(place);
80
+ }
81
+
82
+ export function configureUserAgents(place, clients, runner = runCommand) {
83
+ let connected = readUserAgents(place);
84
+ for (const client of clients) {
85
+ register(place, client, connected, runner);
86
+ if (!connected.includes(client)) {
87
+ connected = [...connected, client];
88
+ saveUserAgents(place, connected);
53
89
  }
54
90
  }
55
- for (const client of prior.filter((name) => !clients.includes(name))) {
56
- if (client === "codex") runAgent("codex", ["plugin", "remove", "marvelous-ui@marvelous-ui"], runner);
57
- else runAgent("claude", ["plugin", "uninstall", "marvelous-ui@marvelous-ui", "--scope", "user"], runner);
91
+ for (const client of connected.filter((name) => !clients.includes(name))) {
92
+ unregister(place, client, runner);
93
+ connected = connected.filter((name) => name !== client);
94
+ saveUserAgents(place, connected);
58
95
  }
59
- saveUserAgents(place, clients);
60
96
  return clients.map((name) => LABELS[name]);
61
97
  }
62
98
 
99
+ export function disconnectUserAgents(place, runner = runCommand) { configureUserAgents(place, [], runner); }
100
+
63
101
  function saveUserAgents(place, clients) {
64
102
  const file = join(place.base, "agents.json");
65
103
  mkdirSync(place.base, { recursive: true });
@@ -0,0 +1,17 @@
1
+ import spawn from "cross-spawn";
2
+
3
+ /** Run agent CLIs, including npm .cmd shims on Windows, without building a shell command. */
4
+ export function runCommand(command, args, options = {}) {
5
+ const result = spawn.sync(command, args, {
6
+ encoding: "utf8",
7
+ stdio: ["ignore", "pipe", "pipe"],
8
+ ...options,
9
+ });
10
+ if (result.error) throw result.error;
11
+ if (result.status !== 0) {
12
+ const error = new Error(`${command} exited with code ${result.status ?? "unknown"}`);
13
+ error.stderr = result.stderr;
14
+ throw error;
15
+ }
16
+ return result.stdout;
17
+ }
package/lib/installer.mjs CHANGED
@@ -27,6 +27,22 @@ export function readState(file) {
27
27
  return state;
28
28
  }
29
29
 
30
+ /** Remove only CLI-owned Pack data. Copied project files and Pro ZIPs remain by default. */
31
+ export function uninstallPack(place, { purgeArchives = false } = {}) {
32
+ const base = resolve(place.base);
33
+ if (existsSync(base) && lstatSync(base).isSymbolicLink()) throw new Error("The Marvelous UI installation directory cannot be a symbolic link.");
34
+ const targets = [place.pack, join(place.base, "versions")];
35
+ if (purgeArchives) targets.push(place.archives);
36
+ for (const target of targets) {
37
+ const absolute = resolve(target);
38
+ if (!absolute.startsWith(base + sep)) throw new Error(`Refusing to remove a path outside the installation: ${absolute}`);
39
+ if (existsSync(absolute) && lstatSync(absolute).isSymbolicLink()) throw new Error(`Refusing to remove a symbolic link: ${absolute}`);
40
+ }
41
+ for (const target of targets) if (existsSync(target)) rmSync(target, { recursive: true, force: true });
42
+ for (const file of [place.state, join(place.base, "agents.json")]) if (existsSync(file)) rmSync(file);
43
+ return { archivesKept: !purgeArchives && existsSync(place.archives), projectFilesKept: existsSync(place.components) };
44
+ }
45
+
30
46
  function saveState(file, state) {
31
47
  mkdirSync(dirname(file), { recursive: true });
32
48
  const tmp = `${file}.${process.pid}.tmp`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "marvelous-ui-library",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "Install and update the Marvelous UI component Pack for coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,5 +22,9 @@
22
22
  "type": "git",
23
23
  "url": "git+https://github.com/baz-fkr/marvelous-ui-library.git",
24
24
  "directory": "cli"
25
+ },
26
+ "dependencies": {
27
+ "cross-spawn": "7.0.6",
28
+ "jsonc-parser": "3.3.1"
25
29
  }
26
30
  }