From b8371e058743ac9e14813cbd33524c9050725b09 Mon Sep 17 00:00:00 2001 From: Julian Noble Date: Tue, 7 Jul 2026 18:00:35 +1000 Subject: [PATCH] script subcommand: lib: scriptlib support; punk::path 0.2.2 - 0.4.0 app-punkscript accepts lib: (with or without .tcl - extensionless appends it) as the shell subcommand does: `punkexe script lib:hello` or bare `punkexe lib:hello` via reclassification. The prefix always wins; a literal path beginning lib: (pathological - colon is illegal in Windows filenames) is reachable via ./lib:... Not-found errors list every searched location; only .tcl runs via the script subcommand. Resolution policy is factored into punk::path::scriptlib_resolve (PUNKARGS-documented) rather than copied inline: kit-internal app/scriptlib first and deliberately not externally overridable, then scriptlib dirs relative to the executable - the same policy app-punkshell encodes inline (todo noted: refactor the shell path onto the shared proc). Share definitions via modules, not launcher control flow. Verified on both generations (punk902z, punksys): lib:hello extensionless and explicit, bare-arg reclassification, not-found candidate listing (bin/scriptlib then /scriptlib for a bin/ exe). Project version 0.4.0 (backward-compatible behaviour addition). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com --- CHANGELOG.md | 5 + punkproject.toml | 2 +- scriptlib/tktimer.tcl | 38 ++++++++ src/lib/AGENTS.md | 2 +- src/lib/app-punkscript/punkscript.tcl | 31 +++++- src/modules/punk/path-999999.0a1.0.tm | 88 +++++++++++++++++ src/modules/punk/path-buildversion.txt | 6 +- .../lib/app-punkscript/punkscript.tcl | 31 +++++- .../punk/{path-0.2.1.tm => path-0.2.2.tm} | 94 ++++++++++++++++++- 9 files changed, 286 insertions(+), 11 deletions(-) create mode 100644 scriptlib/tktimer.tcl rename src/vfs/_vfscommon.vfs/modules/punk/{path-0.2.1.tm => path-0.2.2.tm} (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97d06f8c..c694b9e8 100644 --- a/CHANGELOG.md +++ b/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.4.0] - 2026-07-07 + +- `script` subcommand supports `lib:` scriptlib scripts (with or without `.tcl` extension), matching the `shell` subcommand's scheme: `punkexe script lib:hello`, or bare `punkexe lib:hello` via reclassification. The prefix always wins — a literal path beginning `lib:` (pathological; illegal on Windows filesystems) is reachable via `./lib:...`. Not-found errors list the searched locations. Only `.tcl` runs via the script subcommand. +- punk::path 0.2.2: new `scriptlib_resolve` — the shared `lib:` resolution policy (kit-internal `app/scriptlib` first and not externally overridable, then scriptlib dirs relative to the executable), PUNKARGS-documented. app-punkshell still carries its inline copy of the policy (refactor onto the shared proc noted as todo). + ## [0.3.1] - 2026-07-07 - `script` subcommand: core-command documentation parity with the interactive shell — app-punkscript now loads `punk::args::moduledoc::tclcore` (catch-guarded, ~40ms) so e.g. `'i list' | punkexe script` renders the doc table instead of "Undocumented command". Verified on both generations; kit startup ~0.33s. diff --git a/punkproject.toml b/punkproject.toml index f2853bf9..949c835c 100644 --- a/punkproject.toml +++ b/punkproject.toml @@ -1,3 +1,3 @@ [project] name = "punkshell" -version = "0.3.1" +version = "0.4.0" diff --git a/scriptlib/tktimer.tcl b/scriptlib/tktimer.tcl new file mode 100644 index 00000000..dd89d289 --- /dev/null +++ b/scriptlib/tktimer.tcl @@ -0,0 +1,38 @@ +# timer.tcl - Tk countdown timer +# Usage: timer.tcl +# Creates a Tk window displaying the remaining seconds, counts down to zero, +# then exits with code 0. + +if {[catch {package require tk}]} { + package require Tk +} + +if {$::argc < 1} { + puts stderr "Usage: [file tail [info script]] " + exit 1 +} + +set seconds [lindex $::argv 0] +if {![string is integer -strict $seconds] || $seconds < 0} { + puts stderr "timer: seconds must be a non-negative integer (got '$seconds')" + exit 1 +} + +# Track the absolute end time so drift in `after` scheduling does not skew the count. +set ::timer_end [expr {[clock milliseconds] + $seconds * 1000}] + +proc timer_tick {} { + set remaining [expr {$::timer_end - [clock milliseconds]}] + if {$remaining <= 0} { + .clock configure -text 0 + exit 0 + } + .clock configure -text [expr {int(($remaining + 999) / 1000)}] + after 100 timer_tick +} + +label .clock -text $seconds -font {TkTextFont 48} -padx 40 -pady 30 +pack .clock -expand 1 -anchor center + +wm title . "Timer: $seconds s" +after 100 timer_tick diff --git a/src/lib/AGENTS.md b/src/lib/AGENTS.md index 75dcf927..da4f65a2 100644 --- a/src/lib/AGENTS.md +++ b/src/lib/AGENTS.md @@ -16,7 +16,7 @@ Source of truth for all editable `pkgIndex.tcl`-based library packages. These ar - Libraries here include: - `app-punk/` — Punk REPL app entry (`app-punk::repl`) - `app-punkshell/` — Punkshell app entry (`app-punkshell`); on eof of its input channel the `env(PUNK_PIPE_EOF)` policy (`exit`|`interactive`|unset=console heuristic) decides between terminating and reopening the console for an interactive repl — automated callers should set `exit` or end piped input with an explicit `exit` - - `app-punkscript/` — lean one-shot script runner behind the punk executable `script` subcommand (G-015): runs a file or piped stdin in the default punk shell module/alias environment with honest exit codes; no shellfilter stacks or logging side effects, never falls into an interactive shell — the reliable path for automated/agent callers (prefer this over piping into `shell`); stdin form echoes the script's final result when non-empty + - `app-punkscript/` — lean one-shot script runner behind the punk executable `script` subcommand (G-015): runs a file, a `lib:` scriptlib script (resolved via `punk::path::scriptlib_resolve`), or piped stdin in the default punk shell module/alias environment with honest exit codes; no shellfilter stacks or logging side effects, never falls into an interactive shell — the reliable path for automated/agent callers (prefer this over piping into `shell`); stdin form echoes the script's final result when non-empty - `app-shellspy/` — ShellSpy app entry (`app-shellspy`) - `app_shell/` — Shell app helpers (`app_shell`) - `app_shellrun/` — Shell run helpers (`app_shellrun`) diff --git a/src/lib/app-punkscript/punkscript.tcl b/src/lib/app-punkscript/punkscript.tcl index a76c3ab2..ac7aea04 100644 --- a/src/lib/app-punkscript/punkscript.tcl +++ b/src/lib/app-punkscript/punkscript.tcl @@ -50,7 +50,36 @@ apply {{} { #works with or without piped stdin present - the script may read stdin itself set scriptname [lindex $arglist 0] set scriptargs [lrange $arglist 1 end] - set scriptpath [file normalize $scriptname] + if {[string match -nocase lib:* $scriptname]} { + #scriptlib script: lib: (one or more colons after lib), with or + #without .tcl extension. The prefix always wins - a literal relative + #path beginning 'lib:' (pathological; illegal on Windows filesystems + #anyway) can be reached with ./lib:... or an absolute path. + #Resolution policy is shared with the shell subcommand's scheme via + #punk::path::scriptlib_resolve (vfs-internal scriptlib wins and is + #not externally overridable, then scriptlib dirs relative to the exe). + set libname [string trimleft [string range $scriptname [string first : $scriptname]+1 end] :] + package require punk::path + if {[catch {punk::path::scriptlib_resolve $libname} resolved]} { + puts stderr "punk script: $resolved" + exit 1 + } + set scriptpath [dict get $resolved path] + if {$scriptpath eq ""} { + puts stderr "punk script: scriptlib script '[dict get $resolved scriptname]' not found. Locations checked:" + foreach c [dict get $resolved candidates] { + puts stderr " $c" + } + exit 1 + } + if {[string tolower [file extension $scriptpath]] ne ".tcl"} { + puts stderr "punk script: only .tcl scripts are supported via lib: in the script subcommand (resolved: $scriptpath)" + exit 1 + } + set scriptpath [file normalize $scriptpath] + } else { + set scriptpath [file normalize $scriptname] + } if {![file exists $scriptpath]} { puts stderr "punk script: script file not found: '$scriptpath'" exit 1 diff --git a/src/modules/punk/path-999999.0a1.0.tm b/src/modules/punk/path-999999.0a1.0.tm index 67ea41c3..1e5e26b1 100644 --- a/src/modules/punk/path-999999.0a1.0.tm +++ b/src/modules/punk/path-999999.0a1.0.tm @@ -485,6 +485,87 @@ namespace eval punk::path { return relative } + namespace eval argdoc { + lappend PUNKARGS [list { + @id -id ::punk::path::scriptlib_resolve + @cmd -name "punk::path::scriptlib_resolve"\ + -summary\ + "Resolve a scriptlib script name to a path using the punk kit search policy"\ + -help\ + "Resolves a relative script name (the part after a 'lib:' prefix + in punk executable launch arguments) against the scriptlib + locations, in the same order the punk shell uses: + + 1. the kit vfs internal scriptlib (/app/scriptlib, + or /scriptlib for an unwrapped kit directory). + Internal scripts are deliberately NOT overridable by + external files (integrity and performance). + 2. /scriptlib - e.g /scriptlib for + an executable in /bin. + 3. /scriptlib. + + If the name's extension (lowercased) is not one of the known + scriptlib extensions (.tcl .py .pl .ps1 .sh), '.tcl' is + appended - only .tcl scripts may be named extensionlessly. + + Returns a dict with keys: + path - resolved absolute path, or empty string if not found + scriptname - the name searched for (extension applied) + candidates - the locations that were (or would be) checked, + for use in error messages. + Raises an error if name is not a relative path." + @leaders + name -type string -optional 0 -help\ + "relative script name, with or without a known extension + (any leading lib: prefix should already be stripped)" + }] + } + proc scriptlib_resolve {name} { + #shared resolution policy for the 'lib:' script prefix - used by the punk + #executable 'script' subcommand (app-punkscript); app-punkshell's shell + #subcommand encodes the same policy (todo: refactor it onto this proc). + if {[lindex [pathtype $name] 0] ne "relative"} { + error "punk::path::scriptlib_resolve name must be a relative path (got '$name')" + } + set known_extensions [list .tcl .py .pl .ps1 .sh] + if {[string tolower [file extension $name]] ni $known_extensions} { + #only .tcl scripts allowed to be called extensionlessly + set scriptname $name.tcl + } else { + set scriptname $name + } + set exepath [file dirname [file normalize [file join [info nameofexecutable] ___]]] ;#symlink resolve + set candidates [list] + #1) kit-internal scriptlib - not overridable externally + set has_zipfs_command [expr {[info commands ::tcl::zipfs::root] ne ""}] + set kit_base "" + if {$has_zipfs_command && [file exists [tcl::zipfs::root]]} { + set kit_base [tcl::zipfs::root] + } elseif {[file type $exepath] eq "directory"} { + set kit_base $exepath + } + set kit_libdir "" + if {$has_zipfs_command && $kit_base ne "" && [file exists $kit_base/app/scriptlib]} { + set kit_libdir $kit_base/app/scriptlib + } elseif {[file exists $exepath/scriptlib]} { + set kit_libdir $exepath/scriptlib + } + if {$kit_libdir ne ""} { + lappend candidates $kit_libdir/$scriptname + if {[file exists $kit_libdir/$scriptname]} { + return [dict create path $kit_libdir/$scriptname scriptname $scriptname candidates $candidates] + } + } + #2,3) external scriptlib folders relative to the executable location + set exedir [file dirname $exepath] + foreach dir [list [file join $exedir scriptlib] [file join [file dirname $exedir] scriptlib]] { + lappend candidates [file join $dir $scriptname] + if {[file isdirectory $dir] && [file exists [file join $dir $scriptname]]} { + return [dict create path [file join $dir $scriptname] scriptname $scriptname candidates $candidates] + } + } + return [dict create path "" scriptname $scriptname candidates $candidates] + } proc plain {str} { set str [string map "\\\\ /" $str] @@ -1598,6 +1679,13 @@ namespace eval punk::path::system { } + +namespace eval ::punk::args::register { + #use fully qualified so 8.6 doesn't find existing var in global namespace + #documentation-only PUNKARGS registration - discovered lazily if/when punk::args loads + lappend ::punk::args::register::NAMESPACES ::punk::path::argdoc +} + # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ ## Ready package provide punk::path [namespace eval punk::path { diff --git a/src/modules/punk/path-buildversion.txt b/src/modules/punk/path-buildversion.txt index f3c73491..70bd4b55 100644 --- a/src/modules/punk/path-buildversion.txt +++ b/src/modules/punk/path-buildversion.txt @@ -1,6 +1,4 @@ -0.2.1 +0.2.2 #First line must be a semantic version number #all other lines are ignored. -#0.2.1 - treefilenames: filter '.' and '..' by tail in the hidden-directory glob so recursion does not loop on non-Windows platforms -#0.2.0 - treefilenames now matches hidden files and recurses into hidden directories on all platforms -#0.2.0 - added PUNKARGS documentation for punk::path::relative +#0.2.2 - added scriptlib_resolve: shared resolution policy for the lib: