marvelous-ui-library 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/bin.mjs +106 -16
- package/lib/agents.mjs +69 -0
- package/lib/installer.mjs +78 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +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,
|
|
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`.
|
|
11
11
|
|
|
12
|
-
The Pro Pack requires a Polar license key
|
|
12
|
+
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
13
|
|
|
14
14
|
```bash
|
|
15
15
|
npx marvelous-ui-library@latest update
|
|
@@ -17,4 +17,12 @@ npx marvelous-ui-library@latest update
|
|
|
17
17
|
|
|
18
18
|
`update` compares installed component hashes before replacing files. Locally customized files are reported and preserved. A Pack installed in your user profile does not change copies already installed in other projects. A current Pro license key, including a key from an active yearly Renewal, is required to fetch a newer Pro Pack. For a private Renewal link, contact hello@marvelous-ui.com.
|
|
19
19
|
|
|
20
|
+
Every verified Pro ZIP is saved under `~/.marvelous-ui/archives`, including when the Pack is installed in a project. Keep a backup of this folder if you want to preserve your downloaded releases. To restore a saved release without contacting Polar or the download service:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx marvelous-ui-library@latest restore --version 1.0.0
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
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
|
+
|
|
20
28
|
The Free Pack is MIT licensed. The Pro Pack is subject to the commercial license shipped in its `LICENSE.md`.
|
package/bin.mjs
CHANGED
|
@@ -3,19 +3,22 @@ import { execFileSync } from "node:child_process";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
-
import {
|
|
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
9
|
|
|
8
10
|
const HELP = `Marvelous UI
|
|
9
11
|
|
|
10
|
-
npx marvelous-ui-library@latest init [--scope project|user]
|
|
12
|
+
npx marvelous-ui-library@latest init [--scope project|user]
|
|
11
13
|
npx marvelous-ui-library@latest add <component...> [--scope project|user] [--out path]
|
|
12
14
|
npx marvelous-ui-library@latest update [--scope project|user]
|
|
15
|
+
npx marvelous-ui-library@latest restore [--scope project|user] [--version 1.1.0]
|
|
13
16
|
|
|
14
|
-
Options: --project path, --clients names.
|
|
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.
|
|
15
18
|
`;
|
|
16
19
|
|
|
17
20
|
function options(args) {
|
|
18
|
-
const values = new Set(["scope", "edition", "project", "out", "clients"]);
|
|
21
|
+
const values = new Set(["scope", "edition", "project", "out", "clients", "version"]);
|
|
19
22
|
const parsed = { slugs: [] };
|
|
20
23
|
for (let i = 0; i < args.length; i++) {
|
|
21
24
|
const arg = args[i];
|
|
@@ -36,12 +39,21 @@ async function ask(question, fallback = "") {
|
|
|
36
39
|
finally { rl.close(); }
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
async function askSecret(question) {
|
|
43
|
+
if (!process.stdin.isTTY) return "";
|
|
44
|
+
const muted = new Writable({ write(_chunk, _encoding, done) { done(); } });
|
|
45
|
+
const rl = createInterface({ input: process.stdin, output: muted, terminal: true });
|
|
46
|
+
process.stdout.write(question);
|
|
47
|
+
try { return (await rl.question("")).trim(); }
|
|
48
|
+
finally { rl.close(); process.stdout.write("\n"); }
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
function existingScope(project) {
|
|
40
52
|
return readState(locations("project", project).state) ? "project" : readState(locations("user", project).state) ? "user" : null;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
async function licenseKey() {
|
|
44
|
-
const key = process.env.MARVELOUS_LICENSE_KEY?.trim() || await
|
|
56
|
+
const key = process.env.MARVELOUS_LICENSE_KEY?.trim() || await askSecret("Polar license key: ");
|
|
45
57
|
if (!key) throw new Error("A Pro license key is required. Set MARVELOUS_LICENSE_KEY or run interactively.");
|
|
46
58
|
return key;
|
|
47
59
|
}
|
|
@@ -58,33 +70,110 @@ function chosenOut(project, supplied, componentState) {
|
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
function pluginRefresh(place) {
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
if (place.scope !== "user") return;
|
|
74
|
+
const agents = readUserAgents(place);
|
|
75
|
+
if (agents.length) {
|
|
76
|
+
const names = configureUserAgents(place, agents);
|
|
77
|
+
console.log(`Updated agent plugins: ${names.join(", ")}.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function defaultUserAgents(place) {
|
|
82
|
+
const previous = readUserAgents(place);
|
|
83
|
+
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";
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
async function init(opts, project) {
|
|
66
92
|
const scope = opts.scope ?? existingScope(project) ?? await ask("Install in this project or for this user? [project/user] ", "project");
|
|
67
93
|
const place = locations(scope, project);
|
|
68
94
|
const current = readState(place.state);
|
|
69
|
-
|
|
95
|
+
let key = process.env.MARVELOUS_LICENSE_KEY?.trim() ?? "";
|
|
96
|
+
if (!opts.edition && !key && current?.edition !== "pro") key = await askSecret("Polar license key (optional, press Enter for Free): ");
|
|
97
|
+
const edition = opts.edition ?? (key || current?.edition === "pro" ? "pro" : "free");
|
|
70
98
|
if (!["project", "user"].includes(scope) || !["free", "pro"].includes(edition)) throw new Error("Choose --scope project|user and --edition free|pro.");
|
|
71
|
-
|
|
99
|
+
if (opts.edition === "free" && key) throw new Error("A Polar key was provided with --edition free. Remove the key or choose Pro.");
|
|
100
|
+
let selectedAgents = null;
|
|
101
|
+
if (scope === "user") {
|
|
102
|
+
const choice = opts.clients ?? (process.stdin.isTTY
|
|
103
|
+
? await ask("Which agents should be connected? [codex,claude,none] (comma-separated) ", defaultUserAgents(place))
|
|
104
|
+
: existsSync(join(place.base, "agents.json")) ? defaultUserAgents(place) : "none");
|
|
105
|
+
selectedAgents = parseUserAgents(choice);
|
|
106
|
+
}
|
|
107
|
+
if (selectedAgents) checkUserAgents(selectedAgents);
|
|
108
|
+
const componentState = readState(place.components);
|
|
109
|
+
const hasComponents = !!(current && componentState?.slugs?.length && componentState.sourceScope === scope);
|
|
110
|
+
console.log(`Marvelous UI: ${edition} Pack, ${scope === "user" ? "user profile" : `${detectFramework(project)} project`}${key ? ", Polar key received" : ""}.`);
|
|
72
111
|
if (!current || current.edition !== edition) {
|
|
73
112
|
const meta = await latestRelease(edition);
|
|
74
|
-
const bytes = await downloadRelease(edition, meta, edition === "pro" ? await licenseKey() : "");
|
|
75
|
-
installPack(bytes, edition, meta, place
|
|
113
|
+
const bytes = await downloadRelease(edition, meta, edition === "pro" ? key || await licenseKey() : "");
|
|
114
|
+
installPack(bytes, edition, meta, place, {
|
|
115
|
+
beforeCommit(stagedPack) {
|
|
116
|
+
if (!hasComponents) return;
|
|
117
|
+
const preview = addComponents({ place: { ...place, pack: stagedPack }, project, slugs: componentState.slugs, out: componentState.out, dryRun: true, useCurrentPack: true });
|
|
118
|
+
console.log("Project component changes:");
|
|
119
|
+
report(preview);
|
|
120
|
+
if (preview.conflicts.length) console.log("Customized files will be preserved. Merge those files manually if you want the new edition.");
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (hasComponents) report(addComponents({ place, project, slugs: componentState.slugs, out: componentState.out, useCurrentPack: true }));
|
|
76
124
|
console.log(`Pack ${meta.version} installed in ${place.pack}`);
|
|
77
|
-
} else
|
|
125
|
+
} else {
|
|
126
|
+
if (edition === "pro" && key) await downloadRelease("pro", await latestRelease("pro"), key);
|
|
127
|
+
console.log(`Pack ${current.version} is installed. Run update to fetch a newer version.`);
|
|
128
|
+
}
|
|
78
129
|
if (scope === "project") {
|
|
79
130
|
const out = chosenOut(project, opts.out, readState(place.components));
|
|
80
131
|
const args = [join(place.pack, "scripts", "init-agent.mjs"), project, "--out", out];
|
|
81
132
|
if (opts.clients) args.push("--clients", opts.clients);
|
|
82
133
|
execFileSync(process.execPath, args, { stdio: "inherit" });
|
|
83
134
|
} else {
|
|
84
|
-
|
|
85
|
-
|
|
135
|
+
const names = configureUserAgents(place, selectedAgents);
|
|
136
|
+
console.log(names.length ? `Connected for all projects: ${names.join(", ")}.` : "No agent connected by choice.");
|
|
137
|
+
}
|
|
138
|
+
console.log(scope === "user" && !selectedAgents.length ? "Pack installed." : "Ready. Restart your agent to load Marvelous UI.");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function restore(opts, project) {
|
|
142
|
+
const scope = opts.scope ?? existingScope(project) ?? "project";
|
|
143
|
+
const place = locations(scope, project);
|
|
144
|
+
const archives = listProArchives(place);
|
|
145
|
+
let version = opts.version;
|
|
146
|
+
if (!version) {
|
|
147
|
+
if (!process.stdin.isTTY) throw new Error("Specify --version to restore a saved Pro ZIP noninteractively.");
|
|
148
|
+
if (!archives.length) throw new Error("No saved Pro ZIP found. A new download requires an active key.");
|
|
149
|
+
console.log("Saved Pro ZIPs:");
|
|
150
|
+
archives.forEach((item, index) => console.log(` ${index + 1}. ${item.version} (${item.sha256.slice(0, 12)})`));
|
|
151
|
+
const choice = Number(await ask("Choose a version [1]: ", "1"));
|
|
152
|
+
if (!Number.isSafeInteger(choice) || choice < 1 || choice > archives.length) throw new Error("Choose a listed Pro version.");
|
|
153
|
+
version = archives[choice - 1].version;
|
|
86
154
|
}
|
|
87
|
-
|
|
155
|
+
const { bytes, meta } = readProArchive(place, version);
|
|
156
|
+
const current = readState(place.state);
|
|
157
|
+
const componentState = readState(place.components);
|
|
158
|
+
const hasComponents = !!(current && componentState?.slugs?.length && componentState.sourceScope === scope);
|
|
159
|
+
console.log(`Restoring Pro Pack ${meta.version} from the verified local ZIP.`);
|
|
160
|
+
installPack(bytes, "pro", meta, place, {
|
|
161
|
+
beforeCommit(stagedPack) {
|
|
162
|
+
if (!hasComponents) return;
|
|
163
|
+
const preview = addComponents({ place: { ...place, pack: stagedPack }, project, slugs: componentState.slugs, out: componentState.out, dryRun: true, useCurrentPack: true });
|
|
164
|
+
console.log("Project component changes:");
|
|
165
|
+
report(preview);
|
|
166
|
+
if (preview.conflicts.length) console.log("Customized files will be preserved. Merge those files manually if you want the restored version.");
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
if (hasComponents) report(addComponents({ place, project, slugs: componentState.slugs, out: componentState.out, useCurrentPack: true }));
|
|
170
|
+
if (!current && scope === "project") {
|
|
171
|
+
const out = chosenOut(project, opts.out, componentState);
|
|
172
|
+
const args = [join(place.pack, "scripts", "init-agent.mjs"), project, "--out", out];
|
|
173
|
+
if (opts.clients) args.push("--clients", opts.clients);
|
|
174
|
+
execFileSync(process.execPath, args, { stdio: "inherit" });
|
|
175
|
+
} else pluginRefresh(place);
|
|
176
|
+
console.log(`Pro Pack ${meta.version} restored without contacting the download service.`);
|
|
88
177
|
}
|
|
89
178
|
|
|
90
179
|
async function add(opts, project) {
|
|
@@ -142,11 +231,12 @@ async function run(args = process.argv.slice(2)) {
|
|
|
142
231
|
if (command === "--help" || command === "-h") return console.log(HELP);
|
|
143
232
|
const opts = options(args.slice(1));
|
|
144
233
|
if (!command || opts.help || command === "help") return console.log(HELP);
|
|
145
|
-
if (!["init", "add", "update"].includes(command)) throw new Error(`Unknown command: ${command}\n${HELP}`);
|
|
234
|
+
if (!["init", "add", "update", "restore"].includes(command)) throw new Error(`Unknown command: ${command}\n${HELP}`);
|
|
146
235
|
const project = resolve(opts.project ?? process.cwd());
|
|
147
236
|
if (!existsSync(project)) throw new Error(`Project does not exist: ${project}`);
|
|
148
237
|
if (command === "init") return init(opts, project);
|
|
149
238
|
if (command === "add") return add(opts, project);
|
|
239
|
+
if (command === "restore") return restore(opts, project);
|
|
150
240
|
return update(opts, project);
|
|
151
241
|
}
|
|
152
242
|
|
package/lib/agents.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SUPPORTED = ["codex", "claude"];
|
|
6
|
+
const LABELS = { codex: "Codex", claude: "Claude Code" };
|
|
7
|
+
|
|
8
|
+
export function parseUserAgents(value) {
|
|
9
|
+
const names = [...new Set(value.toLowerCase().split(",").map((name) => name.trim()).filter(Boolean))];
|
|
10
|
+
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;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function readUserAgents(place) {
|
|
18
|
+
const file = join(place.base, "agents.json");
|
|
19
|
+
if (!existsSync(file)) return [];
|
|
20
|
+
const state = JSON.parse(readFileSync(file, "utf8"));
|
|
21
|
+
if (state.schema !== 1 || !Array.isArray(state.clients) || state.clients.some((name) => !SUPPORTED.includes(name))) {
|
|
22
|
+
throw new Error(`Unsupported Marvelous UI agent state: ${file}`);
|
|
23
|
+
}
|
|
24
|
+
return state.clients;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runAgent(command, args, runner) {
|
|
28
|
+
try { runner(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); }
|
|
29
|
+
catch (error) {
|
|
30
|
+
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.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function checkUserAgents(clients, runner = execFileSync) {
|
|
36
|
+
for (const client of clients) runAgent(client, ["--version"], runner);
|
|
37
|
+
}
|
|
38
|
+
|
|
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);
|
|
45
|
+
} 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
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
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);
|
|
58
|
+
}
|
|
59
|
+
saveUserAgents(place, clients);
|
|
60
|
+
return clients.map((name) => LABELS[name]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function saveUserAgents(place, clients) {
|
|
64
|
+
const file = join(place.base, "agents.json");
|
|
65
|
+
mkdirSync(place.base, { recursive: true });
|
|
66
|
+
const temporary = `${file}.${process.pid}.tmp`;
|
|
67
|
+
writeFileSync(temporary, JSON.stringify({ schema: 1, clients }, null, 2) + "\n", { mode: 0o600 });
|
|
68
|
+
renameSync(temporary, file);
|
|
69
|
+
}
|
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";
|
|
@@ -17,7 +17,7 @@ export function locations(scope, project = process.cwd(), home = homedir()) {
|
|
|
17
17
|
try { if (lstatSync(projectBase).isSymbolicLink()) throw new Error("The project Pack directory cannot be a symbolic link."); }
|
|
18
18
|
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
19
19
|
const base = scope === "user" ? join(home, ".marvelous-ui") : projectBase;
|
|
20
|
-
return { scope, base, pack: join(base, "pack"), state: join(base, "install.json"), components: join(projectBase, "components.json") };
|
|
20
|
+
return { scope, base, pack: join(base, "pack"), state: join(base, "install.json"), components: join(projectBase, "components.json"), archives: join(home, ".marvelous-ui", "archives") };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function readState(file) {
|
|
@@ -114,6 +114,72 @@ function validPack(dir, edition, version) {
|
|
|
114
114
|
if (edition === "free" && !existsSync(join(dir, "LICENSE"))) throw new Error("The Free license is missing from the Pack.");
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
function archiveName(meta) { return `pro-${meta.version}-${meta.sha256}.zip`; }
|
|
118
|
+
|
|
119
|
+
function safeArchiveDirectory(directory) {
|
|
120
|
+
const parent = dirname(directory);
|
|
121
|
+
for (const path of [parent, directory]) {
|
|
122
|
+
try { if (lstatSync(path).isSymbolicLink()) throw new Error("The Pro archive directory cannot be a symbolic link."); }
|
|
123
|
+
catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Keep a verified release outside the project before replacing its installed Pack. */
|
|
128
|
+
export function saveProArchive(bytes, meta, place) {
|
|
129
|
+
validMeta(meta);
|
|
130
|
+
if (bytes.length !== meta.bytes || hash(bytes) !== meta.sha256) throw new Error("The Pro archive failed its size or SHA-256 check.");
|
|
131
|
+
const directory = place.archives ?? join(homedir(), ".marvelous-ui", "archives");
|
|
132
|
+
const target = join(directory, archiveName(meta));
|
|
133
|
+
let temporary;
|
|
134
|
+
try {
|
|
135
|
+
safeArchiveDirectory(directory);
|
|
136
|
+
mkdirSync(directory, { recursive: true });
|
|
137
|
+
const conflicting = readdirSync(directory).find((name) => {
|
|
138
|
+
const match = /^pro-(\d+\.\d+\.\d+)-([a-f0-9]{64})\.zip$/.exec(name);
|
|
139
|
+
return match && match[1] === meta.version && match[2] !== meta.sha256;
|
|
140
|
+
});
|
|
141
|
+
if (conflicting) throw new Error(`Pro Pack ${meta.version} is already saved with a different SHA-256. Pack versions must be immutable.`);
|
|
142
|
+
if (existsSync(target)) {
|
|
143
|
+
if (!lstatSync(target).isFile()) throw new Error("The Pro archive path is not a regular file.");
|
|
144
|
+
const prior = readFileSync(target);
|
|
145
|
+
if (prior.length !== meta.bytes || hash(prior) !== meta.sha256) throw new Error("An existing Pro archive is corrupt. Remove or move it, then retry.");
|
|
146
|
+
return target;
|
|
147
|
+
}
|
|
148
|
+
temporary = join(directory, `.${archiveName(meta)}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`);
|
|
149
|
+
writeFileSync(temporary, bytes, { flag: "wx", mode: 0o600 });
|
|
150
|
+
renameSync(temporary, target);
|
|
151
|
+
return target;
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new Error(`The verified Pro ZIP could not be saved to ${target}: ${error.message}`);
|
|
154
|
+
} finally { if (temporary && existsSync(temporary)) rmSync(temporary, { force: true }); }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function listProArchives(place) {
|
|
158
|
+
const directory = place.archives ?? join(homedir(), ".marvelous-ui", "archives");
|
|
159
|
+
safeArchiveDirectory(directory);
|
|
160
|
+
if (!existsSync(directory)) return [];
|
|
161
|
+
return readdirSync(directory).flatMap((name) => {
|
|
162
|
+
const match = /^pro-(\d+\.\d+\.\d+)-([a-f0-9]{64})\.zip$/.exec(name);
|
|
163
|
+
if (!match) return [];
|
|
164
|
+
const path = join(directory, name);
|
|
165
|
+
if (!lstatSync(path).isFile()) return [];
|
|
166
|
+
return [{ version: match[1], sha256: match[2], path }];
|
|
167
|
+
}).sort((a, b) => b.version.localeCompare(a.version, undefined, { numeric: true }) || a.sha256.localeCompare(b.sha256));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function readProArchive(place, version) {
|
|
171
|
+
if (version && !VERSION.test(version)) throw new Error("Specify a valid Pack version, for example 1.1.0.");
|
|
172
|
+
const matches = listProArchives(place).filter((item) => !version || item.version === version);
|
|
173
|
+
if (!matches.length) throw new Error(`No saved Pro ZIP found${version ? ` for ${version}` : ""}. A new download requires an active key.`);
|
|
174
|
+
if (matches.length !== 1) throw new Error("Several Pro ZIPs match. Specify a unique version or move the duplicate archive.");
|
|
175
|
+
const item = matches[0];
|
|
176
|
+
const bytes = readFileSync(item.path);
|
|
177
|
+
const meta = { version: item.version, sha256: item.sha256, bytes: bytes.length };
|
|
178
|
+
validMeta(meta);
|
|
179
|
+
if (hash(bytes) !== meta.sha256) throw new Error(`The saved Pro ZIP for ${item.version} failed its SHA-256 check.`);
|
|
180
|
+
return { bytes, meta, path: item.path };
|
|
181
|
+
}
|
|
182
|
+
|
|
117
183
|
/** Install a verified ZIP atomically so a failed download or npm install keeps the prior Pack. */
|
|
118
184
|
export function installPack(bytes, edition, meta, place, { installDependencies = true, beforeCommit } = {}) {
|
|
119
185
|
validMeta(meta);
|
|
@@ -125,10 +191,17 @@ export function installPack(bytes, edition, meta, place, { installDependencies =
|
|
|
125
191
|
try {
|
|
126
192
|
extractPack(bytes, stage, `marvelous-ui-${edition}`);
|
|
127
193
|
validPack(stage, edition, meta.version);
|
|
194
|
+
if (edition === "pro") saveProArchive(bytes, meta, place);
|
|
128
195
|
if (installDependencies) {
|
|
129
|
-
const npmArgs = ["ci", "--omit=dev", "--ignore-scripts", "--no-audit"];
|
|
130
|
-
|
|
131
|
-
|
|
196
|
+
const npmArgs = ["ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--silent"];
|
|
197
|
+
try {
|
|
198
|
+
if (process.env.npm_execpath?.endsWith(".js")) execFileSync(process.execPath, [process.env.npm_execpath, ...npmArgs], { cwd: stage, stdio: ["ignore", "pipe", "pipe"] });
|
|
199
|
+
else if (process.platform === "win32") execSync(`npm ${npmArgs.join(" ")}`, { cwd: stage, stdio: ["ignore", "pipe", "pipe"] });
|
|
200
|
+
else execFileSync("npm", npmArgs, { cwd: stage, stdio: ["ignore", "pipe", "pipe"] });
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const detail = String(error.stderr ?? error.message ?? "").trim().split(/\r?\n/).at(-1);
|
|
203
|
+
throw new Error(`The Pack dependencies could not be installed${detail ? `: ${detail}` : "."}`);
|
|
204
|
+
}
|
|
132
205
|
}
|
|
133
206
|
beforeCommit?.(stage);
|
|
134
207
|
if (existsSync(backup)) throw new Error("A previous installation is still being recovered.");
|