feat(cli): ported remove cmd to new cli

+ more detailed error messages inc. alternative instructions for removal if not backup is found
This commit is contained in:
dragonwocky 2022-12-06 20:37:39 +11:00
parent a7861be39a
commit ebf15dbfb9
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
3 changed files with 260 additions and 89 deletions

243
bin.mjs
View File

@ -10,13 +10,74 @@ import arg from "arg";
import chalk from "chalk-template"; import chalk from "chalk-template";
import os from "node:os"; import os from "node:os";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import { checkEnhancementStatus, setNotionPath } from "./scripts/electron.mjs"; import {
getResourcePath,
getAppPath,
getBackupPath,
getCachePath,
checkEnhancementVersion,
setNotionPath,
unpackApp,
applyEnhancements,
takeBackup,
restoreBackup,
removeCache,
} from "./scripts/enhance-desktop-app.mjs";
import { existsSync } from "node:fs";
let __quiet = false; let __quiet, __spinner;
const nodeRequire = createRequire(import.meta.url), const nodeRequire = createRequire(import.meta.url),
manifest = nodeRequire("./package.json"), manifest = nodeRequire("./package.json"),
stdout = (...args) => __quiet || process.stdout.write(chalk(...args)), print = (...args) => __quiet || process.stdout.write(chalk(...args)),
stdoutRaw = (value) => __quiet || console.log(value); printObject = (value) => __quiet || console.dir(value, { depth: null });
const hideCursor = () => process.stdout.write("\x1b[?25l"),
showCursor = () => process.stdout.write("\x1b[?25h"),
stopSpinner = () => __spinner?.stop(),
startSpinner = () => {
// cleanup prev spinner
stopSpinner();
let i = 0;
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
interval = setInterval(() => __spinner.step(), 80);
// prevent backspace removing existing stdout
print` `;
__spinner = {
step() {
i++;
// overwrite spinner with next frame
print`\b\b\b {bold.yellow ${frames[i % frames.length]}} `;
hideCursor();
},
stop() {
clearInterval(interval);
// overwrite spinner with arrow on completion
print`\b\b\b {bold.yellow →}\n`;
showCursor();
},
};
},
readStdin = () => {
return new Promise((res) => {
process.stdin.resume();
process.stdin.setEncoding("utf8");
process.stdin.once("data", (key) => {
process.stdin.pause();
res(key);
});
});
},
promptForValue = async (prompt, values) => {
let input;
// prevent line clear remove existing stdout
print`\n`;
do {
// clear line and continue prompting until valid input is received
print`\x1b[1A\r\x1b[K${prompt}`;
input = (await readStdin()).trim();
} while (!values.includes(input));
return input;
};
const commands = [ const commands = [
// ["command", "description"] // ["command", "description"]
@ -25,7 +86,7 @@ const commands = [
["check", "check the current state of the notion app"], ["check", "check the current state of the notion app"],
], ],
options = [ options = [
// ["comma, separated, aliases", [type, "description"]] // ["alias, option=example", [type, "description"]]
[ [
"--path=</path/to/notion/resources>", "--path=</path/to/notion/resources>",
[String, "provide notion installation location (defaults to auto-detected)"], [String, "provide notion installation location (defaults to auto-detected)"],
@ -34,9 +95,9 @@ const commands = [
["--overwrite", [Boolean, ""]], ["--overwrite", [Boolean, ""]],
["-y, --yes", [Boolean, 'skip prompts; assume "yes" and run non-interactively']], ["-y, --yes", [Boolean, 'skip prompts; assume "yes" and run non-interactively']],
["-n, --no", [Boolean, 'skip prompts; assume "no" and run non-interactively']], ["-n, --no", [Boolean, 'skip prompts; assume "no" and run non-interactively']],
["-q, --quiet", [Boolean, "hide all output"]], ["-q, --quiet", [Boolean, 'skip prompts and hide all output; assume "no"']],
["-d, --debug", [Boolean, "show detailed error messages"]], ["-d, --debug", [Boolean, "show detailed error messages"]],
["-j, --json", [Boolean, "display json output"]], ["-j, --json", [Boolean, "display json output (where applicable)"]],
["-h, --help", [Boolean, "display usage information"]], ["-h, --help", [Boolean, "display usage information"]],
["-v, --version", [Boolean, "display version number"]], ["-v, --version", [Boolean, "display version number"]],
], ],
@ -52,24 +113,54 @@ const commands = [
} }
} }
return args; return args;
},
compileOptsToJsonOutput = () => {
// the structure used to define options above
// is convenient and compact, but requires additional
// parsing to understand. this function processes
// options into a more explicitly defined structure
return options.map(([opt, [type, description]]) => {
const option = {
aliases: opt.split(", ").map((alias) => alias.split("=")[0]),
type,
description,
},
example = opt
.split(", ")
.map((alias) => alias.split("=")[1])
.find((value) => value);
if (example) option.example = example;
return option;
});
}; };
const args = arg(compileOptsToArgSpec(options)), const args = arg(compileOptsToArgSpec(options)),
printHelp = () => { printHelp = () => {
const cmdPad = Math.max(...commands.map(([cmd]) => cmd.length)), const { name, version, homepage } = manifest,
optPad = Math.max(...options.map((opt) => opt[0].length)), usage = `${name} <command> [options]`;
parseCmd = (cmd) => chalk` ${cmd[0].padEnd(cmdPad)} {grey :} ${cmd[1]}`, if (args["--json"]) {
parseOpt = (opt) => chalk` ${opt[0].padEnd(optPad)} {grey :} ${opt[1][1]}`; printObject({
stdout`{bold.rgb(245,245,245) ${manifest.name} v${manifest.version}} name,
{grey ${manifest.homepage}} version,
\n{bold.rgb(245,245,245) USAGE} homepage,
{yellow $} ${manifest.name} <command> [options] usage,
\n{bold.rgb(245,245,245) COMMANDS}\n${commands.map(parseCmd).join("\n")} commands: Object.fromEntries(commands),
\n{bold.rgb(245,245,245) OPTIONS}\n${options.map(parseOpt).join("\n")}\n`; options: compileOptsToJsonOutput(),
});
} else {
const cmdPad = Math.max(...commands.map(([cmd]) => cmd.length)),
optPad = Math.max(...options.map((opt) => opt[0].length)),
parseCmd = (cmd) => chalk` ${cmd[0].padEnd(cmdPad)} {grey :} ${cmd[1]}`,
parseOpt = (opt) => chalk` ${opt[0].padEnd(optPad)} {grey :} ${opt[1][1]}`;
print`{bold.whiteBright ${name} v${version}}\n{grey ${homepage}}
\n{bold.whiteBright USAGE}\n${name} <command> [options]
\n{bold.whiteBright COMMANDS}\n${commands.map(parseCmd).join("\n")}
\n{bold.whiteBright OPTIONS}\n${options.map(parseOpt).join("\n")}\n`;
}
}, },
printVersion = () => { printVersion = () => {
if (args["--json"]) { if (args["--json"]) {
stdoutRaw({ printObject({
[manifest.name]: manifest.version, [manifest.name]: manifest.version,
node: process.version.slice(1), node: process.version.slice(1),
platform: process.platform, platform: process.platform,
@ -80,53 +171,105 @@ const args = arg(compileOptsToArgSpec(options)),
const enhancerVersion = `${manifest.name}@v${manifest.version}`, const enhancerVersion = `${manifest.name}@v${manifest.version}`,
nodeVersion = `node@${process.version}`, nodeVersion = `node@${process.version}`,
osVersion = `${process.platform}-${process.arch}/${os.release()}`; osVersion = `${process.platform}-${process.arch}/${os.release()}`;
stdout`${enhancerVersion} via ${nodeVersion} on ${osVersion}\n`; print`${enhancerVersion} via ${nodeVersion} on ${osVersion}\n`;
} }
}; };
if (args["--quiet"]) __quiet = true; if (args["--quiet"]) __quiet = true;
if (args["--help"]) [printHelp(), process.exit()]; if (args["--help"]) [printHelp(), process.exit()];
if (args["--version"]) [printVersion(), process.exit()]; if (args["--version"]) [printVersion(), process.exit()];
if (args["--path"]) setNotionPath(args["--path"]); if (args["--path"]) setNotionPath(args["--path"]);
const defaultPromptValue = args["--yes"]
? "y"
: args["--no"] || args["--quiet"]
? "n"
: undefined;
const notionNotFound = `notion installation not found (corrupted or nonexistent)`,
enhancerNotApplied = `notion-enhancer not applied (notion installation found)`,
onSuccess = chalk`{bold.whiteBright SUCCESS} {green ✔}`,
onFail = chalk`{bold.whiteBright FAILURE} {red ✘}`,
onCancel = chalk`{bold.whiteBright CANCELLED} {red ✘}`;
switch (args["_"][0]) { switch (args["_"][0]) {
case "apply": { case "apply": {
print`{bold.whiteBright [NOTION-ENHANCER] APPLY} `;
// const res = await apply(notionPath, {
// overwritePrevious: promptRes,
// patchPrevious: opts.get("patch") ? true : false,
// takeBackup: opts.get("no-backup") ? false : true,
// });
// if (res) {
// log`{bold.whiteBright SUCCESS} {green ✔}`;
// } else log`{bold.whiteBright CANCELLED} {red ✘}`;
break; break;
} }
case "remove": { case "remove": {
const appPath = getAppPath(),
backupPath = getBackupPath(),
cachePath = getCachePath(),
insertVersion = checkEnhancementVersion();
print`{bold.whiteBright [NOTION-ENHANCER] REMOVE}\n`;
// restore notion to app.bak or app.asar.bak
if (!appPath) {
print` {grey * ${notionNotFound}}\n`;
} else if (insertVersion) {
print` {grey * notion installation found: notion-enhancer v${insertVersion} applied}\n`;
if (backupPath) {
print` {grey * backup found: restoring}`;
startSpinner();
await restoreBackup();
stopSpinner();
} else {
print` {grey * backup not found: skipping}\n`;
print` {red * to remove the notion-enhancer from notion, uninstall notion and then install}\n`;
print` {red a vanilla version of the app from https://www.notion.so/desktop (mac, windows)}\n`;
print` {red or ${manifest.homepage}/getting-started/installation (linux)\n}`;
}
} else print` {grey * ${enhancerNotApplied}: skipping}\n`;
// optionally remove ~/.notion-enhancer
if (existsSync(cachePath)) {
print` {grey * cache found: ${cachePath}}\n`;
let deleteCache;
const prompt = chalk` {inverse > delete? [Y/n]:} `;
if (defaultPromptValue) {
deleteCache = defaultPromptValue;
print`${prompt}${defaultPromptValue}\n`;
} else deleteCache = await promptForValue(prompt, ["Y", "y", "N", "n"]);
if (["Y", "y"].includes(deleteCache)) {
print` {grey * cache found: removing}`;
startSpinner();
await removeCache();
stopSpinner();
} else print` {grey * cache found: keeping}\n`;
} else print` {grey * cache not found: skipping}\n`;
// failure if backup could not be restored
print`${insertVersion && !backupPath ? onFail : onSuccess}\n`;
break; break;
} }
// case "apply": {
// stdout`{bold.rgb(245,245,245) [NOTION-ENHANCER] APPLY}`;
// const res = await apply(notionPath, {
// overwritePrevious: promptRes,
// patchPrevious: opts.get("patch") ? true : false,
// takeBackup: opts.get("no-backup") ? false : true,
// });
// if (res) {
// log`{bold.rgb(245,245,245) SUCCESS} {green ✔}`;
// } else log`{bold.rgb(245,245,245) CANCELLED} {red ✘}`;
// break;
// }
// case "remove": {
// log`{bold.rgb(245,245,245) [NOTION-ENHANCER] REMOVE}`;
// const res = await remove(notionPath, { delCache: promptRes });
// if (res) {
// log`{bold.rgb(245,245,245) SUCCESS} {green ✔}`;
// } else log`{bold.rgb(245,245,245) CANCELLED} {red ✘}`;
// break;
// }
case "check": case "check":
const status = checkEnhancementStatus(); const appPath = getAppPath(),
if (args["--json"]) [stdoutRaw(status), process.exit()]; backupPath = getBackupPath(),
stdout`{bold.rgb(245,245,245) [NOTION-ENHANCER] CHECK:} `; cachePath = getCachePath(),
if (manifest.version === status.insertVersion) { insertVersion = checkEnhancementVersion();
stdout`notion-enhancer v${manifest.version} applied.\n`; if (args["--json"]) {
} else if (status.insertVersion) { printObject({
stdout`notion-enhancer v${manifest.version} applied != v${status.insertVersion} cli.\n`; appPath,
} else if (status.appPath) { backupPath,
stdout`notion-enhancer has not been applied (notion installation found).\n`; cachePath,
} else { doesCacheExist: existsSync(cachePath),
stdout`notion installation not found (corrupted or nonexistent).\n`; insertVersion,
});
process.exit();
} }
print`{bold.whiteBright [NOTION-ENHANCER] CHECK:} `;
if (manifest.version === insertVersion) {
print`notion-enhancer v${manifest.version} applied\n`;
} else if (insertVersion) {
print`notion-enhancer v${manifest.version} applied != v${insertVersion} cli\n`;
} else if (appPath) {
print`${enhancerNotApplied}.\n`;
} else print`${notionNotFound}.\n`;
break; break;
default: default:
printHelp(); printHelp();

View File

@ -14,6 +14,7 @@
"node": ">=16.x.x" "node": ">=16.x.x"
}, },
"dependencies": { "dependencies": {
"@electron/asar": "^3.2.2",
"arg": "^5.0.2", "arg": "^5.0.2",
"chalk-template": "^0.4.0" "chalk-template": "^0.4.0"
}, },

View File

@ -4,7 +4,7 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import chalk from "chalk-template"; import asar from "@electron/asar";
import os from "node:os"; import os from "node:os";
import { promises as fsp, existsSync } from "node:fs"; import { promises as fsp, existsSync } from "node:fs";
import { resolve } from "node:path"; import { resolve } from "node:path";
@ -43,11 +43,11 @@ const nodeRequire = createRequire(import.meta.url),
const setNotionPath = (path) => { const setNotionPath = (path) => {
// sets notion resource path to user provided value // sets notion resource path to user provided value
// e.g. from cli // e.g. with the --path cli option
__notionResources = path; __notionResources = path;
}, },
getNotionResources = () => { getResourcePath = (path) => {
if (__notionResources) return __notionResources; if (__notionResources) return resolve(`${__notionResources}/${path}`);
polyfillWslEnv("LOCALAPPDATA"); polyfillWslEnv("LOCALAPPDATA");
polyfillWslEnv("PROGRAMW6432"); polyfillWslEnv("PROGRAMW6432");
const potentialPaths = [ const potentialPaths = [
@ -63,48 +63,75 @@ const setNotionPath = (path) => {
if (!targetPlatforms.includes(platform)) continue; if (!targetPlatforms.includes(platform)) continue;
if (!existsSync(testPath)) continue; if (!existsSync(testPath)) continue;
__notionResources = testPath; __notionResources = testPath;
return __notionResources; return resolve(`${__notionResources}/${path}`);
} }
}, },
getEnhancerCache = () => { // prefer unpacked if both exist
getAppPath = () => ["app", "app.asar"].map(getResourcePath).find(existsSync),
getBackupPath = () => ["app.bak", "app.asar.bak"].map(getResourcePath).find(existsSync),
getCachePath = () => {
if (__enhancerCache) return __enhancerCache; if (__enhancerCache) return __enhancerCache;
const home = platform === "wsl" ? polyfillWslEnv("HOMEPATH") : os.homedir(); const home = platform === "wsl" ? polyfillWslEnv("HOMEPATH") : os.homedir();
__enhancerCache = resolve(`${home}/.notion-enhancer`); __enhancerCache = resolve(`${home}/.notion-enhancer`);
return __enhancerCache; return __enhancerCache;
},
checkEnhancementVersion = () => {
const insertPath = getResourcePath("app/node_modules/notion-enhancer");
if (!existsSync(insertPath)) return undefined;
const insertManifest = getResourcePath("app/node_modules/notion-enhancer/package.json"),
insertVersion = nodeRequire(insertManifest).version;
return insertVersion;
}; };
const checkEnhancementStatus = () => { const unpackApp = () => {
const resourcePath = (path) => resolve(`${getNotionResources()}/${path}`), const appPath = getAppPath();
doesResourceExist = (path) => existsSync(resourcePath(path)); if (!appPath || !appPath.endsWith("asar")) return false;
asar.extractAll(appPath, appPath.replace(/\.asar$/, ""));
const isAppUnpacked = doesResourceExist("app"), return true;
isAppPacked = doesResourceExist("app.asar"), },
isBackupUnpacked = doesResourceExist("app.bak"), applyEnhancements = () => {
isBackupPacked = doesResourceExist("app.asar.bak"), const appPath = getAppPath();
isEnhancerInserted = doesResourceExist("app/node_module/notion-enhancer"), if (!appPath || appPath.endsWith("asar")) return false;
enhancerInsertManifest = isEnhancerInserted // ...
? resourcePath("app/node_module/notion-enhancer/package.json") return true;
: undefined; },
takeBackup = async () => {
// prefer unpacked if both exist: extraction is slow const appPath = getAppPath();
return { if (!appPath) return false;
appPath: isAppUnpacked const backupPath = getBackupPath();
? resourcePath("app") if (backupPath) await fsp.rm(backupPath, { recursive: true });
: isAppPacked const destPath = `${appPath}.bak`;
? resourcePath("app.asar") if (!appPath.endsWith(".asar")) {
: undefined, await fsp.cp(appPath, destPath, { recursive: true });
backupPath: isBackupUnpacked } else await fsp.rename(appPath, destPath);
? resourcePath("app.bak") return true;
: isBackupPacked },
? resourcePath("app.asar.bak") restoreBackup = async () => {
: undefined, const backupPath = getBackupPath();
cachePath: existsSync(getEnhancerCache()) // if (!backupPath) return false;
? getEnhancerCache() const destPath = backupPath.replace(/\.bak$/, "");
: undefined, if (existsSync(destPath)) await fsp.rm(destPath, { recursive: true });
insertVersion: isEnhancerInserted await fsp.rename(backupPath, destPath);
? nodeRequire(enhancerInsertManifest).version const appPath = getAppPath();
: undefined, if (destPath !== appPath) await fsp.rm(appPath, { recursive: true });
return true;
},
removeCache = async () => {
if (!existsSync(getCachePath())) return;
await fsp.rm(getCachePath());
return true;
}; };
export {
getResourcePath,
getAppPath,
getBackupPath,
getCachePath,
checkEnhancementVersion,
setNotionPath,
unpackApp,
applyEnhancements,
takeBackup,
restoreBackup,
removeCache,
}; };
export { checkEnhancementStatus, setNotionPath };