5 Commits

Author SHA1 Message Date
Space-Banane 7b659b1ef5 version-bump: commit of shame 2026-03-27 16:36:48 +01:00
Space-Banane 3261cc1830 unslopped file operations 2026-03-27 16:35:43 +01:00
Space a76bb15fc9 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 <clawy@reversed.dev>
2026-03-26 14:01:15 +01:00
Space 47cd88f72b feat: log byte count for push and pull 2026-03-20 18:01:29 +01:00
luna 6a98ef4bed feat: log byte count for push and pull, increment version 2026-03-20 17:59:41 +01:00
11 changed files with 270 additions and 93 deletions
+3
View File
@@ -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 <id> --into <path> [--force]`: Pull files from a function into a local directory. - `shsf remote pull --id <id> --into <path> [--force]`: Pull files from a function into a local directory.
- `shsf remote push --id <id> --from <path> [--force]`: Push files from a local directory to a function. - `shsf remote push --id <id> --from <path> [--force]`: Push files from a local directory to a function.
- `shsf req add <package> --id <functionid>`: Add a package to the requirements.txt of a function.
- `shsf req remove <package> --id <functionid>`: Remove a package from the requirements.txt of a function.
## Instructions ## 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. 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.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "shsf-cli", "name": "shsf-cli",
"version": "2.1.7", "version": "2.2.0",
"description": "", "description": "",
"type": "module", "type": "module",
"files": [ "files": [
+8 -8
View File
@@ -4,10 +4,10 @@ import { getApiClient } from "../../api.js";
export const fileCreateDefinition = { export const fileCreateDefinition = {
name: "create", name: "create",
description: "Create a file in a storage.", description: "Create or update a file in a function.",
options: [ options: [
{ name: "--storage-id <id>", description: "Storage ID", required: true }, { name: "--function-id <id>", description: "Function ID", required: true },
{ name: "--path <path>", description: "Remote file path", required: true }, { name: "--filename <name>", description: "Remote filename", required: true },
{ name: "--content <content>", description: "Inline file content" }, { name: "--content <content>", description: "Inline file content" },
{ name: "--source <source>", description: "Local source file path" }, { name: "--source <source>", description: "Local source file path" },
], ],
@@ -34,16 +34,16 @@ export const fileCreateDefinition = {
const client = await getApiClient(); const client = await getApiClient();
const payload = { const payload = {
path: options.path, filename: options.filename,
content, code: content,
}; };
try { try {
const response = await client.post(`/api/storage/${options.storageId}/files`, payload); const response = await client.put(`/api/function/${options.functionId}/file`, payload);
if (response.status === 200 || response.status === 201) { if (response.status === 200 || response.status === 201) {
console.log( console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.path)} created successfully in storage ${chalk.cyan(options.storageId)}.`, `${chalk.green("✓")} File ${chalk.cyan(options.filename)} created/updated successfully in function ${chalk.cyan(options.functionId)}.`,
); );
} else { } else {
console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`); console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`);
@@ -51,7 +51,7 @@ export const fileCreateDefinition = {
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
console.error( console.error(
`${chalk.red("✗")} Failed to create file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`, `${chalk.red("✗")} Failed to create/update file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`,
); );
} else if (error.request) { } else if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`); console.error(`${chalk.red("✗")} No response received from server.`);
+6 -8
View File
@@ -3,22 +3,20 @@ import { getApiClient } from "../../api.js";
export const fileDeleteDefinition = { export const fileDeleteDefinition = {
name: "delete", name: "delete",
description: "Delete a file from a storage.", description: "Delete a file from a function.",
options: [ options: [
{ name: "--storage-id <id>", description: "Storage ID", required: true }, { name: "--function-id <id>", description: "Function ID", required: true },
{ name: "--path <path>", description: "Remote file path", required: true }, { name: "--file-id <id>", description: "File ID (from 'file list')", required: true },
], ],
action: async (options: any) => { action: async (options: any) => {
const client = await getApiClient(); const client = await getApiClient();
try { try {
const response = await client.delete(`/api/storage/${options.storageId}/files`, { const response = await client.delete(`/api/function/${options.functionId}/file/${options.fileId}`);
data: { path: options.path },
});
if (response.status === 200) { if (response.status === 200) {
console.log( console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.path)} deleted from storage ${chalk.cyan(options.storageId)}.`, `${chalk.green("✓")} File ${chalk.cyan(options.fileId)} deleted from function ${chalk.cyan(options.functionId)}.`,
); );
} else { } else {
console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`); console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`);
@@ -26,7 +24,7 @@ export const fileDeleteDefinition = {
} catch (error: any) { } catch (error: any) {
if (error.response) { if (error.response) {
if (error.response.status === 404) { if (error.response.status === 404) {
console.error(`${chalk.red("✗")} File ${chalk.yellow(options.path)} not found.`); console.error(`${chalk.red("✗")} File or Function not found.`);
} else { } else {
console.error( console.error(
`${chalk.red("✗")} Failed to delete file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`, `${chalk.red("✗")} Failed to delete file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`,
+5 -8
View File
@@ -3,18 +3,15 @@ import { getApiClient } from "../../api.js";
export const fileListDefinition = { export const fileListDefinition = {
name: "list", name: "list",
description: "List files in a storage.", description: "List files in a function.",
options: [ options: [
{ name: "--storage-id <id>", description: "Storage ID", required: true }, { name: "--function-id <id>", description: "Function ID", required: true },
{ name: "--prefix <prefix>", description: "Optional path prefix filter" },
], ],
action: async (options: any) => { action: async (options: any) => {
const client = await getApiClient(); const client = await getApiClient();
try { try {
const response = await client.get(`/api/storage/${options.storageId}/files`, { const response = await client.get(`/api/function/${options.functionId}/files`);
params: options.prefix ? { prefix: options.prefix } : undefined,
});
if (response.status === 200 && response.data.data) { if (response.status === 200 && response.data.data) {
const files = response.data.data; const files = response.data.data;
@@ -25,9 +22,9 @@ export const fileListDefinition = {
} }
console.log(chalk.blue("Files:")); console.log(chalk.blue("Files:"));
console.log(chalk.gray(`${"ID".padEnd(25)} ${"Filename"}`));
files.forEach((file: any) => { files.forEach((file: any) => {
const path = file.path || file.name || "(unknown)"; console.log(`${chalk.cyan(file.id.padEnd(25))} ${chalk.white(file.filename)}`);
console.log(`- ${chalk.cyan(path)}`);
}); });
console.log(`\n${chalk.green("✓")} Found ${chalk.bgGreen.black(` ${files.length} `)} files.`); console.log(`\n${chalk.green("✓")} Found ${chalk.bgGreen.black(` ${files.length} `)} files.`);
} else { } else {
-67
View File
@@ -1,67 +0,0 @@
import chalk from "chalk";
import fs from "fs/promises";
import { getApiClient } from "../../api.js";
export const fileOverwriteDefinition = {
name: "overwrite",
description: "Overwrite an existing file in a storage.",
options: [
{ name: "--storage-id <id>", description: "Storage ID", required: true },
{ name: "--path <path>", description: "Remote file path", required: true },
{ name: "--content <content>", description: "Inline file content" },
{ name: "--source <source>", description: "Local source file path" },
],
action: async (options: any) => {
if (!options.content && !options.source) {
console.error(`${chalk.red("✗")} You must provide either --content or --source.`);
return;
}
if (options.content && options.source) {
console.error(`${chalk.red("✗")} Please provide only one of --content or --source.`);
return;
}
let content = options.content;
if (options.source) {
try {
content = await fs.readFile(options.source, "utf8");
} catch (error: any) {
console.error(`${chalk.red("✗")} Failed to read source file: ${chalk.yellow(error.message)}`);
return;
}
}
const client = await getApiClient();
const payload = {
path: options.path,
content,
};
try {
const response = await client.put(`/api/storage/${options.storageId}/files`, payload);
if (response.status === 200) {
console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.path)} overwritten successfully in storage ${chalk.cyan(options.storageId)}.`,
);
} else {
console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`);
}
} catch (error: any) {
if (error.response) {
if (error.response.status === 404) {
console.error(`${chalk.red("✗")} File ${chalk.yellow(options.path)} not found.`);
} else {
console.error(
`${chalk.red("✗")} Failed to overwrite file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`,
);
}
} else if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`);
} else {
console.error(`${chalk.red("✗")} Error: ${error.message}`);
}
}
},
};
+39
View File
@@ -0,0 +1,39 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
export const fileRenameDefinition = {
name: "rename",
description: "Rename a file in a function.",
options: [
{ name: "--function-id <id>", description: "Function ID", required: true },
{ name: "--file-id <id>", description: "File ID", required: true },
{ name: "--new-filename <name>", description: "New filename", required: true },
],
action: async (options: any) => {
const client = await getApiClient();
try {
const response = await client.patch(`/api/function/${options.functionId}/file/${options.fileId}/rename`, {
newFilename: options.newFilename,
});
if (response.status === 200) {
console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.fileId)} renamed to ${chalk.cyan(options.newFilename)} successfully.`,
);
} else {
console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`);
}
} catch (error: any) {
if (error.response) {
console.error(
`${chalk.red("✗")} Failed to rename file: ${chalk.yellow(error.response.data?.message || "Unknown error")}`,
);
} else if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`);
} else {
console.error(`${chalk.red("✗")} Error: ${error.message}`);
}
}
},
};
+2
View File
@@ -26,6 +26,8 @@ export const pullDefinition = {
} }
for (const file of files) { for (const file of files) {
fs.writeFileSync(path.join(options.into, file.name), file.content); fs.writeFileSync(path.join(options.into, file.name), file.content);
const byteSize = Buffer.byteLength(file.content, 'utf8');
console.log(chalk.green(`Pulled ${file.name} (${byteSize} bytes)`));
} }
console.log(chalk.green(`Successfully pulled ${files.length} files into ${options.into}`)); console.log(chalk.green(`Successfully pulled ${files.length} files into ${options.into}`));
} catch (error: any) { } catch (error: any) {
+2 -1
View File
@@ -97,8 +97,9 @@ export const pushDefinition = {
code: file.content, code: file.content,
}); });
const byteSize = Buffer.byteLength(file.content, 'utf8');
console.log( console.log(
chalk.green(`Pushed ${file.filename} to function ${options.id}`), chalk.green(`Pushed ${file.filename} (${byteSize} bytes) to function ${options.id}`),
); );
pushedFilesCount++; pushedFilesCount++;
} }
+99
View File
@@ -0,0 +1,99 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
interface ShsfFile {
name: string;
content: string;
}
export const reqAddDefinition = {
name: "add <package>",
description: "Add a package to the requirements.txt of a function.",
options: [
{ name: "--to <id>", description: "The ID of the function (deprecated alias for --id)." },
{ name: "--id <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 <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}`);
}
}
+105
View File
@@ -0,0 +1,105 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
interface ShsfFile {
name: string;
content: string;
}
export const reqRemoveDefinition = {
name: "remove <package>",
description: "Remove a package from the requirements.txt of a function.",
options: [
{ name: "--from <id>", description: "The ID of the function (deprecated alias for --id)." },
{ name: "--id <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 <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}`);
}
}