@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.
Files changed (4) hide show
  1. package/README.md +30 -1
  2. package/dist/app.mjs +3502 -842
  3. package/lib/cli.mjs +110 -0
  4. 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));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.36.0",
3
+ "version": "0.37.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",