From 344e10ac46fa813ed0fb341aee30b46cfa63f55b Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 20 Mar 2026 16:21:53 +0100 Subject: [PATCH] Add remote sync command and tests --- OpenclawSkill.md | 2 + package.json | 2 +- src/__tests__/remote.test.js | 12 ++++++ src/commands/remote.ts | 77 ++++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/remote.test.js create mode 100644 src/commands/remote.ts diff --git a/OpenclawSkill.md b/OpenclawSkill.md index 98527b0..404327b 100644 --- a/OpenclawSkill.md +++ b/OpenclawSkill.md @@ -50,6 +50,8 @@ this will check the health, and if not setup, it will prompt you to set up the C - `shsf file overwrite`: Overwrite an existing file in a storage. (use `shsf file overwrite -h` first) - `shsf file delete`: Delete a file from a storage. (use `shsf file delete -h` first) - `shsf file list`: List files in a storage. (use `shsf file list -h` first) +- `shsf remote --id --into `: Pull files from a function into a local directory. +- `shsf remote --id --from [--force]`: Push files from a local directory to a function. ## Instructions Use these commands for when you need to interact with shsf from the command line. Its faster than using the ui for almost all ops. diff --git a/package.json b/package.json index 794d254..4fe6051 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shsf-cli", - "version": "2.1.6", + "version": "2.1.7", "description": "", "type": "module", "files": [ diff --git a/src/__tests__/remote.test.js b/src/__tests__/remote.test.js new file mode 100644 index 0000000..ff10260 --- /dev/null +++ b/src/__tests__/remote.test.js @@ -0,0 +1,12 @@ +import { describe, it, expect, vi } from 'vitest'; +import { action } from '../commands/remote.js'; +import axios from 'axios'; + +vi.mock('axios'); + +describe('remote command', () => { + it('should validate inputs', async () => { + // Simple test for now + await expect(action({})).resolves.not.toThrow(); + }); +}); diff --git a/src/commands/remote.ts b/src/commands/remote.ts new file mode 100644 index 0000000..3c137db --- /dev/null +++ b/src/commands/remote.ts @@ -0,0 +1,77 @@ +import { Command } from 'commander'; +import axios from 'axios'; +import { loadConfig } from '../config.js'; +import chalk from 'chalk'; +import fs from 'fs'; +import path from 'path'; + +export async function action(options: any) { + const config = await loadConfig(); + const api = axios.create({ + baseURL: config.apiBaseUrl, + headers: { 'Authorization': `Bearer ${config.token}` } + }); + + if (options.into) { + // PULL + try { + const response = await api.get(`/functions/${options.id}/files`); + const files = response.data; // Assuming { filename: string, content: string }[] + + if (!fs.existsSync(options.into)) { + fs.mkdirSync(options.into, { recursive: true }); + } else if (fs.readdirSync(options.into).length > 0 && !options.force) { + console.error(chalk.red(`Directory ${options.into} is not empty. Use --force to overwrite.`)); + return; + } + + for (const file of files) { + fs.writeFileSync(path.join(options.into, file.filename), file.content); + } + console.log(chalk.green(`Successfully pulled files into ${options.into}`)); + } catch (e) { + console.error(chalk.red('Failed to pull files'), e); + } + } else if (options.from) { + // PUSH + if (!fs.existsSync(options.from)) { + console.error(chalk.red(`Source ${options.from} does not exist.`)); + return; + } + + const files = fs.readdirSync(options.from).map(file => ({ + filename: file, + content: fs.readFileSync(path.join(options.from, file), 'utf-8') + })); + + if (!options.force) { + console.log(chalk.yellow(`Pushing ${files.length} files. Review: ${files.map(f => f.filename).join(', ')}`)); + + if (files.length > 5) { + console.log(chalk.yellow('Too many modifications. Please use --force to confirm.')); + return; + } + } + + try { + await api.post(`/functions/${options.id}/files`, { files }); + console.log(chalk.green('Successfully pushed files.')); + } catch (e) { + console.error(chalk.red('Failed to push files'), e); + } + } else { + console.error(chalk.red('Must specify --into or --from')); + } +} + +export default { + name: 'remote', + description: 'sync remote functions to/from local storage', + options: [ + { name: '--id ', description: 'function id', required: true }, + { name: '--into ', description: 'target directory for pull' }, + { name: '--from ', description: 'source directory for push' }, + { name: '--force', description: 'force push/pull without confirmation' } + ], + action +};