@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 +52 -12
- package/lib/cli.mjs +9 -4
- package/lib/format.mjs +21 -3
- package/package.json +1 -1
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,
|
|
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(
|
|
2164
|
-
if (key.backspace || key.delete) return setValue(
|
|
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)
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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({
|
|
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(
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
12
|
-
|
|
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
|
-
|
|
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) {
|