@harborppm/cli 0.9.0 → 0.9.2

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",
@@ -5482,7 +5490,7 @@ function TaskPanel({ taskId, width, height, go, onClose }) {
5482
5490
  const projects = useHarbor((s) => s.projects);
5483
5491
  const people = useHarbor((s) => s.people);
5484
5492
  const comments = useHarbor((s) => s.comments);
5485
- const viewOnly = useHarbor((s) => s.myRole === "viewer");
5493
+ const viewOnly = useHarbor((s) => s.myRole === "viewer" || s.keepPaused);
5486
5494
  const task = tasks.find((t) => t.id === taskId);
5487
5495
  const [at, setAt] = useState(0);
5488
5496
  const [editing, setEditing] = useState(null);
@@ -5924,6 +5932,7 @@ function App({ ui }) {
5924
5932
  const harborName = useHarbor((s) => s.harborName);
5925
5933
  const workspaceId = useHarbor((s) => s.workspaceId);
5926
5934
  const viewOnly = useHarbor((s) => s.myRole === "viewer");
5935
+ const paused = useHarbor((s) => s.keepPaused);
5927
5936
  const blockPending = useHarbor((s) => s.blockPending);
5928
5937
  useHarbor((s) => s.readPingIds?.length ?? 0);
5929
5938
  const save = useStore(ui, (s) => s.save);
@@ -6339,7 +6348,10 @@ function App({ ui }) {
6339
6348
  bold: true,
6340
6349
  children: harborName ?? "…"
6341
6350
  }),
6342
- viewOnly ? /* @__PURE__ */ jsx(Text, {
6351
+ paused ? /* @__PURE__ */ jsx(Text, {
6352
+ color: theme.warn,
6353
+ children: " · trial ended, view only"
6354
+ }) : viewOnly ? /* @__PURE__ */ jsx(Text, {
6343
6355
  color: theme.warn,
6344
6356
  children: " · view only"
6345
6357
  }) : null
@@ -7022,6 +7034,13 @@ function viewerUpdate(state, partial) {
7022
7034
  blocked
7023
7035
  };
7024
7036
  }
7037
+ /**
7038
+ * Whether this saved harbor can't be changed from here: the person is a
7039
+ * viewer, or its free trial has ended with nobody keeping it (keep.ts).
7040
+ */
7041
+ function readOnlyHarbor(s) {
7042
+ return s.cloud && (s.myRole === "viewer" || Boolean(s.keepPaused));
7043
+ }
7025
7044
 
7026
7045
  //#endregion
7027
7046
  //#region src/lib/demo-seed.ts
@@ -7117,7 +7136,9 @@ function sessionSlice({ set, get, write }) {
7117
7136
  setKeep: (k) => set({
7118
7137
  kept: k.kept,
7119
7138
  keepInterval: k.interval,
7120
- stripeReady: k.stripe
7139
+ stripeReady: k.stripe,
7140
+ trialEndsAt: k.trialEndsAt ?? null,
7141
+ keepPaused: Boolean(k.paused)
7121
7142
  }),
7122
7143
  ensureAccountPerson: ({ accountId, name, personId }) => {
7123
7144
  const hit = get().people.find((p) => p.accountId === accountId);
@@ -7189,7 +7210,7 @@ function sessionSlice({ set, get, write }) {
7189
7210
  set({ selectedTaskId: id });
7190
7211
  },
7191
7212
  log: (text, href) => {
7192
- if (get().cloud && get().myRole === "viewer") return;
7213
+ if (readOnlyHarbor(get())) return;
7193
7214
  set({ activity: [{
7194
7215
  id: uid("a"),
7195
7216
  personId: get().currentUserId,
@@ -8387,6 +8408,9 @@ function peopleSpendSlice({ set, get, write }) {
8387
8408
  * as in a browser tab. No persistence: the server holds the harbor, and
8388
8409
  * sync.ts keeps this copy in step with it.
8389
8410
  */
8411
+ /** What the store tells the screen: a refused edit, an undo that kept something. */
8412
+ /** Why nothing changed in a harbor whose free trial has ended (keep.ts). */
8413
+ const PAUSED_NOTICE = "This harbor's free trial has ended, so that wasn't changed. Its owner can subscribe at harborppm.com/keep.";
8390
8414
  /** An empty harbor: what the store holds before one loads. */
8391
8415
  function blank() {
8392
8416
  return pickData({ currentUserId: "" });
@@ -8396,13 +8420,16 @@ function createAppStore(onNotice = () => {}) {
8396
8420
  const store = createStore()((set, get) => {
8397
8421
  const guardedSet = (partial, replace) => {
8398
8422
  const s = get();
8399
- if (s.myRole !== "viewer") {
8423
+ if (!readOnlyHarbor(s)) {
8400
8424
  set(partial, replace);
8401
8425
  return;
8402
8426
  }
8403
8427
  const next = typeof partial === "function" ? partial(s) : partial;
8404
8428
  const { allowed, blocked } = viewerUpdate(s, next);
8405
- if (blocked) onNotice({ kind: "view-only" });
8429
+ if (blocked) onNotice({
8430
+ kind: "view-only",
8431
+ paused: s.keepPaused
8432
+ });
8406
8433
  set(allowed);
8407
8434
  };
8408
8435
  const write = (partial) => {
@@ -8436,6 +8463,8 @@ function createAppStore(onNotice = () => {}) {
8436
8463
  kept: false,
8437
8464
  keepInterval: null,
8438
8465
  stripeReady: false,
8466
+ keepPaused: false,
8467
+ trialEndsAt: null,
8439
8468
  wipPending: null,
8440
8469
  blockPending: null,
8441
8470
  ...sessionSlice(api),
@@ -9008,6 +9037,16 @@ function createSync(opts) {
9008
9037
  if (told) notice(told.message);
9009
9038
  if (result.conflictIds.length > 0) notice("Someone else saved changes here; pulled in the latest.");
9010
9039
  }
9040
+ } else if ("paused" in result && result.paused) {
9041
+ const now = store.getState();
9042
+ now.setKeep({
9043
+ kept: false,
9044
+ interval: now.keepInterval,
9045
+ stripe: now.stripeReady,
9046
+ trialEndsAt: now.trialEndsAt,
9047
+ paused: true
9048
+ });
9049
+ notice(PAUSED_NOTICE);
9011
9050
  } else if ("readOnly" in result && result.readOnly) {
9012
9051
  store.getState().setMyRole("viewer");
9013
9052
  notice("You can only view this harbor now, so that change wasn't saved.");
@@ -9095,6 +9134,7 @@ function createSync(opts) {
9095
9134
  s.setWorkspace(loaded.id, true);
9096
9135
  s.setWorkspaceVersion(loaded.version);
9097
9136
  s.setMyRole(normalizeRole(loaded.role));
9137
+ if (loaded.keep) s.setKeep(loaded.keep);
9098
9138
  s.setHarborName(loaded.name);
9099
9139
  s.setPhase("ready");
9100
9140
  });
@@ -9279,7 +9319,7 @@ async function startApp(deps, options = {}) {
9279
9319
  const account = plainDeep(deps.account);
9280
9320
  const harbors = plainDeep(deps.harbors);
9281
9321
  const ui = createUiStore();
9282
- const store = createAppStore((n) => ui.setState({ notice: n.kind === "view-only" ? "You can only view this harbor, so that wasn't changed." : n.kept > 0 ? `Undid ${n.label}. ${n.kept} field${n.kept === 1 ? "" : "s"} someone else changed since stayed theirs.` : `Undid ${n.label}.` }));
9322
+ const store = createAppStore((n) => ui.setState({ notice: n.kind === "view-only" ? n.paused ? PAUSED_NOTICE : "You can only view this harbor, so that wasn't changed." : n.kept > 0 ? `Undid ${n.label}. ${n.kept} field${n.kept === 1 ? "" : "s"} someone else changed since stayed theirs.` : `Undid ${n.label}.` }));
9283
9323
  const sync = createSync({
9284
9324
  store,
9285
9325
  transport,
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.2",
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",