ronds_ai 0.1.13 → 0.1.14

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/lib/doctor.js CHANGED
@@ -1,89 +1,89 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const { runGit } = require('./git');
5
-
6
- const SUPPORTED_TOOLS = new Set(['claude', 'codex', 'cursor']);
7
-
8
- function getFailedEventDir() {
9
- return path.join(os.homedir(), '.ronds_ai', 'failed-events');
10
- }
11
-
12
- function readRecentLogs(tool) {
13
- const dir = getFailedEventDir();
14
- if (!fs.existsSync(dir)) {
15
- return [];
16
- }
17
-
18
- return fs.readdirSync(dir)
19
- .filter((name) => name.endsWith('.jsonl') && (name.includes(`-${tool}-error`) || name.includes('-cli-error')))
20
- .sort()
21
- .reverse()
22
- .slice(0, 3)
23
- .map((name) => path.join(dir, name));
24
- }
25
-
26
- function getConfigChecks(tool, baseDir) {
27
- if (tool === 'cursor') {
28
- return [
29
- {
30
- label: '.cursor/hooks.json',
31
- path: path.join(baseDir, '.cursor', 'hooks.json'),
32
- },
33
- ];
34
- }
35
-
36
- if (tool === 'codex') {
37
- return [
38
- {
39
- label: '.codex/hooks.json',
40
- path: path.join(baseDir, '.codex', 'hooks.json'),
41
- },
42
- ];
43
- }
44
-
45
- return [
46
- {
47
- label: '.claude/settings.json',
48
- path: path.join(baseDir, '.claude', 'settings.json'),
49
- },
50
- {
51
- label: '.claude/settings.local.json',
52
- path: path.join(baseDir, '.claude', 'settings.local.json'),
53
- },
54
- ];
55
- }
56
-
57
- function readGitUserEmail(baseDir) {
58
- return runGit(baseDir, ['config', 'user.email'], false);
59
- }
60
-
61
- function readGitRemoteUrl(baseDir) {
62
- return runGit(baseDir, ['remote', 'get-url', 'origin'], false);
63
- }
64
-
65
- function runDoctor(tool, targetDir = process.cwd()) {
66
- const normalizedTool = String(tool || '').trim().toLowerCase();
67
- if (!SUPPORTED_TOOLS.has(normalizedTool)) {
68
- throw new Error(`Unsupported doctor tool: ${tool || ''}`);
69
- }
70
-
71
- const baseDir = path.resolve(targetDir);
72
- const configChecks = getConfigChecks(normalizedTool, baseDir).map((item) => ({
73
- ...item,
74
- exists: fs.existsSync(item.path),
75
- }));
76
-
77
- return {
78
- tool: normalizedTool,
79
- targetDir: baseDir,
80
- gitUserEmail: readGitUserEmail(baseDir),
81
- gitRemoteUrl: readGitRemoteUrl(baseDir),
82
- configChecks,
83
- recentLogs: readRecentLogs(normalizedTool),
84
- };
85
- }
86
-
87
- module.exports = {
88
- runDoctor,
89
- };
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const { runGit } = require('./git');
5
+
6
+ const SUPPORTED_TOOLS = new Set(['claude', 'codex', 'cursor']);
7
+
8
+ function getFailedEventDir() {
9
+ return path.join(os.homedir(), '.ronds_ai', 'failed-events');
10
+ }
11
+
12
+ function readRecentLogs(tool) {
13
+ const dir = getFailedEventDir();
14
+ if (!fs.existsSync(dir)) {
15
+ return [];
16
+ }
17
+
18
+ return fs.readdirSync(dir)
19
+ .filter((name) => name.endsWith('.jsonl') && (name.includes(`-${tool}-error`) || name.includes('-cli-error')))
20
+ .sort()
21
+ .reverse()
22
+ .slice(0, 3)
23
+ .map((name) => path.join(dir, name));
24
+ }
25
+
26
+ function getConfigChecks(tool, baseDir) {
27
+ if (tool === 'cursor') {
28
+ return [
29
+ {
30
+ label: '.cursor/hooks.json',
31
+ path: path.join(baseDir, '.cursor', 'hooks.json'),
32
+ },
33
+ ];
34
+ }
35
+
36
+ if (tool === 'codex') {
37
+ return [
38
+ {
39
+ label: '.codex/hooks.json',
40
+ path: path.join(baseDir, '.codex', 'hooks.json'),
41
+ },
42
+ ];
43
+ }
44
+
45
+ return [
46
+ {
47
+ label: '.claude/settings.json',
48
+ path: path.join(baseDir, '.claude', 'settings.json'),
49
+ },
50
+ {
51
+ label: '.claude/settings.local.json',
52
+ path: path.join(baseDir, '.claude', 'settings.local.json'),
53
+ },
54
+ ];
55
+ }
56
+
57
+ function readGitUserEmail(baseDir) {
58
+ return runGit(baseDir, ['config', 'user.email'], false);
59
+ }
60
+
61
+ function readGitRemoteUrl(baseDir) {
62
+ return runGit(baseDir, ['remote', 'get-url', 'origin'], false);
63
+ }
64
+
65
+ function runDoctor(tool, targetDir = process.cwd()) {
66
+ const normalizedTool = String(tool || '').trim().toLowerCase();
67
+ if (!SUPPORTED_TOOLS.has(normalizedTool)) {
68
+ throw new Error(`Unsupported doctor tool: ${tool || ''}`);
69
+ }
70
+
71
+ const baseDir = path.resolve(targetDir);
72
+ const configChecks = getConfigChecks(normalizedTool, baseDir).map((item) => ({
73
+ ...item,
74
+ exists: fs.existsSync(item.path),
75
+ }));
76
+
77
+ return {
78
+ tool: normalizedTool,
79
+ targetDir: baseDir,
80
+ gitUserEmail: readGitUserEmail(baseDir),
81
+ gitRemoteUrl: readGitRemoteUrl(baseDir),
82
+ configChecks,
83
+ recentLogs: readRecentLogs(normalizedTool),
84
+ };
85
+ }
86
+
87
+ module.exports = {
88
+ runDoctor,
89
+ };
@@ -1,226 +1,226 @@
1
- const fs = require('fs');
2
- const os = require('os');
3
- const path = require('path');
4
- const http = require('http');
5
- const https = require('https');
6
- const { execFileSync } = require('child_process');
7
- const { buildInstallTargets, normalizeScope, normalizeTools } = require('./skills_targets');
8
- const { promptForScope, promptForTools } = require('./skills_prompt');
9
-
10
- const SKILL_DOWNLOAD_BASE_URL = 'https://aihub.ronds.com/api/api/v1/skills/download';
11
- const MAX_REDIRECTS = 5;
12
-
13
- function parseJson(raw, label) {
14
- try {
15
- return JSON.parse(raw);
16
- } catch (error) {
17
- const detail = error instanceof Error ? error.message : String(error);
18
- throw new Error(`${label} is not valid JSON: ${detail}`);
19
- }
20
- }
21
-
22
- function loadDownloadConfig() {
23
- const token = String(process.env.SKILL_DOWNLOAD_TOKEN || '').trim();
24
- const extraHeaders = process.env.SKILL_DOWNLOAD_HEADERS
25
- ? parseJson(process.env.SKILL_DOWNLOAD_HEADERS, 'SKILL_DOWNLOAD_HEADERS')
26
- : {};
27
-
28
- const headers = {
29
- ...extraHeaders,
30
- };
31
-
32
- if (token) {
33
- headers.authorization = `Bearer ${token}`;
34
- }
35
-
36
- return { headers };
37
- }
38
-
39
- function copyDirectoryRecursive(sourceDir, destinationDir) {
40
- const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
41
- fs.mkdirSync(destinationDir, { recursive: true });
42
-
43
- for (const entry of entries) {
44
- const sourcePath = path.join(sourceDir, entry.name);
45
- const destinationPath = path.join(destinationDir, entry.name);
46
-
47
- if (entry.isDirectory()) {
48
- copyDirectoryRecursive(sourcePath, destinationPath);
49
- continue;
50
- }
51
-
52
- if (entry.isSymbolicLink()) {
53
- const targetPath = fs.readlinkSync(sourcePath);
54
- fs.symlinkSync(targetPath, destinationPath);
55
- continue;
56
- }
57
-
58
- fs.copyFileSync(sourcePath, destinationPath);
59
- }
60
- }
61
-
62
- function copyDirectory(sourceDir, destinationDir) {
63
- if (typeof fs.cpSync === 'function') {
64
- fs.cpSync(sourceDir, destinationDir, { recursive: true });
65
- return;
66
- }
67
-
68
- copyDirectoryRecursive(sourceDir, destinationDir);
69
- }
70
-
71
- function streamToFile(url, destinationPath, config, redirectCount = 0) {
72
- return new Promise((resolve, reject) => {
73
- const transport = String(url).startsWith('http://') ? http : https;
74
- const request = transport.get(url, { headers: config.headers }, (response) => {
75
- if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
76
- response.resume();
77
- if (redirectCount >= MAX_REDIRECTS) {
78
- reject(new Error('Too many redirects while downloading skill archive'));
79
- return;
80
- }
81
-
82
- const nextUrl = new URL(response.headers.location, url).toString();
83
- streamToFile(nextUrl, destinationPath, config, redirectCount + 1).then(resolve, reject);
84
- return;
85
- }
86
-
87
- if (response.statusCode !== 200) {
88
- response.resume();
89
- reject(new Error(`Skill download failed with status ${response.statusCode || 0}`));
90
- return;
91
- }
92
-
93
- const fileStream = fs.createWriteStream(destinationPath);
94
- response.pipe(fileStream);
95
-
96
- fileStream.on('finish', () => {
97
- fileStream.close(() => resolve(destinationPath));
98
- });
99
-
100
- fileStream.on('error', (error) => {
101
- fileStream.close(() => reject(error));
102
- });
103
- });
104
-
105
- request.on('error', reject);
106
- });
107
- }
108
-
109
- async function downloadSkillArchive(name, workingDir) {
110
- const archivePath = path.join(workingDir, `${name}.zip`);
111
- const url = `${SKILL_DOWNLOAD_BASE_URL}/${encodeURIComponent(name)}`;
112
- await streamToFile(url, archivePath, loadDownloadConfig());
113
- return {
114
- url,
115
- archivePath,
116
- };
117
- }
118
-
119
- function extractArchive(archivePath, outputDir) {
120
- fs.mkdirSync(outputDir, { recursive: true });
121
- execFileSync('unzip', ['-q', archivePath, '-d', outputDir], {
122
- stdio: ['ignore', 'pipe', 'pipe'],
123
- });
124
- }
125
-
126
- function findSkillRoot(extractDir) {
127
- const directSkillMd = path.join(extractDir, 'SKILL.md');
128
- if (fs.existsSync(directSkillMd)) {
129
- return extractDir;
130
- }
131
-
132
- const entries = fs.readdirSync(extractDir, { withFileTypes: true });
133
- const candidateDirs = entries
134
- .filter((entry) => entry.isDirectory())
135
- .map((entry) => path.join(extractDir, entry.name))
136
- .filter((entryPath) => fs.existsSync(path.join(entryPath, 'SKILL.md')));
137
-
138
- if (candidateDirs.length === 1) {
139
- return candidateDirs[0];
140
- }
141
-
142
- if (candidateDirs.length > 1) {
143
- throw new Error('Downloaded archive contains multiple skill roots');
144
- }
145
-
146
- throw new Error('Downloaded archive does not contain SKILL.md');
147
- }
148
-
149
- function copySkill(sourceDir, destinationDir, force) {
150
- if (fs.existsSync(destinationDir)) {
151
- if (!force) {
152
- throw new Error(`Skill already exists at ${destinationDir}. Use --force to overwrite.`);
153
- }
154
-
155
- fs.rmSync(destinationDir, { recursive: true, force: true });
156
- }
157
-
158
- fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
159
- copyDirectory(sourceDir, destinationDir);
160
- }
161
-
162
- async function resolveInstallOptions(options) {
163
- let tools = normalizeTools(options.tool);
164
- if (tools.length === 0) {
165
- tools = normalizeTools(await promptForTools());
166
- }
167
-
168
- if (tools.length === 0) {
169
- throw new Error('No skills tool selected');
170
- }
171
-
172
- let scope = normalizeScope(options.scope);
173
- if (!scope) {
174
- scope = normalizeScope(await promptForScope());
175
- }
176
-
177
- if (!scope) {
178
- throw new Error('No skills scope selected');
179
- }
180
-
181
- return {
182
- tools,
183
- scope,
184
- projectDir: options.projectDir ? path.resolve(options.projectDir) : process.cwd(),
185
- force: Boolean(options.force),
186
- };
187
- }
188
-
189
- async function runSkillsInstall(name, rawOptions = {}) {
190
- const skillName = String(name || '').trim();
191
- if (!skillName) {
192
- throw new Error('Missing skill name');
193
- }
194
-
195
- const options = await resolveInstallOptions(rawOptions);
196
- const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ronds-ai-skill-'));
197
-
198
- try {
199
- const download = await downloadSkillArchive(skillName, tempRoot);
200
- const extractDir = path.join(tempRoot, 'extracted');
201
- extractArchive(download.archivePath, extractDir);
202
- const skillRoot = findSkillRoot(extractDir);
203
- const targets = buildInstallTargets(options.tools, options.scope, options.projectDir, skillName);
204
-
205
- for (const target of targets) {
206
- copySkill(skillRoot, target.destination, options.force);
207
- }
208
-
209
- return {
210
- name: skillName,
211
- scope: options.scope,
212
- projectDir: options.scope === 'project' ? options.projectDir : '',
213
- archiveUrl: download.url,
214
- installedTargets: targets.map((target) => ({
215
- tools: target.tools,
216
- destination: target.destination,
217
- })),
218
- };
219
- } finally {
220
- fs.rmSync(tempRoot, { recursive: true, force: true });
221
- }
222
- }
223
-
224
- module.exports = {
225
- runSkillsInstall,
226
- };
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const { execFileSync } = require('child_process');
7
+ const { buildInstallTargets, normalizeScope, normalizeTools } = require('./skills_targets');
8
+ const { promptForScope, promptForTools } = require('./skills_prompt');
9
+
10
+ const SKILL_DOWNLOAD_BASE_URL = 'https://aihub.ronds.com/api/api/v1/skills/download';
11
+ const MAX_REDIRECTS = 5;
12
+
13
+ function parseJson(raw, label) {
14
+ try {
15
+ return JSON.parse(raw);
16
+ } catch (error) {
17
+ const detail = error instanceof Error ? error.message : String(error);
18
+ throw new Error(`${label} is not valid JSON: ${detail}`);
19
+ }
20
+ }
21
+
22
+ function loadDownloadConfig() {
23
+ const token = String(process.env.SKILL_DOWNLOAD_TOKEN || '').trim();
24
+ const extraHeaders = process.env.SKILL_DOWNLOAD_HEADERS
25
+ ? parseJson(process.env.SKILL_DOWNLOAD_HEADERS, 'SKILL_DOWNLOAD_HEADERS')
26
+ : {};
27
+
28
+ const headers = {
29
+ ...extraHeaders,
30
+ };
31
+
32
+ if (token) {
33
+ headers.authorization = `Bearer ${token}`;
34
+ }
35
+
36
+ return { headers };
37
+ }
38
+
39
+ function copyDirectoryRecursive(sourceDir, destinationDir) {
40
+ const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
41
+ fs.mkdirSync(destinationDir, { recursive: true });
42
+
43
+ for (const entry of entries) {
44
+ const sourcePath = path.join(sourceDir, entry.name);
45
+ const destinationPath = path.join(destinationDir, entry.name);
46
+
47
+ if (entry.isDirectory()) {
48
+ copyDirectoryRecursive(sourcePath, destinationPath);
49
+ continue;
50
+ }
51
+
52
+ if (entry.isSymbolicLink()) {
53
+ const targetPath = fs.readlinkSync(sourcePath);
54
+ fs.symlinkSync(targetPath, destinationPath);
55
+ continue;
56
+ }
57
+
58
+ fs.copyFileSync(sourcePath, destinationPath);
59
+ }
60
+ }
61
+
62
+ function copyDirectory(sourceDir, destinationDir) {
63
+ if (typeof fs.cpSync === 'function') {
64
+ fs.cpSync(sourceDir, destinationDir, { recursive: true });
65
+ return;
66
+ }
67
+
68
+ copyDirectoryRecursive(sourceDir, destinationDir);
69
+ }
70
+
71
+ function streamToFile(url, destinationPath, config, redirectCount = 0) {
72
+ return new Promise((resolve, reject) => {
73
+ const transport = String(url).startsWith('http://') ? http : https;
74
+ const request = transport.get(url, { headers: config.headers }, (response) => {
75
+ if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
76
+ response.resume();
77
+ if (redirectCount >= MAX_REDIRECTS) {
78
+ reject(new Error('Too many redirects while downloading skill archive'));
79
+ return;
80
+ }
81
+
82
+ const nextUrl = new URL(response.headers.location, url).toString();
83
+ streamToFile(nextUrl, destinationPath, config, redirectCount + 1).then(resolve, reject);
84
+ return;
85
+ }
86
+
87
+ if (response.statusCode !== 200) {
88
+ response.resume();
89
+ reject(new Error(`Skill download failed with status ${response.statusCode || 0}`));
90
+ return;
91
+ }
92
+
93
+ const fileStream = fs.createWriteStream(destinationPath);
94
+ response.pipe(fileStream);
95
+
96
+ fileStream.on('finish', () => {
97
+ fileStream.close(() => resolve(destinationPath));
98
+ });
99
+
100
+ fileStream.on('error', (error) => {
101
+ fileStream.close(() => reject(error));
102
+ });
103
+ });
104
+
105
+ request.on('error', reject);
106
+ });
107
+ }
108
+
109
+ async function downloadSkillArchive(name, workingDir) {
110
+ const archivePath = path.join(workingDir, `${name}.zip`);
111
+ const url = `${SKILL_DOWNLOAD_BASE_URL}/${encodeURIComponent(name)}`;
112
+ await streamToFile(url, archivePath, loadDownloadConfig());
113
+ return {
114
+ url,
115
+ archivePath,
116
+ };
117
+ }
118
+
119
+ function extractArchive(archivePath, outputDir) {
120
+ fs.mkdirSync(outputDir, { recursive: true });
121
+ execFileSync('unzip', ['-q', archivePath, '-d', outputDir], {
122
+ stdio: ['ignore', 'pipe', 'pipe'],
123
+ });
124
+ }
125
+
126
+ function findSkillRoot(extractDir) {
127
+ const directSkillMd = path.join(extractDir, 'SKILL.md');
128
+ if (fs.existsSync(directSkillMd)) {
129
+ return extractDir;
130
+ }
131
+
132
+ const entries = fs.readdirSync(extractDir, { withFileTypes: true });
133
+ const candidateDirs = entries
134
+ .filter((entry) => entry.isDirectory())
135
+ .map((entry) => path.join(extractDir, entry.name))
136
+ .filter((entryPath) => fs.existsSync(path.join(entryPath, 'SKILL.md')));
137
+
138
+ if (candidateDirs.length === 1) {
139
+ return candidateDirs[0];
140
+ }
141
+
142
+ if (candidateDirs.length > 1) {
143
+ throw new Error('Downloaded archive contains multiple skill roots');
144
+ }
145
+
146
+ throw new Error('Downloaded archive does not contain SKILL.md');
147
+ }
148
+
149
+ function copySkill(sourceDir, destinationDir, force) {
150
+ if (fs.existsSync(destinationDir)) {
151
+ if (!force) {
152
+ throw new Error(`Skill already exists at ${destinationDir}. Use --force to overwrite.`);
153
+ }
154
+
155
+ fs.rmSync(destinationDir, { recursive: true, force: true });
156
+ }
157
+
158
+ fs.mkdirSync(path.dirname(destinationDir), { recursive: true });
159
+ copyDirectory(sourceDir, destinationDir);
160
+ }
161
+
162
+ async function resolveInstallOptions(options) {
163
+ let tools = normalizeTools(options.tool);
164
+ if (tools.length === 0) {
165
+ tools = normalizeTools(await promptForTools());
166
+ }
167
+
168
+ if (tools.length === 0) {
169
+ throw new Error('No skills tool selected');
170
+ }
171
+
172
+ let scope = normalizeScope(options.scope);
173
+ if (!scope) {
174
+ scope = normalizeScope(await promptForScope());
175
+ }
176
+
177
+ if (!scope) {
178
+ throw new Error('No skills scope selected');
179
+ }
180
+
181
+ return {
182
+ tools,
183
+ scope,
184
+ projectDir: options.projectDir ? path.resolve(options.projectDir) : process.cwd(),
185
+ force: Boolean(options.force),
186
+ };
187
+ }
188
+
189
+ async function runSkillsInstall(name, rawOptions = {}) {
190
+ const skillName = String(name || '').trim();
191
+ if (!skillName) {
192
+ throw new Error('Missing skill name');
193
+ }
194
+
195
+ const options = await resolveInstallOptions(rawOptions);
196
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ronds-ai-skill-'));
197
+
198
+ try {
199
+ const download = await downloadSkillArchive(skillName, tempRoot);
200
+ const extractDir = path.join(tempRoot, 'extracted');
201
+ extractArchive(download.archivePath, extractDir);
202
+ const skillRoot = findSkillRoot(extractDir);
203
+ const targets = buildInstallTargets(options.tools, options.scope, options.projectDir, skillName);
204
+
205
+ for (const target of targets) {
206
+ copySkill(skillRoot, target.destination, options.force);
207
+ }
208
+
209
+ return {
210
+ name: skillName,
211
+ scope: options.scope,
212
+ projectDir: options.scope === 'project' ? options.projectDir : '',
213
+ archiveUrl: download.url,
214
+ installedTargets: targets.map((target) => ({
215
+ tools: target.tools,
216
+ destination: target.destination,
217
+ })),
218
+ };
219
+ } finally {
220
+ fs.rmSync(tempRoot, { recursive: true, force: true });
221
+ }
222
+ }
223
+
224
+ module.exports = {
225
+ runSkillsInstall,
226
+ };