diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c9d5fb..4506e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,16 @@ a CI job fails when they disagree. ## [Unreleased] +### Fixed + +- **`fleet skill` failed on an installed 0.8.0.** The 0.8.0 archives carry + `skills/`, but `install.sh` and `install.ps1` copy a release's directories + by name and did not name it, so `fleet skill show` and `fleet skill install` + could not find `SKILL.md`. Both installers now copy it, and a test fails if + the bundle gains a directory the installers do not copy. The installers are + fetched from `main`, so re-running the install command fixes an existing + 0.8.0 install; no new release is needed. + ## [0.8.0] — 2026-10-15 The fleet as something a program or an AI agent can drive: a versioned control diff --git a/fleet/test/installers.test.ts b/fleet/test/installers.test.ts new file mode 100644 index 0000000..f7df779 --- /dev/null +++ b/fleet/test/installers.test.ts @@ -0,0 +1,45 @@ +/** + * The installers copy a release's directories by name rather than replacing the + * install directory, because the install directory is also FLEET_HOME and holds + * the fleet's data. So a directory build.mjs adds to the bundle is dropped on + * install unless both installers name it too. + */ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { FLEET } from "./helpers.js"; + +const read = (rel: string) => readFileSync(path.join(FLEET, rel), "utf8"); + +function bundleEntries(): string[] { + const src = read("build.mjs"); + const names = new Set(); + for (const m of src.matchAll(/path\.join\(OUT, "([a-z-]+)/g)) names.add(m[1]); + for (const m of src.matchAll(/^\s*\["[^"]+", "([a-z-]+)"\],?$/gm)) names.add(m[1]); + return [...names].sort(); +} + +function shEntries(): string[] { + const m = /for entry in ([a-z -]+); do/.exec(read("../install.sh")); + assert.ok(m, "install.sh: the `for entry in ...` copy loop was not found"); + return m[1].trim().split(/\s+/).sort(); +} + +function ps1Entries(): string[] { + const m = /foreach \(\$entry in @\(([^)]*)\)\)/.exec(read("../install.ps1")); + assert.ok(m, "install.ps1: the `foreach ($entry in @(...))` copy loop was not found"); + return [...m[1].matchAll(/'([a-z-]+)'/g)].map((x) => x[1]).sort(); +} + +test("build.mjs's top-level entries are found", () => { + assert.ok(bundleEntries().includes("bin"), bundleEntries().join(" ")); +}); + +test("install.sh copies every directory the bundle has", () => { + assert.deepEqual(shEntries(), bundleEntries()); +}); + +test("install.ps1 copies every directory the bundle has", () => { + assert.deepEqual(ps1Entries(), bundleEntries()); +}); diff --git a/install.ps1 b/install.ps1 index 3c1cbfe..ab8f3ff 100644 --- a/install.ps1 +++ b/install.ps1 @@ -241,7 +241,7 @@ function Install-Fleet { # recorded -- and an upgrade that deletes your history is a worse # outcome than an upgrade that fails. New-Item -ItemType Directory -Path $dir -Force | Out-Null - foreach ($entry in @('bin', 'runner-web', 'dash', 'examples', 'schemas')) { + foreach ($entry in @('bin', 'runner-web', 'dash', 'examples', 'schemas', 'skills')) { $from = Join-Path $root $entry if (-not (Test-Path -LiteralPath $from)) { continue } $to = Join-Path $dir $entry diff --git a/install.sh b/install.sh index 49480d5..24898fd 100755 --- a/install.sh +++ b/install.sh @@ -314,7 +314,7 @@ main() { # with it -- every result the fleet has ever recorded -- and an upgrade that # deletes your history is a worse outcome than an upgrade that fails. mkdir -p "$dir" - for entry in bin runner-web dash examples schemas; do + for entry in bin runner-web dash examples schemas skills; do if [ -d "${root}/${entry}" ]; then rm -rf "${dir:?}/${entry}" cp -R "${root}/${entry}" "${dir}/${entry}"