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/README.md +34 -0
- package/banner.png +0 -0
- package/bin/manyplug.js +107 -0
- package/config.json +14 -0
- package/docs/manyplug.1 +273 -0
- package/package.json +41 -0
- package/src/enable.js +86 -0
- package/src/init.js +111 -0
- package/src/install.js +204 -0
- package/src/list.js +90 -0
- package/src/registry-ops.js +45 -0
- package/src/remove.js +114 -0
- package/src/sync.js +179 -0
- package/src/ui.js +70 -0
- package/src/utils.js +71 -0
- package/src/validate.js +121 -0
package/src/install.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { execSync } from 'node:child_process';
|
|
5
|
+
import { formatSize } from './ui.js';
|
|
6
|
+
import { loadLocalRegistry, saveRegistry, fetchRemoteRegistry } from './registry-ops.js';
|
|
7
|
+
import { getDirSize, installPluginFromRepo, installNpmDeps } from './utils.js';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const PLUGINS_DIR = path.join(process.cwd(), 'src', 'plugins');
|
|
11
|
+
|
|
12
|
+
// ------------------------------------------------------------
|
|
13
|
+
// helpers
|
|
14
|
+
// ------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
function onMirror(mirror, status, err) {
|
|
17
|
+
console.log(` ${status === 'ok' ? '+' : 'x'} ${mirror.name}${err ? ': ' + err : ''}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function commandExists(cmd) {
|
|
21
|
+
try {
|
|
22
|
+
const check = process.platform === 'win32' ? `where ${cmd}` : `command -v ${cmd}`;
|
|
23
|
+
execSync(check, { stdio: 'pipe' });
|
|
24
|
+
return true;
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Returns { missing: [], optional: [] } based on manifest.externalDependencies
|
|
31
|
+
function checkExternalDeps(manifest) {
|
|
32
|
+
const ext = manifest.externalDependencies;
|
|
33
|
+
if (!ext || !Object.keys(ext).length) return { missing: [], optional: [] };
|
|
34
|
+
|
|
35
|
+
const missing = [], optional = [];
|
|
36
|
+
|
|
37
|
+
for (const [name, cfg] of Object.entries(ext)) {
|
|
38
|
+
if (commandExists(typeof cfg === 'string' ? cfg : cfg.command)) continue;
|
|
39
|
+
(cfg?.optional ? optional : missing).push(name);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { missing, optional };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function reportExternalDeps(manifest) {
|
|
46
|
+
const { missing, optional } = checkExternalDeps(manifest);
|
|
47
|
+
if (missing.length) console.warn(`warn: missing external deps: ${missing.join(', ')}`);
|
|
48
|
+
if (optional.length) console.log(`info: optional deps not found: ${optional.join(', ')}`);
|
|
49
|
+
return { missing, optional };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function installDeps(manifest, targetDir) {
|
|
53
|
+
const deps = manifest.dependencies;
|
|
54
|
+
if (!deps || !Object.keys(deps).length) return;
|
|
55
|
+
console.log(' installing npm deps...');
|
|
56
|
+
await installNpmDeps(deps, targetDir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function registerPlugin(pluginName, manifest, extra = {}) {
|
|
60
|
+
const registry = await loadLocalRegistry();
|
|
61
|
+
registry.plugins[pluginName] = { ...manifest, ...extra };
|
|
62
|
+
await saveRegistry(registry);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ------------------------------------------------------------
|
|
66
|
+
// install from local path
|
|
67
|
+
// ------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
async function installFromLocal(sourcePath) {
|
|
70
|
+
const src = path.resolve(sourcePath);
|
|
71
|
+
|
|
72
|
+
if (!await fs.pathExists(src))
|
|
73
|
+
return { success: false, error: `path not found: ${src}` };
|
|
74
|
+
|
|
75
|
+
const manifestPath = path.join(src, 'manyplug.json');
|
|
76
|
+
if (!await fs.pathExists(manifestPath))
|
|
77
|
+
return { success: false, error: 'manyplug.json not found' };
|
|
78
|
+
|
|
79
|
+
let manifest;
|
|
80
|
+
try { manifest = await fs.readJson(manifestPath); }
|
|
81
|
+
catch (e) { return { success: false, error: `invalid manyplug.json: ${e.message}` }; }
|
|
82
|
+
|
|
83
|
+
const name = manifest.name || path.basename(src);
|
|
84
|
+
const dest = path.join(PLUGINS_DIR, name);
|
|
85
|
+
|
|
86
|
+
if (await fs.pathExists(dest))
|
|
87
|
+
return { success: false, error: `${name} already installed` };
|
|
88
|
+
|
|
89
|
+
const size = await getDirSize(src);
|
|
90
|
+
console.log(`installing ${name} src=${src} size=${formatSize(size)}`);
|
|
91
|
+
reportExternalDeps(manifest);
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await fs.ensureDir(PLUGINS_DIR);
|
|
95
|
+
await fs.copy(src, dest);
|
|
96
|
+
await installDeps(manifest, dest);
|
|
97
|
+
await registerPlugin(name, manifest, { local: true });
|
|
98
|
+
return { success: true, plugin: name, size };
|
|
99
|
+
} catch (e) {
|
|
100
|
+
return { success: false, error: e.message, plugin: name };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ------------------------------------------------------------
|
|
105
|
+
// install single plugin from remote registry
|
|
106
|
+
// ------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
async function installFromRegistry(pluginName, manifest, mirror) {
|
|
109
|
+
const t = Date.now();
|
|
110
|
+
const dest = path.join(PLUGINS_DIR, pluginName);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const { size } = await installPluginFromRepo({ plugin: pluginName, repo: mirror }, PLUGINS_DIR);
|
|
114
|
+
await installDeps(manifest, dest);
|
|
115
|
+
await registerPlugin(pluginName, manifest);
|
|
116
|
+
return { success: true, plugin: pluginName, size, duration: Date.now() - t };
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return { success: false, error: e.message, plugin: pluginName };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ------------------------------------------------------------
|
|
123
|
+
// install command (entry point)
|
|
124
|
+
// ------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
export async function installCommand(pluginsInput, options = {}) {
|
|
127
|
+
const t = Date.now();
|
|
128
|
+
const names = Array.isArray(pluginsInput) ? pluginsInput : (pluginsInput ? [pluginsInput] : []);
|
|
129
|
+
|
|
130
|
+
await fs.ensureDir(PLUGINS_DIR);
|
|
131
|
+
|
|
132
|
+
// -- local install --
|
|
133
|
+
if (options.local) {
|
|
134
|
+
const r = await installFromLocal(options.local);
|
|
135
|
+
if (!r.success) { console.error(r.error); process.exit(1); }
|
|
136
|
+
console.log(`installed ${r.plugin} size=${formatSize(r.size)} time=${elapsed(t)}s`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!names.length) { console.error('usage: manyplug install <plugin>'); process.exit(1); }
|
|
141
|
+
|
|
142
|
+
// -- fetch remote registry --
|
|
143
|
+
process.stdout.write('fetching registry... ');
|
|
144
|
+
let remoteRegistry, mirror;
|
|
145
|
+
try {
|
|
146
|
+
({ remoteRegistry, selectedMirror: mirror } = await fetchRemoteRegistry(onMirror));
|
|
147
|
+
mirror = mirror.git;
|
|
148
|
+
console.log('ok');
|
|
149
|
+
} catch (e) {
|
|
150
|
+
console.error(`failed: ${e.message}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// -- classify plugins --
|
|
155
|
+
const toInstall = [], toReinstall = [], notFound = [];
|
|
156
|
+
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
const manifest = remoteRegistry.plugins[name];
|
|
159
|
+
if (!manifest) { notFound.push(name); continue; }
|
|
160
|
+
|
|
161
|
+
const installed = await fs.pathExists(path.join(PLUGINS_DIR, name));
|
|
162
|
+
const entry = { name, version: manifest.version, manifest };
|
|
163
|
+
|
|
164
|
+
if (installed && !options.needed) toReinstall.push(entry);
|
|
165
|
+
else if (!installed) toInstall.push(entry);
|
|
166
|
+
// installed + --needed => skip (nothing pushed)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (notFound.length) console.error(`not found: ${notFound.join(', ')}`);
|
|
170
|
+
|
|
171
|
+
const queue = [...toInstall, ...toReinstall];
|
|
172
|
+
if (!queue.length) { console.log('nothing to do'); process.exit(0); }
|
|
173
|
+
|
|
174
|
+
// -- print plan --
|
|
175
|
+
for (const p of toInstall) console.log(`+ ${p.name}@${p.version}`);
|
|
176
|
+
for (const p of toReinstall) console.log(`~ ${p.name}@${p.version} (reinstall)`);
|
|
177
|
+
|
|
178
|
+
// -- remove stale installs --
|
|
179
|
+
for (const p of toReinstall) {
|
|
180
|
+
await fs.remove(path.join(PLUGINS_DIR, p.name));
|
|
181
|
+
const registry = await loadLocalRegistry();
|
|
182
|
+
delete registry.plugins[p.name];
|
|
183
|
+
await saveRegistry(registry);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// -- run queue --
|
|
187
|
+
const results = [];
|
|
188
|
+
for (const p of queue) {
|
|
189
|
+
process.stdout.write(`installing ${p.name}... `);
|
|
190
|
+
const r = await installFromRegistry(p.name, p.manifest, mirror);
|
|
191
|
+
results.push(r);
|
|
192
|
+
console.log(r.success ? 'done' : `FAILED${r.error ? ': ' + r.error : ''}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// -- summary --
|
|
196
|
+
const ok = results.filter(r => r.success).length;
|
|
197
|
+
const bad = results.length - ok;
|
|
198
|
+
console.log(`\n${ok}/${results.length} installed in ${elapsed(t)}s`);
|
|
199
|
+
if (bad) process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
function elapsed(since) { return ((Date.now() - since) / 1000).toFixed(2); }
|
package/src/list.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
const CONF_PATH = path.resolve(process.cwd(), 'manybot.conf');
|
|
6
|
+
const PLUGINS_DIR = path.join(process.cwd(), 'src', 'plugins');
|
|
7
|
+
|
|
8
|
+
// ------------------------------------------------------------
|
|
9
|
+
// conf — reuse same parser as enable-disable reads
|
|
10
|
+
// ------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
function readEnabled() {
|
|
13
|
+
if (!existsSync(CONF_PATH)) return new Set();
|
|
14
|
+
const match = readFileSync(CONF_PATH, 'utf-8').match(/PLUGINS=\[\s*([\s\S]*?)\s*\]/);
|
|
15
|
+
if (!match) return new Set();
|
|
16
|
+
return new Set(match[1].split(',').map(s => s.trim().toLowerCase()).filter(Boolean));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// ------------------------------------------------------------
|
|
20
|
+
// list command
|
|
21
|
+
// ------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export async function listCommand(options = {}) {
|
|
24
|
+
if (!await fs.pathExists(PLUGINS_DIR)) {
|
|
25
|
+
console.error(`error: plugins dir not found: ${PLUGINS_DIR}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const enabled = readEnabled();
|
|
30
|
+
const entries = (await fs.readdir(PLUGINS_DIR, { withFileTypes: true })).filter(e => e.isDirectory());
|
|
31
|
+
const plugins = [];
|
|
32
|
+
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const dir = path.join(PLUGINS_DIR, entry.name);
|
|
35
|
+
const manifestPath = path.join(dir, 'manyplug.json');
|
|
36
|
+
const hasEntry = await fs.pathExists(path.join(dir, 'index.js'));
|
|
37
|
+
const isEnabled = enabled.has(entry.name.toLowerCase());
|
|
38
|
+
|
|
39
|
+
if (!isEnabled && !options.all) continue;
|
|
40
|
+
|
|
41
|
+
let manifest = {};
|
|
42
|
+
try { manifest = await fs.readJson(manifestPath); }
|
|
43
|
+
catch { manifest = { name: entry.name, version: '?', category: '?', _error: true }; }
|
|
44
|
+
|
|
45
|
+
plugins.push({
|
|
46
|
+
name: manifest.name || entry.name,
|
|
47
|
+
version: manifest.version || '-',
|
|
48
|
+
category: manifest.category || '-',
|
|
49
|
+
service: manifest.service === true,
|
|
50
|
+
local: manifest.local === true,
|
|
51
|
+
enabled: isEnabled,
|
|
52
|
+
hasEntry,
|
|
53
|
+
_error: manifest._error || false,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!plugins.length) {
|
|
58
|
+
console.log(options.all ? 'no plugins installed' : 'no enabled plugins (use --all to see all)');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// column widths
|
|
63
|
+
const w = {
|
|
64
|
+
name: Math.max(4, ...plugins.map(p => p.name.length)),
|
|
65
|
+
version: Math.max(7, ...plugins.map(p => p.version.length)),
|
|
66
|
+
category: Math.max(8, ...plugins.map(p => p.category.length)),
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const pad = (s, n) => s.padEnd(n);
|
|
70
|
+
const header = ` ${'name'.padEnd(w.name)} ${'version'.padEnd(w.version)} ${'category'.padEnd(w.category)} type status`;
|
|
71
|
+
console.log(header);
|
|
72
|
+
console.log(' ' + '-'.repeat(header.length - 2));
|
|
73
|
+
|
|
74
|
+
for (const p of plugins) {
|
|
75
|
+
const flag = p.local ? 'L' : p._error ? '!' : ' ';
|
|
76
|
+
const type = p.service ? 'svc' : 'std';
|
|
77
|
+
const status = !p.hasEntry ? 'incomplete' : p.enabled ? 'enabled' : 'disabled';
|
|
78
|
+
console.log(`${flag} ${pad(p.name, w.name)} ${pad(p.version, w.version)} ${pad(p.category, w.category)} ${pad(type, 4)} ${status}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// summary
|
|
82
|
+
const en = plugins.filter(p => p.enabled).length;
|
|
83
|
+
const dis = plugins.length - en;
|
|
84
|
+
const loc = plugins.filter(p => p.local).length;
|
|
85
|
+
const inc = plugins.filter(p => !p.hasEntry).length;
|
|
86
|
+
|
|
87
|
+
console.log('');
|
|
88
|
+
console.log(`total=${plugins.length} enabled=${en} disabled=${dis}${loc ? ' local=' + loc : ''}${inc ? ' incomplete=' + inc : ''}`);
|
|
89
|
+
console.log('L=local svc=service std=standard !=missing index.js');
|
|
90
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const REGISTRY_PATH = path.join(process.cwd(), 'registry.json');
|
|
7
|
+
const CONFIG_PATH = path.join(__dirname, '..', 'config.json');
|
|
8
|
+
|
|
9
|
+
// ------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
function loadConfig() {
|
|
12
|
+
try { return fs.readJsonSync(CONFIG_PATH); }
|
|
13
|
+
catch (e) { console.error(`error: config.json: ${e.message}`); process.exit(1); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const MIRRORS = loadConfig().mirrors;
|
|
17
|
+
|
|
18
|
+
// ------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export async function loadLocalRegistry() {
|
|
21
|
+
try { return await fs.readJson(REGISTRY_PATH); }
|
|
22
|
+
catch { return { lastUpdated: null, plugins: {} }; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function saveRegistry(registry) {
|
|
26
|
+
registry.lastUpdated = new Date().toISOString();
|
|
27
|
+
await fs.writeJson(REGISTRY_PATH, registry, { spaces: 2 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
// onMirror(mirror, 'ok'|'fail', errMsg?) — optional progress callback
|
|
33
|
+
export async function fetchRemoteRegistry(onMirror = () => {}) {
|
|
34
|
+
for (const mirror of MIRRORS) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(`${mirror.fetch}/registry.json`);
|
|
37
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
38
|
+
onMirror(mirror, 'ok');
|
|
39
|
+
return { remoteRegistry: await res.json(), selectedMirror: mirror };
|
|
40
|
+
} catch (e) {
|
|
41
|
+
onMirror(mirror, 'fail', e.message);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw new Error('all mirrors failed');
|
|
45
|
+
}
|
package/src/remove.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { exec } from 'node:child_process';
|
|
4
|
+
import { formatSize } from './ui.js';
|
|
5
|
+
import { loadLocalRegistry, saveRegistry } from './registry-ops.js';
|
|
6
|
+
|
|
7
|
+
const PLUGINS_DIR = path.join(process.cwd(), 'src', 'plugins');
|
|
8
|
+
|
|
9
|
+
// ------------------------------------------------------------
|
|
10
|
+
// helpers
|
|
11
|
+
// ------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
function elapsed(since) { return ((Date.now() - since) / 1000).toFixed(2); }
|
|
14
|
+
|
|
15
|
+
function run(cmd, cwd) {
|
|
16
|
+
return new Promise((res, rej) =>
|
|
17
|
+
exec(cmd, { cwd }, (err, stdout) => err ? rej(err) : res(stdout))
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function getDirSize(dir) {
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
|
|
24
|
+
const p = path.join(dir, entry.name);
|
|
25
|
+
total += entry.isDirectory() ? await getDirSize(p) : (await fs.stat(p)).size;
|
|
26
|
+
}
|
|
27
|
+
return total;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function ask(prompt) {
|
|
31
|
+
process.stdout.write(prompt);
|
|
32
|
+
return new Promise(res => process.stdin.once('data', d => res(d.toString().trim().toLowerCase())));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ------------------------------------------------------------
|
|
36
|
+
// remove command
|
|
37
|
+
// ------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
export async function removeCommand(input, options = {}) {
|
|
40
|
+
const t = Date.now();
|
|
41
|
+
const names = Array.isArray(input) ? input : (input ? [input] : []);
|
|
42
|
+
|
|
43
|
+
if (!names.length) {
|
|
44
|
+
console.error('usage: manyplug remove <plugin> [plugin2...]');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const results = [];
|
|
49
|
+
|
|
50
|
+
for (const name of names) {
|
|
51
|
+
const dir = path.join(PLUGINS_DIR, name);
|
|
52
|
+
const manifestPath = path.join(dir, 'manyplug.json');
|
|
53
|
+
|
|
54
|
+
if (!await fs.pathExists(manifestPath)) {
|
|
55
|
+
console.error(`x ${name}: not installed`);
|
|
56
|
+
results.push({ name, success: false });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let manifest = {};
|
|
61
|
+
try { manifest = await fs.readJson(manifestPath); } catch {}
|
|
62
|
+
|
|
63
|
+
const size = await getDirSize(dir);
|
|
64
|
+
const deps = manifest.dependencies || {};
|
|
65
|
+
const hasDeps = Object.keys(deps).length > 0;
|
|
66
|
+
|
|
67
|
+
console.log(`- ${name}@${manifest.version || '?'} size=${formatSize(size)} path=${path.relative(process.cwd(), dir)}`);
|
|
68
|
+
if (hasDeps) console.log(` deps: ${Object.keys(deps).join(', ')}`);
|
|
69
|
+
|
|
70
|
+
if (!options.yes) {
|
|
71
|
+
const answer = await ask('remove? [y/N] ');
|
|
72
|
+
if (answer !== 'y') {
|
|
73
|
+
console.log(` skipped`);
|
|
74
|
+
results.push({ name, success: false, skipped: true });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
await fs.remove(dir);
|
|
81
|
+
|
|
82
|
+
const registry = await loadLocalRegistry();
|
|
83
|
+
delete registry.plugins[name];
|
|
84
|
+
await saveRegistry(registry);
|
|
85
|
+
|
|
86
|
+
if (hasDeps && options.removeDeps) {
|
|
87
|
+
process.stdout.write(` uninstalling npm deps... `);
|
|
88
|
+
try {
|
|
89
|
+
await run(`npm uninstall ${Object.keys(deps).join(' ')}`, process.cwd());
|
|
90
|
+
console.log('ok');
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.log(`warn: ${e.message}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(` done freed=${formatSize(size)}`);
|
|
97
|
+
results.push({ name, success: true, size });
|
|
98
|
+
} catch (e) {
|
|
99
|
+
console.error(` FAILED: ${e.message}`);
|
|
100
|
+
results.push({ name, success: false, error: e.message });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// summary
|
|
105
|
+
const ok = results.filter(r => r.success).length;
|
|
106
|
+
const bad = results.length - ok;
|
|
107
|
+
const freed = results.reduce((a, r) => a + (r.size || 0), 0);
|
|
108
|
+
|
|
109
|
+
if (names.length > 1) {
|
|
110
|
+
console.log(`\n${ok}/${names.length} removed freed=${formatSize(freed)} time=${elapsed(t)}s`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (bad && !results.every(r => r.skipped)) process.exit(1);
|
|
114
|
+
}
|
package/src/sync.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { formatSize } from './ui.js';
|
|
5
|
+
import { loadLocalRegistry, saveRegistry, fetchRemoteRegistry } from './registry-ops.js';
|
|
6
|
+
import { installPluginFromRepo, installNpmDeps } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PLUGINS_DIR = path.join(process.cwd(), 'src', 'plugins');
|
|
10
|
+
|
|
11
|
+
// ------------------------------------------------------------
|
|
12
|
+
// helpers
|
|
13
|
+
// ------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function elapsed(since) { return ((Date.now() - since) / 1000).toFixed(2); }
|
|
16
|
+
|
|
17
|
+
function onMirror(mirror, status, err) {
|
|
18
|
+
console.log(` ${status === 'ok' ? '+' : 'x'} ${mirror.name}${err ? ': ' + err : ''}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function fetchRegistry() {
|
|
22
|
+
process.stdout.write('fetching registry... ');
|
|
23
|
+
try {
|
|
24
|
+
const r = await fetchRemoteRegistry(onMirror);
|
|
25
|
+
console.log('ok');
|
|
26
|
+
return r;
|
|
27
|
+
} catch (e) {
|
|
28
|
+
console.error(`failed: ${e.message}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function installDeps(manifest, targetDir) {
|
|
34
|
+
const deps = manifest.dependencies;
|
|
35
|
+
if (!deps || !Object.keys(deps).length) return;
|
|
36
|
+
await installNpmDeps(deps, targetDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ------------------------------------------------------------
|
|
40
|
+
// sync command — reconcile local registry with remote
|
|
41
|
+
// ------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
export async function syncCommand(options = {}) {
|
|
44
|
+
const t = Date.now();
|
|
45
|
+
const { remoteRegistry, selectedMirror } = await fetchRegistry();
|
|
46
|
+
const local = await loadLocalRegistry();
|
|
47
|
+
|
|
48
|
+
const synced = {};
|
|
49
|
+
const added = [], updated = [], kept = [], localOnly = [], skipped = [];
|
|
50
|
+
|
|
51
|
+
// reconcile local plugins against remote
|
|
52
|
+
for (const [name, lm] of Object.entries(local.plugins || {})) {
|
|
53
|
+
if (lm.local) {
|
|
54
|
+
localOnly.push(name);
|
|
55
|
+
synced[name] = lm;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const rm = remoteRegistry.plugins?.[name];
|
|
59
|
+
if (rm && lm.version !== rm.version) {
|
|
60
|
+
updated.push(`${name} ${lm.version}->${rm.version}`);
|
|
61
|
+
synced[name] = rm;
|
|
62
|
+
} else {
|
|
63
|
+
kept.push(name);
|
|
64
|
+
synced[name] = lm;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// plugins in remote not installed locally
|
|
69
|
+
for (const [name, rm] of Object.entries(remoteRegistry.plugins || {})) {
|
|
70
|
+
if (!local.plugins?.[name]) skipped.push(name);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// plugins on disk not in registry
|
|
74
|
+
if (await fs.pathExists(PLUGINS_DIR)) {
|
|
75
|
+
for (const entry of await fs.readdir(PLUGINS_DIR, { withFileTypes: true })) {
|
|
76
|
+
if (!entry.isDirectory() || synced[entry.name]) continue;
|
|
77
|
+
const mp = path.join(PLUGINS_DIR, entry.name, 'manyplug.json');
|
|
78
|
+
if (!await fs.pathExists(mp)) continue;
|
|
79
|
+
try {
|
|
80
|
+
const manifest = await fs.readJson(mp);
|
|
81
|
+
added.push(entry.name);
|
|
82
|
+
synced[entry.name] = manifest;
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// summary
|
|
88
|
+
if (added.length) console.log(`+ added: ${added.join(', ')}`);
|
|
89
|
+
if (updated.length) console.log(`~ updated: ${updated.join(', ')}`);
|
|
90
|
+
if (kept.length) console.log(`= kept: ${kept.length} plugin(s)`);
|
|
91
|
+
if (localOnly.length) console.log(`L local: ${localOnly.join(', ')}`);
|
|
92
|
+
if (skipped.length) console.log(`- skipped: ${skipped.length} not installed`);
|
|
93
|
+
|
|
94
|
+
console.log(` source: ${selectedMirror.name}`);
|
|
95
|
+
console.log(` synced: ${Object.keys(synced).length} plugin(s)`);
|
|
96
|
+
|
|
97
|
+
const hasChanges = added.length || updated.length;
|
|
98
|
+
if (hasChanges || options.force) {
|
|
99
|
+
process.stdout.write('saving registry... ');
|
|
100
|
+
try {
|
|
101
|
+
await saveRegistry({ plugins: synced });
|
|
102
|
+
console.log('ok');
|
|
103
|
+
} catch (e) {
|
|
104
|
+
console.error(`failed: ${e.message}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
console.log(`done in ${elapsed(t)}s`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ------------------------------------------------------------
|
|
113
|
+
// update command — install/update all plugins from remote
|
|
114
|
+
// ------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
async function applyPlugin(name, manifest, mirror, isUpdate) {
|
|
117
|
+
const dest = path.join(PLUGINS_DIR, name);
|
|
118
|
+
process.stdout.write(`${isUpdate ? 'updating' : 'installing'} ${name}... `);
|
|
119
|
+
try {
|
|
120
|
+
if (isUpdate && await fs.pathExists(dest)) await fs.remove(dest);
|
|
121
|
+
const { size } = await installPluginFromRepo({ plugin: name, repo: mirror }, PLUGINS_DIR);
|
|
122
|
+
await installDeps(manifest, dest);
|
|
123
|
+
const registry = await loadLocalRegistry();
|
|
124
|
+
registry.plugins[name] = manifest;
|
|
125
|
+
await saveRegistry(registry);
|
|
126
|
+
console.log(`done (${formatSize(size)})`);
|
|
127
|
+
return { success: true, name, size };
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.log(`FAILED: ${e.message}`);
|
|
130
|
+
return { success: false, name, error: e.message };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function updateCommand(options = {}) {
|
|
135
|
+
const t = Date.now();
|
|
136
|
+
const { remoteRegistry, selectedMirror } = await fetchRegistry();
|
|
137
|
+
const local = await loadLocalRegistry();
|
|
138
|
+
|
|
139
|
+
const toInstall = [], toUpdate = [], localOnly = [];
|
|
140
|
+
|
|
141
|
+
for (const [name, rm] of Object.entries(remoteRegistry.plugins || {})) {
|
|
142
|
+
const onDisk = await fs.pathExists(path.join(PLUGINS_DIR, name));
|
|
143
|
+
if (!onDisk) continue; // not installed locally, skip
|
|
144
|
+
const lm = local.plugins?.[name];
|
|
145
|
+
if (lm?.version !== rm.version) toUpdate.push({ name, manifest: rm, from: lm?.version ?? '?', to: rm.version });
|
|
146
|
+
}
|
|
147
|
+
for (const name of Object.keys(local.plugins || {})) {
|
|
148
|
+
if (!remoteRegistry.plugins?.[name]) localOnly.push(name);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// plan
|
|
152
|
+
for (const p of toUpdate) console.log(`~ ${p.name} ${p.from}->${p.to}`);
|
|
153
|
+
if (localOnly.length) console.log(`L local only (skipped): ${localOnly.join(', ')}`);
|
|
154
|
+
|
|
155
|
+
const total = toUpdate.length;
|
|
156
|
+
if (!total) { console.log(`nothing to do (${elapsed(t)}s)`); return; }
|
|
157
|
+
|
|
158
|
+
// confirm
|
|
159
|
+
if (!options.yes) {
|
|
160
|
+
process.stdout.write(`${total} plugin(s) will be updated, continue? [y/N] `);
|
|
161
|
+
const answer = await new Promise(res => {
|
|
162
|
+
process.stdin.once('data', d => res(d.toString().trim().toLowerCase()));
|
|
163
|
+
});
|
|
164
|
+
if (answer !== 'y') { console.log('cancelled'); process.exit(0); }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// run
|
|
168
|
+
const results = [];
|
|
169
|
+
const mirror = selectedMirror.git;
|
|
170
|
+
for (const p of toUpdate) results.push(await applyPlugin(p.name, p.manifest, mirror, true));
|
|
171
|
+
|
|
172
|
+
// summary
|
|
173
|
+
const ok = results.filter(r => r.success).length;
|
|
174
|
+
const bad = results.length - ok;
|
|
175
|
+
const totalSize = results.reduce((a, r) => a + (r.size || 0), 0);
|
|
176
|
+
|
|
177
|
+
console.log(`\n${ok}/${results.length} updated in ${elapsed(t)}s size=${formatSize(totalSize)}`);
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|