@marlinjai/clearify 1.17.0 → 1.19.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/dist/node/{chunk-SVUDQMRW.js → chunk-HVHCUAVU.js} +17 -4
- package/dist/node/cli/index.js +24 -4
- package/dist/node/core/config.d.ts +1 -1
- package/dist/node/{hub-register-VJBXLOKV.js → hub-register-M7C3G3FE.js} +109 -3
- package/dist/node/node/index.js +1 -1
- package/dist/node/node/init.d.ts +16 -0
- package/dist/node/node/init.js +1 -1
- package/package.json +1 -1
|
@@ -280,9 +280,16 @@ ${hasInternal ? `
|
|
|
280
280
|
npx clearify dev Start the dev server
|
|
281
281
|
npx clearify build Build for production
|
|
282
282
|
`);
|
|
283
|
+
const secretMode = options.secretMode ?? "prompt";
|
|
283
284
|
if (options.hub) {
|
|
284
|
-
const { registerWithHub } = await import("./hub-register-
|
|
285
|
-
await registerWithHub({
|
|
285
|
+
const { registerWithHub } = await import("./hub-register-M7C3G3FE.js");
|
|
286
|
+
await registerWithHub({
|
|
287
|
+
projectName: detectProjectName(),
|
|
288
|
+
hubToken: options.hubToken,
|
|
289
|
+
secretMode,
|
|
290
|
+
secretPat: options.secretPat,
|
|
291
|
+
rotateSecret: options.rotateSecret
|
|
292
|
+
});
|
|
286
293
|
} else {
|
|
287
294
|
const { createInterface } = await import("readline");
|
|
288
295
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -291,8 +298,14 @@ ${hasInternal ? `
|
|
|
291
298
|
});
|
|
292
299
|
rl.close();
|
|
293
300
|
if (answer.toLowerCase() === "y") {
|
|
294
|
-
const { registerWithHub } = await import("./hub-register-
|
|
295
|
-
await registerWithHub({
|
|
301
|
+
const { registerWithHub } = await import("./hub-register-M7C3G3FE.js");
|
|
302
|
+
await registerWithHub({
|
|
303
|
+
projectName: detectProjectName(),
|
|
304
|
+
hubToken: options.hubToken,
|
|
305
|
+
secretMode,
|
|
306
|
+
secretPat: options.secretPat,
|
|
307
|
+
rotateSecret: options.rotateSecret
|
|
308
|
+
});
|
|
296
309
|
}
|
|
297
310
|
}
|
|
298
311
|
}
|
package/dist/node/cli/index.js
CHANGED
|
@@ -34,10 +34,30 @@ cli.command("build", "Build static documentation site").action(async () => {
|
|
|
34
34
|
const { build } = await import("../node/index.js");
|
|
35
35
|
await build();
|
|
36
36
|
});
|
|
37
|
-
cli.command("init", "Scaffold a docs folder and optionally register with a hub").option("--no-internal", "Skip creating the internal docs section").option("--no-claude-rules", "Skip creating .claude/rules/clearify-docs.md").option("--hub", "Register this project with a documentation hub").
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
cli.command("init", "Scaffold a docs folder and optionally register with a hub").option("--no-internal", "Skip creating the internal docs section").option("--no-claude-rules", "Skip creating .claude/rules/clearify-docs.md").option("--hub", "Register this project with a documentation hub").option(
|
|
38
|
+
"--hub-token <pat>",
|
|
39
|
+
"GitHub PAT to use instead of OAuth device flow (for environments without CLEARIFY_GITHUB_CLIENT_ID set)"
|
|
40
|
+
).option(
|
|
41
|
+
"--secret-mode <mode>",
|
|
42
|
+
"How to provision HUB_DISPATCH_TOKEN: prompt (interactive, default), auto (encrypt + PUT via Secrets API), manual (print UI instructions), skip (leave it to Terraform)",
|
|
43
|
+
{ default: "prompt" }
|
|
44
|
+
).option(
|
|
45
|
+
"--secret-pat <pat>",
|
|
46
|
+
"PAT value to store as HUB_DISPATCH_TOKEN. Visible in `ps`; prefer --secret-mode=prompt for hidden input"
|
|
47
|
+
).option("--rotate-secret", "Overwrite HUB_DISPATCH_TOKEN even if it already exists on the sub-repo").action(
|
|
48
|
+
async (options) => {
|
|
49
|
+
const { init } = await import("../node/index.js");
|
|
50
|
+
await init({
|
|
51
|
+
noInternal: options.noInternal,
|
|
52
|
+
noClaudeRules: options.noClaudeRules,
|
|
53
|
+
hub: options.hub,
|
|
54
|
+
hubToken: options.hubToken,
|
|
55
|
+
secretMode: options.secretMode,
|
|
56
|
+
secretPat: options.secretPat,
|
|
57
|
+
rotateSecret: options.rotateSecret
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
);
|
|
41
61
|
cli.command("check", "Check for broken internal links and frontmatter issues").option("--frontmatter", "Only run frontmatter validation (skip broken link detection)").action(async (options) => {
|
|
42
62
|
const { checkLinks } = await import("../node/check.js");
|
|
43
63
|
await checkLinks({ frontmatterOnly: options.frontmatter });
|
|
@@ -9,9 +9,9 @@ declare const ClearifyDataSchema: z.ZodObject<{
|
|
|
9
9
|
theme: z.ZodOptional<z.ZodObject<{
|
|
10
10
|
primaryColor: z.ZodOptional<z.ZodString>;
|
|
11
11
|
mode: z.ZodOptional<z.ZodEnum<{
|
|
12
|
+
auto: "auto";
|
|
12
13
|
light: "light";
|
|
13
14
|
dark: "dark";
|
|
14
|
-
auto: "auto";
|
|
15
15
|
}>>;
|
|
16
16
|
}, z.core.$strip>>;
|
|
17
17
|
logo: z.ZodOptional<z.ZodObject<{
|
|
@@ -39,6 +39,80 @@ async function ghApiFetch(url, options = {}) {
|
|
|
39
39
|
const data = await res.json().catch(() => null);
|
|
40
40
|
return { status: res.status, data };
|
|
41
41
|
}
|
|
42
|
+
async function encryptSecret(plaintext, publicKey) {
|
|
43
|
+
const sodium = (await import("libsodium-wrappers")).default;
|
|
44
|
+
await sodium.ready;
|
|
45
|
+
const keyBytes = sodium.from_base64(publicKey, sodium.base64_variants.ORIGINAL);
|
|
46
|
+
const messageBytes = sodium.from_string(plaintext);
|
|
47
|
+
const sealed = sodium.crypto_box_seal(messageBytes, keyBytes);
|
|
48
|
+
return sodium.to_base64(sealed, sodium.base64_variants.ORIGINAL);
|
|
49
|
+
}
|
|
50
|
+
async function getRepoPublicKey(token, owner, repo) {
|
|
51
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/actions/secrets/public-key`;
|
|
52
|
+
const res = await ghApiFetch(url, {
|
|
53
|
+
headers: { Authorization: `token ${token}` }
|
|
54
|
+
});
|
|
55
|
+
if (res.status !== 200 || !res.data?.key || !res.data?.key_id) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Failed to fetch public key for ${owner}/${repo}: ${res.status} ${JSON.stringify(res.data)}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return { key_id: res.data.key_id, key: res.data.key };
|
|
61
|
+
}
|
|
62
|
+
async function storeDispatchToken(token, owner, repo, secretValue) {
|
|
63
|
+
const { key_id, key } = await getRepoPublicKey(token, owner, repo);
|
|
64
|
+
const encrypted_value = await encryptSecret(secretValue, key);
|
|
65
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/actions/secrets/HUB_DISPATCH_TOKEN`;
|
|
66
|
+
const res = await ghApiFetch(url, {
|
|
67
|
+
method: "PUT",
|
|
68
|
+
headers: { Authorization: `token ${token}` },
|
|
69
|
+
body: JSON.stringify({ encrypted_value, key_id })
|
|
70
|
+
});
|
|
71
|
+
if (res.status !== 201 && res.status !== 204) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Failed to write HUB_DISPATCH_TOKEN on ${owner}/${repo}: ${res.status} ${JSON.stringify(res.data)}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function secretExists(token, owner, repo, name) {
|
|
78
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/actions/secrets/${name}`;
|
|
79
|
+
const res = await ghApiFetch(url, { headers: { Authorization: `token ${token}` } });
|
|
80
|
+
return res.status === 200;
|
|
81
|
+
}
|
|
82
|
+
async function promptSecretMode() {
|
|
83
|
+
const { createInterface } = await import("readline");
|
|
84
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
85
|
+
console.log("\n How would you like to provision HUB_DISPATCH_TOKEN on this sub-repo?");
|
|
86
|
+
console.log(" 1) auto paste a PAT now, Clearify encrypts and PUTs it via the Secrets API");
|
|
87
|
+
console.log(" 2) manual print instructions for the GitHub UI (I will paste it myself)");
|
|
88
|
+
console.log(" 3) skip leave it alone (I manage it via Terraform)\n");
|
|
89
|
+
const answer = await new Promise((res) => {
|
|
90
|
+
rl.question(" Choose [1/2/3] (default: 2): ", res);
|
|
91
|
+
});
|
|
92
|
+
rl.close();
|
|
93
|
+
const trimmed = answer.trim();
|
|
94
|
+
if (trimmed === "1" || trimmed.toLowerCase() === "auto") return "auto";
|
|
95
|
+
if (trimmed === "3" || trimmed.toLowerCase() === "skip") return "skip";
|
|
96
|
+
return "manual";
|
|
97
|
+
}
|
|
98
|
+
async function promptHiddenInput(prompt) {
|
|
99
|
+
const password = (await import("@inquirer/password")).default;
|
|
100
|
+
const value = await password({ message: prompt, mask: "*" });
|
|
101
|
+
if (!value) {
|
|
102
|
+
throw new Error("Empty input; aborting.");
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
function printManualSecretInstructions(subRepo, hub) {
|
|
107
|
+
const url = `https://github.com/${subRepo.owner}/${subRepo.repo}/settings/secrets/actions/new`;
|
|
108
|
+
console.log("\n Manual secret provisioning");
|
|
109
|
+
console.log(` 1. Open: ${url}`);
|
|
110
|
+
console.log(" 2. Secret name: HUB_DISPATCH_TOKEN");
|
|
111
|
+
console.log(
|
|
112
|
+
` 3. Value: a fine-grained PAT with "Contents: Read and Write" scope on ${hub.owner}/${hub.repo}, 90-day expiry.`
|
|
113
|
+
);
|
|
114
|
+
console.log(" 4. Save and push a doc change to verify the dispatch fires.\n");
|
|
115
|
+
}
|
|
42
116
|
async function githubDeviceAuth(clientId) {
|
|
43
117
|
const codeRes = await fetch("https://github.com/login/device/code", {
|
|
44
118
|
method: "POST",
|
|
@@ -267,14 +341,46 @@ async function registerWithHub(options) {
|
|
|
267
341
|
console.log(" Generating local files...");
|
|
268
342
|
generateConfig(projectName, options.hubProject);
|
|
269
343
|
generateWorkflow(hub);
|
|
344
|
+
if (options.secretMode !== "skip") {
|
|
345
|
+
const mode = options.secretMode === "prompt" || options.secretMode === void 0 ? await promptSecretMode() : options.secretMode;
|
|
346
|
+
if (mode === "auto") {
|
|
347
|
+
if (!options.rotateSecret) {
|
|
348
|
+
const exists = await secretExists(
|
|
349
|
+
token,
|
|
350
|
+
currentRepo.owner,
|
|
351
|
+
currentRepo.repo,
|
|
352
|
+
"HUB_DISPATCH_TOKEN"
|
|
353
|
+
);
|
|
354
|
+
if (exists) {
|
|
355
|
+
console.log(
|
|
356
|
+
" HUB_DISPATCH_TOKEN already exists on this sub-repo; pass --rotate-secret to overwrite"
|
|
357
|
+
);
|
|
358
|
+
printSuccessFooter(currentRepo);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const secretValue = options.secretPat ?? options.hubToken ?? await promptHiddenInput("Paste the PAT to store as HUB_DISPATCH_TOKEN: ");
|
|
363
|
+
try {
|
|
364
|
+
await storeDispatchToken(token, currentRepo.owner, currentRepo.repo, secretValue);
|
|
365
|
+
console.log(" HUB_DISPATCH_TOKEN written to sub-repo");
|
|
366
|
+
} catch (err) {
|
|
367
|
+
console.warn(` Could not write secret: ${err.message}`);
|
|
368
|
+
printManualSecretInstructions(currentRepo, hub);
|
|
369
|
+
}
|
|
370
|
+
} else if (mode === "manual") {
|
|
371
|
+
printManualSecretInstructions(currentRepo, hub);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
printSuccessFooter(currentRepo);
|
|
375
|
+
}
|
|
376
|
+
function printSuccessFooter(currentRepo) {
|
|
270
377
|
console.log(`
|
|
271
378
|
Hub registration complete!
|
|
272
379
|
|
|
273
380
|
Next steps:
|
|
274
381
|
1. Review clearify.config.ts and update the hubProject description
|
|
275
|
-
2.
|
|
276
|
-
3.
|
|
277
|
-
4. Commit the generated files and push to main
|
|
382
|
+
2. If you manage HUB_DISPATCH_TOKEN via Terraform, add "${currentRepo.repo}" to the repos list in infra/deployments/hub-dispatch/github.tf and apply
|
|
383
|
+
3. Commit the generated files and push to main
|
|
278
384
|
`);
|
|
279
385
|
}
|
|
280
386
|
export {
|
package/dist/node/node/index.js
CHANGED
package/dist/node/node/init.d.ts
CHANGED
|
@@ -2,6 +2,22 @@ interface InitOptions {
|
|
|
2
2
|
noInternal?: boolean;
|
|
3
3
|
noClaudeRules?: boolean;
|
|
4
4
|
hub?: boolean;
|
|
5
|
+
/** PAT to use for GitHub API calls when CLEARIFY_GITHUB_CLIENT_ID is unset. */
|
|
6
|
+
hubToken?: string;
|
|
7
|
+
/**
|
|
8
|
+
* How to provision HUB_DISPATCH_TOKEN on the sub-repo. Defaults to 'prompt'
|
|
9
|
+
* during hub onboarding (interactive menu). Non-interactive modes: 'auto'
|
|
10
|
+
* (encrypt + PUT via Secrets API), 'manual' (print UI instructions), 'skip'
|
|
11
|
+
* (leave it to Terraform).
|
|
12
|
+
*/
|
|
13
|
+
secretMode?: 'prompt' | 'auto' | 'manual' | 'skip';
|
|
14
|
+
/**
|
|
15
|
+
* The PAT value to store as HUB_DISPATCH_TOKEN. Visible in `ps` when passed
|
|
16
|
+
* as a flag; prefer --secret-mode=prompt for the hidden input path.
|
|
17
|
+
*/
|
|
18
|
+
secretPat?: string;
|
|
19
|
+
/** Overwrite HUB_DISPATCH_TOKEN even if it already exists on the sub-repo. */
|
|
20
|
+
rotateSecret?: boolean;
|
|
5
21
|
}
|
|
6
22
|
declare function init(options?: InitOptions): Promise<void>;
|
|
7
23
|
|
package/dist/node/node/init.js
CHANGED