From 85ce7591dacf240d0dea398b14ed02dc18d06d70 Mon Sep 17 00:00:00 2001 From: Jakob Heuser Date: Wed, 21 Oct 2026 09:35:14 -0700 Subject: [PATCH] fix(init): say "Remove Taskless" only of a directory that keeps nothing An upgrade that retires the /tskl command removes one stub from .claude/ while the skill stays, but the confirm asked to "Remove Taskless from .claude/". A target that keeps a skill or command is now described as losing its stubs ("Remove 1 stub from .claude/?"). --- .changeset/init-removal-prompt-keeps-skill.md | 5 +++ packages/cli/src/wizard/steps/summary.ts | 34 ++++++++++++--- packages/cli/test/wizard-steps.test.ts | 43 ++++++++++++++++++- 3 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 .changeset/init-removal-prompt-keeps-skill.md diff --git a/.changeset/init-removal-prompt-keeps-skill.md b/.changeset/init-removal-prompt-keeps-skill.md new file mode 100644 index 00000000..94f97471 --- /dev/null +++ b/.changeset/init-removal-prompt-keeps-skill.md @@ -0,0 +1,5 @@ +--- +"@taskless/cli": patch +--- + +`init` no longer asks to "Remove Taskless from .claude/" when an upgrade only retires a stub there. A directory that keeps its Taskless skill is now described as losing that stub ("Remove 1 stub from .claude/?"). "Remove Taskless from" is reserved for a directory you deselected. diff --git a/packages/cli/src/wizard/steps/summary.ts b/packages/cli/src/wizard/steps/summary.ts index 82a2e62d..b546e008 100644 --- a/packages/cli/src/wizard/steps/summary.ts +++ b/packages/cli/src/wizard/steps/summary.ts @@ -172,26 +172,46 @@ function describeProjectEdits( * misleading; when only canonical removals are present the message falls back * to a generic prompt. Only meaningful when {@link InstallDiff.hasRemovals} * is true. + * + * "Remove Taskless from" is said only of a target that keeps nothing. A target + * that loses a retired stub but keeps its skill is still a Taskless install, + * and telling someone it is being uninstalled makes them decline an upgrade + * that removes one command file. */ export function buildRemovalConfirmMessage( diff: InstallDiff, options: { editsSettings?: boolean } = {} ): string { - const clauses: string[] = []; + const uninstalled: string[] = []; + const trimmed: string[] = []; for (const entry of diff.entries) { if (entry.mode === "canonical") continue; const count = entry.removals.skills.length + entry.removals.commands.length; if (count === 0) continue; - clauses.push( - `${entry.target}/ (${String(count)} stub${count === 1 ? "" : "s"})` - ); + const stubs = `${String(count)} stub${count === 1 ? "" : "s"}`; + const kept = + entry.additions.skills.length + + entry.additions.commands.length + + entry.unchanged.skills.length + + entry.unchanged.commands.length; + if (kept === 0) { + uninstalled.push(`${entry.target}/ (${stubs})`); + } else { + trimmed.push(`${stubs} from ${entry.target}/`); + } } - if (clauses.length === 0) { + if (uninstalled.length === 0 && trimmed.length === 0) { return options.editsSettings === true ? "Some files recorded in the previous install will be removed. Proceed, and make these changes to your settings?" : "Some files recorded in the previous install will be removed. Proceed?"; } + const clauses = [ + ...(uninstalled.length > 0 + ? [`Taskless from ${uninstalled.join(", ")}`] + : []), + ...trimmed, + ]; return options.editsSettings === true - ? `Remove Taskless from ${clauses.join(", ")}, and make these changes to your settings?` - : `Remove Taskless from ${clauses.join(", ")}?`; + ? `Remove ${clauses.join(", ")}, and make these changes to your settings?` + : `Remove ${clauses.join(", ")}?`; } diff --git a/packages/cli/test/wizard-steps.test.ts b/packages/cli/test/wizard-steps.test.ts index 15851127..c750dc11 100644 --- a/packages/cli/test/wizard-steps.test.ts +++ b/packages/cli/test/wizard-steps.test.ts @@ -218,14 +218,15 @@ function diffEntry( target: string, removedSkills: string[], removedCommands: string[] = [], - mode: "reference" | "canonical" = "reference" + mode: "reference" | "canonical" = "reference", + unchangedSkills: string[] = [] ) { return { target, mode, additions: { skills: [], commands: [] }, removals: { skills: removedSkills, commands: removedCommands }, - unchanged: { skills: [], commands: [] }, + unchanged: { skills: unchangedSkills, commands: [] }, }; } @@ -246,6 +247,44 @@ describe("buildRemovalConfirmMessage", () => { ); }); + it("does not call a target that keeps its skill an uninstall", async () => { + // An upgrade that retires the /tskl command leaves the skill in place, so + // the question is about one stub, not about Taskless leaving .claude/. + const { buildRemovalConfirmMessage } = + await import("../src/wizard/steps/summary"); + const message = buildRemovalConfirmMessage( + { + entries: [ + diffEntry(".taskless", [], ["tskl.md"], "canonical", ["taskless"]), + diffEntry(".claude", [], ["tskl.md"], "reference", ["taskless"]), + ], + hasAdditions: false, + hasRemovals: true, + }, + { editsSettings: true } + ); + expect(message).toBe( + "Remove 1 stub from .claude/, and make these changes to your settings?" + ); + expect(message).not.toContain("Remove Taskless"); + }); + + it("names uninstalled targets before trimmed ones", async () => { + const { buildRemovalConfirmMessage } = + await import("../src/wizard/steps/summary"); + const message = buildRemovalConfirmMessage({ + entries: [ + diffEntry(".claude", [], ["tskl.md"], "reference", ["taskless"]), + diffEntry(".cursor", ["taskless"], ["tskl.md"]), + ], + hasAdditions: false, + hasRemovals: true, + }); + expect(message).toBe( + "Remove Taskless from .cursor/ (2 stubs), 1 stub from .claude/?" + ); + }); + it("folds the settings changes into the same question", async () => { const { buildRemovalConfirmMessage } = await import("../src/wizard/steps/summary");