@ethlete/agent-rules 0.1.0-next.2 → 0.1.0-next.20

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.
Files changed (178) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/README.md +350 -29
  3. package/THIRD-PARTY-LICENSES.md +35 -0
  4. package/content/git-hooks/post-checkout.sh +10 -0
  5. package/content/git-hooks/pre-push.sh +5 -0
  6. package/content/hooks/context-warning.py +766 -108
  7. package/content/hooks/subagent-model-policy.py +187 -0
  8. package/content/output-styles/ste-clarity.md +131 -0
  9. package/content/rules/app-styling.md +59 -0
  10. package/content/rules/comments.md +53 -20
  11. package/content/rules/lint-and-format.md +6 -5
  12. package/content/rules/nx-layout.md +47 -0
  13. package/content/rules/reactive-state.md +1 -1
  14. package/content/rules/styling.md +9 -7
  15. package/content/rules/subagent-models.md +29 -0
  16. package/content/skills/angular-patterns/SKILL.md +30 -7
  17. package/content/skills/api-source/SKILL.md +130 -0
  18. package/content/skills/app-testing/SKILL.md +141 -0
  19. package/content/skills/codex-subagent/SKILL.md +90 -0
  20. package/content/skills/codex-subagent/codex-agent.mjs +229 -0
  21. package/content/skills/design-exploration/SKILL.md +205 -0
  22. package/content/skills/design-exploration/check-story.mjs +139 -0
  23. package/content/skills/design-exploration/shoot-template.mjs +61 -0
  24. package/content/skills/domain-modeling/ADR-FORMAT.md +47 -0
  25. package/content/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  26. package/content/skills/domain-modeling/SKILL.md +80 -0
  27. package/content/skills/figma-export/SKILL.md +191 -0
  28. package/content/skills/figma-export/dump-figma-layers.py +83 -0
  29. package/content/skills/figma-export/dump-figma-svg.py +235 -0
  30. package/content/skills/figma-export/measure-template.mjs +87 -0
  31. package/content/skills/git-commit/SKILL.md +6 -7
  32. package/content/skills/git-flow/SKILL.md +87 -0
  33. package/content/skills/grill-with-docs/SKILL.md +13 -0
  34. package/content/skills/grilling/SKILL.md +34 -0
  35. package/content/skills/handoff/SKILL.md +74 -4
  36. package/content/skills/query/SKILL.md +165 -64
  37. package/content/skills/rxjs-signals/SKILL.md +31 -4
  38. package/content/skills/sdk-docs/SKILL.md +50 -14
  39. package/content/skills/sdk-local-build/SKILL.md +30 -10
  40. package/content/skills/sdk-local-build/sdk-local-baseline.mjs +64 -0
  41. package/content/skills/sdk-source/SKILL.md +49 -21
  42. package/content/skills/sdk-update/SKILL.md +96 -0
  43. package/content/skills/story-styling/SKILL.md +10 -8
  44. package/content/skills/styleguide/SKILL.md +12 -7
  45. package/content/skills/styleguide/assets.md +54 -0
  46. package/content/skills/styleguide/changesets.md +17 -0
  47. package/content/skills/styleguide/file-structure.md +73 -0
  48. package/content/skills/styleguide/lint-rule-lookup.md +51 -0
  49. package/content/skills/styleguide/storybook-structure.md +25 -0
  50. package/content/skills/theming/SKILL.md +41 -28
  51. package/content/skills/timetrack/SKILL.md +397 -0
  52. package/content/skills/verify-in-app/SKILL.md +107 -0
  53. package/migrations/app-styling-utilities.md +63 -0
  54. package/migrations/list-state-query-form.md +20 -0
  55. package/migrations/nx-layout.md +29 -0
  56. package/migrations/sdk-components-over-hand-built-ui.md +27 -0
  57. package/migrations/search-query-field.md +52 -0
  58. package/migrations.json +48 -0
  59. package/package.json +16 -2
  60. package/src/index.js +63 -28
  61. package/src/index.js.map +1 -1
  62. package/src/lib/cli-args.d.ts +9 -0
  63. package/src/lib/cli-args.js +48 -0
  64. package/src/lib/cli-args.js.map +1 -0
  65. package/src/lib/commitlint.d.ts +10 -0
  66. package/src/lib/commitlint.js +51 -0
  67. package/src/lib/commitlint.js.map +1 -0
  68. package/src/lib/config.d.ts +38 -4
  69. package/src/lib/config.js +84 -8
  70. package/src/lib/config.js.map +1 -1
  71. package/src/lib/filter.d.ts +2 -0
  72. package/src/lib/filter.js +4 -4
  73. package/src/lib/filter.js.map +1 -1
  74. package/src/lib/frontmatter.d.ts +2 -0
  75. package/src/lib/frontmatter.js +33 -5
  76. package/src/lib/frontmatter.js.map +1 -1
  77. package/src/lib/git-flow/build.d.ts +35 -0
  78. package/src/lib/git-flow/build.js +24 -0
  79. package/src/lib/git-flow/build.js.map +1 -0
  80. package/src/lib/git-flow/config.d.ts +59 -0
  81. package/src/lib/git-flow/config.js +50 -0
  82. package/src/lib/git-flow/config.js.map +1 -0
  83. package/src/lib/git-flow/index.d.ts +6 -0
  84. package/src/lib/git-flow/index.js +10 -0
  85. package/src/lib/git-flow/index.js.map +1 -0
  86. package/src/lib/git-flow/parse.d.ts +49 -0
  87. package/src/lib/git-flow/parse.js +300 -0
  88. package/src/lib/git-flow/parse.js.map +1 -0
  89. package/src/lib/git-flow/rename.d.ts +24 -0
  90. package/src/lib/git-flow/rename.js +70 -0
  91. package/src/lib/git-flow/rename.js.map +1 -0
  92. package/src/lib/git-flow/start.d.ts +49 -0
  93. package/src/lib/git-flow/start.js +57 -0
  94. package/src/lib/git-flow/start.js.map +1 -0
  95. package/src/lib/git-flow/validate.d.ts +34 -0
  96. package/src/lib/git-flow/validate.js +72 -0
  97. package/src/lib/git-flow/validate.js.map +1 -0
  98. package/src/lib/git-flow-command.d.ts +4 -0
  99. package/src/lib/git-flow-command.js +157 -0
  100. package/src/lib/git-flow-command.js.map +1 -0
  101. package/src/lib/git-flow-repair.d.ts +17 -0
  102. package/src/lib/git-flow-repair.js +157 -0
  103. package/src/lib/git-flow-repair.js.map +1 -0
  104. package/src/lib/git-flow-start.d.ts +20 -0
  105. package/src/lib/git-flow-start.js +132 -0
  106. package/src/lib/git-flow-start.js.map +1 -0
  107. package/src/lib/git.d.ts +64 -0
  108. package/src/lib/git.js +131 -0
  109. package/src/lib/git.js.map +1 -0
  110. package/src/lib/gitlab.d.ts +35 -0
  111. package/src/lib/gitlab.js +108 -0
  112. package/src/lib/gitlab.js.map +1 -0
  113. package/src/lib/index.d.ts +2 -0
  114. package/src/lib/index.js +2 -0
  115. package/src/lib/index.js.map +1 -1
  116. package/src/lib/migrate.js +1 -1
  117. package/src/lib/migrate.js.map +1 -1
  118. package/src/lib/output-style-command.d.ts +3 -0
  119. package/src/lib/output-style-command.js +69 -0
  120. package/src/lib/output-style-command.js.map +1 -0
  121. package/src/lib/output-style.d.ts +38 -0
  122. package/src/lib/output-style.js +134 -0
  123. package/src/lib/output-style.js.map +1 -0
  124. package/src/lib/owned-paths.js +27 -3
  125. package/src/lib/owned-paths.js.map +1 -1
  126. package/src/lib/package-runner.d.ts +3 -0
  127. package/src/lib/package-runner.js +69 -0
  128. package/src/lib/package-runner.js.map +1 -0
  129. package/src/lib/plain-text.d.ts +9 -0
  130. package/src/lib/plain-text.js +22 -0
  131. package/src/lib/plain-text.js.map +1 -0
  132. package/src/lib/plan.d.ts +6 -6
  133. package/src/lib/plan.js +147 -40
  134. package/src/lib/plan.js.map +1 -1
  135. package/src/lib/prompt.d.ts +8 -0
  136. package/src/lib/prompt.js +27 -0
  137. package/src/lib/prompt.js.map +1 -0
  138. package/src/lib/render.d.ts +24 -2
  139. package/src/lib/render.js +39 -10
  140. package/src/lib/render.js.map +1 -1
  141. package/src/lib/sync.d.ts +0 -1
  142. package/src/lib/sync.js +23 -15
  143. package/src/lib/sync.js.map +1 -1
  144. package/src/lib/targets/agents-skills.js +1 -1
  145. package/src/lib/targets/agents-skills.js.map +1 -1
  146. package/src/lib/targets/claude-hooks.d.ts +1 -23
  147. package/src/lib/targets/claude-hooks.js +17 -85
  148. package/src/lib/targets/claude-hooks.js.map +1 -1
  149. package/src/lib/targets/claude.js +1 -2
  150. package/src/lib/targets/claude.js.map +1 -1
  151. package/src/lib/targets/codex-hooks.d.ts +12 -0
  152. package/src/lib/targets/codex-hooks.js +34 -0
  153. package/src/lib/targets/codex-hooks.js.map +1 -0
  154. package/src/lib/targets/codex.js +4 -4
  155. package/src/lib/targets/codex.js.map +1 -1
  156. package/src/lib/targets/copilot.js +3 -3
  157. package/src/lib/targets/copilot.js.map +1 -1
  158. package/src/lib/targets/cursor.js +2 -2
  159. package/src/lib/targets/cursor.js.map +1 -1
  160. package/src/lib/targets/git-hooks.d.ts +24 -0
  161. package/src/lib/targets/git-hooks.js +71 -0
  162. package/src/lib/targets/git-hooks.js.map +1 -0
  163. package/src/lib/targets/hooks-shared.d.ts +58 -0
  164. package/src/lib/targets/hooks-shared.js +128 -0
  165. package/src/lib/targets/hooks-shared.js.map +1 -0
  166. package/src/lib/targets/shared.d.ts +1 -9
  167. package/src/lib/targets/shared.js +4 -8
  168. package/src/lib/targets/shared.js.map +1 -1
  169. package/src/lib/timetrack-command.d.ts +11 -0
  170. package/src/lib/timetrack-command.js +1055 -0
  171. package/src/lib/timetrack-command.js.map +1 -0
  172. package/src/lib/timetrack-snapshot.d.ts +55 -0
  173. package/src/lib/timetrack-snapshot.js +120 -0
  174. package/src/lib/timetrack-snapshot.js.map +1 -0
  175. package/src/lib/timetrack.d.ts +561 -0
  176. package/src/lib/timetrack.js +271 -0
  177. package/src/lib/timetrack.js.map +1 -0
  178. package/content/skills/styleguide/STYLEGUIDE.md +0 -520
@@ -0,0 +1,1055 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.timetrackCommand = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs_1 = require("fs");
6
+ const path_1 = require("path");
7
+ const timetrack_1 = require("./timetrack");
8
+ const plain_text_1 = require("./plain-text");
9
+ const timetrack_snapshot_1 = require("./timetrack-snapshot");
10
+ const git_1 = require("./git");
11
+ const FLAGS_WITH_VALUE = [
12
+ '--root',
13
+ '--project',
14
+ '--limit',
15
+ '--summary',
16
+ '--description',
17
+ '--type',
18
+ '--parent',
19
+ '--subject',
20
+ '--issue',
21
+ '--minutes',
22
+ '--at',
23
+ '--out',
24
+ '--day',
25
+ '--from',
26
+ '--to',
27
+ '--state',
28
+ '--remove',
29
+ '--split',
30
+ '--branch',
31
+ '--paths',
32
+ '--claim',
33
+ '--author',
34
+ '--plan',
35
+ '--delete',
36
+ '--repo',
37
+ '--name',
38
+ '--rename',
39
+ '--merge',
40
+ '--into',
41
+ '--resolve',
42
+ '--compare',
43
+ '--stand-in',
44
+ '--context',
45
+ ];
46
+ /** Every human-readable line, with anything a terminal would act on printed rather than obeyed. */
47
+ const say = (line) => console.log((0, plain_text_1.plain)(line));
48
+ 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 : ''); });
49
+ const flagValue = (args, flag) => {
50
+ const index = args.indexOf(flag);
51
+ if (index === -1)
52
+ return undefined;
53
+ const value = args[index + 1];
54
+ if (value === undefined || value.startsWith('--'))
55
+ throw new Error(`${flag} needs a value.`);
56
+ return value;
57
+ };
58
+ const numberFlag = (args, flag) => {
59
+ const raw = flagValue(args, flag);
60
+ if (raw === undefined)
61
+ return undefined;
62
+ const value = raw.trim() ? Number(raw) : Number.NaN;
63
+ if (!Number.isFinite(value))
64
+ throw new Error(`${flag} takes a number, not ${raw}.`);
65
+ return value;
66
+ };
67
+ const countFlag = (args, flag) => {
68
+ const value = numberFlag(args, flag);
69
+ if (value !== undefined && (!Number.isInteger(value) || value <= 0)) {
70
+ throw new Error(`${flag} takes a whole number above zero, not ${value}.`);
71
+ }
72
+ return value;
73
+ };
74
+ const LOCAL_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
75
+ const LOCAL_CLOCK = /^(\d{1,2}):(\d{2})$/;
76
+ /**
77
+ * Reads an instant flag. A date alone is local midnight and a clock alone is that time today, where
78
+ * `Date` would read the first as UTC and refuse the second; anything else goes to `Date` as it is.
79
+ */
80
+ const instantOf = (flag, raw) => {
81
+ const date = LOCAL_DATE.exec(raw);
82
+ const clock = LOCAL_CLOCK.exec(raw);
83
+ const today = new Date();
84
+ const at = date
85
+ ? new Date(Number(date[1]), Number(date[2]) - 1, Number(date[3]))
86
+ : clock
87
+ ? new Date(today.getFullYear(), today.getMonth(), today.getDate(), Number(clock[1]), Number(clock[2]))
88
+ : new Date(raw);
89
+ if (Number.isNaN(at.getTime()))
90
+ throw new Error(`${flag} takes a date, not ${raw}.`);
91
+ return at.getTime();
92
+ };
93
+ const instantFlag = (args) => {
94
+ const raw = flagValue(args, '--at');
95
+ return raw ? instantOf('--at', raw) : Date.now();
96
+ };
97
+ const issueLine = (issue) => [
98
+ issue.key,
99
+ issue.issueType,
100
+ issue.summary,
101
+ ...(issue.parentKey ? [`under ${issue.parentKey}`] : []),
102
+ ...(issue.subject ? [`subject ${issue.subject}`] : []),
103
+ ].join(' ');
104
+ const DAY = /^\d{4}-\d{2}-\d{2}$/;
105
+ const pad = (value) => String(value).padStart(2, '0');
106
+ const today = () => {
107
+ const now = new Date();
108
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
109
+ };
110
+ const countByKind = (events) => {
111
+ var _a, _b;
112
+ const counts = new Map();
113
+ for (const event of events) {
114
+ const kind = String((_a = event.kind) !== null && _a !== void 0 ? _a : 'unknown');
115
+ counts.set(kind, ((_b = counts.get(kind)) !== null && _b !== void 0 ? _b : 0) + 1);
116
+ }
117
+ return [...counts].sort((left, right) => right[1] - left[1]);
118
+ };
119
+ const describeRule = (rule) => {
120
+ var _a, _b;
121
+ const where = rule.appId
122
+ ? `app ${rule.appId}`
123
+ : `${(_a = rule.repoPath) !== null && _a !== void 0 ? _a : 'nowhere'}${rule.branch ? ` @ ${rule.branch}` : ''}`;
124
+ if (rule.donates)
125
+ return `${where} → donate`;
126
+ if (rule.standInId)
127
+ return `${where} → stand-in ${rule.standInId}`;
128
+ return `${where} → ${(_b = rule.issueKey) !== null && _b !== void 0 ? _b : 'no issue'}`;
129
+ };
130
+ const DAY_MS = 24 * 60 * 60000;
131
+ /**
132
+ * The checkout an open placeholder holds at the wider grain, or nothing when it names a branch.
133
+ *
134
+ * Two separate things stop a branch-grained placeholder opening, and either alone is enough, so both
135
+ * are read here. The record itself blocks when it was opened for a checkout and names no branch. A
136
+ * rule blocks when it names the placeholder for a checkout and names no branch — and a record opened
137
+ * before the grain was the branch has no `openedFor` at all, so the rule is the only side that shows
138
+ * it. Until the placeholder goes, the checkout stays at the wider grain.
139
+ */
140
+ const wholeCheckoutHeld = (options) => {
141
+ var _a;
142
+ const { standIn } = options;
143
+ if (standIn.state !== 'open')
144
+ return undefined;
145
+ if (standIn.openedForBranch)
146
+ return undefined;
147
+ if (standIn.openedFor)
148
+ return standIn.openedFor;
149
+ return (_a = options.rules.find((rule) => rule.standInId === standIn.id && rule.repoPath && !rule.branch)) === null || _a === void 0 ? void 0 : _a.repoPath;
150
+ };
151
+ /**
152
+ * The days a delete of this record would leave unnamed, today left out.
153
+ *
154
+ * A delete takes the rule with it, and only the day the app next reviews gets a new placeholder. Any
155
+ * earlier day the record covered reads as unnamed from then on, with nothing left to say what it was.
156
+ */
157
+ const strandedDays = (id) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
158
+ const standIn = (yield (0, timetrack_1.timetrackStandIns)()).find((entry) => entry.id === id);
159
+ const now = today();
160
+ return (standIn === null || standIn === void 0 ? void 0 : standIn.state) === 'open' ? standIn.days.filter((day) => day !== now).sort() : [];
161
+ });
162
+ /**
163
+ * Where a placeholder stands for its work: the checkout, then the narrowest thing it was cut to.
164
+ *
165
+ * Two of them may carry one name and mean two pieces of work, because a spec track and the branch
166
+ * that implements it are both called after the feature. One the user wrote by hand names no
167
+ * checkout, and this answers nothing for it.
168
+ */
169
+ const standInWhere = (standIn) => {
170
+ var _a;
171
+ if (!standIn.openedFor)
172
+ return '';
173
+ const checkout = (_a = standIn.openedFor.split('/').filter(Boolean).pop()) !== null && _a !== void 0 ? _a : standIn.openedFor;
174
+ if (standIn.openedForWorkPath)
175
+ return `${checkout}, ${standIn.openedForWorkPath}`;
176
+ return standIn.openedForBranch ? `${checkout}, on ${standIn.openedForBranch}` : checkout;
177
+ };
178
+ const describeStandIn = (options) => {
179
+ const { standIn } = options;
180
+ const days = Math.floor((Date.now() - standIn.createdAtMs) / DAY_MS);
181
+ const project = standIn.projectKey ? ` in ${standIn.projectKey}` : '';
182
+ const where = standInWhere(standIn);
183
+ const held = wholeCheckoutHeld(options);
184
+ const lines = [
185
+ where ? `\n ${where}` : '',
186
+ held ? `\n covers all of ${held}, so no branch of it gets one of its own` : '',
187
+ ];
188
+ return `${standIn.name}${project} ${days}d old, ${standIn.days.length} day(s) of work${lines.join('')}`;
189
+ };
190
+ const HOUR_MS = 60 * 60000;
191
+ const hours = (ms) => `${(ms / HOUR_MS).toFixed(1)}h`;
192
+ const DECLINE_LINES = {
193
+ 'already-named': 'a rule already names an issue for it',
194
+ 'named-by-stand-in': 'a rule names a placeholder for it, not an issue yet',
195
+ 'no-project-link': 'no project link covers it',
196
+ 'no-history': 'the project holds no Tempo worklog in the span',
197
+ 'project-too-small': 'the project holds too little time to read a habit from',
198
+ 'too-few-days': 'the leading issue spans too few days',
199
+ 'share-too-low': 'the leading issue holds too small a share of the project',
200
+ };
201
+ const describeDecline = (decline) => {
202
+ var _a, _b, _c, _d;
203
+ const measured = decline.issueKey
204
+ ? ` (${decline.issueKey} ${hours((_a = decline.loggedMs) !== null && _a !== void 0 ? _a : 0)} of ${hours((_b = decline.projectMs) !== null && _b !== void 0 ? _b : 0)}, ${Math.round(((_c = decline.share) !== null && _c !== void 0 ? _c : 0) * 100)}%, ${(_d = decline.days) !== null && _d !== void 0 ? _d : 0}d)`
205
+ : '';
206
+ return `${decline.repoPath} — ${DECLINE_LINES[decline.reason]}${measured}`;
207
+ };
208
+ const MINUTE_MS = 60000;
209
+ const clock = (ms) => new Date(ms).toTimeString().slice(0, 5);
210
+ const rowLine = (row) => {
211
+ var _a, _b, _c, _d;
212
+ return [
213
+ row.id,
214
+ `${clock(row.fromMs)}-${clock(row.toMs)}`,
215
+ `${Math.round(row.durationMs / MINUTE_MS)}m`,
216
+ (_b = (_a = row.issueKey) !== null && _a !== void 0 ? _a : row.standInId) !== null && _b !== void 0 ? _b : 'unnamed',
217
+ row.state,
218
+ ...(row.edited ? ['edited'] : []),
219
+ ...(row.hidden ? ['hidden'] : []),
220
+ (_c = row.laneKey) !== null && _c !== void 0 ? _c : 'no lane',
221
+ ...(((_d = row.autoModeSubject) === null || _d === void 0 ? void 0 : _d.kind) === 'context' ? [`context ${row.autoModeSubject.contextId}`] : []),
222
+ ].join(' ');
223
+ };
224
+ const shiftDay = (day, days) => {
225
+ const [year, month, date] = day.split('-').map(Number);
226
+ const shifted = new Date(year, month - 1, date + days);
227
+ return `${shifted.getFullYear()}-${pad(shifted.getMonth() + 1)}-${pad(shifted.getDate())}`;
228
+ };
229
+ const minutesOf = (ms) => Math.round(ms / MINUTE_MS);
230
+ const tempoDayLines = (worklogs) => {
231
+ var _a, _b, _c;
232
+ const days = new Map();
233
+ for (const worklog of worklogs) {
234
+ const issues = (_a = days.get(worklog.day)) !== null && _a !== void 0 ? _a : new Map();
235
+ const key = (_b = worklog.issueKey) !== null && _b !== void 0 ? _b : `#${worklog.issueId}`;
236
+ issues.set(key, ((_c = issues.get(key)) !== null && _c !== void 0 ? _c : 0) + worklog.durationMs);
237
+ days.set(worklog.day, issues);
238
+ }
239
+ return [...days]
240
+ .sort(([left], [right]) => left.localeCompare(right))
241
+ .map(([day, issues]) => {
242
+ const total = [...issues.values()].reduce((sum, ms) => sum + ms, 0);
243
+ const parts = [...issues]
244
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
245
+ .map(([key, ms]) => `${key} ${minutesOf(ms)}m`);
246
+ return `${day} ${hours(total)} ${parts.join(' ')}`;
247
+ });
248
+ };
249
+ const syncWriteLine = (write) => {
250
+ var _a;
251
+ return [
252
+ write.kind.padEnd(6),
253
+ (_a = write.issueKey) !== null && _a !== void 0 ? _a : write.proposalId,
254
+ `${clock(write.fromMs)}-${clock(write.fromMs + write.durationMs)}`,
255
+ `${minutesOf(write.durationMs)}m`,
256
+ write.reason,
257
+ ...(write.tempoWorklogId ? [`worklog ${write.tempoWorklogId}`] : []),
258
+ write.description || '(no description)',
259
+ ...(write.blocked ? [`HELD BACK: ${write.blocked}`] : []),
260
+ ].join(' ');
261
+ };
262
+ /** What a sync of a day would write, row by row, and what it leaves alone. */
263
+ const syncPlanLines = (plan) => {
264
+ const count = (kind) => plan.writes.filter((write) => write.kind === kind).length;
265
+ const blocked = plan.writes.filter((write) => write.blocked).length;
266
+ return [
267
+ plan.writes.length
268
+ ? `${plan.day} plan ${plan.planHash} ${count('create')} create, ${count('update')} update, ${count('delete')} delete`
269
+ : `${plan.day} nothing to write`,
270
+ ...plan.writes.map((write) => ` ${syncWriteLine(write)}`),
271
+ `${plan.unchanged} unchanged, ${plan.skipped} skipped (still awaiting review)`,
272
+ ...(blocked ? [`${blocked} row(s) held back: Tempo would refuse them until they are fixed on the day.`] : []),
273
+ ...(plan.unresolvedKeys.length
274
+ ? [`Jira does not know ${plan.unresolvedKeys.join(', ')}, so nothing is written for those rows.`]
275
+ : []),
276
+ ...(plan.coveredMs
277
+ ? [`${minutesOf(plan.coveredMs)}m of the day is already logged by foreign worklogs, so it is left out.`]
278
+ : []),
279
+ `Foreign worklogs (${plan.foreign.length}), never touched`,
280
+ ...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)'}`; }),
281
+ ];
282
+ };
283
+ /** Every row a write attempted, and what the user has to do about the ones that did not land. */
284
+ const syncRunLines = (run) => [
285
+ ...run.rows.map((row) => {
286
+ var _a;
287
+ return [
288
+ row.status.padEnd(7),
289
+ row.kind.padEnd(6),
290
+ (_a = row.issueKey) !== null && _a !== void 0 ? _a : row.proposalId,
291
+ ...(row.tempoWorklogId ? [`worklog ${row.tempoWorklogId}`] : []),
292
+ ...(row.detail ? [row.detail] : []),
293
+ ].join(' ');
294
+ }),
295
+ ...(run.retryCount
296
+ ? [
297
+ `${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.`,
298
+ ]
299
+ : []),
300
+ ...(run.unrecorded
301
+ ? [
302
+ `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.`,
303
+ ]
304
+ : []),
305
+ ];
306
+ const meetingMs = (events) => events
307
+ .filter((event) => !event.allDay && event.response !== 'declined')
308
+ .reduce((sum, event) => sum + event.endMs - event.startMs, 0);
309
+ const calendarEventLine = (event) => {
310
+ const start = new Date(event.startMs);
311
+ const when = event.allDay
312
+ ? 'all-day'
313
+ : `${pad(start.getHours())}:${pad(start.getMinutes())} ${minutesOf(event.endMs - event.startMs)}m`;
314
+ const answer = event.response === 'accepted' || event.response === 'organizer' ? [] : [event.response];
315
+ return [when, event.title, ...answer].join(' ');
316
+ };
317
+ const calendarDayLines = (events) => {
318
+ var _a;
319
+ const days = new Map();
320
+ for (const event of events)
321
+ days.set(event.day, [...((_a = days.get(event.day)) !== null && _a !== void 0 ? _a : []), event]);
322
+ return [...days]
323
+ .sort(([left], [right]) => left.localeCompare(right))
324
+ .flatMap(([day, dayEvents]) => [
325
+ `${day} ${dayEvents.length} event(s), ${hours(meetingMs(dayEvents))}`,
326
+ ...dayEvents.map((event) => ` ${calendarEventLine(event)}`),
327
+ ]);
328
+ };
329
+ /** Which edits the flags name. `--from` with `--to` is the one pair that names a single change. */
330
+ const namedEdits = (argv) => {
331
+ const named = argv.includes('--from') || argv.includes('--to') ? ['--from/--to'] : [];
332
+ return [
333
+ ...named,
334
+ ...['--issue', '--description', '--state', '--hide', '--show', '--reset'].filter((flag) => argv.includes(flag)),
335
+ ];
336
+ };
337
+ /**
338
+ * The one edit the flags state, or a refusal naming what they said instead.
339
+ *
340
+ * One edit per call, deliberately. A caller that means two changes to one row says so twice, and a
341
+ * flag combination nobody meant can then never be read as a third thing.
342
+ */
343
+ const editOf = (options) => {
344
+ const { argv, rowId } = options;
345
+ const from = flagValue(argv, '--from');
346
+ const to = flagValue(argv, '--to');
347
+ const issueKey = flagValue(argv, '--issue');
348
+ const description = flagValue(argv, '--description');
349
+ const state = flagValue(argv, '--state');
350
+ const named = namedEdits(argv);
351
+ if (named.length > 1) {
352
+ throw new Error(`One edit per call, and these name ${named.length}: ${named.join(', ')}. Run the command once for each.`);
353
+ }
354
+ if (from && to)
355
+ return { kind: 'range', rowId, fromMs: instantOf('--from', from), toMs: instantOf('--to', to) };
356
+ if (from || to)
357
+ throw new Error('Moving a row takes both --from and --to.');
358
+ if (issueKey)
359
+ return { kind: 'issue', rowId, issueKey: issueKey.toUpperCase() };
360
+ if (description !== undefined)
361
+ return { kind: 'description', rowId, description };
362
+ if (state) {
363
+ if (state !== 'accepted' && state !== 'rejected')
364
+ throw new Error(`--state takes accepted or rejected, not ${state}.`);
365
+ return { kind: 'state', rowId, state };
366
+ }
367
+ if (argv.includes('--hide'))
368
+ return { kind: 'hidden', rowId, hidden: true };
369
+ if (argv.includes('--show'))
370
+ return { kind: 'hidden', rowId, hidden: false };
371
+ if (argv.includes('--reset'))
372
+ return { kind: 'reset', rowId };
373
+ throw new Error('Pass one of --from with --to, --issue, --description, --state, --hide, --show or --reset.');
374
+ };
375
+ /**
376
+ * Writes an export nobody but its owner can read.
377
+ *
378
+ * A day's events are the rawest personal record the app holds, and the recommended destination is a
379
+ * shared temporary directory. The file therefore carries mode `0600` from the moment it exists, rather
380
+ * than whatever the umask leaves. `lstatSync` reads the name itself, so a symlink somebody else planted
381
+ * is refused rather than written through, and exclusive creation refuses one that arrives after the
382
+ * check.
383
+ */
384
+ const writeExport = (options) => {
385
+ const { path, data, overwrite } = options;
386
+ const found = (0, fs_1.lstatSync)(path, { throwIfNoEntry: false });
387
+ if (found && !overwrite) {
388
+ throw new Error(`${path} already exists. Pass --overwrite to replace it, or name another file.`);
389
+ }
390
+ if (found && !found.isFile())
391
+ throw new Error(`${path} is not a regular file, so it is not overwritten.`);
392
+ if (found)
393
+ (0, fs_1.unlinkSync)(path);
394
+ const handle = (0, fs_1.openSync)(path, 'wx', 0o600);
395
+ try {
396
+ (0, fs_1.writeFileSync)(handle, data);
397
+ }
398
+ finally {
399
+ (0, fs_1.closeSync)(handle);
400
+ }
401
+ };
402
+ /** `--json` answers data, never a terminal-formatted line, so it is printed as it parses. */
403
+ const printed = (value, json) => {
404
+ if (json)
405
+ console.log(JSON.stringify(value, null, 2));
406
+ return 0;
407
+ };
408
+ /** Every write waits in the app for the user's press, so what a write prints is where to look next. */
409
+ const queuedLines = (queued, what) => [
410
+ `Queued in Timetrack for the user's approval: ${what}.`,
411
+ `Nothing is written until they approve it. Read the outcome with: timetrack approval ${queued.approvalId}`,
412
+ ];
413
+ const printedQueued = (options) => {
414
+ if (!options.json)
415
+ queuedLines(options.queued, options.what).forEach(say);
416
+ return printed(options.queued, options.json);
417
+ };
418
+ const isSyncWithRun = (value) => typeof value === 'object' && value !== null && 'run' in value && typeof value.run === 'object';
419
+ const UNDECIDED = {
420
+ queued: "still waits for the user's approval in Timetrack.",
421
+ rejected: 'the user rejected it. Nothing was written.',
422
+ expired: 'expired at the end of the day it was asked on. Nothing was written. Ask again.',
423
+ };
424
+ const approvalLines = (found) => {
425
+ if (found.status !== 'approved')
426
+ return [`${found.approvalId} ${UNDECIDED[found.status]}`];
427
+ if (found.error !== undefined)
428
+ return [`${found.approvalId} approved, but it failed: ${found.error}`];
429
+ const unrecorded = unrecordedOf(found.result);
430
+ return [
431
+ `${found.approvalId} approved and carried out.`,
432
+ ...(isSyncWithRun(found.result) ? syncRunLines(found.result.run) : ['Pass --json for what it answered.']),
433
+ ...(unrecorded && !isSyncWithRun(found.result)
434
+ ? [`Carried out, but the app's ledger did not follow: ${unrecorded}. A later sync may undo it.`]
435
+ : []),
436
+ ];
437
+ };
438
+ const askedAtOf = (ms) => {
439
+ const at = new Date(ms);
440
+ return `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ${pad(at.getHours())}:${pad(at.getMinutes())}`;
441
+ };
442
+ const waitingLines = (waiting) => {
443
+ if (!waiting.length)
444
+ return ["Nothing waits for the user's approval in Timetrack."];
445
+ return [
446
+ `${waiting.length} wait(s) for the user's approval in Timetrack. Only the user approves one, in the app.`,
447
+ ...waiting.map((item) => {
448
+ var _a;
449
+ return ` ${item.approvalId} ${(_a = item.client) !== null && _a !== void 0 ? _a : 'CLI'} ${item.op} asked ${askedAtOf(item.askedAtMs)} ${item.summary}${item.state === 'running' ? ' (approved, carrying it out)' : ''}`;
450
+ }),
451
+ ];
452
+ };
453
+ const unrecordedOf = (value) => typeof value === 'object' && value !== null && 'unrecorded' in value && typeof value.unrecorded === 'string'
454
+ ? value.unrecorded
455
+ : undefined;
456
+ /** Non-zero for anything that did not land in full, so a caller polling in a script can tell. */
457
+ const approvalExitCode = (found) => {
458
+ if (found.status === 'queued')
459
+ return 0;
460
+ if (found.status !== 'approved' || found.error !== undefined)
461
+ return 1;
462
+ if (unrecordedOf(found.result))
463
+ return 1;
464
+ return isSyncWithRun(found.result) && (found.result.run.retryCount || found.result.run.unrecorded) ? 1 : 0;
465
+ };
466
+ /**
467
+ * Cuts a placeholder that covered a whole checkout into one per directory its commits worked in.
468
+ *
469
+ * The directories are read here rather than in the app: a commit collected before Timetrack recorded
470
+ * file paths carries none in the store, and that is every commit such a placeholder covers. Without
471
+ * `--force` the plan is printed and nothing is written.
472
+ */
473
+ const splitStandIn = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
474
+ var _a, _b, _c, _d;
475
+ const { id, argv, json } = options;
476
+ const standIn = (yield (0, timetrack_1.timetrackStandIns)()).find((entry) => entry.id === id);
477
+ if (!standIn) {
478
+ say(`Timetrack holds no stand-in ${id}.`);
479
+ return 1;
480
+ }
481
+ const repoPath = flagValue(argv, '--repo');
482
+ const checkout = (_a = standIn.openedFor) !== null && _a !== void 0 ? _a : repoPath;
483
+ if (!checkout) {
484
+ say(`${id} names no checkout, so nothing says where its directories are. Name one with --repo <dir>.`);
485
+ return 1;
486
+ }
487
+ const branch = (_b = flagValue(argv, '--branch')) !== null && _b !== void 0 ? _b : (0, git_1.currentBranch)(checkout);
488
+ const author = (_c = flagValue(argv, '--author')) !== null && _c !== void 0 ? _c : (0, git_1.commitAuthorOf)(checkout);
489
+ const commits = (0, git_1.commitPathsOnDays)(Object.assign({ root: checkout, days: standIn.days }, (author ? { author } : {})));
490
+ if (!commits.length) {
491
+ say(`No commit of ${checkout} falls on ${standIn.days.join(', ')}, so nothing says how to split it.`);
492
+ return 1;
493
+ }
494
+ const paths = ((_d = flagValue(argv, '--paths')) !== null && _d !== void 0 ? _d : '')
495
+ .split(',')
496
+ .map((path) => path.trim())
497
+ .filter(Boolean);
498
+ const claim = flagValue(argv, '--claim');
499
+ const apply = argv.includes('--force');
500
+ const projectRoots = (0, git_1.projectRootsOf)(checkout);
501
+ const request = { id, branch, repoPath, commits, projectRoots, paths, claim };
502
+ const answer = yield (0, timetrack_1.timetrackSplitStandIn)(request);
503
+ if (!json) {
504
+ say(`${standIn.name} ${standIn.days.length} day(s), branch ${branch}`);
505
+ say(`Directories the commits name (${answer.candidates.length})`);
506
+ answer.candidates.forEach((piece) => say(` ${piece.workPath} ${piece.commits}c ${piece.days.join(', ')}`));
507
+ if (!answer.pieces.length) {
508
+ say('None of them is an automatic grain. Pick the pieces with --paths <dir>,<dir>.');
509
+ }
510
+ else if (!apply) {
511
+ say(`Would write ${answer.pieces.map((piece) => piece.workPath).join(', ')}. Pass --force to carry it out.`);
512
+ }
513
+ if (claim) {
514
+ const covered = new Set(answer.pieces.flatMap((piece) => piece.days));
515
+ const left = standIn.days.filter((day) => !covered.has(day));
516
+ say(left.length
517
+ ? `${claim} takes ${left.join(', ')}.`
518
+ : `Every day already has a directory, so ${claim} takes none.`);
519
+ }
520
+ else if (answer.remainder.length) {
521
+ say(`No commit claims ${answer.remainder.join(', ')}, so those days go back to unnamed.`);
522
+ say('Name them in the day review, or re-run with --claim <dir> to give them a directory.');
523
+ }
524
+ }
525
+ if (apply && answer.pieces.length) {
526
+ const queued = yield (0, timetrack_1.timetrackApplyStandInSplit)(request);
527
+ return printedQueued({ queued, what: `split ${standIn.name} into ${answer.pieces.length}`, json });
528
+ }
529
+ return printed(answer, json);
530
+ });
531
+ const daysBetween = (from, to) => {
532
+ const days = [];
533
+ for (let day = from; day <= to && days.length < 92; day = shiftDay(day, 1))
534
+ days.push(day);
535
+ return days;
536
+ };
537
+ const gitState = (root) => {
538
+ try {
539
+ const uncommitted = (0, git_1.git)({ root, args: ['status', '--porcelain'] })
540
+ .split('\n')
541
+ .filter(Boolean)
542
+ .map((line) => line.slice(3));
543
+ return { head: (0, git_1.git)({ root, args: ['rev-parse', 'HEAD'] }), uncommitted };
544
+ }
545
+ catch (_a) {
546
+ return {};
547
+ }
548
+ };
549
+ const takeSnapshot = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
550
+ 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([])) }));
551
+ });
552
+ const defaultSnapshotPath = () => (0, path_1.join)((0, path_1.dirname)((0, timetrack_1.timetrackDiscoveryPath)()), 'snapshots', `${new Date().toISOString().replace(/[:.]/g, '-')}.json`);
553
+ const USAGE = `ethlete-agents timetrack — ask the running Timetrack app about Jira
554
+
555
+ The app holds this machine's Jira credentials, so no repository needs a token of its own.
556
+ Every write waits in the app until the user approves it there, and prints an approval id.
557
+ A waiting write can be rejected from here; only the user approves one, in the app.
558
+
559
+ timetrack status Whether the app is reachable, and which projects it holds
560
+ timetrack instance The instance's own levels and its branch-subject candidates
561
+ timetrack issue <KEY> One issue: its summary, type, parent and branch subject
562
+ timetrack search [text] Open issues of the picked projects, most recently touched first
563
+ timetrack project [path] Which Jira project a repository logs into
564
+ timetrack create --summary … File a new issue with the instance's own ticket settings
565
+ timetrack log --issue <KEY> --minutes <n>
566
+ Add a row nothing observed to the day it belongs to
567
+ timetrack day [YYYY-MM-DD] The evidence a day holds, which the encrypted store hides otherwise
568
+ timetrack transcript [YYYY-MM-DD]
569
+ What the app transcribed of your own microphone during the day's calls
570
+ timetrack rows [YYYY-MM-DD] The rows the day drew, with the ids an edit names them by
571
+ timetrack snapshot [from] [to] [--out <file>]
572
+ Record the rows of the last 7 days (default) before a change
573
+ timetrack snapshot --compare <file>
574
+ Print every row the change altered since; exit 1 when any did
575
+ timetrack edit <row-id> … Change one row of a day: its times, its name, its note or its state
576
+ timetrack rules The rules that name a day's work: attribution, project links, apps
577
+ timetrack standins The names the user gave work Jira does not hold yet, and their age
578
+ timetrack standins --remove <id>
579
+ Delete one placeholder, and the rule that named it
580
+ timetrack standins --rename <id> --name <text>
581
+ Give one placeholder another name, keeping its days and its rules
582
+ timetrack standins --merge <id> --into <id>
583
+ Fold one open placeholder into another, with its days and its rules
584
+ timetrack standins --resolve <id> --issue <KEY>
585
+ Resolve one placeholder to an issue, or move a resolved one to another
586
+ timetrack standins --split <id> [--repo <dir>] [--paths <dir>,<dir>] [--claim <dir>] [--author <email>]
587
+ [--force]
588
+ Cut one that covered a whole checkout into one per directory
589
+ timetrack worklogs [from] [to]
590
+ The account's own Tempo worklogs per day (default: the last 7 days)
591
+ timetrack calendar [from] [to]
592
+ The watched calendars' events per day (default: the last 7 days)
593
+ timetrack sync <YYYY-MM-DD> What a Tempo sync of the day would write, as the Sync page plans it
594
+ timetrack sync <YYYY-MM-DD> --write --plan <hash>
595
+ Write that plan to Tempo, once the user confirmed its rows
596
+ timetrack worklog --delete <id> --day <YYYY-MM-DD>
597
+ Delete one of the account's own Tempo worklogs, once the user asked for it
598
+ timetrack naming [YYYY-MM-DD] Which checkouts the day offers a name for, and why the rest do not
599
+ timetrack resync [path…] Read the agent session logs of checkouts again, after they got a link
600
+ timetrack approval <id> Where a queued write stands, and what it answered once approved
601
+ timetrack approvals The writes that wait for the user's approval now, from every caller
602
+ timetrack ask --stand-in <id> | --context <context-id> [--day YYYY-MM-DD]
603
+ Ask auto mode about one subject of a day again, as "Ask auto mode
604
+ again" does (default: today). Writes nothing to Jira or Tempo
605
+ timetrack approval <id> --reject
606
+ Reject one waiting write, as the app's Reject button does. Nothing here
607
+ approves one: only the user does, in the app
608
+
609
+ Options for search
610
+ --project <KEY> Search this project instead of the picked ones
611
+ --mine Only the issues assigned to the account
612
+ --limit <n> How many issues to read (default 100)
613
+
614
+ Options for create
615
+ --summary <text> Required
616
+ --description <text>
617
+ --project <KEY> Required unless the app holds exactly one picked project
618
+ --type <name> Issue type by name (default: the app's configured type)
619
+ --parent <KEY> The story or epic it rolls up to
620
+ --subject <text> The branch subject, written to the instance's subject field
621
+
622
+ Options for log
623
+ --issue <KEY> Required
624
+ --minutes <n> Required
625
+ --at <date> When the work started (default: now)
626
+ --description <text>
627
+
628
+ Options for day
629
+ --out <path> Write the raw answer to a new file, readable by you alone
630
+ --overwrite Replace the file --out names, which is refused otherwise
631
+
632
+ Options for resync
633
+ --replace Overwrite what the store holds for those sessions, after a parser fix
634
+
635
+ Options for sync
636
+ --write Queue the write of the plan. Refused without --plan; the app refuses it on
637
+ approval when the plan changed since
638
+ --plan <hash> The plan hash the read printed, for the rows the user confirmed
639
+
640
+ Options for edit — pass exactly one change
641
+ --day <YYYY-MM-DD> The day the row is on (default: today)
642
+ --from <date> --to <date>
643
+ Move the row, or drag one of its ends
644
+ --issue <KEY> Name the row
645
+ --description <text>
646
+ --state <accepted|rejected>
647
+ --hide Take it off the timeline
648
+ --show Put it back
649
+ --reset Give the app's own row back
650
+
651
+ Options everywhere
652
+ --json Print the raw answer instead of lines
653
+ `;
654
+ /**
655
+ * The agent-facing half of Timetrack: everything here is one call into the running app.
656
+ *
657
+ * Nothing in this file knows a Jira host or a token. That is the point — an agent working in any
658
+ * repository asks the one process that holds them, so a machine has one secret to rotate rather than
659
+ * one per checkout.
660
+ */
661
+ const timetrackCommand = (options) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
662
+ var _a, _b, _c, _d, _e, _f, _g;
663
+ const { root, argv } = options;
664
+ const [subcommand, value] = positionalArgs(argv);
665
+ const json = argv.includes('--json');
666
+ if (subcommand === 'status') {
667
+ const status = yield (0, timetrack_1.timetrackStatus)();
668
+ if (!json) {
669
+ say(`Timetrack ${(0, timetrack_1.timetrackDiscoveryPath)()}`);
670
+ say(` jira ${status.jiraReady ? 'configured' : 'not configured — set it in Timetrack Settings'}`);
671
+ say(` tempo ${status.tempoReady ? 'configured' : 'not configured — no worklog history is read'}`);
672
+ say(` projects ${status.projects.map((project) => project.key).join(', ') || '— none picked'}`);
673
+ say(` subject ${status.subjectField || '— no field configured, the summary is used'}`);
674
+ }
675
+ return printed(status, json);
676
+ }
677
+ if (subcommand === 'instance') {
678
+ const instance = yield (0, timetrack_1.timetrackInstance)();
679
+ if (!json) {
680
+ say('Levels, highest first');
681
+ instance.levels.forEach((level) => say(` ${level.hierarchyLevel} ${level.typeNames.join(', ')}`));
682
+ say(`A parent can be named by ${instance.suggestedParenting}`);
683
+ say(`Branch-subject candidates (${instance.subjectFieldCandidates.length})`);
684
+ instance.subjectFieldCandidates.forEach((field) => say(` ${field.id} ${field.name}`));
685
+ }
686
+ return printed(instance, json);
687
+ }
688
+ if (subcommand === 'issue') {
689
+ if (!value)
690
+ throw new Error('Pass an issue key, e.g. `ethlete-agents timetrack issue FIP-2177`.');
691
+ const issue = yield (0, timetrack_1.timetrackIssue)(value);
692
+ if (!json)
693
+ say(issueLine(issue));
694
+ return printed(issue, json);
695
+ }
696
+ if (subcommand === 'search') {
697
+ const issues = yield (0, timetrack_1.timetrackSearch)({
698
+ text: value,
699
+ projectKey: flagValue(argv, '--project'),
700
+ assignedToMe: argv.includes('--mine'),
701
+ limit: countFlag(argv, '--limit'),
702
+ });
703
+ if (!json) {
704
+ if (issues.length === 0)
705
+ say('No issue matches.');
706
+ issues.forEach((issue) => say(issueLine(issue)));
707
+ }
708
+ return printed(issues, json);
709
+ }
710
+ if (subcommand === 'project') {
711
+ const found = yield (0, timetrack_1.timetrackRepoProject)((0, path_1.resolve)(root, value !== null && value !== void 0 ? value : '.'));
712
+ if (!json) {
713
+ const where = found.inherited ? ' (from a directory above it)' : '';
714
+ if (found.private)
715
+ say(`${found.repoPath} is marked private — work there is logged nowhere${where}.`);
716
+ else if (found.projectKey)
717
+ say(`${found.repoPath} logs into ${found.projectKey}${where}.`);
718
+ else if (found.suggestedProjectKey) {
719
+ say(`${found.repoPath} is linked to nothing. Its name suggests ${found.suggestedProjectKey}.`);
720
+ }
721
+ else
722
+ say(`${found.repoPath} is linked to no project. Link it in Timetrack Settings.`);
723
+ }
724
+ return printed(found, json);
725
+ }
726
+ if (subcommand === 'create') {
727
+ const summary = flagValue(argv, '--summary');
728
+ if (!summary)
729
+ throw new Error('Pass --summary "…".');
730
+ const queued = yield (0, timetrack_1.timetrackCreateIssue)({
731
+ summary,
732
+ description: flagValue(argv, '--description'),
733
+ projectKey: flagValue(argv, '--project'),
734
+ issueTypeName: flagValue(argv, '--type'),
735
+ parentKey: flagValue(argv, '--parent'),
736
+ subject: flagValue(argv, '--subject'),
737
+ });
738
+ return printedQueued({ queued, what: `file the issue ${summary}`, json });
739
+ }
740
+ if (subcommand === 'log') {
741
+ const issueKey = flagValue(argv, '--issue');
742
+ const minutes = numberFlag(argv, '--minutes');
743
+ if (!issueKey)
744
+ throw new Error('Pass --issue <KEY>.');
745
+ if (!minutes || minutes <= 0)
746
+ throw new Error('Pass --minutes <n> above zero.');
747
+ const queued = yield (0, timetrack_1.timetrackAddWorklog)({
748
+ issueKey,
749
+ description: flagValue(argv, '--description'),
750
+ fromMs: instantFlag(argv),
751
+ durationMs: Math.round(minutes * 60000),
752
+ });
753
+ if (!json)
754
+ say('Once approved it is a row on the day, not a Tempo entry — the day still needs a sync.');
755
+ return printedQueued({ queued, what: `a ${minutes}m row for ${issueKey}`, json });
756
+ }
757
+ if (subcommand === 'day') {
758
+ const day = value !== null && value !== void 0 ? value : today();
759
+ if (!DAY.test(day))
760
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
761
+ const found = yield (0, timetrack_1.timetrackDayEvents)(day);
762
+ const outFlag = flagValue(argv, '--out');
763
+ if (outFlag) {
764
+ const out = (0, path_1.resolve)(root, outFlag);
765
+ writeExport({ path: out, data: JSON.stringify(found), overwrite: argv.includes('--overwrite') });
766
+ if (json)
767
+ return printed({ day: found.day, events: found.events.length, out }, json);
768
+ say(`${found.day} ${found.events.length} events written to ${out}, readable by you alone`);
769
+ say('It holds the day as it was observed: window titles, paths and messages. Delete it when you are done.');
770
+ return 0;
771
+ }
772
+ if (!json) {
773
+ say(`${found.day} ${found.events.length} events`);
774
+ countByKind(found.events).forEach(([kind, count]) => say(` ${kind} ${count}`));
775
+ say('Pass --out <path> to write the events themselves, which are far too many to read.');
776
+ }
777
+ return printed(found, json);
778
+ }
779
+ if (subcommand === 'transcript') {
780
+ const day = value !== null && value !== void 0 ? value : today();
781
+ if (!DAY.test(day))
782
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
783
+ const found = yield (0, timetrack_1.timetrackTranscriptDay)(day);
784
+ if (!json) {
785
+ say(`${found.day} ${found.chunks.length} transcript chunks`);
786
+ found.chunks.forEach((chunk) => say(` ${clock(chunk.atMs)} ${chunk.appId} ${chunk.text}`));
787
+ }
788
+ return printed(found, json);
789
+ }
790
+ if (subcommand === 'rows') {
791
+ const day = value !== null && value !== void 0 ? value : today();
792
+ if (!DAY.test(day))
793
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
794
+ const found = yield (0, timetrack_1.timetrackDayRows)(day);
795
+ if (!json) {
796
+ say(`${found.day} ${hours(found.loggedMs)} logged of a ${hours(found.targetMs)} target`);
797
+ found.rows.forEach((row) => say(` ${rowLine(row)}`));
798
+ found.hidden.forEach((row) => say(` ${rowLine(row)}`));
799
+ found.warnings.forEach((warning) => say(` ! ${warning.kind}: ${warning.detail}`));
800
+ }
801
+ return printed(found, json);
802
+ }
803
+ if (subcommand === 'snapshot') {
804
+ const compare = flagValue(argv, '--compare');
805
+ if (compare) {
806
+ const recorded = JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)(root, compare), 'utf8'));
807
+ const now = yield takeSnapshot({ days: recorded.days.map((entry) => entry.day), root });
808
+ const diff = (0, timetrack_snapshot_1.diffSnapshots)(recorded, now);
809
+ if (json)
810
+ console.log(JSON.stringify(diff, null, 2));
811
+ else
812
+ (0, timetrack_snapshot_1.diffLines)(diff).forEach((line) => say(line));
813
+ return diff.changedRows || diff.days.length ? 1 : 0;
814
+ }
815
+ const named = positionalArgs(argv).slice(1);
816
+ const to = (_a = named[1]) !== null && _a !== void 0 ? _a : today();
817
+ const from = (_b = named[0]) !== null && _b !== void 0 ? _b : shiftDay(to, -6);
818
+ if (!DAY.test(from))
819
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
820
+ if (!DAY.test(to))
821
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${to}.`);
822
+ const path = (0, path_1.resolve)(root, (_c = flagValue(argv, '--out')) !== null && _c !== void 0 ? _c : defaultSnapshotPath());
823
+ const recorded = yield takeSnapshot({ days: daysBetween(from, to), root });
824
+ (0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true });
825
+ (0, fs_1.writeFileSync)(path, JSON.stringify(recorded, null, 2), { mode: 0o600 });
826
+ if (json)
827
+ console.log(JSON.stringify({ path, days: recorded.days.length }));
828
+ else
829
+ say(`Recorded ${recorded.days.length} day(s), ${recorded.days.reduce((sum, day) => sum + day.rows.length, 0)} row(s) in ${path}`);
830
+ return 0;
831
+ }
832
+ if (subcommand === 'edit') {
833
+ const day = (_d = flagValue(argv, '--day')) !== null && _d !== void 0 ? _d : today();
834
+ if (!value)
835
+ throw new Error('Pass the row id to edit, as `timetrack rows` prints it.');
836
+ if (!DAY.test(day))
837
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
838
+ const queued = yield (0, timetrack_1.timetrackEditDay)({ day, edits: [editOf({ argv, rowId: value })] });
839
+ return printedQueued({ queued, what: `an edit of row ${value} on ${day}`, json });
840
+ }
841
+ if (subcommand === 'rules') {
842
+ const rules = yield (0, timetrack_1.timetrackRules)();
843
+ if (!json) {
844
+ say(`target ${Math.round(rules.dayTargetMs / 60000)}m day starts at ${rules.dayStartHour}:00`);
845
+ say(`${rules.attributionRules.length} attribution rule(s)`);
846
+ rules.attributionRules.forEach((rule) => say(` ${describeRule(rule)}`));
847
+ say(`${rules.projectLinks.length} project link(s)`);
848
+ rules.projectLinks.forEach((link) => { var _a; return say(` ${link.path} → ${link.private ? 'private' : ((_a = link.projectKey) !== null && _a !== void 0 ? _a : 'no project')}`); });
849
+ }
850
+ return printed(rules, json);
851
+ }
852
+ if (subcommand === 'resync') {
853
+ const named = positionalArgs(argv).slice(1);
854
+ const paths = (named.length ? named : ['.']).map((path) => (0, path_1.resolve)(root, path));
855
+ const replace = argv.includes('--replace');
856
+ const queued = yield (0, timetrack_1.timetrackResyncAgentSessions)(paths, { replace });
857
+ const how = replace ? ', replacing what it stored for them' : '';
858
+ return printedQueued({ queued, what: `read the agent sessions of ${paths.join(', ')} again${how}`, json });
859
+ }
860
+ if (subcommand === 'worklogs') {
861
+ const to = (_e = positionalArgs(argv)[2]) !== null && _e !== void 0 ? _e : today();
862
+ const from = value !== null && value !== void 0 ? value : shiftDay(to, -6);
863
+ if (!DAY.test(from))
864
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
865
+ if (!DAY.test(to))
866
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${to}.`);
867
+ const found = yield (0, timetrack_1.timetrackTempoWorklogs)({ from, to });
868
+ if (!json) {
869
+ const total = found.worklogs.reduce((sum, worklog) => sum + worklog.durationMs, 0);
870
+ say(`${found.from} … ${found.to} ${found.worklogs.length} worklog(s), ${hours(total)}`);
871
+ tempoDayLines(found.worklogs).forEach((line) => say(` ${line}`));
872
+ }
873
+ return printed(found, json);
874
+ }
875
+ if (subcommand === 'worklog') {
876
+ if (argv.includes('--help')) {
877
+ say(USAGE);
878
+ return 0;
879
+ }
880
+ const worklogId = flagValue(argv, '--delete');
881
+ const day = flagValue(argv, '--day');
882
+ if (!worklogId || !/^\d+$/.test(worklogId))
883
+ throw new Error('Pass the Tempo worklog id to delete with --delete <id>.');
884
+ if (!day || !DAY.test(day))
885
+ throw new Error('Pass the day the worklog is on with --day <YYYY-MM-DD>.');
886
+ const queued = yield (0, timetrack_1.timetrackTempoDelete)({ day, worklogId });
887
+ return printedQueued({ queued, what: `delete Tempo worklog ${worklogId} on ${day}`, json });
888
+ }
889
+ if (subcommand === 'sync') {
890
+ if (!value || !DAY.test(value))
891
+ throw new Error('Pass the day to sync as YYYY-MM-DD.');
892
+ const planHash = flagValue(argv, '--plan');
893
+ const write = argv.includes('--write');
894
+ if (write && !planHash) {
895
+ say(`A write names the plan the user confirmed. Read it with \`timetrack sync ${value}\`, show the rows,`);
896
+ say('and pass the hash it prints with --plan once they agree.');
897
+ return 1;
898
+ }
899
+ if (planHash && !write) {
900
+ say('--plan only means something with --write. Nothing was written.');
901
+ return 1;
902
+ }
903
+ if (write && planHash) {
904
+ const queued = yield (0, timetrack_1.timetrackTempoSyncWrite)({ day: value, planHash });
905
+ if (!json)
906
+ say('The app checks the plan again on approval, and refuses it if the day changed since.');
907
+ return printedQueued({ queued, what: `write plan ${planHash} of ${value} to Tempo`, json });
908
+ }
909
+ const found = yield (0, timetrack_1.timetrackTempoSync)(value);
910
+ if (!json) {
911
+ syncPlanLines(found).forEach(say);
912
+ if (found.writes.length) {
913
+ say(`Once the user has confirmed these rows: timetrack sync ${found.day} --write --plan ${found.planHash}`);
914
+ }
915
+ }
916
+ return printed(found, json);
917
+ }
918
+ if (subcommand === 'calendar') {
919
+ const to = (_f = positionalArgs(argv)[2]) !== null && _f !== void 0 ? _f : today();
920
+ const from = value !== null && value !== void 0 ? value : shiftDay(to, -6);
921
+ if (!DAY.test(from))
922
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${from}.`);
923
+ if (!DAY.test(to))
924
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${to}.`);
925
+ const found = yield (0, timetrack_1.timetrackCalendarEvents)({ from, to });
926
+ if (!json) {
927
+ say(`${found.from} … ${found.to} ${found.events.length} event(s) from ${found.calendarIds.length} calendar(s), ${hours(meetingMs(found.events))} in meetings`);
928
+ calendarDayLines(found.events).forEach((line) => say(` ${line}`));
929
+ }
930
+ return printed(found, json);
931
+ }
932
+ if (subcommand === 'naming') {
933
+ const day = value !== null && value !== void 0 ? value : today();
934
+ if (!DAY.test(day))
935
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
936
+ const naming = yield (0, timetrack_1.timetrackNaming)(day);
937
+ if (!json) {
938
+ say(`${day} tempo ${naming.tempoReady ? 'configured' : 'not configured'}`);
939
+ say(`History ${naming.history}, ${naming.historyWorklogs} worklog(s) in the span`);
940
+ if (naming.historyMessage)
941
+ say(` ${naming.historyMessage}`);
942
+ say(`Offered (${naming.offers.length})`);
943
+ naming.offers.forEach((offer) => say(` ${offer.repoPath} → ${offer.issueKey} ${hours(offer.loggedMs)}, ${Math.round(offer.share * 100)}%, ${offer.days}d`));
944
+ say(`Not offered (${naming.declines.length})`);
945
+ naming.declines.forEach((decline) => say(` ${describeDecline(decline)}`));
946
+ }
947
+ return printed(naming, json);
948
+ }
949
+ if (subcommand === 'ask') {
950
+ const standInId = flagValue(argv, '--stand-in');
951
+ const contextId = flagValue(argv, '--context');
952
+ const day = (_g = flagValue(argv, '--day')) !== null && _g !== void 0 ? _g : today();
953
+ const subject = standInId
954
+ ? { kind: 'stand-in', standInId }
955
+ : contextId
956
+ ? { kind: 'context', contextId }
957
+ : undefined;
958
+ if (!subject || (standInId && contextId)) {
959
+ throw new Error('Pass one subject: `timetrack ask --stand-in <id>` or `timetrack ask --context <context-id>`.');
960
+ }
961
+ if (!DAY.test(day))
962
+ throw new Error(`Pass a day as YYYY-MM-DD, not ${day}.`);
963
+ const asked = yield (0, timetrack_1.timetrackAskAutoMode)({ day, subject });
964
+ if (!json) {
965
+ say(asked.status === 'queued'
966
+ ? `${asked.label} on ${asked.day}: queued. The new answer replaces the stored one; what it queues waits for the user's approval.`
967
+ : `${asked.label} on ${asked.day}: already waiting or running, so nothing more was queued.`);
968
+ }
969
+ return printed(asked, json);
970
+ }
971
+ if (subcommand === 'approvals') {
972
+ const waiting = yield (0, timetrack_1.timetrackApprovals)();
973
+ if (!json)
974
+ waitingLines(waiting).forEach(say);
975
+ return printed(waiting, json);
976
+ }
977
+ if (subcommand === 'approval') {
978
+ if (!value)
979
+ throw new Error('Pass the approval id a write printed, e.g. `timetrack approval <id>`.');
980
+ if (argv.includes('--reject')) {
981
+ const rejected = yield (0, timetrack_1.timetrackRejectApproval)(value);
982
+ if (!json)
983
+ say(`${rejected.approvalId} rejected. Nothing was written.`);
984
+ return printed(rejected, json);
985
+ }
986
+ const found = yield (0, timetrack_1.timetrackApprovalStatus)(value);
987
+ if (!json)
988
+ approvalLines(found).forEach(say);
989
+ printed(found, json);
990
+ return approvalExitCode(found);
991
+ }
992
+ if (subcommand === 'standins') {
993
+ const split = flagValue(argv, '--split');
994
+ if (split)
995
+ return yield splitStandIn({ id: split, argv, json });
996
+ const rename = flagValue(argv, '--rename');
997
+ const name = flagValue(argv, '--name');
998
+ if (rename && !name) {
999
+ say(`A rename needs the new name. Pass --name <text> with --rename ${rename}.`);
1000
+ return 1;
1001
+ }
1002
+ if (rename && name) {
1003
+ const queued = yield (0, timetrack_1.timetrackRenameStandIn)({ id: rename, name });
1004
+ return printedQueued({ queued, what: `rename ${rename} to ${name}`, json });
1005
+ }
1006
+ const merge = flagValue(argv, '--merge');
1007
+ const into = flagValue(argv, '--into');
1008
+ if (merge && !into) {
1009
+ say(`A merge needs the placeholder it goes into. Pass --into <id> with --merge ${merge}.`);
1010
+ return 1;
1011
+ }
1012
+ if (merge && into) {
1013
+ const queued = yield (0, timetrack_1.timetrackMergeStandIn)({ id: merge, into });
1014
+ return printedQueued({ queued, what: `merge ${merge} into ${into}`, json });
1015
+ }
1016
+ const resolve = flagValue(argv, '--resolve');
1017
+ const issue = flagValue(argv, '--issue');
1018
+ if (resolve && !issue) {
1019
+ say(`A resolve needs the issue. Pass --issue <KEY> with --resolve ${resolve}.`);
1020
+ return 1;
1021
+ }
1022
+ if (resolve && issue) {
1023
+ const queued = yield (0, timetrack_1.timetrackResolveStandIn)({ id: resolve, issueKey: issue });
1024
+ return printedQueued({ queued, what: `resolve ${resolve} to ${issue.toUpperCase()}`, json });
1025
+ }
1026
+ const remove = flagValue(argv, '--remove');
1027
+ const stranded = remove ? yield strandedDays(remove) : [];
1028
+ if (stranded.length && !argv.includes('--force')) {
1029
+ say(`${remove} holds ${stranded.length} day(s) before today: ${stranded.join(', ')}.`);
1030
+ say('Deleting it takes the rule with it, so those days read as unnamed and nothing reopens them.');
1031
+ say('The app reopens a placeholder for today only. Pass --force if that is what you want.');
1032
+ return 1;
1033
+ }
1034
+ if (remove) {
1035
+ const queued = yield (0, timetrack_1.timetrackRemoveStandIn)(remove);
1036
+ return printedQueued({ queued, what: `delete ${remove} and the rule that named it`, json });
1037
+ }
1038
+ const standIns = yield (0, timetrack_1.timetrackStandIns)();
1039
+ const rules = json ? [] : (yield (0, timetrack_1.timetrackRules)()).attributionRules;
1040
+ const open = standIns
1041
+ .filter((standIn) => standIn.state === 'open')
1042
+ .sort((left, right) => left.createdAtMs - right.createdAtMs);
1043
+ if (!json) {
1044
+ say(`${open.length} open, ${standIns.length - open.length} resolved`);
1045
+ open.forEach((standIn) => say(` ${describeStandIn({ standIn, rules })}`));
1046
+ if (open.length)
1047
+ say('Only the app opens or resolves one — you may delete one, not write one.');
1048
+ }
1049
+ return printed(standIns, json);
1050
+ }
1051
+ say(USAGE);
1052
+ return subcommand === undefined || subcommand === '--help' || subcommand === '-h' ? 0 : 1;
1053
+ });
1054
+ exports.timetrackCommand = timetrackCommand;
1055
+ //# sourceMappingURL=timetrack-command.js.map