bikeshed-now 0.12.0 → 0.14.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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/package.json +1 -1
  3. package/src/index.mjs +164 -20
package/README.md CHANGED
@@ -44,12 +44,20 @@ bikeshed comments storage-migration
44
44
  # Fix that sentence, say what you changed in its thread, then publish the
45
45
  # revision. Reply first: a thread belongs to the version it was written on.
46
46
  bikeshed reply storage-migration <thread> "Rewritten to describe the dual-write period."
47
+ bikeshed resolve storage-migration <thread> # closes it, with a note saying you did
47
48
  bikeshed update storage-migration proposal.md
48
49
 
49
50
  # Given "Can suggest" rather than "Can edit"? Propose the change instead, for a
50
51
  # person to accept or reject.
51
52
  bikeshed suggest storage-migration "a single weekend" --with "two weekends"
52
53
 
54
+ # Own the document, or can approve on it? Decide somebody's suggestions: an
55
+ # accept goes into the draft, a rejection carries the reason its author is sent,
56
+ # and commit publishes the draft as one version.
57
+ bikeshed accept storage-migration <thread>
58
+ bikeshed reject storage-migration <thread> --reason "Already in section 3."
59
+ bikeshed commit storage-migration
60
+
53
61
  # Something nobody has raised yet? Start a thread on the passage it is about.
54
62
  bikeshed comment storage-migration "a single weekend" --body "Is the dual-write fortnight in this?"
55
63
  ```
@@ -72,6 +80,13 @@ bikeshed publish platform.md --id 01-platform
72
80
  If the id is taken, the document is still published, at the id plus a few
73
81
  random characters, and the CLI says so on stderr.
74
82
 
83
+ A rename changes the title and leaves the address alone, so links already sent
84
+ keep working:
85
+
86
+ ```bash
87
+ bikeshed rename 01-platform Platform review, second pass
88
+ ```
89
+
75
90
  Wherever a command takes an id, it also takes the link you were sent. An
76
91
  unlisted document you are not a member of needs the link, because its key is in
77
92
  it; without the key the document answers as an id that does not exist.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bikeshed-now",
3
- "version": "0.12.0",
3
+ "version": "0.14.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "description": "Publish and revise documents on a Bikeshed deployment from the terminal.",
package/src/index.mjs CHANGED
@@ -53,13 +53,26 @@ const USAGE = `bikeshed ${VERSION} — publish and revise documents
53
53
  bikeshed lineage <id> what it was forked from; for its owner, its forks
54
54
  bikeshed get <id> print a document's source
55
55
  bikeshed comments <id> unresolved feedback, with its anchored text
56
+ (--version <v> for an earlier version's)
56
57
  bikeshed comment <id> <text> --body <comment>
57
58
  start a thread on a passage
58
59
  bikeshed reply <id> <thread> <text> reply in a thread
60
+ bikeshed resolve <id> <thread> close a thread once its point is dealt with
61
+ bikeshed reopen <id> <thread> open a resolved thread again
59
62
  bikeshed suggest <id> <text> --with <new text>
60
63
  propose new text for a passage, for a
61
64
  person to accept or reject (--delete to
62
65
  propose removing it)
66
+ bikeshed revise <id> <thread> --with <new text>
67
+ change your suggestion before it is
68
+ decided; what it said before is kept
69
+ bikeshed withdraw <id> <thread> take back your suggestion before it is
70
+ decided; its thread stays
71
+ bikeshed accept <id> <thread> accept a suggestion, as its owner or an
72
+ approver: the change goes into the draft
73
+ bikeshed reject <id> <thread> decline a suggestion, as its owner or an
74
+ approver (--reason <why>)
75
+ bikeshed commit <id> publish the draft as the next version
63
76
  bikeshed open <id> open it in a browser
64
77
 
65
78
  Where a command takes <id> it also takes the link you were sent. An unlisted
@@ -69,6 +82,7 @@ Organising
69
82
  bikeshed folders the folder tree, with ids
70
83
  bikeshed mkdir <name> [--parent <id>] create a folder
71
84
  bikeshed mv <id> <folder|root> move a document
85
+ bikeshed rename <id> <title> change a document's title; its address stays
72
86
 
73
87
  Skills
74
88
  bikeshed skills skills this deployment publishes
@@ -93,15 +107,17 @@ Options
93
107
  --title <text> title for publish or fork (fork default: the original's)
94
108
  --id <id> with publish: the address, /d/<id> (default: from the title)
95
109
  --folder <id> destination folder for publish or fork
96
- --version <v> with fork: the version to copy (default: current);
97
- on its own, which version of bikeshed this is
110
+ --version <v> with fork: the version to copy; with comments: the
111
+ version to read (default: current); on its own,
112
+ which version of bikeshed this is
98
113
  --parent <id> parent folder for mkdir
99
114
  --limit <n> with list: rows per page (default 20, max 50)
100
115
  --cursor <id> with list: the page after the one that printed it
101
116
  --all with list: every page; with comments: resolved too
102
- --with <text> with suggest: what the passage should say instead
103
- --delete with suggest: propose deleting the passage
104
- --note <text> with suggest: why, for whoever decides
117
+ --with <text> with suggest or revise: what the passage should say instead
118
+ --delete with suggest or revise: propose deleting the passage
119
+ --note <text> with suggest or revise: why, for whoever decides
120
+ --reason <text> with reject: why, for the suggestion's author
105
121
  --prefix <text> with suggest: words just before a passage that repeats
106
122
  --suffix <text> with suggest: words just after it
107
123
  --watch with publish/update: republish on every save
@@ -506,6 +522,9 @@ async function comments(flags, [ref]) {
506
522
  const { origin, token } = resolveAuth(flags);
507
523
  const result = await callTool(origin, token, "get_comments", {
508
524
  ...docArgs(ref),
525
+ // Threads belong to the version they were opened on; a version in a link
526
+ // is not taken as a request for one, only the flag is.
527
+ ...(typeof flags.version === "string" ? { version: flags.version } : {}),
509
528
  includeResolved: Boolean(flags.all),
510
529
  });
511
530
  out(flags.json, result, () => {
@@ -518,11 +537,14 @@ async function comments(flags, [ref]) {
518
537
  // which passage to change without opening the document.
519
538
  console.log(`\n── ${thread.threadId}${thread.section ? ` (${thread.section})` : ""}`);
520
539
  if (thread.quote) console.log(` “${thread.quote}”`);
521
- // A suggested edit says what the passage would become, and whether a
522
- // person has decided it yet (SPEC §22).
540
+ // A suggested edit says what the passage would become, whether it is
541
+ // still open, and what its author proposed before changing it (SPEC §22).
523
542
  if (thread.suggestion) {
524
- const change = thread.suggestion.replacement === "" ? "(delete it)" : `“${thread.suggestion.replacement}”`;
525
- console.log(` → ${change} [${thread.suggestion.state}]`);
543
+ const said = (text) => (text === "" ? "(delete it)" : `“${text}”`);
544
+ console.log(` → ${said(thread.suggestion.replacement)} [${thread.suggestion.state}]`);
545
+ for (const earlier of thread.suggestion.revisions ?? []) {
546
+ console.log(` was ${said(earlier.replacement)}${earlier.note ? ` (${earlier.note})` : ""}`);
547
+ }
526
548
  }
527
549
  for (const comment of thread.comments) {
528
550
  const via = comment.via && comment.via !== "web" ? ` [via ${comment.via}]` : "";
@@ -568,6 +590,27 @@ async function reply(flags, [ref, threadId, ...rest]) {
568
590
  out(flags.json, result, () => console.log(`replied in ${threadId}`));
569
591
  }
570
592
 
593
+ /**
594
+ * Close a thread on the current version, or open it again. Either leaves a
595
+ * note in the thread saying who did it and through what, which is how the
596
+ * person who raised the point hears (SPEC §19).
597
+ */
598
+ // `resolve` alone is node:path's, imported above.
599
+ async function resolveThread(flags, args) {
600
+ await setResolved(flags, args, "resolve");
601
+ }
602
+
603
+ async function reopenThread(flags, args) {
604
+ await setResolved(flags, args, "reopen");
605
+ }
606
+
607
+ async function setResolved(flags, [ref, threadId, ...rest], verb) {
608
+ if (!ref || !threadId || rest.length) fail(`usage: bikeshed ${verb} <id> <thread>`);
609
+ const { origin, token } = resolveAuth(flags);
610
+ const result = await callTool(origin, token, `${verb}_thread`, { ...docArgs(ref), threadId });
611
+ out(flags.json, result, () => console.log(`${verb === "resolve" ? "resolved" : "reopened"} ${threadId}`));
612
+ }
613
+
571
614
  /**
572
615
  * Propose new text for one passage of a document's current version. The
573
616
  * passage is quoted as a reader sees it, which is how `comments` prints it, so
@@ -577,16 +620,12 @@ async function suggest(flags, [ref, ...rest]) {
577
620
  const exact = rest.join(" ");
578
621
  const usage = "usage: bikeshed suggest <id> <text> (--with <new text> | --delete) [--note <why>]";
579
622
  if (!ref || !exact) fail(usage);
580
- const replacing = typeof flags.with === "string";
581
- if (replacing === Boolean(flags.delete)) fail(usage);
582
- // An empty --with is what an unset shell variable sends; deleting a passage
583
- // has to be asked for by name.
584
- if (replacing && flags.with === "") fail("an empty --with would delete the passage; say --delete if you mean that");
623
+ const replacement = replacementFrom(flags, usage);
585
624
  const { origin, token } = resolveAuth(flags);
586
625
  const result = await callTool(origin, token, "suggest_edit", {
587
626
  ...docArgs(ref),
588
627
  exact,
589
- replacement: replacing ? flags.with : "",
628
+ replacement,
590
629
  ...(typeof flags.note === "string" ? { note: flags.note } : {}),
591
630
  ...(typeof flags.prefix === "string" ? { prefix: flags.prefix } : {}),
592
631
  ...(typeof flags.suffix === "string" ? { suffix: flags.suffix } : {}),
@@ -596,6 +635,82 @@ async function suggest(flags, [ref, ...rest]) {
596
635
  );
597
636
  }
598
637
 
638
+ /** The replacement `suggest` and `revise` were given: --with, or --delete by name. */
639
+ function replacementFrom(flags, usage) {
640
+ const replacing = typeof flags.with === "string";
641
+ if (replacing === Boolean(flags.delete)) fail(usage);
642
+ // An empty --with is what an unset shell variable sends; deleting a passage
643
+ // has to be asked for by name.
644
+ if (replacing && flags.with === "") fail("an empty --with would delete the passage; say --delete if you mean that");
645
+ return replacing ? flags.with : "";
646
+ }
647
+
648
+ /**
649
+ * Change the text of a suggestion of yours that nobody has decided yet. What
650
+ * it said before is kept, for whoever decides (SPEC §22).
651
+ */
652
+ async function revise(flags, [ref, threadId, ...rest]) {
653
+ const usage = "usage: bikeshed revise <id> <thread> (--with <new text> | --delete) [--note <why>]";
654
+ if (!ref || !threadId || rest.length) fail(usage);
655
+ const replacement = replacementFrom(flags, usage);
656
+ const { origin, token } = resolveAuth(flags);
657
+ const result = await callTool(origin, token, "revise_suggestion", {
658
+ ...docArgs(ref),
659
+ threadId,
660
+ replacement,
661
+ ...(typeof flags.note === "string" ? { note: flags.note } : {}),
662
+ });
663
+ out(flags.json, result, () => console.log(`changed ${threadId}; what it said before is kept`));
664
+ }
665
+
666
+ /**
667
+ * Accept a suggestion, as the document's owner or an approver. The change is
668
+ * made in the draft, where the decisions of a review gather until `commit`
669
+ * publishes them as one version (SPEC §22).
670
+ */
671
+ async function accept(flags, [ref, threadId, ...rest]) {
672
+ if (!ref || !threadId || rest.length) fail("usage: bikeshed accept <id> <thread>");
673
+ const { origin, token } = resolveAuth(flags);
674
+ // Deciding takes a real role on the document, never a link, so only the id is sent.
675
+ const result = await callTool(origin, token, "accept_suggestion", { id: docArgs(ref).id, threadId });
676
+ out(flags.json, result, () => console.log(`accepted ${threadId}; the change is in the draft — \`bikeshed commit\` publishes it`));
677
+ }
678
+
679
+ /** Publish the draft as the document's next version, as "Save as new version" does in the app. */
680
+ async function commit(flags, [ref, ...rest]) {
681
+ if (!ref || rest.length) fail("usage: bikeshed commit <id>");
682
+ const { origin, token } = resolveAuth(flags);
683
+ const result = await callTool(origin, token, "commit_draft", { id: docArgs(ref).id });
684
+ out(flags.json, result, () =>
685
+ console.log(result.deduped ? `nothing new: the draft is ${result.versionId} already` : `published ${result.versionId}`),
686
+ );
687
+ }
688
+
689
+ /**
690
+ * Decline a suggestion, as the document's owner or an approver. The passage
691
+ * stays as it is and the thread closes with a note giving the reason, which
692
+ * its author is sent (SPEC §22).
693
+ */
694
+ async function reject(flags, [ref, threadId, ...rest]) {
695
+ if (!ref || !threadId || rest.length) fail("usage: bikeshed reject <id> <thread> [--reason <why>]");
696
+ const { origin, token } = resolveAuth(flags);
697
+ // Deciding takes a real role on the document, never a link, so only the id is sent.
698
+ const result = await callTool(origin, token, "reject_suggestion", {
699
+ id: docArgs(ref).id,
700
+ threadId,
701
+ ...(typeof flags.reason === "string" ? { reason: flags.reason } : {}),
702
+ });
703
+ out(flags.json, result, () => console.log(`rejected ${threadId}`));
704
+ }
705
+
706
+ /** Take back a suggestion of yours that nobody has decided yet; its thread stays. */
707
+ async function withdraw(flags, [ref, threadId, ...rest]) {
708
+ if (!ref || !threadId || rest.length) fail("usage: bikeshed withdraw <id> <thread>");
709
+ const { origin, token } = resolveAuth(flags);
710
+ const result = await callTool(origin, token, "withdraw_suggestion", { ...docArgs(ref), threadId });
711
+ out(flags.json, result, () => console.log(`withdrew ${threadId}`));
712
+ }
713
+
599
714
  async function folders(flags) {
600
715
  const { origin, token } = resolveAuth(flags);
601
716
  const result = await callTool(origin, token, "list_folders", {});
@@ -706,13 +821,30 @@ async function mv(flags, [id, dest]) {
706
821
  */
707
822
  if (!id || !dest) fail("usage: bikeshed mv <id> <folder|root>");
708
823
  const { origin, token } = resolveAuth(flags);
709
- const args = dest === "root" ? { id } : { id, folder: dest };
824
+ // Moving is an editor's act, and a link's key only lets somebody read, so
825
+ // only the id goes along.
826
+ const doc = docArgs(id).id;
827
+ const args = dest === "root" ? { id: doc } : { id: doc, folder: dest };
710
828
  const result = await callTool(origin, token, "move_document", args);
711
829
  out(flags.json, result, () =>
712
- console.log(result.folder ? `moved ${id} into ${result.folder}` : `moved ${id} to the root`),
830
+ console.log(result.folder ? `moved ${doc} into ${result.folder}` : `moved ${doc} to the root`),
713
831
  );
714
832
  }
715
833
 
834
+ /**
835
+ * Give a document a new title. Its address is its id, which never changes, so
836
+ * every link already sent keeps working (SPEC §4). The title is the rest of the
837
+ * line, so it needs no quotes.
838
+ */
839
+ async function rename(flags, [ref, ...rest]) {
840
+ const title = rest.join(" ").trim();
841
+ if (!ref || !title) fail("usage: bikeshed rename <id> <title>");
842
+ const { origin, token } = resolveAuth(flags);
843
+ // Renaming takes no link key, as in the workspace list: only the id is sent.
844
+ const result = await callTool(origin, token, "rename_document", { id: docArgs(ref).id, title });
845
+ out(flags.json, result, () => console.log(`renamed ${result.id} to “${result.title}”`));
846
+ }
847
+
716
848
  /**
717
849
  * Skills are instructions, not code, and they are fetched from the DEPLOYMENT
718
850
  * rather than bundled with this CLI on purpose: a deployment running an older
@@ -757,10 +889,13 @@ async function skillsCommand(flags, [sub, dir]) {
757
889
  });
758
890
  }
759
891
 
760
- async function open(flags, [id]) {
761
- if (!id) fail("usage: bikeshed open <id>");
892
+ async function open(flags, [ref]) {
893
+ if (!ref) fail("usage: bikeshed open <id>");
762
894
  const { origin } = resolveAuth(flags);
763
- const url = `${origin}/d/${id}`;
895
+ // An unlisted document opens only with its key, and the link is the one place
896
+ // the key lives, so a link keeps it.
897
+ const { id, k } = docArgs(ref);
898
+ const url = `${origin}/d/${encodeURIComponent(id)}${k ? `?k=${encodeURIComponent(k)}` : ""}`;
764
899
  // Best effort, and the address is printed either way. This used to spawn
765
900
  // `start`, which is a cmd.exe builtin rather than a program, so on Windows
766
901
  // the spawn failed and, with nothing listening for the error, crashed.
@@ -802,11 +937,19 @@ const COMMANDS = {
802
937
  comments,
803
938
  comment,
804
939
  reply,
940
+ resolve: resolveThread,
941
+ reopen: reopenThread,
805
942
  suggest,
943
+ revise,
944
+ withdraw,
945
+ accept,
946
+ reject,
947
+ commit,
806
948
  open,
807
949
  folders,
808
950
  mkdir,
809
951
  mv,
952
+ rename,
810
953
  skills: skillsCommand,
811
954
  };
812
955
 
@@ -837,6 +980,7 @@ async function main() {
837
980
  paste: { type: "boolean", default: false },
838
981
  "no-browser": { type: "boolean", default: false },
839
982
  note: { type: "string" },
983
+ reason: { type: "string" },
840
984
  body: { type: "string" },
841
985
  prefix: { type: "string" },
842
986
  suffix: { type: "string" },