@sinoia/hubdoc-tools 1.5.1 → 1.6.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/cli.js +561 -72
  2. package/docs/llm-usage.md +71 -1
  3. package/package.json +1 -1
package/cli.js CHANGED
@@ -13297,9 +13297,9 @@ var require_groupBy = __commonJS({
13297
13297
  } else {
13298
13298
  duration = elementOrOptions.duration, element = elementOrOptions.element, connector = elementOrOptions.connector;
13299
13299
  }
13300
- var groups = /* @__PURE__ */ new Map();
13300
+ var groups2 = /* @__PURE__ */ new Map();
13301
13301
  var notify = function(cb) {
13302
- groups.forEach(cb);
13302
+ groups2.forEach(cb);
13303
13303
  cb(subscriber);
13304
13304
  };
13305
13305
  var handleError = function(err) {
@@ -13312,9 +13312,9 @@ var require_groupBy = __commonJS({
13312
13312
  var groupBySourceSubscriber = new OperatorSubscriber_1.OperatorSubscriber(subscriber, function(value2) {
13313
13313
  try {
13314
13314
  var key_1 = keySelector(value2);
13315
- var group_1 = groups.get(key_1);
13315
+ var group_1 = groups2.get(key_1);
13316
13316
  if (!group_1) {
13317
- groups.set(key_1, group_1 = connector ? connector() : new Subject_1.Subject());
13317
+ groups2.set(key_1, group_1 = connector ? connector() : new Subject_1.Subject());
13318
13318
  var grouped = createGroupedObservable(key_1, group_1);
13319
13319
  subscriber.next(grouped);
13320
13320
  if (duration) {
@@ -13322,7 +13322,7 @@ var require_groupBy = __commonJS({
13322
13322
  group_1.complete();
13323
13323
  durationSubscriber_1 === null || durationSubscriber_1 === void 0 ? void 0 : durationSubscriber_1.unsubscribe();
13324
13324
  }, void 0, void 0, function() {
13325
- return groups.delete(key_1);
13325
+ return groups2.delete(key_1);
13326
13326
  });
13327
13327
  groupBySourceSubscriber.add(innerFrom_1.innerFrom(duration(grouped)).subscribe(durationSubscriber_1));
13328
13328
  }
@@ -13336,7 +13336,7 @@ var require_groupBy = __commonJS({
13336
13336
  return consumer.complete();
13337
13337
  });
13338
13338
  }, handleError, function() {
13339
- return groups.clear();
13339
+ return groups2.clear();
13340
13340
  }, function() {
13341
13341
  teardownAttempted = true;
13342
13342
  return activeGroups === 0;
@@ -71642,8 +71642,8 @@ function estimateDataURLDecodedBytes(url2) {
71642
71642
  pad++;
71643
71643
  }
71644
71644
  }
71645
- const groups = Math.floor(effectiveLen / 4);
71646
- const bytes = groups * 3 - (pad || 0);
71645
+ const groups2 = Math.floor(effectiveLen / 4);
71646
+ const bytes = groups2 * 3 - (pad || 0);
71647
71647
  return bytes > 0 ? bytes : 0;
71648
71648
  }
71649
71649
  return Buffer.byteLength(body, "utf8");
@@ -75269,6 +75269,12 @@ var GroupsApi = class extends BaseAPI {
75269
75269
  return GroupsApiFp(this.configuration).apiV1GroupsPost(contentType, groupMutation, options).then((request) => request(this.axios, this.basePath));
75270
75270
  }
75271
75271
  };
75272
+ var ApiV1GroupsIdPatchContentTypeEnum = {
75273
+ ApplicationJson: "application/json"
75274
+ };
75275
+ var ApiV1GroupsPostContentTypeEnum = {
75276
+ ApplicationJson: "application/json"
75277
+ };
75272
75278
 
75273
75279
  // packages/api-client/src/api/permissions-api.ts
75274
75280
  var PermissionsApiAxiosParamCreator = function(configuration) {
@@ -76328,7 +76334,7 @@ var OAuthTokenService = class {
76328
76334
  res.end("<h1>404 - Not Found</h1><p>This is the OAuth callback server.</p>");
76329
76335
  }
76330
76336
  });
76331
- this.server.listen(port, "127.0.0.1", () => {
76337
+ this.server.listen(port, () => {
76332
76338
  console.log(import_chalk2.default.gray(`\u{1F5A5}\uFE0F Local server started on port ${port}`));
76333
76339
  });
76334
76340
  this.server.on("error", (error) => {
@@ -76459,7 +76465,7 @@ var OAuthTokenService = class {
76459
76465
  isPortAvailable(port) {
76460
76466
  return new Promise((resolve) => {
76461
76467
  const server = import_http4.default.createServer();
76462
- server.listen(port, "127.0.0.1", () => {
76468
+ server.listen(port, () => {
76463
76469
  server.close(() => resolve(true));
76464
76470
  });
76465
76471
  server.on("error", () => resolve(false));
@@ -77133,20 +77139,20 @@ var PermissionManager = class {
77133
77139
  console.log(import_chalk5.default.gray("\u{1F4E5} Pre-loading users and groups cache..."));
77134
77140
  try {
77135
77141
  const usersResponse = await this.usersApi.apiV1UsersGet(void 0, void 0, 100);
77136
- const users = usersResponse.data;
77137
- users.forEach((user) => {
77142
+ const users2 = usersResponse.data;
77143
+ users2.forEach((user) => {
77138
77144
  if (user.email_address) {
77139
77145
  this.userCache.set(user.email_address.toLowerCase(), user);
77140
77146
  }
77141
77147
  });
77142
77148
  const groupsResponse = await this.groupsApi.apiV1GroupsGet(void 0, void 0, 100);
77143
- const groups = groupsResponse.data;
77144
- groups.forEach((group) => {
77149
+ const groups2 = groupsResponse.data;
77150
+ groups2.forEach((group) => {
77145
77151
  if (group.name) {
77146
77152
  this.groupCache.set(group.name.toLowerCase(), group);
77147
77153
  }
77148
77154
  });
77149
- console.log(import_chalk5.default.gray(`\u{1F4E6} Cached ${users.length} users and ${groups.length} groups`));
77155
+ console.log(import_chalk5.default.gray(`\u{1F4E6} Cached ${users2.length} users and ${groups2.length} groups`));
77150
77156
  } catch (error) {
77151
77157
  console.warn(import_chalk5.default.yellow(`\u26A0\uFE0F Warning: Failed to preload cache: ${error.message}`));
77152
77158
  }
@@ -77168,9 +77174,9 @@ var PermissionManager = class {
77168
77174
  1,
77169
77175
  { "q[email_address_eq]": normalizedEmail }
77170
77176
  );
77171
- const users = response.data;
77172
- if (users.length > 0) {
77173
- const user = users[0];
77177
+ const users2 = response.data;
77178
+ if (users2.length > 0) {
77179
+ const user = users2[0];
77174
77180
  this.userCache.set(normalizedEmail, user);
77175
77181
  return user.id;
77176
77182
  }
@@ -77199,9 +77205,9 @@ var PermissionManager = class {
77199
77205
  1,
77200
77206
  { "q[name_eq]": trimmedName }
77201
77207
  );
77202
- const groups = response.data;
77203
- if (groups.length > 0) {
77204
- const group = groups[0];
77208
+ const groups2 = response.data;
77209
+ if (groups2.length > 0) {
77210
+ const group = groups2[0];
77205
77211
  this.groupCache.set(normalizedName, group);
77206
77212
  return group.id;
77207
77213
  }
@@ -77215,12 +77221,12 @@ var PermissionManager = class {
77215
77221
  /**
77216
77222
  * Résout les permissions (emails et noms de groupes) vers des IDs
77217
77223
  */
77218
- async resolvePermissions(permissions) {
77224
+ async resolvePermissions(permissions2) {
77219
77225
  const userIds = [];
77220
77226
  const groupIds = [];
77221
77227
  const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
77222
- if (permissions.users && permissions.users.length > 0) {
77223
- for (const emailOrId of permissions.users) {
77228
+ if (permissions2.users && permissions2.users.length > 0) {
77229
+ for (const emailOrId of permissions2.users) {
77224
77230
  if (uuidRegex.test(emailOrId)) {
77225
77231
  userIds.push(emailOrId);
77226
77232
  } else {
@@ -77231,8 +77237,8 @@ var PermissionManager = class {
77231
77237
  }
77232
77238
  }
77233
77239
  }
77234
- if (permissions.groups && permissions.groups.length > 0) {
77235
- for (const nameOrId of permissions.groups) {
77240
+ if (permissions2.groups && permissions2.groups.length > 0) {
77241
+ for (const nameOrId of permissions2.groups) {
77236
77242
  if (uuidRegex.test(nameOrId)) {
77237
77243
  groupIds.push(nameOrId);
77238
77244
  } else {
@@ -77248,9 +77254,9 @@ var PermissionManager = class {
77248
77254
  /**
77249
77255
  * Assigne les permissions à un document
77250
77256
  */
77251
- async assignDocumentPermissions(documentId, permissions, level) {
77257
+ async assignDocumentPermissions(documentId, permissions2, level) {
77252
77258
  try {
77253
- const { userIds, groupIds } = await this.resolvePermissions(permissions);
77259
+ const { userIds, groupIds } = await this.resolvePermissions(permissions2);
77254
77260
  if (userIds.length === 0 && groupIds.length === 0) {
77255
77261
  console.warn(import_chalk5.default.yellow(`\u26A0\uFE0F No valid permissions to assign for document ${documentId}`));
77256
77262
  return;
@@ -77630,6 +77636,9 @@ var HubDocApiService = class {
77630
77636
  if (mapping.autoClassify !== void 0) {
77631
77637
  formData.append("auto_classify", String(mapping.autoClassify));
77632
77638
  }
77639
+ if (mapping.externalId) {
77640
+ formData.append("external_id", mapping.externalId);
77641
+ }
77633
77642
  if (mapping.metadata) {
77634
77643
  formData.append("metadata_user", JSON.stringify(mapping.metadata));
77635
77644
  }
@@ -84522,6 +84531,7 @@ var CsvManager = class {
84522
84531
  filePath,
84523
84532
  targetFolder: "",
84524
84533
  workspace: "",
84534
+ externalId: "",
84525
84535
  metadata: "",
84526
84536
  permissions: "",
84527
84537
  autoClassify: "false",
@@ -84538,6 +84548,7 @@ var CsvManager = class {
84538
84548
  { id: "filePath", title: "File Path" },
84539
84549
  { id: "targetFolder", title: "Target Folder" },
84540
84550
  { id: "workspace", title: "Workspace" },
84551
+ { id: "externalId", title: "External ID" },
84541
84552
  { id: "metadata", title: "Metadata (key: value, parent.child: value)" },
84542
84553
  { id: "permissions", title: "Permissions (users:r:email1,email2 | users:w:admin | groups:Group Name)" },
84543
84554
  { id: "autoClassify", title: "Auto Classify" },
@@ -84571,7 +84582,7 @@ var CsvManager = class {
84571
84582
  })).on("data", (data) => {
84572
84583
  try {
84573
84584
  const permissionsStr = data["Permissions (users:r:email1,email2 | users:w:admin | groups:Group Name)"] || data["Permissions (users:r:email1@ex.com,email2@ex.com | users:w:admin@ex.com | groups:GroupName)"] || data["Permissions"] || data.Permissions || data.permissions || "";
84574
- const permissions = this.parsePermissions(permissionsStr);
84585
+ const permissions2 = this.parsePermissions(permissionsStr);
84575
84586
  const cleanValue = (value2) => {
84576
84587
  if (value2 === void 0 || value2 === null) return void 0;
84577
84588
  const trimmed = String(value2).trim();
@@ -84581,10 +84592,11 @@ var CsvManager = class {
84581
84592
  filePath: data["File Path"] ?? data.filePath,
84582
84593
  targetFolder: cleanValue(data["Target Folder"] ?? data.targetFolder),
84583
84594
  workspace: cleanValue(data["Workspace"] ?? data.workspace),
84595
+ externalId: cleanValue(data["External ID"] ?? data.externalId),
84584
84596
  metadata: this.parseMetadata(
84585
84597
  data["Metadata (key: value, parent.child: value)"] || data["Metadata (JSON)"] || data.Metadata || data.metadata
84586
84598
  ),
84587
- permissions,
84599
+ permissions: permissions2,
84588
84600
  autoClassify: this.parseBoolean(data["Auto Classify"] || data.autoClassify),
84589
84601
  status: data["Status"] || data.status || "pending",
84590
84602
  progress: parseFloat(data["Progress (%)"] || data.progress || "0"),
@@ -84753,18 +84765,18 @@ var CsvManager = class {
84753
84765
  }
84754
84766
  } else if (type === "groups") {
84755
84767
  if (parts2.length === 2) {
84756
- const groups = parts2[1].split(",").map((g) => g.trim()).filter((g) => g);
84757
- if (groups.length > 0) {
84758
- readPermissions.groups = [...readPermissions.groups || [], ...groups];
84768
+ const groups2 = parts2[1].split(",").map((g) => g.trim()).filter((g) => g);
84769
+ if (groups2.length > 0) {
84770
+ readPermissions.groups = [...readPermissions.groups || [], ...groups2];
84759
84771
  }
84760
84772
  } else if (parts2.length === 3) {
84761
84773
  const level = parts2[1];
84762
- const groups = parts2[2].split(",").map((g) => g.trim()).filter((g) => g);
84763
- if (groups.length > 0) {
84774
+ const groups2 = parts2[2].split(",").map((g) => g.trim()).filter((g) => g);
84775
+ if (groups2.length > 0) {
84764
84776
  if (level === "w") {
84765
- writePermissions.groups = [...writePermissions.groups || [], ...groups];
84777
+ writePermissions.groups = [...writePermissions.groups || [], ...groups2];
84766
84778
  } else {
84767
- readPermissions.groups = [...readPermissions.groups || [], ...groups];
84779
+ readPermissions.groups = [...readPermissions.groups || [], ...groups2];
84768
84780
  }
84769
84781
  }
84770
84782
  }
@@ -84880,6 +84892,7 @@ var CsvManager = class {
84880
84892
  filePath: m.filePath,
84881
84893
  targetFolder: m.targetFolder || "",
84882
84894
  workspace: m.workspace || "",
84895
+ externalId: m.externalId || "",
84883
84896
  metadata: this.serializeMetadata(m.metadata),
84884
84897
  permissions: this.serializePermissions(m.permissions),
84885
84898
  autoClassify: m.autoClassify !== void 0 ? String(m.autoClassify) : "false",
@@ -84896,6 +84909,7 @@ var CsvManager = class {
84896
84909
  { id: "filePath", title: "File Path" },
84897
84910
  { id: "targetFolder", title: "Target Folder" },
84898
84911
  { id: "workspace", title: "Workspace" },
84912
+ { id: "externalId", title: "External ID" },
84899
84913
  { id: "metadata", title: "Metadata (key: value, parent.child: value)" },
84900
84914
  { id: "permissions", title: "Permissions (users:r:email1,email2 | users:w:admin | groups:Group Name)" },
84901
84915
  { id: "autoClassify", title: "Auto Classify" },
@@ -84915,20 +84929,20 @@ var CsvManager = class {
84915
84929
  * Serialize permissions in new format:
84916
84930
  * users:r:email1@ex.com,email2@ex.com | users:w:admin@ex.com | groups:Group Name | groups:w:Admin Group
84917
84931
  */
84918
- serializePermissions(permissions) {
84919
- if (!permissions) return "";
84932
+ serializePermissions(permissions2) {
84933
+ if (!permissions2) return "";
84920
84934
  const parts2 = [];
84921
- if (permissions.read?.users && permissions.read.users.length > 0) {
84922
- parts2.push(`users:r:${permissions.read.users.join(",")}`);
84935
+ if (permissions2.read?.users && permissions2.read.users.length > 0) {
84936
+ parts2.push(`users:r:${permissions2.read.users.join(",")}`);
84923
84937
  }
84924
- if (permissions.read?.groups && permissions.read.groups.length > 0) {
84925
- parts2.push(`groups:${permissions.read.groups.join(",")}`);
84938
+ if (permissions2.read?.groups && permissions2.read.groups.length > 0) {
84939
+ parts2.push(`groups:${permissions2.read.groups.join(",")}`);
84926
84940
  }
84927
- if (permissions.write?.users && permissions.write.users.length > 0) {
84928
- parts2.push(`users:w:${permissions.write.users.join(",")}`);
84941
+ if (permissions2.write?.users && permissions2.write.users.length > 0) {
84942
+ parts2.push(`users:w:${permissions2.write.users.join(",")}`);
84929
84943
  }
84930
- if (permissions.write?.groups && permissions.write.groups.length > 0) {
84931
- parts2.push(`groups:w:${permissions.write.groups.join(",")}`);
84944
+ if (permissions2.write?.groups && permissions2.write.groups.length > 0) {
84945
+ parts2.push(`groups:w:${permissions2.write.groups.join(",")}`);
84932
84946
  }
84933
84947
  return parts2.join(" | ");
84934
84948
  }
@@ -85861,7 +85875,7 @@ var CsvManager2 = class {
85861
85875
  })).on("data", (data) => {
85862
85876
  try {
85863
85877
  const permissionsStr = data["Permissions (users:r:email1,email2 | users:w:admin | groups:Group Name)"] || data["Permissions (users:r:email1@ex.com,email2@ex.com | users:w:admin@ex.com | groups:GroupName)"] || data["Permissions"] || data.Permissions || data.permissions || "";
85864
- const permissions = this.parsePermissions(permissionsStr);
85878
+ const permissions2 = this.parsePermissions(permissionsStr);
85865
85879
  const cleanValue = (value2) => {
85866
85880
  if (value2 === void 0 || value2 === null) return void 0;
85867
85881
  const trimmed = String(value2).trim();
@@ -85874,7 +85888,7 @@ var CsvManager2 = class {
85874
85888
  metadata: this.parseMetadata(
85875
85889
  data["Metadata (key: value, parent.child: value)"] || data["Metadata (JSON)"] || data.Metadata || data.metadata
85876
85890
  ),
85877
- permissions,
85891
+ permissions: permissions2,
85878
85892
  autoClassify: this.parseBoolean(data["Auto Classify"] || data.autoClassify),
85879
85893
  status: data["Status"] || data.status || "pending",
85880
85894
  progress: parseFloat(data["Progress (%)"] || data.progress || "0"),
@@ -86043,18 +86057,18 @@ var CsvManager2 = class {
86043
86057
  }
86044
86058
  } else if (type === "groups") {
86045
86059
  if (parts2.length === 2) {
86046
- const groups = parts2[1].split(",").map((g) => g.trim()).filter((g) => g);
86047
- if (groups.length > 0) {
86048
- readPermissions.groups = [...readPermissions.groups || [], ...groups];
86060
+ const groups2 = parts2[1].split(",").map((g) => g.trim()).filter((g) => g);
86061
+ if (groups2.length > 0) {
86062
+ readPermissions.groups = [...readPermissions.groups || [], ...groups2];
86049
86063
  }
86050
86064
  } else if (parts2.length === 3) {
86051
86065
  const level = parts2[1];
86052
- const groups = parts2[2].split(",").map((g) => g.trim()).filter((g) => g);
86053
- if (groups.length > 0) {
86066
+ const groups2 = parts2[2].split(",").map((g) => g.trim()).filter((g) => g);
86067
+ if (groups2.length > 0) {
86054
86068
  if (level === "w") {
86055
- writePermissions.groups = [...writePermissions.groups || [], ...groups];
86069
+ writePermissions.groups = [...writePermissions.groups || [], ...groups2];
86056
86070
  } else {
86057
- readPermissions.groups = [...readPermissions.groups || [], ...groups];
86071
+ readPermissions.groups = [...readPermissions.groups || [], ...groups2];
86058
86072
  }
86059
86073
  }
86060
86074
  }
@@ -86205,20 +86219,20 @@ var CsvManager2 = class {
86205
86219
  * Serialize permissions in new format:
86206
86220
  * users:r:email1@ex.com,email2@ex.com | users:w:admin@ex.com | groups:Group Name | groups:w:Admin Group
86207
86221
  */
86208
- serializePermissions(permissions) {
86209
- if (!permissions) return "";
86222
+ serializePermissions(permissions2) {
86223
+ if (!permissions2) return "";
86210
86224
  const parts2 = [];
86211
- if (permissions.read?.users && permissions.read.users.length > 0) {
86212
- parts2.push(`users:r:${permissions.read.users.join(",")}`);
86225
+ if (permissions2.read?.users && permissions2.read.users.length > 0) {
86226
+ parts2.push(`users:r:${permissions2.read.users.join(",")}`);
86213
86227
  }
86214
- if (permissions.read?.groups && permissions.read.groups.length > 0) {
86215
- parts2.push(`groups:${permissions.read.groups.join(",")}`);
86228
+ if (permissions2.read?.groups && permissions2.read.groups.length > 0) {
86229
+ parts2.push(`groups:${permissions2.read.groups.join(",")}`);
86216
86230
  }
86217
- if (permissions.write?.users && permissions.write.users.length > 0) {
86218
- parts2.push(`users:w:${permissions.write.users.join(",")}`);
86231
+ if (permissions2.write?.users && permissions2.write.users.length > 0) {
86232
+ parts2.push(`users:w:${permissions2.write.users.join(",")}`);
86219
86233
  }
86220
- if (permissions.write?.groups && permissions.write.groups.length > 0) {
86221
- parts2.push(`groups:w:${permissions.write.groups.join(",")}`);
86234
+ if (permissions2.write?.groups && permissions2.write.groups.length > 0) {
86235
+ parts2.push(`groups:w:${permissions2.write.groups.join(",")}`);
86222
86236
  }
86223
86237
  return parts2.join(" | ");
86224
86238
  }
@@ -86746,13 +86760,13 @@ var PluginImportService = class {
86746
86760
  * Group mappings by connection ID
86747
86761
  */
86748
86762
  groupMappingsByConnection(mappings) {
86749
- return mappings.reduce((groups, mapping) => {
86763
+ return mappings.reduce((groups2, mapping) => {
86750
86764
  const { connectionId } = mapping;
86751
- if (!groups[connectionId]) {
86752
- groups[connectionId] = [];
86765
+ if (!groups2[connectionId]) {
86766
+ groups2[connectionId] = [];
86753
86767
  }
86754
- groups[connectionId].push(mapping);
86755
- return groups;
86768
+ groups2[connectionId].push(mapping);
86769
+ return groups2;
86756
86770
  }, {});
86757
86771
  }
86758
86772
  /**
@@ -87400,6 +87414,8 @@ async function buildApiContext(out) {
87400
87414
  documents: new DocumentsApi(configuration, baseUrl, axiosInstance),
87401
87415
  chunkedUploads: new ChunkedUploadsApi(configuration, baseUrl, axiosInstance),
87402
87416
  users: new UsersApi(configuration, baseUrl, axiosInstance),
87417
+ groups: new GroupsApi(configuration, baseUrl, axiosInstance),
87418
+ permissions: new PermissionsApi(configuration, baseUrl, axiosInstance),
87403
87419
  baseUrl,
87404
87420
  token: token2,
87405
87421
  tokenSource: resolved.source
@@ -89008,10 +89024,437 @@ async function handleTemplatesShow(idOrKey, opts) {
89008
89024
  }
89009
89025
  }
89010
89026
 
89027
+ // apps/cli/cli/handlers/user-handlers.ts
89028
+ var USER_COLUMNS = [
89029
+ { header: "ID", key: "id" },
89030
+ { header: "Email", key: "email_address" },
89031
+ { header: "Name", key: "display_name" },
89032
+ { header: "Role", key: "role" },
89033
+ { header: "External", key: "external_id", maxWidth: 24 }
89034
+ ];
89035
+ function matchesQuery(user, q) {
89036
+ const needle = q.toLowerCase();
89037
+ const haystack = [
89038
+ user.email_address,
89039
+ user.first_name,
89040
+ user.last_name,
89041
+ user.display_name,
89042
+ user.external_id
89043
+ ].filter(Boolean).join(" ").toLowerCase();
89044
+ return haystack.includes(needle);
89045
+ }
89046
+ async function handleUsersList(opts) {
89047
+ const out = output(opts);
89048
+ const ctx = await buildApiContext(out);
89049
+ try {
89050
+ const res = await ctx.users.apiV1UsersGet(
89051
+ void 0,
89052
+ void 0,
89053
+ opts.limit ? Number(opts.limit) : void 0
89054
+ );
89055
+ let items = res.data ?? [];
89056
+ if (opts.q) items = items.filter((u) => matchesQuery(u, opts.q));
89057
+ if (items.length === 50 && !opts.q) {
89058
+ out.note("server may have capped result at 50 (see corex#460) \u2014 refine with --q to filter");
89059
+ }
89060
+ out.list(items, USER_COLUMNS);
89061
+ } catch (err) {
89062
+ out.fail(err);
89063
+ }
89064
+ }
89065
+ async function handleUsersShow(idOrEmail, opts) {
89066
+ const out = output(opts);
89067
+ const ctx = await buildApiContext(out);
89068
+ const looksLikeEmail = idOrEmail.includes("@");
89069
+ let found;
89070
+ try {
89071
+ if (!looksLikeEmail) {
89072
+ const res2 = await ctx.users.apiV1UsersIdGet(idOrEmail);
89073
+ out.show(res2.data, { title: `User ${idOrEmail}` });
89074
+ return;
89075
+ }
89076
+ const res = await ctx.users.apiV1UsersGet();
89077
+ const items = res.data ?? [];
89078
+ found = items.find((u) => (u.email_address || "").toLowerCase() === idOrEmail.toLowerCase());
89079
+ } catch (err) {
89080
+ out.fail(err);
89081
+ return;
89082
+ }
89083
+ if (!found) {
89084
+ out.fail(
89085
+ new Error(
89086
+ `User '${idOrEmail}' not found in the first 50 results. This is likely due to corex#460 (users API server-side filter is broken). Use the UUID directly if you have it.`
89087
+ )
89088
+ );
89089
+ }
89090
+ out.show(found, { title: `User ${idOrEmail}` });
89091
+ }
89092
+
89093
+ // apps/cli/cli/handlers/group-handlers.ts
89094
+ var GROUP_COLUMNS = [
89095
+ { header: "ID", key: "id" },
89096
+ { header: "Name", key: "name" },
89097
+ { header: "Description", key: "description", maxWidth: 40 },
89098
+ { header: "External", key: "external_id", maxWidth: 24 },
89099
+ { header: "Updated", key: "updated_at" }
89100
+ ];
89101
+ async function handleGroupsList(opts) {
89102
+ const out = output(opts);
89103
+ const ctx = await buildApiContext(out);
89104
+ try {
89105
+ const res = await ctx.groups.apiV1GroupsGet(
89106
+ void 0,
89107
+ void 0,
89108
+ opts.limit ? Number(opts.limit) : void 0,
89109
+ opts.page ? Number(opts.page) : void 0
89110
+ );
89111
+ let items = res.data ?? [];
89112
+ if (opts.q) {
89113
+ const needle = opts.q.toLowerCase();
89114
+ items = items.filter(
89115
+ (g) => (g.name || "").toLowerCase().includes(needle) || (g.description || "").toLowerCase().includes(needle)
89116
+ );
89117
+ }
89118
+ out.list(items, GROUP_COLUMNS);
89119
+ } catch (err) {
89120
+ out.fail(err);
89121
+ }
89122
+ }
89123
+ async function handleGroupsShow(id, opts) {
89124
+ const out = output(opts);
89125
+ const ctx = await buildApiContext(out);
89126
+ try {
89127
+ const res = await ctx.groups.apiV1GroupsIdGet(id);
89128
+ out.show(res.data, { title: `Group ${id}` });
89129
+ } catch (err) {
89130
+ out.fail(err);
89131
+ }
89132
+ }
89133
+ async function handleGroupsCreate(opts) {
89134
+ const out = output(opts);
89135
+ const ctx = await buildApiContext(out);
89136
+ const mutation = { name: opts.name };
89137
+ if (opts.description !== void 0) mutation.description = opts.description;
89138
+ if (opts.externalId !== void 0) mutation.external_id = opts.externalId;
89139
+ try {
89140
+ const res = await ctx.groups.apiV1GroupsPost(
89141
+ ApiV1GroupsPostContentTypeEnum.ApplicationJson,
89142
+ mutation
89143
+ );
89144
+ out.ok(`Group created (id=${res.data.id})`, res.data);
89145
+ } catch (err) {
89146
+ out.fail(err);
89147
+ }
89148
+ }
89149
+ async function handleGroupsUpdate(id, opts) {
89150
+ const out = output(opts);
89151
+ const ctx = await buildApiContext(out);
89152
+ if (opts.name === void 0 && opts.description === void 0 && opts.externalId === void 0) {
89153
+ out.fail(new Error("No fields to update"));
89154
+ }
89155
+ let nameValue = opts.name;
89156
+ if (nameValue === void 0) {
89157
+ try {
89158
+ const cur = await ctx.groups.apiV1GroupsIdGet(id);
89159
+ nameValue = cur.data.name;
89160
+ } catch (err) {
89161
+ out.fail(err);
89162
+ return;
89163
+ }
89164
+ }
89165
+ const mutation = { name: nameValue };
89166
+ if (opts.description !== void 0) mutation.description = opts.description;
89167
+ if (opts.externalId !== void 0) mutation.external_id = opts.externalId;
89168
+ try {
89169
+ const res = await ctx.groups.apiV1GroupsIdPatch(
89170
+ id,
89171
+ ApiV1GroupsIdPatchContentTypeEnum.ApplicationJson,
89172
+ mutation
89173
+ );
89174
+ out.ok(`Group ${id} updated`, res.data);
89175
+ } catch (err) {
89176
+ out.fail(err);
89177
+ }
89178
+ }
89179
+ async function handleGroupsDelete(id, opts) {
89180
+ const out = output(opts);
89181
+ const ctx = await buildApiContext(out);
89182
+ if (!opts.yes && !opts.json) {
89183
+ const { confirm } = await lib_default.prompt([
89184
+ {
89185
+ type: "confirm",
89186
+ name: "confirm",
89187
+ message: `Delete group ${id}? This removes all its members and permissions.`,
89188
+ default: false
89189
+ }
89190
+ ]);
89191
+ if (!confirm) {
89192
+ out.note("Aborted");
89193
+ return;
89194
+ }
89195
+ } else if (!opts.yes && opts.json) {
89196
+ out.fail(new Error("Refusing to delete without --yes in --json mode"));
89197
+ }
89198
+ try {
89199
+ await ctx.groups.apiV1GroupsIdDelete(id);
89200
+ out.ok(`Group ${id} deleted`);
89201
+ } catch (err) {
89202
+ out.fail(err);
89203
+ }
89204
+ }
89205
+
89206
+ // apps/cli/cli/handlers/group-member-handlers.ts
89207
+ var MEMBER_COLUMNS = [
89208
+ { header: "Membership ID", key: "id" },
89209
+ { header: "Member ID", key: "member_id" },
89210
+ { header: "Type", key: "member_type" },
89211
+ { header: "External", key: "user_external_id", maxWidth: 24 },
89212
+ { header: "Role", key: "role" }
89213
+ ];
89214
+ function membersBase(groupId) {
89215
+ return `/api/v1/groups/${encodeURIComponent(groupId)}/members`;
89216
+ }
89217
+ async function handleGroupMembersList(opts) {
89218
+ const out = output(opts);
89219
+ const ctx = await buildApiContext(out);
89220
+ try {
89221
+ const res = await ctx.axios.get(membersBase(opts.group));
89222
+ out.list(res.data ?? [], MEMBER_COLUMNS);
89223
+ } catch (err) {
89224
+ out.fail(err);
89225
+ }
89226
+ }
89227
+ async function resolveExternalId(ctx, out, opts) {
89228
+ if (opts.userExternalId) return opts.userExternalId;
89229
+ if (opts.userId) {
89230
+ let ext2;
89231
+ try {
89232
+ const res = await ctx.users.apiV1UsersIdGet(opts.userId);
89233
+ ext2 = res.data?.external_id;
89234
+ } catch (err) {
89235
+ out.fail(err);
89236
+ return "";
89237
+ }
89238
+ if (!ext2) {
89239
+ out.fail(
89240
+ new Error(
89241
+ `User ${opts.userId} has no external_id \u2014 server requires it to create a membership.`
89242
+ )
89243
+ );
89244
+ }
89245
+ return ext2;
89246
+ }
89247
+ if (opts.userEmail) {
89248
+ let found;
89249
+ try {
89250
+ const res = await ctx.users.apiV1UsersGet();
89251
+ const items = res.data ?? [];
89252
+ found = items.find(
89253
+ (u) => (u.email_address || "").toLowerCase() === opts.userEmail.toLowerCase()
89254
+ );
89255
+ } catch (err) {
89256
+ out.fail(err);
89257
+ return "";
89258
+ }
89259
+ if (!found) {
89260
+ out.fail(
89261
+ new Error(
89262
+ `User '${opts.userEmail}' not found in the first 50 results. Server-side email filter is currently broken (corex#460). Look up the UUID via the web UI and pass --user-id, or pass --user-external-id directly.`
89263
+ )
89264
+ );
89265
+ }
89266
+ if (!found.external_id) {
89267
+ out.fail(
89268
+ new Error(
89269
+ `User '${opts.userEmail}' has no external_id \u2014 server requires it to create a membership.`
89270
+ )
89271
+ );
89272
+ }
89273
+ return found.external_id;
89274
+ }
89275
+ out.fail(
89276
+ new Error("Pass one of --user-external-id, --user-id, or --user-email to identify the user.")
89277
+ );
89278
+ return "";
89279
+ }
89280
+ async function handleGroupMembersAdd(opts) {
89281
+ const out = output(opts);
89282
+ const ctx = await buildApiContext(out);
89283
+ const externalId = await resolveExternalId(ctx, out, opts);
89284
+ try {
89285
+ const res = await ctx.axios.post(membersBase(opts.group), { user_external_id: externalId });
89286
+ out.ok(`Membership created (id=${res.data.id})`, res.data);
89287
+ } catch (err) {
89288
+ out.fail(err);
89289
+ }
89290
+ }
89291
+ async function handleGroupMembersRemove(membershipId, opts) {
89292
+ const out = output(opts);
89293
+ const ctx = await buildApiContext(out);
89294
+ if (!opts.yes && !opts.json) {
89295
+ const { confirm } = await lib_default.prompt([
89296
+ {
89297
+ type: "confirm",
89298
+ name: "confirm",
89299
+ message: `Remove membership ${membershipId} from group ${opts.group}?`,
89300
+ default: false
89301
+ }
89302
+ ]);
89303
+ if (!confirm) {
89304
+ out.note("Aborted");
89305
+ return;
89306
+ }
89307
+ } else if (!opts.yes && opts.json) {
89308
+ out.fail(new Error("Refusing to remove without --yes in --json mode"));
89309
+ }
89310
+ try {
89311
+ await ctx.axios.delete(`${membersBase(opts.group)}/${encodeURIComponent(membershipId)}`);
89312
+ out.ok(`Membership ${membershipId} removed`);
89313
+ } catch (err) {
89314
+ out.fail(err);
89315
+ }
89316
+ }
89317
+
89318
+ // apps/cli/cli/handlers/permission-handlers.ts
89319
+ var PERM_COLUMNS = [
89320
+ { header: "ID", key: "id" },
89321
+ { header: "Actor", key: "actor_type", format: (v, row) => `${v}:${row.actor?.display_name ?? row.actor_id?.substring(0, 8)}` },
89322
+ { header: "Level", key: "level" },
89323
+ { header: "On", key: "permissible_type", format: (v) => v?.replace(/^Documents::/, "") ?? "" },
89324
+ { header: "Resource ID", key: "permissible_id" },
89325
+ { header: "Updated", key: "updated_at" }
89326
+ ];
89327
+ var VALID_LEVELS = /* @__PURE__ */ new Set(["read", "write", "admin", "owner", "forbidden"]);
89328
+ var PERMISSIBLE_ALIASES = {
89329
+ workspace: "Documents::Workspace",
89330
+ folder: "Documents::Folder",
89331
+ file: "Documents::File",
89332
+ share_link: "Documents::ShareLink"
89333
+ };
89334
+ var ACTOR_ALIASES = {
89335
+ user: "User",
89336
+ group: "Group",
89337
+ contact: "Contact"
89338
+ };
89339
+ function normalizePermissibleType(input, out) {
89340
+ const lower = input.toLowerCase();
89341
+ if (PERMISSIBLE_ALIASES[lower]) return PERMISSIBLE_ALIASES[lower];
89342
+ if (input.startsWith("Documents::")) return input;
89343
+ out.fail(
89344
+ new Error(
89345
+ `Invalid --on-type: '${input}'. Use workspace/folder/file/share_link or a fully-qualified Documents::\u2026 class name.`
89346
+ )
89347
+ );
89348
+ return "";
89349
+ }
89350
+ function normalizeActorType(input, out) {
89351
+ const lower = input.toLowerCase();
89352
+ if (ACTOR_ALIASES[lower]) return ACTOR_ALIASES[lower];
89353
+ if (["User", "Group", "Contact"].includes(input)) return input;
89354
+ out.fail(new Error(`Invalid --to-type: '${input}'. Use user/group/contact.`));
89355
+ return "";
89356
+ }
89357
+ async function handlePermissionsList(opts) {
89358
+ const out = output(opts);
89359
+ const ctx = await buildApiContext(out);
89360
+ if (!opts.on) {
89361
+ out.fail(
89362
+ new Error(
89363
+ "--on=<resource-id> is required (the server rejects listing without permissible_id/type)."
89364
+ )
89365
+ );
89366
+ }
89367
+ const onType = opts.onType ? normalizePermissibleType(opts.onType, out) : "Documents::Workspace";
89368
+ try {
89369
+ const res = await ctx.axios.get("/api/v1/permissions", {
89370
+ params: {
89371
+ permissible_id: opts.on,
89372
+ permissible_type: onType
89373
+ }
89374
+ });
89375
+ let items = res.data ?? [];
89376
+ if (opts.to) items = items.filter((p) => p.actor_id === opts.to);
89377
+ if (opts.toType) {
89378
+ const norm = normalizeActorType(opts.toType, out);
89379
+ items = items.filter((p) => p.actor_type === norm);
89380
+ }
89381
+ if (opts.level) items = items.filter((p) => p.level === opts.level);
89382
+ out.list(items, PERM_COLUMNS);
89383
+ } catch (err) {
89384
+ out.fail(err);
89385
+ }
89386
+ }
89387
+ async function handlePermissionsGrant(opts) {
89388
+ const out = output(opts);
89389
+ const ctx = await buildApiContext(out);
89390
+ if (!VALID_LEVELS.has(opts.level)) {
89391
+ out.fail(
89392
+ new Error(
89393
+ `Invalid --level: '${opts.level}'. Expected one of: ${[...VALID_LEVELS].join(", ")}`
89394
+ )
89395
+ );
89396
+ }
89397
+ const actorType = normalizeActorType(opts.toType, out);
89398
+ const permissibleType = normalizePermissibleType(opts.onType, out);
89399
+ try {
89400
+ const body = {
89401
+ actor_id: opts.to,
89402
+ actor_type: actorType,
89403
+ permissible_id: opts.on,
89404
+ permissible_type: permissibleType,
89405
+ level: opts.level
89406
+ };
89407
+ if (opts.temporary) body.temporary = true;
89408
+ const res = await ctx.axios.post("/api/v1/permissions", body);
89409
+ out.ok(
89410
+ `Permission granted (${actorType} \u2192 ${opts.level} on ${permissibleType})`,
89411
+ res.data
89412
+ );
89413
+ } catch (err) {
89414
+ out.fail(err);
89415
+ }
89416
+ }
89417
+ async function handlePermissionsRevoke(id, opts) {
89418
+ const out = output(opts);
89419
+ const ctx = await buildApiContext(out);
89420
+ if (!opts.yes && !opts.json) {
89421
+ const { confirm } = await lib_default.prompt([
89422
+ {
89423
+ type: "confirm",
89424
+ name: "confirm",
89425
+ message: `Revoke permission ${id}?`,
89426
+ default: false
89427
+ }
89428
+ ]);
89429
+ if (!confirm) {
89430
+ out.note("Aborted");
89431
+ return;
89432
+ }
89433
+ } else if (!opts.yes && opts.json) {
89434
+ out.fail(new Error("Refusing to revoke without --yes in --json mode"));
89435
+ }
89436
+ try {
89437
+ await ctx.permissions.apiV1PermissionsIdDelete(id);
89438
+ out.ok(`Permission ${id} revoked`);
89439
+ } catch (err) {
89440
+ out.fail(err);
89441
+ }
89442
+ }
89443
+ async function handlePermissionsShow(id, opts) {
89444
+ const out = output(opts);
89445
+ const ctx = await buildApiContext(out);
89446
+ try {
89447
+ const res = await ctx.permissions.apiV1PermissionsIdGet(id);
89448
+ out.show(res.data, { title: `Permission ${id}` });
89449
+ } catch (err) {
89450
+ out.fail(err);
89451
+ }
89452
+ }
89453
+
89011
89454
  // apps/cli/cli.ts
89012
89455
  var getVersion = () => {
89013
89456
  try {
89014
- if (true) return "1.5.1";
89457
+ if (true) return "1.6.1";
89015
89458
  } catch {
89016
89459
  }
89017
89460
  for (const candidate of [
@@ -89241,6 +89684,52 @@ templates.command("ls").description("List active templates").action(async (optio
89241
89684
  templates.command("show <id-or-key>").description("Show a template (full default_parts + page_settings)").action(async (idOrKey, options) => {
89242
89685
  await handleTemplatesShow(idOrKey, withGlobals(options));
89243
89686
  });
89687
+ var users = program2.command("users").description("Look up users (read-only)");
89688
+ users.command("ls").description("List users (currently capped at ~50 by corex#460)").option("--q <string>", "Filter results client-side by name/email/external_id").option("--limit <n>", "Server-side per_page (currently ignored, see corex#460)").action(async (options) => {
89689
+ await handleUsersList(withGlobals(options));
89690
+ });
89691
+ users.command("show <id-or-email>").description("Show a user by UUID or email (email lookup is capped, see corex#460)").action(async (id, options) => {
89692
+ await handleUsersShow(id, withGlobals(options));
89693
+ });
89694
+ var groups = program2.command("groups").description("Manage groups and members");
89695
+ groups.command("ls").description("List groups").option("--q <string>", "Filter by name/description (client-side)").option("--limit <n>").option("--page <n>").action(async (options) => {
89696
+ await handleGroupsList(withGlobals(options));
89697
+ });
89698
+ groups.command("show <id>").description("Show a group by id").action(async (id, options) => {
89699
+ await handleGroupsShow(id, withGlobals(options));
89700
+ });
89701
+ groups.command("create").description("Create a new group").requiredOption("--name <name>", "Group name (must be unique)").option("--description <text>").option("--external-id <id>", "External system identifier").action(async (options) => {
89702
+ await handleGroupsCreate(withGlobals(options));
89703
+ });
89704
+ groups.command("update <id>").description("Update group fields").option("--name <name>").option("--description <text>").option("--external-id <id>").action(async (id, options) => {
89705
+ await handleGroupsUpdate(id, withGlobals(options));
89706
+ });
89707
+ groups.command("delete <id>").description("Delete a group (cascades to memberships and permissions)").option("--yes", "Skip the interactive confirmation").action(async (id, options) => {
89708
+ await handleGroupsDelete(id, withGlobals(options));
89709
+ });
89710
+ var members = groups.command("members").description("Manage the membership of a group");
89711
+ members.command("ls").description("List members of a group").requiredOption("--group <id>", "Parent group id").action(async (options) => {
89712
+ await handleGroupMembersList(withGlobals(options));
89713
+ });
89714
+ members.command("add").description("Add a user to a group").requiredOption("--group <id>", "Parent group id").option("--user-external-id <ext>", "External id of the user (the API requires this)").option("--user-id <uuid>", "User UUID \u2014 looked up via GET /users/:id to extract external_id").option("--user-email <email>", "User email \u2014 best-effort lookup, blocked by corex#460 beyond 50 users").action(async (options) => {
89715
+ await handleGroupMembersAdd(withGlobals(options));
89716
+ });
89717
+ members.command("remove <membership-id>").description("Remove a membership by its id (from `members ls`)").requiredOption("--group <id>", "Parent group id").option("--yes", "Skip the interactive confirmation").action(async (id, options) => {
89718
+ await handleGroupMembersRemove(id, withGlobals(options));
89719
+ });
89720
+ var permissions = program2.command("permissions").description("Manage ACL: grant / list / revoke permissions on documents resources");
89721
+ permissions.command("ls").description("List permissions on a specific resource").requiredOption("--on <resource-id>", "Permissible resource UUID").option("--on-type <type>", "workspace | folder | file | share_link", "workspace").option("--to <actor-id>", "Filter by actor id (client-side)").option("--to-type <type>", "Filter by actor type (user | group | contact)").option("--level <level>", "Filter by level (read|write|admin|owner|forbidden)").action(async (options) => {
89722
+ await handlePermissionsList(withGlobals(options));
89723
+ });
89724
+ permissions.command("show <id>").description("Show a permission by id").action(async (id, options) => {
89725
+ await handlePermissionsShow(id, withGlobals(options));
89726
+ });
89727
+ permissions.command("grant").description("Grant a permission on a resource to a user or group").requiredOption("--to <actor-id>", "Actor UUID (user/group/contact)").requiredOption("--to-type <type>", "user | group | contact").requiredOption("--on <resource-id>", "Permissible resource UUID").requiredOption("--on-type <type>", "workspace | folder | file | share_link").requiredOption("--level <level>", "read | write | admin | owner | forbidden").option("--temporary", "Mark as temporary (auto-expiring per server policy)").action(async (options) => {
89728
+ await handlePermissionsGrant(withGlobals(options));
89729
+ });
89730
+ permissions.command("revoke <id>").description("Revoke a permission by id").option("--yes", "Skip the interactive confirmation").action(async (id, options) => {
89731
+ await handlePermissionsRevoke(id, withGlobals(options));
89732
+ });
89244
89733
  program2.parse();
89245
89734
  /*! Bundled license information:
89246
89735
 
package/docs/llm-usage.md CHANGED
@@ -291,12 +291,82 @@ Response shape (passed through unchanged in `--json` mode):
291
291
  Errors: `{"error": "q is required", "code": "http_400"}` if the query is
292
292
  empty (caught client-side too, before any HTTP call).
293
293
 
294
+ ### Users / Groups / Permissions (actors + ACL)
295
+
296
+ These cover the actors model (who) and the ACL model (what they can do on
297
+ what). All read+write, except `users` which is read-only.
298
+
299
+ ```bash
300
+ # Users (capped at ~50 by corex#460 server-side; filter client-side via --q)
301
+ hubdoc users ls [--q="alice"] [--limit=N] --json
302
+ hubdoc users show <id-or-email> --json
303
+
304
+ # Groups
305
+ hubdoc groups ls [--q="..."] [--limit=N] [--page=N] --json
306
+ hubdoc groups show <id> --json
307
+ hubdoc groups create --name="..." [--description="..."] [--external-id=ID] --json
308
+ hubdoc groups update <id> [--name=...] [--description=...] [--external-id=...] --json
309
+ hubdoc groups delete <id> --yes --json
310
+
311
+ # Group members (nested under `groups`)
312
+ hubdoc groups members ls --group=<group-id> --json
313
+ hubdoc groups members add --group=<group-id> \
314
+ [--user-external-id=EXT | --user-id=UUID | --user-email=EMAIL] --json
315
+ hubdoc groups members remove <membership-id> --group=<group-id> --yes --json
316
+
317
+ # Permissions (ACL: actor × permissible × level)
318
+ hubdoc permissions ls --on=<resource-id> --on-type=workspace|folder|file|share_link \
319
+ [--to=<actor-id>] [--to-type=user|group|contact] [--level=read|write|admin|owner|forbidden] --json
320
+ hubdoc permissions show <id> --json
321
+ hubdoc permissions grant \
322
+ --to=<actor-id> --to-type=user|group|contact \
323
+ --on=<resource-id> --on-type=workspace|folder|file|share_link \
324
+ --level=read|write|admin|owner|forbidden \
325
+ [--temporary] --json
326
+ hubdoc permissions revoke <id> --yes --json
327
+ ```
328
+
329
+ `--on-type` and `--to-type` accept short aliases (`workspace`, `group`, …)
330
+ or the fully-qualified class names (`Documents::Workspace`, `Group`, …).
331
+
332
+ > ⚠️ `users` commands are partially blocked by [corex#460](https://code.plugandwork.net/sinoia/corex/-/issues/460) — `GET /api/v1/users` ignores `per_page`/`page`/`q` server-side and caps silently at 50. `users ls --q` and `users show <email>` work best-effort on those 50 rows. Use UUIDs directly when possible. `groups members add --user-email` is subject to the same limitation.
333
+
334
+ #### End-to-end example: create a team and share a workspace with it
335
+
336
+ Reproduces the real operation that motivated this section:
337
+
338
+ ```bash
339
+ # 1. Workspace + group
340
+ WS=$(hubdoc workspaces create --name="Informations techniques" --json | jq -r '.data.id')
341
+ GRP=$(hubdoc groups create --name="sysadmins" --description="System admins" --json | jq -r '.data.id')
342
+
343
+ # 2. Add members (auto-resolves external_id from email when possible)
344
+ for email in \
345
+ olivier.dirrenberger@sinoia.fr \
346
+ benoit.nicolaeff@plugandwork.fr \
347
+ gaetan.marquet@plugandwork.fr ; do
348
+ hubdoc groups members add --group="$GRP" --user-email="$email" --json
349
+ done
350
+
351
+ # 3. Upload a sensitive file into the workspace
352
+ FILE=$(hubdoc files upload ./creds.txt --workspace="$WS" --json | jq -r '.data.id')
353
+
354
+ # 4. Grant admin permission to the group on the whole workspace
355
+ hubdoc permissions grant \
356
+ --to="$GRP" --to-type=group \
357
+ --on="$WS" --on-type=workspace \
358
+ --level=admin --json
359
+
360
+ # 5. Audit
361
+ hubdoc permissions ls --on="$WS" --on-type=workspace --json \
362
+ | jq '.[] | {actor: .actor.display_name, level}'
363
+ ```
364
+
294
365
  ## Out of scope (planned)
295
366
 
296
367
  - `hubdoc templates create|update|delete` — depends on corex#417 backend
297
368
  extension (the controller is read-only today). The CLI will follow once
298
369
  the API is in place.
299
- - `hubdoc permissions ...` — to be scoped if/when demand confirms.
300
370
  - JSON-API for PAT management (`hubdoc auth tokens ls/revoke`) — the
301
371
  Corex PAT controller is Inertia/web-only today.
302
372
  - `/api/v1/me` — not yet shipped; `whoami` falls back to JWT decode.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sinoia/hubdoc-tools",
3
- "version": "1.5.1",
3
+ "version": "1.6.1",
4
4
  "description": "Professional command-line tool for HubDoc document management and bulk import/export",
5
5
  "main": "cli.js",
6
6
  "bin": {