@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,38 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { NotInitializedError } from '../errors/index.js';
4
+
5
+ /**
6
+ * Searches upward from the starting directory to find the `.worklog` directory.
7
+ * Returns the absolute path of the directory containing `.worklog`.
8
+ * Returns null if not found.
9
+ */
10
+ export function findWorkspaceRoot(startDir: string = process.cwd()): string | null {
11
+ let current = path.resolve(startDir);
12
+ const rootPath = path.parse(current).root;
13
+
14
+ while (true) {
15
+ const candidate = path.join(current, '.worklog');
16
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
17
+ return current;
18
+ }
19
+
20
+ if (current === rootPath) {
21
+ break;
22
+ }
23
+ current = path.dirname(current);
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * Finds the workspace root or throws a NotInitializedError.
31
+ */
32
+ export function getWorkspaceRoot(startDir: string = process.cwd()): string {
33
+ const root = findWorkspaceRoot(startDir);
34
+ if (!root) {
35
+ throw new NotInitializedError();
36
+ }
37
+ return root;
38
+ }
@@ -0,0 +1,53 @@
1
+ import pino from 'pino';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+
5
+ let loggerInstance: pino.Logger | null = null;
6
+
7
+ /**
8
+ * Returns the current logger instance or a silent fallback if not initialized.
9
+ */
10
+ export function getLogger(): pino.Logger {
11
+ if (loggerInstance) {
12
+ return loggerInstance;
13
+ }
14
+
15
+ // Silent fallback before repository discovery
16
+ loggerInstance = pino({
17
+ level: 'silent',
18
+ });
19
+ return loggerInstance;
20
+ }
21
+
22
+ /**
23
+ * Initializes the logger to write to the `.worklog/worklog.log` file in the workspace.
24
+ */
25
+ export function initLogger(workspaceRoot: string): pino.Logger {
26
+ const logDir = path.join(workspaceRoot, '.worklog');
27
+ if (!fs.existsSync(logDir)) {
28
+ fs.mkdirSync(logDir, { recursive: true });
29
+ }
30
+
31
+ const logFile = path.join(logDir, 'worklog.log');
32
+
33
+ loggerInstance = pino(
34
+ {
35
+ level: process.env.WORKLOG_LOG_LEVEL || 'info',
36
+ base: undefined, // remove pid/hostname for clean local logs
37
+ timestamp: () => `,"time":"${new Date().toISOString()}"`,
38
+ },
39
+ pino.destination({
40
+ dest: logFile,
41
+ sync: true, // synchronous write to guarantee flush before CLI exit
42
+ }),
43
+ );
44
+
45
+ return loggerInstance;
46
+ }
47
+
48
+ export const logger = {
49
+ info: (obj: any, msg?: string, ...args: any[]) => getLogger().info(obj, msg, ...args),
50
+ error: (obj: any, msg?: string, ...args: any[]) => getLogger().error(obj, msg, ...args),
51
+ debug: (obj: any, msg?: string, ...args: any[]) => getLogger().debug(obj, msg, ...args),
52
+ warn: (obj: any, msg?: string, ...args: any[]) => getLogger().warn(obj, msg, ...args),
53
+ };
@@ -0,0 +1,95 @@
1
+ import { createRequire } from 'module';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { DatabaseError } from '../errors/index.js';
5
+ import { logger } from '../core/logger.js';
6
+ import type DatabaseType from 'better-sqlite3';
7
+
8
+ // Construct dynamic disk require to bypass Node SEA embedded require constraints
9
+ let DatabaseConstructor: any;
10
+ try {
11
+ const contextPath =
12
+ typeof import.meta !== 'undefined' && import.meta.url
13
+ ? import.meta.url
14
+ : typeof __filename !== 'undefined'
15
+ ? __filename
16
+ : process.cwd() + '/package.json';
17
+
18
+ const requireFromDisk = createRequire(contextPath);
19
+ DatabaseConstructor = requireFromDisk('better-sqlite3');
20
+ } catch {
21
+ try {
22
+ // Fallback: resolve from the current working directory
23
+ const requireFromDisk = createRequire(process.cwd() + '/package.json');
24
+ DatabaseConstructor = requireFromDisk('better-sqlite3');
25
+ } catch {
26
+ // Fallback: resolve from the directory of the executable
27
+ const execDir = path.dirname(process.execPath);
28
+ const requireFromDisk = createRequire(execDir + '/package.json');
29
+ DatabaseConstructor = requireFromDisk('better-sqlite3');
30
+ }
31
+ }
32
+
33
+ let dbInstance: DatabaseType.Database | null = null;
34
+
35
+ /**
36
+ * Connects to the SQLite database located at `<workspaceRoot>/.worklog/worklog.db`.
37
+ * Optimizes SQLite performance parameters for local CLI use.
38
+ */
39
+ export function connectDatabase(workspaceRoot: string): DatabaseType.Database {
40
+ if (dbInstance) {
41
+ return dbInstance;
42
+ }
43
+
44
+ const dbDir = path.join(workspaceRoot, '.worklog');
45
+ if (!fs.existsSync(dbDir)) {
46
+ fs.mkdirSync(dbDir, { recursive: true });
47
+ }
48
+
49
+ const dbPath = path.join(dbDir, 'worklog.db');
50
+
51
+ try {
52
+ const db = new DatabaseConstructor(dbPath);
53
+
54
+ // Performance optimizations for local development CLI
55
+ db.pragma('foreign_keys = ON');
56
+ db.pragma('journal_mode = WAL');
57
+ db.pragma('synchronous = NORMAL');
58
+ db.pragma('temp_store = MEMORY');
59
+
60
+ dbInstance = db;
61
+ logger.debug({ dbPath }, 'Connected to SQLite database successfully');
62
+ return db;
63
+ } catch (error: any) {
64
+ logger.error({ error, dbPath }, 'Failed to connect to SQLite database');
65
+ throw new DatabaseError('Failed to connect to database', error.message);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Returns the currently active database instance.
71
+ * Throws if the database has not been initialized.
72
+ */
73
+ export function getDatabase(): DatabaseType.Database {
74
+ const db = dbInstance;
75
+ if (!db) {
76
+ throw new DatabaseError('Database is not initialized. Call connectDatabase first.');
77
+ }
78
+ return db;
79
+ }
80
+
81
+ /**
82
+ * Closes the database connection cleanly.
83
+ */
84
+ export function closeDatabase(): void {
85
+ if (dbInstance) {
86
+ try {
87
+ dbInstance.close();
88
+ logger.debug('Database connection closed');
89
+ } catch (error: any) {
90
+ logger.error({ error }, 'Error closing database connection');
91
+ } finally {
92
+ dbInstance = null;
93
+ }
94
+ }
95
+ }
@@ -0,0 +1,71 @@
1
+ export const MIGRATIONS: Record<string, string> = {
2
+ '001_initial.sql': `
3
+ CREATE TABLE IF NOT EXISTS work_units (
4
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
5
+ title TEXT NOT NULL,
6
+ type TEXT NOT NULL,
7
+ status TEXT NOT NULL,
8
+ priority TEXT,
9
+ module TEXT,
10
+ description TEXT,
11
+ next_step TEXT,
12
+ decision TEXT,
13
+ blocker TEXT,
14
+ notes TEXT,
15
+ created_at TEXT NOT NULL,
16
+ updated_at TEXT NOT NULL
17
+ );
18
+
19
+ -- Full Text Search Index Table
20
+ CREATE VIRTUAL TABLE IF NOT EXISTS work_units_fts USING fts5(
21
+ title,
22
+ description,
23
+ decision,
24
+ blocker,
25
+ notes,
26
+ content=work_units,
27
+ content_rowid=id
28
+ );
29
+
30
+ -- Auto-sync Insert Trigger
31
+ CREATE TRIGGER IF NOT EXISTS work_units_ai AFTER INSERT ON work_units BEGIN
32
+ INSERT INTO work_units_fts(rowid, title, description, decision, blocker, notes)
33
+ VALUES (new.id, new.title, new.description, new.decision, new.blocker, new.notes);
34
+ END;
35
+
36
+ -- Auto-sync Delete Trigger
37
+ CREATE TRIGGER IF NOT EXISTS work_units_ad AFTER DELETE ON work_units BEGIN
38
+ INSERT INTO work_units_fts(work_units_fts, rowid, title, description, decision, blocker, notes)
39
+ VALUES('delete', old.id, old.title, old.description, old.decision, old.blocker, old.notes);
40
+ END;
41
+
42
+ -- Auto-sync Update Trigger
43
+ CREATE TRIGGER IF NOT EXISTS work_units_au AFTER UPDATE ON work_units BEGIN
44
+ INSERT INTO work_units_fts(work_units_fts, rowid, title, description, decision, blocker, notes)
45
+ VALUES('delete', old.id, old.title, old.description, old.decision, old.blocker, old.notes);
46
+ INSERT INTO work_units_fts(rowid, title, description, decision, blocker, notes)
47
+ VALUES (new.id, new.title, new.description, new.decision, new.blocker, new.notes);
48
+ END;
49
+ `,
50
+ '002_relational_tables.sql': `
51
+ -- Junction table for tags
52
+ CREATE TABLE IF NOT EXISTS work_unit_tags (
53
+ work_unit_id INTEGER NOT NULL,
54
+ tag TEXT NOT NULL,
55
+ PRIMARY KEY (work_unit_id, tag),
56
+ FOREIGN KEY (work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE
57
+ );
58
+
59
+ -- Index for tag filtering performance
60
+ CREATE INDEX IF NOT EXISTS idx_work_unit_tags_tag ON work_unit_tags(tag);
61
+
62
+ -- Junction table for bidirectional relation mapping
63
+ CREATE TABLE IF NOT EXISTS work_unit_relations (
64
+ work_unit_id INTEGER NOT NULL,
65
+ related_work_unit_id INTEGER NOT NULL,
66
+ PRIMARY KEY (work_unit_id, related_work_unit_id),
67
+ FOREIGN KEY (work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE,
68
+ FOREIGN KEY (related_work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE
69
+ );
70
+ `,
71
+ };
@@ -0,0 +1,60 @@
1
+ -- Table to track database migrations
2
+ CREATE TABLE IF NOT EXISTS schema_migrations (
3
+ version TEXT PRIMARY KEY,
4
+ applied_at TEXT NOT NULL
5
+ );
6
+
7
+ -- Core work_units table
8
+ CREATE TABLE IF NOT EXISTS work_units (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ title TEXT NOT NULL,
11
+ type TEXT NOT NULL,
12
+ status TEXT NOT NULL,
13
+ priority TEXT,
14
+ module TEXT,
15
+ description TEXT,
16
+ next_step TEXT,
17
+ decision TEXT,
18
+ blocker TEXT,
19
+ tags TEXT, -- JSON array of strings
20
+ related_work_units TEXT, -- JSON array of integers
21
+ notes TEXT,
22
+ created_at TEXT NOT NULL,
23
+ updated_at TEXT NOT NULL
24
+ );
25
+
26
+ -- Performance indexes for status, type, and dates
27
+ CREATE INDEX IF NOT EXISTS idx_work_units_status ON work_units(status);
28
+ CREATE INDEX IF NOT EXISTS idx_work_units_type ON work_units(type);
29
+ CREATE INDEX IF NOT EXISTS idx_work_units_created_at ON work_units(created_at);
30
+
31
+ -- SQLite FTS5 Search Index Table using work_units as external content
32
+ CREATE VIRTUAL TABLE IF NOT EXISTS work_units_fts USING fts5(
33
+ title,
34
+ description,
35
+ decision,
36
+ blocker,
37
+ notes,
38
+ content='work_units',
39
+ content_rowid='id'
40
+ );
41
+
42
+ -- Trigger to sync insert events to FTS
43
+ CREATE TRIGGER IF NOT EXISTS work_units_ai AFTER INSERT ON work_units BEGIN
44
+ INSERT INTO work_units_fts(rowid, title, description, decision, blocker, notes)
45
+ VALUES (new.id, new.title, new.description, new.decision, new.blocker, new.notes);
46
+ END;
47
+
48
+ -- Trigger to sync delete events to FTS
49
+ CREATE TRIGGER IF NOT EXISTS work_units_ad AFTER DELETE ON work_units BEGIN
50
+ INSERT INTO work_units_fts(work_units_fts, rowid, title, description, decision, blocker, notes)
51
+ VALUES('delete', old.id, old.title, old.description, old.decision, old.blocker, old.notes);
52
+ END;
53
+
54
+ -- Trigger to sync update events to FTS
55
+ CREATE TRIGGER IF NOT EXISTS work_units_au AFTER UPDATE ON work_units BEGIN
56
+ INSERT INTO work_units_fts(work_units_fts, rowid, title, description, decision, blocker, notes)
57
+ VALUES('delete', old.id, old.title, old.description, old.decision, old.blocker, old.notes);
58
+ INSERT INTO work_units_fts(rowid, title, description, decision, blocker, notes)
59
+ VALUES (new.id, new.title, new.description, new.decision, new.blocker, new.notes);
60
+ END;
@@ -0,0 +1,22 @@
1
+ -- Create work_unit_tags table for indexing and querying tags
2
+ CREATE TABLE IF NOT EXISTS work_unit_tags (
3
+ work_unit_id INTEGER NOT NULL,
4
+ tag TEXT NOT NULL,
5
+ PRIMARY KEY (work_unit_id, tag),
6
+ FOREIGN KEY (work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE
7
+ );
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_work_unit_tags_tag ON work_unit_tags(tag);
10
+
11
+ -- Create work_unit_relations table for modeling links between work units
12
+ CREATE TABLE IF NOT EXISTS work_unit_relations (
13
+ work_unit_id INTEGER NOT NULL,
14
+ related_work_unit_id INTEGER NOT NULL,
15
+ PRIMARY KEY (work_unit_id, related_work_unit_id),
16
+ FOREIGN KEY (work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE,
17
+ FOREIGN KEY (related_work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE
18
+ );
19
+
20
+ -- Drop unused JSON columns from the main work_units table
21
+ ALTER TABLE work_units DROP COLUMN tags;
22
+ ALTER TABLE work_units DROP COLUMN related_work_units;
@@ -0,0 +1,65 @@
1
+ import Database from 'better-sqlite3';
2
+ import { logger } from '../core/logger.js';
3
+ import { DatabaseError } from '../errors/index.js';
4
+ import { MIGRATIONS } from './migration-data.js';
5
+
6
+ /**
7
+ * Runs all pending migrations sequentially within a database transaction.
8
+ */
9
+ export function runMigrations(db: Database.Database): void {
10
+ logger.debug('Starting database migrations run');
11
+
12
+ // Step 1: Ensure the schema_migrations table exists
13
+ db.exec(`
14
+ CREATE TABLE IF NOT EXISTS schema_migrations (
15
+ version TEXT PRIMARY KEY,
16
+ applied_at TEXT NOT NULL
17
+ );
18
+ `);
19
+
20
+ // Step 2: Get sorted migration keys
21
+ const files = Object.keys(MIGRATIONS).sort();
22
+
23
+ if (files.length === 0) {
24
+ logger.debug('No migration definitions found');
25
+ return;
26
+ }
27
+
28
+ // Step 3: Get already applied migrations
29
+ const stmt = db.prepare('SELECT version FROM schema_migrations');
30
+ const applied: string[] = stmt.all().map((row: any) => row.version);
31
+
32
+ // Step 4: Apply outstanding migrations in order
33
+ for (const file of files) {
34
+ if (applied.includes(file)) {
35
+ logger.debug({ migration: file }, 'Migration already applied, skipping');
36
+ continue;
37
+ }
38
+
39
+ logger.info({ migration: file }, `Applying database migration: ${file}`);
40
+ const sql = MIGRATIONS[file];
41
+
42
+ // Run migrations inside a transaction
43
+ const runMigrationTx = db.transaction(() => {
44
+ db.exec(sql);
45
+ const insertStmt = db.prepare(
46
+ 'INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)',
47
+ );
48
+ insertStmt.run(file, new Date().toISOString());
49
+ });
50
+
51
+ try {
52
+ runMigrationTx();
53
+ logger.info({ migration: file }, `Migration ${file} applied successfully`);
54
+ } catch (error: any) {
55
+ logger.error({ error, migration: file }, `Failed to apply migration: ${file}`);
56
+ throw new DatabaseError(`Failed to apply migration ${file}`, error.message);
57
+ }
58
+ }
59
+
60
+ logger.debug('Database migrations completed');
61
+ }
62
+ export function getMigrationsDir(): string {
63
+ // Provided for backward compatibility in tests
64
+ return '';
65
+ }
@@ -0,0 +1,51 @@
1
+ export class WorkLogDomainError extends Error {
2
+ constructor(
3
+ message: string,
4
+ public readonly code: string,
5
+ public readonly actionRequired?: string,
6
+ ) {
7
+ super(message);
8
+ this.name = this.constructor.name;
9
+ Object.setPrototypeOf(this, new.target.prototype);
10
+ }
11
+ }
12
+
13
+ export class NotInitializedError extends WorkLogDomainError {
14
+ constructor() {
15
+ super(
16
+ 'Not a WorkLog repository (or any of the parent directories): .worklog',
17
+ 'NOT_INITIALIZED',
18
+ "Run 'worklog init' or 'worklog a' to initialize a new repository.",
19
+ );
20
+ }
21
+ }
22
+
23
+ export class DatabaseError extends WorkLogDomainError {
24
+ constructor(message: string, details?: string) {
25
+ super(
26
+ `Database error: ${message}${details ? ` (${details})` : ''}`,
27
+ 'DATABASE_ERROR',
28
+ 'Please check your database integrity or log files.',
29
+ );
30
+ }
31
+ }
32
+
33
+ export class ValidationError extends WorkLogDomainError {
34
+ constructor(message: string) {
35
+ super(
36
+ `Validation error: ${message}`,
37
+ 'VALIDATION_ERROR',
38
+ 'Please provide valid inputs conforming to the schema.',
39
+ );
40
+ }
41
+ }
42
+
43
+ export class ConfigError extends WorkLogDomainError {
44
+ constructor(message: string) {
45
+ super(
46
+ `Configuration error: ${message}`,
47
+ 'CONFIG_ERROR',
48
+ 'Please check if .worklog/config.json is well-formed.',
49
+ );
50
+ }
51
+ }
@@ -0,0 +1,59 @@
1
+ import { WorkUnit } from '../models/work-unit.js';
2
+
3
+ function escapeCSV(val: any): string {
4
+ if (val === null || val === undefined) return '';
5
+ const str = String(val);
6
+ if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
7
+ return `"${str.replace(/"/g, '""')}"`;
8
+ }
9
+ return str;
10
+ }
11
+
12
+ /**
13
+ * Converts WorkUnits to a standard CSV string.
14
+ */
15
+ export function exportToCSV(units: WorkUnit | WorkUnit[]): string {
16
+ const list = Array.isArray(units) ? units : [units];
17
+ const headers = [
18
+ 'ID',
19
+ 'Title',
20
+ 'Type',
21
+ 'Status',
22
+ 'Priority',
23
+ 'Module',
24
+ 'Description',
25
+ 'Next Step',
26
+ 'Decision',
27
+ 'Blocker',
28
+ 'Tags',
29
+ 'Related Work Units',
30
+ 'Notes',
31
+ 'Created At',
32
+ 'Updated At',
33
+ ];
34
+
35
+ const lines: string[] = [headers.join(',')];
36
+
37
+ for (const u of list) {
38
+ const row = [
39
+ u.id,
40
+ escapeCSV(u.title),
41
+ escapeCSV(u.type),
42
+ escapeCSV(u.status),
43
+ escapeCSV(u.priority),
44
+ escapeCSV(u.module),
45
+ escapeCSV(u.description),
46
+ escapeCSV(u.nextStep),
47
+ escapeCSV(u.decision),
48
+ escapeCSV(u.blocker),
49
+ escapeCSV(u.tags.join(';')),
50
+ escapeCSV(u.relatedWorkUnits.join(';')),
51
+ escapeCSV(u.notes),
52
+ escapeCSV(u.createdAt),
53
+ escapeCSV(u.updatedAt),
54
+ ];
55
+ lines.push(row.join(','));
56
+ }
57
+
58
+ return lines.join('\n') + '\n';
59
+ }
@@ -0,0 +1,8 @@
1
+ import { WorkUnit } from '../models/work-unit.js';
2
+
3
+ /**
4
+ * Converts WorkUnits to a formatted JSON string.
5
+ */
6
+ export function exportToJSON(units: WorkUnit | WorkUnit[]): string {
7
+ return JSON.stringify(units, null, 2) + '\n';
8
+ }
@@ -0,0 +1,71 @@
1
+ import { WorkUnit } from '../models/work-unit.js';
2
+
3
+ function formatSingleMarkdown(u: WorkUnit): string {
4
+ const lines: string[] = [];
5
+ lines.push(`# [#${u.id}] ${u.title}\n`);
6
+ lines.push(`- **Type:** ${u.type}`);
7
+ lines.push(`- **Status:** ${u.status}`);
8
+ if (u.priority) lines.push(`- **Priority:** ${u.priority}`);
9
+ if (u.module) lines.push(`- **Module:** ${u.module}`);
10
+ if (u.tags && u.tags.length > 0) lines.push(`- **Tags:** ${u.tags.join(', ')}`);
11
+ lines.push(`- **Created:** ${u.createdAt}`);
12
+ lines.push(`- **Updated:** ${u.updatedAt}`);
13
+ lines.push('');
14
+
15
+ if (u.description) {
16
+ lines.push('## Description');
17
+ lines.push(u.description);
18
+ lines.push('');
19
+ }
20
+ if (u.nextStep) {
21
+ lines.push(`## Next Step\n-> ${u.nextStep}\n`);
22
+ }
23
+ if (u.decision) {
24
+ lines.push(`## Decision\n✔ ${u.decision}\n`);
25
+ }
26
+ if (u.blocker) {
27
+ lines.push(`## Blocker\n✖ ${u.blocker}\n`);
28
+ }
29
+ if (u.notes) {
30
+ lines.push('## Notes');
31
+ lines.push(u.notes);
32
+ lines.push('');
33
+ }
34
+ return lines.join('\n');
35
+ }
36
+
37
+ /**
38
+ * Converts WorkUnits to a clean Markdown string.
39
+ */
40
+ export function exportToMarkdown(units: WorkUnit | WorkUnit[]): string {
41
+ if (!units) return 'No work units found.\n';
42
+
43
+ if (!Array.isArray(units)) {
44
+ return formatSingleMarkdown(units);
45
+ }
46
+
47
+ if (units.length === 0) {
48
+ return 'No work units found.\n';
49
+ }
50
+
51
+ const lines: string[] = [];
52
+ lines.push('# WorkLog Export\n');
53
+
54
+ const statuses = ['In Progress', 'Blocked', 'Planned', 'Completed', 'Cancelled'] as const;
55
+ for (const status of statuses) {
56
+ const filtered = units.filter((u) => u.status === status);
57
+ if (filtered.length === 0) continue;
58
+
59
+ lines.push(`## ${status}`);
60
+ for (const u of filtered) {
61
+ const moduleStr = u.module ? ` [${u.module}]` : '';
62
+ lines.push(`- **#${u.id}**: ${u.title}${moduleStr} (${u.type})`);
63
+ if (u.nextStep) {
64
+ lines.push(` - *Next step:* ${u.nextStep}`);
65
+ }
66
+ }
67
+ lines.push('');
68
+ }
69
+
70
+ return lines.join('\n');
71
+ }
@@ -0,0 +1,38 @@
1
+ export type WorkUnitType =
2
+ | 'Feature'
3
+ | 'Bug'
4
+ | 'Spike'
5
+ | 'Refactor'
6
+ | 'Improvement'
7
+ | 'Research'
8
+ | 'Decision'
9
+ | 'Blocker'
10
+ | 'Review'
11
+ | 'Idea';
12
+
13
+ export type WorkUnitStatus =
14
+ | 'Planned'
15
+ | 'In Progress'
16
+ | 'Blocked'
17
+ | 'Completed'
18
+ | 'Cancelled';
19
+
20
+ export type WorkUnitPriority = 'Low' | 'Medium' | 'High';
21
+
22
+ export interface WorkUnit {
23
+ id: number;
24
+ title: string;
25
+ type: WorkUnitType;
26
+ status: WorkUnitStatus;
27
+ priority: WorkUnitPriority | null;
28
+ module: string | null;
29
+ description: string | null;
30
+ nextStep: string | null;
31
+ decision: string | null;
32
+ blocker: string | null;
33
+ tags: string[];
34
+ relatedWorkUnits: number[];
35
+ notes: string | null;
36
+ createdAt: string;
37
+ updatedAt: string;
38
+ }