ma-agents 3.0.1 → 3.2.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 (35) hide show
  1. package/.opencode/skills/.ma-agents.json +99 -99
  2. package/README.md +48 -2
  3. package/bin/cli.js +546 -2
  4. package/lib/bmad-extension/module-help.csv +8 -4
  5. package/lib/bmad-extension/skills/add-sprint/SKILL.md +126 -40
  6. package/lib/bmad-extension/skills/add-to-sprint/SKILL.md +116 -142
  7. package/lib/bmad-extension/skills/cleanup-done/.gitkeep +0 -0
  8. package/lib/bmad-extension/skills/cleanup-done/SKILL.md +159 -0
  9. package/lib/bmad-extension/skills/cleanup-done/bmad-skill-manifest.yaml +3 -0
  10. package/lib/bmad-extension/skills/create-bug-story/SKILL.md +75 -7
  11. package/lib/bmad-extension/skills/generate-backlog/SKILL.md +183 -0
  12. package/lib/bmad-extension/skills/generate-backlog/bmad-skill-manifest.yaml +3 -0
  13. package/lib/bmad-extension/skills/modify-sprint/SKILL.md +63 -0
  14. package/lib/bmad-extension/skills/prioritize-backlog/.gitkeep +0 -0
  15. package/lib/bmad-extension/skills/prioritize-backlog/SKILL.md +195 -0
  16. package/lib/bmad-extension/skills/prioritize-backlog/bmad-skill-manifest.yaml +3 -0
  17. package/lib/bmad-extension/skills/remove-from-sprint/.gitkeep +0 -0
  18. package/lib/bmad-extension/skills/remove-from-sprint/SKILL.md +163 -0
  19. package/lib/bmad-extension/skills/remove-from-sprint/bmad-skill-manifest.yaml +3 -0
  20. package/lib/bmad-extension/skills/sprint-status-view/SKILL.md +199 -138
  21. package/lib/bmad-extension/workflows/add-sprint/workflow.md +129 -39
  22. package/lib/bmad-extension/workflows/add-to-sprint/workflow.md +3 -205
  23. package/lib/bmad-extension/workflows/modify-sprint/workflow.md +5 -0
  24. package/lib/bmad-extension/workflows/sprint-status-view/workflow.md +3 -192
  25. package/lib/installer.js +109 -2
  26. package/lib/templates/project-context.template.md +1 -1
  27. package/package.json +2 -2
  28. package/test/cicd-remote-mode.test.js +224 -0
  29. package/test/config-layout.test.js +230 -0
  30. package/test/config-lost-on-update.test.js +363 -0
  31. package/test/config-storage.test.js +275 -0
  32. package/test/cross-repo-validation.test.js +201 -0
  33. package/test/generate-project-context.test.js +148 -2
  34. package/test/portable-paths.test.js +268 -0
  35. package/test/repo-layout.test.js +246 -0
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for Story 16.8: CI/CD Remote Repository Mode
4
+ * Tests collectRepoLayout() with git URL env vars, ciCloneIfNeeded()
5
+ */
6
+ 'use strict';
7
+
8
+ const assert = require('assert');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+
13
+ let passed = 0;
14
+ let failed = 0;
15
+ const errors = [];
16
+
17
+ function test(name, fn) {
18
+ try {
19
+ fn();
20
+ console.log(` \u2713 ${name}`);
21
+ passed++;
22
+ } catch (err) {
23
+ console.error(` \u2717 ${name}: ${err.message}`);
24
+ failed++;
25
+ errors.push({ name, error: err.message });
26
+ }
27
+ }
28
+
29
+ async function asyncTest(name, fn) {
30
+ try {
31
+ await fn();
32
+ console.log(` \u2713 ${name}`);
33
+ passed++;
34
+ } catch (err) {
35
+ console.error(` \u2717 ${name}: ${err.message}`);
36
+ failed++;
37
+ errors.push({ name, error: err.message });
38
+ }
39
+ }
40
+
41
+ // ─── Load module ─────────────────────────────────────────────────────────────
42
+ let collectRepoLayout, ciCloneIfNeeded;
43
+ try {
44
+ ({ collectRepoLayout, ciCloneIfNeeded } = require('../bin/cli.js'));
45
+ } catch (e) {
46
+ console.error('\n FATAL: Cannot load exports from cli.js');
47
+ console.error(' ', e.message);
48
+ process.exit(1);
49
+ }
50
+
51
+ // Helper: save and restore env vars
52
+ function withEnv(vars, fn) {
53
+ const saved = {};
54
+ const envKeys = [
55
+ 'MA_KNOWLEDGEBASE_PATH', 'MA_SPRINT_PATH',
56
+ 'MA_KNOWLEDGEBASE_GIT_URL', 'MA_SPRINT_GIT_URL'
57
+ ];
58
+ for (const k of envKeys) {
59
+ saved[k] = process.env[k];
60
+ delete process.env[k];
61
+ }
62
+ for (const [k, v] of Object.entries(vars)) {
63
+ process.env[k] = v;
64
+ }
65
+ try {
66
+ return fn();
67
+ } finally {
68
+ for (const k of envKeys) {
69
+ if (saved[k] !== undefined) process.env[k] = saved[k];
70
+ else delete process.env[k];
71
+ }
72
+ }
73
+ }
74
+
75
+ // ─── collectRepoLayout: remote mode via env vars ───────────────────────────
76
+ console.log('\ncollectRepoLayout: CI/CD remote mode');
77
+
78
+ asyncTest('--yes + git URL + path returns remote mode', async () => {
79
+ await withEnv({
80
+ MA_KNOWLEDGEBASE_PATH: os.tmpdir(),
81
+ MA_KNOWLEDGEBASE_GIT_URL: 'https://git.example.com/kb.git',
82
+ }, async () => {
83
+ // Create a fake .git dir at tmpdir so clone is skipped
84
+ const gitDir = path.join(os.tmpdir(), '.git');
85
+ const hadGitDir = fs.existsSync(gitDir);
86
+ if (!hadGitDir) fs.mkdirSync(gitDir, { recursive: true });
87
+ try {
88
+ const result = await collectRepoLayout({ yes: true }, null);
89
+ assert.strictEqual(result.knowledgebase.mode, 'remote');
90
+ assert.strictEqual(result.knowledgebase.gitUrl, 'https://git.example.com/kb.git');
91
+ assert.strictEqual(result.knowledgebase.path, path.resolve(os.tmpdir()));
92
+ } finally {
93
+ if (!hadGitDir && fs.existsSync(gitDir)) fs.rmdirSync(gitDir);
94
+ }
95
+ });
96
+ });
97
+
98
+ asyncTest('--yes + path only (no git URL) returns local mode (backward compat)', async () => {
99
+ await withEnv({
100
+ MA_KNOWLEDGEBASE_PATH: '/some/local/path',
101
+ }, async () => {
102
+ const result = await collectRepoLayout({ yes: true }, null);
103
+ assert.strictEqual(result.knowledgebase.mode, 'local');
104
+ assert.strictEqual(result.knowledgebase.path, path.resolve('/some/local/path'));
105
+ assert.strictEqual(result.knowledgebase.gitUrl, undefined, 'should not have gitUrl');
106
+ });
107
+ });
108
+
109
+ asyncTest('--yes + no env vars defaults to single-repo', async () => {
110
+ await withEnv({}, async () => {
111
+ const result = await collectRepoLayout({ yes: true }, null);
112
+ assert.strictEqual(result.knowledgebase.mode, 'same');
113
+ assert.strictEqual(result.sprintManagement.mode, 'same');
114
+ });
115
+ });
116
+
117
+ asyncTest('--yes + sprint git URL + path returns remote for sprint', async () => {
118
+ await withEnv({
119
+ MA_SPRINT_PATH: os.tmpdir(),
120
+ MA_SPRINT_GIT_URL: 'https://git.example.com/sprint.git',
121
+ }, async () => {
122
+ const gitDir = path.join(os.tmpdir(), '.git');
123
+ const hadGitDir = fs.existsSync(gitDir);
124
+ if (!hadGitDir) fs.mkdirSync(gitDir, { recursive: true });
125
+ try {
126
+ const result = await collectRepoLayout({ yes: true }, null);
127
+ assert.strictEqual(result.sprintManagement.mode, 'remote');
128
+ assert.strictEqual(result.sprintManagement.gitUrl, 'https://git.example.com/sprint.git');
129
+ assert.strictEqual(result.knowledgebase.mode, 'same', 'kb should default when not set');
130
+ } finally {
131
+ if (!hadGitDir && fs.existsSync(gitDir)) fs.rmdirSync(gitDir);
132
+ }
133
+ });
134
+ });
135
+
136
+ // ─── ciCloneIfNeeded ───────────────────────────────────────────────────────
137
+ console.log('\nciCloneIfNeeded');
138
+
139
+ test('skips clone when .git directory exists at destination', () => {
140
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-test-'));
141
+ try {
142
+ fs.mkdirSync(path.join(tmpDir, '.git'));
143
+ const clonedRepos = new Map();
144
+ // Should not throw — just logs and returns
145
+ ciCloneIfNeeded(tmpDir, 'https://git.example.com/repo.git', clonedRepos);
146
+ assert.ok(clonedRepos.has(`https://git.example.com/repo.git::${tmpDir}`), 'should mark as cloned');
147
+ } finally {
148
+ fs.rmSync(tmpDir, { recursive: true, force: true });
149
+ }
150
+ });
151
+
152
+ test('deduplicates: same URL+path only processed once', () => {
153
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-test-'));
154
+ try {
155
+ fs.mkdirSync(path.join(tmpDir, '.git'));
156
+ const clonedRepos = new Map();
157
+ ciCloneIfNeeded(tmpDir, 'https://git.example.com/repo.git', clonedRepos);
158
+ // Second call should be a no-op
159
+ ciCloneIfNeeded(tmpDir, 'https://git.example.com/repo.git', clonedRepos);
160
+ assert.strictEqual(clonedRepos.size, 1, 'should only have one entry');
161
+ } finally {
162
+ fs.rmSync(tmpDir, { recursive: true, force: true });
163
+ }
164
+ });
165
+
166
+ test('throws error when destination exists but is not a git repo', () => {
167
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-test-'));
168
+ try {
169
+ // tmpDir exists but has no .git/ — should throw
170
+ try {
171
+ ciCloneIfNeeded(tmpDir, 'https://git.example.com/repo.git', new Map());
172
+ assert.fail('should have thrown');
173
+ } catch (e) {
174
+ assert.ok(e.message.includes('not a git repo'), `error should mention "not a git repo", got: ${e.message}`);
175
+ }
176
+ } finally {
177
+ fs.rmSync(tmpDir, { recursive: true, force: true });
178
+ }
179
+ });
180
+
181
+ // ─── git URL without path env var ──────────────────────────────────────────
182
+ console.log('\ncollectRepoLayout: validation errors');
183
+
184
+ asyncTest('git URL without path env var throws error (knowledgebase)', async () => {
185
+ try {
186
+ await withEnv({
187
+ MA_KNOWLEDGEBASE_GIT_URL: 'https://git.example.com/kb.git',
188
+ // MA_KNOWLEDGEBASE_PATH deliberately NOT set
189
+ }, async () => {
190
+ await collectRepoLayout({ yes: true }, null);
191
+ });
192
+ assert.fail('should have thrown');
193
+ } catch (e) {
194
+ assert.ok(e.message.includes('MA_KNOWLEDGEBASE_GIT_URL requires MA_KNOWLEDGEBASE_PATH'),
195
+ `should mention required PATH, got: ${e.message}`);
196
+ }
197
+ });
198
+
199
+ asyncTest('git URL without path env var throws error (sprint)', async () => {
200
+ try {
201
+ await withEnv({
202
+ MA_SPRINT_GIT_URL: 'https://git.example.com/sprint.git',
203
+ }, async () => {
204
+ await collectRepoLayout({ yes: true }, null);
205
+ });
206
+ assert.fail('should have thrown');
207
+ } catch (e) {
208
+ assert.ok(e.message.includes('MA_SPRINT_GIT_URL requires MA_SPRINT_PATH'),
209
+ `should mention required PATH, got: ${e.message}`);
210
+ }
211
+ });
212
+
213
+ // ─── Summary ───────────────────────────────────────────────────────────────
214
+ async function runAll() {
215
+ await new Promise(resolve => setTimeout(resolve, 100));
216
+ console.log(`\n${passed} passed, ${failed} failed`);
217
+ if (errors.length > 0) {
218
+ console.log('\nFailed tests:');
219
+ errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
220
+ }
221
+ if (failed > 0) process.exit(1);
222
+ }
223
+
224
+ runAll();
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Tests for Story 16.9: Reconfigure Layout Workflow
4
+ * Tests showCurrentLayout(), handleConfigLayout(), and CLI command routing
5
+ */
6
+ 'use strict';
7
+
8
+ const assert = require('assert');
9
+ const path = require('path');
10
+ const fs = require('fs');
11
+ const os = require('os');
12
+
13
+ let passed = 0;
14
+ let failed = 0;
15
+ const errors = [];
16
+
17
+ function test(name, fn) {
18
+ try {
19
+ fn();
20
+ console.log(` \u2713 ${name}`);
21
+ passed++;
22
+ } catch (err) {
23
+ console.error(` \u2717 ${name}: ${err.message}`);
24
+ failed++;
25
+ errors.push({ name, error: err.message });
26
+ }
27
+ }
28
+
29
+ async function asyncTest(name, fn) {
30
+ try {
31
+ await fn();
32
+ console.log(` \u2713 ${name}`);
33
+ passed++;
34
+ } catch (err) {
35
+ console.error(` \u2717 ${name}: ${err.message}`);
36
+ failed++;
37
+ errors.push({ name, error: err.message });
38
+ }
39
+ }
40
+
41
+ // ─── Load module ─────────────────────────────────────────────────────────────
42
+ let showCurrentLayout, handleConfigLayout, writeProjectLayoutYaml, writeRepoLayoutConfig, readExistingLayout;
43
+ try {
44
+ ({ showCurrentLayout, handleConfigLayout, writeProjectLayoutYaml, writeRepoLayoutConfig, readExistingLayout } = require('../bin/cli.js'));
45
+ } catch (e) {
46
+ console.error('\n FATAL: Cannot load exports from cli.js');
47
+ console.error(' ', e.message);
48
+ process.exit(1);
49
+ }
50
+
51
+ function withTmpDir() {
52
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-test-'));
53
+ const origCwd = process.cwd();
54
+ process.chdir(tmpDir);
55
+ return { tmpDir, cleanup() { process.chdir(origCwd); fs.rmSync(tmpDir, { recursive: true, force: true }); } };
56
+ }
57
+
58
+ // ─── showCurrentLayout ─────────────────────────────────────────────────────
59
+ console.log('\nshowCurrentLayout');
60
+
61
+ test('--show with no config displays single-repo default', () => {
62
+ const { cleanup } = withTmpDir();
63
+ try {
64
+ // Capture console output
65
+ const logs = [];
66
+ const origLog = console.log;
67
+ console.log = (...args) => logs.push(args.join(' '));
68
+ showCurrentLayout();
69
+ console.log = origLog;
70
+ const output = logs.join('\n');
71
+ assert.ok(output.includes('Single-repo') || output.includes('single-repo') || output.includes('.'),
72
+ `should mention single-repo, got: ${output}`);
73
+ } finally {
74
+ cleanup();
75
+ }
76
+ });
77
+
78
+ test('--show with multi-repo config displays current layout', () => {
79
+ const { tmpDir, cleanup } = withTmpDir();
80
+ try {
81
+ // Write a layout file
82
+ const dir = path.join(tmpDir, '_bmad-output');
83
+ fs.mkdirSync(dir, { recursive: true });
84
+ fs.writeFileSync(path.join(dir, 'project-layout.yaml'), [
85
+ 'knowledgebase:',
86
+ ' mode: local',
87
+ ' path: "../kb-repo"',
88
+ 'sprint_management:',
89
+ ' mode: same',
90
+ ' path: "."',
91
+ ].join('\n'));
92
+
93
+ const logs = [];
94
+ const origLog = console.log;
95
+ console.log = (...args) => logs.push(args.join(' '));
96
+ showCurrentLayout();
97
+ console.log = origLog;
98
+ const output = logs.join('\n');
99
+ assert.ok(output.includes('Knowledgebase') || output.includes('knowledgebase'),
100
+ `should display knowledgebase, got: ${output}`);
101
+ assert.ok(output.includes('local'), `should display mode, got: ${output}`);
102
+ } finally {
103
+ cleanup();
104
+ }
105
+ });
106
+
107
+ // ─── CLI routing ───────────────────────────────────────────────────────────
108
+ console.log('\nCLI routing');
109
+
110
+ test('config command without subcommand exits with error', () => {
111
+ const { execFileSync } = require('child_process');
112
+ try {
113
+ execFileSync('node', [path.join(__dirname, '..', 'bin', 'cli.js'), 'config'], { stdio: 'pipe' });
114
+ assert.fail('should have exited with error');
115
+ } catch (e) {
116
+ assert.ok(e.status === 1, `should exit with code 1, got ${e.status}`);
117
+ assert.ok(e.stderr.toString().includes('Unknown config subcommand'), `should mention unknown subcommand`);
118
+ }
119
+ });
120
+
121
+ test('config layout --show does not modify files', () => {
122
+ const { tmpDir, cleanup } = withTmpDir();
123
+ try {
124
+ // Note: this just tests that the command runs without error in single-repo mode
125
+ const { execFileSync } = require('child_process');
126
+ const result = execFileSync('node', [
127
+ path.join(__dirname, '..', 'bin', 'cli.js'), 'config', 'layout', '--show'
128
+ ], { stdio: 'pipe', cwd: tmpDir });
129
+ const output = result.toString();
130
+ assert.ok(output.includes('Single-repo') || output.includes('single-repo') || output.includes('.'),
131
+ `should display layout info, got: ${output}`);
132
+ } finally {
133
+ cleanup();
134
+ }
135
+ });
136
+
137
+ // ─── Sequential async runner ───────────────────────────────────────────────
138
+ (async () => {
139
+ // Run async tests sequentially to avoid cwd conflicts
140
+ console.log('\nhandleConfigLayout --yes mode (sequential)');
141
+
142
+ // Test: --yes reconfigure writes updated config files
143
+ await asyncTest('--yes reconfigure writes updated config files', async () => {
144
+ const tmpDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-test-'));
145
+ const origCwd2 = process.cwd();
146
+ process.chdir(tmpDir2);
147
+ const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
148
+ const origSp = process.env.MA_SPRINT_PATH;
149
+ const origKbGit = process.env.MA_KNOWLEDGEBASE_GIT_URL;
150
+ const origSpGit = process.env.MA_SPRINT_GIT_URL;
151
+ try {
152
+ const configDir = path.join(tmpDir2, '_bmad', 'bmm');
153
+ fs.mkdirSync(configDir, { recursive: true });
154
+ fs.writeFileSync(path.join(configDir, 'config.yaml'), '# config\nknowledgebase_path: "."\nsprint_management_path: "."');
155
+
156
+ const kbPath = path.join(path.dirname(tmpDir2), 'kb-repo');
157
+ process.env.MA_KNOWLEDGEBASE_PATH = kbPath;
158
+ delete process.env.MA_SPRINT_PATH;
159
+ delete process.env.MA_KNOWLEDGEBASE_GIT_URL;
160
+ delete process.env.MA_SPRINT_GIT_URL;
161
+
162
+ await handleConfigLayout(['--yes']);
163
+
164
+ const config = fs.readFileSync(path.join(configDir, 'config.yaml'), 'utf-8');
165
+ assert.ok(config.includes('kb-repo'), `kb path should reference kb-repo, got: ${config}`);
166
+ assert.ok(fs.existsSync(path.join(tmpDir2, '_bmad-output', 'project-layout.yaml')),
167
+ 'project-layout.yaml should be created');
168
+ } finally {
169
+ if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
170
+ else delete process.env.MA_KNOWLEDGEBASE_PATH;
171
+ if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
172
+ else delete process.env.MA_SPRINT_PATH;
173
+ if (origKbGit !== undefined) process.env.MA_KNOWLEDGEBASE_GIT_URL = origKbGit;
174
+ else delete process.env.MA_KNOWLEDGEBASE_GIT_URL;
175
+ if (origSpGit !== undefined) process.env.MA_SPRINT_GIT_URL = origSpGit;
176
+ else delete process.env.MA_SPRINT_GIT_URL;
177
+ process.chdir(origCwd2);
178
+ fs.rmSync(tmpDir2, { recursive: true, force: true });
179
+ }
180
+ });
181
+
182
+ // Test: --yes preserves existing layout
183
+ await asyncTest('--yes preserves existing layout when no env vars', async () => {
184
+ const tmpDir3 = fs.mkdtempSync(path.join(os.tmpdir(), 'ma-test-'));
185
+ const origCwd3 = process.cwd();
186
+ process.chdir(tmpDir3);
187
+ const origKb = process.env.MA_KNOWLEDGEBASE_PATH;
188
+ const origSp = process.env.MA_SPRINT_PATH;
189
+ const origKbGit = process.env.MA_KNOWLEDGEBASE_GIT_URL;
190
+ const origSpGit = process.env.MA_SPRINT_GIT_URL;
191
+ try {
192
+ const configDir = path.join(tmpDir3, '_bmad', 'bmm');
193
+ fs.mkdirSync(configDir, { recursive: true });
194
+ fs.writeFileSync(path.join(configDir, 'config.yaml'), '# config\nknowledgebase_path: "../kb"\nsprint_management_path: "."');
195
+ const outDir = path.join(tmpDir3, '_bmad-output');
196
+ fs.mkdirSync(outDir, { recursive: true });
197
+ fs.writeFileSync(path.join(outDir, 'project-layout.yaml'), 'knowledgebase:\n mode: local\n path: "../kb"\nsprint_management:\n mode: same\n path: "."');
198
+
199
+ delete process.env.MA_KNOWLEDGEBASE_PATH;
200
+ delete process.env.MA_SPRINT_PATH;
201
+ delete process.env.MA_KNOWLEDGEBASE_GIT_URL;
202
+ delete process.env.MA_SPRINT_GIT_URL;
203
+
204
+ await handleConfigLayout(['--yes']);
205
+
206
+ assert.ok(fs.existsSync(path.join(outDir, 'project-layout.yaml')),
207
+ 'project-layout.yaml should still exist');
208
+ const config = fs.readFileSync(path.join(configDir, 'config.yaml'), 'utf-8');
209
+ assert.ok(config.includes('kb'), `kb path should be preserved, got: ${config}`);
210
+ } finally {
211
+ if (origKb !== undefined) process.env.MA_KNOWLEDGEBASE_PATH = origKb;
212
+ else delete process.env.MA_KNOWLEDGEBASE_PATH;
213
+ if (origSp !== undefined) process.env.MA_SPRINT_PATH = origSp;
214
+ else delete process.env.MA_SPRINT_PATH;
215
+ if (origKbGit !== undefined) process.env.MA_KNOWLEDGEBASE_GIT_URL = origKbGit;
216
+ else delete process.env.MA_KNOWLEDGEBASE_GIT_URL;
217
+ if (origSpGit !== undefined) process.env.MA_SPRINT_GIT_URL = origSpGit;
218
+ else delete process.env.MA_SPRINT_GIT_URL;
219
+ process.chdir(origCwd3);
220
+ fs.rmSync(tmpDir3, { recursive: true, force: true });
221
+ }
222
+ });
223
+
224
+ console.log(`\n${passed} passed, ${failed} failed`);
225
+ if (errors.length > 0) {
226
+ console.log('\nFailed tests:');
227
+ errors.forEach(e => console.log(` - ${e.name}: ${e.error}`));
228
+ }
229
+ if (failed > 0) process.exit(1);
230
+ })();