bikeshed-now 0.15.0 → 0.16.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 +20 -4
  2. package/package.json +1 -1
  3. package/src/index.mjs +95 -0
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bikeshed-now",
3
- "version": "0.15.0",
3
+ "version": "0.16.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
@@ -88,6 +88,17 @@ Organising
88
88
  bikeshed mv <id> <folder|root> move a document
89
89
  bikeshed rename <id> <title> change a document's title; its address stays
90
90
 
91
+ Management
92
+ bikeshed access <document|folder> <id> inspect direct and inherited permissions
93
+ --add <principals> --role <role> grant/change roles (emails or team:<id>, comma-separated)
94
+ --remove <principals> revoke direct grants; inherited access remains
95
+ bikeshed visibility <id> [private|unlisted|public]
96
+ --public-edit true|false change open editing
97
+ --expires <ISO timestamp|none> set or clear expiry; omitted settings stay unchanged
98
+ bikeshed rename-folder <id> <name> rename a folder you can edit
99
+ bikeshed move-folder <id> <folder|root> move a folder and recompute inherited access
100
+ bikeshed delete-folder <id> owner-only; reparent contents, retain documents
101
+
91
102
  Skills
92
103
  bikeshed skills skills this deployment publishes
93
104
  bikeshed skills install <dir> write them into an agent's skills directory
@@ -788,6 +799,80 @@ async function folder(flags, [id]) {
788
799
  });
789
800
  }
790
801
 
802
+ /** Complete owner-visible grants, including why a removed direct grant may still read. */
803
+ function printManagedAccess(result) {
804
+ console.log(`${result.kind} ${result.id} — owner: ${result.owner}`);
805
+ for (const [role, people] of Object.entries(result.direct)) console.log(`${role}: ${people.join(", ") || "none"}`);
806
+ for (const grant of result.inherited) console.log(`inherited ${grant.role}: ${grant.principal} via ${grant.path}`);
807
+ for (const [role, people] of Object.entries(result.effective)) console.log(`${role}: ${people.join(", ") || "none"}`);
808
+ if (result.visibility !== undefined) printVisibility(result);
809
+ }
810
+
811
+ function printVisibility(result) {
812
+ console.log(`visibility: ${result.visibility}; open editing: ${result.publicEdit ? "on" : "off"}; expires: ${result.expiresAt === null ? "never" : new Date(result.expiresAt).toISOString()}`);
813
+ console.log(result.url);
814
+ }
815
+
816
+ async function access(flags, [kind, ref, ...rest]) {
817
+ if (!["document", "folder"].includes(kind) || !ref || rest.length) fail("usage: bikeshed access <document|folder> <id> [--add <principals> --role <role>] [--remove <principals>]");
818
+ const args = { id: kind === "document" ? docArgs(ref).id : ref };
819
+ const list = raw => {
820
+ if (typeof raw !== "string" || !raw.trim()) fail("principal list must not be empty");
821
+ const values = raw.split(",").map(p => p.trim());
822
+ if (values.some(p => !p)) fail("principal list contains an empty entry");
823
+ return values;
824
+ };
825
+ if (flags.add !== undefined) {
826
+ if (!["viewer", "suggester", "editor", "approver"].includes(flags.role)) fail("--add requires --role viewer|suggester|editor|approver");
827
+ args.add = list(flags.add).map(principal => ({ principal, role: flags.role }));
828
+ } else if (flags.role !== undefined) fail("--role requires --add");
829
+ if (flags.remove !== undefined) args.remove = list(flags.remove);
830
+ const { origin, token } = resolveAuth(flags);
831
+ const result = await callTool(origin, token, `${args.add || args.remove ? "set" : "get"}_${kind}_access`, args);
832
+ out(flags.json, result, () => printManagedAccess(result));
833
+ }
834
+
835
+ async function visibility(flags, [ref, level, ...rest]) {
836
+ 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>]");
837
+ const args = { id: docArgs(ref).id };
838
+ if (level !== undefined) args.visibility = level;
839
+ if (flags["public-edit"] !== undefined) {
840
+ if (!["true", "false"].includes(flags["public-edit"])) fail("--public-edit must be true or false");
841
+ args.publicEdit = flags["public-edit"] === "true";
842
+ }
843
+ if (flags.expires !== undefined) {
844
+ if (flags.expires === "none") args.expiresAt = null;
845
+ else {
846
+ 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");
847
+ args.expiresAt = Date.parse(flags.expires);
848
+ }
849
+ }
850
+ const { origin, token } = resolveAuth(flags);
851
+ const result = await callTool(origin, token, Object.keys(args).length > 1 ? "set_document_visibility" : "get_document_access", args);
852
+ out(flags.json, result, () => printVisibility(result));
853
+ }
854
+
855
+ async function renameFolder(flags, [id, name, ...rest]) {
856
+ if (!id || !name || rest.length) fail("usage: bikeshed rename-folder <id> <name>");
857
+ const { origin, token } = resolveAuth(flags);
858
+ const result = await callTool(origin, token, "rename_folder", { id, name });
859
+ out(flags.json, result, () => console.log(`renamed ${id}: ${result.path}`));
860
+ }
861
+
862
+ async function moveFolder(flags, [id, parent, ...rest]) {
863
+ if (!id || !parent || rest.length) fail("usage: bikeshed move-folder <id> <folder|root>");
864
+ const { origin, token } = resolveAuth(flags);
865
+ const result = await callTool(origin, token, "move_folder", { id, ...(parent === "root" ? {} : { parent }) });
866
+ out(flags.json, result, () => console.log(`moved ${id}: ${result.path}; ${result.access.readers} can read it`));
867
+ }
868
+
869
+ async function deleteFolder(flags, [id, ...rest]) {
870
+ if (!id || rest.length) fail("usage: bikeshed delete-folder <id>");
871
+ const { origin, token } = resolveAuth(flags);
872
+ const result = await callTool(origin, token, "delete_folder", { id });
873
+ out(flags.json, result, () => console.log(`deleted ${id}; moved ${result.reparented.folders} folders and ${result.reparented.documents} documents to ${result.parent ?? "root"}`));
874
+ }
875
+
791
876
  /** Exit status when `changes --wait` gives up: distinct from 1, an error. */
792
877
  const WAIT_TIMED_OUT = 3;
793
878
  /** The shortest wait between checks: each one reads the log and the document. */
@@ -1062,6 +1147,11 @@ const COMMANDS = {
1062
1147
  reject,
1063
1148
  commit,
1064
1149
  open,
1150
+ access,
1151
+ visibility,
1152
+ "rename-folder": renameFolder,
1153
+ "move-folder": moveFolder,
1154
+ "delete-folder": deleteFolder,
1065
1155
  folders,
1066
1156
  folder,
1067
1157
  mkdir,
@@ -1079,6 +1169,11 @@ async function main() {
1079
1169
  allowPositionals: true,
1080
1170
  strict: false,
1081
1171
  options: {
1172
+ add: { type: "string" },
1173
+ remove: { type: "string" },
1174
+ role: { type: "string" },
1175
+ "public-edit": { type: "string" },
1176
+ expires: { type: "string" },
1082
1177
  json: { type: "boolean", default: false },
1083
1178
  watch: { type: "boolean", default: false },
1084
1179
  all: { type: "boolean", default: false },