Skip to content

Commit 4c40201

Browse files
committed
Add post tag subcommand for tag management
Add support for managing post tags via CLI: - post tag list: list all tags with pagination - post tag get: show tag details - post tag create: create a new tag - post tag update: update an existing tag - post tag delete: delete a tag The implementation follows the same nested CLI pattern as comment reply.
1 parent 8d04807 commit 4c40201

4 files changed

Lines changed: 449 additions & 2 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, it, vi } from "vitest";
2+
3+
import { tryRunPostCommand } from "../index.js";
4+
5+
// Mock the tag module
6+
vi.mock("../tag.js", () => ({
7+
buildTagCli: vi.fn(() => {
8+
const mockCli = {
9+
outputHelp: vi.fn(),
10+
parse: vi.fn(),
11+
matchedCommand: undefined,
12+
args: [],
13+
runMatchedCommand: vi.fn(),
14+
};
15+
return mockCli;
16+
}),
17+
}));
18+
19+
describe("tryRunPostCommand", () => {
20+
const mockRuntime = {
21+
getClientsForOptions: vi.fn(),
22+
} as unknown as Parameters<typeof tryRunPostCommand>[1];
23+
24+
it("returns false for non-post commands", async () => {
25+
const result = await tryRunPostCommand(["other"], mockRuntime);
26+
expect(result).toBe(false);
27+
});
28+
29+
it("handles tag subcommand", async () => {
30+
const result = await tryRunPostCommand(["post", "tag", "list"], mockRuntime);
31+
expect(result).toBe(true);
32+
});
33+
34+
it("handles bare tag subcommand", async () => {
35+
const result = await tryRunPostCommand(["post", "tag"], mockRuntime);
36+
expect(result).toBe(true);
37+
});
38+
});

src/commands/post/format.ts

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ListedPostList, Post } from "@halo-dev/api-client";
1+
import type { ListedPostList, Post, Tag, TagList } from "@halo-dev/api-client";
22
import Table from "cli-table3";
33
import dayjs from "dayjs";
44
import stringWidth from "string-width";
@@ -145,3 +145,91 @@ export function printPostDetail(detail: PostDetailPayload, json = false): void {
145145

146146
process.stdout.write('\nUse "--json" to view the full content payload.\n');
147147
}
148+
149+
function getTagListWidths(): number[] {
150+
const width = resolveTerminalWidth();
151+
const nameWidth = 36;
152+
const slugWidth = 24;
153+
const colorWidth = 10;
154+
const createdAtWidth = 17;
155+
const reservedWidth = nameWidth + slugWidth + colorWidth + createdAtWidth + 8;
156+
const displayNameWidth = Math.min(Math.max(20, width - reservedWidth), 40);
157+
return [nameWidth, displayNameWidth, slugWidth, colorWidth, createdAtWidth];
158+
}
159+
160+
export function printTagList(list: TagList, json = false): void {
161+
if (json) {
162+
printJson(list);
163+
return;
164+
}
165+
166+
const widths = getTagListWidths();
167+
const table = new Table({
168+
head: ["NAME", "DISPLAY NAME", "SLUG", "COLOR", "CREATED AT"],
169+
colWidths: widths,
170+
colAligns: ["left", "left", "left", "left", "left"],
171+
style: {
172+
compact: true,
173+
head: [],
174+
border: [],
175+
"padding-left": 0,
176+
"padding-right": 0,
177+
},
178+
wordWrap: false,
179+
chars: {
180+
top: "",
181+
"top-mid": "",
182+
"top-left": "",
183+
"top-right": "",
184+
bottom: "",
185+
"bottom-mid": "",
186+
"bottom-left": "",
187+
"bottom-right": "",
188+
left: "",
189+
"left-mid": "",
190+
mid: "",
191+
"mid-mid": "",
192+
right: "",
193+
"right-mid": "",
194+
middle: " ",
195+
},
196+
});
197+
198+
for (const item of list.items) {
199+
table.push([
200+
item.metadata.name,
201+
truncateDisplayText(item.spec.displayName, widths[1]!),
202+
truncateDisplayText(item.spec.slug, widths[2]!),
203+
item.spec.color ?? "",
204+
formatTimestamp(item.metadata.creationTimestamp ?? undefined),
205+
]);
206+
}
207+
208+
process.stdout.write(`${table.toString()}\n`);
209+
printPaginationFooter({
210+
page: list.page,
211+
size: list.size,
212+
total: list.total,
213+
totalPages: list.totalPages,
214+
hasNext: list.hasNext,
215+
hasPrevious: list.hasPrevious,
216+
itemLabel: "tag",
217+
});
218+
}
219+
220+
export function printTag(tag: Tag, json = false, successMessage?: string): void {
221+
if (json) {
222+
printJson(tag);
223+
return;
224+
}
225+
226+
if (successMessage) {
227+
process.stdout.write(`${successMessage}\n\n`);
228+
}
229+
230+
printDetailObject({
231+
metadata: tag.metadata,
232+
spec: tag.spec,
233+
status: tag.status,
234+
});
235+
}

src/commands/post/index.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { checkbox, input } from "@inquirer/prompts";
66
import axios from "axios";
77
import cac, { type CAC } from "cac";
88

9-
import { tryRunCommandCliRoute } from "../../utils/command-router.js";
9+
import { tryRunCommandCliRoute, tryRunNestedCliRoute } from "../../utils/command-router.js";
1010
import { confirmDangerousAction } from "../../utils/confirmation.js";
1111
import { DEFAULT_CONTENT_RAW_TYPE, renderContentByRawType } from "../../utils/content.js";
1212
import { CliError } from "../../utils/errors.js";
@@ -38,6 +38,7 @@ import {
3838
resolvePostMarkdownImportPayload,
3939
writePostMarkdownDocument,
4040
} from "./markdown.js";
41+
import { buildTagCli } from "./tag.js";
4142
import type { PostMutationInput } from "./types.js";
4243

4344
interface PostCommandOptions {
@@ -1072,6 +1073,8 @@ function buildPostCli(runtime: RuntimeContext): CAC {
10721073
});
10731074
});
10741075

1076+
postCli.command("tag", "Tag management commands");
1077+
10751078
postCli.usage("<command> [flags]");
10761079
postCli.example((bin) => `${bin} list --page 1 --size 20`);
10771080
postCli.example((bin) => `${bin} get my-post --json`);
@@ -1097,6 +1100,21 @@ function buildPostCli(runtime: RuntimeContext): CAC {
10971100
}
10981101

10991102
export async function tryRunPostCommand(args: string[], runtime: RuntimeContext): Promise<boolean> {
1103+
if (args[0] !== "post") {
1104+
return false;
1105+
}
1106+
1107+
if (
1108+
await tryRunNestedCliRoute({
1109+
branch: "tag",
1110+
cliName: "halo post tag",
1111+
args,
1112+
buildCli: () => buildTagCli(runtime),
1113+
})
1114+
) {
1115+
return true;
1116+
}
1117+
11001118
return tryRunCommandCliRoute({
11011119
command: "post",
11021120
cliName: "halo post",

0 commit comments

Comments
 (0)