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
package/lib/jira-api.mjs
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { CONFIG } from './config.mjs';
|
|
2
|
+
import { ApiError } from './api.mjs';
|
|
3
|
+
import { fetchWithPolicy, redactedExcerpt } from './http.mjs';
|
|
4
|
+
import { safeJsonParse } from './util.mjs';
|
|
5
|
+
|
|
6
|
+
export async function jiraApiFetch(token, path, {
|
|
7
|
+
baseUrl = CONFIG.jiraBaseUrl,
|
|
8
|
+
fetchImpl = fetch,
|
|
9
|
+
timeoutMs = CONFIG.httpTimeoutMs,
|
|
10
|
+
retries = CONFIG.httpReadRetries,
|
|
11
|
+
retryDelayMs = CONFIG.httpRetryDelayMs,
|
|
12
|
+
method = 'GET',
|
|
13
|
+
body,
|
|
14
|
+
headers = {}
|
|
15
|
+
} = {}) {
|
|
16
|
+
assertJiraToken(token);
|
|
17
|
+
const url = buildJiraUrl(baseUrl, path);
|
|
18
|
+
const response = await fetchWithPolicy(url.toString(), {
|
|
19
|
+
fetchImpl,
|
|
20
|
+
timeoutMs,
|
|
21
|
+
retries,
|
|
22
|
+
retryDelayMs,
|
|
23
|
+
idempotent: isIdempotentMethod(method),
|
|
24
|
+
method,
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${token}`,
|
|
27
|
+
Accept: 'application/json',
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
...headers
|
|
30
|
+
},
|
|
31
|
+
body: body === undefined ? undefined : JSON.stringify(body)
|
|
32
|
+
});
|
|
33
|
+
const text = await response.text();
|
|
34
|
+
const data = text ? safeJsonParse(text, text) : null;
|
|
35
|
+
const safeBody = redactedExcerpt(text);
|
|
36
|
+
|
|
37
|
+
if (response.status === 401) {
|
|
38
|
+
throw new ApiError('Jira request returned 401 Unauthorized. Run `logwork login jira` with a valid PAT.', {
|
|
39
|
+
status: response.status,
|
|
40
|
+
path,
|
|
41
|
+
body: safeBody
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!response.ok) {
|
|
46
|
+
throw new ApiError(`Jira request failed: ${response.status} ${response.statusText} for ${path}. Body: ${safeBody}`, {
|
|
47
|
+
status: response.status,
|
|
48
|
+
path,
|
|
49
|
+
body: safeBody
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function getJiraMyself(token, options = {}) {
|
|
57
|
+
return normalizeJiraUser(await jiraApiFetch(token, '/rest/api/2/myself', options));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function getJiraIssue(token, issueKey, options = {}) {
|
|
61
|
+
assertIssueKey(issueKey);
|
|
62
|
+
return normalizeJiraIssue(await jiraApiFetch(token, `/rest/api/2/issue/${encodeURIComponent(issueKey)}`, options));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function getJiraIssueWorklogs(token, issueKey, options = {}) {
|
|
66
|
+
assertIssueKey(issueKey);
|
|
67
|
+
const { pageSize = 100, ...fetchOptions } = options;
|
|
68
|
+
const worklogs = [];
|
|
69
|
+
let startAt = 0;
|
|
70
|
+
|
|
71
|
+
while (true) {
|
|
72
|
+
const qs = new URLSearchParams({
|
|
73
|
+
maxResults: String(pageSize),
|
|
74
|
+
startAt: String(startAt)
|
|
75
|
+
});
|
|
76
|
+
const data = await jiraApiFetch(token, `/rest/api/2/issue/${encodeURIComponent(issueKey)}/worklog?${qs}`, fetchOptions);
|
|
77
|
+
const page = Array.isArray(data?.worklogs) ? data.worklogs : [];
|
|
78
|
+
worklogs.push(...page.map(normalizeJiraWorklog));
|
|
79
|
+
|
|
80
|
+
startAt += page.length;
|
|
81
|
+
const total = Number(data?.total);
|
|
82
|
+
if (!page.length || !Number.isFinite(total) || startAt >= total) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return worklogs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function searchJiraIssues(token, {
|
|
91
|
+
jql,
|
|
92
|
+
fields = ['key', 'summary'],
|
|
93
|
+
pageSize = 100
|
|
94
|
+
} = {}, options = {}) {
|
|
95
|
+
if (!String(jql || '').trim()) {
|
|
96
|
+
throw new Error('Jira search requires JQL.');
|
|
97
|
+
}
|
|
98
|
+
const issues = [];
|
|
99
|
+
let startAt = 0;
|
|
100
|
+
|
|
101
|
+
while (true) {
|
|
102
|
+
const qs = new URLSearchParams({
|
|
103
|
+
jql: String(jql),
|
|
104
|
+
fields: fields.join(','),
|
|
105
|
+
maxResults: String(pageSize),
|
|
106
|
+
startAt: String(startAt)
|
|
107
|
+
});
|
|
108
|
+
const data = await jiraApiFetch(token, `/rest/api/2/search?${qs}`, options);
|
|
109
|
+
const page = Array.isArray(data?.issues) ? data.issues : [];
|
|
110
|
+
issues.push(...page.map(normalizeJiraIssue));
|
|
111
|
+
|
|
112
|
+
startAt += page.length;
|
|
113
|
+
const total = Number(data?.total);
|
|
114
|
+
if (!page.length || !Number.isFinite(total) || startAt >= total) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return issues;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function addJiraIssueWorklog(token, issueKey, worklog, options = {}) {
|
|
123
|
+
assertIssueKey(issueKey);
|
|
124
|
+
const qs = new URLSearchParams({ adjustEstimate: 'leave' });
|
|
125
|
+
const path = `/rest/api/2/issue/${encodeURIComponent(issueKey)}/worklog?${qs}`;
|
|
126
|
+
return normalizeJiraWorklog(await jiraApiFetch(token, path, {
|
|
127
|
+
...options,
|
|
128
|
+
method: 'POST',
|
|
129
|
+
retries: 0,
|
|
130
|
+
body: worklog
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function normalizeJiraIssue(issue = {}) {
|
|
135
|
+
const fields = issue.fields || {};
|
|
136
|
+
return {
|
|
137
|
+
id: issue.id ? String(issue.id) : undefined,
|
|
138
|
+
key: issue.key ? String(issue.key) : undefined,
|
|
139
|
+
summary: fields.summary ? String(fields.summary) : '',
|
|
140
|
+
status: fields.status?.name ? String(fields.status.name) : '',
|
|
141
|
+
issueType: fields.issuetype?.name ? String(fields.issuetype.name) : '',
|
|
142
|
+
project: fields.project?.key || fields.project?.name
|
|
143
|
+
? {
|
|
144
|
+
key: fields.project?.key ? String(fields.project.key) : undefined,
|
|
145
|
+
name: fields.project?.name ? String(fields.project.name) : undefined
|
|
146
|
+
}
|
|
147
|
+
: null,
|
|
148
|
+
assignee: fields.assignee ? normalizeJiraUser(fields.assignee) : null
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function normalizeJiraUser(user = {}) {
|
|
153
|
+
return {
|
|
154
|
+
name: user.name ? String(user.name) : undefined,
|
|
155
|
+
key: user.key ? String(user.key) : undefined,
|
|
156
|
+
displayName: user.displayName ? String(user.displayName) : undefined,
|
|
157
|
+
emailAddress: user.emailAddress ? String(user.emailAddress) : undefined,
|
|
158
|
+
active: user.active === undefined ? undefined : Boolean(user.active)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function normalizeJiraWorklog(worklog = {}) {
|
|
163
|
+
return {
|
|
164
|
+
id: worklog.id ? String(worklog.id) : undefined,
|
|
165
|
+
issueId: worklog.issueId ? String(worklog.issueId) : undefined,
|
|
166
|
+
started: worklog.started ? String(worklog.started) : '',
|
|
167
|
+
timeSpent: worklog.timeSpent ? String(worklog.timeSpent) : '',
|
|
168
|
+
timeSpentSeconds: Number.isFinite(Number(worklog.timeSpentSeconds)) ? Number(worklog.timeSpentSeconds) : 0,
|
|
169
|
+
comment: normalizeWorklogComment(worklog.comment),
|
|
170
|
+
author: worklog.author ? normalizeJiraUser(worklog.author) : null
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function buildJiraUrl(baseUrl, path) {
|
|
175
|
+
const base = assertJiraBaseUrl(baseUrl);
|
|
176
|
+
const basePath = base.pathname.endsWith('/') ? base.pathname : `${base.pathname}/`;
|
|
177
|
+
const relativePath = String(path || '').replace(/^\/+/, '');
|
|
178
|
+
return new URL(relativePath, `${base.origin}${basePath}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function assertJiraBaseUrl(rawUrl = CONFIG.jiraBaseUrl) {
|
|
182
|
+
let url;
|
|
183
|
+
try {
|
|
184
|
+
url = new URL(rawUrl);
|
|
185
|
+
} catch {
|
|
186
|
+
throw new Error('Jira base URL must be a valid http(s) URL.');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!['http:', 'https:'].includes(url.protocol)) {
|
|
190
|
+
throw new Error('Jira base URL must use HTTP or HTTPS.');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (url.protocol === 'http:' && !isLocalHttpHost(url.hostname)) {
|
|
194
|
+
throw new Error('Jira base URL must use HTTPS unless it points to localhost or loopback.');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return url;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertJiraToken(token) {
|
|
201
|
+
if (!String(token || '').trim()) {
|
|
202
|
+
throw new Error('Jira PAT token is required.');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function assertIssueKey(issueKey) {
|
|
207
|
+
if (!/^[A-Z][A-Z0-9]+-\d+$/.test(String(issueKey || ''))) {
|
|
208
|
+
throw new Error('Jira issueKey must look like ABC-123.');
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function normalizeWorklogComment(comment) {
|
|
213
|
+
if (typeof comment === 'string') {
|
|
214
|
+
return comment;
|
|
215
|
+
}
|
|
216
|
+
if (comment === undefined || comment === null) {
|
|
217
|
+
return '';
|
|
218
|
+
}
|
|
219
|
+
const text = extractJiraText(comment).trim();
|
|
220
|
+
if (text) {
|
|
221
|
+
return text;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
return JSON.stringify(comment);
|
|
225
|
+
} catch {
|
|
226
|
+
return String(comment);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function extractJiraText(value) {
|
|
231
|
+
if (typeof value === 'string') {
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
if (Array.isArray(value)) {
|
|
235
|
+
return value.map(extractJiraText).filter(Boolean).join(' ');
|
|
236
|
+
}
|
|
237
|
+
if (value && typeof value === 'object') {
|
|
238
|
+
if (typeof value.text === 'string') {
|
|
239
|
+
return value.text;
|
|
240
|
+
}
|
|
241
|
+
if (Array.isArray(value.content)) {
|
|
242
|
+
return extractJiraText(value.content);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return '';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isIdempotentMethod(method) {
|
|
249
|
+
return ['GET', 'HEAD', 'OPTIONS'].includes(String(method || 'GET').toUpperCase());
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isLocalHttpHost(hostname) {
|
|
253
|
+
const host = String(hostname || '').replace(/^\[|\]$/g, '').toLowerCase();
|
|
254
|
+
return host === 'localhost' ||
|
|
255
|
+
host === '127.0.0.1' ||
|
|
256
|
+
host === '::1' ||
|
|
257
|
+
host.startsWith('127.');
|
|
258
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { isCancel, password } from '@clack/prompts';
|
|
2
|
+
import { CONFIG } from './config.mjs';
|
|
3
|
+
import { createCredentialJiraStorage } from './credential-store.mjs';
|
|
4
|
+
import { getJiraMyself, assertJiraBaseUrl } from './jira-api.mjs';
|
|
5
|
+
import { redactText } from './auth-redaction.mjs';
|
|
6
|
+
import { safeJsonParse } from './util.mjs';
|
|
7
|
+
|
|
8
|
+
export function createJiraAuthRequiredError(message = jiraAuthRequiredSummary()) {
|
|
9
|
+
const error = new Error(message);
|
|
10
|
+
error.code = 'JIRA_AUTH_REQUIRED';
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isJiraAuthRequiredError(error) {
|
|
15
|
+
return error?.code === 'JIRA_AUTH_REQUIRED';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function jiraAuthRequiredPayload(error) {
|
|
19
|
+
return {
|
|
20
|
+
status: 'jira_auth_required',
|
|
21
|
+
authRequired: true,
|
|
22
|
+
command: 'logwork login jira',
|
|
23
|
+
summary: error?.message || jiraAuthRequiredSummary()
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loginJira({
|
|
28
|
+
baseUrl = CONFIG.jiraBaseUrl,
|
|
29
|
+
tokenProvider = createTerminalJiraTokenProvider(),
|
|
30
|
+
storage = createCredentialJiraStorage(),
|
|
31
|
+
validateToken = getJiraMyself
|
|
32
|
+
} = {}) {
|
|
33
|
+
const normalizedBaseUrl = normalizeJiraBaseUrl(baseUrl);
|
|
34
|
+
const token = String(await tokenProvider.requestToken({ baseUrl: normalizedBaseUrl }) || '').trim();
|
|
35
|
+
if (!token) {
|
|
36
|
+
throw new Error('Jira PAT token is required.');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const user = await validateToken(token, {
|
|
40
|
+
baseUrl: normalizedBaseUrl
|
|
41
|
+
});
|
|
42
|
+
const session = normalizeJiraSession({
|
|
43
|
+
baseUrl: normalizedBaseUrl,
|
|
44
|
+
token,
|
|
45
|
+
user
|
|
46
|
+
});
|
|
47
|
+
await storage.set(serializeJiraSession(session));
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
...session,
|
|
51
|
+
summary: `Authenticated to Jira ${session.baseUrl} as ${formatJiraUser(session.user)}.`
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function getStoredJiraStatus({
|
|
56
|
+
storage = createCredentialJiraStorage()
|
|
57
|
+
} = {}) {
|
|
58
|
+
const session = normalizeStoredJiraSession(await storage.get());
|
|
59
|
+
if (!session?.token) {
|
|
60
|
+
return {
|
|
61
|
+
authenticated: false,
|
|
62
|
+
summary: 'Not authenticated to Jira. Run `logwork login jira`.'
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
authenticated: true,
|
|
68
|
+
baseUrl: session.baseUrl,
|
|
69
|
+
user: session.user,
|
|
70
|
+
summary: `Authenticated to Jira ${session.baseUrl} as ${formatJiraUser(session.user)}.`
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function logoutJira({
|
|
75
|
+
storage = createCredentialJiraStorage()
|
|
76
|
+
} = {}) {
|
|
77
|
+
const deleted = await storage.delete();
|
|
78
|
+
return {
|
|
79
|
+
deleted,
|
|
80
|
+
summary: deleted ? 'Deleted stored Jira PAT.' : 'No stored Jira PAT found.'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function getStoredJiraSession({
|
|
85
|
+
storage = createCredentialJiraStorage()
|
|
86
|
+
} = {}) {
|
|
87
|
+
const session = normalizeStoredJiraSession(await storage.get());
|
|
88
|
+
if (!session?.token) {
|
|
89
|
+
throw createJiraAuthRequiredError();
|
|
90
|
+
}
|
|
91
|
+
return session;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createTerminalJiraTokenProvider({
|
|
95
|
+
stdin = process.stdin,
|
|
96
|
+
stdout = process.stdout
|
|
97
|
+
} = {}) {
|
|
98
|
+
return {
|
|
99
|
+
async requestToken({ baseUrl }) {
|
|
100
|
+
ensureInteractive(stdin, stdout);
|
|
101
|
+
return handleCancel(await password({
|
|
102
|
+
message: `Jira PAT for ${baseUrl}:`,
|
|
103
|
+
validate(value) {
|
|
104
|
+
if (!String(value || '').trim()) {
|
|
105
|
+
return 'Jira PAT token is required.';
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function normalizeStoredJiraSession(rawValue) {
|
|
115
|
+
if (!rawValue) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const parsed = safeJsonParse(String(rawValue), null);
|
|
120
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
return normalizeJiraSession(parsed);
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function normalizeJiraSession(session = {}) {
|
|
132
|
+
const baseUrl = normalizeJiraBaseUrl(session.baseUrl || CONFIG.jiraBaseUrl);
|
|
133
|
+
const token = String(session.token || '').trim();
|
|
134
|
+
if (!token) {
|
|
135
|
+
throw new Error('Jira session token is required.');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
baseUrl,
|
|
140
|
+
token,
|
|
141
|
+
user: session.user && typeof session.user === 'object' ? sanitizeJiraUser(session.user) : null
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function serializeJiraSession(session) {
|
|
146
|
+
const normalized = normalizeJiraSession(session);
|
|
147
|
+
return JSON.stringify({
|
|
148
|
+
baseUrl: normalized.baseUrl,
|
|
149
|
+
token: normalized.token,
|
|
150
|
+
user: normalized.user
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function formatJiraUser(user) {
|
|
155
|
+
if (!user) {
|
|
156
|
+
return 'unknown user';
|
|
157
|
+
}
|
|
158
|
+
return user.displayName || user.name || user.key || user.emailAddress || 'unknown user';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function sanitizeJiraUser(user) {
|
|
162
|
+
return {
|
|
163
|
+
name: textOrUndefined(user.name),
|
|
164
|
+
key: textOrUndefined(user.key),
|
|
165
|
+
displayName: textOrUndefined(user.displayName),
|
|
166
|
+
emailAddress: textOrUndefined(user.emailAddress),
|
|
167
|
+
active: user.active === undefined ? undefined : Boolean(user.active)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function textOrUndefined(value) {
|
|
172
|
+
if (value === undefined || value === null || String(value).trim() === '') {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return String(value);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function ensureInteractive(stdin, stdout) {
|
|
179
|
+
if (!stdin?.isTTY || !stdout?.isTTY) {
|
|
180
|
+
throw createJiraAuthRequiredError();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function handleCancel(value) {
|
|
185
|
+
if (isCancel(value)) {
|
|
186
|
+
throw new Error('User cancelled Jira authentication.');
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeJiraBaseUrl(value) {
|
|
192
|
+
const url = assertJiraBaseUrl(value);
|
|
193
|
+
const pathname = url.pathname === '/' ? '' : url.pathname.replace(/\/+$/, '');
|
|
194
|
+
return `${url.origin}${pathname}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function jiraAuthRequiredSummary() {
|
|
198
|
+
return 'Jira authentication required. Run `logwork login jira` in a terminal, paste a Jira Personal Access Token there, then retry this MCP tool.';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function safeJiraAuthError(error) {
|
|
202
|
+
return new Error(redactText(error?.message || String(error || 'Jira authentication failed.')));
|
|
203
|
+
}
|