ma-agents 3.5.6 → 3.6.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.
Files changed (53) hide show
  1. package/.ma-agents.json +10 -0
  2. package/AGENTS.md +97 -0
  3. package/MANIFEST.yaml +3 -0
  4. package/README.md +17 -0
  5. package/_bmad-output/implementation-artifacts/21-10-profile-reconfigure.md +30 -6
  6. package/_bmad-output/implementation-artifacts/21-11-profile-uninstall.md +2 -1
  7. package/_bmad-output/implementation-artifacts/21-2-universal-instruction-block-expansion.md +217 -62
  8. package/_bmad-output/implementation-artifacts/21-3-roomodes-template-bmad-modes.md +196 -73
  9. package/_bmad-output/implementation-artifacts/21-4-agents-md-template-opencode.md +242 -53
  10. package/_bmad-output/implementation-artifacts/21-5-clinerules-template-extension.md +180 -41
  11. package/_bmad-output/implementation-artifacts/21-6-onprem-layered-guardrails.md +250 -75
  12. package/_bmad-output/implementation-artifacts/21-7-bmad-persona-phase-prefix.md +221 -89
  13. package/_bmad-output/implementation-artifacts/21-8-vllm-reference-doc-readme.md +121 -63
  14. package/_bmad-output/implementation-artifacts/21-9-tests-validation.md +332 -61
  15. package/_bmad-output/implementation-artifacts/bug-bmad-recompile-fails-on-airgapped-network.md +112 -0
  16. package/_bmad-output/implementation-artifacts/sprint-status.yaml +3 -2
  17. package/bin/cli.js +59 -0
  18. package/docs/deployment/vllm-nemotron.md +130 -0
  19. package/lib/agents.js +17 -2
  20. package/lib/bmad-customize/bmm-analyst.customize.yaml +8 -0
  21. package/lib/bmad-customize/bmm-architect.customize.yaml +2 -0
  22. package/lib/bmad-customize/bmm-dev.customize.yaml +2 -0
  23. package/lib/bmad-customize/bmm-pm.customize.yaml +2 -0
  24. package/lib/bmad-customize/bmm-qa.customize.yaml +2 -0
  25. package/lib/bmad-customize/bmm-quick-flow-solo-dev.customize.yaml +8 -0
  26. package/lib/bmad-customize/bmm-sm.customize.yaml +2 -0
  27. package/lib/bmad-customize/bmm-tech-writer.customize.yaml +2 -0
  28. package/lib/bmad-customize/bmm-ux-designer.customize.yaml +2 -0
  29. package/lib/bmad.js +293 -1
  30. package/lib/installer.js +617 -43
  31. package/lib/merge/roomodes.js +125 -0
  32. package/lib/profile.js +25 -2
  33. package/lib/reconfigure.js +334 -0
  34. package/lib/templates/agents-md.template.md +67 -0
  35. package/lib/templates/clinerules.template.md +13 -0
  36. package/lib/templates/instruction-block-onprem.template.md +86 -0
  37. package/lib/templates/instruction-block-universal.template.md +29 -0
  38. package/lib/templates/roomodes.template.yaml +96 -0
  39. package/lib/uninstall.js +314 -0
  40. package/package.json +4 -3
  41. package/test/agents-md.test.js +398 -0
  42. package/test/bmad-extension.test.js +2 -2
  43. package/test/bmad-persona-phase-prefix.test.js +271 -0
  44. package/test/clinerules.test.js +339 -0
  45. package/test/instruction-block.test.js +388 -0
  46. package/test/integration-verification.test.js +2 -2
  47. package/test/migration-validation.test.js +2 -2
  48. package/test/offline-recompile.test.js +237 -0
  49. package/test/onprem-injection.test.js +425 -32
  50. package/test/onprem-layer.test.js +419 -0
  51. package/test/reconfigure.test.js +436 -0
  52. package/test/roomodes.test.js +343 -0
  53. package/test/uninstall.test.js +402 -0
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Story 21.10 — Tests for the `reconfigure` subcommand orchestrator.
4
+ *
5
+ * AC coverage (one-to-one with acceptance criteria 1–11):
6
+ * 8.1 --yes rejected with pinned message (AC #6)
7
+ * 8.2 missing .ma-agents.json → named error (AC #1)
8
+ * 8.3 same-value → "Profile unchanged" and no writes (AC #4)
9
+ * 8.4 profile flip standard → on-prem re-stamps injection files + .roomodes (AC #5)
10
+ * 8.5 two-step "Continue?" → no aborts with zero writes (AC #7)
11
+ * 8.6 backup files written at <target>.backup-<ISO> (AC #8)
12
+ * 8.7 slug divergence raises RoomodesSlugDivergenceError; --force bypasses (AC #9)
13
+ * 8.8 dual-file drift raises ClinerulesDualFileDriftError (AC #10)
14
+ * 8.9 profileHistory appends; cap at 20; first call creates field (AC #11)
15
+ * 8.10 full round-trip standard → on-prem → standard (NFR46)
16
+ * 8.11 Current profile appears in the prompt message (AC #2)
17
+ */
18
+ 'use strict';
19
+
20
+ const assert = require('assert');
21
+ const path = require('path');
22
+ const fs = require('fs');
23
+ const os = require('os');
24
+
25
+ let passed = 0;
26
+ let failed = 0;
27
+ const errors = [];
28
+
29
+ async function test(name, fn) {
30
+ try {
31
+ await fn();
32
+ console.log(` \u2713 ${name}`);
33
+ passed++;
34
+ } catch (err) {
35
+ console.error(` \u2717 ${name}: ${err.stack || err.message}`);
36
+ failed++;
37
+ errors.push({ name, error: err.message });
38
+ }
39
+ }
40
+
41
+ const {
42
+ reconfigure,
43
+ RoomodesSlugDivergenceError,
44
+ ManifestNotFoundError,
45
+ ReconfigureYesRejectedError,
46
+ YES_REJECT_MESSAGE,
47
+ PROFILE_HISTORY_CAP,
48
+ listTouchedFiles,
49
+ appendProfileHistory
50
+ } = require('../lib/reconfigure');
51
+ const { setProfile, getProfile } = require('../lib/profile');
52
+ const installer = require('../lib/installer');
53
+ const { getAgent } = require('../lib/agents');
54
+
55
+ function mktemp() {
56
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'ma-agents-reconfigure-test-'));
57
+ }
58
+ function cleanup(dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch {} }
59
+
60
+ function silenceLogs(fn) {
61
+ return async (...args) => {
62
+ const origLog = console.log;
63
+ const origErr = console.error;
64
+ const origWarn = console.warn;
65
+ console.log = () => {};
66
+ console.error = () => {};
67
+ console.warn = () => {};
68
+ try { return await fn(...args); }
69
+ finally {
70
+ console.log = origLog;
71
+ console.error = origErr;
72
+ console.warn = origWarn;
73
+ }
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Build a prompts() stub that returns the given sequence of responses,
79
+ * one per prompt call. Throws if the caller asks for more prompts than
80
+ * responses were scripted.
81
+ */
82
+ function scriptedPrompts(responses) {
83
+ let i = 0;
84
+ const stub = async () => {
85
+ if (i >= responses.length) {
86
+ throw new Error(`scriptedPrompts exhausted after ${responses.length} calls`);
87
+ }
88
+ return responses[i++];
89
+ };
90
+ stub.calls = () => i;
91
+ stub.messages = [];
92
+ return stub;
93
+ }
94
+
95
+ /**
96
+ * Install a minimal project scaffold: .ma-agents.json with a given profile and
97
+ * agent list. Does NOT run the real installer (tests isolate the reconfigure
98
+ * orchestration). Returns the tmp dir.
99
+ */
100
+ function setupProject({ profile = 'standard', agents = ['claude-code'] } = {}) {
101
+ const dir = mktemp();
102
+ const manifest = {
103
+ manifestVersion: '1.2.0',
104
+ agent: agents[0],
105
+ agents,
106
+ scope: 'project',
107
+ profile,
108
+ skills: {}
109
+ };
110
+ fs.writeFileSync(
111
+ path.join(dir, '.ma-agents.json'),
112
+ JSON.stringify(manifest, null, 2) + '\n',
113
+ 'utf-8'
114
+ );
115
+ return dir;
116
+ }
117
+
118
+ console.log('\n reconfigure tests (Story 21.10)\n');
119
+
120
+ async function runAll() {
121
+ // 8.1 --yes rejected
122
+ await test('8.1 --yes rejected with pinned message and ReconfigureYesRejectedError (AC #6)', silenceLogs(async () => {
123
+ const dir = setupProject();
124
+ try {
125
+ await assert.rejects(
126
+ () => reconfigure({ projectRoot: dir, argv: ['--yes'], promptsLib: scriptedPrompts([]) }),
127
+ (err) => err instanceof ReconfigureYesRejectedError && err.message === YES_REJECT_MESSAGE
128
+ );
129
+ // Manifest should be untouched.
130
+ const json = JSON.parse(fs.readFileSync(path.join(dir, '.ma-agents.json'), 'utf-8'));
131
+ assert.strictEqual(json.profile, 'standard');
132
+ assert.strictEqual(json.profileHistory, undefined);
133
+ } finally { cleanup(dir); }
134
+ }));
135
+
136
+ // 8.2 missing manifest
137
+ await test('8.2 missing .ma-agents.json → ManifestNotFoundError (AC #1)', silenceLogs(async () => {
138
+ const dir = mktemp();
139
+ try {
140
+ await assert.rejects(
141
+ () => reconfigure({ projectRoot: dir, argv: [], promptsLib: scriptedPrompts([]) }),
142
+ (err) => err instanceof ManifestNotFoundError
143
+ );
144
+ } finally { cleanup(dir); }
145
+ }));
146
+
147
+ // 8.3 same-value short-circuit
148
+ await test('8.3 same-value exits with "Profile unchanged:" and no writes (AC #4)', silenceLogs(async () => {
149
+ const dir = setupProject({ profile: 'standard' });
150
+ try {
151
+ const manifestPath = path.join(dir, '.ma-agents.json');
152
+ const originalBytes = fs.readFileSync(manifestPath);
153
+ const result = await reconfigure({
154
+ projectRoot: dir,
155
+ argv: [],
156
+ promptsLib: scriptedPrompts([{ chosenProfile: 'standard' }])
157
+ });
158
+ assert.strictEqual(result.status, 'unchanged');
159
+ // No writes → byte-identical manifest.
160
+ assert.deepStrictEqual(fs.readFileSync(manifestPath), originalBytes);
161
+ } finally { cleanup(dir); }
162
+ }));
163
+
164
+ // 8.4 profile flip re-stamps injection files + .roomodes
165
+ await test('8.4 profile flip standard→on-prem re-stamps injection files (AC #5)', silenceLogs(async () => {
166
+ const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
167
+ try {
168
+ // Pre-stamp the .claude/CLAUDE.md with standard content via the installer
169
+ // helper so we have a realistic starting state.
170
+ const agent = getAgent('claude-code');
171
+ await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
172
+ const claudePath = path.join(dir, '.claude', 'CLAUDE.md');
173
+ const before = fs.readFileSync(claudePath, 'utf-8');
174
+
175
+ const result = await reconfigure({
176
+ projectRoot: dir,
177
+ argv: [],
178
+ promptsLib: scriptedPrompts([
179
+ { chosenProfile: 'on-prem' }, // profile choice
180
+ { proceed: true } // Continue?
181
+ ])
182
+ });
183
+ assert.strictEqual(result.status, 'reconfigured');
184
+ const after = fs.readFileSync(claudePath, 'utf-8');
185
+ assert.notStrictEqual(after, before, 'file must change');
186
+ // On-prem content ships onprem-specific strings (e.g. /no_think).
187
+ assert.match(after, /\/no_think|str_replace_editor|~\/\.claude\/|Local-LLM|Nemotron|on-prem/i,
188
+ 'on-prem profile output should mention a local-LLM-specific token');
189
+ assert.strictEqual(getProfile(dir), 'on-prem');
190
+ } finally { cleanup(dir); }
191
+ }));
192
+
193
+ // 8.5 two-step confirmation decline
194
+ await test('8.5 declining "Continue?" aborts with zero writes (AC #7)', silenceLogs(async () => {
195
+ const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
196
+ try {
197
+ // Seed .claude/CLAUDE.md with standard content so a real re-stamp would
198
+ // leave a visible diff.
199
+ const agent = getAgent('claude-code');
200
+ await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
201
+ const claudePath = path.join(dir, '.claude', 'CLAUDE.md');
202
+ const before = fs.readFileSync(claudePath);
203
+ const manifestBefore = fs.readFileSync(path.join(dir, '.ma-agents.json'));
204
+
205
+ const result = await reconfigure({
206
+ projectRoot: dir,
207
+ argv: [],
208
+ promptsLib: scriptedPrompts([
209
+ { chosenProfile: 'on-prem' },
210
+ { proceed: false }
211
+ ])
212
+ });
213
+ assert.strictEqual(result.status, 'aborted');
214
+ assert.deepStrictEqual(fs.readFileSync(claudePath), before, 'CLAUDE.md must be untouched');
215
+ assert.deepStrictEqual(
216
+ fs.readFileSync(path.join(dir, '.ma-agents.json')),
217
+ manifestBefore,
218
+ 'manifest must be untouched when user declines'
219
+ );
220
+ } finally { cleanup(dir); }
221
+ }));
222
+
223
+ // 8.6 backup files are written
224
+ await test('8.6 backup files created at <target>.backup-<timestamp> (AC #8)', silenceLogs(async () => {
225
+ const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
226
+ try {
227
+ const agent = getAgent('claude-code');
228
+ await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
229
+ await reconfigure({
230
+ projectRoot: dir,
231
+ argv: [],
232
+ promptsLib: scriptedPrompts([
233
+ { chosenProfile: 'on-prem' },
234
+ { proceed: true }
235
+ ])
236
+ });
237
+ const claudeDir = path.join(dir, '.claude');
238
+ const backups = fs.readdirSync(claudeDir).filter(f => /CLAUDE\.md\.backup-/.test(f));
239
+ assert.ok(backups.length >= 1, `expected at least one backup file, got ${backups.join(',')}`);
240
+ const backupName = backups[0];
241
+ // Canonical format: <target>.backup-<YYYY-MM-DDTHH-mm-ssZ>
242
+ assert.match(backupName, /^CLAUDE\.md\.backup-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(\.\d+)?$/);
243
+ } finally { cleanup(dir); }
244
+ }));
245
+
246
+ // 8.7 slug divergence
247
+ await test('8.7 RoomodesSlugDivergenceError raised when user edited owned slug; --force bypasses (AC #9)', silenceLogs(async () => {
248
+ const dir = setupProject({ profile: 'standard', agents: ['roo-code'] });
249
+ try {
250
+ // Write a .roomodes whose `bmad-architect` entry has a fabricated
251
+ // `whenToUse` that doesn't match the shipped template.
252
+ const yaml = require('js-yaml');
253
+ const divergent = {
254
+ customModes: [
255
+ { slug: 'bmad-architect', name: 'Hacked', whenToUse: 'different', roleDefinition: 'x', groups: ['read'], customInstructions: 'x' }
256
+ ]
257
+ };
258
+ fs.writeFileSync(path.join(dir, '.roomodes'), yaml.dump(divergent), 'utf-8');
259
+
260
+ await assert.rejects(
261
+ () => reconfigure({
262
+ projectRoot: dir,
263
+ argv: [],
264
+ promptsLib: scriptedPrompts([{ chosenProfile: 'on-prem' }])
265
+ }),
266
+ (err) => err instanceof RoomodesSlugDivergenceError && err.divergentSlugs.includes('bmad-architect')
267
+ );
268
+
269
+ // With --force-roomodes-overwrite, proceed through (confirm continue).
270
+ const result = await reconfigure({
271
+ projectRoot: dir,
272
+ argv: ['--force-roomodes-overwrite'],
273
+ promptsLib: scriptedPrompts([
274
+ { chosenProfile: 'on-prem' },
275
+ { proceed: true }
276
+ ])
277
+ });
278
+ assert.strictEqual(result.status, 'reconfigured');
279
+ } finally { cleanup(dir); }
280
+ }));
281
+
282
+ // 8.8 dual-file drift (Cline)
283
+ await test('8.8 ClinerulesDualFileDriftError raised; no --force override (AC #10)', silenceLogs(async () => {
284
+ const dir = setupProject({ profile: 'standard', agents: ['cline'] });
285
+ try {
286
+ // Seed with identical content first.
287
+ const agent = getAgent('cline');
288
+ await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
289
+ // Mutate ONE of the two files inside the markers to induce drift.
290
+ const fileA = path.join(dir, '.cline', 'clinerules.md');
291
+ const content = fs.readFileSync(fileA, 'utf-8');
292
+ const mutated = content.replace(
293
+ /<!-- MA-AGENTS-START -->[\s\S]*?<!-- MA-AGENTS-END -->/,
294
+ '<!-- MA-AGENTS-START -->\nCLI-NO-MATCHY\n<!-- MA-AGENTS-END -->'
295
+ );
296
+ fs.writeFileSync(fileA, mutated, 'utf-8');
297
+
298
+ await assert.rejects(
299
+ () => reconfigure({
300
+ projectRoot: dir,
301
+ argv: [],
302
+ promptsLib: scriptedPrompts([{ chosenProfile: 'on-prem' }])
303
+ }),
304
+ (err) => err && err.name === 'ClinerulesDualFileDriftError'
305
+ );
306
+ } finally { cleanup(dir); }
307
+ }));
308
+
309
+ // 8.9 profileHistory append + cap
310
+ await test('8.9 profileHistory appends; missing-field creation; 20-cap evicts oldest (AC #11)', silenceLogs(async () => {
311
+ const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
312
+ try {
313
+ // First reconfigure creates the field.
314
+ await reconfigure({
315
+ projectRoot: dir,
316
+ argv: [],
317
+ promptsLib: scriptedPrompts([
318
+ { chosenProfile: 'on-prem' },
319
+ { proceed: true }
320
+ ])
321
+ });
322
+ let manifest = JSON.parse(fs.readFileSync(path.join(dir, '.ma-agents.json'), 'utf-8'));
323
+ assert.ok(Array.isArray(manifest.profileHistory));
324
+ assert.strictEqual(manifest.profileHistory.length, 1);
325
+ assert.deepStrictEqual(
326
+ { from: manifest.profileHistory[0].from, to: manifest.profileHistory[0].to, source: manifest.profileHistory[0].source },
327
+ { from: 'standard', to: 'on-prem', source: 'reconfigure' }
328
+ );
329
+ assert.ok(typeof manifest.profileHistory[0].date === 'string');
330
+
331
+ // Now directly invoke appendProfileHistory 25 more times to exercise the cap.
332
+ for (let i = 0; i < 25; i++) {
333
+ appendProfileHistory(dir, {
334
+ date: new Date(2027, 0, i + 1).toISOString(),
335
+ from: 'standard',
336
+ to: 'on-prem',
337
+ source: 'reconfigure'
338
+ });
339
+ }
340
+ manifest = JSON.parse(fs.readFileSync(path.join(dir, '.ma-agents.json'), 'utf-8'));
341
+ assert.strictEqual(
342
+ manifest.profileHistory.length,
343
+ PROFILE_HISTORY_CAP,
344
+ `history should be capped at ${PROFILE_HISTORY_CAP}`
345
+ );
346
+ // Oldest-first eviction: the first entry we wrote (the real reconfigure
347
+ // result) must be gone. The newest entry's date must survive.
348
+ const dates = manifest.profileHistory.map(e => e.date);
349
+ assert.ok(!dates.includes(manifest.profileHistory[0].date === manifest.profileHistory[0].date && undefined));
350
+ const newest = new Date(2027, 0, 25).toISOString();
351
+ assert.ok(dates.includes(newest), 'newest entry must be preserved');
352
+ } finally { cleanup(dir); }
353
+ }));
354
+
355
+ // 8.10 round-trip standard → on-prem → standard
356
+ await test('8.10 round-trip standard → on-prem → standard lands on standard content (NFR46)', silenceLogs(async () => {
357
+ const dir = setupProject({ profile: 'standard', agents: ['claude-code'] });
358
+ try {
359
+ const agent = getAgent('claude-code');
360
+ await installer._testUpdateAgentInstructions(agent, dir, { yesMode: true });
361
+ const baselineStandard = fs.readFileSync(path.join(dir, '.claude', 'CLAUDE.md'), 'utf-8');
362
+
363
+ await reconfigure({
364
+ projectRoot: dir,
365
+ argv: [],
366
+ promptsLib: scriptedPrompts([
367
+ { chosenProfile: 'on-prem' },
368
+ { proceed: true }
369
+ ])
370
+ });
371
+ assert.strictEqual(getProfile(dir), 'on-prem');
372
+
373
+ await reconfigure({
374
+ projectRoot: dir,
375
+ argv: [],
376
+ promptsLib: scriptedPrompts([
377
+ { chosenProfile: 'standard' },
378
+ { proceed: true }
379
+ ])
380
+ });
381
+ assert.strictEqual(getProfile(dir), 'standard');
382
+ const after = fs.readFileSync(path.join(dir, '.claude', 'CLAUDE.md'), 'utf-8');
383
+ // Compare MARKER block content only, since outside-markers content may
384
+ // accumulate nothing here but we want a robust assertion.
385
+ const extract = (s) => {
386
+ const m = s.match(/<!-- MA-AGENTS-START -->([\s\S]*?)<!-- MA-AGENTS-END -->/);
387
+ return m ? m[1] : null;
388
+ };
389
+ assert.strictEqual(extract(after), extract(baselineStandard),
390
+ 'post round-trip marker block must equal the original standard-profile stamp');
391
+
392
+ // Two history entries.
393
+ const manifest = JSON.parse(fs.readFileSync(path.join(dir, '.ma-agents.json'), 'utf-8'));
394
+ assert.strictEqual(manifest.profileHistory.length, 2);
395
+ assert.strictEqual(manifest.profileHistory[0].to, 'on-prem');
396
+ assert.strictEqual(manifest.profileHistory[1].to, 'standard');
397
+ } finally { cleanup(dir); }
398
+ }));
399
+
400
+ // 8.11 prompt text references current value
401
+ await test('8.11 prompt message references current profile value (AC #2)', silenceLogs(async () => {
402
+ const dir = setupProject({ profile: 'on-prem', agents: ['claude-code'] });
403
+ try {
404
+ let seenMessage = null;
405
+ let seenInitial = null;
406
+ const stub = async (opts) => {
407
+ if (opts && opts.type === 'select' && !seenMessage) {
408
+ seenMessage = opts.message;
409
+ seenInitial = opts.initial;
410
+ return { chosenProfile: 'on-prem' }; // same-value → early exit
411
+ }
412
+ return { proceed: false };
413
+ };
414
+ await reconfigure({ projectRoot: dir, argv: [], promptsLib: stub });
415
+ assert.match(seenMessage || '', /Current profile: on-prem\. Change to\?/);
416
+ // AC: default-highlighted option is the persisted value → initial index 0 (on-prem row).
417
+ assert.strictEqual(seenInitial, 0);
418
+ } finally { cleanup(dir); }
419
+ }));
420
+
421
+ // listTouchedFiles smoke
422
+ await test('listTouchedFiles returns instructionFiles + extraInstructionTemplates, sorted', () => {
423
+ const files = listTouchedFiles('/tmp/x', [getAgent('roo-code')]);
424
+ assert.ok(files.includes('.roomodes'));
425
+ assert.ok(files.includes('.roo/rules/00-ma-agents.md'));
426
+ });
427
+ }
428
+
429
+ runAll().then(() => {
430
+ console.log(`\n ${passed} passed, ${failed} failed\n`);
431
+ if (failed > 0) {
432
+ console.error('FAILURES:');
433
+ errors.forEach(e => console.error(` - ${e.name}: ${e.error}`));
434
+ process.exit(1);
435
+ }
436
+ });