@bretwardjames/ghp-cli 0.1.1 → 0.1.3

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 (49) hide show
  1. package/dist/branch-linker.d.ts +31 -9
  2. package/dist/branch-linker.d.ts.map +1 -1
  3. package/dist/branch-linker.js +61 -35
  4. package/dist/branch-linker.js.map +1 -1
  5. package/dist/commands/link-branch.d.ts.map +1 -1
  6. package/dist/commands/link-branch.js +20 -0
  7. package/dist/commands/link-branch.js.map +1 -1
  8. package/dist/commands/plan.d.ts.map +1 -1
  9. package/dist/commands/plan.js +235 -95
  10. package/dist/commands/plan.js.map +1 -1
  11. package/dist/commands/set-field.d.ts.map +1 -1
  12. package/dist/commands/set-field.js +25 -1
  13. package/dist/commands/set-field.js.map +1 -1
  14. package/dist/commands/start.d.ts.map +1 -1
  15. package/dist/commands/start.js +21 -0
  16. package/dist/commands/start.js.map +1 -1
  17. package/dist/commands/switch.d.ts.map +1 -1
  18. package/dist/commands/switch.js +20 -0
  19. package/dist/commands/switch.js.map +1 -1
  20. package/dist/commands/sync.d.ts +2 -0
  21. package/dist/commands/sync.d.ts.map +1 -0
  22. package/dist/commands/sync.js +77 -0
  23. package/dist/commands/sync.js.map +1 -0
  24. package/dist/commands/work.d.ts +5 -0
  25. package/dist/commands/work.d.ts.map +1 -1
  26. package/dist/commands/work.js +226 -36
  27. package/dist/commands/work.js.map +1 -1
  28. package/dist/config.d.ts +1 -0
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js.map +1 -1
  31. package/dist/git-utils.d.ts +6 -52
  32. package/dist/git-utils.d.ts.map +1 -1
  33. package/dist/git-utils.js +7 -137
  34. package/dist/git-utils.js.map +1 -1
  35. package/dist/github-api.d.ts +17 -108
  36. package/dist/github-api.d.ts.map +1 -1
  37. package/dist/github-api.js +48 -579
  38. package/dist/github-api.js.map +1 -1
  39. package/dist/index.js +16 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/table.d.ts +16 -0
  42. package/dist/table.d.ts.map +1 -0
  43. package/dist/table.js +230 -0
  44. package/dist/table.js.map +1 -0
  45. package/dist/types.d.ts +6 -36
  46. package/dist/types.d.ts.map +1 -1
  47. package/dist/types.js +5 -0
  48. package/dist/types.js.map +1 -1
  49. package/package.json +2 -2
@@ -1,15 +1,37 @@
1
- interface BranchLink {
2
- branch: string;
3
- issueNumber: number;
4
- issueTitle: string;
5
- itemId: string;
6
- repo: string;
7
- linkedAt: string;
8
- }
1
+ /**
2
+ * CLI-specific branch linker with file-based storage.
3
+ *
4
+ * This module wraps the core BranchLinker with a file-based StorageAdapter
5
+ * that stores links in ~/.config/ghp-cli/branch-links.json
6
+ */
7
+ import { BranchLinker, type StorageAdapter, type BranchLink } from '@bretwardjames/ghp-core';
8
+ /**
9
+ * File-based storage adapter for CLI usage
10
+ */
11
+ declare const fileStorageAdapter: StorageAdapter;
12
+ declare const linker: BranchLinker;
13
+ /**
14
+ * Create a link between a branch and an issue.
15
+ * Backwards-compatible function signature for existing CLI code.
16
+ */
9
17
  export declare function linkBranch(branch: string, issueNumber: number, issueTitle: string, itemId: string, repo: string): void;
18
+ /**
19
+ * Remove the link for an issue.
20
+ * @returns true if a link was removed, false if no link existed
21
+ */
10
22
  export declare function unlinkBranch(repo: string, issueNumber: number): boolean;
23
+ /**
24
+ * Get the branch linked to an issue.
25
+ */
11
26
  export declare function getBranchForIssue(repo: string, issueNumber: number): string | null;
27
+ /**
28
+ * Get the full link info for a branch.
29
+ */
12
30
  export declare function getIssueForBranch(repo: string, branch: string): BranchLink | null;
31
+ /**
32
+ * Get all links for a repository.
33
+ */
13
34
  export declare function getAllLinksForRepo(repo: string): BranchLink[];
14
- export {};
35
+ export type { BranchLink } from '@bretwardjames/ghp-core';
36
+ export { linker, fileStorageAdapter };
15
37
  //# sourceMappingURL=branch-linker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"branch-linker.d.ts","sourceRoot":"","sources":["../src/branch-linker.ts"],"names":[],"mappings":"AAIA,UAAU,UAAU;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACpB;AAwBD,wBAAgB,UAAU,CACtB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACb,IAAI,CAkBN;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAUvE;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIlF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAGjF;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAG7D"}
1
+ {"version":3,"file":"branch-linker.d.ts","sourceRoot":"","sources":["../src/branch-linker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EACH,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,UAAU,EAClB,MAAM,yBAAyB,CAAC;AAKjC;;GAEG;AACH,QAAA,MAAM,kBAAkB,EAAE,cAmBzB,CAAC;AAGF,QAAA,MAAM,MAAM,cAAuC,CAAC;AAEpD;;;GAGG;AACH,wBAAgB,UAAU,CACtB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACb,IAAI,CAIN;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAcvE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIlF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAGjF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,EAAE,CAG7D;AAGD,YAAY,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAG1D,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1,60 +1,86 @@
1
+ /**
2
+ * CLI-specific branch linker with file-based storage.
3
+ *
4
+ * This module wraps the core BranchLinker with a file-based StorageAdapter
5
+ * that stores links in ~/.config/ghp-cli/branch-links.json
6
+ */
1
7
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
2
8
  import { homedir } from 'os';
3
9
  import { join } from 'path';
10
+ import { BranchLinker, } from '@bretwardjames/ghp-core';
4
11
  const DATA_DIR = join(homedir(), '.config', 'ghp-cli');
5
12
  const LINKS_FILE = join(DATA_DIR, 'branch-links.json');
6
- function loadLinks() {
7
- try {
8
- if (existsSync(LINKS_FILE)) {
9
- const data = readFileSync(LINKS_FILE, 'utf-8');
10
- return JSON.parse(data);
13
+ /**
14
+ * File-based storage adapter for CLI usage
15
+ */
16
+ const fileStorageAdapter = {
17
+ load() {
18
+ try {
19
+ if (existsSync(LINKS_FILE)) {
20
+ const data = readFileSync(LINKS_FILE, 'utf-8');
21
+ return JSON.parse(data);
22
+ }
11
23
  }
12
- }
13
- catch {
14
- // Ignore errors
15
- }
16
- return [];
17
- }
18
- function saveLinks(links) {
19
- if (!existsSync(DATA_DIR)) {
20
- mkdirSync(DATA_DIR, { recursive: true });
21
- }
22
- writeFileSync(LINKS_FILE, JSON.stringify(links, null, 2));
23
- }
24
+ catch {
25
+ // Ignore errors, return empty array
26
+ }
27
+ return [];
28
+ },
29
+ save(links) {
30
+ if (!existsSync(DATA_DIR)) {
31
+ mkdirSync(DATA_DIR, { recursive: true });
32
+ }
33
+ writeFileSync(LINKS_FILE, JSON.stringify(links, null, 2));
34
+ },
35
+ };
36
+ // Create singleton linker instance
37
+ const linker = new BranchLinker(fileStorageAdapter);
38
+ /**
39
+ * Create a link between a branch and an issue.
40
+ * Backwards-compatible function signature for existing CLI code.
41
+ */
24
42
  export function linkBranch(branch, issueNumber, issueTitle, itemId, repo) {
25
- const links = loadLinks();
26
- // Remove existing link for this branch or issue in this repo
27
- const filtered = links.filter(l => !(l.repo === repo && (l.branch === branch || l.issueNumber === issueNumber)));
28
- filtered.push({
29
- branch,
30
- issueNumber,
31
- issueTitle,
32
- itemId,
33
- repo,
34
- linkedAt: new Date().toISOString(),
35
- });
36
- saveLinks(filtered);
43
+ // Use sync-like behavior for backwards compatibility
44
+ // The file adapter is synchronous so this works
45
+ linker.link(branch, issueNumber, issueTitle, itemId, repo);
37
46
  }
47
+ /**
48
+ * Remove the link for an issue.
49
+ * @returns true if a link was removed, false if no link existed
50
+ */
38
51
  export function unlinkBranch(repo, issueNumber) {
39
- const links = loadLinks();
52
+ // Load, filter, and save synchronously for backwards compatibility
53
+ const adapter = fileStorageAdapter;
54
+ const links = adapter.load();
40
55
  const filtered = links.filter(l => !(l.repo === repo && l.issueNumber === issueNumber));
41
56
  if (filtered.length === links.length) {
42
- return false; // Nothing was removed
57
+ return false;
43
58
  }
44
- saveLinks(filtered);
59
+ adapter.save(filtered);
45
60
  return true;
46
61
  }
62
+ /**
63
+ * Get the branch linked to an issue.
64
+ */
47
65
  export function getBranchForIssue(repo, issueNumber) {
48
- const links = loadLinks();
66
+ const links = fileStorageAdapter.load();
49
67
  const link = links.find(l => l.repo === repo && l.issueNumber === issueNumber);
50
68
  return link?.branch || null;
51
69
  }
70
+ /**
71
+ * Get the full link info for a branch.
72
+ */
52
73
  export function getIssueForBranch(repo, branch) {
53
- const links = loadLinks();
74
+ const links = fileStorageAdapter.load();
54
75
  return links.find(l => l.repo === repo && l.branch === branch) || null;
55
76
  }
77
+ /**
78
+ * Get all links for a repository.
79
+ */
56
80
  export function getAllLinksForRepo(repo) {
57
- const links = loadLinks();
81
+ const links = fileStorageAdapter.load();
58
82
  return links.filter(l => l.repo === repo);
59
83
  }
84
+ // Also export the linker instance and adapter for advanced usage
85
+ export { linker, fileStorageAdapter };
60
86
  //# sourceMappingURL=branch-linker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"branch-linker.js","sourceRoot":"","sources":["../src/branch-linker.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAW5B,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AACvD,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC;AAEvD,SAAS,SAAS;IACd,IAAI,CAAC;QACD,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC5B,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACL,gBAAgB;IACpB,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC;AAED,SAAS,SAAS,CAAC,KAAmB;IAClC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACxB,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,UAAU,CACtB,MAAc,EACd,WAAmB,EACnB,UAAkB,EAClB,MAAc,EACd,IAAY;IAEZ,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAE1B,6DAA6D;IAC7D,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAC9B,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,CAAC,WAAW,KAAK,WAAW,CAAC,CAAC,CAC/E,CAAC;IAEF,QAAQ,CAAC,IAAI,CAAC;QACV,MAAM;QACN,WAAW;QACX,UAAU;QACV,MAAM;QACN,IAAI;QACJ,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;KACrC,CAAC,CAAC;IAEH,SAAS,CAAC,QAAQ,CAAC,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,WAAmB;IAC1D,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,WAAW,KAAK,WAAW,CAAC,CAAC,CAAC;IAExF,IAAI,QAAQ,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QACnC,OAAO,KAAK,CAAC,CAAC,sBAAsB;IACxC,CAAC;IAED,SAAS,CAAC,QAAQ,CAAC,CAAC;IACpB,OAAO,IAAI,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,WAAmB;IAC/D,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,WAAW,KAAK,WAAW,CAAC,CAAC;IAC/E,OAAO,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC;AAChC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,MAAc;IAC1D,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC3C,MAAM,KAAK,GAAG,SAAS,EAAE,CAAC;IAC1B,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC9C,CAAC"}
1
+ {"version":3,"file":"branch-linker.js","sourceRoot":"","sources":["../src/branch-linker.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EACH,YAAY,GAGf,MAAM,yBAAyB,CAAC;AAEjC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AACvD,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,mBAAmB,CAAC,CAAC;AAEvD;;GAEG;AACH,MAAM,kBAAkB,GAAmB;IACvC,IAAI;QACA,IAAI,CAAC;YACD,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;gBACzB,MAAM,IAAI,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;gBAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACL,oCAAoC;QACxC,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IAED,IAAI,CAAC,KAAmB;QACpB,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxB,SAAS,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7C,CAAC;QACD,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC9D,CAAC;CACJ,CAAC;AAEF,mCAAmC;AACnC,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,kBAAkB,CAAC,CAAC;AAEpD;;;GAGG;AACH,MAAM,UAAU,UAAU,CACtB,MAAc,EACd,WAAmB,EACnB,UAAkB,EAClB,MAAc,EACd,IAAY;IAEZ,qDAAqD;IACrD,gDAAgD;IAChD,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;AAC/D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,WAAmB;IAC1D,mEAAmE;IACnE,MAAM,OAAO,GAAG,kBAAkB,CAAC;IACnC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAkB,CAAC;IAC7C,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAC9B,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,WAAW,KAAK,WAAW,CAAC,CACtD,CAAC;IAEF,IAAI,QAAQ,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;QACnC,OAAO,KAAK,CAAC;IACjB,CAAC;IAED,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvB,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,WAAmB;IAC/D,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,EAAkB,CAAC;IACxD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,WAAW,KAAK,WAAW,CAAC,CAAC;IAC/E,OAAO,IAAI,EAAE,MAAM,IAAI,IAAI,CAAC;AAChC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,MAAc;IAC1D,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,EAAkB,CAAC;IACxD,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC;AAC3E,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC3C,MAAM,KAAK,GAAG,kBAAkB,CAAC,IAAI,EAAkB,CAAC;IACxD,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AAC9C,CAAC;AAKD,iEAAiE;AACjE,OAAO,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"link-branch.d.ts","sourceRoot":"","sources":["../../src/commands/link-branch.ts"],"names":[],"mappings":"AAKA,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA4CrF"}
1
+ {"version":3,"file":"link-branch.d.ts","sourceRoot":"","sources":["../../src/commands/link-branch.ts"],"names":[],"mappings":"AAKA,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAoErF"}
@@ -40,5 +40,25 @@ export async function linkBranchCommand(issue, branch) {
40
40
  // Create link
41
41
  linkBranch(branchName, issueNumber, item.title, item.id, repo.fullName);
42
42
  console.log(chalk.green('✓'), `Linked "${branchName}" to #${issueNumber}: ${item.title}`);
43
+ // If this branch is currently checked out, apply active label
44
+ const currentBranch = await getCurrentBranch();
45
+ if (currentBranch === branchName) {
46
+ const activeLabel = api.getActiveLabelName();
47
+ // Ensure the label exists
48
+ await api.ensureLabel(repo, activeLabel);
49
+ // Remove label from any other issues that have it
50
+ const issuesWithLabel = await api.findIssuesWithLabel(repo, activeLabel);
51
+ for (const otherIssue of issuesWithLabel) {
52
+ if (otherIssue !== issueNumber) {
53
+ await api.removeLabelFromIssue(repo, otherIssue, activeLabel);
54
+ console.log(chalk.dim(`Removed ${activeLabel} from #${otherIssue}`));
55
+ }
56
+ }
57
+ // Add label to current issue
58
+ const labelAdded = await api.addLabelToIssue(repo, issueNumber, activeLabel);
59
+ if (labelAdded) {
60
+ console.log(chalk.green('✓'), `Applied "${activeLabel}" label (branch is checked out)`);
61
+ }
62
+ }
43
63
  }
44
64
  //# sourceMappingURL=link-branch.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"link-branch.js","sourceRoot":"","sources":["../../src/commands/link-branch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACrE,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAEpE,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAa,EAAE,MAAe;IAClE,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACxC,IAAI,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,wBAAwB,CAAC,CAAC;QAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,oBAAoB;IACpB,MAAM,IAAI,GAAG,MAAM,gBAAgB,EAAE,CAAC;IACtC,IAAI,CAAC,IAAI,EAAE,CAAC;QACR,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,8CAA8C,CAAC,CAAC;QACnF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,8CAA8C;IAC9C,MAAM,UAAU,GAAG,MAAM,IAAI,MAAM,gBAAgB,EAAE,CAAC;IACtD,IAAI,CAAC,UAAU,EAAE,CAAC;QACd,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,oDAAoD,CAAC,CAAC;QACzF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,eAAe;IACf,MAAM,aAAa,GAAG,MAAM,GAAG,CAAC,YAAY,EAAE,CAAC;IAC/C,IAAI,CAAC,aAAa,EAAE,CAAC;QACjB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,wBAAwB,EAAE,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC;QACrF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,iBAAiB;IACjB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,gBAAgB,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IAC3D,IAAI,CAAC,IAAI,EAAE,CAAC;QACR,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,UAAU,WAAW,2BAA2B,CAAC,CAAC;QACrF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IAED,0BAA0B;IAC1B,MAAM,cAAc,GAAG,iBAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IACrE,IAAI,cAAc,EAAE,CAAC;QACjB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,UAAU,WAAW,mBAAmB,cAAc,GAAG,CAAC,CAAC;IAClG,CAAC;IAED,cAAc;IACd,UAAU,CAAC,UAAU,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;IACxE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,WAAW,UAAU,SAAS,WAAW,KAAK,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC;AAC9F,CAAC"}
1
+ {"version":3,"file":"link-branch.js","sourceRoot":"","sources":["../../src/commands/link-branch.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/commands/plan.ts"],"names":[],"mappings":"AAgBA,wBAAsB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CA+MjF"}
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/commands/plan.ts"],"names":[],"mappings":"AAyGA,wBAAsB,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CA2QjF"}
@@ -1,7 +1,71 @@
1
1
  import chalk from 'chalk';
2
2
  import { api } from '../github-api.js';
3
3
  import { detectRepository } from '../git-utils.js';
4
- import { getShortcut, getPlanDefaults, listShortcuts } from '../config.js';
4
+ import { getShortcut, getPlanDefaults, listShortcuts, getConfig } from '../config.js';
5
+ import { displayTable, parseColumns, DEFAULT_COLUMNS, calculateColumnWidths, displayTableWithWidths } from '../table.js';
6
+ /**
7
+ * Parse and apply a GitHub Project view filter expression
8
+ * Format: field:value1,value2 field2:value3 -field3:excluded
9
+ * Special: @me expands to current username
10
+ */
11
+ function applyViewFilter(items, filterExpr, username) {
12
+ if (!filterExpr.trim())
13
+ return items;
14
+ // Parse filter tokens: field:values or -field:values (negation)
15
+ const tokens = filterExpr.match(/(-?\w+):"([^"]+)"|(-?\w+):(\S+)/g) || [];
16
+ let result = items;
17
+ for (const token of tokens) {
18
+ const negated = token.startsWith('-');
19
+ const cleanToken = negated ? token.slice(1) : token;
20
+ // Parse field:values
21
+ const colonIdx = cleanToken.indexOf(':');
22
+ if (colonIdx === -1)
23
+ continue;
24
+ const field = cleanToken.slice(0, colonIdx).toLowerCase();
25
+ let valuesStr = cleanToken.slice(colonIdx + 1);
26
+ // Remove quotes if present
27
+ if (valuesStr.startsWith('"') && valuesStr.endsWith('"')) {
28
+ valuesStr = valuesStr.slice(1, -1);
29
+ }
30
+ // Split values by comma and expand @me
31
+ const values = valuesStr.split(',').map(v => {
32
+ const trimmed = v.trim();
33
+ return trimmed === '@me' ? username : trimmed;
34
+ });
35
+ result = result.filter(item => {
36
+ let matches = false;
37
+ switch (field) {
38
+ case 'status':
39
+ matches = values.some(v => item.status?.toLowerCase() === v.toLowerCase());
40
+ break;
41
+ case 'assignee':
42
+ case 'assignees':
43
+ matches = values.some(v => item.assignees.some(a => a.toLowerCase() === v.toLowerCase()));
44
+ break;
45
+ case 'label':
46
+ case 'labels':
47
+ matches = values.some(v => item.labels.some(l => l.name.toLowerCase().includes(v.toLowerCase())));
48
+ break;
49
+ case 'type':
50
+ matches = values.some(v => item.issueType?.toLowerCase() === v.toLowerCase());
51
+ break;
52
+ case 'repo':
53
+ case 'repository':
54
+ matches = values.some(v => item.repository?.toLowerCase().includes(v.toLowerCase()));
55
+ break;
56
+ default:
57
+ // Check custom fields
58
+ const fieldValue = item.fields[field] ||
59
+ Object.entries(item.fields).find(([k]) => k.toLowerCase() === field)?.[1];
60
+ if (fieldValue) {
61
+ matches = values.some(v => fieldValue.toLowerCase().includes(v.toLowerCase()));
62
+ }
63
+ }
64
+ return negated ? !matches : matches;
65
+ });
66
+ }
67
+ return result;
68
+ }
5
69
  export async function planCommand(shortcut, command) {
6
70
  // Commander passes (shortcut, options) for optional positional args
7
71
  // The options object is passed directly, not as command.opts()
@@ -93,6 +157,36 @@ export async function planCommand(shortcut, command) {
93
157
  }
94
158
  // Apply filters
95
159
  let filteredItems = allItems;
160
+ // --view: filter by project view's filter expression
161
+ if (options.view) {
162
+ // Find the view in target projects
163
+ let viewFound = false;
164
+ for (const project of targetProjects) {
165
+ const views = await api.getProjectViews(project.id);
166
+ const view = views.find(v => v.name.toLowerCase() === options.view.toLowerCase());
167
+ if (view) {
168
+ viewFound = true;
169
+ if (view.filter) {
170
+ // Parse and apply the view's filter
171
+ filteredItems = applyViewFilter(filteredItems, view.filter, api.username || '');
172
+ }
173
+ // Only use items from this project for the view
174
+ filteredItems = filteredItems.filter(item => item.projectId === project.id);
175
+ break;
176
+ }
177
+ }
178
+ if (!viewFound) {
179
+ console.error(chalk.red('View not found:'), options.view);
180
+ console.log(chalk.dim('Available views:'));
181
+ for (const project of targetProjects) {
182
+ const views = await api.getProjectViews(project.id);
183
+ for (const v of views) {
184
+ console.log(` ${chalk.cyan(v.name)}`);
185
+ }
186
+ }
187
+ process.exit(1);
188
+ }
189
+ }
96
190
  // --mine: filter to current user
97
191
  if (options.mine) {
98
192
  filteredItems = filteredItems.filter(item => item.assignees.includes(api.username || ''));
@@ -133,6 +227,10 @@ export async function planCommand(shortcut, command) {
133
227
  if (fieldLower === 'project') {
134
228
  return item.projectTitle.toLowerCase().includes(valueLower);
135
229
  }
230
+ if (fieldLower === 'state') {
231
+ // state can be: open, closed, merged
232
+ return item.state?.toLowerCase() === valueLower;
233
+ }
136
234
  // Check custom project fields
137
235
  const fieldValue = item.fields[field] || item.fields[fieldLower];
138
236
  if (fieldValue) {
@@ -178,14 +276,39 @@ export async function planCommand(shortcut, command) {
178
276
  return 0;
179
277
  });
180
278
  }
279
+ // Determine if we need status column (when items have mixed statuses)
280
+ const needsStatusColumn = options.list || options.all || options.view || (options.group && options.group.toLowerCase() !== 'status');
281
+ const defaultColumnsWithStatus = ['number', 'type', 'title', 'status', 'assignees', 'priority', 'size', 'labels'];
181
282
  // Display based on mode
182
- if (options.list) {
183
- // Simple list view (one item per line, for pickers)
184
- displaySimpleList(filteredItems);
283
+ if (options.group) {
284
+ // Grouped view - group by specified field
285
+ displayGroupedView(filteredItems, options.group, options);
286
+ }
287
+ else if (options.list || options.all || options.view) {
288
+ // Table view for all items or view items (includes status column)
289
+ const label = options.view
290
+ ? `View: ${options.view}`
291
+ : options.mine
292
+ ? 'My Items'
293
+ : options.unassigned
294
+ ? 'Unassigned Items'
295
+ : 'All Items';
296
+ console.log(chalk.bold(label), chalk.dim(`(${filteredItems.length} items)`));
297
+ console.log();
298
+ const columnsConfig = getConfig('columns');
299
+ const columns = columnsConfig ? parseColumns(columnsConfig) : defaultColumnsWithStatus;
300
+ displayTable(filteredItems, columns);
301
+ console.log();
185
302
  }
186
303
  else if (options.status) {
187
- // List view for single status
188
- displayListView(filteredItems, options.status, options);
304
+ // Table view for single status
305
+ const label = options.mine ? `My ${options.status}` : options.unassigned ? `Unassigned ${options.status}` : options.status;
306
+ console.log(chalk.bold(label), chalk.dim(`(${filteredItems.length} items)`));
307
+ console.log();
308
+ const columnsConfig = getConfig('columns');
309
+ const columns = columnsConfig ? parseColumns(columnsConfig) : DEFAULT_COLUMNS;
310
+ displayTable(filteredItems, columns);
311
+ console.log();
189
312
  }
190
313
  else {
191
314
  // Board view
@@ -201,7 +324,8 @@ function getFieldValue(item, fieldName) {
201
324
  case 'title':
202
325
  return item.title;
203
326
  case 'status':
204
- return item.status;
327
+ // Return statusIndex for sorting by project's defined order
328
+ return item.statusIndex;
205
329
  case 'type':
206
330
  return item.type;
207
331
  case 'issuetype':
@@ -238,103 +362,119 @@ function displaySimpleList(items) {
238
362
  console.log(`${num} ${item.title} ${status}`);
239
363
  }
240
364
  }
241
- function displayListView(items, status, opts) {
242
- const label = opts.mine ? `My ${status}` : opts.unassigned ? `Unassigned ${status}` : status;
243
- console.log(chalk.bold(label), chalk.dim(`(${items.length} items)`));
244
- console.log();
365
+ /**
366
+ * Get display value for a field (for grouping headers, not sorting)
367
+ */
368
+ function getFieldDisplayValue(item, fieldName) {
369
+ const lower = fieldName.toLowerCase();
370
+ switch (lower) {
371
+ case 'status':
372
+ return item.status || 'No Status';
373
+ case 'type':
374
+ case 'issuetype':
375
+ case 'issue-type':
376
+ return item.issueType || 'No Type';
377
+ case 'assignee':
378
+ case 'assignees':
379
+ return item.assignees.length > 0 ? item.assignees.join(', ') : 'Unassigned';
380
+ case 'priority':
381
+ return item.fields['Priority'] || item.fields['priority'] || 'No Priority';
382
+ case 'size':
383
+ return item.fields['Size'] || item.fields['size'] || item.fields['Estimate'] || 'No Size';
384
+ case 'label':
385
+ case 'labels':
386
+ return item.labels.length > 0 ? item.labels.map(l => l.name).join(', ') : 'No Labels';
387
+ case 'project':
388
+ return item.projectTitle;
389
+ case 'repo':
390
+ case 'repository':
391
+ return item.repository || 'Unknown';
392
+ default:
393
+ // Check custom fields
394
+ const fieldValue = item.fields[fieldName] ||
395
+ Object.entries(item.fields).find(([k]) => k.toLowerCase() === lower)?.[1];
396
+ return fieldValue || `No ${fieldName}`;
397
+ }
398
+ }
399
+ /**
400
+ * Display items grouped by a field
401
+ */
402
+ function displayGroupedView(items, groupField, opts) {
245
403
  if (items.length === 0) {
246
404
  console.log(chalk.dim('No items found.'));
247
405
  return;
248
406
  }
249
- // Build rows with raw data for width calculation
250
- const rows = items.map(item => ({
251
- num: item.number ? `#${item.number}` : 'draft',
252
- type: item.issueType || '',
253
- title: item.title,
254
- assignees: item.assignees.map(a => '@' + a).join(' '),
255
- priority: item.fields['Priority'] || item.fields['priority'] || '',
256
- size: item.fields['Size'] || item.fields['size'] || item.fields['Estimate'] || item.fields['estimate'] || '',
257
- labels: item.labels,
258
- }));
259
- // Calculate column widths
260
- const numWidth = Math.max(...rows.map(r => r.num.length), 5);
261
- const typeWidth = Math.max(...rows.map(r => r.type.length), 0);
262
- const assigneeWidth = Math.max(...rows.map(r => r.assignees.length), 0);
263
- const priorityWidth = Math.max(...rows.map(r => r.priority.length), 0);
264
- const sizeWidth = Math.max(...rows.map(r => r.size.length), 0);
265
- // Calculate title width (remaining space, min 20, max 60)
266
- const termWidth = process.stdout.columns || 120;
267
- const fixedWidth = numWidth + typeWidth + assigneeWidth + priorityWidth + sizeWidth + 12; // spacing
268
- const titleWidth = Math.max(20, Math.min(60, termWidth - fixedWidth - 4));
269
- // Print header row
270
- const headerParts = [];
271
- headerParts.push(chalk.dim('#'.padEnd(numWidth)));
272
- if (typeWidth > 0) {
273
- headerParts.push(chalk.dim('Type'.padEnd(typeWidth)));
274
- }
275
- headerParts.push(chalk.dim('Title'.padEnd(titleWidth)));
276
- if (assigneeWidth > 0) {
277
- headerParts.push(chalk.dim('Assignee'.padEnd(assigneeWidth)));
407
+ // Group items by the field value
408
+ const groups = new Map();
409
+ const groupOrder = []; // Track order of first appearance
410
+ for (const item of items) {
411
+ const groupValue = getFieldDisplayValue(item, groupField);
412
+ if (!groups.has(groupValue)) {
413
+ groups.set(groupValue, []);
414
+ groupOrder.push(groupValue);
415
+ }
416
+ groups.get(groupValue).push(item);
278
417
  }
279
- if (priorityWidth > 0) {
280
- headerParts.push(chalk.dim('Priority'.padEnd(priorityWidth)));
418
+ // Sort groups by the field's natural order (using getFieldValue for sort key)
419
+ // For status, this will use statusIndex; for others, alphabetical
420
+ const lower = groupField.toLowerCase();
421
+ if (lower === 'status') {
422
+ // Sort by statusIndex of first item in each group
423
+ groupOrder.sort((a, b) => {
424
+ const aItem = groups.get(a)[0];
425
+ const bItem = groups.get(b)[0];
426
+ return aItem.statusIndex - bItem.statusIndex;
427
+ });
281
428
  }
282
- if (sizeWidth > 0) {
283
- headerParts.push(chalk.dim('Size'.padEnd(sizeWidth)));
429
+ else {
430
+ // Alphabetical, but put "No X" / "Unassigned" at the end
431
+ groupOrder.sort((a, b) => {
432
+ const aIsEmpty = a.startsWith('No ') || a === 'Unassigned';
433
+ const bIsEmpty = b.startsWith('No ') || b === 'Unassigned';
434
+ if (aIsEmpty && !bIsEmpty)
435
+ return 1;
436
+ if (!aIsEmpty && bIsEmpty)
437
+ return -1;
438
+ return a.localeCompare(b);
439
+ });
284
440
  }
285
- const hasLabels = rows.some(r => r.labels.length > 0);
286
- if (hasLabels) {
287
- headerParts.push(chalk.dim('Labels'));
441
+ // Determine columns - exclude the group field from columns
442
+ const columnsConfig = getConfig('columns');
443
+ let columns = columnsConfig
444
+ ? parseColumns(columnsConfig)
445
+ : DEFAULT_COLUMNS;
446
+ // Remove the grouped field from columns since it's shown in the header
447
+ const groupFieldLower = groupField.toLowerCase();
448
+ const fieldToColumnMap = {
449
+ 'status': 'status',
450
+ 'type': 'type',
451
+ 'issuetype': 'type',
452
+ 'issue-type': 'type',
453
+ 'assignee': 'assignees',
454
+ 'assignees': 'assignees',
455
+ 'priority': 'priority',
456
+ 'size': 'size',
457
+ 'label': 'labels',
458
+ 'labels': 'labels',
459
+ 'project': 'project',
460
+ 'repo': 'repository',
461
+ 'repository': 'repository',
462
+ };
463
+ const columnToRemove = fieldToColumnMap[groupFieldLower];
464
+ if (columnToRemove) {
465
+ columns = columns.filter(c => c !== columnToRemove);
288
466
  }
289
- console.log(` ${headerParts.join(' ')}`);
290
- console.log(chalk.dim(' ' + '─'.repeat(Math.min(termWidth - 4, fixedWidth + titleWidth + 10))));
291
- // Print rows
292
- for (const row of rows) {
293
- const parts = [];
294
- // Number
295
- parts.push(row.num === 'draft'
296
- ? chalk.dim(row.num.padEnd(numWidth))
297
- : chalk.cyan(row.num.padEnd(numWidth)));
298
- // Issue Type
299
- if (typeWidth > 0) {
300
- parts.push(chalk.yellow(row.type.padEnd(typeWidth)));
301
- }
302
- // Title (truncated if needed)
303
- const truncTitle = row.title.length > titleWidth
304
- ? row.title.substring(0, titleWidth - 1) + '…'
305
- : row.title.padEnd(titleWidth);
306
- parts.push(truncTitle);
307
- // Assignees
308
- if (assigneeWidth > 0) {
309
- parts.push(chalk.cyan(row.assignees.padEnd(assigneeWidth)));
310
- }
311
- // Priority
312
- if (priorityWidth > 0) {
313
- parts.push(chalk.magenta(row.priority.padEnd(priorityWidth)));
314
- }
315
- // Size
316
- if (sizeWidth > 0) {
317
- parts.push(chalk.blue(row.size.padEnd(sizeWidth)));
318
- }
319
- // Labels (not padded, at the end)
320
- if (row.labels.length > 0) {
321
- const labelStr = row.labels.map(l => {
322
- const bg = hexToChalk(l.color);
323
- return bg(` ${l.name} `);
324
- }).join(' ');
325
- parts.push(labelStr);
326
- }
327
- console.log(` ${parts.join(' ')}`);
467
+ // Pre-calculate column widths based on ALL items so tables align across groups
468
+ // We do this by getting the max width needed for each column across all items
469
+ const columnWidths = calculateColumnWidths(items, columns);
470
+ // Display each group
471
+ for (const groupValue of groupOrder) {
472
+ const groupItems = groups.get(groupValue);
473
+ console.log(chalk.bold.cyan(`■ ${groupValue}`) + chalk.dim(` (${groupItems.length})`));
474
+ console.log();
475
+ displayTableWithWidths(groupItems, columns, columnWidths);
476
+ console.log();
328
477
  }
329
- console.log();
330
- }
331
- function hexToChalk(hex) {
332
- const r = parseInt(hex.slice(0, 2), 16);
333
- const g = parseInt(hex.slice(2, 4), 16);
334
- const b = parseInt(hex.slice(4, 6), 16);
335
- const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
336
- const textColor = luminance > 0.5 ? chalk.black : chalk.white;
337
- return (text) => textColor.bgRgb(r, g, b)(text);
338
478
  }
339
479
  async function displayBoardView(items, projects, opts) {
340
480
  for (const project of projects) {