m3 install

This commit is contained in:
Dinhero21 2024-09-11 20:48:17 -03:00
parent 48b0615aaa
commit 03044d2a66
5 changed files with 266 additions and 8 deletions

View file

@ -8,7 +8,7 @@ import type { Writable } from 'stream';
import { exists } from '../../util/file.js';
import { run } from '../process.js';
import program, { moduleDir, modulesDir } from '../program.js';
import program, { moduleName, modulesDir } from '../program.js';
import {
type GlobalModuleData,
type LocalModuleData,
@ -22,7 +22,7 @@ program
.option('-f, --override', 'Overwrite existing module')
.addArgument(new Argument('[name]', 'Name of the module').default(moduleDir))
.addArgument(new Argument('[name]', 'Name of the module').default(moduleName))
.option('--author <author>', 'Author of the module')
.option('--license <license>', 'License of the module')
.option('--description <description>', 'Description of the module')
@ -56,7 +56,7 @@ program
name =
name ??
(await input({ message: 'Name of the module', default: moduleDir }));
(await input({ message: 'Name of the module', default: moduleName }));
const author =
options.author ??
(await input({

258
src/m3/command/install.ts Normal file
View file

@ -0,0 +1,258 @@
import { confirm } from '@inquirer/prompts';
import chalk from 'chalk';
import { cp, rm, writeFile } from 'fs/promises';
import { Listr } from 'listr2';
import { resolve } from 'path';
import type { Writable } from 'stream';
import { setTimeout as sleep } from 'timers/promises';
import { dir } from 'tmp-promise';
import { exists } from '../../util/file.js';
import { getGlobalData } from '../module.js';
import type { Stdio } from '../process.js';
import { run } from '../process.js';
import program, { moduleName, modulesDir } from '../program.js';
import type { GlobalModuleData, LocalModuleData } from '../types.js';
// Calculate Delta
// Install module (and dependencies) (shallow)
// Ask for confirmation
program
.command('install')
.alias('i')
.description(
moduleName === undefined ? 'install a module' : 'install a dependency',
)
.argument('<url>')
.addHelpText(
'after',
'\nExamples:\n m3 i[nstall] https://github.com/DinheroDevelopmentGroup/mmp.chat.git\n m3 i[nstall] github://DinheroDevelopmentGroup/mmp.chat',
)
.option('-y, --yes', 'skip confirmation')
.action(async (url: string, options: { yes: boolean }) => {
if (url.startsWith('github://')) {
url = url.replace('github://', 'https://github.com/');
url += '.git';
}
// Add dependency if in module
if (moduleName !== undefined) {
const globalPath = resolve(modulesDir, moduleName, 'm3.global.json');
const global = await getGlobalData(globalPath);
if (!global.dependencies.includes(url)) global.dependencies.push(url);
await writeFile(globalPath, JSON.stringify(global, null, 2));
}
console.log('Calculating delta...');
const addedUrls = new Set<string>();
interface ModuleData {
manual: boolean;
}
const modules = new Map<string, ModuleData>();
await addModule(url, true);
if (modules.size === 0) {
console.log('Nothing to do.');
return;
}
console.log(`Adding ${modules.size} module(s)...`);
if (!options.yes) {
if (!(await confirm({ message: 'Continue?' }))) return;
}
await new Listr(
Array.from(modules.entries()).map(([url, data]) => ({
title: url,
task: async (_ctx, task) => {
interface Context {
path?: string;
global?: GlobalModuleData;
}
await sleep(1000 * Math.random());
await task
.newListr<Context>(
[
{
title: 'Clone repository',
task: async (ctx) => {
const stdout = task.stdout() as Writable;
ctx.path = await getRepo(url, {
shallow: true,
stdio: { stdout, stderr: stdout },
});
},
},
{
title: 'Parse m3 metadata',
task: async (ctx) => {
const path = ctx.path;
if (path === undefined) throw new Error('undefined path');
const globalPath = resolve(path, 'm3.global.json');
ctx.global = await getGlobalData(globalPath);
},
},
{
title: 'Move directory',
task: async (ctx) => {
const path = ctx.path;
if (path === undefined) throw new Error('undefined path');
const global = ctx.global;
if (global === undefined)
throw new Error('undefined global module data');
const localPath = resolve(modulesDir, global.name);
await cp(path, localPath, {
recursive: true,
});
ctx.path = localPath;
},
},
{
title: 'Generate local module data',
task: async (ctx) => {
const path = ctx.path;
if (path === undefined) throw new Error('undefined path');
const localData: LocalModuleData = {
manual: data.manual,
};
await writeFile(
resolve(path, 'm3.local.json'),
JSON.stringify(localData, undefined, 2),
);
},
},
{
title: 'npm install',
task: async (ctx, task) => {
const path = ctx.path;
await run(
'npm',
['install', '--verbose'],
{ cwd: path },
{ stdout: task.stdout() as Writable },
);
},
},
],
{
concurrent: false,
},
)
.run({});
},
})),
{ concurrent: true, collectErrors: 'minimal' },
).run();
async function addModule(url: string, manual: boolean): Promise<void> {
if (addedUrls.has(url)) return;
addedUrls.add(url);
await withRepo(
url,
async (path) => {
const globalPath = resolve(path, 'm3.global.json');
let global: GlobalModuleData;
try {
global = await getGlobalData(globalPath);
} catch (error) {
throw new Error(
`Failed to parse global module data, ${url} is probably not an m3 module`,
{
cause: error,
},
);
}
const localPath = resolve(modulesDir, global.name);
if (await exists(localPath)) {
return;
}
modules.set(url, { manual });
console.log(chalk.green(`[+] ${global.name}`));
const promises: Promise<void>[] = [];
for (const dependency of global.dependencies) {
promises.push(
(async () => {
await addModule(dependency, false);
})(),
);
}
await Promise.all(promises);
},
{ shallow: true },
);
}
interface GitCloneOptions {
shallow?: boolean;
stdio?: Stdio;
verbose?: boolean;
}
async function gitClone(
url: string,
path: string,
options: GitCloneOptions = {},
): Promise<void> {
await run(
'git',
[
'clone',
url,
path,
(options.shallow ?? false) ? '--depth=1' : undefined,
(options.verbose ?? false) ? '--verbose' : undefined,
].filter((arg) => arg !== undefined),
{},
{},
);
}
async function getRepo(
url: string,
options?: GitCloneOptions,
): Promise<string> {
const { path } = await dir();
await gitClone(url, path, options);
return path;
}
async function withRepo(
url: string,
callback: (path: string) => void | Promise<void>,
options?: GitCloneOptions,
): Promise<void> {
const path = await getRepo(url, options);
await callback(path);
await rm(path, { recursive: true });
}
});

View file

@ -4,7 +4,7 @@ import { resolve } from 'path';
import type { GlobalModuleData, LocalModuleData } from './types.js';
import { GlobalModuleDataSchema, LocalModuleDataSchema } from './types.js';
async function getLocalData(path: string): Promise<LocalModuleData> {
export async function getLocalData(path: string): Promise<LocalModuleData> {
try {
const raw = await readFile(path, 'utf8');
const data = JSON.parse(raw) as unknown;
@ -15,7 +15,7 @@ async function getLocalData(path: string): Promise<LocalModuleData> {
}
}
async function getGlobalData(path: string): Promise<GlobalModuleData> {
export async function getGlobalData(path: string): Promise<GlobalModuleData> {
try {
const raw = await readFile(path, 'utf8');
const data = JSON.parse(raw) as unknown;

View file

@ -27,7 +27,7 @@ export async function run(
if (code === 0) {
resolve();
} else {
reject();
reject(new Error(`Process exited with code ${code}`));
}
});
});

View file

@ -15,7 +15,7 @@ export let modulesDir: string;
// If cwd is a module,
// cwd will be {modulesDir}/{moduleDir}
export let moduleDir: string | undefined;
export let moduleName: string | undefined;
const cwd = process.cwd();
const cwdn = basename(cwd);
@ -26,7 +26,7 @@ const parent = resolve('..');
// (parent is modules dir)
if (basename(parent) === 'modules') {
modulesDir = parent;
moduleDir = cwdn;
moduleName = cwdn;
} else {
switch (cwdn) {
case 'modular-minecraft-proxy':