@ethlete/agent-rules 0.1.0-next.17 → 0.1.0-next.19
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/CHANGELOG.md +35 -0
- package/README.md +14 -2
- package/content/hooks/subagent-model-policy.py +5 -3
- package/content/rules/app-styling.md +6 -1
- package/content/rules/subagent-models.md +7 -7
- package/content/skills/design-exploration/SKILL.md +3 -1
- package/content/skills/query/SKILL.md +10 -2
- package/content/skills/sdk-update/SKILL.md +5 -3
- package/content/skills/timetrack/SKILL.md +97 -27
- package/migrations/app-styling-utilities.md +44 -95
- package/migrations/list-state-query-form.md +12 -95
- package/migrations/nx-layout.md +6 -0
- package/migrations/sdk-components-over-hand-built-ui.md +16 -58
- package/migrations/search-query-field.md +12 -5
- package/migrations.json +5 -5
- package/package.json +1 -1
- package/src/lib/frontmatter.js +32 -5
- package/src/lib/frontmatter.js.map +1 -1
- package/src/lib/git-flow/parse.js +38 -12
- package/src/lib/git-flow/parse.js.map +1 -1
- package/src/lib/git-flow-repair.js +12 -1
- package/src/lib/git-flow-repair.js.map +1 -1
- package/src/lib/git.js +8 -2
- package/src/lib/git.js.map +1 -1
- package/src/lib/gitlab.js +11 -1
- package/src/lib/gitlab.js.map +1 -1
- package/src/lib/migrate.js +1 -1
- package/src/lib/migrate.js.map +1 -1
- package/src/lib/output-style.js +11 -3
- package/src/lib/output-style.js.map +1 -1
- package/src/lib/owned-paths.js +8 -2
- package/src/lib/owned-paths.js.map +1 -1
- package/src/lib/plan.js +20 -4
- package/src/lib/plan.js.map +1 -1
- package/src/lib/render.d.ts +4 -0
- package/src/lib/render.js +8 -2
- package/src/lib/render.js.map +1 -1
- package/src/lib/sync.js +3 -0
- package/src/lib/sync.js.map +1 -1
- package/src/lib/targets/codex.js +1 -1
- package/src/lib/targets/codex.js.map +1 -1
- package/src/lib/targets/copilot.js +1 -1
- package/src/lib/targets/copilot.js.map +1 -1
- package/src/lib/targets/git-hooks.js +2 -1
- package/src/lib/targets/git-hooks.js.map +1 -1
- package/src/lib/targets/hooks-shared.js +1 -1
- package/src/lib/targets/hooks-shared.js.map +1 -1
- package/src/lib/timetrack-command.js +316 -72
- package/src/lib/timetrack-command.js.map +1 -1
- package/src/lib/timetrack-snapshot.d.ts +55 -0
- package/src/lib/timetrack-snapshot.js +120 -0
- package/src/lib/timetrack-snapshot.js.map +1 -0
- package/src/lib/timetrack.d.ts +125 -23
- package/src/lib/timetrack.js +53 -17
- package/src/lib/timetrack.js.map +1 -1
|
@@ -6,6 +6,7 @@ const fs_1 = require("fs");
|
|
|
6
6
|
const path_1 = require("path");
|
|
7
7
|
const timetrack_1 = require("./timetrack");
|
|
8
8
|
const plain_text_1 = require("./plain-text");
|
|
9
|
+
const timetrack_snapshot_1 = require("./timetrack-snapshot");
|
|
9
10
|
const git_1 = require("./git");
|
|
10
11
|
const FLAGS_WITH_VALUE = [
|
|
11
12
|
'--root',
|
|
@@ -30,33 +31,66 @@ const FLAGS_WITH_VALUE = [
|
|
|
30
31
|
'--paths',
|
|
31
32
|
'--claim',
|
|
32
33
|
'--author',
|
|
34
|
+
'--plan',
|
|
35
|
+
'--delete',
|
|
36
|
+
'--repo',
|
|
37
|
+
'--name',
|
|
38
|
+
'--rename',
|
|
39
|
+
'--merge',
|
|
40
|
+
'--into',
|
|
41
|
+
'--compare',
|
|
33
42
|
];
|
|
34
43
|
/** Every human-readable line, with anything a terminal would act on printed rather than obeyed. */
|
|
35
44
|
const say = (line) => console.log((0, plain_text_1.plain)(line));
|
|
36
45
|
const positionalArgs = (args) => args.filter((entry, index) => { var _a; return !entry.startsWith('--') && !FLAGS_WITH_VALUE.includes((_a = args[index - 1]) !== null && _a !== void 0 ? _a : ''); });
|
|
37
46
|
const flagValue = (args, flag) => {
|
|
38
47
|
const index = args.indexOf(flag);
|
|
39
|
-
|
|
48
|
+
if (index === -1)
|
|
49
|
+
return undefined;
|
|
50
|
+
const value = args[index + 1];
|
|
51
|
+
if (value === undefined || value.startsWith('--'))
|
|
52
|
+
throw new Error(`${flag} needs a value.`);
|
|
53
|
+
return value;
|
|
40
54
|
};
|
|
41
55
|
const numberFlag = (args, flag) => {
|
|
42
56
|
const raw = flagValue(args, flag);
|
|
43
57
|
if (raw === undefined)
|
|
44
58
|
return undefined;
|
|
45
|
-
const value = Number(raw);
|
|
59
|
+
const value = raw.trim() ? Number(raw) : Number.NaN;
|
|
46
60
|
if (!Number.isFinite(value))
|
|
47
61
|
throw new Error(`${flag} takes a number, not ${raw}.`);
|
|
48
62
|
return value;
|
|
49
63
|
};
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
const countFlag = (args, flag) => {
|
|
65
|
+
const value = numberFlag(args, flag);
|
|
66
|
+
if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
|
|
67
|
+
throw new Error(`${flag} takes a whole number above zero, not ${value}.`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
};
|
|
71
|
+
const LOCAL_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
72
|
+
const LOCAL_CLOCK = /^(\d{1,2}):(\d{2})$/;
|
|
73
|
+
/**
|
|
74
|
+
* Reads an instant flag. A date alone is local midnight and a clock alone is that time today, where
|
|
75
|
+
* `Date` would read the first as UTC and refuse the second; anything else goes to `Date` as it is.
|
|
76
|
+
*/
|
|
77
|
+
const instantOf = (flag, raw) => {
|
|
78
|
+
const date = LOCAL_DATE.exec(raw);
|
|
79
|
+
const clock = LOCAL_CLOCK.exec(raw);
|
|
80
|
+
const today = new Date();
|
|
81
|
+
const at = date
|
|
82
|
+
? new Date(Number(date[1]), Number(date[2]) - 1, Number(date[3]))
|
|
83
|
+
: clock
|
|
84
|
+
? new Date(today.getFullYear(), today.getMonth(), today.getDate(), Number(clock[1]), Number(clock[2]))
|
|
85
|
+
: new Date(raw);
|
|
56
86
|
if (Number.isNaN(at.getTime()))
|
|
57
|
-
throw new Error(
|
|
87
|
+
throw new Error(`${flag} takes a date, not ${raw}.`);
|
|
58
88
|
return at.getTime();
|
|
59
89
|
};
|
|
90
|
+
const instantFlag = (args) => {
|
|
91
|
+
const raw = flagValue(args, '--at');
|
|
92
|
+
return raw ? instantOf('--at', raw) : Date.now();
|
|
93
|
+
};
|
|
60
94
|
const issueLine = (issue) => [
|
|
61
95
|
issue.key,
|
|
62
96
|
issue.issueType,
|
|
@@ -208,6 +242,63 @@ const tempoDayLines = (worklogs) => {
|
|
|
208
242
|
return `${day} ${hours(total)} ${parts.join(' ')}`;
|
|
209
243
|
});
|
|
210
244
|
};
|
|
245
|
+
const syncWriteLine = (write) => {
|
|
246
|
+
var _a;
|
|
247
|
+
return [
|
|
248
|
+
write.kind.padEnd(6),
|
|
249
|
+
(_a = write.issueKey) !== null && _a !== void 0 ? _a : write.proposalId,
|
|
250
|
+
`${clock(write.fromMs)}-${clock(write.fromMs + write.durationMs)}`,
|
|
251
|
+
`${minutesOf(write.durationMs)}m`,
|
|
252
|
+
write.reason,
|
|
253
|
+
...(write.tempoWorklogId ? [`worklog ${write.tempoWorklogId}`] : []),
|
|
254
|
+
write.description || '(no description)',
|
|
255
|
+
...(write.blocked ? [`HELD BACK: ${write.blocked}`] : []),
|
|
256
|
+
].join(' ');
|
|
257
|
+
};
|
|
258
|
+
/** What a sync of a day would write, row by row, and what it leaves alone. */
|
|
259
|
+
const syncPlanLines = (plan) => {
|
|
260
|
+
const count = (kind) => plan.writes.filter((write) => write.kind === kind).length;
|
|
261
|
+
const blocked = plan.writes.filter((write) => write.blocked).length;
|
|
262
|
+
return [
|
|
263
|
+
plan.writes.length
|
|
264
|
+
? `${plan.day} plan ${plan.planHash} ${count('create')} create, ${count('update')} update, ${count('delete')} delete`
|
|
265
|
+
: `${plan.day} nothing to write`,
|
|
266
|
+
...plan.writes.map((write) => ` ${syncWriteLine(write)}`),
|
|
267
|
+
`${plan.unchanged} unchanged, ${plan.skipped} skipped (still awaiting review)`,
|
|
268
|
+
...(blocked ? [`${blocked} row(s) held back: Tempo would refuse them until they are fixed on the day.`] : []),
|
|
269
|
+
...(plan.unresolvedKeys.length
|
|
270
|
+
? [`Jira does not know ${plan.unresolvedKeys.join(', ')}, so nothing is written for those rows.`]
|
|
271
|
+
: []),
|
|
272
|
+
...(plan.coveredMs
|
|
273
|
+
? [`${minutesOf(plan.coveredMs)}m of the day is already logged by foreign worklogs, so it is left out.`]
|
|
274
|
+
: []),
|
|
275
|
+
`Foreign worklogs (${plan.foreign.length}), never touched`,
|
|
276
|
+
...plan.foreign.map((worklog) => { var _a; return ` ${(_a = worklog.issueKey) !== null && _a !== void 0 ? _a : `#${worklog.issueId}`} ${clock(worklog.fromMs)} ${minutesOf(worklog.durationMs)}m ${worklog.description || '(no description)'}`; }),
|
|
277
|
+
];
|
|
278
|
+
};
|
|
279
|
+
/** Every row a write attempted, and what the user has to do about the ones that did not land. */
|
|
280
|
+
const syncRunLines = (run) => [
|
|
281
|
+
...run.rows.map((row) => {
|
|
282
|
+
var _a;
|
|
283
|
+
return [
|
|
284
|
+
row.status.padEnd(7),
|
|
285
|
+
row.kind.padEnd(6),
|
|
286
|
+
(_a = row.issueKey) !== null && _a !== void 0 ? _a : row.proposalId,
|
|
287
|
+
...(row.tempoWorklogId ? [`worklog ${row.tempoWorklogId}`] : []),
|
|
288
|
+
...(row.detail ? [row.detail] : []),
|
|
289
|
+
].join(' ');
|
|
290
|
+
}),
|
|
291
|
+
...(run.retryCount
|
|
292
|
+
? [
|
|
293
|
+
`${run.retryCount} row(s) did not land. Retry them on the Sync page, which retries this run's own plan: a new plan read straight after a write can miss what Tempo just took.`,
|
|
294
|
+
]
|
|
295
|
+
: []),
|
|
296
|
+
...(run.unrecorded
|
|
297
|
+
? [
|
|
298
|
+
`Written, but not recorded: ${run.unrecorded}. Tempo holds these worklogs and the app no longer owns them. Delete them in Tempo before writing this day again, or the time is logged twice.`,
|
|
299
|
+
]
|
|
300
|
+
: []),
|
|
301
|
+
];
|
|
211
302
|
const meetingMs = (events) => events
|
|
212
303
|
.filter((event) => !event.allDay && event.response !== 'declined')
|
|
213
304
|
.reduce((sum, event) => sum + event.endMs - event.startMs, 0);
|
|
@@ -231,13 +322,6 @@ const calendarDayLines = (events) => {
|
|
|
231
322
|
...dayEvents.map((event) => ` ${calendarEventLine(event)}`),
|
|
232
323
|
]);
|
|
233
324
|
};
|
|
234
|
-
/** `--from` and `--to` take anything `Date` reads, so a caller may pass an ISO instant or a clock. */
|
|
235
|
-
const instantOf = (flag, raw) => {
|
|
236
|
-
const at = new Date(raw);
|
|
237
|
-
if (Number.isNaN(at.getTime()))
|
|
238
|
-
throw new Error(`${flag} takes a date, not ${raw}.`);
|
|
239
|
-
return at.getTime();
|
|
240
|
-
};
|
|
241
325
|
/** Which edits the flags name. `--from` with `--to` is the one pair that names a single change. */
|
|
242
326
|
const namedEdits = (argv) => {
|
|
243
327
|
const named = argv.includes('--from') || argv.includes('--to') ? ['--from/--to'] : [];
|
|
@@ -305,7 +389,7 @@ const writeExport = (options) => {
|
|
|
305
389
|
(0, fs_1.unlinkSync)(path);
|
|
306
390
|
const handle = (0, fs_1.openSync)(path, 'wx', 0o600);
|
|
307
391
|
try {
|
|
308
|
-
(0, fs_1.
|
|
392
|
+
(0, fs_1.writeFileSync)(handle, data);
|
|
309
393
|
}
|
|
310
394
|
finally {
|
|
311
395
|
(0, fs_1.closeSync)(handle);
|
|
@@ -317,6 +401,49 @@ const printed = (value, json) => {
|
|
|
317
401
|
console.log(JSON.stringify(value, null, 2));
|
|
318
402
|
return 0;
|
|
319
403
|
};
|
|
404
|
+
/** Every write waits in the app for the user's press, so what a write prints is where to look next. */
|
|
405
|
+
const queuedLines = (queued, what) => [
|
|
406
|
+
`Queued in Timetrack for the user's approval: ${what}.`,
|
|
407
|
+
`Nothing is written until they approve it. Read the outcome with: timetrack approval ${queued.approvalId}`,
|
|
408
|
+
];
|
|
409
|
+
const printedQueued = (options) => {
|
|
410
|
+
if (!options.json)
|
|
411
|
+
queuedLines(options.queued, options.what).forEach(say);
|
|
412
|
+
return printed(options.queued, options.json);
|
|
413
|
+
};
|
|
414
|
+
const isSyncWithRun = (value) => typeof value === 'object' && value !== null && 'run' in value && typeof value.run === 'object';
|
|
415
|
+
const UNDECIDED = {
|
|
416
|
+
queued: "still waits for the user's approval in Timetrack.",
|
|
417
|
+
rejected: 'the user rejected it. Nothing was written.',
|
|
418
|
+
expired: 'expired at the end of the day it was asked on. Nothing was written. Ask again.',
|
|
419
|
+
};
|
|
420
|
+
const approvalLines = (found) => {
|
|
421
|
+
if (found.status !== 'approved')
|
|
422
|
+
return [`${found.approvalId} ${UNDECIDED[found.status]}`];
|
|
423
|
+
if (found.error !== undefined)
|
|
424
|
+
return [`${found.approvalId} approved, but it failed: ${found.error}`];
|
|
425
|
+
const unrecorded = unrecordedOf(found.result);
|
|
426
|
+
return [
|
|
427
|
+
`${found.approvalId} approved and carried out.`,
|
|
428
|
+
...(isSyncWithRun(found.result) ? syncRunLines(found.result.run) : ['Pass --json for what it answered.']),
|
|
429
|
+
...(unrecorded && !isSyncWithRun(found.result)
|
|
430
|
+
? [`Carried out, but the app's ledger did not follow: ${unrecorded}. A later sync may undo it.`]
|
|
431
|
+
: []),
|
|
432
|
+
];
|
|
433
|
+
};
|
|
434
|
+
const unrecordedOf = (value) => typeof value === 'object' && value !== null && 'unrecorded' in value && typeof value.unrecorded === 'string'
|
|
435
|
+
? value.unrecorded
|
|
436
|
+
: undefined;
|
|
437
|
+
/** Non-zero for anything that did not land in full, so a caller polling in a script can tell. */
|
|
438
|
+
const approvalExitCode = (found) => {
|
|
439
|
+
if (found.status === 'queued')
|
|
440
|
+
return 0;
|
|
441
|
+
if (found.status !== 'approved' || found.error !== undefined)
|
|
442
|
+
return 1;
|
|
443
|
+
if (unrecordedOf(found.result))
|
|
444
|
+
return 1;
|
|
445
|
+
return isSyncWithRun(found.result) && (found.result.run.retryCount || found.result.run.unrecorded) ? 1 : 0;
|
|
446
|
+
};
|
|
320
447
|
/**
|
|
321
448
|
* Cuts a placeholder that covered a whole checkout into one per directory its commits worked in.
|
|
322
449
|
*
|
|
@@ -352,16 +479,8 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
|
|
|
352
479
|
const claim = flagValue(argv, '--claim');
|
|
353
480
|
const apply = argv.includes('--force');
|
|
354
481
|
const projectRoots = (0, git_1.projectRootsOf)(checkout);
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
branch,
|
|
358
|
-
repoPath,
|
|
359
|
-
commits,
|
|
360
|
-
projectRoots,
|
|
361
|
-
paths,
|
|
362
|
-
claim,
|
|
363
|
-
apply,
|
|
364
|
-
});
|
|
482
|
+
const request = { id, branch, repoPath, commits, projectRoots, paths, claim };
|
|
483
|
+
const answer = yield (0, timetrack_1.timetrackSplitStandIn)(request);
|
|
365
484
|
if (!json) {
|
|
366
485
|
say(`${standIn.name} ${standIn.days.length} day(s), branch ${branch}`);
|
|
367
486
|
say(`Directories the commits name (${answer.candidates.length})`);
|
|
@@ -369,10 +488,7 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
|
|
|
369
488
|
if (!answer.pieces.length) {
|
|
370
489
|
say('None of them is an automatic grain. Pick the pieces with --paths <dir>,<dir>.');
|
|
371
490
|
}
|
|
372
|
-
else if (apply) {
|
|
373
|
-
say(`Split into ${answer.pieces.length}, and the rule that named it rewritten one per directory.`);
|
|
374
|
-
}
|
|
375
|
-
else {
|
|
491
|
+
else if (!apply) {
|
|
376
492
|
say(`Would write ${answer.pieces.map((piece) => piece.workPath).join(', ')}. Pass --force to carry it out.`);
|
|
377
493
|
}
|
|
378
494
|
if (claim) {
|
|
@@ -387,11 +503,38 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
|
|
|
387
503
|
say('Name them in the day review, or re-run with --claim <dir> to give them a directory.');
|
|
388
504
|
}
|
|
389
505
|
}
|
|
506
|
+
if (apply && answer.pieces.length) {
|
|
507
|
+
const queued = yield (0, timetrack_1.timetrackApplyStandInSplit)(request);
|
|
508
|
+
return printedQueued({ queued, what: `split ${standIn.name} into ${answer.pieces.length}`, json });
|
|
509
|
+
}
|
|
390
510
|
return printed(answer, json);
|
|
391
511
|
});
|
|
512
|
+
const daysBetween = (from, to) => {
|
|
513
|
+
const days = [];
|
|
514
|
+
for (let day = from; day <= to && days.length < 92; day = shiftDay(day, 1))
|
|
515
|
+
days.push(day);
|
|
516
|
+
return days;
|
|
517
|
+
};
|
|
518
|
+
const gitState = (root) => {
|
|
519
|
+
try {
|
|
520
|
+
const uncommitted = (0, git_1.git)({ root, args: ['status', '--porcelain'] })
|
|
521
|
+
.split('\n')
|
|
522
|
+
.filter(Boolean)
|
|
523
|
+
.map((line) => line.slice(3));
|
|
524
|
+
return { head: (0, git_1.git)({ root, args: ['rev-parse', 'HEAD'] }), uncommitted };
|
|
525
|
+
}
|
|
526
|
+
catch (_a) {
|
|
527
|
+
return {};
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
const takeSnapshot = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
|
|
531
|
+
return (Object.assign(Object.assign({ takenAt: new Date().toISOString() }, gitState(options.root)), { days: yield options.days.reduce((done, day) => tslib_1.__awaiter(void 0, void 0, void 0, function* () { return [...(yield done), (0, timetrack_snapshot_1.snapshotDayOf)(yield (0, timetrack_1.timetrackDayRows)(day))]; }), Promise.resolve([])) }));
|
|
532
|
+
});
|
|
533
|
+
const defaultSnapshotPath = () => (0, path_1.join)((0, path_1.dirname)((0, timetrack_1.timetrackDiscoveryPath)()), 'snapshots', `${new Date().toISOString().replace(/[:.]/g, '-')}.json`);
|
|
392
534
|
const USAGE = `ethlete-agents timetrack — ask the running Timetrack app about Jira
|
|
393
535
|
|
|
394
536
|
The app holds this machine's Jira credentials, so no repository needs a token of its own.
|
|
537
|
+
Every write waits in the app until the user approves it there, and prints an approval id.
|
|
395
538
|
|
|
396
539
|
timetrack status Whether the app is reachable, and which projects it holds
|
|
397
540
|
timetrack instance The instance's own levels and its branch-subject candidates
|
|
@@ -403,6 +546,10 @@ The app holds this machine's Jira credentials, so no repository needs a token of
|
|
|
403
546
|
Add a row nothing observed to the day it belongs to
|
|
404
547
|
timetrack day [YYYY-MM-DD] The evidence a day holds, which the encrypted store hides otherwise
|
|
405
548
|
timetrack rows [YYYY-MM-DD] The rows the day drew, with the ids an edit names them by
|
|
549
|
+
timetrack snapshot [from] [to] [--out <file>]
|
|
550
|
+
Record the rows of the last 7 days (default) before a change
|
|
551
|
+
timetrack snapshot --compare <file>
|
|
552
|
+
Print every row the change altered since; exit 1 when any did
|
|
406
553
|
timetrack edit <row-id> … Change one row of a day: its times, its name, its note or its state
|
|
407
554
|
timetrack rules The rules that name a day's work: attribution, project links, apps
|
|
408
555
|
timetrack standins The names the user gave work Jira does not hold yet, and their age
|
|
@@ -410,6 +557,8 @@ The app holds this machine's Jira credentials, so no repository needs a token of
|
|
|
410
557
|
Delete one placeholder, and the rule that named it
|
|
411
558
|
timetrack standins --rename <id> --name <text>
|
|
412
559
|
Give one placeholder another name, keeping its days and its rules
|
|
560
|
+
timetrack standins --merge <id> --into <id>
|
|
561
|
+
Fold one open placeholder into another, with its days and its rules
|
|
413
562
|
timetrack standins --split <id> [--repo <dir>] [--paths <dir>,<dir>] [--claim <dir>] [--author <email>]
|
|
414
563
|
[--force]
|
|
415
564
|
Cut one that covered a whole checkout into one per directory
|
|
@@ -417,8 +566,14 @@ The app holds this machine's Jira credentials, so no repository needs a token of
|
|
|
417
566
|
The account's own Tempo worklogs per day (default: the last 7 days)
|
|
418
567
|
timetrack calendar [from] [to]
|
|
419
568
|
The watched calendars' events per day (default: the last 7 days)
|
|
569
|
+
timetrack sync <YYYY-MM-DD> What a Tempo sync of the day would write, as the Sync page plans it
|
|
570
|
+
timetrack sync <YYYY-MM-DD> --write --plan <hash>
|
|
571
|
+
Write that plan to Tempo, once the user confirmed its rows
|
|
572
|
+
timetrack worklog --delete <id> --day <YYYY-MM-DD>
|
|
573
|
+
Delete one of the account's own Tempo worklogs, once the user asked for it
|
|
420
574
|
timetrack naming [YYYY-MM-DD] Which checkouts the day offers a name for, and why the rest do not
|
|
421
575
|
timetrack resync [path…] Read the agent session logs of checkouts again, after they got a link
|
|
576
|
+
timetrack approval <id> Where a queued write stands, and what it answered once approved
|
|
422
577
|
|
|
423
578
|
Options for search
|
|
424
579
|
--project <KEY> Search this project instead of the picked ones
|
|
@@ -446,6 +601,11 @@ Options for day
|
|
|
446
601
|
Options for resync
|
|
447
602
|
--replace Overwrite what the store holds for those sessions, after a parser fix
|
|
448
603
|
|
|
604
|
+
Options for sync
|
|
605
|
+
--write Queue the write of the plan. Refused without --plan; the app refuses it on
|
|
606
|
+
approval when the plan changed since
|
|
607
|
+
--plan <hash> The plan hash the read printed, for the rows the user confirmed
|
|
608
|
+
|
|
449
609
|
Options for edit — pass exactly one change
|
|
450
610
|
--day <YYYY-MM-DD> The day the row is on (default: today)
|
|
451
611
|
--from <date> --to <date>
|
|
@@ -468,7 +628,7 @@ Options everywhere
|
|
|
468
628
|
* one per checkout.
|
|
469
629
|
*/
|
|
470
630
|
const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
|
|
471
|
-
var _a, _b, _c;
|
|
631
|
+
var _a, _b, _c, _d, _e, _f;
|
|
472
632
|
const { root, argv } = options;
|
|
473
633
|
const [subcommand, value] = positionalArgs(argv);
|
|
474
634
|
const json = argv.includes('--json');
|
|
@@ -507,7 +667,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
507
667
|
text: value,
|
|
508
668
|
projectKey: flagValue(argv, '--project'),
|
|
509
669
|
assignedToMe: argv.includes('--mine'),
|
|
510
|
-
limit:
|
|
670
|
+
limit: countFlag(argv, '--limit'),
|
|
511
671
|
});
|
|
512
672
|
if (!json) {
|
|
513
673
|
if (issues.length === 0)
|
|
@@ -517,7 +677,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
517
677
|
return printed(issues, json);
|
|
518
678
|
}
|
|
519
679
|
if (subcommand === 'project') {
|
|
520
|
-
const found = yield (0, timetrack_1.timetrackRepoProject)(value !== null && value !== void 0 ? value :
|
|
680
|
+
const found = yield (0, timetrack_1.timetrackRepoProject)((0, path_1.resolve)(root, value !== null && value !== void 0 ? value : '.'));
|
|
521
681
|
if (!json) {
|
|
522
682
|
const where = found.inherited ? ' (from a directory above it)' : '';
|
|
523
683
|
if (found.private)
|
|
@@ -536,7 +696,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
536
696
|
const summary = flagValue(argv, '--summary');
|
|
537
697
|
if (!summary)
|
|
538
698
|
throw new Error('Pass --summary "…".');
|
|
539
|
-
const
|
|
699
|
+
const queued = yield (0, timetrack_1.timetrackCreateIssue)({
|
|
540
700
|
summary,
|
|
541
701
|
description: flagValue(argv, '--description'),
|
|
542
702
|
projectKey: flagValue(argv, '--project'),
|
|
@@ -544,9 +704,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
544
704
|
parentKey: flagValue(argv, '--parent'),
|
|
545
705
|
subject: flagValue(argv, '--subject'),
|
|
546
706
|
});
|
|
547
|
-
|
|
548
|
-
say(`${issue.key} ${summary}`);
|
|
549
|
-
return printed(issue, json);
|
|
707
|
+
return printedQueued({ queued, what: `file the issue ${summary}`, json });
|
|
550
708
|
}
|
|
551
709
|
if (subcommand === 'log') {
|
|
552
710
|
const issueKey = flagValue(argv, '--issue');
|
|
@@ -555,26 +713,27 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
555
713
|
throw new Error('Pass --issue <KEY>.');
|
|
556
714
|
if (!minutes || minutes <= 0)
|
|
557
715
|
throw new Error('Pass --minutes <n> above zero.');
|
|
558
|
-
const
|
|
716
|
+
const queued = yield (0, timetrack_1.timetrackAddWorklog)({
|
|
559
717
|
issueKey,
|
|
560
718
|
description: flagValue(argv, '--description'),
|
|
561
719
|
fromMs: instantFlag(argv),
|
|
562
720
|
durationMs: Math.round(minutes * 60000),
|
|
563
721
|
});
|
|
564
|
-
if (!json)
|
|
565
|
-
say(
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
return printed(worklog, json);
|
|
722
|
+
if (!json)
|
|
723
|
+
say('Once approved it is a row on the day, not a Tempo entry — the day still needs a sync.');
|
|
724
|
+
return printedQueued({ queued, what: `a ${minutes}m row for ${issueKey}`, json });
|
|
569
725
|
}
|
|
570
726
|
if (subcommand === 'day') {
|
|
571
727
|
const day = value !== null && value !== void 0 ? value : today();
|
|
572
728
|
if (!DAY.test(day))
|
|
573
729
|
throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
|
|
574
730
|
const found = yield (0, timetrack_1.timetrackDayEvents)(day);
|
|
575
|
-
const
|
|
576
|
-
if (
|
|
731
|
+
const outFlag = flagValue(argv, '--out');
|
|
732
|
+
if (outFlag) {
|
|
733
|
+
const out = (0, path_1.resolve)(root, outFlag);
|
|
577
734
|
writeExport({ path: out, data: JSON.stringify(found), overwrite: argv.includes('--overwrite') });
|
|
735
|
+
if (json)
|
|
736
|
+
return printed({ day: found.day, events: found.events.length, out }, json);
|
|
578
737
|
say(`${found.day} ${found.events.length} events written to ${out}, readable by you alone`);
|
|
579
738
|
say('It holds the day as it was observed: window titles, paths and messages. Delete it when you are done.');
|
|
580
739
|
return 0;
|
|
@@ -599,19 +758,43 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
599
758
|
}
|
|
600
759
|
return printed(found, json);
|
|
601
760
|
}
|
|
761
|
+
if (subcommand === 'snapshot') {
|
|
762
|
+
const compare = flagValue(argv, '--compare');
|
|
763
|
+
if (compare) {
|
|
764
|
+
const recorded = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)(root, compare), 'utf8'));
|
|
765
|
+
const now = yield takeSnapshot({ days: recorded.days.map((entry) => entry.day), root });
|
|
766
|
+
const diff = (0, timetrack_snapshot_1.diffSnapshots)(recorded, now);
|
|
767
|
+
if (json)
|
|
768
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
769
|
+
else
|
|
770
|
+
(0, timetrack_snapshot_1.diffLines)(diff).forEach((line) => say(line));
|
|
771
|
+
return diff.changedRows || diff.days.length ? 1 : 0;
|
|
772
|
+
}
|
|
773
|
+
const named = positionalArgs(argv).slice(1);
|
|
774
|
+
const to = (_a = named[1]) !== null && _a !== void 0 ? _a : today();
|
|
775
|
+
const from = (_b = named[0]) !== null && _b !== void 0 ? _b : shiftDay(to, -6);
|
|
776
|
+
if (!DAY.test(from))
|
|
777
|
+
throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
|
|
778
|
+
if (!DAY.test(to))
|
|
779
|
+
throw new Error(`Pass a day as YYYY-MM-DD, not ${to}.`);
|
|
780
|
+
const path = (0, path_1.resolve)(root, (_c = flagValue(argv, '--out')) !== null && _c !== void 0 ? _c : defaultSnapshotPath());
|
|
781
|
+
const recorded = yield takeSnapshot({ days: daysBetween(from, to), root });
|
|
782
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
|
|
783
|
+
(0, fs_1.writeFileSync)(path, JSON.stringify(recorded, null, 2), { mode: 0o600 });
|
|
784
|
+
if (json)
|
|
785
|
+
console.log(JSON.stringify({ path, days: recorded.days.length }));
|
|
786
|
+
else
|
|
787
|
+
say(`Recorded ${recorded.days.length} day(s), ${recorded.days.reduce((sum, day) => sum + day.rows.length, 0)} row(s) in ${path}`);
|
|
788
|
+
return 0;
|
|
789
|
+
}
|
|
602
790
|
if (subcommand === 'edit') {
|
|
603
|
-
const day = (
|
|
791
|
+
const day = (_d = flagValue(argv, '--day')) !== null && _d !== void 0 ? _d : today();
|
|
604
792
|
if (!value)
|
|
605
793
|
throw new Error('Pass the row id to edit, as `timetrack rows` prints it.');
|
|
606
794
|
if (!DAY.test(day))
|
|
607
795
|
throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
say(`${edited.day} ${edited.applied} of 1 edit landed`);
|
|
611
|
-
edited.rows.forEach((row) => say(` ${rowLine(row)}`));
|
|
612
|
-
edited.warnings.forEach((warning) => say(` ! ${warning.kind}: ${warning.detail}`));
|
|
613
|
-
}
|
|
614
|
-
return printed(edited, json);
|
|
796
|
+
const queued = yield (0, timetrack_1.timetrackEditDay)({ day, edits: [editOf({ argv, rowId: value })] });
|
|
797
|
+
return printedQueued({ queued, what: `an edit of row ${value} on ${day}`, json });
|
|
615
798
|
}
|
|
616
799
|
if (subcommand === 'rules') {
|
|
617
800
|
const rules = yield (0, timetrack_1.timetrackRules)();
|
|
@@ -626,17 +809,14 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
626
809
|
}
|
|
627
810
|
if (subcommand === 'resync') {
|
|
628
811
|
const named = positionalArgs(argv).slice(1);
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
say(`Timetrack reads the agent sessions of ${resynced.paths.join(', ')} again${how}.`);
|
|
635
|
-
}
|
|
636
|
-
return printed(resynced, json);
|
|
812
|
+
const paths = (named.length ? named : ['.']).map((path) => (0, path_1.resolve)(root, path));
|
|
813
|
+
const replace = argv.includes('--replace');
|
|
814
|
+
const queued = yield (0, timetrack_1.timetrackResyncAgentSessions)(paths, { replace });
|
|
815
|
+
const how = replace ? ', replacing what it stored for them' : '';
|
|
816
|
+
return printedQueued({ queued, what: `read the agent sessions of ${paths.join(', ')} again${how}`, json });
|
|
637
817
|
}
|
|
638
818
|
if (subcommand === 'worklogs') {
|
|
639
|
-
const to = (
|
|
819
|
+
const to = (_e = positionalArgs(argv)[2]) !== null && _e !== void 0 ? _e : today();
|
|
640
820
|
const from = value !== null && value !== void 0 ? value : shiftDay(to, -6);
|
|
641
821
|
if (!DAY.test(from))
|
|
642
822
|
throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
|
|
@@ -650,8 +830,51 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
650
830
|
}
|
|
651
831
|
return printed(found, json);
|
|
652
832
|
}
|
|
833
|
+
if (subcommand === 'worklog') {
|
|
834
|
+
if (argv.includes('--help')) {
|
|
835
|
+
say(USAGE);
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
const worklogId = flagValue(argv, '--delete');
|
|
839
|
+
const day = flagValue(argv, '--day');
|
|
840
|
+
if (!worklogId || !/^\d+$/.test(worklogId))
|
|
841
|
+
throw new Error('Pass the Tempo worklog id to delete with --delete <id>.');
|
|
842
|
+
if (!day || !DAY.test(day))
|
|
843
|
+
throw new Error('Pass the day the worklog is on with --day <YYYY-MM-DD>.');
|
|
844
|
+
const queued = yield (0, timetrack_1.timetrackTempoDelete)({ day, worklogId });
|
|
845
|
+
return printedQueued({ queued, what: `delete Tempo worklog ${worklogId} on ${day}`, json });
|
|
846
|
+
}
|
|
847
|
+
if (subcommand === 'sync') {
|
|
848
|
+
if (!value || !DAY.test(value))
|
|
849
|
+
throw new Error('Pass the day to sync as YYYY-MM-DD.');
|
|
850
|
+
const planHash = flagValue(argv, '--plan');
|
|
851
|
+
const write = argv.includes('--write');
|
|
852
|
+
if (write && !planHash) {
|
|
853
|
+
say(`A write names the plan the user confirmed. Read it with \`timetrack sync ${value}\`, show the rows,`);
|
|
854
|
+
say('and pass the hash it prints with --plan once they agree.');
|
|
855
|
+
return 1;
|
|
856
|
+
}
|
|
857
|
+
if (planHash && !write) {
|
|
858
|
+
say('--plan only means something with --write. Nothing was written.');
|
|
859
|
+
return 1;
|
|
860
|
+
}
|
|
861
|
+
if (write && planHash) {
|
|
862
|
+
const queued = yield (0, timetrack_1.timetrackTempoSyncWrite)({ day: value, planHash });
|
|
863
|
+
if (!json)
|
|
864
|
+
say('The app checks the plan again on approval, and refuses it if the day changed since.');
|
|
865
|
+
return printedQueued({ queued, what: `write plan ${planHash} of ${value} to Tempo`, json });
|
|
866
|
+
}
|
|
867
|
+
const found = yield (0, timetrack_1.timetrackTempoSync)(value);
|
|
868
|
+
if (!json) {
|
|
869
|
+
syncPlanLines(found).forEach(say);
|
|
870
|
+
if (found.writes.length) {
|
|
871
|
+
say(`Once the user has confirmed these rows: timetrack sync ${found.day} --write --plan ${found.planHash}`);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return printed(found, json);
|
|
875
|
+
}
|
|
653
876
|
if (subcommand === 'calendar') {
|
|
654
|
-
const to = (
|
|
877
|
+
const to = (_f = positionalArgs(argv)[2]) !== null && _f !== void 0 ? _f : today();
|
|
655
878
|
const from = value !== null && value !== void 0 ? value : shiftDay(to, -6);
|
|
656
879
|
if (!DAY.test(from))
|
|
657
880
|
throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
|
|
@@ -665,7 +888,9 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
665
888
|
return printed(found, json);
|
|
666
889
|
}
|
|
667
890
|
if (subcommand === 'naming') {
|
|
668
|
-
const day = value &&
|
|
891
|
+
const day = value !== null && value !== void 0 ? value : today();
|
|
892
|
+
if (!DAY.test(day))
|
|
893
|
+
throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
|
|
669
894
|
const naming = yield (0, timetrack_1.timetrackNaming)(day);
|
|
670
895
|
if (!json) {
|
|
671
896
|
say(`${day} tempo ${naming.tempoReady ? 'configured' : 'not configured'}`);
|
|
@@ -679,6 +904,15 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
679
904
|
}
|
|
680
905
|
return printed(naming, json);
|
|
681
906
|
}
|
|
907
|
+
if (subcommand === 'approval') {
|
|
908
|
+
if (!value)
|
|
909
|
+
throw new Error('Pass the approval id a write printed, e.g. `timetrack approval <id>`.');
|
|
910
|
+
const found = yield (0, timetrack_1.timetrackApprovalStatus)(value);
|
|
911
|
+
if (!json)
|
|
912
|
+
approvalLines(found).forEach(say);
|
|
913
|
+
printed(found, json);
|
|
914
|
+
return approvalExitCode(found);
|
|
915
|
+
}
|
|
682
916
|
if (subcommand === 'standins') {
|
|
683
917
|
const split = flagValue(argv, '--split');
|
|
684
918
|
if (split)
|
|
@@ -690,10 +924,18 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
690
924
|
return 1;
|
|
691
925
|
}
|
|
692
926
|
if (rename && name) {
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
927
|
+
const queued = yield (0, timetrack_1.timetrackRenameStandIn)({ id: rename, name });
|
|
928
|
+
return printedQueued({ queued, what: `rename ${rename} to ${name}`, json });
|
|
929
|
+
}
|
|
930
|
+
const merge = flagValue(argv, '--merge');
|
|
931
|
+
const into = flagValue(argv, '--into');
|
|
932
|
+
if (merge && !into) {
|
|
933
|
+
say(`A merge needs the placeholder it goes into. Pass --into <id> with --merge ${merge}.`);
|
|
934
|
+
return 1;
|
|
935
|
+
}
|
|
936
|
+
if (merge && into) {
|
|
937
|
+
const queued = yield (0, timetrack_1.timetrackMergeStandIn)({ id: merge, into });
|
|
938
|
+
return printedQueued({ queued, what: `merge ${merge} into ${into}`, json });
|
|
697
939
|
}
|
|
698
940
|
const remove = flagValue(argv, '--remove');
|
|
699
941
|
const stranded = remove ? yield strandedDays(remove) : [];
|
|
@@ -703,14 +945,16 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
703
945
|
say('The app reopens a placeholder for today only. Pass --force if that is what you want.');
|
|
704
946
|
return 1;
|
|
705
947
|
}
|
|
706
|
-
|
|
948
|
+
if (remove) {
|
|
949
|
+
const queued = yield (0, timetrack_1.timetrackRemoveStandIn)(remove);
|
|
950
|
+
return printedQueued({ queued, what: `delete ${remove} and the rule that named it`, json });
|
|
951
|
+
}
|
|
952
|
+
const standIns = yield (0, timetrack_1.timetrackStandIns)();
|
|
707
953
|
const rules = json ? [] : (yield (0, timetrack_1.timetrackRules)()).attributionRules;
|
|
708
954
|
const open = standIns
|
|
709
955
|
.filter((standIn) => standIn.state === 'open')
|
|
710
956
|
.sort((left, right) => left.createdAtMs - right.createdAtMs);
|
|
711
957
|
if (!json) {
|
|
712
|
-
if (remove)
|
|
713
|
-
say(`Deleted ${remove}, and the rule that named it.`);
|
|
714
958
|
say(`${open.length} open, ${standIns.length - open.length} resolved`);
|
|
715
959
|
open.forEach((standIn) => say(` ${describeStandIn({ standIn, rules })}`));
|
|
716
960
|
if (open.length)
|