bikeshed-now 0.15.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.
Files changed (3) hide show
  1. package/README.md +49 -4
  2. package/package.json +1 -1
  3. package/src/index.mjs +171 -36
package/README.md CHANGED
@@ -131,10 +131,26 @@ nearest has changed since you forked it. On a document you own it also counts
131
131
  its forks. The author of what you fork sees that it was forked — a count, never
132
132
  who.
133
133
 
134
- ## Sharing is not automated
135
-
136
- There is no command that shares a document, changes who can read it, or sets an
137
- expiry. Those are decisions a person makes in the interface, deliberately.
134
+ ## Manage access and folders
135
+
136
+ `bikeshed access document <id>` and `bikeshed access folder <id>` show the
137
+ owner, direct grants, inherited grants and effective membership. Add or change
138
+ a direct role with `--add email@example.com,team:design --role editor`; revoke
139
+ with `--remove email@example.com`. All four roles are supported. The owner
140
+ cannot be removed, and inherited grants must be changed at their source.
141
+ Newly effective readers receive an invitation when email is enabled.
142
+
143
+ `bikeshed visibility <id>` shows settings. Add `private`, `unlisted` or `public`
144
+ to change visibility, `--public-edit true|false` to change open editing, or
145
+ `--expires <ISO-8601 timestamp with timezone|none>` to set or clear expiry.
146
+ Omitted settings stay unchanged. These commands are owner-only, including on
147
+ expired documents; the returned URL includes an unlisted key when needed.
148
+
149
+ `bikeshed rename-folder <id> <name>` and
150
+ `bikeshed move-folder <id> <folder|root>` require edit rights.
151
+ `bikeshed delete-folder <id>` is owner-only: contents move to its parent or the
152
+ root, and access is recomputed. No documents are deleted. All commands support
153
+ `--json`. Existing sign-ins work without reconnecting.
138
154
 
139
155
  ## For agents
140
156
 
@@ -150,3 +166,32 @@ reviewer can always tell which feedback came from a person.
150
166
 
151
167
  Apache-2.0. The deployment it talks to is the same repository:
152
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bikeshed-now",
3
- "version": "0.15.0",
3
+ "version": "0.17.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/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> print a document's source
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>
@@ -88,6 +89,17 @@ Organising
88
89
  bikeshed mv <id> <folder|root> move a document
89
90
  bikeshed rename <id> <title> change a document's title; its address stays
90
91
 
92
+ Management
93
+ bikeshed access <document|folder> <id> inspect direct and inherited permissions
94
+ --add <principals> --role <role> grant/change roles (emails or team:<id>, comma-separated)
95
+ --remove <principals> revoke direct grants; inherited access remains
96
+ bikeshed visibility <id> [private|unlisted|public]
97
+ --public-edit true|false change open editing
98
+ --expires <ISO timestamp|none> set or clear expiry; omitted settings stay unchanged
99
+ bikeshed rename-folder <id> <name> rename a folder you can edit
100
+ bikeshed move-folder <id> <folder|root> move a folder and recompute inherited access
101
+ bikeshed delete-folder <id> owner-only; reparent contents, retain documents
102
+
91
103
  Skills
92
104
  bikeshed skills skills this deployment publishes
93
105
  bikeshed skills install <dir> write them into an agent's skills directory
@@ -111,9 +123,9 @@ Options
111
123
  --title <text> title for publish or fork (fork default: the original's)
112
124
  --id <id> with publish: the address, /d/<id> (default: from the title)
113
125
  --folder <id> destination folder for publish or fork
114
- --version <v> with fork: the version to copy; with comments: the
115
- version to read (default: current); on its own,
116
- which version of bikeshed this is
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
117
129
  --parent <id> parent folder for mkdir
118
130
  --limit <n> with list: rows per page (default 20, max 50)
119
131
  --cursor <id> with list: the page after the one that printed it
@@ -125,7 +137,7 @@ Options
125
137
  60, at least 30)
126
138
  --timeout <s> with changes --wait: give up after this long (default
127
139
  3600), exit 3, and print the cursor to resume from
128
- --base <v> with update: the version you revised; refused if
140
+ --base <v> with update/commit: the version you revised; refused if
129
141
  somebody has published since
130
142
  --all with list: every page; with comments: resolved too
131
143
  --with <text> with suggest or revise: what the passage should say instead
@@ -164,18 +176,31 @@ function out(json, value, human) {
164
176
  * for both — `…/d/<id>[/<version>]?k=<key>` — and the key goes along as the
165
177
  * tools' `k` argument (SPEC §17).
166
178
  */
167
- function docArgs(ref) {
168
- if (!/^https?:\/\//i.test(ref)) return { id: ref };
169
- let url;
170
- try {
171
- url = new URL(ref);
172
- } catch {
173
- 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");
197
+ }
198
+ if (explicit && linkedVersion && explicit !== linkedVersion) {
199
+ fail(`conflicting versions: the link names ${linkedVersion}, the flag names ${explicit}`);
174
200
  }
175
- const match = url.pathname.match(/^\/d\/([^/]+)(?:\/[^/]+)?\/?$/);
176
- if (!match) fail(`not a document link: ${ref}`);
177
- const k = url.searchParams.get("k");
178
- return { id: decodeURIComponent(match[1]), ...(k ? { k } : {}) };
201
+ const version = explicit ?? linkedVersion;
202
+ if (version && (version === "." || version === ".." || /[\/\\?#\s]/.test(version))) fail("invalid version");
203
+ return { id, ...(k ? { k } : {}), ...(version ? { version } : {}) };
179
204
  }
180
205
 
181
206
  const ZIP_MAGIC = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
@@ -485,7 +510,7 @@ async function update(flags, [ref, path]) {
485
510
  if (!ref || !path) fail("usage: bikeshed update <id> <file>");
486
511
  const { origin, token } = resolveAuth(flags);
487
512
  const file = resolve(path);
488
- const doc = docArgs(ref);
513
+ const { version: selectedBase, ...doc } = docArgs(ref, { version: flags.base });
489
514
  const { id } = doc;
490
515
 
491
516
  const once = async () => {
@@ -494,7 +519,7 @@ async function update(flags, [ref, path]) {
494
519
  // text, every byte above 0x7f in it would become U+FFFD on the way.
495
520
  // --base: the version this was revised from, so an update over somebody's
496
521
  // newer save is refused instead of replacing it (SPEC §19).
497
- const base = typeof flags.base === "string" && flags.base ? flags.base : undefined;
522
+ const base = selectedBase;
498
523
  const result = isArchive(file, bytes)
499
524
  ? await uploadFile(origin, token, file, { update: id, k: doc.k, base })
500
525
  : await callTool(origin, token, "update_document", {
@@ -516,10 +541,30 @@ async function update(flags, [ref, path]) {
516
541
  if (flags.watch) await watchFile(file, once);
517
542
  }
518
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
+
519
564
  async function get(flags, [ref]) {
520
565
  if (!ref) fail("usage: bikeshed get <id>");
521
566
  const { origin, token } = resolveAuth(flags);
522
- const doc = docArgs(ref);
567
+ const doc = docArgs(ref, flags);
523
568
  const result = await callTool(origin, token, "get_document", doc);
524
569
  // The tool cuts a long source at a fixed length. Printed as it came, the cut
525
570
  // copy exited 0 and looked complete, and `update` with an edit of it
@@ -544,9 +589,7 @@ async function comments(flags, [ref]) {
544
589
  if (!ref) fail("usage: bikeshed comments <id>");
545
590
  const { origin, token } = resolveAuth(flags);
546
591
  const result = await callTool(origin, token, "get_comments", {
547
- ...docArgs(ref),
548
- // Threads belong to the version they were opened on; a version in a link
549
- // is not taken as a request for one, only the flag is.
592
+ ...docArgs(ref, flags),
550
593
  ...(typeof flags.version === "string" ? { version: flags.version } : {}),
551
594
  includeResolved: Boolean(flags.all),
552
595
  });
@@ -592,7 +635,7 @@ async function comment(flags, [ref, ...rest]) {
592
635
  if (typeof flags.body !== "string" || !flags.body.trim()) fail(usage);
593
636
  const { origin, token } = resolveAuth(flags);
594
637
  const result = await callTool(origin, token, "add_comment", {
595
- ...docArgs(ref),
638
+ ...docArgs(ref, flags),
596
639
  exact,
597
640
  body: flags.body,
598
641
  ...(typeof flags.prefix === "string" ? { prefix: flags.prefix } : {}),
@@ -606,7 +649,7 @@ async function reply(flags, [ref, threadId, ...rest]) {
606
649
  if (!ref || !threadId || !body) fail("usage: bikeshed reply <id> <thread> <text>");
607
650
  const { origin, token } = resolveAuth(flags);
608
651
  const result = await callTool(origin, token, "reply_to_comment", {
609
- ...docArgs(ref),
652
+ ...docArgs(ref, flags),
610
653
  threadId,
611
654
  body,
612
655
  });
@@ -630,7 +673,7 @@ async function reopenThread(flags, args) {
630
673
  async function setResolved(flags, [ref, threadId, ...rest], verb) {
631
674
  if (!ref || !threadId || rest.length) fail(`usage: bikeshed ${verb} <id> <thread>`);
632
675
  const { origin, token } = resolveAuth(flags);
633
- const result = await callTool(origin, token, `${verb}_thread`, { ...docArgs(ref), threadId });
676
+ const result = await callTool(origin, token, `${verb}_thread`, { ...docArgs(ref, flags), threadId });
634
677
  out(flags.json, result, () => console.log(`${verb === "resolve" ? "resolved" : "reopened"} ${threadId}`));
635
678
  }
636
679
 
@@ -646,7 +689,7 @@ async function suggest(flags, [ref, ...rest]) {
646
689
  const replacement = replacementFrom(flags, usage);
647
690
  const { origin, token } = resolveAuth(flags);
648
691
  const result = await callTool(origin, token, "suggest_edit", {
649
- ...docArgs(ref),
692
+ ...docArgs(ref, flags),
650
693
  exact,
651
694
  replacement,
652
695
  ...(typeof flags.note === "string" ? { note: flags.note } : {}),
@@ -678,7 +721,7 @@ async function revise(flags, [ref, threadId, ...rest]) {
678
721
  const replacement = replacementFrom(flags, usage);
679
722
  const { origin, token } = resolveAuth(flags);
680
723
  const result = await callTool(origin, token, "revise_suggestion", {
681
- ...docArgs(ref),
724
+ ...docArgs(ref, flags),
682
725
  threadId,
683
726
  replacement,
684
727
  ...(typeof flags.note === "string" ? { note: flags.note } : {}),
@@ -694,8 +737,9 @@ async function revise(flags, [ref, threadId, ...rest]) {
694
737
  async function accept(flags, [ref, threadId, ...rest]) {
695
738
  if (!ref || !threadId || rest.length) fail("usage: bikeshed accept <id> <thread>");
696
739
  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, "accept_suggestion", { id: docArgs(ref).id, threadId });
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 });
699
743
  out(flags.json, result, () => console.log(`accepted ${threadId}; the change is in the draft — \`bikeshed commit\` publishes it`));
700
744
  }
701
745
 
@@ -703,7 +747,8 @@ async function accept(flags, [ref, threadId, ...rest]) {
703
747
  async function commit(flags, [ref, ...rest]) {
704
748
  if (!ref || rest.length) fail("usage: bikeshed commit <id>");
705
749
  const { origin, token } = resolveAuth(flags);
706
- const result = await callTool(origin, token, "commit_draft", { id: docArgs(ref).id });
750
+ const { id, version } = docArgs(ref, { version: flags.base });
751
+ const result = await callTool(origin, token, "commit_draft", { id, ...(version ? { baseVersion: version } : {}) });
707
752
  out(flags.json, result, () =>
708
753
  console.log(result.deduped ? `nothing new: the draft is ${result.versionId} already` : `published ${result.versionId}`),
709
754
  );
@@ -717,9 +762,10 @@ async function commit(flags, [ref, ...rest]) {
717
762
  async function reject(flags, [ref, threadId, ...rest]) {
718
763
  if (!ref || !threadId || rest.length) fail("usage: bikeshed reject <id> <thread> [--reason <why>]");
719
764
  const { origin, token } = resolveAuth(flags);
720
- // Deciding takes a real role on the document, never a link, so only the id is sent.
765
+ const { id, version } = docArgs(ref, flags);
766
+ // Deciding still takes a real role; a version is only a precondition.
721
767
  const result = await callTool(origin, token, "reject_suggestion", {
722
- id: docArgs(ref).id,
768
+ id, ...(version ? { version } : {}),
723
769
  threadId,
724
770
  ...(typeof flags.reason === "string" ? { reason: flags.reason } : {}),
725
771
  });
@@ -730,7 +776,7 @@ async function reject(flags, [ref, threadId, ...rest]) {
730
776
  async function withdraw(flags, [ref, threadId, ...rest]) {
731
777
  if (!ref || !threadId || rest.length) fail("usage: bikeshed withdraw <id> <thread>");
732
778
  const { origin, token } = resolveAuth(flags);
733
- const result = await callTool(origin, token, "withdraw_suggestion", { ...docArgs(ref), threadId });
779
+ const result = await callTool(origin, token, "withdraw_suggestion", { ...docArgs(ref, flags), threadId });
734
780
  out(flags.json, result, () => console.log(`withdrew ${threadId}`));
735
781
  }
736
782
 
@@ -788,6 +834,80 @@ async function folder(flags, [id]) {
788
834
  });
789
835
  }
790
836
 
837
+ /** Complete owner-visible grants, including why a removed direct grant may still read. */
838
+ function printManagedAccess(result) {
839
+ console.log(`${result.kind} ${result.id} — owner: ${result.owner}`);
840
+ for (const [role, people] of Object.entries(result.direct)) console.log(`${role}: ${people.join(", ") || "none"}`);
841
+ for (const grant of result.inherited) console.log(`inherited ${grant.role}: ${grant.principal} via ${grant.path}`);
842
+ for (const [role, people] of Object.entries(result.effective)) console.log(`${role}: ${people.join(", ") || "none"}`);
843
+ if (result.visibility !== undefined) printVisibility(result);
844
+ }
845
+
846
+ function printVisibility(result) {
847
+ console.log(`visibility: ${result.visibility}; open editing: ${result.publicEdit ? "on" : "off"}; expires: ${result.expiresAt === null ? "never" : new Date(result.expiresAt).toISOString()}`);
848
+ console.log(result.url);
849
+ }
850
+
851
+ async function access(flags, [kind, ref, ...rest]) {
852
+ if (!["document", "folder"].includes(kind) || !ref || rest.length) fail("usage: bikeshed access <document|folder> <id> [--add <principals> --role <role>] [--remove <principals>]");
853
+ const args = { id: kind === "document" ? docArgs(ref).id : ref };
854
+ const list = raw => {
855
+ if (typeof raw !== "string" || !raw.trim()) fail("principal list must not be empty");
856
+ const values = raw.split(",").map(p => p.trim());
857
+ if (values.some(p => !p)) fail("principal list contains an empty entry");
858
+ return values;
859
+ };
860
+ if (flags.add !== undefined) {
861
+ if (!["viewer", "suggester", "editor", "approver"].includes(flags.role)) fail("--add requires --role viewer|suggester|editor|approver");
862
+ args.add = list(flags.add).map(principal => ({ principal, role: flags.role }));
863
+ } else if (flags.role !== undefined) fail("--role requires --add");
864
+ if (flags.remove !== undefined) args.remove = list(flags.remove);
865
+ const { origin, token } = resolveAuth(flags);
866
+ const result = await callTool(origin, token, `${args.add || args.remove ? "set" : "get"}_${kind}_access`, args);
867
+ out(flags.json, result, () => printManagedAccess(result));
868
+ }
869
+
870
+ async function visibility(flags, [ref, level, ...rest]) {
871
+ if (!ref || rest.length || (level !== undefined && !["private", "unlisted", "public"].includes(level))) fail("usage: bikeshed visibility <id> [private|unlisted|public] [--public-edit true|false] [--expires <ISO timestamp|none>]");
872
+ const args = { id: docArgs(ref).id };
873
+ if (level !== undefined) args.visibility = level;
874
+ if (flags["public-edit"] !== undefined) {
875
+ if (!["true", "false"].includes(flags["public-edit"])) fail("--public-edit must be true or false");
876
+ args.publicEdit = flags["public-edit"] === "true";
877
+ }
878
+ if (flags.expires !== undefined) {
879
+ if (flags.expires === "none") args.expiresAt = null;
880
+ else {
881
+ if (typeof flags.expires !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,3})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(flags.expires) || !Number.isFinite(Date.parse(flags.expires))) fail("--expires must be an ISO-8601 timestamp with a timezone, or none");
882
+ args.expiresAt = Date.parse(flags.expires);
883
+ }
884
+ }
885
+ const { origin, token } = resolveAuth(flags);
886
+ const result = await callTool(origin, token, Object.keys(args).length > 1 ? "set_document_visibility" : "get_document_access", args);
887
+ out(flags.json, result, () => printVisibility(result));
888
+ }
889
+
890
+ async function renameFolder(flags, [id, name, ...rest]) {
891
+ if (!id || !name || rest.length) fail("usage: bikeshed rename-folder <id> <name>");
892
+ const { origin, token } = resolveAuth(flags);
893
+ const result = await callTool(origin, token, "rename_folder", { id, name });
894
+ out(flags.json, result, () => console.log(`renamed ${id}: ${result.path}`));
895
+ }
896
+
897
+ async function moveFolder(flags, [id, parent, ...rest]) {
898
+ if (!id || !parent || rest.length) fail("usage: bikeshed move-folder <id> <folder|root>");
899
+ const { origin, token } = resolveAuth(flags);
900
+ const result = await callTool(origin, token, "move_folder", { id, ...(parent === "root" ? {} : { parent }) });
901
+ out(flags.json, result, () => console.log(`moved ${id}: ${result.path}; ${result.access.readers} can read it`));
902
+ }
903
+
904
+ async function deleteFolder(flags, [id, ...rest]) {
905
+ if (!id || rest.length) fail("usage: bikeshed delete-folder <id>");
906
+ const { origin, token } = resolveAuth(flags);
907
+ const result = await callTool(origin, token, "delete_folder", { id });
908
+ out(flags.json, result, () => console.log(`deleted ${id}; moved ${result.reparented.folders} folders and ${result.reparented.documents} documents to ${result.parent ?? "root"}`));
909
+ }
910
+
791
911
  /** Exit status when `changes --wait` gives up: distinct from 1, an error. */
792
912
  const WAIT_TIMED_OUT = 3;
793
913
  /** The shortest wait between checks: each one reads the log and the document. */
@@ -857,7 +977,7 @@ function printChanges(flags, result) {
857
977
  async function fork(flags, [ref]) {
858
978
  if (!ref) fail("usage: bikeshed fork <id> [--version <v>] [--folder <id>] [--title <text>]");
859
979
  const { origin, token } = resolveAuth(flags);
860
- const args = docArgs(ref);
980
+ const args = docArgs(ref, flags);
861
981
  if (typeof flags.version === "string") args.version = flags.version;
862
982
  if (typeof flags.folder === "string") args.folder = flags.folder;
863
983
  if (typeof flags.title === "string") args.title = flags.title;
@@ -1009,8 +1129,8 @@ async function open(flags, [ref]) {
1009
1129
  const { origin } = resolveAuth(flags);
1010
1130
  // An unlisted document opens only with its key, and the link is the one place
1011
1131
  // the key lives, so a link keeps it.
1012
- const { id, k } = docArgs(ref);
1013
- 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)}` : ""}`;
1014
1134
  // Best effort, and the address is printed either way. This used to spawn
1015
1135
  // `start`, which is a cmd.exe builtin rather than a program, so on Windows
1016
1136
  // the spawn failed and, with nothing listening for the error, crashed.
@@ -1049,6 +1169,7 @@ const COMMANDS = {
1049
1169
  fork,
1050
1170
  lineage,
1051
1171
  get,
1172
+ versions,
1052
1173
  comments,
1053
1174
  changes,
1054
1175
  comment,
@@ -1062,6 +1183,11 @@ const COMMANDS = {
1062
1183
  reject,
1063
1184
  commit,
1064
1185
  open,
1186
+ access,
1187
+ visibility,
1188
+ "rename-folder": renameFolder,
1189
+ "move-folder": moveFolder,
1190
+ "delete-folder": deleteFolder,
1065
1191
  folders,
1066
1192
  folder,
1067
1193
  mkdir,
@@ -1079,6 +1205,11 @@ async function main() {
1079
1205
  allowPositionals: true,
1080
1206
  strict: false,
1081
1207
  options: {
1208
+ add: { type: "string" },
1209
+ remove: { type: "string" },
1210
+ role: { type: "string" },
1211
+ "public-edit": { type: "string" },
1212
+ expires: { type: "string" },
1082
1213
  json: { type: "boolean", default: false },
1083
1214
  watch: { type: "boolean", default: false },
1084
1215
  all: { type: "boolean", default: false },
@@ -1123,6 +1254,10 @@ async function main() {
1123
1254
  return;
1124
1255
  }
1125
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
+
1126
1261
  const command = COMMANDS[name];
1127
1262
  if (!command) fail(`unknown command: ${name}\n\n${USAGE}`);
1128
1263