Initialize SHSF CLI project with core structure, configuration, and command handling
This commit is contained in:
+25
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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! ")}`);
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user