manyplug 1.3.0

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/src/ui.js ADDED
@@ -0,0 +1,70 @@
1
+ import ora from 'ora';
2
+ import chalk from 'chalk';
3
+ import readline from 'node:readline';
4
+
5
+ // ------------------------------------------------------------
6
+ // format utils (used by install, remove, init, sync-update)
7
+ // ------------------------------------------------------------
8
+
9
+ export function formatSize(bytes) {
10
+ if (!bytes) return '0 B';
11
+ const units = ['B', 'KB', 'MB', 'GB'];
12
+ let i = 0, n = bytes;
13
+ while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
14
+ return `${n.toFixed(2)} ${units[i]}`;
15
+ }
16
+
17
+ export function formatDuration(ms) {
18
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
19
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
20
+ return `${(ms / 60000).toFixed(1)}m`;
21
+ }
22
+
23
+ // ------------------------------------------------------------
24
+ // spinner (used by sync-update, install)
25
+ // ------------------------------------------------------------
26
+
27
+ export function createSpinner(text) {
28
+ const sp = ora({
29
+ text,
30
+ spinner: { frames: ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'], interval: 80 },
31
+ color: 'cyan'
32
+ });
33
+ return {
34
+ start: (t) => { if (t) sp.text = t; sp.start(); },
35
+ succeed: (t) => sp.succeed(t),
36
+ fail: (t) => sp.fail(t),
37
+ warn: (t) => sp.warn(t),
38
+ stop: () => sp.stop(),
39
+ setText: (t) => { sp.text = t; }
40
+ };
41
+ }
42
+
43
+ // ------------------------------------------------------------
44
+ // confirm prompt (used by sync-update, remove)
45
+ // ------------------------------------------------------------
46
+
47
+ export function confirm(message, defaultValue = true) {
48
+ const hint = defaultValue ? '[Y/n]' : '[y/N]';
49
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
50
+ const ask = () => new Promise(res => rl.question(`${message} ${hint} `, res));
51
+
52
+ return (async () => {
53
+ while (true) {
54
+ const a = (await ask()).trim().toLowerCase();
55
+ if (a === '') { rl.close(); return defaultValue; }
56
+ if (['y','yes'].includes(a)) { rl.close(); return true; }
57
+ if (['n','no'].includes(a)) { rl.close(); return false; }
58
+ console.log(' please answer y or n');
59
+ }
60
+ })();
61
+ }
62
+
63
+ // ------------------------------------------------------------
64
+ // mirror status line (used by registry-ops callback)
65
+ // ------------------------------------------------------------
66
+
67
+ export function mirrorLine(mirror, status, err) {
68
+ const icon = status === 'ok' ? chalk.green('+') : chalk.red('x');
69
+ console.log(` ${icon} ${mirror.name}${err ? ': ' + err : ''}`);
70
+ }
package/src/utils.js ADDED
@@ -0,0 +1,71 @@
1
+ import { exec } from 'node:child_process';
2
+ import fs from 'fs-extra';
3
+ import path from 'path';
4
+ import os from 'node:os';
5
+ import { formatSize } from './ui.js';
6
+
7
+ // ------------------------------------------------------------
8
+
9
+ export function run(cmd, cwd) {
10
+ return new Promise((res, rej) =>
11
+ exec(cmd, { cwd }, (err, stdout, stderr) => {
12
+ if (err) { err.stderr = stderr; return rej(err); }
13
+ res(stdout);
14
+ })
15
+ );
16
+ }
17
+
18
+ export async function getDirSize(dir) {
19
+ let total = 0;
20
+ for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
21
+ const p = path.join(dir, entry.name);
22
+ total += entry.isDirectory() ? await getDirSize(p) : (await fs.stat(p)).size;
23
+ }
24
+ return total;
25
+ }
26
+
27
+ // sparse-clones a single plugin dir from a git repo into pluginsDir
28
+ export async function installPluginFromRepo({ plugin, repo }, pluginsDir) {
29
+ const tmp = path.join(os.tmpdir(), `manyplug-${Date.now()}`);
30
+ const repoDir = path.join(tmp, 'repo');
31
+ await fs.mkdir(tmp, { recursive: true });
32
+
33
+ try {
34
+ process.stdout.write(` cloning ${repo}... `);
35
+ await run(`git clone --filter=blob:none --no-checkout ${repo} ${repoDir}`);
36
+ await run(`git sparse-checkout init --cone`, repoDir);
37
+ await run(`git sparse-checkout set ${plugin}`, repoDir);
38
+ await run(`git checkout`, repoDir);
39
+ console.log('ok');
40
+
41
+ const src = path.join(repoDir, plugin);
42
+ const dest = path.join(pluginsDir, plugin);
43
+
44
+ if (!await fs.pathExists(src))
45
+ throw new Error(`plugin "${plugin}" not found in repo`);
46
+
47
+ const size = await getDirSize(src);
48
+ process.stdout.write(` copying ${formatSize(size)}... `);
49
+ await fs.mkdir(pluginsDir, { recursive: true });
50
+ await fs.cp(src, dest, { recursive: true });
51
+ console.log('ok');
52
+
53
+ return { finalPath: dest, size };
54
+ } finally {
55
+ await fs.rm(tmp, { recursive: true, force: true });
56
+ }
57
+ }
58
+
59
+ export async function installNpmDeps(deps, pluginDir) {
60
+ const list = Object.entries(deps)
61
+ .map(([n, v]) => `${n}@${v === '*' ? 'latest' : v}`)
62
+ .join(' ');
63
+ if (!list) return;
64
+ process.stdout.write(` npm install ${list}... `);
65
+ try {
66
+ await run(`npm install ${list}`, pluginDir);
67
+ console.log('ok');
68
+ } catch (e) {
69
+ console.warn(`warn: npm install failed: ${e.message}`);
70
+ }
71
+ }
@@ -0,0 +1,121 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'path';
3
+ import { execSync } from 'node:child_process';
4
+
5
+ const VALID_CATEGORIES = ['games', 'media', 'utility', 'service', 'admin', 'fun'];
6
+
7
+ // ------------------------------------------------------------
8
+ // rules — each returns an error string or null
9
+ // ------------------------------------------------------------
10
+
11
+ const RULES = {
12
+ name: v => typeof v !== 'string' || !v ? 'required string'
13
+ : !/^[a-z0-9-]+$/.test(v) ? 'lowercase letters, numbers, hyphens only'
14
+ : v.length < 2 || v.length > 50 ? 'length must be 2-50'
15
+ : null,
16
+
17
+ version: v => typeof v !== 'string' || !v ? 'required string'
18
+ : !/^\d+\.\d+\.\d+/.test(v) ? 'must be semver (e.g. 1.0.0)'
19
+ : null,
20
+
21
+ category: v => !VALID_CATEGORIES.includes(v) ? `must be one of: ${VALID_CATEGORIES.join(', ')}`
22
+ : null,
23
+
24
+ service: v => v !== undefined && typeof v !== 'boolean' ? 'must be boolean' : null,
25
+ local: v => v !== undefined && typeof v !== 'boolean' ? 'must be boolean' : null,
26
+ main: v => v !== undefined && typeof v !== 'string' ? 'must be string' : null,
27
+
28
+ dependencies: v => v !== undefined && typeof v !== 'object' ? 'must be object' : null,
29
+ externalDependencies: v => {
30
+ if (v === undefined) return null;
31
+ if (typeof v !== 'object') return 'must be object';
32
+ for (const [n, c] of Object.entries(v)) {
33
+ if (typeof c === 'string') continue;
34
+ if (typeof c !== 'object') return `${n}: must be string or object`;
35
+ if (c.command !== undefined && typeof c.command !== 'string') return `${n}.command must be string`;
36
+ if (c.optional !== undefined && typeof c.optional !== 'boolean') return `${n}.optional must be boolean`;
37
+ }
38
+ return null;
39
+ }
40
+ };
41
+
42
+ const REQUIRED = ['name', 'version', 'category'];
43
+ const KNOWN = new Set([...REQUIRED, 'service', 'local', 'description', 'author', 'license', 'main', 'dependencies', 'externalDependencies']);
44
+
45
+ function commandExists(cmd) {
46
+ try {
47
+ execSync(process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`, { stdio: 'pipe' });
48
+ return true;
49
+ } catch { return false; }
50
+ }
51
+
52
+ // ------------------------------------------------------------
53
+ // validate command
54
+ // ------------------------------------------------------------
55
+
56
+ export async function validateCommand(pluginPath = '.') {
57
+ const abs = path.resolve(pluginPath);
58
+
59
+ if (!await fs.pathExists(abs)) {
60
+ console.error(`error: path not found: ${pluginPath}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ const manifestPath = path.join(abs, 'manyplug.json');
65
+ if (!await fs.pathExists(manifestPath)) {
66
+ console.error(`error: manyplug.json not found in ${pluginPath}`);
67
+ console.error(' hint: run "manyplug init <name>" to scaffold a plugin');
68
+ process.exit(1);
69
+ }
70
+
71
+ let manifest;
72
+ try { manifest = await fs.readJson(manifestPath); }
73
+ catch (e) { console.error(`error: invalid manyplug.json: ${e.message}`); process.exit(1); }
74
+
75
+ const errors = [], warnings = [];
76
+ const err = (field, msg) => errors.push(` error ${field.padEnd(24)} ${msg}`);
77
+ const warn = (field, msg) => warnings.push(` warning ${field.padEnd(24)} ${msg}`);
78
+
79
+ // required fields
80
+ for (const f of REQUIRED)
81
+ if (!(f in manifest)) err(f, 'missing required field');
82
+
83
+ // field rules
84
+ for (const [f, v] of Object.entries(manifest)) {
85
+ if (!KNOWN.has(f)) { warn(f, 'unknown field'); continue; }
86
+ const msg = RULES[f]?.(v);
87
+ if (msg) err(f, msg);
88
+ }
89
+
90
+ // entry point
91
+ const main = manifest.main || 'index.js';
92
+ if (!await fs.pathExists(path.join(abs, main)))
93
+ warn('main', `entry point not found: ${main}`);
94
+
95
+ // locale
96
+ if (!await fs.pathExists(path.join(abs, 'locale')))
97
+ warn('locale', 'no locale/ directory (i18n recommended)');
98
+
99
+ // external deps
100
+ for (const [n, c] of Object.entries(manifest.externalDependencies || {})) {
101
+ const cmd = typeof c === 'string' ? c : c.command;
102
+ const opt = typeof c === 'object' && c.optional;
103
+ if (!commandExists(cmd))
104
+ (opt ? warn : err)(`externalDeps.${n}`, `command not found: ${cmd}`);
105
+ }
106
+
107
+ // output
108
+ const name = manifest.name || path.basename(abs);
109
+ console.log(`${name}@${manifest.version || '?'} path=${abs}`);
110
+
111
+ if (errors.length || warnings.length) {
112
+ if (errors.length) console.log('\n' + errors.join('\n'));
113
+ if (warnings.length) console.log('\n' + warnings.join('\n'));
114
+ } else {
115
+ console.log('ok all checks passed');
116
+ }
117
+
118
+ console.log(`\nerrors=${errors.length} warnings=${warnings.length}`);
119
+
120
+ if (errors.length) process.exit(1);
121
+ }