Skip to content

Commit a3f17ed

Browse files
authored
Merge pull request #318 from syncable-dev/develop
Develop
2 parents 7be5d5e + 1d3c09e commit a3f17ed

3 files changed

Lines changed: 71 additions & 18 deletions

File tree

installer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "syncable-cli-skills",
3-
"version": "0.1.3",
3+
"version": "0.1.5",
44
"type": "module",
55
"description": "Install Syncable CLI skills for AI coding agents (Claude Code, Cursor, Windsurf, Codex, Gemini CLI)",
66
"license": "GPL-3.0",

installer/src/index.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ program
210210
if (syncCtlStatus.status === 'ok') {
211211
console.log(chalk.green(` ✓ sync-ctl v${syncCtlStatus.version}`));
212212
} else if (syncCtlStatus.status === 'outdated') {
213-
console.log(chalk.yellow(` ⚠ sync-ctl v${syncCtlStatus.version} (outdated)`));
213+
const latestInfo = syncCtlStatus.latestVersion ? ` → ${syncCtlStatus.latestVersion} available` : '';
214+
console.log(chalk.yellow(` ⚠ sync-ctl v${syncCtlStatus.version} (outdated${latestInfo})`));
214215
} else {
215216
console.log(chalk.red(' ✗ sync-ctl not found'));
216217
}
@@ -236,21 +237,30 @@ program
236237
if (syncCtlStatus.status === 'missing' || syncCtlStatus.status === 'outdated') {
237238
const cargoNow = await checkCargo();
238239
if (cargoNow.status === 'ok') {
239-
const message = syncCtlStatus.status === 'outdated'
240-
? 'Update syncable-cli via cargo?'
241-
: 'Install syncable-cli via cargo?';
242-
const { installCli } = opts.yes
243-
? { installCli: true }
244-
: await inquirer.prompt([{ type: 'confirm', name: 'installCli', message, default: true }]);
245-
246-
if (installCli) {
247-
const spinner = ora(' Running: cargo install syncable-cli').start();
248-
const force = syncCtlStatus.status === 'outdated';
249-
const success = await installSyncCtl(force);
240+
if (syncCtlStatus.status === 'outdated') {
241+
// Always auto-upgrade to latest — no prompt needed
242+
const latestLabel = syncCtlStatus.latestVersion ? ` to v${syncCtlStatus.latestVersion}` : '';
243+
const spinner = ora(` Upgrading sync-ctl${latestLabel}...`).start();
244+
const success = await installSyncCtl(true); // force = true for upgrade
250245
if (success) {
251-
spinner.succeed(' sync-ctl installed');
246+
spinner.succeed(` sync-ctl upgraded${latestLabel}`);
252247
} else {
253-
spinner.fail(' Failed to install sync-ctl. Try: cargo install syncable-cli');
248+
spinner.fail(' Failed to upgrade sync-ctl. Try: cargo install syncable-cli --force');
249+
}
250+
} else {
251+
// Missing — ask to install
252+
const { installCli } = opts.yes
253+
? { installCli: true }
254+
: await inquirer.prompt([{ type: 'confirm', name: 'installCli', message: 'Install syncable-cli via cargo?', default: true }]);
255+
256+
if (installCli) {
257+
const spinner = ora(' Running: cargo install syncable-cli').start();
258+
const success = await installSyncCtl(false);
259+
if (success) {
260+
spinner.succeed(' sync-ctl installed');
261+
} else {
262+
spinner.fail(' Failed to install sync-ctl. Try: cargo install syncable-cli');
263+
}
254264
}
255265
}
256266
}
@@ -441,7 +451,8 @@ program
441451
const projectOnlyFlag = opts.projectOnly ? ['--project-only'] : [];
442452
const verboseFlag = opts.verbose ? ['--verbose'] : [];
443453
await program.commands.find((c) => c.name() === 'uninstall')!.parseAsync(['node', 'x', ...agentsFlag, ...yesFlag]);
444-
await program.commands.find((c) => c.name() === 'install')!.parseAsync(['node', 'x', '--skip-cli', ...agentsFlag, ...yesFlag, ...dryRunFlag, ...globalOnlyFlag, ...projectOnlyFlag, ...verboseFlag]);
454+
// NOTE: Do NOT pass --skip-cli here — update must always check for and install the latest sync-ctl
455+
await program.commands.find((c) => c.name() === 'install')!.parseAsync(['node', 'x', ...agentsFlag, ...yesFlag, ...dryRunFlag, ...globalOnlyFlag, ...projectOnlyFlag, ...verboseFlag]);
445456
});
446457

447458
program

installer/src/prerequisites/check.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ import { execCommand, commandExists, parseVersion, compareVersions, cargoBinDir
22
import { MIN_SYNC_CTL_VERSION } from '../constants.js';
33
import fs from 'fs';
44
import path from 'path';
5+
import https from 'https';
56

67
export interface PrereqStatus {
78
status: 'ok' | 'missing' | 'outdated';
89
version?: string;
10+
latestVersion?: string;
911
}
1012

1113
export function checkNodeVersion(): PrereqStatus {
@@ -31,6 +33,34 @@ export async function checkCargo(): Promise<PrereqStatus> {
3133
}
3234
}
3335

36+
/**
37+
* Fetch the latest syncable-cli version from crates.io.
38+
* Returns null if the lookup fails (network error, timeout, etc.)
39+
*/
40+
export async function getLatestCratesVersion(): Promise<string | null> {
41+
return new Promise((resolve) => {
42+
const req = https.get(
43+
'https://crates.io/api/v1/crates/syncable-cli',
44+
{ headers: { 'User-Agent': 'syncable-cli-skills-installer' }, timeout: 5_000 },
45+
(res) => {
46+
let data = '';
47+
res.on('data', (chunk: Buffer) => { data += chunk; });
48+
res.on('end', () => {
49+
try {
50+
const json = JSON.parse(data);
51+
const version = json?.crate?.max_version || json?.versions?.[0]?.num;
52+
resolve(version || null);
53+
} catch {
54+
resolve(null);
55+
}
56+
});
57+
},
58+
);
59+
req.on('error', () => resolve(null));
60+
req.on('timeout', () => { req.destroy(); resolve(null); });
61+
});
62+
}
63+
3464
export async function checkSyncCtl(): Promise<PrereqStatus> {
3565
try {
3666
const { stdout } = await execCommand('sync-ctl --version');
@@ -39,12 +69,24 @@ export async function checkSyncCtl(): Promise<PrereqStatus> {
3969
return { status: 'ok', version: stdout.trim() };
4070
}
4171

72+
const currentStr = `${version.major}.${version.minor}.${version.patch}`;
73+
74+
// First check: is it below the hard minimum?
4275
const minVersion = parseVersion(MIN_SYNC_CTL_VERSION);
4376
if (minVersion && compareVersions(version, minVersion) < 0) {
44-
return { status: 'outdated', version: `${version.major}.${version.minor}.${version.patch}` };
77+
return { status: 'outdated', version: currentStr };
78+
}
79+
80+
// Second check: is there a newer version on crates.io?
81+
const latestStr = await getLatestCratesVersion();
82+
if (latestStr) {
83+
const latest = parseVersion(latestStr);
84+
if (latest && compareVersions(version, latest) < 0) {
85+
return { status: 'outdated', version: currentStr, latestVersion: latestStr };
86+
}
4587
}
4688

47-
return { status: 'ok', version: `${version.major}.${version.minor}.${version.patch}` };
89+
return { status: 'ok', version: currentStr };
4890
} catch {
4991
return { status: 'missing' };
5092
}

0 commit comments

Comments
 (0)