Skip to content

Commit

Permalink
feat: better prompt (#52)
Browse files Browse the repository at this point in the history
* feat: add boxen alert

* feat: better prompt

* feat: add spin in wrapRetry

* feat: fallback isTTY
  • Loading branch information
elrrrrrrr authored Nov 6, 2023
1 parent 73b27eb commit ae15130
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 14 deletions.
23 changes: 21 additions & 2 deletions packages/cli/bin/rapid.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ const yargs = require('yargs');
const { NpmFsMode, NYDUS_TYPE } = require('../lib/constants.js');
const util = require('../lib/util');
const fuse_t = require('../lib/fuse_t');
const { Alert } = require('../lib/logger.js');

yargs
const argv = yargs
.command({
command: 'install',
aliases: [ 'i', 'ii' ],
Expand Down Expand Up @@ -48,6 +49,7 @@ yargs
}

await util.shouldFuseSupport();

await install({
cwd,
pkg,
Expand All @@ -57,7 +59,11 @@ yargs
productionMode,
});

console.log('[rapid] install finished');
Alert.success('🚀 Success', [
'All dependencies have been successfully installed.',
'Please refrain from using `rm -rf node_modules` directly.',
'Consider using `rapid clean` or `rapid update` as alternatives.',
]);
// 首次执行 nydusd 后台服务可能会 hang 住输入
process.exit(0);
},
Expand All @@ -81,5 +87,18 @@ yargs
await list(cwd);
},
})
.fail((_, err) => {
Alert.error('🚨 Error', [
err,
'To enable debug mode, add the NODE_DEBUG=rapid before running the command.',
'If the problem continues, please provide feedback at:',
'https://github.com/cnpm/rapid/issues',
]);
})
.help()
.parse();


if (argv._?.length === 0) {
yargs.showHelp();
}
6 changes: 6 additions & 0 deletions packages/cli/lib/fuse_t.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const urllib = require('urllib');
const execa = require('execa');
const os = require('node:os');
const inquirer = require('inquirer');
const { Spin } = require('./logger');


const FUSE_T_INSTALL_PATH = '/usr/local/bin/go-nfsv4';
Expand All @@ -24,13 +25,18 @@ exports.checkFuseT = async function checkFuseT() {
};

exports.installFuseT = async function installFuseT() {
const spin = new Spin({
title: 'Installing fuse-t, it may take a few seconds',
showDots: true,
});
const tmpPath = path.join('/tmp', `${crypto.randomUUID()}.pkg`);
await urllib.request(FUSE_T_DOWNLOAD_URL, {
method: 'GET',
writeStream: fsSync.createWriteStream(tmpPath),
followRedirect: true,
});
await execa.command(`sudo installer -pkg ${tmpPath} -target /`);
spin.success('fuse-t installed successfully');
};

exports.confirmInstallFuseT = async function confirmInstallFuseT() {
Expand Down
102 changes: 96 additions & 6 deletions packages/cli/lib/logger.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const cliProgress = require('cli-progress');
const boxen = require('boxen');

const MAX_TITLE_LENGTH = 11;

Expand All @@ -9,6 +10,8 @@ function padCenter(str, length, char = ' ') {
return char.repeat(padLeft) + str + char.repeat(padRight);
}

const isTTY = process.stdout.isTTY;

class Bar {
constructor({ type, total }) {
const title = padCenter(type, MAX_TITLE_LENGTH);
Expand All @@ -24,10 +27,9 @@ class Bar {
);

this.startTime = Date.now();
this.isTTY = process.stdout.isTTY;

// init
if (this.isTTY) {
if (isTTY) {
this.bar = this.multiBar.create(total, 1, {
status: 'Running',
warning: '',
Expand All @@ -39,22 +41,22 @@ class Bar {

update(current = '') {

if (!this.isTTY) {
if (!isTTY) {
return;
}

const { value, total } = this.bar;
if (value < total) {
this.isTTY && this.bar.update(value + 1, { status: 'Running', message: current });
isTTY && this.bar.update(value + 1, { status: 'Running', message: current });
}

if (value >= total - 1) {
this.isTTY && this.bar.update(total - 1, { status: 'Processing', message: 'Processing...' });
isTTY && this.bar.update(total - 1, { status: 'Processing', message: 'Processing...' });
}
}

stop() {
if (!this.isTTY) {
if (!isTTY) {
console.log('[rapid] %s complete, %dms', this.type, Date.now() - this.startTime);
return;
}
Expand All @@ -70,4 +72,92 @@ class Bar {
}
}

class Alert {
static formatMessage(message) {
if (Array.isArray(message)) {
return message.map(_ => `* ${_}`).join('\n');
}
return message.trim();
}

static error(title = 'Error', message = 'OOPS, something error') {
message = this.formatMessage(message);
if (!isTTY) {
console.log(message);
process.exit(1);
}
const boxedMessage = boxen(message, {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'red',
title,
titleAlignment: 'center',
});
console.log(boxedMessage);
process.exit(1);
}

static success(title = 'Success', message = [ 'Congratulations', 'The operation was successful' ]) {
message = this.formatMessage(message);
if (!isTTY) {
console.log(message);
}
const boxedMessage = boxen(message, {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'green',
title,
titleAlignment: 'center',
});
console.log(boxedMessage);
}
}

class Spin {
constructor({ title = 'processing', showDots = false }) {
const { createSpinner } = require('nanospinner');

if (!isTTY) {
console.log(`[rapid] ${title}`);
return;
}

this.spinner = createSpinner(title).start();
this.dots = 0;
this.start = Date.now();

if (showDots) {
this.interval = setInterval(() => {
this.dots = (this.dots + 1) % 4; // 循环点的数量从0到3
const dotsString = '.'.repeat(this.dots); // 创建一个字符串,包含对应数量的点
this.spinner.update({ text: `${title}${dotsString}` }); // 更新 spinner 文本
}, 200); // 每200毫秒更新一次
}
}

update(message) {
if (!isTTY) {
console.log(`[rapid] ${message}`);
return;
}
this.spinner.update({ text: message });
}

success(message) {
const text = `${message || this.title}: ${Date.now() - this.start}ms`;
if (!isTTY) {
console.log(`[rapid] ${text}`);
return;
}
if (this.showDots) {
clearInterval(this.interval);
}
this.spinner.success({ text });
}
}

exports.Spin = Spin;
exports.Bar = Bar;
exports.Alert = Alert;
7 changes: 7 additions & 0 deletions packages/cli/lib/nydusd/fuse_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ async function mountOverlay(cwd, pkg) {
await wrapRetry({
cmd: async () =>
await execa.command(wrapSudo(`mount -t tmpfs tmpfs ${overlay}`)),
title: 'mount tnpmfs',
});
} else if (os.type() === 'Darwin') {
// hdiutil create -size 512m -fs "APFS" -volname "NewAPFSDisk" -type SPARSE -layout NONE -imagekey diskimage-class=CRawDiskImage loopfile.dmg
Expand All @@ -93,9 +94,11 @@ async function mountOverlay(cwd, pkg) {
await execa.command(
`hdiutil create -size 512m -fs APFS -volname ${volumeName} -type SPARSE -layout NONE -imagekey diskimage-class=CRawDiskImage ${tmpDmg}`
),
title: 'hdiutil create',
});
await wrapRetry({
cmd: async () => await execa.command(`hdiutil attach -mountpoint ${overlay} ${tmpDmg}`),
title: 'hdiutil attach',
});
}
await fs.mkdir(upper, { recursive: true });
Expand Down Expand Up @@ -141,6 +144,7 @@ async function endNydusFs(cwd, pkg, force = true) {
}
await execa.command(`umount ${nodeModulesDir}`);
},
title: 'umount node_modules',
fallback: force
? async () => {
// force 模式再次尝试
Expand All @@ -162,6 +166,7 @@ async function endNydusFs(cwd, pkg, force = true) {
}
await execa.command(`hdiutil detach ${overlay}`);
},
title: 'hdiutil detach',
fallback: force
? async () => {
console.log(`[rapid] use fallback umount -f ${overlay}`);
Expand All @@ -172,9 +177,11 @@ async function endNydusFs(cwd, pkg, force = true) {
} else {
await wrapRetry({
cmd: () => execa.command(wrapSudo(`${umountCmd} ${nodeModulesDir}`)),
title: 'umount node_modules',
});
await wrapRetry({
cmd: () => execa.command(wrapSudo(`${umountCmd} ${overlay}`)),
title: 'umount node_modules',
});
}
await nydusdApi.umount(`/${dirname}`);
Expand Down
31 changes: 25 additions & 6 deletions packages/cli/lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const url = require('node:url');
const crypto = require('node:crypto');
const mapWorkspaces = require('@npmcli/map-workspaces');
const fuse_t = require('./fuse_t');
const { Spin } = require('./logger');

const parser = require('yargs-parser');
const { NpmFsMode } = require('./constants');
Expand All @@ -24,6 +25,7 @@ const {
nydusdBootstrapFile,
nydusdMnt,
} = require('./constants');
const { Alert } = require('./logger');

// node_modules/a -> a
// node_mdoules/@scope/b -> @scope/b
Expand Down Expand Up @@ -55,22 +57,31 @@ function wrapSudo(shScript) {
return `sudo ${shScript}`;
}

async function wrapRetry({ cmd, timeout = 3000, fallback }) {
async function wrapRetry({ cmd, timeout = 3000, fallback, title = 'shell cmd' }) {
// 最多等 3 秒
// 只在第一次失败时才展示 spin
let spin;
const startTime = Date.now();
let done = false;
let count = 0;
while (!done) {
try {
await cmd();
done = true;
spin && spin.success(title);
} catch (error) {
console.info(`[rapid] cmd failed: ${error}, retrying...`);
if (!spin) {
spin = new Spin({ title });
}
// spin.update(`${cmd} failed, ${error}, retrying...`);
if (Date.now() - startTime <= timeout) {
await exports.sleep(300);
count++;
spin.update(`${title} retrying ${count} times ...`);
} else {
if (fallback) {
await fallback();
console.info('[rapid] cmd with fallback success');
spin.success('[rapid] fallback success');
return;
}
throw error;
Expand Down Expand Up @@ -561,9 +572,17 @@ exports.readPkgJSON = async function readPkgJSON(cwd) {
};

exports.readPackageLock = async function readPackageLock(cwd) {
const lockPath = path.join(cwd || exports.findLocalPrefix(), './package-lock.json');
const packageLock = JSON.parse(await fs.readFile(lockPath, 'utf8'));
return { packageLock, lockPath };
try {
const lockPath = path.join(cwd || exports.findLocalPrefix(), './package-lock.json');
const packageLock = JSON.parse(await fs.readFile(lockPath, 'utf8'));
return { packageLock, lockPath };
} catch (e) {
Alert.error('Error', [
'Failed to parse package-lock.json.',
'We only support package-lock.json version 3.',
'Run `npm i --package-lock-only` to generate it.',
]);
}
};

// 列出当前 mount 的 fuse endpoint
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
"@npmcli/map-workspaces": "^3.0.0",
"await-event": "^2.1.0",
"binary-mirror-config": "^2.5.0",
"boxen": "^5.1.2",
"chalk": "^4.0.0",
"cli-progress": "^3.12.0",
"execa": "^5.1.1",
"inquirer": "^8.2.6",
"ms": "^0.7.1",
"nanospinner": "^1.1.0",
"npm-normalize-package-bin": "^3.0.1",
"npm-package-arg": "^10.1.0",
"p-map": "^4.0.0",
Expand Down

0 comments on commit ae15130

Please sign in to comment.