@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.
- package/.github/ISSUE_TEMPLATE/bug_report.md +28 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +19 -0
- package/.github/workflows/release.yml +63 -0
- package/.github/workflows/validate.yml +103 -0
- package/.prettierrc +8 -0
- package/ARCHITECTURE.md +82 -0
- package/CODE_OF_CONDUCT.md +65 -0
- package/CONTRIBUTING.md +74 -0
- package/INSTALLATION.md +98 -0
- package/LICENSE +21 -0
- package/README.md +246 -0
- package/SECURITY.md +18 -0
- package/benchmarks/benchmark.ts +149 -0
- package/benchmarks/vitest.bench.config.ts +9 -0
- package/dist/database/migrations/001_initial.sql +60 -0
- package/dist/database/migrations/002_relational_tables.sql +22 -0
- package/dist/index.cjs +31007 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1873 -0
- package/dist/index.js.map +1 -0
- package/eslint.config.js +38 -0
- package/package.json +49 -0
- package/pnpm-workspace.yaml +6 -0
- package/scripts/build-bin.js +75 -0
- package/sea-config.json +5 -0
- package/src/cli/commands/add.ts +176 -0
- package/src/cli/commands/continue.ts +80 -0
- package/src/cli/commands/dashboard.ts +30 -0
- package/src/cli/commands/export.ts +72 -0
- package/src/cli/commands/init.ts +52 -0
- package/src/cli/commands/list.ts +33 -0
- package/src/cli/commands/search.ts +33 -0
- package/src/cli/commands/show.ts +42 -0
- package/src/cli/commands/stats.ts +23 -0
- package/src/cli/commands/today.ts +27 -0
- package/src/cli/commands/week.ts +93 -0
- package/src/cli/index.ts +294 -0
- package/src/cli/ui.ts +323 -0
- package/src/core/config.ts +69 -0
- package/src/core/discovery.ts +38 -0
- package/src/core/logger.ts +53 -0
- package/src/database/connection.ts +95 -0
- package/src/database/migration-data.ts +71 -0
- package/src/database/migrations/001_initial.sql +60 -0
- package/src/database/migrations/002_relational_tables.sql +22 -0
- package/src/database/migrations.ts +65 -0
- package/src/errors/index.ts +51 -0
- package/src/exporters/csv.ts +59 -0
- package/src/exporters/json.ts +8 -0
- package/src/exporters/markdown.ts +71 -0
- package/src/models/work-unit.ts +38 -0
- package/src/repositories/work-unit.ts +466 -0
- package/src/services/work-unit.ts +131 -0
- package/src/tests/cli/__snapshots__/cli-productivity.test.ts.snap +37 -0
- package/src/tests/cli/__snapshots__/cli.test.ts.snap +20 -0
- package/src/tests/cli/cli-productivity.test.ts +167 -0
- package/src/tests/cli/cli.test.ts +171 -0
- package/src/tests/core/config.test.ts +53 -0
- package/src/tests/core/discovery.test.ts +46 -0
- package/src/tests/database/migrations.test.ts +75 -0
- package/src/tests/repositories/work-unit.test.ts +243 -0
- package/src/tests/services/work-unit.test.ts +87 -0
- package/src/validators/work-unit.ts +63 -0
- package/tsconfig.json +16 -0
- package/tsup.config.ts +50 -0
- package/vitest.config.ts +8 -0
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { getWorkspaceRoot } from '../core/discovery.js';
|
|
5
|
+
import { initLogger, logger } from '../core/logger.js';
|
|
6
|
+
import { connectDatabase, closeDatabase } from '../database/connection.js';
|
|
7
|
+
import { runMigrations } from '../database/migrations.js';
|
|
8
|
+
import { WorkUnitRepository } from '../repositories/work-unit.js';
|
|
9
|
+
import { WorkUnitService } from '../services/work-unit.js';
|
|
10
|
+
import { initCommand } from './commands/init.js';
|
|
11
|
+
import { addCommand } from './commands/add.js';
|
|
12
|
+
import { listCommand } from './commands/list.js';
|
|
13
|
+
import { showCommand } from './commands/show.js';
|
|
14
|
+
import { searchCommand } from './commands/search.js';
|
|
15
|
+
import { todayCommand } from './commands/today.js';
|
|
16
|
+
import { weekCommand } from './commands/week.js';
|
|
17
|
+
import { dashboardCommand } from './commands/dashboard.js';
|
|
18
|
+
import { continueCommand } from './commands/continue.js';
|
|
19
|
+
import { statsCommand } from './commands/stats.js';
|
|
20
|
+
import { exportCommand } from './commands/export.js';
|
|
21
|
+
import { WorkLogDomainError } from '../errors/index.js';
|
|
22
|
+
|
|
23
|
+
const program = new Command();
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.name('worklog')
|
|
27
|
+
.description('WorkLog - Engineering Context Manager')
|
|
28
|
+
.version('0.1.0')
|
|
29
|
+
.option('--verbose', 'enable verbose debug logging')
|
|
30
|
+
.option('--json', 'output in machine-readable JSON format');
|
|
31
|
+
|
|
32
|
+
// Helper to bootstrap database & service
|
|
33
|
+
function bootstrap(): WorkUnitService {
|
|
34
|
+
const root = getWorkspaceRoot();
|
|
35
|
+
initLogger(root);
|
|
36
|
+
const db = connectDatabase(root);
|
|
37
|
+
runMigrations(db);
|
|
38
|
+
const repo = new WorkUnitRepository(db);
|
|
39
|
+
return new WorkUnitService(repo);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Global error handler
|
|
43
|
+
function handleError(error: any) {
|
|
44
|
+
const isVerbose = program.opts().verbose;
|
|
45
|
+
const isJson = program.opts().json;
|
|
46
|
+
|
|
47
|
+
// Database errors represent unexpected system errors rather than user input issues, so they exit with 2
|
|
48
|
+
const isUserError = error instanceof WorkLogDomainError && error.code !== 'DATABASE_ERROR';
|
|
49
|
+
|
|
50
|
+
if (isUserError) {
|
|
51
|
+
logger.error({ error, code: error.code }, 'Domain error occurred');
|
|
52
|
+
if (isJson) {
|
|
53
|
+
console.error(JSON.stringify({ success: false, code: error.code, error: error.message }));
|
|
54
|
+
} else {
|
|
55
|
+
console.error(pc.red(`Error: ${error.message}`));
|
|
56
|
+
if (error.actionRequired) {
|
|
57
|
+
console.error(pc.yellow(`Action required: ${error.actionRequired}`));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
process.exit(1);
|
|
61
|
+
} else {
|
|
62
|
+
logger.error({ error }, 'Unexpected system error');
|
|
63
|
+
if (isJson) {
|
|
64
|
+
console.error(
|
|
65
|
+
JSON.stringify({ success: false, code: 'INTERNAL_ERROR', error: error.message }),
|
|
66
|
+
);
|
|
67
|
+
} else {
|
|
68
|
+
console.error(pc.red(`Unexpected Error: ${error.message}`));
|
|
69
|
+
if (isVerbose) {
|
|
70
|
+
console.error(pc.gray(error.stack || ''));
|
|
71
|
+
} else {
|
|
72
|
+
console.error(pc.yellow('Run with --verbose to view stack traces.'));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Commands definition
|
|
80
|
+
program
|
|
81
|
+
.command('init')
|
|
82
|
+
.alias('i')
|
|
83
|
+
.description('Initialize a new WorkLog repository')
|
|
84
|
+
.option('--name <name>', 'override default project name')
|
|
85
|
+
.action((options) => {
|
|
86
|
+
try {
|
|
87
|
+
const json = program.opts().json || options.json;
|
|
88
|
+
initCommand({ ...options, json });
|
|
89
|
+
} catch (error: any) {
|
|
90
|
+
handleError(error);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command('add')
|
|
96
|
+
.alias('a')
|
|
97
|
+
.description('Create a new Work Unit')
|
|
98
|
+
.option('-t, --title <title>', 'title of the work unit')
|
|
99
|
+
.option(
|
|
100
|
+
'--type <type>',
|
|
101
|
+
'type of the work unit (Feature, Bug, Spike, Refactor, Improvement, Research, Decision, Blocker, Review, Idea)',
|
|
102
|
+
)
|
|
103
|
+
.option(
|
|
104
|
+
'--status <status>',
|
|
105
|
+
'status of the work unit (Planned, In Progress, Blocked, Completed, Cancelled)',
|
|
106
|
+
)
|
|
107
|
+
.option('--priority <priority>', 'priority of the work unit (Low, Medium, High)')
|
|
108
|
+
.option('--module <module>', 'associated component or code module')
|
|
109
|
+
.option('--tags <tags>', 'comma-separated tags list')
|
|
110
|
+
.option('-d, --description <desc>', 'detailed description')
|
|
111
|
+
.option('-n, --next <next>', 'next step/action')
|
|
112
|
+
.option('--notes <notes>', 'implementation notes')
|
|
113
|
+
.action(async (options) => {
|
|
114
|
+
try {
|
|
115
|
+
const service = bootstrap();
|
|
116
|
+
const json = program.opts().json || options.json;
|
|
117
|
+
await addCommand(service, { ...options, json });
|
|
118
|
+
} catch (error: any) {
|
|
119
|
+
handleError(error);
|
|
120
|
+
} finally {
|
|
121
|
+
closeDatabase();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
program
|
|
126
|
+
.command('list')
|
|
127
|
+
.alias('ls')
|
|
128
|
+
.description('List recent Work Units')
|
|
129
|
+
.option('-l, --limit <limit>', 'limit output count', '20')
|
|
130
|
+
.option('-s, --status <status>', 'filter by status')
|
|
131
|
+
.option('-t, --type <type>', 'filter by type')
|
|
132
|
+
.action((options) => {
|
|
133
|
+
try {
|
|
134
|
+
const service = bootstrap();
|
|
135
|
+
const json = program.opts().json || options.json;
|
|
136
|
+
listCommand(service, { ...options, json });
|
|
137
|
+
} catch (error: any) {
|
|
138
|
+
handleError(error);
|
|
139
|
+
} finally {
|
|
140
|
+
closeDatabase();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
program
|
|
145
|
+
.command('show <id>')
|
|
146
|
+
.alias('sh')
|
|
147
|
+
.description('Show details for a specific Work Unit')
|
|
148
|
+
.action((id, options) => {
|
|
149
|
+
try {
|
|
150
|
+
const service = bootstrap();
|
|
151
|
+
const json = program.opts().json || options.json;
|
|
152
|
+
showCommand(service, id, { ...options, json });
|
|
153
|
+
} catch (error: any) {
|
|
154
|
+
handleError(error);
|
|
155
|
+
} finally {
|
|
156
|
+
closeDatabase();
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
program
|
|
161
|
+
.command('search [query]')
|
|
162
|
+
.alias('s')
|
|
163
|
+
.description('Search Work Units by keyword and category filters')
|
|
164
|
+
.option('--tag <tag>', 'filter search results by tag')
|
|
165
|
+
.option('--module <module>', 'filter search results by module')
|
|
166
|
+
.option('--status <status>', 'filter search results by status')
|
|
167
|
+
.option('--type <type>', 'filter search results by type')
|
|
168
|
+
.action((query, options) => {
|
|
169
|
+
try {
|
|
170
|
+
const service = bootstrap();
|
|
171
|
+
const json = program.opts().json || options.json;
|
|
172
|
+
searchCommand(service, query, { ...options, json });
|
|
173
|
+
} catch (error: any) {
|
|
174
|
+
handleError(error);
|
|
175
|
+
} finally {
|
|
176
|
+
closeDatabase();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
program
|
|
181
|
+
.command('today')
|
|
182
|
+
.alias('t')
|
|
183
|
+
.description('Show work units updated today')
|
|
184
|
+
.action((options) => {
|
|
185
|
+
try {
|
|
186
|
+
const service = bootstrap();
|
|
187
|
+
const json = program.opts().json || options.json;
|
|
188
|
+
todayCommand(service, { ...options, json });
|
|
189
|
+
} catch (error: any) {
|
|
190
|
+
handleError(error);
|
|
191
|
+
} finally {
|
|
192
|
+
closeDatabase();
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
program
|
|
197
|
+
.command('week')
|
|
198
|
+
.alias('w')
|
|
199
|
+
.description('Show weekly engineering summary')
|
|
200
|
+
.action((options) => {
|
|
201
|
+
try {
|
|
202
|
+
const service = bootstrap();
|
|
203
|
+
const json = program.opts().json || options.json;
|
|
204
|
+
weekCommand(service, { ...options, json });
|
|
205
|
+
} catch (error: any) {
|
|
206
|
+
handleError(error);
|
|
207
|
+
} finally {
|
|
208
|
+
closeDatabase();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
program
|
|
213
|
+
.command('dashboard')
|
|
214
|
+
.alias('db')
|
|
215
|
+
.description('Show clean project dashboard overview')
|
|
216
|
+
.action((options) => {
|
|
217
|
+
try {
|
|
218
|
+
const service = bootstrap();
|
|
219
|
+
const json = program.opts().json || options.json;
|
|
220
|
+
dashboardCommand(service, { ...options, json });
|
|
221
|
+
} catch (error: any) {
|
|
222
|
+
handleError(error);
|
|
223
|
+
} finally {
|
|
224
|
+
closeDatabase();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
program
|
|
229
|
+
.command('continue')
|
|
230
|
+
.alias('co')
|
|
231
|
+
.description('Resume work on the most recent unfinished task')
|
|
232
|
+
.action(async (options) => {
|
|
233
|
+
try {
|
|
234
|
+
const service = bootstrap();
|
|
235
|
+
const json = program.opts().json || options.json;
|
|
236
|
+
await continueCommand(service, { ...options, json });
|
|
237
|
+
} catch (error: any) {
|
|
238
|
+
handleError(error);
|
|
239
|
+
} finally {
|
|
240
|
+
closeDatabase();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
program
|
|
245
|
+
.command('stats')
|
|
246
|
+
.alias('st')
|
|
247
|
+
.description('Show total stats and category distributions')
|
|
248
|
+
.action((options) => {
|
|
249
|
+
try {
|
|
250
|
+
const service = bootstrap();
|
|
251
|
+
const json = program.opts().json || options.json;
|
|
252
|
+
statsCommand(service, { ...options, json });
|
|
253
|
+
} catch (error: any) {
|
|
254
|
+
handleError(error);
|
|
255
|
+
} finally {
|
|
256
|
+
closeDatabase();
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
program
|
|
261
|
+
.command('export')
|
|
262
|
+
.alias('ex')
|
|
263
|
+
.description('Export Work Units to Markdown, CSV, or JSON')
|
|
264
|
+
.option('-f, --format <format>', 'export format (markdown, csv, json)', 'markdown')
|
|
265
|
+
.option('-o, --output <file>', 'write output directly to a file')
|
|
266
|
+
.option('--id <id>', 'export a single work unit by its ID')
|
|
267
|
+
.action((options) => {
|
|
268
|
+
try {
|
|
269
|
+
const service = bootstrap();
|
|
270
|
+
exportCommand(service, options);
|
|
271
|
+
} catch (error: any) {
|
|
272
|
+
handleError(error);
|
|
273
|
+
} finally {
|
|
274
|
+
closeDatabase();
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Handle invalid / unknown command routing
|
|
279
|
+
program.on('command:*', () => {
|
|
280
|
+
const isJson = program.opts().json;
|
|
281
|
+
const command = program.args.join(' ');
|
|
282
|
+
|
|
283
|
+
if (isJson) {
|
|
284
|
+
console.error(
|
|
285
|
+
JSON.stringify({ success: false, code: 'UNKNOWN_COMMAND', error: `Unknown command: ${command}` }),
|
|
286
|
+
);
|
|
287
|
+
} else {
|
|
288
|
+
console.error(pc.red(`Error: Unknown command: ${command}`));
|
|
289
|
+
console.error(`Run 'worklog --help' for a list of available commands.`);
|
|
290
|
+
}
|
|
291
|
+
process.exit(1);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
program.parse(process.argv);
|
package/src/cli/ui.ts
ADDED
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import Table from 'cli-table3';
|
|
3
|
+
import { WorkUnit, WorkUnitStatus, WorkUnitType, WorkUnitPriority } from '../models/work-unit.js';
|
|
4
|
+
import { WorkUnitStats } from '../repositories/work-unit.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the width of the terminal, defaulting to 80 columns.
|
|
8
|
+
*/
|
|
9
|
+
export function getTerminalWidth(): number {
|
|
10
|
+
return process.stdout.columns || 80;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Truncates text and removes newlines, appending an ellipsis if exceeded.
|
|
15
|
+
*/
|
|
16
|
+
export function truncateText(text: string | null | undefined, maxLength: number): string {
|
|
17
|
+
if (!text) return '';
|
|
18
|
+
const clean = text.replace(/\r?\n|\r/g, ' ').trim();
|
|
19
|
+
if (clean.length <= maxLength) return clean;
|
|
20
|
+
return clean.substring(0, maxLength - 3) + '...';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maps a WorkUnitStatus to a colored terminal string.
|
|
25
|
+
*/
|
|
26
|
+
export function colorStatus(status: WorkUnitStatus): string {
|
|
27
|
+
switch (status) {
|
|
28
|
+
case 'Planned':
|
|
29
|
+
return pc.gray('Planned');
|
|
30
|
+
case 'In Progress':
|
|
31
|
+
return pc.blue('In Progress');
|
|
32
|
+
case 'Blocked':
|
|
33
|
+
return pc.red('Blocked');
|
|
34
|
+
case 'Completed':
|
|
35
|
+
return pc.green('Completed');
|
|
36
|
+
case 'Cancelled':
|
|
37
|
+
return pc.strikethrough(pc.gray('Cancelled'));
|
|
38
|
+
default:
|
|
39
|
+
return status;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Maps a WorkUnitType to a colored terminal string.
|
|
45
|
+
*/
|
|
46
|
+
export function colorType(type: WorkUnitType): string {
|
|
47
|
+
switch (type) {
|
|
48
|
+
case 'Feature':
|
|
49
|
+
return pc.cyan('Feature');
|
|
50
|
+
case 'Bug':
|
|
51
|
+
return pc.red('Bug');
|
|
52
|
+
case 'Spike':
|
|
53
|
+
return pc.magenta('Spike');
|
|
54
|
+
case 'Refactor':
|
|
55
|
+
return pc.yellow('Refactor');
|
|
56
|
+
case 'Improvement':
|
|
57
|
+
return pc.green('Improvement');
|
|
58
|
+
case 'Research':
|
|
59
|
+
return pc.blue('Research');
|
|
60
|
+
case 'Decision':
|
|
61
|
+
return pc.cyan(pc.bold('Decision'));
|
|
62
|
+
case 'Blocker':
|
|
63
|
+
return pc.bgRed(pc.white('Blocker'));
|
|
64
|
+
case 'Review':
|
|
65
|
+
return pc.yellow('Review');
|
|
66
|
+
case 'Idea':
|
|
67
|
+
return pc.gray('Idea');
|
|
68
|
+
default:
|
|
69
|
+
return type;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Maps a WorkUnitPriority to a colored terminal string.
|
|
75
|
+
*/
|
|
76
|
+
export function colorPriority(priority: WorkUnitPriority | null): string {
|
|
77
|
+
if (!priority) return pc.gray('-');
|
|
78
|
+
switch (priority) {
|
|
79
|
+
case 'High':
|
|
80
|
+
return pc.red('High');
|
|
81
|
+
case 'Medium':
|
|
82
|
+
return pc.yellow('Medium');
|
|
83
|
+
case 'Low':
|
|
84
|
+
return pc.green('Low');
|
|
85
|
+
default:
|
|
86
|
+
return priority;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Formats a list of work units as a minimal, git-like text table.
|
|
92
|
+
*/
|
|
93
|
+
export function formatTable(workUnits: WorkUnit[]): string {
|
|
94
|
+
if (workUnits.length === 0) {
|
|
95
|
+
return 'No work units found.';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const table = new Table({
|
|
99
|
+
head: [
|
|
100
|
+
pc.bold('ID'),
|
|
101
|
+
pc.bold('Title'),
|
|
102
|
+
pc.bold('Type'),
|
|
103
|
+
pc.bold('Status'),
|
|
104
|
+
pc.bold('Module'),
|
|
105
|
+
pc.bold('Priority'),
|
|
106
|
+
pc.bold('Created At'),
|
|
107
|
+
],
|
|
108
|
+
chars: {
|
|
109
|
+
top: '',
|
|
110
|
+
'top-mid': '',
|
|
111
|
+
'top-left': '',
|
|
112
|
+
'top-right': '',
|
|
113
|
+
bottom: '',
|
|
114
|
+
'bottom-mid': '',
|
|
115
|
+
'bottom-left': '',
|
|
116
|
+
'bottom-right': '',
|
|
117
|
+
left: '',
|
|
118
|
+
'left-mid': '',
|
|
119
|
+
mid: '',
|
|
120
|
+
'mid-mid': '',
|
|
121
|
+
right: '',
|
|
122
|
+
'right-mid': '',
|
|
123
|
+
middle: ' ',
|
|
124
|
+
},
|
|
125
|
+
style: { 'padding-left': 0, 'padding-right': 2, head: [] },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const width = getTerminalWidth();
|
|
129
|
+
// Reserve width for: ID(5) Type(14) Status(14) Module(12) Priority(10) Date(8) + spacing(12) = 75
|
|
130
|
+
const titleMaxWidth = Math.max(20, width - 75);
|
|
131
|
+
|
|
132
|
+
for (const unit of workUnits) {
|
|
133
|
+
const formattedDate = new Date(unit.createdAt).toLocaleDateString(undefined, {
|
|
134
|
+
month: 'short',
|
|
135
|
+
day: 'numeric',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
table.push([
|
|
139
|
+
pc.gray(`#${unit.id}`),
|
|
140
|
+
truncateText(unit.title, titleMaxWidth),
|
|
141
|
+
colorType(unit.type),
|
|
142
|
+
colorStatus(unit.status),
|
|
143
|
+
unit.module ? pc.yellow(unit.module) : pc.gray('-'),
|
|
144
|
+
colorPriority(unit.priority),
|
|
145
|
+
pc.gray(formattedDate),
|
|
146
|
+
]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return table.toString();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Formats a single WorkUnit as a detail card.
|
|
154
|
+
*/
|
|
155
|
+
export function formatWorkUnitDetail(unit: WorkUnit): string {
|
|
156
|
+
const lines: string[] = [];
|
|
157
|
+
|
|
158
|
+
lines.push(`${pc.bold(pc.cyan(`[#${unit.id}] ${unit.title}`))}`);
|
|
159
|
+
lines.push(`${pc.gray('--------------------------------------------------')}`);
|
|
160
|
+
lines.push(`${pc.bold('Type:')} ${colorType(unit.type)}`);
|
|
161
|
+
lines.push(`${pc.bold('Status:')} ${colorStatus(unit.status)}`);
|
|
162
|
+
|
|
163
|
+
if (unit.priority) {
|
|
164
|
+
lines.push(`${pc.bold('Priority:')} ${colorPriority(unit.priority)}`);
|
|
165
|
+
}
|
|
166
|
+
if (unit.module) {
|
|
167
|
+
lines.push(`${pc.bold('Module:')} ${pc.yellow(unit.module)}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (unit.tags && unit.tags.length > 0) {
|
|
171
|
+
lines.push(`${pc.bold('Tags:')} ${unit.tags.map((t) => pc.blue(t)).join(', ')}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (unit.relatedWorkUnits && unit.relatedWorkUnits.length > 0) {
|
|
175
|
+
lines.push(
|
|
176
|
+
`${pc.bold('Related:')} ${unit.relatedWorkUnits.map((id) => pc.gray(`#${id}`)).join(', ')}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
lines.push(`${pc.bold('Created:')} ${new Date(unit.createdAt).toLocaleString()}`);
|
|
181
|
+
lines.push(`${pc.bold('Updated:')} ${new Date(unit.updatedAt).toLocaleString()}`);
|
|
182
|
+
|
|
183
|
+
if (unit.description) {
|
|
184
|
+
lines.push('');
|
|
185
|
+
lines.push(`${pc.bold('Description:')}`);
|
|
186
|
+
lines.push(unit.description);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (unit.nextStep) {
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push(`${pc.bold('Next Step:')}`);
|
|
192
|
+
lines.push(pc.blue(`-> ${unit.nextStep}`));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (unit.decision) {
|
|
196
|
+
lines.push('');
|
|
197
|
+
lines.push(`${pc.bold('Decision:')}`);
|
|
198
|
+
lines.push(pc.green(`✔ ${unit.decision}`));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (unit.blocker) {
|
|
202
|
+
lines.push('');
|
|
203
|
+
lines.push(`${pc.bold('Blocker:')}`);
|
|
204
|
+
lines.push(pc.red(`✖ ${unit.blocker}`));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (unit.notes) {
|
|
208
|
+
lines.push('');
|
|
209
|
+
lines.push(`${pc.bold('Notes:')}`);
|
|
210
|
+
lines.push(unit.notes);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Formats a dashboard overview screen showing current tasks, blocked work, and recent updates.
|
|
218
|
+
*/
|
|
219
|
+
export function formatDashboard(
|
|
220
|
+
inProgress: WorkUnit[],
|
|
221
|
+
recentlyUpdated: WorkUnit[],
|
|
222
|
+
needsAttention: WorkUnit[],
|
|
223
|
+
): string {
|
|
224
|
+
const lines: string[] = [];
|
|
225
|
+
|
|
226
|
+
lines.push(pc.bold(pc.cyan('=== WORKLOG DASHBOARD ===\n')));
|
|
227
|
+
|
|
228
|
+
lines.push(pc.bold('CURRENTLY IN PROGRESS:'));
|
|
229
|
+
if (inProgress.length === 0) {
|
|
230
|
+
lines.push(pc.gray(' (No tasks currently in progress)'));
|
|
231
|
+
} else {
|
|
232
|
+
for (const u of inProgress) {
|
|
233
|
+
lines.push(
|
|
234
|
+
` ${pc.gray(`#${u.id}`)} ${pc.bold(u.title)}${u.module ? ` [${u.module}]` : ''}`,
|
|
235
|
+
);
|
|
236
|
+
if (u.nextStep) {
|
|
237
|
+
lines.push(` ${pc.blue('->')} ${pc.gray(u.nextStep)}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
lines.push('');
|
|
242
|
+
|
|
243
|
+
lines.push(pc.bold('NEEDS ATTENTION:'));
|
|
244
|
+
if (needsAttention.length === 0) {
|
|
245
|
+
lines.push(pc.gray(' (No blocked or planned tasks requiring attention)'));
|
|
246
|
+
} else {
|
|
247
|
+
for (const u of needsAttention) {
|
|
248
|
+
const reason =
|
|
249
|
+
u.status === 'Blocked'
|
|
250
|
+
? pc.red(`[Blocked: ${u.blocker || 'Unknown'}]`)
|
|
251
|
+
: pc.yellow('[Planned]');
|
|
252
|
+
lines.push(` ${pc.gray(`#${u.id}`)} ${reason} ${u.title}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
lines.push('');
|
|
256
|
+
|
|
257
|
+
lines.push(pc.bold('RECENTLY UPDATED:'));
|
|
258
|
+
if (recentlyUpdated.length === 0) {
|
|
259
|
+
lines.push(pc.gray(' (No recent updates)'));
|
|
260
|
+
} else {
|
|
261
|
+
for (const u of recentlyUpdated) {
|
|
262
|
+
const date = new Date(u.updatedAt).toLocaleDateString(undefined, {
|
|
263
|
+
month: 'short',
|
|
264
|
+
day: 'numeric',
|
|
265
|
+
hour: '2-digit',
|
|
266
|
+
minute: '2-digit',
|
|
267
|
+
});
|
|
268
|
+
lines.push(
|
|
269
|
+
` ${pc.gray(`#${u.id}`)} ${pc.gray(`[${date}]`)} ${u.title} (${colorStatus(u.status)})`,
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return lines.join('\n');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Formats statistics insights screen using clean text bars.
|
|
279
|
+
*/
|
|
280
|
+
export function formatStats(stats: WorkUnitStats): string {
|
|
281
|
+
const lines: string[] = [];
|
|
282
|
+
|
|
283
|
+
lines.push(pc.bold(pc.cyan('=== ENGINEERING INSIGHTS ===\n')));
|
|
284
|
+
lines.push(`${pc.bold('Total Work Units:')} ${stats.totalCount}\n`);
|
|
285
|
+
|
|
286
|
+
// 1. Status Distribution
|
|
287
|
+
lines.push(pc.bold('STATUS DISTRIBUTION:'));
|
|
288
|
+
for (const item of stats.statusDistribution) {
|
|
289
|
+
lines.push(` ${item.status.padEnd(15)} : ${item.count}`);
|
|
290
|
+
}
|
|
291
|
+
lines.push('');
|
|
292
|
+
|
|
293
|
+
// 2. Type Distribution
|
|
294
|
+
lines.push(pc.bold('TYPE DISTRIBUTION:'));
|
|
295
|
+
for (const item of stats.typeDistribution) {
|
|
296
|
+
lines.push(` ${item.type.padEnd(15)} : ${item.count}`);
|
|
297
|
+
}
|
|
298
|
+
lines.push('');
|
|
299
|
+
|
|
300
|
+
// 3. Module Distribution
|
|
301
|
+
lines.push(pc.bold('MODULE DISTRIBUTION:'));
|
|
302
|
+
if (stats.moduleDistribution.length === 0) {
|
|
303
|
+
lines.push(' No modules tracked.');
|
|
304
|
+
} else {
|
|
305
|
+
for (const item of stats.moduleDistribution) {
|
|
306
|
+
lines.push(` ${item.module.padEnd(15)} : ${item.count}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
lines.push('');
|
|
310
|
+
|
|
311
|
+
// 4. Daily Activity over past 7 days
|
|
312
|
+
lines.push(pc.bold('ACTIVITY IN THE PAST 7 DAYS:'));
|
|
313
|
+
if (stats.dailyActivity.length === 0) {
|
|
314
|
+
lines.push(' No work logged in the past 7 days.');
|
|
315
|
+
} else {
|
|
316
|
+
for (const item of stats.dailyActivity) {
|
|
317
|
+
const bar = '*'.repeat(item.count);
|
|
318
|
+
lines.push(` ${item.day} : ${bar} (${item.count})`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return lines.join('\n');
|
|
323
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { ConfigError } from '../errors/index.js';
|
|
5
|
+
|
|
6
|
+
export const ConfigSchema = z.object({
|
|
7
|
+
projectName: z.string(),
|
|
8
|
+
createdAt: z.string(),
|
|
9
|
+
schemaVersion: z.number().default(1),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
13
|
+
|
|
14
|
+
const DEFAULT_CONFIG_FILENAME = 'config.json';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Loads and validates the configuration file from a workspace root.
|
|
18
|
+
*/
|
|
19
|
+
export function loadConfig(workspaceRoot: string): Config {
|
|
20
|
+
const configPath = path.join(workspaceRoot, '.worklog', DEFAULT_CONFIG_FILENAME);
|
|
21
|
+
if (!fs.existsSync(configPath)) {
|
|
22
|
+
throw new ConfigError(`Config file not found at ${configPath}.`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(configPath, 'utf8');
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
return ConfigSchema.parse(parsed);
|
|
29
|
+
} catch (error: any) {
|
|
30
|
+
if (error instanceof z.ZodError) {
|
|
31
|
+
throw new ConfigError(
|
|
32
|
+
`Invalid config format: ${error.issues.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
throw new ConfigError(`Failed to read config file: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Saves and validates a configuration object to a workspace root.
|
|
41
|
+
*/
|
|
42
|
+
export function saveConfig(workspaceRoot: string, config: Config): void {
|
|
43
|
+
const dotWorklog = path.join(workspaceRoot, '.worklog');
|
|
44
|
+
if (!fs.existsSync(dotWorklog)) {
|
|
45
|
+
fs.mkdirSync(dotWorklog, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const configPath = path.join(dotWorklog, DEFAULT_CONFIG_FILENAME);
|
|
49
|
+
try {
|
|
50
|
+
const validated = ConfigSchema.parse(config);
|
|
51
|
+
fs.writeFileSync(configPath, JSON.stringify(validated, null, 2), 'utf8');
|
|
52
|
+
} catch (error: any) {
|
|
53
|
+
throw new ConfigError(`Failed to save config: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Initializes a new configuration object in the workspace root.
|
|
59
|
+
*/
|
|
60
|
+
export function initConfig(workspaceRoot: string, projectName?: string): Config {
|
|
61
|
+
const pName = projectName || path.basename(path.resolve(workspaceRoot));
|
|
62
|
+
const newConfig: Config = {
|
|
63
|
+
projectName: pName,
|
|
64
|
+
createdAt: new Date().toISOString(),
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
};
|
|
67
|
+
saveConfig(workspaceRoot, newConfig);
|
|
68
|
+
return newConfig;
|
|
69
|
+
}
|