@guiho/runx 0.4.0 → 0.5.1
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.
- package/CHANGELOG.md +52 -0
- package/DOCS.md +24 -4
- package/README.md +6 -1
- package/devops/build-binaries.ts +3 -3
- package/devops/devops.xdocs.md +9 -6
- package/devops/extract-release-notes.spec.ts +54 -0
- package/devops/extract-release-notes.ts +40 -0
- package/devops/install.ps1 +29 -6
- package/devops/install.sh +36 -11
- package/devops/installers.spec.ts +152 -5
- package/devops/verify-release-assets.ts +14 -0
- package/docs/architecture/architecture.xdocs.md +2 -1
- package/docs/architecture/cli-architecture.md +18 -9
- package/docs/decisions/decisions.xdocs.md +4 -0
- package/docs/decisions/markdown-release-assets-and-version-scoped-notes.md +84 -0
- package/docs/plans/automatic-agent-maintenance.md +120 -0
- package/docs/plans/plans.xdocs.md +2 -0
- package/docs/plans/rfc-0034-cli-compliance-migration.md +17 -8
- package/docs/plans/upgrade-reliability-implementation.md +1 -1
- package/docs/reviews/implementation/automatic-agent-maintenance-review.md +63 -0
- package/docs/reviews/implementation/bash-installer-review.md +42 -0
- package/docs/reviews/implementation/implementation.xdocs.md +12 -2
- package/docs/reviews/implementation/rfc-0034-cli-compliance-migration-review.md +18 -5
- package/docs/reviews/implementation/unicode-help-tree-review.md +41 -0
- package/docs/reviews/implementation/upgrade-reliability-issue-12-review.md +62 -0
- package/docs/reviews/implementation/upgrade-reliability-issue-13-review.md +63 -0
- package/docs/reviews/plans/automatic-agent-maintenance-review.md +56 -0
- package/docs/reviews/plans/plans.xdocs.md +2 -0
- package/docs/superpowers/specs/2026-07-15-upgrade-reliability-design.md +1 -1
- package/docs/todo/automatic-agent-maintenance-implementation.md +55 -0
- package/docs/todo/automatic-agent-maintenance.md +86 -0
- package/docs/todo/bash-installer.md +45 -0
- package/docs/todo/rfc-0034-cli-compliance-migration-implementation.md +27 -3
- package/docs/todo/rfc-0034-cli-compliance-migration.md +9 -4
- package/docs/todo/todo.xdocs.md +12 -2
- package/docs/todo/unicode-help-tree.md +44 -0
- package/docs/todo/upgrade-reliability.md +51 -0
- package/docs/validation/automatic-agent-maintenance.md +63 -0
- package/docs/validation/bash-installer.md +55 -0
- package/docs/validation/rfc-0034-cli-compliance-migration.md +29 -7
- package/docs/validation/unicode-help-tree.md +43 -0
- package/docs/validation/upgrade-reliability-issue-12.md +74 -0
- package/docs/validation/upgrade-reliability-issue-13.md +72 -0
- package/docs/validation/upgrade-reliability.md +38 -99
- package/docs/validation/validation.xdocs.md +14 -4
- package/library/agent-maintenance.d.ts +19 -0
- package/library/agent-maintenance.d.ts.map +1 -0
- package/library/agent-maintenance.js +49 -0
- package/library/agents.d.ts +8 -2
- package/library/agents.d.ts.map +1 -1
- package/library/agents.js +58 -11
- package/library/cli.d.ts.map +1 -1
- package/library/cli.js +50 -15
- package/library/recovery.js +1 -1
- package/library/release-catalog.d.ts +2 -1
- package/library/release-catalog.d.ts.map +1 -1
- package/library/release-catalog.js +9 -1
- package/library/storage.d.ts +2 -1
- package/library/storage.d.ts.map +1 -1
- package/library/storage.js +13 -1
- package/package.json +1 -1
- package/skills/guiho-s-runx/SKILL.md +11 -0
- package/skills/guiho-s-runx/guiho-s-runx.xdocs.md +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,58 @@ owner: runx
|
|
|
15
15
|
|
|
16
16
|
# Changelog
|
|
17
17
|
|
|
18
|
+
## 0.5.1 - 2026-07-19
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Enforced LF line endings for Bash scripts so Windows checkouts preserve
|
|
23
|
+
portable installer syntax.
|
|
24
|
+
- Gave the detached agent-maintenance integration test an explicit
|
|
25
|
+
platform-appropriate timeout so slower Windows CI runners validate the full
|
|
26
|
+
workflow without changing runtime behavior.
|
|
27
|
+
|
|
28
|
+
## 0.5.0 - 2026-07-19
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Added silent detached maintenance that keeps both global `guiho-s-runx`
|
|
33
|
+
skill installations current and reconciles one nearest managed `AGENTS.md`
|
|
34
|
+
block without changing foreground output or exit behavior.
|
|
35
|
+
- Added explicit regression coverage for every upgrade recovery outcome,
|
|
36
|
+
executable stable/prerelease installer verification, and the Unicode
|
|
37
|
+
description-aligned command tree.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- `runx upgrade list` now returns the complete published catalog, including
|
|
42
|
+
labeled prereleases, unless pagination is explicitly requested.
|
|
43
|
+
- The Linux/macOS installer and generated recovery commands now use Bash with
|
|
44
|
+
`set -euo pipefail`; canonical documentation no longer invokes `sh`.
|
|
45
|
+
- Release delivery now validates the exact twelve native binaries and two
|
|
46
|
+
Markdown agent assets and uses only the matching changelog version section
|
|
47
|
+
for GitHub Release notes.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- Fixed `runx upgrade list` and `runx upgrade check` so Citty does not execute
|
|
52
|
+
the parent upgrade action after the selected leaf or append a second JSON
|
|
53
|
+
document.
|
|
54
|
+
- Fixed automatic agent-resource updates to use idempotent atomic writes,
|
|
55
|
+
preserve user-authored instructions, migrate legacy markers, and remain safe
|
|
56
|
+
under concurrent invocations.
|
|
57
|
+
|
|
58
|
+
## 0.4.1 - 2026-07-18
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
|
|
62
|
+
- Fixed `runx upgrade` so its required `--version`, `--arch`, `--variant`,
|
|
63
|
+
`--dry-run`, and `--format` flags route to the upgrade action instead of
|
|
64
|
+
being hidden by the command group or intercepted as the root version flag.
|
|
65
|
+
- Fixed no-argument startup so a decoded cached update notice is printed before
|
|
66
|
+
the exact RunX banner without waiting for network work.
|
|
67
|
+
- Fixed `runx agent prompt list --names` text output so it prints raw prompt
|
|
68
|
+
names, one per line, while JSON mode remains structured.
|
|
69
|
+
|
|
18
70
|
## 0.4.0 - 2026-07-18
|
|
19
71
|
|
|
20
72
|
### Added
|
package/DOCS.md
CHANGED
|
@@ -99,6 +99,14 @@ Skill install, update, and uninstall default to global scope and target both:
|
|
|
99
99
|
|
|
100
100
|
Use `--local` for the corresponding project-local paths.
|
|
101
101
|
|
|
102
|
+
Ordinary RunX commands schedule a hidden detached worker that compares the
|
|
103
|
+
bundled skill with both global copies and rewrites only missing or stale
|
|
104
|
+
targets. The worker finds the nearest existing `AGENTS.md` from effective
|
|
105
|
+
`--cwd`; when none exists in an ancestor, it creates `AGENTS.md` at effective
|
|
106
|
+
cwd. Reconciliation is atomic, migrates legacy RunX markers, preserves content
|
|
107
|
+
outside managed markers, and is silent when no change is required. Worker
|
|
108
|
+
failures never change foreground output or exit status.
|
|
109
|
+
|
|
102
110
|
Instruction actions manage both `AGENTS.md` and `CLAUDE.md` when both exist,
|
|
103
111
|
the existing one when only one exists, and create `AGENTS.md` when neither
|
|
104
112
|
exists. The exact idempotent boundaries are:
|
|
@@ -114,8 +122,10 @@ names; `agent prompt show <id>` prints only the raw prompt.
|
|
|
114
122
|
## Upgrade And Installation
|
|
115
123
|
|
|
116
124
|
`runx upgrade` accepts `--version`, `--arch`, `--variant`, `--dry-run`, and
|
|
117
|
-
`--format`. x64 defaults to `baseline`. `upgrade list`
|
|
118
|
-
|
|
125
|
+
`--format`. x64 defaults to `baseline`. `upgrade list` returns the complete
|
|
126
|
+
published catalog, including labeled prereleases, by default. Positive
|
|
127
|
+
`--page` and `--per-page` values request an explicit view; `--pre-releases` is
|
|
128
|
+
accepted but is unnecessary because prereleases are never hidden.
|
|
119
129
|
|
|
120
130
|
Replacement is transactional: download, native-format validation, backup,
|
|
121
131
|
replacement, version verification, rollback on failure, agent-skill refresh,
|
|
@@ -150,6 +160,16 @@ runx-windows-arm64.exe
|
|
|
150
160
|
runx-windows-x64.exe
|
|
151
161
|
runx-windows-x64-baseline.exe
|
|
152
162
|
runx-windows-x64-modern.exe
|
|
153
|
-
guiho-s-runx
|
|
154
|
-
guiho-i-runx
|
|
163
|
+
guiho-s-runx.md
|
|
164
|
+
guiho-i-runx.md
|
|
155
165
|
```
|
|
166
|
+
|
|
167
|
+
The Markdown suffix is part of the public GitHub Release filename contract.
|
|
168
|
+
Installers retain the standard installed skill filename `SKILL.md` and use the
|
|
169
|
+
instruction asset contents when reconciling managed instruction blocks. Before
|
|
170
|
+
either write, installers reject empty, executable, binary, invalid UTF-8, or
|
|
171
|
+
misidentified Markdown payloads.
|
|
172
|
+
|
|
173
|
+
Release descriptions contain only the matching `## <version> - <date>`
|
|
174
|
+
changelog section. The publish workflow fails closed when that exact section is
|
|
175
|
+
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 |
|
|
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.
|
package/devops/build-binaries.ts
CHANGED
|
@@ -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()
|
package/devops/devops.xdocs.md
CHANGED
|
@@ -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
|
-
|
|
9
|
-
|
|
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 installer progress, resources, exact stable/prerelease normalization, 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:
|
|
13
|
+
install.sh: Bash installer for Linux or Darwin with strict mode, 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
|
|
28
|
-
|
|
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
|
+
}
|
package/devops/install.ps1
CHANGED
|
@@ -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
|
-
|
|
217
|
-
|
|
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
|
|
2
|
-
set -
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
3
|
|
|
4
4
|
REPO="${RUNX_REPO:-CGuiho/runx}"
|
|
5
5
|
VERSION="${RUNX_VERSION:-latest}"
|
|
@@ -106,6 +106,25 @@ verify_native_binary() {
|
|
|
106
106
|
esac
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
+
verify_markdown_asset() {
|
|
110
|
+
path=$1
|
|
111
|
+
expected_name=$2
|
|
112
|
+
[ -s "$path" ] || fail "downloaded Markdown asset is empty: $path"
|
|
113
|
+
magic=$(od -An -tx1 -N2 "$path" 2>/dev/null | tr -d ' \n')
|
|
114
|
+
[ "$magic" != 4d5a ] || fail "downloaded Markdown asset has a Windows executable header: $path"
|
|
115
|
+
if od -An -tx1 "$path" 2>/dev/null | grep -Eq '(^|[[:space:]])00([[:space:]]|$)'; then
|
|
116
|
+
fail "downloaded Markdown asset contains binary NUL bytes: $path"
|
|
117
|
+
fi
|
|
118
|
+
iconv -f UTF-8 -t UTF-8 "$path" >/dev/null 2>&1 || fail "downloaded Markdown asset is not valid UTF-8 text: $path"
|
|
119
|
+
first_line=$(sed -n '1{s/\r$//;p;}' "$path")
|
|
120
|
+
[ "$first_line" = '---' ] || fail "downloaded Markdown asset does not begin with YAML frontmatter: $path"
|
|
121
|
+
awk -v expected="$expected_name" '
|
|
122
|
+
{ sub(/\r$/, "") }
|
|
123
|
+
$0 == "name: " expected { found=1 }
|
|
124
|
+
END { exit(found ? 0 : 1) }
|
|
125
|
+
' "$path" || fail "downloaded Markdown asset identity does not match $expected_name: $path"
|
|
126
|
+
}
|
|
127
|
+
|
|
109
128
|
verify_installed_version() {
|
|
110
129
|
verify_output=$(mktemp "${TMPDIR:-/tmp}/runx-version.XXXXXX") || return 1
|
|
111
130
|
"$1" --version >"$verify_output" 2>&1 &
|
|
@@ -135,10 +154,10 @@ verify_installed_version() {
|
|
|
135
154
|
ensure_path() {
|
|
136
155
|
PATH="$INSTALL_DIR:$PATH"; export PATH
|
|
137
156
|
profile="$HOME/.profile"
|
|
138
|
-
case "${SHELL
|
|
157
|
+
case "${SHELL:-bash}" in */zsh|zsh) profile="$HOME/.zshrc" ;; */bash|bash) profile="$HOME/.bashrc" ;; */fish|fish) profile="$HOME/.config/fish/config.fish" ;; esac
|
|
139
158
|
mkdir -p -- "$(dirname "$profile")"
|
|
140
159
|
if ! grep -Fq "$INSTALL_DIR" "$profile" 2>/dev/null; then
|
|
141
|
-
if [ "${SHELL
|
|
160
|
+
if [ "${SHELL:-bash}" = fish ] || [ "${SHELL:-bash}" = */fish ]; then printf '\n# Added by runx installer\nfish_add_path %s\n' "$INSTALL_DIR" >>"$profile"
|
|
142
161
|
else printf '\n# Added by runx installer\nexport PATH=%s:$PATH\n' "$INSTALL_DIR" >>"$profile"; fi
|
|
143
162
|
fi
|
|
144
163
|
}
|
|
@@ -183,15 +202,17 @@ install_transactional() {
|
|
|
183
202
|
|
|
184
203
|
install_agent_assets() {
|
|
185
204
|
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"
|
|
205
|
+
skill_url="${tag_base%/}/${encoded_tag}/guiho-s-runx.md"
|
|
206
|
+
prompt_url="${tag_base%/}/${encoded_tag}/guiho-i-runx.md"
|
|
188
207
|
printf 'Downloading skill asset: %s\n' "$skill_url"
|
|
189
|
-
download_asset "$skill_url" "$TMP/guiho-s-runx" || fail 'could not download guiho-s-runx'
|
|
208
|
+
download_asset "$skill_url" "$TMP/guiho-s-runx.md" || fail 'could not download guiho-s-runx.md'
|
|
209
|
+
verify_markdown_asset "$TMP/guiho-s-runx.md" 'guiho-s-runx'
|
|
190
210
|
printf 'Downloading instruction asset: %s\n' "$prompt_url"
|
|
191
|
-
download_asset "$prompt_url" "$TMP/guiho-i-runx" || fail 'could not download guiho-i-runx'
|
|
211
|
+
download_asset "$prompt_url" "$TMP/guiho-i-runx.md" || fail 'could not download guiho-i-runx.md'
|
|
212
|
+
verify_markdown_asset "$TMP/guiho-i-runx.md" 'guiho-i-runx'
|
|
192
213
|
for skill_root in "$HOME/.agents/skills/guiho-s-runx" "$HOME/.claude/skills/guiho-s-runx"; do
|
|
193
214
|
mkdir -p -- "$skill_root"
|
|
194
|
-
install -m 0644 "$TMP/guiho-s-runx" "$skill_root/SKILL.md"
|
|
215
|
+
install -m 0644 "$TMP/guiho-s-runx.md" "$skill_root/SKILL.md"
|
|
195
216
|
printf 'Installed skill: %s\n' "$skill_root/SKILL.md"
|
|
196
217
|
done
|
|
197
218
|
targets=""
|
|
@@ -214,7 +235,7 @@ install_agent_assets() {
|
|
|
214
235
|
cat "$clean_file"
|
|
215
236
|
[ ! -s "$clean_file" ] || printf '\n'
|
|
216
237
|
printf '<!-- BEGIN RUNX — DO NOT EDIT THIS SECTION -->\n'
|
|
217
|
-
cat "$TMP/guiho-i-runx"
|
|
238
|
+
cat "$TMP/guiho-i-runx.md"
|
|
218
239
|
printf '\n<!-- END RUNX -->\n'
|
|
219
240
|
} >"$instruction_file"
|
|
220
241
|
done
|
|
@@ -222,7 +243,7 @@ install_agent_assets() {
|
|
|
222
243
|
|
|
223
244
|
main() {
|
|
224
245
|
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
|
|
246
|
+
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
247
|
OS=$(detect_os); ARCH=$(detect_arch); TARGET_VERSION=$(resolve_target_version)
|
|
227
248
|
TMP=$(mktemp -d "${TMPDIR:-/tmp}/runx-install.XXXXXX")
|
|
228
249
|
mkdir -p -- "$INSTALL_DIR"
|
|
@@ -256,4 +277,8 @@ main() {
|
|
|
256
277
|
printf 'Installed and verified RunX %s at %s\n' "$TARGET_VERSION" "$destination"
|
|
257
278
|
}
|
|
258
279
|
|
|
280
|
+
if [ "${RUNX_INSTALLER_SOURCE_ONLY:-0}" = 1 ]; then
|
|
281
|
+
return 0 2>/dev/null || exit 0
|
|
282
|
+
fi
|
|
283
|
+
|
|
259
284
|
main "$@"
|
|
@@ -3,29 +3,176 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from 'bun:test'
|
|
6
|
+
import { $ } from 'bun'
|
|
6
7
|
|
|
7
8
|
describe('RunX direct installers', () => {
|
|
8
9
|
test('PowerShell installer exposes the complete RFC sequence', async () => {
|
|
9
10
|
const script = await Bun.file(new URL('./install.ps1', import.meta.url)).text()
|
|
10
11
|
expect(script).toContain('Initiating GUIHO CLI Upgrade / Installation Sequence...')
|
|
11
|
-
expect(script).toContain('guiho-s-runx')
|
|
12
|
-
expect(script).toContain('guiho-i-runx')
|
|
12
|
+
expect(script).toContain('guiho-s-runx.md')
|
|
13
|
+
expect(script).toContain('guiho-i-runx.md')
|
|
13
14
|
expect(script).toContain('.agents\\skills\\guiho-s-runx')
|
|
14
15
|
expect(script).toContain('.claude\\skills\\guiho-s-runx')
|
|
15
16
|
expect(script).toContain('BEGIN RUNX — DO NOT EDIT THIS SECTION')
|
|
16
17
|
expect(script).toContain('Test-NativeBinary')
|
|
17
18
|
expect(script).toContain('Install-Transactional')
|
|
19
|
+
expect(script).toContain('function Test-MarkdownAsset')
|
|
20
|
+
expect(script).toContain("Test-MarkdownAsset -Path $skillAsset -ExpectedName 'guiho-s-runx'")
|
|
21
|
+
expect(script).toContain("Test-MarkdownAsset -Path $promptAsset -ExpectedName 'guiho-i-runx'")
|
|
22
|
+
expect(script).toContain('Windows executable header')
|
|
23
|
+
expect(script).toContain('binary NUL bytes')
|
|
24
|
+
expect(script).toContain('not valid UTF-8 text')
|
|
18
25
|
})
|
|
19
26
|
|
|
20
|
-
test('
|
|
27
|
+
test('Bash installer selects Darwin assets and has no runtime Bun dependency', async () => {
|
|
21
28
|
const script = await Bun.file(new URL('./install.sh', import.meta.url)).text()
|
|
29
|
+
expect(script).toStartWith('#!/usr/bin/env bash\nset -euo pipefail')
|
|
22
30
|
expect(script).toContain("Darwin) printf 'darwin")
|
|
23
31
|
expect(script).toContain('--progress-bar')
|
|
24
|
-
expect(script).toContain('guiho-s-runx')
|
|
25
|
-
expect(script).toContain('guiho-i-runx')
|
|
32
|
+
expect(script).toContain('guiho-s-runx.md')
|
|
33
|
+
expect(script).toContain('guiho-i-runx.md')
|
|
26
34
|
expect(script).toContain('.agents/skills/guiho-s-runx')
|
|
27
35
|
expect(script).toContain('.claude/skills/guiho-s-runx')
|
|
36
|
+
expect(script).toContain('verify_markdown_asset()')
|
|
37
|
+
expect(script).toContain("verify_markdown_asset \"$TMP/guiho-s-runx.md\" 'guiho-s-runx'")
|
|
38
|
+
expect(script).toContain("verify_markdown_asset \"$TMP/guiho-i-runx.md\" 'guiho-i-runx'")
|
|
39
|
+
expect(script).toContain('Windows executable header')
|
|
40
|
+
expect(script).toContain('binary NUL bytes')
|
|
41
|
+
expect(script).toContain('iconv -f UTF-8 -t UTF-8')
|
|
28
42
|
expect(script).toContain('BEGIN RUNX — DO NOT EDIT THIS SECTION')
|
|
29
43
|
expect(script).not.toContain(' bun ')
|
|
30
44
|
})
|
|
45
|
+
|
|
46
|
+
test('Bash installer passes syntax, piped startup, exact versions, and executable verification', async () => {
|
|
47
|
+
const bash = await bashExecutable()
|
|
48
|
+
const script = Bun.fileURLToPath(new URL('./install.sh', import.meta.url))
|
|
49
|
+
const syntax = Bun.spawnSync([bash, '-n', script])
|
|
50
|
+
expect(syntax.exitCode).toBe(0)
|
|
51
|
+
|
|
52
|
+
const piped = Bun.spawnSync([
|
|
53
|
+
bash,
|
|
54
|
+
'-c',
|
|
55
|
+
'cat "$1" | RUNX_INSTALLER_SOURCE_ONLY=1 bash -s --',
|
|
56
|
+
'bash',
|
|
57
|
+
script,
|
|
58
|
+
])
|
|
59
|
+
expect(piped.exitCode).toBe(0)
|
|
60
|
+
expect(piped.stderr.toString()).toBe('')
|
|
61
|
+
|
|
62
|
+
const functions = Bun.spawnSync([
|
|
63
|
+
bash,
|
|
64
|
+
'-c',
|
|
65
|
+
[
|
|
66
|
+
'RUNX_INSTALLER_SOURCE_ONLY=1 source "$1"',
|
|
67
|
+
'stable=$(normalize_version v1.2.3)',
|
|
68
|
+
'prerelease=$(normalize_version @guiho/runx@1.3.0-alpha.2)',
|
|
69
|
+
'test "$stable" = 1.2.3',
|
|
70
|
+
'test "$prerelease" = 1.3.0-alpha.2',
|
|
71
|
+
'TARGET_VERSION=$(bun --version)',
|
|
72
|
+
'verify_installed_version "$(command -v bun)"',
|
|
73
|
+
].join('; '),
|
|
74
|
+
'bash',
|
|
75
|
+
script,
|
|
76
|
+
])
|
|
77
|
+
expect(functions.exitCode).toBe(0)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('platform installer rejects a PE payload disguised as Markdown', async () => {
|
|
81
|
+
const temporaryDirectory = `${Bun.env.TEMP ?? Bun.env.TMPDIR ?? '/tmp'}/runx-markdown-${crypto.randomUUID()}`
|
|
82
|
+
const validAsset = `${temporaryDirectory}/valid.md`
|
|
83
|
+
const executableAsset = `${temporaryDirectory}/payload.md`
|
|
84
|
+
await $`mkdir -p ${temporaryDirectory}`
|
|
85
|
+
try {
|
|
86
|
+
await Bun.write(validAsset, '---\nname: guiho-s-runx\n---\n\n# Skill\n')
|
|
87
|
+
await Bun.write(executableAsset, new Uint8Array([0x4d, 0x5a, 0x00, 0x00]))
|
|
88
|
+
|
|
89
|
+
if (process.platform === 'win32') {
|
|
90
|
+
const script = Bun.fileURLToPath(new URL('./install.ps1', import.meta.url))
|
|
91
|
+
const valid = Bun.spawnSync([
|
|
92
|
+
'powershell',
|
|
93
|
+
'-NoLogo',
|
|
94
|
+
'-NoProfile',
|
|
95
|
+
'-NonInteractive',
|
|
96
|
+
'-ExecutionPolicy',
|
|
97
|
+
'Bypass',
|
|
98
|
+
'-Command',
|
|
99
|
+
`$env:RUNX_INSTALLER_SOURCE_ONLY='1'; . '${script.replaceAll("'", "''")}'; Test-MarkdownAsset -Path '${validAsset.replaceAll("'", "''")}' -ExpectedName 'guiho-s-runx'`,
|
|
100
|
+
])
|
|
101
|
+
const invalid = Bun.spawnSync([
|
|
102
|
+
'powershell',
|
|
103
|
+
'-NoLogo',
|
|
104
|
+
'-NoProfile',
|
|
105
|
+
'-NonInteractive',
|
|
106
|
+
'-ExecutionPolicy',
|
|
107
|
+
'Bypass',
|
|
108
|
+
'-Command',
|
|
109
|
+
`$env:RUNX_INSTALLER_SOURCE_ONLY='1'; . '${script.replaceAll("'", "''")}'; Test-MarkdownAsset -Path '${executableAsset.replaceAll("'", "''")}' -ExpectedName 'guiho-s-runx'`,
|
|
110
|
+
])
|
|
111
|
+
expect(valid.exitCode).toBe(0)
|
|
112
|
+
expect(invalid.exitCode).not.toBe(0)
|
|
113
|
+
expect(invalid.stderr.toString()).toContain('Windows executable header')
|
|
114
|
+
} else {
|
|
115
|
+
const script = Bun.fileURLToPath(new URL('./install.sh', import.meta.url))
|
|
116
|
+
const bash = await bashExecutable()
|
|
117
|
+
const valid = Bun.spawnSync([
|
|
118
|
+
bash,
|
|
119
|
+
'-c',
|
|
120
|
+
`RUNX_INSTALLER_SOURCE_ONLY=1 . "$1"; verify_markdown_asset "$2" guiho-s-runx`,
|
|
121
|
+
'bash',
|
|
122
|
+
script,
|
|
123
|
+
validAsset,
|
|
124
|
+
])
|
|
125
|
+
const invalid = Bun.spawnSync([
|
|
126
|
+
bash,
|
|
127
|
+
'-c',
|
|
128
|
+
`RUNX_INSTALLER_SOURCE_ONLY=1 . "$1"; verify_markdown_asset "$2" guiho-s-runx`,
|
|
129
|
+
'bash',
|
|
130
|
+
script,
|
|
131
|
+
executableAsset,
|
|
132
|
+
])
|
|
133
|
+
expect(valid.exitCode).toBe(0)
|
|
134
|
+
expect(invalid.exitCode).not.toBe(0)
|
|
135
|
+
expect(invalid.stderr.toString()).toContain('Windows executable header')
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
await $`rm -rf ${temporaryDirectory}`
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
if (process.platform === 'win32') {
|
|
143
|
+
test('PowerShell installer accepts exact stable and prerelease versions and verifies the executable', () => {
|
|
144
|
+
const script = Bun.fileURLToPath(new URL('./install.ps1', import.meta.url)).replaceAll("'", "''")
|
|
145
|
+
const bun = process.execPath.replaceAll("'", "''")
|
|
146
|
+
const command = [
|
|
147
|
+
"$env:RUNX_INSTALLER_SOURCE_ONLY='1'",
|
|
148
|
+
`. '${script}'`,
|
|
149
|
+
"$stable = Resolve-TargetVersion -RequestedVersion 'v1.2.3'",
|
|
150
|
+
"$prerelease = Resolve-TargetVersion -RequestedVersion '@guiho/runx@1.3.0-alpha.2'",
|
|
151
|
+
"if ($stable -ne '1.2.3' -or $prerelease -ne '1.3.0-alpha.2') { throw 'exact version normalization failed' }",
|
|
152
|
+
`Test-InstalledVersion -Path '${bun}' -ExpectedVersion '${process.versions.bun}'`,
|
|
153
|
+
"$mismatchRejected = $false",
|
|
154
|
+
`try { Test-InstalledVersion -Path '${bun}' -ExpectedVersion '0.0.0-impossible' } catch { $mismatchRejected = $true }`,
|
|
155
|
+
"if (-not $mismatchRejected) { throw 'version mismatch was not rejected' }",
|
|
156
|
+
].join('; ')
|
|
157
|
+
const result = Bun.spawnSync([
|
|
158
|
+
'powershell',
|
|
159
|
+
'-NoLogo',
|
|
160
|
+
'-NoProfile',
|
|
161
|
+
'-NonInteractive',
|
|
162
|
+
'-ExecutionPolicy',
|
|
163
|
+
'Bypass',
|
|
164
|
+
'-Command',
|
|
165
|
+
command,
|
|
166
|
+
])
|
|
167
|
+
expect(result.exitCode).toBe(0)
|
|
168
|
+
})
|
|
169
|
+
}
|
|
31
170
|
})
|
|
171
|
+
|
|
172
|
+
async function bashExecutable(): Promise<string> {
|
|
173
|
+
const gitBash = 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
174
|
+
if (process.platform === 'win32' && await Bun.file(gitBash).exists()) return gitBash
|
|
175
|
+
const bash = Bun.which('bash')
|
|
176
|
+
if (!bash) throw new Error('Bash is required to validate devops/install.sh.')
|
|
177
|
+
return bash
|
|
178
|
+
}
|
|
@@ -14,4 +14,18 @@ if (JSON.stringify(observed) !== JSON.stringify(expected)) {
|
|
|
14
14
|
throw new Error(`Expected exactly ${expected.length} release assets.\nExpected: ${expected.join(', ')}\nObserved: ${observed.join(', ')}`)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
for (const [assetName, expectedName] of [
|
|
18
|
+
['guiho-s-runx.md', 'guiho-s-runx'],
|
|
19
|
+
['guiho-i-runx.md', 'guiho-i-runx'],
|
|
20
|
+
] as const) {
|
|
21
|
+
const bytes = await Bun.file(joinPath(bin, assetName)).bytes()
|
|
22
|
+
if (bytes.length === 0 || (bytes[0] === 0x4d && bytes[1] === 0x5a) || bytes.includes(0)) {
|
|
23
|
+
throw new Error(`Release Markdown asset is empty or binary: ${assetName}`)
|
|
24
|
+
}
|
|
25
|
+
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes).replace(/\r\n/g, '\n')
|
|
26
|
+
if (!text.startsWith('---\n') || !new RegExp(`^name:\\s*${expectedName}\\s*$`, 'm').test(text)) {
|
|
27
|
+
throw new Error(`Release Markdown asset has invalid frontmatter identity: ${assetName}`)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
process.stdout.write(`${JSON.stringify({ count: observed.length, assets: observed }, null, 2)}\n`)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
subject: runx-architecture
|
|
3
|
-
description: Technical architecture for the RunX manifest, Citty command tree, agent
|
|
3
|
+
description: Technical architecture for the RunX manifest, Citty command tree, automatic agent maintenance, and native distribution.
|
|
4
4
|
parent: runx-docs
|
|
5
5
|
children: []
|
|
6
6
|
files:
|
|
@@ -14,6 +14,7 @@ keywords:
|
|
|
14
14
|
- bun
|
|
15
15
|
- cli
|
|
16
16
|
- citty
|
|
17
|
+
- agent maintenance
|
|
17
18
|
flags: []
|
|
18
19
|
status: stable
|
|
19
20
|
---
|