2 Commits

Author SHA1 Message Date
Space-Banane 0434376b83 Bump version to 2.3.1 2026-05-05 12:48:35 +02:00
Space-Banane 681a3cd6df Add function exec-url command 2026-05-05 12:46:24 +02:00
8 changed files with 192 additions and 19 deletions
+20 -1
View File
@@ -30,6 +30,7 @@ this will check the health, and if not setup, it will prompt you to set up the C
- `shsf delete trigger <functionId> <triggerId>`: Deletes a specific trigger from a function.
- `shsf get function <id>`: Get details of a specific function by its ID.
- `shsf get exec-url [--id <id>]`: Get the execution URL for a function. Falls back to `.shsf.json` for the function ID and also prints the alias URL when the function has an `executionAlias`.
- `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.
@@ -81,6 +82,12 @@ You can also create a `.shsf.json` mapping file in the repository root so you do
Command-line flags take precedence over values in `.shsf.json`.
This mapping can also be used with `shsf get exec-url`, so from a mapped function directory you can run:
```bash
shsf get exec-url
```
## 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.
@@ -96,6 +103,18 @@ After creating a function and receiving an ID (for example, 81), you can share t
[UI_URL]/functions/[ID]
```
To get the function execution URL from the CLI:
```bash
shsf get exec-url --id 81
```
If the function has an execution alias configured, the CLI also prints the alias form:
```text
[API_URL]/exec/[executionAlias]
```
## Update
Update with your package manager of choice. ALWAYS pnpm:
```bash
@@ -386,4 +405,4 @@ require (
)
```
Supported Go versions: `1.20`, `1.21`, `1.22`, `1.23`
Supported Go versions: `1.20`, `1.21`, `1.22`, `1.23`
+16
View File
@@ -46,6 +46,22 @@ Quickly see if the system is up and running:
shsf health
```
### 🔗 Execution URL
Get a function's execution URL by ID:
```bash
shsf get exec-url --id <id>
```
If your repo has a `.shsf.json` mapping with an `id`, you can omit `--id`:
```bash
shsf get exec-url
```
The command prints the standard execution URL and, when available, the alias URL based on the function's `executionAlias`.
### 🏗️ Local Development
If you're contributing or running from source:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "shsf-cli",
"version": "2.3.0",
"version": "2.3.1",
"description": "",
"type": "module",
"files": [
+36
View File
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { buildFunctionExecUrls } from "../utils/function_exec_url.js";
describe("buildFunctionExecUrls", () => {
it("builds the ID-based execution URL and trims trailing slashes", () => {
expect(
buildFunctionExecUrls("https://example.com/", {
namespaceId: 4,
executionId: "abc-123",
}),
).toEqual({
executionUrl: "https://example.com/api/exec/4/abc-123",
});
});
it("includes the alias URL when the function has an execution alias", () => {
expect(
buildFunctionExecUrls("https://example.com", {
namespaceId: 7,
executionId: "uuid-456",
executionAlias: "friendly-name",
}),
).toEqual({
executionUrl: "https://example.com/api/exec/7/uuid-456",
aliasUrl: "https://example.com/exec/friendly-name",
});
});
it("throws when required execution URL fields are missing", () => {
expect(() =>
buildFunctionExecUrls("https://example.com", {
executionAlias: "friendly-name",
}),
).toThrow("Function response is missing execution URL fields.");
});
});
+63
View File
@@ -0,0 +1,63 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
import { loadConfig } from "../../config.js";
import { resolveFunctionId } from "../../utils/function_commands.js";
import { buildFunctionExecUrls } from "../../utils/function_exec_url.js";
export const getExecUrlDefinition = {
name: "exec-url",
description: "Get a function execution URL by its ID.",
options: [{ name: "--id <id>", description: "Function ID (falls back to .shsf.json default id)" }],
action: async (options: { id?: string }) => {
const functionId = resolveFunctionId(options);
if (!functionId) return;
await getExecUrl(functionId);
},
};
async function getExecUrl(functionId: string): Promise<void> {
const client = await getApiClient();
const config = await loadConfig();
try {
const response = await client.get(`/api/function/${functionId}`);
const functionData = response.data?.data;
if (!functionData) {
console.error(`${chalk.red("✗")} Unexpected response from server.`);
return;
}
if (!functionData.allow_http) {
console.error(`${chalk.red("✗")} HTTP execution is not allowed for function ${chalk.yellow(functionId)}.`);
return;
}
const urls = buildFunctionExecUrls(config.SHSF_INSTANCE, functionData);
console.log(`${chalk.blue("Execution URL")} for function ${chalk.cyan(functionId)}:`);
console.log(chalk.bgGreen.black(` ${urls.executionUrl} `));
if (urls.aliasUrl) {
console.log(`${chalk.blue("Alias URL")} for function ${chalk.cyan(functionId)}:`);
console.log(chalk.bgGreen.black(` ${urls.aliasUrl} `));
}
} catch (error: any) {
if (error.response) {
if (error.response.status === 404) {
console.error(`${chalk.red("✗")} Function with ID ${chalk.yellow(functionId)} not found.`);
} else {
console.error(`${chalk.red("✗")} Failed to fetch function details.`);
console.error(`Status Code: ${chalk.red(error.response.status)}`);
console.error(
`Message: ${chalk.yellow(error.response.data?.message || "Unknown error from server")}`,
);
}
} else if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`);
} else {
console.error(`${chalk.red("✗")} Error: ${chalk.yellow(error.message)}`);
}
}
}
+2 -17
View File
@@ -1,24 +1,9 @@
import chalk from "chalk";
import { getApiClient } from "../api.js";
import { readMappingFile } from "./push_helpers.js";
import { parseCorsOriginsOption } from "./cors.js";
import { resolveFunctionId } from "./function_commands.js";
export function resolveFunctionId(options: { id?: string }): string | null {
if (options.id) {
return options.id;
}
const mapping = readMappingFile();
if (mapping?.id) {
console.log(chalk.blue(`Using mapped id ${mapping.id} from .shsf.json`));
return mapping.id;
}
console.error(
`${chalk.red("✗")} Function ID is required. Use ${chalk.cyan("--id <id>")} or provide an ${chalk.cyan(".shsf.json")} mapping with an ${chalk.cyan("id")}.`,
);
return null;
}
export { resolveFunctionId } from "./function_commands.js";
export function validateSingleOrigin(origin: string): { origin?: string; error?: string } {
const parsed = parseCorsOriginsOption(origin);
+19
View File
@@ -0,0 +1,19 @@
import chalk from "chalk";
import { readMappingFile } from "./push_helpers.js";
export function resolveFunctionId(options: { id?: string }): string | null {
if (options.id) {
return options.id;
}
const mapping = readMappingFile();
if (mapping?.id) {
console.log(chalk.blue(`Using mapped id ${mapping.id} from .shsf.json`));
return mapping.id;
}
console.error(
`${chalk.red("✗")} Function ID is required. Use ${chalk.cyan("--id <id>")} or provide an ${chalk.cyan(".shsf.json")} mapping with an ${chalk.cyan("id")}.`,
);
return null;
}
+35
View File
@@ -0,0 +1,35 @@
export interface FunctionExecUrlInput {
namespaceId?: number | string;
executionId?: string;
executionAlias?: string | null;
}
export interface FunctionExecUrls {
executionUrl: string;
aliasUrl?: string;
}
function normalizeBaseUrl(instanceUrl: string): string {
return instanceUrl.replace(/\/+$/, "");
}
export function buildFunctionExecUrls(
instanceUrl: string,
functionData: FunctionExecUrlInput,
): FunctionExecUrls {
const baseUrl = normalizeBaseUrl(instanceUrl);
if (functionData.namespaceId === undefined || !functionData.executionId) {
throw new Error("Function response is missing execution URL fields.");
}
const urls: FunctionExecUrls = {
executionUrl: `${baseUrl}/api/exec/${functionData.namespaceId}/${functionData.executionId}`,
};
if (functionData.executionAlias) {
urls.aliasUrl = `${baseUrl}/exec/${functionData.executionAlias}`;
}
return urls;
}