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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aliou/pi-utils-settings

v0.10.0

Published

Shared settings UI and config loader for pi extensions

Readme

@aliou/pi-utils-settings

Shared settings infrastructure for pi extensions. Provides config loading, a settings UI command with scope tabs plus optional extra tabs, and reusable TUI components.

This is a utility library, not a pi extension. It is meant to be used as a dependency by extensions that need a settings UI or JSON config management.

Install

pnpm add @aliou/pi-utils-settings

API

ConfigLoader

Generic JSON config loader with global + project scopes, deep merge, and versioned migrations.

import { ConfigLoader, type Migration } from "@aliou/pi-utils-settings";

interface MyConfig {
  features?: { darkMode?: boolean };
}

interface ResolvedConfig {
  features: { darkMode: boolean };
}

const migrations: Migration<MyConfig>[] = [
  {
    name: "v1-upgrade",
    shouldRun: (config) => !config.features,
    run: (config) => ({ ...config, features: {} }),
  },
];

const configLoader = new ConfigLoader<MyConfig, ResolvedConfig>(
  "my-extension", // reads ~/.pi/agent/extensions/my-extension.json + .pi/extensions/my-extension.json
  { features: { darkMode: false } }, // defaults
  { migrations },
);

await configLoader.load();
const config = configLoader.getConfig(); // ResolvedConfig (defaults merged with global + project)

JSON Schema support

ConfigLoader can inject a $schema field into settings files, giving editors autocomplete and validation. Pair it with buildSchemaUrl and auto-generated schemas from ts-json-schema-generator.

import { ConfigLoader, buildSchemaUrl } from "@aliou/pi-utils-settings";
import pkg from "./package.json";

const schemaUrl = buildSchemaUrl(pkg.name, pkg.version);

const loader = new ConfigLoader<MyConfig, ResolvedConfig>(
  "my-extension",
  defaults,
  { schemaUrl },
);

When schemaUrl is set, save() writes $schema as the first key in the JSON file and load() strips it before returning config to callers.

To generate the schema from your TConfig type, add these scripts to your extension's package.json:

{
  "gen:schema": "ts-json-schema-generator --path src/config.ts --type MyConfig --no-type-check -o schema.json",
  "check:schema": "ts-json-schema-generator --path src/config.ts --type MyConfig --no-type-check -o /tmp/schema-check.json && diff -q schema.json /tmp/schema-check.json"
}

Run pnpm gen:schema to produce schema.json, commit it, and add "schema.json" to files in package.json so it ships with your npm package. Add check:schema to CI to catch drift.

An optional afterMerge hook runs after the deep merge for logic that can't be expressed as a simple merge (e.g., one field replacing another):

new ConfigLoader("my-ext", defaults, {
  afterMerge: (resolved, global, project) => {
    if (project?.customField) {
      resolved.derivedField = project.customField;
    }
    return resolved;
  },
});

registerSettingsCommand

Creates a /name:settings command with scope tabs (Global/Local/Memory), draft-based editing, and Ctrl+S to save.

All changes (boolean toggles, enum cycling, submenu edits) are held in memory as drafts. Nothing is written to disk until the user presses Ctrl+S. Esc exits without saving. Dirty tabs show a * marker.

import { registerSettingsCommand, type SettingsSection } from "@aliou/pi-utils-settings";

registerSettingsCommand<MyConfig, ResolvedConfig>(pi, {
  commandName: "my-ext:settings",
  title: "My Extension Settings",
  configStore: configLoader, // implements ConfigStore interface
  buildSections: (tabConfig, resolved, { setDraft, theme }) => [
    {
      label: "General",
      items: [
        {
          id: "features.darkMode",
          label: "Dark mode",
          description: theme.fg("dim", "Enable dark mode"),
          currentValue: (tabConfig?.features?.darkMode ?? resolved.features.darkMode) ? "on" : "off",
          values: ["on", "off"],
        },
      ],
    },
  ],
});

You can also add non-scope top-level tabs with extraTabs:

import { registerSettingsCommand, type ExtraSettingsTab } from "@aliou/pi-utils-settings";

const extraTabs: ExtraSettingsTab<MyConfig, ResolvedConfig>[] = [
  {
    id: "examples",
    label: "Examples",
    buildSections: ({ resolved, getRawForScope, enabledScopes }) => {
      const globalConfig = getRawForScope("global");
      return [
        {
          label: "Examples",
          items: [
            {
              id: "example.enabledScopes",
              label: "Enabled scopes",
              currentValue: enabledScopes.join(", "),
            },
            {
              id: "example.darkModeDefault",
              label: "Dark mode default",
              currentValue: resolved.features.darkMode ? "on" : "off",
            },
            {
              id: "example.globalPresent",
              label: "Global config",
              currentValue: globalConfig ? "present" : "missing",
              description: "Read-only info tab not tied to a scope.",
            },
          ],
        },
      ];
    },
  },
];

Ctrl+S behavior stays the same: only dirty scope drafts are saved. Extra tabs can still update drafts by calling setDraftForScope(...) from submenu callbacks.

buildSections ctx now includes theme, which is both a SettingsListTheme and full pi Theme. This means you can use list helpers (label, value, hint, ...) and pass the same object to components that require full Theme.

import { Wizard } from "@aliou/pi-utils-settings";

buildSections: (_tabConfig, _resolved, ctx) => [
  {
    label: "Setup",
    items: [
      {
        id: "setup.wizard",
        label: "Run setup",
        currentValue: ctx.theme.fg("accent", "open"),
        submenu: (_value, done) =>
          new Wizard({
            title: "Setup",
            theme: ctx.theme,
            steps: [{ label: "Step", build: () => ({ render: () => [ctx.theme.hint("Ready")], handleInput: () => {} }) }],
            onComplete: () => done("done"),
            onCancel: () => done(undefined),
          }),
      },
    ],
  },
];

Submenu support

Items can open submenus by providing a submenu factory. Use setDraft inside submenu onSave to keep changes in the draft (same save model as simple values):

import { ArrayEditor, setNestedValue } from "@aliou/pi-utils-settings";

{
  id: "tags",
  label: "Tags",
  currentValue: `${tags.length} items`,
  submenu: (_val, done) => {
    let latest = [...tags];
    return new ArrayEditor({
      label: "Tags",
      items: [...tags],
      theme: ctx.theme,
      onSave: (items) => {
        latest = items;
        const updated = structuredClone(tabConfig ?? {}) as MyConfig;
        setNestedValue(updated, "tags", items);
        setDraft(updated);
      },
      onDone: () => done(`${latest.length} items`),
    });
  },
}

SectionedSettings vs SettingsDetailEditor

Use SectionedSettings alone when each row can be edited in one step (toggle, enum cycle, or a simple submenu).

Use SectionedSettings + SettingsDetailEditor when a selected row needs a focused second-level panel with multiple editable fields.

SettingsDetailEditor is data-driven. You pass field descriptors with getters/setters and optional nested submenu callbacks. The component owns keyboard navigation and rendering only.

import {
  ArrayEditor,
  SettingsDetailEditor,
  type SettingsDetailField,
} from "@aliou/pi-utils-settings";
import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";

const fields: SettingsDetailField[] = [
  {
    id: "autoSave",
    type: "boolean",
    label: "Auto save",
    getValue: () => editor.autoSave,
    setValue: (next) => {
      editor.autoSave = next;
    },
  },
  {
    id: "tabSize",
    type: "enum",
    label: "Tab size",
    getValue: () => String(editor.tabSize),
    setValue: (next) => {
      editor.tabSize = Number.parseInt(next, 10);
    },
    options: ["2", "4", "8"],
  },
  {
    id: "favorites",
    type: "submenu",
    label: "Favorites",
    getValue: () => `${favorites.length} items`,
    submenu: (done) =>
      new ArrayEditor({
        label: "Favorites",
        items: [...favorites],
        theme: getSettingsListTheme(),
        onSave: (items) => {
          favorites = items;
        },
        onDone: () => done(`${favorites.length} items`),
      }),
  },
  {
    id: "clear",
    type: "action",
    label: "Clear favorites",
    getValue: () => "destructive",
    onConfirm: () => {
      favorites = [];
    },
    confirmMessage: "Clear all favorites? This cannot be undone.",
  },
];

const detail = new SettingsDetailEditor({
  title: "Editor details",
  fields,
  theme: getSettingsListTheme(),
  onDone: (summary) => done(summary),
  getDoneSummary: () => `${favorites.length} items`,
});

ConfigStore interface

Extensions with custom config loaders can implement ConfigStore directly instead of using ConfigLoader:

interface ConfigStore<TConfig, TResolved> {
  getConfig(): TResolved;
  getRawConfig(scope: "global" | "project"): TConfig | null;
  hasConfig(scope: "global" | "project"): boolean;
  save(scope: "global" | "project", config: TConfig): Promise<void>;
}

Components

  • SectionedSettings: Grouped settings list with search filtering and cursor preservation on update.
  • SettingsDetailEditor: Focused second-level editor for one selected item (text, enum, boolean, nested submenu, destructive action).
  • ArrayEditor: String array editor with add/remove/reorder.
  • PathArrayEditor: Path-focused array editor with Tab completion in add/edit mode.

Helpers

  • setNestedValue(obj, "a.b.c", value): Set a deeply nested value by dot-separated path.
  • getNestedValue(obj, "a.b.c"): Get a deeply nested value by dot-separated path.
  • displayToStorageValue(id, displayValue): Convert display values ("enabled"/"disabled", "on"/"off") to storage values (true/false).
  • getSettingsTheme(theme): Build a combined settings theme (SettingsTheme) usable by both settings-list components and full-theme components like Wizard.

Exports

export { ConfigLoader, type ConfigStore, type Migration } from "./config-loader";
export { registerSettingsCommand, type SettingsCommandOptions } from "./settings-command";
export { SectionedSettings, type SectionedSettingsOptions, type SettingsSection } from "./components/sectioned-settings";
export {
  SettingsDetailEditor,
  type SettingsDetailActionField,
  type SettingsDetailBooleanField,
  type SettingsDetailEditorOptions,
  type SettingsDetailEnumField,
  type SettingsDetailField,
  type SettingsDetailSubmenuField,
  type SettingsDetailTextField,
} from "./components/settings-detail-editor";
export { ArrayEditor, type ArrayEditorOptions } from "./components/array-editor";
export { PathArrayEditor, type PathArrayEditorOptions } from "./components/path-array-editor";
export { setNestedValue, getNestedValue, displayToStorageValue } from "./helpers";
export { buildSchemaUrl } from "./schema";
export { getSettingsTheme, type SettingsTheme } from "./theme";