@ondrej-svec/hog 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,172 +8,6 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
- // src/api.ts
12
- var BASE_URL, TickTickClient;
13
- var init_api = __esm({
14
- "src/api.ts"() {
15
- "use strict";
16
- BASE_URL = "https://api.ticktick.com/open/v1";
17
- TickTickClient = class {
18
- token;
19
- constructor(token) {
20
- this.token = token;
21
- }
22
- async request(method, path, body) {
23
- const url = `${BASE_URL}${path}`;
24
- const init = {
25
- method,
26
- headers: {
27
- Authorization: `Bearer ${this.token}`,
28
- "Content-Type": "application/json"
29
- }
30
- };
31
- if (body !== void 0) {
32
- init.body = JSON.stringify(body);
33
- }
34
- const res = await fetch(url, init);
35
- if (!res.ok) {
36
- const text2 = await res.text();
37
- throw new Error(`TickTick API error ${res.status}: ${text2}`);
38
- }
39
- const text = await res.text();
40
- if (!text) return null;
41
- return JSON.parse(text);
42
- }
43
- async listProjects() {
44
- return await this.request("GET", "/project") ?? [];
45
- }
46
- async getProject(projectId) {
47
- const result = await this.request("GET", `/project/${projectId}`);
48
- if (!result) throw new Error(`TickTick API returned empty response for project ${projectId}`);
49
- return result;
50
- }
51
- async getProjectData(projectId) {
52
- const result = await this.request("GET", `/project/${projectId}/data`);
53
- if (!result)
54
- throw new Error(`TickTick API returned empty response for project data ${projectId}`);
55
- return result;
56
- }
57
- async listTasks(projectId) {
58
- const data = await this.getProjectData(projectId);
59
- return data.tasks ?? [];
60
- }
61
- async getTask(projectId, taskId) {
62
- const result = await this.request("GET", `/project/${projectId}/task/${taskId}`);
63
- if (!result) throw new Error(`TickTick API returned empty response for task ${taskId}`);
64
- return result;
65
- }
66
- async createTask(input) {
67
- const result = await this.request("POST", "/task", input);
68
- if (!result) throw new Error("TickTick API returned empty response for createTask");
69
- return result;
70
- }
71
- async updateTask(input) {
72
- const result = await this.request("POST", `/task/${input.id}`, input);
73
- if (!result) throw new Error(`TickTick API returned empty response for updateTask ${input.id}`);
74
- return result;
75
- }
76
- async completeTask(projectId, taskId) {
77
- await this.request("POST", `/project/${projectId}/task/${taskId}/complete`);
78
- }
79
- async deleteTask(projectId, taskId) {
80
- await this.request("DELETE", `/project/${projectId}/task/${taskId}`);
81
- }
82
- };
83
- }
84
- });
85
-
86
- // src/config.ts
87
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
88
- import { homedir } from "os";
89
- import { isAbsolute, join, normalize } from "path";
90
- import { z } from "zod";
91
- function getAuth() {
92
- if (!existsSync(AUTH_FILE)) return null;
93
- try {
94
- return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
95
- } catch {
96
- return null;
97
- }
98
- }
99
- function getConfig() {
100
- if (!existsSync(CONFIG_FILE)) return {};
101
- try {
102
- return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
103
- } catch {
104
- return {};
105
- }
106
- }
107
- function requireAuth() {
108
- const auth = getAuth();
109
- if (!auth) {
110
- console.error("Not authenticated. Run `hog init` first.");
111
- process.exit(1);
112
- }
113
- return auth;
114
- }
115
- var CONFIG_DIR, AUTH_FILE, CONFIG_FILE, COMPLETION_ACTION_SCHEMA, REPO_NAME_PATTERN, CLAUDE_START_COMMAND_SCHEMA, REPO_CONFIG_SCHEMA, BOARD_CONFIG_SCHEMA, TICKTICK_CONFIG_SCHEMA, PROFILE_SCHEMA, HOG_CONFIG_SCHEMA;
116
- var init_config = __esm({
117
- "src/config.ts"() {
118
- "use strict";
119
- CONFIG_DIR = join(homedir(), ".config", "hog");
120
- AUTH_FILE = join(CONFIG_DIR, "auth.json");
121
- CONFIG_FILE = join(CONFIG_DIR, "config.json");
122
- COMPLETION_ACTION_SCHEMA = z.discriminatedUnion("type", [
123
- z.object({ type: z.literal("updateProjectStatus"), optionId: z.string() }),
124
- z.object({ type: z.literal("closeIssue") }),
125
- z.object({ type: z.literal("addLabel"), label: z.string() })
126
- ]);
127
- REPO_NAME_PATTERN = /^[\w.-]+\/[\w.-]+$/;
128
- CLAUDE_START_COMMAND_SCHEMA = z.object({
129
- command: z.string().min(1),
130
- extraArgs: z.array(z.string())
131
- });
132
- REPO_CONFIG_SCHEMA = z.object({
133
- name: z.string().regex(REPO_NAME_PATTERN, "Must be owner/repo format"),
134
- shortName: z.string().min(1),
135
- projectNumber: z.number().int().positive(),
136
- statusFieldId: z.string().min(1),
137
- dueDateFieldId: z.string().optional(),
138
- completionAction: COMPLETION_ACTION_SCHEMA,
139
- statusGroups: z.array(z.string()).optional(),
140
- localPath: z.string().refine((p) => isAbsolute(p), { message: "localPath must be an absolute path" }).refine((p) => normalize(p) === p, {
141
- message: "localPath must be normalized (no .. segments)"
142
- }).refine((p) => !p.includes("\0"), { message: "localPath must not contain null bytes" }).optional(),
143
- claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional(),
144
- claudePrompt: z.string().optional()
145
- });
146
- BOARD_CONFIG_SCHEMA = z.object({
147
- refreshInterval: z.number().int().min(10).default(60),
148
- backlogLimit: z.number().int().min(1).default(20),
149
- assignee: z.string().min(1),
150
- focusDuration: z.number().int().min(60).default(1500),
151
- claudeStartCommand: CLAUDE_START_COMMAND_SCHEMA.optional(),
152
- claudePrompt: z.string().optional(),
153
- claudeLaunchMode: z.enum(["auto", "tmux", "terminal"]).optional(),
154
- claudeTerminalApp: z.enum(["Terminal", "iTerm", "Ghostty", "WezTerm", "Kitty", "Alacritty"]).optional()
155
- });
156
- TICKTICK_CONFIG_SCHEMA = z.object({
157
- enabled: z.boolean().default(true)
158
- });
159
- PROFILE_SCHEMA = z.object({
160
- repos: z.array(REPO_CONFIG_SCHEMA).default([]),
161
- board: BOARD_CONFIG_SCHEMA,
162
- ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true })
163
- });
164
- HOG_CONFIG_SCHEMA = z.object({
165
- version: z.number().int().default(3),
166
- defaultProjectId: z.string().optional(),
167
- defaultProjectName: z.string().optional(),
168
- repos: z.array(REPO_CONFIG_SCHEMA).default([]),
169
- board: BOARD_CONFIG_SCHEMA,
170
- ticktick: TICKTICK_CONFIG_SCHEMA.default({ enabled: true }),
171
- profiles: z.record(z.string(), PROFILE_SCHEMA).default({}),
172
- defaultProfile: z.string().optional()
173
- });
174
- }
175
- });
176
-
177
11
  // src/github.ts
178
12
  import { execFile, execFileSync } from "child_process";
179
13
  import { promisify } from "util";
@@ -184,6 +18,23 @@ function runGhJson(args) {
184
18
  const output = runGh(args);
185
19
  return JSON.parse(output);
186
20
  }
21
+ function runGhGraphQL(args) {
22
+ try {
23
+ return runGhJson(args);
24
+ } catch (err) {
25
+ if (err && typeof err === "object" && "stdout" in err) {
26
+ const stdout = err.stdout;
27
+ const output = typeof stdout === "string" ? stdout : stdout?.toString("utf-8");
28
+ if (output) {
29
+ try {
30
+ return JSON.parse(output.trim());
31
+ } catch {
32
+ }
33
+ }
34
+ }
35
+ throw err;
36
+ }
37
+ }
187
38
  function fetchRepoIssues(repo, options2 = {}) {
188
39
  const { state = "open", limit = 100 } = options2;
189
40
  const args = [
@@ -206,41 +57,37 @@ function fetchRepoIssues(repo, options2 = {}) {
206
57
  function fetchProjectEnrichment(repo, projectNumber) {
207
58
  const [owner] = repo.split("/");
208
59
  if (!owner) return /* @__PURE__ */ new Map();
209
- const query = `
210
- query($owner: String!, $projectNumber: Int!, $cursor: String) {
211
- organization(login: $owner) {
212
- projectV2(number: $projectNumber) {
213
- items(first: 100, after: $cursor) {
214
- pageInfo { hasNextPage endCursor }
60
+ const projectItemsFragment = `
61
+ projectV2(number: $projectNumber) {
62
+ items(first: 100, after: $cursor) {
63
+ pageInfo { hasNextPage endCursor }
64
+ nodes {
65
+ content {
66
+ ... on Issue {
67
+ number
68
+ }
69
+ }
70
+ fieldValues(first: 20) {
215
71
  nodes {
216
- content {
217
- ... on Issue {
218
- number
219
- }
72
+ ... on ProjectV2ItemFieldDateValue {
73
+ field { ... on ProjectV2Field { name } }
74
+ date
75
+ }
76
+ ... on ProjectV2ItemFieldSingleSelectValue {
77
+ field { ... on ProjectV2SingleSelectField { name } }
78
+ name
220
79
  }
221
- fieldValues(first: 20) {
222
- nodes {
223
- ... on ProjectV2ItemFieldDateValue {
224
- field { ... on ProjectV2Field { name } }
225
- date
226
- }
227
- ... on ProjectV2ItemFieldSingleSelectValue {
228
- field { ... on ProjectV2SingleSelectField { name } }
229
- name
230
- }
231
- ... on ProjectV2ItemFieldTextValue {
232
- field { ... on ProjectV2Field { name } }
233
- text
234
- }
235
- ... on ProjectV2ItemFieldNumberValue {
236
- field { ... on ProjectV2Field { name } }
237
- number
238
- }
239
- ... on ProjectV2ItemFieldIterationValue {
240
- field { ... on ProjectV2IterationField { name } }
241
- title
242
- }
243
- }
80
+ ... on ProjectV2ItemFieldTextValue {
81
+ field { ... on ProjectV2Field { name } }
82
+ text
83
+ }
84
+ ... on ProjectV2ItemFieldNumberValue {
85
+ field { ... on ProjectV2Field { name } }
86
+ number
87
+ }
88
+ ... on ProjectV2ItemFieldIterationValue {
89
+ field { ... on ProjectV2IterationField { name } }
90
+ title
244
91
  }
245
92
  }
246
93
  }
@@ -248,6 +95,12 @@ function fetchProjectEnrichment(repo, projectNumber) {
248
95
  }
249
96
  }
250
97
  `;
98
+ const query = `
99
+ query($owner: String!, $projectNumber: Int!, $cursor: String) {
100
+ organization(login: $owner) { ${projectItemsFragment} }
101
+ user(login: $owner) { ${projectItemsFragment} }
102
+ }
103
+ `;
251
104
  try {
252
105
  const enrichMap = /* @__PURE__ */ new Map();
253
106
  let cursor = null;
@@ -263,8 +116,9 @@ function fetchProjectEnrichment(repo, projectNumber) {
263
116
  `projectNumber=${String(projectNumber)}`
264
117
  ];
265
118
  if (cursor) args.push("-f", `cursor=${cursor}`);
266
- const result = runGhJson(args);
267
- const page = result?.data?.organization?.projectV2?.items;
119
+ const result = runGhGraphQL(args);
120
+ const ownerNode = result?.data?.organization ?? result?.data?.user;
121
+ const page = ownerNode?.projectV2?.items;
268
122
  const nodes = page?.nodes ?? [];
269
123
  for (const item of nodes) {
270
124
  if (!item?.content?.number) continue;
@@ -298,24 +152,26 @@ function fetchProjectEnrichment(repo, projectNumber) {
298
152
  function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
299
153
  const [owner] = repo.split("/");
300
154
  if (!owner) return [];
301
- const query = `
302
- query($owner: String!, $projectNumber: Int!) {
303
- organization(login: $owner) {
304
- projectV2(number: $projectNumber) {
305
- field(name: "Status") {
306
- ... on ProjectV2SingleSelectField {
307
- options {
308
- id
309
- name
310
- }
311
- }
155
+ const statusFragment = `
156
+ projectV2(number: $projectNumber) {
157
+ field(name: "Status") {
158
+ ... on ProjectV2SingleSelectField {
159
+ options {
160
+ id
161
+ name
312
162
  }
313
163
  }
314
164
  }
315
165
  }
316
166
  `;
167
+ const query = `
168
+ query($owner: String!, $projectNumber: Int!) {
169
+ organization(login: $owner) { ${statusFragment} }
170
+ user(login: $owner) { ${statusFragment} }
171
+ }
172
+ `;
317
173
  try {
318
- const result = runGhJson([
174
+ const result = runGhGraphQL([
319
175
  "api",
320
176
  "graphql",
321
177
  "-f",
@@ -325,7 +181,8 @@ function fetchProjectStatusOptions(repo, projectNumber, _statusFieldId) {
325
181
  "-F",
326
182
  `projectNumber=${String(projectNumber)}`
327
183
  ]);
328
- return result?.data?.organization?.projectV2?.field?.options ?? [];
184
+ const ownerNode = result?.data?.organization ?? result?.data?.user;
185
+ return ownerNode?.projectV2?.field?.options ?? [];
329
186
  } catch {
330
187
  return [];
331
188
  }
@@ -339,19 +196,12 @@ var init_github = __esm({
339
196
  }
340
197
  });
341
198
 
342
- // src/types.ts
343
- var init_types = __esm({
344
- "src/types.ts"() {
345
- "use strict";
346
- }
347
- });
348
-
349
- // src/board/constants.ts
199
+ // src/utils.ts
350
200
  function formatError(err) {
351
201
  return err instanceof Error ? err.message : String(err);
352
202
  }
353
- var init_constants = __esm({
354
- "src/board/constants.ts"() {
203
+ var init_utils = __esm({
204
+ "src/utils.ts"() {
355
205
  "use strict";
356
206
  }
357
207
  });
@@ -360,6 +210,8 @@ var init_constants = __esm({
360
210
  var fetch_exports = {};
361
211
  __export(fetch_exports, {
362
212
  SLACK_URL_RE: () => SLACK_URL_RE,
213
+ extractIssueNumbersFromBranch: () => extractIssueNumbersFromBranch,
214
+ extractLinkedIssueNumbers: () => extractLinkedIssueNumbers,
363
215
  extractSlackUrl: () => extractSlackUrl,
364
216
  fetchDashboard: () => fetchDashboard,
365
217
  fetchRecentActivity: () => fetchRecentActivity
@@ -370,6 +222,17 @@ function extractSlackUrl(body) {
370
222
  const match = body.match(SLACK_URL_RE);
371
223
  return match?.[0];
372
224
  }
225
+ function extractIssueNumbersFromBranch(branchName) {
226
+ const matches = branchName.match(/\b(\d{1,5})\b/g);
227
+ if (!matches) return [];
228
+ return [...new Set(matches.map((m) => parseInt(m, 10)).filter((n) => n > 0))];
229
+ }
230
+ function extractLinkedIssueNumbers(title, body) {
231
+ const text = `${title ?? ""} ${body ?? ""}`;
232
+ const matches = text.match(/#(\d{1,5})\b/g);
233
+ if (!matches) return [];
234
+ return [...new Set(matches.map((m) => parseInt(m.slice(1), 10)).filter((n) => n > 0))];
235
+ }
373
236
  function fetchRecentActivity(repoName, shortName) {
374
237
  try {
375
238
  const output = execFileSync2(
@@ -377,9 +240,10 @@ function fetchRecentActivity(repoName, shortName) {
377
240
  [
378
241
  "api",
379
242
  `repos/${repoName}/events`,
380
- "--paginate",
243
+ "-f",
244
+ "per_page=30",
381
245
  "-q",
382
- '.[] | select(.type == "IssuesEvent" or .type == "IssueCommentEvent" or .type == "PullRequestEvent") | {type: .type, actor: .actor.login, action: .payload.action, number: (.payload.issue.number // .payload.pull_request.number), title: (.payload.issue.title // .payload.pull_request.title), body: .payload.comment.body, created_at: .created_at}'
246
+ '.[] | select(.type == "IssuesEvent" or .type == "IssueCommentEvent" or .type == "PullRequestEvent" or .type == "CreateEvent") | {type: .type, actor: .actor.login, action: .payload.action, number: (.payload.issue.number // .payload.pull_request.number), title: (.payload.issue.title // .payload.pull_request.title), body: (.payload.comment.body // .payload.pull_request.body), created_at: .created_at, ref: .payload.ref, ref_type: .payload.ref_type, merged: .payload.pull_request.merged}'
383
247
  ],
384
248
  { encoding: "utf-8", timeout: 15e3 }
385
249
  );
@@ -391,9 +255,26 @@ function fetchRecentActivity(repoName, shortName) {
391
255
  const ev = JSON.parse(line);
392
256
  const timestamp = new Date(ev.created_at);
393
257
  if (timestamp.getTime() < cutoff) continue;
258
+ if (ev.type === "CreateEvent") {
259
+ if (ev.ref_type !== "branch" || !ev.ref) continue;
260
+ const issueNumbers = extractIssueNumbersFromBranch(ev.ref);
261
+ for (const num of issueNumbers) {
262
+ events.push({
263
+ type: "branch_created",
264
+ repoShortName: shortName,
265
+ issueNumber: num,
266
+ actor: ev.actor,
267
+ summary: `created branch ${ev.ref}`,
268
+ timestamp,
269
+ branchName: ev.ref
270
+ });
271
+ }
272
+ continue;
273
+ }
394
274
  if (!ev.number) continue;
395
275
  let eventType;
396
276
  let summary;
277
+ let extras = {};
397
278
  if (ev.type === "IssueCommentEvent") {
398
279
  eventType = "comment";
399
280
  const preview = ev.body ? ev.body.slice(0, 60).replace(/\n/g, " ") : "";
@@ -419,6 +300,45 @@ function fetchRecentActivity(repoName, shortName) {
419
300
  default:
420
301
  continue;
421
302
  }
303
+ } else if (ev.type === "PullRequestEvent") {
304
+ const prNumber = ev.number;
305
+ extras = { prNumber };
306
+ if (ev.action === "opened") {
307
+ eventType = "pr_opened";
308
+ summary = `opened PR #${prNumber}: ${ev.title ?? ""}`;
309
+ } else if (ev.action === "closed" && ev.merged) {
310
+ eventType = "pr_merged";
311
+ summary = `merged PR #${prNumber}: ${ev.title ?? ""}`;
312
+ } else if (ev.action === "closed") {
313
+ eventType = "pr_closed";
314
+ summary = `closed PR #${prNumber}`;
315
+ } else {
316
+ continue;
317
+ }
318
+ const linkedIssues = extractLinkedIssueNumbers(ev.title, ev.body);
319
+ for (const issueNum of linkedIssues) {
320
+ events.push({
321
+ type: eventType,
322
+ repoShortName: shortName,
323
+ issueNumber: issueNum,
324
+ actor: ev.actor,
325
+ summary,
326
+ timestamp,
327
+ prNumber
328
+ });
329
+ }
330
+ if (linkedIssues.length === 0) {
331
+ events.push({
332
+ type: eventType,
333
+ repoShortName: shortName,
334
+ issueNumber: prNumber,
335
+ actor: ev.actor,
336
+ summary,
337
+ timestamp,
338
+ prNumber
339
+ });
340
+ }
341
+ continue;
422
342
  } else {
423
343
  continue;
424
344
  }
@@ -428,7 +348,8 @@ function fetchRecentActivity(repoName, shortName) {
428
348
  issueNumber: ev.number,
429
349
  actor: ev.actor,
430
350
  summary,
431
- timestamp
351
+ timestamp,
352
+ ...extras
432
353
  });
433
354
  } catch {
434
355
  }
@@ -480,21 +401,6 @@ async function fetchDashboard(config2, options2 = {}) {
480
401
  return { repo, issues: [], statusOptions: [], error: formatError(err) };
481
402
  }
482
403
  });
483
- let ticktick = [];
484
- let ticktickError = null;
485
- if (config2.ticktick.enabled) {
486
- try {
487
- const auth = requireAuth();
488
- const api = new TickTickClient(auth.accessToken);
489
- const cfg = getConfig();
490
- if (cfg.defaultProjectId) {
491
- const tasks = await api.listTasks(cfg.defaultProjectId);
492
- ticktick = tasks.filter((t) => t.status !== 2 /* Completed */);
493
- }
494
- } catch (err) {
495
- ticktickError = formatError(err);
496
- }
497
- }
498
404
  const activity = [];
499
405
  for (const repo of repos) {
500
406
  const events = fetchRecentActivity(repo.name, repo.shortName);
@@ -503,8 +409,6 @@ async function fetchDashboard(config2, options2 = {}) {
503
409
  activity.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
504
410
  return {
505
411
  repos: repoData,
506
- ticktick,
507
- ticktickError,
508
412
  activity: activity.slice(0, 15),
509
413
  fetchedAt: /* @__PURE__ */ new Date()
510
414
  };
@@ -513,11 +417,8 @@ var SLACK_URL_RE;
513
417
  var init_fetch = __esm({
514
418
  "src/board/fetch.ts"() {
515
419
  "use strict";
516
- init_api();
517
- init_config();
518
420
  init_github();
519
- init_types();
520
- init_constants();
421
+ init_utils();
521
422
  SLACK_URL_RE = /https:\/\/[^/]+\.slack\.com\/archives\/[A-Z0-9]+\/p[0-9]+/i;
522
423
  }
523
424
  });