@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/dist/index.js
ADDED
|
@@ -0,0 +1,1873 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// node_modules/.pnpm/tsup@8.3.5_postcss@8.5.23_supports-color@7.2.0_typescript@5.7.2/node_modules/tsup/assets/esm_shims.js
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var getFilename = () => fileURLToPath(import.meta.url);
|
|
6
|
+
var __filename = /* @__PURE__ */ getFilename();
|
|
7
|
+
|
|
8
|
+
// src/cli/index.ts
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import pc8 from "picocolors";
|
|
11
|
+
|
|
12
|
+
// src/core/discovery.ts
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
|
|
16
|
+
// src/errors/index.ts
|
|
17
|
+
var WorkLogDomainError = class extends Error {
|
|
18
|
+
constructor(message, code, actionRequired) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.actionRequired = actionRequired;
|
|
22
|
+
this.name = this.constructor.name;
|
|
23
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var NotInitializedError = class extends WorkLogDomainError {
|
|
27
|
+
constructor() {
|
|
28
|
+
super(
|
|
29
|
+
"Not a WorkLog repository (or any of the parent directories): .worklog",
|
|
30
|
+
"NOT_INITIALIZED",
|
|
31
|
+
"Run 'worklog init' or 'worklog a' to initialize a new repository."
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var DatabaseError = class extends WorkLogDomainError {
|
|
36
|
+
constructor(message, details) {
|
|
37
|
+
super(
|
|
38
|
+
`Database error: ${message}${details ? ` (${details})` : ""}`,
|
|
39
|
+
"DATABASE_ERROR",
|
|
40
|
+
"Please check your database integrity or log files."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var ValidationError = class extends WorkLogDomainError {
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(
|
|
47
|
+
`Validation error: ${message}`,
|
|
48
|
+
"VALIDATION_ERROR",
|
|
49
|
+
"Please provide valid inputs conforming to the schema."
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var ConfigError = class extends WorkLogDomainError {
|
|
54
|
+
constructor(message) {
|
|
55
|
+
super(
|
|
56
|
+
`Configuration error: ${message}`,
|
|
57
|
+
"CONFIG_ERROR",
|
|
58
|
+
"Please check if .worklog/config.json is well-formed."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/core/discovery.ts
|
|
64
|
+
function findWorkspaceRoot(startDir = process.cwd()) {
|
|
65
|
+
let current = path.resolve(startDir);
|
|
66
|
+
const rootPath = path.parse(current).root;
|
|
67
|
+
while (true) {
|
|
68
|
+
const candidate = path.join(current, ".worklog");
|
|
69
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
if (current === rootPath) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
current = path.dirname(current);
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function getWorkspaceRoot(startDir = process.cwd()) {
|
|
80
|
+
const root = findWorkspaceRoot(startDir);
|
|
81
|
+
if (!root) {
|
|
82
|
+
throw new NotInitializedError();
|
|
83
|
+
}
|
|
84
|
+
return root;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/core/logger.ts
|
|
88
|
+
import pino from "pino";
|
|
89
|
+
import * as path2 from "path";
|
|
90
|
+
import * as fs2 from "fs";
|
|
91
|
+
var loggerInstance = null;
|
|
92
|
+
function getLogger() {
|
|
93
|
+
if (loggerInstance) {
|
|
94
|
+
return loggerInstance;
|
|
95
|
+
}
|
|
96
|
+
loggerInstance = pino({
|
|
97
|
+
level: "silent"
|
|
98
|
+
});
|
|
99
|
+
return loggerInstance;
|
|
100
|
+
}
|
|
101
|
+
function initLogger(workspaceRoot) {
|
|
102
|
+
const logDir = path2.join(workspaceRoot, ".worklog");
|
|
103
|
+
if (!fs2.existsSync(logDir)) {
|
|
104
|
+
fs2.mkdirSync(logDir, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
const logFile = path2.join(logDir, "worklog.log");
|
|
107
|
+
loggerInstance = pino(
|
|
108
|
+
{
|
|
109
|
+
level: process.env.WORKLOG_LOG_LEVEL || "info",
|
|
110
|
+
base: void 0,
|
|
111
|
+
// remove pid/hostname for clean local logs
|
|
112
|
+
timestamp: () => `,"time":"${(/* @__PURE__ */ new Date()).toISOString()}"`
|
|
113
|
+
},
|
|
114
|
+
pino.destination({
|
|
115
|
+
dest: logFile,
|
|
116
|
+
sync: true
|
|
117
|
+
// synchronous write to guarantee flush before CLI exit
|
|
118
|
+
})
|
|
119
|
+
);
|
|
120
|
+
return loggerInstance;
|
|
121
|
+
}
|
|
122
|
+
var logger = {
|
|
123
|
+
info: (obj, msg, ...args) => getLogger().info(obj, msg, ...args),
|
|
124
|
+
error: (obj, msg, ...args) => getLogger().error(obj, msg, ...args),
|
|
125
|
+
debug: (obj, msg, ...args) => getLogger().debug(obj, msg, ...args),
|
|
126
|
+
warn: (obj, msg, ...args) => getLogger().warn(obj, msg, ...args)
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/database/connection.ts
|
|
130
|
+
import { createRequire } from "module";
|
|
131
|
+
import * as path3 from "path";
|
|
132
|
+
import * as fs3 from "fs";
|
|
133
|
+
var DatabaseConstructor;
|
|
134
|
+
try {
|
|
135
|
+
const contextPath = typeof import.meta !== "undefined" && import.meta.url ? import.meta.url : typeof __filename !== "undefined" ? __filename : process.cwd() + "/package.json";
|
|
136
|
+
const requireFromDisk = createRequire(contextPath);
|
|
137
|
+
DatabaseConstructor = requireFromDisk("better-sqlite3");
|
|
138
|
+
} catch {
|
|
139
|
+
try {
|
|
140
|
+
const requireFromDisk = createRequire(process.cwd() + "/package.json");
|
|
141
|
+
DatabaseConstructor = requireFromDisk("better-sqlite3");
|
|
142
|
+
} catch {
|
|
143
|
+
const execDir = path3.dirname(process.execPath);
|
|
144
|
+
const requireFromDisk = createRequire(execDir + "/package.json");
|
|
145
|
+
DatabaseConstructor = requireFromDisk("better-sqlite3");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
var dbInstance = null;
|
|
149
|
+
function connectDatabase(workspaceRoot) {
|
|
150
|
+
if (dbInstance) {
|
|
151
|
+
return dbInstance;
|
|
152
|
+
}
|
|
153
|
+
const dbDir = path3.join(workspaceRoot, ".worklog");
|
|
154
|
+
if (!fs3.existsSync(dbDir)) {
|
|
155
|
+
fs3.mkdirSync(dbDir, { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
const dbPath = path3.join(dbDir, "worklog.db");
|
|
158
|
+
try {
|
|
159
|
+
const db = new DatabaseConstructor(dbPath);
|
|
160
|
+
db.pragma("foreign_keys = ON");
|
|
161
|
+
db.pragma("journal_mode = WAL");
|
|
162
|
+
db.pragma("synchronous = NORMAL");
|
|
163
|
+
db.pragma("temp_store = MEMORY");
|
|
164
|
+
dbInstance = db;
|
|
165
|
+
logger.debug({ dbPath }, "Connected to SQLite database successfully");
|
|
166
|
+
return db;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
logger.error({ error, dbPath }, "Failed to connect to SQLite database");
|
|
169
|
+
throw new DatabaseError("Failed to connect to database", error.message);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function closeDatabase() {
|
|
173
|
+
if (dbInstance) {
|
|
174
|
+
try {
|
|
175
|
+
dbInstance.close();
|
|
176
|
+
logger.debug("Database connection closed");
|
|
177
|
+
} catch (error) {
|
|
178
|
+
logger.error({ error }, "Error closing database connection");
|
|
179
|
+
} finally {
|
|
180
|
+
dbInstance = null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/database/migration-data.ts
|
|
186
|
+
var MIGRATIONS = {
|
|
187
|
+
"001_initial.sql": `
|
|
188
|
+
CREATE TABLE IF NOT EXISTS work_units (
|
|
189
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
190
|
+
title TEXT NOT NULL,
|
|
191
|
+
type TEXT NOT NULL,
|
|
192
|
+
status TEXT NOT NULL,
|
|
193
|
+
priority TEXT,
|
|
194
|
+
module TEXT,
|
|
195
|
+
description TEXT,
|
|
196
|
+
next_step TEXT,
|
|
197
|
+
decision TEXT,
|
|
198
|
+
blocker TEXT,
|
|
199
|
+
notes TEXT,
|
|
200
|
+
created_at TEXT NOT NULL,
|
|
201
|
+
updated_at TEXT NOT NULL
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
-- Full Text Search Index Table
|
|
205
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS work_units_fts USING fts5(
|
|
206
|
+
title,
|
|
207
|
+
description,
|
|
208
|
+
decision,
|
|
209
|
+
blocker,
|
|
210
|
+
notes,
|
|
211
|
+
content=work_units,
|
|
212
|
+
content_rowid=id
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
-- Auto-sync Insert Trigger
|
|
216
|
+
CREATE TRIGGER IF NOT EXISTS work_units_ai AFTER INSERT ON work_units BEGIN
|
|
217
|
+
INSERT INTO work_units_fts(rowid, title, description, decision, blocker, notes)
|
|
218
|
+
VALUES (new.id, new.title, new.description, new.decision, new.blocker, new.notes);
|
|
219
|
+
END;
|
|
220
|
+
|
|
221
|
+
-- Auto-sync Delete Trigger
|
|
222
|
+
CREATE TRIGGER IF NOT EXISTS work_units_ad AFTER DELETE ON work_units BEGIN
|
|
223
|
+
INSERT INTO work_units_fts(work_units_fts, rowid, title, description, decision, blocker, notes)
|
|
224
|
+
VALUES('delete', old.id, old.title, old.description, old.decision, old.blocker, old.notes);
|
|
225
|
+
END;
|
|
226
|
+
|
|
227
|
+
-- Auto-sync Update Trigger
|
|
228
|
+
CREATE TRIGGER IF NOT EXISTS work_units_au AFTER UPDATE ON work_units BEGIN
|
|
229
|
+
INSERT INTO work_units_fts(work_units_fts, rowid, title, description, decision, blocker, notes)
|
|
230
|
+
VALUES('delete', old.id, old.title, old.description, old.decision, old.blocker, old.notes);
|
|
231
|
+
INSERT INTO work_units_fts(rowid, title, description, decision, blocker, notes)
|
|
232
|
+
VALUES (new.id, new.title, new.description, new.decision, new.blocker, new.notes);
|
|
233
|
+
END;
|
|
234
|
+
`,
|
|
235
|
+
"002_relational_tables.sql": `
|
|
236
|
+
-- Junction table for tags
|
|
237
|
+
CREATE TABLE IF NOT EXISTS work_unit_tags (
|
|
238
|
+
work_unit_id INTEGER NOT NULL,
|
|
239
|
+
tag TEXT NOT NULL,
|
|
240
|
+
PRIMARY KEY (work_unit_id, tag),
|
|
241
|
+
FOREIGN KEY (work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
-- Index for tag filtering performance
|
|
245
|
+
CREATE INDEX IF NOT EXISTS idx_work_unit_tags_tag ON work_unit_tags(tag);
|
|
246
|
+
|
|
247
|
+
-- Junction table for bidirectional relation mapping
|
|
248
|
+
CREATE TABLE IF NOT EXISTS work_unit_relations (
|
|
249
|
+
work_unit_id INTEGER NOT NULL,
|
|
250
|
+
related_work_unit_id INTEGER NOT NULL,
|
|
251
|
+
PRIMARY KEY (work_unit_id, related_work_unit_id),
|
|
252
|
+
FOREIGN KEY (work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE,
|
|
253
|
+
FOREIGN KEY (related_work_unit_id) REFERENCES work_units(id) ON DELETE CASCADE
|
|
254
|
+
);
|
|
255
|
+
`
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// src/database/migrations.ts
|
|
259
|
+
function runMigrations(db) {
|
|
260
|
+
logger.debug("Starting database migrations run");
|
|
261
|
+
db.exec(`
|
|
262
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
263
|
+
version TEXT PRIMARY KEY,
|
|
264
|
+
applied_at TEXT NOT NULL
|
|
265
|
+
);
|
|
266
|
+
`);
|
|
267
|
+
const files = Object.keys(MIGRATIONS).sort();
|
|
268
|
+
if (files.length === 0) {
|
|
269
|
+
logger.debug("No migration definitions found");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const stmt = db.prepare("SELECT version FROM schema_migrations");
|
|
273
|
+
const applied = stmt.all().map((row) => row.version);
|
|
274
|
+
for (const file of files) {
|
|
275
|
+
if (applied.includes(file)) {
|
|
276
|
+
logger.debug({ migration: file }, "Migration already applied, skipping");
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
logger.info({ migration: file }, `Applying database migration: ${file}`);
|
|
280
|
+
const sql = MIGRATIONS[file];
|
|
281
|
+
const runMigrationTx = db.transaction(() => {
|
|
282
|
+
db.exec(sql);
|
|
283
|
+
const insertStmt = db.prepare(
|
|
284
|
+
"INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)"
|
|
285
|
+
);
|
|
286
|
+
insertStmt.run(file, (/* @__PURE__ */ new Date()).toISOString());
|
|
287
|
+
});
|
|
288
|
+
try {
|
|
289
|
+
runMigrationTx();
|
|
290
|
+
logger.info({ migration: file }, `Migration ${file} applied successfully`);
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error({ error, migration: file }, `Failed to apply migration: ${file}`);
|
|
293
|
+
throw new DatabaseError(`Failed to apply migration ${file}`, error.message);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
logger.debug("Database migrations completed");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/repositories/work-unit.ts
|
|
300
|
+
var WorkUnitRepository = class {
|
|
301
|
+
constructor(db) {
|
|
302
|
+
this.db = db;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Creates a new WorkUnit in the database.
|
|
306
|
+
* Inserts relations into work_unit_tags and work_unit_relations under a transaction.
|
|
307
|
+
*/
|
|
308
|
+
create(workUnitData) {
|
|
309
|
+
const createTx = this.db.transaction(() => {
|
|
310
|
+
const insertUnit = this.db.prepare(`
|
|
311
|
+
INSERT INTO work_units (
|
|
312
|
+
title, type, status, priority, module, description, next_step, decision, blocker, notes, created_at, updated_at
|
|
313
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
314
|
+
`);
|
|
315
|
+
const result = insertUnit.run(
|
|
316
|
+
workUnitData.title,
|
|
317
|
+
workUnitData.type,
|
|
318
|
+
workUnitData.status,
|
|
319
|
+
workUnitData.priority,
|
|
320
|
+
workUnitData.module,
|
|
321
|
+
workUnitData.description,
|
|
322
|
+
workUnitData.nextStep,
|
|
323
|
+
workUnitData.decision,
|
|
324
|
+
workUnitData.blocker,
|
|
325
|
+
workUnitData.notes,
|
|
326
|
+
workUnitData.createdAt,
|
|
327
|
+
workUnitData.updatedAt
|
|
328
|
+
);
|
|
329
|
+
const workUnitId = Number(result.lastInsertRowid);
|
|
330
|
+
if (workUnitData.tags && workUnitData.tags.length > 0) {
|
|
331
|
+
const insertTag = this.db.prepare(`
|
|
332
|
+
INSERT INTO work_unit_tags (work_unit_id, tag) VALUES (?, ?)
|
|
333
|
+
`);
|
|
334
|
+
for (const tag of workUnitData.tags) {
|
|
335
|
+
insertTag.run(workUnitId, tag);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (workUnitData.relatedWorkUnits && workUnitData.relatedWorkUnits.length > 0) {
|
|
339
|
+
const insertRelation = this.db.prepare(`
|
|
340
|
+
INSERT INTO work_unit_relations (work_unit_id, related_work_unit_id) VALUES (?, ?)
|
|
341
|
+
`);
|
|
342
|
+
const checkStmt = this.db.prepare("SELECT 1 FROM work_units WHERE id = ?");
|
|
343
|
+
for (const relId of workUnitData.relatedWorkUnits) {
|
|
344
|
+
if (!checkStmt.get(relId)) {
|
|
345
|
+
throw new DatabaseError(`Related work unit with ID ${relId} does not exist.`);
|
|
346
|
+
}
|
|
347
|
+
insertRelation.run(workUnitId, relId);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return workUnitId;
|
|
351
|
+
});
|
|
352
|
+
try {
|
|
353
|
+
const id = createTx();
|
|
354
|
+
return this.findById(id);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
if (error instanceof DatabaseError) throw error;
|
|
357
|
+
throw new DatabaseError("Failed to create work unit", error.message);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Fetches a WorkUnit by ID and joins related tags and relations.
|
|
362
|
+
*/
|
|
363
|
+
findById(id) {
|
|
364
|
+
try {
|
|
365
|
+
const selectUnit = this.db.prepare(`
|
|
366
|
+
SELECT * FROM work_units WHERE id = ?
|
|
367
|
+
`);
|
|
368
|
+
const row = selectUnit.get(id);
|
|
369
|
+
if (!row) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
const selectTags = this.db.prepare(`
|
|
373
|
+
SELECT tag FROM work_unit_tags WHERE work_unit_id = ? ORDER BY tag ASC
|
|
374
|
+
`);
|
|
375
|
+
const tags = selectTags.all(id).map((r) => r.tag);
|
|
376
|
+
const selectRelations = this.db.prepare(`
|
|
377
|
+
SELECT related_work_unit_id FROM work_unit_relations WHERE work_unit_id = ? ORDER BY related_work_unit_id ASC
|
|
378
|
+
`);
|
|
379
|
+
const related = selectRelations.all(id).map((r) => Number(r.related_work_unit_id));
|
|
380
|
+
return {
|
|
381
|
+
id: Number(row.id),
|
|
382
|
+
title: row.title,
|
|
383
|
+
type: row.type,
|
|
384
|
+
status: row.status,
|
|
385
|
+
priority: row.priority,
|
|
386
|
+
module: row.module,
|
|
387
|
+
description: row.description,
|
|
388
|
+
nextStep: row.next_step,
|
|
389
|
+
decision: row.decision,
|
|
390
|
+
blocker: row.blocker,
|
|
391
|
+
tags,
|
|
392
|
+
relatedWorkUnits: related,
|
|
393
|
+
notes: row.notes,
|
|
394
|
+
createdAt: row.created_at,
|
|
395
|
+
updatedAt: row.updated_at
|
|
396
|
+
};
|
|
397
|
+
} catch (error) {
|
|
398
|
+
throw new DatabaseError(`Failed to find work unit by ID ${id}`, error.message);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Updates an existing WorkUnit.
|
|
403
|
+
* Handles differential updates for fields, tags, and relations.
|
|
404
|
+
*/
|
|
405
|
+
update(id, workUnitData) {
|
|
406
|
+
const updateTx = this.db.transaction(() => {
|
|
407
|
+
const existing = this.findById(id);
|
|
408
|
+
if (!existing) {
|
|
409
|
+
throw new DatabaseError(`Work unit with ID ${id} not found.`);
|
|
410
|
+
}
|
|
411
|
+
const updates = [];
|
|
412
|
+
const values = [];
|
|
413
|
+
const fieldsMap = {
|
|
414
|
+
title: "title",
|
|
415
|
+
type: "type",
|
|
416
|
+
status: "status",
|
|
417
|
+
priority: "priority",
|
|
418
|
+
module: "module",
|
|
419
|
+
description: "description",
|
|
420
|
+
nextStep: "next_step",
|
|
421
|
+
decision: "decision",
|
|
422
|
+
blocker: "blocker",
|
|
423
|
+
notes: "notes",
|
|
424
|
+
updatedAt: "updated_at"
|
|
425
|
+
};
|
|
426
|
+
for (const [key, dbCol] of Object.entries(fieldsMap)) {
|
|
427
|
+
if (key in workUnitData) {
|
|
428
|
+
updates.push(`${dbCol} = ?`);
|
|
429
|
+
values.push(workUnitData[key]);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (updates.length > 0) {
|
|
433
|
+
values.push(id);
|
|
434
|
+
const updateStmt = this.db.prepare(`
|
|
435
|
+
UPDATE work_units SET ${updates.join(", ")} WHERE id = ?
|
|
436
|
+
`);
|
|
437
|
+
updateStmt.run(...values);
|
|
438
|
+
}
|
|
439
|
+
if (workUnitData.tags !== void 0) {
|
|
440
|
+
const deleteTags = this.db.prepare("DELETE FROM work_unit_tags WHERE work_unit_id = ?");
|
|
441
|
+
deleteTags.run(id);
|
|
442
|
+
if (workUnitData.tags.length > 0) {
|
|
443
|
+
const insertTag = this.db.prepare(
|
|
444
|
+
"INSERT INTO work_unit_tags (work_unit_id, tag) VALUES (?, ?)"
|
|
445
|
+
);
|
|
446
|
+
for (const tag of workUnitData.tags) {
|
|
447
|
+
insertTag.run(id, tag);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (workUnitData.relatedWorkUnits !== void 0) {
|
|
452
|
+
const deleteRelations = this.db.prepare(
|
|
453
|
+
"DELETE FROM work_unit_relations WHERE work_unit_id = ?"
|
|
454
|
+
);
|
|
455
|
+
deleteRelations.run(id);
|
|
456
|
+
if (workUnitData.relatedWorkUnits.length > 0) {
|
|
457
|
+
const insertRelation = this.db.prepare(
|
|
458
|
+
"INSERT INTO work_unit_relations (work_unit_id, related_work_unit_id) VALUES (?, ?)"
|
|
459
|
+
);
|
|
460
|
+
const checkStmt = this.db.prepare("SELECT 1 FROM work_units WHERE id = ?");
|
|
461
|
+
for (const relId of workUnitData.relatedWorkUnits) {
|
|
462
|
+
if (!checkStmt.get(relId)) {
|
|
463
|
+
throw new DatabaseError(`Related work unit with ID ${relId} does not exist.`);
|
|
464
|
+
}
|
|
465
|
+
insertRelation.run(id, relId);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
try {
|
|
471
|
+
updateTx();
|
|
472
|
+
return this.findById(id);
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (error instanceof DatabaseError) throw error;
|
|
475
|
+
throw new DatabaseError(`Failed to update work unit by ID ${id}`, error.message);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Deletes a WorkUnit. Returns true if changes were made.
|
|
480
|
+
*/
|
|
481
|
+
delete(id) {
|
|
482
|
+
try {
|
|
483
|
+
const deleteStmt = this.db.prepare("DELETE FROM work_units WHERE id = ?");
|
|
484
|
+
const info = deleteStmt.run(id);
|
|
485
|
+
return info.changes > 0;
|
|
486
|
+
} catch (error) {
|
|
487
|
+
throw new DatabaseError(`Failed to delete work unit by ID ${id}`, error.message);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Finds the most recent work units, ordered by creation date descending.
|
|
492
|
+
*/
|
|
493
|
+
findRecent(limit = 10) {
|
|
494
|
+
try {
|
|
495
|
+
const selectIds = this.db.prepare(`
|
|
496
|
+
SELECT id FROM work_units ORDER BY created_at DESC LIMIT ?
|
|
497
|
+
`);
|
|
498
|
+
const rows = selectIds.all(limit);
|
|
499
|
+
return rows.map((row) => this.findById(row.id));
|
|
500
|
+
} catch (error) {
|
|
501
|
+
throw new DatabaseError("Failed to fetch recent work units", error.message);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Finds work units matching a specific status.
|
|
506
|
+
*/
|
|
507
|
+
findByStatus(status) {
|
|
508
|
+
try {
|
|
509
|
+
const selectIds = this.db.prepare(`
|
|
510
|
+
SELECT id FROM work_units WHERE status = ? ORDER BY created_at DESC
|
|
511
|
+
`);
|
|
512
|
+
const rows = selectIds.all(status);
|
|
513
|
+
return rows.map((row) => this.findById(row.id));
|
|
514
|
+
} catch (error) {
|
|
515
|
+
throw new DatabaseError(`Failed to fetch work units by status: ${status}`, error.message);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Finds work units matching a specific type.
|
|
520
|
+
*/
|
|
521
|
+
findByType(type) {
|
|
522
|
+
try {
|
|
523
|
+
const selectIds = this.db.prepare(`
|
|
524
|
+
SELECT id FROM work_units WHERE type = ? ORDER BY created_at DESC
|
|
525
|
+
`);
|
|
526
|
+
const rows = selectIds.all(type);
|
|
527
|
+
return rows.map((row) => this.findById(row.id));
|
|
528
|
+
} catch (error) {
|
|
529
|
+
throw new DatabaseError(`Failed to fetch work units by type: ${type}`, error.message);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Finds all unfinished work units (Planned, In Progress, Blocked) ordered by updatedAt descending.
|
|
534
|
+
*/
|
|
535
|
+
findUnfinished() {
|
|
536
|
+
try {
|
|
537
|
+
const selectIds = this.db.prepare(`
|
|
538
|
+
SELECT id FROM work_units
|
|
539
|
+
WHERE status IN ('Planned', 'In Progress', 'Blocked')
|
|
540
|
+
ORDER BY updated_at DESC
|
|
541
|
+
`);
|
|
542
|
+
const rows = selectIds.all();
|
|
543
|
+
return rows.map((row) => this.findById(row.id));
|
|
544
|
+
} catch (error) {
|
|
545
|
+
throw new DatabaseError("Failed to fetch unfinished work units", error.message);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Finds work units created or updated since an ISO timestamp.
|
|
550
|
+
*/
|
|
551
|
+
findCreatedOrUpdatedAfter(isoString) {
|
|
552
|
+
try {
|
|
553
|
+
const selectIds = this.db.prepare(`
|
|
554
|
+
SELECT id FROM work_units
|
|
555
|
+
WHERE created_at >= ? OR updated_at >= ?
|
|
556
|
+
ORDER BY created_at DESC
|
|
557
|
+
`);
|
|
558
|
+
const rows = selectIds.all(isoString, isoString);
|
|
559
|
+
return rows.map((row) => this.findById(row.id));
|
|
560
|
+
} catch (error) {
|
|
561
|
+
throw new DatabaseError(
|
|
562
|
+
`Failed to fetch work units updated after ${isoString}`,
|
|
563
|
+
error.message
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Runs analytical SQL queries to fetch count aggregates.
|
|
569
|
+
*/
|
|
570
|
+
getStats(sinceIsoString) {
|
|
571
|
+
try {
|
|
572
|
+
const totalCount = this.db.prepare("SELECT COUNT(*) as count FROM work_units").get().count;
|
|
573
|
+
const typeRows = this.db.prepare(
|
|
574
|
+
"SELECT type, COUNT(*) as count FROM work_units GROUP BY type ORDER BY count DESC"
|
|
575
|
+
).all();
|
|
576
|
+
const typeDistribution = typeRows.map((r) => ({ type: r.type, count: r.count }));
|
|
577
|
+
const statusRows = this.db.prepare(
|
|
578
|
+
"SELECT status, COUNT(*) as count FROM work_units GROUP BY status ORDER BY count DESC"
|
|
579
|
+
).all();
|
|
580
|
+
const statusDistribution = statusRows.map((r) => ({ status: r.status, count: r.count }));
|
|
581
|
+
const moduleRows = this.db.prepare(`
|
|
582
|
+
SELECT module, COUNT(*) as count FROM work_units
|
|
583
|
+
WHERE module IS NOT NULL AND module != ''
|
|
584
|
+
GROUP BY module ORDER BY count DESC
|
|
585
|
+
`).all();
|
|
586
|
+
const moduleDistribution = moduleRows.map((r) => ({ module: r.module, count: r.count }));
|
|
587
|
+
const dailyRows = this.db.prepare(`
|
|
588
|
+
SELECT date(created_at) as day, COUNT(*) as count FROM work_units
|
|
589
|
+
WHERE created_at >= ?
|
|
590
|
+
GROUP BY day ORDER BY day ASC
|
|
591
|
+
`).all(sinceIsoString);
|
|
592
|
+
const dailyActivity = dailyRows.map((r) => ({ day: r.day, count: r.count }));
|
|
593
|
+
return {
|
|
594
|
+
totalCount,
|
|
595
|
+
typeDistribution,
|
|
596
|
+
statusDistribution,
|
|
597
|
+
moduleDistribution,
|
|
598
|
+
dailyActivity
|
|
599
|
+
};
|
|
600
|
+
} catch (error) {
|
|
601
|
+
throw new DatabaseError("Failed to fetch aggregate statistics", error.message);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Case-insensitive, structured multi-criteria search combining FTS5 and standard filters.
|
|
606
|
+
*/
|
|
607
|
+
search(query, filters = {}) {
|
|
608
|
+
try {
|
|
609
|
+
let ids = [];
|
|
610
|
+
const hasQuery = query.trim().length > 0;
|
|
611
|
+
if (hasQuery) {
|
|
612
|
+
const trimmed = query.trim();
|
|
613
|
+
const ftsQuery = `"${trimmed}" OR ${trimmed}*`;
|
|
614
|
+
try {
|
|
615
|
+
const stmt = this.db.prepare(`
|
|
616
|
+
SELECT rowid as id FROM work_units_fts WHERE work_units_fts MATCH ?
|
|
617
|
+
`);
|
|
618
|
+
ids = stmt.all(ftsQuery).map((r) => Number(r.id));
|
|
619
|
+
} catch (ftsError) {
|
|
620
|
+
logger.warn({ ftsError, query }, "FTS MATCH failed; using fallback LIKE list");
|
|
621
|
+
const fallbackStmt = this.db.prepare(`
|
|
622
|
+
SELECT id FROM work_units
|
|
623
|
+
WHERE title LIKE ? OR description LIKE ? OR notes LIKE ? OR decision LIKE ? OR blocker LIKE ?
|
|
624
|
+
`);
|
|
625
|
+
const likeParam = `%${trimmed}%`;
|
|
626
|
+
ids = fallbackStmt.all(likeParam, likeParam, likeParam, likeParam, likeParam).map((r) => Number(r.id));
|
|
627
|
+
}
|
|
628
|
+
if (ids.length === 0) {
|
|
629
|
+
return [];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const clauses = [];
|
|
633
|
+
const params = [];
|
|
634
|
+
if (hasQuery) {
|
|
635
|
+
clauses.push(`id IN (${ids.map(() => "?").join(",")})`);
|
|
636
|
+
params.push(...ids);
|
|
637
|
+
}
|
|
638
|
+
if (filters.module) {
|
|
639
|
+
clauses.push("LOWER(module) = LOWER(?)");
|
|
640
|
+
params.push(filters.module);
|
|
641
|
+
}
|
|
642
|
+
if (filters.status) {
|
|
643
|
+
clauses.push("LOWER(status) = LOWER(?)");
|
|
644
|
+
params.push(filters.status);
|
|
645
|
+
}
|
|
646
|
+
if (filters.type) {
|
|
647
|
+
clauses.push("LOWER(type) = LOWER(?)");
|
|
648
|
+
params.push(filters.type);
|
|
649
|
+
}
|
|
650
|
+
if (filters.tag) {
|
|
651
|
+
clauses.push(`id IN (
|
|
652
|
+
SELECT work_unit_id FROM work_unit_tags WHERE LOWER(tag) = LOWER(?)
|
|
653
|
+
)`);
|
|
654
|
+
params.push(filters.tag);
|
|
655
|
+
}
|
|
656
|
+
const whereClause = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
657
|
+
const sql = `
|
|
658
|
+
SELECT id FROM work_units
|
|
659
|
+
${whereClause}
|
|
660
|
+
ORDER BY created_at DESC
|
|
661
|
+
`;
|
|
662
|
+
const rows = this.db.prepare(sql).all(...params);
|
|
663
|
+
return rows.map((row) => this.findById(row.id));
|
|
664
|
+
} catch (error) {
|
|
665
|
+
throw new DatabaseError(`Search operation failed: ${error.message}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
// src/validators/work-unit.ts
|
|
671
|
+
import { z } from "zod";
|
|
672
|
+
var WorkUnitTypeSchema = z.enum([
|
|
673
|
+
"Feature",
|
|
674
|
+
"Bug",
|
|
675
|
+
"Spike",
|
|
676
|
+
"Refactor",
|
|
677
|
+
"Improvement",
|
|
678
|
+
"Research",
|
|
679
|
+
"Decision",
|
|
680
|
+
"Blocker",
|
|
681
|
+
"Review",
|
|
682
|
+
"Idea"
|
|
683
|
+
]);
|
|
684
|
+
var WorkUnitStatusSchema = z.enum([
|
|
685
|
+
"Planned",
|
|
686
|
+
"In Progress",
|
|
687
|
+
"Blocked",
|
|
688
|
+
"Completed",
|
|
689
|
+
"Cancelled"
|
|
690
|
+
]);
|
|
691
|
+
var WorkUnitPrioritySchema = z.enum(["Low", "Medium", "High"]);
|
|
692
|
+
var CreateWorkUnitSchema = z.object({
|
|
693
|
+
title: z.string().trim().min(1, "Title cannot be empty").max(200, "Title cannot exceed 200 characters"),
|
|
694
|
+
type: WorkUnitTypeSchema,
|
|
695
|
+
status: WorkUnitStatusSchema.default("Planned"),
|
|
696
|
+
priority: WorkUnitPrioritySchema.nullable().optional().default(null),
|
|
697
|
+
module: z.string().trim().max(100, "Module cannot exceed 100 characters").nullable().optional().default(null),
|
|
698
|
+
description: z.string().trim().nullable().optional().default(null),
|
|
699
|
+
nextStep: z.string().trim().nullable().optional().default(null),
|
|
700
|
+
decision: z.string().trim().nullable().optional().default(null),
|
|
701
|
+
blocker: z.string().trim().nullable().optional().default(null),
|
|
702
|
+
tags: z.array(
|
|
703
|
+
z.string().trim().min(1, "Tag cannot be empty").max(50, "Tag cannot exceed 50 characters")
|
|
704
|
+
).optional().default([]),
|
|
705
|
+
relatedWorkUnits: z.array(z.number().int().positive()).optional().default([]),
|
|
706
|
+
notes: z.string().trim().nullable().optional().default(null)
|
|
707
|
+
});
|
|
708
|
+
var UpdateWorkUnitSchema = CreateWorkUnitSchema.partial();
|
|
709
|
+
|
|
710
|
+
// src/services/work-unit.ts
|
|
711
|
+
var WorkUnitService = class {
|
|
712
|
+
constructor(repo) {
|
|
713
|
+
this.repo = repo;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Validates creation input at the service boundary, assigns initial timestamps, and persists to database.
|
|
717
|
+
*/
|
|
718
|
+
createWorkUnit(input) {
|
|
719
|
+
const result = CreateWorkUnitSchema.safeParse(input);
|
|
720
|
+
if (!result.success) {
|
|
721
|
+
const errorMsg = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ");
|
|
722
|
+
throw new ValidationError(errorMsg);
|
|
723
|
+
}
|
|
724
|
+
const validated = result.data;
|
|
725
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
726
|
+
const workUnitData = {
|
|
727
|
+
...validated,
|
|
728
|
+
createdAt: now,
|
|
729
|
+
updatedAt: now
|
|
730
|
+
};
|
|
731
|
+
return this.repo.create(workUnitData);
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Validates update input, updates timestamp, and runs repository update statement.
|
|
735
|
+
*/
|
|
736
|
+
updateWorkUnit(id, input) {
|
|
737
|
+
if (!id || id <= 0 || !Number.isInteger(id)) {
|
|
738
|
+
throw new ValidationError("A valid positive integer ID is required.");
|
|
739
|
+
}
|
|
740
|
+
const result = UpdateWorkUnitSchema.safeParse(input);
|
|
741
|
+
if (!result.success) {
|
|
742
|
+
const errorMsg = result.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(", ");
|
|
743
|
+
throw new ValidationError(errorMsg);
|
|
744
|
+
}
|
|
745
|
+
const validated = result.data;
|
|
746
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
747
|
+
const workUnitData = {
|
|
748
|
+
...validated,
|
|
749
|
+
updatedAt: now
|
|
750
|
+
};
|
|
751
|
+
return this.repo.update(id, workUnitData);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Fetches a WorkUnit by its ID. Throws on negative/decimal values.
|
|
755
|
+
*/
|
|
756
|
+
getWorkUnit(id) {
|
|
757
|
+
if (!id || id <= 0 || !Number.isInteger(id)) {
|
|
758
|
+
throw new ValidationError("A valid positive integer ID is required.");
|
|
759
|
+
}
|
|
760
|
+
return this.repo.findById(id);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Deletes a WorkUnit by ID. Returns true if deleted.
|
|
764
|
+
*/
|
|
765
|
+
deleteWorkUnit(id) {
|
|
766
|
+
if (!id || id <= 0 || !Number.isInteger(id)) {
|
|
767
|
+
throw new ValidationError("A valid positive integer ID is required.");
|
|
768
|
+
}
|
|
769
|
+
return this.repo.delete(id);
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Retrieves recent WorkUnits.
|
|
773
|
+
*/
|
|
774
|
+
getRecentWorkUnits(limit) {
|
|
775
|
+
return this.repo.findRecent(limit);
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Retrieves WorkUnits filtered by Status.
|
|
779
|
+
*/
|
|
780
|
+
getWorkUnitsByStatus(status) {
|
|
781
|
+
return this.repo.findByStatus(status);
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Retrieves WorkUnits filtered by Type.
|
|
785
|
+
*/
|
|
786
|
+
getWorkUnitsByType(type) {
|
|
787
|
+
return this.repo.findByType(type);
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Retrieves all unfinished Work Units (Planned, In Progress, Blocked) sorted by updatedAt descending.
|
|
791
|
+
*/
|
|
792
|
+
getUnfinishedWorkUnits() {
|
|
793
|
+
return this.repo.findUnfinished();
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Retrieves Work Units created or updated since an ISO timestamp.
|
|
797
|
+
*/
|
|
798
|
+
getCreatedOrUpdatedAfter(isoString) {
|
|
799
|
+
return this.repo.findCreatedOrUpdatedAfter(isoString);
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Retrieves SQLite aggregated statistics.
|
|
803
|
+
*/
|
|
804
|
+
getStats(sinceIsoString) {
|
|
805
|
+
return this.repo.getStats(sinceIsoString);
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Searches WorkUnits by text query and structured filters.
|
|
809
|
+
*/
|
|
810
|
+
searchWorkUnits(query, filters = {}) {
|
|
811
|
+
return this.repo.search(query, filters);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
// src/cli/commands/init.ts
|
|
816
|
+
import * as fs5 from "fs";
|
|
817
|
+
import * as path5 from "path";
|
|
818
|
+
import pc from "picocolors";
|
|
819
|
+
|
|
820
|
+
// src/core/config.ts
|
|
821
|
+
import * as fs4 from "fs";
|
|
822
|
+
import * as path4 from "path";
|
|
823
|
+
import { z as z2 } from "zod";
|
|
824
|
+
var ConfigSchema = z2.object({
|
|
825
|
+
projectName: z2.string(),
|
|
826
|
+
createdAt: z2.string(),
|
|
827
|
+
schemaVersion: z2.number().default(1)
|
|
828
|
+
});
|
|
829
|
+
var DEFAULT_CONFIG_FILENAME = "config.json";
|
|
830
|
+
function saveConfig(workspaceRoot, config) {
|
|
831
|
+
const dotWorklog = path4.join(workspaceRoot, ".worklog");
|
|
832
|
+
if (!fs4.existsSync(dotWorklog)) {
|
|
833
|
+
fs4.mkdirSync(dotWorklog, { recursive: true });
|
|
834
|
+
}
|
|
835
|
+
const configPath = path4.join(dotWorklog, DEFAULT_CONFIG_FILENAME);
|
|
836
|
+
try {
|
|
837
|
+
const validated = ConfigSchema.parse(config);
|
|
838
|
+
fs4.writeFileSync(configPath, JSON.stringify(validated, null, 2), "utf8");
|
|
839
|
+
} catch (error) {
|
|
840
|
+
throw new ConfigError(`Failed to save config: ${error.message}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
function initConfig(workspaceRoot, projectName) {
|
|
844
|
+
const pName = projectName || path4.basename(path4.resolve(workspaceRoot));
|
|
845
|
+
const newConfig = {
|
|
846
|
+
projectName: pName,
|
|
847
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
848
|
+
schemaVersion: 1
|
|
849
|
+
};
|
|
850
|
+
saveConfig(workspaceRoot, newConfig);
|
|
851
|
+
return newConfig;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// src/cli/commands/init.ts
|
|
855
|
+
function initCommand(options) {
|
|
856
|
+
const targetDir = process.cwd();
|
|
857
|
+
const dotWorklog = path5.join(targetDir, ".worklog");
|
|
858
|
+
const exists = fs5.existsSync(dotWorklog) && fs5.statSync(dotWorklog).isDirectory();
|
|
859
|
+
if (!fs5.existsSync(dotWorklog)) {
|
|
860
|
+
fs5.mkdirSync(dotWorklog, { recursive: true });
|
|
861
|
+
}
|
|
862
|
+
const config = initConfig(targetDir, options.name);
|
|
863
|
+
initLogger(targetDir);
|
|
864
|
+
const db = connectDatabase(targetDir);
|
|
865
|
+
runMigrations(db);
|
|
866
|
+
if (options.json) {
|
|
867
|
+
console.log(
|
|
868
|
+
JSON.stringify({
|
|
869
|
+
success: true,
|
|
870
|
+
message: exists ? "Reinitialized existing WorkLog repository" : "Initialized empty WorkLog repository",
|
|
871
|
+
projectName: config.projectName,
|
|
872
|
+
path: dotWorklog
|
|
873
|
+
})
|
|
874
|
+
);
|
|
875
|
+
} else {
|
|
876
|
+
const verb = exists ? "Reinitialized existing" : "Initialized empty";
|
|
877
|
+
console.log(pc.green(`${verb} WorkLog repository in ${dotWorklog}/`));
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/cli/commands/add.ts
|
|
882
|
+
import prompts from "prompts";
|
|
883
|
+
import pc2 from "picocolors";
|
|
884
|
+
function capitalize(val) {
|
|
885
|
+
if (!val) return val;
|
|
886
|
+
return val.charAt(0).toUpperCase() + val.slice(1).toLowerCase();
|
|
887
|
+
}
|
|
888
|
+
function parseStatus(val) {
|
|
889
|
+
if (!val) return val;
|
|
890
|
+
const lower = val.toLowerCase();
|
|
891
|
+
if (lower === "in-progress" || lower === "inprogress" || lower === "in_progress") {
|
|
892
|
+
return "In Progress";
|
|
893
|
+
}
|
|
894
|
+
return capitalize(val);
|
|
895
|
+
}
|
|
896
|
+
async function addCommand(service, options) {
|
|
897
|
+
let inputData;
|
|
898
|
+
if (!options.title) {
|
|
899
|
+
if (!process.stdin.isTTY) {
|
|
900
|
+
if (options.json) {
|
|
901
|
+
console.error(
|
|
902
|
+
JSON.stringify({
|
|
903
|
+
success: false,
|
|
904
|
+
error: "Title is required in non-interactive mode."
|
|
905
|
+
})
|
|
906
|
+
);
|
|
907
|
+
} else {
|
|
908
|
+
console.error(pc2.red("Error: Title is required in non-interactive mode."));
|
|
909
|
+
}
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
const responses = await prompts(
|
|
913
|
+
[
|
|
914
|
+
{
|
|
915
|
+
type: "text",
|
|
916
|
+
name: "title",
|
|
917
|
+
message: "Title:",
|
|
918
|
+
validate: (val) => val.trim().length > 0 ? true : "Title is required"
|
|
919
|
+
},
|
|
920
|
+
{
|
|
921
|
+
type: "select",
|
|
922
|
+
name: "type",
|
|
923
|
+
message: "Type:",
|
|
924
|
+
choices: [
|
|
925
|
+
{ title: "Feature", value: "Feature" },
|
|
926
|
+
{ title: "Bug", value: "Bug" },
|
|
927
|
+
{ title: "Spike", value: "Spike" },
|
|
928
|
+
{ title: "Refactor", value: "Refactor" },
|
|
929
|
+
{ title: "Improvement", value: "Improvement" },
|
|
930
|
+
{ title: "Research", value: "Research" },
|
|
931
|
+
{ title: "Decision", value: "Decision" },
|
|
932
|
+
{ title: "Blocker", value: "Blocker" },
|
|
933
|
+
{ title: "Review", value: "Review" },
|
|
934
|
+
{ title: "Idea", value: "Idea" }
|
|
935
|
+
],
|
|
936
|
+
initial: 0
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
type: "select",
|
|
940
|
+
name: "status",
|
|
941
|
+
message: "Status:",
|
|
942
|
+
choices: [
|
|
943
|
+
{ title: "Planned", value: "Planned" },
|
|
944
|
+
{ title: "In Progress", value: "In Progress" },
|
|
945
|
+
{ title: "Blocked", value: "Blocked" },
|
|
946
|
+
{ title: "Completed", value: "Completed" },
|
|
947
|
+
{ title: "Cancelled", value: "Cancelled" }
|
|
948
|
+
],
|
|
949
|
+
initial: 0
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
type: "text",
|
|
953
|
+
name: "description",
|
|
954
|
+
message: "Description (optional):",
|
|
955
|
+
format: (val) => val.trim() || null
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
type: "text",
|
|
959
|
+
name: "module",
|
|
960
|
+
message: "Module (optional):",
|
|
961
|
+
format: (val) => val.trim() || null
|
|
962
|
+
},
|
|
963
|
+
{
|
|
964
|
+
type: "text",
|
|
965
|
+
name: "tags",
|
|
966
|
+
message: "Tags (comma-separated, optional):",
|
|
967
|
+
format: (val) => val.trim() || null
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
type: "text",
|
|
971
|
+
name: "nextStep",
|
|
972
|
+
message: "Next Step (optional):",
|
|
973
|
+
format: (val) => val.trim() || null
|
|
974
|
+
}
|
|
975
|
+
],
|
|
976
|
+
{
|
|
977
|
+
onCancel: () => {
|
|
978
|
+
if (options.json) {
|
|
979
|
+
console.log(JSON.stringify({ success: false, error: "Cancelled by user" }));
|
|
980
|
+
} else {
|
|
981
|
+
console.log(pc2.yellow("\nCancelled."));
|
|
982
|
+
}
|
|
983
|
+
process.exit(1);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
);
|
|
987
|
+
const parsedTags = responses.tags ? responses.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
988
|
+
inputData = {
|
|
989
|
+
title: responses.title,
|
|
990
|
+
type: responses.type,
|
|
991
|
+
status: responses.status,
|
|
992
|
+
description: responses.description,
|
|
993
|
+
module: responses.module,
|
|
994
|
+
tags: parsedTags,
|
|
995
|
+
nextStep: responses.nextStep
|
|
996
|
+
};
|
|
997
|
+
} else {
|
|
998
|
+
const parsedTags = options.tags ? options.tags.split(",").map((t) => t.trim()).filter(Boolean) : [];
|
|
999
|
+
inputData = {
|
|
1000
|
+
title: options.title,
|
|
1001
|
+
type: options.type ? capitalize(options.type) : "Feature",
|
|
1002
|
+
status: options.status ? parseStatus(options.status) : "Planned",
|
|
1003
|
+
priority: options.priority ? capitalize(options.priority) : null,
|
|
1004
|
+
module: options.module || null,
|
|
1005
|
+
tags: parsedTags,
|
|
1006
|
+
description: options.description || null,
|
|
1007
|
+
nextStep: options.next || null,
|
|
1008
|
+
notes: options.notes || null
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
const created = service.createWorkUnit(inputData);
|
|
1012
|
+
if (options.json) {
|
|
1013
|
+
console.log(JSON.stringify(created));
|
|
1014
|
+
} else {
|
|
1015
|
+
console.log(pc2.green(`Created work unit #${created.id}: ${created.title}`));
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/cli/ui.ts
|
|
1020
|
+
import pc3 from "picocolors";
|
|
1021
|
+
import Table from "cli-table3";
|
|
1022
|
+
function getTerminalWidth() {
|
|
1023
|
+
return process.stdout.columns || 80;
|
|
1024
|
+
}
|
|
1025
|
+
function truncateText(text, maxLength) {
|
|
1026
|
+
if (!text) return "";
|
|
1027
|
+
const clean = text.replace(/\r?\n|\r/g, " ").trim();
|
|
1028
|
+
if (clean.length <= maxLength) return clean;
|
|
1029
|
+
return clean.substring(0, maxLength - 3) + "...";
|
|
1030
|
+
}
|
|
1031
|
+
function colorStatus(status) {
|
|
1032
|
+
switch (status) {
|
|
1033
|
+
case "Planned":
|
|
1034
|
+
return pc3.gray("Planned");
|
|
1035
|
+
case "In Progress":
|
|
1036
|
+
return pc3.blue("In Progress");
|
|
1037
|
+
case "Blocked":
|
|
1038
|
+
return pc3.red("Blocked");
|
|
1039
|
+
case "Completed":
|
|
1040
|
+
return pc3.green("Completed");
|
|
1041
|
+
case "Cancelled":
|
|
1042
|
+
return pc3.strikethrough(pc3.gray("Cancelled"));
|
|
1043
|
+
default:
|
|
1044
|
+
return status;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
function colorType(type) {
|
|
1048
|
+
switch (type) {
|
|
1049
|
+
case "Feature":
|
|
1050
|
+
return pc3.cyan("Feature");
|
|
1051
|
+
case "Bug":
|
|
1052
|
+
return pc3.red("Bug");
|
|
1053
|
+
case "Spike":
|
|
1054
|
+
return pc3.magenta("Spike");
|
|
1055
|
+
case "Refactor":
|
|
1056
|
+
return pc3.yellow("Refactor");
|
|
1057
|
+
case "Improvement":
|
|
1058
|
+
return pc3.green("Improvement");
|
|
1059
|
+
case "Research":
|
|
1060
|
+
return pc3.blue("Research");
|
|
1061
|
+
case "Decision":
|
|
1062
|
+
return pc3.cyan(pc3.bold("Decision"));
|
|
1063
|
+
case "Blocker":
|
|
1064
|
+
return pc3.bgRed(pc3.white("Blocker"));
|
|
1065
|
+
case "Review":
|
|
1066
|
+
return pc3.yellow("Review");
|
|
1067
|
+
case "Idea":
|
|
1068
|
+
return pc3.gray("Idea");
|
|
1069
|
+
default:
|
|
1070
|
+
return type;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function colorPriority(priority) {
|
|
1074
|
+
if (!priority) return pc3.gray("-");
|
|
1075
|
+
switch (priority) {
|
|
1076
|
+
case "High":
|
|
1077
|
+
return pc3.red("High");
|
|
1078
|
+
case "Medium":
|
|
1079
|
+
return pc3.yellow("Medium");
|
|
1080
|
+
case "Low":
|
|
1081
|
+
return pc3.green("Low");
|
|
1082
|
+
default:
|
|
1083
|
+
return priority;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
function formatTable(workUnits) {
|
|
1087
|
+
if (workUnits.length === 0) {
|
|
1088
|
+
return "No work units found.";
|
|
1089
|
+
}
|
|
1090
|
+
const table = new Table({
|
|
1091
|
+
head: [
|
|
1092
|
+
pc3.bold("ID"),
|
|
1093
|
+
pc3.bold("Title"),
|
|
1094
|
+
pc3.bold("Type"),
|
|
1095
|
+
pc3.bold("Status"),
|
|
1096
|
+
pc3.bold("Module"),
|
|
1097
|
+
pc3.bold("Priority"),
|
|
1098
|
+
pc3.bold("Created At")
|
|
1099
|
+
],
|
|
1100
|
+
chars: {
|
|
1101
|
+
top: "",
|
|
1102
|
+
"top-mid": "",
|
|
1103
|
+
"top-left": "",
|
|
1104
|
+
"top-right": "",
|
|
1105
|
+
bottom: "",
|
|
1106
|
+
"bottom-mid": "",
|
|
1107
|
+
"bottom-left": "",
|
|
1108
|
+
"bottom-right": "",
|
|
1109
|
+
left: "",
|
|
1110
|
+
"left-mid": "",
|
|
1111
|
+
mid: "",
|
|
1112
|
+
"mid-mid": "",
|
|
1113
|
+
right: "",
|
|
1114
|
+
"right-mid": "",
|
|
1115
|
+
middle: " "
|
|
1116
|
+
},
|
|
1117
|
+
style: { "padding-left": 0, "padding-right": 2, head: [] }
|
|
1118
|
+
});
|
|
1119
|
+
const width = getTerminalWidth();
|
|
1120
|
+
const titleMaxWidth = Math.max(20, width - 75);
|
|
1121
|
+
for (const unit of workUnits) {
|
|
1122
|
+
const formattedDate = new Date(unit.createdAt).toLocaleDateString(void 0, {
|
|
1123
|
+
month: "short",
|
|
1124
|
+
day: "numeric"
|
|
1125
|
+
});
|
|
1126
|
+
table.push([
|
|
1127
|
+
pc3.gray(`#${unit.id}`),
|
|
1128
|
+
truncateText(unit.title, titleMaxWidth),
|
|
1129
|
+
colorType(unit.type),
|
|
1130
|
+
colorStatus(unit.status),
|
|
1131
|
+
unit.module ? pc3.yellow(unit.module) : pc3.gray("-"),
|
|
1132
|
+
colorPriority(unit.priority),
|
|
1133
|
+
pc3.gray(formattedDate)
|
|
1134
|
+
]);
|
|
1135
|
+
}
|
|
1136
|
+
return table.toString();
|
|
1137
|
+
}
|
|
1138
|
+
function formatWorkUnitDetail(unit) {
|
|
1139
|
+
const lines = [];
|
|
1140
|
+
lines.push(`${pc3.bold(pc3.cyan(`[#${unit.id}] ${unit.title}`))}`);
|
|
1141
|
+
lines.push(`${pc3.gray("--------------------------------------------------")}`);
|
|
1142
|
+
lines.push(`${pc3.bold("Type:")} ${colorType(unit.type)}`);
|
|
1143
|
+
lines.push(`${pc3.bold("Status:")} ${colorStatus(unit.status)}`);
|
|
1144
|
+
if (unit.priority) {
|
|
1145
|
+
lines.push(`${pc3.bold("Priority:")} ${colorPriority(unit.priority)}`);
|
|
1146
|
+
}
|
|
1147
|
+
if (unit.module) {
|
|
1148
|
+
lines.push(`${pc3.bold("Module:")} ${pc3.yellow(unit.module)}`);
|
|
1149
|
+
}
|
|
1150
|
+
if (unit.tags && unit.tags.length > 0) {
|
|
1151
|
+
lines.push(`${pc3.bold("Tags:")} ${unit.tags.map((t) => pc3.blue(t)).join(", ")}`);
|
|
1152
|
+
}
|
|
1153
|
+
if (unit.relatedWorkUnits && unit.relatedWorkUnits.length > 0) {
|
|
1154
|
+
lines.push(
|
|
1155
|
+
`${pc3.bold("Related:")} ${unit.relatedWorkUnits.map((id) => pc3.gray(`#${id}`)).join(", ")}`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
lines.push(`${pc3.bold("Created:")} ${new Date(unit.createdAt).toLocaleString()}`);
|
|
1159
|
+
lines.push(`${pc3.bold("Updated:")} ${new Date(unit.updatedAt).toLocaleString()}`);
|
|
1160
|
+
if (unit.description) {
|
|
1161
|
+
lines.push("");
|
|
1162
|
+
lines.push(`${pc3.bold("Description:")}`);
|
|
1163
|
+
lines.push(unit.description);
|
|
1164
|
+
}
|
|
1165
|
+
if (unit.nextStep) {
|
|
1166
|
+
lines.push("");
|
|
1167
|
+
lines.push(`${pc3.bold("Next Step:")}`);
|
|
1168
|
+
lines.push(pc3.blue(`-> ${unit.nextStep}`));
|
|
1169
|
+
}
|
|
1170
|
+
if (unit.decision) {
|
|
1171
|
+
lines.push("");
|
|
1172
|
+
lines.push(`${pc3.bold("Decision:")}`);
|
|
1173
|
+
lines.push(pc3.green(`\u2714 ${unit.decision}`));
|
|
1174
|
+
}
|
|
1175
|
+
if (unit.blocker) {
|
|
1176
|
+
lines.push("");
|
|
1177
|
+
lines.push(`${pc3.bold("Blocker:")}`);
|
|
1178
|
+
lines.push(pc3.red(`\u2716 ${unit.blocker}`));
|
|
1179
|
+
}
|
|
1180
|
+
if (unit.notes) {
|
|
1181
|
+
lines.push("");
|
|
1182
|
+
lines.push(`${pc3.bold("Notes:")}`);
|
|
1183
|
+
lines.push(unit.notes);
|
|
1184
|
+
}
|
|
1185
|
+
return lines.join("\n");
|
|
1186
|
+
}
|
|
1187
|
+
function formatDashboard(inProgress, recentlyUpdated, needsAttention) {
|
|
1188
|
+
const lines = [];
|
|
1189
|
+
lines.push(pc3.bold(pc3.cyan("=== WORKLOG DASHBOARD ===\n")));
|
|
1190
|
+
lines.push(pc3.bold("CURRENTLY IN PROGRESS:"));
|
|
1191
|
+
if (inProgress.length === 0) {
|
|
1192
|
+
lines.push(pc3.gray(" (No tasks currently in progress)"));
|
|
1193
|
+
} else {
|
|
1194
|
+
for (const u of inProgress) {
|
|
1195
|
+
lines.push(
|
|
1196
|
+
` ${pc3.gray(`#${u.id}`)} ${pc3.bold(u.title)}${u.module ? ` [${u.module}]` : ""}`
|
|
1197
|
+
);
|
|
1198
|
+
if (u.nextStep) {
|
|
1199
|
+
lines.push(` ${pc3.blue("->")} ${pc3.gray(u.nextStep)}`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
lines.push("");
|
|
1204
|
+
lines.push(pc3.bold("NEEDS ATTENTION:"));
|
|
1205
|
+
if (needsAttention.length === 0) {
|
|
1206
|
+
lines.push(pc3.gray(" (No blocked or planned tasks requiring attention)"));
|
|
1207
|
+
} else {
|
|
1208
|
+
for (const u of needsAttention) {
|
|
1209
|
+
const reason = u.status === "Blocked" ? pc3.red(`[Blocked: ${u.blocker || "Unknown"}]`) : pc3.yellow("[Planned]");
|
|
1210
|
+
lines.push(` ${pc3.gray(`#${u.id}`)} ${reason} ${u.title}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
lines.push("");
|
|
1214
|
+
lines.push(pc3.bold("RECENTLY UPDATED:"));
|
|
1215
|
+
if (recentlyUpdated.length === 0) {
|
|
1216
|
+
lines.push(pc3.gray(" (No recent updates)"));
|
|
1217
|
+
} else {
|
|
1218
|
+
for (const u of recentlyUpdated) {
|
|
1219
|
+
const date = new Date(u.updatedAt).toLocaleDateString(void 0, {
|
|
1220
|
+
month: "short",
|
|
1221
|
+
day: "numeric",
|
|
1222
|
+
hour: "2-digit",
|
|
1223
|
+
minute: "2-digit"
|
|
1224
|
+
});
|
|
1225
|
+
lines.push(
|
|
1226
|
+
` ${pc3.gray(`#${u.id}`)} ${pc3.gray(`[${date}]`)} ${u.title} (${colorStatus(u.status)})`
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
return lines.join("\n");
|
|
1231
|
+
}
|
|
1232
|
+
function formatStats(stats) {
|
|
1233
|
+
const lines = [];
|
|
1234
|
+
lines.push(pc3.bold(pc3.cyan("=== ENGINEERING INSIGHTS ===\n")));
|
|
1235
|
+
lines.push(`${pc3.bold("Total Work Units:")} ${stats.totalCount}
|
|
1236
|
+
`);
|
|
1237
|
+
lines.push(pc3.bold("STATUS DISTRIBUTION:"));
|
|
1238
|
+
for (const item of stats.statusDistribution) {
|
|
1239
|
+
lines.push(` ${item.status.padEnd(15)} : ${item.count}`);
|
|
1240
|
+
}
|
|
1241
|
+
lines.push("");
|
|
1242
|
+
lines.push(pc3.bold("TYPE DISTRIBUTION:"));
|
|
1243
|
+
for (const item of stats.typeDistribution) {
|
|
1244
|
+
lines.push(` ${item.type.padEnd(15)} : ${item.count}`);
|
|
1245
|
+
}
|
|
1246
|
+
lines.push("");
|
|
1247
|
+
lines.push(pc3.bold("MODULE DISTRIBUTION:"));
|
|
1248
|
+
if (stats.moduleDistribution.length === 0) {
|
|
1249
|
+
lines.push(" No modules tracked.");
|
|
1250
|
+
} else {
|
|
1251
|
+
for (const item of stats.moduleDistribution) {
|
|
1252
|
+
lines.push(` ${item.module.padEnd(15)} : ${item.count}`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
lines.push("");
|
|
1256
|
+
lines.push(pc3.bold("ACTIVITY IN THE PAST 7 DAYS:"));
|
|
1257
|
+
if (stats.dailyActivity.length === 0) {
|
|
1258
|
+
lines.push(" No work logged in the past 7 days.");
|
|
1259
|
+
} else {
|
|
1260
|
+
for (const item of stats.dailyActivity) {
|
|
1261
|
+
const bar = "*".repeat(item.count);
|
|
1262
|
+
lines.push(` ${item.day} : ${bar} (${item.count})`);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return lines.join("\n");
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// src/cli/commands/list.ts
|
|
1269
|
+
function listCommand(service, options) {
|
|
1270
|
+
const limitVal = options.limit ? Number(options.limit) : 20;
|
|
1271
|
+
let units = service.getRecentWorkUnits(limitVal);
|
|
1272
|
+
if (options.status) {
|
|
1273
|
+
const filterStatus = options.status.toLowerCase();
|
|
1274
|
+
units = units.filter((u) => u.status.toLowerCase() === filterStatus);
|
|
1275
|
+
}
|
|
1276
|
+
if (options.type) {
|
|
1277
|
+
const filterType = options.type.toLowerCase();
|
|
1278
|
+
units = units.filter((u) => u.type.toLowerCase() === filterType);
|
|
1279
|
+
}
|
|
1280
|
+
if (options.json) {
|
|
1281
|
+
console.log(JSON.stringify(units));
|
|
1282
|
+
} else {
|
|
1283
|
+
console.log(formatTable(units));
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/cli/commands/show.ts
|
|
1288
|
+
import pc4 from "picocolors";
|
|
1289
|
+
function showCommand(service, idStr, options) {
|
|
1290
|
+
const id = Number(idStr);
|
|
1291
|
+
if (isNaN(id) || !Number.isInteger(id) || id <= 0) {
|
|
1292
|
+
if (options.json) {
|
|
1293
|
+
console.log(JSON.stringify({ success: false, error: "A valid positive integer ID is required." }));
|
|
1294
|
+
} else {
|
|
1295
|
+
console.error(pc4.red("Error: A valid positive integer ID is required."));
|
|
1296
|
+
}
|
|
1297
|
+
process.exit(1);
|
|
1298
|
+
}
|
|
1299
|
+
const unit = service.getWorkUnit(id);
|
|
1300
|
+
if (!unit) {
|
|
1301
|
+
if (options.json) {
|
|
1302
|
+
console.log(JSON.stringify({ success: false, error: `Work unit #${id} not found.` }));
|
|
1303
|
+
} else {
|
|
1304
|
+
console.error(pc4.red(`Error: Work unit #${id} not found.`));
|
|
1305
|
+
}
|
|
1306
|
+
process.exit(1);
|
|
1307
|
+
}
|
|
1308
|
+
if (options.json) {
|
|
1309
|
+
console.log(JSON.stringify(unit));
|
|
1310
|
+
} else {
|
|
1311
|
+
console.log(formatWorkUnitDetail(unit));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// src/cli/commands/search.ts
|
|
1316
|
+
function searchCommand(service, query, options) {
|
|
1317
|
+
const textQuery = query || "";
|
|
1318
|
+
const results = service.searchWorkUnits(textQuery, {
|
|
1319
|
+
tag: options.tag,
|
|
1320
|
+
module: options.module,
|
|
1321
|
+
status: options.status,
|
|
1322
|
+
type: options.type
|
|
1323
|
+
});
|
|
1324
|
+
if (options.json) {
|
|
1325
|
+
console.log(JSON.stringify(results));
|
|
1326
|
+
} else {
|
|
1327
|
+
console.log(formatTable(results));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// src/cli/commands/today.ts
|
|
1332
|
+
function todayCommand(service, options) {
|
|
1333
|
+
const startOfToday = /* @__PURE__ */ new Date();
|
|
1334
|
+
startOfToday.setHours(0, 0, 0, 0);
|
|
1335
|
+
const isoString = startOfToday.toISOString();
|
|
1336
|
+
const units = service.getCreatedOrUpdatedAfter(isoString);
|
|
1337
|
+
if (options.json) {
|
|
1338
|
+
console.log(JSON.stringify(units));
|
|
1339
|
+
} else {
|
|
1340
|
+
if (units.length === 0) {
|
|
1341
|
+
console.log("No work units logged today.");
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
console.log(formatTable(units));
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/cli/commands/week.ts
|
|
1349
|
+
import pc5 from "picocolors";
|
|
1350
|
+
function weekCommand(service, options) {
|
|
1351
|
+
const sevenDaysAgo = /* @__PURE__ */ new Date();
|
|
1352
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
1353
|
+
sevenDaysAgo.setHours(0, 0, 0, 0);
|
|
1354
|
+
const isoString = sevenDaysAgo.toISOString();
|
|
1355
|
+
const units = service.getCreatedOrUpdatedAfter(isoString);
|
|
1356
|
+
if (options.json) {
|
|
1357
|
+
console.log(JSON.stringify(units));
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (units.length === 0) {
|
|
1361
|
+
console.log("No work units logged in the past 7 days.");
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
const completedFeatures = units.filter((u) => u.type === "Feature" && u.status === "Completed");
|
|
1365
|
+
const fixedBugs = units.filter((u) => u.type === "Bug" && u.status === "Completed");
|
|
1366
|
+
const decisions = units.filter((u) => u.type === "Decision" || u.decision !== null);
|
|
1367
|
+
const blockers = units.filter((u) => u.type === "Blocker" || u.status === "Blocked");
|
|
1368
|
+
const research = units.filter((u) => u.type === "Research" || u.type === "Spike");
|
|
1369
|
+
console.log(pc5.bold(pc5.cyan("=== WEEKLY ENGINEERING SUMMARY ===\n")));
|
|
1370
|
+
console.log(pc5.bold(`Features Completed (${completedFeatures.length}):`));
|
|
1371
|
+
if (completedFeatures.length === 0) console.log(" None");
|
|
1372
|
+
for (const u of completedFeatures) {
|
|
1373
|
+
console.log(` - #${u.id}: ${u.title}`);
|
|
1374
|
+
}
|
|
1375
|
+
console.log("");
|
|
1376
|
+
console.log(pc5.bold(`Bugs Fixed (${fixedBugs.length}):`));
|
|
1377
|
+
if (fixedBugs.length === 0) console.log(" None");
|
|
1378
|
+
for (const u of fixedBugs) {
|
|
1379
|
+
console.log(` - #${u.id}: ${u.title}`);
|
|
1380
|
+
}
|
|
1381
|
+
console.log("");
|
|
1382
|
+
console.log(pc5.bold(`Decisions Made (${decisions.length}):`));
|
|
1383
|
+
if (decisions.length === 0) console.log(" None");
|
|
1384
|
+
for (const u of decisions) {
|
|
1385
|
+
console.log(` - #${u.id}: ${u.title}`);
|
|
1386
|
+
if (u.decision) console.log(` \u2714 ${pc5.green(u.decision)}`);
|
|
1387
|
+
}
|
|
1388
|
+
console.log("");
|
|
1389
|
+
console.log(pc5.bold(`Blockers Encountered (${blockers.length}):`));
|
|
1390
|
+
if (blockers.length === 0) console.log(" None");
|
|
1391
|
+
for (const u of blockers) {
|
|
1392
|
+
console.log(` - #${u.id}: ${u.title}`);
|
|
1393
|
+
if (u.blocker) console.log(` \u2716 ${pc5.red(u.blocker)}`);
|
|
1394
|
+
}
|
|
1395
|
+
console.log("");
|
|
1396
|
+
console.log(pc5.bold(`Research & Spikes (${research.length}):`));
|
|
1397
|
+
if (research.length === 0) console.log(" None");
|
|
1398
|
+
for (const u of research) {
|
|
1399
|
+
console.log(` - #${u.id}: ${u.title} (${u.type})`);
|
|
1400
|
+
}
|
|
1401
|
+
console.log("");
|
|
1402
|
+
const typeCounts = {};
|
|
1403
|
+
const statusCounts = {};
|
|
1404
|
+
for (const u of units) {
|
|
1405
|
+
typeCounts[u.type] = (typeCounts[u.type] || 0) + 1;
|
|
1406
|
+
statusCounts[u.status] = (statusCounts[u.status] || 0) + 1;
|
|
1407
|
+
}
|
|
1408
|
+
console.log(pc5.bold("WORK UNITS BY STATUS:"));
|
|
1409
|
+
for (const [status, count] of Object.entries(statusCounts)) {
|
|
1410
|
+
console.log(` - ${status.padEnd(15)}: ${count}`);
|
|
1411
|
+
}
|
|
1412
|
+
console.log("");
|
|
1413
|
+
console.log(pc5.bold("WORK UNITS BY TYPE:"));
|
|
1414
|
+
for (const [type, count] of Object.entries(typeCounts)) {
|
|
1415
|
+
console.log(` - ${type.padEnd(15)}: ${count}`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/cli/commands/dashboard.ts
|
|
1420
|
+
function dashboardCommand(service, options) {
|
|
1421
|
+
const inProgress = service.getWorkUnitsByStatus("In Progress");
|
|
1422
|
+
const recentlyUpdated = service.getRecentWorkUnits(5);
|
|
1423
|
+
const blocked = service.getWorkUnitsByStatus("Blocked");
|
|
1424
|
+
const planned = service.getWorkUnitsByStatus("Planned");
|
|
1425
|
+
const needsAttention = [...blocked, ...planned].slice(0, 5);
|
|
1426
|
+
if (options.json) {
|
|
1427
|
+
console.log(
|
|
1428
|
+
JSON.stringify({
|
|
1429
|
+
inProgress,
|
|
1430
|
+
recentlyUpdated,
|
|
1431
|
+
needsAttention
|
|
1432
|
+
})
|
|
1433
|
+
);
|
|
1434
|
+
} else {
|
|
1435
|
+
console.log(formatDashboard(inProgress, recentlyUpdated, needsAttention));
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/cli/commands/continue.ts
|
|
1440
|
+
import prompts2 from "prompts";
|
|
1441
|
+
import pc6 from "picocolors";
|
|
1442
|
+
async function continueCommand(service, options) {
|
|
1443
|
+
const unfinished = service.getUnfinishedWorkUnits();
|
|
1444
|
+
if (unfinished.length === 0) {
|
|
1445
|
+
if (options.json) {
|
|
1446
|
+
console.log(JSON.stringify({ success: false, error: "No unfinished work units found." }));
|
|
1447
|
+
} else {
|
|
1448
|
+
console.log(pc6.yellow("No unfinished work units found."));
|
|
1449
|
+
}
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1452
|
+
let selectedUnit = unfinished[0];
|
|
1453
|
+
if (unfinished.length > 1) {
|
|
1454
|
+
if (!process.stdin.isTTY) {
|
|
1455
|
+
selectedUnit = unfinished[0];
|
|
1456
|
+
} else {
|
|
1457
|
+
const choices = unfinished.map((u) => ({
|
|
1458
|
+
title: `#${u.id} - ${u.title} (${u.status})`,
|
|
1459
|
+
value: u
|
|
1460
|
+
}));
|
|
1461
|
+
const response = await prompts2(
|
|
1462
|
+
{
|
|
1463
|
+
type: "select",
|
|
1464
|
+
name: "unit",
|
|
1465
|
+
message: "Select an unfinished work unit to resume:",
|
|
1466
|
+
choices,
|
|
1467
|
+
initial: 0
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
onCancel: () => {
|
|
1471
|
+
if (options.json) {
|
|
1472
|
+
console.log(JSON.stringify({ success: false, error: "Cancelled by user" }));
|
|
1473
|
+
} else {
|
|
1474
|
+
console.log(pc6.yellow("\nCancelled."));
|
|
1475
|
+
}
|
|
1476
|
+
process.exit(1);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
);
|
|
1480
|
+
if (!response.unit) {
|
|
1481
|
+
process.exit(1);
|
|
1482
|
+
}
|
|
1483
|
+
selectedUnit = response.unit;
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const updated = service.updateWorkUnit(selectedUnit.id, {
|
|
1487
|
+
status: "In Progress"
|
|
1488
|
+
});
|
|
1489
|
+
if (options.json) {
|
|
1490
|
+
console.log(JSON.stringify(updated));
|
|
1491
|
+
} else {
|
|
1492
|
+
console.log(
|
|
1493
|
+
pc6.green(
|
|
1494
|
+
`Resumed work unit #${updated.id}: ${updated.title} (Status updated to 'In Progress')`
|
|
1495
|
+
)
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
// src/cli/commands/stats.ts
|
|
1501
|
+
function statsCommand(service, options) {
|
|
1502
|
+
const sevenDaysAgo = /* @__PURE__ */ new Date();
|
|
1503
|
+
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
1504
|
+
sevenDaysAgo.setHours(0, 0, 0, 0);
|
|
1505
|
+
const stats = service.getStats(sevenDaysAgo.toISOString());
|
|
1506
|
+
if (options.json) {
|
|
1507
|
+
console.log(JSON.stringify(stats));
|
|
1508
|
+
} else {
|
|
1509
|
+
console.log(formatStats(stats));
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// src/cli/commands/export.ts
|
|
1514
|
+
import * as fs6 from "fs";
|
|
1515
|
+
import pc7 from "picocolors";
|
|
1516
|
+
|
|
1517
|
+
// src/exporters/markdown.ts
|
|
1518
|
+
function formatSingleMarkdown(u) {
|
|
1519
|
+
const lines = [];
|
|
1520
|
+
lines.push(`# [#${u.id}] ${u.title}
|
|
1521
|
+
`);
|
|
1522
|
+
lines.push(`- **Type:** ${u.type}`);
|
|
1523
|
+
lines.push(`- **Status:** ${u.status}`);
|
|
1524
|
+
if (u.priority) lines.push(`- **Priority:** ${u.priority}`);
|
|
1525
|
+
if (u.module) lines.push(`- **Module:** ${u.module}`);
|
|
1526
|
+
if (u.tags && u.tags.length > 0) lines.push(`- **Tags:** ${u.tags.join(", ")}`);
|
|
1527
|
+
lines.push(`- **Created:** ${u.createdAt}`);
|
|
1528
|
+
lines.push(`- **Updated:** ${u.updatedAt}`);
|
|
1529
|
+
lines.push("");
|
|
1530
|
+
if (u.description) {
|
|
1531
|
+
lines.push("## Description");
|
|
1532
|
+
lines.push(u.description);
|
|
1533
|
+
lines.push("");
|
|
1534
|
+
}
|
|
1535
|
+
if (u.nextStep) {
|
|
1536
|
+
lines.push(`## Next Step
|
|
1537
|
+
-> ${u.nextStep}
|
|
1538
|
+
`);
|
|
1539
|
+
}
|
|
1540
|
+
if (u.decision) {
|
|
1541
|
+
lines.push(`## Decision
|
|
1542
|
+
\u2714 ${u.decision}
|
|
1543
|
+
`);
|
|
1544
|
+
}
|
|
1545
|
+
if (u.blocker) {
|
|
1546
|
+
lines.push(`## Blocker
|
|
1547
|
+
\u2716 ${u.blocker}
|
|
1548
|
+
`);
|
|
1549
|
+
}
|
|
1550
|
+
if (u.notes) {
|
|
1551
|
+
lines.push("## Notes");
|
|
1552
|
+
lines.push(u.notes);
|
|
1553
|
+
lines.push("");
|
|
1554
|
+
}
|
|
1555
|
+
return lines.join("\n");
|
|
1556
|
+
}
|
|
1557
|
+
function exportToMarkdown(units) {
|
|
1558
|
+
if (!units) return "No work units found.\n";
|
|
1559
|
+
if (!Array.isArray(units)) {
|
|
1560
|
+
return formatSingleMarkdown(units);
|
|
1561
|
+
}
|
|
1562
|
+
if (units.length === 0) {
|
|
1563
|
+
return "No work units found.\n";
|
|
1564
|
+
}
|
|
1565
|
+
const lines = [];
|
|
1566
|
+
lines.push("# WorkLog Export\n");
|
|
1567
|
+
const statuses = ["In Progress", "Blocked", "Planned", "Completed", "Cancelled"];
|
|
1568
|
+
for (const status of statuses) {
|
|
1569
|
+
const filtered = units.filter((u) => u.status === status);
|
|
1570
|
+
if (filtered.length === 0) continue;
|
|
1571
|
+
lines.push(`## ${status}`);
|
|
1572
|
+
for (const u of filtered) {
|
|
1573
|
+
const moduleStr = u.module ? ` [${u.module}]` : "";
|
|
1574
|
+
lines.push(`- **#${u.id}**: ${u.title}${moduleStr} (${u.type})`);
|
|
1575
|
+
if (u.nextStep) {
|
|
1576
|
+
lines.push(` - *Next step:* ${u.nextStep}`);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
lines.push("");
|
|
1580
|
+
}
|
|
1581
|
+
return lines.join("\n");
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
// src/exporters/csv.ts
|
|
1585
|
+
function escapeCSV(val) {
|
|
1586
|
+
if (val === null || val === void 0) return "";
|
|
1587
|
+
const str = String(val);
|
|
1588
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
1589
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
1590
|
+
}
|
|
1591
|
+
return str;
|
|
1592
|
+
}
|
|
1593
|
+
function exportToCSV(units) {
|
|
1594
|
+
const list = Array.isArray(units) ? units : [units];
|
|
1595
|
+
const headers = [
|
|
1596
|
+
"ID",
|
|
1597
|
+
"Title",
|
|
1598
|
+
"Type",
|
|
1599
|
+
"Status",
|
|
1600
|
+
"Priority",
|
|
1601
|
+
"Module",
|
|
1602
|
+
"Description",
|
|
1603
|
+
"Next Step",
|
|
1604
|
+
"Decision",
|
|
1605
|
+
"Blocker",
|
|
1606
|
+
"Tags",
|
|
1607
|
+
"Related Work Units",
|
|
1608
|
+
"Notes",
|
|
1609
|
+
"Created At",
|
|
1610
|
+
"Updated At"
|
|
1611
|
+
];
|
|
1612
|
+
const lines = [headers.join(",")];
|
|
1613
|
+
for (const u of list) {
|
|
1614
|
+
const row = [
|
|
1615
|
+
u.id,
|
|
1616
|
+
escapeCSV(u.title),
|
|
1617
|
+
escapeCSV(u.type),
|
|
1618
|
+
escapeCSV(u.status),
|
|
1619
|
+
escapeCSV(u.priority),
|
|
1620
|
+
escapeCSV(u.module),
|
|
1621
|
+
escapeCSV(u.description),
|
|
1622
|
+
escapeCSV(u.nextStep),
|
|
1623
|
+
escapeCSV(u.decision),
|
|
1624
|
+
escapeCSV(u.blocker),
|
|
1625
|
+
escapeCSV(u.tags.join(";")),
|
|
1626
|
+
escapeCSV(u.relatedWorkUnits.join(";")),
|
|
1627
|
+
escapeCSV(u.notes),
|
|
1628
|
+
escapeCSV(u.createdAt),
|
|
1629
|
+
escapeCSV(u.updatedAt)
|
|
1630
|
+
];
|
|
1631
|
+
lines.push(row.join(","));
|
|
1632
|
+
}
|
|
1633
|
+
return lines.join("\n") + "\n";
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// src/exporters/json.ts
|
|
1637
|
+
function exportToJSON(units) {
|
|
1638
|
+
return JSON.stringify(units, null, 2) + "\n";
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
// src/cli/commands/export.ts
|
|
1642
|
+
function exportCommand(service, options) {
|
|
1643
|
+
let data;
|
|
1644
|
+
if (options.id) {
|
|
1645
|
+
const id = Number(options.id);
|
|
1646
|
+
if (isNaN(id) || !Number.isInteger(id) || id <= 0) {
|
|
1647
|
+
console.error(pc7.red("Error: A valid positive integer ID is required."));
|
|
1648
|
+
process.exit(1);
|
|
1649
|
+
}
|
|
1650
|
+
const unit = service.getWorkUnit(id);
|
|
1651
|
+
if (!unit) {
|
|
1652
|
+
console.error(pc7.red(`Error: Work unit #${id} not found.`));
|
|
1653
|
+
process.exit(1);
|
|
1654
|
+
}
|
|
1655
|
+
data = unit;
|
|
1656
|
+
} else {
|
|
1657
|
+
data = service.getRecentWorkUnits(1e6);
|
|
1658
|
+
}
|
|
1659
|
+
let formatted = "";
|
|
1660
|
+
const format = (options.format || "markdown").toLowerCase();
|
|
1661
|
+
switch (format) {
|
|
1662
|
+
case "markdown":
|
|
1663
|
+
case "md":
|
|
1664
|
+
formatted = exportToMarkdown(data);
|
|
1665
|
+
break;
|
|
1666
|
+
case "csv":
|
|
1667
|
+
formatted = exportToCSV(data);
|
|
1668
|
+
break;
|
|
1669
|
+
case "json":
|
|
1670
|
+
formatted = exportToJSON(data);
|
|
1671
|
+
break;
|
|
1672
|
+
default:
|
|
1673
|
+
console.error(
|
|
1674
|
+
pc7.red(
|
|
1675
|
+
`Error: Unsupported format '${options.format}'. Supported: markdown (md), csv, json.`
|
|
1676
|
+
)
|
|
1677
|
+
);
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1680
|
+
if (options.output) {
|
|
1681
|
+
try {
|
|
1682
|
+
fs6.writeFileSync(options.output, formatted, "utf8");
|
|
1683
|
+
console.log(pc7.green(`Exported successfully to ${options.output}`));
|
|
1684
|
+
} catch (error) {
|
|
1685
|
+
console.error(pc7.red(`Error: Failed to write to file '${options.output}': ${error.message}`));
|
|
1686
|
+
process.exit(1);
|
|
1687
|
+
}
|
|
1688
|
+
} else {
|
|
1689
|
+
process.stdout.write(formatted);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/cli/index.ts
|
|
1694
|
+
var program = new Command();
|
|
1695
|
+
program.name("worklog").description("WorkLog - Engineering Context Manager").version("0.1.0").option("--verbose", "enable verbose debug logging").option("--json", "output in machine-readable JSON format");
|
|
1696
|
+
function bootstrap() {
|
|
1697
|
+
const root = getWorkspaceRoot();
|
|
1698
|
+
initLogger(root);
|
|
1699
|
+
const db = connectDatabase(root);
|
|
1700
|
+
runMigrations(db);
|
|
1701
|
+
const repo = new WorkUnitRepository(db);
|
|
1702
|
+
return new WorkUnitService(repo);
|
|
1703
|
+
}
|
|
1704
|
+
function handleError(error) {
|
|
1705
|
+
const isVerbose = program.opts().verbose;
|
|
1706
|
+
const isJson = program.opts().json;
|
|
1707
|
+
const isUserError = error instanceof WorkLogDomainError && error.code !== "DATABASE_ERROR";
|
|
1708
|
+
if (isUserError) {
|
|
1709
|
+
logger.error({ error, code: error.code }, "Domain error occurred");
|
|
1710
|
+
if (isJson) {
|
|
1711
|
+
console.error(JSON.stringify({ success: false, code: error.code, error: error.message }));
|
|
1712
|
+
} else {
|
|
1713
|
+
console.error(pc8.red(`Error: ${error.message}`));
|
|
1714
|
+
if (error.actionRequired) {
|
|
1715
|
+
console.error(pc8.yellow(`Action required: ${error.actionRequired}`));
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
process.exit(1);
|
|
1719
|
+
} else {
|
|
1720
|
+
logger.error({ error }, "Unexpected system error");
|
|
1721
|
+
if (isJson) {
|
|
1722
|
+
console.error(
|
|
1723
|
+
JSON.stringify({ success: false, code: "INTERNAL_ERROR", error: error.message })
|
|
1724
|
+
);
|
|
1725
|
+
} else {
|
|
1726
|
+
console.error(pc8.red(`Unexpected Error: ${error.message}`));
|
|
1727
|
+
if (isVerbose) {
|
|
1728
|
+
console.error(pc8.gray(error.stack || ""));
|
|
1729
|
+
} else {
|
|
1730
|
+
console.error(pc8.yellow("Run with --verbose to view stack traces."));
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
process.exit(2);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
program.command("init").alias("i").description("Initialize a new WorkLog repository").option("--name <name>", "override default project name").action((options) => {
|
|
1737
|
+
try {
|
|
1738
|
+
const json = program.opts().json || options.json;
|
|
1739
|
+
initCommand({ ...options, json });
|
|
1740
|
+
} catch (error) {
|
|
1741
|
+
handleError(error);
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
program.command("add").alias("a").description("Create a new Work Unit").option("-t, --title <title>", "title of the work unit").option(
|
|
1745
|
+
"--type <type>",
|
|
1746
|
+
"type of the work unit (Feature, Bug, Spike, Refactor, Improvement, Research, Decision, Blocker, Review, Idea)"
|
|
1747
|
+
).option(
|
|
1748
|
+
"--status <status>",
|
|
1749
|
+
"status of the work unit (Planned, In Progress, Blocked, Completed, Cancelled)"
|
|
1750
|
+
).option("--priority <priority>", "priority of the work unit (Low, Medium, High)").option("--module <module>", "associated component or code module").option("--tags <tags>", "comma-separated tags list").option("-d, --description <desc>", "detailed description").option("-n, --next <next>", "next step/action").option("--notes <notes>", "implementation notes").action(async (options) => {
|
|
1751
|
+
try {
|
|
1752
|
+
const service = bootstrap();
|
|
1753
|
+
const json = program.opts().json || options.json;
|
|
1754
|
+
await addCommand(service, { ...options, json });
|
|
1755
|
+
} catch (error) {
|
|
1756
|
+
handleError(error);
|
|
1757
|
+
} finally {
|
|
1758
|
+
closeDatabase();
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
program.command("list").alias("ls").description("List recent Work Units").option("-l, --limit <limit>", "limit output count", "20").option("-s, --status <status>", "filter by status").option("-t, --type <type>", "filter by type").action((options) => {
|
|
1762
|
+
try {
|
|
1763
|
+
const service = bootstrap();
|
|
1764
|
+
const json = program.opts().json || options.json;
|
|
1765
|
+
listCommand(service, { ...options, json });
|
|
1766
|
+
} catch (error) {
|
|
1767
|
+
handleError(error);
|
|
1768
|
+
} finally {
|
|
1769
|
+
closeDatabase();
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
program.command("show <id>").alias("sh").description("Show details for a specific Work Unit").action((id, options) => {
|
|
1773
|
+
try {
|
|
1774
|
+
const service = bootstrap();
|
|
1775
|
+
const json = program.opts().json || options.json;
|
|
1776
|
+
showCommand(service, id, { ...options, json });
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
handleError(error);
|
|
1779
|
+
} finally {
|
|
1780
|
+
closeDatabase();
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
program.command("search [query]").alias("s").description("Search Work Units by keyword and category filters").option("--tag <tag>", "filter search results by tag").option("--module <module>", "filter search results by module").option("--status <status>", "filter search results by status").option("--type <type>", "filter search results by type").action((query, options) => {
|
|
1784
|
+
try {
|
|
1785
|
+
const service = bootstrap();
|
|
1786
|
+
const json = program.opts().json || options.json;
|
|
1787
|
+
searchCommand(service, query, { ...options, json });
|
|
1788
|
+
} catch (error) {
|
|
1789
|
+
handleError(error);
|
|
1790
|
+
} finally {
|
|
1791
|
+
closeDatabase();
|
|
1792
|
+
}
|
|
1793
|
+
});
|
|
1794
|
+
program.command("today").alias("t").description("Show work units updated today").action((options) => {
|
|
1795
|
+
try {
|
|
1796
|
+
const service = bootstrap();
|
|
1797
|
+
const json = program.opts().json || options.json;
|
|
1798
|
+
todayCommand(service, { ...options, json });
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
handleError(error);
|
|
1801
|
+
} finally {
|
|
1802
|
+
closeDatabase();
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
program.command("week").alias("w").description("Show weekly engineering summary").action((options) => {
|
|
1806
|
+
try {
|
|
1807
|
+
const service = bootstrap();
|
|
1808
|
+
const json = program.opts().json || options.json;
|
|
1809
|
+
weekCommand(service, { ...options, json });
|
|
1810
|
+
} catch (error) {
|
|
1811
|
+
handleError(error);
|
|
1812
|
+
} finally {
|
|
1813
|
+
closeDatabase();
|
|
1814
|
+
}
|
|
1815
|
+
});
|
|
1816
|
+
program.command("dashboard").alias("db").description("Show clean project dashboard overview").action((options) => {
|
|
1817
|
+
try {
|
|
1818
|
+
const service = bootstrap();
|
|
1819
|
+
const json = program.opts().json || options.json;
|
|
1820
|
+
dashboardCommand(service, { ...options, json });
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
handleError(error);
|
|
1823
|
+
} finally {
|
|
1824
|
+
closeDatabase();
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
program.command("continue").alias("co").description("Resume work on the most recent unfinished task").action(async (options) => {
|
|
1828
|
+
try {
|
|
1829
|
+
const service = bootstrap();
|
|
1830
|
+
const json = program.opts().json || options.json;
|
|
1831
|
+
await continueCommand(service, { ...options, json });
|
|
1832
|
+
} catch (error) {
|
|
1833
|
+
handleError(error);
|
|
1834
|
+
} finally {
|
|
1835
|
+
closeDatabase();
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
program.command("stats").alias("st").description("Show total stats and category distributions").action((options) => {
|
|
1839
|
+
try {
|
|
1840
|
+
const service = bootstrap();
|
|
1841
|
+
const json = program.opts().json || options.json;
|
|
1842
|
+
statsCommand(service, { ...options, json });
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
handleError(error);
|
|
1845
|
+
} finally {
|
|
1846
|
+
closeDatabase();
|
|
1847
|
+
}
|
|
1848
|
+
});
|
|
1849
|
+
program.command("export").alias("ex").description("Export Work Units to Markdown, CSV, or JSON").option("-f, --format <format>", "export format (markdown, csv, json)", "markdown").option("-o, --output <file>", "write output directly to a file").option("--id <id>", "export a single work unit by its ID").action((options) => {
|
|
1850
|
+
try {
|
|
1851
|
+
const service = bootstrap();
|
|
1852
|
+
exportCommand(service, options);
|
|
1853
|
+
} catch (error) {
|
|
1854
|
+
handleError(error);
|
|
1855
|
+
} finally {
|
|
1856
|
+
closeDatabase();
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
program.on("command:*", () => {
|
|
1860
|
+
const isJson = program.opts().json;
|
|
1861
|
+
const command = program.args.join(" ");
|
|
1862
|
+
if (isJson) {
|
|
1863
|
+
console.error(
|
|
1864
|
+
JSON.stringify({ success: false, code: "UNKNOWN_COMMAND", error: `Unknown command: ${command}` })
|
|
1865
|
+
);
|
|
1866
|
+
} else {
|
|
1867
|
+
console.error(pc8.red(`Error: Unknown command: ${command}`));
|
|
1868
|
+
console.error(`Run 'worklog --help' for a list of available commands.`);
|
|
1869
|
+
}
|
|
1870
|
+
process.exit(1);
|
|
1871
|
+
});
|
|
1872
|
+
program.parse(process.argv);
|
|
1873
|
+
//# sourceMappingURL=index.js.map
|