@getmarrow/install 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # @getmarrow/install
2
+
3
+ Universal installer for Marrow passive agent setup.
4
+
5
+ Use it when you want Marrow to detect the local agent/runtime environment and wire the safest passive integration automatically.
6
+
7
+ ```bash
8
+ npx @getmarrow/install --dry-run
9
+ npx @getmarrow/install --yes
10
+ ```
11
+
12
+ ## What It Detects
13
+
14
+ - OpenClaw-style workspaces
15
+ - Codex/agent instruction files such as `AGENTS.md`
16
+ - Claude Code settings and hooks
17
+ - Cursor project folders
18
+ - MCP config files
19
+ - Node projects
20
+ - Python projects
21
+
22
+ ## Install Modes
23
+
24
+ ```bash
25
+ npx @getmarrow/install --mcp --dry-run
26
+ npx @getmarrow/install --sdk --dry-run
27
+ npx @getmarrow/install --both --dry-run
28
+ npx @getmarrow/install --md --dry-run
29
+ ```
30
+
31
+ `--dry-run` is the default unless `--yes` is passed.
32
+
33
+ ## What It Writes
34
+
35
+ - `.claude/settings.json` passive MCP hooks for tool outcomes and prompt context.
36
+ - `.mcp.json` with the Marrow MCP server entry.
37
+ - `.marrow/passive-runtime.mjs` for SDK passive runtime preload in Node processes.
38
+ - `.marrow/env.example` with required environment variables.
39
+ - `AGENTS.md` instructions for agents that rely on markdown/skills.
40
+ - `.cursor/rules/marrow.mdc` when a Cursor project is detected.
41
+
42
+ ## Self-Test
43
+
44
+ When `MARROW_API_KEY` is present, the installer creates a harmless test decision, commits the outcome, and reads `/v1/agent/status`.
45
+
46
+ ```bash
47
+ MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install --yes
48
+ ```
49
+
50
+ Skip self-test:
51
+
52
+ ```bash
53
+ npx @getmarrow/install --yes --no-self-test
54
+ ```
55
+
56
+ ## Trust Model
57
+
58
+ This package is intended to be open source and auditable. It prints every file it will touch, requires `--yes` to write, does not store API keys in project files, and supports MCP-only, SDK-only, both, and markdown-only setups.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { runCli } = require('../src/installer');
4
+
5
+ runCli(process.argv.slice(2)).catch((error) => {
6
+ const message = error instanceof Error ? error.message : String(error);
7
+ process.stderr.write(`marrow-install failed: ${message}\n`);
8
+ process.exit(1);
9
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@getmarrow/install",
3
+ "version": "0.1.0",
4
+ "description": "Universal installer for Marrow passive agent setup.",
5
+ "bin": {
6
+ "marrow-install": "bin/marrow-install.js"
7
+ },
8
+ "main": "src/installer.js",
9
+ "scripts": {
10
+ "test": "node --test test/*.test.js"
11
+ },
12
+ "keywords": [
13
+ "ai",
14
+ "agents",
15
+ "marrow",
16
+ "mcp",
17
+ "installer",
18
+ "passive-runtime"
19
+ ],
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/getmarrow/marrow-install.git"
23
+ },
24
+ "license": "MIT",
25
+ "files": [
26
+ "bin",
27
+ "src",
28
+ "README.md"
29
+ ],
30
+ "engines": {
31
+ "node": ">=18"
32
+ }
33
+ }
@@ -0,0 +1,502 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const os = require('node:os');
4
+
5
+ const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
6
+ const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
7
+ const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
8
+
9
+ function parseArgs(argv) {
10
+ const options = {
11
+ cwd: process.cwd(),
12
+ yes: false,
13
+ dryRun: false,
14
+ mode: 'auto',
15
+ apiKey: process.env.MARROW_API_KEY || '',
16
+ baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
17
+ agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
18
+ selfTest: true,
19
+ json: false,
20
+ };
21
+
22
+ for (let i = 0; i < argv.length; i += 1) {
23
+ const arg = argv[i];
24
+ if (arg === '--yes' || arg === '-y') options.yes = true;
25
+ else if (arg === '--dry-run') options.dryRun = true;
26
+ else if (arg === '--json') options.json = true;
27
+ else if (arg === '--no-self-test') options.selfTest = false;
28
+ else if (arg === '--self-test') options.selfTest = true;
29
+ else if (arg === '--cwd') options.cwd = path.resolve(argv[++i] || options.cwd);
30
+ else if (arg === '--mode') options.mode = argv[++i] || options.mode;
31
+ else if (arg === '--key') {
32
+ options.apiKey = argv[++i] || options.apiKey;
33
+ options.keyFromArg = true;
34
+ }
35
+ else if (arg === '--base-url') options.baseUrl = argv[++i] || options.baseUrl;
36
+ else if (arg === '--agent-id') options.agentId = argv[++i] || options.agentId;
37
+ else if (arg === '--mcp') options.mode = 'mcp';
38
+ else if (arg === '--sdk') options.mode = 'sdk';
39
+ else if (arg === '--md' || arg === '--instructions') options.mode = 'md';
40
+ else if (arg === '--both') options.mode = 'both';
41
+ else if (arg === '--help' || arg === '-h') {
42
+ options.help = true;
43
+ } else {
44
+ throw new Error(`Unknown argument: ${arg}`);
45
+ }
46
+ }
47
+
48
+ if (!['auto', 'mcp', 'sdk', 'both', 'md'].includes(options.mode)) {
49
+ throw new Error('--mode must be one of auto, mcp, sdk, both, md');
50
+ }
51
+
52
+ return options;
53
+ }
54
+
55
+ function usage() {
56
+ return `Usage:
57
+ npx @getmarrow/install --dry-run
58
+ npx @getmarrow/install --yes
59
+ npx @getmarrow/install --mcp --yes
60
+ npx @getmarrow/install --sdk --yes
61
+
62
+ Options:
63
+ --dry-run Print planned changes without writing
64
+ --yes, -y Write detected config files
65
+ --mode <mode> auto, mcp, sdk, both, or md
66
+ --key <key> Marrow API key for self-test. Prefer MARROW_API_KEY because CLI args can appear in process listings.
67
+ --base-url <url> Marrow API base URL
68
+ --agent-id <id> Agent/fleet id for self-test headers
69
+ --no-self-test Skip API smoke/self-test
70
+ `;
71
+ }
72
+
73
+ function exists(filePath) {
74
+ return fs.existsSync(filePath);
75
+ }
76
+
77
+ function safeRead(filePath) {
78
+ return exists(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
79
+ }
80
+
81
+ function findUp(startDir, names, maxDepth = 8) {
82
+ let dir = path.resolve(startDir);
83
+ for (let depth = 0; depth <= maxDepth; depth += 1) {
84
+ for (const name of names) {
85
+ const candidate = path.join(dir, name);
86
+ if (exists(candidate)) return candidate;
87
+ }
88
+ const parent = path.dirname(dir);
89
+ if (parent === dir) break;
90
+ dir = parent;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ function projectRoot(startDir) {
96
+ const marker = findUp(startDir, ['package.json', 'pyproject.toml', 'requirements.txt', '.git', 'AGENTS.md', 'CLAUDE.md']);
97
+ return marker ? path.dirname(marker) : path.resolve(startDir);
98
+ }
99
+
100
+ function detectEnvironment(cwd = process.cwd(), env = process.env) {
101
+ const root = projectRoot(cwd);
102
+ const home = env.HOME || env.USERPROFILE || os.homedir();
103
+ const paths = {
104
+ root,
105
+ packageJson: path.join(root, 'package.json'),
106
+ pyproject: path.join(root, 'pyproject.toml'),
107
+ requirements: path.join(root, 'requirements.txt'),
108
+ setupPy: path.join(root, 'setup.py'),
109
+ claudeSettings: path.join(root, '.claude', 'settings.json'),
110
+ claudeMd: path.join(root, 'CLAUDE.md'),
111
+ agentsMd: path.join(root, 'AGENTS.md'),
112
+ cursorRules: path.join(root, '.cursor', 'rules', 'marrow.mdc'),
113
+ cursorMcp: path.join(root, '.cursor', 'mcp.json'),
114
+ mcpJson: path.join(root, '.mcp.json'),
115
+ marrowDir: path.join(root, '.marrow'),
116
+ passiveRuntime: path.join(root, '.marrow', 'passive-runtime.mjs'),
117
+ passiveEnv: path.join(root, '.marrow', 'env.example'),
118
+ openclawJson: findUp(root, ['openclaw.json'], 4) || path.join(home, '.openclaw', 'openclaw.json'),
119
+ };
120
+
121
+ return {
122
+ root,
123
+ paths,
124
+ node: exists(paths.packageJson),
125
+ python: exists(paths.pyproject) || exists(paths.requirements) || exists(paths.setupPy),
126
+ claudeCode: exists(paths.claudeSettings) || exists(paths.claudeMd),
127
+ cursor: exists(path.join(root, '.cursor')),
128
+ codex: exists(paths.agentsMd) || exists(path.join(root, '.codex')),
129
+ mcpConfig: exists(paths.mcpJson) || exists(paths.cursorMcp) || exists(paths.claudeSettings),
130
+ openclaw: exists(paths.openclawJson) || Boolean(env.OPENCLAW_HOME || env.OPENCLAW_AGENT_ID),
131
+ };
132
+ }
133
+
134
+ function passiveInstructions() {
135
+ return `${MARROW_BLOCK_START}
136
+ ## Marrow Passive Agent Memory
137
+
138
+ Marrow should run passively after install:
139
+
140
+ - Use MCP hooks when available: \`npx -y @getmarrow/mcp setup\`.
141
+ - Use SDK passive runtime in owned Node processes: \`createPassiveRuntime().install()\`.
142
+ - Before risky work, use Marrow's decision brief or passive prompt hook.
143
+ - After meaningful work, record the outcome so future agents learn from it.
144
+ - Check health with \`marrow_agent_status\` or \`GET /v1/agent/status\`.
145
+
146
+ Required environment:
147
+
148
+ - \`MARROW_API_KEY\`
149
+ - Optional: \`MARROW_BASE_URL\`, \`MARROW_FLEET_AGENT_ID\`
150
+ ${MARROW_BLOCK_END}`;
151
+ }
152
+
153
+ function passiveRuntimeSource() {
154
+ return `import { MarrowClient } from '@getmarrow/sdk';
155
+
156
+ const apiKey = process.env.MARROW_API_KEY;
157
+ if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
158
+ const marrow = new MarrowClient(apiKey, {
159
+ baseUrl: process.env.MARROW_BASE_URL,
160
+ agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID,
161
+ sessionId: process.env.MARROW_SESSION_ID,
162
+ mode: process.env.MARROW_ENFORCEMENT_MODE || 'auto',
163
+ });
164
+
165
+ const runtime = marrow.createPassiveRuntime({
166
+ includeValueReport: process.env.MARROW_PASSIVE_VALUE_REPORT === 'true',
167
+ valueReportPeriod: process.env.MARROW_VALUE_REPORT_PERIOD || '7d',
168
+ });
169
+
170
+ runtime.install();
171
+ globalThis.__MARROW_PASSIVE_RUNTIME__ = runtime;
172
+ }
173
+ `;
174
+ }
175
+
176
+ function envExample() {
177
+ return `MARROW_API_KEY=mrw_live_replace_me
178
+ MARROW_BASE_URL=${DEFAULT_BASE_URL}
179
+ MARROW_FLEET_AGENT_ID=agent-or-fleet-id
180
+ MARROW_ENFORCEMENT_MODE=auto
181
+ MARROW_PASSIVE_BRIEF=auto
182
+ `;
183
+ }
184
+
185
+ function parseJsonObject(filePath) {
186
+ const raw = safeRead(filePath).trim();
187
+ if (!raw) return {};
188
+ const parsed = JSON.parse(raw);
189
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
190
+ throw new Error(`Expected JSON object in ${filePath}`);
191
+ }
192
+ return parsed;
193
+ }
194
+
195
+ function upsertBlock(content, block) {
196
+ if (content.includes(MARROW_BLOCK_START) && content.includes(MARROW_BLOCK_END)) {
197
+ const start = content.indexOf(MARROW_BLOCK_START);
198
+ const end = content.indexOf(MARROW_BLOCK_END) + MARROW_BLOCK_END.length;
199
+ return `${content.slice(0, start)}${block}${content.slice(end)}`;
200
+ }
201
+ const separator = content && !content.endsWith('\n') ? '\n\n' : content ? '\n' : '';
202
+ return `${content}${separator}${block}\n`;
203
+ }
204
+
205
+ function upsertClaudeHooks(settingsPath) {
206
+ const settings = parseJsonObject(settingsPath);
207
+ const hooks = settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)
208
+ ? settings.hooks
209
+ : {};
210
+ const postToolUse = Array.isArray(hooks.PostToolUse) ? [...hooks.PostToolUse] : [];
211
+ const userPromptSubmit = Array.isArray(hooks.UserPromptSubmit) ? [...hooks.UserPromptSubmit] : [];
212
+
213
+ const hasPost = postToolUse.some((entry) => JSON.stringify(entry).includes('npx -y @getmarrow/mcp hook'));
214
+ const hasPrompt = userPromptSubmit.some((entry) => JSON.stringify(entry).includes('npx -y @getmarrow/mcp context-hook'));
215
+
216
+ if (!hasPost) {
217
+ postToolUse.push({
218
+ matcher: 'Bash|Edit|Write|MultiEdit|mcp__(?!marrow_).*',
219
+ hooks: [{ type: 'command', command: 'npx -y @getmarrow/mcp hook' }],
220
+ });
221
+ }
222
+ if (!hasPrompt) {
223
+ userPromptSubmit.push({
224
+ hooks: [{ type: 'command', command: 'npx -y @getmarrow/mcp context-hook' }],
225
+ });
226
+ }
227
+
228
+ settings.hooks = {
229
+ ...hooks,
230
+ PostToolUse: postToolUse,
231
+ UserPromptSubmit: userPromptSubmit,
232
+ };
233
+
234
+ return JSON.stringify(settings, null, 2) + '\n';
235
+ }
236
+
237
+ function upsertMcpServerConfig(filePath) {
238
+ const config = parseJsonObject(filePath);
239
+ const servers = config.mcpServers && typeof config.mcpServers === 'object' && !Array.isArray(config.mcpServers)
240
+ ? config.mcpServers
241
+ : {};
242
+ servers.marrow = {
243
+ command: 'npx',
244
+ args: ['-y', '@getmarrow/mcp'],
245
+ env: {
246
+ MARROW_API_KEY: '${MARROW_API_KEY}',
247
+ MARROW_BASE_URL: '${MARROW_BASE_URL}',
248
+ MARROW_FLEET_AGENT_ID: '${MARROW_FLEET_AGENT_ID}',
249
+ },
250
+ };
251
+ config.mcpServers = servers;
252
+ return JSON.stringify(config, null, 2) + '\n';
253
+ }
254
+
255
+ function buildPlan(detection, options) {
256
+ const mode = options.mode === 'auto'
257
+ ? detection.node && (detection.claudeCode || detection.cursor || detection.codex || detection.openclaw)
258
+ ? 'both'
259
+ : detection.node
260
+ ? 'sdk'
261
+ : 'mcp'
262
+ : options.mode;
263
+ const writes = [];
264
+
265
+ if (mode === 'sdk' || mode === 'both') {
266
+ writes.push({
267
+ type: 'file',
268
+ path: detection.paths.passiveRuntime,
269
+ label: 'SDK passive runtime preload',
270
+ content: passiveRuntimeSource(),
271
+ });
272
+ writes.push({
273
+ type: 'file',
274
+ path: detection.paths.passiveEnv,
275
+ label: 'Marrow passive env example',
276
+ content: envExample(),
277
+ overwrite: false,
278
+ });
279
+ }
280
+
281
+ if (mode === 'mcp' || mode === 'both') {
282
+ if (detection.claudeCode) {
283
+ writes.push({
284
+ type: 'json-transform',
285
+ path: detection.paths.claudeSettings,
286
+ label: 'Claude Code MCP passive hooks',
287
+ transform: upsertClaudeHooks,
288
+ });
289
+ }
290
+ writes.push({
291
+ type: 'json-transform',
292
+ path: detection.paths.mcpJson,
293
+ label: 'Project MCP server config',
294
+ transform: upsertMcpServerConfig,
295
+ });
296
+ }
297
+
298
+ if (mode === 'md' || mode === 'both' || mode === 'mcp') {
299
+ writes.push({
300
+ type: 'md-block',
301
+ path: detection.paths.agentsMd,
302
+ label: 'Agent instructions',
303
+ block: passiveInstructions(),
304
+ });
305
+ }
306
+
307
+ if (detection.cursor && (mode === 'md' || mode === 'both' || mode === 'mcp')) {
308
+ writes.push({
309
+ type: 'file',
310
+ path: detection.paths.cursorRules,
311
+ label: 'Cursor Marrow rule',
312
+ content: passiveInstructions().replace(/<!--[^>]+-->/g, '').trim() + '\n',
313
+ });
314
+ }
315
+
316
+ return { mode, writes };
317
+ }
318
+
319
+ function applyPlan(plan, options) {
320
+ const changes = [];
321
+ for (const write of plan.writes) {
322
+ const before = safeRead(write.path);
323
+ let after;
324
+ if (write.type === 'file') {
325
+ if (write.overwrite === false && before) {
326
+ after = before;
327
+ } else {
328
+ after = write.content;
329
+ }
330
+ } else if (write.type === 'md-block') {
331
+ after = upsertBlock(before, write.block);
332
+ } else if (write.type === 'json-transform') {
333
+ after = write.transform(write.path);
334
+ } else {
335
+ throw new Error(`Unknown write type: ${write.type}`);
336
+ }
337
+
338
+ const changed = before !== after;
339
+ changes.push({ path: write.path, label: write.label, changed });
340
+ if (changed && options.yes && !options.dryRun) {
341
+ fs.mkdirSync(path.dirname(write.path), { recursive: true });
342
+ fs.writeFileSync(write.path, after);
343
+ }
344
+ }
345
+ return changes;
346
+ }
347
+
348
+ async function requestJson(url, options) {
349
+ const res = await fetch(url, options);
350
+ const text = await res.text();
351
+ let json = {};
352
+ try {
353
+ json = text ? JSON.parse(text) : {};
354
+ } catch {
355
+ json = { raw: text.slice(0, 500) };
356
+ }
357
+ if (!res.ok) {
358
+ const message = json.error || json.message || `HTTP ${res.status}`;
359
+ throw new Error(String(message));
360
+ }
361
+ return json.data || json;
362
+ }
363
+
364
+ async function runSelfTest(options) {
365
+ if (!options.selfTest) return { skipped: true, reason: 'disabled' };
366
+ if (!options.apiKey) return { skipped: true, reason: 'missing MARROW_API_KEY' };
367
+
368
+ const headers = {
369
+ authorization: `Bearer ${options.apiKey}`,
370
+ 'content-type': 'application/json',
371
+ 'x-marrow-session-id': `install-${Date.now()}`,
372
+ };
373
+ if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
374
+
375
+ const baseUrl = options.baseUrl.replace(/\/+$/, '');
376
+ const think = await requestJson(`${baseUrl}/v1/agent/think`, {
377
+ method: 'POST',
378
+ headers,
379
+ body: JSON.stringify({
380
+ type: 'process',
381
+ action: 'Marrow passive install self-test: verify SDK/MCP hooks can record a harmless setup event',
382
+ }),
383
+ });
384
+
385
+ const decisionId = think.decision_id || think.decisionId;
386
+ if (!decisionId) throw new Error('self-test did not return decision_id');
387
+
388
+ await requestJson(`${baseUrl}/v1/agent/commit`, {
389
+ method: 'POST',
390
+ headers,
391
+ body: JSON.stringify({
392
+ decision_id: decisionId,
393
+ success: true,
394
+ outcome: 'Marrow passive installer self-test completed successfully',
395
+ }),
396
+ });
397
+
398
+ const status = await requestJson(`${baseUrl}/v1/agent/status`, { headers });
399
+ return {
400
+ skipped: false,
401
+ decision_id: decisionId,
402
+ active: Boolean(status.enabled ?? status.ok),
403
+ health: status.health || null,
404
+ last_event_at: status.last_event_at || null,
405
+ recommended_fix: status.recommended_fix || null,
406
+ };
407
+ }
408
+
409
+ function printReport(report) {
410
+ process.stdout.write(`Marrow passive installer\n`);
411
+ process.stdout.write(`Root: ${report.root}\n`);
412
+ process.stdout.write(`Mode: ${report.mode}\n`);
413
+ process.stdout.write(`Write mode: ${report.writeMode}\n\n`);
414
+
415
+ process.stdout.write('Detected:\n');
416
+ for (const [key, value] of Object.entries(report.detected)) {
417
+ process.stdout.write(`- ${key}: ${value ? 'yes' : 'no'}\n`);
418
+ }
419
+
420
+ process.stdout.write('\nPlanned changes:\n');
421
+ for (const change of report.changes) {
422
+ const marker = change.changed ? (report.writeMode === 'write' ? 'wrote' : 'would write') : 'unchanged';
423
+ process.stdout.write(`- ${marker}: ${change.label} (${change.path})\n`);
424
+ }
425
+
426
+ process.stdout.write('\nSelf-test:\n');
427
+ if (report.selfTest.skipped) {
428
+ process.stdout.write(`- skipped: ${report.selfTest.reason}\n`);
429
+ } else {
430
+ process.stdout.write(`- active: ${report.selfTest.active ? 'yes' : 'no'}\n`);
431
+ process.stdout.write(`- decision_id: ${report.selfTest.decision_id}\n`);
432
+ process.stdout.write(`- health: ${report.selfTest.health || 'unknown'}\n`);
433
+ }
434
+
435
+ if (report.writeMode === 'dry-run') {
436
+ process.stdout.write('\nRun with --yes to write these changes.\n');
437
+ }
438
+
439
+ if (report.warnings.length > 0) {
440
+ process.stdout.write('\nWarnings:\n');
441
+ for (const warning of report.warnings) {
442
+ process.stdout.write(`- ${warning}\n`);
443
+ }
444
+ }
445
+ }
446
+
447
+ async function install(options) {
448
+ const detection = detectEnvironment(options.cwd);
449
+ const plan = buildPlan(detection, options);
450
+ const writeMode = options.yes && !options.dryRun ? 'write' : 'dry-run';
451
+ const changes = applyPlan(plan, options);
452
+ const selfTest = await runSelfTest(options).catch((error) => ({
453
+ skipped: false,
454
+ active: false,
455
+ error: error instanceof Error ? error.message : String(error),
456
+ }));
457
+
458
+ return {
459
+ root: detection.root,
460
+ mode: plan.mode,
461
+ writeMode,
462
+ detected: {
463
+ node: detection.node,
464
+ python: detection.python,
465
+ claudeCode: detection.claudeCode,
466
+ cursor: detection.cursor,
467
+ codex: detection.codex,
468
+ openclaw: detection.openclaw,
469
+ mcpConfig: detection.mcpConfig,
470
+ },
471
+ changes,
472
+ selfTest,
473
+ warnings: options.keyFromArg
474
+ ? ['Avoid --key in shared shells because command-line arguments can be visible in process listings. Prefer MARROW_API_KEY in your environment or secret manager.']
475
+ : [],
476
+ };
477
+ }
478
+
479
+ async function runCli(argv) {
480
+ const options = parseArgs(argv);
481
+ if (options.help) {
482
+ process.stdout.write(usage());
483
+ return;
484
+ }
485
+ const report = await install(options);
486
+ if (options.json) {
487
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
488
+ } else {
489
+ printReport(report);
490
+ }
491
+ }
492
+
493
+ module.exports = {
494
+ parseArgs,
495
+ detectEnvironment,
496
+ buildPlan,
497
+ applyPlan,
498
+ install,
499
+ runSelfTest,
500
+ runCli,
501
+ passiveRuntimeSource,
502
+ };