@guiho/runx 0.4.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/DOCS.md +27 -5
  3. package/README.md +6 -1
  4. package/devops/build-binaries.ts +3 -3
  5. package/devops/devops.xdocs.md +9 -6
  6. package/devops/extract-release-notes.spec.ts +54 -0
  7. package/devops/extract-release-notes.ts +40 -0
  8. package/devops/install.ps1 +29 -6
  9. package/devops/install.sh +61 -22
  10. package/devops/installers.spec.ts +162 -5
  11. package/devops/verify-release-assets.ts +14 -0
  12. package/docs/architecture/architecture.xdocs.md +2 -1
  13. package/docs/architecture/cli-architecture.md +18 -9
  14. package/docs/decisions/decisions.xdocs.md +4 -0
  15. package/docs/decisions/markdown-release-assets-and-version-scoped-notes.md +84 -0
  16. package/docs/plans/automatic-agent-maintenance.md +120 -0
  17. package/docs/plans/linux-installer-latest-release.md +43 -0
  18. package/docs/plans/plans.xdocs.md +6 -0
  19. package/docs/plans/platform-aware-startup-greeting.md +43 -0
  20. package/docs/plans/rfc-0034-cli-compliance-migration.md +17 -8
  21. package/docs/plans/upgrade-reliability-implementation.md +1 -1
  22. package/docs/reviews/implementation/automatic-agent-maintenance-review.md +63 -0
  23. package/docs/reviews/implementation/bash-installer-review.md +42 -0
  24. package/docs/reviews/implementation/implementation.xdocs.md +14 -0
  25. package/docs/reviews/implementation/linux-installer-latest-release-review.md +49 -0
  26. package/docs/reviews/implementation/platform-aware-startup-greeting-review.md +51 -0
  27. package/docs/reviews/implementation/rfc-0034-cli-compliance-migration-review.md +5 -2
  28. package/docs/reviews/implementation/unicode-help-tree-review.md +41 -0
  29. package/docs/reviews/implementation/upgrade-reliability-issue-12-review.md +62 -0
  30. package/docs/reviews/implementation/upgrade-reliability-issue-13-review.md +63 -0
  31. package/docs/reviews/plans/automatic-agent-maintenance-review.md +56 -0
  32. package/docs/reviews/plans/plans.xdocs.md +2 -0
  33. package/docs/superpowers/specs/2026-07-15-upgrade-reliability-design.md +1 -1
  34. package/docs/todo/automatic-agent-maintenance-implementation.md +55 -0
  35. package/docs/todo/automatic-agent-maintenance.md +86 -0
  36. package/docs/todo/bash-installer.md +45 -0
  37. package/docs/todo/linux-installer-latest-release.md +47 -0
  38. package/docs/todo/platform-aware-startup-greeting.md +41 -0
  39. package/docs/todo/rfc-0034-cli-compliance-migration-implementation.md +7 -2
  40. package/docs/todo/rfc-0034-cli-compliance-migration.md +7 -3
  41. package/docs/todo/todo.xdocs.md +14 -0
  42. package/docs/todo/unicode-help-tree.md +44 -0
  43. package/docs/todo/upgrade-reliability.md +51 -0
  44. package/docs/validation/automatic-agent-maintenance.md +63 -0
  45. package/docs/validation/bash-installer.md +55 -0
  46. package/docs/validation/linux-installer-latest-release.md +50 -0
  47. package/docs/validation/platform-aware-startup-greeting.md +50 -0
  48. package/docs/validation/rfc-0034-cli-compliance-migration.md +10 -4
  49. package/docs/validation/unicode-help-tree.md +43 -0
  50. package/docs/validation/upgrade-reliability-issue-12.md +74 -0
  51. package/docs/validation/upgrade-reliability-issue-13.md +72 -0
  52. package/docs/validation/upgrade-reliability.md +38 -99
  53. package/docs/validation/validation.xdocs.md +16 -2
  54. package/library/agent-maintenance.d.ts +19 -0
  55. package/library/agent-maintenance.d.ts.map +1 -0
  56. package/library/agent-maintenance.js +49 -0
  57. package/library/agents.d.ts +6 -1
  58. package/library/agents.d.ts.map +1 -1
  59. package/library/agents.js +58 -11
  60. package/library/cli.d.ts +2 -1
  61. package/library/cli.d.ts.map +1 -1
  62. package/library/cli.js +34 -9
  63. package/library/recovery.js +1 -1
  64. package/library/release-catalog.d.ts +2 -1
  65. package/library/release-catalog.d.ts.map +1 -1
  66. package/library/release-catalog.js +9 -1
  67. package/library/storage.d.ts +2 -1
  68. package/library/storage.d.ts.map +1 -1
  69. package/library/storage.js +13 -1
  70. package/package.json +1 -1
  71. package/skills/guiho-s-runx/SKILL.md +11 -0
  72. package/skills/guiho-s-runx/guiho-s-runx.xdocs.md +4 -1
package/CHANGELOG.md CHANGED
@@ -15,6 +15,62 @@ owner: runx
15
15
 
16
16
  # Changelog
17
17
 
18
+ ## 0.5.2 - 2026-07-20
19
+
20
+ ### Fixed
21
+
22
+ - Changed the Linux/macOS installer to download latest native and agent assets
23
+ through GitHub's stable `releases/latest/download` aliases, avoiding scoped
24
+ package-tag truncation during redirect parsing.
25
+ - Changed the no-argument greeting to report Windows, Linux, or macOS according
26
+ to the runtime operating system.
27
+
28
+ ### Changed
29
+
30
+ - Migrated RunX release configuration from legacy `mirror.config.toml` to
31
+ `mirror.yaml` while preserving package, commit, tag, push, and changelog
32
+ behavior.
33
+
34
+ ## 0.5.1 - 2026-07-19
35
+
36
+ ### Fixed
37
+
38
+ - Enforced LF line endings for Bash scripts so Windows checkouts preserve
39
+ portable installer syntax.
40
+ - Gave the detached agent-maintenance integration test an explicit
41
+ platform-appropriate timeout so slower Windows CI runners validate the full
42
+ workflow without changing runtime behavior.
43
+
44
+ ## 0.5.0 - 2026-07-19
45
+
46
+ ### Added
47
+
48
+ - Added silent detached maintenance that keeps both global `guiho-s-runx`
49
+ skill installations current and reconciles one nearest managed `AGENTS.md`
50
+ block without changing foreground output or exit behavior.
51
+ - Added explicit regression coverage for every upgrade recovery outcome,
52
+ executable stable/prerelease installer verification, and the Unicode
53
+ description-aligned command tree.
54
+
55
+ ### Changed
56
+
57
+ - `runx upgrade list` now returns the complete published catalog, including
58
+ labeled prereleases, unless pagination is explicitly requested.
59
+ - The Linux/macOS installer and generated recovery commands now use Bash with
60
+ `set -euo pipefail`; canonical documentation no longer invokes `sh`.
61
+ - Release delivery now validates the exact twelve native binaries and two
62
+ Markdown agent assets and uses only the matching changelog version section
63
+ for GitHub Release notes.
64
+
65
+ ### Fixed
66
+
67
+ - Fixed `runx upgrade list` and `runx upgrade check` so Citty does not execute
68
+ the parent upgrade action after the selected leaf or append a second JSON
69
+ document.
70
+ - Fixed automatic agent-resource updates to use idempotent atomic writes,
71
+ preserve user-authored instructions, migrate legacy markers, and remain safe
72
+ under concurrent invocations.
73
+
18
74
  ## 0.4.1 - 2026-07-18
19
75
 
20
76
  ### Fixed
package/DOCS.md CHANGED
@@ -27,9 +27,11 @@ before Bun is installed.
27
27
  No arguments prints exactly:
28
28
 
29
29
  ```text
30
- Hello Windows - runx v<version>
30
+ Hello <Windows|Linux|macOS> - runx v<version>
31
31
  ```
32
32
 
33
+ The platform label matches the runtime operating system.
34
+
33
35
  The foreground reads `~/.guiho/runx/cache.json` and never waits for network
34
36
  work. A detached worker validates GitHub release data and atomically refreshes
35
37
  that cache. When a decoded cache announces an update, RunX prints:
@@ -99,6 +101,14 @@ Skill install, update, and uninstall default to global scope and target both:
99
101
 
100
102
  Use `--local` for the corresponding project-local paths.
101
103
 
104
+ Ordinary RunX commands schedule a hidden detached worker that compares the
105
+ bundled skill with both global copies and rewrites only missing or stale
106
+ targets. The worker finds the nearest existing `AGENTS.md` from effective
107
+ `--cwd`; when none exists in an ancestor, it creates `AGENTS.md` at effective
108
+ cwd. Reconciliation is atomic, migrates legacy RunX markers, preserves content
109
+ outside managed markers, and is silent when no change is required. Worker
110
+ failures never change foreground output or exit status.
111
+
102
112
  Instruction actions manage both `AGENTS.md` and `CLAUDE.md` when both exist,
103
113
  the existing one when only one exists, and create `AGENTS.md` when neither
104
114
  exists. The exact idempotent boundaries are:
@@ -114,8 +124,10 @@ names; `agent prompt show <id>` prints only the raw prompt.
114
124
  ## Upgrade And Installation
115
125
 
116
126
  `runx upgrade` accepts `--version`, `--arch`, `--variant`, `--dry-run`, and
117
- `--format`. x64 defaults to `baseline`. `upgrade list` supports positive
118
- `--page` and `--per-page` plus `--pre-releases`.
127
+ `--format`. x64 defaults to `baseline`. `upgrade list` returns the complete
128
+ published catalog, including labeled prereleases, by default. Positive
129
+ `--page` and `--per-page` values request an explicit view; `--pre-releases` is
130
+ accepted but is unnecessary because prereleases are never hidden.
119
131
 
120
132
  Replacement is transactional: download, native-format validation, backup,
121
133
  replacement, version verification, rollback on failure, agent-skill refresh,
@@ -150,6 +162,16 @@ runx-windows-arm64.exe
150
162
  runx-windows-x64.exe
151
163
  runx-windows-x64-baseline.exe
152
164
  runx-windows-x64-modern.exe
153
- guiho-s-runx
154
- guiho-i-runx
165
+ guiho-s-runx.md
166
+ guiho-i-runx.md
155
167
  ```
168
+
169
+ The Markdown suffix is part of the public GitHub Release filename contract.
170
+ Installers retain the standard installed skill filename `SKILL.md` and use the
171
+ instruction asset contents when reconciling managed instruction blocks. Before
172
+ either write, installers reject empty, executable, binary, invalid UTF-8, or
173
+ misidentified Markdown payloads.
174
+
175
+ Release descriptions contain only the matching `## <version> - <date>`
176
+ changelog section. The publish workflow fails closed when that exact section is
177
+ missing and refreshes the notes when rerunning an existing release.
package/README.md CHANGED
@@ -32,13 +32,18 @@ irm https://raw.githubusercontent.com/CGuiho/runx/main/devops/install.ps1 | iex
32
32
  POSIX:
33
33
 
34
34
  ```sh
35
- curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/CGuiho/runx/main/devops/install.sh | sh
35
+ curl --proto '=https' --tlsv1.2 -fsSL https://raw.githubusercontent.com/CGuiho/runx/main/devops/install.sh | bash
36
36
  ```
37
37
 
38
38
  The direct installers select and verify a native asset, install `runx` on PATH,
39
39
  install `guiho-s-runx` into both global agent-tool directories, and reconcile
40
40
  RunX instructions in the current project.
41
41
 
42
+ After installation, ordinary RunX invocations schedule a silent background
43
+ maintenance worker. It repairs missing or stale global skill copies and keeps
44
+ one compact RunX block in the nearest `AGENTS.md` without delaying or changing
45
+ the requested command's output.
46
+
42
47
  The npm package is a Node-compatible bootstrap that downloads and delegates to
43
48
  the version-matched native binary. Bun is not required for npm installation or
44
49
  first execution.
@@ -31,7 +31,7 @@ const binaryTargets: readonly BinaryTarget[] = [
31
31
  { bunTarget: 'bun-windows-x64-modern', assetName: 'runx-windows-x64-modern.exe' },
32
32
  ]
33
33
 
34
- const agentAssetNames = ['guiho-s-runx', 'guiho-i-runx'] as const
34
+ const agentAssetNames = ['guiho-s-runx.md', 'guiho-i-runx.md'] as const
35
35
  const expectedReleaseAssetNames = [...binaryTargets.map((target) => target.assetName), ...agentAssetNames]
36
36
 
37
37
  if (expectedReleaseAssetNames.length !== 14 || new Set(expectedReleaseAssetNames).size !== 14) {
@@ -69,8 +69,8 @@ if (import.meta.main) {
69
69
  process.stdout.write(`built: ${target.assetName}\n`)
70
70
  }
71
71
 
72
- await Bun.write(joinPath(bin, 'guiho-s-runx'), Bun.file(joinPath(root, 'skills', 'guiho-s-runx', 'SKILL.md')))
73
- await Bun.write(joinPath(bin, 'guiho-i-runx'), Bun.file(joinPath(root, 'prompts', 'guiho-i-runx.md')))
72
+ await Bun.write(joinPath(bin, 'guiho-s-runx.md'), Bun.file(joinPath(root, 'skills', 'guiho-s-runx', 'SKILL.md')))
73
+ await Bun.write(joinPath(bin, 'guiho-i-runx.md'), Bun.file(joinPath(root, 'prompts', 'guiho-i-runx.md')))
74
74
 
75
75
  const observed = [...new Bun.Glob('*').scanSync({ cwd: bin, onlyFiles: true })].sort()
76
76
  const expected = [...expectedReleaseAssetNames].sort()
@@ -4,11 +4,13 @@ description: Native release-asset compilation and direct RunX installer scripts.
4
4
  parent: runx
5
5
  children: []
6
6
  files:
7
- build-binaries.ts: Cross-compiles twelve RFC-named native binaries and adds the two exact agent assets.
8
- verify-release-assets.ts: Fails on any missing, duplicate, extra, or legacy release asset name.
9
- installers.spec.ts: Verifies both installers expose progress, validation, dual-tool skills, instruction reconciliation, and no POSIX Bun dependency.
7
+ build-binaries.ts: Cross-compiles twelve RFC-named native binaries and adds the two exact .md agent assets.
8
+ extract-release-notes.ts: Extracts one exact version section from CHANGELOG.md and fails closed when the heading or notes are missing.
9
+ extract-release-notes.spec.ts: Verifies exact heading boundaries and exclusion of frontmatter and other release sections.
10
+ verify-release-assets.ts: Fails on missing, duplicate, extra, legacy, empty, binary, or misidentified release assets.
11
+ installers.spec.ts: Verifies latest-alias and exact-tag URLs, installer progress, resources, executable version matching, Markdown rejection, and no POSIX Bun dependency.
10
12
  install.ps1: Resolves an exact Windows release, downloads and validates a compatible asset, transactionally replaces the canonical executable, verifies its version, and rolls back on failure.
11
- install.sh: POSIX installer for Linux or Darwin with progress, validation, PATH, dual skills, instructions, verification, and rollback.
13
+ install.sh: Bash installer for Linux or Darwin with stable latest-download resolution, exact tagged releases, progress, validation, PATH, dual skills, instructions, verification, and rollback.
12
14
  documents: {}
13
15
  tags:
14
16
  - devops
@@ -24,5 +26,6 @@ flags: []
24
26
  status: stable
25
27
  ---
26
28
 
27
- These scripts build and verify the exact fourteen-asset release and support
28
- direct installation without npm or Bun.
29
+ These scripts build and verify the exact fourteen-asset release, validate the
30
+ downloaded Markdown resource contract, create exact version-only GitHub Release
31
+ notes, and support direct installation without npm or Bun.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+
5
+ import { describe, expect, test } from 'bun:test'
6
+ import { extractReleaseNotes } from './extract-release-notes.js'
7
+
8
+ describe('extractReleaseNotes', () => {
9
+ test('extracts only the exact unbracketed version section', () => {
10
+ const changelog = `---
11
+ name: Example
12
+ ---
13
+
14
+ # Changelog
15
+
16
+ ## 0.4.10 - 2026-07-20
17
+
18
+ ### Fixed
19
+
20
+ - Newer patch.
21
+
22
+ ## 0.4.1 - 2026-07-18
23
+
24
+ ### Fixed
25
+
26
+ - Requested patch.
27
+
28
+ ## 0.4.0 - 2026-07-18
29
+
30
+ ### Added
31
+
32
+ - Older release.
33
+ `
34
+
35
+ expect(extractReleaseNotes(changelog, '0.4.1')).toBe(`## 0.4.1 - 2026-07-18
36
+
37
+ ### Fixed
38
+
39
+ - Requested patch.
40
+ `)
41
+ })
42
+
43
+ test('fails closed when the exact version heading is absent', () => {
44
+ expect(() => extractReleaseNotes('## 0.4.1 - 2026-07-18\n\n- Fixed.\n', '0.4.2')).toThrow(
45
+ 'Expected exactly one changelog heading for version 0.4.2.',
46
+ )
47
+ })
48
+
49
+ test('rejects an empty version section', () => {
50
+ expect(() => extractReleaseNotes('## 0.4.2 - 2026-07-19\n## 0.4.1 - 2026-07-18\n\n- Older.\n', '0.4.2')).toThrow(
51
+ 'Changelog section for version 0.4.2 has no release notes.',
52
+ )
53
+ })
54
+ })
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @copyright Copyright © 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
3
+ */
4
+
5
+ export {
6
+ extractReleaseNotes,
7
+ }
8
+
9
+ function escapeRegExp(value: string): string {
10
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
11
+ }
12
+
13
+ function extractReleaseNotes(changelog: string, version: string): string {
14
+ const normalized = changelog.replace(/\r\n/g, '\n')
15
+ const heading = new RegExp(`^## ${escapeRegExp(version)} - .+$`, 'gm')
16
+ const matches = [...normalized.matchAll(heading)]
17
+ if (matches.length !== 1 || matches[0]?.index === undefined) {
18
+ throw new Error(`Expected exactly one changelog heading for version ${version}.`)
19
+ }
20
+
21
+ const start = matches[0].index
22
+ const nextHeading = normalized.indexOf('\n## ', start + matches[0][0].length)
23
+ const section = normalized.slice(start, nextHeading === -1 ? normalized.length : nextHeading).trim()
24
+ if (!section.includes('\n')) {
25
+ throw new Error(`Changelog section for version ${version} has no release notes.`)
26
+ }
27
+
28
+ return `${section}\n`
29
+ }
30
+
31
+ if (import.meta.main) {
32
+ const [version, outputPath] = Bun.argv.slice(2)
33
+ if (!version || !outputPath) {
34
+ throw new Error('Usage: bun run devops/extract-release-notes.ts <version> <output-path>')
35
+ }
36
+
37
+ const changelog = await Bun.file(new URL('../CHANGELOG.md', import.meta.url)).text()
38
+ await Bun.write(outputPath, extractReleaseNotes(changelog, version))
39
+ process.stdout.write(`wrote release notes for ${version}: ${outputPath}\n`)
40
+ }
@@ -78,6 +78,25 @@ function Test-NativeBinary {
78
78
  finally { $stream.Dispose() }
79
79
  }
80
80
 
81
+ function Test-MarkdownAsset {
82
+ param([string]$Path, [string]$ExpectedName)
83
+ $bytes = [System.IO.File]::ReadAllBytes($Path)
84
+ if ($bytes.Length -eq 0) { throw "Downloaded Markdown asset is empty: $Path" }
85
+ if ($bytes.Length -ge 2 -and $bytes[0] -eq 0x4D -and $bytes[1] -eq 0x5A) {
86
+ throw "Downloaded Markdown asset has a Windows executable header: $Path"
87
+ }
88
+ if ($bytes -contains 0) { throw "Downloaded Markdown asset contains binary NUL bytes: $Path" }
89
+ try {
90
+ $text = ([System.Text.UTF8Encoding]::new($false, $true)).GetString($bytes).Replace("`r`n", "`n")
91
+ } catch {
92
+ throw "Downloaded Markdown asset is not valid UTF-8 text: $Path"
93
+ }
94
+ if (-not $text.StartsWith("---`n")) { throw "Downloaded Markdown asset does not begin with YAML frontmatter: $Path" }
95
+ if ($text -notmatch "(?m)^name:\s*$([Regex]::Escape($ExpectedName))\s*$") {
96
+ throw "Downloaded Markdown asset identity does not match $ExpectedName`: $Path"
97
+ }
98
+ }
99
+
81
100
  function Test-InstalledVersion {
82
101
  param([string]$Path, [string]$ExpectedVersion)
83
102
  $startInfo = New-Object System.Diagnostics.ProcessStartInfo
@@ -156,6 +175,8 @@ function Test-Shadowing {
156
175
  }
157
176
  }
158
177
 
178
+ if ($env:RUNX_INSTALLER_SOURCE_ONLY -eq '1') { return }
179
+
159
180
  $detectedArch = if ($Arch) { $Arch } else { switch ($env:PROCESSOR_ARCHITECTURE) { 'AMD64' { 'x64' } 'ARM64' { 'arm64' } default { throw "Unsupported architecture: $env:PROCESSOR_ARCHITECTURE" } } }
160
181
  if ($detectedArch -notin @('x64', 'arm64')) { throw "Invalid architecture: $detectedArch" }
161
182
  if (-not [Environment]::Is64BitOperatingSystem) { throw 'Unsupported platform: Windows 32-bit is not supported.' }
@@ -208,13 +229,15 @@ try {
208
229
  if (-not $downloadedPath) { throw "No compatible RunX $targetVersion binary found at https://github.com/$Repo/releases" }
209
230
  Write-Host 'Replacing...'
210
231
  Install-Transactional -DownloadedPath $downloadedPath -Destination $destination -ExpectedVersion $targetVersion
211
- $skillAsset = Join-Path $temporaryDirectory 'guiho-s-runx'
212
- $promptAsset = Join-Path $temporaryDirectory 'guiho-i-runx'
232
+ $skillAsset = Join-Path $temporaryDirectory 'guiho-s-runx.md'
233
+ $promptAsset = Join-Path $temporaryDirectory 'guiho-i-runx.md'
213
234
  $assetBase = if ($DownloadBaseUrl) { "$DownloadBaseUrl/$encodedTag" } else { "https://github.com/$Repo/releases/download/$encodedTag" }
214
- Write-Host "Downloading skill asset: $assetBase/guiho-s-runx"
215
- Invoke-WebRequest -Uri "$assetBase/guiho-s-runx" -OutFile $skillAsset -UseBasicParsing
216
- Write-Host "Downloading instruction asset: $assetBase/guiho-i-runx"
217
- Invoke-WebRequest -Uri "$assetBase/guiho-i-runx" -OutFile $promptAsset -UseBasicParsing
235
+ Write-Host "Downloading skill asset: $assetBase/guiho-s-runx.md"
236
+ Invoke-WebRequest -Uri "$assetBase/guiho-s-runx.md" -OutFile $skillAsset -UseBasicParsing
237
+ Test-MarkdownAsset -Path $skillAsset -ExpectedName 'guiho-s-runx'
238
+ Write-Host "Downloading instruction asset: $assetBase/guiho-i-runx.md"
239
+ Invoke-WebRequest -Uri "$assetBase/guiho-i-runx.md" -OutFile $promptAsset -UseBasicParsing
240
+ Test-MarkdownAsset -Path $promptAsset -ExpectedName 'guiho-i-runx'
218
241
  foreach ($skillRoot in @((Join-Path $HOME '.agents\skills\guiho-s-runx'), (Join-Path $HOME '.claude\skills\guiho-s-runx'))) {
219
242
  New-Item -ItemType Directory -Force -Path $skillRoot | Out-Null
220
243
  Copy-Item -LiteralPath $skillAsset -Destination (Join-Path $skillRoot 'SKILL.md') -Force
package/devops/install.sh CHANGED
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env sh
2
- set -eu
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
3
 
4
4
  REPO="${RUNX_REPO:-CGuiho/runx}"
5
5
  VERSION="${RUNX_VERSION:-latest}"
@@ -10,6 +10,7 @@ VARIANT_OVERRIDE=""
10
10
  OS=""
11
11
  ARCH=""
12
12
  TARGET_VERSION=""
13
+ INSTALLED_VERSION=""
13
14
  TMP=""
14
15
 
15
16
  cleanup() { [ -z "$TMP" ] || rm -rf -- "$TMP"; }
@@ -76,11 +77,21 @@ normalize_version() {
76
77
 
77
78
  resolve_target_version() {
78
79
  if [ "$VERSION" != latest ]; then normalize_version "$VERSION"; return; fi
79
- printf 'Resolving latest stable RunX release...\n' >&2
80
- effective=$(curl --fail --location --silent --show-error --proto '=https' --tlsv1.2 --output /dev/null --write-out '%{url_effective}' "https://github.com/${REPO}/releases/latest")
81
- tag=${effective##*/}
82
- tag=$(printf '%s' "$tag" | sed 's/%40/@/g; s/%2[Ff]/\//g')
83
- normalize_version "$tag"
80
+ printf 'latest\n'
81
+ }
82
+
83
+ build_asset_url() {
84
+ asset=$1
85
+ if [ -n "$DOWNLOAD_BASE_URL" ]; then
86
+ printf '%s/%s\n' "${DOWNLOAD_BASE_URL%/}" "$asset"
87
+ return
88
+ fi
89
+ if [ "$TARGET_VERSION" = latest ]; then
90
+ printf 'https://github.com/%s/releases/latest/download/%s\n' "$REPO" "$asset"
91
+ return
92
+ fi
93
+ encoded_tag="%40guiho%2Frunx%40${TARGET_VERSION}"
94
+ printf 'https://github.com/%s/releases/download/%s/%s\n' "$REPO" "$encoded_tag" "$asset"
84
95
  }
85
96
 
86
97
  build_candidates() {
@@ -106,6 +117,25 @@ verify_native_binary() {
106
117
  esac
107
118
  }
108
119
 
120
+ verify_markdown_asset() {
121
+ path=$1
122
+ expected_name=$2
123
+ [ -s "$path" ] || fail "downloaded Markdown asset is empty: $path"
124
+ magic=$(od -An -tx1 -N2 "$path" 2>/dev/null | tr -d ' \n')
125
+ [ "$magic" != 4d5a ] || fail "downloaded Markdown asset has a Windows executable header: $path"
126
+ if od -An -tx1 "$path" 2>/dev/null | grep -Eq '(^|[[:space:]])00([[:space:]]|$)'; then
127
+ fail "downloaded Markdown asset contains binary NUL bytes: $path"
128
+ fi
129
+ iconv -f UTF-8 -t UTF-8 "$path" >/dev/null 2>&1 || fail "downloaded Markdown asset is not valid UTF-8 text: $path"
130
+ first_line=$(sed -n '1{s/\r$//;p;}' "$path")
131
+ [ "$first_line" = '---' ] || fail "downloaded Markdown asset does not begin with YAML frontmatter: $path"
132
+ awk -v expected="$expected_name" '
133
+ { sub(/\r$/, "") }
134
+ $0 == "name: " expected { found=1 }
135
+ END { exit(found ? 0 : 1) }
136
+ ' "$path" || fail "downloaded Markdown asset identity does not match $expected_name: $path"
137
+ }
138
+
109
139
  verify_installed_version() {
110
140
  verify_output=$(mktemp "${TMPDIR:-/tmp}/runx-version.XXXXXX") || return 1
111
141
  "$1" --version >"$verify_output" 2>&1 &
@@ -129,16 +159,21 @@ verify_installed_version() {
129
159
  fi
130
160
  actual=$(cat "$verify_output")
131
161
  rm -f -- "$verify_output"
132
- [ "$actual" = "$TARGET_VERSION" ] || { printf 'error: installed RunX reported %s; expected %s\n' "${actual:-<empty>}" "$TARGET_VERSION" >&2; return 1; }
162
+ normalized=$(normalize_version "$actual") || return 1
163
+ if [ "$TARGET_VERSION" != latest ] && [ "$normalized" != "$TARGET_VERSION" ]; then
164
+ printf 'error: installed RunX reported %s; expected %s\n' "${actual:-<empty>}" "$TARGET_VERSION" >&2
165
+ return 1
166
+ fi
167
+ INSTALLED_VERSION=$normalized
133
168
  }
134
169
 
135
170
  ensure_path() {
136
171
  PATH="$INSTALL_DIR:$PATH"; export PATH
137
172
  profile="$HOME/.profile"
138
- case "${SHELL##*/}" in zsh) profile="$HOME/.zshrc" ;; bash) profile="$HOME/.bashrc" ;; fish) profile="$HOME/.config/fish/config.fish" ;; esac
173
+ case "${SHELL:-bash}" in */zsh|zsh) profile="$HOME/.zshrc" ;; */bash|bash) profile="$HOME/.bashrc" ;; */fish|fish) profile="$HOME/.config/fish/config.fish" ;; esac
139
174
  mkdir -p -- "$(dirname "$profile")"
140
175
  if ! grep -Fq "$INSTALL_DIR" "$profile" 2>/dev/null; then
141
- if [ "${SHELL##*/}" = fish ]; then printf '\n# Added by runx installer\nfish_add_path %s\n' "$INSTALL_DIR" >>"$profile"
176
+ if [ "${SHELL:-bash}" = fish ] || [ "${SHELL:-bash}" = */fish ]; then printf '\n# Added by runx installer\nfish_add_path %s\n' "$INSTALL_DIR" >>"$profile"
142
177
  else printf '\n# Added by runx installer\nexport PATH=%s:$PATH\n' "$INSTALL_DIR" >>"$profile"; fi
143
178
  fi
144
179
  }
@@ -182,16 +217,17 @@ install_transactional() {
182
217
  }
183
218
 
184
219
  install_agent_assets() {
185
- tag_base=${DOWNLOAD_BASE_URL:-"https://github.com/${REPO}/releases/download"}
186
- skill_url="${tag_base%/}/${encoded_tag}/guiho-s-runx"
187
- prompt_url="${tag_base%/}/${encoded_tag}/guiho-i-runx"
220
+ skill_url=$(build_asset_url guiho-s-runx.md)
221
+ prompt_url=$(build_asset_url guiho-i-runx.md)
188
222
  printf 'Downloading skill asset: %s\n' "$skill_url"
189
- download_asset "$skill_url" "$TMP/guiho-s-runx" || fail 'could not download guiho-s-runx'
223
+ download_asset "$skill_url" "$TMP/guiho-s-runx.md" || fail 'could not download guiho-s-runx.md'
224
+ verify_markdown_asset "$TMP/guiho-s-runx.md" 'guiho-s-runx'
190
225
  printf 'Downloading instruction asset: %s\n' "$prompt_url"
191
- download_asset "$prompt_url" "$TMP/guiho-i-runx" || fail 'could not download guiho-i-runx'
226
+ download_asset "$prompt_url" "$TMP/guiho-i-runx.md" || fail 'could not download guiho-i-runx.md'
227
+ verify_markdown_asset "$TMP/guiho-i-runx.md" 'guiho-i-runx'
192
228
  for skill_root in "$HOME/.agents/skills/guiho-s-runx" "$HOME/.claude/skills/guiho-s-runx"; do
193
229
  mkdir -p -- "$skill_root"
194
- install -m 0644 "$TMP/guiho-s-runx" "$skill_root/SKILL.md"
230
+ install -m 0644 "$TMP/guiho-s-runx.md" "$skill_root/SKILL.md"
195
231
  printf 'Installed skill: %s\n' "$skill_root/SKILL.md"
196
232
  done
197
233
  targets=""
@@ -214,7 +250,7 @@ install_agent_assets() {
214
250
  cat "$clean_file"
215
251
  [ ! -s "$clean_file" ] || printf '\n'
216
252
  printf '<!-- BEGIN RUNX — DO NOT EDIT THIS SECTION -->\n'
217
- cat "$TMP/guiho-i-runx"
253
+ cat "$TMP/guiho-i-runx.md"
218
254
  printf '\n<!-- END RUNX -->\n'
219
255
  } >"$instruction_file"
220
256
  done
@@ -222,20 +258,19 @@ install_agent_assets() {
222
258
 
223
259
  main() {
224
260
  parse_args "$@"
225
- for command in awk cat curl date dirname grep head install mktemp mv od rm sed tr uname; do require_command "$command"; done
261
+ for command in awk cat curl date dirname grep head iconv install mktemp mv od rm sed tr uname; do require_command "$command"; done
226
262
  OS=$(detect_os); ARCH=$(detect_arch); TARGET_VERSION=$(resolve_target_version)
227
263
  TMP=$(mktemp -d "${TMPDIR:-/tmp}/runx-install.XXXXXX")
228
264
  mkdir -p -- "$INSTALL_DIR"
229
265
  destination="$INSTALL_DIR/runx"
230
- encoded_tag="%40guiho%2Frunx%40${TARGET_VERSION}"
231
266
  downloaded=""
232
267
  first_asset=$(build_candidates | head -n 1)
233
- source_url="${DOWNLOAD_BASE_URL:-https://github.com/${REPO}/releases/download}/${encoded_tag}/${first_asset}"
268
+ source_url=$(build_asset_url "$first_asset")
234
269
  printf 'Initiating GUIHO CLI Upgrade / Installation Sequence...\n'
235
270
  printf 'Target Version: v%s\nArchitecture: %s\nVariant: %s\nSource URL: %s\n' "$TARGET_VERSION" "$ARCH" "${VARIANT_OVERRIDE:-baseline}" "$source_url"
236
271
  build_candidates | while IFS= read -r asset; do printf '%s\n' "$asset"; done >"$TMP/candidates"
237
272
  while IFS= read -r asset; do
238
- if [ -n "$DOWNLOAD_BASE_URL" ]; then url="${DOWNLOAD_BASE_URL%/}/${encoded_tag}/${asset}"; else url="https://github.com/${REPO}/releases/download/${encoded_tag}/${asset}"; fi
273
+ url=$(build_asset_url "$asset")
239
274
  candidate="$TMP/$asset"
240
275
  printf 'Downloading %s\n' "$url"
241
276
  if download_asset "$url" "$candidate"; then :; else
@@ -253,7 +288,11 @@ main() {
253
288
  install_agent_assets
254
289
  [ "${RUNX_SKIP_PATH_UPDATE:-0}" = 1 ] || ensure_path
255
290
  printf 'Final verification: %s --version\n' "$destination"
256
- printf 'Installed and verified RunX %s at %s\n' "$TARGET_VERSION" "$destination"
291
+ printf 'Installed and verified RunX %s at %s\n' "$INSTALLED_VERSION" "$destination"
257
292
  }
258
293
 
294
+ if [ "${RUNX_INSTALLER_SOURCE_ONLY:-0}" = 1 ]; then
295
+ return 0 2>/dev/null || exit 0
296
+ fi
297
+
259
298
  main "$@"