bikeshed-now 0.4.1 → 0.6.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.
package/README.md CHANGED
@@ -30,6 +30,10 @@ bikeshed comments storage-migration
30
30
  # Fix that sentence, publish the revision, and say what you changed.
31
31
  bikeshed update storage-migration proposal.md
32
32
  bikeshed reply storage-migration <thread> "Rewritten to describe the dual-write period."
33
+
34
+ # Given "Can suggest" rather than "Can edit"? Propose the change instead, for a
35
+ # person to accept or reject.
36
+ bikeshed suggest storage-migration "a single weekend" --with "two weekends"
33
37
  ```
34
38
 
35
39
  Publishing identical bytes is a no-op: the bundle is hashed, and a matching hash
@@ -61,6 +65,24 @@ bikeshed comments 'https://bikeshed.now/d/build-estimate?k=…'
61
65
  `bikeshed --help` lists the rest — listing documents, folders, and installing the
62
66
  agent skills a deployment serves.
63
67
 
68
+ ## Forking
69
+
70
+ To revise a document you can read but do not own, fork it. You get your own
71
+ private copy, at the version you name or the current one, and it remembers what
72
+ it was forked from.
73
+
74
+ ```bash
75
+ bikeshed fork 'https://bikeshed.now/d/build-estimate?k=…'
76
+ # forked build-estimate into build-estimate-x7k2p9 — https://bikeshed.now/d/build-estimate-x7k2p9
77
+ bikeshed lineage build-estimate-x7k2p9
78
+ # forked from build-estimate v3 Build estimate (changed since)
79
+ ```
80
+
81
+ `lineage` shows each original you can open, nearest first, and whether the
82
+ nearest has changed since you forked it. On a document you own it also counts
83
+ its forks. The author of what you fork sees that it was forked — a count, never
84
+ who.
85
+
64
86
  ## Sharing is not automated
65
87
 
66
88
  There is no command that shares a document, changes who can read it, or sets an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bikeshed-now",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
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/client.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  import { readFileSync } from "node:fs";
5
5
  import { basename } from "node:path";
6
6
 
7
- export const CLIENT_NAME = "bikeshed-cli/0.4.1";
7
+ export const CLIENT_NAME = "bikeshed-cli/0.6.0";
8
8
 
9
9
  /**
10
10
  * Three surfaces, and the split is deliberate rather than untidy.
package/src/index.mjs CHANGED
@@ -39,9 +39,15 @@ const USAGE = `bikeshed — publish and revise documents
39
39
  bikeshed list documents you can read
40
40
  bikeshed publish <file> create a new document
41
41
  bikeshed update <id> <file> publish a new version
42
+ bikeshed fork <id> your own copy, which remembers its original
43
+ bikeshed lineage <id> what it was forked from; for its owner, its forks
42
44
  bikeshed get <id> print a document's source
43
45
  bikeshed comments <id> unresolved feedback, with its anchored text
44
46
  bikeshed reply <id> <thread> <text> reply in a thread
47
+ bikeshed suggest <id> <text> --with <new text>
48
+ propose new text for a passage, for a
49
+ person to accept or reject (--delete to
50
+ propose removing it)
45
51
  bikeshed open <id> open it in a browser
46
52
 
47
53
  Where a command takes <id> it also takes the link you were sent. An unlisted
@@ -72,13 +78,19 @@ What you can publish
72
78
 
73
79
  Options
74
80
  --json machine-readable output
75
- --title <text> title for publish
81
+ --title <text> title for publish or fork (fork default: the original's)
76
82
  --id <id> with publish: the address, /d/<id> (default: from the title)
77
- --folder <id> destination folder for publish
83
+ --folder <id> destination folder for publish or fork
84
+ --version <v> with fork: the version to copy (default: current)
78
85
  --parent <id> parent folder for mkdir
79
86
  --limit <n> with list: rows per page (default 20, max 50)
80
87
  --cursor <id> with list: the page after the one that printed it
81
88
  --all with list: every page; with comments: resolved too
89
+ --with <text> with suggest: what the passage should say instead
90
+ --delete with suggest: propose deleting the passage
91
+ --note <text> with suggest: why, for whoever decides
92
+ --prefix <text> with suggest: words just before a passage that repeats
93
+ --suffix <text> with suggest: words just after it
82
94
  --watch with publish/update: republish on every save
83
95
  --origin <url> the deployment (default: the one you logged in to)
84
96
  --token <token> override the stored token
@@ -188,7 +200,8 @@ async function list(flags) {
188
200
  }
189
201
  for (const doc of result.documents) {
190
202
  const marker = doc.visibility === "private" ? " " : doc.visibility === "public" ? "◉" : "◐";
191
- console.log(`${marker} ${doc.id.padEnd(34)} ${doc.version.padEnd(5)} ${doc.title}`);
203
+ // ⑂ marks a fork; where it came from is `bikeshed lineage`'s to say.
204
+ console.log(`${marker} ${doc.id.padEnd(34)} ${doc.version.padEnd(5)} ${doc.title}${doc.forked ? " ⑂" : ""}`);
192
205
  }
193
206
  if (result.nextCursor) console.log(`\n… more: --cursor ${result.nextCursor}`);
194
207
  });
@@ -299,6 +312,12 @@ async function comments(flags, [ref]) {
299
312
  // which passage to change without opening the document.
300
313
  console.log(`\n── ${thread.threadId}${thread.section ? ` (${thread.section})` : ""}`);
301
314
  if (thread.quote) console.log(` “${thread.quote}”`);
315
+ // A suggested edit says what the passage would become, and whether a
316
+ // person has decided it yet (SPEC §22).
317
+ if (thread.suggestion) {
318
+ const change = thread.suggestion.replacement === "" ? "(delete it)" : `“${thread.suggestion.replacement}”`;
319
+ console.log(` → ${change} [${thread.suggestion.state}]`);
320
+ }
302
321
  for (const comment of thread.comments) {
303
322
  const via = comment.via && comment.via !== "web" ? ` [via ${comment.via}]` : "";
304
323
  console.log(` ${comment.author}${via}: ${comment.body}`);
@@ -320,6 +339,34 @@ async function reply(flags, [ref, threadId, ...rest]) {
320
339
  out(flags.json, result, () => console.log(`replied in ${threadId}`));
321
340
  }
322
341
 
342
+ /**
343
+ * Propose new text for one passage of a document's current version. The
344
+ * passage is quoted as a reader sees it, which is how `comments` prints it, so
345
+ * a quote can go straight from one command to the other.
346
+ */
347
+ async function suggest(flags, [ref, ...rest]) {
348
+ const exact = rest.join(" ");
349
+ const usage = "usage: bikeshed suggest <id> <text> (--with <new text> | --delete) [--note <why>]";
350
+ if (!ref || !exact) fail(usage);
351
+ const replacing = typeof flags.with === "string";
352
+ if (replacing === Boolean(flags.delete)) fail(usage);
353
+ // An empty --with is what an unset shell variable sends; deleting a passage
354
+ // has to be asked for by name.
355
+ if (replacing && flags.with === "") fail("an empty --with would delete the passage; say --delete if you mean that");
356
+ const { origin, token } = resolveAuth(flags);
357
+ const result = await callTool(origin, token, "suggest_edit", {
358
+ ...docArgs(ref),
359
+ exact,
360
+ replacement: replacing ? flags.with : "",
361
+ ...(typeof flags.note === "string" ? { note: flags.note } : {}),
362
+ ...(typeof flags.prefix === "string" ? { prefix: flags.prefix } : {}),
363
+ ...(typeof flags.suffix === "string" ? { suffix: flags.suffix } : {}),
364
+ });
365
+ out(flags.json, result, () =>
366
+ console.log(`suggested in ${result.threadId}; a person who can approve will accept or reject it`),
367
+ );
368
+ }
369
+
323
370
  async function folders(flags) {
324
371
  const { origin, token } = resolveAuth(flags);
325
372
  const result = await callTool(origin, token, "list_folders", {});
@@ -343,6 +390,71 @@ async function folders(flags) {
343
390
  });
344
391
  }
345
392
 
393
+ /**
394
+ * Your own copy of a document you can read. The original's author sees that it
395
+ * was forked — a count, never who — and the fork records where it came from,
396
+ * which only people who can open the original are shown (SPEC §21).
397
+ */
398
+ async function fork(flags, [ref]) {
399
+ if (!ref) fail("usage: bikeshed fork <id> [--version <v>] [--folder <id>] [--title <text>]");
400
+ const { origin, token } = resolveAuth(flags);
401
+ const args = docArgs(ref);
402
+ if (typeof flags.version === "string") args.version = flags.version;
403
+ if (typeof flags.folder === "string") args.folder = flags.folder;
404
+ if (typeof flags.title === "string") args.title = flags.title;
405
+ const result = await callTool(origin, token, "fork_document", args);
406
+ out(flags.json, { ...result, url: result.url ?? `${origin}/d/${result.docId}` }, () =>
407
+ console.log(`forked ${args.id} into ${result.docId} — ${result.url ?? `${origin}/d/${result.docId}`}`),
408
+ );
409
+ }
410
+
411
+ /**
412
+ * Where a document came from, nearest first, as far as you can open, and — for
413
+ * its owner — what has been forked from it. The deployment decides what you may
414
+ * see; this only prints it (SPEC §21).
415
+ */
416
+ async function lineage(flags, [ref]) {
417
+ if (!ref) fail("usage: bikeshed lineage <id>");
418
+ const { origin, token } = resolveAuth(flags);
419
+ const result = await callTool(origin, token, "get_lineage", docArgs(ref));
420
+ out(flags.json, result, () => console.log(describeLineage(result)));
421
+ }
422
+
423
+ /** The human form of a get_lineage result. */
424
+ function describeLineage(result) {
425
+ const lines = [];
426
+ if (result.forked) {
427
+ if (result.forkedFrom.length === 0) {
428
+ lines.push("forked from a document you can't open");
429
+ }
430
+ result.forkedFrom.forEach((a, i) => {
431
+ const changed = a.changedSince ? " (changed since)" : "";
432
+ lines.push(`${i === 0 ? "forked from" : " which was"} ${a.id} ${a.version} ${a.title}${changed}`);
433
+ });
434
+ if (result.forkedFrom.length > 0 && result.moreYouCannotOpen) {
435
+ lines.push(" which was forked from a document you can't open");
436
+ }
437
+ } else if (!result.forks) {
438
+ lines.push(`${result.id} is not a fork.`);
439
+ }
440
+ if (result.forks) {
441
+ const { total, youCannotOpen, listed } = result.forks;
442
+ if (lines.length) lines.push("");
443
+ if (total === 0) {
444
+ lines.push("Nobody has forked it yet.");
445
+ } else {
446
+ const times = total === 1 ? "once" : `${total} times`;
447
+ lines.push(youCannotOpen ? `forked ${times}; ${youCannotOpen} you can't open` : `forked ${times}`);
448
+ for (const f of listed) {
449
+ const when = f.forkedAt ? f.forkedAt.slice(0, 10) : "";
450
+ const owner = f.owner ? ` ${f.owner}` : "";
451
+ lines.push(` ${f.id.padEnd(34)} ${f.visibility.padEnd(8)} ${when.padEnd(10)} ${f.title}${owner}`);
452
+ }
453
+ }
454
+ }
455
+ return lines.join("\n");
456
+ }
457
+
346
458
  async function mkdir(flags, rest) {
347
459
  const name = rest.join(" ");
348
460
  if (!name) fail("usage: bikeshed mkdir <name> [--parent <id>]");
@@ -454,9 +566,12 @@ const COMMANDS = {
454
566
  list,
455
567
  publish,
456
568
  update,
569
+ fork,
570
+ lineage,
457
571
  get,
458
572
  comments,
459
573
  reply,
574
+ suggest,
460
575
  open,
461
576
  folders,
462
577
  mkdir,
@@ -480,11 +595,17 @@ async function main() {
480
595
  title: { type: "string" },
481
596
  id: { type: "string" },
482
597
  folder: { type: "string" },
598
+ version: { type: "string" },
483
599
  parent: { type: "string" },
484
600
  origin: { type: "string" },
485
601
  token: { type: "string" },
486
602
  limit: { type: "string" },
487
603
  cursor: { type: "string" },
604
+ with: { type: "string" },
605
+ delete: { type: "boolean", default: false },
606
+ note: { type: "string" },
607
+ prefix: { type: "string" },
608
+ suffix: { type: "string" },
488
609
  },
489
610
  });
490
611