@harborppm/cli 0.74.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.
- package/dist/app.mjs +159 -50
- package/lib/cli.mjs +20 -2
- 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)));
|
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",
|
|
@@ -2821,10 +2827,22 @@ export async function run(argv, io = {}) {
|
|
|
2821
2827
|
return call("status_report", { harbor, program }, (r) => r.markdown);
|
|
2822
2828
|
}
|
|
2823
2829
|
case "export": {
|
|
2824
|
-
const { positionals, values, harbor } = parse(spec(["all"]));
|
|
2830
|
+
const { positionals, values, harbor } = parse(spec(["all", "csv"]));
|
|
2825
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
|
+
}
|
|
2826
2842
|
if (!["tasks", "risks", "review"].includes(what))
|
|
2827
|
-
throw new HarborError(
|
|
2843
|
+
throw new HarborError(
|
|
2844
|
+
"Say what to export: harbor export tasks PROJECT, risks, review or harbor.",
|
|
2845
|
+
);
|
|
2828
2846
|
const project = name.join(" ").trim();
|
|
2829
2847
|
if (what === "tasks" && !project)
|
|
2830
2848
|
throw new HarborError('Give the project: harbor export tasks "NAME".');
|