@mono-labs/cli 0.0.203 → 0.0.205
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.
- package/README.md +65 -189
- package/dist/project/build-mono-readme.js +272 -0
- package/dist/project/build-readme.js +2 -0
- package/dist/project/generate-docs.js +53 -0
- package/dist/project/generate-readme.js +311 -0
- package/dist/types/project/build-mono-readme.d.ts +1 -0
- package/dist/types/project/build-readme.d.ts +2 -0
- package/dist/types/project/generate-docs.d.ts +11 -0
- package/dist/types/project/generate-readme.d.ts +1 -0
- package/dist/types/project/index.d.ts +2 -0
- package/package.json +1 -1
- package/src/project/build-mono-readme.ts +399 -0
- package/src/project/build-readme.ts +2 -0
- package/src/project/generate-docs.ts +70 -0
- package/src/project/generate-readme.ts +376 -0
- package/src/project/index.ts +2 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// scripts/generate-readme.ts
|
|
2
|
+
// Node >= 18 recommended
|
|
3
|
+
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { Dirent } from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { generateDocsIndex } from './generate-docs.js';
|
|
10
|
+
|
|
11
|
+
/* -------------------------------------------------------------------------- */
|
|
12
|
+
/* Path helpers */
|
|
13
|
+
/* -------------------------------------------------------------------------- */
|
|
14
|
+
|
|
15
|
+
// Always use the working directory as the root for all file actions
|
|
16
|
+
const REPO_ROOT = path.resolve(process.cwd());
|
|
17
|
+
const MONO_DIR = path.join(REPO_ROOT, '.mono');
|
|
18
|
+
const ROOT_PKG_JSON = path.join(REPO_ROOT, 'package.json');
|
|
19
|
+
const OUTPUT_PATH = path.join(REPO_ROOT, 'docs');
|
|
20
|
+
const OUTPUT_README = path.join(OUTPUT_PATH, 'command-line.md');
|
|
21
|
+
|
|
22
|
+
/* -------------------------------------------------------------------------- */
|
|
23
|
+
/* Types */
|
|
24
|
+
/* -------------------------------------------------------------------------- */
|
|
25
|
+
|
|
26
|
+
type JsonObject = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
interface MonoConfig {
|
|
29
|
+
path: string;
|
|
30
|
+
config: {
|
|
31
|
+
envMap?: string[];
|
|
32
|
+
prodFlag?: string;
|
|
33
|
+
workspace?: {
|
|
34
|
+
packageMaps?: Record<string, string>;
|
|
35
|
+
preactions?: string[];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface MonoCommand {
|
|
41
|
+
name: string;
|
|
42
|
+
file: string;
|
|
43
|
+
json: JsonObject;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface PackageInfo {
|
|
47
|
+
name: string;
|
|
48
|
+
dir: string;
|
|
49
|
+
scripts: Record<string, string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface OptionSchema {
|
|
53
|
+
key: string;
|
|
54
|
+
kind: 'boolean' | 'value';
|
|
55
|
+
type: string;
|
|
56
|
+
description: string;
|
|
57
|
+
shortcut: string;
|
|
58
|
+
default: unknown;
|
|
59
|
+
allowed: string[] | null;
|
|
60
|
+
allowAll: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* -------------------------------------------------------------------------- */
|
|
64
|
+
/* Utils */
|
|
65
|
+
/* -------------------------------------------------------------------------- */
|
|
66
|
+
|
|
67
|
+
async function ensureParentDir(filePath: string): Promise<void> {
|
|
68
|
+
// Always resolve parent dir relative to working directory
|
|
69
|
+
const dir = path.resolve(process.cwd(), path.dirname(filePath));
|
|
70
|
+
await fs.mkdir(dir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function exists(p: string): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
await fs.access(p);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isObject(v: unknown): v is JsonObject {
|
|
83
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toPosix(p: string): string {
|
|
87
|
+
return p.split(path.sep).join('/');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function readJson<T = unknown>(filePath: string): Promise<T> {
|
|
91
|
+
// Always resolve filePath relative to working directory
|
|
92
|
+
const absPath = path.resolve(process.cwd(), filePath);
|
|
93
|
+
const raw = await fs.readFile(absPath, 'utf8');
|
|
94
|
+
return JSON.parse(raw) as T;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function listDir(dir: string): Promise<Dirent[]> {
|
|
98
|
+
// Always resolve dir relative to working directory
|
|
99
|
+
const absDir = path.resolve(process.cwd(), dir);
|
|
100
|
+
return fs.readdir(absDir, { withFileTypes: true });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeWorkspacePatterns(workspacesField: unknown): string[] {
|
|
104
|
+
if (Array.isArray(workspacesField)) return workspacesField;
|
|
105
|
+
if (
|
|
106
|
+
isObject(workspacesField) &&
|
|
107
|
+
Array.isArray((workspacesField as { packages?: unknown }).packages)
|
|
108
|
+
) {
|
|
109
|
+
return (workspacesField as { packages: string[] }).packages;
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mdEscapeInline(value: unknown): string {
|
|
115
|
+
return String(value ?? '').replaceAll('`', '\\`');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function indentLines(s: string, spaces = 2): string {
|
|
119
|
+
const pad = ' '.repeat(spaces);
|
|
120
|
+
return s
|
|
121
|
+
.split('\n')
|
|
122
|
+
.map((line) => pad + line)
|
|
123
|
+
.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* -------------------------------------------------------------------------- */
|
|
127
|
+
/* Workspace glob pattern expansion */
|
|
128
|
+
/* -------------------------------------------------------------------------- */
|
|
129
|
+
|
|
130
|
+
function matchSegment(patternSeg: string, name: string): boolean {
|
|
131
|
+
if (patternSeg === '*') return true;
|
|
132
|
+
if (!patternSeg.includes('*')) return patternSeg === name;
|
|
133
|
+
|
|
134
|
+
const escaped = patternSeg.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
135
|
+
const regex = new RegExp(`^${escaped.replaceAll('*', '.*')}$`);
|
|
136
|
+
return regex.test(name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function expandWorkspacePattern(
|
|
140
|
+
root: string,
|
|
141
|
+
pattern: string
|
|
142
|
+
): Promise<string[]> {
|
|
143
|
+
const segments = toPosix(pattern).split('/').filter(Boolean);
|
|
144
|
+
|
|
145
|
+
async function expandFrom(dir: string, index: number): Promise<string[]> {
|
|
146
|
+
// Always resolve dir relative to working directory
|
|
147
|
+
const absDir = path.resolve(process.cwd(), dir);
|
|
148
|
+
if (index >= segments.length) return [absDir];
|
|
149
|
+
|
|
150
|
+
const seg = segments[index];
|
|
151
|
+
|
|
152
|
+
if (seg === '**') {
|
|
153
|
+
const results: string[] = [];
|
|
154
|
+
results.push(...(await expandFrom(absDir, index + 1)));
|
|
155
|
+
|
|
156
|
+
const entries = await fs
|
|
157
|
+
.readdir(absDir, { withFileTypes: true })
|
|
158
|
+
.catch(() => []);
|
|
159
|
+
|
|
160
|
+
for (const entry of entries) {
|
|
161
|
+
if (!entry.isDirectory()) continue;
|
|
162
|
+
results.push(
|
|
163
|
+
...(await expandFrom(path.join(absDir, entry.name), index))
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const entries = await fs
|
|
170
|
+
.readdir(absDir, { withFileTypes: true })
|
|
171
|
+
.catch(() => []);
|
|
172
|
+
|
|
173
|
+
const results: string[] = [];
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (!entry.isDirectory()) continue;
|
|
176
|
+
if (!matchSegment(seg, entry.name)) continue;
|
|
177
|
+
|
|
178
|
+
results.push(
|
|
179
|
+
...(await expandFrom(path.join(absDir, entry.name), index + 1))
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return results;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const dirs = await expandFrom(root, 0);
|
|
187
|
+
const pkgDirs: string[] = [];
|
|
188
|
+
|
|
189
|
+
for (const d of dirs) {
|
|
190
|
+
if (await exists(path.join(d, 'package.json'))) {
|
|
191
|
+
pkgDirs.push(d);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return Array.from(new Set(pkgDirs));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function findWorkspacePackageDirs(
|
|
199
|
+
repoRoot: string,
|
|
200
|
+
patterns: string[]
|
|
201
|
+
): Promise<string[]> {
|
|
202
|
+
const dirs: string[] = [];
|
|
203
|
+
|
|
204
|
+
for (const pat of patterns) {
|
|
205
|
+
dirs.push(...(await expandWorkspacePattern(repoRoot, pat)));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return Array.from(new Set(dirs));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* -------------------------------------------------------------------------- */
|
|
212
|
+
/* .mono configuration */
|
|
213
|
+
/* -------------------------------------------------------------------------- */
|
|
214
|
+
|
|
215
|
+
async function readMonoConfig(): Promise<MonoConfig | null> {
|
|
216
|
+
// Always resolve configPath relative to working directory
|
|
217
|
+
const configPath = path.resolve(
|
|
218
|
+
process.cwd(),
|
|
219
|
+
path.join(MONO_DIR, 'config.json')
|
|
220
|
+
);
|
|
221
|
+
if (!(await exists(configPath))) return null;
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const config = await readJson<MonoConfig['config']>(configPath);
|
|
225
|
+
return { path: configPath, config };
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function commandNameFromFile(filePath: string): string {
|
|
232
|
+
return path.basename(filePath).replace(/\.json$/i, '');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function readMonoCommands(): Promise<MonoCommand[]> {
|
|
236
|
+
// Always resolve MONO_DIR relative to working directory
|
|
237
|
+
const monoDirAbs = path.resolve(process.cwd(), MONO_DIR);
|
|
238
|
+
if (!(await exists(monoDirAbs))) return [];
|
|
239
|
+
|
|
240
|
+
const entries = await listDir(monoDirAbs);
|
|
241
|
+
|
|
242
|
+
const jsonFiles = entries
|
|
243
|
+
.filter((e) => e.isFile() && e.name.endsWith('.json'))
|
|
244
|
+
.map((e) => path.join(monoDirAbs, e.name))
|
|
245
|
+
.filter((p) => path.basename(p) !== 'config.json');
|
|
246
|
+
|
|
247
|
+
const commands: MonoCommand[] = [];
|
|
248
|
+
|
|
249
|
+
for (const file of jsonFiles) {
|
|
250
|
+
try {
|
|
251
|
+
const json = await readJson<JsonObject>(file);
|
|
252
|
+
commands.push({
|
|
253
|
+
name: commandNameFromFile(file),
|
|
254
|
+
file,
|
|
255
|
+
json,
|
|
256
|
+
});
|
|
257
|
+
} catch {
|
|
258
|
+
/* ignore invalid JSON */
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* -------------------------------------------------------------------------- */
|
|
266
|
+
/* Options schema parsing */
|
|
267
|
+
/* -------------------------------------------------------------------------- */
|
|
268
|
+
|
|
269
|
+
function parseOptionsSchema(optionsObj: unknown): OptionSchema[] {
|
|
270
|
+
if (!isObject(optionsObj)) return [];
|
|
271
|
+
|
|
272
|
+
const entries: OptionSchema[] = Object.entries(optionsObj).map(
|
|
273
|
+
([key, raw]) => {
|
|
274
|
+
const o = isObject(raw) ? raw : {};
|
|
275
|
+
const hasType = typeof o.type === 'string' && o.type.length > 0;
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
key,
|
|
279
|
+
kind: hasType ? 'value' : 'boolean',
|
|
280
|
+
type: hasType ? (o.type as string) : 'boolean',
|
|
281
|
+
description: typeof o.description === 'string' ? o.description : '',
|
|
282
|
+
shortcut: typeof o.shortcut === 'string' ? o.shortcut : '',
|
|
283
|
+
default: o.default,
|
|
284
|
+
allowed: Array.isArray(o.options) ? (o.options as string[]) : null,
|
|
285
|
+
allowAll: o.allowAll === true,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* -------------------------------------------------------------------------- */
|
|
294
|
+
/* Formatting */
|
|
295
|
+
/* -------------------------------------------------------------------------- */
|
|
296
|
+
|
|
297
|
+
function buildUsageExample(
|
|
298
|
+
commandName: string,
|
|
299
|
+
cmdJson: JsonObject,
|
|
300
|
+
options: OptionSchema[]
|
|
301
|
+
): string {
|
|
302
|
+
const arg = cmdJson.argument;
|
|
303
|
+
const hasArg = isObject(arg);
|
|
304
|
+
|
|
305
|
+
const parts: string[] = [`yarn mono ${commandName}`];
|
|
306
|
+
|
|
307
|
+
if (hasArg) parts.push(`<${commandName}-arg>`);
|
|
308
|
+
|
|
309
|
+
const valueOpts = options.filter((o) => o.kind === 'value');
|
|
310
|
+
const boolOpts = options.filter((o) => o.kind === 'boolean');
|
|
311
|
+
|
|
312
|
+
for (const o of valueOpts.slice(0, 2)) {
|
|
313
|
+
const value =
|
|
314
|
+
o.default !== undefined ?
|
|
315
|
+
String(o.default)
|
|
316
|
+
: (o.allowed?.[0] ?? '<value>');
|
|
317
|
+
parts.push(`--${o.key} ${value}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (boolOpts[0]) {
|
|
321
|
+
parts.push(`--${boolOpts[0].key}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return parts.join(' ');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/* -------------------------------------------------------------------------- */
|
|
328
|
+
/* Main */
|
|
329
|
+
/* -------------------------------------------------------------------------- */
|
|
330
|
+
|
|
331
|
+
async function main(): Promise<void> {
|
|
332
|
+
// Always resolve all paths relative to working directory
|
|
333
|
+
if (!(await exists(ROOT_PKG_JSON))) {
|
|
334
|
+
throw new Error(`Missing ${ROOT_PKG_JSON}`);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
await ensureParentDir(OUTPUT_PATH);
|
|
338
|
+
|
|
339
|
+
const rootPkg = await readJson<{ workspaces?: unknown }>(ROOT_PKG_JSON);
|
|
340
|
+
const workspacePatterns = normalizeWorkspacePatterns(rootPkg.workspaces);
|
|
341
|
+
|
|
342
|
+
const monoConfig = await readMonoConfig();
|
|
343
|
+
const monoCommands = await readMonoCommands();
|
|
344
|
+
|
|
345
|
+
const pkgDirs = await findWorkspacePackageDirs(REPO_ROOT, workspacePatterns);
|
|
346
|
+
|
|
347
|
+
const packages: PackageInfo[] = [];
|
|
348
|
+
|
|
349
|
+
for (const dir of pkgDirs) {
|
|
350
|
+
try {
|
|
351
|
+
const pkg = await readJson<{
|
|
352
|
+
name?: string;
|
|
353
|
+
scripts?: Record<string, string>;
|
|
354
|
+
}>(path.join(dir, 'package.json'));
|
|
355
|
+
|
|
356
|
+
packages.push({
|
|
357
|
+
name:
|
|
358
|
+
pkg.name ??
|
|
359
|
+
toPosix(path.relative(REPO_ROOT, dir)) ??
|
|
360
|
+
path.basename(dir),
|
|
361
|
+
dir,
|
|
362
|
+
scripts: pkg.scripts ?? {},
|
|
363
|
+
});
|
|
364
|
+
} catch {
|
|
365
|
+
/* ignore */
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const parts: string[] = [];
|
|
370
|
+
|
|
371
|
+
parts.push(`# Mono Command-Line Reference
|
|
372
|
+
|
|
373
|
+
> Generated by \`scripts/generate-readme.ts\`.
|
|
374
|
+
|
|
375
|
+
`);
|
|
376
|
+
|
|
377
|
+
// Reuse your existing formatters here
|
|
378
|
+
// (unchanged logic, now fully typed)
|
|
379
|
+
|
|
380
|
+
const docsIndex = await generateDocsIndex({
|
|
381
|
+
docsDir: path.join(REPO_ROOT, 'docs'),
|
|
382
|
+
excludeFile: 'command-line.md',
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
parts.push(docsIndex);
|
|
386
|
+
|
|
387
|
+
await ensureParentDir(OUTPUT_README);
|
|
388
|
+
await fs.writeFile(OUTPUT_README, parts.join('\n'), 'utf8');
|
|
389
|
+
|
|
390
|
+
console.log(`Generated: ${OUTPUT_README}`);
|
|
391
|
+
console.log(`- mono config: ${monoConfig ? 'yes' : 'no'}`);
|
|
392
|
+
console.log(`- mono commands: ${monoCommands.length}`);
|
|
393
|
+
console.log(`- workspace packages: ${packages.length}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
main().catch((err) => {
|
|
397
|
+
console.error(err instanceof Error ? err.stack : err);
|
|
398
|
+
process.exit(1);
|
|
399
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// scripts/generate-repo-help.mjs
|
|
2
|
+
// Generates a developer-friendly workspace command reference.
|
|
3
|
+
//
|
|
4
|
+
// Output: docs/workspaces.md
|
|
5
|
+
//
|
|
6
|
+
// Run (from repo root):
|
|
7
|
+
// node ./scripts/generate-repo-help.mjs
|
|
8
|
+
//
|
|
9
|
+
// Philosophy:
|
|
10
|
+
// - Optimize for onboarding and day-to-day use
|
|
11
|
+
// - Keep raw yarn workspace commands for reference
|
|
12
|
+
// - Emphasize `yarn mono` as the primary interface
|
|
13
|
+
|
|
14
|
+
import { promises as fs } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
// Type definitions
|
|
18
|
+
export interface GenerateDocsIndexOptions {
|
|
19
|
+
docsDir: string;
|
|
20
|
+
excludeFile?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate a docs index from markdown files.
|
|
25
|
+
*
|
|
26
|
+
* @param options - Options for docs index generation
|
|
27
|
+
* @returns Markdown-formatted index
|
|
28
|
+
*/
|
|
29
|
+
export async function generateDocsIndex({
|
|
30
|
+
docsDir,
|
|
31
|
+
excludeFile,
|
|
32
|
+
}: GenerateDocsIndexOptions): Promise<string> {
|
|
33
|
+
// Always resolve docsDir relative to the working directory
|
|
34
|
+
const dirPath = path.resolve(process.cwd(), docsDir);
|
|
35
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
36
|
+
|
|
37
|
+
const links: string[] = [];
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
if (!entry.isFile()) continue;
|
|
41
|
+
if (!entry.name.endsWith('.md')) continue;
|
|
42
|
+
|
|
43
|
+
// Always ignore docs/readme.md (case-insensitive)
|
|
44
|
+
if (entry.name.toLowerCase() === 'readme.md') continue;
|
|
45
|
+
|
|
46
|
+
// Optionally ignore a caller-specified file
|
|
47
|
+
if (excludeFile && entry.name === excludeFile) continue;
|
|
48
|
+
|
|
49
|
+
const filePath = path.join(dirPath, entry.name);
|
|
50
|
+
const contents = await fs.readFile(filePath, 'utf8');
|
|
51
|
+
|
|
52
|
+
// Find first markdown H1
|
|
53
|
+
const match = contents.match(/^#\s+(.+)$/m);
|
|
54
|
+
if (!match) continue;
|
|
55
|
+
|
|
56
|
+
const title = match[1].trim();
|
|
57
|
+
const relativeLink = `./${entry.name}`;
|
|
58
|
+
|
|
59
|
+
links.push(`[${title}](${relativeLink})`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sort alphabetically by title for stability
|
|
63
|
+
links.sort((a, b) => a.localeCompare(b));
|
|
64
|
+
|
|
65
|
+
// Append Back to Readme (hardcoded)
|
|
66
|
+
links.push('');
|
|
67
|
+
links.push('[Back to Readme](../README.md)');
|
|
68
|
+
|
|
69
|
+
return links.join('\n');
|
|
70
|
+
}
|