pageship 0.5.0 → 0.6.1
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 +1 -1
- package/dist/cli.mjs +270 -97
- package/dist/skills/pageship/SKILL.md +13 -2
- package/dist/skills/pageship/references/viewers.md +2 -2
- package/dist/skills/pageship-chart/SKILL.md +1 -1
- package/dist/skills/pageship-code-block/SKILL.md +1 -1
- package/dist/skills/pageship-decision-record/SKILL.md +1 -1
- package/dist/skills/pageship-diagram/SKILL.md +1 -1
- package/dist/skills/pageship-docs/SKILL.md +1 -1
- package/dist/skills/pageship-docs/references/check.md +2 -2
- package/dist/skills/pageship-docs/references/file.md +33 -0
- package/dist/skills/pageship-docs/references/html-validate-rules.mjs +94 -0
- package/dist/skills/pageship-docs/references/html-validate.json +2 -1
- package/dist/skills/pageship-explainer/SKILL.md +1 -1
- package/dist/skills/pageship-interaction/SKILL.md +1 -1
- package/dist/skills/pageship-key-figures/SKILL.md +1 -1
- package/dist/skills/pageship-media/SKILL.md +1 -1
- package/dist/skills/pageship-motion/SKILL.md +1 -1
- package/dist/skills/pageship-plan/SKILL.md +1 -1
- package/dist/skills/pageship-report/SKILL.md +1 -1
- package/dist/skills/pageship-review-report/SKILL.md +1 -1
- package/dist/skills/pageship-slide-deck/SKILL.md +1 -1
- package/dist/skills/pageship-spec/SKILL.md +1 -1
- package/dist/skills/pageship-table/SKILL.md +1 -1
- package/package.json +1 -1
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
|
|
691
|
-
url: "/api/v1/documents/{id}/
|
|
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
|
-
|
|
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);
|
|
@@ -990,7 +957,21 @@ function isGitHubAccountId(value) {
|
|
|
990
957
|
return typeof value === "string" && /^[1-9][0-9]{0,19}(?![\s\S])/u.test(value);
|
|
991
958
|
}
|
|
992
959
|
function validViewer(value) {
|
|
993
|
-
return record(value) && isGitHubAccountId(value.githubAccountId) && isGitHubLogin(value.login) && (value.name === null || typeof value.name === "string") && typeof value.addedAt === "number" && Number.isSafeInteger(value.addedAt) && value.addedAt >= 0;
|
|
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 && typeof value.viewCount === "number" && Number.isSafeInteger(value.viewCount) && value.viewCount >= 0 && (value.lastViewedAt === null || typeof value.lastViewedAt === "number" && Number.isSafeInteger(value.lastViewedAt) && value.lastViewedAt >= 0);
|
|
961
|
+
}
|
|
962
|
+
function normalizeViewer(value) {
|
|
963
|
+
if (!record(value)) return;
|
|
964
|
+
const normalized = {
|
|
965
|
+
...value,
|
|
966
|
+
...value.viewCount === void 0 ? { viewCount: 0 } : {},
|
|
967
|
+
...value.lastViewedAt === void 0 ? { lastViewedAt: null } : {}
|
|
968
|
+
};
|
|
969
|
+
return validViewer(normalized) ? normalized : void 0;
|
|
970
|
+
}
|
|
971
|
+
function normalizeViewers(value) {
|
|
972
|
+
if (!Array.isArray(value)) return;
|
|
973
|
+
const viewers = value.map(normalizeViewer);
|
|
974
|
+
return viewers.every((viewer) => viewer !== void 0) ? viewers : void 0;
|
|
994
975
|
}
|
|
995
976
|
function validUserMatch(value) {
|
|
996
977
|
return record(value) && isGitHubAccountId(value.githubAccountId) && isGitHubLogin(value.login);
|
|
@@ -1014,13 +995,14 @@ const viewerRefusals = {
|
|
|
1014
995
|
owner_account: "You already have access to your own document.",
|
|
1015
996
|
github_sign_in_required: "GitHub refused your sign-in. Sign in to Pageship again.",
|
|
1016
997
|
github_rate_limited: "GitHub is limiting requests. Try again in a minute.",
|
|
1017
|
-
github_unavailable: "GitHub is unavailable. Try again."
|
|
998
|
+
github_unavailable: "GitHub is unavailable. Try again.",
|
|
999
|
+
invalid_sharing: "A change can't both add and remove the same account."
|
|
1018
1000
|
};
|
|
1019
1001
|
function isViewerRefusal(value) {
|
|
1020
1002
|
return typeof value === "string" && Object.hasOwn(viewerRefusals, value);
|
|
1021
1003
|
}
|
|
1022
1004
|
function viewerFailed(response, error) {
|
|
1023
|
-
if (response && record(error) && isViewerRefusal(error.status)) throw new DocumentRequestError(error.status, viewerRefusals[error.status], response.status);
|
|
1005
|
+
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
1006
|
failed(response, error);
|
|
1025
1007
|
}
|
|
1026
1008
|
async function getDocumentViewers(baseUrl, id, options = {}) {
|
|
@@ -1029,35 +1011,45 @@ async function getDocumentViewers(baseUrl, id, options = {}) {
|
|
|
1029
1011
|
path: { id }
|
|
1030
1012
|
});
|
|
1031
1013
|
if (!response?.ok) viewerFailed(response, error);
|
|
1032
|
-
|
|
1014
|
+
const viewers = data?.status === "listed" ? normalizeViewers(data.viewers) : void 0;
|
|
1015
|
+
if (data && viewers) return {
|
|
1016
|
+
...data,
|
|
1017
|
+
viewers
|
|
1018
|
+
};
|
|
1033
1019
|
throw new Error("Invalid document viewer listing response.");
|
|
1034
1020
|
}
|
|
1035
|
-
/**
|
|
1036
|
-
|
|
1037
|
-
|
|
1021
|
+
/**
|
|
1022
|
+
* The most accounts one sharing change adds and removes. The contract's DocumentSharingChange
|
|
1023
|
+
* refuses more; tests/document-contract.test.ts keeps the two equal.
|
|
1024
|
+
*/
|
|
1025
|
+
const sharingLimits = {
|
|
1026
|
+
add: 20,
|
|
1027
|
+
remove: 50
|
|
1028
|
+
};
|
|
1029
|
+
/**
|
|
1030
|
+
* Changes visibility and viewers together or not at all. The server looks up each added account
|
|
1031
|
+
* that isn't a viewer yet, as addDocumentViewer does.
|
|
1032
|
+
*/
|
|
1033
|
+
async function updateDocumentSharing(baseUrl, id, change, options = {}) {
|
|
1034
|
+
const { data, response, error } = await viewersUpdateSharing({
|
|
1038
1035
|
...changeOptions(baseUrl, options),
|
|
1039
1036
|
path: { id },
|
|
1040
|
-
body:
|
|
1041
|
-
githubAccountId: user.githubAccountId,
|
|
1042
|
-
login: user.login
|
|
1043
|
-
}
|
|
1037
|
+
body: change
|
|
1044
1038
|
});
|
|
1045
1039
|
if (!response?.ok) viewerFailed(response, error);
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
...
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
if (data?.status === "removed") return { status: "removed" };
|
|
1060
|
-
throw new Error("Invalid document viewer removal response.");
|
|
1040
|
+
const updated = data?.status === "sharing_updated" ? data : void 0;
|
|
1041
|
+
const document = updated && normalizeDocument(updated.document);
|
|
1042
|
+
const viewers = updated && normalizeViewers(updated.viewers);
|
|
1043
|
+
const added = updated && normalizeViewers(updated.added);
|
|
1044
|
+
const removed = updated && normalizeViewers(updated.removed);
|
|
1045
|
+
if (updated && document && viewers && added && removed && (change.visibility === void 0 || document.visibility === change.visibility)) return {
|
|
1046
|
+
...updated,
|
|
1047
|
+
document,
|
|
1048
|
+
viewers,
|
|
1049
|
+
added,
|
|
1050
|
+
removed
|
|
1051
|
+
};
|
|
1052
|
+
throw new Error("Invalid document sharing response.");
|
|
1061
1053
|
}
|
|
1062
1054
|
async function getGitHubUser(baseUrl, login, options = {}) {
|
|
1063
1055
|
const { data, response, error } = await githubGetUser({
|
|
@@ -1448,6 +1440,8 @@ const outcomes = {
|
|
|
1448
1440
|
restored: "succeeded",
|
|
1449
1441
|
viewers_updated: "succeeded",
|
|
1450
1442
|
skills_installed: "succeeded",
|
|
1443
|
+
cli_updated: "succeeded",
|
|
1444
|
+
cli_current: "succeeded",
|
|
1451
1445
|
authorization_pending: "pending",
|
|
1452
1446
|
expired: "pending",
|
|
1453
1447
|
signed_out: "pending",
|
|
@@ -1483,6 +1477,8 @@ const outcomes = {
|
|
|
1483
1477
|
document_id_taken: "failed",
|
|
1484
1478
|
publication_action_unsupported: "failed",
|
|
1485
1479
|
skills_modified: "failed",
|
|
1480
|
+
install_unknown: "failed",
|
|
1481
|
+
update_failed: "failed",
|
|
1486
1482
|
ambiguous_viewer: "failed",
|
|
1487
1483
|
document_public: "failed",
|
|
1488
1484
|
github_user_not_found: "failed",
|
|
@@ -1504,7 +1500,8 @@ const commandNames = [
|
|
|
1504
1500
|
"visibility",
|
|
1505
1501
|
"viewers",
|
|
1506
1502
|
"restore",
|
|
1507
|
-
"skills"
|
|
1503
|
+
"skills",
|
|
1504
|
+
"update"
|
|
1508
1505
|
];
|
|
1509
1506
|
const isCommandName = (value) => commandNames.some((name) => name === value);
|
|
1510
1507
|
/** Exit codes follow the outcome, except an interruption, which exits 130. */
|
|
@@ -1554,7 +1551,7 @@ function commandLine(command, args) {
|
|
|
1554
1551
|
}
|
|
1555
1552
|
//#endregion
|
|
1556
1553
|
//#region package.json
|
|
1557
|
-
var version = "0.
|
|
1554
|
+
var version = "0.6.1";
|
|
1558
1555
|
//#endregion
|
|
1559
1556
|
//#region src/publish.ts
|
|
1560
1557
|
const execute = promisify(execFile);
|
|
@@ -2427,10 +2424,12 @@ function downloadDocument(options) {
|
|
|
2427
2424
|
}
|
|
2428
2425
|
function changeVisibility(options) {
|
|
2429
2426
|
return asOwner(options.server, options.signal, async ({ origin }, request) => {
|
|
2430
|
-
const
|
|
2431
|
-
const
|
|
2427
|
+
const { document, removed } = await updateDocumentSharing(origin, options.documentId, { visibility: options.visibility }, request);
|
|
2428
|
+
const removedViewers = removed.length;
|
|
2432
2429
|
return {
|
|
2433
|
-
|
|
2430
|
+
status: "visibility_updated",
|
|
2431
|
+
document,
|
|
2432
|
+
removedViewers,
|
|
2434
2433
|
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
2434
|
};
|
|
2436
2435
|
});
|
|
@@ -2510,6 +2509,10 @@ const installNext = {
|
|
|
2510
2509
|
command: "skills",
|
|
2511
2510
|
args: ["install"]
|
|
2512
2511
|
};
|
|
2512
|
+
const updateNext = {
|
|
2513
|
+
command: "update",
|
|
2514
|
+
args: []
|
|
2515
|
+
};
|
|
2513
2516
|
/** The output of `node --version` from PATH, or undefined when it does not run. */
|
|
2514
2517
|
const nodeOnPath = () => new Promise((resolve) => {
|
|
2515
2518
|
execFile("node", ["--version"], {
|
|
@@ -2594,6 +2597,7 @@ async function replaceFolder(source, target) {
|
|
|
2594
2597
|
}
|
|
2595
2598
|
}
|
|
2596
2599
|
const parseMajor = (version) => Number(/^v?(\d+)\./u.exec(version)?.[1] ?? NaN);
|
|
2600
|
+
/** Whether version `installed` is older than `cli`. An unreadable `installed` counts as older. */
|
|
2597
2601
|
const older = (installed, cli) => {
|
|
2598
2602
|
const parts = (version) => version.split(/[.+-]/u).slice(0, 3).map(Number);
|
|
2599
2603
|
const [a, b] = [parts(installed), parts(cli)];
|
|
@@ -2617,7 +2621,7 @@ async function inspectCli(environment, signal) {
|
|
|
2617
2621
|
return {
|
|
2618
2622
|
cli: "older",
|
|
2619
2623
|
latest,
|
|
2620
|
-
message: `A newer Pageship CLI is published (${latest}; this is ${environment.version}).
|
|
2624
|
+
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
2625
|
};
|
|
2622
2626
|
}
|
|
2623
2627
|
/** The recommended skills and Node that the CLI could not find. It installs none of them. */
|
|
@@ -2909,7 +2913,7 @@ function viewerList(documentId, viewers) {
|
|
|
2909
2913
|
}
|
|
2910
2914
|
/**
|
|
2911
2915
|
* Lists a document's viewers, or changes them. Every removal is matched and every added username
|
|
2912
|
-
* looked up
|
|
2916
|
+
* looked up first, then one sharing change applies them all or none.
|
|
2913
2917
|
*/
|
|
2914
2918
|
function manageViewers(options) {
|
|
2915
2919
|
const { documentId, add, remove } = options;
|
|
@@ -2927,13 +2931,11 @@ function manageViewers(options) {
|
|
|
2927
2931
|
...add.flatMap((value) => ["--add", value])
|
|
2928
2932
|
]
|
|
2929
2933
|
};
|
|
2934
|
+
if (add.length > sharingLimits.add || remove.length > sharingLimits.remove) return {
|
|
2935
|
+
status: "usage_error",
|
|
2936
|
+
message: `Add up to ${sharingLimits.add} and remove up to ${sharingLimits.remove} viewers at a time. Nothing was changed.`
|
|
2937
|
+
};
|
|
2930
2938
|
const progress = [];
|
|
2931
|
-
const added = [];
|
|
2932
|
-
const removed = [];
|
|
2933
|
-
const changed = () => added.length || removed.length ? {
|
|
2934
|
-
added,
|
|
2935
|
-
removed
|
|
2936
|
-
} : {};
|
|
2937
2939
|
try {
|
|
2938
2940
|
const { viewers } = await getDocumentViewers(origin, documentId, request);
|
|
2939
2941
|
if (!add.length && !remove.length) return {
|
|
@@ -2965,38 +2967,39 @@ function manageViewers(options) {
|
|
|
2965
2967
|
user,
|
|
2966
2968
|
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
2969
|
};
|
|
2970
|
+
if (removals.some(({ githubAccountId }) => githubAccountId === user.githubAccountId)) return {
|
|
2971
|
+
status: "usage_error",
|
|
2972
|
+
message: `${account(user)} is both added and removed. Nothing was changed.`
|
|
2973
|
+
};
|
|
2968
2974
|
progress.push(`Found ${account(user)}.`);
|
|
2969
2975
|
if (!accounts.some(({ githubAccountId }) => githubAccountId === user.githubAccountId)) accounts.push(user);
|
|
2970
2976
|
}
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
added.push(viewer);
|
|
2979
|
-
progress.push(`Added ${account(viewer)}.`);
|
|
2980
|
-
}
|
|
2981
|
-
const current = await getDocumentViewers(origin, documentId, request);
|
|
2977
|
+
const { added, removed, ...shared } = await updateDocumentSharing(origin, documentId, {
|
|
2978
|
+
add: accounts.map(({ githubAccountId, login }) => ({
|
|
2979
|
+
githubAccountId,
|
|
2980
|
+
login
|
|
2981
|
+
})),
|
|
2982
|
+
remove: removals.map(({ githubAccountId }) => githubAccountId)
|
|
2983
|
+
}, request);
|
|
2982
2984
|
return {
|
|
2983
2985
|
status: "viewers_updated",
|
|
2984
2986
|
documentId,
|
|
2985
2987
|
added,
|
|
2986
2988
|
removed,
|
|
2987
2989
|
notViewers,
|
|
2988
|
-
viewers:
|
|
2990
|
+
viewers: shared.viewers,
|
|
2989
2991
|
message: [
|
|
2990
2992
|
...progress,
|
|
2993
|
+
...removed.map((viewer) => `Removed ${account(viewer)}.`),
|
|
2994
|
+
...added.map((viewer) => `Added ${account(viewer)}.`),
|
|
2991
2995
|
...notViewers.map((value) => `${value} is not a viewer of ${documentId}.`),
|
|
2992
|
-
viewerList(documentId,
|
|
2996
|
+
viewerList(documentId, shared.viewers)
|
|
2993
2997
|
].join("\n")
|
|
2994
2998
|
};
|
|
2995
2999
|
} catch (error) {
|
|
2996
3000
|
const prefix = (result) => ({
|
|
2997
3001
|
...result,
|
|
2998
3002
|
documentId,
|
|
2999
|
-
...changed(),
|
|
3000
3003
|
message: [...progress, result.message].join("\n")
|
|
3001
3004
|
});
|
|
3002
3005
|
if (isRefusal(error)) {
|
|
@@ -3013,20 +3016,182 @@ function manageViewers(options) {
|
|
|
3013
3016
|
});
|
|
3014
3017
|
return prefix(refusal(error, login));
|
|
3015
3018
|
}
|
|
3016
|
-
if (!added.length && !removed.length) throw error;
|
|
3017
3019
|
return prefix(options.signal.aborted ? {
|
|
3018
3020
|
status: "interrupted",
|
|
3019
3021
|
message: `Operation interrupted. Run ${commandLine(next.command, next.args)} to finish.`,
|
|
3020
3022
|
next
|
|
3021
3023
|
} : {
|
|
3022
3024
|
status: "request_failed",
|
|
3023
|
-
message: `${error instanceof Error ? error.message : String(error)} Run ${commandLine(next.command, next.args)} to
|
|
3025
|
+
message: `${error instanceof Error ? error.message : String(error)} Run ${commandLine(next.command, next.args)} to retry.`,
|
|
3024
3026
|
next
|
|
3025
3027
|
});
|
|
3026
3028
|
}
|
|
3027
3029
|
});
|
|
3028
3030
|
}
|
|
3029
3031
|
//#endregion
|
|
3032
|
+
//#region src/update.ts
|
|
3033
|
+
/**
|
|
3034
|
+
* The command that updates a global install with each manager. Keep in sync with the setup
|
|
3035
|
+
* guide. The package names an exact version where one is known: pnpm resolves `latest` to the
|
|
3036
|
+
* newest version older than its minimum release age, so a fresh release would not install.
|
|
3037
|
+
*/
|
|
3038
|
+
const updateCommand$1 = (manager, spec) => ({
|
|
3039
|
+
npm: [
|
|
3040
|
+
"npm",
|
|
3041
|
+
"i",
|
|
3042
|
+
"-g",
|
|
3043
|
+
spec
|
|
3044
|
+
],
|
|
3045
|
+
pnpm: [
|
|
3046
|
+
"pnpm",
|
|
3047
|
+
"add",
|
|
3048
|
+
"-g",
|
|
3049
|
+
spec
|
|
3050
|
+
],
|
|
3051
|
+
bun: [
|
|
3052
|
+
"bun",
|
|
3053
|
+
"add",
|
|
3054
|
+
"-g",
|
|
3055
|
+
spec
|
|
3056
|
+
],
|
|
3057
|
+
yarn: [
|
|
3058
|
+
"yarn",
|
|
3059
|
+
"global",
|
|
3060
|
+
"add",
|
|
3061
|
+
spec
|
|
3062
|
+
],
|
|
3063
|
+
vp: [
|
|
3064
|
+
"vp",
|
|
3065
|
+
"install",
|
|
3066
|
+
"-g",
|
|
3067
|
+
spec
|
|
3068
|
+
]
|
|
3069
|
+
})[manager];
|
|
3070
|
+
const npmRoot = (signal) => new Promise((resolve) => {
|
|
3071
|
+
exec("npm root -g", {
|
|
3072
|
+
timeout: 1e4,
|
|
3073
|
+
windowsHide: true,
|
|
3074
|
+
signal
|
|
3075
|
+
}, (error, stdout) => resolve(error ? void 0 : stdout.trim() || void 0));
|
|
3076
|
+
});
|
|
3077
|
+
/**
|
|
3078
|
+
* Run a command with the terminal attached, or, for `--json`, with its output collected so
|
|
3079
|
+
* standard output stays one JSON value.
|
|
3080
|
+
*/
|
|
3081
|
+
const runCommand = (json) => (argv, signal) => new Promise((resolve, reject) => {
|
|
3082
|
+
const [command, ...args] = argv;
|
|
3083
|
+
if (!json) console.log(`Running ${argv.join(" ")}`);
|
|
3084
|
+
const stdio = json ? [
|
|
3085
|
+
"ignore",
|
|
3086
|
+
"pipe",
|
|
3087
|
+
"pipe"
|
|
3088
|
+
] : "inherit";
|
|
3089
|
+
const child = process.platform === "win32" ? spawn(argv.join(" "), {
|
|
3090
|
+
stdio,
|
|
3091
|
+
shell: true,
|
|
3092
|
+
windowsHide: true
|
|
3093
|
+
}) : spawn(command, args, { stdio });
|
|
3094
|
+
const stop = () => child.kill("SIGTERM");
|
|
3095
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
3096
|
+
const settle = () => signal.removeEventListener("abort", stop);
|
|
3097
|
+
let output = "";
|
|
3098
|
+
const collect = (chunk) => {
|
|
3099
|
+
output = (output + chunk.toString()).slice(-4e3);
|
|
3100
|
+
};
|
|
3101
|
+
child.stdout?.on("data", collect);
|
|
3102
|
+
child.stderr?.on("data", collect);
|
|
3103
|
+
child.once("error", (error) => {
|
|
3104
|
+
settle();
|
|
3105
|
+
reject(error);
|
|
3106
|
+
});
|
|
3107
|
+
child.once("close", (exitCode) => {
|
|
3108
|
+
settle();
|
|
3109
|
+
resolve({
|
|
3110
|
+
exitCode,
|
|
3111
|
+
output
|
|
3112
|
+
});
|
|
3113
|
+
});
|
|
3114
|
+
});
|
|
3115
|
+
/** A path with forward slashes and a trailing slash, compared without case on Windows. */
|
|
3116
|
+
function comparable(path, platform) {
|
|
3117
|
+
const slashed = `${path.replaceAll("\\", "/").replace(/\/+$/u, "")}/`;
|
|
3118
|
+
return platform === "win32" ? slashed.toLowerCase() : slashed;
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3121
|
+
* Which manager installed this CLI globally, from where its package is. Undefined for anything
|
|
3122
|
+
* else, such as a project dependency or an npx cache, which `update` must not replace.
|
|
3123
|
+
*/
|
|
3124
|
+
async function detectManager(environment, signal) {
|
|
3125
|
+
const { platform, env } = environment;
|
|
3126
|
+
const root = comparable(environment.packageRoot, platform);
|
|
3127
|
+
const installed = (folders, layout) => folders.some((folder) => {
|
|
3128
|
+
if (!folder) return false;
|
|
3129
|
+
const base = comparable(folder, platform);
|
|
3130
|
+
const index = root.lastIndexOf(base);
|
|
3131
|
+
return index !== -1 && layout.test(root.slice(index + base.length));
|
|
3132
|
+
});
|
|
3133
|
+
if (installed(["/vite-plus/packages/"], /^pageship\/[^/]+\/(?:lib\/)?node_modules\/pageship\/$/u)) return "vp";
|
|
3134
|
+
if (installed(["/pnpm/global/", env.PNPM_HOME && join(env.PNPM_HOME, "global")], /^[^/]+\/(?:[^/]+\/)?node_modules\/\.pnpm\/pageship@[^/]+\/node_modules\/pageship\/$/u)) return "pnpm";
|
|
3135
|
+
if (installed(["/.bun/", env.BUN_INSTALL], /^install\/global\/node_modules\/pageship\/$/u) || installed([env.BUN_INSTALL_GLOBAL_DIR], /^node_modules\/pageship\/$/u)) return "bun";
|
|
3136
|
+
if (installed(["/yarn/global/", "/yarn/data/global/"], /^node_modules\/pageship\/$/u)) return "yarn";
|
|
3137
|
+
const npm = await environment.npmRoot(signal);
|
|
3138
|
+
const parent = root.slice(0, root.lastIndexOf("/", root.length - 2) + 1);
|
|
3139
|
+
if (npm !== void 0 && parent === comparable(npm, platform)) return "npm";
|
|
3140
|
+
}
|
|
3141
|
+
/**
|
|
3142
|
+
* Update this CLI to the version npm publishes as latest, with the package manager that
|
|
3143
|
+
* installed it. It changes nothing when this CLI is already the latest.
|
|
3144
|
+
*/
|
|
3145
|
+
async function update(environment, signal) {
|
|
3146
|
+
const { version } = environment;
|
|
3147
|
+
const latest = await environment.publishedVersion(signal);
|
|
3148
|
+
if (latest !== void 0 && !older(version, latest)) return {
|
|
3149
|
+
status: "cli_current",
|
|
3150
|
+
message: `Pageship ${version} is the latest version.`,
|
|
3151
|
+
version,
|
|
3152
|
+
latest
|
|
3153
|
+
};
|
|
3154
|
+
const manager = await detectManager(environment, signal);
|
|
3155
|
+
signal.throwIfAborted();
|
|
3156
|
+
if (manager === void 0) return {
|
|
3157
|
+
status: "install_unknown",
|
|
3158
|
+
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.`,
|
|
3159
|
+
version,
|
|
3160
|
+
...latest === void 0 ? {} : { latest },
|
|
3161
|
+
path: environment.packageRoot
|
|
3162
|
+
};
|
|
3163
|
+
const argv = updateCommand$1(manager, `pageship@${latest ?? "latest"}`);
|
|
3164
|
+
const run = argv.join(" ");
|
|
3165
|
+
const ran = await environment.run(argv, signal).catch((error) => {
|
|
3166
|
+
if (signal.aborted) throw error;
|
|
3167
|
+
return {
|
|
3168
|
+
exitCode: null,
|
|
3169
|
+
output: error instanceof Error ? error.message : String(error)
|
|
3170
|
+
};
|
|
3171
|
+
});
|
|
3172
|
+
signal.throwIfAborted();
|
|
3173
|
+
if (ran.exitCode !== 0) return {
|
|
3174
|
+
status: "update_failed",
|
|
3175
|
+
message: [ran.exitCode === null ? `Could not run ${run}.` : `${run} failed with exit code ${ran.exitCode}.`, ran.output.trim()].filter(Boolean).join("\n"),
|
|
3176
|
+
version,
|
|
3177
|
+
...latest === void 0 ? {} : { latest },
|
|
3178
|
+
manager,
|
|
3179
|
+
run,
|
|
3180
|
+
exitCode: ran.exitCode,
|
|
3181
|
+
output: ran.output
|
|
3182
|
+
};
|
|
3183
|
+
const skills = await stat(join(environment.home, ".agents", "skills", "pageship")).then(() => true, () => false);
|
|
3184
|
+
return {
|
|
3185
|
+
status: "cli_updated",
|
|
3186
|
+
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"),
|
|
3187
|
+
version,
|
|
3188
|
+
...latest === void 0 ? {} : { latest },
|
|
3189
|
+
manager,
|
|
3190
|
+
run,
|
|
3191
|
+
...skills ? { next: installNext } : {}
|
|
3192
|
+
};
|
|
3193
|
+
}
|
|
3194
|
+
//#endregion
|
|
3030
3195
|
//#region src/cli.ts
|
|
3031
3196
|
const args = process.argv.slice(2);
|
|
3032
3197
|
const separator = args.indexOf("--");
|
|
@@ -3093,6 +3258,13 @@ const skills = Command.make("skills").pipe(Command.withDescription("Manage the P
|
|
|
3093
3258
|
force,
|
|
3094
3259
|
signal
|
|
3095
3260
|
}))).pipe(Command.withDescription("Install the Pageship skills in ~/.agents/skills, and link them in ~/.claude/skills when ~/.claude exists."))]));
|
|
3261
|
+
const updateCommand = Command.make("update", {}, () => authCommand("update", (signal) => update({
|
|
3262
|
+
...skillsEnvironment,
|
|
3263
|
+
packageRoot: dirname(dirname(fileURLToPath(import.meta.url))),
|
|
3264
|
+
env: process.env,
|
|
3265
|
+
npmRoot,
|
|
3266
|
+
run: runCommand(json)
|
|
3267
|
+
}, 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
3268
|
const login = Command.make("login", {
|
|
3097
3269
|
complete: Flag.boolean("complete").pipe(Flag.withDefault(false), Flag.withDescription("Check a pending authorization once, without waiting.")),
|
|
3098
3270
|
restart: Flag.boolean("restart").pipe(Flag.withDefault(false), Flag.withDescription("Start a new authorization code.")),
|
|
@@ -3253,7 +3425,7 @@ function authCommand(name, operation) {
|
|
|
3253
3425
|
settle(name, result);
|
|
3254
3426
|
if (json) return;
|
|
3255
3427
|
if (thrown) {
|
|
3256
|
-
console.error(`${name === "status" ? "Could not check status" : name === "skills" ? "Could not install the Pageship skills" : "Pageship request failed"}: ${result.message}`);
|
|
3428
|
+
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
3429
|
return;
|
|
3258
3430
|
}
|
|
3259
3431
|
console.log(result.message);
|
|
@@ -3264,7 +3436,7 @@ function authCommand(name, operation) {
|
|
|
3264
3436
|
}
|
|
3265
3437
|
});
|
|
3266
3438
|
}
|
|
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([
|
|
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([
|
|
3268
3440
|
status,
|
|
3269
3441
|
login,
|
|
3270
3442
|
whoami,
|
|
@@ -3275,7 +3447,8 @@ const command = Command.make("pageship").pipe(Command.withDescription("Pageship
|
|
|
3275
3447
|
visibility,
|
|
3276
3448
|
viewers,
|
|
3277
3449
|
restore,
|
|
3278
|
-
skills
|
|
3450
|
+
skills,
|
|
3451
|
+
updateCommand
|
|
3279
3452
|
]), Command.withGlobalFlags([jsonFlag]));
|
|
3280
3453
|
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
3454
|
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.
|
|
6
|
+
version: "0.6.1"
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
6
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Pageship documents
|
|
@@ -12,7 +12,7 @@ Run the linter, html-validate with the config and rules in this folder:
|
|
|
12
12
|
npx --yes html-validate@11.17.0 --config <pageship-docs folder>/references/html-validate.json <file>
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
It reports HTML errors, accessibility problems such as a missing `lang` or `alt` or a skipped heading level, and breaks of
|
|
15
|
+
It reports HTML errors, accessibility problems such as a missing `lang` or `alt` or a skipped heading level, and breaks of four rules: [one standalone file](file.md#one-standalone-file), [trusted libraries only](file.md#trusted-libraries-only) and [link previews](file.md#link-previews) in the file rules, and **Height** in [the design rules](design.md#base-rules-every-file-meets). Fix each problem it reports and run it again. Done when it reports nothing. When `npx` can't run, such as without Node or network, check the file against those four rules by reading it, and say in the hand-over that the linter didn't run.
|
|
16
16
|
|
|
17
17
|
Then:
|
|
18
18
|
|
|
@@ -51,7 +51,7 @@ Then use every control by keyboard and confirm it works and shows focus. Done wh
|
|
|
51
51
|
|
|
52
52
|
## Claims
|
|
53
53
|
|
|
54
|
-
Read the file sentence by sentence beside the source, captions, labels and
|
|
54
|
+
Read the file sentence by sentence beside the source, captions, labels, tooltips and the link preview's description included. Keep each claim the source states. A claim one step past it goes, or becomes an open question: a cause, when something starts or ends, a count, how far a rule reaches, or a new name for something the source names. A caption claims only what its figure draws. Done when you can point to the source's words behind every sentence.
|
|
55
55
|
|
|
56
56
|
## The structure you added
|
|
57
57
|
|
|
@@ -32,6 +32,39 @@ Mermaid, only when a pattern skill calls for it:
|
|
|
32
32
|
|
|
33
33
|
Hosted fonts from `https://fonts.googleapis.com/css2?...` through a `<link rel="stylesheet">`, which can't carry `integrity`.
|
|
34
34
|
|
|
35
|
+
## Link previews
|
|
36
|
+
|
|
37
|
+
Chat apps and social sites show a card for a shared link, built from tags in the file's `<head>`. Every file carries them, after `<meta charset>`, the viewport and `<title>`:
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<title>Q3 support report</title>
|
|
41
|
+
<meta
|
|
42
|
+
name="description"
|
|
43
|
+
content="Ticket volume rose 18% in Q3 while first-reply time held at 2 hours."
|
|
44
|
+
/>
|
|
45
|
+
<meta property="og:type" content="article" />
|
|
46
|
+
<meta property="og:title" content="Q3 support report" />
|
|
47
|
+
<meta
|
|
48
|
+
property="og:description"
|
|
49
|
+
content="Ticket volume rose 18% in Q3 while first-reply time held at 2 hours."
|
|
50
|
+
/>
|
|
51
|
+
<meta property="og:image" content="https://example.com/q3/tickets-by-week.png" />
|
|
52
|
+
<meta property="og:image:alt" content="Tickets by week, July to September" />
|
|
53
|
+
<meta property="og:image:width" content="1200" />
|
|
54
|
+
<meta property="og:image:height" content="630" />
|
|
55
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
56
|
+
<meta name="twitter:image:alt" content="Tickets by week, July to September" />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- **Title.** `<title>` and `og:title` hold the same text: the document's title, as its `h1` gives it.
|
|
60
|
+
- **Description.** `description` and `og:description` hold the same one or two sentences, up to about 160 characters, that state the document's main point as its first lines do. It follows the [content rules](content.md) like any other text in the file.
|
|
61
|
+
- **Type.** `og:type` is `article`.
|
|
62
|
+
- **Image.** The card's image is the first of these that is a PNG, JPEG or GIF file at an `https://` URL: the image or chart that leads the page, then the first image in the page with alt text. Its `alt` becomes `og:image:alt` and `twitter:image:alt`. Its size in pixels, when you measured the image file itself, becomes `og:image:width` and `og:image:height`; a size the page displays it at is not that size, so without a measured size leave those two out. Sites can't show an inline SVG or a `data:` URL, so a page whose images and charts are only those leaves out every image tag.
|
|
63
|
+
- **Card.** `twitter:card` is `summary_large_image` with an image and `summary` without. X reads the title, description and image from the `og:` tags, so the only other `twitter:` tag is `twitter:image:alt`.
|
|
64
|
+
- **No address.** Leave out `og:url` and `<link rel="canonical">`: the file doesn't know where it will be published, and sites use the address they fetched.
|
|
65
|
+
|
|
66
|
+
An edit that changes the title, the main point or the image updates these tags to match.
|
|
67
|
+
|
|
35
68
|
## The Pageship sandbox
|
|
36
69
|
|
|
37
70
|
A published file runs with scripts allowed and everything else blocked, so it works without these:
|
|
@@ -316,11 +316,105 @@ class NoViewportHeight extends Rule {
|
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
const entities = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
|
|
320
|
+
|
|
321
|
+
/** Text with its character references decoded and its runs of white space as single spaces. */
|
|
322
|
+
const plainText = (text) =>
|
|
323
|
+
text
|
|
324
|
+
.replace(/&(?:#(\d+)|#x([\da-f]+)|(\w+));/giu, (match, decimal, hex, name) =>
|
|
325
|
+
decimal || hex
|
|
326
|
+
? String.fromCodePoint(Number.parseInt(decimal ?? hex, decimal ? 10 : 16))
|
|
327
|
+
: (entities[name.toLowerCase()] ?? match),
|
|
328
|
+
)
|
|
329
|
+
.replace(/\s+/gu, " ")
|
|
330
|
+
.trim();
|
|
331
|
+
|
|
332
|
+
/** The tags a link preview reads. Others, such as one `theme-color` per colour scheme, may repeat. */
|
|
333
|
+
const previewTags = /^(?:description|og:.+|twitter:.+)$/u;
|
|
334
|
+
|
|
335
|
+
/** An `https://` URL whose path doesn't end in `.svg`, which link previews don't show. */
|
|
336
|
+
const previewImage = /^https:\/\/(?![^?#]*\.svg(?:[?#]|$))/iu;
|
|
337
|
+
|
|
338
|
+
class LinkPreview extends Rule {
|
|
339
|
+
documentation() {
|
|
340
|
+
return {
|
|
341
|
+
description:
|
|
342
|
+
"Give the file the link preview tags in the file rules (references/file.md): a description, og:type, og:title and og:description matching the title and description, twitter:card, and an og:image only at an https:// URL to an image that isn't SVG, with og:image:alt and twitter:image:alt.",
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
setup() {
|
|
347
|
+
this.on("dom:ready", ({ document }) => {
|
|
348
|
+
const head = document.querySelector("head");
|
|
349
|
+
if (!head) return;
|
|
350
|
+
/** Each `<meta>` content by its `name` or `property`, with the element that has it. */
|
|
351
|
+
const metas = new Map();
|
|
352
|
+
for (const element of document.querySelectorAll("meta")) {
|
|
353
|
+
const key = attributeValue(element, "property") ?? attributeValue(element, "name");
|
|
354
|
+
if (!previewTags.test(key ?? "")) continue;
|
|
355
|
+
if (metas.has(key)) {
|
|
356
|
+
this.report(element, `The file has a second ${key} tag: keep one`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
metas.set(key, { element, content: plainText(attributeValue(element, "content") ?? "") });
|
|
360
|
+
}
|
|
361
|
+
const content = (key) => metas.get(key)?.content ?? "";
|
|
362
|
+
const at = (key) => metas.get(key)?.element ?? head;
|
|
363
|
+
|
|
364
|
+
for (const key of ["description", "og:type", "og:title", "og:description", "twitter:card"]) {
|
|
365
|
+
if (!content(key)) {
|
|
366
|
+
this.report(
|
|
367
|
+
at(key),
|
|
368
|
+
`The file needs the ${key} tag with content, as the file rules give`,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const title = document.querySelector("title");
|
|
373
|
+
if (title && content("og:title") && content("og:title") !== plainText(title.textContent)) {
|
|
374
|
+
this.report(at("og:title"), "og:title must hold the same text as <title>");
|
|
375
|
+
}
|
|
376
|
+
if (content("og:description") && content("og:description") !== content("description")) {
|
|
377
|
+
this.report(at("og:description"), "og:description must hold the same text as description");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const image = content("og:image");
|
|
381
|
+
if (metas.has("og:image") && !image) {
|
|
382
|
+
this.report(
|
|
383
|
+
at("og:image"),
|
|
384
|
+
"og:image is empty: give the image's URL, or leave out the image tags",
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
if (image && !previewImage.test(image)) {
|
|
388
|
+
this.report(
|
|
389
|
+
at("og:image"),
|
|
390
|
+
`og:image "${image}" is not an https:// URL to an image a link preview shows, such as a PNG or JPEG file: use one, or leave out the image tags`,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
for (const key of ["og:image:alt", "twitter:image:alt"]) {
|
|
394
|
+
if (image && !content(key)) {
|
|
395
|
+
this.report(at("og:image"), `og:image needs ${key}, the image's alt text`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (!image && (metas.has("og:image:alt") || metas.has("twitter:image:alt"))) {
|
|
399
|
+
this.report(head, "The file has image alt tags but no og:image: leave them out too");
|
|
400
|
+
}
|
|
401
|
+
const card = image ? "summary_large_image" : "summary";
|
|
402
|
+
if (content("twitter:card") && content("twitter:card") !== card) {
|
|
403
|
+
this.report(
|
|
404
|
+
at("twitter:card"),
|
|
405
|
+
`twitter:card must be "${card}" for a file ${image ? "with" : "without"} og:image`,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
319
412
|
export default {
|
|
320
413
|
name: "pageship",
|
|
321
414
|
rules: {
|
|
322
415
|
"pageship/trusted-libraries": TrustedLibraries,
|
|
323
416
|
"pageship/no-relative-paths": NoRelativePaths,
|
|
324
417
|
"pageship/no-viewport-height": NoViewportHeight,
|
|
418
|
+
"pageship/link-preview": LinkPreview,
|
|
325
419
|
},
|
|
326
420
|
};
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"heading-level": ["error", { "allowMultipleH1": false }],
|
|
9
9
|
"pageship/trusted-libraries": "error",
|
|
10
10
|
"pageship/no-relative-paths": "error",
|
|
11
|
-
"pageship/no-viewport-height": "error"
|
|
11
|
+
"pageship/no-viewport-height": "error",
|
|
12
|
+
"pageship/link-preview": "error"
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
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
|
+
version: "0.6.1"
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Table
|