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. Terminal UI Mono

-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);