From 860d3c53002456540e7f07c7756f7c300b8d6782 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Tue, 6 Oct 2026 10:36:53 -0700 Subject: [PATCH] fix(cli): box the stale-pin notice, and tell agents to run init from --help The notice that package.json pins an older Taskless CLI now draws in the same orange box as the restart-your-agents banner, so it is not lost in the install output. The box moves to install/notice-box.ts and both notices render through it; list items wrap under their own text and a nightly version is never split across rows. Every --help now opens with a line telling an agent that wants to install or update Taskless to run init first, named with getCliPrefix() so a pinned nightly points at itself. --- .changeset/help-agent-install-line.md | 5 + .changeset/init-stale-cli-pins.md | 2 +- packages/cli/src/commands/info.ts | 3 +- packages/cli/src/install/notice-box.ts | 100 ++++++++++++++++++ packages/cli/src/install/pinned-cli.ts | 11 +- packages/cli/src/install/reload-notice.ts | 95 ++--------------- packages/cli/src/util/help.ts | 19 ++++ packages/cli/test/help-flag.test.ts | 11 ++ packages/cli/test/init-no-interactive.test.ts | 36 +++++-- packages/cli/test/pinned-cli.test.ts | 78 ++++++++++++-- 10 files changed, 244 insertions(+), 116 deletions(-) create mode 100644 .changeset/help-agent-install-line.md create mode 100644 packages/cli/src/install/notice-box.ts diff --git a/.changeset/help-agent-install-line.md b/.changeset/help-agent-install-line.md new file mode 100644 index 00000000..b29078e4 --- /dev/null +++ b/.changeset/help-agent-install-line.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +Every `--help` now opens with a line telling an agent that wants to install or update Taskless to run `init` first. An agent without the Taskless skill tends to start from `--help` and improvise commands; `init` installs the skill and recipes that keep sessions consistent. The command names the launcher and the exact build you ran, so a pinned nightly points at itself. diff --git a/.changeset/init-stale-cli-pins.md b/.changeset/init-stale-cli-pins.md index 2ae29fab..9b9a9c08 100644 --- a/.changeset/init-stale-cli-pins.md +++ b/.changeset/init-stale-cli-pins.md @@ -2,4 +2,4 @@ "@taskless/cli": patch --- -`taskless init` names any `package.json` pin of `@taskless/cli` or `@taskless/cli-nightly` that would run an older CLI than the one that just ran (a dependency whose installed build or range is behind, or a script spelling out an older version), with the version to move it to, and offers the bump. Scripts, CI and git hooks run that pin, and a CLI older than the project's `.taskless/` refuses the layout. The install does not edit `package.json`. The workspace packages a project declares (`pnpm-workspace.yaml`, or the `workspaces` field) are read as well as the root, and a dependency is judged by the version that package actually runs: its own `node_modules` link, else the hoisted one. `init --json` and `info --json` both carry the pins as `pinnedCli`, each naming its `manifest`, and plain `info` lists them. The `init` (topic v4), `info` (topic v2) and `update` (topic v15) recipes tell an agent to offer the bump, and `update` now reads the pins from `info --json` rather than re-running `init`. +`taskless init` names any `package.json` pin of `@taskless/cli` or `@taskless/cli-nightly` that would run an older CLI than the one that just ran (a dependency whose installed build or range is behind, or a script spelling out an older version), with the version to move it to, and offers the bump. The notice is boxed like the restart-your-agents banner, so it is not lost in the install output. Scripts, CI and git hooks run that pin, and a CLI older than the project's `.taskless/` refuses the layout. The install does not edit `package.json`. The workspace packages a project declares (`pnpm-workspace.yaml`, or the `workspaces` field) are read as well as the root, and a dependency is judged by the version that package actually runs: its own `node_modules` link, else the hoisted one. `init --json` and `info --json` both carry the pins as `pinnedCli`, each naming its `manifest`, and plain `info` lists them. The `init` (topic v4), `info` (topic v2) and `update` (topic v15) recipes tell an agent to offer the bump, and `update` now reads the pins from `info --json` rather than re-running `init`. diff --git a/packages/cli/src/commands/info.ts b/packages/cli/src/commands/info.ts index ab7817dd..91f76c11 100644 --- a/packages/cli/src/commands/info.ts +++ b/packages/cli/src/commands/info.ts @@ -182,7 +182,8 @@ export const infoCommand = defineCommand({ if (pinnedCli.length > 0) { console.log(""); console.log(`Pinned CLI older than v${__VERSION__}:`); - for (const pin of pinnedCli) console.log(describePin(pin, __VERSION__)); + for (const pin of pinnedCli) + console.log(` - ${describePin(pin, __VERSION__)}`); } console.log(""); diff --git a/packages/cli/src/install/notice-box.ts b/packages/cli/src/install/notice-box.ts new file mode 100644 index 00000000..437814dd --- /dev/null +++ b/packages/cli/src/install/notice-box.ts @@ -0,0 +1,100 @@ +import chalk from "chalk"; + +// Sets `chalk.level` from the real terminal on import. This module renders +// colour, so it establishes that itself rather than inheriting it from +// whichever caller happened to load `wizard/intro.ts` first. +import "../util/color"; + +/** + * The orange box an install draws around a notice it cannot afford to have + * scrolled past. Shared so every such notice reads as the same kind of thing. + */ + +/** Inner text is wrapped to this many columns before the box is sized. */ +const WRAP_COLUMNS = 62; + +/** + * Orange, downsampled by chalk to whatever the terminal actually supports. + * + * Depth comes from `util/color`, imported above for that side effect. It used + * to come from `wizard/intro.ts` by accident, because both callers of the + * original banner import that file for `getCliVersion`. That held, and held + * for a reason no reader of this file could see: a caller that did not import + * the wizard would have got a colourless box with no error and nothing to grep + * for. + */ +const ACCENT = "#ff8c00"; + +/** + * One block of a notice: a paragraph wrapped to the box, or a bulleted list + * whose items wrap under their own text rather than under the bullet. + */ +export type NoticeBlock = string | { items: readonly string[] }; + +/** + * Wrap on spaces, never mid-token. + * + * A nightly version is a single 30-character token, so a wrapper that split on + * width would cut one in half and produce a string nobody can copy. An + * over-long line is allowed to overflow instead, and the box is then sized + * around it. + */ +function wrap(text: string, columns: number): string[] { + const lines: string[] = []; + let line = ""; + for (const word of text.split(" ")) { + if (line === "") { + line = word; + } else if (line.length + 1 + word.length <= columns) { + line = `${line} ${word}`; + } else { + lines.push(line); + line = word; + } + } + if (line !== "") lines.push(line); + return lines; +} + +function renderBlock(block: NoticeBlock): string[] { + if (typeof block === "string") return wrap(block, WRAP_COLUMNS); + return block.items.flatMap((item) => + wrap(item, WRAP_COLUMNS - 2).map( + (line, index) => `${index === 0 ? "-" : " "} ${line}` + ) + ); +} + +/** Draw `heading` and `blocks`, separated by blank lines, inside the box. */ +export function renderNoticeBox( + heading: string, + blocks: readonly NoticeBlock[] +): string { + const body = blocks.flatMap((block, index) => [ + ...(index === 0 ? [] : [""]), + ...renderBlock(block), + ]); + + // Sized to the content, so a long nightly version widens the box rather than + // breaking out of it. Padding is computed on the UNCOLORED text: measuring + // after chalk has run would count escape sequences as characters and leave + // every border ragged. + const inner = + Math.max(heading.length, ...body.map((line) => line.length)) + 4; + + const edge = chalk.hex(ACCENT); + const top = edge(`┌${"─".repeat(inner)}┐`); + const bottom = edge(`└${"─".repeat(inner)}┘`); + const row = (text: string, render: (value: string) => string) => + `${edge("│")} ${render(text)}${" ".repeat(inner - text.length - 4)} ${edge("│")}`; + + return [ + "", + top, + row(heading, (value) => edge.bold(value)), + row("", (value) => value), + ...body.map((line) => row(line, (value) => value)), + bottom, + "", + ].join("\n"); +} diff --git a/packages/cli/src/install/pinned-cli.ts b/packages/cli/src/install/pinned-cli.ts index 57da5ffd..8536d092 100644 --- a/packages/cli/src/install/pinned-cli.ts +++ b/packages/cli/src/install/pinned-cli.ts @@ -5,6 +5,7 @@ import { parse as parseYaml } from "yaml"; import { isRecord } from "../util/is-record"; import { compareSemver, compareVersions } from "../util/version-compare"; +import { renderNoticeBox } from "./notice-box"; /** * Pins in `package.json` that would run a Taskless CLI older than the one that @@ -372,7 +373,7 @@ function bumpTarget(cliVersion: string): { name: string; version: string } { } /** - * One notice line: which manifest and field the pin is in, what it says, and + * One pin, unbulleted: which manifest and field the pin is in, what it says, and * what to change it to. The manifest is named even at the root, so a list * mixing the root with workspace packages reads the same way throughout. */ @@ -384,7 +385,7 @@ export function describePin(pin: PinnedCli, cliVersion: string): string { pin.name === target.name ? `${target.name}@${target.version}` : `${target.name}@${target.version}, replacing ${pin.name}`; - return ` - ${pin.manifest} ${pin.location}: ${pin.name} ${pin.spec}${installed} -> ${move}`; + return `${pin.manifest} ${pin.location}: ${pin.name} ${pin.spec}${installed} -> ${move}`; } /** @@ -426,9 +427,9 @@ export function getPinnedCliNotice( : `This upgrade migrated .taskless/ from schema version ${String(upgraded.from)} to ${String(upgraded.to)}, and a CLI that predates that schema refuses the project (SCAFFOLD_VERSION_MISMATCH). ` + `CI, scripts, and git hooks that run these pins will break on the push that carries the migrated files. ` + `Offer to update them as shown and reinstall dependencies, in the same commit as .taskless/.`; - return [ + return renderNoticeBox("UPDATE PINNED TASKLESS VERSIONS", [ `package.json pins a Taskless CLI older than ${cliVersion}, the version that just ran here:`, - ...pins.map((pin) => describePin(pin, cliVersion)), + { items: pins.map((pin) => describePin(pin, cliVersion)) }, consequence, - ].join("\n"); + ]); } diff --git a/packages/cli/src/install/reload-notice.ts b/packages/cli/src/install/reload-notice.ts index 046dd2f6..30d581a4 100644 --- a/packages/cli/src/install/reload-notice.ts +++ b/packages/cli/src/install/reload-notice.ts @@ -1,9 +1,4 @@ -import chalk from "chalk"; - -// Sets `chalk.level` from the real terminal on import. This module renders -// colour, so it establishes that itself rather than inheriting it from -// whichever caller happened to load `wizard/intro.ts` first. -import "../util/color"; +import { renderNoticeBox } from "./notice-box"; /** * The banner an upgrade owes a session that is already running. @@ -33,45 +28,6 @@ export interface ReloadNoticeInput { cliVersion: string; } -/** Inner text is wrapped to this many columns before the box is sized. */ -const WRAP_COLUMNS = 62; - -/** - * Orange, downsampled by chalk to whatever the terminal actually supports. - * - * Depth comes from `util/color`, imported above for that side effect. It used - * to come from `wizard/intro.ts` by accident, because both callers of this - * module import that file for `getCliVersion`. That held, and held for a reason - * no reader of this file could see: a third caller that did not import the - * wizard would have got a colourless box with no error and nothing to grep for. - */ -const ACCENT = "#ff8c00"; - -/** - * Wrap on spaces, never mid-token. - * - * A nightly version is a single 30-character token, so a wrapper that split on - * width would cut one in half and produce a string nobody can copy. An - * over-long line is allowed to overflow instead, and the box is then sized - * around it. - */ -function wrap(text: string, columns: number): string[] { - const lines: string[] = []; - let line = ""; - for (const word of text.split(" ")) { - if (line === "") { - line = word; - } else if (line.length + 1 + word.length <= columns) { - line = `${line} ${word}`; - } else { - lines.push(line); - line = word; - } - } - if (line !== "") lines.push(line); - return lines; -} - /** * Whether this run changed the version, which is the only thing that makes an * open session stale. @@ -98,46 +54,11 @@ export function versionMoved(input: ReloadNoticeInput): boolean { export function getReloadNotice(input: ReloadNoticeInput): string | undefined { if (!versionMoved(input)) return undefined; - const body = [ - ...wrap( - `Taskless changed from ${input.previousCliVersion ?? ""} to ${input.cliVersion}.`, - WRAP_COLUMNS - ), - "", - ...wrap( - "An AI session that is already open still holds the previous skills, " + - "because most tools read the skill list once, at startup.", - WRAP_COLUMNS - ), - "", - ...wrap( - "Reload skills in your AI tool, or start a new session, before asking " + - "it to use Taskless.", - WRAP_COLUMNS - ), - ]; - - const heading = "RESTART YOUR AGENTS"; - // Sized to the content, so a long nightly version widens the box rather than - // breaking out of it. Padding is computed on the UNCOLORED text: measuring - // after chalk has run would count escape sequences as characters and leave - // every border ragged. - const inner = - Math.max(heading.length, ...body.map((line) => line.length)) + 4; - - const edge = chalk.hex(ACCENT); - const top = edge(`┌${"─".repeat(inner)}┐`); - const bottom = edge(`└${"─".repeat(inner)}┘`); - const row = (text: string, render: (value: string) => string) => - `${edge("│")} ${render(text)}${" ".repeat(inner - text.length - 4)} ${edge("│")}`; - - return [ - "", - top, - row(heading, (value) => edge.bold(value)), - row("", (value) => value), - ...body.map((line) => row(line, (value) => value)), - bottom, - "", - ].join("\n"); + return renderNoticeBox("RESTART YOUR AGENTS", [ + `Taskless changed from ${input.previousCliVersion ?? ""} to ${input.cliVersion}.`, + "An AI session that is already open still holds the previous skills, " + + "because most tools read the skill list once, at startup.", + "Reload skills in your AI tool, or start a new session, before asking " + + "it to use Taskless.", + ]); } diff --git a/packages/cli/src/util/help.ts b/packages/cli/src/util/help.ts index 4e19cbdc..82caac62 100644 --- a/packages/cli/src/util/help.ts +++ b/packages/cli/src/util/help.ts @@ -2,6 +2,7 @@ import { showUsage } from "citty"; import type { ArgsDef, CommandDef, Resolvable } from "citty"; import { splitRawArguments } from "./argv"; +import { getCliPrefix } from "./package-manager"; /** * citty only implements `--help` inside `runMain`, and `runMain`'s help path @@ -52,5 +53,23 @@ export async function showResolvedUsage( command = child; } + console.log(getAgentInstallLine()); + console.log(""); await showUsage(command, parent); } + +/** + * Printed above every usage block, because `--help` is where an agent that + * does not have the Taskless skill yet goes looking. Help written for a + * person sends it improvising commands; `init` installs the skill and + * recipes that make every later session behave the same way. + * + * It names this exact build, as every remedy does: whoever launched a pinned + * nightly chose that version, and `init` should install the same one. + */ +export function getAgentInstallLine(): string { + return ( + `If you're an agent looking to install or update Taskless, run \`${getCliPrefix()} init\` first. ` + + "It may change how this help works, but it is how agents get a consistent Taskless experience." + ); +} diff --git a/packages/cli/test/help-flag.test.ts b/packages/cli/test/help-flag.test.ts index 855ddbbb..ea778fce 100644 --- a/packages/cli/test/help-flag.test.ts +++ b/packages/cli/test/help-flag.test.ts @@ -103,6 +103,17 @@ describe("--help below the root", () => { expect(stdout).toContain("COMMANDS"); expect(stdout).toContain("Manage authentication with taskless.io"); }); + + it.each([[["--help"]], [["auth", "login", "--help"]]])( + "opens %j with the line telling an agent to run init", + async (arguments_) => { + const { stdout } = await execFileAsync("node", [binPath, ...arguments_]); + + const [first] = stdout.split("\n"); + expect(first).toMatch(/^If you're an agent .* init` first\./); + expect(stdout.indexOf("USAGE")).toBeGreaterThan(0); + } + ); }); describe("splitRawArguments", () => { diff --git a/packages/cli/test/init-no-interactive.test.ts b/packages/cli/test/init-no-interactive.test.ts index c060f220..c98fe20e 100644 --- a/packages/cli/test/init-no-interactive.test.ts +++ b/packages/cli/test/init-no-interactive.test.ts @@ -37,6 +37,18 @@ async function installAtVersion(cwd: string, version: string): Promise { await writeFile(manifestPath, JSON.stringify(manifest, null, 2)); } +/** + * The text of every boxed notice in `stdout`, rows joined back into one line, + * so a phrase the box wrapped across two rows can be asserted on whole. + */ +function boxedText(stdout: string): string { + return stdout + .split("\n") + .filter((line) => line.startsWith("│")) + .map((line) => line.slice(1, -1).trim()) + .join(" "); +} + describe("taskless init (the batch install)", () => { let cwd: string; @@ -294,10 +306,12 @@ describe("taskless init (the batch install)", () => { cwd, ]); - expect(stdout).toContain("devDependencies: @taskless/cli 0.0.1"); - expect(stdout).toContain("scripts.lint: @taskless/cli 0.0.1"); - expect(stdout).toMatch(/-> @taskless\/cli@\S+/); - expect(stdout).toContain("Offer to update them as shown"); + const notice = boxedText(stdout); + expect(notice).toContain("UPDATE PINNED TASKLESS VERSIONS"); + expect(notice).toContain("devDependencies: @taskless/cli 0.0.1"); + expect(notice).toContain("scripts.lint: @taskless/cli 0.0.1"); + expect(notice).toMatch(/-> @taskless\/cli@\S+/); + expect(notice).toContain("Offer to update them as shown"); expect(await readFile(join(cwd, "package.json"), "utf8")).toBe(packageJson); // After the upgrade trailer, which it follows from; the onboarding line @@ -336,9 +350,9 @@ describe("taskless init (the batch install)", () => { expect(stdout).toContain( `from schema version 2 to ${String(LATEST_SCHEMA_VERSION)}` ); - expect(stdout).toContain("SCAFFOLD_VERSION_MISMATCH"); - expect(stdout).toContain("same commit as .taskless/"); - expect(stdout).not.toContain("will likely fail"); + expect(boxedText(stdout)).toContain("SCAFFOLD_VERSION_MISMATCH"); + expect(boxedText(stdout)).toContain("same commit as .taskless/"); + expect(boxedText(stdout)).not.toContain("will likely fail"); }); it("does not call a fresh install an upgrade", async () => { @@ -357,8 +371,8 @@ describe("taskless init (the batch install)", () => { ]); expect(stdout).toContain("devDependencies: @taskless/cli 0.0.1"); - expect(stdout).toContain("will likely fail"); - expect(stdout).not.toContain("SCAFFOLD_VERSION_MISMATCH"); + expect(boxedText(stdout)).toContain("will likely fail"); + expect(boxedText(stdout)).not.toContain("SCAFFOLD_VERSION_MISMATCH"); expect(stdout).not.toContain("This upgrade migrated"); }); @@ -381,8 +395,8 @@ describe("taskless init (the batch install)", () => { expect(stdout).not.toContain("next commit"); expect(stdout).toContain("devDependencies: @taskless/cli ^0.0.1"); // Nothing migrated, so the layout the pin reads did not move. - expect(stdout).toContain("will likely fail"); - expect(stdout).not.toContain("SCAFFOLD_VERSION_MISMATCH"); + expect(boxedText(stdout)).toContain("will likely fail"); + expect(boxedText(stdout)).not.toContain("SCAFFOLD_VERSION_MISMATCH"); }); it("carries stale pins on the --json envelope, as an empty list when there are none", async () => { diff --git a/packages/cli/test/pinned-cli.test.ts b/packages/cli/test/pinned-cli.test.ts index 50886c5f..b40180d5 100644 --- a/packages/cli/test/pinned-cli.test.ts +++ b/packages/cli/test/pinned-cli.test.ts @@ -1,6 +1,7 @@ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import chalk from "chalk"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { @@ -375,6 +376,27 @@ describe("findStalePins", () => { }); }); +// eslint-disable-next-line no-control-regex +const ANSI = /\u001B\[[0-9;]*m/g; +const plain = (value: string) => value.replaceAll(ANSI, ""); + +/** + * The notice's text with the box taken off and the wrapping undone, so a + * sentence the box broke across two rows can still be asserted on whole. + */ +function pinnedNotice( + ...args: Parameters +): string | undefined { + const notice = getPinnedCliNotice(...args); + if (notice === undefined) return undefined; + return plain(notice) + .split("\n") + .filter((line) => line.startsWith("│")) + .map((line) => line.slice(1, -1).trim()) + .filter((line) => line !== "") + .join(" "); +} + describe("getPinnedCliNotice", () => { const releasePin: PinnedCli = { manifest: "package.json", @@ -388,8 +410,45 @@ describe("getPinnedCliNotice", () => { expect(getPinnedCliNotice([], "0.11.2")).toBeUndefined(); }); + it("draws a box whose every row is the same width, around a long nightly pin", () => { + // Coloured, so the width is measured on what a terminal shows rather than + // on escape sequences. + const level = chalk.level; + chalk.level = 3; + try { + const notice = getPinnedCliNotice( + [ + { + ...releasePin, + manifest: "packages/some-long-workspace-name/package.json", + name: "@taskless/cli-nightly", + spec: "0.11.1-20260831132610x088fa7c", + installed: "0.11.1-20260831132610x088fa7c", + }, + ], + "0.12.0-20261006162512x92b3715" + ) as string; + expect(notice).not.toBe(plain(notice)); + const rows = plain(notice) + .split("\n") + .filter((line) => line !== ""); + expect(rows[1]).toContain("UPDATE PINNED TASKLESS VERSIONS"); + const widths = new Set(rows.map((line) => line.length)); + expect(widths, `rows: ${[...widths].join(", ")}`).toHaveLength(1); + // Neither version is split across rows, so both can be copied. + for (const version of [ + "0.11.1-20260831132610x088fa7c", + "0.12.0-20261006162512x92b3715", + ]) { + expect(rows.some((line) => line.includes(version))).toBe(true); + } + } finally { + chalk.level = level; + } + }); + it("names every pin with its target, and offers the bump rather than claiming it", () => { - const notice = getPinnedCliNotice( + const notice = pinnedNotice( [ { ...releasePin, spec: "^0.11.0", installed: "0.11.0" }, { @@ -401,17 +460,17 @@ describe("getPinnedCliNotice", () => { "0.11.2" ); expect(notice).toContain( - " - package.json devDependencies: @taskless/cli ^0.11.0 (installed 0.11.0) -> @taskless/cli@0.11.2" + "- package.json devDependencies: @taskless/cli ^0.11.0 (installed 0.11.0) -> @taskless/cli@0.11.2" ); expect(notice).toContain( - " - packages/app/package.json scripts.lint: @taskless/cli 0.10.2 -> @taskless/cli@0.11.2" + "- packages/app/package.json scripts.lint: @taskless/cli 0.10.2 -> @taskless/cli@0.11.2" ); expect(notice).toContain("Offer to update them as shown"); }); it("moves a nightly pin to the release package when a release is running", () => { // There is no @taskless/cli-nightly@0.11.2; nightlies always carry a stamp. - const notice = getPinnedCliNotice( + const notice = pinnedNotice( [{ ...releasePin, name: "@taskless/cli-nightly", spec: "0.11.2-2026x0" }], "0.11.2" ); @@ -421,17 +480,14 @@ describe("getPinnedCliNotice", () => { }); it("moves a release pin to the nightly package when a nightly is running", () => { - const notice = getPinnedCliNotice( - [releasePin], - "0.12.0-20261002181147x023048f" - ); + const notice = pinnedNotice([releasePin], "0.12.0-20261002181147x023048f"); expect(notice).toContain( "-> @taskless/cli-nightly@0.12.0-20261002181147x023048f, replacing @taskless/cli" ); }); it("hedges without a migration: the layout the pin reads did not move", () => { - const notice = getPinnedCliNotice([releasePin], "0.11.2"); + const notice = pinnedNotice([releasePin], "0.11.2"); expect(notice).toContain("will likely fail"); expect(notice).not.toContain("SCAFFOLD_VERSION_MISMATCH"); }); @@ -439,7 +495,7 @@ describe("getPinnedCliNotice", () => { it("states the breakage as certain after a migration, and ties the bump to the commit", () => { // A CLI refuses a scaffold newer than its own highest migration, so the // pin fails on CI's first run against the migrated files. - const notice = getPinnedCliNotice([releasePin], "0.11.2", { + const notice = pinnedNotice([releasePin], "0.11.2", { migrated: { from: 5, to: 9 }, }); expect(notice).toContain("from schema version 5 to 9"); @@ -451,7 +507,7 @@ describe("getPinnedCliNotice", () => { it("does not call a fresh install an upgrade", () => { // A fresh `init` creates `.taskless/` by migrating from schema 0. - const notice = getPinnedCliNotice([releasePin], "0.11.2", { + const notice = pinnedNotice([releasePin], "0.11.2", { migrated: { from: 0, to: 9 }, }); expect(notice).not.toContain("upgrade");