Browse Source

Extract punkboot::utils module + tests, bump project to 0.2.1

Move project-version helper procs (parse_punkproject_version,
read_punkproject_version, read_changelog_latest_version) out of make.tcl
inline definitions into a loadable punkboot::utils module so the parsing
logic is unit-testable.

- src/modules/punkboot/utils-999999.0a1.0.tm: new module (0.1.0) with
  the 3 procs in punkboot::utils namespace, PUNKARGS argdoc blocks.
- src/modules/punkboot/utils-buildversion.txt: initial 0.1.0.
- src/tests/modules/punkboot/utils/testsuites/utils/utils.test: 19 tests
  covering section tracking, missing fields, wrong sections, whitespace,
  quote styles, changelog header parsing, pre-release suffixes. All pass.
- src/make.tcl: inline procs removed; projectversion block now does
  package require punkboot::utils and calls punkboot::utils:: procs.
- Layout make.tcl files (punk.shell-0.1, punk.project-0.1): same edit;
  punk.basic keeps inline procs (no bootsupport tree).
- include_modules.config: added punkboot::utils entry (main + 2 layouts).
- Bootsupport snapshots propagated via make.tcl bootsupport.
- AGENTS.md: project version bump 0.2.0 -> 0.2.1 (patch: new make.tcl
  projectversion subcommand is part of the product surface); added
  clarifying line that make.tcl command interface warrants at least a
  patch bump; src/project_layouts/ added to directories agents should
  not directly modify.
- CHANGELOG.md: 0.2.1 entry.

Assisted-by: harness=opencode; primary-model=openrouter/z-ai/glm-5.2; api-location=openrouter.ai
master
Julian Noble 2 weeks ago
parent
commit
c8072d7945
  1. 3
      AGENTS.md
  2. 5
      CHANGELOG.md
  3. 2
      punkproject.toml
  4. 1
      src/bootsupport/modules/include_modules.config
  5. 132
      src/bootsupport/modules/punkboot/utils-0.1.0.tm
  6. 76
      src/make.tcl
  7. 132
      src/modules/punkboot/utils-999999.0a1.0.tm
  8. 4
      src/modules/punkboot/utils-buildversion.txt
  9. 28
      src/project_layouts/custom/_project/punk.basic/src/make.tcl
  10. 1
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config
  11. 132
      src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkboot/utils-0.1.0.tm
  12. 76
      src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl
  13. 1
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config
  14. 132
      src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkboot/utils-0.1.0.tm
  15. 76
      src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl
  16. 198
      src/tests/modules/punkboot/utils/testsuites/utils/utils.test

3
AGENTS.md

@ -127,6 +127,8 @@ An agent must bump the `punkproject.toml` version as part of its DOX closeout pa
Changes confined to tests, build tooling internals, or non-shipped surfaces do not require a bump. When in doubt, bump patch.
The `make.tcl` command interface is part of the product surface: new subcommands, changed subcommand behaviour, or removed subcommands warrant at least a patch bump. Internal refactors of an existing subcommand's implementation that leave its interface and output unchanged stay exempt as build tooling internals.
### Changelog
Every version bump must add a corresponding entry to `CHANGELOG.md` at the repo root. The latest `## [X.Y.Z]` header in that file must match the `version` field in `punkproject.toml`. Entries are newest-first, one bullet per notable change.
@ -181,6 +183,7 @@ The project version is fully independent of module versions. A module bump (even
- `lib/` (root) — Build output target for `tclsh src/make.tcl libs`
- `modules_tcl8/`, `modules_tcl9/` — Tcl version-specific build output targets
- `lib_tcl8/`, `lib_tcl9/` — Tcl version-specific build output targets
- `src/project_layouts/` — Project layout templates; partly updated by `make.tcl bootsupport` but full synchronisation requires manual work or future `make.tcl` improvements. Agents must not directly edit files under `src/project_layouts/` unless the user explicitly names a path there or specifically requests layout changes. When a `src/make.tcl` change affects layout make.tcl files, note it in the completion report for the user to handle — do not attempt manual synchronisation.
## Repo-wide Notes

5
CHANGELOG.md

@ -5,6 +5,11 @@ The latest `## [X.Y.Z]` header must match the `version` field in `punkproject.to
Entries are newest-first; one bullet per notable change. See the root `AGENTS.md`
"Project Versioning" section for the bump policy.
## [0.2.1] - 2026-07-06
- `make.tcl projectversion`: new advisory subcommand verifying `CHANGELOG.md` matches `punkproject.toml` and warning if `src/` has commits since the last project-version bump.
- Clarified project versioning policy: the `make.tcl` command interface is part of the product surface.
## [0.2.0] - 2026-07-05
- Initial tracking of the punkshell project version in `punkproject.toml`.

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.2.0"
version = "0.2.1"

1
src/bootsupport/modules/include_modules.config

@ -53,6 +53,7 @@ set bootsupport_modules [list\
modules flagfilter\
modules funcl\
modules punk\
modules punkboot::utils\
modules punk::pipe\
modules punkapp\
modules punkcheck\

132
src/bootsupport/modules/punkboot/utils-0.1.0.tm

@ -0,0 +1,132 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2023
#
# @@ Meta Begin
# Application punkboot::utils 0.1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::parse_punkproject_version
@cmd -name "::punkboot::utils::parse_punkproject_version"\
-summary\
"Parse the [project] version from TOML content string"\
-help\
"Scans TOML content for the [project] section and returns
the value of the version key as a string. Returns an empty
string if the section or key is not found."
@leaders
content -type string -optional 0 -help\
"TOML content string to parse"
}]
}
proc parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_punkproject_version
@cmd -name "::punkboot::utils::read_punkproject_version"\
-summary\
"Read the [project] version from a TOML file"\
-help\
"Reads the file at tomlfile and returns the [project]
version value as a string. Returns an empty string if the
file does not exist or the version is not found."
@leaders
tomlfile -type string -optional 0 -help\
"Path to the TOML file to read"
}]
}
proc read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
return [::punkboot::utils::parse_punkproject_version $content]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_changelog_latest_version
@cmd -name "::punkboot::utils::read_changelog_latest_version"\
-summary\
"Read the latest ## [X.Y.Z] version header from a changelog file"\
-help\
"Scans the changelog file from the top and returns the
version string from the first '## [X.Y.Z]' header line.
Returns an empty string if the file does not exist or no
matching header is found."
@leaders
changelogfile -type string -optional 0 -help\
"Path to the changelog file to read"
}]
}
proc read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc
}
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 0.1.0, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 0.1.0
}]
return

76
src/make.tcl

@ -328,55 +328,6 @@ proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
return [expr {$answer eq "y" || $answer eq "yes"}]
}
#------------------------------------------------------------------------------
# Project-version helpers self-contained Tcl, no punk package deps.
# Used by the 'projectversion' subcommand to verify that punkproject.toml
# and CHANGELOG.md stay consistent and that the project version is bumped
# when src/ changes ship user-visible behaviour. Advisory only (warns).
#------------------------------------------------------------------------------
# Read the [project] version from a TOML file via a simple line scan.
# Returns the version string (e.g. "0.2.0") or "" if not found.
proc ::punkboot::lib::read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
# Read the latest ## [X.Y.Z] version header from a changelog file.
# Returns the version string or "" if no header found.
proc ::punkboot::lib::read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
if {"::try" ni [info commands ::try]} {
puts stderr "Tcl interpreter possibly too old - need at least tcl 8.6 - 'try' command not found - aborting"
exit 1
@ -1877,6 +1828,7 @@ if {$::punkboot::command eq "info"} {
}
if {$::punkboot::command eq "projectversion"} {
package require punkboot::utils
set sep [string repeat - 75]
puts stdout $sep
puts stdout "project version check"
@ -1886,8 +1838,8 @@ if {$::punkboot::command eq "projectversion"} {
set cl_file [file join $projectroot CHANGELOG.md]
set src_folder [file join $projectroot src]
set pp_version [::punkboot::lib::read_punkproject_version $pp_file]
set cl_version [::punkboot::lib::read_changelog_latest_version $cl_file]
set pp_version [::punkboot::utils::read_punkproject_version $pp_file]
set cl_version [::punkboot::utils::read_changelog_latest_version $cl_file]
puts stdout "punkproject.toml version : [expr {[string length $pp_version] ? $pp_version : "(not found)"}]"
puts stdout "CHANGELOG.md version : [expr {[string length $cl_version] ? $cl_version : "(not found)"}]"
@ -1906,6 +1858,8 @@ if {$::punkboot::command eq "projectversion"} {
# 2. Staleness: warn if src/ has git commits since the last commit
# touching punkproject.toml (version bump may be overdue).
# If punkproject.toml has an uncommitted version change, the bump is
# already in progress report that only a commit is needed.
# Best-effort silently skipped if git is unavailable or the repo
# is not under git revision control.
set pp_commit ""
@ -1917,8 +1871,24 @@ if {$::punkboot::command eq "projectversion"} {
set src_changes [string trim $src_raw]
if {[string length $src_changes]} {
set n [llength [split $src_changes \n]]
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
# Check whether a bump is already staged in the working tree
# (punkproject.toml modified but uncommitted). If the working-
# tree version differs from HEAD, the bump is done only a
# commit is needed.
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
set bump_uncommitted [expr {[string length $head_version] && [string length $pp_version] && $pp_version ne $head_version}]
if {$bump_uncommitted} {
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ($head_version -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
} else {
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
}
puts stderr " (showing first 5 commits below)"
set i 0
foreach line [split $src_changes \n] {

132
src/modules/punkboot/utils-999999.0a1.0.tm

@ -0,0 +1,132 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2023
#
# @@ Meta Begin
# Application punkboot::utils 999999.0a1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::parse_punkproject_version
@cmd -name "::punkboot::utils::parse_punkproject_version"\
-summary\
"Parse the [project] version from TOML content string"\
-help\
"Scans TOML content for the [project] section and returns
the value of the version key as a string. Returns an empty
string if the section or key is not found."
@leaders
content -type string -optional 0 -help\
"TOML content string to parse"
}]
}
proc parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_punkproject_version
@cmd -name "::punkboot::utils::read_punkproject_version"\
-summary\
"Read the [project] version from a TOML file"\
-help\
"Reads the file at tomlfile and returns the [project]
version value as a string. Returns an empty string if the
file does not exist or the version is not found."
@leaders
tomlfile -type string -optional 0 -help\
"Path to the TOML file to read"
}]
}
proc read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
return [::punkboot::utils::parse_punkproject_version $content]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_changelog_latest_version
@cmd -name "::punkboot::utils::read_changelog_latest_version"\
-summary\
"Read the latest ## [X.Y.Z] version header from a changelog file"\
-help\
"Scans the changelog file from the top and returns the
version string from the first '## [X.Y.Z]' header line.
Returns an empty string if the file does not exist or no
matching header is found."
@leaders
changelogfile -type string -optional 0 -help\
"Path to the changelog file to read"
}]
}
proc read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc
}
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 999999.0a1.0, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 999999.0a1.0
}]
return

4
src/modules/punkboot/utils-buildversion.txt

@ -0,0 +1,4 @@
0.1.0
#First line must be a semantic version number
#all other lines are ignored.
#0.1.0 - initial: parse_punkproject_version, read_punkproject_version, read_changelog_latest_version extracted from make.tcl inline procs

28
src/project_layouts/custom/_project/punk.basic/src/make.tcl

@ -345,6 +345,12 @@ proc ::punkboot::lib::read_punkproject_version {tomlfile} {
} finally {
close $fh
}
return [::punkboot::lib::parse_punkproject_version $content]
}
# Parse the [project] version from TOML content string.
# Returns the version string (e.g. "0.2.0") or "" if not found.
proc ::punkboot::lib::parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
@ -1906,6 +1912,8 @@ if {$::punkboot::command eq "projectversion"} {
# 2. Staleness: warn if src/ has git commits since the last commit
# touching punkproject.toml (version bump may be overdue).
# If punkproject.toml has an uncommitted version change, the bump is
# already in progress report that only a commit is needed.
# Best-effort silently skipped if git is unavailable or the repo
# is not under git revision control.
set pp_commit ""
@ -1917,8 +1925,24 @@ if {$::punkboot::command eq "projectversion"} {
set src_changes [string trim $src_raw]
if {[string length $src_changes]} {
set n [llength [split $src_changes \n]]
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
# Check whether a bump is already staged in the working tree
# (punkproject.toml modified but uncommitted). If the working-
# tree version differs from HEAD, the bump is done only a
# commit is needed.
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::lib::parse_punkproject_version $head_raw]
}
set bump_uncommitted [expr {[string length $head_version] && [string length $pp_version] && $pp_version ne $head_version}]
if {$bump_uncommitted} {
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ($head_version -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
} else {
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
}
puts stderr " (showing first 5 commits below)"
set i 0
foreach line [split $src_changes \n] {

1
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/include_modules.config

@ -53,6 +53,7 @@ set bootsupport_modules [list\
modules flagfilter\
modules funcl\
modules punk\
modules punkboot::utils\
modules punk::pipe\
modules punkapp\
modules punkcheck\

132
src/project_layouts/custom/_project/punk.project-0.1/src/bootsupport/modules/punkboot/utils-0.1.0.tm

@ -0,0 +1,132 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2023
#
# @@ Meta Begin
# Application punkboot::utils 0.1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::parse_punkproject_version
@cmd -name "::punkboot::utils::parse_punkproject_version"\
-summary\
"Parse the [project] version from TOML content string"\
-help\
"Scans TOML content for the [project] section and returns
the value of the version key as a string. Returns an empty
string if the section or key is not found."
@leaders
content -type string -optional 0 -help\
"TOML content string to parse"
}]
}
proc parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_punkproject_version
@cmd -name "::punkboot::utils::read_punkproject_version"\
-summary\
"Read the [project] version from a TOML file"\
-help\
"Reads the file at tomlfile and returns the [project]
version value as a string. Returns an empty string if the
file does not exist or the version is not found."
@leaders
tomlfile -type string -optional 0 -help\
"Path to the TOML file to read"
}]
}
proc read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
return [::punkboot::utils::parse_punkproject_version $content]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_changelog_latest_version
@cmd -name "::punkboot::utils::read_changelog_latest_version"\
-summary\
"Read the latest ## [X.Y.Z] version header from a changelog file"\
-help\
"Scans the changelog file from the top and returns the
version string from the first '## [X.Y.Z]' header line.
Returns an empty string if the file does not exist or no
matching header is found."
@leaders
changelogfile -type string -optional 0 -help\
"Path to the changelog file to read"
}]
}
proc read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc
}
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 0.1.0, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 0.1.0
}]
return

76
src/project_layouts/custom/_project/punk.project-0.1/src/make.tcl

@ -328,55 +328,6 @@ proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
return [expr {$answer eq "y" || $answer eq "yes"}]
}
#------------------------------------------------------------------------------
# Project-version helpers self-contained Tcl, no punk package deps.
# Used by the 'projectversion' subcommand to verify that punkproject.toml
# and CHANGELOG.md stay consistent and that the project version is bumped
# when src/ changes ship user-visible behaviour. Advisory only (warns).
#------------------------------------------------------------------------------
# Read the [project] version from a TOML file via a simple line scan.
# Returns the version string (e.g. "0.2.0") or "" if not found.
proc ::punkboot::lib::read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
# Read the latest ## [X.Y.Z] version header from a changelog file.
# Returns the version string or "" if no header found.
proc ::punkboot::lib::read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
if {"::try" ni [info commands ::try]} {
puts stderr "Tcl interpreter possibly too old - need at least tcl 8.6 - 'try' command not found - aborting"
exit 1
@ -1877,6 +1828,7 @@ if {$::punkboot::command eq "info"} {
}
if {$::punkboot::command eq "projectversion"} {
package require punkboot::utils
set sep [string repeat - 75]
puts stdout $sep
puts stdout "project version check"
@ -1886,8 +1838,8 @@ if {$::punkboot::command eq "projectversion"} {
set cl_file [file join $projectroot CHANGELOG.md]
set src_folder [file join $projectroot src]
set pp_version [::punkboot::lib::read_punkproject_version $pp_file]
set cl_version [::punkboot::lib::read_changelog_latest_version $cl_file]
set pp_version [::punkboot::utils::read_punkproject_version $pp_file]
set cl_version [::punkboot::utils::read_changelog_latest_version $cl_file]
puts stdout "punkproject.toml version : [expr {[string length $pp_version] ? $pp_version : "(not found)"}]"
puts stdout "CHANGELOG.md version : [expr {[string length $cl_version] ? $cl_version : "(not found)"}]"
@ -1906,6 +1858,8 @@ if {$::punkboot::command eq "projectversion"} {
# 2. Staleness: warn if src/ has git commits since the last commit
# touching punkproject.toml (version bump may be overdue).
# If punkproject.toml has an uncommitted version change, the bump is
# already in progress report that only a commit is needed.
# Best-effort silently skipped if git is unavailable or the repo
# is not under git revision control.
set pp_commit ""
@ -1917,8 +1871,24 @@ if {$::punkboot::command eq "projectversion"} {
set src_changes [string trim $src_raw]
if {[string length $src_changes]} {
set n [llength [split $src_changes \n]]
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
# Check whether a bump is already staged in the working tree
# (punkproject.toml modified but uncommitted). If the working-
# tree version differs from HEAD, the bump is done only a
# commit is needed.
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
set bump_uncommitted [expr {[string length $head_version] && [string length $pp_version] && $pp_version ne $head_version}]
if {$bump_uncommitted} {
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ($head_version -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
} else {
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
}
puts stderr " (showing first 5 commits below)"
set i 0
foreach line [split $src_changes \n] {

1
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/include_modules.config

@ -53,6 +53,7 @@ set bootsupport_modules [list\
modules flagfilter\
modules funcl\
modules punk\
modules punkboot::utils\
modules punk::pipe\
modules punkapp\
modules punkcheck\

132
src/project_layouts/custom/_project/punk.shell-0.1/src/bootsupport/modules/punkboot/utils-0.1.0.tm

@ -0,0 +1,132 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2023
#
# @@ Meta Begin
# Application punkboot::utils 0.1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
##e.g package require frobz
package require Tcl 8.6-
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punkboot::utils {
variable version 0.1.0
namespace export parse_punkproject_version read_punkproject_version read_changelog_latest_version
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::parse_punkproject_version
@cmd -name "::punkboot::utils::parse_punkproject_version"\
-summary\
"Parse the [project] version from TOML content string"\
-help\
"Scans TOML content for the [project] section and returns
the value of the version key as a string. Returns an empty
string if the section or key is not found."
@leaders
content -type string -optional 0 -help\
"TOML content string to parse"
}]
}
proc parse_punkproject_version {content} {
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_punkproject_version
@cmd -name "::punkboot::utils::read_punkproject_version"\
-summary\
"Read the [project] version from a TOML file"\
-help\
"Reads the file at tomlfile and returns the [project]
version value as a string. Returns an empty string if the
file does not exist or the version is not found."
@leaders
tomlfile -type string -optional 0 -help\
"Path to the TOML file to read"
}]
}
proc read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
return [::punkboot::utils::parse_punkproject_version $content]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::punkboot::utils::read_changelog_latest_version
@cmd -name "::punkboot::utils::read_changelog_latest_version"\
-summary\
"Read the latest ## [X.Y.Z] version header from a changelog file"\
-help\
"Scans the changelog file from the top and returns the
version string from the first '## [X.Y.Z]' header line.
Returns an empty string if the file does not exist or no
matching header is found."
@leaders
changelogfile -type string -optional 0 -help\
"Path to the changelog file to read"
}]
}
proc read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punkboot::utils ::punkboot::utils::argdoc
}
## Ready
package provide punkboot::utils [tcl::namespace::eval punkboot::utils {
variable version
#- this version number, exactly 0.1.0, is a literal used in src module folders
#- we refer to this sometimes as the magic version number
set version 0.1.0
}]
return

76
src/project_layouts/custom/_project/punk.shell-0.1/src/make.tcl

@ -328,55 +328,6 @@ proc ::punkboot::lib::bootsupport_prompt_yesno {prompt} {
return [expr {$answer eq "y" || $answer eq "yes"}]
}
#------------------------------------------------------------------------------
# Project-version helpers self-contained Tcl, no punk package deps.
# Used by the 'projectversion' subcommand to verify that punkproject.toml
# and CHANGELOG.md stay consistent and that the project version is bumped
# when src/ changes ship user-visible behaviour. Advisory only (warns).
#------------------------------------------------------------------------------
# Read the [project] version from a TOML file via a simple line scan.
# Returns the version string (e.g. "0.2.0") or "" if not found.
proc ::punkboot::lib::read_punkproject_version {tomlfile} {
if {![file exists $tomlfile]} {return ""}
set fh [open $tomlfile r]
try {
set content [read $fh]
} finally {
close $fh
}
set in_project 0
foreach line [split $content \n] {
set trimmed [string trim $line]
if {[string index $trimmed 0] eq {[} && [string index $trimmed end] eq {]}} {
set in_project [expr {$trimmed eq "\[project\]"}]
continue
}
if {$in_project && [regexp {^version\s*=\s*"([^"]+)"} $trimmed _ v]} {
return $v
}
}
return ""
}
# Read the latest ## [X.Y.Z] version header from a changelog file.
# Returns the version string or "" if no header found.
proc ::punkboot::lib::read_changelog_latest_version {changelogfile} {
if {![file exists $changelogfile]} {return ""}
set fh [open $changelogfile r]
try {
set content [read $fh]
} finally {
close $fh
}
foreach line [split $content \n] {
if {[regexp {^##\s*\[([0-9][^\]]*)\]} $line _ v]} {
return $v
}
}
return ""
}
if {"::try" ni [info commands ::try]} {
puts stderr "Tcl interpreter possibly too old - need at least tcl 8.6 - 'try' command not found - aborting"
exit 1
@ -1877,6 +1828,7 @@ if {$::punkboot::command eq "info"} {
}
if {$::punkboot::command eq "projectversion"} {
package require punkboot::utils
set sep [string repeat - 75]
puts stdout $sep
puts stdout "project version check"
@ -1886,8 +1838,8 @@ if {$::punkboot::command eq "projectversion"} {
set cl_file [file join $projectroot CHANGELOG.md]
set src_folder [file join $projectroot src]
set pp_version [::punkboot::lib::read_punkproject_version $pp_file]
set cl_version [::punkboot::lib::read_changelog_latest_version $cl_file]
set pp_version [::punkboot::utils::read_punkproject_version $pp_file]
set cl_version [::punkboot::utils::read_changelog_latest_version $cl_file]
puts stdout "punkproject.toml version : [expr {[string length $pp_version] ? $pp_version : "(not found)"}]"
puts stdout "CHANGELOG.md version : [expr {[string length $cl_version] ? $cl_version : "(not found)"}]"
@ -1906,6 +1858,8 @@ if {$::punkboot::command eq "projectversion"} {
# 2. Staleness: warn if src/ has git commits since the last commit
# touching punkproject.toml (version bump may be overdue).
# If punkproject.toml has an uncommitted version change, the bump is
# already in progress report that only a commit is needed.
# Best-effort silently skipped if git is unavailable or the repo
# is not under git revision control.
set pp_commit ""
@ -1917,8 +1871,24 @@ if {$::punkboot::command eq "projectversion"} {
set src_changes [string trim $src_raw]
if {[string length $src_changes]} {
set n [llength [split $src_changes \n]]
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
# Check whether a bump is already staged in the working tree
# (punkproject.toml modified but uncommitted). If the working-
# tree version differs from HEAD, the bump is done only a
# commit is needed.
set head_version ""
if {![catch {exec git -C $projectroot show HEAD:punkproject.toml} head_raw]} {
set head_version [::punkboot::utils::parse_punkproject_version $head_raw]
}
set bump_uncommitted [expr {[string length $head_version] && [string length $pp_version] && $pp_version ne $head_version}]
if {$bump_uncommitted} {
puts stderr "NOTE: $n commit(s) in src/ since last punkproject.toml commit, but punkproject.toml already bumped ($head_version -> $pp_version, uncommitted)."
puts stderr " Commit punkproject.toml (and CHANGELOG.md) to record the bump and clear this warning."
} else {
puts stderr "WARNING: $n commit(s) in src/ since last punkproject.toml change a project-version bump may be overdue."
puts stderr " Review the changes and bump punkproject.toml per the root AGENTS.md 'Project Versioning' section."
}
puts stderr " (showing first 5 commits below)"
set i 0
foreach line [split $src_changes \n] {

198
src/tests/modules/punkboot/utils/testsuites/utils/utils.test

@ -0,0 +1,198 @@
# -*- tcl -*-
# Tests for punkboot::utils project-version helpers:
# - parse_punkproject_version: TOML content string -> version string
# - read_changelog_latest_version: changelog file -> latest ## [X.Y.Z] header
# Run: tclsh src/tests/runtests.tcl -report compact -show-passes 0 -include-paths modules/punkboot/utils/** utils.test
package require tcltest
package require punk::lib
package require punkboot::utils
namespace eval ::testspace {
namespace import ::tcltest::*
# -------------------------------------------------------------------------
# Helper: write content to a temp file. Returns the file path.
# -------------------------------------------------------------------------
proc write_temp_file {prefix content} {
set dir [punk::lib::tempdir_newfolder -prefix $prefix]
set path [file join $dir data]
set fd [open $path w]
try { puts -nonewline $fd $content } finally { close $fd }
return $path
}
# =========================================================================
# parse_punkproject_version
# =========================================================================
test parse_ppv_standard {Standard [project] version line} -body {
set toml {[project]
name = "punkshell"
version = "0.2.0"
}
::punkboot::utils::parse_punkproject_version $toml
} -result "0.2.0"
test parse_ppv_no_version_field {Missing version key in [project] section} -body {
set toml {[project]
name = "punkshell"
}
::punkboot::utils::parse_punkproject_version $toml
} -result ""
test parse_ppv_version_in_wrong_section {Version in a non-[project] section is ignored} -body {
set toml {[project]
name = "punkshell"
[other]
version = "9.9.9"
}
::punkboot::utils::parse_punkproject_version $toml
} -result ""
test parse_ppv_empty_content {Empty string returns empty} -body {
::punkboot::utils::parse_punkproject_version ""
} -result ""
test parse_ppv_no_section {No section header at all} -body {
::punkboot::utils::parse_punkproject_version {version = "1.0.0"}
} -result ""
test parse_ppv_whitespace_around_equals {Whitespace variations around = are tolerated} -body {
set toml {[project]
version = "0.3.1"
}
::punkboot::utils::parse_punkproject_version $toml
} -result "0.3.1"
test parse_ppv_section_after_project {A later section does not override [project] version} -body {
set toml {[project]
version = "0.2.0"
[dependencies]
version = "1.5.0"
}
::punkboot::utils::parse_punkproject_version $toml
} -result "0.2.0"
test parse_ppv_singlequoted_version {Single-quoted value is not matched (TOML requires double quotes)} -body {
set toml {[project]
version = '0.2.0'
}
::punkboot::utils::parse_punkproject_version $toml
} -result ""
# =========================================================================
# read_punkproject_version
# =========================================================================
test read_ppv_file_not_found {Non-existent file returns empty} -body {
::punkboot::utils::read_punkproject_version /nonexistent/path/to/punkproject.toml
} -result ""
test read_ppv_file_standard {Read a real-ish TOML file} -body {
set path [write_temp_file ppvread {[project]
name = "punkshell"
version = "1.2.3"
}]
set result [::punkboot::utils::read_punkproject_version $path]
file delete -force [file dirname $path]
set result
} -result "1.2.3"
# =========================================================================
# read_changelog_latest_version
# =========================================================================
test read_clv_standard {Standard changelog with one entry} -body {
set content {# Changelog
## [0.2.0] - 2026-07-05
- Initial tracking.
}
set path [write_temp_file clvstd $content]
set result [::punkboot::utils::read_changelog_latest_version $path]
file delete -force [file dirname $path]
set result
} -result "0.2.0"
test read_clv_multiple_entries {First (newest) header returned when multiple exist} -body {
set content {# Changelog
## [0.2.1] - 2026-07-06
- New subcommand.
## [0.2.0] - 2026-07-05
- Initial tracking.
}
set path [write_temp_file clvmulti $content]
set result [::punkboot::utils::read_changelog_latest_version $path]
file delete -force [file dirname $path]
set result
} -result "0.2.1"
test read_clv_file_not_found {Non-existent file returns empty} -body {
::punkboot::utils::read_changelog_latest_version /nonexistent/path/to/CHANGELOG.md
} -result ""
test read_clv_empty_file {Empty file returns empty} -body {
set path [write_temp_file clvempty ""]
set result [::punkboot::utils::read_changelog_latest_version $path]
file delete -force [file dirname $path]
set result
} -result ""
test read_clv_no_headers {File with no ## [X.Y.Z] headers returns empty} -body {
set content {# Changelog
Some prose without a version header.
}
set path [write_temp_file clvnohdr $content]
set result [::punkboot::utils::read_changelog_latest_version $path]
file delete -force [file dirname $path]
set result
} -result ""
test read_clv_extra_spaces {Extra whitespace after ## is tolerated} -body {
set content {## [0.5.0] - 2026-08-01}
set path [write_temp_file clvspc $content]
set result [::punkboot::utils::read_changelog_latest_version $path]
file delete -force [file dirname $path]
set result
} -result "0.5.0"
test read_clv_prerelease_suffix {Version with pre-release suffix is captured} -body {
set content {## [0.2.1-rc1] - 2026-07-06}
set path [write_temp_file clvpre $content]
set result [::punkboot::utils::read_changelog_latest_version $path]
file delete -force [file dirname $path]
set result
} -result "0.2.1-rc1"
test read_clv_single_hash {Single # header is not matched (only ##)} -body {
set content {# [0.1.0] this is a single hash}
set path [write_temp_file clv1h $content]
set result [::punkboot::utils::read_changelog_latest_version $path]
file delete -force [file dirname $path]
set result
} -result ""
test read_clv_triple_hash {Triple ### subheader is not matched} -body {
set content {## [0.2.0] - 2026-07-05
### Fixed
- something
}
set path [write_temp_file clv3h $content]
set result [::punkboot::utils::read_changelog_latest_version $path]
file delete -force [file dirname $path]
set result
} -result "0.2.0"
tcltest::cleanupTests
}
Loading…
Cancel
Save