From c4cd5353bf26a177eb562a45049ffa57105ef34c Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Mon, 9 Mar 2026 21:13:39 -0700 Subject: [PATCH] shout it out --- .gitignore | 36 +++++++++ .npmrc | 1 + README.md | 15 ++++ SPEC.md | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++ bun.lock | 40 ++++++++++ index.ts | 1 + package.json | 38 ++++++++++ tsconfig.json | 43 +++++++++++ 8 files changed, 375 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 SPEC.md create mode 100644 bun.lock create mode 100644 index.ts create mode 100644 package.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41cdcb0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# dependencies (bun install) +node_modules +.sandlot/ +.dev/ + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..6c57d5c --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +registry=https://npm.nose.space diff --git a/README.md b/README.md new file mode 100644 index 0000000..1a21116 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# shout + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.10. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..cf7737b --- /dev/null +++ b/SPEC.md @@ -0,0 +1,201 @@ +# shout + +A transcript-based shell integration test runner. + +## Format + +A `.shout` file is a plain text transcript of a shell session. Lines starting +with `$ ` are commands. Everything after — until the next `$` or end of file +— is the expected output (stdout and stderr combined). + +``` +$ dev new "add auth" +created draft 1 "add auth" + +$ dev save +saved draft 1 (v1) +``` + +Blank lines within expected output are significant. Trailing newline on the +file is ignored. + +### Comments + +`#` after a command is a comment and is stripped before execution. Comments +in expected output are matched literally. + +``` +$ dev new "add auth" # create a draft from timeline HEAD +created draft 1 "add auth" +``` + +### Wildcards + +A `...` on its own line in expected output matches any number of lines +(including zero). + +``` +$ dev log +... +draft 1 "add auth" +``` + +A `...` inline matches any sequence of characters on that line. + +``` +$ dev status +draft 1 "add auth" (v...) +``` + +### Environment + +Each `.shout` file runs in a fresh temporary directory. The directory is +created before the first command and removed after the last (unless +`--keep` is passed). + +The following environment variables are set for every command: + +| Variable | Value | +|---|---| +| `HOME` | the temp directory | +| `PATH` | prepended with the directory containing the binary under test | +| `CUE_DIR` | the temp directory | + +All other environment variables are inherited from the host unless explicitly +cleared with `--clean-env`. + +### Setup blocks + +Commands before the first blank line + command sequence are run as setup and +their output is not asserted. + +Alternatively, a `# ---` line separates setup from the test body explicitly: + +``` +$ export TOKEN=abc +$ cd myproject +# --- +$ dev status +on timeline @ change 0 +``` + +### Exit codes + +By default, a non-zero exit code fails the test regardless of output. To +assert a specific exit code, append `[N]` on the last line of expected output: + +``` +$ dev rm +error: draft 1 has children. use dev rm -f to cascade. +[1] +``` + +`[*]` accepts any non-zero exit code without asserting the value. + +--- + +## CLI + +``` +shout [options] [files|dirs...] +``` + +If no files are given, shout runs all `*.shout` files in the current directory +and subdirectories. Each command in each shout file is run sequentially +(unless `--parallel` is passed). + +### Options + +| Flag | Description | +|---|---| +| `--update` / `-u` | Rewrite expected output in-place with actual output | +| `--keep` / `-k` | Keep temp directories after run (printed to stderr) | +| `--clean-env` | Start with empty environment (only `PATH` and `CUE_DIR` set) | +| `--bin ` | Prepend `` to `PATH` instead of auto-detecting | +| `--timeout ` | Per-command timeout (default: `10s`) | +| `--verbose` / `-v` | Print each command as it runs | +| `--parallel` | Run files in parallel (implies all files run regardless of failures) | + +### Output + +Passing files print a single `.` per file. Failing files print a unified diff: + +``` +FAIL tests/auth.shout + + $ dev rm + - error: draft 1 has children. use dev rm -f to cascade. + + error: draft 1 has dependents. use dev rm -f to cascade. + [1] +``` + +Summary line at the end: + +``` +12 passed, 1 failed in 340ms +``` + +--- + +## Update mode + +`--update` rewrites the expected output sections of each `.shout` file with the +actual output from the run. Commands, comments, and whitespace are preserved. +Wildcard lines are left in place if the actual output matches them; they are +only replaced if the match fails. + +This makes it safe to run `shout --update` routinely after intentional output +changes — review the diff, commit if correct. + +--- + +## File layout + +``` +tests/ + auth.shout + drafts.shout + stack.shout +``` + +No special directory structure is required. `.shout` files can live anywhere. + +--- + +## Implementation notes + +- Bun + TypeScript +- Commands run via `Bun.spawn` with a shell (`/bin/sh -c`) +- Stdout and stderr merged (same as a terminal) +- Each command in a file shares a working directory but runs in a fresh + process — no persistent shell state between commands +- For persistent state (e.g. `cd`, `export`), users wrap in a shell block or + use a setup script + +--- + +## Example + +``` +$ dev new "add auth" +created draft 1 "add auth" + +$ echo 'export function auth() {}' > auth.ts +$ dev save +saved draft 1 (v1) + +$ echo 'export function auth(token: string) {}' > auth.ts +$ dev save +saved draft 1 (v2) + +$ dev status +draft 1 "add auth" (v2) +modified: (none) + +$ dev new "add db" +created draft 2 "add db" + +$ dev rm 1 +error: draft 1 has children. use dev rm -f to cascade. +[1] +``` diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..0f05d9c --- /dev/null +++ b/bun.lock @@ -0,0 +1,40 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "shout", + "dependencies": { + "ansis": "*", + "commander": "14.0.3", + "diff": "^8.0.3", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/diff": "^8.0.0", + }, + "peerDependencies": { + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.10", "https://npm.nose.space/@types/bun/-/bun-1.3.10.tgz", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="], + + "@types/diff": ["@types/diff@8.0.0", "https://npm.nose.space/@types/diff/-/diff-8.0.0.tgz", { "dependencies": { "diff": "*" } }, "sha512-o7jqJM04gfaYrdCecCVMbZhNdG6T1MHg/oQoRFdERLV+4d+V7FijhiEAbFu0Usww84Yijk9yH58U4Jk4HbtzZw=="], + + "@types/node": ["@types/node@25.4.0", "https://npm.nose.space/@types/node/-/node-25.4.0.tgz", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="], + + "ansis": ["ansis@4.2.0", "https://npm.nose.space/ansis/-/ansis-4.2.0.tgz", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], + + "bun-types": ["bun-types@1.3.10", "https://npm.nose.space/bun-types/-/bun-types-1.3.10.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="], + + "commander": ["commander@14.0.3", "https://npm.nose.space/commander/-/commander-14.0.3.tgz", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="], + + "diff": ["diff@8.0.3", "https://npm.nose.space/diff/-/diff-8.0.3.tgz", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + + "typescript": ["typescript@5.9.3", "https://npm.nose.space/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.18.2", "https://npm.nose.space/undici-types/-/undici-types-7.18.2.tgz", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + } +} diff --git a/index.ts b/index.ts new file mode 100644 index 0000000..f67b2c6 --- /dev/null +++ b/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..09167c8 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "@because/shout", + "version": "0.0.0", + "description": "test shell output", + "module": "src/index.ts", + "type": "module", + "files": [ + "src" + ], + "exports": { + ".": "./src/index.ts" + }, + "bin": { + "shout": "src/cli/index.ts" + }, + "scripts": { + "check": "bunx tsc --noEmit", + "build": "./scripts/build.sh", + "cli:build": "bun run scripts/build.ts", + "cli:build:all": "bun run scripts/build.ts --all", + "cli:install": "bun cli:build && sudo cp dist/shout /usr/local/bin", + "cli:link": "ln -sf $(pwd)/src/cli/index.ts ~/.bun/bin/shout", + "cli:uninstall": "sudo rm /usr/local/bin", + "test": "bun test" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/diff": "^8.0.0" + }, + "peerDependencies": { + "typescript": "^5.9.3" + }, + "dependencies": { + "commander": "14.0.3", + "diff": "^8.0.3", + "ansis": "*" + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..d5ad05c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,43 @@ +{ + "exclude": ["apps", "templates"], + "compilerOptions": { + // Environment setup & latest features + "lib": [ + "ESNext", + "DOM" + ], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "allowJs": true, + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "baseUrl": ".", + "paths": { + "$*": [ + "./src/server/*" + ], + "@*": [ + "./src/shared/*" + ], + "%*": [ + "./src/lib/*" + ] + } + } +} \ No newline at end of file