@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 CHANGED
@@ -1,247 +1,123 @@
1
- <div align="center">
1
+ # mono-labs
2
2
 
3
- # mono-labs CLI
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
- ## Quick Start (Beginner Friendly)
8
+ ## What This Is
26
9
 
27
- 1. Install dependencies:
10
+ mono-labs is a monorepo control plane.
28
11
 
29
- ```bash
30
- yarn install
31
- ```
12
+ It combines:
32
13
 
33
- 2. Create a `.mono` directory in your project root.
34
- 3. Add a file `.mono/hello.json`:
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
- 4. Run the command:
18
+ The goal is to make a monorepo behave like a single, coordinated system across:
41
19
 
42
- ```bash
43
- yarn mono hello
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
- ## Core Concepts
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
- Full schemas & rules: see `docs/configuration.md`.
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
- ## Documentation Index
37
+ mono-labs solves this by providing:
98
38
 
99
- | Topic | File |
100
- | ------------------------ | ------------------------- |
101
- | Architecture / internals | `docs/architecture.md` |
102
- | Configuration schema | `docs/configuration.md` |
103
- | Practical examples | `docs/examples.md` |
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
- ## How It Works (Short Version)
47
+ ## High-Level Architecture
109
48
 
110
- 1. CLI scans `.mono/` at startup.
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
- Details: `docs/architecture.md`.
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
- From this repo root:
56
+ 3. Project Orchestration (`src/project`) Environment merging, configuration
57
+ management, monorepo utilities.
125
58
 
126
- ```bash
127
- yarn link
128
- ```
59
+ 4. Infrastructure Integration (`src/cdk`) CDK helpers, stack orchestration,
60
+ CI-friendly deployment primitives.
129
61
 
130
- In a target project:
62
+ Each layer can be used independently, but they are designed to work together.
131
63
 
132
- ```bash
133
- yarn link "@mono-labs/cli"
134
- ```
64
+ ---
135
65
 
136
- Then use:
66
+ ## Quick Start
137
67
 
138
- ```bash
139
- yarn mono <command>
140
- ```
68
+ Create a `.mono` directory and add:
141
69
 
142
- To unlink later:
70
+ .mono/hello.json
143
71
 
144
- ```bash
145
- yarn unlink "@mono-labs/cli"
146
- ```
72
+ { "actions": ["echo Hello World"] }
147
73
 
148
- Alternative (direct file install):
74
+ Run:
149
75
 
150
- ```bash
151
- yarn add file:/absolute/path/to/mono-labs-cli
152
- ```
76
+ yarn mono hello
153
77
 
154
78
  ---
155
79
 
156
- ## Emitting Dynamic Values
80
+ ## Typical Developer Workflow
157
81
 
158
- Inside a `preactions` script output lines like:
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
- Then reference in actions or environments as `${ngrok_api}` or `${region}`.
86
+ yarn mono help
166
87
 
167
88
  ---
168
89
 
169
- ## Example Advanced Command
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
- ## Design Decisions
94
+ - docs/README.txt
200
95
 
201
- - JSON over JS: simpler, toolable, safer for newcomers.
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
- ## Extending
103
+ Advanced:
209
104
 
210
- | Need | Approach |
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
- ## Contributing
110
+ ## Who This Is For
220
111
 
221
- 1. Fork & clone
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
- ## FAQ (Fast Answers)
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,2 @@
1
+ import './build-mono-readme';
2
+ import './generate-readme';
@@ -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
+ }