Skip to content
Closed
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
84 changes: 84 additions & 0 deletions cli/cli-auth-plugins/calmhub-curl.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# Simple wrapper around curl that offers Kerberos authentication (--negotiate -u:)
# Usage: calmhub-curl.sh [-m METHOD] URL
# If METHOD is POST, the request body is read from stdin.

show_help() {
cat <<EOF
Usage: $0 [options] URL
Options:
-m, --method METHOD HTTP method to use (default: GET)
-h, --help Show this help message

Examples:
echo '{"a":1}' | $0 -m POST https://example.com/api
$0 https://example.com

Notes:
- Output goes to stdout. Errors are written to stderr.
- Failure will result in a non-zero exit code.
- Cookies are stored in /var/tmp/calmhub_cookies.\$(id -un) as -rw-------, i.e. current user only.
EOF
}

# Defaults
method=GET
content_type=application/json

# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-m|--method)
if [[ -n $2 ]]; then
method="$2"
shift 2
else
echo "Error: --method requires an argument." >&2
exit 1
fi
;;
-h|--help)
show_help
exit 0
;;
*)
url="$1"
shift
;;
esac
done

if [[ -z "$url" ]]; then
echo "Error: URL is required." >&2
show_help
exit 1
fi

# Normalize method to uppercase
method=$(echo "$method" | tr '[:lower:]' '[:upper:]')

COOKIE_JAR=/var/tmp/calmhub_cookies.$(id -un)
CURL_OPTS="--silent --fail-with-body --location-trusted --negotiate -u: -b $COOKIE_JAR -c $COOKIE_JAR -w %{stderr}%{http_code}"

# Ensure cookie jar file exists with secure permissions (current user ONLY)
old_umask=$(umask)
umask 0077
touch "$COOKIE_JAR"
umask "$old_umask"

# Execute curl. For POST, read body from stdin.
if [[ "$method" == "POST" ]]; then
if [ -t 0 ]; then
# No stdin data; send empty payload
curl ${CURL_OPTS} -X "$method" -H "Content-Type: application/json" --data-binary @- "$url" < /dev/null
exit $?
else
# Pipe actual stdin to curl
curl ${CURL_OPTS} -X "$method" -H "Content-Type: application/json" --data-binary @- "$url"
exit $?
fi
else
# Methods without body
curl ${CURL_OPTS} "$url"
exit $?
fi
3 changes: 2 additions & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@
"calm": "dist/index.js"
},
"scripts": {
"build": "tsup && npm run copy-calm-schema && npm run copy-docify-templates && npm run copy-widgets && npm run copy-ai-tools",
"build": "tsup && npm run copy-calm-schema && npm run copy-docify-templates && npm run copy-widgets && npm run copy-ai-tools && npm run copy-auth-plugins",
"watch": "node watch.mjs",
"copy-calm-schema": "copyfiles \"../calm/release/**/meta/*\" \"../calm/draft/**/meta/*\" dist/calm/",
"copy-docify-templates": "copyfiles \"../shared/dist/template-bundles/**/*\" dist --up 3",
"copy-widgets": "copyfiles \"../calm-widgets/dist/cli/widgets/**/*\" dist --up 4",
"copy-ai-tools": "copyfiles \"../calm-ai/tools/**/*\" \"../calm-ai/CALM.chatmode.md\" dist/calm-ai/ --up 2",
"copy-auth-plugins": "copyfiles \"./cli-auth-plugins/**/*\" dist/plugins/ --up 1",
"test": "vitest run",
"lint": "eslint src",
"lint-fix": "eslint src --fix",
Expand Down
11 changes: 11 additions & 0 deletions cli/src/cli-config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const exampleConfig = {
calmHubUrl: 'https://example.com/calmhub'
};

const exampleConfig2 = {
calmHubPlugin: '/my/plugin/script'
};

describe('cli-config', () => {
beforeEach(() => {
Expand All @@ -33,6 +36,14 @@ describe('cli-config', () => {
expect(config).toEqual(exampleConfig);
});

it('loads second user config from .calm.json in home dir', async () => {
vol.fromJSON({
'/home/user/.calm.json': JSON.stringify(exampleConfig2)
});
const config = await loadCliConfig();
expect(config).toEqual(exampleConfig2);
});

it('returns null when .calm.json does not exist', async () => {
const config = await loadCliConfig();
expect(config).toBeNull();
Expand Down
3 changes: 2 additions & 1 deletion cli/src/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { homedir } from 'os';
import { join } from 'path';

export interface CLIConfig {
calmHubUrl?: string
calmHubUrl?: string,
calmHubPlugin?: string
}

function getUserConfigLocation(): string {
Expand Down
72 changes: 72 additions & 0 deletions cli/src/cli.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,25 @@ let serverModule: typeof import('./server/cli-server');
let templateModule: typeof import('./command-helpers/template');
let optionsModule: typeof import('./command-helpers/generate-options');
let fileSystemDocLoaderModule: typeof import('@finos/calm-shared/dist/document-loader/file-system-document-loader');
let cliConfigModule: typeof import('./cli-config');
let setupCLI: typeof import('./cli').setupCLI;
let parseDocumentLoaderConfig: typeof import('./cli').parseDocumentLoaderConfig;

describe('CLI Commands', () => {
let program: Command;

beforeEach(async () => {
vi.resetModules();
vi.clearAllMocks();
vi.unstubAllEnvs();

calmShared = await import('@finos/calm-shared');
validateModule = await import('./command-helpers/validate');
serverModule = await import('./server/cli-server');
templateModule = await import('./command-helpers/template');
optionsModule = await import('./command-helpers/generate-options');
fileSystemDocLoaderModule = await import('@finos/calm-shared/dist/document-loader/file-system-document-loader');
cliConfigModule = await import('./cli-config');

vi.spyOn(calmShared, 'runGenerate').mockResolvedValue(undefined);
vi.spyOn(calmShared.TemplateProcessor.prototype, 'processTemplate').mockResolvedValue(undefined);
Expand All @@ -45,8 +49,11 @@ describe('CLI Commands', () => {
vi.spyOn(fileSystemDocLoaderModule, 'FileSystemDocumentLoader').mockImplementation(vi.fn());
vi.spyOn(fileSystemDocLoaderModule.FileSystemDocumentLoader.prototype, 'loadMissingDocument').mockResolvedValue({});

vi.spyOn(cliConfigModule, 'loadCliConfig').mockImplementation(vi.fn());

const cliModule = await import('./cli');
setupCLI = cliModule.setupCLI;
parseDocumentLoaderConfig = cliModule.parseDocumentLoaderConfig;

program = new Command();
setupCLI(program);
Expand Down Expand Up @@ -337,4 +344,69 @@ describe('CLI Commands', () => {
});
});

describe('Document Loader options', () => {
it('config when no options selected', async () => {
const config = await parseDocumentLoaderConfig({
});
expect(config.calmHubUrl).toBeUndefined();
expect(config.calmHubPlugin).toBeUndefined();
expect(config.schemaDirectoryPath).toBeUndefined();
expect(config.debug).toBeFalsy();
});

it('config with CalmHub defined in config file', async () => {
vi.spyOn(cliConfigModule, 'loadCliConfig').mockImplementation(() => {
return {
calmHubUrl: 'calmhub.local',
calmHubPlugin: 'plugin-name'
};
});

const config = await parseDocumentLoaderConfig({});
expect(config.calmHubUrl).toBe('calmhub.local');
expect(config.calmHubPlugin).toBe('plugin-name');
});

it('config with CalmHub defined in config file overridden by options', async () => {
vi.spyOn(cliConfigModule, 'loadCliConfig').mockImplementation(() => {
return {
calmHubUrl: 'calmhub.local',
calmHubPlugin: 'plugin-name'
};
});

const config = await parseDocumentLoaderConfig({
calmHubUrl: 'override.local',
calmHubPlugin: 'override-plugin'
});
expect(config.calmHubUrl).toBe('override.local');
expect(config.calmHubPlugin).toBe('override-plugin');
});

it('config with CalmHub defined in config file overridden by environment', async () => {
vi.spyOn(cliConfigModule, 'loadCliConfig').mockImplementation(() => {
return {
calmHubUrl: 'calmhub.local',
calmHubPlugin: 'plugin-name'
};
});

vi.stubEnv('CALM_HUB_URL', 'env.local');
vi.stubEnv('CALM_HUB_PLUGIN', 'env-plugin');
const config = await parseDocumentLoaderConfig({});
expect(config.calmHubUrl).toBe('env.local');
expect(config.calmHubPlugin).toBe('env-plugin');
});

it('config with CalmHub defined in environment overridden by options', async () => {
vi.stubEnv('CALM_HUB_URL', 'env.local');
vi.stubEnv('CALM_HUB_PLUGIN', 'env-plugin');
const config = await parseDocumentLoaderConfig({
calmHubUrl: 'calmhub.local',
calmHubPlugin: 'plugin-name'
});
expect(config.calmHubUrl).toBe('calmhub.local');
expect(config.calmHubPlugin).toBe('plugin-name');
});
});
});
40 changes: 36 additions & 4 deletions cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const VERBOSE_OPTION = '-v, --verbose';
// Generate command options
const PATTERN_OPTION = '-p, --pattern <file>';
const CALMHUB_URL_OPTION = '-c, --calm-hub-url <url>';
const CALMHUB_PLUGIN_OPTION = '--calm-hub-plugin <path>';

// Validate command options
const FORMAT_OPTION = '-f, --format <format>';
Expand Down Expand Up @@ -43,6 +44,7 @@ export function setupCLI(program: Command) {
.requiredOption(OUTPUT_OPTION, 'Path location at which to output the generated file.', 'architecture.json')
.option(SCHEMAS_OPTION, 'Path to the directory containing the meta schemas to use.')
.option(CALMHUB_URL_OPTION, 'URL to CALMHub instance')
.option(CALMHUB_PLUGIN_OPTION, 'Plugin to support custom CALMHub access')
.option(VERBOSE_OPTION, 'Enable verbose logging.', false)
.action(async (options) => {
const debug = !!options.verbose;
Expand All @@ -67,6 +69,8 @@ export function setupCLI(program: Command) {
.default('json')
)
.option(OUTPUT_OPTION, 'Path location at which to output the generated file.')
.option(CALMHUB_URL_OPTION, 'URL to CALMHub instance')
.option(CALMHUB_PLUGIN_OPTION, 'Plugin to support custom CALMHub access')
.option(VERBOSE_OPTION, 'Enable verbose logging.', false)
.action(async (options) => {
const { checkValidateOptions, runValidate } = await import('./command-helpers/validate');
Expand All @@ -78,7 +82,9 @@ export function setupCLI(program: Command) {
verbose: !!options.verbose,
strict: options.strict,
outputFormat: options.format,
outputPath: options.output
outputPath: options.output,
calmHubUrl: options.calmHubUrl,
calmHubPlugin: options.calmHubPlugin
});
});

Expand All @@ -87,6 +93,8 @@ export function setupCLI(program: Command) {
.description('Start a HTTP server to proxy CLI commands. (experimental)')
.option(PORT_OPTION, 'Port to run the server on', '3000')
.requiredOption(SCHEMAS_OPTION, 'Path to the directory containing the meta schemas to use.')
.option(CALMHUB_URL_OPTION, 'URL to CALMHub instance')
.option(CALMHUB_PLUGIN_OPTION, 'Plugin to support custom CALMHub access')
.option(VERBOSE_OPTION, 'Enable verbose logging.', false)
.action(async (options) => {
const { startServer } = await import('./server/cli-server');
Expand Down Expand Up @@ -223,15 +231,39 @@ export async function parseDocumentLoaderConfig(options): Promise<DocumentLoader
const logger = initLogger(options.verbose, 'calm-cli');
const docLoaderOpts: DocumentLoaderOptions = {
calmHubUrl: options.calmHubUrl,
calmHubPlugin: options.calmHubPlugin,
schemaDirectoryPath: options.schemaDirectory,
debug: !!options.verbose
};

const userConfig = await loadCliConfig();
if (userConfig && userConfig.calmHubUrl && !options.calmHubUrl) {
logger.info('Using CALMHub URL from config file: ' + userConfig.calmHubUrl);
docLoaderOpts.calmHubUrl = userConfig.calmHubUrl;

// Priority:
// HIGHEST: Command line options
// MEDIUM: Environment variables
// LOWEST: Config file
if (!docLoaderOpts.calmHubUrl) {
if (process.env.CALM_HUB_URL) {
logger.info('Using CALMHub URL from environment variable: ' + process.env.CALM_HUB_URL);
docLoaderOpts.calmHubUrl = process.env.CALM_HUB_URL;
}
else if (userConfig && userConfig.calmHubUrl) {
logger.info('Using CALMHub URL from config file: ' + userConfig.calmHubUrl);
docLoaderOpts.calmHubUrl = userConfig.calmHubUrl;
}
}

if (!docLoaderOpts.calmHubPlugin) {
if (process.env.CALM_HUB_PLUGIN) {
logger.info('Using CALMHub Plugin from environment variable: ' + process.env.CALM_HUB_PLUGIN);
docLoaderOpts.calmHubPlugin = process.env.CALM_HUB_PLUGIN;
}
else if (userConfig && userConfig.calmHubPlugin) {
logger.info('Using CALMHub Plugin from config file: ' + userConfig.calmHubPlugin);
docLoaderOpts.calmHubPlugin = userConfig.calmHubPlugin;
}
}

return docLoaderOpts;
}

Expand Down
2 changes: 2 additions & 0 deletions cli/src/command-helpers/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface ValidateOptions {
strict: boolean;
outputFormat: ValidateOutputFormat;
outputPath: string;
calmHubUrl?: string;
calmHubPlugin?: string;
}

export async function runValidate(options: ValidateOptions) {
Expand Down
3 changes: 2 additions & 1 deletion shared/src/consts.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const CALM_META_SCHEMA_DIRECTORY = __dirname + '/calm';
export const CALM_META_SCHEMA_DIRECTORY = __dirname + '/calm';
export const CALM_AUTH_PLUGIN_DIRECTORY = __dirname + '/plugins';
Loading
Loading