shout it out
This commit is contained in:
commit
c4cd5353bf
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
|
|
@ -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
|
||||
15
README.md
Normal file
15
README.md
Normal file
|
|
@ -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.
|
||||
201
SPEC.md
Normal file
201
SPEC.md
Normal file
|
|
@ -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 <path>` | Prepend `<path>` to `PATH` instead of auto-detecting |
|
||||
| `--timeout <dur>` | 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]
|
||||
```
|
||||
40
bun.lock
Normal file
40
bun.lock
Normal file
|
|
@ -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=="],
|
||||
}
|
||||
}
|
||||
38
package.json
Normal file
38
package.json
Normal file
|
|
@ -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": "*"
|
||||
}
|
||||
}
|
||||
43
tsconfig.json
Normal file
43
tsconfig.json
Normal file
|
|
@ -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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user