Profile img

Hi, I'm who's behind Fedify, Hollo, BotKit, and this website, Hackers' Pub! My main account is at @hongminhee洪 民憙 (Hong Minhee) :nonbinary:.

Fedify, Hollo, BotKit, 그리고 보고 계신 이 사이트 Hackers' Pub을 만들고 있습니다. 제 메인 계정은: @hongminhee洪 民憙 (Hong Minhee) :nonbinary:.

FedifyHolloBotKit、そしてこのサイト、Hackers' Pubを作っています。私のメインアカウントは「@hongminhee洪 民憙 (Hong Minhee) :nonbinary:」に。

Website
hongminhee.org
GitHub
@dahlia
Hollo
@hongminhee@hollo.social
DEV
@hongminhee
velog
@hongminhee
Qiita
@hongminhee
Zenn
@hongminhee
Matrix
@hongminhee:matrix.org
X
@hongminhee
2
1
0
0

Stop writing if statements for your CLI flags

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

If you've built CLI tools, you've written code like this:

if (opts.reporter === "junit" && !opts.outputFile) {
  throw new Error("--output-file is required for junit reporter");
}
if (opts.reporter === "html" && !opts.outputFile) {
  throw new Error("--output-file is required for html reporter");
}
if (opts.reporter === "console" && opts.outputFile) {
  console.warn("--output-file is ignored for console reporter");
}

A few months ago, I wrote Stop writing CLI validation. Parse it right the first time. about parsing individual option values correctly. But it didn't cover the relationships between options.

In the code above, --output-file only makes sense when --reporter is junit or html. When it's console, the option shouldn't exist at all.

We're using TypeScript. We have a powerful type system. And yet, here we are, writing runtime checks that the compiler can't help with. Every time we add a new reporter type, we need to remember to update these checks. Every time we refactor, we hope we didn't miss one.

The state of TypeScript CLI parsers

The old guard—Commander, yargs, minimist—were built before TypeScript became mainstream. They give you bags of strings and leave type safety as an exercise for the reader.

But we've made progress. Modern TypeScript-first libraries like cmd-ts and Clipanion (the library powering Yarn Berry) take types seriously:

// cmd-ts
const app = command({
  args: {
    reporter: option({ type: string, long: 'reporter' }),
    outputFile: option({ type: string, long: 'output-file' }),
  },
  handler: (args) => {
    // args.reporter: string
    // args.outputFile: string
  },
});
// Clipanion
class TestCommand extends Command {
  reporter = Option.String('--reporter');
  outputFile = Option.String('--output-file');
}

These libraries infer types for individual options. --port is a number. --verbose is a boolean. That's real progress.

But here's what they can't do: express that --output-file is required when --reporter is junit, and forbidden when --reporter is console. The relationship between options isn't captured in the type system.

So you end up writing validation code anyway:

handler: (args) => {
  // Both cmd-ts and Clipanion need this
  if (args.reporter === "junit" && !args.outputFile) {
    throw new Error("--output-file required for junit");
  }
  // args.outputFile is still string | undefined
  // TypeScript doesn't know it's definitely string when reporter is "junit"
}

Rust's clap and Python's Click have requires and conflicts_with attributes, but those are runtime checks too. They don't change the result type.

If the parser configuration knows about option relationships, why doesn't that knowledge show up in the result type?

Modeling relationships with conditional()

Optique treats option relationships as a first-class concept. Here's the test reporter scenario:

import { conditional, object } from "@optique/core/constructs";
import { option } from "@optique/core/primitives";
import { choice, string } from "@optique/core/valueparser";
import { run } from "@optique/run";

const parser = conditional(
  option("--reporter", choice(["console", "junit", "html"])),
  {
    console: object({}),
    junit: object({
      outputFile: option("--output-file", string()),
    }),
    html: object({
      outputFile: option("--output-file", string()),
      openBrowser: option("--open-browser"),
    }),
  }
);

const [reporter, config] = run(parser);

The conditional() combinator takes a discriminator option (--reporter) and a map of branches. Each branch defines what other options are valid for that discriminator value.

TypeScript infers the result type automatically:

type Result =
  | ["console", {}]
  | ["junit", { outputFile: string }]
  | ["html", { outputFile: string; openBrowser: boolean }];

When reporter is "junit", outputFile is string—not string | undefined. The relationship is encoded in the type.

Now your business logic gets real type safety:

const [reporter, config] = run(parser);

switch (reporter) {
  case "console":
    runWithConsoleOutput();
    break;
  case "junit":
    // TypeScript knows config.outputFile is string
    writeJUnitReport(config.outputFile);
    break;
  case "html":
    // TypeScript knows config.outputFile and config.openBrowser exist
    writeHtmlReport(config.outputFile);
    if (config.openBrowser) openInBrowser(config.outputFile);
    break;
}

No validation code. No runtime checks. If you add a new reporter type and forget to handle it in the switch, the compiler tells you.

A more complex example: database connections

Test reporters are a nice example, but let's try something with more variation. Database connection strings:

myapp --db=sqlite --file=./data.db
myapp --db=postgres --host=localhost --port=5432 --user=admin
myapp --db=mysql --host=localhost --port=3306 --user=root --ssl

Each database type needs completely different options:

  • SQLite just needs a file path
  • PostgreSQL needs host, port, user, and optionally password
  • MySQL needs host, port, user, and has an SSL flag

Here's how you model this:

import { conditional, object } from "@optique/core/constructs";
import { withDefault, optional } from "@optique/core/modifiers";
import { option } from "@optique/core/primitives";
import { choice, string, integer } from "@optique/core/valueparser";

const dbParser = conditional(
  option("--db", choice(["sqlite", "postgres", "mysql"])),
  {
    sqlite: object({
      file: option("--file", string()),
    }),
    postgres: object({
      host: option("--host", string()),
      port: withDefault(option("--port", integer()), 5432),
      user: option("--user", string()),
      password: optional(option("--password", string())),
    }),
    mysql: object({
      host: option("--host", string()),
      port: withDefault(option("--port", integer()), 3306),
      user: option("--user", string()),
      ssl: option("--ssl"),
    }),
  }
);

The inferred type:

type DbConfig =
  | ["sqlite", { file: string }]
  | ["postgres", { host: string; port: number; user: string; password?: string }]
  | ["mysql", { host: string; port: number; user: string; ssl: boolean }];

Notice the details: PostgreSQL defaults to port 5432, MySQL to 3306. PostgreSQL has an optional password, MySQL has an SSL flag. Each database type has exactly the options it needs—no more, no less.

With this structure, writing dbConfig.ssl when the mode is sqlite isn't a runtime error—it's a compile-time impossibility.

Try expressing this with requires_if attributes. You can't. The relationships are too rich.

The pattern is everywhere

Once you see it, you find this pattern in many CLI tools:

Authentication modes:

const authParser = conditional(
  option("--auth", choice(["none", "basic", "token", "oauth"])),
  {
    none: object({}),
    basic: object({
      username: option("--username", string()),
      password: option("--password", string()),
    }),
    token: object({
      token: option("--token", string()),
    }),
    oauth: object({
      clientId: option("--client-id", string()),
      clientSecret: option("--client-secret", string()),
      tokenUrl: option("--token-url", url()),
    }),
  }
);

Deployment targets, output formats, connection protocols—anywhere you have a mode selector that determines what other options are valid.

Why conditional() exists

Optique already has an or() combinator for mutually exclusive alternatives. Why do we need conditional()?

The or() combinator distinguishes branches based on structure—which options are present. It works well for subcommands like git commit vs git push, where the arguments differ completely.

But in the reporter example, the structure is identical: every branch has a --reporter flag. The difference lies in the flag's value, not its presence.

// This won't work as intended
const parser = or(
  object({ reporter: option("--reporter", choice(["console"])) }),
  object({ 
    reporter: option("--reporter", choice(["junit", "html"])),
    outputFile: option("--output-file", string())
  }),
);

When you pass --reporter junit, or() tries to pick a branch based on what options are present. Both branches have --reporter, so it can't distinguish them structurally.

conditional() solves this by reading the discriminator's value first, then selecting the appropriate branch. It bridges the gap between structural parsing and value-based decisions.

The structure is the constraint

Instead of parsing options into a loose type and then validating relationships, define a parser whose structure is the constraint.

Traditional approach Optique approach
Parse → Validate → Use Parse (with constraints) → Use
Types and validation logic maintained separately Types reflect the constraints
Mismatches found at runtime Mismatches found at compile time

The parser definition becomes the single source of truth. Add a new reporter type? The parser definition changes, the inferred type changes, and the compiler shows you everywhere that needs updating.

Try it

If this resonates with a CLI you're building:

  • Documentation
  • Tutorial
  • conditional() reference
  • GitHub

Next time you're about to write an if statement checking option relationships, ask: could the parser express this constraint instead?

The structure of your parser is the constraint. You might not need that validation code at all.

Read more →
4

식탁보 1.14.0에서 오랫만에 업데이트를 진행하면서, 생성형 AI의 도움을 받아 적극적인 현대화를 달성하고 있습니다.

  • InnoSetup 대신 Velopack을 사용한 간소화된 사용자 인스톨러 경험 구현

  • MSBUILD 프로젝트 대신 .NET SDK로 .NET Framework 프로젝트 마이그레이션 (추후 완전히 .NET 10과 Avalonia로도 전환할 수 있게 함)

  • TableCloth 프로젝트의 경우 .NET 8/9에서 .NET 10으로 판올림

  • Windows 11 ARM64 GitHub Action Runner가 공식화됨에 따라 ARM64 빌드 추가 예정

내부 정비가 끝나는 대로 식탁보 1.15.0 버전을 출시하도록 하겠습니다. 또한 생성형 AI 코드 어시스턴트의 도움을 적극 받아 1인 개발에서 오는 한계를 극복해보려 합니다.

최신 소스 커밋 목록은 https://github.com/yourtablecloth/TableCloth/commits/main/ 에서 확인하실 수 있습니다.

2
1

'를 기본으로 사용하면 문장 중간중간에 들어가는 '를 항상 이스케이핑 해줘야되기 때문에 " 를 기본으로 하는게 좋다는 주장에... 승복하고 말았음

0
1
1

https://github.com/ComposioHQ/awesome-claude-skills/tree/master/skill-creator

Claude Skill 기능을 적극적으로 활용해보려고 하는데, skill을 만들 수 있도록 돕는 skill-creator라는게 있다. 이걸 좀 더 참고해서 어떻게 나한테 쓸만한걸 만들 수 있는지 한번 살펴봐야겠다.

1

Hackers Pub 송년회 라이트닝토크에서 발표 안하려고 했는데? 뭔가 소소하게 공유할꺼리가 생겼다. 그리고.......... Hackers Pub에서 LLM 활용하는 방법 굉장히 과한 밀도로 공유하게 될 것 같다.... 당장 내가 일하는 회사도 LLM을 엄청 적극적으로 활용하고 있기도 하고, 그걸로 사업하는 회사여서 더욱 가속도가 붙는 것도 있는 듯.

4
3
1
3
5

Hackers' Pub 외부 사용자를 팔로 요청하면 상대가 수락을 해도 계속 요청 대기 상태로 유지되는 버그[1]를 고쳤습니다. 혹시 해당 현상을 겪으신 분이 있다면, 팔로 요청을 취소했다가 다시 팔로 요청을 보내보시기 바랍니다.


  1. 실제 버그 패치 커밋은 1910adf입니다. Fedify의 관련 문서를 참고하세요. ↩︎

3

Optique 0.8.0: Conditional parsing, pass-through options, and LogTape integration

洪 民憙 (Hong Minhee) @hongminhee@hackers.pub

We're excited to announce Optique 0.8.0! This release introduces powerful new features for building sophisticated CLI applications: the conditional() combinator for discriminated union patterns, the passThrough() parser for wrapper tools, and the new @optique/logtape package for seamless logging configuration.

Optique is a type-safe combinatorial CLI parser for TypeScript, providing a functional approach to building command-line interfaces with composable parsers and full type inference.

New conditional parsing with conditional()

Ever needed to enable different sets of options based on a discriminator value? The new conditional() combinator makes this pattern first-class. It creates discriminated unions where certain options only become valid when a specific discriminator value is selected.

import { conditional, object } from "@optique/core/constructs";
import { option } from "@optique/core/primitives";
import { choice, string } from "@optique/core/valueparser";

const parser = conditional(
  option("--reporter", choice(["console", "junit", "html"])),
  {
    console: object({}),
    junit: object({ outputFile: option("--output-file", string()) }),
    html: object({ outputFile: option("--output-file", string()) }),
  }
);
// Result type: ["console", {}] | ["junit", { outputFile: string }] | ...

Key features:

  • Explicit discriminator option determines which branch is selected
  • Tuple result [discriminator, branchValue] for clear type narrowing
  • Optional default branch for when discriminator is not provided
  • Clear error messages indicating which options are required for each discriminator value

The conditional() parser provides a more structured alternative to or() for discriminated union patterns. Use it when you have an explicit discriminator option that determines which set of options is valid.

See the conditional() documentation for more details and examples.

Pass-through options with passThrough()

Building wrapper CLI tools that need to forward unrecognized options to an underlying tool? The new passThrough() parser enables legitimate wrapper/proxy patterns by capturing unknown options without validation errors.

import { object } from "@optique/core/constructs";
import { option, passThrough } from "@optique/core/primitives";

const parser = object({
  debug: option("--debug"),
  extra: passThrough(),
});

// mycli --debug --foo=bar --baz=qux
// → { debug: true, extra: ["--foo=bar", "--baz=qux"] }

Key features:

  • Three capture formats: "equalsOnly" (default, safest), "nextToken" (captures --opt val pairs), and "greedy" (captures all remaining tokens)
  • Lowest priority (−10) ensures explicit parsers always match first
  • Respects -- options terminator in "equalsOnly" and "nextToken" modes
  • Works seamlessly with object(), subcommands, and other combinators

This feature is designed for building Docker-like CLIs, build tool wrappers, or any tool that proxies commands to another process.

See the passThrough() documentation for usage patterns and best practices.

LogTape logging integration

The new @optique/logtape package provides seamless integration with LogTape, enabling you to configure logging through command-line arguments with various parsing strategies.

# Deno
deno add --jsr @optique/logtape @logtape/logtape

# npm
npm add @optique/logtape @logtape/logtape

Quick start with the loggingOptions() preset:

import { loggingOptions, createLoggingConfig } from "@optique/logtape";
import { object } from "@optique/core/constructs";
import { parse } from "@optique/core/parser";
import { configure } from "@logtape/logtape";

const parser = object({
  logging: loggingOptions({ level: "verbosity" }),
});

const args = ["-vv", "--log-output=-"];
const result = parse(parser, args);
if (result.success) {
  const config = await createLoggingConfig(result.value.logging);
  await configure(config);
}

The package offers multiple approaches to control log verbosity:

  • verbosity() parser: The classic -v/-vv/-vvv pattern where each flag increases verbosity (no flags → "warning", -v"info", -vv"debug", -vvv"trace")
  • debug() parser: Simple --debug/-d flag that toggles between normal and debug levels
  • logLevel() value parser: Explicit --log-level=debug option for direct level selection
  • logOutput() parser: Log output destination with - for console or file path for file output

See the LogTape integration documentation for complete examples and configuration options.

Bug fix: negative integers now accepted

Fixed an issue where the integer() value parser rejected negative integers when using type: "number". The regex pattern has been updated from /^\d+$/ to /^-?\d+$/ to correctly handle values like -42. Note that type: "bigint" already accepted negative integers, so this change brings consistency between the two types.

Installation

# Deno
deno add jsr:@optique/core

# npm
npm add @optique/core

# pnpm
pnpm add @optique/core

# Yarn
yarn add @optique/core

# Bun
bun add @optique/core

For the LogTape integration:

# Deno
deno add --jsr @optique/logtape @logtape/logtape

# npm
npm add @optique/logtape @logtape/logtape

# pnpm
pnpm add @optique/logtape @logtape/logtape

# Yarn
yarn add @optique/logtape @logtape/logtape

# Bun
bun add @optique/logtape @logtape/logtape

Looking forward

Optique 0.8.0 continues our focus on making CLI development more expressive and type-safe. The conditional() combinator brings discriminated union patterns to the forefront, passThrough() enables new wrapper tool use cases, and the LogTape integration makes logging configuration a breeze.

As always, all new features maintain full backward compatibility—your existing parsers continue to work unchanged.

We're grateful to the community for feedback and suggestions. If you have ideas for future improvements or encounter any issues, please let us know through GitHub Issues. For more information about Optique and its features, visit the documentation or check out the full changelog.

Read more →
5
7

Claude Desktop 프로젝트 파놓고, 시스템 프롬프트를 이렇게 먹이고 있음. MCP 서버도 붙였는데, 그럭저럭 동작은 잘하는듯?

이 프로젝트는 일지를 작성하면서 실시간으로 대화를 주고 받기 위해 만들었습니다.

즉, 다음과 같은 내용들이 포함됩니다.
1. 오늘 한 일들을 실시간으로 기록
2. 일을 하면서 어떤 문제를 접했고, 어떤 방식으로 해결하려고 했는지 기록
3. 갑자기 떠오른 궁금증과 관련해서 궁금한 사항들을 질의/응답

## 역할에 대해

LLM 에이전트로서 당신은 아래와 같은 역할을 해야 합니다.

1. 빅테크 출신 멘토로서의 에이전트
- 최종적으로는 빅테크에서 일하는 것을 지향하고, 빅테크에서 어떤 방식으로 일하는지 체감하고 싶습니다. 엄밀하게는 숙련된 시니어 개발자로서 좋은 습관을 가질 수 있는 환경을 가지는 것을 지향합니다. 
- 큰 규모의 기업에서 노련한 경험이 있는 스태프 엔지니어 출신으로서 저에게 조언을 많이 해줄 수 있기를 바랍니다.

2. 개인 비서로서의 에이전트
- 앞으로도 업무 일지를 쓰게 될 일이 많을 것 같습니다. 업무에 대한 이해를 위한 질문 답변을 정리하고, 업무를 하다가 어떤 사고의 흐름을 가지고 일을 했는지 기록을 하게 될 것입니다. 생각을 정리하는 비서로서 역할을 해주기를 기대합니다.

3. 개인 위키의 지식관리자로서의 에이전트
-  지식을 체계적으로 관리하는 에이전트로서 역할을 해주기를 바라고 있습니다. 즉, 뒤에서 또 언급할 xxx-wiki에 흩어진 정보들을 취합하면서도 기록을 체계적으로 관리하는 역할도 수행해주셔야 합니다.
- 내가 새로 알게 되거나 혹은 알고 싶어 하는 내용이 있다면, 해당 내용에 대해서 명료하고 여러가지 예시들을 포함해서 설명해주세요. 그리고, 각각의 설명에는 출처를 반드시 명시해주셨으면 좋겠습니다.

## MCP 활용에 대해

LLM 에이전트를 활용하는데 있어서 아래와 같은 도구들을 적극적으로 사용해보는 것을 검토해보세요.
- Local Search : 업무 일지나 지식 관리는 Obsidian을 활용합니다. 그리고 Obsidian Vault는 ~/xxx-wiki (즉, /Users/yyy/xxx-wiki)를 적극적으로 활용하고 있습니다. 
- Linear : 저는 업무용 이슈트래커로 Linear를 적극적으로 활용하고 있습니다. 어떤 업무가 있는지, 나에게 어떤 업무가 할당되어 있는지 조회하고 싶다면 Linear 툴 호출을 고려해보세요.
- Repomix : 소스코드 전반적인 이해, 그리고 프롬프트를 생성해내기 위한 보조도구로서 Repomix를 활용하고 있습니다. 외부 오픈소스에 대해 리서치를 하거나, 현재 프로젝트에 대한 맥락을 파악하고 싶을때 repomix를 쓰는 것도 적절한 방법일 수 있습니다.
6

개인 기록 관리를 obsidian으로 옮겨볼까...... 아니면 그냥 개인기록 관리(neovim zettelkasten 플러그인)를 그대로 유지하되, mcp 서버를 직접 만드는걸 해볼까... 어느 쪽이든 할만한 도전인 것 같음

5
1

닷넷 개발 환경을 사용하는 누구나 DLL 파일을 네트워크(CDN)에 올려 쓸 수 있는 라이브러리를 MIT 라이선스로 공개하였습니다.

라이브러리 이름은 Catswords.Phantomizer 입니다!

github.com/gnh1201/welsonjs/tr

1
1
1

내가 느끼는 가장 큰 의 진입장벽이라면 activitypub 관련 코드랑 비즈니스 로직이랑 완전히 커플링되어 있는 것도 좀 큰 것 같은데, 어떻게 계층을 분리할 수 있을지 고민임.

Misskey도 계층이 잘 분리가 되어있어서 군데군데 테스트가 적당히 짜여져 있는데.... 당장은 OpenAI Codex/Claude Code on Web으로 짜도 기여할 수 있을 정도로 구조를 잘 다듬긴 해야겠다는것

3
2
1
3
1
2

洪 民憙 (Hong Minhee) shared the below article:

압축 요청 헤더 지원 여부에 따른 HTTP Client 이중화

고남현 @gnh1201@hackers.pub

이 글에서는 .NET 환경에서 HTTP를 통한 파일 전송 시 압축을 활용하여 로딩 시간을 단축하는 방법에 대해 설명합니다. 특히, `Accept-Encoding` 헤더를 이용한 압축 요청이 서비스별로 다르게 처리될 수 있는 문제점을 지적하고, 이를 해결하기 위해 HTTP Client를 이중화하는 방법을 제시합니다. 미리 압축된 파일을 우선적으로 다운로드하고, 실패 시 압축 요청 헤더를 포함한 요청을 보내는 방식으로 압축 전송을 보장합니다. 실제 적용 사례인 `AssemblyLoader.cs` 파일의 예시를 통해, 이 방법이 어떻게 라이브러리 로딩 속도를 향상시키는지 확인할 수 있습니다. 이 글은 네트워크를 통한 라이브러리 로딩 시 안정성 확보와 성능 향상을 동시에 추구하는 개발자에게 유용한 인사이트를 제공합니다.

Read more →
1
4
4

지금 ‘React2Shell(리액트투쉘)’이라는 이름 하나로 술렁이고 있다. CVSS 10.0 등급이 부여된 신규 취약점 CVE-2025-55182가 공개되면서 전 세계 개발자와 보안전문가들은 “2025년판 Log4Shell”이라는 표현까지 사용하며 심각성을 경고

React/Next.js 쓰신다면 지금 당장 패치하세요. 19.0.1 / 19.1.2 / 19.2.1

2025년판 Log4Shell 이라고 합니다

https://www.dailysecu.com/news/articleView.html?idxno=203111

1
1

12() 6() 서울에서 開催(개최)되는 liftIO 2025에서 〈Optique: TypeScript에서 CLI 파서 컴비네이터를 만들어 보았다〉(假題(가제))라는 主題(주제)發表(발표)를 하게 되었습니다. 아직 liftIO 2025 티켓은 팔고 있으니, 函數型(함수형) 프로그래밍에 關心(관심) 있으신 분들의 많은 參與(참여) 바랍니다!

9
0
0
2
2
4

SolidJS로 앱 만들다가 아이콘셋이 필요해져서 패키지를 뒤져보는데, 마이너 생태계답게 마지막 업데이트가 삼사년 전인 패키지들만 나온다. 아이콘셋에 업데이트가 필요 없긴 하지. 그래도 최근에 업데이트 된 패키지가 걸리적거리는게 없을 것 같달까. 그러다 활발히 업데이트 중인 unplugin-icons를 찾았다. 이것은 SolidJS용 패키지가 아니었다. 아이콘셋도 아니었다. 거의 모든 아이콘셋을 거의 모든 프레임워크에서 사용할 수 있게 해주는 도구다. 이런 문물이 있었다니. 누가 만들었나 함 보자. 제작자는 Anthony Fu... 아아 또 그인가. 오늘도 비 React 웹 생태계엔 Anthony Fu의 은혜가 넘친다.

SolidJS로 앱 만들다가 아이콘셋이 필요해져서 패키지를 뒤져보는데, 마이너 생태계답게 마지막 업데이트가 삼사년 전인 패키지들만 나온다. 아이콘셋에 업데이트가 필요 없긴 하지. 그래도 최근에 업데이트 된 패키지가 걸리적거리는게 없을 것 같달까. 그러다 활발히 업데이트 중인 unplugin-icons를 찾았다. 이것은 SolidJS용 패키지가 아니었다. 아이콘셋도 아니었다. 거의 모든 아이콘셋을 거의 모든 프레임워크에서 사용할 수 있게 해주는 도구다. 이런 문물이 있었다니. 누가 만들었나 함 보자. 제작자는 Anthony Fu... 아아 또 그인가. 오늘도 비 React 웹 생태계엔 Anthony Fu의 은혜가 넘친다.

5
0
1