@ondrej-svec/hog 1.5.0 → 1.6.1

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/dist/cli.js CHANGED
@@ -163,6 +163,7 @@ var init_config = __esm({
163
163
  shortName: z.string().min(1),
164
164
  projectNumber: z.number().int().positive(),
165
165
  statusFieldId: z.string().min(1),
166
+ dueDateFieldId: z.string().optional(),
166
167
  completionAction: COMPLETION_ACTION_SCHEMA,
167
168
  statusGroups: z.array(z.string()).optional()
168
169
  });
@@ -532,7 +533,7 @@ function fetchProjectFields(repo, issueNumber, projectNumber) {
532
533
  const fieldValues = projectItem.fieldValues?.nodes ?? [];
533
534
  for (const fv of fieldValues) {
534
535
  if (!fv) continue;
535
- if ("date" in fv && fv.field?.name === "Target date") {
536
+ if ("date" in fv && DATE_FIELD_NAME_RE2.test(fv.field?.name ?? "")) {
536
537
  fields.targetDate = fv.date;
537
538
  }
538
539
  if ("name" in fv && fv.field?.name === "Status") {
@@ -594,7 +595,7 @@ function fetchProjectEnrichment(repo, projectNumber) {
594
595
  const fieldValues = item.fieldValues?.nodes ?? [];
595
596
  for (const fv of fieldValues) {
596
597
  if (!fv) continue;
597
- if ("date" in fv && fv.field?.name === "Target date" && fv.date) {
598
+ if ("date" in fv && fv.date && DATE_FIELD_NAME_RE2.test(fv.field?.name ?? "")) {
598
599
  enrichment.targetDate = fv.date;
599
600
  }
600
601
  if ("name" in fv && fv.field?.name === "Status" && fv.name) {
@@ -824,11 +825,91 @@ async function updateProjectItemStatusAsync(repo, issueNumber, projectConfig) {
824
825
  `optionId=${optionId}`
825
826
  ]);
826
827
  }
827
- var execFileAsync;
828
+ async function updateProjectItemDateAsync(repo, issueNumber, projectConfig, dueDate) {
829
+ const [owner, repoName] = repo.split("/");
830
+ const findItemQuery = `
831
+ query($owner: String!, $repo: String!, $issueNumber: Int!) {
832
+ repository(owner: $owner, name: $repo) {
833
+ issue(number: $issueNumber) {
834
+ projectItems(first: 10) {
835
+ nodes {
836
+ id
837
+ project { number }
838
+ }
839
+ }
840
+ }
841
+ }
842
+ }
843
+ `;
844
+ const findResult = await runGhJsonAsync([
845
+ "api",
846
+ "graphql",
847
+ "-f",
848
+ `query=${findItemQuery}`,
849
+ "-F",
850
+ `owner=${owner}`,
851
+ "-F",
852
+ `repo=${repoName}`,
853
+ "-F",
854
+ `issueNumber=${String(issueNumber)}`
855
+ ]);
856
+ const items = findResult?.data?.repository?.issue?.projectItems?.nodes ?? [];
857
+ const projectItem = items.find((item) => item?.project?.number === projectConfig.projectNumber);
858
+ if (!projectItem?.id) return;
859
+ const projectQuery = `
860
+ query($owner: String!) {
861
+ organization(login: $owner) {
862
+ projectV2(number: ${projectConfig.projectNumber}) {
863
+ id
864
+ }
865
+ }
866
+ }
867
+ `;
868
+ const projectResult = await runGhJsonAsync([
869
+ "api",
870
+ "graphql",
871
+ "-f",
872
+ `query=${projectQuery}`,
873
+ "-F",
874
+ `owner=${owner}`
875
+ ]);
876
+ const projectId = projectResult?.data?.organization?.projectV2?.id;
877
+ if (!projectId) return;
878
+ const mutation = `
879
+ mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {
880
+ updateProjectV2ItemFieldValue(
881
+ input: {
882
+ projectId: $projectId
883
+ itemId: $itemId
884
+ fieldId: $fieldId
885
+ value: { date: $date }
886
+ }
887
+ ) {
888
+ projectV2Item { id }
889
+ }
890
+ }
891
+ `;
892
+ await runGhAsync([
893
+ "api",
894
+ "graphql",
895
+ "-f",
896
+ `query=${mutation}`,
897
+ "-F",
898
+ `projectId=${projectId}`,
899
+ "-F",
900
+ `itemId=${projectItem.id}`,
901
+ "-F",
902
+ `fieldId=${projectConfig.dueDateFieldId}`,
903
+ "-F",
904
+ `date=${dueDate}`
905
+ ]);
906
+ }
907
+ var execFileAsync, DATE_FIELD_NAME_RE2;
828
908
  var init_github = __esm({
829
909
  "src/github.ts"() {
830
910
  "use strict";
831
911
  execFileAsync = promisify(execFile);
912
+ DATE_FIELD_NAME_RE2 = /^(target\s*date|due\s*date|due|deadline)$/i;
832
913
  }
833
914
  });
834
915
 
@@ -1162,7 +1243,6 @@ function useActions({
1162
1243
  } else {
1163
1244
  t.resolve(`#${issue.number} \u2192 ${optionName}`);
1164
1245
  }
1165
- refresh();
1166
1246
  }).catch((err) => {
1167
1247
  t.reject(`Status change failed: ${err instanceof Error ? err.message : String(err)}`);
1168
1248
  refresh();
@@ -1222,8 +1302,16 @@ function useActions({
1222
1302
  });
1223
1303
  }, [toast, refresh]);
1224
1304
  const handleCreateIssue = useCallback(
1225
- async (repo, title, body, labels) => {
1226
- const args = ["issue", "create", "--repo", repo, "--title", title, "--body", body];
1305
+ async (repo, title, body, dueDate, labels) => {
1306
+ const repoConfig = configRef.current.repos.find((r) => r.name === repo);
1307
+ let effectiveBody = body;
1308
+ if (dueDate && !repoConfig?.dueDateFieldId) {
1309
+ const dueLine = `Due: ${dueDate}`;
1310
+ effectiveBody = body ? `${body}
1311
+
1312
+ ${dueLine}` : dueLine;
1313
+ }
1314
+ const args = ["issue", "create", "--repo", repo, "--title", title, "--body", effectiveBody];
1227
1315
  if (labels && labels.length > 0) {
1228
1316
  for (const label of labels) {
1229
1317
  args.push("--label", label);
@@ -1235,7 +1323,15 @@ function useActions({
1235
1323
  const output = stdout.trim();
1236
1324
  const match = output.match(/\/(\d+)$/);
1237
1325
  const issueNumber = match?.[1] ? parseInt(match[1], 10) : 0;
1238
- const shortName = configRef.current.repos.find((r) => r.name === repo)?.shortName ?? repo;
1326
+ const shortName = repoConfig?.shortName ?? repo;
1327
+ if (issueNumber > 0 && dueDate && repoConfig?.dueDateFieldId) {
1328
+ const dueDateConfig = {
1329
+ projectNumber: repoConfig.projectNumber,
1330
+ dueDateFieldId: repoConfig.dueDateFieldId
1331
+ };
1332
+ updateProjectItemDateAsync(repo, issueNumber, dueDateConfig, dueDate).catch(() => {
1333
+ });
1334
+ }
1239
1335
  t.resolve(`Created ${shortName}#${issueNumber}`);
1240
1336
  refresh();
1241
1337
  onOverlayDone();
@@ -1386,8 +1482,8 @@ function useActions({
1386
1482
  t.resolve(`Moved ${total} issue${total > 1 ? "s" : ""} to ${optionName}`);
1387
1483
  } else {
1388
1484
  t.reject(`${ok} moved to ${optionName}, ${failed.length} failed`);
1485
+ refresh();
1389
1486
  }
1390
- refresh();
1391
1487
  return failed;
1392
1488
  },
1393
1489
  [toast, refresh, mutateData]
@@ -2778,13 +2874,19 @@ function CreateIssueForm({
2778
2874
  currentLabels: [],
2779
2875
  labelCache: labelCache ?? {},
2780
2876
  onConfirm: (addLabels) => {
2781
- onSubmit(selectedRepo.name, title, "", addLabels.length > 0 ? addLabels : void 0);
2877
+ onSubmit(
2878
+ selectedRepo.name,
2879
+ title,
2880
+ "",
2881
+ null,
2882
+ addLabels.length > 0 ? addLabels : void 0
2883
+ );
2782
2884
  },
2783
2885
  onCancel: () => {
2784
- onSubmit(selectedRepo.name, title, "");
2886
+ onSubmit(selectedRepo.name, title, "", null);
2785
2887
  },
2786
2888
  onError: () => {
2787
- onSubmit(selectedRepo.name, title, "");
2889
+ onSubmit(selectedRepo.name, title, "", null);
2788
2890
  }
2789
2891
  }
2790
2892
  )
@@ -2820,7 +2922,7 @@ function CreateIssueForm({
2820
2922
  setTitle(trimmed);
2821
2923
  setField("labels");
2822
2924
  } else {
2823
- onSubmit(selectedRepo.name, trimmed, "");
2925
+ onSubmit(selectedRepo.name, trimmed, "", null);
2824
2926
  }
2825
2927
  }
2826
2928
  }
@@ -3109,7 +3211,7 @@ function NlCreateOverlay({
3109
3211
  setBody(content.trimEnd());
3110
3212
  } finally {
3111
3213
  onResumeRef.current?.();
3112
- if (tmpFile) {
3214
+ if (tmpFile && tmpDir) {
3113
3215
  try {
3114
3216
  rmSync2(tmpDir, { recursive: true, force: true });
3115
3217
  } catch {
@@ -3186,6 +3288,7 @@ function NlCreateOverlay({
3186
3288
  selectedRepo.name,
3187
3289
  parsed.title,
3188
3290
  text.trim(),
3291
+ parsed.dueDate,
3189
3292
  labels.length > 0 ? labels : void 0
3190
3293
  );
3191
3294
  }
@@ -3196,7 +3299,7 @@ function NlCreateOverlay({
3196
3299
  ] });
3197
3300
  }
3198
3301
  if (parsed) {
3199
- const labels = buildLabelList(parsed);
3302
+ const labels = [...parsed.labels];
3200
3303
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3201
3304
  /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "\u2728 Creating Issue" }),
3202
3305
  /* @__PURE__ */ jsxs9(Box9, { children: [
@@ -3223,7 +3326,6 @@ function NlCreateOverlay({
3223
3326
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Due: " }),
3224
3327
  /* @__PURE__ */ jsx9(Text9, { children: formatDue(parsed.dueDate) })
3225
3328
  ] }) : null,
3226
- parsed.dueDate && selectedRepo && !hasDueLabelInCache(labelCache, selectedRepo.name) ? /* @__PURE__ */ jsx9(Text9, { color: "yellow", children: "\u26A0 No due:* label in this repo \u2014 will try to create label on submit" }) : null,
3227
3329
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Enter:add body Esc:cancel" })
3228
3330
  ] });
3229
3331
  }
@@ -3245,19 +3347,11 @@ function NlCreateOverlay({
3245
3347
  ] });
3246
3348
  }
3247
3349
  function buildLabelList(parsed) {
3248
- const labels = [...parsed.labels];
3249
- if (parsed.dueDate) {
3250
- labels.push(`due:${parsed.dueDate}`);
3251
- }
3252
- return labels;
3253
- }
3254
- function hasDueLabelInCache(labelCache, repoName) {
3255
- return (labelCache[repoName] ?? []).some((l) => l.name.startsWith("due:"));
3350
+ return [...parsed.labels];
3256
3351
  }
3257
3352
  function formatDue(dueDate) {
3258
3353
  const d = /* @__PURE__ */ new Date(`${dueDate}T12:00:00`);
3259
- const human = d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
3260
- return `${human} (label: due:${dueDate})`;
3354
+ return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
3261
3355
  }
3262
3356
  var init_nl_create_overlay = __esm({
3263
3357
  "src/board/components/nl-create-overlay.tsx"() {
@@ -4140,8 +4234,8 @@ function Dashboard({ config: config2, options, activeProfile }) {
4140
4234
  const pendingPickRef = useRef11(null);
4141
4235
  const labelCacheRef = useRef11({});
4142
4236
  const handleCreateIssueWithPrompt = useCallback10(
4143
- (repo, title, body, labels) => {
4144
- actions.handleCreateIssue(repo, title, body, labels).then((result) => {
4237
+ (repo, title, body, dueDate, labels) => {
4238
+ actions.handleCreateIssue(repo, title, body, dueDate, labels).then((result) => {
4145
4239
  if (result) {
4146
4240
  pendingPickRef.current = result;
4147
4241
  ui.enterConfirmPick();
@@ -5057,6 +5151,34 @@ function detectStatusField(owner, projectNumber) {
5057
5151
  if (!statusField) return null;
5058
5152
  return { fieldId: statusField.id, options: statusField.options ?? [] };
5059
5153
  }
5154
+ var DATE_FIELD_NAME_RE = /^(target\s*date|due\s*date|due|deadline)$/i;
5155
+ function detectDateField(owner, projectNumber) {
5156
+ const fields = listProjectFields(owner, projectNumber);
5157
+ return fields.find((f) => DATE_FIELD_NAME_RE.test(f.name)) ?? null;
5158
+ }
5159
+ function createDateField(owner, projectNumber, fieldName) {
5160
+ try {
5161
+ execFileSync(
5162
+ "gh",
5163
+ [
5164
+ "project",
5165
+ "field-create",
5166
+ String(projectNumber),
5167
+ "--owner",
5168
+ owner,
5169
+ "--name",
5170
+ fieldName,
5171
+ "--data-type",
5172
+ "DATE"
5173
+ ],
5174
+ { encoding: "utf-8", timeout: 3e4 }
5175
+ );
5176
+ const fields = listProjectFields(owner, projectNumber);
5177
+ return fields.find((f) => f.name === fieldName)?.id ?? null;
5178
+ } catch {
5179
+ return null;
5180
+ }
5181
+ }
5060
5182
  async function runInit(opts = {}) {
5061
5183
  try {
5062
5184
  await runWizard(opts);
@@ -5140,6 +5262,37 @@ Configuring ${repoName}...`);
5140
5262
  message: " Enter status field ID manually:"
5141
5263
  });
5142
5264
  }
5265
+ console.log(" Detecting due date field...");
5266
+ let dueDateFieldId;
5267
+ const existingDateField = detectDateField(owner, projectNumber);
5268
+ if (existingDateField) {
5269
+ console.log(` Found date field: "${existingDateField.name}" (${existingDateField.id})`);
5270
+ const useDateField = await confirm({
5271
+ message: ` Use "${existingDateField.name}" for due dates?`,
5272
+ default: true
5273
+ });
5274
+ if (useDateField) {
5275
+ dueDateFieldId = existingDateField.id;
5276
+ }
5277
+ } else {
5278
+ console.log(" No due date field found in this project.");
5279
+ const createField = await confirm({
5280
+ message: ' Create a "Due Date" field for due dates?',
5281
+ default: false
5282
+ });
5283
+ if (createField) {
5284
+ console.log(' Creating "Due Date" field...');
5285
+ const newFieldId = createDateField(owner, projectNumber, "Due Date");
5286
+ if (newFieldId) {
5287
+ dueDateFieldId = newFieldId;
5288
+ console.log(` Created "Due Date" field (${newFieldId})`);
5289
+ } else {
5290
+ console.log(" Could not create field \u2014 due dates will be stored in issue body.");
5291
+ }
5292
+ } else {
5293
+ console.log(" Skipped \u2014 due dates will be stored in issue body.");
5294
+ }
5295
+ }
5143
5296
  const completionType = await select({
5144
5297
  message: ` When a task is completed, what should happen on GitHub?`,
5145
5298
  choices: [
@@ -5184,6 +5337,7 @@ Configuring ${repoName}...`);
5184
5337
  shortName,
5185
5338
  projectNumber,
5186
5339
  statusFieldId,
5340
+ ...dueDateFieldId ? { dueDateFieldId } : {},
5187
5341
  completionAction
5188
5342
  });
5189
5343
  }
@@ -5650,7 +5804,7 @@ function resolveProjectId(projectId) {
5650
5804
  process.exit(1);
5651
5805
  }
5652
5806
  var program = new Command();
5653
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.5.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
5807
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.6.1").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
5654
5808
  const opts = thisCommand.opts();
5655
5809
  if (opts.json) setFormat("json");
5656
5810
  if (opts.human) setFormat("human");