punkboot::utils 0.1.1: move vendorupdate dirty-checkout check into module
Move vendor_source_dirty_warnings out of make.tcl into punkboot::utils as
vcs_dirty_warnings (PUNKARGS-documented, caller-label param so the
bootsupport update path can share it per G-026). make.tcl now uses a
guarded lazy require and degrades to skipped-with-warning if the
bootsupport snapshot lacks the proc - a stale/broken snapshot must never
brick the make.tcl commands used to repair it.
Chicken-and-egg-safe ordering used and now documented in
src/bootsupport/AGENTS.md ("Moving make.tcl functionality into
punkboot::utils"): edit src module + bump buildversion, make.tcl modules,
make.tcl bootsupport, only then repoint make.tcl call sites. Boot-phase
code (path setup, punkboot::lib prompts, package-availability checks)
stays self-contained in make.tcl.
Bootsupport snapshot refresh from the sanctioned pipeline: punkboot::utils
0.1.0->0.1.1, shellrun 0.1.2->0.1.4 (punk::args require fix catch-up),
punk::console 0.7.1 content catch-up; layout mirrors via the
punkcheck-tracked sync steps.
New tests: src/tests/modules/punkboot/utils/testsuites/utils/vcsdirty.test
(git fixture: dirty/clean/dedupe/label/unversioned/missing) - 25/25 across
punkboot suites; vendorupdate verified end-to-end through the module path.
Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
@ -8,6 +8,7 @@ Entries are newest-first; one bullet per notable change. See the root `AGENTS.md
## [0.2.5] - 2026-07-06
- `make.tcl vendorupdate` now checks each vendor source project's fossil/git checkout state and warns (non-fatal, once per VCS root) when pulling from a dirty tree — vendored artifacts built from uncommitted source have no committed provenance. Enforcement policy (abort/override, bootsupport coverage) tracked by goal G-026.
- punkboot::utils 0.1.1: the dirty-checkout check lives in the module as `vcs_dirty_warnings` (make.tcl calls it via a guarded lazy require and degrades to skipped if the bootsupport snapshot lacks it); bootsupport snapshots refreshed (punkboot::utils 0.1.1, shellrun 0.1.4, punk::console 0.7.1 content catch-up).
@ -50,6 +50,19 @@ After changing any build-critical module (`punkcheck`, `punk::repo`, `punk::mix`
- Do not add compiled extensions or packages requiring dynamic loading to bootsupport.
- Check `src/bootsupport/modules/README.md` for the copy-as-module workflow.
### Moving make.tcl functionality into punkboot::utils
`punkboot::utils` (source of truth `src/modules/punkboot/utils-999999.0a1.0.tm`) exists to keep make.tcl from growing unwieldy. Only post-boot functionality may move there — make.tcl loads it lazily from the bootsupport snapshot after boot has set the module paths; anything used during early boot (path setup, `::punkboot::lib` prompts/classifiers, package-availability checks) must stay self-contained in make.tcl.
Chicken-and-egg-safe ordering when adding to punkboot::utils (make.tcl must never reference a proc its loaded snapshot lacks):
1. Edit `src/modules/punkboot/utils-999999.0a1.0.tm` and bump its buildversion.
2. `tclsh src/make.tcl modules` (build to root `modules/`).
3. `tclsh src/make.tcl bootsupport` (pull the new build into `src/bootsupport/modules/`).
4. Only then repoint make.tcl call sites to the new proc.
make.tcl call sites must use a guarded `package require punkboot::utils` and degrade gracefully (skip the feature with a warning) when the proc is unavailable — a stale or broken bootsupport snapshot must never brick the make.tcl commands used to repair it (`modules`, `bootsupport`).
## Verification
- `tclsh src/make.tcl packages` succeeds after any modification.
if {![dict exists $guard_conf -inputmode] && ![dict exists $guard_conf -mode]
&& [chan event $input readable] ne ""
&& ![catch {twapi::get_console_handle stdin}]} {
return -code error -errorcode [list PUNK CONSOLE QUERY HOSTAGE_COOKED_READ]\
"punk::console::get_ansi_response_payload refusing to emit query on '$input': tcl 8.6 console is in cooked (line) mode with an armed readable handler - the driver's parked cooked read would swallow the response until Enter (query issued from an idle line-mode prompt?). Use raw mode, or issue queries while the reader is disarmed (e.g. during repl command evaluation)."
}
}
#chunks from input that need to be handled by readers
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} elseif {$fchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot 1
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
#todo - investigate cause of punk86 run hanging sometimes. An 'after 500' before exit in the called script fixes the issue. punk87 doesn't seem to be affected.
lappend PUNKARGS [list {
@id -id ::shellrun::run
@cmd -name "shellrun::run"\
-summary "Run an external command, streaming stdout/stderr to the console in real-time"\
-help\
"Run an external command and return the shellfilter exitinfo dict.
stdout and stderr are written to the console as they arrive;
nothing is captured for the return value. The exitcode is
part of the returned dict — a non-zero exitcode is NOT raised
as a Tcl error, the caller must check it explicitly.
Use -tcl to run a Tcl script/command instead of an external
process; in that case the returned dict carries a 'result'
key (or error/errorCode/errorInfo on failure) instead of an
exitcode."
@leaders -min 0 -max 0
@opts
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-nonewline -type none -help\
"Accepted for compatibility with runout/runerr/runx. Has no
effect on run because run does not capture or return stdout."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc run {args} {
#set_last_run_display [list]
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set runoptslong [dict get $splitargs runoptslong]
#set cmdargs [dict get $splitargs cmdargs]
set argd [punk::args::parse $args withid ::shellrun::run]
lassign [dict values $argd] leaders opts values received
@ -206,11 +176,25 @@ namespace eval shellrun {
lappend PUNKARGS [list {
@id -id ::shellrun::runconsole
@cmd -name "shellrun::runconsole"\
-summary "Run a command via exec with stdout/stderr directed to the console, mimicking Tcl's unknown handler"\
-help\
"Resolve cmdname via auto_execok and exec it with stdout
and stderr redirected to the active console. The result is
returned via uplevel with the exec result/options dict,
mirroring the way Tcl's unknown proc handles external
commands.
Unlike run, this uses exec semantics — there is no
exitcode in the return value and stdout/stderr are not
captured separately."
@leaders -min 0 -max 0
@opts
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the command to resolve via auto_execok and run."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed to the resolved command."
}]
#run in the way tcl unknown does - but without regard to auto_noexec
proc runconsole {args} {
@ -256,16 +240,42 @@ namespace eval shellrun {
}
lappend PUNKARGS [list {
@id -id ::shellrun::runout
@cmd -name "shellrun::runout"\
-summary "Run an external command and return its stdout"\
-help\
"Run an external command, capture stdout and stderr, and
return stdout as the result. stderr and the exitcode are
emitted via repl telemetry chunks but are not part of the
return value.
By default no console output is produced while the command
runs; use -echo to tee stdout and stderr to the console in
real-time. Use -nonewline to strip a trailing newline from
the returned stdout.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return value."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from the returned
stdout."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runout {args} {
set argd [punk::args::parse $args withid ::shellrun::runout]
@ -284,11 +294,6 @@ namespace eval shellrun {
set runerr ""
set RST [a]
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set cmdargs [dict get $splitargs cmdargs]
#puts stdout "RUNOUT cmdargs: $cmdargs"
#todo add -data boolean and -data lastwrite to -settings with default being -data all
@ -422,16 +427,42 @@ namespace eval shellrun {
lappend PUNKARGS [list {
@id -id ::shellrun::runerr
@cmd -name "shellrun::runerr"\
-summary "Run an external command and return its stderr"\
-help\
"Run an external command, capture stdout and stderr, and
return stderr as the result. stdout and the exitcode are
emitted via repl telemetry chunks but are not part of the
return value.
By default no console output is produced while the command
runs; use -echo to tee stdout and stderr to the console in
real-time. Use -nonewline to strip a trailing newline from
the returned stderr.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return value."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from the returned
stderr."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runerr {args} {
set argd [punk::args::parse $args withid ::shellrun::runerr]
@ -449,10 +480,6 @@ namespace eval shellrun {
set runout ""
set runerr ""
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set cmdargs [dict get $splitargs cmdargs]
set callopts [dict create]
if {[dict exists $received "-tcl"]} {
dict set callopts -tclscript 1
@ -567,6 +594,43 @@ namespace eval shellrun {
}
lappend PUNKARGS [list {
@id -id ::shellrun::runx
@cmd -name "shellrun::runx"\
-summary "Run an external command and return a dict of stdout, stderr, and exitcode"\
-help\
"Run an external command, capture stdout and stderr, and
return a dict containing stdout, stderr, and exitcode (or
result/error/errorCode/errorInfo for -tcl calls).
No console output is produced unless -echo is given. Use
-nonewline to strip trailing newlines from captured stdout
and stderr before returning.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return dict."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from captured stdout
and stderr in the returned dict."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runx {args} {
#set_last_run_display [list]
variable runout
@ -574,11 +638,10 @@ namespace eval shellrun {
set runout ""
set runerr ""
set splitargs [get_run_opts $args]
set runopts [dict get $splitargs runopts]
set cmdargs [dict get $splitargs cmdargs]
set argd [punk::args::parse $args withid ::shellrun::runx]
lassign [dict values $argd] leaders opts values received
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} elseif {$fchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot 1
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
#0.1.1 - added vcs_dirty_warnings (dirty fossil/git checkout provenance warnings; used by make.tcl vendorupdate, shared with bootsupport update per goal G-026)
if {![dict exists $guard_conf -inputmode] && ![dict exists $guard_conf -mode]
&& [chan event $input readable] ne ""
&& ![catch {twapi::get_console_handle stdin}]} {
return -code error -errorcode [list PUNK CONSOLE QUERY HOSTAGE_COOKED_READ]\
"punk::console::get_ansi_response_payload refusing to emit query on '$input': tcl 8.6 console is in cooked (line) mode with an armed readable handler - the driver's parked cooked read would swallow the response until Enter (query issued from an idle line-mode prompt?). Use raw mode, or issue queries while the reader is disarmed (e.g. during repl command evaluation)."
}
}
#chunks from input that need to be handled by readers
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} elseif {$fchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot 1
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
#todo - investigate cause of punk86 run hanging sometimes. An 'after 500' before exit in the called script fixes the issue. punk87 doesn't seem to be affected.
lappend PUNKARGS [list {
@id -id ::shellrun::run
@cmd -name "shellrun::run"\
-summary "Run an external command, streaming stdout/stderr to the console in real-time"\
-help\
"Run an external command and return the shellfilter exitinfo dict.
stdout and stderr are written to the console as they arrive;
nothing is captured for the return value. The exitcode is
part of the returned dict — a non-zero exitcode is NOT raised
as a Tcl error, the caller must check it explicitly.
Use -tcl to run a Tcl script/command instead of an external
process; in that case the returned dict carries a 'result'
key (or error/errorCode/errorInfo on failure) instead of an
exitcode."
@leaders -min 0 -max 0
@opts
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-nonewline -type none -help\
"Accepted for compatibility with runout/runerr/runx. Has no
effect on run because run does not capture or return stdout."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc run {args} {
#set_last_run_display [list]
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set runoptslong [dict get $splitargs runoptslong]
#set cmdargs [dict get $splitargs cmdargs]
set argd [punk::args::parse $args withid ::shellrun::run]
lassign [dict values $argd] leaders opts values received
@ -206,11 +176,25 @@ namespace eval shellrun {
lappend PUNKARGS [list {
@id -id ::shellrun::runconsole
@cmd -name "shellrun::runconsole"\
-summary "Run a command via exec with stdout/stderr directed to the console, mimicking Tcl's unknown handler"\
-help\
"Resolve cmdname via auto_execok and exec it with stdout
and stderr redirected to the active console. The result is
returned via uplevel with the exec result/options dict,
mirroring the way Tcl's unknown proc handles external
commands.
Unlike run, this uses exec semantics — there is no
exitcode in the return value and stdout/stderr are not
captured separately."
@leaders -min 0 -max 0
@opts
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the command to resolve via auto_execok and run."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed to the resolved command."
}]
#run in the way tcl unknown does - but without regard to auto_noexec
proc runconsole {args} {
@ -256,16 +240,42 @@ namespace eval shellrun {
}
lappend PUNKARGS [list {
@id -id ::shellrun::runout
@cmd -name "shellrun::runout"\
-summary "Run an external command and return its stdout"\
-help\
"Run an external command, capture stdout and stderr, and
return stdout as the result. stderr and the exitcode are
emitted via repl telemetry chunks but are not part of the
return value.
By default no console output is produced while the command
runs; use -echo to tee stdout and stderr to the console in
real-time. Use -nonewline to strip a trailing newline from
the returned stdout.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return value."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from the returned
stdout."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runout {args} {
set argd [punk::args::parse $args withid ::shellrun::runout]
@ -284,11 +294,6 @@ namespace eval shellrun {
set runerr ""
set RST [a]
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set cmdargs [dict get $splitargs cmdargs]
#puts stdout "RUNOUT cmdargs: $cmdargs"
#todo add -data boolean and -data lastwrite to -settings with default being -data all
@ -422,16 +427,42 @@ namespace eval shellrun {
lappend PUNKARGS [list {
@id -id ::shellrun::runerr
@cmd -name "shellrun::runerr"\
-summary "Run an external command and return its stderr"\
-help\
"Run an external command, capture stdout and stderr, and
return stderr as the result. stdout and the exitcode are
emitted via repl telemetry chunks but are not part of the
return value.
By default no console output is produced while the command
runs; use -echo to tee stdout and stderr to the console in
real-time. Use -nonewline to strip a trailing newline from
the returned stderr.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return value."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from the returned
stderr."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runerr {args} {
set argd [punk::args::parse $args withid ::shellrun::runerr]
@ -449,10 +480,6 @@ namespace eval shellrun {
set runout ""
set runerr ""
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set cmdargs [dict get $splitargs cmdargs]
set callopts [dict create]
if {[dict exists $received "-tcl"]} {
dict set callopts -tclscript 1
@ -567,6 +594,43 @@ namespace eval shellrun {
}
lappend PUNKARGS [list {
@id -id ::shellrun::runx
@cmd -name "shellrun::runx"\
-summary "Run an external command and return a dict of stdout, stderr, and exitcode"\
-help\
"Run an external command, capture stdout and stderr, and
return a dict containing stdout, stderr, and exitcode (or
result/error/errorCode/errorInfo for -tcl calls).
No console output is produced unless -echo is given. Use
-nonewline to strip trailing newlines from captured stdout
and stderr before returning.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return dict."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from captured stdout
and stderr in the returned dict."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runx {args} {
#set_last_run_display [list]
variable runout
@ -574,11 +638,10 @@ namespace eval shellrun {
set runout ""
set runerr ""
set splitargs [get_run_opts $args]
set runopts [dict get $splitargs runopts]
set cmdargs [dict get $splitargs cmdargs]
set argd [punk::args::parse $args withid ::shellrun::runx]
lassign [dict values $argd] leaders opts values received
if {![dict exists $guard_conf -inputmode] && ![dict exists $guard_conf -mode]
&& [chan event $input readable] ne ""
&& ![catch {twapi::get_console_handle stdin}]} {
return -code error -errorcode [list PUNK CONSOLE QUERY HOSTAGE_COOKED_READ]\
"punk::console::get_ansi_response_payload refusing to emit query on '$input': tcl 8.6 console is in cooked (line) mode with an armed readable handler - the driver's parked cooked read would swallow the response until Enter (query issued from an idle line-mode prompt?). Use raw mode, or issue queries while the reader is disarmed (e.g. during repl command evaluation)."
}
}
#chunks from input that need to be handled by readers
break ;#report at the nearest VCS root(s); a dual git+fossil root is caught in one pass
}
set parent [file dirname $dir]
if {$parent eq $dir} { break }
set dir $parent
}
if {$fossilroot ne "" && ![dict exists $checkedroots fossil,$fossilroot] && [llength [auto_execok fossil]]} {
dict set checkedroots fossil,$fossilroot 1
#fossil changes must run from within the checkout
set original_cwd [pwd]
if {[catch {
cd $fossilroot
set fchanges [string trim [exec {*}[auto_execok fossil] changes]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine fossil state of source project at $fossilroot ($errM)"
} elseif {$fchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $fossilroot has uncommitted fossil changes ([llength [split $fchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
}
cd $original_cwd
}
if {$gitroot ne "" && ![dict exists $checkedroots git,$gitroot] && [llength [auto_execok git]]} {
dict set checkedroots git,$gitroot 1
if {[catch {
set gchanges [string trim [exec {*}[auto_execok git] -C $gitroot status --porcelain]]
} errM]} {
lappend warnings "WARNING: ${label}could not determine git state of source project at $gitroot ($errM)"
} elseif {$gchanges ne ""} {
lappend warnings "WARNING: ${label}source project at $gitroot has uncommitted git changes ([llength [split $gchanges \n]] file(s)) - artifacts built from a dirty tree have no committed provenance"
#todo - investigate cause of punk86 run hanging sometimes. An 'after 500' before exit in the called script fixes the issue. punk87 doesn't seem to be affected.
lappend PUNKARGS [list {
@id -id ::shellrun::run
@cmd -name "shellrun::run"\
-summary "Run an external command, streaming stdout/stderr to the console in real-time"\
-help\
"Run an external command and return the shellfilter exitinfo dict.
stdout and stderr are written to the console as they arrive;
nothing is captured for the return value. The exitcode is
part of the returned dict — a non-zero exitcode is NOT raised
as a Tcl error, the caller must check it explicitly.
Use -tcl to run a Tcl script/command instead of an external
process; in that case the returned dict carries a 'result'
key (or error/errorCode/errorInfo on failure) instead of an
exitcode."
@leaders -min 0 -max 0
@opts
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-nonewline -type none -help\
"Accepted for compatibility with runout/runerr/runx. Has no
effect on run because run does not capture or return stdout."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc run {args} {
#set_last_run_display [list]
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set runoptslong [dict get $splitargs runoptslong]
#set cmdargs [dict get $splitargs cmdargs]
set argd [punk::args::parse $args withid ::shellrun::run]
lassign [dict values $argd] leaders opts values received
@ -206,11 +176,25 @@ namespace eval shellrun {
lappend PUNKARGS [list {
@id -id ::shellrun::runconsole
@cmd -name "shellrun::runconsole"\
-summary "Run a command via exec with stdout/stderr directed to the console, mimicking Tcl's unknown handler"\
-help\
"Resolve cmdname via auto_execok and exec it with stdout
and stderr redirected to the active console. The result is
returned via uplevel with the exec result/options dict,
mirroring the way Tcl's unknown proc handles external
commands.
Unlike run, this uses exec semantics — there is no
exitcode in the return value and stdout/stderr are not
captured separately."
@leaders -min 0 -max 0
@opts
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the command to resolve via auto_execok and run."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed to the resolved command."
}]
#run in the way tcl unknown does - but without regard to auto_noexec
proc runconsole {args} {
@ -256,16 +240,42 @@ namespace eval shellrun {
}
lappend PUNKARGS [list {
@id -id ::shellrun::runout
@cmd -name "shellrun::runout"\
-summary "Run an external command and return its stdout"\
-help\
"Run an external command, capture stdout and stderr, and
return stdout as the result. stderr and the exitcode are
emitted via repl telemetry chunks but are not part of the
return value.
By default no console output is produced while the command
runs; use -echo to tee stdout and stderr to the console in
real-time. Use -nonewline to strip a trailing newline from
the returned stdout.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return value."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from the returned
stdout."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runout {args} {
set argd [punk::args::parse $args withid ::shellrun::runout]
@ -284,11 +294,6 @@ namespace eval shellrun {
set runerr ""
set RST [a]
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set cmdargs [dict get $splitargs cmdargs]
#puts stdout "RUNOUT cmdargs: $cmdargs"
#todo add -data boolean and -data lastwrite to -settings with default being -data all
@ -422,16 +427,42 @@ namespace eval shellrun {
lappend PUNKARGS [list {
@id -id ::shellrun::runerr
@cmd -name "shellrun::runerr"\
-summary "Run an external command and return its stderr"\
-help\
"Run an external command, capture stdout and stderr, and
return stderr as the result. stdout and the exitcode are
emitted via repl telemetry chunks but are not part of the
return value.
By default no console output is produced while the command
runs; use -echo to tee stdout and stderr to the console in
real-time. Use -nonewline to strip a trailing newline from
the returned stderr.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none
-nonewline -type none
-tcl -type none -default 0
-debug -type none -default 0
--timeout= -type integer
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return value."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from the returned
stderr."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string
cmdarg -type any -multiple 1 -optional 1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runerr {args} {
set argd [punk::args::parse $args withid ::shellrun::runerr]
@ -449,10 +480,6 @@ namespace eval shellrun {
set runout ""
set runerr ""
#set splitargs [get_run_opts $args]
#set runopts [dict get $splitargs runopts]
#set cmdargs [dict get $splitargs cmdargs]
set callopts [dict create]
if {[dict exists $received "-tcl"]} {
dict set callopts -tclscript 1
@ -567,6 +594,43 @@ namespace eval shellrun {
}
lappend PUNKARGS [list {
@id -id ::shellrun::runx
@cmd -name "shellrun::runx"\
-summary "Run an external command and return a dict of stdout, stderr, and exitcode"\
-help\
"Run an external command, capture stdout and stderr, and
return a dict containing stdout, stderr, and exitcode (or
result/error/errorCode/errorInfo for -tcl calls).
No console output is produced unless -echo is given. Use
-nonewline to strip trailing newlines from captured stdout
and stderr before returning.
Use -tcl to run a Tcl script/command instead of an external
process."
@leaders -min 0 -max 0
@opts
-echo -type none -help\
"Tee stdout and stderr to the console in real-time while still
capturing them for the return dict."
-nonewline -type none -help\
"Strip a trailing newline (\\r\\n or \\n) from captured stdout
and stderr in the returned dict."
-tcl -type none -default 0 -help\
"Interpret cmdname (and cmdarg) as a Tcl command/script rather
than an external process."
-debug -type none -default 0 -help\
"Enable debug diagnostics from shellfilter::run."
--timeout= -type integer -help\
"Timeout in milliseconds for the external process."
@values -min 1 -max -1
cmdname -type string -help\
"Name of the external command (or Tcl command when -tcl is
given)."
cmdarg -type any -multiple 1 -optional 1 -help\
"Additional arguments passed through to the command."
}]
proc runx {args} {
#set_last_run_display [list]
variable runout
@ -574,11 +638,10 @@ namespace eval shellrun {
set runout ""
set runerr ""
set splitargs [get_run_opts $args]
set runopts [dict get $splitargs runopts]
set cmdargs [dict get $splitargs cmdargs]
set argd [punk::args::parse $args withid ::shellrun::runx]
lassign [dict values $argd] leaders opts values received