@vanshbhardwaj/worklog 1.0.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.
Files changed (68) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +19 -0
  4. package/.github/workflows/release.yml +63 -0
  5. package/.github/workflows/validate.yml +103 -0
  6. package/.prettierrc +8 -0
  7. package/ARCHITECTURE.md +82 -0
  8. package/CODE_OF_CONDUCT.md +65 -0
  9. package/CONTRIBUTING.md +74 -0
  10. package/INSTALLATION.md +98 -0
  11. package/LICENSE +21 -0
  12. package/README.md +246 -0
  13. package/SECURITY.md +18 -0
  14. package/benchmarks/benchmark.ts +149 -0
  15. package/benchmarks/vitest.bench.config.ts +9 -0
  16. package/dist/database/migrations/001_initial.sql +60 -0
  17. package/dist/database/migrations/002_relational_tables.sql +22 -0
  18. package/dist/index.cjs +31007 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1873 -0
  22. package/dist/index.js.map +1 -0
  23. package/eslint.config.js +38 -0
  24. package/package.json +49 -0
  25. package/pnpm-workspace.yaml +6 -0
  26. package/scripts/build-bin.js +75 -0
  27. package/sea-config.json +5 -0
  28. package/src/cli/commands/add.ts +176 -0
  29. package/src/cli/commands/continue.ts +80 -0
  30. package/src/cli/commands/dashboard.ts +30 -0
  31. package/src/cli/commands/export.ts +72 -0
  32. package/src/cli/commands/init.ts +52 -0
  33. package/src/cli/commands/list.ts +33 -0
  34. package/src/cli/commands/search.ts +33 -0
  35. package/src/cli/commands/show.ts +42 -0
  36. package/src/cli/commands/stats.ts +23 -0
  37. package/src/cli/commands/today.ts +27 -0
  38. package/src/cli/commands/week.ts +93 -0
  39. package/src/cli/index.ts +294 -0
  40. package/src/cli/ui.ts +323 -0
  41. package/src/core/config.ts +69 -0
  42. package/src/core/discovery.ts +38 -0
  43. package/src/core/logger.ts +53 -0
  44. package/src/database/connection.ts +95 -0
  45. package/src/database/migration-data.ts +71 -0
  46. package/src/database/migrations/001_initial.sql +60 -0
  47. package/src/database/migrations/002_relational_tables.sql +22 -0
  48. package/src/database/migrations.ts +65 -0
  49. package/src/errors/index.ts +51 -0
  50. package/src/exporters/csv.ts +59 -0
  51. package/src/exporters/json.ts +8 -0
  52. package/src/exporters/markdown.ts +71 -0
  53. package/src/models/work-unit.ts +38 -0
  54. package/src/repositories/work-unit.ts +466 -0
  55. package/src/services/work-unit.ts +131 -0
  56. package/src/tests/cli/__snapshots__/cli-productivity.test.ts.snap +37 -0
  57. package/src/tests/cli/__snapshots__/cli.test.ts.snap +20 -0
  58. package/src/tests/cli/cli-productivity.test.ts +167 -0
  59. package/src/tests/cli/cli.test.ts +171 -0
  60. package/src/tests/core/config.test.ts +53 -0
  61. package/src/tests/core/discovery.test.ts +46 -0
  62. package/src/tests/database/migrations.test.ts +75 -0
  63. package/src/tests/repositories/work-unit.test.ts +243 -0
  64. package/src/tests/services/work-unit.test.ts +87 -0
  65. package/src/validators/work-unit.ts +63 -0
  66. package/tsconfig.json +16 -0
  67. package/tsup.config.ts +50 -0
  68. package/vitest.config.ts +8 -0
@@ -0,0 +1,176 @@
1
+ import prompts from 'prompts';
2
+ import pc from 'picocolors';
3
+ import { WorkUnitService } from '../../services/work-unit.js';
4
+
5
+ interface AddOptions {
6
+ title?: string;
7
+ type?: string;
8
+ status?: string;
9
+ priority?: string;
10
+ module?: string;
11
+ tags?: string;
12
+ description?: string;
13
+ next?: string;
14
+ notes?: string;
15
+ json?: boolean;
16
+ }
17
+
18
+ function capitalize(val: string): string {
19
+ if (!val) return val;
20
+ return val.charAt(0).toUpperCase() + val.slice(1).toLowerCase();
21
+ }
22
+
23
+ function parseStatus(val: string): string {
24
+ if (!val) return val;
25
+ const lower = val.toLowerCase();
26
+ if (lower === 'in-progress' || lower === 'inprogress' || lower === 'in_progress') {
27
+ return 'In Progress';
28
+ }
29
+ return capitalize(val);
30
+ }
31
+
32
+ /**
33
+ * Creates a new Work Unit. If the title is omitted, starts interactive prompting.
34
+ */
35
+ export async function addCommand(service: WorkUnitService, options: AddOptions): Promise<void> {
36
+ let inputData: any;
37
+
38
+ if (!options.title) {
39
+ // Check if stdin is a TTY before starting prompts
40
+ if (!process.stdin.isTTY) {
41
+ if (options.json) {
42
+ console.error(
43
+ JSON.stringify({
44
+ success: false,
45
+ error: 'Title is required in non-interactive mode.',
46
+ }),
47
+ );
48
+ } else {
49
+ console.error(pc.red('Error: Title is required in non-interactive mode.'));
50
+ }
51
+ process.exit(1);
52
+ }
53
+
54
+ // Interactive Mode
55
+ const responses = await prompts(
56
+ [
57
+ {
58
+ type: 'text',
59
+ name: 'title',
60
+ message: 'Title:',
61
+ validate: (val) => (val.trim().length > 0 ? true : 'Title is required'),
62
+ },
63
+ {
64
+ type: 'select',
65
+ name: 'type',
66
+ message: 'Type:',
67
+ choices: [
68
+ { title: 'Feature', value: 'Feature' },
69
+ { title: 'Bug', value: 'Bug' },
70
+ { title: 'Spike', value: 'Spike' },
71
+ { title: 'Refactor', value: 'Refactor' },
72
+ { title: 'Improvement', value: 'Improvement' },
73
+ { title: 'Research', value: 'Research' },
74
+ { title: 'Decision', value: 'Decision' },
75
+ { title: 'Blocker', value: 'Blocker' },
76
+ { title: 'Review', value: 'Review' },
77
+ { title: 'Idea', value: 'Idea' },
78
+ ],
79
+ initial: 0,
80
+ },
81
+ {
82
+ type: 'select',
83
+ name: 'status',
84
+ message: 'Status:',
85
+ choices: [
86
+ { title: 'Planned', value: 'Planned' },
87
+ { title: 'In Progress', value: 'In Progress' },
88
+ { title: 'Blocked', value: 'Blocked' },
89
+ { title: 'Completed', value: 'Completed' },
90
+ { title: 'Cancelled', value: 'Cancelled' },
91
+ ],
92
+ initial: 0,
93
+ },
94
+ {
95
+ type: 'text',
96
+ name: 'description',
97
+ message: 'Description (optional):',
98
+ format: (val) => val.trim() || null,
99
+ },
100
+ {
101
+ type: 'text',
102
+ name: 'module',
103
+ message: 'Module (optional):',
104
+ format: (val) => val.trim() || null,
105
+ },
106
+ {
107
+ type: 'text',
108
+ name: 'tags',
109
+ message: 'Tags (comma-separated, optional):',
110
+ format: (val) => val.trim() || null,
111
+ },
112
+ {
113
+ type: 'text',
114
+ name: 'nextStep',
115
+ message: 'Next Step (optional):',
116
+ format: (val) => val.trim() || null,
117
+ },
118
+ ],
119
+ {
120
+ onCancel: () => {
121
+ if (options.json) {
122
+ console.log(JSON.stringify({ success: false, error: 'Cancelled by user' }));
123
+ } else {
124
+ console.log(pc.yellow('\nCancelled.'));
125
+ }
126
+ process.exit(1);
127
+ },
128
+ },
129
+ );
130
+
131
+ const parsedTags = responses.tags
132
+ ? responses.tags
133
+ .split(',')
134
+ .map((t: string) => t.trim())
135
+ .filter(Boolean)
136
+ : [];
137
+
138
+ inputData = {
139
+ title: responses.title,
140
+ type: responses.type,
141
+ status: responses.status,
142
+ description: responses.description,
143
+ module: responses.module,
144
+ tags: parsedTags,
145
+ nextStep: responses.nextStep,
146
+ };
147
+ } else {
148
+ // Non-Interactive Mode
149
+ const parsedTags = options.tags
150
+ ? options.tags
151
+ .split(',')
152
+ .map((t: string) => t.trim())
153
+ .filter(Boolean)
154
+ : [];
155
+
156
+ inputData = {
157
+ title: options.title,
158
+ type: options.type ? capitalize(options.type) : 'Feature',
159
+ status: options.status ? parseStatus(options.status) : 'Planned',
160
+ priority: options.priority ? capitalize(options.priority) : null,
161
+ module: options.module || null,
162
+ tags: parsedTags,
163
+ description: options.description || null,
164
+ nextStep: options.next || null,
165
+ notes: options.notes || null,
166
+ };
167
+ }
168
+
169
+ const created = service.createWorkUnit(inputData);
170
+
171
+ if (options.json) {
172
+ console.log(JSON.stringify(created));
173
+ } else {
174
+ console.log(pc.green(`Created work unit #${created.id}: ${created.title}`));
175
+ }
176
+ }
@@ -0,0 +1,80 @@
1
+ import prompts from 'prompts';
2
+ import pc from 'picocolors';
3
+ import { WorkUnitService } from '../../services/work-unit.js';
4
+
5
+ interface ContinueOptions {
6
+ json?: boolean;
7
+ }
8
+
9
+ /**
10
+ * Resumes work on the most recent unfinished Work Unit.
11
+ */
12
+ export async function continueCommand(
13
+ service: WorkUnitService,
14
+ options: ContinueOptions,
15
+ ): Promise<void> {
16
+ const unfinished = service.getUnfinishedWorkUnits(); // sorted by updated_at desc
17
+
18
+ if (unfinished.length === 0) {
19
+ if (options.json) {
20
+ console.log(JSON.stringify({ success: false, error: 'No unfinished work units found.' }));
21
+ } else {
22
+ console.log(pc.yellow('No unfinished work units found.'));
23
+ }
24
+ return;
25
+ }
26
+
27
+ let selectedUnit = unfinished[0];
28
+
29
+ if (unfinished.length > 1) {
30
+ if (!process.stdin.isTTY) {
31
+ // Non-interactive: auto-resume the most recently touched unit
32
+ selectedUnit = unfinished[0];
33
+ } else {
34
+ const choices = unfinished.map((u) => ({
35
+ title: `#${u.id} - ${u.title} (${u.status})`,
36
+ value: u,
37
+ }));
38
+
39
+ const response = await prompts(
40
+ {
41
+ type: 'select',
42
+ name: 'unit',
43
+ message: 'Select an unfinished work unit to resume:',
44
+ choices,
45
+ initial: 0,
46
+ },
47
+ {
48
+ onCancel: () => {
49
+ if (options.json) {
50
+ console.log(JSON.stringify({ success: false, error: 'Cancelled by user' }));
51
+ } else {
52
+ console.log(pc.yellow('\nCancelled.'));
53
+ }
54
+ process.exit(1);
55
+ },
56
+ },
57
+ );
58
+
59
+ if (!response.unit) {
60
+ process.exit(1);
61
+ }
62
+ selectedUnit = response.unit;
63
+ }
64
+ }
65
+
66
+ // Update status to In Progress
67
+ const updated = service.updateWorkUnit(selectedUnit.id, {
68
+ status: 'In Progress',
69
+ });
70
+
71
+ if (options.json) {
72
+ console.log(JSON.stringify(updated));
73
+ } else {
74
+ console.log(
75
+ pc.green(
76
+ `Resumed work unit #${updated.id}: ${updated.title} (Status updated to 'In Progress')`,
77
+ ),
78
+ );
79
+ }
80
+ }
@@ -0,0 +1,30 @@
1
+ import { WorkUnitService } from '../../services/work-unit.js';
2
+ import { formatDashboard } from '../ui.js';
3
+
4
+ interface DashboardOptions {
5
+ json?: boolean;
6
+ }
7
+
8
+ /**
9
+ * Renders the WorkLog project overview dashboard.
10
+ */
11
+ export function dashboardCommand(service: WorkUnitService, options: DashboardOptions): void {
12
+ const inProgress = service.getWorkUnitsByStatus('In Progress');
13
+ const recentlyUpdated = service.getRecentWorkUnits(5);
14
+
15
+ const blocked = service.getWorkUnitsByStatus('Blocked');
16
+ const planned = service.getWorkUnitsByStatus('Planned');
17
+ const needsAttention = [...blocked, ...planned].slice(0, 5); // limit to 5 to keep dashboard concise
18
+
19
+ if (options.json) {
20
+ console.log(
21
+ JSON.stringify({
22
+ inProgress,
23
+ recentlyUpdated,
24
+ needsAttention,
25
+ }),
26
+ );
27
+ } else {
28
+ console.log(formatDashboard(inProgress, recentlyUpdated, needsAttention));
29
+ }
30
+ }
@@ -0,0 +1,72 @@
1
+ import * as fs from 'fs';
2
+ import pc from 'picocolors';
3
+ import { WorkUnitService } from '../../services/work-unit.js';
4
+ import { exportToMarkdown } from '../../exporters/markdown.js';
5
+ import { exportToCSV } from '../../exporters/csv.js';
6
+ import { exportToJSON } from '../../exporters/json.js';
7
+ import { WorkUnit } from '../../models/work-unit.js';
8
+
9
+ interface ExportOptions {
10
+ format?: string;
11
+ output?: string;
12
+ id?: string;
13
+ }
14
+
15
+ /**
16
+ * Handles exporting all work units or a single work unit into Markdown, CSV, or JSON formats.
17
+ */
18
+ export function exportCommand(service: WorkUnitService, options: ExportOptions): void {
19
+ let data: WorkUnit | WorkUnit[];
20
+
21
+ if (options.id) {
22
+ const id = Number(options.id);
23
+ if (isNaN(id) || !Number.isInteger(id) || id <= 0) {
24
+ console.error(pc.red('Error: A valid positive integer ID is required.'));
25
+ process.exit(1);
26
+ }
27
+ const unit = service.getWorkUnit(id);
28
+ if (!unit) {
29
+ console.error(pc.red(`Error: Work unit #${id} not found.`));
30
+ process.exit(1);
31
+ }
32
+ data = unit;
33
+ } else {
34
+ // Retrieve all units (up to 1,000,000 limit)
35
+ data = service.getRecentWorkUnits(1000000);
36
+ }
37
+
38
+ let formatted = '';
39
+ const format = (options.format || 'markdown').toLowerCase();
40
+
41
+ switch (format) {
42
+ case 'markdown':
43
+ case 'md':
44
+ formatted = exportToMarkdown(data);
45
+ break;
46
+ case 'csv':
47
+ formatted = exportToCSV(data);
48
+ break;
49
+ case 'json':
50
+ formatted = exportToJSON(data);
51
+ break;
52
+ default:
53
+ console.error(
54
+ pc.red(
55
+ `Error: Unsupported format '${options.format}'. Supported: markdown (md), csv, json.`,
56
+ ),
57
+ );
58
+ process.exit(1);
59
+ }
60
+
61
+ if (options.output) {
62
+ try {
63
+ fs.writeFileSync(options.output, formatted, 'utf8');
64
+ console.log(pc.green(`Exported successfully to ${options.output}`));
65
+ } catch (error: any) {
66
+ console.error(pc.red(`Error: Failed to write to file '${options.output}': ${error.message}`));
67
+ process.exit(1);
68
+ }
69
+ } else {
70
+ process.stdout.write(formatted);
71
+ }
72
+ }
@@ -0,0 +1,52 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import pc from 'picocolors';
4
+ import { initConfig } from '../../core/config.js';
5
+ import { connectDatabase } from '../../database/connection.js';
6
+ import { runMigrations } from '../../database/migrations.js';
7
+ import { initLogger } from '../../core/logger.js';
8
+
9
+ interface InitOptions {
10
+ name?: string;
11
+ json?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Initializes a new WorkLog repository in the current working directory.
16
+ */
17
+ export function initCommand(options: InitOptions): void {
18
+ const targetDir = process.cwd();
19
+ const dotWorklog = path.join(targetDir, '.worklog');
20
+
21
+ const exists = fs.existsSync(dotWorklog) && fs.statSync(dotWorklog).isDirectory();
22
+
23
+ if (!fs.existsSync(dotWorklog)) {
24
+ fs.mkdirSync(dotWorklog, { recursive: true });
25
+ }
26
+
27
+ // 1. Initialize configuration
28
+ const config = initConfig(targetDir, options.name);
29
+
30
+ // 2. Initialize logger
31
+ initLogger(targetDir);
32
+
33
+ // 3. Connect database & run migrations
34
+ const db = connectDatabase(targetDir);
35
+ runMigrations(db);
36
+
37
+ if (options.json) {
38
+ console.log(
39
+ JSON.stringify({
40
+ success: true,
41
+ message: exists
42
+ ? 'Reinitialized existing WorkLog repository'
43
+ : 'Initialized empty WorkLog repository',
44
+ projectName: config.projectName,
45
+ path: dotWorklog,
46
+ }),
47
+ );
48
+ } else {
49
+ const verb = exists ? 'Reinitialized existing' : 'Initialized empty';
50
+ console.log(pc.green(`${verb} WorkLog repository in ${dotWorklog}/`));
51
+ }
52
+ }
@@ -0,0 +1,33 @@
1
+ import { WorkUnitService } from '../../services/work-unit.js';
2
+ import { formatTable } from '../ui.js';
3
+
4
+ interface ListOptions {
5
+ limit?: string;
6
+ status?: string;
7
+ type?: string;
8
+ json?: boolean;
9
+ }
10
+
11
+ /**
12
+ * Lists recent Work Units with optional filters.
13
+ */
14
+ export function listCommand(service: WorkUnitService, options: ListOptions): void {
15
+ const limitVal = options.limit ? Number(options.limit) : 20;
16
+ let units = service.getRecentWorkUnits(limitVal);
17
+
18
+ if (options.status) {
19
+ const filterStatus = options.status.toLowerCase();
20
+ units = units.filter((u) => u.status.toLowerCase() === filterStatus);
21
+ }
22
+
23
+ if (options.type) {
24
+ const filterType = options.type.toLowerCase();
25
+ units = units.filter((u) => u.type.toLowerCase() === filterType);
26
+ }
27
+
28
+ if (options.json) {
29
+ console.log(JSON.stringify(units));
30
+ } else {
31
+ console.log(formatTable(units));
32
+ }
33
+ }
@@ -0,0 +1,33 @@
1
+ import { WorkUnitService } from '../../services/work-unit.js';
2
+ import { formatTable } from '../ui.js';
3
+
4
+ interface SearchOptions {
5
+ tag?: string;
6
+ module?: string;
7
+ status?: string;
8
+ type?: string;
9
+ json?: boolean;
10
+ }
11
+
12
+ /**
13
+ * Searches Work Units by text queries and/or structured category filters.
14
+ */
15
+ export function searchCommand(
16
+ service: WorkUnitService,
17
+ query: string | undefined,
18
+ options: SearchOptions,
19
+ ): void {
20
+ const textQuery = query || '';
21
+ const results = service.searchWorkUnits(textQuery, {
22
+ tag: options.tag,
23
+ module: options.module,
24
+ status: options.status,
25
+ type: options.type,
26
+ });
27
+
28
+ if (options.json) {
29
+ console.log(JSON.stringify(results));
30
+ } else {
31
+ console.log(formatTable(results));
32
+ }
33
+ }
@@ -0,0 +1,42 @@
1
+ import pc from 'picocolors';
2
+ import { WorkUnitService } from '../../services/work-unit.js';
3
+ import { formatWorkUnitDetail } from '../ui.js';
4
+
5
+ interface ShowOptions {
6
+ json?: boolean;
7
+ }
8
+
9
+ /**
10
+ * Displays detailed information about a single Work Unit.
11
+ */
12
+ export function showCommand(
13
+ service: WorkUnitService,
14
+ idStr: string,
15
+ options: ShowOptions,
16
+ ): void {
17
+ const id = Number(idStr);
18
+ if (isNaN(id) || !Number.isInteger(id) || id <= 0) {
19
+ if (options.json) {
20
+ console.log(JSON.stringify({ success: false, error: 'A valid positive integer ID is required.' }));
21
+ } else {
22
+ console.error(pc.red('Error: A valid positive integer ID is required.'));
23
+ }
24
+ process.exit(1);
25
+ }
26
+
27
+ const unit = service.getWorkUnit(id);
28
+ if (!unit) {
29
+ if (options.json) {
30
+ console.log(JSON.stringify({ success: false, error: `Work unit #${id} not found.` }));
31
+ } else {
32
+ console.error(pc.red(`Error: Work unit #${id} not found.`));
33
+ }
34
+ process.exit(1);
35
+ }
36
+
37
+ if (options.json) {
38
+ console.log(JSON.stringify(unit));
39
+ } else {
40
+ console.log(formatWorkUnitDetail(unit));
41
+ }
42
+ }
@@ -0,0 +1,23 @@
1
+ import { WorkUnitService } from '../../services/work-unit.js';
2
+ import { formatStats } from '../ui.js';
3
+
4
+ interface StatsOptions {
5
+ json?: boolean;
6
+ }
7
+
8
+ /**
9
+ * Renders analytical distributions and activity stats.
10
+ */
11
+ export function statsCommand(service: WorkUnitService, options: StatsOptions): void {
12
+ const sevenDaysAgo = new Date();
13
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
14
+ sevenDaysAgo.setHours(0, 0, 0, 0);
15
+
16
+ const stats = service.getStats(sevenDaysAgo.toISOString());
17
+
18
+ if (options.json) {
19
+ console.log(JSON.stringify(stats));
20
+ } else {
21
+ console.log(formatStats(stats));
22
+ }
23
+ }
@@ -0,0 +1,27 @@
1
+ import { WorkUnitService } from '../../services/work-unit.js';
2
+ import { formatTable } from '../ui.js';
3
+
4
+ interface TodayOptions {
5
+ json?: boolean;
6
+ }
7
+
8
+ /**
9
+ * Displays work units updated or created today.
10
+ */
11
+ export function todayCommand(service: WorkUnitService, options: TodayOptions): void {
12
+ const startOfToday = new Date();
13
+ startOfToday.setHours(0, 0, 0, 0);
14
+ const isoString = startOfToday.toISOString();
15
+
16
+ const units = service.getCreatedOrUpdatedAfter(isoString);
17
+
18
+ if (options.json) {
19
+ console.log(JSON.stringify(units));
20
+ } else {
21
+ if (units.length === 0) {
22
+ console.log('No work units logged today.');
23
+ return;
24
+ }
25
+ console.log(formatTable(units));
26
+ }
27
+ }
@@ -0,0 +1,93 @@
1
+ import pc from 'picocolors';
2
+ import { WorkUnitService } from '../../services/work-unit.js';
3
+
4
+ interface WeekOptions {
5
+ json?: boolean;
6
+ }
7
+
8
+ /**
9
+ * Displays a weekly engineering summary of work logged over the past 7 days.
10
+ */
11
+ export function weekCommand(service: WorkUnitService, options: WeekOptions): void {
12
+ const sevenDaysAgo = new Date();
13
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
14
+ sevenDaysAgo.setHours(0, 0, 0, 0);
15
+ const isoString = sevenDaysAgo.toISOString();
16
+
17
+ const units = service.getCreatedOrUpdatedAfter(isoString);
18
+
19
+ if (options.json) {
20
+ console.log(JSON.stringify(units));
21
+ return;
22
+ }
23
+
24
+ if (units.length === 0) {
25
+ console.log('No work units logged in the past 7 days.');
26
+ return;
27
+ }
28
+
29
+ // Filter categories
30
+ const completedFeatures = units.filter((u) => u.type === 'Feature' && u.status === 'Completed');
31
+ const fixedBugs = units.filter((u) => u.type === 'Bug' && u.status === 'Completed');
32
+ const decisions = units.filter((u) => u.type === 'Decision' || u.decision !== null);
33
+ const blockers = units.filter((u) => u.type === 'Blocker' || u.status === 'Blocked');
34
+ const research = units.filter((u) => u.type === 'Research' || u.type === 'Spike');
35
+
36
+ console.log(pc.bold(pc.cyan('=== WEEKLY ENGINEERING SUMMARY ===\n')));
37
+
38
+ console.log(pc.bold(`Features Completed (${completedFeatures.length}):`));
39
+ if (completedFeatures.length === 0) console.log(' None');
40
+ for (const u of completedFeatures) {
41
+ console.log(` - #${u.id}: ${u.title}`);
42
+ }
43
+ console.log('');
44
+
45
+ console.log(pc.bold(`Bugs Fixed (${fixedBugs.length}):`));
46
+ if (fixedBugs.length === 0) console.log(' None');
47
+ for (const u of fixedBugs) {
48
+ console.log(` - #${u.id}: ${u.title}`);
49
+ }
50
+ console.log('');
51
+
52
+ console.log(pc.bold(`Decisions Made (${decisions.length}):`));
53
+ if (decisions.length === 0) console.log(' None');
54
+ for (const u of decisions) {
55
+ console.log(` - #${u.id}: ${u.title}`);
56
+ if (u.decision) console.log(` ✔ ${pc.green(u.decision)}`);
57
+ }
58
+ console.log('');
59
+
60
+ console.log(pc.bold(`Blockers Encountered (${blockers.length}):`));
61
+ if (blockers.length === 0) console.log(' None');
62
+ for (const u of blockers) {
63
+ console.log(` - #${u.id}: ${u.title}`);
64
+ if (u.blocker) console.log(` ✖ ${pc.red(u.blocker)}`);
65
+ }
66
+ console.log('');
67
+
68
+ console.log(pc.bold(`Research & Spikes (${research.length}):`));
69
+ if (research.length === 0) console.log(' None');
70
+ for (const u of research) {
71
+ console.log(` - #${u.id}: ${u.title} (${u.type})`);
72
+ }
73
+ console.log('');
74
+
75
+ // Status & Type counts
76
+ const typeCounts: Record<string, number> = {};
77
+ const statusCounts: Record<string, number> = {};
78
+ for (const u of units) {
79
+ typeCounts[u.type] = (typeCounts[u.type] || 0) + 1;
80
+ statusCounts[u.status] = (statusCounts[u.status] || 0) + 1;
81
+ }
82
+
83
+ console.log(pc.bold('WORK UNITS BY STATUS:'));
84
+ for (const [status, count] of Object.entries(statusCounts)) {
85
+ console.log(` - ${status.padEnd(15)}: ${count}`);
86
+ }
87
+ console.log('');
88
+
89
+ console.log(pc.bold('WORK UNITS BY TYPE:'));
90
+ for (const [type, count] of Object.entries(typeCounts)) {
91
+ console.log(` - ${type.padEnd(15)}: ${count}`);
92
+ }
93
+ }