@ondrej-svec/hog 1.5.0 → 1.6.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.
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
 
@@ -1222,8 +1303,16 @@ function useActions({
1222
1303
  });
1223
1304
  }, [toast, refresh]);
1224
1305
  const handleCreateIssue = useCallback(
1225
- async (repo, title, body, labels) => {
1226
- const args = ["issue", "create", "--repo", repo, "--title", title, "--body", body];
1306
+ async (repo, title, body, dueDate, labels) => {
1307
+ const repoConfig = configRef.current.repos.find((r) => r.name === repo);
1308
+ let effectiveBody = body;
1309
+ if (dueDate && !repoConfig?.dueDateFieldId) {
1310
+ const dueLine = `Due: ${dueDate}`;
1311
+ effectiveBody = body ? `${body}
1312
+
1313
+ ${dueLine}` : dueLine;
1314
+ }
1315
+ const args = ["issue", "create", "--repo", repo, "--title", title, "--body", effectiveBody];
1227
1316
  if (labels && labels.length > 0) {
1228
1317
  for (const label of labels) {
1229
1318
  args.push("--label", label);
@@ -1235,7 +1324,15 @@ function useActions({
1235
1324
  const output = stdout.trim();
1236
1325
  const match = output.match(/\/(\d+)$/);
1237
1326
  const issueNumber = match?.[1] ? parseInt(match[1], 10) : 0;
1238
- const shortName = configRef.current.repos.find((r) => r.name === repo)?.shortName ?? repo;
1327
+ const shortName = repoConfig?.shortName ?? repo;
1328
+ if (issueNumber > 0 && dueDate && repoConfig?.dueDateFieldId) {
1329
+ const dueDateConfig = {
1330
+ projectNumber: repoConfig.projectNumber,
1331
+ dueDateFieldId: repoConfig.dueDateFieldId
1332
+ };
1333
+ updateProjectItemDateAsync(repo, issueNumber, dueDateConfig, dueDate).catch(() => {
1334
+ });
1335
+ }
1239
1336
  t.resolve(`Created ${shortName}#${issueNumber}`);
1240
1337
  refresh();
1241
1338
  onOverlayDone();
@@ -2778,13 +2875,19 @@ function CreateIssueForm({
2778
2875
  currentLabels: [],
2779
2876
  labelCache: labelCache ?? {},
2780
2877
  onConfirm: (addLabels) => {
2781
- onSubmit(selectedRepo.name, title, "", addLabels.length > 0 ? addLabels : void 0);
2878
+ onSubmit(
2879
+ selectedRepo.name,
2880
+ title,
2881
+ "",
2882
+ null,
2883
+ addLabels.length > 0 ? addLabels : void 0
2884
+ );
2782
2885
  },
2783
2886
  onCancel: () => {
2784
- onSubmit(selectedRepo.name, title, "");
2887
+ onSubmit(selectedRepo.name, title, "", null);
2785
2888
  },
2786
2889
  onError: () => {
2787
- onSubmit(selectedRepo.name, title, "");
2890
+ onSubmit(selectedRepo.name, title, "", null);
2788
2891
  }
2789
2892
  }
2790
2893
  )
@@ -2820,7 +2923,7 @@ function CreateIssueForm({
2820
2923
  setTitle(trimmed);
2821
2924
  setField("labels");
2822
2925
  } else {
2823
- onSubmit(selectedRepo.name, trimmed, "");
2926
+ onSubmit(selectedRepo.name, trimmed, "", null);
2824
2927
  }
2825
2928
  }
2826
2929
  }
@@ -3109,7 +3212,7 @@ function NlCreateOverlay({
3109
3212
  setBody(content.trimEnd());
3110
3213
  } finally {
3111
3214
  onResumeRef.current?.();
3112
- if (tmpFile) {
3215
+ if (tmpFile && tmpDir) {
3113
3216
  try {
3114
3217
  rmSync2(tmpDir, { recursive: true, force: true });
3115
3218
  } catch {
@@ -3186,6 +3289,7 @@ function NlCreateOverlay({
3186
3289
  selectedRepo.name,
3187
3290
  parsed.title,
3188
3291
  text.trim(),
3292
+ parsed.dueDate,
3189
3293
  labels.length > 0 ? labels : void 0
3190
3294
  );
3191
3295
  }
@@ -3196,7 +3300,7 @@ function NlCreateOverlay({
3196
3300
  ] });
3197
3301
  }
3198
3302
  if (parsed) {
3199
- const labels = buildLabelList(parsed);
3303
+ const labels = [...parsed.labels];
3200
3304
  return /* @__PURE__ */ jsxs9(Box9, { flexDirection: "column", children: [
3201
3305
  /* @__PURE__ */ jsx9(Text9, { color: "cyan", bold: true, children: "\u2728 Creating Issue" }),
3202
3306
  /* @__PURE__ */ jsxs9(Box9, { children: [
@@ -3223,7 +3327,6 @@ function NlCreateOverlay({
3223
3327
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Due: " }),
3224
3328
  /* @__PURE__ */ jsx9(Text9, { children: formatDue(parsed.dueDate) })
3225
3329
  ] }) : 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
3330
  /* @__PURE__ */ jsx9(Text9, { dimColor: true, children: "Enter:add body Esc:cancel" })
3228
3331
  ] });
3229
3332
  }
@@ -3245,19 +3348,11 @@ function NlCreateOverlay({
3245
3348
  ] });
3246
3349
  }
3247
3350
  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:"));
3351
+ return [...parsed.labels];
3256
3352
  }
3257
3353
  function formatDue(dueDate) {
3258
3354
  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})`;
3355
+ return d.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" });
3261
3356
  }
3262
3357
  var init_nl_create_overlay = __esm({
3263
3358
  "src/board/components/nl-create-overlay.tsx"() {
@@ -4140,8 +4235,8 @@ function Dashboard({ config: config2, options, activeProfile }) {
4140
4235
  const pendingPickRef = useRef11(null);
4141
4236
  const labelCacheRef = useRef11({});
4142
4237
  const handleCreateIssueWithPrompt = useCallback10(
4143
- (repo, title, body, labels) => {
4144
- actions.handleCreateIssue(repo, title, body, labels).then((result) => {
4238
+ (repo, title, body, dueDate, labels) => {
4239
+ actions.handleCreateIssue(repo, title, body, dueDate, labels).then((result) => {
4145
4240
  if (result) {
4146
4241
  pendingPickRef.current = result;
4147
4242
  ui.enterConfirmPick();
@@ -5057,6 +5152,34 @@ function detectStatusField(owner, projectNumber) {
5057
5152
  if (!statusField) return null;
5058
5153
  return { fieldId: statusField.id, options: statusField.options ?? [] };
5059
5154
  }
5155
+ var DATE_FIELD_NAME_RE = /^(target\s*date|due\s*date|due|deadline)$/i;
5156
+ function detectDateField(owner, projectNumber) {
5157
+ const fields = listProjectFields(owner, projectNumber);
5158
+ return fields.find((f) => DATE_FIELD_NAME_RE.test(f.name)) ?? null;
5159
+ }
5160
+ function createDateField(owner, projectNumber, fieldName) {
5161
+ try {
5162
+ execFileSync(
5163
+ "gh",
5164
+ [
5165
+ "project",
5166
+ "field-create",
5167
+ String(projectNumber),
5168
+ "--owner",
5169
+ owner,
5170
+ "--name",
5171
+ fieldName,
5172
+ "--data-type",
5173
+ "DATE"
5174
+ ],
5175
+ { encoding: "utf-8", timeout: 3e4 }
5176
+ );
5177
+ const fields = listProjectFields(owner, projectNumber);
5178
+ return fields.find((f) => f.name === fieldName)?.id ?? null;
5179
+ } catch {
5180
+ return null;
5181
+ }
5182
+ }
5060
5183
  async function runInit(opts = {}) {
5061
5184
  try {
5062
5185
  await runWizard(opts);
@@ -5140,6 +5263,37 @@ Configuring ${repoName}...`);
5140
5263
  message: " Enter status field ID manually:"
5141
5264
  });
5142
5265
  }
5266
+ console.log(" Detecting due date field...");
5267
+ let dueDateFieldId;
5268
+ const existingDateField = detectDateField(owner, projectNumber);
5269
+ if (existingDateField) {
5270
+ console.log(` Found date field: "${existingDateField.name}" (${existingDateField.id})`);
5271
+ const useDateField = await confirm({
5272
+ message: ` Use "${existingDateField.name}" for due dates?`,
5273
+ default: true
5274
+ });
5275
+ if (useDateField) {
5276
+ dueDateFieldId = existingDateField.id;
5277
+ }
5278
+ } else {
5279
+ console.log(" No due date field found in this project.");
5280
+ const createField = await confirm({
5281
+ message: ' Create a "Due Date" field for due dates?',
5282
+ default: false
5283
+ });
5284
+ if (createField) {
5285
+ console.log(' Creating "Due Date" field...');
5286
+ const newFieldId = createDateField(owner, projectNumber, "Due Date");
5287
+ if (newFieldId) {
5288
+ dueDateFieldId = newFieldId;
5289
+ console.log(` Created "Due Date" field (${newFieldId})`);
5290
+ } else {
5291
+ console.log(" Could not create field \u2014 due dates will be stored in issue body.");
5292
+ }
5293
+ } else {
5294
+ console.log(" Skipped \u2014 due dates will be stored in issue body.");
5295
+ }
5296
+ }
5143
5297
  const completionType = await select({
5144
5298
  message: ` When a task is completed, what should happen on GitHub?`,
5145
5299
  choices: [
@@ -5184,6 +5338,7 @@ Configuring ${repoName}...`);
5184
5338
  shortName,
5185
5339
  projectNumber,
5186
5340
  statusFieldId,
5341
+ ...dueDateFieldId ? { dueDateFieldId } : {},
5187
5342
  completionAction
5188
5343
  });
5189
5344
  }
@@ -5650,7 +5805,7 @@ function resolveProjectId(projectId) {
5650
5805
  process.exit(1);
5651
5806
  }
5652
5807
  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) => {
5808
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.6.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
5654
5809
  const opts = thisCommand.opts();
5655
5810
  if (opts.json) setFormat("json");
5656
5811
  if (opts.human) setFormat("human");