洪 民憙 (Hong Minhee) :nonbinary:'s avatar

洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · 993 following · 1394 followers

An intersectionalist, feminist, and socialist living in Seoul (UTC+09:00). @tokolovesme's spouse. Who's behind @fedify, @hollo, and @botkit. Write some free software in , , , & . They/them.

서울에 사는 交叉女性主義者이자 社會主義者. 金剛兔(@tokolovesme)의 配偶者. @fedify, @hollo, @botkit 메인테이너. , , , 等으로 自由 소프트웨어 만듦.

()

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

Hello, I'm an open source software engineer in my late 30s living in , , and an avid advocate of and the .

I'm the creator of @fedify, an server framework in , @hollo, an ActivityPub-enabled microblogging software for single users, and @botkit, a simple ActivityPub bot framework.

I'm also very interested in East Asian languages (so-called ) and . Feel free to talk to me in , (), or (), or even in Literary Chinese (, )!

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee) :nonbinary:'s post

安寧(안녕)하세요, 저는 서울에 살고 있는 30() 後半(후반) 오픈 소스 소프트웨어 엔지니어이며, 自由(자유)·오픈 소스 소프트웨어와 聯合宇宙(연합우주)(fediverse)의 熱烈(열렬)支持者(지지자)입니다.

저는 TypeScript() ActivityPub 서버 프레임워크인 @fedify 프로젝트와 싱글 유저() ActivityPub 마이크로블로그인 @hollo 프로젝트와 ActivityPub 봇 프레임워크인 @botkit 프로젝트의 製作者(제작자)이기도 합니다.

저는 ()아시아 言語(언어)(이른바 )와 유니코드에도 關心(관심)이 많습니다. 聯合宇宙(연합우주)에서는 國漢文混用體(국한문 혼용체)를 쓰고 있어요! 제게 韓國語(한국어)英語(영어), 日本語(일본어)로 말을 걸어주세요. (아니면, 漢文(한문)으로도!)

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee) :nonbinary:'s post

こんにちは、私はソウルに住んでいる30代後半のオープンソースソフトウェアエンジニアで、自由・オープンソースソフトウェアとフェディバースの熱烈な支持者です。名前は洪 民憙ホン・ミンヒです。

私はTypeScript用のActivityPubサーバーフレームワークである「@fedify」と、ActivityPubをサポートする1人用マイクロブログである 「@hollo」と、ActivityPubのボットを作成する為のシンプルなフレームワークである「@botkit」の作者でもあります。

私は東アジア言語(いわゆるCJK)とUnicodeにも興味が多いです。日本語、英語、韓国語で話しかけてください。(または、漢文でも!)

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

() 가보고 싶은 곳들:

  • 延邊(연변)
  • 哈爾賓(합이빈)
  • 深圳(선전)
  • 神戶(고베)
  • 廣島(히로시마)
  • 橫濱(요코하마)
  • 沖繩(충승)
洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to XiNiHa's post

@xiniha.dev They need to adopt tsgo as soon as possible…

XiNiHa's avatar
XiNiHa

@xiniha.dev@bsky.brid.gy · Reply to 洪 民憙 (Hong Minhee) :nonbinary:'s post

I believe most of it would be spent on running tsc to build the documentation from it 🫠

白林檎美和@一般丼。's avatar
白林檎美和@一般丼。

@whtapple@ippandon.hopto.org · Reply to 洪 民憙 (Hong Minhee) :nonbinary:'s post

@hongminhee なつかすぃ…

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee) :nonbinary:'s post

この頃の日本のR&B、結構好きなんだよね。例えばMISIAの「つつみ込むように…」(1998)とか、倉木麻衣の「Love, Day After Tomorrow」(1999)とか。

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

宇多田ヒカルのファーストアルバム『First Love』(1999)は、(記憶が正しければ多分)私が初めて聴いたJ-popなんだけど、25年経った今聴いてもやっぱ良いんだよね。個人的に宇多田ヒカルの最近の音楽も好きだけど、なんだかんだこのアルバムほどグッとくるアルバムは無いかな。

Jiwon's avatar
Jiwon

@z9mb1@hackers.pub

fedify를 다른 프로그래밍 언어로 사용할 수 있다면, 어떤 언어가 좋으신가요?

Python -> 하트 Rust -> 축하

기타 언어는 답글로 달아주세요.

julian's avatar
julian

@julian@community.nodebb.org

<p>At Piefed office hours, <a href="https://piefed.social/u/rimu">@<bdi>rimu@piefed.social</bdi></a> and I got to talking about what's next for Piefed and the Threadiverse WG.</p> <p>One of those things is moving stuff between communities (or in bbs parlance: moving topics between categories/forums).</p> <p>Rimu suggested we use the already-existing <code>as:Move</code> activity, sent by the community (a group actor), with <code>origin</code> and <code>target</code> set, and with <code>object</code> being the post id itself.</p>

At Piefed office hours, @rimu@piefed.social and I got to talking about what's next for Piefed and the Threadiverse WG.

One of those things is moving stuff between communities (or in bbs parlance: moving topics between categories/forums).

Rimu suggested we use the already-existing as:Move activity, sent by the community (a group actor), with origin and target set, and with object being the post id itself.

I suggested we update this to use the resolvable context collection as object instead, which Piefed has supported since v1.2.

That should be enough to get a proof-of-concept implementation going between Piefed and NodeBB... a question remained as to whether this should be Announce(Move(Object)) or simply Move(Object).

Argument for former was that it was similar verbiage to other 1b12 actions.

Argument for the latter was that this is merely 1b12 adjacent and needn't follow prior art.

We'll likely put together an FEP for this.

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

Why doesn't Bash's programmable completion provide the cursor offset within the word being completed? With all the complexity around word splitting—shell quoting, escpaing, expansions—figuring out the intra-word cursor position by hand is a nightmare. Would it really be so hard for Bash to offer this info natively, rather than leaving script authors to replicate the shell's own parsing logic?

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee) :nonbinary:'s post

Okay, it seems up now.

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

Is gnu.org down or just me?

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to 우주스타 아이도루 랭호 🌠's post

@rangho_220 Hollo도 처음에는 Bun이었는데 메모리가 새서 Node.js로 바꿨었어요. ㅋㅋㅋ

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to 우주스타 아이도루 랭호 🌠's post

@rangho_220 프런트엔드에는 모르겠는데 백엔드에는 안 좋은 것 같아요. 메모리 릭이 너무 많아요.

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to 우주스타 아이도루 랭호 🌠's post

@rangho_220 보는 것만 되는 걸로 알고 있습니다.

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

Why is publishing packages to so slow? A lot of the CI time for the Fedify project is spent waiting for the JSR server to process the packages we've uploaded.

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

사람들이 LLM에 ()을 떼는 것도 理解(이해)는 간다…

Eric Schultz's avatar
Eric Schultz

@wwahammy@treehouse.systems

I've signed the Plan Vert letter, calling on Rails Core and the wider Ruby community to fork Rails and cut ties with DHH and his work.

Please sign, the future of Rails and Ruby depends on it. github.com/Plan-Vert/open-lett

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to Jaeyeol Lee (a.k.a. kodingwarrior) :vim:'s post

@kodingwarrior 大體(대체)建物(건물)()이 좁은 것 같아요. 아마도 집값과 耐震設計(내진설계)影響(영향)…!?

wakest ⁂'s avatar
wakest ⁂

@liaizon@social.wake.st · Reply to wakest ⁂'s post

Its been a while! I just added (hackers.pub), (@elk) and (@elgg) icons to at iconography.fediverse.info

The icons for Hackers' Pub, Elk and Elgg on a white background
ALT text detailsThe icons for Hackers' Pub, Elk and Elgg on a white background
洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee) :nonbinary:'s post

The problem is that Fedify's Activity Vocabulary API supports property hydration. Fedify intentionally hides the following three states of properties of Activity Vocabulary objects, which seems to hinder the application of an origin-based security model:

  1. When a complete object is embedded within a property of a JSON-LD object.
  2. When a property of a JSON-LD object references an object by URI.
  3. When it was initially #2, but the property has since been hydrated.
洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

I'm migrating Fedify from implementing FEP-c7d3 Ownership to FEP-fe34: Origin-based security model, and man, this is more complicated than I thought. It looks like it's going to require major changes to how the Activity Vocabulary API works. This isn't easy… 😂

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social · Reply to wakest ⁂'s post

@liaizon @Tak I think it's a bug on our side (Hackers' Pub).

Esurio's avatar
Esurio

@esurio1673@c.koliosky.com

🥴

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

  • MicroSoft
  • Github
  • Iphone
  • Javascript
  • CoPilot
  • NodeJS
  • NeoVim
洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

I really want Kagi Translate API.

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

Optique 0.5.0 is out! Enhanced error handling and message customization for TypeScript CLI parsing.

Key improvements:

  • Fully customizable error messages
  • Automatic error conversion for withDefault callbacks
  • Better help text and module organization
  • 100% backward compatible
洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


We're pleased to announce the release of Optique 0.5.0, which brings significant improvements to error handling, help text generation, and overall developer experience. This release maintains full backward compatibility, so you can upgrade without modifying existing code.

Better code organization through module separation

The large @optique/core/parser module has been refactored into three focused modules that better reflect their purposes. Primitive parsers like option() and argument() now live in @optique/core/primitives, modifier functions such as optional() and withDefault() have moved to @optique/core/modifiers, and combinator functions including object() and or() are now in @optique/core/constructs.

// Before: everything from one module
import { 
  option, flag, argument,        // primitives
  optional, withDefault, multiple, // modifiers
  object, or, merge              // constructs
} from "@optique/core/parser";

// After: organized imports (recommended)
import { option, flag, argument } from "@optique/core/primitives";
import { optional, withDefault, multiple } from "@optique/core/modifiers";
import { object, or, merge } from "@optique/core/constructs";

While we recommend importing from these specialized modules for better clarity, all functions continue to be re-exported from the original @optique/core/parser module to ensure your existing code works unchanged. This reorganization makes the codebase more maintainable and helps developers understand the relationships between different parser types.

Smarter error handling with automatic conversion

One of the most requested features has been better error handling for default value callbacks in withDefault(). Previously, if your callback threw an error—say, when an environment variable wasn't set—that error would bubble up as a runtime exception. Starting with 0.5.0, these errors are automatically caught and converted to parser-level errors, providing consistent error formatting and proper exit codes.

// Before (0.4.x): runtime exception that crashes the app
const parser = object({
  apiUrl: withDefault(option("--url", url()), () => {
    if (!process.env.API_URL) {
      throw new Error("API_URL not set"); // Uncaught exception!
    }
    return new URL(process.env.API_URL);
  })
});

// After (0.5.0): graceful parser error
const parser = object({
  apiUrl: withDefault(option("--url", url()), () => {
    if (!process.env.API_URL) {
      throw new Error("API_URL not set"); // Automatically caught and formatted
    }
    return new URL(process.env.API_URL);
  })
});

We've also introduced the WithDefaultError class, which accepts structured messages instead of plain strings. This means you can now throw errors with rich formatting that matches the rest of Optique's error output:

import { WithDefaultError, message, envVar } from "@optique/core";

const parser = object({
  // Plain error - automatically converted to text
  databaseUrl: withDefault(option("--db", url()), () => {
    if (!process.env.DATABASE_URL) {
      throw new Error("Database URL not configured");
    }
    return new URL(process.env.DATABASE_URL);
  }),

  // Rich error with structured message
  apiToken: withDefault(option("--token", string()), () => {
    if (!process.env.API_TOKEN) {
      throw new WithDefaultError(
        message`Environment variable ${envVar("API_TOKEN")} is required for authentication`
      );
    }
    return process.env.API_TOKEN;
  })
});

The new envVar message component ensures environment variables are visually distinct in error messages, appearing bold and underlined in colored output or wrapped in backticks in plain text.

More helpful help text with custom default descriptions

Default values in help text can sometimes be misleading, especially when they come from environment variables or are computed at runtime. Optique 0.5.0 allows you to customize how default values appear in help output through an optional third parameter to withDefault().

import { withDefault, message, envVar } from "@optique/core";

const parser = object({
  // Before: shows actual URL value in help
  apiUrl: withDefault(
    option("--api-url", url()),
    new URL("https://api.example.com")
  ),
  // Help shows: --api-url URL [https://api.example.com]

  // After: shows descriptive text
  apiUrl: withDefault(
    option("--api-url", url()),
    new URL("https://api.example.com"),
    { message: message`Default API endpoint` }
  ),
  // Help shows: --api-url URL [Default API endpoint]
});

This is particularly useful for environment variables and computed defaults:

const parser = object({
  // Environment variable
  authToken: withDefault(
    option("--token", string()),
    () => process.env.AUTH_TOKEN || "anonymous",
    { message: message`${envVar("AUTH_TOKEN")} or anonymous` }
  ),
  // Help shows: --token STRING [AUTH_TOKEN or anonymous]

  // Computed value
  workers: withDefault(
    option("--workers", integer()),
    () => os.cpus().length,
    { message: message`Number of CPU cores` }
  ),
  // Help shows: --workers INT [Number of CPU cores]

  // Sensitive information
  apiKey: withDefault(
    option("--api-key", string()),
    () => process.env.SECRET_KEY || "",
    { message: message`From secure storage` }
  ),
  // Help shows: --api-key STRING [From secure storage]
});

Instead of displaying the actual default value, you can now show descriptive text that better explains where the value comes from. This is particularly useful for sensitive information like API tokens or for computed defaults like the number of CPU cores.

The help system now properly handles ANSI color codes in default value displays, maintaining dim styling even when inner components have their own color formatting. This ensures default values remain visually distinct from the main help text.

Comprehensive error message customization

We've added a systematic way to customize error messages across all parser types and combinators. Every parser now accepts an errors option that lets you provide context-specific feedback instead of generic error messages. This applies to primitive parsers, value parsers, combinators, and even specialized parsers in companion packages.

Primitive parser errors

import { option, flag, argument, command } from "@optique/core/primitives";
import { message, optionName, metavar } from "@optique/core/message";

// Option parser with custom errors
const serverPort = option("--port", integer(), {
  errors: {
    missing: message`Server port is required. Use ${optionName("--port")} to specify.`,
    invalidValue: (error) => message`Invalid port number: ${error}`,
    endOfInput: message`${optionName("--port")} requires a ${metavar("PORT")} number.`
  }
});

// Command parser with custom errors
const deployCommand = command("deploy", deployParser, {
  errors: {
    notMatched: (expected, actual) => 
      message`Unknown command "${actual}". Did you mean "${expected}"?`
  }
});

Value parser errors

Error customization can be static messages for consistent errors or dynamic functions that incorporate the problematic input:

import { integer, choice, string } from "@optique/core/valueparser";

// Integer with range validation
const port = integer({
  min: 1024,
  max: 65535,
  errors: {
    invalidInteger: message`Port must be a valid number.`,
    belowMinimum: (value, min) =>
      message`Port ${String(value)} is reserved. Use ${String(min)} or higher.`,
    aboveMaximum: (value, max) =>
      message`Port ${String(value)} exceeds maximum. Use ${String(max)} or lower.`
  }
});

// Choice with helpful suggestions
const logLevel = choice(["debug", "info", "warn", "error"], {
  errors: {
    invalidChoice: (input, choices) =>
      message`"${input}" is not a valid log level. Choose from: ${values(choices)}.`
  }
});

// String with pattern validation
const email = string({
  pattern: /^[^@]+@[^@]+\.[^@]+$/,
  errors: {
    patternMismatch: (input) =>
      message`"${input}" is not a valid email address. Use format: user@example.com`
  }
});

Combinator errors

import { or, multiple, object } from "@optique/core/constructs";

// Or combinator with custom no-match error
const format = or(
  flag("--json"),
  flag("--yaml"),
  flag("--xml"),
  {
    errors: {
      noMatch: message`Please specify an output format: --json, --yaml, or --xml.`,
      unexpectedInput: (token) =>
        message`Unknown format option "${token}".`
    }
  }
);

// Multiple parser with count validation
const inputFiles = multiple(argument(string()), {
  min: 1,
  max: 5,
  errors: {
    tooFew: (count, min) =>
      message`At least ${String(min)} file required, but got ${String(count)}.`,
    tooMany: (count, max) =>
      message`Maximum ${String(max)} files allowed, but got ${String(count)}.`
  }
});

Package-specific errors

Both @optique/run and @optique/temporal packages have been updated with error customization support for their specialized parsers:

// @optique/run path parser
import { path } from "@optique/run/valueparser";

const configFile = option("--config", path({
  mustExist: true,
  type: "file",
  extensions: [".json", ".yaml"],
  errors: {
    pathNotFound: (input) =>
      message`Configuration file "${input}" not found. Please check the path.`,
    notAFile: (input) =>
      message`"${input}" is a directory. Please specify a file.`,
    invalidExtension: (input, extensions, actual) =>
      message`Invalid config format "${actual}". Use ${values(extensions)}.`
  }
}));

// @optique/temporal instant parser
import { instant, duration } from "@optique/temporal";

const timestamp = option("--time", instant({
  errors: {
    invalidFormat: (input) =>
      message`"${input}" is not a valid timestamp. Use ISO 8601 format: 2024-01-01T12:00:00Z`
  }
}));

const timeout = option("--timeout", duration({
  errors: {
    invalidFormat: (input) =>
      message`"${input}" is not a valid duration. Use ISO 8601 format: PT30S (30 seconds), PT5M (5 minutes)`
  }
}));

Error customization integrates seamlessly with Optique's structured message format, ensuring consistent styling across all error output. The system helps you provide helpful, actionable feedback that guides users toward correct usage rather than leaving them confused by generic error messages.

Looking forward

This release focuses on improving the developer experience without breaking existing code. Every new feature is opt-in, and all changes maintain backward compatibility. We believe these improvements make Optique more pleasant to work with, especially when building user-friendly CLI applications that need clear error messages and helpful documentation.

We're grateful to the community members who suggested these improvements and helped shape this release through discussions and issue reports. Your feedback continues to drive Optique's evolution toward being a more capable and ergonomic CLI parser for TypeScript.

To upgrade to Optique 0.5.0, simply update your dependencies:

npm update @optique/core @optique/run
# or
deno update

For detailed migration guidance and API documentation, please refer to the official documentation. While no code changes are required, we encourage you to explore the new error customization options and help text improvements to enhance your CLI applications.

洪 民憙 (Hong Minhee)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hackers.pub


We're pleased to announce the release of Optique 0.5.0, which brings significant improvements to error handling, help text generation, and overall developer experience. This release maintains full backward compatibility, so you can upgrade without modifying existing code.

Better code organization through module separation

The large @optique/core/parser module has been refactored into three focused modules that better reflect their purposes. Primitive parsers like option() and argument() now live in @optique/core/primitives, modifier functions such as optional() and withDefault() have moved to @optique/core/modifiers, and combinator functions including object() and or() are now in @optique/core/constructs.

// Before: everything from one module
import { 
  option, flag, argument,        // primitives
  optional, withDefault, multiple, // modifiers
  object, or, merge              // constructs
} from "@optique/core/parser";

// After: organized imports (recommended)
import { option, flag, argument } from "@optique/core/primitives";
import { optional, withDefault, multiple } from "@optique/core/modifiers";
import { object, or, merge } from "@optique/core/constructs";

While we recommend importing from these specialized modules for better clarity, all functions continue to be re-exported from the original @optique/core/parser module to ensure your existing code works unchanged. This reorganization makes the codebase more maintainable and helps developers understand the relationships between different parser types.

Smarter error handling with automatic conversion

One of the most requested features has been better error handling for default value callbacks in withDefault(). Previously, if your callback threw an error—say, when an environment variable wasn't set—that error would bubble up as a runtime exception. Starting with 0.5.0, these errors are automatically caught and converted to parser-level errors, providing consistent error formatting and proper exit codes.

// Before (0.4.x): runtime exception that crashes the app
const parser = object({
  apiUrl: withDefault(option("--url", url()), () => {
    if (!process.env.API_URL) {
      throw new Error("API_URL not set"); // Uncaught exception!
    }
    return new URL(process.env.API_URL);
  })
});

// After (0.5.0): graceful parser error
const parser = object({
  apiUrl: withDefault(option("--url", url()), () => {
    if (!process.env.API_URL) {
      throw new Error("API_URL not set"); // Automatically caught and formatted
    }
    return new URL(process.env.API_URL);
  })
});

We've also introduced the WithDefaultError class, which accepts structured messages instead of plain strings. This means you can now throw errors with rich formatting that matches the rest of Optique's error output:

import { WithDefaultError, message, envVar } from "@optique/core";

const parser = object({
  // Plain error - automatically converted to text
  databaseUrl: withDefault(option("--db", url()), () => {
    if (!process.env.DATABASE_URL) {
      throw new Error("Database URL not configured");
    }
    return new URL(process.env.DATABASE_URL);
  }),

  // Rich error with structured message
  apiToken: withDefault(option("--token", string()), () => {
    if (!process.env.API_TOKEN) {
      throw new WithDefaultError(
        message`Environment variable ${envVar("API_TOKEN")} is required for authentication`
      );
    }
    return process.env.API_TOKEN;
  })
});

The new envVar message component ensures environment variables are visually distinct in error messages, appearing bold and underlined in colored output or wrapped in backticks in plain text.

More helpful help text with custom default descriptions

Default values in help text can sometimes be misleading, especially when they come from environment variables or are computed at runtime. Optique 0.5.0 allows you to customize how default values appear in help output through an optional third parameter to withDefault().

import { withDefault, message, envVar } from "@optique/core";

const parser = object({
  // Before: shows actual URL value in help
  apiUrl: withDefault(
    option("--api-url", url()),
    new URL("https://api.example.com")
  ),
  // Help shows: --api-url URL [https://api.example.com]

  // After: shows descriptive text
  apiUrl: withDefault(
    option("--api-url", url()),
    new URL("https://api.example.com"),
    { message: message`Default API endpoint` }
  ),
  // Help shows: --api-url URL [Default API endpoint]
});

This is particularly useful for environment variables and computed defaults:

const parser = object({
  // Environment variable
  authToken: withDefault(
    option("--token", string()),
    () => process.env.AUTH_TOKEN || "anonymous",
    { message: message`${envVar("AUTH_TOKEN")} or anonymous` }
  ),
  // Help shows: --token STRING [AUTH_TOKEN or anonymous]

  // Computed value
  workers: withDefault(
    option("--workers", integer()),
    () => os.cpus().length,
    { message: message`Number of CPU cores` }
  ),
  // Help shows: --workers INT [Number of CPU cores]

  // Sensitive information
  apiKey: withDefault(
    option("--api-key", string()),
    () => process.env.SECRET_KEY || "",
    { message: message`From secure storage` }
  ),
  // Help shows: --api-key STRING [From secure storage]
});

Instead of displaying the actual default value, you can now show descriptive text that better explains where the value comes from. This is particularly useful for sensitive information like API tokens or for computed defaults like the number of CPU cores.

The help system now properly handles ANSI color codes in default value displays, maintaining dim styling even when inner components have their own color formatting. This ensures default values remain visually distinct from the main help text.

Comprehensive error message customization

We've added a systematic way to customize error messages across all parser types and combinators. Every parser now accepts an errors option that lets you provide context-specific feedback instead of generic error messages. This applies to primitive parsers, value parsers, combinators, and even specialized parsers in companion packages.

Primitive parser errors

import { option, flag, argument, command } from "@optique/core/primitives";
import { message, optionName, metavar } from "@optique/core/message";

// Option parser with custom errors
const serverPort = option("--port", integer(), {
  errors: {
    missing: message`Server port is required. Use ${optionName("--port")} to specify.`,
    invalidValue: (error) => message`Invalid port number: ${error}`,
    endOfInput: message`${optionName("--port")} requires a ${metavar("PORT")} number.`
  }
});

// Command parser with custom errors
const deployCommand = command("deploy", deployParser, {
  errors: {
    notMatched: (expected, actual) => 
      message`Unknown command "${actual}". Did you mean "${expected}"?`
  }
});

Value parser errors

Error customization can be static messages for consistent errors or dynamic functions that incorporate the problematic input:

import { integer, choice, string } from "@optique/core/valueparser";

// Integer with range validation
const port = integer({
  min: 1024,
  max: 65535,
  errors: {
    invalidInteger: message`Port must be a valid number.`,
    belowMinimum: (value, min) =>
      message`Port ${String(value)} is reserved. Use ${String(min)} or higher.`,
    aboveMaximum: (value, max) =>
      message`Port ${String(value)} exceeds maximum. Use ${String(max)} or lower.`
  }
});

// Choice with helpful suggestions
const logLevel = choice(["debug", "info", "warn", "error"], {
  errors: {
    invalidChoice: (input, choices) =>
      message`"${input}" is not a valid log level. Choose from: ${values(choices)}.`
  }
});

// String with pattern validation
const email = string({
  pattern: /^[^@]+@[^@]+\.[^@]+$/,
  errors: {
    patternMismatch: (input) =>
      message`"${input}" is not a valid email address. Use format: user@example.com`
  }
});

Combinator errors

import { or, multiple, object } from "@optique/core/constructs";

// Or combinator with custom no-match error
const format = or(
  flag("--json"),
  flag("--yaml"),
  flag("--xml"),
  {
    errors: {
      noMatch: message`Please specify an output format: --json, --yaml, or --xml.`,
      unexpectedInput: (token) =>
        message`Unknown format option "${token}".`
    }
  }
);

// Multiple parser with count validation
const inputFiles = multiple(argument(string()), {
  min: 1,
  max: 5,
  errors: {
    tooFew: (count, min) =>
      message`At least ${String(min)} file required, but got ${String(count)}.`,
    tooMany: (count, max) =>
      message`Maximum ${String(max)} files allowed, but got ${String(count)}.`
  }
});

Package-specific errors

Both @optique/run and @optique/temporal packages have been updated with error customization support for their specialized parsers:

// @optique/run path parser
import { path } from "@optique/run/valueparser";

const configFile = option("--config", path({
  mustExist: true,
  type: "file",
  extensions: [".json", ".yaml"],
  errors: {
    pathNotFound: (input) =>
      message`Configuration file "${input}" not found. Please check the path.`,
    notAFile: (input) =>
      message`"${input}" is a directory. Please specify a file.`,
    invalidExtension: (input, extensions, actual) =>
      message`Invalid config format "${actual}". Use ${values(extensions)}.`
  }
}));

// @optique/temporal instant parser
import { instant, duration } from "@optique/temporal";

const timestamp = option("--time", instant({
  errors: {
    invalidFormat: (input) =>
      message`"${input}" is not a valid timestamp. Use ISO 8601 format: 2024-01-01T12:00:00Z`
  }
}));

const timeout = option("--timeout", duration({
  errors: {
    invalidFormat: (input) =>
      message`"${input}" is not a valid duration. Use ISO 8601 format: PT30S (30 seconds), PT5M (5 minutes)`
  }
}));

Error customization integrates seamlessly with Optique's structured message format, ensuring consistent styling across all error output. The system helps you provide helpful, actionable feedback that guides users toward correct usage rather than leaving them confused by generic error messages.

Looking forward

This release focuses on improving the developer experience without breaking existing code. Every new feature is opt-in, and all changes maintain backward compatibility. We believe these improvements make Optique more pleasant to work with, especially when building user-friendly CLI applications that need clear error messages and helpful documentation.

We're grateful to the community members who suggested these improvements and helped shape this release through discussions and issue reports. Your feedback continues to drive Optique's evolution toward being a more capable and ergonomic CLI parser for TypeScript.

To upgrade to Optique 0.5.0, simply update your dependencies:

npm update @optique/core @optique/run
# or
deno update

For detailed migration guidance and API documentation, please refer to the official documentation. While no code changes are required, we encourage you to explore the new error customization options and help text improvements to enhance your CLI applications.

洪 民憙 (Hong Minhee) :nonbinary:'s avatar
洪 民憙 (Hong Minhee) :nonbinary:

@hongminhee@hollo.social

名畫(명화)

chomu

@chomu@oeee.cafe

종이를 물고 있는 퍼렁공뇽

https://github.com/fedify-dev/fedify

종이를 물고 있는 퍼렁공뇽
ALT text details종이를 물고 있는 퍼렁공뇽
Mitchell Hashimoto's avatar
Mitchell Hashimoto

@mitchellh@hachyderm.io

Libghostty is coming. 👻 The first library will be libghostty-vt: a zero-dependency (not even libc!) library that provides an API for parsing terminal sequences and maintaining terminal state, extracted directly from Ghostty's real-world proven core. mitchellh.com/writing/libghost

← Newer
Older →