@tacuchi/agent-factory 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ const yaml = require('js-yaml');
2
+
3
+ const VALID_MODELS = ['opus', 'sonnet', 'haiku', 'inherit'];
4
+ const VALID_TOOLS = ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'WebSearch', 'Task'];
5
+
6
+ const SECRET_PATTERNS = [
7
+ /AIzaSy[\w-]{30,}/,
8
+ /sk-[A-Za-z0-9]{20,}/,
9
+ /ghp_[A-Za-z0-9]{36}/,
10
+ /gho_[A-Za-z0-9]{36}/,
11
+ /-----BEGIN (RSA |EC )?PRIVATE KEY-----/,
12
+ /api[_-]?key\s*[:=]\s*['"][^'"]{10,}['"]/i,
13
+ /token\s*[:=]\s*['"][^'"]{10,}['"]/i,
14
+ /password\s*[:=]\s*['"][^'"]{3,}['"]/i,
15
+ ];
16
+
17
+ const ABSOLUTE_PATH_PATTERN = /(?:\/(?:Users|home|root|etc|var)\/|[A-Z]:\\)/;
18
+
19
+ class AgentValidator {
20
+ constructor() {
21
+ this.errors = [];
22
+ this.warnings = [];
23
+ this.info = [];
24
+ }
25
+
26
+ addError(code, message, metadata = {}) {
27
+ this.errors.push({ level: 'error', code, message, metadata });
28
+ }
29
+
30
+ addWarning(code, message, metadata = {}) {
31
+ this.warnings.push({ level: 'warning', code, message, metadata });
32
+ }
33
+
34
+ addInfo(code, message, metadata = {}) {
35
+ this.info.push({ level: 'info', code, message, metadata });
36
+ }
37
+
38
+ isValid() {
39
+ return this.errors.length === 0;
40
+ }
41
+
42
+ getScore() {
43
+ const errorPenalty = this.errors.length * 25;
44
+ const warningPenalty = this.warnings.length * 5;
45
+ return Math.max(0, 100 - errorPenalty - warningPenalty);
46
+ }
47
+
48
+ getResults() {
49
+ return {
50
+ valid: this.isValid(),
51
+ score: this.getScore(),
52
+ errorCount: this.errors.length,
53
+ warningCount: this.warnings.length,
54
+ infoCount: this.info.length,
55
+ errors: this.errors,
56
+ warnings: this.warnings,
57
+ info: this.info,
58
+ };
59
+ }
60
+
61
+ reset() {
62
+ this.errors = [];
63
+ this.warnings = [];
64
+ this.info = [];
65
+ }
66
+
67
+ validate(content, filename = '') {
68
+ this.reset();
69
+
70
+ if (!content || typeof content !== 'string') {
71
+ this.addError('VAL_E001', 'Content is empty or not a string');
72
+ return this.getResults();
73
+ }
74
+
75
+ const frontmatter = this.extractFrontmatter(content);
76
+
77
+ if (frontmatter) {
78
+ this.validateFrontmatter(frontmatter);
79
+ } else {
80
+ this.addInfo('VAL_I001', 'No YAML frontmatter found (Codex/plain format)');
81
+ }
82
+
83
+ this.checkSecrets(content);
84
+ this.checkAbsolutePaths(content);
85
+
86
+ if (filename && !this.isKebabCase(filename.replace(/\.md$/, ''))) {
87
+ this.addWarning('VAL_W010', `Filename "${filename}" should use kebab-case`);
88
+ }
89
+
90
+ const bodyLength = this.extractBody(content).trim().length;
91
+ if (bodyLength < 50) {
92
+ this.addWarning('VAL_W011', `Agent body is very short (${bodyLength} chars)`);
93
+ }
94
+
95
+ return this.getResults();
96
+ }
97
+
98
+ extractFrontmatter(content) {
99
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
100
+ if (!match) return null;
101
+
102
+ try {
103
+ return yaml.load(match[1]);
104
+ } catch (err) {
105
+ this.addError('VAL_E002', `Invalid YAML frontmatter: ${err.message}`);
106
+ return null;
107
+ }
108
+ }
109
+
110
+ extractBody(content) {
111
+ const match = content.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
112
+ return match ? match[1] : content;
113
+ }
114
+
115
+ validateFrontmatter(fm) {
116
+ if (!fm.name) {
117
+ this.addError('VAL_E003', 'Missing required field: name');
118
+ } else if (!this.isKebabCase(fm.name)) {
119
+ this.addWarning('VAL_W001', `Agent name "${fm.name}" should use kebab-case`);
120
+ }
121
+
122
+ if (!fm.description) {
123
+ this.addError('VAL_E004', 'Missing required field: description');
124
+ } else if (fm.description.length < 10) {
125
+ this.addWarning('VAL_W002', 'Description is too short');
126
+ }
127
+
128
+ if (!fm.model) {
129
+ this.addWarning('VAL_W003', 'Missing field: model (will use default)');
130
+ } else if (!VALID_MODELS.includes(fm.model)) {
131
+ this.addWarning('VAL_W004', `Model "${fm.model}" is not a standard value: ${VALID_MODELS.join(', ')}`);
132
+ }
133
+
134
+ if (fm.tools) {
135
+ const tools = typeof fm.tools === 'string' ? fm.tools.split(',').map((t) => t.trim()) : fm.tools;
136
+ const invalid = tools.filter((t) => !VALID_TOOLS.includes(t));
137
+ if (invalid.length > 0) {
138
+ this.addWarning('VAL_W005', `Unknown tools: ${invalid.join(', ')}`);
139
+ }
140
+ }
141
+ }
142
+
143
+ checkSecrets(content) {
144
+ for (const pattern of SECRET_PATTERNS) {
145
+ if (pattern.test(content)) {
146
+ this.addError('VAL_E010', `Potential secret detected matching pattern: ${pattern.source.slice(0, 30)}...`);
147
+ }
148
+ }
149
+ }
150
+
151
+ checkAbsolutePaths(content) {
152
+ if (ABSOLUTE_PATH_PATTERN.test(content)) {
153
+ this.addWarning('VAL_W020', 'Content contains absolute paths; prefer relative paths or $CLAUDE_PROJECT_DIR');
154
+ }
155
+ }
156
+
157
+ isKebabCase(str) {
158
+ return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(str);
159
+ }
160
+ }
161
+
162
+ module.exports = { AgentValidator, VALID_MODELS, VALID_TOOLS };
@@ -0,0 +1,62 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const yaml = require('js-yaml');
4
+
5
+ const TOOLS_BY_ROLE = {
6
+ specialist: 'Read, Write, Edit, Bash',
7
+ coordinator: 'Read, Bash, Task',
8
+ reviewer: 'Read, Grep, Glob, Bash',
9
+ architect: 'Read, Grep, Glob, Bash',
10
+ };
11
+
12
+ function buildClaudeFormat(name, description, model, tools, body) {
13
+ const frontmatter = yaml.dump(
14
+ { name, description, tools, model },
15
+ { lineWidth: -1, quotingType: '"', forceQuotes: false }
16
+ ).trim();
17
+ return `---\n${frontmatter}\n---\n\n${body}\n`;
18
+ }
19
+
20
+ function buildCodexFormat(body) {
21
+ return `${body}\n`;
22
+ }
23
+
24
+ function buildDescription(role, primaryTech, framework) {
25
+ const tech = framework ? `${framework} (${primaryTech})` : primaryTech;
26
+ const descriptions = {
27
+ specialist: `Specialist agent for ${tech} development`,
28
+ coordinator: 'Workspace coordinator with cross-repo visibility',
29
+ reviewer: `Code reviewer for ${tech} projects`,
30
+ architect: `Software architect for ${tech} systems`,
31
+ };
32
+ return descriptions[role] || `Agent for ${tech}`;
33
+ }
34
+
35
+ async function writeAgent({ name, role, model, tools, body, outputDir, target }) {
36
+ const results = { claude: null, codex: null };
37
+ const description = body.split('\n').find((l) => l.trim() && !l.startsWith('#'))?.trim() || name;
38
+
39
+ const resolvedTools = tools || TOOLS_BY_ROLE[role] || 'Read, Write, Edit, Bash';
40
+
41
+ if (target === 'claude' || target === 'all') {
42
+ const claudeDir = path.join(outputDir, '.claude', 'agents');
43
+ await fs.ensureDir(claudeDir);
44
+ const claudeContent = buildClaudeFormat(name, description, model, resolvedTools, body);
45
+ const claudePath = path.join(claudeDir, `${name}.md`);
46
+ await fs.writeFile(claudePath, claudeContent, 'utf8');
47
+ results.claude = claudePath;
48
+ }
49
+
50
+ if (target === 'codex' || target === 'all') {
51
+ const codexDir = path.join(outputDir, '.agents');
52
+ await fs.ensureDir(codexDir);
53
+ const codexContent = buildCodexFormat(body);
54
+ const codexPath = path.join(codexDir, `${name}.md`);
55
+ await fs.writeFile(codexPath, codexContent, 'utf8');
56
+ results.codex = codexPath;
57
+ }
58
+
59
+ return results;
60
+ }
61
+
62
+ module.exports = { writeAgent, buildClaudeFormat, buildCodexFormat, buildDescription, TOOLS_BY_ROLE };
@@ -0,0 +1,319 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ function sanitize(value, maxLen = 100) {
5
+ if (!value) return '';
6
+ return value
7
+ .replace(/[\x00-\x09\x0B-\x1F]/g, '')
8
+ .replace(/`/g, '')
9
+ .replace(/\$\([^)]*\)/g, '')
10
+ .replace(/\$\{[^}]*\}/g, '')
11
+ .replace(/<!--[^>]*-->/g, '')
12
+ .replace(/\{\{[^}]*\}\}/g, '')
13
+ .replace(/[<>]/g, '')
14
+ .slice(0, maxLen)
15
+ .trim();
16
+ }
17
+
18
+ async function findFiles(dir, extensions, maxDepth = 2) {
19
+ const results = [];
20
+
21
+ async function walk(current, depth) {
22
+ if (depth > maxDepth) return;
23
+ try {
24
+ const entries = await fs.readdir(current, { withFileTypes: true });
25
+ for (const entry of entries) {
26
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'vendor') continue;
27
+ const full = path.join(current, entry.name);
28
+ if (entry.isDirectory()) {
29
+ await walk(full, depth + 1);
30
+ } else if (extensions.some((ext) => entry.name.endsWith(ext))) {
31
+ results.push(full);
32
+ }
33
+ }
34
+ } catch {
35
+ // directorio inaccesible
36
+ }
37
+ }
38
+
39
+ await walk(dir, 0);
40
+ return results;
41
+ }
42
+
43
+ function readJsonSafe(filePath) {
44
+ try {
45
+ return fs.readJsonSync(filePath);
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function readFileSafe(filePath) {
52
+ try {
53
+ return fs.readFileSync(filePath, 'utf8');
54
+ } catch {
55
+ return '';
56
+ }
57
+ }
58
+
59
+ function extractVersion(content, pattern) {
60
+ const match = content.match(pattern);
61
+ return match ? match[1] : '';
62
+ }
63
+
64
+ async function detect(repoPath) {
65
+ const abs = path.resolve(repoPath);
66
+ let primaryTech = '';
67
+ let framework = '';
68
+ let verifyCommands = '';
69
+ const stackParts = [];
70
+
71
+ // --- JS/TS: package.json ---
72
+ const pkgPath = path.join(abs, 'package.json');
73
+ if (await fs.pathExists(pkgPath)) {
74
+ const pkg = readJsonSafe(pkgPath);
75
+ if (pkg) {
76
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
77
+
78
+ if (deps['@angular/core']) {
79
+ const ver = (deps['@angular/core'] || '').replace(/[~^]/g, '').split('.')[0];
80
+ primaryTech = 'TypeScript';
81
+ framework = `Angular ${ver}`;
82
+ verifyCommands = 'ng build, ng test, ng lint';
83
+ stackParts.push(`Angular ${ver}`);
84
+ } else if (deps.next) {
85
+ primaryTech = 'TypeScript/JS';
86
+ framework = 'Next.js';
87
+ verifyCommands = 'npm run build, npm run lint';
88
+ stackParts.push('Next.js');
89
+ } else if (deps.react) {
90
+ primaryTech = 'TypeScript/JS';
91
+ framework = 'React';
92
+ verifyCommands = 'npm run build, npm test';
93
+ stackParts.push('React');
94
+ } else if (deps.vue) {
95
+ primaryTech = 'TypeScript/JS';
96
+ framework = 'Vue.js';
97
+ verifyCommands = 'npm run build, npm test';
98
+ stackParts.push('Vue.js');
99
+ } else if (deps.svelte) {
100
+ primaryTech = 'TypeScript/JS';
101
+ framework = 'Svelte';
102
+ verifyCommands = 'npm run build, npm run check';
103
+ stackParts.push('Svelte');
104
+ } else if (deps.nuxt) {
105
+ primaryTech = 'TypeScript/JS';
106
+ framework = 'Nuxt';
107
+ verifyCommands = 'npm run build, npm run lint';
108
+ stackParts.push('Nuxt');
109
+ } else if (deps.express || deps.fastify || deps.koa || deps.hono) {
110
+ primaryTech = 'TypeScript/JS';
111
+ framework = 'Node.js';
112
+ verifyCommands = 'npm run build, npm test';
113
+ stackParts.push('Node.js');
114
+ }
115
+ }
116
+ }
117
+
118
+ // --- Java: pom.xml (Maven) ---
119
+ const pomPath = path.join(abs, 'pom.xml');
120
+ if (await fs.pathExists(pomPath)) {
121
+ const pom = readFileSafe(pomPath);
122
+
123
+ if (pom.includes('spring-boot-starter')) {
124
+ const sbVersion = extractVersion(pom, /spring-boot-starter-parent[\s\S]*?<version>([\d.]+)<\/version>/);
125
+ const javaVersion =
126
+ extractVersion(pom, /<java\.version>([\d.]+)<\/java\.version>/) ||
127
+ extractVersion(pom, /<maven\.compiler\.source>([\d.]+)<\/maven\.compiler\.source>/);
128
+
129
+ primaryTech = javaVersion ? `Java ${javaVersion}` : 'Java';
130
+ framework = sbVersion ? `Spring Boot ${sbVersion} + Maven` : 'Spring Boot + Maven';
131
+ verifyCommands = 'mvn compile, mvn test, mvn verify';
132
+ stackParts.push(sbVersion ? `Spring Boot ${sbVersion}` : 'Spring Boot');
133
+ stackParts.push('Maven');
134
+ if (javaVersion) stackParts.push(`Java ${javaVersion}`);
135
+
136
+ if (pom.includes('spring-boot-starter-data-jpa')) stackParts.push('JPA');
137
+ if (pom.includes('spring-cloud-starter-openfeign')) stackParts.push('Feign');
138
+ if (pom.includes('postgresql')) stackParts.push('PostgreSQL');
139
+ if (pom.includes('mysql-connector')) stackParts.push('MySQL');
140
+ if (pom.includes('spring-boot-starter-data-mongodb')) stackParts.push('MongoDB');
141
+ }
142
+ }
143
+
144
+ // --- Java/Kotlin: build.gradle(.kts) ---
145
+ if (!primaryTech) {
146
+ const gradleKts = path.join(abs, 'build.gradle.kts');
147
+ const gradleGroovy = path.join(abs, 'build.gradle');
148
+ const gradlePath = (await fs.pathExists(gradleKts)) ? gradleKts : (await fs.pathExists(gradleGroovy)) ? gradleGroovy : null;
149
+
150
+ if (gradlePath) {
151
+ const gradle = readFileSafe(gradlePath);
152
+ if (gradle.includes('org.springframework.boot')) {
153
+ primaryTech = gradlePath.endsWith('.kts') ? 'Kotlin' : 'Java/Kotlin';
154
+ framework = 'Spring Boot + Gradle';
155
+ verifyCommands = 'gradle build, gradle test';
156
+ stackParts.push('Spring Boot', 'Gradle');
157
+ }
158
+ }
159
+ }
160
+
161
+ // --- Python ---
162
+ if (!primaryTech) {
163
+ const pyprojectPath = path.join(abs, 'pyproject.toml');
164
+ const reqsPath = path.join(abs, 'requirements.txt');
165
+ let pyContent = '';
166
+
167
+ if (await fs.pathExists(pyprojectPath)) {
168
+ pyContent = readFileSafe(pyprojectPath);
169
+ } else if (await fs.pathExists(reqsPath)) {
170
+ pyContent = readFileSafe(reqsPath);
171
+ }
172
+
173
+ if (pyContent) {
174
+ const lower = pyContent.toLowerCase();
175
+ if (lower.includes('django')) {
176
+ primaryTech = 'Python';
177
+ framework = 'Django';
178
+ verifyCommands = 'python manage.py test';
179
+ stackParts.push('Django');
180
+ } else if (lower.includes('fastapi')) {
181
+ primaryTech = 'Python';
182
+ framework = 'FastAPI';
183
+ verifyCommands = 'pytest';
184
+ stackParts.push('FastAPI');
185
+ } else if (lower.includes('flask')) {
186
+ primaryTech = 'Python';
187
+ framework = 'Flask';
188
+ verifyCommands = 'pytest';
189
+ stackParts.push('Flask');
190
+ } else {
191
+ primaryTech = 'Python';
192
+ verifyCommands = 'pytest';
193
+ stackParts.push('Python');
194
+ }
195
+ }
196
+ }
197
+
198
+ // --- Go ---
199
+ if (!primaryTech) {
200
+ const goModPath = path.join(abs, 'go.mod');
201
+ if (await fs.pathExists(goModPath)) {
202
+ const goMod = readFileSafe(goModPath);
203
+ const goVer = extractVersion(goMod, /^go\s+([\d.]+)/m);
204
+ primaryTech = goVer ? `Go ${goVer}` : 'Go';
205
+ verifyCommands = 'go build ./..., go test ./...';
206
+ stackParts.push(primaryTech);
207
+ }
208
+ }
209
+
210
+ // --- Rust ---
211
+ if (!primaryTech) {
212
+ const cargoPath = path.join(abs, 'Cargo.toml');
213
+ if (await fs.pathExists(cargoPath)) {
214
+ primaryTech = 'Rust';
215
+ verifyCommands = 'cargo build, cargo test';
216
+ stackParts.push('Rust');
217
+ }
218
+ }
219
+
220
+ // --- Dart / Flutter ---
221
+ if (!primaryTech) {
222
+ const pubspecPath = path.join(abs, 'pubspec.yaml');
223
+ if (await fs.pathExists(pubspecPath)) {
224
+ const pubspec = readFileSafe(pubspecPath);
225
+ if (pubspec.includes('flutter')) {
226
+ primaryTech = 'Dart';
227
+ framework = 'Flutter';
228
+ verifyCommands = 'flutter analyze, flutter test';
229
+ stackParts.push('Flutter');
230
+ } else {
231
+ primaryTech = 'Dart';
232
+ verifyCommands = 'dart analyze, dart test';
233
+ stackParts.push('Dart');
234
+ }
235
+ }
236
+ }
237
+
238
+ // --- .NET ---
239
+ if (!primaryTech) {
240
+ const csprojFiles = await findFiles(abs, ['.csproj'], 1);
241
+ if (csprojFiles.length > 0) {
242
+ primaryTech = 'C#';
243
+ framework = '.NET';
244
+ verifyCommands = 'dotnet build, dotnet test';
245
+ stackParts.push('.NET');
246
+ }
247
+ }
248
+
249
+ // --- Ruby ---
250
+ if (!primaryTech) {
251
+ const gemfilePath = path.join(abs, 'Gemfile');
252
+ if (await fs.pathExists(gemfilePath)) {
253
+ const gemfile = readFileSafe(gemfilePath);
254
+ primaryTech = 'Ruby';
255
+ if (gemfile.includes('rails')) {
256
+ framework = 'Rails';
257
+ verifyCommands = 'rails test';
258
+ stackParts.push('Rails');
259
+ } else if (gemfile.includes('sinatra')) {
260
+ framework = 'Sinatra';
261
+ verifyCommands = 'ruby test/';
262
+ stackParts.push('Sinatra');
263
+ } else {
264
+ verifyCommands = 'ruby -c';
265
+ stackParts.push('Ruby');
266
+ }
267
+ }
268
+ }
269
+
270
+ // --- Supplementary ---
271
+ if (await fs.pathExists(path.join(abs, 'tsconfig.json'))) {
272
+ if (!stackParts.includes('TypeScript') && primaryTech !== 'TypeScript') {
273
+ stackParts.push('TypeScript');
274
+ }
275
+ }
276
+
277
+ const hasSCSS =
278
+ (await findFiles(abs, ['.scss'], 2)).length > 0;
279
+ if (hasSCSS) stackParts.push('SCSS');
280
+
281
+ const tailwindFiles = await findFiles(abs, ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs'], 1);
282
+ if (tailwindFiles.length > 0) stackParts.push('Tailwind CSS');
283
+
284
+ if (await fs.pathExists(pkgPath)) {
285
+ const pkgRaw = readFileSafe(pkgPath);
286
+ if (pkgRaw.includes('"bootstrap"')) stackParts.push('Bootstrap');
287
+ if (pkgRaw.includes('"@angular/material"')) stackParts.push('Angular Material');
288
+ }
289
+
290
+ if (await fs.pathExists(path.join(abs, 'Dockerfile'))) {
291
+ stackParts.push('Docker');
292
+ }
293
+
294
+ if (await fs.pathExists(path.join(abs, 'docker-compose.yml')) || await fs.pathExists(path.join(abs, 'docker-compose.yaml'))) {
295
+ if (!stackParts.includes('Docker')) stackParts.push('Docker');
296
+ stackParts.push('Docker Compose');
297
+ }
298
+
299
+ // --- Fallback ---
300
+ if (!primaryTech) {
301
+ primaryTech = 'Generic';
302
+ }
303
+
304
+ const stackCsv = stackParts.length > 0 ? stackParts.map((p) => sanitize(p, 50)).join(', ') : sanitize(primaryTech, 50);
305
+
306
+ return {
307
+ primaryTech: sanitize(primaryTech, 50),
308
+ framework: sanitize(framework, 100),
309
+ verifyCommands: sanitize(verifyCommands, 200),
310
+ stackParts: stackParts.map((p) => sanitize(p, 50)),
311
+ stackCsv,
312
+ };
313
+ }
314
+
315
+ function deriveAlias(repoPath) {
316
+ return path.basename(repoPath).replace(/[_.]/g, '-').slice(0, 30);
317
+ }
318
+
319
+ module.exports = { detect, deriveAlias, sanitize, findFiles };
@@ -0,0 +1,31 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+
4
+ const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates', 'roles');
5
+
6
+ function render(template, data) {
7
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
8
+ if (key in data) {
9
+ return data[key] ?? '';
10
+ }
11
+ return match;
12
+ });
13
+ }
14
+
15
+ async function renderFile(templateName, data) {
16
+ const filePath = path.join(TEMPLATES_DIR, templateName);
17
+ const template = await fs.readFile(filePath, 'utf8');
18
+ return render(template, data);
19
+ }
20
+
21
+ function listTemplates() {
22
+ if (!fs.existsSync(TEMPLATES_DIR)) {
23
+ return [];
24
+ }
25
+ return fs
26
+ .readdirSync(TEMPLATES_DIR)
27
+ .filter((f) => f.endsWith('.md.tmpl'))
28
+ .map((f) => f.replace('.md.tmpl', ''));
29
+ }
30
+
31
+ module.exports = { render, renderFile, listTemplates, TEMPLATES_DIR };
@@ -0,0 +1,57 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+
4
+ let _quiet = false;
5
+
6
+ const COLORS = {
7
+ primary: '#7C3AED',
8
+ success: '#10B981',
9
+ warning: '#F59E0B',
10
+ error: '#EF4444',
11
+ info: '#3B82F6',
12
+ muted: '#6B7280',
13
+ };
14
+
15
+ const NOOP = () => {};
16
+
17
+ const log = {
18
+ info: (msg) => !_quiet && console.log(chalk.hex(COLORS.info)('ℹ'), msg),
19
+ success: (msg) => !_quiet && console.log(chalk.hex(COLORS.success)('✓'), msg),
20
+ warn: (msg) => !_quiet && console.log(chalk.hex(COLORS.warning)('⚠'), msg),
21
+ error: (msg) => !_quiet && console.log(chalk.hex(COLORS.error)('✗'), msg),
22
+ muted: (msg) => !_quiet && console.log(chalk.hex(COLORS.muted)(msg)),
23
+ label: (label, value) =>
24
+ !_quiet && console.log(chalk.hex(COLORS.muted)(label + ':'), chalk.white(value)),
25
+ };
26
+
27
+ const SPINNER_STUB = {
28
+ start() { return this; },
29
+ succeed() { return this; },
30
+ fail() { return this; },
31
+ stop() { return this; },
32
+ };
33
+
34
+ function spinner(text) {
35
+ if (_quiet) return SPINNER_STUB;
36
+ return ora({ text, color: 'magenta' });
37
+ }
38
+
39
+ function banner(version) {
40
+ if (_quiet) return;
41
+ const title = 'agent-factory';
42
+ const colored = title
43
+ .split('')
44
+ .map((c, i) => chalk.hex(i < 6 ? '#7C3AED' : '#A78BFA')(c))
45
+ .join('');
46
+ console.log(`\n 🏭 ${colored} ${chalk.gray(`v${version}`)}\n`);
47
+ }
48
+
49
+ function setQuiet(value) {
50
+ _quiet = !!value;
51
+ }
52
+
53
+ function isQuiet() {
54
+ return _quiet;
55
+ }
56
+
57
+ module.exports = { log, spinner, banner, setQuiet, isQuiet, COLORS, chalk };