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,508 @@
|
|
|
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
|
+
'AI agent skill (log work from your commits)',
|
|
439
|
+
'- Claude Code / Codex: run `logwork skill install`.',
|
|
440
|
+
'- Other MCP clients: use the MCP prompt "log_from_commits".',
|
|
441
|
+
'',
|
|
442
|
+
'Verify',
|
|
443
|
+
'- Restart or reload your IDE MCP tools after pasting config.',
|
|
444
|
+
'- In the IDE, ask: Check my logwork for this week.',
|
|
445
|
+
'- An old "logwork-helper" MCP entry can be deleted; it points at the old runtime.',
|
|
446
|
+
'',
|
|
447
|
+
'Safety',
|
|
448
|
+
'- Do not paste passwords, 2FA codes, Bearer tokens, cookies, or raw curl auth logs into AI chat.',
|
|
449
|
+
`- MCP config only needs command and args; tokens stay in ${storeLabel}.`,
|
|
450
|
+
'- Project mappings are stored separately and never store Resource Optimiser tokens.'
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
return sections.join('\n');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function formatAuthStatus(authResult) {
|
|
457
|
+
if (authResult?.ok) {
|
|
458
|
+
return 'completed';
|
|
459
|
+
}
|
|
460
|
+
if (authResult?.ok === false) {
|
|
461
|
+
return `not completed (${redactText(authResult.error || 'unknown error')})`;
|
|
462
|
+
}
|
|
463
|
+
return 'not run';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function formatNextSteps(authResult) {
|
|
467
|
+
const steps = [];
|
|
468
|
+
let index = 1;
|
|
469
|
+
if (!authResult?.ok) {
|
|
470
|
+
steps.push(`${index}. Log in to Resource Optimiser:`);
|
|
471
|
+
steps.push(' logwork login');
|
|
472
|
+
index += 1;
|
|
473
|
+
}
|
|
474
|
+
steps.push(`${index}. Paste one MCP config below into your IDE.`);
|
|
475
|
+
steps.push(`${index + 1}. Restart or reload your IDE MCP tools.`);
|
|
476
|
+
steps.push(`${index + 2}. Verify with: Check my logwork for this week.`);
|
|
477
|
+
return steps;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function formatTerminalCommandInstructions(linkResult = null) {
|
|
481
|
+
const commands = [
|
|
482
|
+
'Terminal command installed:',
|
|
483
|
+
' logwork open the terminal UI',
|
|
484
|
+
' logwork login log in to Resource Optimiser',
|
|
485
|
+
' logwork --help list all commands'
|
|
486
|
+
];
|
|
487
|
+
|
|
488
|
+
if (!linkResult || (linkResult.linked === true && linkResult.commandAvailable === true)) {
|
|
489
|
+
return commands.join('\n');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (linkResult.linked === true && linkResult.commandAvailable === false) {
|
|
493
|
+
return [
|
|
494
|
+
...commands,
|
|
495
|
+
'',
|
|
496
|
+
`Warning: ${linkResult.warning}`,
|
|
497
|
+
'If `logwork` is not found, open a new terminal or ensure npm global bin is on PATH.'
|
|
498
|
+
].join('\n');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return [
|
|
502
|
+
'Terminal command link did not complete.',
|
|
503
|
+
`Reason: ${linkResult.error}`,
|
|
504
|
+
'',
|
|
505
|
+
'Fallback:',
|
|
506
|
+
' npm install -g logwork-cli'
|
|
507
|
+
].join('\n');
|
|
508
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { SKILL_NAME, readLogworkSkill } from '../lib/agent-skill.mjs';
|
|
5
|
+
|
|
6
|
+
// `logwork skill` print the agent skill (SKILL.md)
|
|
7
|
+
// `logwork skill install` copy it to ~/.claude/skills and ~/.codex/skills
|
|
8
|
+
export const SKILL_TARGETS = Object.freeze({
|
|
9
|
+
claude: (home) => join(home, '.claude', 'skills', SKILL_NAME, 'SKILL.md'),
|
|
10
|
+
codex: (home) => join(home, '.codex', 'skills', SKILL_NAME, 'SKILL.md')
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function parseSkillArgs(args = []) {
|
|
14
|
+
const options = { action: 'print', targets: [], dir: undefined, help: false };
|
|
15
|
+
const positional = [];
|
|
16
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
17
|
+
const arg = args[index];
|
|
18
|
+
if (arg === '--help' || arg === '-h') {
|
|
19
|
+
options.help = true;
|
|
20
|
+
} else if (arg === '--target' || arg === '--dir') {
|
|
21
|
+
const value = args[index + 1];
|
|
22
|
+
if (!value || value.startsWith('--')) {
|
|
23
|
+
throw new Error(`${arg} requires a value.`);
|
|
24
|
+
}
|
|
25
|
+
if (arg === '--dir') {
|
|
26
|
+
options.dir = value;
|
|
27
|
+
} else {
|
|
28
|
+
options.targets.push(...value.split(','));
|
|
29
|
+
}
|
|
30
|
+
index += 1;
|
|
31
|
+
} else if (arg.startsWith('-')) {
|
|
32
|
+
throw new Error(`Unknown skill option: ${arg}`);
|
|
33
|
+
} else {
|
|
34
|
+
positional.push(arg);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (positional.length > 1 || (positional[0] && !['print', 'install'].includes(positional[0]))) {
|
|
38
|
+
throw new Error('Usage: logwork skill [print|install] [--target claude|codex|all] [--dir <path>]');
|
|
39
|
+
}
|
|
40
|
+
options.action = positional[0] || 'print';
|
|
41
|
+
const targets = options.targets.length ? options.targets : ['all'];
|
|
42
|
+
options.targets = targets.includes('all') ? Object.keys(SKILL_TARGETS) : targets;
|
|
43
|
+
for (const target of options.targets) {
|
|
44
|
+
if (!SKILL_TARGETS[target]) {
|
|
45
|
+
throw new Error(`Unknown skill target: ${target}. Use claude, codex or all.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return options;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function run(args = [], {
|
|
52
|
+
home = homedir(),
|
|
53
|
+
print = console.log,
|
|
54
|
+
mkdirFn = mkdir,
|
|
55
|
+
writeFileFn = writeFile
|
|
56
|
+
} = {}) {
|
|
57
|
+
const options = parseSkillArgs(args);
|
|
58
|
+
if (options.help) {
|
|
59
|
+
print(formatSkillHelp());
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
const content = `${readLogworkSkill({ withFrontmatter: true }).trim()}\n`;
|
|
63
|
+
if (options.action === 'print') {
|
|
64
|
+
print(content);
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
const paths = options.dir
|
|
68
|
+
? [join(resolve(options.dir), SKILL_NAME, 'SKILL.md')]
|
|
69
|
+
: options.targets.map((target) => SKILL_TARGETS[target](home));
|
|
70
|
+
for (const path of paths) {
|
|
71
|
+
await mkdirFn(join(path, '..'), { recursive: true });
|
|
72
|
+
await writeFileFn(path, content, 'utf8');
|
|
73
|
+
print(`Installed the logwork skill: ${path}`);
|
|
74
|
+
}
|
|
75
|
+
print('Restart the agent so it picks up the skill. The same procedure is served as the MCP prompt "log_from_commits".');
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function formatSkillHelp() {
|
|
80
|
+
return `Usage:
|
|
81
|
+
logwork skill Print the agent skill (SKILL.md)
|
|
82
|
+
logwork skill install Install it for Claude Code and Codex
|
|
83
|
+
logwork skill install --target claude|codex|all
|
|
84
|
+
logwork skill install --dir <path> Install under <path>/logwork/SKILL.md
|
|
85
|
+
(for example a repo's .claude/skills)
|
|
86
|
+
|
|
87
|
+
The skill tells an AI agent how to log accurate work from your git commits:
|
|
88
|
+
get_logwork -> draft_from_commits -> list_tasks -> review with you -> log_work.
|
|
89
|
+
Other agents (Cursor, Copilot) can use the MCP prompt "log_from_commits", or
|
|
90
|
+
paste \`logwork skill\` output into their rules.`;
|
|
91
|
+
}
|
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
|
+
}
|