@mono-labs/cli 0.0.207 → 0.0.209

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,182 +1,159 @@
1
1
  // scripts/generate-readme.mjs
2
2
  // Node >= 18 recommended
3
-
4
- import { promises as fs, Dirent } from 'node:fs';
3
+ import { promises as fs } from 'node:fs';
4
+ import { Dirent } from 'node:fs';
5
5
  import path from 'node:path';
6
6
  import { generateDocsIndex } from './generate-docs.js';
7
7
 
8
- /* -------------------------------------------------------------------------- */
9
- /* Types */
10
- /* -------------------------------------------------------------------------- */
11
-
12
- type MonoConfig = {
13
- path: string;
14
- config: {
15
- envMap?: string[];
16
- prodFlag?: string;
17
- workspace?: {
18
- packageMaps?: Record<string, string>;
19
- preactions?: string[];
20
- };
21
- };
22
- };
23
-
24
- type MonoCommand = {
25
- name: string;
26
- file: string;
27
- json: {
28
- description?: string;
29
- argument?: {
30
- type?: string;
31
- description?: string;
32
- default?: unknown;
33
- required?: boolean;
34
- };
35
- options?: Record<string, unknown>;
36
- environments?: Record<string, Record<string, string>>;
37
- preactions?: string[];
38
- actions?: string[];
39
- };
40
- };
41
-
42
- type PackageInfo = {
43
- name: string;
44
- dir: string;
45
- scripts: Record<string, string>;
46
- };
47
-
48
- type OptionSchema = {
49
- key: string;
50
- kind: 'boolean' | 'value';
51
- type: string;
52
- description: string;
53
- shortcut: string;
54
- default: unknown;
55
- allowed: string[] | null;
56
- allowAll: boolean;
57
- };
58
-
59
- /* -------------------------------------------------------------------------- */
60
- /* Constants */
61
- /* -------------------------------------------------------------------------- */
62
-
63
8
  const REPO_ROOT = path.resolve(process.cwd());
64
9
  const MONO_DIR = path.join(REPO_ROOT, '.mono');
65
10
  const ROOT_PKG_JSON = path.join(REPO_ROOT, 'package.json');
66
11
  const OUTPUT_PATH = path.join(REPO_ROOT, 'docs');
67
12
  const OUTPUT_README = path.join(OUTPUT_PATH, 'command-line.md');
68
13
 
69
- /* -------------------------------------------------------------------------- */
70
- /* Utils */
71
- /* -------------------------------------------------------------------------- */
72
-
73
14
  async function ensureParentDir(filePath: string): Promise<void> {
74
- await fs.mkdir(path.dirname(filePath), { recursive: true });
15
+ const dir = path.dirname(filePath);
16
+ console.log(`[ensureParentDir] Ensuring directory:`, dir);
17
+ await fs.mkdir(dir, { recursive: true });
75
18
  }
76
19
 
20
+ // ---------- utils ----------
77
21
  async function exists(p: string): Promise<boolean> {
78
22
  try {
79
23
  await fs.access(p);
24
+ // Log existence check
25
+ console.log(`[exists] Path exists:`, p);
80
26
  return true;
81
27
  } catch {
28
+ console.log(`[exists] Path does NOT exist:`, p);
82
29
  return false;
83
30
  }
84
31
  }
85
-
86
32
  function isObject(v: unknown): v is Record<string, unknown> {
87
- return typeof v === 'object' && v !== null && !Array.isArray(v);
33
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
88
34
  }
89
-
90
35
  function toPosix(p: string): string {
91
36
  return p.split(path.sep).join('/');
92
37
  }
93
-
94
- async function readJson<T>(filePath: string): Promise<T> {
95
- return JSON.parse(await fs.readFile(filePath, 'utf8')) as T;
38
+ async function readJson<T = any>(filePath: string): Promise<T> {
39
+ console.log(`[readJson] Reading JSON file:`, filePath);
40
+ const raw = await fs.readFile(filePath, 'utf8');
41
+ try {
42
+ const parsed = JSON.parse(raw);
43
+ console.log(`[readJson] Successfully parsed:`, filePath);
44
+ return parsed;
45
+ } catch (err) {
46
+ console.error(`[readJson] Failed to parse JSON:`, filePath, err);
47
+ throw err;
48
+ }
96
49
  }
97
-
98
50
  async function listDir(dir: string): Promise<Dirent[]> {
99
- return fs.readdir(dir, { withFileTypes: true });
51
+ console.log(`[listDir] Listing directory:`, dir);
52
+ const entries = await fs.readdir(dir, { withFileTypes: true });
53
+ console.log(`[listDir] Found ${entries.length} entries in:`, dir);
54
+ return entries;
100
55
  }
101
-
102
56
  function normalizeWorkspacePatterns(workspacesField: unknown): string[] {
103
- if (Array.isArray(workspacesField)) return workspacesField;
57
+ if (Array.isArray(workspacesField)) return workspacesField as string[];
104
58
  if (
105
59
  isObject(workspacesField) &&
106
- Array.isArray((workspacesField as { packages?: unknown }).packages)
107
- ) {
108
- return (workspacesField as { packages: string[] }).packages;
109
- }
60
+ Array.isArray((workspacesField as any).packages)
61
+ )
62
+ return (workspacesField as any).packages;
110
63
  return [];
111
64
  }
112
-
113
65
  function mdEscapeInline(s: string): string {
114
- return s.replaceAll('`', '\\`');
66
+ return String(s ?? '').replaceAll('`', '\`');
115
67
  }
116
-
117
68
  function indentLines(s: string, spaces = 2): string {
118
69
  const pad = ' '.repeat(spaces);
119
- return s
70
+ return String(s ?? '')
120
71
  .split('\n')
121
72
  .map((l) => pad + l)
122
73
  .join('\n');
123
74
  }
124
75
 
125
- /* -------------------------------------------------------------------------- */
126
- /* Workspace globbing */
127
- /* -------------------------------------------------------------------------- */
128
-
76
+ // ---------- workspace glob matching (supports *, **, and plain segments) ----------
129
77
  function matchSegment(patternSeg: string, name: string): boolean {
130
78
  if (patternSeg === '*') return true;
131
79
  if (!patternSeg.includes('*')) return patternSeg === name;
132
80
  const escaped = patternSeg.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
133
- return new RegExp(`^${escaped.replaceAll('*', '.*')}$`).test(name);
81
+ const regex = new RegExp('^' + escaped.replaceAll('*', '.*') + '$');
82
+ return regex.test(name);
134
83
  }
135
84
 
136
85
  async function expandWorkspacePattern(
137
86
  root: string,
138
87
  pattern: string
139
88
  ): Promise<string[]> {
89
+ console.log(
90
+ `[expandWorkspacePattern] Expanding pattern:`,
91
+ pattern,
92
+ `from root:`,
93
+ root
94
+ );
140
95
  const segs = toPosix(pattern).split('/').filter(Boolean);
141
96
 
142
- async function expandFrom(dir: string, idx: number): Promise<string[]> {
143
- if (idx >= segs.length) return [dir];
144
-
145
- const entries = await fs
146
- .readdir(dir, { withFileTypes: true })
147
- .catch(() => []);
148
-
149
- const seg = segs[idx];
97
+ async function expandFrom(dir: string, segIndex: number): Promise<string[]> {
98
+ console.log(`[expandFrom] Directory:`, dir, `Segment index:`, segIndex);
99
+ if (segIndex >= segs.length) return [dir];
100
+ const seg = segs[segIndex];
101
+ console.log(`[expandFrom] Segment:`, seg);
150
102
 
151
103
  if (seg === '**') {
152
- const nested = await Promise.all(
153
- entries
154
- .filter((e) => e.isDirectory())
155
- .map((e) => expandFrom(path.join(dir, e.name), idx))
104
+ const results: string[] = [];
105
+ results.push(...(await expandFrom(dir, segIndex + 1)));
106
+ const entries = await fs
107
+ .readdir(dir, { withFileTypes: true })
108
+ .catch(() => []);
109
+ console.log(
110
+ `[expandFrom] '**' entries in ${dir}:`,
111
+ entries.map((e) => e.name)
156
112
  );
157
-
158
- return [...(await expandFrom(dir, idx + 1)), ...nested.flat()];
113
+ for (const e of entries) {
114
+ if (!e.isDirectory()) continue;
115
+ console.log(
116
+ `[expandFrom] Recursing into subdir:`,
117
+ path.join(dir, e.name)
118
+ );
119
+ results.push(...(await expandFrom(path.join(dir, e.name), segIndex)));
120
+ }
121
+ return results;
159
122
  }
160
123
 
161
- const nested = await Promise.all(
162
- entries
163
- .filter((e) => e.isDirectory() && matchSegment(seg, e.name))
164
- .map((e) => expandFrom(path.join(dir, e.name), idx + 1))
124
+ const entries = await fs
125
+ .readdir(dir, { withFileTypes: true })
126
+ .catch(() => []);
127
+ console.log(
128
+ `[expandFrom] Entries in ${dir}:`,
129
+ entries.map((e) => e.name)
165
130
  );
166
-
167
- return nested.flat();
131
+ const results: string[] = [];
132
+ for (const e of entries) {
133
+ if (!e.isDirectory()) continue;
134
+ if (!matchSegment(seg, e.name)) continue;
135
+ console.log(
136
+ `[expandFrom] Matched segment '${seg}' with directory:`,
137
+ e.name
138
+ );
139
+ results.push(...(await expandFrom(path.join(dir, e.name), segIndex + 1)));
140
+ }
141
+ return results;
168
142
  }
169
143
 
170
144
  const dirs = await expandFrom(root, 0);
171
-
172
- const pkgDirs = (
173
- await Promise.all(
174
- dirs.map(async (d) =>
175
- (await exists(path.join(d, 'package.json'))) ? d : null
176
- )
177
- )
178
- ).filter(Boolean) as string[];
179
-
145
+ console.log(`[expandWorkspacePattern] Expanded directories:`, dirs);
146
+ const pkgDirs: string[] = [];
147
+ for (const d of dirs) {
148
+ const pkgPath = path.join(d, 'package.json');
149
+ if (await exists(pkgPath)) {
150
+ console.log(`[expandWorkspacePattern] Found package.json:`, pkgPath);
151
+ pkgDirs.push(d);
152
+ } else {
153
+ console.log(`[expandWorkspacePattern] No package.json in:`, d);
154
+ }
155
+ }
156
+ console.log(`[expandWorkspacePattern] Final package directories:`, pkgDirs);
180
157
  return [...new Set(pkgDirs)];
181
158
  }
182
159
 
@@ -184,23 +161,46 @@ async function findWorkspacePackageDirs(
184
161
  repoRoot: string,
185
162
  workspacePatterns: string[]
186
163
  ): Promise<string[]> {
187
- const resolved = await Promise.all(
188
- workspacePatterns.map((p) => expandWorkspacePattern(repoRoot, p))
164
+ console.log(
165
+ `[findWorkspacePackageDirs] repoRoot:`,
166
+ repoRoot,
167
+ `workspacePatterns:`,
168
+ workspacePatterns
189
169
  );
190
- return [...new Set(resolved.flat())];
170
+ const dirs: string[] = [];
171
+ for (const pat of workspacePatterns) {
172
+ console.log(`[findWorkspacePackageDirs] Expanding pattern:`, pat);
173
+ const expanded = await expandWorkspacePattern(repoRoot, pat);
174
+ console.log(
175
+ `[findWorkspacePackageDirs] Expanded dirs for pattern '${pat}':`,
176
+ expanded
177
+ );
178
+ dirs.push(...expanded);
179
+ }
180
+ const uniqueDirs = [...new Set(dirs)];
181
+ console.log(
182
+ `[findWorkspacePackageDirs] Final unique package dirs:`,
183
+ uniqueDirs
184
+ );
185
+ return uniqueDirs;
191
186
  }
192
187
 
193
- /* -------------------------------------------------------------------------- */
194
- /* Mono config + commands */
195
- /* -------------------------------------------------------------------------- */
196
-
188
+ // ---------- .mono parsing ----------
197
189
  async function readMonoConfig(): Promise<MonoConfig | null> {
198
190
  const configPath = path.join(MONO_DIR, 'config.json');
199
- if (!(await exists(configPath))) return null;
200
- return {
201
- path: configPath,
202
- config: await readJson<MonoConfig['config']>(configPath),
203
- };
191
+ console.log(`[readMonoConfig] Looking for mono config at:`, configPath);
192
+ if (!(await exists(configPath))) {
193
+ console.log(`[readMonoConfig] No mono config found.`);
194
+ return null;
195
+ }
196
+ try {
197
+ const config = await readJson<any>(configPath);
198
+ console.log(`[readMonoConfig] Loaded mono config.`);
199
+ return { path: configPath, config };
200
+ } catch (err) {
201
+ console.error(`[readMonoConfig] Failed to load mono config:`, err);
202
+ return null;
203
+ }
204
204
  }
205
205
 
206
206
  function commandNameFromFile(filePath: string): string {
@@ -208,68 +208,390 @@ function commandNameFromFile(filePath: string): string {
208
208
  }
209
209
 
210
210
  async function readMonoCommands(): Promise<MonoCommand[]> {
211
- if (!(await exists(MONO_DIR))) return [];
212
-
211
+ console.log(`[readMonoCommands] Reading mono commands from:`, MONO_DIR);
212
+ if (!(await exists(MONO_DIR))) {
213
+ console.log(`[readMonoCommands] Mono directory does not exist.`);
214
+ return [];
215
+ }
213
216
  const entries = await listDir(MONO_DIR);
214
217
 
215
- const commands = await Promise.all(
216
- entries
217
- .filter(
218
- (e) =>
219
- e.isFile() && e.name.endsWith('.json') && e.name !== 'config.json'
220
- )
221
- .map(async (e) => {
222
- const file = path.join(MONO_DIR, e.name);
223
- const json = await readJson<MonoCommand['json']>(file);
224
- return {
225
- name: commandNameFromFile(file),
226
- file,
227
- json,
228
- };
229
- })
230
- );
218
+ const jsonFiles = entries
219
+ .filter((e) => e.isFile() && e.name.toLowerCase().endsWith('.json'))
220
+ .map((e) => path.join(MONO_DIR, e.name))
221
+ .filter((p) => path.basename(p).toLowerCase() !== 'config.json');
222
+
223
+ console.log(`[readMonoCommands] Found JSON files:`, jsonFiles);
224
+ const commands: MonoCommand[] = [];
225
+ for (const file of jsonFiles) {
226
+ try {
227
+ console.log(`[readMonoCommands] Reading command file:`, file);
228
+ const j = await readJson<any>(file);
229
+ commands.push({
230
+ name: commandNameFromFile(file),
231
+ file,
232
+ json: j,
233
+ });
234
+ console.log(
235
+ `[readMonoCommands] Successfully loaded command:`,
236
+ commandNameFromFile(file)
237
+ );
238
+ } catch (err) {
239
+ console.error(
240
+ `[readMonoCommands] Failed to load command file:`,
241
+ file,
242
+ err
243
+ );
244
+ // skip invalid json
245
+ }
246
+ }
231
247
 
232
- return commands.sort((a, b) => a.name.localeCompare(b.name));
248
+ commands.sort((a, b) => a.name.localeCompare(b.name));
249
+ console.log(
250
+ `[readMonoCommands] Final sorted commands:`,
251
+ commands.map((c) => c.name)
252
+ );
253
+ return commands;
233
254
  }
234
255
 
235
- /* -------------------------------------------------------------------------- */
236
- /* Options parsing */
237
- /* -------------------------------------------------------------------------- */
256
+ // ---------- mono docs formatting ----------
257
+ type OptionSchema = {
258
+ key: string;
259
+ kind: 'boolean' | 'value';
260
+ type: string;
261
+ description: string;
262
+ shortcut: string;
263
+ default: any;
264
+ allowed: string[] | null;
265
+ allowAll: boolean;
266
+ };
267
+
268
+ type MonoConfig = {
269
+ path: string;
270
+ config: any;
271
+ };
238
272
 
239
273
  function parseOptionsSchema(optionsObj: unknown): OptionSchema[] {
274
+ // New structure supports:
275
+ // - optionKey: { type: "string", default, options: [], allowAll, shortcut, description }
276
+ // - boolean toggle: { shortcut, description } (no type)
240
277
  if (!isObject(optionsObj)) return [];
241
278
 
242
- return Object.entries(optionsObj)
243
- .map(([key, raw]) => {
279
+ const entries: OptionSchema[] = Object.entries(optionsObj).map(
280
+ ([key, raw]) => {
244
281
  const o = isObject(raw) ? raw : {};
245
- const hasType = typeof o.type === 'string';
246
-
282
+ const hasType = typeof o.type === 'string' && o.type.trim().length > 0;
283
+ const isBoolToggle = !hasType; // in your examples, booleans omit `type`
247
284
  return {
248
285
  key,
249
- kind: hasType ? ('value' as const) : ('boolean' as const),
250
- type: hasType ? (o.type as string) : 'boolean',
286
+ kind: isBoolToggle ? 'boolean' : 'value',
287
+ type: hasType ? String(o.type) : 'boolean',
251
288
  description: typeof o.description === 'string' ? o.description : '',
252
289
  shortcut: typeof o.shortcut === 'string' ? o.shortcut : '',
253
290
  default: o.default,
254
- allowed:
255
- Array.isArray(o.options) ?
256
- o.options.filter((x): x is string => typeof x === 'string')
257
- : null,
291
+ allowed: Array.isArray(o.options) ? o.options : null,
258
292
  allowAll: o.allowAll === true,
259
293
  };
260
- })
261
- .sort((a, b) => a.key.localeCompare(b.key));
294
+ }
295
+ );
296
+
297
+ entries.sort((a, b) => a.key.localeCompare(b.key));
298
+ return entries;
262
299
  }
263
300
 
264
- /* -------------------------------------------------------------------------- */
265
- /* Main */
266
- /* -------------------------------------------------------------------------- */
301
+ function buildUsageExample(
302
+ commandName: string,
303
+ cmdJson: any,
304
+ options: OptionSchema[]
305
+ ): string {
306
+ const arg = cmdJson?.argument;
307
+ const hasArg = isObject(arg);
308
+ const argToken = hasArg ? `<${commandName}-arg>` : '';
309
+
310
+ // choose a representative value option to show
311
+ const valueOpts = options.filter((o) => o.kind === 'value');
312
+ const boolOpts = options.filter((o) => o.kind === 'boolean');
313
+
314
+ const exampleParts = [`yarn mono ${commandName}`];
315
+ if (argToken) exampleParts.push(argToken);
316
+
317
+ // include at most 2 value options and 1 boolean in the example for readability
318
+ for (const o of valueOpts.slice(0, 2)) {
319
+ const flag = `--${o.key}`;
320
+ const val =
321
+ o.default !== undefined ? o.default : (o.allowed?.[0] ?? '<value>');
322
+ exampleParts.push(`${flag} ${val}`);
323
+ }
324
+ if (boolOpts.length) {
325
+ exampleParts.push(`--${boolOpts[0].key}`);
326
+ }
267
327
 
268
- async function main(): Promise<void> {
269
- if (!(await exists(ROOT_PKG_JSON))) {
270
- throw new Error(`Missing: ${ROOT_PKG_JSON}`);
328
+ return exampleParts.join(' ');
329
+ }
330
+
331
+ function formatMonoConfigSection(monoConfig: MonoConfig | null): string {
332
+ const lines: string[] = [];
333
+ lines.push('## Mono configuration');
334
+ lines.push('');
335
+
336
+ if (!monoConfig) {
337
+ lines.push('_No `.mono/config.json` found._');
338
+ return lines.join('\n');
339
+ }
340
+
341
+ const c = monoConfig.config;
342
+ lines.push(
343
+ `Source: \`${toPosix(path.relative(REPO_ROOT, monoConfig.path))}\``
344
+ );
345
+ lines.push('');
346
+
347
+ if (Array.isArray(c.envMap) && c.envMap.length) {
348
+ lines.push('### envMap');
349
+ lines.push('');
350
+ lines.push(
351
+ '- ' + c.envMap.map((x: string) => `\`${mdEscapeInline(x)}\``).join(', ')
352
+ );
353
+ lines.push('');
271
354
  }
272
355
 
356
+ const pkgMaps = c?.workspace?.packageMaps;
357
+ if (pkgMaps && isObject(pkgMaps) && Object.keys(pkgMaps).length) {
358
+ lines.push('### Workspace aliases (packageMaps)');
359
+ lines.push('');
360
+ const entries = Object.entries(pkgMaps).sort(([a], [b]) =>
361
+ a.localeCompare(b)
362
+ );
363
+ for (const [alias, target] of entries) {
364
+ lines.push(
365
+ `- \`${mdEscapeInline(alias)}\` → \`${mdEscapeInline(String(target))}\``
366
+ );
367
+ }
368
+ lines.push('');
369
+ }
370
+
371
+ const pre = c?.workspace?.preactions;
372
+ if (Array.isArray(pre) && pre.length) {
373
+ lines.push('### Global preactions');
374
+ lines.push('');
375
+ lines.push('```bash');
376
+ for (const p of pre) lines.push(String(p));
377
+ lines.push('```');
378
+ lines.push('');
379
+ }
380
+
381
+ if (typeof c.prodFlag === 'string' && c.prodFlag.trim()) {
382
+ lines.push('### prodFlag');
383
+ lines.push('');
384
+ lines.push(
385
+ `Production flag keyword: \`${mdEscapeInline(c.prodFlag.trim())}\``
386
+ );
387
+ lines.push('');
388
+ }
389
+
390
+ return lines.join('\n');
391
+ }
392
+
393
+ type MonoCommand = {
394
+ name: string;
395
+ file: string;
396
+ json: any;
397
+ };
398
+
399
+ function formatMonoCommandsSection(commands: MonoCommand[]): string {
400
+ const lines: string[] = [];
401
+ lines.push('## Mono commands');
402
+ lines.push('');
403
+ lines.push(
404
+ 'Generated from `.mono/*.json` (excluding `config.json`). Each filename becomes a command:'
405
+ );
406
+ lines.push('');
407
+ lines.push('```bash');
408
+ lines.push('yarn mono <command> [argument] [--options]');
409
+ lines.push('```');
410
+ lines.push('');
411
+
412
+ if (!commands.length) {
413
+ lines.push('_No mono command JSON files found._');
414
+ return lines.join('\n');
415
+ }
416
+
417
+ // Index
418
+ lines.push('### Command index');
419
+ lines.push('');
420
+ for (const c of commands) {
421
+ const desc =
422
+ typeof c.json?.description === 'string' ? c.json.description.trim() : '';
423
+ const suffix = desc ? ` — ${desc}` : '';
424
+ lines.push(
425
+ `- [\`${mdEscapeInline(c.name)}\`](#mono-command-${mdEscapeInline(c.name).toLowerCase()})${suffix}`
426
+ );
427
+ }
428
+ lines.push('');
429
+
430
+ for (const c of commands) {
431
+ const j = c.json || {};
432
+ const rel = toPosix(path.relative(REPO_ROOT, c.file));
433
+ const anchor = `mono-command-${c.name.toLowerCase()}`;
434
+
435
+ const desc = typeof j.description === 'string' ? j.description.trim() : '';
436
+ const arg = j.argument;
437
+ const options = parseOptionsSchema(j.options);
438
+
439
+ lines.push('---');
440
+ lines.push(`### Mono command: ${c.name}`);
441
+ lines.push(`<a id="${anchor}"></a>`);
442
+ lines.push('');
443
+ lines.push(`Source: \`${rel}\``);
444
+ lines.push('');
445
+
446
+ if (desc) {
447
+ lines.push(`**Description:** ${mdEscapeInline(desc)}`);
448
+ lines.push('');
449
+ }
450
+
451
+ // Usage
452
+ lines.push('**Usage**');
453
+ lines.push('');
454
+ lines.push('```bash');
455
+ lines.push(
456
+ `yarn mono ${c.name}${isObject(arg) ? ` <${c.name}-arg>` : ''} [--options]`
457
+ );
458
+ lines.push('```');
459
+ lines.push('');
460
+ lines.push('Example:');
461
+ lines.push('');
462
+ lines.push('```bash');
463
+ lines.push(buildUsageExample(c.name, j, options));
464
+ lines.push('```');
465
+ lines.push('');
466
+
467
+ // Argument
468
+ if (isObject(arg)) {
469
+ lines.push('**Argument**');
470
+ lines.push('');
471
+ const bits: string[] = [];
472
+ if (typeof arg.type === 'string')
473
+ bits.push(`type: \`${mdEscapeInline(arg.type)}\``);
474
+ if (arg.default !== undefined)
475
+ bits.push(`default: \`${mdEscapeInline(String(arg.default))}\``);
476
+ if (typeof arg.description === 'string')
477
+ bits.push(mdEscapeInline(arg.description));
478
+ lines.push(`- ${bits.join(' • ') || '_(no details)_'} `);
479
+ lines.push('');
480
+ }
481
+
482
+ // Options
483
+ if (options.length) {
484
+ lines.push('**Options**');
485
+ lines.push('');
486
+ lines.push('| Option | Type | Shortcut | Default | Allowed | Notes |');
487
+ lines.push('|---|---:|:---:|---:|---|---|');
488
+ for (const o of options) {
489
+ const optCol =
490
+ o.kind === 'boolean' ?
491
+ `\`--${mdEscapeInline(o.key)}\``
492
+ : `\`--${mdEscapeInline(o.key)} <${mdEscapeInline(o.key)}>\``;
493
+ const typeCol = `\`${mdEscapeInline(o.type)}\``;
494
+ const shortCol = o.shortcut ? `\`-${mdEscapeInline(o.shortcut)}\`` : '';
495
+ const defCol =
496
+ o.default !== undefined ? `\`${mdEscapeInline(o.default)}\`` : '';
497
+ const allowedCol =
498
+ o.allowed ?
499
+ o.allowed.map((x) => `\`${mdEscapeInline(x)}\``).join(', ')
500
+ : '';
501
+ const notes = [
502
+ o.allowAll ? 'allowAll' : '',
503
+ o.description ? mdEscapeInline(o.description) : '',
504
+ ]
505
+ .filter(Boolean)
506
+ .join(' • ');
507
+ lines.push(
508
+ `| ${optCol} | ${typeCol} | ${shortCol} | ${defCol} | ${allowedCol} | ${notes} |`
509
+ );
510
+ }
511
+ lines.push('');
512
+ }
513
+
514
+ // Environments
515
+ if (
516
+ j.environments &&
517
+ isObject(j.environments) &&
518
+ Object.keys(j.environments).length
519
+ ) {
520
+ lines.push('**Environment Variables**');
521
+ lines.push('');
522
+ const envs = Object.entries(j.environments).sort(([a], [b]) =>
523
+ a.localeCompare(b)
524
+ );
525
+ for (const [envName, envObj] of envs) {
526
+ lines.push(`- \`${mdEscapeInline(envName)}\``);
527
+ if (isObject(envObj) && Object.keys(envObj).length) {
528
+ const kv = Object.entries(envObj).sort(([a], [b]) =>
529
+ a.localeCompare(b)
530
+ );
531
+ lines.push(
532
+ indentLines(
533
+ kv
534
+ .map(
535
+ ([k, v]) =>
536
+ `- \`${mdEscapeInline(k)}\` = \`${mdEscapeInline(String(v))}\``
537
+ )
538
+ .join('\n'),
539
+ 2
540
+ )
541
+ );
542
+ }
543
+ }
544
+ lines.push('');
545
+ }
546
+
547
+ // preactions/actions
548
+ if (Array.isArray(j.preactions) && j.preactions.length) {
549
+ lines.push('**Preactions**');
550
+ lines.push('');
551
+ lines.push('```bash');
552
+ for (const p of j.preactions) lines.push(String(p));
553
+ lines.push('```');
554
+ lines.push('');
555
+ }
556
+
557
+ if (Array.isArray(j.actions) && j.actions.length) {
558
+ lines.push('**Actions**');
559
+ lines.push('');
560
+ lines.push('```bash');
561
+ for (const a of j.actions) lines.push(String(a));
562
+ lines.push('```');
563
+ lines.push('');
564
+ }
565
+ }
566
+
567
+ return lines.join('\n');
568
+ }
569
+
570
+ // ---------- workspace scripts summary ----------
571
+
572
+ // Define PackageInfo type
573
+ type PackageInfo = {
574
+ name: string;
575
+ dir: string;
576
+ scripts: Record<string, string>;
577
+ };
578
+
579
+ function collectScripts(packages: PackageInfo[]): Map<string, string[]> {
580
+ const scriptToPackages = new Map<string, string[]>();
581
+ for (const p of packages) {
582
+ for (const scriptName of Object.keys(p.scripts || {})) {
583
+ if (!scriptToPackages.has(scriptName))
584
+ scriptToPackages.set(scriptName, []);
585
+ scriptToPackages.get(scriptName)!.push(p.name);
586
+ }
587
+ }
588
+ return scriptToPackages;
589
+ }
590
+
591
+ // ---------- main ----------
592
+ async function main(): Promise<void> {
593
+ if (!(await exists(ROOT_PKG_JSON)))
594
+ throw new Error(`Missing: ${ROOT_PKG_JSON}`);
273
595
  await ensureParentDir(OUTPUT_PATH);
274
596
 
275
597
  const rootPkg = await readJson<any>(ROOT_PKG_JSON);
@@ -277,47 +599,59 @@ async function main(): Promise<void> {
277
599
 
278
600
  const monoConfig = await readMonoConfig();
279
601
  const monoCommands = await readMonoCommands();
280
- const pkgDirs = await findWorkspacePackageDirs(REPO_ROOT, workspacePatterns);
281
602
 
282
- const packages: PackageInfo[] = await Promise.all(
283
- pkgDirs.map(async (dir) => {
284
- const pj = await readJson<any>(path.join(dir, 'package.json'));
285
- return {
603
+ const pkgDirs = await findWorkspacePackageDirs(REPO_ROOT, workspacePatterns);
604
+ console.log(`[main] Package directories found:`, pkgDirs);
605
+ const packages: PackageInfo[] = [];
606
+ for (const dir of pkgDirs) {
607
+ try {
608
+ const pkgPath = path.join(dir, 'package.json');
609
+ console.log(`[main] Reading package.json:`, pkgPath);
610
+ const pj = await readJson<any>(pkgPath);
611
+ packages.push({
286
612
  name:
287
613
  pj.name ||
288
614
  toPosix(path.relative(REPO_ROOT, dir)) ||
289
615
  path.basename(dir),
290
616
  dir,
291
- scripts: pj.scripts ?? {},
292
- };
293
- })
294
- );
617
+ scripts: pj.scripts || {},
618
+ });
619
+ console.log(`[main] Loaded package:`, pj.name || dir);
620
+ } catch (err) {
621
+ console.error(`[main] Failed to load package.json for:`, dir, err);
622
+ // skip
623
+ }
624
+ }
295
625
 
296
626
  const parts: string[] = [];
297
-
298
- parts.push(`# Mono Command-Line Reference
299
-
627
+ parts.push(`# ⚙️ Command Line Reference
628
+
300
629
  > Generated by \`scripts/generate-readme.mjs\`.
301
630
  > Update \`.mono/config.json\`, \`.mono/*.json\`, and workspace package scripts to change this output.
302
631
 
303
632
  `);
633
+ parts.push(formatMonoConfigSection(monoConfig));
634
+ parts.push('');
635
+ parts.push(formatMonoCommandsSection(monoCommands));
636
+ parts.push('');
304
637
 
305
- const docsIndex = await generateDocsIndex({
638
+ const val = await generateDocsIndex({
306
639
  docsDir: path.join(REPO_ROOT, 'docs'),
307
640
  excludeFile: 'command-line.md',
308
641
  });
309
642
 
310
- parts.push(docsIndex);
643
+ val.split('\n').forEach((line) => parts.push(line));
311
644
 
645
+ await ensureParentDir(OUTPUT_README);
312
646
  await fs.writeFile(OUTPUT_README, parts.join('\n'), 'utf8');
313
647
 
314
- console.log(`Generated: ${OUTPUT_README}`);
315
- console.log(`- mono config: ${monoConfig ? 'yes' : 'no'}`);
316
- console.log(`- mono commands: ${monoCommands.length}`);
317
- console.log(`- workspace packages: ${packages.length}`);
648
+ console.log(`[main] Generated: ${OUTPUT_README}`);
649
+ console.log(`[main] mono config: ${monoConfig ? 'yes' : 'no'}`);
650
+ console.log(`[main] mono commands: ${monoCommands.length}`);
651
+ console.log(`[main] workspace packages: ${packages.length}`);
318
652
  }
319
653
 
320
654
  main().catch((err) => {
321
- console.error(err);
655
+ console.error(err?.stack || String(err));
322
656
  process.exitCode = 1;
323
657
  });