@forumulate/cli 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Forumulate
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @forumulate/cli
2
+
3
+ Command-line interface for [Forumulate](https://github.com/alphajew420/forumulate) — manage orgs, spaces, docs, and search from your terminal, built on `@forumulate/sdk`.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ npm install -g @forumulate/cli
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```sh
14
+ export FORUMULATE_URL=https://your-forumulate.example.com # default http://localhost:4000
15
+ export FORUMULATE_TOKEN=cfo_... # a Personal Access Token
16
+
17
+ forumulate whoami
18
+ forumulate orgs
19
+ forumulate spaces <orgId>
20
+ forumulate search <orgId> how to deploy
21
+ forumulate publish <orgId> <spaceId> <pageId>
22
+ forumulate snapshot <orgId> <spaceId> "Before the big migration"
23
+ ```
24
+
25
+ Add `--json` to any command for raw JSON output, `--version` for the version, and `forumulate help` for the full list.
26
+
27
+ ## `forumulate doctor`
28
+
29
+ Check a live site from the outside, the way a reader and a crawler see it:
30
+
31
+ ```sh
32
+ forumulate doctor https://your-domain.example.com/docs
33
+ ```
34
+
35
+ It needs no token, because none of this is private. Each check is something
36
+ customers have found out about after their readers did:
37
+
38
+ | Check | What a failure means |
39
+ |---|---|
40
+ | `health` | The health endpoint answered something other than JSON. A plain-text 500 during a deploy reads as a permanent bug, so a careful client stops retrying a few-minute outage. |
41
+ | `public-read` | A logged-out reader is redirected to sign-in. Public docs behind a login are invisible to readers and to crawlers — and invisible from inside a signed-in session, which is why this is worth a command. |
42
+ | `canonical` | The `rel=canonical` names another host. Search engines credit the canonical host, so a platform URL here hands your authority to the platform. |
43
+ | `html-untouched` | Something in the path is rewriting your markup. Cloudflare's Email Obfuscation is the usual one: it injects a decoder script with no CSP nonce, so the script is refused and every address on the page renders as `[email protected]` with a dead link. |
44
+ | `cache-headers` | The document is served `no-store` or with no `Cache-Control`, so every reader pays full origin latency and a CDN in front of it does nothing. |
45
+ | `robots` / `sitemap` | `robots.txt` disallows the path, or names no sitemap. A `Disallow` here means the page is never indexed, however good it is. Only the `User-agent: *` group counts — a rule aimed at one named crawler is a deliberate choice. |
46
+ | `crawler-access` | A crawler gets a different status from a reader. A WAF rule or bot challenge that answers crawlers differently removes the site from search with nothing in any log to say so. |
47
+ | `feeds` | No feed beside the page. Readers and aggregators look next to the content, not at an API URL. |
48
+
49
+ Exit code is non-zero when anything failed, so it works in a deploy pipeline.
50
+ `--json` gives the full report. The checks speak plain HTTP and assume nothing
51
+ about the server, so pointing it at a staging mount — or at a competitor —
52
+ produces the same report: a doctor that only passes on its author's deployment
53
+ is a checklist with extra steps.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const sdk_1 = require("@forumulate/sdk");
5
+ const run_1 = require("./run");
6
+ async function main() {
7
+ const baseUrl = process.env.FORUMULATE_URL ?? 'http://localhost:4000';
8
+ const token = process.env.FORUMULATE_TOKEN;
9
+ const client = new sdk_1.ForumulateClient({ baseUrl, token });
10
+ const code = await (0, run_1.run)(process.argv.slice(2), {
11
+ client,
12
+ out: (l) => process.stdout.write(l + '\n'),
13
+ err: (l) => process.stderr.write(l + '\n'),
14
+ });
15
+ process.exit(code);
16
+ }
17
+ void main();
18
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;AACA,yCAAmD;AACnD,+BAA4B;AAE5B,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,uBAAuB,CAAC;IACtE,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC3C,MAAM,MAAM,GAAG,IAAI,sBAAgB,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,MAAM,IAAA,SAAG,EAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QAC5C,MAAM;QACN,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC;QAC1C,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC;KAC3C,CAAC,CAAC;IACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,KAAK,IAAI,EAAE,CAAC"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The Content Sync Protocol, as a suite anything can be run against.
3
+ *
4
+ * What this checks is not "is it Forumulate" — it is a set of properties any
5
+ * documentation platform can have and most do not: a findable description, one
6
+ * envelope, a cursor that walks a collection without dropping or repeating a
7
+ * row, an incremental window with a server-defined watermark, explicit
8
+ * tombstones, a per-item hash, conditional requests, and rate limits that name
9
+ * their window.
10
+ *
11
+ * It uses plain fetch and no SDK on purpose. A conformance suite that only runs
12
+ * against the platform that wrote it proves nothing and convinces nobody.
13
+ */
14
+ export declare const PROTOCOL_VERSION = "2026-09-01";
15
+ export interface Check {
16
+ id: string;
17
+ title: string;
18
+ state: 'pass' | 'fail' | 'skip';
19
+ detail: string;
20
+ /** Why a client is worse off when this fails. */
21
+ matters: string;
22
+ }
23
+ export interface ConformanceInput {
24
+ /** The collection to walk, absolute. e.g. https://api.example.com/orgs/1/spaces/2/items */
25
+ collection: string;
26
+ token?: string;
27
+ /** The key the collection is under, if it is not derivable from the path. */
28
+ key?: string;
29
+ fetchImpl?: typeof fetch;
30
+ /** The version header a client states. Absent means the platform has none. */
31
+ versionHeader?: {
32
+ name: string;
33
+ value: string;
34
+ };
35
+ }
36
+ export interface ConformanceReport {
37
+ protocol: string;
38
+ collection: string;
39
+ checks: Check[];
40
+ passed: number;
41
+ failed: number;
42
+ }
43
+ export declare function runConformance(input: ConformanceInput): Promise<ConformanceReport>;
44
+ //# sourceMappingURL=conformance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.d.ts","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAE7C,MAAM,WAAW,KAAK;IACpB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,2FAA2F;IAC3F,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,8EAA8E;IAC9E,aAAa,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CACjD;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAsB,cAAc,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA2KxF"}
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PROTOCOL_VERSION = void 0;
4
+ exports.runConformance = runConformance;
5
+ /**
6
+ * The Content Sync Protocol, as a suite anything can be run against.
7
+ *
8
+ * What this checks is not "is it Forumulate" — it is a set of properties any
9
+ * documentation platform can have and most do not: a findable description, one
10
+ * envelope, a cursor that walks a collection without dropping or repeating a
11
+ * row, an incremental window with a server-defined watermark, explicit
12
+ * tombstones, a per-item hash, conditional requests, and rate limits that name
13
+ * their window.
14
+ *
15
+ * It uses plain fetch and no SDK on purpose. A conformance suite that only runs
16
+ * against the platform that wrote it proves nothing and convinces nobody.
17
+ */
18
+ exports.PROTOCOL_VERSION = '2026-09-01';
19
+ const nounOf = (url) => {
20
+ const path = new URL(url).pathname.split('/').filter(Boolean);
21
+ return path[path.length - 1] ?? 'items';
22
+ };
23
+ async function runConformance(input) {
24
+ const doFetch = input.fetchImpl ?? fetch;
25
+ const key = input.key ?? nounOf(input.collection);
26
+ const checks = [];
27
+ const headers = { accept: 'application/json' };
28
+ if (input.token)
29
+ headers.authorization = `Bearer ${input.token}`;
30
+ if (input.versionHeader)
31
+ headers[input.versionHeader.name] = input.versionHeader.value;
32
+ const get = async (url, extra = {}) => {
33
+ const res = await doFetch(url, { headers: { ...headers, ...extra } });
34
+ const text = await res.text();
35
+ let body = {};
36
+ try {
37
+ body = text ? JSON.parse(text) : {};
38
+ }
39
+ catch {
40
+ body = {};
41
+ }
42
+ return { res, body };
43
+ };
44
+ const add = (id, title, state, detail, matters) => checks.push({ id, title, state, detail, matters });
45
+ // 1 — the description is findable from the collection itself.
46
+ const first = await get(withQuery(input.collection, { limit: '2' }));
47
+ const serviceDesc = first.res.headers.get('link') ?? '';
48
+ add('CSP-1', 'The description is findable from any response', /rel="?service-desc"?/.test(serviceDesc) ? 'pass' : 'fail', serviceDesc ? `link: ${serviceDesc.slice(0, 120)}` : 'no Link header', 'Without it an integrator probes five paths, finds nothing, and writes a defensive parser instead of generating a client.');
49
+ // 2 — one envelope, keyed after the path.
50
+ const rows = Array.isArray(body(first.body, key)) ? body(first.body, key) : null;
51
+ add('CSP-2', `The collection is keyed after its path (\`${key}\`)`, rows ? 'pass' : 'fail', rows ? `${rows.length} row(s)` : `keys were: ${Object.keys(first.body).join(', ') || 'none'}`, 'A key that does not match its path makes every other endpoint a guess.');
52
+ const page = first.body.page;
53
+ add('CSP-3', 'The page carries limit, has_more, next_cursor and total', page && 'has_more' in page && 'next_cursor' in page ? 'pass' : 'fail', page ? JSON.stringify(page) : 'no `page` object', 'A caller cannot tell a short page from the last page, so a sync silently stops early.');
54
+ // 3 — the walk visits every row exactly once.
55
+ if (rows && page) {
56
+ const seen = [];
57
+ let cursor = page.next_cursor ?? null;
58
+ let guard = 0;
59
+ for (const row of rows)
60
+ seen.push(String(row.id));
61
+ while (cursor && guard++ < 50) {
62
+ const next = await get(withQuery(input.collection, { limit: '2', cursor }));
63
+ const nextRows = body(next.body, key) ?? [];
64
+ for (const row of nextRows)
65
+ seen.push(String(row.id));
66
+ cursor = next.body.page?.next_cursor ?? null;
67
+ }
68
+ const unique = new Set(seen);
69
+ add('CSP-4', 'A cursor walk visits every row exactly once', unique.size === seen.length ? 'pass' : 'fail', `${seen.length} row(s), ${unique.size} distinct`, 'A cursor that repeats or skips rows corrupts a corpus without ever erroring.');
70
+ const total = page.total;
71
+ add('CSP-5', 'The walk agrees with the reported total', typeof total === 'number' ? (unique.size === total ? 'pass' : 'fail') : 'skip', typeof total === 'number' ? `walked ${unique.size}, total says ${total}` : 'no total reported', 'A total nobody can reconcile against is a number that hides a truncated sync.');
72
+ }
73
+ // 4 — the incremental window, and the watermark that drives it.
74
+ const sync = first.body.sync;
75
+ add('CSP-6', 'A server-defined checkpoint comes back with the page', sync?.checkpoint ? 'pass' : 'fail', sync?.checkpoint ?? 'no `sync.checkpoint`', 'Without one, every client invents its own watermark out of its own clock and loses rows to skew.');
76
+ if (sync?.checkpoint) {
77
+ const since = await get(withQuery(input.collection, { updated_since: sync.checkpoint }));
78
+ const sinceRows = body(since.body, key) ?? [];
79
+ add('CSP-7', 'The checkpoint narrows the answer', sinceRows.length <= (rows?.length ?? 0) + 1 ? 'pass' : 'fail', `${sinceRows.length} row(s) changed since the checkpoint`, 'An incremental sync that returns everything is a full sync wearing a parameter.');
80
+ const bad = await get(withQuery(input.collection, { updated_since: 'yesterday-ish' }));
81
+ add('CSP-8', 'An unparseable window is refused, not ignored', bad.res.status >= 400 && bad.res.status < 500 ? 'pass' : 'fail', `answered ${bad.res.status}`, 'Silently ignoring it hands back a full list that looks like a successful incremental sync.');
82
+ }
83
+ // 5 — tombstones.
84
+ const deleted = await get(withQuery(input.collection, { include_deleted: 'true', limit: '50' }));
85
+ const deletedRows = body(deleted.body, key) ?? [];
86
+ add('CSP-9', 'Removals are explicit, not inferred from absence', deleted.res.ok && deletedRows.some((r) => 'deleted' in r) ? 'pass' : deleted.res.ok ? 'skip' : 'fail', deleted.res.ok
87
+ ? deletedRows.some((r) => 'deleted' in r)
88
+ ? 'tombstones present'
89
+ : 'nothing has been removed in the retention window'
90
+ : `answered ${deleted.res.status}`, 'Absence means "archived" on a full sync and "not on this page" on an incremental one. Conflating them deletes a corpus.');
91
+ // 6 — a hash per item, and conditional requests.
92
+ const withContent = await get(withQuery(input.collection, { include: 'body', limit: '1' }));
93
+ const item = (body(withContent.body, key) ?? [])[0];
94
+ add('CSP-10', 'Each item carries a content hash', item && typeof item.contentHash === 'string' ? 'pass' : 'fail', item ? Object.keys(item).join(', ').slice(0, 120) : 'no rows to inspect', 'Without it a client re-embeds everything to discover that nothing changed.');
95
+ const etag = first.res.headers.get('etag');
96
+ if (etag) {
97
+ const again = await doFetch(withQuery(input.collection, { limit: '2' }), {
98
+ headers: { ...headers, 'if-none-match': etag },
99
+ });
100
+ add('CSP-11', 'A conditional request answers 304', again.status === 304 ? 'pass' : 'fail', `answered ${again.status}`, 'Polling costs a full transfer to learn nothing moved.');
101
+ }
102
+ else {
103
+ add('CSP-11', 'A conditional request answers 304', 'skip', 'no ETag on the collection', 'Polling costs a full transfer to learn nothing moved.');
104
+ }
105
+ // 7 — limits that name their window.
106
+ const limit = first.res.headers.get('ratelimit-limit');
107
+ const policy = first.res.headers.get('ratelimit-policy');
108
+ add('CSP-12', 'Rate limits are stated, with their window', limit && policy ? 'pass' : limit ? 'fail' : 'fail', [limit && `limit ${limit}`, policy && `policy ${policy}`].filter(Boolean).join(', ') || 'neither header', 'A limit with no period is a number per something, and a client that guesses wrong either crawls or is refused.');
109
+ const failed = checks.filter((c) => c.state === 'fail').length;
110
+ return {
111
+ protocol: exports.PROTOCOL_VERSION,
112
+ collection: input.collection,
113
+ checks,
114
+ passed: checks.filter((c) => c.state === 'pass').length,
115
+ failed,
116
+ };
117
+ }
118
+ function withQuery(url, params) {
119
+ const u = new URL(url);
120
+ for (const [k, v] of Object.entries(params))
121
+ u.searchParams.set(k, v);
122
+ return u.toString();
123
+ }
124
+ function body(payload, key) {
125
+ return payload[key] ?? payload[key.replace(/s$/, '')] ?? undefined;
126
+ }
127
+ //# sourceMappingURL=conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conformance.js","sourceRoot":"","sources":["../src/conformance.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { Check } from './conformance';
2
+ /**
3
+ * Everything that is wrong with a live site, from the outside.
4
+ *
5
+ * A customer putting their docs on a custom domain has to verify by hand that
6
+ * the health endpoint answers JSON, that the canonical link points at their
7
+ * host rather than ours, that a logged-out reader gets the page rather than a
8
+ * sign-in, that the feeds exist where their readers will look, that redirects
9
+ * actually redirect, and that nothing in the path is rewriting their HTML.
10
+ * Each of those is a sentence in a checklist somebody reads once and forgets,
11
+ * and all of them were things this customer discovered after going live.
12
+ *
13
+ * So they are a command. It takes a URL and nothing else, speaks plain HTTP, and
14
+ * is the same report whether it is pointed at us, at a staging mount, or at a
15
+ * competitor — a doctor that only works on its author's deployment is a
16
+ * checklist with extra steps.
17
+ */
18
+ export interface DoctorInput {
19
+ /** A reader URL on the site to examine, e.g. https://example.com/docs */
20
+ url: string;
21
+ /** The API base, when it is not the same origin (defaults to the site's). */
22
+ apiBase?: string;
23
+ fetchImpl?: typeof fetch;
24
+ }
25
+ export interface DoctorReport {
26
+ url: string;
27
+ checks: Check[];
28
+ passed: number;
29
+ failed: number;
30
+ warnings: number;
31
+ }
32
+ export declare function runDoctor(input: DoctorInput): Promise<DoctorReport>;
33
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAE3C;;;;;;;;;;;;;;;GAeG;AACH,MAAM,WAAW,WAAW;IAC1B,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAKD,wBAAsB,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CA2JzE"}
package/dist/doctor.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runDoctor = runDoctor;
4
+ /** A crawler-ish UA: some CDNs answer bots differently, which is the point. */
5
+ const CRAWLER_UA = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
6
+ async function runDoctor(input) {
7
+ const doFetch = input.fetchImpl ?? fetch;
8
+ const site = new URL(input.url);
9
+ const origin = site.origin;
10
+ const apiBase = (input.apiBase ?? origin).replace(/\/$/, '');
11
+ const checks = [];
12
+ const add = (id, title, state, detail, matters) => checks.push({ id, title, state, detail, matters });
13
+ const get = async (url, headers = {}) => {
14
+ try {
15
+ const res = await doFetch(url, { headers, redirect: 'manual' });
16
+ const text = res.status < 400 || res.status >= 500 ? await res.text() : await res.text();
17
+ return { ok: true, res, text };
18
+ }
19
+ catch (e) {
20
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
21
+ }
22
+ };
23
+ // 1. Health answers JSON, in every state.
24
+ const health = await get(`${apiBase}/api/health`, { accept: 'application/json' });
25
+ if (!health.ok) {
26
+ add('health', 'Health endpoint answers', 'fail', health.error, 'A caller cannot tell an outage from a bug.');
27
+ }
28
+ else {
29
+ let parsed = null;
30
+ try {
31
+ parsed = JSON.parse(health.text);
32
+ }
33
+ catch {
34
+ parsed = null;
35
+ }
36
+ if (parsed && typeof parsed === 'object') {
37
+ add('health', 'Health endpoint answers JSON', 'pass', `${health.res.status} with a JSON body.`, '');
38
+ }
39
+ else {
40
+ add('health', 'Health endpoint answers JSON', 'fail', `${health.res.status} with a non-JSON body: ${health.text.slice(0, 80)}`, 'A plain-text 500 reads as a permanent bug, so a careful client stops retrying a few-minute outage.');
41
+ }
42
+ }
43
+ // 2. A logged-out reader gets the page, not a sign-in.
44
+ const anon = await get(site.toString(), { accept: 'text/html' });
45
+ const html = anon.ok ? anon.text : '';
46
+ if (!anon.ok) {
47
+ add('public-read', 'A logged-out reader can read the page', 'fail', anon.error, 'Public docs behind a login are invisible to readers and to crawlers.');
48
+ }
49
+ else if (anon.res.status >= 300 && anon.res.status < 400) {
50
+ const to = anon.res.headers.get('location') ?? '';
51
+ const login = /sign-?in|login|auth/i.test(to);
52
+ add('public-read', 'A logged-out reader can read the page', login ? 'fail' : 'pass', `${anon.res.status} → ${to}`, login ? 'A public page that redirects to sign-in is a login wall; crawlers index nothing.' : '');
53
+ }
54
+ else {
55
+ add('public-read', 'A logged-out reader can read the page', anon.res.status === 200 ? 'pass' : 'fail', `${anon.res.status}`, 'Public docs must answer an anonymous GET.');
56
+ }
57
+ // 3. The canonical link names THIS host.
58
+ const canonical = /<link[^>]+rel=["']canonical["'][^>]+href=["']([^"']+)["']/i.exec(html)?.[1]
59
+ ?? /<link[^>]+href=["']([^"']+)["'][^>]+rel=["']canonical["']/i.exec(html)?.[1];
60
+ if (!html) {
61
+ add('canonical', 'Canonical link points at this host', 'skip', 'No HTML to read.', '');
62
+ }
63
+ else if (!canonical) {
64
+ add('canonical', 'Canonical link points at this host', 'fail', 'No rel=canonical in the document.', 'Without one, two hosts serving the same page compete, and the wrong one can win.');
65
+ }
66
+ else {
67
+ const same = safeHost(canonical) === site.host;
68
+ add('canonical', 'Canonical link points at this host', same ? 'pass' : 'fail', canonical, same ? '' : 'Search engines credit the canonical host, so a platform URL here hands your authority to the platform.');
69
+ }
70
+ // 4. Nothing in the path is rewriting the HTML.
71
+ if (html) {
72
+ const rewritten = html.includes('/cdn-cgi/scripts/') && html.includes('email-decode');
73
+ const scrambled = html.includes('data-cfemail');
74
+ add('html-untouched', 'No proxy is rewriting the markup', rewritten || scrambled ? 'fail' : 'pass', rewritten || scrambled
75
+ ? 'Cloudflare Email Obfuscation is rewriting addresses on this page.'
76
+ : 'No rewritten email addresses.', 'The injected decoder has no CSP nonce, so it is refused and every address renders as [email protected] with a dead link. Turn Email Obfuscation off for these paths.');
77
+ }
78
+ // 5. The document is cacheable, and says so.
79
+ if (anon.ok) {
80
+ const cc = anon.res.headers.get('cache-control') ?? '';
81
+ const bad = /no-store/i.test(cc) || cc === '';
82
+ add('cache-headers', 'The document carries a usable Cache-Control', bad ? 'fail' : 'pass', cc || '(none)', 'Without one, every reader pays full origin latency and a CDN in front of it does nothing.');
83
+ }
84
+ // 6. Crawlers are let in, and told where to look.
85
+ const robots = await get(`${origin}/robots.txt`, { 'user-agent': CRAWLER_UA });
86
+ if (!robots.ok || robots.res.status !== 200) {
87
+ add('robots', 'robots.txt is served', 'fail', robots.ok ? `${robots.res.status}` : robots.error, 'Without it a crawler guesses, and some guess "do not index".');
88
+ }
89
+ else {
90
+ const blocked = blocksPath(robots.text, site.pathname);
91
+ add('robots', 'robots.txt allows this path', blocked ? 'fail' : 'pass', blocked ? `Disallowed by robots.txt: ${site.pathname}` : `Allows ${site.pathname || '/'}`, 'A Disallow here means the page is never indexed, however good it is.');
92
+ add('sitemap', 'robots.txt names a sitemap', /^\s*sitemap:/im.test(robots.text) ? 'pass' : 'fail', /^\s*sitemap:/im.test(robots.text) ? 'Declared.' : 'No Sitemap: line.', 'A crawler finds pages faster when it is handed the list.');
93
+ }
94
+ // 7. As a crawler, the same page.
95
+ const asBot = await get(site.toString(), { accept: 'text/html', 'user-agent': CRAWLER_UA });
96
+ if (asBot.ok) {
97
+ add('crawler-access', 'A crawler gets the same answer as a reader', asBot.res.status === (anon.ok ? anon.res.status : 200) ? 'pass' : 'fail', `reader ${anon.ok ? anon.res.status : '?'}, crawler ${asBot.res.status}`, 'A WAF rule or bot challenge that answers crawlers differently removes the site from search without telling anybody.');
98
+ }
99
+ // 8. The feeds exist where a reader will look for them.
100
+ const feed = await get(`${origin}${site.pathname.replace(/\/$/, '')}/rss.xml`, { accept: 'application/rss+xml' });
101
+ if (feed.ok) {
102
+ add('feeds', 'A feed is served under this path', feed.res.status === 200 ? 'pass' : 'skip', `${feed.res.status} for ${site.pathname.replace(/\/$/, '')}/rss.xml`, 'Readers and aggregators look beside the page, not at an API URL.');
103
+ }
104
+ return {
105
+ url: input.url,
106
+ checks,
107
+ passed: checks.filter((c) => c.state === 'pass').length,
108
+ failed: checks.filter((c) => c.state === 'fail').length,
109
+ warnings: checks.filter((c) => c.state === 'skip').length,
110
+ };
111
+ }
112
+ function safeHost(href) {
113
+ try {
114
+ return new URL(href).host;
115
+ }
116
+ catch {
117
+ return null;
118
+ }
119
+ }
120
+ /**
121
+ * Does robots.txt disallow this path for everyone?
122
+ *
123
+ * Only the `User-agent: *` group is read: a rule aimed at one named crawler is
124
+ * a deliberate choice, and reporting it as a fault would train people to
125
+ * ignore the report.
126
+ */
127
+ function blocksPath(robots, pathname) {
128
+ const path = pathname || '/';
129
+ let inStar = false;
130
+ for (const raw of robots.split(/\r?\n/)) {
131
+ const line = raw.replace(/#.*$/, '').trim();
132
+ if (!line)
133
+ continue;
134
+ const [field, ...rest] = line.split(':');
135
+ const value = rest.join(':').trim();
136
+ const key = (field ?? '').trim().toLowerCase();
137
+ if (key === 'user-agent')
138
+ inStar = value === '*';
139
+ else if (inStar && key === 'disallow' && value && path.startsWith(value))
140
+ return true;
141
+ else if (inStar && key === 'allow' && value && path.startsWith(value))
142
+ return false;
143
+ }
144
+ return false;
145
+ }
146
+ //# sourceMappingURL=doctor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"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"}
package/dist/run.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { ForumulateClient } from '@forumulate/sdk';
2
+ export declare const VERSION = "0.1.0";
3
+ export interface RunDeps {
4
+ /** The API client. Injected so the CLI is testable without a live server. */
5
+ client: ForumulateClient;
6
+ out: (line: string) => void;
7
+ err: (line: string) => void;
8
+ }
9
+ /**
10
+ * Parse and execute a CLI invocation. Pure w.r.t. I/O (uses the injected deps),
11
+ * so it can be unit-tested with a fake client. Returns a process exit code.
12
+ */
13
+ export declare function run(argv: string[], deps: RunDeps): Promise<number>;
14
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAIxD,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,MAAM,WAAW,OAAO;IACtB,6EAA6E;IAC7E,MAAM,EAAE,gBAAgB,CAAC;IACzB,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5B,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAgCD;;;GAGG;AACH,wBAAsB,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAwKxE"}
package/dist/run.js ADDED
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.VERSION = void 0;
4
+ exports.run = run;
5
+ const conformance_1 = require("./conformance");
6
+ const doctor_1 = require("./doctor");
7
+ exports.VERSION = '0.1.0';
8
+ const HELP = `forumulate — Forumulate command-line interface
9
+
10
+ Usage:
11
+ forumulate <command> [args]
12
+
13
+ Commands:
14
+ whoami Show the authenticated user
15
+ orgs List your organizations
16
+ spaces <orgId> List spaces in an org
17
+ items <orgId> <spaceId> List what is in a space, with where each
18
+ piece's content lives
19
+ search <orgId> <query...> Unified search across docs/forum/blog
20
+ publish <orgId> <spaceId> <pageId> Publish a page
21
+ snapshot <orgId> <spaceId> <label> Create a space version snapshot
22
+ conformance <collectionUrl> Run the Content Sync Protocol suite against
23
+ any platform's collection endpoint
24
+ doctor <readerUrl> Check a live site from the outside: health,
25
+ public read, canonical, cache headers,
26
+ crawler access, feeds, HTML rewriting
27
+ help Show this help
28
+
29
+ Environment:
30
+ FORUMULATE_URL API base URL (default http://localhost:4000)
31
+ FORUMULATE_TOKEN Personal Access Token (Bearer)
32
+
33
+ Flags:
34
+ --json Emit raw JSON instead of a formatted summary
35
+ --version Print the CLI version
36
+ `;
37
+ /**
38
+ * Parse and execute a CLI invocation. Pure w.r.t. I/O (uses the injected deps),
39
+ * so it can be unit-tested with a fake client. Returns a process exit code.
40
+ */
41
+ async function run(argv, deps) {
42
+ const args = argv.filter((a) => !a.startsWith('--'));
43
+ const flags = new Set(argv.filter((a) => a.startsWith('--')));
44
+ const json = flags.has('--json');
45
+ const [cmd, ...rest] = args;
46
+ if (flags.has('--version')) {
47
+ deps.out(exports.VERSION);
48
+ return 0;
49
+ }
50
+ if (!cmd || cmd === 'help' || flags.has('--help')) {
51
+ deps.out(HELP);
52
+ return cmd && cmd !== 'help' ? 1 : 0;
53
+ }
54
+ const dump = (v) => deps.out(JSON.stringify(v, null, 2));
55
+ try {
56
+ switch (cmd) {
57
+ case 'whoami': {
58
+ const { user } = await deps.client.auth.me();
59
+ if (json)
60
+ dump(user);
61
+ else
62
+ deps.out(`${user.name} <${user.email}> (${user.id})`);
63
+ return 0;
64
+ }
65
+ case 'orgs': {
66
+ const { orgs } = await deps.client.orgs.list();
67
+ if (json)
68
+ dump(orgs);
69
+ else
70
+ for (const o of orgs)
71
+ deps.out(`${o.id} ${o.slug} ${o.name}`);
72
+ return 0;
73
+ }
74
+ case 'spaces': {
75
+ const [orgId] = rest;
76
+ if (!orgId)
77
+ return usage(deps, 'spaces <orgId>');
78
+ const { spaces } = await deps.client.spaces.list(orgId);
79
+ if (json)
80
+ dump(spaces);
81
+ else
82
+ for (const s of spaces)
83
+ deps.out(`${s.id} ${s.slug} [${s.type}] ${s.name}`);
84
+ return 0;
85
+ }
86
+ /*
87
+ * Enumerate a space, and say where the content is.
88
+ *
89
+ * `spaces` tells you a space holds 1,226 pieces; nothing told you how to
90
+ * walk them. The flag that matters is `contentIn`: `sections` is filled
91
+ * only for a piece written against a shape, so a space of free-form pages
92
+ * answers `include=sections` with empty objects and a sync reports
93
+ * success having carried nothing. This asks for `body` as well, and
94
+ * prints the per-row answer so the distinction is visible from a
95
+ * terminal rather than discovered a week later.
96
+ */
97
+ case 'items': {
98
+ const [orgId, spaceId] = rest;
99
+ if (!orgId || !spaceId)
100
+ return usage(deps, 'items <orgId> <spaceId>');
101
+ const res = await deps.client.items.list(orgId, spaceId, {
102
+ limit: 50,
103
+ include: ['sections', 'body'],
104
+ });
105
+ if (json)
106
+ dump(res);
107
+ else {
108
+ for (const i of res.items) {
109
+ const where = i.contentIn ? `[${i.contentIn}]` : '[no content]';
110
+ deps.out(`${i.id} ${where} ${i.title}`);
111
+ }
112
+ // `page.total` against what we actually printed: the check that would
113
+ // have caught a sync that quietly carried 6 of 1,226 pieces.
114
+ if (res.page) {
115
+ const total = res.page.total;
116
+ deps.out(`-- ${res.items.length} shown${total === undefined ? '' : ` of ${total}`}` +
117
+ (res.page.has_more ? `, more available (cursor: ${res.page.next_cursor ?? ''})` : ''));
118
+ }
119
+ }
120
+ return 0;
121
+ }
122
+ case 'search': {
123
+ const [orgId, ...q] = rest;
124
+ if (!orgId || q.length === 0)
125
+ return usage(deps, 'search <orgId> <query...>');
126
+ const { hits } = await deps.client.search.unified(orgId, q.join(' '));
127
+ if (json)
128
+ dump(hits);
129
+ else
130
+ for (const h of hits)
131
+ deps.out(`[${h.sourceType}] ${h.title} ${h.url}`);
132
+ return 0;
133
+ }
134
+ case 'publish': {
135
+ const [orgId, spaceId, pageId] = rest;
136
+ if (!orgId || !spaceId || !pageId)
137
+ return usage(deps, 'publish <orgId> <spaceId> <pageId>');
138
+ await deps.client.spaces.publishPage(orgId, spaceId, pageId);
139
+ deps.out(`Published ${pageId}.`);
140
+ return 0;
141
+ }
142
+ case 'snapshot': {
143
+ const [orgId, spaceId, ...label] = rest;
144
+ if (!orgId || !spaceId || label.length === 0)
145
+ return usage(deps, 'snapshot <orgId> <spaceId> <label>');
146
+ const { snapshot } = await deps.client.snapshots.create(orgId, spaceId, label.join(' '));
147
+ if (json)
148
+ dump(snapshot);
149
+ else
150
+ deps.out(`Snapshot created.`);
151
+ return 0;
152
+ }
153
+ /*
154
+ * The suite that says whether a platform can be synced correctly, run
155
+ * against whichever one you point it at. Ours passes; publishing a
156
+ * conformance suite that only runs against its author proves nothing.
157
+ */
158
+ case 'conformance': {
159
+ const [collection] = rest;
160
+ if (!collection) {
161
+ deps.err('Usage: forumulate conformance <collectionUrl>');
162
+ return 1;
163
+ }
164
+ const report = await (0, conformance_1.runConformance)({
165
+ collection,
166
+ token: process.env.FORUMULATE_TOKEN,
167
+ versionHeader: { name: 'forumulate-version', value: '2026-09-01' },
168
+ });
169
+ if (json) {
170
+ dump(report);
171
+ }
172
+ else {
173
+ deps.out(`Content Sync Protocol ${report.protocol} — ${report.collection}`);
174
+ for (const check of report.checks) {
175
+ const mark = check.state === 'pass' ? 'PASS' : check.state === 'fail' ? 'FAIL' : 'SKIP';
176
+ deps.out(` ${mark} ${check.id} ${check.title}`);
177
+ deps.out(` ${check.detail}`);
178
+ if (check.state === 'fail')
179
+ deps.out(` why it matters: ${check.matters}`);
180
+ }
181
+ deps.out(`${report.passed} passed, ${report.failed} failed`);
182
+ }
183
+ return report.failed > 0 ? 1 : 0;
184
+ }
185
+ /*
186
+ * The going-live checklist, as something that runs.
187
+ *
188
+ * Every item here is one a customer found out about after readers did:
189
+ * health answering plain text during a deploy, a canonical link pointing
190
+ * at our host instead of theirs, a CDN rewriting their email addresses
191
+ * into a script the CSP then refused, feeds that existed only at an API
192
+ * URL. Read once, forgotten, discovered live. It takes a reader URL and
193
+ * speaks plain HTTP, so it works against a staging mount or a competitor
194
+ * just as well — a doctor that only passes on its author's deployment is
195
+ * a checklist with extra steps.
196
+ */
197
+ case 'doctor': {
198
+ const [url] = rest;
199
+ if (!url)
200
+ return usage(deps, 'doctor <readerUrl>');
201
+ const report = await (0, doctor_1.runDoctor)({ url, apiBase: process.env.FORUMULATE_URL });
202
+ if (json) {
203
+ dump(report);
204
+ }
205
+ else {
206
+ deps.out(`Checking ${report.url}`);
207
+ for (const check of report.checks) {
208
+ const mark = check.state === 'pass' ? 'PASS' : check.state === 'fail' ? 'FAIL' : 'SKIP';
209
+ deps.out(` ${mark} ${check.id} ${check.title}`);
210
+ deps.out(` ${check.detail}`);
211
+ if (check.state === 'fail' && check.matters)
212
+ deps.out(` why it matters: ${check.matters}`);
213
+ }
214
+ deps.out(`${report.passed} passed, ${report.failed} failed, ${report.warnings} skipped`);
215
+ }
216
+ return report.failed > 0 ? 1 : 0;
217
+ }
218
+ default:
219
+ deps.err(`Unknown command: ${cmd}`);
220
+ deps.out(HELP);
221
+ return 1;
222
+ }
223
+ }
224
+ catch (e) {
225
+ const err = e;
226
+ deps.err(`Error${err.status ? ` ${err.status}` : ''}${err.code ? ` (${err.code})` : ''}: ${err.message ?? String(e)}`);
227
+ return 1;
228
+ }
229
+ }
230
+ function usage(deps, form) {
231
+ deps.err(`Usage: forumulate ${form}`);
232
+ return 1;
233
+ }
234
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../src/run.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@forumulate/cli",
3
+ "version": "0.1.0",
4
+ "description": "Command-line interface for Forumulate — manage orgs, spaces, docs, and search from your terminal.",
5
+ "license": "MIT",
6
+ "homepage": "https://forumulate.com/guides",
7
+ "bugs": {
8
+ "url": "https://forumulate.com/community"
9
+ },
10
+ "keywords": [
11
+ "forumulate",
12
+ "cli",
13
+ "docs",
14
+ "documentation",
15
+ "publish",
16
+ "static-site",
17
+ "knowledge-base",
18
+ "doctor"
19
+ ],
20
+ "bin": {
21
+ "forumulate": "dist/cli.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "dependencies": {
32
+ "@forumulate/sdk": "0.1.0"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.9.0",
36
+ "typescript": "^5.6.3",
37
+ "vitest": "^2.1.4"
38
+ },
39
+ "author": "Forumulate",
40
+ "scripts": {
41
+ "build": "tsc -p tsconfig.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test": "vitest run"
44
+ }
45
+ }