bikeshed-now 0.16.0 → 0.17.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 +29 -0
- package/package.json +1 -1
- package/src/index.mjs +76 -36
package/README.md
CHANGED
|
@@ -166,3 +166,32 @@ reviewer can always tell which feedback came from a person.
|
|
|
166
166
|
|
|
167
167
|
Apache-2.0. The deployment it talks to is the same repository:
|
|
168
168
|
[DeputyHumans/bikeshed-now](https://github.com/DeputyHumans/bikeshed-now).
|
|
169
|
+
|
|
170
|
+
### Previous versions and write guards
|
|
171
|
+
|
|
172
|
+
Use `list_versions({id, limit:20, cursor})` or `bikeshed versions <id|link>
|
|
173
|
+
--all --json` to discover published history. Pages are newest first, default 20,
|
|
174
|
+
maximum 50; pass `nextCursor` as `cursor` until null. Each row has a pinned URL.
|
|
175
|
+
History uses the document's current access rules and expiry. A missing or
|
|
176
|
+
withdrawn version is an error, never a request for the latest version.
|
|
177
|
+
|
|
178
|
+
Read a chosen version with `get_document({id, version:"v2"})` and
|
|
179
|
+
`get_comments({id, version:"v2", includeResolved:true})`, or
|
|
180
|
+
`bikeshed get <id> --version v2` and `bikeshed comments <id> --version v2 --all`.
|
|
181
|
+
Read results name `currentVersion` and `isCurrent`; historical write capabilities
|
|
182
|
+
are false. Existing comments and suggestions remain readable as an archive.
|
|
183
|
+
Older versions cannot be edited or receive, revise, delete, reply to, resolve,
|
|
184
|
+
reopen, accept, reject, claim or withdraw feedback. Fork one to make a new,
|
|
185
|
+
independent document with `fork_document({id, version:"v2"})`.
|
|
186
|
+
|
|
187
|
+
CLI `get`, `comments`, `fork`, and `open` preserve a version in a pasted
|
|
188
|
+
`/d/<id>/<version>?k=...` URL. Feedback commands also preserve it as a write
|
|
189
|
+
guard. `--version` must agree with the URL; conflicts fail. Keys are retained.
|
|
190
|
+
Document-wide sharing, folder and lineage operations still address the document.
|
|
191
|
+
|
|
192
|
+
Before changing feedback, read the latest version and its feedback, then pass
|
|
193
|
+
that `version` to every feedback tool (`--version` in the CLI). Pass
|
|
194
|
+
`baseVersion` to `update_document` and `commit_draft` (`--base` on `bikeshed
|
|
195
|
+
update` and `bikeshed commit`). A versioned URL supplies the CLI base too.
|
|
196
|
+
If the head changes, the server refuses the write. Read the new head and
|
|
197
|
+
reassess the request; never silently redirect a historical request to it.
|
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -51,7 +51,8 @@ const USAGE = `bikeshed ${VERSION} — publish and revise documents
|
|
|
51
51
|
bikeshed update <id> <file> publish a new version
|
|
52
52
|
bikeshed fork <id> your own copy, which remembers its original
|
|
53
53
|
bikeshed lineage <id> what it was forked from; for its owner, its forks
|
|
54
|
-
bikeshed get <id>
|
|
54
|
+
bikeshed get <id> [--version <v>] print a published version's source
|
|
55
|
+
bikeshed versions <id> list published history (--all for every page)
|
|
55
56
|
bikeshed comments <id> unresolved feedback, with its anchored text
|
|
56
57
|
(--version <v> for an earlier version's)
|
|
57
58
|
bikeshed changes <id> --since <cursor|last|all>
|
|
@@ -122,9 +123,9 @@ Options
|
|
|
122
123
|
--title <text> title for publish or fork (fork default: the original's)
|
|
123
124
|
--id <id> with publish: the address, /d/<id> (default: from the title)
|
|
124
125
|
--folder <id> destination folder for publish or fork
|
|
125
|
-
--version <v>
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
--version <v> get/comments/fork/open: the published version to read
|
|
127
|
+
or copy; feedback commands: require that version to
|
|
128
|
+
still be current. On its own: the CLI version
|
|
128
129
|
--parent <id> parent folder for mkdir
|
|
129
130
|
--limit <n> with list: rows per page (default 20, max 50)
|
|
130
131
|
--cursor <id> with list: the page after the one that printed it
|
|
@@ -136,7 +137,7 @@ Options
|
|
|
136
137
|
60, at least 30)
|
|
137
138
|
--timeout <s> with changes --wait: give up after this long (default
|
|
138
139
|
3600), exit 3, and print the cursor to resume from
|
|
139
|
-
--base <v> with update: the version you revised; refused if
|
|
140
|
+
--base <v> with update/commit: the version you revised; refused if
|
|
140
141
|
somebody has published since
|
|
141
142
|
--all with list: every page; with comments: resolved too
|
|
142
143
|
--with <text> with suggest or revise: what the passage should say instead
|
|
@@ -175,18 +176,31 @@ function out(json, value, human) {
|
|
|
175
176
|
* for both — `…/d/<id>[/<version>]?k=<key>` — and the key goes along as the
|
|
176
177
|
* tools' `k` argument (SPEC §17).
|
|
177
178
|
*/
|
|
178
|
-
function docArgs(ref) {
|
|
179
|
-
|
|
180
|
-
let
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
fail(`not a document link: ${ref}`);
|
|
179
|
+
function docArgs(ref, flags) {
|
|
180
|
+
let id = ref;
|
|
181
|
+
let k;
|
|
182
|
+
let linkedVersion;
|
|
183
|
+
if (/^https?:\/\//i.test(ref)) {
|
|
184
|
+
let url;
|
|
185
|
+
try { url = new URL(ref); } catch { fail(`not a document link: ${ref}`); }
|
|
186
|
+
const match = url.pathname.match(/^\/d\/([^/]+)(?:\/([^/]+))?\/?$/);
|
|
187
|
+
if (!match) fail(`not a document link: ${ref}`);
|
|
188
|
+
id = decodeURIComponent(match[1]);
|
|
189
|
+
linkedVersion = match[2] ? decodeURIComponent(match[2]) : undefined;
|
|
190
|
+
k = url.searchParams.get("k");
|
|
191
|
+
}
|
|
192
|
+
// Document-wide operations deliberately use only the document id/key.
|
|
193
|
+
if (flags === undefined) return { id, ...(k ? { k } : {}) };
|
|
194
|
+
const explicit = flags.version;
|
|
195
|
+
if (explicit !== undefined && (typeof explicit !== "string" || !explicit || /[\/\\?#\s]/.test(explicit))) {
|
|
196
|
+
fail("invalid version");
|
|
185
197
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
if (explicit && linkedVersion && explicit !== linkedVersion) {
|
|
199
|
+
fail(`conflicting versions: the link names ${linkedVersion}, the flag names ${explicit}`);
|
|
200
|
+
}
|
|
201
|
+
const version = explicit ?? linkedVersion;
|
|
202
|
+
if (version && (version === "." || version === ".." || /[\/\\?#\s]/.test(version))) fail("invalid version");
|
|
203
|
+
return { id, ...(k ? { k } : {}), ...(version ? { version } : {}) };
|
|
190
204
|
}
|
|
191
205
|
|
|
192
206
|
const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
|
|
@@ -496,7 +510,7 @@ async function update(flags, [ref, path]) {
|
|
|
496
510
|
if (!ref || !path) fail("usage: bikeshed update <id> <file>");
|
|
497
511
|
const { origin, token } = resolveAuth(flags);
|
|
498
512
|
const file = resolve(path);
|
|
499
|
-
const doc = docArgs(ref);
|
|
513
|
+
const { version: selectedBase, ...doc } = docArgs(ref, { version: flags.base });
|
|
500
514
|
const { id } = doc;
|
|
501
515
|
|
|
502
516
|
const once = async () => {
|
|
@@ -505,7 +519,7 @@ async function update(flags, [ref, path]) {
|
|
|
505
519
|
// text, every byte above 0x7f in it would become U+FFFD on the way.
|
|
506
520
|
// --base: the version this was revised from, so an update over somebody's
|
|
507
521
|
// newer save is refused instead of replacing it (SPEC §19).
|
|
508
|
-
const base =
|
|
522
|
+
const base = selectedBase;
|
|
509
523
|
const result = isArchive(file, bytes)
|
|
510
524
|
? await uploadFile(origin, token, file, { update: id, k: doc.k, base })
|
|
511
525
|
: await callTool(origin, token, "update_document", {
|
|
@@ -527,10 +541,30 @@ async function update(flags, [ref, path]) {
|
|
|
527
541
|
if (flags.watch) await watchFile(file, once);
|
|
528
542
|
}
|
|
529
543
|
|
|
544
|
+
/** The published history, paged by the deployment rather than guessed from ids. */
|
|
545
|
+
async function versions(flags, [ref, ...rest]) {
|
|
546
|
+
if (!ref || rest.length) fail("usage: bikeshed versions <id> [--limit N] [--cursor …] [--all] [--json]");
|
|
547
|
+
const { origin, token } = resolveAuth(flags);
|
|
548
|
+
const limit = flags.limit === undefined ? 20 : Number(flags.limit);
|
|
549
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 50) fail("--limit must be an integer from 1 to 50");
|
|
550
|
+
let cursor = flags.cursor;
|
|
551
|
+
const rows = [];
|
|
552
|
+
let result;
|
|
553
|
+
do {
|
|
554
|
+
result = await callTool(origin, token, "list_versions", { ...docArgs(ref), limit, ...(cursor ? { cursor } : {}) });
|
|
555
|
+
rows.push(...result.versions);
|
|
556
|
+
cursor = result.nextCursor;
|
|
557
|
+
} while (flags.all && cursor);
|
|
558
|
+
out(flags.json, { ...result, versions: rows }, () => {
|
|
559
|
+
for (const row of rows) console.log(`${row.isCurrent ? "*" : " "} ${row.version} ${row.publishedAt ?? ""} ${row.label} ${row.url ?? ""}`);
|
|
560
|
+
if (cursor) console.log(`… more: bikeshed versions ${ref} --cursor ${cursor}`);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
530
564
|
async function get(flags, [ref]) {
|
|
531
565
|
if (!ref) fail("usage: bikeshed get <id>");
|
|
532
566
|
const { origin, token } = resolveAuth(flags);
|
|
533
|
-
const doc = docArgs(ref);
|
|
567
|
+
const doc = docArgs(ref, flags);
|
|
534
568
|
const result = await callTool(origin, token, "get_document", doc);
|
|
535
569
|
// The tool cuts a long source at a fixed length. Printed as it came, the cut
|
|
536
570
|
// copy exited 0 and looked complete, and `update` with an edit of it
|
|
@@ -555,9 +589,7 @@ async function comments(flags, [ref]) {
|
|
|
555
589
|
if (!ref) fail("usage: bikeshed comments <id>");
|
|
556
590
|
const { origin, token } = resolveAuth(flags);
|
|
557
591
|
const result = await callTool(origin, token, "get_comments", {
|
|
558
|
-
...docArgs(ref),
|
|
559
|
-
// Threads belong to the version they were opened on; a version in a link
|
|
560
|
-
// is not taken as a request for one, only the flag is.
|
|
592
|
+
...docArgs(ref, flags),
|
|
561
593
|
...(typeof flags.version === "string" ? { version: flags.version } : {}),
|
|
562
594
|
includeResolved: Boolean(flags.all),
|
|
563
595
|
});
|
|
@@ -603,7 +635,7 @@ async function comment(flags, [ref, ...rest]) {
|
|
|
603
635
|
if (typeof flags.body !== "string" || !flags.body.trim()) fail(usage);
|
|
604
636
|
const { origin, token } = resolveAuth(flags);
|
|
605
637
|
const result = await callTool(origin, token, "add_comment", {
|
|
606
|
-
...docArgs(ref),
|
|
638
|
+
...docArgs(ref, flags),
|
|
607
639
|
exact,
|
|
608
640
|
body: flags.body,
|
|
609
641
|
...(typeof flags.prefix === "string" ? { prefix: flags.prefix } : {}),
|
|
@@ -617,7 +649,7 @@ async function reply(flags, [ref, threadId, ...rest]) {
|
|
|
617
649
|
if (!ref || !threadId || !body) fail("usage: bikeshed reply <id> <thread> <text>");
|
|
618
650
|
const { origin, token } = resolveAuth(flags);
|
|
619
651
|
const result = await callTool(origin, token, "reply_to_comment", {
|
|
620
|
-
...docArgs(ref),
|
|
652
|
+
...docArgs(ref, flags),
|
|
621
653
|
threadId,
|
|
622
654
|
body,
|
|
623
655
|
});
|
|
@@ -641,7 +673,7 @@ async function reopenThread(flags, args) {
|
|
|
641
673
|
async function setResolved(flags, [ref, threadId, ...rest], verb) {
|
|
642
674
|
if (!ref || !threadId || rest.length) fail(`usage: bikeshed ${verb} <id> <thread>`);
|
|
643
675
|
const { origin, token } = resolveAuth(flags);
|
|
644
|
-
const result = await callTool(origin, token, `${verb}_thread`, { ...docArgs(ref), threadId });
|
|
676
|
+
const result = await callTool(origin, token, `${verb}_thread`, { ...docArgs(ref, flags), threadId });
|
|
645
677
|
out(flags.json, result, () => console.log(`${verb === "resolve" ? "resolved" : "reopened"} ${threadId}`));
|
|
646
678
|
}
|
|
647
679
|
|
|
@@ -657,7 +689,7 @@ async function suggest(flags, [ref, ...rest]) {
|
|
|
657
689
|
const replacement = replacementFrom(flags, usage);
|
|
658
690
|
const { origin, token } = resolveAuth(flags);
|
|
659
691
|
const result = await callTool(origin, token, "suggest_edit", {
|
|
660
|
-
...docArgs(ref),
|
|
692
|
+
...docArgs(ref, flags),
|
|
661
693
|
exact,
|
|
662
694
|
replacement,
|
|
663
695
|
...(typeof flags.note === "string" ? { note: flags.note } : {}),
|
|
@@ -689,7 +721,7 @@ async function revise(flags, [ref, threadId, ...rest]) {
|
|
|
689
721
|
const replacement = replacementFrom(flags, usage);
|
|
690
722
|
const { origin, token } = resolveAuth(flags);
|
|
691
723
|
const result = await callTool(origin, token, "revise_suggestion", {
|
|
692
|
-
...docArgs(ref),
|
|
724
|
+
...docArgs(ref, flags),
|
|
693
725
|
threadId,
|
|
694
726
|
replacement,
|
|
695
727
|
...(typeof flags.note === "string" ? { note: flags.note } : {}),
|
|
@@ -705,8 +737,9 @@ async function revise(flags, [ref, threadId, ...rest]) {
|
|
|
705
737
|
async function accept(flags, [ref, threadId, ...rest]) {
|
|
706
738
|
if (!ref || !threadId || rest.length) fail("usage: bikeshed accept <id> <thread>");
|
|
707
739
|
const { origin, token } = resolveAuth(flags);
|
|
708
|
-
|
|
709
|
-
|
|
740
|
+
const { id, version } = docArgs(ref, flags);
|
|
741
|
+
// Deciding still takes a real role; a version is only a precondition.
|
|
742
|
+
const result = await callTool(origin, token, "accept_suggestion", { id, ...(version ? { version } : {}), threadId });
|
|
710
743
|
out(flags.json, result, () => console.log(`accepted ${threadId}; the change is in the draft — \`bikeshed commit\` publishes it`));
|
|
711
744
|
}
|
|
712
745
|
|
|
@@ -714,7 +747,8 @@ async function accept(flags, [ref, threadId, ...rest]) {
|
|
|
714
747
|
async function commit(flags, [ref, ...rest]) {
|
|
715
748
|
if (!ref || rest.length) fail("usage: bikeshed commit <id>");
|
|
716
749
|
const { origin, token } = resolveAuth(flags);
|
|
717
|
-
const
|
|
750
|
+
const { id, version } = docArgs(ref, { version: flags.base });
|
|
751
|
+
const result = await callTool(origin, token, "commit_draft", { id, ...(version ? { baseVersion: version } : {}) });
|
|
718
752
|
out(flags.json, result, () =>
|
|
719
753
|
console.log(result.deduped ? `nothing new: the draft is ${result.versionId} already` : `published ${result.versionId}`),
|
|
720
754
|
);
|
|
@@ -728,9 +762,10 @@ async function commit(flags, [ref, ...rest]) {
|
|
|
728
762
|
async function reject(flags, [ref, threadId, ...rest]) {
|
|
729
763
|
if (!ref || !threadId || rest.length) fail("usage: bikeshed reject <id> <thread> [--reason <why>]");
|
|
730
764
|
const { origin, token } = resolveAuth(flags);
|
|
731
|
-
|
|
765
|
+
const { id, version } = docArgs(ref, flags);
|
|
766
|
+
// Deciding still takes a real role; a version is only a precondition.
|
|
732
767
|
const result = await callTool(origin, token, "reject_suggestion", {
|
|
733
|
-
id:
|
|
768
|
+
id, ...(version ? { version } : {}),
|
|
734
769
|
threadId,
|
|
735
770
|
...(typeof flags.reason === "string" ? { reason: flags.reason } : {}),
|
|
736
771
|
});
|
|
@@ -741,7 +776,7 @@ async function reject(flags, [ref, threadId, ...rest]) {
|
|
|
741
776
|
async function withdraw(flags, [ref, threadId, ...rest]) {
|
|
742
777
|
if (!ref || !threadId || rest.length) fail("usage: bikeshed withdraw <id> <thread>");
|
|
743
778
|
const { origin, token } = resolveAuth(flags);
|
|
744
|
-
const result = await callTool(origin, token, "withdraw_suggestion", { ...docArgs(ref), threadId });
|
|
779
|
+
const result = await callTool(origin, token, "withdraw_suggestion", { ...docArgs(ref, flags), threadId });
|
|
745
780
|
out(flags.json, result, () => console.log(`withdrew ${threadId}`));
|
|
746
781
|
}
|
|
747
782
|
|
|
@@ -942,7 +977,7 @@ function printChanges(flags, result) {
|
|
|
942
977
|
async function fork(flags, [ref]) {
|
|
943
978
|
if (!ref) fail("usage: bikeshed fork <id> [--version <v>] [--folder <id>] [--title <text>]");
|
|
944
979
|
const { origin, token } = resolveAuth(flags);
|
|
945
|
-
const args = docArgs(ref);
|
|
980
|
+
const args = docArgs(ref, flags);
|
|
946
981
|
if (typeof flags.version === "string") args.version = flags.version;
|
|
947
982
|
if (typeof flags.folder === "string") args.folder = flags.folder;
|
|
948
983
|
if (typeof flags.title === "string") args.title = flags.title;
|
|
@@ -1094,8 +1129,8 @@ async function open(flags, [ref]) {
|
|
|
1094
1129
|
const { origin } = resolveAuth(flags);
|
|
1095
1130
|
// An unlisted document opens only with its key, and the link is the one place
|
|
1096
1131
|
// the key lives, so a link keeps it.
|
|
1097
|
-
const { id, k } = docArgs(ref);
|
|
1098
|
-
const url = `${origin}/d/${encodeURIComponent(id)}${k ? `?k=${encodeURIComponent(k)}` : ""}`;
|
|
1132
|
+
const { id, k, version } = docArgs(ref, flags);
|
|
1133
|
+
const url = `${origin}/d/${encodeURIComponent(id)}${version ? `/${encodeURIComponent(version)}` : ""}${k ? `?k=${encodeURIComponent(k)}` : ""}`;
|
|
1099
1134
|
// Best effort, and the address is printed either way. This used to spawn
|
|
1100
1135
|
// `start`, which is a cmd.exe builtin rather than a program, so on Windows
|
|
1101
1136
|
// the spawn failed and, with nothing listening for the error, crashed.
|
|
@@ -1134,6 +1169,7 @@ const COMMANDS = {
|
|
|
1134
1169
|
fork,
|
|
1135
1170
|
lineage,
|
|
1136
1171
|
get,
|
|
1172
|
+
versions,
|
|
1137
1173
|
comments,
|
|
1138
1174
|
changes,
|
|
1139
1175
|
comment,
|
|
@@ -1218,6 +1254,10 @@ async function main() {
|
|
|
1218
1254
|
return;
|
|
1219
1255
|
}
|
|
1220
1256
|
|
|
1257
|
+
const versionCommands = new Set(["get", "comments", "fork", "open", "comment", "reply", "resolve", "reopen", "suggest", "revise", "withdraw", "accept", "reject"]);
|
|
1258
|
+
if (values.version !== undefined && !versionCommands.has(name)) fail(`--version is not supported by ${name}; update and commit use --base`);
|
|
1259
|
+
if (values.base !== undefined && !["update", "commit"].includes(name)) fail(`--base is not supported by ${name}`);
|
|
1260
|
+
|
|
1221
1261
|
const command = COMMANDS[name];
|
|
1222
1262
|
if (!command) fail(`unknown command: ${name}\n\n${USAGE}`);
|
|
1223
1263
|
|