From a76bb15fc9e885ed75c8dd8f620bf7e3367a5c63 Mon Sep 17 00:00:00 2001 From: Space <64922620+Space-Banane@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:01:15 +0100 Subject: [PATCH] feat: add req subcommands for managing requirements.txt (#5) * feat: add req subcommands for managing requirements.txt * bump: version 2.1.9 * fix: address codex review comments (flag consistency, regex-based pkg parsing, error handling) --------- Co-authored-by: Luna --- OpenclawSkill.md | 3 ++ package.json | 2 +- src/commands/req/add.ts | 99 ++++++++++++++++++++++++++++++++++ src/commands/req/remove.ts | 105 +++++++++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 1 deletion(-) create mode 100644 src/commands/req/add.ts create mode 100644 src/commands/req/remove.ts diff --git a/OpenclawSkill.md b/OpenclawSkill.md index 8af5792..431c0e8 100644 --- a/OpenclawSkill.md +++ b/OpenclawSkill.md @@ -53,6 +53,9 @@ this will check the health, and if not setup, it will prompt you to set up the C - `shsf remote pull --id --into [--force]`: Pull files from a function into a local directory. - `shsf remote push --id --from [--force]`: Push files from a local directory to a function. +- `shsf req add --id `: Add a package to the requirements.txt of a function. +- `shsf req remove --id `: Remove a package from the requirements.txt of 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 4e73dfe..e5721e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shsf-cli", - "version": "2.1.8", + "version": "2.1.9", "description": "", "type": "module", "files": [ diff --git a/src/commands/req/add.ts b/src/commands/req/add.ts new file mode 100644 index 0000000..1f2c646 --- /dev/null +++ b/src/commands/req/add.ts @@ -0,0 +1,99 @@ +import chalk from "chalk"; +import { getApiClient } from "../../api.js"; + +interface ShsfFile { + name: string; + content: string; +} + +export const reqAddDefinition = { + name: "add ", + description: "Add a package to the requirements.txt of a function.", + options: [ + { name: "--to ", description: "The ID of the function (deprecated alias for --id)." }, + { name: "--id ", description: "The ID of the function.", required: true }, + ], + action: async (pkg: string, options: { to?: string; id?: string }) => { + const functionId = options.id ?? options.to; + if (!functionId) { + console.error(`${chalk.red("✗")} Error: Function ID is required. Use ${chalk.cyan("--id ")}.`); + return; + } + const client = await getApiClient(); + + try { + // 1. Get current files of the function + const response = await client.get(`/api/function/${functionId}/files`); + let files: ShsfFile[] = response.data.data; + if (!Array.isArray(files)) files = []; + + let requirementsFile = files.find(f => f.name === "requirements.txt"); + let content = requirementsFile ? requirementsFile.content : ""; + + // 2. Add the package + const lines = content.split("\n"); + + // Robust package name extraction + const getPkgName = (line: string) => { + const withoutComment = line.trim().split("#", 1)[0]; + const basePart = withoutComment.split(";", 1)[0].trim(); + if (!basePart) return null; + return basePart.split(/[!=>~<]/)[0].trim().toLowerCase(); + }; + + const pkgToAddName = getPkgName(pkg); + if (!pkgToAddName) { + console.error(`${chalk.red("✗")} Invalid package format.`); + return; + } + + const exists = lines.some(line => { + const lineName = getPkgName(line); + return lineName === pkgToAddName; + }); + + if (exists) { + console.log(`${chalk.yellow("!")} Package ${chalk.cyan(pkgToAddName)} is already in requirements.txt.`); + return; + } + + // Preserve existing content structure + if (content && !content.endsWith("\n")) { + content += "\n"; + } + content += pkg + "\n"; + + // 3. Get function details to find storageId + const funcResponse = await client.get(`/api/function/${functionId}`); + const func = funcResponse.data.data; + if (!func || !func.namespace || !func.namespace.storageId) { + throw new Error("Could not determine storage ID for function."); + } + const storageId = func.namespace.storageId; + + // 4. Overwrite/Create requirements.txt + await client.put(`/api/storage/${storageId}/files`, { + path: "requirements.txt", + content: content + }); + + console.log(`${chalk.green("✓")} Added ${chalk.cyan(pkg)} to requirements.txt for function ${chalk.yellow(functionId)}.`); + } catch (error: any) { + handleError(error, "add package"); + } + }, +}; + +function handleError(error: any, task: string) { + if (error.response) { + console.error(`${chalk.red("✗")} Failed to ${task}: ${chalk.yellow(error.response.data?.message || error.message)}`); + } else if (error.request) { + console.error( + `${chalk.red("✗")} Failed to ${task}: ${chalk.yellow( + "No response received from server. Please check your network connection and try again." + )}` + ); + } else { + console.error(`${chalk.red("✗")} Error: ${error.message}`); + } +} diff --git a/src/commands/req/remove.ts b/src/commands/req/remove.ts new file mode 100644 index 0000000..ecb670a --- /dev/null +++ b/src/commands/req/remove.ts @@ -0,0 +1,105 @@ +import chalk from "chalk"; +import { getApiClient } from "../../api.js"; + +interface ShsfFile { + name: string; + content: string; +} + +export const reqRemoveDefinition = { + name: "remove ", + description: "Remove a package from the requirements.txt of a function.", + options: [ + { name: "--from ", description: "The ID of the function (deprecated alias for --id)." }, + { name: "--id ", description: "The ID of the function.", required: true }, + ], + action: async (pkgToRemove: string, options: { from?: string; id?: string }) => { + const functionId = options.id ?? options.from; + if (!functionId) { + console.error(`${chalk.red("✗")} Error: Function ID is required. Use ${chalk.cyan("--id ")}.`); + return; + } + const client = await getApiClient(); + + try { + // 1. Get current files of the function + const response = await client.get(`/api/function/${functionId}/files`); + let files: ShsfFile[] = response.data.data; + if (!Array.isArray(files)) files = []; + + const requirementsFile = files.find(f => f.name === "requirements.txt"); + if (!requirementsFile) { + console.log(`${chalk.yellow("!")} No requirements.txt found for function ${chalk.yellow(functionId)}.`); + return; + } + + const content = requirementsFile.content; + const lines = content.split("\n"); + + const targetPkgName = pkgToRemove.trim().toLowerCase(); + let removed = false; + + const newLines = lines.filter(line => { + const trimmedLine = line.trim(); + // Preserve empty/whitespace-only lines + if (!trimmedLine) return true; + + // Strip inline comments and environment markers for matching purposes + const withoutComment = trimmedLine.split("#", 1)[0]; + const basePart = withoutComment.split(";", 1)[0].trim(); + + // If there's no actual requirement (e.g., comment-only line), keep the line + if (!basePart) return true; + + // Robust extraction (handles != <= >= ~= == and < >) + const currentPkgName = basePart.split(/[!=>~<]/)[0].trim().toLowerCase(); + + if (currentPkgName === targetPkgName) { + removed = true; + return false; + } + + return true; + }); + + if (!removed) { + console.log(`${chalk.yellow("!")} Package ${chalk.cyan(pkgToRemove)} was not found in requirements.txt.`); + return; + } + + const newContent = newLines.join("\n"); + + // 2. Get function details to find storageId + const funcResponse = await client.get(`/api/function/${functionId}`); + const func = funcResponse.data.data; + if (!func || !func.namespace || !func.namespace.storageId) { + throw new Error("Could not determine storage ID for function."); + } + const storageId = func.namespace.storageId; + + // 3. Overwrite requirements.txt + await client.put(`/api/storage/${storageId}/files`, { + path: "requirements.txt", + content: newContent + }); + + console.log(`${chalk.green("✓")} Removed ${chalk.cyan(pkgToRemove)} from requirements.txt for function ${chalk.yellow(functionId)}.`); + } catch (error: any) { + handleError(error, "remove package"); + } + }, +}; + +function handleError(error: any, task: string) { + if (error.response) { + console.error(`${chalk.red("✗")} Failed to ${task}: ${chalk.yellow(error.response.data?.message || error.message)}`); + } else if (error.request) { + console.error( + `${chalk.red("✗")} Failed to ${task}: ${chalk.yellow( + "No response received from server. Please check your network connection and try again." + )}` + ); + } else { + console.error(`${chalk.red("✗")} Error: ${error.message}`); + } +}