npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@easbot/terminal

v0.3.24

Published

Terminal interaction toolkit for the EASBOT ecosystem — themed colors, ANSI utilities, OSC-8 hyperlinks, command formatting, table rendering, styled prompts, safe stream writes, and terminal-state recovery

Readme

English | 中文

@easbot/terminal

终端交互工具库 — 为 EASBOT 生态提供统一的终端输出、格式化、提示、Prompt 与状态恢复能力。

简介

@easbot/terminal 是一个面向 EASBOT 生态的终端交互工具库,封装了命令行 / TUI 场景下常用的能力:终端主题色、ANSI 处理、表格渲染、命令输出格式化、提示样式化、JSON 规范输出、人类输出统一封装(printHuman / renderLines)、一个统一的交互式 Prompt namespace、一个可搜索的多选交互 prompt、一个CLI 输入 helper、一个轻量级 markdown 渲染器、图标 / 状态符号统一常量、Loader 旋转动画、终端状态恢复等。所有能力都基于终端能力自动检测(NO_COLOR / FORCE_COLOR / TTY)进行降级,可在管道、文件重定向等非 TTY 场景下安全使用。

本包完全 vendor-neutral:不绑定任何外部文档站或 vendor-specific 配置。formatRootedLink / formatRootLink 等 helper 显式接受 root 参数,由调用方决定集成方式。

特性

  • 终端主题:基于 chalk + Lobster 调色板,提供统一的强调色 / 信息 / 成功 / 警告 / 错误等样式
  • 终端能力检测:自动识别 TTY、颜色等级、NO_COLOR / FORCE_COLOR,不支持富文本时自动降级为纯文本
  • ANSI 工具:剥离 SGR / OSC-8 序列,计算可见字符宽度,处理 Unicode / emoji 文本对齐
  • 路径显示:支持 ~ / $EASBOT_HOME 缩短用户主目录路径(resolveUserPath / shortenHomePath / displayPath
  • OSC-8 超链接:跨平台终端超链接,自动检测终端能力,提供降级文本
  • 表格渲染renderTable 支持自适应列宽、Unicode / ASCII / 无边框三种样式、文本换行、ANSI 样式保持
  • 命令输出格式化:成功 / 错误 / 警告 / 信息 / 标题 / 键值对 / 命令示例 / 列表项 / 进度状态 / 计数等
  • 提示样式化:纯样式化封装(selectStyled / note / stylePromptMessage / stylePromptTitle / stylePromptHint
  • 统一交互 Prompt namespacePrompt.select / groupMultiselect / text / password / confirm / multiselect / intro / outro / cancel / Log.* / spinner —— 业务方唯一交互入口(详见"Prompt namespace"节)
  • 可搜索多选 promptSearch.multiselect 是交互式、实时过滤的多选 prompt,支持完整键盘控制、可选锁定分区、对 stdin / 信号优雅清理
  • Loader 旋转动画Loader.create() 返回stateless spinner 实例(不继承 tui 组件模型),可定制帧 / 颜色 / 消息
  • Input CLI 输入Input.readText / Input.readConfirm / Input.readPassword 是 readline 薄包装的 CLI prompt;支持注入 input / output 用于测试隔离
  • Markdown 渲染renderMarkdown 解析极简 markdown 子集(标题 / 列表 / 代码块 / 加粗 / 斜体 / 链接),不引 marked,保持 CLI 包体小
  • 人类输出统一封装printHuman(lines, { title, outro }) 把分组细节 + 总结 + outro 收尾一页搞定,所有 CLI 子命令默认人类输出走这条
  • 图标 / 状态符号统一:所有 ✓ / ✗ / ⚠ / ℹ / ◆ / ● / ○ / │ / ─ / ┘ / • / ↡ / ⊘ 等图标硬编码仅出现在 symbols.ts,业务方通过 Symbol.statusIcon(...) / Symbol.colorize(...) 调用
  • JSON 规范输出emitJson / emitJsonOk / emitJsonError / wrapJson / wrapError 实现 CLI --json 规范的 { ok, data, meta? } / { ok, false, error: { code, message } } 协议
  • 安全流写入createSafeStreamWriter 自动处理 EPIPE / EIO,流关闭后静默忽略后续写入
  • 进度行管理:注册 / 清除 / 注销活动进度行,仅在 TTY 下生效
  • 终端状态恢复restoreTerminalState 在退出 / 异常时重置光标、鼠标跟踪、括号粘贴模式
  • 色彩一致性:所有样式化输出统一经过 isRich() + colorize(),快照 / 非 TTY 场景不会泄漏 ANSI 转义
  • TTY 注入:交互组件(formatTerminalLinkSearch.multiselectInput.readText 等)接受可选 isTTY 参数,单元测试不再依赖 process.stdout.isTTY

安装

pnpm add @easbot/terminal

快速开始

主题与终端能力

import { theme, isRich, colorize } from '@easbot/terminal';

const rich = isRich();
console.log(colorize(rich, theme.accent, '重要提示'));
console.log(colorize(rich, theme.success, '✓ 操作成功'));
console.log(colorize(rich, theme.error, '✗ 操作失败'));

命令输出格式化

import {
  formatSuccess,
  formatError,
  formatWarning,
  formatInfo,
  formatHeading,
  formatKeyValue,
  formatCommandExample,
  formatListItem,
  formatStatus,
} from '@easbot/terminal';

console.log(formatHeading('构建产物'));
console.log(formatKeyValue('入口', './dist/index.mjs'));
console.log(formatKeyValue('格式', 'esm / cjs'));
console.log(formatSuccess('构建完成'));
console.log(formatError('缺少必填参数 --name'));
console.log(formatWarning('检测到未提交变更'));
console.log(formatCommandExample('pnpm build', '构建所有包'));
console.log(formatListItem('◇', 'lint', 'biome check .'));
console.log(formatStatus('running', '正在执行构建...'));

路径与超链接

import { resolveUserPath, resolveConfigDir, displayPath, formatTerminalLink } from '@easbot/terminal';

const configDir = resolveConfigDir(); // ~/.config/easbot(如设了 EASBOT_STATE_DIR 则用之)
console.log(displayPath(configDir));

// TTY: OSC-8 链接;非 TTY: "Label (URL)" 降级
console.log(formatTerminalLink('EASBOT 仓库', 'https://github.com/houjallen/easbot'));

// 通用根域名 helper(vendor-neutral —— 调用方决定 root)
import { formatRootedLink } from '@easbot/terminal';
console.log(formatRootedLink('https://docs.example.com', '/guide', '快速开始'));

表格渲染

import { renderTable } from '@easbot/terminal';

console.log(
  renderTable({
    columns: [
      { key: 'name', header: '名称', align: 'left' },
      { key: 'version', header: '版本', align: 'left' },
      { key: 'status', header: '状态', align: 'center' },
    ],
    rows: [
      { name: '@easbot/terminal', version: '0.3.13', status: '✓' },
      { name: '@easbot/note', version: '0.3.13', status: '✓' },
    ],
    width: 80,
    border: 'unicode',
  }),
);

人类输出统一封装(printHuman)

import { printHuman, renderLines } from '@easbot/terminal';

// 业务侧常见的"分组细节 + 总结 + outro"一页输出
printHuman(
  ['  ✓ eas-agent-creation', '  ✓ eas-agent-evolution', '  ✗ eas-legacy-tool'],
  {
    title: 'Project Skills Update',
    outro: 'Run `easbot skills list` to see current state.',
    empty: 'No skills tracked in lock file.',
  },
);

// renderLines:构造 list / find 风格的分组树状
const lines = renderLines({
  title: 'Project Skills (3)',
  groups: [
    { key: 'universal (.agents/skills)', rows: ['  ✓ foo', '  ✓ bar'] },
    { key: 'claude-code', rows: ['  ✓ baz'] },
  ],
});

Prompt namespace(统一交互入口

Prompt@easbot/terminal 提供的统一交互 namespace —— 业务方唯一推荐的交互入口。内部按 B+A 混合方案封装:

  • select / groupMultiselect / text / password / confirm:薄包 @clack/prompts(多年踩坑调优过的 raw mode / 信号 / 取消)
  • multiselect:自实现,调用本包 Search.multiselect(fuzzy + 锁定分区 + 翻页)
  • intro / outro / cancel / Log.* / spinner:自实现(chalk + theme),统一配色到本包 theme
import { Prompt } from '@easbot/terminal';

// 单选
const choice = await Prompt.select({
  message: '选择安装范围',
  options: [
    { value: 'project', label: 'Project', hint: '当前目录' },
    { value: 'global', label: 'Global', hint: '主目录' },
  ],
});
if (Prompt.isCancel(choice)) Prompt.cancel('用户已取消');

// 多选
const selected = await Prompt.multiselect({
  message: '选择要启用的 Agent',
  options: [
    { value: 'claude-code', label: 'Claude Code' },
    { value: 'easbot', label: 'EASBot' },
  ],
  initialValues: ['easbot'], // 预选
  required: true,
});

// 分组多选
const skills = await Prompt.groupMultiselect({
  message: '选择要安装的技能',
  options: {
    General: [{ value: 'foo', label: 'foo' }, { value: 'bar', label: 'bar' }],
    'Advanced Plugins': [{ value: 'baz', label: 'baz' }],
  },
});

// 流程骨架
Prompt.intro(' skills ');
// ... do something ...
Prompt.outro(pc.green('Done!'));

// 取消:Prompt.cancel 是 `never`,内置 process.exit(0)
if (Prompt.isCancel(selected)) Prompt.cancel('用户已取消');

// 状态流(统一配色到 terminal theme)
Prompt.Log.info('操作成功');
Prompt.Log.warn('警告');
Prompt.Log.error('错误');
Prompt.Log.step('步骤');
Prompt.Log.message('普通消息(无前缀)');

// Spinner
const spinner = Prompt.spinner();
spinner.start('解析源...');
spinner.stop(`Found ${count} skill(s)`);

取消语义收口

  • Prompt.cancelSymbol —— 唯一 sentinel(与 Search.cancelSymbol 同引用)
  • Prompt.isCancel(value) —— 类型守卫,同时识别 Prompt.cancelSymbol 与 clack 的 Symbol('')
  • Prompt.cancel(message?) —— never 返回 + 内置 process.exit(0)

样式化提示(低阶辅助)

import { selectStyled, note, wrapNoteMessage } from '@easbot/terminal';

const choice = await selectStyled({
  message: '请选择要执行的操作',
  options: [
    { value: 'build', label: '构建', hint: '运行 pnpm build' },
    { value: 'test', label: '测试', hint: '运行 pnpm test:run' },
  ],
});

note(wrapNoteMessage('配置文件已成功保存到 ~/.config/app/config.json'), '成功');

图标 / 符号(统一硬编码源

业务方禁止直接写 ✓ ✗ ⚠ ℹ ◆ ● ○ │ ─ • 等字面量。所有图标必须通过 Symbol namespace 取得。

import { Symbol as Glyph } from '@easbot/terminal';

// 状态图标(自带主题色)
console.log(`${Glyph.statusIcon('success')} 操作成功`); // ✓ 绿色
console.log(`${Glyph.statusIcon('error')} 操作失败`); // ✗ 红色
console.log(`${Glyph.statusIcon('warning')} 警告`); // ⚠ 黄色
console.log(`${Glyph.statusIcon('info')} 提示`); // ℹ 蓝色
console.log(`${Glyph.statusIcon('active')} 进行中`); // ◆ 强调色
console.log(`${Glyph.statusIcon('skipped')} 已跳过`); // ↡ 灰色
console.log(`${Glyph.statusIcon('blocked')} 已拦截`); // ⊘ 红色

// 装饰条
console.log(Glyph.bar.vertical); // │
console.log(Glyph.bar.bullet); // •

// 任意字符套上主题色
console.log(Glyph.colorize('自定义字符', 'success'));

Loader 旋转动画

import { Loader, colorize, isRich, theme } from '@easbot/terminal';

const loader = Loader.create({
  message: '正在加载…',
  spinnerColor: (s) => colorize(isRich(), theme.accent, s),
  messageColor: (s) => colorize(isRich(), theme.muted, s),
});
loader.start();

// 每帧调用 line() 取出最新一行的文本
process.stdout.write(`\r${loader.line()}\x1b[K`);

loader.stop();
process.stdout.write('\n');

Loader 是一个 namespace(stateless 工厂),不依赖 tui 组件模型 —— 返回一个普通对象,方法:start / stop / isRunning / line / setMessage / setIndicator。默认:10 帧 Braille spinner,80 ms 间隔。

Input CLI 输入

import { Input } from '@easbot/terminal';

const name = await Input.readText({ message: '请输入名称:' });
const ok = await Input.readConfirm({ message: '继续吗?' });
const pw = await Input.readPassword({ message: '密码:' });

Input 是一个 namespace,含 3 个 stateless helper。测试时通过 input / output 注入 mock stream(使用 __tests__/helpers/mock-terminal.tsMockTerminalIO)。

Markdown 渲染

import { renderMarkdown } from '@easbot/terminal';

console.log(renderMarkdown('# 标题\n\n这是一段 **加粗** 文本。').join('\n'));

支持 # / ## / ### 标题、- / * / 1. 列表、围栏代码块、行内 **加粗** / *斜体* / `code` / [文本](链接)不依赖 marked,保持 CLI 包体小。

可搜索多选(Search.multiselect

import { Search, cancelSymbol } from '@easbot/terminal';

const result = await Search.multiselect({
  message: '请选择要启用的 Agent:',
  items: [
    { value: 'a', label: 'Agent A', hint: '通用任务' },
    { value: 'b', label: 'Agent B', hint: '代码导向' },
  ],
  required: true,
  lockedSection: {
    title: '始终包含',
    items: [{ value: 'core', label: '核心 Agent' }],
    hiddenCount: 2,
  },
  // 测试 / 嵌入式场景可显式注入 TTY 状态:
  // isTTY: true  // 强制启用 raw mode + 颜色
  // isTTY: false // 强制禁用 raw mode 与颜色
});

if (result === cancelSymbol) return;
// result 为已选项的值数组(type T[])

键盘约定:↑↓ ←→ 单步移动 · PageUp/Down 翻页 · Home/End 跳首末 · Space 切换 · Backspace 删除搜索字符 · Enter 提交 · Esc / Ctrl+C 取消。

安全流写入与状态恢复

import { createSafeStreamWriter, restoreTerminalState, registerActiveProgressLine } from '@easbot/terminal';

const writer = createSafeStreamWriter();
registerActiveProgressLine(process.stdout);

process.on('exit', () => restoreTerminalState('process exit'));
process.on('SIGINT', () => restoreTerminalState('SIGINT', { exit: true }));

// 流断开(如管道关闭)后再次写入不会抛 EPIPE
writer.write('first\n');
writer.write('second\n');

CLI --json 规范输出

import { emitJson, emitJsonOk, emitJsonError, wrapJson, wrapError } from '@easbot/terminal';

// 低阶:构造 envelope
const env = wrapJson({ items: [{ id: 1 }] }, { count: 1 });
// { ok: true, data: { items: [{ id: 1 }] }, meta: { count: 1 } }
const err = wrapError({ reason: 'not found' });
// { ok: false, data: { reason: 'not found' } }

// 高阶:直接 emit 到 stdout
emitJsonOk({ items: [{ id: 1 }, { id: 2 }] }, { count: 2 });
// stdout: { "ok": true, "data": { "items": [...] }, "meta": { "count": 2 } }

emitJsonError('E_NOT_FOUND', '资源不存在');
// stdout: { "ok": false, "error": { "code": "E_NOT_FOUND", "message": "资源不存在" } }

API 总览

主题与样式

  • theme — 主题颜色对象(accent / accentBright / accentDim / info / success / warn / error / muted / heading / command / option
  • isRich() — 判断终端是否支持富文本颜色
  • colorize(rich, color, value) — 条件着色工具
  • LOBSTER_PALETTE — Lobster 调色板原始色值(8 种颜色,as const 锁定)

ANSI 工具

  • stripAnsi(input) — 剥离 ANSI SGR / OSC-8 序列,返回纯文本
  • visibleWidth(input) — 计算剥离 ANSI 后的可见字符宽度

路径与显示

  • resolveUserPath(input) — 解析用户路径(支持 ~ 波浪号)
  • resolveConfigDir(env?, homedir?) — 解析配置目录(可被 EASBOT_STATE_DIR 覆盖;默认 ~/.config/easbot
  • resolveHomeDir() — 解析用户主目录
  • shortenHomePath(input) / displayPath(input) — 缩短路径中的主目录部分
  • shortenHomeInString(input) / displayString(input) — 替换字符串中所有匹配的主目录路径
  • resolveEffectiveHomeDir / resolveRequiredHomeDir / expandHomePrefix — 主目录解析工具
  • formatTerminalLink(label, url, opts?, isTTY?) — 格式化 OSC-8 终端超链接,支持 isTTY 注入
  • formatRootedLink(root, path, label?, opts?, isTTY?) — 通用根 URL 链接 helper(vendor-neutral)
  • formatRootLink(root, label?, isTTY?) — 通用根 URL 单链接

命令输出格式化

  • formatSuccess / formatError / formatWarning / formatInfo — 带图标的前缀消息
  • formatHeading / formatKeyValue / formatCommandExample / formatListItem — 章节与列表
  • formatStatus / formatSeparator / formatEmptyMessage / formatCount — 进度与计数

表格

  • TableColumn / RenderTableOptions — 表格列配置与渲染选项类型
  • renderTable(opts) — 渲染表格字符串

人类输出统一封装

  • PrintHumanOptions / RenderLinesGroup / RenderLinesOptions — 类型
  • printHuman(lines, opts)lines + title + outro 一页输出(boxed 走 note()
  • formatPrintHuman(lines, opts) — 返回字符串而非直接打印(用于嵌入更大输出)
  • renderLines(opts) — 构造分组树状行(list / find / store list 风格)
  • printRenderLines(opts) — 同上但直接打印
  • formatListTruncated(items, maxShow?) / formatList(items, maxShow?) — 截断列表(≤N 全显 + "N more" 提示)

提示样式化(低阶)

  • stylePromptMessage / stylePromptTitle / stylePromptHint — 提示文本样式化
  • selectStyled<T>(params) — 样式化的选择提示
  • note(message, title?, options?) — 样式化的注释框(支持 columns / maxWidth 注入)
  • wrapNoteMessage(message, options?) — 注释消息自动换行

Prompt namespace(统一交互入口)

业务方唯一推荐的交互入口。细节见上文"Prompt namespace"节:

  • Prompt.select<T>(opts) — 单选
  • Prompt.groupMultiselect<T>(opts) — 分组多选
  • Prompt.multiselect<T>(opts) — 搜索多选(带 fuzzy + 锁定分区 + 翻页)
  • Prompt.text(opts) — 文本输入
  • Prompt.password(opts) — 隐藏输入
  • Prompt.confirm(opts) — y/n 确认
  • Prompt.intro(title) — 流程开篇(heading + 装饰线)
  • Prompt.outro(message?, { success? }) — 流程收尾(success/error 主题色)
  • Prompt.cancel(message?)never,内置 process.exit(0)
  • Prompt.cancelSymbol — 唯一 sentinel(与 Search.cancelSymbol 同引用)
  • Prompt.isCancel(value) — 类型守卫
  • Prompt.spinner(opts?) — Loader.create 薄包装,返回 SpinnerInstance
  • Prompt.SpinnerInstance{ start, stop, update, line }
  • Prompt.Log.info / warn / error / success / step / message(msg, detail?) — 状态流

图标 / 符号(统一硬编码源)

  • Symbol namespace:
    • Symbol.icons: Record<SymbolKind, string> — 状态图标裸字符(success/error/warning/info/accent/muted/heading/active/submit/cancel/locked/pending/radioActive/radioInactive/skipped/blocked/cursor
    • Symbol.bar.{ vertical, horizontal, cornerBottom, cornerBottomRight, teeRight, stepActive, bullet }
    • Symbol.colorize(char, kind) — 任意字符套上主题色
    • Symbol.statusIcon(kind) — 返回 Symbol.icons[kind] + 主题色
    • Symbol.prefixed(kind, text)"<icon> <text>" 形式前缀
  • SymbolKind 类型

Loader 旋转动画

Loadernamespace,提供单一工厂方法:

  • Loader.create(options?)Loader.Instance
    • options.message? — 主文本(默认 'Loading...'
    • options.spinnerColor? — 帧字符颜色函数(默认 colorize(isRich(), theme.accent, ...)
    • options.messageColor? — 消息颜色函数(默认 colorize(isRich(), theme.muted, ...)
    • options.indicator?{ frames?: string[]; intervalMs?: number }(默认 10 帧 Braille / 80 ms)
  • Loader.Instance
    • start() — 幂等(重启 interval)
    • stop() — 幂等
    • isRunning() — 当前动画状态
    • line() — 当前渲染行('帧 消息'
    • setMessage(message) — 更新文本
    • setIndicator(indicator?) — 更新帧 / 间隔;自动重启

Input CLI 输入

Inputnamespace,含 3 个 stateless helper。测试时通过 input / output 注入 mock stream。

  • Input.readText(options) — 读单行。Options: message / hint? / defaultValue? / hidden? / messageColor? / hintColor? / input? / output?
  • Input.readConfirm(options) — y/n 确认,无效输入会递归重试。Options: message / hint? / defaultValue?(默认 true)/ messageColor? / hintColor? / input? / output?
  • Input.readPassword(options)readTexthidden: true 别名

Markdown 渲染

  • renderMarkdown(md, opts?)string[](每行一段)
    • 支持:# / ## / ### 标题、--- 分隔线、- / * / 1. 列表、围栏代码块(```)、行内 **加粗** / *斜体* / `code` / [文本](链接)
    • opts.headingColor? / boldColor? / italicColor? / codeColor? / linkColor? — 覆盖默认主题的颜色函数

JSON 规范输出

  • wrapJson<T>(data, meta?, ok?)JsonEnvelope<T>(构造 envelope;ok 默认 true)
  • wrapError<T>(data, meta?)JsonEnvelope<T>(ok:false 错误态)
  • emitJson(envelope, indent?) → 序列化 + console.log
  • emitCommandJson(envelope) — 同上固定 indent=2
  • emitJsonOk(data, meta?) — 直接 emit { ok: true, data, meta? }
  • emitJsonError(code, message) — 直接 emit { ok: false, error: { code, message } }
  • JsonEnvelope<T>{ ok: boolean; data: T; meta?: Record<string, unknown> }

流与状态

  • SafeStreamWriterOptions / SafeStreamWriter — 安全流写入器类型
  • createSafeStreamWriter(options?) — 创建安全流写入器,自动处理 EPIPE / EIO
  • registerActiveProgressLine(stream) / clearActiveProgressLine() / unregisterActiveProgressLine(stream?) — 进度行管理
  • restoreTerminalState(reason?, options?) — 恢复终端状态(光标 / 鼠标 / 括号粘贴模式)
  • RestoreTerminalStateOptions{ resumeStdin? | resumeStdinIfPaused? }(默认 false;Docker TTY 行为)

其它

  • styleHealthChannelLine(line, rich) — 健康检查通道行样式化
  • StatusKind'pending' | 'running' | 'success' | 'error' | 'skipped'

环境变量

| 变量 | 作用 | |------|------| | NO_COLOR | 存在时禁用终端颜色(除非同时设置了 FORCE_COLOR),遵循 no-color.org 规范 | | FORCE_COLOR | 非空且不为 '0' 时强制启用颜色 | | HOME / USERPROFILE | 主目录解析的备选来源(OS 默认值) | | EASBOT_TEST_HOME | 测试专用 最高优先级的主目录覆盖(详见 home-dir.ts) | | EASBOT_HOME | 显式指定主目录;设置后路径显示前缀由 ~ 切换为 $EASBOT_HOME | | EASBOT_STATE_DIR | 配置目录覆盖项 |

主目录解析顺序(高 → 低)EASBOT_TEST_HOMEEASBOT_HOMEHOMEUSERPROFILEos.homedir()

开发

# 安装依赖
pnpm install

# 构建
pnpm build

# 测试
pnpm test:run

# 类型检查
pnpm type-check

# Lint / Format
pnpm lint
pnpm format:fix

发布

# Windows
powershell -ExecutionPolicy Bypass -File scripts/publish.ps1

# Unix/macOS
bash scripts/publish.sh

许可证

MIT