@marlinjai/clearify 1.13.0 → 1.14.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.
@@ -1,6 +1,21 @@
1
1
  // src/node/init.ts
2
- import { resolve } from "path";
3
- import { existsSync, mkdirSync, writeFileSync } from "fs";
2
+ import { resolve, basename } from "path";
3
+ import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
4
+ function detectProjectName() {
5
+ const cwd = process.cwd();
6
+ const pkgPath = resolve(cwd, "package.json");
7
+ if (existsSync(pkgPath)) {
8
+ try {
9
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
10
+ if (pkg.name) {
11
+ const raw = pkg.name.replace(/^@[^/]+\//, "");
12
+ return raw.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
13
+ }
14
+ } catch {
15
+ }
16
+ }
17
+ return basename(cwd).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
18
+ }
4
19
  async function init(options = {}) {
5
20
  const cwd = process.cwd();
6
21
  const publicDir = resolve(cwd, "docs/public");
@@ -265,6 +280,21 @@ ${hasInternal ? `
265
280
  npx clearify dev Start the dev server
266
281
  npx clearify build Build for production
267
282
  `);
283
+ if (options.hub) {
284
+ const { registerWithHub } = await import("./hub-register-TCVGPF7N.js");
285
+ await registerWithHub({ projectName: detectProjectName() });
286
+ } else {
287
+ const { createInterface } = await import("readline");
288
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
289
+ const answer = await new Promise((resolve2) => {
290
+ rl.question(" Register this project with a documentation hub? [y/N] ", resolve2);
291
+ });
292
+ rl.close();
293
+ if (answer.toLowerCase() === "y") {
294
+ const { registerWithHub } = await import("./hub-register-TCVGPF7N.js");
295
+ await registerWithHub({ projectName: detectProjectName() });
296
+ }
297
+ }
268
298
  }
269
299
 
270
300
  export {
@@ -34,9 +34,9 @@ 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").option("--no-internal", "Skip creating the internal docs section").option("--no-claude-rules", "Skip creating .claude/rules/clearify-docs.md").action(async (options) => {
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").action(async (options) => {
38
38
  const { init } = await import("../node/index.js");
39
- await init({ noInternal: options.noInternal, noClaudeRules: options.noClaudeRules });
39
+ await init({ noInternal: options.noInternal, noClaudeRules: options.noClaudeRules, hub: options.hub });
40
40
  });
41
41
  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
42
  const { checkLinks } = await import("../node/check.js");
@@ -0,0 +1,282 @@
1
+ // src/node/hub-register.ts
2
+ import { resolve, basename } from "path";
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
+ import { execSync } from "child_process";
5
+ function detectProjectName() {
6
+ const cwd = process.cwd();
7
+ const pkgPath = resolve(cwd, "package.json");
8
+ if (existsSync(pkgPath)) {
9
+ try {
10
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
11
+ if (pkg.name) {
12
+ const raw = pkg.name.replace(/^@[^/]+\//, "");
13
+ return raw.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
14
+ }
15
+ } catch {
16
+ }
17
+ }
18
+ return basename(cwd).replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
19
+ }
20
+ function getGitRemoteInfo() {
21
+ try {
22
+ const url = execSync("git remote get-url origin", { encoding: "utf-8" }).trim();
23
+ const match = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
24
+ if (match) return { owner: match[1], repo: match[2] };
25
+ } catch {
26
+ }
27
+ return null;
28
+ }
29
+ async function ghApiFetch(url, options = {}) {
30
+ const res = await fetch(url, {
31
+ method: options.method ?? "GET",
32
+ headers: {
33
+ Accept: "application/vnd.github.v3+json",
34
+ "Content-Type": "application/json",
35
+ ...options.headers
36
+ },
37
+ body: options.body
38
+ });
39
+ const data = await res.json().catch(() => null);
40
+ return { status: res.status, data };
41
+ }
42
+ async function githubDeviceAuth(clientId) {
43
+ const codeRes = await fetch("https://github.com/login/device/code", {
44
+ method: "POST",
45
+ headers: {
46
+ Accept: "application/json",
47
+ "Content-Type": "application/json"
48
+ },
49
+ body: JSON.stringify({ client_id: clientId, scope: "repo" })
50
+ });
51
+ const codeData = await codeRes.json();
52
+ console.log(`
53
+ Open this URL in your browser: ${codeData.verification_uri}`);
54
+ console.log(` Enter code: ${codeData.user_code}
55
+ `);
56
+ try {
57
+ const platform = process.platform;
58
+ if (platform === "darwin") {
59
+ execSync(`open "${codeData.verification_uri}"`);
60
+ } else if (platform === "linux") {
61
+ execSync(`xdg-open "${codeData.verification_uri}"`);
62
+ } else if (platform === "win32") {
63
+ execSync(`start "${codeData.verification_uri}"`);
64
+ }
65
+ } catch {
66
+ }
67
+ const interval = (codeData.interval || 5) * 1e3;
68
+ const deadline = Date.now() + codeData.expires_in * 1e3;
69
+ while (Date.now() < deadline) {
70
+ await new Promise((r) => setTimeout(r, interval));
71
+ const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
72
+ method: "POST",
73
+ headers: {
74
+ Accept: "application/json",
75
+ "Content-Type": "application/json"
76
+ },
77
+ body: JSON.stringify({
78
+ client_id: clientId,
79
+ device_code: codeData.device_code,
80
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
81
+ })
82
+ });
83
+ const tokenData = await tokenRes.json();
84
+ if (tokenData.access_token) {
85
+ console.log(" GitHub authentication successful!\n");
86
+ return tokenData.access_token;
87
+ }
88
+ if (tokenData.error === "authorization_pending") {
89
+ continue;
90
+ }
91
+ if (tokenData.error === "slow_down") {
92
+ await new Promise((r) => setTimeout(r, 5e3));
93
+ continue;
94
+ }
95
+ if (tokenData.error === "expired_token") {
96
+ throw new Error("Device code expired. Please try again.");
97
+ }
98
+ if (tokenData.error === "access_denied") {
99
+ throw new Error("Authorization denied by user.");
100
+ }
101
+ throw new Error(`OAuth error: ${tokenData.error} \u2014 ${tokenData.error_description}`);
102
+ }
103
+ throw new Error("Device code expired. Please try again.");
104
+ }
105
+ async function promptHubRepo() {
106
+ const { createInterface } = await import("readline");
107
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
108
+ const answer = await new Promise((resolve2) => {
109
+ rl.question(" Hub repository (owner/repo): ", resolve2);
110
+ });
111
+ rl.close();
112
+ const parts = answer.trim().split("/");
113
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
114
+ throw new Error(`Invalid repository format: "${answer}". Expected "owner/repo".`);
115
+ }
116
+ return { owner: parts[0], repo: parts[1] };
117
+ }
118
+ async function updateHubRegistry(token, hub, project) {
119
+ const authHeaders = { Authorization: `token ${token}` };
120
+ const apiBase = "https://api.github.com";
121
+ const fileUrl = `${apiBase}/repos/${hub.owner}/${hub.repo}/contents/clearify.data.json`;
122
+ const getRes = await ghApiFetch(fileUrl, { headers: authHeaders });
123
+ let currentData = { hub: { projects: [] } };
124
+ let fileSha;
125
+ if (getRes.status === 200 && getRes.data?.content) {
126
+ const decoded = Buffer.from(getRes.data.content, "base64").toString("utf-8");
127
+ currentData = JSON.parse(decoded);
128
+ fileSha = getRes.data.sha;
129
+ if (!currentData.hub) currentData.hub = { projects: [] };
130
+ if (!currentData.hub.projects) currentData.hub.projects = [];
131
+ } else if (getRes.status !== 404) {
132
+ throw new Error(`Failed to read hub registry: ${getRes.status} ${JSON.stringify(getRes.data)}`);
133
+ }
134
+ const existingIdx = currentData.hub.projects.findIndex(
135
+ (p) => p.name === project.name
136
+ );
137
+ const entry = {
138
+ name: project.name,
139
+ description: project.description,
140
+ status: "active",
141
+ mode: "embed",
142
+ git: {
143
+ repo: project.gitRepoUrl,
144
+ ref: project.gitRef,
145
+ path: project.docsPath
146
+ }
147
+ };
148
+ if (existingIdx >= 0) {
149
+ currentData.hub.projects[existingIdx] = entry;
150
+ console.log(` Updated existing project "${project.name}" in hub registry`);
151
+ } else {
152
+ currentData.hub.projects.push(entry);
153
+ console.log(` Added project "${project.name}" to hub registry`);
154
+ }
155
+ const updatedContent = Buffer.from(
156
+ JSON.stringify(currentData, null, 2) + "\n"
157
+ ).toString("base64");
158
+ const putBody = {
159
+ message: `docs: register ${project.name} in hub`,
160
+ content: updatedContent
161
+ };
162
+ if (fileSha) putBody.sha = fileSha;
163
+ const putRes = await ghApiFetch(fileUrl, {
164
+ method: "PUT",
165
+ headers: authHeaders,
166
+ body: JSON.stringify(putBody)
167
+ });
168
+ if (putRes.status !== 200 && putRes.status !== 201) {
169
+ throw new Error(
170
+ `Failed to update hub registry: ${putRes.status} ${JSON.stringify(putRes.data)}`
171
+ );
172
+ }
173
+ console.log(" Hub registry updated successfully");
174
+ }
175
+ function generateConfig(projectName, hubProject) {
176
+ const cwd = process.cwd();
177
+ const configPath = resolve(cwd, "clearify.config.ts");
178
+ if (existsSync(configPath)) {
179
+ console.log(" clearify.config.ts already exists \u2014 skipping config generation");
180
+ console.log(" Add a hubProject block manually if needed");
181
+ return;
182
+ }
183
+ const description = hubProject?.description ?? "Project documentation";
184
+ const config = `import { defineConfig } from '@marlinjai/clearify';
185
+
186
+ export default defineConfig({
187
+ name: '${projectName}',
188
+ sections: [
189
+ { label: 'Docs', docsDir: './docs/public' },
190
+ ],
191
+ theme: {
192
+ primaryColor: '#3B82F6',
193
+ mode: 'auto',
194
+ },
195
+ hubProject: {
196
+ description: '${description}',
197
+ status: 'active',
198
+ },
199
+ });
200
+ `;
201
+ writeFileSync(configPath, config);
202
+ console.log(" Created clearify.config.ts with hubProject config");
203
+ }
204
+ function generateWorkflow(hub) {
205
+ const cwd = process.cwd();
206
+ const workflowDir = resolve(cwd, ".github/workflows");
207
+ const workflowPath = resolve(workflowDir, "docs-trigger.yml");
208
+ if (existsSync(workflowPath)) {
209
+ console.log(" .github/workflows/docs-trigger.yml already exists \u2014 skipping");
210
+ return;
211
+ }
212
+ mkdirSync(workflowDir, { recursive: true });
213
+ const workflow = `name: Docs Trigger
214
+
215
+ on:
216
+ push:
217
+ branches: [main]
218
+ paths:
219
+ - 'docs/**'
220
+ - 'clearify.config.ts'
221
+
222
+ jobs:
223
+ notify-hub:
224
+ runs-on: ubuntu-latest
225
+ steps:
226
+ - name: Trigger hub rebuild
227
+ run: |
228
+ curl -X POST \\
229
+ -H "Authorization: token \${{ secrets.HUB_DISPATCH_TOKEN }}" \\
230
+ -H "Accept: application/vnd.github.v3+json" \\
231
+ https://api.github.com/repos/${hub.owner}/${hub.repo}/dispatches \\
232
+ -d '{"event_type": "docs-update"}'
233
+ `;
234
+ writeFileSync(workflowPath, workflow);
235
+ console.log(" Created .github/workflows/docs-trigger.yml");
236
+ }
237
+ async function registerWithHub(options) {
238
+ 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");
242
+ process.exit(1);
243
+ }
244
+ const projectName = options.projectName ?? detectProjectName();
245
+ const currentRepo = getGitRemoteInfo();
246
+ if (!currentRepo) {
247
+ console.error("\n Error: Could not detect git remote origin.");
248
+ console.error(" Make sure this is a git repository with a GitHub remote.\n");
249
+ process.exit(1);
250
+ }
251
+ console.log(`
252
+ Registering "${projectName}" with a documentation hub
253
+ `);
254
+ const hub = await promptHubRepo();
255
+ console.log(` Hub: ${hub.owner}/${hub.repo}
256
+ `);
257
+ console.log(" Starting GitHub authentication...");
258
+ const token = await githubDeviceAuth(clientId);
259
+ console.log(" Updating hub registry...");
260
+ await updateHubRegistry(token, hub, {
261
+ name: projectName,
262
+ description: options.hubProject?.description ?? "Project documentation",
263
+ gitRepoUrl: `https://github.com/${currentRepo.owner}/${currentRepo.repo}.git`,
264
+ gitRef: "main",
265
+ docsPath: "docs/public"
266
+ });
267
+ console.log(" Generating local files...");
268
+ generateConfig(projectName, options.hubProject);
269
+ generateWorkflow(hub);
270
+ console.log(`
271
+ Hub registration complete!
272
+
273
+ Next steps:
274
+ 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
278
+ `);
279
+ }
280
+ export {
281
+ registerWithHub
282
+ };
@@ -11,7 +11,7 @@ import {
11
11
  import "../chunk-B2Q23JW3.js";
12
12
  import {
13
13
  init
14
- } from "../chunk-EKN6DWXT.js";
14
+ } from "../chunk-ZNCC6M7V.js";
15
15
  import {
16
16
  checkFrontmatter,
17
17
  checkLinks
@@ -1,6 +1,7 @@
1
1
  interface InitOptions {
2
2
  noInternal?: boolean;
3
3
  noClaudeRules?: boolean;
4
+ hub?: boolean;
4
5
  }
5
6
  declare function init(options?: InitOptions): Promise<void>;
6
7
 
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  init
3
- } from "../chunk-EKN6DWXT.js";
3
+ } from "../chunk-ZNCC6M7V.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.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "An open-source documentation site generator. Turn markdown into beautiful docs.",
5
5
  "type": "module",
6
6
  "bin": {