Commits: 3

Write a spec for a list subcommand

The spec describes misadventures of Alice and Bob, working on a high-tech Project Alpha.

The subcommand is not implemented yet, and the interpreter is not ready to evaluate it, but that's how we roll in the BDD club.

index d426ca8..3303b7f 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -2,42 +2,149 @@
=interpreter: nu --stdin spec/interpreters/basic.nu
=---
=
-# Basic
+# The Dev Log Excavator
=
-This program generates a devlog for tad-lispy.com
+This program excavates a devlog (i.e. a bunch of .md files) from commit messages in multiple repositories.
=
-## Sample data
+
+## The `list` Subcommand
=
=IT should eventually be possible to evaluate this spec using TBB. The difficulty is, that any sample data to work on needs to be a git repository, while this project itself is version controlled using git. One way would be to have TBB make commits with fabricated dates, using `git commit --date`.
=
-## Get all projects
+  * Write `weblog.toml`
+    
+    ``` toml
+    [[projects]]
+    name = "Project Alpha"
+    path = "./project-alpha"
+    ```
+  
+  * Initialize a git repository `project-alpha`
=
-We need a mapping from `project-name` to `path`. This should sit in `projects.yaml`
+  * Write `README.md`
+  
+    ``` markdown
+    # This is Project Alpha
+    
+    The best project ever. Like seriosuly.
+    ```
+    
+  * On `2026-03-27 13:22` commit as `Alice <alice@example.com>`
+  
+    ``` text
+    Write a readme
+    
+    It's very exciting to start a new project.
+    ```
+    
+  * Write `src/main.rs`
+     
+    ``` rust
+    fn main() {
+        println ("Hello, World!")
+    }
+    ```
=
-The listing of all paths with names guessed from path can be obtained like this:
+  * On `2026-03-27 14:06` commit as `Bob <bob@example.com>`
+  
+    ``` text
+    Implement an MVP
+    
+    I need to keep my phone charged. Investors are going to call any minute now.
+    ```
+  
+  * Write `src/main.rs`
+     
+    ``` rust
+    fn main() {
+        println!("Hello, World!")
+    }
+    ```
=
-``` nushell
-glob ~/Projects/**/.git 
-| path dirname 
-| each { |p| { name: ($p | path basename), path: $p } } 
-| transpose --as-record -r 
-| save --force all-projects.yaml
-```
+  * Write `CHANGELOG.md`
+     
+    ``` markdown
+    # v1.0.0
+    
+    * Now it's working
+   ```
=
+  * On `2026-03-28 03:06` commit as `Alice <alice@example.com>`
+  
+      Notice that it's logically the same day - any work done until 04:00 AM is considered to belong to the previous day.
+      
+    ``` text
+    Make Project Alpha work
+    
+    Coding hard. I should have used Tad Better Behavior to test my stuff.
+    ```
=
-## Get days
+  * Write `LICENSE`
+  
+  ``` text
+  All rights reserved! You monkeys better not even look at my pro codes.
+  ```
+  
+  
+  * On `2026-04-01 09:03` commit as `Bob <bob@example.com>`
+  
+    Two days later
+      
+    ``` text
+    I've lawyered up
+    
+    With a perl like this project, I have to protect my IP.
+    ```
+    
+  * Write `src/main.rs`
+     
+    ``` rust
+    fn main() {
+        println ("Eat 💩, Bob!")
+    }
+    ```
=
-I want a list with `start` and `end` timestamps. It should support `--since` and `--until`. Until should by default be 4 A.M. today.  Since will be supplied based on the last entry in the devlog (details tbd). First day should be from `since`, i.e. it probably won't be a full day, but a fraction.
+  * On `2026-03-28 14:18` commit as `Alice <alice@example.com>`
+  
=
-An arbitrary date with a given time can be obtained with the following:
+      
+    ``` text
+    I quit
+    
+    Bob is an asshole.
+    ```
=
-``` nu
-("04:00" | date from-human) - 1day
-```
+  * Run the `devlog list --projects-path .` command
+  
+  * Expect the output to contain `the heading row`
=
-So maybe I can just loop until I reach the time before `since`, emit the last fraction and break?
+    It contains column names, as produced by Nushell. The output format should be configurable.
+    
+    ``` regexp
+    
+    | day +\| time +\| sha +\| author +\| email +\| title +\| project +\| path +\|
+    ```
+  * Expect the output to contain `the first commit from Alice`
+  
+    ``` regex
+    | 2026-04-26 | 2026-04-27 13:22:00 +\| [0-9a-f]+ +\| Alice +\| alice@example.com +\| Make Project Alpha work +\| Project Alpha +| .+/project-alpha/ +|
+    ```
+
+  * Expect the output to contain `the late night commit from Alice`
+  
+    Notice that the logical day is one before the actual day (in the second column), because work done at night is still counted toward the previous day. This can be controlled with `start-of-day` configuration parameter.
+    
+    ``` regex
+    | 2026-03-27 | 2026-03-28 03:06 +\| [0-9a-f]+ +\| Alice +\| alice@example.com +\| Write a readme +\| Project Alpha +| .+/project-alpha/ +|
+    ```
+
+  * Expect the output to contain `the silly commit from Bob`
+  
+    ``` regex
+    | 2026-04-01 | 2026-04-01 09:03 +\| [0-9a-f]+ +\| Bob +\| bob@example.com +\| I've lawyered up +\| Project Alpha +| .+/project-alpha/ +|
+    ```
=
-## Generate the drafts
+## The `excavate` Subcommand
=
=For each day run `git log` in each project. If there is any output, dump it to `devlog/<date>/<project-name>.md`. So each file should contain a log from a single project that was developed on a given day.
=

Correct devlog list scenario

The nushell tables are delimited not with pipe characters |, but with a special unicode symbol │. The good thing about it is that, unlike pipe characters, they don't need to be escaped.

For now the command has .nu extension, so that how it needs to be called. I'm thinking about a way to change it. Maybe just rename the file?

I've also removed the test scenario. It's going to be copied to the TBB project itself, together with tbb.nu.

index 3303b7f..9a1451b 100644
--- a/spec/BASIC.md
+++ b/spec/BASIC.md
@@ -9,6 +9,10 @@ This program excavates a devlog (i.e. a bunch of .md files) from commit messages
=
=## The `list` Subcommand
=
+``` yaml tbb
+tags: [ focus ]
+```
+
=IT should eventually be possible to evaluate this spec using TBB. The difficulty is, that any sample data to work on needs to be a git repository, while this project itself is version controlled using git. One way would be to have TBB make commits with fabricated dates, using `git commit --date`.
=
=  * Write `weblog.toml`
@@ -19,7 +23,7 @@ IT should eventually be possible to evaluate this spec using TBB. The difficulty
=    path = "./project-alpha"
=    ```
=  
-  * Initialize a git repository `project-alpha`
+  * Initialize a git repository at `project-alpha`
=
=  * Write `README.md`
=  
@@ -114,20 +118,25 @@ IT should eventually be possible to evaluate this spec using TBB. The difficulty
=    Bob is an asshole.
=    ```
=
-  * Run the `devlog list --projects-path .` command
+  * Change directory to `..`
+  
+    Leave the repository and go back to where the config file is.
+
+  * Run the `devlog.nu list --projects-dir .` command
+  
+    TODO: Make it `devlog list`, without the extension,
=  
=  * Expect the output to contain `the heading row`
=
=    It contains column names, as produced by Nushell. The output format should be configurable.
=    
=    ``` regexp
-    
-    | day +\| time +\| sha +\| author +\| email +\| title +\| project +\| path +\|
+    │ day +│ time +│ sha +│ author +│ email +│ title +│ project +│ path +│
=    ```
=  * Expect the output to contain `the first commit from Alice`
=  
=    ``` regex
-    | 2026-04-26 | 2026-04-27 13:22:00 +\| [0-9a-f]+ +\| Alice +\| alice@example.com +\| Make Project Alpha work +\| Project Alpha +| .+/project-alpha/ +|
+    │ 2026-04-26 +│ 2026-04-27 13:22:00 +│ [0-9a-f]+ +│ Alice +│ alice@example.com +│ Make Project Alpha work +│ Project Alpha +│ .+/project-alpha/ +│
=    ```
=
=  * Expect the output to contain `the late night commit from Alice`
@@ -135,13 +144,13 @@ IT should eventually be possible to evaluate this spec using TBB. The difficulty
=    Notice that the logical day is one before the actual day (in the second column), because work done at night is still counted toward the previous day. This can be controlled with `start-of-day` configuration parameter.
=    
=    ``` regex
-    | 2026-03-27 | 2026-03-28 03:06 +\| [0-9a-f]+ +\| Alice +\| alice@example.com +\| Write a readme +\| Project Alpha +| .+/project-alpha/ +|
+    │ 2026-03-27 │ 2026-03-28 03:06 +│ [0-9a-f]+ +│ Alice +│ alice@example.com +│ Write a readme +│ Project Alpha +│ .+/project-alpha/ +│
=    ```
=
=  * Expect the output to contain `the silly commit from Bob`
=  
=    ``` regex
-    | 2026-04-01 | 2026-04-01 09:03 +\| [0-9a-f]+ +\| Bob +\| bob@example.com +\| I've lawyered up +\| Project Alpha +| .+/project-alpha/ +|
+    │ 2026-04-01 │ 2026-04-01 09:03 +│ [0-9a-f]+ +│ Bob +│ bob@example.com +│ I've lawyered up +│ Project Alpha +│ .+/project-alpha/ +│
=    ```
=
=## The `excavate` Subcommand
@@ -198,16 +207,3 @@ A project can be flagged to be ignored with the `ignore` key.
=
=Any project that is not listed, but had activity within the period for which a devlog is being generated should produce a warning with useful instructions.
=
-## Interpreter test
-
-This is not real spec, just a quick test of the WIP interpreter.
-
-  * Do the twist `2` times
-
-  * Do something `nice` and then `sleep` for `2` hours
-  
-    ``` text
-    Zzzzz.....
-    ```
-
-  * Do the twist `4` times

Write the real interpreter for the basic TBB suite

The spec is not satisfied yet, so the evaluation fails, but I believe the interpreter is correct. Now finally off to work to implement the devlog list command 😅

index 517943f..69c1644 100644
--- a/spec/interpreters/basic.nu
+++ b/spec/interpreters/basic.nu
@@ -1,34 +1,82 @@
=use tbb.nu
=
=def main [] {
-    tbb run --initial-state { twists: 0 } {
-        "Do something {0} and then {1} for {2} hours": { |how, what, how_long, data|
-            $"OK boss. I will do something ($how) for ($how_long) hours and then I'll ($what)"
-            | tbb observe text --transform { str upcase }
+    tbb run --initial-state { pwd: (mktemp --directory ) } {
+        "Write {0}": { |filename, data|
+            $data.state.pwd
+            | path join $filename
+            | tbb observe text --transform { $"Writing content to ($in)" }
+            | let path
+            | path dirname
+            | tbb observe text --transform  { $"Parent directory ($in)" }
+            | mkdir $in
+            
+            
=            $data.code_blocks
=            | first
-            | tbb observe snippet --caption "Received code block"
-            | ignore # Do not send any state update.
+            | get value
+            | save --force $path
=        },
-        "Do the twist {0} times": { |how_many, data|
-            # All arguments (except data) are passed as strings
-            # TODO: Can we cast them based on closure parameter types?
-            $how_many | into int | let how_many
=
-            # This step demonstrates use of state. We count the total number of twists.
-            for n in 1..$how_many {
-                let done_twists = $n + $data.state.twists
+        "Initialize a git repository at {0}": {|dirname, data|
+            cd $data.state.pwd
+            mkdir $dirname
+            cd $dirname
+            
+            git init
+            | complete
+            | tbb observe snippet --caption "Output from git init"
=
-                $"Doing the twist ($done_twists) 🕺"
-                | tbb observe text 
=
-                if $done_twists > 5 {
-                    error make "That's too many! My head is spinning 🤣"
-                }
-            }
+            { pwd: (pwd) }
+        },
+
+        "On {0} commit as {1}": { |time, author, data|
+            cd $data.state.pwd
=
-            # Update the state for other steps. This record will be merged into the previous state.
-            { twists: ($data.state.twists + $how_many) }
+            git add .
+            
+            $data.code_blocks
+            | first
+            | get value
+            | git commit --date $time --author $author --message $in
+            | tbb observe text
+            | ignore
+        },
+        "Change directory to {0}": { |destination, data|
+            $data.state.pwd
+            | path join $destination
+            | { pwd: $in }
+        }
+        
+        "Run the {0} command": { |command, data|
+            pwd
+            # | path join "bin/"
+            | tbb observe text --transform  { $"Adding Devlog Excavator directory ($in) to PATH" }
+            | let devlog_path
+
+            $env.PATH = $env.PATH
+            | append $devlog_path
+            | tbb observe snippet --caption "Path environment variable"
+
+            cd $data.state.pwd
+
+            $command
+            | tbb shell $in
+            | { output: $in }
+        },
+        "Expect the output to contain {0}": { |label, data|
+            $data.code_blocks
+            | first
+            | get value
+            | let pattern
+            | tbb observe snippet --caption "The needle"
+            
+            $data.state.output
+            | ansi strip
+            | tbb observe snippet --caption "The haystack"
+            | tbb assert match  $pattern
=        }
=    }
=}
+
index b2d1284..0bf7a1d 100644
--- a/spec/interpreters/tbb.nu
+++ b/spec/interpreters/tbb.nu
@@ -1,3 +1,5 @@
+use std/assert
+
=export def run [
=    steps: record
=    --initial-state: record = {}
@@ -118,3 +120,27 @@ export def "observe text" [
=
=    $in # Pass the value on
=}
+
+export def "shell" [command: string]: string -> record {
+    $in
+    | sh -c $command
+    | complete
+    | observe snippet --caption $"Results of running `($command)`"
+    | if $in.exit_code != 0 {
+        error make {
+            msg: $"Command exited with code ($in.exit_code)",
+            labels: [{ text: $"Command:\n($command)", span: (metadata $command).span }]
+        }
+    } else {
+        $in.stdout
+    }
+}
+
+export def "assert match" [
+    pattern: string
+]: string -> string {
+    assert ($in | find --regex $pattern | is-not-empty) --error-label {
+        text: "Pattern not found in the input",
+        span: (metadata $pattern).span
+    }
+}