3 Commits

9 changed files with 149 additions and 83 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "shsf-cli",
"version": "2.2.7",
"version": "2.2.8",
"description": "",
"type": "module",
"files": [
+47
View File
@@ -0,0 +1,47 @@
import { describe, it, expect } from "vitest";
import {
listFiles,
createOrUpdateFile,
deleteFile,
renameFile,
} from "../utils/fileops.js";
function makeClient(overrides: any = {}) {
return {
get: async (u: string) => ({ status: 200, data: { data: overrides.getData || [] } }),
put: async (u: string, p: any) => ({ status: overrides.putStatus || 200, data: { ok: true } }),
delete: async (u: string, opts: any) => ({ status: overrides.deleteStatus || 200, data: {} }),
patch: async (u: string, p: any) => ({ status: overrides.patchStatus || 200, data: {} }),
};
}
describe("fileops", () => {
it("lists files", async () => {
const client = makeClient({ getData: [{ id: "1", name: "a.txt" }] });
const files = await listFiles(client, "fn1");
expect(files).toHaveLength(1);
const file = files[0];
expect(file).toHaveProperty("id");
expect(file.id).toBe("1");
// Accept either `name` or `filename` depending on API shape
expect(file.name || file.filename).toBe("a.txt");
});
it("creates or updates a file", async () => {
const client = makeClient({ putStatus: 201 });
const res = await createOrUpdateFile(client, "fn1", "a.txt", "hello");
expect(res).toBeDefined();
});
it("deletes a file", async () => {
const client = makeClient({ deleteStatus: 200 });
const ok = await deleteFile(client, "fn1", "file1");
expect(ok).toBe(true);
});
it("renames a file", async () => {
const client = makeClient({ patchStatus: 200 });
const res = await renameFile(client, "fn1", "file1", "b.txt");
expect(res).toBeDefined();
});
});
+6 -23
View File
@@ -1,6 +1,7 @@
import chalk from "chalk";
import fs from "fs/promises";
import { getApiClient } from "../../api.js";
import { createOrUpdateFile, handleAxiosError } from "../../utils/fileops.js";
export const fileCreateDefinition = {
name: "create",
@@ -33,31 +34,13 @@ export const fileCreateDefinition = {
}
const client = await getApiClient();
const payload = {
filename: options.filename,
code: content,
};
try {
const response = await client.put(`/api/function/${options.functionId}/file`, payload);
if (response.status === 200 || response.status === 201) {
console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.filename)} created/updated successfully in function ${chalk.cyan(options.functionId)}.`,
);
} else {
console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`);
}
await createOrUpdateFile(client, options.functionId, options.filename, content as string);
console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.filename)} created/updated successfully in function ${chalk.cyan(options.functionId)}.`,
);
} catch (error: any) {
if (error.response) {
console.error(
`${chalk.red("✗")} Failed to create/update 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}`);
}
handleAxiosError(error);
}
},
};
+6 -23
View File
@@ -1,5 +1,6 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
import { deleteFile, handleAxiosError } from "../../utils/fileops.js";
export const fileDeleteDefinition = {
name: "delete",
@@ -10,31 +11,13 @@ export const fileDeleteDefinition = {
],
action: async (options: any) => {
const client = await getApiClient();
try {
const response = await client.delete(`/api/function/${options.functionId}/file/${options.fileId}`);
if (response.status === 200) {
console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.fileId)} deleted from function ${chalk.cyan(options.functionId)}.`,
);
} else {
console.log(`${chalk.yellow("!")} Unexpected response from server: ${response.status}`);
}
await deleteFile(client, options.functionId, options.fileId, undefined);
console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.fileId)} deleted from function ${chalk.cyan(options.functionId)}.`,
);
} catch (error: any) {
if (error.response) {
if (error.response.status === 404) {
console.error(`${chalk.red("✗")} File or Function not found.`);
} else {
console.error(
`${chalk.red("✗")} Failed to delete 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}`);
}
handleAxiosError(error);
}
},
};
+6 -14
View File
@@ -1,5 +1,6 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
import { listFiles, handleAxiosError } from "../../utils/fileops.js";
export const fileListDefinition = {
name: "list",
@@ -11,10 +12,9 @@ export const fileListDefinition = {
const client = await getApiClient();
try {
const response = await client.get(`/api/function/${options.functionId}/files`);
const files = await listFiles(client, options.functionId);
if (response.status === 200 && response.data.data) {
const files = response.data.data;
if (files) {
if (!Array.isArray(files) || files.length === 0) {
console.log(`${chalk.yellow("!")} No files found.`);
@@ -24,23 +24,15 @@ export const fileListDefinition = {
console.log(chalk.blue("Files:"));
console.log(chalk.gray(`${"ID".padEnd(25)} ${"Filename"}`));
files.forEach((file: any) => {
console.log(`${chalk.cyan(file.id.padEnd(25))} ${chalk.white(file.filename)}`);
const id = String(file.id ?? file.name ?? "");
console.log(`${chalk.cyan(id.padEnd(25))} ${chalk.white(file.filename || file.name)}`);
});
console.log(`\n${chalk.green("✓")} Found ${chalk.bgGreen.black(` ${files.length} `)} files.`);
} else {
console.log(`${chalk.yellow("!")} Unexpected response from server.`);
console.log(`Status Code: ${chalk.blue(response.status)}`);
}
} catch (error: any) {
if (error.response) {
console.error(
`${chalk.red("✗")} Failed to list files: ${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}`);
}
handleAxiosError(error);
}
},
};
+6 -21
View File
@@ -1,5 +1,6 @@
import chalk from "chalk";
import { getApiClient } from "../../api.js";
import { renameFile, handleAxiosError } from "../../utils/fileops.js";
export const fileRenameDefinition = {
name: "rename",
@@ -11,29 +12,13 @@ export const fileRenameDefinition = {
],
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}`);
}
await renameFile(client, options.functionId, options.fileId, options.newFilename);
console.log(
`${chalk.green("✓")} File ${chalk.cyan(options.fileId)} renamed to ${chalk.cyan(options.newFilename)} successfully.`,
);
} 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}`);
}
handleAxiosError(error);
}
},
};
+3 -1
View File
@@ -12,10 +12,12 @@ import { createHash } from "crypto";
import type { ApiClient } from "../../types/apiClient.js";
async function deleteNonexistentFiles(
currentFiles: any[],
files: any[],
client: any,
client: ApiClient,
options: any,
): Promise<{ didDeletion: boolean }> {
let didDeletion = false;
+11
View File
@@ -0,0 +1,11 @@
// Minimal API client interface used by this CLI. Keep it small so tests can
// provide lightweight mocks without implementing the full AxiosInstance.
export interface ApiClient {
get(url: string, config?: any): Promise<{ status: number; data: any }>;
put(url: string, payload?: any, config?: any): Promise<{ status: number; data: any }>;
delete(url: string, config?: any): Promise<{ status: number; data: any }>;
patch(url: string, payload?: any, config?: any): Promise<{ status: number; data: any }>;
}
export type { ApiClient as ApiClientType };
+63
View File
@@ -0,0 +1,63 @@
import chalk from "chalk";
import type { ApiClient } from "../types/apiClient.js";
export async function listFiles(client: ApiClient, functionId: string) {
const response = await client.get(`/api/function/${functionId}/files`);
if (response.status === 200) return response.data.data || [];
throw new Error(`Unexpected status ${response.status}`);
}
export async function createOrUpdateFile(client: ApiClient, functionId: string, filename: string, code: string) {
const payload = { filename, code };
const response = await client.put(`/api/function/${functionId}/file`, payload);
if (response.status === 200 || response.status === 201) return response.data;
throw new Error(`Unexpected status ${response.status}`);
}
export async function deleteFile(client: ApiClient, functionId: string, fileId: string, filename?: string) {
// Some endpoints expect filename on body; include if provided.
const opts: any = {};
if (filename) opts.data = { filename };
const response = await client.delete(`/api/function/${functionId}/file/${fileId}`, opts);
if (response.status === 200) return true;
throw new Error(`Unexpected status ${response.status}`);
}
export async function renameFile(client: ApiClient, functionId: string, fileId: string, newFilename: string) {
const response = await client.patch(
`/api/function/${functionId}/file/${fileId}/rename`,
{ newFilename },
);
if (response.status === 200) return response.data;
throw new Error(`Unexpected status ${response.status}`);
}
export function handleAxiosError(error: any) {
if (error.response) {
// If the server signals a CLI file failure, stop further commands.
if (error.response.data?.cli_file_fail) {
console.error(`${chalk.red("✗")} ${error.response.data.message || "File operation blocked by server. (Probably because git is configured)"}`);
return { errorType: "cli_file_fail", reason: error.response.data.cli_file_fail, details: error.response };
}
if (error.response.status === 404) {
console.error(`${chalk.red("✗")} Not found.`);
return { errorType: "notfound" };
}
console.error(`${chalk.red("✗")} ${error.response.data?.message || error.response.statusText}`);
return { errorType: "response", details: error.response };
}
if (error.request) {
console.error(`${chalk.red("✗")} No response received from server.`);
return { errorType: "norequest" };
}
console.error(`${chalk.red("✗")} Error: ${error.message}`);
return { errorType: "other" };
}
export default {
listFiles,
createOrUpdateFile,
deleteFile,
renameFile,
handleAxiosError,
};