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,227 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getStoredAuthStatus,
|
|
3
|
+
loginResourceOptimiser,
|
|
4
|
+
logoutResourceOptimiser
|
|
5
|
+
} from '../lib/auth.mjs';
|
|
6
|
+
import { authenticateWithApiSession } from '../lib/api-auth.mjs';
|
|
7
|
+
import { createAuthDiagnosticsRecorder } from '../lib/auth-diagnostics.mjs';
|
|
8
|
+
import { credentialStoreLabel } from '../lib/credential-store.mjs';
|
|
9
|
+
import {
|
|
10
|
+
getStoredJiraStatus,
|
|
11
|
+
loginJira,
|
|
12
|
+
logoutJira,
|
|
13
|
+
safeJiraAuthError
|
|
14
|
+
} from '../lib/jira-auth.mjs';
|
|
15
|
+
|
|
16
|
+
// `logwork login|logout|status [ro|jira]`. Resource Optimiser is the default
|
|
17
|
+
// target. Secrets are only ever typed in this terminal and stored in the OS
|
|
18
|
+
// credential store; they are never accepted as arguments.
|
|
19
|
+
|
|
20
|
+
export async function runLogin(argv = []) {
|
|
21
|
+
const { target, args } = splitTarget(argv);
|
|
22
|
+
if (wantsHelp(args)) {
|
|
23
|
+
console.log(formatAuthHelp());
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
if (target === 'jira') {
|
|
27
|
+
const result = await runJiraLoginCommand(parseJiraLoginArgs(args));
|
|
28
|
+
return result.ok ? 0 : 1;
|
|
29
|
+
}
|
|
30
|
+
parseLoginArgs(args);
|
|
31
|
+
const result = await runLoginCommand();
|
|
32
|
+
return result.ok ? 0 : 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runLogout(argv = []) {
|
|
36
|
+
const { target, args } = splitTarget(argv);
|
|
37
|
+
if (wantsHelp(args)) {
|
|
38
|
+
console.log(formatAuthHelp());
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
assertNoArgs(args, 'logout');
|
|
42
|
+
const result = target === 'jira' ? await logoutJira() : await logoutResourceOptimiser();
|
|
43
|
+
console.log(result.summary);
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function runStatus(argv = []) {
|
|
48
|
+
if (wantsHelp(argv)) {
|
|
49
|
+
console.log(formatAuthHelp());
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
const { target, args } = splitTarget(argv);
|
|
53
|
+
assertNoArgs(args, 'status');
|
|
54
|
+
const targets = argv.length ? [target] : ['ro', 'jira'];
|
|
55
|
+
let authenticated = true;
|
|
56
|
+
for (const item of targets) {
|
|
57
|
+
const result = item === 'jira' ? await getStoredJiraStatus() : await getStoredAuthStatus();
|
|
58
|
+
console.log(`${item === 'jira' ? 'Jira' : 'Resource Optimiser'}: ${result.summary}`);
|
|
59
|
+
if (item === 'ro' && !result.authenticated) {
|
|
60
|
+
authenticated = false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return authenticated ? 0 : 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatAuthHelp() {
|
|
67
|
+
const storeLabel = credentialStoreLabel();
|
|
68
|
+
return `Usage:
|
|
69
|
+
logwork login [ro|jira] [--base-url <url>]
|
|
70
|
+
logwork logout [ro|jira]
|
|
71
|
+
logwork status [ro|jira]
|
|
72
|
+
|
|
73
|
+
Targets:
|
|
74
|
+
ro Resource Optimiser (default). Email, password and 2FA are asked in this terminal.
|
|
75
|
+
jira Jira Personal Access Token, pasted in this terminal.
|
|
76
|
+
|
|
77
|
+
Sessions are stored in ${storeLabel}, never in files or command arguments.
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--base-url <url> Jira base URL for \`login jira\`. Defaults to https://jira-vnv.vinova.sg`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function splitTarget(argv = []) {
|
|
84
|
+
const [first, ...rest] = argv;
|
|
85
|
+
if (first === 'ro' || first === 'jira') {
|
|
86
|
+
return { target: first, args: rest };
|
|
87
|
+
}
|
|
88
|
+
return { target: 'ro', args: argv };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function wantsHelp(args) {
|
|
92
|
+
return args.includes('--help') || args.includes('-h');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function assertNoArgs(args, command) {
|
|
96
|
+
if (args.length) {
|
|
97
|
+
throw new Error(`Unknown ${command} option: ${args[0]}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function runLoginCommand({
|
|
102
|
+
login = loginResourceOptimiser,
|
|
103
|
+
createRecorder = createAuthDiagnosticsRecorder,
|
|
104
|
+
stdout = console.log,
|
|
105
|
+
stderr = console.error
|
|
106
|
+
} = {}) {
|
|
107
|
+
stdout('Starting Resource Optimiser API auth. Secrets are requested in this terminal and are not stored.');
|
|
108
|
+
const diagnosticsRecorder = createRecorder();
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const result = await login({
|
|
112
|
+
authenticator: (options) => authenticateWithApiSession({
|
|
113
|
+
...options,
|
|
114
|
+
diagnosticsRecorder
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
stdout(result.summary);
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
result
|
|
121
|
+
};
|
|
122
|
+
} catch (error) {
|
|
123
|
+
stderr(error.message);
|
|
124
|
+
try {
|
|
125
|
+
const written = await diagnosticsRecorder.writeFailure(error);
|
|
126
|
+
stderr(`Auth failed. Sanitized diagnostics saved to ${written.path}. Attempt: ${written.attemptId}`);
|
|
127
|
+
} catch (writeError) {
|
|
128
|
+
stderr(`Auth failed. Unable to write sanitized diagnostics: ${writeError.message}`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
error
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function parseLoginArgs(args = []) {
|
|
138
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
139
|
+
const arg = args[index];
|
|
140
|
+
|
|
141
|
+
if (arg === '--api-mode') {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (arg === '--browser' || arg.startsWith('--browser=')) {
|
|
146
|
+
throw new Error('Browser auth has been removed. Run `logwork login` for API-only auth.');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (arg === '--browser-mode') {
|
|
150
|
+
throw new Error('Browser auth has been removed. Run `logwork login` for API-only auth.');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (arg === 'record') {
|
|
154
|
+
throw new Error('Auth recording has been removed because auth is API-only.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (arg === '--help' || arg === '-h') {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
throw new Error(`Unknown auth login option: ${arg}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { mode: 'api' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function runJiraLoginCommand({
|
|
168
|
+
baseUrl,
|
|
169
|
+
login = loginJira,
|
|
170
|
+
stdout = console.log,
|
|
171
|
+
stderr = console.error
|
|
172
|
+
} = {}) {
|
|
173
|
+
stdout('Starting Jira PAT auth. Paste the token in this terminal; it is stored only in the OS credential store.');
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const result = await login({
|
|
177
|
+
...(baseUrl ? { baseUrl } : {})
|
|
178
|
+
});
|
|
179
|
+
stdout(result.summary);
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
result
|
|
183
|
+
};
|
|
184
|
+
} catch (error) {
|
|
185
|
+
const safeError = safeJiraAuthError(error);
|
|
186
|
+
stderr(safeError.message);
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
error: safeError
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function parseJiraLoginArgs(args = []) {
|
|
195
|
+
const options = {};
|
|
196
|
+
|
|
197
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
198
|
+
const arg = args[index];
|
|
199
|
+
|
|
200
|
+
if (arg === '--help' || arg === '-h') {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (arg === '--base-url') {
|
|
205
|
+
const value = args[index + 1];
|
|
206
|
+
if (!value || value.startsWith('--')) {
|
|
207
|
+
throw new Error('--base-url requires a value.');
|
|
208
|
+
}
|
|
209
|
+
options.baseUrl = value;
|
|
210
|
+
index += 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (arg.startsWith('--base-url=')) {
|
|
215
|
+
const value = arg.slice('--base-url='.length);
|
|
216
|
+
if (!value) {
|
|
217
|
+
throw new Error('--base-url requires a value.');
|
|
218
|
+
}
|
|
219
|
+
options.baseUrl = value;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw new Error(`Unknown jira login option: ${arg}`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return options;
|
|
227
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { draftLogworkFromCommits } from '../lib/commit-workflow.mjs';
|
|
2
|
+
import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
|
|
3
|
+
|
|
4
|
+
// `logwork commits [period] [--repo path]... [--author x] [--no-bookings] [--json]`:
|
|
5
|
+
// turn your git commits into a logwork draft. Read-only.
|
|
6
|
+
export function parseCommitsArgs(args = []) {
|
|
7
|
+
const options = { period: 'today', repos: [], author: undefined, useBookings: true, json: false, help: false };
|
|
8
|
+
const positional = [];
|
|
9
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
10
|
+
const arg = args[index];
|
|
11
|
+
if (arg === '--help' || arg === '-h') {
|
|
12
|
+
options.help = true;
|
|
13
|
+
} else if (arg === '--json') {
|
|
14
|
+
options.json = true;
|
|
15
|
+
} else if (arg === '--no-bookings') {
|
|
16
|
+
options.useBookings = false;
|
|
17
|
+
} else if (arg === '--repo' || arg === '--author') {
|
|
18
|
+
const value = args[index + 1];
|
|
19
|
+
if (!value || value.startsWith('--')) {
|
|
20
|
+
throw new Error(`${arg} requires a value.`);
|
|
21
|
+
}
|
|
22
|
+
if (arg === '--repo') {
|
|
23
|
+
options.repos.push(value);
|
|
24
|
+
} else {
|
|
25
|
+
options.author = value;
|
|
26
|
+
}
|
|
27
|
+
index += 1;
|
|
28
|
+
} else if (arg.startsWith('--repo=')) {
|
|
29
|
+
options.repos.push(arg.slice('--repo='.length));
|
|
30
|
+
} else if (arg.startsWith('--author=')) {
|
|
31
|
+
options.author = arg.slice('--author='.length);
|
|
32
|
+
} else if (arg.startsWith('-')) {
|
|
33
|
+
throw new Error(`Unknown commits option: ${arg}`);
|
|
34
|
+
} else {
|
|
35
|
+
positional.push(arg);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (positional.length > 1) {
|
|
39
|
+
throw new Error('commits accepts exactly one preset period.');
|
|
40
|
+
}
|
|
41
|
+
if (positional[0]) {
|
|
42
|
+
options.period = normalizeReconciliationPeriod(positional[0]);
|
|
43
|
+
}
|
|
44
|
+
return options;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function run(args = [], { draft = draftLogworkFromCommits, print = console.log } = {}) {
|
|
48
|
+
const options = parseCommitsArgs(args);
|
|
49
|
+
if (options.help) {
|
|
50
|
+
print(formatCommitsHelp());
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
const result = await draft({
|
|
54
|
+
period: options.period,
|
|
55
|
+
repos: options.repos,
|
|
56
|
+
author: options.author,
|
|
57
|
+
useBookings: options.useBookings
|
|
58
|
+
});
|
|
59
|
+
print(options.json ? JSON.stringify(result, null, 2) : result.summary);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function formatCommitsHelp() {
|
|
64
|
+
return `Usage:
|
|
65
|
+
logwork commits [today|yesterday|this-week|last-week|this-month|last-month]
|
|
66
|
+
[--repo <path>]... [--author <email>] [--no-bookings] [--json]
|
|
67
|
+
|
|
68
|
+
Reads your commits (all branches, your git user.email) from each repo (default:
|
|
69
|
+
the current directory) and drafts logwork lines:
|
|
70
|
+
+<hours> <worklog task> | <description>
|
|
71
|
+
|
|
72
|
+
- Commits are grouped per day by repo, ticket key and kind of work.
|
|
73
|
+
- The kind of work picks the worklog task: feat -> Feature Development,
|
|
74
|
+
fix -> Bug Fix, refactor/perf/review -> Code Review & Refactoring,
|
|
75
|
+
test -> Testing Activities, ci/build/release -> Deployment & Release,
|
|
76
|
+
docs/design -> Requirements & Design, chore -> Other Activities.
|
|
77
|
+
- Hours split the day's RO remaining time (booked - logged) by how much work
|
|
78
|
+
each group holds. With --no-bookings or without login, a weekday is 8h.
|
|
79
|
+
|
|
80
|
+
Nothing is written. Review the draft, then paste it into /logwork or give it
|
|
81
|
+
to your AI agent (log_work).`;
|
|
82
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { confirm, isCancel } from '@clack/prompts';
|
|
2
|
+
import { parseRoDeleteArgs } from '../lib/ro-delete-args.mjs';
|
|
3
|
+
import {
|
|
4
|
+
applyRoLogworkDelete,
|
|
5
|
+
previewRoLogworkDelete
|
|
6
|
+
} from '../lib/ro-delete-workflow.mjs';
|
|
7
|
+
|
|
8
|
+
export async function runRoDeleteCommand(args = [], {
|
|
9
|
+
preview = previewRoLogworkDelete,
|
|
10
|
+
apply = applyRoLogworkDelete,
|
|
11
|
+
promptConfirm = defaultConfirm,
|
|
12
|
+
isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
13
|
+
print = console.log
|
|
14
|
+
} = {}) {
|
|
15
|
+
const options = parseRoDeleteArgs(args);
|
|
16
|
+
if (options.help) {
|
|
17
|
+
print(formatRoDeleteHelp());
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (options.json && !options.yes) {
|
|
22
|
+
throw new Error('--json requires --yes so stdout remains valid JSON.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const deletePreview = await preview({ logworkId: options.logworkId });
|
|
26
|
+
if (!options.json) {
|
|
27
|
+
print(deletePreview.summary);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let approved = options.yes;
|
|
31
|
+
if (!approved) {
|
|
32
|
+
if (!isTTY) {
|
|
33
|
+
throw new Error('Interactive confirmation is unavailable. Re-run with --yes.');
|
|
34
|
+
}
|
|
35
|
+
approved = await promptConfirm(`Delete RO logwork entry ${deletePreview.logworkId}?`);
|
|
36
|
+
}
|
|
37
|
+
if (!approved) {
|
|
38
|
+
print('RO logwork delete cancelled.');
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await apply({
|
|
43
|
+
preview: deletePreview,
|
|
44
|
+
confirm: true
|
|
45
|
+
});
|
|
46
|
+
print(options.json ? JSON.stringify(result, null, 2) : result.summary);
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function formatRoDeleteHelp() {
|
|
51
|
+
return `Usage:
|
|
52
|
+
logwork delete <logwork-id> [--yes] [--json]
|
|
53
|
+
|
|
54
|
+
Soft-deletes one Resource Optimiser logwork entry (submitted or approved only).
|
|
55
|
+
The command previews the entry, checks for concurrent changes, then calls DELETE /logwork/entries/:id.
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
-y, --yes Delete without interactive confirmation
|
|
59
|
+
--json Print structured JSON; requires --yes
|
|
60
|
+
-h, --help Show this help`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function defaultConfirm(message) {
|
|
64
|
+
const result = await confirm({
|
|
65
|
+
message,
|
|
66
|
+
initialValue: false
|
|
67
|
+
});
|
|
68
|
+
return !isCancel(result) && result === true;
|
|
69
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { generateDiagnosticsReport } from '../lib/diagnostics.mjs';
|
|
2
|
+
|
|
3
|
+
export async function run(args = []) {
|
|
4
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
5
|
+
printHelp();
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
if (args.length) {
|
|
9
|
+
throw new Error(`Unknown doctor option: ${args[0]}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const result = await generateDiagnosticsReport();
|
|
13
|
+
console.log(result.summary);
|
|
14
|
+
console.log('Send this sanitized file to the developer. Do not send raw curl logs, cookies, passwords, OTPs, or tokens.');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function printHelp() {
|
|
18
|
+
console.log(`Usage:
|
|
19
|
+
logwork doctor
|
|
20
|
+
|
|
21
|
+
Checks your setup and writes a sanitized support report under ~/.logwork-cli/diagnostics.
|
|
22
|
+
The report redacts tokens, cookies, passwords, OTPs, auth codes, and raw HTML.
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { confirm, isCancel } from '@clack/prompts';
|
|
2
|
+
import { parseRoEditArgs } from '../lib/ro-edit-args.mjs';
|
|
3
|
+
import {
|
|
4
|
+
applyRoLogworkEdit,
|
|
5
|
+
previewRoLogworkEdit
|
|
6
|
+
} from '../lib/ro-edit-workflow.mjs';
|
|
7
|
+
|
|
8
|
+
export async function runRoEditCommand(args = [], {
|
|
9
|
+
preview = previewRoLogworkEdit,
|
|
10
|
+
apply = applyRoLogworkEdit,
|
|
11
|
+
promptConfirm = defaultConfirm,
|
|
12
|
+
isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
13
|
+
print = console.log
|
|
14
|
+
} = {}) {
|
|
15
|
+
const options = parseRoEditArgs(args);
|
|
16
|
+
if (options.help) {
|
|
17
|
+
print(formatRoEditHelp());
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (options.json && !options.yes) {
|
|
22
|
+
throw new Error('--json requires --yes so stdout remains valid JSON.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const editPreview = await preview({
|
|
26
|
+
logworkId: options.logworkId,
|
|
27
|
+
hours: options.hours,
|
|
28
|
+
taskName: options.taskName,
|
|
29
|
+
description: options.description,
|
|
30
|
+
typeOfWork: options.typeOfWork
|
|
31
|
+
});
|
|
32
|
+
if (!options.json) {
|
|
33
|
+
print(editPreview.summary);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let approved = options.yes;
|
|
37
|
+
if (!approved) {
|
|
38
|
+
if (!isTTY) {
|
|
39
|
+
throw new Error('Interactive confirmation is unavailable. Re-run with --yes.');
|
|
40
|
+
}
|
|
41
|
+
approved = await promptConfirm(`Apply RO logwork edit ${editPreview.previewId}?`);
|
|
42
|
+
}
|
|
43
|
+
if (!approved) {
|
|
44
|
+
print('RO logwork edit cancelled.');
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const result = await apply({
|
|
49
|
+
preview: editPreview,
|
|
50
|
+
confirm: true
|
|
51
|
+
});
|
|
52
|
+
print(options.json ? JSON.stringify(result, null, 2) : result.summary);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function formatRoEditHelp() {
|
|
57
|
+
return `Usage:
|
|
58
|
+
logwork edit <id> [--hours <h>] [--task <task>] [--desc <text>] [--type <type>] [--yes] [--json]
|
|
59
|
+
|
|
60
|
+
Changes one Resource Optimiser entry. Project and date are preserved. Get the id
|
|
61
|
+
from \`logwork query\`.
|
|
62
|
+
|
|
63
|
+
RO has no staff edit API, so apply replaces the entry: it creates the corrected
|
|
64
|
+
entry, then deletes the original (the new entry gets a new id and is approved
|
|
65
|
+
immediately). If the delete fails, the new entry is rolled back. Only submitted
|
|
66
|
+
or approved entries can be edited; rejected entries are fixed with resubmit.
|
|
67
|
+
|
|
68
|
+
Options:
|
|
69
|
+
--hours <h> New hours (alias: --logtimes)
|
|
70
|
+
--task <task> New worklog task from the project catalog, or #<id> (alias: --task-name)
|
|
71
|
+
--desc <text> New description: what you did (alias: --description)
|
|
72
|
+
--type <type> New type of work: create, correct, improve, other
|
|
73
|
+
(follows the new task's catalog type when the task changes)
|
|
74
|
+
-y, --yes Apply without the confirmation prompt
|
|
75
|
+
--json Print the result as JSON (requires --yes)
|
|
76
|
+
-h, --help Show this help
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
logwork edit 304032 --task "Bug Fix" --desc "Fix date picker timezone (SAP-42)"`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function defaultConfirm(message) {
|
|
83
|
+
const result = await confirm({
|
|
84
|
+
message,
|
|
85
|
+
initialValue: false
|
|
86
|
+
});
|
|
87
|
+
return !isCancel(result) && result === true;
|
|
88
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { queryApplyLedger } from '../lib/apply-ledger.mjs';
|
|
2
|
+
|
|
3
|
+
export function parseHistoryArgs(args = []) {
|
|
4
|
+
const options = {
|
|
5
|
+
target: undefined,
|
|
6
|
+
limit: 20,
|
|
7
|
+
json: false
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
11
|
+
const arg = args[index];
|
|
12
|
+
if (arg === '--target') {
|
|
13
|
+
options.target = args[index + 1];
|
|
14
|
+
index += 1;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (arg === '--limit') {
|
|
18
|
+
options.limit = Number(args[index + 1]);
|
|
19
|
+
index += 1;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (arg === '--json') {
|
|
23
|
+
options.json = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === '--help' || arg === '-h') {
|
|
27
|
+
return { help: true };
|
|
28
|
+
}
|
|
29
|
+
throw new Error(`Unknown history option: ${arg}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return options;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function runHistoryCommand(args = [], {
|
|
36
|
+
query = queryApplyLedger,
|
|
37
|
+
print = console.log
|
|
38
|
+
} = {}) {
|
|
39
|
+
const options = parseHistoryArgs(args);
|
|
40
|
+
if (options.help) {
|
|
41
|
+
printHistoryHelp(print);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const result = await query({
|
|
46
|
+
target: options.target,
|
|
47
|
+
limit: options.limit
|
|
48
|
+
});
|
|
49
|
+
print(options.json ? JSON.stringify(result, null, 2) : result.summary);
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function printHistoryHelp(print = console.log) {
|
|
54
|
+
print(`Usage:
|
|
55
|
+
logwork history [--target ro|jira|both] [--limit <count>] [--json]
|
|
56
|
+
|
|
57
|
+
Shows the local apply result ledger. Target both means entries applied from a combined Both flow.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function main() {
|
|
61
|
+
await runHistoryCommand(process.argv.slice(2));
|
|
62
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { queryLogwork } from '../lib/query-workflow.mjs';
|
|
2
|
+
import { normalizeReconciliationPeriod } from '../lib/reconciliation-workflow.mjs';
|
|
3
|
+
|
|
4
|
+
// `logwork query [period] [--json]`: booked vs logged Resource Optimiser hours,
|
|
5
|
+
// with each entry's id, worklog task, description and status.
|
|
6
|
+
export function parseQueryArgs(args = []) {
|
|
7
|
+
const options = { period: 'today', json: false, help: false };
|
|
8
|
+
const positional = [];
|
|
9
|
+
for (const arg of args) {
|
|
10
|
+
if (arg === '--json') {
|
|
11
|
+
options.json = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (arg === '--help' || arg === '-h') {
|
|
15
|
+
options.help = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (arg.startsWith('-')) {
|
|
19
|
+
throw new Error(`Unknown query option: ${arg}`);
|
|
20
|
+
}
|
|
21
|
+
positional.push(arg);
|
|
22
|
+
}
|
|
23
|
+
if (positional.length > 1) {
|
|
24
|
+
throw new Error('Query accepts exactly one preset period.');
|
|
25
|
+
}
|
|
26
|
+
if (positional[0]) {
|
|
27
|
+
options.period = normalizeReconciliationPeriod(positional[0]);
|
|
28
|
+
}
|
|
29
|
+
return options;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function run(args = [], {
|
|
33
|
+
query = queryLogwork,
|
|
34
|
+
print = console.log
|
|
35
|
+
} = {}) {
|
|
36
|
+
const options = parseQueryArgs(args);
|
|
37
|
+
if (options.help) {
|
|
38
|
+
print(formatQueryHelp());
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
const result = await query({ period: options.period, includeEntries: true });
|
|
42
|
+
print(options.json ? JSON.stringify(result, null, 2) : result.summary);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatQueryHelp() {
|
|
47
|
+
return `Usage:
|
|
48
|
+
logwork query [today|yesterday|this-week|last-week|this-month|last-month] [--json]
|
|
49
|
+
|
|
50
|
+
Shows booked vs logged Resource Optimiser hours per day and project, with each
|
|
51
|
+
entry's id (use it with \`logwork edit\` / \`logwork delete\`). Defaults to today.`;
|
|
52
|
+
}
|