@harborppm/cli 0.49.0 → 0.50.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.
Files changed (3) hide show
  1. package/dist/app.mjs +34 -1
  2. package/lib/cli.mjs +34 -14
  3. package/package.json +1 -1
package/dist/app.mjs CHANGED
@@ -25377,9 +25377,30 @@ function dockCompare(data, projectId, changes, now) {
25377
25377
  pace: plan.pace,
25378
25378
  shift: a && b ? dayNumber(b) - dayNumber(a) : null,
25379
25379
  load,
25380
- stale: plan.stale
25380
+ stale: plan.stale,
25381
+ notes: dockNotes(plan.project, realTasks, dockTide, now)
25381
25382
  };
25382
25383
  }
25384
+ /**
25385
+ * What to know before trusting the shift: the pace is read from weeks of
25386
+ * finished work, so a short or lumpy record (work marked done in one go) or
25387
+ * a due date that holds the finish can leave the dates where they were.
25388
+ */
25389
+ function dockNotes(project, realTasks, docked, now) {
25390
+ if (docked.kind !== "forecast") return [];
25391
+ const notes = [];
25392
+ const weekly = weeklyFinished(realTasks, project.startDate, now);
25393
+ const finished = weekly.reduce((a, b) => a + b, 0);
25394
+ const busiest = Math.max(0, ...weekly);
25395
+ if (weekly.length < 2) notes.push("The pace comes from under two weeks of finished work, so treat the dates as rough.");
25396
+ else if (docked.rough) notes.push(`Only ${finished} task${finished === 1 ? "" : "s"} finished in the last ${weekly.length} weeks, so treat the dates as rough.`);
25397
+ if (weekly.length >= 2 && finished >= 4 && busiest * 2 > finished) notes.push(`${busiest} of the ${finished} tasks finished lately were marked done in one week, so the pace jumps from week to week and a few tasks more or less may not move the dates.`);
25398
+ if (docked.heldBy?.dueDate) {
25399
+ const name = taskKey(docked.heldBy, project) ?? docked.heldBy.title;
25400
+ notes.push(`${name} is due ${shortDate(docked.heldBy.dueDate)}, after the pace would finish, so its due date sets the finish and work done before then doesn't move it.`);
25401
+ }
25402
+ return notes;
25403
+ }
25383
25404
  /** "11 days sooner", "3 days later", "Same day", or null without both forecasts. */
25384
25405
  function shiftText(shift) {
25385
25406
  if (shift == null) return null;
@@ -25573,6 +25594,10 @@ function tryLines(data, project, changes, cmp, today) {
25573
25594
  const end = docked.project.endDate;
25574
25595
  if (end >= today && real.tide.kind === "forecast" && docked.tide.kind === "forecast") out.push(row(`Making ${shortDate(end)}`, end === real.project.endDate ? chanceText(real.tide.chanceByEnd) : "-", chanceText(docked.tide.chanceByEnd)));
25575
25596
  if (cmp.pace !== 1) out.push(row("Crew pace", "100%", `${Math.round(cmp.pace * 100)}%`));
25597
+ if (changes.length && cmp.notes.length) {
25598
+ out.push("");
25599
+ for (const n of cmp.notes) out.push(` Note: ${n}`);
25600
+ }
25576
25601
  if (cmp.load.length) {
25577
25602
  out.push("", " Due in the next six weeks:");
25578
25603
  for (const l of cmp.load) {
@@ -25621,6 +25646,14 @@ async function tryHere(opts) {
25621
25646
  lines
25622
25647
  };
25623
25648
  if ((opts.apply || opts.save) && changes.length === 0) throw new HarborError("There are no changes to apply or save. Add some, like --drop RL-4.");
25649
+ if (opts.apply && !opts.confirm) {
25650
+ const gone = changes.flatMap((c) => {
25651
+ if (c.kind !== "drop") return [];
25652
+ const t = data.tasks.find((x) => x.id === c.taskId);
25653
+ return t ? [`${taskKey(t, project) ?? t.id} ${t.title}`] : [];
25654
+ });
25655
+ if (gone.length) throw new HarborError(`--apply removes ${gone.length === 1 ? "this task" : `these ${gone.length} tasks`} from the harbor:\n${gone.map((g) => ` ${g}`).join("\n")}\nTo remove ${gone.length === 1 ? "it" : "them"}, run it again with --confirm. Nothing was changed.`);
25656
+ }
25624
25657
  if (opts.apply) {
25625
25658
  const s = store.getState();
25626
25659
  result.applied = applyDock({
package/lib/cli.mjs CHANGED
@@ -85,7 +85,7 @@ export const HELP = `harbor: Harbor from the terminal
85
85
  harbor try PROJECT [--drop KEYS] [--add "TITLE" [--count N]]
86
86
  [--due KEY=DATE] [--give KEY=NAME] [--end DATE]
87
87
  [--borrow NAME[=DAYS]] [--release NAME]
88
- [--saved] [--apply|--save|--discard]
88
+ [--saved] [--apply [--confirm]|--save|--discard]
89
89
  dry dock: the forecast with plan
90
90
  changes tried on a copy; nothing
91
91
  changes without --apply
@@ -566,6 +566,8 @@ const DETAIL = {
566
566
  " harbor try PROJECT [changes] reads the forecast with the changes",
567
567
  " tried on a copy of the plan",
568
568
  " harbor try PROJECT [changes] --apply writes: makes the changes for real",
569
+ " (--confirm too when it drops tasks:",
570
+ " they're removed from the harbor)",
569
571
  " harbor try PROJECT [changes] --save writes: keeps them on the project to",
570
572
  " show someone (Forecast tab, Open dry",
571
573
  " dock)",
@@ -574,7 +576,8 @@ const DETAIL = {
574
576
  " harbor try PROJECT --discard writes: throws the saved ones away",
575
577
  "",
576
578
  " Changes, each taking several with commas between:",
577
- " --drop RL-4,RL-5 take tasks out of the plan (--apply removes them)",
579
+ " --drop RL-4,RL-5 take tasks out of the plan (--apply --confirm",
580
+ " removes them)",
578
581
  ' --add "TITLE" --count N new work, N tasks called TITLE',
579
582
  " --due RL-4=fri move a due date (none takes it off)",
580
583
  " --give RL-4=Sam give a task to someone (none: nobody)",
@@ -588,6 +591,13 @@ const DETAIL = {
588
591
  " date, the crew's pace, and the load of everyone the changes touch. A",
589
592
  " borrowed person adds an average crew member's pace for each day a week",
590
593
  " they give; a released one takes away their share of what was finished.",
594
+ " Notes under it say when the dates may not move: a short or lumpy record",
595
+ " of finished work, or a due date that sets the finish.",
596
+ "",
597
+ " --json gives each plan's endDate, open, likely, veryLikely, chanceByEnd",
598
+ " and tasksPerWeek (noForecast says why when there are no dates), the",
599
+ " shiftDays between them, crewPacePercent (100 is the crew as it is) and",
600
+ " the notes.",
591
601
  ],
592
602
  report: [
593
603
  " harbor report PROGRAM reads the program's status report",
@@ -1169,7 +1179,7 @@ export async function run(argv, io = {}) {
1169
1179
 
1170
1180
  if (command === "try") {
1171
1181
  const { values, positionals } = parseArgs(rest, {
1172
- flags: ["apply", "save", "discard", "saved", "json"],
1182
+ flags: ["apply", "confirm", "save", "discard", "saved", "json"],
1173
1183
  options: ["harbor", "drop", "add", "count", "due", "give", "end", "borrow", "release"],
1174
1184
  });
1175
1185
  if (positionals.length !== 1)
@@ -1180,6 +1190,8 @@ export async function run(argv, io = {}) {
1180
1190
  );
1181
1191
  if (["apply", "save", "discard"].filter((k) => values[k]).length > 1)
1182
1192
  throw new HarborError("Use one of --apply, --save or --discard.");
1193
+ if (values.confirm && !values.apply)
1194
+ throw new HarborError("--confirm goes with --apply, to remove the tasks it drops.");
1183
1195
  const creds = await readCredentials(env);
1184
1196
  if (!hasFullAccess(creds))
1185
1197
  throw new HarborError(
@@ -1198,6 +1210,7 @@ export async function run(argv, io = {}) {
1198
1210
  project: positionals[0],
1199
1211
  flags: values,
1200
1212
  apply: Boolean(values.apply),
1213
+ confirm: Boolean(values.confirm),
1201
1214
  save: Boolean(values.save),
1202
1215
  discard: Boolean(values.discard),
1203
1216
  say,
@@ -1216,14 +1229,19 @@ export async function run(argv, io = {}) {
1216
1229
  return 0;
1217
1230
  }
1218
1231
  if (values.json) {
1219
- const side = (s) => ({
1220
- endDate: s.project.endDate,
1221
- open: s.open,
1222
- likely: s.tide.kind === "forecast" ? s.tide.likely : null,
1223
- veryLikely: s.tide.kind === "forecast" ? s.tide.veryLikely : null,
1224
- chanceByEnd: s.tide.kind === "forecast" ? s.tide.chanceByEnd : null,
1225
- forecast: s.tide.kind,
1226
- });
1232
+ const side = (s) => {
1233
+ const f = s.tide.kind === "forecast" ? s.tide : null;
1234
+ return {
1235
+ endDate: s.project.endDate,
1236
+ open: s.open,
1237
+ likely: f?.likely ?? null,
1238
+ veryLikely: f?.veryLikely ?? null,
1239
+ chanceByEnd: f?.chanceByEnd ?? null,
1240
+ tasksPerWeek: f?.perWeek ?? null,
1241
+ // Why there are no dates, when there aren't: no pace yet, paused, done.
1242
+ noForecast: f ? null : s.tide.kind,
1243
+ };
1244
+ };
1227
1245
  const c = result.comparison;
1228
1246
  out(
1229
1247
  jsonText({
@@ -1232,10 +1250,10 @@ export async function run(argv, io = {}) {
1232
1250
  real: side(c.real),
1233
1251
  dryDock: side(c.docked),
1234
1252
  shiftDays: c.shift,
1235
- pace: c.pace,
1253
+ crewPacePercent: Math.round(c.pace * 100),
1236
1254
  ...(result.applied !== undefined ? { applied: result.applied } : {}),
1237
1255
  ...(result.saved ? { saved: true } : {}),
1238
- notes,
1256
+ notes: [...c.notes, ...notes],
1239
1257
  }),
1240
1258
  );
1241
1259
  return 0;
@@ -1790,7 +1808,9 @@ export async function run(argv, io = {}) {
1790
1808
  );
1791
1809
  }
1792
1810
  if (values.confirm || values["over-limit"]) {
1793
- throw new HarborError("--confirm and --over-limit are for moving a task: harbor submit TASK.");
1811
+ throw new HarborError(
1812
+ "--confirm and --over-limit are for moving a task: harbor submit TASK.",
1813
+ );
1794
1814
  }
1795
1815
  if (values.mine && values.owner) throw new HarborError("Say --mine or --owner, not both.");
1796
1816
  return call(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.49.0",
3
+ "version": "0.50.0",
4
4
  "description": "Harbor from the terminal: a full-screen app with everything, plus commands for scripts and agents.",
5
5
  "keywords": [
6
6
  "harbor",