@maz-ui/node 5.0.0-canary.ef64a02.0 → 5.1.0-beta.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 CHANGED
@@ -18,6 +18,7 @@ npm install @maz-ui/node
18
18
  - 🔧 **Highly configurable** - custom loggers with different levels and reporters
19
19
  - 🛡️ **Error handling** - comprehensive error management
20
20
  - 🎯 **Multiple log types** - info, debug, trace, start, ready, fail, box
21
+ - 🤖 **AI agents setup** - `setupAgents` configures the AI coding agents of a project for maz-ui (rules and MCP server), as `npx @maz-ui/mcp init` does: [documentation](https://maz-ui.com/ecosystem/node/setup-agents)
21
22
 
22
23
  ## Usage
23
24
 
@@ -364,11 +365,12 @@ All logger methods automatically handle error objects using `getErrorMessage` ut
364
365
 
365
366
  - **consola** - Elegant console logging with fancy formatting
366
367
  - **@maz-ui/utils** - Error message formatting
368
+ - **jsonc-parser** - Edits the MCP config files of `setupAgents` while keeping their formatting and comments
367
369
  - **Node.js built-ins** - child_process for command execution
368
370
 
369
371
  ## Requirements
370
372
 
371
- - Node.js >= 18.0.0
373
+ - Node.js >= 22.12.0
372
374
  - TypeScript support included
373
375
 
374
376
  ## Contributing
package/dist/index.d.mts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ExecOptions } from 'node:child_process';
1
2
  import * as consola from 'consola';
2
3
  import { ConsolaOptions, InputLogObject, ConsolaReporter, ConsolaInstance } from 'consola';
3
4
  export { ConsolaOptions } from 'consola';
@@ -51,28 +52,88 @@ interface CustomLogger {
51
52
  warn: (message: string, ...args: any[]) => void;
52
53
  debug: (message: string, ...args: any[]) => void;
53
54
  }
54
- declare function execPromise(command: string, { logger, packageName, noSuccess, noStdout, noStderr, noError, logLevel, cwd, }?: {
55
+ /**
56
+ * Options for {@link execPromise}.
57
+ *
58
+ * The following fields are forwarded as-is to `child_process.exec`:
59
+ * - `cwd` - working directory of the command. Defaults to the current one.
60
+ * - `env` - environment variables, **merged on top of `process.env`**.
61
+ * - `timeout` - maximum run time in milliseconds before the command is killed. No timeout by default.
62
+ * - `maxBuffer` - maximum bytes allowed on stdout/stderr before the command is killed. Defaults to `1024 * 1024` (1 MiB).
63
+ * - `killSignal` - signal used to kill the command on timeout or abort. Defaults to `'SIGTERM'`.
64
+ * - `shell` - shell used to run the command. Defaults to `/bin/sh` (Unix) or `cmd.exe` (Windows).
65
+ * - `signal` - an `AbortSignal` that, once aborted, kills the command and rejects the promise.
66
+ */
67
+ type ExecPromiseOptions = Pick<ExecOptions, 'cwd' | 'timeout' | 'env' | 'maxBuffer' | 'killSignal' | 'shell' | 'signal'> & {
68
+ /**
69
+ * Custom logger used for every output. Falls back to the package logger.
70
+ */
55
71
  logger?: CustomLogger;
72
+ /**
73
+ * Prefix added to every log line, formatted as `[packageName]: `.
74
+ */
56
75
  packageName?: string;
57
76
  /**
58
- * Don't log success message
77
+ * Don't log the success message.
78
+ * @default false
59
79
  */
60
80
  noSuccess?: boolean;
61
81
  /**
62
- * Don't log stdout
82
+ * Don't log stdout.
83
+ * @default false
63
84
  */
64
85
  noStdout?: boolean;
65
86
  /**
66
- * Don't log stderr
87
+ * Don't log stderr.
88
+ * @default false
67
89
  */
68
90
  noStderr?: boolean;
69
91
  /**
70
- * Don't log error
92
+ * Don't log the error when the command fails.
93
+ * @default false
71
94
  */
72
95
  noError?: boolean;
96
+ /**
97
+ * Logging level applied to the default logger before running the command.
98
+ */
73
99
  logLevel?: LogLevel;
74
- cwd?: string;
75
- }): Promise<{
100
+ };
101
+ /**
102
+ * Runs a shell command and resolves with its `stdout` and `stderr`.
103
+ *
104
+ * Secrets (npm auth tokens, `--token`/`--password` flags, provider tokens,
105
+ * JWTs, basic-auth URLs) are automatically masked in every log line and in the
106
+ * rejected error via {@link redactSecrets}. The resolved `stdout`/`stderr` are
107
+ * returned untouched.
108
+ *
109
+ * @param command - The shell command to execute.
110
+ * @returns A promise resolving with the command `stdout` and `stderr`.
111
+ * @throws If the command exits with a non-zero code, times out, is aborted, or
112
+ * exceeds `maxBuffer`. The rejected error is redacted.
113
+ *
114
+ * @example Basic usage
115
+ * ```ts
116
+ * const { stdout } = await execPromise('node --version')
117
+ * ```
118
+ *
119
+ * @example Fail the command after 5 seconds
120
+ * ```ts
121
+ * await execPromise('pnpm install', { timeout: 5000 })
122
+ * ```
123
+ *
124
+ * @example Inject environment variables (merged on top of `process.env`)
125
+ * ```ts
126
+ * await execPromise('node build.mjs', { env: { NODE_ENV: 'production' } })
127
+ * ```
128
+ *
129
+ * @example Cancel the command with an `AbortController`
130
+ * ```ts
131
+ * const controller = new AbortController()
132
+ * const promise = execPromise('long-task', { signal: controller.signal })
133
+ * controller.abort()
134
+ * ```
135
+ */
136
+ declare function execPromise(command: string, { logger, packageName, noSuccess, noStdout, noStderr, noError, logLevel, cwd, timeout, env, maxBuffer, killSignal, shell, signal, }?: ExecPromiseOptions): Promise<{
76
137
  stdout: string;
77
138
  stderr: string;
78
139
  }>;
@@ -88,5 +149,99 @@ declare function printBanner({ name, version, options, }: {
88
149
  };
89
150
  }): void;
90
151
 
91
- export { createLogger, execPromise, logger, printBanner };
92
- export type { LogLevel, Logger, LoggerOptions };
152
+ /**
153
+ * Masks secrets that have a recognizable syntactic context inside a string.
154
+ *
155
+ * Only deterministic patterns are redacted (npm auth tokens, named secret flags
156
+ * such as `--token`/`--password`, prefixed provider tokens like `npm_`/`ghp_`,
157
+ * JWTs, and basic-auth credentials in URLs). Context-free random strings are left
158
+ * untouched because they cannot be detected without false positives.
159
+ *
160
+ * Tokens long enough (>= 16 chars) keep their first and last 4 characters visible
161
+ * (e.g. `npm_***J1iF`) so the right credential can still be recognized; shorter
162
+ * secrets are fully masked.
163
+ */
164
+ declare function redactSecrets(input: string): string;
165
+ /**
166
+ * Returns the given error with its `message`, `cmd` and `stack` redacted via
167
+ * {@link redactSecrets}, so a thrown command never carries secrets to callers.
168
+ */
169
+ declare function redactError<T>(error: T): T;
170
+
171
+ /**
172
+ * AI coding agents supported by `setupAgents`.
173
+ *
174
+ * - `claude`: Claude Code rule (`.claude/rules/maz-ui.md`) and MCP server (`.mcp.json`)
175
+ * - `agents-md`: section in `AGENTS.md` (Codex, Cursor, Copilot, Windsurf, Zed, Gemini...)
176
+ * - `cursor`: MCP server in `.cursor/mcp.json` (rules come from `AGENTS.md`)
177
+ * - `vscode`: MCP server in `.vscode/mcp.json` for GitHub Copilot (rules come from `AGENTS.md`)
178
+ */
179
+ type AgentTarget = 'claude' | 'agents-md' | 'cursor' | 'vscode';
180
+ declare const AGENT_TARGETS: AgentTarget[];
181
+ interface SetupAgentsOptions {
182
+ /** Root of the project (where `AGENTS.md`, `.claude/`, `.mcp.json` live) */
183
+ rootDir: string;
184
+ /** Agents to configure. Default: the agents detected in the project */
185
+ targets?: AgentTarget[];
186
+ /** Write the maz-ui rules (Claude Code rule, `AGENTS.md` section). Default `true` */
187
+ rules?: boolean;
188
+ /** Register the maz-ui MCP server. Default `true` */
189
+ mcp?: boolean;
190
+ /** Compute the changes without writing anything */
191
+ dryRun?: boolean;
192
+ /** Remove everything `setupAgents` added */
193
+ remove?: boolean;
194
+ /** Folder of the maz-ui agent docs. Default: `docs/` of the maz-ui package installed in `rootDir` */
195
+ docsDir?: string;
196
+ /** Command used to start the MCP server */
197
+ mcpServer?: {
198
+ command: string;
199
+ args: string[];
200
+ };
201
+ }
202
+ type SetupAgentsAction = 'create' | 'update' | 'remove' | 'unchanged' | 'skip';
203
+ interface SetupAgentsChange {
204
+ /** Path relative to `rootDir` */
205
+ file: string;
206
+ action: SetupAgentsAction;
207
+ reason?: string;
208
+ }
209
+ interface SetupAgentsResult {
210
+ targets: AgentTarget[];
211
+ version: string;
212
+ changes: SetupAgentsChange[];
213
+ }
214
+ declare const MANAGED_MARKER = "Managed by maz-ui";
215
+ declare const AGENTS_MD_BEGIN = "<!-- BEGIN:maz-ui -->";
216
+ declare const AGENTS_MD_END = "<!-- END:maz-ui -->";
217
+ declare const MCP_SERVER_NAME = "maz-ui";
218
+ declare const DEFAULT_MCP_SERVER: {
219
+ command: string;
220
+ args: string[];
221
+ };
222
+ /** Detects the AI agents configured in a project */
223
+ declare function detectAgentTargets(rootDir: string): AgentTarget[];
224
+ interface AgentDocs {
225
+ directory: string;
226
+ version: string;
227
+ claudeRule: string;
228
+ agentsMdSection: string;
229
+ }
230
+ /**
231
+ * Finds `node_modules/<name>` from a directory up to the filesystem root, like
232
+ * the Node resolution but without the global folders (`NODE_PATH`), so only a
233
+ * package installed for the project is found.
234
+ */
235
+ declare function findInstalledPackage(fromDir: string, name: string): string | undefined;
236
+ /** Reads the agent files shipped in `maz-ui/docs/agents` */
237
+ declare function readAgentDocs(rootDir: string, docsDir?: string): AgentDocs;
238
+ /**
239
+ * Configures the AI coding agents of a project to use maz-ui: rules (version
240
+ * matched with the installed maz-ui) and the maz-ui MCP server. Idempotent and
241
+ * conservative: it only writes files it owns, a delimited section of
242
+ * `AGENTS.md` and its own entry of the MCP config files.
243
+ */
244
+ declare function setupAgents(options: SetupAgentsOptions): SetupAgentsResult;
245
+
246
+ export { AGENTS_MD_BEGIN, AGENTS_MD_END, AGENT_TARGETS, DEFAULT_MCP_SERVER, MANAGED_MARKER, MCP_SERVER_NAME, createLogger, detectAgentTargets, execPromise, findInstalledPackage, logger, printBanner, readAgentDocs, redactError, redactSecrets, setupAgents };
247
+ export type { AgentDocs, AgentTarget, ExecPromiseOptions, LogLevel, Logger, LoggerOptions, SetupAgentsAction, SetupAgentsChange, SetupAgentsOptions, SetupAgentsResult };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { ExecOptions } from 'node:child_process';
1
2
  import * as consola from 'consola';
2
3
  import { ConsolaOptions, InputLogObject, ConsolaReporter, ConsolaInstance } from 'consola';
3
4
  export { ConsolaOptions } from 'consola';
@@ -51,28 +52,88 @@ interface CustomLogger {
51
52
  warn: (message: string, ...args: any[]) => void;
52
53
  debug: (message: string, ...args: any[]) => void;
53
54
  }
54
- declare function execPromise(command: string, { logger, packageName, noSuccess, noStdout, noStderr, noError, logLevel, cwd, }?: {
55
+ /**
56
+ * Options for {@link execPromise}.
57
+ *
58
+ * The following fields are forwarded as-is to `child_process.exec`:
59
+ * - `cwd` - working directory of the command. Defaults to the current one.
60
+ * - `env` - environment variables, **merged on top of `process.env`**.
61
+ * - `timeout` - maximum run time in milliseconds before the command is killed. No timeout by default.
62
+ * - `maxBuffer` - maximum bytes allowed on stdout/stderr before the command is killed. Defaults to `1024 * 1024` (1 MiB).
63
+ * - `killSignal` - signal used to kill the command on timeout or abort. Defaults to `'SIGTERM'`.
64
+ * - `shell` - shell used to run the command. Defaults to `/bin/sh` (Unix) or `cmd.exe` (Windows).
65
+ * - `signal` - an `AbortSignal` that, once aborted, kills the command and rejects the promise.
66
+ */
67
+ type ExecPromiseOptions = Pick<ExecOptions, 'cwd' | 'timeout' | 'env' | 'maxBuffer' | 'killSignal' | 'shell' | 'signal'> & {
68
+ /**
69
+ * Custom logger used for every output. Falls back to the package logger.
70
+ */
55
71
  logger?: CustomLogger;
72
+ /**
73
+ * Prefix added to every log line, formatted as `[packageName]: `.
74
+ */
56
75
  packageName?: string;
57
76
  /**
58
- * Don't log success message
77
+ * Don't log the success message.
78
+ * @default false
59
79
  */
60
80
  noSuccess?: boolean;
61
81
  /**
62
- * Don't log stdout
82
+ * Don't log stdout.
83
+ * @default false
63
84
  */
64
85
  noStdout?: boolean;
65
86
  /**
66
- * Don't log stderr
87
+ * Don't log stderr.
88
+ * @default false
67
89
  */
68
90
  noStderr?: boolean;
69
91
  /**
70
- * Don't log error
92
+ * Don't log the error when the command fails.
93
+ * @default false
71
94
  */
72
95
  noError?: boolean;
96
+ /**
97
+ * Logging level applied to the default logger before running the command.
98
+ */
73
99
  logLevel?: LogLevel;
74
- cwd?: string;
75
- }): Promise<{
100
+ };
101
+ /**
102
+ * Runs a shell command and resolves with its `stdout` and `stderr`.
103
+ *
104
+ * Secrets (npm auth tokens, `--token`/`--password` flags, provider tokens,
105
+ * JWTs, basic-auth URLs) are automatically masked in every log line and in the
106
+ * rejected error via {@link redactSecrets}. The resolved `stdout`/`stderr` are
107
+ * returned untouched.
108
+ *
109
+ * @param command - The shell command to execute.
110
+ * @returns A promise resolving with the command `stdout` and `stderr`.
111
+ * @throws If the command exits with a non-zero code, times out, is aborted, or
112
+ * exceeds `maxBuffer`. The rejected error is redacted.
113
+ *
114
+ * @example Basic usage
115
+ * ```ts
116
+ * const { stdout } = await execPromise('node --version')
117
+ * ```
118
+ *
119
+ * @example Fail the command after 5 seconds
120
+ * ```ts
121
+ * await execPromise('pnpm install', { timeout: 5000 })
122
+ * ```
123
+ *
124
+ * @example Inject environment variables (merged on top of `process.env`)
125
+ * ```ts
126
+ * await execPromise('node build.mjs', { env: { NODE_ENV: 'production' } })
127
+ * ```
128
+ *
129
+ * @example Cancel the command with an `AbortController`
130
+ * ```ts
131
+ * const controller = new AbortController()
132
+ * const promise = execPromise('long-task', { signal: controller.signal })
133
+ * controller.abort()
134
+ * ```
135
+ */
136
+ declare function execPromise(command: string, { logger, packageName, noSuccess, noStdout, noStderr, noError, logLevel, cwd, timeout, env, maxBuffer, killSignal, shell, signal, }?: ExecPromiseOptions): Promise<{
76
137
  stdout: string;
77
138
  stderr: string;
78
139
  }>;
@@ -88,5 +149,99 @@ declare function printBanner({ name, version, options, }: {
88
149
  };
89
150
  }): void;
90
151
 
91
- export { createLogger, execPromise, logger, printBanner };
92
- export type { LogLevel, Logger, LoggerOptions };
152
+ /**
153
+ * Masks secrets that have a recognizable syntactic context inside a string.
154
+ *
155
+ * Only deterministic patterns are redacted (npm auth tokens, named secret flags
156
+ * such as `--token`/`--password`, prefixed provider tokens like `npm_`/`ghp_`,
157
+ * JWTs, and basic-auth credentials in URLs). Context-free random strings are left
158
+ * untouched because they cannot be detected without false positives.
159
+ *
160
+ * Tokens long enough (>= 16 chars) keep their first and last 4 characters visible
161
+ * (e.g. `npm_***J1iF`) so the right credential can still be recognized; shorter
162
+ * secrets are fully masked.
163
+ */
164
+ declare function redactSecrets(input: string): string;
165
+ /**
166
+ * Returns the given error with its `message`, `cmd` and `stack` redacted via
167
+ * {@link redactSecrets}, so a thrown command never carries secrets to callers.
168
+ */
169
+ declare function redactError<T>(error: T): T;
170
+
171
+ /**
172
+ * AI coding agents supported by `setupAgents`.
173
+ *
174
+ * - `claude`: Claude Code rule (`.claude/rules/maz-ui.md`) and MCP server (`.mcp.json`)
175
+ * - `agents-md`: section in `AGENTS.md` (Codex, Cursor, Copilot, Windsurf, Zed, Gemini...)
176
+ * - `cursor`: MCP server in `.cursor/mcp.json` (rules come from `AGENTS.md`)
177
+ * - `vscode`: MCP server in `.vscode/mcp.json` for GitHub Copilot (rules come from `AGENTS.md`)
178
+ */
179
+ type AgentTarget = 'claude' | 'agents-md' | 'cursor' | 'vscode';
180
+ declare const AGENT_TARGETS: AgentTarget[];
181
+ interface SetupAgentsOptions {
182
+ /** Root of the project (where `AGENTS.md`, `.claude/`, `.mcp.json` live) */
183
+ rootDir: string;
184
+ /** Agents to configure. Default: the agents detected in the project */
185
+ targets?: AgentTarget[];
186
+ /** Write the maz-ui rules (Claude Code rule, `AGENTS.md` section). Default `true` */
187
+ rules?: boolean;
188
+ /** Register the maz-ui MCP server. Default `true` */
189
+ mcp?: boolean;
190
+ /** Compute the changes without writing anything */
191
+ dryRun?: boolean;
192
+ /** Remove everything `setupAgents` added */
193
+ remove?: boolean;
194
+ /** Folder of the maz-ui agent docs. Default: `docs/` of the maz-ui package installed in `rootDir` */
195
+ docsDir?: string;
196
+ /** Command used to start the MCP server */
197
+ mcpServer?: {
198
+ command: string;
199
+ args: string[];
200
+ };
201
+ }
202
+ type SetupAgentsAction = 'create' | 'update' | 'remove' | 'unchanged' | 'skip';
203
+ interface SetupAgentsChange {
204
+ /** Path relative to `rootDir` */
205
+ file: string;
206
+ action: SetupAgentsAction;
207
+ reason?: string;
208
+ }
209
+ interface SetupAgentsResult {
210
+ targets: AgentTarget[];
211
+ version: string;
212
+ changes: SetupAgentsChange[];
213
+ }
214
+ declare const MANAGED_MARKER = "Managed by maz-ui";
215
+ declare const AGENTS_MD_BEGIN = "<!-- BEGIN:maz-ui -->";
216
+ declare const AGENTS_MD_END = "<!-- END:maz-ui -->";
217
+ declare const MCP_SERVER_NAME = "maz-ui";
218
+ declare const DEFAULT_MCP_SERVER: {
219
+ command: string;
220
+ args: string[];
221
+ };
222
+ /** Detects the AI agents configured in a project */
223
+ declare function detectAgentTargets(rootDir: string): AgentTarget[];
224
+ interface AgentDocs {
225
+ directory: string;
226
+ version: string;
227
+ claudeRule: string;
228
+ agentsMdSection: string;
229
+ }
230
+ /**
231
+ * Finds `node_modules/<name>` from a directory up to the filesystem root, like
232
+ * the Node resolution but without the global folders (`NODE_PATH`), so only a
233
+ * package installed for the project is found.
234
+ */
235
+ declare function findInstalledPackage(fromDir: string, name: string): string | undefined;
236
+ /** Reads the agent files shipped in `maz-ui/docs/agents` */
237
+ declare function readAgentDocs(rootDir: string, docsDir?: string): AgentDocs;
238
+ /**
239
+ * Configures the AI coding agents of a project to use maz-ui: rules (version
240
+ * matched with the installed maz-ui) and the maz-ui MCP server. Idempotent and
241
+ * conservative: it only writes files it owns, a delimited section of
242
+ * `AGENTS.md` and its own entry of the MCP config files.
243
+ */
244
+ declare function setupAgents(options: SetupAgentsOptions): SetupAgentsResult;
245
+
246
+ export { AGENTS_MD_BEGIN, AGENTS_MD_END, AGENT_TARGETS, DEFAULT_MCP_SERVER, MANAGED_MARKER, MCP_SERVER_NAME, createLogger, detectAgentTargets, execPromise, findInstalledPackage, logger, printBanner, readAgentDocs, redactError, redactSecrets, setupAgents };
247
+ export type { AgentDocs, AgentTarget, ExecPromiseOptions, LogLevel, Logger, LoggerOptions, SetupAgentsAction, SetupAgentsChange, SetupAgentsOptions, SetupAgentsResult };
package/dist/index.mjs CHANGED
@@ -3,6 +3,9 @@ import process$1 from 'node:process';
3
3
  import { blueBright } from 'colorette';
4
4
  import { createConsola } from 'consola';
5
5
  import figlet from 'figlet';
6
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { parse, applyEdits, modify } from 'jsonc-parser';
6
9
 
7
10
  const LogLevelMap = {
8
11
  silent: Number.NEGATIVE_INFINITY,
@@ -53,6 +56,43 @@ function createLogger(options) {
53
56
  }
54
57
  const logger = createLogger();
55
58
 
59
+ const MASK = "***";
60
+ const VISIBLE_PREFIX = 4;
61
+ const MIN_LENGTH_TO_REVEAL = 16;
62
+ const SECRET_FLAG_NAMES = "password|passwd|pwd|pass|token|api[-_]?key|secret|authorization|auth[-_]?token|access[-_]?token|refresh[-_]?token|client[-_]?secret|private[-_]?key|credentials?|auth";
63
+ const keepEnds = (value) => value.length < MIN_LENGTH_TO_REVEAL ? MASK : `${value.slice(0, VISIBLE_PREFIX)}${MASK}${value.slice(-4)}`;
64
+ const keepPrefix = (_match, prefix, value) => `${prefix}${keepEnds(value)}`;
65
+ const REDACTION_RULES = [
66
+ [/(:?_authToken=)([^\s&]+)/gi, keepPrefix],
67
+ [new RegExp(`(--(?:${SECRET_FLAG_NAMES})=)(\\S+)`, "gi"), keepPrefix],
68
+ [new RegExp(`(--(?:${SECRET_FLAG_NAMES})\\s+)(?!-)(\\S+)`, "gi"), keepPrefix],
69
+ [/(\/\/[^/:@\s]+:)([^/@\s]+)@/g, (_match, prefix, value) => `${prefix}${keepEnds(value)}@`],
70
+ [/npm_[A-Za-z0-9]{10,}/g, keepEnds],
71
+ [/gh[posru]_[A-Za-z0-9]{20,}/g, keepEnds],
72
+ [/github_pat_\w{20,}/g, keepEnds],
73
+ [/glpat-[\w-]{15,}/g, keepEnds],
74
+ [/xox[baprs]-[A-Za-z0-9-]{10,}/g, keepEnds],
75
+ [/AKIA[0-9A-Z]{16}/g, keepEnds],
76
+ [/sk-[\w-]{16,}/g, keepEnds],
77
+ [/eyJ[\w-]+\.eyJ[\w-]+\.[\w-]+/g, keepEnds]
78
+ ];
79
+ const REDACTABLE_ERROR_KEYS = ["message", "cmd", "stack"];
80
+ function redactSecrets(input) {
81
+ return REDACTION_RULES.reduce((acc, [pattern, replacement]) => acc.replace(pattern, replacement), input);
82
+ }
83
+ function redactError(error) {
84
+ if (!error || typeof error !== "object") {
85
+ return error;
86
+ }
87
+ for (const key of REDACTABLE_ERROR_KEYS) {
88
+ const value = error[key];
89
+ if (typeof value === "string") {
90
+ error[key] = redactSecrets(value);
91
+ }
92
+ }
93
+ return error;
94
+ }
95
+
56
96
  async function execPromise(command, {
57
97
  logger: logger$1,
58
98
  packageName,
@@ -61,35 +101,52 @@ async function execPromise(command, {
61
101
  noStderr = false,
62
102
  noError = false,
63
103
  logLevel,
64
- cwd
104
+ cwd,
105
+ timeout,
106
+ env,
107
+ maxBuffer,
108
+ killSignal,
109
+ shell,
110
+ signal
65
111
  } = {}) {
66
112
  if (logLevel) {
67
113
  logger.setLevel(logLevel);
68
114
  }
69
115
  const internalLogger = logger$1 ?? logger;
70
116
  const packageNameStr = packageName ? `[${packageName}]: ` : "";
117
+ const safeCommand = redactSecrets(command);
71
118
  return await new Promise((resolve, reject) => {
72
- exec(command, { cwd }, (error, stdout, stderr) => {
119
+ exec(command, {
120
+ cwd,
121
+ timeout,
122
+ maxBuffer,
123
+ killSignal,
124
+ shell,
125
+ signal,
126
+ env: env ? { ...process$1.env, ...env } : void 0
127
+ }, (error, stdout, stderr) => {
73
128
  if (stdout) {
74
- internalLogger.debug(`${command} - stdout output:`, stdout);
129
+ internalLogger.debug(`${safeCommand} - stdout output:`, redactSecrets(stdout));
75
130
  }
76
131
  if (stderr) {
77
- internalLogger.debug(`${command} - stderr output:`, stderr);
132
+ internalLogger.debug(`${safeCommand} - stderr output:`, redactSecrets(stderr));
78
133
  }
79
134
  if (stdout && !noStdout) {
80
- internalLogger.log(`${packageNameStr}stdout -`, stdout.trim());
135
+ internalLogger.log(`${packageNameStr}stdout -`, redactSecrets(stdout.trim()));
81
136
  }
82
137
  if (stderr && !noStderr) {
83
- internalLogger.log(`${packageNameStr}stderr -`, stderr.trim());
138
+ internalLogger.log(`${packageNameStr}stderr -`, redactSecrets(stderr.trim()));
84
139
  }
85
140
  if (error) {
141
+ const safeError = redactError(error);
86
142
  if (!noError) {
87
- internalLogger.error(`${packageNameStr}${command} failed`, error);
143
+ const reason = error.killed && timeout ? `timed out after ${timeout}ms` : "failed";
144
+ internalLogger.error(`${packageNameStr}${safeCommand} ${reason}`, safeError);
88
145
  }
89
- reject(error);
146
+ reject(safeError);
90
147
  } else {
91
148
  if (!noSuccess) {
92
- internalLogger.info(`${packageNameStr}${command} - Success!`);
149
+ internalLogger.info(`${packageNameStr}${safeCommand} - Success!`);
93
150
  }
94
151
  resolve({ stdout, stderr });
95
152
  }
@@ -126,4 +183,234 @@ function printBanner({
126
183
  logger.break();
127
184
  }
128
185
 
129
- export { createLogger, execPromise, logger, printBanner };
186
+ const AGENT_TARGETS = ["claude", "agents-md", "cursor", "vscode"];
187
+ const MANAGED_MARKER = "Managed by maz-ui";
188
+ const AGENTS_MD_BEGIN = "<!-- BEGIN:maz-ui -->";
189
+ const AGENTS_MD_END = "<!-- END:maz-ui -->";
190
+ const MCP_SERVER_NAME = "maz-ui";
191
+ const DEFAULT_MCP_SERVER = { command: "npx", args: ["-y", "@maz-ui/mcp@latest"] };
192
+ const CLAUDE_RULE_FILE = ".claude/rules/maz-ui.md";
193
+ const CLAUDE_MEMORY_FILES = ["CLAUDE.md", ".claude/CLAUDE.md", "CLAUDE.local.md"];
194
+ function managedHeader(version) {
195
+ return `<!-- ${MANAGED_MARKER} ${version}: generated by \`npx @maz-ui/mcp init\` (or the \`mazUi.agents\` Nuxt option). Do not edit, your changes would be overwritten: write your own rules elsewhere. -->`;
196
+ }
197
+ function detectAgentTargets(rootDir) {
198
+ const has = (path) => existsSync(join(rootDir, path));
199
+ const targets = /* @__PURE__ */ new Set();
200
+ if (has(".claude") || CLAUDE_MEMORY_FILES.some(has) || has(".mcp.json"))
201
+ targets.add("claude");
202
+ if (has(".cursor") || has(".cursorrules"))
203
+ targets.add("cursor");
204
+ if (has(".vscode/mcp.json") || has(".github/copilot-instructions.md") || has(".github/instructions"))
205
+ targets.add("vscode");
206
+ if (has("AGENTS.md") || targets.has("cursor") || targets.has("vscode") || has(".codex") || has(".windsurf") || has(".devin") || has(".windsurfrules") || has("GEMINI.md"))
207
+ targets.add("agents-md");
208
+ return AGENT_TARGETS.filter((target) => targets.has(target));
209
+ }
210
+ function findInstalledPackage(fromDir, name) {
211
+ let directory = fromDir;
212
+ for (; ; ) {
213
+ const candidate = join(directory, "node_modules", name);
214
+ if (existsSync(join(candidate, "package.json")))
215
+ return candidate;
216
+ const parent = dirname(directory);
217
+ if (parent === directory)
218
+ return void 0;
219
+ directory = parent;
220
+ }
221
+ }
222
+ function readAgentDocs(rootDir, docsDir) {
223
+ let directory = docsDir;
224
+ if (!directory) {
225
+ const packageDir = findInstalledPackage(rootDir, "maz-ui");
226
+ if (!packageDir)
227
+ throw new Error(`maz-ui is not installed in ${rootDir}: install it first (\`npm install maz-ui\`).`);
228
+ directory = join(packageDir, "docs");
229
+ }
230
+ const manifestFile = join(directory, "manifest.json");
231
+ const claudeRuleFile = join(directory, "agents/claude-rule.md");
232
+ const agentsMdFile = join(directory, "agents/agents-md.md");
233
+ if (!existsSync(manifestFile) || !existsSync(claudeRuleFile) || !existsSync(agentsMdFile)) {
234
+ throw new Error(`The installed maz-ui does not ship the agent docs (${directory}): upgrade maz-ui to the latest version.`);
235
+ }
236
+ return {
237
+ directory,
238
+ version: JSON.parse(readFileSync(manifestFile, "utf8")).version,
239
+ claudeRule: readFileSync(claudeRuleFile, "utf8").trim(),
240
+ agentsMdSection: readFileSync(agentsMdFile, "utf8").trim()
241
+ };
242
+ }
243
+ class ChangeSet {
244
+ constructor(rootDir, dryRun) {
245
+ this.rootDir = rootDir;
246
+ this.dryRun = dryRun;
247
+ }
248
+ changes = [];
249
+ absolute(file) {
250
+ return join(this.rootDir, file);
251
+ }
252
+ read(file) {
253
+ const path = this.absolute(file);
254
+ return existsSync(path) ? readFileSync(path, "utf8") : void 0;
255
+ }
256
+ write(file, content, current) {
257
+ if (current === content) {
258
+ this.changes.push({ file, action: "unchanged" });
259
+ return;
260
+ }
261
+ this.changes.push({ file, action: current === void 0 ? "create" : "update" });
262
+ if (!this.dryRun) {
263
+ mkdirSync(dirname(this.absolute(file)), { recursive: true });
264
+ writeFileSync(this.absolute(file), content);
265
+ }
266
+ }
267
+ delete(file) {
268
+ this.changes.push({ file, action: "remove" });
269
+ if (!this.dryRun)
270
+ rmSync(this.absolute(file), { force: true });
271
+ }
272
+ skip(file, reason) {
273
+ this.changes.push({ file, action: "skip", reason });
274
+ }
275
+ unchanged(file, reason) {
276
+ this.changes.push({ file, action: "unchanged", reason });
277
+ }
278
+ }
279
+ function isOwned(content) {
280
+ return content === void 0 || content.includes(MANAGED_MARKER);
281
+ }
282
+ function syncOwnedFile(changes, file, content) {
283
+ const current = changes.read(file);
284
+ if (!isOwned(current)) {
285
+ changes.skip(file, "file exists and was not generated by maz-ui: left untouched");
286
+ return;
287
+ }
288
+ if (content === void 0) {
289
+ if (current !== void 0)
290
+ changes.delete(file);
291
+ return;
292
+ }
293
+ changes.write(file, content, current);
294
+ }
295
+ function syncAgentsMd(changes, section) {
296
+ const file = "AGENTS.md";
297
+ const current = changes.read(file);
298
+ const begin = current?.indexOf(AGENTS_MD_BEGIN) ?? -1;
299
+ const end = current?.indexOf(AGENTS_MD_END) ?? -1;
300
+ const hasBlock = begin !== -1 && end > begin;
301
+ if (section === void 0) {
302
+ if (!current || !hasBlock)
303
+ return;
304
+ const remaining = `${current.slice(0, begin)}${current.slice(end + AGENTS_MD_END.length)}`.replace(/\n{3,}/g, "\n\n").trim();
305
+ if (remaining)
306
+ changes.write(file, `${remaining}
307
+ `, current);
308
+ else
309
+ changes.delete(file);
310
+ return;
311
+ }
312
+ const block = `${AGENTS_MD_BEGIN}
313
+
314
+ ${section}
315
+
316
+ ${AGENTS_MD_END}`;
317
+ if (!current) {
318
+ changes.write(file, `${block}
319
+ `, current);
320
+ } else if (hasBlock) {
321
+ changes.write(file, `${current.slice(0, begin)}${block}${current.slice(end + AGENTS_MD_END.length)}`, current);
322
+ } else {
323
+ changes.write(file, `${current.trimEnd()}
324
+
325
+ ${block}
326
+ `, current);
327
+ }
328
+ }
329
+ const MCP_FILES = {
330
+ claude: { file: ".mcp.json", rootKey: "mcpServers", withType: true },
331
+ cursor: { file: ".cursor/mcp.json", rootKey: "mcpServers", withType: false },
332
+ vscode: { file: ".vscode/mcp.json", rootKey: "servers", withType: true }
333
+ };
334
+ function referencesMazUiMcp(server) {
335
+ const serialized = JSON.stringify(server);
336
+ return serialized.includes("@maz-ui/mcp") || serialized.includes("maz-ui-mcp");
337
+ }
338
+ function detectFormatting(content) {
339
+ const indent = /^( +|\t)"/m.exec(content)?.[1] ?? " ";
340
+ return { insertSpaces: !indent.includes(" "), tabSize: indent.includes(" ") ? 1 : indent.length, eol: content.includes("\r\n") ? "\r\n" : "\n" };
341
+ }
342
+ function isJsonObject(value) {
343
+ return typeof value === "object" && value !== null && !Array.isArray(value);
344
+ }
345
+ function syncMcpConfig(changes, config, server, remove) {
346
+ const current = changes.read(config.file);
347
+ const errors = [];
348
+ const parsed = current === void 0 ? {} : parse(current, errors, { allowTrailingComma: true });
349
+ if (errors.length > 0 || !isJsonObject(parsed) || parsed[config.rootKey] !== void 0 && !isJsonObject(parsed[config.rootKey])) {
350
+ changes.skip(config.file, "invalid JSON: left untouched");
351
+ return;
352
+ }
353
+ const servers = parsed[config.rootKey] ?? {};
354
+ const existing = Object.entries(servers).find(([, value2]) => referencesMazUiMcp(value2));
355
+ const base = current ?? "{}\n";
356
+ const formatting = detectFormatting(base);
357
+ if (remove) {
358
+ if (existing?.[0] === MCP_SERVER_NAME) {
359
+ const content2 = applyEdits(base, modify(base, [config.rootKey, MCP_SERVER_NAME], void 0, { formattingOptions: formatting }));
360
+ const remaining = parse(content2);
361
+ const isEmpty = Object.keys(remaining).length === 1 && Object.keys(remaining[config.rootKey]).length === 0;
362
+ if (isEmpty)
363
+ changes.delete(config.file);
364
+ else
365
+ changes.write(config.file, content2, current);
366
+ }
367
+ return;
368
+ }
369
+ if (existing) {
370
+ changes.unchanged(config.file, `maz-ui MCP server already registered as "${existing[0]}"`);
371
+ return;
372
+ }
373
+ if (servers[MCP_SERVER_NAME] !== void 0) {
374
+ changes.skip(config.file, `a "${MCP_SERVER_NAME}" server already exists with another command: left untouched`);
375
+ return;
376
+ }
377
+ const value = config.withType ? { type: "stdio", ...server } : { ...server };
378
+ const content = applyEdits(base, modify(base, [config.rootKey, MCP_SERVER_NAME], value, { formattingOptions: formatting }));
379
+ changes.write(config.file, content.endsWith("\n") ? content : `${content}
380
+ `, current);
381
+ }
382
+ function syncRules(changes, rootDir, targets, docs) {
383
+ const version = docs?.version ?? "";
384
+ const hasClaudeMemory = CLAUDE_MEMORY_FILES.some((file) => existsSync(join(rootDir, file)));
385
+ const claudeReadsAgentsMd = targets.includes("agents-md") && !hasClaudeMemory;
386
+ if (targets.includes("claude")) {
387
+ const rule = docs && !claudeReadsAgentsMd ? `${managedHeader(version)}
388
+
389
+ ${docs.claudeRule}
390
+ ` : void 0;
391
+ syncOwnedFile(changes, CLAUDE_RULE_FILE, rule);
392
+ }
393
+ if (targets.includes("agents-md"))
394
+ syncAgentsMd(changes, docs ? `${managedHeader(version)}
395
+
396
+ ${docs.agentsMdSection}` : void 0);
397
+ }
398
+ function setupAgents(options) {
399
+ const { rootDir, rules = true, mcp = true, dryRun = false, remove = false } = options;
400
+ const targets = options.targets ?? detectAgentTargets(rootDir);
401
+ const docs = remove || !rules ? void 0 : readAgentDocs(rootDir, options.docsDir);
402
+ const version = docs?.version ?? "";
403
+ const changes = new ChangeSet(rootDir, dryRun);
404
+ const server = options.mcpServer ?? DEFAULT_MCP_SERVER;
405
+ if (rules || remove)
406
+ syncRules(changes, rootDir, targets, docs);
407
+ if (mcp || remove) {
408
+ for (const target of ["claude", "cursor", "vscode"]) {
409
+ if (targets.includes(target))
410
+ syncMcpConfig(changes, MCP_FILES[target], server, remove);
411
+ }
412
+ }
413
+ return { targets, version, changes: changes.changes };
414
+ }
415
+
416
+ export { AGENTS_MD_BEGIN, AGENTS_MD_END, AGENT_TARGETS, DEFAULT_MCP_SERVER, MANAGED_MARKER, MCP_SERVER_NAME, createLogger, detectAgentTargets, execPromise, findInstalledPackage, logger, printBanner, readAgentDocs, redactError, redactSecrets, setupAgents };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@maz-ui/node",
3
3
  "type": "module",
4
- "version": "5.0.0-canary.ef64a02.0",
4
+ "version": "5.1.0-beta.0",
5
5
  "description": "Node utilities for Maz-UI library",
6
6
  "author": "Louis Mazel <me@loicmazuel.com>",
7
7
  "license": "MIT",
@@ -15,7 +15,9 @@
15
15
  "utilities",
16
16
  "maz-ui",
17
17
  "promise",
18
- "logger"
18
+ "logger",
19
+ "ai",
20
+ "agents"
19
21
  ],
20
22
  "sideEffects": false,
21
23
  "publishConfig": {
@@ -25,7 +27,8 @@
25
27
  ".": {
26
28
  "monorepo:dev": "./src/index.ts",
27
29
  "types": "./dist/index.d.ts",
28
- "import": "./dist/index.mjs"
30
+ "import": "./dist/index.mjs",
31
+ "default": "./dist/index.mjs"
29
32
  }
30
33
  },
31
34
  "main": "./dist/index.mjs",
@@ -36,12 +39,13 @@
36
39
  "dist"
37
40
  ],
38
41
  "engines": {
39
- "node": ">=18.0.0"
42
+ "node": ">=22.12.0"
40
43
  },
41
44
  "dependencies": {
42
45
  "colorette": "^2.0.20",
43
46
  "consola": "^3.4.2",
44
- "figlet": "^1.11.0"
47
+ "figlet": "^1.12.0",
48
+ "jsonc-parser": "^3.3.1"
45
49
  },
46
50
  "devDependencies": {
47
51
  "unbuild": "^3.6.1"
@@ -52,7 +56,6 @@
52
56
  ]
53
57
  },
54
58
  "scripts": {
55
- "codegen": "nx build",
56
59
  "build": "unbuild",
57
60
  "dev": "unbuild --stub",
58
61
  "typecheck": "tsc --noEmit --skipLibCheck",