marvelous-ui-library 1.0.1 → 1.0.3
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 +50 -9
- package/lib/agent-targets.mjs +161 -0
- package/lib/agents.mjs +104 -0
- package/lib/command.mjs +17 -0
- package/lib/installer.mjs +26 -4
- 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,7 +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";
|
|
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";
|
|
8
9
|
|
|
9
10
|
const HELP = `Marvelous UI
|
|
10
11
|
|
|
@@ -12,8 +13,9 @@ const HELP = `Marvelous UI
|
|
|
12
13
|
npx marvelous-ui-library@latest add <component...> [--scope project|user] [--out path]
|
|
13
14
|
npx marvelous-ui-library@latest update [--scope project|user]
|
|
14
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]
|
|
15
17
|
|
|
16
|
-
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.
|
|
17
19
|
`;
|
|
18
20
|
|
|
19
21
|
function options(args) {
|
|
@@ -22,6 +24,7 @@ function options(args) {
|
|
|
22
24
|
for (let i = 0; i < args.length; i++) {
|
|
23
25
|
const arg = args[i];
|
|
24
26
|
if (arg === "--help" || arg === "-h") parsed.help = true;
|
|
27
|
+
else if (arg === "--purge-archives") parsed.purgeArchives = true;
|
|
25
28
|
else if (arg.startsWith("--")) {
|
|
26
29
|
const name = arg.slice(2);
|
|
27
30
|
if (!values.has(name) || !args[i + 1] || args[i + 1].startsWith("--")) throw new Error(`Invalid option: ${arg}`);
|
|
@@ -69,8 +72,18 @@ function chosenOut(project, supplied, componentState) {
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
function pluginRefresh(place) {
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
if (place.scope !== "user") return;
|
|
76
|
+
const agents = readUserAgents(place);
|
|
77
|
+
if (agents.length) {
|
|
78
|
+
const names = configureUserAgents(place, agents);
|
|
79
|
+
console.log(`Updated agent plugins: ${names.join(", ")}.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function defaultUserAgents(place) {
|
|
84
|
+
const previous = readUserAgents(place);
|
|
85
|
+
if (existsSync(join(place.base, "agents.json"))) return previous.length ? previous.join(",") : "none";
|
|
86
|
+
return detectUserAgents(place).join(",") || "none";
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
async function init(opts, project) {
|
|
@@ -82,9 +95,22 @@ async function init(opts, project) {
|
|
|
82
95
|
const edition = opts.edition ?? (key || current?.edition === "pro" ? "pro" : "free");
|
|
83
96
|
if (!["project", "user"].includes(scope) || !["free", "pro"].includes(edition)) throw new Error("Choose --scope project|user and --edition free|pro.");
|
|
84
97
|
if (opts.edition === "free" && key) throw new Error("A Polar key was provided with --edition free. Remove the key or choose Pro.");
|
|
98
|
+
let selectedAgents = null;
|
|
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
|
+
}
|
|
105
|
+
const choice = opts.clients ?? (process.stdin.isTTY
|
|
106
|
+
? await ask("Choose numbers or names, 'all' for detected, 'none' for Pack only: ", defaultUserAgents(place))
|
|
107
|
+
: existsSync(join(place.base, "agents.json")) ? defaultUserAgents(place) : "none");
|
|
108
|
+
selectedAgents = parseUserAgents(choice, available);
|
|
109
|
+
}
|
|
110
|
+
if (selectedAgents) checkUserAgents(selectedAgents);
|
|
85
111
|
const componentState = readState(place.components);
|
|
86
112
|
const hasComponents = !!(current && componentState?.slugs?.length && componentState.sourceScope === scope);
|
|
87
|
-
console.log(`Marvelous UI: ${edition} Pack, ${scope
|
|
113
|
+
console.log(`Marvelous UI: ${edition} Pack, ${scope === "user" ? "user profile" : `${detectFramework(project)} project`}${key ? ", Polar key received" : ""}.`);
|
|
88
114
|
if (!current || current.edition !== edition) {
|
|
89
115
|
const meta = await latestRelease(edition);
|
|
90
116
|
const bytes = await downloadRelease(edition, meta, edition === "pro" ? key || await licenseKey() : "");
|
|
@@ -109,10 +135,24 @@ async function init(opts, project) {
|
|
|
109
135
|
if (opts.clients) args.push("--clients", opts.clients);
|
|
110
136
|
execFileSync(process.execPath, args, { stdio: "inherit" });
|
|
111
137
|
} else {
|
|
112
|
-
|
|
113
|
-
|
|
138
|
+
const names = configureUserAgents(place, selectedAgents);
|
|
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.");
|
|
114
141
|
}
|
|
115
|
-
console.log("Ready.
|
|
142
|
+
console.log(scope === "user" && !selectedAgents.length ? "Pack installed." : "Ready. Restart your agent to load Marvelous UI.");
|
|
143
|
+
}
|
|
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.");
|
|
116
156
|
}
|
|
117
157
|
|
|
118
158
|
async function restore(opts, project) {
|
|
@@ -208,12 +248,13 @@ async function run(args = process.argv.slice(2)) {
|
|
|
208
248
|
if (command === "--help" || command === "-h") return console.log(HELP);
|
|
209
249
|
const opts = options(args.slice(1));
|
|
210
250
|
if (!command || opts.help || command === "help") return console.log(HELP);
|
|
211
|
-
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}`);
|
|
212
252
|
const project = resolve(opts.project ?? process.cwd());
|
|
213
253
|
if (!existsSync(project)) throw new Error(`Project does not exist: ${project}`);
|
|
214
254
|
if (command === "init") return init(opts, project);
|
|
215
255
|
if (command === "add") return add(opts, project);
|
|
216
256
|
if (command === "restore") return restore(opts, project);
|
|
257
|
+
if (command === "uninstall") return uninstall(opts, project);
|
|
217
258
|
return update(opts, project);
|
|
218
259
|
}
|
|
219
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
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { cursorPresent, installCursor, installOpenCode, opencodePresent, removeCursor, removeOpenCode } from "./agent-targets.mjs";
|
|
4
|
+
import { runCommand } from "./command.mjs";
|
|
5
|
+
|
|
6
|
+
export const SUPPORTED = ["codex", "claude", "cursor", "opencode"];
|
|
7
|
+
export const LABELS = { codex: "Codex", claude: "Claude Code", cursor: "Cursor", opencode: "OpenCode" };
|
|
8
|
+
|
|
9
|
+
export function parseUserAgents(value, available = []) {
|
|
10
|
+
const names = [...new Set(value.toLowerCase().split(",").map((name) => name.trim()).filter(Boolean))];
|
|
11
|
+
if (names.length === 1 && names[0] === "none") return [];
|
|
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)];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readUserAgents(place) {
|
|
21
|
+
const file = join(place.base, "agents.json");
|
|
22
|
+
if (!existsSync(file)) return [];
|
|
23
|
+
const state = JSON.parse(readFileSync(file, "utf8"));
|
|
24
|
+
if (state.schema !== 1 || !Array.isArray(state.clients) || state.clients.some((name) => !SUPPORTED.includes(name))) {
|
|
25
|
+
throw new Error(`Unsupported Marvelous UI agent state: ${file}`);
|
|
26
|
+
}
|
|
27
|
+
return state.clients;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function runAgent(command, args, runner, phase = "setup") {
|
|
31
|
+
try { return runner(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); }
|
|
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.`);
|
|
34
|
+
const detail = String(error.stderr ?? error.message ?? "").trim().split(/\r?\n/).at(-1);
|
|
35
|
+
throw new Error(`${LABELS[command]} ${phase} failed${detail ? `: ${detail}` : "."}${phase === "check" ? "" : " The Pack remains installed. Rerun init to retry agent setup."}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
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
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
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);
|
|
60
|
+
} else {
|
|
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
|
+
runAgent("codex", ["plugin", "marketplace", "remove", "marvelous-ui"], runner);
|
|
72
|
+
} else if (client === "claude") {
|
|
73
|
+
runAgent("claude", ["plugin", "uninstall", "marvelous-ui@marvelous-ui", "--scope", "user"], runner);
|
|
74
|
+
runAgent("claude", ["plugin", "marketplace", "remove", "marvelous-ui"], runner);
|
|
75
|
+
} else if (client === "cursor") removeCursor(place);
|
|
76
|
+
else removeOpenCode(place);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function configureUserAgents(place, clients, runner = runCommand) {
|
|
80
|
+
let connected = readUserAgents(place);
|
|
81
|
+
for (const client of clients) {
|
|
82
|
+
register(place, client, connected, runner);
|
|
83
|
+
if (!connected.includes(client)) {
|
|
84
|
+
connected = [...connected, client];
|
|
85
|
+
saveUserAgents(place, connected);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const client of connected.filter((name) => !clients.includes(name))) {
|
|
89
|
+
unregister(place, client, runner);
|
|
90
|
+
connected = connected.filter((name) => name !== client);
|
|
91
|
+
saveUserAgents(place, connected);
|
|
92
|
+
}
|
|
93
|
+
return clients.map((name) => LABELS[name]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function disconnectUserAgents(place, runner = runCommand) { configureUserAgents(place, [], runner); }
|
|
97
|
+
|
|
98
|
+
function saveUserAgents(place, clients) {
|
|
99
|
+
const file = join(place.base, "agents.json");
|
|
100
|
+
mkdirSync(place.base, { recursive: true });
|
|
101
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
102
|
+
writeFileSync(temporary, JSON.stringify({ schema: 1, clients }, null, 2) + "\n", { mode: 0o600 });
|
|
103
|
+
renameSync(temporary, file);
|
|
104
|
+
}
|
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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
1
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
@@ -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`;
|
|
@@ -193,9 +209,15 @@ export function installPack(bytes, edition, meta, place, { installDependencies =
|
|
|
193
209
|
validPack(stage, edition, meta.version);
|
|
194
210
|
if (edition === "pro") saveProArchive(bytes, meta, place);
|
|
195
211
|
if (installDependencies) {
|
|
196
|
-
const npmArgs = ["ci", "--omit=dev", "--ignore-scripts", "--no-audit"];
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
const npmArgs = ["ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--silent"];
|
|
213
|
+
try {
|
|
214
|
+
if (process.env.npm_execpath?.endsWith(".js")) execFileSync(process.execPath, [process.env.npm_execpath, ...npmArgs], { cwd: stage, stdio: ["ignore", "pipe", "pipe"] });
|
|
215
|
+
else if (process.platform === "win32") execSync(`npm ${npmArgs.join(" ")}`, { cwd: stage, stdio: ["ignore", "pipe", "pipe"] });
|
|
216
|
+
else execFileSync("npm", npmArgs, { cwd: stage, stdio: ["ignore", "pipe", "pipe"] });
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const detail = String(error.stderr ?? error.message ?? "").trim().split(/\r?\n/).at(-1);
|
|
219
|
+
throw new Error(`The Pack dependencies could not be installed${detail ? `: ${detail}` : "."}`);
|
|
220
|
+
}
|
|
199
221
|
}
|
|
200
222
|
beforeCommit?.(stage);
|
|
201
223
|
if (existsSync(backup)) throw new Error("A previous installation is still being recovered.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "marvelous-ui-library",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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
|
}
|