@marlinjai/clearify 1.18.0 → 1.20.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.
@@ -282,7 +282,7 @@ ${hasInternal ? `
282
282
  `);
283
283
  const secretMode = options.secretMode ?? "prompt";
284
284
  if (options.hub) {
285
- const { registerWithHub } = await import("./hub-register-VJBXLOKV.js");
285
+ const { registerWithHub } = await import("./hub-register-7NF5BP3E.js");
286
286
  await registerWithHub({
287
287
  projectName: detectProjectName(),
288
288
  hubToken: options.hubToken,
@@ -298,7 +298,7 @@ ${hasInternal ? `
298
298
  });
299
299
  rl.close();
300
300
  if (answer.toLowerCase() === "y") {
301
- const { registerWithHub } = await import("./hub-register-VJBXLOKV.js");
301
+ const { registerWithHub } = await import("./hub-register-7NF5BP3E.js");
302
302
  await registerWithHub({
303
303
  projectName: detectProjectName(),
304
304
  hubToken: options.hubToken,
@@ -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",
@@ -236,9 +310,15 @@ jobs:
236
310
  }
237
311
  async function registerWithHub(options) {
238
312
  const clientId = process.env.CLEARIFY_GITHUB_CLIENT_ID;
239
- if (!clientId) {
240
- console.error("\n Error: CLEARIFY_GITHUB_CLIENT_ID environment variable is not set.");
241
- console.error(" Set it to your GitHub OAuth App client ID and try again.\n");
313
+ if (!clientId && !options.hubToken) {
314
+ console.error("\n Error: no GitHub credentials available for hub onboarding.");
315
+ console.error(" Clearify needs one of these to write the hub registry entry:\n");
316
+ console.error(" 1) Set CLEARIFY_GITHUB_CLIENT_ID to a GitHub OAuth App client ID");
317
+ console.error(" (create one at https://github.com/settings/applications/new,");
318
+ console.error(' scope "repo"), then re-run this command.');
319
+ console.error(" 2) Pass --hub-token <pat> to use a Personal Access Token directly");
320
+ console.error(' (fine-grained PAT with "Contents: Read and Write" on the hub repo).');
321
+ console.error("");
242
322
  process.exit(1);
243
323
  }
244
324
  const projectName = options.projectName ?? detectProjectName();
@@ -254,8 +334,14 @@ async function registerWithHub(options) {
254
334
  const hub = await promptHubRepo();
255
335
  console.log(` Hub: ${hub.owner}/${hub.repo}
256
336
  `);
257
- console.log(" Starting GitHub authentication...");
258
- const token = await githubDeviceAuth(clientId);
337
+ let token;
338
+ if (options.hubToken) {
339
+ console.log(" Using provided --hub-token for GitHub API calls.");
340
+ token = options.hubToken;
341
+ } else {
342
+ console.log(" Starting GitHub authentication...");
343
+ token = await githubDeviceAuth(clientId);
344
+ }
259
345
  console.log(" Updating hub registry...");
260
346
  await updateHubRegistry(token, hub, {
261
347
  name: projectName,
@@ -267,14 +353,46 @@ async function registerWithHub(options) {
267
353
  console.log(" Generating local files...");
268
354
  generateConfig(projectName, options.hubProject);
269
355
  generateWorkflow(hub);
356
+ if (options.secretMode !== "skip") {
357
+ const mode = options.secretMode === "prompt" || options.secretMode === void 0 ? await promptSecretMode() : options.secretMode;
358
+ if (mode === "auto") {
359
+ if (!options.rotateSecret) {
360
+ const exists = await secretExists(
361
+ token,
362
+ currentRepo.owner,
363
+ currentRepo.repo,
364
+ "HUB_DISPATCH_TOKEN"
365
+ );
366
+ if (exists) {
367
+ console.log(
368
+ " HUB_DISPATCH_TOKEN already exists on this sub-repo; pass --rotate-secret to overwrite"
369
+ );
370
+ printSuccessFooter(currentRepo);
371
+ return;
372
+ }
373
+ }
374
+ const secretValue = options.secretPat ?? options.hubToken ?? await promptHiddenInput("Paste the PAT to store as HUB_DISPATCH_TOKEN: ");
375
+ try {
376
+ await storeDispatchToken(token, currentRepo.owner, currentRepo.repo, secretValue);
377
+ console.log(" HUB_DISPATCH_TOKEN written to sub-repo");
378
+ } catch (err) {
379
+ console.warn(` Could not write secret: ${err.message}`);
380
+ printManualSecretInstructions(currentRepo, hub);
381
+ }
382
+ } else if (mode === "manual") {
383
+ printManualSecretInstructions(currentRepo, hub);
384
+ }
385
+ }
386
+ printSuccessFooter(currentRepo);
387
+ }
388
+ function printSuccessFooter(currentRepo) {
270
389
  console.log(`
271
390
  Hub registration complete!
272
391
 
273
392
  Next steps:
274
393
  1. Review clearify.config.ts and update the hubProject description
275
- 2. Add "${currentRepo.repo}" to the repos list in infra/deployments/hub-dispatch/github.tf
276
- 3. Run: cd infra/deployments/hub-dispatch && ../../scripts/tfrun.sh apply
277
- 4. Commit the generated files and push to main
394
+ 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
395
+ 3. Commit the generated files and push to main
278
396
  `);
279
397
  }
280
398
  export {
@@ -11,7 +11,7 @@ import {
11
11
  import "../chunk-B2Q23JW3.js";
12
12
  import {
13
13
  init
14
- } from "../chunk-7Z2GR22P.js";
14
+ } from "../chunk-JZZ2BNIL.js";
15
15
  import {
16
16
  checkFrontmatter,
17
17
  checkLinks
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  init
3
- } from "../chunk-7Z2GR22P.js";
3
+ } from "../chunk-JZZ2BNIL.js";
4
4
  export {
5
5
  init
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marlinjai/clearify",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "description": "An open-source documentation site generator. Turn markdown into beautiful docs.",
5
5
  "type": "module",
6
6
  "bin": {