This commit is contained in:
gzkoala
2026-03-07 16:28:17 +08:00
352 changed files with 33316 additions and 19 deletions

91
CHANGELOG.md Normal file
View File

@@ -0,0 +1,91 @@
<!--
---
title: Project Caffeine Changelog (更新日志)
description: 记录 Project Caffeine 项目的所有显著更改、版本迭代与发布历史,遵循 Keep a Changelog 与语义化版本规范。
type: Changelog
version: v0.1.1 (Arabica) - Sprint 2
file: CHANGELOG.md
author: Gitconomy Research-郭晧
date: 2026-03-07
tags:
- Project Caffeine
- Changelog
- Release Notes
- Version Control
license: CC BY-SA 4.0
status: Active
---
-->
# Changelog (更新日志)
本项目的所有显著更改都将记录在此文件中。
本项目遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 规范,并且项目版本号采用 [Semantic Versioning (语义化版本)](https://semver.org/lang/zh-CN/)。
---
## [0.1.1] - 2026-03-06
### Added (新增)
- **接入 MCP Prompts 原语**:新增 `prompts/list``prompts/get` 接口,向大模型暴露静态思维框架模板,支持降低前置上下文长度。
- **多维静态思维框架库**:在 `src/models/frameworks/` 目录下新增 `5W3H``SCQA``SWOT``PESTLE` 等基于 JSON 格式的静态思维框架模板。
- **新增意图拆解工具**:开发 `generate_search_queries` 工具,支持将用户模糊的自然语言查询自动拆解为 3-5 个专业检索词,为后续文献检索提供广度解析。
- **输入参数严格校验**:在 `schemas.ts` 中基于 Zod 新增针对 `generate_search_queries` 工具及 Prompts 原语的强类型参数校验规则。
- **底层角色矩阵与输出规范**:建立多智能体角色矩阵 (Persona Matrix) 雏形,通过系统消息 (System Prompt) 及 Few-Shot 示例,**强制约束大模型输出标准的 Markdown 格式报告**。
- **测试与质量保障体系**制定《Project Caffeine 项目测试规范指南》与《MCP Inspector 使用说明文档》,确立包含单元测试、协议集成、负载性能与安全审计的四级自动化测试体系。
### Changed (变更)
- **重构提示词服务**:将 `promptService.ts` 升级为多框架管理器,支持从本地静态 JSON 文件中动态加载思维框架库。
- **扩展工具控制器**:更新 `toolsController.ts`,新增对意图拆解服务的路由分发能力。
- **优化构建脚本**:在 `package.json``build` 脚本中引入跨平台构建工具 `copyfiles`,以确保在执行 `tsc` 编译时,静态 JSON 文件能够自动同步至 `dist/models/frameworks/` 目录 _(依据历史对话)_
### Deprecated (废弃)
_(无)_
### Removed (移除)
_(无)_
### Fixed (修复)
- **静态资源编译丢失问题**:修复了因 TypeScript 原生编译器 (`tsc`) 不拷贝非 `.ts` 文件,导致运行时大模型发起 `prompts/get` 请求时抛出 `MCP error -32603: 获取框架失败` 的问题 _(依据历史对话)_
### Security (安全)
- **非法参数防注入**:通过引入 Zod 模型层校验,在服务端自动拦截因客户端大模型未正确生成必填参数(如 SCQA 框架缺失 `situation` 字段)而导致的无效负载,并标准抛出 `JSON-RPC -32602` 错误机制。
---
## [0.1.0] - 2026-03-03
### Added (新增)
- **初始化本地基础设施**:基于 Node.js (v18+) 和 TypeScript 搭建底层架构,配置主入口 `src/app.ts` 实例化官方 MCP SDK。
- **零网络开销通信**:实现基于 `stdio` (标准输入输出) 传输层的本地环境工作流,支持 Cherry Studio 无缝挂载。
- **单点提示词策略引擎**:开发纯本地业务逻辑 `promptService.ts`,向客户端注册 `generate_5_whys` 工具,支持将查询主题拆解为 5 Whys 多层追问。
- **本地知识库集成**:内置数据适配器,暴露 `list_local_notes``read_local_note` 两个核心工具,支持大模型直接读取本地 Obsidian (.md) 文件夹内容。
- **知识库资源暴露**:新增被动资源读取协议 `obsidian-index` (`obsidian://vault/index`),向客户端暴露本地知识库的完整目录结构。
- **源码级联调环境**:配置 `tsconfig.json` 生成 `sourceMap`,并在 `.vscode/launch.json` 中配置 `--inspect=9229` 端口映射,**实现基于底层 Node 进程的断点与日志监控**。
### Changed (变更)
_(无)_
### Deprecated (废弃)
_(无)_
### Removed (移除)
_(无)_
### Fixed (修复)
_(无)_
### Security (安全)
- **沙箱隔离与越权防御**:在 `read_local_note` 本地资源服务中实现**严格的路径防穿越Path Traversal安全校验**,将 AI 生成的指令视为不可信负载,直接拦截并阻断读取指定工作目录之外的恶意文件请求。

View File

@@ -84,7 +84,7 @@ status: "Active"
| **版本** | **开发目标** | **设计文档** | 开发文档 |
| ---------------------------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| [`v0.1.0`](./projects/arabica/src/sprint1/README.md) | 部署基于 Node.js 和 Express.js 的轻量级服务,验证 MCP 协议组件间通讯、大语言模型推理等基本运行环境。<br> | [Arabicat Sprint1系统设计文档](./projects/arabica/docs/design/arabica-sprint1-architecture-specification.md) | [Arabicat Sprint1系统开发文档](./projects/arabica/docs/design/arabica-sprint1-development-specification.md) |
| [`v0.1.0`](./projects/arabica/src/sprint1/README.md) | 部署基于 Node.js 开发环境,验证 MCP 协议组件间通讯、大语言模型推理等基本运行环境。<br> | [Arabicat Sprint1系统设计文档](./projects/arabica/docs/design/arabica-sprint1-architecture-specification.md) | [Arabicat Sprint1系统开发文档](./projects/arabica/docs/design/arabica-sprint1-development-specification.md) |
| [`v0.1.1`](./projects/arabica/src/sprint2/README.md) | 基于 Sprint 1 原型,扩展为支持 MCP Prompts 原语的多框架引擎,实现意图拆解工具与本地知识库集成,构建模块化、可扩展的提示词策略服务器。 | [Arabica Sprint2系统设计文档](./projects/arabica/docs/design/arabica-sprint2-architecture-specification.md) | [Arabica Sprint2系统开发文档](./projects/arabica/docs/design/arabica-sprint2-devekopment-specification.md) |
---

View File

@@ -0,0 +1,254 @@
<!--
---
title: Project Caffeine 项目代码测试规范指南
description: 为保证 Project Caffeine 提示词策略 MCP Server 的高可用性与健壮性制定的代码测试规范,涵盖单元测试、集成测试及相关最佳实践。
type: Testing Guide
version: v1.0.0
file: project-caffeine-code-testing-specification-guide.md
author: Gitconomy Research-郭晧
date: 2026-03-07
tags:
- Project Caffeine
- Testing
- Jest
- MCP Server
- Quality Assurance
license: CC BY-SA 4.0
status: Active
---
-->
# Project Caffeine 项目代码测试规范指南
为保证 Project Caffeine 提示词策略 MCP Server 的高可用性、健壮性及代码质量,特制定本代码测试规范。本指南主要适用于开发阶段的自动化测试(单元测试与集成测试)及相关最佳实践。
## 1. 测试工具栈建议
本项目推荐使用 **Jest** 作为核心测试框架,搭配 `ts-jest` 无缝支持 TypeScript 原生测试。
- **测试框架**: `jest`, `@types/jest`
- **TypeScript 支持**: `ts-jest`
- **Mock 工具**: Jest 内置 Mock 功能 (`jest.mock()`)
- **手动/交互式测试**: MCP Inspector
_(如尚未安装,可通过 `npm install --save-dev jest ts-jest @types/jest` 安装,并使用 `npx ts-jest config:init` 初始化配置。)_
```bash
npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init
```
---
## 2. 测试分层策略
基于项目的经典三层架构(接入层 `app.ts` -> 控制层 `controllers` -> 服务层 `services`),测试策略分为以下三个层级:
### 2.1 服务层单元测试 (Service Unit Tests)
- **目标**: 验证纯业务逻辑的正确性,这是测试的重中之重。
- **重点对象 (以 v0.1.1 为例)**: `intentService.ts` (意图拆解算法), `resourceService.ts` (文件操作), `promptService.ts` (JSON 解析与组装)。
- **策略**: 对于纯函数(例如 v0.1.1 中的 `generateSearchQueries`),提供不同的输入(边界值、空值、正常值)验证输出;对于涉及文件系统的操作(例如 `readObsidianNote`**必须使用 Mock** 拦截原生 `fs` 调用,严禁在单元测试中真实读写物理磁盘。
### 2.2 控制层单元测试 (Controller Unit Tests)
- **目标**: 验证参数的 Zod 校验逻辑以及请求路由分发的正确性。
- **重点对象 (以 v0.1.1 为例)**: `promptsController.ts`, `toolsController.ts`
- **策略**: Mock 掉底层的 Service 函数。重点测试:当传入非法参数时(如不带 `.md` 后缀的文件名Zod Schema 是否能正确拦截并返回 `isError: true` 和标准的 MCP 错误响应格式。
### 2.3 集成/E2E测试 (Integration Tests)
- **目标**: 验证 MCP Server 与客户端之间的 STDIO 协议通信及功能全链路。
- **策略**: 自动化层面投入较少,主要依赖 **MCP Inspector** 进行人工或半自动化点检。
---
## 3. 测试文件命名与目录结构
- **目录位置**: 测试文件应与被测试的源码文件同级,统一放在同级的 `__tests__` 文件夹中,或直接与源码文件同级。
- **命名规范**: 以 `.test.ts``.spec.ts` 结尾。
- 例如(以 v0.1.1 为例):被测文件 `src/services/intentService.ts`,测试文件应为 `src/services/__tests__/intentService.test.ts`
---
## 4. 单元测试编写规范 (编写范例)
### 4.1 纯粹逻辑的测试 (无副作用)
针对 `intentService.ts` 中的 `generateSearchQueries` 函数,采用 **Given-When-Then** (假设-当-那么) 模式或清晰的用例描述:
```
// src/services/__tests__/intentService.test.ts
import { generateSearchQueries } from '../intentService';
describe('intentService -> generateSearchQueries', () => {
it('当输入常规查询时应该正确分词并返回3-5个检索词', () => {
const result = generateSearchQueries('新能源汽车电池回收技术');
expect(result.length).toBeGreaterThanOrEqual(3);
expect(result.length).toBeLessThanOrEqual(5);
expect(result).toContain('新能源汽车电池回收技术 相关研究'); // 验证补全逻辑
});
it('当输入为空字符串时,应该返回默认的后备检索词', () => {
const result = generateSearchQueries(' ');
expect(result).toEqual(['通用研究主题']);
});
it('当输入带有大量标点符号时,应该正确清洗', () => {
const result = generateSearchQueries('AI芯片市场趋势2026;');
// 验证标点符号是否被正确视为空格分割
expect(result).toContain('AI芯片');
expect(result).toContain('市场趋势');
});
});
```
### 4.2 依赖外部系统(文件系统 IO的 Mock 测试
针对 `resourceService.ts`,绝不允许污染真实的 `OBSIDIAN_VAULT_PATH`
```
// src/services/__tests__/resourceService.test.ts
import { readObsidianNote, saveNote } from '../resourceService';
import fs from 'fs/promises';
import path from 'path';
// 全局 Mock fs 模块
jest.mock('fs/promises');
describe('resourceService', () => {
beforeEach(() => {
jest.clearAllMocks(); // 每个用例前清除 mock 状态
});
describe('readObsidianNote', () => {
it('当发生路径遍历攻击时 (../),应该抛出安全警告', async () => {
await expect(readObsidianNote('../../etc/passwd')).rejects.toThrow('安全警告:越权访问拦截!');
});
it('当读取合法路径时,应该返回文件内容', async () => {
// 模拟 fs.readFile 返回成功
(fs.readFile as jest.Mock).mockResolvedValueOnce('# Mock Note Content');
const content = await readObsidianNote('test.md');
expect(content).toBe('# Mock Note Content');
expect(fs.readFile).toHaveBeenCalledTimes(1);
});
});
});
```
### 4.3 控制层的数据校验 (Zod Schema) 测试
针对 `toolsController.ts`,验证 Zod 的拦截机制与 MCP 响应格式是否统一:
```
// src/controllers/__tests__/toolsController.test.ts
import { handleToolCall } from '../toolsController';
import * as resourceService from '../../services/resourceService';
jest.mock('../../services/resourceService');
describe('toolsController -> handleToolCall', () => {
it('当调用 save_note 且文件名缺失 .md 后缀时,应该返回 Zod 拦截的错误响应', async () => {
const params = { filename: 'invalidName', content: 'test' };
const result = await handleToolCall('save_note', params);
expect(result.isError).toBe(true);
expect(result.content[0].type).toBe('text');
expect(result.content[0].text).toContain('文件名必须以 .md 结尾'); // 验证 Zod 自定义错误消息
});
it('当调用未知的工具名时,应该返回未知工具的错误响应', async () => {
const result = await handleToolCall('unknown_tool', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具: unknown_tool');
});
});
```
---
## 5. 测试覆盖率标准
为了保障核心功能的稳定性,项目 CI/CD 流程中应设置覆盖率门槛:
- **Service 层**: 语句覆盖率 (Statements) 不低于 **85%**
- **Controller 层**: 分支覆盖率 (Branches) 不低于 **80%**
- 配置命令:`jest --coverage`
---
## 6. 测试编写的红线规定
1. **禁止真实 I/O**: 单元测试中严禁发起真实的磁盘读写或网络请求。必须使用 Mock。
2. **独立性**: 每个 `it` 用例必须相互独立。禁止用例 A 的运行结果作为用例 B 的依赖。必须善用 `beforeEach``afterEach` 清理状态(例如 `jest.clearAllMocks()`)。
3. **断言明确**: 不要只断言 `expect(result).toBeDefined()`。必须断言具体的数据结构或内容,例如 MCP 要求的 `content: [{ type: "text", text: "..." }]` 结构。
4. **涵盖异常流**: 测试不仅要覆盖“Happy Path”快乐路径即正常执行的流程**必须**编写针对 `throw Error` 和 Zod `isError: true` 的异常分支测试Unhappy Path
**附注**: 编写完测试后,建议将 `npm run test``npm run test:coverage` 配置入 `package.json``scripts` 中,以便日常开发与构建流集成。
---
## 7. 测试执行与查看指引
项目的 `package.json` 中已集成了标准化的测试脚本指令,开发者可以直接在终端使用以下命令执行测试:
### 7.1 运行所有测试
```bash
npm run test
```
- **功能说明**Jest 会自动全局扫描您的项目,找到所有符合命名规范的测试文件(如 `*.test.ts``*.spec.ts`),并串行/并行执行其中的所有用例。
- **输出查看**:终端会实时输出每个用例的测试结果,绿色的 `PASS` 表示通过,红色的 `FAIL` 表示失败及具体的报错堆栈。
### 7.2 运行测试并生成代码覆盖率报告
```bash
npm run test:coverage
```
- **功能说明**:在跑完所有测试用例的同时,额外收集代码被测试用例“触碰”的情况,借此衡量测试的完备度。
- **输出查看**
- **终端报表**:在控制台底部会输出一张表格,直观展示当前项目的“语句 (Stmts)”、“分支 (Branch)”、“函数 (Funcs)”和“行 (Lines)”覆盖率比例。
- **网页视图**:命令执行完毕后,项目根目录会自动生成一个 `coverage/` 文件夹。您可以通过浏览器打开 `coverage/lcov-report/index.html`,以直观的界面逐行查看哪些代码片段处于“漏测”状态。
### 7.3 运行指定文件的测试
如果您正在专注开发某个模块(如意图拆解服务),不需要每次都全量执行测试,可在命令后追加关键词或文件名进行过滤:
```bash
npm run test -- intentService
```
- **功能说明**Jest 将仅匹配文件名中包含 `intentService` 的测试文件并执行,从而大幅提高 TDD测试驱动开发环节下的反馈效率。
---
## 许可声明
本文档采用 **知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.

View File

@@ -0,0 +1,169 @@
<!--
---
title: "Project Caffeine 代码编写规范指南"
description: "为 Project Caffeine 项目开发人员提供一致的编码风格、系统架构指引和最佳实践的规范文档"
type: "Guide"
version: "v0.1.0"
file: project-caffeine-coding-specification-guidelines.md
author: "Gitconomy Research-郭晧"
date: 2026-03-02
tags:
- Project Caffeine
- 代码规范
- TypeScript
- MCP
- Node.js
license: "CC BY-SA 4.0"
status: "Active"
---
-->
# Project Caffeine 代码编写指南
## 1. 目的与概述
本代码编写规范旨在为 **Project Caffeine** 项目的开发人员提供一致的编码风格和最佳实践,确保代码的可读性、可维护性和团队协作的效率。遵循这些规范将有助于提升代码质量、减少错误并优化开发过程。
---
## 2. 技术栈与工程化基础
所有代码必须遵循清晰、简洁、结构化的原则,并基于以下核心技术栈构建:
- **核心语言**:所有核心功能必须使用 TypeScript 编写。强烈建议优先使用 TypeScript以利用其静态类型检查减少运行时错误。
- **运行环境**:服务端必须使用 Node.js (LTS v20+)。需确保高效的异步执行和非阻塞 I/O 操作。
- **工程架构**:采用原生 npm Workspaces 进行 Monorepo单体仓库包管理。必须在根目录统一管控共享的 JSON-RPC Schema 与多个微服务子包。
---
## 3. 代码风格与命名规则
## 3.1 格式与排版
- **缩进**:使用 **2 个空格**作为缩进(严禁使用 Tab
- **行长度**:每行代码的字符数应不超过 **120 个字符**,避免横向滚动条,便于阅读和维护。
- **文件结尾**:每个文件的结尾必须保留一个空行。
- **函数复杂度**:长函数或复杂逻辑必须拆分为多个函数,确保每个函数的职责单一。
## 3.2 命名规范
- **变量与函数**:使用 `camelCase`(小驼峰命名法),如 `userProfile`, `generateReport`
- **类与接口**:使用 `PascalCase`(大驼峰命名法),如 `UserService`, `ReportGenerator`
- **常量**:使用 `UPPER_SNAKE_CASE`(全大写蛇形命名法),如 `MAX_RETRIES`, `API_TIMEOUT`
- **文件与目录**:使用 `kebab-case`(短横线命名法),如 `user-service.ts`, `data-fetcher.ts`
---
## 4. 注释与文档
每个模块、类和复杂函数必须包含文件头部文档或块级注释,简要说明其作用。必须使用标准的 **JSDoc** 格式来解释功能、参数和返回值:
```TypeScript
/**
* 计算两个数的和。
* @param {number} a - 第一个加数。
* @param {number} b - 第二个加数。
* @returns {number} - 返回两数之和。
*/
function add(a: number, b: number): number {
return a + b;
}
```
---
## 5. 开源许可证声明
**所有源代码文件的顶部必须包含开源相关的版权信息与 SPDX 格式的许可证标识。** 本项目源代码统一采用 **MIT 许可证**。在每个 `.ts` 或者 `。js` 文件头部必须添加如下块级注释:
```plaintext
/**
* Project Caffeine
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*/
```
为了保障代码的可追溯性并尊重每一位开发者的劳动成果,当新的团队成员或开源社区开发者对该文件进行了**实质性修改或重构**时,应当在头部的 `Contributors` 列表中追加自己的信息。
在每个 `.ts``.js` 文件头部,必须添加如下块级注释模板:
```plaintext
/**
* Project Caffeine
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
* - [新贡献者姓名/ID] <[联系邮箱]> ([简述贡献内容,例如:重构了 LRU 缓存模块 / 2026-03])
* - [其他贡献者姓名] <[联系邮箱]> ([例如:修复了 MCP 握手超时的 Bug / 2026-04])
*/
```
---
## 6. 目录结构与模块化解耦
系统应保持高度的模块化,功能模块应独立且彼此解耦。建议采用以下标准目录结构:
- `/src/controllers`:控制器,处理传入的 JSON-RPC 或 HTTP 请求。
- `/src/services`:服务层,包含核心的 MCP 业务逻辑。
- `/src/models`:数据模型与 Schema 定义。
- `/src/routes`:路由定义(适用于 HTTP+SSE 传输模式)。
- `/src/utils`:跨模块共享的工具函数。
- `/config`:环境与系统配置文件。
- `/tests`:单元测试与集成测试文件。
---
## 7. MCP 协议与核心原语
系统全面采用 JSON 格式作为基础数据承载体,并依赖 **JSON-RPC 2.0** 协议规范来管理消息交换。
- **传输层**:本地服务必须采用 STDIO 协议进行无网络开销的直接通信。云端部署则采用 HTTP + SSE 模式。
- **Tools (工具)**:暴露给 LLM 的操作必须通过 `tools/list` 注册,并包含严谨的 `inputSchema`
- **Resources (资源)**:被动的静态上下文数据需通过 `resources/list``resources/read` 暴露。
- **Prompts (提示词)**:作为复用模板,通过 `prompts/list` 暴露,指导模型构造标准化交互结构。
---
## 8. 异常处理与日志记录
## 7.1 错误捕获与响应
- 在业务逻辑中,必须使用 `try-catch` 语句来捕获和处理可能的错误。
- 对于 HTTP/SSE 传输层,遇到预期错误需返回适当的 HTTP 状态码(如 400 错误请求、404 未找到资源)。对于不可预见的严重异常,返回 500 错误。
- 在 MCP 协议层,所有错误必须被标准封装为 JSON-RPC 错误对象,并返回给客户端。
## 7.2 日志系统
- 必须使用成熟的日志库(如 `winston``log4js`)来记录事件。
- 明确日志级别:`info`(正常操作流程)、`warn`(潜在问题)、`error`(系统异常)。
- 记录重要事件如用户操作、API 调用),以便追踪、审计和发现模型幻觉。
- **安全红线**绝对禁止在生产环境日志中记录敏感信息如用户密码、API 密钥、未脱敏的凭证)。
---
## 8. 性能优化与上下文管理
- **异步编程**:必须使用 `async/await``Promise` 处理网络与文件 I/O确保不阻塞 Node.js 事件循环。
- **语义分块**:对于长篇文档的读取,服务端必须在本地完成文本解析与切割,仅将高度相关的片段同步给客户端,防止 LLM Token 耗尽。
- **缓存与数据库**:对于频繁查询的数据,采用 LRU最近最少使用缓存策略或引入外部缓存如 Redis以减少负载。同时需使用合适的索引、查询优化和分页技术避免数据库性能瓶颈。
---
## 9. 安全性与权限管控
协议必须遵循“零信任架构原则”,将 AI 生成的指令视为不可信负载。
- **数据加密**:敏感信息必须加密存储,使用 `bcrypt``argon2` 进行密码哈希处理。
- **认证与授权**:系统通信应使用 JWT 或 OAuth 2.0/2.1 进行认证,并采用作用域限定防止令牌滥用。
- **Roots 隔离**:必须实现 Roots根目录机制服务端依据宿主应用传递的 URI 列表划定沙箱,阻断越权访问和路径遍历漏洞。
- 定期进行安全审计,确保代码没有易受攻击的注入漏洞。
---
## 许可声明
本文档采用 **知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.

View File

@@ -1,30 +1,33 @@
<!--
---
title: "Project Caffeine Arabica版本 Srpint1开发者指南"
description: "Arabica版本第一个迭代版本的开发者指南说明实现一个最小化的5 Wys提示词策略 MCP Server 功能"
type: "Development Guide"
file: arabical-sprint1-development-specification-guide.md
version: "v1.0.0"
author: "Gitconomy Research-郭晧"
date: 2026-03-01
title: 提示词策略 MCP Server 原型设计文档
description: Project Caffeine 提示词策略 MCP Server 的最小可行性功能MVP原型设计涵盖 5 Whys 模板调用、增强提示词合成及 Node.js 环境工作流验证
type: Development Guide
file: project-caffeine-mvp-sprint1-architecture-design.md
version: v1.0.3 (Arabica)
author: Gitconomy Research-郭晧
date: 2026-03-1
last-update: 2026-03-07
update-description: 修复部分文本显示格式。
tags:
- Project Caffeine
- MCP
- LLM
- JSON-RPC 2.0
- TypeScript
- MCP Server
- MVP
- Srpint1
- Prompt Strategy
- 5 Whys
- Node.js
license: "CC BY-SA 4.0"
status: "Active"
license: CC BY-SA 4.0
status: Active
---
-->
# Arabica Sprint1 版本开发指南说明
# Arabica Sprint1 版本开发指南
## 1. 环境前置要求
在开始部署前,请确保开发机已安装以下软件:
- **Node.js**: v18 LTS 或更高版本。
- **Node.js**: v20 LTS 或更高版本。
- **Visual Studio Code (VS Code)**: 作为主力开发与断点调试 IDE。
- **Cherry Studio**: 最新版,作为发起请求的 MCP Client大模型中枢
- **本地知识库**: 一个存放 `.md` 格式笔记的本地文件夹(如 Obsidian Vault
@@ -112,7 +115,7 @@ touch src/app.ts src/services/promptService.ts src/services/resourceService.ts .
---
## 4. 核心业务代码实现
## 4. 核心业务代码实现
### 4.1. `src/services/promptService.ts` (提示词策略生成)
@@ -145,7 +148,7 @@ export function generate5Whys(query: string): string[] {
}
```
### ## 4.2 `src/services/resourceService.ts` (本地知识库访问)
### 4.2 `src/services/resourceService.ts` (本地知识库访问)
带有严格路径防穿越Path Traversal安全校验的本地文件读取服务。
@@ -321,7 +324,7 @@ _(保持这个终端窗口在后台运行不要关闭)_
1. 进入 Cherry Studio 的 **设置 -> MCP**
2. 添加或修改 Server**关键在于你要指向编译后的 `dist/app.js` 而不是 `src/app.ts`**
2. 添加或修改 Server关键在于你要指向编译后的 `dist/app.js` 而不是 `src/app.ts`
- **Command**: `node`
- **Args**: `["--inspect=9229", "/project-caffeine-sprint1/dist/app.js"]`

View File

@@ -0,0 +1,143 @@
<!--
---
title: Arabica v0.1.1 MCP Inspector 测试指南
description: Arabica v0.1.1如何通过该工具进行协议交互与集成测试?
type: Testing Guide
version: 1.0.0
file: arabica-sprint2-mcp-inspector-testing-guide.md
author: Gitconomy Research-郭晧
date: 2026-03-07
tags:
- Project Caffeine
- MCP Inspector
- Integration Testing
- Quality Assurance
- Node.js
license: CC BY-SA 4.0
status: Active
---
-->
## 1. 准备工作
在启动 Inspector 之前,请确保您的项目已经完成了最新的代码编译。
1. 打开终端,进入项目根目录 (`project-caffeine`)。
2. 确保所有依赖已安装:
```
npm install
```
3. 编译 TypeScript 代码并复制 JSON 配置文件到 `dist` 目录:
```
npm run build
```
---
## 2. 启动 MCP Inspector
MCP Inspector 作为一个独立的 Node 包运行,它会通过 STDIO (标准输入/输出) 唤起我们的 Server。
在项目根目录下,运行以下命令启动 Inspector
```
npx @modelcontextprotocol/inspector node dist/app.js //app.js的绝对地址
```
**启动成功后:**
终端会输出一个本地链接(例如是 `http://localhost:6274` 或类似地址)。在浏览器中打开该链接,即可进入 MCP Inspector 可视化界面。
---
## 3. 界面概览与测试流程
MCP Inspector 的界面主要分为:
- **顶栏/侧边栏**:用于在 **Prompts提示词**、**Tools工具** 和 **Resources资源** 之间切换。
- **左侧列表**:显示当前 Server 声明的所有可用项。
- **右侧面板**:用于输入参数并执行请求,同时展示 Server 返回的原始 JSON 结果。
### 3.1 测试 Prompts (多维思维框架)
我们在 Sprint 2 中注册了 5 个核心思维框架。
**测试步骤:**
1. 在顶部/侧边菜单中选择 **Prompts**。
2. 左侧列表中应该会列出 `scqa`, `5whys`, `5w3h`, `swot`, `pestle`。
3. **点击 `scqa`**
- 右侧会显示该 Prompt 需要的参数表单。
- **situation** (必填): 输入 `一家传统零售企业过去三年线上销售额年均增长仅3%`
- **context** (可选): 输入 `公司拥有300家实体店网络`
- **objective** (可选): 输入 `两年内线上销售增速达到15%`
4. 点击 **Run / Get Prompt** 按钮。
5. **验证结果**
- 在下方的结果视图中,您应该能看到一条完整的 `messages` 数组。
- 检查最后一个 `user` 角色的 `content`,确认您的输入已被正确替换到模板中,并且模板底部包含了我们在 JSON 配置文件中定义的严谨 JSON 输出约束。
### 3.2 测试 Tools (意图拆解与本地文件)
我们在 Sprint 2 中保留并扩展了工具类方法。
**测试步骤:**
1. 切换到 **Tools** 面板。
2. 左侧列表应显示 `generate_search_queries`, `list_local_notes`, `read_local_note`, `save_note`。
3. **测试意图拆解 (`generate_search_queries`)**
- 在右侧的 `query` 参数框中输入:`新能源汽车电池回收技术难点`
- 点击执行。
- **验证结果**:检查输出的 `content` 数组,应该返回一段包含 3-5 个专业检索词的 JSON 字符串。
4. **测试文件读取 (`read_local_note`)**(可选,需确保本地知识库路径配置正确):
- 输入 `filename` 为 `test.md`(假设您的 Vault 中有此文件)。
- 点击执行并查看返回的 Markdown 文本内容。
### 3.3 测试 Resources (本地知识库注入)
通过 Resources客户端可以直接浏览并挂载本地文件作为大模型上下文。
**测试步骤:**
1. 切换到 **Resources** 面板。
2. **列表测试**
- Inspector 启动时会自动调用 `resources/list`。左侧应该会展示您在 `OBSIDIAN_VAULT_PATH` 目录下所有的 `.md` 文件列表。
3. **读取测试**
- 点击列表中的任意一个笔记URI 类似 `note://local/xxx.md`)。
- **验证结果**:右侧的 Content 区域应正确渲染或展示该 Markdown 文件的文本内容。
----
## 许可声明
本文档采用 **知识共享署名--相同方式共享 4.0 国际许可协议 (CC BY--SA 4.0)** 进行许可,© 2025-2026 Gitconomy Research.

View File

@@ -0,0 +1,138 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Project Caffeine
* 单元测试: app.ts (接入层)
*/
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
const promptsController_1 = require("../controllers/promptsController");
const toolsController_1 = require("../controllers/toolsController");
const resourceService = __importStar(require("../services/resourceService"));
// 1. 全局 Mock MCP SDK 和传输层,防止测试时真实启动服务器
jest.mock('@modelcontextprotocol/sdk/server/mcp.js');
jest.mock('@modelcontextprotocol/sdk/server/stdio.js');
jest.mock('../controllers/promptsController');
jest.mock('../controllers/toolsController');
jest.mock('../services/resourceService');
describe('app.ts 接入层逻辑测试', () => {
let mockServerInstance;
beforeEach(() => {
jest.clearAllMocks();
// 2. 模拟 McpServer 实例的行为
mockServerInstance = {
prompt: jest.fn(),
tool: jest.fn(),
resource: jest.fn(),
connect: jest.fn().mockResolvedValue(undefined),
};
// 让 McpServer 构造函数返回这个 mock 实例
mcp_js_1.McpServer.mockImplementation(() => mockServerInstance);
// 拦截 console 输出以保持测试日志整洁
jest.spyOn(console, 'error').mockImplementation(() => { });
// 核心:使用 isolateModules 重新 require app.ts确保顶层注册代码被执行
jest.isolateModules(() => {
require('../app');
});
});
describe('Prompts 逻辑验证 (思维框架)', () => {
it('验证参数自动补全:漏传可选参数时应当补全为 "无"', async () => {
// 提取 scqa 注册时的回调函数 (第三个参数)
const scqaCall = mockServerInstance.prompt.mock.calls.find((c) => c[0] === 'scqa');
expect(scqaCall).toBeDefined();
const handler = scqaCall[2];
// 模拟控制器返回一个标准消息
promptsController_1.handlePromptsGet.mockResolvedValueOnce({
messages: [{ role: 'user', content: { text: 'test' } }]
});
// 触发回调:只传入必填的 situation不传 context 和 objective
await handler({ situation: '当前的背景' });
// 验证app.ts 应该调用控制器并补全了参数
expect(promptsController_1.handlePromptsGet).toHaveBeenCalledWith('scqa', {
situation: '当前的背景',
context: '无', // 自动补全逻辑
objective: '无' // 自动补全逻辑
});
});
it('验证消息角色过滤:应当只保留 user 和 assistant 消息', async () => {
const handler = mockServerInstance.prompt.mock.calls.find((c) => c[0] === 'scqa')[2];
// 模拟控制器返回包含 system 消息的序列
promptsController_1.handlePromptsGet.mockResolvedValueOnce({
messages: [
{ role: 'system', content: { type: 'text', text: '指令' } },
{ role: 'user', content: { type: 'text', text: '问题' } },
{ role: 'assistant', content: { type: 'text', text: '回答' } }
]
});
const result = await handler({ situation: 's' });
// 验证system 消息应该被 filter 过滤掉
expect(result.messages).toHaveLength(2);
expect(result.messages[0].role).toBe('user');
expect(result.messages[1].role).toBe('assistant');
// 验证元数据补全
expect(result.messages[0].content._meta).toBeUndefined(); // 应按代码逻辑设为 undefined
});
});
describe('Tools 逻辑验证 (执行工具)', () => {
it('应当将控制器返回的结果正确映射为 type: "text" 格式', async () => {
const toolCall = mockServerInstance.tool.mock.calls.find((c) => c[0] === 'save_note');
const handler = toolCall[2];
// 模拟控制器返回原始文本内容
toolsController_1.handleToolCall.mockResolvedValueOnce({
content: [{ text: '保存成功' }]
});
const result = await handler({ filename: 'a.md', content: 'c' });
// 验证app.ts 应该通过 .map 补全了 type: "text"
expect(result.content[0]).toEqual({
type: 'text',
text: '保存成功'
});
});
});
describe('Resources 逻辑验证 (静态资源)', () => {
it('验证资源读取:应当从 note:// URI 中解码文件名并调用服务层', async () => {
// 寻找 resource 注册时的读取处理函数 (第三个参数)
const readHandler = mockServerInstance.resource.mock.calls[0][2];
const mockUri = { href: 'note://local/%E6%B5%8B%E8%AF%95.md' }; // "测试.md" 的编码
const mockParams = { filename: '%E6%B5%8B%E8%AF%95.md' };
resourceService.readObsidianNote.mockResolvedValueOnce('# 笔记内容');
const result = await readHandler(mockUri, mockParams);
// 验证URI 是否被正确 decodeURIComponent 解码为 "测试.md"
expect(resourceService.readObsidianNote).toHaveBeenCalledWith('测试.md');
expect(result.contents[0].text).toBe('# 笔记内容');
});
});
});
//# sourceMappingURL=app.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.test.js","sourceRoot":"","sources":["../../src/__test__/app.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,2 @@
"use strict";
//# sourceMappingURL=app.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"app.test.js","sourceRoot":"","sources":["../src/app.test.ts"],"names":[],"mappings":""}

View File

@@ -0,0 +1,153 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// src/controllers/__tests__/promptsController.test.ts
const promptsController_1 = require("../promptsController");
const promptService = __importStar(require("../../services/promptService"));
// 模拟 promptService 模块
jest.mock('../../services/promptService');
// 类型断言,便于 TypeScript 识别模拟函数的类型
const mockedPromptService = jest.mocked(promptService);
describe('promptsController', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('handlePromptsList', () => {
it('应该正确将服务层返回的框架列表映射为 prompts/list 响应格式', async () => {
// 模拟服务层返回的数据
const mockFrameworks = [
{
name: 'scqa',
description: 'SCQA 框架',
parameters: [
{ name: 'situation', description: '情境', required: true },
{ name: 'complication', description: '复杂性', required: true }
],
template: '...', // 这些字段不会被 list 使用,但模拟时可省略
systemPrompt: '...'
},
{
name: 'swot',
description: 'SWOT 分析',
parameters: [
{ name: 'entity', description: '分析对象', required: true }
],
template: '...'
}
];
mockedPromptService.listFrameworks.mockResolvedValue(mockFrameworks);
const result = await (0, promptsController_1.handlePromptsList)();
// 验证返回结构符合 MCP prompts/list 规范
expect(result).toHaveProperty('prompts');
expect(Array.isArray(result.prompts)).toBe(true);
expect(result.prompts).toHaveLength(2);
// 验证每个 prompt 的字段映射正确
expect(result.prompts[0]).toEqual({
name: 'scqa',
description: 'SCQA 框架',
arguments: [
{ name: 'situation', description: '情境', required: true },
{ name: 'complication', description: '复杂性', required: true }
]
});
expect(result.prompts[1]).toEqual({
name: 'swot',
description: 'SWOT 分析',
arguments: [
{ name: 'entity', description: '分析对象', required: true }
]
});
// 验证服务层被调用一次
expect(mockedPromptService.listFrameworks).toHaveBeenCalledTimes(1);
});
it('当服务层返回空数组时,应该返回空列表', async () => {
mockedPromptService.listFrameworks.mockResolvedValue([]);
const result = await (0, promptsController_1.handlePromptsList)();
expect(result).toEqual({ prompts: [] });
expect(mockedPromptService.listFrameworks).toHaveBeenCalledTimes(1);
});
it('当服务层抛出异常时,应该传播异常(由调用方处理)', async () => {
const error = new Error('加载框架失败');
mockedPromptService.listFrameworks.mockRejectedValue(error);
await expect((0, promptsController_1.handlePromptsList)()).rejects.toThrow('加载框架失败');
});
});
describe('handlePromptsGet', () => {
const mockName = 'scqa';
const mockArgs = { situation: '市场增长放缓' };
it('当服务层成功返回时,应该返回包含 description 和 messages 的响应', async () => {
const mockServiceResult = {
messages: [
{
role: 'system',
content: { type: "text", text: '你是一名战略顾问' }
},
{
role: 'user',
content: { type: "text", text: '请分析情境:市场增长放缓' }
}
]
};
mockedPromptService.getFramework.mockResolvedValue(mockServiceResult);
const result = await (0, promptsController_1.handlePromptsGet)(mockName, mockArgs);
// 验证返回结构
expect(result).toHaveProperty('description', '框架: scqa');
expect(result).toHaveProperty('messages');
expect(result.messages).toEqual(mockServiceResult.messages);
// 验证服务层被正确调用
expect(mockedPromptService.getFramework).toHaveBeenCalledWith(mockName, mockArgs);
});
it('当 args 为 undefined 时,应传入空对象给服务层', async () => {
const mockServiceResult = { messages: [] };
mockedPromptService.getFramework.mockResolvedValue(mockServiceResult);
// 调用时第二个参数为 undefined
await (0, promptsController_1.handlePromptsGet)(mockName, undefined);
expect(mockedPromptService.getFramework).toHaveBeenCalledWith(mockName, {} // 预期被转换为空对象
);
});
it('当服务层抛出错误时,应重新抛出错误,并包装错误信息', async () => {
const serviceError = new Error('框架不存在');
mockedPromptService.getFramework.mockRejectedValue(serviceError);
// 验证抛出的错误包含原始信息
await expect((0, promptsController_1.handlePromptsGet)(mockName, mockArgs)).rejects.toThrow('获取框架失败: 框架不存在');
});
it('当服务层抛出非 Error 类型时,也应正确处理', async () => {
// 模拟服务层抛出一个字符串
mockedPromptService.getFramework.mockRejectedValue('some string error');
await expect((0, promptsController_1.handlePromptsGet)(mockName, mockArgs)).rejects.toThrow('获取框架失败: some string error');
});
});
});
//# sourceMappingURL=promptsController.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"promptsController.test.js","sourceRoot":"","sources":["../../../src/controllers/__test__/promptsController.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,153 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// src/controllers/__tests__/promptsController.test.ts
const promptsController_1 = require("../promptsController");
const promptService = __importStar(require("../../services/promptService"));
// 模拟 promptService 模块
jest.mock('../../services/promptService');
// 类型断言,便于 TypeScript 识别模拟函数的类型
const mockedPromptService = jest.mocked(promptService);
describe('promptsController', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('handlePromptsList', () => {
it('应该正确将服务层返回的框架列表映射为 prompts/list 响应格式', async () => {
// 模拟服务层返回的数据
const mockFrameworks = [
{
name: 'scqa',
description: 'SCQA 框架',
parameters: [
{ name: 'situation', description: '情境', required: true },
{ name: 'complication', description: '复杂性', required: true }
],
template: '...', // 这些字段不会被 list 使用,但模拟时可省略
systemPrompt: '...'
},
{
name: 'swot',
description: 'SWOT 分析',
parameters: [
{ name: 'entity', description: '分析对象', required: true }
],
template: '...'
}
];
mockedPromptService.listFrameworks.mockResolvedValue(mockFrameworks);
const result = await (0, promptsController_1.handlePromptsList)();
// 验证返回结构符合 MCP prompts/list 规范
expect(result).toHaveProperty('prompts');
expect(Array.isArray(result.prompts)).toBe(true);
expect(result.prompts).toHaveLength(2);
// 验证每个 prompt 的字段映射正确
expect(result.prompts[0]).toEqual({
name: 'scqa',
description: 'SCQA 框架',
arguments: [
{ name: 'situation', description: '情境', required: true },
{ name: 'complication', description: '复杂性', required: true }
]
});
expect(result.prompts[1]).toEqual({
name: 'swot',
description: 'SWOT 分析',
arguments: [
{ name: 'entity', description: '分析对象', required: true }
]
});
// 验证服务层被调用一次
expect(mockedPromptService.listFrameworks).toHaveBeenCalledTimes(1);
});
it('当服务层返回空数组时,应该返回空列表', async () => {
mockedPromptService.listFrameworks.mockResolvedValue([]);
const result = await (0, promptsController_1.handlePromptsList)();
expect(result).toEqual({ prompts: [] });
expect(mockedPromptService.listFrameworks).toHaveBeenCalledTimes(1);
});
it('当服务层抛出异常时,应该传播异常(由调用方处理)', async () => {
const error = new Error('加载框架失败');
mockedPromptService.listFrameworks.mockRejectedValue(error);
await expect((0, promptsController_1.handlePromptsList)()).rejects.toThrow('加载框架失败');
});
});
describe('handlePromptsGet', () => {
const mockName = 'scqa';
const mockArgs = { situation: '市场增长放缓' };
it('当服务层成功返回时,应该返回包含 description 和 messages 的响应', async () => {
const mockServiceResult = {
messages: [
{
role: 'system',
content: { type: "text", text: '你是一名战略顾问' }
},
{
role: 'user',
content: { type: "text", text: '请分析情境:市场增长放缓' }
}
]
};
mockedPromptService.getFramework.mockResolvedValue(mockServiceResult);
const result = await (0, promptsController_1.handlePromptsGet)(mockName, mockArgs);
// 验证返回结构
expect(result).toHaveProperty('description', '框架: scqa');
expect(result).toHaveProperty('messages');
expect(result.messages).toEqual(mockServiceResult.messages);
// 验证服务层被正确调用
expect(mockedPromptService.getFramework).toHaveBeenCalledWith(mockName, mockArgs);
});
it('当 args 为 undefined 时,应传入空对象给服务层', async () => {
const mockServiceResult = { messages: [] };
mockedPromptService.getFramework.mockResolvedValue(mockServiceResult);
// 调用时第二个参数为 undefined
await (0, promptsController_1.handlePromptsGet)(mockName, undefined);
expect(mockedPromptService.getFramework).toHaveBeenCalledWith(mockName, {} // 预期被转换为空对象
);
});
it('当服务层抛出错误时,应重新抛出错误,并包装错误信息', async () => {
const serviceError = new Error('框架不存在');
mockedPromptService.getFramework.mockRejectedValue(serviceError);
// 验证抛出的错误包含原始信息
await expect((0, promptsController_1.handlePromptsGet)(mockName, mockArgs)).rejects.toThrow('获取框架失败: 框架不存在');
});
it('当服务层抛出非 Error 类型时,也应正确处理', async () => {
// 模拟服务层抛出一个字符串
mockedPromptService.getFramework.mockRejectedValue('some string error');
await expect((0, promptsController_1.handlePromptsGet)(mockName, mockArgs)).rejects.toThrow('获取框架失败: some string error');
});
});
});
//# sourceMappingURL=promtsController.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"promtsController.test.js","sourceRoot":"","sources":["../../../src/controllers/__test__/promtsController.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,215 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
// src/controllers/__tests__/toolsController.test.ts
const toolsController_1 = require("../toolsController");
const intentService = __importStar(require("../../services/intentService"));
const resourceService = __importStar(require("../../services/resourceService"));
// 模拟所有依赖的服务
jest.mock('../../services/intentService');
jest.mock('../../services/resourceService');
// 类型断言,方便 TypeScript 识别模拟函数
const mockedIntentService = jest.mocked(intentService);
const mockedResourceService = jest.mocked(resourceService);
describe('toolsController', () => {
beforeEach(() => {
jest.clearAllMocks();
// 可选:模拟 console.error 避免测试输出干扰
jest.spyOn(console, 'error').mockImplementation(() => { });
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('handleToolCall', () => {
describe('未知工具名称', () => {
it('应返回包含未知工具错误信息的响应', async () => {
const result = await (0, toolsController_1.handleToolCall)('unknown_tool', {});
expect(result).toEqual({
content: [{ type: 'text', text: '未知工具: unknown_tool' }],
isError: true
});
});
});
describe('generate_search_queries 工具', () => {
const toolName = 'generate_search_queries';
const validParams = { query: '新能源汽车电池回收' };
it('参数校验失败时,应返回参数错误响应', async () => {
// 传入缺少 query 的参数
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
// 确保服务层未被调用
expect(mockedIntentService.generateSearchQueries).not.toHaveBeenCalled();
});
it('服务层成功执行时,应返回检索词数组的 JSON 字符串', async () => {
const mockQueries = ['动力电池回收', '锂离子再生', '环保法规'];
mockedIntentService.generateSearchQueries.mockReturnValue(mockQueries);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBeUndefined(); // 成功时不设置 isError
expect(result.content).toEqual([
{ type: 'text', text: JSON.stringify(mockQueries, null, 2) }
]);
expect(mockedIntentService.generateSearchQueries).toHaveBeenCalledWith(validParams.query);
});
it('服务层抛出异常时,应返回执行失败响应', async () => {
const error = new Error('意图拆解失败');
mockedIntentService.generateSearchQueries.mockImplementation(() => {
throw error;
});
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('执行失败: 意图拆解失败');
});
it('当服务层抛出非 Error 类型时,应返回包含字符串化信息的失败响应', async () => {
// 模拟抛出一个字符串
mockedIntentService.generateSearchQueries.mockImplementation(() => {
throw 'some string error';
});
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('执行失败: some string error');
});
});
describe('list_local_notes 工具', () => {
const toolName = 'list_local_notes';
it('服务层成功返回笔记列表时,应返回文本列表', async () => {
const mockNotes = ['note1.md', 'note2.md', 'note3.md'];
mockedResourceService.listObsidianNotes.mockResolvedValue(mockNotes);
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBeUndefined();
expect(result.content[0].text).toContain('找到了以下笔记');
expect(result.content[0].text).toContain('note1.md\nnote2.md\nnote3.md');
});
it('服务层返回空列表时,应返回“未找到笔记”', async () => {
mockedResourceService.listObsidianNotes.mockResolvedValue([]);
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.content[0].text).toBe('未找到笔记。');
});
it('服务层抛出异常时,应返回执行失败响应', async () => {
const error = new Error('目录不可读');
mockedResourceService.listObsidianNotes.mockRejectedValue(error);
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('执行失败: 目录不可读');
});
it('服务层抛出非 Error 类型时,应返回字符串化信息的失败响应', async () => {
mockedResourceService.listObsidianNotes.mockRejectedValue('权限错误');
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('执行失败: 权限错误');
});
});
describe('read_local_note 工具', () => {
const toolName = 'read_local_note';
const validParams = { filename: 'test.md' };
it('参数校验失败(缺少 filename应返回参数错误响应', async () => {
const result = await (0, toolsController_1.handleToolCall)(toolName, {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
expect(result.content[0].text).toContain('文件名不能为空');
expect(mockedResourceService.readObsidianNote).not.toHaveBeenCalled();
});
it('参数校验失败filename 不含 .md应返回参数错误响应', async () => {
const result = await (0, toolsController_1.handleToolCall)(toolName, { filename: 'test.txt' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
expect(result.content[0].text).toContain('文件名必须包含 .md 后缀');
});
it('服务层成功读取笔记时,应返回笔记内容', async () => {
const mockContent = '# 测试笔记\n这是一段内容。';
mockedResourceService.readObsidianNote.mockResolvedValue(mockContent);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBeUndefined();
expect(result.content).toEqual([{ type: 'text', text: mockContent }]);
expect(mockedResourceService.readObsidianNote).toHaveBeenCalledWith(validParams.filename);
});
it('服务层抛出异常时,应返回读取失败响应', async () => {
const error = new Error('文件不存在');
mockedResourceService.readObsidianNote.mockRejectedValue(error);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('读取失败: 文件不存在');
});
it('服务层抛出非 Error 类型时,应返回字符串化信息的失败响应', async () => {
mockedResourceService.readObsidianNote.mockRejectedValue('权限不足');
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('读取失败: 权限不足');
});
});
describe('save_note 工具', () => {
const toolName = 'save_note';
const validParams = { filename: 'new.md', content: '# 新笔记' };
it('参数校验失败(缺少 filename应返回参数错误响应', async () => {
const result = await (0, toolsController_1.handleToolCall)(toolName, { content: '# 内容' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
expect(result.content[0].text).toContain('filename'); // 错误信息应提及 filename
expect(mockedResourceService.saveNote).not.toHaveBeenCalled();
});
it('参数校验失败filename 不含 .md应返回参数错误响应', async () => {
const result = await (0, toolsController_1.handleToolCall)(toolName, {
filename: 'new.txt',
content: '# 内容'
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
expect(result.content[0].text).toContain('.md');
});
it('服务层成功保存笔记时,应返回成功信息', async () => {
const mockMessage = '笔记已保存至: /path/new.md';
mockedResourceService.saveNote.mockResolvedValue(mockMessage);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBeUndefined();
expect(result.content).toEqual([{ type: 'text', text: mockMessage }]);
expect(mockedResourceService.saveNote).toHaveBeenCalledWith(validParams.filename, validParams.content);
});
it('服务层抛出异常时,应返回保存失败响应', async () => {
const error = new Error('磁盘空间不足');
mockedResourceService.saveNote.mockRejectedValue(error);
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('保存失败: 磁盘空间不足');
});
it('服务层抛出非 Error 类型时,应返回字符串化信息的失败响应', async () => {
mockedResourceService.saveNote.mockRejectedValue('写入错误');
const result = await (0, toolsController_1.handleToolCall)(toolName, validParams);
expect(result.isError).toBe(true);
expect(result.content[0].text).toBe('保存失败: 写入错误');
});
});
});
});
//# sourceMappingURL=toolsControll.test.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,159 @@
"use strict";
/**
* Project Caffeine
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
const toolsController_1 = require("../toolsController");
const intentService = __importStar(require("../../services/intentService"));
const resourceService = __importStar(require("../../services/resourceService"));
// 1. Mock 服务层模块,防止真实 I/O 和复杂逻辑干扰
jest.mock('../../services/intentService');
jest.mock('../../services/resourceService');
describe('toolsController 单元测试', () => {
// 类型断言方便调用 mock 方法
const mockedIntentService = intentService;
const mockedResourceService = resourceService;
beforeEach(() => {
jest.clearAllMocks();
});
// ==========================================
// 1. 未知工具处理
// ==========================================
it('应当正确处理未定义的工具名称', async () => {
const result = await (0, toolsController_1.handleToolCall)('invalid_tool_name', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('未知工具: invalid_tool_name');
});
// ==========================================
// 2. generate_search_queries 工具测试
// ==========================================
describe('generate_search_queries', () => {
it('当 query 参数为空或缺失时,应当返回参数错误响应', async () => {
// 测试空字符串
const resultEmpty = await (0, toolsController_1.handleToolCall)('generate_search_queries', { query: '' });
expect(resultEmpty.isError).toBe(true);
expect(resultEmpty.content[0].text).toContain('参数错误');
// 容错处理:由于不同环境 Zod 报错结构差异,只要包含“参数错误”即视为拦截成功
// 但理想情况下应包含具体提示,这里我们断言它确实被拦截了
});
it('当输入合法时,应当调用服务并返回格式化后的结果', async () => {
mockedIntentService.generateSearchQueries.mockReturnValue(['词1', '词2']);
const result = await (0, toolsController_1.handleToolCall)('generate_search_queries', { query: '人工智能' });
expect(mockedIntentService.generateSearchQueries).toHaveBeenCalledWith('人工智能');
expect(result.content[0].text).toContain('词1');
expect(result.isError).toBeUndefined();
});
});
// ==========================================
// 3. list_local_notes 工具测试
// ==========================================
describe('list_local_notes', () => {
it('当成功获取笔记列表时,应当返回带预览格式的文本', async () => {
mockedResourceService.listObsidianNotes.mockResolvedValue(['a.md', 'b.md']);
const result = await (0, toolsController_1.handleToolCall)('list_local_notes', {});
expect(result.content[0].text).toContain('找到了以下笔记');
expect(result.content[0].text).toContain('a.md\nb.md');
});
it('当笔记列表为空时,应当返回友好提示', async () => {
mockedResourceService.listObsidianNotes.mockResolvedValue([]);
const result = await (0, toolsController_1.handleToolCall)('list_local_notes', {});
expect(result.content[0].text).toBe('未找到笔记。');
});
});
// ==========================================
// 4. read_local_note 工具测试
// ==========================================
describe('read_local_note', () => {
it('校验测试:当缺少参数时应当返回参数错误', async () => {
const result = await (0, toolsController_1.handleToolCall)('read_local_note', {});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
});
it('校验测试:当文件名后缀不正确时应当拦截', async () => {
const result = await (0, toolsController_1.handleToolCall)('read_local_note', { filename: 'test.txt' });
expect(result.isError).toBe(true);
// 匹配“参数错误”或具体的“后缀”提示
expect(result.content[0].text).toMatch(/参数错误|结尾|后缀/);
});
it('成功流:应当返回服务层读取的笔记内容', async () => {
mockedResourceService.readObsidianNote.mockResolvedValue('# 笔记内容');
const result = await (0, toolsController_1.handleToolCall)('read_local_note', { filename: 'test.md' });
expect(mockedResourceService.readObsidianNote).toHaveBeenCalledWith('test.md');
expect(result.content[0].text).toBe('# 笔记内容');
});
it('异常流:当服务层抛出错误时,应当捕获并返回', async () => {
mockedResourceService.readObsidianNote.mockRejectedValue(new Error('文件不存在'));
const result = await (0, toolsController_1.handleToolCall)('read_local_note', { filename: 'test.md' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('读取失败: 文件不存在');
});
});
// ==========================================
// 5. save_note 工具测试
// ==========================================
describe('save_note', () => {
it('校验测试:当缺少必需参数时应当报错', async () => {
const result = await (0, toolsController_1.handleToolCall)('save_note', { filename: 'new.md' });
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('参数错误');
});
it('成功流:应当调用服务层保存并返回成功提示', async () => {
mockedResourceService.saveNote.mockResolvedValue('保存成功至 path');
const result = await (0, toolsController_1.handleToolCall)('save_note', {
filename: 'new.md',
content: 'hello'
});
expect(mockedResourceService.saveNote).toHaveBeenCalledWith('new.md', 'hello');
expect(result.content[0].text).toBe('保存成功至 path');
});
it('异常流:当捕获到非标准错误(字符串)时,也应能正常返回', async () => {
mockedResourceService.saveNote.mockRejectedValue('磁盘已满');
const result = await (0, toolsController_1.handleToolCall)('save_note', {
filename: 'new.md',
content: 'hello'
});
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('保存失败: 磁盘已满');
});
});
});
//# sourceMappingURL=toolsController.test.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,50 @@
{
"name": "5w3h",
"description": "5W3H 分析法:从 What、Why、Who、When、Where、How、How much、How feel 八个维度全面结构化拆解问题,并基于全景拆解得出行动洞察。",
"persona": "structured_thinker",
"parameters": [
{
"name": "topic",
"description": "需要分析的核心主题、事件或问题",
"required": true
},
{
"name": "context",
"description": "该主题发生的特定背景、前置条件或行业环境(可选)",
"required": false
},
{
"name": "objective",
"description": "期望通过此次分析达成的核心目标(可选)",
"required": false
}
],
"template": "请使用 5W3H 框架分析以下主题:\n核心主题 (Topic){{topic}}\n背景信息 (Context){{context}}\n分析目标 (Objective){{objective}}\n\n请依次从 What(是什么)、Why(为什么)、Who(谁)、When(何时)、Where(何地)、How(如何做)、How much(多少)、How feel(感受如何) 八个维度展开结构化深度拆解,并在最后给出可执行的行动洞察。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"what\": \"描述问题的本质、核心现象或定义的准确陈述\",\n \"why\": \"分析现象产生的原因、背景动因或深层目的\",\n \"who\": \"识别涉及的核心主体、执行者、利益相关者或受众角色\",\n \"when\": \"明确事件发生的时间节点、生命周期、频率或关键时间窗\",\n \"where\": \"界定问题发生的空间范围、物理地点、系统模块或应用场景\",\n \"how\": \"探讨解决问题的途径、落地机制、实施过程或运转模式\",\n \"howMuch\": \"量化指标,如影响规模、涉及成本、人员数量、收益等核心数据\",\n \"howFeel\": \"描述各利益相关方的直观感受、情绪反馈、满意度或体验评价\",\n \"actionableInsights\": [\n \"基于上述8个维度的全景拆解提炼出的第1条具体、可落地的行动指导建议\",\n \"第2条可落地的行动指导建议\"\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导此拆解结构的关键前提假设\"]\n }\n}",
"systemPrompt": "你是一位擅长运用 5W3H 框架进行结构化思考的分析师,能够全面且精确地覆盖事件的八个维度,确保分析的系统性,并能由繁入简提炼行动洞察。你必须严格遵守 JSON 格式输出规范,确保数据可直接被下游程序结构化解析。",
"examples": [
{
"input": {
"topic": "公司推行远程办公政策",
"context": "疫情后复工,但员工普遍期望保留一定的弹性工作时间",
"objective": "评估该政策的影响面,并为平稳落地提供建议"
},
"output": "{\n \"what\": \"允许员工定期或永久性在公司以外(主要为家中)完成工作任务的正式制度与新型工作模式。\",\n \"why\": \"顺应混合办公模式普及的趋势,提升员工满意度与工作生活平衡,减少通勤消耗,并降低公司办公场地的物理运营成本。\",\n \"who\": \"直接主体为全体适岗员工管理主体为各级管理者与HR支持主体为IT部门间接利益相关者包括外部客户与供应商。\",\n \"when\": \"计划于2024年Q3开始小范围试点Q4全面推广包含前期准备、中期过渡、后期稳定三个完整阶段。\",\n \"where\": \"适用于总部及各分支机构的非现场操作岗位,实际工作地点延伸至员工家中或符合保密要求的远程协作空间。\",\n \"how\": \"通过发布政策文档、发放远程软硬件工具包、开展管理者赋能培训以及将考核标准从工时导向强力转向成果导向OKR来落地。\",\n \"howMuch\": \"预计覆盖全公司60%约800名员工初期软硬件投入约200万长期预计每年节省150万场地费员工日均节省通勤1.5小时。\",\n \"howFeel\": \"员工普遍期待且满意度高但部分中层管理者存在强烈的失控感与焦虑IT部门面临短期内支持压力暴增的疲惫感。\",\n \"actionableInsights\": [\n \"针对管理者的失控感HR需在Q3试点前组织专项的异步协同与在线目标管理培训。\",\n \"IT部门需提前建立远程支持工单系统及自助排障知识库以缓解全员推广初期的咨询并发压力。\"\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"公司核心业务流程已基本实现数字化,支持云端协作\",\n \"员工居家环境具备基础的网络办公条件\"\n ]\n }\n}"
},
{
"input": {
"topic": "城市共享单车乱停乱放问题",
"context": "市中心早晚高峰期间,大量单车堵塞地铁口和人行盲道",
"objective": "全面拆解现象成因,为城市管理部门提供治理抓手"
},
"output": "{\n \"what\": \"共享单车使用结束后,用户未按规定停放至指定电子围栏或物理区域,导致违规占用盲道、消防通道及绿化带的现象。\",\n \"why\": \"用户追求极致便利的惰性心理;企业为抢占市场超量投放且电子围栏技术精度不足;政府缺乏实时动态的联合监管与企业惩处机制。\",\n \"who\": \"违规主体为部分单车用户;管理责任方为单车运营企业及城市街道城管部门;受损主体为普通路人、视障群体及社区居民。\",\n \"when\": \"乱停放现象全天候存在但早晚潮汐通勤高峰期后上午9点、下午7点及周末节假日的商圈周边尤为严重且集中。\",\n \"where\": \"高度集中于地铁出入口、大型商圈外围、老旧写字楼周边以及高校门口等短途接驳需求极其旺盛且停车资源匮乏的区域。\",\n \"how\": \"需通过升级高精度北斗定位强制入栏结算、城管与企业建立动态配额联动扣分机制、以及用户信用分与骑行费率挂钩等综合手段治理。\",\n \"howMuch\": \"某一线城市日均需额外清理2万辆违停单车耗费500名专职人员年隐性财政支出超2000万元且占该市出行投诉总量的30%。\",\n \"howFeel\": \"普通市民对出行受阻感到强烈反感与愤怒;企业面对高昂运维搬运成本感到无奈;基层城管面临极大管理压力与反复回潮的疲劳感。\",\n \"actionableInsights\": [\n \"交警与城管部门应联合约谈头部企业,推行‘按违停率扣减下季度投放配额’的强力商业杠杆机制。\",\n \"在地铁口等潮汐热点区域,引入带有物理锁孔的立体停车架,从物理空间上规范停放形态并增加单点容量。\"\n ],\n \"metadata\": {\n \"confidenceScore\": 0.92,\n \"assumptions\": [\n \"北斗高精度定位芯片在主流共享单车上的普及率已达到技术要求\",\n \"政府具备获取各企业实时车辆定位数据的权限\"\n ]\n }\n}"
},
{
"input": {
"topic": "某餐饮品牌推出植物肉新品的市场反响",
"context": "品牌希望迎合年轻人的低碳饮食趋势,在核心门店试点推出了植物肉汉堡",
"objective": "评估首月市场反馈,决定是否向二三线城市全面推广"
},
"output": "{\n \"what\": \"餐饮品牌推出以大豆、豌豆等植物蛋白为原料、模拟真肉口感和外观的植物基汉堡及相关配套轻食餐品。\",\n \"why\": \"为迎合全球健康饮食、低碳环保的新消费趋势打造品牌差异化标签并重点吸引年轻Z世代和弹性素食人群。\",\n \"who\": \"目标受众为年轻白领、健身达人及环保主义者;执行方包括内部研发、市场营销团队及外部植物基供应链头部供应商。\",\n \"when\": \"2024年3月配合春季健康季节点首发上市目前处于上市满首月的市场反馈收集、数据盘点与复盘期。\",\n \"where\": \"首批在一线城市北上广深的50家核心商圈门店及线上主流外卖平台同步推出暂未下沉至二三线城市门店。\",\n \"how\": \"通过KOL探店种草、推出尝鲜价折扣套餐、在门店布置碳足迹减排科普展板等线上线下全渠道营销组合拳推向市场。\",\n \"howMuch\": \"首月总销量达5万份占总营收8%客单价较常规产品溢价15%营销推广费用约200万获取全网曝光超1000万次。\",\n \"howFeel\": \"60%尝鲜用户对口感表示满意并愿复购30%认为价格偏高且有‘科技与狠活’的过度加工顾虑,环保组织则给予了高度赞誉。\",\n \"actionableInsights\": [\n \"针对过度加工的顾虑需在后续营销中公开并强调植物肉的清洁配料表突出0胆固醇、高蛋白的健康属性。\",\n \"暂缓向二三线城市全面推广考虑到30%用户对价格敏感,建议先优化供应链,推出‘半份植物肉+半份蔬菜’的高性价比平替沙拉碗进行下沉测试。\"\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\n \"一线城市消费者对植物基食品的溢价接受度和概念认知度显著高于下沉市场\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,50 @@
{
"name": "5whys",
"description": "5 Whys 分析法:通过连续追问五次“为什么”,剥开表层现象,探究并锁定导致问题发生的根本原因 (Root Cause),并推导解决方案。",
"persona": "root_cause_analyst",
"parameters": [
{
"name": "problem",
"description": "需要分析的核心问题、故障或不良现象",
"required": true
},
{
"name": "context",
"description": "问题发生的背景信息、前置条件或受影响的范围(可选)",
"required": false
},
{
"name": "goal",
"description": "期望通过解决此问题达成的最终目标(可选)",
"required": false
}
],
"template": "请使用 5 Whys 分析法探究以下问题的根本原因:\n核心问题 (Problem){{problem}}\n背景信息 (Context){{context}}\n改善目标 (Goal){{goal}}\n\n请严格按照 5 Whys 的逻辑,从表象问题出发,连续进行 5 次有逻辑因果关系的追问,直到找到根本原因,并给出针对根本原因的解决方案。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"problemStatement\": \"清晰界定当前面临的问题\",\n \"whys\": [\n {\n \"level\": 1,\n \"question\": \"为什么会出现上述问题?\",\n \"answer\": \"第一层直接原因\"\n },\n {\n \"level\": 2,\n \"question\": \"基于上一个回答,进一步追问为什么?\",\n \"answer\": \"第二层深层原因\"\n },\n {\n \"level\": 3,\n \"question\": \"继续追问为什么?\",\n \"answer\": \"第三层原因\"\n },\n {\n \"level\": 4,\n \"question\": \"继续追问为什么?\",\n \"answer\": \"第四层原因\"\n },\n {\n \"level\": 5,\n \"question\": \"追问根本机制或系统性缺陷?\",\n \"answer\": \"第五层根本原因\"\n }\n ],\n \"rootCause\": \"综合上述推导,用一句话总结最终的根本原因(通常是系统、流程或管理机制的问题)\",\n \"actionableSolutions\": [\n {\n \"action\": \"针对根本原因的具体整改措施\",\n \"owner\": \"负责执行的角色或部门\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导此因果链条的关键假设\"]\n }\n}",
"systemPrompt": "你是一位擅长根因分析的问题解决专家,能够通过系统性追问深入挖掘问题的根本原因。你必须严格遵守 JSON 格式输出规范,确保推导链条清晰且可直接被下游程序结构化解析。",
"examples": [
{
"input": {
"problem": "某互联网公司近期新注册用户留存率大幅下降",
"context": "公司主打年轻人的社交APP近期刚投入了大量买量预算但次日留存跌破了警戒线",
"goal": "止住留存率下滑趋势,提升新用户的早期活跃度"
},
"output": "{\n \"problemStatement\": \"公司投入大量买量预算后,新注册用户的次日留存率大幅下跌,未达到预期活跃度。\",\n \"whys\": [\n {\n \"level\": 1,\n \"question\": \"为什么近期新用户留存率大幅下降?\",\n \"answer\": \"因为大量新用户在注册后的一小时内就卸载了APP或停止了活跃。\"\n },\n {\n \"level\": 2,\n \"question\": \"为什么新用户在一小时内就停止活跃?\",\n \"answer\": \"因为他们觉得新用户引导流程过于复杂繁琐,在第三步就放弃了。\"\n },\n {\n \"level\": 3,\n \"question\": \"为什么新用户引导流程复杂繁琐?\",\n \"answer\": \"因为引导步骤多达五步,且强制要求填写过多个人真实信息(如行业、真实头像等)才能进入主界面。\"\n },\n {\n \"level\": 4,\n \"question\": \"为什么引导步骤设计为五步且强制索要信息?\",\n \"answer\": \"因为产品和运营团队希望在注册漏斗最前端收集最全面的用户画像,以便后续做精准算法推荐。\"\n },\n {\n \"level\": 5,\n \"question\": \"为什么产品团队为了收集画像而牺牲了基础的注册转化体验?\",\n \"answer\": \"因为内部KPI考核导向存在偏差设计决策基于内部业务诉求且上线前缺乏可用性测试和A/B测试验证。\"\n }\n ],\n \"rootCause\": \"产品决策机制存在缺陷以内部数据收集目标凌驾于用户体验之上且关键路径改版缺乏科学的A/B测试验证闭环。\",\n \"actionableSolutions\": [\n {\n \"action\": \"立即回滚或精简注册流程,将部分非核心信息的收集后置到用户深度体验产品后\",\n \"owner\": \"产品团队\"\n },\n {\n \"action\": \"建立核心链路改版必须通过A/B测试且留存率不降才能全量发布的流程红线\",\n \"owner\": \"质量与数据分析团队\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.90,\n \"assumptions\": [\n \"新买量的渠道质量与历史渠道一致,非渠道刷量导致\",\n \"留存率的下降与最近一次版本的注册流程改版在时间点上高度重合\"\n ]\n }\n}"
},
{
"input": {
"problem": "某制造工厂生产线设备故障率近期异常上升",
"context": "生产旺季即将到来订单压力极大当前设备停机导致产能出现20%的缺口",
"goal": "迅速降低设备故障率,恢复并保障生产旺季的产能供应"
},
"output": "{\n \"problemStatement\": \"生产旺季前夕,工厂核心生产线设备故障率异常上升,导致停机频发并造成严重产能缺口。\",\n \"whys\": [\n {\n \"level\": 1,\n \"question\": \"为什么设备故障率近期异常上升?\",\n \"answer\": \"因为核心设备的传动轴承等关键部件磨损速度异常加快,导致频繁卡死。\"\n },\n {\n \"level\": 2,\n \"question\": \"为什么关键部件磨损速度异常加快?\",\n \"answer\": \"因为传动部件的润滑严重不足,导致物理摩擦急剧增大。\"\n },\n {\n \"level\": 3,\n \"question\": \"为什么会发生润滑严重不足的情况?\",\n \"answer\": \"因为设备的自动润滑系统出现了堵塞,未能按预定计划加注润滑油。\"\n },\n {\n \"level\": 4,\n \"question\": \"为什么自动润滑系统堵塞没有被及时发现和处理?\",\n \"answer\": \"因为一线维护人员在日常巡检中忽略了对自动润滑系统的检查,误以为它既然是‘自动’的就不需要人工干预。\"\n },\n {\n \"level\": 5,\n \"question\": \"为什么维护人员会产生这种误解并忽略检查?\",\n \"answer\": \"因为设备维护的标准作业程序 (SOP) 文件存在漏洞,未将‘自动润滑系统巡检’明确写入日常检查表,导致责任和标准缺失。\"\n }\n ],\n \"rootCause\": \"设备维护流程与SOP存在管理盲区未覆盖自动化附件的日常监督导致预防性维护失效。\",\n \"actionableSolutions\": [\n {\n \"action\": \"立即全面清理自动润滑系统并更换受损轴承,恢复生产线运转\",\n \"owner\": \"设备维修组\"\n },\n {\n \"action\": \"修订全厂设备点检SOP将所有自动化辅助系统纳入每日必检项并对维修人员进行全员培训\",\n \"owner\": \"工程与设备管理部\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.95,\n \"assumptions\": [\n \"润滑油的标号和品质符合设备要求,非油品质量问题\",\n \"设备并未处于超负荷设计极限运行状态\"\n ]\n }\n}"
},
{
"input": {
"problem": "高校图书馆自习室“占座”现象严重,引发大量投诉",
"context": "期末考试临近,学生对学习空间需求暴增。大量座位上放着书本但长时间无人使用,引发学生间的矛盾",
"goal": "提高图书馆座位的实际利用率,平息学生群体的不满情绪"
},
"output": "{\n \"problemStatement\": \"期末复习高峰期,图书馆自习室存在大量‘物品占座但无人使用’的现象,导致实际资源浪费并引发学生强烈不满。\",\n \"whys\": [\n {\n \"level\": 1,\n \"question\": \"为什么自习室‘占座’现象如此严重?\",\n \"answer\": \"因为很多学生为了确保自己随时有座位,习惯用书本占位后去上课或吃饭,且长时间不返回。\"\n },\n {\n \"level\": 2,\n \"question\": \"为什么学生能够肆无忌惮地长时间占座而不被制止?\",\n \"answer\": \"因为现场没有有效的制止机制,管理员巡查频率极低,且其他学生不敢私自清理他人物品。\"\n },\n {\n \"level\": 3,\n \"question\": \"为什么管理员巡查频率低且不主动清理?\",\n \"answer\": \"因为管理员人手不足,更重要的是,缺乏专门针对‘占座超时’的具体判定标准和管理授权。\"\n },\n {\n \"level\": 4,\n \"question\": \"为什么图书馆一直没有制定占座的判定标准和管理制度?\",\n \"answer\": \"因为图书馆管理层此前一直认为占座是‘学生道德和自觉性’问题,不愿采取强硬的管理手段引发可能的冲突。\"\n },\n {\n \"level\": 5,\n \"question\": \"为什么管理层会将管理责任推给‘自觉’,而不积极介入?\",\n \"answer\": \"因为图书馆缺乏有效的数据监控手段和学生意见反馈渠道,管理层与学生真实痛点脱节,未意识到资源错配的严重性。\"\n }\n ],\n \"rootCause\": \"图书馆管理层服务意识与数字化管理手段双重滞后,未建立科学的空间资源分配规则与违规惩处机制。\",\n \"actionableSolutions\": [\n {\n \"action\": \"紧急出台《自习室防占座管理规定》明确离座超过45分钟即视为放弃并安排专人定期清理滞留物品\",\n \"owner\": \"图书馆馆长办公室\"\n },\n {\n \"action\": \"加快引入基于微信小程序的‘座位预约与扫码签到系统’,用技术手段实现座位资源的动态分配与黑名单机制\",\n \"owner\": \"图书馆信息技术部\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"图书馆整体座位数在绝对数量上确实无法满足期末全体学生的同时自习需求,属于存量博弈\",\n \"学生群体对引入公平的预约机制具有较高的接受度\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,50 @@
{
"name": "pestle",
"description": "PESTLE 宏观环境分析:从政治(P)、经济(E)、社会(S)、技术(T)、法律(L)、环境(E)六个维度,全面结构化评估目标行业或领域的外部宏观环境。",
"persona": "macro_environment_analyst",
"parameters": [
{
"name": "domain",
"description": "需要分析的具体行业、市场或业务领域",
"required": true
},
{
"name": "region",
"description": "目标地域范围(如:全球、中国、北美、特定省市等,可选)",
"required": false
},
{
"name": "timeframe",
"description": "分析的时间跨度当前现状、未来3-5年等可选",
"required": false
}
],
"template": "请使用 PESTLE 框架分析以下行业/领域的宏观环境:\n目标行业/领域 (Domain){{domain}}\n地域范围 (Region){{region}}\n时间跨度 (Timeframe){{timeframe}}\n\n请依次从政治、经济、社会、技术、法律、环境六个维度展开深度评估并给出战略建议。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"political\": {\n \"analysis\": \"政治因素分析,如政府政策、稳定性、贸易导向等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"economic\": {\n \"analysis\": \"经济因素分析,如经济增长、汇率、消费能力等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"social\": {\n \"analysis\": \"社会因素分析,如人口结构、文化趋势、生活方式等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"technological\": {\n \"analysis\": \"技术因素分析,如技术创新、研发周期、自动化替代等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"legal\": {\n \"analysis\": \"法律因素分析,如劳动法、数据安全、知识产权等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"environmental\": {\n \"analysis\": \"环境因素分析如气候变化、ESG标准、碳排放政策等\",\n \"impactLevel\": \"High|Medium|Low\",\n \"trend\": \"Positive|Negative|Neutral\"\n },\n \"strategicAdvice\": [\n {\n \"strategy\": \"基于上述宏观环境得出的具体战略建议\",\n \"focusArea\": \"研发|市场|合规|供应链等\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导该分析所基于的关键宏观假设\"]\n }\n}",
"systemPrompt": "你是一位宏观环境分析专家,擅长运用 PESTLE 框架评估行业外部环境,能够结合具体数据和发展趋势进行深入洞察。你必须严格遵守 JSON 格式输出规范,确保数据可直接被下游程序结构化解析。",
"examples": [
{
"input": {
"domain": "人工智能芯片行业",
"region": "中国",
"timeframe": "未来3-5年"
},
"output": "{\n \"political\": {\n \"analysis\": \"中国政府将AI芯片列为国家战略新兴产业通过大基金等提供支持。同时外部大国对华芯片出口管制持续收紧倒逼国内加速自主可控。政府东数西算工程也为国产芯片创造了庞大的算力底座应用场景。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"economic\": {\n \"analysis\": \"AI大模型带来的算力需求旺盛市场规模年增速预计超30%。然而,宏观经济弱复苏可能导致部分下游行业资本开支收缩,且先进制程的代工成本持续高企,对芯片设计企业的现金流带来挑战。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Neutral\"\n },\n \"social\": {\n \"analysis\": \"全社会数字化转型加速AI应用向制造、医疗等实体经济渗透。公众对数据安全和算法伦理关注度上升。此外高校相关专业扩招使得底层架构研发人才供给逐渐增加但高端领军人才依然稀缺。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Positive\"\n },\n \"technological\": {\n \"analysis\": \"GPU仍占主导但ASIC、类脑芯片等新架构不断涌现。先进封装Chiplet成为绕开先进制程封锁、提升性能的关键途径。国内在推理芯片端已接近国际水平但在训练芯片端仍存代差。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"legal\": {\n \"analysis\": \"《数据安全法》等法规对芯片底层的数据处理合规提出要求。知识产权保护力度加大专利战可能频发。海外实体清单限制了部分EDA工具和半导体IP的获取带来法律和合规断供风险。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Negative\"\n },\n \"environmental\": {\n \"analysis\": \"芯片制造是高耗能产业双碳目标下政府对晶圆厂能耗监管趋严。同时AI数据中心对算力芯片的能效比(PPA)要求极高低功耗设计成为核心竞争力符合ESG投资趋势。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Positive\"\n },\n \"strategicAdvice\": [\n {\n \"strategy\": \"加大基于Chiplet架构和RISC-V开源指令集的研发投入对冲先进制程和商业IP断供的风险。\",\n \"focusArea\": \"研发\"\n },\n {\n \"strategy\": \"紧贴‘东数西算’等政府新基建项目,聚焦信创市场进行国产化替代销售。\",\n \"focusArea\": \"市场\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"全球地缘政治博弈在未来3-5年内不会显著缓和\",\n \"国内算力需求将持续保持指数级增长\"\n ]\n }\n}"
},
{
"input": {
"domain": "新能源汽车电池回收行业",
"region": "欧洲",
"timeframe": "当前至2030年"
},
"output": "{\n \"political\": {\n \"analysis\": \"欧盟致力于实现‘欧洲绿色协议’目标,高度依赖海外关键矿物(锂、钴、镍)。通过立法推动本土电池供应链闭环,以减少对亚洲材料的依赖,政治支持力度空前。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"economic\": {\n \"analysis\": \"由于原始矿产资源价格波动巨大回收材料的经济价值凸显。预计到2030年欧洲将有大量初代电动车电池退役催生百亿欧元级别的回收市场吸引大量风险投资和传统车企资本涌入。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"social\": {\n \"analysis\": \"欧洲民众环保意识极强,对循环经济接受度高。消费者在购车时越来越关注产品的全生命周期碳足迹,社会舆论对不可回收的高污染电池持强烈抵制态度。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Positive\"\n },\n \"technological\": {\n \"analysis\": \"湿法冶金和火法冶金技术正面临优化,以提高金属回收率并降低能耗。电池直接修复与梯次利用(如降级用于储能)的检测和筛选技术是当前的研发热点,自动化拆解技术急需突破。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Neutral\"\n },\n \"legal\": {\n \"analysis\": \"新《欧盟电池法》强制规定了新电池中必须包含最低比例的回收金属如16%的钴、6%的锂等),并要求实施‘电池护照’制度。法规极其严苛,对企业的合规溯源能力提出巨大挑战。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"environmental\": {\n \"analysis\": \"电池回收本身旨在解决环境问题但回收工艺尤其是传统火法可能产生二次污染和高碳排放。行业必须采用清洁能源驱动的低碳回收工艺以符合欧盟严苛的ESG环保审核。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Neutral\"\n },\n \"strategicAdvice\": [\n {\n \"strategy\": \"加快建立基于区块链的‘电池护照’数据平台,实现全链条数据追踪,满足欧盟合规要求。\",\n \"focusArea\": \"合规与数字化\"\n },\n {\n \"strategy\": \"与欧洲本土大型车企建立早期回收联盟锁定未来5年的退役电池废料来源。\",\n \"focusArea\": \"供应链\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.92,\n \"assumptions\": [\n \"新《欧盟电池法》将按预期时间表严格执行不倒退\",\n \"新能源车保有量达到预期规模,提供充足的退役电池\"\n ]\n }\n}"
},
{
"input": {
"domain": "在线职业教育",
"region": "全球(聚焦新兴市场)",
"timeframe": "当前现状"
},
"output": "{\n \"political\": {\n \"analysis\": \"多国政府为缓解结构性失业,出台政策鼓励成人技能重塑。部分新兴市场政府提供财政补贴支持数字化技能培训,但同时对跨境在线教育内容和数据出境的审查也在趋严。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Neutral\"\n },\n \"economic\": {\n \"analysis\": \"全球经济增长放缓导致就业压力增大职场人逆周期自我提升投资意愿增强。然而新兴市场用户支付能力有限平台需要探索B2B2C通过企业采购或微支付等灵活的商业模式。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"social\": {\n \"analysis\": \"终身学习理念普及斜杠青年和自由职业者群体扩大。年轻一代习惯碎片化、移动化学习。由于AI对基础文职工作的冲击社会对软技能和复合型技术技能的培训需求急剧上升。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"technological\": {\n \"analysis\": \"AIGC技术正在重塑在线教育实现课程内容的自动化生成、个性化自适应学习路径以及7x24小时的AI虚拟导师辅导大幅降低了教研和辅导的人力成本。\",\n \"impactLevel\": \"High\",\n \"trend\": \"Positive\"\n },\n \"legal\": {\n \"analysis\": \"面临不同国家的隐私保护法如GDPR合规挑战。AI生成内容的版权归属问题尚不明确。此外部分国家对颁发职业资格证书的线上机构设有严格的准入资质限制。\",\n \"impactLevel\": \"Medium\",\n \"trend\": \"Negative\"\n },\n \"environmental\": {\n \"analysis\": \"在线教育作为无纸化、零通勤的低碳行业契合ESG理念。云服务器带来的碳排放虽然存在但总体环境负面影响极小。\",\n \"impactLevel\": \"Low\",\n \"trend\": \"Neutral\"\n },\n \"strategicAdvice\": [\n {\n \"strategy\": \"全面集成AIGC能力推出AI私教功能以低成本在下沉市场和新兴市场提供个性化体验。\",\n \"focusArea\": \"研发与产品\"\n },\n {\n \"strategy\": \"加强与目标市场本地企业HR部门的合作从单纯卖课向培训+推荐就业’闭环服务转型。\",\n \"focusArea\": \"市场与销售\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\n \"AI技术能够有效替代部分基础人工辅导工作\",\n \"新兴市场互联网基础设施足以支持流畅的视频与AI互动\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,34 @@
{
"name": "scqa",
"description": "SCQA 架构Situation、Complication、Question、Answer适用于问题分析与方案构建",
"persona": "scqa_facilitator",
"parameters": [
{
"name": "situation",
"description": "背景描述",
"required": true
}
],
"template": "请使用 SCQA 架构分析以下情境:\n情境 (Situation){{situation}}\n\n请依次构建\n1. 复杂化 (Complication):指出情境中存在的矛盾或挑战。\n2. 问题 (Question):基于复杂化提炼出核心问题。\n3. 答案 (Answer):提出解决问题的初步方案或分析路径。\n\n输出格式要求使用 Markdown 标题分节,每个部分至少 200 字。",
"systemPrompt": "你是一位资深战略分析师,擅长运用 SCQA 框架构建逻辑严谨的问题分析报告。",
"examples": [
{
"input": {
"situation": "一家传统零售企业过去三年线上销售额年均增长仅 3%,而行业平均水平为 15%。公司拥有实体店网络和稳定供应链,但数字化转型进展缓慢。"
},
"output": "## 情境 (Situation)\n该传统零售企业拥有超过 20 年的历史,在全国拥有 300 家实体门店,供应链体系成熟,品牌认知度高。然而,随着电商的崛起,消费者购物习惯向线上迁移,公司虽然建立了官网和入驻第三方平台,但线上业务增长乏力,过去三年复合增长率仅为 3%,远低于行业 15% 的平均水平。\n\n## 复杂化 (Complication)\n尽管公司意识到数字化转型的必要性并投入资金建设线上渠道但收效甚微。主要矛盾在于\n1. **线上线下割裂**:实体店和线上业务分属不同团队,缺乏协同,导致库存无法共享,促销活动不一致。\n2. **技术投入不足**IT 部门仍以维护 ERP 系统为主,缺乏数据分析和个性化推荐能力,用户体验落后于纯电商对手。\n3. **组织文化阻力**:管理层多来自传统零售背景,对数字营销和用户运营理解不深,决策偏向保守,创新项目难以快速落地。\n\n## 问题 (Question)\n在资源有限且行业竞争加剧的背景下如何通过有效的数字化转型策略在两年内将线上销售增速提升至行业平均水平15% 以上),同时保持实体店的盈利能力?\n\n## 答案 (Answer)\n建议采取“线上线下融合OMO”战略具体路径如下\n1. **组织重构**:成立独立的数字化事业部,由 CEO 直接领导,整合线上线下团队,建立统一考核指标(如全域 GMV、用户生命周期价值。\n2. **技术升级**:引入成熟的电商中台,实现库存、会员、营销三通;部署 CDP 平台,利用客户数据构建精准画像,推动个性化推荐和自动化营销。\n3. **场景创新**:在门店部署智能终端,支持线上下单门店提货、门店缺货时引导至线上完成购买;试点“直播+门店”模式,将店员培养为导购主播,延伸服务半径。\n4. **敏捷试点**:选择一线城市 20 家门店作为试点,快速验证模式,成功后复制推广。同时设立创新基金,鼓励内部孵化新零售项目。\n\n通过以上措施预计一年内可提升线上增速至 10%,两年内达到 15%,并带动整体营收增长。"
},
{
"input": {
"situation": "某三甲医院近期患者满意度调查得分持续下降,尤其在挂号、候诊环节抱怨最多。医院已推行线上预约,但现场仍存在排队过长、秩序混乱等问题。"
},
"output": "## 情境 (Situation)\n该医院是区域医疗中心日均门诊量约 8000 人次。为缓解现场压力,两年前上线了微信预约挂号系统,目前预约率已达 60%。然而近期满意度调查显示,总体得分从 92 分降至 85 分,其中“挂号便捷性”和“候诊时间”两项降幅最大。\n\n## 复杂化 (Complication)\n线上预约本应分流患者但实际并未有效改善体验\n1. **预约时段不准**:预约时段为 1 小时窗口,但医生看诊速度不一,导致患者实际等待时间远超预期。\n2. **现场管理混乱**:仍有大量老年患者不会使用手机,依赖现场窗口挂号,与预约患者混流,加剧排队。\n3. **信息系统割裂**:预约系统与诊室叫号系统未打通,护士需手动核对,效率低下。\n4. **医患沟通不足**:医生问诊时间压缩,患者感觉“被敷衍”,投诉增多。\n\n## 问题 (Question)\n在不增加医生工作负荷的前提下如何通过流程优化和技术手段将患者平均候诊时间缩短 30%,并提升整体满意度?\n\n## 答案 (Answer)\n提出“精准预约+分时段报到”综合方案:\n1. **精准分时段预约**:将预约窗口缩短至 15 分钟,根据历史看诊速度动态调整每个时段号源,减少患者等候。\n2. **推行分时段报到制**:患者仅需在预约时段前 15 分钟到院扫码报到,过早到达者无法提前进入候诊队列,避免现场拥堵。\n3. **设置“无手机通道”**:保留 2 个现场窗口专为老年人服务,并配备志愿者协助,同时推广自助机。\n4. **打通信息系统**:预约平台与院内 HIS、叫号系统实时对接实现自动排队叫号。\n5. **增加诊前沟通**:在候诊区屏幕播放科普视频,护士提供基础咨询,缓解患者焦虑。\n\n实施后预计平均候诊时间由 45 分钟降至 30 分钟,满意度提升至 90 分以上。"
},
{
"input": {
"situation": "某知识付费平台“课程完课率”不足 10%,用户购买课程后往往只学几节课就放弃,导致复购率低、口碑下滑。平台内容质量不差,但用户缺乏学习动力和督促。"
},
"output": "## 情境 (Situation)\n该平台上线三年累计用户 500 万,主打职场技能、个人成长类课程。课程由知名讲师制作,平均评分 4.7 分。然而后台数据显示,完课率(完成 80% 以上课程)仅为 8%,大部分用户购买后一周内活跃,之后迅速流失。\n\n## 复杂化 (Complication)\n内容优质但完课率低矛盾在于\n1. **学习模式单一**:课程多为视频录播,缺乏互动和反馈,用户容易感到枯燥。\n2. **缺乏外部督促**:用户靠自律学习,一旦工作繁忙或热情减退,课程就被搁置。\n3. **目标感弱**:用户购买时冲动,但无明确学习计划和成果预期。\n4. **社群运营缺失**:用户之间无交流,无法形成学习氛围和互相激励。\n\n## 问题 (Question)\n如何在不增加大量人力成本的前提下通过产品机制和轻度运营将课程完课率提升至 30% 以上,并带动复购?\n\n## 答案 (Answer)\n设计“游戏化+社群轻运营”方案:\n1. **学习路径设计**:将课程拆分为每日 15 分钟的任务包,用户可按节奏完成,系统自动提醒。\n2. **积分与勋章体系**:每完成一节课获得积分,连续学习解锁勋章,可兑换优惠券或实物。\n3. **“学伴”匹配**:根据用户兴趣标签,系统自动匹配 3-5 人组成学习小组,共享进度,互相督促。\n4. **定期直播答疑**:每月邀请讲师进行直播,解答课程疑问,并鼓励学员分享学习心得。\n5. **毕业设计**:课程结束后布置小项目,用户提交后可获得电子证书,增强成就感。\n6. **数据驱动干预**:当用户超过 3 天未学习,自动推送定制化提醒或推荐下一个学习任务。\n\n通过上述机制预计完课率可提升至 25-30%,复购率增加 20%,同时形成口碑传播。"
}
]
}

View File

@@ -0,0 +1,50 @@
{
"name": "swot",
"description": "SWOT 分析:从内部优势(S)、内部劣势(W)、外部机会(O)、外部威胁(T)四个维度全面评估分析对象并推导交叉战略方案如SO/ST/WO/WT。",
"persona": "strategy_advisor",
"parameters": [
{
"name": "entity",
"description": "分析对象(如企业、产品、项目、个人等)",
"required": true
},
{
"name": "context",
"description": "补充的行业背景、当前阶段或面临的核心挑战(可选)",
"required": false
},
{
"name": "competitors",
"description": "主要竞争对手或对标对象(可选)",
"required": false
}
],
"template": "请使用 SWOT 框架分析以下对象:\n分析对象 (Entity){{entity}}\n背景信息 (Context){{context}}\n主要竞争对手 (Competitors){{competitors}}\n\n请依次从内部优势、内部劣势、外部机会、外部威胁四个维度展开评估并结合 SWOT 交叉矩阵给出战略建议。\n\n输出格式要求请严格按以下 JSON 格式输出你的分析结果,不要包含任何 Markdown 代码块标记(如反引号包裹的 json直接输出纯 JSON 文本:\n{\n \"strengths\": [\n { \"factor\": \"优势点名称\", \"reasoning\": \"简要论证\", \"impactLevel\": \"High|Medium|Low\" }\n ],\n \"weaknesses\": [\n { \"factor\": \"劣势点名称\", \"reasoning\": \"简要论证\", \"impactLevel\": \"High|Medium|Low\" }\n ],\n \"opportunities\": [\n { \"factor\": \"机会点名称\", \"reasoning\": \"简要论证\", \"urgency\": \"High|Medium|Low\" }\n ],\n \"threats\": [\n { \"factor\": \"威胁点名称\", \"reasoning\": \"简要论证\", \"severity\": \"High|Medium|Low\" }\n ],\n \"actionableStrategies\": [\n {\n \"strategyType\": \"SO增长战略 | ST防御战略 | WO扭转战略 | WT生存战略\",\n \"description\": \"结合具体优劣势和外部环境得出的具体行动建议\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\"推导该分析所基于的关键假设\"]\n }\n}",
"systemPrompt": "你是一位资深的战略分析顾问,擅长使用 SWOT 框架进行竞争态势分析。你必须清晰区分内外部因素,并通过交叉分析推导可执行的战略。你必须严格遵守 JSON 格式输出规范,确保数据可直接被下游程序结构化解析。",
"examples": [
{
"input": {
"entity": "一家专注于新能源汽车电池研发的初创公司",
"context": "A轮融资仅够支撑18个月急需实现技术向商业的转化并打入头部整车厂供应链",
"competitors": "宁德时代、LG新能源等传统电池巨头"
},
"output": "{\n \"strengths\": [\n { \"factor\": \"核心技术领先\", \"reasoning\": \"创始团队拥有5项固态电池核心专利实验室能量密度比市面主流产品高30%。\", \"impactLevel\": \"High\" },\n { \"factor\": \"轻资产与高灵活性\", \"reasoning\": \"采用研发+技术授权模式,决策链路短,无需背负沉重的重资产建厂包袱。\", \"impactLevel\": \"Medium\" }\n ],\n \"weaknesses\": [\n { \"factor\": \"资金链脆弱\", \"reasoning\": \"A轮资金仅能维持18个月的研发抗风险能力弱难以支撑长周期的商业化试错。\", \"impactLevel\": \"High\" },\n { \"factor\": \"缺乏量产与品控经验\", \"reasoning\": \"团队主要为科研背景,中试到规模化量产的技术转化能力未经验证。\", \"impactLevel\": \"High\" }\n ],\n \"opportunities\": [\n { \"factor\": \"高端车型对高能量密度电池渴求\", \"reasoning\": \"整车厂为缓解里程焦虑,对下一代固态电池技术存在巨大的潜在订单需求。\", \"urgency\": \"High\" },\n { \"factor\": \"车企供应链‘去中心化’趋势\", \"reasoning\": \"整车厂不愿被单一巨头(如宁德时代)绑定,积极扶持‘二供’和‘三供’。\", \"urgency\": \"Medium\" }\n ],\n \"threats\": [\n { \"factor\": \"巨头挤压与技术路线封锁\", \"reasoning\": \"传统巨头已投入巨资研发固态电池,初创公司面临巨大的资本与专利壁垒。\", \"severity\": \"High\" },\n { \"factor\": \"原材料价格剧烈波动\", \"reasoning\": \"上游关键矿石材料价格波动易导致研发和中试成本失控。\", \"severity\": \"Medium\" }\n ],\n \"actionableStrategies\": [\n {\n \"strategyType\": \"ST防御战略\",\n \"description\": \"凭借核心专利S选择与头部整车厂建立独家联合研发实验室通过技术入股或预付款形式绑定客户抵御传统电池巨头的资本挤压T缓解资金压力。\"\n },\n {\n \"strategyType\": \"WO扭转战略\",\n \"description\": \"利用车企扶持二供的意愿O引入具备制造业经验的外部高管或与成熟代工厂合资弥补团队缺乏量产经验的短板W。\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.88,\n \"assumptions\": [\n \"固态电池技术在未来两年内能够达到量产前夕的中试标准\",\n \"资本市场对固态电池赛道依然保持较高估值期待\"\n ]\n }\n}"
},
{
"input": {
"entity": "个人职业转型:从技术开发岗转向产品经理岗",
"context": "30岁5年后端开发经验公司内部有产品线扩张的转岗机会",
"competitors": "外部经验丰富的产品经理候选人、内部更年轻的转岗者"
},
"output": "{\n \"strengths\": [\n { \"factor\": \"技术底层理解深\", \"reasoning\": \"5年开发经验使得对系统架构、边界条件及开发成本有精准预判不易被技术团队忽悠。\", \"impactLevel\": \"High\" },\n { \"factor\": \"业务熟悉度高\", \"reasoning\": \"作为内部员工,熟悉公司现有产品线历史债务及组织协作文化,上手成本极低。\", \"impactLevel\": \"High\" }\n ],\n \"weaknesses\": [\n { \"factor\": \"缺乏用户洞察经验\", \"reasoning\": \"长期面对机器和代码,缺乏系统的竞品分析、用户访谈及交互设计思维训练。\", \"impactLevel\": \"High\" },\n { \"factor\": \"沟通习惯偏向确定性\", \"reasoning\": \"习惯以确定性的技术视角看待问题,需适应产品早期阶段高度模糊和频繁变更的沟通场景。\", \"impactLevel\": \"Medium\" }\n ],\n \"opportunities\": [\n { \"factor\": \"公司内部业务扩张\", \"reasoning\": \"公司正进行中台化或技术驱动型产品线扩张,亟需懂业务逻辑的技术型产品经理。\", \"urgency\": \"High\" },\n { \"factor\": \"AI与大模型浪潮\", \"reasoning\": \"AI产品的设计越来越需要懂算法和接口边界的复合型人才这是纯业务型PM的软肋。\", \"urgency\": \"Medium\" }\n ],\n \"threats\": [\n { \"factor\": \"年龄与试错成本\", \"reasoning\": \"30岁转岗意味着放弃原有技术积累的溢价且一旦转型失败难以退回原岗。\", \"severity\": \"High\" },\n { \"factor\": \"外部熟练工竞争\", \"reasoning\": \"行业内涌现大量自带成熟产品方法论的外部候选人,竞争激烈。\", \"severity\": \"High\" }\n ],\n \"actionableStrategies\": [\n {\n \"strategyType\": \"SO增长战略\",\n \"description\": \"利用自身深厚的技术背景和对内部系统的熟悉S主动申请主导偏底层逻辑或技术中台类的产品线O形成差异化竞争。\"\n },\n {\n \"strategyType\": \"WO扭转战略\",\n \"description\": \"在内部寻找资深业务型PM作为导师并在日常工作中主动承担原型绘制和用户调研任务O快速补齐需求分析短板W。\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.85,\n \"assumptions\": [\n \"公司内部拥有较为宽容的转岗试用机制\",\n \"目标产品线确实属于技术逻辑较重的领域\"\n ]\n }\n}"
},
{
"input": {
"entity": "某地方性连锁超市",
"context": "在本市拥有40家门店主打生鲜当前正面临客流严重下滑的危机",
"competitors": "美团优选、多多买菜等全国性社区团购平台"
},
"output": "{\n \"strengths\": [\n { \"factor\": \"本地供应链深耕\", \"reasoning\": \"与本地农户长期合作,生鲜直采品质可控,生鲜品类的信任度高于纯线上平台。\", \"impactLevel\": \"High\" },\n { \"factor\": \"高密度的前置网点\", \"reasoning\": \"40家门店深入社区腹地具备开展最后一公里即时零售的天然地理优势。\", \"impactLevel\": \"Medium\" }\n ],\n \"weaknesses\": [\n { \"factor\": \"数字化运营能力弱\", \"reasoning\": \"缺乏精准的用户画像与私域流量池,营销手段依赖传统的纸质海报,触达率极低。\", \"impactLevel\": \"High\" },\n { \"factor\": \"规模采购成本劣势\", \"reasoning\": \"标品(日化、零食)的采购体量远不及全国性互联网巨头,价格竞争处于下风。\", \"impactLevel\": \"High\" }\n ],\n \"opportunities\": [\n { \"factor\": \"即时零售(即时配送)红利\", \"reasoning\": \"消费者对‘半小时达’的即时性需求增加,而社区团购通常是‘次日达’。\", \"urgency\": \"High\" },\n { \"factor\": \"适老化消费场景构建\", \"reasoning\": \"老龄化趋势下,老年人依然偏好实体店挑拣生鲜的体验和人际交流。\", \"urgency\": \"Medium\" }\n ],\n \"threats\": [\n { \"factor\": \"巨头低价倾销抢夺客流\", \"reasoning\": \"社区团购利用资本优势持续高额补贴,导致价格敏感型顾客大量流失。\", \"severity\": \"High\" },\n { \"factor\": \"运营成本刚性上升\", \"reasoning\": \"实体门店租金、人工成本逐年攀升,持续挤压本已微薄的零售利润。\", \"severity\": \"High\" }\n ],\n \"actionableStrategies\": [\n {\n \"strategyType\": \"ST防御战略\",\n \"description\": \"避开与互联网巨头在标品上的价格战T利用生鲜直采优势S主打高品质、即买即得的差异化生鲜体验巩固基本盘。\"\n },\n {\n \"strategyType\": \"WO扭转战略\",\n \"description\": \"借势即时零售平台(如入驻美团/饿了么O将实体店转化为前置仓弥补自身数字化与配送能力的短板W拓展年轻客群。\"\n }\n ],\n \"metadata\": {\n \"confidenceScore\": 0.90,\n \"assumptions\": [\n \"超市有一定资金支撑初期接入第三方即时配送平台的改造成本\",\n \"本地核心消费群体仍愿为高品质生鲜支付一定的溢价\"\n ]\n }\n}"
}
]
}

View File

@@ -0,0 +1,58 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Project Caffeine
* 单元测试: intentService.ts
*/
const intentService_1 = require("../intentService");
describe('intentService -> generateSearchQueries', () => {
it('异常流:当输入为空字符串或全空格时,应当返回默认的后备检索词', () => {
// 验证空字符串
expect((0, intentService_1.generateSearchQueries)('')).toEqual(['通用研究主题']);
// 验证全空格
expect((0, intentService_1.generateSearchQueries)(' ')).toEqual(['通用研究主题']);
// 验证 null/undefined (通过 any 强制绕过 ts 检查,确保防御性编程生效)
expect((0, intentService_1.generateSearchQueries)(null)).toEqual(['通用研究主题']);
});
it('正常流:当输入包含多个有效关键词时,应当正确分词、去重并返回', () => {
// 包含重复的 "人工智能"
const query = '人工智能 机器学习 深度学习 人工智能';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证去重逻辑
expect(result).toEqual(['人工智能', '机器学习', '深度学习']);
expect(result).toHaveLength(3);
});
it('边界测试:当输入包含各种中英文标点符号时,应当正确替换为空格并分词', () => {
const query = 'AI芯片市场趋势2026;未来发展、产业格局';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证所有的标点都被视为了分隔符
expect(result).toEqual(['AI芯片', '市场趋势', '2026', '未来发展', '产业格局']);
});
it('边界测试:当有效检索词少于 3 个时,应当触发补全机制', () => {
const query = '量子计算';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证数量是否被补全到了 3 个
expect(result).toHaveLength(3);
// 验证第一项是有效词本身
expect(result[0]).toBe('量子计算');
// 验证后两项是被 "${query} 相关研究" 占位补全的
expect(result[1]).toBe('量子计算 相关研究');
expect(result[2]).toBe('量子计算 相关研究');
});
it('边界测试:当有效检索词超过 5 个时,应当执行截取操作', () => {
const query = '苹果 香蕉 橘子 葡萄 西瓜 芒果 樱桃';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证长度严格限制在 5
expect(result).toHaveLength(5);
// 验证截取的是前 5 个有效词
expect(result).toEqual(['苹果', '香蕉', '橘子', '葡萄', '西瓜']);
});
it('边界测试:应当自动过滤掉长度小于 2 的无意义单字(如停用词)', () => {
// "论"、"的"、"与" 长度均为 1应该被抛弃
const query = '论 AI 的 发展 与 IT 行业';
const result = (0, intentService_1.generateSearchQueries)(query);
// 验证单字被正确过滤
expect(result).toEqual(['AI', '发展', 'IT', '行业']);
});
});
//# sourceMappingURL=intentService.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"intentService.test.js","sourceRoot":"","sources":["../../../src/services/__test__/intentService.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,147 @@
"use strict";
/**
* Project Caffeine
* 单元测试: promptService.ts
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
// 1. 全局 Mock fs/promises 模块
jest.mock('fs/promises');
// 2. 全局 Mock personas.json提供测试专用的角色系统提示词
jest.mock('../../models/personas/personas.json', () => [
{ id: 'test_advisor', name: '测试顾问', systemPrompt: '这是一条测试专用的系统提示词' }
], { virtual: true });
describe('promptService', () => {
// 使用 any 替代 typeof import避免部分环境中未深度配置 ts-jest 导致的 Babel 解析报错
let promptService;
// 用于承载每次重置后的全新 fs mock 实例
let fsMock;
beforeEach(async () => {
jest.clearAllMocks(); // 清理 mock 的调用统计
jest.resetModules(); // 核心:重置模块注册表,清空 promptService 内部的 frameworksCache 状态
// 【关键修复】在 resetModules 之后,必须重新 require mock 的 fs 模块!
// 否则 promptService 内部引用的 fs 和这里外部的 fs 不是同一个实例,导致 mock 返回值失效并引发 TypeError。
fsMock = require('fs/promises');
promptService = await Promise.resolve().then(() => __importStar(require('../promptService')));
});
// 用于测试的伪造思维框架 JSON 数据
const mockFrameworkJson = {
name: "test_framework",
description: "用于测试的伪造框架",
parameters: [{ name: "topic", description: "测试主题", required: true }],
template: "请分析这个主题:{{topic}}",
persona: "test_advisor",
examples: [
{
input: { topic: "人工智能" },
output: "人工智能的分析结果"
}
]
};
describe('listFrameworks', () => {
it('应当成功读取并返回框架的元数据(且必须剔除 template 等内部字段)', async () => {
// Given: 使用重新获取的 fsMock 模拟文件系统的返回
fsMock.readdir.mockResolvedValueOnce(['test_framework.json']);
fsMock.readFile.mockResolvedValueOnce(JSON.stringify(mockFrameworkJson));
// When: 调用服务
const result = await promptService.listFrameworks();
// Then: 断言文件系统调用次数及返回值格式
expect(fsMock.readdir).toHaveBeenCalledTimes(1);
expect(fsMock.readFile).toHaveBeenCalledTimes(1);
expect(result).toHaveLength(1);
// 验证返回的元数据对象是否符合预期
expect(result[0]).toEqual({
name: "test_framework",
description: "用于测试的伪造框架",
parameters: [{ name: "topic", description: "测试主题", required: true }]
});
// 断言安全红线:不应该将模板内容暴露在 list 接口中
expect(result[0].template).toBeUndefined();
});
it('异常流:当框架目录读取失败时,应当安全捕获错误并返回空数组', async () => {
// 【新增优化】拦截并静音 console.error避免预期的错误日志污染终端测试面板
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
// 模拟磁盘异常
fsMock.readdir.mockRejectedValueOnce(new Error('目录不存在或无权限'));
const result = await promptService.listFrameworks();
// 验证业务逻辑:服务不会崩溃,而是返回容错的空数组
expect(result).toEqual([]);
// 验证系统确实捕获并打印了预期的错误
expect(consoleSpy).toHaveBeenCalledWith('[PromptService] 加载框架失败:', expect.any(Error));
// 测试完毕后恢复 console.error 的正常行为
consoleSpy.mockRestore();
});
});
describe('getFramework', () => {
beforeEach(() => {
// 为 getFramework 的测试统一挂载成功的 Mock 返回
fsMock.readdir.mockResolvedValue(['test_framework.json']);
fsMock.readFile.mockResolvedValue(JSON.stringify(mockFrameworkJson));
});
it('异常流:当请求不存在的框架名称时,应当抛出明确的错误', async () => {
await expect(promptService.getFramework('unknown_framework', {}))
.rejects.toThrow('框架 "unknown_framework" 不存在');
});
it('核心逻辑:应当正确解析角色,并组装 System、Few-Shot 及 Current User 消息序列', async () => {
// When: 请求框架,并传入动态参数
const result = await promptService.getFramework('test_framework', { topic: '云计算' });
const messages = result.messages;
// Then: 应当精确包含 4 条消息
// (1x System, 1x Example User, 1x Example Assistant, 1x Current User)
expect(messages).toHaveLength(4);
// 1. 验证 System 提示词 (是否成功匹配并读取到了 Mock 的 persona 数据)
expect(messages[0].role).toBe('system');
expect(messages[0].content.text).toBe('这是一条测试专用的系统提示词');
// 2. 验证 Few-Shot 示例中的 User 消息 (模板占位符是否被示例 input 正确替换)
expect(messages[1].role).toBe('user');
expect(messages[1].content.text).toBe('请分析这个主题:人工智能');
// 3. 验证 Few-Shot 示例中的 Assistant 消息
expect(messages[2].role).toBe('assistant');
expect(messages[2].content.text).toBe('人工智能的分析结果');
// 4. 验证当前用户请求 ({{topic}} 占位符是否被传入的 '云计算' 正确替换)
expect(messages[3].role).toBe('user');
expect(messages[3].content.text).toBe('请分析这个主题:云计算');
});
it('边界情况:如果多次调用 getFramework验证内部框架缓存 (frameworksCache) 是否生效', async () => {
// 第一次调用,会触发读盘
await promptService.getFramework('test_framework', { topic: '测试1' });
// 第二次调用,应该直接命中内部的 frameworksCache 缓存
await promptService.getFramework('test_framework', { topic: '测试2' });
// 断言:整个生命周期中,读盘操作应只有 1 次
expect(fsMock.readdir).toHaveBeenCalledTimes(1);
expect(fsMock.readFile).toHaveBeenCalledTimes(1);
});
});
});
//# sourceMappingURL=promptService.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"promptService.test.js","sourceRoot":"","sources":["../../../src/services/__test__/promptService.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,107 @@
"use strict";
/**
* Project Caffeine v0.1.1
* Copyright (c) 2025-2026 Gitconomy Research
*
* SPDX-License-Identifier: MIT
*
* Contributors:
* - 郭晧 <guohao@gitconomy.org> (Initial Author)
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.listObsidianNotes = listObsidianNotes;
exports.readObsidianNote = readObsidianNote;
exports.saveNote = saveNote;
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
/**
* 本地知识库的根目录路径。
*
* 该目录存放所有 Markdown 笔记文件,所有文件操作均限定在此目录内,
* 以防止路径遍历攻击。
*
* @constant {string}
*/
const OBSIDIAN_VAULT_PATH = '/home/wguo/Downloads/MyVault'; // 【⚠️ 重要配置】请修改为你电脑上真实的 Markdown 笔记文件夹绝对路径!
/**
* 列出知识库中所有 Markdown 笔记的文件名。
*
* 该函数读取 OBSIDIAN_VAULT_PATH 目录下的所有文件,过滤出以 .md 结尾
* (不区分大小写)的文件,并返回文件名列表。若目录不存在或无权限访问,
* 则返回空数组并打印错误日志。
*
* @returns {Promise<string[]>} 包含所有笔记文件名的数组,若失败则返回空数组。
*/
async function listObsidianNotes() {
try {
const files = await promises_1.default.readdir(OBSIDIAN_VAULT_PATH);
return files.filter(file => file.toLowerCase().endsWith('.md'));
}
catch (error) {
console.error(`[Project Caffeine] 无法读取知识库目录: ${error.message}`);
return [];
}
}
/**
* 读取指定笔记文件的完整内容。
*
* 该函数首先对文件名进行安全校验,确保文件位于知识库目录内,
* 防止路径遍历攻击。校验通过后,读取文件内容并返回。
*
* @param {string} filename - 要读取的笔记文件名(必须包含 .md 后缀)
* @returns {Promise<string>} 笔记文件的文本内容
* @throws {Error} 当文件名导致路径越界时抛出安全警告
* @throws {Error} 当文件不存在或无权限读取时抛出错误
*/
async function readObsidianNote(filename) {
const targetPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH, filename);
const safeVaultPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH);
// 核心防御:防止大模型通过传入 "../../" 读取系统敏感文件
if (!targetPath.startsWith(safeVaultPath)) {
throw new Error(`安全警告:越权访问拦截!禁止读取目录外的文件: ${filename}`);
}
try {
const content = await promises_1.default.readFile(targetPath, 'utf-8');
return content;
}
catch (error) {
throw new Error(`无法读取笔记 [${filename}]: 文件可能不存在或无权限。`);
}
}
/**
* 保存笔记到本地知识库。
*
* 该函数将内容写入指定文件,执行以下校验和操作:
* 1. 验证文件名是否以 .md 结尾。
* 2. 验证文件路径是否在知识库目录内,防止路径遍历攻击。
* 3. 确保知识库目录存在(若不存在则自动创建)。
* 4. 将内容写入文件。
*
* @param {string} filename - 笔记文件名(必须以 .md 结尾)
* @param {string} content - 笔记内容Markdown 格式)
* @returns {Promise<string>} 保存成功的提示信息,包含文件绝对路径
* @throws {Error} 当文件名不以 .md 结尾时抛出错误
* @throws {Error} 当文件名导致路径越界时抛出错误
* @throws {Error} 当目录创建失败或文件写入失败时抛出错误
*/
async function saveNote(filename, content) {
// 1. 验证文件名是否以 .md 结尾
if (!filename.endsWith('.md')) {
throw new Error('文件名必须以 .md 结尾');
}
// 2. 防止路径遍历攻击:解析绝对路径,并检查是否在 NOTES_DIR 下
const fullPath = path_1.default.resolve(OBSIDIAN_VAULT_PATH, filename);
const relative = path_1.default.relative(OBSIDIAN_VAULT_PATH, fullPath);
if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
throw new Error('无效的文件名,不允许访问上层目录');
}
// 3. 确保目标目录存在(可选,如果 NOTES_DIR 必须存在则可跳过)
await promises_1.default.mkdir(OBSIDIAN_VAULT_PATH, { recursive: true });
// 4. 写入文件
await promises_1.default.writeFile(fullPath, content, 'utf-8');
return `笔记已保存至: ${fullPath}`;
}
//# sourceMappingURL=resourceService.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resourceService.js","sourceRoot":"","sources":["../../../src/services/__test__/resourceService.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG;;;;;AA2BH,8CAQC;AAcD,4CAeC;AAmBD,4BAmBC;AApGD,2DAA6B;AAC7B,gDAAwB;AAExB;;;;;;;GAOG;AAGH,MAAM,mBAAmB,GAAG,8BAA8B,CAAC,CAAC,2CAA2C;AAEvG;;;;;;;;GAQG;AAEI,KAAK,UAAU,iBAAiB;IACnC,IAAI,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,kBAAE,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;QACpD,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IACpE,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,iCAAiC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QAChE,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AAEI,KAAK,UAAU,gBAAgB,CAAC,QAAgB;IACnD,MAAM,UAAU,GAAG,cAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAC/D,MAAM,aAAa,GAAG,cAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAExD,mCAAmC;IACnC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,IAAI,CAAC;QACD,MAAM,OAAO,GAAG,MAAM,kBAAE,CAAC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACvD,OAAO,OAAO,CAAC;IACnB,CAAC;IAAC,OAAO,KAAU,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,WAAW,QAAQ,iBAAiB,CAAC,CAAC;IAC1D,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AAEI,KAAK,UAAU,QAAQ,CAAC,QAAgB,EAAE,OAAe;IAC9D,qBAAqB;IACrB,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,eAAe,CAAC,CAAC;IACnC,CAAC;IAED,wCAAwC;IACxC,MAAM,QAAQ,GAAG,cAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,cAAI,CAAC,QAAQ,CAAC,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAC9D,IAAI,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,cAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC3D,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC;IACtC,CAAC;IAED,wCAAwC;IACxC,MAAM,kBAAE,CAAC,KAAK,CAAC,mBAAmB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzD,UAAU;IACV,MAAM,kBAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAC/C,OAAO,WAAW,QAAQ,EAAE,CAAC;AAC/B,CAAC"}

View File

@@ -0,0 +1,113 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
/**
* Project Caffeine
* 单元测试: resourceService.ts
*/
const promises_1 = __importDefault(require("fs/promises"));
const path_1 = __importDefault(require("path"));
const resourceService_1 = require("../resourceService");
// 全局 Mock fs/promises 模块,严禁真实磁盘 I/O
jest.mock('fs/promises');
describe('resourceService', () => {
// 提取源码中硬编码的知识库路径,用于动态构建断言的预期路径
const MOCK_VAULT_PATH = '/home/wguo/Downloads/MyVault';
beforeEach(() => {
// 确保每个用例运行前,清空 mock 的调用历史,保持独立性
jest.clearAllMocks();
});
// ==================================================================
// 测试: listObsidianNotes
// ==================================================================
describe('listObsidianNotes', () => {
it('正常流:应当正确读取目录,并只过滤出 .md 结尾的文件(忽略大小写)', async () => {
// 模拟 fs.readdir 返回多种类型的文件
const mockFiles = ['note1.md', 'note2.MD', 'image.png', 'folder', 'test.txt'];
promises_1.default.readdir.mockResolvedValueOnce(mockFiles);
const result = await (0, resourceService_1.listObsidianNotes)();
expect(promises_1.default.readdir).toHaveBeenCalledWith(MOCK_VAULT_PATH);
// 验证过滤逻辑:只保留 .md 和 .MD
expect(result).toEqual(['note1.md', 'note2.MD']);
expect(result).toHaveLength(2);
});
it('异常流:当目录读取失败(如不存在或无权限)时,应当安全捕获并返回空数组', async () => {
// 拦截 console.error避免预期的报错污染终端视图
const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
promises_1.default.readdir.mockRejectedValueOnce(new Error('Permission denied'));
const result = await (0, resourceService_1.listObsidianNotes)();
// 验证:服务没有崩溃,而是优雅降级返回空数组
expect(result).toEqual([]);
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('[Project Caffeine] 无法读取知识库目录:'));
consoleSpy.mockRestore();
});
});
// ==================================================================
// 测试: readObsidianNote
// ==================================================================
describe('readObsidianNote', () => {
it('安全红线测试:发生相对路径遍历攻击 (../) 时,应当拦截并抛出安全警告', async () => {
const maliciousFilename = '../../etc/passwd';
// 验证越权访问是否被拦截
await expect((0, resourceService_1.readObsidianNote)(maliciousFilename)).rejects.toThrow(`安全警告:越权访问拦截!禁止读取目录外的文件: ${maliciousFilename}`);
// 验证未发生任何实质性的文件读取
expect(promises_1.default.readFile).not.toHaveBeenCalled();
});
it('正常流:当请求合法文件名时,应当正确读取并返回文件内容', async () => {
const validFilename = 'test_note.md';
const expectedContent = '# 这是一个测试笔记';
const expectedPath = path_1.default.resolve(MOCK_VAULT_PATH, validFilename);
promises_1.default.readFile.mockResolvedValueOnce(expectedContent);
const result = await (0, resourceService_1.readObsidianNote)(validFilename);
expect(promises_1.default.readFile).toHaveBeenCalledWith(expectedPath, 'utf-8');
expect(result).toBe(expectedContent);
});
it('异常流:当合法文件不存在或读取失败时,应当抛出统一的业务错误', async () => {
const validFilename = 'missing.md';
promises_1.default.readFile.mockRejectedValueOnce(new Error('ENOENT'));
await expect((0, resourceService_1.readObsidianNote)(validFilename)).rejects.toThrow(`无法读取笔记 [${validFilename}]: 文件可能不存在或无权限。`);
});
});
// ==================================================================
// 测试: saveNote
// ==================================================================
describe('saveNote', () => {
it('边界测试:当文件名不以 .md 结尾时,应当拒绝保存', async () => {
await expect((0, resourceService_1.saveNote)('test.txt', '内容')).rejects.toThrow('文件名必须以 .md 结尾');
expect(promises_1.default.writeFile).not.toHaveBeenCalled();
});
it('安全红线测试:发生绝对路径越界写入时,应当拦截并抛出错误', async () => {
await expect((0, resourceService_1.saveNote)('/root/hack.md', '内容')).rejects.toThrow('无效的文件名,不允许访问上层目录');
expect(promises_1.default.writeFile).not.toHaveBeenCalled();
});
it('安全红线测试:发生相对路径遍历写入 (../) 时,应当拦截并抛出错误', async () => {
await expect((0, resourceService_1.saveNote)('../outside.md', '内容')).rejects.toThrow('无效的文件名,不允许访问上层目录');
expect(promises_1.default.writeFile).not.toHaveBeenCalled();
});
it('正常流:当输入合法时,应当先确保目录存在,然后成功写入文件并返回提示', async () => {
const filename = 'new_insight.md';
const content = '## 新的洞察发现';
const expectedPath = path_1.default.resolve(MOCK_VAULT_PATH, filename);
promises_1.default.mkdir.mockResolvedValueOnce(undefined);
promises_1.default.writeFile.mockResolvedValueOnce(undefined);
const result = await (0, resourceService_1.saveNote)(filename, content);
// 1. 验证是否调用了创建目录 (递归模式)
expect(promises_1.default.mkdir).toHaveBeenCalledWith(MOCK_VAULT_PATH, { recursive: true });
// 2. 验证是否向正确路径写入了文件
expect(promises_1.default.writeFile).toHaveBeenCalledWith(expectedPath, content, 'utf-8');
// 3. 验证返回的成功确认消息
expect(result).toBe(`笔记已保存至: ${expectedPath}`);
});
it('异常流:当底层磁盘写入发生崩溃时,应当将错误向上抛出', async () => {
const filename = 'fail_test.md';
const errorMsg = 'Disk Full';
promises_1.default.mkdir.mockResolvedValueOnce(undefined);
promises_1.default.writeFile.mockRejectedValueOnce(new Error(errorMsg));
// 测试是否会将文件系统的原生错误冒泡(便于被 controller 层捕获)
await expect((0, resourceService_1.saveNote)(filename, '内容')).rejects.toThrow(errorMsg);
});
});
});
//# sourceMappingURL=resourceService.test.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"resourceService.test.js","sourceRoot":"","sources":["../../../src/services/__test__/resourceService.test.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,6 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['/node_modules/', '/dist/'] // 忽略 dist 目录!
};

View File

@@ -0,0 +1 @@
../copyfiles/copyfiles

1
projects/arabica/src/sprint2/node_modules/.bin/copyup generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../copyfiles/copyfiles

1
projects/arabica/src/sprint2/node_modules/.bin/mkdirp generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../mkdirp/bin/cmd.js

View File

@@ -0,0 +1,37 @@
declare namespace ansiRegex {
interface Options {
/**
Match only the first ANSI escape.
@default false
*/
onlyFirst: boolean;
}
}
/**
Regular expression for matching ANSI escape codes.
@example
```
import ansiRegex = require('ansi-regex');
ansiRegex().test('\u001B[4mcake\u001B[0m');
//=> true
ansiRegex().test('cake');
//=> false
'\u001B[4mcake\u001B[0m'.match(ansiRegex());
//=> ['\u001B[4m', '\u001B[0m']
'\u001B[4mcake\u001B[0m'.match(ansiRegex({onlyFirst: true}));
//=> ['\u001B[4m']
'\u001B]8;;https://github.com\u0007click\u001B]8;;\u0007'.match(ansiRegex());
//=> ['\u001B]8;;https://github.com\u0007', '\u001B]8;;\u0007']
```
*/
declare function ansiRegex(options?: ansiRegex.Options): RegExp;
export = ansiRegex;

View File

@@ -0,0 +1,10 @@
'use strict';
module.exports = ({onlyFirst = false} = {}) => {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
].join('|');
return new RegExp(pattern, onlyFirst ? undefined : 'g');
};

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,55 @@
{
"name": "ansi-regex",
"version": "5.0.1",
"description": "Regular expression for matching ANSI escape codes",
"license": "MIT",
"repository": "chalk/ansi-regex",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "sindresorhus.com"
},
"engines": {
"node": ">=8"
},
"scripts": {
"test": "xo && ava && tsd",
"view-supported": "node fixtures/view-codes.js"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"ansi",
"styles",
"color",
"colour",
"colors",
"terminal",
"console",
"cli",
"string",
"tty",
"escape",
"formatting",
"rgb",
"256",
"shell",
"xterm",
"command-line",
"text",
"regex",
"regexp",
"re",
"match",
"test",
"find",
"pattern"
],
"devDependencies": {
"ava": "^2.4.0",
"tsd": "^0.9.0",
"xo": "^0.25.3"
}
}

View File

@@ -0,0 +1,78 @@
# ansi-regex
> Regular expression for matching [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code)
## Install
```
$ npm install ansi-regex
```
## Usage
```js
const ansiRegex = require('ansi-regex');
ansiRegex().test('\u001B[4mcake\u001B[0m');
//=> true
ansiRegex().test('cake');
//=> false
'\u001B[4mcake\u001B[0m'.match(ansiRegex());
//=> ['\u001B[4m', '\u001B[0m']
'\u001B[4mcake\u001B[0m'.match(ansiRegex({onlyFirst: true}));
//=> ['\u001B[4m']
'\u001B]8;;https://github.com\u0007click\u001B]8;;\u0007'.match(ansiRegex());
//=> ['\u001B]8;;https://github.com\u0007', '\u001B]8;;\u0007']
```
## API
### ansiRegex(options?)
Returns a regex for matching ANSI escape codes.
#### options
Type: `object`
##### onlyFirst
Type: `boolean`<br>
Default: `false` *(Matches any ANSI escape codes in a string)*
Match only the first ANSI escape.
## FAQ
### Why do you test for codes not in the ECMA 48 standard?
Some of the codes we run as a test are codes that we acquired finding various lists of non-standard or manufacturer specific codes. We test for both standard and non-standard codes, as most of them follow the same or similar format and can be safely matched in strings without the risk of removing actual string content. There are a few non-standard control codes that do not follow the traditional format (i.e. they end in numbers) thus forcing us to exclude them from the test because we cannot reliably match them.
On the historical side, those ECMA standards were established in the early 90's whereas the VT100, for example, was designed in the mid/late 70's. At that point in time, control codes were still pretty ungoverned and engineers used them for a multitude of things, namely to activate hardware ports that may have been proprietary. Somewhere else you see a similar 'anarchy' of codes is in the x86 architecture for processors; there are a ton of "interrupts" that can mean different things on certain brands of processors, most of which have been phased out.
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Josh Junon](https://github.com/qix-)
---
<div align="center">
<b>
<a href="https://tidelift.com/subscription/pkg/npm-ansi-regex?utm_source=npm-ansi-regex&utm_medium=referral&utm_campaign=readme">Get professional support for this package with a Tidelift subscription</a>
</b>
<br>
<sub>
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
</sub>
</div>

View File

@@ -0,0 +1,345 @@
declare type CSSColor =
| 'aliceblue'
| 'antiquewhite'
| 'aqua'
| 'aquamarine'
| 'azure'
| 'beige'
| 'bisque'
| 'black'
| 'blanchedalmond'
| 'blue'
| 'blueviolet'
| 'brown'
| 'burlywood'
| 'cadetblue'
| 'chartreuse'
| 'chocolate'
| 'coral'
| 'cornflowerblue'
| 'cornsilk'
| 'crimson'
| 'cyan'
| 'darkblue'
| 'darkcyan'
| 'darkgoldenrod'
| 'darkgray'
| 'darkgreen'
| 'darkgrey'
| 'darkkhaki'
| 'darkmagenta'
| 'darkolivegreen'
| 'darkorange'
| 'darkorchid'
| 'darkred'
| 'darksalmon'
| 'darkseagreen'
| 'darkslateblue'
| 'darkslategray'
| 'darkslategrey'
| 'darkturquoise'
| 'darkviolet'
| 'deeppink'
| 'deepskyblue'
| 'dimgray'
| 'dimgrey'
| 'dodgerblue'
| 'firebrick'
| 'floralwhite'
| 'forestgreen'
| 'fuchsia'
| 'gainsboro'
| 'ghostwhite'
| 'gold'
| 'goldenrod'
| 'gray'
| 'green'
| 'greenyellow'
| 'grey'
| 'honeydew'
| 'hotpink'
| 'indianred'
| 'indigo'
| 'ivory'
| 'khaki'
| 'lavender'
| 'lavenderblush'
| 'lawngreen'
| 'lemonchiffon'
| 'lightblue'
| 'lightcoral'
| 'lightcyan'
| 'lightgoldenrodyellow'
| 'lightgray'
| 'lightgreen'
| 'lightgrey'
| 'lightpink'
| 'lightsalmon'
| 'lightseagreen'
| 'lightskyblue'
| 'lightslategray'
| 'lightslategrey'
| 'lightsteelblue'
| 'lightyellow'
| 'lime'
| 'limegreen'
| 'linen'
| 'magenta'
| 'maroon'
| 'mediumaquamarine'
| 'mediumblue'
| 'mediumorchid'
| 'mediumpurple'
| 'mediumseagreen'
| 'mediumslateblue'
| 'mediumspringgreen'
| 'mediumturquoise'
| 'mediumvioletred'
| 'midnightblue'
| 'mintcream'
| 'mistyrose'
| 'moccasin'
| 'navajowhite'
| 'navy'
| 'oldlace'
| 'olive'
| 'olivedrab'
| 'orange'
| 'orangered'
| 'orchid'
| 'palegoldenrod'
| 'palegreen'
| 'paleturquoise'
| 'palevioletred'
| 'papayawhip'
| 'peachpuff'
| 'peru'
| 'pink'
| 'plum'
| 'powderblue'
| 'purple'
| 'rebeccapurple'
| 'red'
| 'rosybrown'
| 'royalblue'
| 'saddlebrown'
| 'salmon'
| 'sandybrown'
| 'seagreen'
| 'seashell'
| 'sienna'
| 'silver'
| 'skyblue'
| 'slateblue'
| 'slategray'
| 'slategrey'
| 'snow'
| 'springgreen'
| 'steelblue'
| 'tan'
| 'teal'
| 'thistle'
| 'tomato'
| 'turquoise'
| 'violet'
| 'wheat'
| 'white'
| 'whitesmoke'
| 'yellow'
| 'yellowgreen';
declare namespace ansiStyles {
interface ColorConvert {
/**
The RGB color space.
@param red - (`0`-`255`)
@param green - (`0`-`255`)
@param blue - (`0`-`255`)
*/
rgb(red: number, green: number, blue: number): string;
/**
The RGB HEX color space.
@param hex - A hexadecimal string containing RGB data.
*/
hex(hex: string): string;
/**
@param keyword - A CSS color name.
*/
keyword(keyword: CSSColor): string;
/**
The HSL color space.
@param hue - (`0`-`360`)
@param saturation - (`0`-`100`)
@param lightness - (`0`-`100`)
*/
hsl(hue: number, saturation: number, lightness: number): string;
/**
The HSV color space.
@param hue - (`0`-`360`)
@param saturation - (`0`-`100`)
@param value - (`0`-`100`)
*/
hsv(hue: number, saturation: number, value: number): string;
/**
The HSV color space.
@param hue - (`0`-`360`)
@param whiteness - (`0`-`100`)
@param blackness - (`0`-`100`)
*/
hwb(hue: number, whiteness: number, blackness: number): string;
/**
Use a [4-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#3/4-bit) to set text color.
*/
ansi(ansi: number): string;
/**
Use an [8-bit unsigned number](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) to set text color.
*/
ansi256(ansi: number): string;
}
interface CSPair {
/**
The ANSI terminal control sequence for starting this style.
*/
readonly open: string;
/**
The ANSI terminal control sequence for ending this style.
*/
readonly close: string;
}
interface ColorBase {
readonly ansi: ColorConvert;
readonly ansi256: ColorConvert;
readonly ansi16m: ColorConvert;
/**
The ANSI terminal control sequence for ending this color.
*/
readonly close: string;
}
interface Modifier {
/**
Resets the current color chain.
*/
readonly reset: CSPair;
/**
Make text bold.
*/
readonly bold: CSPair;
/**
Emitting only a small amount of light.
*/
readonly dim: CSPair;
/**
Make text italic. (Not widely supported)
*/
readonly italic: CSPair;
/**
Make text underline. (Not widely supported)
*/
readonly underline: CSPair;
/**
Inverse background and foreground colors.
*/
readonly inverse: CSPair;
/**
Prints the text, but makes it invisible.
*/
readonly hidden: CSPair;
/**
Puts a horizontal line through the center of the text. (Not widely supported)
*/
readonly strikethrough: CSPair;
}
interface ForegroundColor {
readonly black: CSPair;
readonly red: CSPair;
readonly green: CSPair;
readonly yellow: CSPair;
readonly blue: CSPair;
readonly cyan: CSPair;
readonly magenta: CSPair;
readonly white: CSPair;
/**
Alias for `blackBright`.
*/
readonly gray: CSPair;
/**
Alias for `blackBright`.
*/
readonly grey: CSPair;
readonly blackBright: CSPair;
readonly redBright: CSPair;
readonly greenBright: CSPair;
readonly yellowBright: CSPair;
readonly blueBright: CSPair;
readonly cyanBright: CSPair;
readonly magentaBright: CSPair;
readonly whiteBright: CSPair;
}
interface BackgroundColor {
readonly bgBlack: CSPair;
readonly bgRed: CSPair;
readonly bgGreen: CSPair;
readonly bgYellow: CSPair;
readonly bgBlue: CSPair;
readonly bgCyan: CSPair;
readonly bgMagenta: CSPair;
readonly bgWhite: CSPair;
/**
Alias for `bgBlackBright`.
*/
readonly bgGray: CSPair;
/**
Alias for `bgBlackBright`.
*/
readonly bgGrey: CSPair;
readonly bgBlackBright: CSPair;
readonly bgRedBright: CSPair;
readonly bgGreenBright: CSPair;
readonly bgYellowBright: CSPair;
readonly bgBlueBright: CSPair;
readonly bgCyanBright: CSPair;
readonly bgMagentaBright: CSPair;
readonly bgWhiteBright: CSPair;
}
}
declare const ansiStyles: {
readonly modifier: ansiStyles.Modifier;
readonly color: ansiStyles.ForegroundColor & ansiStyles.ColorBase;
readonly bgColor: ansiStyles.BackgroundColor & ansiStyles.ColorBase;
readonly codes: ReadonlyMap<number, number>;
} & ansiStyles.BackgroundColor & ansiStyles.ForegroundColor & ansiStyles.Modifier;
export = ansiStyles;

View File

@@ -0,0 +1,163 @@
'use strict';
const wrapAnsi16 = (fn, offset) => (...args) => {
const code = fn(...args);
return `\u001B[${code + offset}m`;
};
const wrapAnsi256 = (fn, offset) => (...args) => {
const code = fn(...args);
return `\u001B[${38 + offset};5;${code}m`;
};
const wrapAnsi16m = (fn, offset) => (...args) => {
const rgb = fn(...args);
return `\u001B[${38 + offset};2;${rgb[0]};${rgb[1]};${rgb[2]}m`;
};
const ansi2ansi = n => n;
const rgb2rgb = (r, g, b) => [r, g, b];
const setLazyProperty = (object, property, get) => {
Object.defineProperty(object, property, {
get: () => {
const value = get();
Object.defineProperty(object, property, {
value,
enumerable: true,
configurable: true
});
return value;
},
enumerable: true,
configurable: true
});
};
/** @type {typeof import('color-convert')} */
let colorConvert;
const makeDynamicStyles = (wrap, targetSpace, identity, isBackground) => {
if (colorConvert === undefined) {
colorConvert = require('color-convert');
}
const offset = isBackground ? 10 : 0;
const styles = {};
for (const [sourceSpace, suite] of Object.entries(colorConvert)) {
const name = sourceSpace === 'ansi16' ? 'ansi' : sourceSpace;
if (sourceSpace === targetSpace) {
styles[name] = wrap(identity, offset);
} else if (typeof suite === 'object') {
styles[name] = wrap(suite[targetSpace], offset);
}
}
return styles;
};
function assembleStyles() {
const codes = new Map();
const styles = {
modifier: {
reset: [0, 0],
// 21 isn't widely supported and 22 does the same thing
bold: [1, 22],
dim: [2, 22],
italic: [3, 23],
underline: [4, 24],
inverse: [7, 27],
hidden: [8, 28],
strikethrough: [9, 29]
},
color: {
black: [30, 39],
red: [31, 39],
green: [32, 39],
yellow: [33, 39],
blue: [34, 39],
magenta: [35, 39],
cyan: [36, 39],
white: [37, 39],
// Bright color
blackBright: [90, 39],
redBright: [91, 39],
greenBright: [92, 39],
yellowBright: [93, 39],
blueBright: [94, 39],
magentaBright: [95, 39],
cyanBright: [96, 39],
whiteBright: [97, 39]
},
bgColor: {
bgBlack: [40, 49],
bgRed: [41, 49],
bgGreen: [42, 49],
bgYellow: [43, 49],
bgBlue: [44, 49],
bgMagenta: [45, 49],
bgCyan: [46, 49],
bgWhite: [47, 49],
// Bright color
bgBlackBright: [100, 49],
bgRedBright: [101, 49],
bgGreenBright: [102, 49],
bgYellowBright: [103, 49],
bgBlueBright: [104, 49],
bgMagentaBright: [105, 49],
bgCyanBright: [106, 49],
bgWhiteBright: [107, 49]
}
};
// Alias bright black as gray (and grey)
styles.color.gray = styles.color.blackBright;
styles.bgColor.bgGray = styles.bgColor.bgBlackBright;
styles.color.grey = styles.color.blackBright;
styles.bgColor.bgGrey = styles.bgColor.bgBlackBright;
for (const [groupName, group] of Object.entries(styles)) {
for (const [styleName, style] of Object.entries(group)) {
styles[styleName] = {
open: `\u001B[${style[0]}m`,
close: `\u001B[${style[1]}m`
};
group[styleName] = styles[styleName];
codes.set(style[0], style[1]);
}
Object.defineProperty(styles, groupName, {
value: group,
enumerable: false
});
}
Object.defineProperty(styles, 'codes', {
value: codes,
enumerable: false
});
styles.color.close = '\u001B[39m';
styles.bgColor.close = '\u001B[49m';
setLazyProperty(styles.color, 'ansi', () => makeDynamicStyles(wrapAnsi16, 'ansi16', ansi2ansi, false));
setLazyProperty(styles.color, 'ansi256', () => makeDynamicStyles(wrapAnsi256, 'ansi256', ansi2ansi, false));
setLazyProperty(styles.color, 'ansi16m', () => makeDynamicStyles(wrapAnsi16m, 'rgb', rgb2rgb, false));
setLazyProperty(styles.bgColor, 'ansi', () => makeDynamicStyles(wrapAnsi16, 'ansi16', ansi2ansi, true));
setLazyProperty(styles.bgColor, 'ansi256', () => makeDynamicStyles(wrapAnsi256, 'ansi256', ansi2ansi, true));
setLazyProperty(styles.bgColor, 'ansi16m', () => makeDynamicStyles(wrapAnsi16m, 'rgb', rgb2rgb, true));
return styles;
}
// Make the export immutable
Object.defineProperty(module, 'exports', {
enumerable: true,
get: assembleStyles
});

View File

@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,56 @@
{
"name": "ansi-styles",
"version": "4.3.0",
"description": "ANSI escape codes for styling strings in the terminal",
"license": "MIT",
"repository": "chalk/ansi-styles",
"funding": "https://github.com/chalk/ansi-styles?sponsor=1",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "sindresorhus.com"
},
"engines": {
"node": ">=8"
},
"scripts": {
"test": "xo && ava && tsd",
"screenshot": "svg-term --command='node screenshot' --out=screenshot.svg --padding=3 --width=55 --height=3 --at=1000 --no-cursor"
},
"files": [
"index.js",
"index.d.ts"
],
"keywords": [
"ansi",
"styles",
"color",
"colour",
"colors",
"terminal",
"console",
"cli",
"string",
"tty",
"escape",
"formatting",
"rgb",
"256",
"shell",
"xterm",
"log",
"logging",
"command-line",
"text"
],
"dependencies": {
"color-convert": "^2.0.1"
},
"devDependencies": {
"@types/color-convert": "^1.9.0",
"ava": "^2.3.0",
"svg-term-cli": "^2.1.1",
"tsd": "^0.11.0",
"xo": "^0.25.3"
}
}

View File

@@ -0,0 +1,152 @@
# ansi-styles [![Build Status](https://travis-ci.org/chalk/ansi-styles.svg?branch=master)](https://travis-ci.org/chalk/ansi-styles)
> [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors_and_Styles) for styling strings in the terminal
You probably want the higher-level [chalk](https://github.com/chalk/chalk) module for styling your strings.
<img src="screenshot.svg" width="900">
## Install
```
$ npm install ansi-styles
```
## Usage
```js
const style = require('ansi-styles');
console.log(`${style.green.open}Hello world!${style.green.close}`);
// Color conversion between 16/256/truecolor
// NOTE: If conversion goes to 16 colors or 256 colors, the original color
// may be degraded to fit that color palette. This means terminals
// that do not support 16 million colors will best-match the
// original color.
console.log(style.bgColor.ansi.hsl(120, 80, 72) + 'Hello world!' + style.bgColor.close);
console.log(style.color.ansi256.rgb(199, 20, 250) + 'Hello world!' + style.color.close);
console.log(style.color.ansi16m.hex('#abcdef') + 'Hello world!' + style.color.close);
```
## API
Each style has an `open` and `close` property.
## Styles
### Modifiers
- `reset`
- `bold`
- `dim`
- `italic` *(Not widely supported)*
- `underline`
- `inverse`
- `hidden`
- `strikethrough` *(Not widely supported)*
### Colors
- `black`
- `red`
- `green`
- `yellow`
- `blue`
- `magenta`
- `cyan`
- `white`
- `blackBright` (alias: `gray`, `grey`)
- `redBright`
- `greenBright`
- `yellowBright`
- `blueBright`
- `magentaBright`
- `cyanBright`
- `whiteBright`
### Background colors
- `bgBlack`
- `bgRed`
- `bgGreen`
- `bgYellow`
- `bgBlue`
- `bgMagenta`
- `bgCyan`
- `bgWhite`
- `bgBlackBright` (alias: `bgGray`, `bgGrey`)
- `bgRedBright`
- `bgGreenBright`
- `bgYellowBright`
- `bgBlueBright`
- `bgMagentaBright`
- `bgCyanBright`
- `bgWhiteBright`
## Advanced usage
By default, you get a map of styles, but the styles are also available as groups. They are non-enumerable so they don't show up unless you access them explicitly. This makes it easier to expose only a subset in a higher-level module.
- `style.modifier`
- `style.color`
- `style.bgColor`
###### Example
```js
console.log(style.color.green.open);
```
Raw escape codes (i.e. without the CSI escape prefix `\u001B[` and render mode postfix `m`) are available under `style.codes`, which returns a `Map` with the open codes as keys and close codes as values.
###### Example
```js
console.log(style.codes.get(36));
//=> 39
```
## [256 / 16 million (TrueColor) support](https://gist.github.com/XVilka/8346728)
`ansi-styles` uses the [`color-convert`](https://github.com/Qix-/color-convert) package to allow for converting between various colors and ANSI escapes, with support for 256 and 16 million colors.
The following color spaces from `color-convert` are supported:
- `rgb`
- `hex`
- `keyword`
- `hsl`
- `hsv`
- `hwb`
- `ansi`
- `ansi256`
To use these, call the associated conversion function with the intended output, for example:
```js
style.color.ansi.rgb(100, 200, 15); // RGB to 16 color ansi foreground code
style.bgColor.ansi.rgb(100, 200, 15); // RGB to 16 color ansi background code
style.color.ansi256.hsl(120, 100, 60); // HSL to 256 color ansi foreground code
style.bgColor.ansi256.hsl(120, 100, 60); // HSL to 256 color ansi foreground code
style.color.ansi16m.hex('#C0FFEE'); // Hex (RGB) to 16 million color foreground code
style.bgColor.ansi16m.hex('#C0FFEE'); // Hex (RGB) to 16 million color background code
```
## Related
- [ansi-escapes](https://github.com/sindresorhus/ansi-escapes) - ANSI escape codes for manipulating the terminal
## Maintainers
- [Sindre Sorhus](https://github.com/sindresorhus)
- [Josh Junon](https://github.com/qix-)
## For enterprise
Available as part of the Tidelift Subscription.
The maintainers of `ansi-styles` and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/npm-ansi-styles?utm_source=npm-ansi-styles&utm_medium=referral&utm_campaign=enterprise&utm_term=repo)

View File

@@ -0,0 +1,2 @@
tidelift: "npm/balanced-match"
patreon: juliangruber

View File

@@ -0,0 +1,21 @@
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,97 @@
# balanced-match
Match balanced string pairs, like `{` and `}` or `<b>` and `</b>`. Supports regular expressions as well!
[![build status](https://secure.travis-ci.org/juliangruber/balanced-match.svg)](http://travis-ci.org/juliangruber/balanced-match)
[![downloads](https://img.shields.io/npm/dm/balanced-match.svg)](https://www.npmjs.org/package/balanced-match)
[![testling badge](https://ci.testling.com/juliangruber/balanced-match.png)](https://ci.testling.com/juliangruber/balanced-match)
## Example
Get the first matching pair of braces:
```js
var balanced = require('balanced-match');
console.log(balanced('{', '}', 'pre{in{nested}}post'));
console.log(balanced('{', '}', 'pre{first}between{second}post'));
console.log(balanced(/\s+\{\s+/, /\s+\}\s+/, 'pre { in{nest} } post'));
```
The matches are:
```bash
$ node example.js
{ start: 3, end: 14, pre: 'pre', body: 'in{nested}', post: 'post' }
{ start: 3,
end: 9,
pre: 'pre',
body: 'first',
post: 'between{second}post' }
{ start: 3, end: 17, pre: 'pre', body: 'in{nest}', post: 'post' }
```
## API
### var m = balanced(a, b, str)
For the first non-nested matching pair of `a` and `b` in `str`, return an
object with those keys:
* **start** the index of the first match of `a`
* **end** the index of the matching `b`
* **pre** the preamble, `a` and `b` not included
* **body** the match, `a` and `b` not included
* **post** the postscript, `a` and `b` not included
If there's no match, `undefined` will be returned.
If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `['{', 'a', '']` and `{a}}` will match `['', 'a', '}']`.
### var r = balanced.range(a, b, str)
For the first non-nested matching pair of `a` and `b` in `str`, return an
array with indexes: `[ <a index>, <b index> ]`.
If there's no match, `undefined` will be returned.
If the `str` contains more `a` than `b` / there are unmatched pairs, the first match that was closed will be used. For example, `{{a}` will match `[ 1, 3 ]` and `{a}}` will match `[0, 2]`.
## Installation
With [npm](https://npmjs.org) do:
```bash
npm install balanced-match
```
## Security contact information
To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security).
Tidelift will coordinate the fix and disclosure.
## License
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,62 @@
'use strict';
module.exports = balanced;
function balanced(a, b, str) {
if (a instanceof RegExp) a = maybeMatch(a, str);
if (b instanceof RegExp) b = maybeMatch(b, str);
var r = range(a, b, str);
return r && {
start: r[0],
end: r[1],
pre: str.slice(0, r[0]),
body: str.slice(r[0] + a.length, r[1]),
post: str.slice(r[1] + b.length)
};
}
function maybeMatch(reg, str) {
var m = str.match(reg);
return m ? m[0] : null;
}
balanced.range = range;
function range(a, b, str) {
var begs, beg, left, right, result;
var ai = str.indexOf(a);
var bi = str.indexOf(b, ai + 1);
var i = ai;
if (ai >= 0 && bi > 0) {
if(a===b) {
return [ai, bi];
}
begs = [];
left = str.length;
while (i >= 0 && !result) {
if (i == ai) {
begs.push(i);
ai = str.indexOf(a, i + 1);
} else if (begs.length == 1) {
result = [ begs.pop(), bi ];
} else {
beg = begs.pop();
if (beg < left) {
left = beg;
right = bi;
}
bi = str.indexOf(b, i + 1);
}
i = ai < bi && ai >= 0 ? ai : bi;
}
if (begs.length) {
result = [ left, right ];
}
}
return result;
}

View File

@@ -0,0 +1,48 @@
{
"name": "balanced-match",
"description": "Match balanced character pairs, like \"{\" and \"}\"",
"version": "1.0.2",
"repository": {
"type": "git",
"url": "git://github.com/juliangruber/balanced-match.git"
},
"homepage": "https://github.com/juliangruber/balanced-match",
"main": "index.js",
"scripts": {
"test": "tape test/test.js",
"bench": "matcha test/bench.js"
},
"devDependencies": {
"matcha": "^0.7.0",
"tape": "^4.6.0"
},
"keywords": [
"match",
"regexp",
"test",
"balanced",
"parse"
],
"author": {
"name": "Julian Gruber",
"email": "mail@juliangruber.com",
"url": "http://juliangruber.com"
},
"license": "MIT",
"testling": {
"files": "test/*.js",
"browsers": [
"ie/8..latest",
"firefox/20..latest",
"firefox/nightly",
"chrome/25..latest",
"chrome/canary",
"opera/12..latest",
"opera/next",
"safari/5.1..latest",
"ipad/6.0..latest",
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
}
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,129 @@
# brace-expansion
[Brace expansion](https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html),
as known from sh/bash, in JavaScript.
[![build status](https://secure.travis-ci.org/juliangruber/brace-expansion.svg)](http://travis-ci.org/juliangruber/brace-expansion)
[![downloads](https://img.shields.io/npm/dm/brace-expansion.svg)](https://www.npmjs.org/package/brace-expansion)
[![Greenkeeper badge](https://badges.greenkeeper.io/juliangruber/brace-expansion.svg)](https://greenkeeper.io/)
[![testling badge](https://ci.testling.com/juliangruber/brace-expansion.png)](https://ci.testling.com/juliangruber/brace-expansion)
## Example
```js
var expand = require('brace-expansion');
expand('file-{a,b,c}.jpg')
// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg']
expand('-v{,,}')
// => ['-v', '-v', '-v']
expand('file{0..2}.jpg')
// => ['file0.jpg', 'file1.jpg', 'file2.jpg']
expand('file-{a..c}.jpg')
// => ['file-a.jpg', 'file-b.jpg', 'file-c.jpg']
expand('file{2..0}.jpg')
// => ['file2.jpg', 'file1.jpg', 'file0.jpg']
expand('file{0..4..2}.jpg')
// => ['file0.jpg', 'file2.jpg', 'file4.jpg']
expand('file-{a..e..2}.jpg')
// => ['file-a.jpg', 'file-c.jpg', 'file-e.jpg']
expand('file{00..10..5}.jpg')
// => ['file00.jpg', 'file05.jpg', 'file10.jpg']
expand('{{A..C},{a..c}}')
// => ['A', 'B', 'C', 'a', 'b', 'c']
expand('ppp{,config,oe{,conf}}')
// => ['ppp', 'pppconfig', 'pppoe', 'pppoeconf']
```
## API
```js
var expand = require('brace-expansion');
```
### var expanded = expand(str)
Return an array of all possible and valid expansions of `str`. If none are
found, `[str]` is returned.
Valid expansions are:
```js
/^(.*,)+(.+)?$/
// {a,b,...}
```
A comma separated list of options, like `{a,b}` or `{a,{b,c}}` or `{,a,}`.
```js
/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/
// {x..y[..incr]}
```
A numeric sequence from `x` to `y` inclusive, with optional increment.
If `x` or `y` start with a leading `0`, all the numbers will be padded
to have equal length. Negative numbers and backwards iteration work too.
```js
/^-?\d+\.\.-?\d+(\.\.-?\d+)?$/
// {x..y[..incr]}
```
An alphabetic sequence from `x` to `y` inclusive, with optional increment.
`x` and `y` must be exactly one character, and if given, `incr` must be a
number.
For compatibility reasons, the string `${` is not eligible for brace expansion.
## Installation
With [npm](https://npmjs.org) do:
```bash
npm install brace-expansion
```
## Contributors
- [Julian Gruber](https://github.com/juliangruber)
- [Isaac Z. Schlueter](https://github.com/isaacs)
## Sponsors
This module is proudly supported by my [Sponsors](https://github.com/juliangruber/sponsors)!
Do you want to support modules like this to improve their quality, stability and weigh in on new features? Then please consider donating to my [Patreon](https://www.patreon.com/juliangruber). Not sure how much of my modules you're using? Try [feross/thanks](https://github.com/feross/thanks)!
## License
(MIT)
Copyright (c) 2013 Julian Gruber &lt;julian@juliangruber.com&gt;
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,201 @@
var concatMap = require('concat-map');
var balanced = require('balanced-match');
module.exports = expandTop;
var escSlash = '\0SLASH'+Math.random()+'\0';
var escOpen = '\0OPEN'+Math.random()+'\0';
var escClose = '\0CLOSE'+Math.random()+'\0';
var escComma = '\0COMMA'+Math.random()+'\0';
var escPeriod = '\0PERIOD'+Math.random()+'\0';
function numeric(str) {
return parseInt(str, 10) == str
? parseInt(str, 10)
: str.charCodeAt(0);
}
function escapeBraces(str) {
return str.split('\\\\').join(escSlash)
.split('\\{').join(escOpen)
.split('\\}').join(escClose)
.split('\\,').join(escComma)
.split('\\.').join(escPeriod);
}
function unescapeBraces(str) {
return str.split(escSlash).join('\\')
.split(escOpen).join('{')
.split(escClose).join('}')
.split(escComma).join(',')
.split(escPeriod).join('.');
}
// Basically just str.split(","), but handling cases
// where we have nested braced sections, which should be
// treated as individual members, like {a,{b,c},d}
function parseCommaParts(str) {
if (!str)
return [''];
var parts = [];
var m = balanced('{', '}', str);
if (!m)
return str.split(',');
var pre = m.pre;
var body = m.body;
var post = m.post;
var p = pre.split(',');
p[p.length-1] += '{' + body + '}';
var postParts = parseCommaParts(post);
if (post.length) {
p[p.length-1] += postParts.shift();
p.push.apply(p, postParts);
}
parts.push.apply(parts, p);
return parts;
}
function expandTop(str) {
if (!str)
return [];
// I don't know why Bash 4.3 does this, but it does.
// Anything starting with {} will have the first two bytes preserved
// but *only* at the top level, so {},a}b will not expand to anything,
// but a{},b}c will be expanded to [a}c,abc].
// One could argue that this is a bug in Bash, but since the goal of
// this module is to match Bash's rules, we escape a leading {}
if (str.substr(0, 2) === '{}') {
str = '\\{\\}' + str.substr(2);
}
return expand(escapeBraces(str), true).map(unescapeBraces);
}
function identity(e) {
return e;
}
function embrace(str) {
return '{' + str + '}';
}
function isPadded(el) {
return /^-?0\d/.test(el);
}
function lte(i, y) {
return i <= y;
}
function gte(i, y) {
return i >= y;
}
function expand(str, isTop) {
var expansions = [];
var m = balanced('{', '}', str);
if (!m || /\$$/.test(m.pre)) return [str];
var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body);
var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body);
var isSequence = isNumericSequence || isAlphaSequence;
var isOptions = m.body.indexOf(',') >= 0;
if (!isSequence && !isOptions) {
// {a},b}
if (m.post.match(/,(?!,).*\}/)) {
str = m.pre + '{' + m.body + escClose + m.post;
return expand(str);
}
return [str];
}
var n;
if (isSequence) {
n = m.body.split(/\.\./);
} else {
n = parseCommaParts(m.body);
if (n.length === 1) {
// x{{a,b}}y ==> x{a}y x{b}y
n = expand(n[0], false).map(embrace);
if (n.length === 1) {
var post = m.post.length
? expand(m.post, false)
: [''];
return post.map(function(p) {
return m.pre + n[0] + p;
});
}
}
}
// at this point, n is the parts, and we know it's not a comma set
// with a single entry.
// no need to expand pre, since it is guaranteed to be free of brace-sets
var pre = m.pre;
var post = m.post.length
? expand(m.post, false)
: [''];
var N;
if (isSequence) {
var x = numeric(n[0]);
var y = numeric(n[1]);
var width = Math.max(n[0].length, n[1].length)
var incr = n.length == 3
? Math.abs(numeric(n[2]))
: 1;
var test = lte;
var reverse = y < x;
if (reverse) {
incr *= -1;
test = gte;
}
var pad = n.some(isPadded);
N = [];
for (var i = x; test(i, y); i += incr) {
var c;
if (isAlphaSequence) {
c = String.fromCharCode(i);
if (c === '\\')
c = '';
} else {
c = String(i);
if (pad) {
var need = width - c.length;
if (need > 0) {
var z = new Array(need + 1).join('0');
if (i < 0)
c = '-' + z + c.slice(1);
else
c = z + c;
}
}
}
N.push(c);
}
} else {
N = concatMap(n, function(el) { return expand(el, false) });
}
for (var j = 0; j < N.length; j++) {
for (var k = 0; k < post.length; k++) {
var expansion = pre + N[j] + post[k];
if (!isTop || isSequence || expansion)
expansions.push(expansion);
}
}
return expansions;
}

View File

@@ -0,0 +1,50 @@
{
"name": "brace-expansion",
"description": "Brace expansion as known from sh/bash",
"version": "1.1.12",
"repository": {
"type": "git",
"url": "git://github.com/juliangruber/brace-expansion.git"
},
"homepage": "https://github.com/juliangruber/brace-expansion",
"main": "index.js",
"scripts": {
"test": "tape test/*.js",
"gentest": "bash test/generate.sh",
"bench": "matcha test/perf/bench.js"
},
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
},
"devDependencies": {
"matcha": "^0.7.0",
"tape": "^4.6.0"
},
"keywords": [],
"author": {
"name": "Julian Gruber",
"email": "mail@juliangruber.com",
"url": "http://juliangruber.com"
},
"license": "MIT",
"testling": {
"files": "test/*.js",
"browsers": [
"ie/8..latest",
"firefox/20..latest",
"firefox/nightly",
"chrome/25..latest",
"chrome/canary",
"opera/12..latest",
"opera/next",
"safari/5.1..latest",
"ipad/6.0..latest",
"iphone/6.0..latest",
"android-browser/4.2..latest"
]
},
"publishConfig": {
"tag": "1.x"
}
}

View File

@@ -0,0 +1,121 @@
# Change Log
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [7.0.4](https://www.github.com/yargs/cliui/compare/v7.0.3...v7.0.4) (2020-11-08)
### Bug Fixes
* **deno:** import UIOptions from definitions ([#97](https://www.github.com/yargs/cliui/issues/97)) ([f04f343](https://www.github.com/yargs/cliui/commit/f04f3439bc78114c7e90f82ff56f5acf16268ea8))
### [7.0.3](https://www.github.com/yargs/cliui/compare/v7.0.2...v7.0.3) (2020-10-16)
### Bug Fixes
* **exports:** node 13.0 and 13.1 require the dotted object form _with_ a string fallback ([#93](https://www.github.com/yargs/cliui/issues/93)) ([eca16fc](https://www.github.com/yargs/cliui/commit/eca16fc05d26255df3280906c36d7f0e5b05c6e9))
### [7.0.2](https://www.github.com/yargs/cliui/compare/v7.0.1...v7.0.2) (2020-10-14)
### Bug Fixes
* **exports:** node 13.0-13.6 require a string fallback ([#91](https://www.github.com/yargs/cliui/issues/91)) ([b529d7e](https://www.github.com/yargs/cliui/commit/b529d7e432901af1af7848b23ed6cf634497d961))
### [7.0.1](https://www.github.com/yargs/cliui/compare/v7.0.0...v7.0.1) (2020-08-16)
### Bug Fixes
* **build:** main should be build/index.cjs ([dc29a3c](https://www.github.com/yargs/cliui/commit/dc29a3cc617a410aa850e06337b5954b04f2cb4d))
## [7.0.0](https://www.github.com/yargs/cliui/compare/v6.0.0...v7.0.0) (2020-08-16)
### ⚠ BREAKING CHANGES
* tsc/ESM/Deno support (#82)
* modernize deps and build (#80)
### Build System
* modernize deps and build ([#80](https://www.github.com/yargs/cliui/issues/80)) ([339d08d](https://www.github.com/yargs/cliui/commit/339d08dc71b15a3928aeab09042af94db2f43743))
### Code Refactoring
* tsc/ESM/Deno support ([#82](https://www.github.com/yargs/cliui/issues/82)) ([4b777a5](https://www.github.com/yargs/cliui/commit/4b777a5fe01c5d8958c6708695d6aab7dbe5706c))
## [6.0.0](https://www.github.com/yargs/cliui/compare/v5.0.0...v6.0.0) (2019-11-10)
### ⚠ BREAKING CHANGES
* update deps, drop Node 6
### Code Refactoring
* update deps, drop Node 6 ([62056df](https://www.github.com/yargs/cliui/commit/62056df))
## [5.0.0](https://github.com/yargs/cliui/compare/v4.1.0...v5.0.0) (2019-04-10)
### Bug Fixes
* Update wrap-ansi to fix compatibility with latest versions of chalk. ([#60](https://github.com/yargs/cliui/issues/60)) ([7bf79ae](https://github.com/yargs/cliui/commit/7bf79ae))
### BREAKING CHANGES
* Drop support for node < 6.
<a name="4.1.0"></a>
## [4.1.0](https://github.com/yargs/cliui/compare/v4.0.0...v4.1.0) (2018-04-23)
### Features
* add resetOutput method ([#57](https://github.com/yargs/cliui/issues/57)) ([7246902](https://github.com/yargs/cliui/commit/7246902))
<a name="4.0.0"></a>
## [4.0.0](https://github.com/yargs/cliui/compare/v3.2.0...v4.0.0) (2017-12-18)
### Bug Fixes
* downgrades strip-ansi to version 3.0.1 ([#54](https://github.com/yargs/cliui/issues/54)) ([5764c46](https://github.com/yargs/cliui/commit/5764c46))
* set env variable FORCE_COLOR. ([#56](https://github.com/yargs/cliui/issues/56)) ([7350e36](https://github.com/yargs/cliui/commit/7350e36))
### Chores
* drop support for node < 4 ([#53](https://github.com/yargs/cliui/issues/53)) ([b105376](https://github.com/yargs/cliui/commit/b105376))
### Features
* add fallback for window width ([#45](https://github.com/yargs/cliui/issues/45)) ([d064922](https://github.com/yargs/cliui/commit/d064922))
### BREAKING CHANGES
* officially drop support for Node < 4
<a name="3.2.0"></a>
## [3.2.0](https://github.com/yargs/cliui/compare/v3.1.2...v3.2.0) (2016-04-11)
### Bug Fixes
* reduces tarball size ([acc6c33](https://github.com/yargs/cliui/commit/acc6c33))
### Features
* adds standard-version for release management ([ff84e32](https://github.com/yargs/cliui/commit/ff84e32))

View File

@@ -0,0 +1,14 @@
Copyright (c) 2015, Contributors
Permission to use, copy, modify, and/or distribute this software
for any purpose with or without fee is hereby granted, provided
that the above copyright notice and this permission notice
appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE
LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES
OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@@ -0,0 +1,141 @@
# cliui
![ci](https://github.com/yargs/cliui/workflows/ci/badge.svg)
[![NPM version](https://img.shields.io/npm/v/cliui.svg)](https://www.npmjs.com/package/cliui)
[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
![nycrc config on GitHub](https://img.shields.io/nycrc/yargs/cliui)
easily create complex multi-column command-line-interfaces.
## Example
```js
const ui = require('cliui')()
ui.div('Usage: $0 [command] [options]')
ui.div({
text: 'Options:',
padding: [2, 0, 1, 0]
})
ui.div(
{
text: "-f, --file",
width: 20,
padding: [0, 4, 0, 4]
},
{
text: "the file to load." +
chalk.green("(if this description is long it wraps).")
,
width: 20
},
{
text: chalk.red("[required]"),
align: 'right'
}
)
console.log(ui.toString())
```
## Deno/ESM Support
As of `v7` `cliui` supports [Deno](https://github.com/denoland/deno) and
[ESM](https://nodejs.org/api/esm.html#esm_ecmascript_modules):
```typescript
import cliui from "https://deno.land/x/cliui/deno.ts";
const ui = cliui({})
ui.div('Usage: $0 [command] [options]')
ui.div({
text: 'Options:',
padding: [2, 0, 1, 0]
})
ui.div({
text: "-f, --file",
width: 20,
padding: [0, 4, 0, 4]
})
console.log(ui.toString())
```
<img width="500" src="screenshot.png">
## Layout DSL
cliui exposes a simple layout DSL:
If you create a single `ui.div`, passing a string rather than an
object:
* `\n`: characters will be interpreted as new rows.
* `\t`: characters will be interpreted as new columns.
* `\s`: characters will be interpreted as padding.
**as an example...**
```js
var ui = require('./')({
width: 60
})
ui.div(
'Usage: node ./bin/foo.js\n' +
' <regex>\t provide a regex\n' +
' <glob>\t provide a glob\t [required]'
)
console.log(ui.toString())
```
**will output:**
```shell
Usage: node ./bin/foo.js
<regex> provide a regex
<glob> provide a glob [required]
```
## Methods
```js
cliui = require('cliui')
```
### cliui({width: integer})
Specify the maximum width of the UI being generated.
If no width is provided, cliui will try to get the current window's width and use it, and if that doesn't work, width will be set to `80`.
### cliui({wrap: boolean})
Enable or disable the wrapping of text in a column.
### cliui.div(column, column, column)
Create a row with any number of columns, a column
can either be a string, or an object with the following
options:
* **text:** some text to place in the column.
* **width:** the width of a column.
* **align:** alignment, `right` or `center`.
* **padding:** `[top, right, bottom, left]`.
* **border:** should a border be placed around the div?
### cliui.span(column, column, column)
Similar to `div`, except the next row will be appended without
a new line being created.
### cliui.resetOutput()
Resets the UI elements of the current cliui instance, maintaining the values
set for `width` and `wrap`.

View File

@@ -0,0 +1,302 @@
'use strict';
const align = {
right: alignRight,
center: alignCenter
};
const top = 0;
const right = 1;
const bottom = 2;
const left = 3;
class UI {
constructor(opts) {
var _a;
this.width = opts.width;
this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
this.rows = [];
}
span(...args) {
const cols = this.div(...args);
cols.span = true;
}
resetOutput() {
this.rows = [];
}
div(...args) {
if (args.length === 0) {
this.div('');
}
if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
return this.applyLayoutDSL(args[0]);
}
const cols = args.map(arg => {
if (typeof arg === 'string') {
return this.colFromString(arg);
}
return arg;
});
this.rows.push(cols);
return cols;
}
shouldApplyLayoutDSL(...args) {
return args.length === 1 && typeof args[0] === 'string' &&
/[\t\n]/.test(args[0]);
}
applyLayoutDSL(str) {
const rows = str.split('\n').map(row => row.split('\t'));
let leftColumnWidth = 0;
// simple heuristic for layout, make sure the
// second column lines up along the left-hand.
// don't allow the first column to take up more
// than 50% of the screen.
rows.forEach(columns => {
if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
}
});
// generate a table:
// replacing ' ' with padding calculations.
// using the algorithmically generated width.
rows.forEach(columns => {
this.div(...columns.map((r, i) => {
return {
text: r.trim(),
padding: this.measurePadding(r),
width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
};
}));
});
return this.rows[this.rows.length - 1];
}
colFromString(text) {
return {
text,
padding: this.measurePadding(text)
};
}
measurePadding(str) {
// measure padding without ansi escape codes
const noAnsi = mixin.stripAnsi(str);
return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
}
toString() {
const lines = [];
this.rows.forEach(row => {
this.rowToString(row, lines);
});
// don't display any lines with the
// hidden flag set.
return lines
.filter(line => !line.hidden)
.map(line => line.text)
.join('\n');
}
rowToString(row, lines) {
this.rasterize(row).forEach((rrow, r) => {
let str = '';
rrow.forEach((col, c) => {
const { width } = row[c]; // the width with padding.
const wrapWidth = this.negatePadding(row[c]); // the width without padding.
let ts = col; // temporary string used during alignment/padding.
if (wrapWidth > mixin.stringWidth(col)) {
ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
}
// align the string within its column.
if (row[c].align && row[c].align !== 'left' && this.wrap) {
const fn = align[row[c].align];
ts = fn(ts, wrapWidth);
if (mixin.stringWidth(ts) < wrapWidth) {
ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1);
}
}
// apply border and padding to string.
const padding = row[c].padding || [0, 0, 0, 0];
if (padding[left]) {
str += ' '.repeat(padding[left]);
}
str += addBorder(row[c], ts, '| ');
str += ts;
str += addBorder(row[c], ts, ' |');
if (padding[right]) {
str += ' '.repeat(padding[right]);
}
// if prior row is span, try to render the
// current row on the prior line.
if (r === 0 && lines.length > 0) {
str = this.renderInline(str, lines[lines.length - 1]);
}
});
// remove trailing whitespace.
lines.push({
text: str.replace(/ +$/, ''),
span: row.span
});
});
return lines;
}
// if the full 'source' can render in
// the target line, do so.
renderInline(source, previousLine) {
const match = source.match(/^ */);
const leadingWhitespace = match ? match[0].length : 0;
const target = previousLine.text;
const targetTextWidth = mixin.stringWidth(target.trimRight());
if (!previousLine.span) {
return source;
}
// if we're not applying wrapping logic,
// just always append to the span.
if (!this.wrap) {
previousLine.hidden = true;
return target + source;
}
if (leadingWhitespace < targetTextWidth) {
return source;
}
previousLine.hidden = true;
return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft();
}
rasterize(row) {
const rrows = [];
const widths = this.columnWidths(row);
let wrapped;
// word wrap all columns, and create
// a data-structure that is easy to rasterize.
row.forEach((col, c) => {
// leave room for left and right padding.
col.width = widths[c];
if (this.wrap) {
wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
}
else {
wrapped = col.text.split('\n');
}
if (col.border) {
wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
}
// add top and bottom padding.
if (col.padding) {
wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
}
wrapped.forEach((str, r) => {
if (!rrows[r]) {
rrows.push([]);
}
const rrow = rrows[r];
for (let i = 0; i < c; i++) {
if (rrow[i] === undefined) {
rrow.push('');
}
}
rrow.push(str);
});
});
return rrows;
}
negatePadding(col) {
let wrapWidth = col.width || 0;
if (col.padding) {
wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
}
if (col.border) {
wrapWidth -= 4;
}
return wrapWidth;
}
columnWidths(row) {
if (!this.wrap) {
return row.map(col => {
return col.width || mixin.stringWidth(col.text);
});
}
let unset = row.length;
let remainingWidth = this.width;
// column widths can be set in config.
const widths = row.map(col => {
if (col.width) {
unset--;
remainingWidth -= col.width;
return col.width;
}
return undefined;
});
// any unset widths should be calculated.
const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
return widths.map((w, i) => {
if (w === undefined) {
return Math.max(unsetWidth, _minWidth(row[i]));
}
return w;
});
}
}
function addBorder(col, ts, style) {
if (col.border) {
if (/[.']-+[.']/.test(ts)) {
return '';
}
if (ts.trim().length !== 0) {
return style;
}
return ' ';
}
return '';
}
// calculates the minimum width of
// a column, based on padding preferences.
function _minWidth(col) {
const padding = col.padding || [];
const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
if (col.border) {
return minWidth + 4;
}
return minWidth;
}
function getWindowWidth() {
/* istanbul ignore next: depends on terminal */
if (typeof process === 'object' && process.stdout && process.stdout.columns) {
return process.stdout.columns;
}
return 80;
}
function alignRight(str, width) {
str = str.trim();
const strWidth = mixin.stringWidth(str);
if (strWidth < width) {
return ' '.repeat(width - strWidth) + str;
}
return str;
}
function alignCenter(str, width) {
str = str.trim();
const strWidth = mixin.stringWidth(str);
/* istanbul ignore next */
if (strWidth >= width) {
return str;
}
return ' '.repeat((width - strWidth) >> 1) + str;
}
let mixin;
function cliui(opts, _mixin) {
mixin = _mixin;
return new UI({
width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
});
}
// Bootstrap cliui with CommonJS dependencies:
const stringWidth = require('string-width');
const stripAnsi = require('strip-ansi');
const wrap = require('wrap-ansi');
function ui(opts) {
return cliui(opts, {
stringWidth,
stripAnsi,
wrap
});
}
module.exports = ui;

View File

@@ -0,0 +1,287 @@
'use strict';
const align = {
right: alignRight,
center: alignCenter
};
const top = 0;
const right = 1;
const bottom = 2;
const left = 3;
export class UI {
constructor(opts) {
var _a;
this.width = opts.width;
this.wrap = (_a = opts.wrap) !== null && _a !== void 0 ? _a : true;
this.rows = [];
}
span(...args) {
const cols = this.div(...args);
cols.span = true;
}
resetOutput() {
this.rows = [];
}
div(...args) {
if (args.length === 0) {
this.div('');
}
if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string') {
return this.applyLayoutDSL(args[0]);
}
const cols = args.map(arg => {
if (typeof arg === 'string') {
return this.colFromString(arg);
}
return arg;
});
this.rows.push(cols);
return cols;
}
shouldApplyLayoutDSL(...args) {
return args.length === 1 && typeof args[0] === 'string' &&
/[\t\n]/.test(args[0]);
}
applyLayoutDSL(str) {
const rows = str.split('\n').map(row => row.split('\t'));
let leftColumnWidth = 0;
// simple heuristic for layout, make sure the
// second column lines up along the left-hand.
// don't allow the first column to take up more
// than 50% of the screen.
rows.forEach(columns => {
if (columns.length > 1 && mixin.stringWidth(columns[0]) > leftColumnWidth) {
leftColumnWidth = Math.min(Math.floor(this.width * 0.5), mixin.stringWidth(columns[0]));
}
});
// generate a table:
// replacing ' ' with padding calculations.
// using the algorithmically generated width.
rows.forEach(columns => {
this.div(...columns.map((r, i) => {
return {
text: r.trim(),
padding: this.measurePadding(r),
width: (i === 0 && columns.length > 1) ? leftColumnWidth : undefined
};
}));
});
return this.rows[this.rows.length - 1];
}
colFromString(text) {
return {
text,
padding: this.measurePadding(text)
};
}
measurePadding(str) {
// measure padding without ansi escape codes
const noAnsi = mixin.stripAnsi(str);
return [0, noAnsi.match(/\s*$/)[0].length, 0, noAnsi.match(/^\s*/)[0].length];
}
toString() {
const lines = [];
this.rows.forEach(row => {
this.rowToString(row, lines);
});
// don't display any lines with the
// hidden flag set.
return lines
.filter(line => !line.hidden)
.map(line => line.text)
.join('\n');
}
rowToString(row, lines) {
this.rasterize(row).forEach((rrow, r) => {
let str = '';
rrow.forEach((col, c) => {
const { width } = row[c]; // the width with padding.
const wrapWidth = this.negatePadding(row[c]); // the width without padding.
let ts = col; // temporary string used during alignment/padding.
if (wrapWidth > mixin.stringWidth(col)) {
ts += ' '.repeat(wrapWidth - mixin.stringWidth(col));
}
// align the string within its column.
if (row[c].align && row[c].align !== 'left' && this.wrap) {
const fn = align[row[c].align];
ts = fn(ts, wrapWidth);
if (mixin.stringWidth(ts) < wrapWidth) {
ts += ' '.repeat((width || 0) - mixin.stringWidth(ts) - 1);
}
}
// apply border and padding to string.
const padding = row[c].padding || [0, 0, 0, 0];
if (padding[left]) {
str += ' '.repeat(padding[left]);
}
str += addBorder(row[c], ts, '| ');
str += ts;
str += addBorder(row[c], ts, ' |');
if (padding[right]) {
str += ' '.repeat(padding[right]);
}
// if prior row is span, try to render the
// current row on the prior line.
if (r === 0 && lines.length > 0) {
str = this.renderInline(str, lines[lines.length - 1]);
}
});
// remove trailing whitespace.
lines.push({
text: str.replace(/ +$/, ''),
span: row.span
});
});
return lines;
}
// if the full 'source' can render in
// the target line, do so.
renderInline(source, previousLine) {
const match = source.match(/^ */);
const leadingWhitespace = match ? match[0].length : 0;
const target = previousLine.text;
const targetTextWidth = mixin.stringWidth(target.trimRight());
if (!previousLine.span) {
return source;
}
// if we're not applying wrapping logic,
// just always append to the span.
if (!this.wrap) {
previousLine.hidden = true;
return target + source;
}
if (leadingWhitespace < targetTextWidth) {
return source;
}
previousLine.hidden = true;
return target.trimRight() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimLeft();
}
rasterize(row) {
const rrows = [];
const widths = this.columnWidths(row);
let wrapped;
// word wrap all columns, and create
// a data-structure that is easy to rasterize.
row.forEach((col, c) => {
// leave room for left and right padding.
col.width = widths[c];
if (this.wrap) {
wrapped = mixin.wrap(col.text, this.negatePadding(col), { hard: true }).split('\n');
}
else {
wrapped = col.text.split('\n');
}
if (col.border) {
wrapped.unshift('.' + '-'.repeat(this.negatePadding(col) + 2) + '.');
wrapped.push("'" + '-'.repeat(this.negatePadding(col) + 2) + "'");
}
// add top and bottom padding.
if (col.padding) {
wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
}
wrapped.forEach((str, r) => {
if (!rrows[r]) {
rrows.push([]);
}
const rrow = rrows[r];
for (let i = 0; i < c; i++) {
if (rrow[i] === undefined) {
rrow.push('');
}
}
rrow.push(str);
});
});
return rrows;
}
negatePadding(col) {
let wrapWidth = col.width || 0;
if (col.padding) {
wrapWidth -= (col.padding[left] || 0) + (col.padding[right] || 0);
}
if (col.border) {
wrapWidth -= 4;
}
return wrapWidth;
}
columnWidths(row) {
if (!this.wrap) {
return row.map(col => {
return col.width || mixin.stringWidth(col.text);
});
}
let unset = row.length;
let remainingWidth = this.width;
// column widths can be set in config.
const widths = row.map(col => {
if (col.width) {
unset--;
remainingWidth -= col.width;
return col.width;
}
return undefined;
});
// any unset widths should be calculated.
const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
return widths.map((w, i) => {
if (w === undefined) {
return Math.max(unsetWidth, _minWidth(row[i]));
}
return w;
});
}
}
function addBorder(col, ts, style) {
if (col.border) {
if (/[.']-+[.']/.test(ts)) {
return '';
}
if (ts.trim().length !== 0) {
return style;
}
return ' ';
}
return '';
}
// calculates the minimum width of
// a column, based on padding preferences.
function _minWidth(col) {
const padding = col.padding || [];
const minWidth = 1 + (padding[left] || 0) + (padding[right] || 0);
if (col.border) {
return minWidth + 4;
}
return minWidth;
}
function getWindowWidth() {
/* istanbul ignore next: depends on terminal */
if (typeof process === 'object' && process.stdout && process.stdout.columns) {
return process.stdout.columns;
}
return 80;
}
function alignRight(str, width) {
str = str.trim();
const strWidth = mixin.stringWidth(str);
if (strWidth < width) {
return ' '.repeat(width - strWidth) + str;
}
return str;
}
function alignCenter(str, width) {
str = str.trim();
const strWidth = mixin.stringWidth(str);
/* istanbul ignore next */
if (strWidth >= width) {
return str;
}
return ' '.repeat((width - strWidth) >> 1) + str;
}
let mixin;
export function cliui(opts, _mixin) {
mixin = _mixin;
return new UI({
width: (opts === null || opts === void 0 ? void 0 : opts.width) || getWindowWidth(),
wrap: opts === null || opts === void 0 ? void 0 : opts.wrap
});
}

View File

@@ -0,0 +1,27 @@
// Minimal replacement for ansi string helpers "wrap-ansi" and "strip-ansi".
// to facilitate ESM and Deno modules.
// TODO: look at porting https://www.npmjs.com/package/wrap-ansi to ESM.
// The npm application
// Copyright (c) npm, Inc. and Contributors
// Licensed on the terms of The Artistic License 2.0
// See: https://github.com/npm/cli/blob/4c65cd952bc8627811735bea76b9b110cc4fc80e/lib/utils/ansi-trim.js
const ansi = new RegExp('\x1b(?:\\[(?:\\d+[ABCDEFGJKSTm]|\\d+;\\d+[Hfm]|' +
'\\d+;\\d+;\\d+m|6n|s|u|\\?25[lh])|\\w)', 'g');
export function stripAnsi(str) {
return str.replace(ansi, '');
}
export function wrap(str, width) {
const [start, end] = str.match(ansi) || ['', ''];
str = stripAnsi(str);
let wrapped = '';
for (let i = 0; i < str.length; i++) {
if (i !== 0 && (i % width) === 0) {
wrapped += '\n';
}
wrapped += str.charAt(i);
}
if (start && end) {
wrapped = `${start}${wrapped}${end}`;
}
return wrapped;
}

View File

@@ -0,0 +1,13 @@
// Bootstrap cliui with CommonJS dependencies:
import { cliui } from './build/lib/index.js'
import { wrap, stripAnsi } from './build/lib/string-utils.js'
export default function ui (opts) {
return cliui(opts, {
stringWidth: (str) => {
return [...str].length
},
stripAnsi,
wrap
})
}

View File

@@ -0,0 +1,83 @@
{
"name": "cliui",
"version": "7.0.4",
"description": "easily create complex multi-column command-line-interfaces",
"main": "build/index.cjs",
"exports": {
".": [
{
"import": "./index.mjs",
"require": "./build/index.cjs"
},
"./build/index.cjs"
]
},
"type": "module",
"module": "./index.mjs",
"scripts": {
"check": "standardx '**/*.ts' && standardx '**/*.js' && standardx '**/*.cjs'",
"fix": "standardx --fix '**/*.ts' && standardx --fix '**/*.js' && standardx --fix '**/*.cjs'",
"pretest": "rimraf build && tsc -p tsconfig.test.json && cross-env NODE_ENV=test npm run build:cjs",
"test": "c8 mocha ./test/*.cjs",
"test:esm": "c8 mocha ./test/esm/cliui-test.mjs",
"postest": "check",
"coverage": "c8 report --check-coverage",
"precompile": "rimraf build",
"compile": "tsc",
"postcompile": "npm run build:cjs",
"build:cjs": "rollup -c",
"prepare": "npm run compile"
},
"repository": "yargs/cliui",
"standard": {
"ignore": [
"**/example/**"
],
"globals": [
"it"
]
},
"keywords": [
"cli",
"command-line",
"layout",
"design",
"console",
"wrap",
"table"
],
"author": "Ben Coe <ben@npmjs.com>",
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
},
"devDependencies": {
"@types/node": "^14.0.27",
"@typescript-eslint/eslint-plugin": "^4.0.0",
"@typescript-eslint/parser": "^4.0.0",
"@wessberg/rollup-plugin-ts": "^1.3.2",
"c8": "^7.3.0",
"chai": "^4.2.0",
"chalk": "^4.1.0",
"cross-env": "^7.0.2",
"eslint": "^7.6.0",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"gts": "^3.0.0",
"mocha": "^8.1.1",
"rimraf": "^3.0.2",
"rollup": "^2.23.1",
"standardx": "^7.0.0",
"typescript": "^4.0.0"
},
"files": [
"build",
"index.mjs",
"!*.d.ts"
],
"engine": {
"node": ">=10"
}
}

View File

@@ -0,0 +1,54 @@
# 1.0.0 - 2016-01-07
- Removed: unused speed test
- Added: Automatic routing between previously unsupported conversions
([#27](https://github.com/Qix-/color-convert/pull/27))
- Removed: `xxx2xxx()` and `xxx2xxxRaw()` functions
([#27](https://github.com/Qix-/color-convert/pull/27))
- Removed: `convert()` class
([#27](https://github.com/Qix-/color-convert/pull/27))
- Changed: all functions to lookup dictionary
([#27](https://github.com/Qix-/color-convert/pull/27))
- Changed: `ansi` to `ansi256`
([#27](https://github.com/Qix-/color-convert/pull/27))
- Fixed: argument grouping for functions requiring only one argument
([#27](https://github.com/Qix-/color-convert/pull/27))
# 0.6.0 - 2015-07-23
- Added: methods to handle
[ANSI](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) 16/256 colors:
- rgb2ansi16
- rgb2ansi
- hsl2ansi16
- hsl2ansi
- hsv2ansi16
- hsv2ansi
- hwb2ansi16
- hwb2ansi
- cmyk2ansi16
- cmyk2ansi
- keyword2ansi16
- keyword2ansi
- ansi162rgb
- ansi162hsl
- ansi162hsv
- ansi162hwb
- ansi162cmyk
- ansi162keyword
- ansi2rgb
- ansi2hsl
- ansi2hsv
- ansi2hwb
- ansi2cmyk
- ansi2keyword
([#18](https://github.com/harthur/color-convert/pull/18))
# 0.5.3 - 2015-06-02
- Fixed: hsl2hsv does not return `NaN` anymore when using `[0,0,0]`
([#15](https://github.com/harthur/color-convert/issues/15))
---
Check out commit logs for older releases

View File

@@ -0,0 +1,21 @@
Copyright (c) 2011-2016 Heather Arthur <fayearthur@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,68 @@
# color-convert
[![Build Status](https://travis-ci.org/Qix-/color-convert.svg?branch=master)](https://travis-ci.org/Qix-/color-convert)
Color-convert is a color conversion library for JavaScript and node.
It converts all ways between `rgb`, `hsl`, `hsv`, `hwb`, `cmyk`, `ansi`, `ansi16`, `hex` strings, and CSS `keyword`s (will round to closest):
```js
var convert = require('color-convert');
convert.rgb.hsl(140, 200, 100); // [96, 48, 59]
convert.keyword.rgb('blue'); // [0, 0, 255]
var rgbChannels = convert.rgb.channels; // 3
var cmykChannels = convert.cmyk.channels; // 4
var ansiChannels = convert.ansi16.channels; // 1
```
# Install
```console
$ npm install color-convert
```
# API
Simply get the property of the _from_ and _to_ conversion that you're looking for.
All functions have a rounded and unrounded variant. By default, return values are rounded. To get the unrounded (raw) results, simply tack on `.raw` to the function.
All 'from' functions have a hidden property called `.channels` that indicates the number of channels the function expects (not including alpha).
```js
var convert = require('color-convert');
// Hex to LAB
convert.hex.lab('DEADBF'); // [ 76, 21, -2 ]
convert.hex.lab.raw('DEADBF'); // [ 75.56213190997677, 20.653827952644754, -2.290532499330533 ]
// RGB to CMYK
convert.rgb.cmyk(167, 255, 4); // [ 35, 0, 98, 0 ]
convert.rgb.cmyk.raw(167, 255, 4); // [ 34.509803921568626, 0, 98.43137254901961, 0 ]
```
### Arrays
All functions that accept multiple arguments also support passing an array.
Note that this does **not** apply to functions that convert from a color that only requires one value (e.g. `keyword`, `ansi256`, `hex`, etc.)
```js
var convert = require('color-convert');
convert.rgb.hex(123, 45, 67); // '7B2D43'
convert.rgb.hex([123, 45, 67]); // '7B2D43'
```
## Routing
Conversions that don't have an _explicitly_ defined conversion (in [conversions.js](conversions.js)), but can be converted by means of sub-conversions (e.g. XYZ -> **RGB** -> CMYK), are automatically routed together. This allows just about any color model supported by `color-convert` to be converted to any other model, so long as a sub-conversion path exists. This is also true for conversions requiring more than one step in between (e.g. LCH -> **LAB** -> **XYZ** -> **RGB** -> Hex).
Keep in mind that extensive conversions _may_ result in a loss of precision, and exist only to be complete. For a list of "direct" (single-step) conversions, see [conversions.js](conversions.js).
# Contribute
If there is a new model you would like to support, or want to add a direct conversion between two existing models, please send us a pull request.
# License
Copyright &copy; 2011-2016, Heather Arthur and Josh Junon. Licensed under the [MIT License](LICENSE).

View File

@@ -0,0 +1,839 @@
/* MIT license */
/* eslint-disable no-mixed-operators */
const cssKeywords = require('color-name');
// NOTE: conversions should only return primitive values (i.e. arrays, or
// values that give correct `typeof` results).
// do not use box values types (i.e. Number(), String(), etc.)
const reverseKeywords = {};
for (const key of Object.keys(cssKeywords)) {
reverseKeywords[cssKeywords[key]] = key;
}
const convert = {
rgb: {channels: 3, labels: 'rgb'},
hsl: {channels: 3, labels: 'hsl'},
hsv: {channels: 3, labels: 'hsv'},
hwb: {channels: 3, labels: 'hwb'},
cmyk: {channels: 4, labels: 'cmyk'},
xyz: {channels: 3, labels: 'xyz'},
lab: {channels: 3, labels: 'lab'},
lch: {channels: 3, labels: 'lch'},
hex: {channels: 1, labels: ['hex']},
keyword: {channels: 1, labels: ['keyword']},
ansi16: {channels: 1, labels: ['ansi16']},
ansi256: {channels: 1, labels: ['ansi256']},
hcg: {channels: 3, labels: ['h', 'c', 'g']},
apple: {channels: 3, labels: ['r16', 'g16', 'b16']},
gray: {channels: 1, labels: ['gray']}
};
module.exports = convert;
// Hide .channels and .labels properties
for (const model of Object.keys(convert)) {
if (!('channels' in convert[model])) {
throw new Error('missing channels property: ' + model);
}
if (!('labels' in convert[model])) {
throw new Error('missing channel labels property: ' + model);
}
if (convert[model].labels.length !== convert[model].channels) {
throw new Error('channel and label counts mismatch: ' + model);
}
const {channels, labels} = convert[model];
delete convert[model].channels;
delete convert[model].labels;
Object.defineProperty(convert[model], 'channels', {value: channels});
Object.defineProperty(convert[model], 'labels', {value: labels});
}
convert.rgb.hsl = function (rgb) {
const r = rgb[0] / 255;
const g = rgb[1] / 255;
const b = rgb[2] / 255;
const min = Math.min(r, g, b);
const max = Math.max(r, g, b);
const delta = max - min;
let h;
let s;
if (max === min) {
h = 0;
} else if (r === max) {
h = (g - b) / delta;
} else if (g === max) {
h = 2 + (b - r) / delta;
} else if (b === max) {
h = 4 + (r - g) / delta;
}
h = Math.min(h * 60, 360);
if (h < 0) {
h += 360;
}
const l = (min + max) / 2;
if (max === min) {
s = 0;
} else if (l <= 0.5) {
s = delta / (max + min);
} else {
s = delta / (2 - max - min);
}
return [h, s * 100, l * 100];
};
convert.rgb.hsv = function (rgb) {
let rdif;
let gdif;
let bdif;
let h;
let s;
const r = rgb[0] / 255;
const g = rgb[1] / 255;
const b = rgb[2] / 255;
const v = Math.max(r, g, b);
const diff = v - Math.min(r, g, b);
const diffc = function (c) {
return (v - c) / 6 / diff + 1 / 2;
};
if (diff === 0) {
h = 0;
s = 0;
} else {
s = diff / v;
rdif = diffc(r);
gdif = diffc(g);
bdif = diffc(b);
if (r === v) {
h = bdif - gdif;
} else if (g === v) {
h = (1 / 3) + rdif - bdif;
} else if (b === v) {
h = (2 / 3) + gdif - rdif;
}
if (h < 0) {
h += 1;
} else if (h > 1) {
h -= 1;
}
}
return [
h * 360,
s * 100,
v * 100
];
};
convert.rgb.hwb = function (rgb) {
const r = rgb[0];
const g = rgb[1];
let b = rgb[2];
const h = convert.rgb.hsl(rgb)[0];
const w = 1 / 255 * Math.min(r, Math.min(g, b));
b = 1 - 1 / 255 * Math.max(r, Math.max(g, b));
return [h, w * 100, b * 100];
};
convert.rgb.cmyk = function (rgb) {
const r = rgb[0] / 255;
const g = rgb[1] / 255;
const b = rgb[2] / 255;
const k = Math.min(1 - r, 1 - g, 1 - b);
const c = (1 - r - k) / (1 - k) || 0;
const m = (1 - g - k) / (1 - k) || 0;
const y = (1 - b - k) / (1 - k) || 0;
return [c * 100, m * 100, y * 100, k * 100];
};
function comparativeDistance(x, y) {
/*
See https://en.m.wikipedia.org/wiki/Euclidean_distance#Squared_Euclidean_distance
*/
return (
((x[0] - y[0]) ** 2) +
((x[1] - y[1]) ** 2) +
((x[2] - y[2]) ** 2)
);
}
convert.rgb.keyword = function (rgb) {
const reversed = reverseKeywords[rgb];
if (reversed) {
return reversed;
}
let currentClosestDistance = Infinity;
let currentClosestKeyword;
for (const keyword of Object.keys(cssKeywords)) {
const value = cssKeywords[keyword];
// Compute comparative distance
const distance = comparativeDistance(rgb, value);
// Check if its less, if so set as closest
if (distance < currentClosestDistance) {
currentClosestDistance = distance;
currentClosestKeyword = keyword;
}
}
return currentClosestKeyword;
};
convert.keyword.rgb = function (keyword) {
return cssKeywords[keyword];
};
convert.rgb.xyz = function (rgb) {
let r = rgb[0] / 255;
let g = rgb[1] / 255;
let b = rgb[2] / 255;
// Assume sRGB
r = r > 0.04045 ? (((r + 0.055) / 1.055) ** 2.4) : (r / 12.92);
g = g > 0.04045 ? (((g + 0.055) / 1.055) ** 2.4) : (g / 12.92);
b = b > 0.04045 ? (((b + 0.055) / 1.055) ** 2.4) : (b / 12.92);
const x = (r * 0.4124) + (g * 0.3576) + (b * 0.1805);
const y = (r * 0.2126) + (g * 0.7152) + (b * 0.0722);
const z = (r * 0.0193) + (g * 0.1192) + (b * 0.9505);
return [x * 100, y * 100, z * 100];
};
convert.rgb.lab = function (rgb) {
const xyz = convert.rgb.xyz(rgb);
let x = xyz[0];
let y = xyz[1];
let z = xyz[2];
x /= 95.047;
y /= 100;
z /= 108.883;
x = x > 0.008856 ? (x ** (1 / 3)) : (7.787 * x) + (16 / 116);
y = y > 0.008856 ? (y ** (1 / 3)) : (7.787 * y) + (16 / 116);
z = z > 0.008856 ? (z ** (1 / 3)) : (7.787 * z) + (16 / 116);
const l = (116 * y) - 16;
const a = 500 * (x - y);
const b = 200 * (y - z);
return [l, a, b];
};
convert.hsl.rgb = function (hsl) {
const h = hsl[0] / 360;
const s = hsl[1] / 100;
const l = hsl[2] / 100;
let t2;
let t3;
let val;
if (s === 0) {
val = l * 255;
return [val, val, val];
}
if (l < 0.5) {
t2 = l * (1 + s);
} else {
t2 = l + s - l * s;
}
const t1 = 2 * l - t2;
const rgb = [0, 0, 0];
for (let i = 0; i < 3; i++) {
t3 = h + 1 / 3 * -(i - 1);
if (t3 < 0) {
t3++;
}
if (t3 > 1) {
t3--;
}
if (6 * t3 < 1) {
val = t1 + (t2 - t1) * 6 * t3;
} else if (2 * t3 < 1) {
val = t2;
} else if (3 * t3 < 2) {
val = t1 + (t2 - t1) * (2 / 3 - t3) * 6;
} else {
val = t1;
}
rgb[i] = val * 255;
}
return rgb;
};
convert.hsl.hsv = function (hsl) {
const h = hsl[0];
let s = hsl[1] / 100;
let l = hsl[2] / 100;
let smin = s;
const lmin = Math.max(l, 0.01);
l *= 2;
s *= (l <= 1) ? l : 2 - l;
smin *= lmin <= 1 ? lmin : 2 - lmin;
const v = (l + s) / 2;
const sv = l === 0 ? (2 * smin) / (lmin + smin) : (2 * s) / (l + s);
return [h, sv * 100, v * 100];
};
convert.hsv.rgb = function (hsv) {
const h = hsv[0] / 60;
const s = hsv[1] / 100;
let v = hsv[2] / 100;
const hi = Math.floor(h) % 6;
const f = h - Math.floor(h);
const p = 255 * v * (1 - s);
const q = 255 * v * (1 - (s * f));
const t = 255 * v * (1 - (s * (1 - f)));
v *= 255;
switch (hi) {
case 0:
return [v, t, p];
case 1:
return [q, v, p];
case 2:
return [p, v, t];
case 3:
return [p, q, v];
case 4:
return [t, p, v];
case 5:
return [v, p, q];
}
};
convert.hsv.hsl = function (hsv) {
const h = hsv[0];
const s = hsv[1] / 100;
const v = hsv[2] / 100;
const vmin = Math.max(v, 0.01);
let sl;
let l;
l = (2 - s) * v;
const lmin = (2 - s) * vmin;
sl = s * vmin;
sl /= (lmin <= 1) ? lmin : 2 - lmin;
sl = sl || 0;
l /= 2;
return [h, sl * 100, l * 100];
};
// http://dev.w3.org/csswg/css-color/#hwb-to-rgb
convert.hwb.rgb = function (hwb) {
const h = hwb[0] / 360;
let wh = hwb[1] / 100;
let bl = hwb[2] / 100;
const ratio = wh + bl;
let f;
// Wh + bl cant be > 1
if (ratio > 1) {
wh /= ratio;
bl /= ratio;
}
const i = Math.floor(6 * h);
const v = 1 - bl;
f = 6 * h - i;
if ((i & 0x01) !== 0) {
f = 1 - f;
}
const n = wh + f * (v - wh); // Linear interpolation
let r;
let g;
let b;
/* eslint-disable max-statements-per-line,no-multi-spaces */
switch (i) {
default:
case 6:
case 0: r = v; g = n; b = wh; break;
case 1: r = n; g = v; b = wh; break;
case 2: r = wh; g = v; b = n; break;
case 3: r = wh; g = n; b = v; break;
case 4: r = n; g = wh; b = v; break;
case 5: r = v; g = wh; b = n; break;
}
/* eslint-enable max-statements-per-line,no-multi-spaces */
return [r * 255, g * 255, b * 255];
};
convert.cmyk.rgb = function (cmyk) {
const c = cmyk[0] / 100;
const m = cmyk[1] / 100;
const y = cmyk[2] / 100;
const k = cmyk[3] / 100;
const r = 1 - Math.min(1, c * (1 - k) + k);
const g = 1 - Math.min(1, m * (1 - k) + k);
const b = 1 - Math.min(1, y * (1 - k) + k);
return [r * 255, g * 255, b * 255];
};
convert.xyz.rgb = function (xyz) {
const x = xyz[0] / 100;
const y = xyz[1] / 100;
const z = xyz[2] / 100;
let r;
let g;
let b;
r = (x * 3.2406) + (y * -1.5372) + (z * -0.4986);
g = (x * -0.9689) + (y * 1.8758) + (z * 0.0415);
b = (x * 0.0557) + (y * -0.2040) + (z * 1.0570);
// Assume sRGB
r = r > 0.0031308
? ((1.055 * (r ** (1.0 / 2.4))) - 0.055)
: r * 12.92;
g = g > 0.0031308
? ((1.055 * (g ** (1.0 / 2.4))) - 0.055)
: g * 12.92;
b = b > 0.0031308
? ((1.055 * (b ** (1.0 / 2.4))) - 0.055)
: b * 12.92;
r = Math.min(Math.max(0, r), 1);
g = Math.min(Math.max(0, g), 1);
b = Math.min(Math.max(0, b), 1);
return [r * 255, g * 255, b * 255];
};
convert.xyz.lab = function (xyz) {
let x = xyz[0];
let y = xyz[1];
let z = xyz[2];
x /= 95.047;
y /= 100;
z /= 108.883;
x = x > 0.008856 ? (x ** (1 / 3)) : (7.787 * x) + (16 / 116);
y = y > 0.008856 ? (y ** (1 / 3)) : (7.787 * y) + (16 / 116);
z = z > 0.008856 ? (z ** (1 / 3)) : (7.787 * z) + (16 / 116);
const l = (116 * y) - 16;
const a = 500 * (x - y);
const b = 200 * (y - z);
return [l, a, b];
};
convert.lab.xyz = function (lab) {
const l = lab[0];
const a = lab[1];
const b = lab[2];
let x;
let y;
let z;
y = (l + 16) / 116;
x = a / 500 + y;
z = y - b / 200;
const y2 = y ** 3;
const x2 = x ** 3;
const z2 = z ** 3;
y = y2 > 0.008856 ? y2 : (y - 16 / 116) / 7.787;
x = x2 > 0.008856 ? x2 : (x - 16 / 116) / 7.787;
z = z2 > 0.008856 ? z2 : (z - 16 / 116) / 7.787;
x *= 95.047;
y *= 100;
z *= 108.883;
return [x, y, z];
};
convert.lab.lch = function (lab) {
const l = lab[0];
const a = lab[1];
const b = lab[2];
let h;
const hr = Math.atan2(b, a);
h = hr * 360 / 2 / Math.PI;
if (h < 0) {
h += 360;
}
const c = Math.sqrt(a * a + b * b);
return [l, c, h];
};
convert.lch.lab = function (lch) {
const l = lch[0];
const c = lch[1];
const h = lch[2];
const hr = h / 360 * 2 * Math.PI;
const a = c * Math.cos(hr);
const b = c * Math.sin(hr);
return [l, a, b];
};
convert.rgb.ansi16 = function (args, saturation = null) {
const [r, g, b] = args;
let value = saturation === null ? convert.rgb.hsv(args)[2] : saturation; // Hsv -> ansi16 optimization
value = Math.round(value / 50);
if (value === 0) {
return 30;
}
let ansi = 30
+ ((Math.round(b / 255) << 2)
| (Math.round(g / 255) << 1)
| Math.round(r / 255));
if (value === 2) {
ansi += 60;
}
return ansi;
};
convert.hsv.ansi16 = function (args) {
// Optimization here; we already know the value and don't need to get
// it converted for us.
return convert.rgb.ansi16(convert.hsv.rgb(args), args[2]);
};
convert.rgb.ansi256 = function (args) {
const r = args[0];
const g = args[1];
const b = args[2];
// We use the extended greyscale palette here, with the exception of
// black and white. normal palette only has 4 greyscale shades.
if (r === g && g === b) {
if (r < 8) {
return 16;
}
if (r > 248) {
return 231;
}
return Math.round(((r - 8) / 247) * 24) + 232;
}
const ansi = 16
+ (36 * Math.round(r / 255 * 5))
+ (6 * Math.round(g / 255 * 5))
+ Math.round(b / 255 * 5);
return ansi;
};
convert.ansi16.rgb = function (args) {
let color = args % 10;
// Handle greyscale
if (color === 0 || color === 7) {
if (args > 50) {
color += 3.5;
}
color = color / 10.5 * 255;
return [color, color, color];
}
const mult = (~~(args > 50) + 1) * 0.5;
const r = ((color & 1) * mult) * 255;
const g = (((color >> 1) & 1) * mult) * 255;
const b = (((color >> 2) & 1) * mult) * 255;
return [r, g, b];
};
convert.ansi256.rgb = function (args) {
// Handle greyscale
if (args >= 232) {
const c = (args - 232) * 10 + 8;
return [c, c, c];
}
args -= 16;
let rem;
const r = Math.floor(args / 36) / 5 * 255;
const g = Math.floor((rem = args % 36) / 6) / 5 * 255;
const b = (rem % 6) / 5 * 255;
return [r, g, b];
};
convert.rgb.hex = function (args) {
const integer = ((Math.round(args[0]) & 0xFF) << 16)
+ ((Math.round(args[1]) & 0xFF) << 8)
+ (Math.round(args[2]) & 0xFF);
const string = integer.toString(16).toUpperCase();
return '000000'.substring(string.length) + string;
};
convert.hex.rgb = function (args) {
const match = args.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);
if (!match) {
return [0, 0, 0];
}
let colorString = match[0];
if (match[0].length === 3) {
colorString = colorString.split('').map(char => {
return char + char;
}).join('');
}
const integer = parseInt(colorString, 16);
const r = (integer >> 16) & 0xFF;
const g = (integer >> 8) & 0xFF;
const b = integer & 0xFF;
return [r, g, b];
};
convert.rgb.hcg = function (rgb) {
const r = rgb[0] / 255;
const g = rgb[1] / 255;
const b = rgb[2] / 255;
const max = Math.max(Math.max(r, g), b);
const min = Math.min(Math.min(r, g), b);
const chroma = (max - min);
let grayscale;
let hue;
if (chroma < 1) {
grayscale = min / (1 - chroma);
} else {
grayscale = 0;
}
if (chroma <= 0) {
hue = 0;
} else
if (max === r) {
hue = ((g - b) / chroma) % 6;
} else
if (max === g) {
hue = 2 + (b - r) / chroma;
} else {
hue = 4 + (r - g) / chroma;
}
hue /= 6;
hue %= 1;
return [hue * 360, chroma * 100, grayscale * 100];
};
convert.hsl.hcg = function (hsl) {
const s = hsl[1] / 100;
const l = hsl[2] / 100;
const c = l < 0.5 ? (2.0 * s * l) : (2.0 * s * (1.0 - l));
let f = 0;
if (c < 1.0) {
f = (l - 0.5 * c) / (1.0 - c);
}
return [hsl[0], c * 100, f * 100];
};
convert.hsv.hcg = function (hsv) {
const s = hsv[1] / 100;
const v = hsv[2] / 100;
const c = s * v;
let f = 0;
if (c < 1.0) {
f = (v - c) / (1 - c);
}
return [hsv[0], c * 100, f * 100];
};
convert.hcg.rgb = function (hcg) {
const h = hcg[0] / 360;
const c = hcg[1] / 100;
const g = hcg[2] / 100;
if (c === 0.0) {
return [g * 255, g * 255, g * 255];
}
const pure = [0, 0, 0];
const hi = (h % 1) * 6;
const v = hi % 1;
const w = 1 - v;
let mg = 0;
/* eslint-disable max-statements-per-line */
switch (Math.floor(hi)) {
case 0:
pure[0] = 1; pure[1] = v; pure[2] = 0; break;
case 1:
pure[0] = w; pure[1] = 1; pure[2] = 0; break;
case 2:
pure[0] = 0; pure[1] = 1; pure[2] = v; break;
case 3:
pure[0] = 0; pure[1] = w; pure[2] = 1; break;
case 4:
pure[0] = v; pure[1] = 0; pure[2] = 1; break;
default:
pure[0] = 1; pure[1] = 0; pure[2] = w;
}
/* eslint-enable max-statements-per-line */
mg = (1.0 - c) * g;
return [
(c * pure[0] + mg) * 255,
(c * pure[1] + mg) * 255,
(c * pure[2] + mg) * 255
];
};
convert.hcg.hsv = function (hcg) {
const c = hcg[1] / 100;
const g = hcg[2] / 100;
const v = c + g * (1.0 - c);
let f = 0;
if (v > 0.0) {
f = c / v;
}
return [hcg[0], f * 100, v * 100];
};
convert.hcg.hsl = function (hcg) {
const c = hcg[1] / 100;
const g = hcg[2] / 100;
const l = g * (1.0 - c) + 0.5 * c;
let s = 0;
if (l > 0.0 && l < 0.5) {
s = c / (2 * l);
} else
if (l >= 0.5 && l < 1.0) {
s = c / (2 * (1 - l));
}
return [hcg[0], s * 100, l * 100];
};
convert.hcg.hwb = function (hcg) {
const c = hcg[1] / 100;
const g = hcg[2] / 100;
const v = c + g * (1.0 - c);
return [hcg[0], (v - c) * 100, (1 - v) * 100];
};
convert.hwb.hcg = function (hwb) {
const w = hwb[1] / 100;
const b = hwb[2] / 100;
const v = 1 - b;
const c = v - w;
let g = 0;
if (c < 1) {
g = (v - c) / (1 - c);
}
return [hwb[0], c * 100, g * 100];
};
convert.apple.rgb = function (apple) {
return [(apple[0] / 65535) * 255, (apple[1] / 65535) * 255, (apple[2] / 65535) * 255];
};
convert.rgb.apple = function (rgb) {
return [(rgb[0] / 255) * 65535, (rgb[1] / 255) * 65535, (rgb[2] / 255) * 65535];
};
convert.gray.rgb = function (args) {
return [args[0] / 100 * 255, args[0] / 100 * 255, args[0] / 100 * 255];
};
convert.gray.hsl = function (args) {
return [0, 0, args[0]];
};
convert.gray.hsv = convert.gray.hsl;
convert.gray.hwb = function (gray) {
return [0, 100, gray[0]];
};
convert.gray.cmyk = function (gray) {
return [0, 0, 0, gray[0]];
};
convert.gray.lab = function (gray) {
return [gray[0], 0, 0];
};
convert.gray.hex = function (gray) {
const val = Math.round(gray[0] / 100 * 255) & 0xFF;
const integer = (val << 16) + (val << 8) + val;
const string = integer.toString(16).toUpperCase();
return '000000'.substring(string.length) + string;
};
convert.rgb.gray = function (rgb) {
const val = (rgb[0] + rgb[1] + rgb[2]) / 3;
return [val / 255 * 100];
};

View File

@@ -0,0 +1,81 @@
const conversions = require('./conversions');
const route = require('./route');
const convert = {};
const models = Object.keys(conversions);
function wrapRaw(fn) {
const wrappedFn = function (...args) {
const arg0 = args[0];
if (arg0 === undefined || arg0 === null) {
return arg0;
}
if (arg0.length > 1) {
args = arg0;
}
return fn(args);
};
// Preserve .conversion property if there is one
if ('conversion' in fn) {
wrappedFn.conversion = fn.conversion;
}
return wrappedFn;
}
function wrapRounded(fn) {
const wrappedFn = function (...args) {
const arg0 = args[0];
if (arg0 === undefined || arg0 === null) {
return arg0;
}
if (arg0.length > 1) {
args = arg0;
}
const result = fn(args);
// We're assuming the result is an array here.
// see notice in conversions.js; don't use box types
// in conversion functions.
if (typeof result === 'object') {
for (let len = result.length, i = 0; i < len; i++) {
result[i] = Math.round(result[i]);
}
}
return result;
};
// Preserve .conversion property if there is one
if ('conversion' in fn) {
wrappedFn.conversion = fn.conversion;
}
return wrappedFn;
}
models.forEach(fromModel => {
convert[fromModel] = {};
Object.defineProperty(convert[fromModel], 'channels', {value: conversions[fromModel].channels});
Object.defineProperty(convert[fromModel], 'labels', {value: conversions[fromModel].labels});
const routes = route(fromModel);
const routeModels = Object.keys(routes);
routeModels.forEach(toModel => {
const fn = routes[toModel];
convert[fromModel][toModel] = wrapRounded(fn);
convert[fromModel][toModel].raw = wrapRaw(fn);
});
});
module.exports = convert;

View File

@@ -0,0 +1,48 @@
{
"name": "color-convert",
"description": "Plain color conversion functions",
"version": "2.0.1",
"author": "Heather Arthur <fayearthur@gmail.com>",
"license": "MIT",
"repository": "Qix-/color-convert",
"scripts": {
"pretest": "xo",
"test": "node test/basic.js"
},
"engines": {
"node": ">=7.0.0"
},
"keywords": [
"color",
"colour",
"convert",
"converter",
"conversion",
"rgb",
"hsl",
"hsv",
"hwb",
"cmyk",
"ansi",
"ansi16"
],
"files": [
"index.js",
"conversions.js",
"route.js"
],
"xo": {
"rules": {
"default-case": 0,
"no-inline-comments": 0,
"operator-linebreak": 0
}
},
"devDependencies": {
"chalk": "^2.4.2",
"xo": "^0.24.0"
},
"dependencies": {
"color-name": "~1.1.4"
}
}

View File

@@ -0,0 +1,97 @@
const conversions = require('./conversions');
/*
This function routes a model to all other models.
all functions that are routed have a property `.conversion` attached
to the returned synthetic function. This property is an array
of strings, each with the steps in between the 'from' and 'to'
color models (inclusive).
conversions that are not possible simply are not included.
*/
function buildGraph() {
const graph = {};
// https://jsperf.com/object-keys-vs-for-in-with-closure/3
const models = Object.keys(conversions);
for (let len = models.length, i = 0; i < len; i++) {
graph[models[i]] = {
// http://jsperf.com/1-vs-infinity
// micro-opt, but this is simple.
distance: -1,
parent: null
};
}
return graph;
}
// https://en.wikipedia.org/wiki/Breadth-first_search
function deriveBFS(fromModel) {
const graph = buildGraph();
const queue = [fromModel]; // Unshift -> queue -> pop
graph[fromModel].distance = 0;
while (queue.length) {
const current = queue.pop();
const adjacents = Object.keys(conversions[current]);
for (let len = adjacents.length, i = 0; i < len; i++) {
const adjacent = adjacents[i];
const node = graph[adjacent];
if (node.distance === -1) {
node.distance = graph[current].distance + 1;
node.parent = current;
queue.unshift(adjacent);
}
}
}
return graph;
}
function link(from, to) {
return function (args) {
return to(from(args));
};
}
function wrapConversion(toModel, graph) {
const path = [graph[toModel].parent, toModel];
let fn = conversions[graph[toModel].parent][toModel];
let cur = graph[toModel].parent;
while (graph[cur].parent) {
path.unshift(graph[cur].parent);
fn = link(conversions[graph[cur].parent][cur], fn);
cur = graph[cur].parent;
}
fn.conversion = path;
return fn;
}
module.exports = function (fromModel) {
const graph = deriveBFS(fromModel);
const conversion = {};
const models = Object.keys(graph);
for (let len = models.length, i = 0; i < len; i++) {
const toModel = models[i];
const node = graph[toModel];
if (node.parent === null) {
// No possible conversion, or this node is the source model.
continue;
}
conversion[toModel] = wrapConversion(toModel, graph);
}
return conversion;
};

View File

@@ -0,0 +1,8 @@
The MIT License (MIT)
Copyright (c) 2015 Dmitry Ivanov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,11 @@
A JSON with color names and its values. Based on http://dev.w3.org/csswg/css-color/#named-colors.
[![NPM](https://nodei.co/npm/color-name.png?mini=true)](https://nodei.co/npm/color-name/)
```js
var colors = require('color-name');
colors.red //[255,0,0]
```
<a href="LICENSE"><img src="https://upload.wikimedia.org/wikipedia/commons/0/0c/MIT_logo.svg" width="120"/></a>

View File

@@ -0,0 +1,152 @@
'use strict'
module.exports = {
"aliceblue": [240, 248, 255],
"antiquewhite": [250, 235, 215],
"aqua": [0, 255, 255],
"aquamarine": [127, 255, 212],
"azure": [240, 255, 255],
"beige": [245, 245, 220],
"bisque": [255, 228, 196],
"black": [0, 0, 0],
"blanchedalmond": [255, 235, 205],
"blue": [0, 0, 255],
"blueviolet": [138, 43, 226],
"brown": [165, 42, 42],
"burlywood": [222, 184, 135],
"cadetblue": [95, 158, 160],
"chartreuse": [127, 255, 0],
"chocolate": [210, 105, 30],
"coral": [255, 127, 80],
"cornflowerblue": [100, 149, 237],
"cornsilk": [255, 248, 220],
"crimson": [220, 20, 60],
"cyan": [0, 255, 255],
"darkblue": [0, 0, 139],
"darkcyan": [0, 139, 139],
"darkgoldenrod": [184, 134, 11],
"darkgray": [169, 169, 169],
"darkgreen": [0, 100, 0],
"darkgrey": [169, 169, 169],
"darkkhaki": [189, 183, 107],
"darkmagenta": [139, 0, 139],
"darkolivegreen": [85, 107, 47],
"darkorange": [255, 140, 0],
"darkorchid": [153, 50, 204],
"darkred": [139, 0, 0],
"darksalmon": [233, 150, 122],
"darkseagreen": [143, 188, 143],
"darkslateblue": [72, 61, 139],
"darkslategray": [47, 79, 79],
"darkslategrey": [47, 79, 79],
"darkturquoise": [0, 206, 209],
"darkviolet": [148, 0, 211],
"deeppink": [255, 20, 147],
"deepskyblue": [0, 191, 255],
"dimgray": [105, 105, 105],
"dimgrey": [105, 105, 105],
"dodgerblue": [30, 144, 255],
"firebrick": [178, 34, 34],
"floralwhite": [255, 250, 240],
"forestgreen": [34, 139, 34],
"fuchsia": [255, 0, 255],
"gainsboro": [220, 220, 220],
"ghostwhite": [248, 248, 255],
"gold": [255, 215, 0],
"goldenrod": [218, 165, 32],
"gray": [128, 128, 128],
"green": [0, 128, 0],
"greenyellow": [173, 255, 47],
"grey": [128, 128, 128],
"honeydew": [240, 255, 240],
"hotpink": [255, 105, 180],
"indianred": [205, 92, 92],
"indigo": [75, 0, 130],
"ivory": [255, 255, 240],
"khaki": [240, 230, 140],
"lavender": [230, 230, 250],
"lavenderblush": [255, 240, 245],
"lawngreen": [124, 252, 0],
"lemonchiffon": [255, 250, 205],
"lightblue": [173, 216, 230],
"lightcoral": [240, 128, 128],
"lightcyan": [224, 255, 255],
"lightgoldenrodyellow": [250, 250, 210],
"lightgray": [211, 211, 211],
"lightgreen": [144, 238, 144],
"lightgrey": [211, 211, 211],
"lightpink": [255, 182, 193],
"lightsalmon": [255, 160, 122],
"lightseagreen": [32, 178, 170],
"lightskyblue": [135, 206, 250],
"lightslategray": [119, 136, 153],
"lightslategrey": [119, 136, 153],
"lightsteelblue": [176, 196, 222],
"lightyellow": [255, 255, 224],
"lime": [0, 255, 0],
"limegreen": [50, 205, 50],
"linen": [250, 240, 230],
"magenta": [255, 0, 255],
"maroon": [128, 0, 0],
"mediumaquamarine": [102, 205, 170],
"mediumblue": [0, 0, 205],
"mediumorchid": [186, 85, 211],
"mediumpurple": [147, 112, 219],
"mediumseagreen": [60, 179, 113],
"mediumslateblue": [123, 104, 238],
"mediumspringgreen": [0, 250, 154],
"mediumturquoise": [72, 209, 204],
"mediumvioletred": [199, 21, 133],
"midnightblue": [25, 25, 112],
"mintcream": [245, 255, 250],
"mistyrose": [255, 228, 225],
"moccasin": [255, 228, 181],
"navajowhite": [255, 222, 173],
"navy": [0, 0, 128],
"oldlace": [253, 245, 230],
"olive": [128, 128, 0],
"olivedrab": [107, 142, 35],
"orange": [255, 165, 0],
"orangered": [255, 69, 0],
"orchid": [218, 112, 214],
"palegoldenrod": [238, 232, 170],
"palegreen": [152, 251, 152],
"paleturquoise": [175, 238, 238],
"palevioletred": [219, 112, 147],
"papayawhip": [255, 239, 213],
"peachpuff": [255, 218, 185],
"peru": [205, 133, 63],
"pink": [255, 192, 203],
"plum": [221, 160, 221],
"powderblue": [176, 224, 230],
"purple": [128, 0, 128],
"rebeccapurple": [102, 51, 153],
"red": [255, 0, 0],
"rosybrown": [188, 143, 143],
"royalblue": [65, 105, 225],
"saddlebrown": [139, 69, 19],
"salmon": [250, 128, 114],
"sandybrown": [244, 164, 96],
"seagreen": [46, 139, 87],
"seashell": [255, 245, 238],
"sienna": [160, 82, 45],
"silver": [192, 192, 192],
"skyblue": [135, 206, 235],
"slateblue": [106, 90, 205],
"slategray": [112, 128, 144],
"slategrey": [112, 128, 144],
"snow": [255, 250, 250],
"springgreen": [0, 255, 127],
"steelblue": [70, 130, 180],
"tan": [210, 180, 140],
"teal": [0, 128, 128],
"thistle": [216, 191, 216],
"tomato": [255, 99, 71],
"turquoise": [64, 224, 208],
"violet": [238, 130, 238],
"wheat": [245, 222, 179],
"white": [255, 255, 255],
"whitesmoke": [245, 245, 245],
"yellow": [255, 255, 0],
"yellowgreen": [154, 205, 50]
};

View File

@@ -0,0 +1,28 @@
{
"name": "color-name",
"version": "1.1.4",
"description": "A list of color names and its values",
"main": "index.js",
"files": [
"index.js"
],
"scripts": {
"test": "node test.js"
},
"repository": {
"type": "git",
"url": "git@github.com:colorjs/color-name.git"
},
"keywords": [
"color-name",
"color",
"color-keyword",
"keyword"
],
"author": "DY <dfcreative@gmail.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/colorjs/color-name/issues"
},
"homepage": "https://github.com/colorjs/color-name"
}

View File

@@ -0,0 +1,4 @@
language: node_js
node_js:
- 0.4
- 0.6

View File

@@ -0,0 +1,18 @@
This software is released under the MIT license:
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,62 @@
concat-map
==========
Concatenative mapdashery.
[![browser support](http://ci.testling.com/substack/node-concat-map.png)](http://ci.testling.com/substack/node-concat-map)
[![build status](https://secure.travis-ci.org/substack/node-concat-map.png)](http://travis-ci.org/substack/node-concat-map)
example
=======
``` js
var concatMap = require('concat-map');
var xs = [ 1, 2, 3, 4, 5, 6 ];
var ys = concatMap(xs, function (x) {
return x % 2 ? [ x - 0.1, x, x + 0.1 ] : [];
});
console.dir(ys);
```
***
```
[ 0.9, 1, 1.1, 2.9, 3, 3.1, 4.9, 5, 5.1 ]
```
methods
=======
``` js
var concatMap = require('concat-map')
```
concatMap(xs, fn)
-----------------
Return an array of concatenated elements by calling `fn(x, i)` for each element
`x` and each index `i` in the array `xs`.
When `fn(x, i)` returns an array, its result will be concatenated with the
result array. If `fn(x, i)` returns anything else, that value will be pushed
onto the end of the result array.
install
=======
With [npm](http://npmjs.org) do:
```
npm install concat-map
```
license
=======
MIT
notes
=====
This module was written while sitting high above the ground in a tree.

View File

@@ -0,0 +1,6 @@
var concatMap = require('../');
var xs = [ 1, 2, 3, 4, 5, 6 ];
var ys = concatMap(xs, function (x) {
return x % 2 ? [ x - 0.1, x, x + 0.1 ] : [];
});
console.dir(ys);

View File

@@ -0,0 +1,13 @@
module.exports = function (xs, fn) {
var res = [];
for (var i = 0; i < xs.length; i++) {
var x = fn(xs[i], i);
if (isArray(x)) res.push.apply(res, x);
else res.push(x);
}
return res;
};
var isArray = Array.isArray || function (xs) {
return Object.prototype.toString.call(xs) === '[object Array]';
};

View File

@@ -0,0 +1,43 @@
{
"name" : "concat-map",
"description" : "concatenative mapdashery",
"version" : "0.0.1",
"repository" : {
"type" : "git",
"url" : "git://github.com/substack/node-concat-map.git"
},
"main" : "index.js",
"keywords" : [
"concat",
"concatMap",
"map",
"functional",
"higher-order"
],
"directories" : {
"example" : "example",
"test" : "test"
},
"scripts" : {
"test" : "tape test/*.js"
},
"devDependencies" : {
"tape" : "~2.4.0"
},
"license" : "MIT",
"author" : {
"name" : "James Halliday",
"email" : "mail@substack.net",
"url" : "http://substack.net"
},
"testling" : {
"files" : "test/*.js",
"browsers" : {
"ie" : [ 6, 7, 8, 9 ],
"ff" : [ 3.5, 10, 15.0 ],
"chrome" : [ 10, 22 ],
"safari" : [ 5.1 ],
"opera" : [ 12 ]
}
}
}

View File

@@ -0,0 +1,39 @@
var concatMap = require('../');
var test = require('tape');
test('empty or not', function (t) {
var xs = [ 1, 2, 3, 4, 5, 6 ];
var ixes = [];
var ys = concatMap(xs, function (x, ix) {
ixes.push(ix);
return x % 2 ? [ x - 0.1, x, x + 0.1 ] : [];
});
t.same(ys, [ 0.9, 1, 1.1, 2.9, 3, 3.1, 4.9, 5, 5.1 ]);
t.same(ixes, [ 0, 1, 2, 3, 4, 5 ]);
t.end();
});
test('always something', function (t) {
var xs = [ 'a', 'b', 'c', 'd' ];
var ys = concatMap(xs, function (x) {
return x === 'b' ? [ 'B', 'B', 'B' ] : [ x ];
});
t.same(ys, [ 'a', 'B', 'B', 'B', 'c', 'd' ]);
t.end();
});
test('scalars', function (t) {
var xs = [ 'a', 'b', 'c', 'd' ];
var ys = concatMap(xs, function (x) {
return x === 'b' ? [ 'B', 'B', 'B' ] : x;
});
t.same(ys, [ 'a', 'B', 'B', 'B', 'c', 'd' ]);
t.end();
});
test('undefs', function (t) {
var xs = [ 'a', 'b', 'c', 'd' ];
var ys = concatMap(xs, function () {});
t.same(ys, [ undefined, undefined, undefined, undefined ]);
t.end();
});

View File

@@ -0,0 +1,15 @@
{
"node": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"newcap": true,
"noarg": true,
"sub": true,
"undef": "nofunc",
"strict": true,
"white": true,
"indent": 2,
"trailing": true,
"quotmark": "single"
}

View File

@@ -0,0 +1,6 @@
language: node_js
node_js:
- 10
- 12
- "14"
- lts/*

View File

@@ -0,0 +1,47 @@
#!/usr/bin/env node
'use strict';
var copyfiles = require('./index');
var args = require('yargs');
var path = require('path');
args.alias('u', 'up')
.describe('u', 'slice a path off the bottom of the paths')
.number('u')
if (path.basename(args.argv.$0) === 'copyup') {
args.default('u', '1');
}
args.alias('a', 'all')
.describe('a', 'include files & directories begining with a dot (.)')
.boolean('a')
.alias('f','flat')
.describe('f', 'flatten the output')
.boolean('f')
.alias('e', 'exclude')
.describe('e', 'pattern or glob to exclude (may be passed multiple times)')
.alias('E', 'error')
.boolean('E')
.describe('E', 'throw error if nothing is coppied')
.alias('V', 'verbose')
.boolean('V')
.describe('V', 'print more information to console')
.alias('s','soft')
.boolean('s')
.describe('s', 'do not overwrite destination files if they exist')
.alias('F', 'follow')
.boolean('F')
.describe('F', 'follow symbolink links')
.usage('$0 [options] inFile [more files ...] outDirectory')
.help()
.alias('v', 'version')
.alias('h', 'help');
var argv = args.argv;
if (argv.flat) {
argv.up = true;
}
copyfiles(argv._, argv, function (err) {
if (err) {
console.error(err);
process.exit(1);
} else {
process.exit(0);
}
});

167
projects/arabica/src/sprint2/node_modules/copyfiles/index.js generated vendored Executable file
View File

@@ -0,0 +1,167 @@
'use strict';
var path = require('path');
var fs = require('fs');
var glob = require('glob');
var mkdirp = require('mkdirp');
var untildify = require('untildify');
var through = require('through2').obj;
var noms = require('noms').obj;
function toStream(array) {
var length = array.length;
var i = 0;
return noms(function (done) {
if (i >= length) {
this.push(null);
}
this.push(array[i++]);
done();
});
}
function depth(string) {
return path.normalize(string).split(path.sep).length - 1;
}
function dealWith(inPath, up) {
if (!up) {
return inPath;
}
if (up === true) {
return path.basename(inPath);
}
if (depth(inPath) < up) {
throw new Error('cant go up that far');
}
return path.join.apply(path, path.normalize(inPath).split(path.sep).slice(up));
}
var copyFile = _copyFile;
function _copyFile (src, dst, opts, callback) {
fs.createReadStream(src)
.pipe(fs.createWriteStream(dst, {
mode: opts.mode
}))
.once('error', callback)
.once('finish', function () {
fs.chmod(dst, opts.mode, function (err) {
callback(err);
})
})
}
if (fs.copyFile) {
copyFile = function (src, dst, opts, callback) {
fs.copyFile(src, dst, callback);
}
}
function makeDebug(config) {
if (config.verbose) {
return function (thing) {
console.log(thing);
}
}
return function () {}
}
module.exports = copyFiles;
function copyFiles(args, config, callback) {
if (typeof config === 'function') {
callback = config;
config = {
up:0
};
}
if (typeof config !== 'object' && config) {
config = {
up: config
};
}
var debug = makeDebug(config);
var copied = false;
var opts = config.up || 0;
var soft = config.soft;
if (typeof callback !== 'function') {
throw new Error('callback is not optional');
}
var input = args.slice();
var outDir = input.pop();
var globOpts = {};
if (config.exclude) {
globOpts.ignore = config.exclude;
}
if (config.all) {
globOpts.dot = true;
}
if (config.follow) {
globOpts.follow = true;
}
outDir = outDir.startsWith('~') ? untildify(outDir) : outDir;
toStream(input.map(function(srcP) {return srcP.startsWith('~') ? untildify(srcP) : srcP;}))
.pipe(through(function (pathName, _, next) {
var self = this;
glob(pathName, globOpts, function (err, paths) {
if (err) {
return next(err);
}
paths.forEach(function (unglobbedPath) {
debug(`unglobed path: ${unglobbedPath}`);
self.push(unglobbedPath);
});
next();
});
}))
.on('error', callback)
.pipe(through(function (pathName, _, next) {
fs.stat(pathName, function (err, pathStat) {
if (err) {
return next(err);
}
var outName = path.join(outDir, dealWith(pathName, opts));
function done(){
mkdirp(path.dirname(outName)).then(()=>{
next(null, {
pathName: pathName,
pathStat: pathStat
});
}, next);
}
if (pathStat.isDirectory()) {
debug(`skipping, is directory: ${pathName}`)
return next();
}
if (!pathStat.isFile()) {
return next(new Error('how can it be neither file nor folder?'))
}
if (!soft) {
return done();
}
fs.stat(outName, function(err){
if(!err){
//file exists
return next()
}
if (err.code === 'ENOENT') {
//file does not exist
return done();
}
// other error
return next(err)
})
});
}))
.on('error', callback)
.pipe(through(function (obj, _, next) {
if (!copied) {
copied = true;
}
var pathName = obj.pathName;
var pathStat = obj.pathStat;
var outName = path.join(outDir, dealWith(pathName, opts));
debug(`copy from: ${pathName}`)
debug(`copy to: ${outName}`)
copyFile(pathName, outName, pathStat, next)
}))
.on('error', callback)
.on('finish', function () {
if (config.error && !copied) {
return callback(new Error('nothing coppied'));
}
callback();
});
}

View File

@@ -0,0 +1,19 @@
# Copyright (c) 2014-2018 Calvin Metcalf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
**THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.**

View File

@@ -0,0 +1,33 @@
{
"name": "copyfiles",
"version": "2.4.1",
"description": "copy some files",
"main": "index.js",
"bin": {
"copyfiles": "./copyfiles",
"copyup": "./copyfiles"
},
"scripts": {
"test": "tape test/test.*.js | tspec"
},
"author": "",
"license": "MIT",
"dependencies": {
"glob": "^7.0.5",
"minimatch": "^3.0.3",
"mkdirp": "^1.0.4",
"noms": "0.0.0",
"through2": "^2.0.1",
"untildify": "^4.0.0",
"yargs": "^16.1.0"
},
"repository": {
"type": "git",
"url": "git://github.com/calvinmetcalf/copyfiles.git"
},
"devDependencies": {
"rimraf": "^2.2.6",
"tape": "^4.6.0",
"tap-spec": "^5.0.0"
}
}

View File

@@ -0,0 +1,108 @@
copyfiles [![Build Status](https://travis-ci.org/calvinmetcalf/copyfiles.svg)](https://travis-ci.org/calvinmetcalf/copyfiles)
===
copy files easily
### Install
```bash
npm install copyfiles -g
```
### Command Line
```bash
Usage: copyfiles [options] inFile [more files ...] outDirectory
Options:
-u, --up slice a path off the bottom of the paths [number]
-a, --all include files & directories begining with a dot (.) [boolean]
-f, --flat flatten the output [boolean]
-e, --exclude pattern or glob to exclude (may be passed multiple times)
-E, --error throw error if nothing is copied [boolean]
-V, --verbose print more information to console [boolean]
-s, --soft do not overwrite destination files if they exist [boolean]
-F, --follow follow symbolink links [boolean]
-v, --version Show version number [boolean]
-h, --help Show help [boolean]
```
copy some files, give it a bunch of arguments, (which can include globs), the last one
is the out directory (which it will create if necessary). Note: on windows globs must be **double quoted**, everybody else can quote however they please.
```bash
copyfiles foo foobar foo/bar/*.js out
```
you now have a directory called out, with the files foo and foobar in it, it also has a directory named foo with a directory named
bar in it that has all the files from foo/bar that match the glob.
If all the files are in a folder that you don't want in the path out path, ex:
```bash
copyfiles something/*.js out
```
which would put all the js files in `out/something`, you can use the `--up` (or `-u`) option
```bash
copyfiles -u 1 something/*.js out
```
which would put all the js files in `out`
you can also just do -f which will flatten all the output into one directory, so with files ./foo/a.txt and ./foo/bar/b.txt
```bash
copyfiles -f ./foo/*.txt ./foo/bar/*.txt out
```
will put a.txt and b.txt into out
if your terminal doesn't support globstars then you can quote them
```bash
copyfiles -f ./foo/**/*.txt out
```
does not work by default on a mac
but
```bash
copyfiles -f "./foo/**/*.txt" out
```
does.
You could quote globstars as a part of input:
```bash
copyfiles some.json "./some_folder/*.json" ./dist/ && echo 'JSON files copied.'
```
You can use the -e option to exclude some files from the pattern, so to exclude all all files ending in .test.js you could do
```bash
copyfiles -e "**/*.test.js" -f ./foo/**/*.js out
```
Other options include
- `-a` or `--all` which includes files that start with a dot.
- `-s` or `--soft` to soft copy, which will not overwrite existing files.
- `-F` or `--follow` which follows symbolinks
## copyup
also creates a `copyup` command which is identical to `copyfiles` but `-up` defaults to 1
### Programic API
```js
var copyfiles = require('copyfiles');
copyfiles([paths], opt, callback);
```
takes an array of paths, last one is the destination path, also takes an optional argument which the -u option if a number, otherwise if it's `true` it's the flat option or if it is an object it is a hash of the various options (the long version e.g. up, all, flat, exclude, error, verbose, follow, and soft)
### Tilde support for home directory
when the src/dest path start with tilde for home directory under windows, please make sure -u or -f is added in options or use copyup command. if not you will get `Error: Illegal characters in path.`

View File

@@ -0,0 +1,336 @@
'use strict';
var test = require('tape');
var copyfiles = require('../');
var rimraf = require('rimraf');
var fs = require('fs');
var _mkdirp = require('mkdirp');
var cp = require('child_process');
var glob = require('glob');
const mkdirp = (path, cb) => {
_mkdirp(path).then(()=>{
cb();
}, cb);
}
function after(t) {
rimraf('output', function (err) {
t.error(err, 'rm out');
rimraf('input', function (err) {
t.error(err, 'rm input');
t.end();
});
});
}
function before(t) {
mkdirp('input/other', function (err) {
t.error(err, 'rm input');
t.end();
});
}
test('normal', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/c.js', 'c');
copyfiles(['input/*.txt', 'output'], function (err) {
t.error(err, 'copyfiles');
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
});
t.test('teardown', after);
});
test('modes', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a', {
mode: 33261
});
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/c.js', 'c');
copyfiles(['input/*.txt', 'output'], function (err) {
t.error(err, 'copyfiles');
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.equals(fs.statSync('output/input/a.txt').mode, 33261, 'correct mode')
t.end();
});
});
});
t.test('teardown', after);
});
test('exclude', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/c.js.txt', 'c');
fs.writeFileSync('input/d.ps.txt', 'd');
copyfiles( ['input/*.txt', 'output'], {
exclude: ['**/*.js.txt', '**/*.ps.txt']
}, function (err) {
t.error(err, 'copyfiles');
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
});
t.test('teardown', after);
});
test('exclude cl', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/c.js.txt', 'c');
fs.writeFileSync('input/d.ps.txt', 'd');
cp.spawnSync('./copyfiles', ['-e', '**/*.js.txt', '-e', '**/*.ps.txt', 'input/*.txt', 'output']);
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
t.test('teardown', after);
});
test('all', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/.c.txt', 'c');
copyfiles( ['input/*.txt', 'output'], {
all: true
}, function (err) {
t.error(err, 'copyfiles');
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['.c.txt', 'a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
});
t.test('teardown', after);
});
test('all from cl', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/.c.txt', 'c');
cp.spawnSync('./copyfiles', ['-a', 'input/*.txt', 'output']);
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['.c.txt', 'a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
t.test('teardown', after);
});
test('error on nothing coppied', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/.c.txt', 'c');
var out = cp.spawnSync('./copyfiles', ['-E', 'input/*.txt', 'output']);
t.ok(out.status, 'should error');
t.end();
});
t.test('teardown', after);
});
test('all from cl 2', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/.c.txt', 'c');
cp.spawnSync('./copyfiles', ['--all', 'input/*.txt', 'output']);
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['.c.txt', 'a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
t.test('teardown', after);
});
test('soft', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
mkdirp('output/input/other', function(){
fs.writeFileSync('input/a.txt', 'inputA');
fs.writeFileSync('output/input/a.txt', 'outputA');
t.equal( fs.readFileSync('output/input/a.txt').toString(), 'outputA' )
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/other/c.txt', 'inputC');
fs.writeFileSync('output/input/other/c.txt', 'outputC');
fs.writeFileSync('input/other/d.txt', 'd');
copyfiles(['input/**/*.txt', 'output'], {soft:true}, function (err) {
t.error(err, 'copyfiles');
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt', 'other'], 'correct number of things');
t.equal( fs.readFileSync('output/input/a.txt').toString(), 'outputA' )
t.equal( fs.readFileSync('output/input/b.txt').toString(), 'b')
t.equal( fs.readFileSync('output/input/other/c.txt').toString(), 'outputC')
t.end();
});
});
})
});
t.test('teardown', after);
});
test('soft from cl', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
mkdirp('output/input/other', function(){
fs.writeFileSync('input/a.txt', 'inputA');
fs.writeFileSync('output/input/a.txt', 'outputA');
t.equal( fs.readFileSync('output/input/a.txt').toString(), 'outputA' )
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/other/c.txt', 'inputC');
fs.writeFileSync('output/input/other/c.txt', 'outputC');
fs.writeFileSync('input/other/d.txt', 'd');
cp.spawnSync('./copyfiles', ['-s', 'input/**/*.txt', 'output']);
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt', 'other'], 'correct number of things');
t.equal( fs.readFileSync('output/input/a.txt').toString(), 'outputA' )
t.equal( fs.readFileSync('output/input/b.txt').toString(), 'b')
t.equal( fs.readFileSync('output/input/other/c.txt').toString(), 'outputC')
t.end();
});
});
});
t.test('teardown', after);
});
test('soft from cl 2', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
mkdirp('output/input/other', function(){
fs.writeFileSync('input/a.txt', 'inputA');
fs.writeFileSync('output/input/a.txt', 'outputA');
t.equal( fs.readFileSync('output/input/a.txt').toString(), 'outputA' )
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/other/c.txt', 'inputC');
fs.writeFileSync('output/input/other/c.txt', 'outputC');
fs.writeFileSync('input/other/d.txt', 'd');
cp.spawnSync('./copyfiles', ['--soft', 'input/**/*.txt', 'output']);
fs.readdir('output/input', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt', 'other'], 'correct number of things');
t.equal( fs.readFileSync('output/input/a.txt').toString(), 'outputA' )
t.equal( fs.readFileSync('output/input/b.txt').toString(), 'b')
t.equal( fs.readFileSync('output/input/other/c.txt').toString(), 'outputC')
t.end();
});
});
});
t.test('teardown', after);
});
test('with up', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/c.js', 'c');
copyfiles(['input/*.txt', 'output'], 1, function (err) {
t.error(err, 'copyfiles');
fs.readdir('output', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
});
t.test('teardown', after);
});
test('with up cl', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/c.js', 'c');
cp.spawnSync('./copyfiles', ['-u', '1', 'input/*.txt', 'output']);
fs.readdir('output', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
t.test('teardown', after);
});
test('with copyup', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/c.js', 'c');
cp.spawnSync('./copyup', ['input/*.txt', 'output']);
fs.readdir('output', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
t.test('teardown', after);
});
test('with up 2', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/other/a.txt', 'a');
fs.writeFileSync('input/other/b.txt', 'b');
fs.writeFileSync('input/other/c.js', 'c');
copyfiles(['input/**/*.txt', 'output'], 2, function (err) {
t.error(err, 'copyfiles');
fs.readdir('output', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
});
t.test('teardown', after);
});
test('flatten', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.writeFileSync('input/other/a.txt', 'a');
fs.writeFileSync('input/b.txt', 'b');
fs.writeFileSync('input/other/c.js', 'c');
copyfiles(['input/**/*.txt', 'output'], true, function (err) {
t.error(err, 'copyfiles');
fs.readdir('output', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['a.txt', 'b.txt'], 'correct number of things');
t.end();
});
});
});
t.test('teardown', after);
});
test('follow', function (t) {
t.test('setup', before);
t.test('copy stuff', function (t) {
fs.mkdirSync('input/origin');
fs.mkdirSync('input/origin/inner');
fs.writeFileSync('input/origin/inner/a.txt', 'a');
fs.symlinkSync('origin', 'input/dest');
copyfiles(['input/**/*.txt', 'output'], { up: 1, follow: true }, function (err) {
t.error(err, 'copyfiles');
glob('output/**/*.txt', function (err, files) {
t.error(err, 'readdir');
t.deepEquals(files, ['output/dest/inner/a.txt', 'output/origin/inner/a.txt'], 'correct number of things');
t.end();
});
});
});
t.test('teardown', after);
});

View File

@@ -0,0 +1,19 @@
Copyright Node.js contributors. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

View File

@@ -0,0 +1,3 @@
# core-util-is
The `util.is*` functions introduced in Node v0.12.

View File

@@ -0,0 +1,107 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
// NOTE: These type checking functions intentionally don't use `instanceof`
// because it is fragile and can be easily faked with `Object.create()`.
function isArray(arg) {
if (Array.isArray) {
return Array.isArray(arg);
}
return objectToString(arg) === '[object Array]';
}
exports.isArray = isArray;
function isBoolean(arg) {
return typeof arg === 'boolean';
}
exports.isBoolean = isBoolean;
function isNull(arg) {
return arg === null;
}
exports.isNull = isNull;
function isNullOrUndefined(arg) {
return arg == null;
}
exports.isNullOrUndefined = isNullOrUndefined;
function isNumber(arg) {
return typeof arg === 'number';
}
exports.isNumber = isNumber;
function isString(arg) {
return typeof arg === 'string';
}
exports.isString = isString;
function isSymbol(arg) {
return typeof arg === 'symbol';
}
exports.isSymbol = isSymbol;
function isUndefined(arg) {
return arg === void 0;
}
exports.isUndefined = isUndefined;
function isRegExp(re) {
return objectToString(re) === '[object RegExp]';
}
exports.isRegExp = isRegExp;
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
exports.isObject = isObject;
function isDate(d) {
return objectToString(d) === '[object Date]';
}
exports.isDate = isDate;
function isError(e) {
return (objectToString(e) === '[object Error]' || e instanceof Error);
}
exports.isError = isError;
function isFunction(arg) {
return typeof arg === 'function';
}
exports.isFunction = isFunction;
function isPrimitive(arg) {
return arg === null ||
typeof arg === 'boolean' ||
typeof arg === 'number' ||
typeof arg === 'string' ||
typeof arg === 'symbol' || // ES6 symbol
typeof arg === 'undefined';
}
exports.isPrimitive = isPrimitive;
exports.isBuffer = require('buffer').Buffer.isBuffer;
function objectToString(o) {
return Object.prototype.toString.call(o);
}

View File

@@ -0,0 +1,38 @@
{
"name": "core-util-is",
"version": "1.0.3",
"description": "The `util.is*` functions introduced in Node v0.12.",
"main": "lib/util.js",
"files": [
"lib"
],
"repository": {
"type": "git",
"url": "git://github.com/isaacs/core-util-is"
},
"keywords": [
"util",
"isBuffer",
"isArray",
"isNumber",
"isString",
"isRegExp",
"isThis",
"isThat",
"polyfill"
],
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"license": "MIT",
"bugs": {
"url": "https://github.com/isaacs/core-util-is/issues"
},
"scripts": {
"test": "tap test.js",
"preversion": "npm test",
"postversion": "npm publish",
"prepublishOnly": "git push origin --follow-tags"
},
"devDependencies": {
"tap": "^15.0.9"
}
}

View File

@@ -0,0 +1,20 @@
Copyright Mathias Bynens <https://mathiasbynens.be/>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -0,0 +1,73 @@
# emoji-regex [![Build status](https://travis-ci.org/mathiasbynens/emoji-regex.svg?branch=master)](https://travis-ci.org/mathiasbynens/emoji-regex)
_emoji-regex_ offers a regular expression to match all emoji symbols (including textual representations of emoji) as per the Unicode Standard.
This repository contains a script that generates this regular expression based on [the data from Unicode v12](https://github.com/mathiasbynens/unicode-12.0.0). Because of this, the regular expression can easily be updated whenever new emoji are added to the Unicode standard.
## Installation
Via [npm](https://www.npmjs.com/):
```bash
npm install emoji-regex
```
In [Node.js](https://nodejs.org/):
```js
const emojiRegex = require('emoji-regex');
// Note: because the regular expression has the global flag set, this module
// exports a function that returns the regex rather than exporting the regular
// expression itself, to make it impossible to (accidentally) mutate the
// original regular expression.
const text = `
\u{231A}: ⌚ default emoji presentation character (Emoji_Presentation)
\u{2194}\u{FE0F}: ↔️ default text presentation character rendered as emoji
\u{1F469}: 👩 emoji modifier base (Emoji_Modifier_Base)
\u{1F469}\u{1F3FF}: 👩🏿 emoji modifier base followed by a modifier
`;
const regex = emojiRegex();
let match;
while (match = regex.exec(text)) {
const emoji = match[0];
console.log(`Matched sequence ${ emoji } — code points: ${ [...emoji].length }`);
}
```
Console output:
```
Matched sequence ⌚ — code points: 1
Matched sequence ⌚ — code points: 1
Matched sequence ↔️ — code points: 2
Matched sequence ↔️ — code points: 2
Matched sequence 👩 — code points: 1
Matched sequence 👩 — code points: 1
Matched sequence 👩🏿 — code points: 2
Matched sequence 👩🏿 — code points: 2
```
To match emoji in their textual representation as well (i.e. emoji that are not `Emoji_Presentation` symbols and that arent forced to render as emoji by a variation selector), `require` the other regex:
```js
const emojiRegex = require('emoji-regex/text.js');
```
Additionally, in environments which support ES2015 Unicode escapes, you may `require` ES2015-style versions of the regexes:
```js
const emojiRegex = require('emoji-regex/es2015/index.js');
const emojiRegexText = require('emoji-regex/es2015/text.js');
```
## Author
| [![twitter/mathias](https://gravatar.com/avatar/24e08a9ea84deb17ae121074d0f17125?s=70)](https://twitter.com/mathias "Follow @mathias on Twitter") |
|---|
| [Mathias Bynens](https://mathiasbynens.be/) |
## License
_emoji-regex_ is available under the [MIT](https://mths.be/mit) license.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
declare module 'emoji-regex' {
function emojiRegex(): RegExp;
export default emojiRegex;
}
declare module 'emoji-regex/text' {
function emojiRegex(): RegExp;
export default emojiRegex;
}
declare module 'emoji-regex/es2015' {
function emojiRegex(): RegExp;
export default emojiRegex;
}
declare module 'emoji-regex/es2015/text' {
function emojiRegex(): RegExp;
export default emojiRegex;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,50 @@
{
"name": "emoji-regex",
"version": "8.0.0",
"description": "A regular expression to match all Emoji-only symbols as per the Unicode Standard.",
"homepage": "https://mths.be/emoji-regex",
"main": "index.js",
"types": "index.d.ts",
"keywords": [
"unicode",
"regex",
"regexp",
"regular expressions",
"code points",
"symbols",
"characters",
"emoji"
],
"license": "MIT",
"author": {
"name": "Mathias Bynens",
"url": "https://mathiasbynens.be/"
},
"repository": {
"type": "git",
"url": "https://github.com/mathiasbynens/emoji-regex.git"
},
"bugs": "https://github.com/mathiasbynens/emoji-regex/issues",
"files": [
"LICENSE-MIT.txt",
"index.js",
"index.d.ts",
"text.js",
"es2015/index.js",
"es2015/text.js"
],
"scripts": {
"build": "rm -rf -- es2015; babel src -d .; NODE_ENV=es2015 babel src -d ./es2015; node script/inject-sequences.js",
"test": "mocha",
"test:watch": "npm run test -- --watch"
},
"devDependencies": {
"@babel/cli": "^7.2.3",
"@babel/core": "^7.3.4",
"@babel/plugin-proposal-unicode-property-regex": "^7.2.0",
"@babel/preset-env": "^7.3.4",
"mocha": "^6.0.2",
"regexgen": "^1.3.0",
"unicode-12.0.0": "^0.7.9"
}
}

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More