Compare commits

...

34 Commits

Author SHA1 Message Date
Julian Noble c0412a5660 G-117 notes: archived-goal marker on the G-107 parser reference (lint sweep) 1 week ago
Julian Noble 291d750b7d G-117 proposed: self-describing family runtimes - embedded artifact record + metadata schema v1 1 week ago
Julian Noble 3ca7d1a8ad suite.tcl: materialize manifest.uuid in every fossil checkout (provenance completeness) 1 week ago
Julian Noble a76e73db31 punk-runtime: toml-aware list -remote for the published family artifacts 1 week ago
Julian Noble 6fdeabe1c4 G-116 proposed: suite-built tcltls with zig-built crypto backend (bi-family battery) 1 week ago
Julian Noble 7f88b2d244 templates modpod: build-synced punk-runtime.cmd (platforms/defaults/ordering work) 1 week ago
Julian Noble 49f14060e1 G-103 achieved: runtime kit family - flip, archive, reference sweep (wrap trial PASS) 1 week ago
Julian Noble d81a78f437 modpod template: sync punk-runtime.cmd canonical-platform work 1 week ago
Julian Noble cb650487ec layouts make.tcl: platform_punk boot helper - canonical platform-dir names 1 week ago
Julian Noble d5d4bcd6c1 bootsupport: punk 0.2.6 -> 0.2.7 (help platforms topic) 1 week ago
Julian Noble 640d53f7cb bin/AGENTS: record .cmd-extension invocation guideline for future user-facing docs 1 week ago
Julian Noble a423d840e3 punk-runtime: deterministic ordinal listing order; AGENTS .ps1-twin knowledge corrected 1 week ago
Julian Noble f842b286d5 punk-runtime list -remote: surface server default + active selection 1 week ago
Julian Noble 0ac4058bcd punk-runtime: no-name fetch defaults come from the server's curated defaults.txt 1 week ago
Julian Noble 5d3950799b Platform matrix: win32-ix86 - supported for third-party hosting, buildsuite candidate 1 week ago
Julian Noble 197f55f1b2 punk-runtime list -remote: local column uses the shared candidate filter (ps1) 1 week ago
Julian Noble 2ce3edc390 punk-runtime: 'platforms ?-remote?' action - server platform discovery via punkbin platforms.txt 1 week ago
Julian Noble 457cff1837 G-115 proposed: declarative .vfs composition - toml-defined kit payloads with drop-in preservation 1 week ago
Julian Noble 52fffe02d0 G-067 notes: archived-goal marker on the G-107 evidence reference (lint sweep) 1 week ago
Julian Noble a48f4dd996 Goals: weave 2026-07-22 binary-policy framing - motivations, in-tree operation, punkbin artifact classes 1 week ago
Julian Noble f1a8f6ba48 G-114 proposed: per-platform tm module roots - platform-segregated binary .tm via tcl:™️:path 1 week ago
Julian Noble ba3e44582f cookfs1.9.0: relocate misplaced pkgIndex library to vendorlib_tcl8/win32-x86_64 1 week ago
Julian Noble 3a49aa4c48 punk::platform buildsuite axis; record binary-.tm platform-segregation constraints (G-109) 1 week ago
Julian Noble cae9f36d3e Canonical punkshell platform names: punk::platform module, help topic, tree sync (G-105 groundwork) 1 week ago
Julian Noble d3bfc4484c G-103/G-105 groundwork: punk-runtime -platform surface, help action, no-args usage 1 week ago
Julian Noble e669733f64 G-103 progress: full core-gate evidence refresh recorded (69552 run, 9 baselined) + current artifact emission 1 week ago
Julian Noble 254f615f9b make.tcl outputs: console 0.8.0 (G-106 fallback overhaul) + repl comment refresh into bootsupport and _vfscommon 1 week ago
Julian Noble 9deea90782 G-106 achieved flip: archive move (index entry -> GOALS-archive, detail file -> goals/archive) 1 week ago
Julian Noble 168a98d85f G-106: powershell console-mode fallback overhaul - punk::console 0.8.0, canonical server script, tests (project 0.17.8) 1 week ago
Julian Noble 9651d73b6c G-101 notes: the promised G-103 family payload contract now exists - record the 8.6 target shape 1 week ago
Julian Noble 61cf903dd9 G-103: punk-runtime list/use surface artifact metadata; use materializes -rN artifacts 1 week ago
Julian Noble 5aacc6a02a G-113 proposed: tty-aware make.tcl colour output; interim NO_COLOR agent guidance 1 week ago
Julian Noble 6a92d63cf5 G-103: runtime kit family assembly - kit-family + kit-family-artifacts steps, all members verified 1 week ago
Julian Noble b3cff22a6f goals: G-112<->G-104 make.tcl-surface pairing notes; G-023 reconciled to G-103 kit naming (user-approved) 1 week ago
  1. 48
      CHANGELOG.md
  2. 8
      GOALS-archive.md
  3. 28
      GOALS.md
  4. 88
      bin/AGENTS.md
  5. 1012
      bin/punk-runtime.cmd
  6. 42
      goals/G-004-no-committed-binaries.md
  7. 2
      goals/G-006-prebuilt-artifact-download.md
  8. 9
      goals/G-013-raw-mode-default.md
  9. 3
      goals/G-018-zig-plain-tclsh-kits.md
  10. 30
      goals/G-023-version-named-binaries.md
  11. 3
      goals/G-060-qemu-test-matrix.md
  12. 3
      goals/G-066-pkgindex-tm-repackaging.md
  13. 27
      goals/G-067-module-artifact-channel.md
  14. 6
      goals/G-089-scriptlib-kits-and-modes.md
  15. 7
      goals/G-099-suite-tcl86-buildsuite.md
  16. 12
      goals/G-101-tcl86-kit-container-strategy.md
  17. 9
      goals/G-104-maketcl-buildsuite-surface.md
  18. 61
      goals/G-105-buildsuite-cross-target.md
  19. 46
      goals/G-106-powershell-consolemode-fallback.md
  20. 6
      goals/G-108-buildsuite-debug-tier.md
  21. 17
      goals/G-109-libunknown-manifest-multiname-tm.md
  22. 3
      goals/G-110-sharedlib-extraction-cache.md
  23. 6
      goals/G-112-maketcl-subcommand-rename.md
  24. 31
      goals/G-113-maketcl-tty-aware-colour.md
  25. 51
      goals/G-114-per-platform-tm-roots.md
  26. 45
      goals/G-115-declarative-vfs-composition.md
  27. 43
      goals/G-116-suite-built-tcltls.md
  28. 79
      goals/G-117-self-describing-runtimes.md
  29. 222
      goals/archive/G-103-runtime-kit-family.md
  30. 209
      goals/archive/G-106-powershell-consolemode-fallback.md
  31. 2
      punkproject.toml
  32. 36
      scriptlib/utils/pwsh/README.md
  33. 201
      scriptlib/utils/pwsh/consolemode.ps1
  34. 91
      scriptlib/utils/pwsh/consolemode_enableraw.ps1
  35. 144
      scriptlib/utils/pwsh/consolemode_server.ps1
  36. 244
      scriptlib/utils/pwsh/consolemode_server_async.2ps1
  37. 541
      scriptlib/utils/pwsh/consolemode_server_async.ps1
  38. 266
      scriptlib/utils/pwsh/consolemode_server_async1.ps1
  39. 1
      src/AGENTS.md
  40. 77
      src/bootsupport/modules/punk-0.2.7.tm
  41. 753
      src/bootsupport/modules/punk/console-0.8.0.tm
  42. 4
      src/bootsupport/modules/punk/repl-0.5.2.tm
  43. 77
      src/buildsuites/suite_tcl90/README.md
  44. 190
      src/buildsuites/suite_tcl90/build905.zig
  45. 10
      src/buildsuites/suite_tcl90/build_common.zig
  46. 14
      src/buildsuites/suite_tcl90/build_tclthread/build_tclthread.zig
  47. 39
      src/buildsuites/suite_tcl90/build_tclvfs/build_tclvfs_shared.zig
  48. 14
      src/buildsuites/suite_tcl90/build_tk/build_tk.zig
  49. 22
      src/buildsuites/suite_tcl90/suite.tcl
  50. 156
      src/buildsuites/suite_tcl90/tools/family_artifacts.tcl
  51. 201
      src/buildsuites/suite_tcl90/tools/family_check.tcl
  52. 19
      src/make.tcl
  53. 75
      src/modules/punk-999999.0a1.0.tm
  54. 3
      src/modules/punk-buildversion.txt
  55. 1
      src/modules/punk/AGENTS.md
  56. 747
      src/modules/punk/console-999999.0a1.0.tm
  57. 8
      src/modules/punk/console-buildversion.txt
  58. 978
      src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd
  59. 19
      src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl
  60. 237
      src/modules/punk/platform-999999.0a1.0.tm
  61. 4
      src/modules/punk/platform-buildversion.txt
  62. 4
      src/modules/punk/repl-999999.0a1.0.tm
  63. 19
      src/project_layouts/vendor/punk/basic/src/make.tcl
  64. 1012
      src/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd
  65. 19
      src/project_layouts/vendor/punk/project-0.1/src/make.tcl
  66. 6
      src/runtime/AGENTS.md
  67. 17
      src/runtime/mapvfs.config
  68. 512
      src/scriptapps/bin/punk-runtime.bash
  69. 500
      src/scriptapps/bin/punk-runtime.ps1
  70. 149
      src/tests/modules/punk/console/testsuites/console/psfallback.test
  71. 8
      src/vendorlib_tcl8/README.md
  72. 9
      src/vendorlib_tcl8/freebsd-amd64/README.md
  73. 4
      src/vendorlib_tcl8/freebsd-arm64/README.md
  74. 5
      src/vendorlib_tcl8/freebsd-x86_64/README.md
  75. 4
      src/vendorlib_tcl8/linux-arm/README.md
  76. 4
      src/vendorlib_tcl8/linux-arm64/README.md
  77. 4
      src/vendorlib_tcl8/macosx-arm64/README.md
  78. 10
      src/vendorlib_tcl8/msys-x86_64/README.md
  79. 6
      src/vendorlib_tcl8/win32-ix86/README.md
  80. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/asyncworker_process.tcl
  81. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/asyncworker_thread.tcl
  82. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/cookfs1.9.0.dll
  83. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/fsindex.tcl
  84. 4
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/pages.tcl
  85. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/pkgIndex.tcl
  86. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/pkgconfig.tcl
  87. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/readerchannel.tcl
  88. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/vfs.tcl
  89. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/writer.tcl
  90. 0
      src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/writerchannel.tcl
  91. 8
      src/vendorlib_tcl9/README.md
  92. 9
      src/vendorlib_tcl9/freebsd-amd64/README.md
  93. 4
      src/vendorlib_tcl9/freebsd-arm64/README.md
  94. 5
      src/vendorlib_tcl9/freebsd-x86_64/README.md
  95. 4
      src/vendorlib_tcl9/linux-arm/README.md
  96. 4
      src/vendorlib_tcl9/linux-arm64/README.md
  97. 4
      src/vendorlib_tcl9/macosx-arm64/README.md
  98. 10
      src/vendorlib_tcl9/msys-x86_64/README.md
  99. 6
      src/vendorlib_tcl9/win32-ix86/README.md
  100. 19
      src/vfs/_config/project_main.tcl
  101. Some files were not shown because too many files have changed in this diff Show More

48
CHANGELOG.md

@ -5,6 +5,54 @@ 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.18.8] - 2026-07-22
- `bin/punk-runtime.cmd` `list -remote`: remote-only rows skip support files (artifact metadata tomls etc now ride in the server's sha1sums beside runtimes - same extension set the local candidate filter excludes); the `(= server default)` active marker also recognizes a materialized working copy via its beside-toml artifact identity (the default names an immutable `-r<N>` artifact while the active is typically its working name - exact-name matching alone would never fire post-family). Verified live against the first published family artifacts.
## [0.18.7] - 2026-07-22
- `bin/punk-runtime.cmd`: deterministic listing order everywhere. Investigation of an ordering discrepancy between launch paths found two edition divergences: the `.ps1` twin runs under the INVOKING powershell (pwsh 7) while the `.cmd` routes via wrap-pinned Windows PowerShell 5, and both Hashtable enumeration order AND culture-sensitive `Sort-Object` collation (NLS vs ICU) differ between those editions. All name orderings now use ordinal comparison (ps1) / `LC_ALL=C` (bash), which agree byte-for-byte across pwsh, powershell and bash. Also corrected `bin/AGENTS.md`: the `.ps1` twin is SELF-MATERIALIZED by the polyglot's batch layer (created when missing, `fc`-compared and re-copied when stale) - the previous "refresh both on re-wrap" manual-copy guidance was stale; twin verified regenerating byte-identical after deletion.
## [0.18.6] - 2026-07-22
- `bin/punk-runtime.cmd` `list -remote`: surfaces the selection state - summary lines show the platform's server default (per `defaults.txt`, best-effort fetch) and the locally active runtime (annotated `(= server default)` on match); the active runtime's row carries the local listing's `*` marker and the default's row (local or remote-only) is annotated `(server default)`. Also fixed a latent bash-payload bug the work exposed: CRLF server sha1sums made locally-present runtimes additionally appear as remote-only rows (`\r`-suffixed name failed the existence test; msys grep strips `\r`, bash `read` does not) - punkbin's maintenance script now writes LF sha1sums too.
## [0.18.5] - 2026-07-22
- `bin/punk-runtime.cmd`: the default runtime a no-name `fetch` retrieves now comes from the artifact server's curated root-level `defaults.txt` (`<platform> <runtimename>` per line - a punkbin release decision, updated there in the same change-set that publishes the artifact it points at; validated by punkbin's maintenance script) instead of values baked into the payloads. Works for any `-platform` (the recommendation is per-platform server data); platforms without a recorded default, and servers without the file, get an actionable message; cached-copy fallback on network failure. The baked per-platform defaults were removed from both payloads - one of them (linux-arm) had already drifted from the server's actual artifact name, which is precisely the failure mode the server-side file eliminates.
## [0.18.4] - 2026-07-22
- Platform matrix: `win32-ix86` (32-bit x86 windows) added to `punk::platform` / `help platforms` - status supported, runtime+lib tiers, buildsuite `candidate` (zig can target it; a buildsuite is undetermined - hosting for available third-party runtimes/libs is supported regardless). `vendorlib_tcl8`/`_tcl9` gained the platform dirs; `punk-runtime` detects genuine 32-bit windows hosts (32-bit shells on 64-bit OS keep the x86_64 default); `punk::platform::normalize` folds hand-typed `i386`/`i486`/`i586`/`i686` to `ix86`.
## [0.18.3] - 2026-07-22
- `bin/punk-runtime.cmd` `list -remote` (powershell payload): the local column now uses the shared runtime-candidate filter, so `active.toml`, artifact metadata tomls, stray `.log` files, `.tmp`/build copies and directories no longer appear as local runtimes in the server comparison (bash payload already filtered).
## [0.18.2] - 2026-07-22
- `bin/punk-runtime.cmd`: new `platforms ?-remote?` action - lists local `bin/runtime/*` platform folders (local platform marked), or the platforms the artifact server serves via the server's new root-level `platforms.txt` discovery manifest (raw-file servers have no directory listing; the manifest is now part of the punkbin layout contract, generated by punkbin's maintenance script, and third-party mirrors using the layout carry the same file). Local presence marked, cached-copy fallback, actionable message for pre-convention servers.
## [0.18.1] - 2026-07-22
- `punk::platform` records + `help platforms` gain a `buildsuite` axis (supported/planned/candidate/none): whether the punkshell zig buildsuites produce runtimes for a platform - deliberately separate from the artifact-tree tiers, since punkbin-structured repos can host runtimes built by any mechanism (a platform can be runtime-tier hosted while buildsuite=none, e.g. netbsd/dragonflybsd). openbsd/netbsd/dragonflybsd records now carry the runtime tier (hosted third-party runtimes possible).
## [0.18.0] - 2026-07-22
- Canonical punkshell platform names (platform-folder synchronization survey; G-105 groundwork). New module `punk::platform` 0.1.0: `platforms` (canonical records with status supported/dormant/recognized and artifact-tree tiers), `normalize` (folds Tcl platform-package version-dependent aliases: amd64->x86_64, aarch64->arm64, macos->macosx, macosx arm->arm64), `local ?-tier lib|runtime?` (runtime tier collapses macOS per-arch to the universal `macosx` runtime-store name). New `help platforms` shell topic (punk 0.2.7) renders the canon with the running interpreter marked and the raw platform-package identifiers for comparison. Boot machinery (punkboot platform_generic snips in punk_main.tcl/project_main.tcl/make.tcl) now normalizes via an inline copy of the same mapping (platform_punk). `src/vendorlib_tcl8`/`_tcl9` platform subfolders synchronized to the canon: freebsd-amd64 renamed freebsd-x86_64, new freebsd-arm64/linux-arm64/macosx-arm64 (+ tracked linux-arm), msys-x86_64 marked dormant pending a utility decision. punk-runtime bash platform prongs emit canonical names (aarch64->linux-arm64 with no fetch default yet, arch-aware freebsd/openbsd/netbsd/dragonflybsd).
## [0.17.9] - 2026-07-22
- `bin/punk-runtime.cmd` (G-103/G-105 groundwork): cross-platform staging surface - `fetch`/`list`/`use` accept `-platform <p>` (punkbin platform-dir names; default = local platform, overridable via `PUNK_RUNTIME_PLATFORM` env) for listing/fetching/selecting runtimes of OTHER platforms (cross-build staging and guest provisioning); foreign fetches require an explicit runtime name; `list` flags runtimes whose metadata `target` disagrees with their folder (`!TARGET-MISMATCH`); `run` stays local-only (rejects `-platform`, ignores the env override). New `help` action (full operator reference: actions, options, env vars, examples) and a proper usage block on no-args invocation. Bash payload's FreeBSD platform dir aligned to punkbin's actual `freebsd-x86_64` (was `freebsd-amd64`). Layout-shipped copy refreshed.
## [0.17.8] - 2026-07-22
- punk::console 0.8.0 (G-106): the powershell console-mode fallback (raw mode on twapi-less windows runtimes) is now dependable and quiet. The persistent pwsh/powershell server starts lazily on first raw enable/disable (loading punk::console no longer spawns a process or prints "twapi not present..." to stderr), is shared process-wide, watches the owning process's pid and exits with it (no orphan pwsh processes), and no longer dies mid-session (the old 20s keepalive with no ping sender, plus a listener defect that killed it after the first message, are both fixed). disableRaw now works via the server on runtimes without `-inputmode` (tcl 8.6 path); enableRaw confirms the flip via the live tcl9 `-inputmode` read before returning. The server script resolves from env `PUNK_PS_CONSOLEMODE_SCRIPT`, the project tree, or an embedded copy in the module - kits and unusual cwds no longer depend on argv0-relative luck. `PUNK_PS_CONSOLEMODE_DEBUG=1` surfaces diagnostics. `scriptlib/utils/pwsh` reconciled to the one canonical server script (experiment variants retired, README added).
## [0.17.7] - 2026-07-22
- `bin/punk-runtime.cmd` (G-103): `list` surfaces per-runtime artifact metadata from `<rootname>.toml` records beside the runtimes (variant, tcl patchlevel, revision, piperepl policy, source artifact of a materialized copy); `use <artifact-r<N>-name>` materializes an immutable `-r<N>` artifact into its working name (metadata toml copied alongside) and selects it; `fetch` of an `-r<N>` artifact also retrieves its metadata toml from punkbin (absence tolerated). Runtime-candidate listing now excludes directories and `.log` files in both payloads. Layout-shipped copy refreshed.
## [0.17.6] - 2026-07-21
- punk::mix::cli 0.5.2: the modpod zip build survives a failing `zipfs mkzip` by falling back to `punk::zip::mkzip` (the established path for zipfs-less interpreters), clearing the partial zip mkzip leaves behind and noting the interpreter + upstream ticket on stderr. Motivation: Tcl 8.7 builds predating core fix `c971e6c7c4` (tkt 7d5f1c13089d463e7796, "zipfs mkzip broken on Windows dotfiles") die with "non-unique path name" on dot-prefixed entries — hit by the templates modpod's layout `.fossil-custom` payloads when building under 8.7a6. Fallback-built modpods verified mounting identically under 9.0.3 and 8.7a6.

8
GOALS-archive.md

@ -144,3 +144,11 @@ Acceptance: bin/ presents punk-bits, punk-runtime and punk-tclargs (wrapper twin
### G-107 [achieved 2026-07-21] Buildsuite library test runs: record/gate external library testsuites → detail: goals/archive/G-107-buildsuite-library-tests.md
Scope: src/buildsuites/suite_tcl90/ (per-library test steps in recipe, tools/ runners, tracked dispositioned baselines), src/buildsuites/_build/ (report outputs via existing globs); pattern inherited by later suites (suite_tcl86)
Acceptance: thread and tclvfs run gated with tracked dispositioned baselines, deterministic across two consecutive full runs; tcllib (with tcllibc accelerators engaged) and tklib run in record mode emitting report artifacts whose paths are recorded in this file; a Tk step exists opt-in with a recorded first census; per-library policy is overridable at invocation without recipe edits; a testsuite that fails to complete (no totals) fails its step in every mode; the evidence-summary shape G-103 metadata consumes is documented here.
### G-106 [achieved 2026-07-22] powershell console-mode fallback: maintained raw-mode path for twapi-less runtimes → detail: goals/archive/G-106-powershell-consolemode-fallback.md
Scope: src/modules/punk/console-999999.0a1.0.tm (enableRaw_powershell/disableRaw_powershell + the persistent server lifecycle), scriptlib/utils/pwsh/ (consolemode_server_async.ps1 canonical + experiment-variant reconciliation), ps-script resolution/packaging across launch contexts (argv0-derived pstooldir; kits via the G-089 scriptlib-in-kits interplay), src/make.tcl shell (the verified launch context)
Acceptance: on a twapi-less suite runtime, raw enable/disable work via the fallback from (a) the make.tcl shell launch and (b) a kit / plain-tclsh repl launch, with no stderr noise in normal operation; the server lifecycle is verified - starts once per session, stays up for the session's raw transitions (the 2026-07-20 observed early-shutdown mode diagnosed and fixed), and shuts down with the session leaving no orphan pwsh processes; ps-script resolution no longer depends solely on argv0 parent-dir derivation (works from kits and unusual cwds, with the fallback-to-pwd branch replaced by something principled); scriptlib/utils/pwsh is reconciled to one canonical server script with the experiment variants retired or explicitly labelled; the fallback's role as the no-twapi contingency is documented where G-103's twapi investigation will find it, and a repeatable verification recipe is recorded in this file.
### G-103 [achieved 2026-07-22] runtime kit family from buildsuites: plain / punk / bi kits with attached batteries + artifact metadata → detail: goals/archive/G-103-runtime-kit-family.md
Scope: src/buildsuites/suite_tcl90/ (kit assembly steps in recipe/driver; pattern for later suites), src/vfs/_config + src/runtime/mapvfs.config (as consumers of the new runtime names), punkbin repo layout + metadata (c:/repo/jn/punkbin; compatible repos), src/scriptapps runtime scriptset (punk-runtime list/use - renamed under G-097, achieved 2026-07-21)
Acceptance: suite_tcl90 produces named artifacts for at least plain, punk, and one bi (+Tk) kit; each verified self-contained from a path with no external Tcl visible (info library resolves into the attached zip; package require checks for Thread, vfs + representative vfs::* packages, tcllib module + tcllibc acceleration engaged; bi adds Tk create/destroy) with the checks recorded here; the punk kit demonstrates piperepl active by default and disabled via the documented env opt-out, the plain kit demonstrates stock behaviour (no patch); artifact metadata (variant, versions, target, source provenance) is emitted alongside the binaries in the punkbin layout and surfaced by the runtime scriptset's list/use; the naming scheme for family members is documented, with piperepl-patched runtime executables carrying 'punk' in the name to distinguish them from unpatched (e.g tclsh905punk.exe - the tcl-patchlevel / punk-version / separator questions resolved and the decision recorded here) and mapvfs.config consuming the punk/bi runtimes under the decided names; the 8.6 family variant is explicitly deferred to the G-101 container investigation (static vfs / metakit patching questions).

28
GOALS.md

@ -346,10 +346,6 @@ Detail: goals/G-100-suite-tcl86-tk-tcllib.md
Scope: investigation + decision record under src/buildsuites/suite_tcl86/ (or successor mechanism), TEMP_REFERENCE/metakit + TEMP_REFERENCE/KitCreator (read-only guidance), src/runtime/mapvfs.config ('kit' type consumers), src/make.tcl kit-wrap path (as consumer)
Detail: goals/G-101-tcl86-kit-container-strategy.md
### G-103 [active] runtime kit family from buildsuites: plain / punk / bi kits with attached batteries + artifact metadata
Scope: src/buildsuites/suite_tcl90/ (kit assembly steps in recipe/driver; pattern for later suites), src/vfs/_config + src/runtime/mapvfs.config (as consumers of the new runtime names), punkbin repo layout + metadata (c:/repo/jn/punkbin; compatible repos), src/scriptapps runtime scriptset (punk-runtime list/use - renamed under G-097, achieved 2026-07-21)
Detail: goals/G-103-runtime-kit-family.md
### G-104 [proposed] make.tcl buildsuite surface: list / info / build
Scope: src/make.tcl (buildsuite subcommand group), src/buildsuites/*/ (suite self-description contract: an 'info'/describe affordance per suite - suite.tcl action or manifest record), documentation (make.tcl help text, src/buildsuites README/AGENTS)
Detail: goals/G-104-maketcl-buildsuite-surface.md
@ -358,10 +354,6 @@ Detail: goals/G-104-maketcl-buildsuite-surface.md
Scope: src/buildsuites/suite_tcl90/ (target parameterization of recipe + driver; per-target out prefixes), src/buildsuites/ (layout/naming decision: target is an INVOCATION DIMENSION, suite folders stay version-named - no per-target tree copies), bin/runtime platform-dir naming alignment (punkshell's win32-x86_64-style names vs zig triples), punkbin per-target artifact layout (with G-103 metadata carrying the target), local WSL as the first cross-verification environment
Detail: goals/G-105-buildsuite-cross-target.md
### G-106 [proposed] powershell console-mode fallback: maintained raw-mode path for twapi-less runtimes
Scope: src/modules/punk/console-999999.0a1.0.tm (enableRaw_powershell/disableRaw_powershell + the persistent server lifecycle), scriptlib/utils/pwsh/ (consolemode_server_async.ps1 canonical + experiment-variant reconciliation), ps-script resolution/packaging across launch contexts (argv0-derived pstooldir; kits via the G-089 scriptlib-in-kits interplay), src/make.tcl shell (the verified launch context)
Detail: goals/G-106-powershell-consolemode-fallback.md
### G-108 [proposed] Debug buildsuite tier: tcl::test-enabled runtimes (-dbg<n>) with recorded constrained-test runs
Scope: src/buildsuites/suite_tcl90/ (build905.zig debug knobs + tcl::test wiring, suite.tcl debug variant steps, tools/test_gate.tcl record runs); kit-family debug members coordinate with G-103
Detail: goals/G-108-buildsuite-debug-tier.md
@ -381,3 +373,23 @@ Detail: goals/G-111-modpod-tidy-tests.md
### G-112 [proposed] make.tcl subcommand rename: stage-true vocabulary around the promotion gates
Scope: src/make.tcl (subcommand tables, dispatch gating, help/workflow text), src/project_layouts/vendor/punk/basic/src/make.tcl + src/project_layouts/vendor/punk/project-0.1/src/make.tcl + src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl (layout copies via established sync channels), AGENTS.md + src/AGENTS.md + src/modules/AGENTS.md + src/lib/AGENTS.md + src/vfs/AGENTS.md + src/tests/AGENTS.md + src/tests/shell/AGENTS.md + README.md + src/README.md (workflow references), src/tests/shell/testsuites/punkexe/scriptexec.test + src/tests/shell/testsuites/punkexe/staticruntime.test (as touched)
Detail: goals/G-112-maketcl-subcommand-rename.md
### G-113 [proposed] make.tcl colour output is terminal-aware: SGR only on interactive tty, off when piped
Scope: src/make.tcl (startup colour policy, define_global_ansi), src/AGENTS.md (invocation guidance), src/tests/shell/testsuites/punkexe/ (piped-output characterization if a suite proves feasible); layout make.tcl copies via established sync channels
Detail: goals/G-113-maketcl-tty-aware-colour.md
### G-114 [proposed] Per-platform tm module roots: platform-segregated binary .tm via tcl::tm::path
Scope: src/vfs/_config/punk_main.tcl + src/vfs/_config/project_main.tcl + src/make.tcl (boot tm-path wiring), src/modules/punk/platform-999999.0a1.0.tm (canon names, as consumer), new per-platform tm root trees (layout/naming decision - sibling roots beside src/vendormodules_tclX / src/modules_tclX), src/project_layouts (layout seeding), modpod demonstration artifact, tree READMEs; coordinates with G-109 (manifest target-platform field stays that goal's item)
Detail: goals/G-114-per-platform-tm-roots.md
### G-115 [proposed] Declarative .vfs composition: toml-defined kit payloads with drop-in preservation
Scope: src/make.tcl (vfs assembly), src/runtime/vendorlib_vfs.toml (existing per-package declaration surface - fold/supersede settled in the work), src/vfs/ (per-.vfs declaration files + README), punk::mix machinery as touched, src/project_layouts (seeding for derived projects); coordinates with G-067 (artifact sources), G-006 (consent), G-004 (binary-free committed tree)
Detail: goals/G-115-declarative-vfs-composition.md
### G-116 [proposed] Suite-built tcltls with a zig-built crypto backend: bi-family battery, prebuilt replacement path
Scope: src/buildsuites/suite_tcl90/ (build_tcltls module + crypto-backend build, sources.config + build.zig.zon source records, kit-family bi payload + metadata extension, test_gate record step), src/vfs kit payloads carrying vendored tcltls binaries (current-state reference + recorded disposition only - removal stays G-004-era work), punkbin (as eventual artifact destination via the G-067 library class); consumers punk::imap4 / punk::netbox (package require tls) as verification context
Detail: goals/G-116-suite-built-tcltls.md
### G-117 [proposed] Self-describing family runtimes: embedded artifact record + metadata schema v1
Scope: src/buildsuites/suite_tcl90/ (kit-family staging embeds the record; family_artifacts.tcl schema v1 fields + emission ordering; -Doriginurl/-Dpackager options), tools/family_check.tcl (embedded-record verification), src/scriptapps/bin/punk-runtime.* + bin/punk-runtime.cmd via rewrap ('info' action; schema-tolerant parsing), punkbin AGENTS.md (record relationship + schema/field documentation)
Detail: goals/G-117-self-describing-runtimes.md

88
bin/AGENTS.md

@ -55,9 +55,35 @@ external tool under its own name (`dtplite`, `sdx`, `kettle`), and `getpunk`
(already punkshell-specific; intended future single-download cross-platform entry
point). Policy decided by the user 2026-07-20 (G-096); the remaining pre-policy names
were swept under G-097 (2026-07-21): `punk-bits`, `punk-runtime`, `punk-tclargs` and the
punk- prefixed selfsign experiment scripts. Where launchability convenience warrants it a `.ps1` twin of the generated
`.cmd` ships alongside (windows users may start from cmd.exe or powershell); the twin
is a byte-copy under the other extension - refresh both on re-wrap.
punk- prefixed selfsign experiment scripts.
`.ps1` twin (corrected 2026-07-22 - the twin is SELF-MATERIALIZED, not manually
refreshed): the generated `.cmd`'s batch layer creates `<name>.ps1` beside itself
when missing AND `fc`-compares/re-copies it whenever content differs (powershell
needs a `.ps1` extension for `-File`), so the twin regenerates on any `.cmd` launch
and self-heals after a re-wrap - no manual copy step. The twin is a byte-copy of the
polyglot and is VCS-ignored (a runtime artifact, unlike the committed `.cmd`).
Caveats: a user who only ever launches the `.ps1` directly can ride a stale twin
until the next `.cmd`-path launch heals it; and the two launch paths run DIFFERENT
powershells - `./bin/<name>` in a powershell session resolves the `.ps1` and runs it
under the INVOKING shell (pwsh 7 or powershell 5), while the `.cmd` route is pinned
by the wrap toml (`cmd.exe /c powershell` = Windows PowerShell) - payload code must
be edition-portable and avoid implementation-defined behaviour (e.g. Hashtable
enumeration order differs between the editions; sort explicitly).
INVOCATION GUIDELINE for user-facing documentation (user policy 2026-07-22; no
user-facing guidelines existed yet when recorded - apply to all future README/help/
doc examples involving the polyglot `bin/*.cmd` utilities): show invocations WITH
the `.cmd` extension, e.g. `bin/punk-runtime.cmd list`, even though extensionless
invocation happens to work on windows in some cases. Rationale: extensionless
resolution in powershell prefers the `.ps1` twin, whose direct execution is
ExecutionPolicy-gated (Restricted/AllSigned hosts fail) and runs under the invoking
edition, whereas the `.cmd` always executes and follows the wrap-pinned, policy-
bypassing tested path. The SAME `.cmd` file also runs on unix shells (the polyglot
needs no extension stripping - `./bin/punk-runtime.cmd` from bash/zsh, or
`bash bin/punk-runtime.cmd`), so one form serves all platforms and the only
platform-specific difference left in examples is path separator style
(`bin/punk-runtime.cmd` vs `bin\punk-runtime.cmd`).
### Launch package modes (built punk shells)
@ -76,8 +102,12 @@ The first argument to a built punk shell may be a dash-delimited package mode co
verification against its `sha1sums.txt` - both the powershell payload on windows and the
bash payload on unix verify; a fetch whose checksum fails leaves only a `.tmp`).
`list -remote` compares local runtimes against the server's sha1sums (Same version /
UPDATE AVAILABLE / not-listed, plus remote-only entries; the bash payload falls back to
a cached sha1sums.txt with a warning when the server is unreachable). The artifact
UPDATE AVAILABLE / not-listed, plus remote-only entries; cached sha1sums.txt fallback
with a warning when the server is unreachable). It also surfaces the selection state:
summary lines show the platform's server default (from `defaults.txt`, best-effort)
and the locally active runtime (annotated `(= server default)` when they match), the
active runtime's row carries the same `*` marker the local listing uses, and the
default's row - local or remote-only - is annotated `(server default)`. The artifact
server base url is overridable via `PUNKBIN_URL` (mirrors/testing) in both payloads.
Which runtime `run` launches is the per-machine "active" selection in
`bin/runtime/<platform>/active.toml` (constrained single-key toml `active = "<name>"`,
@ -87,6 +117,54 @@ written by `punk-runtime.cmd use <name>`, marked in `list`, VCS-ignored via the
it errors listing candidates rather than guessing. The first `fetch` sets the active
runtime only when none is recorded.
G-103 runtime-family artifact metadata (both payloads): a runtime may carry a
`<rootname>.toml` metadata record beside it (emitted by the suite_tcl90
`kit-family-artifacts` step; fetched from punkbin alongside `-r<N>`-named artifacts -
absence tolerated for pre-family runtimes). `list` shows a per-runtime summary from it
(variant, tcl patchlevel, revision, piperepl policy, and - on a materialized working
copy - which immutable artifact it came from). `use <artifact-r<N>-name>` MATERIALIZES
the immutable artifact into its WORKING name (the name minus `-r<N>` - what mapvfs and
projects reference), copies the metadata toml alongside, and selects the working name;
`use <workingname>` selects as before. Root-name handling strips only a `.exe` suffix
(dotted tcl patchlevels make generic last-dot stripping wrong for extensionless unix
names). Candidate listing excludes directories and `.txt/.toml/.tm/.tmp/.log` files.
Cross-platform surface (G-105 groundwork; both payloads): `fetch`/`list`/`use` accept
`-platform <p>` where `<p>` is a punkbin platform-DIR name (`win32-x86_64`,
`linux-x86_64`, `macosx`, ... - never zig triples; the buildsuite maps triples to
platform dirs at build time). Resolution: `-platform` arg > `PUNK_RUNTIME_PLATFORM`
env > the detected local platform; validation is shape-only, lowercase (the artifact
server's URL paths are case-sensitive - the server is the truth for what exists).
Foreign-platform folders serve cross-build staging/provisioning: a foreign `fetch`
requires an explicit runtime name (no foreign defaults), `use -platform` manages that
folder's `active.toml`/materialization (the marker travels with the folder when
deployed; unix exec bits are restored at deploy time), and `list` flags a runtime
whose metadata `target` disagrees with the folder it sits in (`!TARGET-MISMATCH`).
`run` is LOCAL ONLY - it rejects a leading `-platform` and ignores the env override
(later `run` args pass through to the runtime untouched). A no-args invocation prints
a short usage block; the `help` action gives the full operator reference (actions,
options, env vars incl `PUNKBIN_URL`, examples). `platforms ?-remote?` enumerates
platform folders: local `bin/runtime/*` dirs (local platform marked), or - with
`-remote` - the platforms the artifact server serves, read from the server's
root-level `platforms.txt` discovery manifest (part of the punkbin layout contract,
generated by punkbin's `src/build_sha1sums.tcl`; third-party mirrors using the layout
carry the same file - raw-file servers have no directory listing, so the manifest IS
the discovery mechanism), with local presence marked and cached-copy fallback;
servers without the manifest get an actionable message.
The default runtime a no-name `fetch` retrieves is NOT baked into the payloads: it
comes from the server's root-level curated `defaults.txt` (`<platform> <runtimename>`
per line - a punkbin RELEASE DECISION, hand-edited there as part of each publication
change-set and validated by punkbin's `src/build_sha1sums.tcl`; mirrors may curate
their own). The lookup keys on the resolved platform, so a no-name
`fetch -platform <p>` works too; platforms without a recorded default, and servers
without the file, produce an actionable name-it-explicitly message (cached-copy
fallback on network failure). Platform names follow the CANONICAL
punkshell platform-dir names defined by the `punk::platform` module and surfaced as
`help platforms` in the punk shell (cpu tokens normalized: amd64->x86_64,
aarch64->arm64; the bash payload's local-platform prongs emit canonical names - the
FreeBSD dir was realigned from `freebsd-amd64` to `freebsd-x86_64` accordingly).
### Interactive verification shells
- Interactive console/repl verification should cover both Tcl generations - behaviour can differ materially (e.g. the Tcl 8.6 windows console channel driver vs the Tcl 9 rewrite). Use a Tcl 8.6-based punk shell (`punksys.exe`) and a current Tcl 9-based punk shell (named for the Tcl release it embeds, e.g. `punk902z.exe` at the time of writing - ask the user which is current rather than assuming). `info patchlevel` in-session confirms the runtime.

1012
bin/punk-runtime.cmd

File diff suppressed because it is too large Load Diff

42
goals/G-004-no-committed-binaries.md

@ -9,6 +9,16 @@ Acceptance: a scan of the committed tree finds no executable binaries (shared li
The ultimate aim is that the committed repository contains no executable binaries. This is a hygiene and reproducibility goal: binaries in version control are opaque to review, bloat the repo, and make the build non-reproducible from source. Today the repo violates this — binary libraries are committed in the VFS folders (`src/vfs/`), `src/vendorlib/`, `src/vendormodules/`, and `src/bootsupport/`. These are present because there is currently no automated way to rebuild or retrieve them; removing them before an alternative exists would break builds on a clean checkout.
Two further motivations recorded 2026-07-22 (user framing):
- **Third-party distribution eligibility**: a binary-free punkshell is a candidate
for distribution platforms that disallow checked-in binaries.
- **The consent promise as a product commitment**: punkshell guards the promise
that binaries reach the user's machine over the network ONLY by explicit user
action/consent unless configured otherwise (the G-006 gate and its G-067
sibling are the enforcement points). A binary-free repo makes the promise
auditable: everything binary is retrieved, and every retrieval is consented.
Zip-based `.tm` modules are a permitted exception to the "no binaries" rule because they are module archives, not executables — but a zip-based `.tm` that embeds an executable (e.g. a native shared library packaged inside the module) is not permitted. The distinction is content, not file extension.
This goal is the integrating outcome of two enabling goals:
@ -40,6 +50,38 @@ A standing repo-wide rule in root `AGENTS.md` (User Preferences) already directs
## Notes
- 2026-07-22 (user framing): achieving this goal must NOT stop binaries
OPERATING in the tree - the no-checkin policy is about the COMMITTED tree
only. Two working modes stay first-class forever: (a) quick local
experimentation with uncommitted binaries dropped into the working tree;
(b) derived projects (dev project.new) with a PERMISSIVE binary policy -
generated projects may commit binaries under their own rules. Consequences
for the Approach: the step-4 scan targets the committed tree (never a
working-tree police); ignore rules added at removal time double as the
drop-in enablement (local binaries stay conveniently uncommitted); and
boot/loading machinery (auto_path platform dirs, tm roots, libunknown,
modpod) must keep supporting in-tree binaries indefinitely - G-114's
same-version drop-in-replacement acceptance is the tm-side expression of
the same requirement. Layout seeding keeps the binary policy per-project
(punkshell strict, derived projects owner-chosen).
- 2026-07-22 (user framing + assessment): EXIT PATHS for the src/vfs/*.vfs
interim exception - the .vfs binary payloads leave the committed tree via
some mix of: (i) suite-built runtime batteries (G-103, achieved 2026-07-22
- see goals/archive/G-103-runtime-kit-family.md - attached images
already shrink kit .vfs payloads toward pure punkshell script payload);
(ii) per-package punkbin LIBRARY artifacts (G-067 channel - buildsuite-built
.tm/pkg libraries as a second punkbin artifact class) declared into kit
assembly; (iii) possibly whole zipped binary-carrying .vfs archives as a
THIRD punkbin artifact class. Assessment 2026-07-22 (agent, user question):
prefer (i)+(ii) as the primary paths - per-package artifacts dedupe across
kits, keep provenance/versioning granular, reuse the platform canon and the
G-103 metadata pattern, and share one consent story; whole-.vfs archives
couple unrelated components and churn on any payload change, so (iii) is
recorded as an OPEN option for release-snapshot/derived-project convenience
rather than the primary mechanism - decide when removal actually forces the
payloads out. A declarative .vfs composition surface (toml-defined payload
pulling from (i)/(ii), preserving drop-in simplicity) is the candidate
glue - see the G-115 proposal (2026-07-22) if adopted.
- Depends on G-005 and G-006. Sequence G-005 ∥ G-006, then G-004.
- The scan in step 4 should distinguish "executable binary" from "zip archive" and from "zip archive containing an executable." A naive extension-based check is insufficient; content inspection (file magic, zip listing) is required.
- The AGENTS.md user-preference rule is the transition guard: it stops agents from adding new binaries during the G-005/G-006 development period while the developer's existing vendor/vfs binary commits are known and intentional.

2
goals/G-006-prebuilt-artifact-download.md

@ -13,7 +13,7 @@ Two source categories:
1. **Default:** a separate, related binary-artifacts repository (sibling to this source repo, not part of it) holding pre-built artifacts for supported platforms.
2. **User-configured:** a user-supplied source URL (internal mirror, local file server, etc.) that overrides the default.
The critical behavioural requirement is **consent gating by default**: the download must not happen silently. A first-time build that silently reaches out to a remote source would be surprising and a network-exfiltration concern. The user must explicitly opt in — via a config flag set before the build, or an interactive prompt at build time — before any download occurs. Once consent is given (e.g. a config flag persisted), subsequent builds for the same artifact set don't re-prompt.
The critical behavioural requirement is **consent gating by default**: the download must not happen silently. A first-time build that silently reaches out to a remote source would be surprising and a network-exfiltration concern. (2026-07-22 user framing: this gate is a PRODUCT COMMITMENT, not just anti-surprise hygiene - punkshell promises that binaries are only retrieved from the network by explicit user action/consent unless configured otherwise. This goal and its G-067 sibling are the enforcement points; a binary-free repo per G-004 is what makes the promise auditable.) The user must explicitly opt in — via a config flag set before the build, or an interactive prompt at build time — before any download occurs. Once consent is given (e.g. a config flag persisted), subsequent builds for the same artifact set don't re-prompt.
This goal is parallel to and independent of G-005. A user with zig uses G-005; a user without zig (or choosing not to build) uses G-006. Either path satisfies G-004's retrieval requirement.

9
goals/G-013-raw-mode-default.md

@ -75,3 +75,12 @@ stays (see inventory item 4).
8.6 parked reader - rejected: the 8.6 console driver parks a cooked ReadConsole
that a raw flip cannot rescue (established 2026-07-05); cooperation is not
achievable there, only fail-fast.
## Notes
- G-106 (archived) hardened the powershell console-mode fallback, removing the
twapi-less-runtime blocker dimension of this goal: raw enable/disable are now
dependable on twapi-less tcl9 runtimes (lazy singleton server, quiet, verified on
the suite runtimes and kit shape; first enable of a session costs ~0.6-0.7s server
spawn - an eager warm-up at repl init is noted there as a future nicety if raw
becomes the default). See goals/archive/G-106-powershell-consolemode-fallback.md.

3
goals/G-018-zig-plain-tclsh-kits.md

@ -62,7 +62,8 @@ This defines one end of a deliberate spectrum of buildable executables:
with the tcl library in its attached zip; stock script/piped invocations,
no punk namespaces) - it remains READILY ACCESSIBLE as a build product of
every suite run (_build/<suite>/out/bin), deliberately NOT showcased or
published as a product. The showcased family lives in G-103, whose 'plain'
published as a product. The showcased family lives in G-103 (achieved 2026-07-22 - see
goals/archive/G-103-runtime-kit-family.md), whose 'plain'
member is plain-for-punk-purposes (no piperepl patch, but batteries -
Thread/tclvfs/tcllib - attached), close enough to plain for our purposes.
make.tcl surfacing of suite builds is G-104. The executable-spectrum framing

30
goals/G-023-version-named-binaries.md

@ -2,8 +2,8 @@
Status: proposed
Scope: src/make.tcl, src/runtime/ (mapping config - see G-024), bin/ (build outputs)
Goal: project builds produce version-named punk executables for tcl 8.6 and tcl 9 as the project version advances - punk8-<major>-<minor>-<patch>.exe / punk9-<major>-<minor>-<patch>.exe per version, punk8-dev.exe / punk9-dev.exe tracking the latest build, and plain punk8.exe / punk9.exe created initially then replaced only when an actual release is tagged - tolerating the disk growth for now.
Acceptance: a project build at the current punkproject.toml version produces punk8-<M>-<m>-<p>.exe and punk9-<M>-<m>-<p>.exe (names derived from the version, not hand-maintained) plus punk8-dev.exe / punk9-dev.exe updated to that same build; rebuilding at an unchanged version refreshes that version's binaries and -dev without touching other versions' outputs; plain punk8.exe / punk9.exe exist and are replaced only by an explicit release step - a normal build never overwrites them; the scheme is declared succinctly via the G-024 toml mapping (no per-version config edits); archival/deletion of accumulated versioned binaries is out of scope with the trigger question recorded in the detail file.
Goal: project builds produce version-named punk executables for tcl 8.6 and tcl 9 as the project version advances, adopting the G-103 kit-artifact naming decision - punk8-<punkshellversion>.exe / punk9-<punkshellversion>.exe with dotted versions (e.g punk9-0.5.0.exe, prerelease punk9-0.5.0b2.exe) per version - plus the policy layer this goal owns: punk8-dev.exe / punk9-dev.exe tracking the latest build, and plain punk8.exe / punk9.exe created initially then replaced only when an actual release is tagged - tolerating the disk growth for now.
Acceptance: a project build at the current punkproject.toml version produces punk8-<version>.exe and punk9-<version>.exe in the G-103-decided dotted form (names derived from the version, not hand-maintained - e.g punk9-0.5.0.exe) plus punk8-dev.exe / punk9-dev.exe updated to that same build; rebuilding at an unchanged version refreshes that version's binaries and -dev without touching other versions' outputs; plain punk8.exe / punk9.exe exist and are replaced only by an explicit release step - a normal build never overwrites them; the scheme is declared succinctly via the G-024 toml mapping (no per-version config edits) and matches the kit-artifact tier of the G-103 naming decision; archival/deletion of accumulated versioned binaries is out of scope with the trigger question recorded in the detail file.
## Context
@ -17,15 +17,25 @@ side; this goal addresses the naming side).
Naming scheme:
- `punk8-<major>-<minor>-<patch>.exe` / `punk9-<major>-<minor>-<patch>.exe` -
produced as the version advances; a rebuild at an unchanged version refreshes
that version's binaries in place.
- `punk8-<punkshellversion>.exe` / `punk9-<punkshellversion>.exe` (dotted, e.g
`punk9-0.5.0.exe`; prerelease `punk9-0.5.0b2.exe`) - produced as the version
advances; a rebuild at an unchanged version refreshes that version's binaries
in place. The version axis is the PUNKSHELL version (the payload), never the
tcl patchlevel - that is metadata's job (G-103 decision).
- `punk8-dev.exe` / `punk9-dev.exe` - always the latest build, refreshed every
build.
- `punk8.exe` / `punk9.exe` - created initially, then replaced only by an
explicit release step when an actual release is tagged. A normal build never
touches them.
Tier mapping (G-103 naming decision, user-approved 2026-07-21): the versioned
names above are that decision's published/archived KIT ARTIFACT form
punk<major>-<punkshellversion>(.exe) verbatim; plain punk8.exe / punk9.exe are
its major-only WORKING names, with this goal supplying their materialization
policy (the explicit release step); -dev is an additional working-name flavour
alongside the decision's recognized dev-matrix style (punk905, punk902z, ...),
which stays available via explicit mapvfs entries.
The `8`/`9` generation split matches the existing dual-generation verification
practice (bin/AGENTS.md: a Tcl 8.6 punk shell and a current Tcl 9 punk shell).
@ -66,3 +76,13 @@ binary-artifacts repository). Revisit when the accumulation actually bites.
version/provenance - the complement of name-encoded versioning once files
are renamed/copied), G-006 (future artifact-store home for archived
versions), G-018/G-019 (other points on the executable-flavour spectrum).
- 2026-07-21: reconciled with the G-103 runtime/kit naming decision
(user-approved reconciliation): version encoding flipped from the original
hyphenated punk9-<major>-<minor>-<patch>.exe draft to the decided dotted
punk<major>-<punkshellversion>.exe form, and the goal is now framed as the
make.tcl/bin-side ADOPTION of that decision's kit-artifact tier plus the
-dev / release-gated-plain-name policy layer (which the decision deliberately
leaves under project-owner control). Naming authority: G-103's NAMING
DECISION section (achieved 2026-07-22 - goals/archive/G-103-runtime-kit-family.md; the
decision text is stable there
flipped).

3
goals/G-060-qemu-test-matrix.md

@ -65,4 +65,5 @@ direction rather than a new ad-hoc location.
- 2026-07-21: concrete cross-platform test item for this matrix - temp-DLL/dir
cleanup after loading binary batteries out of a kit's VFS (Tcl core TIP 741,
windows-framed): per guest, verify extracted temp copies do not accumulate, or
characterize the flaw. Detail + requirement: G-103 Notes.
characterize the flaw. Detail + requirement: G-103 Notes (achieved
2026-07-22 - see goals/archive/G-103-runtime-kit-family.md).

3
goals/G-066-pkgindex-tm-repackaging.md

@ -47,7 +47,8 @@ current goal touches this.
then the inner binary 'load's from that zipfs mount and EXTRACTS to
%TEMP%\TCL<hex>\<dll> - the SAME copy-to-temp machinery as a direct-from-kit dll
load, so the TIP-741 non-cleanup/accumulation flaw applies equally (one temp dir
per binary, persists on exit; see G-103 Notes). Three loading locations all
per binary, persists on exit; see G-103 Notes - achieved 2026-07-22,
archived at goals/archive/G-103-runtime-kit-family.md). Three loading locations all
converge on this one mechanism: (1) dll direct in a kit's zipfs, (2) standalone
.tm on disk, (3) .tm nested INSIDE a kit's zipfs - the nested (zip-in-zip) mount
works IN PLACE with NO pre-extraction of the .tm itself; only the dll load hits

27
goals/G-067-module-artifact-channel.md

@ -38,7 +38,32 @@ and pull (G-065) instead of re-vendoring by hand.
- 2026-07-21 (user): binary-bearing zip-based .tm (modpod) are an intended publish
payload here - single-file downloadable libraries carrying manifest + binary +
script. Consuming-side load extracts the binary to %TEMP%\TCL* with the TIP-741
non-cleanup flaw on windows (characterized - G-066/G-103 Notes). Single-lowercase-
non-cleanup flaw on windows (characterized - G-066 Notes and G-103, achieved
2026-07-22 - see goals/archive/G-103-runtime-kit-family.md). Single-lowercase-
name .tm (TIP 590) are plain-tclsh loadable; multi-require-name .tm need libunknown
(punkshell-only, must be documented). Generation + multi-name discovery detail:
G-066 Notes.
- 2026-07-22 (from archived G-103 - see goals/archive/G-103-runtime-kit-family.md):
the family runtime -rN artifacts + toml metadata are PUBLISHED (2026-07-22,
user-directed; punkbin b5c258f pushed): tclsh9.0.5-r1/-punk-r1/-punk-bi-r1
.exe+.toml in win32-x86_64/, sha1sums regenerated, defaults.txt flipped to
tclsh9.0.5-punk-r1.exe in the same change-set (the recorded release process,
exercised for real). r1 is now immutable - republishing those (patchlevel,
variant) pairs needs -Dfamilyrev=2. Live-verified: no-name fetch resolves the
new default with metadata; list -remote marks the default row and recognizes
the materialized active via its toml artifact identity. These runtime
artifacts are punkbin's FIRST metadata-carrying class; this goal's library
class follows the same per-platform + toml pattern.
- 2026-07-22 (user framing): the zig BUILDSUITES are a producing source for this
channel - libraries the suites build (thread/tk/tcltls-class) get packaged as
per-platform .tm/pkg artifacts when they are not going directly into a kit, and
published to punkbin, which grows from a runtimes-only repo into MULTIPLE
ARTIFACT CLASSES (runtimes + module/library artifacts; a possible third class -
whole zipped binary .vfs archives - is recorded as open in G-004 Notes with a
prefer-per-package assessment). Library artifacts follow the platform canon
(punk::platform dir names; one platform per file per G-114's segregation
rationale) and carry G-103-style metadata (versions, provenance uuids,
toolchain, test evidence per G-107, achieved - see goals/archive/G-107-buildsuite-library-tests.md). Retrieval-target interplay with G-004: fetches
land in working-tree vendor areas - in punkshell itself ignore rules keep them
UNCOMMITTED (the committed tree stays binary-free; the promise stays auditable),
while derived projects commit them or not per their own binary policy.

6
goals/G-089-scriptlib-kits-and-modes.md

@ -56,3 +56,9 @@ Findings from the 2026-07-18 lib: extensionless work (punk::path 0.3.0):
archive would violate the no-new-binaries rule (G-004); if kit-dispatch
coverage is wanted, generate a throwaway kit at test time or defer until
G-005/G-006 tooling exists.
- G-106 (archived) removed one former consumer of this goal's delivery mechanism:
the powershell console-mode server script no longer depends on scriptlib being
present in kits - punk::console carries an embedded copy as the resolution floor
(a resolvable scriptlib/utils/pwsh file still wins when present, so shipping
scriptlib remains dev-friendly, just not required for raw mode). See
goals/archive/G-106-powershell-consolemode-fallback.md.

7
goals/G-099-suite-tcl86-buildsuite.md

@ -53,7 +53,12 @@ later.
## Notes
- 2026-07-21 (user): SEQUENCING CONFIRMED - G-097 (achieved 2026-07-21) then G-103 precede this arc; the
- 2026-07-21 (user): SEQUENCING CONFIRMED - G-097 (achieved 2026-07-21) then G-103 (achieved 2026-07-22 - see goals/archive/G-103-runtime-kit-family.md) precede this arc - BOTH LANDED, the arc is unblocked; the
suite_tcl86 fork is taken after the kit-family assembly steps exist in suite_tcl90
so the 8.6 suite is born expressing family products (and G-101 inherits a concrete
payload contract).
- G-106 (archived) recorded a verification item for this arc's twapi-less 8.6
runtimes: the powershell console-mode fallback's disableraw-via-server path (the
branch runtimes without -inputmode rely on to restore cooked mode) is coded but
only tcl9-verified - run the G-106 recipe against a suite-built 8.6 runtime when
one exists. See goals/archive/G-106-powershell-consolemode-fallback.md (Notes).

12
goals/G-101-tcl86-kit-container-strategy.md

@ -25,7 +25,8 @@ zip-wrap). 8.6 has no zipfs, so a kit needs a vfs container and a boot mechanism
make.tcl's existing 'kit' wrap type drives sdx/mk4-style wrapping; whichever
container is chosen must either fit that path or come with a documented successor.
The G-103 runtime kit family (plain/punk/bi kits with thread + tclvfs + tcllib
The G-103 runtime kit family (achieved 2026-07-22 - see
goals/archive/G-103-runtime-kit-family.md; plain/punk/bi kits with thread + tclvfs + tcllib
batteries in the attached container) extends to 8.6 THROUGH this investigation: the
user notes (2026-07-20) that in the 8.6 case tclvfs may have to be STATIC and there
may be metakit-specific patching - both are part of this goal's assessment, and the
@ -58,3 +59,12 @@ wrap), and only worth activating once G-099/G-100 land.
those copies is a known wart (Tcl core TIP 741, windows-framed). This goal's
"loading binary dlls from or via the container" acceptance should characterize
that cleanup per platform - TIP 741 detail + requirement in G-103 Notes.
- 2026-07-22: the G-103 family attached-image contract this goal was promised as
its concrete payload target now EXISTS (G-103 Progress 2026-07-22; archived): a family
runtime's image is tcl_library/ at the container root position the boot
searches, tm tree at tcl9/<ver> (8.6: tcl8/<ver>), batteries at lib/<pkg>
with installed-shape pkgIndexes + dlls at bin/, and a one-line stock
auto_path hook at lib/pkgIndex.tcl. The 8.6 container investigation targets
delivering THAT payload shape (adjusted for 8.6 boot: no zipfs /app anchor -
the metakit/cookfs/zipvfs mount point plays the /app role and the hook
mechanism must be re-verified against 8.6's tclPkgUnknown).

9
goals/G-104-maketcl-buildsuite-surface.md

@ -29,9 +29,14 @@ separate subsystem (kit wrapping) but the same ergonomic family - a candidate to
execute alongside this goal.
Relationships: G-096 (achieved 2026-07-20 - suite.tcl driver + copy-and-tweak workflow this fronts),
G-102 (achieved 2026-07-21 - driver shape settled; the info/build contract holds), G-103 (family
G-102 (achieved 2026-07-21 - driver shape settled; the info/build contract holds), G-103 (achieved 2026-07-22 - see goals/archive/G-103-runtime-kit-family.md; family
products appear in info output), G-105 (target dimension forwarded when it exists).
## Notes
(none yet)
- 2026-07-21 (user-approved pairing): G-112 (stage-true subcommand rename) edits the same
make.tcl subcommand surface - SUBOPTS/SUMMARIES/HELPTEXTS tables, help/workflow output.
Ordering: G-112 lands first, or the two run together as one make.tcl-surface window, so
this goal's buildsuite group and workflow text are written once in the renamed
vocabulary (the sibling-ergonomics 'vfs' wishes above become 'bake'-named under G-112).
Mirror note in G-112.

61
goals/G-105-buildsuite-cross-target.md

@ -34,7 +34,7 @@ INVERTED):
Relationships: G-060 (qemu cross-platform matrix - WSL is the cheap first rung;
qemu covers what WSL cannot), G-099/G-100 (the 8.6 suite inherits the target
dimension), G-102 (achieved 2026-07-21 - driver shape settled; target becomes a build-graph dimension), G-103
dimension), G-102 (achieved 2026-07-21 - driver shape settled; target becomes a build-graph dimension), G-103 (achieved 2026-07-22 - see goals/archive/G-103-runtime-kit-family.md)
(family x target = the punkbin artifact matrix), G-006 (punkbin artifact consent).
## Notes
@ -63,3 +63,62 @@ dimension), G-102 (achieved 2026-07-21 - driver shape settled; target becomes a
is fully characterized (confirmed flaw, both direct-kit and modpod-.tm binary
loads - G-103/G-066 Notes); the linux expectation stays no-accumulation
(unlink-while-mapped) but is UNVERIFIED until a runtime exists.
- 2026-07-22: punk-runtime grew the cross-platform staging surface this goal
will consume (user-approved; details in the archived G-103 Progress
2026-07-22):
fetch/list/use -platform <punkbin platform-dir name> with local-platform
default (PUNK_RUNTIME_PLATFORM env override), foreign fetch name-required,
list !TARGET-MISMATCH integrity flag from the G-103 metadata target field,
run local-only. Platform-dir naming alignment progress: bash payload's
FreeBSD dir aligned to punkbin's actual freebsd-x86_64 (was freebsd-amd64).
Still open for this goal's alignment item: the zig-triple <-> platform-dir
mapping itself, openbsd-amd64 (payload) vs the x86_64 convention, and a
windows-arm default for the ps1 payload once punkbin carries such a folder.
- 2026-07-22 CANONICAL PLATFORM NAMES SETTLED (user-directed survey of punkbin vs
punkshell platform folders; resolves this goal's "platform-dir naming alignment"
item on the punkshell side - the zig-triple <-> platform-dir mapping remains this
goal's build-time work). Canon = punk::platform module (src/modules/punk/
platform-999999.0a1.0.tm; 'help platforms' topic): <os>-<cpu> punkshell
platform-dir names with cpu normalized amd64->x86_64, aarch64->arm64 (arm =
32-bit; on macosx arm folds to arm64), os macos->macosx; special universal
'macosx' for the runtime tier (punkbin + bin/runtime keep ONE macOS folder -
make.tcl convention blessed), per-arch macosx-x86_64/macosx-arm64 for lib trees.
Supported: win32-x86_64, linux-x86_64, linux-arm64, linux-arm, macosx,
macosx-x86_64, macosx-arm64, freebsd-x86_64, freebsd-arm64. Dormant:
msys-x86_64 (utility under user review). Recognized (no artifacts):
openbsd/netbsd/dragonflybsd-x86_64. Tcl's platform package stays the RAW
identifier (version-varying outputs: 1.1.x renamed macos, aarch64 passthrough);
punk::platform::normalize is the folding layer; the punkboot platform_generic
snips (punk_main/project_main/make.tcl) gained an inline platform_punk
normalization (kept in sync by comment contract). vendorlib_tcl8/9 platform
subfolders synchronized to canon (freebsd-amd64 -> freebsd-x86_64 rename;
freebsd-arm64/linux-arm64/macosx-arm64 added; stray untracked macosx-arm
empties removed). punk-runtime prongs emit canon (linux-arm64 has NO fetch
default until punkbin carries that folder). FLAGGED for user decisions:
(a) punkbin linux-arm holds tclkit-902-Linux64-arm-dyn which by its name is an
arm64 build - recommend a linux-arm64 punkbin folder at next arm publish
(artifacts immutable, no moves); (b) msys-x86_64 disposition; (c) vendored
platform-1.0.19 vs core 1.1.x re-vendor (normalization makes punkshell robust
either way - upgrade optional); (d) src/vfs stays UNcategorized by platform
(mapvfs mappings + purpose-names carry the platform axis) - acceptable now;
recommend revisiting when G-105 cross-wraps land (a mapvfs platform column per
make.tcl's existing x-platform TODO beats restructuring the .vfs tree).
- 2026-07-22 addendum (user review): punk::platform records gained a
'buildsuite' axis (supported|planned|candidate|none) - whether OUR zig
buildsuite system produces runtimes for a platform - deliberately separate
from the tiers axis, because punkbin (or third-party repos using the same
structure) can host runtimes built by any mechanism: a platform can be
runtime-tier hosted while buildsuite=none (netbsd/dragonflybsd-class
examples). Current values: win32-x86_64 supported (suite_tcl90),
linux-x86_64 planned (this goal), linux-arm64/linux-arm/macosx/freebsd-*
candidate, remainder none. This goal updates the field as targets land.
- 2026-07-22 canon addendum (user): win32-ix86 added to the punk::platform
matrix - status supported, tiers runtime+lib, buildsuite CANDIDATE (zig can
target 32-bit windows but a buildsuite is undetermined; hosting for
available third-party runtimes/libs is wanted regardless - the
hosting-vs-building axis working as intended). vendorlib_tcl8/9 gained the
win32-ix86 dirs per the tree contract; punk-runtime local detection returns
win32-ix86 on genuine 32-bit windows hosts (ps1: PROCESSOR_ARCHITECTURE x86
without ARCHITEW6432 - a 32-bit shell on a 64-bit OS keeps x86_64 since the
runtime store serves what the OS runs; bash: i*86 machine in the MINGW32/
CYGWIN prongs); normalize folds hand-typed i386/i486/i586/i686 to ix86.

46
goals/G-106-powershell-consolemode-fallback.md

@ -1,46 +0,0 @@
# G-106 powershell console-mode fallback: maintained raw-mode path for twapi-less runtimes
Status: proposed
Scope: src/modules/punk/console-999999.0a1.0.tm (enableRaw_powershell/disableRaw_powershell + the persistent server lifecycle), scriptlib/utils/pwsh/ (consolemode_server_async.ps1 canonical + experiment-variant reconciliation), ps-script resolution/packaging across launch contexts (argv0-derived pstooldir; kits via the G-089 scriptlib-in-kits interplay), src/make.tcl shell (the verified launch context)
Goal: the powershell console-mode fallback - a persistent pwsh/powershell named-pipe server driven by punk::console when twapi is absent - is a MAINTAINED, dependable raw-mode path for twapi-less runtimes (the suite-built shells and kits, until/unless zig-built twapi lands): quiet in normal operation, reliable across the session lifecycle, and resolvable from every supported launch shape rather than only argv0-relative luck.
Acceptance: on a twapi-less suite runtime, raw enable/disable work via the fallback from (a) the make.tcl shell launch and (b) a kit / plain-tclsh repl launch, with no stderr noise in normal operation; the server lifecycle is verified - starts once per session, stays up for the session's raw transitions (the 2026-07-20 observed early-shutdown mode diagnosed and fixed), and shuts down with the session leaving no orphan pwsh processes; ps-script resolution no longer depends solely on argv0 parent-dir derivation (works from kits and unusual cwds, with the fallback-to-pwd branch replaced by something principled); scriptlib/utils/pwsh is reconciled to one canonical server script with the experiment variants retired or explicitly labelled; the fallback's role as the no-twapi contingency is documented where G-103's twapi investigation will find it, and a repeatable verification recipe is recorded in this file.
## Context
User verification 2026-07-20 on the freshly built tclsh90spr (piperepl
variant from G-096, achieved 2026-07-20): with `tcl::tm::add .../modules` + `package require repl; repl::init;
repl::start stdin`, basic punk features are functional on the bare suite runtime -
except raw mode, which is twapi-reliant. Launched instead via
`./tclsh90spr .../src/make.tcl shell`, the powershell fallback engaged and raw mode
WORKED - modulo consolemode_server_async.ps1 noise and an early server shutdown.
That working-but-rough state is what this goal hardens. User direction: "we would
like to look at maintaining this powershell fallback - especially if we can't get
zig-based twapi builds going."
Mechanism today (punk::console): when twapi is absent, enableRaw/disableRaw are
aliased to *_powershell variants that talk over a named pipe to a persistent
`consolemode_server_async.ps1` process (templated per-console id; pwsh.exe probed
first, then powershell.exe). Known weak points at goal creation:
- Resolution: pstooldir = argv0's grandparent + /scriptlib/utils/pwsh, else [pwd] -
works for `make.tcl shell` (argv0 = src/make.tcl -> repo root) and little else by
construction. Kits need the script carried/resolved properly (G-089 scriptlib in
kits).
- Lifecycle: observed early shutdown of the server mid-session; startup noise on
stderr ("twapi not present, using persistent powershell process: ..." plus ps1
output).
- Hygiene: scriptlib/utils/pwsh holds several sibling experiments
(consolemode_server.ps1, *_async1.ps1, *_async.2ps1, consolemode.ps1,
consolemode_enableraw.ps1) with no marking of which is canonical.
Relationships: G-103 (bi-kit twapi under the zig-only policy is
needs-investigation - THIS fallback is the mitigation; if zig-built twapi proves
impractical the fallback becomes the primary raw path on suite runtimes and this
goal's priority rises), G-013 (raw mode as the repl default ultimately requires raw
mode to be dependable on twapi-less runtimes), G-089 (scriptlib in kits - the
delivery vehicle for the ps script), G-061-era console-context findings (agent
console-context traps around attached consoles apply to verifying this).
## Notes
(none yet)

6
goals/G-108-buildsuite-debug-tier.md

@ -44,9 +44,11 @@ TCL_COMPILE_STATS similarly must be absent (not =0) for release performance.
-mode record with distinct artifact names (e.g. tclcore-dbg1.summary/.log);
never part of the default gate composite; a run that produces no totals
still fails its step (G-107 semantics).
- Kit family debug members follow G-103's family definition; this goal only
- Kit family debug members follow G-103's family definition (achieved
2026-07-22 - see goals/archive/G-103-runtime-kit-family.md); this goal only
requires the feasibility outcome plus naming convention (-dbg<n>) — full
family production may land with/after G-103.
family production landed under G-103 (kit-family/kit-family-artifacts
suite steps) - debug members extend that settled shape.
- Debug tiers are opt-in build invocations (-D flags / named steps), not new
copy-and-tweak suite trees, unless implementation shows a variant tree is
cleaner (sources.config precedent).

17
goals/G-109-libunknown-manifest-multiname-tm.md

@ -55,3 +55,20 @@ mount path - the 8.6 leg of acceptance), TIP 590 (lowercase naming), G-110
## Notes
- 2026-07-21: drafted and approved (user: "approved - apply both").
- 2026-07-22 (platform-canon survey discussion; user experience recorded):
binary-bearing .tm modpods are PER-PLATFORM artifacts, and multi-platform
"fat" .tm files are the wrong shape - the user tried a multi-platform thread
.tm in the past and found it unworkable: (a) downloads far larger than any
one consumer needs, and (b) same-version shadowing - one name-version
resolves to ONE file under tm handling with no arch awareness, so a user
cannot drop in a replacement .tm carrying their platform when a same-version
copy is already on the path. Platform segregation for binary .tm therefore
has to happen via tcl::tm::path - PER-PLATFORM TM ROOTS selected by the boot
for the running platform (punk::platform canon names). NOTE the structural
constraint: platform dirs canNOT nest inside an existing tm root - a
subdirectory of a tm path is a NAMESPACE component (foo/bar-1.tm = foo::bar),
so per-platform roots must be sibling trees registered separately (layout
naming TBD when the need lands, e.g vendormodules_tcl9-<platform>/ siblings
or a byplatform/<platform>/ parent). Candidate defensive extra for THIS
goal's manifest format: a declared target-platform field, letting libunknown
skip/refuse wrong-platform binary .tm even when someone shares a path.

3
goals/G-110-sharedlib-extraction-cache.md

@ -75,7 +75,8 @@ prototype); (2) a suite core patch to the zipfs load path
loads use the cache; (3) upstream conversation (a third option next to
TIPs 741/709).
Relationships: G-103 (family runtimes are the heaviest consumer - every kit
Relationships: G-103 (achieved 2026-07-22 - see
goals/archive/G-103-runtime-kit-family.md; family runtimes are the heaviest consumer - every kit
battery load), G-101 (Metakit 8.6 kits share the copy-to-temp shape),
G-066/G-067/G-109 (binary modpod .tm artifacts), G-105/G-060 (per-platform
characterization of the existing behaviour), Tcl TIPs 741/709.

6
goals/G-112-maketcl-subcommand-rename.md

@ -70,3 +70,9 @@ act rather than an input folder.
build-reproducibility bug.
- Artifact provenance carrier: kits reference the committed tree (punkorigin.toml / kit
metadata direction, G-027 interim carrier) - VCS stays the payload ledger.
- 2026-07-21 (user-approved pairing): G-104 (make.tcl buildsuite surface) edits the same
make.tcl subcommand surface - SUBOPTS/SUMMARIES/HELPTEXTS tables, help/workflow output.
Ordering: this goal lands first, or the two run together as one make.tcl-surface
window, so G-104's buildsuite group and the workflow text are written once in the
stage-true vocabulary. G-104's sibling-ergonomics note still reads `make.tcl 'vfs'` -
that text joins this goal's rename sweep when both land. Mirror note in G-104.

31
goals/G-113-maketcl-tty-aware-colour.md

@ -0,0 +1,31 @@
# G-113 make.tcl colour output is terminal-aware: SGR only on interactive tty, off when piped
Status: proposed
Scope: src/make.tcl (startup colour policy, define_global_ansi), src/AGENTS.md (invocation guidance), src/tests/shell/testsuites/punkexe/ (piped-output characterization if a suite proves feasible); layout make.tcl copies via established sync channels
Goal: make.tcl emits ANSI SGR colour only when its output goes to an interactive terminal. Piped/redirected runs (agent harnesses, CI, log capture) produce SGR-free output with no caller action required; interactive terminal runs keep full colour by default; explicit env overrides work in both directions.
Acceptance: With NO_COLOR unset, representative informational and build subcommands (at minimum check, help, workflow, and one build command such as modules) run with stdout redirected to a file produce zero ESC (0x1B) bytes on stdout and stderr, verified on tclsh 9.x and a repo punk kit. The same commands on a real interactive terminal retain colour (manual verification, Windows console plus one unix tty). NO_COLOR=1 still suppresses all colour; a documented force-colour override re-enables colour on piped output. The Tcl 8.6 probe-fallback policy is documented and behaves as decided. The interim src/AGENTS.md NO_COLOR bullet is revised to describe the implemented behaviour.
## Context
Agents drive `tclsh src/make.tcl ...` with captured/piped output on every build-task closeout; today that output carries raw SGR sequences unless the caller remembered NO_COLOR=1, which no AGENTS doc mentioned before the interim bullet (see Notes). Humans at a terminal want the colour. The single existing switch (::punk::console::colour_disabled, set at make.tcl startup from NO_COLOR) is honoured dynamically by punk::ansi a+/a and by the define_global_ansi fallback branch, so auto-detection only needs to set that same variable from a tty probe at startup - no downstream changes.
## Approach
- Startup policy in make.tcl, evaluated before any output: NO_COLOR set -> colour off; force-colour env (name settled in the work; FORCE_COLOR is the de-facto standard) -> colour on; otherwise probe stdout and enable colour only when it is a terminal.
- Probe technique: channel-config key presence on stdout, mirroring the stdin_is_interactive precedent (chan configure key test; no package needed, core Tcl only).
- Key the decision on stdout (primary output channel); stderr colour follows the same switch for simplicity - agents capture both anyway, and the force override covers the user-redirecting-stdout case.
- Tcl 8.6 has no reliable probe (see make.tcl:316 comment); fail direction for colour is OFF (safe for agents), unlike stdin's ON. Document; reconsider only if 8.6 interactive colour matters in practice.
- Out of scope: the REPL launched by `make.tcl shell` (shell colour is the shell's own concern - colour on/off + NO_COLOR there).
## Alternatives considered
- Guidance-only (agents always set NO_COLOR=1) - rejected as the sole fix: relies on every harness remembering; kept as the documented explicit override and landed as interim guidance.
- Post-hoc SGR stripping in harnesses - rejected: pushes the burden onto every consumer.
- Per-channel colour policy (stderr coloured when it alone is a tty) - more faithful but more state; default is the stdout-keyed single switch, settled in the work.
## Notes
- 2026-07-22: interim src/AGENTS.md Work Guidance bullet landed (agents set NO_COLOR=1 for captured make.tcl runs) ahead of this goal's implementation; revise it to describe the auto-detection + overrides when this goal lands (tracked in Acceptance).
- Existing machinery: NO_COLOR -> colour_disabled (make.tcl:5-7); punk::ansi honours colour_disabled dynamically with a -forcecolour in-module override (src/modules/punk/ansi-999999.0a1.0.tm); PUNKBOOT_PLAIN is the env-switch precedent (make.tcl:1655); stdin probe precedent at make.tcl:319.
- Related: G-030 (achieved - prompt-free flags + the stdin probe this mirrors), G-011 (console stderr semantics - loose relation), G-104/G-112 (both touch make.tcl subcommand surfaces - coordinate if either goes active first).
- make.tcl interface is product surface (root AGENTS.md): this behaviour change ships with a punkproject.toml bump (patch) + CHANGELOG entry.

51
goals/G-114-per-platform-tm-roots.md

@ -0,0 +1,51 @@
# G-114 Per-platform tm module roots: platform-segregated binary .tm via tcl::tm::path
Status: proposed
Scope: src/vfs/_config/punk_main.tcl + src/vfs/_config/project_main.tcl + src/make.tcl (boot tm-path wiring), src/modules/punk/platform-999999.0a1.0.tm (canon names, as consumer), new per-platform tm root trees (layout/naming decision - sibling roots beside src/vendormodules_tclX / src/modules_tclX), src/project_layouts (layout seeding), modpod demonstration artifact, tree READMEs; coordinates with G-109 (manifest target-platform field stays that goal's item)
Goal: binary-bearing .tm modules (modpods) are distributed ONE PLATFORM PER FILE and installed under per-platform TM ROOTS that the boot registers only for the running platform (canonical punk::platform names) - so no consumer downloads other platforms' binaries, a user can drop in their platform's copy of a same-name-version module without same-version shadowing, and wrong-platform .tm files present on a shared or synced tree are never registered or loaded.
Acceptance: the root layout + naming decision is recorded in this file (including the structural constraint that platform dirs cannot nest inside an existing tm root - tm subdirectories are namespace components - so per-platform roots are sibling trees); the boot (punk_main.tcl, project_main.tcl, make.tcl) registers exactly the running platform's root(s), verified on the tcl9 kit and an 8.6 shell (or the 8.6 limitation recorded); a demonstration binary modpod .tm placed in the win32-x86_64 root loads via package require on windows while a same-name-version copy placed in a foreign platform's root is demonstrably not registered; the same-version drop-in-replacement scenario (the fat-tm objection) is demonstrated working across platform roots; generated-project layouts seed the structure; tree READMEs and punk::platform docs updated.
## Context
Drafted 2026-07-22 from the platform-canon survey discussion (user-approved
wording). Motivating evidence is the user's prior fat-tm experience, recorded
in G-109 Notes the same day: a multi-platform thread .tm proved unworkable -
(a) downloads far larger than any one consumer needs, and (b) same-version
shadowing: tm handling resolves one name-version to ONE file with no arch
awareness, so a user cannot drop in a replacement .tm carrying their platform
when a same-version copy is already on the path. Platform segregation for
binary .tm therefore has to ride tcl::tm::path - per-platform tm roots
selected by the boot for the running platform.
Structural constraint driving the layout decision: platform directories
canNOT nest inside an existing tm root, because a subdirectory of a tm path
is a NAMESPACE component (foo/bar-1.0.tm = package foo::bar - see
src/vendormodules_tcl8/tdbc/sqlite3-1.1.5.tm = tdbc::sqlite3 for the
legitimate use of subdirs). Per-platform roots must therefore be SIBLING
trees registered separately (candidate shapes: vendormodules_tcl<N>-<platform>/
siblings, or a byplatform/<platform>/ parent) - the naming decision is this
goal's first item.
Division of labour with G-109 (libunknown manifest-declared multi-name
discovery): this goal owns the roots, the boot wiring and the demonstration;
G-109 owns the manifest format, where a declared target-platform field would
additionally let libunknown skip wrong-platform binary .tm even when someone
shares a path (defensive extra recorded in G-109 Notes 2026-07-22).
Related: G-066 (modpod generation - the demonstration artifact's tooling;
self-mounting binary modpods characterized working there), G-110 (the binary
inside a .tm loads via the extraction path investigated there), G-034 (8.6
modpod mount path - bears on the 8.6 leg of acceptance), punk::platform
(canonical platform names; 'help platforms').
Provenance boundary (2026-07-22 user framing, G-004): the binary .tm these
roots hold are NEVER checked into punkshell - they arrive via the G-067
artifact channel (punkbin library-class artifacts, consent-gated) or as local
uncommitted drop-ins, and the same-version drop-in acceptance below is the
tm-side expression of G-004's binaries-must-still-operate-in-tree
requirement. Derived projects may commit them under their own binary policy.
Nothing needs this until binary-bearing .tm distribution starts in earnest -
creating empty tm-root siblings ahead of that machinery would be speculative
structure (2026-07-22 assessment). The goal exists so the layout and boot
work land deliberately when G-109-era binary tms arrive, not ad hoc.

45
goals/G-115-declarative-vfs-composition.md

@ -0,0 +1,45 @@
# G-115 Declarative .vfs composition: toml-defined kit payloads with drop-in preservation
Status: proposed
Scope: src/make.tcl (vfs assembly), src/runtime/vendorlib_vfs.toml (existing per-package declaration surface - fold/supersede settled in the work), src/vfs/ (per-.vfs declaration files + README), punk::mix machinery as touched, src/project_layouts (seeding for derived projects); coordinates with G-067 (artifact sources), G-006 (consent), G-004 (binary-free committed tree)
Goal: a .vfs folder's payload can be DECLARED in a toml definition (sources: suite build products, punkbin runtime/library artifacts via the consent-gated channels, vendor trees) and materialized into the folder by the build - while the folder remains the operative assembly area: undeclared dropped-in files (binary libs/modules included) are preserved with documented precedence, so derived projects with permissive binary policies and gitignored test .vfs folders keep drop-in simplicity with no declaration required.
Acceptance: a punkshell kit .vfs (or demonstration .vfs) builds from a toml declaration reproducing its payload on a clean tree, with declared binary content arriving via consented retrieval or local build products; an undeclared dropped-in file survives re-materialization per the documented precedence; a .vfs with NO declaration builds exactly as today (pure drop-in mode unchanged); the declaration format and precedence rules are documented; the vendorlib_vfs.toml relationship is settled with rationale; the layout store seeds the convention for derived projects.
## Context
Drafted 2026-07-22 (user-approved wording) from the binary-policy framing
discussion: with G-004 removing checked-in binaries from punkshell, the kit
.vfs payloads' binary content must come from SOMEWHERE reproducible - suite
build products (G-103-class batteries and library builds), punkbin artifact
classes retrieved through the G-006/G-067 consent gates, or vendor trees.
A per-.vfs toml declaration is the glue that makes a kit payload buildable
on a clean binary-free tree, while the answer to "can we still just drop a
dll in?" must stay YES - the folder remains operative, declarations are
optional, and undeclared drop-ins are preserved (the operate-in-tree
requirement recorded in G-004 Notes 2026-07-22: local experimentation and
permissive derived projects are first-class forever).
Precedents already in the tree that make this credible:
- src/runtime/vendorlib_vfs.toml - per-package per-kit declarations ALREADY
drive vendorlib-to-vfs inclusion (with superseded-version removal); this
goal generalizes that surface (fold or supersede - settled in the work).
- layout_materialize (G-087, achieved) - the declarative-plus-folder hybrid
with overlay/anti mechanics is proven machinery in this codebase.
- tomlish is vendored, and toml is the accepted format for punkshell-context
configuration (the 2026-07-20 toml drop applies only to dependency-free
BUILDSUITE BOOTSTRAP configs - recorded in G-103 Context; achieved
2026-07-22, archived at goals/archive/G-103-runtime-kit-family.md).
Relationship to the punkbin third-class question (G-004 Notes 2026-07-22):
declarative composition pulling per-package artifacts is the
preferred-assessment alternative to storing whole zipped binary .vfs
archives - if this goal lands, the third class likely stays unnecessary
except as a release-snapshot convenience.
Precedence design (the drop-in guarantee) is the core design work:
materialization must never clobber an undeclared file, and the documented
rules must state what happens when a declaration and a drop-in collide on
the same path (drop-in wins vs declared wins vs error - decided in the work,
with punkcheck-style tracking of materialized content the likely mechanism
for telling the two apart).

43
goals/G-116-suite-built-tcltls.md

@ -0,0 +1,43 @@
# G-116 Suite-built tcltls with a zig-built crypto backend: bi-family battery, prebuilt replacement path
Status: proposed
Scope: src/buildsuites/suite_tcl90/ (build_tcltls module + crypto-backend build, sources.config + build.zig.zon source records, kit-family bi payload + metadata extension, test_gate record step), src/vfs kit payloads carrying vendored tcltls binaries (current-state reference + recorded disposition only - removal stays G-004-era work), punkbin (as eventual artifact destination via the G-067 library class); consumers punk::imap4 / punk::netbox (package require tls) as verification context
Goal: suite_tcl90 builds tcltls (core.tcl-lang.org/tcltls fossil, the thread/tclvfs/tk fetch pattern) together with its crypto backend (LibreSSL-portable or no-asm OpenSSL - decided and recorded in this file) entirely under the zig-only toolchain policy, statically linked so the resulting tcl9 tls extension is self-contained; it joins the bi-family attached payload as the first battery beyond Tk, loads via package require tls from a bi family kit, and its own testsuite runs under the suite-built shell through the G-107 machinery - replacing the need for the vendored prebuilt tcltls binaries and advancing G-004.
Acceptance: the crypto-backend choice (LibreSSL-portable vs no-asm OpenSSL) is recorded here with rationale and pinned source records in BOTH suite flows (sources.config + build.zig.zon); a suite step builds backend + tcltls reproducibly and the extension loads in the suite shell AND in a rebuilt bi family kit (package require tls plus a functional handshake: a loopback connection to an ephemeral local TLS server using a generated self-signed certificate - no external network); tcltls's own tests/ suite runs as a record-mode test_gate step emitting the G-107 evidence summary (gate promotion per the standard disposition-plus-two-run-deterministic bar, recorded either way); the bi kit-family payload and artifact metadata carry the tls version and the backend name+version; the disposition of the existing vendored tcltls kit payloads (tcltls1.7.22/1.7.23, tls2.0b2, punk9linux tcltls.so) is recorded here - they REMAIN until user-directed removal, actual removal being G-004-era work outside this goal.
## Context
Drafted 2026-07-22 (user-approved wording) to give the tcltls bi-battery line a
live goal home after G-103's archive (achieved 2026-07-22 - see
goals/archive/G-103-runtime-kit-family.md, whose Notes carry the original
2026-07-21 material this goal formalizes):
- DECISION 2026-07-21 (user, recorded in archived G-103): tcltls approved into
the bi-battery enumeration - deliberately NOT gating family progress; a
dedicated goal was named as the option "if the crypto build proves large" -
this is that goal, drafted at the natural point rather than under pressure.
- FEASIBILITY (the crux, unchanged from the archived analysis): tcltls itself
is a thin C extension (tls.c/tlsIO.c compile under zig cc like
thread/tclvfs/tk) but LINKS a crypto backend - upstream OpenSSL, LibreSSL
also supported. No crypto library is in the tree, so the real work is the
backend build under the zig-only policy: pragmatic paths are
LibreSSL-portable (autoconf/cmake, no perl Configure) or a no-asm OpenSSL,
compiled with zig cc, with tcltls linked STATIC against it (mirroring the
tcllibc/tk build shape). This is the largest new build dependency the family
takes on - hence its own goal with its own acceptance.
- TEST VALUE (archived G-103 observation): zig-built crypto + tcltls against a
static tclsh is a combination nobody upstream tests - the record-mode run is
evidence of exactly the kind G-107 (achieved - see
goals/archive/G-107-buildsuite-library-tests.md) was built to capture.
- INTERIM STATE: TLS ships today ONLY as vendored prebuilt binaries in kit
.vfs payloads (tcltls1.7.22/1.7.23 + tls2.0b2 dlls; punk9linux tcltls.so) -
committed binaries G-004 wants gone, tolerated pending their build story.
Real consumers exist now: punk::imap4 and punk::netbox both
'package require tls'.
Relationships: G-004 (a suite-built tls is the prebuilts' replacement path),
G-067 (once built, per-platform tls artifacts are library-class punkbin
candidates), G-105 (tls joins the family x target matrix when cross-targets
land), G-107 (achieved - test machinery this goal's record step uses),
archived G-103 (family payload contract and metadata shape this battery
extends).

79
goals/G-117-self-describing-runtimes.md

@ -0,0 +1,79 @@
# G-117 Self-describing family runtimes: embedded artifact record + metadata schema v1
Status: proposed
Scope: src/buildsuites/suite_tcl90/ (kit-family staging embeds the record; family_artifacts.tcl schema v1 fields + emission ordering; -Doriginurl/-Dpackager options), tools/family_check.tcl (embedded-record verification), src/scriptapps/bin/punk-runtime.* + bin/punk-runtime.cmd via rewrap ('info' action; schema-tolerant parsing), punkbin AGENTS.md (record relationship + schema/field documentation)
Goal: every family runtime carries a copy of its artifact metadata record INSIDE the attached image (e.g /app/punkbin-artifact.toml, written at kit-family staging time - necessarily WITHOUT the final sha1, which is computed over the finished binary and lives only in the sidecar toml + sha1sums), so a runtime separated from its sidecar remains identifiable via a punk-runtime 'info' action (embedded record and sidecar shown side by side, disagreements flagged); AND the metadata record is hardened as SCHEMA v1: schema (format version), build_id (uuid stamped in embedded AND sidecar - the offline correlation key for renamed copies), origin (canonical artifact repo the artifact was built FOR - mirrors preserve it; -Doriginurl, default punkbin), packager (declared identity; -Dpackager > env > git identity > unrecorded), project/project_url, license summary, build_host_platform - so records circulating on punkbin-compatible repos from different packagers stay interpretable and attributable.
Acceptance: kit-family embeds the record in all three members with the embed-then-hash ordering documented (embedded copy carries the v1 fields but no self-sha1; the sidecar + sha1sums remain the integrity authority); family_check verifies the embedded record exists and its variant/patchlevel/battery fields MATCH the probed facts for each member; 'punk-runtime info <name>' reports identity for (a) a fetched artifact with its sidecar and (b) a bare RENAMED copy with no sidecar - the embedded-read mechanism is decided and documented in the work (zip-central-directory read of the exe-appended archive by the host payload where available vs a cooperative probe that executes the target with a generated script, execution caveat stated) - and flags embedded-vs-sidecar disagreement; the v1 field set is documented in punkbin AGENTS.md with each field's semantics (notably origin's built-for-not-published-on meaning and packager's declarative-not-verified status, with minisign sidecars noted as the verification complement); family_artifacts emits all v1 fields with build_id present and IDENTICAL in embedded and sidecar copies, and the emitted records carry brief '#' comment lines documenting the non-obvious fields (toml-spec comments, ignored by the existing line-based consumers); punk-runtime metadata parsing tolerates unknown fields and absent schema (pre-v1 r1 records keep working); both payloads in parity with the roundtrip pin green; the next family emission after this goal lands is r2 carrying the full v1 record (r1 stays immutable as published).
## Context
Drafted 2026-07-22 (user-approved wording) from two exchanges during the G-103
publication session:
MOTIVATION (embedding): the published family artifacts are metadata-carrying
via SIDECAR tomls only - nothing is stamped inside the executables (a
deliberate G-103 naming-decision property: the metadata record is
authoritative, the filename identifies, the binary stays unstamped). The
implication: an exe separated from its toml (out-of-band copy, rename) is
identifiable only by executing probes against it or sha1-matching it back to
a repo's sha1sums. Embedding a COPY of the record in the attached image makes
runtimes self-describing without changing the authority model - the sidecar +
sha1sums remain the integrity authority; the embedded copy is a convenience
duplicate that cannot contain its own final sha1 (embed at staging -> wrap ->
hash -> sidecar; no circularity).
SCHEMA v1 (user-floated fields, analysed 2026-07-22):
- origin: the user's "repository url it was first published on" idea, with a
semantic reframe dissolving the timing problem they spotted (emission
precedes publication; publish-time sidecar mutation would permanently
diverge embedded vs sidecar - the exact disagreement 'info' flags): origin
= the canonical artifact repo the artifact was BUILT FOR, known at emission
(-Doriginurl, default punkbin), truthful regardless of publish timing, and
preserved by mirrors - an artifact found on a third-party repo still
declares its home. Where-it-actually-lives is the hosting repo's own
business (a repo-level identity file beside platforms.txt/defaults.txt is a
separate later idea).
- packager: declared identity for multi-packager punkbin-compatible
ecosystems (-Dpackager > env > the building checkout's git identity >
"unrecorded"). DECLARATIVE, not proof - the verifiable layer is signing,
with punkbin's existing minisign practice (zig tool archives) as precedent;
per-artifact .minisig sidecars are the natural future complement,
deliberately outside the metadata file (a signature cannot live inside
what it signs).
- schema: format-version field - the most important future-proofing addition;
nearly free now, impossible to retrofit onto records already circulating.
- build_id: uuid stamped in BOTH embedded and sidecar copies - the offline
correlation key that re-joins a renamed stray exe to its record without
content hashing.
- license summary + project/project_url: distribution-platform eligibility
(the G-004 motivation) asks licensing first; the project pointer makes a
stray artifact self-explaining beyond its origin repo. Component licenses
ride inside the image (tcl_library etc).
- build_host_platform: near-free now, genuinely interesting once G-105
cross-builds exist (built ON win32-x86_64 FOR linux-x86_64).
- REJECTED: lifecycle fields (superseded_by, expires) - repo-level curation
(defaults.txt and successors), not artifact facts; artifacts stay immutable
statements of what they are.
Comment lines: emitted records gain brief '#' comments documenting non-obvious
fields (origin/packager semantics). Toml-spec comments; the existing
line-based consumers (punk-runtime ps1 regex / bash sed per-line matching,
the summary parsers from G-107, achieved - see
goals/archive/G-107-buildsuite-library-tests.md) ignore them by construction - family_artifacts already
emits a '#' header line today.
Read-mechanism design fork (the main in-goal decision): exe-appended zips are
readable from the central directory by generic zip readers (.NET
System.IO.Compression for the ps1 payload, unzip for bash) WITHOUT executing
the target - preferred, since identifying an untrusted stray binary by
running it is what an identity mechanism should avoid; the cooperative probe
(execute the target with a generated script) is the documented fallback for
hosts without a zip reader.
Relationships: archived G-103 (metadata shape + family staging this extends -
see goals/archive/G-103-runtime-kit-family.md), G-067 (library-class
artifacts should adopt the same schema + embedding pattern), G-105 (embedded
target field aids cross-platform staging hygiene alongside the sidecar
!TARGET-MISMATCH check), G-006/G-067 consent gates (unchanged - metadata
travels with artifacts through the existing channels).

222
goals/G-103-runtime-kit-family.md → goals/archive/G-103-runtime-kit-family.md

@ -1,6 +1,6 @@
# G-103 runtime kit family from buildsuites: plain / punk / bi kits with attached batteries + artifact metadata
Status: active
Status: achieved 2026-07-22
Scope: src/buildsuites/suite_tcl90/ (kit assembly steps in recipe/driver; pattern for later suites), src/vfs/_config + src/runtime/mapvfs.config (as consumers of the new runtime names), punkbin repo layout + metadata (c:/repo/jn/punkbin; compatible repos), src/scriptapps runtime scriptset (punk-runtime list/use - renamed under G-097, achieved 2026-07-21)
Goal: buildsuites produce a defined FAMILY of runnable, self-contained runtime kits - executables whose info library and core batteries live in the initially attached zip, depending on no external filesystem tree: (1) a PLAIN tclsh kit carrying loadable Thread, tclvfs with as many vfs::* packages as we have, and tcllib+tcllibc; (2) a PUNK kit = the plain kit with the TCLSH_PIPEREPL patch applied and ENABLED BY DEFAULT (env opt-out, not opt-in - users wanting stock behaviour take the plain kit); (3) BI (batteries-included) punk kits additionally carrying libraries WE BUILD - Tk first; tcltls (with a zig-built OpenSSL/LibreSSL backend), sqlite3/tdom/twapi as future build targets - all in the attached zip. These are the project's 'runtime' executables: punkbin candidates carrying metadata (variant, component versions, target platform, provenance) that runtime list/use subcommands can surface.
Acceptance: suite_tcl90 produces named artifacts for at least plain, punk, and one bi (+Tk) kit; each verified self-contained from a path with no external Tcl visible (info library resolves into the attached zip; package require checks for Thread, vfs + representative vfs::* packages, tcllib module + tcllibc acceleration engaged; bi adds Tk create/destroy) with the checks recorded here; the punk kit demonstrates piperepl active by default and disabled via the documented env opt-out, the plain kit demonstrates stock behaviour (no patch); artifact metadata (variant, versions, target, source provenance) is emitted alongside the binaries in the punkbin layout and surfaced by the runtime scriptset's list/use; the naming scheme for family members is documented, with piperepl-patched runtime executables carrying 'punk' in the name to distinguish them from unpatched (e.g tclsh905punk.exe - the tcl-patchlevel / punk-version / separator questions resolved and the decision recorded here) and mapvfs.config consuming the punk/bi runtimes under the decided names; the 8.6 family variant is explicitly deferred to the G-101 container investigation (static vfs / metakit patching questions).
@ -30,9 +30,10 @@ third-party binaries. Tk is built today; sqlite3 is a straightforward future bui
MSVC-leaning - its buildability under the zig-only policy needs investigation before
it can join a bi kit (until then twapi remains a kit-vfs vendored payload as in
punk9wintk903.vfs, outside this family's attached-zip guarantee). The no-twapi
mitigation for raw-mode console control is the powershell console-mode fallback -
maintenance tracked as G-106, whose priority rises if zig-built twapi proves
impractical.
mitigation for raw-mode console control is the powershell console-mode fallback,
hardened and verified on this family's runtimes under G-106 (achieved 2026-07-22 -
see goals/archive/G-106-powershell-consolemode-fallback.md); if zig-built twapi
proves impractical, that fallback is the primary raw path on family runtimes.
Metadata format note: toml is acceptable HERE (runtime list/use run in punkshell
contexts where vendored tomlish exists) - the 2026-07-20 toml drop applies to
@ -232,3 +233,216 @@ contract the sequencing note promises G-101.
zipfs) extracts to %TEMP%\TCL* with the SAME non-cleanup flaw (details in G-066
Notes). So windows = flaw confirmed for BOTH direct-kit and modpod-.tm binary
loads. Linux/WSL leg is blocked on a linux tcl9 runtime - see G-105 Notes.
- 2026-07-21: G-023 (version-named punk binaries) reconciled to this goal's
NAMING DECISION (user-approved): it adopts the kit-artifact tier
punk<major>-<punkshellversion>.exe verbatim and layers its -dev /
release-gated-plain-name policy on the working-name tier.
- G-106 (archived) recorded the no-twapi raw-mode contingency this goal's twapi
investigation weighs against: the powershell console-mode fallback is verified
working on the family runtimes (tclsh90spr + the zipfs kit shape, make.tcl shell
and bare repl launches) - lazy singleton pwsh server, quiet, parent-pid
self-reaping, script embedded in punk::console so kits need no scriptlib on
disk; env PUNK_PS_CONSOLEMODE_DEBUG=1 for diagnostics. If zig-built twapi proves
impractical, bi kits can ship without twapi and rely on this path for raw mode -
see goals/archive/G-106-powershell-consolemode-fallback.md (recipe in its Notes).
## Progress
### 2026-07-22 increment: kit-family + kit-family-artifacts steps landed, all three members verified
ATTACHED-IMAGE LAYOUT DECISION (resolves the Context "open at activation"
items - staging-tree convention and payload layout): the image mirrors the
installed prefix, with two boot-anchored placements and one punkshell-authored
hook file:
- `tcl_library/` at the app root (the C-level zipfs boot looks only for
`/app/main.tcl` and `/app/tcl_library`; NO main.tcl is included - stock boot
falls through to the ordinary interactive shell, per the Context requirement
that payload sit where the STOCK boot searches).
- tm modules at `tcl9/<ver>/` beside it (tm.tcl Defaults anchors at
`[file dirname [info library]]` = `/app`).
- batteries under `lib/<pkg>/` with their INSTALLED-SHAPE pkgIndexes unchanged
(`$dir/../../bin` dll references resolve to `/app/bin`), dlls under `bin/` -
the same shape the suite's out/ prefix and the existing punk .vfs payloads
use, so pkgIndex generation stays single-source (build_tclthread/build_tk
return their generated pkgIndex + version facts; tclvfs's configured package
generation is shared between prefix install and family staging).
- `lib/pkgIndex.tcl`: a one-line stock hook (`lappend ::auto_path $dir`)
joining lib/ to the package search - auto_path starts as
`[tcl_library, /app]`, the /app scan sources `/app/*/pkgIndex.tcl`, and
tclPkgUnknown re-scans auto_path growth mid-scan (the same stock mechanism
tcllib's own top-level index uses). No C changes, no boot script.
- Tk script library resolution: tcl_findLibrary iterates auto_path entries
joined with `tk9.0`, so `/app/lib/tk9.0` is found once the hook has run
(package require Tk goes through the pkgIndex scan first by construction).
Landed (suite_tcl90, all in the default build pipeline):
- `kit-family` step: two WriteFiles staging trees (core, bi) + three
zipfs_mkimg wraps emit the working-name products into `out/family/`:
tclsh9.0.5.exe (prefix exe tclsh90s), tclsh9.0.5-punk.exe (tclsh90spr),
tclsh9.0.5-punk-bi.exe (tclsh90spr + Tk/tklib payload).
- `tools/family_check.tcl`: per-member self-containment verification - the kit
is copied ALONE into a scratch dir and probed from there (defeats the
`<exedir>/../lib` tm root and exe-relative tcl_findLibrary entries) with
TCL_LIBRARY/TK_LIBRARY/TCLLIBPATH/VFS_LIBRARY/TCL*_TM_PATH scrubbed and
TCLSH_PIPEREPL controlled per probe. The piperepl discriminator is
`[info exists ::tclsh(istty)]` in a script-arg run (G-096 matrix: machinery
published iff patched AND gate open; script-arg runs never set dorepl, so no
console-reopen/hang risk).
- `kit-family-artifacts` step + `tools/family_artifacts.tcl`: punkbin-layout
emission into `out/family/punkbin/win32-x86_64/` - immutable -r<N> artifact
copies (`-Dfamilyrev=N`, default 1), per-artifact toml metadata (variant,
working name, revision, target, sha1, size, built, tcl patchlevel, piperepl
policy incl the TCLSH_PIPEREPL=0 opt-out, attached battery versions,
suite/toolchain/optimize, per-source checkout uuids where materialized -
manifest.uuid is a per-repo fossil setting: tcl/tk/thread carry it,
tclvfs/tcllib/tklib record "unrecorded" - and G-107 test-evidence result
lines from out/testreports/*.summary), plus punkbin-format sha1sums.txt.
Deliberately runs UNDER the plain family kit itself (sha1 via its attached
tcllib) - each emission re-proves the runtime executes real tooling
self-contained. Depends on the checks: only verified kits get records.
- Consumers: working names copied to bin/runtime/win32-x86_64/ (machine-local,
gitignored) and mapvfs.config entries added - tclsh9.0.5-punk.exe +
punk9wintk905.vfs -> punk9_beta, tclsh9.0.5-punk-bi.exe +
punk9win_for_tkruntime.vfs -> punk9bi_beta (*_beta trial convention; kit
working names per the naming decision).
VERIFICATION 2026-07-22 (acceptance self-containment checks; suite build on the
9.0.5 checkout 1a9c3b9d96, zig 0.16.0, ReleaseFast; family_check probes run
from a scratch dir with scrubbed env - full pipeline
`suite.tcl build` PASS end-to-end including both new steps):
- ALL THREE members: info patchlevel 9.0.5; `info library` =
//zipfs:/app/tcl_library (attached image); tzdata + encodings reachable;
`package require platform` served from the attached tm tree; Thread 3.0.7
loads from the attached zip AND executes (thread::create + cross-thread eval
= 42); vfs 1.4.2 + vfs::zip 1.0.4 + vfs::urltype 1.0, with a functional
vfs::zip mount round-trip (zipfs mkzip a scratch payload, mount via tclvfs,
read back, unmount); tcllib md5 2.0.9 with the tcllibc critcl accelerator
ENGAGED (md5::accel(critcl)=1).
- plain tclsh9.0.5.exe: stock behaviour proven - no ::tclsh machinery with env
unset AND with TCLSH_PIPEREPL=1 (unpatched binary ignores the enable).
- punk tclsh9.0.5-punk.exe / punk-bi tclsh9.0.5-punk-bi.exe: piperepl ACTIVE BY
DEFAULT (::tclsh machinery published with env unset), disabled via the
documented TCLSH_PIPEREPL=0 opt-out.
- punk-bi additionally: Tk 9.0.2 loads from the attached zip - button
create/destroy + root destroy clean; tklib tooltip 2.0.4.
- Artifacts emitted (current emission 2026-07-22, after a FULL core test-gate
refresh replaced a stale partial tclcore.summary - gate PASS: 69552 run,
56039 passed, 13504 skipped, 9 failed all baselined; metadata [tests] now
carries the full evidence set): tclsh9.0.5-r1.exe (sha1 969ac1e1..., 6455
KB), tclsh9.0.5-punk-r1.exe (63bc8f57..., 6458 KB),
tclsh9.0.5-punk-bi-r1.exe (a6a23f24..., 9165 KB) + per-artifact tomls +
sha1sums.txt, generated by the plain family kit itself. Working copies in
bin/runtime/win32-x86_64 re-materialized from this emission via
'punk-runtime use' (re-wrapped artifacts get new zip bytes/sha1s - immutable
-rN discipline starts at PUBLICATION; pre-publication regeneration of r1 is
the dev loop).
### 2026-07-22 increment: punk-runtime list/use artifact-metadata surfacing (acceptance item landed)
Scriptset extension (both payloads, ps1 + bash, wrapped to bin/punk-runtime.cmd
per the bin/AGENTS.md polyglot workflow; layout-shipped copy + .ps1 twin
refreshed; roundtrip pin test runtimecmd_roundtrip PASS):
- `list` surfaces a per-runtime metadata summary parsed from the `<rootname>.toml`
record beside each runtime: variant, tcl patchlevel, revision, piperepl
on/off, and - on a materialized working copy - `from=<artifact>` (which
immutable -r<N> artifact it came from).
- `use <artifact-r<N>-name>` MATERIALIZES the immutable artifact into its
WORKING name (name minus -r<N>) with the metadata toml copied alongside, then
selects the working name - the naming decision's "runtime use materializes a
chosen -rN artifact" mapping, so republishing never churns consumers.
`use <workingname>` selects as before.
- `fetch` of an -r<N>-named artifact also retrieves its metadata toml from the
punkbin layout (absence tolerated - pre-family runtimes have no records).
- Root-name handling strips only `.exe` (dotted patchlevels break last-dot
stripping for extensionless unix names) - the same fix applied to
family_artifacts.tcl's artifact/toml naming (exe_split) for the G-105
cross-target future. Candidate listing now excludes directories and .log
files in both payloads (parity cleanup).
VERIFIED 2026-07-22 on the real wrapped bin/punk-runtime.cmd (windows
powershell 5 branch) and the bash payload directly (git-bash): `use
tclsh9.0.5-punk-r1.exe` materialized tclsh9.0.5-punk.exe + toml and selected
it; all three members materialized the same way; `list` shows the summaries in
both payloads identically; `punk-runtime run <probe.tcl>` launched the active
punk family runtime and reported 9.0.5 / piperepl machinery present /
tcl_library=//zipfs:/app/tcl_library. Project 0.17.7 (patch bump + changelog:
punk-runtime is shipped product surface).
### 2026-07-22 increment: punk-runtime cross-platform surface + help (user-directed; G-105 groundwork)
User-approved argument surface for future cross-builds (design discussion
2026-07-22): fetch/list/use accept `-platform <p>` (punkbin platform-DIR names,
never zig triples; resolution -platform arg > PUNK_RUNTIME_PLATFORM env > local
default; shape-only lowercase validation - the server is the truth). Foreign
fetch requires an explicit runtime name (no foreign defaults); `use -platform`
manages that folder's active.toml/materialization (the marker travels with the
folder at deploy time - the provisioning story; exec bits restored on the
receiving side); `list` gains a !TARGET-MISMATCH integrity flag when a
runtime's metadata target disagrees with its folder; `run` is local-only
(rejects a leading -platform, ignores the env override, later args pass to the
runtime untouched). Plus a `help` action (full operator reference) and a real
usage block on no-args. Bash FreeBSD platform dir aligned to punkbin's actual
freebsd-x86_64 (was freebsd-amd64 - a G-105 naming-alignment item found during
the design pass). Parity bug caught in verification: powershell -notmatch is
case-insensitive - validation uses -cnotmatch (URL paths are case-sensitive).
Verified on both payloads directly AND through the rewrapped polyglot
(noargs/help/list/foreign-use-materialize/mismatch-flag/foreign-fetch-refusal/
run-guard/env-override incl run's immunity to it); roundtrip pin PASS; layout
copy + .ps1 twin refreshed; project 0.17.9.
### 2026-07-22 increment: kit-wrap trial PASS - acceptance complete, goal achieved
The mapvfs consumption was exercised with real kit builds (user go-ahead;
quiet tree, no punk shells locking bin exes): make.tcl project built and
deployed punk9_beta.exe (tclsh9.0.5-punk.exe + punk9wintk905.vfs, 28.9 MB)
and punk9bi_beta.exe (tclsh9.0.5-punk-bi.exe + punk9win_for_tkruntime.vfs,
58.3 MB). The user's own earlier same-day build had already wrapped them
(punkcheck reported no-change on the agent's confirming run - two
independent builds agree). Probes run UNDER each kit:
- both: patchlevel 9.0.5, tcl_library //zipfs:/app/tcl_library (merged
runtime+vfs image), PIPEREPL MACHINERY PRESENT (the punk-family basis
carries into wrapped kits), vfs 1.4.2, md5 2.0.9 + tcllibc 2.0 engaged,
punk::args 0.12.6 (vfs payload side), Tk 9.0.2 with widget
create/destroy.
- Thread resolves to 3.0.7: the runtime's attached battery wins over the
vfs payload's legacy thread3.0.1 exactly as designed (versioned package
resolution over the union image).
- punk9bi_beta's Tk necessarily comes from the RUNTIME's attached image
(punk9win_for_tkruntime.vfs carries no Tk by design) - the bi battery
demonstrably serves a real kit.
Remaining-work resolution (both items closed):
- kit-wrap trial of the mapvfs entries: DONE (this increment). Interactive
beta trial and punk9_beta/punk9bi_beta promotion remain ordinary
*_beta-convention user activities, outside this contract.
- 8.6-family deferral pointer recorded against G-101 at closing (see G-101
Notes 2026-07-22).
Post-achievement follow-throughs (not acceptance items): punkbin
publication of the family -rN artifacts remains user-gated (the
defaults.txt flip process is ready - pointer left in G-067's channel notes); the tcltls bi-battery line (crypto backend under the
zig-only policy) loses its goal home with this archive - flagged for a
candidate goal decision; punk9wintk905.vfs still carries the legacy
thread3.0.1 payload dir (harmless - runtime 3.0.7 wins - cleanup can ride
any future payload refresh).
- 2026-07-22: punkbin layout contract gained a root-level platforms.txt
discovery manifest (generated by punkbin src/build_sha1sums.tcl; committed
locally in punkbin 7b8a244, push = user decision) - raw-file artifact
servers have no directory listing, so the manifest is how punk-runtime's
new 'platforms -remote' action (and third-party mirrors per the Context
"compatible repos" note) enumerate served platforms. Verified end-to-end
against a file:// mirror simulation of the local punkbin checkout.
- 2026-07-22: punkbin layout also gained a curated root-level defaults.txt
(punkbin c4d948f) - the per-platform recommended default a no-name
'punk-runtime fetch' retrieves. A RELEASE DECISION as server data: updated in
the same punkbin change-set that publishes the artifact it points at
(validated by punkbin's build_sha1sums.tcl), so flipping the recommendation
to a family artifact at publication time is a one-line edit with no punkshell
release. The payloads' baked defaults were removed (one had already drifted
from the server's actual linux-arm artifact name).

209
goals/archive/G-106-powershell-consolemode-fallback.md

@ -0,0 +1,209 @@
# G-106 powershell console-mode fallback: maintained raw-mode path for twapi-less runtimes
Status: achieved 2026-07-22
Scope: src/modules/punk/console-999999.0a1.0.tm (enableRaw_powershell/disableRaw_powershell + the persistent server lifecycle), scriptlib/utils/pwsh/ (consolemode_server_async.ps1 canonical + experiment-variant reconciliation), ps-script resolution/packaging across launch contexts (argv0-derived pstooldir; kits via the G-089 scriptlib-in-kits interplay), src/make.tcl shell (the verified launch context)
Goal: the powershell console-mode fallback - a persistent pwsh/powershell named-pipe server driven by punk::console when twapi is absent - is a MAINTAINED, dependable raw-mode path for twapi-less runtimes (the suite-built shells and kits, until/unless zig-built twapi lands): quiet in normal operation, reliable across the session lifecycle, and resolvable from every supported launch shape rather than only argv0-relative luck.
Acceptance: on a twapi-less suite runtime, raw enable/disable work via the fallback from (a) the make.tcl shell launch and (b) a kit / plain-tclsh repl launch, with no stderr noise in normal operation; the server lifecycle is verified - starts once per session, stays up for the session's raw transitions (the 2026-07-20 observed early-shutdown mode diagnosed and fixed), and shuts down with the session leaving no orphan pwsh processes; ps-script resolution no longer depends solely on argv0 parent-dir derivation (works from kits and unusual cwds, with the fallback-to-pwd branch replaced by something principled); scriptlib/utils/pwsh is reconciled to one canonical server script with the experiment variants retired or explicitly labelled; the fallback's role as the no-twapi contingency is documented where G-103's twapi investigation will find it, and a repeatable verification recipe is recorded in this file.
## Context
User verification 2026-07-20 on the freshly built tclsh90spr (piperepl
variant from G-096, achieved 2026-07-20): with `tcl::tm::add .../modules` + `package require repl; repl::init;
repl::start stdin`, basic punk features are functional on the bare suite runtime -
except raw mode, which is twapi-reliant. Launched instead via
`./tclsh90spr .../src/make.tcl shell`, the powershell fallback engaged and raw mode
WORKED - modulo consolemode_server_async.ps1 noise and an early server shutdown.
That working-but-rough state is what this goal hardens. User direction: "we would
like to look at maintaining this powershell fallback - especially if we can't get
zig-based twapi builds going."
Mechanism today (punk::console): when twapi is absent, enableRaw/disableRaw are
aliased to *_powershell variants that talk over a named pipe to a persistent
`consolemode_server_async.ps1` process (templated per-console id; pwsh.exe probed
first, then powershell.exe). Known weak points at goal creation:
- Resolution: pstooldir = argv0's grandparent + /scriptlib/utils/pwsh, else [pwd] -
works for `make.tcl shell` (argv0 = src/make.tcl -> repo root) and little else by
construction. Kits need the script carried/resolved properly (G-089 scriptlib in
kits).
- Lifecycle: observed early shutdown of the server mid-session; startup noise on
stderr ("twapi not present, using persistent powershell process: ..." plus ps1
output).
- Hygiene: scriptlib/utils/pwsh holds several sibling experiments
(consolemode_server.ps1, *_async1.ps1, *_async.2ps1, consolemode.ps1,
consolemode_enableraw.ps1) with no marking of which is canonical.
Relationships: G-103 (bi-kit twapi under the zig-only policy is
needs-investigation - THIS fallback is the mitigation; if zig-built twapi proves
impractical the fallback becomes the primary raw path on suite runtimes and this
goal's priority rises), G-013 (raw mode as the repl default ultimately requires raw
mode to be dependable on twapi-less runtimes), G-089 (scriptlib in kits - the
delivery vehicle for the ps script), G-061-era console-context findings (agent
console-context traps around attached consoles apply to verifying this).
## Approach
Design decisions of the 2026-07-22 overhaul (punk::console 0.8.0):
- Root cause of the 2026-07-20 early shutdown - two compounding defects in the old
server script: (1) a 20s keepalive killed the server when no ping arrived, but only
`enableraw` messages refreshed the ping and NO tcl-side pinger ever existed, so the
server always died ~20s after the last raw enable ("ping stale for pipe ... -
exiting" was this kill's console noise); (2) the per-connection
`StreamReader.Close()` disposed the underlying pipe stream, so the following
`Disconnect()/Dispose()` threw and killed the listener runspace after the FIRST
message - later toggles could not reach the server even while its process lived.
- Keepalive replaced by a parent-process watch: the tcl side templates its pid into
the script; the server holds a `System.Diagnostics.Process` object obtained once by
that pid (handle-based, immune to pid reuse) and exits when `HasExited` reports the
owner gone (<=5s poll cadence). Orphan prevention needs no cooperation from the tcl
side - hard kills included.
- Lazy singleton server: spawn moved from module load to first
enableRaw/disableRaw use, so loading punk::console in piped/non-console or
worker-thread contexts spawns nothing and prints nothing. Server identity lives in
tsv `punk_console` (`ps_server_pipename`/`ps_server_pid`/`ps_server_spawntime`)
under `tsv::lock`, so all interps/threads of a process share ONE server (the
codethread and the repl thread previously would each have spawned their own).
- Quiet by default: the server child is spawned `-nop -nol -noni` with stdout/stderr
redirected to NUL (stdin MUST stay inherited - the console input handle is how the
server reaches the console); all ps1 output is debug-gated. Env
`PUNK_PS_CONSOLEMODE_DEBUG=1` enables diagnostics on both sides (tcl spawn note on
stderr + unredirected ps1 write-host trail). Failure paths (server unreachable,
spawn failure) still emit one actionable stderr line - that is not normal
operation.
- Resolution chain (`punk::console::system::ps_consolemode_script_get`): env
`PUNK_PS_CONSOLEMODE_SCRIPT` override (used only when the file exists) ->
argv0-derived project root -> module-location-derived roots (2-up for built
`modules/`, 3-up for `src/modules/`; a zipfs kit module path just fails the probes)
-> embedded copy of the script carried in the module
(`ps_consolemode_script_embedded`). The `[pwd]` fallback is gone. The embedded copy
makes kits and arbitrary cwds resolution-proof with no scriptlib delivery
dependency (the G-089 interplay is thereby optional for THIS script: a resolvable
scriptlib file wins - dev-editable - and the embedded text is the floor).
- Delivery stays `-c <script text>` (not a dropped file): needs nothing on disk at
run time and is not subject to ExecutionPolicy restrictions that can block -File
runs. The ps1's semicolon-after-each-command convention supports that delivery.
- Protocol: one line per named-pipe connection - `enableraw | disableraw | ping |
exit`. The listener now forwards disableraw (previously swallowed - on tcl 8.6,
where `-inputmode` is unavailable, cooked mode could never be restored via the
fallback); a null-message connection (a probe) no longer shuts the server down;
`AutoResetEvent` replaces ManualResetEvent+late-Reset (lost-wakeup race).
- enableRaw returns only after the flip is observable where the runtime can read
live mode (tcl9 `-inputmode` poll, 750ms cap, result note carries
`confirmed 0|1|unknown`) - closes the fire-and-forget race between the pipe write
and the server applying the console flags.
- `ps_consolemode_send` retries connection to a deadline (15s within 15s of spawn -
powershell startup - else 2.5s), and on an unreachable recorded server clears the
recorded state and respawns once. `ps_consolemode_server_stop` is an explicit
best-effort shutdown for tests/tools (not needed for orphan prevention).
- stty remains the last-resort branch after the server path (unchanged semantics:
useful only in msys/mintty-without-winpty environments and when powershell is
entirely absent).
- posh-git MIT attribution for the NativeConsoleMethods C# snippet now rides in the
canonical script (and thus the embedded copy) - the third-party source copy
(consolemode.ps1) could then be retired with the other variants.
## Notes
- Verification recipe (repeatable):
1. Sync + resolution suite (any platform, no server spawned):
`tclsh90 src/tests/runtests.tcl -report compact -show-passes 0 -include-paths "modules/punk/console/***"`
- psfallback.test pins the embedded-copy/canonical-file sync, placeholder
presence, and the resolution chain (env override + fallthrough).
2. Functional lifecycle selftest on a twapi-less runtime. The selftest must run
with a REAL console that is not the driving shell's console: launch it via
`Start-Process <twapi-less tclsh> <selftest.tcl> <resultfile> <moduledir> -WindowStyle Hidden`
(Start-Process gives a console app a fresh console; Hidden keeps it invisible;
do NOT redirect stdin). Selftest body (write results to `<resultfile>`, tm-path
add `<moduledir>`, `package require punk::console`, assert `has_twapi=0`):
enableRaw -> assert `chan configure stdin -inputmode` reads `raw` (tcl9's live
GetConsoleMode read is the independent cross-check); disableRaw -> `normal`;
repeat; idle 25s (the old keepalive died at 20s) then another cycle;
`punk::console::system::ps_consolemode_server_stop` (expect `stopped 1`);
enableRaw again (respawn) + disableRaw; record
`tsv::get punk_console ps_server_pid` after each spawn. Driver afterwards:
wait >=12s past process exit and assert every recorded server pid is gone
(parent-watch reaping). Force the embedded resolution variant by setting
`set ::punk::console::system::module_dir C:/nonexistent` before the first
enableRaw with the selftest located outside any checkout, and assert
`ps_consolemode_script_get` reports `source=embedded`.
3. Live-repl spot check (the acceptance launch shapes): boot
`<twapi-less tclsh> src/make.tcl shell` (shape a) or
`<twapi-less kit> <script: tcl::tm::path add <repo>/modules; package require punk::repl; repl::init; repl::start stdin>`
(shape b) hidden as above with stderr redirected to a file; from a helper
process do FreeConsole + AttachConsole(<shell pid>) + open CONIN$ +
WriteConsoleInput to type `punk::console::enableRaw` + Enter at the live
prompt; GetConsoleMode(CONIN$) before/after shows ENABLE_LINE_INPUT|ECHO
cleared (observed 0x1E7 -> 0x1E1); a pwsh child of the shell with
`punkshell_ps_consolemode` in its command line confirms engagement; type
`punk::console::disableRaw` (mode returns, observed 0x1E1 -> 0x1E7); kill the
shell and assert the server exits <=12s; assert the stderr capture has no
fallback-related lines (grep: `twapi not present|pipename|Started named
pipe|consolemode_server|ping stale|persistent powershell`).
4. Debug knob when anything misbehaves: `PUNK_PS_CONSOLEMODE_DEBUG=1` in the tcl
process's environment (tcl-side spawn note + unredirected ps1 diagnostics).
- Residue - tcl 8.6 twapi-less: the no-`-inputmode` disableraw-via-server path is
now CODED (previously stty-or-error) but only tcl9-verified; verify when the G-099
8.6 suite produces a twapi-less 8.6 runtime.
- Residue - punk kits with twapi (e.g punk905): unaffected by design (twapi branch
wins). Note their kit-stamped punk::console pre-registration satisfies a plain
`package require punk::console` in bare `tcl::tm::add` contexts, shadowing newer
built modules - the known kit-registration behaviour, relevant to verification
setups only.
- Residue - src/vfs/project.vfs's pinned punk::console 0.1.1 copy still carries the
old load-time spawn referencing the retired consolemode_server.ps1 (user-curated
vfs payload for generated projects; not touched by this goal).
- Repl integration note: raw engages on demand (lazy spawn) - the first
enableRaw of a twapi-less session pays the server spawn (~0.6-0.7s with pwsh 7
observed; powershell.exe cold is slower), later toggles are ~1-30ms. An eager
warm-up at repl init on twapi-less interactive sessions is a possible future
nicety if the first-prompt delay is felt.
## Progress
### 2026-07-22: overhaul landed (punk::console 0.8.0) + full verification - acceptance met
Landed: punk::console 0.8.0 (see console-buildversion.txt changelog for the
per-change list; design in ## Approach), canonical
scriptlib/utils/pwsh/consolemode_server_async.ps1 rewrite, five experiment variants
retired (consolemode_server.ps1, consolemode_server_async1.ps1,
consolemode_server_async.2ps1, consolemode.ps1, consolemode_enableraw.ps1) with
scriptlib/utils/pwsh/README.md labelling the canonical + echotest.ps1, new console
testsuite psfallback.test (embedded-sync + resolution pins), punk::repl stale
comment fix (no behaviour change), project 0.17.8.
Verification evidence (all on suite build 9.0.5 runtimes, zig 0.16.0; driver
scripts per the ## Notes recipe):
- Console testsuite: 76/76 pass incl the 5 new psfallback tests (native tclsh 9.0.3
runner).
- Hidden-console lifecycle selftest, tclsh90spr (has_twapi=0), against src modules
AND built modules, plus zipfs kit tclsh90sprzip against built modules - all
SELFTEST-PASS: first enable ~0.6-0.7s incl server spawn with `confirmed 1` and
-inputmode reading `raw`; warm toggles ~30ms; survived 25s idle then toggled in
~30ms (old keepalive died at 20s); explicit stop `stopped 1`; respawn cycle ok;
both server generations reaped (explicit stop + parent-watch after process kill),
zero orphans.
- Embedded-resolution forced run (bogus module_dir, scratchpad argv0/cwd):
`source=embedded`, full enable/disable cycle + stop PASS - kits/unusual cwds need
no scriptlib on disk.
- Acceptance shape (a): `tclsh90spr src/make.tcl shell` live repl in a hidden
console; keystrokes injected via AttachConsole+WriteConsoleInput typing
`punk::console::enableRaw` / `punk::console::disableRaw` at the prompt; external
GetConsoleMode reads flipped 0x1E7 -> 0x1E1 -> 0x1E7 (LINE|ECHO cleared and
restored); server child engaged; hard-kill of the shell -> server gone <=12s;
stderr capture free of fallback noise (8 lines, all unrelated boot messages).
- Acceptance shape (b): tclsh90sprzip (zipfs kit) running the bare-runtime repl
formula (`tcl::tm::path add <repo>/modules; package require punk::repl;
repl::init; repl::start stdin`) - same injection sequence, same mode flips,
engagement, reaping and quiet stderr.
- punk905 (punk kit WITH twapi): twapi path unaffected - enableRaw flips modes via
twapi with no server spawned.
- ps1 standalone lifecycle smoke (pwsh -f, debug mode): start/ping/null-probe/bogus
message/multi-connection survival/exit-message shutdown + watched-parent-death
shutdown all pass; debug output only.
Remaining manual item: none gating. The user's interactive feel of raw mode on the
suite runtimes (typing experience) is the natural follow-up confirmation; the
mechanical acceptance criteria are all verified above. 8.6-runtime verification is
recorded as residue against the G-099 arc (see ## Notes).

2
punkproject.toml

@ -1,4 +1,4 @@
[project]
name = "punkshell"
version = "0.17.6"
version = "0.18.8"
license = "BSD-2-Clause"

36
scriptlib/utils/pwsh/README.md

@ -0,0 +1,36 @@
# scriptlib/utils/pwsh
Powershell helper scripts for punkshell.
## consolemode_server_async.ps1 (canonical - G-106)
The powershell console-mode fallback server: a persistent named-pipe server that
punk::console launches when twapi is absent on windows, to flip the console's line/echo
input flags for raw mode. This file is the canonical maintained copy; the module
`src/modules/punk/console-999999.0a1.0.tm` carries an embedded copy
(`punk::console::system::ps_consolemode_script_embedded`) as the last-resort resolution
for kits and unusual cwds. Keep the two in sync - the console testsuite
`src/tests/modules/punk/console/testsuites/console/psfallback.test` fails when they
diverge.
Key facts (details in the script header and `goals/archive/G-106-powershell-consolemode-fallback.md`):
- Delivered to powershell via `-c <script text>` with `<punkshell_*>` placeholders
substituted (no script file needed at run time; not subject to ExecutionPolicy).
Semicolons after each command are required by that delivery convention.
- Protocol: one line per named-pipe connection - `enableraw | disableraw | ping | exit`.
- The server watches the owning tcl process's pid and exits with it (orphan prevention).
- Quiet by default; `PUNK_PS_CONSOLEMODE_DEBUG=1` in the launching tcl process's
environment enables diagnostics on both sides.
- The NativeConsoleMethods C# snippet derives from posh-git (MIT - attribution carried in
the script).
Superseded experiment variants (consolemode_server.ps1, consolemode_server_async1.ps1,
consolemode_server_async.2ps1, consolemode.ps1 - the posh-git original this derives from -
and the one-shot consolemode_enableraw.ps1) were retired 2026-07-22 as part of G-106; see
VCS history to recover them.
## echotest.ps1
Standalone one-line scratch script (`write-host "test"`) - not part of the console-mode
fallback machinery.

201
scriptlib/utils/pwsh/consolemode.ps1

@ -1,201 +0,0 @@
# from github.com/dahlbyk/posh-git
# ------------------------------------------------------------------------------------
#Copyright (c) 2010-2018 Keith Dahlby, Keith Hill, and contributors
#Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
#The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
#THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# ------------------------------------------------------------------------------------
# Always skip setting the console mode on non-Windows platforms.
if (($PSVersionTable.PSVersion.Major -ge 6) -and !$IsWindows) {
function Set-ConsoleMode {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
param()
}
return
}
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
ENABLE_WINDOW_INPUT = 0x0008
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_INSERT_MODE = 0x0020
ENABLE_QUICK_EDIT_MODE = 0x0040
ENABLE_EXTENDED_FLAGS = 0x0080
ENABLE_AUTO_POSITION = 0x0100
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0200
}
[Flags()]
enum ConsoleModeOutputFlags
{
ENABLE_PROCESSED_OUTPUT = 0x0001
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
}
function Set-ConsoleMode
{
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
param(
[Parameter(ParameterSetName = "ANSI")]
[switch]
$ANSI,
[Parameter(ParameterSetName = "Mode")]
[uint32]
$Mode,
[switch]
$StandardInput
)
begin {
# Module import is speeded up by deferring the Add-Type until the first time this function is called.
# Add the NativeConsoleMethods type but only once per session.
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
}
end {
if ($ANSI)
{
$outputMode = [NativeConsoleMethods]::GetConsoleMode($false)
$null = [NativeConsoleMethods]::SetConsoleMode($false, $outputMode -bor [ConsoleModeOutputFlags]::ENABLE_VIRTUAL_TERMINAL_PROCESSING)
if ($StandardInput)
{
$inputMode = [NativeConsoleMethods]::GetConsoleMode($true)
$null = [NativeConsoleMethods]::SetConsoleMode($true, $inputMode -bor [ConsoleModeInputFlags]::ENABLE_VIRTUAL_TERMINAL_PROCESSING)
}
}
else
{
[NativeConsoleMethods]::SetConsoleMode($StandardInput, $Mode)
}
}
}
function Get-ConsoleMode
{
[Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")]
param(
[switch]
$StandardInput
)
begin {
# Module import is speeded up by deferring the Add-Type until the first time this function is called.
# Add the NativeConsoleMethods type but only once per session.
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
}
end {
$mode = [NativeConsoleMethods]::GetConsoleMode($StandardInput)
write-Output $mode
return
}
}
function psmain {
param (
[validateSet('enableRaw', 'disableRaw')]
[string]$Action
)
$inputflags = Get-ConsoleMode -StandardInput
$resultflags = $inputflags #default
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked"
if ($action -eq "enableraw") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT)
$adjustedflags = $inputflags -band ($disable)
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags)
}
} else {
#raw mode
$initialstate = "raw"
if ($action -eq "disableraw") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags)
}
}
#return in format that can act as a tcl dict
write-host "startflags: $inputflags initialstate: $initialstate action: $Action endflags: $resultflags"
}
psmain @args
#write-host (Get-ConsoleMode)
#Set-ConsoleMode -ANSI -StandardInput
#write-host (Get-ConsoleMode)
#test toggle
#if ((($inputflags -band [ConsoleModeInputFlags]::ENABLE_QUICK_EDIT_MODE)) -eq [ConsoleModeInputFlags]::ENABLE_QUICK_EDIT_MODE) {
# #quick edit is on
# write-host "quick edit is on"
# $adjustedflags = $inputflags -band (-bnot [uint32][ConsoleModeInputFlags]::ENABLE_QUICK_EDIT_MODE)
# $resultflags = [NativeConsoleMethods]::SetConsoleMode($true, $adjustedflags)
##
#} else {
# #quick edit is off
# write-host "quick edit is off"
# $resultflags = [NativeConsoleMethods]::SetConsoleMode($true, $inputflags -bor [ConsoleModeInputFlags]::ENABLE_QUICK_EDIT_MODE)
#}
#todo - parameters so it doesn't act as a toggle
#we want to be able to explicitly set raw vs cooked
#multi

91
scriptlib/utils/pwsh/consolemode_enableraw.ps1

@ -1,91 +0,0 @@
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter!
;
if ($PSVersionTable.PSVersion.Major -le 5) {
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e
#Presumably users are supposed to know not to have custom paths for powershell desktop containing a 'powershell' subfolder??
;
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';';
};
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
;
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
};
function psmain {
param (
[validateSet('enableRaw', 'disableRaw')]
[string]$Action
);
# $inputflags = Get-ConsoleMode -StandardInput;
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true);
$resultflags = $inputflags;
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked";
if ($action -eq "enableraw") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT);
$adjustedflags = $inputflags -band ($disable);
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
} else {
#raw mode
$initialstate = "raw";
if ($action -eq "disableraw") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT;
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
}
#return in format that can act as a tcl dict
#write-host "startflags: $inputflags initialstate: $initialstate action: $Action endflags: $resultflags";
};
psmain 'enableRaw';

144
scriptlib/utils/pwsh/consolemode_server.ps1

@ -1,144 +0,0 @@
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter!
;
if ($PSVersionTable.PSVersion.Major -le 5) {
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e
#Presumably users are supposed to know not to have custom paths for powershell desktop containing a 'powershell' subfolder??
;
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';';
};
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
;
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
};
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
function psmain {
param (
[validateSet('enableRaw', 'disableRaw')]
[string]$Action
);
# $inputflags = Get-ConsoleMode -StandardInput;
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true);
$resultflags = $inputflags;
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked";
if ($action -eq "enableraw") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT);
$adjustedflags = $inputflags -band ($disable);
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
} else {
#raw mode
$initialstate = "raw";
if ($action -eq "disableraw") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT;
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
}
#return in format that can act as a tcl dict
#write-host "startflags: $inputflags initialstate: $initialstate action: $Action endflags: $resultflags";
};
# psmain 'enableRaw';
$consoleid = $args[0];
if ([string]::IsNullOrEmpty($consoleid)) {
$consoleid= "<punkshell_consoleid>"
};
$pipeName = "punkshell_ps_consolemode_$consoleid";
"pipename: $pipeName"
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName);
try {
while ($true) {
#"Waiting for connection on '$pipeName'";
$pipeServer.WaitForConnection();
#"Connection established";
$pipeReader = New-Object System.IO.StreamReader($pipeServer);
#$pipeWriter = New-Object System.IO.StreamWriter($pipeServer);
#$pipeWriter.AutoFlush = $true;
$request = $pipeReader.ReadLine();
# "Received request: $request";
if ($request -eq "exit") {
"consolemode_server.ps1 Exiting";
exit;
} elseif ($request -eq "") {
#"Empty input";
$pipeServer.Disconnect();
#"Disconnected";
continue;
} elseif ($request -eq $none) {
"Remote disconnected before sending";
$pipeServer.Disconnect();
"Disconnected";
continue;
} elseif ($request -eq "enableraw") {
#$result = psmain 'enableRaw';
$null = psmain 'enableRaw'
# "Sending result: '$result'";
#$pipeWriter.Write($result);
$pipeServer.Disconnect();
continue;
} else {
"consolemode_server.ps1 ignoring request: $request";
$pipeServer.Disconnect();
continue;
}
}
}
finally {
$pipeServer.Dispose();
};

244
scriptlib/utils/pwsh/consolemode_server_async.2ps1

@ -1,244 +0,0 @@
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter!
;
if ($PSVersionTable.PSVersion.Major -le 5) {
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e
#Presumably users are supposed to know not to have custom paths for powershell desktop containing a 'powershell' subfolder??
;
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';';
};
$helper = @'
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Management.Automation.Runspaces;
public class RunspacedDelegateFactory
{
public static Delegate NewRunspacedDelegate(Delegate _delegate, Runspace runspace)
{
Action setRunspace = () => Runspace.DefaultRunspace = runspace;
return ConcatActionToDelegate(setRunspace, _delegate);
}
private static Expression ExpressionInvoke(Delegate _delegate, params Expression[] arguments)
{
var invokeMethod = _delegate.GetType().GetMethod("Invoke");
return Expression.Call(Expression.Constant(_delegate), invokeMethod, arguments);
}
public static Delegate ConcatActionToDelegate(Action a, Delegate d)
{
var parameters =
d.GetType().GetMethod("Invoke").GetParameters()
.Select(p => Expression.Parameter(p.ParameterType, p.Name))
.ToArray();
Expression body = Expression.Block(ExpressionInvoke(a), ExpressionInvoke(d, parameters));
var lambda = Expression.Lambda(d.GetType(), body, parameters);
var compiled = lambda.Compile();
return compiled;
}
}
'@
add-type -TypeDefinition $helper
#region console
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
;
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
};
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
function psmain {
param (
[validateSet('enableRaw', 'disableRaw')]
[string]$Action
);
# $inputflags = Get-ConsoleMode -StandardInput;
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true);
$resultflags = $inputflags;
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked";
if ($action -eq "enableraw") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT);
$adjustedflags = $inputflags -band ($disable);
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
} else {
#raw mode
$initialstate = "raw";
if ($action -eq "disableraw") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT;
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
}
#return in format that can act as a tcl dict
#write-host "startflags: $inputflags initialstate: $initialstate action: $Action endflags: $resultflags";
};
# psmain 'enableRaw';
#endregion console
$consoleid = $args[0];
if ([string]::IsNullOrEmpty($consoleid)) {
$consoleid= "<punkshell_consoleid>"
};
$pipeName = "punkshell_ps_consolemode_$consoleid";
"pipename: $pipeName";
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream(
$pipeName,
[System.IO.Pipes.PipeDirection]::In,
1,
[System.IO.Pipes.PipeTransmissionMode]::Byte,
[System.IO.Pipes.PipeOptions]::Asynchronous
);
#$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName);
;
# Define the callback function for when a client connects
$callback = [System.AsyncCallback]{
param($asyncResult);
$se = $asyncResult.AsyncState.Sync;
$ps = $asyncResult.AsyncState.Pipeserver;
$pn = $asyncResult.AsyncState.Pipename;
Write-Host "Client connected - $pn";
# End the asynchronous wait operation
;
$ps.EndWaitForConnection($asyncResult);
#??
;
# You can now perform read/write operations with the client
# For example, create a StreamReader and StreamWriter
;
$streamReader = New-Object System.IO.StreamReader($ps);
#$streamWriter = New-Object System.IO.StreamWriter($pipeServer);
#$streamWriter.AutoFlush = $true;
try {
$message = $streamReader.ReadLine();
Write-Host "Received: $message";
;
#$asyncResult.Message = $message;
;
} catch {
Write-Error "Error during communication: $($_.Exception.Message)";
} finally {
# sever connection with client but keep the named pipe
if ($streamReader -ne $null) {
Write-Host "streamreader closing";
$streamReader.Close();
Write-Host "streamreader closed";
}
Write-Host "Client disconnecting. $pn";
$ps.Disconnect();
Write-Host "Client disconnected. $pn";
#$ps.Disconnect();
#[System.Console]::Out.Flush();
;
};
write-host "HERE";
$se.set();
# Optionally, you can call BeginWaitForConnection again to listen for another client
# if your server is designed for multiple connections over time.
# $pipeServer.BeginWaitForConnection($callback, $null)
$ps.BeginWaitForConnection($callback, $null)
;
};
$syncEvent = New-Object System.Threading.ManualResetEvent($false);
$runspacedDelegate = [RunspacedDelegateFactory]::NewRunspacedDelegate($callback, [Runspace]::DefaultRunspace);
$loop = 0;
while ($loop -lt 15) {
Write-Host "Waiting for client connection on pipe '$pipeName'...";
# Begin the asynchronous wait for a client connection
;
$state = [PSCustomObject]@{
Loop = $loop
Sync = $SyncEvent
Pipeserver = $pipeServer
Pipename = $pipename
Message = ""
};
$x = $pipeServer.BeginWaitForConnection($runspacedDelegate, $state );
$syncEvent.WaitOne(10000);
# $x | Get-Member | write-host
;
write-host "msg: $(${x}.AsyncState.Message)"
if ($x.IsCompleted) {
write-host "completed";
};
$SyncEvent.reset();
$loop += 1;
#$cli = New-Object System.IO.Pipes.NamedPipeClientStream($pipeName);
#$cli.w
}
# Keep the script running to allow the asynchronous operation to complete
# In a real-world scenario, you might have a loop or other logic here.
#Read-Host "Press Enter to exit the server."
# Clean up
$pipeServer.Dispose();

541
scriptlib/utils/pwsh/consolemode_server_async.ps1

@ -1,263 +1,278 @@
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter!
;
if ($PSVersionTable.PSVersion.Major -le 5) {
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e
#Presumably users are supposed to know not to have custom paths for powershell desktop containing a 'powershell' subfolder??
;
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';';
};
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
;
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
ENABLE_WINDOW_INPUT = 0x0008
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_INSERT_MODE = 0x0020
ENABLE_QUICK_EDIT_MODE = 0x0040
ENABLE_EXTENDED_FLAGS = 0x0080
ENABLE_AUTO_POSITION = 0x0100
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
};
[Flags()]
enum ConsoleModeOutputFlags
{
ENABLE_PROCESSED_OUTPUT = 0x0001
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
DISABLE_NEWLINE_AUTO_RETURN = 0x0008
};
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
function rawmode {
param (
[validateSet('enable', 'disable')]
[string]$Action
);
# $inputflags = Get-ConsoleMode -StandardInput;
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true);
$resultflags = $inputflags;
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked";
if ($action -eq "enable") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT);
$adjustedflags = $inputflags -band ($disable);
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
} else {
#raw mode
$initialstate = "raw";
if ($action -eq "disable") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT;
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
}
#return in format that can act as a tcl dict
#write-host "startflags: $inputflags initialstate: $initialstate action: $Action endflags: $resultflags";
};
# rawmode 'enable';
$consoleid = $args[0];
if ([string]::IsNullOrEmpty($consoleid)) {
$consoleid= "<punkshell_consoleid>"
};
$pipeName = "punkshell_ps_consolemode_$consoleid";
"pipename: $pipeName"
#$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName);
$sharedData = [hashtable]::Synchronized(@{}) #TSV
$scriptblock = {
param($tsv);
Add-Type -AssemblyName System.IO.Pipes;
#$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream(
# $pipeName,
# [System.IO.Pipes.PipeDirection]::In,
# 1,
# [System.IO.Pipes.PipeTransmissionMode]::Byte,
# [System.IO.Pipes.PipeOptions]::Asynchronous
#);
;
$serverloop = 0;
while ($true) {
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName);
$serverloop += 1;
$pipeServer.WaitForConnection();
#write-host "Connection established";
$reader = New-Object System.IO.StreamReader($pipeServer);
if ($reader -ne $null) {
$message = $reader.ReadLine();
$reader.Close();
$reader.Dispose();
if ($message -ne $null) {
if ($message -eq "exit") {
#write-host "consolemode_server.ps1 exiting";
$tsv.State = "done";
break;
} elseif ($message -eq "enableraw") {
#write-host "RECEIVED: $message";
$tsv.Message = $message;
#$tsv.Message = "$pipeName serverloop: $serverloop";
$tsv.Ping = Get-Date;
$msync.Set();
};
} else {
# write-host "consolemode_server.ps1 null-msg";
$tsv.State = "done";
break;
};
};
$pipeServer.Disconnect();
$pipeServer.Dispose();
};
exit;
};
$keepalive_timeout = 20; #number of seconds without ping or other message, after which we terminate the process.
try {
$syncEvent = New-Object System.Threading.ManualResetEvent($false);
$runspace = [runspacefactory]::CreateRunspace();
[void]$runspace.Open();
$runspace.SessionStateProxy.SetVariable("pipeName", $pipeName);
$runspace.SessionStateProxy.SetVariable("pipeServer", $null);
$runspace.SessionStateProxy.SetVariable("msync", $syncEvent);
$powershell = [System.Management.Automation.PowerShell]::Create();
$powershell.Runspace = $runspace;
[void]$powershell.Addscript($scriptblock).AddArgument($sharedData);
$sharedData.State = "running";
$sharedData.Ping = Get-Date;
$asyncResult = $powershell.BeginInvoke();
write-Host "Started named pipe server $pipeName in runspace"
$loop = 0;
while ($true) {
$loop += 1;
#write-host "loop $loop";
[void]$syncEvent.WaitOne(($keepalive_timeout * 1000 / 2));
$msg = $sharedData.Message;
#Write-Host "$pipeName Last message: $msg";
$sharedData.Message = "";
if ($msg -eq "enableraw") {
$null = rawmode 'enable'
} elseif ($msg -eq "disableraw") {
$null = rawmode 'disable'
}
#write-host "STATE: $(${sharedData}.State)"
if ($(${sharedData}.State) -eq "done") {
break;
};
$tnow = Get-Date;
$elapsed = New-TimeSpan -Start $sharedData.Ping -End $tnow;
if ($elapsed.TotalSeconds -lt $keepalive_timeout) {
# write-host "ping ok";
} else {
write-host "ping stale for pipe $pipeName - exiting";
break;
}
[void]$syncEvent.Reset();
# start-sleep -Milliseconds 300
};
} finally {
# Failing to properly shut down the run process can leave an orphan powershell process
# We need to use a client for the named pipe to send an exit message.
# Write-Host "terminating process for $pipeName";
try {
# Write-Host "creating cli for $pipeName";
$cli = New-Object System.IO.Pipes.NamedPipeClientStream($pipeName);
$cli.connect(1000);
#Write-Host "sending exit for $pipeName";
$writer = new-object System.IO.StreamWriter($cli);
$writer.writeline("exit");
$writer.flush();
#Write-Host "disposing of cli for $pipeName";
$cli.Dispose();
} catch {
write-host "error during cli tidyup";
Write-Error "error: $($PSItem.ToString())";
Write-Host "Detailed Exception Message: $($PSItem.Exception.Message)";
};
try {
if ($null -ne $runspace) {
#Write-Host "closing runspace for $pipeName";
$runspace.Close();
#Write-Host "disposing of runspace for $pipeName";
$runspace.Dispose();
};
} catch {
write-host "error during runspace tidyup";
Write-Error "error: $($PSItem.ToString())";
Write-Host "Detailed Exception Message: $($PSItem.Exception.Message)";
} finally {
};
try {
if ($null -ne $powershell) {
#Write-Host "tidying up powershell for $pipeName";
$powershell.dispose();
};
} finally {
};
};
write-host "consolemode_server_async.ps1 shutdown for pipe $pipeName";
exit 0;
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter!
;
# consolemode_server_async.ps1 - punkshell console-mode fallback server (canonical copy) (G-106)
# Persistent named-pipe server used by punk::console when twapi is absent on windows.
# The Tcl side (punk::console::system) resolves this file - or falls back to its embedded copy - and launches:
# pwsh|powershell -nop -nol -noni -c <this text with the <punkshell_*> placeholders substituted>
# The -c (command string) mechanism is deliberate: it needs no script file at run time (kits) and is not
# subject to ExecutionPolicy restrictions that can block -File runs.
# The server process shares the launching process's console; it must NOT have its stdin redirected
# (the console input handle is how it reaches the console), and it never reads stdin.
# Placeholders (each overridable by a positional arg for standalone debug runs):
# <punkshell_consoleid> $args[0] unique id suffix for the pipe name
# <punkshell_parentpid> $args[1] pid of the owning tcl process; the server exits when it does
# <punkshell_psdebug> $args[2] "1" enables diagnostic write-host output (default silent)
# Standalone debug run example (from this directory):
# pwsh -nop -nol -f consolemode_server_async.ps1 test1 0 1
# Protocol (one line per named-pipe connection): enableraw | disableraw | ping | exit
# MAINTENANCE: src/modules/punk/console-999999.0a1.0.tm carries this file's text as
# punk::console::system::ps_consolemode_script_embedded (the last-resort resolution for kits and
# unusual cwds). Keep the two in sync - the console testsuite (psfallback.test) fails when they diverge.
;
# The NativeConsoleMethods C# snippet below derives from posh-git (github.com/dahlbyk/posh-git):
# Copyright (c) 2010-2018 Keith Dahlby, Keith Hill, and contributors - MIT license.
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
;
if ($PSVersionTable.PSVersion.Major -le 5) {
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e
;
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';';
};
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
;
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
ENABLE_WINDOW_INPUT = 0x0008
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_INSERT_MODE = 0x0020
ENABLE_QUICK_EDIT_MODE = 0x0040
ENABLE_EXTENDED_FLAGS = 0x0080
ENABLE_AUTO_POSITION = 0x0100
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
};
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
};
function rawmode {
param (
[validateSet('enable', 'disable')]
[string]$Action
);
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
};
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true);
$resultflags = $inputflags;
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked";
if ($action -eq "enable") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT);
$adjustedflags = $inputflags -band ($disable);
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
};
} else {
#raw mode
$initialstate = "raw";
if ($action -eq "disable") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT;
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
};
};
};
$consoleid = $args[0];
if ([string]::IsNullOrEmpty($consoleid)) {
$consoleid = "<punkshell_consoleid>"
};
$parentpidraw = $args[1];
if ([string]::IsNullOrEmpty($parentpidraw)) {
$parentpidraw = "<punkshell_parentpid>"
};
$psdebugraw = $args[2];
if ([string]::IsNullOrEmpty($psdebugraw)) {
$psdebugraw = "<punkshell_psdebug>"
};
$psdebug = ($psdebugraw -eq "1");
$pipeName = "punkshell_ps_consolemode_$consoleid";
function dbg {
param([string]$m);
if ($psdebug) {
write-host "consolemode_server($pipeName): $m"
};
};
dbg "starting - parentpidraw: $parentpidraw";
# Parent-process watch: hold a Process object obtained once by pid (handle-based, so immune to
# pid reuse) and poll HasExited in the main loop. This is the orphan-prevention guarantee - the
# owning tcl process does not need to send exit for this server to go away (G-106).
# An unparseable/zero parent pid (e.g a standalone run with placeholders unsubstituted) disables the watch.
$parentproc = $null;
[int]$parentpidnum = 0;
if ([int]::TryParse($parentpidraw, [ref]$parentpidnum) -and ($parentpidnum -gt 0)) {
try {
$parentproc = [System.Diagnostics.Process]::GetProcessById($parentpidnum);
} catch {
dbg "parent process $parentpidnum not found - exiting";
exit 1;
};
};
$sharedData = [hashtable]::Synchronized(@{});
$scriptblock = {
param($tsv);
Add-Type -AssemblyName System.IO.Pipes;
$keepgoing = $true;
while ($keepgoing) {
$pipeServer = $null;
try {
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName);
try {
$pipeServer.WaitForConnection();
$reader = New-Object System.IO.StreamReader($pipeServer);
$message = $reader.ReadLine();
if ($message -eq "exit") {
$tsv.State = "done";
[void]$msync.Set();
$keepgoing = $false;
} elseif (($message -eq "enableraw") -or ($message -eq "disableraw")) {
$tsv.Message = $message;
[void]$msync.Set();
} elseif ($message -eq "ping") {
# liveness probe - wake the main loop, no mode action
;
[void]$msync.Set();
};
# A null message (a connection that closed without sending a line - e.g a probe)
# and unknown messages are ignored; only an explicit exit message or
# parent-process death shuts the server down.
# Do NOT Close/Dispose the StreamReader here: that disposes the underlying pipe
# stream and a subsequent Disconnect/Dispose on it throws, killing this listener.
# Disposing the pipe stream (finally below) is the whole per-connection cleanup.
} finally {
if ($null -ne $pipeServer) {
$pipeServer.Dispose();
};
};
} catch {
# unexpected listener failure (e.g pipe name already in use) - shut down rather than spin
;
$tsv.State = "done";
[void]$msync.Set();
$keepgoing = $false;
};
};
};
$exitcode = 0;
try {
# AutoResetEvent: WaitOne consumes the signal atomically, so a Set that lands while the main
# loop is servicing a message is never lost (the next WaitOne returns immediately).
$syncEvent = New-Object System.Threading.AutoResetEvent($false);
$runspace = [runspacefactory]::CreateRunspace();
[void]$runspace.Open();
$runspace.SessionStateProxy.SetVariable("pipeName", $pipeName);
$runspace.SessionStateProxy.SetVariable("msync", $syncEvent);
$powershell = [System.Management.Automation.PowerShell]::Create();
$powershell.Runspace = $runspace;
[void]$powershell.Addscript($scriptblock).AddArgument($sharedData);
$sharedData.State = "running";
$sharedData.Message = "";
$asyncResult = $powershell.BeginInvoke();
dbg "named pipe server started in runspace";
while ($true) {
[void]$syncEvent.WaitOne(5000);
$msg = $sharedData.Message;
$sharedData.Message = "";
if ($msg -eq "enableraw") {
dbg "enableraw";
$null = rawmode 'enable';
} elseif ($msg -eq "disableraw") {
dbg "disableraw";
$null = rawmode 'disable';
};
if ($sharedData.State -eq "done") {
dbg "exit message received";
break;
};
if (($null -ne $parentproc) -and $parentproc.HasExited) {
dbg "parent process $parentpidnum has exited";
break;
};
};
} finally {
# The listener runspace may be parked in WaitForConnection - connect once and send exit to
# unblock it, otherwise the process could linger on a parked runspace thread. Skipped when
# the listener already shut itself down (State done - it exited on an exit message or error).
if ($sharedData.State -ne "done") {
try {
$cli = New-Object System.IO.Pipes.NamedPipeClientStream($pipeName);
$cli.connect(1000);
$writer = new-object System.IO.StreamWriter($cli);
$writer.writeline("exit");
$writer.flush();
$cli.Dispose();
} catch {
dbg "listener unblock skipped: $($PSItem.Exception.Message)";
};
};
try {
if ($null -ne $runspace) {
$runspace.Close();
$runspace.Dispose();
};
} catch {
dbg "runspace tidyup error: $($PSItem.Exception.Message)";
};
try {
if ($null -ne $powershell) {
$powershell.dispose();
};
} catch {
dbg "powershell tidyup error: $($PSItem.Exception.Message)";
};
};
dbg "shutdown complete";
exit $exitcode;

266
scriptlib/utils/pwsh/consolemode_server_async1.ps1

@ -1,266 +0,0 @@
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter!
;
if ($PSVersionTable.PSVersion.Major -le 5) {
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e
#Presumably users are supposed to know not to have custom paths for powershell desktop containing a 'powershell' subfolder??
;
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';';
};
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
;
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
};
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
function psmain {
param (
[validateSet('enableRaw', 'disableRaw')]
[string]$Action
);
# $inputflags = Get-ConsoleMode -StandardInput;
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
}
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true);
$resultflags = $inputflags;
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked";
if ($action -eq "enableraw") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT);
$adjustedflags = $inputflags -band ($disable);
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
} else {
#raw mode
$initialstate = "raw";
if ($action -eq "disableraw") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT;
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
}
}
#return in format that can act as a tcl dict
#write-host "startflags: $inputflags initialstate: $initialstate action: $Action endflags: $resultflags";
};
# psmain 'enableRaw'
;
$consoleid = $args[0];
if ([string]::IsNullOrEmpty($consoleid)) {
$consoleid= "<punkshell_consoleid>"
};
$pipeName = "punkshell_ps_consolemode_$consoleid";
"pipename: $pipeName"
# Create the NamedPipeServerStream
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream(
$pipeName,
[System.IO.Pipes.PipeDirection]::In,
1,
[System.IO.Pipes.PipeTransmissionMode]::Message,
[System.IO.Pipes.PipeOptions]::Asynchronous
);
;
#$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName);
;
# Create a synchronization object (ManualResetEvent) to signal completion
;
$syncEvent = New-Object System.Threading.ManualResetEvent($false)
# Define the callback function for BeginWaitForConnection
$connectionCallback = [System.AsyncCallback]{
param($ar,$s);
# Get the NamedPipeServerStream from the AsyncResult object
;
$server = $ar.AsyncState;
# End the asynchronous wait operation
;
$server.EndWaitForConnection($ar);
Write-Host "Client connected!";
# Create a new runspace to handle client communication
;
$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace();
$runspace.Open();
# Create a PowerShell pipeline within the runspace
;
$powershell = [System.Management.Automation.PowerShell]::Create();
$powershell.Runspace = $runspace;
$scriptBlock = {
param($pipeStream);
$reader = New-Object System.IO.StreamReader($pipeStream);
#$writer = New-Object System.IO.StreamWriter($pipeStream);
#$writer.AutoFlush = $true;
$message = $reader.ReadLine();
Write-Host "Received from client: $message";
#$response = "Server received: $message"
#$writer.WriteLine($response)
#Write-Host "Sent to client: $response"
# Disconnect the pipe to allow new connections if desired
;
$pipeStream.Disconnect();
};
# Add the script block to the PowerShell pipeline and pass the pipe stream
;
$powershell.AddScript($scriptBlock).AddArgument($server);
# Invoke the pipeline asynchronously
;
$asyncResult = $powershell.BeginInvoke();
# You can do other work here while the client communication happens in the runspace
;
;
# Wait for the pipeline to complete and close the runspace
$powershell.EndInvoke($asyncResult);
$powershell.Dispose();
$runspace.Close();
$runspace.Dispose();
Write-Host "Client communication handled. Waiting for next connection...";
$s.Set();
# Begin waiting for the next connection;
#$server.BeginWaitForConnection($callback, $server);
;
}
$global:keep_listening = $true;
while ($global:keep_listening) {
# Begin waiting for a client connection asynchronously
;
$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace();
$runspace.Open();
# Create a PowerShell pipeline within the runspace
;
$powershell = [System.Management.Automation.PowerShell]::Create();
$powershell.Runspace = $runspace;
$scriptblock = {
param($p,$s);
Write-Host "Waiting for client connection on pipe: $p";
$p.BeginWaitForConnection($connectionCallback, $p,$s);
$s.WaitOne(10000);
}
$powershell.AddScript($scriptBlock)
[void]$powershell.AddParameter('p',$pipeName);
[void]$powershell.AddParameter('s',$syncEvent);
$asyncResult = $powershell.BeginInvoke();
# You can do other work here while the client communication happens in the runspace
;
write-host "interim"
;
# Wait for the pipeline to complete and close the runspace
$powershell.EndInvoke($asyncResult);
$powershell.Dispose();
$runspace.Close();
$runspace.Dispose();
write-host "looping"
};
#$pipeServer.BeginWaitForConnection($connectionCallback, $pipeServer);
Write-Host "Server shutting down.";
$pipeServer.Dispose();
#try {
# while ($true) {
# #"Waiting for connection on '$pipeName'";
# $pipeServer.WaitForConnection();
# #"Connection established";
# $pipeReader = New-Object System.IO.StreamReader($pipeServer);
# #$pipeWriter = New-Object System.IO.StreamWriter($pipeServer);
# #$pipeWriter.AutoFlush = $true;
# $request = $pipeReader.ReadLine();
# # "Received request: $request";
# if ($request -eq "exit") {
# "consolemode_server.ps1 Exiting";
# exit;
# } elseif ($request -eq "") {
# #"Empty input";
# $pipeServer.Disconnect();
# #"Disconnected";
# continue;
# } elseif ($request -eq $none) {
# "Remote disconnected before sending";
# $pipeServer.Disconnect();
# "Disconnected";
# continue;
# } elseif ($request -eq "enableraw") {
# #$result = psmain 'enableRaw';
# $null = psmain 'enableRaw'
# # "Sending result: '$result'";
# #$pipeWriter.Write($result);
# $pipeServer.Disconnect();
# continue;
# } else {
# "consolemode_server.ps1 ignoring request: $request";
# $pipeServer.Disconnect();
# continue;
# }
# }
#}
#finally {
# $pipeServer.Dispose();
#};

1
src/AGENTS.md

@ -63,6 +63,7 @@ Recovery after a wrong path guess:
- Use `tclsh src/make.tcl modules` to build just the module packages.
- Use `tclsh src/make.tcl libs` to build just the library packages.
- Use `tclsh src/make.tcl packages` to build both modules and libraries.
- Agent runs of `tclsh src/make.tcl ...` capture/pipe output: set `NO_COLOR=1` in the environment so captured output carries no ANSI SGR sequences (honoured from startup). Interactive terminal runs keep colour by default. G-113 tracks making this automatic via tty detection - revise this bullet when it lands.
- Use `tclsh src/make.tcl vendorupdate` to refresh vendormodules from config. It warns (non-fatal) for each source project whose fossil/git checkout is dirty - vendored artifacts built from a dirty tree have no committed provenance; commit in the source project to clear the warning (enforcement policy tracked by goal G-026).
- All build/promotion commands (`project`, `packages`, `modules`, `libs`, `vfs`, `vfslibs`, `bin`, `bootsupport`, `vfscommonupdate`) warn similarly when this project's own `src/` has uncommitted fossil/git changes (dirt outside `src/` is ignored). Warn-only by default; pass `-dirty-abort` to make the check aborting. For evaluating uncommitted source without a build, prefer `<builtexe> src` / `<builtexe> src shell`. The check is `punkboot::utils::vcs_dirty_warnings` (optional scope argument) loaded guardedly from bootsupport - if the snapshot is stale/missing the check degrades to a skip notice (but `-dirty-abort` then aborts rather than silently losing the requested strictness).
- Provenance warnings (dirty-src gate + vendorupdate source-project check) print with a plain column-0 `PROVENANCE-WARNING:` token (grep for it in captured build output) and are recapped at the end of the run via make.tcl's wrapped `::exit`. Interactive terminal runs get a 3-second ctrl-c grace countdown before a dirty build proceeds; piped/agent runs skip it. `tclsh src/make.tcl check` reports the current src provenance status and what the build commands would do.

77
src/bootsupport/modules/punk-0.2.6.tm → src/bootsupport/modules/punk-0.2.7.tm

@ -8983,10 +8983,77 @@ namespace eval punk {
return $chunks
}
register topics {help} "List help topics"
register tcl {} "Tcl version warnings"
register env {environment} "punkshell environment vars"
register console {term terminal} "Some console behaviour tests and warnings"
punk::args::define {
@id -id ::punk::helptopic::platforms
@cmd -name "help platforms"\
-summary\
"Canonical punkshell platform names."\
-help\
"Show the canonical punkshell platform-dir names
(punk::platform::platforms) with status, the artifact-tree
tiers each name serves, and the buildsuite axis (whether the
punkshell zig buildsuites produce runtimes for the platform -
separate from hosting: punkbin-structured repos can carry
runtimes built by any mechanism), marking the running
interpreter's own platform. These names organize the punkbin
artifact repo's platform folders, bin/runtime/<platform>/
(punk-runtime fetch/list/use -platform), the lib_tclX /
vendorlib_tclX auto_path dirs wired by the boot, and the
runtime-artifact metadata target field. The raw Tcl
platform-package identifiers are shown for comparison
(punk::platform::normalize folds their version-dependent
aliases - amd64/aarch64/macos - into the canonical names)."
@values -min 0 -max 0
}
proc platforms {context args} {
if {[catch {package require punk::platform} errM]} {
return [list [list stderr "help platforms: punk::platform package not available ($errM)\n"]]
}
set frametype [dict get $context frametype]
set chunks [list]
set local_lib [punk::platform::local -tier lib]
set local_runtime [punk::platform::local -tier runtime]
set title "[a+ brightgreen] Canonical punkshell platform names: "
set t [textblock::class::table new -show_seps 0]
$t configure -frametype $frametype
$t add_column -headers [list "Platform"]
$t add_column -headers [list "Status"]
$t add_column -headers [list "Tiers"]
$t add_column -headers [list "Buildsuite"]
$t add_column -headers [list "Notes"]
dict for {p pinfo} [punk::platform::platforms] {
set pshown $p
if {$p eq $local_lib || $p eq $local_runtime} {
set pshown "* $p"
}
$t add_row [list $pshown [dict get $pinfo status] [join [dict get $pinfo tiers] ,] [dict get $pinfo buildsuite] [dict get $pinfo notes]]
}
foreach c {0 1 2 3} {
$t configure_column $c -minwidth [expr {[$t column_datawidth $c] + 2}]
}
$t configure -title $title
set text [$t print]
$t destroy
lappend chunks [list stdout $text]
set detail "* = this interpreter. local: lib-tier $local_lib"
if {$local_runtime ne $local_lib} {
append detail " runtime-tier $local_runtime"
}
append detail "\ntiers: runtime = punkbin + bin/runtime folders; lib = lib_tclX/vendorlib_tclX auto_path dirs"
append detail "\nbuildsuite: whether the punkshell zig buildsuites produce runtimes for the platform"
append detail "\n(a separate axis - punkbin-structured repos can host runtimes built by any mechanism)"
catch {
append detail "\nraw Tcl platform package ([package present platform]): generic [platform::generic] identify [platform::identify]"
}
lappend chunks [list stdout $detail\n]
return $chunks
}
register topics {help} "List help topics"
register tcl {} "Tcl version warnings"
register env {environment} "punkshell environment vars"
register console {term terminal} "Some console behaviour tests and warnings"
register platforms {platform} "Canonical punkshell platform names"
}
#return list of {chan chunk} elements
@ -9438,7 +9505,7 @@ punkcheck::cli set_alias punkcheck
package provide punk [namespace eval punk {
#FUNCTL
variable version
set version 0.2.6
set version 0.2.7
}]

753
src/bootsupport/modules/punk/console-0.7.2.tm → src/bootsupport/modules/punk/console-0.8.0.tm

@ -7,7 +7,7 @@
# (C) 2023
#
# @@ Meta Begin
# Application punk::console 0.7.2
# Application punk::console 0.8.0
# Meta platform tcl
# Meta license <unspecified>
# @@ Meta End
@ -17,7 +17,7 @@
# doctools header
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
#*** !doctools
#[manpage_begin punkshell_module_punk::console 0 0.7.2]
#[manpage_begin punkshell_module_punk::console 0 0.8.0]
#[copyright "2024"]
#[titledesc {punk console}] [comment {-- Name section and table of contents description --}]
#[moddesc {punk console}] [comment {-- Description at end of page heading --}]
@ -5746,6 +5746,599 @@ namespace eval punk::console::check {
}
namespace eval punk::console::system {
#-------------------------------------------------------------------------------------
#G-106: powershell console-mode fallback (raw mode for twapi-less windows runtimes)
#A persistent powershell/pwsh named-pipe server process, shared process-wide via tsv,
#flips the console's line/echo input flags on request (the flags live on the console
#object, so a sibling process attached to the same console can set them for us).
#Started lazily on first use (loading punk::console in a piped/non-console or
#worker-thread context must not spawn processes or emit noise); the server watches this
#process's pid and exits with it, so no orphan pwsh processes are left behind.
#Quiet by default - set env PUNK_PS_CONSOLEMODE_DEBUG=1 for diagnostics on both sides.
#-------------------------------------------------------------------------------------
#module location captured at load time - [info script] is empty later at call time,
#and ::argv0 is absent in secondary threads.
variable module_dir ""
catch {
set module_dir [file dirname [file normalize [info script]]]
}
#Embedded copy of scriptlib/utils/pwsh/consolemode_server_async.ps1 (the canonical
#maintained file) - the last-resort script source so the fallback works from kits and
#unusual cwds with no scriptlib on disk. Leading/trailing whitespace is insignificant
#(the text is delivered to powershell via -c). Keep in sync with the canonical file -
#the console testsuite (psfallback.test) compares them.
variable ps_consolemode_script_embedded {
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter!
;
# consolemode_server_async.ps1 - punkshell console-mode fallback server (canonical copy) (G-106)
# Persistent named-pipe server used by punk::console when twapi is absent on windows.
# The Tcl side (punk::console::system) resolves this file - or falls back to its embedded copy - and launches:
# pwsh|powershell -nop -nol -noni -c <this text with the <punkshell_*> placeholders substituted>
# The -c (command string) mechanism is deliberate: it needs no script file at run time (kits) and is not
# subject to ExecutionPolicy restrictions that can block -File runs.
# The server process shares the launching process's console; it must NOT have its stdin redirected
# (the console input handle is how it reaches the console), and it never reads stdin.
# Placeholders (each overridable by a positional arg for standalone debug runs):
# <punkshell_consoleid> $args[0] unique id suffix for the pipe name
# <punkshell_parentpid> $args[1] pid of the owning tcl process; the server exits when it does
# <punkshell_psdebug> $args[2] "1" enables diagnostic write-host output (default silent)
# Standalone debug run example (from this directory):
# pwsh -nop -nol -f consolemode_server_async.ps1 test1 0 1
# Protocol (one line per named-pipe connection): enableraw | disableraw | ping | exit
# MAINTENANCE: src/modules/punk/console-0.8.0.tm carries this file's text as
# punk::console::system::ps_consolemode_script_embedded (the last-resort resolution for kits and
# unusual cwds). Keep the two in sync - the console testsuite (psfallback.test) fails when they diverge.
;
# The NativeConsoleMethods C# snippet below derives from posh-git (github.com/dahlbyk/posh-git):
# Copyright (c) 2010-2018 Keith Dahlby, Keith Hill, and contributors - MIT license.
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
;
if ($PSVersionTable.PSVersion.Major -le 5) {
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e
;
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';';
};
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
;
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
ENABLE_WINDOW_INPUT = 0x0008
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_INSERT_MODE = 0x0020
ENABLE_QUICK_EDIT_MODE = 0x0040
ENABLE_EXTENDED_FLAGS = 0x0080
ENABLE_AUTO_POSITION = 0x0100
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
};
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
};
function rawmode {
param (
[validateSet('enable', 'disable')]
[string]$Action
);
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
};
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true);
$resultflags = $inputflags;
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked";
if ($action -eq "enable") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT);
$adjustedflags = $inputflags -band ($disable);
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
};
} else {
#raw mode
$initialstate = "raw";
if ($action -eq "disable") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT;
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
};
};
};
$consoleid = $args[0];
if ([string]::IsNullOrEmpty($consoleid)) {
$consoleid = "<punkshell_consoleid>"
};
$parentpidraw = $args[1];
if ([string]::IsNullOrEmpty($parentpidraw)) {
$parentpidraw = "<punkshell_parentpid>"
};
$psdebugraw = $args[2];
if ([string]::IsNullOrEmpty($psdebugraw)) {
$psdebugraw = "<punkshell_psdebug>"
};
$psdebug = ($psdebugraw -eq "1");
$pipeName = "punkshell_ps_consolemode_$consoleid";
function dbg {
param([string]$m);
if ($psdebug) {
write-host "consolemode_server($pipeName): $m"
};
};
dbg "starting - parentpidraw: $parentpidraw";
# Parent-process watch: hold a Process object obtained once by pid (handle-based, so immune to
# pid reuse) and poll HasExited in the main loop. This is the orphan-prevention guarantee - the
# owning tcl process does not need to send exit for this server to go away (G-106).
# An unparseable/zero parent pid (e.g a standalone run with placeholders unsubstituted) disables the watch.
$parentproc = $null;
[int]$parentpidnum = 0;
if ([int]::TryParse($parentpidraw, [ref]$parentpidnum) -and ($parentpidnum -gt 0)) {
try {
$parentproc = [System.Diagnostics.Process]::GetProcessById($parentpidnum);
} catch {
dbg "parent process $parentpidnum not found - exiting";
exit 1;
};
};
$sharedData = [hashtable]::Synchronized(@{});
$scriptblock = {
param($tsv);
Add-Type -AssemblyName System.IO.Pipes;
$keepgoing = $true;
while ($keepgoing) {
$pipeServer = $null;
try {
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName);
try {
$pipeServer.WaitForConnection();
$reader = New-Object System.IO.StreamReader($pipeServer);
$message = $reader.ReadLine();
if ($message -eq "exit") {
$tsv.State = "done";
[void]$msync.Set();
$keepgoing = $false;
} elseif (($message -eq "enableraw") -or ($message -eq "disableraw")) {
$tsv.Message = $message;
[void]$msync.Set();
} elseif ($message -eq "ping") {
# liveness probe - wake the main loop, no mode action
;
[void]$msync.Set();
};
# A null message (a connection that closed without sending a line - e.g a probe)
# and unknown messages are ignored; only an explicit exit message or
# parent-process death shuts the server down.
# Do NOT Close/Dispose the StreamReader here: that disposes the underlying pipe
# stream and a subsequent Disconnect/Dispose on it throws, killing this listener.
# Disposing the pipe stream (finally below) is the whole per-connection cleanup.
} finally {
if ($null -ne $pipeServer) {
$pipeServer.Dispose();
};
};
} catch {
# unexpected listener failure (e.g pipe name already in use) - shut down rather than spin
;
$tsv.State = "done";
[void]$msync.Set();
$keepgoing = $false;
};
};
};
$exitcode = 0;
try {
# AutoResetEvent: WaitOne consumes the signal atomically, so a Set that lands while the main
# loop is servicing a message is never lost (the next WaitOne returns immediately).
$syncEvent = New-Object System.Threading.AutoResetEvent($false);
$runspace = [runspacefactory]::CreateRunspace();
[void]$runspace.Open();
$runspace.SessionStateProxy.SetVariable("pipeName", $pipeName);
$runspace.SessionStateProxy.SetVariable("msync", $syncEvent);
$powershell = [System.Management.Automation.PowerShell]::Create();
$powershell.Runspace = $runspace;
[void]$powershell.Addscript($scriptblock).AddArgument($sharedData);
$sharedData.State = "running";
$sharedData.Message = "";
$asyncResult = $powershell.BeginInvoke();
dbg "named pipe server started in runspace";
while ($true) {
[void]$syncEvent.WaitOne(5000);
$msg = $sharedData.Message;
$sharedData.Message = "";
if ($msg -eq "enableraw") {
dbg "enableraw";
$null = rawmode 'enable';
} elseif ($msg -eq "disableraw") {
dbg "disableraw";
$null = rawmode 'disable';
};
if ($sharedData.State -eq "done") {
dbg "exit message received";
break;
};
if (($null -ne $parentproc) -and $parentproc.HasExited) {
dbg "parent process $parentpidnum has exited";
break;
};
};
} finally {
# The listener runspace may be parked in WaitForConnection - connect once and send exit to
# unblock it, otherwise the process could linger on a parked runspace thread. Skipped when
# the listener already shut itself down (State done - it exited on an exit message or error).
if ($sharedData.State -ne "done") {
try {
$cli = New-Object System.IO.Pipes.NamedPipeClientStream($pipeName);
$cli.connect(1000);
$writer = new-object System.IO.StreamWriter($cli);
$writer.writeline("exit");
$writer.flush();
$cli.Dispose();
} catch {
dbg "listener unblock skipped: $($PSItem.Exception.Message)";
};
};
try {
if ($null -ne $runspace) {
$runspace.Close();
$runspace.Dispose();
};
} catch {
dbg "runspace tidyup error: $($PSItem.Exception.Message)";
};
try {
if ($null -ne $powershell) {
$powershell.dispose();
};
} catch {
dbg "powershell tidyup error: $($PSItem.Exception.Message)";
};
};
dbg "shutdown complete";
exit $exitcode;
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::console::system::ps_consolemode_script_get
@cmd -name "punk::console::system::ps_consolemode_script_get"\
-summary\
"Resolve the powershell console-mode fallback server script text."\
-help\
"Resolves the consolemode_server_async.ps1 script text used by the
twapi-less windows raw-mode fallback (G-106).
Resolution order: PUNK_PS_CONSOLEMODE_SCRIPT env override (used only
when the named file exists), the argv0-derived project location, then
module-location-derived project locations, then the embedded copy
carried by this module (so kits and unusual cwds always resolve).
Returns a dict with keys: source (env|scriptlib|embedded), path (empty
string for embedded) and contents (raw script text with <punkshell_*>
placeholders unsubstituted)."
}]
lappend PUNKARGS [list {
@id -id ::punk::console::system::ps_consolemode_server_ensure
@cmd -name "punk::console::system::ps_consolemode_server_ensure"\
-summary\
"Ensure the process-wide powershell console-mode server is started."\
-help\
"Ensures a single persistent powershell console-mode server process
exists for this tcl process (windows only). Server identity is shared
across all interps/threads via tsv punk_console ps_server_* entries;
the first caller spawns (pwsh.exe preferred, powershell.exe fallback),
later callers reuse. The server is passed this process's pid and exits
when this process does.
Returns a dict: ok (0|1), and on ok=1 pipename, pid, spawntime
(ms clock) and just_started (1 when this call spawned the server);
on ok=0 an error key describes why."
}]
lappend PUNKARGS [list {
@id -id ::punk::console::system::ps_consolemode_send
@cmd -name "punk::console::system::ps_consolemode_send"\
-summary\
"Send a command to the powershell console-mode server."\
-help\
"Sends one protocol message (enableraw|disableraw|ping|exit) to the
powershell console-mode server, ensuring the server first (spawning it
if required). Connection attempts retry until the deadline - a freshly
spawned server is given a generous window for powershell startup. If
the recorded server proves unreachable the recorded state is cleared
and one respawn is attempted.
Returns a dict: ok (0|1), and on failure an error key."
@opts
-deadlinems -type integer -optional 1 -help\
"Override the per-attempt connection deadline in milliseconds.
Default: 15000 within 15s of server spawn (powershell startup),
2500 thereafter."
@values -min 1 -max 1
msg -type string -help\
"Protocol message: enableraw, disableraw, ping or exit."
}]
lappend PUNKARGS [list {
@id -id ::punk::console::system::ps_consolemode_server_stop
@cmd -name "punk::console::system::ps_consolemode_server_stop"\
-summary\
"Stop the recorded powershell console-mode server, if any."\
-help\
"Best-effort shutdown of the recorded powershell console-mode server:
sends the exit protocol message (short deadline, no respawn) and clears
the recorded tsv state. Not required for orphan prevention - the server
watches this process's pid and exits with it - but useful for tests and
for releasing the server early.
Returns a dict: ok 1, stopped (1 if the exit message was delivered),
and pipename (when a server was recorded)."
}]
}
proc ps_consolemode_script_get {} {
#G-106 - see PUNKARGS definition for behaviour contract
variable module_dir
variable ps_consolemode_script_embedded
set relpath scriptlib/utils/pwsh/consolemode_server_async.ps1
set candidates [list]
if {[info exists ::env(PUNK_PS_CONSOLEMODE_SCRIPT)] && $::env(PUNK_PS_CONSOLEMODE_SCRIPT) ne ""} {
#explicit dev/test override - falls through if the named file is missing
lappend candidates [list env $::env(PUNK_PS_CONSOLEMODE_SCRIPT)]
}
if {[info exists ::argv0] && $::argv0 ne ""} {
#argv0 grandparent as project root: covers src/make.tcl (repo root) and bin/<kit> launches
lappend candidates [list scriptlib [file dirname [file dirname [file normalize $::argv0]]]/$relpath]
}
if {$module_dir ne ""} {
#module in <project>/modules/punk -> project root is 2 up
#module in <project>/src/modules/punk -> project root is 3 up
#(a zipfs kit module path simply fails the file-exists probes)
lappend candidates [list scriptlib [file dirname [file dirname $module_dir]]/$relpath]
lappend candidates [list scriptlib [file dirname [file dirname [file dirname $module_dir]]]/$relpath]
}
foreach cand $candidates {
lassign $cand source path
if {[file exists $path]} {
set readok [expr {![catch {
set fd [open $path r]
chan configure $fd -translation binary
set contents [read $fd]
close $fd
}]}]
if {$readok} {
return [dict create source $source path $path contents $contents]
}
}
}
return [dict create source embedded path "" contents $ps_consolemode_script_embedded]
}
proc ps_consolemode_server_ensure {} {
#G-106 - see PUNKARGS definition for behaviour contract
if {"windows" ne $::tcl_platform(platform)} {
return [dict create ok 0 just_started 0 error "powershell consolemode server is windows-only"]
}
#fast path - another interp/thread (or an earlier call) already started the server
if {[tsv::exists punk_console ps_server_pipename]} {
set result [dict create ok 1 just_started 0]
dict set result pipename [tsv::get punk_console ps_server_pipename]
dict set result pid [tsv::get punk_console ps_server_pid]
dict set result spawntime [tsv::get punk_console ps_server_spawntime]
return $result
}
set ps_cmd [auto_execok pwsh.exe]
if {$ps_cmd eq ""} {
set ps_cmd [auto_execok powershell.exe]
}
if {$ps_cmd eq ""} {
return [dict create ok 0 just_started 0 error "neither pwsh.exe nor powershell.exe found on PATH"]
}
set scriptinfo [::punk::console::system::ps_consolemode_script_get]
set psdebug 0
if {[info exists ::env(PUNK_PS_CONSOLEMODE_DEBUG)] && $::env(PUNK_PS_CONSOLEMODE_DEBUG) ni [list "" 0]} {
set psdebug 1
}
set spawned 0
set spawn_error ""
set pipename ""
set spawnpid ""
set spawntime 0
tsv::lock punk_console {
if {[tsv::exists punk_console ps_server_pipename]} {
#another thread won the race
set pipename [tsv::get punk_console ps_server_pipename]
set spawnpid [tsv::get punk_console ps_server_pid]
set spawntime [tsv::get punk_console ps_server_spawntime]
} else {
set ps_consoleid [pid]-[expr {int(999999 * rand())+1}]
set contents [string map [list <punkshell_consoleid> $ps_consoleid <punkshell_parentpid> [pid] <punkshell_psdebug> $psdebug] [dict get $scriptinfo contents]]
set pipename {\\.\pipe\punkshell_ps_consolemode_}
append pipename $ps_consoleid
#stdin must stay inherited - the console input handle is how the server reaches
#the console. stdout/stderr are silenced unless debugging.
if {$psdebug} {
puts stderr "punk::console: starting persistent powershell consolemode server pipename: $pipename (script source: [dict get $scriptinfo source] [dict get $scriptinfo path])"
set spawncatch [catch {exec {*}$ps_cmd -nop -nol -noni -c $contents &} spawnresult]
} else {
set spawncatch [catch {exec {*}$ps_cmd -nop -nol -noni -c $contents > NUL 2> NUL &} spawnresult]
}
if {$spawncatch} {
set spawn_error $spawnresult
} else {
set spawnpid [lindex $spawnresult 0]
set spawntime [clock milliseconds]
tsv::set punk_console ps_server_pipename $pipename
tsv::set punk_console ps_server_pid $spawnpid
tsv::set punk_console ps_server_spawntime $spawntime
set spawned 1
}
}
}
if {$spawn_error ne ""} {
return [dict create ok 0 just_started 0 error "failed to launch powershell consolemode server: $spawn_error"]
}
set result [dict create ok 1 just_started $spawned]
dict set result pipename $pipename
dict set result pid $spawnpid
dict set result spawntime $spawntime
return $result
}
proc ps_consolemode_send {msg args} {
#G-106 - see PUNKARGS definition for behaviour contract
#manual args parsing - called on raw enable/disable paths
set opt_deadlinems ""
foreach {k v} $args {
switch -- $k {
-deadlinems {
set opt_deadlinems $v
}
default {
error "ps_consolemode_send unknown option '$k' - known options: -deadlinems"
}
}
}
set ensure [::punk::console::system::ps_consolemode_server_ensure]
if {![dict get $ensure ok]} {
return [dict create ok 0 error [dict get $ensure error]]
}
set attempt 0
set errMsg ""
while 1 {
incr attempt
set deadlinems $opt_deadlinems
if {$deadlinems eq ""} {
#a freshly spawned server (from this call or another interp moments ago) can take
#seconds to begin listening (powershell startup) - allow for it
set age [expr {[clock milliseconds] - [dict get $ensure spawntime]}]
if {$age < 15000} {
set deadlinems 15000
} else {
set deadlinems 2500
}
}
set pipename [dict get $ensure pipename]
set endtime [expr {[clock milliseconds] + $deadlinems}]
while {[clock milliseconds] < $endtime} {
set ok_write 0
if {![catch {open $pipename w} pipe]} {
if {![catch {
chan configure $pipe -buffering line
puts -nonewline $pipe "$msg\r\n"
close $pipe
} errMsg]} {
set ok_write 1
} else {
catch {close $pipe}
}
} else {
set errMsg $pipe
}
if {$ok_write} {
return [dict create ok 1 attempt $attempt]
}
after 100
}
if {$attempt >= 2} {
return [dict create ok 0 error "failed to reach powershell consolemode server on $pipename: $errMsg"]
}
#the recorded server may be stale (e.g killed externally) - clear the recorded state
#(only if it still names the pipe we tried) and respawn once
tsv::lock punk_console {
if {[tsv::exists punk_console ps_server_pipename] && [tsv::get punk_console ps_server_pipename] eq $pipename} {
tsv::unset punk_console ps_server_pipename
tsv::unset punk_console ps_server_pid
tsv::unset punk_console ps_server_spawntime
}
}
set ensure [::punk::console::system::ps_consolemode_server_ensure]
if {![dict get $ensure ok]} {
return [dict create ok 0 error [dict get $ensure error]]
}
}
}
proc ps_consolemode_server_stop {} {
#G-106 - see PUNKARGS definition for behaviour contract
if {![tsv::exists punk_console ps_server_pipename]} {
return [dict create ok 1 stopped 0 note "no powershell consolemode server recorded"]
}
set pipename [tsv::get punk_console ps_server_pipename]
#deliberately not via ps_consolemode_send - that would respawn an unreachable server
set sent 0
set endtime [expr {[clock milliseconds] + 1000}]
while {[clock milliseconds] < $endtime} {
if {![catch {open $pipename w} pipe]} {
if {![catch {
chan configure $pipe -buffering line
puts -nonewline $pipe "exit\r\n"
close $pipe
}]} {
set sent 1
break
} else {
catch {close $pipe}
}
}
after 100
}
tsv::lock punk_console {
if {[tsv::exists punk_console ps_server_pipename] && [tsv::get punk_console ps_server_pipename] eq $pipename} {
tsv::unset punk_console ps_server_pipename
tsv::unset punk_console ps_server_pid
tsv::unset punk_console ps_server_spawntime
}
}
return [dict create ok 1 stopped $sent pipename $pipename]
}
proc enableRaw_stty {{channel stdin}} {
upvar ::punk::console::previous_stty_state_$channel previous_stty_state_$channel
@ -5917,67 +6510,56 @@ namespace eval punk::console::system {
}
proc enableRaw_powershell {{channel stdin}} {
#enableRaw_powershell is a fallback for when twapi is not present.
#It uses a persistent powershell process to set the console mode to raw, by writing commands to a named pipe that the powershell process is listening on.
#This does not need to be used in windows in the rare case where the console is an alternative terminal that supports stty (e.g mintty without winpty)
#- but it is really intended for use in environments where twapi is not present and stty doesn't work (e.g standard windows console).
#puts stderr "punk::console::enableRaw"
#enableRaw_powershell is a fallback for when twapi is not present (G-106).
#It asks the persistent powershell console-mode server (started lazily on first use,
#shared process-wide via tsv, self-terminating with this process - see
#punk::console::system::ps_consolemode_server_ensure) to clear the console's
#line/echo input flags. The channel argument is unused by the server path - the
#server acts on the process console's input handle.
#stty remains as a last resort: it does not *usually* work on windows
#(the msys/cygwin stty is a subprocess - useful to retrieve info but generally unable
#to affect the calling process/console). An exception is an msys/cygwin terminal such
#as mintty configured without winpty (e.g env MSYS = disable_pcon prior to launch).
upvar ::punk::console::previous_stty_state_$channel previous_stty_state_$channel
upvar ::punk::console::ps_consolemode_contents ps_consolemode_contents
upvar ::punk::console::ps_pipename ps_pipename
if {[info exists ps_consolemode_contents]} {
#ps_pipename e.g \\.\pipe\punkwinshell_ps_consolemode_12345-1223456
set trynum 0
set wrote 0
while {$trynum < 5} {
incr trynum
if {![catch {
set pipe [open $ps_pipename w]
} errMsg]} {
chan conf $pipe -buffering line
puts -nonewline $pipe "enableraw\r\n"
#flush $pipe
#after 10
#close $pipe
set wrote 1
break
} else {
after 100
if {"windows" eq $::tcl_platform(platform)} {
set sendresult [::punk::console::system::ps_consolemode_send enableraw]
if {[dict get $sendresult ok]} {
tsv::set punk_console is_raw 1
#the server applies the console flags asynchronously (fire-and-forget protocol).
#Where the runtime can read the live mode (tcl9 -inputmode on a console channel),
#wait briefly for the flip so callers can rely on raw being active on return.
set confirmed unknown
if {[dict exists [chan configure $channel] -inputmode]} {
set confirmed 0
set endtime [expr {[clock milliseconds] + 750}]
while {[clock milliseconds] < $endtime} {
if {[dict get [chan configure $channel] -inputmode] eq "raw"} {
set confirmed 1
break
}
after 25
}
}
return [list $channel [list from unknown to raw note "set via powershell consolemode server (confirmed $confirmed)"]]
}
if {$wrote} {
tsv::set punk_console is_raw 1
#after 100
close $pipe
} else {
puts stderr "write to $ps_pipename failed trynum: $trynum\n$errMsg"
}
} elseif {[set sttycmd [auto_execok stty]] ne ""} {
#todo - something else entirely
#this approach does not *usually* work on windows
#the msys/cygwin stty command is launched as a subprocess - can be used to retrieve info
# but seems to be useless as far as affecting the calling process/console
#An exception is when running in an msys/cygwin terminal - e.g mintty *when* it is configured to not use winpty
#(e.g by setting environment variable MSYS = disable_pcon, prior to launch.)
#not normal operation - fall through to stty attempt with an actionable note
puts stderr "punk::console::enableRaw: powershell consolemode server unavailable ([dict get $sendresult error]) - trying stty"
}
if {[set sttycmd [auto_execok stty]] ne ""} {
if {[set previous_stty_state_$channel] eq ""} {
set previous_stty_state_$channel [exec {*}$sttycmd -g <@$channel]
}
exec {*}$sttycmd raw -echo <@$channel
tsv::set punk_console is_raw 1
#review - inconsistent return dict
return [dict create stdin [list from [set previous_stty_state_$channel] to "" note "fixme - to state not shown"]]
} else {
error "punk::console::enableRaw Unable to use twapi or stty to set raw mode - aborting"
error "punk::console::enableRaw Unable to use twapi, the powershell consolemode server, or stty to set raw mode - aborting"
}
}
proc disableRaw_powershell {{channel stdin}} {
#disableRaw powershell version
#disableRaw powershell/fallback version (G-106)
upvar ::punk::console::previous_stty_state_$channel previous_stty_state_$channel
set ch_state [chan conf $channel]
@ -5986,11 +6568,20 @@ namespace eval punk::console::system {
tsv::set punk_console is_raw 0
return [list $channel [list from [dict get $ch_state -inputmode] to normal]]
} else {
#tcl <= 8.6x doesn't support -inputmode
#tcl <= 8.6x doesn't support -inputmode - ask the powershell consolemode server
#to restore the console's line/echo input flags
if {"windows" eq $::tcl_platform(platform)} {
set sendresult [::punk::console::system::ps_consolemode_send disableraw]
if {[dict get $sendresult ok]} {
tsv::set punk_console is_raw 0
return [list $channel [list from unknown to normal note "set via powershell consolemode server"]]
}
#not normal operation - fall through to stty attempt with an actionable note
puts stderr "punk::console::disableRaw: powershell consolemode server unavailable ([dict get $sendresult error]) - trying stty"
}
if {[set sttycmd [auto_execok stty]] ne ""} {
#this doesn't work on windows
#It may seem to - only because running *any* external utility can exit raw mode
set sttycmd [auto_execok stty]
if {[set previous_stty_state_$channel] ne ""} {
exec {*}$sttycmd [set previous_stty_state_$channel]
set previous_stty_state_$channel ""
@ -6002,7 +6593,7 @@ namespace eval punk::console::system {
#probably not. We should work out how to read the stty result flags and set a result.. or just limit from,to to showing echo and lineedit states.
return [list stdin [list from "[set previous_stty_state_$channel]" to "" note "fixme - to state not shown"]]
} else {
error "punk::console::disableRaw Unable to use twapi or stty to unset raw mode - aborting"
error "punk::console::disableRaw Unable to use twapi, the powershell consolemode server, or stty to unset raw mode - aborting"
}
}
}
@ -6181,52 +6772,14 @@ namespace eval punk::console {
proc disableRaw {{channel stdin}} [info body ::punk::console::system::disableRaw_twapi]
} else {
variable ps_consolemode_pid
variable ps_consolemode_contents
variable ps_pipename
if {![info exists ps_consolemode_contents]} {
#start persistent powershell consolemode_server.ps1 named pipe server
#::argv0 is absent in secondary threads (thread::create workers, codethreads) -
#the module must remain loadable there
if {[info exists ::argv0] && $::argv0 ne ""} {
set pstooldir [file dirname [file dirname [file normalize $::argv0]]]/scriptlib/utils/pwsh
} else {
set pstooldir [pwd]
}
#set ps_script $pstooldir/consolemode_server.ps1
set ps_script $pstooldir/consolemode_server_async.ps1
if {[file exists $ps_script]} {
set fd [open $ps_script r]
chan configure $fd -translation binary
set ps_consoleid [pid]-[expr {int(999 * rand())+1}]
set ps_consolemode_contents [string map [list "<punkshell_consoleid>" $ps_consoleid] [read $fd]]
close $fd
#set ps_consolemode_pipe [twapi::namedpipe_client {//./pipe/punkshell_ps_consolemode} -access write]
#set ps_cmd [auto_execok pwsh.exe]
set ps_cmd [auto_execok pwsh.exe]
if {$ps_cmd eq ""} {
set ps_cmd [auto_execok powershell.exe]
}
if {$ps_cmd ne ""} {
set ps_consolemode_pid [exec {*}$ps_cmd -nop -nol -c $ps_consolemode_contents &]
set ps_pipename {\\.\pipe\punkshell_ps_consolemode_}
append ps_pipename $ps_consoleid
puts stderr "twapi not present, using persistent powershell process: pipename: $ps_pipename pid: $ps_consolemode_pid"
#todo - taskkill /F /PID $ps_consolemode_pid
#when?
#review
#if {[catch {puts "pidinfo: [::tcl::process::status $ps_consolemode_pid]"} errM]} {
# puts stderr "--- failed to get process status for $ps_consolemode_pid\n$errM"
#}
#set p [open {\\.\pipe\punkshell_ps_consolemode} w]
#chan conf $p -buffering none -blocking 1
#puts $p ""
#close $p
}
}
}
#no twapi - use the powershell console-mode fallback (G-106).
#The persistent powershell named-pipe server is started lazily on first
#enableRaw/disableRaw use (punk::console::system::ps_consolemode_server_ensure),
#not at module load: loading punk::console in a piped/non-console or worker-thread
#context must not spawn processes or emit noise. Server state is process-wide
#(tsv punk_console ps_server_*), the server watches this process's pid and exits
#with it, and script resolution no longer depends on argv0 alone
#(see punk::console::system::ps_consolemode_script_get).
proc enableRaw {{channel stdin}} [info body ::punk::console::system::enableRaw_powershell]
proc disableRaw {{channel stdin}} [info body ::punk::console::system::disableRaw_powershell]
@ -6344,7 +6897,7 @@ namespace eval punk::console {
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::console ::punk::console::argdoc ::punk::console::internal ::punk::console::local ::punk::console::ansi ::punk::console::check
lappend ::punk::args::register::NAMESPACES ::punk::console ::punk::console::argdoc ::punk::console::internal ::punk::console::local ::punk::console::ansi ::punk::console::check ::punk::console::system ::punk::console::system::argdoc
}
@ -6353,7 +6906,7 @@ namespace eval ::punk::args::register {
## Ready
package provide punk::console [namespace eval punk::console {
variable version
set version 0.7.2
set version 0.8.0
}]
return

4
src/bootsupport/modules/punk/repl-0.5.2.tm

@ -3317,7 +3317,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
} else {
# -inputmode unavailable
#tcl 8.6 doesn't have -inputmode - meaning it has to call punk:console::enableRaw each time
#enableRaw on windows without twapi involves launching a pwsh process - which gives a noticeable lag in keyboard input.
#enableRaw on windows without twapi is a named-pipe write to the persistent powershell
#consolemode server (G-106) - cheap once running, but the first use of a session pays the
#server spawn (powershell startup).
#enableRaw on Unix involves a call to stty - which is generally fast - but still to be avoided if not required.
set re_enable_raw_required 1
}

77
src/buildsuites/suite_tcl90/README.md

@ -1,11 +1,13 @@
# suite_tcl90 - zig build of the Tcl 9.0 windows runtime (G-096, G-102)
# suite_tcl90 - zig build of the Tcl 9.0 windows runtime (G-096, G-102, G-103)
The first real, tracked buildsuite: reproducibly builds `tclsh90s.exe` (static) and
`tclsh90szip.exe` (self-contained zipfs) plus the TCLSH_PIPEREPL variants and the
thread/vfs/tk extension dlls, tklib/tcllib installs and critcl-built tcllibc, from
Tcl core-9-0-branch sources, using zig only (no MS toolchain, per the project
toolchain policy). (vqtcl/vlerq was removed 2026-07-20 - an old-tclkit experiment;
future tclkit investigation has TEMP_REFERENCE/metakit and TEMP_REFERENCE/KitCreator
toolchain policy) - and assembles them into the G-103 RUNTIME KIT FAMILY:
verified self-contained batteries-attached runtimes (see "Runtime kit family"
below). (vqtcl/vlerq was removed 2026-07-20 - an old-tclkit experiment; future
tclkit investigation has TEMP_REFERENCE/metakit and TEMP_REFERENCE/KitCreator
as reference material.)
Everything transient lives under `src/buildsuites/_build/suite_tcl90/` (VCS-ignored via
@ -27,7 +29,8 @@ run from THIS directory with the pinned zig. Sources come from `build.zig.zon` -
content-hashed per-checkin tarball pins ('refresh' = bump a pin; `zig fetch
--save=<name> <url>` edits the manifest). Steps for the nested staged build:
`-Dsteps=...` (repeat the flag), default install install-libraries make-zipfs smoke
tklib tcllib tcllibc. The test gate: `zig build bootstrap -Dsteps=test-gate` (or
tklib tcllib tcllibc kit-family kit-family-artifacts. The test gate:
`zig build bootstrap -Dsteps=test-gate` (or
invoke the staged build file directly, as suite.tcl does). zig extracts fetched
packages into `zig-pkg/` beside the manifest and `.zig-cache/` for the outer
invocation - both VCS-ignored.
@ -107,6 +110,62 @@ Timing-sensitive suites (thread) must not run in parallel with heavy ones:
`suite.tcl test` passes `-j1` so combined step invocations serialize (zig
otherwise runs independent steps concurrently).
## Runtime kit family (G-103)
The `kit-family` step (in the default build pipeline) assembles the project's
runtime kit family - runnable, self-contained executables whose info library AND
core batteries live in the initially attached zip, depending on no external
filesystem tree:
- **plain** `tclsh<patchlevel>.exe` (e.g `tclsh9.0.5.exe`) - stock shell (no
piperepl patch) + batteries: loadable Thread, tclvfs with the vfs::* packages,
tcllib + tcllibc (critcl accelerators).
- **punk** `tclsh<patchlevel>-punk.exe` - the same batteries on the
TCLSH_PIPEREPL-patched shell, gate ENABLED BY DEFAULT (opt out with
`TCLSH_PIPEREPL=0`; users wanting stock semantics take the plain kit).
- **punk-bi** `tclsh<patchlevel>-punk-bi.exe` - punk + the batteries-included
tier we build: Tk (dll + script library) and tklib. (tcltls joins when its
zig-built crypto backend lands - see the G-103 goal notes.)
Attached-image layout (also the make.tcl kit contract - a future kit build
extracts this image and merges its .vfs payload over it): `tcl_library/` at the
app root (the C-level zipfs boot looks only for `/app/main.tcl` and
`/app/tcl_library`; no main.tcl is included, so the stock boot falls through to
the ordinary interactive shell), tm modules at `tcl9/<ver>/` beside it (tm.tcl
anchors at the dirname of tcl_library), batteries under `lib/<pkg>/` with their
installed-shape pkgIndexes (`$dir/../../bin` dll references resolve to
`/app/bin`), dlls under `bin/`, and a one-line stock pkgIndex hook at
`lib/pkgIndex.tcl` joining `lib/` to the package search (auto_path is
`[tcl_library, /app]`, and tclPkgUnknown re-scans auto_path growth mid-scan -
the same mechanism tcllib's own top-level index uses). Battery dlls load out of
zipfs via the core's copy-to-temp fallback - the TIP-741 temp-dir accumulation
wart is characterized in the G-103 goal notes.
Every member is verified by `tools/family_check.tcl` before the step passes:
the kit is copied ALONE into a scratch dir and probed from there with a
scrubbed environment (no external Tcl visible) - patchlevel, zipfs tcl_library,
tzdata/encodings, tm modules, functional Thread (cross-thread eval), tclvfs
(vfs::zip mount round-trip), tcllib md5 with the tcllibc accelerator engaged,
piperepl default/opt-out behaviour per variant, and Tk create/destroy + tklib
for bi. Products land in `out/family/`.
The `kit-family-artifacts` step (also default; depends on the checks) emits the
punkbin ARTIFACT tier into `out/family/punkbin/win32-x86_64/`: immutable
`-r<N>`-named copies (revision via `-Dfamilyrev=N`, suite.tcl
`-zigargs {-Dfamilyrev=N}`; default 1) plus per-artifact `<name>.toml` metadata
(variant, working name, sha1/size, tcl patchlevel, piperepl policy, attached
battery versions, source-checkout provenance, toolchain, G-107 test-evidence
result lines) and a punkbin-format `sha1sums.txt`. The emission deliberately
runs UNDER the plain family kit itself (sha1 via its attached tcllib) - every
run re-proves the family runtime executes real tooling self-contained.
Publication to the real punkbin repo remains a deliberate user step, deferred
per the goal notes until the family shape is accepted.
Consumers: working-name runtimes are copied into `bin/runtime/win32-x86_64/`
and referenced by `src/runtime/mapvfs.config` (punk -> punk9wintk905.vfs as
`punk9_beta`, punk-bi -> punk9win_for_tkruntime.vfs as `punk9bi_beta`, per the
*_beta trial convention).
## Pinned zig
The suite pin is **zig 0.16.0** (current official release; adopted 2026-07-20 -
@ -140,10 +199,12 @@ producing stale/duplicate artifacts at link time).
CRT provides the exe entry.
- `tools/` - `wrapfiletofile.zig` (uuid-header overlay generation),
`stagetree.zig` (bootstrap-mode source/recipe materializer),
`zipfs_mkzip.tcl` / `zipfs_mkimg.tcl` (make-zipfs drivers), and the
suite-built-shell step scripts `test_gate.tcl` / `suite_smoke.tcl` /
`pkg_smoke.tcl` (G-102). `test_gate.tcl` is the shared testsuite engine
(G-107): gate/record modes, driver selection, evidence summaries.
`zipfs_mkzip.tcl` / `zipfs_mkimg.tcl` (make-zipfs + kit-family wrap drivers),
and the suite-built-shell step scripts `test_gate.tcl` / `suite_smoke.tcl` /
`pkg_smoke.tcl` (G-102) plus `family_check.tcl` / `family_artifacts.tcl`
(G-103: kit-family self-containment verification; punkbin-layout artifact +
metadata emission). `test_gate.tcl` is the shared testsuite engine (G-107):
gate/record modes, driver selection, evidence summaries.
- `expected_test_failures.txt` (tcl core) / `expected_test_failures_thread.txt` /
`expected_test_failures_tclvfs.txt` - tracked dispositioned gate baselines.
- `patches/` - recovered 2024 experiment patches, including TCLSH_PIPEREPL

190
src/buildsuites/suite_tcl90/build905.zig

@ -16,6 +16,7 @@ const tclvfs_shared = @import("build_tclvfs/build_tclvfs_shared.zig");
const build_tclthread = @import("build_tclthread/build_tclthread.zig").build_tclthread;
const build_tk = @import("build_tk/build_tk.zig").build_tk; //G-098
const TkBuild = @import("build_tk/build_tk.zig").TkBuild; //G-103
const tcl_major_version = "9";
const tcl_nodot_version = "90"; //VER in makefile
@ -1953,12 +1954,25 @@ pub fn build(b: *std.Build) !void {
});
install_libraries.dependOn(&cookiejar_install.step);
const tm_http_install = b.addInstallFileWithDir(b.path(tcl_source_folder ++ "/library/http/http.tcl"), .prefix, b.pathJoin(&.{ module_install_dir_rel, tcl_dot_version, "http-2.10.2.tm" }));
//(G-102: the former decorative 'echo --Installing package X--' banner steps were
//removed - 'echo' is a cmd builtin, and spawning it as a program only worked when
//a coreutils echo.exe happened to be on PATH; in a scrubbed environment the five
//banner spawns failed and took install-libraries down transitively.)
install_libraries.dependOn(&tm_http_install.step);
//tm modules under the module path (lib/tcl9/9.0). Version-stamped names follow
//the checkout's library tree; single source for the prefix install AND the
//G-103 family staging (a kit's tm root is /app/tcl9/<ver> - tm.tcl anchors at
//the dirname of the attached tcl_library).
const tm_installs = [_]struct { src: []const u8, tm: []const u8 }{
.{ .src = "/library/http/http.tcl", .tm = "http-2.10.2.tm" },
.{ .src = "/library/msgcat/msgcat.tcl", .tm = "msgcat-1.7.1.tm" },
.{ .src = "/library/tcltest/tcltest.tcl", .tm = "tcltest-2.5.11.tm" },
.{ .src = "/library/platform/platform.tcl", .tm = "platform-1.1.1.tm" },
.{ .src = "/library/platform/shell.tcl", .tm = "platform/shell-1.1.4.tm" },
};
inline for (tm_installs) |tmi| {
const tm_inst = b.addInstallFileWithDir(b.path(tcl_source_folder ++ tmi.src), .prefix, b.pathJoin(&.{ module_install_dir_rel, tcl_dot_version, tmi.tm }));
install_libraries.dependOn(&tm_inst.step);
}
//opt/*.tcl
const opt_tcl_install = b.addInstallDirectory(.{
@ -1980,18 +1994,6 @@ pub fn build(b: *std.Build) !void {
const pkgindex_install = b.addInstallFile(b.path(tcl_source_folder ++ "/library/manifest.txt"), b.pathJoin(&.{ script_install_dir_rel, "pkgIndex.tcl" }));
install_libraries.dependOn(&pkgindex_install.step);
const tm_msgcat_install = b.addInstallFileWithDir(b.path(tcl_source_folder ++ "/library/msgcat/msgcat.tcl"), .prefix, b.pathJoin(&.{ module_install_dir_rel, tcl_dot_version, "msgcat-1.7.1.tm" }));
install_libraries.dependOn(&tm_msgcat_install.step);
const tm_tcltest_install = b.addInstallFileWithDir(b.path(tcl_source_folder ++ "/library/tcltest/tcltest.tcl"), .prefix, b.pathJoin(&.{ module_install_dir_rel, tcl_dot_version, "tcltest-2.5.11.tm" }));
install_libraries.dependOn(&tm_tcltest_install.step);
const tm_platform_install = b.addInstallFileWithDir(b.path(tcl_source_folder ++ "/library/platform/platform.tcl"), .prefix, b.pathJoin(&.{ module_install_dir_rel, tcl_dot_version, "platform-1.1.1.tm" }));
install_libraries.dependOn(&tm_platform_install.step);
const tm_shell_install = b.addInstallFileWithDir(b.path(tcl_source_folder ++ "/library/platform/shell.tcl"), .prefix, b.pathJoin(&.{ module_install_dir_rel, tcl_dot_version, "platform", "shell-1.1.4.tm" }));
install_libraries.dependOn(&tm_shell_install.step);
const encodings_install = b.addInstallDirectory(.{
.source_dir = b.path(tcl_source_folder ++ "/library/encoding"),
.include_extensions = &.{".enc"},
@ -2175,16 +2177,18 @@ pub fn build(b: *std.Build) !void {
//****************************
const thread_lib_compile = try build_tclthread(tcl_source_folder, "../tclthread", b, target, optimize, finalstublib);
const thread_build = try build_tclthread(tcl_source_folder, "../tclthread", b, target, optimize, finalstublib);
const build_tclthread_step = b.step("build-tclthread", "build tcl thread library");
build_tclthread_step.dependOn(&thread_lib_compile.step);
build_tclthread_step.dependOn(&thread_build.lib.step);
b.getInstallStep().dependOn(build_tclthread_step);
//G-098: Tk loadable extension (windows target only for now)
//G-098: Tk loadable extension (windows target only for now). The build facts
//are hoisted (optional) so the G-103 bi-family staging can consume them.
var tk_build: ?TkBuild = null;
if (target.result.os.tag == .windows) {
const tk_lib_compile = try build_tk(tcl_source_folder, "../tk9", b, target, optimize, finalstublib);
tk_build = try build_tk(tcl_source_folder, "../tk9", b, target, optimize, finalstublib);
const build_tk_step = b.step("build-tk", "build tk shared library (tcl9tk90.dll) + tk script library install");
build_tk_step.dependOn(&tk_lib_compile.step);
build_tk_step.dependOn(&tk_build.?.lib.step);
}
//if !ZIPFS_BUILD
@ -2327,9 +2331,153 @@ pub fn build(b: *std.Build) !void {
tcllibc_smoke.step.dependOn(b.getInstallStep());
tcllibc_step.dependOn(&tcllibc_smoke.step);
const testreports_dir = common.replaceAll(b, b.pathJoin(&.{ b.install_path, "testreports" }), "\\", "/");
// ================== G-103 runtime kit family ==================
//PLAIN (stock shell + batteries), PUNK (piperepl-patched shell, gate
//default-ON with TCLSH_PIPEREPL=0 opt-out, same batteries), PUNK-BI
//(punk + the libraries we build: Tk dll + script lib, tklib). Working
//names carry the dotted tcl patchlevel with punk-marked patched shells
//(tclsh9.0.5.exe / tclsh9.0.5-punk.exe / tclsh9.0.5-punk-bi.exe - G-103
//naming decision); -r<N> artifact copies are kit-family-artifacts' job.
//
//Attached-image layout (the make.tcl kit contract: what a future kit
//build extracts and merges under its .vfs payload):
// tcl_library/ core script library (the C-level zipfs boot looks only
// for /app/main.tcl and /app/tcl_library; no main.tcl -
// stock fallthrough to the interactive shell)
// tcl9/<ver>/ tm modules (tm.tcl anchors at dirname of tcl_library)
// lib/<pkg>/ batteries with their installed-shape pkgIndexes
// ($dir/../../bin dll references resolve to /app/bin)
// bin/<dll> battery dlls (loaded from zipfs via copy-to-temp -
// the characterized TIP-741 cleanup wart, goal Notes)
// lib/pkgIndex.tcl one-line stock hook joining lib/ to the package
// search: auto_path is [tcl_library, /app] and
// tclPkgUnknown re-scans auto_path growth mid-scan (the
// same mechanism tcllib's own top-level index uses)
if (target.result.os.tag == .windows) {
const family_step = b.step("kit-family", "assemble + verify the G-103 runtime kit family (plain/punk/punk-bi): self-contained batteries-attached shells -> <prefix>/family");
const family_libext_pkgindex =
\\#punkshell runtime kit family (G-103): stock package-index hook extending
\\#the package search into the attached image's lib/ tree (tclPkgUnknown
\\#re-scans auto_path growth mid-scan - the same mechanism tcllib's own
\\#top-level pkgIndex uses). The batteries keep their installed-shape
\\#indexes ($dir/../../bin dll references resolve to /app/bin).
\\if {$dir ni $::auto_path} {lappend ::auto_path $dir}
\\
;
const tkb = tk_build.?; //windows target (checked just above)
var family_tree_base: [2]std.Build.LazyPath = undefined;
for (0..2) |ti| { //0 = core payload (plain+punk), 1 = bi payload
const famwf = b.addWriteFiles();
//core script library at the boot position (tzdata/encoding ride
//inside the source library tree), dde/registry dlls inside
const fam_tcl_library = famwf.addCopyDirectory(b.path(tcl_source_folder ++ "/library"), "base/tcl_library", .{});
_ = famwf.addCopyFile(b.path(tcl_source_folder ++ "/library/manifest.txt"), "base/tcl_library/pkgIndex.tcl");
_ = famwf.addCopyFile(install_dde_dll.artifact.getEmittedBin(), "base/tcl_library/dde/" ++ dde_dll_file);
_ = famwf.addCopyFile(reg_dll.getEmittedBin(), "base/tcl_library/registry/" ++ reg_dll_file);
//tm modules (same set the prefix install carries)
inline for (tm_installs) |tmi| {
_ = famwf.addCopyFile(b.path(tcl_source_folder ++ tmi.src), "base/tcl9/" ++ tcl_dot_version ++ "/" ++ tmi.tm);
}
//package-search hook for the lib/ tree
_ = famwf.add("base/lib/pkgIndex.tcl", family_libext_pkgindex);
//batteries (installed shape)
try tclvfs_shared.familyadd_tclvfs_package("../tclvfs", b, famwf, "base/lib/vfs" ++ tclvfs_shared.dotversion, tclvfs_compile);
_ = famwf.addCopyFile(thread_build.pkgidx, b.fmt("base/lib/thread{s}/pkgIndex.tcl", .{thread_build.version}));
_ = famwf.addCopyFile(thread_build.lib.getEmittedBin(), b.fmt("base/bin/{s}", .{thread_build.dll_file}));
_ = famwf.addCopyDirectory(tcllib_out, b.fmt("base/lib/tcllib{s}", .{tcllib_ver}), .{});
_ = famwf.addCopyDirectory(critcl_libout, "base/lib", .{}); //carries tcllibc/
if (ti == 1) {
_ = famwf.addCopyDirectory(b.path("../tk9/library"), b.fmt("base/lib/{s}", .{tkb.libdir}), .{});
_ = famwf.addCopyFile(tkb.pkgidx, b.fmt("base/lib/{s}/pkgIndex.tcl", .{tkb.libdir}));
_ = famwf.addCopyFile(tkb.lib.getEmittedBin(), b.fmt("base/bin/{s}", .{tkb.dll_file}));
_ = famwf.addCopyDirectory(tklib_out, b.fmt("base/lib/tklib{s}", .{tklib_ver}), .{});
}
family_tree_base[ti] = fam_tcl_library.dirname();
}
const FamilyKit = struct {
variant: []const u8,
working_name: []const u8,
prefix_exe: *std.Build.Step.Compile,
tree: usize,
};
const family_kits = [_]FamilyKit{
.{ .variant = "plain", .working_name = b.fmt("tclsh{s}.exe", .{tcl_h_patchlevel}), .prefix_exe = tclsh_exe, .tree = 0 },
.{ .variant = "punk", .working_name = b.fmt("tclsh{s}-punk.exe", .{tcl_h_patchlevel}), .prefix_exe = tclsh_pr_exe, .tree = 0 },
.{ .variant = "punk-bi", .working_name = b.fmt("tclsh{s}-punk-bi.exe", .{tcl_h_patchlevel}), .prefix_exe = tclsh_pr_exe, .tree = 1 },
};
var family_checks: [family_kits.len]*std.Build.Step = undefined;
var family_installed: [family_kits.len][]const u8 = undefined;
for (family_kits, 0..) |fk, ki| {
const wrap = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(wrap);
wrap.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
wrap.addFileArg(b.path("tools/zipfs_mkimg.tcl"));
wrap.addArg("-outfile");
const wrapped = wrap.addOutputFileArg(fk.working_name);
wrap.addArg("-indir");
wrap.addDirectoryArg(family_tree_base[fk.tree]);
wrap.addArg("-strip");
wrap.addDirectoryArg(family_tree_base[fk.tree]);
wrap.addArg("-infile");
wrap.addArtifactArg(fk.prefix_exe);
const inst = b.addInstallFileWithDir(wrapped, .prefix, b.fmt("family/{s}", .{fk.working_name}));
inst.step.dependOn(&wrap.step);
const installed_path = common.replaceAll(b, b.pathJoin(&.{ b.install_path, "family", fk.working_name }), "\\", "/");
family_installed[ki] = installed_path;
//self-containment verification: the tool copies the kit alone into
//a scratch dir and probes from there with a scrubbed environment
//(no external Tcl visible - see tools/family_check.tcl)
const check = b.addRunArtifact(tclsh_exe);
common.scrubTclEnv(check);
check.setEnvironmentVariable("TCL_LIBRARY", tcl_library_src);
check.has_side_effects = true;
check.addFileArg(b.path("tools/family_check.tcl"));
check.addArgs(&.{ "-exe", installed_path, "-variant", fk.variant, "-expectpatch", tcl_h_patchlevel });
check.addArgs(&.{ "-thread", thread_build.version, "-vfs", tclvfs_shared.dotversion, "-tcllib", tcllib_ver });
if (std.mem.eql(u8, fk.variant, "punk-bi")) {
check.addArgs(&.{ "-tk", tkb.patchlevel, "-tklib", tklib_ver });
}
check.step.dependOn(&inst.step);
family_checks[ki] = &check.step;
family_step.dependOn(&check.step);
}
//-- kit-family-artifacts: punkbin-layout emission (-r<N> immutable
//artifact names + per-artifact toml metadata + sha1sums), run UNDER
//THE PLAIN FAMILY KIT itself - every emission doubles as a proof the
//family runtime executes real tooling from its attached batteries.
//Depends on the checks: only verified kits get artifact records.
//Publication to the real punkbin repo stays a deliberate user step.
const familyrev = b.option(u32, "familyrev", "assembly revision N for the -r<N> family artifact names (kit-family-artifacts; default 1)") orelse 1;
const family_artifacts_step = b.step("kit-family-artifacts", "emit punkbin-layout artifact copies (-r<N>) + toml metadata + sha1sums for the verified family kits -> <prefix>/family/punkbin/<target>");
const artifacts_outdir = common.replaceAll(b, b.pathJoin(&.{ b.install_path, "family", "punkbin", "win32-x86_64" }), "\\", "/");
const uuid_tcl = common.manifestUuid(b, tcl_source_folder);
const uuid_thread = common.manifestUuid(b, "../tclthread");
const uuid_tclvfs = common.manifestUuid(b, "../tclvfs");
const uuid_tk = common.manifestUuid(b, "../tk9");
const uuid_tcllib = common.manifestUuid(b, "../tcllib");
const uuid_tklib = common.manifestUuid(b, "../tklib");
const emit = b.addSystemCommand(&.{family_installed[0]});
common.scrubTclEnv(emit);
emit.has_side_effects = true;
emit.addFileArg(b.path("tools/family_artifacts.tcl"));
emit.addArgs(&.{ "-outdir", artifacts_outdir, "-rev", b.fmt("{d}", .{familyrev}), "-target", "win32-x86_64" });
emit.addArgs(&.{ "-suite", "suite_tcl90", "-tclpatch", tcl_h_patchlevel, "-zig", builtin.zig_version_string, "-optimize", @tagName(optimize) });
emit.addArgs(&.{ "-components", b.fmt("Thread {s} vfs {s} tcllib {s} tcllibc {s}", .{ thread_build.version, tclvfs_shared.dotversion, tcllib_ver, tcllib_ver }) });
emit.addArgs(&.{ "-bicomponents", b.fmt("Tk {s} tklib {s}", .{ tkb.patchlevel, tklib_ver }) });
emit.addArgs(&.{ "-provenance", b.fmt("tcl {s} thread {s} tclvfs {s} tk {s} tcllib {s} tklib {s}", .{ uuid_tcl, uuid_thread, uuid_tclvfs, uuid_tk, uuid_tcllib, uuid_tklib }) });
emit.addArgs(&.{ "-testreports", testreports_dir });
emit.addArgs(&.{ "-kits", b.fmt("plain {{{s}}} punk {{{s}}} punk-bi {{{s}}}", .{ family_installed[0], family_installed[1], family_installed[2] }) });
for (family_checks) |cs| emit.step.dependOn(cs);
family_artifacts_step.dependOn(&emit.step);
}
// ==================
//-- test-gate: the core testsuite under the built shell, gated on parsed
//totals vs the tracked dispositioned baseline (all.tcl's exit code lies)
const testreports_dir = common.replaceAll(b, b.pathJoin(&.{ b.install_path, "testreports" }), "\\", "/");
const gate_step = b.step("test-gate", "tcl core testsuite under the built shell, gated on parsed totals vs expected_test_failures.txt");
const testargs_opt = b.option([]const u8, "testargs", "extra tcltest args for test-gate (e.g -file http.test)") orelse "";
const gate_run = b.addRunArtifact(tclsh_exe);
@ -2547,7 +2695,7 @@ fn bootstrapMode(b: *std.Build) !void {
//suite.tcl performs after its fossil staging. Per-zig-version cache dir (object
//caches must never be shared across zig versions - user-confirmed hazard), same
//name suite.tcl derives so both flows share the staged cache.
const steps_opt = b.option([]const []const u8, "steps", "steps for the staged build (repeat the flag; default: install install-libraries make-zipfs smoke tklib tcllib tcllibc - the full pipeline short of test-gate)") orelse @as([]const []const u8, &.{ "install", "install-libraries", "make-zipfs", "smoke", "tklib", "tcllib", "tcllibc" });
const steps_opt = b.option([]const []const u8, "steps", "steps for the staged build (repeat the flag; default: install install-libraries make-zipfs smoke tklib tcllib tcllibc kit-family kit-family-artifacts - the full pipeline short of test-gate)") orelse @as([]const []const u8, &.{ "install", "install-libraries", "make-zipfs", "smoke", "tklib", "tcllib", "tcllibc", "kit-family", "kit-family-artifacts" });
var cachever: []const u8 = builtin.zig_version_string;
cachever = common.replaceAll(b, cachever, "+", "_");
cachever = common.replaceAll(b, cachever, "/", "_");

10
src/buildsuites/suite_tcl90/build_common.zig

@ -53,6 +53,16 @@ pub fn scrubTclEnv(run: *std.Build.Step.Run) void {
}
}
//checkout uuid from a tree's manifest.uuid when the repo materializes it (a
//per-repo fossil 'manifest' setting: tcl/tk/thread enable it, tclvfs/tcllib/
//tklib do not - true for live checkouts AND fossil tarball exports alike), else
//"unrecorded" so provenance emission stays total (G-103 artifact metadata).
pub fn manifestUuid(b: *std.Build, tree_from_root: []const u8) []const u8 {
const abs = b.pathFromRoot(b.fmt("{s}/manifest.uuid", .{tree_from_root}));
const data = std.Io.Dir.cwd().readFileAlloc(b.graph.io, abs, b.allocator, .limited(4096)) catch return "unrecorded";
return std.mem.trim(u8, data, " \t\r\n");
}
//package version from a TEA tree's configure.ac 'AC_INIT([name],[version])'
//line (thread, tclvfs, ...). G-107: version-derived artifact names must follow
//the checkout - hardcoded versions stamp mismatched sources (the thread recipe

14
src/buildsuites/suite_tcl90/build_tclthread/build_tclthread.zig

@ -7,7 +7,17 @@ const common = @import("../build_common.zig");
// declaratively construct a build graph that will be executed by an external
// runner.
//pub fn build(b: *std.Build) !void {}
pub fn build_tclthread(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, stublib: *std.Build.Step.Compile) !*std.Build.Step.Compile {
//G-103: the recipe's kit-family staging consumes the same generated pkgIndex and
//version-derived names as the prefix install - returned alongside the compile.
pub const TclThreadBuild = struct {
lib: *std.Build.Step.Compile,
pkgidx: std.Build.LazyPath,
version: []const u8, //e.g "3.0.7" (from the checkout's AC_INIT)
dll_file: []const u8, //e.g "tcl9thread307.dll"
};
pub fn build_tclthread(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, stublib: *std.Build.Step.Compile) !TclThreadBuild {
//Version derived from the checkout's AC_INIT (G-107): the recipe formerly
//hardcoded 3.0.1, compiling newer trunk sources with a stale PACKAGE_VERSION -
//the dll then self-reported the wrong version and the thread testsuite's
@ -186,5 +196,5 @@ pub fn build_tclthread(comptime tcldir: []const u8, comptime subdir: []const u8,
const pkgidx_install = b.addInstallFileWithDir(pkgidx, .prefix, b.fmt("lib/thread{s}/pkgIndex.tcl", .{thread_version}));
b.getInstallStep().dependOn(&pkgidx_install.step);
return lib;
return .{ .lib = lib, .pkgidx = pkgidx, .version = thread_version, .dll_file = pkg_lib_file };
}

39
src/buildsuites/suite_tcl90/build_tclvfs/build_tclvfs_shared.zig

@ -4,12 +4,24 @@ const common = @import("../build_common.zig");
const tclvfs_dotversion = "1.4.2";
const tclvfs_nodotversion = "142";
pub const dotversion = tclvfs_dotversion;
pub const dll_file = "tcl9vfs" ++ tclvfs_nodotversion ++ ".dll";
//G-102: install the tclvfs script package (lib/vfs<ver>) into the prefix - the two
//configure-products (vfs.tcl, pkgIndex.tcl) generated from their .in templates at
//configure time, the library scripts + template dir copied, and the dll alongside
//(vfs.tcl loads it from its own dir via ::vfs::self). Formerly a suite.tcl
//post-build block. Windows dll naming - the cross-target story is G-105's.
//configure time. Shared by the prefix install and the G-103 family staging so the
//package is generated from one derivation.
fn configuredContent(b: *std.Build, comptime subdir: []const u8, comptime template: []const u8) []const u8 {
var t: []const u8 = common.readSourceFile(b, subdir ++ "/" ++ template);
t = common.replaceAll(b, t, "@PACKAGE_VERSION@", tclvfs_dotversion);
t = common.replaceAll(b, t, "@PKG_LIB_FILE9@", dll_file);
t = common.replaceAll(b, t, "@PKG_LIB_FILE8@", "tclvfs" ++ tclvfs_nodotversion ++ ".dll");
return t;
}
//G-102: install the tclvfs script package (lib/vfs<ver>) into the prefix - the two
//generated configure-products, the library scripts + template dir copied, and the
//dll alongside (vfs.tcl loads it from its own dir via ::vfs::self). Formerly a
//suite.tcl post-build block. Windows dll naming - the cross-target story is G-105's.
pub fn install_tclvfs_package(comptime subdir: []const u8, b: *std.Build, tclvfs_compile: *std.Build.Step.Compile) !void {
const pkgsub = "lib/vfs" ++ tclvfs_dotversion;
const install_scripts = b.addInstallDirectory(.{
@ -31,18 +43,25 @@ pub fn install_tclvfs_package(comptime subdir: []const u8, b: *std.Build, tclvfs
.{ "library/vfs.tcl.in", "vfs.tcl" },
.{ "pkgIndex.tcl.in", "pkgIndex.tcl" },
}) |pair| {
var t: []const u8 = common.readSourceFile(b, subdir ++ "/" ++ pair[0]);
t = common.replaceAll(b, t, "@PACKAGE_VERSION@", tclvfs_dotversion);
t = common.replaceAll(b, t, "@PKG_LIB_FILE9@", "tcl9vfs" ++ tclvfs_nodotversion ++ ".dll");
t = common.replaceAll(b, t, "@PKG_LIB_FILE8@", "tclvfs" ++ tclvfs_nodotversion ++ ".dll");
const gen = wf.add(pair[1], t);
const gen = wf.add(pair[1], configuredContent(b, subdir, pair[0]));
const inst = b.addInstallFileWithDir(gen, .prefix, pkgsub ++ "/" ++ pair[1]);
b.getInstallStep().dependOn(&inst.step);
}
const dll_inst = b.addInstallFileWithDir(tclvfs_compile.getEmittedBin(), .prefix, pkgsub ++ "/tcl9vfs" ++ tclvfs_nodotversion ++ ".dll");
const dll_inst = b.addInstallFileWithDir(tclvfs_compile.getEmittedBin(), .prefix, pkgsub ++ "/" ++ dll_file);
b.getInstallStep().dependOn(&dll_inst.step);
}
//G-103: write the SAME installed-shape vfs package into a family staging tree
//(WriteFiles) at destsub (e.g "base/lib/vfs1.4.2") - scripts, template dir,
//generated vfs.tcl/pkgIndex.tcl, dll inside the package dir.
pub fn familyadd_tclvfs_package(comptime subdir: []const u8, b: *std.Build, wf: *std.Build.Step.WriteFile, comptime destsub: []const u8, tclvfs_compile: *std.Build.Step.Compile) !void {
_ = wf.addCopyDirectory(b.path(subdir ++ "/library"), destsub, .{ .include_extensions = &.{".tcl"} });
_ = wf.addCopyDirectory(b.path(subdir ++ "/library/template"), destsub ++ "/template", .{});
_ = wf.add(destsub ++ "/vfs.tcl", configuredContent(b, subdir, "library/vfs.tcl.in"));
_ = wf.add(destsub ++ "/pkgIndex.tcl", configuredContent(b, subdir, "pkgIndex.tcl.in"));
_ = wf.addCopyFile(tclvfs_compile.getEmittedBin(), destsub ++ "/" ++ dll_file);
}
pub fn vfsadd_tclvfs_files(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, vfswrite: *std.Build.Step.WriteFile) !*std.Build.Step.WriteFile {
//_ = tcldir;
//const version = "1.4.2";

14
src/buildsuites/suite_tcl90/build_tk/build_tk.zig

@ -48,7 +48,17 @@ const ttk_names = [_][]const u8{
"ttkTreeview", "ttkWidget", "ttkStubInit",
};
pub fn build_tk(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, stublib: *std.Build.Step.Compile) !*std.Build.Step.Compile {
//G-103: the recipe's kit-family staging consumes the same generated pkgIndex,
//script-library source and version facts as the prefix install.
pub const TkBuild = struct {
lib: *std.Build.Step.Compile,
pkgidx: std.Build.LazyPath,
patchlevel: []const u8, //e.g "9.0.2" (tk.h TK_PATCH_LEVEL)
libdir: []const u8, //install dir name, e.g "tk9.0"
dll_file: []const u8, //e.g "tcl9tk90.dll"
};
pub fn build_tk(comptime tcldir: []const u8, comptime subdir: []const u8, b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, stublib: *std.Build.Step.Compile) !TkBuild {
const lib = b.addLibrary(.{
.linkage = .dynamic,
.name = "tcl9tk90",
@ -200,5 +210,5 @@ pub fn build_tk(comptime tcldir: []const u8, comptime subdir: []const u8, b: *st
const pkgidx_install = b.addInstallFileWithDir(pkgidx, .prefix, "lib/tk9.0/pkgIndex.tcl");
b.getInstallStep().dependOn(&pkgidx_install.step);
return lib;
return .{ .lib = lib, .pkgidx = pkgidx, .patchlevel = tk_h_patchlevel, .libdir = "tk9.0", .dll_file = "tcl9tk90.dll" };
}

22
src/buildsuites/suite_tcl90/suite.tcl

@ -22,8 +22,13 @@
# -seedfossils <dir> read-only seed collection for missing clones (default ~/.fossils; "" disables)
# -all <0|1> with 'clean': reserved (the clone store lives outside the stage)
# -steps <list> zig build steps. 'build' default: {install install-libraries
# make-zipfs smoke tklib tcllib tcllibc} - the full pipeline
# short of the test steps. 'test' default: {test-gate} (the
# make-zipfs smoke tklib tcllib tcllibc kit-family
# kit-family-artifacts} - the full pipeline short of the test
# steps, ending in the G-103 runtime kit family (verified
# self-contained plain/punk/punk-bi shells -> out/family plus
# punkbin-layout -r<N> artifacts + toml metadata; artifact
# revision via -zigargs {-Dfamilyrev=N}).
# 'test' default: {test-gate} (the
# core suite gate); other test steps (G-107 library runs):
# test-libraries (thread+tclvfs gated, tcllib+tklib recorded),
# test-thread test-tclvfs test-tcllib test-tklib, and the
@ -76,7 +81,7 @@ array set opt {
-optimize ReleaseFast
-tclbranch {}
-refresh 0
-steps {install install-libraries make-zipfs smoke tklib tcllib tcllibc}
-steps {install install-libraries make-zipfs smoke tklib tcllib tcllibc kit-family kit-family-artifacts}
-testargs {}
-zigargs {}
-repofolder {}
@ -225,6 +230,17 @@ proc fossil_source {name url branch dir} {
run fossil update $branch
cd $savedpwd
}
#provenance: materialize manifest.uuid regardless of the upstream repo's
#versioned 'manifest' setting (tcl/tk/thread enable it; tclvfs/tcllib/tklib
#do not - the G-103-era artifact metadata recorded 'unrecorded' for those).
#The LOCAL checkout-scoped setting 'u' generates manifest.uuid immediately
#and keeps it current across updates; idempotent on every build. (The zon
#bootstrap flow's tarballs share the limitation for those repos - checkin
#uuids there would come from the pin URLs; recorded, not yet wired.)
set savedpwd [pwd]
cd $dir
run fossil settings manifest u
cd $savedpwd
}
proc git_source {name url ref dir} {

156
src/buildsuites/suite_tcl90/tools/family_artifacts.tcl

@ -0,0 +1,156 @@
#family_artifacts.tcl (G-103): emit punkbin-layout artifact copies + per-artifact
#toml metadata + sha1sums for the verified runtime kit family members.
#
#Deliberately run UNDER THE PLAIN FAMILY KIT itself (not the builder shell): the
#sha1 digests come from the kit's attached tcllib/tcllibc, so every emission run
#doubles as a proof that the family runtime executes real tooling self-contained.
#
#Artifact tier naming (G-103 naming decision): <workingname minus .exe>-r<N>.exe -
#immutable punkbin names; the -r<N> assembly revision comes from the invocation
#(-rev, suite option -Dfamilyrev). The emitted tree mirrors a punkbin platform
#folder: <outdir>/<artifact>.exe + <artifact>.toml + sha1sums.txt (punkbin
#format: '<sha1> *<filename>'). Publication to the real punkbin repo is a
#deliberate user step (copy + build_sha1sums.tcl there); it is DEFERRED per the
#goal notes until the family shape is accepted.
#
#args: -outdir <dir> -rev <N> -target <punkbin platform, e.g win32-x86_64>
# -suite <name> -tclpatch <patchlevel> -zig <version> -optimize <mode>
# -components {<name> <ver> ...} (attached battery versions, all variants)
# -bicomponents {<name> <ver> ...} (additional bi-only batteries)
# -provenance {<name> <uuid> ...} (source checkout uuids)
# -testreports <dir> (G-107 evidence summaries; optional)
# -kits {<variant> <workingexepath> ...}
proc fail {msg} {puts stderr "family_artifacts FAIL: $msg"; flush stderr; exit 1}
proc note {msg} {puts stdout "family_artifacts: $msg"; flush stdout}
array set opt {
-outdir {} -rev 1 -target {} -suite {} -tclpatch {} -zig {} -optimize {}
-components {} -bicomponents {} -provenance {} -testreports {} -kits {}
}
foreach {k v} $argv {
if {![info exists opt($k)]} {fail "unknown option '$k'"}
set opt($k) $v
}
foreach req {-outdir -target -suite -tclpatch -zig -optimize -components -kits} {
if {$opt($req) eq ""} {fail "missing required option $req"}
}
if {![string is integer -strict $opt(-rev)] || $opt(-rev) < 1} {fail "-rev must be a positive integer"}
if {[catch {package require sha1} sha1ver]} {
fail "package require sha1 failed under [info nameofexecutable] - the family kit must carry tcllib: $sha1ver"
}
proc toml_str {s} {
#basic toml string: escape backslash and double-quote (values here are names,
#versions, uuids, iso dates - no control chars expected)
return "\"[string map {\\ \\\\ \" \\\"} $s]\""
}
proc exe_split {name} {
#{root ext} splitting only a .exe suffix - dotted tcl patchlevels make
#[file rootname] wrong for extensionless (unix) artifact names
#(tclsh9.0.5-punk-r1 would truncate at the last version dot)
if {[string match -nocase "*.exe" $name]} {
return [list [string range $name 0 end-4] .exe]
}
return [list $name ""]
}
file mkdir $opt(-outdir)
set built [clock format [clock seconds] -format %Y-%m-%dT%H:%M:%SZ -timezone :UTC]
set sha1lines {}
set emitted {}
foreach {variant kitpath} $opt(-kits) {
set kitpath [file normalize $kitpath]
if {![file exists $kitpath]} {fail "kit exe not found: $kitpath"}
set working [file tail $kitpath]
lassign [exe_split $working] wroot wext
set artifact "$wroot-r$opt(-rev)$wext"
set dest [file join $opt(-outdir) $artifact]
file delete -force $dest
file copy $kitpath $dest
set sha1 [sha1::sha1 -hex -file $dest]
set size [file size $dest]
lappend sha1lines "$sha1 *$artifact"
set components $opt(-components)
if {$variant eq "punk-bi"} {lappend components {*}$opt(-bicomponents)}
set batteries {}
foreach {n v} $components {lappend batteries [toml_str "$n $v"]}
set m {}
lappend m "#punkshell runtime artifact metadata (G-103) - generated by family_artifacts.tcl"
lappend m "\[artifact\]"
lappend m "name = [toml_str $artifact]"
lappend m "class = \"runtime\""
lappend m "variant = [toml_str $variant]"
lappend m "working_name = [toml_str $working]"
lappend m "revision = $opt(-rev)"
lappend m "target = [toml_str $opt(-target)]"
lappend m "sha1 = [toml_str $sha1]"
lappend m "size = $size"
lappend m "built = [toml_str $built]"
lappend m ""
lappend m "\[runtime\]"
lappend m "tcl_patchlevel = [toml_str $opt(-tclpatch)]"
set pr [expr {$variant ne "plain"}]
lappend m "piperepl = [expr {$pr ? "true" : "false"}]"
if {$pr} {
lappend m "piperepl_default = \"on\""
lappend m "piperepl_opt_out = \"TCLSH_PIPEREPL=0\""
}
lappend m "attached_batteries = \[[join $batteries {, }]\]"
lappend m ""
lappend m "\[provenance\]"
lappend m "suite = [toml_str $opt(-suite)]"
lappend m "toolchain = [toml_str "zig $opt(-zig)"]"
lappend m "optimize = [toml_str $opt(-optimize)]"
foreach {n uuid} $opt(-provenance) {
lappend m "${n}_checkout = [toml_str $uuid]"
}
#G-107 evidence summaries available at emission time (result lines only; the
#full line-record summaries stay the canonical evidence artifacts)
if {$opt(-testreports) ne "" && [file isdirectory $opt(-testreports)]} {
set tlines {}
foreach sf [lsort [glob -nocomplain -directory $opt(-testreports) *.summary]] {
set rec [dict create]
set f [open $sf r]
foreach line [split [read $f] \n] {
set line [string trim $line]
if {$line eq "" || [string index $line 0] eq "#"} continue
if {[catch {llength $line} n] || $n < 2} continue
dict set rec [lindex $line 0] [lrange $line 1 end]
}
close $f
if {![dict exists $rec library] || ![dict exists $rec result]} continue
set lib [dict get $rec library]
set parts [list "result=[dict get $rec result]"]
foreach fkey {mode total passed skipped failed} {
if {[dict exists $rec $fkey]} {lappend parts "$fkey=[dict get $rec $fkey]"}
}
lappend tlines "$lib = [toml_str [join $parts { }]]"
}
if {[llength $tlines]} {
lappend m ""
lappend m "\[tests\]"
lappend m {*}$tlines
}
}
set mf [file join $opt(-outdir) "[lindex [exe_split $artifact] 0].toml"]
set f [open $mf w]
fconfigure $f -translation lf
puts $f [join $m \n]
close $f
lappend emitted "$variant -> $artifact"
note "emitted $artifact (sha1 $sha1, [expr {$size/1024}] KB) + [file tail $mf]"
}
set f [open [file join $opt(-outdir) sha1sums.txt] w]
fconfigure $f -translation lf
puts $f [join $sha1lines \n]
close $f
note "sha1sums.txt written ([llength $sha1lines] artifacts)"
puts "family_artifacts OK: [join $emitted {; }] -> $opt(-outdir)"
exit 0

201
src/buildsuites/suite_tcl90/tools/family_check.tcl

@ -0,0 +1,201 @@
#family_check.tcl (G-103): self-containment verification for one runtime kit
#family member (plain / punk / punk-bi). Run under the suite-built BUILDER shell;
#the KIT exe under test is exec'd as probe children.
#
#Why the copy-to-scratch: beside the installed out/family location sits ../lib
#(the suite prefix), which the stock tm-path setup (<exedir>/../lib) and
#tcl_findLibrary's exe-relative entries would silently satisfy. The kit is copied
#alone into a fresh scratch dir and probed FROM THERE with cwd=scratch, so only
#the attached image can serve the probes ("no external Tcl visible").
#
#The probe environment is scrubbed of every tcl redirection var (TCL_LIBRARY,
#TK_LIBRARY, TCLLIBPATH, VFS_LIBRARY, TCL*_TM_PATH); TCLSH_PIPEREPL is controlled
#per probe - it is the punk-vs-plain discriminator (G-096 behaviour matrix: with
#the gate open the patched shell publishes ::tclsh(istty) before the startup
#script runs; with TCLSH_PIPEREPL=0 or unpatched sources no ::tclsh machinery
#exists). Script-arg probes never set ::tclsh(dorepl), so no probe can land in
#the console-reopen repl (no hang risk).
#
#args: -exe <kitexe> -variant plain|punk|punk-bi -expectpatch <patchlevel>
# -thread <ver> -vfs <ver> -tcllib <ver> ?-tk <patchlevel>? ?-tklib <ver>?
proc fail {msg} {puts stderr "family_check FAIL: $msg"; flush stderr; exit 1}
proc note {msg} {puts stdout "family_check: $msg"; flush stdout}
array set opt {-exe {} -variant {} -expectpatch {} -thread {} -vfs {} -tcllib {} -tk {} -tklib {}}
foreach {k v} $argv {
if {![info exists opt($k)]} {fail "unknown option '$k'"}
set opt($k) $v
}
foreach req {-exe -variant -expectpatch -thread -vfs -tcllib} {
if {$opt($req) eq ""} {fail "missing required option $req"}
}
if {$opt(-variant) ni {plain punk punk-bi}} {fail "-variant must be plain|punk|punk-bi"}
if {$opt(-variant) eq "punk-bi" && ($opt(-tk) eq "" || $opt(-tklib) eq "")} {
fail "punk-bi variant requires -tk and -tklib expected versions"
}
set exe [file normalize $opt(-exe)]
if {![file exists $exe]} {fail "kit exe not found: $exe"}
#-- scrub the probe environment (children inherit ::env at exec time) ---------
foreach ev [array names ::env] {
if {$ev in {TCL_LIBRARY TK_LIBRARY TCLLIBPATH VFS_LIBRARY TCLSH_PIPEREPL}
|| [string match TCL*_TM_PATH $ev]} {
unset ::env($ev)
}
}
#-- scratch dir with the kit copied in alone ---------------------------------
set scratch [file join [file tempdir] "punkfamilycheck_[pid]_[clock clicks -microseconds]"]
file mkdir $scratch
set kitname [file tail $exe]
set kit [file join $scratch $kitname]
file copy $exe $kit
proc probe {name script args} {
#write the probe script into the scratch dir and run the kit on it with
#cwd=scratch; env pairs in $args are set for the child and removed after.
#returns the child output; fails hard on nonzero exit.
global scratch kit
set sf [file join $scratch probe_$name.tcl]
set f [open $sf w]
puts $f $script
close $f
foreach {ev val} $args {set ::env($ev) $val}
set rc [catch {exec $kit $sf 2>@1} out]
foreach {ev val} $args {unset ::env($ev)}
if {$rc} {fail "probe '$name' failed (kit $kit):\n$out"}
return $out
}
proc checks {output} {
#parse 'CHECK <key> <value>' lines into a dict
set d [dict create]
foreach line [split $output \n] {
set line [string trim $line]
if {[string match "CHECK *" $line]} {
dict set d [lindex $line 1] [lindex $line 2]
}
}
return $d
}
proc asserteq {d key expect what} {
if {![dict exists $d $key]} {fail "$what: no CHECK line for '$key' (got: $d)"}
set got [dict get $d $key]
if {$got ne $expect} {fail "$what: $key = '$got', expected '$expect'"}
}
#-- probe: core batteries (all variants) -------------------------------------
#Also proves cwd independence: the probe chdirs into a subdir it creates, so
#relative-path leakage from the scratch dir itself would surface.
set core_script {
proc out {k v} {puts [list CHECK $k $v]}
out patchlevel [info patchlevel]
out tcl_library $::tcl_library
out library_zipfs [string match //zipfs:/app/* $::tcl_library]
#attached-library facts (suite_smoke parity)
out tz [expr {[catch {clock format 0 -gmt 0 -format %Z}] ? "FAIL" : "ok"}]
out encoding [expr {[catch {encoding convertto cp1250 test}] ? "FAIL" : "ok"}]
#tm path serves modules from the attached image (tcl9/<ver> beside tcl_library)
out platform_tm [expr {[catch {package require platform} v] ? "FAIL:$v" : "ok"}]
#Thread: version + functional cross-thread eval
if {[catch {package require Thread} tver]} {out thread "FAIL:$tver"} else {
out thread $tver
set tid [thread::create]
out thread_eval [thread::send $tid {expr {6*7}}]
thread::release $tid
}
#tclvfs: version + representative vfs::* + functional zip mount round-trip
#(the kit's own zipfs mkzip makes the test archive - no external tools)
if {[catch {package require vfs} vver]} {out vfs "FAIL:$vver"} else {
out vfs $vver
out vfs_zip [expr {[catch {package require vfs::zip} zv] ? "FAIL:$zv" : $zv}]
out vfs_urltype [expr {[catch {package require vfs::urltype} uv] ? "FAIL:$uv" : $uv}]
set d [file join [pwd] vfstest]
file mkdir $d/payload
set f [open $d/payload/hello.txt w]; puts -nonewline $f "family-vfs-roundtrip"; close $f
cd $d
tcl::zipfs::mkzip probe.zip payload payload
set mnt [vfs::zip::Mount [file join $d probe.zip] zipmnt]
set f [open zipmnt/hello.txt r]; set data [read $f]; close $f
vfs::zip::Unmount $mnt zipmnt
out vfs_roundtrip [expr {$data eq "family-vfs-roundtrip" ? "ok" : "FAIL:$data"}]
}
#tcllib module + tcllibc acceleration engaged (pkg_smoke -accel parity)
if {[catch {package require md5} mver]} {out md5 "FAIL:$mver"} else {
out md5 $mver
out tcllibc [expr {[catch {package require tcllibc} cv] ? "FAIL:$cv" : $cv}]
upvar #0 ::md5::accel accelarr
out md5_accel [expr {[info exists accelarr(critcl)] && $accelarr(critcl) ? 1 : 0}]
}
exit 0
}
set d [checks [probe core $core_script]]
asserteq $d patchlevel $opt(-expectpatch) "core"
asserteq $d library_zipfs 1 "core (tcl_library=[dict get $d tcl_library])"
asserteq $d tz ok "core"
asserteq $d encoding ok "core"
asserteq $d platform_tm ok "core"
asserteq $d thread $opt(-thread) "core"
asserteq $d thread_eval 42 "core"
asserteq $d vfs $opt(-vfs) "core"
asserteq $d vfs_roundtrip ok "core"
asserteq $d md5_accel 1 "core (tcllibc=[expr {[dict exists $d tcllibc]?[dict get $d tcllibc]:"?"}])"
foreach k {vfs_zip vfs_urltype tcllibc} {
if {[string match FAIL* [dict get $d $k]]} {fail "core: $k [dict get $d $k]"}
}
note "core OK ($opt(-variant)): tcl [dict get $d patchlevel] library [dict get $d tcl_library] thread [dict get $d thread] vfs [dict get $d vfs] (zip [dict get $d vfs_zip], urltype [dict get $d vfs_urltype]) md5 [dict get $d md5] accel=1"
#-- probe: Tk create/destroy (bi only) ---------------------------------------
if {$opt(-variant) eq "punk-bi"} {
set tk_script {
proc out {k v} {puts [list CHECK $k $v]}
if {[catch {package require Tk} tkver]} {out tk "FAIL:$tkver"; exit 0}
out tk $tkver
wm withdraw .
button .b -text family
out tk_widget [winfo exists .b]
destroy .b
out tk_widget_destroyed [expr {![winfo exists .b]}]
destroy .
out tk_done ok
exit 0
}
set d [checks [probe tk $tk_script]]
asserteq $d tk $opt(-tk) "tk"
asserteq $d tk_widget 1 "tk"
asserteq $d tk_widget_destroyed 1 "tk"
asserteq $d tk_done ok "tk"
#tklib rides only in the bi payload
set tklib_script {
proc out {k v} {puts [list CHECK $k $v]}
package require Tk
wm withdraw .
out tooltip [expr {[catch {package require tooltip} v] ? "FAIL:$v" : $v}]
destroy .
exit 0
}
set d [checks [probe tklib $tklib_script]]
if {[string match FAIL* [dict get $d tooltip]]} {fail "tklib: tooltip [dict get $d tooltip]"}
note "tk OK: Tk $opt(-tk) create/destroy + tklib tooltip [dict get $d tooltip]"
}
#-- probe: piperepl gate (variant discriminator) ------------------------------
set gate_script {puts [list CHECK tclshmachinery [info exists ::tclsh(istty)]]; exit 0}
set d_default [checks [probe gate_default $gate_script]]
set d_optout [checks [probe gate_optout $gate_script TCLSH_PIPEREPL 0]]
if {$opt(-variant) eq "plain"} {
#unpatched: no machinery regardless of the env var (even explicitly enabled)
asserteq $d_default tclshmachinery 0 "piperepl (plain, env unset)"
set d_on [checks [probe gate_on $gate_script TCLSH_PIPEREPL 1]]
asserteq $d_on tclshmachinery 0 "piperepl (plain, TCLSH_PIPEREPL=1)"
note "piperepl OK (plain): stock behaviour - no ::tclsh machinery with env unset or =1"
} else {
#punk kits: ACTIVE BY DEFAULT (machinery published), documented opt-out =0
asserteq $d_default tclshmachinery 1 "piperepl ($opt(-variant), env unset - default must be ON)"
asserteq $d_optout tclshmachinery 0 "piperepl ($opt(-variant), TCLSH_PIPEREPL=0 opt-out)"
note "piperepl OK ($opt(-variant)): active by default, disabled via TCLSH_PIPEREPL=0"
}
file delete -force $scratch
puts "family_check OK: $opt(-variant) $kitname self-contained (scratch-dir probes, scrubbed env)"
exit 0

19
src/make.tcl

@ -278,6 +278,23 @@ namespace eval ::punkboot::lib {
return "${plat}-${cpu}"
}
proc platform_punk {} {
#canonical punkshell platform-dir name: platform_generic normalized.
#INLINE COPY of punk::platform::normalize (src/modules/punk/platform-*.tm;
#'help platforms' documents the canon) - the boot stage cannot package
#require, so keep this mapping in sync with that module:
#amd64->x86_64, aarch64->arm64, macos->macosx, macosx arm->arm64.
set parts [split [platform_generic] -]
set cpu [lindex $parts end]
set os [join [lrange $parts 0 end-1] -]
if {$os eq "macos"} {set os macosx}
switch -- $cpu {
amd64 {set cpu x86_64}
aarch64 {set cpu arm64}
arm {if {$os eq "macosx"} {set cpu arm64}}
}
return "${os}-${cpu}"
}
}
@ -370,7 +387,7 @@ set startdir [pwd]
# -------------------------------------------------------------------------------------
set bootsupport_module_paths [list]
set bootsupport_library_paths [list]
set this_platform_generic [punkboot::lib::platform_generic]
set this_platform_generic [punkboot::lib::platform_punk] ;#normalized punkshell platform-dir name (punk::platform canon)
#we always create these lists in order of desired precedence.
# - this is the same order when adding to auto_path - but will need to be reversed when using tcl:tm::add
if {[file exists [file join $::punkboot::scriptfolder bootsupport]]} {

75
src/modules/punk-999999.0a1.0.tm

@ -8983,10 +8983,77 @@ namespace eval punk {
return $chunks
}
register topics {help} "List help topics"
register tcl {} "Tcl version warnings"
register env {environment} "punkshell environment vars"
register console {term terminal} "Some console behaviour tests and warnings"
punk::args::define {
@id -id ::punk::helptopic::platforms
@cmd -name "help platforms"\
-summary\
"Canonical punkshell platform names."\
-help\
"Show the canonical punkshell platform-dir names
(punk::platform::platforms) with status, the artifact-tree
tiers each name serves, and the buildsuite axis (whether the
punkshell zig buildsuites produce runtimes for the platform -
separate from hosting: punkbin-structured repos can carry
runtimes built by any mechanism), marking the running
interpreter's own platform. These names organize the punkbin
artifact repo's platform folders, bin/runtime/<platform>/
(punk-runtime fetch/list/use -platform), the lib_tclX /
vendorlib_tclX auto_path dirs wired by the boot, and the
runtime-artifact metadata target field. The raw Tcl
platform-package identifiers are shown for comparison
(punk::platform::normalize folds their version-dependent
aliases - amd64/aarch64/macos - into the canonical names)."
@values -min 0 -max 0
}
proc platforms {context args} {
if {[catch {package require punk::platform} errM]} {
return [list [list stderr "help platforms: punk::platform package not available ($errM)\n"]]
}
set frametype [dict get $context frametype]
set chunks [list]
set local_lib [punk::platform::local -tier lib]
set local_runtime [punk::platform::local -tier runtime]
set title "[a+ brightgreen] Canonical punkshell platform names: "
set t [textblock::class::table new -show_seps 0]
$t configure -frametype $frametype
$t add_column -headers [list "Platform"]
$t add_column -headers [list "Status"]
$t add_column -headers [list "Tiers"]
$t add_column -headers [list "Buildsuite"]
$t add_column -headers [list "Notes"]
dict for {p pinfo} [punk::platform::platforms] {
set pshown $p
if {$p eq $local_lib || $p eq $local_runtime} {
set pshown "* $p"
}
$t add_row [list $pshown [dict get $pinfo status] [join [dict get $pinfo tiers] ,] [dict get $pinfo buildsuite] [dict get $pinfo notes]]
}
foreach c {0 1 2 3} {
$t configure_column $c -minwidth [expr {[$t column_datawidth $c] + 2}]
}
$t configure -title $title
set text [$t print]
$t destroy
lappend chunks [list stdout $text]
set detail "* = this interpreter. local: lib-tier $local_lib"
if {$local_runtime ne $local_lib} {
append detail " runtime-tier $local_runtime"
}
append detail "\ntiers: runtime = punkbin + bin/runtime folders; lib = lib_tclX/vendorlib_tclX auto_path dirs"
append detail "\nbuildsuite: whether the punkshell zig buildsuites produce runtimes for the platform"
append detail "\n(a separate axis - punkbin-structured repos can host runtimes built by any mechanism)"
catch {
append detail "\nraw Tcl platform package ([package present platform]): generic [platform::generic] identify [platform::identify]"
}
lappend chunks [list stdout $detail\n]
return $chunks
}
register topics {help} "List help topics"
register tcl {} "Tcl version warnings"
register env {environment} "punkshell environment vars"
register console {term terminal} "Some console behaviour tests and warnings"
register platforms {platform} "Canonical punkshell platform names"
}
#return list of {chan chunk} elements

3
src/modules/punk-buildversion.txt

@ -1,6 +1,7 @@
0.2.6
0.2.7
#First line must be a semantic version number
#all other lines are ignored.
#0.2.7 - new 'help platforms' topic (aliases: platform): canonical punkshell platform-dir names from punk::platform::platforms rendered with status/tiers/buildsuite/notes (buildsuite = zig-buildsuite runtime production, a separate axis from hosting), the running interpreter's lib-tier and runtime-tier names marked, and the raw Tcl platform-package identifiers (generic/identify) shown for comparison. Degrades cleanly when punk::platform is unavailable. All platform/platforms prefixes are mutually ambiguous so they fall through to command lookup (env/environment precedent).
#0.2.6 - G-076: 'help tcl' renders the mitigated/mitigation buginfo axis (punk::lib 0.4.3+): a triggered check reporting mitigated keeps its severity level but displays 'warning level: <level> (mitigated)' in subdued grey (term-grey foreground) instead of the level colour, followed by an indented 'mitigated: <text>' block when mitigation text is supplied. Unmitigated warnings render unchanged.
#0.2.5 - G-045: ::punk::helptopic::define_docs converts from interim left-margin authoring to indented block-form values under the new punk::args @normalize directive (the constructed-definition normalization consumer proof): basehelp/topichelp/help_chunks-extra are braced indented blocks with a structural leading newline, the generated definitions declare @normalize, and the -unindentedfields declarations from punk 0.2.4 are dropped. Rendered 'i help'/'i help_chunks' output unchanged (verified aligned incl. the blank-line separator in help_chunks).
#0.2.4 - G-045: 'i help' usage table alignment - ::punk::helptopic::define_docs authors its help text at the left margin and declares -unindentedfields {-help} on both the generated @cmd line (honoured as of punk::args 0.6.1) and the topic argument line. Previously the @cmd -help braced literal carried ~16 spaces of source indent into the constructed definition (no whole-block normalization), rendering Description continuations +12 right of the first line, and the \n-relative topic -help rendered its first line +4 (the injected display prefix). Both blocks now render flush. Text content unchanged (manual ~70-col folding retained).

1
src/modules/punk/AGENTS.md

@ -33,6 +33,7 @@ Source of truth for all modules under the `punk::*` namespace. This is the prima
- punk::console uses the documented `-console` convention throughout: a `-console` value may be a 2-element {in out} channel list, an anchored `opunk::console` instance name, or an `::opunk::Console` object value (resolved via `punk::console::console_spec_resolve`). Query functions use the hybrid pattern (legacy trailing positional spec also accepted, parsed by `punk::console::internal::hybrid_console_spec`) - new query procs must follow it, with tests (see `src/tests/modules/punk/console/testsuites/console/queryprocs.test`). PUNKARGS definitions include the `-console` option via the shared fragments `::punk::console::argdoc::console_opts` (query/set functions) or `::punk::console::argdoc::console_emit_opts` (emit functions) rather than duplicating the option text; never re-add a `-minsize 2` constraint to `-console` (it rejects instance-name specs). The internal `get_size_using_*` size mechanisms deliberately remain canonical-pair positional (always fed by `get_size`).
- punk::console emit-side functions (the `punk::console::ansi::*` emit wrappers, mouse/paste toggles, `vt52`, `set_tabstop_width`, `titleset`, top-level `move`, and the width-test probes) accept an optional trailing `-console <consolespec>` pair, parsed manually for performance by `punk::console::internal::opt_console_out`/`opt_console_channels` (`_var` variants for procs whose args-tail also carries row/col/data triples). Each carries a documentation-only PUNKARGS definition that includes the shared `::punk::console::argdoc::console_emit_opts` fragment via `punk::args::resolved_def`; keep manual parsing and PUNKARGS synchronized. New emit procs must follow this pattern. Tests live in `src/tests/modules/punk/console/testsuites/console/emitconsole.test`.
- punk::console terminal-property facts (is_vt52, tabwidth, cell_size, last_da1_result, grapheme_cluster_support, check::has_bug_*) are per-console: read/write them via `punk::console::console_fact_get`/`console_fact_set`, keyed by canonical {in out} channel pair. The store is tsv-backed (G-007) so all threads read the same values: the process-default console `{stdin stdout}` keeps the legacy namespace variables (`::punk::console::is_vt52`, `tabwidth`, ...) as its authoritative local storage with write traces mirroring into tsv `punk_console_facts` (so existing external readers and direct writers keep working); non-default consoles store facts only in tsv with an owner-qualified key. Do not bypass the helpers for non-default consoles; use `console_fact_clear` (not direct store manipulation) to reset facts in tests. `ansi_wanted`/`colour_disabled` (string-generation gates), `ansi_available` and raw-mode state are deliberately process-global (rationale documented at the fact store in the module). Tests live in `src/tests/modules/punk/console/testsuites/console/consolefacts.test`.
- punk::console powershell console-mode fallback (G-106, achieved 2026-07-22): on twapi-less windows runtimes, enableRaw/disableRaw are served by a persistent pwsh/powershell named-pipe server (`punk::console::system::ps_consolemode_*` procs). Contract points: the server starts lazily on first use (module load must never spawn processes or emit noise), is a process-wide singleton via tsv `punk_console ps_server_*` under `tsv::lock`, watches the owning process pid and exits with it (no orphan cleanup needed), and is quiet by default (`PUNK_PS_CONSOLEMODE_DEBUG=1` for diagnostics both sides; the spawn redirects child stdout/stderr to NUL but stdin MUST stay inherited - the console input handle is how the server reaches the console). Script resolution: env `PUNK_PS_CONSOLEMODE_SCRIPT` -> argv0-derived -> module-dir-derived -> the embedded copy in the module; the canonical maintained script is `scriptlib/utils/pwsh/consolemode_server_async.ps1` and the embedded copy must stay in sync with it - pinned by `src/tests/modules/punk/console/testsuites/console/psfallback.test`, so edit both together (delivery is powershell `-c <text>`, deliberately not a file: kits carry nothing on disk and ExecutionPolicy cannot block it). Verification recipe: goals/archive/G-106-powershell-consolemode-fallback.md Notes.
- punk::console has a console ownership registry (G-007): `console_owner_register`/`console_owner_get`/`console_owner_forget`, tsv `punk_console_owners` keyed by canonical {in out} pair. Ownership is captured when an opunk::console instance is anchored (via `::opunk::console::lifecycle_callback`, wired by `ensure_object_integration`) and by `default_console`; an unregistered console reads as "operate locally". For `{stdin stdout}` first registration wins and only the owner's forget releases the entry. Owner liveness is validated at consult time. The `dec_has_mode`/`ansi_has_mode` caches live in tsv `punk_console_modecache`.
- punk::console terminal queries are owner-routed (G-007 choke-point brokering): `internal::get_ansi_response_payload` consults `internal::console_route_owner` after spec resolution and forwards the whole call to the console-owning thread via synchronous `thread::send` when the caller is not the owner, so queueing, raw-mode cycling and cooperative reader handling execute in the owner's context and every query proc above the choke point inherits the routing. Routing applies to the default console `{stdin stdout}` only: non-std channel names are thread-local, so an {in out} pair spec names the calling thread's own console and always operates locally (unregistered/self-owned/dead-owner likewise - single-interp behaviour is unchanged). The synchronous send relies on the owner servicing events while the caller blocks (the repl does this while a codethread runs - same property as the repl-installed vt52/colour/mode aliases, which are unaffected because a call arriving in the owner resolves to owner==self). Tests live in `src/tests/modules/punk/console/testsuites/console/ownerrouting.test`.
- Use `punk::args::parse` with `@id` references in `argdoc` namespaces for public API procs.

747
src/modules/punk/console-999999.0a1.0.tm

@ -5746,6 +5746,599 @@ namespace eval punk::console::check {
}
namespace eval punk::console::system {
#-------------------------------------------------------------------------------------
#G-106: powershell console-mode fallback (raw mode for twapi-less windows runtimes)
#A persistent powershell/pwsh named-pipe server process, shared process-wide via tsv,
#flips the console's line/echo input flags on request (the flags live on the console
#object, so a sibling process attached to the same console can set them for us).
#Started lazily on first use (loading punk::console in a piped/non-console or
#worker-thread context must not spawn processes or emit noise); the server watches this
#process's pid and exits with it, so no orphan pwsh processes are left behind.
#Quiet by default - set env PUNK_PS_CONSOLEMODE_DEBUG=1 for diagnostics on both sides.
#-------------------------------------------------------------------------------------
#module location captured at load time - [info script] is empty later at call time,
#and ::argv0 is absent in secondary threads.
variable module_dir ""
catch {
set module_dir [file dirname [file normalize [info script]]]
}
#Embedded copy of scriptlib/utils/pwsh/consolemode_server_async.ps1 (the canonical
#maintained file) - the last-resort script source so the fallback works from kits and
#unusual cwds with no scriptlib on disk. Leading/trailing whitespace is insignificant
#(the text is delivered to powershell via -c). Keep in sync with the canonical file -
#the console testsuite (psfallback.test) compares them.
variable ps_consolemode_script_embedded {
#!SEMICOLONS must be placed after each command as scriptdata needs to be sent to powershell directly with the -c parameter!
;
# consolemode_server_async.ps1 - punkshell console-mode fallback server (canonical copy) (G-106)
# Persistent named-pipe server used by punk::console when twapi is absent on windows.
# The Tcl side (punk::console::system) resolves this file - or falls back to its embedded copy - and launches:
# pwsh|powershell -nop -nol -noni -c <this text with the <punkshell_*> placeholders substituted>
# The -c (command string) mechanism is deliberate: it needs no script file at run time (kits) and is not
# subject to ExecutionPolicy restrictions that can block -File runs.
# The server process shares the launching process's console; it must NOT have its stdin redirected
# (the console input handle is how it reaches the console), and it never reads stdin.
# Placeholders (each overridable by a positional arg for standalone debug runs):
# <punkshell_consoleid> $args[0] unique id suffix for the pipe name
# <punkshell_parentpid> $args[1] pid of the owning tcl process; the server exits when it does
# <punkshell_psdebug> $args[2] "1" enables diagnostic write-host output (default silent)
# Standalone debug run example (from this directory):
# pwsh -nop -nol -f consolemode_server_async.ps1 test1 0 1
# Protocol (one line per named-pipe connection): enableraw | disableraw | ping | exit
# MAINTENANCE: src/modules/punk/console-999999.0a1.0.tm carries this file's text as
# punk::console::system::ps_consolemode_script_embedded (the last-resort resolution for kits and
# unusual cwds). Keep the two in sync - the console testsuite (psfallback.test) fails when they diverge.
;
# The NativeConsoleMethods C# snippet below derives from posh-git (github.com/dahlbyk/posh-git):
# Copyright (c) 2010-2018 Keith Dahlby, Keith Hill, and contributors - MIT license.
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
;
if ($PSVersionTable.PSVersion.Major -le 5) {
# For Windows PowerShell, we want to remove any PowerShell 7 paths from PSModulePath
#snipped from https://github.com/PowerShell/DSC/pull/777/commits/af9b99a4d38e0cf1e54c4bbd89cbb6a8a8598c4e
;
$env:PSModulePath = ($env:PSModulePath -split ';' | Where-Object { $_ -notlike '*\powershell\*' }) -join ';';
};
$consoleModeSource = @"
using System;
using System.Runtime.InteropServices;
public class NativeConsoleMethods
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern IntPtr GetStdHandle(int handleId);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool GetConsoleMode(IntPtr hConsoleOutput, out uint dwMode);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool SetConsoleMode(IntPtr hConsoleOutput, uint dwMode);
public static uint GetConsoleMode(bool input = false)
{
var handle = GetStdHandle(input ? -10 : -11);
uint mode;
if (GetConsoleMode(handle, out mode))
{
return mode;
}
return 0xffffffff;
}
public static uint SetConsoleMode(bool input, uint mode)
{
var handle = GetStdHandle(input ? -10 : -11);
if (SetConsoleMode(handle, mode))
{
return GetConsoleMode(input);
}
return 0xffffffff;
}
}
"@
;
[Flags()]
enum ConsoleModeInputFlags
{
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
ENABLE_WINDOW_INPUT = 0x0008
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_INSERT_MODE = 0x0020
ENABLE_QUICK_EDIT_MODE = 0x0040
ENABLE_EXTENDED_FLAGS = 0x0080
ENABLE_AUTO_POSITION = 0x0100
ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
};
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
};
function rawmode {
param (
[validateSet('enable', 'disable')]
[string]$Action
);
if (!('NativeConsoleMethods' -as [System.Type])) {
Add-Type $consoleModeSource
};
$inputFlags = [NativeConsoleMethods]::GetConsoleMode($true);
$resultflags = $inputflags;
if (($inputflags -band [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -eq [ConsoleModeInputFlags]::ENABLE_LINE_INPUT) {
#cooked mode
$initialstate = "cooked";
if ($action -eq "enable") {
#disable cooked flags
$disable = [uint32](-bnot [uint32][ConsoleModeInputFlags]::ENABLE_LINE_INPUT) -band ( -bnot [uint32][ConsoleModeInputFlags]::ENABLE_ECHO_INPUT);
$adjustedflags = $inputflags -band ($disable);
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
};
} else {
#raw mode
$initialstate = "raw";
if ($action -eq "disable") {
#set cooked flags
$adjustedflags = $inputflags -bor [ConsoleModeInputFlags]::ENABLE_LINE_INPUT -bor [ConsoleModeInputFlags]::ENABLE_ECHO_INPUT;
$resultflags = [NativeConsoleMethods]::SetConsoleMode($true,$adjustedflags);
};
};
};
$consoleid = $args[0];
if ([string]::IsNullOrEmpty($consoleid)) {
$consoleid = "<punkshell_consoleid>"
};
$parentpidraw = $args[1];
if ([string]::IsNullOrEmpty($parentpidraw)) {
$parentpidraw = "<punkshell_parentpid>"
};
$psdebugraw = $args[2];
if ([string]::IsNullOrEmpty($psdebugraw)) {
$psdebugraw = "<punkshell_psdebug>"
};
$psdebug = ($psdebugraw -eq "1");
$pipeName = "punkshell_ps_consolemode_$consoleid";
function dbg {
param([string]$m);
if ($psdebug) {
write-host "consolemode_server($pipeName): $m"
};
};
dbg "starting - parentpidraw: $parentpidraw";
# Parent-process watch: hold a Process object obtained once by pid (handle-based, so immune to
# pid reuse) and poll HasExited in the main loop. This is the orphan-prevention guarantee - the
# owning tcl process does not need to send exit for this server to go away (G-106).
# An unparseable/zero parent pid (e.g a standalone run with placeholders unsubstituted) disables the watch.
$parentproc = $null;
[int]$parentpidnum = 0;
if ([int]::TryParse($parentpidraw, [ref]$parentpidnum) -and ($parentpidnum -gt 0)) {
try {
$parentproc = [System.Diagnostics.Process]::GetProcessById($parentpidnum);
} catch {
dbg "parent process $parentpidnum not found - exiting";
exit 1;
};
};
$sharedData = [hashtable]::Synchronized(@{});
$scriptblock = {
param($tsv);
Add-Type -AssemblyName System.IO.Pipes;
$keepgoing = $true;
while ($keepgoing) {
$pipeServer = $null;
try {
$pipeServer = New-Object System.IO.Pipes.NamedPipeServerStream($pipeName);
try {
$pipeServer.WaitForConnection();
$reader = New-Object System.IO.StreamReader($pipeServer);
$message = $reader.ReadLine();
if ($message -eq "exit") {
$tsv.State = "done";
[void]$msync.Set();
$keepgoing = $false;
} elseif (($message -eq "enableraw") -or ($message -eq "disableraw")) {
$tsv.Message = $message;
[void]$msync.Set();
} elseif ($message -eq "ping") {
# liveness probe - wake the main loop, no mode action
;
[void]$msync.Set();
};
# A null message (a connection that closed without sending a line - e.g a probe)
# and unknown messages are ignored; only an explicit exit message or
# parent-process death shuts the server down.
# Do NOT Close/Dispose the StreamReader here: that disposes the underlying pipe
# stream and a subsequent Disconnect/Dispose on it throws, killing this listener.
# Disposing the pipe stream (finally below) is the whole per-connection cleanup.
} finally {
if ($null -ne $pipeServer) {
$pipeServer.Dispose();
};
};
} catch {
# unexpected listener failure (e.g pipe name already in use) - shut down rather than spin
;
$tsv.State = "done";
[void]$msync.Set();
$keepgoing = $false;
};
};
};
$exitcode = 0;
try {
# AutoResetEvent: WaitOne consumes the signal atomically, so a Set that lands while the main
# loop is servicing a message is never lost (the next WaitOne returns immediately).
$syncEvent = New-Object System.Threading.AutoResetEvent($false);
$runspace = [runspacefactory]::CreateRunspace();
[void]$runspace.Open();
$runspace.SessionStateProxy.SetVariable("pipeName", $pipeName);
$runspace.SessionStateProxy.SetVariable("msync", $syncEvent);
$powershell = [System.Management.Automation.PowerShell]::Create();
$powershell.Runspace = $runspace;
[void]$powershell.Addscript($scriptblock).AddArgument($sharedData);
$sharedData.State = "running";
$sharedData.Message = "";
$asyncResult = $powershell.BeginInvoke();
dbg "named pipe server started in runspace";
while ($true) {
[void]$syncEvent.WaitOne(5000);
$msg = $sharedData.Message;
$sharedData.Message = "";
if ($msg -eq "enableraw") {
dbg "enableraw";
$null = rawmode 'enable';
} elseif ($msg -eq "disableraw") {
dbg "disableraw";
$null = rawmode 'disable';
};
if ($sharedData.State -eq "done") {
dbg "exit message received";
break;
};
if (($null -ne $parentproc) -and $parentproc.HasExited) {
dbg "parent process $parentpidnum has exited";
break;
};
};
} finally {
# The listener runspace may be parked in WaitForConnection - connect once and send exit to
# unblock it, otherwise the process could linger on a parked runspace thread. Skipped when
# the listener already shut itself down (State done - it exited on an exit message or error).
if ($sharedData.State -ne "done") {
try {
$cli = New-Object System.IO.Pipes.NamedPipeClientStream($pipeName);
$cli.connect(1000);
$writer = new-object System.IO.StreamWriter($cli);
$writer.writeline("exit");
$writer.flush();
$cli.Dispose();
} catch {
dbg "listener unblock skipped: $($PSItem.Exception.Message)";
};
};
try {
if ($null -ne $runspace) {
$runspace.Close();
$runspace.Dispose();
};
} catch {
dbg "runspace tidyup error: $($PSItem.Exception.Message)";
};
try {
if ($null -ne $powershell) {
$powershell.dispose();
};
} catch {
dbg "powershell tidyup error: $($PSItem.Exception.Message)";
};
};
dbg "shutdown complete";
exit $exitcode;
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::console::system::ps_consolemode_script_get
@cmd -name "punk::console::system::ps_consolemode_script_get"\
-summary\
"Resolve the powershell console-mode fallback server script text."\
-help\
"Resolves the consolemode_server_async.ps1 script text used by the
twapi-less windows raw-mode fallback (G-106).
Resolution order: PUNK_PS_CONSOLEMODE_SCRIPT env override (used only
when the named file exists), the argv0-derived project location, then
module-location-derived project locations, then the embedded copy
carried by this module (so kits and unusual cwds always resolve).
Returns a dict with keys: source (env|scriptlib|embedded), path (empty
string for embedded) and contents (raw script text with <punkshell_*>
placeholders unsubstituted)."
}]
lappend PUNKARGS [list {
@id -id ::punk::console::system::ps_consolemode_server_ensure
@cmd -name "punk::console::system::ps_consolemode_server_ensure"\
-summary\
"Ensure the process-wide powershell console-mode server is started."\
-help\
"Ensures a single persistent powershell console-mode server process
exists for this tcl process (windows only). Server identity is shared
across all interps/threads via tsv punk_console ps_server_* entries;
the first caller spawns (pwsh.exe preferred, powershell.exe fallback),
later callers reuse. The server is passed this process's pid and exits
when this process does.
Returns a dict: ok (0|1), and on ok=1 pipename, pid, spawntime
(ms clock) and just_started (1 when this call spawned the server);
on ok=0 an error key describes why."
}]
lappend PUNKARGS [list {
@id -id ::punk::console::system::ps_consolemode_send
@cmd -name "punk::console::system::ps_consolemode_send"\
-summary\
"Send a command to the powershell console-mode server."\
-help\
"Sends one protocol message (enableraw|disableraw|ping|exit) to the
powershell console-mode server, ensuring the server first (spawning it
if required). Connection attempts retry until the deadline - a freshly
spawned server is given a generous window for powershell startup. If
the recorded server proves unreachable the recorded state is cleared
and one respawn is attempted.
Returns a dict: ok (0|1), and on failure an error key."
@opts
-deadlinems -type integer -optional 1 -help\
"Override the per-attempt connection deadline in milliseconds.
Default: 15000 within 15s of server spawn (powershell startup),
2500 thereafter."
@values -min 1 -max 1
msg -type string -help\
"Protocol message: enableraw, disableraw, ping or exit."
}]
lappend PUNKARGS [list {
@id -id ::punk::console::system::ps_consolemode_server_stop
@cmd -name "punk::console::system::ps_consolemode_server_stop"\
-summary\
"Stop the recorded powershell console-mode server, if any."\
-help\
"Best-effort shutdown of the recorded powershell console-mode server:
sends the exit protocol message (short deadline, no respawn) and clears
the recorded tsv state. Not required for orphan prevention - the server
watches this process's pid and exits with it - but useful for tests and
for releasing the server early.
Returns a dict: ok 1, stopped (1 if the exit message was delivered),
and pipename (when a server was recorded)."
}]
}
proc ps_consolemode_script_get {} {
#G-106 - see PUNKARGS definition for behaviour contract
variable module_dir
variable ps_consolemode_script_embedded
set relpath scriptlib/utils/pwsh/consolemode_server_async.ps1
set candidates [list]
if {[info exists ::env(PUNK_PS_CONSOLEMODE_SCRIPT)] && $::env(PUNK_PS_CONSOLEMODE_SCRIPT) ne ""} {
#explicit dev/test override - falls through if the named file is missing
lappend candidates [list env $::env(PUNK_PS_CONSOLEMODE_SCRIPT)]
}
if {[info exists ::argv0] && $::argv0 ne ""} {
#argv0 grandparent as project root: covers src/make.tcl (repo root) and bin/<kit> launches
lappend candidates [list scriptlib [file dirname [file dirname [file normalize $::argv0]]]/$relpath]
}
if {$module_dir ne ""} {
#module in <project>/modules/punk -> project root is 2 up
#module in <project>/src/modules/punk -> project root is 3 up
#(a zipfs kit module path simply fails the file-exists probes)
lappend candidates [list scriptlib [file dirname [file dirname $module_dir]]/$relpath]
lappend candidates [list scriptlib [file dirname [file dirname [file dirname $module_dir]]]/$relpath]
}
foreach cand $candidates {
lassign $cand source path
if {[file exists $path]} {
set readok [expr {![catch {
set fd [open $path r]
chan configure $fd -translation binary
set contents [read $fd]
close $fd
}]}]
if {$readok} {
return [dict create source $source path $path contents $contents]
}
}
}
return [dict create source embedded path "" contents $ps_consolemode_script_embedded]
}
proc ps_consolemode_server_ensure {} {
#G-106 - see PUNKARGS definition for behaviour contract
if {"windows" ne $::tcl_platform(platform)} {
return [dict create ok 0 just_started 0 error "powershell consolemode server is windows-only"]
}
#fast path - another interp/thread (or an earlier call) already started the server
if {[tsv::exists punk_console ps_server_pipename]} {
set result [dict create ok 1 just_started 0]
dict set result pipename [tsv::get punk_console ps_server_pipename]
dict set result pid [tsv::get punk_console ps_server_pid]
dict set result spawntime [tsv::get punk_console ps_server_spawntime]
return $result
}
set ps_cmd [auto_execok pwsh.exe]
if {$ps_cmd eq ""} {
set ps_cmd [auto_execok powershell.exe]
}
if {$ps_cmd eq ""} {
return [dict create ok 0 just_started 0 error "neither pwsh.exe nor powershell.exe found on PATH"]
}
set scriptinfo [::punk::console::system::ps_consolemode_script_get]
set psdebug 0
if {[info exists ::env(PUNK_PS_CONSOLEMODE_DEBUG)] && $::env(PUNK_PS_CONSOLEMODE_DEBUG) ni [list "" 0]} {
set psdebug 1
}
set spawned 0
set spawn_error ""
set pipename ""
set spawnpid ""
set spawntime 0
tsv::lock punk_console {
if {[tsv::exists punk_console ps_server_pipename]} {
#another thread won the race
set pipename [tsv::get punk_console ps_server_pipename]
set spawnpid [tsv::get punk_console ps_server_pid]
set spawntime [tsv::get punk_console ps_server_spawntime]
} else {
set ps_consoleid [pid]-[expr {int(999999 * rand())+1}]
set contents [string map [list <punkshell_consoleid> $ps_consoleid <punkshell_parentpid> [pid] <punkshell_psdebug> $psdebug] [dict get $scriptinfo contents]]
set pipename {\\.\pipe\punkshell_ps_consolemode_}
append pipename $ps_consoleid
#stdin must stay inherited - the console input handle is how the server reaches
#the console. stdout/stderr are silenced unless debugging.
if {$psdebug} {
puts stderr "punk::console: starting persistent powershell consolemode server pipename: $pipename (script source: [dict get $scriptinfo source] [dict get $scriptinfo path])"
set spawncatch [catch {exec {*}$ps_cmd -nop -nol -noni -c $contents &} spawnresult]
} else {
set spawncatch [catch {exec {*}$ps_cmd -nop -nol -noni -c $contents > NUL 2> NUL &} spawnresult]
}
if {$spawncatch} {
set spawn_error $spawnresult
} else {
set spawnpid [lindex $spawnresult 0]
set spawntime [clock milliseconds]
tsv::set punk_console ps_server_pipename $pipename
tsv::set punk_console ps_server_pid $spawnpid
tsv::set punk_console ps_server_spawntime $spawntime
set spawned 1
}
}
}
if {$spawn_error ne ""} {
return [dict create ok 0 just_started 0 error "failed to launch powershell consolemode server: $spawn_error"]
}
set result [dict create ok 1 just_started $spawned]
dict set result pipename $pipename
dict set result pid $spawnpid
dict set result spawntime $spawntime
return $result
}
proc ps_consolemode_send {msg args} {
#G-106 - see PUNKARGS definition for behaviour contract
#manual args parsing - called on raw enable/disable paths
set opt_deadlinems ""
foreach {k v} $args {
switch -- $k {
-deadlinems {
set opt_deadlinems $v
}
default {
error "ps_consolemode_send unknown option '$k' - known options: -deadlinems"
}
}
}
set ensure [::punk::console::system::ps_consolemode_server_ensure]
if {![dict get $ensure ok]} {
return [dict create ok 0 error [dict get $ensure error]]
}
set attempt 0
set errMsg ""
while 1 {
incr attempt
set deadlinems $opt_deadlinems
if {$deadlinems eq ""} {
#a freshly spawned server (from this call or another interp moments ago) can take
#seconds to begin listening (powershell startup) - allow for it
set age [expr {[clock milliseconds] - [dict get $ensure spawntime]}]
if {$age < 15000} {
set deadlinems 15000
} else {
set deadlinems 2500
}
}
set pipename [dict get $ensure pipename]
set endtime [expr {[clock milliseconds] + $deadlinems}]
while {[clock milliseconds] < $endtime} {
set ok_write 0
if {![catch {open $pipename w} pipe]} {
if {![catch {
chan configure $pipe -buffering line
puts -nonewline $pipe "$msg\r\n"
close $pipe
} errMsg]} {
set ok_write 1
} else {
catch {close $pipe}
}
} else {
set errMsg $pipe
}
if {$ok_write} {
return [dict create ok 1 attempt $attempt]
}
after 100
}
if {$attempt >= 2} {
return [dict create ok 0 error "failed to reach powershell consolemode server on $pipename: $errMsg"]
}
#the recorded server may be stale (e.g killed externally) - clear the recorded state
#(only if it still names the pipe we tried) and respawn once
tsv::lock punk_console {
if {[tsv::exists punk_console ps_server_pipename] && [tsv::get punk_console ps_server_pipename] eq $pipename} {
tsv::unset punk_console ps_server_pipename
tsv::unset punk_console ps_server_pid
tsv::unset punk_console ps_server_spawntime
}
}
set ensure [::punk::console::system::ps_consolemode_server_ensure]
if {![dict get $ensure ok]} {
return [dict create ok 0 error [dict get $ensure error]]
}
}
}
proc ps_consolemode_server_stop {} {
#G-106 - see PUNKARGS definition for behaviour contract
if {![tsv::exists punk_console ps_server_pipename]} {
return [dict create ok 1 stopped 0 note "no powershell consolemode server recorded"]
}
set pipename [tsv::get punk_console ps_server_pipename]
#deliberately not via ps_consolemode_send - that would respawn an unreachable server
set sent 0
set endtime [expr {[clock milliseconds] + 1000}]
while {[clock milliseconds] < $endtime} {
if {![catch {open $pipename w} pipe]} {
if {![catch {
chan configure $pipe -buffering line
puts -nonewline $pipe "exit\r\n"
close $pipe
}]} {
set sent 1
break
} else {
catch {close $pipe}
}
}
after 100
}
tsv::lock punk_console {
if {[tsv::exists punk_console ps_server_pipename] && [tsv::get punk_console ps_server_pipename] eq $pipename} {
tsv::unset punk_console ps_server_pipename
tsv::unset punk_console ps_server_pid
tsv::unset punk_console ps_server_spawntime
}
}
return [dict create ok 1 stopped $sent pipename $pipename]
}
proc enableRaw_stty {{channel stdin}} {
upvar ::punk::console::previous_stty_state_$channel previous_stty_state_$channel
@ -5917,67 +6510,56 @@ namespace eval punk::console::system {
}
proc enableRaw_powershell {{channel stdin}} {
#enableRaw_powershell is a fallback for when twapi is not present.
#It uses a persistent powershell process to set the console mode to raw, by writing commands to a named pipe that the powershell process is listening on.
#This does not need to be used in windows in the rare case where the console is an alternative terminal that supports stty (e.g mintty without winpty)
#- but it is really intended for use in environments where twapi is not present and stty doesn't work (e.g standard windows console).
#puts stderr "punk::console::enableRaw"
#enableRaw_powershell is a fallback for when twapi is not present (G-106).
#It asks the persistent powershell console-mode server (started lazily on first use,
#shared process-wide via tsv, self-terminating with this process - see
#punk::console::system::ps_consolemode_server_ensure) to clear the console's
#line/echo input flags. The channel argument is unused by the server path - the
#server acts on the process console's input handle.
#stty remains as a last resort: it does not *usually* work on windows
#(the msys/cygwin stty is a subprocess - useful to retrieve info but generally unable
#to affect the calling process/console). An exception is an msys/cygwin terminal such
#as mintty configured without winpty (e.g env MSYS = disable_pcon prior to launch).
upvar ::punk::console::previous_stty_state_$channel previous_stty_state_$channel
upvar ::punk::console::ps_consolemode_contents ps_consolemode_contents
upvar ::punk::console::ps_pipename ps_pipename
if {[info exists ps_consolemode_contents]} {
#ps_pipename e.g \\.\pipe\punkwinshell_ps_consolemode_12345-1223456
set trynum 0
set wrote 0
while {$trynum < 5} {
incr trynum
if {![catch {
set pipe [open $ps_pipename w]
} errMsg]} {
chan conf $pipe -buffering line
puts -nonewline $pipe "enableraw\r\n"
#flush $pipe
#after 10
#close $pipe
set wrote 1
break
} else {
after 100
if {"windows" eq $::tcl_platform(platform)} {
set sendresult [::punk::console::system::ps_consolemode_send enableraw]
if {[dict get $sendresult ok]} {
tsv::set punk_console is_raw 1
#the server applies the console flags asynchronously (fire-and-forget protocol).
#Where the runtime can read the live mode (tcl9 -inputmode on a console channel),
#wait briefly for the flip so callers can rely on raw being active on return.
set confirmed unknown
if {[dict exists [chan configure $channel] -inputmode]} {
set confirmed 0
set endtime [expr {[clock milliseconds] + 750}]
while {[clock milliseconds] < $endtime} {
if {[dict get [chan configure $channel] -inputmode] eq "raw"} {
set confirmed 1
break
}
after 25
}
}
return [list $channel [list from unknown to raw note "set via powershell consolemode server (confirmed $confirmed)"]]
}
if {$wrote} {
tsv::set punk_console is_raw 1
#after 100
close $pipe
} else {
puts stderr "write to $ps_pipename failed trynum: $trynum\n$errMsg"
}
} elseif {[set sttycmd [auto_execok stty]] ne ""} {
#todo - something else entirely
#this approach does not *usually* work on windows
#the msys/cygwin stty command is launched as a subprocess - can be used to retrieve info
# but seems to be useless as far as affecting the calling process/console
#An exception is when running in an msys/cygwin terminal - e.g mintty *when* it is configured to not use winpty
#(e.g by setting environment variable MSYS = disable_pcon, prior to launch.)
#not normal operation - fall through to stty attempt with an actionable note
puts stderr "punk::console::enableRaw: powershell consolemode server unavailable ([dict get $sendresult error]) - trying stty"
}
if {[set sttycmd [auto_execok stty]] ne ""} {
if {[set previous_stty_state_$channel] eq ""} {
set previous_stty_state_$channel [exec {*}$sttycmd -g <@$channel]
}
exec {*}$sttycmd raw -echo <@$channel
tsv::set punk_console is_raw 1
#review - inconsistent return dict
return [dict create stdin [list from [set previous_stty_state_$channel] to "" note "fixme - to state not shown"]]
} else {
error "punk::console::enableRaw Unable to use twapi or stty to set raw mode - aborting"
error "punk::console::enableRaw Unable to use twapi, the powershell consolemode server, or stty to set raw mode - aborting"
}
}
proc disableRaw_powershell {{channel stdin}} {
#disableRaw powershell version
#disableRaw powershell/fallback version (G-106)
upvar ::punk::console::previous_stty_state_$channel previous_stty_state_$channel
set ch_state [chan conf $channel]
@ -5986,11 +6568,20 @@ namespace eval punk::console::system {
tsv::set punk_console is_raw 0
return [list $channel [list from [dict get $ch_state -inputmode] to normal]]
} else {
#tcl <= 8.6x doesn't support -inputmode
#tcl <= 8.6x doesn't support -inputmode - ask the powershell consolemode server
#to restore the console's line/echo input flags
if {"windows" eq $::tcl_platform(platform)} {
set sendresult [::punk::console::system::ps_consolemode_send disableraw]
if {[dict get $sendresult ok]} {
tsv::set punk_console is_raw 0
return [list $channel [list from unknown to normal note "set via powershell consolemode server"]]
}
#not normal operation - fall through to stty attempt with an actionable note
puts stderr "punk::console::disableRaw: powershell consolemode server unavailable ([dict get $sendresult error]) - trying stty"
}
if {[set sttycmd [auto_execok stty]] ne ""} {
#this doesn't work on windows
#It may seem to - only because running *any* external utility can exit raw mode
set sttycmd [auto_execok stty]
if {[set previous_stty_state_$channel] ne ""} {
exec {*}$sttycmd [set previous_stty_state_$channel]
set previous_stty_state_$channel ""
@ -6002,7 +6593,7 @@ namespace eval punk::console::system {
#probably not. We should work out how to read the stty result flags and set a result.. or just limit from,to to showing echo and lineedit states.
return [list stdin [list from "[set previous_stty_state_$channel]" to "" note "fixme - to state not shown"]]
} else {
error "punk::console::disableRaw Unable to use twapi or stty to unset raw mode - aborting"
error "punk::console::disableRaw Unable to use twapi, the powershell consolemode server, or stty to unset raw mode - aborting"
}
}
}
@ -6181,52 +6772,14 @@ namespace eval punk::console {
proc disableRaw {{channel stdin}} [info body ::punk::console::system::disableRaw_twapi]
} else {
variable ps_consolemode_pid
variable ps_consolemode_contents
variable ps_pipename
if {![info exists ps_consolemode_contents]} {
#start persistent powershell consolemode_server.ps1 named pipe server
#::argv0 is absent in secondary threads (thread::create workers, codethreads) -
#the module must remain loadable there
if {[info exists ::argv0] && $::argv0 ne ""} {
set pstooldir [file dirname [file dirname [file normalize $::argv0]]]/scriptlib/utils/pwsh
} else {
set pstooldir [pwd]
}
#set ps_script $pstooldir/consolemode_server.ps1
set ps_script $pstooldir/consolemode_server_async.ps1
if {[file exists $ps_script]} {
set fd [open $ps_script r]
chan configure $fd -translation binary
set ps_consoleid [pid]-[expr {int(999 * rand())+1}]
set ps_consolemode_contents [string map [list "<punkshell_consoleid>" $ps_consoleid] [read $fd]]
close $fd
#set ps_consolemode_pipe [twapi::namedpipe_client {//./pipe/punkshell_ps_consolemode} -access write]
#set ps_cmd [auto_execok pwsh.exe]
set ps_cmd [auto_execok pwsh.exe]
if {$ps_cmd eq ""} {
set ps_cmd [auto_execok powershell.exe]
}
if {$ps_cmd ne ""} {
set ps_consolemode_pid [exec {*}$ps_cmd -nop -nol -c $ps_consolemode_contents &]
set ps_pipename {\\.\pipe\punkshell_ps_consolemode_}
append ps_pipename $ps_consoleid
puts stderr "twapi not present, using persistent powershell process: pipename: $ps_pipename pid: $ps_consolemode_pid"
#todo - taskkill /F /PID $ps_consolemode_pid
#when?
#review
#if {[catch {puts "pidinfo: [::tcl::process::status $ps_consolemode_pid]"} errM]} {
# puts stderr "--- failed to get process status for $ps_consolemode_pid\n$errM"
#}
#set p [open {\\.\pipe\punkshell_ps_consolemode} w]
#chan conf $p -buffering none -blocking 1
#puts $p ""
#close $p
}
}
}
#no twapi - use the powershell console-mode fallback (G-106).
#The persistent powershell named-pipe server is started lazily on first
#enableRaw/disableRaw use (punk::console::system::ps_consolemode_server_ensure),
#not at module load: loading punk::console in a piped/non-console or worker-thread
#context must not spawn processes or emit noise. Server state is process-wide
#(tsv punk_console ps_server_*), the server watches this process's pid and exits
#with it, and script resolution no longer depends on argv0 alone
#(see punk::console::system::ps_consolemode_script_get).
proc enableRaw {{channel stdin}} [info body ::punk::console::system::enableRaw_powershell]
proc disableRaw {{channel stdin}} [info body ::punk::console::system::disableRaw_powershell]
@ -6344,7 +6897,7 @@ namespace eval punk::console {
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::console ::punk::console::argdoc ::punk::console::internal ::punk::console::local ::punk::console::ansi ::punk::console::check
lappend ::punk::args::register::NAMESPACES ::punk::console ::punk::console::argdoc ::punk::console::internal ::punk::console::local ::punk::console::ansi ::punk::console::check ::punk::console::system ::punk::console::system::argdoc
}

8
src/modules/punk/console-buildversion.txt

@ -1,6 +1,12 @@
0.7.2
0.8.0
#First line must be a semantic version number
#all other lines are ignored.
#0.8.0 - G-106 powershell console-mode fallback overhaul (raw mode on twapi-less windows runtimes). Lifecycle: server starts lazily on first enableRaw/disableRaw (module load no longer spawns a process or prints to stderr), is a process-wide singleton shared across interps/threads (tsv punk_console ps_server_* under tsv::lock), and watches the owning process pid (handle-based Process.HasExited) so it exits with the session - no orphan pwsh processes. The 20s ping-staleness keepalive is removed: it was the root cause of the observed mid-session early shutdown (no tcl-side pinger ever existed).
#0.8.0 - server script listener defects fixed: per-connection StreamReader.Close disposed the underlying pipe stream so the next Disconnect/Dispose threw and killed the listener runspace after the first message; disableraw was swallowed (only enableraw was forwarded to the mode loop); a null-message connection (e.g a probe) no longer shuts the server down; AutoResetEvent replaces ManualResetEvent+late-Reset (lost-wakeup race).
#0.8.0 - new punk::console::system procs (PUNKARGS-documented): ps_consolemode_script_get (resolution chain: env PUNK_PS_CONSOLEMODE_SCRIPT -> argv0-derived -> module-location-derived -> embedded copy in this module; the old [pwd] fallback is gone - kits and unusual cwds now always resolve), ps_consolemode_server_ensure (singleton spawn), ps_consolemode_send (deadline-based connect with generous window for powershell startup, one respawn on unreachable server), ps_consolemode_server_stop (explicit shutdown for tests/tools).
#0.8.0 - enableRaw_powershell waits for the flip to be observable before returning where the runtime can read live mode (tcl9 -inputmode poll, 750ms cap) - closes the fire-and-forget race between the pipe write and the server applying the console flags; disableRaw_powershell on runtimes without -inputmode (tcl 8.6) now asks the server (previously stty-or-error, so cooked mode could not be restored via the fallback).
#0.8.0 - quiet in normal operation: server child spawned with stdout/stderr redirected to NUL (stdin stays inherited - the console input handle is how the server reaches the console), all ps1 diagnostics debug-gated; env PUNK_PS_CONSOLEMODE_DEBUG=1 enables diagnostics on both sides (tcl spawn note + ps1 write-host trail, unredirected).
#0.8.0 - canonical script scriptlib/utils/pwsh/consolemode_server_async.ps1 rewritten (protocol: enableraw|disableraw|ping|exit; parent-pid watch; posh-git MIT attribution for the NativeConsoleMethods C# snippet); the module's embedded copy is kept in sync by the new console testsuite psfallback.test.
#0.7.2 - PUNKARGS fix: console_fact_get/console_fact_set key argument now declares -choiceprefix 0 so documented behaviour matches the manual positional parser (exact key match required, no prefix matching - the proc feeds key directly into a dict lookup that has no prefix normalization)
#0.7.1 - new idle-reader hostage guard in get_ansi_response_payload: on a tcl 8.6 windows console in cooked (line) mode with a readable handler armed on stdin (the idle-at-a-line-mode-prompt condition - e.g. a query fired from an after-script or worker thread while the shell waits for input), the query now fails fast with errorcode {PUNK CONSOLE QUERY HOSTAGE_COOKED_READ} and emits nothing, instead of timing out (~500ms) and having the response swallowed by the driver's parked cooked ReadConsole until Enter then leaking to the line reader as phantom input. Mid-command queries (repl reader disarmed) and raw mode are unaffected; best-effort - a parked read can outlive a removed handler, so the guard catches the systematic case only
#0.7.1 - compound emit-then-query operations flush their emissions before querying: get_size_using_cursormove/get_size_using_cursorrestore (the far-corner move), test_char_width (positioning and the measured test emission) and test_string_cursor (alt-screen/move/erase). Under G-007 routing the position query may execute in the console-owning thread, whose flush acts on its own channel instance for the same OS handle - the caller's unflushed emissions then reach the terminal after the query measures, e.g. get_size returning 'columns 1' on tcl 8.6 where the -winsize shortcut is unavailable and the ANSI mechanism actually runs (pre-routing this was masked because emit and query shared one channel instance, so the query's flush pushed the emissions too). Error paths flush their cursor-restore emissions likewise.

978
src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd vendored

File diff suppressed because it is too large Load Diff

19
src/modules/punk/mix/#modpod-templates-999999.0a1.0/templates/project_layouts/vendor/punk/project-0.1/src/make.tcl vendored

@ -278,6 +278,23 @@ namespace eval ::punkboot::lib {
return "${plat}-${cpu}"
}
proc platform_punk {} {
#canonical punkshell platform-dir name: platform_generic normalized.
#INLINE COPY of punk::platform::normalize (src/modules/punk/platform-*.tm;
#'help platforms' documents the canon) - the boot stage cannot package
#require, so keep this mapping in sync with that module:
#amd64->x86_64, aarch64->arm64, macos->macosx, macosx arm->arm64.
set parts [split [platform_generic] -]
set cpu [lindex $parts end]
set os [join [lrange $parts 0 end-1] -]
if {$os eq "macos"} {set os macosx}
switch -- $cpu {
amd64 {set cpu x86_64}
aarch64 {set cpu arm64}
arm {if {$os eq "macosx"} {set cpu arm64}}
}
return "${os}-${cpu}"
}
}
@ -370,7 +387,7 @@ set startdir [pwd]
# -------------------------------------------------------------------------------------
set bootsupport_module_paths [list]
set bootsupport_library_paths [list]
set this_platform_generic [punkboot::lib::platform_generic]
set this_platform_generic [punkboot::lib::platform_punk] ;#normalized punkshell platform-dir name (punk::platform canon)
#we always create these lists in order of desired precedence.
# - this is the same order when adding to auto_path - but will need to be reversed when using tcl:tm::add
if {[file exists [file join $::punkboot::scriptfolder bootsupport]]} {

237
src/modules/punk/platform-999999.0a1.0.tm

@ -0,0 +1,237 @@
# -*- tcl -*-
# Maintenance Instruction: leave the 999999.xxx.x as is and use 'pmix make' or src/make.tcl to update from <pkg>-buildversion.txt
#
# Please consider using a BSD or MIT style license for greatest compatibility with the Tcl ecosystem.
# Code using preferred Tcl licenses can be eligible for inclusion in Tcllib, Tklib and the punk package repository.
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
# (C) 2026
#
# @@ Meta Begin
# Application punk::platform 999999.0a1.0
# Meta platform tcl
# Meta license BSD
# @@ Meta End
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Requirements
package require platform ;#Tcl's own platform package (vendored 1.0.19 in kits; 1.1.x in tcl9 core) - the RAW identifier underneath this module's normalization
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
namespace eval punk::platform {
#punk::platform - the CANONICAL punkshell platform naming layer (2026-07-22
#platform-folder synchronization; groundwork for the G-105 cross-target arc).
#
#punkshell artifact trees are organized by PLATFORM-DIR names of the form
#<os>-<cpu> (e.g win32-x86_64), used by: the punkbin artifact repo's top-level
#folders, bin/runtime/<platform>/ (the runtime store punk-runtime manages),
#lib_tclX/<platform> + vendorlib_tclX/<platform> auto_path dirs (wired by the
#boot machinery), and the G-103 runtime artifact metadata 'target' field.
#
#Tcl's platform package remains the raw identifier (platform::generic /
#platform::identify) but its outputs vary by version and pass some machine
#names through unmapped - the differences this module's normalize absorbs:
# - cpu 'amd64' (BSD machine names) -> x86_64
# - cpu 'aarch64' (linux arm64) -> arm64
# - os 'macos' (platform 1.1.x on osVersion>19; older versions and the
# boot's snipped copy say 'macosx') -> macosx
# - macosx cpu 'arm' -> arm64 (platform::generic's 'arm*' glob collapses
# the darwin 'arm64' machine name to 'arm'; Apple silicon is 64-bit only,
# so the arm token on macosx always means arm64. Elsewhere 'arm' is a
# genuine 32-bit token and is preserved.)
#The special name 'macosx' (no cpu suffix) is the RUNTIME-tier convention for
#universal (multi-arch) macOS binaries - punkbin and bin/runtime keep one
#universal folder; the per-arch macosx-x86_64/macosx-arm64 names serve the
#lib-tree tier where per-arch payloads are the norm.
#
#The boot machinery (punkboot::lib in src/vfs/_config/punk_main.tcl /
#project_main.tcl and src/make.tcl) cannot 'package require' at its stage and
#carries a snipped platform::generic plus an inline copy of THIS module's
#normalization mapping - if the mapping here changes, those copies must
#change with it (each carries a pointer comment to this module).
namespace eval argdoc {
variable PUNKARGS
}
#canonical platform records. status:
# supported - a punkshell target: artifact trees exist or are planned and
# tooling/build arcs (G-103/G-105) treat it as a real target
# dormant - recognized and folders may exist, but utility is under review
# recognized - a name the naming scheme reserves; no artifacts or tooling yet
#tiers: which artifact-tree tiers use the name -
# runtime = punkbin platform folders + bin/runtime/<platform>/
# lib = lib_tclX/<platform> + vendorlib_tclX/<platform> auto_path dirs
#buildsuite: whether OUR zig buildsuite system (src/buildsuites; G-103 kit
#family, G-105 cross-target) produces runtimes for the platform. A SEPARATE
#AXIS from tiers deliberately: punkbin (or a third-party repo using the same
#structure) can host runtimes built by any mechanism - a platform can be
#runtime-tier hosted while buildsuite=none (we may never build it ourselves).
# supported - a tracked suite produces family artifacts today
# planned - named in an active/proposed goal (e.g G-105 linux-first)
# candidate - plausible zig target, no committed goal
# none - no build intention; hosted/third-party runtimes only
variable platforms {
win32-x86_64 {status supported tiers {runtime lib} buildsuite supported notes "primary development platform (suite_tcl90)"}
win32-ix86 {status supported tiers {runtime lib} buildsuite candidate notes "32-bit x86; hosting for available third-party runtimes/libs - zig can target it but a buildsuite is undetermined"}
linux-x86_64 {status supported tiers {runtime lib} buildsuite planned notes "G-105 first cross-target, WSL-verified"}
linux-arm64 {status supported tiers {runtime lib} buildsuite candidate notes "aarch64; punkbin's existing arm kit predates the arm64 name and sits in linux-arm"}
linux-arm {status supported tiers {runtime lib} buildsuite candidate notes "32-bit arm"}
macosx {status supported tiers {runtime} buildsuite candidate notes "universal (multi-arch) macOS binaries - runtime tier keeps one folder; zig darwin cross needs SDK work"}
macosx-x86_64 {status supported tiers {lib} buildsuite none notes "would follow a macosx runtime arc"}
macosx-arm64 {status supported tiers {lib} buildsuite none notes "Apple silicon; would follow a macosx runtime arc"}
freebsd-x86_64 {status supported tiers {runtime lib} buildsuite candidate notes ""}
freebsd-arm64 {status supported tiers {runtime lib} buildsuite candidate notes "no artifacts yet"}
msys-x86_64 {status dormant tiers {lib} buildsuite none notes "msys/cygwin-built tclsh runtimes; utility under review"}
openbsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
netbsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
dragonflybsd-x86_64 {status recognized tiers {runtime} buildsuite none notes "hosted third-party runtimes possible"}
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::platform::platforms
@cmd -name punk::platform::platforms\
-summary\
"Canonical punkshell platform records."\
-help\
"Return the canonical punkshell platform-name records as a dict
keyed by platform-dir name (e.g win32-x86_64). Each value is a
dict with keys:
status supported|dormant|recognized
tiers which artifact-tree tiers use the name
(runtime = punkbin + bin/runtime folders,
lib = lib_tclX/vendorlib_tclX auto_path dirs)
buildsuite supported|planned|candidate|none - whether the
punkshell zig buildsuite system produces
runtimes for the platform. Deliberately a
separate axis from tiers: punkbin-structured
repos can host runtimes built by any mechanism,
so a platform can be runtime-tier hosted while
buildsuite=none.
notes free-text qualifiers
These names are the contract for punkbin platform folders,
bin/runtime/<platform>/, lib_tclX/vendorlib_tclX platform
dirs and the G-103 runtime-artifact metadata target field.
See also 'help platforms' in the punk shell."
@values -min 0 -max 0
}]
}
proc platforms {} {
variable platforms
return $platforms
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::platform::normalize
@cmd -name punk::platform::normalize\
-summary\
"Normalize a platform identifier to the canonical punkshell name."\
-help\
"Normalize a platform::generic-style <os>-<cpu> identifier (or an
already-canonical punkshell name) to the canonical punkshell
platform-dir name:
cpu amd64 -> x86_64, aarch64 -> arm64
os macos -> macosx (Tcl platform 1.1.x renamed modern macOS)
macosx cpu arm -> arm64 (platform::generic's arm* glob folds
the darwin arm64 machine name to arm; Apple silicon is
64-bit only - non-macosx 'arm' stays 32-bit arm)
Unrecognized os/cpu tokens pass through unchanged - normalize
never invents names, it only folds known aliases."
@values -min 1 -max 1
platform -type string -help\
"Platform identifier, e.g the result of platform::generic."
}]
}
proc normalize {platform} {
set parts [split $platform -]
if {[llength $parts] < 2} {
#single-token names (e.g the universal 'macosx') - fold macos only
if {$platform eq "macos"} {
return macosx
}
return $platform
}
#os may itself contain a dash in principle - treat last token as cpu
set cpu [lindex $parts end]
set os [join [lrange $parts 0 end-1] -]
switch -- $os {
macos {set os macosx}
}
switch -- $cpu {
amd64 {set cpu x86_64}
aarch64 {set cpu arm64}
i386 - i486 - i586 - i686 {set cpu ix86}
arm {
if {$os eq "macosx"} {
set cpu arm64
}
}
}
return "${os}-${cpu}"
}
namespace eval argdoc {
variable PUNKARGS
lappend PUNKARGS [list {
@id -id ::punk::platform::local
@cmd -name punk::platform::local\
-summary\
"Canonical punkshell platform name of the running interpreter."\
-help\
"Return the canonical punkshell platform-dir name for the running
interpreter: platform::generic normalized per
punk::platform::normalize.
With -tier runtime, macosx per-arch names collapse to the
universal 'macosx' (the runtime-store convention: punkbin and
bin/runtime keep one universal macOS folder; the same collapse
src/make.tcl applies when locating bin/runtime/<platform>).
-tier lib (the default) returns the per-arch name used by the
lib_tclX/vendorlib_tclX auto_path dirs."
@opts
-tier -type string -default lib -choices {lib runtime} -help\
"Artifact-tree tier the name is for."
@values -min 0 -max 0
}]
}
proc local {args} {
set tier lib
foreach {k v} $args {
switch -- $k {
-tier {
if {$v ni {lib runtime}} {
error "punk::platform::local - invalid -tier '$v' (expected lib|runtime)"
}
set tier $v
}
default {
error "punk::platform::local - unknown option '$k' (known: -tier)"
}
}
}
set p [normalize [::platform::generic]]
if {$tier eq "runtime" && [string match macosx-* $p]} {
return macosx
}
return $p
}
}
namespace eval ::punk::args::register {
#use fully qualified so 8.6 doesn't find existing var in global namespace
lappend ::punk::args::register::NAMESPACES ::punk::platform
}
# ++ +++ +++ +++ +++ +++ +++ +++ +++ +++ +++
## Ready
package provide punk::platform [namespace eval punk::platform {
variable version
set version 999999.0a1.0
}]
return

4
src/modules/punk/platform-buildversion.txt

@ -0,0 +1,4 @@
0.1.0
#First line must be a semantic version number
#all other lines are ignored.
#0.1.0 - initial release (2026-07-22 platform-folder synchronization; G-105 groundwork): the canonical punkshell platform naming layer over Tcl's platform package - platforms (canonical records: status/tiers/buildsuite/notes; buildsuite = whether the punkshell zig buildsuites produce runtimes for the platform, deliberately a separate axis from the tiers a name organizes - punkbin-structured repos can host runtimes built by any mechanism, so runtime-tier hosting can coexist with buildsuite=none), normalize (amd64->x86_64, aarch64->arm64, macos->macosx, macosx arm->arm64), local ?-tier lib|runtime? (runtime tier collapses macosx per-arch to the universal macosx runtime-store name). Canon consumed by: punkbin platform folders, bin/runtime/<platform>/, lib_tclX/vendorlib_tclX platform dirs, G-103 artifact metadata targets, 'help platforms' topic. The boot machinery (punkboot::lib platform_generic snips in punk_main.tcl/project_main.tcl/make.tcl) carries an inline copy of the normalization mapping - keep in sync.

4
src/modules/punk/repl-999999.0a1.0.tm

@ -3317,7 +3317,9 @@ proc repl::repl_process_data {inputchan chunktype chunk stdinlines prompt_config
} else {
# -inputmode unavailable
#tcl 8.6 doesn't have -inputmode - meaning it has to call punk:console::enableRaw each time
#enableRaw on windows without twapi involves launching a pwsh process - which gives a noticeable lag in keyboard input.
#enableRaw on windows without twapi is a named-pipe write to the persistent powershell
#consolemode server (G-106) - cheap once running, but the first use of a session pays the
#server spawn (powershell startup).
#enableRaw on Unix involves a call to stty - which is generally fast - but still to be avoided if not required.
set re_enable_raw_required 1
}

19
src/project_layouts/vendor/punk/basic/src/make.tcl vendored

@ -278,6 +278,23 @@ namespace eval ::punkboot::lib {
return "${plat}-${cpu}"
}
proc platform_punk {} {
#canonical punkshell platform-dir name: platform_generic normalized.
#INLINE COPY of punk::platform::normalize (src/modules/punk/platform-*.tm;
#'help platforms' documents the canon) - the boot stage cannot package
#require, so keep this mapping in sync with that module:
#amd64->x86_64, aarch64->arm64, macos->macosx, macosx arm->arm64.
set parts [split [platform_generic] -]
set cpu [lindex $parts end]
set os [join [lrange $parts 0 end-1] -]
if {$os eq "macos"} {set os macosx}
switch -- $cpu {
amd64 {set cpu x86_64}
aarch64 {set cpu arm64}
arm {if {$os eq "macosx"} {set cpu arm64}}
}
return "${os}-${cpu}"
}
}
@ -370,7 +387,7 @@ set startdir [pwd]
# -------------------------------------------------------------------------------------
set bootsupport_module_paths [list]
set bootsupport_library_paths [list]
set this_platform_generic [punkboot::lib::platform_generic]
set this_platform_generic [punkboot::lib::platform_punk] ;#normalized punkshell platform-dir name (punk::platform canon)
#we always create these lists in order of desired precedence.
# - this is the same order when adding to auto_path - but will need to be reversed when using tcl:tm::add
if {[file exists [file join $::punkboot::scriptfolder bootsupport]]} {

1012
src/project_layouts/vendor/punk/project-0.1/bin/punk-runtime.cmd vendored

File diff suppressed because it is too large Load Diff

19
src/project_layouts/vendor/punk/project-0.1/src/make.tcl vendored

@ -278,6 +278,23 @@ namespace eval ::punkboot::lib {
return "${plat}-${cpu}"
}
proc platform_punk {} {
#canonical punkshell platform-dir name: platform_generic normalized.
#INLINE COPY of punk::platform::normalize (src/modules/punk/platform-*.tm;
#'help platforms' documents the canon) - the boot stage cannot package
#require, so keep this mapping in sync with that module:
#amd64->x86_64, aarch64->arm64, macos->macosx, macosx arm->arm64.
set parts [split [platform_generic] -]
set cpu [lindex $parts end]
set os [join [lrange $parts 0 end-1] -]
if {$os eq "macos"} {set os macosx}
switch -- $cpu {
amd64 {set cpu x86_64}
aarch64 {set cpu arm64}
arm {if {$os eq "macosx"} {set cpu arm64}}
}
return "${os}-${cpu}"
}
}
@ -370,7 +387,7 @@ set startdir [pwd]
# -------------------------------------------------------------------------------------
set bootsupport_module_paths [list]
set bootsupport_library_paths [list]
set this_platform_generic [punkboot::lib::platform_generic]
set this_platform_generic [punkboot::lib::platform_punk] ;#normalized punkshell platform-dir name (punk::platform canon)
#we always create these lists in order of desired precedence.
# - this is the same order when adding to auto_path - but will need to be reversed when using tcl:tm::add
if {[file exists [file join $::punkboot::scriptfolder bootsupport]]} {

6
src/runtime/AGENTS.md

@ -7,12 +7,12 @@ Houses the `mapvfs.config` that maps VFS payloads to platform runtimes, plus the
## Ownership
- Agents must not modify runtime executables or `mapvfs.config` unless explicitly asked.
- Runtime `.exe` files are platform binaries installed via `bin/runtime.cmd` and must not be edited.
- Runtime `.exe` files are platform binaries installed via `bin/punk-runtime.cmd` (renamed from `runtime.cmd` under G-097) and must not be edited.
## Local Contracts
- `mapvfs.config` defines which `src/vfs/*.vfs` folders combine with which runtime binaries.
- Runtime executables are placed here by the `bin/runtime.cmd` helper or manually.
- `mapvfs.config` defines which `src/vfs/*.vfs` folders combine with which runtime binaries (stored under `bin/runtime/<platform>/`).
- Runtime executables are placed in `bin/runtime/<platform>/` by the `bin/punk-runtime.cmd` helper, manually, or (G-103 family runtimes) copied from the suite_tcl90 `kit-family` build products under `src/buildsuites/_build/suite_tcl90/out/family/`.
- The `_build/` subdirectory holds build intermediates; it can be safely deleted.
## Work Guidance

17
src/runtime/mapvfs.config

@ -60,9 +60,8 @@ tclsfe-x64.exe {punk9wintk903.vfs punk91 zip}
#suite_tcl90 zig build (G-096/G-098, both achieved 2026-07-20) - runtime copied manually from
# src/buildsuites/_build/suite_tcl90/out/bin/tclsh90szip.exe (static tclsh 9.0.5 with attached zip
# carrying the tcl library). tk/thread dlls + script libs ride in the .vfs for now - future: attach
# at least tclvfs & thread to the runtime's own zip (G-103 kit family; a piperepl-runtime punk kit
# based on tclsh90sprzip is also G-103 territory).
# carrying the tcl library). tk/thread dlls + script libs ride in the .vfs for this entry; the
# G-103 family runtimes below supersede that arrangement.
#_beta convention: wrap freshly suite-built runtimes as *_beta kits even BEFORE they have passed the
# punk test suites - so new-runtime behaviour can be trialled interactively. Drop _beta on acceptance.
# (punk905_beta trialled and PROMOTED to punk905 2026-07-21.)
@ -70,6 +69,18 @@ tclsfe-x64.exe {punk9wintk903.vfs punk91 zip}
#5
tclsh905.exe {punk9wintk905.vfs punk905 zip}
#G-103 runtime kit family (suite_tcl90 'kit-family' step): self-contained runtimes whose attached
# zip already carries the tcl library PLUS Thread/tclvfs/tcllib+tcllibc (bi adds Tk+tklib). WORKING
# names per the G-103 naming decision (dotted tcl patchlevel; piperepl-patched runtimes carry
# 'punk'; -r<N> stays on the immutable punkbin artifact tier - 'runtime use' materializes an
# artifact into these names). Copied from src/buildsuites/_build/suite_tcl90/out/family/.
#punk runtime + the full punk vfs (which still carries its own tk/thread/etc payload - duplicated
# batteries resolve to the highest version, i.e. the runtime's; the vfs payload slims as G-103
# progresses):
tclsh9.0.5-punk.exe {punk9wintk905.vfs punk9_beta zip}
#bi runtime already carries Tk - pair with the for-tkruntime vfs (no tk payload in the vfs):
tclsh9.0.5-punk-bi.exe {punk9win_for_tkruntime.vfs punk9bi_beta zip}
#----------------------------------------------
#experiment - what happens when we run against a 'wish' runtime? Will we have stdin stdout problems?
#tksfe-twapi-x64.exe {punk9wintk903.vfs punkwish91 zip}

512
src/scriptapps/bin/punk-runtime.bash

@ -12,64 +12,73 @@ scriptroot="${basename%.*}" #e.g "punk-runtime"
#artifact server base url - overridable for mirrors/testing
url_kitbase="${PUNKBIN_URL:-https://www.gitea1.intx.com.au/jn/punkbin/raw/branch/master}"
runtime_available=0
#$OSTYPE varies in capitalization across for example zsh and bash
#uname probably a more consistent bet
arch=$(uname -m) #machine/architecture
plat=$(uname -s) #platform/system
#even though most of the platform prongs are very similar,
#we keep the code separate so it can be tweaked easily for unexpected differences
#each prong sets archdir (local folder) and, where a runtime is published for the
#platform, rt_default (default runtime name). archtail (server folder) is derived
#from archdir below.
#each prong sets local_platform (the CANONICAL punkshell platform-DIR name - see
#punk::platform / 'help platforms': cpu tokens normalized amd64->x86_64,
#aarch64->arm64). Per-platform default fetch runtimes are NOT baked here - they
#are the server's curated defaults.txt (a punkbin release decision).
narch="$arch"
case "$narch" in
amd64) narch="x86_64";;
aarch64|arm64) narch="arm64";;
esac
if [[ "$plat" = "Linux"* ]]; then
if [[ "$arch" = "x86_64"* ]]; then
archdir="${scriptdir}/runtime/linux-x86_64"
rt_default="tclkit-902-Linux64-intel-dyn"
runtime_available=1
local_platform="linux-x86_64"
elif [[ "$narch" = "arm64" ]]; then
#canonical arm64 (aarch64). punkbin's existing arm kit predates the
#arm64 name and sits in linux-arm (the server's defaults.txt records
#per-platform recommendations).
local_platform="linux-arm64"
elif [[ "$arch" = "arm"* ]]; then
archdir="${scriptdir}/runtime/linux-arm"
rt_default="tclkit-902-Linux64-arm-dyn"
runtime_available=1
#32-bit arm
local_platform="linux-arm"
else
archdir="${scriptdir}/runtime/linux-$arch"
local_platform="linux-$narch"
fi
os="linux"
elif [[ "$plat" = "Darwin"* ]]; then
os="macosx"
#assumed to be Mach-O 'universal binaries' for both x86-64 and arm? - REVIEW
archdir="${scriptdir}/runtime/macosx"
rt_default="tclkit-902-Darwin64-dyn"
runtime_available=1
#universal (multi-arch) binaries - the runtime tier keeps ONE macosx folder
#(per-arch macosx-x86_64/macosx-arm64 names serve the lib tree tier)
local_platform="macosx"
elif [[ "$plat" = "FreeBSD"* ]]; then
archdir="${scriptdir}/runtime/freebsd-amd64"
#canonical names (2026-07-22): freebsd-x86_64 (punkbin's actual folder; this
#payload previously said freebsd-amd64) / freebsd-arm64
local_platform="freebsd-$narch"
os="freebsd"
elif [[ "$plat" == "DragonFly"* ]]; then
archdir="${scriptdir}/runtime/dragonflybsd-$arch"
local_platform="dragonflybsd-$narch"
os="dragonflybsd"
elif [[ "$plat" == "NetBSD"* ]]; then
archdir="${scriptdir}/runtime/netbsd-$arch"
local_platform="netbsd-$narch"
os="netbsd"
elif [[ "$plat" == "OpenBSD"* ]]; then
archdir="${scriptdir}/runtime/openbsd-amd64"
local_platform="openbsd-$narch"
os="openbsd"
elif [[ "$plat" == "MINGW32"* ]]; then
#REVIEW
#32-bit msys shell - a genuine 32-bit machine reports i*86; a 32-bit shell
#on a 64-bit OS is rare enough that the machine arch decides (REVIEW)
os="win32"
archdir="${scriptdir}/runtime/win32-x86_64"
rt_default="tclsh902z.exe"
runtime_available=1
case "$arch" in
i*86) local_platform="win32-ix86";;
*) local_platform="win32-x86_64";;
esac
elif [[ "$plat" == "MINGW64"* ]]; then
#REVIEW
os="win32"
archdir="${scriptdir}/runtime/win32-x86_64"
rt_default="tclsh902z.exe"
runtime_available=1
local_platform="win32-x86_64"
elif [[ "$plat" == "CYGWIN_NT"* ]]; then
os="win32"
archdir="${scriptdir}/runtime/win32-x86_64"
rt_default="tclsh902z.exe"
runtime_available=1
case "$arch" in
i*86) local_platform="win32-ix86";;
*) local_platform="win32-x86_64";;
esac
elif [[ "$plat" == "MSYS_NT"* ]]; then
echo MSYS
os="win32"
@ -80,14 +89,59 @@ elif [[ "$plat" == "MSYS_NT"* ]]; then
#"c:/windows/system32/" is quite likely in the path ahead of msys,git etc.
#This breaks calls to various unix utils such as sed etc (wsl related?)
export PATH="$shellfolder${PATH:+:${PATH}}"
archdir="${scriptdir}/runtime/win32-x86_64"
rt_default="tclsh902z.exe"
runtime_available=1
local_platform="win32-x86_64"
else
archdir="${scriptdir}/runtime/other"
local_platform="other"
os="other"
fi
archtail="${archdir##*/}" #server folder name e.g linux-x86_64
#-- platform resolution (G-105 cross-build staging) ---------------------------
#The prongs above detect the LOCAL platform. fetch/list/use accept
#'-platform <p>' to aim at another punkbin platform-dir (cross-build staging /
#provisioning); resolution: -platform arg > PUNK_RUNTIME_PLATFORM env > local.
#Values are punkbin platform-dir names (e.g win32-x86_64, linux-x86_64, macosx)
#- NOT zig triples (the buildsuite maps triples to platform dirs at build time).
#Validation is shape-only: the server's sha1sums/404 is the truth for what
#exists. 'run' is LOCAL ONLY (foreign binaries are not runnable here) - only a
#LEADING -platform is rejected there; later args belong to the launched runtime.
action="${1:-}"
[[ $# -gt 0 ]] && shift
platform_opt=""
case "$action" in
fetch|list|use|platforms)
newargs=()
while [[ $# -gt 0 ]]; do
if [[ "$1" == "-platform" ]]; then
shift
if [[ $# -eq 0 ]]; then
echo "option -platform requires a value (a punkbin platform-dir name, e.g linux-x86_64)"
exit 1
fi
platform_opt="$1"
else
newargs+=("$1")
fi
shift
done
set -- ${newargs[@]+"${newargs[@]}"}
;;
run)
if [[ "${1:-}" == "-platform" ]]; then
echo "'run' launches the LOCAL platform's active runtime - it takes no -platform option"
echo "(foreign-platform folders are cross-build staging; deploy them to their platform to run)"
exit 1
fi
;;
esac
platform="${platform_opt:-${PUNK_RUNTIME_PLATFORM:-$local_platform}}"
if ! [[ "$platform" =~ ^[a-z0-9][a-z0-9_-]*$ ]]; then
echo "invalid platform name '$platform' (expected a punkbin platform-dir name such as win32-x86_64, linux-x86_64, macosx)"
exit 1
fi
is_local=0
[[ "$platform" == "$local_platform" ]] && is_local=1
archdir="${scriptdir}/runtime/${platform}"
archtail="$platform" #server folder name e.g linux-x86_64
active_file="${archdir}/active.toml"
#sha1 of a file - tool varies by platform (sha1sum: linux/coreutils, shasum: macosx,
@ -118,20 +172,176 @@ set_active() {
printf 'active = "%s"\n' "$1" > "$active_file"
echo "active runtime for $archtail set to: $1 (recorded in $active_file)"
}
#installed runtime candidates - exclude build copies and non-runtime files
#installed runtime candidates - exclude directories, build copies and
#non-runtime files (ps1-payload parity: files only, same extension excludes).
#LC_ALL=C: glob expansion order is collation-dependent - C-locale byte order
#matches the ps1 payload's ordinal sorting, so listings agree everywhere.
list_candidates() {
local f LC_ALL=C
if [[ -d "$archdir" ]]; then
ls -1 "$archdir" | grep -v '_BUILDCOPY$' | grep -v '\.txt$' | grep -v '\.toml$' | grep -v '\.tm$' | grep -v '\.tmp$'
for f in "$archdir"/*; do
[[ -f "$f" ]] || continue
f="${f##*/}"
case "$f" in
*_BUILDCOPY*|*.txt|*.toml|*.tm|*.tmp|*.log) continue;;
esac
printf '%s\n' "$f"
done
fi
}
#name minus a .exe suffix only - dotted tcl patchlevels (tclsh9.0.5-punk) make
#generic last-dot extension stripping wrong for extensionless unix names
rootname_of() {
case "$1" in
*.exe|*.EXE) printf '%s' "${1%.???}";;
*) printf '%s' "$1";;
esac
}
#G-103 artifact metadata: a runtime may carry a <rootname>.toml beside it (emitted
#by the buildsuite kit-family-artifacts step / fetched from punkbin). Prints a
#short "[variant=... tcl=... rN ...]" summary for list output, "" when absent.
metadata_summary() {
local exename="$1" tomlfile parts variant tclpatch revision piperepl artname target
tomlfile="$archdir/$(rootname_of "$exename").toml"
[[ -f "$tomlfile" ]] || return 0
variant=$(sed -n 's/^[[:space:]]*variant[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" | head -n 1)
tclpatch=$(sed -n 's/^[[:space:]]*tcl_patchlevel[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" | head -n 1)
revision=$(sed -n 's/^[[:space:]]*revision[[:space:]]*=[[:space:]]*\([0-9][0-9]*\).*/\1/p' "$tomlfile" | head -n 1)
piperepl=$(sed -n 's/^[[:space:]]*piperepl[[:space:]]*=[[:space:]]*\(true\|false\).*/\1/p' "$tomlfile" | head -n 1)
artname=$(sed -n 's/^[[:space:]]*name[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" | head -n 1)
target=$(sed -n 's/^[[:space:]]*target[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$tomlfile" | head -n 1)
parts=""
[[ -n "$variant" ]] && parts="$parts variant=$variant"
[[ -n "$tclpatch" ]] && parts="$parts tcl=$tclpatch"
[[ -n "$revision" ]] && parts="$parts r$revision"
if [[ "$piperepl" == "true" ]]; then parts="$parts piperepl=on"
elif [[ "$piperepl" == "false" ]]; then parts="$parts piperepl=off"; fi
#a materialized working copy records which immutable artifact it came from
[[ -n "$artname" && "$artname" != "$exename" ]] && parts="$parts from=$artname"
#integrity flag: a runtime filed under a platform folder its metadata says it
#was not built for (cross-platform fetch/staging misfiling)
[[ -n "$target" && "$target" != "$archtail" ]] && parts="$parts !TARGET-MISMATCH:$target"
[[ -n "$parts" ]] && printf '[%s]' "${parts# }"
}
#operator help (the 'help' action; the no-args case shows show_usage only).
#Keep in sync with the ps1 payload's Show-PunkRuntimeHelp/Show-PunkRuntimeUsage.
show_usage() {
echo "Usage: $0 {fetch|list|use|run|platforms|help}"
echo " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin"
echo " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata"
echo " use <name> ?-platform <p>? select active / materialize -r<N> artifact"
echo " run ?args...? launch the active local-platform runtime"
echo " platforms ?-remote? local platform folders / platforms the server serves"
echo " help full help (options, env vars, examples)"
}
show_help() {
echo "punk-runtime - manage the plain Tcl runtimes under bin/runtime/<platform>/"
echo ""
show_usage
echo ""
echo "Actions:"
echo " fetch ?<name>? ?-platform <p>?"
echo " Download a runtime from the punkbin artifact server into"
echo " bin/runtime/<p>/ with sha1 verification against the server's"
echo " sha1sums.txt. -r<N>-named family artifacts also fetch their"
echo " <rootname>.toml metadata record. Omitting <name> fetches the"
echo " platform's recommended default per the server's curated"
echo " defaults.txt (a punkbin release decision; works with -platform"
echo " too) - platforms without a recorded default need an explicit"
echo " <name>."
echo " list ?-remote? ?-platform <p>?"
echo " List installed runtimes with artifact-metadata summaries"
echo " (variant, tcl patchlevel, revision, piperepl policy, source"
echo " artifact of a materialized copy; a !TARGET-MISMATCH flag marks"
echo " a runtime filed under the wrong platform folder). -remote"
echo " compares local runtimes against the server's sha1sums, marks"
echo " the locally ACTIVE runtime (*) and annotates the platform's"
echo " recommended runtime ((server default), per the server's"
echo " defaults.txt when available) - so active-vs-default is"
echo " visible at a glance."
echo " use <name> ?-platform <p>?"
echo " Select the runtime that 'run' launches (records active.toml in"
echo " the platform folder). An immutable -r<N> ARTIFACT name is"
echo " MATERIALIZED into its WORKING name (name minus -r<N> - what"
echo " mapvfs and projects reference), metadata toml copied alongside."
echo " run ?args...?"
echo " Launch the ACTIVE runtime of the LOCAL platform, passing args."
echo " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole"
echo " installed candidate. Takes no -platform (foreign binaries are"
echo " not runnable here)."
echo " platforms ?-remote?"
echo " List local platform folders under bin/runtime/. With -remote,"
echo " list the platforms the artifact server serves - read from the"
echo " server's platforms.txt discovery manifest (part of the punkbin"
echo " layout; third-party mirrors carry the same file), with local"
echo " presence marked. Servers without the manifest get an actionable"
echo " message (name platforms explicitly with -platform)."
echo ""
echo "Platforms: punkbin platform-dir names (e.g win32-x86_64, linux-x86_64,"
echo " macosx) - not zig triples. Default is the local platform; override"
echo " order: -platform argument > PUNK_RUNTIME_PLATFORM env var."
echo " Foreign-platform folders serve cross-build staging and provisioning:"
echo " active.toml travels with the folder when it is deployed to its real"
echo " platform (unix exec bits are restored at deploy time - e.g rsync/tar/"
echo " chmod on the receiving side)."
echo ""
echo "Env: PUNKBIN_URL (artifact server base url), PUNK_RUNTIME_PLATFORM,"
echo " PUNK_ACTIVE_RUNTIME (run-time selection override)"
echo ""
echo "Examples:"
echo " $0 platforms -remote"
echo " $0 fetch tclsh9.0.5-punk-r1.exe"
echo " $0 use tclsh9.0.5-punk-r1.exe (materializes tclsh9.0.5-punk.exe)"
echo " $0 list -remote -platform linux-x86_64"
echo " $0 fetch tclsh9.0.5-punk-r1 -platform linux-x86_64"
echo " $0 use tclsh9.0.5-punk-r1 -platform linux-x86_64"
echo " $0 run myscript.tcl arg1 arg2"
}
#G-103 artifact-tier names carry -r<N> before the (optional) .exe suffix; prints
#the WORKING name (artifact minus -r<N>) when the name is artifact-tier, else ""
working_name_of() {
local w
w=$(printf '%s' "$1" | sed -E 's/^(.*)-r[0-9]+(\.[Ee][Xx][Ee])?$/\1\2/')
if [[ "$w" != "$1" ]]; then printf '%s' "$w"; fi
}
case "$1" in
case "$action" in
"fetch")
runtime="$rt_default"
if [[ -n "$2" ]]; then
runtime="$2"
if [[ "$archtail" == "other" ]]; then
echo "Unrecognised local platform - name one explicitly with -platform"
echo "(canonical names: 'help platforms' in the punk shell)"
exit 1
fi
if [[ ( "$runtime_available" -eq 1 || -n "$2" ) && "$archtail" != "other" && -n "$runtime" ]]; then
runtime="${1:-}"
if [[ -z "$runtime" ]]; then
#no name given: consult the server's curated defaults.txt - the punkbin
#release recommendation (updated there as part of each publication
#change-set; see punkbin AGENTS.md). Per-platform server data, so it
#works for any -platform, not just local.
defaultsurl="${url_kitbase}/defaults.txt"
defaultslocal="${scriptdir}/runtime/defaults.txt"
mkdir -p "${scriptdir}/runtime"
if ! curl -fsSL --output "$defaultslocal" "$defaultsurl"; then
if [[ -f "$defaultslocal" ]]; then
echo "WARNING: could not fetch $defaultsurl - using cached copy at $defaultslocal"
else
echo "Could not fetch $defaultsurl"
echo "No server default available - name a runtime explicitly:"
echo " $0 fetch <runtimename> ?-platform $archtail?"
echo " (see available names: $0 list -remote -platform $archtail)"
exit 1
fi
fi
runtime=$(awk -v p="$archtail" '$1==p {print $2; exit}' "$defaultslocal")
if [[ -z "$runtime" ]]; then
echo "No default recorded for platform '$archtail' in the server's defaults.txt - name a runtime explicitly:"
echo " $0 fetch <runtimename> ?-platform $archtail?"
echo " (see available names: $0 list -remote -platform $archtail)"
exit 1
fi
echo "using server default for $archtail: $runtime"
fi
if [[ -n "$runtime" ]]; then
url="${url_kitbase}/${archtail}/${runtime}"
output="${archdir}/${runtime}"
sha1url="${url_kitbase}/${archtail}/sha1sums.txt"
@ -200,17 +410,25 @@ case "$1" in
exit 1
fi
fi
#G-103: family artifacts (-r<N> names) carry a metadata toml alongside
#on the server - fetch it too; absence is fine (pre-family runtimes)
if [[ -n "$(working_name_of "$runtime")" ]]; then
tomlname="$(rootname_of "$runtime").toml"
if curl -fsSL --output "${archdir}/${tomlname}" "${url_kitbase}/${archtail}/${tomlname}"; then
echo "artifact metadata saved at ${archdir}/${tomlname}"
else
rm -f "${archdir}/${tomlname}"
echo "no artifact metadata toml on server for $runtime (ok for pre-family runtimes)"
fi
fi
#first fetch establishes the active runtime; later fetches never steal it
if [[ -z "$(get_active)" ]]; then
set_active "$runtime"
fi
else
echo "No default runtime currently published for $os ($archtail)"
echo "If one exists on the server, name it explicitly: $0 fetch <runtimename>"
fi
;;
"list")
if [[ "$2" == "-remote" ]]; then
if [[ "${1:-}" == "-remote" ]]; then
#compare local runtimes with those listed on the artifact server
#(functional parity with the powershell payload's 'list -remote')
sha1url="${url_kitbase}/${archtail}/sha1sums.txt"
@ -229,12 +447,43 @@ case "$1" in
echo "No sha1 tool found (tried sha1sum, shasum, sha1, openssl) - cannot compare local runtimes."
exit 1
fi
#server-default + local-active surfacing (G-103): the server's curated
#defaults.txt names the recommended runtime per platform - best-effort
#fetch (cached/absent both fine for a listing), then mark the matching
#row and the locally active one so "is my active the recommended
#default?" is answerable at a glance.
activename=$(get_active)
platform_default=""
defaultslocal="${scriptdir}/runtime/defaults.txt"
curl -fsL --silent --output "$defaultslocal" "${url_kitbase}/defaults.txt" 2>/dev/null || :
if [[ -f "$defaultslocal" ]]; then
platform_default=$(awk -v p="$archtail" '$1==p {print $2; exit}' "$defaultslocal")
fi
echo "-----------------------------------------------------------------------"
echo "Runtimes for $archtail"
echo "Local $archdir"
echo "Remote ${url_kitbase}/${archtail}"
if [[ -n "$platform_default" ]]; then
echo "server default for $archtail: $platform_default"
fi
if [[ -n "$activename" ]]; then
#the default names an immutable -r<N> ARTIFACT while the active is
#typically its materialized WORKING name - the beside-toml's 'name'
#field records which artifact the working copy came from, so match
#on either identity
active_artifact=""
activetoml="$archdir/$(rootname_of "$activename").toml"
if [[ -f "$activetoml" ]]; then
active_artifact=$(sed -n 's/^[[:space:]]*name[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' "$activetoml" | head -n 1)
fi
matchnote=""
if [[ -n "$platform_default" ]] && [[ "$activename" == "$platform_default" || "$active_artifact" == "$platform_default" ]]; then
matchnote=" (= server default)"
fi
echo "active (local): $activename$matchnote"
fi
echo "-----------------------------------------------------------------------"
echo "Local Remote"
echo " Local Remote"
echo "-----------------------------------------------------------------------"
for f in $(list_candidates); do
local_sha1=$(lcase "$(sha1_of "$archdir/$f")")
@ -246,16 +495,31 @@ case "$1" in
else
rhs="UPDATE AVAILABLE"
fi
printf '%-35s %s\n' "$f" "$rhs"
mark=" "
[[ "$f" == "$activename" ]] && mark="* "
annot=""
[[ -n "$platform_default" && "$f" == "$platform_default" ]] && annot=" (server default)"
printf '%s%-35s %s%s\n' "$mark" "$f" "$rhs" "$annot"
done
#remote-only entries
while IFS= read -r line; do
rname=$(printf '%s' "$line" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p')
#remote-only entries, sorted (ps1-payload parity - its dict enumeration
#is explicitly sorted). tr strips CRLF \r: msys grep strips it in text
#mode but bash read does not - without this a locally-present runtime
#also shows as a remote-only row (\r-suffixed name fails the -f test)
tr -d '\r' < "$sha1local" | sed -n 's/^[0-9a-fA-F]\{40\} \*\(.*\)$/\1/p' | LC_ALL=C sort | while IFS= read -r rname; do
#skip support files riding in the server's sha1sums beside the
#runtimes (artifact metadata tomls etc) - same extension set the
#local candidate filter excludes
case "$rname" in
*.txt|*.toml|*.tm|*.tmp|*.log) continue;;
esac
if [[ -n "$rname" && ! -f "$archdir/$rname" ]]; then
printf '%-35s %s\n' "-" "$rname"
annot=""
[[ -n "$platform_default" && "$rname" == "$platform_default" ]] && annot=" (server default)"
printf ' %-35s %s%s\n' "-" "$rname" "$annot"
fi
done < "$sha1local"
done
echo "-----------------------------------------------------------------------"
echo "* = active (local selection)"
else
if [[ -d "$archdir" ]]; then
candidates=$(list_candidates)
@ -263,16 +527,19 @@ case "$1" in
count=$(printf '%s\n' "$candidates" | grep -c . )
echo "$count runtime(s) in $archdir"
for f in $candidates; do
meta=$(metadata_summary "$f")
if [[ "$f" == "$active" ]]; then
echo "* $f (active)"
printf '* %-35s (active) %s\n' "$f" "$meta"
else
echo " $f"
printf ' %-35s %s\n' "$f" "$meta"
fi
done
if [[ -n "$active" && ! -f "$archdir/$active" ]]; then
echo "WARNING: active runtime '$active' (from $active_file) is not present - use '$0 use <name>' to reselect"
fi
echo "Use: '$0 list -remote' to compare local runtimes with those available on the artifact server"
echo "Use: '$0 use <name>' to select the runtime that 'run' launches"
echo "Use: '$0 use <artifact-r<N>-name>' to materialize an immutable -r<N> artifact into its working name and select it"
else
echo "No runtimes available in $archdir"
echo " Use '$0 fetch' to install."
@ -280,9 +547,12 @@ case "$1" in
fi
;;
"use")
if [[ -z "$2" ]]; then
echo "Usage: $0 use <runtimename>"
echo "Installed candidates:"
#with -platform: manage THAT platform folder's selection/materialization
#(cross-build staging - active.toml travels with the folder when deployed;
#'run' on this machine only ever consults the local platform)
if [[ -z "${1:-}" ]]; then
echo "Usage: $0 use <runtimename> ?-platform <p>?"
echo "Installed candidates ($platform):"
for f in $(list_candidates); do
echo " $f"
done
@ -292,25 +562,53 @@ case "$1" in
fi
exit 1
fi
case "$2" in
case "$1" in
*_BUILDCOPY|*.txt|*.toml|*.tm|*.tmp)
echo "'$2' is not a selectable runtime"
echo "'$1' is not a selectable runtime"
exit 1
;;
esac
if [[ ! -f "$archdir/$2" ]]; then
echo "No runtime named '$2' found in $archdir"
if [[ ! -f "$archdir/$1" ]]; then
echo "No runtime named '$1' found in $archdir"
echo "Installed candidates:"
for f in $(list_candidates); do
echo " $f"
done
exit 1
fi
set_active "$2"
#G-103 artifact-tier names (-r<N>, immutable): 'use' MATERIALIZES the
#artifact into its WORKING name (name minus -r<N> - what mapvfs and
#projects reference), copies its metadata toml alongside, and selects
#the working name. Republishing artifacts never churns consumers.
#(chmod is a no-op when staging unix runtimes from windows filesystems -
#exec bits are restored at deploy time, e.g rsync/tar/chmod on the guest.)
working=$(working_name_of "$1")
if [[ -n "$working" ]]; then
cp -f "$archdir/$1" "$archdir/$working"
chmod +x "$archdir/$working"
srctoml="$archdir/$(rootname_of "$1").toml"
desttoml="$archdir/$(rootname_of "$working").toml"
if [[ -f "$srctoml" ]]; then
cp -f "$srctoml" "$desttoml"
echo "materialized $working from artifact $1 (metadata toml copied alongside)"
else
echo "materialized $working from artifact $1 (no metadata toml found beside the artifact)"
fi
set_active "$working"
else
set_active "$1"
fi
;;
"run")
#resolution order: PUNK_ACTIVE_RUNTIME env override, active.toml, single
#installed candidate - otherwise error with candidates (no last-in-list guessing)
#installed candidate - otherwise error with candidates (no last-in-list guessing).
#LOCAL platform only: the -platform arg is rejected in the scan above, and a
#PUNK_RUNTIME_PLATFORM env override is likewise ignored here (foreign
#binaries are not runnable; the env override serves list/fetch/use).
if [[ "$is_local" -ne 1 ]]; then
archdir="${scriptdir}/runtime/${local_platform}"
active_file="${archdir}/active.toml"
fi
activeruntime=""
if [[ -n "$PUNK_ACTIVE_RUNTIME" ]]; then
activeruntime="$PUNK_ACTIVE_RUNTIME"
@ -347,13 +645,93 @@ case "$1" in
fi
activeruntime_fullpath="$archdir/$activeruntime"
#echo "using $activeruntime_fullpath"
shift
#echo "args: $@"
#(the action was already shifted off during the option scan - "$@" is
#exactly the runtime's argument list)
$activeruntime_fullpath "$@"
;;
"platforms")
#enumerate platform folders: local (bin/runtime/*) and, with -remote, the
#server's platforms.txt discovery manifest (raw-file servers have no
#directory listing - the manifest is part of the punkbin layout contract;
#third-party mirrors carry the same file). -platform is ignored here
#(this action enumerates ALL platforms).
rtroot="${scriptdir}/runtime"
localdirs=""
if [[ -d "$rtroot" ]]; then
localdirs=$(LC_ALL=C ls -1 "$rtroot" 2>/dev/null | while read -r d; do [[ -d "$rtroot/$d" ]] && echo "$d"; done)
fi
if [[ "${1:-}" == "-remote" ]]; then
manifesturl="${url_kitbase}/platforms.txt"
manifestlocal="${rtroot}/platforms.txt"
mkdir -p "$rtroot"
if curl -fsSL --output "$manifestlocal" "$manifesturl"; then
echo "Fetched $manifesturl"
elif [[ -f "$manifestlocal" ]]; then
echo "WARNING: could not fetch $manifesturl - using cached copy at $manifestlocal"
else
echo "No platforms.txt available from $manifesturl"
echo "(pre-convention punkbin or third-party mirror without the discovery manifest)"
echo "Name platforms explicitly with -platform; canonical names: 'help platforms' in the punk shell"
exit 1
fi
echo "-----------------------------------------------------------------------"
echo "Platforms served by ${url_kitbase}"
echo "-----------------------------------------------------------------------"
remoteplatforms=""
while IFS= read -r line; do
line="${line#"${line%%[![:space:]]*}"}"
[[ -z "$line" || "${line:0:1}" == "#" ]] && continue
remoteplatforms="$remoteplatforms $line"
marks=""
[[ "$line" == "$local_platform" ]] && marks="local platform"
if printf '%s\n' $localdirs | grep -qx "$line"; then
[[ -n "$marks" ]] && marks="$marks, "
marks="${marks}local dir present"
fi
if [[ -n "$marks" ]]; then
printf ' %-25s (%s)\n' "$line" "$marks"
else
printf ' %-25s\n' "$line"
fi
done < "$manifestlocal"
for d in $localdirs; do
if ! printf '%s\n' $remoteplatforms | grep -qx "$d"; then
printf ' %-25s (local dir only - not served remotely)\n' "$d"
fi
done
echo "-----------------------------------------------------------------------"
echo "Use: '$0 list -remote -platform <name>' to see a platform's runtimes"
else
echo "-----------------------------------------------------------------------"
echo "Local platform folders under $rtroot"
echo "-----------------------------------------------------------------------"
if [[ -z "$localdirs" ]]; then
echo " (none - 'fetch' creates the local platform's folder)"
fi
for d in $localdirs; do
if [[ "$d" == "$local_platform" ]]; then
printf '* %-25s (local platform)\n' "$d"
else
printf ' %-25s\n' "$d"
fi
done
echo "-----------------------------------------------------------------------"
echo "Use: '$0 platforms -remote' to see platforms served by the artifact server"
echo "Canonical platform names: 'help platforms' in the punk shell"
fi
;;
"help")
show_help
;;
"")
#no action: short usage + pointer at the fuller help
show_usage
echo ""
echo "'$0 help' gives options, env vars and examples."
;;
*)
echo "Usage: $0 {fetch|list|use|run}"
echo "received $@"
echo "unknown action '$action'"
show_usage
exit 1
;;
esac

500
src/scriptapps/bin/punk-runtime.ps1

@ -62,13 +62,182 @@ function Set-PunkActiveRuntime {
[System.IO.File]::WriteAllText($activefile, "active = `"$name`"`n", [System.Text.UTF8Encoding]::new($false))
Write-Host "active runtime set to: $name (recorded in $activefile)"
}
#installed runtime candidates - exclude build copies and non-runtime files
#installed runtime candidates - exclude build copies and non-runtime files.
#ORDINAL name order: culture-sensitive Sort-Object collates differently between
#windows powershell (NLS) and pwsh (ICU) - both launch paths exist (the .ps1
#twin runs under the invoking shell) - and ordinal matches the bash payload's
#LC_ALL=C ordering, so listings agree byte-for-byte everywhere.
function Get-PunkRuntimeCandidates {
param([string] $archfolder)
if (-not (Test-Path -Path $archfolder -PathType Container)) {
return @()
}
return @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp") -contains $_.Extension) } | Sort-Object Name)
$files = @(get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY*' | Where-object {-not ($(".txt",".toml",".tm",".tmp",".log") -contains $_.Extension) })
if ($files.Count -le 1) {
return $files
}
$byname = @{}
foreach ($f in $files) { $byname[$f.Name] = $f }
$names = @($byname.Keys)
[array]::Sort($names, [System.StringComparer]::Ordinal)
$out = @()
foreach ($n in $names) { $out += $byname[$n] }
return $out
}
#operator help (the 'help' action; the no-args case shows the Usage block only).
#Keep in sync with the bash payload's show_help/show_usage.
function Show-PunkRuntimeUsage {
write-host "Usage: punk-runtime.cmd {fetch|list|use|run|platforms|help}"
write-host " fetch ?<name>? ?-platform <p>? download+verify a runtime from punkbin"
write-host " list ?-remote? ?-platform <p>? installed (or server) runtimes + metadata"
write-host " use <name> ?-platform <p>? select active / materialize -r<N> artifact"
write-host " run ?args...? launch the active local-platform runtime"
write-host " platforms ?-remote? local platform folders / platforms the server serves"
write-host " help full help (options, env vars, examples)"
}
function Show-PunkRuntimeHelp {
write-host "punk-runtime - manage the plain Tcl runtimes under bin/runtime/<platform>/"
write-host ""
Show-PunkRuntimeUsage
write-host ""
write-host "Actions:"
write-host " fetch ?<name>? ?-platform <p>?"
write-host " Download a runtime from the punkbin artifact server into"
write-host " bin/runtime/<p>/ with sha1 verification against the server's"
write-host " sha1sums.txt. -r<N>-named family artifacts also fetch their"
write-host " <rootname>.toml metadata record. Omitting <name> fetches the"
write-host " platform's recommended default per the server's curated"
write-host " defaults.txt (a punkbin release decision; works with -platform"
write-host " too) - platforms without a recorded default need an explicit"
write-host " <name>."
write-host " list ?-remote? ?-platform <p>?"
write-host " List installed runtimes with artifact-metadata summaries"
write-host " (variant, tcl patchlevel, revision, piperepl policy, source"
write-host " artifact of a materialized copy; a !TARGET-MISMATCH flag marks"
write-host " a runtime filed under the wrong platform folder). -remote"
write-host " compares local runtimes against the server's sha1sums, marks"
write-host " the locally ACTIVE runtime (*) and annotates the platform's"
write-host " recommended runtime ((server default), per the server's"
write-host " defaults.txt when available) - so active-vs-default is"
write-host " visible at a glance."
write-host " use <name> ?-platform <p>?"
write-host " Select the runtime that 'run' launches (records active.toml in"
write-host " the platform folder). An immutable -r<N> ARTIFACT name is"
write-host " MATERIALIZED into its WORKING name (name minus -r<N> - what"
write-host " mapvfs and projects reference), metadata toml copied alongside."
write-host " run ?args...?"
write-host " Launch the ACTIVE runtime of the LOCAL platform, passing args."
write-host " Resolution: PUNK_ACTIVE_RUNTIME env > active.toml > sole"
write-host " installed candidate. Takes no -platform (foreign binaries are"
write-host " not runnable here)."
write-host " platforms ?-remote?"
write-host " List local platform folders under bin/runtime/. With -remote,"
write-host " list the platforms the artifact server serves - read from the"
write-host " server's platforms.txt discovery manifest (part of the punkbin"
write-host " layout; third-party mirrors carry the same file), with local"
write-host " presence marked. Servers without the manifest get an actionable"
write-host " message (name platforms explicitly with -platform)."
write-host ""
write-host "Platforms: punkbin platform-dir names (e.g win32-x86_64, linux-x86_64,"
write-host " macosx) - not zig triples. Default is the local platform; override"
write-host " order: -platform argument > PUNK_RUNTIME_PLATFORM env var."
write-host " Foreign-platform folders serve cross-build staging and provisioning:"
write-host " active.toml travels with the folder when it is deployed to its real"
write-host " platform (unix exec bits are restored at deploy time - e.g rsync/tar/"
write-host " chmod on the receiving side)."
write-host ""
write-host "Env: PUNKBIN_URL (artifact server base url), PUNK_RUNTIME_PLATFORM,"
write-host " PUNK_ACTIVE_RUNTIME (run-time selection override)"
write-host ""
write-host "Examples:"
write-host " punk-runtime.cmd platforms -remote"
write-host " punk-runtime.cmd fetch tclsh9.0.5-punk-r1.exe"
write-host " punk-runtime.cmd use tclsh9.0.5-punk-r1.exe (materializes tclsh9.0.5-punk.exe)"
write-host " punk-runtime.cmd list -remote -platform linux-x86_64"
write-host " punk-runtime.cmd fetch tclsh9.0.5-punk-r1 -platform linux-x86_64"
write-host " punk-runtime.cmd use tclsh9.0.5-punk-r1 -platform linux-x86_64"
write-host " punk-runtime.cmd run myscript.tcl arg1 arg2"
}
#platform (target) resolution for fetch/list/use: -platform arg > PUNK_RUNTIME_PLATFORM
#env > the local default. Values are punkbin/bin-runtime platform-DIR names (e.g
#win32-x86_64, linux-x86_64, macosx) - NOT zig triples (the buildsuite maps triples to
#platform dirs at build time; punk-runtime speaks only the punkbin tier). Validation is
#deliberately shape-only: the server's sha1sums/404 is the truth for what exists.
#This payload runs on windows - the local default is win32-x86_64, or win32-ix86 on
#a GENUINE 32-bit windows host (PROCESSOR_ARCHITECTURE x86 with no
#PROCESSOR_ARCHITEW6432 - a 32-bit shell on a 64-bit OS keeps the x86_64 default:
#the runtime store serves what the OS can run). A windows-arm default becomes a
#question when punkbin carries such a folder.
$script:PunkLocalPlatform = "win32-x86_64"
if ($env:PROCESSOR_ARCHITECTURE -eq 'x86' -and -not $env:PROCESSOR_ARCHITEW6432) {
$script:PunkLocalPlatform = "win32-ix86"
}
function Resolve-PunkRuntimePlatform {
param([string] $requested)
$plat = $script:PunkLocalPlatform
if ($requested) {
$plat = $requested
} elseif ($env:PUNK_RUNTIME_PLATFORM) {
$plat = $env:PUNK_RUNTIME_PLATFORM
}
#-cnotmatch: platform-dir names are lowercase and the artifact-server URL path
#is case-sensitive (default -notmatch is case-insensitive and would pass typos
#like Win32-X86_64 through to a server 404); bash payload parity (=~ is
#case-sensitive there)
if ($plat -cnotmatch '^[a-z0-9][a-z0-9_-]*$') {
write-host "invalid platform name '$plat' (expected a punkbin platform-dir name such as win32-x86_64, linux-x86_64, macosx)"
exit 1
}
return $plat
}
#name minus a .exe suffix only - dotted tcl patchlevels (tclsh9.0.5-punk) make
#generic last-dot extension stripping wrong for extensionless unix names
function Get-PunkRuntimeRootName {
param([string] $name)
if ($name -match '(?i)\.exe$') {
return $name.Substring(0, $name.Length - 4)
}
return $name
}
#G-103 artifact metadata: a runtime may carry a <rootname>.toml beside it (emitted
#by the buildsuite kit-family-artifacts step / fetched from punkbin). Returns a
#short "[variant=... tcl=... rN ...]" summary for list output, "" when absent.
function Get-PunkRuntimeMetadataSummary {
param([string] $archfolder, [string] $exename, [string] $expectedplatform = "")
$tomlfile = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $exename) + ".toml")
if (-not (Test-Path -Path $tomlfile -PathType Leaf)) {
return ""
}
$fields = @{}
foreach ($line in (Get-Content -Path $tomlfile)) {
$m = [regex]::Match($line, '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"(.*)"\s*$')
if ($m.Success) {
$fields[$m.Groups[1].Value] = $m.Groups[2].Value
continue
}
$m = [regex]::Match($line, '^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([0-9]+|true|false)\s*$')
if ($m.Success) {
$fields[$m.Groups[1].Value] = $m.Groups[2].Value
}
}
$parts = @()
if ($fields.ContainsKey('variant')) { $parts += "variant=$($fields['variant'])" }
if ($fields.ContainsKey('tcl_patchlevel')) { $parts += "tcl=$($fields['tcl_patchlevel'])" }
if ($fields.ContainsKey('revision')) { $parts += "r$($fields['revision'])" }
if ($fields.ContainsKey('piperepl')) {
if ($fields['piperepl'] -eq 'true') { $parts += "piperepl=on" } else { $parts += "piperepl=off" }
}
#a materialized working copy records which immutable artifact it came from
if ($fields.ContainsKey('name') -and $fields['name'] -ne $exename) { $parts += "from=$($fields['name'])" }
#integrity flag: a runtime filed under a platform folder its metadata says it
#was not built for (cross-platform fetch/staging misfiling)
if ($expectedplatform -ne "" -and $fields.ContainsKey('target') -and $fields['target'] -ne $expectedplatform) {
$parts += "!TARGET-MISMATCH:$($fields['target'])"
}
if ($parts.Count -eq 0) {
return ""
}
return "[" + ($parts -join " ") + "]"
}
function psmain {
@ -78,7 +247,9 @@ function psmain {
[Parameter(Mandatory=$false, Position = 0)][string] $action = ""
)
dynamicparam {
if ($action -eq 'list') {
if ($action -eq 'list' -or $action -eq 'platforms') {
#shared dynamic params: -remote for both; -platform is meaningful to
#'list' and ignored by 'platforms' (which enumerates ALL platforms)
$parameterAttribute = [System.Management.Automation.ParameterAttribute]@{
ParameterSetName = "listruntime"
Mandatory = $false
@ -88,8 +259,19 @@ function psmain {
$dynParam1 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'remote', [switch], $attributeCollection
)
#-platform (G-105 cross-build staging): punkbin platform-dir name to list for
$platformAttribute = [System.Management.Automation.ParameterAttribute]@{
ParameterSetName = "listruntime"
Mandatory = $false
}
$platformAttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$platformAttributeCollection.Add($platformAttribute)
$dynParam2 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'platform', [string], $platformAttributeCollection
)
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$paramDictionary.Add('remote', $dynParam1)
$paramDictionary.Add('platform', $dynParam2)
return $paramDictionary
} elseif ($action -eq 'fetch' -or $action -eq 'use') {
#GetDynamicParamDictionary ParameterDefinitions
@ -105,8 +287,21 @@ function psmain {
'runtime', [string], $attributeCollection
)
#-platform (G-105 cross-build staging): punkbin platform-dir name to fetch
#into / select within (default: the local platform)
$platformAttribute = [System.Management.Automation.ParameterAttribute]@{
ParameterSetName = "fetchruntime"
Mandatory = $false
}
$platformAttributeCollection = [System.Collections.ObjectModel.Collection[System.Attribute]]::new()
$platformAttributeCollection.Add($platformAttribute)
$dynParam2 = [System.Management.Automation.RuntimeDefinedParameter]::new(
'platform', [string], $platformAttributeCollection
)
$paramDictionary = [System.Management.Automation.RuntimeDefinedParameterDictionary]::new()
$paramDictionary.Add('runtime', $dynParam1)
$paramDictionary.Add('platform', $dynParam2)
return $paramDictionary
} elseif ($action -eq 'run') {
#GetDynamicParamDictionary ParameterDefinitions
@ -151,7 +346,7 @@ function psmain {
'action' {
write-host "got action " $PSBoundParameters.action
Set-Variable -Name $_ -Value $PSBoundParameters."$_"
$known_actions = @("fetch", "list", "use", "run")
$known_actions = @("fetch", "list", "use", "run", "platforms", "help")
if (-not($known_actions -contains $action)) {
write-host "action '$action' not understood. Known_actions: $known_actions"
exit 1
@ -183,16 +378,56 @@ function psmain {
}
switch ($action) {
'fetch' {
$arch = "win32-x86_64"
$archfolder = Join-Path -Path $rtfolder -ChildPath "$arch"
$arch = Resolve-PunkRuntimePlatform $PSBoundParameters["platform"]
$archfolder = Join-Path -Path $rtfolder -ChildPath "$arch"
$archurl = "$artifacturl/$arch"
$sha1url = "$archurl/sha1sums.txt"
$runtime = "tclsh902z.exe"
foreach ($boundparam in $PSBoundParameters.Keys) {
write-host "fetchopt: $boundparam $($PSBoundParameters[$boundparam])"
}
$runtime = ""
if ( $PSBoundParameters["runtime"].Length ) {
$runtime = $PSBoundParameters["runtime"]
} else {
#no name given: consult the server's curated defaults.txt - the
#punkbin release recommendation (updated there as part of each
#publication change-set; see punkbin AGENTS.md). Per-platform
#server data, so it works for any -platform, not just local.
$defaultsurl = "$artifacturl/defaults.txt"
$defaultslocal = Join-Path -Path $rtfolder -ChildPath "defaults.txt"
if (-not (Test-Path -Path $rtfolder -PathType Container)) {
new-item -Path $rtfolder -ItemType Directory -force | out-null
}
try {
Invoke-WebRequest -Uri $defaultsurl -OutFile $defaultslocal -ErrorAction Stop
} catch {
if (Test-Path -Path $defaultslocal -PathType Leaf) {
Write-Host "WARNING: could not fetch ${defaultsurl}: $($_.Exception.Message)"
Write-Host "WARNING: using cached copy at $defaultslocal"
} else {
Write-Host "Could not fetch ${defaultsurl}: $($_.Exception.Message)"
Write-Host "No server default available - name a runtime explicitly:"
Write-Host " punk-runtime.cmd fetch <runtimename> ?-platform ${arch}?"
Write-Host " (see available names: punk-runtime.cmd list -remote -platform $arch)"
exit 1
}
}
foreach ($line in (Get-Content -Path $defaultslocal)) {
$line = $line.Trim()
if ($line -eq "" -or $line.StartsWith("#")) { continue }
$parts = -split $line
if ($parts.Count -ge 2 -and $parts[0] -eq $arch) {
$runtime = $parts[1]
break
}
}
if ($runtime -eq "") {
Write-Host "No default recorded for platform '$arch' in the server's defaults.txt - name a runtime explicitly:"
Write-Host " punk-runtime.cmd fetch <runtimename> ?-platform ${arch}?"
Write-Host " (see available names: punk-runtime.cmd list -remote -platform $arch)"
exit 1
}
Write-Host "using server default for ${arch}: $runtime"
}
$fileurl = "$archurl/$runtime"
@ -276,6 +511,19 @@ function psmain {
Write-Host "Local copy of runtime at $output seems to match sha1 checksum of file on server."
Write-Host "No download required"
}
#G-103: family artifacts (-r<N> names) carry a metadata toml alongside
#on the server - fetch it too; absence is fine (pre-family runtimes)
if ($runtime -match '^(.*)-r([0-9]+)(\.[Ee][Xx][Ee])?$') {
$tomlname = (Get-PunkRuntimeRootName $runtime) + ".toml"
$tomlurl = "$archurl/$tomlname"
$tomllocal = Join-Path -Path $archfolder -ChildPath $tomlname
try {
Invoke-WebRequest -Uri $tomlurl -OutFile $tomllocal -ErrorAction Stop
Write-Host "artifact metadata saved at $tomllocal"
} catch {
Write-Host "no artifact metadata toml on server for $runtime (ok for pre-family runtimes)"
}
}
#first fetch establishes the active runtime; later fetches never steal it
if ((Get-PunkActiveRuntime $archfolder) -eq "") {
Set-PunkActiveRuntime $archfolder $runtime
@ -291,8 +539,11 @@ function psmain {
}
}
'use' {
#select the active runtime for subsequent 'run' calls
$arch = "win32-x86_64"
#select the active runtime for subsequent 'run' calls. With -platform:
#manage THAT platform folder's selection/materialization (cross-build
#staging - the active.toml travels with the folder when deployed; 'run'
#on this machine only ever consults the local platform).
$arch = Resolve-PunkRuntimePlatform $PSBoundParameters["platform"]
$archfolder = Join-Path -Path $rtfolder -ChildPath "$arch"
$candidates = Get-PunkRuntimeCandidates $archfolder
$rtname = ""
@ -300,8 +551,8 @@ function psmain {
$rtname = $PSBoundParameters["runtime"]
}
if ($rtname -eq "") {
write-host "Usage: punk-runtime.cmd use <runtimename>"
write-host "Installed candidates:"
write-host "Usage: punk-runtime.cmd use <runtimename> ?-platform <p>?"
write-host "Installed candidates ($arch):"
foreach ($f in $candidates) {
write-host " $($f.Name)"
}
@ -319,13 +570,42 @@ function psmain {
}
exit 1
}
Set-PunkActiveRuntime $archfolder $rtname
#G-103 artifact-tier names (-r<N>, immutable): 'use' MATERIALIZES the
#artifact into its WORKING name (name minus -r<N> - what mapvfs and
#projects reference), copies its metadata toml alongside, and selects
#the working name. Republishing artifacts never churns consumers.
$am = [regex]::Match($rtname, '^(.*)-r([0-9]+)(\.[Ee][Xx][Ee])?$')
if ($am.Success) {
$working = $am.Groups[1].Value + $am.Groups[3].Value
$srcexe = Join-Path -Path $archfolder -ChildPath $rtname
$destexe = Join-Path -Path $archfolder -ChildPath $working
Copy-Item -Path $srcexe -Destination $destexe -Force
$srctoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $rtname) + ".toml")
$desttoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $working) + ".toml")
if (Test-Path -Path $srctoml -PathType Leaf) {
Copy-Item -Path $srctoml -Destination $desttoml -Force
write-host "materialized $working from artifact $rtname (metadata toml copied alongside)"
} else {
write-host "materialized $working from artifact $rtname (no metadata toml found beside the artifact)"
}
Set-PunkActiveRuntime $archfolder $working
} else {
Set-PunkActiveRuntime $archfolder $rtname
}
}
'run' {
#launch the active runtime, passing arguments.
#resolution order: PUNK_ACTIVE_RUNTIME env override, active.toml, single
#installed candidate - otherwise error with candidates (no last-in-list guessing)
$arch = "win32-x86_64"
#installed candidate - otherwise error with candidates (no last-in-list guessing).
#LOCAL PLATFORM ONLY - a foreign platform's binaries are not runnable here.
#Only a LEADING -platform is rejected: everything after 'run' belongs to
#the runtime, so a later arg spelled -platform must pass through untouched.
if ($PSBoundParameters.opts.Length -gt 0 -and $PSBoundParameters.opts[0] -eq '-platform') {
write-host "'run' launches the LOCAL platform's active runtime - it takes no -platform option"
write-host "(foreign-platform folders are cross-build staging; deploy them to their platform to run)"
exit 1
}
$arch = $script:PunkLocalPlatform
$archfolder = Join-Path -Path $rtfolder -ChildPath "$arch"
if (-not(Test-Path -Path $archfolder -PathType Container)) {
write-host "No runtimes seem to be installed for $arch`nPlease use 'punk-runtime.cmd fetch' to install"
@ -401,9 +681,9 @@ function psmain {
}
}
'list' {
#todo - option to list for other os-arch
$arch = 'win32-x86_64'
$archfolder = Join-Path -Path $rtfolder -ChildPath "$arch"
#-platform lists another platform's folder/server dir (cross-build staging)
$arch = Resolve-PunkRuntimePlatform $PSBoundParameters["platform"]
$archfolder = Join-Path -Path $rtfolder -ChildPath "$arch"
$sha1local = join-path -Path $archfolder -ChildPath "sha1sums.txt"
$archurl = "$artifacturl/$arch"
$sha1url = "$archurl/sha1sums.txt"
@ -444,26 +724,81 @@ function psmain {
$localdict = @{}
if (test-path -Path $archfolder -Type Container) {
$dircontents = (get-childItem -Path $archfolder -File | Where-object Name -Notlike '*_BUILDCOPY.*' | Where-object {-not ($(".txt",".tm") -contains $_.Extension) })
#shared candidate filter (bash payload parity - its -remote loop
#already uses list_candidates): excludes directories, build
#copies and .txt/.toml/.tm/.tmp/.log support files, so
#active.toml / metadata tomls / stray logs never show as
#local runtimes in the comparison
$dircontents = Get-PunkRuntimeCandidates $archfolder
foreach ($f in $dircontents) {
$local_sha1 = Get-FileHash -Path $(${f}.FullName) -Algorithm SHA1
$localdict[$f.Name] = ${local_sha1}.Hash
}
}
#server-default + local-active surfacing (G-103): the server's
#curated defaults.txt names the recommended runtime per platform -
#best-effort fetch (cached/absent both fine for a listing), then
#mark the matching row and the locally active one so "is my
#active the recommended default?" is answerable at a glance.
$activename = Get-PunkActiveRuntime $archfolder
$platform_default = ""
$defaultslocal = Join-Path -Path $rtfolder -ChildPath "defaults.txt"
try {
Invoke-WebRequest -Uri "$artifacturl/defaults.txt" -OutFile $defaultslocal -ErrorAction Stop
} catch { }
if (Test-Path -Path $defaultslocal -PathType Leaf) {
foreach ($line in (Get-Content -Path $defaultslocal)) {
$line = $line.Trim()
if ($line -eq "" -or $line.StartsWith("#")) { continue }
$parts = -split $line
if ($parts.Count -ge 2 -and $parts[0] -eq $arch) {
$platform_default = $parts[1]
break
}
}
}
Write-host "-----------------------------------------------------------------------"
Write-host "Runtimes for $arch"
Write-host "Local $archfolder"
Write-host "Remote $archurl"
if ($platform_default -ne "") {
Write-host "server default for ${arch}: $platform_default"
}
if ($activename -ne "") {
#the default names an immutable -r<N> ARTIFACT while the active
#is typically its materialized WORKING name - the beside-toml's
#'name' field records which artifact the working copy came from,
#so match on either identity
$active_artifact = ""
$activetoml = Join-Path -Path $archfolder -ChildPath ((Get-PunkRuntimeRootName $activename) + ".toml")
if (Test-Path -Path $activetoml -PathType Leaf) {
foreach ($tline in (Get-Content -Path $activetoml)) {
$m = [regex]::Match($tline, '^\s*name\s*=\s*"(.*)"\s*$')
if ($m.Success) { $active_artifact = $m.Groups[1].Value; break }
}
}
$matchnote = ""
if ($platform_default -ne "" -and ($activename -eq $platform_default -or $active_artifact -eq $platform_default)) {
$matchnote = " (= server default)"
}
Write-host "active (local): $activename$matchnote"
}
Write-host "-----------------------------------------------------------------------"
Write-host "Local Remote"
Write-host " Local Remote"
Write-host "-----------------------------------------------------------------------"
# 12345678910234567892023456789302345
# 12345678910234567892023456789302345
$G = "`e[32m" #Green
$Y = "`e[33m" #Yellow
$R = "`e[31m" #Red
$RST = "`e[m"
foreach ($key in $localdict.Keys) {
#explicit ORDINAL sort: Hashtable key enumeration order is
#undefined (and differs between the powershell editions), and
#culture-sensitive Sort-Object collates differently too (NLS vs
#ICU) - ordinal agrees across editions and with bash LC_ALL=C
$localkeys = @($localdict.Keys)
[array]::Sort($localkeys, [System.StringComparer]::Ordinal)
foreach ($key in $localkeys) {
$local_sha1 = $($localdict[$key])
if ($remotedict.ContainsKey($key)) {
if ($local_sha1 -eq $remotedict[$key]) {
@ -480,18 +815,32 @@ function psmain {
#ansi problems from cmd.exe not in windows terminal - review
$C = ""
$RST = ""
$mark = " "
if ($key -eq $activename) { $mark = "* " }
$annot = ""
if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" }
$lhs = "$key".PadRight(35, ' ')
write-host -nonewline "${C}${lhs}${RST}"
write-host $rhs
write-host -nonewline "${mark}${C}${lhs}${RST}"
write-host "$rhs$annot"
}
$lhs_missing = "-".PadRight(35, ' ')
foreach ($key in $remotedict.Keys) {
$remotekeys = @($remotedict.Keys)
[array]::Sort($remotekeys, [System.StringComparer]::Ordinal)
foreach ($key in $remotekeys) {
if (-not ($localdict.ContainsKey($key))) {
write-host -nonewline $lhs_missing
write-host $key
#skip support files riding in the server's sha1sums beside
#the runtimes (artifact metadata tomls etc) - same extension
#set the local candidate filter excludes
$rext = [System.IO.Path]::GetExtension($key)
if ($(".txt",".toml",".tm",".tmp",".log") -contains $rext) { continue }
$annot = ""
if ($platform_default -ne "" -and $key -eq $platform_default) { $annot = " (server default)" }
write-host -nonewline " $lhs_missing"
write-host "$key$annot"
}
}
Write-host "-----------------------------------------------------------------------"
Write-host "* = active (local selection)"
} else {
if (test-path -Path $archfolder -Type Container) {
@ -502,10 +851,12 @@ function psmain {
write-host "$(${dircontents}.count) runtime(s) in $archfolder"
Write-host "-----------------------------------------------------------------------"
foreach ($f in $dircontents) {
$meta = Get-PunkRuntimeMetadataSummary $archfolder $f.Name $arch
$lhs = "$($f.Name)".PadRight(35, ' ')
if ($f.Name -eq $activename) {
write-host "* $($f.Name) (active)"
write-host "* $lhs (active) $meta"
} else {
write-host " $($f.Name)"
write-host " $lhs $meta"
}
}
Write-host "-----------------------------------------------------------------------"
@ -514,19 +865,100 @@ function psmain {
}
Write-host "Use: 'list -remote' to compare local runtimes with those available on the artifact server"
Write-host "Use: 'use <name>' to select the runtime that 'run' launches"
Write-host "Use: 'use <artifact-r<N>-name>' to materialize an immutable -r<N> artifact into its working name and select it"
} else {
write-host "No runtimes seem to be installed for $arch in $archfolder`nPlease use 'punk-runtime.cmd fetch' to install."
write-host "Use 'punk-runtime.cmd list -remote' to see available runtimes for $arch"
}
}
}
default {
$actions = @("fetch", "list", "use", "run")
write-host "Available actions: $actions"
write-host "received"
foreach ($boundparam in $PSBoundParameters.opts) {
write-host $boundparam
'platforms' {
#enumerate platform folders: local (bin/runtime/*) and, with
#-remote, the server's platforms.txt discovery manifest (raw-file
#servers have no directory listing - the manifest is part of the
#punkbin layout contract; third-party mirrors carry the same file)
$localdirs = @()
if (Test-Path -Path $rtfolder -PathType Container) {
$localdirs = @(Get-ChildItem -Path $rtfolder -Directory | Select-Object -ExpandProperty Name)
[array]::Sort($localdirs, [System.StringComparer]::Ordinal)
}
if ( $PSBoundParameters.ContainsKey('remote') ) {
$manifesturl = "$artifacturl/platforms.txt"
$manifestlocal = Join-Path -Path $rtfolder -ChildPath "platforms.txt"
if (-not (Test-Path -Path $rtfolder -PathType Container)) {
new-item -Path $rtfolder -ItemType Directory -force | out-null
}
try {
Invoke-WebRequest -Uri $manifesturl -OutFile $manifestlocal -ErrorAction Stop
Write-Host "Fetched $manifesturl"
} catch {
if (Test-Path -Path $manifestlocal -PathType Leaf) {
Write-Host "WARNING: could not fetch ${manifesturl}: $($_.Exception.Message)"
Write-Host "WARNING: using cached copy at $manifestlocal"
} else {
Write-Host "No platforms.txt available from ${manifesturl}: $($_.Exception.Message)"
Write-Host "(pre-convention punkbin or third-party mirror without the discovery manifest)"
Write-Host "Name platforms explicitly with -platform; canonical names: 'help platforms' in the punk shell"
exit 1
}
}
$remoteplatforms = @()
foreach ($line in (Get-Content -Path $manifestlocal)) {
$line = $line.Trim()
if ($line -eq "" -or $line.StartsWith("#")) { continue }
$remoteplatforms += $line
}
Write-host "-----------------------------------------------------------------------"
Write-Host "Platforms served by $artifacturl"
Write-host "-----------------------------------------------------------------------"
foreach ($p in $remoteplatforms) {
$marks = @()
if ($p -eq $script:PunkLocalPlatform) { $marks += "local platform" }
if ($localdirs -contains $p) { $marks += "local dir present" }
$lhs = "$p".PadRight(25, ' ')
if ($marks.Count -gt 0) {
write-host " $lhs ($($marks -join ', '))"
} else {
write-host " $lhs"
}
}
foreach ($d in $localdirs) {
if (-not ($remoteplatforms -contains $d)) {
$lhs = "$d".PadRight(25, ' ')
write-host " $lhs (local dir only - not served remotely)"
}
}
Write-host "-----------------------------------------------------------------------"
Write-Host "Use: 'list -remote -platform <name>' to see a platform's runtimes"
} else {
Write-host "-----------------------------------------------------------------------"
Write-Host "Local platform folders under $rtfolder"
Write-host "-----------------------------------------------------------------------"
if ($localdirs.Count -eq 0) {
write-host " (none - 'fetch' creates the local platform's folder)"
}
foreach ($d in $localdirs) {
$lhs = "$d".PadRight(25, ' ')
if ($d -eq $script:PunkLocalPlatform) {
write-host "* $lhs (local platform)"
} else {
write-host " $lhs"
}
}
Write-host "-----------------------------------------------------------------------"
Write-Host "Use: 'platforms -remote' to see platforms served by the artifact server"
Write-Host "Canonical platform names: 'help platforms' in the punk shell"
}
}
'help' {
Show-PunkRuntimeHelp
}
default {
#no action given (unknown actions were already rejected in the
#process block): show the short usage and point at 'help'
Show-PunkRuntimeUsage
write-host ""
write-host "'punk-runtime.cmd help' gives options, env vars and examples."
}
}

149
src/tests/modules/punk/console/testsuites/console/psfallback.test

@ -0,0 +1,149 @@
package require tcltest
tcltest::configure {*}$::argv
#min-version bound documents that these tests target the dev module's API and protects against
#stable bootsupport copies shadowing the alpha dev version if this file is sourced outside
#runtests.tcl (whose testinterp runs 'package prefer latest' making the bound redundant there).
package require punk::console 999999.0a1.0-
#Tests for the G-106 powershell console-mode fallback support machinery in punk::console::system:
#script resolution (ps_consolemode_script_get) and the embedded-copy sync contract with the
#canonical scriptlib/utils/pwsh/consolemode_server_async.ps1. Deliberately does NOT start a
#server or flip console modes - lifecycle verification is the G-106 recipe's hidden-console
#selftest (goals/archive/G-106-powershell-consolemode-fallback.md), which cannot run inside a
#shared test console.
namespace eval ::testspace {
namespace import ::tcltest::*
variable common {
set result ""
}
#locate the repo checkout root by walking up from this test file - the canonical ps1 lives at
#<root>/scriptlib/utils/pwsh/consolemode_server_async.ps1. Walk-up (rather than a fixed depth)
#survives file moves; capped to avoid scanning to the filesystem root.
variable canonical_ps1 ""
variable testfiledir [file dirname [file normalize [info script]]]
proc find_canonical {} {
variable testfiledir
set dir $testfiledir
for {set i 0} {$i < 12} {incr i} {
set cand [file join $dir scriptlib utils pwsh consolemode_server_async.ps1]
if {[file exists $cand]} {
return $cand
}
set parent [file dirname $dir]
if {$parent eq $dir} {
break
}
set dir $parent
}
return ""
}
set canonical_ps1 [find_canonical]
tcltest::testConstraint canonicalps1found [expr {$canonical_ps1 ne ""}]
proc normalized_text {text} {
#line-ending + edge-whitespace insensitive comparison form: the embedded copy is delivered
#to powershell via -c where leading/trailing whitespace is insignificant, and checkouts may
#vary line endings
return [string trim [string map [list \r\n \n] $text]]
}
variable env_save {
set saved_env_script unset
if {[info exists ::env(PUNK_PS_CONSOLEMODE_SCRIPT)]} {
set saved_env_script [set ::env(PUNK_PS_CONSOLEMODE_SCRIPT)]
unset ::env(PUNK_PS_CONSOLEMODE_SCRIPT)
}
}
variable env_restore {
if {$saved_env_script ne "unset"} {
set ::env(PUNK_PS_CONSOLEMODE_SCRIPT) $saved_env_script
} else {
unset -nocomplain ::env(PUNK_PS_CONSOLEMODE_SCRIPT)
}
}
#added 2026-07-22 (agent, G-106) - embedded-copy sync contract and script resolution chain
test psfallback_embedded_matches_canonical {embedded server script matches the canonical scriptlib file}\
-constraints canonicalps1found\
-setup $common -body {
variable canonical_ps1
set fd [open $canonical_ps1 r]
chan configure $fd -translation binary
set filetext [read $fd]
close $fd
set embedded [set ::punk::console::system::ps_consolemode_script_embedded]
lappend result [expr {[normalized_text $filetext] eq [normalized_text $embedded]}]
lappend result [expr {[string length [normalized_text $embedded]] > 1000}]
}\
-result [list\
1\
1\
]
test psfallback_placeholders_present {template placeholders present in embedded and canonical copies}\
-constraints canonicalps1found\
-setup $common -body {
variable canonical_ps1
set fd [open $canonical_ps1 r]
chan configure $fd -translation binary
set filetext [read $fd]
close $fd
set embedded [set ::punk::console::system::ps_consolemode_script_embedded]
foreach ph {<punkshell_consoleid> <punkshell_parentpid> <punkshell_psdebug>} {
lappend result [expr {[string first $ph $embedded] >= 0}][expr {[string first $ph $filetext] >= 0}]
}
set result
}\
-result [list\
11\
11\
11\
]
test psfallback_script_get_shape {ps_consolemode_script_get returns source/path/contents dict with non-empty contents}\
-setup $common -body {
set sg [punk::console::system::ps_consolemode_script_get]
lappend result [expr {[dict exists $sg source] && [dict exists $sg path] && [dict exists $sg contents]}]
lappend result [expr {[dict get $sg source] in {env scriptlib embedded}}]
lappend result [expr {[string length [dict get $sg contents]] > 1000}]
}\
-result [list\
1\
1\
1\
]
test psfallback_script_get_env_override {PUNK_PS_CONSOLEMODE_SCRIPT env override wins when the file exists}\
-setup [join [list $common $env_save] \n] -body {
set overridefile [tcltest::makeFile "# override script content marker-xyzzy" psfallback_override.ps1]
set ::env(PUNK_PS_CONSOLEMODE_SCRIPT) $overridefile
set sg [punk::console::system::ps_consolemode_script_get]
lappend result [dict get $sg source]
lappend result [expr {[string first "marker-xyzzy" [dict get $sg contents]] >= 0}]
}\
-cleanup [join [list $env_restore {
tcltest::removeFile psfallback_override.ps1
}] \n]\
-result [list\
env\
1\
]
test psfallback_script_get_env_override_missing {missing env-override file falls through to another source}\
-setup [join [list $common $env_save] \n] -body {
set ::env(PUNK_PS_CONSOLEMODE_SCRIPT) [file join [tcltest::temporaryDirectory] no_such_psfallback_file.ps1]
set sg [punk::console::system::ps_consolemode_script_get]
lappend result [expr {[dict get $sg source] ne "env"}]
}\
-cleanup $env_restore\
-result [list\
1\
]
cleanupTests
}
namespace delete ::testspace

8
src/vendorlib_tcl8/README.md

@ -6,3 +6,11 @@ These should generally be kept to a minimum
- todo: dependency and version number tracking; along with the provision of a mechanism for the project end-users to update.
Platform subfolder structure (2026-07-22): the platform-named subfolders
follow the CANONICAL punkshell platform-dir names defined by the
punk::platform module (also surfaced as 'help platforms' in the punk shell)
plus the special 'allplatforms' folder for platform-independent libraries.
The boot machinery adds allplatforms plus the running platform's folder to
auto_path. Keep the set of subfolders here in sync with punk::platform's
supported/dormant records across BOTH vendorlib_tcl8 and vendorlib_tcl9.

9
src/vendorlib_tcl8/freebsd-amd64/README.md

@ -1,9 +0,0 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the freebsd-amd64 platform.
Note that amd64 is equivalent to x86_64 in this context.

4
src/vendorlib_tcl8/freebsd-arm64/README.md

@ -0,0 +1,4 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the freebsd-arm64 platform.
(Canonical punkshell platform name - see punk::platform / 'help platforms')

5
src/vendorlib_tcl8/freebsd-x86_64/README.md

@ -0,0 +1,5 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the freebsd-x86_64 platform.
(Renamed from freebsd-amd64 2026-07-22: punkshell canonical platform names
normalize amd64 to x86_64 - see punk::platform / 'help platforms')

4
src/vendorlib_tcl8/linux-arm/README.md

@ -0,0 +1,4 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the linux-arm platform.
(Canonical punkshell platform name - see punk::platform / 'help platforms')

4
src/vendorlib_tcl8/linux-arm64/README.md

@ -0,0 +1,4 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the linux-arm64 platform.
(Canonical punkshell platform name - see punk::platform / 'help platforms')

4
src/vendorlib_tcl8/macosx-arm64/README.md

@ -0,0 +1,4 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the macosx-arm64 platform.
(Canonical punkshell platform name - see punk::platform / 'help platforms')

10
src/vendorlib_tcl8/msys-x86_64/README.md

@ -1,8 +1,6 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the msys-x86_64 platform.
This is a somewhat unix-like environment running on windows.
pkgIndex.tcl based libraries specific to the msys-x86_64 platform
(msys/cygwin-built tclsh runtimes).
Status: DORMANT - recognized canonical name but utility under review
(2026-07-22 platform survey; see punk::platform / 'help platforms').

6
src/vendorlib_tcl8/win32-ix86/README.md

@ -0,0 +1,6 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the win32-ix86 platform (32-bit x86
windows). Hosting for available third-party libraries - a punkshell buildsuite
for this platform is undetermined (canonical punkshell platform name - see
punk::platform / 'help platforms')

0
src/vendormodules_tcl8/cookfs1.9.0/asyncworker_process.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/asyncworker_process.tcl

0
src/vendormodules_tcl8/cookfs1.9.0/asyncworker_thread.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/asyncworker_thread.tcl

0
src/vendormodules_tcl8/cookfs1.9.0/cookfs1.9.0.dll → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/cookfs1.9.0.dll

0
src/vendormodules_tcl8/cookfs1.9.0/fsindex.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/fsindex.tcl

4
src/vendormodules_tcl8/cookfs1.9.0/pages.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/pages.tcl

@ -512,9 +512,7 @@ proc cookfs::tcl::pages::pagewrite {name contents} {
if {!$c(haschanged)} {
seek $c(fh) $c(indexoffset) start
} else {
# FUTURE: Optimize to avoid seeking in subsequent writes
# Consider tracking file position to eliminate redundant seek operations
# when writing multiple pages sequentially
# TODO: optimize not to seek in subsequent writes
seek $c(fh) 0 end
}
if {[catch {

0
src/vendormodules_tcl8/cookfs1.9.0/pkgIndex.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/pkgIndex.tcl

0
src/vendormodules_tcl8/cookfs1.9.0/pkgconfig.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/pkgconfig.tcl

0
src/vendormodules_tcl8/cookfs1.9.0/readerchannel.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/readerchannel.tcl

0
src/vendormodules_tcl8/cookfs1.9.0/vfs.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/vfs.tcl

0
src/vendormodules_tcl8/cookfs1.9.0/writer.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/writer.tcl

0
src/vendormodules_tcl8/cookfs1.9.0/writerchannel.tcl → src/vendorlib_tcl8/win32-x86_64/cookfs1.9.0/writerchannel.tcl

8
src/vendorlib_tcl9/README.md

@ -6,3 +6,11 @@ These should generally be kept to a minimum
- todo: dependency and version number tracking; along with the provision of a mechanism for the project end-users to update.
Platform subfolder structure (2026-07-22): the platform-named subfolders
follow the CANONICAL punkshell platform-dir names defined by the
punk::platform module (also surfaced as 'help platforms' in the punk shell)
plus the special 'allplatforms' folder for platform-independent libraries.
The boot machinery adds allplatforms plus the running platform's folder to
auto_path. Keep the set of subfolders here in sync with punk::platform's
supported/dormant records across BOTH vendorlib_tcl8 and vendorlib_tcl9.

9
src/vendorlib_tcl9/freebsd-amd64/README.md

@ -1,9 +0,0 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the freebsd-amd64 platform.
Note that amd64 is equivalent to x86_64 in this context.

4
src/vendorlib_tcl9/freebsd-arm64/README.md

@ -0,0 +1,4 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the freebsd-arm64 platform.
(Canonical punkshell platform name - see punk::platform / 'help platforms')

5
src/vendorlib_tcl9/freebsd-x86_64/README.md

@ -0,0 +1,5 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the freebsd-x86_64 platform.
(Renamed from freebsd-amd64 2026-07-22: punkshell canonical platform names
normalize amd64 to x86_64 - see punk::platform / 'help platforms')

4
src/vendorlib_tcl9/linux-arm/README.md

@ -0,0 +1,4 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the linux-arm platform.
(Canonical punkshell platform name - see punk::platform / 'help platforms')

4
src/vendorlib_tcl9/linux-arm64/README.md

@ -0,0 +1,4 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the linux-arm64 platform.
(Canonical punkshell platform name - see punk::platform / 'help platforms')

4
src/vendorlib_tcl9/macosx-arm64/README.md

@ -0,0 +1,4 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the macosx-arm64 platform.
(Canonical punkshell platform name - see punk::platform / 'help platforms')

10
src/vendorlib_tcl9/msys-x86_64/README.md

@ -1,8 +1,6 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the msys-x86_64 platform.
This is a somewhat unix-like environment running on windows.
pkgIndex.tcl based libraries specific to the msys-x86_64 platform
(msys/cygwin-built tclsh runtimes).
Status: DORMANT - recognized canonical name but utility under review
(2026-07-22 platform survey; see punk::platform / 'help platforms').

6
src/vendorlib_tcl9/win32-ix86/README.md

@ -0,0 +1,6 @@
Tcl library dependencies
pkgIndex.tcl based libraries specific to the win32-ix86 platform (32-bit x86
windows). Hosting for available third-party libraries - a punkshell buildsuite
for this platform is undetermined (canonical punkshell platform name - see
punk::platform / 'help platforms')

19
src/vfs/_config/project_main.tcl

@ -124,6 +124,23 @@ apply { args {
return "${plat}-${cpu}"
}
proc platform_punk {} {
#canonical punkshell platform-dir name: platform_generic normalized.
#INLINE COPY of punk::platform::normalize (src/modules/punk/platform-*.tm;
#'help platforms' documents the canon) - the boot stage cannot package
#require, so keep this mapping in sync with that module:
#amd64->x86_64, aarch64->arm64, macos->macosx, macosx arm->arm64.
set parts [split [platform_generic] -]
set cpu [lindex $parts end]
set os [join [lrange $parts 0 end-1] -]
if {$os eq "macos"} {set os macosx}
switch -- $cpu {
amd64 {set cpu x86_64}
aarch64 {set cpu arm64}
arm {if {$os eq "macosx"} {set cpu arm64}}
}
return "${os}-${cpu}"
}
}
set has_zipfs [expr {[info commands tcl::zipfs::root] ne ""}]
@ -609,7 +626,7 @@ apply { args {
#so we prepend to auto_path using a slightly inefficient method. Should be fine on relatively small list like this
#eventually it should just be something like 'ledit ::auto_path -1 -1 $libfolder'
if {"dev" in $package_modes} {
set platform [::punkboot::platform_generic]
set platform [::punkboot::platform_punk]
#on windows - case differences dont matter - but can stop us finding path in auto_path
#on other platforms, case differences could represent different paths
#review

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save