@harborppm/cli 0.35.0 → 0.37.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 +34 -1
- package/dist/app.mjs +3554 -846
- package/lib/cli.mjs +162 -0
- package/lib/format.mjs +40 -1
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
notesText,
|
|
30
30
|
voyageText,
|
|
31
31
|
handoverText,
|
|
32
|
+
agentWorkText,
|
|
32
33
|
raidList,
|
|
33
34
|
programTree,
|
|
34
35
|
projectList,
|
|
@@ -85,6 +86,14 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
85
86
|
harbor roadmap PROJECT [--dates|--no-dates] [--off]
|
|
86
87
|
publish a project's public roadmap
|
|
87
88
|
|
|
89
|
+
harbor init [PROJECT] [--hook] keep a project's tasks as files in
|
|
90
|
+
this repository (.harbor/), one
|
|
91
|
+
per task, in step both ways
|
|
92
|
+
harbor init --local [NAME] a plan in this folder alone, no
|
|
93
|
+
account; harbor here opens it
|
|
94
|
+
harbor sync [--quiet] send the files' edits to Harbor
|
|
95
|
+
and bring its changes back
|
|
96
|
+
|
|
88
97
|
harbor program "NAME" [--portfolio P] [--lead me|NAME] [--starts DATE]
|
|
89
98
|
[--ends DATE]
|
|
90
99
|
harbor project "NAME" --program P [--lead me|NAME] [--starts DATE]
|
|
@@ -125,6 +134,11 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
125
134
|
[--keep ID,ID] [--confirm]
|
|
126
135
|
hand someone's work to the crew
|
|
127
136
|
harbor handback PERSON give it back after time away
|
|
137
|
+
harbor agent [--agent NAME] the tasks given to an AI agent on
|
|
138
|
+
the crew, in the order to do them
|
|
139
|
+
harbor agent report TASK "WHAT YOU DID" [--start] [--agent NAME]
|
|
140
|
+
the agent's note; moves the task
|
|
141
|
+
to In review (--start: doing)
|
|
128
142
|
|
|
129
143
|
Every command takes --json, and --harbor NAME when you're in more than one
|
|
130
144
|
harbor (or set one with harbor use). harbor help COMMAND shows just that one.
|
|
@@ -383,6 +397,30 @@ export function voyageForTerminal(r, project = r.project) {
|
|
|
383
397
|
|
|
384
398
|
/** Commands whose forms do different things: one line each, saying which read and which write. */
|
|
385
399
|
const DETAIL = {
|
|
400
|
+
init: [
|
|
401
|
+
" harbor init [PROJECT] [--hook] writes: .harbor/ at the top of this",
|
|
402
|
+
" repository, one file per task in",
|
|
403
|
+
" PROJECT (a key like RR or a name;",
|
|
404
|
+
" the folder's name when left out)",
|
|
405
|
+
" harbor init --local [NAME] writes: a plan that lives only in",
|
|
406
|
+
" this folder, with no account;",
|
|
407
|
+
" harbor here opens it full screen",
|
|
408
|
+
"",
|
|
409
|
+
" Each file has title, status, owner, due, priority and waits between --- lines,",
|
|
410
|
+
" and the task's notes below. Edit them, add new ones (any name; Harbor gives",
|
|
411
|
+
" them a key), review them in pull requests, then run harbor sync. --hook adds",
|
|
412
|
+
" a git post-commit hook that runs harbor sync after each commit. Run init on",
|
|
413
|
+
" a local plan to move its tasks into PROJECT.",
|
|
414
|
+
],
|
|
415
|
+
sync: [
|
|
416
|
+
" harbor sync [--quiet] writes: sends what the files changed to",
|
|
417
|
+
" Harbor and rewrites the files from it",
|
|
418
|
+
"",
|
|
419
|
+
" A field you changed and nobody changed on the website goes to Harbor; one",
|
|
420
|
+
" changed in both places keeps the website's, and says so. Removing a file",
|
|
421
|
+
" doesn't delete the task (mark it done instead). On a fresh clone, run",
|
|
422
|
+
" harbor sync before editing. --quiet prints only problems.",
|
|
423
|
+
],
|
|
386
424
|
limit: [
|
|
387
425
|
" harbor limit PROJECT reads its board's column limits",
|
|
388
426
|
" harbor limit PROJECT COLUMN N writes: COLUMN takes at most N tasks",
|
|
@@ -434,6 +472,33 @@ const DETAIL = {
|
|
|
434
472
|
"",
|
|
435
473
|
' PERSON is a name, an id, or "me".',
|
|
436
474
|
],
|
|
475
|
+
handback: [
|
|
476
|
+
" harbor handback PERSON writes, straight away: gives back",
|
|
477
|
+
" what was handed over for their time",
|
|
478
|
+
" away and is still with whoever took",
|
|
479
|
+
" it, and stops showing them as away",
|
|
480
|
+
"",
|
|
481
|
+
" Anything someone else has since taken, finished or closed stays where it",
|
|
482
|
+
" is. If PERSON isn't marked away, it says so and changes nothing.",
|
|
483
|
+
" harbor handover PERSON --until DATE is what marks someone away.",
|
|
484
|
+
"",
|
|
485
|
+
' PERSON is a name, an id, or "me".',
|
|
486
|
+
],
|
|
487
|
+
agent: [
|
|
488
|
+
" harbor agent reads the tasks people gave the crew's",
|
|
489
|
+
" agent (an AI crew member on People),",
|
|
490
|
+
" in progress first, with their notes",
|
|
491
|
+
' harbor agent report TASK "TEXT" writes: the agent\'s note on the task,',
|
|
492
|
+
" and moves it to In review for a person",
|
|
493
|
+
" to check",
|
|
494
|
+
' harbor agent report TASK "TEXT" --start',
|
|
495
|
+
" writes: the note, and moves it to",
|
|
496
|
+
" In progress",
|
|
497
|
+
"",
|
|
498
|
+
" --agent NAME picks the agent when the crew has more than one. An agent",
|
|
499
|
+
" never marks its own work Done; a person does, on the website or in the",
|
|
500
|
+
" terminal app.",
|
|
501
|
+
],
|
|
437
502
|
voyage: [
|
|
438
503
|
" harbor voyage PROJECT reads a Done project's voyage log",
|
|
439
504
|
' harbor voyage PROJECT "YOUR LINE" writes: adds or changes your line in it',
|
|
@@ -609,6 +674,15 @@ export async function run(argv, io = {}) {
|
|
|
609
674
|
}
|
|
610
675
|
if (!terminal) throw new HarborError("The full-screen app needs a terminal.");
|
|
611
676
|
const { values } = parseArgs(rest, { flags: [], options: ["harbor", "server"] });
|
|
677
|
+
// In a folder with a local plan (harbor init --local), the app opens on it.
|
|
678
|
+
if (!values.harbor && !values.server) {
|
|
679
|
+
const bundled = await loadApp();
|
|
680
|
+
const dir = await bundled.localPlanAt(io.cwd ?? process.cwd());
|
|
681
|
+
if (dir) {
|
|
682
|
+
await bundled.runLocalApp({ dir, theme: (await readSettings(env)).theme });
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
612
686
|
let creds = await readCredentials(env);
|
|
613
687
|
if (!hasFullAccess(creds)) {
|
|
614
688
|
out(
|
|
@@ -807,6 +881,75 @@ export async function run(argv, io = {}) {
|
|
|
807
881
|
return 0;
|
|
808
882
|
}
|
|
809
883
|
|
|
884
|
+
if (command === "init" || command === "sync") {
|
|
885
|
+
const init = command === "init";
|
|
886
|
+
const { values, positionals } = parseArgs(rest, {
|
|
887
|
+
flags: init ? ["local", "hook", "json"] : ["quiet", "json"],
|
|
888
|
+
options: init ? ["harbor"] : [],
|
|
889
|
+
});
|
|
890
|
+
const cwd = io.cwd ?? process.cwd();
|
|
891
|
+
const lines = [];
|
|
892
|
+
const say = values.json
|
|
893
|
+
? (line) => lines.push(line)
|
|
894
|
+
: values.quiet
|
|
895
|
+
? (line) => {
|
|
896
|
+
if (!line.startsWith("⚓")) out(plain(line));
|
|
897
|
+
}
|
|
898
|
+
: (line) => out(plain(line));
|
|
899
|
+
const app = await loadApp();
|
|
900
|
+
if (init && values.local) {
|
|
901
|
+
if (positionals.length > 1)
|
|
902
|
+
throw new HarborError(`harbor init --local takes one name; put it in quotes.`);
|
|
903
|
+
const config = await app.initLocalHere({ cwd, name: positionals[0], say });
|
|
904
|
+
if (values.json) out(jsonText({ plan: config, notes: lines }));
|
|
905
|
+
return 0;
|
|
906
|
+
}
|
|
907
|
+
if (positionals.length > (init ? 1 : 0))
|
|
908
|
+
throw new HarborError(
|
|
909
|
+
init
|
|
910
|
+
? "harbor init takes one project; put a name with spaces in quotes."
|
|
911
|
+
: "harbor sync takes no words; it syncs the plan of the folder you're in.",
|
|
912
|
+
);
|
|
913
|
+
// Plans move through /api/app, as the full-screen app does: full access.
|
|
914
|
+
const transport = async () => {
|
|
915
|
+
const creds = await readCredentials(env);
|
|
916
|
+
if (!hasFullAccess(creds))
|
|
917
|
+
throw new HarborError(
|
|
918
|
+
creds
|
|
919
|
+
? "This sign-in can only read. Run harbor login (without --read-only) to keep plans in step."
|
|
920
|
+
: "Not signed in. Run harbor login, or harbor signup if you're new; harbor init --local needs no account.",
|
|
921
|
+
);
|
|
922
|
+
return app.planTransport(
|
|
923
|
+
tokenSource({ creds, save: (c) => writeCredentials(c, env), fetcher }),
|
|
924
|
+
fetcher,
|
|
925
|
+
);
|
|
926
|
+
};
|
|
927
|
+
if (init) {
|
|
928
|
+
const creds = await readCredentials(env);
|
|
929
|
+
const config = await app.initHere({
|
|
930
|
+
cwd,
|
|
931
|
+
transport: await transport(),
|
|
932
|
+
project: positionals[0],
|
|
933
|
+
hook: Boolean(values.hook),
|
|
934
|
+
say,
|
|
935
|
+
pickHarbor: (list) => {
|
|
936
|
+
if (values.harbor) return app.harborNamed(list, values.harbor);
|
|
937
|
+
const saved = list.find((h) => h.id === creds?.harbor);
|
|
938
|
+
if (saved) return saved;
|
|
939
|
+
if (list.length === 1) return list[0];
|
|
940
|
+
throw new HarborError(
|
|
941
|
+
`You're in ${list.length} harbors (${list.map((h) => h.name).join(", ")}). Add --harbor NAME, or run harbor use NAME first.`,
|
|
942
|
+
);
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
if (values.json) out(jsonText({ plan: config, notes: lines }));
|
|
946
|
+
return 0;
|
|
947
|
+
}
|
|
948
|
+
const report = await app.syncHere({ cwd, transport, say });
|
|
949
|
+
if (values.json) out(jsonText({ ...(report ?? { local: true }), notes: lines }));
|
|
950
|
+
return 0;
|
|
951
|
+
}
|
|
952
|
+
|
|
810
953
|
// A mistyped command says so, signed in or not.
|
|
811
954
|
if (commandHelp(command) === null) {
|
|
812
955
|
throw new HarborError(noCommand(command));
|
|
@@ -1461,6 +1604,25 @@ export async function run(argv, io = {}) {
|
|
|
1461
1604
|
},
|
|
1462
1605
|
);
|
|
1463
1606
|
}
|
|
1607
|
+
case "agent": {
|
|
1608
|
+
const { positionals, values, harbor } = parse(spec(["start"], ["agent"]));
|
|
1609
|
+
if (positionals[0] !== "report") {
|
|
1610
|
+
none(positionals);
|
|
1611
|
+
return call("agent_work", { harbor, agent: values.agent }, agentWorkText);
|
|
1612
|
+
}
|
|
1613
|
+
const [, task, ...words] = positionals;
|
|
1614
|
+
const note = words.join(" ").trim();
|
|
1615
|
+
if (!task || !note)
|
|
1616
|
+
throw new HarborError(
|
|
1617
|
+
'Give the task and what the agent did: harbor agent report RR-4 "Drafted it".',
|
|
1618
|
+
);
|
|
1619
|
+
return call(
|
|
1620
|
+
"agent_update",
|
|
1621
|
+
{ harbor, task, note, status: values.start ? "doing" : "review", agent: values.agent },
|
|
1622
|
+
(r) =>
|
|
1623
|
+
`${r.updated.key ?? r.updated.title} is ${r.updated.status === "review" ? "in review for a person to check" : "in progress"}, with ${r.commentedAs}'s note.`,
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1464
1626
|
case "handback": {
|
|
1465
1627
|
const { positionals, harbor } = parse(spec());
|
|
1466
1628
|
const person = positionals.join(" ").trim();
|
package/lib/format.mjs
CHANGED
|
@@ -412,9 +412,19 @@ export function handoverText(r, { confirmHint = true } = {}) {
|
|
|
412
412
|
group = i.group;
|
|
413
413
|
lines.push("", `${group}:`);
|
|
414
414
|
}
|
|
415
|
+
const due = i.due && i.kind !== "project" && i.kind !== "program" ? ` (${i.due})` : "";
|
|
416
|
+
lines.push(` ${i.what}${due}${i.to ? ` → ${i.to}` : ""}`);
|
|
417
|
+
}
|
|
418
|
+
if (r.nobodyToTake) {
|
|
415
419
|
lines.push(
|
|
416
|
-
|
|
420
|
+
"",
|
|
421
|
+
`${r.nobodyToTake} Invite someone with harbor invite EMAIL, then run this again.`,
|
|
417
422
|
);
|
|
423
|
+
if (confirmHint)
|
|
424
|
+
lines.push(
|
|
425
|
+
`Until then, --confirm moves nothing; with --until DATE it only marks ${r.person} as away.`,
|
|
426
|
+
);
|
|
427
|
+
return lines.join("\n");
|
|
418
428
|
}
|
|
419
429
|
const room = (r.takers ?? []).filter((t) => t.note).map((t) => `${t.who} ${t.note}`);
|
|
420
430
|
if (room.length) lines.push("", `This week: ${room.join(", ")}.`);
|
|
@@ -425,3 +435,32 @@ export function handoverText(r, { confirmHint = true } = {}) {
|
|
|
425
435
|
);
|
|
426
436
|
return lines.join("\n");
|
|
427
437
|
}
|
|
438
|
+
|
|
439
|
+
/** agent_work: the agent's tasks in the order to do them, then what waits on a person. */
|
|
440
|
+
export function agentWorkText(r) {
|
|
441
|
+
if (!r.agent) return r.note;
|
|
442
|
+
const lines = [`Work for ${r.agent}${r.addedBy ? ` (added by ${r.addedBy})` : ""}`];
|
|
443
|
+
if (!r.tasks?.length) lines.push("", "Nothing to do: no open tasks are assigned to it.");
|
|
444
|
+
for (const t of r.tasks ?? []) {
|
|
445
|
+
const bits = [t.status === "doing" ? "in progress" : t.status, t.priority];
|
|
446
|
+
if (t.due) bits.push(`due ${t.due}`);
|
|
447
|
+
lines.push(
|
|
448
|
+
"",
|
|
449
|
+
`${t.key ?? t.id} ${t.title} (${bits.join(", ")}${t.project ? `, ${t.project}` : ""})`,
|
|
450
|
+
);
|
|
451
|
+
if (t.notes) lines.push(...t.notes.split("\n").map((l) => ` ${l}`));
|
|
452
|
+
for (const c of t.checklist ?? []) lines.push(` [${c.done ? "x" : " "}] ${c.step}`);
|
|
453
|
+
for (const c of t.comments ?? []) lines.push(` ${c.author ?? "Someone"}: ${c.body}`);
|
|
454
|
+
}
|
|
455
|
+
if (r.total > (r.tasks ?? []).length) lines.push("", `…and ${r.total - r.tasks.length} more.`);
|
|
456
|
+
if (r.forReview?.length)
|
|
457
|
+
lines.push(
|
|
458
|
+
"",
|
|
459
|
+
`Waiting for a person to check: ${r.forReview.map((t) => t.key ?? t.title).join(", ")}.`,
|
|
460
|
+
);
|
|
461
|
+
lines.push(
|
|
462
|
+
"",
|
|
463
|
+
'When a task is done, harbor agent report TASK "WHAT YOU DID" moves it to In review for a person to check.',
|
|
464
|
+
);
|
|
465
|
+
return lines.join("\n");
|
|
466
|
+
}
|