mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 23:36:51 +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
120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import pc from "picocolors";
|
|
import { resolvePublishedVersion, type CommandRunner } from "./install.js";
|
|
import { packageVersion } from "../version.js";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
const defaultRunCommand: CommandRunner = (command, args, options) =>
|
|
execFileAsync(command, args, { ...options, encoding: "utf8" });
|
|
|
|
export type ChannelName = "stable" | "beta" | "nightly" | "canary";
|
|
|
|
export type ChannelDescriptor = {
|
|
channel: ChannelName;
|
|
distTag: string;
|
|
cadence: string;
|
|
audience: string;
|
|
};
|
|
|
|
// Ordered from most to least stable — the order users should consider them.
|
|
export const RELEASE_CHANNELS: readonly ChannelDescriptor[] = [
|
|
{
|
|
channel: "stable",
|
|
distTag: "latest",
|
|
cadence: "manual, soaked in beta for 3+ days",
|
|
audience: "the recommended release for almost everyone",
|
|
},
|
|
{
|
|
channel: "beta",
|
|
distTag: "beta",
|
|
cadence: "manual promotion behind an approval gate",
|
|
audience: "release candidates: what stable becomes a few days later",
|
|
},
|
|
{
|
|
channel: "nightly",
|
|
distTag: "nightly",
|
|
cadence: "once a night, smoke-gated",
|
|
audience: "yesterday's merges, tested as a unit",
|
|
},
|
|
{
|
|
channel: "canary",
|
|
distTag: "canary",
|
|
cadence: "every merge to master",
|
|
audience: "the bleeding edge",
|
|
},
|
|
];
|
|
|
|
const CALVER_RE = /^\d{4}\.\d{1,4}\.\d+$/;
|
|
const PRERELEASE_RE = /^\d{4}\.\d{1,4}\.\d+-(canary|nightly|beta)\.\d+$/;
|
|
|
|
// Published versions carry their lane in the version string; the source
|
|
// checkout's package.json holds a placeholder that matches neither form.
|
|
export function channelForVersion(version: string): ChannelName | "unknown" {
|
|
const prerelease = version.match(PRERELEASE_RE);
|
|
if (prerelease) return prerelease[1] as ChannelName;
|
|
if (CALVER_RE.test(version)) return "stable";
|
|
return "unknown";
|
|
}
|
|
|
|
export type ChannelState = ChannelDescriptor & { version: string | null };
|
|
|
|
export async function collectChannelState(
|
|
runCommand: CommandRunner = defaultRunCommand,
|
|
): Promise<ChannelState[]> {
|
|
const resolved = await Promise.allSettled(
|
|
RELEASE_CHANNELS.map((entry) => resolvePublishedVersion(entry.distTag, runCommand)),
|
|
);
|
|
return RELEASE_CHANNELS.map((entry, index) => {
|
|
const outcome = resolved[index];
|
|
return {
|
|
...entry,
|
|
version: outcome.status === "fulfilled" ? outcome.value : null,
|
|
};
|
|
});
|
|
}
|
|
|
|
export type ChannelsOptions = { json?: boolean };
|
|
|
|
export async function channelsCommand(
|
|
options: ChannelsOptions = {},
|
|
runCommand: CommandRunner = defaultRunCommand,
|
|
): Promise<void> {
|
|
const state = await collectChannelState(runCommand);
|
|
const currentChannel = channelForVersion(packageVersion);
|
|
|
|
if (options.json) {
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
current: { version: packageVersion, channel: currentChannel },
|
|
channels: state,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
console.log(pc.bold("Paperclip release channels"));
|
|
console.log("");
|
|
for (const entry of state) {
|
|
const version = entry.version ?? pc.yellow("unavailable");
|
|
console.log(` ${pc.bold(entry.channel.padEnd(8))} ${version}`);
|
|
console.log(` ${" ".repeat(8)} ${pc.dim(`${entry.cadence} — ${entry.audience}`)}`);
|
|
console.log(` ${" ".repeat(8)} ${pc.dim(`npx paperclipai@${entry.distTag} onboard`)}`);
|
|
console.log("");
|
|
}
|
|
|
|
if (currentChannel === "unknown") {
|
|
console.log(
|
|
`This install reports version ${pc.bold(packageVersion)}, which does not map to a published channel (source checkouts report the repository placeholder).`,
|
|
);
|
|
} else {
|
|
console.log(`This install is version ${pc.bold(packageVersion)} on the ${pc.bold(currentChannel)} channel.`);
|
|
}
|
|
console.log(`Docker images use the same names: ghcr.io/paperclipai/paperclip:{latest,beta,nightly,canary}`);
|
|
}
|