logwork-cli 1.2.0 → 1.3.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/README.md +37 -25
- package/RELEASE.md +3 -2
- package/bin/logwork.mjs +2 -2
- package/commands/commits.mjs +78 -14
- package/commands/mcp.mjs +266 -0
- package/commands/setup.mjs +161 -40
- package/docs/advanced.md +7 -6
- package/docs/mcp-setup.md +80 -28
- package/docs/operations-guide.md +18 -3
- package/docs/recipes.md +18 -5
- package/docs/security.md +20 -1
- package/examples/mcp/antigravity.mcp_config.json +2 -2
- package/examples/mcp/codex.config.toml +2 -2
- package/examples/mcp/copilot.mcp.json +2 -2
- package/lib/commit-workflow.mjs +160 -5
- package/lib/diagnostics.mjs +72 -9
- package/lib/launcher.mjs +303 -0
- package/lib/manual-commands.mjs +2 -2
- package/lib/manual-ink-app.mjs +34 -1
- package/lib/manual-ink-interaction.mjs +12 -0
- package/lib/manual-ink-ui.mjs +38 -0
- package/lib/manual-mcp.mjs +68 -22
- package/lib/manual-repl.mjs +147 -34
- package/lib/mcp-install.mjs +930 -0
- package/lib/mcp-server.mjs +2 -2
- package/lib/node-runtime.mjs +52 -12
- package/lib/project-mapping-workflow.mjs +2 -20
- package/lib/query-workflow.mjs +19 -0
- package/lib/reminder-scheduler.mjs +34 -18
- package/lib/reminder-service.mjs +18 -4
- package/lib/repo-project-suggestions.mjs +285 -0
- package/lib/ui.mjs +44 -0
- package/package.json +1 -1
- package/skills/logwork/SKILL.md +6 -1
package/lib/manual-repl.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stdin as defaultInput, stdout as defaultOutput } from 'node:process';
|
|
2
2
|
import { resolve as resolvePath } from 'node:path';
|
|
3
|
-
import { confirm, isCancel } from '@clack/prompts';
|
|
3
|
+
import { confirm, isCancel, select } from '@clack/prompts';
|
|
4
4
|
import { getStoredAuthStatus, loginResourceOptimiser } from './auth.mjs';
|
|
5
5
|
import { queryApplyLedger } from './apply-ledger.mjs';
|
|
6
6
|
import { isAuthRequiredError } from './auth-errors.mjs';
|
|
@@ -49,8 +49,11 @@ import {
|
|
|
49
49
|
import {
|
|
50
50
|
enrichRoPreviewWithJiraProjects,
|
|
51
51
|
listLogworkProjects,
|
|
52
|
+
resolveProjectSelection,
|
|
52
53
|
upsertProjectMapping
|
|
53
54
|
} from './project-mapping-workflow.mjs';
|
|
55
|
+
import { suggestProjectsForRepo } from './commit-workflow.mjs';
|
|
56
|
+
import { repoProjectFromOption, repoProjectSelectOptions } from './ui.mjs';
|
|
54
57
|
import { queryLogwork } from './query-workflow.mjs';
|
|
55
58
|
import { parseRoEditArgs } from './ro-edit-args.mjs';
|
|
56
59
|
import {
|
|
@@ -366,26 +369,7 @@ export async function executeManualCommand(command, session, context) {
|
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
if (command.type === 'map') {
|
|
369
|
-
|
|
370
|
-
...command.tickets,
|
|
371
|
-
...(command.repos || []).map((repo) => `repo ${resolvePath(cwd, repo)}`)
|
|
372
|
-
].join(', ');
|
|
373
|
-
const ok = await prompts.confirm(`Map ${subject} to projectMemberId ${command.projectMemberId}?`);
|
|
374
|
-
if (!ok) {
|
|
375
|
-
print('Mapping cancelled.');
|
|
376
|
-
return { exit: false };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const result = await runWithInlineAuth(() => workflows.upsertProjectMapping({
|
|
380
|
-
projectMemberId: command.projectMemberId,
|
|
381
|
-
tickets: command.tickets,
|
|
382
|
-
keywords: [],
|
|
383
|
-
...(command.repos?.length ? { repos: command.repos } : {}),
|
|
384
|
-
scope: 'user',
|
|
385
|
-
confirm: true,
|
|
386
|
-
cwd
|
|
387
|
-
}), { workflows, print });
|
|
388
|
-
print(result.summary);
|
|
372
|
+
await executeMapCommand(command, { cwd, workflows, prompts, print });
|
|
389
373
|
return { exit: false };
|
|
390
374
|
}
|
|
391
375
|
|
|
@@ -506,10 +490,115 @@ export function createManualPrompts() {
|
|
|
506
490
|
throw new Error('Cancelled.');
|
|
507
491
|
}
|
|
508
492
|
return Boolean(answer);
|
|
493
|
+
},
|
|
494
|
+
// { message, options: [{ value, label, hint }], initialValue } -> value,
|
|
495
|
+
// or null when cancelled.
|
|
496
|
+
async select({ message, options, initialValue }) {
|
|
497
|
+
const answer = await select({ message, options, initialValue });
|
|
498
|
+
return isCancel(answer) ? null : answer;
|
|
509
499
|
}
|
|
510
500
|
};
|
|
511
501
|
}
|
|
512
502
|
|
|
503
|
+
// /map with a project member id saves it as before. Otherwise the project is
|
|
504
|
+
// chosen by name (case-insensitive, then a unique substring) or, with no name
|
|
505
|
+
// or an ambiguous one, from a picker of member projects; for a repo the
|
|
506
|
+
// picker lists the suggested projects first.
|
|
507
|
+
async function executeMapCommand(command, { cwd, workflows, prompts, print }) {
|
|
508
|
+
const subject = [
|
|
509
|
+
...command.tickets,
|
|
510
|
+
...(command.repos || []).map((repo) => `repo ${resolvePath(cwd, repo)}`)
|
|
511
|
+
].join(', ');
|
|
512
|
+
const save = (project) => runWithInlineAuth(() => workflows.upsertProjectMapping({
|
|
513
|
+
projectMemberId: project.projectMemberId,
|
|
514
|
+
...(project.projectMemberId === undefined ? { projectName: project.projectName } : {}),
|
|
515
|
+
tickets: command.tickets,
|
|
516
|
+
keywords: [],
|
|
517
|
+
...(command.repos?.length ? { repos: command.repos } : {}),
|
|
518
|
+
scope: 'user',
|
|
519
|
+
confirm: true,
|
|
520
|
+
cwd
|
|
521
|
+
}), { workflows, print });
|
|
522
|
+
|
|
523
|
+
if (command.projectMemberId !== undefined) {
|
|
524
|
+
const ok = await prompts.confirm(`Map ${subject} to projectMemberId ${command.projectMemberId}?`);
|
|
525
|
+
if (!ok) {
|
|
526
|
+
print('Mapping cancelled.');
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
print((await save({ projectMemberId: command.projectMemberId })).summary);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const { projects = [] } = await runWithInlineAuth(() => workflows.listLogworkProjects({ cwd }), { workflows, print });
|
|
534
|
+
if (!projects.length) {
|
|
535
|
+
throw new Error('No Resource Optimiser project memberships found to map to.');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
let project = null;
|
|
539
|
+
let pickerProjects = projects;
|
|
540
|
+
let pickerNote = '';
|
|
541
|
+
if (command.projectQuery) {
|
|
542
|
+
const resolution = resolveProjectSelection(projects, { projectName: command.projectQuery });
|
|
543
|
+
project = resolution.project || null;
|
|
544
|
+
if (!project) {
|
|
545
|
+
pickerNote = `${resolution.reason}.`;
|
|
546
|
+
pickerProjects = resolution.candidates?.length ? resolution.candidates : projects;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (project) {
|
|
551
|
+
const ok = await prompts.confirm(`Map ${subject} to ${project.projectName}?`);
|
|
552
|
+
if (!ok) {
|
|
553
|
+
print('Mapping cancelled.');
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
let suggestions = [];
|
|
558
|
+
if (command.repos?.length && !command.projectQuery) {
|
|
559
|
+
const suggested = await runWithInlineAuth(() => workflows.suggestProjectsForRepo({
|
|
560
|
+
repo: command.repos[0],
|
|
561
|
+
projects,
|
|
562
|
+
cwd
|
|
563
|
+
}), { workflows, print }).catch((error) => {
|
|
564
|
+
if (isAuthRequiredError(error)) {
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
pickerNote = `No suggestions: ${error.message}`;
|
|
568
|
+
return null;
|
|
569
|
+
});
|
|
570
|
+
suggestions = suggested?.suggestions || [];
|
|
571
|
+
}
|
|
572
|
+
if (pickerNote) {
|
|
573
|
+
print(pickerNote);
|
|
574
|
+
}
|
|
575
|
+
project = await pickMapProject({ subject, suggestions, projects: pickerProjects, prompts, print });
|
|
576
|
+
if (!project) {
|
|
577
|
+
print('Mapping cancelled.');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
print((await save(project)).summary);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
async function pickMapProject({ subject, suggestions, projects, prompts, print }) {
|
|
586
|
+
const { items, options, initialValue } = repoProjectSelectOptions(suggestions, projects, { skipLabel: 'Cancel' });
|
|
587
|
+
if (typeof prompts.select === 'function') {
|
|
588
|
+
return repoProjectFromOption(items, await prompts.select({
|
|
589
|
+
message: `Which RO project should ${subject} map to?`,
|
|
590
|
+
options,
|
|
591
|
+
initialValue
|
|
592
|
+
}));
|
|
593
|
+
}
|
|
594
|
+
// Without a picker, list the projects; the user re-runs /map with a name.
|
|
595
|
+
print([
|
|
596
|
+
`Which RO project should ${subject} map to? Re-run /map with the project name:`,
|
|
597
|
+
...items.map((item, index) => ` ${index + 1}. ${item.project.projectName}${item.suggestion ? ` (${item.suggestion.reasons.join('; ')})` : ''}`)
|
|
598
|
+
].join('\n'));
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
513
602
|
async function readPasteBlock({ readLine, print, target = RO_TARGET }) {
|
|
514
603
|
if (typeof readLine !== 'function') {
|
|
515
604
|
throw new Error('/logwork requires an interactive terminal.');
|
|
@@ -889,26 +978,21 @@ function parseTargetCommand(type, parts, {
|
|
|
889
978
|
};
|
|
890
979
|
}
|
|
891
980
|
|
|
892
|
-
const MAP_USAGE = 'Usage: /map <TICKET_PREFIX[,PREFIX...]>
|
|
981
|
+
const MAP_USAGE = 'Usage: /map <TICKET_PREFIX[,PREFIX...]> [project name or id] | /map --repo [path] [project name or id]';
|
|
982
|
+
const PATH_LIKE = /^(?:\.{1,2}(?:[\\/]|$)|[\\/]|~(?:[\\/]|$)|[A-Za-z]:[\\/])/;
|
|
893
983
|
|
|
894
984
|
function parseMapCommand(parts) {
|
|
895
985
|
if (parts[0] === '--repo') {
|
|
896
|
-
|
|
897
|
-
// quoted.
|
|
898
|
-
const projectMemberId = parts.length > 1 ? parts[parts.length - 1] : '';
|
|
899
|
-
const repo = parts.slice(1, -1).join(' ').replace(/^(["'])(.*)\1$/, '$2').trim() || '.';
|
|
900
|
-
if (!projectMemberId || projectMemberId.startsWith('-')) {
|
|
901
|
-
throw new Error(MAP_USAGE);
|
|
902
|
-
}
|
|
986
|
+
const { repo, project } = splitRepoAndProject(parts.slice(1));
|
|
903
987
|
return {
|
|
904
988
|
type: 'map',
|
|
905
989
|
tickets: [],
|
|
906
990
|
repos: [repo],
|
|
907
|
-
|
|
991
|
+
...mapProjectFields(project)
|
|
908
992
|
};
|
|
909
993
|
}
|
|
910
994
|
|
|
911
|
-
if (parts.length
|
|
995
|
+
if (!parts.length || parts[0].startsWith('-')) {
|
|
912
996
|
throw new Error(MAP_USAGE);
|
|
913
997
|
}
|
|
914
998
|
|
|
@@ -916,19 +1000,47 @@ function parseMapCommand(parts) {
|
|
|
916
1000
|
.split(',')
|
|
917
1001
|
.map((ticket) => ticket.trim().toUpperCase())
|
|
918
1002
|
.filter(Boolean);
|
|
919
|
-
const projectMemberId = parts[1];
|
|
920
1003
|
|
|
921
|
-
if (!tickets.length
|
|
1004
|
+
if (!tickets.length) {
|
|
922
1005
|
throw new Error(MAP_USAGE);
|
|
923
1006
|
}
|
|
924
1007
|
|
|
925
1008
|
return {
|
|
926
1009
|
type: 'map',
|
|
927
1010
|
tickets,
|
|
928
|
-
|
|
1011
|
+
...mapProjectFields(parts.slice(1).join(' ').trim())
|
|
929
1012
|
};
|
|
930
1013
|
}
|
|
931
1014
|
|
|
1015
|
+
// "/map --repo [path] [project]": the path defaults to the current folder. A
|
|
1016
|
+
// path starts with ".", "/", "~" or a drive letter, or is quoted (for spaces);
|
|
1017
|
+
// anything else is the project. The old form "<path> <projectMemberId>" still
|
|
1018
|
+
// reads any path.
|
|
1019
|
+
function splitRepoAndProject(tokens) {
|
|
1020
|
+
const rest = tokens.join(' ').trim();
|
|
1021
|
+
const quoted = /^(["'])(.*?)\1(?:\s+(.*))?$/.exec(rest);
|
|
1022
|
+
if (quoted) {
|
|
1023
|
+
return { repo: quoted[2].trim() || '.', project: (quoted[3] || '').trim() };
|
|
1024
|
+
}
|
|
1025
|
+
if (tokens.length >= 2 && /^\d+$/.test(tokens.at(-1))) {
|
|
1026
|
+
return { repo: tokens.slice(0, -1).join(' '), project: tokens.at(-1) };
|
|
1027
|
+
}
|
|
1028
|
+
if (tokens.length && PATH_LIKE.test(tokens[0])) {
|
|
1029
|
+
return { repo: tokens[0], project: tokens.slice(1).join(' ').trim() };
|
|
1030
|
+
}
|
|
1031
|
+
return { repo: '.', project: rest };
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function mapProjectFields(project) {
|
|
1035
|
+
if (!project) {
|
|
1036
|
+
return {};
|
|
1037
|
+
}
|
|
1038
|
+
if (project.startsWith('-')) {
|
|
1039
|
+
throw new Error(MAP_USAGE);
|
|
1040
|
+
}
|
|
1041
|
+
return /^\d+$/.test(project) ? { projectMemberId: project } : { projectQuery: project };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
932
1044
|
export function formatProjects(result, weekly = null, projectFilter = undefined) {
|
|
933
1045
|
const lines = [result.summary];
|
|
934
1046
|
const projects = filterProjects(result.projects || [], projectFilter);
|
|
@@ -1064,6 +1176,7 @@ function defaultWorkflows() {
|
|
|
1064
1176
|
applyJiraWorklogBatch,
|
|
1065
1177
|
listLogworkProjects,
|
|
1066
1178
|
upsertProjectMapping,
|
|
1179
|
+
suggestProjectsForRepo,
|
|
1067
1180
|
loginResourceOptimiser,
|
|
1068
1181
|
getStoredAuthStatus,
|
|
1069
1182
|
getStoredJiraStatus,
|