@theholocron/holocron-plugin-fern 5.0.0-alpha.43 → 5.0.0-alpha.430
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 +3 -1
- package/dist/index.d.mts +53 -21
- package/dist/index.mjs +126 -16
- package/package.json +24 -21
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<!-- alex disable host-hostess -->
|
|
2
|
+
|
|
1
3
|
<!-- editorconfig-checker-disable-file -->
|
|
2
4
|
|
|
3
5
|
# `@theholocron/holocron-plugin-fern`
|
|
@@ -83,7 +85,7 @@ running `holocron setup`.
|
|
|
83
85
|
|
|
84
86
|
## CI secrets
|
|
85
87
|
|
|
86
|
-
Add `HOLOCRON_FERN_TOKEN` to your repo's GitHub Secrets. The `wiki
|
|
88
|
+
Add `HOLOCRON_FERN_TOKEN` to your repo's GitHub Secrets. The `publish-wiki` thin caller
|
|
87
89
|
passes it to the Fern CLI during `fern generate --docs`.
|
|
88
90
|
|
|
89
91
|
| Secret | Purpose |
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { AuthError, DnsRecordRequest, ResolveTokenInput, Wiki, WikiProvisionOpts, WikiProxyConfig } from "@theholocron/cli";
|
|
2
|
-
|
|
3
|
-
declare const
|
|
4
|
-
|
|
1
|
+
import { AuthError, DnsRecordRequest, PluginToken, ResolveTokenInput, Wiki, WikiProvisionOpts, WikiProxyConfig } from "@theholocron/cli";
|
|
2
|
+
/** Resolves the Fern token from `--token`, `HOLOCRON_FERN_TOKEN`, `FERN_TOKEN` or the keyring entry for `fern`, and throws `AuthError` when none is set. */
|
|
3
|
+
export declare const resolveToken: import("@theholocron/cli").TokenResolver;
|
|
4
|
+
/** Every token this plugin takes, for `holocron doctor secrets --verify`, with the layers that hold it. */
|
|
5
|
+
export declare const tokens: PluginToken[];
|
|
6
|
+
/** The Fern CLI version written into `fern/fern.config.json`. */
|
|
7
|
+
export declare const FERN_VERSION = "5.114.1";
|
|
8
|
+
/** Options for the Fern `wiki` capability. */
|
|
9
|
+
interface FernWikiOptions extends ResolveTokenInput {
|
|
5
10
|
/** Absolute path to the working repo root. Injected by the loader from RuntimeContext. */
|
|
6
11
|
repoRoot?: string;
|
|
7
|
-
/** Active org name (e.g. "
|
|
12
|
+
/** Active org name (e.g. "acme"). Injected by the loader from RuntimeContext. */
|
|
8
13
|
org?: string;
|
|
9
14
|
/**
|
|
10
15
|
* "owner/name" repo coordinate. Injected by the loader from RuntimeContext.
|
|
@@ -12,15 +17,15 @@ interface FernWikiOptions {
|
|
|
12
17
|
*/
|
|
13
18
|
repo?: string;
|
|
14
19
|
/**
|
|
15
|
-
* Base domain for the Fern docs site (e.g. "wiki.
|
|
20
|
+
* Base domain for the Fern docs site (e.g. "wiki.example.dev").
|
|
16
21
|
*
|
|
17
22
|
* When provided without a path, the repo name is appended automatically:
|
|
18
|
-
* domain "wiki.
|
|
19
|
-
* → custom-domain: wiki.
|
|
23
|
+
* domain "wiki.example.dev" + repo "owner/holocron"
|
|
24
|
+
* → custom-domain: wiki.example.dev/holocron
|
|
20
25
|
* → multi-source: true
|
|
21
26
|
*
|
|
22
27
|
* You can also supply the full path explicitly:
|
|
23
|
-
* domain "wiki.
|
|
28
|
+
* domain "wiki.example.dev/holocron"
|
|
24
29
|
*
|
|
25
30
|
* Note: password protection is configured in the Fern Dashboard
|
|
26
31
|
* (https://dashboard.buildwithfern.com) — it is not configurable via docs.yml.
|
|
@@ -30,7 +35,7 @@ interface FernWikiOptions {
|
|
|
30
35
|
* Fern workspace org slug. Defaults to `org` (the GitHub org name).
|
|
31
36
|
* Set this explicitly when the Fern workspace name differs from the
|
|
32
37
|
* GitHub org — e.g. the workspace was registered as "holocron" while
|
|
33
|
-
* the GitHub org is "
|
|
38
|
+
* the GitHub org is "acme".
|
|
34
39
|
*/
|
|
35
40
|
fernOrg?: string;
|
|
36
41
|
/**
|
|
@@ -45,27 +50,54 @@ interface FernWikiOptions {
|
|
|
45
50
|
*/
|
|
46
51
|
icon?: string;
|
|
47
52
|
}
|
|
48
|
-
|
|
53
|
+
/** `wiki` capability for Fern: writes the Fern config and docs scaffold for a repo. */
|
|
54
|
+
export declare class FernWiki implements Wiki {
|
|
49
55
|
private readonly opts;
|
|
56
|
+
/** Capability key; always `"wiki"`. */
|
|
50
57
|
readonly key: "wiki";
|
|
58
|
+
/** Provider name; always `"fern"`. */
|
|
51
59
|
readonly providerName = "fern";
|
|
52
60
|
constructor(opts: FernWikiOptions);
|
|
53
61
|
provision(callOpts?: WikiProvisionOpts): Promise<string>;
|
|
54
62
|
proxyConfig(): WikiProxyConfig | null;
|
|
63
|
+
/**
|
|
64
|
+
* Confirms `HOLOCRON_FERN_TOKEN`/`FERN_TOKEN` resolves, or a Fern token
|
|
65
|
+
* is stored in the keyring — not a live check. Fern exposes no
|
|
66
|
+
* lightweight endpoint to verify a token actually works (the only
|
|
67
|
+
* token-consuming operation is a full `fern generate`, see
|
|
68
|
+
* holocron#1079); this only confirms one is configured, the same way
|
|
69
|
+
* CI's `fern generate --docs` step would find it.
|
|
70
|
+
*
|
|
71
|
+
* Deliberately ignores `cliToken` (`--token`): CI's step only ever sees
|
|
72
|
+
* the env var from a GitHub Actions secret, never a CLI flag — and a
|
|
73
|
+
* bare `--token <value>` meant for some OTHER provider is forwarded to
|
|
74
|
+
* every loaded plugin as a fallback (see AGENTS.md), so honoring it
|
|
75
|
+
* here would report "configured" even when no Fern-specific credential
|
|
76
|
+
* exists anywhere CI would actually look.
|
|
77
|
+
*
|
|
78
|
+
* Only `AuthError` (the specific "no token found" signal) is treated as
|
|
79
|
+
* `ok: false` — any other error (e.g. a real keyring failure) is a
|
|
80
|
+
* genuine problem and must surface as a failure, not get misreported as
|
|
81
|
+
* a missing token.
|
|
82
|
+
*/
|
|
83
|
+
whoami(): Promise<{
|
|
84
|
+
ok: boolean;
|
|
85
|
+
}>;
|
|
55
86
|
dnsRecord(): DnsRecordRequest | null;
|
|
56
87
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
declare function wiki(opts: FernPluginOptions): Wiki;
|
|
64
|
-
declare function createPlugin(options?: FernPluginOptions): {
|
|
88
|
+
/** Options for the Fern plugin; the same as the `wiki` capability's. */
|
|
89
|
+
export type FernPluginOptions = FernWikiOptions;
|
|
90
|
+
/** Builds the `wiki` capability from the plugin options. */
|
|
91
|
+
export declare function wiki(opts: FernPluginOptions): Wiki;
|
|
92
|
+
/** Creates the Fern plugin: its `connection` and `token` probes and the `wiki` capability. */
|
|
93
|
+
export declare function createPlugin(options?: FernPluginOptions): {
|
|
65
94
|
name: string;
|
|
95
|
+
probes: {
|
|
96
|
+
connection: import("@theholocron/cli").Probe;
|
|
97
|
+
token: import("@theholocron/cli").Probe;
|
|
98
|
+
};
|
|
66
99
|
capabilities: {
|
|
67
100
|
wiki: () => Wiki;
|
|
68
101
|
};
|
|
69
102
|
};
|
|
70
|
-
|
|
71
|
-
export { AuthError, FERN_VERSION, FernPluginOptions, FernWiki, type FernWikiOptions, type ResolveTokenInput, createPlugin, resolveToken, wiki };
|
|
103
|
+
export { AuthError, type FernWikiOptions, type ResolveTokenInput };
|
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,37 @@
|
|
|
1
|
+
import { AuthError, connectionProbe, createResolveToken, tokenExpectations } from "@theholocron/cli";
|
|
1
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
3
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
|
|
4
|
+
import { stringifyJson } from "@theholocron/json-utils";
|
|
5
|
+
// src/probes/token/expectations.ts
|
|
6
|
+
/**
|
|
7
|
+
* Where the Fern token is held: CI only (the `publish-wiki` workflow), so `ci` is the one layer declared. Fern has no
|
|
8
|
+
* endpoint that reports a token's permissions, so none are declared and the probe says so instead of guessing. This is
|
|
9
|
+
* the plugin's `tokens` declaration, read through `tokenExpectations()`; change it and the Fern note in
|
|
10
|
+
* `docs/src/content/docs/tokens.mdx` together.
|
|
11
|
+
*/
|
|
12
|
+
const EXPECTED = { layers: ["ci"] };
|
|
13
|
+
// src/auth.ts
|
|
14
|
+
/** Resolves the Fern token from `--token`, `HOLOCRON_FERN_TOKEN`, `FERN_TOKEN` or the keyring entry for `fern`, and throws `AuthError` when none is set. */
|
|
15
|
+
const resolveToken = createResolveToken({
|
|
16
|
+
envName: "HOLOCRON_FERN_TOKEN",
|
|
17
|
+
vendorEnvName: "FERN_TOKEN",
|
|
18
|
+
keyringService: "fern",
|
|
19
|
+
errorMessage: "no Fern token found. Set HOLOCRON_FERN_TOKEN / FERN_TOKEN, or run: holocron new token fern <token>"
|
|
20
|
+
});
|
|
21
|
+
/** Every token this plugin takes, for `holocron doctor secrets --verify`, with the layers that hold it. */
|
|
22
|
+
const tokens = [{
|
|
23
|
+
...resolveToken.token,
|
|
24
|
+
expect: EXPECTED
|
|
25
|
+
}];
|
|
26
|
+
// src/capabilities/wiki.ts
|
|
27
|
+
/** The Fern CLI version written into `fern/fern.config.json`. */
|
|
5
28
|
const FERN_VERSION = "5.114.1";
|
|
29
|
+
/** `wiki` capability for Fern: writes the Fern config and docs scaffold for a repo. */
|
|
6
30
|
var FernWiki = class {
|
|
7
31
|
opts;
|
|
32
|
+
/** Capability key; always `"wiki"`. */
|
|
8
33
|
key = "wiki";
|
|
34
|
+
/** Provider name; always `"fern"`. */
|
|
9
35
|
providerName = "fern";
|
|
10
36
|
constructor(opts) {
|
|
11
37
|
this.opts = opts;
|
|
@@ -33,6 +59,38 @@ var FernWiki = class {
|
|
|
33
59
|
headers: { "X-Fern-Host": this.opts.domain.split("/")[0] }
|
|
34
60
|
};
|
|
35
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Confirms `HOLOCRON_FERN_TOKEN`/`FERN_TOKEN` resolves, or a Fern token
|
|
64
|
+
* is stored in the keyring — not a live check. Fern exposes no
|
|
65
|
+
* lightweight endpoint to verify a token actually works (the only
|
|
66
|
+
* token-consuming operation is a full `fern generate`, see
|
|
67
|
+
* holocron#1079); this only confirms one is configured, the same way
|
|
68
|
+
* CI's `fern generate --docs` step would find it.
|
|
69
|
+
*
|
|
70
|
+
* Deliberately ignores `cliToken` (`--token`): CI's step only ever sees
|
|
71
|
+
* the env var from a GitHub Actions secret, never a CLI flag — and a
|
|
72
|
+
* bare `--token <value>` meant for some OTHER provider is forwarded to
|
|
73
|
+
* every loaded plugin as a fallback (see AGENTS.md), so honoring it
|
|
74
|
+
* here would report "configured" even when no Fern-specific credential
|
|
75
|
+
* exists anywhere CI would actually look.
|
|
76
|
+
*
|
|
77
|
+
* Only `AuthError` (the specific "no token found" signal) is treated as
|
|
78
|
+
* `ok: false` — any other error (e.g. a real keyring failure) is a
|
|
79
|
+
* genuine problem and must surface as a failure, not get misreported as
|
|
80
|
+
* a missing token.
|
|
81
|
+
*/
|
|
82
|
+
async whoami() {
|
|
83
|
+
try {
|
|
84
|
+
resolveToken({
|
|
85
|
+
...this.opts,
|
|
86
|
+
cliToken: void 0
|
|
87
|
+
});
|
|
88
|
+
return { ok: true };
|
|
89
|
+
} catch (err) {
|
|
90
|
+
if (err instanceof AuthError) return { ok: false };
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
36
94
|
dnsRecord() {
|
|
37
95
|
const { org, domain, fernOrg } = this.opts;
|
|
38
96
|
if (!domain) return null;
|
|
@@ -52,10 +110,10 @@ async function writeFernConfig({ repoRoot, fernOrg }) {
|
|
|
52
110
|
const fernDir = join(repoRoot, "fern");
|
|
53
111
|
await mkdir(fernDir, { recursive: true });
|
|
54
112
|
const org = fernOrg ?? "holocron";
|
|
55
|
-
const content =
|
|
113
|
+
const content = stringifyJson({
|
|
56
114
|
organization: org,
|
|
57
115
|
version: FERN_VERSION
|
|
58
|
-
}
|
|
116
|
+
});
|
|
59
117
|
await writeFile(join(fernDir, "fern.config.json"), content, "utf8");
|
|
60
118
|
return `fern.config.json: org=${org}, version=${FERN_VERSION}`;
|
|
61
119
|
}
|
|
@@ -130,24 +188,76 @@ function updateInstancesBlock(content, instanceUrl, customDomain, multiSource, r
|
|
|
130
188
|
if (match[0] === newBlock) return content;
|
|
131
189
|
return content.replace(blockRe, newBlock);
|
|
132
190
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
191
|
+
// src/probes/token/parse-layer.ts
|
|
192
|
+
/** Reads `--layer <word>` (or `--layer=<word>`) from the words after the probe name, which the CLI passes when `--environment` named a layer. */
|
|
193
|
+
function parseLayer(args) {
|
|
194
|
+
for (let i = 0; i < args.length; i++) {
|
|
195
|
+
const arg = args[i];
|
|
196
|
+
if (arg === "--layer") return args[i + 1];
|
|
197
|
+
if (arg.startsWith("--layer=")) return arg.slice(8);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// src/probes/token/token.ts
|
|
201
|
+
/**
|
|
202
|
+
* The `token` probe for Fern. Fern has no endpoint that reports who a token is or what it may do, so the probe cannot
|
|
203
|
+
* judge permissions: it checks that a token is set, says whether the layer holds it when `--layer` is passed (because
|
|
204
|
+
* `--environment` named one) against the plugin's `tokens` declaration, and states plainly that permissions are not
|
|
205
|
+
* checked. It makes no request and never prints the token.
|
|
206
|
+
*/
|
|
207
|
+
function tokenProbe(ctx) {
|
|
208
|
+
return {
|
|
209
|
+
summary: "Check that a Fern token is set, and for a layer that it is one that should hold it. Fern cannot report a token's permissions, so those are not checked. Read-only, no request.",
|
|
210
|
+
writes: false,
|
|
211
|
+
acceptsToken: true,
|
|
212
|
+
async run(probe) {
|
|
213
|
+
probe.secret(ctx.token());
|
|
214
|
+
probe.say({
|
|
215
|
+
status: "ok",
|
|
216
|
+
label: "token",
|
|
217
|
+
detail: "set"
|
|
218
|
+
});
|
|
219
|
+
const layer = parseLayer(probe.args ?? []);
|
|
220
|
+
if (layer) {
|
|
221
|
+
const declared = tokenExpectations(tokens, { layer }).length > 0;
|
|
222
|
+
probe.say(declared ? {
|
|
223
|
+
status: "ok",
|
|
224
|
+
label: layer,
|
|
225
|
+
detail: "a layer that should hold this token"
|
|
226
|
+
} : {
|
|
227
|
+
status: "info",
|
|
228
|
+
label: layer,
|
|
229
|
+
detail: "not a layer this token is declared for, so it is only described"
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
probe.say({
|
|
233
|
+
status: "info",
|
|
234
|
+
label: "permissions",
|
|
235
|
+
detail: "Fern cannot report a token's permissions, so they are not checked"
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
// src/index.ts
|
|
241
|
+
/** Builds the `wiki` capability from the plugin options. */
|
|
143
242
|
function wiki(opts) {
|
|
144
243
|
return new FernWiki(opts);
|
|
145
244
|
}
|
|
245
|
+
/** Creates the Fern plugin: its `connection` and `token` probes and the `wiki` capability. */
|
|
146
246
|
function createPlugin(options = {}) {
|
|
147
247
|
return {
|
|
148
248
|
name: "@theholocron/holocron-plugin-fern",
|
|
249
|
+
probes: {
|
|
250
|
+
connection: connectionProbe({
|
|
251
|
+
provider: "Fern",
|
|
252
|
+
token: () => resolveToken(options),
|
|
253
|
+
verify: async () => ({
|
|
254
|
+
ok: true,
|
|
255
|
+
subject: "a token is set (Fern cannot check it from here)"
|
|
256
|
+
})
|
|
257
|
+
}),
|
|
258
|
+
token: tokenProbe({ token: () => resolveToken(options) })
|
|
259
|
+
},
|
|
149
260
|
capabilities: { wiki: () => wiki(options) }
|
|
150
261
|
};
|
|
151
262
|
}
|
|
152
|
-
|
|
153
|
-
export { AuthError, FERN_VERSION, FernWiki, createPlugin, resolveToken, wiki };
|
|
263
|
+
export { AuthError, FERN_VERSION, FernWiki, createPlugin, resolveToken, tokens, wiki };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-fern",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.430",
|
|
4
4
|
"description": "Holocron plugin for Fern. Implements the wiki capability for engineering knowledge publishing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fern",
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"plugin",
|
|
9
9
|
"wiki"
|
|
10
10
|
],
|
|
11
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://docs.theholocron.dev/holocron/plugins/fern/",
|
|
12
12
|
"bugs": "https://github.com/theholocron/holocron/issues",
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
|
15
15
|
"url": "git+https://github.com/theholocron/holocron.git",
|
|
16
|
-
"directory": "
|
|
16
|
+
"directory": "plugins/holocron-plugin-fern"
|
|
17
17
|
},
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"author": "Newton Koumantzelis",
|
|
@@ -29,25 +29,28 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist"
|
|
31
31
|
],
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@theholocron/json-utils": "^1.17.0-alpha.4"
|
|
34
|
+
},
|
|
32
35
|
"devDependencies": {
|
|
33
|
-
"@theholocron/eslint-config": "^8.4.5",
|
|
34
|
-
"@theholocron/tsconfig": "^8.4.5",
|
|
35
|
-
"@theholocron/tsdown-config": "^8.4.5",
|
|
36
|
-
"@theholocron/vitest-config": "^8.4.5",
|
|
37
36
|
"@types/node": "^26",
|
|
38
|
-
"@vitest/coverage-v8": "^
|
|
39
|
-
"@vitest/eslint-plugin": "^1.6.
|
|
40
|
-
"eslint": "^10.
|
|
41
|
-
"eslint-plugin-n": "^18.
|
|
42
|
-
"globals": "^17.
|
|
43
|
-
"tsdown": "^0.
|
|
44
|
-
"tsx": "4.23.
|
|
37
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
38
|
+
"@vitest/eslint-plugin": "^1.6.28",
|
|
39
|
+
"eslint": "^10.14.0",
|
|
40
|
+
"eslint-plugin-n": "^18.5.0",
|
|
41
|
+
"globals": "^17.14.0",
|
|
42
|
+
"tsdown": "^0.23.5",
|
|
43
|
+
"tsx": "4.23.15",
|
|
45
44
|
"typescript": "^5.9.3",
|
|
46
|
-
"vitest": "^
|
|
47
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
45
|
+
"vitest": "^5.1.0",
|
|
46
|
+
"@theholocron/cli": "5.0.0-alpha.430",
|
|
47
|
+
"@theholocron/eslint-config": "^8.9.0-alpha.17",
|
|
48
|
+
"@theholocron/tsconfig": "^8.9.0-alpha.17",
|
|
49
|
+
"@theholocron/tsdown-config": "^8.9.0-alpha.17",
|
|
50
|
+
"@theholocron/vitest-config": "^8.9.0-alpha.17"
|
|
48
51
|
},
|
|
49
52
|
"peerDependencies": {
|
|
50
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
53
|
+
"@theholocron/cli": "5.0.0-alpha.430"
|
|
51
54
|
},
|
|
52
55
|
"engines": {
|
|
53
56
|
"node": ">=22"
|
|
@@ -56,10 +59,10 @@
|
|
|
56
59
|
"access": "public"
|
|
57
60
|
},
|
|
58
61
|
"scripts": {
|
|
59
|
-
"
|
|
60
|
-
"
|
|
62
|
+
"build": "tsdown",
|
|
63
|
+
"lint:code": "eslint .",
|
|
64
|
+
"test:unit": "vitest run --coverage",
|
|
61
65
|
"test:watch": "vitest",
|
|
62
|
-
"
|
|
63
|
-
"verification.unitTests": "holocron run verification.unitTests --"
|
|
66
|
+
"typecheck": "tsc --noEmit"
|
|
64
67
|
}
|
|
65
68
|
}
|