Browse Source

vfscommonupdate: promote app-punkscript 1.1 + boot core 0.4.0 + moduledoc punkexe 0.5.0 (G-077); swept commandstack 0.7.0 + tcltestrun 0.4.1

make.tcl packages + vfscommonupdate outputs (punkcheck-managed):
- app-punkscript 1.0 -> 1.1 and punkboot/core.tcl 0.3.0 -> 0.4.0 (the G-077 -e one-liner surfaces)
- punk/args/moduledoc/punkexe 0.4.0 -> 0.5.0 (G-077 documented forms)
- swept promotions recorded as left-to-next-cycle by their commits: commandstack 0.6.0 -> 0.7.0 (git 31c0cd04) and punk/tcltestrun 0.3.1 -> 0.4.1 (git 977eade5)
kits punk902z/punksys/punk86 rebaked from this payload (bin/ outputs untracked)
Assisted-by: harness=opencode; primary-model=openrouter/moonshotai/kimi-k3; api-location=openrouter.ai
master
Julian Noble 7 days ago
parent
commit
c8fd165189
  1. 2
      src/vfs/_vfscommon.vfs/lib/app-punkscript/pkgIndex.tcl
  2. 39
      src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl
  3. 228
      src/vfs/_vfscommon.vfs/modules/commandstack-0.7.0.tm
  4. 160
      src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.5.0.tm
  5. 110
      src/vfs/_vfscommon.vfs/modules/punk/tcltestrun-0.4.1.tm
  6. 46
      src/vfs/_vfscommon.vfs/punkboot/core.tcl

2
src/vfs/_vfscommon.vfs/lib/app-punkscript/pkgIndex.tcl

@ -1 +1 @@
package ifneeded app-punkscript 1.0 [list source [file join $dir punkscript.tcl]]
package ifneeded app-punkscript 1.1 [list source [file join $dir punkscript.tcl]]

39
src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl

@ -1,9 +1,16 @@
package provide app-punkscript 1.0
package provide app-punkscript 1.1
#Lean one-shot script runner for the punk executable 'script' subcommand (goal G-015).
#
#Contract (G-015 - see goals/archive/G-015-script-subcommand-piped-stdin.md):
# - runs a script FILE (first argument, remaining arguments become the script's ::argv)
# or, with no arguments, the whole of piped/redirected stdin as the script.
# - G-077: a first argument of exactly '-e' runs the SECOND argument as a Tcl
# one-liner (remaining arguments become ::argv, ::argv0 is '-e') - the
# perl/python/node one-liner reflex in the punk script environment. Stock
# tclsh's -e misparse (argv-swallow + stdin read) is never reproduced: '-e'
# with no following script argument is a usage error, and this form never
# reads stdin itself (the one-liner may). A file literally named '-e' stays
# reachable as './-e'.
# - the script interp carries the default punk shell module/alias environment
# (package punk registers the deck aliases such as 'dev'; punk::aliascore
# provides the utility aliases) so one-liners like 'dev projects.work *x*'
@ -68,6 +75,36 @@ apply {{} {
catch {package require punk::args::moduledoc::tclcore}
set arglist $::argv
if {[llength $arglist] && [lindex $arglist 0] eq "-e"} {
#-e one-liner form (G-077): script -e <script> ?args...? - evaluate the
#given Tcl code with ::argv0 '-e' and the remaining arguments in ::argv.
#Works with or without piped stdin present: stdin is never read here,
#the one-liner may read it itself. '-e' with no following script
#argument is a usage error - never an interactive fall-through (and
#never stock tclsh's argv-swallow + stdin-read misparse).
if {[llength $arglist] < 2} {
puts stderr "punk script: -e requires a script argument"
puts stderr "usage: <punkexe> script -e <script> ?args...?"
puts stderr " or: <punkexe> -e <script> ?args...?"
exit 1
}
set onescript [lindex $arglist 1]
set ::argv0 "-e"
set ::argv [lrange $arglist 2 end]
set ::argc [llength $::argv]
set ::tcl_interactive 0
if {[catch {uplevel #0 $onescript} result]} {
puts stderr $::errorInfo
flush stderr
exit 1
}
#one-shot eval ergonomics, as for the stdin form: a non-empty final
#result is written to stdout (file-form scripts keep pure semantics).
if {[string length $result]} {
puts stdout $result
}
apply $finish 0
}
if {[llength $arglist]} {
#file form: scriptname ?args...?
#works with or without piped stdin present - the script may read stdin itself

228
src/vfs/_vfscommon.vfs/modules/commandstack-0.6.0.tm → src/vfs/_vfscommon.vfs/modules/commandstack-0.7.0.tm

@ -11,6 +11,16 @@
# - oo dispatch features may be a better implementation - especially for allowing undoing command renames in the middle of a stack.
# - document that replacement command should use 'commandstack::get_next_command <cmd> <renamer>' for delegating to command as it was prior to rename
#changes:
#2026-08-04 (G-160 follow-on)
# - remove_rename convenience forms (the long-parked code todos):
# pop_rename (pop a renamer's topmost entry - per command, or searched
# across live stacks when unambiguous), remove_renamer (remove ALL of a
# renamer's entries across every live stack - the unload-my-package form),
# and restore_original (unwind a command's whole stack to the original
# implementation regardless of renamer; not gated by known_renamers so it
# works after state loss such as a module re-source). All three route
# through remove_rename's re-linking/token-map machinery; stacks parked
# by Rename_stack are invisible to the renamer-wide forms.
#2026-08-03 (hygiene pass - G-160)
# - fix tokenid counter: was incremented on an apply-local variable so it was stuck at 1 -
# tokenids are now unique and monotonic per (renamer,command), so same-renamer re-renames
@ -201,6 +211,21 @@ namespace eval commandstack {
unique per (renamer, command), so any entry - not just the topmost -
is removable by its exact token.
Convenience removal forms
commandstack::pop_rename <renamer> ?command?
Pop the renamer's topmost entry and return the removed record -
on the given command, or searched across all live stacks when the
renamer's entries live on only one command (multi-command entries
are an ambiguity error).
commandstack::remove_renamer <renamer>
Remove every entry the renamer recorded, across all live stacks -
the unload-my-package form. Returns the removed records keyed by
command. Stacks parked by Rename_stack are left untouched.
commandstack::restore_original <command>
Unwind the command's whole stack regardless of renamer, restoring
the original implementation (a repair/reset operation - not gated
by known_renamers). Returns the number of records removed.
Inspection
commandstack::get_stack ?command? - rename records (or all stacks;
raw stacks-dict key tried first,
@ -653,11 +678,6 @@ namespace eval commandstack {
uplevel 2 [list $COMMANDSTACKNEXT {*}$args]
}
#todo - concept of 'pop' for renamer. Remove topmost entry specific to the renamer
#todo - removal by token to allow renamer to have multiple entries for one command but to remove one that is not the topmost
#todo - removal of all entries pertaining to a particular renamer
#todo - allow restore to bottom-most implementation (original) - regardless of what renamers have cooperated in the stack?
#remove by token, or by commandname if called from same context as original rename_command
#If only a commandname is supplied, and there were multiple renames from the same context (same -renamer) only the topmost is removed.
#A call to remove_rename with no token or renamer, and from a namespace context which didn't perform a rename will not remove anything.
@ -764,6 +784,202 @@ namespace eval commandstack {
return [list]
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::pop_rename
@cmd -name "commandstack::pop_rename" -&
-summary -&
"Pop a renamer's topmost rename-stack entry." -&
-help -&
{Removes the topmost (most recently stacked) rename entry
belonging to renamer and returns the removed record - the
LIFO undo for a package that renames as it loads and
unwinds as it unloads. With command given, the pop is
restricted to that command's stack (equivalent to
'remove_rename [list $command $renamer]'). Without command,
every live stack is searched: when the renamer's entries
all live on one command the pop happens there; entries
spread across multiple commands are ambiguous and raise an
error naming those commands (supply command, or use
commandstack::remove_renamer to remove all of them).
Stacks parked by Rename_stack are maintenance state and are
not searched. The renamer must be known to commandstack or
an error is raised. Returns the empty string when the
renamer has no matching entry.}
@values -min 1 -max 2
renamer -type string -help -&
"Renamer string recorded at rename time (must be known
to commandstack)."
command -type string -optional 1 -help -&
"Restrict the pop to this command's stack (resolved in
the caller's namespace context)."
}]
}
proc pop_rename {renamer {command ""}} {
variable all_stacks
variable known_renamers
variable debug
if {$renamer ni $known_renamers} {
error "(commandstack::pop_rename) ERROR: renamer $renamer not in list of known_renamers '$known_renamers'. Supply the renamer string recorded at rename time."
}
if {$command ne ""} {
set command [uplevel 1 [list namespace which $command]]
if {$command eq "" || ![dict exists $all_stacks $command]} {
return ""
}
set commands [list $command]
} else {
#find the live stacks holding entries for this renamer. Stacks parked
#by Rename_stack are skipped: their records keep the original token
#command, which never equals the parked key.
set commands [list]
dict for {key stack} $all_stacks {
if {![llength $stack]} {
continue
}
if {[lindex [dict get [lindex $stack 0] token] 0] ne $key} {
continue
}
if {[lsearch -index 3 $stack $renamer] > -1} {
lappend commands $key
}
}
if {[llength $commands] > 1} {
error "(commandstack::pop_rename) ERROR: renamer '$renamer' has entries on multiple commands ([join $commands {, }]) - supply the command argument, or use commandstack::remove_renamer to remove all of its entries"
}
if {![llength $commands]} {
return ""
}
}
set command [lindex $commands 0]
set stack [dict get $all_stacks $command]
set topmost [lindex [lsearch -all -index 3 $stack $renamer] end]
if {$topmost eq ""} {
return ""
}
set record [lindex $stack $topmost]
remove_rename [list $command $renamer]
if {$debug} {
puts stderr "(commandstack::pop_rename) popped [dict get $record token]"
}
return $record
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::remove_renamer
@cmd -name "commandstack::remove_renamer" -&
-summary -&
"Remove every rename-stack entry belonging to a renamer." -&
-help -&
{The unload-my-package convenience: removes ALL entries
recorded for renamer across every live command stack. Each
command's entries are popped topmost-first through the same
re-linking machinery as remove_rename, so other renamers'
overrides keep delegating correctly. Stacks parked by
Rename_stack are maintenance state and are left untouched.
The renamer must be known to commandstack or an error is
raised; the renamer is NOT removed from known_renamers.
Returns a dict keyed by command name whose values are the
removed records (topmost-first); an empty dict when the
renamer had no entries.}
@values -min 1 -max 1
renamer -type string -help -&
"Renamer string recorded at rename time (must be known
to commandstack)."
}]
}
proc remove_renamer {renamer} {
variable all_stacks
variable known_renamers
variable debug
if {$renamer ni $known_renamers} {
error "(commandstack::remove_renamer) ERROR: renamer $renamer not in list of known_renamers '$known_renamers'. Supply the renamer string recorded at rename time."
}
set removed [dict create]
#remove_rename mutates all_stacks only under the command key it is
#given, so iterating over this snapshot of the stacks dict is safe.
#Stacks parked by Rename_stack are skipped: their records keep the
#original token command, which never equals the parked key.
dict for {command stack} $all_stacks {
if {![llength $stack]} {
continue
}
if {[lindex [dict get [lindex $stack 0] token] 0] ne $command} {
continue
}
while {[set topmost [lindex [lsearch -all -index 3 [dict get $all_stacks $command] $renamer] end]] ne ""} {
set record [lindex [dict get $all_stacks $command] $topmost]
remove_rename [list $command $renamer]
dict lappend removed $command $record
if {$debug} {
puts stderr "(commandstack::remove_renamer) removed [dict get $record token]"
}
}
}
return $removed
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::restore_original
@cmd -name "commandstack::restore_original" -&
-summary -&
"Unwind a command's whole rename stack, restoring the original implementation." -&
-help -&
{Removes EVERY record on the command's live rename stack -
regardless of which renamers contributed them - and
restores the bottom-of-stack (original) implementation as
the live command. Records are removed topmost-first through
the same machinery as remove_rename.
This is the repair/reset operation: it is deliberately NOT
gated by known_renamers (unlike remove_rename) - renamers
evidenced by the stack records are registered into
known_renamers first, mirroring rename_command, so a
restore still works after state loss such as a module
re-source that reset known_renamers while stacks survived.
Returns the number of records removed: 0 when the command
has no live stack (never renamed, or only the empty residue
entry - prune that with Delete_stack).}
@values -min 1 -max 1
command -type string -help -&
"Command name (resolved with 'namespace which' in the
caller's context)."
}]
}
proc restore_original {command} {
variable all_stacks
variable known_renamers
variable debug
set command [uplevel 1 [list namespace which $command]]
if {$command eq "" || ![dict exists $all_stacks $command]} {
return 0
}
set stack [dict get $all_stacks $command]
if {![llength $stack]} {
return 0
}
#a repair operation must not be gated by known_renamers (which a module
#re-source may have reset while stacks survived) - register the renamers
#the stack evidences, mirroring rename_command
foreach record $stack {
set record_renamer [dict get $record renamer]
if {$record_renamer ni $known_renamers} {
lappend known_renamers $record_renamer
}
}
set removed_count 0
while {[llength $stack]} {
remove_rename [dict get [lindex $stack end] token]
set stack [dict get $all_stacks $command]
incr removed_count
}
if {$debug} {
puts stderr "(commandstack::restore_original) restored '$command' to its original implementation ($removed_count override(s) unwound)"
}
return $removed_count
}
namespace eval argdoc {
lappend PUNKARGS [list {
@id -id ::commandstack::show_stack
@ -1019,7 +1235,7 @@ namespace eval ::punk::args::register {
}
package provide commandstack [namespace eval commandstack {
set version 0.6.0
set version 0.7.0
}]

160
src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.4.0.tm → src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.5.0.tm

@ -8,7 +8,7 @@
# (C) 2026
#
# @@ Meta Begin
# Application punk::args::moduledoc::punkexe 0.4.0
# Application punk::args::moduledoc::punkexe 0.5.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
@ -18,7 +18,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::args::moduledoc::punkexe 0 0.4.0]
#[manpage_begin punkshell_module_punk::args::moduledoc::punkexe 0 0.5.0]
#[copyright "2026"]
#[titledesc {punk executable launch documentation}] [comment {-- Name section and table of contents description --}]
#[moddesc {-}] [comment {-- Description at end of page heading --}]
@ -84,10 +84,10 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe
@cmd -name punkexe\
-summary\
"punk executable launch: package modes and subcommand dispatch."\
-help\
@cmd -name punkexe -&
-summary -&
"punk executable launch: package modes and subcommand dispatch." -&
-help -&
"Launch surface of the punk shell executables ('<punkexe>' below
stands for any punk kit executable, e.g punk905, or a generated
project's kit).
@ -106,7 +106,9 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
The next argument selects a subcommand from the choices below.
When the first non-mode argument is not a known subcommand:
with no arguments at all the interactive punk shell is
launched ('shell'); a first argument naming an existing file
launched ('shell'); a first argument of exactly '-e' runs the
following argument as a Tcl one-liner (handled as for
'script -e' - G-077); a first argument naming an existing file
(or a lib:* scriptlib reference) is treated as a script
invocation (handled as for the 'script' subcommand); anything
else is refused with this usage on stderr, exit 1 (G-032 -
@ -129,7 +131,7 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
per-kit table is the authority for a given executable."
@form -synopsis "<punkexe> ?packagemode? ?subcommand? ?arg ...?"
@leaders -min 0 -max 2
packagemode -type string -optional 1 -default internal -help\
packagemode -type string -optional 1 -default internal -help -&
"Package-source mode: an ordered dash-separated list of path
blocks, optionally scoped with the 'proj:' prefix.
Blocks (each adds a group of module/library paths):
@ -155,7 +157,7 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
ties (faithful to the project's snapshot vintage)
The launch reports the detected project root and effective
precedence."
subcommand -type string -optional 1 -choicerestricted 0 -choices {tclsh script shell punk shellspy buildinfo help} -choicelabels {
subcommand -type string -optional 1 -choicerestricted 0 -choicecolumns 1 -choices {tclsh script shell punk shellspy buildinfo help} -choicelabels {
tclsh
" Run as a (near) stock tclsh - no punk modules loaded."
script
@ -170,11 +172,11 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
" Print the kit's build/identity stamp report and exit (G-025)."
help
" Show usage for the launch surface or one of its subcommands (G-032)."
} -help\
} -help -&
"Subcommand to run. Each subcommand's own contract is documented
under its definition id (script)::punkexe::<subcommand>."
@values -min 0 -max -1
arg -type any -optional 1 -multiple 1 -help\
arg -type any -optional 1 -multiple 1 -help -&
"Arguments for the selected subcommand."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
@ -184,14 +186,15 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
#early link), item 6 (piped whole-buffer eval + honest exit codes),
#item 8 (istty read-only by convention). The 'no -e one-liner' note is
#the G-077 boundary: stock parity INSIDE this subcommand is deliberate;
#one-liner affordances belong to the top-level/'script' surfaces (G-077).
#the one-liner affordance it points at ('-e' at the top level and as
#the script subcommand's first argument) landed with G-077 (0.5.0).
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::tclsh
@cmd -name "punkexe tclsh"\
-summary\
"Run the punk executable as a (near) stock tclsh - no punk modules."\
-help\
@cmd -name "punkexe tclsh" -&
-summary -&
"Run the punk executable as a (near) stock tclsh - no punk modules." -&
-help -&
"Run the executable as a plain tclsh: no punk modules are loaded
and behaviour matches stock 'tclsh' as closely as the kit boot
allows. The subcommand is an EXTENSION of standard tclsh
@ -205,8 +208,10 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
input comes from stdin. In particular there is no -e/-c
one-liner flag - '<punkexe> tclsh -e (script)' puts '-e' and the
script text into ::argv and reads stdin, exactly like stock
tclsh. That parity is deliberate; one-liner affordances belong
to the punk-level launch surfaces, not this subcommand.
tclsh. That parity is deliberate; the one-liner affordance
lives on the punk-level launch surfaces instead (G-077): use
'<punkexe> -e <script> ?arg ...?' or '<punkexe> script -e
<script> ?arg ...?', never this subcommand.
G-032 help carve-out (the single deliberate exception): a help
flag (-help/--help/-h//?) as the SOLE argument renders this
usage and exits 0; the same flag accompanied by ANY further
@ -267,28 +272,28 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
@form -form {scriptfile} -synopsis "<punkexe> tclsh ?-encoding name? scriptfile ?arg ...?"
@leaders -min 0 -max 0
@opts
-encoding -type string -optional 1 -typesynopsis name -help\
-encoding -type string -optional 1 -typesynopsis name -help -&
"Encoding used to read scriptfile (as for 'source -encoding').
Recognised only immediately after 'tclsh' and only when the
following argument does not begin with '-'; an incomplete
-encoding form falls through to the no-script form (stock
behaviour)."
Recognised only immediately after 'tclsh' and only when the
following argument does not begin with '-'; an incomplete
-encoding form falls through to the no-script form (stock
behaviour)."
@values -min 1 -max -1
scriptfile -type string -optional 0 -help\
scriptfile -type string -optional 0 -help -&
"Script file to source and exit. ::argv0 and 'info script' are
the (normalized) script path; remaining args are ::argv. The
script may set ::tclsh(dorepl) 1 (piperepl runtimes) to enter
the interactive repl after it completes."
arg -type any -optional 1 -multiple 1 -help\
the (normalized) script path; remaining args are ::argv. The
script may set ::tclsh(dorepl) 1 (piperepl runtimes) to enter
the interactive repl after it completes."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the script in ::argv."
@form -form {stdin} -synopsis "<punkexe> tclsh ?arg ...?"
@values -min 0 -max -1
arg -type any -optional 1 -multiple 1 -help\
arg -type any -optional 1 -multiple 1 -help -&
"With no script file all arguments (including any leading '-'
arguments) stay in ::argv; ::argv0 is the executable path.
Input comes from stdin: the interactive repl on a console
(piperepl runtimes), evaluation of piped input otherwise."
arguments) stay in ::argv; ::argv0 is the executable path.
Input comes from stdin: the interactive repl on a console
(piperepl runtimes), evaluation of piped input otherwise."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
@ -301,10 +306,10 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::script
@cmd -name "punkexe script"\
-summary\
"Run a script non-interactively in the punk script environment."\
-help\
@cmd -name "punkexe script" -&
-summary -&
"Run a script non-interactively in the punk script environment." -&
-help -&
"Run a Tcl script and exit with an honest exit code
(app-punkscript): script errors exit nonzero and the launch
plumbing emits nothing of its own on stdout/stderr, so
@ -318,24 +323,45 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
from the scriptlib locations associated with the executable,
including extensionless and shebang'd scripts).
A first argument of exactly '-e' runs the SECOND argument as a
Tcl one-liner (G-077): non-interactive, ::argv0 is '-e',
arguments after the script text land in ::argv, and a
non-empty final result is echoed to stdout (the same one-shot
eval ergonomics as the stdin form). '-e' with no following
script argument is a usage error - never an interactive
fall-through, and never stock tclsh's -e misparse (argv-swallow
plus stdin read). The form works with or without piped stdin
present; stdin is read only if the one-liner itself reads it.
A file literally named '-e' stays reachable as './-e'. The
same one-liner is available at the top level as
'<punkexe> -e <script> ?arg ...?'.
A help flag (-help/--help/-h//?) as the FIRST argument renders
this usage and exits 0 (G-032); in any later position it is an
ordinary script argument."
@form -synopsis "<punkexe> script ?scriptfile|lib:name? ?arg ...?"
@form -form {file} -synopsis "<punkexe> script ?scriptfile|lib:name? ?arg ...?"
@values -min 0 -max -1
scriptfile -type string -optional 1 -help\
scriptfile -type string -optional 1 -help -&
"Script file path, or lib:name for scriptlib resolution.
Omitted: the script is read from piped stdin."
arg -type any -optional 1 -multiple 1 -help\
Omitted: the script is read from piped stdin."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the script in ::argv."
@form -form {oneliner} -synopsis "<punkexe> script -e <script> ?arg ...?"
@values -min 1 -max -1
script -type string -optional 0 -help -&
"Tcl code to evaluate as a one-liner (G-077): errors print
errorInfo to stderr with exit 1; a non-empty final result
is echoed to stdout."
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed to the one-liner in ::argv (::argv0 is '-e')."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::shell
@cmd -name "punkexe shell"\
-summary\
"Interactive punk shell (repl)."\
-help\
@cmd -name "punkexe shell" -&
-summary -&
"Interactive punk shell (repl)." -&
-help -&
"Launch the interactive punk shell (app-punkshell). With
arguments, a script can be run first with the interactive
shell maintained afterwards. This is also the default when the
@ -344,16 +370,16 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
usage and exits 0 (G-032)."
@form -synopsis "<punkexe> shell ?arg ...?"
@values -min 0 -max -1
arg -type any -optional 1 -multiple 1 -help\
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed through to the shell launch."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::punk
@cmd -name "punkexe punk"\
-summary\
"punk shell launch; piped-stdin friendly."\
-help\
@cmd -name "punkexe punk" -&
-summary -&
"punk shell launch; piped-stdin friendly." -&
-help -&
"Launch the punk shell. With arguments the app-punkshell launch
path is used (as for 'shell'); without arguments the
interactive shell is launched directly. Commands piped into
@ -364,16 +390,16 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
usage and exits 0 (G-032)."
@form -synopsis "<punkexe> punk ?arg ...?"
@values -min 0 -max -1
arg -type any -optional 1 -multiple 1 -help\
arg -type any -optional 1 -multiple 1 -help -&
"Arguments passed through to the shell launch."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::shellspy
@cmd -name "punkexe shellspy"\
-summary\
"Experimental shellspy command-line processor."\
-help\
@cmd -name "punkexe shellspy" -&
-summary -&
"Experimental shellspy command-line processor." -&
-help -&
"Pass all arguments to the experimental shellspy command-line
processor (app-shellspy). Note: 'shellspy' is only this launch
subcommand - it is not the project name (the project is
@ -382,7 +408,7 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
usage and exits 0 (G-032)."
@form -synopsis "<punkexe> shellspy ?arg ...?"
@values -min 0 -max -1
arg -type any -optional 1 -multiple 1 -help\
arg -type any -optional 1 -multiple 1 -help -&
"Arguments for the shellspy processor."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
@ -395,10 +421,10 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::buildinfo
@cmd -name "punkexe buildinfo"\
-summary\
"Print the kit's build/identity report and exit (G-025)."\
-help\
@cmd -name "punkexe buildinfo" -&
-summary -&
"Print the kit's build/identity report and exit (G-025)." -&
-help -&
"Print a machine-parseable build/identity report on stdout and
exit 0 - no repl fallthrough, nothing else on stdout, so the
output is usable by exec-style and piped callers.
@ -438,10 +464,10 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
lappend PUNKARGS [list {
@id -id (script)::punkexe::help
@cmd -name "punkexe help"\
-summary\
"Show usage for the launch surface or one of its subcommands (G-032)."\
-help\
@cmd -name "punkexe help" -&
-summary -&
"Show usage for the launch surface or one of its subcommands (G-032)." -&
-help -&
"Render launch usage on stdout and exit 0 (G-032).
With no argument: the top-level table - the package modes and
@ -473,10 +499,10 @@ tcl::namespace::eval punk::args::moduledoc::punkexe {
actual subcommand set."
@form -synopsis "<punkexe> help ?subcommand?"
@values -min 0 -max 1
subject -type string -optional 1 -choices {tclsh script shell punk shellspy buildinfo help} -help\
subject -type string -optional 1 -choices {tclsh script shell punk shellspy buildinfo help} -help -&
"Subcommand to show usage for. In a kit the valid choices are
the live launch surface: the built-ins plus any
project-registered subcommands."
the live launch surface: the built-ins plus any
project-registered subcommands."
}]
# -- --- --- --- --- --- --- --- --- --- --- --- --- ---
}
@ -501,7 +527,7 @@ namespace eval ::punk::args::register {
package provide punk::args::moduledoc::punkexe [tcl::namespace::eval punk::args::moduledoc::punkexe {
variable pkg punk::args::moduledoc::punkexe
variable version
set version 0.4.0
set version 0.5.0
}]
return

110
src/vfs/_vfscommon.vfs/modules/punk/tcltestrun-0.3.1.tm → src/vfs/_vfscommon.vfs/modules/punk/tcltestrun-0.4.1.tm

@ -8,7 +8,7 @@
# (C) 2026
#
# @@ Meta Begin
# Application punk::tcltestrun 0.3.1
# Application punk::tcltestrun 0.4.1
# Meta platform tcl
# Meta license BSD
# @@ Meta End
@ -36,7 +36,12 @@ tcl::namespace::eval punk::tcltestrun {
"Parse a dict containing stderr and stdout keys into testresult summary data."\
-help\
"Parse a dict containing stderr and stdout keys into testresult summary data.
The dict is expected to be in the format returned by shellrun::runx from execution of a tcltest script"
The dict is expected to be in the format returned by shellrun::runx from execution of a tcltest script.
Intended for streams produced with tcltest -verbose {body pass skip start error line usec} (the
runtests.tcl configuration). Failing tests whose descriptions contain embedded newlines (multi-line
opening banners) are parsed with full fidelity when the stream carries -verbose start events; without
start events such banners are not recognised and parsing degrades to the historic single-line-only
behaviour."
@leaders
@opts
@values -min 1 -max 2
@ -81,12 +86,63 @@ tcl::namespace::eval punk::tcltestrun {
set test_case_pass [dict create]
#dict set test_case_pass microseconds 0
set test_case_time [dict create]
#most recently started test name, from "---- <name> start" lines (-verbose start).
#Anchor for multi-line opening-banner recognition (G-161): without start events the
#parser retains its historic single-line-banner-only behaviour.
set current_start_name ""
switch -- $what {
stdout {
foreach ln [split $chunk \n] {
set ln_trimright [string trimright $ln]
incr i
set fail_stage [dict get $test_case_fail stage]
if {$fail_stage eq "banner"} {
#G-161: accumulating the remainder of a multi-line opening banner.
#tcltest emits the opening banner as ONE puts of
# ==== <name> <description> FAILED
#and trims only the description's ENDS, so a description containing
#embedded newlines makes the banner span physical lines: the first line
#starts "==== <name> " (recognised by the anchored entry point below) and
#the final line always ends " FAILED" (the description's last line is
#non-whitespace-terminated, so the space before FAILED is guaranteed).
if {[string match "* FAILED" $ln_trimright]} {
#banner complete - same state as a single-line opener
dict append test_case_fail test_openingtext "\n[string range $ln_trimright 0 end-7]"
dict set test_case_fail stage open
dict unset test_case_fail banner_lines
dict append results out "<stdout><$pkg> $ln" \n
continue
} elseif {[string match "==== Contents of test case:*" $ln]} {
#terminator line missed (pathological description content) - treat the
#banner as complete and take the open->contents transition so the
#parser cannot wedge waiting for a line that already went past
dict set test_case_fail stage contents
dict unset test_case_fail banner_lines
dict append results out "<stdout><$pkg> $ln" \n
continue
} elseif {[string match "++++ * took *" $ln_trimright]
|| [string match "++++ * PASSED" $ln_trimright]
|| [string match "++++ * SKIPPED: *" $ln_trimright]
|| [string match "---- * start" $ln_trimright]
|| [string match "*:*Total*Passed*Skipped*Failed*" $ln]
|| [dict get $test_case_fail banner_lines] >= 100} {
#a recognisable event line (or an implausibly long banner) while
#accumulating: this was not a banner after all - abandon capture and
#reprocess the current line through normal handling below.
#Already-buffered lines were ordinary output; they remain in 'out'
#where they were appended as they streamed.
dict set test_case_fail stage ""
dict set test_case_fail test_openingtext ""
dict unset test_case_fail banner_lines
set fail_stage ""
#deliberate fall-through - no continue
} else {
dict append test_case_fail test_openingtext "\n$ln"
dict incr test_case_fail banner_lines
dict append results out "<stdout><$pkg> $ln" \n
continue
}
}
if {$fail_stage eq ""} {
#not within a test case failure section, so we can parse summary lines and other output normally.
@ -124,6 +180,17 @@ tcl::namespace::eval punk::tcltestrun {
continue
}
# ---- <testname_words> start (-verbose start; the runner always enables it)
# Track the most recently started test: the anchor that lets a multi-line
# opening banner be recognised by name prefix (G-161). The name may itself
# contain spaces and even the word "start", so anchor on the LAST " start".
if {[string match "---- * start" $ln_trimright]} {
set start_pos [string last " start" $ln_trimright]
set current_start_name [string range $ln_trimright 5 [expr {$start_pos - 1}]]
dict append results out "<stdout><$pkg> $ln" \n
continue
}
if {[string match "Tests ended at*" $ln]} {
#review - what outputs this?
#puts stdout "<stdout><$pkg> $ln"
@ -153,6 +220,19 @@ tcl::namespace::eval punk::tcltestrun {
dict set test_case_fail stage open
set line_inner [string range $ln 5 end-7] ;#trim leading ====<space> and trailing <space>FAILED
dict set test_case_fail test_openingtext $line_inner
} elseif {$current_start_name ne "" && [string match "==== *" $ln]
&& [string equal -length [string length "$current_start_name "] "$current_start_name " [string range $ln 5 end]]} {
#G-161: entry point to a MULTI-LINE opening banner - a line starting
#"==== <current test name> " that does not end " FAILED" is the first
#physical line of a banner whose description contains embedded newlines
#(single tcltest puts; see the banner stage above for the terminator).
#Anchoring on the current started test's name (exact prefix compare, no
#glob) means ordinary output cannot open banner capture, and streams
#without -verbose start events never take this branch - their parsing
#is unchanged.
dict set test_case_fail stage banner
dict set test_case_fail test_openingtext [string range $ln 5 end]
dict set test_case_fail banner_lines 1
}
dict append results out "<stdout><$pkg> $ln" \n
@ -212,10 +292,14 @@ tcl::namespace::eval punk::tcltestrun {
if {$fail_stage eq "error" && ![string match "==== * FAILED" $ln_trimright]} {
if {[string match "---- errorInfo: *" $ln]} {
dict append results out "<stdout><$pkg> $ln" \n
dict append test_case_fail errorinfo "[string range $ln 15 end]\n"
#"---- errorInfo: " is 16 chars (indices 0-15) - capture from index 16
#so the value carries no leading space (0.4.1; historically ranged from
#15 and every captured value led with the prefix's trailing space)
dict append test_case_fail errorinfo "[string range $ln 16 end]\n"
continue
} elseif {[string match "---- errorCode: *" $ln]} {
dict set test_case_fail errorcode "[string range $ln 15 end]"
#same 16-char prefix and index-16 capture as errorInfo above (0.4.1)
dict set test_case_fail errorcode "[string range $ln 16 end]"
dict append results out "<stdout><$pkg> $ln" \n
continue
} elseif {[string match "---- *" $ln]} {
@ -232,7 +316,20 @@ tcl::namespace::eval punk::tcltestrun {
# Result capture for FAILED (non-error) tests: grab "---- Result was:" and "---- Result should have been" blocks.
if {$fail_stage eq "contents"} {
if {[dict exists $test_case_fail result_stage] && [dict get $test_case_fail result_stage] ne ""} {
if {[string match "---- *" $ln] || [string match "==== * FAILED" $ln_trimright] || [string match "++++ *" $ln]} {
if {[string match "---- Result should have been*" $ln]} {
# Direct transition result_was -> result_expected. The two blocks are
# adjacent single puts, so this opener always arrives while the
# result_was capture is still ACTIVE. Before G-161 the line merely
# reset the active capture and the fall-through skipped the opener
# elseif below: result_expected was never captured (absent from every
# report) and the expected-value lines leaked into test_body.
dict set test_case_fail result_stage "result_expected"
if {![dict exists $test_case_fail result_expected]} {
dict set test_case_fail result_expected ""
}
dict append results out "<stdout><$pkg> $ln" \n
continue
} elseif {[string match "---- *" $ln] || [string match "==== * FAILED" $ln_trimright] || [string match "++++ *" $ln]} {
dict set test_case_fail result_stage ""
# fall through to normal handling below (do not continue)
} else {
@ -304,6 +401,7 @@ tcl::namespace::eval punk::tcltestrun {
dict unset test_case_fail result_was
dict unset test_case_fail result_expected
dict unset test_case_fail result_stage
dict unset test_case_fail banner_lines
dict append results out "<stdout><$pkg> $ln" \n
continue ;#skip to next line.
@ -543,7 +641,7 @@ namespace eval ::punk::args::register {
package provide punk::tcltestrun [tcl::namespace::eval punk::tcltestrun {
variable pkg punk::tcltestrun
variable version
set version 0.3.1
set version 0.4.1
}]
return

46
src/vfs/_vfscommon.vfs/punkboot/core.tcl

@ -11,7 +11,8 @@
#package_mode parsing (minted|os|internal|src with the proj: scope prefix,
#G-033), module-path and auto_path assembly, punk::libunknown, src-mode
##modpod registration, then the default subcommand dispatch
#(tclsh/shellspy/punk/shell/script/buildinfo/help) extended by the thin
#(tclsh/shellspy/punk/shell/script/buildinfo/help plus the G-077 top-level
#'-e' one-liner reclassification to 'script -e') extended by the thin
#main's declared project subcommands (G-031 registration model - see the
#dispatch section near the end of this file). The launch surface documents
#itself via punk::args (G-032): '<punkexe> help ?subcommand?', '-help' and
@ -51,7 +52,7 @@
namespace eval ::punkboot {
#boot-core identity (G-031; stamped into kits + reported by the G-025
#buildinfo surfaces)
variable core_version 0.3.0
variable core_version 0.4.0
}
if {![info exists ::punkboot::launch_args]} {
namespace eval ::punkboot [list variable launch_args $::argv]
@ -1703,6 +1704,7 @@ if {![info exists ::punkboot::launch_defaults]} {
set exebase [file rootname [file tail [info nameofexecutable]]]
set lines [list]
lappend lines "Usage: $exebase ?packagemode? ?subcommand? ?arg ...?"
lappend lines " $exebase ?packagemode? -e <script> ?arg ...? (one-liner - same as 'script -e', G-077)"
lappend lines " packagemode: ordered dash-separated list of internal|minted|os|src (default internal),"
lappend lines " optionally scoped with the 'proj:' prefix (e.g proj:internal-src)"
lappend lines " subcommands:"
@ -1793,6 +1795,7 @@ if {![info exists ::punkboot::launch_defaults]} {
puts stderr "known subcommands: [join [launcher_plain_choices] {, }]"
}
puts stderr "use '$exebase help' for the launch surface, or '$exebase script $word ?arg ...?' to force script interpretation"
puts stderr "for a one-liner use '$exebase -e <script> ?arg ...?' (equivalently '$exebase script -e <script> ?arg ...?')"
return 1
}
}
@ -1872,19 +1875,32 @@ if {![info exists ::punkboot::launch_defaults]} {
::punkboot::launcher_show_help
exit 0
}
set subcommand $default_unknownfirst
if {$subcommand eq "script"} {
#G-032 unknown-first-arg reclassification rule (recorded in
#goals/G-032-launcher-punkargs.md): reclassify to 'script'
#only when the argument plausibly names a script - an
#existing file path, or a lib:* scriptlib reference. Anything
#else is refused with usage on stderr (exit 1); when
#punk::args is unavailable the refusal degrades to the
#historic always-reclassify behaviour.
set unknown_word [lindex $subcommand_arglist 0]
if {!([file exists $unknown_word] || [string match -nocase lib:* $unknown_word])} {
if {[::punkboot::launcher_unknown_first_error $unknown_word]} {
exit 1
#G-077: a first argument of exactly '-e' is the one-liner form -
#reclassify to 'script' with the WHOLE arglist ('-e' included) as
#its arguments, so the script subcommand's own '-e' handling runs
#it. This sits ahead of the unknown-first-arg refusal below, which
#would otherwise reject '-e' as naming no existing file. Same
#carve-out shape as the help flags: only script-default kits
#intercept it - a tool-style kit keeps '-e' routed to its own
#processor, and '<punkexe> tclsh -e ...' keeps stock parity
#(the tclsh subcommand never reaches this path).
if {$subcommand eq "-e" && $default_unknownfirst eq "script"} {
set subcommand script
} else {
set subcommand $default_unknownfirst
if {$subcommand eq "script"} {
#G-032 unknown-first-arg reclassification rule (recorded in
#goals/G-032-launcher-punkargs.md): reclassify to 'script'
#only when the argument plausibly names a script - an
#existing file path, or a lib:* scriptlib reference. Anything
#else is refused with usage on stderr (exit 1); when
#punk::args is unavailable the refusal degrades to the
#historic always-reclassify behaviour.
set unknown_word [lindex $subcommand_arglist 0]
if {!([file exists $unknown_word] || [string match -nocase lib:* $unknown_word])} {
if {[::punkboot::launcher_unknown_first_error $unknown_word]} {
exit 1
}
}
}
}

Loading…
Cancel
Save