From a49cd3d3afc11bc7ac85c6d96f740fac1f09ddfa Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 13 Oct 2026 00:04:29 +0000 Subject: [PATCH] Publish the client to npm and serve a curl installer at /install.sh @profullstack/tsbb, @profullstack/tsbb-tui and @profullstack/tsbb-mcp 0.12.1 are now on npm. They were names in package.json only: the apps import TypeScript and the workspace-only @tsbb/* packages, and Node will not strip types under node_modules, so a published copy of the source cannot run. - scripts/build-npm.mjs (pnpm build:npm) bundles each bin with esbuild into npm//, inlining @tsbb/* and keeping only real npm deps (synconfig, hqtui). Installs standalone with npm i -g on Node 22.6+. - /install.sh on every board: installs all three with bun or npm, ~/.local instead of sudo, then a PATH check. llms.txt names it. - tsbb-tui's bin imported ../src/config.ts, which moved to @tsbb/client; it crashed on start. Fixed, and --version for tsbb-tui and tsbb-mcp. - The board commands (serve, init, migrate, admin...) print how to get a checkout when run from the npm install instead of a module error. - README, docs/CLI.md and docs/MCP.md lead with the curl line. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 1 + README.md | 8 +- apps/cli/bin/tsbb.mjs | 19 ++ apps/mcp/bin/tsbb-mcp.mjs | 5 + apps/mcp/src/main.ts | 4 +- apps/server/public/install.sh | 59 ++++++ apps/server/src/routes/discovery.ts | 12 ++ apps/tui/bin/tsbb-tui.mjs | 9 +- docs/CLI.md | 17 +- docs/MCP.md | 23 +-- package.json | 2 + pnpm-lock.yaml | 271 ++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + scripts/build-npm.mjs | 139 ++++++++++++++ test/discovery.test.ts | 10 + 15 files changed, 560 insertions(+), 21 deletions(-) create mode 100644 apps/server/public/install.sh create mode 100644 scripts/build-npm.mjs diff --git a/.gitignore b/.gitignore index 78b6c69..c72da9a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ data/ uploads/ .DS_Store coverage/ +npm/ diff --git a/README.md b/README.md index 9394c4c..42c1383 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ pnpm exec tsbb init pnpm start ``` +Just want to use a board from the terminal? `curl -fsSL https://tsbb.dev/install.sh | sh` +installs the client (`tsbb`, `tsbb-tui`, `tsbb-mcp`) from npm. + Open , put in your email address, and click the link. **The first account to sign in becomes the administrator.** @@ -216,9 +219,12 @@ Turn it off in the admin panel and the `frame-src` permission goes with it. ## The terminal client ``` -pnpm exec tsbb-tui https://forum.example.com +curl -fsSL https://tsbb.dev/install.sh | sh # tsbb, tsbb-tui and tsbb-mcp +tsbb-tui https://forum.example.com ``` +From a checkout, `pnpm exec tsbb-tui https://forum.example.com` is the same client. + Press `L` to sign in: the client shows a short code, you approve it in a browser, and the board hands the terminal a token — once. It is a *client* of a centralised install, not a second board: it holds no database and can only see diff --git a/apps/cli/bin/tsbb.mjs b/apps/cli/bin/tsbb.mjs index 9198a75..b21909c 100755 --- a/apps/cli/bin/tsbb.mjs +++ b/apps/cli/bin/tsbb.mjs @@ -87,6 +87,25 @@ if (existsSync(envPath)) { const baseUrl = process.env.TSBB_BASE_URL ?? 'http://localhost:3000'; +/* + * Running a board needs the whole repository (server, migrations, plugins). The + * npm package (curl -fsSL https://tsbb.dev/install.sh | sh) is only the client + * half, so say how to get a board instead of failing on a missing module. + */ +const BOARD_COMMANDS = new Set([ + 'init', 'serve', 'start', 'worker', 'migrate', 'status', 'update', 'upgrade', + 'admin', 'unadmin', 'invite', 'plugin', 'plugins', +]); +if (BOARD_COMMANDS.has(command) && !existsSync(join(REPO_ROOT, 'apps/server/src/index.ts'))) { + console.error(`tsbb ${command} runs a board, which needs a checkout of the repository: + + git clone https://github.com/profullstack/tsbb && cd tsbb + pnpm install && node apps/cli/bin/tsbb.mjs init + +This install is the client: tsbb login, forums, latest, read, post, mcp (see tsbb --help).`); + process.exit(1); +} + /** * Split the flags out of the arguments. * diff --git a/apps/mcp/bin/tsbb-mcp.mjs b/apps/mcp/bin/tsbb-mcp.mjs index b39b656..bc06cba 100644 --- a/apps/mcp/bin/tsbb-mcp.mjs +++ b/apps/mcp/bin/tsbb-mcp.mjs @@ -12,5 +12,10 @@ try { /* the version is a label in the handshake, not something to fail over */ } +if (process.argv[2] === '--version' || process.argv[2] === '-v') { + console.log(version); + process.exit(0); +} + const { main } = await import('../src/main.ts'); process.exitCode = await main(process.argv.slice(2), version); diff --git a/apps/mcp/src/main.ts b/apps/mcp/src/main.ts index 5b894a8..aadddd4 100644 --- a/apps/mcp/src/main.ts +++ b/apps/mcp/src/main.ts @@ -37,8 +37,8 @@ Claude Desktop MCP config: { "mcpServers": { "myboard": { - "command": "node", - "args": ["/path/to/tsbb/apps/mcp/bin/tsbb-mcp.mjs", "--server", "https://board.example"], + "command": "tsbb-mcp", + "args": ["--server", "https://board.example"], "env": { "TSBB_TOKEN": "…" } } } diff --git a/apps/server/public/install.sh b/apps/server/public/install.sh new file mode 100644 index 0000000..157377f --- /dev/null +++ b/apps/server/public/install.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# Install the tsbb bulletin board client: tsbb (CLI), tsbb-tui (terminal client) +# and tsbb-mcp (MCP server). npm: @profullstack/tsbb, -tui, -mcp. +# +# curl -fsSL https://tsbb.dev/install.sh | sh +# +# Works against any tsbb board. Needs Node >= 22.6 (the commands run on Node). +# Installs with bun if present, otherwise npm. When the global npm prefix is not writable it installs into +# ~/.local instead of asking for sudo. Re-run it to upgrade. +set -eu + +PKGS="@profullstack/tsbb@latest @profullstack/tsbb-tui@latest @profullstack/tsbb-mcp@latest" +BIN="tsbb" + +say() { printf '%s\n' "$*"; } +die() { printf 'tsbb install: %s\n' "$*" >&2; exit 1; } +have() { command -v "$1" >/dev/null 2>&1; } + +node_ok() { + have node || return 1 + node -e 'const [a,b]=process.versions.node.split(".").map(Number);process.exit(a>22||(a===22&&b>=6)?0:1)' 2>/dev/null +} + +say "Installing tsbb, tsbb-tui and tsbb-mcp..." + +node_ok || die "Node >= 22.6 required (found $(node -v 2>/dev/null || echo none)). Install it from https://nodejs.org" + +if have bun; then + bun add -g $PKGS + BIN_DIR="${BUN_INSTALL:-$HOME/.bun}/bin" +elif have npm; then + prefix=$(npm config get prefix) + modules="$prefix/lib/node_modules" + [ -d "$modules" ] || modules="$prefix/lib" + if [ -w "$modules" ] && [ -w "$prefix/bin" ]; then + npm install -g $PKGS + BIN_DIR="$prefix/bin" + else + BIN_DIR="$HOME/.local/bin" + say "npm prefix $prefix is not writable; installing into $HOME/.local" + npm install -g --prefix "$HOME/.local" $PKGS + fi +else + die "needs npm (comes with Node: https://nodejs.org) or bun (https://bun.sh)" +fi + +say "" +if have "$BIN"; then + say "tsbb $("$BIN" --version 2>/dev/null || echo '') installed (with tsbb-tui and tsbb-mcp)." +else + say "$BIN installed to $BIN_DIR, which is not on your PATH. Add it:" + say " export PATH=\"$BIN_DIR:\$PATH\"" +fi +say "" +say "Next:" +say " tsbb login tsbb.dev # approve a code in your browser" +say " tsbb latest # recently active topics" +say " tsbb-tui tsbb.dev # read and post in the terminal" +say " tsbb --help # everything else, including tsbb mcp" diff --git a/apps/server/src/routes/discovery.ts b/apps/server/src/routes/discovery.ts index 5e5add7..b91d7ba 100644 --- a/apps/server/src/routes/discovery.ts +++ b/apps/server/src/routes/discovery.ts @@ -240,6 +240,17 @@ export function discoveryRoutes(services: Services) { ); }); + // --- install.sh ----------------------------------------------------------- + // The client installer: curl -fsSL https:///install.sh | sh. It + // installs tsbb, tsbb-tui and tsbb-mcp from npm, which work against any board. + + app.get('/install.sh', (c) => + c.body(readFileSync(join(PUBLIC_DIR, 'install.sh'), 'utf8'), 200, { + 'content-type': 'text/x-shellscript; charset=utf-8', + 'cache-control': 'public, max-age=600', + }), + ); + // --- llms.txt ------------------------------------------------------------- app.get('/llms.txt', async (c) => { @@ -299,6 +310,7 @@ export function discoveryRoutes(services: Services) { link('OpenAPI description', '/api/v1/openapi.json', 'The REST API, machine-readable.'), link('MCP endpoint', '/api/mcp', 'Streamable HTTP MCP server; a bearer token makes it act as a member.'), link('Agent skill', '/skill.md', 'What an agent can do here and how to authenticate.'), + `- Terminal client, CLI and stdio MCP server: \`curl -fsSL ${absolute('/install.sh')} | sh\` (npm: @profullstack/tsbb, @profullstack/tsbb-tui, @profullstack/tsbb-mcp)`, '', '## Optional', '', diff --git a/apps/tui/bin/tsbb-tui.mjs b/apps/tui/bin/tsbb-tui.mjs index 65f9476..760fa44 100755 --- a/apps/tui/bin/tsbb-tui.mjs +++ b/apps/tui/bin/tsbb-tui.mjs @@ -1,9 +1,16 @@ #!/usr/bin/env node -import { currentBoard, loadConfig, normaliseServer, rememberBoard } from '../src/config.ts'; +import { currentBoard, loadConfig, normaliseServer, rememberBoard } from '@tsbb/client'; import { run } from '../src/app.ts'; const argv = process.argv.slice(2); +if (argv.includes('--version') || argv.includes('-v')) { + const { readFileSync } = await import('node:fs'); + const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); + console.log(pkg.version); + process.exit(0); +} + if (argv.includes('--help') || argv.includes('-h')) { console.log(`tsbb-tui — terminal client for a tsbb bulletin board diff --git a/docs/CLI.md b/docs/CLI.md index a07243c..407b488 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -14,16 +14,21 @@ anybody else's. The packages are not on npm yet, so both jobs are done from a checkout: ``` -git clone https://github.com/profullstack/tsbb -cd tsbb && pnpm install -pnpm exec tsbb --help +curl -fsSL https://tsbb.dev/install.sh | sh ``` -Put it on your path if you are going to use it from elsewhere, which is what the -rest of this page assumes: +That installs `tsbb`, `tsbb-tui` and `tsbb-mcp` from npm (`@profullstack/tsbb`, +`-tui`, `-mcp`; Node 22.6+), with bun if you have it, and into `~/.local` rather +than asking for sudo. Re-run it to upgrade. + +The installed `tsbb` is the client half: everything under "Using a board". The +commands that *run* a board (`init`, `serve`, `migrate`, `admin`, …) need a +checkout: ``` -ln -s "$PWD/apps/cli/bin/tsbb.mjs" ~/.local/bin/tsbb +git clone https://github.com/profullstack/tsbb +cd tsbb && pnpm install +pnpm exec tsbb --help ``` ## Signing in diff --git a/docs/MCP.md b/docs/MCP.md index 1497a9b..4bdd45e 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -18,18 +18,20 @@ missing from `list_forums` is a forum that member cannot read. ## Over stdio -`tsbb-mcp` is not on npm yet, so point your client at the binary in a checkout: +Install it (with `tsbb` and `tsbb-tui`; Node 22.6+): + +``` +curl -fsSL https://tsbb.dev/install.sh | sh +``` + +Then point your client at it: ```json { "mcpServers": { "myboard": { - "command": "node", - "args": [ - "/path/to/tsbb/apps/mcp/bin/tsbb-mcp.mjs", - "--server", - "https://forum.example.com" - ], + "command": "tsbb-mcp", + "args": ["--server", "https://forum.example.com"], "env": { "TSBB_TOKEN": "tsbb_…" } } } @@ -39,12 +41,11 @@ missing from `list_forums` is a forum that member cannot read. Or, in Claude Code: ``` -claude mcp add myboard -- node /path/to/tsbb/apps/mcp/bin/tsbb-mcp.mjs --server https://forum.example.com +claude mcp add myboard -- tsbb-mcp --server https://forum.example.com ``` -Nothing needs building — Node runs the TypeScript directly — but the checkout -does need `pnpm install` once, because the server shares the board's client -package. +Without installing anything: `"command": "npx", "args": ["-y", "@profullstack/tsbb-mcp", "--server", "…"]`. +From a checkout, `node /path/to/tsbb/apps/mcp/bin/tsbb-mcp.mjs` is the same program. Where the token comes from, in order: `--token`, `TSBB_TOKEN`, then whichever board you last ran `tsbb login` against. If you have already signed in with the diff --git a/package.json b/package.json index 6da9402..0e3c6be 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,14 @@ "tui": "node apps/tui/src/index.ts", "test": "node --test \"test/*.test.ts\"", "typecheck": "tsc -p tsconfig.json --noEmit", + "build:npm": "node scripts/build-npm.mjs", "format": "prettier --write ." }, "devDependencies": { "@profullstack/bridges": "^0.2.0", "@profullstack/hqtui": "^0.5.0", "@types/node": "^24.3.0", + "esbuild": "0.25.10", "hono": "^4.6.14", "prettier": "^3.3.3", "typescript": "^5.9.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a927ff3..aef005f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@types/node': specifier: ^24.3.0 version: 24.13.3 + esbuild: + specifier: 0.25.10 + version: 0.25.10 hono: specifier: ^4.6.14 version: 4.13.5 @@ -231,6 +234,162 @@ packages: '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@hono/node-server@1.19.17': resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==} engines: {node: '>=18.14.1'} @@ -500,6 +659,11 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + fetch-blob@3.2.0: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} @@ -643,6 +807,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + '@hono/node-server@1.19.17(hono@4.13.5)': dependencies: hono: 4.13.5 @@ -847,6 +1089,35 @@ snapshots: detect-libc@2.1.2: {} + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + fetch-blob@3.2.0: dependencies: node-domexception: 1.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 301fd3f..a3cb194 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,8 @@ packages: - 'packages/*' - 'apps/*' - 'plugins/*' +allowBuilds: + esbuild: false minimumReleaseAgeExclude: - '@profullstack/hqtui@0.1.9 || 0.5.0' - '@profullstack/synconfig@0.1.2' diff --git a/scripts/build-npm.mjs b/scripts/build-npm.mjs new file mode 100644 index 0000000..b781a3d --- /dev/null +++ b/scripts/build-npm.mjs @@ -0,0 +1,139 @@ +#!/usr/bin/env node +/** + * Build the three npm packages: @profullstack/tsbb (CLI), -tui and -mcp. + * + * node scripts/build-npm.mjs # writes npm// + * (cd npm/tsbb && npm publish) # and the same for tsbb-tui, tsbb-mcp + * + * The apps import TypeScript and the workspace-only @tsbb/* packages, and Node + * refuses to strip types inside node_modules, so a published copy of the source + * cannot run. Each bin is bundled into one ESM file instead, with the @tsbb/* + * code inlined and only real npm dependencies left external. The result installs + * standalone with `npm i -g`, which is what https://tsbb.dev/install.sh does. + * + * `tsbb serve`, `init` and the other board commands need the repository and are + * not bundled: the published CLI tells you how to get a checkout. + */ +import { build } from 'esbuild'; +import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const OUT = join(ROOT, 'npm'); +const NODE_FLOOR = '>=22.6'; + +const read = (path) => JSON.parse(readFileSync(join(ROOT, path), 'utf8')); + +/** Versions of the real npm packages, taken from whichever workspace declares them. */ +const versions = {}; +for (const path of ['apps/tui/package.json', 'packages/client/package.json', 'packages/mcp/package.json']) { + for (const [name, range] of Object.entries(read(path).dependencies ?? {})) { + if (!range.startsWith('workspace:')) versions[name] = range; + } +} + +const packages = [ + { + dir: 'tsbb', + app: 'apps/cli', + bin: 'tsbb', + // Reached only from a checkout (the board commands), so left as runtime imports. + external: ['../src/commands.ts', '@tsbb/mail'], + keywords: ['bulletin-board', 'forum', 'bbs', 'cli', 'mcp'], + }, + { + dir: 'tsbb-tui', + app: 'apps/tui', + bin: 'tsbb-tui', + external: [], + keywords: ['bulletin-board', 'forum', 'bbs', 'tui', 'terminal'], + }, + { + dir: 'tsbb-mcp', + app: 'apps/mcp', + bin: 'tsbb-mcp', + external: [], + keywords: ['bulletin-board', 'forum', 'mcp', 'model-context-protocol'], + }, +]; + +rmSync(OUT, { recursive: true, force: true }); + +for (const pkg of packages) { + const source = read(`${pkg.app}/package.json`); + const dest = join(OUT, pkg.dir); + mkdirSync(join(dest, 'bin'), { recursive: true }); + + const result = await build({ + entryPoints: [join(ROOT, pkg.app, 'bin', `${pkg.bin}.mjs`)], + outfile: join(dest, 'bin', `${pkg.bin}.mjs`), + bundle: true, + platform: 'node', + format: 'esm', + target: 'node22', + packages: 'bundle', + external: [...pkg.external, ...Object.keys(versions)], + metafile: true, + logLevel: 'warning', + // Bundled CommonJS (if any) needs a require() in an ES module. + banner: { js: "import { createRequire as __tsbbRequire } from 'node:module';\nconst require = __tsbbRequire(import.meta.url);" }, + }); + + // Only the externals the bundle really imports become dependencies. + const used = new Set(); + for (const output of Object.values(result.metafile.outputs)) { + for (const imp of output.imports) if (imp.external) used.add(imp.path); + } + const dependencies = Object.fromEntries( + Object.entries(versions).filter(([name]) => [...used].some((path) => path === name || path.startsWith(`${name}/`))), + ); + + const manifest = { + name: source.name, + version: source.version, + description: source.description, + type: 'module', + license: source.license, + author: source.author, + homepage: 'https://tsbb.dev', + repository: { type: 'git', url: 'git+https://github.com/profullstack/tsbb.git', directory: pkg.app }, + bugs: { url: 'https://github.com/profullstack/tsbb/issues' }, + keywords: pkg.keywords, + bin: { [pkg.bin]: `bin/${pkg.bin}.mjs` }, + files: ['bin', 'README.md', 'LICENSE'], + engines: { node: NODE_FLOOR }, + dependencies, + publishConfig: { access: 'public' }, + }; + writeFileSync(join(dest, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`); + copyFileSync(join(ROOT, 'LICENSE'), join(dest, 'LICENSE')); + writeFileSync(join(dest, 'README.md'), readme(manifest, pkg.bin)); + console.log(`npm/${pkg.dir}: ${manifest.name}@${manifest.version} deps=${Object.keys(dependencies).join(',') || 'none'}`); +} + +function readme(manifest, bin) { + const usage = { + tsbb: 'tsbb login tsbb.dev # approve a code in your browser\ntsbb latest # recently active topics\ntsbb read \ntsbb post "Title" "Body"\ntsbb mcp # serve the board to an AI assistant\ntsbb --help', + 'tsbb-tui': 'tsbb-tui tsbb.dev # read and post in the terminal\ntsbb-tui --help', + 'tsbb-mcp': 'tsbb-mcp --server tsbb.dev [--token tsbb_...] [--read-only]', + }[bin]; + return `# ${manifest.name} + +${manifest.description} + +\`\`\`sh +curl -fsSL https://tsbb.dev/install.sh | sh # installs tsbb, tsbb-tui and tsbb-mcp +npm i -g ${manifest.name} # or just this one (Node ${NODE_FLOOR.slice(2)}+) +\`\`\` + +\`\`\`sh +${usage} +\`\`\` + +Running a board of your own needs the repository: https://github.com/profullstack/tsbb + +Docs: https://tsbb.dev · MIT, ${manifest.author} +`; +} + diff --git a/test/discovery.test.ts b/test/discovery.test.ts index 61d3957..cdaa2f7 100644 --- a/test/discovery.test.ts +++ b/test/discovery.test.ts @@ -72,6 +72,16 @@ describe('discovery files and headers', () => { assert.ok(body.includes('User-agent: ClaudeBot')); }); + it('serves the client installer as a shell script, not a page', async () => { + const response = await get('/install.sh'); + assert.equal(response.status, 200); + assert.match(response.headers.get('content-type') ?? '', /^text\/x-shellscript/); + const body = await response.text(); + assert.ok(body.startsWith('#!/bin/sh')); + assert.ok(body.includes('@profullstack/tsbb@latest')); + assert.ok((await (await get('/llms.txt')).text()).includes('/install.sh | sh')); + }); + it('serves a sitemap index whose files list the public pages and topics', async () => { const index = await (await get('/sitemap.xml')).text(); assert.ok(index.includes('