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,504 @@
|
|
|
1
|
+
import { constants as fsConstants, promises as fs } from 'node:fs';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { delimiter, dirname, join, relative, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { redactText } from '../lib/auth-redaction.mjs';
|
|
6
|
+
import { credentialStoreLabel } from '../lib/credential-store.mjs';
|
|
7
|
+
import { helperHome } from '../lib/paths.mjs';
|
|
8
|
+
import {
|
|
9
|
+
MCP_SERVER_NAME,
|
|
10
|
+
defaultMcpServerPath,
|
|
11
|
+
formatCodexToml,
|
|
12
|
+
stdioMcpServersConfig,
|
|
13
|
+
vscodeMcpConfig
|
|
14
|
+
} from '../lib/manual-mcp.mjs';
|
|
15
|
+
import { loadReminderConfig } from '../lib/reminder-config.mjs';
|
|
16
|
+
import { installReminderScheduler } from '../lib/reminder-scheduler.mjs';
|
|
17
|
+
|
|
18
|
+
// Package name before the rename. Its global install provides a `logwork` bin
|
|
19
|
+
// that would block `npm link` of logwork-cli, so setup removes it first.
|
|
20
|
+
const LEGACY_PACKAGE_NAME = 'logwork-helper';
|
|
21
|
+
const PACKAGE_NAMES = ['logwork-cli', LEGACY_PACKAGE_NAME];
|
|
22
|
+
|
|
23
|
+
const EXCLUDED_DIRS = new Set([
|
|
24
|
+
'.git',
|
|
25
|
+
'node_modules'
|
|
26
|
+
]);
|
|
27
|
+
|
|
28
|
+
// `logwork setup`: copy this package into ~/.logwork-cli, install its
|
|
29
|
+
// dependencies, link the `logwork` command, refresh a migrated reminder, and
|
|
30
|
+
// print the MCP configs.
|
|
31
|
+
export async function run(argv = []) {
|
|
32
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
33
|
+
console.log(formatSetupHelp());
|
|
34
|
+
return 0;
|
|
35
|
+
}
|
|
36
|
+
const options = parseSetupUserArgs(argv);
|
|
37
|
+
const sourceDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
38
|
+
const targetDir = helperHome();
|
|
39
|
+
await installUserRuntime({ options, sourceDir, targetDir });
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function formatSetupHelp() {
|
|
44
|
+
return `Usage:
|
|
45
|
+
logwork setup [--login|--no-login]
|
|
46
|
+
|
|
47
|
+
Installs the runtime to ~/.logwork-cli, links the \`logwork\` command, and prints
|
|
48
|
+
MCP configs for Cursor, Antigravity, Claude Code, Codex and VS Code.
|
|
49
|
+
Settings, mappings and the login session from an older logwork-helper install
|
|
50
|
+
are carried over automatically.
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--login Log in to Resource Optimiser after installing
|
|
54
|
+
--no-login Do not ask to log in`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function installUserRuntime({
|
|
58
|
+
options,
|
|
59
|
+
sourceDir,
|
|
60
|
+
targetDir,
|
|
61
|
+
mkdirFn = fs.mkdir,
|
|
62
|
+
copyFn = copyDirectory,
|
|
63
|
+
cleanupFn = sanitizeRuntimeMetadata,
|
|
64
|
+
installFn = installDependencies,
|
|
65
|
+
linkFn = linkGlobalBinaries,
|
|
66
|
+
authFn = maybeRunAuthLogin,
|
|
67
|
+
reminderFn = refreshReminderSchedule,
|
|
68
|
+
printFn = printMcpInstructions
|
|
69
|
+
}) {
|
|
70
|
+
await mkdirFn(targetDir, { recursive: true });
|
|
71
|
+
if (resolve(sourceDir) !== resolve(targetDir)) {
|
|
72
|
+
await copyFn(sourceDir, targetDir, sourceDir);
|
|
73
|
+
}
|
|
74
|
+
const cleanupResult = await cleanupFn(targetDir);
|
|
75
|
+
await installFn(targetDir);
|
|
76
|
+
const linkResult = await linkFn(targetDir);
|
|
77
|
+
const authResult = await authFn(targetDir, options);
|
|
78
|
+
const reminderResult = await reminderFn(targetDir);
|
|
79
|
+
await printFn(targetDir, authResult, linkResult, cleanupResult, reminderResult);
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
targetDir,
|
|
83
|
+
authResult,
|
|
84
|
+
linkResult,
|
|
85
|
+
cleanupResult,
|
|
86
|
+
reminderResult
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A reminder enabled by an older install still points at the old runtime.
|
|
91
|
+
// Re-register it so the OS scheduler runs this runtime (and the legacy entry
|
|
92
|
+
// is removed by installReminderScheduler).
|
|
93
|
+
async function refreshReminderSchedule(targetDir) {
|
|
94
|
+
try {
|
|
95
|
+
const config = await loadReminderConfig();
|
|
96
|
+
if (!config?.enabled) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
await installReminderScheduler(config, { home: targetDir });
|
|
100
|
+
return { ok: true };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
return { ok: false, error: error.message };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function copyDirectory(sourceDir, targetDir, rootDir) {
|
|
107
|
+
const entries = await fs.readdir(sourceDir, { withFileTypes: true });
|
|
108
|
+
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (EXCLUDED_DIRS.has(entry.name)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sourcePath = join(sourceDir, entry.name);
|
|
115
|
+
const targetPath = join(targetDir, relative(rootDir, sourcePath));
|
|
116
|
+
|
|
117
|
+
if (entry.isDirectory()) {
|
|
118
|
+
await fs.mkdir(targetPath, { recursive: true });
|
|
119
|
+
await copyDirectory(sourcePath, targetDir, rootDir);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await fs.mkdir(dirname(targetPath), { recursive: true });
|
|
128
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function installDependencies(targetDir) {
|
|
133
|
+
return resolveInstallArgs(targetDir)
|
|
134
|
+
.then((args) => runCommand('npm', args, {
|
|
135
|
+
cwd: targetDir,
|
|
136
|
+
stdio: 'inherit'
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function runCommand(command, args, options = {}) {
|
|
141
|
+
return new Promise((resolveValue, reject) => {
|
|
142
|
+
const child = spawn(command, args, options);
|
|
143
|
+
|
|
144
|
+
child.on('error', reject);
|
|
145
|
+
child.on('close', (code) => {
|
|
146
|
+
if (code === 0) {
|
|
147
|
+
resolveValue();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}.`));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function resolveInstallArgs(targetDir) {
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(join(targetDir, 'package-lock.json'));
|
|
159
|
+
return ['ci', '--omit=dev'];
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error.code !== 'ENOENT') {
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
return ['install', '--omit=dev'];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export async function sanitizeRuntimeMetadata(targetDir, {
|
|
169
|
+
readFileFn = fs.readFile,
|
|
170
|
+
writeFileFn = fs.writeFile
|
|
171
|
+
} = {}) {
|
|
172
|
+
const changes = [];
|
|
173
|
+
const packagePath = join(targetDir, 'package.json');
|
|
174
|
+
const packageLockPath = join(targetDir, 'package-lock.json');
|
|
175
|
+
|
|
176
|
+
await updateJsonFile(packagePath, {
|
|
177
|
+
readFileFn,
|
|
178
|
+
writeFileFn,
|
|
179
|
+
changes
|
|
180
|
+
}, (json) => {
|
|
181
|
+
for (const name of PACKAGE_NAMES) {
|
|
182
|
+
if (isStaleSelfDependency(json.dependencies?.[name])) {
|
|
183
|
+
delete json.dependencies[name];
|
|
184
|
+
changes.push(`package.json dependencies.${name}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return json;
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await updateJsonFile(packageLockPath, {
|
|
191
|
+
readFileFn,
|
|
192
|
+
writeFileFn,
|
|
193
|
+
changes
|
|
194
|
+
}, (json) => {
|
|
195
|
+
for (const name of PACKAGE_NAMES) {
|
|
196
|
+
if (isStaleSelfDependency(json.packages?.['']?.dependencies?.[name])) {
|
|
197
|
+
delete json.packages[''].dependencies[name];
|
|
198
|
+
changes.push(`package-lock.json packages[""].dependencies.${name}`);
|
|
199
|
+
}
|
|
200
|
+
if (json.packages?.[`node_modules/${name}`]) {
|
|
201
|
+
delete json.packages[`node_modules/${name}`];
|
|
202
|
+
changes.push(`package-lock.json packages["node_modules/${name}"]`);
|
|
203
|
+
}
|
|
204
|
+
if (json.dependencies?.[`node_modules/${name}`]) {
|
|
205
|
+
delete json.dependencies[`node_modules/${name}`];
|
|
206
|
+
changes.push(`package-lock.json dependencies["node_modules/${name}"]`);
|
|
207
|
+
}
|
|
208
|
+
if (isStaleSelfDependency(json.dependencies?.[name]?.version)) {
|
|
209
|
+
delete json.dependencies[name];
|
|
210
|
+
changes.push(`package-lock.json dependencies.${name}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return json;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
changed: changes.length > 0,
|
|
218
|
+
changes
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function updateJsonFile(path, { readFileFn, writeFileFn }, update) {
|
|
223
|
+
let text;
|
|
224
|
+
try {
|
|
225
|
+
text = await readFileFn(path, 'utf8');
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error.code === 'ENOENT') {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const json = JSON.parse(text);
|
|
234
|
+
const before = JSON.stringify(json);
|
|
235
|
+
const updated = update(json);
|
|
236
|
+
const after = JSON.stringify(updated);
|
|
237
|
+
if (before !== after) {
|
|
238
|
+
await writeFileFn(path, `${JSON.stringify(updated, null, 2)}\n`, 'utf8');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function isStaleSelfDependency(value) {
|
|
243
|
+
return typeof value === 'string' && /^file:.*logwork-(helper|cli).*\.tgz$/i.test(value);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function linkGlobalBinaries(targetDir, {
|
|
247
|
+
runner = runCommand,
|
|
248
|
+
commandFinder = findCommandOnPath
|
|
249
|
+
} = {}) {
|
|
250
|
+
// Best effort: drop the pre-rename global package so its `logwork` bin does
|
|
251
|
+
// not block linking this one. A no-op when it is not installed.
|
|
252
|
+
await runner('npm', ['rm', '-g', LEGACY_PACKAGE_NAME], {
|
|
253
|
+
cwd: targetDir,
|
|
254
|
+
stdio: 'ignore'
|
|
255
|
+
}).catch(() => {});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
await runner('npm', ['link'], {
|
|
259
|
+
cwd: targetDir,
|
|
260
|
+
stdio: 'inherit'
|
|
261
|
+
});
|
|
262
|
+
} catch (error) {
|
|
263
|
+
return {
|
|
264
|
+
linked: false,
|
|
265
|
+
commandAvailable: false,
|
|
266
|
+
error: error.message
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const commandAvailable = await commandFinder('logwork');
|
|
271
|
+
return {
|
|
272
|
+
linked: true,
|
|
273
|
+
commandAvailable,
|
|
274
|
+
warning: commandAvailable
|
|
275
|
+
? null
|
|
276
|
+
: '`logwork` was linked, but it is not visible on PATH in this shell.'
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function findCommandOnPath(commandName, {
|
|
281
|
+
platform = process.platform,
|
|
282
|
+
envPath = process.env.PATH || '',
|
|
283
|
+
accessFn = fs.access
|
|
284
|
+
} = {}) {
|
|
285
|
+
const pathEntries = envPath.split(platform === 'win32' ? ';' : delimiter).filter(Boolean);
|
|
286
|
+
const names = platform === 'win32'
|
|
287
|
+
? [commandName, `${commandName}.cmd`, `${commandName}.exe`, `${commandName}.bat`]
|
|
288
|
+
: [commandName];
|
|
289
|
+
for (const pathEntry of pathEntries) {
|
|
290
|
+
for (const name of names) {
|
|
291
|
+
const candidate = resolve(pathEntry, name);
|
|
292
|
+
try {
|
|
293
|
+
await accessFn(candidate, fsConstants.X_OK);
|
|
294
|
+
return true;
|
|
295
|
+
} catch {
|
|
296
|
+
// Keep searching PATH.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function parseSetupUserArgs(args = []) {
|
|
305
|
+
let loginMode = 'prompt';
|
|
306
|
+
|
|
307
|
+
for (const arg of args) {
|
|
308
|
+
if (arg === '--login') {
|
|
309
|
+
loginMode = 'required';
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (arg === '--no-login') {
|
|
313
|
+
loginMode = 'skip';
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
throw new Error(`Unknown setup option: ${arg}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return { loginMode };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function maybeRunAuthLogin(targetDir, { loginMode }) {
|
|
323
|
+
if (loginMode === 'skip') {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const shouldRun = loginMode === 'required' || await shouldPromptLogin();
|
|
328
|
+
if (!shouldRun) {
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await runAuthLogin(targetDir);
|
|
334
|
+
return { ok: true };
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if (loginMode === 'required') {
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
ok: false,
|
|
341
|
+
error: error.message
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function shouldPromptLogin() {
|
|
347
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const { confirm, isCancel } = await import('@clack/prompts');
|
|
352
|
+
const { getStoredAuthStatus } = await import('../lib/auth.mjs');
|
|
353
|
+
const status = await getStoredAuthStatus().catch(() => ({ authenticated: false }));
|
|
354
|
+
if (status.authenticated) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const answer = await confirm({
|
|
359
|
+
message: 'Log in to Resource Optimiser now?',
|
|
360
|
+
initialValue: true
|
|
361
|
+
});
|
|
362
|
+
if (isCancel(answer)) {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return Boolean(answer);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function runAuthLogin(targetDir) {
|
|
370
|
+
return new Promise((resolveValue, reject) => {
|
|
371
|
+
const child = spawn(process.execPath, [resolve(targetDir, 'bin', 'logwork.mjs'), 'login'], {
|
|
372
|
+
cwd: targetDir,
|
|
373
|
+
stdio: 'inherit',
|
|
374
|
+
env: {
|
|
375
|
+
...process.env,
|
|
376
|
+
LOGWORK_HOME: targetDir
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
child.on('error', reject);
|
|
380
|
+
child.on('close', (code) => {
|
|
381
|
+
if (code === 0) {
|
|
382
|
+
resolveValue();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
reject(new Error(`Resource Optimiser auth login failed with exit code ${code}.`));
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function printMcpInstructions(targetDir, authResult = null, linkResult = null, cleanupResult = null, reminderResult = null) {
|
|
391
|
+
console.log(formatSetupUserInstructions({
|
|
392
|
+
targetDir,
|
|
393
|
+
authResult,
|
|
394
|
+
linkResult,
|
|
395
|
+
cleanupResult,
|
|
396
|
+
reminderResult
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function formatSetupUserInstructions({
|
|
401
|
+
targetDir,
|
|
402
|
+
authResult = null,
|
|
403
|
+
linkResult = null,
|
|
404
|
+
cleanupResult = null,
|
|
405
|
+
reminderResult = null,
|
|
406
|
+
platform = process.platform
|
|
407
|
+
} = {}) {
|
|
408
|
+
const entryPath = resolve(targetDir, 'bin', 'logwork.mjs');
|
|
409
|
+
const storeLabel = credentialStoreLabel({ platform });
|
|
410
|
+
const sections = [
|
|
411
|
+
`logwork-cli installed to ${targetDir}.`,
|
|
412
|
+
'',
|
|
413
|
+
'Install status',
|
|
414
|
+
`- Runtime: ${targetDir}`,
|
|
415
|
+
'- Dependencies: installed',
|
|
416
|
+
cleanupResult?.changed
|
|
417
|
+
? '- Runtime metadata: removed stale local tarball dependency'
|
|
418
|
+
: '- Runtime metadata: ready',
|
|
419
|
+
`- Auth: ${formatAuthStatus(authResult)}`,
|
|
420
|
+
...(reminderResult ? [`- Reminder: ${reminderResult.ok ? 're-registered for this runtime' : `not refreshed (${redactText(reminderResult.error || 'unknown error')}); run \`logwork reminder enable\``}`] : []),
|
|
421
|
+
'',
|
|
422
|
+
formatTerminalCommandInstructions(linkResult),
|
|
423
|
+
'',
|
|
424
|
+
'Next steps',
|
|
425
|
+
...formatNextSteps(authResult),
|
|
426
|
+
'',
|
|
427
|
+
`MCP config (server name "${MCP_SERVER_NAME}", runs: node ${entryPath} mcp)`,
|
|
428
|
+
'',
|
|
429
|
+
'Cursor / Antigravity / Claude Code (.mcp.json):',
|
|
430
|
+
JSON.stringify(stdioMcpServersConfig(entryPath), null, 2),
|
|
431
|
+
'',
|
|
432
|
+
'Codex config.toml:',
|
|
433
|
+
formatCodexToml(entryPath),
|
|
434
|
+
'',
|
|
435
|
+
'GitHub Copilot / VS Code mcp.json:',
|
|
436
|
+
JSON.stringify(vscodeMcpConfig(entryPath), null, 2),
|
|
437
|
+
'',
|
|
438
|
+
'Verify',
|
|
439
|
+
'- Restart or reload your IDE MCP tools after pasting config.',
|
|
440
|
+
'- In the IDE, ask: Check my logwork for this week.',
|
|
441
|
+
'- An old "logwork-helper" MCP entry can be deleted; it points at the old runtime.',
|
|
442
|
+
'',
|
|
443
|
+
'Safety',
|
|
444
|
+
'- Do not paste passwords, 2FA codes, Bearer tokens, cookies, or raw curl auth logs into AI chat.',
|
|
445
|
+
`- MCP config only needs command and args; tokens stay in ${storeLabel}.`,
|
|
446
|
+
'- Project mappings are stored separately and never store Resource Optimiser tokens.'
|
|
447
|
+
];
|
|
448
|
+
|
|
449
|
+
return sections.join('\n');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function formatAuthStatus(authResult) {
|
|
453
|
+
if (authResult?.ok) {
|
|
454
|
+
return 'completed';
|
|
455
|
+
}
|
|
456
|
+
if (authResult?.ok === false) {
|
|
457
|
+
return `not completed (${redactText(authResult.error || 'unknown error')})`;
|
|
458
|
+
}
|
|
459
|
+
return 'not run';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function formatNextSteps(authResult) {
|
|
463
|
+
const steps = [];
|
|
464
|
+
let index = 1;
|
|
465
|
+
if (!authResult?.ok) {
|
|
466
|
+
steps.push(`${index}. Log in to Resource Optimiser:`);
|
|
467
|
+
steps.push(' logwork login');
|
|
468
|
+
index += 1;
|
|
469
|
+
}
|
|
470
|
+
steps.push(`${index}. Paste one MCP config below into your IDE.`);
|
|
471
|
+
steps.push(`${index + 1}. Restart or reload your IDE MCP tools.`);
|
|
472
|
+
steps.push(`${index + 2}. Verify with: Check my logwork for this week.`);
|
|
473
|
+
return steps;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export function formatTerminalCommandInstructions(linkResult = null) {
|
|
477
|
+
const commands = [
|
|
478
|
+
'Terminal command installed:',
|
|
479
|
+
' logwork open the terminal UI',
|
|
480
|
+
' logwork login log in to Resource Optimiser',
|
|
481
|
+
' logwork --help list all commands'
|
|
482
|
+
];
|
|
483
|
+
|
|
484
|
+
if (!linkResult || (linkResult.linked === true && linkResult.commandAvailable === true)) {
|
|
485
|
+
return commands.join('\n');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (linkResult.linked === true && linkResult.commandAvailable === false) {
|
|
489
|
+
return [
|
|
490
|
+
...commands,
|
|
491
|
+
'',
|
|
492
|
+
`Warning: ${linkResult.warning}`,
|
|
493
|
+
'If `logwork` is not found, open a new terminal or ensure npm global bin is on PATH.'
|
|
494
|
+
].join('\n');
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return [
|
|
498
|
+
'Terminal command link did not complete.',
|
|
499
|
+
`Reason: ${linkResult.error}`,
|
|
500
|
+
'',
|
|
501
|
+
'Fallback:',
|
|
502
|
+
' npm install -g logwork-cli'
|
|
503
|
+
].join('\n');
|
|
504
|
+
}
|
package/commands/tui.mjs
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { runManualRepl } from '../lib/manual-repl.mjs';
|
|
3
|
+
import { checkForUpdates, formatUpdateNotice } from '../lib/update-service.mjs';
|
|
4
|
+
|
|
5
|
+
// `logwork` with no command: the Ink terminal UI.
|
|
6
|
+
export async function run(argv = []) {
|
|
7
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
8
|
+
console.log(formatTuiHelp());
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
if (argv.length) {
|
|
12
|
+
throw new Error(`Unknown option: ${argv[0]}. Run \`logwork --help\` for commands.`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const updateNotice = process.stdin.isTTY && process.stdout.isTTY
|
|
16
|
+
? await getStartupUpdateNotice()
|
|
17
|
+
: '';
|
|
18
|
+
await runManualRepl({
|
|
19
|
+
cwd: resolve(process.cwd()),
|
|
20
|
+
updateNotice
|
|
21
|
+
});
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getStartupUpdateNotice({ checkForUpdatesFn = checkForUpdates } = {}) {
|
|
26
|
+
try {
|
|
27
|
+
return formatUpdateNotice(await checkForUpdatesFn());
|
|
28
|
+
} catch {
|
|
29
|
+
return '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatTuiHelp() {
|
|
34
|
+
return `Usage:
|
|
35
|
+
logwork
|
|
36
|
+
|
|
37
|
+
Opens the terminal UI. Type / for live command suggestions, Esc to exit.
|
|
38
|
+
|
|
39
|
+
Common commands inside the UI:
|
|
40
|
+
/query this-week Booked vs logged hours with entry ids
|
|
41
|
+
/logwork Paste or write a weekly block, preview, then apply
|
|
42
|
+
/edit-logwork 304032 --task Bug Fix --desc Fix date picker (SAP-42)
|
|
43
|
+
/reconcile this-week Compare RO and Jira
|
|
44
|
+
/projects List RO projects
|
|
45
|
+
/map SCB 5234 Map a ticket prefix to a project
|
|
46
|
+
/mcp Show MCP config for an IDE
|
|
47
|
+
/history Local apply history
|
|
48
|
+
|
|
49
|
+
Run \`logwork --help\` for the CLI commands.`;
|
|
50
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { confirm, isCancel } from '@clack/prompts';
|
|
2
|
+
import {
|
|
3
|
+
checkForUpdates,
|
|
4
|
+
installUpdate,
|
|
5
|
+
parseSemver
|
|
6
|
+
} from '../lib/update-service.mjs';
|
|
7
|
+
|
|
8
|
+
export function parseUpdateArgs(argv) {
|
|
9
|
+
const args = [...argv];
|
|
10
|
+
let command = 'check';
|
|
11
|
+
let force = false;
|
|
12
|
+
let json = false;
|
|
13
|
+
let yes = false;
|
|
14
|
+
let version;
|
|
15
|
+
let help = false;
|
|
16
|
+
|
|
17
|
+
if (args[0] && !args[0].startsWith('-')) {
|
|
18
|
+
command = args.shift();
|
|
19
|
+
}
|
|
20
|
+
if (!['check', 'install'].includes(command)) {
|
|
21
|
+
throw new Error(`Unknown update command: ${command}. Use check or install.`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
25
|
+
const arg = args[index];
|
|
26
|
+
if (arg === '--help' || arg === '-h') {
|
|
27
|
+
help = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (arg === '--json') {
|
|
31
|
+
json = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === '--force') {
|
|
35
|
+
force = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (arg === '--yes' || arg === '-y') {
|
|
39
|
+
yes = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (arg === '--version') {
|
|
43
|
+
version = args[index + 1];
|
|
44
|
+
if (!version) {
|
|
45
|
+
throw new Error('--version requires an exact SemVer value.');
|
|
46
|
+
}
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith('--version=')) {
|
|
51
|
+
version = arg.slice('--version='.length);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
throw new Error(`Unknown update option: ${arg}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (command === 'check' && (yes || version)) {
|
|
58
|
+
throw new Error('--yes and --version are only valid with update install.');
|
|
59
|
+
}
|
|
60
|
+
if (command === 'install' && force) {
|
|
61
|
+
throw new Error('--force is only valid with update check; install always checks npm again.');
|
|
62
|
+
}
|
|
63
|
+
if (version && !parseSemver(version)) {
|
|
64
|
+
throw new Error('--version must be an exact SemVer value such as 1.2.3.');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { command, force, json, yes, version, help };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function runUpdateCli(argv, {
|
|
71
|
+
checkForUpdatesFn = checkForUpdates,
|
|
72
|
+
installUpdateFn = installUpdate,
|
|
73
|
+
promptConfirm = defaultConfirm,
|
|
74
|
+
isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
75
|
+
output = process.stdout
|
|
76
|
+
} = {}) {
|
|
77
|
+
const options = parseUpdateArgs(argv);
|
|
78
|
+
if (options.help) {
|
|
79
|
+
output.write(`${formatUpdateHelp()}\n`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (options.command === 'check') {
|
|
84
|
+
const result = await checkForUpdatesFn({ force: options.force });
|
|
85
|
+
printResult(output, result, options.json);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const latest = await checkForUpdatesFn({
|
|
90
|
+
force: true,
|
|
91
|
+
allowStaleOnError: false
|
|
92
|
+
});
|
|
93
|
+
const targetVersion = options.version || latest.latestVersion;
|
|
94
|
+
if (targetVersion !== latest.latestVersion) {
|
|
95
|
+
throw new Error(`Only the npm latest version can be installed. Requested ${targetVersion}; latest is ${latest.latestVersion}.`);
|
|
96
|
+
}
|
|
97
|
+
if (!latest.updateAvailable) {
|
|
98
|
+
printResult(output, latest, options.json);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let approved = options.yes;
|
|
103
|
+
if (!approved) {
|
|
104
|
+
if (!isTTY) {
|
|
105
|
+
throw new Error('Interactive confirmation is unavailable. Re-run with --yes to install the update.');
|
|
106
|
+
}
|
|
107
|
+
approved = await promptConfirm(`Install Logwork Helper ${targetVersion}?`);
|
|
108
|
+
}
|
|
109
|
+
if (!approved) {
|
|
110
|
+
output.write('Update cancelled.\n');
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const result = await installUpdateFn({
|
|
115
|
+
version: targetVersion,
|
|
116
|
+
confirm: true,
|
|
117
|
+
stdio: options.json ? 'pipe' : 'inherit'
|
|
118
|
+
});
|
|
119
|
+
printResult(output, result, options.json);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function formatUpdateHelp() {
|
|
124
|
+
return `Usage:
|
|
125
|
+
logwork update check [--force] [--json]
|
|
126
|
+
logwork update install [--version <semver>] [--yes] [--json]
|
|
127
|
+
|
|
128
|
+
Commands:
|
|
129
|
+
check Compare the installed version with npm latest. Results are cached for 24 hours.
|
|
130
|
+
install Re-check npm latest, install that exact version, and verify the local runtime.
|
|
131
|
+
|
|
132
|
+
Options:
|
|
133
|
+
--force Ignore the update cache when checking
|
|
134
|
+
--version <semver> Install only when this exact version is npm latest
|
|
135
|
+
-y, --yes Skip the interactive install confirmation
|
|
136
|
+
--json Print structured JSON output
|
|
137
|
+
-h, --help Show this help`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function defaultConfirm(message) {
|
|
141
|
+
const result = await confirm({
|
|
142
|
+
message,
|
|
143
|
+
initialValue: false
|
|
144
|
+
});
|
|
145
|
+
return !isCancel(result) && result === true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function printResult(output, result, json) {
|
|
149
|
+
if (json) {
|
|
150
|
+
output.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
output.write(`${result.summary}\n`);
|
|
154
|
+
if (result.updateAvailable) {
|
|
155
|
+
output.write(`Release: ${result.releaseUrl}\n`);
|
|
156
|
+
output.write('Install: logwork update install\n');
|
|
157
|
+
}
|
|
158
|
+
if (result.warning) {
|
|
159
|
+
output.write(`Warning: ${result.warning}\n`);
|
|
160
|
+
}
|
|
161
|
+
}
|