7 Commits

Author SHA1 Message Date
Morten Olsen
11f76a7378 refact: ink based terminal view (#17) 2025-05-19 16:22:59 +02:00
morten-olsen
4514972880 docs: generated README 2025-05-19 08:32:21 +00:00
Morten Olsen
c7b9abf868 feat: add javascript code block support (#13) 2025-05-19 10:31:12 +02:00
morten-olsen
bf14ef97b8 docs: generated README 2025-05-19 07:15:52 +00:00
Morten Olsen
ab2bb38f39 docs: add road-map 2025-05-19 09:14:47 +02:00
Morten Olsen
1d055e49f1 feat: rename 2025-05-19 09:07:21 +02:00
Morten Olsen
b308c7f9fe fix: align yaml option with documentation 2025-05-18 22:36:25 +02:00
23 changed files with 1266 additions and 166 deletions

314
README.md
View File

@@ -21,12 +21,51 @@ It allows developers to create API documentation that is always accurate and up-
* **Tutorials & Guides:** Build step-by-step guides where each HTTP interaction is shown with its real output.
* **Rapid Prototyping:** Quickly experiment with APIs and document your findings.
## Roadmap
* **Programmatic API** Use `http.md` inside existing scripts and pipelines
* **Environment Varaiables** Support using the runners environment variables in templates
* **JavaScript script support** Add JavaScript code blocks with execution, which will allow more advanced use-cases
* **Asserts** Add the ability to make HTTP assertions to use the document for testing
* **Templates** Write re-usable templates which can be used in documents
## Content
* [Key Features](#key-features)
* [Use Cases](#use-cases)
* [Roadmap](#roadmap)
* [Content](#content)
* [Installation](#installation)
* [Getting Started](#getting-started)
* [Your First Request](#your-first-request)
* [Rendering Documents](#rendering-documents)
* [Core Concepts](#core-concepts)
* [HTTP Request Blocks](#http-request-blocks)
* [The `::response` Directive](#the-response-directive)
* [Request IDs](#request-ids)
* [Templating with Handlebars](#templating-with-handlebars)
* [Available Variables for Templating](#available-variables-for-templating)
* [Templating Examples](#templating-examples)
* [Managing Documents](#managing-documents)
* [Embedding Other Documents (`::md`)](#embedding-other-documents-md)
* [Advanced Usage](#advanced-usage)
* [Using Input Variables](#using-input-variables)
* [JavaScript Execution](#javascript-execution)
* [HTTP Block Configuration Options](#http-block-configuration-options)
* [Directive Options](#directive-options)
* [`::response` Directive Options](#response-directive-options)
* [`::md[{file}]` Directive Options](#mdfile-directive-options)
* [`::input[{name}]` Directive Options](#inputname-directive-options)
* [Command-Line Interface (CLI)](#command-line-interface-cli)
* [`http.md dev <source_file.md>`](#httpmd-dev-source_filemd)
* [`http.md build <source_file.md> <output_file.md>`](#httpmd-build-source_filemd-output_filemd)
## Installation
Install `http.md` globally using npm:
```shell
npm i -g @morten-olsen/httpmd
npm i -g @morten-olsen/http.md
```
## Getting Started
@@ -63,13 +102,13 @@ You have two primary ways to render your `http.md` file:
For a development server that outputs to your terminal and watches for changes:
```shell
httpmd dev example.md
http.md dev example.md
```
With watch mode:
```shell
httpmd dev --watch example.md
http.md dev --watch example.md
```
This command will process `example.md`, execute the HTTP requests, and print the resulting markdown (with responses filled in) to the terminal. With `--watch`, any changes to `example.md` will trigger a re-run.
@@ -78,13 +117,13 @@ You have two primary ways to render your `http.md` file:
To generate a new markdown file with the responses and templated values rendered:
```shell
httpmd build example.md output.md
http.md build example.md output.md
```
With watch mode:
```shell
httpmd build --watch example.md output.md
http.md build --watch example.md output.md
```
This creates `output.md`, which is a static snapshot of `example.md` after all requests have been executed and templating applied. This file is suitable for version control, sharing, or integration with static site generators.
@@ -105,14 +144,14 @@ Content-Type: application/json
And here is the response:
```
```http
HTTP/200 OK
access-control-allow-credentials: true
access-control-allow-origin: *
connection: keep-alive
content-length: 555
content-length: 557
content-type: application/json
date: Sun, 18 May 2025 19:12:17 GMT
date: Mon, 19 May 2025 08:31:44 GMT
server: gunicorn/19.9.0
{
@@ -129,12 +168,12 @@ server: gunicorn/19.9.0
"Host": "httpbin.org",
"Sec-Fetch-Mode": "cors",
"User-Agent": "node",
"X-Amzn-Trace-Id": "Root=1-682a3111-131bcbff690b03fd64aa4617"
"X-Amzn-Trace-Id": "Root=1-682aec70-4f9d0a877f1453210a7009b6"
},
"json": {
"greeting": "Hello, http.md!"
},
"origin": "23.96.180.7",
"origin": "40.71.224.172",
"url": "https://httpbin.org/post"
}
@@ -193,23 +232,23 @@ You can assign a unique ID to an `http` request block. This allows you to:
1. Reference its specific response in a `::response` directive.
2. Access its request and response data in [Templating](https://www.google.com/search?q=%23templating-with-handlebars) via the `requests` and `responses` dictionaries.
To add an ID, include `id=yourUniqueId` in the `http` block's info string:
To add an ID, include `#yourUniqueId` or `id=yourUniqueId` in the `http` block's info string:
````markdown
# Document with Multiple Requests
First, create a resource:
```http id=createUser
```http #createUser,yaml,json
POST https://httpbin.org/post
Content-Type: application/json
{"username": "alpha"}
username: alpha
```
Then, fetch a different resource:
```http id=getItem
```http #getItem
GET https://httpbin.org/get?item=123
```
@@ -221,6 +260,96 @@ Response from getting the item:
````
<details>
<summary>Output</summary>
````markdown
# Document with Multiple Requests
First, create a resource:
```http
POST https://httpbin.org/post
Content-Type: application/json
{"username":"alpha"}
```
Then, fetch a different resource:
```http
GET https://httpbin.org/get?item=123
```
Response from creating the user:
```http
HTTP/200 OK
access-control-allow-credentials: true
access-control-allow-origin: *
connection: keep-alive
content-length: 502
content-type: application/json
date: Mon, 19 May 2025 08:31:44 GMT
server: gunicorn/19.9.0
{
"args": {},
"data": "username: alpha",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "br, gzip, deflate",
"Accept-Language": "*",
"Content-Length": "15",
"Content-Type": "application/json",
"Host": "httpbin.org",
"Sec-Fetch-Mode": "cors",
"User-Agent": "node",
"X-Amzn-Trace-Id": "Root=1-682aec70-52423dd76328a4e37066ba0e"
},
"json": null,
"origin": "40.71.224.172",
"url": "https://httpbin.org/post"
}
```
Response from getting the item:
```http
HTTP/200 OK
access-control-allow-credentials: true
access-control-allow-origin: *
connection: keep-alive
content-length: 383
content-type: application/json
date: Mon, 19 May 2025 08:31:44 GMT
server: gunicorn/19.9.0
{
"args": {
"item": "123"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "br, gzip, deflate",
"Accept-Language": "*",
"Host": "httpbin.org",
"Sec-Fetch-Mode": "cors",
"User-Agent": "node",
"X-Amzn-Trace-Id": "Root=1-682aec70-1509df0125913c7c042c1116"
},
"origin": "40.71.224.172",
"url": "https://httpbin.org/get?item=123"
}
```
````
</details>
## Templating with Handlebars
`http.md` uses [Handlebars](https://handlebarsjs.com/) for templating, allowing you to create dynamic content within your markdown files. You can inject data from request responses, input variables, and other requests into your HTTP blocks or general markdown text.
@@ -257,7 +386,7 @@ Within your markdown document, the following variables are available in the Hand
* **`input`** (Object): A dictionary of variables passed to `http.md` via the command line using the `-i` or `--input` flag.
* Example: If you run `httpmd dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
* Example: If you run `http.md dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
### Templating Examples
@@ -302,14 +431,14 @@ Now, let's fetch the item using a (mocked) ID from the response:
GET https://httpbin.org/anything/My New Item
```
```
```http
HTTP/200 OK
access-control-allow-credentials: true
access-control-allow-origin: *
connection: keep-alive
content-length: 451
content-length: 453
content-type: application/json
date: Sun, 18 May 2025 19:12:18 GMT
date: Mon, 19 May 2025 08:31:44 GMT
server: gunicorn/19.9.0
{
@@ -324,11 +453,11 @@ server: gunicorn/19.9.0
"Host": "httpbin.org",
"Sec-Fetch-Mode": "cors",
"User-Agent": "node",
"X-Amzn-Trace-Id": "Root=1-682a3112-4bbb29111129c1556c487ca1"
"X-Amzn-Trace-Id": "Root=1-682aec70-0086cbec627144c72e8dd560"
},
"json": null,
"method": "GET",
"origin": "23.96.180.7",
"origin": "40.71.224.172",
"url": "https://httpbin.org/anything/My New Item"
}
@@ -365,9 +494,10 @@ The requests from the embedded document are processed, and their `request` and `
Assume `_shared_requests.md` contains:
````markdown
```http id=sharedGetRequest
```http #sharedGetRequest
GET https://httpbin.org/get
```
````
Then, in `main.md`:
@@ -379,7 +509,7 @@ Let's include some shared requests:
::md[./_shared_requests.md]
The shared GET request returned:
The shared GET request returned: {{response.statusText}}
Now, a request specific to this document:
@@ -387,12 +517,76 @@ Now, a request specific to this document:
POST https://httpbin.org/post
Content-Type: application/json
{"dataFromMain": "someValue", "sharedUrl": ""}
{"dataFromMain": "someValue", "sharedUrl": "{{requests.sharedGetRequest.url}}"}
```
::response
````
<details>
<summary>Output</summary>
````markdown
# Main Document
Let's include some shared requests:
```http
GET https://httpbin.org/get
```
The shared GET request returned: OK
Now, a request specific to this document:
```http
POST https://httpbin.org/post
Content-Type: application/json
{"dataFromMain": "someValue", "sharedUrl": "https://httpbin.org/get"}
```
```http
HTTP/200 OK
access-control-allow-credentials: true
access-control-allow-origin: *
connection: keep-alive
content-length: 642
content-type: application/json
date: Mon, 19 May 2025 08:31:44 GMT
server: gunicorn/19.9.0
{
"args": {},
"data": "{\"dataFromMain\": \"someValue\", \"sharedUrl\": \"https://httpbin.org/get\"}",
"files": {},
"form": {},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "br, gzip, deflate",
"Accept-Language": "*",
"Content-Length": "69",
"Content-Type": "application/json",
"Host": "httpbin.org",
"Sec-Fetch-Mode": "cors",
"User-Agent": "node",
"X-Amzn-Trace-Id": "Root=1-682aec70-2a4b678f42e2be503f693fd5"
},
"json": {
"dataFromMain": "someValue",
"sharedUrl": "https://httpbin.org/get"
},
"origin": "40.71.224.172",
"url": "https://httpbin.org/post"
}
```
````
</details>
When `main.md` is processed, `_shared_requests.md` will be embedded, its `sharedGetRequest` will be executed, and its data will be available for templating.
## Advanced Usage
@@ -404,7 +598,7 @@ You can pass external data into your `http.md` documents using the `-i` (or `--i
**CLI Command:**
```shell
httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
http.md build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
```
**Markdown Usage (`mydoc.md`):**
@@ -420,6 +614,68 @@ Authorization: Bearer
**Security Note:** For sensitive data like API keys, using input variables is highly recommended over hardcoding them in your markdown files. Avoid committing files with plaintext secrets; instead, provide them at runtime via the CLI.
### JavaScript Execution
You can execute `javascript` blocks by adding a `run` option which allows programmatically changing the context, making request assertions and solve other more advanced use cases
**Example:**
````markdown
```javascript run
input.test = "Hello World";
```
::input[test]
```http json
POST https://httpbin.org/post
{"input": "{{input.test}}"}
```
```javascript run,hidden
// Use chai's `expect`, `assert` or `should` to make assumptions
expect(response.body.json.input).to.equal("Hello World");
```
````
<details>
<summary>Output</summary>
````markdown
```javascript
input.test = "Hello World";
```
```
test=Hello World
```
```http
POST https://httpbin.org/post
{"input": "Hello World"}
```
````
</details>
**Options:**
* `run`: If present the code block will be executed
* Example: ` ```javascript run `
* `hidden`: If present the code block will not be included in the resulting output
* Example: ` ```javascript hidden `
* `output`: If present the code blocks return value will be rendered as a `yaml` code block
### HTTP Block Configuration Options
You can configure the behavior of each `http` code block by adding options to its info string, separated by commas.
@@ -513,9 +769,9 @@ The `::input` directive is used to declare expected input variables
## Command-Line Interface (CLI)
The `httpmd` tool provides the following commands:
The `http.md` tool provides the following commands:
### `httpmd dev <source_file.md>`
### `http.md dev <source_file.md>`
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
@@ -527,10 +783,10 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
**Example:**
```shell
httpmd dev api_tests.md --watch -i host=localhost:3000
http.md dev api_tests.md --watch -i host=localhost:3000
```
### `httpmd build <source_file.md> <output_file.md>`
### `http.md build <source_file.md> <output_file.md>`
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and saves the resulting markdown to `<output_file.md>`.
@@ -542,5 +798,5 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
**Example:**
```shell
httpmd build official_api_docs.md public/api_docs_v1.md -i version=v1.0
http.md build official_api_docs.md public/api_docs_v1.md -i version=v1.0
```

View File

@@ -21,6 +21,14 @@ It allows developers to create API documentation that is always accurate and up-
- **Tutorials & Guides:** Build step-by-step guides where each HTTP interaction is shown with its real output.
- **Rapid Prototyping:** Quickly experiment with APIs and document your findings.
## Roadmap
- **Programmatic API** Use `http.md` inside existing scripts and pipelines
- **Environment Varaiables** Support using the runners environment variables in templates
- **JavaScript script support** Add JavaScript code blocks with execution, which will allow more advanced use-cases
- **Asserts** Add the ability to make HTTP assertions to use the document for testing
- **Templates** Write re-usable templates which can be used in documents
## Content
::toc
@@ -30,7 +38,7 @@ It allows developers to create API documentation that is always accurate and up-
Install `http.md` globally using npm:
```shell
npm i -g @morten-olsen/httpmd
npm i -g @morten-olsen/http.md
```
## Getting Started
@@ -43,7 +51,7 @@ Create a file named `example.md`:
::raw-md[./examples/getting-started.md]
### Rendering Documents
### Rendering Document
You have two primary ways to render your `http.md` file:
@@ -51,13 +59,13 @@ You have two primary ways to render your `http.md` file:
For a development server that outputs to your terminal and watches for changes:
```shell
httpmd dev example.md
http.md dev example.md
```
With watch mode:
```shell
httpmd dev --watch example.md
http.md dev --watch example.md
```
This command will process `example.md`, execute the HTTP requests, and print the resulting markdown (with responses filled in) to the terminal. With `--watch`, any changes to `example.md` will trigger a re-run.
@@ -66,13 +74,13 @@ You have two primary ways to render your `http.md` file:
To generate a new markdown file with the responses and templated values rendered:
```shell
httpmd build example.md output.md
http.md build example.md output.md
```
With watch mode:
```shell
httpmd build --watch example.md output.md
http.md build --watch example.md output.md
```
This creates `output.md`, which is a static snapshot of `example.md` after all requests have been executed and templating applied. This file is suitable for version control, sharing, or integration with static site generators.
@@ -179,7 +187,7 @@ Within your markdown document, the following variables are available in the Hand
- **`input`** (Object): A dictionary of variables passed to `http.md` via the command line using the `-i` or `--input` flag.
- Example: If you run `httpmd dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
- Example: If you run `http.md dev -i userId=123 -i apiKey=secret myfile.md`, you can use `{{input.userId}}` and `{{input.apiKey}}`.
### Templating Examples
@@ -203,7 +211,7 @@ _(Note: `httpbin.org/post` wraps the JSON sent in a "json" field in its response
GET https://httpbin.org/status/201
```
The request to `/status/201` completed with status code: **{{response.status}}**.
The request to `/status/201` completed with status code: **{{{response.status}}}**.
````
## Managing Documents
@@ -244,15 +252,15 @@ You can pass external data into your `http.md` documents using the `-i` (or `--i
**CLI Command:**
```shell
httpmd build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
http.md build mydoc.md output.md -i baseUrl=https://api.production.example.com -i apiKey=YOUR_SECRET_KEY
```
**Markdown Usage (`mydoc.md`):**
````markdown
```http
GET {{input.baseUrl}}/users/1
Authorization: Bearer {{input.apiKey}}
GET {{{input.baseUrl}}}/users/1
Authorization: Bearer {{{input.apiKey}}}
```
::response
@@ -260,6 +268,33 @@ Authorization: Bearer {{input.apiKey}}
**Security Note:** For sensitive data like API keys, using input variables is highly recommended over hardcoding them in your markdown files. Avoid committing files with plaintext secrets; instead, provide them at runtime via the CLI.
### JavaScript Execution
You can execute `javascript` blocks by adding a `run` option which allows programmatically changing the context, making request assertions and solve other more advanced use cases
**Example:**
::raw-md[./examples/with-javascript.md]
<details>
<summary>Output</summary>
::raw-md[./examples/with-javascript.md]{render}
</details>
**Options:**
- `run`: If present the code block will be executed
- Example: ` ```javascript run `
- `hidden`: If present the code block will not be included in the resulting output
- Example: ` ```javascript hidden `
- `output`: If present the code blocks return value will be rendered as a `yaml` code block
### HTTP Block Configuration Options
You can configure the behavior of each `http` code block by adding options to its info string, separated by commas.
@@ -301,8 +336,8 @@ You can configure the behavior of each `http` code block by adding options to it
````markdown
```http id=complexRequest,json,yaml,hidden
POST {{input.apiEndpoint}}/data
X-API-Key: {{input.apiKey}}
POST {{{input.apiEndpoint}}}/data
X-API-Key: {{{input.apiKey}}}
Content-Type: application/json
# Request body written in YAML, will be converted to JSON
@@ -353,9 +388,9 @@ The `::input` directive is used to declare expected input variables
## Command-Line Interface (CLI)
The `httpmd` tool provides the following commands:
The `http.md` tool provides the following commands:
### `httpmd dev <source_file.md>`
### `http.md dev <source_file.md>`
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and prints the resulting markdown to the **terminal (stdout)**.
@@ -367,10 +402,10 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
**Example:**
```shell
httpmd dev api_tests.md --watch -i host=localhost:3000
http.md dev api_tests.md --watch -i host=localhost:3000
```
### `httpmd build <source_file.md> <output_file.md>`
### `http.md build <source_file.md> <output_file.md>`
Processes the `<source_file.md>`, executes all HTTP requests, resolves templates, and saves the resulting markdown to `<output_file.md>`.
@@ -382,5 +417,5 @@ Processes the `<source_file.md>`, executes all HTTP requests, resolves templates
**Example:**
```shell
httpmd build official_api_docs.md public/api_docs_v1.md -i version=v1.0
http.md build official_api_docs.md public/api_docs_v1.md -i version=v1.0
```

View File

@@ -0,0 +1,17 @@
```javascript run
input.test = "Hello World";
```
::input[test]
```http json
POST https://httpbin.org/post
{"input": "{{input.test}}"}
```
```javascript run,hidden
// Use chai's `expect`, `assert` or `should` to make assumptions
expect(response.body.json.input).to.equal("Hello World");
```

View File

@@ -2,7 +2,7 @@
First, create a resource:
```http #createUser,format=yaml
```http #createUser,yaml,json
POST https://httpbin.org/post
Content-Type: application/json

View File

@@ -1,11 +1,11 @@
{
"name": "@morten-olsen/httpmd",
"name": "@morten-olsen/http.md",
"version": "1.0.0",
"description": "",
"main": "dist/exports.js",
"type": "module",
"bin": {
"httpmd": "./bin/cli.mjs"
"http.md": "./bin/cli.mjs"
},
"files": [
"dist",
@@ -27,26 +27,32 @@
"devDependencies": {
"@pnpm/find-workspace-packages": "^6.0.9",
"@types/blessed": "^0.1.25",
"@types/chai": "^5.2.2",
"@types/ink": "^2.0.3",
"@types/marked-terminal": "^6.1.1",
"@types/mdast": "^4.0.4",
"@types/node": "^22.15.18",
"@types/react": "^18.3.12",
"@types/terminal-kit": "^2.5.7",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"dependencies": {
"blessed": "^0.1.81",
"chai": "^5.2.0",
"chalk": "^5.4.1",
"commander": "^14.0.0",
"dotenv": "^16.5.0",
"eventemitter3": "^5.0.1",
"handlebars": "^4.7.8",
"hastscript": "^9.0.1",
"ink": "^5.2.1",
"marked": "^15.0.11",
"marked-terminal": "^7.3.0",
"mdast-util-to-markdown": "^2.1.2",
"mdast-util-to-string": "^4.0.0",
"mdast-util-toc": "^7.1.0",
"react": "^18.3.1",
"rehype-stringify": "^10.0.1",
"remark-behead": "^3.1.0",
"remark-directive": "^4.0.0",
@@ -57,6 +63,7 @@
"terminal-kit": "^3.1.2",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0",
"wrap-ansi": "^9.0.0",
"yaml": "^2.8.0"
}
}

396
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
blessed:
specifier: ^0.1.81
version: 0.1.81
chai:
specifier: ^5.2.0
version: 5.2.0
chalk:
specifier: ^5.4.1
version: 5.4.1
@@ -29,6 +32,9 @@ importers:
hastscript:
specifier: ^9.0.1
version: 9.0.1
ink:
specifier: ^5.2.1
version: 5.2.1(@types/react@18.3.21)(react@18.3.1)
marked:
specifier: ^15.0.11
version: 15.0.11
@@ -44,6 +50,9 @@ importers:
mdast-util-toc:
specifier: ^7.1.0
version: 7.1.0
react:
specifier: ^18.3.1
version: 18.3.1
rehype-stringify:
specifier: ^10.0.1
version: 10.0.1
@@ -74,6 +83,9 @@ importers:
unist-util-visit:
specifier: ^5.0.0
version: 5.0.0
wrap-ansi:
specifier: ^9.0.0
version: 9.0.0
yaml:
specifier: ^2.8.0
version: 2.8.0
@@ -84,6 +96,12 @@ importers:
'@types/blessed':
specifier: ^0.1.25
version: 0.1.25
'@types/chai':
specifier: ^5.2.2
version: 5.2.2
'@types/ink':
specifier: ^2.0.3
version: 2.0.3(@types/react@18.3.21)(react@18.3.1)
'@types/marked-terminal':
specifier: ^6.1.1
version: 6.1.1
@@ -93,6 +111,9 @@ importers:
'@types/node':
specifier: ^22.15.18
version: 22.15.18
'@types/react':
specifier: ^18.3.12
version: 18.3.21
'@types/terminal-kit':
specifier: ^2.5.7
version: 2.5.7
@@ -105,6 +126,10 @@ importers:
packages:
'@alcalzone/ansi-tokenize@0.1.3':
resolution: {integrity: sha512-3yWxPTq3UQ/FY9p1ErPxIyfT64elWaMvM9lIHnaqpyft63tkxodF5aUElYHrdisWve5cETkh1+KBw1yJuW0aRw==}
engines: {node: '>=14.13.1'}
'@babel/code-frame@7.27.1':
resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
engines: {node: '>=6.9.0'}
@@ -437,12 +462,22 @@ packages:
'@types/cardinal@2.1.1':
resolution: {integrity: sha512-/xCVwg8lWvahHsV2wXZt4i64H1sdL+sN1Uoq7fAc8/FA6uYHjuIveDwPwvGUYp4VZiv85dVl6J/Bum3NDAOm8g==}
'@types/chai@5.2.2':
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/deep-eql@4.0.2':
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
'@types/hast@3.0.4':
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
'@types/ink@2.0.3':
resolution: {integrity: sha512-DYKIKEJqhsGfQ/jgX0t9BzfHmBJ/9dBBT2MDsHAQRAfOPhEe7LZm5QeNBx1J34/e108StCPuJ3r4bh1y38kCJA==}
deprecated: This is a stub types definition. ink provides its own type definitions, so you do not need this installed.
'@types/marked-terminal@6.1.1':
resolution: {integrity: sha512-DfoUqkmFDCED7eBY9vFUhJ9fW8oZcMAK5EwRDQ9drjTbpQa+DnBTQQCwWhTFVf4WsZ6yYcJTI8D91wxTWXRZZQ==}
@@ -458,6 +493,12 @@ packages:
'@types/node@22.15.18':
resolution: {integrity: sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg==}
'@types/prop-types@15.7.14':
resolution: {integrity: sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==}
'@types/react@18.3.21':
resolution: {integrity: sha512-gXLBtmlcRJeT09/sI4PxVwyrku6SaNUj/6cMubjE6T6XdY1fDmBL7r0nX0jbSZPU/Xr0KuwLLZh6aOYY5d91Xw==}
'@types/ssri@7.1.5':
resolution: {integrity: sha512-odD/56S3B51liILSk5aXJlnYt99S6Rt9EFDDqGtJM26rKHApHcwyU/UoYHrzKkdkHMAIquGWCuHtQTbes+FRQw==}
@@ -510,6 +551,10 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.1:
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
engines: {node: '>=12'}
any-promise@1.3.0:
resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==}
@@ -522,6 +567,14 @@ packages:
as-table@1.0.55:
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
auto-bind@5.0.1:
resolution: {integrity: sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
bail@2.0.2:
resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==}
@@ -564,6 +617,10 @@ packages:
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
chai@5.2.0:
resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==}
engines: {node: '>=12'}
chalk@4.1.2:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
@@ -588,6 +645,10 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
check-error@2.1.1:
resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==}
engines: {node: '>= 16'}
chroma-js@2.6.0:
resolution: {integrity: sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==}
@@ -595,10 +656,18 @@ packages:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'}
cli-boxes@3.0.0:
resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
engines: {node: '>=10'}
cli-columns@4.0.0:
resolution: {integrity: sha512-XW2Vg+w+L9on9wtwKpyzluIPCWXjaBahI7mTcYjx+BVIYD9c3yqcv/yKC7CmdCZat4rq2yiE1UMSJC5ivKfMtQ==}
engines: {node: '>= 10'}
cli-cursor@4.0.0:
resolution: {integrity: sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
cli-highlight@2.1.11:
resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==}
engines: {node: '>=8.0.0', npm: '>=5.0.0'}
@@ -608,6 +677,10 @@ packages:
resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
engines: {node: 10.* || >= 12.*}
cli-truncate@4.0.0:
resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==}
engines: {node: '>=18'}
cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
@@ -615,6 +688,10 @@ packages:
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
engines: {node: '>=0.8'}
code-excerpt@4.0.0:
resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@@ -632,6 +709,10 @@ packages:
config-chain@1.1.13:
resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==}
convert-to-spaces@2.0.1:
resolution: {integrity: sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
@@ -640,6 +721,9 @@ packages:
resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==}
engines: {node: '>=8'}
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
cwise-compiler@1.1.3:
resolution: {integrity: sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==}
@@ -658,6 +742,10 @@ packages:
decode-named-character-reference@1.1.0:
resolution: {integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==}
deep-eql@5.0.2:
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
engines: {node: '>=6'}
defaults@1.0.4:
resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==}
@@ -676,6 +764,9 @@ packages:
resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
engines: {node: '>=12'}
emoji-regex@10.4.0:
resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -689,6 +780,9 @@ packages:
error-ex@1.3.2:
resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==}
es-toolkit@1.38.0:
resolution: {integrity: sha512-OT3AxczYYd3W50bCj4V0hKoOAfqIy9tof0leNQYekEDxVKir3RTVTJOLij7VAe6fsCNsGhC0JqIkURpMXTCSEA==}
esbuild@0.25.4:
resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==}
engines: {node: '>=18'}
@@ -698,6 +792,10 @@ packages:
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
engines: {node: '>=6'}
escape-string-regexp@2.0.0:
resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==}
engines: {node: '>=8'}
escape-string-regexp@4.0.0:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
@@ -742,6 +840,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.3.0:
resolution: {integrity: sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==}
engines: {node: '>=18'}
get-source@2.0.12:
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
@@ -800,6 +902,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
indent-string@5.0.0:
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
engines: {node: '>=12'}
individual@3.0.0:
resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==}
@@ -813,6 +919,19 @@ packages:
resolution: {integrity: sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
ink@5.2.1:
resolution: {integrity: sha512-BqcUyWrG9zq5HIwW6JcfFHsIYebJkWWb4fczNah1goUO0vv5vneIlfwuS85twyJ5hYR/y18FlAYUxrO9ChIWVg==}
engines: {node: '>=18'}
peerDependencies:
'@types/react': '>=18.0.0'
react: '>=18.0.0'
react-devtools-core: ^4.19.1
peerDependenciesMeta:
'@types/react':
optional: true
react-devtools-core:
optional: true
iota-array@1.0.0:
resolution: {integrity: sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==}
@@ -839,6 +958,14 @@ packages:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-fullwidth-code-point@4.0.0:
resolution: {integrity: sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==}
engines: {node: '>=12'}
is-fullwidth-code-point@5.0.0:
resolution: {integrity: sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==}
engines: {node: '>=18'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
@@ -846,6 +973,11 @@ packages:
is-hexadecimal@2.0.1:
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
is-in-ci@1.0.0:
resolution: {integrity: sha512-eUuAjybVTHMYWm/U+vBO1sY/JOCgoPCXRxzdju0K+K0BiGW0SChEL1MLC0PoCIR1OlPo5YAp8HuQoUlsWEICwg==}
engines: {node: '>=18'}
hasBin: true
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
@@ -911,6 +1043,13 @@ packages:
longest-streak@3.1.0:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
loupe@3.1.3:
resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==}
map-age-cleaner@0.1.3:
resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==}
engines: {node: '>=6'}
@@ -1179,6 +1318,10 @@ packages:
parse5@6.0.1:
resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==}
patch-console@2.0.0:
resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
path-absolute@1.0.1:
resolution: {integrity: sha512-gds5iRhSeOcDtj8gfWkRHLtZKTPsFVuh7utbjYtvnclw4XM+ffRzJrwqMhOD1PVqef7nBLmgsu1vIujjvAJrAw==}
engines: {node: '>=4'}
@@ -1194,6 +1337,10 @@ packages:
resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==}
engines: {node: '>=8.15'}
pathval@2.0.0:
resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==}
engines: {node: '>= 14.16'}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
@@ -1229,6 +1376,16 @@ packages:
resolution: {integrity: sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==}
engines: {node: '>=8'}
react-reconciler@0.29.2:
resolution: {integrity: sha512-zZQqIiYgDCTP/f1N/mAR10nJGrPD2ZR+jDSEsKWJHYC7Cm2wodlwbR3upZRdC3cjIjSlTLNVyO7Iu0Yy7t2AYg==}
engines: {node: '>=0.10.0'}
peerDependencies:
react: ^18.3.1
react@18.3.1:
resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
read-ini-file@4.0.0:
resolution: {integrity: sha512-zz4qv/sKETv7nAkATqSJ9YMbKD8NXRPuA8d17VdYCuNYrVstB1S6UAMU6aytf5vRa9MESbZN7jLZdcmrOxz4gg==}
engines: {node: '>=14.6'}
@@ -1274,6 +1431,10 @@ packages:
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
restore-cursor@4.0.0:
resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
@@ -1296,6 +1457,9 @@ packages:
resolution: {integrity: sha512-vdTshSQ2JsRCgT8eKZWNJIL26C6bVqy1SOmuCMlKHegVeo8KYRobRrefOdUq9OozSPUUiSxrylteeRmLOMFfWg==}
engines: {node: '>=12'}
scheduler@0.23.2:
resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==}
semver@7.7.2:
resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
engines: {node: '>=10'}
@@ -1327,6 +1491,14 @@ packages:
resolution: {integrity: sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==}
engines: {node: '>=8'}
slice-ansi@5.0.0:
resolution: {integrity: sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==}
engines: {node: '>=12'}
slice-ansi@7.1.0:
resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==}
engines: {node: '>=18'}
sort-keys@4.2.0:
resolution: {integrity: sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg==}
engines: {node: '>=8'}
@@ -1341,6 +1513,10 @@ packages:
split2@3.2.2:
resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==}
stack-utils@2.0.6:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'}
stacktracey@2.1.8:
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
@@ -1356,6 +1532,10 @@ packages:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
@@ -1366,6 +1546,10 @@ packages:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
strip-ansi@7.1.0:
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
engines: {node: '>=12'}
strip-bom@4.0.0:
resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==}
engines: {node: '>=8'}
@@ -1429,6 +1613,10 @@ packages:
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
engines: {node: '>=8'}
type-fest@4.41.0:
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
engines: {node: '>=16'}
typescript@5.8.3:
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
engines: {node: '>=14.17'}
@@ -1528,6 +1716,10 @@ packages:
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
engines: {node: '>=8'}
widest-line@5.0.0:
resolution: {integrity: sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==}
engines: {node: '>=18'}
wordwrap@1.0.0:
resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==}
@@ -1535,6 +1727,10 @@ packages:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
wrap-ansi@9.0.0:
resolution: {integrity: sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==}
engines: {node: '>=18'}
write-file-atomic@5.0.1:
resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -1543,6 +1739,18 @@ packages:
resolution: {integrity: sha512-FdNA4RyH1L43TlvGG8qOMIfcEczwA5ij+zLXUy3Z83CjxhLvcV7/Q/8pk22wnCgYw7PJhtK+7lhO+qqyT4NdvQ==}
engines: {node: '>=16.14'}
ws@8.18.2:
resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==}
engines: {node: '>=10.0.0'}
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: '>=5.0.2'
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -1560,11 +1768,19 @@ packages:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
yoga-layout@3.2.1:
resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
snapshots:
'@alcalzone/ansi-tokenize@0.1.3':
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 4.0.0
'@babel/code-frame@7.27.1':
dependencies:
'@babel/helper-validator-identifier': 7.27.1
@@ -1916,14 +2132,30 @@ snapshots:
'@types/cardinal@2.1.1': {}
'@types/chai@5.2.2':
dependencies:
'@types/deep-eql': 4.0.2
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
'@types/deep-eql@4.0.2': {}
'@types/hast@3.0.4':
dependencies:
'@types/unist': 3.0.3
'@types/ink@2.0.3(@types/react@18.3.21)(react@18.3.1)':
dependencies:
ink: 5.2.1(@types/react@18.3.21)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- bufferutil
- react
- react-devtools-core
- utf-8-validate
'@types/marked-terminal@6.1.1':
dependencies:
'@types/cardinal': 2.1.1
@@ -1943,6 +2175,13 @@ snapshots:
dependencies:
undici-types: 6.21.0
'@types/prop-types@15.7.14': {}
'@types/react@18.3.21':
dependencies:
'@types/prop-types': 15.7.14
csstype: 3.1.3
'@types/ssri@7.1.5':
dependencies:
'@types/node': 22.15.18
@@ -1990,6 +2229,8 @@ snapshots:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.1: {}
any-promise@1.3.0: {}
archy@1.0.0: {}
@@ -2000,6 +2241,10 @@ snapshots:
dependencies:
printable-characters: 1.0.42
assertion-error@2.0.1: {}
auto-bind@5.0.1: {}
bail@2.0.2: {}
better-path-resolve@1.0.0:
@@ -2044,6 +2289,14 @@ snapshots:
ccount@2.0.1: {}
chai@5.2.0:
dependencies:
assertion-error: 2.0.1
check-error: 2.1.1
deep-eql: 5.0.2
loupe: 3.1.3
pathval: 2.0.0
chalk@4.1.2:
dependencies:
ansi-styles: 4.3.0
@@ -2061,15 +2314,23 @@ snapshots:
character-reference-invalid@2.0.1: {}
check-error@2.1.1: {}
chroma-js@2.6.0: {}
cli-boxes@2.2.1: {}
cli-boxes@3.0.0: {}
cli-columns@4.0.0:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
cli-cursor@4.0.0:
dependencies:
restore-cursor: 4.0.0
cli-highlight@2.1.11:
dependencies:
chalk: 4.1.2
@@ -2085,6 +2346,11 @@ snapshots:
optionalDependencies:
'@colors/colors': 1.5.0
cli-truncate@4.0.0:
dependencies:
slice-ansi: 5.0.0
string-width: 7.2.0
cliui@7.0.4:
dependencies:
string-width: 4.2.3
@@ -2093,6 +2359,10 @@ snapshots:
clone@1.0.4: {}
code-excerpt@4.0.0:
dependencies:
convert-to-spaces: 2.0.1
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@@ -2108,6 +2378,8 @@ snapshots:
ini: 1.3.8
proto-list: 1.2.4
convert-to-spaces@2.0.1: {}
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
@@ -2116,6 +2388,8 @@ snapshots:
crypto-random-string@2.0.0: {}
csstype@3.1.3: {}
cwise-compiler@1.1.3:
dependencies:
uniq: 1.0.1
@@ -2130,6 +2404,8 @@ snapshots:
dependencies:
character-entities: 2.0.2
deep-eql@5.0.2: {}
defaults@1.0.4:
dependencies:
clone: 1.0.4
@@ -2144,6 +2420,8 @@ snapshots:
dotenv@16.5.0: {}
emoji-regex@10.4.0: {}
emoji-regex@8.0.0: {}
emojilib@2.4.0: {}
@@ -2154,6 +2432,8 @@ snapshots:
dependencies:
is-arrayish: 0.2.1
es-toolkit@1.38.0: {}
esbuild@0.25.4:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.4
@@ -2184,6 +2464,8 @@ snapshots:
escalade@3.2.0: {}
escape-string-regexp@2.0.0: {}
escape-string-regexp@4.0.0: {}
escape-string-regexp@5.0.0: {}
@@ -2229,6 +2511,8 @@ snapshots:
get-caller-file@2.0.5: {}
get-east-asian-width@1.3.0: {}
get-source@2.0.12:
dependencies:
data-uri-to-buffer: 2.0.2
@@ -2299,6 +2583,8 @@ snapshots:
imurmurhash@0.1.4: {}
indent-string@5.0.0: {}
individual@3.0.0: {}
inherits@2.0.4: {}
@@ -2307,6 +2593,39 @@ snapshots:
ini@3.0.1: {}
ink@5.2.1(@types/react@18.3.21)(react@18.3.1):
dependencies:
'@alcalzone/ansi-tokenize': 0.1.3
ansi-escapes: 7.0.0
ansi-styles: 6.2.1
auto-bind: 5.0.1
chalk: 5.4.1
cli-boxes: 3.0.0
cli-cursor: 4.0.0
cli-truncate: 4.0.0
code-excerpt: 4.0.0
es-toolkit: 1.38.0
indent-string: 5.0.0
is-in-ci: 1.0.0
patch-console: 2.0.0
react: 18.3.1
react-reconciler: 0.29.2(react@18.3.1)
scheduler: 0.23.2
signal-exit: 3.0.7
slice-ansi: 7.1.0
stack-utils: 2.0.6
string-width: 7.2.0
type-fest: 4.41.0
widest-line: 5.0.0
wrap-ansi: 9.0.0
ws: 8.18.2
yoga-layout: 3.2.1
optionalDependencies:
'@types/react': 18.3.21
transitivePeerDependencies:
- bufferutil
- utf-8-validate
iota-array@1.0.0: {}
is-alphabetical@2.0.1: {}
@@ -2326,12 +2645,20 @@ snapshots:
is-fullwidth-code-point@3.0.0: {}
is-fullwidth-code-point@4.0.0: {}
is-fullwidth-code-point@5.0.0:
dependencies:
get-east-asian-width: 1.3.0
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-hexadecimal@2.0.1: {}
is-in-ci@1.0.0: {}
is-number@7.0.0: {}
is-plain-obj@2.1.0: {}
@@ -2377,6 +2704,12 @@ snapshots:
longest-streak@3.1.0: {}
loose-envify@1.4.0:
dependencies:
js-tokens: 4.0.0
loupe@3.1.3: {}
map-age-cleaner@0.1.3:
dependencies:
p-defer: 1.0.0
@@ -2849,6 +3182,8 @@ snapshots:
parse5@6.0.1: {}
patch-console@2.0.0: {}
path-absolute@1.0.1: {}
path-key@3.1.1: {}
@@ -2859,6 +3194,8 @@ snapshots:
dependencies:
unique-string: 2.0.0
pathval@2.0.0: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
@@ -2881,6 +3218,16 @@ snapshots:
quick-lru@4.0.1: {}
react-reconciler@0.29.2(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react@18.3.1:
dependencies:
loose-envify: 1.4.0
read-ini-file@4.0.0:
dependencies:
ini: 3.0.1
@@ -2960,6 +3307,11 @@ snapshots:
resolve-pkg-maps@1.0.0: {}
restore-cursor@4.0.0:
dependencies:
onetime: 5.1.2
signal-exit: 3.0.7
reusify@1.1.0: {}
right-pad@1.1.1: {}
@@ -2980,6 +3332,10 @@ snapshots:
execa: 5.1.1
path-name: 1.0.0
scheduler@0.23.2:
dependencies:
loose-envify: 1.4.0
semver@7.7.2: {}
setimmediate@1.0.5: {}
@@ -3002,6 +3358,16 @@ snapshots:
dependencies:
unicode-emoji-modifier-base: 1.0.0
slice-ansi@5.0.0:
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 4.0.0
slice-ansi@7.1.0:
dependencies:
ansi-styles: 6.2.1
is-fullwidth-code-point: 5.0.0
sort-keys@4.2.0:
dependencies:
is-plain-obj: 2.1.0
@@ -3014,6 +3380,10 @@ snapshots:
dependencies:
readable-stream: 3.6.2
stack-utils@2.0.6:
dependencies:
escape-string-regexp: 2.0.0
stacktracey@2.1.8:
dependencies:
as-table: 1.0.55
@@ -3032,6 +3402,12 @@ snapshots:
is-fullwidth-code-point: 3.0.0
strip-ansi: 6.0.1
string-width@7.2.0:
dependencies:
emoji-regex: 10.4.0
get-east-asian-width: 1.3.0
strip-ansi: 7.1.0
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
@@ -3045,6 +3421,10 @@ snapshots:
dependencies:
ansi-regex: 5.0.1
strip-ansi@7.1.0:
dependencies:
ansi-regex: 6.1.0
strip-bom@4.0.0: {}
strip-comments-strings@1.2.0: {}
@@ -3106,6 +3486,8 @@ snapshots:
type-fest@0.6.0: {}
type-fest@4.41.0: {}
typescript@5.8.3: {}
uglify-js@3.19.3:
@@ -3230,6 +3612,10 @@ snapshots:
dependencies:
string-width: 4.2.3
widest-line@5.0.0:
dependencies:
string-width: 7.2.0
wordwrap@1.0.0: {}
wrap-ansi@7.0.0:
@@ -3238,6 +3624,12 @@ snapshots:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi@9.0.0:
dependencies:
ansi-styles: 6.2.1
string-width: 7.2.0
strip-ansi: 7.1.0
write-file-atomic@5.0.1:
dependencies:
imurmurhash: 0.1.4
@@ -3248,6 +3640,8 @@ snapshots:
js-yaml: 4.1.0
write-file-atomic: 5.0.1
ws@8.18.2: {}
y18n@5.0.8: {}
yaml@2.8.0: {}
@@ -3264,4 +3658,6 @@ snapshots:
y18n: 5.0.8
yargs-parser: 20.2.9
yoga-layout@3.2.1: {}
zwitch@2.0.4: {}

View File

@@ -1,13 +1,14 @@
import { program } from 'commander';
import { resolve } from 'node:path';
import { Marked } from 'marked';
import { markedTerminal } from 'marked-terminal';
import { execute } from '../execution/execution.js';
import { Context } from '../context/context.js';
import { writeFile } from 'node:fs/promises';
import { Watcher } from '../watcher/watcher.js';
import { UI } from './ui/ui.js';
import { wrapBody } from '../theme/theme.html.js';
import { loadInputFiles } from '../utils/input.js';
import { InvalidFormatError } from '../utils/errors.js';
import { renderUI, State } from './ui/ui.js';
import { Marked } from 'marked';
@@ -16,23 +17,27 @@ program
.argument('<name>', 'http.md file name')
.description('Run a http.md document')
.option('-w, --watch', 'watch for changes')
.option('-f, --file <file...>', 'input files (-f foo.js -f bar.json)')
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
.action(async (name, options) => {
const marked = new Marked();
marked.use(markedTerminal() as any);
const {
file: f = [],
watch = false,
input: i = [],
} = options;
const ui = new UI();
const input = Object.fromEntries(
const input = {
...Object.fromEntries(
i.map((item: string) => {
const [key, value] = item.split('=');
return [key, value];
})
);
),
...loadInputFiles(f),
};
const state = new State<any>({
markdown: 'Loading',
});
const filePath = resolve(process.cwd(), name);
const build = async () => {
@@ -43,8 +48,10 @@ program
context,
});
const markdown = await marked.parse(result.markdown);
ui.content = markdown;
state.setState({
error: result.error ? result.error instanceof Error ? result.error.message : result.error : undefined,
markdown: result.markdown,
});
return {
...result,
@@ -53,10 +60,7 @@ program
}
const result = await build();
ui.screen.key(['r'], () => {
build();
});
renderUI(state);
if (watch) {
const watcher = new Watcher();
@@ -75,23 +79,28 @@ program
.argument('<name>', 'http.md file name')
.argument('<output>', 'output file name')
.description('Run a http.md document')
.option('-f, --format <format>', 'output format (html, markdown)')
.option('-f, --file <file...>', 'input files (-f foo.js -f bar.json)')
.option('--format <format>', 'output format (html, markdown)')
.option('-w, --watch', 'watch for changes')
.option('-i, --input <input...>', 'input variables (-i foo=bar -i baz=qux)')
.action(async (name, output, options) => {
const {
watch = false,
file: f = [],
input: i = [],
format = 'markdown',
} = options;
const input = Object.fromEntries(
const input = {
...Object.fromEntries(
i.map((item: string) => {
const [key, value] = item.split('=');
return [key, value];
})
);
),
...loadInputFiles(f),
}
const filePath = resolve(process.cwd(), name);
const build = async () => {
@@ -102,6 +111,10 @@ program
context,
});
if (result.error) {
console.error(result.error);
}
if (format === 'html') {
const marked = new Marked();
const html = await marked.parse(result.markdown);
@@ -109,7 +122,7 @@ program
} else if (format === 'markdown') {
await writeFile(output, result.markdown);
} else {
throw new Error('Invalid format');
throw new InvalidFormatError('Invalid format');
}
return {
...result,
@@ -119,6 +132,10 @@ program
const result = await build();
if (result.error && !watch) {
process.exit(1);
}
if (watch) {
const watcher = new Watcher();
watcher.watchFiles(Array.from(result.context.files));

View File

@@ -0,0 +1,55 @@
import React, { useMemo } from 'react';
import { Box, Text, useInput } from 'ink';
import { marked } from 'marked';
import TerminalRenderer from 'marked-terminal';
import wrapAnsi from 'wrap-ansi';
import os from 'os';
import { useTerminalHeight, useTerminalWidth } from '../hooks/terminal.js';
type ScrollableMarkdownProps = {
markdownContent: string;
};
const renderer = new TerminalRenderer({});
marked.setOptions({ renderer: renderer as any });
const ScrollableMarkdown = ({ markdownContent }: ScrollableMarkdownProps) => {
const [scrollPosition, setScrollPosition] = React.useState(0);
const terminalHeight = useTerminalHeight()
const terminalWidth = useTerminalWidth();
const rendered = useMemo(() => {
return marked(markdownContent) as string;
}, [markdownContent]);
const wrapped = useMemo(() => {
return wrapAnsi(rendered, terminalWidth, {
hard: true,
trim: false,
wordWrap: true,
}).split(os.EOL);
}, [rendered, terminalWidth]);
const buffer = useMemo(() => {
return wrapped.slice(scrollPosition, scrollPosition + terminalHeight).join(os.EOL);
}, [wrapped, scrollPosition, terminalHeight]);
useInput((input, key) => {
if (key.downArrow) {
setScrollPosition(prev => prev + 1);
}
if (key.upArrow) {
setScrollPosition(prev => Math.max(0, prev - 1));
}
if (key.escape) {
process.exit(0);
}
console.error('a', input, key);
});
return (
<Box flexDirection="column" flexShrink={1} flexGrow={1} overflow="hidden">
<Text>{buffer}</Text>
</Box>
);
};
export { ScrollableMarkdown };

View File

@@ -0,0 +1,39 @@
import { useEffect, useState } from "react"
const useTerminalWidth = () => {
const [width, setWidth] = useState(process.stdout.columns || 80);
useEffect(() => {
const handleResize = () => {
setWidth(process.stdout.columns);
};
process.stdout.on("resize", handleResize);
return () => {
process.stdout.off("resize", handleResize);
};
}, []);
return width;
}
const useTerminalHeight = () => {
const [height, setHeight] = useState(process.stdout.rows || 24);
useEffect(() => {
const handleResize = () => {
setHeight(process.stdout.rows);
};
process.stdout.on("resize", handleResize);
return () => {
process.stdout.off("resize", handleResize);
};
}, []);
return height;
}
export { useTerminalWidth, useTerminalHeight }

View File

@@ -0,0 +1,80 @@
import { EventEmitter } from 'eventemitter3';
import React, { createContext, useRef, useState, useSyncExternalStore } from "react";
type StateEvents = {
update: () => void;
}
class State<T = any> extends EventEmitter<StateEvents> {
state: T;
constructor(initialState: T) {
super();
this.state = initialState;
}
public get value() {
return this.state;
}
public setState = (state: T) => {
this.state = state;
this.emit('update');
}
public subscribe = (callback: () => void) => {
this.on('update', callback);
return () => {
this.off('update', callback);
};
}
}
type StateContextValue<T> = {
state: State<T>;
}
const StateContext = createContext<StateContextValue<any> | null>(null);
type StateProviderProps<T> = {
state: State<T>;
children: React.ReactNode;
}
const StateProvider = <T,>({ state, children }: StateProviderProps<T>) => {
return (
<StateContext.Provider value={{ state }}>
{children}
</StateContext.Provider>
);
};
const useStateContext = <T = any>() => {
const context = React.useContext(StateContext);
if (!context) {
throw new Error("useStateContext must be used within a StateProvider");
}
return context as StateContextValue<T>;
}
const useStateValue = <T = any>(
selector: (state: T) => any = (state) => state,
) => {
const context = useStateContext<T>();
const value = useRef<T>(selector(context.state.value));
useSyncExternalStore(
context.state.subscribe,
() => {
const next = selector(context.state.value);
if (next !== value.current) {
value.current = next;
}
return value.current;
},
() => value.current,
);
return value.current;
}
export { State, StateProvider, useStateContext, useStateValue };

View File

@@ -1,70 +0,0 @@
import blessed from 'blessed';
import chalk from 'chalk';
class UI {
#box: blessed.Widgets.BoxElement;
#screen: blessed.Widgets.Screen;
constructor() {
const screen = blessed.screen({
smartCSR: true,
title: 'Markdown Viewer'
});
const scrollableBox = blessed.box({ // Or blessed.scrollablebox
parent: screen,
top: 0,
left: 0,
width: '100%',
height: '100%',
content: '',
scrollable: true,
alwaysScroll: true,
keys: true,
vi: true, // vi-like keybindings
mouse: true,
scrollbar: {
ch: ' ',
track: {
bg: 'cyan'
},
style: {
inverse: true
}
},
style: {
fg: 'white',
bg: 'black'
}
});
this.#box = scrollableBox;
this.#screen = screen;
screen.key(['escape', 'q', 'C-c'], () => {
return process.exit(0);
});
scrollableBox.focus();
screen.render();
}
public get screen() {
return this.#screen;
}
public set content(content: string) {
const originalLines = content.split('\n');
const maxLineNoDigits = String(originalLines.length).length; // For padding
const linesWithNumbers = originalLines.map((line, index) => {
const lineNumber = String(index + 1).padStart(maxLineNoDigits, ' ');
const styledLineNumber = chalk.dim.yellow(`${lineNumber} | `);
return `${styledLineNumber}${line}`;
});
const contentWithLineNumbers = linesWithNumbers.join('\n');
this.#box.setContent(contentWithLineNumbers);
this.#screen.render();
}
}
export { UI };

69
src/cli/ui/ui.tsx Normal file
View File

@@ -0,0 +1,69 @@
import { Box, render, Text, useApp, useInput } from "ink"
import { ScrollableMarkdown } from './components/scrollable-markdown.js';
import { useStateValue, State, StateProvider } from './state/state.js';
import { useEffect, useState } from "react";
import { useTerminalHeight } from "./hooks/terminal.js";
const MarkdownView = () => {
const markdown = useStateValue((state) => state.markdown);
const error = useStateValue((state) => state.error);
return (
<Box flexGrow={1} flexDirection="column">
<Box flexDirection="column" flexGrow={1} overflow="hidden">
<ScrollableMarkdown markdownContent={markdown} />
</Box>
{error && (
<Box flexDirection="column" padding={1}>
<Text color="red">{error}</Text>
</Box>
)}
</Box>
);
}
const App = () => {
const { exit } = useApp();
const height = useTerminalHeight();
useInput((_input, key) => {
if (key.escape) {
process.exit(0);
}
});
useEffect(
() => {
const enterAltScreenCommand = "\x1b[?1049h";
const leaveAltScreenCommand = "\x1b[?1049l";
process.stdout.write(enterAltScreenCommand);
const onExit = () => {
exit();
process.stdout.write(leaveAltScreenCommand);
}
process.on("exit", onExit);
return () => {
process.stdout.write(leaveAltScreenCommand);
process.off("exit", onExit);
}
}, []
);
return (
<Box height={height} flexDirection="column">
<MarkdownView />
<Box>
<Text>Press esc to exit</Text>
</Box>
</Box>
)
}
const renderUI = (state: State) => {
render(
<StateProvider state={state}>
<App />
</StateProvider>
);
}
export { renderUI, State }

View File

@@ -51,6 +51,7 @@ type ExexutionExecuteOptions = {
}
const execute = async (file: string, options: ExexutionExecuteOptions) => {
let error: unknown | undefined;
const { context } = options;
context.files.add(file);
const content = await readFile(file, 'utf-8');
@@ -94,6 +95,7 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
});
for (const step of steps) {
try {
const { node, action } = step;
const options: ExecutionStepOptions = {
file,
@@ -103,11 +105,16 @@ const execute = async (file: string, options: ExexutionExecuteOptions) => {
root,
};
await action(options);
} catch (e) {
error = e;
break;
}
}
const markdown = parser.stringify(root);
return {
error,
root,
markdown,
};

View File

@@ -5,7 +5,7 @@ const codeHandler: ExecutionHandler = ({
node,
addStep,
}) => {
if (node.type !== 'code' || node.lang === 'http') {
if (node.type !== 'code' || node.lang === 'http' || node.lang === 'javascript') {
return;
}
const optionParts = node.meta?.split(',') || [];

View File

@@ -44,7 +44,7 @@ const httpHandler: ExecutionHandler = ({
);
let parsedBody = body;
if (options.format === 'yaml') {
if (options.yaml) {
try {
const parsed = YAML.parse(body);
parsedBody = JSON.stringify(parsed);

View File

@@ -1,5 +1,6 @@
import { toString } from 'mdast-util-to-string';
import { type ExecutionHandler } from '../execution.js';
import { ParsingError, RequiredError } from '../../utils/errors.js';
const inputHandler: ExecutionHandler = ({
addStep,
@@ -15,7 +16,7 @@ const inputHandler: ExecutionHandler = ({
const name = toString(node);
if (node.attributes?.required === '' && context.input[name] === undefined) {
throw new Error(`Input "${name}" is required`);
throw new RequiredError(name);
}
if (node.attributes?.default !== undefined && context.input[name] === undefined) {
@@ -27,7 +28,7 @@ const inputHandler: ExecutionHandler = ({
if (format === 'number') {
context.input[name] = Number(context.input[name]);
if (context.input[name] !== undefined && isNaN(Number(context.input[name]))) {
throw new Error(`Input "${name}" must be a number, but got "${context.input[name]}"`);
throw new ParsingError(`Input "${name}" must be a number, but got "${context.input[name]}"`);
}
}
if (format === 'boolean') {
@@ -40,14 +41,14 @@ const inputHandler: ExecutionHandler = ({
try {
context.input[name] = JSON.parse(String(context.input[name]));
} catch (error) {
throw new Error(`Input "${name}" must be a valid JSON, but got "${context.input[name]}"`);
throw new ParsingError(`Input "${name}" must be a valid JSON, but got "${context.input[name]}"`);
}
}
if (format === 'date') {
const date = new Date(context.input[name] as string);
if (isNaN(date.getTime())) {
throw new Error(`Input "${name}" must be a valid date, but got "${context.input[name]}"`);
throw new ParsingError(`Input "${name}" must be a valid date, but got "${context.input[name]}"`);
}
context.input[name] = date;
}

View File

@@ -0,0 +1,74 @@
import Handlebars from "handlebars";
import YAML from "yaml";
import { should, expect, assert } from 'chai';
import { ExecutionHandler } from "../execution.js";
import { ScriptError } from "../../utils/errors.js";
const javascriptHandler: ExecutionHandler = ({
node,
parent,
index,
addStep,
}) => {
if (node.type !== 'code' || node.lang !== 'javascript') {
return;
}
const optionParts = node.meta?.split(',') || [];
node.meta = undefined;
const options = Object.fromEntries(
optionParts.filter(Boolean).map((option) => {
const [key, value] = option.split('=');
return [key.trim(), value?.trim() || true];
})
);
addStep({
type: 'code',
node,
action: async ({ context }) => {
const template = Handlebars.compile(node.value);
const content = template(context);
node.value = content;
if (options['run'] === true) {
const api = {
assert,
should,
expect,
...context,
}
try {
// eslint-disable-next-line no-new-func
const asyncFunc = new Function(
...Object.keys(api),
`return (async () => { ${content} })()`
);
const result = await asyncFunc(...Object.values(api));
if (options.output === true && index !== undefined) {
if (result !== undefined) {
parent?.children?.splice(index + 1, 0, {
type: 'code',
lang: 'yaml',
value: YAML.stringify(result, null, 2),
meta: undefined,
});
}
}
} catch (error) {
if (index !== undefined) {
parent?.children?.splice(index + 1, 0, {
type: 'code',
value: `Error: ${error instanceof Error ? error.message : String(error)}`,
meta: undefined,
});
}
throw new ScriptError(error instanceof Error ? error.message : String(error))
}
}
if (options.hidden === true && parent && index !== undefined) {
parent.children?.splice(index, 1);
}
},
});
};
export { javascriptHandler };

View File

@@ -1,6 +1,8 @@
import { dirname, resolve } from 'path';
import { toString } from 'mdast-util-to-string'
import { execute, type ExecutionHandler } from '../execution.js';
import { FileNotFoundError } from '../../utils/errors.js';
import { existsSync } from 'fs';
const fileHandler: ExecutionHandler = ({
addStep,
@@ -18,8 +20,8 @@ const fileHandler: ExecutionHandler = ({
dirname(file),
toString(node)
);
if (!filePath) {
throw new Error('File path is required');
if (!existsSync(filePath)) {
throw new FileNotFoundError(filePath);
}
const { root: newRoot } = await execute(filePath, {
context,

View File

@@ -3,6 +3,8 @@ import { Context } from '../../context/context.js';
import { execute, type ExecutionHandler } from '../execution.js';
import { dirname, resolve } from 'path';
import { toString } from 'mdast-util-to-string';
import { FileNotFoundError } from '../../utils/errors.js';
import { existsSync } from 'fs';
const rawMdHandler: ExecutionHandler = ({
addStep,
@@ -20,6 +22,9 @@ const rawMdHandler: ExecutionHandler = ({
dirname(file),
toString(node),
);
if (!existsSync(name)) {
throw new FileNotFoundError(name);
}
const context = new Context({
input: {},
});

View File

@@ -7,6 +7,7 @@ import { responseHandler } from "./handlers.response.js";
import { textHandler } from "./handlers.text.js";
import { codeHandler } from "./handlers.code.js";
import { tocHandler } from "./handlers.toc.js";
import { javascriptHandler } from "./handlers.javascript.js";
const handlers = [
fileHandler,
@@ -16,6 +17,7 @@ const handlers = [
inputHandler,
rawMdHandler,
codeHandler,
javascriptHandler,
] satisfies ExecutionHandler[];
const postHandlers = [

44
src/utils/errors.ts Normal file
View File

@@ -0,0 +1,44 @@
class BaseError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
class FileNotFoundError extends BaseError {
constructor(filePath: string) {
super(`File not found: ${filePath}`);
}
}
class InvalidFileError extends BaseError {
constructor(filePath: string) {
super(`Invalid file: ${filePath}`);
}
}
class InvalidFormatError extends BaseError {
constructor(format: string) {
super(`Invalid format: ${format}`);
}
}
class ScriptError extends BaseError {
constructor(message: string) {
super(`Script error: ${message}`);
}
}
class ParsingError extends BaseError {
constructor(message: string) {
super(`Parsing error: ${message}`);
}
}
class RequiredError extends BaseError {
constructor(name: string) {
super(`Required input "${name}" is missing`);
}
}
export { BaseError, FileNotFoundError, InvalidFileError, InvalidFormatError, ScriptError, ParsingError, RequiredError };

60
src/utils/input.ts Normal file
View File

@@ -0,0 +1,60 @@
import { extname } from "path";
import { FileNotFoundError, InvalidFileError } from "./errors.js";
import { existsSync } from "fs";
import YAML from "yaml";
import { readFile } from "fs/promises";
const loadJsonFile = async (filePath: string) => {
const content = await readFile(filePath, "utf-8");
return JSON.parse(content);
}
const loadYamlFile = async (filePath: string) => {
const content = await readFile(filePath, "utf-8");
return YAML.parse(content);
}
const loadJsFile = async (filePath: string) => {
const { default: content } = await import(filePath);
return content;
}
const loadInputFiles = async (filePaths: string[]) => {
let inputs: Record<string, unknown> = {};
for (const filePath of filePaths) {
const type = extname(filePath);
if (!existsSync(filePath)) {
throw new FileNotFoundError(filePath);
}
try {
switch (type) {
case ".json":
inputs = {
...inputs,
...(await loadJsonFile(filePath)),
};
break;
case ".yaml":
case ".yml":
inputs = {
...inputs,
...(await loadYamlFile(filePath)),
};
break;
case ".js":
inputs = {
...inputs,
...(await loadJsFile(filePath)),
};
break;
default:
throw new InvalidFileError(filePath);
}
} catch (error) {
throw new InvalidFileError(filePath);
}
}
}
export { loadInputFiles };

View File

@@ -10,7 +10,11 @@
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"skipLibCheck": true,
"outDir": "dist"
"outDir": "dist",
"jsx": "react-jsx"
},
"include": ["src/**/*.ts"]
"include": [
"src/**/*.ts",
"src/cli/ui/ui.tsx"
]
}