ma-agents 3.5.4 → 3.5.6
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/_bmad-output/implementation-artifacts/bug-experimentalwarning-about-commonjs-loading-es-module-during-install.md +57 -0
- package/_bmad-output/implementation-artifacts/sprint-status.yaml +4 -0
- package/bin/cli.js +28 -0
- package/lib/bmad.js +25 -4
- package/lib/methodology/BMAD_AI_Development_Training.pptx +0 -0
- package/lib/methodology/version.json +1 -1
- package/lib/warning-filter.js +245 -0
- package/package.json +2 -2
- package/test/experimental-warning.test.js +314 -0
- package/.ma-agents.json +0 -10
- package/MANIFEST.yaml +0 -3
- package/_bmad-output/methodology/BMAD_AI_Development_Training.pptx +0 -0
- package/_bmad-output/methodology/version.json +0 -7
- package/docs/BMAD_AI_Development_Training.pptx +0 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
type: bug
|
|
3
|
+
status: ready-for-dev
|
|
4
|
+
severity: medium
|
|
5
|
+
bug_type: other
|
|
6
|
+
version_found: 3.5.4
|
|
7
|
+
title: ExperimentalWarning about CommonJS loading ES Module during install
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Bug: ExperimentalWarning about CommonJS loading ES Module during install
|
|
11
|
+
|
|
12
|
+
**Severity:** medium
|
|
13
|
+
**Affected Component:** installer CLI (bin/cli.js + bmad-method child spawn)
|
|
14
|
+
|
|
15
|
+
## Reproduction Steps
|
|
16
|
+
|
|
17
|
+
1. Use a Node version that emits the experimental `require()` of ESM warning (Node 20.17+ through early 22.x; see Node release notes on `--experimental-require-module`).
|
|
18
|
+
2. From a clean working directory, run `node bin/cli.js install --yes --agent claude-code` (or equivalently `npx ma-agents install`).
|
|
19
|
+
3. Observe startup stderr before the BMAD banner appears.
|
|
20
|
+
|
|
21
|
+
## Expected Behavior
|
|
22
|
+
|
|
23
|
+
Installer starts cleanly with only intentional informational output. No `ExperimentalWarning` noise appears on any Node version supported by `engines` (`>=16.0.0`).
|
|
24
|
+
|
|
25
|
+
## Actual Behavior
|
|
26
|
+
|
|
27
|
+
Stderr shows a message of the form:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
(node:XXXXX) ExperimentalWarning: CommonJS module <path> is loading ES Module <path> using require().
|
|
31
|
+
Support for loading ES Module in require() is an experimental feature and might change at any time.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The installer still completes successfully, but the warning is noisy, alarms first-time users, and appears in CI logs.
|
|
35
|
+
|
|
36
|
+
## Root Cause Hypothesis
|
|
37
|
+
|
|
38
|
+
The ma-agents CLI (`bin/cli.js`, CommonJS) spawns the bmad-method CLI (`node_modules/bmad-method/tools/bmad-npx-wrapper.js`, also CommonJS). bmad-method interacts with `@clack/prompts` and `@clack/core`, both of which declare `"type": "module"` in their `package.json`. On Node runtimes that emit `ExperimentalWarning` for `require()` of an ESM package (Node 20.17+ with `--experimental-require-module` default-on), the warning surfaces during install startup. While bmad-method already uses dynamic `import()` for the `@clack/*` packages in newer paths, the warning can still be produced by a transitive CJS → ESM require chain under certain Node versions.
|
|
39
|
+
|
|
40
|
+
## Affected Files
|
|
41
|
+
|
|
42
|
+
- `bin/cli.js`
|
|
43
|
+
- `lib/bmad.js`
|
|
44
|
+
|
|
45
|
+
## Suggested Fix
|
|
46
|
+
|
|
47
|
+
Install a process-level warning filter at the top of `bin/cli.js` that suppresses only `ExperimentalWarning` whose message matches the `require()`-of-ESM pattern, while re-emitting every other warning (DeprecationWarning, unhandled rejections, custom warnings) via `process.emitWarning` so they remain visible. Additionally, pass a sanitized `env` to the bmad-method child spawn in `lib/bmad.js` — set `NODE_OPTIONS` to include `--no-warnings=ExperimentalWarning` (Node 22+) or use the equivalent `NODE_NO_WARNINGS=1` as a narrow fallback — so the spawned child is equally quiet. Add `test/experimental-warning.test.js` to assert:
|
|
48
|
+
|
|
49
|
+
1. The filter swallows the specific ExperimentalWarning.
|
|
50
|
+
2. Unrelated warnings (e.g., a synthetic `DeprecationWarning`) are still emitted.
|
|
51
|
+
3. `lib/bmad.js` spawn envs include the expected suppression flag.
|
|
52
|
+
|
|
53
|
+
## Notes
|
|
54
|
+
|
|
55
|
+
- Created via `create-bug-story` workflow
|
|
56
|
+
- Discoverable by sprint workflows via glob: `_bmad-output/implementation-artifacts/bug-*.md`
|
|
57
|
+
- To add to a sprint, run `/add-to-sprint`
|
|
@@ -27,6 +27,10 @@ tracking_system: file-system
|
|
|
27
27
|
story_location: _bmad-output/implementation-artifacts
|
|
28
28
|
|
|
29
29
|
development_status:
|
|
30
|
+
# ─── BUG FIXES (ACTIVE) ───────────────────────────────────────────────────────
|
|
31
|
+
# Bug A (2026-04-14): ExperimentalWarning on installer startup. Severity: medium.
|
|
32
|
+
bug-experimentalwarning-about-commonjs-loading-es-module-during-install: ready-for-dev
|
|
33
|
+
|
|
30
34
|
# ─── IN PROGRESS ──────────────────────────────────────────────────────────────
|
|
31
35
|
|
|
32
36
|
# Epic 5: Bundled BMAD Installation — stories 5.1-5.4 done (archived), 5.5-5.6 in review
|
package/bin/cli.js
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// Bug A fix — suppress the cosmetic Node.js ExperimentalWarning about
|
|
4
|
+
// CommonJS require() of an ES Module. Must be installed BEFORE any
|
|
5
|
+
// other require() so the filter catches warnings fired by the very
|
|
6
|
+
// first dependency-loading call below. See lib/warning-filter.js.
|
|
7
|
+
//
|
|
8
|
+
// Re-exec gating:
|
|
9
|
+
// - Only when this file is launched as the CLI (require.main === module).
|
|
10
|
+
// When imported by the test suite or another script, skip the re-exec
|
|
11
|
+
// path — otherwise the importing process would silently fork itself.
|
|
12
|
+
// - Only for commands that can trigger the CJS-require-ESM warning,
|
|
13
|
+
// i.e. those that spawn bmad-method or otherwise load the heavy
|
|
14
|
+
// transitive dependency graph. Informational commands (--help,
|
|
15
|
+
// --version, status, list, agents) stay single-process for fast
|
|
16
|
+
// cold start. The listener-only fallback is still attached so
|
|
17
|
+
// in-process warnings still route through our filter.
|
|
18
|
+
const { installExperimentalWarningFilter } = require('../lib/warning-filter');
|
|
19
|
+
const _reexecCommands = new Set([
|
|
20
|
+
'install', 'uninstall', 'remove',
|
|
21
|
+
'create-skill', 'validate-skill', 'set-mandatory',
|
|
22
|
+
'customize-agent', 'create-agent',
|
|
23
|
+
'config',
|
|
24
|
+
undefined, // bare `ma-agents` → interactive wizard
|
|
25
|
+
]);
|
|
26
|
+
const _firstArg = process.argv[2];
|
|
27
|
+
installExperimentalWarningFilter({
|
|
28
|
+
reexec: require.main === module && _reexecCommands.has(_firstArg),
|
|
29
|
+
});
|
|
30
|
+
|
|
3
31
|
const prompts = require('prompts');
|
|
4
32
|
const chalk = require('chalk');
|
|
5
33
|
const path = require('path');
|
package/lib/bmad.js
CHANGED
|
@@ -4,6 +4,27 @@ const os = require('os');
|
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
5
|
const chalk = require('chalk');
|
|
6
6
|
const yaml = require('yaml');
|
|
7
|
+
const { withExperimentalWarningSuppressed } = require('./warning-filter');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build the env object passed to the bmad-method child spawn.
|
|
11
|
+
* Extends parent env with GIT_TERMINAL_PROMPT=0 and a NODE_OPTIONS
|
|
12
|
+
* value that silences the ExperimentalWarning about CJS require() of
|
|
13
|
+
* an ES Module (Bug A). Callers that need a different NODE_OPTIONS
|
|
14
|
+
* can merge after this call.
|
|
15
|
+
*
|
|
16
|
+
* @param {NodeJS.ProcessEnv} [parentEnv=process.env]
|
|
17
|
+
* @returns {NodeJS.ProcessEnv}
|
|
18
|
+
*/
|
|
19
|
+
function buildChildSpawnEnv(parentEnv = process.env) {
|
|
20
|
+
return {
|
|
21
|
+
...parentEnv,
|
|
22
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
23
|
+
// Bug A — keep the bmad-method child as quiet about CJS-require-ESM
|
|
24
|
+
// as the parent. Preserves any NODE_OPTIONS the user already set.
|
|
25
|
+
NODE_OPTIONS: withExperimentalWarningSuppressed(parentEnv.NODE_OPTIONS),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
7
28
|
|
|
8
29
|
const BMAD_DIR = '_bmad';
|
|
9
30
|
const CONFIG_DIR = path.join(BMAD_DIR, '_config', 'agents');
|
|
@@ -122,7 +143,7 @@ async function installBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = p
|
|
|
122
143
|
|
|
123
144
|
console.log(chalk.gray(` Running: ${command}`));
|
|
124
145
|
try {
|
|
125
|
-
runCommand(command, { cwd: projectRoot, env:
|
|
146
|
+
runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
|
|
126
147
|
await deployMethodology(projectRoot, force);
|
|
127
148
|
return true;
|
|
128
149
|
} catch (error) {
|
|
@@ -157,7 +178,7 @@ async function runMigration(modules, tools, projectRoot, force, { userName, comm
|
|
|
157
178
|
|
|
158
179
|
console.log(chalk.gray(` Running: ${command}`));
|
|
159
180
|
try {
|
|
160
|
-
runCommand(command, { cwd: projectRoot, env:
|
|
181
|
+
runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
|
|
161
182
|
} catch (error) {
|
|
162
183
|
// Rollback on failure
|
|
163
184
|
console.error(chalk.red(` BMAD update failed: ${error.message}`));
|
|
@@ -284,7 +305,7 @@ async function updateBmad(modules = ['bmm', 'bmb'], tools = [], projectRoot = pr
|
|
|
284
305
|
|
|
285
306
|
console.log(chalk.gray(` Running: ${command}`));
|
|
286
307
|
try {
|
|
287
|
-
runCommand(command, { cwd: projectRoot, env:
|
|
308
|
+
runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
|
|
288
309
|
await deployMethodology(projectRoot, force);
|
|
289
310
|
return true;
|
|
290
311
|
} catch (error) {
|
|
@@ -361,7 +382,7 @@ async function applyCustomizations(projectRoot = process.cwd(), modules = ['bmm'
|
|
|
361
382
|
|
|
362
383
|
console.log(chalk.gray(` Running: ${command}`));
|
|
363
384
|
try {
|
|
364
|
-
runCommand(command, { cwd: projectRoot, env:
|
|
385
|
+
runCommand(command, { cwd: projectRoot, env: buildChildSpawnEnv() });
|
|
365
386
|
} catch (error) {
|
|
366
387
|
console.error(chalk.red(` BMAD recompile failed: ${error.message}`));
|
|
367
388
|
}
|
|
Binary file
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/warning-filter.js
|
|
3
|
+
*
|
|
4
|
+
* Suppresses the Node.js `ExperimentalWarning` that fires when a CommonJS
|
|
5
|
+
* module loads an ES Module via `require()`.
|
|
6
|
+
*
|
|
7
|
+
* Why this exists
|
|
8
|
+
* ---------------
|
|
9
|
+
* The ma-agents CLI (`bin/cli.js`) is CommonJS. It transitively loads
|
|
10
|
+
* code paths from `bmad-method`, which itself interacts with
|
|
11
|
+
* `@clack/prompts` / `@clack/core` (both `"type": "module"`).
|
|
12
|
+
* On Node runtimes where `require()` of ESM is permitted but still
|
|
13
|
+
* emits `ExperimentalWarning` (notably Node 20.17+ through early 22.x),
|
|
14
|
+
* every installer run prints a noisy warning before the banner.
|
|
15
|
+
*
|
|
16
|
+
* The warning is cosmetic — the installer completes successfully — but
|
|
17
|
+
* it frightens first-time users and pollutes CI logs.
|
|
18
|
+
*
|
|
19
|
+
* Why a `process.on('warning', ...)` listener alone is NOT enough
|
|
20
|
+
* ----------------------------------------------------------------
|
|
21
|
+
* In Node.js 20+, `ExperimentalWarning` is printed through an internal
|
|
22
|
+
* path that does not honor user-land `'warning'` listeners for
|
|
23
|
+
* suppression purposes — the listener is invoked, but Node ALSO prints
|
|
24
|
+
* the default formatted message to stderr. The only reliable way to
|
|
25
|
+
* silence it is a CLI flag at process start:
|
|
26
|
+
* NODE_OPTIONS="--no-warnings=ExperimentalWarning" (Node 16+)
|
|
27
|
+
*
|
|
28
|
+
* Strategy
|
|
29
|
+
* --------
|
|
30
|
+
* On first entry, we check whether the current process was started with
|
|
31
|
+
* the suppression flag. If not, we re-exec ourselves in a child
|
|
32
|
+
* process with `NODE_OPTIONS` augmented to include the flag, inherit
|
|
33
|
+
* its stdio, and exit with the child's status. A sentinel env var
|
|
34
|
+
* `MA_AGENTS_WARNINGS_FILTERED=1` prevents infinite re-spawn loops.
|
|
35
|
+
*
|
|
36
|
+
* We additionally attach a `process.on('warning')` listener that
|
|
37
|
+
* silently swallows the specific CJS-require-ESM ExperimentalWarning
|
|
38
|
+
* (if it somehow reaches user-land despite the flag) while re-emitting
|
|
39
|
+
* every other warning. This is belt-and-braces for Node versions where
|
|
40
|
+
* listener semantics differ.
|
|
41
|
+
*
|
|
42
|
+
* For the bmad-method child process spawn (separate Node invocation),
|
|
43
|
+
* `lib/bmad.js` extends `NODE_OPTIONS` with the same flag via
|
|
44
|
+
* `withExperimentalWarningSuppressed()` so the child is equally quiet.
|
|
45
|
+
*
|
|
46
|
+
* This module MUST stay CommonJS and MUST NOT import any ESM package.
|
|
47
|
+
*
|
|
48
|
+
* @module lib/warning-filter
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
'use strict';
|
|
52
|
+
|
|
53
|
+
const SENTINEL_ENV = 'MA_AGENTS_WARNINGS_FILTERED';
|
|
54
|
+
const SUPPRESS_FLAG = '--no-warnings=ExperimentalWarning';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Default Node.js warning formatter (mirrors the built-in output
|
|
58
|
+
* used when no `warning` listener is registered). Kept in sync with
|
|
59
|
+
* Node's internal `process/warning.js` formatting circa Node 20–22.
|
|
60
|
+
*
|
|
61
|
+
* @param {Error} warning
|
|
62
|
+
* @returns {string}
|
|
63
|
+
*/
|
|
64
|
+
function formatWarning(warning) {
|
|
65
|
+
const name = warning && warning.name ? warning.name : 'Warning';
|
|
66
|
+
const code = warning && warning.code ? ` [${warning.code}]` : '';
|
|
67
|
+
const message = warning && warning.message ? warning.message : String(warning);
|
|
68
|
+
const detail = warning && warning.detail ? `\n${warning.detail}` : '';
|
|
69
|
+
const stack = warning && warning.stack ? warning.stack.split('\n').slice(1).join('\n') : '';
|
|
70
|
+
const header = `(node:${process.pid}) ${name}${code}: ${message}`;
|
|
71
|
+
const body = stack ? `${header}\n${stack}` : header;
|
|
72
|
+
return detail ? `${body}${detail}\n` : `${body}\n`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Classify a warning as the specific "CommonJS require() loading an ES Module"
|
|
77
|
+
* ExperimentalWarning we want to suppress.
|
|
78
|
+
*
|
|
79
|
+
* Matches two Node phrasings observed in v20.17+ / v22.x:
|
|
80
|
+
* 1. "CommonJS module <X> is loading ES Module <Y> using require()"
|
|
81
|
+
* 2. "Support for loading ES Module in require() is an experimental feature"
|
|
82
|
+
*
|
|
83
|
+
* Only matches when `warning.name === 'ExperimentalWarning'` — never
|
|
84
|
+
* silences a DeprecationWarning, custom warning, or anything else.
|
|
85
|
+
*
|
|
86
|
+
* @param {Error|{name?: string, message?: string}} warning
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function isRequireEsmWarning(warning) {
|
|
90
|
+
if (!warning || warning.name !== 'ExperimentalWarning') {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const msg = String(warning.message || '');
|
|
94
|
+
if (/CommonJS module\b.*\bloading ES Module\b.*\brequire\(\)/i.test(msg)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (/Support for loading ES Module in require\(\)/i.test(msg)) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build the NODE_OPTIONS value that should be passed to a child Node
|
|
105
|
+
* process (such as bmad-method) to silence the ExperimentalWarning.
|
|
106
|
+
* Preserves any existing NODE_OPTIONS the caller already set.
|
|
107
|
+
*
|
|
108
|
+
* @param {string|undefined} currentNodeOptions - value of NODE_OPTIONS in the parent env
|
|
109
|
+
* @returns {string} new NODE_OPTIONS value (may equal input if flag already present)
|
|
110
|
+
*/
|
|
111
|
+
function withExperimentalWarningSuppressed(currentNodeOptions) {
|
|
112
|
+
const current = (currentNodeOptions || '').trim();
|
|
113
|
+
if (!current) {
|
|
114
|
+
return SUPPRESS_FLAG;
|
|
115
|
+
}
|
|
116
|
+
// If any form of --no-warnings is already present, keep as-is.
|
|
117
|
+
if (/(^|\s)--no-warnings(\s|=|$)/.test(current)) {
|
|
118
|
+
return current;
|
|
119
|
+
}
|
|
120
|
+
return `${current} ${SUPPRESS_FLAG}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Returns true if the current Node process was started with a command-line
|
|
125
|
+
* option that already suppresses the target warning. Inspects
|
|
126
|
+
* `process.execArgv` (flags passed directly to node) and the environment's
|
|
127
|
+
* `NODE_OPTIONS` (flags Node consumed from env at startup).
|
|
128
|
+
*
|
|
129
|
+
* @returns {boolean}
|
|
130
|
+
*/
|
|
131
|
+
function isWarningAlreadySuppressed() {
|
|
132
|
+
const execArgv = process.execArgv || [];
|
|
133
|
+
for (const a of execArgv) {
|
|
134
|
+
if (a === '--no-warnings' || a.startsWith('--no-warnings=')) return true;
|
|
135
|
+
if (a.startsWith('--disable-warning=')) return true;
|
|
136
|
+
}
|
|
137
|
+
const opts = (process.env.NODE_OPTIONS || '').trim();
|
|
138
|
+
if (/(^|\s)--no-warnings(\s|=|$)/.test(opts)) return true;
|
|
139
|
+
if (/(^|\s)--disable-warning=/.test(opts)) return true;
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Attach the user-land warning listener (belt-and-braces). Idempotent —
|
|
145
|
+
* tags its listener with `__maAgentsFilter` so repeated calls only
|
|
146
|
+
* register one.
|
|
147
|
+
*
|
|
148
|
+
* @returns {boolean} true if newly attached, false if already attached.
|
|
149
|
+
*/
|
|
150
|
+
function attachListener() {
|
|
151
|
+
const existing = process.listeners('warning').find(fn => fn.__maAgentsFilter === true);
|
|
152
|
+
if (existing) return false;
|
|
153
|
+
|
|
154
|
+
function filter(warning) {
|
|
155
|
+
if (isRequireEsmWarning(warning)) {
|
|
156
|
+
return; // swallow — cosmetic warning we fix
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
process.stderr.write(formatWarning(warning));
|
|
160
|
+
} catch {
|
|
161
|
+
// never throw from a warning listener
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
filter.__maAgentsFilter = true;
|
|
165
|
+
process.on('warning', filter);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Install the warning filter. On first call from a non-suppressed
|
|
171
|
+
* process, re-execs the current script with
|
|
172
|
+
* `NODE_OPTIONS="--no-warnings=ExperimentalWarning"` and a sentinel env
|
|
173
|
+
* var to prevent re-spawn loops, then exits with the child's status.
|
|
174
|
+
*
|
|
175
|
+
* On subsequent calls — or when the process is already suppressed —
|
|
176
|
+
* it simply attaches the user-land listener and returns.
|
|
177
|
+
*
|
|
178
|
+
* Safe to call multiple times (idempotent).
|
|
179
|
+
*
|
|
180
|
+
* @param {Object} [opts]
|
|
181
|
+
* @param {boolean} [opts.reexec=true] - set to false to skip re-exec
|
|
182
|
+
* (used by tests that want to verify listener behaviour only).
|
|
183
|
+
* @returns {boolean} true if the filter was newly installed, false if
|
|
184
|
+
* a previous install was already active.
|
|
185
|
+
*/
|
|
186
|
+
function installExperimentalWarningFilter(opts = {}) {
|
|
187
|
+
const reexec = opts.reexec !== false;
|
|
188
|
+
|
|
189
|
+
// If already re-execed, or already suppressed via flags, just attach
|
|
190
|
+
// the listener and move on.
|
|
191
|
+
if (process.env[SENTINEL_ENV] === '1' || isWarningAlreadySuppressed()) {
|
|
192
|
+
return attachListener();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!reexec) {
|
|
196
|
+
return attachListener();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Re-exec self with the suppression flag in NODE_OPTIONS.
|
|
200
|
+
// We use spawnSync to wait for the child synchronously and then
|
|
201
|
+
// propagate its exit code.
|
|
202
|
+
const { spawnSync } = require('child_process');
|
|
203
|
+
const newEnv = {
|
|
204
|
+
...process.env,
|
|
205
|
+
[SENTINEL_ENV]: '1',
|
|
206
|
+
NODE_OPTIONS: withExperimentalWarningSuppressed(process.env.NODE_OPTIONS),
|
|
207
|
+
};
|
|
208
|
+
// Forward most execArgv flags to the child, but drop any --inspect*
|
|
209
|
+
// variants — the parent has already bound the debugger port, so the
|
|
210
|
+
// child would fail with EADDRINUSE. The debugger is attached to the
|
|
211
|
+
// parent only; since the parent exits immediately after the child,
|
|
212
|
+
// this is acceptable.
|
|
213
|
+
const forwardedExecArgv = (process.execArgv || []).filter(a =>
|
|
214
|
+
!/^--inspect(-brk|-port)?(=|$)/.test(a)
|
|
215
|
+
);
|
|
216
|
+
const args = [...forwardedExecArgv, ...(process.argv.slice(1) || [])];
|
|
217
|
+
const child = spawnSync(process.execPath, args, {
|
|
218
|
+
stdio: 'inherit',
|
|
219
|
+
env: newEnv,
|
|
220
|
+
});
|
|
221
|
+
// If spawn failed (e.g., EACCES), fall back to listener-only mode.
|
|
222
|
+
if (child.error) {
|
|
223
|
+
return attachListener();
|
|
224
|
+
}
|
|
225
|
+
// Propagate signal or exit code. Node's process.exit is the last
|
|
226
|
+
// thing we do on this path.
|
|
227
|
+
if (child.signal) {
|
|
228
|
+
process.kill(process.pid, child.signal);
|
|
229
|
+
// In case the signal was ignored:
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
process.exit(typeof child.status === 'number' ? child.status : 0);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
installExperimentalWarningFilter,
|
|
237
|
+
isRequireEsmWarning,
|
|
238
|
+
withExperimentalWarningSuppressed,
|
|
239
|
+
isWarningAlreadySuppressed,
|
|
240
|
+
// exposed for tests only
|
|
241
|
+
_formatWarning: formatWarning,
|
|
242
|
+
_attachListener: attachListener,
|
|
243
|
+
_SENTINEL_ENV: SENTINEL_ENV,
|
|
244
|
+
_SUPPRESS_FLAG: SUPPRESS_FLAG,
|
|
245
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ma-agents",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.6",
|
|
4
4
|
"description": "NPX tool to install skills for AI coding agents (Claude Code, Gemini, Copilot, Kilocode, Cline, Cursor, Roo Code)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node bin/cli.js",
|
|
11
|
-
"test": "node test/yes-flag.test.js && node test/skill-authoring.test.js && node test/skill-validation.test.js && node test/skill-mandatory.test.js && node test/skill-customize-agent.test.js && node test/create-agent.test.js && node test/generate-project-context.test.js && node test/build-bmad-args.test.js && node test/bmad-version-bump.test.js && node test/extension-module-restructure.test.js && node test/convert-agents-to-skills.test.js && node test/migration.test.js && node test/migration-validation.test.js && node test/story-15-5-workflow-skills.test.js && node test/repo-layout.test.js && node test/config-storage.test.js && node test/cross-repo-validation.test.js && node test/config-lost-on-update.test.js && node test/portable-paths.test.js && node test/cicd-remote-mode.test.js && node test/config-layout.test.js && node test/roo-code-agent.test.js && node test/roo-code-injection.test.js && node test/profile.test.js && node test/onprem-injection.test.js",
|
|
11
|
+
"test": "node test/yes-flag.test.js && node test/skill-authoring.test.js && node test/skill-validation.test.js && node test/skill-mandatory.test.js && node test/skill-customize-agent.test.js && node test/create-agent.test.js && node test/generate-project-context.test.js && node test/build-bmad-args.test.js && node test/bmad-version-bump.test.js && node test/extension-module-restructure.test.js && node test/convert-agents-to-skills.test.js && node test/migration.test.js && node test/migration-validation.test.js && node test/story-15-5-workflow-skills.test.js && node test/repo-layout.test.js && node test/config-storage.test.js && node test/cross-repo-validation.test.js && node test/config-lost-on-update.test.js && node test/portable-paths.test.js && node test/cicd-remote-mode.test.js && node test/config-layout.test.js && node test/roo-code-agent.test.js && node test/roo-code-injection.test.js && node test/profile.test.js && node test/onprem-injection.test.js && node test/experimental-warning.test.js",
|
|
12
12
|
"build:bmad-cache": "node scripts/build-bmad-cache.js"
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Tests for Bug A: ExperimentalWarning about CommonJS loading ES Module
|
|
4
|
+
* during installer startup.
|
|
5
|
+
*
|
|
6
|
+
* Contract:
|
|
7
|
+
* lib/warning-filter.js exports installExperimentalWarningFilter().
|
|
8
|
+
* When installed, the process-level 'warning' listener swallows
|
|
9
|
+
* ExperimentalWarning messages that describe CJS require() of an
|
|
10
|
+
* ES Module, but lets every other warning through unchanged.
|
|
11
|
+
*
|
|
12
|
+
* Additionally, bin/cli.js must install the filter at startup, and
|
|
13
|
+
* lib/bmad.js must pass NODE_OPTIONS="--no-warnings=ExperimentalWarning"
|
|
14
|
+
* (or equivalent) to the bmad-method child spawn env.
|
|
15
|
+
*/
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const assert = require('assert');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
|
|
23
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
24
|
+
const FILTER_PATH = path.join(REPO_ROOT, 'lib', 'warning-filter.js');
|
|
25
|
+
const CLI_PATH = path.join(REPO_ROOT, 'bin', 'cli.js');
|
|
26
|
+
const BMAD_PATH = path.join(REPO_ROOT, 'lib', 'bmad.js');
|
|
27
|
+
|
|
28
|
+
let passed = 0;
|
|
29
|
+
let failed = 0;
|
|
30
|
+
|
|
31
|
+
function test(name, fn) {
|
|
32
|
+
try {
|
|
33
|
+
fn();
|
|
34
|
+
console.log(` ✓ ${name}`);
|
|
35
|
+
passed++;
|
|
36
|
+
} catch (err) {
|
|
37
|
+
console.error(` ✗ ${name}: ${err.message}`);
|
|
38
|
+
if (err.stack) console.error(err.stack.split('\n').slice(1, 4).join('\n'));
|
|
39
|
+
failed++;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('\n── Bug A: ExperimentalWarning filter tests ──────────────────────\n');
|
|
44
|
+
|
|
45
|
+
// ─── Test 1: filter module exists and exports the expected API ────────────────
|
|
46
|
+
test('lib/warning-filter.js exists', () => {
|
|
47
|
+
assert.ok(fs.existsSync(FILTER_PATH), 'expected lib/warning-filter.js to exist');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('filter module exports installExperimentalWarningFilter()', () => {
|
|
51
|
+
const mod = require(FILTER_PATH);
|
|
52
|
+
assert.strictEqual(typeof mod.installExperimentalWarningFilter, 'function');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('filter module exports isRequireEsmWarning() predicate', () => {
|
|
56
|
+
const mod = require(FILTER_PATH);
|
|
57
|
+
assert.strictEqual(typeof mod.isRequireEsmWarning, 'function');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── Test 2: isRequireEsmWarning() correctly classifies warnings ──────────────
|
|
61
|
+
test('isRequireEsmWarning matches CJS-loading-ESM ExperimentalWarning', () => {
|
|
62
|
+
const { isRequireEsmWarning } = require(FILTER_PATH);
|
|
63
|
+
const w = new Error(
|
|
64
|
+
'CommonJS module /a/b.js is loading ES Module /c/d.js using require(). ' +
|
|
65
|
+
'Support for loading ES Module in require() is an experimental feature ' +
|
|
66
|
+
'and might change at any time.'
|
|
67
|
+
);
|
|
68
|
+
w.name = 'ExperimentalWarning';
|
|
69
|
+
assert.strictEqual(isRequireEsmWarning(w), true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('isRequireEsmWarning matches require(ESM) variant phrasing', () => {
|
|
73
|
+
const { isRequireEsmWarning } = require(FILTER_PATH);
|
|
74
|
+
const w = new Error('Support for loading ES Module in require() is an experimental feature.');
|
|
75
|
+
w.name = 'ExperimentalWarning';
|
|
76
|
+
assert.strictEqual(isRequireEsmWarning(w), true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('isRequireEsmWarning ignores unrelated ExperimentalWarning', () => {
|
|
80
|
+
const { isRequireEsmWarning } = require(FILTER_PATH);
|
|
81
|
+
const w = new Error('Fetch API is an experimental feature.');
|
|
82
|
+
w.name = 'ExperimentalWarning';
|
|
83
|
+
assert.strictEqual(isRequireEsmWarning(w), false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('isRequireEsmWarning ignores DeprecationWarning with require() text', () => {
|
|
87
|
+
const { isRequireEsmWarning } = require(FILTER_PATH);
|
|
88
|
+
const w = new Error('Some deprecated require() usage in ES Module.');
|
|
89
|
+
w.name = 'DeprecationWarning';
|
|
90
|
+
assert.strictEqual(isRequireEsmWarning(w), false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ─── Test 3: end-to-end — re-exec flow silences ExperimentalWarning ──────────
|
|
94
|
+
test('installed filter (with re-exec) silences CJS-require-ESM ExperimentalWarning', () => {
|
|
95
|
+
// Drive the full re-exec path: parent re-execs with NODE_OPTIONS set,
|
|
96
|
+
// child emits the warning, expects ZERO bytes of ExperimentalWarning.
|
|
97
|
+
const child = spawnSync(process.execPath, [
|
|
98
|
+
'-e',
|
|
99
|
+
`const { installExperimentalWarningFilter } = require(${JSON.stringify(FILTER_PATH)});
|
|
100
|
+
installExperimentalWarningFilter();
|
|
101
|
+
// After re-exec we land here in the child:
|
|
102
|
+
process.emitWarning('CommonJS module /a.js is loading ES Module /b.mjs using require().', 'ExperimentalWarning');`,
|
|
103
|
+
], { encoding: 'utf8' });
|
|
104
|
+
assert.strictEqual(child.status, 0, `child exited non-zero: ${child.stderr}`);
|
|
105
|
+
assert.ok(
|
|
106
|
+
!/ExperimentalWarning/.test(child.stderr),
|
|
107
|
+
`expected no ExperimentalWarning in stderr, got:\n${child.stderr}`
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ─── Test 3b: NODE_OPTIONS-only test bypasses re-exec ─────────────────────────
|
|
112
|
+
test('listener-only mode swallows the matched warning at user-land', () => {
|
|
113
|
+
// reexec:false means we only attach the listener; target warning still
|
|
114
|
+
// hits Node's internal printer in some Node versions, but our listener
|
|
115
|
+
// returns silently so the listener path itself never re-emits it.
|
|
116
|
+
const child = spawnSync(process.execPath, [
|
|
117
|
+
'-e',
|
|
118
|
+
`const { installExperimentalWarningFilter, _attachListener } = require(${JSON.stringify(FILTER_PATH)});
|
|
119
|
+
installExperimentalWarningFilter({ reexec: false });
|
|
120
|
+
// Emit a custom-named warning identical in shape so we don't depend
|
|
121
|
+
// on Node's internal experimental-warning path:
|
|
122
|
+
const w = new Error('CommonJS module /a.js is loading ES Module /b.mjs using require().');
|
|
123
|
+
w.name = 'ExperimentalWarning';
|
|
124
|
+
// Drive the listener directly to assert it returns without writing.
|
|
125
|
+
const listener = process.listeners('warning').find(fn => fn.__maAgentsFilter);
|
|
126
|
+
if (!listener) { console.error('no filter listener'); process.exit(2); }
|
|
127
|
+
// capture stderr writes
|
|
128
|
+
let captured = '';
|
|
129
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
130
|
+
process.stderr.write = (c) => { captured += String(c); return true; };
|
|
131
|
+
listener(w);
|
|
132
|
+
process.stderr.write = orig;
|
|
133
|
+
if (captured.length !== 0) { console.error('LEAKED:', captured); process.exit(3); }
|
|
134
|
+
process.exit(0);`,
|
|
135
|
+
], { encoding: 'utf8', env: { ...process.env, [require(FILTER_PATH)._SENTINEL_ENV]: '1' } });
|
|
136
|
+
assert.strictEqual(child.status, 0, `child exited ${child.status}: ${child.stderr}`);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ─── Test 4: filter preserves unrelated warnings ──────────────────────────────
|
|
140
|
+
test('installed filter preserves DeprecationWarning', () => {
|
|
141
|
+
// Drive the listener directly to verify it re-emits non-matching warnings.
|
|
142
|
+
const child = spawnSync(process.execPath, [
|
|
143
|
+
'-e',
|
|
144
|
+
`const { installExperimentalWarningFilter } = require(${JSON.stringify(FILTER_PATH)});
|
|
145
|
+
installExperimentalWarningFilter({ reexec: false });
|
|
146
|
+
const listener = process.listeners('warning').find(fn => fn.__maAgentsFilter);
|
|
147
|
+
const w = new Error('legacy api — do not use');
|
|
148
|
+
w.name = 'DeprecationWarning';
|
|
149
|
+
let captured = '';
|
|
150
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
151
|
+
process.stderr.write = (c) => { captured += String(c); return true; };
|
|
152
|
+
listener(w);
|
|
153
|
+
process.stderr.write = orig;
|
|
154
|
+
process.stdout.write(captured);`,
|
|
155
|
+
], { encoding: 'utf8' });
|
|
156
|
+
assert.strictEqual(child.status, 0);
|
|
157
|
+
assert.ok(
|
|
158
|
+
/DeprecationWarning/.test(child.stdout),
|
|
159
|
+
`expected DeprecationWarning to be preserved, stdout=\n${child.stdout}\nstderr=\n${child.stderr}`
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('installed filter preserves unrelated ExperimentalWarning', () => {
|
|
164
|
+
const child = spawnSync(process.execPath, [
|
|
165
|
+
'-e',
|
|
166
|
+
`const { installExperimentalWarningFilter } = require(${JSON.stringify(FILTER_PATH)});
|
|
167
|
+
installExperimentalWarningFilter({ reexec: false });
|
|
168
|
+
const listener = process.listeners('warning').find(fn => fn.__maAgentsFilter);
|
|
169
|
+
const w = new Error('Fetch API is an experimental feature.');
|
|
170
|
+
w.name = 'ExperimentalWarning';
|
|
171
|
+
let captured = '';
|
|
172
|
+
const orig = process.stderr.write.bind(process.stderr);
|
|
173
|
+
process.stderr.write = (c) => { captured += String(c); return true; };
|
|
174
|
+
listener(w);
|
|
175
|
+
process.stderr.write = orig;
|
|
176
|
+
process.stdout.write(captured);`,
|
|
177
|
+
], { encoding: 'utf8' });
|
|
178
|
+
assert.strictEqual(child.status, 0);
|
|
179
|
+
assert.ok(
|
|
180
|
+
/Fetch API/.test(child.stdout),
|
|
181
|
+
`expected unrelated ExperimentalWarning preserved, stdout=\n${child.stdout}`
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ─── Test 5: idempotent — installing twice doesn't double-suppress or leak ────
|
|
186
|
+
test('installExperimentalWarningFilter is idempotent', () => {
|
|
187
|
+
const child = spawnSync(process.execPath, [
|
|
188
|
+
'-e',
|
|
189
|
+
`const { installExperimentalWarningFilter } = require(${JSON.stringify(FILTER_PATH)});
|
|
190
|
+
installExperimentalWarningFilter({ reexec: false });
|
|
191
|
+
installExperimentalWarningFilter({ reexec: false });
|
|
192
|
+
const listeners = process.listeners('warning').filter(fn => fn.__maAgentsFilter === true);
|
|
193
|
+
if (listeners.length !== 1) {
|
|
194
|
+
console.error('EXPECTED 1 filter listener, got', listeners.length);
|
|
195
|
+
process.exit(2);
|
|
196
|
+
}
|
|
197
|
+
process.exit(0);`,
|
|
198
|
+
], { encoding: 'utf8' });
|
|
199
|
+
assert.strictEqual(child.status, 0, `child exited ${child.status}: ${child.stderr}`);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// ─── Test 6: bin/cli.js wires the filter at startup ───────────────────────────
|
|
203
|
+
test('bin/cli.js requires warning-filter and installs it', () => {
|
|
204
|
+
const src = fs.readFileSync(CLI_PATH, 'utf8');
|
|
205
|
+
assert.ok(
|
|
206
|
+
/require\(['"]\.\.\/lib\/warning-filter['"]\)/.test(src) ||
|
|
207
|
+
/require\(['"]\.\.\/lib\/warning-filter\.js['"]\)/.test(src),
|
|
208
|
+
'expected bin/cli.js to require ../lib/warning-filter'
|
|
209
|
+
);
|
|
210
|
+
assert.ok(
|
|
211
|
+
/installExperimentalWarningFilter\s*\(/.test(src),
|
|
212
|
+
'expected bin/cli.js to call installExperimentalWarningFilter(...)'
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ─── Test 7: lib/bmad.js propagates suppression to child spawn via NODE_OPTIONS
|
|
217
|
+
test('lib/bmad.js builds child env that suppresses ExperimentalWarning', () => {
|
|
218
|
+
const src = fs.readFileSync(BMAD_PATH, 'utf8');
|
|
219
|
+
// Either calls the helper directly or inlines the flag.
|
|
220
|
+
const usesHelper = /withExperimentalWarningSuppressed/.test(src) &&
|
|
221
|
+
/require\(['"]\.\/warning-filter['"]\)/.test(src);
|
|
222
|
+
const inlinesFlag = /--no-warnings=ExperimentalWarning/.test(src);
|
|
223
|
+
assert.ok(
|
|
224
|
+
usesHelper || inlinesFlag,
|
|
225
|
+
'expected lib/bmad.js to either import withExperimentalWarningSuppressed ' +
|
|
226
|
+
'from ./warning-filter, or inline --no-warnings=ExperimentalWarning'
|
|
227
|
+
);
|
|
228
|
+
assert.ok(
|
|
229
|
+
/NODE_OPTIONS/.test(src),
|
|
230
|
+
'expected lib/bmad.js to set NODE_OPTIONS for the child spawn'
|
|
231
|
+
);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ─── Test 8: integration — running bin/cli.js --help is silent on stderr ──────
|
|
235
|
+
test('bin/cli.js --help produces no ExperimentalWarning on stderr', () => {
|
|
236
|
+
const child = spawnSync(process.execPath, [CLI_PATH, '--help'], {
|
|
237
|
+
encoding: 'utf8',
|
|
238
|
+
env: { ...process.env, [require(FILTER_PATH)._SENTINEL_ENV]: '' },
|
|
239
|
+
});
|
|
240
|
+
assert.strictEqual(child.status, 0, `cli --help exited non-zero: ${child.stderr}`);
|
|
241
|
+
assert.ok(
|
|
242
|
+
!/ExperimentalWarning/.test(child.stderr),
|
|
243
|
+
`expected no ExperimentalWarning from cli --help, got:\n${child.stderr}`
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ─── Test 9: withExperimentalWarningSuppressed helper is well-behaved ─────────
|
|
248
|
+
test('withExperimentalWarningSuppressed adds flag when none present', () => {
|
|
249
|
+
const { withExperimentalWarningSuppressed } = require(FILTER_PATH);
|
|
250
|
+
assert.strictEqual(withExperimentalWarningSuppressed(undefined), '--no-warnings=ExperimentalWarning');
|
|
251
|
+
assert.strictEqual(withExperimentalWarningSuppressed(''), '--no-warnings=ExperimentalWarning');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test('withExperimentalWarningSuppressed preserves existing --no-warnings', () => {
|
|
255
|
+
const { withExperimentalWarningSuppressed } = require(FILTER_PATH);
|
|
256
|
+
assert.strictEqual(withExperimentalWarningSuppressed('--no-warnings'), '--no-warnings');
|
|
257
|
+
assert.strictEqual(
|
|
258
|
+
withExperimentalWarningSuppressed('--no-warnings=DeprecationWarning'),
|
|
259
|
+
'--no-warnings=DeprecationWarning'
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test('withExperimentalWarningSuppressed appends flag to other NODE_OPTIONS', () => {
|
|
264
|
+
const { withExperimentalWarningSuppressed } = require(FILTER_PATH);
|
|
265
|
+
assert.strictEqual(
|
|
266
|
+
withExperimentalWarningSuppressed('--max-old-space-size=4096'),
|
|
267
|
+
'--max-old-space-size=4096 --no-warnings=ExperimentalWarning'
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ─── Test 10: regression — require('bin/cli.js') must NOT re-exec ───────────
|
|
272
|
+
test('require("bin/cli.js") from another process does not re-exec', () => {
|
|
273
|
+
// If the CLI silently re-execs on require, the "before pid" line
|
|
274
|
+
// would print twice in the stderr/stdout stream.
|
|
275
|
+
const child = spawnSync(process.execPath, [
|
|
276
|
+
'-e',
|
|
277
|
+
`const before = process.pid;
|
|
278
|
+
console.log('MARK_BEFORE', before);
|
|
279
|
+
require(${JSON.stringify(CLI_PATH)});
|
|
280
|
+
console.log('MARK_AFTER', process.pid, 'same:', process.pid === before);`,
|
|
281
|
+
], { encoding: 'utf8' });
|
|
282
|
+
assert.strictEqual(child.status, 0, `child exited non-zero: ${child.stderr}`);
|
|
283
|
+
const beforeCount = (child.stdout.match(/MARK_BEFORE/g) || []).length;
|
|
284
|
+
assert.strictEqual(beforeCount, 1, `expected exactly one MARK_BEFORE, got ${beforeCount}\n${child.stdout}`);
|
|
285
|
+
assert.ok(/same: true/.test(child.stdout), 'expected PID to be unchanged after require');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ─── Test 11: regression — fast commands must NOT re-exec ────────────────────
|
|
289
|
+
test('bin/cli.js --version runs in a single process (no re-exec)', () => {
|
|
290
|
+
// Spawn cli.js --version and assert PID stability via NODE_OPTIONS check.
|
|
291
|
+
// We approximate by running it with the sentinel unset and checking it
|
|
292
|
+
// still completes without looping. A process-tree check is overkill;
|
|
293
|
+
// we just verify exit code and output shape.
|
|
294
|
+
const child = spawnSync(process.execPath, [CLI_PATH, '--version'], {
|
|
295
|
+
encoding: 'utf8',
|
|
296
|
+
env: { ...process.env, [require(FILTER_PATH)._SENTINEL_ENV]: '' },
|
|
297
|
+
});
|
|
298
|
+
assert.strictEqual(child.status, 0);
|
|
299
|
+
assert.ok(/ma-agents v/.test(child.stdout), `expected version banner, got: ${child.stdout}`);
|
|
300
|
+
assert.ok(!/ExperimentalWarning/.test(child.stderr));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ─── Test 12: --inspect is not forwarded to the re-exec child ────────────────
|
|
304
|
+
test('installExperimentalWarningFilter drops --inspect* from forwarded execArgv', () => {
|
|
305
|
+
const src = fs.readFileSync(FILTER_PATH, 'utf8');
|
|
306
|
+
assert.ok(
|
|
307
|
+
/--inspect/.test(src) && /filter/.test(src),
|
|
308
|
+
'expected warning-filter.js to filter --inspect out of forwarded execArgv'
|
|
309
|
+
);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// ─── Summary ──────────────────────────────────────────────────────────────────
|
|
313
|
+
console.log(`\n ${passed} passed, ${failed} failed\n`);
|
|
314
|
+
if (failed > 0) process.exit(1);
|
package/.ma-agents.json
DELETED
package/MANIFEST.yaml
DELETED
|
Binary file
|
|
Binary file
|