@ondrej-svec/hog 1.14.1 → 1.16.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/README.md CHANGED
@@ -88,7 +88,9 @@ Labels are abbreviated automatically: `size:M` → `[M]`, `priority:high` → `[
88
88
  | Key | Action |
89
89
  |-----|--------|
90
90
  | `p` | Pick up issue — assign to yourself + optional TickTick task |
91
- | `a` / `u` | Assign / unassign collaborator |
91
+ | `a` | Assign issue to yourself (no-op if already assigned to anyone) |
92
+ | `u` | Undo last reversible action |
93
+ | `e` | Edit issue in `$EDITOR` — change assignee, title, status, labels, body |
92
94
  | `m` | Change project status |
93
95
  | `l` | Add / remove labels |
94
96
  | `c` | Add comment |
package/dist/cli.js CHANGED
@@ -694,6 +694,18 @@ function fetchProjectFields(repo, issueNumber, projectNumber) {
694
694
  field { ... on ProjectV2SingleSelectField { name } }
695
695
  name
696
696
  }
697
+ ... on ProjectV2ItemFieldTextValue {
698
+ field { ... on ProjectV2Field { name } }
699
+ text
700
+ }
701
+ ... on ProjectV2ItemFieldNumberValue {
702
+ field { ... on ProjectV2Field { name } }
703
+ number
704
+ }
705
+ ... on ProjectV2ItemFieldIterationValue {
706
+ field { ... on ProjectV2IterationField { name } }
707
+ title
708
+ }
697
709
  }
698
710
  }
699
711
  }
@@ -724,11 +736,17 @@ function fetchProjectFields(repo, issueNumber, projectNumber) {
724
736
  const fieldValues = projectItem.fieldValues?.nodes ?? [];
725
737
  for (const fv of fieldValues) {
726
738
  if (!fv) continue;
727
- if ("date" in fv && DATE_FIELD_NAME_RE2.test(fv.field?.name ?? "")) {
739
+ const fieldName = fv.field?.name ?? "";
740
+ if ("date" in fv && DATE_FIELD_NAME_RE2.test(fieldName)) {
728
741
  fields.targetDate = fv.date;
729
- }
730
- if ("name" in fv && fv.field?.name === "Status") {
742
+ } else if ("name" in fv && fieldName === "Status") {
731
743
  fields.status = fv.name;
744
+ } else if (fieldName) {
745
+ const value = "text" in fv && fv.text != null ? fv.text : "number" in fv && fv.number != null ? String(fv.number) : "name" in fv && fv.name != null ? fv.name : "title" in fv && fv.title != null ? fv.title : null;
746
+ if (value != null) {
747
+ if (!fields.customFields) fields.customFields = {};
748
+ fields.customFields[fieldName] = value;
749
+ }
732
750
  }
733
751
  }
734
752
  return fields;
@@ -761,6 +779,18 @@ function fetchProjectEnrichment(repo, projectNumber) {
761
779
  field { ... on ProjectV2SingleSelectField { name } }
762
780
  name
763
781
  }
782
+ ... on ProjectV2ItemFieldTextValue {
783
+ field { ... on ProjectV2Field { name } }
784
+ text
785
+ }
786
+ ... on ProjectV2ItemFieldNumberValue {
787
+ field { ... on ProjectV2Field { name } }
788
+ number
789
+ }
790
+ ... on ProjectV2ItemFieldIterationValue {
791
+ field { ... on ProjectV2IterationField { name } }
792
+ title
793
+ }
764
794
  }
765
795
  }
766
796
  }
@@ -793,11 +823,17 @@ function fetchProjectEnrichment(repo, projectNumber) {
793
823
  const fieldValues = item.fieldValues?.nodes ?? [];
794
824
  for (const fv of fieldValues) {
795
825
  if (!fv) continue;
796
- if ("date" in fv && fv.date && DATE_FIELD_NAME_RE2.test(fv.field?.name ?? "")) {
826
+ const fieldName = fv.field?.name ?? "";
827
+ if ("date" in fv && fv.date && DATE_FIELD_NAME_RE2.test(fieldName)) {
797
828
  enrichment.targetDate = fv.date;
798
- }
799
- if ("name" in fv && fv.field?.name === "Status" && fv.name) {
829
+ } else if ("name" in fv && fieldName === "Status" && fv.name) {
800
830
  enrichment.projectStatus = fv.name;
831
+ } else if (fieldName) {
832
+ const value = "text" in fv && fv.text != null ? fv.text : "number" in fv && fv.number != null ? String(fv.number) : "name" in fv && fv.name != null ? fv.name : "title" in fv && fv.title != null ? fv.title : null;
833
+ if (value != null) {
834
+ if (!enrichment.customFields) enrichment.customFields = {};
835
+ enrichment.customFields[fieldName] = value;
836
+ }
801
837
  }
802
838
  }
803
839
  enrichMap.set(item.content.number, enrichment);
@@ -2270,7 +2306,8 @@ function useKeyboard({
2270
2306
  handleEnterFuzzyPicker,
2271
2307
  handleEnterEditIssue,
2272
2308
  handleUndo,
2273
- handleToggleLog
2309
+ handleToggleLog,
2310
+ showDetailPanel
2274
2311
  ]
2275
2312
  );
2276
2313
  const inputActive = ui.state.mode === "normal" || ui.state.mode === "multiSelect" || ui.state.mode === "focus" || ui.state.mode === "overlay:detail";
@@ -4192,8 +4229,9 @@ var init_help_overlay = __esm({
4192
4229
  category: "Actions",
4193
4230
  items: [
4194
4231
  { key: "p", desc: "Pick issue (assign + TickTick)" },
4195
- { key: "a", desc: "Assign to self" },
4232
+ { key: "a", desc: "Assign to self (no-op if already assigned)" },
4196
4233
  { key: "u", desc: "Undo last reversible action" },
4234
+ { key: "e", desc: "Edit issue in $EDITOR (title, assignee, status, labels)" },
4197
4235
  { key: "c", desc: "Comment on issue" },
4198
4236
  { key: "m", desc: "Move status" },
4199
4237
  { key: "e", desc: "Edit issue in $EDITOR" },
@@ -4469,7 +4507,7 @@ function SearchBar({ defaultValue, onChange, onSubmit }) {
4469
4507
  TextInput5,
4470
4508
  {
4471
4509
  defaultValue,
4472
- placeholder: "search...",
4510
+ placeholder: "title, label, status, @user, #123, unassigned\u2026",
4473
4511
  onChange,
4474
4512
  onSubmit
4475
4513
  }
@@ -5315,6 +5353,31 @@ function RefreshAge({ lastRefresh }) {
5315
5353
  timeAgo(lastRefresh)
5316
5354
  ] });
5317
5355
  }
5356
+ function matchesSearch(issue, query) {
5357
+ if (!query.trim()) return true;
5358
+ const tokens = query.toLowerCase().trim().split(/\s+/);
5359
+ const labels = issue.labels ?? [];
5360
+ const assignees = issue.assignees ?? [];
5361
+ return tokens.every((token) => {
5362
+ if (token.startsWith("#")) {
5363
+ const num = parseInt(token.slice(1), 10);
5364
+ return !Number.isNaN(num) && issue.number === num;
5365
+ }
5366
+ if (token.startsWith("@")) {
5367
+ const login = token.slice(1);
5368
+ return assignees.some((a) => a.login.toLowerCase().includes(login));
5369
+ }
5370
+ if (token === "unassigned") return assignees.length === 0;
5371
+ if (token === "assigned") return assignees.length > 0;
5372
+ if (issue.title.toLowerCase().includes(token)) return true;
5373
+ if (labels.some((l) => l.name.toLowerCase().includes(token))) return true;
5374
+ if (issue.projectStatus?.toLowerCase().includes(token)) return true;
5375
+ if (issue.customFields && Object.values(issue.customFields).some((v) => v.toLowerCase().includes(token)))
5376
+ return true;
5377
+ if (assignees.some((a) => a.login.toLowerCase().includes(token))) return true;
5378
+ return false;
5379
+ });
5380
+ }
5318
5381
  function Dashboard({ config: config2, options, activeProfile }) {
5319
5382
  const { exit } = useApp();
5320
5383
  const refreshMs = config2.board.refreshInterval * 1e3;
@@ -5364,8 +5427,7 @@ function Dashboard({ config: config2, options, activeProfile }) {
5364
5427
  })).filter((rd) => rd.issues.length > 0);
5365
5428
  }
5366
5429
  if (!searchQuery) return filtered;
5367
- const q = searchQuery.toLowerCase();
5368
- return filtered.map((rd) => ({ ...rd, issues: rd.issues.filter((i) => i.title.toLowerCase().includes(q)) })).filter((rd) => rd.issues.length > 0);
5430
+ return filtered.map((rd) => ({ ...rd, issues: rd.issues.filter((i) => matchesSearch(i, searchQuery)) })).filter((rd) => rd.issues.length > 0);
5369
5431
  }, [allRepos, searchQuery, mineOnly, config2.board.assignee]);
5370
5432
  const boardTree = useMemo3(() => buildBoardTree(repos, allActivity), [repos, allActivity]);
5371
5433
  const [selectedRepoIdx, setSelectedRepoIdx] = useState16(0);
@@ -6165,6 +6227,7 @@ async function fetchDashboard(config2, options = {}) {
6165
6227
  ...issue,
6166
6228
  ...e?.targetDate !== void 0 ? { targetDate: e.targetDate } : {},
6167
6229
  ...e?.projectStatus !== void 0 ? { projectStatus: e.projectStatus } : {},
6230
+ ...e?.customFields !== void 0 ? { customFields: e.customFields } : {},
6168
6231
  ...slackUrl ? { slackThreadUrl: slackUrl } : {}
6169
6232
  };
6170
6233
  });
@@ -6788,6 +6851,145 @@ Config written to ${CONFIG_DIR}/config.json`);
6788
6851
  console.log(" hog task list # List TickTick tasks");
6789
6852
  console.log(" hog config show # View configuration\n");
6790
6853
  }
6854
+ async function runReposAdd(initialRepoName) {
6855
+ try {
6856
+ await runReposAddWizard(initialRepoName);
6857
+ } catch (error) {
6858
+ if (error instanceof Error && error.message.includes("User force closed")) {
6859
+ console.log("\nCancelled. No changes were made.");
6860
+ return;
6861
+ }
6862
+ throw error;
6863
+ }
6864
+ }
6865
+ async function runReposAddWizard(initialRepoName) {
6866
+ console.log("\n\u{1F417} hog config repos:add\n");
6867
+ const cfg = loadFullConfig();
6868
+ let repoName = initialRepoName;
6869
+ if (!repoName) {
6870
+ console.log("Fetching repositories...");
6871
+ const allRepos = listAllRepos();
6872
+ const configuredNames = new Set(cfg.repos.map((r) => r.name));
6873
+ const available = allRepos.filter((r) => !configuredNames.has(r.nameWithOwner));
6874
+ if (available.length === 0) {
6875
+ console.log(
6876
+ "No more repositories available to add. All accessible repos are already tracked."
6877
+ );
6878
+ return;
6879
+ }
6880
+ repoName = await select({
6881
+ message: "Select repository to add:",
6882
+ choices: available.map((r) => ({ name: r.nameWithOwner, value: r.nameWithOwner }))
6883
+ });
6884
+ }
6885
+ if (!validateRepoName(repoName)) {
6886
+ console.error("Invalid repo name. Use owner/repo format (e.g. myorg/myrepo).");
6887
+ process.exit(1);
6888
+ }
6889
+ if (findRepo(cfg, repoName)) {
6890
+ console.error(`Repo "${repoName}" is already configured.`);
6891
+ process.exit(1);
6892
+ }
6893
+ const [owner, name] = repoName.split("/");
6894
+ console.log(`
6895
+ Configuring ${repoName}...`);
6896
+ console.log(" Fetching GitHub Projects...");
6897
+ const projects = listOrgProjects(owner);
6898
+ let projectNumber;
6899
+ if (projects.length === 0) {
6900
+ console.log(" No GitHub Projects found. Enter project number manually.");
6901
+ const num = await input({ message: ` Project number for ${repoName}:` });
6902
+ projectNumber = Number.parseInt(num, 10);
6903
+ } else {
6904
+ projectNumber = await select({
6905
+ message: ` Select project for ${repoName}:`,
6906
+ choices: projects.map((p) => ({ name: `#${p.number} \u2014 ${p.title}`, value: p.number }))
6907
+ });
6908
+ }
6909
+ console.log(" Detecting status field...");
6910
+ const statusInfo = detectStatusField(owner, projectNumber);
6911
+ let statusFieldId;
6912
+ if (statusInfo) {
6913
+ statusFieldId = statusInfo.fieldId;
6914
+ console.log(` Found status field: ${statusFieldId}`);
6915
+ } else {
6916
+ console.log(" Could not auto-detect status field.");
6917
+ statusFieldId = await input({ message: " Enter status field ID manually:" });
6918
+ }
6919
+ console.log(" Detecting due date field...");
6920
+ let dueDateFieldId;
6921
+ const existingDateField = detectDateField(owner, projectNumber);
6922
+ if (existingDateField) {
6923
+ console.log(` Found date field: "${existingDateField.name}" (${existingDateField.id})`);
6924
+ const useDateField = await confirm({
6925
+ message: ` Use "${existingDateField.name}" for due dates?`,
6926
+ default: true
6927
+ });
6928
+ if (useDateField) {
6929
+ dueDateFieldId = existingDateField.id;
6930
+ }
6931
+ } else {
6932
+ console.log(" No due date field found.");
6933
+ const createField = await confirm({
6934
+ message: ' Create a "Due Date" field for this project?',
6935
+ default: false
6936
+ });
6937
+ if (createField) {
6938
+ console.log(' Creating "Due Date" field...');
6939
+ const newFieldId = createDateField(owner, projectNumber, "Due Date");
6940
+ if (newFieldId) {
6941
+ dueDateFieldId = newFieldId;
6942
+ console.log(` Created "Due Date" field (${newFieldId})`);
6943
+ } else {
6944
+ console.log(" Could not create field \u2014 due dates will be stored in issue body.");
6945
+ }
6946
+ }
6947
+ }
6948
+ const completionType = await select({
6949
+ message: " When a task is completed, what should happen on GitHub?",
6950
+ choices: [
6951
+ { name: "Close the issue", value: "closeIssue" },
6952
+ { name: "Add a label (e.g. review:pending)", value: "addLabel" },
6953
+ { name: "Update project status column", value: "updateProjectStatus" }
6954
+ ]
6955
+ });
6956
+ let completionAction;
6957
+ if (completionType === "addLabel") {
6958
+ const label = await input({ message: " Label to add:", default: "review:pending" });
6959
+ completionAction = { type: "addLabel", label };
6960
+ } else if (completionType === "updateProjectStatus") {
6961
+ const statusOptions = statusInfo?.options ?? [];
6962
+ let optionId;
6963
+ if (statusOptions.length > 0) {
6964
+ optionId = await select({
6965
+ message: " Status to set when completed:",
6966
+ choices: statusOptions.map((o) => ({ name: o.name, value: o.id }))
6967
+ });
6968
+ } else {
6969
+ optionId = await input({ message: " Status option ID to set:" });
6970
+ }
6971
+ completionAction = { type: "updateProjectStatus", optionId };
6972
+ } else {
6973
+ completionAction = { type: "closeIssue" };
6974
+ }
6975
+ const shortName2 = await input({
6976
+ message: ` Short name for ${repoName}:`,
6977
+ default: name
6978
+ });
6979
+ const newRepo = {
6980
+ name: repoName,
6981
+ shortName: shortName2,
6982
+ projectNumber,
6983
+ statusFieldId,
6984
+ ...dueDateFieldId ? { dueDateFieldId } : {},
6985
+ completionAction
6986
+ };
6987
+ cfg.repos.push(newRepo);
6988
+ saveFullConfig(cfg);
6989
+ console.log(`
6990
+ Added ${shortName2} \u2192 ${repoName}`);
6991
+ console.log(" Run: hog board --live\n");
6992
+ }
6791
6993
 
6792
6994
  // src/cli.ts
6793
6995
  init_log_persistence();
@@ -7211,7 +7413,7 @@ function resolveProjectId(projectId) {
7211
7413
  process.exit(1);
7212
7414
  }
7213
7415
  var program = new Command();
7214
- program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.14.1").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7416
+ program.name("hog").description("Personal command deck \u2014 unified task dashboard for GitHub Projects + TickTick").version("1.16.0").option("--json", "Force JSON output").option("--human", "Force human-readable output").hook("preAction", (thisCommand) => {
7215
7417
  const opts = thisCommand.opts();
7216
7418
  if (opts.json) setFormat("json");
7217
7419
  if (opts.human) setFormat("human");
@@ -7393,10 +7595,18 @@ config.command("repos").description("List configured repositories").action(() =>
7393
7595
  }
7394
7596
  }
7395
7597
  });
7396
- config.command("repos:add <name>").description("Add a repository to track (owner/repo format)").requiredOption("--project-number <n>", "GitHub project number").requiredOption("--status-field-id <id>", "Project status field ID").requiredOption(
7598
+ config.command("repos:add [name]").description("Add a repository to track (interactive wizard, or pass flags for scripted use)").option("--project-number <n>", "GitHub project number (skips interactive prompt)").option("--status-field-id <id>", "Project status field ID (skips interactive prompt)").option(
7397
7599
  "--completion-type <type>",
7398
7600
  "Completion action: addLabel, updateProjectStatus, closeIssue"
7399
- ).option("--completion-option-id <id>", "Option ID for updateProjectStatus").option("--completion-label <label>", "Label for addLabel").action((name, opts) => {
7601
+ ).option("--completion-option-id <id>", "Option ID for updateProjectStatus").option("--completion-label <label>", "Label for addLabel").action(async (name, opts) => {
7602
+ if (!(opts.projectNumber && opts.statusFieldId)) {
7603
+ await runReposAdd(name);
7604
+ return;
7605
+ }
7606
+ if (!name) {
7607
+ console.error("Name argument required in non-interactive mode.");
7608
+ process.exit(1);
7609
+ }
7400
7610
  if (!validateRepoName(name)) {
7401
7611
  console.error("Invalid repo name. Use owner/repo format (e.g., myorg/myrepo)");
7402
7612
  process.exit(1);
@@ -7407,6 +7617,10 @@ config.command("repos:add <name>").description("Add a repository to track (owner
7407
7617
  process.exit(1);
7408
7618
  }
7409
7619
  const shortName2 = name.split("/")[1] ?? name;
7620
+ if (!opts.completionType) {
7621
+ console.error("--completion-type required in non-interactive mode");
7622
+ process.exit(1);
7623
+ }
7410
7624
  let completionAction;
7411
7625
  switch (opts.completionType) {
7412
7626
  case "addLabel":