diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d8a01d3..776a650 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,6 +24,10 @@ jobs:
- run: pnpm lint
- run: pnpm test
+ # Every `exports`/`bin` target exists in the built tree. A subpath nothing
+ # in the repo imports compiles clean and 404s for the first consumer.
+ - run: pnpm check:exports
+
# The playgrounds render in a pipe, with no terminal at all.
- run: pnpm dev gallery --static --width 100 --height 30
- run: pnpm dev charts --static --ascii --mono --width 100
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..a9b8be7
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,60 @@
+name: Release
+
+on:
+ push:
+ tags: ['v*']
+ workflow_dispatch:
+ inputs:
+ dry_run:
+ description: 'Pack and check, publish nothing'
+ type: boolean
+ default: true
+
+permissions:
+ contents: read
+ id-token: write # npm provenance signs the tarballs with this
+
+jobs:
+ release:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: pnpm/action-setup@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: pnpm
+ registry-url: https://registry.npmjs.org
+
+ - run: pnpm install --frozen-lockfile
+
+ # The same gate CI runs. A tag is not a reason to publish something that
+ # does not build.
+ - run: pnpm build
+ - run: pnpm typecheck
+ - run: pnpm lint
+ - run: pnpm test
+
+ # Every `exports` and `bin` target has to exist in the built tree. tsc
+ # cannot catch a subpath nothing in the repo imports.
+ - run: node scripts/check-exports.mjs
+
+ # The tag says what the packages must say. A tag that disagrees with
+ # package.json publishes a version nobody asked for.
+ - name: Check the tag against the versions
+ if: startsWith(github.ref, 'refs/tags/v')
+ run: node scripts/check-version.mjs "${GITHUB_REF_NAME#v}"
+
+ - name: Pack
+ run: pnpm -r --filter='!@textui/registry' --filter='!@textui/playground' exec npm pack --dry-run
+
+ # `pnpm publish -r` goes in dependency order and rewrites `workspace:^`
+ # to the real version. Private packages are skipped, which is what holds
+ # documents, textide and textide-git back.
+ - name: Publish
+ if: startsWith(github.ref, 'refs/tags/v') && inputs.dry_run != true
+ run: pnpm publish -r --access public --no-git-checks
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ NPM_CONFIG_PROVENANCE: true
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..86c0d80
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,38 @@
+# Changelog
+
+The publishable packages release as a set, under one version. `workspace:^`
+between them means a mixed set resolves to a combination nobody tested, so the
+tag is the version and every package carries it.
+
+This file records the set. Anything package-specific says which package.
+
+## Unreleased
+
+### 0.1.0 - the first publish
+
+Six packages: [`textui`](packages/facade), [`@textui/core`](packages/core),
+[`@textui/widgets`](packages/widgets), [`@textui/terminal`](packages/terminal),
+[`@textui/testing`](packages/testing) and [`@textui/cli`](packages/cli).
+
+`@textui/documents`, `@textui/textide` and `@textui/textide-git` are in the
+repository and build in CI, but are held back from this release - they are
+marked `private` until their surface settles, so `pnpm publish -r` skips them.
+
+Pre-1.0: the surface is still moving.
+
+#### Fixed before publishing
+
+- `@textui/core` declared an `./hooks` export subpath pointing at
+ `dist/hooks/`, which is never emitted - hooks live in `runtime/hooks.ts` and
+ are already re-exported from the root. Nothing in the repository imported the
+ subpath, so nothing caught it; it would have been `ERR_MODULE_NOT_FOUND` for
+ the first consumer who tried it. The subpath is gone, and
+ `scripts/check-exports.mjs` now runs in CI so the next one fails a PR.
+- Every package ships the MIT `LICENSE` in its tarball. `license: "MIT"` in the
+ manifest is not the licence text, and npm only includes a `LICENSE` that sits
+ in the package's own directory.
+- Package READMEs linked to sibling packages relatively (`../core`), which
+ resolves in the repository and 404s on npmjs.com. They are absolute now.
+- The documents guide said the JSON adapter ships in `@textui/core/adapters`.
+ It ships in `@textui/documents`; `core/src/adapters` is a deliberately empty
+ placeholder, and says so.
diff --git a/DEVELOPER.md b/DEVELOPER.md
new file mode 100644
index 0000000..77272ea
--- /dev/null
+++ b/DEVELOPER.md
@@ -0,0 +1,86 @@
+# Developing TextUI
+
+For working *on* TextUI. To build something *with* it, start at
+[`README.md`](README.md) and the [documentation](https://softov.github.io/textui/).
+
+Node >= 22, pnpm 10.
+
+## Getting set up
+
+```bash
+pnpm install
+pnpm build # every package
+pnpm typecheck # every workspace
+pnpm test # every suite
+pnpm dev --list # the playgrounds
+pnpm dev gallery # open one
+```
+
+Releases are cut from a tag and publish as a set -
+[`RELEASING.md`](RELEASING.md) is the runbook, [`CHANGELOG.md`](CHANGELOG.md)
+the record.
+
+The docs site is Jekyll, and needs no Ruby on your machine - it builds in a
+container:
+
+```bash
+scripts/docs-serve.sh # live, with reload, at localhost:4000/textui/
+scripts/docs-serve.sh --build # build once, into docs/_site
+scripts/docs-preview.py # serve what was built, at localhost:8000/textui/
+scripts/docs-preview.py --host 0.0.0.0 # ...and reachable from the network
+node scripts/check-docs.mjs # the nav tree, links and titles
+```
+
+`docs-preview.py` exists because the site is built with `baseurl: /textui`, so every link in it is absolute at `/textui/...`. A plain `python -m http.server` over `docs/_site` 404s on all of it; this one mounts the site under the prefix the pages actually ask for.
+
+Node ≥ 22, pnpm 10.
+
+## The acceptance test
+
+The three layouts this project started from - a dense bordered console, an airy borderless report, and a workbench frame - are one architecture with three registrations. `playground/test/playgrounds.test.tsx` mounts the same component under all of them, and under six themes, at three terminal widths, with and without Unicode and colour. If a shell ever needs a component the others cannot use, the boundary is in the wrong place.
+
+## Why TypeScript, and how close it is to needing no build
+
+Types are stripped rather than compiled now. Node has erased them since 22.6
+behind a flag, and by default since 23.6 - so a `.ts` file with no non-erasable
+syntax in it is a file Node runs. Nothing transpiles it; the annotations are
+skipped the way a comment is.
+
+That is the direction this library is aimed at. It has no dependencies, so the
+only thing between the source and a `node` invocation is the syntax it uses -
+and most of the syntax is already fine. Types, interfaces, generics,
+`satisfies`, `as`, `import type`: all erasable, all stripped.
+
+**What is not, here:** fourteen parameter properties (`constructor(private x: T)`)
+across twelve files. That form declares a field *and* assigns it, so there is
+runtime behaviour inside a type annotation and stripping cannot be correct.
+Enums and value-carrying namespaces are the other two, and this codebase has
+neither.
+
+Setting `"erasableSyntaxOnly": true` in the tsconfig would make the compiler
+refuse the non-erasable forms, turning this from an aim into a constraint. It
+is not set yet, and the fourteen are still there - each one a mechanical
+change, the field written out and assigned in the body. Treat this section as
+the direction the library is aimed at, not a property it already has.
+
+## Releasing
+
+The publishable packages go out as a set, from a tag.
+[`RELEASING.md`](RELEASING.md) is the runbook and [`CHANGELOG.md`](CHANGELOG.md)
+the record. Two guards run in CI:
+
+```bash
+pnpm check:exports # every exports/bin target exists in the built tree
+pnpm check:version 0.1.0 # the tag and the manifests agree
+```
+
+`check:exports` is there because tsc cannot catch a subpath nothing in the
+repository imports - `@textui/core` shipped a broken `./hooks` for exactly that
+reason, and it compiled clean the whole time.
+
+## Conventions
+
+The rules a change has to hold to are in [`CLAUDE.md`](CLAUDE.md): what lives in
+`types/`, why registries are late-binding, why the store is the only state, and
+how colour, glyphs and sizing work. A component that breaks one of those will
+pass `pnpm test` and still be wrong.
diff --git a/README.md b/README.md
index a956e5c..94c6201 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,10 @@
A dependency-free TypeScript terminal UI runtime. Screens are plain data; JSX is one way to write them.
+```bash
+npm install textui @textui/widgets
+```
+
```tsx
import { render } from 'textui';
import { registerBuiltins } from '@textui/widgets';
@@ -18,28 +22,16 @@ await waitUntilExit();
Everything else follows from that: one reactive store addressed by paths, typed registries for components, commands, themes, shells and resources, and a renderer that diffs cells rather than redrawing frames.
-## Why TypeScript, and how close it is to needing no build
-
-Types are stripped rather than compiled now. Node has erased them since 22.6
-behind a flag, and by default since 23.6 - so a `.ts` file with no non-erasable
-syntax in it is a file Node runs. Nothing transpiles it; the annotations are
-skipped the way a comment is.
-
-That is the direction this library is aimed at. It has **no dependencies**, so
-the only thing between the source and a `node` invocation is the syntax it uses
-- and most of the syntax is already fine. Types, interfaces, generics,
-`satisfies`, `as`, `import type`: all erasable, all stripped.
+## No dependencies
-**What is not, here:** eleven parameter properties (`constructor(private x: T)`)
-across ten files. That form declares a field *and* assigns it, so there is
-runtime behaviour inside a type annotation and stripping cannot be correct.
-Enums and value-carrying namespaces are the other two, and this codebase has
-neither.
+Nothing is installed alongside it. `@textui/core` has an empty `dependencies`,
+and the packages above it depend only on each other - so what you audit is what
+you get, and the tree does not grow behind your back.
-`"erasableSyntaxOnly": true` in the tsconfig makes the compiler refuse the
-non-erasable forms, so the constraint is enforced rather than remembered. The
-eleven are a mechanical change - the field written out and assigned in the
-body. Worth doing before it is worth claiming.
+That also keeps the source close to running unbuilt: Node has erased types by
+default since 23.6, and most of this codebase is already erasable syntax. The
+full argument, and what is still in the way, is in
+[`DEVELOPER.md`](DEVELOPER.md).
## Packages
@@ -51,6 +43,14 @@ body. Worth doing before it is worth claiming.
| [`@textui/terminal`](packages/terminal) | Terminal adapters, capability detection, ANSI writing, input decoding |
| [`@textui/testing`](packages/testing) | Headless harness: semantic queries, input, resizing, time |
| [`@textui/cli`](packages/cli) | `textui init / add / create / doctor`, and primitives for your own CLI |
+
+Also in the repository, not yet published:
+
+| Package | What it is |
+| --- | --- |
+| [`@textui/documents`](packages/documents) | Document buffers, resource viewers and content adapters |
+| [`@textui/textide`](packages/textide) | An IDE that runs in a terminal, built on TextUI |
+| [`@textui/textide-git`](packages/textide-git) | Git for textide, as a loadable extension |
| [`components/`](components) | The source-copy registry - components you own, not import |
| [`playground/`](playground) | The showcase, fourteen focused playgrounds, and a filesystem explorer |
@@ -122,37 +122,10 @@ Here's what TextUI actually looks like and what it can do.
-Now let's break down how it's built.
-
-## Development
-
-```bash
-pnpm install
-pnpm build # every package
-pnpm typecheck # every workspace
-pnpm test # every suite
-pnpm dev --list # the playgrounds
-pnpm dev gallery # open one
-```
-
-The docs site is Jekyll, and needs no Ruby on your machine - it builds in a
-container:
-
-```bash
-scripts/docs-serve.sh # live, with reload, at localhost:4000/textui/
-scripts/docs-serve.sh --build # build once, into docs/_site
-scripts/docs-preview.py # serve what was built, at localhost:8000/textui/
-scripts/docs-preview.py --host 0.0.0.0 # ...and reachable from the network
-node scripts/check-docs.mjs # the nav tree, links and titles
-```
-
-`docs-preview.py` exists because the site is built with `baseurl: /textui`, so every link in it is absolute at `/textui/...`. A plain `python -m http.server` over `docs/_site` 404s on all of it; this one mounts the site under the prefix the pages actually ask for.
-
-Node ≥ 22, pnpm 10.
-
-## The acceptance test
+## Developing
-The three layouts this project started from - a dense bordered console, an airy borderless report, and a workbench frame - are one architecture with three registrations. `playground/test/playgrounds.test.tsx` mounts the same component under all of them, and under six themes, at three terminal widths, with and without Unicode and colour. If a shell ever needs a component the others cannot use, the boundary is in the wrong place.
+Working on TextUI rather than with it - building, testing, the playgrounds, the
+docs site and how a release is cut - is in [`DEVELOPER.md`](DEVELOPER.md).
## License
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..7239c37
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,82 @@
+# Releasing
+
+The publishable packages release as a set, under one version. They depend on
+each other with `workspace:^`, so a mixed set resolves to a combination nobody
+tested. The tag is the version; every publishable package carries it.
+
+## What publishes
+
+Six, in dependency order - `pnpm publish -r` works this out itself:
+
+```
+@textui/core
+ -> @textui/widgets, @textui/terminal
+ -> textui, @textui/testing, @textui/cli
+```
+
+`@textui/documents`, `@textui/textide` and `@textui/textide-git` are marked
+`private` in their manifests. That is what holds them back, and it is the only
+thing that does - a filter you have to remember is a filter that gets
+forgotten. Remove `private` when their surface settles and they join the set.
+
+## Once, before the first publish
+
+The `@textui` scope is reserved. What is still needed:
+
+- An npm **automation** token with publish rights on the `@textui` scope and on
+ the unscoped `textui` name, stored as the `NPM_TOKEN` repository secret:
+
+ ```bash
+ gh secret set NPM_TOKEN # paste the token when prompted
+ ```
+
+ It has to be an automation token, not a classic one - a token with 2FA on
+ publish cannot be used unattended, and the workflow has no way to answer the
+ prompt.
+
+- The unscoped `textui` name confirmed as yours. The scope covers `@textui/*`
+ but not the facade package, which publishes as bare `textui`.
+
+Nothing else is required: provenance is signed with the workflow's `id-token`
+permission, which is already granted in `release.yml`.
+
+## Cutting one
+
+1. Land everything. `main` green.
+2. Set the version on the six publishable manifests, and move the
+ `## Unreleased` heading in [`CHANGELOG.md`](CHANGELOG.md) down to the new
+ version.
+3. `pnpm check:version ` - it fails if any of the six disagrees.
+4. Tag and push:
+
+ ```bash
+ git tag v0.1.0
+ git push origin v0.1.0
+ ```
+
+The `Release` workflow runs the same gate CI runs - build, typecheck, lint,
+test - plus `check:exports` and the tag/version check, packs every package, and
+only then publishes with provenance.
+
+## Rehearsing one
+
+The workflow takes a manual `workflow_dispatch` with `dry_run` on by default:
+it runs the whole gate and packs every tarball, and publishes nothing. Use it
+before the first real tag.
+
+Locally:
+
+```bash
+pnpm build && pnpm check:exports # what a consumer will actually resolve
+pnpm -r exec npm pack --dry-run # what is in each tarball
+```
+
+## What the guards are for
+
+- **`scripts/check-exports.mjs`** - every `exports` and `bin` target exists in
+ the built tree and is covered by `files[]`, and every package has a README
+ and a LICENSE. tsc cannot catch a subpath nothing in the repository imports;
+ `@textui/core` shipped a broken `./hooks` for exactly that reason. Runs in
+ CI on every pull request.
+- **`scripts/check-version.mjs`** - the tag and the manifests agree. Runs in
+ the release workflow, on tag only.
diff --git a/docs/documents/adapters.md b/docs/documents/adapters.md
index 86c6287..2648d1f 100644
--- a/docs/documents/adapters.md
+++ b/docs/documents/adapters.md
@@ -30,7 +30,7 @@ registration.dispose(); // removes exactly what it added
`register(app)` escape hatch for anything the fields cannot express. They are
registered in that order, so a viewer always has its kind to match against.
-The JSON adapter shipped in `@textui/core/adapters` is the worked example:
+The JSON adapter shipped in `@textui/documents` is the worked example:
```ts
diff --git a/package.json b/package.json
index ca0457a..857d7aa 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,8 @@
"test": "pnpm -r test",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
+ "check:exports": "node scripts/check-exports.mjs",
+ "check:version": "node scripts/check-version.mjs",
"clean": "rm -rf packages/*/dist packages/*/*.tsbuildinfo playground/.dev examples/*/.dev packages/textide/.dev",
"docs:props": "node scripts/docs/extract-props.mjs && node scripts/docs/gen-components.mjs",
"docs:check": "node scripts/docs/extract-props.mjs && node scripts/docs/gen-components.mjs --check && node scripts/docs/check-links.mjs && node scripts/check-docs.mjs",
diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/cli/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 70f96ef..af6a7b7 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -32,7 +32,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
diff --git a/packages/core/LICENSE b/packages/core/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/core/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/core/README.md b/packages/core/README.md
index 0e851e4..51a1931 100644
--- a/packages/core/README.md
+++ b/packages/core/README.md
@@ -30,7 +30,7 @@ are the strings themselves, so `` and `` produce the identical node.
The four primitives - `box`, `text`, `canvas`, `spacer` - are here because the
layout engine and the painter are the things that reason about them. The
-eighty-seven components built out of them are [`@textui/widgets`](../widgets),
+eighty-seven components built out of them are [`@textui/widgets`](https://github.com/softov/textui/tree/main/packages/widgets),
a separate package that depends on this one.
That split is not tidiness. An imported component travels on its own node, so a
diff --git a/packages/core/package.json b/packages/core/package.json
index d0c0619..b0ff624 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -33,7 +33,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
@@ -55,10 +56,6 @@
"./themes": {
"types": "./dist/themes/index.d.ts",
"import": "./dist/themes/index.js"
- },
- "./hooks": {
- "types": "./dist/hooks/index.d.ts",
- "import": "./dist/hooks/index.js"
}
},
"scripts": {
diff --git a/packages/documents/LICENSE b/packages/documents/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/documents/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/documents/package.json b/packages/documents/package.json
index 574b442..c81f389 100644
--- a/packages/documents/package.json
+++ b/packages/documents/package.json
@@ -1,6 +1,7 @@
{
"name": "@textui/documents",
"version": "0.1.0",
+ "private": true,
"description": "Document buffers, resource viewers and content adapters for TextUI",
"keywords": [
"terminal",
@@ -30,7 +31,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
diff --git a/packages/facade/LICENSE b/packages/facade/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/facade/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/facade/README.md b/packages/facade/README.md
index e95b4cf..7807083 100644
--- a/packages/facade/README.md
+++ b/packages/facade/README.md
@@ -28,8 +28,8 @@ console.log('App exited');
## What this package is
-The runtime ([`@textui/core`](../core)), a terminal to put it on
-([`@textui/terminal`](../terminal)), and `render`. Every other name here is
+The runtime ([`@textui/core`](https://github.com/softov/textui/tree/main/packages/core)), a terminal to put it on
+([`@textui/terminal`](https://github.com/softov/textui/tree/main/packages/terminal)), and `render`. Every other name here is
re-exported from those two - importing from them directly is the same thing
with a longer name.
diff --git a/packages/facade/package.json b/packages/facade/package.json
index 0d2ae92..da86210 100644
--- a/packages/facade/package.json
+++ b/packages/facade/package.json
@@ -32,7 +32,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
diff --git a/packages/terminal/LICENSE b/packages/terminal/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/terminal/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/terminal/README.md b/packages/terminal/README.md
index 1c6f176..6b3d0cb 100644
--- a/packages/terminal/README.md
+++ b/packages/terminal/README.md
@@ -1,6 +1,6 @@
# @textui/terminal
-What [`@textui/core`](../core) needs to reach an actual terminal: adapters,
+What [`@textui/core`](https://github.com/softov/textui/tree/main/packages/core) needs to reach an actual terminal: adapters,
capability detection, ANSI writing and input decoding.
```bash
diff --git a/packages/terminal/package.json b/packages/terminal/package.json
index f6131f3..7280685 100644
--- a/packages/terminal/package.json
+++ b/packages/terminal/package.json
@@ -31,7 +31,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
diff --git a/packages/testing/LICENSE b/packages/testing/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/testing/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/testing/package.json b/packages/testing/package.json
index bcf2ba6..4b916a7 100644
--- a/packages/testing/package.json
+++ b/packages/testing/package.json
@@ -30,7 +30,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
diff --git a/packages/textide-git/LICENSE b/packages/textide-git/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/textide-git/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/textide-git/README.md b/packages/textide-git/README.md
index 0658ea1..b6b2731 100644
--- a/packages/textide-git/README.md
+++ b/packages/textide-git/README.md
@@ -1,6 +1,6 @@
# @textui/textide-git
-Git for [textide](../textide): the branch you are on, what has changed, diffs
+Git for [textide](https://github.com/softov/textui/tree/main/packages/textide): the branch you are on, what has changed, diffs
you can open as tabs, staging, commits and branch switching.
It is a **loadable extension**, not part of the editor. Nothing in textide
diff --git a/packages/textide-git/package.json b/packages/textide-git/package.json
index 98e2d29..06758f9 100644
--- a/packages/textide-git/package.json
+++ b/packages/textide-git/package.json
@@ -1,6 +1,7 @@
{
"name": "@textui/textide-git",
"version": "0.1.0",
+ "private": true,
"description": "Git for textide: status, diff, stage, commit and branches, as a loadable extension",
"keywords": [
"terminal",
@@ -30,7 +31,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
diff --git a/packages/textide/LICENSE b/packages/textide/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/textide/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/textide/README.md b/packages/textide/README.md
index 5e301fd..08c309e 100644
--- a/packages/textide/README.md
+++ b/packages/textide/README.md
@@ -26,7 +26,7 @@ pnpm textide --colors 4 # sixteen colours
pnpm textide --colors 0 # none
```
-Every glyph has three tiers - the theme's in [`glyphs.ts`](../core/src/themes/glyphs.ts), textide's own in [`icons.ts`](src/icons.ts) - and a test asserts the ASCII tier is actually ASCII, because a fallback holding one stray `⌸` fails on exactly the terminal it exists for.
+Every glyph has three tiers - the theme's in [`glyphs.ts`](https://github.com/softov/textui/tree/main/packages/core/src/themes/glyphs.ts), textide's own in [`icons.ts`](https://github.com/softov/textui/tree/main/packages/textide/src/icons.ts) - and a test asserts the ASCII tier is actually ASCII, because a fallback holding one stray `⌸` fails on exactly the terminal it exists for.
**`--log-file` and `--log-unix` send a running commentary somewhere else.** What has focus, what the chrome did, every command that ran. `examples/logtail.mjs` listens on a socket.
@@ -161,7 +161,7 @@ a project's extensions belong, rather than beside the editor. One that fails to
load is reported and skipped; an editor that will not open because a plugin is
missing has made the plugin mandatory.
-[`@textui/textide-git`](../textide-git) is the one that exists, and it is the
+[`@textui/textide-git`](https://github.com/softov/textui/tree/main/packages/textide-git) is the one that exists, and it is the
proof the boundary is in the right place: git arrives as an adapter, some
commands, a component and a mount, and unloading it leaves nothing behind.
diff --git a/packages/textide/package.json b/packages/textide/package.json
index db5a339..e93e4ad 100644
--- a/packages/textide/package.json
+++ b/packages/textide/package.json
@@ -1,6 +1,7 @@
{
"name": "@textui/textide",
"version": "0.1.0",
+ "private": true,
"description": "An IDE that runs in a terminal, built on TextUI",
"keywords": [
"terminal",
@@ -32,7 +33,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
diff --git a/packages/widgets/LICENSE b/packages/widgets/LICENSE
new file mode 100644
index 0000000..d29b883
--- /dev/null
+++ b/packages/widgets/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2026 Softov
+
+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.
diff --git a/packages/widgets/README.md b/packages/widgets/README.md
index e8e855b..70b6e41 100644
--- a/packages/widgets/README.md
+++ b/packages/widgets/README.md
@@ -1,7 +1,7 @@
# @textui/widgets
The component catalog: eighty-seven components built out of the four primitives
-in [`@textui/core`](../core).
+in [`@textui/core`](https://github.com/softov/textui/tree/main/packages/core).
```bash
npm install @textui/widgets
@@ -66,4 +66,4 @@ No dependencies beyond `@textui/core`, and no `node:` imports. Node 22+ and Bun.
Every component has its own page, with the prop table generated from the source
-and a working example - see [`docs/components/`](../../docs/components).
+and a working example - see [`docs/components/`](https://softov.github.io/textui/components/).
diff --git a/packages/widgets/package.json b/packages/widgets/package.json
index f097dcf..3121bbb 100644
--- a/packages/widgets/package.json
+++ b/packages/widgets/package.json
@@ -31,7 +31,8 @@
"files": [
"dist",
"src",
- "README.md"
+ "README.md",
+ "LICENSE"
],
"exports": {
".": {
diff --git a/scripts/check-exports.mjs b/scripts/check-exports.mjs
new file mode 100644
index 0000000..785f1b8
--- /dev/null
+++ b/scripts/check-exports.mjs
@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+/**
+ * Every `exports` and `bin` target in a publishable package has to exist in
+ * the built tree.
+ *
+ * tsc cannot catch this. A subpath nothing in the repo imports is a promise
+ * to consumers and to nobody else, so it compiles clean and fails on the
+ * first `import ... from '@textui/core/hooks'` after publish. Run it against
+ * a built tree - it reads dist, it does not build it.
+ */
+import { readdirSync, readFileSync, existsSync } from 'node:fs';
+import { join, dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const pkgDir = join(root, 'packages');
+
+let checked = 0;
+let skipped = 0;
+const problems = [];
+
+for (const name of readdirSync(pkgDir).sort()) {
+ const dir = join(pkgDir, name);
+ const manifest = join(dir, 'package.json');
+ if (!existsSync(manifest)) continue;
+
+ const pkg = JSON.parse(readFileSync(manifest, 'utf8'));
+ if (pkg.private) {
+ skipped++;
+ continue;
+ }
+ checked++;
+
+ // `exports` nests conditions arbitrarily deep; `bin` is a string or a map.
+ // Either way the leaves are the paths, so walk to the strings.
+ const targets = [];
+ const walk = (node, trail) => {
+ if (typeof node === 'string') targets.push([node, trail]);
+ else if (node && typeof node === 'object')
+ for (const [k, v] of Object.entries(node)) walk(v, trail ? `${trail} ${k}` : k);
+ };
+ walk(pkg.exports, '');
+ walk(pkg.bin, 'bin');
+
+ for (const [target, trail] of targets) {
+ if (!target.startsWith('.')) continue; // a bare specifier is a redirect, not a file
+ if (!existsSync(join(dir, target)))
+ problems.push(`${pkg.name}: ${trail || '.'} -> ${target} does not exist`);
+ }
+
+ // `files` decides the tarball. A target outside it resolves here and 404s
+ // for the consumer, which is the same bug one step later.
+ const files = pkg.files ?? [];
+ for (const [target] of targets) {
+ if (!target.startsWith('./')) continue;
+ const top = target.slice(2).split('/')[0];
+ if (!files.includes(top))
+ problems.push(`${pkg.name}: ${target} is not covered by files[] (${files.join(', ')})`);
+ }
+
+ for (const required of ['README.md', 'LICENSE'])
+ if (!existsSync(join(dir, required)))
+ problems.push(`${pkg.name}: ${required} is missing`);
+}
+
+for (const p of problems) console.error(` ${p}`);
+console.log(
+ `${checked} publishable packages checked, ${skipped} private, ${problems.length} problems`,
+);
+process.exit(problems.length ? 1 : 0);
diff --git a/scripts/check-version.mjs b/scripts/check-version.mjs
new file mode 100644
index 0000000..64fdfda
--- /dev/null
+++ b/scripts/check-version.mjs
@@ -0,0 +1,35 @@
+#!/usr/bin/env node
+/**
+ * Every publishable package agrees with the tag.
+ *
+ * The packages release as a set - `workspace:^` between them means a mixed
+ * set is a set that resolves to versions nobody tested together. Called with
+ * the tag minus its leading `v`.
+ */
+import { readdirSync, readFileSync, existsSync } from 'node:fs';
+import { join, dirname, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const expected = process.argv[2];
+if (!expected) {
+ console.error('usage: check-version.mjs ');
+ process.exit(2);
+}
+
+const pkgDir = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'packages');
+const problems = [];
+let checked = 0;
+
+for (const name of readdirSync(pkgDir).sort()) {
+ const manifest = join(pkgDir, name, 'package.json');
+ if (!existsSync(manifest)) continue;
+ const pkg = JSON.parse(readFileSync(manifest, 'utf8'));
+ if (pkg.private) continue;
+ checked++;
+ if (pkg.version !== expected)
+ problems.push(`${pkg.name} is ${pkg.version}, tag says ${expected}`);
+}
+
+for (const p of problems) console.error(` ${p}`);
+console.log(`${checked} publishable packages at ${expected}, ${problems.length} problems`);
+process.exit(problems.length ? 1 : 0);