pageship 0.5.0 → 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,30 +675,18 @@ 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",
680
- ...options,
681
- headers: {
682
- "Content-Type": "application/json",
683
- ...options.headers
684
- }
685
- });
686
678
  const viewersList = (options) => (options.client ?? client).get({
687
679
  url: "/api/v1/documents/{id}/viewers",
688
680
  ...options
689
681
  });
690
- const viewersAdd = (options) => (options.client ?? client).post({
691
- url: "/api/v1/documents/{id}/viewers",
682
+ const viewersUpdateSharing = (options) => (options.client ?? client).patch({
683
+ url: "/api/v1/documents/{id}/sharing",
692
684
  ...options,
693
685
  headers: {
694
686
  "Content-Type": "application/json",
695
687
  ...options.headers
696
688
  }
697
689
  });
698
- const viewersRemove = (options) => (options.client ?? client).delete({
699
- url: "/api/v1/documents/{id}/viewers/{githubAccountId}",
700
- ...options
701
- });
702
690
  const githubGetUser = (options) => (options.client ?? client).get({
703
691
  url: "/api/v1/github/users/{login}",
704
692
  ...options
@@ -797,10 +785,12 @@ function validRevision(value, documentId) {
797
785
  var DocumentRequestError = class extends Error {
798
786
  status;
799
787
  httpStatus;
800
- constructor(status, message, httpStatus) {
788
+ account;
789
+ constructor(status, message, httpStatus, account) {
801
790
  super(message);
802
791
  this.status = status;
803
792
  this.httpStatus = httpStatus;
793
+ this.account = account;
804
794
  }
805
795
  };
806
796
  function failed(response, error) {
@@ -921,29 +911,6 @@ async function restoreDocumentRevision(baseUrl, id, input, options = {}) {
921
911
  if (record(data) && data.status === "idempotency_conflict") return { status: "idempotency_conflict" };
922
912
  throw new Error("Invalid document restoration response. Retry with the same idempotency key.");
923
913
  }
924
- async function updateDocumentVisibility(baseUrl, id, visibility, options = {}) {
925
- const { data: received, response, error } = await documentsUpdateVisibility({
926
- ...changeOptions(baseUrl, options),
927
- path: { id },
928
- body: { visibility }
929
- });
930
- if (!response?.ok) {
931
- if (response?.status === 400) throw new DocumentRequestError("invalid_visibility", "Choose private or public.", 400);
932
- if (response?.status === 401) throw new DocumentRequestError("signed_out", "Sign in to update this document.", 401);
933
- if (response?.status === 403) throw new DocumentRequestError("uninvited", "Access unavailable.", 403);
934
- if (response?.status === 404) throw new DocumentRequestError("unavailable", "Document unavailable.", 404);
935
- throw new Error("Could not update document visibility. Try again.", { cause: error });
936
- }
937
- const data = received;
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 {
941
- status: "visibility_updated",
942
- document: updated,
943
- removedViewers
944
- };
945
- throw new Error("Invalid document visibility response.");
946
- }
947
914
  async function publishDocument(baseUrl, input, options = {}) {
948
915
  if (input.action) {
949
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);
@@ -1014,13 +981,14 @@ const viewerRefusals = {
1014
981
  owner_account: "You already have access to your own document.",
1015
982
  github_sign_in_required: "GitHub refused your sign-in. Sign in to Pageship again.",
1016
983
  github_rate_limited: "GitHub is limiting requests. Try again in a minute.",
1017
- github_unavailable: "GitHub is unavailable. Try again."
984
+ github_unavailable: "GitHub is unavailable. Try again.",
985
+ invalid_sharing: "A change can't both add and remove the same account."
1018
986
  };
1019
987
  function isViewerRefusal(value) {
1020
988
  return typeof value === "string" && Object.hasOwn(viewerRefusals, value);
1021
989
  }
1022
990
  function viewerFailed(response, error) {
1023
- if (response && record(error) && isViewerRefusal(error.status)) throw new DocumentRequestError(error.status, viewerRefusals[error.status], response.status);
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);
1024
992
  failed(response, error);
1025
993
  }
1026
994
  async function getDocumentViewers(baseUrl, id, options = {}) {
@@ -1032,32 +1000,35 @@ async function getDocumentViewers(baseUrl, id, options = {}) {
1032
1000
  if (data?.status === "listed" && Array.isArray(data.viewers) && data.viewers.every(validViewer)) return data;
1033
1001
  throw new Error("Invalid document viewer listing response.");
1034
1002
  }
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({
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({
1038
1017
  ...changeOptions(baseUrl, options),
1039
1018
  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
- }
1019
+ body: change
1057
1020
  });
1058
1021
  if (!response?.ok) viewerFailed(response, error);
1059
- if (data?.status === "removed") return { status: "removed" };
1060
- throw new Error("Invalid document viewer removal response.");
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.");
1061
1032
  }
1062
1033
  async function getGitHubUser(baseUrl, login, options = {}) {
1063
1034
  const { data, response, error } = await githubGetUser({
@@ -1448,6 +1419,8 @@ const outcomes = {
1448
1419
  restored: "succeeded",
1449
1420
  viewers_updated: "succeeded",
1450
1421
  skills_installed: "succeeded",
1422
+ cli_updated: "succeeded",
1423
+ cli_current: "succeeded",
1451
1424
  authorization_pending: "pending",
1452
1425
  expired: "pending",
1453
1426
  signed_out: "pending",
@@ -1483,6 +1456,8 @@ const outcomes = {
1483
1456
  document_id_taken: "failed",
1484
1457
  publication_action_unsupported: "failed",
1485
1458
  skills_modified: "failed",
1459
+ install_unknown: "failed",
1460
+ update_failed: "failed",
1486
1461
  ambiguous_viewer: "failed",
1487
1462
  document_public: "failed",
1488
1463
  github_user_not_found: "failed",
@@ -1504,7 +1479,8 @@ const commandNames = [
1504
1479
  "visibility",
1505
1480
  "viewers",
1506
1481
  "restore",
1507
- "skills"
1482
+ "skills",
1483
+ "update"
1508
1484
  ];
1509
1485
  const isCommandName = (value) => commandNames.some((name) => name === value);
1510
1486
  /** Exit codes follow the outcome, except an interruption, which exits 130. */
@@ -1554,7 +1530,7 @@ function commandLine(command, args) {
1554
1530
  }
1555
1531
  //#endregion
1556
1532
  //#region package.json
1557
- var version = "0.5.0";
1533
+ var version = "0.6.0";
1558
1534
  //#endregion
1559
1535
  //#region src/publish.ts
1560
1536
  const execute = promisify(execFile);
@@ -2427,10 +2403,12 @@ function downloadDocument(options) {
2427
2403
  }
2428
2404
  function changeVisibility(options) {
2429
2405
  return asOwner(options.server, options.signal, async ({ origin }, request) => {
2430
- const result = await updateDocumentVisibility(origin, options.documentId, options.visibility, request);
2431
- const { document, removedViewers } = result;
2406
+ const { document, removed } = await updateDocumentSharing(origin, options.documentId, { visibility: options.visibility }, request);
2407
+ const removedViewers = removed.length;
2432
2408
  return {
2433
- ...result,
2409
+ status: "visibility_updated",
2410
+ document,
2411
+ removedViewers,
2434
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}`
2435
2413
  };
2436
2414
  });
@@ -2510,6 +2488,10 @@ const installNext = {
2510
2488
  command: "skills",
2511
2489
  args: ["install"]
2512
2490
  };
2491
+ const updateNext = {
2492
+ command: "update",
2493
+ args: []
2494
+ };
2513
2495
  /** The output of `node --version` from PATH, or undefined when it does not run. */
2514
2496
  const nodeOnPath = () => new Promise((resolve) => {
2515
2497
  execFile("node", ["--version"], {
@@ -2594,6 +2576,7 @@ async function replaceFolder(source, target) {
2594
2576
  }
2595
2577
  }
2596
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. */
2597
2580
  const older = (installed, cli) => {
2598
2581
  const parts = (version) => version.split(/[.+-]/u).slice(0, 3).map(Number);
2599
2582
  const [a, b] = [parts(installed), parts(cli)];
@@ -2617,7 +2600,7 @@ async function inspectCli(environment, signal) {
2617
2600
  return {
2618
2601
  cli: "older",
2619
2602
  latest,
2620
- 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)}.`
2621
2604
  };
2622
2605
  }
2623
2606
  /** The recommended skills and Node that the CLI could not find. It installs none of them. */
@@ -2909,7 +2892,7 @@ function viewerList(documentId, viewers) {
2909
2892
  }
2910
2893
  /**
2911
2894
  * 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.
2895
+ * looked up first, then one sharing change applies them all or none.
2913
2896
  */
2914
2897
  function manageViewers(options) {
2915
2898
  const { documentId, add, remove } = options;
@@ -2927,13 +2910,11 @@ function manageViewers(options) {
2927
2910
  ...add.flatMap((value) => ["--add", value])
2928
2911
  ]
2929
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
+ };
2930
2917
  const progress = [];
2931
- const added = [];
2932
- const removed = [];
2933
- const changed = () => added.length || removed.length ? {
2934
- added,
2935
- removed
2936
- } : {};
2937
2918
  try {
2938
2919
  const { viewers } = await getDocumentViewers(origin, documentId, request);
2939
2920
  if (!add.length && !remove.length) return {
@@ -2965,38 +2946,39 @@ function manageViewers(options) {
2965
2946
  user,
2966
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.`
2967
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
+ };
2968
2953
  progress.push(`Found ${account(user)}.`);
2969
2954
  if (!accounts.some(({ githubAccountId }) => githubAccountId === user.githubAccountId)) accounts.push(user);
2970
2955
  }
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);
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);
2982
2963
  return {
2983
2964
  status: "viewers_updated",
2984
2965
  documentId,
2985
2966
  added,
2986
2967
  removed,
2987
2968
  notViewers,
2988
- viewers: current.viewers,
2969
+ viewers: shared.viewers,
2989
2970
  message: [
2990
2971
  ...progress,
2972
+ ...removed.map((viewer) => `Removed ${account(viewer)}.`),
2973
+ ...added.map((viewer) => `Added ${account(viewer)}.`),
2991
2974
  ...notViewers.map((value) => `${value} is not a viewer of ${documentId}.`),
2992
- viewerList(documentId, current.viewers)
2975
+ viewerList(documentId, shared.viewers)
2993
2976
  ].join("\n")
2994
2977
  };
2995
2978
  } catch (error) {
2996
2979
  const prefix = (result) => ({
2997
2980
  ...result,
2998
2981
  documentId,
2999
- ...changed(),
3000
2982
  message: [...progress, result.message].join("\n")
3001
2983
  });
3002
2984
  if (isRefusal(error)) {
@@ -3013,20 +2995,182 @@ function manageViewers(options) {
3013
2995
  });
3014
2996
  return prefix(refusal(error, login));
3015
2997
  }
3016
- if (!added.length && !removed.length) throw error;
3017
2998
  return prefix(options.signal.aborted ? {
3018
2999
  status: "interrupted",
3019
3000
  message: `Operation interrupted. Run ${commandLine(next.command, next.args)} to finish.`,
3020
3001
  next
3021
3002
  } : {
3022
3003
  status: "request_failed",
3023
- message: `${error instanceof Error ? error.message : String(error)} Run ${commandLine(next.command, next.args)} to finish.`,
3004
+ message: `${error instanceof Error ? error.message : String(error)} Run ${commandLine(next.command, next.args)} to retry.`,
3024
3005
  next
3025
3006
  });
3026
3007
  }
3027
3008
  });
3028
3009
  }
3029
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
3030
3174
  //#region src/cli.ts
3031
3175
  const args = process.argv.slice(2);
3032
3176
  const separator = args.indexOf("--");
@@ -3093,6 +3237,13 @@ const skills = Command.make("skills").pipe(Command.withDescription("Manage the P
3093
3237
  force,
3094
3238
  signal
3095
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+."));
3096
3247
  const login = Command.make("login", {
3097
3248
  complete: Flag.boolean("complete").pipe(Flag.withDefault(false), Flag.withDescription("Check a pending authorization once, without waiting.")),
3098
3249
  restart: Flag.boolean("restart").pipe(Flag.withDefault(false), Flag.withDescription("Start a new authorization code.")),
@@ -3253,7 +3404,7 @@ function authCommand(name, operation) {
3253
3404
  settle(name, result);
3254
3405
  if (json) return;
3255
3406
  if (thrown) {
3256
- 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}`);
3257
3408
  return;
3258
3409
  }
3259
3410
  console.log(result.message);
@@ -3264,7 +3415,7 @@ function authCommand(name, operation) {
3264
3415
  }
3265
3416
  });
3266
3417
  }
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([
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([
3268
3419
  status,
3269
3420
  login,
3270
3421
  whoami,
@@ -3275,7 +3426,8 @@ const command = Command.make("pageship").pipe(Command.withDescription("Pageship
3275
3426
  visibility,
3276
3427
  viewers,
3277
3428
  restore,
3278
- skills
3429
+ skills,
3430
+ updateCommand
3279
3431
  ]), Command.withGlobalFlags([jsonFlag]));
3280
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;
3281
3433
  const program = Command.runWith(command, {
@@ -3,7 +3,7 @@ name: pageship
3
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.5.0"
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
 
@@ -12,7 +12,7 @@ Set `PAGESHIP_DOCUMENT` to the document's `id`, then add each named username wit
12
12
  pageship --json viewers "$PAGESHIP_DOCUMENT" --add octocat --add hubot
13
13
  ```
14
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.
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
16
 
17
17
  ## List and remove viewers
18
18
 
@@ -40,4 +40,4 @@ pageship --json viewers "$PAGESHIP_DOCUMENT" --remove octocat
40
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
41
  - `github_rate_limited` or `github_unavailable`: wait a minute, then run the returned `next`.
42
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.
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.
@@ -3,7 +3,7 @@ name: pageship-chart
3
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.5.0"
6
+ version: "0.6.0"
7
7
  ---
8
8
 
9
9
  # Chart
@@ -3,7 +3,7 @@ name: pageship-code-block
3
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.5.0"
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.5.0"
6
+ version: "0.6.0"
7
7
  ---
8
8
 
9
9
  # Decision record
@@ -3,7 +3,7 @@ name: pageship-diagram
3
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.5.0"
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.5.0"
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.5.0"
6
+ version: "0.6.0"
7
7
  ---
8
8
 
9
9
  # Explainer
@@ -3,7 +3,7 @@ name: pageship-interaction
3
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.5.0"
6
+ version: "0.6.0"
7
7
  ---
8
8
 
9
9
  # Interaction
@@ -3,7 +3,7 @@ name: pageship-key-figures
3
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.5.0"
6
+ version: "0.6.0"
7
7
  ---
8
8
 
9
9
  # Key figures
@@ -3,7 +3,7 @@ name: pageship-media
3
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.5.0"
6
+ version: "0.6.0"
7
7
  ---
8
8
 
9
9
  # Media
@@ -3,7 +3,7 @@ name: pageship-motion
3
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.5.0"
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.5.0"
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.5.0"
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.5.0"
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.5.0"
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.5.0"
6
+ version: "0.6.0"
7
7
  ---
8
8
 
9
9
  # Spec
@@ -3,7 +3,7 @@ name: pageship-table
3
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.5.0"
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.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Pageship CLI for checking server API compatibility.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {