Skip to content

Commit 0658932

Browse files
authored
fix(blueprint): adding delete (#111)
## Description <!-- Provide a brief description of your changes --> **Note:** PR titles should follow [Conventional Commits](https://www.conventionalcommits.org/) format (e.g., `feat(devbox): add support for custom env vars` or `fix(snapshot): resolve pagination issue`) as they are used for automatic release notes generation. ## Type of Change <!-- Mark the relevant option with an 'x' --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [ ] Test updates ## Related Issues <!-- Link to related issues using #issue-number --> Closes # ## Changes Made <!-- Describe the changes in detail --> ## Testing <!-- Describe how you tested your changes --> - [ ] I have tested locally - [ ] I have added/updated tests - [ ] All existing tests pass ## Checklist - [ ] My code follows the code style of this project - [ ] I have performed a self-review of my own code - [ ] I have commented my code, particularly in hard-to-understand areas - [ ] I have updated the documentation accordingly - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published ## Screenshots (if applicable) <!-- Add screenshots to help explain your changes --> ## Additional Notes <!-- Any additional information that reviewers should know -->
1 parent 4f54933 commit 0658932

5 files changed

Lines changed: 184 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ rli blueprint list # List all blueprints
119119
rli blueprint create # Create a new blueprint
120120
rli blueprint get <name-or-id> # Get blueprint details by name or ID (...
121121
rli blueprint logs <name-or-id> # Get blueprint build logs by name or I...
122+
rli blueprint delete <id> # Delete a blueprint by ID
122123
rli blueprint prune <name> # Delete old blueprint builds, keeping ...
123124
rli blueprint from-dockerfile # Create a blueprint from a Dockerfile ...
124125
```

src/commands/blueprint/delete.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Delete blueprint command
3+
*/
4+
5+
import { getClient } from "../../utils/client.js";
6+
import { output, outputError } from "../../utils/output.js";
7+
8+
interface DeleteOptions {
9+
output?: string;
10+
}
11+
12+
export async function deleteBlueprint(id: string, options: DeleteOptions = {}) {
13+
try {
14+
const client = getClient();
15+
16+
await client.blueprints.delete(id);
17+
18+
// Default: just output the ID for easy scripting
19+
if (!options.output || options.output === "text") {
20+
console.log(id);
21+
} else {
22+
output(
23+
{ id, status: "deleted" },
24+
{ format: options.output, defaultFormat: "json" },
25+
);
26+
}
27+
} catch (error) {
28+
outputError("Failed to delete blueprint", error);
29+
}
30+
}

src/commands/blueprint/prune.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ function categorizeBlueprints(blueprints: Blueprint[], keepCount: number) {
7575
successful.sort((a, b) => (b.create_time_ms || 0) - (a.create_time_ms || 0));
7676

7777
// Determine what to keep and delete
78+
// keepCount of 0 means delete all (including successful builds)
7879
const toKeep = successful.slice(0, keepCount);
7980
const toDelete = [...successful.slice(keepCount), ...failed];
8081

@@ -136,7 +137,11 @@ function displaySummary(
136137
// Show what will be kept
137138
console.log(`\nKeeping (${result.toKeep.length} most recent successful):`);
138139
if (result.toKeep.length === 0) {
139-
console.log(" (none - no successful builds found)");
140+
if (result.successful.length === 0) {
141+
console.log(" (none - no successful builds found)");
142+
} else {
143+
console.log(" (none)");
144+
}
140145
} else {
141146
for (const blueprint of result.toKeep) {
142147
console.log(
@@ -241,8 +246,8 @@ export async function pruneBlueprints(
241246
const autoConfirm = options.yes || false;
242247
const keepCount = parseInt(options.keep || "1", 10);
243248

244-
if (isNaN(keepCount) || keepCount < 1) {
245-
outputError("--keep must be a positive integer");
249+
if (isNaN(keepCount) || keepCount < 0) {
250+
outputError("--keep must be a non-negative integer");
246251
}
247252

248253
// Fetch all blueprints with the given name

src/utils/commands.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,20 @@ export function createProgram(): Command {
468468
await getBlueprintLogs({ id, ...options });
469469
});
470470

471+
blueprint
472+
.command("delete <id>")
473+
.description("Delete a blueprint by ID")
474+
.alias("rm")
475+
.option(
476+
"-o, --output [format]",
477+
"Output format: text|json|yaml (default: text)",
478+
)
479+
.action(async (id, options) => {
480+
const { deleteBlueprint } =
481+
await import("../commands/blueprint/delete.js");
482+
await deleteBlueprint(id, options);
483+
});
484+
471485
blueprint
472486
.command("prune <name>")
473487
.description(
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* Tests for blueprint delete command
3+
*/
4+
5+
import { jest, describe, it, expect, beforeEach } from "@jest/globals";
6+
7+
// Mock dependencies using the path alias
8+
const mockDelete = jest.fn();
9+
jest.unstable_mockModule("@/utils/client.js", () => ({
10+
getClient: () => ({
11+
blueprints: {
12+
delete: mockDelete,
13+
},
14+
}),
15+
}));
16+
17+
const mockOutput = jest.fn();
18+
const mockOutputError = jest.fn();
19+
jest.unstable_mockModule("@/utils/output.js", () => ({
20+
output: mockOutput,
21+
outputError: mockOutputError,
22+
}));
23+
24+
describe("deleteBlueprint", () => {
25+
beforeEach(() => {
26+
jest.clearAllMocks();
27+
(console.log as jest.Mock).mockClear();
28+
mockDelete.mockReset();
29+
mockOutput.mockReset();
30+
mockOutputError.mockReset();
31+
});
32+
33+
it("should delete a blueprint by ID", async () => {
34+
mockDelete.mockResolvedValue(undefined);
35+
36+
const { deleteBlueprint } = await import(
37+
"@/commands/blueprint/delete.js"
38+
);
39+
await deleteBlueprint("bpt_abc123", {});
40+
41+
expect(mockDelete).toHaveBeenCalledWith("bpt_abc123");
42+
expect(console.log).toHaveBeenCalledWith("bpt_abc123");
43+
});
44+
45+
it("should output JSON format when requested", async () => {
46+
mockDelete.mockResolvedValue(undefined);
47+
48+
const { deleteBlueprint } = await import(
49+
"@/commands/blueprint/delete.js"
50+
);
51+
await deleteBlueprint("bpt_json123", { output: "json" });
52+
53+
expect(mockDelete).toHaveBeenCalledWith("bpt_json123");
54+
expect(mockOutput).toHaveBeenCalledWith(
55+
{ id: "bpt_json123", status: "deleted" },
56+
{ format: "json", defaultFormat: "json" },
57+
);
58+
expect(console.log).not.toHaveBeenCalledWith("bpt_json123");
59+
});
60+
61+
it("should output YAML format when requested", async () => {
62+
mockDelete.mockResolvedValue(undefined);
63+
64+
const { deleteBlueprint } = await import(
65+
"@/commands/blueprint/delete.js"
66+
);
67+
await deleteBlueprint("bpt_yaml456", { output: "yaml" });
68+
69+
expect(mockDelete).toHaveBeenCalledWith("bpt_yaml456");
70+
expect(mockOutput).toHaveBeenCalledWith(
71+
{ id: "bpt_yaml456", status: "deleted" },
72+
{ format: "yaml", defaultFormat: "json" },
73+
);
74+
});
75+
76+
it("should output just the ID in text format (default)", async () => {
77+
mockDelete.mockResolvedValue(undefined);
78+
79+
const { deleteBlueprint } = await import(
80+
"@/commands/blueprint/delete.js"
81+
);
82+
await deleteBlueprint("bpt_text789", { output: "text" });
83+
84+
expect(console.log).toHaveBeenCalledWith("bpt_text789");
85+
expect(mockOutput).not.toHaveBeenCalled();
86+
});
87+
88+
it("should output just the ID when no output option is provided", async () => {
89+
mockDelete.mockResolvedValue(undefined);
90+
91+
const { deleteBlueprint } = await import(
92+
"@/commands/blueprint/delete.js"
93+
);
94+
await deleteBlueprint("bpt_default", {});
95+
96+
expect(console.log).toHaveBeenCalledWith("bpt_default");
97+
expect(mockOutput).not.toHaveBeenCalled();
98+
});
99+
100+
it("should handle API errors gracefully", async () => {
101+
const apiError = new Error("API Error: Forbidden");
102+
mockDelete.mockRejectedValue(apiError);
103+
104+
const { deleteBlueprint } = await import(
105+
"@/commands/blueprint/delete.js"
106+
);
107+
await deleteBlueprint("bpt_error", {});
108+
109+
expect(mockOutputError).toHaveBeenCalledWith(
110+
"Failed to delete blueprint",
111+
apiError,
112+
);
113+
});
114+
115+
it("should handle dependent snapshot errors gracefully", async () => {
116+
const apiError = new Error(
117+
"Blueprint has dependent snapshots and cannot be deleted",
118+
);
119+
mockDelete.mockRejectedValue(apiError);
120+
121+
const { deleteBlueprint } = await import(
122+
"@/commands/blueprint/delete.js"
123+
);
124+
await deleteBlueprint("bpt_has_snapshots", {});
125+
126+
expect(mockOutputError).toHaveBeenCalledWith(
127+
"Failed to delete blueprint",
128+
apiError,
129+
);
130+
});
131+
});

0 commit comments

Comments
 (0)