logwork-cli 0.0.0-stage → 1.1.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/LICENSE +11 -0
- package/README.md +330 -2
- package/RELEASE.md +89 -0
- package/bin/logwork.mjs +104 -0
- package/commands/auth.mjs +227 -0
- package/commands/commits.mjs +82 -0
- package/commands/delete.mjs +69 -0
- package/commands/doctor.mjs +24 -0
- package/commands/edit.mjs +88 -0
- package/commands/history.mjs +62 -0
- package/commands/query.mjs +52 -0
- package/commands/quick.mjs +163 -0
- package/commands/reconcile.mjs +55 -0
- package/commands/release.mjs +77 -0
- package/commands/reminder.mjs +172 -0
- package/commands/setup.mjs +508 -0
- package/commands/skill.mjs +91 -0
- package/commands/tui.mjs +50 -0
- package/commands/update.mjs +161 -0
- package/docs/advanced.md +334 -0
- package/docs/mcp-setup.md +368 -0
- package/docs/operations-guide.md +587 -0
- package/docs/recipes.md +259 -0
- package/docs/security.md +151 -0
- package/examples/mcp/antigravity.mcp_config.json +8 -0
- package/examples/mcp/codex.config.toml +5 -0
- package/examples/mcp/copilot.mcp.json +9 -0
- package/lib/agent-skill.mjs +16 -0
- package/lib/api-auth.mjs +1259 -0
- package/lib/api.mjs +1422 -0
- package/lib/apply-ledger.mjs +227 -0
- package/lib/atomic-file.mjs +24 -0
- package/lib/auth-diagnostics.mjs +113 -0
- package/lib/auth-errors.mjs +22 -0
- package/lib/auth-redaction.mjs +78 -0
- package/lib/auth-terminal.mjs +97 -0
- package/lib/auth.mjs +323 -0
- package/lib/batch-parser.mjs +145 -0
- package/lib/batch-workflow.mjs +842 -0
- package/lib/commit-workflow.mjs +402 -0
- package/lib/config.mjs +191 -0
- package/lib/credential-store.mjs +130 -0
- package/lib/diagnostics.mjs +307 -0
- package/lib/entrypoint.mjs +14 -0
- package/lib/file-lock.mjs +44 -0
- package/lib/http.mjs +125 -0
- package/lib/jira-api.mjs +258 -0
- package/lib/jira-auth.mjs +203 -0
- package/lib/jira-workflow.mjs +523 -0
- package/lib/keychain.mjs +105 -0
- package/lib/legacy-migration.mjs +66 -0
- package/lib/list-navigation.mjs +19 -0
- package/lib/logwork-config.mjs +199 -0
- package/lib/logwork-f03-enrich.mjs +41 -0
- package/lib/macos.mjs +33 -0
- package/lib/manual-apply-state.mjs +35 -0
- package/lib/manual-commands.mjs +294 -0
- package/lib/manual-drafts.mjs +154 -0
- package/lib/manual-ink-app.mjs +1654 -0
- package/lib/manual-ink-auth.mjs +349 -0
- package/lib/manual-ink-ui.mjs +675 -0
- package/lib/manual-input.mjs +21 -0
- package/lib/manual-logwork-wizard.mjs +310 -0
- package/lib/manual-mcp.mjs +187 -0
- package/lib/manual-repl.mjs +1037 -0
- package/lib/manual-targets.mjs +78 -0
- package/lib/mcp-server.mjs +833 -0
- package/lib/os-notification.mjs +105 -0
- package/lib/package-info.mjs +17 -0
- package/lib/paths.mjs +68 -0
- package/lib/project-identity.mjs +66 -0
- package/lib/project-mapping-workflow.mjs +309 -0
- package/lib/project-resolver.mjs +373 -0
- package/lib/query-workflow.mjs +553 -0
- package/lib/reconciliation-workflow.mjs +446 -0
- package/lib/release-doctor.mjs +448 -0
- package/lib/reminder-config.mjs +71 -0
- package/lib/reminder-scheduler.mjs +301 -0
- package/lib/reminder-service.mjs +118 -0
- package/lib/reminder-state.mjs +38 -0
- package/lib/reminder-workflow.mjs +299 -0
- package/lib/ro-delete-args.mjs +38 -0
- package/lib/ro-delete-workflow.mjs +115 -0
- package/lib/ro-edit-args.mjs +142 -0
- package/lib/ro-edit-workflow.mjs +632 -0
- package/lib/ro-entry-reader.mjs +56 -0
- package/lib/ro-resubmit-workflow.mjs +134 -0
- package/lib/terminal-auth-prompts.mjs +120 -0
- package/lib/ui.mjs +133 -0
- package/lib/update-service.mjs +350 -0
- package/lib/util.mjs +66 -0
- package/lib/windows-credential-manager.mjs +260 -0
- package/lib/worklog-task-resolver.mjs +113 -0
- package/package.json +73 -4
- package/setup.sh +22 -0
- package/skills/logwork/SKILL.md +43 -0
|
@@ -0,0 +1,1037 @@
|
|
|
1
|
+
import { stdin as defaultInput, stdout as defaultOutput } from 'node:process';
|
|
2
|
+
import { confirm, isCancel } from '@clack/prompts';
|
|
3
|
+
import { getStoredAuthStatus, loginResourceOptimiser } from './auth.mjs';
|
|
4
|
+
import { queryApplyLedger } from './apply-ledger.mjs';
|
|
5
|
+
import { isAuthRequiredError } from './auth-errors.mjs';
|
|
6
|
+
import { applyLogworkBatch, previewLogworkBatch } from './batch-workflow.mjs';
|
|
7
|
+
import {
|
|
8
|
+
applyJiraWorklogBatch,
|
|
9
|
+
previewJiraWorklogBatch
|
|
10
|
+
} from './jira-workflow.mjs';
|
|
11
|
+
import {
|
|
12
|
+
getStoredJiraStatus,
|
|
13
|
+
isJiraAuthRequiredError
|
|
14
|
+
} from './jira-auth.mjs';
|
|
15
|
+
import {
|
|
16
|
+
buildLogworkText,
|
|
17
|
+
buildProjectOverrides,
|
|
18
|
+
formatHours,
|
|
19
|
+
parseTaskLine
|
|
20
|
+
} from './manual-logwork-wizard.mjs';
|
|
21
|
+
import {
|
|
22
|
+
BOTH_TARGET,
|
|
23
|
+
jiraLoginInstruction,
|
|
24
|
+
JIRA_TARGET,
|
|
25
|
+
parseTargetToken,
|
|
26
|
+
previewTargets,
|
|
27
|
+
RO_TARGET,
|
|
28
|
+
targetLabel
|
|
29
|
+
} from './manual-targets.mjs';
|
|
30
|
+
import { findManualCommand, formatManualHelp } from './manual-commands.mjs';
|
|
31
|
+
import {
|
|
32
|
+
buildManualApplyConfirmation,
|
|
33
|
+
buildManualUnbookedConfirmation,
|
|
34
|
+
getManualApplyBlocker,
|
|
35
|
+
manualUnbookedEntryCount
|
|
36
|
+
} from './manual-apply-state.mjs';
|
|
37
|
+
import { runManualInkApp } from './manual-ink-app.mjs';
|
|
38
|
+
import { createManualLineReader } from './manual-input.mjs';
|
|
39
|
+
import { generateDiagnosticsReport } from './diagnostics.mjs';
|
|
40
|
+
import {
|
|
41
|
+
formatMcpClientOptions,
|
|
42
|
+
formatMcpSetup,
|
|
43
|
+
resolveMcpClient,
|
|
44
|
+
resolveMcpClientSelection
|
|
45
|
+
} from './manual-mcp.mjs';
|
|
46
|
+
import {
|
|
47
|
+
enrichRoPreviewWithJiraProjects,
|
|
48
|
+
listLogworkProjects,
|
|
49
|
+
upsertProjectMapping
|
|
50
|
+
} from './project-mapping-workflow.mjs';
|
|
51
|
+
import { queryLogwork } from './query-workflow.mjs';
|
|
52
|
+
import { parseRoEditArgs } from './ro-edit-args.mjs';
|
|
53
|
+
import {
|
|
54
|
+
applyRoLogworkEdit,
|
|
55
|
+
previewRoLogworkEdit
|
|
56
|
+
} from './ro-edit-workflow.mjs';
|
|
57
|
+
import {
|
|
58
|
+
normalizeReconciliationPeriod,
|
|
59
|
+
reconcileLogwork
|
|
60
|
+
} from './reconciliation-workflow.mjs';
|
|
61
|
+
import {
|
|
62
|
+
projectIdentityKey,
|
|
63
|
+
projectMatchesFilter,
|
|
64
|
+
sameProjectIdentity
|
|
65
|
+
} from './project-identity.mjs';
|
|
66
|
+
|
|
67
|
+
export const MANUAL_HELP = formatManualHelp();
|
|
68
|
+
|
|
69
|
+
export function createManualSession() {
|
|
70
|
+
return {
|
|
71
|
+
activeTarget: RO_TARGET,
|
|
72
|
+
previews: {
|
|
73
|
+
[RO_TARGET]: null,
|
|
74
|
+
[JIRA_TARGET]: null
|
|
75
|
+
},
|
|
76
|
+
logworkDraft: null
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function runManualRepl({
|
|
81
|
+
input = defaultInput,
|
|
82
|
+
output = defaultOutput,
|
|
83
|
+
cwd = process.cwd(),
|
|
84
|
+
workflows = defaultWorkflows(),
|
|
85
|
+
prompts = createManualPrompts(),
|
|
86
|
+
showIntro = true,
|
|
87
|
+
updateNotice = ''
|
|
88
|
+
} = {}) {
|
|
89
|
+
if (input?.isTTY && output?.isTTY) {
|
|
90
|
+
await runManualInkApp({
|
|
91
|
+
input,
|
|
92
|
+
output,
|
|
93
|
+
cwd,
|
|
94
|
+
workflows,
|
|
95
|
+
controller: {
|
|
96
|
+
createManualSession,
|
|
97
|
+
executeManualCommand,
|
|
98
|
+
parseManualCommand
|
|
99
|
+
},
|
|
100
|
+
showIntro,
|
|
101
|
+
updateNotice
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const lineReader = createManualLineReader({
|
|
107
|
+
input,
|
|
108
|
+
output
|
|
109
|
+
});
|
|
110
|
+
const session = createManualSession();
|
|
111
|
+
const context = {
|
|
112
|
+
cwd,
|
|
113
|
+
workflows,
|
|
114
|
+
prompts,
|
|
115
|
+
print(message = '') {
|
|
116
|
+
output.write(`${message}\n`);
|
|
117
|
+
},
|
|
118
|
+
readLine(prompt = '') {
|
|
119
|
+
return lineReader.readLine(prompt);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
if (showIntro) {
|
|
125
|
+
context.print('Logwork terminal UI. Type /help for commands.');
|
|
126
|
+
if (updateNotice) {
|
|
127
|
+
context.print(updateNotice);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
while (true) {
|
|
132
|
+
let line;
|
|
133
|
+
try {
|
|
134
|
+
line = await context.readLine('logwork> ');
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error.code === 'ERR_USE_AFTER_CLOSE' || /closed|end of input|EOF/i.test(error.message)) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
let command;
|
|
142
|
+
try {
|
|
143
|
+
command = parseManualCommand(line);
|
|
144
|
+
const result = await executeManualCommand(command, session, context);
|
|
145
|
+
if (result.exit) {
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
context.print(`Error: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
lineReader.close();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function parseManualCommand(line) {
|
|
158
|
+
const input = String(line || '').trim();
|
|
159
|
+
if (!input) {
|
|
160
|
+
return { type: 'noop' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!input.startsWith('/')) {
|
|
164
|
+
throw new Error(`Unknown command: ${input}. Type /help for commands.`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const [rawCommand, ...parts] = input.split(/\s+/);
|
|
168
|
+
const commandInfo = findManualCommand(rawCommand);
|
|
169
|
+
const command = commandInfo?.name;
|
|
170
|
+
|
|
171
|
+
if (command === '/help') {
|
|
172
|
+
return { type: 'help' };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (command === '/auth') {
|
|
176
|
+
return parseTargetCommand('auth', parts, { allowBoth: false });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (command === '/status') {
|
|
180
|
+
return parseTargetCommand('status', parts, { allowBoth: false });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (command === '/diagnostics') {
|
|
184
|
+
return { type: 'diagnostics' };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (command === '/history') {
|
|
188
|
+
return parseTargetCommand('history', parts, { allowBoth: true });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (command === '/projects') {
|
|
192
|
+
return {
|
|
193
|
+
type: 'projects',
|
|
194
|
+
project: parts.join(' ').trim() || undefined
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (command === '/logwork') {
|
|
199
|
+
return parseLogworkCommand(parts);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (command === '/edit-logwork') {
|
|
203
|
+
const options = parseRoEditArgs(parts);
|
|
204
|
+
if (options.yes || options.json) {
|
|
205
|
+
throw new Error('/edit-logwork always previews and confirms interactively; --yes and --json are not supported here.');
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
type: 'edit_ro_logwork',
|
|
209
|
+
logworkId: options.logworkId,
|
|
210
|
+
hours: options.hours,
|
|
211
|
+
taskName: options.taskName,
|
|
212
|
+
description: options.description,
|
|
213
|
+
typeOfWork: options.typeOfWork
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (command === '/mcp') {
|
|
218
|
+
const clientName = parts.join(' ').trim();
|
|
219
|
+
if (!clientName) {
|
|
220
|
+
return {
|
|
221
|
+
type: 'mcp',
|
|
222
|
+
client: undefined
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
const client = resolveMcpClient(clientName);
|
|
226
|
+
if (!client) {
|
|
227
|
+
throw new Error('Unknown MCP client. Use: /mcp cursor | antigravity | copilot | claude-code | codex');
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
type: 'mcp',
|
|
231
|
+
client: client.key
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (command === '/apply') {
|
|
236
|
+
return parseTargetCommand('apply', parts, { allowBoth: true });
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (command === '/query') {
|
|
240
|
+
return {
|
|
241
|
+
type: 'query',
|
|
242
|
+
args: parseQueryArgs(parts.join(' ').trim())
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (command === '/reconcile') {
|
|
247
|
+
if (parts.length > 1) {
|
|
248
|
+
throw new Error('Usage: /reconcile today | yesterday | this-week | last-week | this-month | last-month');
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
type: 'reconcile',
|
|
252
|
+
period: parts[0] ? normalizeReconciliationPeriod(parts[0]) : undefined
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (command === '/map') {
|
|
257
|
+
return parseMapCommand(parts);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
throw new Error(`Unknown command: ${rawCommand}. Type /help for commands.`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function executeManualCommand(command, session, context) {
|
|
264
|
+
const {
|
|
265
|
+
cwd = process.cwd(),
|
|
266
|
+
workflows = defaultWorkflows(),
|
|
267
|
+
prompts = createManualPrompts(),
|
|
268
|
+
print = console.log,
|
|
269
|
+
readLine
|
|
270
|
+
} = context;
|
|
271
|
+
|
|
272
|
+
if (command.type === 'noop') {
|
|
273
|
+
return { exit: false };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (command.type === 'help') {
|
|
277
|
+
print(MANUAL_HELP);
|
|
278
|
+
return { exit: false };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (command.type === 'auth') {
|
|
282
|
+
const target = command.target || await readAuthTarget({ readLine, print });
|
|
283
|
+
if (target === JIRA_TARGET) {
|
|
284
|
+
print(jiraLoginInstruction());
|
|
285
|
+
return { exit: false };
|
|
286
|
+
}
|
|
287
|
+
const result = await workflows.loginResourceOptimiser();
|
|
288
|
+
print(result.summary);
|
|
289
|
+
return { exit: false };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (command.type === 'status') {
|
|
293
|
+
if (command.target === RO_TARGET) {
|
|
294
|
+
const result = await workflows.getStoredAuthStatus();
|
|
295
|
+
print(result.summary);
|
|
296
|
+
return { exit: false };
|
|
297
|
+
}
|
|
298
|
+
if (command.target === JIRA_TARGET) {
|
|
299
|
+
const result = await workflows.getStoredJiraStatus();
|
|
300
|
+
print(result.summary);
|
|
301
|
+
return { exit: false };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const ro = await workflows.getStoredAuthStatus();
|
|
305
|
+
const jira = await workflows.getStoredJiraStatus();
|
|
306
|
+
print([
|
|
307
|
+
'Auth status:',
|
|
308
|
+
`- Resource Optimiser: ${ro.summary}`,
|
|
309
|
+
`- Jira: ${jira.summary}`
|
|
310
|
+
].join('\n'));
|
|
311
|
+
return { exit: false };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (command.type === 'diagnostics') {
|
|
315
|
+
const result = await workflows.generateDiagnosticsReport({ cwd });
|
|
316
|
+
print(result.summary);
|
|
317
|
+
print('Send this sanitized file to the developer. Do not send raw curl logs, cookies, passwords, OTPs, or tokens.');
|
|
318
|
+
return { exit: false };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (command.type === 'history') {
|
|
322
|
+
const result = await workflows.queryApplyLedger({
|
|
323
|
+
target: command.target,
|
|
324
|
+
cwd,
|
|
325
|
+
limit: 20
|
|
326
|
+
});
|
|
327
|
+
print(result.summary);
|
|
328
|
+
return { exit: false };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (command.type === 'query') {
|
|
332
|
+
const result = await runWithInlineAuth(() => workflows.queryLogwork({
|
|
333
|
+
...command.args,
|
|
334
|
+
cwd,
|
|
335
|
+
includeEntries: true
|
|
336
|
+
}), { workflows, print });
|
|
337
|
+
print(result.summary);
|
|
338
|
+
return { exit: false };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (command.type === 'reconcile') {
|
|
342
|
+
const result = await runWithJiraAuthInstructions(() => runWithInlineAuth(() => workflows.reconcileLogwork({
|
|
343
|
+
period: command.period || 'this_week',
|
|
344
|
+
cwd
|
|
345
|
+
}), { workflows, print }));
|
|
346
|
+
print(result.summary);
|
|
347
|
+
return { exit: false };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (command.type === 'projects') {
|
|
351
|
+
const result = await runWithInlineAuth(async () => {
|
|
352
|
+
const projectsResult = await workflows.listLogworkProjects({ cwd });
|
|
353
|
+
const weekly = await workflows.queryLogwork({
|
|
354
|
+
period: 'this_week',
|
|
355
|
+
project: command.project,
|
|
356
|
+
cwd,
|
|
357
|
+
includeEntries: false
|
|
358
|
+
});
|
|
359
|
+
return { projectsResult, weekly };
|
|
360
|
+
}, { workflows, print });
|
|
361
|
+
print(formatProjects(result.projectsResult, result.weekly, command.project));
|
|
362
|
+
return { exit: false };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (command.type === 'map') {
|
|
366
|
+
const ok = await prompts.confirm(`Map ${command.tickets.join(', ')} to projectMemberId ${command.projectMemberId}?`);
|
|
367
|
+
if (!ok) {
|
|
368
|
+
print('Mapping cancelled.');
|
|
369
|
+
return { exit: false };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const result = await runWithInlineAuth(() => workflows.upsertProjectMapping({
|
|
373
|
+
projectMemberId: command.projectMemberId,
|
|
374
|
+
tickets: command.tickets,
|
|
375
|
+
keywords: [],
|
|
376
|
+
scope: 'user',
|
|
377
|
+
confirm: true,
|
|
378
|
+
cwd
|
|
379
|
+
}), { workflows, print });
|
|
380
|
+
print(result.summary);
|
|
381
|
+
return { exit: false };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (command.type === 'logwork') {
|
|
385
|
+
const target = command.target || (command.text ? RO_TARGET : await readLogworkTarget({ readLine, print }));
|
|
386
|
+
const text = command.text || await readPasteBlock({ readLine, print, target });
|
|
387
|
+
const previews = await previewManualText({
|
|
388
|
+
target,
|
|
389
|
+
text,
|
|
390
|
+
projectOverrides: command.projectOverrides || {},
|
|
391
|
+
cwd,
|
|
392
|
+
workflows,
|
|
393
|
+
print
|
|
394
|
+
});
|
|
395
|
+
setSessionPreviews(session, target, previews);
|
|
396
|
+
print(formatManualPreviewSummaries(target, previews));
|
|
397
|
+
return { exit: false };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (command.type === 'edit_ro_logwork') {
|
|
401
|
+
const editPreview = await runWithInlineAuth(() => workflows.previewRoLogworkEdit({
|
|
402
|
+
logworkId: command.logworkId,
|
|
403
|
+
hours: command.hours,
|
|
404
|
+
taskName: command.taskName,
|
|
405
|
+
description: command.description,
|
|
406
|
+
typeOfWork: command.typeOfWork
|
|
407
|
+
}), { workflows, print });
|
|
408
|
+
print(editPreview.summary);
|
|
409
|
+
const confirmed = await prompts.confirm(`Apply RO logwork edit ${editPreview.previewId}?`, false);
|
|
410
|
+
if (!confirmed) {
|
|
411
|
+
print('RO logwork edit cancelled.');
|
|
412
|
+
return { exit: false };
|
|
413
|
+
}
|
|
414
|
+
const result = await runWithInlineAuth(() => workflows.applyRoLogworkEdit({
|
|
415
|
+
preview: editPreview,
|
|
416
|
+
confirm: true,
|
|
417
|
+
cwd
|
|
418
|
+
}), { workflows, print });
|
|
419
|
+
print(result.summary);
|
|
420
|
+
return { exit: false };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (command.type === 'mcp') {
|
|
424
|
+
const client = command.client || await readMcpClient({ readLine, print });
|
|
425
|
+
print(formatMcpSetup({ client }));
|
|
426
|
+
return { exit: false };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (command.type === 'apply') {
|
|
430
|
+
await applyManualTarget({
|
|
431
|
+
target: command.target || session.activeTarget || RO_TARGET,
|
|
432
|
+
session,
|
|
433
|
+
cwd,
|
|
434
|
+
workflows,
|
|
435
|
+
prompts,
|
|
436
|
+
print
|
|
437
|
+
});
|
|
438
|
+
return { exit: false };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
throw new Error(`Unsupported command type: ${command.type}`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function readMcpClient({ readLine, print }) {
|
|
445
|
+
if (typeof readLine !== 'function') {
|
|
446
|
+
throw new Error('/mcp requires an interactive terminal or a client name, for example: /mcp cursor');
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
print(formatMcpClientOptions());
|
|
450
|
+
while (true) {
|
|
451
|
+
const answer = await readLine('mcp client> ');
|
|
452
|
+
const client = resolveMcpClientSelection(answer);
|
|
453
|
+
if (client) {
|
|
454
|
+
return client.key;
|
|
455
|
+
}
|
|
456
|
+
print('Unknown MCP client. Choose a number or type: cursor, antigravity, copilot, claude-code, codex.');
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function readLogworkTarget({ readLine, print }) {
|
|
461
|
+
if (typeof readLine !== 'function') {
|
|
462
|
+
throw new Error('/logwork requires a target when not using the interactive picker: /logwork ro | jira | both');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
print('Choose logwork target: ro, jira, or both.');
|
|
466
|
+
while (true) {
|
|
467
|
+
const target = parseTargetToken(await readLine('target> '));
|
|
468
|
+
if (target) {
|
|
469
|
+
return target;
|
|
470
|
+
}
|
|
471
|
+
print('Unknown target. Use: ro, jira, or both.');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async function readAuthTarget({ readLine, print }) {
|
|
476
|
+
if (typeof readLine !== 'function') {
|
|
477
|
+
throw new Error('/auth requires a target when not using the interactive picker: /auth ro | jira');
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
print('Choose auth target: ro or jira.');
|
|
481
|
+
while (true) {
|
|
482
|
+
const target = parseTargetToken(await readLine('auth target> '), { allowBoth: false });
|
|
483
|
+
if (target) {
|
|
484
|
+
return target;
|
|
485
|
+
}
|
|
486
|
+
print('Unknown target. Use: ro or jira.');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export function createManualPrompts() {
|
|
491
|
+
return {
|
|
492
|
+
async confirm(message, initialValue = true) {
|
|
493
|
+
const answer = await confirm({
|
|
494
|
+
message,
|
|
495
|
+
initialValue
|
|
496
|
+
});
|
|
497
|
+
if (isCancel(answer)) {
|
|
498
|
+
throw new Error('Cancelled.');
|
|
499
|
+
}
|
|
500
|
+
return Boolean(answer);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async function readPasteBlock({ readLine, print, target = RO_TARGET }) {
|
|
506
|
+
if (typeof readLine !== 'function') {
|
|
507
|
+
throw new Error('/logwork requires an interactive terminal.');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
print(`Paste ${targetLabel(target)} weekly logwork text. Finish with /end on its own line.`);
|
|
511
|
+
const lines = [];
|
|
512
|
+
while (true) {
|
|
513
|
+
const line = await readLine('');
|
|
514
|
+
if (line.trim() === '/end') {
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
lines.push(line);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const text = lines.join('\n').trim();
|
|
521
|
+
if (!text) {
|
|
522
|
+
throw new Error('Preview text is empty.');
|
|
523
|
+
}
|
|
524
|
+
return text;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function previewManualText({
|
|
528
|
+
target,
|
|
529
|
+
text,
|
|
530
|
+
projectOverrides = {},
|
|
531
|
+
cwd,
|
|
532
|
+
workflows,
|
|
533
|
+
print
|
|
534
|
+
}) {
|
|
535
|
+
const previews = {};
|
|
536
|
+
if (previewTargets(target).includes(RO_TARGET)) {
|
|
537
|
+
previews[RO_TARGET] = await runWithInlineAuth(() => workflows.previewLogworkBatch({
|
|
538
|
+
text,
|
|
539
|
+
projectOverrides,
|
|
540
|
+
cwd
|
|
541
|
+
}), { workflows, print });
|
|
542
|
+
}
|
|
543
|
+
if (previewTargets(target).includes(JIRA_TARGET)) {
|
|
544
|
+
previews[JIRA_TARGET] = await runWithJiraAuthInstructions(() => workflows.previewJiraWorklogBatch({
|
|
545
|
+
text
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
if (target === BOTH_TARGET && previews[RO_TARGET] && previews[JIRA_TARGET]) {
|
|
549
|
+
previews[RO_TARGET] = enrichRoPreviewWithJiraProjects(previews[RO_TARGET], previews[JIRA_TARGET]);
|
|
550
|
+
}
|
|
551
|
+
return previews;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function setSessionPreviews(session, target, previews) {
|
|
555
|
+
session.activeTarget = target;
|
|
556
|
+
session.previews = {
|
|
557
|
+
...(session.previews || {}),
|
|
558
|
+
...previews
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function getSessionPreview(session, target) {
|
|
563
|
+
return session.previews?.[target] || null;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function clearSessionPreview(session, target) {
|
|
567
|
+
if (session.previews) {
|
|
568
|
+
session.previews[target] = null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function clearSessionDraftIfNoPreviews(session) {
|
|
573
|
+
if (!session.previews?.[RO_TARGET] && !session.previews?.[JIRA_TARGET]) {
|
|
574
|
+
session.logworkDraft = null;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function formatManualPreviewSummaries(target, previews) {
|
|
579
|
+
return previewTargets(target)
|
|
580
|
+
.map((previewTarget) => {
|
|
581
|
+
const preview = previews[previewTarget];
|
|
582
|
+
return [
|
|
583
|
+
`${targetLabel(previewTarget)} preview:`,
|
|
584
|
+
preview?.summary || 'No preview returned.'
|
|
585
|
+
].join('\n');
|
|
586
|
+
})
|
|
587
|
+
.join('\n\n');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function applyManualTarget({
|
|
591
|
+
target,
|
|
592
|
+
flowTarget = target,
|
|
593
|
+
session,
|
|
594
|
+
cwd,
|
|
595
|
+
workflows,
|
|
596
|
+
prompts,
|
|
597
|
+
print
|
|
598
|
+
}) {
|
|
599
|
+
if (target === BOTH_TARGET) {
|
|
600
|
+
let appliedAny = false;
|
|
601
|
+
for (const previewTarget of [RO_TARGET, JIRA_TARGET]) {
|
|
602
|
+
const applied = await applyManualTarget({
|
|
603
|
+
target: previewTarget,
|
|
604
|
+
flowTarget: BOTH_TARGET,
|
|
605
|
+
session,
|
|
606
|
+
cwd,
|
|
607
|
+
workflows,
|
|
608
|
+
prompts,
|
|
609
|
+
print
|
|
610
|
+
});
|
|
611
|
+
appliedAny = appliedAny || applied;
|
|
612
|
+
}
|
|
613
|
+
if (!appliedAny && !getSessionPreview(session, RO_TARGET) && !getSessionPreview(session, JIRA_TARGET)) {
|
|
614
|
+
print('No preview available. Run /logwork first.');
|
|
615
|
+
}
|
|
616
|
+
return appliedAny;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (target === JIRA_TARGET) {
|
|
620
|
+
return applyManualJiraPreview({
|
|
621
|
+
flowTarget,
|
|
622
|
+
session,
|
|
623
|
+
workflows,
|
|
624
|
+
prompts,
|
|
625
|
+
print
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return applyManualRoPreview({
|
|
630
|
+
flowTarget,
|
|
631
|
+
session,
|
|
632
|
+
cwd,
|
|
633
|
+
workflows,
|
|
634
|
+
prompts,
|
|
635
|
+
print
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
async function applyManualRoPreview({
|
|
640
|
+
flowTarget,
|
|
641
|
+
session,
|
|
642
|
+
cwd,
|
|
643
|
+
workflows,
|
|
644
|
+
prompts,
|
|
645
|
+
print
|
|
646
|
+
}) {
|
|
647
|
+
const preview = getSessionPreview(session, RO_TARGET);
|
|
648
|
+
const blocker = getManualApplyBlocker(preview);
|
|
649
|
+
if (blocker) {
|
|
650
|
+
print(blocker);
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const confirmed = await prompts.confirm(buildManualApplyConfirmation(preview));
|
|
655
|
+
if (!confirmed) {
|
|
656
|
+
print('Resource Optimiser apply cancelled.');
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
let allowUnbooked = false;
|
|
661
|
+
if (manualUnbookedEntryCount(preview)) {
|
|
662
|
+
allowUnbooked = await prompts.confirm(buildManualUnbookedConfirmation(preview));
|
|
663
|
+
if (!allowUnbooked) {
|
|
664
|
+
print('Resource Optimiser apply cancelled because unbooked entries were not approved.');
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const result = await runWithInlineAuth(() => workflows.applyLogworkBatch({
|
|
670
|
+
batch: preview,
|
|
671
|
+
confirm: true,
|
|
672
|
+
allowUnbooked,
|
|
673
|
+
cwd,
|
|
674
|
+
ledgerContext: { flowTarget }
|
|
675
|
+
}), { workflows, print });
|
|
676
|
+
print(result.summary);
|
|
677
|
+
if (result.verification?.summary) {
|
|
678
|
+
print('');
|
|
679
|
+
print(result.verification.summary);
|
|
680
|
+
}
|
|
681
|
+
clearSessionPreview(session, RO_TARGET);
|
|
682
|
+
clearSessionDraftIfNoPreviews(session);
|
|
683
|
+
return true;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function applyManualJiraPreview({
|
|
687
|
+
flowTarget,
|
|
688
|
+
session,
|
|
689
|
+
workflows,
|
|
690
|
+
prompts,
|
|
691
|
+
print
|
|
692
|
+
}) {
|
|
693
|
+
const preview = getSessionPreview(session, JIRA_TARGET);
|
|
694
|
+
const blocker = getManualJiraApplyBlocker(preview);
|
|
695
|
+
if (blocker) {
|
|
696
|
+
print(blocker);
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const confirmed = await prompts.confirm(buildManualJiraApplyConfirmation(preview));
|
|
701
|
+
if (!confirmed) {
|
|
702
|
+
print('Jira apply cancelled.');
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const result = await runWithJiraAuthInstructions(() => workflows.applyJiraWorklogBatch({
|
|
707
|
+
batch: preview,
|
|
708
|
+
confirm: true,
|
|
709
|
+
ledgerContext: { flowTarget }
|
|
710
|
+
}));
|
|
711
|
+
print(result.summary);
|
|
712
|
+
clearSessionPreview(session, JIRA_TARGET);
|
|
713
|
+
clearSessionDraftIfNoPreviews(session);
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function getManualJiraApplyBlocker(preview) {
|
|
718
|
+
if (!preview) {
|
|
719
|
+
return 'No Jira preview available. Run /logwork jira first.';
|
|
720
|
+
}
|
|
721
|
+
if (preview.errors?.length) {
|
|
722
|
+
return 'Cannot apply Jira preview with parse errors.';
|
|
723
|
+
}
|
|
724
|
+
const blocked = (preview.entries || []).filter((entry) => entry.status !== 'ready');
|
|
725
|
+
if (blocked.length) {
|
|
726
|
+
return `Cannot apply Jira preview with ${blocked.length} blocked entries.`;
|
|
727
|
+
}
|
|
728
|
+
if (!(preview.entries || []).length) {
|
|
729
|
+
return 'Cannot apply Jira preview with no entries.';
|
|
730
|
+
}
|
|
731
|
+
return '';
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function buildManualJiraApplyConfirmation(preview) {
|
|
735
|
+
const entries = preview.entries || [];
|
|
736
|
+
const hours = entries.reduce((sum, entry) => sum + Number(entry.hours || 0), 0);
|
|
737
|
+
const issues = [...new Set(entries.map((entry) => entry.issueKey).filter(Boolean))];
|
|
738
|
+
return `Apply Jira worklog preview with ${entries.length} entries, ${formatHours(hours)}h total, issues ${issues.join(', ') || 'unknown'}?`;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export async function previewManualLogworkDraft({
|
|
742
|
+
date,
|
|
743
|
+
project,
|
|
744
|
+
tasks,
|
|
745
|
+
cwd = process.cwd(),
|
|
746
|
+
workflows = defaultWorkflows(),
|
|
747
|
+
print = console.log
|
|
748
|
+
}) {
|
|
749
|
+
const text = buildLogworkText({ date, tasks });
|
|
750
|
+
const projectOverrides = buildProjectOverrides({
|
|
751
|
+
date,
|
|
752
|
+
tasks,
|
|
753
|
+
projectMemberId: project.projectMemberId
|
|
754
|
+
});
|
|
755
|
+
return runWithInlineAuth(() => workflows.previewLogworkBatch({
|
|
756
|
+
text,
|
|
757
|
+
projectOverrides,
|
|
758
|
+
cwd
|
|
759
|
+
}), { workflows, print });
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export function addDraftTask({ draft, line }) {
|
|
763
|
+
const task = parseTaskLine(line);
|
|
764
|
+
return {
|
|
765
|
+
...draft,
|
|
766
|
+
tasks: [...(draft.tasks || []), task]
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export function removeDraftTask({ draft, index }) {
|
|
771
|
+
const taskIndex = Number(index) - 1;
|
|
772
|
+
if (!Number.isInteger(taskIndex) || taskIndex < 0 || taskIndex >= (draft.tasks || []).length) {
|
|
773
|
+
throw new Error(`No task found at index ${index}.`);
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
...draft,
|
|
777
|
+
tasks: draft.tasks.filter((_, currentIndex) => currentIndex !== taskIndex)
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function runWithInlineAuth(action, { workflows, print }) {
|
|
782
|
+
try {
|
|
783
|
+
return await action();
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (!isAuthRequiredError(error)) {
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
print('Resource Optimiser authentication required. Starting terminal auth...');
|
|
789
|
+
const result = await workflows.loginResourceOptimiser();
|
|
790
|
+
print(result.summary);
|
|
791
|
+
return action();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
async function runWithJiraAuthInstructions(action) {
|
|
796
|
+
try {
|
|
797
|
+
return await action();
|
|
798
|
+
} catch (error) {
|
|
799
|
+
if (!isJiraAuthRequiredError(error)) {
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
throw new Error(jiraLoginInstruction());
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function parseQueryArgs(value) {
|
|
807
|
+
if (!value || value === 'today') {
|
|
808
|
+
return { period: 'today' };
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
if (value === 'yesterday') {
|
|
812
|
+
return { period: 'yesterday' };
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (value === 'this-week' || value === 'this_week' || value === 'week') {
|
|
816
|
+
return { period: 'this_week' };
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (value === 'last-week' || value === 'last_week') {
|
|
820
|
+
return { period: 'last_week' };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (value === 'this-month' || value === 'this_month' || value === 'month') {
|
|
824
|
+
return { period: 'this_month' };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (value === 'last-month' || value === 'last_month') {
|
|
828
|
+
return { period: 'last_month' };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
throw new Error('Usage: /query today | yesterday | this-week | last-week | this-month | last-month');
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function parseLogworkCommand(parts) {
|
|
835
|
+
const first = parts[0];
|
|
836
|
+
const target = parseTargetToken(first);
|
|
837
|
+
if (!target) {
|
|
838
|
+
return {
|
|
839
|
+
type: 'logwork',
|
|
840
|
+
text: parts.join(' ').trim() || undefined
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return {
|
|
845
|
+
type: 'logwork',
|
|
846
|
+
target,
|
|
847
|
+
text: parts.slice(1).join(' ').trim() || undefined
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function parseTargetCommand(type, parts, {
|
|
852
|
+
allowBoth
|
|
853
|
+
} = {}) {
|
|
854
|
+
if (!parts.length) {
|
|
855
|
+
return {
|
|
856
|
+
type
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const target = parseTargetToken(parts[0], { allowBoth });
|
|
861
|
+
if (!target) {
|
|
862
|
+
throw new Error(`Usage: /${type} ${allowBoth ? 'ro | jira | both' : 'ro | jira'}`);
|
|
863
|
+
}
|
|
864
|
+
if (parts.length > 1) {
|
|
865
|
+
throw new Error(`Usage: /${type} ${allowBoth ? 'ro | jira | both' : 'ro | jira'}`);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
return {
|
|
869
|
+
type,
|
|
870
|
+
target
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function parseMapCommand(parts) {
|
|
875
|
+
if (parts.length < 2) {
|
|
876
|
+
throw new Error('Usage: /map <TICKET_PREFIX[,PREFIX...]> <projectMemberId>');
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const tickets = parts[0]
|
|
880
|
+
.split(',')
|
|
881
|
+
.map((ticket) => ticket.trim().toUpperCase())
|
|
882
|
+
.filter(Boolean);
|
|
883
|
+
const projectMemberId = parts[1];
|
|
884
|
+
|
|
885
|
+
if (!tickets.length || !projectMemberId) {
|
|
886
|
+
throw new Error('Usage: /map <TICKET_PREFIX[,PREFIX...]> <projectMemberId>');
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
return {
|
|
890
|
+
type: 'map',
|
|
891
|
+
tickets,
|
|
892
|
+
projectMemberId
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
export function formatProjects(result, weekly = null, projectFilter = undefined) {
|
|
897
|
+
const lines = [result.summary];
|
|
898
|
+
const projects = filterProjects(result.projects || [], projectFilter);
|
|
899
|
+
const chartedProjectKeys = new Set();
|
|
900
|
+
const chartBlocks = formatWeeklyProjectCharts(projects, weekly, projectFilter, chartedProjectKeys);
|
|
901
|
+
|
|
902
|
+
if (chartBlocks.length) {
|
|
903
|
+
lines.push('');
|
|
904
|
+
lines.push('This week:');
|
|
905
|
+
lines.push(...chartBlocks);
|
|
906
|
+
} else if (projectFilter) {
|
|
907
|
+
lines.push('');
|
|
908
|
+
lines.push(`No weekly booking/logwork chart found for "${projectFilter}".`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const otherProjects = projects.filter((project) => !chartedProjectKeys.has(projectIdentityKey(project)));
|
|
912
|
+
if (otherProjects.length) {
|
|
913
|
+
lines.push('');
|
|
914
|
+
lines.push(chartBlocks.length ? 'Other memberships:' : 'Projects:');
|
|
915
|
+
for (const project of otherProjects) {
|
|
916
|
+
lines.push(`- ${project.projectMemberId}: ${project.projectName} (projectId: ${project.projectId ?? 'unknown'})`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (result.mappings?.length) {
|
|
921
|
+
lines.push('');
|
|
922
|
+
lines.push('Mappings:');
|
|
923
|
+
for (const mapping of result.mappings) {
|
|
924
|
+
lines.push(`- ${mapping.tickets.join(', ')} -> ${mapping.projectName}${mapping.projectMemberId ? ` (${mapping.projectMemberId})` : ''}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return lines.join('\n');
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function formatWeeklyProjectCharts(projects, weekly, projectFilter, chartedProjectKeys = new Set()) {
|
|
932
|
+
if (!weekly?.projects?.length) {
|
|
933
|
+
return [];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const lines = [];
|
|
937
|
+
const weeklyProjects = weekly.projects.filter((project) => (
|
|
938
|
+
!projectFilter || projectMatchesFilter(project, projectFilter)
|
|
939
|
+
));
|
|
940
|
+
|
|
941
|
+
for (const weeklyProject of weeklyProjects) {
|
|
942
|
+
const membership = projects.find((project) => sameProjectIdentity(project, weeklyProject)) || weeklyProject;
|
|
943
|
+
const key = projectIdentityKey(membership);
|
|
944
|
+
chartedProjectKeys.add(key);
|
|
945
|
+
if (lines.length) {
|
|
946
|
+
lines.push('');
|
|
947
|
+
}
|
|
948
|
+
lines.push(...formatProjectChart({
|
|
949
|
+
project: {
|
|
950
|
+
...membership,
|
|
951
|
+
bookedHours: weeklyProject.bookedHours,
|
|
952
|
+
loggedHours: weeklyProject.loggedHours
|
|
953
|
+
},
|
|
954
|
+
days: projectDays(weekly?.days || [], weeklyProject)
|
|
955
|
+
}));
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
return lines;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export function formatProjectChart({ project, days = [], width = 20 }) {
|
|
962
|
+
const bookedHours = Number(project.bookedHours || 0);
|
|
963
|
+
const loggedHours = Number(project.loggedHours || 0);
|
|
964
|
+
const ratio = bookedHours > 0 ? loggedHours / bookedHours : (loggedHours > 0 ? 1 : 0);
|
|
965
|
+
const filled = Math.max(0, Math.min(width, Math.round(Math.min(ratio, 1) * width)));
|
|
966
|
+
const percent = bookedHours > 0 ? `${Math.round(ratio * 100)}%` : (loggedHours > 0 ? 'unbooked' : '0%');
|
|
967
|
+
const over = bookedHours > 0 && loggedHours > bookedHours
|
|
968
|
+
? ` (+${formatHours(loggedHours - bookedHours)}h over)`
|
|
969
|
+
: '';
|
|
970
|
+
|
|
971
|
+
return [
|
|
972
|
+
project.projectName,
|
|
973
|
+
`memberId: ${project.projectMemberId ?? 'unknown'} · projectId: ${project.projectId ?? 'unknown'}`,
|
|
974
|
+
`This week: ${formatHours(loggedHours)}h logged / ${formatHours(bookedHours)}h booked${over}`,
|
|
975
|
+
`[${'█'.repeat(filled)}${'░'.repeat(width - filled)}] ${percent}`,
|
|
976
|
+
formatDayChips(days)
|
|
977
|
+
];
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function projectDays(days, project) {
|
|
981
|
+
return days
|
|
982
|
+
.map((day) => {
|
|
983
|
+
const matching = (day.projects || []).find((candidate) => sameProjectIdentity(candidate, project));
|
|
984
|
+
if (!matching) {
|
|
985
|
+
return null;
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
date: day.date,
|
|
989
|
+
bookedHours: matching.bookedHours,
|
|
990
|
+
loggedHours: matching.loggedHours
|
|
991
|
+
};
|
|
992
|
+
})
|
|
993
|
+
.filter(Boolean);
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function formatDayChips(days) {
|
|
997
|
+
if (!days.length) {
|
|
998
|
+
return 'No booking/logwork found this week.';
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return days.map((day) => (
|
|
1002
|
+
`${weekdayName(day.date)} ${formatHours(day.loggedHours)}/${formatHours(day.bookedHours)}`
|
|
1003
|
+
)).join(' ');
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function weekdayName(localDateISO) {
|
|
1007
|
+
const [year, month, day] = String(localDateISO).split('-').map(Number);
|
|
1008
|
+
const date = new Date(year, month - 1, day);
|
|
1009
|
+
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][date.getDay()] || localDateISO;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function filterProjects(projects, projectFilter) {
|
|
1013
|
+
if (!projectFilter) {
|
|
1014
|
+
return projects;
|
|
1015
|
+
}
|
|
1016
|
+
return projects.filter((project) => projectMatchesFilter(project, projectFilter));
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function defaultWorkflows() {
|
|
1020
|
+
return {
|
|
1021
|
+
queryLogwork,
|
|
1022
|
+
reconcileLogwork,
|
|
1023
|
+
previewLogworkBatch,
|
|
1024
|
+
applyLogworkBatch,
|
|
1025
|
+
previewRoLogworkEdit,
|
|
1026
|
+
applyRoLogworkEdit,
|
|
1027
|
+
previewJiraWorklogBatch,
|
|
1028
|
+
applyJiraWorklogBatch,
|
|
1029
|
+
listLogworkProjects,
|
|
1030
|
+
upsertProjectMapping,
|
|
1031
|
+
loginResourceOptimiser,
|
|
1032
|
+
getStoredAuthStatus,
|
|
1033
|
+
getStoredJiraStatus,
|
|
1034
|
+
queryApplyLedger,
|
|
1035
|
+
generateDiagnosticsReport
|
|
1036
|
+
};
|
|
1037
|
+
}
|