@harborppm/cli 0.44.0 → 0.46.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 +9 -3
- package/dist/app.mjs +8650 -231
- package/lib/cli.mjs +165 -13
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -78,6 +78,13 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
78
78
|
projects with health and open work
|
|
79
79
|
(--json adds each board's limits)
|
|
80
80
|
harbor forecast [PROJECT] [--program P] when projects will likely finish
|
|
81
|
+
harbor try PROJECT [--drop KEYS] [--add "TITLE" [--count N]]
|
|
82
|
+
[--due KEY=DATE] [--give KEY=NAME] [--end DATE]
|
|
83
|
+
[--borrow NAME[=DAYS]] [--release NAME]
|
|
84
|
+
[--saved] [--apply|--save|--discard]
|
|
85
|
+
dry dock: the forecast with plan
|
|
86
|
+
changes tried on a copy; nothing
|
|
87
|
+
changes without --apply
|
|
81
88
|
harbor hill [--program P] where each project is on its hill
|
|
82
89
|
harbor hill PROJECT --at 0-100|top|done move it: up to 50 is figuring it
|
|
83
90
|
out, past 50 is getting it done
|
|
@@ -115,7 +122,9 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
115
122
|
harbor add "TITLE" --project P [--assignee me|none|NAME] [--due DATE]
|
|
116
123
|
[--priority low|medium|high|urgent] [--status S]
|
|
117
124
|
[--over-limit]
|
|
118
|
-
harbor start TASK |
|
|
125
|
+
harbor start TASK | submit TASK | done TASK [--confirm] [--over-limit]
|
|
126
|
+
move a task to In play, In review
|
|
127
|
+
or Done
|
|
119
128
|
harbor edit TASK [--title T] [--assignee me|none|NAME] [--due DATE|none]
|
|
120
129
|
[--priority P] [--status S] [--notes TEXT]
|
|
121
130
|
[--repeat weekly|biweekly|monthly|never]
|
|
@@ -139,9 +148,10 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
139
148
|
[--notes T]
|
|
140
149
|
one risk, issue or decision in
|
|
141
150
|
full, or change it
|
|
142
|
-
harbor review [--mine|--owner NAME] [--portfolio P] [--done]
|
|
151
|
+
harbor review [WORDS] [--mine|--owner NAME] [--portfolio P] [--done]
|
|
143
152
|
the review: programs worst first,
|
|
144
|
-
each with its next move
|
|
153
|
+
each with its next move (reads
|
|
154
|
+
only; harbor submit moves a task)
|
|
145
155
|
harbor next-move PROGRAM "TEXT" write a program's next move
|
|
146
156
|
harbor health --program P|--project P|--portfolio P STATE [--note WHY]
|
|
147
157
|
set its health by hand, or auto
|
|
@@ -534,6 +544,33 @@ const DETAIL = {
|
|
|
534
544
|
" than the pace would finish sets the dates, and the forecast names it. It",
|
|
535
545
|
" warns when under two weeks of pace is all there is.",
|
|
536
546
|
],
|
|
547
|
+
try: [
|
|
548
|
+
" harbor try PROJECT [changes] reads the forecast with the changes",
|
|
549
|
+
" tried on a copy of the plan",
|
|
550
|
+
" harbor try PROJECT [changes] --apply writes: makes the changes for real",
|
|
551
|
+
" harbor try PROJECT [changes] --save writes: keeps them on the project to",
|
|
552
|
+
" show someone (Forecast tab, Open dry",
|
|
553
|
+
" dock)",
|
|
554
|
+
" harbor try PROJECT --saved reads the saved ones (add changes,",
|
|
555
|
+
" --apply or --save as above)",
|
|
556
|
+
" harbor try PROJECT --discard writes: throws the saved ones away",
|
|
557
|
+
"",
|
|
558
|
+
" Changes, each taking several with commas between:",
|
|
559
|
+
" --drop RL-4,RL-5 take tasks out of the plan (--apply removes them)",
|
|
560
|
+
' --add "TITLE" --count N new work, N tasks called TITLE',
|
|
561
|
+
" --due RL-4=fri move a due date (none takes it off)",
|
|
562
|
+
" --give RL-4=Sam give a task to someone (none: nobody)",
|
|
563
|
+
" --end 2026-12-19 move the project's end date",
|
|
564
|
+
" --borrow Sam=3 someone joins 3 days a week (5 if left out);",
|
|
565
|
+
" --apply raises their share of the program",
|
|
566
|
+
" --release Priya someone leaves: their open tasks lose their owner",
|
|
567
|
+
"",
|
|
568
|
+
" It prints the real plan and the changed one side by side: when it's",
|
|
569
|
+
" probably and very likely done, open tasks, the chance of making the end",
|
|
570
|
+
" date, the crew's pace, and the load of everyone the changes touch. A",
|
|
571
|
+
" borrowed person adds an average crew member's pace for each day a week",
|
|
572
|
+
" they give; a released one takes away their share of what was finished.",
|
|
573
|
+
],
|
|
537
574
|
report: [
|
|
538
575
|
" harbor report PROGRAM reads the program's status report",
|
|
539
576
|
"",
|
|
@@ -658,8 +695,10 @@ const DETAIL = {
|
|
|
658
695
|
" project in or hold a task in",
|
|
659
696
|
" harbor review --owner NAME the same for someone else",
|
|
660
697
|
" harbor review --portfolio P one portfolio's",
|
|
661
|
-
|
|
698
|
+
' harbor review "WORDS" only programs whose name has them',
|
|
662
699
|
"",
|
|
700
|
+
" The review only reads. To move a task to In review, run harbor submit",
|
|
701
|
+
" TASK (harbor review TASK did that before 0.45.0, and now says so).",
|
|
663
702
|
' Write a next move with harbor next-move PROGRAM "TEXT"; set a health by',
|
|
664
703
|
" hand with harbor health. harbor export review prints it as CSV.",
|
|
665
704
|
],
|
|
@@ -789,6 +828,13 @@ async function harborNamed(client, key) {
|
|
|
789
828
|
|
|
790
829
|
const TASK_KEY = /^[A-Za-z][A-Za-z0-9]{0,9}-\d+$/;
|
|
791
830
|
|
|
831
|
+
/** True when every word of `words` is somewhere in `name`, any case. */
|
|
832
|
+
const nameHas = (name, words) =>
|
|
833
|
+
words
|
|
834
|
+
.toLowerCase()
|
|
835
|
+
.split(/\s+/)
|
|
836
|
+
.every((w) => name.toLowerCase().includes(w));
|
|
837
|
+
|
|
792
838
|
/**
|
|
793
839
|
* A task by its key, else a project by name, else a task by id: its web
|
|
794
840
|
* link and what to call it. Harbor's own refusal when nothing matches.
|
|
@@ -919,6 +965,7 @@ export async function run(argv, io = {}) {
|
|
|
919
965
|
await writeCredentials({ ...now, harbor: h.id, harborName: h.name }, env);
|
|
920
966
|
},
|
|
921
967
|
theme: (await readSettings(env)).theme,
|
|
968
|
+
browse: io.browse ?? openBrowser,
|
|
922
969
|
});
|
|
923
970
|
return 0;
|
|
924
971
|
}
|
|
@@ -1069,6 +1116,100 @@ export async function run(argv, io = {}) {
|
|
|
1069
1116
|
return 0;
|
|
1070
1117
|
}
|
|
1071
1118
|
|
|
1119
|
+
if (command === "try") {
|
|
1120
|
+
const { values, positionals } = parseArgs(rest, {
|
|
1121
|
+
flags: ["apply", "save", "discard", "saved", "json"],
|
|
1122
|
+
options: ["harbor", "drop", "add", "count", "due", "give", "end", "borrow", "release"],
|
|
1123
|
+
});
|
|
1124
|
+
if (positionals.length !== 1)
|
|
1125
|
+
throw new HarborError(
|
|
1126
|
+
positionals.length
|
|
1127
|
+
? "harbor try takes one project; put a name with spaces in quotes."
|
|
1128
|
+
: "Say which project: harbor try PROJECT --drop RL-4. harbor help try has the rest.",
|
|
1129
|
+
);
|
|
1130
|
+
if (["apply", "save", "discard"].filter((k) => values[k]).length > 1)
|
|
1131
|
+
throw new HarborError("Use one of --apply, --save or --discard.");
|
|
1132
|
+
const creds = await readCredentials(env);
|
|
1133
|
+
if (!hasFullAccess(creds))
|
|
1134
|
+
throw new HarborError(
|
|
1135
|
+
creds
|
|
1136
|
+
? "This sign-in can only read. Run harbor login (without --read-only) to use dry dock."
|
|
1137
|
+
: "Not signed in. Run harbor login, or harbor signup if you're new.",
|
|
1138
|
+
);
|
|
1139
|
+
const app = await loadApp();
|
|
1140
|
+
const notes = [];
|
|
1141
|
+
const say = values.json ? (line) => notes.push(line) : (line) => out(plain(line));
|
|
1142
|
+
const result = await app.tryHere({
|
|
1143
|
+
transport: app.planTransport(
|
|
1144
|
+
tokenSource({ creds, save: (c) => writeCredentials(c, env), fetcher }),
|
|
1145
|
+
fetcher,
|
|
1146
|
+
),
|
|
1147
|
+
project: positionals[0],
|
|
1148
|
+
flags: values,
|
|
1149
|
+
apply: Boolean(values.apply),
|
|
1150
|
+
save: Boolean(values.save),
|
|
1151
|
+
discard: Boolean(values.discard),
|
|
1152
|
+
say,
|
|
1153
|
+
pickHarbor: (list) => {
|
|
1154
|
+
if (values.harbor) return app.harborNamed(list, values.harbor);
|
|
1155
|
+
const saved = list.find((h) => h.id === creds?.harbor);
|
|
1156
|
+
if (saved) return saved;
|
|
1157
|
+
if (list.length === 1) return list[0];
|
|
1158
|
+
throw new HarborError(
|
|
1159
|
+
`You're in ${list.length} harbors (${list.map((h) => h.name).join(", ")}). Add --harbor NAME, or run harbor use NAME first.`,
|
|
1160
|
+
);
|
|
1161
|
+
},
|
|
1162
|
+
});
|
|
1163
|
+
if (!result) {
|
|
1164
|
+
if (values.json) out(jsonText({ notes }));
|
|
1165
|
+
return 0;
|
|
1166
|
+
}
|
|
1167
|
+
if (values.json) {
|
|
1168
|
+
const side = (s) => ({
|
|
1169
|
+
endDate: s.project.endDate,
|
|
1170
|
+
open: s.open,
|
|
1171
|
+
likely: s.tide.kind === "forecast" ? s.tide.likely : null,
|
|
1172
|
+
veryLikely: s.tide.kind === "forecast" ? s.tide.veryLikely : null,
|
|
1173
|
+
chanceByEnd: s.tide.kind === "forecast" ? s.tide.chanceByEnd : null,
|
|
1174
|
+
forecast: s.tide.kind,
|
|
1175
|
+
});
|
|
1176
|
+
const c = result.comparison;
|
|
1177
|
+
out(
|
|
1178
|
+
jsonText({
|
|
1179
|
+
project: { id: result.project.id, key: result.project.key, name: result.project.name },
|
|
1180
|
+
changes: result.changes,
|
|
1181
|
+
real: side(c.real),
|
|
1182
|
+
dryDock: side(c.docked),
|
|
1183
|
+
shiftDays: c.shift,
|
|
1184
|
+
pace: c.pace,
|
|
1185
|
+
...(result.applied !== undefined ? { applied: result.applied } : {}),
|
|
1186
|
+
...(result.saved ? { saved: true } : {}),
|
|
1187
|
+
notes,
|
|
1188
|
+
}),
|
|
1189
|
+
);
|
|
1190
|
+
return 0;
|
|
1191
|
+
}
|
|
1192
|
+
for (const line of result.lines) out(plain(line));
|
|
1193
|
+
out("");
|
|
1194
|
+
if (result.applied !== undefined)
|
|
1195
|
+
out(
|
|
1196
|
+
plain(
|
|
1197
|
+
`⚓ Applied ${result.applied} change${result.applied === 1 ? "" : "s"} to ${result.project.name}.`,
|
|
1198
|
+
),
|
|
1199
|
+
);
|
|
1200
|
+
else if (result.saved)
|
|
1201
|
+
out(
|
|
1202
|
+
plain(
|
|
1203
|
+
`⚓ Saved on ${result.project.name}. It's on the Forecast tab under Open dry dock; --apply makes it real.`,
|
|
1204
|
+
),
|
|
1205
|
+
);
|
|
1206
|
+
else if (result.changes.length)
|
|
1207
|
+
out(
|
|
1208
|
+
"Nothing was changed. Add --apply to make these changes, or --save to keep them to show someone.",
|
|
1209
|
+
);
|
|
1210
|
+
return 0;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1072
1213
|
if (command === "init" || command === "sync") {
|
|
1073
1214
|
const init = command === "init";
|
|
1074
1215
|
const { values, positionals } = parseArgs(rest, {
|
|
@@ -1581,6 +1722,7 @@ export async function run(argv, io = {}) {
|
|
|
1581
1722
|
);
|
|
1582
1723
|
}
|
|
1583
1724
|
case "start":
|
|
1725
|
+
case "submit":
|
|
1584
1726
|
case "review":
|
|
1585
1727
|
case "done": {
|
|
1586
1728
|
const { positionals, values, harbor } = parse(
|
|
@@ -1588,7 +1730,17 @@ export async function run(argv, io = {}) {
|
|
|
1588
1730
|
? spec(["confirm", "over-limit", "mine", "done"], ["owner", "portfolio"])
|
|
1589
1731
|
: spec(["confirm", "over-limit"]),
|
|
1590
1732
|
);
|
|
1591
|
-
if (command === "review"
|
|
1733
|
+
if (command === "review") {
|
|
1734
|
+
// The review only reads: a task key here once moved the task, so it says where that went.
|
|
1735
|
+
const words = positionals.join(" ").trim();
|
|
1736
|
+
if (TASK_KEY.test(words)) {
|
|
1737
|
+
throw new HarborError(
|
|
1738
|
+
`harbor review only reads the review now. To move ${words} to In review, run harbor submit ${words}.`,
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
if (values.confirm || values["over-limit"]) {
|
|
1742
|
+
throw new HarborError("--confirm and --over-limit are for moving a task: harbor submit TASK.");
|
|
1743
|
+
}
|
|
1592
1744
|
if (values.mine && values.owner) throw new HarborError("Say --mine or --owner, not both.");
|
|
1593
1745
|
return call(
|
|
1594
1746
|
"program_review",
|
|
@@ -1598,15 +1750,14 @@ export async function run(argv, io = {}) {
|
|
|
1598
1750
|
portfolio: values.portfolio,
|
|
1599
1751
|
includeDone: Boolean(values.done),
|
|
1600
1752
|
},
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
"--mine, --owner, --portfolio and --done are for the review: harbor review on its own.",
|
|
1753
|
+
(r) =>
|
|
1754
|
+
words && r.programs.length === 0
|
|
1755
|
+
? `No program in the review matches "${words}".`
|
|
1756
|
+
: reviewList(r),
|
|
1757
|
+
(r) => (words ? { ...r, programs: r.programs.filter((p) => nameHas(p.name, words)) } : r),
|
|
1607
1758
|
);
|
|
1608
1759
|
}
|
|
1609
|
-
const status = { start: "doing",
|
|
1760
|
+
const status = { start: "doing", submit: "review", done: "done" }[command];
|
|
1610
1761
|
return call(
|
|
1611
1762
|
"update_task",
|
|
1612
1763
|
{
|
|
@@ -2005,7 +2156,8 @@ export async function run(argv, io = {}) {
|
|
|
2005
2156
|
([k, v]) => !["harbor", "item"].includes(k) && v !== undefined,
|
|
2006
2157
|
);
|
|
2007
2158
|
if (!changes.length) {
|
|
2008
|
-
|
|
2159
|
+
// --json gives the item itself, as harbor show does a task.
|
|
2160
|
+
return call("get_raid", { harbor, item }, raidDetail, (r) => r.item ?? r);
|
|
2009
2161
|
}
|
|
2010
2162
|
return call("update_raid", args, (r) => `Updated.\n\n${raidDetail(r.updated)}`);
|
|
2011
2163
|
}
|