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");