mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 05:31:46 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The release channels (canary → nightly → beta → stable) select by install target, and users discover them today only through maintainer-oriented docs > - A user who wants to know "which lane am I on, and what else is there" has no self-serve answer > - The channel rollout planned a read-only CLI command for exactly this > - This pull request adds `paperclipai channels`: every lane with the version its dist-tag resolves to, the install command for it, and which lane the running install follows > - The benefit is self-serve lane discovery without reading release documentation ## Linked Issues or Issue Description Refs #11008 — the user-facing discovery surface for the channel model completed there. **Subsystem affected** CLI: `cli/src/commands/channels.ts` (new), `cli/src/index.ts`, `doc/CHANNELS.md`, tests. **Problem or motivation** Channel selection is install-based (`@latest` / `@beta` / `@nightly` / `@canary`), but nothing in the product tells a user which channel their install follows or what the other lanes currently resolve to. The information lives in `doc/CHANNELS.md` and the npm registry, neither of which a running install surfaces. **Proposed solution** A read-only `paperclipai channels` command: prints each channel with the version its dist-tag currently resolves to (per-lane registry lookups that degrade to `unavailable` individually), the install command for each, and the running install's lane parsed from its version suffix — source checkouts carry the repository's placeholder version and are reported as unmapped rather than guessed. `--json` emits the same data for scripting. ## What Changed - `cli/src/commands/channels.ts` (new): channel table, lane parsing, registry resolution, human and `--json` output - `cli/src/index.ts`: registers `channels` - `doc/CHANNELS.md`: "Seeing where you are" section - `cli/src/__tests__/channels.test.ts` (new): lane parsing including unknown versions, full resolution against a fake runner, per-lane degradation, table/dist-tag sync ## Verification - `vitest run cli/src/__tests__/channels.test.ts`: 5 pass - `pnpm typecheck` in `cli/` - Live run against the real registry shows all four lanes with their current versions (`2026.722.0` / `2026.811.0-beta.0` / `2026.811.0-nightly.0` / canary) and correctly reports a source checkout as unmapped ## Risks - Low. Read-only command reusing the existing `resolvePublishedVersion` registry helper; no state, no auth, no publish surface ## Model Used Claude Fable 5 (`claude-fable-5`, Anthropic) in Claude Code, with extended thinking and full tool use. All changes model-authored under human direction. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green (pending — will confirm before merge) - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups (pending — will confirm before merge) - [x] I will address all Greptile and reviewer comments before requesting merge
82 lines
2.8 KiB
TypeScript
82 lines
2.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
channelForVersion,
|
|
collectChannelState,
|
|
RELEASE_CHANNELS,
|
|
} from "../commands/channels.js";
|
|
import type { CommandRunner } from "../commands/install.js";
|
|
|
|
describe("channelForVersion", () => {
|
|
it("maps published versions to their lane", () => {
|
|
expect(channelForVersion("2026.811.0")).toBe("stable");
|
|
expect(channelForVersion("2026.811.0-beta.0")).toBe("beta");
|
|
expect(channelForVersion("2026.811.0-nightly.0")).toBe("nightly");
|
|
expect(channelForVersion("2026.811.0-canary.3")).toBe("canary");
|
|
});
|
|
|
|
it("treats non-CalVer versions as unknown instead of guessing", () => {
|
|
expect(channelForVersion("0.3.1")).toBe("unknown");
|
|
expect(channelForVersion("2026.811.0-rc.1")).toBe("unknown");
|
|
expect(channelForVersion("garbage")).toBe("unknown");
|
|
});
|
|
});
|
|
|
|
describe("collectChannelState", () => {
|
|
const versionsByTag: Record<string, string> = {
|
|
latest: "2026.722.0",
|
|
beta: "2026.811.0-beta.0",
|
|
nightly: "2026.811.0-nightly.0",
|
|
canary: "2026.811.0-canary.3",
|
|
};
|
|
|
|
const fakeRunner: CommandRunner = async (_command, args) => {
|
|
const spec = (args ?? []).find((arg) => arg.startsWith("paperclipai@"));
|
|
const tag = spec?.slice("paperclipai@".length) ?? "";
|
|
const version = versionsByTag[tag];
|
|
if (!version) throw new Error(`unexpected dist-tag: ${tag}`);
|
|
return { stdout: JSON.stringify(version), stderr: "" };
|
|
};
|
|
|
|
it("resolves every channel's dist-tag from the registry", async () => {
|
|
const state = await collectChannelState(fakeRunner);
|
|
|
|
expect(state.map((entry) => entry.channel)).toEqual([
|
|
"stable",
|
|
"beta",
|
|
"nightly",
|
|
"canary",
|
|
]);
|
|
expect(state.map((entry) => entry.version)).toEqual([
|
|
"2026.722.0",
|
|
"2026.811.0-beta.0",
|
|
"2026.811.0-nightly.0",
|
|
"2026.811.0-canary.3",
|
|
]);
|
|
});
|
|
|
|
it("degrades a single unavailable channel to null without failing the rest", async () => {
|
|
const flakyRunner: CommandRunner = async (command, args, options) => {
|
|
const spec = (args ?? []).find((arg) => arg.startsWith("paperclipai@"));
|
|
if (spec === "paperclipai@nightly") throw new Error("registry timeout");
|
|
return fakeRunner(command, args, options);
|
|
};
|
|
|
|
const state = await collectChannelState(flakyRunner);
|
|
const byChannel = Object.fromEntries(state.map((entry) => [entry.channel, entry.version]));
|
|
|
|
expect(byChannel.nightly).toBeNull();
|
|
expect(byChannel.stable).toBe("2026.722.0");
|
|
expect(byChannel.beta).toBe("2026.811.0-beta.0");
|
|
expect(byChannel.canary).toBe("2026.811.0-canary.3");
|
|
});
|
|
|
|
it("keeps the channel table and dist-tags in sync", () => {
|
|
expect(RELEASE_CHANNELS.map((entry) => entry.distTag)).toEqual([
|
|
"latest",
|
|
"beta",
|
|
"nightly",
|
|
"canary",
|
|
]);
|
|
});
|
|
});
|