@harborppm/cli 0.73.0 → 0.75.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 +164 -54
  2. package/lib/cli.mjs +23 -4
  3. package/package.json +1 -1
package/dist/app.mjs CHANGED
@@ -18722,6 +18722,139 @@ function toCsv(headers, rows) {
18722
18722
  return [headers, ...rows].map((r) => r.map(cell).join(",")).join("\r\n");
18723
18723
  }
18724
18724
 
18725
+ // src/lib/payload.ts
18726
+ /**
18727
+ * The document fields of a store state, by reference, with stable empties for
18728
+ * anything missing. Cheap enough to run on every render; feeds `useHarborData`.
18729
+ */
18730
+ function pickData(s) {
18731
+ return {
18732
+ people: s.people ?? NONE,
18733
+ portfolios: s.portfolios ?? NONE,
18734
+ programs: s.programs ?? NONE,
18735
+ projects: s.projects ?? NONE,
18736
+ milestones: s.milestones ?? NONE,
18737
+ raidItems: s.raidItems ?? NONE,
18738
+ constraints: s.constraints ?? NONE,
18739
+ tasks: s.tasks ?? NONE,
18740
+ comments: s.comments ?? NONE,
18741
+ attachments: s.attachments ?? NONE,
18742
+ activity: s.activity ?? NONE,
18743
+ currentUserId: s.currentUserId,
18744
+ boardConfigs: s.boardConfigs ?? NO_CONFIGS,
18745
+ readPingIds: s.readPingIds ?? NONE,
18746
+ ropes: s.ropes ?? NONE,
18747
+ checkIns: s.checkIns ?? NONE,
18748
+ spendEntries: s.spendEntries ?? NONE,
18749
+ reflections: s.reflections ?? NONE,
18750
+ priorityRules: s.priorityRules ?? null,
18751
+ workCalendar: s.workCalendar ?? null
18752
+ };
18753
+ }
18754
+ /** The document as sent to the server: normalized and without inline file data. */
18755
+ function payloadOf(s) {
18756
+ const data = pickData(s);
18757
+ return {
18758
+ ...data,
18759
+ people: data.people.map((p) => ({
18760
+ ...p,
18761
+ accountId: p.accountId ?? null
18762
+ })),
18763
+ attachments: data.attachments.map((a) => a.url?.startsWith("data:") ? {
18764
+ ...a,
18765
+ url: null
18766
+ } : a)
18767
+ };
18768
+ }
18769
+ var NONE, NO_CONFIGS;
18770
+ var init_payload = __esmMin((() => {
18771
+ NONE = [];
18772
+ NO_CONFIGS = {};
18773
+ }));
18774
+
18775
+ // src/lib/harbor-export.ts
18776
+ /**
18777
+ * A copy of a whole harbor for its owner to keep: everything in the document
18778
+ * as one JSON file, and every task in one spreadsheet. The website downloads
18779
+ * them (components/export-dialog.tsx), agents and the terminal get the same
18780
+ * through the export_harbor tool (`harbor export harbor`). Owners only: the
18781
+ * file holds everyone's work, comments and time off.
18782
+ */
18783
+ init_blockers();
18784
+ init_indexes();
18785
+ init_payload();
18786
+ init_swatches();
18787
+ init_task_keys();
18788
+ /** Bumped when the file's shape changes in a way a reader would notice. */
18789
+ const EXPORT_VERSION = 1;
18790
+ /**
18791
+ * The whole document, as the server keeps it. Left out: who is looking
18792
+ * (`currentUserId`) and which notifications they've read, which belong to the
18793
+ * person exporting, not the harbor. Files uploaded to a task are listed with
18794
+ * their links; images pasted straight into this browser aren't on the server
18795
+ * and come without one.
18796
+ */
18797
+ function harborExport(data, harbor, now) {
18798
+ const { currentUserId: _me, readPingIds: _read, ...doc } = payloadOf(data);
18799
+ const counts = {};
18800
+ for (const [k, v] of Object.entries(doc)) if (Array.isArray(v)) counts[k] = v.length;
18801
+ return {
18802
+ format: "harbor-export",
18803
+ version: 1,
18804
+ exportedAt: now.toISOString(),
18805
+ harbor,
18806
+ counts,
18807
+ data: doc
18808
+ };
18809
+ }
18810
+ /**
18811
+ * Every task in the harbor, one row each, with its project and program. Key
18812
+ * first, as in a project's own export, so a project's rows can go back in
18813
+ * through Import.
18814
+ */
18815
+ function allTasksCsv(data) {
18816
+ const projects = byId(data.projects);
18817
+ const programs = byId(data.programs);
18818
+ const people = byId(data.people);
18819
+ const tasks = byId(data.tasks);
18820
+ const keyOf = (t) => taskKey(t, projects.get(t.projectId)) ?? t.title;
18821
+ return toCsv([
18822
+ "Key",
18823
+ "Title",
18824
+ "Project",
18825
+ "Program",
18826
+ "Status",
18827
+ "Priority",
18828
+ "Owner",
18829
+ "Due",
18830
+ "Done",
18831
+ "Blocked by",
18832
+ "Created",
18833
+ "Description"
18834
+ ], data.tasks.map((t) => {
18835
+ const project = projects.get(t.projectId);
18836
+ const program = project ? programs.get(project.programId) : void 0;
18837
+ return [
18838
+ taskKey(t, project) ?? "",
18839
+ t.title,
18840
+ project?.name ?? "",
18841
+ program?.name ?? "",
18842
+ TASK_STATUS_LABEL[t.status],
18843
+ PRIORITY_LABEL[t.priority],
18844
+ t.assigneeId && people.get(t.assigneeId)?.name || "",
18845
+ t.dueDate ?? "",
18846
+ t.status === "done" ? "yes" : "no",
18847
+ blockerIds(t).map((id) => tasks.get(id)).filter((b) => Boolean(b)).map(keyOf).join("; "),
18848
+ t.createdAt?.slice(0, 10) ?? "",
18849
+ t.description
18850
+ ];
18851
+ }));
18852
+ }
18853
+ /** "cedar-and-co-2026-11-01": a file name for the harbor on a day. */
18854
+ function exportName(harborName, now) {
18855
+ return `${harborName.normalize("NFKD").replace(/\p{M}/gu, "").replace(/&/g, " and ").replace(/[^\w]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "harbor"}-${now.toISOString().slice(0, 10)}`;
18856
+ }
18857
+
18725
18858
  // src/lib/exports.ts
18726
18859
  /**
18727
18860
  * The spreadsheets Harbor hands out: a project's tasks, the risk register and
@@ -20408,6 +20541,32 @@ const HARBOR_TOOLS = [
20408
20541
  };
20409
20542
  }
20410
20543
  }),
20544
+ tool({
20545
+ name: "export_harbor",
20546
+ title: "Download the whole harbor",
20547
+ description: "A copy of the whole harbor for its owner to keep: as json, one file with every portfolio, program, project, task, comment, milestone, risk, decision, person and day off; as csv, every task in one spreadsheet with its project and program. Owners only, since it holds everyone's work; anyone can use export_csv for one project or the register." + DATA_NOTE,
20548
+ input: object({
20549
+ harbor: harborArg,
20550
+ as: _enum(["json", "csv"]).default("json")
20551
+ }),
20552
+ run: (ctx, input) => {
20553
+ if (ctx.harbor.role !== "owner") throw new ToolError(`Only an owner can download the whole of ${ctx.harbor.name}. export_csv gives a project's tasks or the register.`);
20554
+ const base = exportName(ctx.harbor.name, ctx.now);
20555
+ if (input.as === "csv") return {
20556
+ filename: `${base}-tasks.csv`,
20557
+ csv: allTasksCsv(ctx.data)
20558
+ };
20559
+ const file = harborExport(ctx.data, {
20560
+ id: ctx.harbor.id,
20561
+ name: ctx.harbor.name
20562
+ }, ctx.now);
20563
+ return {
20564
+ filename: `${base}.json`,
20565
+ counts: file.counts,
20566
+ json: JSON.stringify(file, null, 2)
20567
+ };
20568
+ }
20569
+ }),
20411
20570
  tool({
20412
20571
  name: "list_risks",
20413
20572
  title: "Risks, issues and decisions",
@@ -21154,56 +21313,6 @@ const LIST_HARBORS = {
21154
21313
  input: object({})
21155
21314
  };
21156
21315
 
21157
- // src/lib/payload.ts
21158
- /**
21159
- * The document fields of a store state, by reference, with stable empties for
21160
- * anything missing. Cheap enough to run on every render; feeds `useHarborData`.
21161
- */
21162
- function pickData(s) {
21163
- return {
21164
- people: s.people ?? NONE,
21165
- portfolios: s.portfolios ?? NONE,
21166
- programs: s.programs ?? NONE,
21167
- projects: s.projects ?? NONE,
21168
- milestones: s.milestones ?? NONE,
21169
- raidItems: s.raidItems ?? NONE,
21170
- constraints: s.constraints ?? NONE,
21171
- tasks: s.tasks ?? NONE,
21172
- comments: s.comments ?? NONE,
21173
- attachments: s.attachments ?? NONE,
21174
- activity: s.activity ?? NONE,
21175
- currentUserId: s.currentUserId,
21176
- boardConfigs: s.boardConfigs ?? NO_CONFIGS,
21177
- readPingIds: s.readPingIds ?? NONE,
21178
- ropes: s.ropes ?? NONE,
21179
- checkIns: s.checkIns ?? NONE,
21180
- spendEntries: s.spendEntries ?? NONE,
21181
- reflections: s.reflections ?? NONE,
21182
- priorityRules: s.priorityRules ?? null,
21183
- workCalendar: s.workCalendar ?? null
21184
- };
21185
- }
21186
- /** The document as sent to the server: normalized and without inline file data. */
21187
- function payloadOf(s) {
21188
- const data = pickData(s);
21189
- return {
21190
- ...data,
21191
- people: data.people.map((p) => ({
21192
- ...p,
21193
- accountId: p.accountId ?? null
21194
- })),
21195
- attachments: data.attachments.map((a) => a.url?.startsWith("data:") ? {
21196
- ...a,
21197
- url: null
21198
- } : a)
21199
- };
21200
- }
21201
- var NONE, NO_CONFIGS;
21202
- var init_payload = __esmMin((() => {
21203
- NONE = [];
21204
- NO_CONFIGS = {};
21205
- }));
21206
-
21207
21316
  // src/lib/view-only.ts
21208
21317
  function isDocumentKey(key, state) {
21209
21318
  documentKeys ??= new Set(Object.keys(pickData(state)));
@@ -30928,15 +31037,16 @@ const ANSI = {
30928
31037
  };
30929
31038
  /** One ticker line for the terminal: the time dim, the rest bright when it's yours. */
30930
31039
  function watchText(line, at, opts) {
30931
- const time = wallClock(at).slice(11, 16);
30932
- const room = (opts.width ?? 0) - 6;
31040
+ const time = `${wallClock(at).slice(11, 16)} ${zoneName(at)}`.trim();
31041
+ const room = (opts.width ?? 0) - time.length - 1;
31042
+ const indent = " ".repeat(time.length + 1);
30933
31043
  const fit = (s, n) => opts.width && s.length > n ? `${s.slice(0, Math.max(1, n - 1))}…` : s;
30934
31044
  const text = fit(line.text, room);
30935
31045
  const quote = line.quote ? fit(`“${line.quote}”`, room - 2) : null;
30936
- if (!opts.colour) return [`${time} ${text}`, ...quote ? [` ${quote}`] : []].join("\n");
31046
+ if (!opts.colour) return [`${time} ${text}`, ...quote ? [`${indent}${quote}`] : []].join("\n");
30937
31047
  const tone = line.tone === "plain" ? "" : ANSI[line.tone];
30938
31048
  const body = line.mine ? `${ANSI.bold}${tone}${text}${ANSI.reset}` : `${tone || ANSI.dim}${text}${ANSI.reset}`;
30939
- return [`${ANSI.dim}${time}${ANSI.reset} ${body}`, ...quote ? [`${ANSI.dim} ${quote}${ANSI.reset}`] : []].join("\n");
31049
+ return [`${ANSI.dim}${time}${ANSI.reset} ${body}`, ...quote ? [`${ANSI.dim}${indent}${quote}${ANSI.reset}`] : []].join("\n");
30940
31050
  }
30941
31051
  async function watchOn(opts) {
30942
31052
  const out = (text) => opts.write(`${text}\n`);
package/lib/cli.mjs CHANGED
@@ -233,6 +233,7 @@ export const HELP = `harbor: Harbor from the terminal
233
233
  harbor report PROGRAM a program's status report
234
234
  harbor export tasks PROJECT|risks|review [--all]
235
235
  a spreadsheet (CSV) to stdout
236
+ harbor export harbor [--csv] the whole harbor as JSON (owners)
236
237
  harbor import FILE [--project P] [--portfolio P] [--no-people]
237
238
  add tasks from a CSV file
238
239
  harbor premortem PROJECT ["WHAT WENT WRONG"] [--start]
@@ -870,11 +871,16 @@ const DETAIL = {
870
871
  " harbor export risks [--all] reads the risk register as CSV",
871
872
  " (--all adds closed ones)",
872
873
  " harbor export review reads the review as CSV",
874
+ " harbor export harbor reads the whole harbor as JSON:",
875
+ " every project, task, comment, risk,",
876
+ " person and day off (owners only)",
877
+ " --csv every task instead, one row each with its project",
873
878
  "",
874
879
  " The same spreadsheets the website's Export buttons download. They print;",
875
880
  " harbor export tasks RR > tasks.csv saves one. A task export starts with",
876
881
  " each task's key, so harbor import matches those tasks by key. With",
877
882
  " --json, rows is one object per row, keyed by column.",
883
+ " harbor export harbor > cedar.json keeps a copy of everything.",
878
884
  ],
879
885
  import: [
880
886
  " harbor import FILE --project P writes: adds the file's tasks",
@@ -985,8 +991,9 @@ const DETAIL = {
985
991
  ],
986
992
  watch: [
987
993
  " harbor watch reads, live: each change to the harbor",
988
- " as it lands, one line with the time:",
989
- " cards moving, comments, health changes",
994
+ " as it lands, one line with its time",
995
+ " and zone: cards moving, comments,",
996
+ " health changes",
990
997
  " --project P one project's changes only",
991
998
  " --bell ring the terminal's bell when something of yours changes",
992
999
  " --since TIME start with every change since TIME (14:30 today, or",
@@ -2820,10 +2827,22 @@ export async function run(argv, io = {}) {
2820
2827
  return call("status_report", { harbor, program }, (r) => r.markdown);
2821
2828
  }
2822
2829
  case "export": {
2823
- const { positionals, values, harbor } = parse(spec(["all"]));
2830
+ const { positionals, values, harbor } = parse(spec(["all", "csv"]));
2824
2831
  const [what, ...name] = positionals;
2832
+ if (what === "harbor") {
2833
+ none(name);
2834
+ // The whole harbor, owners only: JSON by default, every task with --csv.
2835
+ return call(
2836
+ "export_harbor",
2837
+ { harbor, as: values.csv ? "csv" : "json" },
2838
+ (r) => (r.csv ? r.csv.replace(/\r\n/g, "\n") : r.json),
2839
+ (r) => (asJson && r.json ? JSON.parse(r.json) : r),
2840
+ );
2841
+ }
2825
2842
  if (!["tasks", "risks", "review"].includes(what))
2826
- throw new HarborError("Say what to export: harbor export tasks PROJECT, risks or review.");
2843
+ throw new HarborError(
2844
+ "Say what to export: harbor export tasks PROJECT, risks, review or harbor.",
2845
+ );
2827
2846
  const project = name.join(" ").trim();
2828
2847
  if (what === "tasks" && !project)
2829
2848
  throw new HarborError('Give the project: harbor export tasks "NAME".');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.73.0",
3
+ "version": "0.75.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",