Skip to content

Releases: toiroakr/politty

v0.11.3

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 13 Jul 13:08
ebbc15b

Patch Changes

  • bc7bcfc: Add SkillCommandOptions.descriptions to override the description text of the skills command and each of its built-in subcommands (sync/add/remove/list), letting a host CLI brand the skills command without re-wrapping the command tree by hand. Keys refer to the subcommand's canonical role, independent of any commandMap rename — descriptions.add still applies after commandMap.add renames the subcommand to something else. Omitting descriptions (or any of its keys) preserves politty's existing default text exactly.

v0.11.2

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 09 Jul 12:33
6d69010

Patch Changes

  • 21e5dce: Add args.$source(name) to expose whether a resolved arg value came from an explicit CLI token ("cli"), a field.env fallback ("env"), or neither ("default"). This lets a command's run() handler distinguish an explicitly-typed value from an environment-variable fallback without re-deriving flag spellings from the schema, and works correctly for positional fields even when the typed value happens to equal the env var. $source correctly resolves both camelCase and kebab-case field name lookups, and correctly reports "default" for a local field that collides with a same-named global field but is resolved via the local schema's own default.

    $source's parameter is typed as a plain string rather than a schema-derived key, since a stricter type broke type-checking for the documented discriminated-union args pattern.

    Field names starting with "$" are now rejected at command-definition time (ReservedFieldNameError), since that prefix is reserved for framework-injected helpers like $source and is unusable as a real CLI flag anyway (an unquoted $name gets shell-expanded before the program sees it).

  • c199535: Fix global/local arg collisions on a shared field name. Two changes:

    • Command definitions where a global field and a same-named local field have different definitions (different type bucket, positional vs. flag, or different enum values) now throw FieldTypeConflictError at validation time — previously this silently passed even though the two fields didn't actually agree on what values are valid.
    • When the definitions are identical, a flag shared between globalArgs and a command's own schema now resolves correctly regardless of where it's typed — including when the command's own field is required and has no default. Previously, typing the flag before the subcommand parsed it correctly as the global value, but the command's own same-named field — having received nothing of its own — would either fail local validation (if required) or unconditionally get overwritten by its own default during the final args merge, discarding what the user actually typed. Typing the same flag after the subcommand already worked correctly and is unaffected.

    Also fixes an unrelated bug found while implementing the above: a prompt resolver returning { field: undefined } for a field it chose not to prompt for could previously clobber a real CLI/env value already provided for that field.

    Two smaller follow-ups to the same-name conflict detection:

    • validateCommand() now accepts a globalArgs option and checks every command and subcommand in the tree against it, so a FieldTypeConflictError/case-variant collision on a rarely-invoked subcommand can be caught upfront instead of only at the moment that subcommand actually gets parsed.
    • The same-named field comparison now also considers whether the field is positional, so a global flag and a same-named local positional argument are correctly treated as conflicting rather than as identical.
  • 48d54d9: Improve withSkillCommand's type safety and customizability for host CLIs:

    • withSkillCommand's return type now reflects the injected skills subcommand (subCommands.skills is typed as AnyCommand and no longer optional), so consumers no longer need an as AnyCommand cast to access it.
    • SkillCommandOptions.globalArgs accepts the same schema passed to runMain/runCommand's globalArgs. When it already declares a same-named non-positional boolean verbose/json field, skills add/skills sync's --verbose and skills list's --json are automatically omitted from their own schema, so the host's global flag of the same name takes priority — no manual configuration needed. (A same-named non-boolean field doesn't trigger this; politty itself rejects that combination with FieldTypeConflictError at parse time, so such a field must be renamed on one side instead.)
    • SkillFlagOverrides.verbose.alias renames or disables skills add/skills sync --verbose's short alias — a collision independent of globalArgs's field-name auto-detection (e.g. the host's -v belongs to an unrelated flag, not one named verbose).
    • SkillCommandOptions.commandMap lets a host CLI rename skills add/skills remove and control their aliases: { add?: string[]; remove?: string[] }, where the first array element becomes the subcommand's dispatched name and the rest become aliases. Default: add: ["add", "install"], remove: ["remove", "uninstall"]. withSkillCommand throws if any resulting name or alias collides with sync/list or with each other, or isn't a safe token (empty string, leading dash, whitespace, etc.).
    • SkillCommandOptions.unknownKeys ("strict" | "strip" | "passthrough", default "strip") controls unknown-flag handling for add/sync/remove/list's own schemas uniformly — set "strict" to match a host CLI that uses z.strictObject() throughout. "passthrough" matches z.object().passthrough(): no warning, but (unlike "strip") the flag's value is kept on the parsed args under its raw CLI name instead of dropped. Never affects values that legitimately arrive via globalArgs.

v0.11.1

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 08 Jul 12:59
a9b0fca

Patch Changes

  • cdab697: Fix two argv-parsing bugs that silently produced wrong values instead of erroring:

    • An option expecting a value (e.g. z.coerce.number()) followed by a negative-number-looking token, such as --count -5, no longer treats the flag as boolean true and mis-parses the following token as combined short flags. The token is now consumed as the option's value. Other dash-prefixed tokens (--, or another flag like --verbose) are left alone so they aren't silently swallowed as a literal value.
    • --flag=true / --flag=false now correctly coerce to booleans for z.boolean()-typed fields instead of failing validation with "expected boolean, received string".
  • c69cee6: Fix the internal-subcommand bypass in runMain incorrectly matching prototype-inherited property names such as __proto__, __defineGetter__, or __lookupGetter__. Previously, invoking a CLI with one of these as the first positional (e.g. mycli __proto__) would silently skip the user-provided setup/cleanup/prompt hooks even though no such subcommand was ever registered, because the lookup read through Object.prototype instead of checking for an own property.

  • fd67602: Fix engines.node to accurately reflect the runtime requirement. The package uses node:util's styleText, which was added in Node 20.12.0 / 21.7.0; the previous >=18 declaration allowed installs that crashed on import with SyntaxError: The requested module 'node:util' does not provide an export named 'styleText'. The build target was also updated from node18 to node20.12 to match.

  • 6515894: Fix subcommand resolution (resolveSubcommand, resolveSubcommandWithAlias, and the shell-completion context parser) incorrectly matching prototype-inherited property names such as __proto__ or constructor as if they were registered subcommands. This follows up on the same class of bug fixed in runMain's internal-subcommand bypass, applying the Object.hasOwn guard to the remaining lookups that read through Object.prototype.

  • df62418: Add the missing MIT LICENSE file (the package has declared "license": "MIT" without shipping the license text) and set the previously empty author field in package.json. Also remove the stale package-lock.json that had been accidentally committed to this pnpm-managed project, and pin the pkg-pr-new preview-publish tool to an exact version instead of running pnpm dlx's latest unconditionally in a pull-requests: write job.

v0.11.0

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 29 Jun 03:03
c353aae

Minor Changes

  • 50013d2: Disable default boolean negation unless negation: true is set.

    Boolean options no longer accept --no-<name> or --no<Name> by default. Set negation: true to enable and advertise the default negation form, set negation to a string to use a custom negation name, or leave it unset / set it to false to reject default negation.

v0.10.1

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 25 Jun 04:50
62fab9b

Patch Changes

  • b4b1dd0: onUnknownSubcommand is no longer invoked for a command that defines its own run. Such a command's first positional is a real argument, so an installed <cli>-<name> plugin must never shadow it — which previously could make the command's meaning depend on what was on PATH. Plugin dispatch now only happens for pure subcommand-group commands (no run).

v0.10.0

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 23 Jun 01:58
7d492a8

Minor Changes

  • b45b891: Positional tokens not consumed by the schema are now surfaced rather than silently ignored.

    For commands with subcommands, any unconsumed bare token that is not a known subcommand name is treated as an unknown subcommand attempt and exits with code 1 (with a did-you-mean suggestion when a similar name exists). Tokens after --, dash-prefixed tokens, and tokens that match a known subcommand name are excluded from this check — they fall through to the unknownKeysMode positional handling instead.

    For commands without subcommands, behaviour follows the schema's unknownKeysMode:

    • strict (z.strictObject / .strict()): exits with code 1
    • strip / default (z.object): emits a warning and continues
    • passthrough (z.looseObject / .passthrough()): silently ignores (no change)

    Schema-less commands (no args defined) now correctly capture all tokens — including flag-like ones such as -x — as positionals so stray tokens are still detected.

Patch Changes

  • ebbd553: Extract shared resolveLongOption() for long-option resolution, eliminating duplicated negation rules between argv-parser and subcommand-scanner.

  • 0429277: Unify the options-rendering paths in src/docs through a shared (rows × columns) intermediate (toOptionRows + emitMarkdownTable/emitMarkdownList). The markdown table and list renderers, their *FromArray variants, and renderArgsTable's column-filtered path now share one place where per-option display decisions (negation handling, alias ordering, placeholder resolution) live.

    Rendered output is unchanged except for one cosmetic detail: renderArgsTable(args, { columns }) now emits the canonical fixed-width table separator (|--------|...) instead of header-length, space-padded dashes (| ------ | ... |). The two forms render identically as Markdown.

v0.9.2

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 19 Jun 07:11
68d48ad

Patch Changes

  • 3d338d5: Show subcommands as <command> (required) when the parent command has no run handler, and [command] (optional) when it does

v0.9.1

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 18 Jun 07:40
e16b37e

Patch Changes

  • 860dbe2: skills add now accepts install and skills remove accepts uninstall as aliases, matching the verbs most package-manager-trained users reach for first. Both spellings dispatch to the same command, so existing invocations continue to work; help output lists the aliases under each command.
  • b631ccd: Address Copilot review feedback on politty/skill
    • scanSourceDir now wraps the source directory's statSync in try/catch instead of guarding with existsSync. Permission/IO errors (EACCES/EPERM) on the source directory are surfaced as read-failed ScanErrors with the original error message, where previously they were silently misclassified as missing-source.
    • assertSafeName in the installer now also rejects names longer than 64 characters, matching the frontmatter schema's documented 1..64 length constraint. The check stays a deliberately independent (defense-in-depth) duplicate of the schema rather than a shared import.
    • skills list --json no longer interleaves scan-error summary lines into stdout. The machine-readable JSON payload stays the only thing on stdout in --json mode; per-error stderr warnings still fire so operators can see what was skipped. Previously a malformed source SKILL.md could corrupt the JSON output.
    • listStatus now reserves missing for dangling canonical symlinks (the only state removeOwnedSkill's cleanup path can actually clean up). A real directory at .agents/skills/<name> without a readable SKILL.md is now reported as unstamped, which routes it through the no-clobber guard instead of incorrectly promising the slot can be reaped.
    • installSkill now refuses up-front when the source path overlaps any install destination — the canonical .agents/skills/<name> slot or any SYMLINK_TARGETS agent slot (e.g. .claude/skills/<name>) — in both mode: "copy" and mode: "symlink". Previously only copy-mode overlap with the canonical slot was checked: a source sitting at an agent slot survived the canonical check and then got rm-rf'd by populateAgentDirs's own clearInstallSlot once the stamp matched, taking the source data with it (and in symlink mode creating a canonical↔agent symlink loop). The copy-mode case where the destination ended up inside the source — recursing until the path/disk limit was hit because the cyclic-symlink detector does not catch it (no symlink involved) — is covered by the same guard.
    • The overlap-guard helper pathsOverlap is now boundary-aware: only .. and ..<sep>... count as "escaping" the outer path. The previous startsWith("..") test misclassified valid same-level siblings whose names happened to start with .. (e.g. a source at .agents/skills/<name>/..backup) as outside, so the guard missed real overlaps and clearInstallSlot would have rm-rf'd the canonical slot while the source was nested inside it.
    • findOwnedInstalledSkills now distinguishes spec-incompatible legacy names from IO errors when reading an installed skill's ownership. Previously the try { readInstalledOwnership } catch { continue } swallowed every throw, so EACCES/EPERM on an installed SKILL.md left an unreadable orphan in place while skills sync reported success. Names that can't have been produced by this CLI (failing the spec pattern or the 1..64 length) are now filtered out by a local defense-in-depth duplicate of the schema before the call; the catch path is reserved for real IO failures and emits a stderr warning per affected slot.
    • logScanErrors now treats read-failed errors whose path === sourceDir as directory-level failures (matching scanFailedAtRoot). A readdirSync(sourceDir) failure such as EACCES previously rendered as "Skipping skill at " with no stdout summary, masking a broken source directory as a single malformed skill.
    • skills remove (no-argument path) now branches on scanFailedAtRoot before printing its no-op summary, matching add/list/sync. A missing or unreadable sourceDir previously rendered as "No skills found in source directory; nothing to remove." — a legitimate-empty-bundle message that contradicted the directory-level warning loadSkills already emitted.
    • removeInstalledSlot and clearInstallSlot now only swallow ENOENT/ENOTDIR from the slot's lstatSync; other failures (EACCES/EPERM/IO) propagate. Previously a permissions or IO failure on the slot was silently treated as "nothing to do", so uninstallSkill / installSkill would report success while leaving the slot intact — and a subsequent install would then fail the no-clobber guard with no actionable context.
    • findOwnedInstalledSkills now only treats ENOENT/ENOTDIR on .agents/skills as a legitimate "no installed skills" result. Other readdirSync failures (EACCES/EPERM/IO) are warned to stderr — previously every throw was swallowed, so a permission failure on the install root made sync's orphan reconciliation and --exclude validation silently skip the installed-skill universe, masking a permission problem as success.
    • The no-clobber guard in installSkill's caller (addSkill) now uses slot presence + non-dangling-symlink instead of hasInstalledSkill, and mirrors the route-to-source check used by cleanupBrokenSlot/findOwnedInstalledSkills when classifying a dangling canonical as reapable. Previously a live symlink at .agents/skills/<name> whose target lacks a SKILL.md (foreign / partial install) passed the guard (because hasInstalledSkill resolves the symlink and finds no file) and was silently unlinked by clearInstallSlot, even though listStatus already classifies the same state as unstamped. The route check additionally protects the dangling case in the shared .agents/skills/ namespace: a dangling canonical pointing at another politty-based CLI's (now-uninstalled) source dir is no longer treated as "almost certainly ours" — only dangling symlinks that still route into this CLI's sourceDir are reaped, matching the rule the other two callers already enforce. The three paths now agree.
    • Re-synced package-lock.json with package.json so npm-based installs (npm ci) can resolve the runtime yaml dependency. The lockfile had been left untouched since yaml was promoted to a top-level dependency, so anyone installing the package via npm would have ended up without a critical scanner dependency. pnpm-lock.yaml was already in sync.
    • removeInstalledSlot and clearInstallSlot now refuse to unlink a foreign symlink at an agent-specific slot (.claude/skills/<name>). The two functions accept a restrictSymlinkTo option that the agent-slot call sites set to the canonical slot, and unlinking proceeds only when the existing symlink routes there. Previously, a symlink another tool installed at the same agent path was silently unlinked: uninstallSkill did so during skills remove/sync, and populateAgentDirs's clearInstallSlot did so during skills add/sync. Stamped canonical slots stay under unconditional ownership (the stamp resolves the ambiguity); the unstamped/dangling cases are handled by the dangling-route guard below.
    • cleanupBrokenSlot (the dangling-symlink reaper invoked by skills remove/sync when our canonical is broken) now applies the same route-to-our-canonical check to each agent-slot symlink before unlinking. Previously the sweep treated any dangling agent-slot symlink with the same skill name as ours; a foreign tool's dangling symlink pointing at its own canonical (in the shared .claude/skills/<name> namespace) would have been silently deleted.
    • cleanupBrokenSlot and findOwnedInstalledSkills now confirm a dangling canonical symlink still routes into this CLI's sourceDir before treating it as ours. .agents/skills/<name> is a namespace shared by every politty-based CLI (each writes its own ownership stamp); a dangling canonical has no stamp to read, so without the routing check we would silently reap a foreign CLI's stale install during skills remove/sync (and include it in --exclude validation). The lexical containment check resolves the deepest existing ancestor of the (dangling) target through realpathSync so it survives macOS /tmp/private/tmp remapping, projects mounted via a symlink, and pnpm-style node_modules hops. Live canonical symlinks still go through the ownership-stamp path.
    • removeInstalledSlot now verifies the canonical-slot symlink's routed-to SKILL.md carries expectedStamp before unlinking, matching the route-check that already gates agent-specific slots. Previously a programmatic uninstallSkill(name, cwd, { expectedOwnership }) would unconditionally unlink a live canonical symlink, so another politty-based CLI's live install in the shared .agents/skills/<name> namespace could be deleted. expectedStamp === null keeps the legacy permissive behaviour for teardown helpers that opt out of ownership checks.
    • docs/skill-management.md no longer claims skills remove (no-argument form) cleans up every skill owned by the CLI. The implementation iterates only skills currently discovered in sourceDir, so owned orphans (skills the CLI previously installed but no longer ships) survive a bare remove — they require skills sync or naming them explicitly. The previous wording invited users to expect remove to behave like sync's orphan reconciliation.
    • installSkill now runs the source-vs-destination overlap guard before mkdirSync(canonicalParent) and each SYMLINK_TARGETS parent's mkdirSync. Previously a failing overlap install (typically a single-skill source rooted at the project directory, so sourcePathcwd) still left freshly-created .agents/skills/ and <agent>/skills/ directories inside the source tree before throwing. The pre-check uses a deepest-existing-ancestor + lexical-tail resolution so the comparison still catches /tmp/private/tmp realpath remaps when the destination parents don't exis...
Read more

v0.9.0

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 18 Jun 05:18
f6bf7e6

Minor Changes

  • b7c8ebc: Reduced installation size (2.9MB to approx. 630KB, approx. 78% reduction)
    • Excluded source maps (.map) from the distribution
    • Explicitly excluded build artifacts such as build cache (tsconfig.tsbuildinfo) using the files field
    • BREAKING: Discontinued CJS distribution and changed to ESM-only (loading via require() is no longer possible, removed .cjs and index.d.cts)

Patch Changes

  • dfd1241: Removed the string-width runtime dependency
    • Replaced it with a lightweight built-in implementation (using Node's stripVTControlCharacters for ANSI stripping) used by the Markdown renderer
    • politty now has zero runtime dependencies

v0.8.0

Choose a tag to compare

@toiroakr-release-bot toiroakr-release-bot released this 16 Jun 05:42
6d6812d

Minor Changes

  • 16b8503: Make files-mode documentation fully generated (marker-free) by default and resolve template links by specificity.
    • Fully generated files output by default (breaking). files-mode generation no longer emits <!-- politty:...:start/end --> markers; each file is regenerated as a whole. With targetCommands, only files containing a target command are processed, but each is rebuilt in full. Set the new customizable: true option on GenerateDocConfig when you want to hand-edit the output and have politty preserve your edits via markers (in-place section updates). When customizable is set, a command whose generated output gains a section the file lacks is reported as a non-fatal warning (run with POLITTY_DOCS_DOCTOR=true POLITTY_DOCS_UPDATE=true to insert it, or leave it removed to opt the section out). path/rootDoc output still uses markers; templates remain marker-free.
    • Specificity-based link resolution. Cross-output links now point at the output that renders a command most specifically — a dedicated per-command page ({{politty:command:config}}) wins over a full-tree page ({{politty:command}}) for that command and its descendants, regardless of registration order.
    • Adds a markerless option to DefaultRendererOptions.

Patch Changes

  • eea3f6a: Add template-based documentation generation. A new templates option on GenerateDocConfig maps output paths to template files containing {{politty:...}} placeholders; the output is fully generated from the template and contains no politty markers. Templates can exclude specific placeholders with politty.exclude front matter.