Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 57 additions & 1 deletion src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ interface CLIArgs {
serverUrl?: string;
logout?: boolean;
listPlans?: boolean;
env?: string[];
envFile?: string[];
ignore?: string[];
quiet?: boolean;
verbose?: boolean;
json?: boolean;
dryRun?: boolean;
}

const parsePlan = (planType: string): Plans | undefined => {
Expand Down Expand Up @@ -125,7 +132,56 @@ const optionsDefinition: ArgumentConfig<CLIArgs> = {
alias: 'u',
optional: true
},
confDir: { type: String, alias: 'c', optional: true }
confDir: { type: String, alias: 'c', optional: true },
env: {
type: String,
alias: 'E',
optional: true,
multiple: true,
description:
'Set environment variable (can be used multiple times): -E KEY=VALUE'
},
envFile: {
type: String,
optional: true,
multiple: true,
description:
'Path to .env file (can be used multiple times): --envFile .env.production'
},
ignore: {
type: String,
alias: 'I',
optional: true,
multiple: true,
description:
'Ignore pattern for files (can be used multiple times): -I "*.log" -I "node_modules"'
},
quiet: {
type: Boolean,
alias: 'q',
defaultValue: false,
optional: true,
description: 'Suppress non-essential output'
},
verbose: {
type: Boolean,
alias: 'V',
defaultValue: false,
optional: true,
description: 'Show detailed output for debugging'
},
json: {
type: Boolean,
defaultValue: false,
optional: true,
description: 'Output results in JSON format (useful for scripting)'
},
dryRun: {
type: Boolean,
defaultValue: false,
optional: true,
description: 'Show what would be deployed without actually deploying'
}
};

const parseOptions: ParseOptions<CLIArgs> = {
Expand Down
121 changes: 110 additions & 11 deletions src/cli/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,134 @@ import { Languages } from '@metacall/protocol/language';
import { ProtocolError } from '@metacall/protocol/protocol';
import chalk from 'chalk';

// Output mode configuration
let outputMode: 'normal' | 'quiet' | 'verbose' | 'json' = 'normal';

export const setOutputMode = (
mode: 'normal' | 'quiet' | 'verbose' | 'json'
): void => {
outputMode = mode;
};

export const getOutputMode = (): 'normal' | 'quiet' | 'verbose' | 'json' =>
outputMode;

export const isQuiet = (): boolean => outputMode === 'quiet';
export const isVerbose = (): boolean => outputMode === 'verbose';
export const isJson = (): boolean => outputMode === 'json';

/**
* Log informational message (suppressed in quiet mode)
*/
export const info = (message: string): void => {
if (isQuiet()) return;
if (isJson()) return;
// eslint-disable-next-line no-console
console.log(chalk.cyanBright.bold('i') + ' ' + chalk.cyan(message));
};

/**
* Log warning message (shown in all modes except json)
*/
export const warn = (message: string): void => {
if (isJson()) return;
// eslint-disable-next-line no-console
console.warn(chalk.yellowBright.bold('!') + ' ' + chalk.yellow(message));
};

export const error = (message: string, exitCode = 1): never => {
/**
* Log debug/verbose message (only shown in verbose mode)
*/
export const debug = (message: string): void => {
if (!isVerbose()) return;
// eslint-disable-next-line no-console
console.error(chalk.redBright.bold('X') + ' ' + chalk.red(message));
console.log(chalk.gray(' [debug] ' + message));
};

/**
* Log success message
*/
export const success = (message: string): void => {
if (isQuiet()) return;
if (isJson()) return;
// eslint-disable-next-line no-console
console.log(chalk.greenBright.bold('✓') + ' ' + chalk.green(message));
};

/**
* Log error and exit (always shown)
*/
export const error = (message: string, exitCode = 1): never => {
if (isJson()) {
// eslint-disable-next-line no-console
console.error(JSON.stringify({ error: message, exitCode }));
} else {
// eslint-disable-next-line no-console
console.error(chalk.redBright.bold('X') + ' ' + chalk.red(message));
}
return process.exit(exitCode);
};

/**
* Log API error and exit (always shown)
*/
export const apiError = (err: ProtocolError): never => {
// eslint-disable-next-line no-console
console.error(
chalk.redBright.bold('X') +
chalk.redBright(
` Server responded with error code: ${
err.response?.status || ''
} ${err.response?.data as string}`
)
);
const status = err.response?.status || 'unknown';
const data = err.response?.data as string;

if (isJson()) {
// eslint-disable-next-line no-console
console.error(
JSON.stringify({
error: 'API Error',
status,
message: data,
exitCode: 1
})
);
} else {
// eslint-disable-next-line no-console
console.error(
chalk.redBright.bold('X') +
chalk.redBright(
` Server responded with error code: ${status} ${data}`
)
);
}
return process.exit(1);
};

/**
* Output JSON data (only in json mode)
*/
export const jsonOutput = (data: unknown): void => {
if (!isJson()) return;
// eslint-disable-next-line no-console
console.log(JSON.stringify(data, null, 2));
};

/**
* Format language name with color
*/
export const printLanguage = (language: LanguageId): string =>
chalk
.hex(Languages[language].hexColor)
.bold(Languages[language].displayName);

/**
* Print a styled header
*/
export const header = (text: string): void => {
if (isQuiet() || isJson()) return;
// eslint-disable-next-line no-console
console.log('\n' + chalk.bold.underline(text) + '\n');
};

/**
* Print a step indicator
*/
export const step = (stepNum: number, total: number, message: string): void => {
if (isQuiet() || isJson()) return;
// eslint-disable-next-line no-console
console.log(chalk.dim(`[${stepNum}/${total}]`) + ' ' + message);
};
25 changes: 18 additions & 7 deletions src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import Progress from './cli/progress';
import { languageSelection, listSelection } from './cli/selection';
import { logs } from './logs';
import { isInteractive } from './tty';
import { getEnv, loadFilesToRun, zip } from './utils';
import { filterFiles, getEnv, loadFilesToRun, zip } from './utils';
import { debug } from './cli/messages';

export enum ErrorCode {
Ok = 0,
Expand All @@ -42,15 +43,26 @@ export const deployPackage = async (
let descriptor = await generatePackage(rootPath);

const deploy = async (additionalJsons: MetaCallJSON[]) => {
// TODO: We should cache the plan and ask for it only once

const descriptor = await generatePackage(rootPath);

// Apply ignore patterns if specified
const filesToDeploy = filterFiles(descriptor.files, args['ignore']);

if (args['ignore']?.length) {
const ignoredCount =
descriptor.files.length - filesToDeploy.length;
if (ignoredCount > 0) {
debug(
`Ignored ${ignoredCount} file(s) based on ignore patterns`
);
}
}

const { progress, pulse, hide } = Progress();

const archive = await zip(
rootPath,
descriptor.files,
filesToDeploy,
progress,
pulse,
hide
Expand All @@ -65,8 +77,7 @@ export const deployPackage = async (
descriptor.runners
);

// TODO: We can ask for environment variables here too and cache them
const env = await getEnv(rootPath);
const env = await getEnv(rootPath, args['env'], args['envFile']);

info(`Deploying ${rootPath}...\n`);

Expand Down Expand Up @@ -243,7 +254,7 @@ export const deployFromRepository = async (

const name = (await api.add(url, selectedBranch, [])).id;

const env = await getEnv();
const env = await getEnv(undefined, args['env'], args['envFile']);

const deploy = await api.deploy(
name,
Expand Down
78 changes: 57 additions & 21 deletions src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,63 @@ import { ErrorCode } from './deploy';
const helpText = `
Official CLI for metacall-deploy

Usage: metacall-deploy [--args]

Options
Alias Flag Accepts Description
-h --help string Prints a user manual to assist you in using the CLI.
-v --version nothing Prints current version of the CLI.
-a --addrepo string Deploy from repository.
-w --workdir string Accepts path to application directory.
-d --dev nothing Run CLI in dev mode (deploy locally to metacall/faas).
-n --projectName string Accepts name of the application.
-e --email string Accepts email id for authentication.
-p --password string Accepts password for authentication.
-t --token string Accepts token for authentication, either pass email & password or token.
-f --force nothing Accepts boolean value: it deletes the deployment present on an existing plan and deploys again.
-P --plan string Accepts type of plan: "Essential", "Standard", "Premium".
-i --inspect string Lists out all the deployments with specifications (it defaults to Table format, otherwise they are serialized into specified format: Table | Raw | OpenAPI).
-D --delete nothing Accepts boolean value: it provides you all the available deployment options to delete.
-l --logout nothing Accepts boolean value: use it in order to expire your current session.
-r --listPlans nothing Accepts boolean value: list all the plans that are offered in your account using it.
-u --serverUrl string Change the base URL for the FaaS.
-c --confDir string Overwrite the default configuration directory.`;
Usage: metacall-deploy [options]

Authentication Options:
-e, --email <email> Email for authentication
-p, --password <pass> Password for authentication
-t, --token <token> API token for authentication (alternative to email/password)
-l, --logout Expire current session

Deployment Options:
-w, --workdir <path> Path to application directory (default: current directory)
-a, --addrepo <url> Deploy from a Git repository URL
-n, --projectName <name> Name for the deployment (default: directory name)
-P, --plan <plan> Plan type: "Essential", "Standard", or "Premium"
-f, --force Delete existing deployment and redeploy
--dryRun Show what would be deployed without deploying

Environment Variables:
-E, --env <KEY=VALUE> Set environment variable (can be repeated)
--envFile <path> Load environment variables from file (can be repeated)

File Filtering:
-I, --ignore <pattern> Ignore files matching pattern (can be repeated)
Examples: -I "*.log" -I "node_modules"

Output Options:
-q, --quiet Suppress non-essential output
-V, --verbose Show detailed debug output
--json Output results in JSON format (for scripting)
-i, --inspect [format] List deployments (Table | Raw | OpenAPIv3)

Management:
-D, --delete Interactively select and delete a deployment
-r, --listPlans List available subscription plans

Advanced:
-d, --dev Run in dev mode (deploy to local metacall/faas)
-u, --serverUrl <url> Override the FaaS base URL
-c, --confDir <path> Override the configuration directory

General:
-v, --version Show CLI version
-h, --help Show this help message

Examples:
$ metacall-deploy Deploy current directory
$ metacall-deploy -w ./my-app Deploy specific directory
$ metacall-deploy -a https://github.com/... Deploy from repository
$ metacall-deploy -E DB_HOST=localhost Deploy with environment variable
$ metacall-deploy --envFile .env.prod Deploy with env file
$ metacall-deploy -I "*.test.js" -I ".git" Deploy ignoring test files
$ metacall-deploy --dryRun Preview deployment without deploying
$ metacall-deploy -i List all deployments
$ metacall-deploy -i Raw List deployments in raw format
$ metacall-deploy --json -i List deployments as JSON

For more information, visit: https://github.com/metacall/deploy
`;

export const printHelp = (): void => {
console.log(helpText);
Expand Down
Loading