diff --git a/OpenclawSkill.md b/OpenclawSkill.md index 898240c..dad04a8 100644 --- a/OpenclawSkill.md +++ b/OpenclawSkill.md @@ -52,6 +52,11 @@ this will check the health, and if not setup, it will prompt you to set up the C - `shsf file rename`: Rename a file in a function. (use `shsf file rename -h` first) - `shsf file delete`: Delete a file from a function. (use `shsf file delete -h` first) +- `shsf env add --id --name --value `: Adds or updates an environment variable for a function. +- `shsf env remove --id --name `: Removes a specific environment variable from a function. +- `shsf env list --id `: Lists all environment variables for a function. +- `shsf env flush --id `: Removes ALL environment variables from a function. + - `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. @@ -67,33 +72,6 @@ Get the ui url with: 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 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: ``` diff --git a/package.json b/package.json index a8f14b5..7372f33 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shsf-cli", - "version": "2.2.2", + "version": "2.2.3", "description": "", "type": "module", "files": [ diff --git a/src/commands/env/add.ts b/src/commands/env/add.ts new file mode 100644 index 0000000..c3c1315 --- /dev/null +++ b/src/commands/env/add.ts @@ -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 ", description: "Function ID", required: true }, + { + name: "--name ", + description: "Environment variable name", + required: true, + }, + { + name: "--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)}`, + ); + } + } + }, +}; diff --git a/src/commands/env/flush.ts b/src/commands/env/flush.ts new file mode 100644 index 0000000..e56e602 --- /dev/null +++ b/src/commands/env/flush.ts @@ -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 ", 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}`); + } + }, +}; \ No newline at end of file diff --git a/src/commands/env/list.ts b/src/commands/env/list.ts new file mode 100644 index 0000000..7be67c0 --- /dev/null +++ b/src/commands/env/list.ts @@ -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 ", 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)}`); + } + } + }, +}; diff --git a/src/commands/env/remove.ts b/src/commands/env/remove.ts new file mode 100644 index 0000000..13ae978 --- /dev/null +++ b/src/commands/env/remove.ts @@ -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 ", description: "Function ID", required: true }, + { + 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}`); + } + }, +}; \ No newline at end of file