Skip to content

Commit 55a5a30

Browse files
committed
chore: address valid robustness issues in last Copilot review
1 parent 27027e5 commit 55a5a30

File tree

3 files changed

+85
-19
lines changed

3 files changed

+85
-19
lines changed

.claude/skills/src/update-checker.ts

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,11 @@ export class UpdateChecker {
7979
return;
8080
}
8181

82-
// Parse version from output like "qsv 0.132.0"
83-
const match = output.match(/qsv\s+(\d+\.\d+\.\d+)/);
82+
// Parse version from output like "qsv 0.132.0" or "qsv 0.132.0-beta"
83+
// Handle multiple spaces, extra text, and pre-release tags
84+
const match = output.match(/qsv\s+(\d+\.\d+\.\d+)(?:-[\w.]+)?/);
8485
if (match) {
86+
// Only use the main version number (ignore pre-release tags for now)
8587
resolve(match[1]);
8688
} else {
8789
reject(new Error(`Could not parse qsv version from: ${output}`));
@@ -98,19 +100,33 @@ export class UpdateChecker {
98100
* Get version that skills were generated with (from skill JSON files)
99101
*/
100102
getSkillsVersion(): string {
101-
try {
102-
// Read version from any skill file (they all have the same version)
103-
const sampleSkillPath = join(this.skillsDir, 'qsv-stats.json');
104-
if (!existsSync(sampleSkillPath)) {
105-
return 'unknown';
106-
}
103+
// Try multiple skill files as fallbacks for resilience
104+
const skillFilesToTry = [
105+
'qsv-stats.json',
106+
'qsv-select.json',
107+
'qsv-count.json',
108+
'qsv-search.json'
109+
];
110+
111+
for (const skillFile of skillFilesToTry) {
112+
try {
113+
const skillPath = join(this.skillsDir, skillFile);
114+
if (!existsSync(skillPath)) {
115+
continue;
116+
}
107117

108-
const skill = JSON.parse(readFileSync(sampleSkillPath, 'utf-8'));
109-
return skill.version || 'unknown';
110-
} catch (error) {
111-
console.error('[UpdateChecker] Failed to read skills version:', error);
112-
return 'unknown';
118+
const skill = JSON.parse(readFileSync(skillPath, 'utf-8'));
119+
if (skill.version) {
120+
return skill.version;
121+
}
122+
} catch (error) {
123+
// Try next file
124+
continue;
125+
}
113126
}
127+
128+
console.warn('[UpdateChecker] Could not determine skills version from any skill file');
129+
return 'unknown';
114130
}
115131

116132
/**
@@ -153,6 +169,11 @@ export class UpdateChecker {
153169
writeFileSync(this.versionFilePath, JSON.stringify(info, null, 2), 'utf-8');
154170
} catch (error) {
155171
console.error('[UpdateChecker] Failed to save version info:', error);
172+
console.warn(
173+
'[UpdateChecker] WARNING: Version info could not be persisted at',
174+
this.versionFilePath,
175+
'- update checks may be repeated or version tracking may be inaccurate.'
176+
);
156177
}
157178
}
158179

@@ -177,9 +198,19 @@ export class UpdateChecker {
177198
return null;
178199
}
179200

180-
const data = await response.json() as { tag_name: string };
201+
const data: unknown = await response.json();
202+
if (
203+
!data ||
204+
typeof data !== 'object' ||
205+
data === null ||
206+
typeof (data as { tag_name?: unknown }).tag_name !== 'string'
207+
) {
208+
console.error('[UpdateChecker] GitHub API response missing valid tag_name field');
209+
return null;
210+
}
211+
const tagName = (data as { tag_name: string }).tag_name;
181212
// Tag format is typically "v0.132.0" or "0.132.0"
182-
const version = data.tag_name.replace(/^v/, '');
213+
const version = tagName.replace(/^v/, '');
183214
return version;
184215
} catch (error) {
185216
console.error('[UpdateChecker] Failed to check GitHub releases:', error);
@@ -189,11 +220,20 @@ export class UpdateChecker {
189220

190221
/**
191222
* Compare semantic versions (simple implementation)
223+
* Handles only numeric versions like "1.2.3" (pre-release tags are ignored)
192224
*/
193225
private compareVersions(v1: string, v2: string): number {
194226
const parts1 = v1.split('.').map(Number);
195227
const parts2 = v2.split('.').map(Number);
196228

229+
// Validate that all parts are valid numbers
230+
if (parts1.some(isNaN) || parts2.some(isNaN)) {
231+
console.warn(
232+
`[UpdateChecker] Invalid version format: "${v1}" or "${v2}" - comparison may be incorrect`
233+
);
234+
return 0; // Treat as equal if we can't compare
235+
}
236+
197237
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
198238
const part1 = parts1[i] || 0;
199239
const part2 = parts2[i] || 0;

src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,13 @@ fn main() -> QsvExitCode {
268268
#[cfg(feature = "mcp")]
269269
{
270270
if args.flag_update_mcp_skills {
271-
util::log_end(qsv_args, now);
272271
match mcp_skills_gen::generate_mcp_skills() {
273-
Ok(()) => return QsvExitCode::Good,
272+
Ok(()) => {
273+
util::log_end(qsv_args, now);
274+
return QsvExitCode::Good;
275+
},
274276
Err(e) => {
277+
util::log_end(qsv_args, now);
275278
werr!("MCP skills generation error: {e}");
276279
return QsvExitCode::Bad;
277280
},

src/mcp_skills_gen.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -601,21 +601,44 @@ pub fn generate_mcp_skills() -> CliResult<()> {
601601
// Determine repository root - look for Cargo.toml with src/cmd
602602
// This command must be run from within the qsv repository directory
603603
let mut repo_root = std::env::current_dir()?;
604+
let original_dir = repo_root.clone();
605+
606+
let mut iterations = 0;
607+
const MAX_ITERATIONS: usize = 100; // Prevent infinite loops
608+
604609
loop {
605610
if repo_root.join("Cargo.toml").exists() && repo_root.join("src/cmd").exists() {
606611
break;
607612
}
613+
614+
iterations += 1;
615+
if iterations >= MAX_ITERATIONS {
616+
return fail_clierror!(
617+
"Could not find qsv repository root after checking {} parent directories. \
618+
This command must be run from within the qsv repository directory \
619+
(where Cargo.toml and src/cmd exist).\n\
620+
Original directory: {}\n\
621+
\n\
622+
If you're using a package-installed qsv binary, you need to:\n\
623+
1. Clone the qsv repository: git clone https://github.com/dathere/qsv.git\n\
624+
2. cd into the repository: cd qsv\n\
625+
3. Run: qsv --update-mcp-skills",
626+
MAX_ITERATIONS,
627+
original_dir.display()
628+
);
629+
}
630+
608631
if !repo_root.pop() {
609632
return fail_clierror!(
610633
"Could not find qsv repository root. This command must be run from within \
611634
the qsv repository directory (where Cargo.toml and src/cmd exist).\n\
612-
Current directory: {}\n\
635+
Original directory: {}\n\
613636
\n\
614637
If you're using a package-installed qsv binary, you need to:\n\
615638
1. Clone the qsv repository: git clone https://github.com/dathere/qsv.git\n\
616639
2. cd into the repository: cd qsv\n\
617640
3. Run: qsv --update-mcp-skills",
618-
std::env::current_dir()?.display()
641+
original_dir.display()
619642
);
620643
}
621644
}

0 commit comments

Comments
 (0)