@marlinjai/clearify 1.20.0 → 2.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.
@@ -116,9 +116,6 @@
116
116
  "description": {
117
117
  "type": "string"
118
118
  },
119
- "href": {
120
- "type": "string"
121
- },
122
119
  "repo": {
123
120
  "type": "string"
124
121
  },
@@ -144,64 +141,165 @@
144
141
  "group": {
145
142
  "type": "string"
146
143
  },
147
- "mode": {
148
- "type": "string",
149
- "enum": [
150
- "link",
151
- "embed",
152
- "inject"
153
- ]
154
- },
155
- "git": {
156
- "type": "object",
157
- "properties": {
158
- "repo": {
159
- "type": "string"
144
+ "source": {
145
+ "oneOf": [
146
+ {
147
+ "type": "object",
148
+ "properties": {
149
+ "kind": {
150
+ "type": "string",
151
+ "const": "none"
152
+ }
153
+ },
154
+ "required": [
155
+ "kind"
156
+ ],
157
+ "additionalProperties": false
160
158
  },
161
- "ref": {
162
- "type": "string"
159
+ {
160
+ "type": "object",
161
+ "properties": {
162
+ "kind": {
163
+ "type": "string",
164
+ "const": "git"
165
+ },
166
+ "repo": {
167
+ "type": "string"
168
+ },
169
+ "ref": {
170
+ "type": "string"
171
+ },
172
+ "path": {
173
+ "type": "string"
174
+ },
175
+ "sparse": {
176
+ "type": "boolean"
177
+ }
178
+ },
179
+ "required": [
180
+ "kind",
181
+ "repo"
182
+ ],
183
+ "additionalProperties": false
163
184
  },
164
- "path": {
165
- "type": "string"
185
+ {
186
+ "type": "object",
187
+ "properties": {
188
+ "kind": {
189
+ "type": "string",
190
+ "const": "url"
191
+ },
192
+ "url": {
193
+ "type": "string"
194
+ }
195
+ },
196
+ "required": [
197
+ "kind",
198
+ "url"
199
+ ],
200
+ "additionalProperties": false
166
201
  },
167
- "sparse": {
168
- "type": "boolean"
202
+ {
203
+ "type": "object",
204
+ "properties": {
205
+ "kind": {
206
+ "type": "string",
207
+ "const": "inline"
208
+ },
209
+ "markdown": {
210
+ "type": "string"
211
+ }
212
+ },
213
+ "required": [
214
+ "kind",
215
+ "markdown"
216
+ ],
217
+ "additionalProperties": false
169
218
  }
170
- },
171
- "required": [
172
- "repo"
173
- ],
174
- "additionalProperties": false
219
+ ]
175
220
  },
176
- "embedSections": {
177
- "anyOf": [
221
+ "placement": {
222
+ "oneOf": [
178
223
  {
179
- "type": "string",
180
- "const": "all"
224
+ "type": "object",
225
+ "properties": {
226
+ "kind": {
227
+ "type": "string",
228
+ "const": "card"
229
+ },
230
+ "href": {
231
+ "type": "string"
232
+ }
233
+ },
234
+ "required": [
235
+ "kind",
236
+ "href"
237
+ ],
238
+ "additionalProperties": false
181
239
  },
182
240
  {
183
- "type": "string",
184
- "const": "public"
241
+ "type": "object",
242
+ "properties": {
243
+ "kind": {
244
+ "type": "string",
245
+ "const": "tab"
246
+ },
247
+ "sections": {
248
+ "anyOf": [
249
+ {
250
+ "type": "string",
251
+ "const": "all"
252
+ },
253
+ {
254
+ "type": "string",
255
+ "const": "public"
256
+ },
257
+ {
258
+ "type": "array",
259
+ "items": {
260
+ "type": "string"
261
+ }
262
+ }
263
+ ]
264
+ }
265
+ },
266
+ "required": [
267
+ "kind"
268
+ ],
269
+ "additionalProperties": false
185
270
  },
186
271
  {
187
- "type": "array",
188
- "items": {
189
- "type": "string"
190
- }
272
+ "type": "object",
273
+ "properties": {
274
+ "kind": {
275
+ "type": "string",
276
+ "const": "nested"
277
+ },
278
+ "into": {
279
+ "type": "string"
280
+ },
281
+ "docsPath": {
282
+ "type": "string"
283
+ },
284
+ "group": {
285
+ "type": "string"
286
+ }
287
+ },
288
+ "required": [
289
+ "kind",
290
+ "into"
291
+ ],
292
+ "additionalProperties": false
191
293
  }
192
294
  ]
193
- },
194
- "injectInto": {
195
- "type": "string"
196
- },
197
- "docsPath": {
198
- "type": "string"
199
295
  }
200
296
  },
201
297
  "required": [
202
298
  "name",
203
299
  "description",
204
- "status"
300
+ "status",
301
+ "source",
302
+ "placement"
205
303
  ],
206
304
  "additionalProperties": false
207
305
  }
@@ -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-7NF5BP3E.js");
285
+ const { registerWithHub } = await import("./hub-register-QB73GUF2.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-7NF5BP3E.js");
301
+ const { registerWithHub } = await import("./hub-register-QB73GUF2.js");
302
302
  await registerWithHub({
303
303
  projectName: detectProjectName(),
304
304
  hubToken: options.hubToken,
@@ -2,7 +2,7 @@ import {
2
2
  loadUserConfig,
3
3
  resolveConfig,
4
4
  resolveSections
5
- } from "./chunk-2WML2D77.js";
5
+ } from "./chunk-ZWF6VCSQ.js";
6
6
  import {
7
7
  scanDocs
8
8
  } from "./chunk-CTL4YL7C.js";
@@ -6,14 +6,18 @@ import {
6
6
  import {
7
7
  mermaidContentHash
8
8
  } from "./chunk-SCZZB7OE.js";
9
+ import {
10
+ generateLlmsFullTxt,
11
+ generateLlmsTxt
12
+ } from "./chunk-TVDWHKQF.js";
9
13
  import {
10
14
  clearifyPlugin
11
- } from "./chunk-GWYMXKDL.js";
15
+ } from "./chunk-Q5FUAK6J.js";
12
16
  import {
13
17
  loadUserConfig,
14
18
  resolveConfig,
15
19
  resolveSections
16
- } from "./chunk-2WML2D77.js";
20
+ } from "./chunk-ZWF6VCSQ.js";
17
21
  import {
18
22
  buildSectionData
19
23
  } from "./chunk-CTL4YL7C.js";
@@ -72,6 +76,29 @@ function generateSitemap(routes, siteUrl) {
72
76
  ${urls.join("\n")}
73
77
  </urlset>`;
74
78
  }
79
+ function formatBytes(bytes) {
80
+ if (bytes < 1024) return `${bytes} B`;
81
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
82
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
83
+ }
84
+ function collectLlmsDocs(sections) {
85
+ const out = [];
86
+ for (const sd of sections) {
87
+ for (const doc of sd.docs) {
88
+ const title = doc.frontmatter.title ?? "Untitled";
89
+ const optional = (doc.frontmatter.order ?? 0) >= 9e3;
90
+ const markdown = existsSync(doc.filePath) ? readFileSync(doc.filePath, "utf-8") : "";
91
+ out.push({
92
+ title,
93
+ routePath: doc.routePath,
94
+ sectionLabel: sd.section.label,
95
+ markdown,
96
+ optional
97
+ });
98
+ }
99
+ }
100
+ return out;
101
+ }
75
102
  function generateRobotsTxt(siteUrl) {
76
103
  const lines = ["User-agent: *", "Allow: /"];
77
104
  if (siteUrl) {
@@ -312,6 +339,19 @@ async function buildSite() {
312
339
  const sitemap = generateSitemap(sitemapRoutes, config.siteUrl);
313
340
  writeFileSync(resolve(outDir, "sitemap.xml"), sitemap);
314
341
  console.log(" Generated sitemap.xml");
342
+ if (config.generateLlmsTxt !== false) {
343
+ const llmsDocs = collectLlmsDocs(sectionDataList);
344
+ const llmsTxt = generateLlmsTxt(config, llmsDocs);
345
+ const llmsFullTxt = generateLlmsFullTxt(config, llmsDocs);
346
+ const llmsTxtPath = resolve(outDir, "llms.txt");
347
+ const llmsFullTxtPath = resolve(outDir, "llms-full.txt");
348
+ writeFileSync(llmsTxtPath, llmsTxt);
349
+ writeFileSync(llmsFullTxtPath, llmsFullTxt);
350
+ const llmsTxtSize = Buffer.byteLength(llmsTxt, "utf-8");
351
+ const llmsFullSize = Buffer.byteLength(llmsFullTxt, "utf-8");
352
+ console.log(` Generated ${llmsTxtPath} (${formatBytes(llmsTxtSize)})`);
353
+ console.log(` Generated ${llmsFullTxtPath} (${formatBytes(llmsFullSize)})`);
354
+ }
315
355
  rmSync(ssrOutDir, { recursive: true, force: true });
316
356
  console.log(" Cleaned up SSR build artifacts.");
317
357
  if (mermaidStrategy === "build") {
@@ -10,7 +10,7 @@ import {
10
10
  resolveSections,
11
11
  scanHubProjects,
12
12
  writeDataConfig
13
- } from "./chunk-2WML2D77.js";
13
+ } from "./chunk-ZWF6VCSQ.js";
14
14
  import {
15
15
  buildSectionData
16
16
  } from "./chunk-CTL4YL7C.js";
@@ -0,0 +1,134 @@
1
+ // src/core/llms-txt.ts
2
+ import { readFileSync } from "fs";
3
+ import matter from "gray-matter";
4
+ function normalizeBase(siteUrl) {
5
+ if (!siteUrl) return "";
6
+ return siteUrl.replace(/\/$/, "");
7
+ }
8
+ function toAbsoluteUrl(base, routePath) {
9
+ if (!base) return routePath;
10
+ if (!routePath.startsWith("/")) return `${base}/${routePath}`;
11
+ return `${base}${routePath}`;
12
+ }
13
+ function stripFrontmatterAndH1(markdown) {
14
+ const { content } = matter(markdown);
15
+ return content.replace(/^\s*#\s+[^\n]*\n+/, "").trimStart();
16
+ }
17
+ function firstParagraphSummary(markdown) {
18
+ const parsed = matter(markdown);
19
+ const fromFrontmatter = typeof parsed.data.summary === "string" && parsed.data.summary.trim() || typeof parsed.data.description === "string" && parsed.data.description.trim() || "";
20
+ const body = parsed.content;
21
+ const lines = body.split(/\r?\n/);
22
+ let inFence = false;
23
+ const paragraph = [];
24
+ for (const rawLine of lines) {
25
+ const line = rawLine.trimEnd();
26
+ if (/^```/.test(line)) {
27
+ inFence = !inFence;
28
+ if (paragraph.length > 0) break;
29
+ continue;
30
+ }
31
+ if (inFence) continue;
32
+ if (!line.trim()) {
33
+ if (paragraph.length > 0) break;
34
+ continue;
35
+ }
36
+ if (/^(#{1,6}\s|>\s|[-*+]\s|\d+\.\s|\|.*\|\s*$|<)/.test(line)) {
37
+ if (paragraph.length > 0) break;
38
+ continue;
39
+ }
40
+ paragraph.push(line.trim());
41
+ }
42
+ const text = paragraph.join(" ").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/([*_]{1,3})([^*_]+)\1/g, "$2").replace(/\s+/g, " ").trim();
43
+ return text || fromFrontmatter;
44
+ }
45
+ function uniqueOrdered(items) {
46
+ const seen = /* @__PURE__ */ new Set();
47
+ const out = [];
48
+ for (const item of items) {
49
+ if (seen.has(item)) continue;
50
+ seen.add(item);
51
+ out.push(item);
52
+ }
53
+ return out;
54
+ }
55
+ function buildHeader(config) {
56
+ const name = config.name.trim();
57
+ const tagline = config.hubProject?.description && config.hubProject.description.trim() || `Documentation for ${name}.`;
58
+ return `# ${name}
59
+
60
+ > ${tagline}
61
+
62
+ `;
63
+ }
64
+ function generateLlmsTxt(config, docs) {
65
+ const base = normalizeBase(config.siteUrl);
66
+ let out = buildHeader(config);
67
+ const mainDocs = docs.filter((d) => !d.optional);
68
+ const optionalDocs = docs.filter((d) => d.optional);
69
+ const sectionLabels = uniqueOrdered(mainDocs.map((d) => d.sectionLabel));
70
+ for (const label of sectionLabels) {
71
+ const sectionDocs = mainDocs.filter((d) => d.sectionLabel === label);
72
+ if (sectionDocs.length === 0) continue;
73
+ out += `## ${label}
74
+
75
+ `;
76
+ for (const doc of sectionDocs) {
77
+ const url = toAbsoluteUrl(base, doc.routePath);
78
+ const summary = firstParagraphSummary(doc.markdown);
79
+ if (summary) {
80
+ out += `- [${doc.title}](${url}): ${summary}
81
+ `;
82
+ } else {
83
+ out += `- [${doc.title}](${url})
84
+ `;
85
+ }
86
+ }
87
+ out += "\n";
88
+ }
89
+ if (optionalDocs.length > 0) {
90
+ out += `## Optional
91
+
92
+ `;
93
+ for (const doc of optionalDocs) {
94
+ const url = toAbsoluteUrl(base, doc.routePath);
95
+ const summary = firstParagraphSummary(doc.markdown);
96
+ if (summary) {
97
+ out += `- [${doc.title}](${url}): ${summary}
98
+ `;
99
+ } else {
100
+ out += `- [${doc.title}](${url})
101
+ `;
102
+ }
103
+ }
104
+ out += "\n";
105
+ }
106
+ return out.trimEnd() + "\n";
107
+ }
108
+ function generateLlmsFullTxt(config, docs) {
109
+ const base = normalizeBase(config.siteUrl);
110
+ let out = buildHeader(config);
111
+ for (const doc of docs) {
112
+ const url = toAbsoluteUrl(base, doc.routePath);
113
+ const body = stripFrontmatterAndH1(doc.markdown);
114
+ out += `## ${doc.title}
115
+
116
+ `;
117
+ out += `Source: ${url}
118
+
119
+ `;
120
+ if (body) {
121
+ out += body.trimEnd() + "\n\n";
122
+ }
123
+ }
124
+ return out.trimEnd() + "\n";
125
+ }
126
+ function readMarkdownFile(filePath) {
127
+ return readFileSync(filePath, "utf-8");
128
+ }
129
+
130
+ export {
131
+ generateLlmsTxt,
132
+ generateLlmsFullTxt,
133
+ readMarkdownFile
134
+ };
@@ -42,28 +42,44 @@ var OpenAPIConfigSchema = z.object({
42
42
  basePath: z.string().default("/api"),
43
43
  generatePages: z.boolean().default(true)
44
44
  }).optional();
45
+ var HubProjectSourceSchema = z.discriminatedUnion("kind", [
46
+ z.object({ kind: z.literal("none") }),
47
+ z.object({
48
+ kind: z.literal("git"),
49
+ repo: z.string(),
50
+ ref: z.string().optional(),
51
+ path: z.string().optional(),
52
+ sparse: z.boolean().optional()
53
+ }),
54
+ z.object({ kind: z.literal("url"), url: z.string() }),
55
+ z.object({ kind: z.literal("inline"), markdown: z.string() })
56
+ ]);
57
+ var HubProjectPlacementSchema = z.discriminatedUnion("kind", [
58
+ z.object({ kind: z.literal("card"), href: z.string() }),
59
+ z.object({
60
+ kind: z.literal("tab"),
61
+ sections: z.union([z.literal("all"), z.literal("public"), z.array(z.string())]).optional()
62
+ }),
63
+ z.object({
64
+ kind: z.literal("nested"),
65
+ into: z.string(),
66
+ docsPath: z.string().optional(),
67
+ group: z.string().optional()
68
+ })
69
+ ]);
45
70
  var HubProjectSchema = z.object({
46
71
  name: z.string(),
47
72
  description: z.string(),
48
- href: z.string().optional(),
49
73
  repo: z.string().optional(),
50
74
  status: z.enum(["active", "beta", "planned", "deprecated"]).default("active"),
51
75
  icon: z.string().optional(),
52
76
  tags: z.array(z.string()).optional(),
53
77
  group: z.string().optional(),
54
- mode: z.enum(["link", "embed", "inject"]).optional(),
55
- git: RemoteGitSourceSchema.optional(),
56
- embedSections: z.union([
57
- z.literal("all"),
58
- z.literal("public"),
59
- z.array(z.string())
60
- ]).optional(),
61
- injectInto: z.string().optional(),
62
- docsPath: z.string().optional()
78
+ source: HubProjectSourceSchema,
79
+ placement: HubProjectPlacementSchema
63
80
  });
64
81
  var HubProjectPartialSchema = z.object({
65
82
  description: z.string(),
66
- href: z.string().optional(),
67
83
  repo: z.string().optional(),
68
84
  status: z.enum(["active", "beta", "planned", "deprecated"]).default("active"),
69
85
  icon: z.string().optional(),
@@ -101,7 +117,8 @@ var ClearifyConfigSchema = z.object({
101
117
  hubProject: HubProjectPartialSchema,
102
118
  customCss: z.string().optional(),
103
119
  headTags: z.array(z.string()).optional(),
104
- includeReadme: z.boolean().default(false)
120
+ includeReadme: z.boolean().default(false),
121
+ generateLlmsTxt: z.boolean().default(true)
105
122
  });
106
123
  var ClearifyDataSchema = z.object({
107
124
  name: z.string().optional(),
@@ -249,6 +266,11 @@ function resolveConfig(userConfig = {}, root) {
249
266
  if (!userConfig.name && root) {
250
267
  parsed.name = detectProjectName(root);
251
268
  }
269
+ if (parsed.generateLlmsTxt && !parsed.siteUrl) {
270
+ throw new Error(
271
+ "clearify config: `generateLlmsTxt` is enabled (default) but `siteUrl` is not set. Set `siteUrl` to the production URL of your docs site, or set `generateLlmsTxt: false` to disable."
272
+ );
273
+ }
252
274
  return parsed;
253
275
  }
254
276
  function slugify(str) {
@@ -335,8 +357,14 @@ function buildStagingOverlay(stagingDir, originalDir, injections) {
335
357
  symlinkSync(sourcePath, destPath);
336
358
  }
337
359
  }
360
+ function isReservedSource(source) {
361
+ return source.kind === "url" || source.kind === "inline";
362
+ }
338
363
  async function scanHubProjects(config, root) {
339
- if (!config.hub?.scan && !config.hub?.projects?.some((p) => p.mode === "embed" || p.mode === "inject")) {
364
+ const hasGitTabOrNested = config.hub?.projects?.some(
365
+ (p) => p.source.kind === "git" && (p.placement.kind === "tab" || p.placement.kind === "nested")
366
+ ) ?? false;
367
+ if (!config.hub?.scan && !hasGitTabOrNested) {
340
368
  return config;
341
369
  }
342
370
  const scannedProjects = [];
@@ -350,15 +378,17 @@ async function scanHubProjects(config, root) {
350
378
  if (!childConfig.hubProject) continue;
351
379
  const name = childConfig.name ?? basename(dirname(configPath));
352
380
  const hp = childConfig.hubProject;
381
+ const href = childConfig.siteUrl;
353
382
  scannedProjects.push({
354
383
  name,
355
384
  description: hp.description,
356
- href: hp.href ?? childConfig.siteUrl,
357
385
  repo: hp.repo,
358
386
  status: hp.status,
359
387
  icon: hp.icon,
360
388
  tags: hp.tags,
361
- group: hp.group
389
+ group: hp.group,
390
+ source: { kind: "none" },
391
+ placement: { kind: "card", href: href ?? "#" }
362
392
  });
363
393
  } catch {
364
394
  }
@@ -369,18 +399,26 @@ async function scanHubProjects(config, root) {
369
399
  ...scannedProjects.filter((p) => !manualNames.has(p.name)),
370
400
  ...config.hub?.projects ?? []
371
401
  ];
402
+ for (const project of merged) {
403
+ if (isReservedSource(project.source)) {
404
+ throw new Error(
405
+ `Hub project "${project.name}": source.kind "${project.source.kind}" is reserved and not implemented. Use source.kind "none" or "git".`
406
+ );
407
+ }
408
+ }
372
409
  const embedSections = [];
373
410
  for (const project of merged) {
374
- if (project.mode !== "embed" || !project.git) continue;
411
+ if (project.source.kind !== "git" || project.placement.kind !== "tab") continue;
375
412
  const cacheDir = config.hub?.cacheDir ? resolve(root, config.hub.cacheDir) : resolve(root, "node_modules/.cache/clearify-remote");
376
413
  try {
377
414
  const { resolveRemoteSource } = await import("./core/remote.js");
378
- const { localPath } = await resolveRemoteSource(project.git, cacheDir);
415
+ const { kind: _kind, ...gitSource } = project.source;
416
+ const { localPath } = await resolveRemoteSource(gitSource, cacheDir);
379
417
  const remoteConfig = await loadUserConfig(localPath);
380
418
  const remoteSections = remoteConfig.sections ?? [
381
419
  { label: remoteConfig.name ?? project.name, docsDir: remoteConfig.docsDir ?? "./docs" }
382
420
  ];
383
- const filter = project.embedSections ?? "public";
421
+ const filter = project.placement.sections ?? "public";
384
422
  const filtered = remoteSections.filter((s) => {
385
423
  if (filter === "all") return true;
386
424
  if (filter === "public") return !s.draft;
@@ -400,46 +438,44 @@ async function scanHubProjects(config, root) {
400
438
  exclude: s.exclude
401
439
  });
402
440
  }
403
- console.log(` Hub embed "${project.name}": ${filtered.length} sections pulled`);
441
+ console.log(` Hub tab "${project.name}": ${filtered.length} sections pulled`);
404
442
  } catch (err) {
405
443
  console.warn(
406
- ` \u26A0 Hub embed "${project.name}" failed:`,
444
+ ` Hub tab "${project.name}" failed:`,
407
445
  err instanceof Error ? err.message : err
408
446
  );
409
447
  }
410
448
  }
411
449
  const injectionsBySection = /* @__PURE__ */ new Map();
412
450
  for (const project of merged) {
413
- if (project.mode !== "inject" || !project.git) continue;
451
+ if (project.source.kind !== "git" || project.placement.kind !== "nested") continue;
414
452
  const cacheDir = config.hub?.cacheDir ? resolve(root, config.hub.cacheDir) : resolve(root, "node_modules/.cache/clearify-remote");
415
453
  try {
416
454
  const { resolveRemoteSource } = await import("./core/remote.js");
417
- const { localPath, cached } = await resolveRemoteSource(project.git, cacheDir);
418
- const docsPath = project.docsPath ?? "docs";
455
+ const { kind: _kind, ...gitSource } = project.source;
456
+ const { localPath, cached } = await resolveRemoteSource(gitSource, cacheDir);
457
+ const docsPath = project.placement.docsPath ?? "docs";
419
458
  const absoluteDocsPath = resolve(localPath, docsPath);
420
459
  if (!existsSync(absoluteDocsPath)) {
421
460
  console.warn(
422
- ` \u26A0 Hub inject "${project.name}": docs path "${docsPath}" not found in cloned repo.`
461
+ ` Hub nested "${project.name}": docs path "${docsPath}" not found in cloned repo.`
423
462
  );
424
463
  continue;
425
464
  }
426
- const targetLabel = project.injectInto;
427
- if (!targetLabel) {
428
- console.warn(` \u26A0 Hub inject "${project.name}": missing injectInto field.`);
429
- continue;
430
- }
465
+ const targetLabel = project.placement.into;
431
466
  if (!injectionsBySection.has(targetLabel)) {
432
467
  injectionsBySection.set(targetLabel, /* @__PURE__ */ new Map());
433
468
  }
434
469
  const slugifiedName = slugify(project.name);
435
- const relPath = project.group ? `projects/${project.group}/${slugifiedName}` : `projects/${slugifiedName}`;
470
+ const groupSeg = project.placement.group;
471
+ const relPath = groupSeg ? `projects/${groupSeg}/${slugifiedName}` : `projects/${slugifiedName}`;
436
472
  injectionsBySection.get(targetLabel).set(relPath, absoluteDocsPath);
437
473
  console.log(
438
- ` Hub inject "${project.name}": ${cached ? "cached" : "cloned"} \u2192 ${relPath}`
474
+ ` Hub nested "${project.name}": ${cached ? "cached" : "cloned"} to ${relPath}`
439
475
  );
440
476
  } catch (err) {
441
477
  console.warn(
442
- ` \u26A0 Hub inject "${project.name}" failed:`,
478
+ ` Hub nested "${project.name}" failed:`,
443
479
  err instanceof Error ? err.message : err
444
480
  );
445
481
  }