Initialize SHSF CLI project with core structure, configuration, and command handling

This commit is contained in:
Space-Banane
2026-03-18 15:47:45 +01:00
commit 6831669de7
10 changed files with 337 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
import axios, { AxiosInstance } from 'axios';
import { loadConfig } from './config.js';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pkg = require('../package.json');
let apiClient: AxiosInstance | null = null;
export async function getApiClient(): Promise<AxiosInstance> {
if (apiClient) return apiClient;
const config = await loadConfig();
apiClient = axios.create({
baseURL: config.SHSF_INSTANCE,
headers: {
'x-access-key': config.SHSF_TOKEN,
'Content-Type': 'application/json',
"User-Agent": "SHSF-CLI/" + pkg.version
},
});
return apiClient;
}
+64
View File
@@ -0,0 +1,64 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { program } from "./index.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
function getCommandFiles(dir: string): string[] {
const files = fs.readdirSync(dir);
let commandFiles: string[] = [];
for (const file of files) {
const fullPath = path.join(dir, file);
if (fs.statSync(fullPath).isDirectory()) {
commandFiles = commandFiles.concat(getCommandFiles(fullPath));
} else if (file.endsWith('.ts') || file.endsWith('.js')) {
commandFiles.push(fullPath);
}
}
return commandFiles;
}
export async function resolveCommands() {
const commandsPath = path.join(__dirname, 'commands');
if (!fs.existsSync(commandsPath)) return;
const commandFiles = getCommandFiles(commandsPath);
// Group commands by their directory relative to 'commands'
const groupMap = new Map<string, any>();
groupMap.set('', program);
for (const absolutePath of commandFiles) {
const relativePathFromCommands = path.relative(commandsPath, absolutePath);
const pathParts = path.dirname(relativePathFromCommands).split(path.sep).filter(p => p !== '.');
let currentParent = program;
let currentGroupPath = '';
for (const part of pathParts) {
currentGroupPath = currentGroupPath ? path.join(currentGroupPath, part) : part;
if (!groupMap.has(currentGroupPath)) {
const groupCommand = currentParent.command(part).description(`${part} commands`);
groupMap.set(currentGroupPath, groupCommand);
}
currentParent = groupMap.get(currentGroupPath);
}
const relativeImportPath = './' + path.relative(__dirname, absolutePath).replace(/\\/g, '/');
const module = await import(relativeImportPath);
// Look for a definition object (e.g., healthDefinition) or a default export with name/description/action
const definition = module.default || Object.values(module).find((val: any) => val && val.name && val.action);
if (definition && definition.name && definition.action) {
currentParent
.command(definition.name)
.description(definition.description || '')
.action(definition.action);
}
}
}
+52
View File
@@ -0,0 +1,52 @@
import chalk from "chalk";
import { getApiClient } from "../api.js";
export const healthDefinition = {
name: "health",
description: "Perform a health check on the current SHSF instance.",
action: async () => {
await healthCheck();
},
};
async function healthCheck() {
const client = await getApiClient();
try {
const response = await client.get("/health");
if (response.status === 200 && response.data.status === "OK") {
console.log(
`${chalk.green("✓")} SHSF Status: ${chalk.bgGreen.black(" HEALTHY ")}`,
);
} else {
console.log(
`${chalk.yellow("!")} SHSF Status: ${chalk.bgYellow.black(" UNEXPECTED RESPONSE ")}`,
);
console.log(`Status Code: ${chalk.blue(response.status)}`);
console.log(`Response Data: ${JSON.stringify(response.data)}`);
}
} catch (error: any) {
if (error.response) {
console.error(
`${chalk.red("✗")} System Status: ${chalk.bgRed.black(" UNHEALTHY ")}`,
);
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("✗")} SHSF Status: ${chalk.bgRed.black(" DISCONNECTED ")}`,
);
console.error(
`Error: ${chalk.yellow("Could not connect to the SHSF instance. Check your connection or SHSF_INSTANCE URL.")}`,
);
} else {
console.error(
`${chalk.red("✗")} SHSF Status: ${chalk.bgRed.black(" LOCAL ERROR ")}`,
);
console.error(`Error: ${chalk.yellow(error.message)}`);
}
}
}
+9
View File
@@ -0,0 +1,9 @@
import chalk from "chalk";
export const helloworldDefinition = {
name: "helloworld",
description: "Prints Hello, World! to the terminal.",
action: async () => {
console.log(`${chalk.green("✓")} ${chalk.bgGreen.black(" Hello, World! ")}`);
},
};
+80
View File
@@ -0,0 +1,80 @@
import os from 'os';
import path from 'path';
import fs from 'fs';
import dotenv from 'dotenv';
import chalk from 'chalk';
import inquirer from 'inquirer';
export interface SHSFConfig {
SHSF_INSTANCE: string;
SHSF_TOKEN: string;
}
const CONFIG_FILE_NAME = '.shsf_config';
export function getConfigPath(): string {
return path.join(os.homedir(), CONFIG_FILE_NAME);
}
async function promptForConfig(): Promise<SHSFConfig> {
console.log(chalk.cyan('\nSetting up SHSF configuration...\n'));
const answers = await inquirer.prompt([
{
type: 'input',
name: 'SHSF_INSTANCE',
message: 'Enter your SHSF instance URL (e.g., https://your-instance-api.yourdomain.com):',
validate: (input: string) => {
try {
new URL(input);
return true;
} catch {
return 'Please enter a valid URL (including http(s)://)';
}
},
},
{
type: 'password',
name: 'SHSF_TOKEN',
message: 'Enter your SHSF access token (masked):',
validate: (input: string) => input.trim().length > 0 || 'Token cannot be empty',
},
]);
const configContent = `# SHSF Configuration\nSHSF_INSTANCE=${answers.SHSF_INSTANCE}\nSHSF_TOKEN=${answers.SHSF_TOKEN}\n`;
const configPath = getConfigPath();
try {
fs.writeFileSync(configPath, configContent);
console.log(chalk.green(`\n✓ Configuration saved to ${configPath}\n`));
} catch (error: any) {
console.error(chalk.red(`\nError saving configuration: ${error.message}\n`));
process.exit(1);
}
return answers as SHSFConfig;
}
export async function loadConfig(): Promise<SHSFConfig> {
const configPath = getConfigPath();
if (!fs.existsSync(configPath)) {
return await promptForConfig();
}
const configContent = fs.readFileSync(configPath, 'utf-8');
const parsed = dotenv.parse(configContent);
const instance = parsed.SHSF_INSTANCE;
const token = parsed.SHSF_TOKEN;
if (!instance || !token) {
console.error(chalk.yellow(`Missing required configuration in ${configPath}`));
return await promptForConfig();
}
return {
SHSF_INSTANCE: instance,
SHSF_TOKEN: token,
};
}
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env node
import { Command } from 'commander';
import chalk from 'chalk';
import { resolveCommands } from './commands.js';
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const pckg = require('../package.json');
export const program = new Command();
program
.name('shsf')
.description('SHSF CLI tool to manage your serverless functions.')
.version(pckg.version);
await resolveCommands();
program
.on('command:*', () => {
console.error(chalk.red(`\nInvalid command: ${program.args.join(' ')}\n`));
process.exit(1);
});
program.parse(process.argv);
if (!process.argv.slice(2).length) {
program.outputHelp();
}