Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/init-removal-prompt-keeps-skill.md
Original file line number Diff line number Diff line change
@@ -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.
34 changes: 27 additions & 7 deletions packages/cli/src/wizard/steps/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(", ")}?`;
}
43 changes: 41 additions & 2 deletions packages/cli/test/wizard-steps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] },
};
}

Expand All @@ -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");
Expand Down
Loading