@fanzie/task-cli 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +55 -0
  2. package/dist/cli.js +598 -0
  3. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,55 @@
1
+ # @fanzie/task-cli
2
+
3
+ CLI tool for managing Feature/UserStory/AC task hierarchies in Feishu Bitable.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @fanzie/task-cli
9
+ ```
10
+
11
+ ## Setup
12
+
13
+ ```bash
14
+ fz-task-cli auth init
15
+ ```
16
+
17
+ ## Commands
18
+
19
+ ### Feature
20
+
21
+ - `fz-task-cli feature add <title>` — Create a Feature
22
+ - `fz-task-cli feature add --json '{"title":"x","priority":"P0"}'` — Create with fields
23
+ - `fz-task-cli feature list` — List all Features
24
+ - `fz-task-cli feature get <id>` — Get Feature detail + child Stories
25
+ - `fz-task-cli feature update <id> --status TEST_SPEC` — Update fields
26
+ - `fz-task-cli feature delete <id>` — Delete (no children required)
27
+
28
+ ### Story
29
+
30
+ - `fz-task-cli story add <title> --feature <id>` — Create under Feature
31
+ - `fz-task-cli story list [--feature <id>]` — List Stories
32
+ - `fz-task-cli story get <id>` — Get Story + child ACs
33
+ - `fz-task-cli story update <id> --status DOING` — Update
34
+ - `fz-task-cli story delete <id>` — Delete (no child ACs required)
35
+
36
+ ### AC (Acceptance Criteria)
37
+
38
+ - `fz-task-cli ac add <title> --story <id>` — Create under Story
39
+ - `fz-task-cli ac list [--story <id>]` — List ACs
40
+ - `fz-task-cli ac get <id>` — Get AC detail
41
+ - `fz-task-cli ac update <id> --status TESTING` — Update
42
+ - `fz-task-cli ac delete <id>` — Delete
43
+
44
+ ### Options
45
+
46
+ - `--format json` — JSON output
47
+ - `--profile <name>` — Config profile
48
+
49
+ ## Status Flow
50
+
51
+ ```
52
+ TODO → TEST_SPEC → DOING → TESTING → VERIFIED → DONE
53
+ ↕ ↕ ↕ ↕
54
+ BLOCKED (from any, except DONE)
55
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,598 @@
1
+ #!/usr/bin/env node
2
+ import { Command as e } from "commander";
3
+ import * as t from "node:readline";
4
+ import * as n from "node:fs";
5
+ import * as r from "node:path";
6
+ import * as i from "node:os";
7
+ import * as a from "js-yaml";
8
+ import * as o from "@larksuiteoapi/node-sdk";
9
+ //#region src/config.ts
10
+ var s = r.join(i.homedir(), ".config", "fz-task"), c = r.join(s, "config.yaml"), l = r.join(s, ".env");
11
+ function u(e) {
12
+ let t = new URL(e), n = t.pathname.split("/").filter(Boolean), r = n.indexOf("wiki"), i = n.indexOf("base"), a = r === -1 ? i : r;
13
+ if (a === -1 || a + 1 >= n.length) throw Error(`Invalid bitable URL: cannot extract appToken from ${e}`);
14
+ let o = n[a + 1], s = t.searchParams.get("table");
15
+ if (!s) throw Error(`Invalid bitable URL: missing table parameter in ${e}`);
16
+ return {
17
+ appToken: o,
18
+ tableId: s
19
+ };
20
+ }
21
+ function d(e, t) {
22
+ return e.replace(/\$\{(\w+)\}/g, (e, n) => t[n] ?? e);
23
+ }
24
+ function f(e) {
25
+ let t = e ?? l, r = {};
26
+ if (!n.existsSync(t)) return r;
27
+ let i = n.readFileSync(t, "utf-8");
28
+ for (let e of i.split("\n")) {
29
+ let t = e.trim();
30
+ if (!t || t.startsWith("#")) continue;
31
+ let n = t.indexOf("=");
32
+ if (n === -1) continue;
33
+ let i = t.slice(0, n).trim();
34
+ r[i] = t.slice(n + 1).trim();
35
+ }
36
+ return r;
37
+ }
38
+ function p() {
39
+ let e = f(r.join(process.cwd(), ".env")), t = f(), i = {
40
+ ...process.env,
41
+ ...t,
42
+ ...e
43
+ };
44
+ if (!n.existsSync(c)) return null;
45
+ let o = n.readFileSync(c, "utf-8"), s = a.load(o);
46
+ if (!s?.profiles) return null;
47
+ let l = {
48
+ profiles: {},
49
+ defaultProfile: s.defaultProfile ?? "default"
50
+ };
51
+ for (let [e, t] of Object.entries(s.profiles)) {
52
+ let n = t;
53
+ l.profiles[e] = {
54
+ auth: {
55
+ appId: d(n.auth?.appId ?? "", i),
56
+ appSecret: d(n.auth?.appSecret ?? "", i)
57
+ },
58
+ bitable: {
59
+ url: n.bitable?.url ?? "",
60
+ appToken: n.bitable?.appToken ?? "",
61
+ tableId: n.bitable?.tableId ?? ""
62
+ }
63
+ };
64
+ }
65
+ return l;
66
+ }
67
+ function m(e, t) {
68
+ let n = t ?? process.env.FZ_TASK_PROFILE ?? e.defaultProfile, r = e.profiles[n];
69
+ if (!r) throw Error(`Profile "${n}" not found. Available: ${Object.keys(e.profiles).join(", ")}`);
70
+ return r;
71
+ }
72
+ function h() {
73
+ n.mkdirSync(s, { recursive: !0 });
74
+ }
75
+ function g(e) {
76
+ h();
77
+ let t = a.dump(e);
78
+ n.writeFileSync(c, t, "utf-8");
79
+ }
80
+ function _(e, t) {
81
+ h();
82
+ let r = `FZ_TASK_APP_ID=${e}\nFZ_TASK_APP_SECRET=${t}\n`;
83
+ n.writeFileSync(l, r, "utf-8");
84
+ }
85
+ //#endregion
86
+ //#region src/client.ts
87
+ function v(e) {
88
+ let t = {};
89
+ return e.title !== void 0 && (t.Text = e.title), e.type !== void 0 && (t.type = e.type), e.status !== void 0 && (t.status = e.status), e.parent !== void 0 && (t.parent = e.parent), e.doc_url !== void 0 && (t.doc_url = {
90
+ link: e.doc_url,
91
+ text: e.doc_url
92
+ }), e.assignee !== void 0 && (t.assignee = e.assignee), e.description !== void 0 && (t.description = e.description), e.previous_status !== void 0 && (t.previous_status = e.previous_status), e.priority !== void 0 && (t.priority = e.priority), t;
93
+ }
94
+ function y(e) {
95
+ let t = e.fields, n, r = t.parent;
96
+ if (Array.isArray(r) && r.length > 0) {
97
+ let e = r[0];
98
+ e.record_ids?.length > 0 && (n = e.record_ids);
99
+ }
100
+ let i;
101
+ return typeof t.doc_url == "object" && t.doc_url !== null ? i = t.doc_url.link : typeof t.doc_url == "string" && (i = t.doc_url), {
102
+ record_id: e.record_id,
103
+ fields: {
104
+ title: t.Text ?? "",
105
+ type: t.type,
106
+ status: t.status,
107
+ parent: n,
108
+ priority: t.priority,
109
+ doc_url: i,
110
+ assignee: t.assignee,
111
+ description: t.description,
112
+ previous_status: t.previous_status
113
+ }
114
+ };
115
+ }
116
+ var b = class {
117
+ larkClient;
118
+ appToken;
119
+ tableId;
120
+ constructor(e) {
121
+ this.appToken = e.appToken, this.tableId = e.tableId, this.larkClient = new o.Client({
122
+ appId: e.appId,
123
+ appSecret: e.appSecret,
124
+ appType: o.AppType.SelfBuild,
125
+ domain: o.Domain.Feishu
126
+ });
127
+ }
128
+ async createRecord(e) {
129
+ let t = v(e), n = await this.larkClient.bitable.appTableRecord.create({
130
+ path: {
131
+ app_token: this.appToken,
132
+ table_id: this.tableId
133
+ },
134
+ data: { fields: t }
135
+ });
136
+ if (n.code !== 0) throw Error(`createRecord failed: ${n.msg}`);
137
+ let r = n.data?.record;
138
+ if (!r) throw Error("createRecord: no record in response");
139
+ return y(r);
140
+ }
141
+ async getRecord(e) {
142
+ let t = await this.larkClient.bitable.appTableRecord.get({ path: {
143
+ app_token: this.appToken,
144
+ table_id: this.tableId,
145
+ record_id: e
146
+ } });
147
+ if (t.code !== 0) throw Error(`getRecord failed: ${t.msg}`);
148
+ let n = t.data?.record;
149
+ if (!n) throw Error("getRecord: no record in response");
150
+ return y(n);
151
+ }
152
+ async listRecords(e) {
153
+ let t = [];
154
+ e?.type && t.push(`CurrentValue.[type] = "${e.type}"`), e?.status && t.push(`CurrentValue.[status] = "${e.status}"`);
155
+ let n = t.length > 0 ? t.length === 1 ? t[0] : `AND(${t.join(",")})` : void 0, r = [], i;
156
+ do {
157
+ let e = { page_size: 500 };
158
+ n && (e.filter = n), i && (e.page_token = i);
159
+ let t = await this.larkClient.bitable.appTableRecord.list({
160
+ path: {
161
+ app_token: this.appToken,
162
+ table_id: this.tableId
163
+ },
164
+ params: e
165
+ });
166
+ if (t.code !== 0) throw Error(`listRecords failed: ${t.msg}`);
167
+ let a = t.data?.items ?? [];
168
+ for (let e of a) r.push(y(e));
169
+ i = t.data?.has_more ? t.data.page_token : void 0;
170
+ } while (i);
171
+ return r;
172
+ }
173
+ async updateRecord(e, t) {
174
+ let n = v(t), r = await this.larkClient.bitable.appTableRecord.update({
175
+ path: {
176
+ app_token: this.appToken,
177
+ table_id: this.tableId,
178
+ record_id: e
179
+ },
180
+ data: { fields: n }
181
+ });
182
+ if (r.code !== 0) throw Error(`updateRecord failed: ${r.msg}`);
183
+ let i = r.data?.record;
184
+ if (!i) throw Error("updateRecord: no record in response");
185
+ return y(i);
186
+ }
187
+ async deleteRecord(e) {
188
+ let t = await this.larkClient.bitable.appTableRecord.delete({ path: {
189
+ app_token: this.appToken,
190
+ table_id: this.tableId,
191
+ record_id: e
192
+ } });
193
+ if (t.code !== 0) throw Error(`deleteRecord failed: ${t.msg}`);
194
+ }
195
+ async getChildren(e) {
196
+ return (await this.listRecords()).filter((t) => Array.isArray(t.fields.parent) && t.fields.parent.includes(e));
197
+ }
198
+ };
199
+ //#endregion
200
+ //#region src/commands/auth.ts
201
+ function x(e) {
202
+ let n = t.createInterface({
203
+ input: process.stdin,
204
+ output: process.stdout
205
+ });
206
+ return new Promise((t) => {
207
+ n.question(e, (e) => {
208
+ n.close(), t(e.trim());
209
+ });
210
+ });
211
+ }
212
+ function S(e) {
213
+ let t = e.command("auth").description("Authentication management");
214
+ t.command("status").description("Show current config and connection status").action(async () => {
215
+ let t = p();
216
+ t || (console.error("No config found. Run `fz-task-cli auth init` to set up."), process.exit(3));
217
+ try {
218
+ let n = e.opts().profile ?? t.defaultProfile, r = m(t, e.opts().profile);
219
+ console.log(`Profile: ${n}`), console.log(`App ID: ${r.auth.appId.slice(0, 6)}...${r.auth.appId.slice(-4)}`), console.log(`App Token: ${r.bitable.appToken}`), console.log(`Table ID: ${r.bitable.tableId}`), console.log("Status: OK");
220
+ } catch (e) {
221
+ console.error(e.message), process.exit(3);
222
+ }
223
+ }), t.command("init").description("Interactive setup").action(async () => {
224
+ let e = await x("Feishu App ID: "), t = await x("Feishu App Secret: "), n = await x("Bitable URL: "), r, i;
225
+ try {
226
+ ({appToken: r, tableId: i} = u(n));
227
+ } catch (e) {
228
+ console.error(`URL parsing failed: ${e.message}`), process.exit(2);
229
+ }
230
+ g({
231
+ profiles: { default: {
232
+ auth: {
233
+ appId: "${FZ_TASK_APP_ID}",
234
+ appSecret: "${FZ_TASK_APP_SECRET}"
235
+ },
236
+ bitable: {
237
+ url: n,
238
+ appToken: r,
239
+ tableId: i
240
+ }
241
+ } },
242
+ defaultProfile: "default"
243
+ }), _(e, t), console.log("Config written to ~/.config/fz-task/config.yaml"), console.log("Credentials written to ~/.config/fz-task/.env"), console.log(`App Token: ${r}`), console.log(`Table ID: ${i}`), console.log("Verifying connection...");
244
+ try {
245
+ await new b({
246
+ appId: e,
247
+ appSecret: t,
248
+ appToken: r,
249
+ tableId: i
250
+ }).listRecords({ type: "Feature" }), console.log("Connection verified successfully!");
251
+ } catch (e) {
252
+ console.error(`Connection failed: ${e.message}`), process.exit(3);
253
+ }
254
+ });
255
+ }
256
+ //#endregion
257
+ //#region src/types.ts
258
+ var C = [
259
+ "TODO",
260
+ "TEST_SPEC",
261
+ "DOING",
262
+ "TESTING",
263
+ "VERIFIED",
264
+ "DONE"
265
+ ], w = new Map(C.map((e, t) => [e, t]));
266
+ function T(e, t) {
267
+ if (e === "DONE") return !1;
268
+ if (t === "BLOCKED" || e === "BLOCKED") return !0;
269
+ let n = w.get(e), r = w.get(t);
270
+ return n === void 0 || r === void 0 ? !1 : r === n + 1 || r === n - 1;
271
+ }
272
+ function E(e) {
273
+ if (e === "DONE") return [];
274
+ if (e === "BLOCKED") return [...C];
275
+ let t = w.get(e);
276
+ if (t === void 0) return [];
277
+ let n = [];
278
+ return t > 0 && n.push(C[t - 1]), t < C.length - 1 && n.push(C[t + 1]), n.push("BLOCKED"), n;
279
+ }
280
+ //#endregion
281
+ //#region src/formatter.ts
282
+ function D(e, t, n) {
283
+ if (n === "json") return JSON.stringify({
284
+ ...e,
285
+ children: t
286
+ }, null, 2);
287
+ let r = [];
288
+ if (r.push(`ID: ${e.record_id}`), r.push(`Title: ${e.fields.title}`), r.push(`Type: ${e.fields.type}`), r.push(`Status: ${e.fields.status}`), e.fields.priority && r.push(`Priority: ${e.fields.priority}`), e.fields.assignee && r.push(`Assignee: ${e.fields.assignee}`), e.fields.doc_url && r.push(`Doc URL: ${e.fields.doc_url}`), e.fields.description && r.push(`Description: ${e.fields.description}`), t.length > 0) {
289
+ let n = e.fields.type === "Feature" ? "Stories" : "ACs";
290
+ r.push(""), r.push(`Children (${t.length} ${n}):`);
291
+ for (let e of t) r.push(` ${e.record_id} [${e.fields.status}] ${e.fields.title}`);
292
+ }
293
+ return r.join("\n");
294
+ }
295
+ function O(e, t) {
296
+ if (t === "json") return JSON.stringify(e, null, 2);
297
+ if (e.length === 0) return "No records found.";
298
+ let n = [];
299
+ n.push("ID Type Status Priority Title"), n.push("-".repeat(80));
300
+ for (let t of e) n.push(`${t.record_id}\t${t.fields.type}\t${t.fields.status}\t${t.fields.priority ?? "-"}\t${t.fields.title}`);
301
+ return n.join("\n");
302
+ }
303
+ //#endregion
304
+ //#region src/commands/feature.ts
305
+ function k(e) {
306
+ let t = p();
307
+ t || (console.error("No config found. Run `fz-task-cli auth init` to set up."), process.exit(3));
308
+ let n = m(t, e.parent?.opts().profile);
309
+ return new b({
310
+ appId: n.auth.appId,
311
+ appSecret: n.auth.appSecret,
312
+ appToken: n.bitable.appToken,
313
+ tableId: n.bitable.tableId
314
+ });
315
+ }
316
+ function A(e) {
317
+ return e.parent?.opts().format ?? "table";
318
+ }
319
+ function j(e) {
320
+ let t = e.command("feature").description("Feature management commands");
321
+ t.command("add [title]").description("Create a new Feature").option("--json <json>", "JSON string with fields").action(async (e, n, r) => {
322
+ let i = k(t), a = A(t), o = {};
323
+ if (n.json) try {
324
+ o = JSON.parse(n.json);
325
+ } catch {
326
+ console.error("Invalid JSON provided."), process.exit(2);
327
+ }
328
+ e && (o.title = e), o.title || (console.error("Title is required. Provide as argument or in --json."), process.exit(2)), o.type = "Feature", o.status ||= "TODO";
329
+ try {
330
+ let e = await i.createRecord(o);
331
+ console.log(D(e, [], a));
332
+ } catch (e) {
333
+ console.error(`Failed to create feature: ${e.message}`), process.exit(1);
334
+ }
335
+ }), t.command("list").description("List all Features").action(async (e, n) => {
336
+ let r = k(t), i = A(t);
337
+ try {
338
+ let e = await r.listRecords({ type: "Feature" });
339
+ console.log(O(e, i));
340
+ } catch (e) {
341
+ console.error(`Failed to list features: ${e.message}`), process.exit(1);
342
+ }
343
+ }), t.command("get <id>").description("Get a Feature by ID").action(async (e, n, r) => {
344
+ let i = k(t), a = A(t), o;
345
+ try {
346
+ o = await i.getRecord(e);
347
+ } catch (e) {
348
+ console.error(`Record not found: ${e.message}`), process.exit(4);
349
+ }
350
+ o.fields.type !== "Feature" && (console.error(`Record ${e} is not a Feature (type: ${o.fields.type}).`), process.exit(2));
351
+ try {
352
+ let t = await i.getChildren(e);
353
+ console.log(D(o, t, a));
354
+ } catch (e) {
355
+ console.error(`Failed to get children: ${e.message}`), process.exit(1);
356
+ }
357
+ }), t.command("update <id>").description("Update a Feature").option("--status <status>", "New status").option("--priority <priority>", "New priority").option("--assignee <assignee>", "New assignee").option("--doc-url <url>", "New doc URL").option("--title <title>", "New title").option("--description <description>", "New description").action(async (e, n, r) => {
358
+ let i = k(t), a = A(t), o;
359
+ try {
360
+ o = await i.getRecord(e);
361
+ } catch (e) {
362
+ console.error(`Record not found: ${e.message}`), process.exit(4);
363
+ }
364
+ o.fields.type !== "Feature" && (console.error(`Record ${e} is not a Feature (type: ${o.fields.type}).`), process.exit(2));
365
+ let s = {};
366
+ if (n.status) {
367
+ let e = n.status, t = o.fields.status;
368
+ if (!T(t, e)) {
369
+ let n = E(t);
370
+ console.error(`Invalid status transition: ${t} -> ${e}. Allowed: ${n.join(", ")}`), process.exit(2);
371
+ }
372
+ s.status = e, e === "BLOCKED" && (s.previous_status = t), t === "BLOCKED" && (s.previous_status = void 0);
373
+ }
374
+ n.priority && (s.priority = n.priority), n.assignee && (s.assignee = n.assignee), n.docUrl && (s.doc_url = n.docUrl), n.title && (s.title = n.title), n.description && (s.description = n.description);
375
+ try {
376
+ let t = await i.updateRecord(e, s), n = await i.getChildren(e);
377
+ console.log(D(t, n, a));
378
+ } catch (e) {
379
+ console.error(`Failed to update feature: ${e.message}`), process.exit(1);
380
+ }
381
+ }), t.command("delete <id>").description("Delete a Feature").action(async (e, n, r) => {
382
+ let i = k(t), a;
383
+ try {
384
+ a = await i.getRecord(e);
385
+ } catch (e) {
386
+ console.error(`Record not found: ${e.message}`), process.exit(4);
387
+ }
388
+ a.fields.type !== "Feature" && (console.error(`Record ${e} is not a Feature (type: ${a.fields.type}).`), process.exit(2));
389
+ let o = await i.getChildren(e);
390
+ o.length > 0 && (console.error(`Cannot delete Feature ${e}: has ${o.length} child stories. Delete children first.`), process.exit(2));
391
+ try {
392
+ await i.deleteRecord(e), console.log(`Feature ${e} deleted.`);
393
+ } catch (e) {
394
+ console.error(`Failed to delete feature: ${e.message}`), process.exit(1);
395
+ }
396
+ });
397
+ }
398
+ //#endregion
399
+ //#region src/commands/story.ts
400
+ function M(e) {
401
+ let t = p();
402
+ t || (console.error("No config found. Run `fz-task-cli auth init` to set up."), process.exit(3));
403
+ let n = m(t, e.parent?.opts().profile);
404
+ return new b({
405
+ appId: n.auth.appId,
406
+ appSecret: n.auth.appSecret,
407
+ appToken: n.bitable.appToken,
408
+ tableId: n.bitable.tableId
409
+ });
410
+ }
411
+ function N(e) {
412
+ return e.parent?.opts().format ?? "table";
413
+ }
414
+ function P(e) {
415
+ let t = e.command("story").description("UserStory management commands");
416
+ t.command("add [title]").description("Create a new UserStory under a Feature").requiredOption("--feature <id>", "Parent Feature record ID").option("--json <json>", "JSON string with fields").action(async (e, n, r) => {
417
+ let i = M(t), a = N(t), o;
418
+ try {
419
+ o = await i.getRecord(n.feature);
420
+ } catch (e) {
421
+ console.error(`Feature not found: ${e.message}`), process.exit(4);
422
+ }
423
+ o.fields.type !== "Feature" && (console.error(`Record ${n.feature} is not a Feature (type: ${o.fields.type}).`), process.exit(2));
424
+ let s = {};
425
+ if (n.json) try {
426
+ s = JSON.parse(n.json);
427
+ } catch {
428
+ console.error("Invalid JSON provided."), process.exit(2);
429
+ }
430
+ e && (s.title = e), s.title || (console.error("Title is required. Provide as argument or in --json."), process.exit(2)), s.type = "UserStory", s.parent = [n.feature], s.status ||= "TODO";
431
+ try {
432
+ let e = await i.createRecord(s);
433
+ console.log(D(e, [], a));
434
+ } catch (e) {
435
+ console.error(`Failed to create story: ${e.message}`), process.exit(1);
436
+ }
437
+ }), t.command("list").description("List UserStories, optionally filtered by Feature").option("--feature <id>", "Parent Feature record ID to filter by").action(async (e, n) => {
438
+ let r = M(t), i = N(t);
439
+ try {
440
+ let t;
441
+ t = e.feature ? (await r.getChildren(e.feature)).filter((e) => e.fields.type === "UserStory") : await r.listRecords({ type: "UserStory" }), console.log(O(t, i));
442
+ } catch (e) {
443
+ console.error(`Failed to list stories: ${e.message}`), process.exit(1);
444
+ }
445
+ }), t.command("get <id>").description("Get a UserStory by ID").action(async (e, n, r) => {
446
+ let i = M(t), a = N(t), o;
447
+ try {
448
+ o = await i.getRecord(e);
449
+ } catch (e) {
450
+ console.error(`Record not found: ${e.message}`), process.exit(4);
451
+ }
452
+ o.fields.type !== "UserStory" && (console.error(`Record ${e} is not a UserStory (type: ${o.fields.type}).`), process.exit(2));
453
+ try {
454
+ let t = await i.getChildren(e);
455
+ console.log(D(o, t, a));
456
+ } catch (e) {
457
+ console.error(`Failed to get children: ${e.message}`), process.exit(1);
458
+ }
459
+ }), t.command("update <id>").description("Update a UserStory").option("--status <status>", "New status").option("--priority <priority>", "New priority").option("--assignee <assignee>", "New assignee").option("--doc-url <url>", "New doc URL").option("--title <title>", "New title").option("--description <description>", "New description").action(async (e, n, r) => {
460
+ let i = M(t), a = N(t), o;
461
+ try {
462
+ o = await i.getRecord(e);
463
+ } catch (e) {
464
+ console.error(`Record not found: ${e.message}`), process.exit(4);
465
+ }
466
+ o.fields.type !== "UserStory" && (console.error(`Record ${e} is not a UserStory (type: ${o.fields.type}).`), process.exit(2));
467
+ let s = {};
468
+ if (n.status) {
469
+ let e = n.status, t = o.fields.status;
470
+ if (!T(t, e)) {
471
+ let n = E(t);
472
+ console.error(`Invalid status transition: ${t} -> ${e}. Allowed: ${n.join(", ")}`), process.exit(2);
473
+ }
474
+ s.status = e, e === "BLOCKED" && (s.previous_status = t), t === "BLOCKED" && (s.previous_status = void 0);
475
+ }
476
+ n.priority && (s.priority = n.priority), n.assignee && (s.assignee = n.assignee), n.docUrl && (s.doc_url = n.docUrl), n.title && (s.title = n.title), n.description && (s.description = n.description);
477
+ try {
478
+ let t = await i.updateRecord(e, s), n = await i.getChildren(e);
479
+ console.log(D(t, n, a));
480
+ } catch (e) {
481
+ console.error(`Failed to update story: ${e.message}`), process.exit(1);
482
+ }
483
+ }), t.command("delete <id>").description("Delete a UserStory").action(async (e, n, r) => {
484
+ let i = M(t), a;
485
+ try {
486
+ a = await i.getRecord(e);
487
+ } catch (e) {
488
+ console.error(`Record not found: ${e.message}`), process.exit(4);
489
+ }
490
+ a.fields.type !== "UserStory" && (console.error(`Record ${e} is not a UserStory (type: ${a.fields.type}).`), process.exit(2));
491
+ let o = await i.getChildren(e);
492
+ o.length > 0 && (console.error(`Cannot delete UserStory ${e}: has ${o.length} child ACs. Delete children first.`), process.exit(2));
493
+ try {
494
+ await i.deleteRecord(e), console.log(`UserStory ${e} deleted.`);
495
+ } catch (e) {
496
+ console.error(`Failed to delete story: ${e.message}`), process.exit(1);
497
+ }
498
+ });
499
+ }
500
+ //#endregion
501
+ //#region src/commands/ac.ts
502
+ function F(e) {
503
+ let t = p();
504
+ t || (console.error("No config found. Run `fz-task-cli auth init` to set up."), process.exit(3));
505
+ let n = m(t, e.parent?.opts().profile);
506
+ return new b({
507
+ appId: n.auth.appId,
508
+ appSecret: n.auth.appSecret,
509
+ appToken: n.bitable.appToken,
510
+ tableId: n.bitable.tableId
511
+ });
512
+ }
513
+ function I(e) {
514
+ return e.parent?.opts().format ?? "table";
515
+ }
516
+ function L(e) {
517
+ let t = e.command("ac").description("AC (Acceptance Criteria) management commands");
518
+ t.command("add [title]").description("Create a new AC under a UserStory").requiredOption("--story <id>", "Parent UserStory record ID").option("--json <json>", "JSON string with fields").action(async (e, n, r) => {
519
+ let i = F(t), a = I(t), o;
520
+ try {
521
+ o = await i.getRecord(n.story);
522
+ } catch (e) {
523
+ console.error(`UserStory not found: ${e.message}`), process.exit(4);
524
+ }
525
+ o.fields.type !== "UserStory" && (console.error(`Record ${n.story} is not a UserStory (type: ${o.fields.type}).`), process.exit(2));
526
+ let s = {};
527
+ if (n.json) try {
528
+ s = JSON.parse(n.json);
529
+ } catch {
530
+ console.error("Invalid JSON provided."), process.exit(2);
531
+ }
532
+ e && (s.title = e), s.title || (console.error("Title is required. Provide as argument or in --json."), process.exit(2)), s.type = "AC", s.parent = [n.story], s.status ||= "TODO";
533
+ try {
534
+ let e = await i.createRecord(s);
535
+ console.log(D(e, [], a));
536
+ } catch (e) {
537
+ console.error(`Failed to create AC: ${e.message}`), process.exit(1);
538
+ }
539
+ }), t.command("list").description("List ACs, optionally filtered by UserStory").option("--story <id>", "Parent UserStory record ID to filter by").action(async (e, n) => {
540
+ let r = F(t), i = I(t);
541
+ try {
542
+ let t;
543
+ t = e.story ? (await r.getChildren(e.story)).filter((e) => e.fields.type === "AC") : await r.listRecords({ type: "AC" }), console.log(O(t, i));
544
+ } catch (e) {
545
+ console.error(`Failed to list ACs: ${e.message}`), process.exit(1);
546
+ }
547
+ }), t.command("get <id>").description("Get an AC by ID").action(async (e, n, r) => {
548
+ let i = F(t), a = I(t), o;
549
+ try {
550
+ o = await i.getRecord(e);
551
+ } catch (e) {
552
+ console.error(`Record not found: ${e.message}`), process.exit(4);
553
+ }
554
+ o.fields.type !== "AC" && (console.error(`Record ${e} is not an AC (type: ${o.fields.type}).`), process.exit(2)), console.log(D(o, [], a));
555
+ }), t.command("update <id>").description("Update an AC").option("--status <status>", "New status").option("--priority <priority>", "New priority").option("--assignee <assignee>", "New assignee").option("--doc-url <url>", "New doc URL").option("--title <title>", "New title").option("--description <description>", "New description").action(async (e, n, r) => {
556
+ let i = F(t), a = I(t), o;
557
+ try {
558
+ o = await i.getRecord(e);
559
+ } catch (e) {
560
+ console.error(`Record not found: ${e.message}`), process.exit(4);
561
+ }
562
+ o.fields.type !== "AC" && (console.error(`Record ${e} is not an AC (type: ${o.fields.type}).`), process.exit(2));
563
+ let s = {};
564
+ if (n.status) {
565
+ let e = n.status, t = o.fields.status;
566
+ if (!T(t, e)) {
567
+ let n = E(t);
568
+ console.error(`Invalid status transition: ${t} -> ${e}. Allowed: ${n.join(", ")}`), process.exit(2);
569
+ }
570
+ s.status = e, e === "BLOCKED" && (s.previous_status = t), t === "BLOCKED" && (s.previous_status = void 0);
571
+ }
572
+ n.priority && (s.priority = n.priority), n.assignee && (s.assignee = n.assignee), n.docUrl && (s.doc_url = n.docUrl), n.title && (s.title = n.title), n.description && (s.description = n.description);
573
+ try {
574
+ let t = await i.updateRecord(e, s);
575
+ console.log(D(t, [], a));
576
+ } catch (e) {
577
+ console.error(`Failed to update AC: ${e.message}`), process.exit(1);
578
+ }
579
+ }), t.command("delete <id>").description("Delete an AC").action(async (e, n, r) => {
580
+ let i = F(t), a;
581
+ try {
582
+ a = await i.getRecord(e);
583
+ } catch (e) {
584
+ console.error(`Record not found: ${e.message}`), process.exit(4);
585
+ }
586
+ a.fields.type !== "AC" && (console.error(`Record ${e} is not an AC (type: ${a.fields.type}).`), process.exit(2));
587
+ try {
588
+ await i.deleteRecord(e), console.log(`AC ${e} deleted.`);
589
+ } catch (e) {
590
+ console.error(`Failed to delete AC: ${e.message}`), process.exit(1);
591
+ }
592
+ });
593
+ }
594
+ //#endregion
595
+ //#region src/cli.ts
596
+ var R = new e();
597
+ R.name("fz-task-cli").description("Feishu Bitable task management CLI").version("0.1.0").option("--profile <name>", "config profile name").option("--format <format>", "output format (json|table)", "table"), S(R), j(R), P(R), L(R), R.parse();
598
+ //#endregion
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@fanzie/task-cli",
3
+ "version": "0.1.1",
4
+ "description": "CLI tool for managing Feature/UserStory/AC task hierarchies in Feishu Bitable",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "fz-task-cli": "./dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "feishu",
17
+ "bitable",
18
+ "task",
19
+ "cli",
20
+ "project-management"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/dannnney/fz-task-cli.git"
27
+ },
28
+ "engines": {
29
+ "node": ">=20"
30
+ },
31
+ "dependencies": {
32
+ "@larksuiteoapi/node-sdk": "^1.59.0",
33
+ "commander": "^14.0.3",
34
+ "js-yaml": "^4.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/js-yaml": "^4.0.9",
38
+ "@types/node": "^25.5.0",
39
+ "typescript": "^5.9.3",
40
+ "vite": "^8.0.1"
41
+ },
42
+ "scripts": {
43
+ "dev": "node --experimental-strip-types src/cli.ts",
44
+ "build": "vite build",
45
+ "test": "bun test"
46
+ }
47
+ }