@ethlete/agent-rules 0.1.0-next.16 → 0.1.0-next.18
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 +50 -0
- package/README.md +14 -6
- package/content/hooks/subagent-model-policy.py +5 -3
- package/content/rules/app-styling.md +47 -0
- package/content/rules/comments.md +4 -1
- package/content/rules/nx-layout.md +47 -0
- package/content/rules/styling.md +1 -1
- package/content/rules/subagent-models.md +7 -7
- package/content/skills/angular-patterns/SKILL.md +23 -0
- package/content/skills/app-testing/SKILL.md +141 -0
- package/content/skills/design-exploration/SKILL.md +3 -1
- package/content/skills/git-flow/SKILL.md +4 -4
- package/content/skills/query/SKILL.md +150 -82
- package/content/skills/rxjs-signals/SKILL.md +20 -0
- package/content/skills/sdk-docs/SKILL.md +35 -7
- package/content/skills/sdk-update/SKILL.md +12 -7
- package/content/skills/story-styling/SKILL.md +7 -6
- package/content/skills/styleguide/lint-rule-lookup.md +1 -1
- package/content/skills/theming/SKILL.md +3 -4
- package/content/skills/timetrack/SKILL.md +154 -49
- package/content/skills/verify-in-app/SKILL.md +107 -0
- package/migrations/app-styling-utilities.md +114 -0
- package/migrations/list-state-query-form.md +103 -0
- package/migrations/nx-layout.md +23 -0
- package/migrations/sdk-components-over-hand-built-ui.md +69 -0
- package/migrations/search-query-field.md +45 -0
- package/migrations.json +43 -0
- package/package.json +4 -1
- package/src/index.js +5 -4
- package/src/index.js.map +1 -1
- package/src/lib/config.d.ts +2 -0
- package/src/lib/config.js +16 -3
- package/src/lib/config.js.map +1 -1
- package/src/lib/filter.d.ts +2 -0
- package/src/lib/filter.js +4 -4
- package/src/lib/filter.js.map +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/package-runner.d.ts +1 -0
- package/src/lib/package-runner.js +34 -0
- package/src/lib/package-runner.js.map +1 -0
- package/src/lib/plan.js +29 -3
- 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 +22 -12
- 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 +344 -60
- package/src/lib/timetrack-command.js.map +1 -1
- package/src/lib/timetrack.d.ts +185 -16
- package/src/lib/timetrack.js +66 -15
- package/src/lib/timetrack.js.map +1 -1
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.timetrackCommand = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
const fs_1 = require("fs");
|
|
6
|
+
const path_1 = require("path");
|
|
6
7
|
const timetrack_1 = require("./timetrack");
|
|
7
8
|
const plain_text_1 = require("./plain-text");
|
|
8
9
|
const git_1 = require("./git");
|
|
@@ -29,33 +30,65 @@ const FLAGS_WITH_VALUE = [
|
|
|
29
30
|
'--paths',
|
|
30
31
|
'--claim',
|
|
31
32
|
'--author',
|
|
33
|
+
'--plan',
|
|
34
|
+
'--delete',
|
|
35
|
+
'--repo',
|
|
36
|
+
'--name',
|
|
37
|
+
'--rename',
|
|
38
|
+
'--merge',
|
|
39
|
+
'--into',
|
|
32
40
|
];
|
|
33
41
|
/** Every human-readable line, with anything a terminal would act on printed rather than obeyed. */
|
|
34
42
|
const say = (line) => console.log((0, plain_text_1.plain)(line));
|
|
35
43
|
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 : ''); });
|
|
36
44
|
const flagValue = (args, flag) => {
|
|
37
45
|
const index = args.indexOf(flag);
|
|
38
|
-
|
|
46
|
+
if (index === -1)
|
|
47
|
+
return undefined;
|
|
48
|
+
const value = args[index + 1];
|
|
49
|
+
if (value === undefined || value.startsWith('--'))
|
|
50
|
+
throw new Error(`${flag} needs a value.`);
|
|
51
|
+
return value;
|
|
39
52
|
};
|
|
40
53
|
const numberFlag = (args, flag) => {
|
|
41
54
|
const raw = flagValue(args, flag);
|
|
42
55
|
if (raw === undefined)
|
|
43
56
|
return undefined;
|
|
44
|
-
const value = Number(raw);
|
|
57
|
+
const value = raw.trim() ? Number(raw) : Number.NaN;
|
|
45
58
|
if (!Number.isFinite(value))
|
|
46
59
|
throw new Error(`${flag} takes a number, not ${raw}.`);
|
|
47
60
|
return value;
|
|
48
61
|
};
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
62
|
+
const countFlag = (args, flag) => {
|
|
63
|
+
const value = numberFlag(args, flag);
|
|
64
|
+
if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
|
|
65
|
+
throw new Error(`${flag} takes a whole number above zero, not ${value}.`);
|
|
66
|
+
}
|
|
67
|
+
return value;
|
|
68
|
+
};
|
|
69
|
+
const LOCAL_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
70
|
+
const LOCAL_CLOCK = /^(\d{1,2}):(\d{2})$/;
|
|
71
|
+
/**
|
|
72
|
+
* Reads an instant flag. A date alone is local midnight and a clock alone is that time today, where
|
|
73
|
+
* `Date` would read the first as UTC and refuse the second; anything else goes to `Date` as it is.
|
|
74
|
+
*/
|
|
75
|
+
const instantOf = (flag, raw) => {
|
|
76
|
+
const date = LOCAL_DATE.exec(raw);
|
|
77
|
+
const clock = LOCAL_CLOCK.exec(raw);
|
|
78
|
+
const today = new Date();
|
|
79
|
+
const at = date
|
|
80
|
+
? new Date(Number(date[1]), Number(date[2]) - 1, Number(date[3]))
|
|
81
|
+
: clock
|
|
82
|
+
? new Date(today.getFullYear(), today.getMonth(), today.getDate(), Number(clock[1]), Number(clock[2]))
|
|
83
|
+
: new Date(raw);
|
|
55
84
|
if (Number.isNaN(at.getTime()))
|
|
56
|
-
throw new Error(
|
|
85
|
+
throw new Error(`${flag} takes a date, not ${raw}.`);
|
|
57
86
|
return at.getTime();
|
|
58
87
|
};
|
|
88
|
+
const instantFlag = (args) => {
|
|
89
|
+
const raw = flagValue(args, '--at');
|
|
90
|
+
return raw ? instantOf('--at', raw) : Date.now();
|
|
91
|
+
};
|
|
59
92
|
const issueLine = (issue) => [
|
|
60
93
|
issue.key,
|
|
61
94
|
issue.issueType,
|
|
@@ -182,12 +215,110 @@ const rowLine = (row) => {
|
|
|
182
215
|
(_c = row.laneKey) !== null && _c !== void 0 ? _c : 'no lane',
|
|
183
216
|
].join(' ');
|
|
184
217
|
};
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
218
|
+
const shiftDay = (day, days) => {
|
|
219
|
+
const [year, month, date] = day.split('-').map(Number);
|
|
220
|
+
const shifted = new Date(year, month - 1, date + days);
|
|
221
|
+
return `${shifted.getFullYear()}-${pad(shifted.getMonth() + 1)}-${pad(shifted.getDate())}`;
|
|
222
|
+
};
|
|
223
|
+
const minutesOf = (ms) => Math.round(ms / MINUTE_MS);
|
|
224
|
+
const tempoDayLines = (worklogs) => {
|
|
225
|
+
var _a, _b, _c;
|
|
226
|
+
const days = new Map();
|
|
227
|
+
for (const worklog of worklogs) {
|
|
228
|
+
const issues = (_a = days.get(worklog.day)) !== null && _a !== void 0 ? _a : new Map();
|
|
229
|
+
const key = (_b = worklog.issueKey) !== null && _b !== void 0 ? _b : `#${worklog.issueId}`;
|
|
230
|
+
issues.set(key, ((_c = issues.get(key)) !== null && _c !== void 0 ? _c : 0) + worklog.durationMs);
|
|
231
|
+
days.set(worklog.day, issues);
|
|
232
|
+
}
|
|
233
|
+
return [...days]
|
|
234
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
235
|
+
.map(([day, issues]) => {
|
|
236
|
+
const total = [...issues.values()].reduce((sum, ms) => sum + ms, 0);
|
|
237
|
+
const parts = [...issues]
|
|
238
|
+
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
239
|
+
.map(([key, ms]) => `${key} ${minutesOf(ms)}m`);
|
|
240
|
+
return `${day} ${hours(total)} ${parts.join(' ')}`;
|
|
241
|
+
});
|
|
242
|
+
};
|
|
243
|
+
const syncWriteLine = (write) => {
|
|
244
|
+
var _a;
|
|
245
|
+
return [
|
|
246
|
+
write.kind.padEnd(6),
|
|
247
|
+
(_a = write.issueKey) !== null && _a !== void 0 ? _a : write.proposalId,
|
|
248
|
+
`${clock(write.fromMs)}-${clock(write.fromMs + write.durationMs)}`,
|
|
249
|
+
`${minutesOf(write.durationMs)}m`,
|
|
250
|
+
write.reason,
|
|
251
|
+
...(write.tempoWorklogId ? [`worklog ${write.tempoWorklogId}`] : []),
|
|
252
|
+
write.description || '(no description)',
|
|
253
|
+
...(write.blocked ? [`HELD BACK: ${write.blocked}`] : []),
|
|
254
|
+
].join(' ');
|
|
255
|
+
};
|
|
256
|
+
/** What a sync of a day would write, row by row, and what it leaves alone. */
|
|
257
|
+
const syncPlanLines = (plan) => {
|
|
258
|
+
const count = (kind) => plan.writes.filter((write) => write.kind === kind).length;
|
|
259
|
+
const blocked = plan.writes.filter((write) => write.blocked).length;
|
|
260
|
+
return [
|
|
261
|
+
plan.writes.length
|
|
262
|
+
? `${plan.day} plan ${plan.planHash} ${count('create')} create, ${count('update')} update, ${count('delete')} delete`
|
|
263
|
+
: `${plan.day} nothing to write`,
|
|
264
|
+
...plan.writes.map((write) => ` ${syncWriteLine(write)}`),
|
|
265
|
+
`${plan.unchanged} unchanged, ${plan.skipped} skipped (still awaiting review)`,
|
|
266
|
+
...(blocked ? [`${blocked} row(s) held back: Tempo would refuse them until they are fixed on the day.`] : []),
|
|
267
|
+
...(plan.unresolvedKeys.length
|
|
268
|
+
? [`Jira does not know ${plan.unresolvedKeys.join(', ')}, so nothing is written for those rows.`]
|
|
269
|
+
: []),
|
|
270
|
+
...(plan.coveredMs
|
|
271
|
+
? [`${minutesOf(plan.coveredMs)}m of the day is already logged by foreign worklogs, so it is left out.`]
|
|
272
|
+
: []),
|
|
273
|
+
`Foreign worklogs (${plan.foreign.length}), never touched`,
|
|
274
|
+
...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)'}`; }),
|
|
275
|
+
];
|
|
276
|
+
};
|
|
277
|
+
/** Every row a write attempted, and what the user has to do about the ones that did not land. */
|
|
278
|
+
const syncRunLines = (run) => [
|
|
279
|
+
...run.rows.map((row) => {
|
|
280
|
+
var _a;
|
|
281
|
+
return [
|
|
282
|
+
row.status.padEnd(7),
|
|
283
|
+
row.kind.padEnd(6),
|
|
284
|
+
(_a = row.issueKey) !== null && _a !== void 0 ? _a : row.proposalId,
|
|
285
|
+
...(row.tempoWorklogId ? [`worklog ${row.tempoWorklogId}`] : []),
|
|
286
|
+
...(row.detail ? [row.detail] : []),
|
|
287
|
+
].join(' ');
|
|
288
|
+
}),
|
|
289
|
+
...(run.retryCount
|
|
290
|
+
? [
|
|
291
|
+
`${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.`,
|
|
292
|
+
]
|
|
293
|
+
: []),
|
|
294
|
+
...(run.unrecorded
|
|
295
|
+
? [
|
|
296
|
+
`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.`,
|
|
297
|
+
]
|
|
298
|
+
: []),
|
|
299
|
+
];
|
|
300
|
+
const meetingMs = (events) => events
|
|
301
|
+
.filter((event) => !event.allDay && event.response !== 'declined')
|
|
302
|
+
.reduce((sum, event) => sum + event.endMs - event.startMs, 0);
|
|
303
|
+
const calendarEventLine = (event) => {
|
|
304
|
+
const start = new Date(event.startMs);
|
|
305
|
+
const when = event.allDay
|
|
306
|
+
? 'all-day'
|
|
307
|
+
: `${pad(start.getHours())}:${pad(start.getMinutes())} ${minutesOf(event.endMs - event.startMs)}m`;
|
|
308
|
+
const answer = event.response === 'accepted' || event.response === 'organizer' ? [] : [event.response];
|
|
309
|
+
return [when, event.title, ...answer].join(' ');
|
|
310
|
+
};
|
|
311
|
+
const calendarDayLines = (events) => {
|
|
312
|
+
var _a;
|
|
313
|
+
const days = new Map();
|
|
314
|
+
for (const event of events)
|
|
315
|
+
days.set(event.day, [...((_a = days.get(event.day)) !== null && _a !== void 0 ? _a : []), event]);
|
|
316
|
+
return [...days]
|
|
317
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
318
|
+
.flatMap(([day, dayEvents]) => [
|
|
319
|
+
`${day} ${dayEvents.length} event(s), ${hours(meetingMs(dayEvents))}`,
|
|
320
|
+
...dayEvents.map((event) => ` ${calendarEventLine(event)}`),
|
|
321
|
+
]);
|
|
191
322
|
};
|
|
192
323
|
/** Which edits the flags name. `--from` with `--to` is the one pair that names a single change. */
|
|
193
324
|
const namedEdits = (argv) => {
|
|
@@ -256,7 +387,7 @@ const writeExport = (options) => {
|
|
|
256
387
|
(0, fs_1.unlinkSync)(path);
|
|
257
388
|
const handle = (0, fs_1.openSync)(path, 'wx', 0o600);
|
|
258
389
|
try {
|
|
259
|
-
(0, fs_1.
|
|
390
|
+
(0, fs_1.writeFileSync)(handle, data);
|
|
260
391
|
}
|
|
261
392
|
finally {
|
|
262
393
|
(0, fs_1.closeSync)(handle);
|
|
@@ -268,6 +399,49 @@ const printed = (value, json) => {
|
|
|
268
399
|
console.log(JSON.stringify(value, null, 2));
|
|
269
400
|
return 0;
|
|
270
401
|
};
|
|
402
|
+
/** Every write waits in the app for the user's press, so what a write prints is where to look next. */
|
|
403
|
+
const queuedLines = (queued, what) => [
|
|
404
|
+
`Queued in Timetrack for the user's approval: ${what}.`,
|
|
405
|
+
`Nothing is written until they approve it. Read the outcome with: timetrack approval ${queued.approvalId}`,
|
|
406
|
+
];
|
|
407
|
+
const printedQueued = (options) => {
|
|
408
|
+
if (!options.json)
|
|
409
|
+
queuedLines(options.queued, options.what).forEach(say);
|
|
410
|
+
return printed(options.queued, options.json);
|
|
411
|
+
};
|
|
412
|
+
const isSyncWithRun = (value) => typeof value === 'object' && value !== null && 'run' in value && typeof value.run === 'object';
|
|
413
|
+
const UNDECIDED = {
|
|
414
|
+
queued: "still waits for the user's approval in Timetrack.",
|
|
415
|
+
rejected: 'the user rejected it. Nothing was written.',
|
|
416
|
+
expired: 'expired at the end of the day it was asked on. Nothing was written. Ask again.',
|
|
417
|
+
};
|
|
418
|
+
const approvalLines = (found) => {
|
|
419
|
+
if (found.status !== 'approved')
|
|
420
|
+
return [`${found.approvalId} ${UNDECIDED[found.status]}`];
|
|
421
|
+
if (found.error !== undefined)
|
|
422
|
+
return [`${found.approvalId} approved, but it failed: ${found.error}`];
|
|
423
|
+
const unrecorded = unrecordedOf(found.result);
|
|
424
|
+
return [
|
|
425
|
+
`${found.approvalId} approved and carried out.`,
|
|
426
|
+
...(isSyncWithRun(found.result) ? syncRunLines(found.result.run) : ['Pass --json for what it answered.']),
|
|
427
|
+
...(unrecorded && !isSyncWithRun(found.result)
|
|
428
|
+
? [`Carried out, but the app's ledger did not follow: ${unrecorded}. A later sync may undo it.`]
|
|
429
|
+
: []),
|
|
430
|
+
];
|
|
431
|
+
};
|
|
432
|
+
const unrecordedOf = (value) => typeof value === 'object' && value !== null && 'unrecorded' in value && typeof value.unrecorded === 'string'
|
|
433
|
+
? value.unrecorded
|
|
434
|
+
: undefined;
|
|
435
|
+
/** Non-zero for anything that did not land in full, so a caller polling in a script can tell. */
|
|
436
|
+
const approvalExitCode = (found) => {
|
|
437
|
+
if (found.status === 'queued')
|
|
438
|
+
return 0;
|
|
439
|
+
if (found.status !== 'approved' || found.error !== undefined)
|
|
440
|
+
return 1;
|
|
441
|
+
if (unrecordedOf(found.result))
|
|
442
|
+
return 1;
|
|
443
|
+
return isSyncWithRun(found.result) && (found.result.run.retryCount || found.result.run.unrecorded) ? 1 : 0;
|
|
444
|
+
};
|
|
271
445
|
/**
|
|
272
446
|
* Cuts a placeholder that covered a whole checkout into one per directory its commits worked in.
|
|
273
447
|
*
|
|
@@ -303,16 +477,8 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
|
|
|
303
477
|
const claim = flagValue(argv, '--claim');
|
|
304
478
|
const apply = argv.includes('--force');
|
|
305
479
|
const projectRoots = (0, git_1.projectRootsOf)(checkout);
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
branch,
|
|
309
|
-
repoPath,
|
|
310
|
-
commits,
|
|
311
|
-
projectRoots,
|
|
312
|
-
paths,
|
|
313
|
-
claim,
|
|
314
|
-
apply,
|
|
315
|
-
});
|
|
480
|
+
const request = { id, branch, repoPath, commits, projectRoots, paths, claim };
|
|
481
|
+
const answer = yield (0, timetrack_1.timetrackSplitStandIn)(request);
|
|
316
482
|
if (!json) {
|
|
317
483
|
say(`${standIn.name} ${standIn.days.length} day(s), branch ${branch}`);
|
|
318
484
|
say(`Directories the commits name (${answer.candidates.length})`);
|
|
@@ -320,10 +486,7 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
|
|
|
320
486
|
if (!answer.pieces.length) {
|
|
321
487
|
say('None of them is an automatic grain. Pick the pieces with --paths <dir>,<dir>.');
|
|
322
488
|
}
|
|
323
|
-
else if (apply) {
|
|
324
|
-
say(`Split into ${answer.pieces.length}, and the rule that named it rewritten one per directory.`);
|
|
325
|
-
}
|
|
326
|
-
else {
|
|
489
|
+
else if (!apply) {
|
|
327
490
|
say(`Would write ${answer.pieces.map((piece) => piece.workPath).join(', ')}. Pass --force to carry it out.`);
|
|
328
491
|
}
|
|
329
492
|
if (claim) {
|
|
@@ -338,11 +501,16 @@ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, func
|
|
|
338
501
|
say('Name them in the day review, or re-run with --claim <dir> to give them a directory.');
|
|
339
502
|
}
|
|
340
503
|
}
|
|
504
|
+
if (apply && answer.pieces.length) {
|
|
505
|
+
const queued = yield (0, timetrack_1.timetrackApplyStandInSplit)(request);
|
|
506
|
+
return printedQueued({ queued, what: `split ${standIn.name} into ${answer.pieces.length}`, json });
|
|
507
|
+
}
|
|
341
508
|
return printed(answer, json);
|
|
342
509
|
});
|
|
343
510
|
const USAGE = `ethlete-agents timetrack — ask the running Timetrack app about Jira
|
|
344
511
|
|
|
345
512
|
The app holds this machine's Jira credentials, so no repository needs a token of its own.
|
|
513
|
+
Every write waits in the app until the user approves it there, and prints an approval id.
|
|
346
514
|
|
|
347
515
|
timetrack status Whether the app is reachable, and which projects it holds
|
|
348
516
|
timetrack instance The instance's own levels and its branch-subject candidates
|
|
@@ -361,10 +529,23 @@ The app holds this machine's Jira credentials, so no repository needs a token of
|
|
|
361
529
|
Delete one placeholder, and the rule that named it
|
|
362
530
|
timetrack standins --rename <id> --name <text>
|
|
363
531
|
Give one placeholder another name, keeping its days and its rules
|
|
532
|
+
timetrack standins --merge <id> --into <id>
|
|
533
|
+
Fold one open placeholder into another, with its days and its rules
|
|
364
534
|
timetrack standins --split <id> [--repo <dir>] [--paths <dir>,<dir>] [--claim <dir>] [--author <email>]
|
|
365
535
|
[--force]
|
|
366
536
|
Cut one that covered a whole checkout into one per directory
|
|
537
|
+
timetrack worklogs [from] [to]
|
|
538
|
+
The account's own Tempo worklogs per day (default: the last 7 days)
|
|
539
|
+
timetrack calendar [from] [to]
|
|
540
|
+
The watched calendars' events per day (default: the last 7 days)
|
|
541
|
+
timetrack sync <YYYY-MM-DD> What a Tempo sync of the day would write, as the Sync page plans it
|
|
542
|
+
timetrack sync <YYYY-MM-DD> --write --plan <hash>
|
|
543
|
+
Write that plan to Tempo, once the user confirmed its rows
|
|
544
|
+
timetrack worklog --delete <id> --day <YYYY-MM-DD>
|
|
545
|
+
Delete one of the account's own Tempo worklogs, once the user asked for it
|
|
367
546
|
timetrack naming [YYYY-MM-DD] Which checkouts the day offers a name for, and why the rest do not
|
|
547
|
+
timetrack resync [path…] Read the agent session logs of checkouts again, after they got a link
|
|
548
|
+
timetrack approval <id> Where a queued write stands, and what it answered once approved
|
|
368
549
|
|
|
369
550
|
Options for search
|
|
370
551
|
--project <KEY> Search this project instead of the picked ones
|
|
@@ -389,6 +570,14 @@ Options for day
|
|
|
389
570
|
--out <path> Write the raw answer to a new file, readable by you alone
|
|
390
571
|
--overwrite Replace the file --out names, which is refused otherwise
|
|
391
572
|
|
|
573
|
+
Options for resync
|
|
574
|
+
--replace Overwrite what the store holds for those sessions, after a parser fix
|
|
575
|
+
|
|
576
|
+
Options for sync
|
|
577
|
+
--write Queue the write of the plan. Refused without --plan; the app refuses it on
|
|
578
|
+
approval when the plan changed since
|
|
579
|
+
--plan <hash> The plan hash the read printed, for the rows the user confirmed
|
|
580
|
+
|
|
392
581
|
Options for edit — pass exactly one change
|
|
393
582
|
--day <YYYY-MM-DD> The day the row is on (default: today)
|
|
394
583
|
--from <date> --to <date>
|
|
@@ -411,7 +600,7 @@ Options everywhere
|
|
|
411
600
|
* one per checkout.
|
|
412
601
|
*/
|
|
413
602
|
const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
|
|
414
|
-
var _a;
|
|
603
|
+
var _a, _b, _c;
|
|
415
604
|
const { root, argv } = options;
|
|
416
605
|
const [subcommand, value] = positionalArgs(argv);
|
|
417
606
|
const json = argv.includes('--json');
|
|
@@ -450,7 +639,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
450
639
|
text: value,
|
|
451
640
|
projectKey: flagValue(argv, '--project'),
|
|
452
641
|
assignedToMe: argv.includes('--mine'),
|
|
453
|
-
limit:
|
|
642
|
+
limit: countFlag(argv, '--limit'),
|
|
454
643
|
});
|
|
455
644
|
if (!json) {
|
|
456
645
|
if (issues.length === 0)
|
|
@@ -460,7 +649,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
460
649
|
return printed(issues, json);
|
|
461
650
|
}
|
|
462
651
|
if (subcommand === 'project') {
|
|
463
|
-
const found = yield (0, timetrack_1.timetrackRepoProject)(value !== null && value !== void 0 ? value :
|
|
652
|
+
const found = yield (0, timetrack_1.timetrackRepoProject)((0, path_1.resolve)(root, value !== null && value !== void 0 ? value : '.'));
|
|
464
653
|
if (!json) {
|
|
465
654
|
const where = found.inherited ? ' (from a directory above it)' : '';
|
|
466
655
|
if (found.private)
|
|
@@ -479,7 +668,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
479
668
|
const summary = flagValue(argv, '--summary');
|
|
480
669
|
if (!summary)
|
|
481
670
|
throw new Error('Pass --summary "…".');
|
|
482
|
-
const
|
|
671
|
+
const queued = yield (0, timetrack_1.timetrackCreateIssue)({
|
|
483
672
|
summary,
|
|
484
673
|
description: flagValue(argv, '--description'),
|
|
485
674
|
projectKey: flagValue(argv, '--project'),
|
|
@@ -487,9 +676,7 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
487
676
|
parentKey: flagValue(argv, '--parent'),
|
|
488
677
|
subject: flagValue(argv, '--subject'),
|
|
489
678
|
});
|
|
490
|
-
|
|
491
|
-
say(`${issue.key} ${summary}`);
|
|
492
|
-
return printed(issue, json);
|
|
679
|
+
return printedQueued({ queued, what: `file the issue ${summary}`, json });
|
|
493
680
|
}
|
|
494
681
|
if (subcommand === 'log') {
|
|
495
682
|
const issueKey = flagValue(argv, '--issue');
|
|
@@ -498,26 +685,27 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
498
685
|
throw new Error('Pass --issue <KEY>.');
|
|
499
686
|
if (!minutes || minutes <= 0)
|
|
500
687
|
throw new Error('Pass --minutes <n> above zero.');
|
|
501
|
-
const
|
|
688
|
+
const queued = yield (0, timetrack_1.timetrackAddWorklog)({
|
|
502
689
|
issueKey,
|
|
503
690
|
description: flagValue(argv, '--description'),
|
|
504
691
|
fromMs: instantFlag(argv),
|
|
505
692
|
durationMs: Math.round(minutes * 60000),
|
|
506
693
|
});
|
|
507
|
-
if (!json)
|
|
508
|
-
say(
|
|
509
|
-
|
|
510
|
-
}
|
|
511
|
-
return printed(worklog, json);
|
|
694
|
+
if (!json)
|
|
695
|
+
say('Once approved it is a row on the day, not a Tempo entry — the day still needs a sync.');
|
|
696
|
+
return printedQueued({ queued, what: `a ${minutes}m row for ${issueKey}`, json });
|
|
512
697
|
}
|
|
513
698
|
if (subcommand === 'day') {
|
|
514
699
|
const day = value !== null && value !== void 0 ? value : today();
|
|
515
700
|
if (!DAY.test(day))
|
|
516
701
|
throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
|
|
517
702
|
const found = yield (0, timetrack_1.timetrackDayEvents)(day);
|
|
518
|
-
const
|
|
519
|
-
if (
|
|
703
|
+
const outFlag = flagValue(argv, '--out');
|
|
704
|
+
if (outFlag) {
|
|
705
|
+
const out = (0, path_1.resolve)(root, outFlag);
|
|
520
706
|
writeExport({ path: out, data: JSON.stringify(found), overwrite: argv.includes('--overwrite') });
|
|
707
|
+
if (json)
|
|
708
|
+
return printed({ day: found.day, events: found.events.length, out }, json);
|
|
521
709
|
say(`${found.day} ${found.events.length} events written to ${out}, readable by you alone`);
|
|
522
710
|
say('It holds the day as it was observed: window titles, paths and messages. Delete it when you are done.');
|
|
523
711
|
return 0;
|
|
@@ -548,13 +736,8 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
548
736
|
throw new Error('Pass the row id to edit, as `timetrack rows` prints it.');
|
|
549
737
|
if (!DAY.test(day))
|
|
550
738
|
throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
say(`${edited.day} ${edited.applied} of 1 edit landed`);
|
|
554
|
-
edited.rows.forEach((row) => say(` ${rowLine(row)}`));
|
|
555
|
-
edited.warnings.forEach((warning) => say(` ! ${warning.kind}: ${warning.detail}`));
|
|
556
|
-
}
|
|
557
|
-
return printed(edited, json);
|
|
739
|
+
const queued = yield (0, timetrack_1.timetrackEditDay)({ day, edits: [editOf({ argv, rowId: value })] });
|
|
740
|
+
return printedQueued({ queued, what: `an edit of row ${value} on ${day}`, json });
|
|
558
741
|
}
|
|
559
742
|
if (subcommand === 'rules') {
|
|
560
743
|
const rules = yield (0, timetrack_1.timetrackRules)();
|
|
@@ -567,8 +750,90 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
567
750
|
}
|
|
568
751
|
return printed(rules, json);
|
|
569
752
|
}
|
|
753
|
+
if (subcommand === 'resync') {
|
|
754
|
+
const named = positionalArgs(argv).slice(1);
|
|
755
|
+
const paths = (named.length ? named : ['.']).map((path) => (0, path_1.resolve)(root, path));
|
|
756
|
+
const replace = argv.includes('--replace');
|
|
757
|
+
const queued = yield (0, timetrack_1.timetrackResyncAgentSessions)(paths, { replace });
|
|
758
|
+
const how = replace ? ', replacing what it stored for them' : '';
|
|
759
|
+
return printedQueued({ queued, what: `read the agent sessions of ${paths.join(', ')} again${how}`, json });
|
|
760
|
+
}
|
|
761
|
+
if (subcommand === 'worklogs') {
|
|
762
|
+
const to = (_b = positionalArgs(argv)[2]) !== null && _b !== void 0 ? _b : today();
|
|
763
|
+
const from = value !== null && value !== void 0 ? value : shiftDay(to, -6);
|
|
764
|
+
if (!DAY.test(from))
|
|
765
|
+
throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
|
|
766
|
+
if (!DAY.test(to))
|
|
767
|
+
throw new Error(`Pass a day as YYYY-MM-DD, not ${to}.`);
|
|
768
|
+
const found = yield (0, timetrack_1.timetrackTempoWorklogs)({ from, to });
|
|
769
|
+
if (!json) {
|
|
770
|
+
const total = found.worklogs.reduce((sum, worklog) => sum + worklog.durationMs, 0);
|
|
771
|
+
say(`${found.from} … ${found.to} ${found.worklogs.length} worklog(s), ${hours(total)}`);
|
|
772
|
+
tempoDayLines(found.worklogs).forEach((line) => say(` ${line}`));
|
|
773
|
+
}
|
|
774
|
+
return printed(found, json);
|
|
775
|
+
}
|
|
776
|
+
if (subcommand === 'worklog') {
|
|
777
|
+
if (argv.includes('--help')) {
|
|
778
|
+
say(USAGE);
|
|
779
|
+
return 0;
|
|
780
|
+
}
|
|
781
|
+
const worklogId = flagValue(argv, '--delete');
|
|
782
|
+
const day = flagValue(argv, '--day');
|
|
783
|
+
if (!worklogId || !/^\d+$/.test(worklogId))
|
|
784
|
+
throw new Error('Pass the Tempo worklog id to delete with --delete <id>.');
|
|
785
|
+
if (!day || !DAY.test(day))
|
|
786
|
+
throw new Error('Pass the day the worklog is on with --day <YYYY-MM-DD>.');
|
|
787
|
+
const queued = yield (0, timetrack_1.timetrackTempoDelete)({ day, worklogId });
|
|
788
|
+
return printedQueued({ queued, what: `delete Tempo worklog ${worklogId} on ${day}`, json });
|
|
789
|
+
}
|
|
790
|
+
if (subcommand === 'sync') {
|
|
791
|
+
if (!value || !DAY.test(value))
|
|
792
|
+
throw new Error('Pass the day to sync as YYYY-MM-DD.');
|
|
793
|
+
const planHash = flagValue(argv, '--plan');
|
|
794
|
+
const write = argv.includes('--write');
|
|
795
|
+
if (write && !planHash) {
|
|
796
|
+
say(`A write names the plan the user confirmed. Read it with \`timetrack sync ${value}\`, show the rows,`);
|
|
797
|
+
say('and pass the hash it prints with --plan once they agree.');
|
|
798
|
+
return 1;
|
|
799
|
+
}
|
|
800
|
+
if (planHash && !write) {
|
|
801
|
+
say('--plan only means something with --write. Nothing was written.');
|
|
802
|
+
return 1;
|
|
803
|
+
}
|
|
804
|
+
if (write && planHash) {
|
|
805
|
+
const queued = yield (0, timetrack_1.timetrackTempoSyncWrite)({ day: value, planHash });
|
|
806
|
+
if (!json)
|
|
807
|
+
say('The app checks the plan again on approval, and refuses it if the day changed since.');
|
|
808
|
+
return printedQueued({ queued, what: `write plan ${planHash} of ${value} to Tempo`, json });
|
|
809
|
+
}
|
|
810
|
+
const found = yield (0, timetrack_1.timetrackTempoSync)(value);
|
|
811
|
+
if (!json) {
|
|
812
|
+
syncPlanLines(found).forEach(say);
|
|
813
|
+
if (found.writes.length) {
|
|
814
|
+
say(`Once the user has confirmed these rows: timetrack sync ${found.day} --write --plan ${found.planHash}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return printed(found, json);
|
|
818
|
+
}
|
|
819
|
+
if (subcommand === 'calendar') {
|
|
820
|
+
const to = (_c = positionalArgs(argv)[2]) !== null && _c !== void 0 ? _c : today();
|
|
821
|
+
const from = value !== null && value !== void 0 ? value : shiftDay(to, -6);
|
|
822
|
+
if (!DAY.test(from))
|
|
823
|
+
throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
|
|
824
|
+
if (!DAY.test(to))
|
|
825
|
+
throw new Error(`Pass a day as YYYY-MM-DD, not ${to}.`);
|
|
826
|
+
const found = yield (0, timetrack_1.timetrackCalendarEvents)({ from, to });
|
|
827
|
+
if (!json) {
|
|
828
|
+
say(`${found.from} … ${found.to} ${found.events.length} event(s) from ${found.calendarIds.length} calendar(s), ${hours(meetingMs(found.events))} in meetings`);
|
|
829
|
+
calendarDayLines(found.events).forEach((line) => say(` ${line}`));
|
|
830
|
+
}
|
|
831
|
+
return printed(found, json);
|
|
832
|
+
}
|
|
570
833
|
if (subcommand === 'naming') {
|
|
571
|
-
const day = value &&
|
|
834
|
+
const day = value !== null && value !== void 0 ? value : today();
|
|
835
|
+
if (!DAY.test(day))
|
|
836
|
+
throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
|
|
572
837
|
const naming = yield (0, timetrack_1.timetrackNaming)(day);
|
|
573
838
|
if (!json) {
|
|
574
839
|
say(`${day} tempo ${naming.tempoReady ? 'configured' : 'not configured'}`);
|
|
@@ -582,6 +847,15 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
582
847
|
}
|
|
583
848
|
return printed(naming, json);
|
|
584
849
|
}
|
|
850
|
+
if (subcommand === 'approval') {
|
|
851
|
+
if (!value)
|
|
852
|
+
throw new Error('Pass the approval id a write printed, e.g. `timetrack approval <id>`.');
|
|
853
|
+
const found = yield (0, timetrack_1.timetrackApprovalStatus)(value);
|
|
854
|
+
if (!json)
|
|
855
|
+
approvalLines(found).forEach(say);
|
|
856
|
+
printed(found, json);
|
|
857
|
+
return approvalExitCode(found);
|
|
858
|
+
}
|
|
585
859
|
if (subcommand === 'standins') {
|
|
586
860
|
const split = flagValue(argv, '--split');
|
|
587
861
|
if (split)
|
|
@@ -593,10 +867,18 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
593
867
|
return 1;
|
|
594
868
|
}
|
|
595
869
|
if (rename && name) {
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
870
|
+
const queued = yield (0, timetrack_1.timetrackRenameStandIn)({ id: rename, name });
|
|
871
|
+
return printedQueued({ queued, what: `rename ${rename} to ${name}`, json });
|
|
872
|
+
}
|
|
873
|
+
const merge = flagValue(argv, '--merge');
|
|
874
|
+
const into = flagValue(argv, '--into');
|
|
875
|
+
if (merge && !into) {
|
|
876
|
+
say(`A merge needs the placeholder it goes into. Pass --into <id> with --merge ${merge}.`);
|
|
877
|
+
return 1;
|
|
878
|
+
}
|
|
879
|
+
if (merge && into) {
|
|
880
|
+
const queued = yield (0, timetrack_1.timetrackMergeStandIn)({ id: merge, into });
|
|
881
|
+
return printedQueued({ queued, what: `merge ${merge} into ${into}`, json });
|
|
600
882
|
}
|
|
601
883
|
const remove = flagValue(argv, '--remove');
|
|
602
884
|
const stranded = remove ? yield strandedDays(remove) : [];
|
|
@@ -606,14 +888,16 @@ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0,
|
|
|
606
888
|
say('The app reopens a placeholder for today only. Pass --force if that is what you want.');
|
|
607
889
|
return 1;
|
|
608
890
|
}
|
|
609
|
-
|
|
891
|
+
if (remove) {
|
|
892
|
+
const queued = yield (0, timetrack_1.timetrackRemoveStandIn)(remove);
|
|
893
|
+
return printedQueued({ queued, what: `delete ${remove} and the rule that named it`, json });
|
|
894
|
+
}
|
|
895
|
+
const standIns = yield (0, timetrack_1.timetrackStandIns)();
|
|
610
896
|
const rules = json ? [] : (yield (0, timetrack_1.timetrackRules)()).attributionRules;
|
|
611
897
|
const open = standIns
|
|
612
898
|
.filter((standIn) => standIn.state === 'open')
|
|
613
899
|
.sort((left, right) => left.createdAtMs - right.createdAtMs);
|
|
614
900
|
if (!json) {
|
|
615
|
-
if (remove)
|
|
616
|
-
say(`Deleted ${remove}, and the rule that named it.`);
|
|
617
901
|
say(`${open.length} open, ${standIns.length - open.length} resolved`);
|
|
618
902
|
open.forEach((standIn) => say(` ${describeStandIn({ standIn, rules })}`));
|
|
619
903
|
if (open.length)
|