pageship 0.6.1 → 0.7.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
@@ -1,6 +1,6 @@
1
1
  # Pageship CLI
2
2
 
3
- Pageship is an HTML publishing service in development. This CLI signs you in to the hosted Pageship service through a browser, publishes HTML files as documents behind stable links, and lists, downloads, restores and changes the visibility of the documents you published. It also installs the Pageship skills for your agents.
3
+ Pageship is an HTML publishing service in development. This CLI signs you in to the hosted Pageship service through a browser, publishes HTML files as documents behind stable links, and lists, downloads, restores and changes the visibility of the documents you published. It also reads documents other people shared with you, by their links. It also installs the Pageship skills for your agents.
4
4
 
5
5
  The full reference, with every command, option and result, is at https://pageship.dev/docs/cli.
6
6
 
package/dist/cli.mjs CHANGED
@@ -675,6 +675,18 @@ const documentsRestore = (options) => (options.client ?? client).post({
675
675
  ...options.headers
676
676
  }
677
677
  });
678
+ const installationsList = (options) => (options?.client ?? client).get({
679
+ url: "/api/v1/installations",
680
+ ...options
681
+ });
682
+ const installationsRevoke = (options) => (options.client ?? client).post({
683
+ url: "/api/v1/installations/revoke",
684
+ ...options,
685
+ headers: {
686
+ "Content-Type": "application/json",
687
+ ...options.headers
688
+ }
689
+ });
678
690
  const viewersList = (options) => (options.client ?? client).get({
679
691
  url: "/api/v1/documents/{id}/viewers",
680
692
  ...options
@@ -701,6 +713,23 @@ const workspaceDomainPattern = workspaceDomain.replaceAll(".", "\\.");
701
713
  const workspaceHostname = (subdomain) => `${subdomain}.${workspaceDomain}`;
702
714
  /** A valid workspace label: the subdomain of one workspace address, as a DNS label. */
703
715
  const workspaceLabelPattern = /^[a-z\d](?:[a-z\d-]{0,61}[a-z\d])?$/u;
716
+ /**
717
+ * Service names that are never a workspace. Migration `0002_personal_subdomains.sql` reserves
718
+ * them in the address table, and readiness checks that the table still holds each one.
719
+ */
720
+ const reservedWorkspaceSubdomains = [
721
+ "www",
722
+ "api",
723
+ "app",
724
+ "auth",
725
+ "admin",
726
+ "dashboard",
727
+ "docs",
728
+ "status",
729
+ "support",
730
+ "mail",
731
+ "pageship"
732
+ ];
704
733
  //#endregion
705
734
  //#region ../../packages/api-client/src/address.ts
706
735
  /** Generated TypeScript does not validate remote JSON, so check an address before showing it. */
@@ -858,7 +887,11 @@ async function getDocumentRevisions(baseUrl, id, options = {}) {
858
887
  };
859
888
  throw new Error("Invalid document revision listing response.");
860
889
  }
861
- /** Exact owner-only bytes of the current revision, or of one selected historical revision. */
890
+ /**
891
+ * Exact bytes of the current revision, or of one selected historical revision. The owner reads
892
+ * any revision; another admitted account reads the current one of a public document, or of a
893
+ * private one it is a viewer of.
894
+ */
862
895
  async function getDocumentContent(baseUrl, id, options = {}) {
863
896
  const request = {
864
897
  ...requestOptions(baseUrl, options),
@@ -1061,6 +1094,31 @@ async function getGitHubUser(baseUrl, login, options = {}) {
1061
1094
  throw new Error("Invalid GitHub user response.");
1062
1095
  }
1063
1096
  //#endregion
1097
+ //#region ../../packages/api-client/src/installations.ts
1098
+ async function requestInstallations(baseUrl, installationId, { signal, token } = {}) {
1099
+ const timeout = AbortSignal.timeout(1e4);
1100
+ const options = {
1101
+ baseUrl,
1102
+ credentials: token ? "omit" : "include",
1103
+ headers: token ? { Authorization: `Bearer ${token}` } : void 0,
1104
+ redirect: "error",
1105
+ cache: "no-store",
1106
+ throwOnError: false,
1107
+ signal: signal ? AbortSignal.any([timeout, signal]) : timeout
1108
+ };
1109
+ const { data, response, error } = installationId === void 0 ? await installationsList(options) : await installationsRevoke({
1110
+ ...options,
1111
+ cache: "default",
1112
+ body: { installationId }
1113
+ });
1114
+ if (!response?.ok) throw new Error("Could not update CLI installations. Try again.", { cause: error });
1115
+ if (data?.status === "signed_out" || data?.status === "uninvited" || data?.status === "unavailable" || data?.status === "revoked") return { status: data.status };
1116
+ if (data?.status === "listed" && Array.isArray(data.installations) && data.installations.every((entry) => entry && typeof entry.id === "string" && entry.id.length > 0 && typeof entry.createdAt === "string" && Number.isFinite(Date.parse(entry.createdAt)) && typeof entry.expiresAt === "string" && Number.isFinite(Date.parse(entry.expiresAt)))) return data;
1117
+ throw new Error("Invalid CLI installations response.");
1118
+ }
1119
+ /** Revoke one CLI installation. With `token`, a CLI revokes its own installation. */
1120
+ const revokeInstallation = (baseUrl, installationId, options) => requestInstallations(baseUrl, installationId, options);
1121
+ //#endregion
1064
1122
  //#region ../../packages/api-client/src/index.ts
1065
1123
  async function getWorkspace(baseUrl, options = {}) {
1066
1124
  const timeout = AbortSignal.timeout(1e4);
@@ -1436,12 +1494,14 @@ const outcomes = {
1436
1494
  published: "succeeded",
1437
1495
  listed: "succeeded",
1438
1496
  downloaded: "succeeded",
1497
+ read: "succeeded",
1439
1498
  visibility_updated: "succeeded",
1440
1499
  restored: "succeeded",
1441
1500
  viewers_updated: "succeeded",
1442
1501
  skills_installed: "succeeded",
1443
1502
  cli_updated: "succeeded",
1444
1503
  cli_current: "succeeded",
1504
+ logged_out: "succeeded",
1445
1505
  authorization_pending: "pending",
1446
1506
  expired: "pending",
1447
1507
  signed_out: "pending",
@@ -1468,6 +1528,7 @@ const outcomes = {
1468
1528
  invalid_attempt: "failed",
1469
1529
  invalid_action: "failed",
1470
1530
  invalid_file: "failed",
1531
+ invalid_document: "failed",
1471
1532
  invalid_output: "failed",
1472
1533
  invalid_restore: "failed",
1473
1534
  output_exists: "failed",
@@ -1493,10 +1554,12 @@ const commandNames = [
1493
1554
  "status",
1494
1555
  "login",
1495
1556
  "whoami",
1557
+ "logout",
1496
1558
  "publish",
1497
1559
  "list",
1498
1560
  "history",
1499
1561
  "download",
1562
+ "read",
1500
1563
  "visibility",
1501
1564
  "viewers",
1502
1565
  "restore",
@@ -1551,7 +1614,7 @@ function commandLine(command, args) {
1551
1614
  }
1552
1615
  //#endregion
1553
1616
  //#region package.json
1554
- var version = "0.6.1";
1617
+ var version = "0.7.0";
1555
1618
  //#endregion
1556
1619
  //#region src/publish.ts
1557
1620
  const execute = promisify(execFile);
@@ -2270,6 +2333,48 @@ async function identify(serverValue, signal) {
2270
2333
  }
2271
2334
  }
2272
2335
  //#endregion
2336
+ //#region src/document-reference.ts
2337
+ const documentPath = /^\/d\/([^/-]+)(?:-[^/]+)?(?:\/revisions\/([^/]+))?$/u;
2338
+ /** True for any URL, so the value is read as a link rather than as a malformed ID. */
2339
+ const isUrl = (value) => URL.canParse(value);
2340
+ /**
2341
+ * Reads a document ID from `list`, or a document or revision link the user pasted.
2342
+ * A link must be one a workspace host would serve, on any workspace: the server decides
2343
+ * access, so a link to a document this account can't read gets the same answer as an
2344
+ * unknown ID.
2345
+ * `revisionId` is an explicit `--revision`; it must agree with a revision link.
2346
+ */
2347
+ function documentReference(value, revisionId) {
2348
+ const named = isDocumentId(value) ? { documentId: value } : documentLink(value);
2349
+ if (!named) return {
2350
+ status: "invalid_document",
2351
+ message: `${value} is not a Pageship document. Use a six-character document ID from list, or a document link such as https://<workspace>.${workspaceDomain}/d/<id>-<slug>.`
2352
+ };
2353
+ if (revisionId && named.revisionId && revisionId.toLowerCase() !== named.revisionId.toLowerCase()) return {
2354
+ status: "invalid_document",
2355
+ message: `The link names revision ${named.revisionId}, but --revision names ${revisionId}. Use one of them.`
2356
+ };
2357
+ const revision = named.revisionId ?? revisionId;
2358
+ return {
2359
+ documentId: named.documentId,
2360
+ ...revision ? { revisionId: revision } : {}
2361
+ };
2362
+ }
2363
+ function documentLink(value) {
2364
+ if (!isUrl(value)) return;
2365
+ const url = new URL(value);
2366
+ const suffix = `.${workspaceDomain}`;
2367
+ const label = url.hostname.slice(0, -suffix.length);
2368
+ if (url.protocol !== "https:" || url.port || url.username || url.password || !url.hostname.endsWith(suffix) || !workspaceLabelPattern.test(label) || reservedWorkspaceSubdomains.some((name) => name === label)) return;
2369
+ const [, documentId, revisionId] = documentPath.exec(url.pathname) ?? [];
2370
+ if (!isDocumentId(documentId)) return;
2371
+ if (revisionId === void 0) return { documentId };
2372
+ return isRevisionId(revisionId) ? {
2373
+ documentId,
2374
+ revisionId
2375
+ } : void 0;
2376
+ }
2377
+ //#endregion
2273
2378
  //#region src/documents.ts
2274
2379
  /** `1 viewer`, `2 viewers`. */
2275
2380
  const viewerCount = (count) => `${count} viewer${count === 1 ? "" : "s"}`;
@@ -2386,12 +2491,16 @@ function invalidOutput(output, error) {
2386
2491
  message: `Could not write ${output}. ${error instanceof Error ? error.message : "Check the path and try again."}`
2387
2492
  };
2388
2493
  }
2389
- function downloadDocument(options) {
2494
+ /** `document` is an ID from `list` or a document or revision link; see `documentReference`. */
2495
+ async function downloadDocument(options) {
2496
+ const reference = documentReference(options.document, options.revisionId);
2497
+ if ("status" in reference) return reference;
2498
+ const { documentId, revisionId } = reference;
2390
2499
  const output = resolve(options.output);
2391
2500
  return asOwner(options.server, options.signal, async ({ origin }, request) => {
2392
- const bytes = await getDocumentContent(origin, options.documentId, {
2501
+ const bytes = await getDocumentContent(origin, documentId, {
2393
2502
  ...request,
2394
- revisionId: options.revisionId
2503
+ revisionId
2395
2504
  });
2396
2505
  let file;
2397
2506
  try {
@@ -2414,11 +2523,48 @@ function downloadDocument(options) {
2414
2523
  await file.close();
2415
2524
  return {
2416
2525
  status: "downloaded",
2417
- documentId: options.documentId,
2418
- revisionId: options.revisionId ?? null,
2526
+ documentId,
2527
+ revisionId: revisionId ?? null,
2419
2528
  output,
2420
2529
  byteLength: bytes.byteLength,
2421
- message: `Saved ${bytes.byteLength} bytes of ${options.documentId}${options.revisionId ? ` revision ${options.revisionId}` : ""} to ${output}.`
2530
+ message: `Saved ${bytes.byteLength} bytes of ${documentId}${revisionId ? ` revision ${revisionId}` : ""} to ${output}.`
2531
+ };
2532
+ });
2533
+ }
2534
+ /**
2535
+ * Reads a document's HTML for printing. `document` is an ID or a link, as for download. The
2536
+ * owner reads any revision; anyone reads a public document's current one, and a viewer a
2537
+ * private one's. The server answers a document this
2538
+ * account can't read like a missing one, so one message covers all three.
2539
+ */
2540
+ async function readDocument(options) {
2541
+ const reference = documentReference(options.document);
2542
+ if ("status" in reference) return reference;
2543
+ const { documentId, revisionId } = reference;
2544
+ return asOwner(options.server, options.signal, async ({ origin }, request) => {
2545
+ let bytes;
2546
+ try {
2547
+ bytes = await getDocumentContent(origin, documentId, {
2548
+ ...request,
2549
+ revisionId
2550
+ });
2551
+ } catch (error) {
2552
+ if (isRefusal(error) && error.status === "unavailable") return {
2553
+ status: "unavailable",
2554
+ documentId,
2555
+ revisionId: revisionId ?? null,
2556
+ message: `${options.document} is unavailable. It doesn't exist, was deleted, or this account can't read it.${revisionId ? " Only the owner can read an earlier revision." : ""} Check the link, and that this installation is signed in as the owner or a viewer if it is private.`
2557
+ };
2558
+ throw error;
2559
+ }
2560
+ return {
2561
+ status: "read",
2562
+ documentId,
2563
+ revisionId: revisionId ?? null,
2564
+ byteLength: bytes.byteLength,
2565
+ html: new TextDecoder().decode(bytes),
2566
+ bytes,
2567
+ message: `Read ${bytes.byteLength} bytes of ${documentId}${revisionId ? ` revision ${revisionId}` : ""}.`
2422
2568
  };
2423
2569
  });
2424
2570
  }
@@ -2489,6 +2635,111 @@ function restoreRevision(options) {
2489
2635
  });
2490
2636
  }
2491
2637
  //#endregion
2638
+ //#region src/logout.ts
2639
+ const errorText = (error) => error instanceof Error ? error.message : String(error);
2640
+ const exists = (path) => lstat(path).then(() => true, (error) => {
2641
+ if (error.code === "ENOENT") return false;
2642
+ throw error;
2643
+ });
2644
+ /**
2645
+ * Sign this installation out: revoke its session on the server, as the dashboard's Revoke
2646
+ * access does, then delete the stored credential. The credential is deleted only once the
2647
+ * server confirms the session is gone, so a failure never leaves a live session that the user
2648
+ * believes has ended.
2649
+ */
2650
+ async function logout(serverValue, signal) {
2651
+ const selection = selectServer(serverValue);
2652
+ const server = selection.origin;
2653
+ const retry = {
2654
+ command: "logout",
2655
+ args: [...selection.args]
2656
+ };
2657
+ const retryLine = commandLine(retry.command, retry.args);
2658
+ /** A failure that keeps the credential. Only a retry that can succeed gets `next`. */
2659
+ const credentialKept = (status, reason, retryable = true) => ({
2660
+ status,
2661
+ message: `${reason} Your credential is kept, so this installation is still signed in.${retryable ? ` Run ${retryLine} to try again.` : ""}`,
2662
+ ...retryable ? { next: retry } : {}
2663
+ });
2664
+ const notSignedIn = "This installation is not signed in, so there was nothing to sign out of.";
2665
+ let store;
2666
+ try {
2667
+ const directory = configDirectory();
2668
+ if (isAbsolute(directory) && !await exists(directory)) return {
2669
+ status: "logged_out",
2670
+ revoked: false,
2671
+ message: notSignedIn
2672
+ };
2673
+ store = await credentialStore(server);
2674
+ } catch (error) {
2675
+ return credentialKept("request_failed", `Could not open the stored credential. ${errorText(error)}`);
2676
+ }
2677
+ try {
2678
+ const credentials = await store.read();
2679
+ const discarded = credentials.pending ? " An authorization code waiting for approval was discarded. If you approved it, its session may still be listed under CLI installations in the dashboard; revoke it there." : "";
2680
+ if (!credentials.token) {
2681
+ if (credentials.pending) await store.write({});
2682
+ return {
2683
+ status: "logged_out",
2684
+ revoked: false,
2685
+ message: `${notSignedIn}${discarded}`
2686
+ };
2687
+ }
2688
+ const token = credentials.token;
2689
+ let revoked;
2690
+ try {
2691
+ const compatibility = await checkCompatibility(server, { signal });
2692
+ if (compatibility.status !== "compatible") return {
2693
+ ...compatibility,
2694
+ ...credentialKept(compatibility.status, compatibility.message, false)
2695
+ };
2696
+ const access = await getWorkspace(server, {
2697
+ token,
2698
+ signal
2699
+ });
2700
+ const notAdmitted = {
2701
+ status: "uninvited",
2702
+ message: `This account is no longer admitted to this Pageship server, so the server already refuses every request from this installation. It did not remove the installation, though. To sign out fully, ask the server owner to admit the account again and then run ${retryLine}, or to remove the installation. Your credential is kept.`
2703
+ };
2704
+ if (access.status === "uninvited") return notAdmitted;
2705
+ if (access.status === "signed_out") revoked = false;
2706
+ else if (!access.installation) return credentialKept("unsupported", "This server does not report the CLI installation, so the CLI cannot revoke it. Update the server, or revoke the installation in the dashboard under CLI installations.", false);
2707
+ else {
2708
+ const result = await revokeInstallation(server, access.installation.id, {
2709
+ token,
2710
+ signal
2711
+ });
2712
+ if (result.status === "uninvited") return notAdmitted;
2713
+ if (result.status === "revoked") revoked = true;
2714
+ else if (result.status === "signed_out") revoked = false;
2715
+ else return credentialKept("request_failed", "The server did not revoke this installation.");
2716
+ }
2717
+ } catch (error) {
2718
+ if (signal.aborted) return credentialKept("interrupted", "Sign-out was interrupted, and the server may not have revoked this installation.");
2719
+ return credentialKept("request_failed", `Could not sign out through the Pageship server. ${errorText(error)}`);
2720
+ }
2721
+ try {
2722
+ await store.write({});
2723
+ } catch (error) {
2724
+ return {
2725
+ status: "request_failed",
2726
+ revoked,
2727
+ message: `The server ${revoked ? "revoked this installation" : "had already revoked or expired this credential"}, but the stored credential could not be deleted. ${errorText(error)} Run ${retryLine} to delete it.`,
2728
+ next: retry
2729
+ };
2730
+ }
2731
+ return {
2732
+ status: "logged_out",
2733
+ revoked,
2734
+ message: `${revoked ? "Signed out. The server revoked this installation, and its stored credential is deleted." : "Signed out. The server had already revoked or expired this credential, and the stored copy is deleted."}${discarded}`
2735
+ };
2736
+ } catch (error) {
2737
+ return credentialKept("request_failed", `Could not use the stored credential. ${errorText(error)}`);
2738
+ } finally {
2739
+ await store.close();
2740
+ }
2741
+ }
2742
+ //#endregion
2492
2743
  //#region src/skill-files.ts
2493
2744
  const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?=\r?\n|$)/u;
2494
2745
  /** The CLI version a packed SKILL.md was stamped with, or undefined for an unstamped one. */
@@ -3286,6 +3537,7 @@ const login = Command.make("login", {
3286
3537
  });
3287
3538
  })).pipe(Command.withDescription("Authorize this installation through a browser and choose your workspace address."));
3288
3539
  const whoami = Command.make("whoami", { server: serverFlag }, ({ server }) => authCommand("whoami", (signal) => identify(Option.getOrUndefined(server), signal))).pipe(Command.withDescription("Identify your personal workspace using the stored credential."));
3540
+ const logoutCommand = Command.make("logout", { server: serverFlag }, ({ server }) => authCommand("logout", (signal) => logout(Option.getOrUndefined(server), signal))).pipe(Command.withDescription("Sign this installation out: revoke its access on the server, then delete the stored credential."));
3289
3541
  const idempotencyKeyFlag = Flag.string("idempotency-key").pipe(Flag.filter((value) => /^[A-Za-z0-9_-]{8,128}$/u.test(value), () => "Use an 8-128 character idempotency key containing letters, digits, underscores or hyphens."), Flag.withDescription("Retry an uncertain operation with the key its result returned."), Flag.optional);
3290
3542
  const publishCommand = Command.make("publish", {
3291
3543
  file: Argument.string("file"),
@@ -3330,18 +3582,34 @@ const history = Command.make("history", {
3330
3582
  cursor: Option.getOrUndefined(cursor),
3331
3583
  signal
3332
3584
  }))).pipe(Command.withDescription("List a document's revisions, newest first, 50 per page."));
3585
+ const documentArgument = Argument.string("document").pipe(Argument.filter((value) => isDocumentId(value) || isUrl(value), () => "Use a six-character document ID from list, or the document's https:// link."), Argument.withDescription("Six-character document ID from list, or the document's link, including a revision link."));
3333
3586
  const download = Command.make("download", {
3334
- documentId: documentIdArgument,
3587
+ document: documentArgument,
3335
3588
  server: serverFlag,
3336
3589
  output: Flag.string("output").pipe(Flag.withDescription("New file to write the exact HTML bytes to. Existing files are kept.")),
3337
3590
  revision: revisionFlag("revision", "Download this historical revision instead of the current one.").pipe(Flag.optional)
3338
- }, ({ documentId, server, output, revision }) => authCommand("download", (signal) => downloadDocument({
3591
+ }, ({ document, server, output, revision }) => authCommand("download", (signal) => downloadDocument({
3339
3592
  server: Option.getOrUndefined(server),
3340
- documentId,
3593
+ document,
3341
3594
  output,
3342
3595
  revisionId: Option.getOrUndefined(revision),
3343
3596
  signal
3344
- }))).pipe(Command.withDescription("Save a document's current or historical HTML to a new file. Visibility is unchanged."));
3597
+ }))).pipe(Command.withDescription("Save a document's current or historical HTML to a new file, by ID or link. Visibility is unchanged."));
3598
+ const read = Command.make("read", {
3599
+ document: documentArgument,
3600
+ server: serverFlag
3601
+ }, ({ document, server }) => {
3602
+ let bytes;
3603
+ return authCommand("read", async (signal) => {
3604
+ const { bytes: read, ...result } = await readDocument({
3605
+ server: Option.getOrUndefined(server),
3606
+ document,
3607
+ signal
3608
+ });
3609
+ bytes = read;
3610
+ return result;
3611
+ }, (result) => bytes ? process.stdout.write(bytes) : console.error(result.message));
3612
+ }).pipe(Command.withDescription("Print a document's current HTML to stdout, by ID or link: your own, a public one or one shared with you. The owner can also read a revision link."));
3345
3613
  const visibility = Command.make("visibility", {
3346
3614
  documentId: documentIdArgument,
3347
3615
  visibility: Argument.choice("visibility", ["private", "public"]),
@@ -3402,7 +3670,8 @@ function sameCommand(name) {
3402
3670
  args: rest
3403
3671
  };
3404
3672
  }
3405
- function authCommand(name, operation) {
3673
+ /** Runs one command and reports its result. `print` replaces the human output of the message. */
3674
+ function authCommand(name, operation, print) {
3406
3675
  return Effect.promise(async () => {
3407
3676
  const controller = new AbortController();
3408
3677
  const interrupt = () => controller.abort();
@@ -3428,6 +3697,7 @@ function authCommand(name, operation) {
3428
3697
  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}`);
3429
3698
  return;
3430
3699
  }
3700
+ if (print) return print(result);
3431
3701
  console.log(result.message);
3432
3702
  if (typeof result.verificationUriComplete === "string" && typeof result.userCode === "string" && result.next) console.log(`Open ${result.verificationUriComplete}\nCheck code ${result.userCode}.\nThen run: ${commandLine(result.next.command, result.next.args)}`);
3433
3703
  } finally {
@@ -3436,14 +3706,16 @@ function authCommand(name, operation) {
3436
3706
  }
3437
3707
  });
3438
3708
  }
3439
- 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([
3709
+ 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. Sign it out with logout. Manage published documents with list, history, download, visibility, viewers and restore. Read a document you own or were shared, by ID or link, with read. Install the Pageship skills for your agents with skills install, and update the CLI with update."), Command.withSubcommands([
3440
3710
  status,
3441
3711
  login,
3442
3712
  whoami,
3713
+ logoutCommand,
3443
3714
  publishCommand,
3444
3715
  list,
3445
3716
  history,
3446
3717
  download,
3718
+ read,
3447
3719
  visibility,
3448
3720
  viewers,
3449
3721
  restore,
@@ -3451,6 +3723,17 @@ const command = Command.make("pageship").pipe(Command.withDescription("Pageship
3451
3723
  updateCommand
3452
3724
  ]), Command.withGlobalFlags([jsonFlag]));
3453
3725
  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;
3726
+ /** The subcommand named before `--`, skipping global flags and the values they take. */
3727
+ function subcommandName() {
3728
+ for (let index = 0; index < flagArgs.length; index++) {
3729
+ const arg = flagArgs[index];
3730
+ if (arg === "--json" && booleanValue(flagArgs[index + 1]) !== void 0) index++;
3731
+ else if (arg === "--log-level" || arg === "--completions") index++;
3732
+ else if (!arg.startsWith("-")) return arg;
3733
+ }
3734
+ }
3735
+ const builtInFlag = /^(?:--help|-h|--version|-v|--completions|--wizard)(?:=|$)/u;
3736
+ const readsDocument = !json && subcommandName() === "read" && !flagArgs.some((arg) => builtInFlag.test(arg));
3454
3737
  const program = Command.runWith(command, {
3455
3738
  version,
3456
3739
  renderErrors: !json
@@ -3475,7 +3758,10 @@ await Effect.runPromise(json ? program.pipe(Effect.provideService(CliOutput.Form
3475
3758
  ...console,
3476
3759
  log: () => {},
3477
3760
  error: () => {}
3478
- }), Effect.provide(CliConfig.layer({ builtIns: [GlobalFlag.Help, GlobalFlag.Version] }))) : program);
3761
+ }), Effect.provide(CliConfig.layer({ builtIns: [GlobalFlag.Help, GlobalFlag.Version] }))) : readsDocument ? program.pipe(Effect.provideService(Console.Console, {
3762
+ ...console,
3763
+ log: console.error
3764
+ })) : program);
3479
3765
  if (json) {
3480
3766
  if (!envelope) settle(null, {
3481
3767
  status: "internal_error",
@@ -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 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.
3
+ description: Publish an HTML file to Pageship, read Pageship documents and manage what is published. Use when the user pastes or names a Pageship document link (https://<workspace>.pageship.dev/d/<id>, not a link to the pageship.dev site, the dashboard or the API), or 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 or out of Pageship. To write, edit, rename or move a document, load pageship-docs instead.
4
4
  license: MIT
5
5
  metadata:
6
- version: "0.6.1"
6
+ version: "0.7.0"
7
7
  ---
8
8
 
9
9
  # Pageship
@@ -91,6 +91,16 @@ On `reauthentication_required`, ask the user to sign in to the Pageship dashboar
91
91
 
92
92
  When `login` returns `cancelled`, `interrupted` or `request_failed` with a `user`, authorization succeeded and the credential is kept, but no address is selected. Run the returned `next` when the user is ready.
93
93
 
94
+ ## Sign out
95
+
96
+ Only when the user asks to sign this installation out of Pageship, run:
97
+
98
+ ```sh
99
+ pageship --json logout
100
+ ```
101
+
102
+ On `logged_out`, the installation is signed out and its credential is deleted. `revoked: false` means there was nothing to revoke, which also counts as signed out. Report the `message` either way: it can ask the user to revoke a leftover session in the dashboard. On any other status, report the `message`, which says whether the installation is still signed in. Run the returned `next`, when there is one, only when the user wants to retry.
103
+
94
104
  ## Publish
95
105
 
96
106
  Set the shell placeholder `PAGESHIP_FILE` to one nonempty `.html` or `.htm` file. Pageship uses the server's configured per-file size limit. A new document is private. An existing source-path match does not overwrite a document.
@@ -141,6 +151,10 @@ When the user names the people to share it with, keep it private and add them as
141
151
 
142
152
  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).
143
153
 
154
+ ## Read a document link
155
+
156
+ A document link is `https://<workspace>.pageship.dev/d/<id>`, often followed by `-<slug>` and sometimes by `/revisions/<revisionId>`. When the user pastes one or asks about one, read it with `pageship read`, as in [read](references/read.md), instead of fetching it: a private document's link answers a fetch like a missing page, and `read` also reads public documents. Fetch a document link only when the CLI isn't installed or the user doesn't want to sign in; that works for public documents alone. Links to `pageship.dev` itself, `app.pageship.dev` or `api.pageship.dev` are not document links; fetch or open them as usual.
157
+
144
158
  ## Use another server
145
159
 
146
160
  The CLI uses the hosted Pageship service by default. Only when the user names another Pageship server, such as a local development server, add `--server <origin>` to every command you start yourself. Returned `next` and `resolutions` arguments already carry that server, so run them unchanged.
@@ -150,12 +164,13 @@ The CLI uses the hosted Pageship service by default. Only when the user names an
150
164
  Load the reference for the task:
151
165
 
152
166
  - [List and history](references/list-and-history.md): find a document's `id`, `visibility`, current `revisionId` and `url`, and inspect its revisions.
153
- - [Download](references/download.md): read a document's HTML, current or from an earlier revision.
167
+ - [Read](references/read.md): print a document's current HTML, by its ID or the link the user pasted, for a document the user owns, a public one, or one the user was added to as a viewer.
168
+ - [Download](references/download.md): save a document's HTML to a file, current or from an earlier revision, by its ID or the link the user pasted.
154
169
  - [Visibility](references/visibility.md): make a document public or private.
155
170
  - [Viewers](references/viewers.md): list, add or remove the GitHub users who can read a private document.
156
171
  - [Restore](references/restore.md): make an earlier revision current again.
157
172
 
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.
173
+ Deleting a document, recovering a deleted one, changing a document's display title and revoking another 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.
159
174
 
160
175
  ## Handle other outcomes
161
176
 
@@ -1,9 +1,9 @@
1
1
  # Download
2
2
 
3
- To read a document's HTML, run:
3
+ To save a document's HTML to a file, run:
4
4
 
5
5
  ```sh
6
6
  pageship --json download "$PAGESHIP_DOCUMENT" --output "$PAGESHIP_OUTPUT"
7
7
  ```
8
8
 
9
- Set `PAGESHIP_DOCUMENT` to the document's `id` from `list`, and `PAGESHIP_OUTPUT` to a path that does not exist. For a historical revision, add `--revision <revisionId>` with an ID from `history`. The bytes go only to that file. On `output_exists`, nothing was written or replaced; choose another path. A download never changes the document or its visibility.
9
+ Set `PAGESHIP_DOCUMENT` to the document's `id` from `list`, or to the document link the user pasted, such as `https://<workspace>.pageship.dev/d/<id>-<slug>`; you don't need `list` to find the ID. Set `PAGESHIP_OUTPUT` to a path that does not exist. For a historical revision, add `--revision <revisionId>` with an ID from `history`, or pass the revision link, which ends in `/revisions/<revisionId>`. On `invalid_document`, the value is not a Pageship document link; ask the user for the right link or ID. The bytes go only to that file. On `output_exists`, nothing was written or replaced; choose another path. A download never changes the document or its visibility. For another person's document, public or shared with the user, download gets the current revision only. To read HTML without writing a file, use [read](read.md).
@@ -0,0 +1,17 @@
1
+ # Read
2
+
3
+ To read a document's HTML, run:
4
+
5
+ ```sh
6
+ pageship --json read "$PAGESHIP_DOCUMENT"
7
+ ```
8
+
9
+ Set `PAGESHIP_DOCUMENT` to the document link the user pasted, such as `https://<workspace>.pageship.dev/d/<id>-<slug>`, or to the document's `id` from `list`. On `read`, the document's HTML is in `html`, and `revisionId` is `null` for the current revision. Without `--json`, the command prints the HTML alone on standard output, so you can redirect it to a file.
10
+
11
+ Reading works for any public document, and for a private document's owner and its viewers, the GitHub accounts the owner shared it with, signed in to this installation. The owner can also pass a revision link, which ends in `/revisions/<revisionId>`, to read that revision; a viewer reads the current revision only.
12
+
13
+ - `invalid_document`: the value isn't a document link. Links to the pageship.dev site, the dashboard and the API are refused before any request. Fetch or open those as usual.
14
+ - `unavailable`: the document doesn't exist, was deleted, or the signed-in account can't read it. Pageship doesn't say which. Tell the user, and ask them to check the link and which GitHub account they signed in with; `pageship --json whoami` shows it. Someone who should have access needs the owner to add their account as a viewer.
15
+ - `signed_out`: sign in as [Sign in](../SKILL.md#sign-in) describes, then read again.
16
+
17
+ Reading never changes the document, and doesn't count as a view.
@@ -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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1"
6
+ version: "0.7.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.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Pageship CLI for checking server API compatibility.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {