@mono-labs/cli 0.0.204 → 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/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/README.md
CHANGED
|
@@ -1,247 +1,123 @@
|
|
|
1
|
-
|
|
1
|
+
# mono-labs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
Declarative, token-aware task runner for JavaScript/TypeScript monorepos.
|
|
6
|
-
Configure commands with simple JSON – no custom scripting required.
|
|
7
|
-
|
|
8
|
-
</div>
|
|
9
|
-
|
|
10
|
-
## Why This Exists
|
|
11
|
-
|
|
12
|
-
You often need a repeatable set of steps to bootstrap or run your full stack
|
|
13
|
-
(web, mobile, backend, infra). Traditional npm scripts become tangled. This CLI
|
|
14
|
-
lets you:
|
|
15
|
-
|
|
16
|
-
- Describe commands in `.mono/*.json` files
|
|
17
|
-
- Emit dynamic values from scripts (`{out:token value}`)
|
|
18
|
-
- Inject those values into later commands & environment variables
|
|
19
|
-
- Run multiple background services + one attached foreground process
|
|
20
|
-
|
|
21
|
-
No publishing needed: you can link and iterate locally.
|
|
3
|
+
Declarative monorepo orchestration, project tooling, and infrastructure
|
|
4
|
+
integration — built to scale real systems, not just scripts.
|
|
22
5
|
|
|
23
6
|
---
|
|
24
7
|
|
|
25
|
-
##
|
|
8
|
+
## What This Is
|
|
26
9
|
|
|
27
|
-
|
|
10
|
+
mono-labs is a monorepo control plane.
|
|
28
11
|
|
|
29
|
-
|
|
30
|
-
yarn install
|
|
31
|
-
```
|
|
12
|
+
It combines:
|
|
32
13
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
```json
|
|
37
|
-
{ "actions": ["echo Hello World"] }
|
|
38
|
-
```
|
|
14
|
+
- a declarative, token-aware CLI runtime
|
|
15
|
+
- project-level orchestration utilities
|
|
16
|
+
- infrastructure and CI integration primitives
|
|
39
17
|
|
|
40
|
-
|
|
18
|
+
The goal is to make a monorepo behave like a single, coordinated system across:
|
|
41
19
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
You should see `Hello World`.
|
|
47
|
-
|
|
48
|
-
### Adding a Command with an Argument
|
|
49
|
-
|
|
50
|
-
```json
|
|
51
|
-
// .mono/greet.json
|
|
52
|
-
{
|
|
53
|
-
"actions": ["echo Hi ${arg}"],
|
|
54
|
-
"argument": { "description": "Name to greet", "default": "friend" }
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
yarn mono greet # Hi friend
|
|
60
|
-
yarn mono greet Alice # Hi Alice
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Adding an Option
|
|
64
|
-
|
|
65
|
-
```json
|
|
66
|
-
// .mono/build.json
|
|
67
|
-
{
|
|
68
|
-
"actions": ["echo Building for ${platform} debug=${debug}"],
|
|
69
|
-
"options": {
|
|
70
|
-
"platform": { "type": "string", "default": "ios" },
|
|
71
|
-
"debug": { "description": "Enable debug mode" }
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
yarn mono build --platform android --debug
|
|
78
|
-
```
|
|
20
|
+
- local development
|
|
21
|
+
- CI pipelines
|
|
22
|
+
- deployments
|
|
23
|
+
- infrastructure management
|
|
79
24
|
|
|
80
25
|
---
|
|
81
26
|
|
|
82
|
-
##
|
|
83
|
-
|
|
84
|
-
| Concept | Summary |
|
|
85
|
-
| -------------- | --------------------------------------------------------------------------------------------- |
|
|
86
|
-
| `.mono/*.json` | Each file (except `config.json`) becomes a command. `dev.json` -> `yarn mono dev`. |
|
|
87
|
-
| `preactions` | Sequential setup commands whose output can define tokens. |
|
|
88
|
-
| `actions` | Main workload commands. All but last run detached; last is attached (interactive). |
|
|
89
|
-
| Tokens | Printed from preactions as `{out:key value}` and later substituted as `${key}`. |
|
|
90
|
-
| Environments | `environments.dev` / `environments.stage` provide token-aware env vars. Use `--stage` switch. |
|
|
91
|
-
| Data Layer | Merges defaults, user flags, argument, and emitted tokens. |
|
|
27
|
+
## What Problems It Solves
|
|
92
28
|
|
|
93
|
-
|
|
29
|
+
Most monorepos suffer from:
|
|
94
30
|
|
|
95
|
-
|
|
31
|
+
- duplicated scripts across packages
|
|
32
|
+
- environment drift between dev and CI
|
|
33
|
+
- infrastructure logic isolated in pipelines
|
|
34
|
+
- brittle bash scripts
|
|
35
|
+
- slow onboarding
|
|
96
36
|
|
|
97
|
-
|
|
37
|
+
mono-labs solves this by providing:
|
|
98
38
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
| Troubleshooting | `docs/troubleshooting.md` |
|
|
39
|
+
- declarative command definitions
|
|
40
|
+
- shared runtime state via tokens
|
|
41
|
+
- reusable project utilities
|
|
42
|
+
- programmatic CDK helpers
|
|
43
|
+
- one mental model for dev, CI, and deploy
|
|
105
44
|
|
|
106
45
|
---
|
|
107
46
|
|
|
108
|
-
##
|
|
47
|
+
## High-Level Architecture
|
|
109
48
|
|
|
110
|
-
|
|
111
|
-
2. Builds Commander commands for each JSON file.
|
|
112
|
-
3. When invoked: merges defaults + flags + argument into data layer.
|
|
113
|
-
4. Runs `preactions` (foreground) capturing `{out:key value}` tokens.
|
|
114
|
-
5. Spawns each action (background except last). Performs `${token}`
|
|
115
|
-
substitution.
|
|
116
|
-
6. Cleans background processes on exit or Ctrl+C.
|
|
49
|
+
mono-labs is intentionally layered:
|
|
117
50
|
|
|
118
|
-
|
|
51
|
+
1. `.mono/` Declarative command definitions (JSON).
|
|
119
52
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
## Local Development / Linking
|
|
53
|
+
2. CLI Runtime (`bin` + `lib`) Loads `.mono`, builds commands, executes
|
|
54
|
+
workflows, manages processes.
|
|
123
55
|
|
|
124
|
-
|
|
56
|
+
3. Project Orchestration (`src/project`) Environment merging, configuration
|
|
57
|
+
management, monorepo utilities.
|
|
125
58
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
```
|
|
59
|
+
4. Infrastructure Integration (`src/cdk`) CDK helpers, stack orchestration,
|
|
60
|
+
CI-friendly deployment primitives.
|
|
129
61
|
|
|
130
|
-
|
|
62
|
+
Each layer can be used independently, but they are designed to work together.
|
|
131
63
|
|
|
132
|
-
|
|
133
|
-
yarn link "@mono-labs/cli"
|
|
134
|
-
```
|
|
64
|
+
---
|
|
135
65
|
|
|
136
|
-
|
|
66
|
+
## Quick Start
|
|
137
67
|
|
|
138
|
-
|
|
139
|
-
yarn mono <command>
|
|
140
|
-
```
|
|
68
|
+
Create a `.mono` directory and add:
|
|
141
69
|
|
|
142
|
-
|
|
70
|
+
.mono/hello.json
|
|
143
71
|
|
|
144
|
-
|
|
145
|
-
yarn unlink "@mono-labs/cli"
|
|
146
|
-
```
|
|
72
|
+
{ "actions": ["echo Hello World"] }
|
|
147
73
|
|
|
148
|
-
|
|
74
|
+
Run:
|
|
149
75
|
|
|
150
|
-
|
|
151
|
-
yarn add file:/absolute/path/to/mono-labs-cli
|
|
152
|
-
```
|
|
76
|
+
yarn mono hello
|
|
153
77
|
|
|
154
78
|
---
|
|
155
79
|
|
|
156
|
-
##
|
|
80
|
+
## Typical Developer Workflow
|
|
157
81
|
|
|
158
|
-
|
|
82
|
+
yarn mono dev yarn mono serve yarn mono mobile
|
|
159
83
|
|
|
160
|
-
|
|
161
|
-
{out:ngrok_api https://1234.ngrok.dev}
|
|
162
|
-
{out:region us-east-1}
|
|
163
|
-
```
|
|
84
|
+
If unsure:
|
|
164
85
|
|
|
165
|
-
|
|
86
|
+
yarn mono help
|
|
166
87
|
|
|
167
88
|
---
|
|
168
89
|
|
|
169
|
-
##
|
|
170
|
-
|
|
171
|
-
```json
|
|
172
|
-
// .mono/dev.json
|
|
173
|
-
{
|
|
174
|
-
"preactions": ["docker compose up -d", "node scripts/ngrok_setup"],
|
|
175
|
-
"actions": [
|
|
176
|
-
"yarn backend dynamodb-admin -p 8082 --dynamo-endpoint=http://localhost:8000",
|
|
177
|
-
"yarn mono backend server"
|
|
178
|
-
],
|
|
179
|
-
"argument": { "type": "string", "default": "dev" },
|
|
180
|
-
"options": {
|
|
181
|
-
"stage": { "description": "Use stage env" },
|
|
182
|
-
"profile": { "type": "string", "description": "Profile name" }
|
|
183
|
-
},
|
|
184
|
-
"environments": {
|
|
185
|
-
"dev": { "API_URL": "${ngrok_api}", "MODE": "dev" },
|
|
186
|
-
"stage": { "API_URL": "${ngrok_api}", "MODE": "stage" }
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
Run:
|
|
192
|
-
|
|
193
|
-
```bash
|
|
194
|
-
yarn mono dev --profile alpha
|
|
195
|
-
```
|
|
90
|
+
## Documentation Index
|
|
196
91
|
|
|
197
|
-
|
|
92
|
+
Start here:
|
|
198
93
|
|
|
199
|
-
|
|
94
|
+
- docs/README.txt
|
|
200
95
|
|
|
201
|
-
|
|
202
|
-
- Single positional argument: keeps mental model small.
|
|
203
|
-
- Token system: decouples script output from later steps.
|
|
204
|
-
- Background/foreground split: stable dev server orchestration.
|
|
96
|
+
Key docs:
|
|
205
97
|
|
|
206
|
-
|
|
98
|
+
- docs/architecture.md
|
|
99
|
+
- docs/configuration.md
|
|
100
|
+
- docs/examples.md
|
|
101
|
+
- docs/troubleshooting.md
|
|
207
102
|
|
|
208
|
-
|
|
103
|
+
Advanced:
|
|
209
104
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
| Multiple arguments | Extend `cliFactory.js` to parse more. |
|
|
213
|
-
| JSON schema validation | Add Ajv in `boot()` loader. |
|
|
214
|
-
| Parallel preactions | Modify `runHasteCommand.js` to `Promise.all`. |
|
|
215
|
-
| Different token syntax | Adjust regex in `runForeground.js`. |
|
|
105
|
+
- docs/project-orchestration.md
|
|
106
|
+
- docs/infrastructure-integration.md
|
|
216
107
|
|
|
217
108
|
---
|
|
218
109
|
|
|
219
|
-
##
|
|
110
|
+
## Who This Is For
|
|
220
111
|
|
|
221
|
-
|
|
222
|
-
2. Create a feature branch
|
|
223
|
-
3. Add/adjust tests (future roadmap)
|
|
224
|
-
4. Submit PR with clear description
|
|
112
|
+
mono-labs is designed for teams that:
|
|
225
113
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
| Question | Answer |
|
|
231
|
-
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
|
|
232
|
-
| How do I list commands? | Look at filenames in `.mono/` or run `yarn mono --help`. |
|
|
233
|
-
| How do I pass env vars manually? | `MY_VAR=1 yarn mono dev` (POSIX) or `set MY_VAR=1 && yarn mono dev` (CMD) or `$env:MY_VAR=1; yarn mono dev` (PowerShell). |
|
|
234
|
-
| Does it support Windows? | Yes; process cleanup uses `taskkill`. |
|
|
235
|
-
| What if a token is missing? | It stays literal (`${token}`); no crash. |
|
|
114
|
+
- run full-stack systems
|
|
115
|
+
- manage real infrastructure
|
|
116
|
+
- care about reproducibility
|
|
117
|
+
- want dev and CI to behave the same
|
|
236
118
|
|
|
237
119
|
---
|
|
238
120
|
|
|
239
121
|
## License
|
|
240
122
|
|
|
241
123
|
MIT © Contributors
|
|
242
|
-
|
|
243
|
-
---
|
|
244
|
-
|
|
245
|
-
## Next Steps
|
|
246
|
-
|
|
247
|
-
Jump to: `docs/examples.md` for hands-on learning.
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// scripts/generate-readme.ts
|
|
2
|
+
// Node >= 18 recommended
|
|
3
|
+
import { promises as fs } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { generateDocsIndex } from './generate-docs.js';
|
|
6
|
+
/* -------------------------------------------------------------------------- */
|
|
7
|
+
/* Path helpers */
|
|
8
|
+
/* -------------------------------------------------------------------------- */
|
|
9
|
+
// Always use the working directory as the root for all file actions
|
|
10
|
+
const REPO_ROOT = path.resolve(process.cwd());
|
|
11
|
+
const MONO_DIR = path.join(REPO_ROOT, '.mono');
|
|
12
|
+
const ROOT_PKG_JSON = path.join(REPO_ROOT, 'package.json');
|
|
13
|
+
const OUTPUT_PATH = path.join(REPO_ROOT, 'docs');
|
|
14
|
+
const OUTPUT_README = path.join(OUTPUT_PATH, 'command-line.md');
|
|
15
|
+
/* -------------------------------------------------------------------------- */
|
|
16
|
+
/* Utils */
|
|
17
|
+
/* -------------------------------------------------------------------------- */
|
|
18
|
+
async function ensureParentDir(filePath) {
|
|
19
|
+
// Always resolve parent dir relative to working directory
|
|
20
|
+
const dir = path.resolve(process.cwd(), path.dirname(filePath));
|
|
21
|
+
await fs.mkdir(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
async function exists(p) {
|
|
24
|
+
try {
|
|
25
|
+
await fs.access(p);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function isObject(v) {
|
|
33
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
34
|
+
}
|
|
35
|
+
function toPosix(p) {
|
|
36
|
+
return p.split(path.sep).join('/');
|
|
37
|
+
}
|
|
38
|
+
async function readJson(filePath) {
|
|
39
|
+
// Always resolve filePath relative to working directory
|
|
40
|
+
const absPath = path.resolve(process.cwd(), filePath);
|
|
41
|
+
const raw = await fs.readFile(absPath, 'utf8');
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
async function listDir(dir) {
|
|
45
|
+
// Always resolve dir relative to working directory
|
|
46
|
+
const absDir = path.resolve(process.cwd(), dir);
|
|
47
|
+
return fs.readdir(absDir, { withFileTypes: true });
|
|
48
|
+
}
|
|
49
|
+
function normalizeWorkspacePatterns(workspacesField) {
|
|
50
|
+
if (Array.isArray(workspacesField))
|
|
51
|
+
return workspacesField;
|
|
52
|
+
if (isObject(workspacesField) &&
|
|
53
|
+
Array.isArray(workspacesField.packages)) {
|
|
54
|
+
return workspacesField.packages;
|
|
55
|
+
}
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
function mdEscapeInline(value) {
|
|
59
|
+
return String(value ?? '').replaceAll('`', '\\`');
|
|
60
|
+
}
|
|
61
|
+
function indentLines(s, spaces = 2) {
|
|
62
|
+
const pad = ' '.repeat(spaces);
|
|
63
|
+
return s
|
|
64
|
+
.split('\n')
|
|
65
|
+
.map((line) => pad + line)
|
|
66
|
+
.join('\n');
|
|
67
|
+
}
|
|
68
|
+
/* -------------------------------------------------------------------------- */
|
|
69
|
+
/* Workspace glob pattern expansion */
|
|
70
|
+
/* -------------------------------------------------------------------------- */
|
|
71
|
+
function matchSegment(patternSeg, name) {
|
|
72
|
+
if (patternSeg === '*')
|
|
73
|
+
return true;
|
|
74
|
+
if (!patternSeg.includes('*'))
|
|
75
|
+
return patternSeg === name;
|
|
76
|
+
const escaped = patternSeg.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
77
|
+
const regex = new RegExp(`^${escaped.replaceAll('*', '.*')}$`);
|
|
78
|
+
return regex.test(name);
|
|
79
|
+
}
|
|
80
|
+
async function expandWorkspacePattern(root, pattern) {
|
|
81
|
+
const segments = toPosix(pattern).split('/').filter(Boolean);
|
|
82
|
+
async function expandFrom(dir, index) {
|
|
83
|
+
// Always resolve dir relative to working directory
|
|
84
|
+
const absDir = path.resolve(process.cwd(), dir);
|
|
85
|
+
if (index >= segments.length)
|
|
86
|
+
return [absDir];
|
|
87
|
+
const seg = segments[index];
|
|
88
|
+
if (seg === '**') {
|
|
89
|
+
const results = [];
|
|
90
|
+
results.push(...(await expandFrom(absDir, index + 1)));
|
|
91
|
+
const entries = await fs
|
|
92
|
+
.readdir(absDir, { withFileTypes: true })
|
|
93
|
+
.catch(() => []);
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (!entry.isDirectory())
|
|
96
|
+
continue;
|
|
97
|
+
results.push(...(await expandFrom(path.join(absDir, entry.name), index)));
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
const entries = await fs
|
|
102
|
+
.readdir(absDir, { withFileTypes: true })
|
|
103
|
+
.catch(() => []);
|
|
104
|
+
const results = [];
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
if (!entry.isDirectory())
|
|
107
|
+
continue;
|
|
108
|
+
if (!matchSegment(seg, entry.name))
|
|
109
|
+
continue;
|
|
110
|
+
results.push(...(await expandFrom(path.join(absDir, entry.name), index + 1)));
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
const dirs = await expandFrom(root, 0);
|
|
115
|
+
const pkgDirs = [];
|
|
116
|
+
for (const d of dirs) {
|
|
117
|
+
if (await exists(path.join(d, 'package.json'))) {
|
|
118
|
+
pkgDirs.push(d);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return Array.from(new Set(pkgDirs));
|
|
122
|
+
}
|
|
123
|
+
async function findWorkspacePackageDirs(repoRoot, patterns) {
|
|
124
|
+
const dirs = [];
|
|
125
|
+
for (const pat of patterns) {
|
|
126
|
+
dirs.push(...(await expandWorkspacePattern(repoRoot, pat)));
|
|
127
|
+
}
|
|
128
|
+
return Array.from(new Set(dirs));
|
|
129
|
+
}
|
|
130
|
+
/* -------------------------------------------------------------------------- */
|
|
131
|
+
/* .mono configuration */
|
|
132
|
+
/* -------------------------------------------------------------------------- */
|
|
133
|
+
async function readMonoConfig() {
|
|
134
|
+
// Always resolve configPath relative to working directory
|
|
135
|
+
const configPath = path.resolve(process.cwd(), path.join(MONO_DIR, 'config.json'));
|
|
136
|
+
if (!(await exists(configPath)))
|
|
137
|
+
return null;
|
|
138
|
+
try {
|
|
139
|
+
const config = await readJson(configPath);
|
|
140
|
+
return { path: configPath, config };
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function commandNameFromFile(filePath) {
|
|
147
|
+
return path.basename(filePath).replace(/\.json$/i, '');
|
|
148
|
+
}
|
|
149
|
+
async function readMonoCommands() {
|
|
150
|
+
// Always resolve MONO_DIR relative to working directory
|
|
151
|
+
const monoDirAbs = path.resolve(process.cwd(), MONO_DIR);
|
|
152
|
+
if (!(await exists(monoDirAbs)))
|
|
153
|
+
return [];
|
|
154
|
+
const entries = await listDir(monoDirAbs);
|
|
155
|
+
const jsonFiles = entries
|
|
156
|
+
.filter((e) => e.isFile() && e.name.endsWith('.json'))
|
|
157
|
+
.map((e) => path.join(monoDirAbs, e.name))
|
|
158
|
+
.filter((p) => path.basename(p) !== 'config.json');
|
|
159
|
+
const commands = [];
|
|
160
|
+
for (const file of jsonFiles) {
|
|
161
|
+
try {
|
|
162
|
+
const json = await readJson(file);
|
|
163
|
+
commands.push({
|
|
164
|
+
name: commandNameFromFile(file),
|
|
165
|
+
file,
|
|
166
|
+
json,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* ignore invalid JSON */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
174
|
+
}
|
|
175
|
+
/* -------------------------------------------------------------------------- */
|
|
176
|
+
/* Options schema parsing */
|
|
177
|
+
/* -------------------------------------------------------------------------- */
|
|
178
|
+
function parseOptionsSchema(optionsObj) {
|
|
179
|
+
if (!isObject(optionsObj))
|
|
180
|
+
return [];
|
|
181
|
+
const entries = Object.entries(optionsObj).map(([key, raw]) => {
|
|
182
|
+
const o = isObject(raw) ? raw : {};
|
|
183
|
+
const hasType = typeof o.type === 'string' && o.type.length > 0;
|
|
184
|
+
return {
|
|
185
|
+
key,
|
|
186
|
+
kind: hasType ? 'value' : 'boolean',
|
|
187
|
+
type: hasType ? o.type : 'boolean',
|
|
188
|
+
description: typeof o.description === 'string' ? o.description : '',
|
|
189
|
+
shortcut: typeof o.shortcut === 'string' ? o.shortcut : '',
|
|
190
|
+
default: o.default,
|
|
191
|
+
allowed: Array.isArray(o.options) ? o.options : null,
|
|
192
|
+
allowAll: o.allowAll === true,
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
return entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
196
|
+
}
|
|
197
|
+
/* -------------------------------------------------------------------------- */
|
|
198
|
+
/* Formatting */
|
|
199
|
+
/* -------------------------------------------------------------------------- */
|
|
200
|
+
function buildUsageExample(commandName, cmdJson, options) {
|
|
201
|
+
const arg = cmdJson.argument;
|
|
202
|
+
const hasArg = isObject(arg);
|
|
203
|
+
const parts = [`yarn mono ${commandName}`];
|
|
204
|
+
if (hasArg)
|
|
205
|
+
parts.push(`<${commandName}-arg>`);
|
|
206
|
+
const valueOpts = options.filter((o) => o.kind === 'value');
|
|
207
|
+
const boolOpts = options.filter((o) => o.kind === 'boolean');
|
|
208
|
+
for (const o of valueOpts.slice(0, 2)) {
|
|
209
|
+
const value = o.default !== undefined ?
|
|
210
|
+
String(o.default)
|
|
211
|
+
: (o.allowed?.[0] ?? '<value>');
|
|
212
|
+
parts.push(`--${o.key} ${value}`);
|
|
213
|
+
}
|
|
214
|
+
if (boolOpts[0]) {
|
|
215
|
+
parts.push(`--${boolOpts[0].key}`);
|
|
216
|
+
}
|
|
217
|
+
return parts.join(' ');
|
|
218
|
+
}
|
|
219
|
+
/* -------------------------------------------------------------------------- */
|
|
220
|
+
/* Main */
|
|
221
|
+
/* -------------------------------------------------------------------------- */
|
|
222
|
+
async function main() {
|
|
223
|
+
// Always resolve all paths relative to working directory
|
|
224
|
+
if (!(await exists(ROOT_PKG_JSON))) {
|
|
225
|
+
throw new Error(`Missing ${ROOT_PKG_JSON}`);
|
|
226
|
+
}
|
|
227
|
+
await ensureParentDir(OUTPUT_PATH);
|
|
228
|
+
const rootPkg = await readJson(ROOT_PKG_JSON);
|
|
229
|
+
const workspacePatterns = normalizeWorkspacePatterns(rootPkg.workspaces);
|
|
230
|
+
const monoConfig = await readMonoConfig();
|
|
231
|
+
const monoCommands = await readMonoCommands();
|
|
232
|
+
const pkgDirs = await findWorkspacePackageDirs(REPO_ROOT, workspacePatterns);
|
|
233
|
+
const packages = [];
|
|
234
|
+
for (const dir of pkgDirs) {
|
|
235
|
+
try {
|
|
236
|
+
const pkg = await readJson(path.join(dir, 'package.json'));
|
|
237
|
+
packages.push({
|
|
238
|
+
name: pkg.name ??
|
|
239
|
+
toPosix(path.relative(REPO_ROOT, dir)) ??
|
|
240
|
+
path.basename(dir),
|
|
241
|
+
dir,
|
|
242
|
+
scripts: pkg.scripts ?? {},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
/* ignore */
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const parts = [];
|
|
250
|
+
parts.push(`# Mono Command-Line Reference
|
|
251
|
+
|
|
252
|
+
> Generated by \`scripts/generate-readme.ts\`.
|
|
253
|
+
|
|
254
|
+
`);
|
|
255
|
+
// Reuse your existing formatters here
|
|
256
|
+
// (unchanged logic, now fully typed)
|
|
257
|
+
const docsIndex = await generateDocsIndex({
|
|
258
|
+
docsDir: path.join(REPO_ROOT, 'docs'),
|
|
259
|
+
excludeFile: 'command-line.md',
|
|
260
|
+
});
|
|
261
|
+
parts.push(docsIndex);
|
|
262
|
+
await ensureParentDir(OUTPUT_README);
|
|
263
|
+
await fs.writeFile(OUTPUT_README, parts.join('\n'), 'utf8');
|
|
264
|
+
console.log(`Generated: ${OUTPUT_README}`);
|
|
265
|
+
console.log(`- mono config: ${monoConfig ? 'yes' : 'no'}`);
|
|
266
|
+
console.log(`- mono commands: ${monoCommands.length}`);
|
|
267
|
+
console.log(`- workspace packages: ${packages.length}`);
|
|
268
|
+
}
|
|
269
|
+
main().catch((err) => {
|
|
270
|
+
console.error(err instanceof Error ? err.stack : err);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
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
|
+
import { promises as fs } from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
/**
|
|
16
|
+
* Generate a docs index from markdown files.
|
|
17
|
+
*
|
|
18
|
+
* @param options - Options for docs index generation
|
|
19
|
+
* @returns Markdown-formatted index
|
|
20
|
+
*/
|
|
21
|
+
export async function generateDocsIndex({ docsDir, excludeFile, }) {
|
|
22
|
+
// Always resolve docsDir relative to the working directory
|
|
23
|
+
const dirPath = path.resolve(process.cwd(), docsDir);
|
|
24
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
25
|
+
const links = [];
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (!entry.isFile())
|
|
28
|
+
continue;
|
|
29
|
+
if (!entry.name.endsWith('.md'))
|
|
30
|
+
continue;
|
|
31
|
+
// Always ignore docs/readme.md (case-insensitive)
|
|
32
|
+
if (entry.name.toLowerCase() === 'readme.md')
|
|
33
|
+
continue;
|
|
34
|
+
// Optionally ignore a caller-specified file
|
|
35
|
+
if (excludeFile && entry.name === excludeFile)
|
|
36
|
+
continue;
|
|
37
|
+
const filePath = path.join(dirPath, entry.name);
|
|
38
|
+
const contents = await fs.readFile(filePath, 'utf8');
|
|
39
|
+
// Find first markdown H1
|
|
40
|
+
const match = contents.match(/^#\s+(.+)$/m);
|
|
41
|
+
if (!match)
|
|
42
|
+
continue;
|
|
43
|
+
const title = match[1].trim();
|
|
44
|
+
const relativeLink = `./${entry.name}`;
|
|
45
|
+
links.push(`[${title}](${relativeLink})`);
|
|
46
|
+
}
|
|
47
|
+
// Sort alphabetically by title for stability
|
|
48
|
+
links.sort((a, b) => a.localeCompare(b));
|
|
49
|
+
// Append Back to Readme (hardcoded)
|
|
50
|
+
links.push('');
|
|
51
|
+
links.push('[Back to Readme](../README.md)');
|
|
52
|
+
return links.join('\n');
|
|
53
|
+
}
|