@sinoia/hubdoc-tools 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/cli.js +551 -70
  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) {
@@ -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;
@@ -84571,7 +84577,7 @@ var CsvManager = class {
84571
84577
  })).on("data", (data) => {
84572
84578
  try {
84573
84579
  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);
84580
+ const permissions2 = this.parsePermissions(permissionsStr);
84575
84581
  const cleanValue = (value2) => {
84576
84582
  if (value2 === void 0 || value2 === null) return void 0;
84577
84583
  const trimmed = String(value2).trim();
@@ -84584,7 +84590,7 @@ var CsvManager = class {
84584
84590
  metadata: this.parseMetadata(
84585
84591
  data["Metadata (key: value, parent.child: value)"] || data["Metadata (JSON)"] || data.Metadata || data.metadata
84586
84592
  ),
84587
- permissions,
84593
+ permissions: permissions2,
84588
84594
  autoClassify: this.parseBoolean(data["Auto Classify"] || data.autoClassify),
84589
84595
  status: data["Status"] || data.status || "pending",
84590
84596
  progress: parseFloat(data["Progress (%)"] || data.progress || "0"),
@@ -84753,18 +84759,18 @@ var CsvManager = class {
84753
84759
  }
84754
84760
  } else if (type === "groups") {
84755
84761
  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];
84762
+ const groups2 = parts2[1].split(",").map((g) => g.trim()).filter((g) => g);
84763
+ if (groups2.length > 0) {
84764
+ readPermissions.groups = [...readPermissions.groups || [], ...groups2];
84759
84765
  }
84760
84766
  } else if (parts2.length === 3) {
84761
84767
  const level = parts2[1];
84762
- const groups = parts2[2].split(",").map((g) => g.trim()).filter((g) => g);
84763
- if (groups.length > 0) {
84768
+ const groups2 = parts2[2].split(",").map((g) => g.trim()).filter((g) => g);
84769
+ if (groups2.length > 0) {
84764
84770
  if (level === "w") {
84765
- writePermissions.groups = [...writePermissions.groups || [], ...groups];
84771
+ writePermissions.groups = [...writePermissions.groups || [], ...groups2];
84766
84772
  } else {
84767
- readPermissions.groups = [...readPermissions.groups || [], ...groups];
84773
+ readPermissions.groups = [...readPermissions.groups || [], ...groups2];
84768
84774
  }
84769
84775
  }
84770
84776
  }
@@ -84915,20 +84921,20 @@ var CsvManager = class {
84915
84921
  * Serialize permissions in new format:
84916
84922
  * users:r:email1@ex.com,email2@ex.com | users:w:admin@ex.com | groups:Group Name | groups:w:Admin Group
84917
84923
  */
84918
- serializePermissions(permissions) {
84919
- if (!permissions) return "";
84924
+ serializePermissions(permissions2) {
84925
+ if (!permissions2) return "";
84920
84926
  const parts2 = [];
84921
- if (permissions.read?.users && permissions.read.users.length > 0) {
84922
- parts2.push(`users:r:${permissions.read.users.join(",")}`);
84927
+ if (permissions2.read?.users && permissions2.read.users.length > 0) {
84928
+ parts2.push(`users:r:${permissions2.read.users.join(",")}`);
84923
84929
  }
84924
- if (permissions.read?.groups && permissions.read.groups.length > 0) {
84925
- parts2.push(`groups:${permissions.read.groups.join(",")}`);
84930
+ if (permissions2.read?.groups && permissions2.read.groups.length > 0) {
84931
+ parts2.push(`groups:${permissions2.read.groups.join(",")}`);
84926
84932
  }
84927
- if (permissions.write?.users && permissions.write.users.length > 0) {
84928
- parts2.push(`users:w:${permissions.write.users.join(",")}`);
84933
+ if (permissions2.write?.users && permissions2.write.users.length > 0) {
84934
+ parts2.push(`users:w:${permissions2.write.users.join(",")}`);
84929
84935
  }
84930
- if (permissions.write?.groups && permissions.write.groups.length > 0) {
84931
- parts2.push(`groups:w:${permissions.write.groups.join(",")}`);
84936
+ if (permissions2.write?.groups && permissions2.write.groups.length > 0) {
84937
+ parts2.push(`groups:w:${permissions2.write.groups.join(",")}`);
84932
84938
  }
84933
84939
  return parts2.join(" | ");
84934
84940
  }
@@ -85861,7 +85867,7 @@ var CsvManager2 = class {
85861
85867
  })).on("data", (data) => {
85862
85868
  try {
85863
85869
  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);
85870
+ const permissions2 = this.parsePermissions(permissionsStr);
85865
85871
  const cleanValue = (value2) => {
85866
85872
  if (value2 === void 0 || value2 === null) return void 0;
85867
85873
  const trimmed = String(value2).trim();
@@ -85874,7 +85880,7 @@ var CsvManager2 = class {
85874
85880
  metadata: this.parseMetadata(
85875
85881
  data["Metadata (key: value, parent.child: value)"] || data["Metadata (JSON)"] || data.Metadata || data.metadata
85876
85882
  ),
85877
- permissions,
85883
+ permissions: permissions2,
85878
85884
  autoClassify: this.parseBoolean(data["Auto Classify"] || data.autoClassify),
85879
85885
  status: data["Status"] || data.status || "pending",
85880
85886
  progress: parseFloat(data["Progress (%)"] || data.progress || "0"),
@@ -86043,18 +86049,18 @@ var CsvManager2 = class {
86043
86049
  }
86044
86050
  } else if (type === "groups") {
86045
86051
  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];
86052
+ const groups2 = parts2[1].split(",").map((g) => g.trim()).filter((g) => g);
86053
+ if (groups2.length > 0) {
86054
+ readPermissions.groups = [...readPermissions.groups || [], ...groups2];
86049
86055
  }
86050
86056
  } else if (parts2.length === 3) {
86051
86057
  const level = parts2[1];
86052
- const groups = parts2[2].split(",").map((g) => g.trim()).filter((g) => g);
86053
- if (groups.length > 0) {
86058
+ const groups2 = parts2[2].split(",").map((g) => g.trim()).filter((g) => g);
86059
+ if (groups2.length > 0) {
86054
86060
  if (level === "w") {
86055
- writePermissions.groups = [...writePermissions.groups || [], ...groups];
86061
+ writePermissions.groups = [...writePermissions.groups || [], ...groups2];
86056
86062
  } else {
86057
- readPermissions.groups = [...readPermissions.groups || [], ...groups];
86063
+ readPermissions.groups = [...readPermissions.groups || [], ...groups2];
86058
86064
  }
86059
86065
  }
86060
86066
  }
@@ -86205,20 +86211,20 @@ var CsvManager2 = class {
86205
86211
  * Serialize permissions in new format:
86206
86212
  * users:r:email1@ex.com,email2@ex.com | users:w:admin@ex.com | groups:Group Name | groups:w:Admin Group
86207
86213
  */
86208
- serializePermissions(permissions) {
86209
- if (!permissions) return "";
86214
+ serializePermissions(permissions2) {
86215
+ if (!permissions2) return "";
86210
86216
  const parts2 = [];
86211
- if (permissions.read?.users && permissions.read.users.length > 0) {
86212
- parts2.push(`users:r:${permissions.read.users.join(",")}`);
86217
+ if (permissions2.read?.users && permissions2.read.users.length > 0) {
86218
+ parts2.push(`users:r:${permissions2.read.users.join(",")}`);
86213
86219
  }
86214
- if (permissions.read?.groups && permissions.read.groups.length > 0) {
86215
- parts2.push(`groups:${permissions.read.groups.join(",")}`);
86220
+ if (permissions2.read?.groups && permissions2.read.groups.length > 0) {
86221
+ parts2.push(`groups:${permissions2.read.groups.join(",")}`);
86216
86222
  }
86217
- if (permissions.write?.users && permissions.write.users.length > 0) {
86218
- parts2.push(`users:w:${permissions.write.users.join(",")}`);
86223
+ if (permissions2.write?.users && permissions2.write.users.length > 0) {
86224
+ parts2.push(`users:w:${permissions2.write.users.join(",")}`);
86219
86225
  }
86220
- if (permissions.write?.groups && permissions.write.groups.length > 0) {
86221
- parts2.push(`groups:w:${permissions.write.groups.join(",")}`);
86226
+ if (permissions2.write?.groups && permissions2.write.groups.length > 0) {
86227
+ parts2.push(`groups:w:${permissions2.write.groups.join(",")}`);
86222
86228
  }
86223
86229
  return parts2.join(" | ");
86224
86230
  }
@@ -86746,13 +86752,13 @@ var PluginImportService = class {
86746
86752
  * Group mappings by connection ID
86747
86753
  */
86748
86754
  groupMappingsByConnection(mappings) {
86749
- return mappings.reduce((groups, mapping) => {
86755
+ return mappings.reduce((groups2, mapping) => {
86750
86756
  const { connectionId } = mapping;
86751
- if (!groups[connectionId]) {
86752
- groups[connectionId] = [];
86757
+ if (!groups2[connectionId]) {
86758
+ groups2[connectionId] = [];
86753
86759
  }
86754
- groups[connectionId].push(mapping);
86755
- return groups;
86760
+ groups2[connectionId].push(mapping);
86761
+ return groups2;
86756
86762
  }, {});
86757
86763
  }
86758
86764
  /**
@@ -87400,6 +87406,8 @@ async function buildApiContext(out) {
87400
87406
  documents: new DocumentsApi(configuration, baseUrl, axiosInstance),
87401
87407
  chunkedUploads: new ChunkedUploadsApi(configuration, baseUrl, axiosInstance),
87402
87408
  users: new UsersApi(configuration, baseUrl, axiosInstance),
87409
+ groups: new GroupsApi(configuration, baseUrl, axiosInstance),
87410
+ permissions: new PermissionsApi(configuration, baseUrl, axiosInstance),
87403
87411
  baseUrl,
87404
87412
  token: token2,
87405
87413
  tokenSource: resolved.source
@@ -89008,10 +89016,437 @@ async function handleTemplatesShow(idOrKey, opts) {
89008
89016
  }
89009
89017
  }
89010
89018
 
89019
+ // apps/cli/cli/handlers/user-handlers.ts
89020
+ var USER_COLUMNS = [
89021
+ { header: "ID", key: "id" },
89022
+ { header: "Email", key: "email_address" },
89023
+ { header: "Name", key: "display_name" },
89024
+ { header: "Role", key: "role" },
89025
+ { header: "External", key: "external_id", maxWidth: 24 }
89026
+ ];
89027
+ function matchesQuery(user, q) {
89028
+ const needle = q.toLowerCase();
89029
+ const haystack = [
89030
+ user.email_address,
89031
+ user.first_name,
89032
+ user.last_name,
89033
+ user.display_name,
89034
+ user.external_id
89035
+ ].filter(Boolean).join(" ").toLowerCase();
89036
+ return haystack.includes(needle);
89037
+ }
89038
+ async function handleUsersList(opts) {
89039
+ const out = output(opts);
89040
+ const ctx = await buildApiContext(out);
89041
+ try {
89042
+ const res = await ctx.users.apiV1UsersGet(
89043
+ void 0,
89044
+ void 0,
89045
+ opts.limit ? Number(opts.limit) : void 0
89046
+ );
89047
+ let items = res.data ?? [];
89048
+ if (opts.q) items = items.filter((u) => matchesQuery(u, opts.q));
89049
+ if (items.length === 50 && !opts.q) {
89050
+ out.note("server may have capped result at 50 (see corex#460) \u2014 refine with --q to filter");
89051
+ }
89052
+ out.list(items, USER_COLUMNS);
89053
+ } catch (err) {
89054
+ out.fail(err);
89055
+ }
89056
+ }
89057
+ async function handleUsersShow(idOrEmail, opts) {
89058
+ const out = output(opts);
89059
+ const ctx = await buildApiContext(out);
89060
+ const looksLikeEmail = idOrEmail.includes("@");
89061
+ let found;
89062
+ try {
89063
+ if (!looksLikeEmail) {
89064
+ const res2 = await ctx.users.apiV1UsersIdGet(idOrEmail);
89065
+ out.show(res2.data, { title: `User ${idOrEmail}` });
89066
+ return;
89067
+ }
89068
+ const res = await ctx.users.apiV1UsersGet();
89069
+ const items = res.data ?? [];
89070
+ found = items.find((u) => (u.email_address || "").toLowerCase() === idOrEmail.toLowerCase());
89071
+ } catch (err) {
89072
+ out.fail(err);
89073
+ return;
89074
+ }
89075
+ if (!found) {
89076
+ out.fail(
89077
+ new Error(
89078
+ `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.`
89079
+ )
89080
+ );
89081
+ }
89082
+ out.show(found, { title: `User ${idOrEmail}` });
89083
+ }
89084
+
89085
+ // apps/cli/cli/handlers/group-handlers.ts
89086
+ var GROUP_COLUMNS = [
89087
+ { header: "ID", key: "id" },
89088
+ { header: "Name", key: "name" },
89089
+ { header: "Description", key: "description", maxWidth: 40 },
89090
+ { header: "External", key: "external_id", maxWidth: 24 },
89091
+ { header: "Updated", key: "updated_at" }
89092
+ ];
89093
+ async function handleGroupsList(opts) {
89094
+ const out = output(opts);
89095
+ const ctx = await buildApiContext(out);
89096
+ try {
89097
+ const res = await ctx.groups.apiV1GroupsGet(
89098
+ void 0,
89099
+ void 0,
89100
+ opts.limit ? Number(opts.limit) : void 0,
89101
+ opts.page ? Number(opts.page) : void 0
89102
+ );
89103
+ let items = res.data ?? [];
89104
+ if (opts.q) {
89105
+ const needle = opts.q.toLowerCase();
89106
+ items = items.filter(
89107
+ (g) => (g.name || "").toLowerCase().includes(needle) || (g.description || "").toLowerCase().includes(needle)
89108
+ );
89109
+ }
89110
+ out.list(items, GROUP_COLUMNS);
89111
+ } catch (err) {
89112
+ out.fail(err);
89113
+ }
89114
+ }
89115
+ async function handleGroupsShow(id, opts) {
89116
+ const out = output(opts);
89117
+ const ctx = await buildApiContext(out);
89118
+ try {
89119
+ const res = await ctx.groups.apiV1GroupsIdGet(id);
89120
+ out.show(res.data, { title: `Group ${id}` });
89121
+ } catch (err) {
89122
+ out.fail(err);
89123
+ }
89124
+ }
89125
+ async function handleGroupsCreate(opts) {
89126
+ const out = output(opts);
89127
+ const ctx = await buildApiContext(out);
89128
+ const mutation = { name: opts.name };
89129
+ if (opts.description !== void 0) mutation.description = opts.description;
89130
+ if (opts.externalId !== void 0) mutation.external_id = opts.externalId;
89131
+ try {
89132
+ const res = await ctx.groups.apiV1GroupsPost(
89133
+ ApiV1GroupsPostContentTypeEnum.ApplicationJson,
89134
+ mutation
89135
+ );
89136
+ out.ok(`Group created (id=${res.data.id})`, res.data);
89137
+ } catch (err) {
89138
+ out.fail(err);
89139
+ }
89140
+ }
89141
+ async function handleGroupsUpdate(id, opts) {
89142
+ const out = output(opts);
89143
+ const ctx = await buildApiContext(out);
89144
+ if (opts.name === void 0 && opts.description === void 0 && opts.externalId === void 0) {
89145
+ out.fail(new Error("No fields to update"));
89146
+ }
89147
+ let nameValue = opts.name;
89148
+ if (nameValue === void 0) {
89149
+ try {
89150
+ const cur = await ctx.groups.apiV1GroupsIdGet(id);
89151
+ nameValue = cur.data.name;
89152
+ } catch (err) {
89153
+ out.fail(err);
89154
+ return;
89155
+ }
89156
+ }
89157
+ const mutation = { name: nameValue };
89158
+ if (opts.description !== void 0) mutation.description = opts.description;
89159
+ if (opts.externalId !== void 0) mutation.external_id = opts.externalId;
89160
+ try {
89161
+ const res = await ctx.groups.apiV1GroupsIdPatch(
89162
+ id,
89163
+ ApiV1GroupsIdPatchContentTypeEnum.ApplicationJson,
89164
+ mutation
89165
+ );
89166
+ out.ok(`Group ${id} updated`, res.data);
89167
+ } catch (err) {
89168
+ out.fail(err);
89169
+ }
89170
+ }
89171
+ async function handleGroupsDelete(id, opts) {
89172
+ const out = output(opts);
89173
+ const ctx = await buildApiContext(out);
89174
+ if (!opts.yes && !opts.json) {
89175
+ const { confirm } = await lib_default.prompt([
89176
+ {
89177
+ type: "confirm",
89178
+ name: "confirm",
89179
+ message: `Delete group ${id}? This removes all its members and permissions.`,
89180
+ default: false
89181
+ }
89182
+ ]);
89183
+ if (!confirm) {
89184
+ out.note("Aborted");
89185
+ return;
89186
+ }
89187
+ } else if (!opts.yes && opts.json) {
89188
+ out.fail(new Error("Refusing to delete without --yes in --json mode"));
89189
+ }
89190
+ try {
89191
+ await ctx.groups.apiV1GroupsIdDelete(id);
89192
+ out.ok(`Group ${id} deleted`);
89193
+ } catch (err) {
89194
+ out.fail(err);
89195
+ }
89196
+ }
89197
+
89198
+ // apps/cli/cli/handlers/group-member-handlers.ts
89199
+ var MEMBER_COLUMNS = [
89200
+ { header: "Membership ID", key: "id" },
89201
+ { header: "Member ID", key: "member_id" },
89202
+ { header: "Type", key: "member_type" },
89203
+ { header: "External", key: "user_external_id", maxWidth: 24 },
89204
+ { header: "Role", key: "role" }
89205
+ ];
89206
+ function membersBase(groupId) {
89207
+ return `/api/v1/groups/${encodeURIComponent(groupId)}/members`;
89208
+ }
89209
+ async function handleGroupMembersList(opts) {
89210
+ const out = output(opts);
89211
+ const ctx = await buildApiContext(out);
89212
+ try {
89213
+ const res = await ctx.axios.get(membersBase(opts.group));
89214
+ out.list(res.data ?? [], MEMBER_COLUMNS);
89215
+ } catch (err) {
89216
+ out.fail(err);
89217
+ }
89218
+ }
89219
+ async function resolveExternalId(ctx, out, opts) {
89220
+ if (opts.userExternalId) return opts.userExternalId;
89221
+ if (opts.userId) {
89222
+ let ext2;
89223
+ try {
89224
+ const res = await ctx.users.apiV1UsersIdGet(opts.userId);
89225
+ ext2 = res.data?.external_id;
89226
+ } catch (err) {
89227
+ out.fail(err);
89228
+ return "";
89229
+ }
89230
+ if (!ext2) {
89231
+ out.fail(
89232
+ new Error(
89233
+ `User ${opts.userId} has no external_id \u2014 server requires it to create a membership.`
89234
+ )
89235
+ );
89236
+ }
89237
+ return ext2;
89238
+ }
89239
+ if (opts.userEmail) {
89240
+ let found;
89241
+ try {
89242
+ const res = await ctx.users.apiV1UsersGet();
89243
+ const items = res.data ?? [];
89244
+ found = items.find(
89245
+ (u) => (u.email_address || "").toLowerCase() === opts.userEmail.toLowerCase()
89246
+ );
89247
+ } catch (err) {
89248
+ out.fail(err);
89249
+ return "";
89250
+ }
89251
+ if (!found) {
89252
+ out.fail(
89253
+ new Error(
89254
+ `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.`
89255
+ )
89256
+ );
89257
+ }
89258
+ if (!found.external_id) {
89259
+ out.fail(
89260
+ new Error(
89261
+ `User '${opts.userEmail}' has no external_id \u2014 server requires it to create a membership.`
89262
+ )
89263
+ );
89264
+ }
89265
+ return found.external_id;
89266
+ }
89267
+ out.fail(
89268
+ new Error("Pass one of --user-external-id, --user-id, or --user-email to identify the user.")
89269
+ );
89270
+ return "";
89271
+ }
89272
+ async function handleGroupMembersAdd(opts) {
89273
+ const out = output(opts);
89274
+ const ctx = await buildApiContext(out);
89275
+ const externalId = await resolveExternalId(ctx, out, opts);
89276
+ try {
89277
+ const res = await ctx.axios.post(membersBase(opts.group), { user_external_id: externalId });
89278
+ out.ok(`Membership created (id=${res.data.id})`, res.data);
89279
+ } catch (err) {
89280
+ out.fail(err);
89281
+ }
89282
+ }
89283
+ async function handleGroupMembersRemove(membershipId, opts) {
89284
+ const out = output(opts);
89285
+ const ctx = await buildApiContext(out);
89286
+ if (!opts.yes && !opts.json) {
89287
+ const { confirm } = await lib_default.prompt([
89288
+ {
89289
+ type: "confirm",
89290
+ name: "confirm",
89291
+ message: `Remove membership ${membershipId} from group ${opts.group}?`,
89292
+ default: false
89293
+ }
89294
+ ]);
89295
+ if (!confirm) {
89296
+ out.note("Aborted");
89297
+ return;
89298
+ }
89299
+ } else if (!opts.yes && opts.json) {
89300
+ out.fail(new Error("Refusing to remove without --yes in --json mode"));
89301
+ }
89302
+ try {
89303
+ await ctx.axios.delete(`${membersBase(opts.group)}/${encodeURIComponent(membershipId)}`);
89304
+ out.ok(`Membership ${membershipId} removed`);
89305
+ } catch (err) {
89306
+ out.fail(err);
89307
+ }
89308
+ }
89309
+
89310
+ // apps/cli/cli/handlers/permission-handlers.ts
89311
+ var PERM_COLUMNS = [
89312
+ { header: "ID", key: "id" },
89313
+ { header: "Actor", key: "actor_type", format: (v, row) => `${v}:${row.actor?.display_name ?? row.actor_id?.substring(0, 8)}` },
89314
+ { header: "Level", key: "level" },
89315
+ { header: "On", key: "permissible_type", format: (v) => v?.replace(/^Documents::/, "") ?? "" },
89316
+ { header: "Resource ID", key: "permissible_id" },
89317
+ { header: "Updated", key: "updated_at" }
89318
+ ];
89319
+ var VALID_LEVELS = /* @__PURE__ */ new Set(["read", "write", "admin", "owner", "forbidden"]);
89320
+ var PERMISSIBLE_ALIASES = {
89321
+ workspace: "Documents::Workspace",
89322
+ folder: "Documents::Folder",
89323
+ file: "Documents::File",
89324
+ share_link: "Documents::ShareLink"
89325
+ };
89326
+ var ACTOR_ALIASES = {
89327
+ user: "User",
89328
+ group: "Group",
89329
+ contact: "Contact"
89330
+ };
89331
+ function normalizePermissibleType(input, out) {
89332
+ const lower = input.toLowerCase();
89333
+ if (PERMISSIBLE_ALIASES[lower]) return PERMISSIBLE_ALIASES[lower];
89334
+ if (input.startsWith("Documents::")) return input;
89335
+ out.fail(
89336
+ new Error(
89337
+ `Invalid --on-type: '${input}'. Use workspace/folder/file/share_link or a fully-qualified Documents::\u2026 class name.`
89338
+ )
89339
+ );
89340
+ return "";
89341
+ }
89342
+ function normalizeActorType(input, out) {
89343
+ const lower = input.toLowerCase();
89344
+ if (ACTOR_ALIASES[lower]) return ACTOR_ALIASES[lower];
89345
+ if (["User", "Group", "Contact"].includes(input)) return input;
89346
+ out.fail(new Error(`Invalid --to-type: '${input}'. Use user/group/contact.`));
89347
+ return "";
89348
+ }
89349
+ async function handlePermissionsList(opts) {
89350
+ const out = output(opts);
89351
+ const ctx = await buildApiContext(out);
89352
+ if (!opts.on) {
89353
+ out.fail(
89354
+ new Error(
89355
+ "--on=<resource-id> is required (the server rejects listing without permissible_id/type)."
89356
+ )
89357
+ );
89358
+ }
89359
+ const onType = opts.onType ? normalizePermissibleType(opts.onType, out) : "Documents::Workspace";
89360
+ try {
89361
+ const res = await ctx.axios.get("/api/v1/permissions", {
89362
+ params: {
89363
+ permissible_id: opts.on,
89364
+ permissible_type: onType
89365
+ }
89366
+ });
89367
+ let items = res.data ?? [];
89368
+ if (opts.to) items = items.filter((p) => p.actor_id === opts.to);
89369
+ if (opts.toType) {
89370
+ const norm = normalizeActorType(opts.toType, out);
89371
+ items = items.filter((p) => p.actor_type === norm);
89372
+ }
89373
+ if (opts.level) items = items.filter((p) => p.level === opts.level);
89374
+ out.list(items, PERM_COLUMNS);
89375
+ } catch (err) {
89376
+ out.fail(err);
89377
+ }
89378
+ }
89379
+ async function handlePermissionsGrant(opts) {
89380
+ const out = output(opts);
89381
+ const ctx = await buildApiContext(out);
89382
+ if (!VALID_LEVELS.has(opts.level)) {
89383
+ out.fail(
89384
+ new Error(
89385
+ `Invalid --level: '${opts.level}'. Expected one of: ${[...VALID_LEVELS].join(", ")}`
89386
+ )
89387
+ );
89388
+ }
89389
+ const actorType = normalizeActorType(opts.toType, out);
89390
+ const permissibleType = normalizePermissibleType(opts.onType, out);
89391
+ try {
89392
+ const body = {
89393
+ actor_id: opts.to,
89394
+ actor_type: actorType,
89395
+ permissible_id: opts.on,
89396
+ permissible_type: permissibleType,
89397
+ level: opts.level
89398
+ };
89399
+ if (opts.temporary) body.temporary = true;
89400
+ const res = await ctx.axios.post("/api/v1/permissions", body);
89401
+ out.ok(
89402
+ `Permission granted (${actorType} \u2192 ${opts.level} on ${permissibleType})`,
89403
+ res.data
89404
+ );
89405
+ } catch (err) {
89406
+ out.fail(err);
89407
+ }
89408
+ }
89409
+ async function handlePermissionsRevoke(id, opts) {
89410
+ const out = output(opts);
89411
+ const ctx = await buildApiContext(out);
89412
+ if (!opts.yes && !opts.json) {
89413
+ const { confirm } = await lib_default.prompt([
89414
+ {
89415
+ type: "confirm",
89416
+ name: "confirm",
89417
+ message: `Revoke permission ${id}?`,
89418
+ default: false
89419
+ }
89420
+ ]);
89421
+ if (!confirm) {
89422
+ out.note("Aborted");
89423
+ return;
89424
+ }
89425
+ } else if (!opts.yes && opts.json) {
89426
+ out.fail(new Error("Refusing to revoke without --yes in --json mode"));
89427
+ }
89428
+ try {
89429
+ await ctx.permissions.apiV1PermissionsIdDelete(id);
89430
+ out.ok(`Permission ${id} revoked`);
89431
+ } catch (err) {
89432
+ out.fail(err);
89433
+ }
89434
+ }
89435
+ async function handlePermissionsShow(id, opts) {
89436
+ const out = output(opts);
89437
+ const ctx = await buildApiContext(out);
89438
+ try {
89439
+ const res = await ctx.permissions.apiV1PermissionsIdGet(id);
89440
+ out.show(res.data, { title: `Permission ${id}` });
89441
+ } catch (err) {
89442
+ out.fail(err);
89443
+ }
89444
+ }
89445
+
89011
89446
  // apps/cli/cli.ts
89012
89447
  var getVersion = () => {
89013
89448
  try {
89014
- if (true) return "1.5.1";
89449
+ if (true) return "1.6.0";
89015
89450
  } catch {
89016
89451
  }
89017
89452
  for (const candidate of [
@@ -89241,6 +89676,52 @@ templates.command("ls").description("List active templates").action(async (optio
89241
89676
  templates.command("show <id-or-key>").description("Show a template (full default_parts + page_settings)").action(async (idOrKey, options) => {
89242
89677
  await handleTemplatesShow(idOrKey, withGlobals(options));
89243
89678
  });
89679
+ var users = program2.command("users").description("Look up users (read-only)");
89680
+ 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) => {
89681
+ await handleUsersList(withGlobals(options));
89682
+ });
89683
+ 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) => {
89684
+ await handleUsersShow(id, withGlobals(options));
89685
+ });
89686
+ var groups = program2.command("groups").description("Manage groups and members");
89687
+ groups.command("ls").description("List groups").option("--q <string>", "Filter by name/description (client-side)").option("--limit <n>").option("--page <n>").action(async (options) => {
89688
+ await handleGroupsList(withGlobals(options));
89689
+ });
89690
+ groups.command("show <id>").description("Show a group by id").action(async (id, options) => {
89691
+ await handleGroupsShow(id, withGlobals(options));
89692
+ });
89693
+ 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) => {
89694
+ await handleGroupsCreate(withGlobals(options));
89695
+ });
89696
+ groups.command("update <id>").description("Update group fields").option("--name <name>").option("--description <text>").option("--external-id <id>").action(async (id, options) => {
89697
+ await handleGroupsUpdate(id, withGlobals(options));
89698
+ });
89699
+ groups.command("delete <id>").description("Delete a group (cascades to memberships and permissions)").option("--yes", "Skip the interactive confirmation").action(async (id, options) => {
89700
+ await handleGroupsDelete(id, withGlobals(options));
89701
+ });
89702
+ var members = groups.command("members").description("Manage the membership of a group");
89703
+ members.command("ls").description("List members of a group").requiredOption("--group <id>", "Parent group id").action(async (options) => {
89704
+ await handleGroupMembersList(withGlobals(options));
89705
+ });
89706
+ 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) => {
89707
+ await handleGroupMembersAdd(withGlobals(options));
89708
+ });
89709
+ 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) => {
89710
+ await handleGroupMembersRemove(id, withGlobals(options));
89711
+ });
89712
+ var permissions = program2.command("permissions").description("Manage ACL: grant / list / revoke permissions on documents resources");
89713
+ 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) => {
89714
+ await handlePermissionsList(withGlobals(options));
89715
+ });
89716
+ permissions.command("show <id>").description("Show a permission by id").action(async (id, options) => {
89717
+ await handlePermissionsShow(id, withGlobals(options));
89718
+ });
89719
+ 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) => {
89720
+ await handlePermissionsGrant(withGlobals(options));
89721
+ });
89722
+ permissions.command("revoke <id>").description("Revoke a permission by id").option("--yes", "Skip the interactive confirmation").action(async (id, options) => {
89723
+ await handlePermissionsRevoke(id, withGlobals(options));
89724
+ });
89244
89725
  program2.parse();
89245
89726
  /*! Bundled license information:
89246
89727
 
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.0",
4
4
  "description": "Professional command-line tool for HubDoc document management and bulk import/export",
5
5
  "main": "cli.js",
6
6
  "bin": {