├── tests ├── integration │ ├── fixtures │ │ ├── home.in │ │ ├── exit.in │ │ ├── browse_showbrowsepage.in │ │ ├── exit.out │ │ ├── home.out │ │ └── browse_showbrowsepage.out │ └── test.ts ├── test_deps.ts ├── tools │ └── expected_output.ts └── test_registry.ts ├── version.ts ├── docs ├── home.png ├── browse.png ├── search.png ├── settings.png ├── showcase.png └── module_info.png ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── test.yml ├── common.ts ├── deps.ts ├── egg.json ├── flag_checker.ts ├── pages ├── help_page.ts ├── home_page.ts ├── registries_page.ts ├── browse_page.ts ├── search_page.ts ├── settings_page.ts └── module_page.ts ├── LICENSE ├── utils.ts ├── cli ├── settings_cli.ts └── search_cli.ts ├── flag_parser.ts ├── theme.ts ├── examples └── example_registry_addon.ts ├── registries ├── registry.ts ├── registry_handler.ts ├── nest_land.ts └── deno_land.ts ├── ui.ts ├── mod.ts ├── settings.ts └── README.md /tests/integration/fixtures/home.in: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /version.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = 'v0.1.1'; -------------------------------------------------------------------------------- /tests/integration/fixtures/exit.in: -------------------------------------------------------------------------------- 1 | w 2 | \x1b[A 3 | -------------------------------------------------------------------------------- /tests/integration/fixtures/browse_showbrowsepage.in: -------------------------------------------------------------------------------- 1 | \x1b[A 2 | -------------------------------------------------------------------------------- /tests/integration/fixtures/exit.out: -------------------------------------------------------------------------------- 1 | ? ×××××KOPO CLI >> exit 2 | -------------------------------------------------------------------------------- /docs/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littletof/kopo-cli/HEAD/docs/home.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | eggs-debug.log 3 | 4 | tests/tools/*.in 5 | tests/tools/*.out -------------------------------------------------------------------------------- /docs/browse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littletof/kopo-cli/HEAD/docs/browse.png -------------------------------------------------------------------------------- /docs/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littletof/kopo-cli/HEAD/docs/search.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littletof/kopo-cli/HEAD/docs/settings.png -------------------------------------------------------------------------------- /docs/showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littletof/kopo-cli/HEAD/docs/showcase.png -------------------------------------------------------------------------------- /docs/module_info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/littletof/kopo-cli/HEAD/docs/module_info.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | ko_fi: littletof 2 | custom: ['https://www.buymeacoffee.com/littletof', 'https://coindrop.to/littletof'] 3 | -------------------------------------------------------------------------------- /tests/integration/fixtures/home.out: -------------------------------------------------------------------------------- 1 | ##CLS-KOPO## 2 | 3 | ? ×××××KOPO CLI 4 | >> browse 5 | search 6 | registries 7 | settings (no localStorage) 8 | help 9 | exit##up6####cha17## -------------------------------------------------------------------------------- /common.ts: -------------------------------------------------------------------------------- 1 | import { renderMarkdown } from "./deps.ts"; 2 | import { KopoOptions, Settings } from "./settings.ts"; 3 | 4 | export async function printReadme(text: string) { 5 | let readme = text; 6 | if (!await Settings.getKopoOption(KopoOptions.rawreadme)) { 7 | readme = renderMarkdown(text); 8 | } 9 | 10 | console.log(readme); 11 | 12 | return readme.split("\n").length; 13 | } 14 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export * as colors from "https://deno.land/std@0.119.0/fmt/colors.ts"; 2 | export { parse } from "https://deno.land/std@0.119.0/flags/mod.ts"; 3 | export type { Args } from "https://deno.land/std@0.119.0/flags/mod.ts"; 4 | 5 | export { renderMarkdown } from "https://deno.land/x/charmd@v0.0.1/mod.ts"; 6 | 7 | export { Command } from "https://deno.land/x/cliffy@v0.19.0/command/mod.ts"; 8 | export { Input } from "https://deno.land/x/cliffy@v0.19.1/prompt/input.ts"; 9 | export { Select } from "https://deno.land/x/cliffy@v0.19.1/prompt/select.ts"; 10 | export type { SelectValueOptions } from "https://deno.land/x/cliffy@v0.19.1/prompt/select.ts"; 11 | -------------------------------------------------------------------------------- /tests/integration/fixtures/browse_showbrowsepage.out: -------------------------------------------------------------------------------- 1 | ? ×××××KOPO CLI >> browse 2 | ##CLS-KOPO## 3 | KOPO CLI - Browsing - 🐛 Test registry 4 | 5 | ##cl-up2####up1## 6 | ? ××××× 7 | >> test1 10 * - test1 description 8 | test2 20 * - test2 description 9 | test3 30 * - test3 description 10 | test4 40 * - test4 description 11 | 12 | ---------- 1 / 1 (4) ---------- 13 | Search 14 | Next page 15 | Previous page 16 | Back##up10####cha34## -------------------------------------------------------------------------------- /tests/test_deps.ts: -------------------------------------------------------------------------------- 1 | /* std */ 2 | export { 3 | assert, 4 | assertEquals, 5 | assertStrictEquals, 6 | assertThrows, 7 | } from "https://deno.land/std@0.119.0/testing/asserts.ts"; 8 | export { 9 | bold, 10 | red, 11 | stripColor, 12 | } from "https://deno.land/std@0.119.0/fmt/colors.ts"; 13 | export { dirname } from "https://deno.land/std@0.119.0/path/mod.ts"; 14 | export { expandGlob } from "https://deno.land/std@0.119.0/fs/mod.ts"; 15 | export type { WalkEntry } from "https://deno.land/std@0.119.0/fs/mod.ts"; 16 | export { copy } from "https://deno.land/std@0.119.0/io/util.ts"; 17 | export { iter } from "https://deno.land/std@0.119.0/io/util.ts"; -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | stable: 11 | name: Deno Stable 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | os: [windows-latest] # macOS-latest, ubuntu-latest # importing addon from file fails on mac, ubuntu 17 | deno: [v1.x] 18 | steps: 19 | - name: Setup repo 20 | uses: actions/checkout@v2 21 | 22 | - name: Setup Deno 23 | uses: denoland/setup-deno@v1 24 | with: 25 | deno-version: ${{ matrix.deno }} 26 | 27 | - name: Run tests 28 | run: deno test --unstable --allow-run --allow-read -------------------------------------------------------------------------------- /egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://x.nest.land/eggs@0.3.3/src/schema.json", 3 | "name": "kopo", 4 | "description": "A Deno registry browser in the terminal", 5 | "homepage": "https://github.com/littletof/kopo-cli", 6 | "version": "v0.1.1", 7 | "unstable": true, 8 | "files": [ 9 | "./docs/**/*", 10 | "./registries/*", 11 | "./deps.ts", 12 | "./flag_checker.ts", 13 | "./flag_parser.ts", 14 | "./LICENSE", 15 | "./mod.ts", 16 | "./README.md", 17 | "./state_machine.ts", 18 | "./types.ts", 19 | "./utils.ts" 20 | ], 21 | "checkFormat": false, 22 | "checkTests": false, 23 | "checkInstallation": false, 24 | "check": true, 25 | "entry": "./mod.ts", 26 | "ignore": [], 27 | "unlisted": false 28 | } 29 | -------------------------------------------------------------------------------- /tests/tools/expected_output.ts: -------------------------------------------------------------------------------- 1 | import {runPrompt} from '../integration/test.ts'; 2 | 3 | /** 4 | You need 2 `\x1b[A` at the end of the file, to get the proper output. 5 | If you put one at the start, then one after each eg. arrow input, you will get the output for each step. 6 | */ 7 | const inputPath = new URL('exp.in', import.meta.url).href.replace(/file:\/\/\/?/, ""); 8 | const outputPath = new URL('exp.out', import.meta.url).href.replace(/file:\/\/\/?/, ""); 9 | 10 | const result = (await runPrompt(inputPath)) 11 | 12 | console.log(JSON.stringify(result.split('##CLSX##'), undefined, ' ')); 13 | 14 | const parsedOutput = result.replace("#END#", "") 15 | .split('##CLSX##') 16 | .at(-1); 17 | 18 | Deno.writeFileSync(outputPath, new TextEncoder().encode(parsedOutput)); 19 | console.log('DONE'); -------------------------------------------------------------------------------- /flag_checker.ts: -------------------------------------------------------------------------------- 1 | import { getFlags, toEmojiList } from "./flag_parser.ts"; 2 | 3 | if (!Deno.args?.[0]) { 4 | console.log("Please provide a path or url to the file to test"); 5 | } 6 | 7 | const path = Deno.args[0]; 8 | try { 9 | let text; 10 | if (/https?:\/\//.test(path)) { 11 | console.log(`Trying to fetch remote file: ${path}`); 12 | const resp = await fetch(path); 13 | text = await resp.text(); 14 | } else { 15 | console.log(`Trying to read file: ${path}`); 16 | text = Deno.readTextFileSync(path); 17 | } 18 | 19 | const flags = getFlags(text); 20 | 21 | if (flags) { 22 | console.log("The retrieved flags: \n"); 23 | console.log(flags); 24 | console.log(`\nAs emojis: ${toEmojiList(flags)}`); 25 | console.log("\n"); 26 | } else { 27 | console.log("\nCouldn't retrieve any flags\n"); 28 | } 29 | } catch { 30 | console.error( 31 | "Couldn't read file. Make sure --allow-read or --allow-net flag is provided.", 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /pages/help_page.ts: -------------------------------------------------------------------------------- 1 | import { Args, renderMarkdown } from "../deps.ts"; 2 | import { Theme } from "../theme.ts"; 3 | import { UI } from "../ui.ts"; 4 | import { VERSION } from "../version.ts"; 5 | 6 | export class HelpPage { 7 | static async show(args: Args, options?: {}) { 8 | console.log(renderMarkdown(`**KOPO CLI - Help**\n${HelpPage.helpText}`, {listIcons: ['', ' '], extensions: [{generateNode: (gnfn, node) => {if(node.type==="link"){return `${node.children![0].value}: ${Theme.accent(node.url!)}`}}}]})); 9 | 10 | await UI.selectList({ 11 | message: ' ', 12 | options: [ 13 | UI.listOptions.back 14 | ] 15 | }) 16 | } 17 | 18 | static readonly helpText = ` 19 | [Version](${VERSION}) 20 | [Repository](https://github.com/littletof/kopo-cli) 21 | [Feedback](https://github.com/littletof/kopo-cli/issues) 22 | 23 | - **UI**: 24 | - browse 25 | - search 26 | - registries 27 | - settings 28 | - help 29 | 30 | 31 | - **Commands**: 32 | - search 33 | - settings 34 | `; 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Szalay Kristóf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020- Szalay Kristóf. All rights reserved. MIT license. 2 | import { colors } from './deps.ts'; 3 | 4 | 5 | export function upInCL(num: number = 1) { 6 | return `\x1B[${num}A`; 7 | } 8 | 9 | export function backspace(num: number = 1) { 10 | return '\u0008'.repeat(num); 11 | } 12 | 13 | export function separator(num: number = 20) { 14 | return '-'.repeat(num); 15 | } 16 | 17 | export function menuState(name: string, disabled: boolean) { 18 | if(!disabled) { 19 | return colors.bold(colors.white(name)); 20 | } else { 21 | return colors.gray(name); 22 | } 23 | } 24 | 25 | export function paginateArray(array: T[], page: number, pageSize: number) { 26 | return array.slice((Math.max(0, page)-1) * pageSize, Math.min(array.length, page * pageSize)); 27 | } 28 | 29 | export async function fetchJSON(path: string): Promise { 30 | try { 31 | return await ((await fetch(path)).json()); 32 | } catch (e){ 33 | return {error: true}; 34 | } 35 | } 36 | export async function fetchText(path: string) { 37 | return await ((await fetch(path)).text()); 38 | } 39 | 40 | export function random(array: T[], take: number = 1) { 41 | // https://stackoverflow.com/a/19270021/1497170 42 | 43 | let result = new Array(take), 44 | len = array.length, 45 | taken = new Array(len); 46 | if (take > len) 47 | throw new RangeError("getRandom: more elements taken than available"); 48 | while (take--) { 49 | var x = Math.floor(Math.random() * len); 50 | result[take] = array[x in taken ? taken[x] : x]; 51 | taken[x] = --len in taken ? taken[len] : len; 52 | } 53 | return result; 54 | } -------------------------------------------------------------------------------- /cli/settings_cli.ts: -------------------------------------------------------------------------------- 1 | import { Args } from "../deps.ts"; 2 | import { Settings } from "../settings.ts"; 3 | 4 | export async function settingsCLI(args: Args) { 5 | if(args._.length < 2) { 6 | console.error('Settings command needs export or import subcommand'); 7 | } 8 | 9 | switch(args._[1]) { 10 | case "export": 11 | await exportSettings(args._[2] as any); 12 | break; 13 | case "import": 14 | await importSettings(args._[2] as any, {yes: args.yes}) 15 | break; 16 | } 17 | } 18 | 19 | async function exportSettings(path?: string) { 20 | 21 | if(!Settings.isSettingsAvailable()) { 22 | throw new Error('No settings are available. Have you provided the --location flag?'); 23 | } 24 | 25 | const exportPath = path ?? `./${getExportFileName()}`; 26 | 27 | await Deno.permissions.request({name: 'write', path: exportPath}); 28 | Deno.writeTextFileSync(exportPath, JSON.stringify(await Settings.getAllSetOptions(), undefined, 4)); 29 | } 30 | 31 | async function importSettings(path: string, options: {yes?: boolean}) { 32 | if(!Settings.isSettingsAvailable()) { 33 | throw new Error('No settings are available. Have you provided the --location flag?'); 34 | } 35 | 36 | if(options.yes || confirm('Importing settings will overwrite the current settings. Do you want to continue?')) { 37 | await Deno.permissions.request({name: 'read', path}); 38 | const settingsText = Deno.readTextFileSync(path); 39 | const settings = JSON.parse(settingsText); 40 | 41 | await Settings.clearAllOptions(); 42 | await Promise.all( 43 | settings.map((is: any) => { 44 | Settings.setOption(is.key, is.value); 45 | }) 46 | ); 47 | 48 | console.log(`Settings were successfully imported from ${path}`); 49 | } 50 | 51 | } 52 | 53 | function getExportFileName() { 54 | return `kopo_cli_settings_${Intl.DateTimeFormat('hu').format(new Date()).replaceAll(/\.\s/g, '_').replaceAll(/\./g, '')}.json`; 55 | } -------------------------------------------------------------------------------- /flag_parser.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020- Szalay Kristóf. All rights reserved. MIT license. 2 | const FtoEMap: {[key in FlagType]: string} = { 3 | "--unstable": '🚧', 4 | "--allow-net": "🌐", 5 | "--allow-read": "🔍", // 👓 6 | "--allow-write": "💾", 7 | "--allow-hrtime": "⏱", 8 | "--allow-run": "⚠", 9 | "--allow-all": "🔮", 10 | "--allow-env": "🧭", 11 | "--allow-plugin": "🧩", 12 | "--allow-ffi": "🧩", 13 | "--location": "🔰" 14 | } 15 | 16 | const flagRegexp = new RegExp(/\| .*\s?\`(--[^`]*)\`\s*\|([\s_\*(?:Y|yes)]*)\|(?:([^\|\n]*)\|)?/g); 17 | 18 | export function flagToEmoji(flag: FlagType) { 19 | return FtoEMap[flag]; 20 | } 21 | 22 | export function toEmojiList(flags?: Flags) { 23 | return flags ? `${flags.required?.map(f => flagToEmoji(f.flag)).join(" ")}` + (flags.optional.length ? ` (${flags.optional?.map(f => flagToEmoji(f.flag)).join(" ")})` : '') : '-'; // TODO undefined flags 24 | } 25 | 26 | export type FlagType = 27 | "--unstable" 28 | | "--allow-net" 29 | | "--allow-read" 30 | | "--allow-write" 31 | | "--allow-hrtime" 32 | | "--allow-run" 33 | | "--allow-all" 34 | | "--allow-env" 35 | | "--allow-plugin" 36 | | "--allow-ffi" 37 | | "--location" 38 | 39 | export interface Flags { 40 | required: {flag: FlagType, description?: string}[]; 41 | optional: {flag: FlagType, description?: string}[]; 42 | } 43 | export function getFlags(text: string): Flags | undefined { 44 | let result; 45 | const required = []; 46 | const optional = []; 47 | 48 | while((result = flagRegexp.exec(text)) !== null) { 49 | const flag = { 50 | flag: result[1] as unknown as FlagType, 51 | description: result[3].trim() 52 | } 53 | 54 | // _ -> ignore 55 | if(result[2].includes('_')){ continue; } 56 | 57 | // \* or yes -> required 58 | if(/\s*(\*|[Yy]es)\s*/.test(result[2])) { 59 | required.push(flag); 60 | } else { 61 | optional.push(flag); 62 | } 63 | } 64 | 65 | if(!required.length && !optional.length) { 66 | return undefined; 67 | } 68 | 69 | return { 70 | required, 71 | optional 72 | } 73 | } -------------------------------------------------------------------------------- /theme.ts: -------------------------------------------------------------------------------- 1 | import { colors } from "./deps.ts"; 2 | import { KopoOptions, Settings } from "./settings.ts"; 3 | import { random } from "./utils.ts"; 4 | 5 | export class Theme { 6 | 7 | static colors = colors; 8 | 9 | static themes: {[key: string]: {fg: (str: string) => string, bg: (str: string) => string}} = { 10 | "blue": {fg : colors.blue, bg: colors.bgBlue}, 11 | "cyan": {fg : colors.cyan, bg: colors.bgCyan}, 12 | "gray": {fg : colors.gray, bg: colors.bgBrightBlack}, 13 | "green": {fg : colors.green, bg: colors.bgGreen}, 14 | "magenta": {fg : colors.magenta, bg: colors.bgMagenta}, 15 | "red": {fg : colors.red, bg: colors.bgRed}, 16 | "yellow": {fg : colors.yellow, bg: colors.bgYellow}, 17 | "white": {fg : colors.white, bg: colors.bgWhite}, 18 | "random": {fg : a => a, bg: a => a}, 19 | // "heavy_random": a => a, 20 | }; 21 | 22 | static accent = (str: string) => str; 23 | static accentBg = (str: string) => str; 24 | 25 | static async init() { 26 | this.setThemeColors(await Settings.getKopoOption(KopoOptions.theme)); 27 | } 28 | 29 | static setThemeColors(theme: string) { 30 | this.accent = this.getColorForTheme(theme); 31 | this.accentBg = this.getBgColorForTheme(theme); 32 | } 33 | 34 | static getColorForTheme(theme: string) { 35 | if(theme === "random") { 36 | return this.themes[random(Object.keys(this.themes).filter(k => k !== 'heavy_random'))[0]].fg; 37 | } else if(theme === "heavy_random") { 38 | return (str: string) => this.themes[random(Object.keys(this.themes))[0]].fg(str); 39 | } else { 40 | return this.themes[theme].fg || colors.yellow; 41 | } 42 | } 43 | 44 | static getBgColorForTheme(theme: string) { // TODO refactor 45 | if(theme === "random") { 46 | return this.themes[random(Object.keys(this.themes).filter(k => k !== 'heavy_random'))[0]].bg; 47 | } else if(theme === "heavy_random") { 48 | return (str: string) => this.themes[random(Object.keys(this.themes))[0]].bg(str); 49 | } else { 50 | return this.themes[theme].bg || colors.yellow; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /examples/example_registry_addon.ts: -------------------------------------------------------------------------------- 1 | import { colors } from "../deps.ts"; 2 | import { Registry } from "../registries/registry.ts"; 3 | 4 | export class TestRegistry extends Registry { 5 | static key = 'test'; 6 | 7 | getWellKnownPath() { 8 | return "random"; 9 | } 10 | 11 | getRegistryInfo() { 12 | return { 13 | key: TestRegistry.key, 14 | name: colors.magenta('TEST/x'), 15 | icon: '🐛', 16 | url: 'https://test.land', 17 | description: '`TEST ADDON REGISTRY`' 18 | } 19 | } 20 | 21 | async getAllModuleNames() { 22 | return ['test1', 'test2']; 23 | } 24 | 25 | async getVersionsOfModule(moduleName: string) { 26 | return ['1', '2', '3'] 27 | } 28 | 29 | async getModulesList(query?: string, page: number=1, pageSize: number = 20) { 30 | return { 31 | modules: [ 32 | { 33 | name: 'testmodule', 34 | description: 'nincs', 35 | starCount: 5, 36 | owner: 'me', 37 | } 38 | ], 39 | page: 1, 40 | pageSize: 1, 41 | totalModules: 1, 42 | totalPages: 1, 43 | } 44 | } 45 | 46 | async getModuleInfo(moduleName: string, version?: string) { 47 | return { 48 | origin: "TEST" as any, 49 | info: { 50 | name: 'testt', 51 | description: 'testt', 52 | start_count: 5, 53 | versions: ['testt'], 54 | latestVersion: 'testt', 55 | 56 | repository: 'testt', 57 | moduleRoute: 'testt', 58 | // importRoute: 'testt', 59 | }, 60 | invalidVersion: false, 61 | currentVersion: 'testt', 62 | uploadedAt: new Date(), 63 | readmePath: 'testt', 64 | readmeText: 'testt', 65 | } 66 | } 67 | 68 | private getRepositoryPath(upload_options?: {type: string, repository: string, ref: string}, version?: string): string | undefined { 69 | return 'semmi'; 70 | } 71 | } 72 | 73 | export function getAddonRegistry() { 74 | return new TestRegistry(); 75 | } -------------------------------------------------------------------------------- /pages/home_page.ts: -------------------------------------------------------------------------------- 1 | import { Args } from "../deps.ts"; 2 | import { UI } from "../ui.ts"; 3 | import { Settings } from "../settings.ts"; 4 | import { OptionsPage } from "./settings_page.ts"; 5 | import { RegistriesPage } from "./registries_page.ts"; 6 | import { BrowsePage } from "./browse_page.ts"; 7 | import { SearchPage } from "./search_page.ts"; 8 | import { HelpPage } from "./help_page.ts"; 9 | 10 | export class HomePage { 11 | static async show(args: Args, options?: {}) { 12 | 13 | const homeOptions = { 14 | browse: UI.selectListOption({name: 'browse'}), 15 | search: UI.selectListOption({name: 'search'}), 16 | registries: UI.selectListOption({name: 'registries'}), 17 | settings: UI.selectListOption({name: "settings", disabled: !Settings.isSettingsAvailable(), disabledName: "settings (no localStorage)"}), 18 | help: UI.selectListOption({name: 'help'}), 19 | exit: UI.selectListOption({name: 'exit'}), 20 | } 21 | 22 | const selected = await UI.selectList({ 23 | message: "KOPO CLI", 24 | options: [ 25 | homeOptions.browse, 26 | homeOptions.search, 27 | homeOptions.registries, 28 | homeOptions.settings, 29 | homeOptions.help, 30 | homeOptions.exit, 31 | ], 32 | // default: "exit" 33 | }); 34 | 35 | if(homeOptions.browse.is(selected)) { 36 | UI.cls(); 37 | await BrowsePage.show(args); 38 | } 39 | 40 | if(homeOptions.search.is(selected)) { 41 | UI.cls(); 42 | await SearchPage.show(args); 43 | } 44 | 45 | if(homeOptions.registries.is(selected)) { 46 | UI.cls(); 47 | await RegistriesPage.show(args); 48 | } 49 | 50 | if(homeOptions.settings.is(selected)) { 51 | UI.cls(); 52 | await OptionsPage.show(args); 53 | } 54 | 55 | if(homeOptions.help.is(selected)) { 56 | UI.cls(); 57 | await HelpPage.show(args); 58 | } 59 | 60 | if(selected !== 'exit') { // TODO remove 61 | UI.cls(); 62 | await HomePage.show(args); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /registries/registry.ts: -------------------------------------------------------------------------------- 1 | import type {Flags} from "../flag_parser.ts"; 2 | 3 | 4 | export interface ModulesListPage { 5 | modules: { 6 | name: string; 7 | description?: string; 8 | starCount?: number; 9 | owner?: string; 10 | }[]; 11 | page: number; 12 | pageSize: number; 13 | totalModules: number; 14 | totalPages: number; 15 | query?: string; 16 | } 17 | export interface ModuleInfo { 18 | origin?: string; 19 | info?: { 20 | name: string; 21 | description?: string; 22 | start_count?: number; 23 | versions?: string[]; 24 | latestVersion?: string; 25 | 26 | repository?: string; 27 | moduleRoute?: string; 28 | }, 29 | invalidVersion?: boolean; 30 | currentVersion?: string, 31 | uploadedAt?: Date; 32 | flags?: Flags; 33 | readmePath?: string; 34 | readmeText?: string; 35 | } 36 | 37 | export abstract class Registry { 38 | addonUrl?: string; 39 | fromSettings?: boolean; 40 | 41 | static cache = new Map(); 42 | 43 | abstract getRegistryInfo(): {name: string, key: string, icon?: string, description?: string, url?: string}; 44 | abstract getWellKnownPath(): string; 45 | 46 | abstract getModulesList(query?: string, page?: number, pageSize?: number): Promise; 47 | abstract getModuleInfo(moduleName: string, version?: string): Promise; 48 | 49 | abstract getAllModuleNames(): Promise; 50 | abstract getVersionsOfModule(moduleName: string, version?: string): Promise; 51 | 52 | async fetch(path: string, opts?: {cache?: boolean, text?: boolean}): Promise { // TODO cli flag to overwrite cache --no-cache 53 | const pathUrl = new URL(path); 54 | 55 | if((await Deno.permissions.request({name: "net", host: pathUrl.host})).state !== 'granted') { 56 | console.error('NO NET'); 57 | return undefined; 58 | } 59 | 60 | if(opts?.cache && Registry.cache.has(path)) { 61 | return Registry.cache.get(path) as T; 62 | } 63 | 64 | try { 65 | const fetched = await fetch(pathUrl) 66 | const result = await (opts?.text ? fetched.text() : fetched.json()); 67 | if(opts?.cache) { 68 | Registry.cache.set(pathUrl.toString(), result); 69 | } 70 | return result; 71 | } catch (e){ 72 | return undefined; 73 | } 74 | } 75 | 76 | guessReadmePath(paths: string[]) { 77 | return paths.map(p => ({original: p, lower: p.toLowerCase()})).sort().find(p => p.lower.includes("readme.md"))?.original; 78 | } 79 | } -------------------------------------------------------------------------------- /tests/test_registry.ts: -------------------------------------------------------------------------------- 1 | import { Registry } from "../registries/registry.ts"; 2 | 3 | export class TestRegistry extends Registry { 4 | static key = 'test_registry'; 5 | 6 | getWellKnownPath() { 7 | return "test_wellknown"; 8 | } 9 | 10 | getRegistryInfo() { 11 | return { 12 | key: TestRegistry.key, 13 | name: 'Test registry', 14 | icon: '🐛', 15 | url: 'https://test.land', 16 | description: '`TEST ADDON REGISTRY`' 17 | } 18 | } 19 | 20 | async getAllModuleNames() { 21 | return ['test1', 'test2', 'test3', 'test4']; 22 | } 23 | 24 | async getVersionsOfModule(moduleName: string) { 25 | return ['v1', 'v2', 'v3'] 26 | } 27 | 28 | async getModulesList(query?: string, page: number=1, pageSize: number = 20) { 29 | return { 30 | modules: [ 31 | { name: 'test1', description: 'test1 description', starCount: 10, owner: 'me' }, 32 | { name: 'test2', description: 'test2 description', starCount: 20, owner: 'me' }, 33 | { name: 'test3', description: 'test3 description', starCount: 30, owner: 'me' }, 34 | { name: 'test4', description: 'test4 description', starCount: 40, owner: 'me' }, 35 | ], 36 | page: 1, 37 | pageSize: 1, 38 | totalModules: 4, 39 | totalPages: 1, 40 | } 41 | } 42 | 43 | async getModuleInfo(moduleName: string, version?: string) { 44 | if(!['test1','test2','test3','test4'].includes(moduleName)) { 45 | return undefined; 46 | } 47 | 48 | return { 49 | origin: TestRegistry.key, 50 | info: { 51 | name: moduleName, 52 | description: moduleName + " descrition", 53 | start_count: 5, 54 | versions: ['v1', 'v2', 'v3'], 55 | latestVersion: 'v3', 56 | 57 | repository: 'https://test.repo', 58 | moduleRoute: 'https://x/test.repo', 59 | // importRoute: 'testt', 60 | flags: { 61 | required: [{flag: "--unstable", description: "needed"}], 62 | optional: [{flag: "--location", description: "optional"}], 63 | } 64 | }, 65 | invalidVersion: false, 66 | currentVersion: 'testt', 67 | uploadedAt: new Date(2021,7,27,12,34), 68 | readmePath: 'testt', 69 | readmeText: 'testt', 70 | } 71 | } 72 | 73 | private getRepositoryPath(upload_options?: {type: string, repository: string, ref: string}, version?: string): string | undefined { 74 | return 'none'; 75 | } 76 | } 77 | 78 | export function getAddonRegistry() { 79 | return new TestRegistry(); 80 | } -------------------------------------------------------------------------------- /ui.ts: -------------------------------------------------------------------------------- 1 | import { renderMarkdown } from "./deps.ts"; 2 | import { Select, SelectValueOptions, Input } from "./deps.ts"; 3 | import { Theme } from "./theme.ts"; 4 | import { backspace, upInCL } from "./utils.ts"; 5 | 6 | export class UI { 7 | static listOptions = { 8 | back: UI.selectListOption({name: "Back", value: "kopo_back"}), 9 | separator: UI.selectListOption({name: "-".repeat(30), value: "kopo_separator", disabled: true}), 10 | empty: UI.selectListOption({name: " ", value: "kopo_empty", disabled: true}), 11 | disabled: (name: string) => UI.selectListOption({name, value: "kopo_disabled", disabled: true}), 12 | } 13 | 14 | static clearLine() { 15 | console.log(upInCL(1) + " ".repeat(70) + upInCL(1)); 16 | } 17 | 18 | static upInCL(lines?: number) { 19 | console.log(upInCL(lines)); 20 | } 21 | 22 | static cls() { 23 | console.log('\x1Bc'); 24 | } 25 | 26 | static async selectList(opts: {message: string, options: SelectValueOptions, default?: string, hint?: string, maxRows?: number}) { 27 | return await Select.prompt({ 28 | message: `${backspace(5)}${opts.message}`, 29 | options: opts.options.map(opt => (opt as any)['_ui_'] ? opt : UI.selectListOption(opt as any)), 30 | listPointer: `${Theme.accent('>>')}\x1b[1m`, 31 | // search: true, 32 | // searchIcon: '?*', 33 | // searchLabel: 'Search', 34 | // transform: value => value+'!!', // selected value transform 35 | // transform: value => '', 36 | pointer: '>>', // after selected 37 | keys: { 38 | previous: ['w', '8', 'up'], 39 | next: ['s', '2', 'down'], 40 | nextPage: ['n'], previousPage: ['p'] 41 | }, 42 | default: opts.default, 43 | maxRows: opts.maxRows ?? 20, 44 | hint: opts.hint, 45 | }); 46 | } 47 | 48 | static selectListOption(opts: string | {name: string, value?: string, disabled?: boolean, disabledName?: string}) { 49 | if(typeof opts === 'string') { 50 | opts = {name: opts}; 51 | } 52 | 53 | const value = opts.value ?? opts.name; 54 | 55 | return { 56 | name: (opts.disabled ? `${Theme.colors.gray(opts.disabledName ?? opts.name)}` : opts.name) + "\x1b[39m\x1b[0m", 57 | value, 58 | disabled: opts.disabled, 59 | _ui_: true, 60 | is(option: string) { 61 | return option === value; 62 | } 63 | } 64 | } 65 | 66 | static listHint(hint: string) { 67 | return UI.selectListOption({name: backspace(5) + renderMarkdown(`*${hint}*`), disabled: true}); 68 | } 69 | 70 | static async input(opts: {message: string, suggestions?: string[] | Promise}) { 71 | return await Input.prompt({ 72 | message: `${backspace(5)}${opts.message}`, 73 | suggestions: await opts.suggestions, 74 | pointer: ">>", 75 | // keys: {complete: ["enter", "right"]} 76 | }); 77 | } 78 | 79 | static async confirm(opts: {message: string}){ 80 | return confirm(opts.message); 81 | } 82 | } -------------------------------------------------------------------------------- /pages/registries_page.ts: -------------------------------------------------------------------------------- 1 | import { Args, renderMarkdown } from "../deps.ts"; 2 | import { RegistryHandler } from "../registries/registry_handler.ts"; 3 | import { KopoOptions, Settings } from "../settings.ts"; 4 | import { Theme } from "../theme.ts"; 5 | import { UI } from "../ui.ts"; 6 | import { upInCL } from "../utils.ts"; 7 | 8 | export class RegistriesPage { 9 | static async show(args: Args, options?: {}): Promise { 10 | 11 | const registriesSettings = await Settings.getKopoOption(KopoOptions.registries); 12 | 13 | const registriesOptions = (await RegistryHandler.getAllRegistries()).map(r => { 14 | const reg = r.getRegistryInfo(); 15 | 16 | /* ${reg.icon || '🗂🧮'} */ 17 | return UI.selectListOption({ 18 | name: `${r.addonUrl ? Theme.colors.yellow(`${r.fromSettings ? '*': '~'} `): ''}${reg.name}${registriesSettings?.[reg.key] === false ? Theme.colors.gray(' (disabled)'):''}`, 19 | value: reg.key 20 | }); 21 | }); 22 | 23 | const selectedOption = await UI.selectList({ 24 | message: 'KOPO CLI - Registries', 25 | options: [ 26 | ...registriesOptions, 27 | UI.listOptions.separator, 28 | UI.listOptions.back 29 | ] 30 | }); 31 | 32 | if(UI.listOptions.back.is(selectedOption)) { 33 | return; 34 | } 35 | 36 | // console.log(upInCL(2)); 37 | UI.clearLine(); 38 | const registry = RegistryHandler.getRegistry(selectedOption); 39 | const info = registry.getRegistryInfo(); 40 | 41 | const addonInfo = `${registry.addonUrl ? Theme.colors.yellow(`**Addon from ${(registry.fromSettings ? 'settings' : '*--registries* cli flag')}**\nPath: ${Theme.colors.cyan(registry.addonUrl)}`) : ''}` 42 | 43 | const details = renderMarkdown(`**KOPO CLI - Registries - ${info.icon ? info.icon + " ":""}${info.name}**\n${addonInfo}\n${info.description ? '> ' + info.description : ''}`)+`\nHome page: ${Theme.colors.cyan(info.url ? `${info.url}` : '')}`; 44 | const lines = details.split('\n').length; 45 | console.log(details); 46 | console.log(); 47 | UI.clearLine(); 48 | 49 | const disabled = registriesSettings?.[selectedOption] === false; 50 | const cliAddon = !!registry.addonUrl && !registry.fromSettings; // could allow disable, but if provied as flag, user should use it. 51 | 52 | const registryOptions = { 53 | enable: UI.selectListOption({name: 'Enable', disabled: !disabled || cliAddon, value: 'enable'}), 54 | disable: UI.selectListOption({name: 'Disable', disabled: disabled || cliAddon, value: 'disable'}), 55 | } 56 | 57 | const selected = await UI.selectList({ 58 | message: ' ', 59 | options: [ 60 | registryOptions.enable, 61 | registryOptions.disable, 62 | UI.listOptions.separator, 63 | UI.listOptions.back 64 | ] 65 | }); 66 | 67 | UI.clearLine(); 68 | console.log(upInCL(lines)); 69 | 70 | if(registryOptions.enable.is(selected) || registryOptions.disable.is(selected)) { 71 | const newEnabled = selected === registryOptions.enable.value; 72 | 73 | await Settings.setOption(KopoOptions.registries.key, Object.assign(registriesSettings, {[selectedOption]:newEnabled})); 74 | } 75 | 76 | 77 | UI.cls(); 78 | return await this.show(args, options); 79 | } 80 | } -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2020- Szalay Kristóf. All rights reserved. MIT license. 2 | import { Args, parse } from "./deps.ts"; 3 | import { KopoOptions, Settings } from "./settings.ts"; 4 | import { Theme } from "./theme.ts"; 5 | import { HomePage } from "./pages/home_page.ts"; 6 | import { UI } from "./ui.ts"; 7 | import { settingsCLI } from "./cli/settings_cli.ts"; 8 | import { RegistryHandler } from "./registries/registry_handler.ts"; 9 | import { search } from "./cli/search_cli.ts"; 10 | import { upInCL } from "./utils.ts"; 11 | 12 | /* 13 | 14 | /\ /\ 15 | | || | 16 | ..___[.][.] \ ________ 17 | | \ / ______| 18 | \_____ \______________________/ / 19 | | / 20 | \ | 21 | \ | 22 | | | _____________| / 23 | ____|__/ ___/ \_ \ | 24 | / __________/ \ |\ | 25 | |_| | | | | | | 26 | | | | | | | 27 | _/ / _/ / _/ / 28 | |___/ |___/ |___/ 29 | ____________________________________________________ 30 | KOPO CLI 31 | */ 32 | 33 | // deno run --allow-net --unstable --location https://kopo.mod.land mod.ts ui 34 | // deno run --allow-net --unstable --location https://kopo.mod.land --allow-write --allow-read mod.ts settings import ./test.json --yes 35 | 36 | async function startUI(args: Args) { 37 | if (await Settings.getKopoOption(KopoOptions.cls)) { 38 | UI.cls(); 39 | } 40 | 41 | if(await Settings.getKopoOption(KopoOptions.winprint)) { 42 | Deno.stdout.write = ((x: any) => { console.log(new TextDecoder().decode(x) + upInCL(1));}) as any; 43 | } 44 | 45 | console.log(); // so upInCL+clear doesnt jump 46 | 47 | await HomePage.show(args); 48 | } 49 | 50 | async function run() { 51 | const parsedArgs = parse(Deno.args, { 52 | boolean: ["json", "readme", "readme-raw", "exact", "yes", "flags"], 53 | alias: { e: "exact", v: "version", y: "yes" }, 54 | }); 55 | 56 | await Theme.init(); 57 | await RegistryHandler.initRegistries(parsedArgs); 58 | 59 | if (parsedArgs._?.length) { 60 | const cmd = parsedArgs._[0]; 61 | 62 | switch (cmd) { 63 | case "search": 64 | await search(parsedArgs); 65 | break; 66 | case "ui": 67 | await startUI(parsedArgs); 68 | break; 69 | 70 | case "settings": { 71 | await settingsCLI(parsedArgs); 72 | break; 73 | } 74 | } 75 | } else { 76 | await startUI(parsedArgs); 77 | } 78 | } 79 | 80 | await run(); 81 | 82 | // deno run --allow-net --no-check mod.ts search pretty 83 | // deno run --allow-net --no-check mod.ts search pretty --json 84 | // deno run --allow-net --no-check mod.ts search pretty_benching -e 85 | // deno run --allow-net --no-check mod.ts search pretty_benching -e --version v0.1.1 86 | // deno run --allow-net --no-check mod.ts search pretty_benching -e --json 87 | // deno run --allow-net --no-check mod.ts search pretty_benching -e --readme 88 | // deno run --allow-net --no-check mod.ts search pretty_benching -e --readme-raw 89 | 90 | // --------- 91 | // --no-prompt 92 | // if no registry is defined (-d / -n) in a --readme search eg., select deno, or next in line. 93 | // otherwise, if found in multiple registries give a prompt for the user to select from. but they should be the same... 94 | // -e --detailed 95 | // gives all versions, others from json output + readme by default?! 96 | // search from an import route 97 | // kopo find?? https://deno.land/x/kopo@v0.0.2/parse_flags.ts -> kopo search kopo -e -v v0.0.2 98 | 99 | // TODO test with module husky 100 | -------------------------------------------------------------------------------- /tests/integration/test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertEquals, 3 | dirname, 4 | expandGlob, 5 | WalkEntry, 6 | iter 7 | } from "../test_deps.ts"; 8 | 9 | import { upInCL } from "../../utils.ts"; 10 | 11 | const baseDir = `${dirname(import.meta.url).replace("file://", "")}`; 12 | 13 | for await (const file: WalkEntry of expandGlob(`${baseDir}/fixtures/*.in`)) { 14 | if (file.isFile) { 15 | const name = file.name.replace(/_/g, " ").replace(".in", ""); 16 | Deno.test({ 17 | name: `kopo - integration - ${name}`, 18 | async fn() { 19 | const output: string = await runPrompt(file.path); 20 | const expectedOutput: string = await getExpectedOutput(file.path); 21 | // console.log(JSON.stringify(cleanOutput(output).split('##CLSX##'))); 22 | assertEquals( 23 | cleanOutput(output) 24 | .replace("#END#", "") 25 | .split('##CLSX##') 26 | .at(-1), 27 | expectedOutput 28 | .replace(/\r\n/g, "\n") 29 | 30 | ); 31 | } 32 | }); 33 | } 34 | } 35 | 36 | function cleanOutput(output: string) { 37 | return output 38 | .replaceAll(/\r\n/g, "\n") 39 | .replaceAll('\x1Bc', "##CLS-KOPO##") // used by Kopo 40 | .replaceAll('\u001b[0J', '##CLSX##') // cls 41 | .replaceAll(upInCL(1) + " ".repeat(70) + upInCL(1)+'\n', '##cl-up2##') 42 | .replaceAll('\u001b[?25l', ''/* , '#l#' */) // hidecursor 43 | .replaceAll('\u001b[?25h', ''/* , '#h#' */) // showcursor 44 | //.replaceAll('\u001b[6A', ''/* , '#˘#' */) // cursor up 6 45 | .replaceAll('\u001b[G', ''/* , '#g#' */) // cursor stuff? 46 | // .replaceAll('\u001b[17G', '') // cursor horizontal absolute. 47 | .replace(/\u001b\[[0-9]+m/g, '') // text formatting 48 | .replaceAll('\b', '×') 49 | .replaceAll(/\u001b\[(\d+)G/g, '##cha$1##') // cursor horizontal absolute. 1 = first char of the line 50 | .replaceAll(/\u001b\[(\d+)A/g,"##up$1##") // cursor up \d 51 | .replace(/##CLSX##$/g, '#END#') // clsx at the end 52 | // .replaceAll(/\n[^×\n]*×+/g, '\n') // clear before backspace 53 | } 54 | 55 | async function getExpectedOutput(path: string) { 56 | const osOutputPath = path.replace(/\.in$/, `.${Deno.build.os}.out`); 57 | try { 58 | return await Deno.readTextFile(osOutputPath); 59 | } catch (_) { 60 | const outputPath = path.replace(/\.in$/, ".out"); 61 | return await Deno.readTextFile(outputPath); 62 | } 63 | } 64 | 65 | function getCmdFlagsForFile(filePath: string): string[] { 66 | /* if (file.name === "input_no_location_flag.ts") { 67 | return [ 68 | "--unstable", 69 | "--allow-all", 70 | ]; 71 | } 72 | return [ 73 | "--unstable", 74 | "--allow-all", 75 | "--location", 76 | "https://kopo.mod.land", 77 | ]; */ 78 | return [ 79 | "--allow-net", // TODO minimal 80 | "--allow-read", // TODO min glob 81 | "--unstable", 82 | ] 83 | } 84 | 85 | export async function runPrompt(filePath: string): Promise { 86 | const inputText = await Deno.readTextFile(filePath); 87 | const flags = getCmdFlagsForFile(filePath); 88 | const process = Deno.run({ 89 | stdin: "piped", 90 | stdout: "piped", 91 | cmd: [ 92 | "deno", 93 | "run", 94 | ...flags, 95 | 'mod.ts', 96 | "-r", new URL('../test_registry.ts', import.meta.url).href, // TODO not always needed, eg. registrySelect 97 | ], 98 | env: { 99 | NO_COLOR: "true", 100 | },clearEnv:true 101 | }); 102 | 103 | 104 | let result = ''; 105 | process.stdin.write(new TextEncoder().encode(inputText)).then(async _ => {await process.stdin.write(new TextEncoder().encode('\u0003'));/* Simulate CTRL+C */}); 106 | for await (let a of iter(process.stdout)) { 107 | result += new TextDecoder().decode(a); 108 | } 109 | 110 | process.stdin.close(); 111 | process.stdout.close(); 112 | process.close(); 113 | 114 | // assert(bytesCopied > 0, "No bytes copied"); 115 | 116 | return cleanOutput(result); 117 | } -------------------------------------------------------------------------------- /pages/browse_page.ts: -------------------------------------------------------------------------------- 1 | import { Args, renderMarkdown } from "../deps.ts"; 2 | import { Registry } from "../registries/registry.ts"; 3 | import { RegistryHandler } from "../registries/registry_handler.ts"; 4 | import { KopoOptions, Settings } from "../settings.ts"; 5 | import { Theme } from "../theme.ts"; 6 | import { UI } from "../ui.ts"; 7 | import { ModulePage } from "./module_page.ts"; 8 | import { SearchPage } from "./search_page.ts"; 9 | 10 | export class BrowsePage { 11 | static async show(args: Args, options?: {}) { 12 | const registry = await RegistryHandler.getRegistryWithSelector(); 13 | if(!registry) { 14 | return; // TODO error msg 15 | } 16 | 17 | return await this.showBrowsePage(registry, args, options); 18 | } 19 | 20 | static async showBrowsePage(registry: Registry, args: Args, options?: {page?: number, last?: string, query?: string}): Promise { 21 | const info = registry.getRegistryInfo(); 22 | const title = renderMarkdown(`**KOPO CLI - ${options?.query ? 'Searching' : 'Browsing'} - ${info.icon ? info.icon + " ":""}${info.name}${options?.query? ` - ${Theme.accent(options.query)}` : ''}**`); 23 | console.log(title); 24 | 25 | UI.clearLine(); 26 | 27 | options = Object.assign({page: 1}, options); // default options 28 | 29 | const moduleList = await registry.getModulesList(options.query, options.page, await Settings.getKopoOption(KopoOptions.pagesize)); 30 | const separatorWithInfo = Object.assign({}, UI.listOptions.separator, {name: `---------- ${Theme.colors.bold(`${moduleList.page}`)} ${Theme.colors.reset(Theme.accent('/'))} ${moduleList.totalPages} ${Theme.colors.gray(`(${moduleList.totalModules})`)} ----------`}) 31 | 32 | UI.upInCL(1); 33 | 34 | const browseOptions = { 35 | next: UI.selectListOption({name: 'Next page', value:'kopo_next', disabled: moduleList.totalPages === 0 || moduleList.page === moduleList.totalPages}), 36 | prev: UI.selectListOption({name: 'Previous page', value:'kopo_prev', disabled: moduleList.totalPages === 0 || moduleList.page === 1}), 37 | search: UI.selectListOption({name: 'Search', value:'kopo_search'}), 38 | } 39 | 40 | const emojiStar = Deno.build.os !== "windows" || await Settings.getKopoOption(KopoOptions.winprint); 41 | const star = emojiStar ? '⭐' : Theme.colors.yellow('*'); 42 | const starPad = emojiStar ? 16 : 26; 43 | 44 | const selected = await UI.selectList({ 45 | message: ' ', 46 | options: [ 47 | ...moduleList.modules.map(m => UI.selectListOption({ 48 | name: `${m.name.padEnd(28)}${isNaN(m.starCount as number) ? '' : `${Theme.colors.italic(`${m.starCount}`)} ${star}`.padStart(starPad)} - ${Theme.colors.gray(m.description?.slice(0, 50) + (m.description && m.description.length > 50 ? "...": ""))}`, 49 | value: `kopomodule#${m.name}` 50 | })), 51 | ...(moduleList.modules.length === 0 ? [UI.listOptions.disabled('No modules found...')] : []), 52 | 53 | UI.listOptions.empty, 54 | separatorWithInfo, 55 | ...(options.query ? []: [browseOptions.search]), 56 | browseOptions.next, 57 | browseOptions.prev, 58 | UI.listOptions.back 59 | ], 60 | default: options.last, 61 | maxRows: await Settings.getKopoOption(KopoOptions.pagesize) + 10 62 | }); 63 | 64 | if(browseOptions.search.is(selected)) { 65 | UI.cls(); 66 | await SearchPage.show(args, {registries: [registry]}); 67 | UI.cls(); 68 | return this.showBrowsePage(registry, args, options); 69 | } 70 | 71 | if(browseOptions.next.is(selected)) { 72 | UI.cls(); 73 | await this.showBrowsePage(registry, args, {page: moduleList.page+1, last: moduleList.page+1 !== moduleList.totalPages ? browseOptions.next.value : undefined}); 74 | } 75 | if(browseOptions.prev.is(selected)) { 76 | UI.cls(); 77 | await this.showBrowsePage(registry, args, {page: moduleList.page-1, last: moduleList.page-1 !== 1 ? browseOptions.prev.value : undefined}); 78 | } 79 | 80 | if(selected.startsWith('kopomodule#')) { 81 | UI.cls(); 82 | await ModulePage.show(args, {module: selected.split('#')[1], registry, showTitle: true}); 83 | options.last = selected; 84 | return await BrowsePage.showBrowsePage(registry, args, options); 85 | } 86 | 87 | UI.clearLine(); 88 | } 89 | } -------------------------------------------------------------------------------- /cli/search_cli.ts: -------------------------------------------------------------------------------- 1 | import { Args,renderMarkdown } from "../deps.ts"; 2 | import { HomePage } from "../pages/home_page.ts"; 3 | import { ModulePage } from "../pages/module_page.ts"; 4 | import { SearchPage } from "../pages/search_page.ts"; 5 | import { ModulesListPage } from "../registries/registry.ts"; 6 | import { RegistryHandler } from "../registries/registry_handler.ts"; 7 | import { Settings,KopoOptions } from "../settings.ts"; 8 | import { Theme } from "../theme.ts"; 9 | import { UI } from "../ui.ts"; 10 | 11 | export async function search(args: Args) { 12 | 13 | if(args._.length>1) { 14 | const searchTerm = `${args._[1]}`; 15 | const registry = (await RegistryHandler.getRegistries())[0]; 16 | 17 | if(args.exact) { 18 | const module = await registry.getModuleInfo(searchTerm, args.version); 19 | 20 | if(!module) { 21 | console.error("Module not found. If you want to search for a term don't use the \"--exact\" or \"-e\" flag.\n"); 22 | return; 23 | } 24 | 25 | if(module?.invalidVersion) { 26 | console.error(`Error: ${module.info!.name} doesn't seem to have a version [${args.version}].`); 27 | module.info?.versions ? console.error(`Latest version is: [${module.info.latestVersion}]\nOther possible versions: [${module.info?.versions.join(", ")}]` ) : {}; 28 | return; 29 | } 30 | 31 | if(args.flags) { 32 | if(args.json) { 33 | console.log(JSON.stringify(module.flags, undefined, 4)); 34 | return; 35 | } 36 | 37 | const latest = module.info?.latestVersion === module.currentVersion; 38 | const title = Theme.colors.bold(`${Theme.accent(module.info?.name!)}${module.invalidVersion ? '' : ` @ ${module.currentVersion}${latest ? Theme.colors.gray(' (latest)') : ''}`}\n`); 39 | console.log(title); 40 | await ModulePage.renderFlagsInfo(module); 41 | return; 42 | } 43 | 44 | if(args.json) { 45 | console.log(JSON.stringify(Object.assign(module, {readmeText: undefined}), undefined, 4)); 46 | return; 47 | } 48 | 49 | if(args.readme && module?.readmeText) { 50 | console.log(renderMarkdown(module.readmeText)); 51 | return; 52 | } 53 | 54 | if(args["readme-raw"] && module?.readmeText) { 55 | console.log(module.readmeText); 56 | return; 57 | } 58 | 59 | await ModulePage.renderModuleInfo(module); 60 | return; 61 | } else { // not exact 62 | 63 | // const moduleList = await registry.getModulesList(searchTerm); 64 | const registryResult: {[key: string]: ModulesListPage['modules']} = await (await RegistryHandler.getRegistries()).reduce(async (prev, r) => Object.assign(await prev, {[r.getRegistryInfo().key]: (await r.getModulesList(searchTerm)).modules}), Promise.resolve({})); 65 | if(args.json) { 66 | console.log(JSON.stringify(registryResult, undefined, 4)); 67 | return; 68 | } 69 | if(Object.keys(registryResult).length && Object.values(registryResult).some((r: ModulesListPage['modules']) => !!r?.length)) { 70 | 71 | if(args.readme) { 72 | // console.log("readme without -e?!") 73 | return; 74 | } 75 | 76 | 77 | console.log(`Search result in registries: `); 78 | 79 | Object.keys(registryResult).forEach(key => { 80 | if(registryResult[key].length) { 81 | console.log(`\t${key}:`); 82 | registryResult[key].forEach(m => console.log(`\t\t- ${`${m.name}:`.padEnd(20)} ${m.description?.slice(0, 70) + (!m.description || m.description?.length > 70 ? "…": "")}`)) 83 | } else { 84 | console.log(`\t${key}: Not found`); 85 | } 86 | }); 87 | 88 | 89 | } else { 90 | console.log("No module found"); 91 | } 92 | } 93 | 94 | 95 | } else { 96 | if(await Settings.getKopoOption(KopoOptions.cls)) { 97 | UI.cls(); 98 | } 99 | 100 | return await SearchPage.show(args); 101 | /* UI.cls(); 102 | return await HomePage.show(args); */ 103 | } 104 | } -------------------------------------------------------------------------------- /settings.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "./theme.ts"; 2 | 3 | export interface ISettings { 4 | isSettingsAvailable(): boolean; 5 | getOption(key: OptionType, def?: T): Promise; 6 | setOption(key: OptionType, value: T): Promise; 7 | removeOption(key: OptionType): Promise; 8 | getAllSetOptions(): Promise<{key: OptionType, value: Object}[] | []>; 9 | clearAllOptions(): Promise; 10 | } 11 | 12 | export class LocalStorageSettings implements ISettings{ 13 | isSettingsAvailable() { 14 | return this.hasLocationFlag(); 15 | } 16 | 17 | hasLocationFlag() { 18 | try { 19 | const a = location.href; 20 | return true; 21 | } catch { 22 | return false; 23 | } 24 | } 25 | 26 | async getOption(key: OptionType, def?: T) { 27 | if(!this.isSettingsAvailable()) { 28 | return def; 29 | } 30 | 31 | const value = localStorage.getItem(key); 32 | return value ? JSON.parse(value) : def; 33 | } 34 | 35 | async setOption(key: OptionType, value: T) { 36 | if(!this.isSettingsAvailable()) { 37 | return; 38 | } 39 | 40 | localStorage.setItem(key, JSON.stringify(value)); 41 | } 42 | 43 | async removeOption(key: OptionType) { 44 | if(!this.isSettingsAvailable()) { 45 | return; 46 | } 47 | 48 | localStorage.removeItem(key); 49 | } 50 | 51 | async getAllSetOptions() { 52 | if(!this.isSettingsAvailable()) { 53 | return []; 54 | } 55 | 56 | return await Promise.all(new Array(localStorage.length).fill(0).map((_, i) => localStorage.key(i)!).map(async k => ({key: k, value: await this.getOption(k)}))); 57 | } 58 | 59 | async clearAllOptions() { 60 | if(!this.isSettingsAvailable()) { 61 | return; 62 | } 63 | 64 | localStorage.clear(); 65 | } 66 | } 67 | 68 | export interface OptionConf { 69 | name: string; 70 | key:string; 71 | help?: string; 72 | /** To hide in settings page */ 73 | hidden?: boolean; 74 | def?: any; 75 | /** value transformer */ 76 | valueTf?: (v: any) => string; 77 | valueSet?: any[]; 78 | onChange?: (nv: any) => Promise; 79 | } 80 | 81 | export type OptionType = Extract; 82 | 83 | export function booleanOption(options: OptionConf & {valueSet?: never, valueTf?: never}): OptionConf { 84 | return {...options, valueSet: [true, false], valueTf: v => v ? Theme.colors.green('true') : Theme.colors.gray('false')} 85 | } 86 | 87 | export const KopoOptions: {[key: string]: OptionConf} = { 88 | "theme": {key: "theme", name: "Theme", def: "blue", valueTf: (v:string) => Theme.getColorForTheme(v)(v), valueSet: Object.keys(Theme.themes), onChange: async v => await Theme.init()}, 89 | "cls": booleanOption({key: "cls", name: "Cls on start", def: true, help: "When set to `true`, the console is cleared on start.\nSame as a `cls` command."}), 90 | "rawreadme": booleanOption({key: "rawreadme", name: "Print raw readme", def: false}), 91 | "pagesize": {key: "pagesize", name: "Page size", def: 10, valueSet: [5, 10, 25, 50, 100], help: "This defines, how many modules should appear on one page, when\nbrowsing or searching modules.\nOnly change this, if you have enough vertical terminal space."}, 92 | 93 | "registries": {key: "registries", name: "Registries", hidden: true, def: {}}, 94 | "registry_addons": {key: "registry_addons", name: "Registry addons", def: [], valueSet: [], valueTf: v => v?.length ? `${v?.length} addons`: 'No addons'}, 95 | 96 | // UNSTABLE 97 | 98 | "winprint": booleanOption({key: "winprint", name: "Windows print", def: false, hidden: true, help: "unstable. Changes stdout to console log, so special charactes print correctly on windows"}), 99 | } 100 | 101 | export class Settings { 102 | static settingsStrategy = new LocalStorageSettings(); 103 | 104 | static isSettingsAvailable() { 105 | return this.settingsStrategy.isSettingsAvailable(); 106 | } 107 | 108 | static async getKopoOption(opt: OptionConf) { 109 | return await Settings.getOption(opt.key, opt.def); 110 | } 111 | 112 | static async getOption(key: OptionType, def?: T) { 113 | return await this.settingsStrategy.getOption(key, def); 114 | } 115 | 116 | static async setOption(key: OptionType, value: T) { 117 | await this.settingsStrategy.setOption(key, value); 118 | } 119 | 120 | static async removeOption(key: OptionType) { 121 | await this.settingsStrategy.removeOption(key); 122 | } 123 | 124 | static async getAllSetOptions() { 125 | return await this.settingsStrategy.getAllSetOptions(); 126 | } 127 | 128 | static async clearAllOptions() { 129 | return await this.settingsStrategy.clearAllOptions(); 130 | } 131 | } -------------------------------------------------------------------------------- /pages/search_page.ts: -------------------------------------------------------------------------------- 1 | import { Args } from "../deps.ts"; 2 | import { Registry } from "../registries/registry.ts"; 3 | import { RegistryHandler } from "../registries/registry_handler.ts"; 4 | import { Theme } from "../theme.ts"; 5 | import { UI } from "../ui.ts"; 6 | import { backspace } from "../utils.ts"; 7 | import { BrowsePage } from "./browse_page.ts"; 8 | import { ModulePage } from "./module_page.ts"; 9 | 10 | export class SearchPage { 11 | static async show(args: Args, options?: {searchTerm?: string, exact?: boolean, registries?: Registry[], version?: string}): Promise { 12 | options = Object.assign({}, options); 13 | 14 | if(!options.searchTerm) { 15 | options.searchTerm = await UI.input({ 16 | message: 'KOPO CLI - Search - Enter search term' 17 | }); 18 | 19 | if(!options.searchTerm) { 20 | return; 21 | } 22 | 23 | options.version = options.searchTerm.split("@")?.[1]; 24 | options.searchTerm = options.searchTerm.split("@")?.[0]; 25 | } 26 | 27 | UI.cls(); 28 | const searchIndicator = `Searching for [${Theme.accent(options.searchTerm)}${options.version? ` @ ${options.version}`: ''}]...`; 29 | console.log( searchIndicator + backspace(searchIndicator.length) + `\x1B[${1}A`); 30 | 31 | await this.showSearchResult(args, options as any); 32 | } 33 | 34 | static async showSearchResult(args: Args, options: {searchTerm: string, exact?: boolean, registries?: Registry[], version?: string}): Promise { 35 | const registries = options.registries ?? await RegistryHandler.getRegistries(); 36 | 37 | const exactMatches = (await Promise.all(registries.map(async registry => { 38 | const moduleInfo = await registry.getModuleInfo(options.searchTerm, options.version); 39 | if(!moduleInfo) { 40 | return undefined; 41 | } 42 | 43 | return { 44 | registry, 45 | result: moduleInfo 46 | } 47 | }))).filter(res => !!res); 48 | 49 | 50 | // one registry and no exact match -> browse 51 | if(exactMatches.length === 0 && registries.length === 1) { 52 | return await BrowsePage.showBrowsePage(registries[0], args, {query: options.searchTerm}); 53 | } 54 | 55 | const exactOpts = exactMatches.map(r => ({result: r, ...UI.selectListOption({ 56 | name: `${r?.registry.getRegistryInfo().name} - ${Theme.accent(`${r?.result.info?.name}`)}${` @ ${r!.result.invalidVersion ? r?.result.info?.latestVersion : r?.result.currentVersion}`}`, 57 | value: r?.registry.getRegistryInfo().key, 58 | })})) 59 | .filter(r => !r.result?.result.invalidVersion); // when searching with version, only show exact version matches. TODO rework, show module, with not exact version 60 | 61 | const browseRegistryOptions = registries.map(r => ({registry: r, ...UI.selectListOption({name: r.getRegistryInfo().name, value: `kopo_browse_${r.getRegistryInfo().key}`})})); 62 | 63 | const finalOptions = []; 64 | if(!options.exact) { 65 | finalOptions.push( 66 | UI.listOptions.disabled(Theme.colors.white(Theme.colors.bold(backspace(2) + 'Exact matches:'))), 67 | ...(exactOpts.length > 0 ? exactOpts : [UI.listOptions.disabled('No exact matches...')]), 68 | 69 | UI.listOptions.empty, 70 | UI.listOptions.disabled(Theme.colors.white(Theme.colors.bold(backspace(2) + 'Search similar modules in:'))), 71 | ...browseRegistryOptions, 72 | ); 73 | } else { 74 | // only exact matches, without headers 75 | finalOptions.push(...(exactOpts.length > 0 ? exactOpts : [UI.listOptions.disabled('No exact matches...')])); 76 | } 77 | 78 | const selected = await UI.selectList({ 79 | message: `KOPO CLI - ${registries.length === 1? `${registries[0].getRegistryInfo().name} - ` : ''}Search for: ${Theme.accent(options.searchTerm)}${options.version? ` @ ${options.version}`: ''}`, 80 | options: [ 81 | UI.listOptions.empty, 82 | ...finalOptions, 83 | 84 | UI.listOptions.empty, 85 | UI.listOptions.separator, 86 | UI.listOptions.back 87 | ], 88 | }); 89 | 90 | const selectedExact = exactOpts.find(eo => eo.is(selected)); 91 | if(selectedExact) { 92 | UI.cls(); 93 | await ModulePage.show(args, {module: selectedExact.result?.result.info?.name!, registry: selectedExact.result?.registry!, showTitle: true, version: options.version}); 94 | UI.cls(); 95 | return await this.showSearchResult(args, options); 96 | } 97 | 98 | const selectedBrowseRegistry = browseRegistryOptions.find(ro => ro.is(selected)); 99 | if(selectedBrowseRegistry) { 100 | UI.cls(); 101 | await BrowsePage.showBrowsePage(selectedBrowseRegistry.registry, args, {query: options.searchTerm}); 102 | UI.cls(); 103 | return await this.showSearchResult(args, options); 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /registries/registry_handler.ts: -------------------------------------------------------------------------------- 1 | import { Args, parse } from "../deps.ts"; 2 | import { KopoOptions, Settings } from "../settings.ts"; 3 | import { UI } from "../ui.ts"; 4 | import { DenoRegistry } from "./deno_land.ts"; 5 | import { NestRegistry } from "./nest_land.ts"; 6 | import { Registry } from "./registry.ts"; 7 | 8 | export class RegistryHandler { 9 | static readonly registries: {[key: string]: Registry} = { 10 | [DenoRegistry.key]: new DenoRegistry(), 11 | [NestRegistry.key]: new NestRegistry() 12 | } 13 | 14 | static async initRegistries(args: Args) { 15 | const registryAddonsFromSettings = await Settings.getKopoOption(KopoOptions.registry_addons); 16 | const loadedRegistryAddons = (await RegistryHandler.loadRegistriesFromUrl(registryAddonsFromSettings)).map(la => {la.fromSettings = true; return la}); 17 | 18 | const cliRegistries = this.getRegistriesListFromArgs(Deno.args) || {addons:[]}; 19 | const loadedCliRegistries = await RegistryHandler.loadRegistriesFromUrl(cliRegistries.addons); 20 | 21 | [...loadedRegistryAddons, ...loadedCliRegistries].forEach(addon => this.registries[addon.getRegistryInfo().key] = addon); 22 | } 23 | 24 | static getRegistry(key: string): Registry { 25 | return (this.registries as any)[key]; 26 | } 27 | 28 | static async getAllRegistries() { 29 | return Object.values(RegistryHandler.registries); 30 | } 31 | 32 | static async getRegistries() { 33 | const allRegistries = await this.getAllRegistries(); 34 | 35 | const registriesInSettings = await Settings.getKopoOption(KopoOptions.registries); 36 | const registryIsNonDisabled = (registry: Registry) => { 37 | return registriesInSettings[registry.getRegistryInfo().key] !== false; 38 | } 39 | 40 | const cliRegisties = this.getRegistriesListFromArgs(Deno.args); 41 | const registryAddedFromCli = (registry: Registry) => { 42 | return cliRegisties?.keys.includes(registry.getRegistryInfo().key) 43 | || (registry.addonUrl && cliRegisties?.addons.includes(registry.addonUrl)); 44 | } 45 | 46 | return allRegistries.filter(r => cliRegisties ? registryAddedFromCli(r) : registryIsNonDisabled(r)); 47 | } 48 | 49 | static async getRegistryWithSelector(): Promise { 50 | const availableRegistries = await RegistryHandler.getRegistries(); 51 | 52 | if(!availableRegistries.length) { 53 | console.log('NO registries available. Check your registry settings!'); 54 | } 55 | 56 | let selectedRegistry; 57 | if(availableRegistries.length > 1) { 58 | const selected: string = await UI.selectList({ 59 | message:'Select a registry to browse', 60 | options: [ 61 | ...availableRegistries.map((r, i) => { 62 | const reg = r.getRegistryInfo(); 63 | return UI.selectListOption({name: `${reg.name}`, value: `${i}`}); 64 | }), 65 | UI.listOptions.separator, 66 | UI.listOptions.back 67 | ] 68 | }); 69 | UI.clearLine(); 70 | 71 | if(!UI.listOptions.back.is(selected)) { 72 | selectedRegistry = availableRegistries[selected as any]; 73 | } 74 | } else { 75 | selectedRegistry = availableRegistries[0]; 76 | } 77 | 78 | return selectedRegistry; 79 | } 80 | 81 | private static async loadRegistriesFromUrl(urls: string[]) { 82 | const addons: Registry[] = (await Promise.all(urls.map(async (raUrl: string, i: number) => { 83 | try { 84 | const addon = await import(raUrl); 85 | return Object.assign(addon.getAddonRegistry(),{addonUrl: raUrl}); 86 | } catch(e) { 87 | console.log(e, `Cant import addon from: ${raUrl}`); 88 | } 89 | })) as Registry[]).filter((addon: Registry) => !!addon); 90 | 91 | return addons; 92 | } 93 | 94 | private static getRegistriesListFromArgs(args: string[]): {addons: string[]; keys: string[]} | undefined { 95 | const parsedArgs = parse(args, {alias: {r: "registries"}}); 96 | if(parsedArgs.registries) { 97 | return this.getRegistriesListFromFlag(parsedArgs.registries); 98 | } 99 | return undefined; 100 | } 101 | 102 | private static getRegistriesListFromFlag(regs: string | string[] | true): {addons: string[]; keys: string[]} { 103 | const groupedRegistries: {addons: string[]; keys: string[]} = {addons: [], keys: []}; 104 | 105 | if(typeof regs === 'boolean') { 106 | throw new Error('Needs registry keys or addon paths, when providing --registries flag'); 107 | } 108 | const cliRegs = Array.isArray(regs) ? regs : regs.split(','); 109 | 110 | cliRegs.forEach(reg => { 111 | try { 112 | new URL(reg); 113 | groupedRegistries.addons.push(reg); 114 | } catch { 115 | groupedRegistries.keys.push(reg); 116 | } 117 | }) 118 | 119 | return groupedRegistries; 120 | } 121 | } -------------------------------------------------------------------------------- /registries/nest_land.ts: -------------------------------------------------------------------------------- 1 | import { getFlags } from "../flag_parser.ts"; 2 | import { paginateArray } from "../utils.ts"; 3 | import { ModuleInfo, Registry } from "./registry.ts"; 4 | 5 | export interface NestModuleInfo { 6 | name: string, 7 | normalizedName: string, 8 | owner: string, 9 | description?: string, 10 | repository?: string, 11 | latestVersion?: string, 12 | latestStableVersion?: string, 13 | packageUploadNames?: string[], 14 | locked?: boolean, 15 | malicious?: boolean, 16 | unlisted: boolean, 17 | updatedAt: string, 18 | createdAt: string 19 | } 20 | export interface NestModuleVersionInfo { 21 | name: string; 22 | package: { 23 | name: string; 24 | owner: string; 25 | description: string; 26 | createdAt: string; 27 | 28 | }; 29 | entry: string; 30 | version: string; 31 | prefix: string; 32 | malicious: unknown; 33 | files: {[key: string]: {inManifest: string, txId: string}}; 34 | } 35 | export class NestRegistry extends Registry { 36 | static key = 'nest'; 37 | 38 | getWellKnownPath() { 39 | return "https://intellisense.nest.land/deno-import-intellisense.json"; 40 | } 41 | 42 | getRegistryInfo() { 43 | return { 44 | key: NestRegistry.key, 45 | name: 'x.nest.land', 46 | icon: '🥚', 47 | url: 'https://nest.land', 48 | description: `Nest.land combines \`Deno\` with the \`Arweave\`.\nHere you can publish your Deno modules to the permaweb, where they can never be deleted.\nThis avoids a major pitfall for web-based module imports while allowing developers to leverage Deno's import design!` 49 | } 50 | } 51 | 52 | async getAllModuleNames() { 53 | const response = await this.fetch("https://intellisense.nest.land/api/x", {cache: true}); 54 | if(!response) { 55 | return []; 56 | } 57 | 58 | return response.concat("std"); 59 | } 60 | 61 | async getVersionsOfModule(moduleName: string) { 62 | const response = await this.fetch(`https://intellisense.nest.land/api/x/${moduleName}`, {cache: true}); 63 | if(!response) { 64 | return []; 65 | } 66 | 67 | return response; 68 | } 69 | 70 | async getModulesList(query?: string, page: number=1, pageSize: number = 20) { 71 | const allNestModules = await this.fetch(`https://x.nest.land/api/packages`, {cache: true}); 72 | 73 | if(!allNestModules) { 74 | return {modules: [], page, pageSize, totalModules: 0, totalPages: 0, query}; 75 | } 76 | 77 | const filteredModules = query ? allNestModules.filter(m => m.name?.includes(query) || m.description?.includes(query)) 78 | : allNestModules; 79 | const modulesOnPage = paginateArray(filteredModules, page, pageSize); 80 | 81 | const count = query ? modulesOnPage.length : allNestModules.length; 82 | 83 | return { 84 | modules: (modulesOnPage || []).map(d => ({name: d.name, description: d.description, owner: d.owner})), 85 | query, 86 | page, 87 | pageSize, 88 | totalModules: count, 89 | totalPages: Math.ceil(count/pageSize), 90 | }; 91 | } 92 | async getModuleInfo(moduleName: string, version?: string) { 93 | const moduleInfo = await this.fetch(`https://x.nest.land/api/package/${moduleName}`); 94 | // could use https://intellisense.nest.land/api/x/MODULE, but thats one extra ... 95 | 96 | if(!moduleInfo) { 97 | return undefined; 98 | } 99 | 100 | const latestVersion = moduleInfo.latestVersion?.split("@")[1]; 101 | version = version ?? (latestVersion || undefined); 102 | 103 | const moduleData: Partial = { 104 | origin: NestRegistry.key, 105 | info: { 106 | name: moduleInfo.name, 107 | description: moduleInfo.description, 108 | versions: moduleInfo.packageUploadNames?.map(pn => pn.split("@")[1]), 109 | latestVersion: latestVersion, 110 | repository: moduleInfo.repository, 111 | moduleRoute: `https://nest.land/package/${moduleName}` 112 | } 113 | }; 114 | 115 | const invalidVersion = !!version && !moduleData.info?.versions?.includes(version); 116 | 117 | moduleData.invalidVersion = invalidVersion; 118 | 119 | if(!invalidVersion) { 120 | moduleData.currentVersion = version; 121 | 122 | const versionInfo = await this.fetch(`https://x.nest.land/api/package/${moduleName}/${version}`); 123 | 124 | if(versionInfo) { 125 | moduleData.uploadedAt = new Date(versionInfo.package.createdAt); 126 | moduleData.readmePath = this.guessReadmePath(Object.keys(versionInfo.files).map(k => versionInfo.files[k].inManifest)); 127 | // TODO https://nest.land/api/readme?mod=kopo@v0.1.1 128 | if(moduleData.readmePath) { 129 | const readmeText = await this.fetch(`https://x.nest.land/${moduleName}@${version}${moduleData.readmePath}`, {text: true, cache: true}); 130 | moduleData.readmeText = readmeText; 131 | moduleData.flags = getFlags(readmeText || ""); 132 | } 133 | } 134 | } 135 | 136 | 137 | 138 | return moduleData; 139 | } 140 | } -------------------------------------------------------------------------------- /pages/settings_page.ts: -------------------------------------------------------------------------------- 1 | import { Args, SelectValueOptions } from "../deps.ts"; 2 | import { UI } from "../ui.ts"; 3 | import { KopoOptions,OptionConf,Settings } from "../settings.ts"; 4 | import {backspace, upInCL} from '../utils.ts'; 5 | import { Theme } from "../theme.ts"; 6 | 7 | export class OptionsPage { 8 | static async show(args: Args, state?: {selected?: string}): Promise { 9 | const options: SelectValueOptions = (await Promise.all(Object.values(KopoOptions).map(async opt => { 10 | if(opt.hidden) { 11 | return; 12 | } 13 | 14 | 15 | let setValue = await Settings.getOption(opt.key, typeof opt.def !== "undefined" ? opt.def : "default"); 16 | if(opt.valueTf) { 17 | setValue = opt.valueTf!(setValue); 18 | } 19 | 20 | return ({ 21 | name: `${`${opt.name}:`.padEnd(20)} ${setValue}`, 22 | value: opt.key 23 | }) 24 | }))).filter(o => !!o) as SelectValueOptions; 25 | 26 | const listOptions = { 27 | clear: UI.selectListOption({name: 'Clear all settings', value: 'kopo_clear'}), 28 | allOptions: UI.selectListOption({name: 'List set options', value: 'kopo_all'}), 29 | exportSettings: UI.selectListOption({name: `Export settings ${Theme.colors.green('>>')}`, value: 'kopo_export_settings'}), 30 | importSettings: UI.selectListOption({name: `Import settings ${Theme.accent('<<')}`,value: 'kopo_import_settings', disabled: true}), 31 | } 32 | 33 | const selectedOption = await UI.selectList({ 34 | message: "KOPO CLI - Settings", 35 | options: [ 36 | ...options, 37 | UI.listOptions.separator, 38 | // listOptions.allOptions, 39 | listOptions.exportSettings, 40 | // TODO listOptions.importSettings, 41 | listOptions.clear, 42 | UI.listOptions.separator, 43 | UI.listOptions.back 44 | ], 45 | }); 46 | 47 | if(Object.keys(KopoOptions).map(k => KopoOptions[k].key).includes(selectedOption)) { 48 | UI.clearLine() 49 | await this.showSubOptions(args, {optionKey: selectedOption}); 50 | UI.clearLine() 51 | return await this.show(args, state); 52 | } 53 | 54 | if(listOptions.allOptions.is(selectedOption)) { 55 | console.log(upInCL(2), await Settings.getAllSetOptions()) 56 | // console.log(await Settings.getAllSetOptions()); 57 | console.log(); 58 | } 59 | if(listOptions.clear.is(selectedOption)) { 60 | if(await UI.confirm({message: 'Are you sure, you want to delete all your settings?'})) { 61 | await Settings.clearAllOptions(); 62 | console.log("Cleared"); 63 | 64 | // TODO reload settings properly 65 | await Theme.init(); 66 | } 67 | 68 | UI.cls(); 69 | return await this.show(args, state); 70 | } 71 | if(listOptions.exportSettings.is(selectedOption)) { 72 | const exportPath = `./kopo_cli_settings_${Intl.DateTimeFormat('hu').format(new Date()).replaceAll(/\.\s/g, '_').replaceAll(/\./g, '')}.json`; 73 | // TODO move to cli command, so can use path autocomplete for different path. // kopo settings export path 74 | if(await UI.confirm({message: 'Exporting settings will need write permissions. Do you want to continue?'})) { 75 | const perm_to_export = await Deno.permissions.request({name: 'write', path: exportPath}); 76 | Deno.writeTextFileSync(exportPath, JSON.stringify(await Settings.getAllSetOptions(),undefined, 4)); 77 | } 78 | 79 | return await this.show(args, state); 80 | } 81 | 82 | if(listOptions.importSettings.is(selectedOption)){ 83 | // TODO 84 | } 85 | } 86 | 87 | static listHint(option: OptionConf) { 88 | return (option.help ? [UI.listHint(option.help)] : []); 89 | } 90 | 91 | static async showSubOptions(args: Args, state: {optionKey: string}) { 92 | const option = KopoOptions[state.optionKey]; 93 | 94 | const listOptions = { 95 | sub_reset: UI.selectListOption({name: "Reset to default", value: "reset"}) 96 | } 97 | 98 | if(option.valueSet) { 99 | const defaultValue = option.valueSet.indexOf(await Settings.getOption(option.key, option.def)); 100 | 101 | const selectedOption = await UI.selectList({ 102 | message: `KOPO CLI - Settings - ${option.name}`, 103 | options: [ 104 | ...OptionsPage.listHint(option), 105 | ...option.valueSet.map((k, i) => ({name: option.valueTf ? option.valueTf(k) : k, value: `${i}`})), //value by index, because other is not allowed 106 | UI.listOptions.separator, 107 | listOptions.sub_reset, 108 | UI.listOptions.back 109 | ], 110 | default: defaultValue !== -1 ? defaultValue.toString() : undefined, // get index of default value 111 | // hint: option.help ? UI.listHint(option.help).name : undefined 112 | // hint: option.help 113 | }); 114 | 115 | if(option.valueSet.includes(option.valueSet[+selectedOption])) { 116 | await Settings.setOption(option.key, option.valueSet[+selectedOption]); 117 | if(option.onChange) { 118 | await option.onChange(option.valueSet[+selectedOption]); 119 | } 120 | return; 121 | } 122 | 123 | if(listOptions.sub_reset.is(selectedOption)) { 124 | await Settings.removeOption(option.key); 125 | if(option.onChange) { 126 | await option.onChange(option.valueSet[+selectedOption]); 127 | } 128 | return; 129 | } 130 | } 131 | } 132 | } -------------------------------------------------------------------------------- /registries/deno_land.ts: -------------------------------------------------------------------------------- 1 | import { getFlags } from "../flag_parser.ts"; 2 | import { Registry,ModuleInfo } from "./registry.ts"; 3 | 4 | export interface DenoModuleListDataType { 5 | total_count: number; 6 | options: {limit?: number, page?: number, sort?: string}; 7 | results: {name: string, description?: string, star_count?: number, search_score?: number}[] 8 | } 9 | export class DenoRegistry extends Registry { 10 | static key = 'deno'; 11 | 12 | getWellKnownPath() { 13 | return "https://deno.land/.well-known/deno-import-intellisense.json"; 14 | } 15 | 16 | getRegistryInfo() { 17 | return { 18 | key: DenoRegistry.key, 19 | name: 'deno.land/x', 20 | icon: '🦕', 21 | url: 'https://deno.land/x', 22 | description: '`deno.land/x` is a hosting service for Deno scripts.\nIt caches releases of open source modules stored on `GitHub` and serves them at one easy to remember domain.' 23 | } 24 | } 25 | 26 | async getAllModuleNames() { 27 | const response = await this.fetch("https://api.deno.land/modules?simple=1", {cache: true}); 28 | if(!response) { 29 | return []; 30 | } 31 | 32 | return response.concat("std"); 33 | } 34 | 35 | async getVersionsOfModule(moduleName: string) { 36 | const response = await this.fetch(`https://deno.land/_vsc1/modules/${moduleName}`, {cache: true}); 37 | if(!response) { 38 | return []; 39 | } 40 | 41 | return response; 42 | } 43 | 44 | async getModulesList(query?: string, page: number=1, pageSize: number = 20) { 45 | const response = await this.fetch<{success?: boolean; data: DenoModuleListDataType}>( 46 | `https://api.deno.land/modules?page=${page}&limit=${pageSize}${query? `&query=${query}`: ""}` 47 | ); 48 | if(!response?.success) { 49 | return {modules: [], page, pageSize, totalModules: 0, totalPages: 0, query}; 50 | } 51 | 52 | if(query) { 53 | response.data.total_count = response.data.results.length; 54 | } 55 | 56 | return { 57 | modules: (response.data.results || []).map(d => ({name: d.name, description: d.description, starCount: d.star_count})), 58 | query, 59 | page, 60 | pageSize, 61 | totalModules: response.data.total_count, 62 | totalPages: Math.ceil(response.data.total_count/pageSize), 63 | }; 64 | } 65 | 66 | async getModuleInfo(moduleName: string, version?: string) { 67 | 68 | // https://cdn.deno.land/MODULE/meta/versions.json -> {latest, versions:[]} // https://deno.land/_vsc1/modules/MODULE 69 | // https://api.deno.land/modules/MODULE -> sima info desc, name, star_count 70 | // https://cdn.deno.land/MODULE/versions/v0.3.0/meta/meta.json -> {uploaded_at, upload_options: {type: github, repository: "denosaurs/cache", ref: "0.2.12"}, directory_listing: {path: "/cache.ts", size: 2240, type: "file/dir"}[]} 71 | // https://cdn.deno.land/MODULE/versions/v0.3.0/raw/README.md 72 | 73 | const moduleInfo = await this.fetch<{success: boolean, data: {name: string, description?: string, star_count?: number}}>(`https://api.deno.land/modules/${moduleName}`); 74 | 75 | if(!moduleInfo || !moduleInfo.success) { 76 | return undefined; 77 | } 78 | 79 | const moduleData: Partial = {origin: DenoRegistry.key}; 80 | 81 | const versionInfo = await this.fetch<{latest: string, versions: string[]}>(`https://cdn.deno.land/${moduleName}/meta/versions.json`); 82 | 83 | version = version ?? (versionInfo?.latest || undefined); 84 | 85 | const invalidVersion = !!version && !versionInfo?.versions.includes(version); 86 | 87 | moduleData.info = { 88 | versions: versionInfo?.versions, 89 | latestVersion: versionInfo?.latest, 90 | name: moduleInfo.data.name, 91 | description: moduleInfo.data.description, 92 | start_count: moduleInfo.data.star_count, 93 | moduleRoute: `https://deno.land/x/${moduleName}${version!== versionInfo?.latest ? `@${version}`: ""}`, 94 | }; 95 | moduleData.invalidVersion = invalidVersion; 96 | 97 | if(!invalidVersion) { 98 | moduleData.currentVersion = version; 99 | 100 | const metaInfo = await this.fetch< 101 | {uploaded_at: string, upload_options: {type: string, repository: string, ref: string}, directory_listing: {path: string, size: number, type: "file" | "dir"}[]} 102 | >(`https://cdn.deno.land/${moduleName}/versions/${version}/meta/meta.json`); 103 | 104 | if(metaInfo) { 105 | moduleData.info.repository = this.getRepositoryPath(metaInfo.upload_options, version !== versionInfo?.latest ? version : undefined); 106 | moduleData.uploadedAt = new Date(metaInfo.uploaded_at); 107 | 108 | const readmePath = this.guessReadmePath(metaInfo.directory_listing.filter(dl => dl.type === "file").map(f => f.path)); 109 | if(readmePath) { 110 | moduleData.readmePath = readmePath; 111 | 112 | const readmeText = await this.fetch(`https://cdn.deno.land/${moduleName}/versions/${version}/raw${readmePath}`, {text: true, cache: true}); 113 | moduleData.readmeText = readmeText; 114 | moduleData.flags = getFlags(readmeText || ""); 115 | } 116 | } 117 | 118 | } 119 | 120 | return moduleData; 121 | } 122 | 123 | private getRepositoryPath(upload_options?: {type: string, repository: string, ref: string}, version?: string): string | undefined { 124 | if(!upload_options) { 125 | return; 126 | } 127 | 128 | switch(upload_options.type) { 129 | case "github": return `https://github.com/${upload_options.repository}${version ? `/tree/${version}` : ''}` 130 | default: return `${upload_options.type} - ${upload_options.repository}`; 131 | } 132 | } 133 | } -------------------------------------------------------------------------------- /pages/module_page.ts: -------------------------------------------------------------------------------- 1 | import { Args, renderMarkdown } from "../deps.ts"; 2 | import { printReadme } from "../common.ts"; 3 | import { flagToEmoji, toEmojiList } from "../flag_parser.ts"; 4 | import { ModuleInfo, Registry } from "../registries/registry.ts"; 5 | import { RegistryHandler } from "../registries/registry_handler.ts"; 6 | import { Theme } from "../theme.ts"; 7 | import { UI } from "../ui.ts"; 8 | import { SearchPage } from "./search_page.ts"; 9 | 10 | export class ModulePage { 11 | static async show(args: Args, options: {module: string, registry: Registry, version?: string, showTitle?: boolean}): Promise { 12 | if(options.showTitle) { 13 | const regInfo = options.registry.getRegistryInfo(); 14 | const title = renderMarkdown(`**KOPO CLI - ${regInfo.icon ? regInfo.icon + " ":""}${regInfo.name} - Module - ${Theme.accent(options.module)}${options.version ? ` @ ${options.version}` : ''}**`); 15 | console.log(title); 16 | } 17 | const module = await options.registry.getModuleInfo(options.module, options.version); 18 | // console.log(module); 19 | if(!module) { 20 | return; // TODO errormsg 21 | } 22 | 23 | const lines = await this.renderModuleInfo(module); 24 | 25 | const moduleOptionsCandidates = { 26 | readme: UI.selectListOption({name: 'Show README', value: 'readme'}), 27 | flags: UI.selectListOption({name: 'Show flags info', value: 'flags'}), 28 | diff_version: UI.selectListOption({name: 'Select a different version', value: 'diff_ver'}), 29 | other_registries: UI.selectListOption({name: 'Check other registries', value: 'other_regs'}), 30 | } 31 | const moduleOptions = []; 32 | 33 | if(module.readmeText) { 34 | moduleOptions.push(moduleOptionsCandidates.readme); 35 | } 36 | 37 | if(module.flags) { 38 | moduleOptions.push(moduleOptionsCandidates.flags); 39 | } 40 | 41 | if(module.info?.versions?.length! > 1) { 42 | moduleOptions.push(moduleOptionsCandidates.diff_version); 43 | } 44 | 45 | if((await RegistryHandler.getRegistries()).length > 1){ 46 | moduleOptions.push(moduleOptionsCandidates.other_registries); 47 | } 48 | 49 | const selected = await UI.selectList({ 50 | message: ' ', 51 | options: [ 52 | UI.listOptions.separator, 53 | ...moduleOptions, 54 | UI.listOptions.back 55 | ] 56 | }); 57 | 58 | UI.upInCL(lines+3); 59 | 60 | if(UI.listOptions.back.is(selected)) { 61 | UI.cls(); 62 | return; 63 | } 64 | 65 | if(moduleOptionsCandidates.readme.is(selected)) { 66 | UI.cls(); 67 | console.log(Theme.colors.bgRed(Theme.colors.white((`------------ Start of README for ${module.info?.name} ------------\n`)))); 68 | await printReadme(module.readmeText || ''); 69 | console.log(Theme.colors.gray((`------------ End of README for (${module.info?.name})------------\n`))); 70 | 71 | return await this.show(args, options); 72 | } 73 | 74 | if(moduleOptionsCandidates.flags.is(selected)) { 75 | UI.cls(); 76 | 77 | const regInfo = options.registry.getRegistryInfo(); 78 | const title = renderMarkdown(`**KOPO CLI - ${regInfo.icon ? regInfo.icon + " ":""}${regInfo.name} - Module - ${Theme.accent(options.module)} @ ${module.currentVersion} - Flags 🚩**`); 79 | console.log(title); 80 | 81 | this.renderFlagsInfo(module); 82 | 83 | await UI.selectList({ 84 | message: ' ', 85 | options: [ 86 | UI.listOptions.back 87 | ], 88 | }); 89 | 90 | UI.cls(); 91 | return await this.show(args, options); 92 | } 93 | 94 | if(moduleOptionsCandidates.diff_version.is(selected)) { 95 | // options.registry.getVersionsOfModule() 96 | UI.cls(); 97 | const version = await UI.selectList({ // TODO allow search??? 98 | message: `KOPO CLI - Module - ${options.module} - Select version`, 99 | options: [ 100 | UI.listOptions.back, 101 | UI.listOptions.separator, 102 | UI.listOptions.empty, 103 | ...(module.info?.versions?.map(v => UI.selectListOption({name: v})) || []), 104 | UI.listOptions.empty, 105 | UI.listOptions.separator, 106 | UI.listOptions.back 107 | ], 108 | 109 | default: module.currentVersion, 110 | maxRows: 10 111 | }); 112 | if(UI.listOptions.back.is(version)) { 113 | UI.clearLine(); 114 | return await this.show(args, options); 115 | } 116 | const newVersion: any = {}; 117 | Object.assign(newVersion, options, {version}); 118 | 119 | UI.cls(); 120 | return await this.show(args, newVersion); 121 | } 122 | 123 | if(moduleOptionsCandidates.other_registries.is(selected)) { 124 | UI.cls(); 125 | await SearchPage.showSearchResult(args, {searchTerm: module.info?.name!, exact: true}); 126 | UI.cls(); 127 | return await this.show(args, options); 128 | } 129 | } 130 | 131 | static async renderModuleInfo(module: ModuleInfo) { 132 | let moduleInfoText = ""; 133 | 134 | const latest = module.info?.latestVersion === module.currentVersion; 135 | moduleInfoText += Theme.colors.bold(`${Theme.accent(module.info?.name!)}${module.invalidVersion ? '' : ` @ ${module.currentVersion}${latest ? Theme.colors.gray(' (latest)') : ''}`}\n`); 136 | 137 | 138 | const description = renderMarkdown(`> ${module.info?.description}`)+"\n"; 139 | moduleInfoText += description; 140 | 141 | 142 | if(!isNaN(module.info?.start_count as any)) { 143 | moduleInfoText += `**Stars:** ${Theme.colors.italic(`${module.info!.start_count}`)}⭐\n`; 144 | } 145 | if(module.flags) { 146 | moduleInfoText += `**Flags:** ${toEmojiList(module.flags)}\n`; 147 | } 148 | if(module.uploadedAt) { 149 | moduleInfoText += `**Uploaded at:** ${Intl.DateTimeFormat(undefined, {dateStyle: "short", timeStyle: "short"}).format(module.uploadedAt)}\n`; 150 | } 151 | if(!latest && module.info?.latestVersion) { 152 | moduleInfoText += `**Latest version:** ${module.info?.latestVersion}\n`; 153 | } 154 | moduleInfoText += '\n'; 155 | moduleInfoText += ` 📍 ${Theme.colors.cyan(module.info?.repository || '-')}\n`; 156 | moduleInfoText += `📦 ${Theme.colors.cyan(module.info?.moduleRoute || '-')}\n`; // 🔗 157 | 158 | console.log(renderMarkdown(moduleInfoText)); 159 | 160 | return moduleInfoText.split('\n').length; 161 | } 162 | 163 | static async renderFlagsInfo(module: ModuleInfo) { 164 | let text = ''; 165 | 166 | if(!module.flags?.required && !module.flags?.optional) { 167 | console.log(Theme.colors.gray('No flag definition found for this module.')); 168 | } 169 | 170 | if(module.flags?.required.length) { 171 | text+='|Required|Description|\n|:--|:--|\n' 172 | module.flags?.required.forEach(f => text+= `|${flagToEmoji(f.flag)} \`${f.flag}\`|${f.description}|\n`); 173 | } 174 | 175 | if(module.flags?.optional.length) { 176 | text+=`| ${Theme.colors.blue(Theme.colors.bold(`Optional`))} | ${Theme.colors.blue(Theme.colors.bold(`Description`))} |\n${text.length ? '' : '|:--|:--|\n'}`; 177 | module.flags?.optional.forEach(f => text+= `|${flagToEmoji(f.flag)} \`${f.flag}\`|${f.description}|\n`); 178 | } 179 | 180 | console.log(renderMarkdown(text)); 181 | } 182 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kopo Cli 🐶 2 | 3 | *A Deno registry browser in the terminal* 4 | 5 | [![deno badge](https://img.shields.io/badge/deno.land/x-success?logo=deno&logoColor=black&labelColor=white&color=black)](https://deno.land/x/kopo) 6 | [![nest badge](https://nest.land/badge.svg)](https://nest.land/package/kopo) 7 | 8 | ![showcase](docs/browse.png) 9 | 10 | ## Description 11 | 12 | `kopo` is a cli tool, which helps you browse the Deno registries in your terminal in an easy, nicely presented way. 13 | 14 | It supports `deno.land/x` and `x.nest.land` by default, but also enables you to use [addons](#registries-and-addons), to access different registries. 15 | 16 | ## Usage 17 | 18 | `kopo` is intended to be as an installed user script. For that, run: 19 | 20 | ```bash 21 | deno install --unstable --allow-net -f --name kopo --location https://kopo.mod.land https://deno.land/x/kopo@v0.1.1/mod.ts 22 | ``` 23 | 24 | To just try it out, run the following command: 25 | 26 | ```bash 27 | deno run --unstable --allow-net --location https://kopo.mod.land https://deno.land/x/kopo@v0.1.1/mod.ts 28 | ``` 29 | 30 | ## Features 31 | 32 | - browse the registries 33 | - search through registries for a keyword/specific module 34 | - get a specific module's info, eg: description, stars, repo, versions, readme 35 | - print a module's readme as a formatted markdown text with [charmd](https://deno.land/x/charmd) 36 | - show what [flags](#-flags) a module requires 37 | - An addon system, to access private or not yet added registries. 38 | - Persisted settings, like theme, disabled registries, other preferences + export, import of it 39 | 40 | `kopo`'s features can be accessed in two may separate ways. 41 | 42 | - Use it as an interactive application with a menu system 43 | - Issue simple commands and get only what you need in the cli 44 | 45 | ## As an app 46 | 47 | If installed with the command above, you simply need to issue the `kopo` command in your terminal, than the main menu should appear: 48 | 49 | ![home](./docs/home.png) 50 | 51 | After that, you can navigate the menu using the or w s buttons. (On windows you can't use the arrows currently :/ [issue](https://github.com/denoland/deno/issues/5945)) 52 | 53 | ### Browse 54 | 55 | Select one from the enabled registries, than browse the paginated modules list or search for a term. 56 | 57 | ![browse](./docs/browse.png) 58 | 59 | ### Search 60 | 61 | Issue a global search in all registries. 62 | 63 | ![search](docs/search.png) 64 | 65 | Use the `@` and a version to search for a specific version, like `kopo@v0.0.2`. 66 | If the search term isn't a modules name, you can search the registries for that term with the options at the bottom. 67 | 68 | ### Module info 69 | 70 | After you select a module, you will see it's info: 71 | 72 | ![module_info](docs/module_info.png) 73 | 74 | From here, you can print it's README as a formatted MarkDown(using [charmd](https://github.com/littletof/charmd)) if one was found for it, get details about its [flags](#-flags), see other versions of it or see which other registries have this module registered. 75 | 76 | ### Settings 77 | If the `--location` flag was provided, you can access the settings, where you can tailor some features to your likings: 78 | 79 | ![settings](docs/settings.png) 80 | 81 | ## As a `CLI` tool 82 | 83 | > 🚧 The API needs work, so it's bound to be changed 🚧 84 | 85 | ### Search 86 | 87 | Your starting point is: 88 | ```bash 89 | kopo search 90 | ``` 91 | Without any following parameters, the search menu will pop up for you. 92 | 93 | If you want to search for a specific search term eg `charmd` issue: 94 | ```bash 95 | kopo search charmd 96 | ``` 97 | 98 | If you know the module's name you are searching for add `--exact` or `-e` flag. 99 | ```bash 100 | kopo search kopo -e 101 | ``` 102 | This will get the first exact match from the first registry and display the modules info for you. Also, you can add `-v [exact version]` to get a specific version of the module. 103 | 104 | If you have `-e` you can add `--readme` or `--read-raw` so the modules **README** will be printed instead. 105 | ```bash 106 | kopo search charmd -e --readme 107 | ``` 108 | Adding `--flags` will print the module's [flags](#-flags) described in the readme if found. 109 | ```bash 110 | kopo search kopo -e --flags 111 | ``` 112 | 113 | Adding `--json` flag for any of the above commands, will result in a formatted json output, instead of formatted text. 114 | ```bash 115 | kopo search charmd -e --json 116 | ``` 117 | 118 | ### Settings 119 | You can export or import your settings which are stored in localStorage. 120 | > If `--allow-read` and `--allow-write` flags are not provided at install, the script will request it in runtime. 121 | 122 | ```bash 123 | kopo settings export settings.json 124 | kopo settings import settings.json 125 | ``` 126 | 127 | ## Registries and Addons 128 | 129 | Currently `deno.land/x` and `x.nest.land` is supported by default. However, you can use addons to integrate with other, not yet supported or private registries. 130 | 131 | For this, first you need to have path (remote or local) to the registry addon, which should `extend` the [`Registry`](./registries/registry.ts) class. ([example](./examples/example_registry_addon.ts)) 132 | 133 | With that, you can add this path to your exported settings json and import it back into the app or use the `-r` flag for any of the commands. 134 |
135 | settings.json 136 | 137 | ```json 138 | [ 139 | { 140 | "key": "registry_addons", 141 | "value": [ 142 | "https://raw.githubusercontent.com/littletof/kopo-cli/remaster/examples/example_registry_addon.ts" 143 | ] 144 | } 145 | ] 146 | ``` 147 | 148 |
149 | 150 | This flag will override which registries `kopo` will use during its runtime. 151 | ```bash 152 | kopo -r deno,nest,https://raw.githubusercontent.com/littletof/kopo-cli/remaster/examples/example_registry_addon.ts 153 | ``` 154 | > Use `file:///` prefix for any local files: `file:///C:/example.ts` 155 | 156 | If the `-r` flag is used, no registry, that is not defined will be accessible. 157 | Also, if `-r` is not used, you can enable/disable your builtin and through settings added registries in the application. 158 | 159 | For example, if you want to search only the `nest` registry you can use: 160 | ```bash 161 | kopo search kopo -e -r nest --readme 162 | ``` 163 | 164 | ### 🚩 Flags 165 | 166 | > 🚧 This is just an experimental feature currently 🚧 167 | 168 | This proposes a concept, where each module describes what its required and optional flags are in a parseable way. 169 | 170 | #### How it works 171 | 172 | The parser is [here](./flag_parser.ts) 173 | 174 | Currently it searches for markdown table rows, which contain 2 or 3 columns. 175 | 176 | - The first column needs to contain the flag inside backticks like ( `--unstable` ). Things can be before it in the column, but only spaces are allowed after it. 177 | - The second column indicates, whether the flag is required (`*`, `Yes` or `yes`) or optional (empty). Putting `_` in this column tells the parser to ignore the row. 178 | - The optional third column can serve as a description why the flag is needed. 179 | 180 | A table could look something like this: 181 | 182 | 183 | |Flag| Required |Reason| 184 | |:--|:-:|:--| 185 | | 🚧 `--unstable` | * | Needed for [Cliffy](https://github.com/c4spar/deno-cliffy) to work | 186 | | 🌐 `--allow-net` | * | To fetch data from the repos | 187 | | 🔰 `--location` | | To save settings. `https://kopo.mod.land` | 188 | | 🔍 `--allow-read` | _ | Needed for cache info | 189 | | 💾 `--allow-write` | _ | Save favourites | 190 | | ⏱ `--allow-hrtime` | _ | Allows precise benchmarking | 191 | | ⚠ `--allow-run` | _ | Needed for feature x | 192 | | 🧭 `--allow-env` | _ | Needed to access your ENV | 193 | | 🧩 `--allow-plugin` | _ | **`Deprecated`** Old plugin system | 194 | | 🧩 `--allow-ffi` | _ | For Deno plugin system | 195 | | 🔮 `--allow-all` | _ | It should never be required | 196 | 197 | > Also keep in mind, that you can hide this inside a comment if you dont find a proper place for it in your readme, inside the `` tags, but than only the tools processing your readme can see it. 198 | 199 | #### Testing your file for flags 200 | 201 | There is a small util that you can use to test your file for the flags. It can be found [here](./flag_checker.ts). 202 | 203 | To use it simply run: 204 | 205 | ```bash 206 | deno run --allow-read https://deno.land/x/kopo@v0.1.1/flag_checker.ts ./README.md 207 | ``` 208 | 209 | or for remote files: 210 | 211 | ```bash 212 | deno run --allow-net https://deno.land/x/kopo@v0.1.1/flag_checker.ts https://raw.githubusercontent.com/littletof/kopo-cli/master/README.md 213 | ``` 214 | 215 | ## Contribution 216 | 217 | Issues, pull requests and feedback are always welcome. 218 | 219 | 220 | ## TODOs 221 | 222 | - [ ] add module import url processing for search, etc.. 223 | - [ ] more tests 224 | ## Licence 225 | 226 | Copyright 2020- Szalay Kristóf. All rights reserved. MIT license. 227 | --------------------------------------------------------------------------------