relayax-cli 0.4.33 β†’ 0.4.35

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.
@@ -398,7 +398,11 @@ function registerInstall(program) {
398
398
  (0, installer_js_1.printRequiresCheck)(requiresResults);
399
399
  const setupCmd = resolvedAgent.commands.find((c) => c.name.startsWith('setup-'));
400
400
  if (setupCmd && requiresResults.some((r) => r.status === 'missing' || r.status === 'warn')) {
401
- console.log(`\n \x1b[36mπŸ‘‰ 섀정이 ν•„μš”ν•©λ‹ˆλ‹€: \x1b[1m/${setupCmd.name}\x1b[0m\x1b[36m 을 μ‹€ν–‰ν•˜μ„Έμš”\x1b[0m`);
401
+ const toolNames = selectedTools
402
+ ? selectedTools.map((t) => t.name).slice(0, 2).join(' λ˜λŠ” ')
403
+ : 'Claude Code';
404
+ console.log(`\n \x1b[36mπŸ‘‰ 섀정이 ν•„μš”ν•©λ‹ˆλ‹€.\x1b[0m`);
405
+ console.log(` \x1b[36m ${toolNames}λ₯Ό μ—΄κ³  \x1b[1m/${setupCmd.name}\x1b[0m\x1b[36m 을 μž…λ ₯ν•˜μ„Έμš”\x1b[0m`);
402
406
  }
403
407
  }
404
408
  }
@@ -16,6 +16,7 @@ const error_report_js_1 = require("../lib/error-report.js");
16
16
  const step_tracker_js_1 = require("../lib/step-tracker.js");
17
17
  const git_operations_js_1 = require("../lib/git-operations.js");
18
18
  const setup_command_js_1 = require("../lib/setup-command.js");
19
+ const requires_suggest_js_1 = require("../lib/requires-suggest.js");
19
20
  // eslint-disable-next-line @typescript-eslint/no-var-requires
20
21
  const cliPkg = require('../../package.json');
21
22
  const VALID_DIRS = ['skills', 'agents', 'rules', 'commands', 'bin'];
@@ -681,6 +682,28 @@ function registerPublish(program) {
681
682
  console.error(` β†’ relay.yaml에 visibility: ${config.visibility} μ €μž₯됨 (${visLabelMap[config.visibility]})\n`);
682
683
  }
683
684
  }
685
+ // ── Requires μžλ™ 감지 + μ œμ•ˆ ──
686
+ if (isTTY && !json) {
687
+ const suggestions = (0, requires_suggest_js_1.suggestRequires)(relayDir, config.requires);
688
+ if (suggestions.length > 0) {
689
+ console.error('\n\x1b[33m⚑ requires에 λΉ μ§„ ν•­λͺ©μ΄ κ°μ§€λ˜μ—ˆμŠ΅λ‹ˆλ‹€:\x1b[0m');
690
+ for (const line of (0, requires_suggest_js_1.formatSuggestions)(suggestions)) {
691
+ console.error(line);
692
+ }
693
+ const { confirm } = await import('@inquirer/prompts');
694
+ const addThem = await confirm({
695
+ message: 'requires에 μΆ”κ°€ν• κΉŒμš”?',
696
+ default: true,
697
+ });
698
+ if (addThem) {
699
+ config.requires = (0, requires_suggest_js_1.mergeIntoRequires)(config.requires ?? {}, suggestions);
700
+ const yamlData = js_yaml_1.default.load(fs_1.default.readFileSync(relayYamlPath, 'utf-8'));
701
+ yamlData.requires = config.requires;
702
+ fs_1.default.writeFileSync(relayYamlPath, js_yaml_1.default.dump(yamlData, { lineWidth: 120 }), 'utf-8');
703
+ console.error(' β†’ relay.yaml에 requires μ—…λ°μ΄νŠΈλ¨\n');
704
+ }
705
+ }
706
+ }
684
707
  // Generate setup command BEFORE detectCommands so it's included in metadata
685
708
  {
686
709
  const commandsDir = path_1.default.join(relayDir, 'commands');
@@ -0,0 +1,23 @@
1
+ import type { Requires } from '../commands/publish.js';
2
+ interface Suggestion {
3
+ category: 'env' | 'cli' | 'pip' | 'runtime';
4
+ name: string;
5
+ source: string;
6
+ description?: string;
7
+ setup_hint?: string;
8
+ install?: string;
9
+ required?: boolean;
10
+ }
11
+ /**
12
+ * .relay/ 디렉토리λ₯Ό μŠ€μΊ”ν•˜μ—¬ requires에 λΉ μ§„ ν•­λͺ©μ„ μ œμ•ˆν•œλ‹€.
13
+ */
14
+ export declare function suggestRequires(relayDir: string, currentRequires?: Requires): Suggestion[];
15
+ /**
16
+ * μ œμ•ˆ ν•­λͺ©μ„ μ‚¬λžŒμ΄ 읽기 쒋은 ν˜•νƒœλ‘œ ν¬λ§·ν•œλ‹€.
17
+ */
18
+ export declare function formatSuggestions(suggestions: Suggestion[]): string[];
19
+ /**
20
+ * μ œμ•ˆ ν•­λͺ©μ„ requires 객체에 λ³‘ν•©ν•œλ‹€.
21
+ */
22
+ export declare function mergeIntoRequires(requires: Requires, suggestions: Suggestion[]): Requires;
23
+ export {};
@@ -0,0 +1,295 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.suggestRequires = suggestRequires;
7
+ exports.formatSuggestions = formatSuggestions;
8
+ exports.mergeIntoRequires = mergeIntoRequires;
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const path_1 = __importDefault(require("path"));
11
+ // Python stdlib β€” 이 λͺ©λ‘μ— μ—†λŠ” importλŠ” pip νŒ¨ν‚€μ§€ 후보
12
+ const PYTHON_STDLIB = new Set([
13
+ 'abc', 'argparse', 'ast', 'asyncio', 'base64', 'bisect', 'calendar',
14
+ 'cmath', 'codecs', 'collections', 'concurrent', 'contextlib', 'copy',
15
+ 'csv', 'ctypes', 'dataclasses', 'datetime', 'decimal', 'difflib',
16
+ 'email', 'enum', 'errno', 'filecmp', 'fnmatch', 'fractions', 'ftplib',
17
+ 'functools', 'gc', 'gettext', 'glob', 'gzip', 'hashlib', 'heapq',
18
+ 'hmac', 'html', 'http', 'imaplib', 'importlib', 'inspect', 'io',
19
+ 'itertools', 'json', 'keyword', 'linecache', 'locale', 'logging',
20
+ 'lzma', 'math', 'mimetypes', 'multiprocessing', 'numbers', 'operator',
21
+ 'os', 'pathlib', 'pickle', 'platform', 'plistlib', 'pprint',
22
+ 'profile', 'pstats', 'queue', 'random', 're', 'readline',
23
+ 'reprlib', 'resource', 'rlcompleter', 'sched', 'secrets', 'select',
24
+ 'shelve', 'shlex', 'shutil', 'signal', 'site', 'smtplib', 'socket',
25
+ 'socketserver', 'sqlite3', 'ssl', 'stat', 'statistics', 'string',
26
+ 'struct', 'subprocess', 'sys', 'sysconfig', 'syslog', 'tempfile',
27
+ 'textwrap', 'threading', 'time', 'timeit', 'token', 'tokenize',
28
+ 'tomllib', 'trace', 'traceback', 'tracemalloc', 'types', 'typing',
29
+ 'unicodedata', 'unittest', 'urllib', 'uuid', 'venv', 'warnings',
30
+ 'wave', 'weakref', 'webbrowser', 'xml', 'xmlrpc', 'zipfile', 'zipimport', 'zlib',
31
+ // 자주 μ“°μ΄λŠ” μ„œλΈŒλͺ¨λ“ˆ
32
+ 'os.path', 'collections.abc', 'concurrent.futures', 'urllib.parse',
33
+ 'http.client', 'http.server', 'email.mime',
34
+ ]);
35
+ // pip νŒ¨ν‚€μ§€λͺ… β†’ import 이름 λ§€ν•‘ (λ‹€λ₯Έ 경우만)
36
+ const PIP_IMPORT_MAP = {
37
+ 'Pillow': 'PIL',
38
+ 'google-genai': 'google',
39
+ 'scikit-learn': 'sklearn',
40
+ 'python-dotenv': 'dotenv',
41
+ 'beautifulsoup4': 'bs4',
42
+ 'opencv-python': 'cv2',
43
+ 'pyyaml': 'yaml',
44
+ };
45
+ // import 이름 β†’ pip νŒ¨ν‚€μ§€λͺ… μ—­λ§€ν•‘
46
+ const IMPORT_TO_PIP = {};
47
+ for (const [pip, imp] of Object.entries(PIP_IMPORT_MAP)) {
48
+ IMPORT_TO_PIP[imp] = pip;
49
+ }
50
+ /**
51
+ * .relay/ 디렉토리λ₯Ό μŠ€μΊ”ν•˜μ—¬ requires에 λΉ μ§„ ν•­λͺ©μ„ μ œμ•ˆν•œλ‹€.
52
+ */
53
+ function suggestRequires(relayDir, currentRequires) {
54
+ const suggestions = [];
55
+ const existing = normalizeExisting(currentRequires);
56
+ // λͺ¨λ“  슀크립트 파일 μˆ˜μ§‘
57
+ const scripts = findScripts(relayDir);
58
+ for (const scriptPath of scripts) {
59
+ const content = fs_1.default.readFileSync(scriptPath, 'utf-8');
60
+ const relName = path_1.default.relative(relayDir, scriptPath);
61
+ const ext = path_1.default.extname(scriptPath);
62
+ // Python 슀크립트
63
+ if (ext === '.py') {
64
+ // ν™˜κ²½λ³€μˆ˜ 감지: os.environ.get("VAR") / os.environ["VAR"] / os.getenv("VAR")
65
+ const envPattern = /os\.environ\.get\(\s*['"](\w+)['"]/g;
66
+ const envPattern2 = /os\.environ\[['"](\w+)['"]\]/g;
67
+ const envPattern3 = /os\.getenv\(\s*['"](\w+)['"]/g;
68
+ for (const pattern of [envPattern, envPattern2, envPattern3]) {
69
+ let match;
70
+ while ((match = pattern.exec(content)) !== null) {
71
+ const varName = match[1];
72
+ if (!existing.envNames.has(varName)) {
73
+ suggestions.push({ category: 'env', name: varName, source: relName });
74
+ }
75
+ }
76
+ }
77
+ // pip νŒ¨ν‚€μ§€ 감지: import xxx / from xxx import
78
+ const importPattern = /^(?:import|from)\s+(\w+)/gm;
79
+ let match;
80
+ while ((match = importPattern.exec(content)) !== null) {
81
+ const moduleName = match[1];
82
+ if (PYTHON_STDLIB.has(moduleName))
83
+ continue;
84
+ const pipName = IMPORT_TO_PIP[moduleName] ?? moduleName;
85
+ if (!existing.pipNames.has(pipName) && !existing.pipNames.has(moduleName)) {
86
+ suggestions.push({ category: 'pip', name: pipName, source: relName });
87
+ }
88
+ }
89
+ // shebang β†’ python3 λŸ°νƒ€μž„
90
+ if (content.startsWith('#!/') && content.includes('python')) {
91
+ if (!existing.hasRuntime.python) {
92
+ suggestions.push({ category: 'runtime', name: 'python', source: relName });
93
+ }
94
+ }
95
+ }
96
+ // Node/TS 슀크립트
97
+ if (ext === '.js' || ext === '.ts' || ext === '.mjs') {
98
+ // ν™˜κ²½λ³€μˆ˜ 감지: process.env.VAR / process.env['VAR']
99
+ const envPattern = /process\.env\.(\w+)/g;
100
+ const envPattern2 = /process\.env\[['"](\w+)['"]\]/g;
101
+ for (const pattern of [envPattern, envPattern2]) {
102
+ let match;
103
+ while ((match = pattern.exec(content)) !== null) {
104
+ const varName = match[1];
105
+ if (['NODE_ENV', 'PATH', 'HOME', 'PWD'].includes(varName))
106
+ continue;
107
+ if (!existing.envNames.has(varName)) {
108
+ suggestions.push({ category: 'env', name: varName, source: relName });
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ // setup-* μŠ€ν‚¬ 감지: κ΄€λ ¨ envκ°€ requires에 μ—†μœΌλ©΄ μ œμ•ˆ
115
+ const skillsDir = path_1.default.join(relayDir, 'skills');
116
+ if (fs_1.default.existsSync(skillsDir)) {
117
+ for (const entry of fs_1.default.readdirSync(skillsDir, { withFileTypes: true })) {
118
+ if (entry.isDirectory() && entry.name.startsWith('setup-')) {
119
+ const setupName = entry.name; // e.g., setup-kling
120
+ const skillMd = path_1.default.join(skillsDir, setupName, 'SKILL.md');
121
+ if (fs_1.default.existsSync(skillMd)) {
122
+ const skillContent = fs_1.default.readFileSync(skillMd, 'utf-8');
123
+ // SKILL.mdμ—μ„œ ν™˜κ²½λ³€μˆ˜ μ°Έμ‘° μ°ΎκΈ°
124
+ const envRefs = /[A-Z][A-Z0-9_]{2,}/g;
125
+ let match;
126
+ while ((match = envRefs.exec(skillContent)) !== null) {
127
+ const varName = match[0];
128
+ if (['SKILL', 'WHEN', 'THEN', 'SHALL', 'MUST', 'TODO', 'NOTE', 'IMPORTANT', 'WARNING', 'ERROR', 'JSON', 'API', 'URL', 'HTTP', 'HTTPS', 'MCP', 'CLI', 'SDK', 'README', 'YAML'].includes(varName))
129
+ continue;
130
+ if (varName.endsWith('_KEY') || varName.endsWith('_TOKEN') || varName.endsWith('_SECRET') || varName.endsWith('_SESSION')) {
131
+ if (!existing.envNames.has(varName)) {
132
+ suggestions.push({
133
+ category: 'env',
134
+ name: varName,
135
+ source: `skills/${setupName}/SKILL.md`,
136
+ setup_hint: `/${setupName} μŠ€ν‚¬μ„ μ‹€ν–‰ν•˜μ„Έμš”`,
137
+ required: false,
138
+ });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ // CLI 도ꡬ 감지: subprocess.run(['cmd', ...]) / execSync('cmd ...')
147
+ for (const scriptPath of scripts) {
148
+ const content = fs_1.default.readFileSync(scriptPath, 'utf-8');
149
+ const relName = path_1.default.relative(relayDir, scriptPath);
150
+ // Python subprocess
151
+ const subprocessPattern = /subprocess\.(?:run|Popen|call)\(\s*\[?\s*['"](\w+)['"]/g;
152
+ let match;
153
+ while ((match = subprocessPattern.exec(content)) !== null) {
154
+ const cmd = match[1];
155
+ if (['python', 'python3', 'node', 'npm', 'npx'].includes(cmd))
156
+ continue;
157
+ if (!existing.cliNames.has(cmd)) {
158
+ suggestions.push({ category: 'cli', name: cmd, source: relName });
159
+ }
160
+ }
161
+ // Node execSync
162
+ const execPattern = /execSync\(\s*['"`](\w+)/g;
163
+ while ((match = execPattern.exec(content)) !== null) {
164
+ const cmd = match[1];
165
+ if (['node', 'npm', 'npx', 'git'].includes(cmd))
166
+ continue;
167
+ if (!existing.cliNames.has(cmd)) {
168
+ suggestions.push({ category: 'cli', name: cmd, source: relName });
169
+ }
170
+ }
171
+ }
172
+ // 쀑볡 제거
173
+ const seen = new Set();
174
+ return suggestions.filter((s) => {
175
+ const key = `${s.category}:${s.name}`;
176
+ if (seen.has(key))
177
+ return false;
178
+ seen.add(key);
179
+ return true;
180
+ });
181
+ }
182
+ function normalizeExisting(requires) {
183
+ const envNames = new Set();
184
+ const cliNames = new Set();
185
+ const pipNames = new Set();
186
+ const npmNames = new Set();
187
+ const hasRuntime = { node: false, python: false };
188
+ if (!requires)
189
+ return { envNames, cliNames, pipNames, npmNames, hasRuntime };
190
+ for (const e of requires.env ?? [])
191
+ envNames.add(e.name);
192
+ for (const c of requires.cli ?? [])
193
+ cliNames.add(c.name);
194
+ for (const n of requires.npm ?? [])
195
+ npmNames.add(typeof n === 'string' ? n : n.name);
196
+ // pip은 Requires νƒ€μž…μ— μ—†μ§€λ§Œ relay.yaml에 μ‘΄μž¬ν•  수 있음
197
+ const raw = requires;
198
+ if (Array.isArray(raw.pip)) {
199
+ for (const p of raw.pip)
200
+ pipNames.add(typeof p === 'string' ? p : p.name);
201
+ }
202
+ if (requires.runtime?.node)
203
+ hasRuntime.node = true;
204
+ if (requires.runtime?.python)
205
+ hasRuntime.python = true;
206
+ return { envNames, cliNames, pipNames, npmNames, hasRuntime };
207
+ }
208
+ function findScripts(dir) {
209
+ const scripts = [];
210
+ if (!fs_1.default.existsSync(dir))
211
+ return scripts;
212
+ function walk(d) {
213
+ for (const entry of fs_1.default.readdirSync(d, { withFileTypes: true })) {
214
+ if (entry.name.startsWith('.'))
215
+ continue;
216
+ const full = path_1.default.join(d, entry.name);
217
+ if (entry.isDirectory()) {
218
+ walk(full);
219
+ }
220
+ else if (/\.(py|js|ts|mjs|sh)$/.test(entry.name)) {
221
+ scripts.push(full);
222
+ }
223
+ }
224
+ }
225
+ walk(dir);
226
+ return scripts;
227
+ }
228
+ /**
229
+ * μ œμ•ˆ ν•­λͺ©μ„ μ‚¬λžŒμ΄ 읽기 쒋은 ν˜•νƒœλ‘œ ν¬λ§·ν•œλ‹€.
230
+ */
231
+ function formatSuggestions(suggestions) {
232
+ const lines = [];
233
+ const envs = suggestions.filter((s) => s.category === 'env');
234
+ const clis = suggestions.filter((s) => s.category === 'cli');
235
+ const pips = suggestions.filter((s) => s.category === 'pip');
236
+ const runtimes = suggestions.filter((s) => s.category === 'runtime');
237
+ if (envs.length > 0) {
238
+ lines.push(' ν™˜κ²½λ³€μˆ˜:');
239
+ for (const e of envs) {
240
+ const hint = e.setup_hint ? ` (${e.setup_hint})` : '';
241
+ lines.push(` ${e.name}${hint} β€” ${e.source}μ—μ„œ 감지`);
242
+ }
243
+ }
244
+ if (clis.length > 0) {
245
+ lines.push(' CLI 도ꡬ:');
246
+ for (const c of clis)
247
+ lines.push(` ${c.name} β€” ${c.source}μ—μ„œ 감지`);
248
+ }
249
+ if (pips.length > 0) {
250
+ lines.push(' pip νŒ¨ν‚€μ§€:');
251
+ for (const p of pips)
252
+ lines.push(` ${p.name} β€” ${p.source}μ—μ„œ 감지`);
253
+ }
254
+ if (runtimes.length > 0) {
255
+ lines.push(' λŸ°νƒ€μž„:');
256
+ for (const r of runtimes)
257
+ lines.push(` ${r.name} β€” ${r.source}μ—μ„œ 감지`);
258
+ }
259
+ return lines;
260
+ }
261
+ /**
262
+ * μ œμ•ˆ ν•­λͺ©μ„ requires 객체에 λ³‘ν•©ν•œλ‹€.
263
+ */
264
+ function mergeIntoRequires(requires, suggestions) {
265
+ const result = { ...requires };
266
+ for (const s of suggestions) {
267
+ if (s.category === 'env') {
268
+ if (!result.env)
269
+ result.env = [];
270
+ result.env.push({
271
+ name: s.name,
272
+ required: s.required ?? true,
273
+ description: s.description,
274
+ setup_hint: s.setup_hint,
275
+ });
276
+ }
277
+ else if (s.category === 'cli') {
278
+ if (!result.cli)
279
+ result.cli = [];
280
+ result.cli.push({
281
+ name: s.name,
282
+ install: s.install,
283
+ });
284
+ }
285
+ else if (s.category === 'runtime') {
286
+ if (!result.runtime)
287
+ result.runtime = {};
288
+ if (s.name === 'python')
289
+ result.runtime.python = '>=3.8';
290
+ if (s.name === 'node')
291
+ result.runtime.node = '>=18';
292
+ }
293
+ }
294
+ return result;
295
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "relayax-cli",
3
- "version": "0.4.33",
3
+ "version": "0.4.35",
4
4
  "description": "RelayAX Agent Team Marketplace CLI - Install and manage agent teams",
5
5
  "main": "dist/index.js",
6
6
  "bin": {