@harborppm/cli 0.36.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 +30 -1
- package/dist/app.mjs +3502 -842
- package/lib/cli.mjs +110 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -86,6 +86,14 @@ export const HELP = `harbor: Harbor from the terminal
|
|
|
86
86
|
harbor roadmap PROJECT [--dates|--no-dates] [--off]
|
|
87
87
|
publish a project's public roadmap
|
|
88
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
|
+
|
|
89
97
|
harbor program "NAME" [--portfolio P] [--lead me|NAME] [--starts DATE]
|
|
90
98
|
[--ends DATE]
|
|
91
99
|
harbor project "NAME" --program P [--lead me|NAME] [--starts DATE]
|
|
@@ -389,6 +397,30 @@ export function voyageForTerminal(r, project = r.project) {
|
|
|
389
397
|
|
|
390
398
|
/** Commands whose forms do different things: one line each, saying which read and which write. */
|
|
391
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
|
+
],
|
|
392
424
|
limit: [
|
|
393
425
|
" harbor limit PROJECT reads its board's column limits",
|
|
394
426
|
" harbor limit PROJECT COLUMN N writes: COLUMN takes at most N tasks",
|
|
@@ -642,6 +674,15 @@ export async function run(argv, io = {}) {
|
|
|
642
674
|
}
|
|
643
675
|
if (!terminal) throw new HarborError("The full-screen app needs a terminal.");
|
|
644
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
|
+
}
|
|
645
686
|
let creds = await readCredentials(env);
|
|
646
687
|
if (!hasFullAccess(creds)) {
|
|
647
688
|
out(
|
|
@@ -840,6 +881,75 @@ export async function run(argv, io = {}) {
|
|
|
840
881
|
return 0;
|
|
841
882
|
}
|
|
842
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
|
+
|
|
843
953
|
// A mistyped command says so, signed in or not.
|
|
844
954
|
if (commandHelp(command) === null) {
|
|
845
955
|
throw new HarborError(noCommand(command));
|