pageship 0.4.1 → 0.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.
package/README.md CHANGED
@@ -14,7 +14,7 @@ pnpm add -g pageship
14
14
  bun add -g pageship
15
15
  ```
16
16
 
17
- Then run `pageship --version` to check that the shell finds it. To have an agent install the CLI and the Pageship skills and sign you in, give it the [setup guide](https://pageship.dev/setup-guide) instead.
17
+ Then run `pageship --version` to check that the shell finds it. Run `pageship update` later to update it with the same package manager. To have an agent install the CLI and the Pageship skills and sign you in, give it the [setup guide](https://pageship.dev/setup-guide) instead.
18
18
 
19
19
  ## Get started
20
20
 
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { execFile } from "node:child_process";
2
+ import { exec, execFile, spawn } from "node:child_process";
3
3
  import { access, cp, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, stat, symlink, writeFile } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { createInterface } from "node:readline/promises";
@@ -675,14 +675,22 @@ const documentsRestore = (options) => (options.client ?? client).post({
675
675
  ...options.headers
676
676
  }
677
677
  });
678
- const documentsUpdateVisibility = (options) => (options.client ?? client).patch({
679
- url: "/api/v1/documents/{id}/visibility",
678
+ const viewersList = (options) => (options.client ?? client).get({
679
+ url: "/api/v1/documents/{id}/viewers",
680
+ ...options
681
+ });
682
+ const viewersUpdateSharing = (options) => (options.client ?? client).patch({
683
+ url: "/api/v1/documents/{id}/sharing",
680
684
  ...options,
681
685
  headers: {
682
686
  "Content-Type": "application/json",
683
687
  ...options.headers
684
688
  }
685
689
  });
690
+ const githubGetUser = (options) => (options.client ?? client).get({
691
+ url: "/api/v1/github/users/{login}",
692
+ ...options
693
+ });
686
694
  //#endregion
687
695
  //#region ../../packages/api-client/src/generated/workspace-domain.gen.ts
688
696
  /** Every workspace address and document URL is a subdomain of this domain. */
@@ -760,14 +768,16 @@ function validTitle(value) {
760
768
  }
761
769
  function normalizeDocument(value) {
762
770
  if (!record(value) || typeof value.slug !== "string") return;
763
- const normalized = value.title === void 0 ? {
771
+ const normalized = {
764
772
  ...value,
765
- title: value.slug
766
- } : value;
773
+ ...value.title === void 0 ? { title: value.slug } : {},
774
+ ...value.viewCount === void 0 ? { viewCount: 0 } : {},
775
+ ...value.sharedWith === void 0 ? { sharedWith: 0 } : {}
776
+ };
767
777
  return validDocument(normalized) ? normalized : void 0;
768
778
  }
769
779
  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);
780
+ 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
781
  }
772
782
  function validRevision(value, documentId) {
773
783
  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);
@@ -775,10 +785,12 @@ function validRevision(value, documentId) {
775
785
  var DocumentRequestError = class extends Error {
776
786
  status;
777
787
  httpStatus;
778
- constructor(status, message, httpStatus) {
788
+ account;
789
+ constructor(status, message, httpStatus, account) {
779
790
  super(message);
780
791
  this.status = status;
781
792
  this.httpStatus = httpStatus;
793
+ this.account = account;
782
794
  }
783
795
  };
784
796
  function failed(response, error) {
@@ -899,26 +911,6 @@ async function restoreDocumentRevision(baseUrl, id, input, options = {}) {
899
911
  if (record(data) && data.status === "idempotency_conflict") return { status: "idempotency_conflict" };
900
912
  throw new Error("Invalid document restoration response. Retry with the same idempotency key.");
901
913
  }
902
- async function updateDocumentVisibility(baseUrl, id, visibility, options = {}) {
903
- const { data: received, response, error } = await documentsUpdateVisibility({
904
- ...changeOptions(baseUrl, options),
905
- path: { id },
906
- body: { visibility }
907
- });
908
- if (!response?.ok) {
909
- if (response?.status === 400) throw new DocumentRequestError("invalid_visibility", "Choose private or public.", 400);
910
- if (response?.status === 401) throw new DocumentRequestError("signed_out", "Sign in to update this document.", 401);
911
- if (response?.status === 403) throw new DocumentRequestError("uninvited", "Access unavailable.", 403);
912
- if (response?.status === 404) throw new DocumentRequestError("unavailable", "Document unavailable.", 404);
913
- throw new Error("Could not update document visibility. Try again.", { cause: error });
914
- }
915
- const data = received;
916
- if (record(data) && data.status === "visibility_updated" && validDocument(data.document) && data.document.visibility === visibility) return {
917
- status: "visibility_updated",
918
- document: data.document
919
- };
920
- throw new Error("Invalid document visibility response.");
921
- }
922
914
  async function publishDocument(baseUrl, input, options = {}) {
923
915
  if (input.action) {
924
916
  if (!(await getDocumentLimits(baseUrl, options)).publicationActions.includes(input.action)) throw new DocumentRequestError("publication_action_unsupported", "This server does not support explicit publication actions. Ask the operator to update the server. No HTML was uploaded.", 400);
@@ -957,6 +949,96 @@ async function publishDocument(baseUrl, input, options = {}) {
957
949
  if (record(data) && (data.status === "idempotency_conflict" || data.status === "address_required" || data.status === "unavailable" || data.status === "document_id_taken")) return { status: data.status };
958
950
  throw new Error("Invalid publication response. Retry with the same idempotency key.");
959
951
  }
952
+ /** Same GitHub username grammar as the Effect contract. */
953
+ function isGitHubLogin(value) {
954
+ return typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9-]{0,38}(?![\s\S])/u.test(value);
955
+ }
956
+ function isGitHubAccountId(value) {
957
+ return typeof value === "string" && /^[1-9][0-9]{0,19}(?![\s\S])/u.test(value);
958
+ }
959
+ function validViewer(value) {
960
+ 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;
961
+ }
962
+ function validUserMatch(value) {
963
+ return record(value) && isGitHubAccountId(value.githubAccountId) && isGitHubLogin(value.login);
964
+ }
965
+ function validUserProfile(value) {
966
+ return record(value) && (value.name === null || typeof value.name === "string") && (value.type === "user" || value.type === "organization" || value.type === "other") && validUserMatch(value);
967
+ }
968
+ /**
969
+ * One message per viewer refusal. The body's status picks it, not the HTTP code: a refused
970
+ * GitHub token and a signed-out session are both 401.
971
+ */
972
+ const viewerRefusals = {
973
+ signed_out: "Sign in to manage viewers.",
974
+ uninvited: "Access unavailable.",
975
+ forbidden: "A trusted browser origin is required.",
976
+ unavailable: "Document unavailable.",
977
+ document_public: "Make the document private before adding viewers.",
978
+ github_user_not_found: "GitHub has no user with that username.",
979
+ github_account_mismatch: "That username now belongs to a different GitHub account. Look it up again.",
980
+ not_personal_account: "Only a personal GitHub account can be a viewer.",
981
+ owner_account: "You already have access to your own document.",
982
+ github_sign_in_required: "GitHub refused your sign-in. Sign in to Pageship again.",
983
+ github_rate_limited: "GitHub is limiting requests. Try again in a minute.",
984
+ github_unavailable: "GitHub is unavailable. Try again.",
985
+ invalid_sharing: "A change can't both add and remove the same account."
986
+ };
987
+ function isViewerRefusal(value) {
988
+ return typeof value === "string" && Object.hasOwn(viewerRefusals, value);
989
+ }
990
+ function viewerFailed(response, error) {
991
+ if (response && record(error) && isViewerRefusal(error.status)) throw new DocumentRequestError(error.status, validUserMatch(error.account) && typeof error.message === "string" ? error.message : viewerRefusals[error.status], response.status, validUserMatch(error.account) ? error.account : void 0);
992
+ failed(response, error);
993
+ }
994
+ async function getDocumentViewers(baseUrl, id, options = {}) {
995
+ const { data, response, error } = await viewersList({
996
+ ...requestOptions(baseUrl, options),
997
+ path: { id }
998
+ });
999
+ if (!response?.ok) viewerFailed(response, error);
1000
+ if (data?.status === "listed" && Array.isArray(data.viewers) && data.viewers.every(validViewer)) return data;
1001
+ throw new Error("Invalid document viewer listing response.");
1002
+ }
1003
+ /**
1004
+ * The most accounts one sharing change adds and removes. The contract's DocumentSharingChange
1005
+ * refuses more; tests/document-contract.test.ts keeps the two equal.
1006
+ */
1007
+ const sharingLimits = {
1008
+ add: 20,
1009
+ remove: 50
1010
+ };
1011
+ /**
1012
+ * Changes visibility and viewers together or not at all. The server looks up each added account
1013
+ * that isn't a viewer yet, as addDocumentViewer does.
1014
+ */
1015
+ async function updateDocumentSharing(baseUrl, id, change, options = {}) {
1016
+ const { data, response, error } = await viewersUpdateSharing({
1017
+ ...changeOptions(baseUrl, options),
1018
+ path: { id },
1019
+ body: change
1020
+ });
1021
+ if (!response?.ok) viewerFailed(response, error);
1022
+ const document = data?.status === "sharing_updated" ? normalizeDocument(data.document) : void 0;
1023
+ if (data && document && (change.visibility === void 0 || document.visibility === change.visibility) && [
1024
+ data.viewers,
1025
+ data.added,
1026
+ data.removed
1027
+ ].every((list) => Array.isArray(list) && list.every(validViewer))) return {
1028
+ ...data,
1029
+ document
1030
+ };
1031
+ throw new Error("Invalid document sharing response.");
1032
+ }
1033
+ async function getGitHubUser(baseUrl, login, options = {}) {
1034
+ const { data, response, error } = await githubGetUser({
1035
+ ...requestOptions(baseUrl, options),
1036
+ path: { login }
1037
+ });
1038
+ if (!response?.ok) viewerFailed(response, error);
1039
+ if (data?.status === "found" && validUserProfile(data.user)) return data;
1040
+ throw new Error("Invalid GitHub user response.");
1041
+ }
960
1042
  //#endregion
961
1043
  //#region ../../packages/api-client/src/index.ts
962
1044
  async function getWorkspace(baseUrl, options = {}) {
@@ -971,7 +1053,17 @@ async function getWorkspace(baseUrl, options = {}) {
971
1053
  throwOnError: false
972
1054
  });
973
1055
  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 };
1056
+ if (data?.status === "signed_out") return { status: data.status };
1057
+ if (data?.status === "uninvited") {
1058
+ const user = data.user;
1059
+ return user && typeof user.name === "string" && (user.login === void 0 || typeof user.login === "string") ? {
1060
+ status: data.status,
1061
+ user: {
1062
+ name: user.name,
1063
+ ...user.login ? { login: user.login } : {}
1064
+ }
1065
+ } : { status: data.status };
1066
+ }
975
1067
  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
1068
  throw new Error("Invalid workspace response.");
977
1069
  }
@@ -1325,7 +1417,10 @@ const outcomes = {
1325
1417
  downloaded: "succeeded",
1326
1418
  visibility_updated: "succeeded",
1327
1419
  restored: "succeeded",
1420
+ viewers_updated: "succeeded",
1328
1421
  skills_installed: "succeeded",
1422
+ cli_updated: "succeeded",
1423
+ cli_current: "succeeded",
1329
1424
  authorization_pending: "pending",
1330
1425
  expired: "pending",
1331
1426
  signed_out: "pending",
@@ -1336,6 +1431,7 @@ const outcomes = {
1336
1431
  repository_mismatch: "pending",
1337
1432
  marker_unavailable: "pending",
1338
1433
  revision_conflict: "pending",
1434
+ github_sign_in_required: "pending",
1339
1435
  cancelled: "pending",
1340
1436
  usage_error: "failed",
1341
1437
  internal_error: "failed",
@@ -1359,7 +1455,17 @@ const outcomes = {
1359
1455
  idempotency_conflict: "failed",
1360
1456
  document_id_taken: "failed",
1361
1457
  publication_action_unsupported: "failed",
1362
- skills_modified: "failed"
1458
+ skills_modified: "failed",
1459
+ install_unknown: "failed",
1460
+ update_failed: "failed",
1461
+ ambiguous_viewer: "failed",
1462
+ document_public: "failed",
1463
+ github_user_not_found: "failed",
1464
+ github_account_mismatch: "failed",
1465
+ not_personal_account: "failed",
1466
+ owner_account: "failed",
1467
+ github_rate_limited: "failed",
1468
+ github_unavailable: "failed"
1363
1469
  };
1364
1470
  const isStatus = (value) => Object.hasOwn(outcomes, value);
1365
1471
  const commandNames = [
@@ -1371,8 +1477,10 @@ const commandNames = [
1371
1477
  "history",
1372
1478
  "download",
1373
1479
  "visibility",
1480
+ "viewers",
1374
1481
  "restore",
1375
- "skills"
1482
+ "skills",
1483
+ "update"
1376
1484
  ];
1377
1485
  const isCommandName = (value) => commandNames.some((name) => name === value);
1378
1486
  /** Exit codes follow the outcome, except an interruption, which exits 130. */
@@ -1422,7 +1530,7 @@ function commandLine(command, args) {
1422
1530
  }
1423
1531
  //#endregion
1424
1532
  //#region package.json
1425
- var version = "0.4.1";
1533
+ var version = "0.6.0";
1426
1534
  //#endregion
1427
1535
  //#region src/publish.ts
1428
1536
  const execute = promisify(execFile);
@@ -2142,6 +2250,25 @@ async function identify(serverValue, signal) {
2142
2250
  }
2143
2251
  //#endregion
2144
2252
  //#region src/documents.ts
2253
+ /** `1 viewer`, `2 viewers`. */
2254
+ const viewerCount = (count) => `${count} viewer${count === 1 ? "" : "s"}`;
2255
+ /** The result for a request the server refused. `login` signs in again. */
2256
+ function refusal(error, login) {
2257
+ const status = error.status;
2258
+ if (status === "signed_out") return {
2259
+ status,
2260
+ message: `Your credential was revoked, expired or is invalid. Run ${commandLine(login.command, login.args)} to sign in again.`,
2261
+ next: login
2262
+ };
2263
+ if (status === "uninvited") return {
2264
+ status,
2265
+ message: "This account is not invited. Contact the server owner."
2266
+ };
2267
+ return {
2268
+ status,
2269
+ message: error.message
2270
+ };
2271
+ }
2145
2272
  /** Checks compatibility and the stored credential; the server enforces ownership and admission. */
2146
2273
  async function asOwner(serverValue, signal, operation) {
2147
2274
  const selection = selectServer(serverValue);
@@ -2174,14 +2301,11 @@ async function asOwner(serverValue, signal, operation) {
2174
2301
  signal
2175
2302
  });
2176
2303
  } 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
- };
2304
+ if (!isRefusal(error)) throw error;
2305
+ return refusal(error, next);
2183
2306
  }
2184
2307
  }
2308
+ const isRefusal = (error) => error instanceof DocumentRequestError && isStatus(error.status);
2185
2309
  function listDocuments(options) {
2186
2310
  return asOwner(options.server, options.signal, async ({ origin, args }, request) => {
2187
2311
  const page = await getDocuments(origin, {
@@ -2201,7 +2325,7 @@ function listDocuments(options) {
2201
2325
  ...next ? { next } : {},
2202
2326
  message: [
2203
2327
  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}`),
2328
+ ...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
2329
  ...next ? [`More documents: run ${commandLine(next.command, next.args)}`] : []
2206
2330
  ].join("\n")
2207
2331
  };
@@ -2279,10 +2403,13 @@ function downloadDocument(options) {
2279
2403
  }
2280
2404
  function changeVisibility(options) {
2281
2405
  return asOwner(options.server, options.signal, async ({ origin }, request) => {
2282
- const result = await updateDocumentVisibility(origin, options.documentId, options.visibility, request);
2406
+ const { document, removed } = await updateDocumentSharing(origin, options.documentId, { visibility: options.visibility }, request);
2407
+ const removedViewers = removed.length;
2283
2408
  return {
2284
- ...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}`
2409
+ status: "visibility_updated",
2410
+ document,
2411
+ removedViewers,
2412
+ 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
2413
  };
2287
2414
  });
2288
2415
  }
@@ -2361,6 +2488,10 @@ const installNext = {
2361
2488
  command: "skills",
2362
2489
  args: ["install"]
2363
2490
  };
2491
+ const updateNext = {
2492
+ command: "update",
2493
+ args: []
2494
+ };
2364
2495
  /** The output of `node --version` from PATH, or undefined when it does not run. */
2365
2496
  const nodeOnPath = () => new Promise((resolve) => {
2366
2497
  execFile("node", ["--version"], {
@@ -2445,6 +2576,7 @@ async function replaceFolder(source, target) {
2445
2576
  }
2446
2577
  }
2447
2578
  const parseMajor = (version) => Number(/^v?(\d+)\./u.exec(version)?.[1] ?? NaN);
2579
+ /** Whether version `installed` is older than `cli`. An unreadable `installed` counts as older. */
2448
2580
  const older = (installed, cli) => {
2449
2581
  const parts = (version) => version.split(/[.+-]/u).slice(0, 3).map(Number);
2450
2582
  const [a, b] = [parts(installed), parts(cli)];
@@ -2468,7 +2600,7 @@ async function inspectCli(environment, signal) {
2468
2600
  return {
2469
2601
  cli: "older",
2470
2602
  latest,
2471
- message: `A newer Pageship CLI is published (${latest}; this is ${environment.version}). Update it with the package manager that installed it, then run ${commandLine(installNext.command, installNext.args)}.`
2603
+ message: `A newer Pageship CLI is published (${latest}; this is ${environment.version}). Run ${commandLine(updateNext.command, updateNext.args)} to update it, then run ${commandLine(installNext.command, installNext.args)}.`
2472
2604
  };
2473
2605
  }
2474
2606
  /** The recommended skills and Node that the CLI could not find. It installs none of them. */
@@ -2752,6 +2884,293 @@ async function checkReadiness(serverValue, signal) {
2752
2884
  };
2753
2885
  }
2754
2886
  //#endregion
2887
+ //#region src/viewers.ts
2888
+ /** `octocat (Mona Lisa), GitHub ID 583231` */
2889
+ const account = ({ login, name, githubAccountId }) => `${login}${name ? ` (${name})` : ""}, GitHub ID ${githubAccountId}`;
2890
+ function viewerList(documentId, viewers) {
2891
+ return viewers.length ? [`Viewers of ${documentId}:`, ...viewers.map((viewer) => ` ${account(viewer)}`)].join("\n") : `${documentId} has no viewers.`;
2892
+ }
2893
+ /**
2894
+ * Lists a document's viewers, or changes them. Every removal is matched and every added username
2895
+ * looked up first, then one sharing change applies them all or none.
2896
+ */
2897
+ function manageViewers(options) {
2898
+ const { documentId, add, remove } = options;
2899
+ return asOwner(options.server, options.signal, async ({ origin, args }, request) => {
2900
+ const login = {
2901
+ command: "login",
2902
+ args
2903
+ };
2904
+ const next = {
2905
+ command: "viewers",
2906
+ args: [
2907
+ documentId,
2908
+ ...args,
2909
+ ...remove.flatMap((value) => ["--remove", value]),
2910
+ ...add.flatMap((value) => ["--add", value])
2911
+ ]
2912
+ };
2913
+ if (add.length > sharingLimits.add || remove.length > sharingLimits.remove) return {
2914
+ status: "usage_error",
2915
+ message: `Add up to ${sharingLimits.add} and remove up to ${sharingLimits.remove} viewers at a time. Nothing was changed.`
2916
+ };
2917
+ const progress = [];
2918
+ try {
2919
+ const { viewers } = await getDocumentViewers(origin, documentId, request);
2920
+ if (!add.length && !remove.length) return {
2921
+ status: "listed",
2922
+ documentId,
2923
+ viewers,
2924
+ message: viewerList(documentId, viewers)
2925
+ };
2926
+ const removals = [];
2927
+ const notViewers = [];
2928
+ for (const value of remove) {
2929
+ const matches = /^[0-9]+$/u.test(value) ? viewers.filter((viewer) => viewer.githubAccountId === value) : viewers.filter((viewer) => viewer.login.toLowerCase() === value.toLowerCase());
2930
+ if (matches.length > 1) return {
2931
+ status: "ambiguous_viewer",
2932
+ documentId,
2933
+ value,
2934
+ matches,
2935
+ 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")
2936
+ };
2937
+ if (!matches[0]) notViewers.push(value);
2938
+ else if (!removals.includes(matches[0])) removals.push(matches[0]);
2939
+ }
2940
+ const accounts = [];
2941
+ for (const value of add) {
2942
+ const { user } = await getGitHubUser(origin, value, request);
2943
+ if (user.type !== "user") return {
2944
+ status: "not_personal_account",
2945
+ documentId,
2946
+ user,
2947
+ 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.`
2948
+ };
2949
+ if (removals.some(({ githubAccountId }) => githubAccountId === user.githubAccountId)) return {
2950
+ status: "usage_error",
2951
+ message: `${account(user)} is both added and removed. Nothing was changed.`
2952
+ };
2953
+ progress.push(`Found ${account(user)}.`);
2954
+ if (!accounts.some(({ githubAccountId }) => githubAccountId === user.githubAccountId)) accounts.push(user);
2955
+ }
2956
+ const { added, removed, ...shared } = await updateDocumentSharing(origin, documentId, {
2957
+ add: accounts.map(({ githubAccountId, login }) => ({
2958
+ githubAccountId,
2959
+ login
2960
+ })),
2961
+ remove: removals.map(({ githubAccountId }) => githubAccountId)
2962
+ }, request);
2963
+ return {
2964
+ status: "viewers_updated",
2965
+ documentId,
2966
+ added,
2967
+ removed,
2968
+ notViewers,
2969
+ viewers: shared.viewers,
2970
+ message: [
2971
+ ...progress,
2972
+ ...removed.map((viewer) => `Removed ${account(viewer)}.`),
2973
+ ...added.map((viewer) => `Added ${account(viewer)}.`),
2974
+ ...notViewers.map((value) => `${value} is not a viewer of ${documentId}.`),
2975
+ viewerList(documentId, shared.viewers)
2976
+ ].join("\n")
2977
+ };
2978
+ } catch (error) {
2979
+ const prefix = (result) => ({
2980
+ ...result,
2981
+ documentId,
2982
+ message: [...progress, result.message].join("\n")
2983
+ });
2984
+ if (isRefusal(error)) {
2985
+ const { status } = error;
2986
+ if (status === "github_sign_in_required") return prefix({
2987
+ status,
2988
+ 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)}.`,
2989
+ next
2990
+ });
2991
+ if (status === "github_rate_limited" || status === "github_unavailable") return prefix({
2992
+ status,
2993
+ message: error.message,
2994
+ next
2995
+ });
2996
+ return prefix(refusal(error, login));
2997
+ }
2998
+ return prefix(options.signal.aborted ? {
2999
+ status: "interrupted",
3000
+ message: `Operation interrupted. Run ${commandLine(next.command, next.args)} to finish.`,
3001
+ next
3002
+ } : {
3003
+ status: "request_failed",
3004
+ message: `${error instanceof Error ? error.message : String(error)} Run ${commandLine(next.command, next.args)} to retry.`,
3005
+ next
3006
+ });
3007
+ }
3008
+ });
3009
+ }
3010
+ //#endregion
3011
+ //#region src/update.ts
3012
+ /**
3013
+ * The command that updates a global install with each manager. Keep in sync with the setup
3014
+ * guide. The package names an exact version where one is known: pnpm resolves `latest` to the
3015
+ * newest version older than its minimum release age, so a fresh release would not install.
3016
+ */
3017
+ const updateCommand$1 = (manager, spec) => ({
3018
+ npm: [
3019
+ "npm",
3020
+ "i",
3021
+ "-g",
3022
+ spec
3023
+ ],
3024
+ pnpm: [
3025
+ "pnpm",
3026
+ "add",
3027
+ "-g",
3028
+ spec
3029
+ ],
3030
+ bun: [
3031
+ "bun",
3032
+ "add",
3033
+ "-g",
3034
+ spec
3035
+ ],
3036
+ yarn: [
3037
+ "yarn",
3038
+ "global",
3039
+ "add",
3040
+ spec
3041
+ ],
3042
+ vp: [
3043
+ "vp",
3044
+ "install",
3045
+ "-g",
3046
+ spec
3047
+ ]
3048
+ })[manager];
3049
+ const npmRoot = (signal) => new Promise((resolve) => {
3050
+ exec("npm root -g", {
3051
+ timeout: 1e4,
3052
+ windowsHide: true,
3053
+ signal
3054
+ }, (error, stdout) => resolve(error ? void 0 : stdout.trim() || void 0));
3055
+ });
3056
+ /**
3057
+ * Run a command with the terminal attached, or, for `--json`, with its output collected so
3058
+ * standard output stays one JSON value.
3059
+ */
3060
+ const runCommand = (json) => (argv, signal) => new Promise((resolve, reject) => {
3061
+ const [command, ...args] = argv;
3062
+ if (!json) console.log(`Running ${argv.join(" ")}`);
3063
+ const stdio = json ? [
3064
+ "ignore",
3065
+ "pipe",
3066
+ "pipe"
3067
+ ] : "inherit";
3068
+ const child = process.platform === "win32" ? spawn(argv.join(" "), {
3069
+ stdio,
3070
+ shell: true,
3071
+ windowsHide: true
3072
+ }) : spawn(command, args, { stdio });
3073
+ const stop = () => child.kill("SIGTERM");
3074
+ signal.addEventListener("abort", stop, { once: true });
3075
+ const settle = () => signal.removeEventListener("abort", stop);
3076
+ let output = "";
3077
+ const collect = (chunk) => {
3078
+ output = (output + chunk.toString()).slice(-4e3);
3079
+ };
3080
+ child.stdout?.on("data", collect);
3081
+ child.stderr?.on("data", collect);
3082
+ child.once("error", (error) => {
3083
+ settle();
3084
+ reject(error);
3085
+ });
3086
+ child.once("close", (exitCode) => {
3087
+ settle();
3088
+ resolve({
3089
+ exitCode,
3090
+ output
3091
+ });
3092
+ });
3093
+ });
3094
+ /** A path with forward slashes and a trailing slash, compared without case on Windows. */
3095
+ function comparable(path, platform) {
3096
+ const slashed = `${path.replaceAll("\\", "/").replace(/\/+$/u, "")}/`;
3097
+ return platform === "win32" ? slashed.toLowerCase() : slashed;
3098
+ }
3099
+ /**
3100
+ * Which manager installed this CLI globally, from where its package is. Undefined for anything
3101
+ * else, such as a project dependency or an npx cache, which `update` must not replace.
3102
+ */
3103
+ async function detectManager(environment, signal) {
3104
+ const { platform, env } = environment;
3105
+ const root = comparable(environment.packageRoot, platform);
3106
+ const installed = (folders, layout) => folders.some((folder) => {
3107
+ if (!folder) return false;
3108
+ const base = comparable(folder, platform);
3109
+ const index = root.lastIndexOf(base);
3110
+ return index !== -1 && layout.test(root.slice(index + base.length));
3111
+ });
3112
+ if (installed(["/vite-plus/packages/"], /^pageship\/[^/]+\/(?:lib\/)?node_modules\/pageship\/$/u)) return "vp";
3113
+ if (installed(["/pnpm/global/", env.PNPM_HOME && join(env.PNPM_HOME, "global")], /^[^/]+\/(?:[^/]+\/)?node_modules\/\.pnpm\/pageship@[^/]+\/node_modules\/pageship\/$/u)) return "pnpm";
3114
+ if (installed(["/.bun/", env.BUN_INSTALL], /^install\/global\/node_modules\/pageship\/$/u) || installed([env.BUN_INSTALL_GLOBAL_DIR], /^node_modules\/pageship\/$/u)) return "bun";
3115
+ if (installed(["/yarn/global/", "/yarn/data/global/"], /^node_modules\/pageship\/$/u)) return "yarn";
3116
+ const npm = await environment.npmRoot(signal);
3117
+ const parent = root.slice(0, root.lastIndexOf("/", root.length - 2) + 1);
3118
+ if (npm !== void 0 && parent === comparable(npm, platform)) return "npm";
3119
+ }
3120
+ /**
3121
+ * Update this CLI to the version npm publishes as latest, with the package manager that
3122
+ * installed it. It changes nothing when this CLI is already the latest.
3123
+ */
3124
+ async function update(environment, signal) {
3125
+ const { version } = environment;
3126
+ const latest = await environment.publishedVersion(signal);
3127
+ if (latest !== void 0 && !older(version, latest)) return {
3128
+ status: "cli_current",
3129
+ message: `Pageship ${version} is the latest version.`,
3130
+ version,
3131
+ latest
3132
+ };
3133
+ const manager = await detectManager(environment, signal);
3134
+ signal.throwIfAborted();
3135
+ if (manager === void 0) return {
3136
+ status: "install_unknown",
3137
+ message: `This Pageship CLI, in ${environment.packageRoot}, was not installed globally by npm, pnpm, bun, Yarn or Vite+, so update cannot tell how to replace it. Update it the way you installed it.`,
3138
+ version,
3139
+ ...latest === void 0 ? {} : { latest },
3140
+ path: environment.packageRoot
3141
+ };
3142
+ const argv = updateCommand$1(manager, `pageship@${latest ?? "latest"}`);
3143
+ const run = argv.join(" ");
3144
+ const ran = await environment.run(argv, signal).catch((error) => {
3145
+ if (signal.aborted) throw error;
3146
+ return {
3147
+ exitCode: null,
3148
+ output: error instanceof Error ? error.message : String(error)
3149
+ };
3150
+ });
3151
+ signal.throwIfAborted();
3152
+ if (ran.exitCode !== 0) return {
3153
+ status: "update_failed",
3154
+ message: [ran.exitCode === null ? `Could not run ${run}.` : `${run} failed with exit code ${ran.exitCode}.`, ran.output.trim()].filter(Boolean).join("\n"),
3155
+ version,
3156
+ ...latest === void 0 ? {} : { latest },
3157
+ manager,
3158
+ run,
3159
+ exitCode: ran.exitCode,
3160
+ output: ran.output
3161
+ };
3162
+ const skills = await stat(join(environment.home, ".agents", "skills", "pageship")).then(() => true, () => false);
3163
+ return {
3164
+ status: "cli_updated",
3165
+ message: [latest === void 0 ? `Updated Pageship from ${version} with ${run}.` : `Updated Pageship from ${version} to ${latest} with ${run}.`, ...skills ? [`Run ${commandLine(installNext.command, installNext.args)} to update the Pageship skills to match.`] : []].join("\n"),
3166
+ version,
3167
+ ...latest === void 0 ? {} : { latest },
3168
+ manager,
3169
+ run,
3170
+ ...skills ? { next: installNext } : {}
3171
+ };
3172
+ }
3173
+ //#endregion
2755
3174
  //#region src/cli.ts
2756
3175
  const args = process.argv.slice(2);
2757
3176
  const separator = args.indexOf("--");
@@ -2818,6 +3237,13 @@ const skills = Command.make("skills").pipe(Command.withDescription("Manage the P
2818
3237
  force,
2819
3238
  signal
2820
3239
  }))).pipe(Command.withDescription("Install the Pageship skills in ~/.agents/skills, and link them in ~/.claude/skills when ~/.claude exists."))]));
3240
+ const updateCommand = Command.make("update", {}, () => authCommand("update", (signal) => update({
3241
+ ...skillsEnvironment,
3242
+ packageRoot: dirname(dirname(fileURLToPath(import.meta.url))),
3243
+ env: process.env,
3244
+ npmRoot,
3245
+ run: runCommand(json)
3246
+ }, signal))).pipe(Command.withDescription("Update this CLI to the latest version with the package manager that installed it: npm, pnpm, bun, Yarn or Vite+."));
2821
3247
  const login = Command.make("login", {
2822
3248
  complete: Flag.boolean("complete").pipe(Flag.withDefault(false), Flag.withDescription("Check a pending authorization once, without waiting.")),
2823
3249
  restart: Flag.boolean("restart").pipe(Flag.withDefault(false), Flag.withDescription("Start a new authorization code.")),
@@ -2905,6 +3331,18 @@ const visibility = Command.make("visibility", {
2905
3331
  visibility,
2906
3332
  signal
2907
3333
  }))).pipe(Command.withDescription("Make a document private or public."));
3334
+ const viewers = Command.make("viewers", {
3335
+ documentId: documentIdArgument,
3336
+ 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)),
3337
+ 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)),
3338
+ server: serverFlag
3339
+ }, ({ documentId, add, remove, server }) => authCommand("viewers", (signal) => manageViewers({
3340
+ server: Option.getOrUndefined(server),
3341
+ documentId,
3342
+ add,
3343
+ remove,
3344
+ signal
3345
+ }))).pipe(Command.withDescription("List the GitHub users who can read a private document, or add and remove them."));
2908
3346
  const restore = Command.make("restore", {
2909
3347
  documentId: documentIdArgument,
2910
3348
  server: serverFlag,
@@ -2966,7 +3404,7 @@ function authCommand(name, operation) {
2966
3404
  settle(name, result);
2967
3405
  if (json) return;
2968
3406
  if (thrown) {
2969
- console.error(`${name === "status" ? "Could not check status" : name === "skills" ? "Could not install the Pageship skills" : "Pageship request failed"}: ${result.message}`);
3407
+ console.error(`${name === "status" ? "Could not check status" : name === "skills" ? "Could not install the Pageship skills" : name === "update" ? "Could not update Pageship" : "Pageship request failed"}: ${result.message}`);
2970
3408
  return;
2971
3409
  }
2972
3410
  console.log(result.message);
@@ -2977,7 +3415,7 @@ function authCommand(name, operation) {
2977
3415
  }
2978
3416
  });
2979
3417
  }
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([
3418
+ 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, and update the CLI with update."), Command.withSubcommands([
2981
3419
  status,
2982
3420
  login,
2983
3421
  whoami,
@@ -2986,8 +3424,10 @@ const command = Command.make("pageship").pipe(Command.withDescription("Pageship
2986
3424
  history,
2987
3425
  download,
2988
3426
  visibility,
3427
+ viewers,
2989
3428
  restore,
2990
- skills
3429
+ skills,
3430
+ updateCommand
2991
3431
  ]), Command.withGlobalFlags([jsonFlag]));
2992
3432
  const effectiveArgs = args.every((arg, index) => arg === "--json" || arg.startsWith("--json=") && booleanValue(arg.slice(7)) !== void 0 || index > 0 && args[index - 1] === "--json" && booleanValue(arg) !== void 0) ? [...args, "--help"] : args;
2993
3433
  const program = Command.runWith(command, {
@@ -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.6.0"
7
7
  ---
8
8
 
9
9
  # Pageship
@@ -33,6 +33,7 @@ With `--json`, the CLI prints one JSON object on one line. Parse it before you d
33
33
  - `next` is one follow-up command. `resolutions` holds named alternatives for a decision, each a follow-up command or a list of them.
34
34
  - Other fields depend on the status, such as `document`, `candidates`, `choices`, `user` or `idempotencyKey`.
35
35
  - `skills install` returns `skills_installed` or `skills_modified`, as described in [Install the Pageship skills](#install-the-pageship-skills).
36
+ - `update` returns `cli_updated`, `cli_current`, `install_unknown` or `update_failed`, as described in [Update the CLI](#update-the-cli).
36
37
 
37
38
  A follow-up has a `command` and an `args` array. Run it as `pageship --json <command> <args...>` with `args` unchanged. They already carry the server, target document, expected revision and retry key, so never rebuild them from memory.
38
39
 
@@ -50,7 +51,17 @@ pageship --json status
50
51
 
51
52
  Status only reads; it never signs in, claims an address or uploads. On `admitted`, sign-in and the workspace address are set, unless `next` is a `login`: that comes from an older server that does not report the address, and running it confirms the address. A `next` of `skills install` on `admitted` is about the skills, below. On `signed_out` or `address_required`, sign in as described below. On `uninvited`, report the `message`: the user must ask the server owner for an invitation, or sign in with another account using `login --restart`. On `unreachable`, `client_too_old`, `server_too_old`, `unsupported` or `configuration_unreadable`, report the `message`; signing in does not fix these. A ready status does not prove that a publish will succeed.
52
53
 
53
- Status also reports the Pageship skills in `skills`: `missing`, `current` or `older` than the CLI. They never block publishing, and a status that is otherwise ready still succeeds. When `skills` is `missing` or `older`, tell the user, and install them as below only if they agree. Never run a `skills install` from `next` without asking. When `cli` is `older`, a newer CLI is published: tell the user, and offer to update it with the package manager that installed it before installing the skills.
54
+ Status also reports the Pageship skills in `skills`: `missing`, `current` or `older` than the CLI. They never block publishing, and a status that is otherwise ready still succeeds. When `skills` is `missing` or `older`, tell the user, and install them as below only if they agree. Never run a `skills install` from `next` without asking. When `cli` is `older`, a newer CLI is published: tell the user, and offer to update it, as below, before installing the skills.
55
+
56
+ ## Update the CLI
57
+
58
+ With the user's agreement, update the CLI with the package manager that installed it:
59
+
60
+ ```sh
61
+ pageship --json update
62
+ ```
63
+
64
+ On `cli_updated`, the new version is installed. Its `next` is a `skills install`, present when the Pageship skills are installed, since they still match the old version: install them as below if the user agrees. On `cli_current`, the CLI is already the latest. On `install_unknown`, no supported package manager installed it globally, so nothing ran; tell the user to update it the way they installed it. On `update_failed`, report the `message`, which carries the package manager's error.
54
65
 
55
66
  ## Install the Pageship skills
56
67
 
@@ -97,7 +108,7 @@ After a successful upload the CLI adds the document marker to the end of the loc
97
108
  On `published`, tell the user:
98
109
 
99
110
  - the document URL, `document.url`;
100
- - who can read it, from `document.visibility`: a private document opens only for its owner, signed in;
111
+ - who can read it, from `document.visibility`: a private document opens only for its owner and its viewers, signed in;
101
112
  - whether this publish created a new document or revised an existing one, from `change`: `created` or `revised`.
102
113
 
103
114
  Keep `idempotencyKey` only while you resolve an uncertain attempt.
@@ -124,7 +135,11 @@ This works for any document the user owns, even when the file moved or belongs t
124
135
 
125
136
  ## Share a document
126
137
 
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).
138
+ 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.
139
+
140
+ 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.
141
+
142
+ 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
143
 
129
144
  ## Use another server
130
145
 
@@ -137,6 +152,7 @@ Load the reference for the task:
137
152
  - [List and history](references/list-and-history.md): find a document's `id`, `visibility`, current `revisionId` and `url`, and inspect its revisions.
138
153
  - [Download](references/download.md): read a document's HTML, current or from an earlier revision.
139
154
  - [Visibility](references/visibility.md): make a document public or private.
155
+ - [Viewers](references/viewers.md): list, add or remove the GitHub users who can read a private document.
140
156
  - [Restore](references/restore.md): make an earlier revision current again.
141
157
 
142
158
  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. On `viewers_updated`, `added` holds each account it added, with `login`, `name` and `githubAccountId`. 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
+ The command adds and removes everyone together or no one. On any failure, nothing changed. Its `next` repeats the whole command. It takes up to 20 `--add` and 50 `--remove` values; split a longer list across several commands.
@@ -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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.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.6.0",
4
4
  "description": "Pageship CLI for checking server API compatibility.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {