Browse Source

G-015 achieved: punk executable script subcommand (app-punkscript) - 0.3.0

New lean app package src/lib/app-punkscript behind the (previously stub)
script subcommand in punk_main.tcl:

- `<commands> | <punkexe> script` runs piped stdin to EOF - no trailing
  exit needed; `<punkexe> script <file> ?args?` runs a file with
  conventional ::argv0/::argv (bare non-subcommand first arg still
  reclassifies to the file form). Script may itself read stdin in the
  file form.
- Default punk shell environment via shared definitions (package punk
  registers deck aliases e.g dev; punk::aliascore::init adds the utility
  aliases) - no local alias lists, no boilerplate in piped one-liners.
- Honest exit codes: 0 success, 1 on error with errorInfo on stderr,
  script''s own exit honoured. No shellfilter stacks/transforms, no
  logging side effects, no interactive fallback (terminal stdin with no
  scriptname is a usage error, never a blocking read or shell).
- Stdin form echoes the script''s final result when non-empty (one-shot
  eval ergonomics: return-value commands like `dev projects.work <glob>`
  emit their table without a puts wrapper); file form keeps pure script
  semantics.

Verified on both generations (punk902z/tcl9 from _build - bin deploy was
file-locked; punksys/tcl8.6 from bin): piped success/error/exit-code,
file form with args and stdin passthrough, missing-file error, motivating
one-liner `dev projects.work *tomlish*` emitting the project table with
exit 0, `src` mode combination (dev modules load), and `shell` subcommand
regression smoke. G-015 flipped to achieved 2026-07-07 per GOALS.md
rules; goal detail records the verification and the manual console
spot-check item. Project version 0.3.0 (new user-visible shell
capability - minor bump).

Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.com
master
Julian Noble 1 week ago
parent
commit
9316a2f17d
  1. 4
      CHANGELOG.md
  2. 2
      GOALS.md
  3. 2
      goals/G-015-script-subcommand-piped-stdin.md
  4. 2
      punkproject.toml
  5. 2
      src/lib/AGENTS.md
  6. 1
      src/lib/app-punkscript/pkgIndex.tcl
  7. 97
      src/lib/app-punkscript/punkscript.tcl
  8. 11
      src/vfs/_config/punk_main.tcl
  9. 1
      src/vfs/_vfscommon.vfs/lib/app-punkscript/pkgIndex.tcl
  10. 97
      src/vfs/_vfscommon.vfs/lib/app-punkscript/punkscript.tcl

4
CHANGELOG.md

@ -5,6 +5,10 @@ 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.3.0] - 2026-07-07
- `<punkexe> script` completed (goal G-015 achieved): runs a script file (`script <file> ?args?`, conventional ::argv0/::argv) or the whole of piped stdin (`<commands> | <punkexe> script` — no trailing `exit` needed) in the default punk shell module/alias environment, so one-liners like `dev projects.work *name*` work with no boilerplate. Honest exit codes (0 success, 1 error with errorInfo on stderr, script's own `exit` honoured); no shellfilter transforms or logging side effects; never falls into an interactive shell. Implemented as the lean `app-punkscript` package; the stdin form echoes the script's final result when non-empty. Verified on both Tcl generations (punk902z, punksys).
## [0.2.7] - 2026-07-07
- vendored tomlish 1.1.10: the library API now carries punk::args (PUNKARGS) documentation (from_toml, to_dict, from_dict, to_toml, update_tomlish_from_dict) — the G-014 tomlish precondition is met (documented upstream, re-vendored from a clean checkout); plus the explicit `tomlish::cmdline_main` entry point.

2
GOALS.md

@ -134,7 +134,7 @@ Detail: goals/G-014-punk-config-toml.md
Goal: ::punk::config loads stored configuration from toml files in the XDG-located config dir - parsed via the vendored tomlish module, never an ad-hoc parser - and consumers resolve settings with per-named-subshell overrides, so features like the G-013 debug-view startup defaults read declared user configuration instead of hardcoded fallbacks.
Acceptance: a setting declared in a toml file under the XDG-located config dir is visible through the punk::config API at repl startup, and with no config files present built-in defaults apply with no errors beyond the existing missing-dir notice; a named subshell resolves its own overriding value for a key also defined at the parent/default scope, and a subshell with no override inherits the outer value (proven with at least one real key); at least one shipped feature (the G-013 editbuf-view startup default is the natural first) reads its default through this path rather than a hardcoded value; all toml reading/writing in punk::config goes through the tomlish module, and the tomlish API procs punk::config consumes carry punk::args (PUNKARGS) documentation - added upstream in the tomlish project and re-vendored here before punk::config implementation proceeds.
### G-015 [active] Punk executable `script` subcommand: reliable non-interactive piped/script execution
### G-015 [achieved 2026-07-07] Punk executable `script` subcommand: reliable non-interactive piped/script execution
Scope: src/vfs/_config/punk_main.tcl, src/lib/app-punkshell/punkshell.tcl (script path or a leaner dedicated app package)
Detail: goals/G-015-script-subcommand-piped-stdin.md
Goal: `<punkexe> script [<scriptname>] [<args>...]` executes a script file or piped stdin content (scriptname optional when input is piped) in an interp preloaded with the basic punk modules and aliases a punk shell provides by default, and always terminates with the script's success/failure as its exit code - without the `shell` subcommand's shellfilter channel transforms/logging stacks and without ever dropping into an interactive shell - so agents can reliably make piped script calls to punk executables.

2
goals/G-015-script-subcommand-piped-stdin.md

@ -1,6 +1,6 @@
# G-015 Punk executable `script` subcommand: reliable non-interactive piped/script execution
Status: active (2026-07-07)
Status: achieved 2026-07-07 (implemented as src/lib/app-punkscript; verified on punk902z/tcl9 and punksys/tcl8.6 kits: piped success/error/exit-code cases, file form with args and stdin passthrough, and the motivating `dev projects.work *tomlish*` one-liner - table emitted, exit 0, no boilerplate. Stdin form additionally echoes the script's final result when non-empty, giving return-value commands one-liner ergonomics; file form keeps pure script semantics. Terminal-stdin-with-no-args yields a usage error by design - spot-check from a real console typed session remains a manual item.)
Scope: src/vfs/_config/punk_main.tcl, src/lib/app-punkshell/punkshell.tcl (script path or a leaner dedicated app package)
Acceptance: as in root GOALS.md index (canonical).

2
punkproject.toml

@ -1,3 +1,3 @@
[project]
name = "punkshell"
version = "0.2.7"
version = "0.3.0"

2
src/lib/AGENTS.md

@ -16,6 +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-shellspy/` — ShellSpy app entry (`app-shellspy`)
- `app_shell/` — Shell app helpers (`app_shell`)
- `app_shellrun/` — Shell run helpers (`app_shellrun`)
@ -37,6 +38,7 @@ Source of truth for all editable `pkgIndex.tcl`-based library packages. These ar
- `app-punk/` — Punk REPL app entry
- `app-punkshell/` — Punkshell app entry
- `app-punkscript/` — One-shot script runner (`script` subcommand)
- `app-shellspy/` — ShellSpy app entry
- `app_shell/` — Shell app helpers
- `app_shellrun/` — Shell run helpers

1
src/lib/app-punkscript/pkgIndex.tcl

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

97
src/lib/app-punkscript/punkscript.tcl

@ -0,0 +1,97 @@
package provide app-punkscript 1.0
#Lean one-shot script runner for the punk executable 'script' subcommand (goal G-015).
#
#Contract (G-015 - see goals/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.
# - 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*'
# need no package require boilerplate.
# - NO shellfilter channel stacks/transforms, no logging side effects, no repl,
# and no interactive fallback under any eof/error condition. This package is
# deliberately isolated from app-punkshell (share definitions via modules,
# never launcher control flow).
# - the launch plumbing emits nothing on stdout/stderr; only the script's own
# output (and, on error, the error report to stderr) appears.
# - exit codes: 0 on success, 1 on script error or usage error; a script's own
# explicit exit code is honoured.
#
#punk_main.tcl sets ::argv to the arguments after the 'script' subcommand
#before requiring this package (a bare non-subcommand first argument is also
#reclassified as 'script <arg...>').
apply {{} {
#stdin-is-a-terminal detection, self-contained (no punkboot dependency so
#this package can also host in plain tclsh contexts).
#Windows consoles expose -inputmode on stdin; unix terminals expose -mode.
set stdin_is_terminal [apply {{} {
if {[catch {chan configure stdin} conf]} {
return 0 ;#no usable stdin at all - treat as non-terminal
}
expr {[dict exists $conf -inputmode] || [dict exists $conf -mode]}
}}]
#default punk shell environment via shared definitions - no local alias lists
package require punk
package require punk::aliascore
punk::aliascore::init
set arglist $::argv
if {[llength $arglist]} {
#file form: scriptname ?args...?
#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 {![file exists $scriptpath]} {
puts stderr "punk script: script file not found: '$scriptpath'"
exit 1
}
set ::argv0 $scriptpath
set ::argv $scriptargs
set ::argc [llength $scriptargs]
set ::tcl_interactive 0
info script $scriptpath
if {[catch {uplevel #0 [list source $scriptpath]}]} {
puts stderr $::errorInfo
flush stderr
exit 1
}
flush stdout
flush stderr
exit 0
} else {
#stdin form: read the whole of piped/redirected stdin as the script.
#An interactive terminal with no scriptname is a usage error - never
#block on a console read and never fall through to a shell.
if {$stdin_is_terminal} {
puts stderr "punk script: no script file specified and stdin is a terminal"
puts stderr "usage: <punkexe> script <scriptfile> ?args...?"
puts stderr " or: <commands> | <punkexe> script"
exit 1
}
set script [read stdin]
set ::argv0 "-"
set ::argv {}
set ::argc 0
set ::tcl_interactive 0
if {[catch {uplevel #0 $script} result]} {
puts stderr $::errorInfo
flush stderr
exit 1
}
#one-shot eval ergonomics for the stdin form only: the script's value
#(its final command's result) is written to stdout when non-empty, so
#one-liners whose commands RETURN their output (e.g 'dev projects.work
#<glob>' - displayed by the repl's result display in a shell) emit it
#here too without needing a puts wrapper. File-form scripts keep pure
#script semantics (results discarded).
if {[string length $result]} {
puts stdout $result
}
flush stdout
flush stderr
exit 0
}
}}

11
src/vfs/_config/punk_main.tcl

@ -1161,11 +1161,12 @@ apply { args {
}
}
script {
#run a script and exit
puts stderr "main.tcl launching script with args: $subcommand_arglist"
puts stderr "tcl_interactive: $::tcl_interactive"
set ::tcl_interactive [punkboot::is_interactive]
puts stderr "tcl_interactive: $::tcl_interactive"
#run a script (file argument, or piped stdin when no argument) and exit - goal G-015
#lean dedicated app package: default punk shell module/alias environment,
#no shellfilter stacks/transforms, no interactive fallback, honest exit codes.
#The launch plumbing must emit nothing on stdout/stderr (exec-style callers).
set ::tcl_interactive 0
package require app-punkscript
}
shell {
#app-punkshell supports running a script and maintaining an interactive shell afterwards

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

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

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

@ -0,0 +1,97 @@
package provide app-punkscript 1.0
#Lean one-shot script runner for the punk executable 'script' subcommand (goal G-015).
#
#Contract (G-015 - see goals/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.
# - 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*'
# need no package require boilerplate.
# - NO shellfilter channel stacks/transforms, no logging side effects, no repl,
# and no interactive fallback under any eof/error condition. This package is
# deliberately isolated from app-punkshell (share definitions via modules,
# never launcher control flow).
# - the launch plumbing emits nothing on stdout/stderr; only the script's own
# output (and, on error, the error report to stderr) appears.
# - exit codes: 0 on success, 1 on script error or usage error; a script's own
# explicit exit code is honoured.
#
#punk_main.tcl sets ::argv to the arguments after the 'script' subcommand
#before requiring this package (a bare non-subcommand first argument is also
#reclassified as 'script <arg...>').
apply {{} {
#stdin-is-a-terminal detection, self-contained (no punkboot dependency so
#this package can also host in plain tclsh contexts).
#Windows consoles expose -inputmode on stdin; unix terminals expose -mode.
set stdin_is_terminal [apply {{} {
if {[catch {chan configure stdin} conf]} {
return 0 ;#no usable stdin at all - treat as non-terminal
}
expr {[dict exists $conf -inputmode] || [dict exists $conf -mode]}
}}]
#default punk shell environment via shared definitions - no local alias lists
package require punk
package require punk::aliascore
punk::aliascore::init
set arglist $::argv
if {[llength $arglist]} {
#file form: scriptname ?args...?
#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 {![file exists $scriptpath]} {
puts stderr "punk script: script file not found: '$scriptpath'"
exit 1
}
set ::argv0 $scriptpath
set ::argv $scriptargs
set ::argc [llength $scriptargs]
set ::tcl_interactive 0
info script $scriptpath
if {[catch {uplevel #0 [list source $scriptpath]}]} {
puts stderr $::errorInfo
flush stderr
exit 1
}
flush stdout
flush stderr
exit 0
} else {
#stdin form: read the whole of piped/redirected stdin as the script.
#An interactive terminal with no scriptname is a usage error - never
#block on a console read and never fall through to a shell.
if {$stdin_is_terminal} {
puts stderr "punk script: no script file specified and stdin is a terminal"
puts stderr "usage: <punkexe> script <scriptfile> ?args...?"
puts stderr " or: <commands> | <punkexe> script"
exit 1
}
set script [read stdin]
set ::argv0 "-"
set ::argv {}
set ::argc 0
set ::tcl_interactive 0
if {[catch {uplevel #0 $script} result]} {
puts stderr $::errorInfo
flush stderr
exit 1
}
#one-shot eval ergonomics for the stdin form only: the script's value
#(its final command's result) is written to stdout when non-empty, so
#one-liners whose commands RETURN their output (e.g 'dev projects.work
#<glob>' - displayed by the repl's result display in a shell) emit it
#here too without needing a puts wrapper. File-form scripts keep pure
#script semantics (results discarded).
if {[string length $result]} {
puts stdout $result
}
flush stdout
flush stderr
exit 0
}
}}
Loading…
Cancel
Save