npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

cli-state

v0.0.6

Published

> ⚠️this library is totally unstable and incomplete.

Downloads

3

Readme

CLI-state

⚠️this library is totally unstable and incomplete.

This library aims to make it easy to read, persist, and prompt for required settings for CLI's.

Two secondary goals we might not reach but want to consider:

  • make caching easy for offline friendly CLI's
  • make it easy to store history of inputs for autocompletion support or suggestion of what user should do next

High Level Philosophy of CLI state

Here are some sources of CLI config:

  • CLI flags (prefer flags over arguments)
  • Project Config (e.g. package.json, an rc file read by cosmiconfig, or some other special file e.g. toml)
  • Project Filesystem (eg do you have special folders/files setup)
  • Machine Config
  • Remote user account settings
  • Remote team account settings
  • Remote global telemetry based defaults

While offline, we may also lean on caches as sources of state:

  • Project cache
  • Machine Cache
  • Cached user account settings
  • Cached team account settings
  • Cached global telemetry based defaults

Config vs State

We make a (possibly confusing?) distinction between config vs state. Config is static - it is what it is before the CLI starts, and doesn't change. State is dynamic, you can set state during your CLI session and expect it to persist between sessions. If a state is missing, we can prompt for it, and then offer to persist it, or tell the user how to override it in future with a flag.

Getting Started

For maximum context, these examples will show how to integrate cli-state with oclif.

We start with a custom base command:

// src/base.ts
import Command from '@oclif/command';
import { initCLIState } from 'cli-state';

export default abstract class extends Command {
  async init() {
    // do some initialization
    initCLIState({ projectStatePath: '.exampleCLI' });
  }
}

this makes sure we initialize our CLI state with the basic settings we want every time we run a command.

Then we are free to read and write to and from our configs wherever we want:

// src/commands/hello.ts
import { flags } from '@oclif/command';
import BaseCommand from '../../base';
import { globalState, projectState } from 'cli-state';

export default class Hello extends BaseCommand {
  static description = 'describe the command here';

  static examples = [
    `$ example hello
hello world from ./src/hello.ts!
`,
  ];

  static flags = {
    help: flags.help({ char: 'h' }),
    // flag with a value (-n, --name=VALUE)
    name: flags.string({ char: 'n', description: 'name to print' }),
    file: flags.string({ description: 'file for writing' }),
    // flag with no value (-f, --force)
    force: flags.boolean({ char: 'f' }),
  };

  static args = [{ name: 'file' }];

  async run() {
    const { args, flags } = this.parse(Hello);
    const name = flags.name || 'world';
    this.log(`hello ${name} from ./src/commands/hello.ts`);
    globalState.set('name', name);
    const type = globalState.get('name');
    console.log(type, typeof type); // types are preserved after deserialization
    console.log('globalState path:', globalState.path); // where the globalState is stored
    projectState.set('name'); // same thing for project config
  }
}

Exported API

cli-state is really a thin wrapper over a few libraries. Here is the public API (there are no default exports):

initCLIState

initCLIState({
  projectStatePath: string;
  globalConfOptions?: Conf.Options<ConfigTypes>;
  projectConfOptions?: Conf.Options<ConfigTypes>;
  frecencyOpts?: {
    idAttribute?: string | idAttrFn;
    timeStampsLimit?: number;
    recentSelectionsLimit?: number;
    exactQueryMatchWeight?: number;
    subQueryMatchWeight?: number;
    recentSelectionsMatchWeight?: number;
  };
})

This must be run before any of the other functions.

Only projectStatePath is mandatory, and should reflect where you want your project config stored, e.g. in a .netlify folder.

Both globalConfOptions and projectConfOptions take the same options as https://github.com/sindresorhus/conf, which you can see here. In particular you may wish to define a schema. We tweak the options slightly: globalConfOptions sets a telemetry flag and a unique cliId by default, and projectConfOptions overrides conf's cwd to make the file project specific.

frecencyOpts take from https://github.com/mixmaxhq/frecency except key and storageProvider have been omitted. You set key later and storageProvider will be set for you. By default, we have made the idAttribute be "value" instead of "_id", since that is far more common for CLI prompting. but you can override this.

globalState and projectState

These two objects have the entire public instance API of https://github.com/sindresorhus/conf. You can view them here: https://github.com/sindresorhus/conf#instance, in particular .get, .set, and .store.

These are singletons for the life of your CLI.

accessGlobalFrecency

accessGlobalFrecency: (key: string) => Frecency;

This function takes a key and creates a Frecency object, which is stored in the user's default/specified cache folder according to env-paths. This is explicitly meant for recording and sorting prompt inputs, for example Autocompletes, to help learn from usage.

You can create as many as you like, as long as your keys are unique. Remember we overrode the default idAttribute to value instead of _id, but if you want to override it further, pass frecencyOpts.idAttribute during initialization.

The frecency docs aren't great so here is a working example.

const freq = accessGlobalFrecency('country');
const choices = freq.sort({
  results: ['apple', 'banana', 'pear', 'orange', 'pineapple'].map(normalize),
  // keepScores: true,
});
const question = {
  type: 'autocomplete',
  name: 'fruits',
  message: 'Where to?',
  limit: 5,
  suggest(input: string, choices: Choice[]) {
    const list = choices.filter(caseInsensitiveFilter(input));
    return freq.sort({ searchQuery: input, results: list });
  },
  choices,
};

let { fruits } = await prompt(question);
const tosave = { searchQuery: fruits, selectedId: fruits };
freq.save(tosave);
console.log({ fruits });

// helper functions defined
type Choice = { value: string; message: string; name: string };

function normalize(foo: string) {
  return { value: foo, message: foo, name: foo };
}
function caseInsensitiveFilter(input: string) {
  return (choice: Choice) =>
    choice.message.toLowerCase().startsWith(input.toLowerCase());
}

TSDX Bootstrap

This project was bootstrapped with TSDX.