commit c4cd5353bf26a177eb562a45049ffa57105ef34c Author: Chris Wanstrath Date: Mon Mar 9 21:13:39 2026 -0700 shout it out 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