generated from jonahsnider/typescript-starter
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b7f6c60
commit 6aef24e
Showing
16 changed files
with
375 additions
and
174 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,7 @@ | |
- Get typical usages of a command: | ||
|
||
`how {{command}}` | ||
|
||
- Refresh knowledge base: | ||
|
||
`how refresh` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import fs from 'node:fs/promises'; | ||
import path from 'node:path'; | ||
import {URL} from 'node:url'; | ||
import {Command, Option} from 'clipanion'; | ||
import {CommandError} from '../utils/errors.js'; | ||
import * as Glow from '../utils/glow/index.js'; | ||
import * as Tldr from '../utils/tldr.js'; | ||
|
||
class MissingCommandError extends CommandError { | ||
constructor(message: string) { | ||
super(message, false); | ||
this.name = 'MissingCommandError'; | ||
} | ||
} | ||
|
||
export class HowCommand extends Command { | ||
static paths = [Command.Default, ['view']]; | ||
|
||
// eslint-disable-next-line new-cap | ||
static usage = Command.Usage({ | ||
description: 'Learn how to use a CLI app.', | ||
examples: [ | ||
['Basic command', '$0 tar'], | ||
['Space-delimited command', '$0 git status'], | ||
['Slugified command', '$0 git-status'], | ||
], | ||
}); | ||
|
||
readonly command = Option.String(); | ||
// eslint-disable-next-line new-cap | ||
readonly subCommand = Option.Rest(); | ||
|
||
async execute() { | ||
const app = [this.command, ...this.subCommand].join('-') || 'how'; | ||
|
||
let rawMarkdown: Tldr.RawMarkdown; | ||
|
||
const preparations: Array<Promise<void>> = [Glow.prepare()]; | ||
|
||
if (app === 'how') { | ||
rawMarkdown = (await fs.readFile(new URL(path.join('..', '..', 'docs', 'how.md'), import.meta.url), 'utf-8')) as Tldr.RawMarkdown; | ||
} else { | ||
const tldrPrepare = Tldr.prepare(); | ||
preparations.push(tldrPrepare); | ||
await tldrPrepare; | ||
|
||
const rawTldr = await Tldr.read(app); | ||
|
||
if (rawTldr === null) { | ||
throw new MissingCommandError("That command doesn't exist in the knowledge base (try running `how refresh`)"); | ||
} | ||
|
||
rawMarkdown = rawTldr; | ||
} | ||
|
||
await Promise.all(preparations); | ||
|
||
const formattedTldr: Tldr.FormattedMarkdown = Tldr.format(rawMarkdown); | ||
|
||
const rawGlow = await Glow.render(formattedTldr); | ||
|
||
const formattedGlow = Glow.format(rawGlow); | ||
|
||
this.context.stdout.write(formattedGlow); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './how-command.js'; | ||
export * from './refresh-command.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import {Command} from 'clipanion'; | ||
import * as Tldr from '../utils/tldr.js'; | ||
|
||
export class RefreshCommand extends Command { | ||
static paths = [['refresh'], ['-r']]; | ||
|
||
// eslint-disable-next-line new-cap | ||
static usage = Command.Usage({ | ||
description: 'Refresh the downloaded knowledge base.', | ||
category: 'Meta', | ||
}); | ||
|
||
async execute() { | ||
await Tldr.refresh(); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,113 +1,30 @@ | ||
#!/usr/bin/env node | ||
import fs from 'node:fs/promises'; | ||
import os from 'node:os'; | ||
import path from 'node:path'; | ||
import process, {stdout} from 'node:process'; | ||
import process from 'node:process'; | ||
import {URL} from 'node:url'; | ||
import type {PathLike} from 'node:fs'; | ||
import execa from 'execa'; | ||
import {convert} from 'convert'; | ||
import {Stopwatch} from '@jonahsnider/util'; | ||
import {Builtins, Cli} from 'clipanion'; | ||
import updateNotifier from 'update-notifier'; | ||
import {getOsKind, resolveVersion, updateGlow} from './fetch-glow/index.js'; | ||
import {formatGlow, formatTldr} from './format.js'; | ||
import {desiredGlowVersion, options} from './options.js'; | ||
import {glowPath, tldrPath} from './paths.js'; | ||
import * as Commands from './commands/index.js'; | ||
|
||
updateNotifier({pkg: JSON.parse(await fs.readFile(new URL(path.join('..', 'package.json'), import.meta.url), 'utf-8')) as updateNotifier.Package}).notify(); | ||
const args = process.argv.slice(2); | ||
|
||
try { | ||
await fs.access(tldrPath); | ||
} catch { | ||
await fs.mkdir(tldrPath, {recursive: true}); | ||
await execa('git', ['clone', 'https://github.com/tldr-pages/tldr.git', tldrPath]); | ||
console.log('downloaded the database of Markdown examples'); | ||
} | ||
|
||
if (options.glowVersion !== desiredGlowVersion) { | ||
// Currently installed version is not the same as the desired version | ||
|
||
const osKind = getOsKind(); | ||
const version = resolveVersion(osKind); | ||
|
||
try { | ||
const stopwatch = Stopwatch.start(); | ||
await updateGlow(version); | ||
|
||
const duration = convert(stopwatch.end(), 'ms').to('s').toFixed(2); | ||
const pkg = JSON.parse(await fs.readFile(new URL(path.join('..', 'package.json'), import.meta.url), 'utf-8')) as updateNotifier.Package; | ||
|
||
console.log(`downloaded a new binary for Glow (makes Markdown pretty, took ${duration}s)`); | ||
} catch (error) { | ||
console.error('failed to download a new binary for Glow (makes Markdown pretty)', error); | ||
process.exit(1); | ||
} | ||
} | ||
|
||
if (Math.random() > 0.95) { | ||
try { | ||
const stopwatch = Stopwatch.start(); | ||
await execa('git', ['pull'], {cwd: tldrPath}); | ||
const duration = convert(stopwatch.end(), 'ms').to('s').toFixed(2); | ||
|
||
console.log(`you got unlucky and the cache for the Markdown database was refreshed (took ${duration}s)`); | ||
} catch (error) { | ||
console.error('failed to refresh the cache for the Markdown database', error); | ||
} | ||
} | ||
updateNotifier({pkg}).notify(); | ||
|
||
const command = process.argv[process.argv.length - 1]; | ||
const categories = ['common', 'linux']; | ||
const cli = new Cli({ | ||
binaryLabel: `how`, | ||
binaryName: `how`, | ||
binaryVersion: pkg.version, | ||
}); | ||
|
||
switch (os.platform()) { | ||
case 'darwin': | ||
categories.unshift('osx'); | ||
break; | ||
case 'cygwin': | ||
case 'win32': | ||
categories.unshift('windows'); | ||
break; | ||
case 'sunos': | ||
console.log('how are you even running node on sunos???'); | ||
categories.unshift('sunos'); | ||
break; | ||
case 'android': | ||
categories.unshift('android'); | ||
break; | ||
default: | ||
break; | ||
// eslint-disable-next-line @typescript-eslint/naming-convention | ||
for (const Command of Object.values(Commands)) { | ||
cli.register(Command); | ||
} | ||
|
||
const potentialPaths: PathLike[] = categories.map(dir => path.join(tldrPath, 'pages', dir, `${command}.md`)); | ||
|
||
if ( | ||
// Command was probably a path (ex. '/home/jonah/programming/how/tsc_output') | ||
command.includes(path.sep) || | ||
// Command is this program | ||
command === 'how' | ||
) { | ||
potentialPaths.unshift(new URL(path.join('..', 'docs', 'how.md'), import.meta.url)); | ||
} | ||
|
||
for (const potentialPath of potentialPaths) { | ||
/* eslint-disable no-await-in-loop */ | ||
let contents: string; | ||
|
||
try { | ||
contents = await fs.readFile(potentialPath, 'utf-8'); | ||
} catch { | ||
continue; | ||
} | ||
|
||
const formatted = formatTldr(contents); | ||
|
||
const tldr = await execa(glowPath, ['-s', 'dark', '-'], { | ||
input: formatted, | ||
}); | ||
/* eslint-enable no-await-in-loop */ | ||
|
||
stdout.write(formatGlow(tldr.stdout)); | ||
process.exit(0); | ||
} | ||
cli.register(Builtins.HelpCommand); | ||
cli.register(Builtins.VersionCommand); | ||
|
||
console.error('no'); | ||
process.exit(1); | ||
await cli.runExit(args); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,38 +1,78 @@ | ||
import * as fs from 'node:fs/promises'; | ||
import os from 'node:os'; | ||
import path from 'node:path'; | ||
import {cacheDir, optionsPath} from './paths.js'; | ||
import {pathExists} from 'path-exists'; | ||
import {CACHE_DIR, OPTIONS_PATH} from './paths.js'; | ||
|
||
export interface Options { | ||
interface OptionsV1 { | ||
versionNumber: 1; | ||
/** Currently installed version for glow. */ | ||
glowVersion: string | null; | ||
versionNumber: 1; | ||
} | ||
|
||
interface OptionsV2 { | ||
versionNumber: 2; | ||
/** Currently installed version for glow. */ | ||
glowVersion: string | null; | ||
/** Timestamp of last refresh. */ | ||
lastRefresh: number; | ||
} | ||
|
||
type OptionsLatest = OptionsV2; | ||
type Options = OptionsV1 | OptionsV2; | ||
|
||
export {OptionsLatest as Optinons}; | ||
|
||
/** | ||
* The desired version of Glow to download locally. | ||
* @see https://github.com/charmbracelet/glow/releases | ||
*/ | ||
export const desiredGlowVersion = '1.4.1'; | ||
export const DESIRED_GLOW_VERSION = '1.4.1'; | ||
|
||
export const defaultOptions: Options = { | ||
export const DEFAULT_OPTIONS: Readonly<OptionsLatest> = { | ||
versionNumber: 2, | ||
glowVersion: null, | ||
versionNumber: 1, | ||
lastRefresh: 0, | ||
}; | ||
|
||
async function exists() { | ||
return pathExists(OPTIONS_PATH); | ||
} | ||
|
||
/** | ||
* Flush options object to filesystem. | ||
* @param data - Options to flush | ||
*/ | ||
export async function flushOptions(data: Options): Promise<void> { | ||
await fs.writeFile(optionsPath, `${JSON.stringify(data, undefined, 2)}${os.EOL}`, 'utf-8'); | ||
export async function flushOptions(data: OptionsLatest): Promise<void> { | ||
await fs.writeFile(OPTIONS_PATH, `${JSON.stringify(data, undefined, 2)}${os.EOL}`, 'utf-8'); | ||
} | ||
|
||
try { | ||
await fs.access(optionsPath); | ||
} catch { | ||
await fs.mkdir(cacheDir, {recursive: true}); | ||
await flushOptions(defaultOptions); | ||
async function readOptions(): Promise<OptionsLatest> { | ||
if (!(await exists())) { | ||
await fs.mkdir(CACHE_DIR, {recursive: true}); | ||
await flushOptions(DEFAULT_OPTIONS); | ||
} | ||
|
||
const contents = await fs.readFile(path.join(OPTIONS_PATH), 'utf-8'); | ||
|
||
const rawOptions = JSON.parse(contents) as Options; | ||
|
||
switch (rawOptions.versionNumber) { | ||
case 1: { | ||
const options: OptionsLatest = {...DEFAULT_OPTIONS, ...rawOptions, versionNumber: 2}; | ||
await flushOptions(options); | ||
|
||
return readOptions(); | ||
} | ||
|
||
case 2: { | ||
return rawOptions; | ||
} | ||
|
||
default: { | ||
throw new RangeError(`Unknown options version`); | ||
} | ||
} | ||
} | ||
|
||
export const options = JSON.parse(await fs.readFile(path.join(optionsPath), 'utf-8')) as Options; | ||
export const options = await readOptions(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import {homedir} from 'node:os'; | ||
import path from 'node:path'; | ||
|
||
export const cacheDir = path.join(homedir(), '.cache', 'how'); | ||
export const optionsPath = path.join(cacheDir, 'options.json'); | ||
export const tldrPath = path.join(cacheDir, 'tldr'); | ||
export const glowPath = path.join(cacheDir, 'glow'); | ||
export const CACHE_DIR = path.join(homedir(), '.cache', 'how'); | ||
export const OPTIONS_PATH = path.join(CACHE_DIR, 'options.json'); | ||
export const TLDR_PATH = path.join(CACHE_DIR, 'tldr'); | ||
export const GLOW_PATH = path.join(CACHE_DIR, 'glow'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
export class CommandError extends Error { | ||
constructor(message: string, stack?: boolean) { | ||
super(message); | ||
this.name = 'CommandError'; | ||
|
||
if (stack === false) { | ||
this.stack = undefined; | ||
} | ||
} | ||
} |
Oops, something went wrong.