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 +13 -1
- package/bin.mjs +29 -11
- package/lib/agent-targets.mjs +161 -0
- package/lib/agents.mjs +68 -30
- package/lib/command.mjs +17 -0
- package/lib/installer.mjs +16 -0
- package/package.json +5 -1
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.
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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]}
|
|
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
|
|
36
|
-
|
|
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
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
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 });
|
package/lib/command.mjs
ADDED
|
@@ -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.
|
|
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
|
}
|