@nocoo/zhe 1.16.1 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,289 @@
1
+ /**
2
+ * zhe tag — Manage tags (create, update, delete, list)
3
+ *
4
+ * The plural `zhe tags` command (list-only) is preserved for back-compat;
5
+ * `zhe tag list` is provided as the consistent alias.
6
+ */
7
+ import * as readline from "node:readline";
8
+ import { defineCommand, pc } from "@nocoo/cli-base";
9
+ import { ApiClient, ApiClientError, EXIT_AUTH_REQUIRED, EXIT_ERROR, EXIT_INVALID_ARGS, EXIT_NOT_FOUND, EXIT_RATE_LIMITED, } from "../api/client.js";
10
+ import { getApiKey } from "../config.js";
11
+ import { formatTagsTable, resolveTagRef } from "../utils.js";
12
+ // ── Helpers ──
13
+ function requireAuth() {
14
+ const apiKey = getApiKey();
15
+ if (!apiKey) {
16
+ console.log(pc.red("Not authenticated. Run `zhe login` first."));
17
+ process.exit(EXIT_AUTH_REQUIRED);
18
+ }
19
+ return apiKey;
20
+ }
21
+ function handleApiError(error) {
22
+ if (error instanceof ApiClientError) {
23
+ if (error.status === 404) {
24
+ console.log(pc.red("Tag not found."));
25
+ process.exit(EXIT_NOT_FOUND);
26
+ }
27
+ console.log(pc.red(`Error: ${error.message}`));
28
+ if (error.status === 401) {
29
+ process.exit(EXIT_AUTH_REQUIRED);
30
+ }
31
+ if (error.status === 429) {
32
+ process.exit(EXIT_RATE_LIMITED);
33
+ }
34
+ process.exit(EXIT_ERROR);
35
+ }
36
+ throw error;
37
+ }
38
+ async function confirm(message) {
39
+ return new Promise((resolve) => {
40
+ const rl = readline.createInterface({
41
+ input: process.stdin,
42
+ output: process.stdout,
43
+ });
44
+ rl.question(`${message} [y/N] `, (answer) => {
45
+ rl.close();
46
+ resolve(answer.toLowerCase() === "y");
47
+ });
48
+ });
49
+ }
50
+ const HEX_COLOR_PATTERN = /^#?[0-9A-Fa-f]{6}$/;
51
+ /**
52
+ * Validate hex color and normalize to `#rrggbb` form.
53
+ * Returns null with an error printed if invalid.
54
+ */
55
+ function normalizeColor(input) {
56
+ if (!HEX_COLOR_PATTERN.test(input)) {
57
+ console.log(pc.red("--color must be a 6-digit hex color (e.g. '#3b82f6' or 'ff5500')."));
58
+ return null;
59
+ }
60
+ return input.startsWith("#") ? input : `#${input}`;
61
+ }
62
+ // ── Subcommands ──
63
+ const listSubcommand = defineCommand({
64
+ meta: {
65
+ name: "list",
66
+ description: "List all tags",
67
+ },
68
+ args: {
69
+ json: {
70
+ type: "boolean",
71
+ description: "Output as JSON",
72
+ },
73
+ },
74
+ async run({ args }) {
75
+ const apiKey = requireAuth();
76
+ const client = new ApiClient(apiKey);
77
+ try {
78
+ const response = await client.listTags();
79
+ if (args.json) {
80
+ console.log(JSON.stringify(response, null, 2));
81
+ return;
82
+ }
83
+ if (response.tags.length === 0) {
84
+ console.log(pc.dim("No tags found."));
85
+ return;
86
+ }
87
+ console.log(formatTagsTable(response.tags));
88
+ }
89
+ catch (error) {
90
+ handleApiError(error);
91
+ }
92
+ },
93
+ });
94
+ const createSubcommand = defineCommand({
95
+ meta: {
96
+ name: "create",
97
+ description: "Create a new tag",
98
+ },
99
+ args: {
100
+ name: {
101
+ type: "positional",
102
+ description: "Tag name",
103
+ required: true,
104
+ },
105
+ color: {
106
+ type: "string",
107
+ alias: "c",
108
+ description: "Tag color as 6-digit hex (e.g. '#3b82f6' or 'ff5500')",
109
+ required: true,
110
+ },
111
+ json: {
112
+ type: "boolean",
113
+ description: "Output as JSON",
114
+ },
115
+ },
116
+ async run({ args }) {
117
+ const apiKey = requireAuth();
118
+ const client = new ApiClient(apiKey);
119
+ const name = args.name.trim();
120
+ if (!name) {
121
+ console.log(pc.red("Tag name cannot be empty."));
122
+ process.exit(EXIT_INVALID_ARGS);
123
+ }
124
+ const color = normalizeColor(args.color);
125
+ if (color === null) {
126
+ process.exit(EXIT_INVALID_ARGS);
127
+ }
128
+ const request = { name, color };
129
+ try {
130
+ const response = await client.createTag(request);
131
+ if (args.json) {
132
+ console.log(JSON.stringify(response, null, 2));
133
+ return;
134
+ }
135
+ const { tag } = response;
136
+ console.log(pc.green(`✓ Created tag ${pc.cyan(tag.name)} ${pc.dim(`(${tag.id})`)} ${tag.color}`));
137
+ }
138
+ catch (error) {
139
+ handleApiError(error);
140
+ }
141
+ },
142
+ });
143
+ const updateSubcommand = defineCommand({
144
+ meta: {
145
+ name: "update",
146
+ description: "Update a tag (rename or change color)",
147
+ },
148
+ args: {
149
+ ref: {
150
+ type: "positional",
151
+ description: "Tag name or UUID",
152
+ required: true,
153
+ },
154
+ name: {
155
+ type: "string",
156
+ alias: "n",
157
+ description: "New tag name",
158
+ },
159
+ color: {
160
+ type: "string",
161
+ alias: "c",
162
+ description: "New color as 6-digit hex (e.g. '#3b82f6' or 'ff5500')",
163
+ },
164
+ json: {
165
+ type: "boolean",
166
+ description: "Output as JSON",
167
+ },
168
+ },
169
+ async run({ args }) {
170
+ const apiKey = requireAuth();
171
+ const client = new ApiClient(apiKey);
172
+ const ref = args.ref;
173
+ const request = {};
174
+ if (args.name !== undefined) {
175
+ const trimmed = args.name.trim();
176
+ if (!trimmed) {
177
+ console.log(pc.red("--name cannot be empty."));
178
+ process.exit(EXIT_INVALID_ARGS);
179
+ }
180
+ request.name = trimmed;
181
+ }
182
+ if (args.color !== undefined) {
183
+ const color = normalizeColor(args.color);
184
+ if (color === null) {
185
+ process.exit(EXIT_INVALID_ARGS);
186
+ }
187
+ request.color = color;
188
+ }
189
+ if (Object.keys(request).length === 0) {
190
+ console.log(pc.yellow("No changes specified. Pass --name and/or --color."));
191
+ process.exit(EXIT_INVALID_ARGS);
192
+ }
193
+ try {
194
+ const resolved = await resolveTagRef(client, ref);
195
+ if (resolved.kind === "not_found") {
196
+ console.log(pc.red(`Tag not found: ${ref}.`));
197
+ process.exit(EXIT_NOT_FOUND);
198
+ }
199
+ if (resolved.kind === "ambiguous") {
200
+ console.log(pc.red(`Multiple tags match "${ref}". Please use the tag ID or rename duplicates.`));
201
+ process.exit(EXIT_INVALID_ARGS);
202
+ }
203
+ const response = await client.updateTag(resolved.id, request);
204
+ if (args.json) {
205
+ console.log(JSON.stringify(response, null, 2));
206
+ return;
207
+ }
208
+ const { tag } = response;
209
+ console.log(pc.green(`✓ Updated tag ${pc.cyan(tag.name)} ${pc.dim(`(${tag.id})`)} ${tag.color}`));
210
+ }
211
+ catch (error) {
212
+ handleApiError(error);
213
+ }
214
+ },
215
+ });
216
+ const deleteSubcommand = defineCommand({
217
+ meta: {
218
+ name: "delete",
219
+ description: "Delete a tag",
220
+ },
221
+ args: {
222
+ ref: {
223
+ type: "positional",
224
+ description: "Tag name or UUID",
225
+ required: true,
226
+ },
227
+ yes: {
228
+ type: "boolean",
229
+ alias: "y",
230
+ description: "Skip confirmation prompt",
231
+ },
232
+ json: {
233
+ type: "boolean",
234
+ description: "Output as JSON",
235
+ },
236
+ },
237
+ async run({ args }) {
238
+ const apiKey = requireAuth();
239
+ const client = new ApiClient(apiKey);
240
+ const ref = args.ref;
241
+ try {
242
+ const resolved = await resolveTagRef(client, ref);
243
+ if (resolved.kind === "not_found") {
244
+ console.log(pc.red(`Tag not found: ${ref}.`));
245
+ process.exit(EXIT_NOT_FOUND);
246
+ }
247
+ if (resolved.kind === "ambiguous") {
248
+ console.log(pc.red(`Multiple tags match "${ref}". Please use the tag ID or rename duplicates.`));
249
+ process.exit(EXIT_INVALID_ARGS);
250
+ }
251
+ const { id: tagId, name: tagName } = resolved;
252
+ const tagLabel = tagName ?? tagId;
253
+ if (!args.yes) {
254
+ if (!process.stdin.isTTY) {
255
+ console.log(pc.red("Refusing to delete without --yes in non-interactive mode."));
256
+ process.exit(EXIT_INVALID_ARGS);
257
+ }
258
+ const confirmed = await confirm(`Delete tag "${tagLabel}" (${tagId})?`);
259
+ if (!confirmed) {
260
+ console.log(pc.dim("Cancelled."));
261
+ return;
262
+ }
263
+ }
264
+ await client.deleteTag(tagId);
265
+ if (args.json) {
266
+ console.log(JSON.stringify({ success: true, id: tagId, name: tagName ?? null }, null, 2));
267
+ return;
268
+ }
269
+ console.log(pc.green(`✓ Deleted tag ${pc.cyan(tagLabel)} ${pc.dim(`(${tagId})`)}`));
270
+ }
271
+ catch (error) {
272
+ handleApiError(error);
273
+ }
274
+ },
275
+ });
276
+ // ── Main command ──
277
+ export const tagCommand = defineCommand({
278
+ meta: {
279
+ name: "tag",
280
+ description: "Manage tags (create, update, delete, list)",
281
+ },
282
+ subCommands: {
283
+ create: createSubcommand,
284
+ update: updateSubcommand,
285
+ delete: deleteSubcommand,
286
+ list: listSubcommand,
287
+ },
288
+ });
289
+ //# sourceMappingURL=tag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tag.js","sourceRoot":"","sources":["../../src/commands/tag.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  import { defineCommand, runMain } from "@nocoo/cli-base";
6
6
  import { createCommand } from "./commands/create.js";
7
7
  import { deleteCommand } from "./commands/delete.js";
8
+ import { folderCommand } from "./commands/folder.js";
8
9
  import { foldersCommand } from "./commands/folders.js";
9
10
  import { getCommand } from "./commands/get.js";
10
11
  import { ideaCommand } from "./commands/idea.js";
@@ -13,6 +14,7 @@ import { listCommand } from "./commands/list.js";
13
14
  import { loginCommand } from "./commands/login.js";
14
15
  import { logoutCommand } from "./commands/logout.js";
15
16
  import { openCommand } from "./commands/open.js";
17
+ import { tagCommand } from "./commands/tag.js";
16
18
  import { tagsCommand } from "./commands/tags.js";
17
19
  import { updateCommand } from "./commands/update.js";
18
20
  import { CLI_VERSION } from "./version.js";
@@ -28,7 +30,9 @@ const main = defineCommand({
28
30
  list: listCommand,
29
31
  inbox: inboxCommand,
30
32
  folders: foldersCommand,
33
+ folder: folderCommand,
31
34
  tags: tagsCommand,
35
+ tag: tagCommand,
32
36
  idea: ideaCommand,
33
37
  create: createCommand,
34
38
  get: getCommand,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,IAAI,GAAG,aAAa,CAAC;IAC1B,IAAI,EAAE;QACL,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,WAAW;QACpB,WAAW,EAAE,qCAAqC;KAClD;IACD,WAAW,EAAE;QACZ,KAAK,EAAE,YAAY;QACnB,MAAM,EAAE,aAAa;QACrB,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,YAAY;QACnB,OAAO,EAAE,cAAc;QACvB,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,WAAW;QACjB,MAAM,EAAE,aAAa;QACrB,GAAG,EAAE,UAAU;QACf,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,aAAa;QACrB,IAAI,EAAE,WAAW;KACjB;CACD,CAAC,CAAC;AAEH,OAAO,CAAC,IAAI,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,IAAI,GAAG,aAAa,CAAC;IAC1B,IAAI,EAAE;QACL,IAAI,EAAE,KAAK;QACX,OAAO,EAAE,WAAW;QACpB,WAAW,EAAE,qCAAqC;KAClD;IACD,WAAW,EAAE;QACZ,KAAK,EAAE,YAAY;QACnB,MAAM,EAAE,aAAa;QACrB,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,YAAY;QACnB,OAAO,EAAE,cAAc;QACvB,MAAM,EAAE,aAAa;QACrB,IAAI,EAAE,WAAW;QACjB,GAAG,EAAE,UAAU;QACf,IAAI,EAAE,WAAW;QACjB,MAAM,EAAE,aAAa;QACrB,GAAG,EAAE,UAAU;QACf,MAAM,EAAE,aAAa;QACrB,MAAM,EAAE,aAAa;QACrB,IAAI,EAAE,WAAW;KACjB;CACD,CAAC,CAAC;AAEH,OAAO,CAAC,IAAI,CAAC,CAAC"}
package/dist/utils.d.ts CHANGED
@@ -29,6 +29,8 @@ export declare function truncate(str: string, maxLength: number): string;
29
29
  export interface FormatLinksTableOptions {
30
30
  wide?: boolean;
31
31
  folderMap?: Map<string, string>;
32
+ tagMap?: Map<string, string>;
33
+ showTags?: boolean;
32
34
  }
33
35
  /**
34
36
  * Format links as a table
@@ -42,10 +44,13 @@ export declare function formatLinksMinimal(links: Link[]): string;
42
44
  * Format a single link for detailed display
43
45
  * @param link - The link to format
44
46
  * @param folderName - Optional folder name (if not provided, shows folder ID)
47
+ * @param tagMap - Optional map of tagId → name (falls back to tagId if missing)
45
48
  */
46
- export declare function formatLinkDetail(link: Link, folderName?: string): string;
49
+ export declare function formatLinkDetail(link: Link, folderName?: string, tagMap?: Map<string, string>): string;
50
+ export declare function parsePositiveInt(input: string): number | null;
47
51
  /**
48
- * Parse a link ID from string, return null if invalid
52
+ * Parse a link ID from string, return null if invalid.
53
+ * Uses strict positive-integer parsing.
49
54
  */
50
55
  export declare function parseLinkId(input: string): number | null;
51
56
  /**
@@ -62,10 +67,39 @@ export declare function formatTagsTable(tags: Tag[]): string;
62
67
  * Returns null if folder is not found (with error message printed).
63
68
  */
64
69
  export declare function resolveFolderName(client: ApiClient, input: string): Promise<string | null>;
70
+ /**
71
+ * Open a URL in the default browser.
72
+ * Uses spawn() with argv to prevent shell injection.
73
+ */
74
+ export declare function openInBrowser(url: string): void;
65
75
  /**
66
76
  * Resolve a tag name to tag ID via API.
67
77
  * If input looks like a UUID, use it directly.
68
78
  * Returns null if tag is not found (with error message printed).
69
79
  */
70
80
  export declare function resolveTagName(client: ApiClient, input: string): Promise<string | null>;
81
+ /**
82
+ * Resolve a tag reference (name or UUID) to its canonical id + name.
83
+ *
84
+ * Unlike `resolveTagName`, this:
85
+ * - Distinguishes `not_found` (zero matches) from `ambiguous` (multiple
86
+ * name matches), letting callers map them to different exit codes.
87
+ * - For UUID input, returns immediately without calling `listTags()` so
88
+ * that API keys with only `tags:write` (no `tags:read`) can still
89
+ * update/delete by ID. The server is the source of truth for whether
90
+ * the tag exists. `name` is `undefined` in that case; callers should
91
+ * fall back to the UUID in any prompt text.
92
+ *
93
+ * Does NOT print any error itself — callers decide messaging and exit codes.
94
+ */
95
+ export type TagRef = {
96
+ kind: "found";
97
+ id: string;
98
+ name: string | undefined;
99
+ } | {
100
+ kind: "not_found";
101
+ } | {
102
+ kind: "ambiguous";
103
+ };
104
+ export declare function resolveTagRef(client: ApiClient, input: string): Promise<TagRef>;
71
105
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAExD;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,GAAE,uBAA4B,GACnC,MAAM,CAyDR;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAKxD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,CAyBxE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMxD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAmB5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAkBnD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8BxB;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CACnC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4BxB"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,gBAAgB,CAAC;AAExD;;GAEG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOjD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAIlD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGtD;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAK/D;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACvC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC/B,KAAK,EAAE,IAAI,EAAE,EACb,OAAO,GAAE,uBAA4B,GACnC,MAAM,CA+ER;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAKxD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC/B,IAAI,EAAE,IAAI,EACV,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC1B,MAAM,CAmCR;AAUD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS7D;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAExD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,CAmB5D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAkBnD;AAED;;;;GAIG;AACH,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA8BxB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAgC/C;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CACnC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA4BxB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,MAAM,GACf;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACvD;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAEzB,wBAAsB,aAAa,CAClC,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CAyBjB"}
package/dist/utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Shared utilities for zhe CLI
3
3
  */
4
+ import { spawn } from "node:child_process";
4
5
  import { pc } from "@nocoo/cli-base";
5
6
  /**
6
7
  * Mask an API key for display: zhe_abcd...wxyz
@@ -50,13 +51,27 @@ export function formatLinksTable(links, options = {}) {
50
51
  if (links.length === 0) {
51
52
  return "No links found.";
52
53
  }
53
- const { wide = false, folderMap } = options;
54
+ const { wide = false, folderMap, tagMap, showTags = false } = options;
54
55
  const showFolders = links.some((l) => l.folderId);
56
+ const formatTags = (link) => {
57
+ // Prefer embedded tags (carries names) — avoids needing a tagMap
58
+ // fetch from the caller when /links already returned tag details.
59
+ if (link.tags && link.tags.length > 0) {
60
+ return link.tags.map((t) => t.name).join(",");
61
+ }
62
+ const ids = link.tagIds ?? [];
63
+ if (ids.length === 0)
64
+ return "";
65
+ return ids.map((id) => tagMap?.get(id) ?? id.slice(0, 8)).join(",");
66
+ };
55
67
  if (wide) {
56
68
  // Wide mode: no truncation
57
- const header = showFolders
58
- ? "ID SLUG URL FOLDER CLICKS CREATED"
59
- : "ID SLUG URL CLICKS CREATED";
69
+ let header = "ID SLUG URL ";
70
+ if (showFolders)
71
+ header += "FOLDER ";
72
+ if (showTags)
73
+ header += "TAGS ";
74
+ header += "CLICKS CREATED";
60
75
  const separator = "─".repeat(header.length);
61
76
  const rows = links.map((link) => {
62
77
  const id = String(link.id).padEnd(6);
@@ -64,21 +79,28 @@ export function formatLinksTable(links, options = {}) {
64
79
  const url = link.originalUrl.padEnd(48);
65
80
  const clicks = String(link.clicks).padEnd(7);
66
81
  const created = formatDate(link.createdAt);
82
+ let row = `${id} ${slug} ${url} `;
67
83
  if (showFolders) {
68
84
  const folderName = link.folderId
69
85
  ? (folderMap?.get(link.folderId) ?? link.folderId.slice(0, 8))
70
86
  : "";
71
- const folder = folderName.padEnd(16);
72
- return `${id} ${slug} ${url} ${folder} ${clicks} ${created}`;
87
+ row += `${folderName.padEnd(16)} `;
88
+ }
89
+ if (showTags) {
90
+ row += `${formatTags(link).padEnd(26)} `;
73
91
  }
74
- return `${id} ${slug} ${url} ${clicks} ${created}`;
92
+ row += `${clicks} ${created}`;
93
+ return row;
75
94
  });
76
95
  return [header, separator, ...rows].join("\n");
77
96
  }
78
97
  // Default compact mode with truncation
79
- const header = showFolders
80
- ? "ID SLUG URL FOLDER CLICKS CREATED"
81
- : "ID SLUG URL CLICKS CREATED";
98
+ let header = "ID SLUG URL ";
99
+ if (showFolders)
100
+ header += "FOLDER ";
101
+ if (showTags)
102
+ header += "TAGS ";
103
+ header += "CLICKS CREATED";
82
104
  const separator = "─".repeat(header.length);
83
105
  const rows = links.map((link) => {
84
106
  const id = String(link.id).padEnd(6);
@@ -86,14 +108,18 @@ export function formatLinksTable(links, options = {}) {
86
108
  const url = truncate(link.originalUrl, 32).padEnd(32);
87
109
  const clicks = String(link.clicks).padEnd(7);
88
110
  const created = formatDate(link.createdAt);
111
+ let row = `${id} ${slug} ${url} `;
89
112
  if (showFolders) {
90
113
  const folderName = link.folderId
91
114
  ? (folderMap?.get(link.folderId) ?? link.folderId.slice(0, 8))
92
115
  : "";
93
- const folder = truncate(folderName, 12).padEnd(12);
94
- return `${id} ${slug} ${url} ${folder} ${clicks} ${created}`;
116
+ row += `${truncate(folderName, 12).padEnd(12)} `;
117
+ }
118
+ if (showTags) {
119
+ row += `${truncate(formatTags(link), 12).padEnd(12)} `;
95
120
  }
96
- return `${id} ${slug} ${url} ${clicks} ${created}`;
121
+ row += `${clicks} ${created}`;
122
+ return row;
97
123
  });
98
124
  return [header, separator, ...rows].join("\n");
99
125
  }
@@ -110,8 +136,9 @@ export function formatLinksMinimal(links) {
110
136
  * Format a single link for detailed display
111
137
  * @param link - The link to format
112
138
  * @param folderName - Optional folder name (if not provided, shows folder ID)
139
+ * @param tagMap - Optional map of tagId → name (falls back to tagId if missing)
113
140
  */
114
- export function formatLinkDetail(link, folderName) {
141
+ export function formatLinkDetail(link, folderName, tagMap) {
115
142
  const lines = [
116
143
  `Link #${link.id}`,
117
144
  "",
@@ -124,6 +151,16 @@ export function formatLinkDetail(link, folderName) {
124
151
  const folderDisplay = folderName ?? link.folderId;
125
152
  lines.push(` Folder: ${folderDisplay}`);
126
153
  }
154
+ if (link.tags && link.tags.length > 0) {
155
+ lines.push(` Tags: ${link.tags.map((t) => t.name).join(", ")}`);
156
+ }
157
+ else {
158
+ const tagIds = link.tagIds ?? [];
159
+ if (tagIds.length > 0) {
160
+ const tagDisplay = tagIds.map((id) => tagMap?.get(id) ?? id).join(", ");
161
+ lines.push(` Tags: ${tagDisplay}`);
162
+ }
163
+ }
127
164
  if (link.note) {
128
165
  lines.push(` Note: ${link.note}`);
129
166
  }
@@ -132,15 +169,29 @@ export function formatLinkDetail(link, folderName) {
132
169
  return lines.join("\n");
133
170
  }
134
171
  /**
135
- * Parse a link ID from string, return null if invalid
172
+ * Strict positive-integer parser.
173
+ * Rejects empty strings, 0, negatives, decimals, scientific notation,
174
+ * hex literals, and any junk-suffixed input (e.g. "12abc", "1e2", "0x10").
175
+ * Only accepts decimal digit sequences without a leading zero.
136
176
  */
137
- export function parseLinkId(input) {
138
- const id = Number.parseInt(input, 10);
139
- if (Number.isNaN(id) || id <= 0) {
177
+ const STRICT_POSITIVE_INT = /^[1-9]\d*$/;
178
+ export function parsePositiveInt(input) {
179
+ if (!STRICT_POSITIVE_INT.test(input)) {
180
+ return null;
181
+ }
182
+ const id = Number(input);
183
+ if (!Number.isSafeInteger(id) || id <= 0) {
140
184
  return null;
141
185
  }
142
186
  return id;
143
187
  }
188
+ /**
189
+ * Parse a link ID from string, return null if invalid.
190
+ * Uses strict positive-integer parsing.
191
+ */
192
+ export function parseLinkId(input) {
193
+ return parsePositiveInt(input);
194
+ }
144
195
  /**
145
196
  * Format folders as a table
146
197
  */
@@ -203,6 +254,43 @@ export async function resolveFolderName(client, input) {
203
254
  }
204
255
  return matches[0].id;
205
256
  }
257
+ /**
258
+ * Open a URL in the default browser.
259
+ * Uses spawn() with argv to prevent shell injection.
260
+ */
261
+ export function openInBrowser(url) {
262
+ const platform = process.platform;
263
+ let command;
264
+ let args;
265
+ if (platform === "darwin") {
266
+ command = "open";
267
+ args = [url];
268
+ }
269
+ else if (platform === "linux") {
270
+ command = "xdg-open";
271
+ args = [url];
272
+ }
273
+ else if (platform === "win32") {
274
+ command = "cmd";
275
+ args = ["/c", "start", "", url];
276
+ }
277
+ else {
278
+ console.log(pc.dim(`Unable to open browser on ${platform}. Visit: ${url}`));
279
+ return;
280
+ }
281
+ const child = spawn(command, args, { stdio: "ignore" });
282
+ const fallback = () => console.log(pc.dim(`Failed to open browser. Visit: ${url}`));
283
+ let failed = false;
284
+ child.on("error", () => {
285
+ failed = true;
286
+ fallback();
287
+ });
288
+ child.on("close", (code) => {
289
+ if (!failed && code !== 0) {
290
+ fallback();
291
+ }
292
+ });
293
+ }
206
294
  /**
207
295
  * Resolve a tag name to tag ID via API.
208
296
  * If input looks like a UUID, use it directly.
@@ -226,4 +314,25 @@ export async function resolveTagName(client, input) {
226
314
  }
227
315
  return matches[0].id;
228
316
  }
317
+ export async function resolveTagRef(client, input) {
318
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
319
+ if (uuidPattern.test(input)) {
320
+ // Trust the UUID; let the destructive op's PATCH/DELETE be the
321
+ // authoritative existence check. Avoids requiring tags:read.
322
+ // Normalize to lowercase: tag IDs are stored lowercase, but the
323
+ // regex accepts uppercase (RFC 4122 allows either case in
324
+ // notation). The DB is `WHERE id = ?` text-equal, so an
325
+ // uppercase UUID would 404 on a tag that actually exists.
326
+ return { kind: "found", id: input.toLowerCase(), name: undefined };
327
+ }
328
+ const { tags } = await client.listTags();
329
+ const matches = tags.filter((t) => t.name.toLowerCase() === input.toLowerCase());
330
+ if (matches.length === 0) {
331
+ return { kind: "not_found" };
332
+ }
333
+ if (matches.length > 1) {
334
+ return { kind: "ambiguous" };
335
+ }
336
+ return { kind: "found", id: matches[0].id, name: matches[0].name };
337
+ }
229
338
  //# sourceMappingURL=utils.js.map