homegraph 1.5.8 → 1.6.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/extraction/grammars.d.ts +19 -0
  3. package/dist/extraction/grammars.js +44 -1
  4. package/dist/extraction/index.js +17 -12
  5. package/dist/extraction/languages/arkts.d.ts +40 -7
  6. package/dist/extraction/languages/arkts.js +290 -84
  7. package/dist/extraction/tree-sitter.js +12 -3
  8. package/dist/index.js +23 -12
  9. package/dist/mcp/arkts-evidence-packs.js +1 -0
  10. package/dist/mcp/daemon.d.ts +21 -3
  11. package/dist/mcp/daemon.js +60 -6
  12. package/dist/mcp/engine.d.ts +26 -0
  13. package/dist/mcp/engine.js +143 -7
  14. package/dist/mcp/index-availability.d.ts +40 -10
  15. package/dist/mcp/index-availability.js +89 -23
  16. package/dist/mcp/index.js +9 -0
  17. package/dist/mcp/indexable-root.d.ts +22 -0
  18. package/dist/mcp/indexable-root.js +140 -0
  19. package/dist/mcp/liveness-watchdog.d.ts +6 -1
  20. package/dist/mcp/liveness-watchdog.js +17 -5
  21. package/dist/mcp/locate-contract.d.ts +50 -0
  22. package/dist/mcp/locate-contract.js +146 -0
  23. package/dist/mcp/server-instructions.d.ts +2 -6
  24. package/dist/mcp/server-instructions.js +15 -5
  25. package/dist/mcp/session.js +15 -0
  26. package/dist/mcp/tools.d.ts +52 -0
  27. package/dist/mcp/tools.js +750 -103
  28. package/dist/project-map/index.d.ts +45 -0
  29. package/dist/project-map/index.js +373 -45
  30. package/dist/resolution/callback-synthesizer.js +251 -0
  31. package/dist/resolution/frameworks/arkts-entry.d.ts +35 -6
  32. package/dist/resolution/frameworks/arkts-entry.js +513 -30
  33. package/dist/resolution/index.js +25 -21
  34. package/dist/runtime-log.d.ts +52 -0
  35. package/dist/runtime-log.js +199 -0
  36. package/dist/search/query-plan-provider.js +3 -2
  37. package/dist/search/query-plan.js +21 -19
  38. package/dist/search/query-utils.d.ts +13 -0
  39. package/dist/search/query-utils.js +64 -0
  40. package/package.json +2 -2
@@ -30,4 +30,49 @@ export interface BuiltProjectMap {
30
30
  */
31
31
  export declare function buildProjectMapScan(projectRoot: string): BuiltProjectMap;
32
32
  export declare function parseBuildPhase(raw: string | null | undefined): BuildPhase | null;
33
+ /**
34
+ * Spec 0040 — optional bundleName/name from app.json5 (AppScope or root).
35
+ * Best-effort; returns null on missing/parse failure.
36
+ */
37
+ export declare function readHarmonyAppBundleName(projectRoot: string): string | null;
38
+ /**
39
+ * Spec 0040 — project-relative paths to Spec 0039 route profile JSON under a module root.
40
+ */
41
+ export declare function listHarmonyRouteProfilesUnderModule(projectRoot: string, moduleRootPath: string, cap?: number): string[];
42
+ /** Spec 0040 — oh-package name at module root, if any. */
43
+ export declare function readModuleOhPackageName(projectRoot: string, moduleRootPath: string): string | null;
44
+ /** Spec 0042 B — path-only Harmony resource inventory (no parse, no edges). */
45
+ export interface HarmonyResourceInventory {
46
+ stringJson: string[];
47
+ rawfiles: string[];
48
+ mediaDirs: Array<{
49
+ dir: string;
50
+ countsByExt: Record<string, number>;
51
+ }>;
52
+ /** Spec 0048 — form_config.json / shortcuts_config.json paths. */
53
+ capabilityProfiles: string[];
54
+ unindexedDirs: Array<{
55
+ path: string;
56
+ reason: string;
57
+ }>;
58
+ truncated: boolean;
59
+ }
60
+ /**
61
+ * Bounded walk: whitelist paths under `resources/…` plus package dirs with zero
62
+ * indexed sources. Does not read JSON bodies or media bytes.
63
+ */
64
+ export declare function scanHarmonyResourceInventory(projectRoot: string, options?: {
65
+ indexedPaths?: Iterable<string>;
66
+ }): HarmonyResourceInventory;
67
+ /** Markdown section for Spec 0042 B (empty string when nothing to show). */
68
+ export declare function formatHarmonyResourceInventory(inv: HarmonyResourceInventory): string;
69
+ /**
70
+ * Spec 0048 §4 — module roster with oh-package name + local file: deps.
71
+ */
72
+ export declare function formatHarmonyModuleRoster(projectRoot: string, modules: Array<{
73
+ name: string;
74
+ rootPath: string;
75
+ kind: string;
76
+ fileCount: number;
77
+ }>, maxRows?: number): string;
33
78
  //# sourceMappingURL=index.d.ts.map
@@ -41,6 +41,12 @@ var __importStar = (this && this.__importStar) || (function () {
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.buildProjectMapScan = buildProjectMapScan;
43
43
  exports.parseBuildPhase = parseBuildPhase;
44
+ exports.readHarmonyAppBundleName = readHarmonyAppBundleName;
45
+ exports.listHarmonyRouteProfilesUnderModule = listHarmonyRouteProfilesUnderModule;
46
+ exports.readModuleOhPackageName = readModuleOhPackageName;
47
+ exports.scanHarmonyResourceInventory = scanHarmonyResourceInventory;
48
+ exports.formatHarmonyResourceInventory = formatHarmonyResourceInventory;
49
+ exports.formatHarmonyModuleRoster = formatHarmonyModuleRoster;
44
50
  const fs = __importStar(require("fs"));
45
51
  const path = __importStar(require("path"));
46
52
  const extraction_1 = require("../extraction");
@@ -103,7 +109,8 @@ function discoverModules(projectRoot) {
103
109
  rootPath: '',
104
110
  kind: 'root',
105
111
  });
106
- const harmony = listHarmonyProjectModulesLoose(projectRoot);
112
+ // Same parser as ArkTS dirty-module mapping (Spec 0034): bare keys + single quotes.
113
+ const harmony = (0, arkts_1.listHarmonyProjectModules)(projectRoot);
107
114
  if (harmony.length > 0) {
108
115
  for (const m of harmony) {
109
116
  ensure({
@@ -132,47 +139,6 @@ function discoverModules(projectRoot) {
132
139
  }
133
140
  return [...byId.values()];
134
141
  }
135
- function listHarmonyProjectModulesLoose(projectRoot) {
136
- const strict = (0, arkts_1.listHarmonyProjectModules)(projectRoot);
137
- if (strict.length > 0)
138
- return strict;
139
- // Real DevEco `build-profile.json5` often uses unquoted keys; the strict
140
- // parser only strips comments/trailing commas. Quote bare keys and retry.
141
- const profilePath = path.join(projectRoot, 'build-profile.json5');
142
- if (!fs.existsSync(profilePath))
143
- return [];
144
- let raw;
145
- try {
146
- const text = fs.readFileSync(profilePath, 'utf-8');
147
- const stripped = text
148
- .replace(/\/\/.*$/gm, '')
149
- .replace(/\/\*[\s\S]*?\*\//g, '')
150
- .replace(/,(\s*[}\]])/g, '$1')
151
- .replace(/([{,]\s*)([A-Za-z_][\w]*)\s*:/g, '$1"$2":');
152
- raw = JSON.parse(stripped);
153
- }
154
- catch {
155
- return [];
156
- }
157
- if (!raw || typeof raw !== 'object')
158
- return [];
159
- const modules = raw.modules;
160
- if (!Array.isArray(modules))
161
- return [];
162
- const out = [];
163
- for (const entry of modules) {
164
- if (!entry || typeof entry !== 'object')
165
- continue;
166
- const rec = entry;
167
- if (typeof rec.name !== 'string' || typeof rec.srcPath !== 'string')
168
- continue;
169
- const srcPath = (0, arkts_1.normalizeHarmonyModuleSrcPath)(rec.srcPath);
170
- if (!srcPath)
171
- continue;
172
- out.push({ name: rec.name, srcPath });
173
- }
174
- return out;
175
- }
176
142
  function discoverOhpmPackages(projectRoot) {
177
143
  const out = [];
178
144
  const queue = [{ rel: '', depth: 0 }];
@@ -218,9 +184,19 @@ function discoverOhpmPackages(projectRoot) {
218
184
  }
219
185
  function readOhpmName(manifestAbs) {
220
186
  try {
221
- // eslint-disable-next-line @typescript-eslint/no-require-imports
222
- const parsed = require('jsonc-parser').parse(fs.readFileSync(manifestAbs, 'utf-8'));
223
- return typeof parsed?.name === 'string' && parsed.name.trim() ? parsed.name.trim() : null;
187
+ const text = fs.readFileSync(manifestAbs, 'utf-8');
188
+ try {
189
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
190
+ const parsed = require('jsonc-parser').parse(text);
191
+ if (typeof parsed?.name === 'string' && parsed.name.trim())
192
+ return parsed.name.trim();
193
+ }
194
+ catch {
195
+ /* regex */
196
+ }
197
+ const m = text.match(/["']name["']\s*:\s*["']([^"']+)["']/) ||
198
+ text.match(/name\s*:\s*["']([^"']+)["']/);
199
+ return m?.[1]?.trim() || null;
224
200
  }
225
201
  catch {
226
202
  return null;
@@ -257,4 +233,356 @@ function parseBuildPhase(raw) {
257
233
  }
258
234
  return null;
259
235
  }
236
+ const SKELETON_SKIP_DIRS = new Set([
237
+ 'node_modules',
238
+ 'oh_modules',
239
+ '.git',
240
+ '.homegraph',
241
+ '.hvigor',
242
+ '.preview',
243
+ 'build',
244
+ 'dist',
245
+ 'out',
246
+ '.cxx',
247
+ ]);
248
+ const ROUTE_PROFILE_WALK_MAX_DEPTH = 8;
249
+ const ROUTE_PROFILE_DIR_BUDGET = 400;
250
+ const ROUTE_PROFILE_CAP_PER_MODULE = 8;
251
+ /**
252
+ * Spec 0040 — optional bundleName/name from app.json5 (AppScope or root).
253
+ * Best-effort; returns null on missing/parse failure.
254
+ */
255
+ function readHarmonyAppBundleName(projectRoot) {
256
+ for (const rel of ['AppScope/app.json5', 'app.json5']) {
257
+ const abs = path.join(projectRoot, rel);
258
+ if (!fs.existsSync(abs))
259
+ continue;
260
+ try {
261
+ const text = fs.readFileSync(abs, 'utf-8');
262
+ let bundle = null;
263
+ try {
264
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
265
+ const parsed = require('jsonc-parser').parse(text);
266
+ if (parsed?.app && typeof parsed.app.bundleName === 'string' && parsed.app.bundleName.trim()) {
267
+ bundle = parsed.app.bundleName.trim();
268
+ }
269
+ else if (typeof parsed?.bundleName === 'string' && parsed.bundleName.trim()) {
270
+ bundle = parsed.bundleName.trim();
271
+ }
272
+ else if (typeof parsed?.name === 'string' && parsed.name.trim()) {
273
+ bundle = parsed.name.trim();
274
+ }
275
+ }
276
+ catch {
277
+ /* regex fallback below */
278
+ }
279
+ if (!bundle) {
280
+ const m = text.match(/["']bundleName["']\s*:\s*["']([^"']+)["']/) ||
281
+ text.match(/bundleName\s*:\s*["']([^"']+)["']/);
282
+ if (m?.[1])
283
+ bundle = m[1];
284
+ }
285
+ if (bundle)
286
+ return bundle;
287
+ }
288
+ catch {
289
+ continue;
290
+ }
291
+ }
292
+ return null;
293
+ }
294
+ /**
295
+ * Spec 0040 — project-relative paths to Spec 0039 route profile JSON under a module root.
296
+ */
297
+ function listHarmonyRouteProfilesUnderModule(projectRoot, moduleRootPath, cap = ROUTE_PROFILE_CAP_PER_MODULE) {
298
+ const rootAbs = path.resolve(projectRoot);
299
+ const modAbs = moduleRootPath
300
+ ? path.resolve(projectRoot, moduleRootPath)
301
+ : rootAbs;
302
+ if (!fs.existsSync(modAbs))
303
+ return [];
304
+ const found = [];
305
+ const queue = [{ abs: modAbs, depth: 0 }];
306
+ let visited = 0;
307
+ while (queue.length > 0 && found.length < cap) {
308
+ const { abs, depth } = queue.shift();
309
+ if (++visited > ROUTE_PROFILE_DIR_BUDGET)
310
+ break;
311
+ let entries;
312
+ try {
313
+ entries = fs.readdirSync(abs, { withFileTypes: true });
314
+ }
315
+ catch {
316
+ continue;
317
+ }
318
+ for (const e of entries) {
319
+ if (found.length >= cap)
320
+ break;
321
+ const child = path.join(abs, e.name);
322
+ if (e.isDirectory()) {
323
+ if (depth >= ROUTE_PROFILE_WALK_MAX_DEPTH)
324
+ continue;
325
+ if (e.name.startsWith('.') || SKELETON_SKIP_DIRS.has(e.name))
326
+ continue;
327
+ queue.push({ abs: child, depth: depth + 1 });
328
+ continue;
329
+ }
330
+ if (!e.isFile())
331
+ continue;
332
+ const rel = path.relative(rootAbs, child).replace(/\\/g, '/');
333
+ if ((0, grammars_1.isHarmonyRouteProfileJson)(rel))
334
+ found.push(rel);
335
+ }
336
+ }
337
+ return found.sort();
338
+ }
339
+ /** Spec 0040 — oh-package name at module root, if any. */
340
+ function readModuleOhPackageName(projectRoot, moduleRootPath) {
341
+ const abs = path.join(projectRoot, moduleRootPath || '.', OHPM_MANIFEST);
342
+ return readOhpmName(abs);
343
+ }
344
+ const RESOURCE_WALK_MAX_DEPTH = 14;
345
+ const RESOURCE_DIR_BUDGET = 4000;
346
+ const RESOURCE_STRING_CAP = 24;
347
+ const RESOURCE_RAWFILE_CAP = 40;
348
+ const RESOURCE_MEDIA_DIR_CAP = 24;
349
+ const RESOURCE_UNINDEXED_CAP = 16;
350
+ const RESOURCE_CAPABILITY_CAP = 16;
351
+ const RESOURCE_CONFIG_BASENAMES = new Set([
352
+ 'shortcuts_config.json',
353
+ 'form_config.json',
354
+ ]);
355
+ /**
356
+ * Bounded walk: whitelist paths under `resources/…` plus package dirs with zero
357
+ * indexed sources. Does not read JSON bodies or media bytes.
358
+ */
359
+ function scanHarmonyResourceInventory(projectRoot, options) {
360
+ const rootAbs = path.resolve(projectRoot);
361
+ const out = {
362
+ stringJson: [],
363
+ rawfiles: [],
364
+ mediaDirs: [],
365
+ capabilityProfiles: [],
366
+ unindexedDirs: [],
367
+ truncated: false,
368
+ };
369
+ if (!fs.existsSync(rootAbs))
370
+ return out;
371
+ const indexed = new Set();
372
+ for (const p of options?.indexedPaths ?? []) {
373
+ indexed.add(p.replace(/\\/g, '/'));
374
+ }
375
+ const mediaCounts = new Map();
376
+ const packageDirs = new Map();
377
+ const queue = [{ abs: rootAbs, depth: 0 }];
378
+ let visited = 0;
379
+ while (queue.length > 0) {
380
+ const { abs, depth } = queue.shift();
381
+ if (++visited > RESOURCE_DIR_BUDGET) {
382
+ out.truncated = true;
383
+ break;
384
+ }
385
+ let entries;
386
+ try {
387
+ entries = fs.readdirSync(abs, { withFileTypes: true });
388
+ }
389
+ catch {
390
+ continue;
391
+ }
392
+ for (const e of entries) {
393
+ const child = path.join(abs, e.name);
394
+ const rel = path.relative(rootAbs, child).replace(/\\/g, '/');
395
+ if (e.isDirectory()) {
396
+ if (depth >= RESOURCE_WALK_MAX_DEPTH)
397
+ continue;
398
+ if (e.name.startsWith('.') || SKELETON_SKIP_DIRS.has(e.name))
399
+ continue;
400
+ queue.push({ abs: child, depth: depth + 1 });
401
+ continue;
402
+ }
403
+ if (!e.isFile() || !rel || rel.startsWith('..'))
404
+ continue;
405
+ if (/(?:^|\/)resources\/(?:[^/]+\/)*element\/string\.json$/i.test(rel)) {
406
+ if (out.stringJson.length < RESOURCE_STRING_CAP)
407
+ out.stringJson.push(rel);
408
+ else
409
+ out.truncated = true;
410
+ continue;
411
+ }
412
+ if (/(?:^|\/)resources\/(?:[^/]+\/)*rawfile\//i.test(rel)) {
413
+ if (out.rawfiles.length < RESOURCE_RAWFILE_CAP)
414
+ out.rawfiles.push(rel);
415
+ else
416
+ out.truncated = true;
417
+ continue;
418
+ }
419
+ const mediaMatch = rel.match(/^(.*?\/resources\/(?:[^/]+\/)*(?:base\/)?media)\/[^/]+$/i);
420
+ if (mediaMatch) {
421
+ const dir = mediaMatch[1].replace(/\\/g, '/');
422
+ const ext = path.extname(rel).slice(1).toLowerCase() || 'bin';
423
+ const counts = mediaCounts.get(dir) ?? {};
424
+ counts[ext] = (counts[ext] ?? 0) + 1;
425
+ mediaCounts.set(dir, counts);
426
+ continue;
427
+ }
428
+ const base = rel.split('/').pop()?.toLowerCase() ?? '';
429
+ if (RESOURCE_CONFIG_BASENAMES.has(base)) {
430
+ if (out.capabilityProfiles.length < RESOURCE_CAPABILITY_CAP) {
431
+ if (!out.capabilityProfiles.includes(rel))
432
+ out.capabilityProfiles.push(rel);
433
+ }
434
+ else {
435
+ out.truncated = true;
436
+ }
437
+ continue;
438
+ }
439
+ if (base === 'oh-package.json5' || base === 'module.json5') {
440
+ const dirRel = rel.includes('/') ? rel.slice(0, rel.lastIndexOf('/')) : '';
441
+ const cur = packageDirs.get(dirRel) ?? { hasOhpm: false, hasModule: false };
442
+ if (base === 'oh-package.json5')
443
+ cur.hasOhpm = true;
444
+ else
445
+ cur.hasModule = true;
446
+ packageDirs.set(dirRel, cur);
447
+ }
448
+ }
449
+ }
450
+ for (const [dir, counts] of [...mediaCounts.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
451
+ if (out.mediaDirs.length >= RESOURCE_MEDIA_DIR_CAP) {
452
+ out.truncated = true;
453
+ break;
454
+ }
455
+ out.mediaDirs.push({ dir, countsByExt: counts });
456
+ }
457
+ out.stringJson.sort((a, b) => a.localeCompare(b));
458
+ out.rawfiles.sort((a, b) => a.localeCompare(b));
459
+ out.capabilityProfiles.sort((a, b) => a.localeCompare(b));
460
+ // Unindexed package/module dirs: on disk but no indexed source under them.
461
+ for (const [dirRel, flags] of [...packageDirs.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
462
+ if (out.unindexedDirs.length >= RESOURCE_UNINDEXED_CAP) {
463
+ out.truncated = true;
464
+ break;
465
+ }
466
+ if (!dirRel)
467
+ continue; // project root always "indexed" via map
468
+ const prefix = `${dirRel}/`;
469
+ const hasIndexed = [...indexed].some((p) => p === dirRel || p.startsWith(prefix));
470
+ if (hasIndexed)
471
+ continue;
472
+ // Skip pure resource-only package markers under resources/ (not a module root).
473
+ if (/(?:^|\/)resources\//i.test(dirRel))
474
+ continue;
475
+ const reason = flags.hasOhpm
476
+ ? 'oh-package.json5 present; 0 indexed source files'
477
+ : 'module.json5 present; 0 indexed source files';
478
+ out.unindexedDirs.push({ path: dirRel, reason });
479
+ }
480
+ return out;
481
+ }
482
+ /** Markdown section for Spec 0042 B (empty string when nothing to show). */
483
+ function formatHarmonyResourceInventory(inv) {
484
+ const lines = [];
485
+ const hasResources = inv.stringJson.length > 0
486
+ || inv.rawfiles.length > 0
487
+ || inv.mediaDirs.length > 0
488
+ || inv.capabilityProfiles.length > 0;
489
+ if (hasResources) {
490
+ lines.push('### HarmonyOS resources');
491
+ if (inv.stringJson.length) {
492
+ const shown = inv.stringJson.slice(0, 12);
493
+ lines.push(`- string.json (${inv.stringJson.length}): ${shown.map((p) => `\`${p}\``).join(', ')}`
494
+ + (inv.stringJson.length > shown.length ? `, +${inv.stringJson.length - shown.length} more` : ''));
495
+ }
496
+ if (inv.capabilityProfiles.length) {
497
+ const shown = inv.capabilityProfiles.slice(0, 12);
498
+ lines.push(`- capability profiles (${inv.capabilityProfiles.length}): ${shown.map((p) => `\`${p}\``).join(', ')}`
499
+ + (inv.capabilityProfiles.length > shown.length
500
+ ? `, +${inv.capabilityProfiles.length - shown.length} more`
501
+ : ''));
502
+ }
503
+ if (inv.rawfiles.length) {
504
+ const shown = inv.rawfiles.slice(0, 12);
505
+ lines.push(`- rawfile (${inv.rawfiles.length}): ${shown.map((p) => `\`${p}\``).join(', ')}`
506
+ + (inv.rawfiles.length > shown.length ? `, +${inv.rawfiles.length - shown.length} more` : ''));
507
+ }
508
+ for (const m of inv.mediaDirs.slice(0, 8)) {
509
+ const counts = Object.entries(m.countsByExt)
510
+ .sort((a, b) => a[0].localeCompare(b[0]))
511
+ .map(([ext, n]) => `${ext}=${n}`)
512
+ .join(', ');
513
+ lines.push(`- media: \`${m.dir}\`${counts ? ` (${counts})` : ''}`);
514
+ }
515
+ if (inv.mediaDirs.length > 8) {
516
+ lines.push(`- media: … +${inv.mediaDirs.length - 8} more dirs`);
517
+ }
518
+ }
519
+ if (inv.unindexedDirs.length) {
520
+ if (lines.length)
521
+ lines.push('');
522
+ lines.push('### On disk, not in graph');
523
+ for (const u of inv.unindexedDirs) {
524
+ lines.push(`- \`${u.path}/\` (${u.reason})`);
525
+ }
526
+ }
527
+ if (inv.truncated) {
528
+ lines.push('');
529
+ lines.push('_resource inventory truncated (bounded walk)_');
530
+ }
531
+ return lines.join('\n');
532
+ }
533
+ function readOhpmLocalDeps(manifestAbs, cap = 8) {
534
+ try {
535
+ const text = fs.readFileSync(manifestAbs, 'utf-8');
536
+ let deps;
537
+ try {
538
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
539
+ const parsed = require('jsonc-parser').parse(text);
540
+ deps = parsed?.dependencies;
541
+ }
542
+ catch {
543
+ deps = undefined;
544
+ }
545
+ if (!deps || typeof deps !== 'object')
546
+ return [];
547
+ const out = [];
548
+ for (const [name, ver] of Object.entries(deps)) {
549
+ if (out.length >= cap)
550
+ break;
551
+ if (typeof ver !== 'string')
552
+ continue;
553
+ if (!/^(file:|\.\.?\/)/i.test(ver.trim()))
554
+ continue;
555
+ out.push(`\`${name}\` (${ver.trim()})`);
556
+ }
557
+ return out;
558
+ }
559
+ catch {
560
+ return [];
561
+ }
562
+ }
563
+ /**
564
+ * Spec 0048 §4 — module roster with oh-package name + local file: deps.
565
+ */
566
+ function formatHarmonyModuleRoster(projectRoot, modules, maxRows = 24) {
567
+ const rows = [];
568
+ for (const m of modules) {
569
+ if (rows.length >= maxRows)
570
+ break;
571
+ if (m.kind === 'root' && !m.rootPath)
572
+ continue;
573
+ const rootLabel = m.rootPath || '.';
574
+ const ohpmAbs = path.join(projectRoot, m.rootPath || '.', OHPM_MANIFEST);
575
+ const pkg = readOhpmName(ohpmAbs);
576
+ const localDeps = readOhpmLocalDeps(ohpmAbs);
577
+ let line = `- \`${m.name}\` (\`${rootLabel}\`) · ${m.kind} · ${m.fileCount} files`;
578
+ if (pkg)
579
+ line += ` · oh-package \`${pkg}\``;
580
+ if (localDeps.length)
581
+ line += ` · deps: ${localDeps.join(', ')}`;
582
+ rows.push(line);
583
+ }
584
+ if (rows.length === 0)
585
+ return '';
586
+ return ['### Module roster', ...rows].join('\n');
587
+ }
260
588
  //# sourceMappingURL=index.js.map