From 203073ab2b7f27fe3b07545321b712bcac4e4ea0 Mon Sep 17 00:00:00 2001
From: Softov
Date: Mon, 24 Aug 2026 16:25:20 -0400
Subject: [PATCH 1/4] fix: core promised a ./hooks subpath that was never built
The export map pointed `./hooks` at `dist/hooks/`, which tsc has no reason to
emit - the hooks live in `runtime/hooks.ts` and are already re-exported from
the root. Nothing in the repository imported the subpath, so it compiled clean
and would have been ERR_MODULE_NOT_FOUND for the first consumer who tried it.
A type checker cannot catch that, so `check-exports.mjs` does: every exports
and bin target has to exist in the built tree and be covered by files[], and
every publishable package needs a README and a LICENSE. It runs in CI on every
pull request, beside `check-version.mjs`, which the release workflow uses to
hold the tag and the manifests to the same number.
---
.github/workflows/ci.yml | 4 +++
package.json | 2 ++
packages/core/package.json | 7 ++--
scripts/check-exports.mjs | 70 ++++++++++++++++++++++++++++++++++++++
scripts/check-version.mjs | 35 +++++++++++++++++++
5 files changed, 113 insertions(+), 5 deletions(-)
create mode 100644 scripts/check-exports.mjs
create mode 100644 scripts/check-version.mjs
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/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/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/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);
From 09cee1679732551308bf42b0d7cfb9423e273a08 Mon Sep 17 00:00:00 2001
From: Softov
Date: Mon, 24 Aug 2026 16:25:31 -0400
Subject: [PATCH 2/4] chore: every package ships the licence it claims, and
only six ship at all
`"license": "MIT"` in a manifest is a label, not the text, and npm only picks up
a LICENSE sitting in the package's own directory - so nine packages declared a
licence they did not carry.
documents, textide and textide-git are marked private. They build in CI and
they are in the repository, but their surface has not settled and an IDE is not
something to tie to the runtime's release cadence. Being private is what holds
them back, and deliberately the only thing that does: a filter you have to
remember at publish time is a filter that gets forgotten.
---
packages/cli/LICENSE | 21 +++++++++++++++++++++
packages/cli/package.json | 3 ++-
packages/core/LICENSE | 21 +++++++++++++++++++++
packages/documents/LICENSE | 21 +++++++++++++++++++++
packages/documents/package.json | 4 +++-
packages/facade/LICENSE | 21 +++++++++++++++++++++
packages/facade/package.json | 3 ++-
packages/terminal/LICENSE | 21 +++++++++++++++++++++
packages/terminal/package.json | 3 ++-
packages/testing/LICENSE | 21 +++++++++++++++++++++
packages/testing/package.json | 3 ++-
packages/textide-git/LICENSE | 21 +++++++++++++++++++++
packages/textide-git/package.json | 4 +++-
packages/textide/LICENSE | 21 +++++++++++++++++++++
packages/textide/package.json | 4 +++-
packages/widgets/LICENSE | 21 +++++++++++++++++++++
packages/widgets/package.json | 3 ++-
17 files changed, 208 insertions(+), 8 deletions(-)
create mode 100644 packages/cli/LICENSE
create mode 100644 packages/core/LICENSE
create mode 100644 packages/documents/LICENSE
create mode 100644 packages/facade/LICENSE
create mode 100644 packages/terminal/LICENSE
create mode 100644 packages/testing/LICENSE
create mode 100644 packages/textide-git/LICENSE
create mode 100644 packages/textide/LICENSE
create mode 100644 packages/widgets/LICENSE
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/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/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/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/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/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/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": {
".": {
From 61a84d1784c385882ace3485d668e310682ea5aa Mon Sep 17 00:00:00 2001
From: Softov
Date: Mon, 24 Aug 2026 16:25:31 -0400
Subject: [PATCH 3/4] chore: a release is a tag, and the gate stands in front
of it
`pnpm publish -r` already walks the dependency order and rewrites workspace:^,
so the workflow's job is to refuse rather than to sequence: the same build,
typecheck, lint and test CI runs, then the exports guard, then a check that the
tag and the manifests agree. It packs every tarball before it publishes any,
and signs them with provenance.
workflow_dispatch runs all of that and publishes nothing, so the first real tag
is not also the first rehearsal.
---
.github/workflows/release.yml | 60 +++++++++++++++++++++++++
CHANGELOG.md | 38 ++++++++++++++++
RELEASING.md | 82 +++++++++++++++++++++++++++++++++++
3 files changed, 180 insertions(+)
create mode 100644 .github/workflows/release.yml
create mode 100644 CHANGELOG.md
create mode 100644 RELEASING.md
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/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.
From e48e288b19d7523fa9df263044ed229e3d01ed18 Mon Sep 17 00:00:00 2001
From: Softov
Date: Mon, 24 Aug 2026 16:25:40 -0400
Subject: [PATCH 4/4] docs: the README is for using TextUI, DEVELOPER.md for
working on it
Someone arriving from npm wants the install line, the one idea, the packages
and a picture. Building the workspace, running the playgrounds, serving the
Jekyll site and cutting a release are all real, and none of them are that -
they move to DEVELOPER.md, along with the erasable-syntax argument, whose count
had drifted to fourteen across twelve files and whose tsconfig flag is not
actually set. The README keeps the claim it can make: no dependencies.
Two things that were simply wrong: the documents guide put the JSON adapter in
`@textui/core/adapters`, which is a deliberately empty placeholder that says so
itself - it is in `@textui/documents`. And six package READMEs linked to
siblings as `../core`, which resolves in the repository and 404s on npmjs.com.
---
DEVELOPER.md | 86 ++++++++++++++++++++++++++++++++++
README.md | 73 +++++++++--------------------
docs/documents/adapters.md | 2 +-
packages/core/README.md | 2 +-
packages/facade/README.md | 4 +-
packages/terminal/README.md | 2 +-
packages/textide-git/README.md | 2 +-
packages/textide/README.md | 4 +-
packages/widgets/README.md | 4 +-
9 files changed, 119 insertions(+), 60 deletions(-)
create mode 100644 DEVELOPER.md
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/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/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/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/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/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/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/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/).