Skip to content

Commit 6ce100b

Browse files
committed
cp dines
1 parent 4f54933 commit 6ce100b

3 files changed

Lines changed: 302 additions & 0 deletions

File tree

src/commands/blueprint/delete.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/**
2+
* Delete blueprint command
3+
* Supports both blueprint ID (bpt_...) and name
4+
*/
5+
6+
import { getClient } from "../../utils/client.js";
7+
import { output, outputError } from "../../utils/output.js";
8+
9+
interface DeleteOptions {
10+
output?: string;
11+
}
12+
13+
export async function deleteBlueprint(
14+
nameOrId: string,
15+
options: DeleteOptions = {},
16+
) {
17+
try {
18+
const client = getClient();
19+
20+
let blueprintId = nameOrId;
21+
22+
// If it's not an ID, resolve by name
23+
if (!nameOrId.startsWith("bpt_")) {
24+
const result = await client.blueprints.list({ name: nameOrId });
25+
const blueprints = result.blueprints || [];
26+
27+
if (blueprints.length === 0) {
28+
outputError(
29+
`Blueprint not found: ${nameOrId}`,
30+
new Error("Blueprint not found"),
31+
);
32+
return;
33+
}
34+
35+
// Use exact match if available, otherwise first result
36+
const blueprint =
37+
blueprints.find((b) => b.name === nameOrId) || blueprints[0];
38+
blueprintId = blueprint.id;
39+
}
40+
41+
await client.blueprints.delete(blueprintId);
42+
43+
// Default: just output the ID for easy scripting
44+
if (!options.output || options.output === "text") {
45+
console.log(blueprintId);
46+
} else {
47+
output(
48+
{ id: blueprintId, status: "deleted" },
49+
{ format: options.output, defaultFormat: "json" },
50+
);
51+
}
52+
} catch (error) {
53+
outputError("Failed to delete blueprint", error);
54+
}
55+
}

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 <name-or-id>")
473+
.description("Delete a blueprint by name or ID (IDs start with bpt_)")
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: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
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+
const mockList = jest.fn();
10+
jest.unstable_mockModule("@/utils/client.js", () => ({
11+
getClient: () => ({
12+
blueprints: {
13+
delete: mockDelete,
14+
list: mockList,
15+
},
16+
}),
17+
}));
18+
19+
const mockOutput = jest.fn();
20+
const mockOutputError = jest.fn();
21+
jest.unstable_mockModule("@/utils/output.js", () => ({
22+
output: mockOutput,
23+
outputError: mockOutputError,
24+
}));
25+
26+
describe("deleteBlueprint", () => {
27+
beforeEach(() => {
28+
jest.clearAllMocks();
29+
(console.log as jest.Mock).mockClear();
30+
mockDelete.mockReset();
31+
mockList.mockReset();
32+
mockOutput.mockReset();
33+
mockOutputError.mockReset();
34+
});
35+
36+
it("should delete a blueprint by ID directly", async () => {
37+
mockDelete.mockResolvedValue(undefined);
38+
39+
const { deleteBlueprint } = await import(
40+
"@/commands/blueprint/delete.js"
41+
);
42+
await deleteBlueprint("bpt_abc123", {});
43+
44+
expect(mockList).not.toHaveBeenCalled();
45+
expect(mockDelete).toHaveBeenCalledWith("bpt_abc123");
46+
expect(console.log).toHaveBeenCalledWith("bpt_abc123");
47+
});
48+
49+
it("should resolve blueprint by name and delete", async () => {
50+
mockList.mockResolvedValue({
51+
blueprints: [{ id: "bpt_resolved", name: "my-blueprint" }],
52+
});
53+
mockDelete.mockResolvedValue(undefined);
54+
55+
const { deleteBlueprint } = await import(
56+
"@/commands/blueprint/delete.js"
57+
);
58+
await deleteBlueprint("my-blueprint", {});
59+
60+
expect(mockList).toHaveBeenCalledWith({ name: "my-blueprint" });
61+
expect(mockDelete).toHaveBeenCalledWith("bpt_resolved");
62+
expect(console.log).toHaveBeenCalledWith("bpt_resolved");
63+
});
64+
65+
it("should prefer exact name match when resolving by name", async () => {
66+
mockList.mockResolvedValue({
67+
blueprints: [
68+
{ id: "bpt_partial", name: "my-blueprint-v2" },
69+
{ id: "bpt_exact", name: "my-blueprint" },
70+
],
71+
});
72+
mockDelete.mockResolvedValue(undefined);
73+
74+
const { deleteBlueprint } = await import(
75+
"@/commands/blueprint/delete.js"
76+
);
77+
await deleteBlueprint("my-blueprint", {});
78+
79+
expect(mockDelete).toHaveBeenCalledWith("bpt_exact");
80+
expect(console.log).toHaveBeenCalledWith("bpt_exact");
81+
});
82+
83+
it("should fall back to first result when no exact name match", async () => {
84+
mockList.mockResolvedValue({
85+
blueprints: [
86+
{ id: "bpt_first", name: "my-blueprint-v1" },
87+
{ id: "bpt_second", name: "my-blueprint-v2" },
88+
],
89+
});
90+
mockDelete.mockResolvedValue(undefined);
91+
92+
const { deleteBlueprint } = await import(
93+
"@/commands/blueprint/delete.js"
94+
);
95+
await deleteBlueprint("my-blueprint", {});
96+
97+
expect(mockDelete).toHaveBeenCalledWith("bpt_first");
98+
expect(console.log).toHaveBeenCalledWith("bpt_first");
99+
});
100+
101+
it("should output error when blueprint name is not found", async () => {
102+
mockList.mockResolvedValue({ blueprints: [] });
103+
104+
const { deleteBlueprint } = await import(
105+
"@/commands/blueprint/delete.js"
106+
);
107+
await deleteBlueprint("nonexistent-blueprint", {});
108+
109+
expect(mockOutputError).toHaveBeenCalledWith(
110+
"Blueprint not found: nonexistent-blueprint",
111+
expect.any(Error),
112+
);
113+
expect(mockDelete).not.toHaveBeenCalled();
114+
});
115+
116+
it("should handle empty blueprints array from API", async () => {
117+
mockList.mockResolvedValue({});
118+
119+
const { deleteBlueprint } = await import(
120+
"@/commands/blueprint/delete.js"
121+
);
122+
await deleteBlueprint("nonexistent", {});
123+
124+
expect(mockOutputError).toHaveBeenCalledWith(
125+
"Blueprint not found: nonexistent",
126+
expect.any(Error),
127+
);
128+
expect(mockDelete).not.toHaveBeenCalled();
129+
});
130+
131+
it("should output JSON format when requested", async () => {
132+
mockDelete.mockResolvedValue(undefined);
133+
134+
const { deleteBlueprint } = await import(
135+
"@/commands/blueprint/delete.js"
136+
);
137+
await deleteBlueprint("bpt_json123", { output: "json" });
138+
139+
expect(mockDelete).toHaveBeenCalledWith("bpt_json123");
140+
expect(mockOutput).toHaveBeenCalledWith(
141+
{ id: "bpt_json123", status: "deleted" },
142+
{ format: "json", defaultFormat: "json" },
143+
);
144+
expect(console.log).not.toHaveBeenCalledWith("bpt_json123");
145+
});
146+
147+
it("should output YAML format when requested", async () => {
148+
mockDelete.mockResolvedValue(undefined);
149+
150+
const { deleteBlueprint } = await import(
151+
"@/commands/blueprint/delete.js"
152+
);
153+
await deleteBlueprint("bpt_yaml456", { output: "yaml" });
154+
155+
expect(mockDelete).toHaveBeenCalledWith("bpt_yaml456");
156+
expect(mockOutput).toHaveBeenCalledWith(
157+
{ id: "bpt_yaml456", status: "deleted" },
158+
{ format: "yaml", defaultFormat: "json" },
159+
);
160+
});
161+
162+
it("should output just the ID in text format (default)", async () => {
163+
mockDelete.mockResolvedValue(undefined);
164+
165+
const { deleteBlueprint } = await import(
166+
"@/commands/blueprint/delete.js"
167+
);
168+
await deleteBlueprint("bpt_text789", { output: "text" });
169+
170+
expect(console.log).toHaveBeenCalledWith("bpt_text789");
171+
expect(mockOutput).not.toHaveBeenCalled();
172+
});
173+
174+
it("should output just the ID when no output option is provided", async () => {
175+
mockDelete.mockResolvedValue(undefined);
176+
177+
const { deleteBlueprint } = await import(
178+
"@/commands/blueprint/delete.js"
179+
);
180+
await deleteBlueprint("bpt_default", {});
181+
182+
expect(console.log).toHaveBeenCalledWith("bpt_default");
183+
expect(mockOutput).not.toHaveBeenCalled();
184+
});
185+
186+
it("should handle API errors on delete gracefully", async () => {
187+
const apiError = new Error("API Error: Forbidden");
188+
mockDelete.mockRejectedValue(apiError);
189+
190+
const { deleteBlueprint } = await import(
191+
"@/commands/blueprint/delete.js"
192+
);
193+
await deleteBlueprint("bpt_error", {});
194+
195+
expect(mockOutputError).toHaveBeenCalledWith(
196+
"Failed to delete blueprint",
197+
apiError,
198+
);
199+
});
200+
201+
it("should handle API errors on list gracefully", async () => {
202+
const apiError = new Error("API Error: Network failure");
203+
mockList.mockRejectedValue(apiError);
204+
205+
const { deleteBlueprint } = await import(
206+
"@/commands/blueprint/delete.js"
207+
);
208+
await deleteBlueprint("some-name", {});
209+
210+
expect(mockOutputError).toHaveBeenCalledWith(
211+
"Failed to delete blueprint",
212+
apiError,
213+
);
214+
expect(mockDelete).not.toHaveBeenCalled();
215+
});
216+
217+
it("should output resolved ID in text format when deleting by name", async () => {
218+
mockList.mockResolvedValue({
219+
blueprints: [{ id: "bpt_resolved_id", name: "named-blueprint" }],
220+
});
221+
mockDelete.mockResolvedValue(undefined);
222+
223+
const { deleteBlueprint } = await import(
224+
"@/commands/blueprint/delete.js"
225+
);
226+
await deleteBlueprint("named-blueprint", { output: "json" });
227+
228+
expect(mockOutput).toHaveBeenCalledWith(
229+
{ id: "bpt_resolved_id", status: "deleted" },
230+
{ format: "json", defaultFormat: "json" },
231+
);
232+
});
233+
});

0 commit comments

Comments
 (0)