洪 民憙 (Hong Minhee)'s avatar

洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · 966 following · 1308 followers

An intersectionalist, feminist, and socialist guy 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)'s avatar
洪 民憙 (Hong Minhee)

@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)'s avatar
洪 民憙 (Hong Minhee)

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

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

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

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

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

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

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

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

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

wwj's avatar
wwj

@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)'s avatar
洪 民憙 (Hong Minhee)

@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)'s avatar
洪 民憙 (Hong Minhee)

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

Okay, it seems up now.

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

@hongminhee@hollo.social

Is gnu.org down or just me?

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

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

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

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

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

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

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

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

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

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

@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)'s avatar
洪 民憙 (Hong Minhee)

@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)'s avatar
洪 民憙 (Hong Minhee)

@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)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social · Reply to 洪 民憙 (Hong Minhee)'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)'s avatar
洪 民憙 (Hong Minhee)

@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)'s avatar
洪 民憙 (Hong Minhee)

@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)'s avatar
洪 民憙 (Hong Minhee)

@hongminhee@hollo.social

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

@hongminhee@hollo.social

I really want Kagi Translate API.

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

@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)'s avatar
洪 民憙 (Hong Minhee)

@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

오브젝티프's avatar
오브젝티프

@objectif@mitir.social

그렇습니다. 우울하지 않은 사람도 "우울할 때에는 상담하기"를 평소에 열심히 외워 둘 필요가 있습니다. 왜냐?

심리학에는 결핍의 덫(scarcity trap)이라는 개념이 있는데요. 사람은 시간이나 금전 등 어떤 자원이 결핍(scarce)되면, 심리적 압박을 받아 시야가 좁아집니다(tunnel vision). 이로 인해 올바른 결정과 실행을 못하게 됩니다. 그러면 자원의 결핍(scarcity)이 더 심해집니다.

이렇게 얘기하면 떠오르는 전형적인 예시가 있습니다. 열악한 노동 조건과 저임금에 시달리는 노동자가, 스트레스 때문에 퇴근 후 술이나 도박 등의 즉각적 쾌락에 돈과 시간과 건강을 다 탕진해 버리는 것이죠. 하지만, 높은 소득과 지위를 누리던 대기업 간부도 투신자살을 해서 충격을 주곤 합니다.

사람이 이 덫에 빠지게 되는 계기가 한두 가지가 아닙니다. 환경적 요인으로 인해 우울감이 발생하기도 하고, 반대로 우울감이 사회생활에 지장을 초래해 환경적 요인을 조성하기도 합니다. 그리고 어떤 식으로든 이 덫에 빠지면,

- 심리적 압박으로 시야가 좁아지고
- 그로 인해 어리석은 판단을 하게 되고
- 그 어리석은 판단으로 인해 더욱 궁지에 몰리고
- 심리적 압박이 더 커키고
- 더 어리석은 짓을 저지를 수가 있습니다.

이 악순환이 누적되면, 돈 많다는 사람들에게도, 똑똑하고 가방끈 길다는 사람들에게도, 얼마든지 비극이 일어나는 것입니다.

우울장애의 가장 큰 무서움이 이것입니다. 현대 사회가 개인에게 도움을 제공하는 모든 체계에는 전제가 있습니다. "개인이 적어도 이기적 동기는 잘 가지고 있을 것." 우울감이 지속되면 이 전제가 깨집니다. 스스로에게 이로운, 이기적인 판단조차 제대로 할 수 없게 됩니다.

그러니 "우울할 때에는 상담하기"를 기억합시다. 평소에 외워 두지 않으면, 우울할 때에 떠오르지 않습니다.

물론 한국은 우울장애에 대한 인지적 관점이 많이 부족한 사회입니다. 그러나 연락처 목록을 뒤져 보면 한두 명 정도는 믿고 이야기할 사람이 있을 것입니다.

주변 사람들을 못 믿겠다면, 일면식도 없는 전문가를 찾읍시다. 한국의 정신과 전문의나 상담심리사 등, 우울한 사람에게 도움을 줄 분들의 숙련도나 전문성은 뜻밖에도 전반적으로 뛰어난 편입니다. 믿고 도움을 청해 봅시다.


RE: https://gameguard.moe/notes/acyejg21pqcx00xl

게임가드's avatar
게임가드

@gameguard.moe@gameguard.moe

제발 힘들면 힘들다고 이야기해주십쇼
속에만 쌓아두다가 소울스트림 가지 마시고..
해결까지는 장담 못해드리지만 들어드릴 수는 있으니 저 뿐만이 아니라도 주변 믿을 수 있는 친구에게라도 이야기를 해주세요.
소울스트림으로 훌쩍 떠나버리시면 당신을 바라보던 남아있는 사람들도 많이 힘들어요.
제발.. 제발 부탁드립니다.

me's avatar
me

@me@changkyun.kim

새 글이 연합우주에 잘 배달되는지 확인합니다.

새 글이 연합우주에 잘 배달되는지 확인합니다.

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

@hongminhee@hollo.social

괴델과 美國(미국) 憲法(헌법)虛點(허점)

잇창명 EatChangmyeong💕🐱's avatar
잇창명 EatChangmyeong💕🐱

@eatch@hackers.pub

Laurens Hof님의 ‘Blueskyism’, Political Violence, and Open Social Networks Under Authoritarianism을 번역한 글로, 원본과 동일하게 CC BY-SA 4.0으로 배포합니다. 미국 정치와 관련된 오역이 있을 수 있습니다.

최근 개방 소셜 네트워크의 지형이 급격하게 변화하고 있다. 빅테크 플랫폼을 대체할 소셜 네트워크를 만든다는 것은 원래부터 근본적으로 정치적인 프로젝트였지만, 미국의 현 시국으로 인해 또 다른 차원의 위기감이 닥치고 있다. 중도 논객들은 블루스카이를 좌편향 플랫폼으로 규정하는 데 온 신경을 쏟고 있고, X 렉카들은 블루스카이 사용자들이 찰리 커크의 죽음을 희화화(celebration)한다는 날조된 내러티브를 확대재생산하며, 블루스카이를 포함해 모든 민주진보 플랫폼을 폐쇄하고 기소하라는 파시즘적인 목소리는 커져만 간다. 빅테크의 대체 플랫폼을 세우는 프로젝트는 미 의회의 검열 움직임과 앱 장터에서 블루스카이를 내리라는 요구 속에서 권위주의 미국과 충돌하고 있다.

블루스카이즘(Blueskyism)

미국의 정치 논객 네이트 실버는 9월 초에 블루스카이즘에 대해 다룬 적이 있다. 글은 블루스카이에서 사용자 활동이 감소하고 있다는 통계를 제시하면서 시작한다 (이는 사실이다). 실버는 블루스카이즘은 블루스카이 이전부터 이미 있었던 것이라는 논지로 글을 이어가고, 블루스카이즘을 정치 스펙트럼의 반대쪽에 있는 사람들을 비난하고, 학술적 권위를 중시하며, 현상에 지나치게 불만을 품는 태도로 설명하며 이를 거부한다.

블로거 겸 중도 논객인 노아 스미스 역시 실버의 이 글과 관련해 미국 좌익의 블루스카이화(The Bluesky-ization of the American left)라는 글을 작성했다. 2010년 후반에 바리 웨이스가 트위터에서 지나친 비판을 받은 것을 속상해하고, 자멜 부이가 본인을 '반사회적인 괴짜 루저'라고 지칭한 것에 대해 화내는 것이 글의 요지다.

포면적으로만 보면 두 글 모두 SNS에 상주하는 어떤 중도 논객이 본인이 블루스카이에서 욕을 먹고 있다고 화내는 내용에 불과하다. 이들은 블루스카이가 망해가는 SNS라는 그림을 그리고 있다. 두 글에서 언급하지 않는 사실이지만, 일단 실버는 학술계에서 블루스카이가 얼마나 중요한 위치를 차지하고 있는지를 공개적으로 인정한 바가 있다.

그러나 두 글이 작성된 진짜 목적은 따로 있다.

  • X 활동을 그만두는 것을 고민하는 사람들에게 블루스카이는 어차피 진짜 대안이 될 수 없다는 논리로 X에 남아도 된다는 도덕적 핑계를 제시한다.
  • 블루스카이는 민주진보 플랫폼이라는 인식이 있다는 인정 구조(permission structure)를 만들어낸다. 여기서 이 두 글을 언급한 이유가 이것이다. 개인적으로 글의 내용 자체도 옹졸하고 주제 역시 잘 이해하지 못하고 있다고 느껴지지만, 블루스카이가 '좌편향'된 플랫폼이라는 시각을 확산시키는 데 이는 중요하지 않다.

찰리 커크의 피살

찰리 커크 얘기는 지겹도록 들었을 테니, 핵심만 요약하겠다.

  • 블루스카이는 찰리 커크의 죽음을 희화화하는 플랫폼이라는 인식이 생겼으며, 특히 팀 어번의 "블루스카이에 올라오는 모든 글이 암살을 희화화하고 있다. 믿을 수 없이 역겨운 사람들이다."라는 글에 일론 머스크가 "냉혹한 살인을 축하하고 있는 것이다"라며 인용한 것이 2600만 조회수를 기록하면서 이런 관점의 확대재생산에 일조하고 있다.
  • 이에 대해 슬레이트는 블루스카이는 찰리 커크의 죽음을 희화화하고 있지 않다(No, Bluesky Isn't Celebrating the Death of Charlie Kirk)라는 반박 기사에서 [블루스카이] 플랫폼의 실상을 확인하고, 일론 머스크가 확산시킨 게시물들이 거짓부렁임을 밝혔다. 개방된 데이터가 바로 블루스카이의 강점이고, Catch Up과 같은 피드에서도 어제 하루 동안 플랫폼 전체에서 좋아요를 가장 많이 받은 게시물을 확인할 수 있다. 이와 같은 방식으로 플랫폼의 '시대정신'이 무엇인지 상당히 객관적으로 확인할 수 있으며, 인기 게시물 중 커크의 죽음을 희화화하는 것이 없었다는 것 역시 확인된다.
  • 커크는 합리적인 말을 하는 기독교 자기계발 스피커와 혐오로 점철된 스피커라는 사실상 두 얼굴을 가지고 있다. 듣기 좋은 말을 해주는 자기계발 스피커라는 커크의 거짓을 혐오자로서의 커크가 까발리기 때문에 후자는 존재할 수 없었다. 본인이 형성하고 싶은 거짓된 현실을 커크 본인의 발언이 위협하는 것이 본인의 세계관이라면, 커크의 피살을 심도 있게 다루는 것이 '희화화'처럼 보일 수 있겠다.
  • 머스크를 비롯한 파시스트들이 블루스카이를 왜 그렇게 싫어하는지도 두 얼굴의 커크로 설명할 수 있다. 블루스카이는 합리적인 말을 하는 자기계발 스피커 커크라는 거짓을 까발리는 발언을 허용하는 곳이다. 파시즘의 핵심은 지배이며, 자신의 권력을 이용해 거짓된 현실을 사람들에게 강요하는 것이다. 블루스카이와 같이 사람들이 이런 조작된 내러티브를 거부할 수 있는 개방 네트워크는 권위적 통제라는 계획 자체에 존재론적 위협이 된다.

모더레이션 문제

찰리 커크의 피살 직후 살인범에 대해 아무런 정보도 없었던 시점에 X에서는 '좌파'에 대한 비난이 시작되었고, 전쟁도 불사하겠다는 극단적인 주장이 생겨나기도 했다. 한편 블루스카이에서는 정치 지형이 바뀌었으니 앞으로의 행보에 유의해야 한다는 발빠른 인식을 가졌다.

블루스카이 CEO 제이 그레이버와 COO 로즈 왕 모두 정치적 폭력을 반대한다는 입장을 내세웠다. 그레이버가 블루스카이 CEO로서 정치 상황에 대해 입장을 낸 것은 이번이 처음으로, 이번 사건이 블루스카이라는 기업에 악영향을 미칠 수 있음을 분명히 알고 있었던 것이다.

블루스카이의 신뢰·안전팀에서는 모더레이션에 자사 규정을 엄격하게 적용하기 시작했으며, 과소 제재보다는 과잉 제재가 낫다는 방향을 잡은 듯하다. 이로 인해 블루스카이 측의 지나친 모더레이션으로 인해 계정이 부당하게 정지되었다고 호소하는 사례 역시 발생하고 있다. 개별적인 결정을 모두 평가하지는 않겠지만, 개인적으로 실제 현실만큼이나 현실에 대한 인식 역시 중요하다고 본다. 블루스카이의 모더레이션 결정이 옳았든 아니든, 사용자층 다수의 인식은 블루스카이의 제재가 잘못되었다는 쪽으로 기울게 된 것이다.

이 인식 변화에서 가장 주목할 만한 사례는 블루스카이가 커크의 피살에 대해 "rest in piss"(역자 주: '삼각 고인돌에 면봉을 비빕니다'에 빗댈 수 있는 표현)라고 작성한 사용자에게 24시간 계정 정지 조치를 시작했다는 것이다. 이 정책으로 인해 네이선 그레이슨 역시 피해를 입었으며, 애프터매스에 블루스카이 모더레이션의 현 주소를 분석한 기사를 기고하기도 했다. 이 기사에서는 어떤 글은 삭제하고, 어떤 낚시성 글은 무시하고, 일부 게시물이나 계정의 제재는 명확한 소통 없이 갑자기 취소되기도 하는 등 규정이 일관성 없이 적용되는 블루스카이 모더레이션의 혼란스러운 실상을 확인할 수 있다.

한편 CTO 폴 프레이지는 다른 주제에 관한 대화에서 성의 없이 "rest in piss 제재는 취소했음"이라고 언급한 것을 찾아볼 수 있다. 제재 조치에서 실수가 있었음을 공개적으로 밝힌 것이 CEO도 신뢰·안전팀의 팀장도 아닌 CTO라는 것 자체가 블루스카이 모더레이션의 무능을 방증한다. 제재가 취소된 사용자들에게 아무런 통보도 없었다는 것은 말할 필요도 없다.

블루스카이는 지금 난처한 상황에 처해 있다. 규정을 위반하는 게시물을 삭제하라는 정치적 압력이 그 어느 때보다도 높다. 일론 머스크를 비롯해 지구상에서 가장 큰 권력을 가진 자들이 블루스카이에서 커크의 죽음을 희화화하는 게시물을 찾아다니고, 찾아낸 게시물을 통해 블루스카이에 정치적 압박을 가하고 있다. 그레이버가 청문회에 소환되어 블루스카이 규정을 위반하고 우익 엘리트층에게도 불리한 게시물을 제재하지 않은 이유를 해명해야 할 위협 역시 실재한다. 이 맥락에서 Bluesky PBC가 자사 플랫폼을 과잉 제재하는 분위기로 흐른 것은 이해할 수 있다. 하지만 파시스트에 대한 유화책은 해결책이 되지 못한다. 머스크가 블루스카이에서 커크의 죽음을 희화화하는 인기 게시물을 더 이상 찾지 못하자 한 행동은 가짜 계정의 게시물임이 역력히 드러나는 재게시 0건, 좋아요 0건의 게시물 스크린샷을 공유하는 것이었다. 머스크는 스크린샷을 인용하면서 "맞서 싸우지 않으면 우리를 다 죽일 것"이라는 글을 남겼고, 이는 1000만 회 이상의 조회수를 기록하고 있다. 머스크는 사악한 블루스카이라는 본인의 내러티브를 계속 재생산하고 블루스카이는 사용자층에게서 신뢰를 잃었으니 결국 두 마리 토끼를 모두 놓친 셈이다.

정치적 여파

찰리 커크의 피살은 정계에 많은 영향을 미치고 있으며, 그 중 블루스카이와 관련이 있는 것을 나열하면 다음과 같다.

  • 대통령 비서실장 스티븐 밀러의 말대로 미국 정부는 커크의 피살을 정적을 탄압할 기회로 활용하고 있으며, 이는 블루스카이가 좌파와 민주당 지지자들[만]을 위한 플랫폼이라는 정치 논객들의 프레이밍 강화로 이어지는 악순환이 된다. 미국 정부가 블루스카이를 '좌편향' 플랫폼으로 인식할수록 블루스카이가 정치 탄압의 표적이 될 확률 역시 높아진다. 이 시나리오가 실제로 발생할 개연성이 얼마나 되는지에 대한 분석은 권위주의 정권에서의 정치적 음모를 잘 아는 전문가에게 맡기겠지만, 내가 보기에는 미국 정부와 이해관계가 일치하는 자들이 '좌편향' 플랫폼을 탄압하라며 압박을 가하고 있는 상황에 블루스카이를 '좌편향' 플랫폼으로 프레이밍하려는 노골적인 압력이 같이 있다는 것 자체가 특기할 만하다.

  • 보수 매체인 페더럴리스트는 한 발짝 더 나아가 민주적인 플랫폼을 탄압해야 한다고 주장하고 있다. 해당 기사에서는 "애플, 아마존, 구글은 2021년 국회의사당 습격에 연루되어 있다는 거짓 의혹을 받은 소셜 미디어 앱 Parler를 플랫폼에서 내렸지만, 디스코드와 블루스카이가 정치적 폭력을 희화화하고 장려하고 있다는 사실에는 침묵하고 있다. 빅테크 과점 체제에 법적 책임이라는 이름의 매가 필요할 때이다."라는 내용을 찾아볼 수 있다.

    페더럴리스트는 오늘날 정보 환경에서 권력의 하부 구조를 잘 이해하고 있다. 절대 다수의 사람들은 휴대폰에 설치된 앱을 통해 인터넷에 접속한다. 구글과 애플은 모든 사람들의 휴대폰에 설치될 수 있는 앱과 없는 앱을 통제함으로써 문고리 권력을 톡톡히 누리고 있다. 이 두 기업이 개방 소셜 웹에 접속하는 앱을 양대 스토어에서 거부한다면 블루스카이/AT 프로토콜뿐만 아니라 연합우주/ActivityPub 역시 막을 방법이 없다.

  • 하원 의원 클레이 히긴스는 메타, 유튜브, 틱톡, X, 트루스소셜, 블루스카이의 CEO에게 서한을 보내 "찰리 커크의 정치적 암살을 희화화하는 글 일체를 즉각 삭제할 것, 또한 작성자를 식별해 플랫폼에서 차단하고 새로운 페이지 생성 역시 제재할 것"을 요구하고, 위원회 소속으로서의 권한으로 해당 기업에 이행을 강제할 수 있다는 점 역시 들었다. 이 서한은 명백히 정부에 의한 합법적 표현 검열의 예시로 들 수 있으며, 한편으로는 미국 정부의 구성원이 블루스카이를 사용자의 표현의 자유를 제한해야 할 통제의 대상으로 보고 있다는 점 역시 확인할 수 있다.

회복탄력성이 있는 네트워크를 만들려면

블루스카이가 창립된 것은 빅테크 플랫폼의 역학과 이들의 행동이 사회에 악영향으로 돌아오는 현상이 엔시티피케이션(enshittification)이라는 단어로 설명이 잘 되던 때였다. 이 글에서 사용하는 엔시티피케이션은 코리 닥터로의 원래 설명 그대로 플랫폼이 과점 지위를 악용해 예측 가능한 패턴으로써 가치를 착취하는 것을 말하며, 사용자에게 인센티브를 주어 시장 점유율을 확보하고, 비즈니스 고객을 위해 사용자 경험을 저하시킨 뒤, 결국 네트워크 효과로 록인이 발생하면 확보한 모든 사용자를 쥐어짜는 세 단계로 이루어진다.

빅테크 플랫폼들이 그 어느 때보다도 큰 규모와 권력을 가지고 있지만, 급변하는 환경 속에서 빅테크의 권력은 권위주의 정부와 일치된 이해관계를 만들어냈다. 빅테크 플랫폼에서의 경험이 더 나빠진 요인 역시 여기서 나온다. 빅테크와 미국의 권위주의 정부의 이해관계가 일치하면서 어떤 표현이 허용되고 장려되는지의 경계를 바꾸었다. (가자지구의 집단학살을 다루는 글 등) 좌익으로 취급되는 이슈에 관한 대화는 알고리즘이 밀어내는 한편, 이민자에 대한 노골적인 혐오는 수익 창출에 전혀 문제가 되지 않는다.

이제 블루스카이는 두 가지 문제에 직면해 있다.

  • 블루스카이는 트위터와 비슷하지만 트위터의 고질적인 문제(특히 플랫폼 록인)를 해결한 플랫폼, 그리고 이를 가능케 하는 AT 프로토콜 모두가 핵심이다. 이는 웹사이트의 태그라인인 "소셜 미디어는 소수의 기업에서 통제하기에는 너무나도 중요합니다. [블루스카이는] 우리 모두가 소셜 미디어의 미래를 그릴 수 있도록 소셜 인터넷의 열린 기반을 만들어 나가고 있습니다."에서 특히 잘 드러난다. 블루스카이에서 프로토콜 개발의 중심에 놓은 '신뢰할 수 있는 출구'(credible exit)라는 개념 역시 여기에서 비롯된다. 원한다면 블루스카이를 떠나서 다른 플랫폼으로 갈 수 있어야 한다는 것이다. '출구' 부분은 프로토콜의 인프라 아키텍처로써 구현되며, 실제로 블루스카이를 떠나 다른 플랫폼으로 가는 것이 가능하다. 하지만 '신뢰' 부분은 더 어려운 문제다. 블루스카이는 프로토콜의 2위 플랫폼과 비교해 최소 4자리의 규모 차이가 있기 때문에 집단적인 수준에서는 딱히 신뢰할 수 없는 출구가 된다. 개인 차원의 탈출은 가능하지만, 나머지 생태계는 아직 블루스카이급의 사용자 수와 트래픽을 감당할 수 없다.
  • 엔시티피케이션과 이에 대한 방어책은 회사 측의 유해한 의사결정으로부터 사용자를 보호하는 것이며, 노골적으로 자유롭고 합법적인 표현을 검열하려고 하는 권위주의 정부에 대항할 수 있는 네트워크를 만드는 데는 다른 위협 모델이 필요하다. 하지만 블루스카이는 첫 번째 문제를 해결하는 데만 골몰해 있다. 웹사이트의 설명인 "대중에게는 생동하는 온라인 공간이 필요합니다. 우리는 결코 개인이나 기업에게 팔리지 않는 공간을 만드는 데 전념합니다."에서도 명백하게 블루스카이와 AT 프로토콜이 엔시티피케이션을 예방할 것이라는 점을 다루고, 플랫폼에 대한 정부의 간섭에 대한 언급은 전혀 없다. 트럼프가 재선에 성공한 뒤 9개월 동안 세상에 얼마나 빠르게 바뀌었는지를 생각하면 이 역시 이해할 수 있지만, 페더럴리스트의 주장을 받아들여서 양대 스토어에서 ATmosphere에 접속하는 앱을 내리게 하는 정부에 대항하려면 신뢰할 수 있는 출구를 제공하는 것이 아닌 다른 위협 모델이 필요하다.

2025년에 회복탄력성이 있는 네트워크를 만들려면 엔시티피케이션에 대항하는 것뿐만 아니라 권위주의에도 대항할 수 있는 아키텍처가 필요하다. 블루스카이가 자랑하는 '신뢰할 수 있는 출구'의 인프라는 프로토콜 안에서 다른 플랫폼으로 이동하는 것뿐만 아니라 개방 소셜 생태계 전체의 이해가 앱 장터 및 정부와 충돌하는 경우까지 고려해야 할 것이다. 권위주의 정부와 빅테크 과점 체제가 합심해서 정적의 공간을 파괴한다면, 기술적이든 사회적이든 솔루션은 이 새로운 위협 역시 막아내어야 한다. 유해한 경영 판단뿐만 아니라 조직적인 정치 탄압에도 살아남을 수 있는 인프라를 상상하고 구현할 수 있을지가 우리의 시험대이다. 지금 회복탄력성이 있는 소셜 네트워크를 만든다는 것은 '좌편향' 플랫폼으로 낙인찍힌 앱이 앱 장터에서 내려갈 수 있고, 개방 프로토콜을 유지관리하는 것이 저항 운동이 되는 미래에 대비하는 것이기도 하다.

Jihyeok Seo's avatar
Jihyeok Seo

@jihyeok@hackers.pub · Reply to Sebastian Jambor's post

@crepels Thank you so much for the effort. I've built ActivityPub support into https://oeee.cafe and https://typo.blue using https://activitypub.academy. It's been an invaluable resource for me and numerous other ActivityPub application developers!

Sebastian Jambor's avatar
Sebastian Jambor

@crepels@mastodon.social · Reply to Jihyeok Seo's post

@jihyeok Thanks for letting me know, and thanks for the offer of support! But money at the moment is not the issue. Unfortunately, I just don't have time at the moment to implement proper monitoring and making the system more resilient.
For now, I fixed it manually, so it should work again.

← Newer
Older →