9 Commits

Author SHA1 Message Date
Space-Banane e953466786 fix: update version number to 2.2.4 in package.json 2026-04-01 13:03:58 +02:00
Space-Banane c43c37ce34 feat: add unpushable file checks to prevent uploading certain file types 2026-04-01 13:01:47 +02:00
Space-Banane 13900343a3 feat: add commands for managing environment variables in functions & removed ui docs from skill 2026-03-27 17:18:00 +01:00
Space-Banane cc54ef85e1 fix: update command syntax for function execution in documentation 2026-03-27 16:56:54 +01:00
Space-Banane 67f05af7a5 feat: add execute command for function execution with streaming options 2026-03-27 16:55:28 +01:00
Space-Banane 37f6d50034 fix: requirements.txt modifications 2026-03-27 16:50:04 +01:00
Space-Banane a1f80d38f2 update skill to reflect changes 2026-03-27 16:38:18 +01:00
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
15 changed files with 522 additions and 160 deletions
+11 -31
View File
@@ -33,6 +33,8 @@ this will check the health, and if not setup, it will prompt you to set up the C
- `shsf get namespace <id>`: Get details of a specific namespace by its ID - `shsf get namespace <id>`: Get details of a specific namespace by its ID
- `shsf get trigger <functionId> <triggerId>`: Get details of a specific trigger from a function. - `shsf get trigger <functionId> <triggerId>`: Get details of a specific trigger from a function.
- `shsf function execute --id <id> [--payload <json>] [--no-stream]`: Execute a function and stream the output (debug/internal).
- `shsf storage create --name <name> --purpose <purpose>`: Create a new storage. - `shsf storage create --name <name> --purpose <purpose>`: Create a new storage.
- `shsf storage delete --name <name>`: Delete a storage. - `shsf storage delete --name <name>`: Delete a storage.
- `shsf storage list`: List all storages. - `shsf storage list`: List all storages.
@@ -45,10 +47,15 @@ this will check the health, and if not setup, it will prompt you to set up the C
- `shsf update namespace <id>`: Update a specific namespace by its ID. (use `shsf update namespace -h` first) - `shsf update namespace <id>`: Update a specific namespace by its ID. (use `shsf update namespace -h` first)
- `shsf update trigger <functionId> <triggerId>`: Update a specific trigger from a function. (use `shsf update trigger -h` first) - `shsf update trigger <functionId> <triggerId>`: Update a specific trigger from a function. (use `shsf update trigger -h` first)
- `shsf file create`: Create a file in a storage. (use `shsf file create -h` first) - `shsf file create`: Create/update a file in a function. (use `shsf file create -h` first)
- `shsf file overwrite`: Overwrite an existing file in a storage. (use `shsf file overwrite -h` first) - `shsf file list`: List files in a function. (use `shsf file list -h` first)
- `shsf file delete`: Delete a file from a storage. (use `shsf file delete -h` first) - `shsf file rename`: Rename a file in a function. (use `shsf file rename -h` first)
- `shsf file list`: List files in a storage. (use `shsf file list -h` first) - `shsf file delete`: Delete a file from a function. (use `shsf file delete -h` first)
- `shsf env add --id <id> --name <name> --value <value>`: Adds or updates an environment variable for a function.
- `shsf env remove --id <id> --name <name>`: Removes a specific environment variable from a function.
- `shsf env list --id <id>`: Lists all environment variables for a function.
- `shsf env flush --id <id>`: Removes ALL environment variables from a function.
- `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.
@@ -65,33 +72,6 @@ Get the ui url with:
shsf uiurl shsf uiurl
``` ```
## Available UI Routes
Below are the main routes you can use in the SHSF web UI. These are not clickable links, but you can navigate to them in your browser.
**General**
- `/`: Home Page
**Documentation**
- `/docs`: Documentation
**Account**
- `/account`: Account Settings
- `/login`: Login
- `/register`: Register
**Functions**
- `/functions`: Shows your functions, grouped by their respective namespaces (requires auth).
- `/functions/:id`: View details for a specific function (requires auth).
**Other**
- `/admin`: Admin dashboard (admin only, requires authentication)
- `/storage`: Storage management (requires auth)
- `/cron-jobs`: Cron job management (requires auth)
- `/access-tokens`: Access token management (requires auth)
- `/guest-users`: Guest user management (requires auth)
- `/guest-access`: Guest access portal
### Example ### Example
After creating a function and receiving an ID (for example, 81), you can share the following URL with your human so they can view the function in the UI: After creating a function and receiving an ID (for example, 81), you can share the following URL with your human so they can view the function in the UI:
``` ```
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "shsf-cli", "name": "shsf-cli",
"version": "2.1.9", "version": "2.2.4",
"description": "", "description": "",
"type": "module", "type": "module",
"files": [ "files": [
+96
View File
@@ -0,0 +1,96 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
export const updateFunctionDefinition = {
name: "add",
description:
"Adds or updates environment variables for a serverless function.",
options: [
{ name: "--id <id>", description: "Function ID", required: true },
{
name: "--name <name>",
description: "Environment variable name",
required: true,
},
{
name: "--value <value>",
description: "Environment variable value",
required: true,
},
],
action: async (options: any) => {
if (!options.id || !options.name || !options.value) {
console.error(
`${chalk.red("✗")} Error: Missing required options (--id, --name, --value)`,
);
return;
}
const client = await getApiClient();
let existingEnv: { name: string; value: string }[] = [];
try {
const response = await client.get(`/api/function/${options.id}`);
const functionData = response.data.data || response.data;
existingEnv = functionData.env || [];
if (typeof existingEnv === 'string') {
try {
existingEnv = JSON.parse(existingEnv);
} catch (e: any) {
existingEnv = [];
}
}
if (!Array.isArray(existingEnv)) {
existingEnv = [];
}
} catch (error: any) {
console.error(
`${chalk.red("✗")} Error fetching function data: ${error.message}`,
);
return;
}
const newEnv = [...existingEnv];
const existingIndex = newEnv.findIndex((env) => env.name === options.name);
const isUpdate = existingIndex !== -1;
if (isUpdate) {
newEnv[existingIndex].value = options.value;
} else {
newEnv.push({ name: options.name, value: options.value });
}
const totalCount = newEnv.length;
try {
const response = await client.patch(`/api/function/${options.id}`, {
environment: newEnv,
});
if (response.status === 200) {
const actionText = isUpdate ? "updated" : "added";
console.log(
`${chalk.green("✓")} Environment variable ${chalk.cyan(options.name)} ${actionText} successfully for function ${chalk.cyan(options.id)}.`,
);
console.log(
`${chalk.blue("")} Total environment variables: ${totalCount}`,
);
} else {
console.error(
`${chalk.red("✗")} Unexpected response from server: ${response.status}`,
);
}
} catch (error: any) {
if (error.response?.status === 404) {
console.error(
`${chalk.red("✗")} Function with ID ${chalk.yellow(options.id)} not found.`,
);
} else {
console.error(
`${chalk.red("✗")} Failed to update: ${chalk.yellow(error.response?.data?.message || error.message)}`,
);
}
}
},
};
+33
View File
@@ -0,0 +1,33 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
export const flushFunctionDefinition = {
name: "flush",
description: "Removes ALL environment variables from a serverless function.",
options: [
{ name: "--id <id>", description: "Function ID", required: true },
],
action: async (options: any) => {
if (!options.id) {
console.error(`${chalk.red("✗")} Error: --id is required`);
return;
}
const client = await getApiClient();
try {
const response = await client.patch(`/api/function/${options.id}`, {
environment: [],
});
if (response.status === 200) {
console.log(
`${chalk.green("✓")} All environment variables flushed for function ${chalk.cyan(options.id)}.`
);
console.log(`${chalk.blue("")} Total environment variables: 0`);
}
} catch (error: any) {
console.error(`${chalk.red("✗")} Error: ${error.response?.data?.message || error.message}`);
}
},
};
+53
View File
@@ -0,0 +1,53 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
export const listEnvDefinition = {
name: "list",
description: "Lists all environment variables for a serverless function.",
options: [
{ name: "--id <id>", description: "Function ID", required: true },
],
action: async (options: any) => {
if (!options.id) {
console.error(`${chalk.red("✗")} Error: --id is required`);
return;
}
const client = await getApiClient();
try {
const response = await client.get(`/api/function/${options.id}`);
const functionData = response.data.data || response.data;
let env = functionData.env;
if (typeof env === 'string' && env.length > 0) {
try {
env = JSON.parse(env);
} catch (e: any) {
console.error(`${chalk.red("✗")} Error parsing env string: ${e.message}`);
env = [];
}
}
const envList: { name: string; value: string }[] = Array.isArray(env) ? env : [];
if (envList.length === 0) {
console.log(`${chalk.yellow("!")} No environment variables found for function ${chalk.cyan(options.id)}.`);
return;
}
console.log(`${chalk.blue("")} Environment variables for function ${chalk.cyan(options.id)}:`);
envList.forEach((item) => {
console.log(`${chalk.green("•")} ${chalk.cyan(item.name)}=${chalk.white(item.value)}`);
});
console.log(`${chalk.blue("")} Total: ${envList.length}`);
} catch (error: any) {
if (error.response?.status === 404) {
console.error(`${chalk.red("✗")} Function with ID ${chalk.yellow(options.id)} not found.`);
} else {
console.error(`${chalk.red("✗")} Error fetching environment variables: ${chalk.yellow(error.response?.data?.message || error.message)}`);
}
}
},
};
+62
View File
@@ -0,0 +1,62 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
export const removeFunctionDefinition = {
name: "remove",
description: "Removes a specific environment variable from a serverless function.",
options: [
{ name: "--id <id>", description: "Function ID", required: true },
{
name: "--name <name>",
description: "Environment variable name to remove",
required: true,
},
],
action: async (options: any) => {
if (!options.id || !options.name) {
console.error(`${chalk.red("✗")} Error: --id and --name are required`);
return;
}
const client = await getApiClient();
try {
const response = await client.get(`/api/function/${options.id}`);
const functionData = response.data.data || response.data;
let existingEnv: { name: string; value: string }[] = functionData.env || [];
if (typeof existingEnv === 'string') {
try {
existingEnv = JSON.parse(existingEnv);
} catch (e: any) {
existingEnv = [];
}
}
if (!Array.isArray(existingEnv)) {
existingEnv = [];
}
const initialCount = existingEnv.length;
const filteredEnv = existingEnv.filter((env) => env.name !== options.name);
if (filteredEnv.length === initialCount) {
console.warn(`${chalk.yellow("!")} Variable ${chalk.cyan(options.name)} not found. No changes made.`);
return;
}
const patchResponse = await client.patch(`/api/function/${options.id}`, {
environment: filteredEnv,
});
if (patchResponse.status === 200) {
console.log(
`${chalk.green("✓")} Variable ${chalk.cyan(options.name)} removed successfully from function ${chalk.cyan(options.id)}.`
);
console.log(`${chalk.blue("")} Remaining environment variables: ${filteredEnv.length}`);
}
} catch (error: any) {
console.error(`${chalk.red("✗")} Error: ${error.response?.data?.message || error.message}`);
}
},
};
+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}`);
}
}
},
};
+152
View File
@@ -0,0 +1,152 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
import { AxiosResponse } from "axios";
export const functionExecuteDefinition = {
name: "execute",
description: "Execute a function and stream the output (debug/internal).",
options: [
{ name: "--id <id>", description: "Function ID", required: true },
{ name: "--payload <json>", description: "JSON payload for execution", required: false },
{ name: "--no-stream", description: "Disable streaming and get final result only", required: false, default: false },
],
action: async (options: any) => {
const client = await getApiClient();
// Explicitly check for noStream from commander
// Commander uses camelsCase for options, so --no-stream becomes stream: false
// but we use --no-stream which commander natively handles as a boolean toggle
const useStream = options.stream !== false;
let payload = {};
if (options.payload) {
try {
payload = JSON.parse(options.payload);
} catch (error: any) {
console.error(`${chalk.red("✗")} Invalid JSON payload: ${chalk.yellow(error.message)}`);
return;
}
}
const handleJsonChunk = (chunk: string) => {
const trimmed = chunk.trim();
if (!trimmed) return;
try {
const obj = JSON.parse(trimmed);
if (obj.type === "output" && obj.content) {
process.stdout.write(obj.content);
} else if (obj.type === "end") {
if (obj.exitCode !== 0 && obj.exitCode !== undefined) {
console.error(`\n${chalk.red("✗")} Execution failed with exit code ${obj.exitCode}`);
} else {
console.log(`\n${chalk.green("✓")} Execution completed.`);
}
if (obj.result !== undefined) {
console.log(chalk.blue("Result:"), typeof obj.result === 'object' ? JSON.stringify(obj.result, null, 2) : obj.result);
}
if (obj.took) {
const total = obj.took.find((t: any) => t.description === "Total");
if (total) {
console.log(chalk.gray(`Total time: ${total.value}s`));
}
}
}
} catch (e) {
// If it's not JSON, it might be raw output from a non-stream response
// that got into the stream handler mistakenly or just raw noise
if (trimmed.length > 0) {
process.stdout.write(chunk);
}
// Not a full JSON object yet or invalid JSON
}
};
console.log(`${chalk.blue("")} Executing function ${chalk.cyan(options.id)} (stream: ${useStream})...`);
try {
const response = await client.post(`/api/function/${options.id}/execute`,
{ run: payload },
{
params: { stream: useStream ? "true" : "false" },
responseType: useStream ? "stream" : "json"
}
);
if (useStream) {
const stream = (response as AxiosResponse).data;
let buffer = "";
return new Promise<void>((resolve, reject) => {
stream.on("data", (chunk: any) => {
const str = chunk.toString();
buffer += str;
// Try to split by potential JSON boundaries
// Matches cases like ...}{...
const pattern = /\}\s*\{/g;
let lastIndex = 0;
let match;
while ((match = pattern.exec(buffer)) !== null) {
const part = buffer.slice(lastIndex, match.index + 1);
handleJsonChunk(part);
lastIndex = match.index + match[0].indexOf('{');
}
buffer = buffer.slice(lastIndex);
// Attempt to parse the remaining buffer if it looks like a complete object
try {
if (buffer.trim().startsWith('{') && buffer.trim().endsWith('}')) {
handleJsonChunk(buffer);
buffer = "";
}
} catch (e) {
// Wait for more data
}
});
stream.on("end", () => {
if (buffer.trim()) {
handleJsonChunk(buffer);
}
resolve();
});
stream.on("error", (err: Error) => {
console.error(`\n${chalk.red("✗")} Stream error: ${err.message}`);
reject(err);
});
});
} else {
// Non-stream returns just the output content or the body
const result = response.data;
if (typeof result === 'string') {
process.stdout.write(result);
} else if (result && result.data && typeof result.data === 'string') {
process.stdout.write(result.data);
} else if (result && typeof result.output === 'string') {
process.stdout.write(result.output);
} else if (result && typeof result.result === 'string') {
process.stdout.write(result.result);
} else if (result && result.data && result.data.output) {
process.stdout.write(result.data.output);
} else {
process.stdout.write(JSON.stringify(result, null, 2));
}
console.log(`\n${chalk.green("✓")} Execution completed.`);
}
} catch (error: any) {
if (error.response) {
console.error(
`${chalk.red("✗")} Execution failed: ${chalk.yellow(error.response.data?.message || error.response.statusText || "Unknown error")}`,
);
} else if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`);
} else {
console.error(`${chalk.red("✗")} Error: ${error.message}`);
}
}
},
};
+41 -6
View File
@@ -4,6 +4,30 @@ import fs from "fs";
import path from "path"; import path from "path";
import { createHash } from "crypto"; import { createHash } from "crypto";
const unpushableFiles = [
".png",
".jpg",
".jpeg",
".gif",
".bmp",
".ico",
".zip",
".tar",
".gz",
".7z",
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".exe",
".dll",
".so",
".dylib"
];
async function deleteNonexistentFiles( async function deleteNonexistentFiles(
currentFiles: any[], currentFiles: any[],
files: any[], files: any[],
@@ -19,7 +43,8 @@ async function deleteNonexistentFiles(
{ {
data: { filename: currentFile.name }, data: { filename: currentFile.name },
}, },
);} catch (error: any) { );
} catch (error: any) {
throw error; throw error;
} }
console.log( console.log(
@@ -76,9 +101,6 @@ export const pushDefinition = {
(f: any) => f.name === file.filename, (f: any) => f.name === file.filename,
); );
if (currentFile) { if (currentFile) {
// If the file content exists in the response, we can hash it.
// Note: currentFiles from /api/function/:id/files might not have content by default depending on API,
// but the original code assumes it does.
const currentFileHash = createHash("md5") const currentFileHash = createHash("md5")
.update(currentFile.content || "") .update(currentFile.content || "")
.digest("hex"); .digest("hex");
@@ -92,14 +114,27 @@ export const pushDefinition = {
} }
} }
const filenameLower = file.filename.toLowerCase();
// Unpushable file check
if (unpushableFiles.some((suffix) => filenameLower.endsWith(suffix))) {
console.log(
chalk.yellow(
`Skipping ${file.filename}, it matches unpushable patterns for function ${options.id}`,
),
);
continue;
}
await client.put(`/api/function/${options.id}/file`, { await client.put(`/api/function/${options.id}/file`, {
filename: file.filename, filename: file.filename,
code: file.content, code: file.content,
}); });
const byteSize = Buffer.byteLength(file.content, 'utf8'); const byteSize = Buffer.byteLength(file.content, "utf8");
console.log( console.log(
chalk.green(`Pushed ${file.filename} (${byteSize} bytes) to function ${options.id}`), chalk.green(
`Pushed ${file.filename} (${byteSize} bytes) to function ${options.id}`,
),
); );
pushedFilesCount++; pushedFilesCount++;
} }
+5 -13
View File
@@ -63,18 +63,10 @@ export const reqAddDefinition = {
} }
content += pkg + "\n"; content += pkg + "\n";
// 3. Get function details to find storageId // 3. Overwrite/Create requirements.txt using function-based API
const funcResponse = await client.get(`/api/function/${functionId}`); await client.put(`/api/function/${functionId}/file`, {
const func = funcResponse.data.data; filename: "requirements.txt",
if (!func || !func.namespace || !func.namespace.storageId) { code: content,
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)}.`); console.log(`${chalk.green("✓")} Added ${chalk.cyan(pkg)} to requirements.txt for function ${chalk.yellow(functionId)}.`);
@@ -86,7 +78,7 @@ export const reqAddDefinition = {
function handleError(error: any, task: string) { function handleError(error: any, task: string) {
if (error.response) { if (error.response) {
console.error(`${chalk.red("✗")} Failed to ${task}: ${chalk.yellow(error.response.data?.message || error.message)}`); console.error(`${chalk.red("✗")} Failed to ${task}: ${chalk.yellow(error.response.data?.message || (error.response.data ? JSON.stringify(error.response.data) : error.message))}`);
} else if (error.request) { } else if (error.request) {
console.error( console.error(
`${chalk.red("✗")} Failed to ${task}: ${chalk.yellow( `${chalk.red("✗")} Failed to ${task}: ${chalk.yellow(
+5 -13
View File
@@ -69,18 +69,10 @@ export const reqRemoveDefinition = {
const newContent = newLines.join("\n"); const newContent = newLines.join("\n");
// 2. Get function details to find storageId // 2. Overwrite requirements.txt using function-based API
const funcResponse = await client.get(`/api/function/${functionId}`); await client.put(`/api/function/${functionId}/file`, {
const func = funcResponse.data.data; filename: "requirements.txt",
if (!func || !func.namespace || !func.namespace.storageId) { code: newContent,
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)}.`); console.log(`${chalk.green("✓")} Removed ${chalk.cyan(pkgToRemove)} from requirements.txt for function ${chalk.yellow(functionId)}.`);
@@ -92,7 +84,7 @@ export const reqRemoveDefinition = {
function handleError(error: any, task: string) { function handleError(error: any, task: string) {
if (error.response) { if (error.response) {
console.error(`${chalk.red("✗")} Failed to ${task}: ${chalk.yellow(error.response.data?.message || error.message)}`); console.error(`${chalk.red("✗")} Failed to ${task}: ${chalk.yellow(error.response.data?.message || (error.response.data ? JSON.stringify(error.response.data) : error.message))}`);
} else if (error.request) { } else if (error.request) {
console.error( console.error(
`${chalk.red("✗")} Failed to ${task}: ${chalk.yellow( `${chalk.red("✗")} Failed to ${task}: ${chalk.yellow(