Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion installer/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "syncable-cli-skills",
"version": "0.1.3",
"version": "0.1.5",
"type": "module",
"description": "Install Syncable CLI skills for AI coding agents (Claude Code, Cursor, Windsurf, Codex, Gemini CLI)",
"license": "GPL-3.0",
Expand Down
41 changes: 26 additions & 15 deletions installer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,8 @@ program
if (syncCtlStatus.status === 'ok') {
console.log(chalk.green(` ✓ sync-ctl v${syncCtlStatus.version}`));
} else if (syncCtlStatus.status === 'outdated') {
console.log(chalk.yellow(` ⚠ sync-ctl v${syncCtlStatus.version} (outdated)`));
const latestInfo = syncCtlStatus.latestVersion ? ` → ${syncCtlStatus.latestVersion} available` : '';
console.log(chalk.yellow(` ⚠ sync-ctl v${syncCtlStatus.version} (outdated${latestInfo})`));
} else {
console.log(chalk.red(' ✗ sync-ctl not found'));
}
Expand All @@ -236,21 +237,30 @@ program
if (syncCtlStatus.status === 'missing' || syncCtlStatus.status === 'outdated') {
const cargoNow = await checkCargo();
if (cargoNow.status === 'ok') {
const message = syncCtlStatus.status === 'outdated'
? 'Update syncable-cli via cargo?'
: 'Install syncable-cli via cargo?';
const { installCli } = opts.yes
? { installCli: true }
: await inquirer.prompt([{ type: 'confirm', name: 'installCli', message, default: true }]);

if (installCli) {
const spinner = ora(' Running: cargo install syncable-cli').start();
const force = syncCtlStatus.status === 'outdated';
const success = await installSyncCtl(force);
if (syncCtlStatus.status === 'outdated') {
// Always auto-upgrade to latest — no prompt needed
const latestLabel = syncCtlStatus.latestVersion ? ` to v${syncCtlStatus.latestVersion}` : '';
const spinner = ora(` Upgrading sync-ctl${latestLabel}...`).start();
const success = await installSyncCtl(true); // force = true for upgrade
if (success) {
spinner.succeed(' sync-ctl installed');
spinner.succeed(` sync-ctl upgraded${latestLabel}`);
} else {
spinner.fail(' Failed to install sync-ctl. Try: cargo install syncable-cli');
spinner.fail(' Failed to upgrade sync-ctl. Try: cargo install syncable-cli --force');
}
} else {
// Missing — ask to install
const { installCli } = opts.yes
? { installCli: true }
: await inquirer.prompt([{ type: 'confirm', name: 'installCli', message: 'Install syncable-cli via cargo?', default: true }]);

if (installCli) {
const spinner = ora(' Running: cargo install syncable-cli').start();
const success = await installSyncCtl(false);
if (success) {
spinner.succeed(' sync-ctl installed');
} else {
spinner.fail(' Failed to install sync-ctl. Try: cargo install syncable-cli');
}
}
}
}
Expand Down Expand Up @@ -441,7 +451,8 @@ program
const projectOnlyFlag = opts.projectOnly ? ['--project-only'] : [];
const verboseFlag = opts.verbose ? ['--verbose'] : [];
await program.commands.find((c) => c.name() === 'uninstall')!.parseAsync(['node', 'x', ...agentsFlag, ...yesFlag]);
await program.commands.find((c) => c.name() === 'install')!.parseAsync(['node', 'x', '--skip-cli', ...agentsFlag, ...yesFlag, ...dryRunFlag, ...globalOnlyFlag, ...projectOnlyFlag, ...verboseFlag]);
// NOTE: Do NOT pass --skip-cli here — update must always check for and install the latest sync-ctl
await program.commands.find((c) => c.name() === 'install')!.parseAsync(['node', 'x', ...agentsFlag, ...yesFlag, ...dryRunFlag, ...globalOnlyFlag, ...projectOnlyFlag, ...verboseFlag]);
});

program
Expand Down
46 changes: 44 additions & 2 deletions installer/src/prerequisites/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import { execCommand, commandExists, parseVersion, compareVersions, cargoBinDir
import { MIN_SYNC_CTL_VERSION } from '../constants.js';
import fs from 'fs';
import path from 'path';
import https from 'https';

export interface PrereqStatus {
status: 'ok' | 'missing' | 'outdated';
version?: string;
latestVersion?: string;
}

export function checkNodeVersion(): PrereqStatus {
Expand All @@ -31,6 +33,34 @@ export async function checkCargo(): Promise<PrereqStatus> {
}
}

/**
* Fetch the latest syncable-cli version from crates.io.
* Returns null if the lookup fails (network error, timeout, etc.)
*/
export async function getLatestCratesVersion(): Promise<string | null> {
return new Promise((resolve) => {
const req = https.get(
'https://crates.io/api/v1/crates/syncable-cli',
{ headers: { 'User-Agent': 'syncable-cli-skills-installer' }, timeout: 5_000 },
(res) => {
let data = '';
res.on('data', (chunk: Buffer) => { data += chunk; });
res.on('end', () => {
try {
const json = JSON.parse(data);
const version = json?.crate?.max_version || json?.versions?.[0]?.num;
resolve(version || null);
} catch {
resolve(null);
}
});
},
);
req.on('error', () => resolve(null));
req.on('timeout', () => { req.destroy(); resolve(null); });
});
}

export async function checkSyncCtl(): Promise<PrereqStatus> {
try {
const { stdout } = await execCommand('sync-ctl --version');
Expand All @@ -39,12 +69,24 @@ export async function checkSyncCtl(): Promise<PrereqStatus> {
return { status: 'ok', version: stdout.trim() };
}

const currentStr = `${version.major}.${version.minor}.${version.patch}`;

// First check: is it below the hard minimum?
const minVersion = parseVersion(MIN_SYNC_CTL_VERSION);
if (minVersion && compareVersions(version, minVersion) < 0) {
return { status: 'outdated', version: `${version.major}.${version.minor}.${version.patch}` };
return { status: 'outdated', version: currentStr };
}

// Second check: is there a newer version on crates.io?
const latestStr = await getLatestCratesVersion();
if (latestStr) {
const latest = parseVersion(latestStr);
if (latest && compareVersions(version, latest) < 0) {
return { status: 'outdated', version: currentStr, latestVersion: latestStr };
}
}

return { status: 'ok', version: `${version.major}.${version.minor}.${version.patch}` };
return { status: 'ok', version: currentStr };
} catch {
return { status: 'missing' };
}
Expand Down
Loading