diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/project_main.tcl b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/project_main.tcl index a8b8ffe6..eb437b45 100644 --- a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/project_main.tcl +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/project_main.tcl @@ -15,7 +15,9 @@ # detection, package modes (internal/minted/src/os + proj: - see the # punk::args::moduledoc::punkexe package for the documented launch surface), # module and library path setup, punk::libunknown, and the default dispatch -# (tclsh/shellspy/punk/shell/script). +# (tclsh/shellspy/punk/shell/script/buildinfo/help) with its G-032 help +# surface (' help', '-help', ' -help' - tabled usage +# listing your registered subcommands, degrading to a plain list). # #WHAT TO EDIT # Only the ==CUSTOMIZE== blocks below. Everything else is contract plumbing. @@ -44,8 +46,9 @@ apply { args { #A dict mapping subcommand name -> Tcl handler script. The handler runs #after boot with ::argv/::argc already holding the subcommand's arguments; #the typical handler is a one-liner requiring your app package. Built-in - #subcommands (tclsh shellspy punk shell script) come from the boot core - #and cannot be shadowed - a colliding name is reported and ignored. + #subcommands (tclsh shellspy punk shell script buildinfo help) come from + #the boot core and cannot be shadowed - a colliding name is reported and + #ignored. # #Example - a kit for an app 'mytool' with a maintenance subcommand: # namespace eval ::punkboot {variable project_subcommands [dict create \ @@ -54,6 +57,21 @@ apply { args { # ]} namespace eval ::punkboot {variable project_subcommands [dict create]} + #Optional per-name help/parse metadata (G-032): with this declared, your + #subcommands appear in ' help' with summaries, 'help ' / + #' -help' render your punk::args definition, and parse 1 validates + #arguments through it before your handler runs (usage errors tabled, + #exit 1). All keys optional; a name without an entry keeps full + #passthrough semantics. Contract detail: punk::args::moduledoc::punkboot + #registration definition. + # namespace eval ::punkboot {variable project_subcommand_info [dict create \ + # mytool {summary "Frob the widgets in a directory." \ + # argsid (script)::mykit::mytool \ + # package punk::args::moduledoc::mykit \ + # parse 0} \ + # ]} + namespace eval ::punkboot {variable project_subcommand_info [dict create]} + # ==CUSTOMIZE== 2. launch defaults -------------------------------------- #A dict with optional keys deciding what a launch without an explicit #subcommand does: diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punk_main.tcl b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punk_main.tcl index eb4543b4..53b054c2 100644 --- a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punk_main.tcl +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punk_main.tcl @@ -18,11 +18,17 @@ apply { args { #dict: subcommand name -> Tcl handler script. The handler runs with #::argv/::argc already holding the subcommand's arguments; the typical #handler is just 'package require app-yourapp'. Built-in subcommands - #(tclsh shellspy punk shell script) come from the boot core and cannot be - #shadowed. punkshell itself declares none. + #(tclsh shellspy punk shell script buildinfo help) come from the boot + #core and cannot be shadowed. punkshell itself declares none. #Example: # namespace eval ::punkboot {variable project_subcommands [dict create \ # mytool {package require app-mytool}]} + #Optional help/parse metadata per declared name (G-032 - drives the + #launcher 'help' tables and argument gates; all keys optional, see + #punk::args::moduledoc::punkboot's registration definition): + # namespace eval ::punkboot {variable project_subcommand_info [dict create \ + # mytool {summary "One-line summary." argsid (script)::mykit::mytool \ + # package punk::args::moduledoc::mykit parse 0}]} namespace eval ::punkboot {variable project_subcommands [dict create]} # ==CUSTOMIZE== launch defaults ------------------------------------------ diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punkboot_core.tcl b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punkboot_core.tcl index 41e20475..738bc1fb 100644 --- a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punkboot_core.tcl +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_config/punkboot_core.tcl @@ -11,9 +11,13 @@ #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) extended by the thin main's declared -#project subcommands (G-031 registration model - see the dispatch section -#near the end of this file). +#(tclsh/shellspy/punk/shell/script/buildinfo/help) 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): ' help ?subcommand?', '-help' and +#' -help' render tabled usage from the moduledoc::punkexe +#definitions plus the live registrations, degrading to a plain subcommand +#list when that stack is unavailable. # #The package_mode surface (documented in full by punk::args::moduledoc::punkexe): #an optional FIRST launch argument of the form minted|os|internal|src or any @@ -33,6 +37,11 @@ # to this file's own path) # project_subcommands dict: subcommand name -> handler script (see the # dispatch section; built-ins cannot be shadowed) +# project_subcommand_info dict: subcommand name -> info dict with +# optional keys summary/argsid/package/parse feeding +# the G-032 launcher help + declared-parse wiring +# (see the launcher help section; a name without an +# info entry keeps full passthrough semantics) # launch_defaults dict: noargs / unknownfirst (see dispatch section) # ------------------------------------------------------------------------------ @@ -42,7 +51,7 @@ namespace eval ::punkboot { #boot-core identity (G-031; stamped into kits + reported by the G-025 #buildinfo surfaces) - variable core_version 0.2.1 + variable core_version 0.3.0 } if {![info exists ::punkboot::launch_args]} { namespace eval ::punkboot [list variable launch_args $::argv] @@ -53,6 +62,9 @@ if {![info exists ::punkboot::main_script]} { if {![info exists ::punkboot::project_subcommands]} { namespace eval ::punkboot {variable project_subcommands [dict create]} } +if {![info exists ::punkboot::project_subcommand_info]} { + namespace eval ::punkboot {variable project_subcommand_info [dict create]} +} if {![info exists ::punkboot::launch_defaults]} { namespace eval ::punkboot {variable launch_defaults [dict create]} } @@ -1509,20 +1521,291 @@ if {![info exists ::punkboot::launch_defaults]} { } namespace eval ::punkboot [list variable boot_state $boot_state_dict] + #--------------------------------------------------------------- + #Launcher help + declared-parse machinery (G-032) + #The launch surface documents itself through punk::args (definition ids + #from the punk::args::moduledoc::punkexe family): + # help ?subcommand? tabled usage; the top-level table lists + # built-ins AND project-registered + # subcommands with summaries + # -help same as bare 'help' - only on kits whose + # unknownfirst default is 'script' (a + # tool-style kit routes flags to its own + # processor; its surface stays reachable + # via the 'help' word) + # -help the subcommand's tabled usage when -help + # is its FIRST argument (tclsh: only when + # -help is the SOLE argument - stock + # parity keeps every other dash form in + # ::argv; project subcommands: only when + # the registration declares an argsid) + #Everything is guarded and lazy (the G-030 degradation doctrine): nothing + #here loads punk::args on a normal boot/dispatch path. When punk::args or + #the moduledoc definitions are unavailable (or PUNKBOOT_PLAIN=1 - same + #env hook as make.tcl), help degrades to a plain subcommand list, parse + #gates degrade to the historic switch semantics, and boot never fails. + #The tabled rendering additionally depends on the punk::ansi/textblock + #stack - rendering degrades (minimal errorstyle) independently of parsing. + #The per-kit top-level usage id (script)::punkexe.launcher is cloned from + #the static (script)::punkexe definition at first render, with the + #subcommand choices/choicelabels replaced by this kit's LIVE surface; + #(script)::punkexe.launcherhelp is the same clone of + #(script)::punkexe::help for subject validation. + #--------------------------------------------------------------- + namespace eval ::punkboot { + variable help_flags [list -help --help -h /?] + variable launch_builtin_subcommands [list] + variable launch_subcommands [list] + variable launcher_punkargs_state "" ;#"" unprobed | ok | plain + variable launcher_builtin_summaries [dict create \ + tclsh "Run as a (near) stock tclsh - no punk modules loaded." \ + script "Run a script non-interactively in the punk script environment; honest exit codes." \ + shell "Interactive punk shell (repl)." \ + punk "punk shell launch; piped-stdin friendly." \ + shellspy "Experimental shellspy command-line processor." \ + buildinfo "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)." \ + ] + proc launcher_punkargs_ready {} { + #lazy guarded capability probe, one verdict per process. ok requires + #punk::args AND the moduledoc launch definitions to load and resolve + #with the snapshot actually available in this kit/context. + variable launcher_punkargs_state + if {$launcher_punkargs_state ne ""} { + return [expr {$launcher_punkargs_state eq "ok"}] + } + set launcher_punkargs_state plain + if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBOOT_PLAIN)]} { + return 0 + } + if {[catch { + package require punk::args + package require punk::args::moduledoc::punkexe + if {[punk::args::get_spec (script)::punkexe] eq ""} { + error "definition (script)::punkexe did not resolve" + } + punk::args::get_spec (script)::punkexe::buildinfo + punk::args::get_spec (script)::punkexe::help + }]} { + return 0 + } + set launcher_punkargs_state ok + return 1 + } + proc launcher_errstyle {} { + #tabled (standard) parse errors when the rendering stack is + #loadable; minimal otherwise - rendering degrades independently of + #parsing (the G-030 split) + catch {package require punk::ansi} + catch {package require textblock} + if {[package provide punk::ansi] ne "" && [package provide textblock] ne ""} { + return standard + } + return minimal + } + proc launcher_subcommand_summary {name} { + #plain-text one-line summary for a known subcommand (built-in table + #or the project registration's declared summary) + variable launcher_builtin_summaries + variable project_subcommand_info + if {[dict exists $launcher_builtin_summaries $name]} { + return [dict get $launcher_builtin_summaries $name] + } + if {[dict exists $project_subcommand_info $name summary]} { + return [dict get $project_subcommand_info $name summary] + } + return "Project-declared subcommand (no summary registered)." + } + proc launcher_live_choices {} { + #this kit's subcommand set in display order: the static definition's + #choice order first (the documented built-in family), then 'help', + #then project registrations in declaration order + variable launch_subcommands + set ordered [list] + catch { + set spec [punk::args::get_spec (script)::punkexe] + foreach c [dict get $spec FORMS _default ARG_INFO subcommand -choices] { + if {$c in $launch_subcommands && $c ni $ordered} { + lappend ordered $c + } + } + } + foreach c $launch_subcommands { + if {$c ni $ordered} { + lappend ordered $c + } + } + return $ordered + } + proc launcher_overview_id {} { + #define (script)::punkexe.launcher: the per-kit top-level usage - + #the moduledoc (script)::punkexe definition with the subcommand + #choices/choicelabels replaced by this kit's live surface. + #Returns the id, or "" when the clone cannot be built. + variable launch_builtin_subcommands + set id (script)::punkexe.launcher + set existing "" + catch {set existing [punk::args::raw_def $id]} + if {$existing ne ""} { + return $id + } + if {[catch { + set spec [punk::args::get_spec (script)::punkexe] + set labels [dict get $spec FORMS _default ARG_INFO subcommand -choicelabels] + set choices [launcher_live_choices] + foreach c $choices { + if {$c eq "help" || $c ni $launch_builtin_subcommands || ![dict exists $labels $c]} { + dict set labels $c " [launcher_subcommand_summary $c]" + } + } + set ov [dict create \ + @id [list -id $id] \ + subcommand [list -choices $choices -choicelabels $labels] \ + ] + punk::args::define [punk::args::resolved_def -override $ov (script)::punkexe] + }]} { + return "" + } + return $id + } + proc launcher_helpdef_id {} { + #define (script)::punkexe.launcherhelp: the moduledoc + #(script)::punkexe::help definition with the subject choices + #replaced by the live subcommand set, so 'help ' validates + #(and prefix-resolves) against what this kit actually offers. + set id (script)::punkexe.launcherhelp + set existing "" + catch {set existing [punk::args::raw_def $id]} + if {$existing ne ""} { + return $id + } + if {[catch { + set ov [dict create \ + @id [list -id $id] \ + subject [list -choices [launcher_live_choices]] \ + ] + punk::args::define [punk::args::resolved_def -override $ov (script)::punkexe::help] + }]} { + return "" + } + return $id + } + proc launcher_plain_help {} { + #degraded help: plain subcommand list built without punk::args + set exebase [file rootname [file tail [info nameofexecutable]]] + set lines [list] + lappend lines "Usage: $exebase ?packagemode? ?subcommand? ?arg ...?" + 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:" + foreach sub [launcher_plain_choices] { + lappend lines [format " %-12s %s" $sub [launcher_subcommand_summary $sub]] + } + lappend lines " ('$exebase help ?subcommand?' shows detail; plain list because the punk::args" + lappend lines " tabled help stack is unavailable in this context, or PUNKBOOT_PLAIN is set)" + return [join $lines \n] + } + proc launcher_plain_choices {} { + #display order without punk::args: documented built-in order, then + #project registrations in declaration order + variable launch_subcommands + set ordered [list] + foreach c [list tclsh script shell punk shellspy buildinfo help] { + if {$c in $launch_subcommands} { + lappend ordered $c + } + } + foreach c $launch_subcommands { + if {$c ni $ordered} { + lappend ordered $c + } + } + return $ordered + } + proc launcher_show_help {{subject ""}} { + #render help to stdout: the top-level launch surface (empty + #subject) or one subcommand's usage. Tabled via punk::args when + #available; degrades to the plain list/summary. Never errors. + variable launch_builtin_subcommands + variable project_subcommand_info + if {[launcher_punkargs_ready]} { + set id "" + if {$subject eq ""} { + set id [launcher_overview_id] + } elseif {$subject eq "help"} { + set id [launcher_helpdef_id] + } elseif {$subject in $launch_builtin_subcommands} { + set id (script)::punkexe::$subject + } else { + #project-registered: guarded require of the declared + #package (if any), then the declared definition id (if any) + if {[dict exists $project_subcommand_info $subject package]} { + catch {package require [dict get $project_subcommand_info $subject package]} + } + if {[dict exists $project_subcommand_info $subject argsid]} { + set id [dict get $project_subcommand_info $subject argsid] + } else { + #help by registration alone: the summary line + puts stdout "$subject - [launcher_subcommand_summary $subject]" + puts stdout "(no punk::args definition registered for this subcommand - if it is an application entry point it may provide its own -help handling)" + return + } + } + if {$id ne "" && ![catch {punk::args::usage $id} out]} { + puts stdout $out + return + } + #fall through to the plain forms on any render failure + } + if {$subject eq ""} { + puts stdout [launcher_plain_help] + } else { + puts stdout "$subject - [launcher_subcommand_summary $subject]" + puts stdout "(plain help: the punk::args tabled help stack is unavailable in this context, or PUNKBOOT_PLAIN is set)" + } + return + } + proc launcher_unknown_first_error {word} { + #G-032 reclassification rule: an unknown first argument that names + #no existing file (and is no lib:* scriptlib reference) is an + #obvious mistake - refuse it with usage on stderr instead of + #silently attempting it as a script. Returns 1 when the refusal + #was emitted (caller exits 1); 0 when punk::args is unavailable + #(caller falls back to the historic script reclassification). + variable launch_subcommands + if {![launcher_punkargs_ready]} { + return 0 + } + set exebase [file rootname [file tail [info nameofexecutable]]] + puts stderr "$exebase: unknown subcommand '$word' (no such subcommand, and no such script file exists)" + set id [launcher_overview_id] + if {$id ne "" && ![catch {punk::args::usage $id} out]} { + puts stderr $out + } else { + 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" + return 1 + } + } + #--------------------------------------------------------------- #Subcommand selection (G-031 registration model) - #Built-ins (tclsh/shellspy/punk/shell/script) ship with this core; a thin - #main may extend the set via ::punkboot::project_subcommands (dict: - #name -> handler script, evaluated in the dispatch below with ::argv/::argc - #holding the subcommand's arguments). Built-in names cannot be shadowed - - #a colliding declaration is reported and ignored. + #Built-ins (tclsh/shellspy/punk/shell/script/buildinfo/help) ship with + #this core; a thin main may extend the set via + #::punkboot::project_subcommands (dict: name -> handler script, evaluated + #in the dispatch below with ::argv/::argc holding the subcommand's + #arguments). Built-in names cannot be shadowed - a colliding declaration + #is reported and ignored. ::punkboot::project_subcommand_info optionally + #carries per-name help/parse metadata (summary/argsid/package/parse - see + #the launcher help section above, G-032). #::punkboot::launch_defaults keys (both optional): # noargs subcommand assumed for a bare launch (default: shell) # unknownfirst 'script' treats a non-subcommand first argument as a # script invocation (default), or the name of a known # subcommand to receive the whole arglist as its arguments #--------------------------------------------------------------- - set builtin_subcommands [list tclsh shellspy punk shell script buildinfo] + set builtin_subcommands [list tclsh shellspy punk shell script buildinfo help] set known_subcommands $builtin_subcommands foreach k [dict keys $::punkboot::project_subcommands] { if {$k in $builtin_subcommands} { @@ -1531,6 +1814,9 @@ if {![info exists ::punkboot::launch_defaults]} { lappend known_subcommands $k } } + #publish for the launcher help machinery (and post-boot probes) + set ::punkboot::launch_builtin_subcommands $builtin_subcommands + set ::punkboot::launch_subcommands $known_subcommands if {[dict exists $::punkboot::launch_defaults noargs]} { set default_noargs [dict get $::punkboot::launch_defaults noargs] } else { @@ -1544,10 +1830,56 @@ if {![info exists ::punkboot::launch_defaults]} { set subcommand [lindex $arglist 0] if {$subcommand in $known_subcommands} { set subcommand_arglist [lrange $arglist 1 end] + #G-032: a help flag as the subcommand's FIRST argument renders that + #subcommand's usage. tclsh: only when it is the SOLE argument (stock + #parity keeps every other leading-dash form - including '-help' with + #further arguments - in ::argv); project subcommands: only when the + #registration declares an argsid (a handler-only registration keeps + #full passthrough - the application may do its own -help handling). + if {[lindex $subcommand_arglist 0] in $::punkboot::help_flags} { + set do_subhelp 0 + if {$subcommand eq "tclsh"} { + if {[llength $subcommand_arglist] == 1} { + set do_subhelp 1 + } + } elseif {$subcommand in $builtin_subcommands} { + set do_subhelp 1 + } elseif {[dict exists $::punkboot::project_subcommand_info $subcommand argsid]} { + set do_subhelp 1 + } + if {$do_subhelp} { + ::punkboot::launcher_show_help $subcommand + exit 0 + } + } } else { set subcommand_arglist $arglist if {[llength $subcommand_arglist]} { + #G-032: a help flag in first position is a launch-surface help + #request on kits whose unknown-first default is 'script'. + #Tool-style kits (unknownfirst names a project subcommand) keep + #routing flags to their processor - their surface stays + #discoverable via the 'help' subcommand word. + if {$subcommand in $::punkboot::help_flags && $default_unknownfirst eq "script"} { + ::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 + } + } + } } else { set subcommand $default_noargs } @@ -1726,8 +2058,20 @@ if {![info exists ::punkboot::launch_defaults]} { #implementation is punk::buildinfo - this arm is the thin #exe-subcommand wrapper. Stdout carries only the report; honest #exit code; no repl fallthrough (G-015-compatible). + #G-032: the argument contract ((script)::punkexe::buildinfo, + #@values -min 0 -max 0) is enforced through punk::args when + #available: any argument earns the historic one-line refusal PLUS + #the tabled usage error on stderr, exit 2 (buildinfo.test pins the + #one-liner and the exit code). The empty-argument fast path never + #touches punk::args; a '-help' first argument was already + #intercepted at selection. if {[llength $subcommand_arglist]} { puts stderr "buildinfo: takes no arguments" + if {[::punkboot::launcher_punkargs_ready]} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid (script)::punkexe::buildinfo} parse_msg]} { + puts stderr $parse_msg + } + } exit 2 } if {[catch {package require punk::buildinfo} errM]} { @@ -1736,6 +2080,41 @@ if {![info exists ::punkboot::launch_defaults]} { } exit [punk::buildinfo::main] } + help { + #G-032: launcher help surface. Bare 'help' renders the top-level + #tabled usage (built-ins + project-registered subcommands with + #summaries); 'help ' renders that subcommand's usage. + #Subject validation parses through the launcherhelp clone when + #punk::args is available (tabled choice errors on stderr exit 1, + #unambiguous prefixes resolve); degraded mode accepts exact names + #only and renders the plain forms. Help goes to stdout, exit 0 + #(G-015 output-cleanliness/exit-code doctrine). + if {![llength $subcommand_arglist]} { + ::punkboot::launcher_show_help + exit 0 + } + if {[::punkboot::launcher_punkargs_ready]} { + set helpdef_id [::punkboot::launcher_helpdef_id] + if {$helpdef_id ne ""} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid $helpdef_id} argd]} { + puts stderr $argd + exit 1 + } + set help_subject "" + catch {set help_subject [dict get $argd values subject]} + ::punkboot::launcher_show_help $help_subject + exit 0 + } + } + #degraded: exact-name subject only + set help_subject [lindex $subcommand_arglist 0] + if {[llength $subcommand_arglist] > 1 || $help_subject ni $::punkboot::launch_subcommands} { + puts stderr "help: expected 'help ?subcommand?' with subcommand one of: [join [::punkboot::launcher_plain_choices] {, }]" + exit 1 + } + ::punkboot::launcher_show_help $help_subject + exit 0 + } default { #a project-declared subcommand (thin-main customization point): the #handler script runs in this frame, ::argv/::argc already hold the @@ -1743,6 +2122,30 @@ if {![info exists ::punkboot::launch_defaults]} { #misdeclared launch_defaults value - the selection above maps every #launch form to a known subcommand. if {[dict exists $::punkboot::project_subcommands $subcommand]} { + #G-032 declared-parse gate: a registration may opt its + #arguments into punk::args validation via + #project_subcommand_info keys parse=1 + argsid (optional + #package requirement). A parse failure is a tabled usage + #error on stderr, exit 1, and the handler is not invoked. + #Degrades to the historic no-validation passthrough when + #punk::args (or the declared id) is unavailable. + if {[dict exists $::punkboot::project_subcommand_info $subcommand parse] + && [string is true -strict [dict get $::punkboot::project_subcommand_info $subcommand parse]] + && [dict exists $::punkboot::project_subcommand_info $subcommand argsid] + && [::punkboot::launcher_punkargs_ready]} { + if {[dict exists $::punkboot::project_subcommand_info $subcommand package]} { + catch {package require [dict get $::punkboot::project_subcommand_info $subcommand package]} + } + set project_argsid [dict get $::punkboot::project_subcommand_info $subcommand argsid] + set project_argsid_ok 0 + catch {set project_argsid_ok [expr {[punk::args::raw_def $project_argsid] ne ""}]} + if {$project_argsid_ok} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid $project_argsid} parse_msg]} { + puts stderr $parse_msg + exit 1 + } + } + } eval [dict get $::punkboot::project_subcommands $subcommand] } else { puts stderr "main.tcl: no handler for subcommand '$subcommand' (check launch_defaults in the kit's main.tcl) - known subcommands: $known_subcommands" diff --git a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_vfscommon.vfs/punkboot/core.tcl b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_vfscommon.vfs/punkboot/core.tcl index 41e20475..738bc1fb 100644 --- a/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_vfscommon.vfs/punkboot/core.tcl +++ b/src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/vfs/_vfscommon.vfs/punkboot/core.tcl @@ -11,9 +11,13 @@ #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) extended by the thin main's declared -#project subcommands (G-031 registration model - see the dispatch section -#near the end of this file). +#(tclsh/shellspy/punk/shell/script/buildinfo/help) 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): ' help ?subcommand?', '-help' and +#' -help' render tabled usage from the moduledoc::punkexe +#definitions plus the live registrations, degrading to a plain subcommand +#list when that stack is unavailable. # #The package_mode surface (documented in full by punk::args::moduledoc::punkexe): #an optional FIRST launch argument of the form minted|os|internal|src or any @@ -33,6 +37,11 @@ # to this file's own path) # project_subcommands dict: subcommand name -> handler script (see the # dispatch section; built-ins cannot be shadowed) +# project_subcommand_info dict: subcommand name -> info dict with +# optional keys summary/argsid/package/parse feeding +# the G-032 launcher help + declared-parse wiring +# (see the launcher help section; a name without an +# info entry keeps full passthrough semantics) # launch_defaults dict: noargs / unknownfirst (see dispatch section) # ------------------------------------------------------------------------------ @@ -42,7 +51,7 @@ namespace eval ::punkboot { #boot-core identity (G-031; stamped into kits + reported by the G-025 #buildinfo surfaces) - variable core_version 0.2.1 + variable core_version 0.3.0 } if {![info exists ::punkboot::launch_args]} { namespace eval ::punkboot [list variable launch_args $::argv] @@ -53,6 +62,9 @@ if {![info exists ::punkboot::main_script]} { if {![info exists ::punkboot::project_subcommands]} { namespace eval ::punkboot {variable project_subcommands [dict create]} } +if {![info exists ::punkboot::project_subcommand_info]} { + namespace eval ::punkboot {variable project_subcommand_info [dict create]} +} if {![info exists ::punkboot::launch_defaults]} { namespace eval ::punkboot {variable launch_defaults [dict create]} } @@ -1509,20 +1521,291 @@ if {![info exists ::punkboot::launch_defaults]} { } namespace eval ::punkboot [list variable boot_state $boot_state_dict] + #--------------------------------------------------------------- + #Launcher help + declared-parse machinery (G-032) + #The launch surface documents itself through punk::args (definition ids + #from the punk::args::moduledoc::punkexe family): + # help ?subcommand? tabled usage; the top-level table lists + # built-ins AND project-registered + # subcommands with summaries + # -help same as bare 'help' - only on kits whose + # unknownfirst default is 'script' (a + # tool-style kit routes flags to its own + # processor; its surface stays reachable + # via the 'help' word) + # -help the subcommand's tabled usage when -help + # is its FIRST argument (tclsh: only when + # -help is the SOLE argument - stock + # parity keeps every other dash form in + # ::argv; project subcommands: only when + # the registration declares an argsid) + #Everything is guarded and lazy (the G-030 degradation doctrine): nothing + #here loads punk::args on a normal boot/dispatch path. When punk::args or + #the moduledoc definitions are unavailable (or PUNKBOOT_PLAIN=1 - same + #env hook as make.tcl), help degrades to a plain subcommand list, parse + #gates degrade to the historic switch semantics, and boot never fails. + #The tabled rendering additionally depends on the punk::ansi/textblock + #stack - rendering degrades (minimal errorstyle) independently of parsing. + #The per-kit top-level usage id (script)::punkexe.launcher is cloned from + #the static (script)::punkexe definition at first render, with the + #subcommand choices/choicelabels replaced by this kit's LIVE surface; + #(script)::punkexe.launcherhelp is the same clone of + #(script)::punkexe::help for subject validation. + #--------------------------------------------------------------- + namespace eval ::punkboot { + variable help_flags [list -help --help -h /?] + variable launch_builtin_subcommands [list] + variable launch_subcommands [list] + variable launcher_punkargs_state "" ;#"" unprobed | ok | plain + variable launcher_builtin_summaries [dict create \ + tclsh "Run as a (near) stock tclsh - no punk modules loaded." \ + script "Run a script non-interactively in the punk script environment; honest exit codes." \ + shell "Interactive punk shell (repl)." \ + punk "punk shell launch; piped-stdin friendly." \ + shellspy "Experimental shellspy command-line processor." \ + buildinfo "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)." \ + ] + proc launcher_punkargs_ready {} { + #lazy guarded capability probe, one verdict per process. ok requires + #punk::args AND the moduledoc launch definitions to load and resolve + #with the snapshot actually available in this kit/context. + variable launcher_punkargs_state + if {$launcher_punkargs_state ne ""} { + return [expr {$launcher_punkargs_state eq "ok"}] + } + set launcher_punkargs_state plain + if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBOOT_PLAIN)]} { + return 0 + } + if {[catch { + package require punk::args + package require punk::args::moduledoc::punkexe + if {[punk::args::get_spec (script)::punkexe] eq ""} { + error "definition (script)::punkexe did not resolve" + } + punk::args::get_spec (script)::punkexe::buildinfo + punk::args::get_spec (script)::punkexe::help + }]} { + return 0 + } + set launcher_punkargs_state ok + return 1 + } + proc launcher_errstyle {} { + #tabled (standard) parse errors when the rendering stack is + #loadable; minimal otherwise - rendering degrades independently of + #parsing (the G-030 split) + catch {package require punk::ansi} + catch {package require textblock} + if {[package provide punk::ansi] ne "" && [package provide textblock] ne ""} { + return standard + } + return minimal + } + proc launcher_subcommand_summary {name} { + #plain-text one-line summary for a known subcommand (built-in table + #or the project registration's declared summary) + variable launcher_builtin_summaries + variable project_subcommand_info + if {[dict exists $launcher_builtin_summaries $name]} { + return [dict get $launcher_builtin_summaries $name] + } + if {[dict exists $project_subcommand_info $name summary]} { + return [dict get $project_subcommand_info $name summary] + } + return "Project-declared subcommand (no summary registered)." + } + proc launcher_live_choices {} { + #this kit's subcommand set in display order: the static definition's + #choice order first (the documented built-in family), then 'help', + #then project registrations in declaration order + variable launch_subcommands + set ordered [list] + catch { + set spec [punk::args::get_spec (script)::punkexe] + foreach c [dict get $spec FORMS _default ARG_INFO subcommand -choices] { + if {$c in $launch_subcommands && $c ni $ordered} { + lappend ordered $c + } + } + } + foreach c $launch_subcommands { + if {$c ni $ordered} { + lappend ordered $c + } + } + return $ordered + } + proc launcher_overview_id {} { + #define (script)::punkexe.launcher: the per-kit top-level usage - + #the moduledoc (script)::punkexe definition with the subcommand + #choices/choicelabels replaced by this kit's live surface. + #Returns the id, or "" when the clone cannot be built. + variable launch_builtin_subcommands + set id (script)::punkexe.launcher + set existing "" + catch {set existing [punk::args::raw_def $id]} + if {$existing ne ""} { + return $id + } + if {[catch { + set spec [punk::args::get_spec (script)::punkexe] + set labels [dict get $spec FORMS _default ARG_INFO subcommand -choicelabels] + set choices [launcher_live_choices] + foreach c $choices { + if {$c eq "help" || $c ni $launch_builtin_subcommands || ![dict exists $labels $c]} { + dict set labels $c " [launcher_subcommand_summary $c]" + } + } + set ov [dict create \ + @id [list -id $id] \ + subcommand [list -choices $choices -choicelabels $labels] \ + ] + punk::args::define [punk::args::resolved_def -override $ov (script)::punkexe] + }]} { + return "" + } + return $id + } + proc launcher_helpdef_id {} { + #define (script)::punkexe.launcherhelp: the moduledoc + #(script)::punkexe::help definition with the subject choices + #replaced by the live subcommand set, so 'help ' validates + #(and prefix-resolves) against what this kit actually offers. + set id (script)::punkexe.launcherhelp + set existing "" + catch {set existing [punk::args::raw_def $id]} + if {$existing ne ""} { + return $id + } + if {[catch { + set ov [dict create \ + @id [list -id $id] \ + subject [list -choices [launcher_live_choices]] \ + ] + punk::args::define [punk::args::resolved_def -override $ov (script)::punkexe::help] + }]} { + return "" + } + return $id + } + proc launcher_plain_help {} { + #degraded help: plain subcommand list built without punk::args + set exebase [file rootname [file tail [info nameofexecutable]]] + set lines [list] + lappend lines "Usage: $exebase ?packagemode? ?subcommand? ?arg ...?" + 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:" + foreach sub [launcher_plain_choices] { + lappend lines [format " %-12s %s" $sub [launcher_subcommand_summary $sub]] + } + lappend lines " ('$exebase help ?subcommand?' shows detail; plain list because the punk::args" + lappend lines " tabled help stack is unavailable in this context, or PUNKBOOT_PLAIN is set)" + return [join $lines \n] + } + proc launcher_plain_choices {} { + #display order without punk::args: documented built-in order, then + #project registrations in declaration order + variable launch_subcommands + set ordered [list] + foreach c [list tclsh script shell punk shellspy buildinfo help] { + if {$c in $launch_subcommands} { + lappend ordered $c + } + } + foreach c $launch_subcommands { + if {$c ni $ordered} { + lappend ordered $c + } + } + return $ordered + } + proc launcher_show_help {{subject ""}} { + #render help to stdout: the top-level launch surface (empty + #subject) or one subcommand's usage. Tabled via punk::args when + #available; degrades to the plain list/summary. Never errors. + variable launch_builtin_subcommands + variable project_subcommand_info + if {[launcher_punkargs_ready]} { + set id "" + if {$subject eq ""} { + set id [launcher_overview_id] + } elseif {$subject eq "help"} { + set id [launcher_helpdef_id] + } elseif {$subject in $launch_builtin_subcommands} { + set id (script)::punkexe::$subject + } else { + #project-registered: guarded require of the declared + #package (if any), then the declared definition id (if any) + if {[dict exists $project_subcommand_info $subject package]} { + catch {package require [dict get $project_subcommand_info $subject package]} + } + if {[dict exists $project_subcommand_info $subject argsid]} { + set id [dict get $project_subcommand_info $subject argsid] + } else { + #help by registration alone: the summary line + puts stdout "$subject - [launcher_subcommand_summary $subject]" + puts stdout "(no punk::args definition registered for this subcommand - if it is an application entry point it may provide its own -help handling)" + return + } + } + if {$id ne "" && ![catch {punk::args::usage $id} out]} { + puts stdout $out + return + } + #fall through to the plain forms on any render failure + } + if {$subject eq ""} { + puts stdout [launcher_plain_help] + } else { + puts stdout "$subject - [launcher_subcommand_summary $subject]" + puts stdout "(plain help: the punk::args tabled help stack is unavailable in this context, or PUNKBOOT_PLAIN is set)" + } + return + } + proc launcher_unknown_first_error {word} { + #G-032 reclassification rule: an unknown first argument that names + #no existing file (and is no lib:* scriptlib reference) is an + #obvious mistake - refuse it with usage on stderr instead of + #silently attempting it as a script. Returns 1 when the refusal + #was emitted (caller exits 1); 0 when punk::args is unavailable + #(caller falls back to the historic script reclassification). + variable launch_subcommands + if {![launcher_punkargs_ready]} { + return 0 + } + set exebase [file rootname [file tail [info nameofexecutable]]] + puts stderr "$exebase: unknown subcommand '$word' (no such subcommand, and no such script file exists)" + set id [launcher_overview_id] + if {$id ne "" && ![catch {punk::args::usage $id} out]} { + puts stderr $out + } else { + 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" + return 1 + } + } + #--------------------------------------------------------------- #Subcommand selection (G-031 registration model) - #Built-ins (tclsh/shellspy/punk/shell/script) ship with this core; a thin - #main may extend the set via ::punkboot::project_subcommands (dict: - #name -> handler script, evaluated in the dispatch below with ::argv/::argc - #holding the subcommand's arguments). Built-in names cannot be shadowed - - #a colliding declaration is reported and ignored. + #Built-ins (tclsh/shellspy/punk/shell/script/buildinfo/help) ship with + #this core; a thin main may extend the set via + #::punkboot::project_subcommands (dict: name -> handler script, evaluated + #in the dispatch below with ::argv/::argc holding the subcommand's + #arguments). Built-in names cannot be shadowed - a colliding declaration + #is reported and ignored. ::punkboot::project_subcommand_info optionally + #carries per-name help/parse metadata (summary/argsid/package/parse - see + #the launcher help section above, G-032). #::punkboot::launch_defaults keys (both optional): # noargs subcommand assumed for a bare launch (default: shell) # unknownfirst 'script' treats a non-subcommand first argument as a # script invocation (default), or the name of a known # subcommand to receive the whole arglist as its arguments #--------------------------------------------------------------- - set builtin_subcommands [list tclsh shellspy punk shell script buildinfo] + set builtin_subcommands [list tclsh shellspy punk shell script buildinfo help] set known_subcommands $builtin_subcommands foreach k [dict keys $::punkboot::project_subcommands] { if {$k in $builtin_subcommands} { @@ -1531,6 +1814,9 @@ if {![info exists ::punkboot::launch_defaults]} { lappend known_subcommands $k } } + #publish for the launcher help machinery (and post-boot probes) + set ::punkboot::launch_builtin_subcommands $builtin_subcommands + set ::punkboot::launch_subcommands $known_subcommands if {[dict exists $::punkboot::launch_defaults noargs]} { set default_noargs [dict get $::punkboot::launch_defaults noargs] } else { @@ -1544,10 +1830,56 @@ if {![info exists ::punkboot::launch_defaults]} { set subcommand [lindex $arglist 0] if {$subcommand in $known_subcommands} { set subcommand_arglist [lrange $arglist 1 end] + #G-032: a help flag as the subcommand's FIRST argument renders that + #subcommand's usage. tclsh: only when it is the SOLE argument (stock + #parity keeps every other leading-dash form - including '-help' with + #further arguments - in ::argv); project subcommands: only when the + #registration declares an argsid (a handler-only registration keeps + #full passthrough - the application may do its own -help handling). + if {[lindex $subcommand_arglist 0] in $::punkboot::help_flags} { + set do_subhelp 0 + if {$subcommand eq "tclsh"} { + if {[llength $subcommand_arglist] == 1} { + set do_subhelp 1 + } + } elseif {$subcommand in $builtin_subcommands} { + set do_subhelp 1 + } elseif {[dict exists $::punkboot::project_subcommand_info $subcommand argsid]} { + set do_subhelp 1 + } + if {$do_subhelp} { + ::punkboot::launcher_show_help $subcommand + exit 0 + } + } } else { set subcommand_arglist $arglist if {[llength $subcommand_arglist]} { + #G-032: a help flag in first position is a launch-surface help + #request on kits whose unknown-first default is 'script'. + #Tool-style kits (unknownfirst names a project subcommand) keep + #routing flags to their processor - their surface stays + #discoverable via the 'help' subcommand word. + if {$subcommand in $::punkboot::help_flags && $default_unknownfirst eq "script"} { + ::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 + } + } + } } else { set subcommand $default_noargs } @@ -1726,8 +2058,20 @@ if {![info exists ::punkboot::launch_defaults]} { #implementation is punk::buildinfo - this arm is the thin #exe-subcommand wrapper. Stdout carries only the report; honest #exit code; no repl fallthrough (G-015-compatible). + #G-032: the argument contract ((script)::punkexe::buildinfo, + #@values -min 0 -max 0) is enforced through punk::args when + #available: any argument earns the historic one-line refusal PLUS + #the tabled usage error on stderr, exit 2 (buildinfo.test pins the + #one-liner and the exit code). The empty-argument fast path never + #touches punk::args; a '-help' first argument was already + #intercepted at selection. if {[llength $subcommand_arglist]} { puts stderr "buildinfo: takes no arguments" + if {[::punkboot::launcher_punkargs_ready]} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid (script)::punkexe::buildinfo} parse_msg]} { + puts stderr $parse_msg + } + } exit 2 } if {[catch {package require punk::buildinfo} errM]} { @@ -1736,6 +2080,41 @@ if {![info exists ::punkboot::launch_defaults]} { } exit [punk::buildinfo::main] } + help { + #G-032: launcher help surface. Bare 'help' renders the top-level + #tabled usage (built-ins + project-registered subcommands with + #summaries); 'help ' renders that subcommand's usage. + #Subject validation parses through the launcherhelp clone when + #punk::args is available (tabled choice errors on stderr exit 1, + #unambiguous prefixes resolve); degraded mode accepts exact names + #only and renders the plain forms. Help goes to stdout, exit 0 + #(G-015 output-cleanliness/exit-code doctrine). + if {![llength $subcommand_arglist]} { + ::punkboot::launcher_show_help + exit 0 + } + if {[::punkboot::launcher_punkargs_ready]} { + set helpdef_id [::punkboot::launcher_helpdef_id] + if {$helpdef_id ne ""} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid $helpdef_id} argd]} { + puts stderr $argd + exit 1 + } + set help_subject "" + catch {set help_subject [dict get $argd values subject]} + ::punkboot::launcher_show_help $help_subject + exit 0 + } + } + #degraded: exact-name subject only + set help_subject [lindex $subcommand_arglist 0] + if {[llength $subcommand_arglist] > 1 || $help_subject ni $::punkboot::launch_subcommands} { + puts stderr "help: expected 'help ?subcommand?' with subcommand one of: [join [::punkboot::launcher_plain_choices] {, }]" + exit 1 + } + ::punkboot::launcher_show_help $help_subject + exit 0 + } default { #a project-declared subcommand (thin-main customization point): the #handler script runs in this frame, ::argv/::argc already hold the @@ -1743,6 +2122,30 @@ if {![info exists ::punkboot::launch_defaults]} { #misdeclared launch_defaults value - the selection above maps every #launch form to a known subcommand. if {[dict exists $::punkboot::project_subcommands $subcommand]} { + #G-032 declared-parse gate: a registration may opt its + #arguments into punk::args validation via + #project_subcommand_info keys parse=1 + argsid (optional + #package requirement). A parse failure is a tabled usage + #error on stderr, exit 1, and the handler is not invoked. + #Degrades to the historic no-validation passthrough when + #punk::args (or the declared id) is unavailable. + if {[dict exists $::punkboot::project_subcommand_info $subcommand parse] + && [string is true -strict [dict get $::punkboot::project_subcommand_info $subcommand parse]] + && [dict exists $::punkboot::project_subcommand_info $subcommand argsid] + && [::punkboot::launcher_punkargs_ready]} { + if {[dict exists $::punkboot::project_subcommand_info $subcommand package]} { + catch {package require [dict get $::punkboot::project_subcommand_info $subcommand package]} + } + set project_argsid [dict get $::punkboot::project_subcommand_info $subcommand argsid] + set project_argsid_ok 0 + catch {set project_argsid_ok [expr {[punk::args::raw_def $project_argsid] ne ""}]} + if {$project_argsid_ok} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid $project_argsid} parse_msg]} { + puts stderr $parse_msg + exit 1 + } + } + } eval [dict get $::punkboot::project_subcommands $subcommand] } else { puts stderr "main.tcl: no handler for subcommand '$subcommand' (check launch_defaults in the kit's main.tcl) - known subcommands: $known_subcommands" diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.1.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.2.0.tm similarity index 78% rename from src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.1.0.tm rename to src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.2.0.tm index 84c2bb53..c6af3ed4 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.1.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkboot-0.2.0.tm @@ -6,7 +6,7 @@ # (C) 2026 # # @@ Meta Begin -# Application punk::args::moduledoc::punkboot 0.1.0 +# Application punk::args::moduledoc::punkboot 0.2.0 # Meta platform tcl # Meta license BSD # @@ Meta End @@ -64,6 +64,10 @@ tcl::namespace::eval punk::args::moduledoc::punkboot { project_subcommands optional dict: subcommand name -> handler script (see the registration definition id (script)::punkboot::registration) + project_subcommand_info optional dict: subcommand name -> + help/parse metadata (summary/argsid/ + package/parse - G-032; same + registration definition) launch_defaults optional dict: noargs / unknownfirst then source the core, first hit of: /punkboot/core.tcl (baked kit) @@ -99,22 +103,52 @@ tcl::namespace::eval punk::args::moduledoc::punkboot { "::punkboot::project_subcommands - dict mapping subcommand name -> Tcl handler script. Consulted by the boot core's default dispatch AFTER the built-ins (tclsh shellspy punk - shell script buildinfo): a declared name extends the launch - surface; a name colliding with a built-in is reported on - stderr and ignored (built-ins cannot be shadowed). The + shell script buildinfo help): a declared name extends the + launch surface; a name colliding with a built-in is reported + on stderr and ignored (built-ins cannot be shadowed). The handler runs in the boot frame with ::argv/::argc already holding the subcommand's arguments - typically a one-line 'package require app-yourapp'. + ::punkboot::project_subcommand_info - optional dict mapping a + declared subcommand name -> an info dict consumed by the + G-032 launcher help/parse wiring. All keys optional: + summary one-line summary shown beside the name in the + top-level 'help' table + argsid punk::args definition id documenting the + subcommand: 'help ' and ' -help' + render it. Without an argsid, ' -help' + stays FULL passthrough to the handler (the app + may do its own -help) and 'help ' shows + the summary line. + package package to require (guarded) before resolving + argsid - typically a moduledoc-style package + carrying the definition + parse boolean (default 0): validate the subcommand's + arguments through argsid BEFORE the handler runs; + a punk::args usage error (stderr, exit 1) stops + dispatch. Degrades to no-validation when + punk::args or the id is unavailable. + A name without an info entry keeps full passthrough + semantics. Help for a registered subcommand appears in the + launcher's tables by registration alone - no dispatch edits. + ::punkboot::launch_defaults - dict with optional keys: noargs subcommand assumed for a bare launch (default: shell) unknownfirst 'script' treats a non-subcommand first - argument as a script invocation (default), or - the name of a known subcommand to receive the - whole arglist (tool-style kits) - - Both dicts default to empty: an undeclared thin main yields + argument that names an existing file (or a + lib:* scriptlib reference) as a script + invocation and refuses anything else with + usage (default; the refusal is G-032 and + degrades to always-treat-as-script without + punk::args), or the name of a known + subcommand to receive the whole arglist + (tool-style kits - these also keep help + FLAGS routed to that subcommand; the 'help' + word remains the launch-surface entry) + + All dicts default to empty: an undeclared thin main yields exactly the punkshell launch behaviour." }] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- @@ -216,6 +250,6 @@ namespace eval ::punk::args::register { ## Ready package provide punk::args::moduledoc::punkboot [tcl::namespace::eval punk::args::moduledoc::punkboot { variable version - set version 0.1.0 + set version 0.2.0 }] return diff --git a/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.3.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.4.0.tm similarity index 75% rename from src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.3.0.tm rename to src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.4.0.tm index d7807171..7755f1de 100644 --- a/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.3.0.tm +++ b/src/vfs/_vfscommon.vfs/modules/punk/args/moduledoc/punkexe-0.4.0.tm @@ -8,7 +8,7 @@ # (C) 2026 # # @@ Meta Begin -# Application punk::args::moduledoc::punkexe 0.3.0 +# Application punk::args::moduledoc::punkexe 0.4.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.3.0] +#[manpage_begin punkshell_module_punk::args::moduledoc::punkexe 0 0.4.0] #[copyright "2026"] #[titledesc {punk executable launch documentation}] [comment {-- Name section and table of contents description --}] #[moddesc {-}] [comment {-- Description at end of page heading --}] @@ -26,17 +26,20 @@ #[keywords module launch subcommand tclsh] #[description] #[para] punk::args definitions documenting the punk executable launch dispatch -#[para] (the subcommand family implemented in the kit boot script src/vfs/_config/punk_main.tcl) +#[para] (the subcommand family implemented in the shared boot core sourced by the +#[para] kit boot script - masters src/vfs/_config/punkboot_core.tcl + punk_main.tcl) # ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++ #*** !doctools #[section Overview] #[para] These definitions are the documentation home for the launch subcommand -#[para] family of the punk executables: tclsh, script, shell, punk, shellspy -#[para] (G-118 item 10). They are authored to be consumable by the future -#[para] launcher help/parse wiring (goal G-032) - rendering, option parsing and -#[para] degradation wiring stay with that goal. Id convention follows the +#[para] family of the punk executables: tclsh, script, shell, punk, shellspy, +#[para] buildinfo, help (G-118 item 10). The launcher help/parse wiring (G-032) +#[para] renders and parses from them at dispatch time: the boot core's help +#[para] surface clones (script)::punkexe / (script)::punkexe::help per kit with +#[para] the live subcommand set, and per-subcommand ids back ' +#[para] -help' and the declared argument gates. Id convention follows the #[para] make.tcl precedent for script-level surfaces: (script)::punkexe and #[para] (script)::punkexe::, with explicit @form -synopsis overrides #[para] since a constructed id is not an invocable command name. @@ -75,9 +78,9 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { variable PUNKARGS namespace eval argdoc { - #Top-level launch dispatch (kit boot script punk_main.tcl, mirrored in - #project_main.tcl for generated projects). Display-first definitions: - #the dispatch itself does not parse via punk::args yet (goal G-032). + #Top-level launch dispatch (thin mains punk_main.tcl/project_main.tcl + #sourcing the shared boot core - G-031). The core's G-032 help/parse + #wiring consumes these definitions at dispatch time (see the Overview). # -- --- --- --- --- --- --- --- --- --- --- --- --- --- lappend PUNKARGS [list { @id -id (script)::punkexe @@ -102,9 +105,28 @@ 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'); any other first argument is treated as a script - invocation (handled as for the 'script' subcommand)." + with no arguments at all the interactive punk shell is + launched ('shell'); 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 - + the refusal degrades to the historic treat-as-script + behaviour when punk::args is unavailable in the kit/context). + + Help (G-032): 'help' or '-help' as the first non-mode + argument renders this usage; 'help ' or + ' -help' renders a subcommand's own usage + (see the 'help' choice below for the carve-outs). A kit + whose thin main routes unknown first arguments to a project + subcommand (a tool-style kit) keeps routing help FLAGS to + that subcommand - its launch surface stays reachable via the + 'help' subcommand word. Project-registered subcommands + appear in the rendered table by registration alone, with + the summaries and definition ids their registrations + declare (see punk::args::moduledoc::punkboot's registration + definition). These built-in launch behaviours are subject + to the thin main's declared launch defaults - the rendered + per-kit table is the authority for a given executable." @form -synopsis " ?packagemode? ?subcommand? ?arg ...?" @leaders -min 0 -max 2 packagemode -type string -optional 1 -default internal -help\ @@ -133,7 +155,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} -choicelabels { + subcommand -type string -optional 1 -choicerestricted 0 -choices {tclsh script shell punk shellspy buildinfo help} -choicelabels { tclsh " Run as a (near) stock tclsh - no punk modules loaded." script @@ -146,6 +168,8 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { " Experimental shellspy command-line processor." buildinfo " 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\ "Subcommand to run. Each subcommand's own contract is documented under its definition id (script)::punkexe::." @@ -183,6 +207,10 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { 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. + 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 + argument keeps the stock all-args-to-::argv semantics above. A scriptfile of the form 'lib:name' is refused with a pointer to the 'script' subcommand: scriptlib resolution is a punk @@ -264,9 +292,12 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { }] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- - #Sibling subcommands: summary-level contracts. Deep punk::args parse - #definitions for these surfaces are G-032's lane ('script' is its - #natural first parsed subcommand). + #Sibling subcommands: summary-level contracts. These are declared + #PASSTHROUGH surfaces (G-032 decision): their arguments belong to the + #script/app they launch, so dispatch never parses them - only the + #first-argument help flag is intercepted (see each -help body). + #buildinfo below is the dispatch-parsed subcommand (exact @values + #contract enforced through punk::args). # -- --- --- --- --- --- --- --- --- --- --- --- --- --- lappend PUNKARGS [list { @id -id (script)::punkexe::script @@ -285,7 +316,11 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { is a usage error - no interactive fallback), or via scriptlib resolution when the argument has the form 'lib:name' (resolved from the scriptlib locations associated with the executable, - including extensionless and shebang'd scripts)." + including extensionless and shebang'd scripts). + + 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 " script ?scriptfile|lib:name? ?arg ...?" @values -min 0 -max -1 scriptfile -type string -optional 1 -help\ @@ -304,7 +339,9 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { "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 - executable is launched with no arguments at all." + executable is launched with no arguments at all. + A help flag (-help etc) as the FIRST argument renders this + usage and exits 0 (G-032)." @form -synopsis " shell ?arg ...?" @values -min 0 -max -1 arg -type any -optional 1 -multiple 1 -help\ @@ -322,7 +359,9 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { interactive shell is launched directly. Commands piped into stdin are supported (e.g: echo 'puts hello' | punk) and stderr is kept clean of launch chatter so exec-style - callers do not misreport failure." + callers do not misreport failure. + A help flag (-help etc) as the FIRST argument renders this + usage and exits 0 (G-032)." @form -synopsis " punk ?arg ...?" @values -min 0 -max -1 arg -type any -optional 1 -multiple 1 -help\ @@ -338,7 +377,9 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { "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 - punkshell)." + punkshell). + A help flag (-help etc) as the FIRST argument renders this + usage and exits 0 (G-032)." @form -synopsis " shellspy ?arg ...?" @values -min 0 -max -1 arg -type any -optional 1 -multiple 1 -help\ @@ -376,12 +417,68 @@ tcl::namespace::eval punk::args::moduledoc::punkexe { stamp_present=0 marks an unstamped kit; fields of absent notions are omitted rather than invented. + Arguments are refused: exit 2 with a one-line refusal plus + this usage as a punk::args error table on stderr (G-032; the + table degrades away when punk::args is unavailable). A help + flag (-help etc) as the first argument renders this usage and + exits 0. + In-process equivalent (no exec): the 'buildinfo' shell command / punk::buildinfo::report." @form -synopsis " buildinfo" @values -min 0 -max 0 }] # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + + #help subcommand: the G-032 launcher help surface. The boot core + #renders per-kit clones of these definitions: (script)::punkexe.launcher + #(the top-level table with the kit's LIVE subcommand set - built-ins + + #project registrations) and (script)::punkexe.launcherhelp (this + #definition with the subject choices replaced by that live set). + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- + 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\ + "Render launch usage on stdout and exit 0 (G-032). + + With no argument: the top-level table - the package modes and + every subcommand this kit's launch surface carries (the + built-in family plus project-registered subcommands, each + with its one-line summary). + + With a subcommand argument: that subcommand's own usage. For + a project-registered subcommand this is the punk::args + definition id its registration declares; a registration + without one gets its registered summary line (such a + subcommand may implement its own -help handling - the + launcher does not intercept its arguments). + + ' -help' renders the same top-level table on kits + whose unknown-first-argument default is 'script'; + ' -help' renders the subcommand usage + (see the top-level definition for the tool-style-kit and + tclsh carve-outs). + + Degradation (G-030 doctrine): when punk::args, these + definitions, or the rendering stack are unavailable in the + kit/context - or PUNKBOOT_PLAIN=1 is set - help degrades to + a plain subcommand list and boot/dispatch never fail on the + help path. + + The subject choices below are the built-in family; in a live + kit the valid (and prefix-resolvable) choices are that kit's + actual subcommand set." + @form -synopsis " help ?subcommand?" + @values -min 0 -max 1 + 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." + }] + # -- --- --- --- --- --- --- --- --- --- --- --- --- --- } #*** !doctools @@ -404,7 +501,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.3.0 + set version 0.4.0 }] return diff --git a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm index 1db630ca..18b81fff 100644 Binary files a/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm and b/src/vfs/_vfscommon.vfs/modules/punk/mix/templates-0.2.0.tm differ diff --git a/src/vfs/_vfscommon.vfs/punkboot/core.tcl b/src/vfs/_vfscommon.vfs/punkboot/core.tcl index 41e20475..eb9d4d87 100644 --- a/src/vfs/_vfscommon.vfs/punkboot/core.tcl +++ b/src/vfs/_vfscommon.vfs/punkboot/core.tcl @@ -11,9 +11,13 @@ #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) extended by the thin main's declared -#project subcommands (G-031 registration model - see the dispatch section -#near the end of this file). +#(tclsh/shellspy/punk/shell/script/buildinfo/help) 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): ' help ?subcommand?', '-help' and +#' -help' render tabled usage from the moduledoc::punkexe +#definitions plus the live registrations, degrading to a plain subcommand +#list when that stack is unavailable. # #The package_mode surface (documented in full by punk::args::moduledoc::punkexe): #an optional FIRST launch argument of the form minted|os|internal|src or any @@ -33,6 +37,11 @@ # to this file's own path) # project_subcommands dict: subcommand name -> handler script (see the # dispatch section; built-ins cannot be shadowed) +# project_subcommand_info dict: subcommand name -> info dict with +# optional keys summary/argsid/package/parse feeding +# the G-032 launcher help + declared-parse wiring +# (see the launcher help section; a name without an +# info entry keeps full passthrough semantics) # launch_defaults dict: noargs / unknownfirst (see dispatch section) # ------------------------------------------------------------------------------ @@ -42,7 +51,7 @@ namespace eval ::punkboot { #boot-core identity (G-031; stamped into kits + reported by the G-025 #buildinfo surfaces) - variable core_version 0.2.1 + variable core_version 0.3.0 } if {![info exists ::punkboot::launch_args]} { namespace eval ::punkboot [list variable launch_args $::argv] @@ -53,6 +62,9 @@ if {![info exists ::punkboot::main_script]} { if {![info exists ::punkboot::project_subcommands]} { namespace eval ::punkboot {variable project_subcommands [dict create]} } +if {![info exists ::punkboot::project_subcommand_info]} { + namespace eval ::punkboot {variable project_subcommand_info [dict create]} +} if {![info exists ::punkboot::launch_defaults]} { namespace eval ::punkboot {variable launch_defaults [dict create]} } @@ -1509,20 +1521,299 @@ if {![info exists ::punkboot::launch_defaults]} { } namespace eval ::punkboot [list variable boot_state $boot_state_dict] + #--------------------------------------------------------------- + #Launcher help + declared-parse machinery (G-032) + #The launch surface documents itself through punk::args (definition ids + #from the punk::args::moduledoc::punkexe family): + # help ?subcommand? tabled usage; the top-level table lists + # built-ins AND project-registered + # subcommands with summaries + # -help same as bare 'help' - only on kits whose + # unknownfirst default is 'script' (a + # tool-style kit routes flags to its own + # processor; its surface stays reachable + # via the 'help' word) + # -help the subcommand's tabled usage when -help + # is its FIRST argument (tclsh: only when + # -help is the SOLE argument - stock + # parity keeps every other dash form in + # ::argv; project subcommands: only when + # the registration declares an argsid) + #Everything is guarded and lazy (the G-030 degradation doctrine): nothing + #here loads punk::args on a normal boot/dispatch path. When punk::args or + #the moduledoc definitions are unavailable (or PUNKBOOT_PLAIN=1 - same + #env hook as make.tcl), help degrades to a plain subcommand list, parse + #gates degrade to the historic switch semantics, and boot never fails. + #The tabled rendering additionally depends on the punk::ansi/textblock + #stack - rendering degrades (minimal errorstyle) independently of parsing. + #The per-kit top-level usage id (script)::punkexe.launcher is cloned from + #the static (script)::punkexe definition at first render, with the + #subcommand choices/choicelabels replaced by this kit's LIVE surface; + #(script)::punkexe.launcherhelp is the same clone of + #(script)::punkexe::help for subject validation. + #--------------------------------------------------------------- + namespace eval ::punkboot { + variable help_flags [list -help --help -h /?] + variable launch_builtin_subcommands [list] + variable launch_subcommands [list] + variable launcher_punkargs_state "" ;#"" unprobed | ok | plain + variable launcher_builtin_summaries [dict create \ + tclsh "Run as a (near) stock tclsh - no punk modules loaded." \ + script "Run a script non-interactively in the punk script environment; honest exit codes." \ + shell "Interactive punk shell (repl)." \ + punk "punk shell launch; piped-stdin friendly." \ + shellspy "Experimental shellspy command-line processor." \ + buildinfo "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)." \ + ] + proc launcher_punkargs_ready {} { + #lazy guarded capability probe, one verdict per process. ok requires + #punk::args AND the moduledoc launch definitions to load and resolve + #with the snapshot actually available in this kit/context. + variable launcher_punkargs_state + if {$launcher_punkargs_state ne ""} { + return [expr {$launcher_punkargs_state eq "ok"}] + } + set launcher_punkargs_state plain + if {[info exists ::env(PUNKBOOT_PLAIN)] && [string is true -strict $::env(PUNKBOOT_PLAIN)]} { + return 0 + } + if {[catch { + package require punk::args + #punk::args (and the punk::ansi/textblock render stack) call + #punk::lib without requiring it (known module-web gap): on + #Tcl 8.6 the choices-grid render path needs punk::lib's + #forward-compat builtins (lpop etc). Guarded - require order + #matters (punk::lib itself hard-requires punk::args, so this + #call-time direction breaks no cycle); a kit without punk::lib + #still probes ok and rendering degrades where it must. + catch {package require punk::lib} + package require punk::args::moduledoc::punkexe + if {[punk::args::get_spec (script)::punkexe] eq ""} { + error "definition (script)::punkexe did not resolve" + } + punk::args::get_spec (script)::punkexe::buildinfo + punk::args::get_spec (script)::punkexe::help + }]} { + return 0 + } + set launcher_punkargs_state ok + return 1 + } + proc launcher_errstyle {} { + #tabled (standard) parse errors when the rendering stack is + #loadable; minimal otherwise - rendering degrades independently of + #parsing (the G-030 split) + catch {package require punk::ansi} + catch {package require textblock} + if {[package provide punk::ansi] ne "" && [package provide textblock] ne ""} { + return standard + } + return minimal + } + proc launcher_subcommand_summary {name} { + #plain-text one-line summary for a known subcommand (built-in table + #or the project registration's declared summary) + variable launcher_builtin_summaries + variable project_subcommand_info + if {[dict exists $launcher_builtin_summaries $name]} { + return [dict get $launcher_builtin_summaries $name] + } + if {[dict exists $project_subcommand_info $name summary]} { + return [dict get $project_subcommand_info $name summary] + } + return "Project-declared subcommand (no summary registered)." + } + proc launcher_live_choices {} { + #this kit's subcommand set in display order: the static definition's + #choice order first (the documented built-in family), then 'help', + #then project registrations in declaration order + variable launch_subcommands + set ordered [list] + catch { + set spec [punk::args::get_spec (script)::punkexe] + foreach c [dict get $spec FORMS _default ARG_INFO subcommand -choices] { + if {$c in $launch_subcommands && $c ni $ordered} { + lappend ordered $c + } + } + } + foreach c $launch_subcommands { + if {$c ni $ordered} { + lappend ordered $c + } + } + return $ordered + } + proc launcher_overview_id {} { + #define (script)::punkexe.launcher: the per-kit top-level usage - + #the moduledoc (script)::punkexe definition with the subcommand + #choices/choicelabels replaced by this kit's live surface. + #Returns the id, or "" when the clone cannot be built. + variable launch_builtin_subcommands + set id (script)::punkexe.launcher + set existing "" + catch {set existing [punk::args::raw_def $id]} + if {$existing ne ""} { + return $id + } + if {[catch { + set spec [punk::args::get_spec (script)::punkexe] + set labels [dict get $spec FORMS _default ARG_INFO subcommand -choicelabels] + set choices [launcher_live_choices] + foreach c $choices { + if {$c eq "help" || $c ni $launch_builtin_subcommands || ![dict exists $labels $c]} { + dict set labels $c " [launcher_subcommand_summary $c]" + } + } + set ov [dict create \ + @id [list -id $id] \ + subcommand [list -choices $choices -choicelabels $labels] \ + ] + punk::args::define [punk::args::resolved_def -override $ov (script)::punkexe] + }]} { + return "" + } + return $id + } + proc launcher_helpdef_id {} { + #define (script)::punkexe.launcherhelp: the moduledoc + #(script)::punkexe::help definition with the subject choices + #replaced by the live subcommand set, so 'help ' validates + #(and prefix-resolves) against what this kit actually offers. + set id (script)::punkexe.launcherhelp + set existing "" + catch {set existing [punk::args::raw_def $id]} + if {$existing ne ""} { + return $id + } + if {[catch { + set ov [dict create \ + @id [list -id $id] \ + subject [list -choices [launcher_live_choices]] \ + ] + punk::args::define [punk::args::resolved_def -override $ov (script)::punkexe::help] + }]} { + return "" + } + return $id + } + proc launcher_plain_help {} { + #degraded help: plain subcommand list built without punk::args + set exebase [file rootname [file tail [info nameofexecutable]]] + set lines [list] + lappend lines "Usage: $exebase ?packagemode? ?subcommand? ?arg ...?" + 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:" + foreach sub [launcher_plain_choices] { + lappend lines [format " %-12s %s" $sub [launcher_subcommand_summary $sub]] + } + lappend lines " ('$exebase help ?subcommand?' shows detail; plain list because the punk::args" + lappend lines " tabled help stack is unavailable in this context, or PUNKBOOT_PLAIN is set)" + return [join $lines \n] + } + proc launcher_plain_choices {} { + #display order without punk::args: documented built-in order, then + #project registrations in declaration order + variable launch_subcommands + set ordered [list] + foreach c [list tclsh script shell punk shellspy buildinfo help] { + if {$c in $launch_subcommands} { + lappend ordered $c + } + } + foreach c $launch_subcommands { + if {$c ni $ordered} { + lappend ordered $c + } + } + return $ordered + } + proc launcher_show_help {{subject ""}} { + #render help to stdout: the top-level launch surface (empty + #subject) or one subcommand's usage. Tabled via punk::args when + #available; degrades to the plain list/summary. Never errors. + variable launch_builtin_subcommands + variable project_subcommand_info + if {[launcher_punkargs_ready]} { + set id "" + if {$subject eq ""} { + set id [launcher_overview_id] + } elseif {$subject eq "help"} { + set id [launcher_helpdef_id] + } elseif {$subject in $launch_builtin_subcommands} { + set id (script)::punkexe::$subject + } else { + #project-registered: guarded require of the declared + #package (if any), then the declared definition id (if any) + if {[dict exists $project_subcommand_info $subject package]} { + catch {package require [dict get $project_subcommand_info $subject package]} + } + if {[dict exists $project_subcommand_info $subject argsid]} { + set id [dict get $project_subcommand_info $subject argsid] + } else { + #help by registration alone: the summary line + puts stdout "$subject - [launcher_subcommand_summary $subject]" + puts stdout "(no punk::args definition registered for this subcommand - if it is an application entry point it may provide its own -help handling)" + return + } + } + if {$id ne "" && ![catch {punk::args::usage $id} out]} { + puts stdout $out + return + } + #fall through to the plain forms on any render failure + } + if {$subject eq ""} { + puts stdout [launcher_plain_help] + } else { + puts stdout "$subject - [launcher_subcommand_summary $subject]" + puts stdout "(plain help: the punk::args tabled help stack is unavailable in this context, or PUNKBOOT_PLAIN is set)" + } + return + } + proc launcher_unknown_first_error {word} { + #G-032 reclassification rule: an unknown first argument that names + #no existing file (and is no lib:* scriptlib reference) is an + #obvious mistake - refuse it with usage on stderr instead of + #silently attempting it as a script. Returns 1 when the refusal + #was emitted (caller exits 1); 0 when punk::args is unavailable + #(caller falls back to the historic script reclassification). + variable launch_subcommands + if {![launcher_punkargs_ready]} { + return 0 + } + set exebase [file rootname [file tail [info nameofexecutable]]] + puts stderr "$exebase: unknown subcommand '$word' (no such subcommand, and no such script file exists)" + set id [launcher_overview_id] + if {$id ne "" && ![catch {punk::args::usage $id} out]} { + puts stderr $out + } else { + 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" + return 1 + } + } + #--------------------------------------------------------------- #Subcommand selection (G-031 registration model) - #Built-ins (tclsh/shellspy/punk/shell/script) ship with this core; a thin - #main may extend the set via ::punkboot::project_subcommands (dict: - #name -> handler script, evaluated in the dispatch below with ::argv/::argc - #holding the subcommand's arguments). Built-in names cannot be shadowed - - #a colliding declaration is reported and ignored. + #Built-ins (tclsh/shellspy/punk/shell/script/buildinfo/help) ship with + #this core; a thin main may extend the set via + #::punkboot::project_subcommands (dict: name -> handler script, evaluated + #in the dispatch below with ::argv/::argc holding the subcommand's + #arguments). Built-in names cannot be shadowed - a colliding declaration + #is reported and ignored. ::punkboot::project_subcommand_info optionally + #carries per-name help/parse metadata (summary/argsid/package/parse - see + #the launcher help section above, G-032). #::punkboot::launch_defaults keys (both optional): # noargs subcommand assumed for a bare launch (default: shell) # unknownfirst 'script' treats a non-subcommand first argument as a # script invocation (default), or the name of a known # subcommand to receive the whole arglist as its arguments #--------------------------------------------------------------- - set builtin_subcommands [list tclsh shellspy punk shell script buildinfo] + set builtin_subcommands [list tclsh shellspy punk shell script buildinfo help] set known_subcommands $builtin_subcommands foreach k [dict keys $::punkboot::project_subcommands] { if {$k in $builtin_subcommands} { @@ -1531,6 +1822,9 @@ if {![info exists ::punkboot::launch_defaults]} { lappend known_subcommands $k } } + #publish for the launcher help machinery (and post-boot probes) + set ::punkboot::launch_builtin_subcommands $builtin_subcommands + set ::punkboot::launch_subcommands $known_subcommands if {[dict exists $::punkboot::launch_defaults noargs]} { set default_noargs [dict get $::punkboot::launch_defaults noargs] } else { @@ -1544,10 +1838,56 @@ if {![info exists ::punkboot::launch_defaults]} { set subcommand [lindex $arglist 0] if {$subcommand in $known_subcommands} { set subcommand_arglist [lrange $arglist 1 end] + #G-032: a help flag as the subcommand's FIRST argument renders that + #subcommand's usage. tclsh: only when it is the SOLE argument (stock + #parity keeps every other leading-dash form - including '-help' with + #further arguments - in ::argv); project subcommands: only when the + #registration declares an argsid (a handler-only registration keeps + #full passthrough - the application may do its own -help handling). + if {[lindex $subcommand_arglist 0] in $::punkboot::help_flags} { + set do_subhelp 0 + if {$subcommand eq "tclsh"} { + if {[llength $subcommand_arglist] == 1} { + set do_subhelp 1 + } + } elseif {$subcommand in $builtin_subcommands} { + set do_subhelp 1 + } elseif {[dict exists $::punkboot::project_subcommand_info $subcommand argsid]} { + set do_subhelp 1 + } + if {$do_subhelp} { + ::punkboot::launcher_show_help $subcommand + exit 0 + } + } } else { set subcommand_arglist $arglist if {[llength $subcommand_arglist]} { + #G-032: a help flag in first position is a launch-surface help + #request on kits whose unknown-first default is 'script'. + #Tool-style kits (unknownfirst names a project subcommand) keep + #routing flags to their processor - their surface stays + #discoverable via the 'help' subcommand word. + if {$subcommand in $::punkboot::help_flags && $default_unknownfirst eq "script"} { + ::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 + } + } + } } else { set subcommand $default_noargs } @@ -1726,8 +2066,20 @@ if {![info exists ::punkboot::launch_defaults]} { #implementation is punk::buildinfo - this arm is the thin #exe-subcommand wrapper. Stdout carries only the report; honest #exit code; no repl fallthrough (G-015-compatible). + #G-032: the argument contract ((script)::punkexe::buildinfo, + #@values -min 0 -max 0) is enforced through punk::args when + #available: any argument earns the historic one-line refusal PLUS + #the tabled usage error on stderr, exit 2 (buildinfo.test pins the + #one-liner and the exit code). The empty-argument fast path never + #touches punk::args; a '-help' first argument was already + #intercepted at selection. if {[llength $subcommand_arglist]} { puts stderr "buildinfo: takes no arguments" + if {[::punkboot::launcher_punkargs_ready]} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid (script)::punkexe::buildinfo} parse_msg]} { + puts stderr $parse_msg + } + } exit 2 } if {[catch {package require punk::buildinfo} errM]} { @@ -1736,6 +2088,41 @@ if {![info exists ::punkboot::launch_defaults]} { } exit [punk::buildinfo::main] } + help { + #G-032: launcher help surface. Bare 'help' renders the top-level + #tabled usage (built-ins + project-registered subcommands with + #summaries); 'help ' renders that subcommand's usage. + #Subject validation parses through the launcherhelp clone when + #punk::args is available (tabled choice errors on stderr exit 1, + #unambiguous prefixes resolve); degraded mode accepts exact names + #only and renders the plain forms. Help goes to stdout, exit 0 + #(G-015 output-cleanliness/exit-code doctrine). + if {![llength $subcommand_arglist]} { + ::punkboot::launcher_show_help + exit 0 + } + if {[::punkboot::launcher_punkargs_ready]} { + set helpdef_id [::punkboot::launcher_helpdef_id] + if {$helpdef_id ne ""} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid $helpdef_id} argd]} { + puts stderr $argd + exit 1 + } + set help_subject "" + catch {set help_subject [dict get $argd values subject]} + ::punkboot::launcher_show_help $help_subject + exit 0 + } + } + #degraded: exact-name subject only + set help_subject [lindex $subcommand_arglist 0] + if {[llength $subcommand_arglist] > 1 || $help_subject ni $::punkboot::launch_subcommands} { + puts stderr "help: expected 'help ?subcommand?' with subcommand one of: [join [::punkboot::launcher_plain_choices] {, }]" + exit 1 + } + ::punkboot::launcher_show_help $help_subject + exit 0 + } default { #a project-declared subcommand (thin-main customization point): the #handler script runs in this frame, ::argv/::argc already hold the @@ -1743,6 +2130,30 @@ if {![info exists ::punkboot::launch_defaults]} { #misdeclared launch_defaults value - the selection above maps every #launch form to a known subcommand. if {[dict exists $::punkboot::project_subcommands $subcommand]} { + #G-032 declared-parse gate: a registration may opt its + #arguments into punk::args validation via + #project_subcommand_info keys parse=1 + argsid (optional + #package requirement). A parse failure is a tabled usage + #error on stderr, exit 1, and the handler is not invoked. + #Degrades to the historic no-validation passthrough when + #punk::args (or the declared id) is unavailable. + if {[dict exists $::punkboot::project_subcommand_info $subcommand parse] + && [string is true -strict [dict get $::punkboot::project_subcommand_info $subcommand parse]] + && [dict exists $::punkboot::project_subcommand_info $subcommand argsid] + && [::punkboot::launcher_punkargs_ready]} { + if {[dict exists $::punkboot::project_subcommand_info $subcommand package]} { + catch {package require [dict get $::punkboot::project_subcommand_info $subcommand package]} + } + set project_argsid [dict get $::punkboot::project_subcommand_info $subcommand argsid] + set project_argsid_ok 0 + catch {set project_argsid_ok [expr {[punk::args::raw_def $project_argsid] ne ""}]} + if {$project_argsid_ok} { + if {[catch {punk::args::parse $subcommand_arglist -errorstyle [::punkboot::launcher_errstyle] withid $project_argsid} parse_msg]} { + puts stderr $parse_msg + exit 1 + } + } + } eval [dict get $::punkboot::project_subcommands $subcommand] } else { puts stderr "main.tcl: no handler for subcommand '$subcommand' (check launch_defaults in the kit's main.tcl) - known subcommands: $known_subcommands"