Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .agents/skills/mops-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,12 @@ mops toolchain use lintoko 0.10.0 # pin specific version
mops toolchain use pocket-ic 12.0.0 # pin for replica tests / benchmarks (pin a specific version; `latest` may resolve to one the bundled pic-js client doesn't support)
mops toolchain update moc # update to latest (requires existing [toolchain] entry)
mops toolchain update # update all tools to latest
mops toolchain info <tool> # show release info (latest, pinned, history)
mops toolchain info <tool> --versions # list all stable releases (scripting)
mops toolchain bin moc # print path to binary
```

**Agent note**: `toolchain use <tool>` without a version opens an interactive picker — do not use in scripts or agents. Always pass a version or `latest`. `toolchain update` only works when the tool already has a `[toolchain]` entry.
**Agent note**: `toolchain use <tool>` without a version opens an interactive picker — do not use in scripts or agents. Always pass a version or `latest`. `toolchain update` only works when the tool already has a `[toolchain]` entry. `toolchain info <tool> --versions` works without `mops.toml`; set `GITHUB_TOKEN` when listing tools with long release histories.

### Enhanced migrations

Expand Down
3 changes: 3 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Next

- `mops toolchain info <tool> --versions` lists all stable GitHub release versions for a toolchain tool (moc, lintoko, wasmtime, pocket-ic), one per line — for scripting and cache warming.
- `mops toolchain info <tool>` shows latest stable release, pinned version, and recent version history.

## 2.16.1

- Fix `mops bench` crashing on moc 0.15+ with the default `--gc copying`: `--copying-gc` is rejected under enhanced orthogonal persistence, which became the default persistence mode in 2.16.0. The default GC is now `incremental` (moc's default and the only collector available under EOP). Selecting a legacy collector (`copying`, `compacting`, `generational`) now implies `--legacy-persistence`, since moc only accepts them there.
Expand Down
12 changes: 12 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,18 @@ toolchainCommand
await toolchain.update(tool);
});

toolchainCommand
.command("info")
.description("Show release information about a toolchain tool")
.addArgument(new Argument("<tool>", "tool to look up").choices(TOOLCHAINS))
.option(
"--versions",
"List all stable release versions, one per line (oldest to newest)",
)
.action(async (tool: Tool, options) => {
await toolchain.info(tool, options);
});

toolchainCommand
.command("bin")
.description("Get path to the tool binary")
Expand Down
97 changes: 78 additions & 19 deletions cli/commands/toolchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import * as pocketIc from "./pocket-ic.js";
import * as wasmtime from "./wasmtime.js";
import * as lintoko from "./lintoko.js";
import { FILE_PATH_REGEX } from "../../constants.js";
import * as toolchainUtils from "./toolchain-utils.js";
import type { ReleaseInfo } from "./release-tags.js";

function label(text: string): string {
return chalk.bold(text.padEnd(16));
}

export interface ToolchainInfoOptions {
versions?: boolean;
}

function getToolUtils(tool: Tool) {
if (tool === "moc") {
Expand Down Expand Up @@ -250,25 +260,23 @@ async function promptVersion(tool: Tool): Promise<string> {
type: "select",
name: "version",
message: `Select ${tool} version`,
choices: releases.map(
(
release: { published_at: string | number | Date; tag_name: string },
i: any,
) => {
let date = new Date(release.published_at).toLocaleDateString(
undefined,
{ year: "numeric", month: "short", day: "numeric" },
);
return {
title:
release.tag_name +
chalk.gray(
` ${date}${currentIndex === i ? chalk.italic(" (current)") : ""}`,
),
value: release.tag_name,
};
},
),
choices: releases.map((release: ReleaseInfo, i) => {
let date = release.published_at
? new Date(release.published_at).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})
: "";
return {
title:
release.tag_name +
chalk.gray(
` ${date}${currentIndex === i ? chalk.italic(" (current)") : ""}`,
),
value: release.tag_name,
};
}),
initial: currentIndex == -1 ? 0 : currentIndex,
});

Expand Down Expand Up @@ -347,6 +355,56 @@ async function update(tool?: Tool) {
}
}

async function info(tool: Tool, options: ToolchainInfoOptions = {}) {
let toolUtils = getToolUtils(tool);

if (options.versions) {
let versions = await toolchainUtils.getAllReleaseTags(toolUtils.repo);
for (let ver of versions) {
console.log(ver);
}
return;
}

let [versions, latest] = await Promise.all([
toolchainUtils.getAllReleaseTags(toolUtils.repo),
toolUtils.getLatestReleaseTag(),
]);

let configFile = getClosestConfigFile();
let pinned = configFile
? readConfig(configFile).toolchain?.[tool]
: undefined;

console.log("");
console.log(chalk.green.bold(tool));

if (latest) {
console.log(chalk.yellow(`latest: ${latest}`));
}

if (pinned) {
console.log(`${label("pinned")}${pinned}`);
}

console.log("");
console.log(
`${label("repository")}${chalk.cyan(`https://github.com/${toolUtils.repo}`)}`,
);

if (versions.length > 0) {
let versionsDisplay = versions.slice(-10).reverse().join(", ");
let extra =
versions.length > 10
? ` ${chalk.gray(`(+${versions.length - 10} more)`)}`
: "";
console.log("");
console.log(`${label("versions")}${versionsDisplay}${extra}`);
}

console.log("");
}

// return current version from mops.toml
async function bin(tool: Tool, { fallback = false } = {}): Promise<string> {
let hasConfig = getClosestConfigFile();
Expand Down Expand Up @@ -399,6 +457,7 @@ export let toolchain = {
use,
update,
bin,
info,
installAll,
checkToolchainInited,
};
26 changes: 26 additions & 0 deletions cli/commands/toolchain/release-tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SemVer } from "semver";

export type ReleaseInfo = {
tag_name: string;
published_at: string | null;
prerelease: boolean;
draft: boolean;
};

export let sortReleaseTags = (tags: string[]): string[] => {
return [...tags].sort((a, b) => {
try {
return new SemVer(a).compare(new SemVer(b));
} catch {
return a.localeCompare(b);
}
});
};

export let stableReleaseTags = (releases: ReleaseInfo[]): string[] => {
return sortReleaseTags(
releases
.filter((release) => !release.draft && !release.prerelease)
.map((release) => release.tag_name),
);
};
100 changes: 85 additions & 15 deletions cli/commands/toolchain/toolchain-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import { Octokit } from "octokit";
import { extract as extractTar } from "tar";

import { getRootDir } from "../../mops.js";
import { stableReleaseTags, type ReleaseInfo } from "./release-tags.js";

export type { ReleaseInfo } from "./release-tags.js";
export { sortReleaseTags, stableReleaseTags } from "./release-tags.js";

export const TOOLCHAINS = ["moc", "wasmtime", "pocket-ic", "lintoko"];

Expand Down Expand Up @@ -75,19 +79,78 @@ export let downloadAndExtract = async (
deleteSync([tmpDir], { force: true });
};

export let getAllReleases = async (repo: string): Promise<ReleaseInfo[]> => {
let octokit = new Octokit();
let releases: ReleaseInfo[] = [];

for (let page = 1; ; page++) {
let res = await octokit.request(`GET /repos/${repo}/releases`, {
per_page: 100,
page,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (res.status !== 200) {
console.error("Releases fetch error");
process.exit(1);
}
if (res.data.length === 0) {
break;
}
for (let release of res.data) {
releases.push({
tag_name: release.tag_name.replace(/^v/, ""),
published_at: release.published_at,
prerelease: release.prerelease,
draft: release.draft,
});
}
if (res.data.length < 100) {
break;
}
}

return releases;
};

export let getAllReleaseTags = async (repo: string): Promise<string[]> => {
return stableReleaseTags(await getAllReleases(repo));
};

export let getLatestReleaseTag = async (repo: string): Promise<string> => {
let releases = await getReleases(repo);
let release = releases.find(
(release: any) => !release.prerelease && !release.draft,
);
if (!release?.tag_name) {
console.error(`Failed to fetch latest release tag for ${repo}`);
process.exit(1);
let octokit = new Octokit();

for (let page = 1; ; page++) {
let res = await octokit.request(`GET /repos/${repo}/releases`, {
per_page: 100,
page,
headers: {
"X-GitHub-Api-Version": "2022-11-28",
},
});
if (res.status !== 200) {
console.error("Releases fetch error");
process.exit(1);
}
if (res.data.length === 0) {
break;
}
for (let release of res.data) {
if (!release.draft && !release.prerelease) {
return release.tag_name.replace(/^v/, "");
}
}
if (res.data.length < 100) {
break;
}
}
return release.tag_name.replace(/^v/, "");

console.error(`Failed to fetch latest release tag for ${repo}`);
process.exit(1);
};

export let getReleases = async (repo: string) => {
export let getReleases = async (repo: string): Promise<ReleaseInfo[]> => {
let octokit = new Octokit();
let res = await octokit.request(`GET /repos/${repo}/releases`, {
per_page: 10,
Expand All @@ -96,13 +159,20 @@ export let getReleases = async (repo: string) => {
},
});
if (res.status !== 200) {
console.log("Releases fetch error");
console.error("Releases fetch error");
process.exit(1);
}
return res.data.map((release: any) => {
return {
...release,
return res.data.map(
(release: {
tag_name: string;
published_at: string | null;
prerelease: boolean;
draft: boolean;
}): ReleaseInfo => ({
tag_name: release.tag_name.replace(/^v/, ""),
};
});
published_at: release.published_at,
prerelease: release.prerelease,
draft: release.draft,
}),
);
};
42 changes: 42 additions & 0 deletions cli/tests/release-tags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { describe, expect, test } from "@jest/globals";
import {
sortReleaseTags,
stableReleaseTags,
type ReleaseInfo,
} from "../commands/toolchain/release-tags";

describe("toolchain-utils release tags", () => {
test("sortReleaseTags orders semver ascending", () => {
expect(sortReleaseTags(["1.2.0", "0.9.0", "1.10.0", "1.0.0"])).toEqual([
"0.9.0",
"1.0.0",
"1.2.0",
"1.10.0",
]);
});

test("stableReleaseTags excludes drafts and prereleases", () => {
let releases: ReleaseInfo[] = [
{
tag_name: "1.1.0",
published_at: "2024-01-03T00:00:00Z",
prerelease: false,
draft: false,
},
{
tag_name: "1.2.0-beta",
published_at: "2024-01-02T00:00:00Z",
prerelease: true,
draft: false,
},
{
tag_name: "1.0.0",
published_at: "2024-01-01T00:00:00Z",
prerelease: false,
draft: true,
},
];

expect(stableReleaseTags(releases)).toEqual(["1.1.0"]);
});
});
1 change: 1 addition & 0 deletions docs/docs/cli/5-toolchain/01-toolchain-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,6 @@ lintoko = "../custom-lintoko/bin/lintoko"
- [`mops toolchain init`](/cli/mops-toolchain-init)
- [`mops toolchain use`](/cli/mops-toolchain-use)
- [`mops toolchain update`](/cli/mops-toolchain-update)
- [`mops toolchain info`](/cli/mops-toolchain-info)
- [`mops toolchain bin`](/cli/mops-toolchain-bin)
- [`mops toolchain reset`](/cli/mops-toolchain-reset)
Loading
Loading