@harborppm/cli 0.9.0 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  // Built from src/tui by cli/build.mjs. Edit the source, not this file.
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { Box, Text, render, useApp, useInput, useWindowSize } from "ink";
4
- import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
4
+ import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
 
7
7
  //#region src/lib/clock.ts
@@ -2157,15 +2157,23 @@ applyTheme("dark");
2157
2157
  */
2158
2158
  /** One line of text. Enter keeps it, Esc leaves it as it was. */
2159
2159
  function TextField({ label, initial = "", placeholder, hint, onSubmit, onCancel }) {
2160
- const [value, setValue] = useState(initial);
2160
+ const [value, setShown] = useState(initial);
2161
+ const typed = useRef(initial);
2162
+ const setValue = (next) => {
2163
+ typed.current = next;
2164
+ setShown(next);
2165
+ };
2161
2166
  useInput((input, key) => {
2162
2167
  if (key.escape) return onCancel();
2163
- if (key.return) return onSubmit(value);
2164
- if (key.backspace || key.delete) return setValue((v) => v.slice(0, -1));
2168
+ if (key.return) return onSubmit(typed.current);
2169
+ if (key.backspace || key.delete) return setValue(typed.current.slice(0, -1));
2165
2170
  if (key.ctrl && input === "u") return setValue("");
2166
2171
  if (key.ctrl || key.meta || key.tab || key.upArrow || key.downArrow) return;
2167
2172
  if (key.leftArrow || key.rightArrow) return;
2168
- if (input) setValue((v) => v + input.replace(/[\r\n]+/g, " "));
2173
+ if (!input) return;
2174
+ const ends = /[\r\n]+$/.test(input);
2175
+ setValue(typed.current + input.replace(/[\r\n]+$/, "").replace(/[\r\n]+/g, " "));
2176
+ if (ends) onSubmit(typed.current);
2169
2177
  });
2170
2178
  return /* @__PURE__ */ jsxs(Box, {
2171
2179
  flexDirection: "column",
@@ -7022,6 +7030,13 @@ function viewerUpdate(state, partial) {
7022
7030
  blocked
7023
7031
  };
7024
7032
  }
7033
+ /**
7034
+ * Whether this saved harbor can't be changed from here: the person is a
7035
+ * viewer, or its free trial has ended with nobody keeping it (keep.ts).
7036
+ */
7037
+ function readOnlyHarbor(s) {
7038
+ return s.cloud && (s.myRole === "viewer" || Boolean(s.keepPaused));
7039
+ }
7025
7040
 
7026
7041
  //#endregion
7027
7042
  //#region src/lib/demo-seed.ts
@@ -7117,7 +7132,9 @@ function sessionSlice({ set, get, write }) {
7117
7132
  setKeep: (k) => set({
7118
7133
  kept: k.kept,
7119
7134
  keepInterval: k.interval,
7120
- stripeReady: k.stripe
7135
+ stripeReady: k.stripe,
7136
+ trialEndsAt: k.trialEndsAt ?? null,
7137
+ keepPaused: Boolean(k.paused)
7121
7138
  }),
7122
7139
  ensureAccountPerson: ({ accountId, name, personId }) => {
7123
7140
  const hit = get().people.find((p) => p.accountId === accountId);
@@ -7189,7 +7206,7 @@ function sessionSlice({ set, get, write }) {
7189
7206
  set({ selectedTaskId: id });
7190
7207
  },
7191
7208
  log: (text, href) => {
7192
- if (get().cloud && get().myRole === "viewer") return;
7209
+ if (readOnlyHarbor(get())) return;
7193
7210
  set({ activity: [{
7194
7211
  id: uid("a"),
7195
7212
  personId: get().currentUserId,
package/lib/cli.mjs CHANGED
@@ -174,8 +174,9 @@ export function hint(message) {
174
174
  // zod's "Invalid option: expected one of "low"|"high"" reads as the choices.
175
175
  const said = why
176
176
  .join(": ")
177
- .replace(/^Invalid option: expected one of (.*)$/, (_, list) =>
178
- `pick one of ${list.replace(/"/g, "").split("|").join(", ")}`,
177
+ .replace(
178
+ /^Invalid option: expected one of (.*)$/,
179
+ (_, list) => `pick one of ${list.replace(/"/g, "").split("|").join(", ")}`,
179
180
  );
180
181
  if (!seen.has(flag)) seen.set(flag, said);
181
182
  }
@@ -436,7 +437,11 @@ export async function run(argv, io = {}) {
436
437
  if (!creds)
437
438
  throw new HarborError("Not signed in. Run harbor login, or harbor signup if you're new.");
438
439
  const client = createClient({ creds, save: (c) => writeCredentials(c, env), fetcher });
439
- const opts = { today: todayLocal() };
440
+ // A terminal's lines fit its width; piped output stays whole for scripts.
441
+ const opts = {
442
+ today: todayLocal(),
443
+ width: process.stdout.isTTY ? process.stdout.columns : undefined,
444
+ };
440
445
 
441
446
  const call = async (name, args, show) => {
442
447
  const result = await client.call(name, args).catch((err) => {
@@ -530,7 +535,7 @@ export async function run(argv, io = {}) {
530
535
  const lines = [
531
536
  taskLine(
532
537
  { ...t, waitingOn: t.waitingOn.filter((w) => w.status !== "done").length },
533
- opts,
538
+ { ...opts, width: undefined },
534
539
  ),
535
540
  ];
536
541
  if (t.description) lines.push("", t.description);
package/lib/format.mjs CHANGED
@@ -8,8 +8,12 @@ const STATUS = {
8
8
  done: "done",
9
9
  };
10
10
 
11
- /** `mine` leaves out whose each task is: every one is yours. */
12
- export function taskLine(t, { today, mine = false } = {}) {
11
+ /**
12
+ * `mine` leaves out whose each task is: every one is yours. `width` (the
13
+ * terminal's, when printing to one) shortens a long title so the line fits;
14
+ * piped output keeps every title whole.
15
+ */
16
+ export function taskLine(t, { today, mine = false, width } = {}) {
13
17
  const late = t.due && today && t.due < today && t.status !== "done";
14
18
  const bits = [
15
19
  STATUS[t.status] ?? t.status,
@@ -19,7 +23,21 @@ export function taskLine(t, { today, mine = false } = {}) {
19
23
  t.project ?? null,
20
24
  t.waitingOn ? `waits on ${t.waitingOn}` : null,
21
25
  ].filter(Boolean);
22
- return `${t.key ?? t.id} ${t.title} (${bits.join(" · ")})`;
26
+ const key = `${t.key ?? t.id} `;
27
+ const tail = ` (${bits.join(" · ")})`;
28
+ return key + fitTitle(t.title, width ? width - 1 - key.length - tail.length : Infinity) + tail;
29
+ }
30
+
31
+ /** The title cut to `room` characters with an ellipsis, but never below 20. */
32
+ function fitTitle(title, room) {
33
+ const chars = [...title];
34
+ if (chars.length <= room) return title;
35
+ return (
36
+ chars
37
+ .slice(0, Math.max(20, room) - 1)
38
+ .join("")
39
+ .trimEnd() + "…"
40
+ );
23
41
  }
24
42
 
25
43
  export function myWork(w, opts) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harborppm/cli",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
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",