@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 +3 -1
- package/dist/cli.js +228 -14
- package/dist/cli.js.map +1 -1
- package/dist/fetch-worker.js +22 -3
- package/dist/fetch-worker.js.map +1 -1
- package/package.json +1 -1
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`
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
|
|
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.
|
|
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
|
|
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":
|