@task-mcp/shared 1.0.22 → 1.0.24

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 (184) hide show
  1. package/dist/algorithms/critical-path.d.ts +47 -0
  2. package/dist/algorithms/critical-path.d.ts.map +1 -0
  3. package/dist/algorithms/critical-path.js +340 -0
  4. package/dist/algorithms/critical-path.js.map +1 -0
  5. package/dist/algorithms/critical-path.test.d.ts +2 -0
  6. package/dist/algorithms/critical-path.test.d.ts.map +1 -0
  7. package/dist/algorithms/critical-path.test.js +184 -0
  8. package/dist/algorithms/critical-path.test.js.map +1 -0
  9. package/dist/algorithms/dependency-integrity.d.ts +81 -0
  10. package/dist/algorithms/dependency-integrity.d.ts.map +1 -0
  11. package/dist/algorithms/dependency-integrity.js +209 -0
  12. package/dist/algorithms/dependency-integrity.js.map +1 -0
  13. package/dist/algorithms/dependency-integrity.test.d.ts +2 -0
  14. package/dist/algorithms/dependency-integrity.test.d.ts.map +1 -0
  15. package/dist/algorithms/dependency-integrity.test.js +296 -0
  16. package/dist/algorithms/dependency-integrity.test.js.map +1 -0
  17. package/dist/algorithms/index.d.ts +5 -0
  18. package/dist/algorithms/index.d.ts.map +1 -0
  19. package/dist/algorithms/index.js +5 -0
  20. package/dist/algorithms/index.js.map +1 -0
  21. package/dist/algorithms/tech-analysis.d.ts +106 -0
  22. package/dist/algorithms/tech-analysis.d.ts.map +1 -0
  23. package/dist/algorithms/tech-analysis.js +351 -0
  24. package/dist/algorithms/tech-analysis.js.map +1 -0
  25. package/dist/algorithms/tech-analysis.test.d.ts +2 -0
  26. package/dist/algorithms/tech-analysis.test.d.ts.map +1 -0
  27. package/dist/algorithms/tech-analysis.test.js +330 -0
  28. package/dist/algorithms/tech-analysis.test.js.map +1 -0
  29. package/dist/algorithms/topological-sort.d.ts +58 -0
  30. package/dist/algorithms/topological-sort.d.ts.map +1 -0
  31. package/dist/algorithms/topological-sort.js +201 -0
  32. package/dist/algorithms/topological-sort.js.map +1 -0
  33. package/dist/algorithms/topological-sort.test.d.ts +2 -0
  34. package/dist/algorithms/topological-sort.test.d.ts.map +1 -0
  35. package/dist/algorithms/topological-sort.test.js +154 -0
  36. package/dist/algorithms/topological-sort.test.js.map +1 -0
  37. package/dist/index.d.ts +4 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +7 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/schemas/inbox.d.ts +55 -0
  42. package/dist/schemas/inbox.d.ts.map +1 -0
  43. package/dist/schemas/inbox.js +25 -0
  44. package/dist/schemas/inbox.js.map +1 -0
  45. package/dist/schemas/index.d.ts +7 -0
  46. package/dist/schemas/index.d.ts.map +1 -0
  47. package/dist/schemas/index.js +17 -0
  48. package/dist/schemas/index.js.map +1 -0
  49. package/dist/schemas/llm-guide.d.ts +147 -0
  50. package/dist/schemas/llm-guide.d.ts.map +1 -0
  51. package/dist/schemas/llm-guide.js +72 -0
  52. package/dist/schemas/llm-guide.js.map +1 -0
  53. package/dist/schemas/project.d.ts +177 -0
  54. package/dist/schemas/project.d.ts.map +1 -0
  55. package/dist/schemas/project.js +56 -0
  56. package/dist/schemas/project.js.map +1 -0
  57. package/dist/schemas/response-format.d.ts +148 -0
  58. package/dist/schemas/response-format.d.ts.map +1 -0
  59. package/dist/schemas/response-format.js +18 -0
  60. package/dist/schemas/response-format.js.map +1 -0
  61. package/dist/schemas/response-schema.d.ts +307 -0
  62. package/dist/schemas/response-schema.d.ts.map +1 -0
  63. package/dist/schemas/response-schema.js +78 -0
  64. package/dist/schemas/response-schema.js.map +1 -0
  65. package/dist/schemas/response-schema.test.d.ts +2 -0
  66. package/dist/schemas/response-schema.test.d.ts.map +1 -0
  67. package/dist/schemas/response-schema.test.js +256 -0
  68. package/dist/schemas/response-schema.test.js.map +1 -0
  69. package/dist/schemas/state.d.ts +17 -0
  70. package/dist/schemas/state.d.ts.map +1 -0
  71. package/dist/schemas/state.js +17 -0
  72. package/dist/schemas/state.js.map +1 -0
  73. package/dist/schemas/task.d.ts +881 -0
  74. package/dist/schemas/task.d.ts.map +1 -0
  75. package/dist/schemas/task.js +177 -0
  76. package/dist/schemas/task.js.map +1 -0
  77. package/dist/schemas/view.d.ts +143 -0
  78. package/dist/schemas/view.d.ts.map +1 -0
  79. package/dist/schemas/view.js +48 -0
  80. package/dist/schemas/view.js.map +1 -0
  81. package/dist/utils/dashboard-renderer.d.ts +93 -0
  82. package/dist/utils/dashboard-renderer.d.ts.map +1 -0
  83. package/dist/utils/dashboard-renderer.js +416 -0
  84. package/dist/utils/dashboard-renderer.js.map +1 -0
  85. package/dist/utils/dashboard-renderer.test.d.ts +2 -0
  86. package/dist/utils/dashboard-renderer.test.d.ts.map +1 -0
  87. package/dist/utils/dashboard-renderer.test.js +772 -0
  88. package/dist/utils/dashboard-renderer.test.js.map +1 -0
  89. package/dist/utils/date.d.ts +94 -0
  90. package/dist/utils/date.d.ts.map +1 -0
  91. package/dist/utils/date.js +323 -0
  92. package/dist/utils/date.js.map +1 -0
  93. package/dist/utils/date.test.d.ts +2 -0
  94. package/dist/utils/date.test.d.ts.map +1 -0
  95. package/dist/utils/date.test.js +276 -0
  96. package/dist/utils/date.test.js.map +1 -0
  97. package/dist/utils/hierarchy.d.ts +102 -0
  98. package/dist/utils/hierarchy.d.ts.map +1 -0
  99. package/dist/utils/hierarchy.js +236 -0
  100. package/dist/utils/hierarchy.js.map +1 -0
  101. package/dist/utils/hierarchy.test.d.ts +2 -0
  102. package/dist/utils/hierarchy.test.d.ts.map +1 -0
  103. package/dist/utils/hierarchy.test.js +423 -0
  104. package/dist/utils/hierarchy.test.js.map +1 -0
  105. package/dist/utils/id.d.ts +60 -0
  106. package/dist/utils/id.d.ts.map +1 -0
  107. package/dist/utils/id.js +118 -0
  108. package/dist/utils/id.js.map +1 -0
  109. package/dist/utils/id.test.d.ts +2 -0
  110. package/dist/utils/id.test.d.ts.map +1 -0
  111. package/dist/utils/id.test.js +193 -0
  112. package/dist/utils/id.test.js.map +1 -0
  113. package/dist/utils/index.d.ts +12 -0
  114. package/dist/utils/index.d.ts.map +1 -0
  115. package/dist/utils/index.js +34 -0
  116. package/dist/utils/index.js.map +1 -0
  117. package/dist/utils/natural-language.d.ts +111 -0
  118. package/dist/utils/natural-language.d.ts.map +1 -0
  119. package/dist/utils/natural-language.js +297 -0
  120. package/dist/utils/natural-language.js.map +1 -0
  121. package/dist/utils/natural-language.test.d.ts +2 -0
  122. package/dist/utils/natural-language.test.d.ts.map +1 -0
  123. package/dist/utils/natural-language.test.js +197 -0
  124. package/dist/utils/natural-language.test.js.map +1 -0
  125. package/dist/utils/priority-queue.d.ts +17 -0
  126. package/dist/utils/priority-queue.d.ts.map +1 -0
  127. package/dist/utils/priority-queue.js +62 -0
  128. package/dist/utils/priority-queue.js.map +1 -0
  129. package/dist/utils/priority-queue.test.d.ts +2 -0
  130. package/dist/utils/priority-queue.test.d.ts.map +1 -0
  131. package/dist/utils/priority-queue.test.js +82 -0
  132. package/dist/utils/priority-queue.test.js.map +1 -0
  133. package/dist/utils/projection.d.ts +65 -0
  134. package/dist/utils/projection.d.ts.map +1 -0
  135. package/dist/utils/projection.js +180 -0
  136. package/dist/utils/projection.js.map +1 -0
  137. package/dist/utils/projection.test.d.ts +2 -0
  138. package/dist/utils/projection.test.d.ts.map +1 -0
  139. package/dist/utils/projection.test.js +341 -0
  140. package/dist/utils/projection.test.js.map +1 -0
  141. package/dist/utils/terminal-ui.d.ts +208 -0
  142. package/dist/utils/terminal-ui.d.ts.map +1 -0
  143. package/dist/utils/terminal-ui.js +614 -0
  144. package/dist/utils/terminal-ui.js.map +1 -0
  145. package/dist/utils/terminal-ui.test.d.ts +2 -0
  146. package/dist/utils/terminal-ui.test.d.ts.map +1 -0
  147. package/dist/utils/terminal-ui.test.js +683 -0
  148. package/dist/utils/terminal-ui.test.js.map +1 -0
  149. package/dist/utils/workspace.d.ts +102 -0
  150. package/dist/utils/workspace.d.ts.map +1 -0
  151. package/dist/utils/workspace.js +183 -0
  152. package/dist/utils/workspace.js.map +1 -0
  153. package/dist/utils/workspace.test.d.ts +2 -0
  154. package/dist/utils/workspace.test.d.ts.map +1 -0
  155. package/dist/utils/workspace.test.js +97 -0
  156. package/dist/utils/workspace.test.js.map +1 -0
  157. package/package.json +5 -1
  158. package/src/algorithms/critical-path.test.ts +227 -0
  159. package/src/algorithms/critical-path.ts +14 -34
  160. package/src/algorithms/dependency-integrity.test.ts +335 -0
  161. package/src/algorithms/dependency-integrity.ts +4 -13
  162. package/src/algorithms/tech-analysis.test.ts +405 -0
  163. package/src/algorithms/tech-analysis.ts +27 -27
  164. package/src/algorithms/topological-sort.test.ts +182 -0
  165. package/src/algorithms/topological-sort.ts +6 -10
  166. package/src/schemas/index.ts +2 -13
  167. package/src/schemas/response-format.ts +6 -6
  168. package/src/schemas/response-schema.test.ts +314 -0
  169. package/src/schemas/response-schema.ts +25 -20
  170. package/src/schemas/task.ts +4 -22
  171. package/src/utils/dashboard-renderer.test.ts +976 -0
  172. package/src/utils/dashboard-renderer.ts +27 -59
  173. package/src/utils/date.test.ts +329 -0
  174. package/src/utils/date.ts +2 -10
  175. package/src/utils/hierarchy.test.ts +488 -0
  176. package/src/utils/hierarchy.ts +4 -5
  177. package/src/utils/id.test.ts +235 -0
  178. package/src/utils/index.ts +7 -1
  179. package/src/utils/natural-language.test.ts +234 -0
  180. package/src/utils/priority-queue.test.ts +103 -0
  181. package/src/utils/projection.test.ts +430 -0
  182. package/src/utils/terminal-ui.test.ts +831 -0
  183. package/src/utils/terminal-ui.ts +53 -54
  184. package/src/utils/workspace.test.ts +125 -0
@@ -185,11 +185,9 @@ export function isFullWidth(char: string): boolean {
185
185
  (code >= 0xffe0 && code <= 0xffe6) || // Fullwidth Symbols
186
186
  (code >= 0x20000 && code <= 0x2ffff) || // CJK Extension B, C, D, E, F
187
187
  (code >= 0x30000 && code <= 0x3ffff) || // CJK Extension G, H, I
188
-
189
188
  // Symbols (typically wide in terminals)
190
189
  (code >= 0x2600 && code <= 0x26ff) || // Misc Symbols (sun, moon, stars, etc.)
191
190
  (code >= 0x2700 && code <= 0x27bf) || // Dingbats
192
-
193
191
  // Emoji ranges (comprehensive coverage)
194
192
  (code >= 0x1f1e0 && code <= 0x1f1ff) || // Regional Indicator Symbols (flags)
195
193
  (code >= 0x1f300 && code <= 0x1f5ff) || // Misc Symbols & Pictographs
@@ -202,7 +200,6 @@ export function isFullWidth(char: string): boolean {
202
200
  (code >= 0x1fa00 && code <= 0x1fa6f) || // Chess Symbols
203
201
  (code >= 0x1fa70 && code <= 0x1faff) || // Symbols & Pictographs Extended-A
204
202
  (code >= 0x1fb00 && code <= 0x1fbff) || // Symbols for Legacy Computing
205
-
206
203
  // Additional emoji-related
207
204
  (code >= 0x231a && code <= 0x231b) || // Watch, Hourglass
208
205
  (code >= 0x23e9 && code <= 0x23f3) || // Media control symbols
@@ -278,7 +275,11 @@ export const visibleLength = displayWidth;
278
275
  /**
279
276
  * Pad string to width (accounting for display width)
280
277
  */
281
- export function pad(str: string, width: number, align: "left" | "right" | "center" = "left"): string {
278
+ export function pad(
279
+ str: string,
280
+ width: number,
281
+ align: "left" | "right" | "center" = "left"
282
+ ): string {
282
283
  const len = displayWidth(str);
283
284
  const diff = width - len;
284
285
  if (diff <= 0) return str;
@@ -359,7 +360,8 @@ export function progressBar(
359
360
  const filledCount = Math.round((percent / 100) * width);
360
361
  const emptyCount = width - filledCount;
361
362
 
362
- const bar = color(filled.repeat(filledCount), filledColor) + color(empty.repeat(emptyCount), emptyColor);
363
+ const bar =
364
+ color(filled.repeat(filledCount), filledColor) + color(empty.repeat(emptyCount), emptyColor);
363
365
 
364
366
  return showPercent ? `${bar} ${percent}%` : bar;
365
367
  }
@@ -383,7 +385,10 @@ export function box(content: string, options: BoxOptions = {}): string {
383
385
  const { padding = 1, borderColor = "cyan", title, rounded = true } = options;
384
386
 
385
387
  const lines = content.split("\n");
386
- const maxLen = Math.max(...lines.map(l => displayWidth(stripAnsi(l))), title ? title.length + 2 : 0);
388
+ const maxLen = Math.max(
389
+ ...lines.map((l) => displayWidth(stripAnsi(l))),
390
+ title ? title.length + 2 : 0
391
+ );
387
392
  const innerWidth = options.width ? options.width - 2 - padding * 2 : maxLen + padding * 2;
388
393
 
389
394
  const tl = rounded ? BOX.rTopLeft : BOX.topLeft;
@@ -402,7 +407,10 @@ export function box(content: string, options: BoxOptions = {}): string {
402
407
  const remaining = innerWidth - titlePart.length;
403
408
  const leftPad = Math.floor(remaining / 2);
404
409
  const rightPad = remaining - leftPad;
405
- top = applyBorder(tl + h.repeat(leftPad)) + c.bold(titlePart) + applyBorder(h.repeat(rightPad) + tr);
410
+ top =
411
+ applyBorder(tl + h.repeat(leftPad)) +
412
+ c.bold(titlePart) +
413
+ applyBorder(h.repeat(rightPad) + tr);
406
414
  } else {
407
415
  top = applyBorder(tl + h.repeat(innerWidth) + tr);
408
416
  }
@@ -412,10 +420,16 @@ export function box(content: string, options: BoxOptions = {}): string {
412
420
  const paddingLines = Array(padding).fill(padLine);
413
421
 
414
422
  // Content lines
415
- const contentLines = lines.map(line => {
423
+ const contentLines = lines.map((line) => {
416
424
  const lineWidth = displayWidth(stripAnsi(line));
417
425
  const padRight = innerWidth - lineWidth - padding;
418
- return applyBorder(v) + " ".repeat(padding) + line + " ".repeat(Math.max(0, padRight)) + applyBorder(v);
426
+ return (
427
+ applyBorder(v) +
428
+ " ".repeat(padding) +
429
+ line +
430
+ " ".repeat(Math.max(0, padRight)) +
431
+ applyBorder(v)
432
+ );
419
433
  });
420
434
 
421
435
  // Bottom border
@@ -452,16 +466,16 @@ export function drawBox(lines: string[], options: BoxOptions = {}): string[] {
452
466
  const rightBorder = remainingWidth - leftBorder;
453
467
  result.push(
454
468
  applyBorder(BOX.topLeft) +
455
- applyBorder(BOX.horizontal.repeat(leftBorder)) +
456
- c.label(titleStr) +
457
- applyBorder(BOX.horizontal.repeat(rightBorder)) +
458
- applyBorder(BOX.topRight)
469
+ applyBorder(BOX.horizontal.repeat(leftBorder)) +
470
+ c.label(titleStr) +
471
+ applyBorder(BOX.horizontal.repeat(rightBorder)) +
472
+ applyBorder(BOX.topRight)
459
473
  );
460
474
  } else {
461
475
  result.push(
462
476
  applyBorder(BOX.topLeft) +
463
- applyBorder(BOX.horizontal.repeat(boxWidth - 2)) +
464
- applyBorder(BOX.topRight)
477
+ applyBorder(BOX.horizontal.repeat(boxWidth - 2)) +
478
+ applyBorder(BOX.topRight)
465
479
  );
466
480
  }
467
481
 
@@ -469,18 +483,18 @@ export function drawBox(lines: string[], options: BoxOptions = {}): string[] {
469
483
  for (const line of lines) {
470
484
  result.push(
471
485
  applyBorder(BOX.vertical) +
472
- padStr +
473
- padEnd(line, innerWidth) +
474
- padStr +
475
- applyBorder(BOX.vertical)
486
+ padStr +
487
+ padEnd(line, innerWidth) +
488
+ padStr +
489
+ applyBorder(BOX.vertical)
476
490
  );
477
491
  }
478
492
 
479
493
  // Bottom border
480
494
  result.push(
481
495
  applyBorder(BOX.bottomLeft) +
482
- applyBorder(BOX.horizontal.repeat(boxWidth - 2)) +
483
- applyBorder(BOX.bottomRight)
496
+ applyBorder(BOX.horizontal.repeat(boxWidth - 2)) +
497
+ applyBorder(BOX.bottomRight)
484
498
  );
485
499
 
486
500
  return result;
@@ -494,9 +508,9 @@ export function drawBox(lines: string[], options: BoxOptions = {}): string[] {
494
508
  * Place multiple boxes side by side (string input, string output)
495
509
  */
496
510
  export function sideBySide(boxes: string[], gap: number = 2): string {
497
- const boxLines = boxes.map(b => b.split("\n"));
498
- const maxHeight = Math.max(...boxLines.map(lines => lines.length));
499
- const boxWidths = boxLines.map(lines => Math.max(...lines.map(l => displayWidth(l))));
511
+ const boxLines = boxes.map((b) => b.split("\n"));
512
+ const maxHeight = Math.max(...boxLines.map((lines) => lines.length));
513
+ const boxWidths = boxLines.map((lines) => Math.max(...lines.map((l) => displayWidth(l))));
500
514
 
501
515
  // Pad each box to max height
502
516
  const paddedBoxLines = boxLines.map((lines, i) => {
@@ -504,7 +518,7 @@ export function sideBySide(boxes: string[], gap: number = 2): string {
504
518
  while (lines.length < maxHeight) {
505
519
  lines.push(" ".repeat(width));
506
520
  }
507
- return lines.map(line => {
521
+ return lines.map((line) => {
508
522
  const lineWidth = displayWidth(line);
509
523
  if (lineWidth < width) {
510
524
  return line + " ".repeat(width - lineWidth);
@@ -518,7 +532,7 @@ export function sideBySide(boxes: string[], gap: number = 2): string {
518
532
  const gapStr = " ".repeat(gap);
519
533
 
520
534
  for (let i = 0; i < maxHeight; i++) {
521
- const lineParts = paddedBoxLines.map(lines => lines[i] ?? "");
535
+ const lineParts = paddedBoxLines.map((lines) => lines[i] ?? "");
522
536
  result.push(lineParts.join(gapStr));
523
537
  }
524
538
 
@@ -529,14 +543,8 @@ export function sideBySide(boxes: string[], gap: number = 2): string {
529
543
  * Merge two boxes side by side (array input, array output)
530
544
  * For MCP server compatibility
531
545
  */
532
- export function sideBySideArrays(
533
- leftLines: string[],
534
- rightLines: string[],
535
- gap = 2
536
- ): string[] {
537
- const leftWidth = leftLines.length > 0
538
- ? Math.max(...leftLines.map(displayWidth))
539
- : 0;
546
+ export function sideBySideArrays(leftLines: string[], rightLines: string[], gap = 2): string[] {
547
+ const leftWidth = leftLines.length > 0 ? Math.max(...leftLines.map(displayWidth)) : 0;
540
548
 
541
549
  const maxLines = Math.max(leftLines.length, rightLines.length);
542
550
  const result: string[] = [];
@@ -580,10 +588,10 @@ export function table<T extends Record<string, unknown>>(
580
588
  const { headerColor = "cyan", borderColor = "gray" } = options;
581
589
 
582
590
  // Calculate column widths
583
- const widths = columns.map(col => {
591
+ const widths = columns.map((col) => {
584
592
  const headerWidth = displayWidth(col.header);
585
593
  const maxDataWidth = Math.max(
586
- ...data.map(row => {
594
+ ...data.map((row) => {
587
595
  const val = col.format ? col.format(row[col.key], row) : String(row[col.key] ?? "");
588
596
  return displayWidth(val);
589
597
  }),
@@ -601,10 +609,10 @@ export function table<T extends Record<string, unknown>>(
601
609
  .join(` ${border} `);
602
610
 
603
611
  // Separator
604
- const separator = widths.map(w => hBorder.repeat(w)).join(color(`─${BOX.cross}─`, borderColor));
612
+ const separator = widths.map((w) => hBorder.repeat(w)).join(color(`─${BOX.cross}─`, borderColor));
605
613
 
606
614
  // Data rows
607
- const dataRows = data.map(row =>
615
+ const dataRows = data.map((row) =>
608
616
  columns
609
617
  .map((col, i) => {
610
618
  const w = widths[i] ?? 0;
@@ -621,33 +629,24 @@ export function table<T extends Record<string, unknown>>(
621
629
  * Render table with full borders (array output)
622
630
  * For MCP server compatibility
623
631
  */
624
- export function renderTable(
625
- columns: TableColumn[],
626
- rows: Record<string, unknown>[]
627
- ): string[] {
632
+ export function renderTable(columns: TableColumn[], rows: Record<string, unknown>[]): string[] {
628
633
  const colWidths: number[] = columns.map((col) => {
629
634
  const headerWidth = col.header.length;
630
- const maxValueWidth = Math.max(
631
- ...rows.map((row) => String(row[col.key] ?? "").length)
632
- );
635
+ const maxValueWidth = Math.max(...rows.map((row) => String(row[col.key] ?? "").length));
633
636
  return col.width ?? Math.max(headerWidth, maxValueWidth);
634
637
  });
635
638
 
636
639
  const result: string[] = [];
637
640
 
638
641
  // Header row
639
- const headerCells = columns.map((col, i) =>
640
- c.label(center(col.header, colWidths[i] ?? 0))
642
+ const headerCells = columns.map((col, i) => c.label(center(col.header, colWidths[i] ?? 0)));
643
+ result.push(
644
+ c.muted(BOX.vertical) + headerCells.join(c.muted(BOX.vertical)) + c.muted(BOX.vertical)
641
645
  );
642
- result.push(c.muted(BOX.vertical) + headerCells.join(c.muted(BOX.vertical)) + c.muted(BOX.vertical));
643
646
 
644
647
  // Separator
645
648
  const separator = columns.map((_, i) => BOX.horizontal.repeat(colWidths[i] ?? 0));
646
- result.push(
647
- c.muted(BOX.teeRight) +
648
- c.muted(separator.join(BOX.cross)) +
649
- c.muted(BOX.teeLeft)
650
- );
649
+ result.push(c.muted(BOX.teeRight) + c.muted(separator.join(BOX.cross)) + c.muted(BOX.teeLeft));
651
650
 
652
651
  // Data rows
653
652
  for (const row of rows) {
@@ -782,5 +781,5 @@ export function banner(text: string): string {
782
781
  }
783
782
  }
784
783
 
785
- return lines.map(l => c.cyan(l)).join("\n");
784
+ return lines.map((l) => c.cyan(l)).join("\n");
786
785
  }
@@ -0,0 +1,125 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ normalizeWorkspace,
4
+ detectWorkspaceSync,
5
+ detectWorkspace,
6
+ getWorkspaceFromPath,
7
+ getGitRepoRoot,
8
+ getGitRepoRootSync,
9
+ } from "./workspace";
10
+
11
+ describe("normalizeWorkspace", () => {
12
+ test("converts to lowercase", () => {
13
+ expect(normalizeWorkspace("MyProject")).toBe("myproject");
14
+ });
15
+
16
+ test("replaces spaces with hyphens", () => {
17
+ expect(normalizeWorkspace("my project")).toBe("my-project");
18
+ });
19
+
20
+ test("removes special characters", () => {
21
+ expect(normalizeWorkspace("my@project!")).toBe("myproject");
22
+ });
23
+
24
+ test("handles empty string", () => {
25
+ expect(normalizeWorkspace("")).toBe("default");
26
+ });
27
+
28
+ test("keeps hyphens and underscores", () => {
29
+ expect(normalizeWorkspace("my-project_name")).toBe("my-project_name");
30
+ });
31
+
32
+ test("trims leading and trailing hyphens", () => {
33
+ expect(normalizeWorkspace("--my-project--")).toBe("my-project");
34
+ });
35
+
36
+ test("handles multiple spaces", () => {
37
+ // Multiple spaces become multiple hyphens, then cleaned by special char removal
38
+ expect(normalizeWorkspace("my project")).toBe("my-project");
39
+ });
40
+
41
+ test("handles mixed case with special chars", () => {
42
+ expect(normalizeWorkspace("My@Project#Name")).toBe("myprojectname");
43
+ });
44
+ });
45
+
46
+ describe("getWorkspaceFromPath", () => {
47
+ test("extracts workspace from path", () => {
48
+ expect(getWorkspaceFromPath("/home/user/projects/my-app")).toBe("my-app");
49
+ });
50
+
51
+ test("handles path with .tasks suffix", () => {
52
+ expect(getWorkspaceFromPath("/projects/my-app/.tasks")).toBe("my-app");
53
+ });
54
+
55
+ test("handles path with trailing slash", () => {
56
+ expect(getWorkspaceFromPath("/projects/my-app/.tasks/")).toBe("my-app");
57
+ });
58
+
59
+ test("normalizes the extracted name", () => {
60
+ expect(getWorkspaceFromPath("/projects/My App")).toBe("my-app");
61
+ });
62
+ });
63
+
64
+ describe("getGitRepoRootSync", () => {
65
+ test("returns string or null for current directory", () => {
66
+ const result = getGitRepoRootSync();
67
+ expect(result === null || typeof result === "string").toBe(true);
68
+ });
69
+
70
+ test("returns null for non-git directory", () => {
71
+ const result = getGitRepoRootSync("/tmp");
72
+ expect(result).toBeNull();
73
+ });
74
+ });
75
+
76
+ describe("getGitRepoRoot", () => {
77
+ test("returns string or null for current directory", async () => {
78
+ const result = await getGitRepoRoot();
79
+ expect(result === null || typeof result === "string").toBe(true);
80
+ });
81
+
82
+ test("returns null for non-git directory", async () => {
83
+ const result = await getGitRepoRoot("/tmp");
84
+ expect(result).toBeNull();
85
+ });
86
+ });
87
+
88
+ describe("detectWorkspaceSync", () => {
89
+ test("returns a workspace name", () => {
90
+ const workspace = detectWorkspaceSync();
91
+ expect(typeof workspace).toBe("string");
92
+ expect(workspace.length).toBeGreaterThan(0);
93
+ });
94
+
95
+ test("uses tasksDir path when provided and no git repo", () => {
96
+ // This tests the fallback to tasksDir when in a non-git directory
97
+ const workspace = detectWorkspaceSync("/some/path/my-project/.tasks");
98
+ expect(typeof workspace).toBe("string");
99
+ });
100
+
101
+ test("returns normalized workspace name", () => {
102
+ const workspace = detectWorkspaceSync();
103
+ // Should be lowercase with no special characters except - and _
104
+ expect(workspace).toMatch(/^[a-z0-9_-]+$/);
105
+ });
106
+ });
107
+
108
+ describe("detectWorkspace", () => {
109
+ test("returns a workspace name asynchronously", async () => {
110
+ const workspace = await detectWorkspace();
111
+ expect(typeof workspace).toBe("string");
112
+ expect(workspace.length).toBeGreaterThan(0);
113
+ });
114
+
115
+ test("uses tasksDir path when provided", async () => {
116
+ const workspace = await detectWorkspace("/some/path/test-project/.tasks");
117
+ expect(typeof workspace).toBe("string");
118
+ });
119
+
120
+ test("returns normalized workspace name", async () => {
121
+ const workspace = await detectWorkspace();
122
+ // Should be lowercase with no special characters except - and _
123
+ expect(workspace).toMatch(/^[a-z0-9_-]+$/);
124
+ });
125
+ });