logwork-cli 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +11 -0
- package/README.md +287 -2
- package/RELEASE.md +89 -0
- package/bin/logwork.mjs +101 -0
- package/commands/auth.mjs +227 -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 +504 -0
- package/commands/tui.mjs +50 -0
- package/commands/update.mjs +161 -0
- package/docs/advanced.md +334 -0
- package/docs/mcp-setup.md +347 -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/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/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 +793 -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 +72 -4
- package/setup.sh +22 -0
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const PACKAGE_NAME = 'logwork-cli';
|
|
6
|
+
const PACK_CACHE_PATH = '/private/tmp/logwork-cli-npm-cache';
|
|
7
|
+
|
|
8
|
+
export async function runReleaseDoctor({
|
|
9
|
+
cwd = process.cwd(),
|
|
10
|
+
full = false,
|
|
11
|
+
runCommand = runCommandDefault,
|
|
12
|
+
readFileFn = readFile,
|
|
13
|
+
readdirFn = readdir
|
|
14
|
+
} = {}) {
|
|
15
|
+
const checks = [];
|
|
16
|
+
const packageState = await readPackageState({ cwd, readFileFn });
|
|
17
|
+
checks.push(packageState.check);
|
|
18
|
+
|
|
19
|
+
const version = packageState.packageJson?.version;
|
|
20
|
+
checks.push(checkSelfDependency(packageState));
|
|
21
|
+
checks.push(await checkGitStatus({ cwd, runCommand }));
|
|
22
|
+
checks.push(await checkNpmRegistry({ cwd, runCommand }));
|
|
23
|
+
checks.push(await checkNpmAuth({ cwd, runCommand }));
|
|
24
|
+
|
|
25
|
+
if (version) {
|
|
26
|
+
checks.push(await checkNpmVersion({ cwd, runCommand, version }));
|
|
27
|
+
checks.push(await checkGitTag({ cwd, runCommand, version }));
|
|
28
|
+
checks.push(await checkGitHubAuth({ cwd, runCommand }));
|
|
29
|
+
checks.push(await checkGitHubRelease({ cwd, runCommand, version, packageJson: packageState.packageJson }));
|
|
30
|
+
} else {
|
|
31
|
+
checks.push(skipCheck('npm version', 'Skipped because package version could not be read.'));
|
|
32
|
+
checks.push(skipCheck('git tag', 'Skipped because package version could not be read.'));
|
|
33
|
+
checks.push(skipCheck('github auth', 'Skipped because package version could not be read.'));
|
|
34
|
+
checks.push(skipCheck('github release', 'Skipped because package version could not be read.'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
checks.push(await checkTarballArtifacts({ cwd, readdirFn }));
|
|
38
|
+
|
|
39
|
+
if (full) {
|
|
40
|
+
checks.push(await checkCommand({
|
|
41
|
+
name: 'git diff --check',
|
|
42
|
+
cwd,
|
|
43
|
+
runCommand,
|
|
44
|
+
command: 'git',
|
|
45
|
+
args: ['diff', '--check'],
|
|
46
|
+
gitSafe: true
|
|
47
|
+
}));
|
|
48
|
+
checks.push(await checkCommand({
|
|
49
|
+
name: 'npm test',
|
|
50
|
+
cwd,
|
|
51
|
+
runCommand,
|
|
52
|
+
command: 'npm',
|
|
53
|
+
args: ['test']
|
|
54
|
+
}));
|
|
55
|
+
checks.push(await checkCommand({
|
|
56
|
+
name: 'npm audit',
|
|
57
|
+
cwd,
|
|
58
|
+
runCommand,
|
|
59
|
+
command: 'npm',
|
|
60
|
+
args: ['run', 'audit:prod']
|
|
61
|
+
}));
|
|
62
|
+
checks.push(await checkCommand({
|
|
63
|
+
name: 'npm pack',
|
|
64
|
+
cwd,
|
|
65
|
+
runCommand,
|
|
66
|
+
command: 'npm',
|
|
67
|
+
args: ['pack', '--dry-run', '--cache', PACK_CACHE_PATH]
|
|
68
|
+
}));
|
|
69
|
+
} else {
|
|
70
|
+
checks.push(skipCheck('full gates', 'Run `logwork release doctor --full` before publishing to execute tests, audit, pack, and diff checks.'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return buildReleaseDoctorResult({
|
|
74
|
+
packageName: packageState.packageJson?.name || PACKAGE_NAME,
|
|
75
|
+
version,
|
|
76
|
+
full,
|
|
77
|
+
checks
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatReleaseDoctorReport(result) {
|
|
82
|
+
const lines = [
|
|
83
|
+
`Release doctor: ${result.status}`,
|
|
84
|
+
`Package: ${result.packageName}@${result.version || 'unknown'}`,
|
|
85
|
+
`Mode: ${result.full ? 'full' : 'fast'}`,
|
|
86
|
+
''
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const check of result.checks) {
|
|
90
|
+
lines.push(`[${check.status.toUpperCase()}] ${check.name}: ${check.summary}`);
|
|
91
|
+
if (check.details) {
|
|
92
|
+
lines.push(` ${check.details}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (result.nextSteps.length) {
|
|
97
|
+
lines.push('');
|
|
98
|
+
lines.push('Next steps:');
|
|
99
|
+
for (const step of result.nextSteps) {
|
|
100
|
+
lines.push(`- ${step}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return lines.join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function runCommandDefault(command, args = [], {
|
|
108
|
+
cwd = process.cwd(),
|
|
109
|
+
gitSafe = false
|
|
110
|
+
} = {}) {
|
|
111
|
+
return new Promise((resolve) => {
|
|
112
|
+
const env = {
|
|
113
|
+
...process.env,
|
|
114
|
+
...(gitSafe ? { GIT_CONFIG_GLOBAL: '/dev/null' } : {})
|
|
115
|
+
};
|
|
116
|
+
const child = spawn(command, args, {
|
|
117
|
+
cwd,
|
|
118
|
+
env,
|
|
119
|
+
shell: false,
|
|
120
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
121
|
+
});
|
|
122
|
+
let stdout = '';
|
|
123
|
+
let stderr = '';
|
|
124
|
+
|
|
125
|
+
child.stdout.on('data', (chunk) => {
|
|
126
|
+
stdout += chunk.toString();
|
|
127
|
+
});
|
|
128
|
+
child.stderr.on('data', (chunk) => {
|
|
129
|
+
stderr += chunk.toString();
|
|
130
|
+
});
|
|
131
|
+
child.on('error', (error) => {
|
|
132
|
+
resolve({
|
|
133
|
+
code: 127,
|
|
134
|
+
stdout,
|
|
135
|
+
stderr: error.message
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
child.on('close', (code) => {
|
|
139
|
+
resolve({
|
|
140
|
+
code: code ?? 1,
|
|
141
|
+
stdout,
|
|
142
|
+
stderr
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function readPackageState({ cwd, readFileFn }) {
|
|
149
|
+
try {
|
|
150
|
+
const packageJson = JSON.parse(await readFileFn(join(cwd, 'package.json'), 'utf8'));
|
|
151
|
+
const packageLock = JSON.parse(await readFileFn(join(cwd, 'package-lock.json'), 'utf8'));
|
|
152
|
+
const mismatches = [];
|
|
153
|
+
if (packageLock.version !== packageJson.version) {
|
|
154
|
+
mismatches.push(`package-lock version is ${packageLock.version || 'missing'}`);
|
|
155
|
+
}
|
|
156
|
+
if (packageLock.packages?.['']?.version !== packageJson.version) {
|
|
157
|
+
mismatches.push(`package-lock root package version is ${packageLock.packages?.['']?.version || 'missing'}`);
|
|
158
|
+
}
|
|
159
|
+
if (mismatches.length) {
|
|
160
|
+
return {
|
|
161
|
+
packageJson,
|
|
162
|
+
packageLock,
|
|
163
|
+
check: failCheck('package version', `package.json is ${packageJson.version}, but ${mismatches.join(', ')}.`)
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
packageJson,
|
|
168
|
+
packageLock,
|
|
169
|
+
check: okCheck('package version', `package.json and package-lock agree on ${packageJson.version}.`)
|
|
170
|
+
};
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
packageJson: null,
|
|
174
|
+
packageLock: null,
|
|
175
|
+
check: failCheck('package version', `Unable to read package metadata: ${error.message}`)
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function checkSelfDependency({ packageJson, packageLock }) {
|
|
181
|
+
if (!packageJson) {
|
|
182
|
+
return skipCheck('self dependency', 'Skipped because package.json could not be read.');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const dependencyFields = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
|
|
186
|
+
const offenders = [];
|
|
187
|
+
for (const field of dependencyFields) {
|
|
188
|
+
if (packageJson[field]?.[PACKAGE_NAME]) {
|
|
189
|
+
offenders.push(`package.json ${field}.${PACKAGE_NAME}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (packageLock?.packages?.['']?.dependencies?.[PACKAGE_NAME]) {
|
|
194
|
+
offenders.push(`package-lock root dependencies.${PACKAGE_NAME}`);
|
|
195
|
+
}
|
|
196
|
+
if (packageLock?.packages?.[`node_modules/${PACKAGE_NAME}`]) {
|
|
197
|
+
offenders.push(`package-lock node_modules/${PACKAGE_NAME}`);
|
|
198
|
+
}
|
|
199
|
+
if (packageLock?.dependencies?.[PACKAGE_NAME]) {
|
|
200
|
+
offenders.push(`package-lock dependencies.${PACKAGE_NAME}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (offenders.length) {
|
|
204
|
+
return failCheck('self dependency', `Remove stale local package references: ${offenders.join(', ')}.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return okCheck('self dependency', 'No local self-dependency is present.');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function checkGitStatus({ cwd, runCommand }) {
|
|
211
|
+
const result = await runCommand('git', ['status', '--short', '--branch'], { cwd, gitSafe: true });
|
|
212
|
+
if (result.code !== 0) {
|
|
213
|
+
return failCheck('git status', 'Unable to read git status.', sanitizeCommandOutput(result));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const lines = result.stdout.trim().split('\n').filter(Boolean);
|
|
217
|
+
const branch = lines[0] || '';
|
|
218
|
+
const dirty = lines.slice(1);
|
|
219
|
+
if (!branch.startsWith('## main')) {
|
|
220
|
+
return failCheck('git status', `Release must run from main. Current branch line: ${branch || 'unknown'}.`);
|
|
221
|
+
}
|
|
222
|
+
if (/\[(ahead|behind|gone)/.test(branch)) {
|
|
223
|
+
return failCheck('git status', `main must be synced before publishing. Current branch line: ${branch}.`);
|
|
224
|
+
}
|
|
225
|
+
if (dirty.length) {
|
|
226
|
+
return failCheck('git status', `Worktree has ${dirty.length} pending changes. Commit or clean them before publishing.`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return okCheck('git status', 'main is clean and synced.');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function checkNpmRegistry({ cwd, runCommand }) {
|
|
233
|
+
const result = await runCommand('npm', ['config', 'get', 'registry'], { cwd });
|
|
234
|
+
if (result.code !== 0) {
|
|
235
|
+
return failCheck('npm registry', 'Unable to read npm registry.', sanitizeCommandOutput(result));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const registry = result.stdout.trim();
|
|
239
|
+
if (registry !== 'https://registry.npmjs.org/') {
|
|
240
|
+
return failCheck('npm registry', `Expected https://registry.npmjs.org/ but got ${registry || 'empty'}.`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return okCheck('npm registry', registry);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function checkNpmAuth({ cwd, runCommand }) {
|
|
247
|
+
const result = await runCommand('npm', ['whoami'], { cwd });
|
|
248
|
+
if (result.code !== 0) {
|
|
249
|
+
return failCheck('npm auth', 'npm is not authenticated. Run `npm login --registry=https://registry.npmjs.org/`.', safeNpmAuthError(result));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return okCheck('npm auth', `Logged in as ${result.stdout.trim()}.`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function checkNpmVersion({ cwd, runCommand, version }) {
|
|
256
|
+
const result = await runCommand('npm', ['view', PACKAGE_NAME, 'version'], { cwd });
|
|
257
|
+
if (result.code !== 0) {
|
|
258
|
+
return warnCheck('npm latest', 'Unable to read npm latest version.', sanitizeCommandOutput(result));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const latest = result.stdout.trim();
|
|
262
|
+
const comparison = compareSemver(version, latest);
|
|
263
|
+
if (comparison === null) {
|
|
264
|
+
return warnCheck('npm latest', `Unable to compare local ${version} with npm latest ${latest}.`);
|
|
265
|
+
}
|
|
266
|
+
if (comparison <= 0) {
|
|
267
|
+
return failCheck('npm latest', `Local version ${version} is not newer than npm latest ${latest}. Bump version before publishing.`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return okCheck('npm latest', `Local version ${version} is newer than npm latest ${latest}.`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function checkGitTag({ cwd, runCommand, version }) {
|
|
274
|
+
const tag = `v${version}`;
|
|
275
|
+
const local = await runCommand('git', ['tag', '--list', tag], { cwd, gitSafe: true });
|
|
276
|
+
const remote = await runCommand('git', ['ls-remote', '--tags', 'origin', tag], { cwd, gitSafe: true });
|
|
277
|
+
if (local.code !== 0) {
|
|
278
|
+
return failCheck('git tag', `Unable to inspect local tag ${tag}.`, sanitizeCommandOutput(local));
|
|
279
|
+
}
|
|
280
|
+
if (remote.code !== 0) {
|
|
281
|
+
return warnCheck('git tag', `Unable to inspect remote tag ${tag}.`, sanitizeCommandOutput(remote));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const exists = Boolean(local.stdout.trim() || remote.stdout.trim());
|
|
285
|
+
if (exists) {
|
|
286
|
+
return warnCheck('git tag', `${tag} already exists locally or on origin. Do not recreate it.`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return okCheck('git tag', `${tag} does not exist yet.`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function checkGitHubAuth({ cwd, runCommand }) {
|
|
293
|
+
const result = await runCommand('gh', ['auth', 'status'], { cwd });
|
|
294
|
+
if (result.code !== 0) {
|
|
295
|
+
return failCheck('github auth', 'GitHub CLI is not authenticated or lacks required scopes.', sanitizeCommandOutput(result));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
299
|
+
if (!/repo/.test(output)) {
|
|
300
|
+
return warnCheck('github auth', 'GitHub CLI is authenticated, but repo scope was not visible in auth status output.');
|
|
301
|
+
}
|
|
302
|
+
if (!/workflow/.test(output)) {
|
|
303
|
+
return warnCheck('github auth', 'GitHub CLI is authenticated, but workflow scope was not visible in auth status output.');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return okCheck('github auth', 'GitHub CLI auth is available with repo/workflow scope.');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function checkGitHubRelease({ cwd, runCommand, version, packageJson }) {
|
|
310
|
+
const repo = parseGitHubRepo(packageJson?.repository?.url || packageJson?.homepage || '');
|
|
311
|
+
if (!repo) {
|
|
312
|
+
return warnCheck('github release', 'Unable to infer GitHub repo from package metadata.');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const tag = `v${version}`;
|
|
316
|
+
const result = await runCommand('gh', ['release', 'view', tag, '--repo', repo], { cwd });
|
|
317
|
+
if (result.code === 0) {
|
|
318
|
+
return warnCheck('github release', `Release ${tag} already exists on ${repo}.`);
|
|
319
|
+
}
|
|
320
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
321
|
+
if (/release not found/i.test(output) || /Not Found/i.test(output)) {
|
|
322
|
+
return okCheck('github release', `Release ${tag} does not exist yet on ${repo}.`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return warnCheck('github release', `Unable to verify release ${tag} on ${repo}.`, sanitizeCommandOutput(result));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function checkTarballArtifacts({ cwd, readdirFn }) {
|
|
329
|
+
try {
|
|
330
|
+
const files = await readdirFn(cwd);
|
|
331
|
+
const artifacts = files.filter((file) => file.endsWith('.tgz'));
|
|
332
|
+
if (artifacts.length) {
|
|
333
|
+
return warnCheck('tarball artifacts', `Remove generated tarballs before release: ${artifacts.join(', ')}.`);
|
|
334
|
+
}
|
|
335
|
+
return okCheck('tarball artifacts', 'No root .tgz artifacts found.');
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return warnCheck('tarball artifacts', `Unable to inspect root directory: ${error.message}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function checkCommand({ name, cwd, runCommand, command, args, gitSafe = false }) {
|
|
342
|
+
const result = await runCommand(command, args, { cwd, gitSafe });
|
|
343
|
+
if (result.code !== 0) {
|
|
344
|
+
return failCheck(name, `${name} failed.`, sanitizeCommandOutput(result));
|
|
345
|
+
}
|
|
346
|
+
return okCheck(name, `${name} passed.`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildReleaseDoctorResult({ packageName, version, full, checks }) {
|
|
350
|
+
const failed = checks.filter((check) => check.status === 'fail');
|
|
351
|
+
const warned = checks.filter((check) => check.status === 'warn');
|
|
352
|
+
const status = failed.length ? 'failed' : warned.length ? 'warning' : 'ok';
|
|
353
|
+
const nextSteps = [];
|
|
354
|
+
|
|
355
|
+
if (failed.some((check) => check.name === 'npm auth')) {
|
|
356
|
+
nextSteps.push('Run `npm login --registry=https://registry.npmjs.org/`, then retry `npm whoami`.');
|
|
357
|
+
}
|
|
358
|
+
if (!full) {
|
|
359
|
+
nextSteps.push('Run `logwork release doctor --full` before publishing.');
|
|
360
|
+
}
|
|
361
|
+
if (status === 'ok' && full) {
|
|
362
|
+
nextSteps.push('Publish with `npm publish --access public`, then verify with `npm view logwork-cli version`.');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return {
|
|
366
|
+
packageName,
|
|
367
|
+
version,
|
|
368
|
+
full,
|
|
369
|
+
status,
|
|
370
|
+
ok: failed.length === 0,
|
|
371
|
+
checks,
|
|
372
|
+
nextSteps
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function okCheck(name, summary, details = '') {
|
|
377
|
+
return createCheck('ok', name, summary, details);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function warnCheck(name, summary, details = '') {
|
|
381
|
+
return createCheck('warn', name, summary, details);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function failCheck(name, summary, details = '') {
|
|
385
|
+
return createCheck('fail', name, summary, details);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function skipCheck(name, summary, details = '') {
|
|
389
|
+
return createCheck('skip', name, summary, details);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function createCheck(status, name, summary, details = '') {
|
|
393
|
+
return {
|
|
394
|
+
status,
|
|
395
|
+
name,
|
|
396
|
+
summary,
|
|
397
|
+
...(details ? { details } : {})
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function sanitizeCommandOutput(result) {
|
|
402
|
+
return [result.stdout, result.stderr]
|
|
403
|
+
.join('\n')
|
|
404
|
+
.replace(/\/\/registry\.npmjs\.org\/:_authToken=[^\s]+/g, '//registry.npmjs.org/:_authToken=[REDACTED]')
|
|
405
|
+
.replace(/npm_[A-Za-z0-9_-]+/g, '[REDACTED_NPM_TOKEN]')
|
|
406
|
+
.replace(/gho_[A-Za-z0-9_]+/g, '[REDACTED_GITHUB_TOKEN]')
|
|
407
|
+
.trim();
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function safeNpmAuthError(result) {
|
|
411
|
+
const output = sanitizeCommandOutput(result);
|
|
412
|
+
return output.replace(/A complete log of this run can be found in:.+/g, 'npm wrote a local debug log; do not paste it into chat.');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function parseGitHubRepo(value) {
|
|
416
|
+
const text = String(value || '');
|
|
417
|
+
const match = text.match(/github\.com[:/]([^/\s]+)\/([^/\s#]+?)(?:\.git)?(?:[#/].*)?$/);
|
|
418
|
+
if (!match) {
|
|
419
|
+
return '';
|
|
420
|
+
}
|
|
421
|
+
return `${match[1]}/${match[2]}`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function compareSemver(left, right) {
|
|
425
|
+
const a = parseSemver(left);
|
|
426
|
+
const b = parseSemver(right);
|
|
427
|
+
if (!a || !b) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for (let index = 0; index < 3; index += 1) {
|
|
432
|
+
if (a[index] > b[index]) {
|
|
433
|
+
return 1;
|
|
434
|
+
}
|
|
435
|
+
if (a[index] < b[index]) {
|
|
436
|
+
return -1;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return 0;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function parseSemver(value) {
|
|
443
|
+
const match = String(value || '').trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
444
|
+
if (!match) {
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
return match.slice(1).map((part) => Number(part));
|
|
448
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { atomicWriteFile } from './atomic-file.mjs';
|
|
3
|
+
import { reminderConfigPath } from './paths.mjs';
|
|
4
|
+
|
|
5
|
+
export const REMINDER_TARGETS = Object.freeze(['ro', 'jira', 'both']);
|
|
6
|
+
export const WEEKDAY_NUMBERS = Object.freeze([1, 2, 3, 4, 5]);
|
|
7
|
+
export const DEFAULT_REMINDER_TIME = '17:00';
|
|
8
|
+
export const DEFAULT_REMINDER_TARGET = 'both';
|
|
9
|
+
|
|
10
|
+
export function defaultReminderConfig() {
|
|
11
|
+
return {
|
|
12
|
+
enabled: false,
|
|
13
|
+
time: DEFAULT_REMINDER_TIME,
|
|
14
|
+
target: DEFAULT_REMINDER_TARGET,
|
|
15
|
+
days: [...WEEKDAY_NUMBERS]
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function loadReminderConfig({ path = reminderConfigPath() } = {}) {
|
|
20
|
+
try {
|
|
21
|
+
return normalizeReminderConfig(JSON.parse(await readFile(path, 'utf8')));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError) {
|
|
24
|
+
return defaultReminderConfig();
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function saveReminderConfig(config, { path = reminderConfigPath() } = {}) {
|
|
31
|
+
const normalized = normalizeReminderConfig(config);
|
|
32
|
+
await atomicWriteFile(path, `${JSON.stringify(normalized, null, 2)}\n`);
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function normalizeReminderConfig(config = {}) {
|
|
37
|
+
const defaults = defaultReminderConfig();
|
|
38
|
+
const time = normalizeReminderTime(config.time ?? defaults.time);
|
|
39
|
+
const target = String(config.target ?? defaults.target).trim().toLowerCase();
|
|
40
|
+
if (!REMINDER_TARGETS.includes(target)) {
|
|
41
|
+
throw new Error('Reminder target must be ro, jira, or both.');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const days = normalizeReminderDays(config.days ?? defaults.days);
|
|
45
|
+
return {
|
|
46
|
+
enabled: Boolean(config.enabled),
|
|
47
|
+
time,
|
|
48
|
+
target,
|
|
49
|
+
days
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizeReminderTime(value) {
|
|
54
|
+
const time = String(value || '').trim();
|
|
55
|
+
const match = /^(\d{2}):(\d{2})$/.exec(time);
|
|
56
|
+
if (!match || Number(match[1]) > 23 || Number(match[2]) > 59) {
|
|
57
|
+
throw new Error('Reminder time must use 24-hour HH:mm format.');
|
|
58
|
+
}
|
|
59
|
+
return time;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function normalizeReminderDays(value) {
|
|
63
|
+
if (!Array.isArray(value) || !value.length) {
|
|
64
|
+
throw new Error('Reminder days must contain at least one weekday.');
|
|
65
|
+
}
|
|
66
|
+
const days = [...new Set(value.map(Number))].sort((left, right) => left - right);
|
|
67
|
+
if (days.some((day) => !Number.isInteger(day) || day < 1 || day > 5)) {
|
|
68
|
+
throw new Error('Reminder days must be weekdays Monday through Friday.');
|
|
69
|
+
}
|
|
70
|
+
return days;
|
|
71
|
+
}
|