Skip to content

Instantly share code, notes, and snippets.

@ArixAR
Last active October 30, 2025 19:02
Show Gist options
  • Select an option

  • Save ArixAR/1e4c9672a8c95e398d369efe5ff54807 to your computer and use it in GitHub Desktop.

Select an option

Save ArixAR/1e4c9672a8c95e398d369efe5ff54807 to your computer and use it in GitHub Desktop.
Sync Hollow Knight & Silksong saves between PC and Switch

Hollow Knight & Silksong Save Sync Tool

⚠️ Notice: This CLI version is no longer actively maintained. For the best experience, please use the GUI version available at: https://github.com/ArixAR/hollow-sync

A cross-platform save synchronization tool for both Hollow Knight and Hollow Knight: Silksong that lets you transfer your progress between PC and Nintendo Switch.

Never used Node.js before? Click here for step-by-step guide

Beginner Guide

Step 1: Install Node.js

  1. Go to nodejs.org
  2. Click the green "Windows Installer (.msi)" button
  3. Run the downloaded installer and keep clicking "Next" until it's done
  4. Restart your computer after installation

Step 2: Download the script

  1. Go to the Hollow Sync Script
  2. Click the "Raw" button in the top-right corner of the script
  3. Right-click the page and select "Save As..."
  4. Save it as hollow-sync.js on your Desktop (make sure it ends with .js)

Step 3: Install dependencies

  1. Press Windows key + R, type cmd, press Enter
  2. Type: cd Desktop and press Enter
  3. Type: npm install aes-js and press Enter
  4. Wait for the installation to complete

Step 4: Run the tool

  1. Type: node hollow-sync.js auto-setup and press Enter
  2. Follow the tool's instructions to set up your saves

Troubleshooting:

  • If you get "'node' is not recognized", restart your computer after installing Node.js
  • If the script won't save as .js, select "All Files" in the file type dropdown when saving
  • Make sure you've played Hollow Knight or Silksong at least once before running the tool

Important

This tool requires homebrew on your Switch. Always backup your saves before using. Use at your own risk.

Requirements

Installation

  1. Download the Hollow Sync Script and save as hollow-sync.js
  2. Open terminal/command prompt in the same folder
  3. Run npm install aes-js

How to Use

Step 1: Setup

node hollow-sync.js auto-setup

The tool will:

  1. Scan for both Hollow Knight and Silksong saves
  2. Show you all found saves grouped by game
  3. Ask you to choose which game to set up: Choose game (1-2):

Step 2: Sync saves

node hollow-sync.js sync

Step 3: Transfer to Switch

  1. Put your Switch SD card in your PC
  2. Copy the generated backup folder to the appropriate JKSV folder:
    • Hollow Knight: /JKSV/Hollow Knight/
    • Silksong: /JKSV/Hollow Knight Silksong/
  3. Use JKSV on Switch to restore the backup

Alternative: Automatic Cloud Sync

For a more automated approach, you can set up JKSV to sync directly with cloud storage instead of manually transferring files via SD card.

Setup: Configure automatic cloud backups using either:

Once cloud sync is configured:

# Point the tool to your cloud folder instead of local files
node hollow-sync.js setup 1 /path/to/your/cloud/folder/backup-folder
# (Will prompt you to choose game)

# Sync normally - the tool will handle cloud files
node hollow-sync.js sync
# (Will prompt you to choose game)

How it works:

  1. JKSV automatically backs up your Switch saves to cloud storage
  2. Configure this tool to use your cloud folder path
  3. Sync between PC and cloud stored Switch saves
  4. Changes automatically sync across both platforms

Benefits: No more SD card removal, automatic backups, and seamless syncing once configured.

Note: You still need this tool for format conversion since PC and Switch use different save file formats.

Other Commands

# List all available save files
node hollow-sync.js list

# Setup specific save slot
node hollow-sync.js setup 1

# Check status
node hollow-sync.js status

# Force sync direction
node hollow-sync.js sync --pc      # PC to Switch
node hollow-sync.js sync --switch  # Switch to PC

# Convert files directly
node hollow-sync.js to-switch user1.dat
node hollow-sync.js to-pc save.json

# Create and restore JKSV backups
node hollow-sync.js backup user1.dat my-backup-folder
node hollow-sync.js restore my-backup-folder extracted-saves

# Skip prompts with --game flag
node hollow-sync.js setup 1 --game hk
node hollow-sync.js sync --game silksong
node hollow-sync.js status --game hk

Save Locations

PC:

  • Hollow Knight: C:\Users\[Username]\AppData\LocalLow\Team Cherry\Hollow Knight\
  • Silksong: C:\Users\[Username]\AppData\LocalLow\Team Cherry\Hollow Knight Silksong\[UserID]

Switch (JKSV folders):

  • Hollow Knight: /JKSV/Hollow Knight/
  • Silksong: /JKSV/Hollow Knight Silksong/

Troubleshooting

Can't find saves? Make sure you've played Hollow Knight or Silksong and saved at least once.

Game selection not working? You can always use --game hk or --game silksong to skip the prompt.

Sync conflict? Use --pc or --switch flags to choose which save to keep.

Switch issues? Check that JKSV is installed and you're using the exact SD card path.

Advanced: Switch 1 → Switch 2 Save Migration

If you want to move your Hollow Knight or Silksong saves directly from Switch 1 to Switch 2, this requires additional steps beyond the normal PC ↔ Switch sync.

Warning: This process is advanced and carries risk. Only attempt if you fully understand the implications.

See the full guide here: Switch Save Transfer Guide

#!/usr/bin/env node
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
const { TextEncoder, TextDecoder } = require('util');
const colors = {
reset: '\x1b[0m',
bright: '\x1b[1m',
dim: '\x1b[2m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
gray: '\x1b[90m'
};
const log = {
info: msg => console.log(`${colors.blue}[INFO]${colors.reset} ${msg}`),
success: msg => console.log(`${colors.green}[SUCCESS]${colors.reset} ${msg}`),
warn: msg => console.log(`${colors.yellow}[WARNING]${colors.reset} ${msg}`),
error: msg => console.log(`${colors.red}[ERROR]${colors.reset} ${msg}`),
status: msg => console.log(`${colors.cyan}[STATUS]${colors.reset} ${msg}`),
progress: msg => console.log(`${colors.magenta}[PROGRESS]${colors.reset} ${msg}`),
section: title => console.log(`\n${colors.bright}${colors.cyan}=== ${title.toUpperCase()} ===${colors.reset}`),
line: () => console.log(`${colors.gray}----------------------------------------${colors.reset}`),
path: msg => console.log(`${colors.dim} ${msg}${colors.reset}`),
highlight: msg => console.log(`${colors.bright}${colors.white}${msg}${colors.reset}`)
};
// thx @bloodorca <https://github.com/bloodorca/hollow/blob/master/src/functions.js#L4-L105>
const aes = require('aes-js');
const ENCRYPTION_KEY = 'UKu52ePUBwetZ9wNX88o54dnfKRu0T1l';
const SAVE_HEADER = [0, 1, 0, 0, 0, 255, 255, 255, 255, 1, 0, 0, 0, 0, 0, 0, 0, 6, 1, 0, 0, 0];
const gameKey = new TextEncoder().encode(ENCRYPTION_KEY);
const cipher = new aes.ModeOfOperation.ecb(gameKey);
function encrypt(data) {
const padding = 16 - data.length % 16;
const padded = new Uint8Array(data.length + padding);
padded.fill(padding);
padded.set(data);
return cipher.encrypt(padded);
}
function decrypt(data) {
const decrypted = cipher.decrypt(data);
const padding = decrypted[decrypted.length - 1];
return decrypted.subarray(0, -padding);
}
function createLengthPrefix(length) {
const bytes = [];
while (length >= 0x80) {
bytes.push((length & 0x7F) | 0x80);
length >>>= 7;
}
bytes.push(length & 0x7F);
return bytes;
}
function addSaveHeader(data) {
const lengthBytes = createLengthPrefix(data.length);
const result = new Uint8Array(SAVE_HEADER.length + lengthBytes.length + data.length + 1);
result.set(SAVE_HEADER);
result.set(lengthBytes, SAVE_HEADER.length);
result.set(data, SAVE_HEADER.length + lengthBytes.length);
result[result.length - 1] = 11;
return result;
}
function removeSaveHeader(data) {
let headerEnd = SAVE_HEADER.length;
// skip variable length prefix
for (let i = 0; i < 5; i++) {
headerEnd++;
if ((data[SAVE_HEADER.length + i] & 0x80) === 0) break;
}
return data.subarray(headerEnd, data.length - 1);
}
// save format conversion
function pcToSwitch(pcSaveData) {
const withoutHeader = removeSaveHeader(pcSaveData);
const base64Data = new TextDecoder().decode(withoutHeader);
const encryptedData = new Uint8Array(Buffer.from(base64Data, 'base64'));
const jsonData = decrypt(encryptedData);
return new TextDecoder().decode(jsonData);
}
function switchToPC(jsonString) {
const jsonData = new TextEncoder().encode(jsonString);
const encryptedData = encrypt(jsonData);
const base64String = Buffer.from(encryptedData).toString('base64');
const base64Data = new TextEncoder().encode(base64String);
return addSaveHeader(base64Data);
}
// JKSV backup handling
async function createBackup(saveFile, outputDir) {
await fs.mkdir(outputDir, { recursive: true });
const saveData = await fs.readFile(saveFile);
const outputPath = path.join(outputDir, path.basename(saveFile));
await fs.writeFile(outputPath, saveData);
}
async function extractBackup(backupDir, outputDir) {
await fs.mkdir(outputDir, { recursive: true });
const extractedFiles = [];
const files = await fs.readdir(backupDir);
for (const fileName of files) {
if (fileName.includes('user') || fileName.endsWith('.dat')) {
const sourcePath = path.join(backupDir, fileName);
const outputPath = path.join(outputDir, fileName);
await fs.copyFile(sourcePath, outputPath);
extractedFiles.push(outputPath);
}
}
return extractedFiles;
}
// game definitions
const GAMES = {
hk: {
name: 'Hollow Knight',
displayName: 'Hollow Knight',
path: 'Hollow Knight',
jksvPath: 'Hollow Knight',
configSuffix: 'hk'
},
silksong: {
name: 'Silksong',
displayName: 'Silksong',
path: 'Hollow Knight Silksong',
jksvPath: 'Hollow Knight Silksong',
configSuffix: 'silksong'
}
};
// save file detection
function getGamePaths(gameKey = null) {
const allPaths = [];
const gamesToCheck = gameKey ? [gameKey] : Object.keys(GAMES);
for (const game of gamesToCheck) {
const gamePaths = [];
// first try: os.homedir() (most reliable)
try {
const homeDir = os.homedir();
if (homeDir && homeDir !== '/') {
gamePaths.push(path.join(homeDir, 'AppData', 'LocalLow', 'Team Cherry', GAMES[game].path));
}
} catch { }
// second try: os.userInfo() (if available)
try {
const userInfo = os.userInfo();
if (userInfo && userInfo.username) {
gamePaths.push(path.join('C:', 'Users', userInfo.username, 'AppData', 'LocalLow', 'Team Cherry', GAMES[game].path));
}
} catch { }
// last try: environment variables (fallback)
const envUsername = process.env.USERNAME || process.env.USER;
if (envUsername) {
gamePaths.push(path.join('C:', 'Users', envUsername, 'AppData', 'LocalLow', 'Team Cherry', GAMES[game].path));
}
// deduplicate paths for this game and add to result
const uniquePaths = [...new Set(gamePaths)];
uniquePaths.forEach(gamePath => {
allPaths.push({
game,
path: gamePath
});
});
}
return allPaths;
}
async function scanForSaves(scanPath, results = []) {
try {
const files = await fs.readdir(scanPath);
const saveFiles = files.filter(file => file.match(/^user[1-4]\.dat$/));
for (const file of saveFiles) {
const filePath = path.join(scanPath, file);
const stats = await fs.stat(filePath);
const slot = parseInt(file.replace('user', '').replace('.dat', ''));
results.push({
slot,
file,
path: filePath,
modified: stats.mtime,
size: stats.size,
directory: scanPath
});
}
} catch { }
}
async function findSaveFiles(basePath, gameKey) {
const results = [];
// check base directory
await scanForSaves(basePath, results);
// check numbered subdirectories (user IDs)
try {
const items = await fs.readdir(basePath, { withFileTypes: true });
for (const item of items) {
if (item.isDirectory() && item.name.match(/^\d+$/)) {
const subPath = path.join(basePath, item.name);
await scanForSaves(subPath, results);
}
}
} catch { }
// add game info to each result
results.forEach(save => {
save.game = gameKey;
save.gameName = GAMES[gameKey].name;
save.gameDisplayName = GAMES[gameKey].displayName;
});
return results.sort((a, b) => b.modified - a.modified);
}
async function detectAllSaves(gameFilter = null) {
const searchPaths = getGamePaths(gameFilter);
const allSaves = [];
const seenPaths = new Set();
for (const {game, path: searchPath} of searchPaths) {
try {
await fs.access(searchPath);
const saves = await findSaveFiles(searchPath, game);
saves.forEach(save => {
// deduplicate by absolute path
const normalizedPath = path.resolve(save.path).toLowerCase();
if (!seenPaths.has(normalizedPath)) {
seenPaths.add(normalizedPath);
save.basePath = searchPath;
allSaves.push(save);
}
});
} catch { }
}
return allSaves.sort((a, b) => b.modified - a.modified);
}
class SaveManager {
constructor(gameKey = 'silksong') {
this.gameKey = gameKey;
this.game = GAMES[gameKey];
this.configFile = `${gameKey}-sync.json`;
this.config = null;
}
async loadConfig() {
try {
const data = await fs.readFile(this.configFile, 'utf8');
this.config = JSON.parse(data);
} catch {
this.config = {
pcSave: '',
switchSave: '',
lastSync: null
};
}
}
async saveConfig() {
await fs.writeFile(this.configFile, JSON.stringify(this.config, null, 2));
}
async getFileInfo(filePath) {
try {
const stats = await fs.stat(filePath);
return { exists: true, modified: stats.mtime };
} catch {
return { exists: false, modified: null };
}
}
async isJKSVFormat(filePath) {
try {
const stat = await fs.stat(filePath);
return stat.isDirectory();
} catch {
return false;
}
}
async autoSetup() {
log.progress(`Scanning for ${this.game.displayName} save files...`);
const saves = await detectAllSaves(this.gameKey);
if (saves.length === 0) {
log.error(`No ${this.game.displayName} save files found`);
log.info(`Make sure you have played ${this.game.displayName} and saved at least once`);
return;
}
log.success(`Found ${saves.length} save file(s)`);
log.line();
saves.forEach((save, index) => {
const recent = index === 0 ? colors.green + ' (most recent)' + colors.reset : '';
log.info(`${save.gameDisplayName} - Slot ${save.slot}${recent}`);
log.path(`Modified: ${save.modified.toLocaleString()}`);
log.path(`Path: ${save.path}`);
const saveDir = path.basename(save.directory);
const baseDir = path.basename(save.basePath);
if (saveDir !== baseDir) {
log.path(`Directory: ${saveDir}`);
}
});
const selectedSave = saves[0];
const outputName = `${this.gameKey}-slot${selectedSave.slot}-backup`;
const success = await this.configure(selectedSave.path, outputName);
if (success !== false) {
log.success(`Configured sync for ${selectedSave.gameDisplayName} slot ${selectedSave.slot}`);
}
}
async setupSlot(slot, outputPath = null) {
const slotNum = parseInt(slot);
if (slotNum < 1 || slotNum > 4) {
log.error('Save slot must be between 1 and 4');
return;
}
log.progress(`Looking for ${this.game.displayName} save slot ${slotNum}...`);
const saves = await detectAllSaves(this.gameKey);
const slotSaves = saves.filter(save => save.slot === slotNum);
if (slotSaves.length === 0) {
log.error(`No ${this.game.displayName} save found for slot ${slotNum}`);
log.info('Available slots:');
saves.forEach(save => {
log.info(` Slot ${save.slot}: ${save.gameDisplayName}`);
});
return;
}
if (slotSaves.length > 1) {
log.info(`Multiple saves found for slot ${slotNum}:`);
slotSaves.forEach((save, index) => {
log.info(` ${index + 1}. ${save.gameDisplayName}`);
});
log.info('Using most recent save');
}
const selectedSave = slotSaves[0];
const defaultOutput = outputPath || `${this.gameKey}-slot${slotNum}-backup`;
const success = await this.configure(selectedSave.path, defaultOutput);
if (success !== false) {
log.success(`Configured sync for ${selectedSave.gameDisplayName} slot ${slotNum}`);
}
}
async configure(pcPath, switchPath) {
await this.loadConfig();
const switchExt = path.extname(switchPath).toLowerCase();
let isValidSwitchPath = false;
try {
const stats = await fs.stat(switchPath);
if (stats.isDirectory()) {
isValidSwitchPath = true;
} else if (switchExt === '.json') {
isValidSwitchPath = true;
}
} catch {
if (switchExt === '.json' || !switchExt) {
isValidSwitchPath = true;
}
}
if (!isValidSwitchPath) {
log.error('Switch save path must be a folder (JKSV) or .json file');
log.info('Examples:');
log.info(' silksong-slot1-backup (JKSV backup folder - recommended)');
log.info(' silksong-slot1.json (Switch JSON format)');
return false;
}
this.config.pcSave = path.resolve(pcPath);
this.config.switchSave = path.resolve(switchPath);
await this.saveConfig();
log.success(`${this.game.displayName} save sync configured`);
log.path(`PC save: ${this.config.pcSave}`);
log.path(`Switch save: ${this.config.switchSave}`);
}
async sync(forceDirection = null) {
await this.loadConfig();
if (!this.config.pcSave || !this.config.switchSave) {
log.error(`${this.game.displayName} save sync not configured`);
log.info('Run "auto-setup" or "setup" command first');
return;
}
const pcInfo = await this.getFileInfo(this.config.pcSave);
const switchInfo = await this.getFileInfo(this.config.switchSave);
if (!pcInfo.exists && !switchInfo.exists) {
log.error('No save files found at configured paths');
return;
}
let direction = forceDirection;
if (!direction) {
if (!pcInfo.exists) {
direction = 'switch-to-pc';
} else if (!switchInfo.exists) {
direction = 'pc-to-switch';
} else {
direction = pcInfo.modified > switchInfo.modified ? 'pc-to-switch' : 'switch-to-pc';
if (this.config.lastSync) {
const lastSync = new Date(this.config.lastSync);
const pcNewer = pcInfo.modified > lastSync;
const switchNewer = switchInfo.modified > lastSync;
if (pcNewer && switchNewer) {
log.warn('Conflict: Both saves modified since last sync');
log.info(`PC save: ${pcInfo.modified.toLocaleString()}`);
log.info(`Switch save: ${switchInfo.modified.toLocaleString()}`);
log.line();
log.info('Choose which save to keep:');
log.info(' Use "sync --pc" to keep PC save');
log.info(' Use "sync --switch" to keep Switch save');
return;
}
}
}
}
if (direction === 'pc-to-switch') {
await this.syncPCToSwitch();
} else {
await this.syncSwitchToPC();
}
this.config.lastSync = new Date().toISOString();
await this.saveConfig();
log.success('Sync completed');
}
async syncPCToSwitch() {
log.progress('Syncing PC save to Switch format');
if (await this.isJKSVFormat(this.config.switchSave)) {
await createBackup(this.config.pcSave, this.config.switchSave);
log.success('JKSV backup created');
log.line();
log.highlight('NEXT STEPS:');
log.info('1. Insert your Switch SD card into your computer');
log.info(`2. Navigate to: /JKSV/${this.game.jksvPath}/`);
log.info(`3. Copy the folder: ${path.basename(this.config.switchSave)}`);
log.info('4. Safely eject SD card and insert back into Switch');
log.info('5. Use JKSV on Switch to restore the backup');
} else {
const pcData = await fs.readFile(this.config.pcSave);
const switchData = pcToSwitch(new Uint8Array(pcData));
await fs.mkdir(path.dirname(this.config.switchSave), { recursive: true });
await fs.writeFile(this.config.switchSave, switchData, 'utf8');
log.success('PC save converted to Switch format');
}
}
async syncSwitchToPC() {
log.progress('Syncing Switch save to PC format');
if (await this.isJKSVFormat(this.config.switchSave)) {
const tempDir = path.join(path.dirname(this.config.pcSave), 'temp-extract');
await fs.mkdir(tempDir, { recursive: true });
const extracted = await extractBackup(this.config.switchSave, tempDir);
if (extracted.length === 0) {
throw new Error('No save files found in backup');
}
await fs.copyFile(extracted[0], this.config.pcSave);
await fs.rm(tempDir, { recursive: true });
log.success('Save extracted from JKSV backup');
} else {
const switchData = await fs.readFile(this.config.switchSave, 'utf8');
const pcData = switchToPC(switchData);
await fs.mkdir(path.dirname(this.config.pcSave), { recursive: true });
await fs.writeFile(this.config.pcSave, pcData);
log.success('Switch save converted to PC format');
}
}
async showStatus() {
await this.loadConfig();
if (!this.config.pcSave) {
log.error(`${this.game.displayName} save sync not configured`);
log.info('Run "auto-setup" command first');
return;
}
log.section(`${this.game.displayName} Save Sync Status`);
const pcInfo = await this.getFileInfo(this.config.pcSave);
const switchInfo = await this.getFileInfo(this.config.switchSave);
const pcStatus = pcInfo.exists ? colors.green + 'EXISTS' + colors.reset : colors.red + 'NOT FOUND' + colors.reset;
const switchStatus = switchInfo.exists ? colors.green + 'EXISTS' + colors.reset : colors.red + 'NOT FOUND' + colors.reset;
const switchType = this.isJKSVFormat(this.config.switchSave) ? '(JKSV Backup)' : '(JSON Format)';
log.status(`PC save: ${pcStatus}`);
log.path(`Path: ${this.config.pcSave}`);
if (pcInfo.exists) {
log.path(`Last modified: ${pcInfo.modified.toLocaleString()}`);
}
log.status(`Switch save: ${switchStatus} ${switchType}`);
log.path(`Path: ${this.config.switchSave}`);
if (switchInfo.exists) {
log.path(`Last modified: ${switchInfo.modified.toLocaleString()}`);
}
log.line();
if (this.config.lastSync) {
log.info(`Last sync: ${new Date(this.config.lastSync).toLocaleString()}`);
} else {
log.info('Last sync: Never');
}
}
}
function showHelp() {
log.highlight('\nHollow Knight Save Manager');
log.line();
log.section('Commands');
console.log(' auto-setup Auto-detect and setup save sync');
console.log(' setup <slot> [output] Setup save sync for specific slot');
console.log(' sync Sync saves between PC and Switch');
console.log(' status Show current configuration');
console.log(' list List all available save files\n');
console.log(' to-switch <save.dat> [output] Convert PC save to Switch format');
console.log(' to-pc <save.json> [output] Convert Switch save to PC format\n');
console.log(' backup <save.dat> <backup-folder> Create JKSV backup');
console.log(' restore <backup-folder> <folder> Extract JKSV backup');
log.section('Examples');
console.log(`${colors.dim}# Auto-detect and setup${colors.reset}`);
console.log(`node ${path.basename(process.argv[1])} auto-setup`);
console.log(`${colors.dim}# Manual setup for save slot 1${colors.reset}`);
console.log(`node ${path.basename(process.argv[1])} setup 1`);
console.log(`${colors.dim}# Skip game selection with --game flag${colors.reset}`);
console.log(`node ${path.basename(process.argv[1])} sync --game hk`);
console.log(`${colors.dim}# Sync your saves${colors.reset}`);
console.log(`node ${path.basename(process.argv[1])} sync`);
log.section('Save Slots');
console.log(' Slot 1: user1.dat Slot 2: user2.dat');
console.log(' Slot 3: user3.dat Slot 4: user4.dat');
log.section('Save Locations');
console.log(`${colors.bright}Windows:${colors.reset}`);
log.path('C:/Users/[Username]/AppData/LocalLow/Team Cherry/Hollow Knight/');
log.path('C:/Users/[Username]/AppData/LocalLow/Team Cherry/Hollow Knight Silksong/[UserID]/');
console.log(`${colors.bright}Switch (JKSV):${colors.reset}`);
log.path('/JKSV/Hollow Knight/');
log.path('/JKSV/Hollow Knight Silksong/');
}
async function selectGame(availableGames = null) {
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
if (availableGames && availableGames.length === 1) {
// only one game available -> auto-select
rl.close();
resolve(availableGames[0]);
return;
}
const gamesToShow = availableGames || Object.keys(GAMES);
log.section('Select Game');
gamesToShow.forEach((gameKey, index) => {
log.info(`${index + 1}. ${GAMES[gameKey].displayName}`);
});
rl.question(`\nChoose game (1-${gamesToShow.length}): `, (answer) => {
const choice = parseInt(answer) - 1;
rl.close();
if (choice >= 0 && choice < gamesToShow.length) {
resolve(gamesToShow[choice]);
} else {
log.error('Invalid choice');
process.exit(1);
}
});
});
}
function parseGameArg(args) {
const gameIndex = args.indexOf('--game');
if (gameIndex !== -1 && gameIndex + 1 < args.length) {
const gameArg = args[gameIndex + 1].toLowerCase();
if (GAMES[gameArg]) {
// remove --game and its value from args
args.splice(gameIndex, 2);
return gameArg;
} else {
log.error(`Unknown game: ${gameArg}`);
log.info('Available games: hk, silksong');
process.exit(1);
}
}
return null; // no game specified
}
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('help') || args.includes('--help')) {
showHelp();
return;
}
const command = args[0];
let gameKey = parseGameArg(args);
try {
if (command === 'auto-setup') {
if (gameKey === null) {
log.progress('Scanning for all save files...');
const allSaves = await detectAllSaves();
if (allSaves.length === 0) {
log.error('No Hollow Knight or Silksong save files found');
log.info('Make sure you have played at least one of the games and saved at least once');
return;
}
log.success(`Found ${allSaves.length} save file(s)`);
log.line();
// group saves by game
const gameGroups = {};
allSaves.forEach(save => {
if (!gameGroups[save.game]) gameGroups[save.game] = [];
gameGroups[save.game].push(save);
});
// display all saves grouped by game
Object.keys(gameGroups).forEach(gameKey => {
const saves = gameGroups[gameKey];
log.section(`${GAMES[gameKey].displayName}`);
saves.forEach((save, index) => {
const recent = index === 0 ? colors.green + ' (most recent in game)' + colors.reset : '';
log.info(`Slot ${save.slot}${recent}`);
log.path(`Modified: ${save.modified.toLocaleString()}`);
log.path(`Path: ${save.path}`);
});
log.line();
});
gameKey = await selectGame(Object.keys(gameGroups));
}
const manager = new SaveManager(gameKey);
await manager.autoSetup();
} else if (command === 'setup') {
if (args.length < 2) {
log.error('Save slot number required');
log.info('Usage: setup <slot-number> [output-file]');
return;
}
if (gameKey === null) {
gameKey = await selectGame();
}
const manager = new SaveManager(gameKey);
await manager.setupSlot(args[1], args[2]);
} else if (command === 'sync') {
if (gameKey === null) {
gameKey = await selectGame();
}
const manager = new SaveManager(gameKey);
let direction = null;
if (args.includes('--pc')) direction = 'pc-to-switch';
if (args.includes('--switch')) direction = 'switch-to-pc';
await manager.sync(direction);
} else if (command === 'status') {
if (gameKey === null) {
gameKey = await selectGame();
}
const manager = new SaveManager(gameKey);
await manager.showStatus();
} else if (command === 'list') {
log.progress('Scanning for all save files...');
const allSaves = await detectAllSaves();
if (allSaves.length === 0) {
log.error('No save files found');
log.info('Make sure you have played Hollow Knight or Silksong and saved at least once');
return;
}
log.success(`Found ${allSaves.length} save file(s)`);
log.line();
const gameGroups = {};
allSaves.forEach(save => {
if (!gameGroups[save.game]) gameGroups[save.game] = [];
gameGroups[save.game].push(save);
});
Object.keys(gameGroups).forEach(gameKey => {
const saves = gameGroups[gameKey];
log.section(`${GAMES[gameKey].displayName}`);
saves.forEach((save, index) => {
const recent = index === 0 ? colors.green + ' (most recent)' + colors.reset : '';
log.info(`Slot ${save.slot}${recent}`);
log.path(`Modified: ${save.modified.toLocaleString()}`);
log.path(`Path: ${save.path}`);
});
log.line();
});
} else if (command === 'to-switch') {
if (args.length < 2) {
log.error('PC save file required');
log.info('Usage: to-switch <save.dat> [output.json]');
return;
}
const input = args[1];
const output = args[2] || input.replace('.dat', '.json');
log.progress('Converting PC save to Switch format');
const pcData = await fs.readFile(input);
const jsonData = pcToSwitch(new Uint8Array(pcData));
JSON.parse(jsonData);
await fs.writeFile(output, jsonData, 'utf8');
log.success('Conversion completed');
log.info(`Switch save: ${output}`);
} else if (command === 'to-pc') {
if (args.length < 2) {
log.error('Switch save file required');
log.info('Usage: to-pc <save.json> [output.dat]');
return;
}
const input = args[1];
const output = args[2] || input.replace('.json', '.dat');
log.progress('Converting Switch save to PC format');
const jsonContent = await fs.readFile(input, 'utf8');
JSON.parse(jsonContent);
const pcData = switchToPC(jsonContent);
await fs.writeFile(output, pcData);
log.success('Conversion completed');
log.info(`PC save: ${output}`);
} else if (command === 'backup') {
if (args.length < 3) {
log.error('Save file and backup name required');
log.info('Usage: backup <save.dat> <backup-folder>');
return;
}
log.progress('Creating JKSV backup');
await createBackup(args[1], args[2]);
log.success('JKSV backup created');
log.info(`Backup: ${args[2]}`);
log.line();
log.highlight('NEXT STEPS:');
log.info('1. Insert your Switch SD card into your computer');
log.info(`2. Navigate to: /switch/JKSV/${GAMES.silksong.jksvPath}/`);
log.info(`3. Copy the file: ${path.basename(args[2])}`);
log.info('4. Safely eject SD card and insert back into Switch');
log.info('5. Use JKSV on Switch to restore the backup');
} else if (command === 'restore') {
if (args.length < 3) {
log.error('Backup file and output folder required');
log.info('Usage: restore <backup-folder> <output-folder>');
return;
}
log.progress('Extracting JKSV backup');
await fs.mkdir(args[2], { recursive: true });
const extracted = await extractBackup(args[1], args[2]);
log.success(`Extracted ${extracted.length} save file(s)`);
log.info(`Output: ${args[2]}`);
extracted.forEach(file => {
log.info(` - ${path.basename(file)}`);
});
} else {
log.error(`Unknown command: ${command}`);
log.info('Run without arguments to see available commands');
}
} catch (error) {
if (error.message.includes('ENOENT')) {
log.error('File not found');
log.info('Check the file path and try again');
} else if (error.message.includes('JSON')) {
log.error('Invalid save file format');
log.info('Make sure the file is a valid save file');
} else {
log.error(`Operation failed: ${error.message}`);
}
process.exit(1);
}
}
process.on('uncaughtException', (error) => {
log.error(`Unexpected error: ${error.message}`);
process.exit(1);
});
process.on('unhandledRejection', (error) => {
log.error(`Unexpected error: ${error.message}`);
process.exit(1);
});
if (require.main === module) {
main();
}
@3l1te404
Copy link

Thanks mate!

@Radik-slowpoke
Copy link

Radik-slowpoke commented Sep 14, 2025

Hi, can you help me, please?
I did as you wrote in your instructions, but JKSV says that the meta file is missing, which is why all the save slots are empty when starting the game.

@ArixAR
Copy link
Author

ArixAR commented Sep 15, 2025

Hi, can you help me, please? I did as you wrote in your instructions, but JKSV says that the meta file is missing, which is why all the save slots are empty when starting the game.

I’ll check on this today and push a fix for it. Did you use your first slot or another save slot? Also did you try to open the generated .zip file? Can it be open or does it show any error?

@ArixAR
Copy link
Author

ArixAR commented Sep 16, 2025

Okay, the latest CLI version should now be working perfectly fine. I do recommned to use the GUI version instead as I won't be maintaining this CLI version anymore.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment