pageship 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -683,6 +683,26 @@ const documentsUpdateVisibility = (options) => (options.client ?? client).patch(
683
683
  ...options.headers
684
684
  }
685
685
  });
686
+ const viewersList = (options) => (options.client ?? client).get({
687
+ url: "/api/v1/documents/{id}/viewers",
688
+ ...options
689
+ });
690
+ const viewersAdd = (options) => (options.client ?? client).post({
691
+ url: "/api/v1/documents/{id}/viewers",
692
+ ...options,
693
+ headers: {
694
+ "Content-Type": "application/json",
695
+ ...options.headers
696
+ }
697
+ });
698
+ const viewersRemove = (options) => (options.client ?? client).delete({
699
+ url: "/api/v1/documents/{id}/viewers/{githubAccountId}",
700
+ ...options
701
+ });
702
+ const githubGetUser = (options) => (options.client ?? client).get({
703
+ url: "/api/v1/github/users/{login}",
704
+ ...options
705
+ });
686
706
  //#endregion
687
707
  //#region ../../packages/api-client/src/generated/workspace-domain.gen.ts
688
708
  /** Every workspace address and document URL is a subdomain of this domain. */
@@ -760,14 +780,16 @@ function validTitle(value) {
760
780
  }
761
781
  function normalizeDocument(value) {
762
782
  if (!record(value) || typeof value.slug !== "string") return;
763
- const normalized = value.title === void 0 ? {
783
+ const normalized = {
764
784
  ...value,
765
- title: value.slug
766
- } : value;
785
+ ...value.title === void 0 ? { title: value.slug } : {},
786
+ ...value.viewCount === void 0 ? { viewCount: 0 } : {},
787
+ ...value.sharedWith === void 0 ? { sharedWith: 0 } : {}
788
+ };
767
789
  return validDocument(normalized) ? normalized : void 0;
768
790
  }
769
791
  function validDocument(value) {
770
- return record(value) && isDocumentId(value.id) && typeof value.slug === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*(?![\s\S])/u.test(value.slug) && validTitle(value.title) && typeof value.sourcePath === "string" && value.sourcePath.length <= 1024 && /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)[^\x00-\x1f\x7f\\:]*\.[hH][tT][mM][lL]?(?![\s\S])/u.test(value.sourcePath) && (value.repository === null || isRepository(value.repository)) && (value.visibility === "private" || value.visibility === "public") && isRevisionId(value.revisionId) && typeof value.byteLength === "number" && Number.isSafeInteger(value.byteLength) && value.byteLength > 0 && typeof value.createdAt === "number" && Number.isSafeInteger(value.createdAt) && value.createdAt >= 0 && typeof value.publishedAt === "number" && Number.isSafeInteger(value.publishedAt) && value.publishedAt >= 0 && typeof value.url === "string" && new RegExp(`^https://[a-z0-9-]+\\.${workspaceDomainPattern}/d/${value.id}-${value.slug}$`, "u").test(value.url);
792
+ return record(value) && isDocumentId(value.id) && typeof value.slug === "string" && /^[a-z0-9]+(?:-[a-z0-9]+)*(?![\s\S])/u.test(value.slug) && validTitle(value.title) && typeof value.sourcePath === "string" && value.sourcePath.length <= 1024 && /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)[^\x00-\x1f\x7f\\:]*\.[hH][tT][mM][lL]?(?![\s\S])/u.test(value.sourcePath) && (value.repository === null || isRepository(value.repository)) && (value.visibility === "private" || value.visibility === "public") && isRevisionId(value.revisionId) && typeof value.byteLength === "number" && Number.isSafeInteger(value.byteLength) && value.byteLength > 0 && typeof value.createdAt === "number" && Number.isSafeInteger(value.createdAt) && value.createdAt >= 0 && typeof value.publishedAt === "number" && Number.isSafeInteger(value.publishedAt) && value.publishedAt >= 0 && typeof value.viewCount === "number" && Number.isSafeInteger(value.viewCount) && value.viewCount >= 0 && typeof value.sharedWith === "number" && Number.isSafeInteger(value.sharedWith) && value.sharedWith >= 0 && typeof value.url === "string" && new RegExp(`^https://[a-z0-9-]+\\.${workspaceDomainPattern}/d/${value.id}-${value.slug}$`, "u").test(value.url);
771
793
  }
772
794
  function validRevision(value, documentId) {
773
795
  return record(value) && isRevisionId(value.id) && typeof value.createdAt === "number" && Number.isSafeInteger(value.createdAt) && value.createdAt >= 0 && typeof value.byteLength === "number" && Number.isSafeInteger(value.byteLength) && value.byteLength > 0 && typeof value.isCurrent === "boolean" && (value.cliVersion === null || isCliVersion(value.cliVersion)) && typeof value.url === "string" && new RegExp(`^https://[a-z0-9-]+\\.${workspaceDomainPattern}/d/${documentId}-[a-z0-9]+(?:-[a-z0-9]+)*/revisions/${value.id}$`, "u").test(value.url);
@@ -913,9 +935,12 @@ async function updateDocumentVisibility(baseUrl, id, visibility, options = {}) {
913
935
  throw new Error("Could not update document visibility. Try again.", { cause: error });
914
936
  }
915
937
  const data = received;
916
- if (record(data) && data.status === "visibility_updated" && validDocument(data.document) && data.document.visibility === visibility) return {
938
+ const updated = record(data) && data.status === "visibility_updated" ? normalizeDocument(data.document) : void 0;
939
+ const removedViewers = record(data) && data.removedViewers !== void 0 ? data.removedViewers : 0;
940
+ if (updated?.visibility === visibility && typeof removedViewers === "number" && Number.isSafeInteger(removedViewers) && removedViewers >= 0) return {
917
941
  status: "visibility_updated",
918
- document: data.document
942
+ document: updated,
943
+ removedViewers
919
944
  };
920
945
  throw new Error("Invalid document visibility response.");
921
946
  }
@@ -957,6 +982,92 @@ async function publishDocument(baseUrl, input, options = {}) {
957
982
  if (record(data) && (data.status === "idempotency_conflict" || data.status === "address_required" || data.status === "unavailable" || data.status === "document_id_taken")) return { status: data.status };
958
983
  throw new Error("Invalid publication response. Retry with the same idempotency key.");
959
984
  }
985
+ /** Same GitHub username grammar as the Effect contract. */
986
+ function isGitHubLogin(value) {
987
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9-]{0,38}(?![\s\S])/u.test(value);
988
+ }
989
+ function isGitHubAccountId(value) {
990
+ return typeof value === "string" && /^[1-9][0-9]{0,19}(?![\s\S])/u.test(value);
991
+ }
992
+ function validViewer(value) {
993
+ return record(value) && isGitHubAccountId(value.githubAccountId) && isGitHubLogin(value.login) && (value.name === null || typeof value.name === "string") && typeof value.addedAt === "number" && Number.isSafeInteger(value.addedAt) && value.addedAt >= 0;
994
+ }
995
+ function validUserMatch(value) {
996
+ return record(value) && isGitHubAccountId(value.githubAccountId) && isGitHubLogin(value.login);
997
+ }
998
+ function validUserProfile(value) {
999
+ return record(value) && (value.name === null || typeof value.name === "string") && (value.type === "user" || value.type === "organization" || value.type === "other") && validUserMatch(value);
1000
+ }
1001
+ /**
1002
+ * One message per viewer refusal. The body's status picks it, not the HTTP code: a refused
1003
+ * GitHub token and a signed-out session are both 401.
1004
+ */
1005
+ const viewerRefusals = {
1006
+ signed_out: "Sign in to manage viewers.",
1007
+ uninvited: "Access unavailable.",
1008
+ forbidden: "A trusted browser origin is required.",
1009
+ unavailable: "Document unavailable.",
1010
+ document_public: "Make the document private before adding viewers.",
1011
+ github_user_not_found: "GitHub has no user with that username.",
1012
+ github_account_mismatch: "That username now belongs to a different GitHub account. Look it up again.",
1013
+ not_personal_account: "Only a personal GitHub account can be a viewer.",
1014
+ owner_account: "You already have access to your own document.",
1015
+ github_sign_in_required: "GitHub refused your sign-in. Sign in to Pageship again.",
1016
+ github_rate_limited: "GitHub is limiting requests. Try again in a minute.",
1017
+ github_unavailable: "GitHub is unavailable. Try again."
1018
+ };
1019
+ function isViewerRefusal(value) {
1020
+ return typeof value === "string" && Object.hasOwn(viewerRefusals, value);
1021
+ }
1022
+ function viewerFailed(response, error) {
1023
+ if (response && record(error) && isViewerRefusal(error.status)) throw new DocumentRequestError(error.status, viewerRefusals[error.status], response.status);
1024
+ failed(response, error);
1025
+ }
1026
+ async function getDocumentViewers(baseUrl, id, options = {}) {
1027
+ const { data, response, error } = await viewersList({
1028
+ ...requestOptions(baseUrl, options),
1029
+ path: { id }
1030
+ });
1031
+ if (!response?.ok) viewerFailed(response, error);
1032
+ if (data?.status === "listed" && Array.isArray(data.viewers) && data.viewers.every(validViewer)) return data;
1033
+ throw new Error("Invalid document viewer listing response.");
1034
+ }
1035
+ /** Adds the account the owner confirmed. The server looks the login up again before adding it. */
1036
+ async function addDocumentViewer(baseUrl, id, user, options = {}) {
1037
+ const { data, response, error } = await viewersAdd({
1038
+ ...changeOptions(baseUrl, options),
1039
+ path: { id },
1040
+ body: {
1041
+ githubAccountId: user.githubAccountId,
1042
+ login: user.login
1043
+ }
1044
+ });
1045
+ if (!response?.ok) viewerFailed(response, error);
1046
+ if (data?.status === "added" && validViewer(data.viewer) && data.viewer.githubAccountId === user.githubAccountId) return data;
1047
+ throw new Error("Invalid document viewer response.");
1048
+ }
1049
+ /** Removing an account that isn't a viewer succeeds. */
1050
+ async function removeDocumentViewer(baseUrl, id, githubAccountId, options = {}) {
1051
+ const { data, response, error } = await viewersRemove({
1052
+ ...changeOptions(baseUrl, options),
1053
+ path: {
1054
+ id,
1055
+ githubAccountId
1056
+ }
1057
+ });
1058
+ if (!response?.ok) viewerFailed(response, error);
1059
+ if (data?.status === "removed") return { status: "removed" };
1060
+ throw new Error("Invalid document viewer removal response.");
1061
+ }
1062
+ async function getGitHubUser(baseUrl, login, options = {}) {
1063
+ const { data, response, error } = await githubGetUser({
1064
+ ...requestOptions(baseUrl, options),
1065
+ path: { login }
1066
+ });
1067
+ if (!response?.ok) viewerFailed(response, error);
1068
+ if (data?.status === "found" && validUserProfile(data.user)) return data;
1069
+ throw new Error("Invalid GitHub user response.");
1070
+ }
960
1071
  //#endregion
961
1072
  //#region ../../packages/api-client/src/index.ts
962
1073
  async function getWorkspace(baseUrl, options = {}) {
@@ -971,7 +1082,17 @@ async function getWorkspace(baseUrl, options = {}) {
971
1082
  throwOnError: false
972
1083
  });
973
1084
  if (!response?.ok) throw new Error("Could not load your workspace.", { cause: error });
974
- if (data?.status === "signed_out" || data?.status === "uninvited") return { status: data.status };
1085
+ if (data?.status === "signed_out") return { status: data.status };
1086
+ if (data?.status === "uninvited") {
1087
+ const user = data.user;
1088
+ return user && typeof user.name === "string" && (user.login === void 0 || typeof user.login === "string") ? {
1089
+ status: data.status,
1090
+ user: {
1091
+ name: user.name,
1092
+ ...user.login ? { login: user.login } : {}
1093
+ }
1094
+ } : { status: data.status };
1095
+ }
975
1096
  if (data?.status === "admitted" && typeof data.user?.id === "string" && data.user.id.length > 0 && typeof data.user?.name === "string" && (data.user.login === void 0 || typeof data.user.login === "string") && (data.user.githubAccountId === void 0 || typeof data.user.githubAccountId === "string" && /^[1-9]\d*$/u.test(data.user.githubAccountId)) && typeof data.workspace?.id === "string" && data.workspace.id.length > 0 && (data.address === void 0 || data.address === null || isWorkspaceAddress(data.address))) return data;
976
1097
  throw new Error("Invalid workspace response.");
977
1098
  }
@@ -1325,6 +1446,7 @@ const outcomes = {
1325
1446
  downloaded: "succeeded",
1326
1447
  visibility_updated: "succeeded",
1327
1448
  restored: "succeeded",
1449
+ viewers_updated: "succeeded",
1328
1450
  skills_installed: "succeeded",
1329
1451
  authorization_pending: "pending",
1330
1452
  expired: "pending",
@@ -1336,6 +1458,7 @@ const outcomes = {
1336
1458
  repository_mismatch: "pending",
1337
1459
  marker_unavailable: "pending",
1338
1460
  revision_conflict: "pending",
1461
+ github_sign_in_required: "pending",
1339
1462
  cancelled: "pending",
1340
1463
  usage_error: "failed",
1341
1464
  internal_error: "failed",
@@ -1359,7 +1482,15 @@ const outcomes = {
1359
1482
  idempotency_conflict: "failed",
1360
1483
  document_id_taken: "failed",
1361
1484
  publication_action_unsupported: "failed",
1362
- skills_modified: "failed"
1485
+ skills_modified: "failed",
1486
+ ambiguous_viewer: "failed",
1487
+ document_public: "failed",
1488
+ github_user_not_found: "failed",
1489
+ github_account_mismatch: "failed",
1490
+ not_personal_account: "failed",
1491
+ owner_account: "failed",
1492
+ github_rate_limited: "failed",
1493
+ github_unavailable: "failed"
1363
1494
  };
1364
1495
  const isStatus = (value) => Object.hasOwn(outcomes, value);
1365
1496
  const commandNames = [
@@ -1371,6 +1502,7 @@ const commandNames = [
1371
1502
  "history",
1372
1503
  "download",
1373
1504
  "visibility",
1505
+ "viewers",
1374
1506
  "restore",
1375
1507
  "skills"
1376
1508
  ];
@@ -1422,7 +1554,7 @@ function commandLine(command, args) {
1422
1554
  }
1423
1555
  //#endregion
1424
1556
  //#region package.json
1425
- var version = "0.4.1";
1557
+ var version = "0.5.0";
1426
1558
  //#endregion
1427
1559
  //#region src/publish.ts
1428
1560
  const execute = promisify(execFile);
@@ -2142,6 +2274,25 @@ async function identify(serverValue, signal) {
2142
2274
  }
2143
2275
  //#endregion
2144
2276
  //#region src/documents.ts
2277
+ /** `1 viewer`, `2 viewers`. */
2278
+ const viewerCount = (count) => `${count} viewer${count === 1 ? "" : "s"}`;
2279
+ /** The result for a request the server refused. `login` signs in again. */
2280
+ function refusal(error, login) {
2281
+ const status = error.status;
2282
+ if (status === "signed_out") return {
2283
+ status,
2284
+ message: `Your credential was revoked, expired or is invalid. Run ${commandLine(login.command, login.args)} to sign in again.`,
2285
+ next: login
2286
+ };
2287
+ if (status === "uninvited") return {
2288
+ status,
2289
+ message: "This account is not invited. Contact the server owner."
2290
+ };
2291
+ return {
2292
+ status,
2293
+ message: error.message
2294
+ };
2295
+ }
2145
2296
  /** Checks compatibility and the stored credential; the server enforces ownership and admission. */
2146
2297
  async function asOwner(serverValue, signal, operation) {
2147
2298
  const selection = selectServer(serverValue);
@@ -2174,14 +2325,11 @@ async function asOwner(serverValue, signal, operation) {
2174
2325
  signal
2175
2326
  });
2176
2327
  } catch (error) {
2177
- if (!(error instanceof DocumentRequestError && isStatus(error.status))) throw error;
2178
- return {
2179
- status: error.status,
2180
- message: error.status === "signed_out" ? `Your credential was revoked, expired or is invalid. Run ${login} to sign in again.` : error.status === "uninvited" ? "This account is not invited. Contact the server owner." : error.message,
2181
- ...error.status === "signed_out" ? { next } : {}
2182
- };
2328
+ if (!isRefusal(error)) throw error;
2329
+ return refusal(error, next);
2183
2330
  }
2184
2331
  }
2332
+ const isRefusal = (error) => error instanceof DocumentRequestError && isStatus(error.status);
2185
2333
  function listDocuments(options) {
2186
2334
  return asOwner(options.server, options.signal, async ({ origin, args }, request) => {
2187
2335
  const page = await getDocuments(origin, {
@@ -2201,7 +2349,7 @@ function listDocuments(options) {
2201
2349
  ...next ? { next } : {},
2202
2350
  message: [
2203
2351
  page.documents.length ? "Documents:" : options.cursor ? "No more documents." : "No documents.",
2204
- ...page.documents.map((document) => `${document.id} ${document.visibility} ${document.title}\n` + (document.repository ? ` repository ${document.repository}\n` : "") + ` source ${document.sourcePath}\n current revision ${document.revisionId}\n ${document.url}`),
2352
+ ...page.documents.map((document) => `${document.id} ${document.visibility} ${viewerCount(document.sharedWith)} ${document.title}\n` + (document.repository ? ` repository ${document.repository}\n` : "") + ` source ${document.sourcePath}\n current revision ${document.revisionId}\n ${document.url}`),
2205
2353
  ...next ? [`More documents: run ${commandLine(next.command, next.args)}`] : []
2206
2354
  ].join("\n")
2207
2355
  };
@@ -2280,9 +2428,10 @@ function downloadDocument(options) {
2280
2428
  function changeVisibility(options) {
2281
2429
  return asOwner(options.server, options.signal, async ({ origin }, request) => {
2282
2430
  const result = await updateDocumentVisibility(origin, options.documentId, options.visibility, request);
2431
+ const { document, removedViewers } = result;
2283
2432
  return {
2284
2433
  ...result,
2285
- message: result.document.visibility === "public" ? `${result.document.id} is public. Anyone with the URL can read its current revision: ${result.document.url}` : `${result.document.id} is private. Only you can read it after signing in: ${result.document.url}`
2434
+ message: document.visibility === "public" ? `${document.id} is public.${removedViewers ? ` Removed its ${viewerCount(removedViewers)}.` : ""} Anyone with the URL can read its current revision: ${document.url}` : `${document.id} is private. Only you and its ${document.sharedWith ? viewerCount(document.sharedWith) : "viewers"} can read it after signing in: ${document.url}`
2286
2435
  };
2287
2436
  });
2288
2437
  }
@@ -2752,6 +2901,132 @@ async function checkReadiness(serverValue, signal) {
2752
2901
  };
2753
2902
  }
2754
2903
  //#endregion
2904
+ //#region src/viewers.ts
2905
+ /** `octocat (Mona Lisa), GitHub ID 583231` */
2906
+ const account = ({ login, name, githubAccountId }) => `${login}${name ? ` (${name})` : ""}, GitHub ID ${githubAccountId}`;
2907
+ function viewerList(documentId, viewers) {
2908
+ return viewers.length ? [`Viewers of ${documentId}:`, ...viewers.map((viewer) => ` ${account(viewer)}`)].join("\n") : `${documentId} has no viewers.`;
2909
+ }
2910
+ /**
2911
+ * Lists a document's viewers, or changes them. Every removal is matched and every added username
2912
+ * looked up before anything changes. Removals run first, then additions.
2913
+ */
2914
+ function manageViewers(options) {
2915
+ const { documentId, add, remove } = options;
2916
+ return asOwner(options.server, options.signal, async ({ origin, args }, request) => {
2917
+ const login = {
2918
+ command: "login",
2919
+ args
2920
+ };
2921
+ const next = {
2922
+ command: "viewers",
2923
+ args: [
2924
+ documentId,
2925
+ ...args,
2926
+ ...remove.flatMap((value) => ["--remove", value]),
2927
+ ...add.flatMap((value) => ["--add", value])
2928
+ ]
2929
+ };
2930
+ const progress = [];
2931
+ const added = [];
2932
+ const removed = [];
2933
+ const changed = () => added.length || removed.length ? {
2934
+ added,
2935
+ removed
2936
+ } : {};
2937
+ try {
2938
+ const { viewers } = await getDocumentViewers(origin, documentId, request);
2939
+ if (!add.length && !remove.length) return {
2940
+ status: "listed",
2941
+ documentId,
2942
+ viewers,
2943
+ message: viewerList(documentId, viewers)
2944
+ };
2945
+ const removals = [];
2946
+ const notViewers = [];
2947
+ for (const value of remove) {
2948
+ const matches = /^[0-9]+$/u.test(value) ? viewers.filter((viewer) => viewer.githubAccountId === value) : viewers.filter((viewer) => viewer.login.toLowerCase() === value.toLowerCase());
2949
+ if (matches.length > 1) return {
2950
+ status: "ambiguous_viewer",
2951
+ documentId,
2952
+ value,
2953
+ matches,
2954
+ message: [`${value} matches ${matches.length} viewers of ${documentId}. Nothing was changed. Remove one by its GitHub ID with --remove <id>:`, ...matches.map((viewer) => ` ${account(viewer)}`)].join("\n")
2955
+ };
2956
+ if (!matches[0]) notViewers.push(value);
2957
+ else if (!removals.includes(matches[0])) removals.push(matches[0]);
2958
+ }
2959
+ const accounts = [];
2960
+ for (const value of add) {
2961
+ const { user } = await getGitHubUser(origin, value, request);
2962
+ if (user.type !== "user") return {
2963
+ status: "not_personal_account",
2964
+ documentId,
2965
+ user,
2966
+ message: `${account(user)} is ${user.type === "organization" ? "an organization" : "not a personal account"}. Only a personal GitHub account can be a viewer. Nothing was changed.`
2967
+ };
2968
+ progress.push(`Found ${account(user)}.`);
2969
+ if (!accounts.some(({ githubAccountId }) => githubAccountId === user.githubAccountId)) accounts.push(user);
2970
+ }
2971
+ for (const viewer of removals) {
2972
+ await removeDocumentViewer(origin, documentId, viewer.githubAccountId, request);
2973
+ removed.push(viewer);
2974
+ progress.push(`Removed ${account(viewer)}.`);
2975
+ }
2976
+ for (const user of accounts) {
2977
+ const { viewer } = await addDocumentViewer(origin, documentId, user, request);
2978
+ added.push(viewer);
2979
+ progress.push(`Added ${account(viewer)}.`);
2980
+ }
2981
+ const current = await getDocumentViewers(origin, documentId, request);
2982
+ return {
2983
+ status: "viewers_updated",
2984
+ documentId,
2985
+ added,
2986
+ removed,
2987
+ notViewers,
2988
+ viewers: current.viewers,
2989
+ message: [
2990
+ ...progress,
2991
+ ...notViewers.map((value) => `${value} is not a viewer of ${documentId}.`),
2992
+ viewerList(documentId, current.viewers)
2993
+ ].join("\n")
2994
+ };
2995
+ } catch (error) {
2996
+ const prefix = (result) => ({
2997
+ ...result,
2998
+ documentId,
2999
+ ...changed(),
3000
+ message: [...progress, result.message].join("\n")
3001
+ });
3002
+ if (isRefusal(error)) {
3003
+ const { status } = error;
3004
+ if (status === "github_sign_in_required") return prefix({
3005
+ status,
3006
+ message: `GitHub refused the sign-in Pageship uses to look up GitHub users. Sign in to the Pageship dashboard again with GitHub, then run ${commandLine(next.command, next.args)}.`,
3007
+ next
3008
+ });
3009
+ if (status === "github_rate_limited" || status === "github_unavailable") return prefix({
3010
+ status,
3011
+ message: error.message,
3012
+ next
3013
+ });
3014
+ return prefix(refusal(error, login));
3015
+ }
3016
+ if (!added.length && !removed.length) throw error;
3017
+ return prefix(options.signal.aborted ? {
3018
+ status: "interrupted",
3019
+ message: `Operation interrupted. Run ${commandLine(next.command, next.args)} to finish.`,
3020
+ next
3021
+ } : {
3022
+ status: "request_failed",
3023
+ message: `${error instanceof Error ? error.message : String(error)} Run ${commandLine(next.command, next.args)} to finish.`,
3024
+ next
3025
+ });
3026
+ }
3027
+ });
3028
+ }
3029
+ //#endregion
2755
3030
  //#region src/cli.ts
2756
3031
  const args = process.argv.slice(2);
2757
3032
  const separator = args.indexOf("--");
@@ -2905,6 +3180,18 @@ const visibility = Command.make("visibility", {
2905
3180
  visibility,
2906
3181
  signal
2907
3182
  }))).pipe(Command.withDescription("Make a document private or public."));
3183
+ const viewers = Command.make("viewers", {
3184
+ documentId: documentIdArgument,
3185
+ add: Flag.string("add").pipe(Flag.filter((value) => isGitHubLogin(value), () => "Use a GitHub username."), Flag.withDescription("Look up this GitHub username and add the account it names as a viewer. Repeat to add several."), Flag.atLeast(0)),
3186
+ remove: Flag.string("remove").pipe(Flag.filter((value) => isGitHubLogin(value), () => "Use a viewer's GitHub username or GitHub ID."), Flag.withDescription("Remove the viewer with this GitHub username or GitHub ID. Repeat to remove several."), Flag.atLeast(0)),
3187
+ server: serverFlag
3188
+ }, ({ documentId, add, remove, server }) => authCommand("viewers", (signal) => manageViewers({
3189
+ server: Option.getOrUndefined(server),
3190
+ documentId,
3191
+ add,
3192
+ remove,
3193
+ signal
3194
+ }))).pipe(Command.withDescription("List the GitHub users who can read a private document, or add and remove them."));
2908
3195
  const restore = Command.make("restore", {
2909
3196
  documentId: documentIdArgument,
2910
3197
  server: serverFlag,
@@ -2977,7 +3264,7 @@ function authCommand(name, operation) {
2977
3264
  }
2978
3265
  });
2979
3266
  }
2980
- const command = Command.make("pageship").pipe(Command.withDescription("Pageship publishes private HTML documents on the hosted Pageship service.\n\nEarly development: check whether this installation is ready with the status command. Authorize this installation with login, then publish one HTML file with publish. Manage published documents with list, history, download, visibility and restore. Install the Pageship skills for your agents with skills install."), Command.withSubcommands([
3267
+ const command = Command.make("pageship").pipe(Command.withDescription("Pageship publishes private HTML documents on the hosted Pageship service.\n\nEarly development: check whether this installation is ready with the status command. Authorize this installation with login, then publish one HTML file with publish. Manage published documents with list, history, download, visibility, viewers and restore. Install the Pageship skills for your agents with skills install."), Command.withSubcommands([
2981
3268
  status,
2982
3269
  login,
2983
3270
  whoami,
@@ -2986,6 +3273,7 @@ const command = Command.make("pageship").pipe(Command.withDescription("Pageship
2986
3273
  history,
2987
3274
  download,
2988
3275
  visibility,
3276
+ viewers,
2989
3277
  restore,
2990
3278
  skills
2991
3279
  ]), Command.withGlobalFlags([jsonFlag]));
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship
3
- description: Publish an HTML file to Pageship and manage what is published. Use when the user asks to publish, share or get a link to a document, update its published copy, change its visibility, list, inspect history, download or restore published documents, or sign in to Pageship. To write, edit, rename or move a document, load pageship-docs instead.
3
+ description: Publish an HTML file to Pageship and manage what is published. Use when the user asks to publish, share or get a link to a document, update its published copy, change its visibility or viewers, list, inspect history, download or restore published documents, or sign in to Pageship. To write, edit, rename or move a document, load pageship-docs instead.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Pageship
@@ -97,7 +97,7 @@ After a successful upload the CLI adds the document marker to the end of the loc
97
97
  On `published`, tell the user:
98
98
 
99
99
  - the document URL, `document.url`;
100
- - who can read it, from `document.visibility`: a private document opens only for its owner, signed in;
100
+ - who can read it, from `document.visibility`: a private document opens only for its owner and its viewers, signed in;
101
101
  - whether this publish created a new document or revised an existing one, from `change`: `created` or `revised`.
102
102
 
103
103
  Keep `idempotencyKey` only while you resolve an uncertain attempt.
@@ -124,7 +124,11 @@ This works for any document the user owns, even when the file moved or belongs t
124
124
 
125
125
  ## Share a document
126
126
 
127
- A request to share a document, or to send someone its link, is a request to publish it. Publish it as above and report the publish. A new document is private. When the result is private, ask whether to make it public, and say in the question that anyone with the link can then read it. When it is already public, because the user made it public earlier, say so and don't ask. Change visibility only after the user agrees, as in [visibility](references/visibility.md).
127
+ A request to share a document, or to send someone its link, is a request to publish it. Publish it as above and report the publish. A new document is private.
128
+
129
+ When the user names the people to share it with, keep it private and add them as viewers, as in [viewers](references/viewers.md). Sharing with named people never makes a document public.
130
+
131
+ When the user names no one and the result is private, ask whether to make it public, and say in the question that anyone with the link can then read it. Mention that it can instead stay private and be shared with named GitHub users. When it is already public, because the user made it public earlier, say so and don't ask. Change visibility only after the user agrees, as in [visibility](references/visibility.md).
128
132
 
129
133
  ## Use another server
130
134
 
@@ -137,6 +141,7 @@ Load the reference for the task:
137
141
  - [List and history](references/list-and-history.md): find a document's `id`, `visibility`, current `revisionId` and `url`, and inspect its revisions.
138
142
  - [Download](references/download.md): read a document's HTML, current or from an earlier revision.
139
143
  - [Visibility](references/visibility.md): make a document public or private.
144
+ - [Viewers](references/viewers.md): list, add or remove the GitHub users who can read a private document.
140
145
  - [Restore](references/restore.md): make an earlier revision current again.
141
146
 
142
147
  Deleting a document, recovering a deleted one, changing a document's display title and revoking a CLI installation happen in the dashboard, at https://app.pageship.dev for the hosted service. The CLI has no commands for them yet, so give the user the dashboard link.
@@ -0,0 +1,43 @@
1
+ # Viewers
2
+
3
+ A viewer is a GitHub user who can read a private document after signing in with GitHub. The owner always can. Viewers see only the current revision, never the history.
4
+
5
+ ## Share with named people
6
+
7
+ Add viewers only for people the user named in this conversation. When the user names a person without a GitHub username, ask for it; a GitHub username is the only way to name a viewer. Sharing with people keeps the document private: a request to share a document with someone is never a request to make it public. A public document can't have viewers, so if it is public, ask the user whether to make it private first, as in [visibility](visibility.md), and add no one until they agree.
8
+
9
+ Set `PAGESHIP_DOCUMENT` to the document's `id`, then add each named username with its own `--add`:
10
+
11
+ ```sh
12
+ pageship --json viewers "$PAGESHIP_DOCUMENT" --add octocat --add hubot
13
+ ```
14
+
15
+ The CLI looks each username up on GitHub before it changes anything. `added` holds each account it added, with `login`, `name` and `githubAccountId`, on `viewers_updated` and on a failure partway through. Whatever the status, show the user every account in `added` by `login` and `name`, and ask them to confirm each is the person they meant. A username can belong to someone else. If one is wrong, remove it as below. Then give the user the document's `url` to send: Pageship sends no invitations.
16
+
17
+ ## List and remove viewers
18
+
19
+ To see who can read the document, run:
20
+
21
+ ```sh
22
+ pageship --json viewers "$PAGESHIP_DOCUMENT"
23
+ ```
24
+
25
+ On `listed`, `viewers` holds each viewer's `login`, `name` and `githubAccountId`. A viewer's `login` is the one they had when added or last signed in, so two viewers can show the same `login`.
26
+
27
+ Remove viewers only when the user asks. Pass a `login` or a `githubAccountId`, one `--remove` each. A value of only digits is always a `githubAccountId`, so remove a viewer whose `login` is all digits by its `githubAccountId`:
28
+
29
+ ```sh
30
+ pageship --json viewers "$PAGESHIP_DOCUMENT" --remove octocat
31
+ ```
32
+
33
+ `notViewers` names values that matched no viewer; nothing was needed for them. On `ambiguous_viewer`, the value matched more than one viewer and nothing changed. Show the user `matches` and ask which one, then remove it by its `githubAccountId`.
34
+
35
+ ## Outcomes
36
+
37
+ - `github_user_not_found`, `not_personal_account` or `owner_account`: nothing changed for that username. Report the `message` and ask the user for the right GitHub username. Only a personal account can be a viewer, and the owner can always read their own document.
38
+ - `github_account_mismatch`: the username moved to another account between the lookup and the change. Report the `message` and ask the user to confirm the username before you run the same command again.
39
+ - `document_public`: the document is public. Ask whether to make it private before adding anyone.
40
+ - `github_sign_in_required`: GitHub refused the sign-in Pageship uses to look up users. Ask the user to sign in to the Pageship dashboard again with GitHub, then run the returned `next`. A CLI `login` doesn't fix this.
41
+ - `github_rate_limited` or `github_unavailable`: wait a minute, then run the returned `next`.
42
+
43
+ A failure partway through keeps the changes already made, listed in `added` and `removed`. Its `next` repeats the command, which finishes the rest without adding or removing anyone twice.
@@ -1,6 +1,6 @@
1
1
  # Visibility
2
2
 
3
- A private document opens only for its owner, signed in. Anyone with the link can read a public one, without signing in.
3
+ A private document opens only for its owner and its [viewers](viewers.md), signed in. Anyone with the link can read a public one, without signing in. Making a document public removes all its viewers.
4
4
 
5
5
  Change visibility only when the user asked for a policy by name, such as "make it public", or agreed when you asked after sharing. Set `PAGESHIP_DOCUMENT` to the document's `id`, then run one of:
6
6
 
@@ -9,4 +9,4 @@ pageship --json visibility "$PAGESHIP_DOCUMENT" public
9
9
  pageship --json visibility "$PAGESHIP_DOCUMENT" private
10
10
  ```
11
11
 
12
- On `visibility_updated`, report the returned `document.visibility` and `document.url`. Publishing a new revision keeps the document's visibility.
12
+ On `visibility_updated`, report the returned `document.visibility` and `document.url`, and `removedViewers` when it is more than 0. Publishing a new revision keeps the document's visibility.
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship-chart
3
- description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for charts in a Pageship HTML document, loaded by a pageship- kind skill before it draws a chart.
3
+ description: "Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for charts in a Pageship HTML document, loaded by a pageship- kind skill before it draws a chart."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Chart
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship-code-block
3
- description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for code in a Pageship HTML document, such as source code, commands, configuration or logs, loaded by a pageship- kind skill before the first code block.
3
+ description: "Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for code in a Pageship HTML document, such as source code, commands, configuration or logs, loaded by a pageship- kind skill before the first code block."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Code block
@@ -3,7 +3,7 @@ name: pageship-decision-record
3
3
  description: HTML decision records, as the second step of pageship-docs. For any request to record, write, regenerate, edit or move a decision, load pageship-docs first; load this skill first only when the user names it.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Decision record
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship-diagram
3
- description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for diagrams in a Pageship HTML document, such as a process, a timeline, a system map or states, loaded by a pageship- kind skill before it draws a diagram.
3
+ description: "Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for diagrams in a Pageship HTML document, such as a process, a timeline, a system map or states, loaded by a pageship- kind skill before it draws a diagram."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Diagram
@@ -3,7 +3,7 @@ name: pageship-docs
3
3
  description: First step for any document people read, such as a report, plan, spec, explainer, review, decision record or slide deck, in HTML or Markdown. Load it before any other pageship- skill. Use when the user asks for one of these documents, asks to regenerate, redesign, edit, rename, move or copy an HTML source file, or asks to record whether they want HTML or Markdown.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Pageship documents
@@ -3,7 +3,7 @@ name: pageship-explainer
3
3
  description: HTML explainers, as the second step of pageship-docs. For any request to explain how something works, or to write, regenerate, edit or move an explainer, load pageship-docs first; load this skill first only when the user names it.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Explainer
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship-interaction
3
- description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for controls in a Pageship HTML document, such as stepping through stages, tracing a path, tabs, filters, revealing detail and pointer states on charts and diagrams, loaded by a pageship- kind skill before it adds any control.
3
+ description: "Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for controls in a Pageship HTML document, such as stepping through stages, tracing a path, tabs, filters, revealing detail and pointer states on charts and diagrams, loaded by a pageship- kind skill before it adds any control."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Interaction
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship-key-figures
3
- description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for the few numbers and callouts a Pageship HTML document opens with, loaded by a pageship- kind skill before it writes key figures.
3
+ description: "Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for the few numbers and callouts a Pageship HTML document opens with, loaded by a pageship- kind skill before it writes key figures."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Key figures
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship-media
3
- description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for images, video and audio in a Pageship HTML document, loaded by a pageship- kind skill before it shows media.
3
+ description: "Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for images, video and audio in a Pageship HTML document, loaded by a pageship- kind skill before it shows media."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Media
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship-motion
3
- description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for animation and transitions in a Pageship HTML document, such as a chart that grows into view, a sequence that plays through a diagram, or a marker moving along a path, loaded by a pageship- kind skill before it draws a chart, a graphic or a sequence, or before anything else on the page moves.
3
+ description: "Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for animation and transitions in a Pageship HTML document, such as a chart that grows into view, a sequence that plays through a diagram, or a marker moving along a path, loaded by a pageship- kind skill before it draws a chart, a graphic or a sequence, or before anything else on the page moves."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Motion
@@ -3,7 +3,7 @@ name: pageship-plan
3
3
  description: HTML plans, as the second step of pageship-docs. For any request to write, regenerate, edit or move a plan, load pageship-docs first; load this skill first only when the user names it.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Plan
@@ -3,7 +3,7 @@ name: pageship-report
3
3
  description: HTML reports, as the second step of pageship-docs. For any request to write, regenerate, edit or move a report, load pageship-docs first; load this skill first only when the user names it.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Report
@@ -3,7 +3,7 @@ name: pageship-review-report
3
3
  description: HTML review reports, as the second step of pageship-docs. For any request to review a change or a plan and write up the findings, or to write, regenerate, edit or move a review report, load pageship-docs first; load this skill first only when the user names it.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Review report
@@ -3,7 +3,7 @@ name: pageship-slide-deck
3
3
  description: HTML slide decks, as the second step of pageship-docs. For any request to make slides or a presentation, or to regenerate, edit or move a deck, load pageship-docs first; load this skill first only when the user names it.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Slide deck
@@ -3,7 +3,7 @@ name: pageship-spec
3
3
  description: HTML specs, as the second step of pageship-docs. For any request to write, regenerate, edit or move a spec, load pageship-docs first; load this skill first only when the user names it.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Spec
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: pageship-table
3
- description: Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for tables in a Pageship HTML document, loaded by a pageship- kind skill before it writes a table.
3
+ description: "Later step of pageship-docs: for a request to write or edit a document, load pageship-docs first; load this skill first only when the user names it. Pattern for tables in a Pageship HTML document, loaded by a pageship- kind skill before it writes a table."
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.4.1"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # Table
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pageship",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Pageship CLI for checking server API compatibility.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {