diff --git a/calm-ai/tools/calm-cli-instructions.md b/calm-ai/tools/calm-cli-instructions.md index bd1ba90ba..57b32bb5a 100644 --- a/calm-ai/tools/calm-cli-instructions.md +++ b/calm-ai/tools/calm-cli-instructions.md @@ -21,7 +21,7 @@ Run `calm` with no arguments to see the top-level help: calm ``` -This displays available commands such as `generate`, `validate`, `init-ai`, `server`, `template`, and `docify`. +This displays available commands such as `generate`, `validate`, `init-ai`, `template`, and `docify`. ## Generate Architectures from Patterns @@ -134,19 +134,6 @@ At present Github Copilot (`copilot`), AWS Kiro (`kiro`), and Claude Code (`clau This generates custom prompts for the specified to use CALM-aware tools (nodes, relationships, interfaces, controls, flows, patterns, metadata). -## CLI Server (Experimental) - -Expose CLI functionality over HTTP: - -```shell -calm server --schema-directory -``` - -Endpoints (default `http://127.0.0.1:3000`): - -- `GET /health` for health checks -- `POST /calm/validate` with a CALM model payload to validate - ## Template Command Generate arbitrary files from CALM models using Handlebars bundles: diff --git a/cli/AGENTS.md b/cli/AGENTS.md index 4a7af9492..7c371ba90 100644 --- a/cli/AGENTS.md +++ b/cli/AGENTS.md @@ -48,9 +48,8 @@ npm run copy-ai-tools # Copy AI agent files from ../calm-ai/ 1. **generate** - Generate architecture from CALM pattern 2. **validate** - Validate architecture against pattern 3. **init-ai** - Install AI Assistant support for CALM -4. **server** - HTTP server proxy (experimental) -5. **template** - Generate files from Handlebars templates -6. **docify** - Generate documentation websites (supports `--scaffold` for two-stage workflow) +4. **template** - Generate files from Handlebars templates +5. **docify** - Generate documentation websites (supports `--scaffold` for two-stage workflow) ### Important Directories ``` @@ -59,7 +58,6 @@ src/ ├── cli-config.ts # Configuration helpers ├── index.ts # Entry point ├── command-helpers/ # Shared utilities for commands -├── server/ # HTTP server implementation (experimental) └── test_helpers/ # Test utilities ``` diff --git a/cli/README.md b/cli/README.md index a8339ab6b..3f9220d9d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -33,7 +33,6 @@ Options: Commands: generate [options] Generate an architecture from a CALM pattern file. validate [options] Validate that an architecture conforms to a given CALM pattern. - server [options] Start a HTTP server to proxy CLI commands. (experimental) template [options] Generate files from a CALM model using a Handlebars template bundle. docify [options] Generate a documentation website off your CALM model. init-ai [options] Augment a git repository with AI assistance for CALM @@ -240,32 +239,6 @@ WARN issues: which is just letting you know that you have left in some placeholder values which might have been generated with the generate command. This isn't a full break, but it implies that you've forgotten to fill out a detail in your architecture. -## Calm CLI server (Experimental) - -It may be required to have the operations of the CALM CLI available over rest. -The `validate` command has been made available over an API. - -```shell -calm server --schema-directory calm -``` - -Note that CalmHub can be used by setting the `-c, --calm-hub-url` argument. - -```shell -curl http://127.0.0.1:3000/health - -# Missing schema key -curl -H "Content-Type: application/json" -X POST http://127.0.0.1:3000/calm/validate --data @cli/test_fixtures/validation_route/invalid_api_gateway_instantiation_missing_schema_key.json - -# Schema value is invalid -curl -H "Content-Type: application/json" -X POST http://127.0.0.1:3000/calm/validate --data @cli/test_fixtures/validation_route/invalid_api_gateway_instantiation_schema_points_to_missing_schema.json - -# instantiation is valid -curl -H "Content-Type: application/json" -X POST http://127.0.0.1:3000/calm/validate --data @cli/test_fixtures/validation_route/valid_instantiation.json - - -``` - ## CALM init-ai The `init-ai` command sets up AI-powered development assistance for CALM architecture modeling by configuring a specialized VSCode agent with comprehensive tool prompts. At present two AI Assistant providers are supported: Github Copilot and AWS Kiro. diff --git a/cli/package.json b/cli/package.json index a6213490d..48996fe45 100644 --- a/cli/package.json +++ b/cli/package.json @@ -43,7 +43,6 @@ "commander": "^14.0.0", "copyfiles": "^2.4.1", "execa": "^9.6.0", - "express-rate-limit": "^8.0.0", "mkdirp": "^3.0.1", "ts-node": "10.9.2" }, @@ -52,13 +51,11 @@ "@semantic-release/exec": "^7.1.0", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.0", - "@types/supertest": "^6.0.3", "@types/xml2js": "^0.4.14", "axios": "^1.13.5", "chokidar": "^4.0.3", "semantic-release": "^25.0.0", - "supertest": "^7.1.0", "tsup": "^8.4.0", "xml2js": "^0.6.2" } -} +} \ No newline at end of file diff --git a/cli/src/cli.e2e.spec.ts b/cli/src/cli.e2e.spec.ts index be3a48279..791e7d9ca 100644 --- a/cli/src/cli.e2e.spec.ts +++ b/cli/src/cli.e2e.spec.ts @@ -4,9 +4,7 @@ import * as fs from 'fs'; import * as os from 'os'; import { execa } from 'execa'; import { parseStringPromise } from 'xml2js'; -import axios from 'axios'; import { expectDirectoryMatch, expectFilesMatch } from '@finos/calm-shared'; -import { spawn } from 'node:child_process'; import { STATIC_GETTING_STARTED_MAPPING_PATH } from './test_helpers/getting-started-url-mapping'; const millisPerSecond = 1000; @@ -436,60 +434,6 @@ describe('CLI Integration Tests', () => { expect(actual).toEqual(expected); }); - test('server command starts and responds to /health', async () => { - const schemaDir = path.resolve(__dirname, '../../calm'); - const serverProcess = spawn( - calm(), ['server', '--port', '3002', '--schema-directory', schemaDir], - { - cwd: tempDir, - stdio: 'inherit', - detached: true, - } - ); - await new Promise((r) => setTimeout(r, 5 * millisPerSecond)); - try { - const res = await axios.get('http://127.0.0.1:3002/health'); - expect(res.status).toBe(200); - expect(res.data.status).toBe('OK'); - } finally { - if (serverProcess.pid) process.kill(-serverProcess.pid); - } - }); - - test('server command starts and validates an architecture', async () => { - const schemaDir = path.resolve(__dirname, '../test_fixtures/api-gateway'); - const serverProcess = spawn( - calm(), ['server', '--port', '3003', '--schema-directory', schemaDir], - { - cwd: tempDir, - stdio: 'inherit', - detached: true, - } - ); - - const validArchitecture = fs.readFileSync( - path.join(__dirname, '../test_fixtures/validation_route/valid_instantiation.json'), - 'utf8' - ); - - await new Promise((r) => setTimeout(r, 5 * millisPerSecond)); - - try { - const res = await axios.post( - 'http://127.0.0.1:3003/calm/validate', - JSON.parse(validArchitecture), - { headers: { 'Content-Type': 'application/json' } } - ); - expect(res.status).toBe(201); - expect(JSON.stringify(res.data)).toContain('jsonSchemaValidationOutputs'); - expect(JSON.stringify(res.data)).toContain('spectralSchemaValidationOutputs'); - expect(JSON.stringify(res.data)).toContain('hasErrors'); - expect(JSON.stringify(res.data)).toContain('hasWarnings'); - } finally { - if (serverProcess.pid) process.kill(-serverProcess.pid); - } - }); - test('template command generates expected output', async () => { const fixtureDir = path.resolve(__dirname, '../test_fixtures/template'); const testModelPath = path.join( diff --git a/cli/src/cli.spec.ts b/cli/src/cli.spec.ts index d87e3c894..5d4356aa0 100644 --- a/cli/src/cli.spec.ts +++ b/cli/src/cli.spec.ts @@ -11,7 +11,6 @@ import { parseDocumentLoaderConfig } from './cli'; let calmShared: typeof import('@finos/calm-shared'); let validateModule: typeof import('./command-helpers/validate'); -let serverModule: typeof import('./server/cli-server'); let templateModule: typeof import('./command-helpers/template'); let optionsModule: typeof import('./command-helpers/generate-options'); let setupCLI: typeof import('./cli').setupCLI; @@ -27,7 +26,6 @@ describe('CLI Commands', () => { 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'); @@ -38,7 +36,6 @@ describe('CLI Commands', () => { vi.spyOn(validateModule, 'runValidate').mockResolvedValue(undefined); vi.spyOn(validateModule, 'checkValidateOptions').mockResolvedValue(undefined); - vi.spyOn(serverModule, 'startServer').mockImplementation(vi.fn()); vi.spyOn(templateModule, 'getUrlToLocalFileMap').mockReturnValue(new Map()); vi.spyOn(optionsModule, 'promptUserForOptions').mockResolvedValue([]); @@ -98,23 +95,6 @@ describe('CLI Commands', () => { }); }); - describe('Server Command', () => { - it('should call startServer with correct options', async () => { - await program.parseAsync([ - 'node', 'cli.js', 'server', - '--port', '4000', - '--schema-directory', 'mySchemas', - '--verbose', - ]); - - expect(serverModule.startServer).toHaveBeenCalledWith( - '4000', - expect.any(calmShared.SchemaDirectory), - true, - ); - }); - }); - describe('Template Command', () => { let processorConstructorSpy: MockInstance<(this: TemplateProcessor, inputPath: string, templateBundlePath: string, outputPath: string, urlToLocalPathMapping: Map, mode?: TemplateProcessingMode) => TemplateProcessor>; diff --git a/cli/src/cli.ts b/cli/src/cli.ts index f14879d6b..ba44dbb8a 100644 --- a/cli/src/cli.ts +++ b/cli/src/cli.ts @@ -21,9 +21,6 @@ const CALMHUB_URL_OPTION = '-c, --calm-hub-url '; const FORMAT_OPTION = '-f, --format '; const STRICT_OPTION = '--strict'; -// Server command options -const PORT_OPTION = '--port '; - // Template and Docify command options const BUNDLE_OPTION = '-b, --bundle '; const TEMPLATE_OPTION = '-t, --template '; @@ -105,22 +102,6 @@ Validation requires: }); }); - program - .command('server') - .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(VERBOSE_OPTION, 'Enable verbose logging.', false) - .option(CALMHUB_URL_OPTION, 'URL to CALMHub instance') - .action(async (options) => { - const { startServer } = await import('./server/cli-server'); - const debug = !!options.verbose; - const docLoaderOpts = await parseDocumentLoaderConfig(options); - const docLoader = buildDocumentLoader(docLoaderOpts); - const schemaDirectory = await buildSchemaDirectory(docLoader, debug); - startServer(options.port, schemaDirectory, debug); - }); - program .command('template') .description('Generate files from a CALM model using a template bundle, a single file, or a directory of templates') diff --git a/cli/src/server/cli-server.integration.spec.ts b/cli/src/server/cli-server.integration.spec.ts deleted file mode 100644 index e51c05f59..000000000 --- a/cli/src/server/cli-server.integration.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import request from 'supertest'; -import { startServer } from './cli-server'; -import { SchemaDirectory } from '@finos/calm-shared'; - -function getAvailablePort() { - // Use 0 to let the OS assign an available port - return String(0); -} - -describe('startServer (integration)', () => { - it('should start the server, respond to /health, and shut down', async () => { - const schemaDirectory = {} as SchemaDirectory; - const port = getAvailablePort(); - const serverInstance = startServer(port, schemaDirectory, false); - // Wait for server to be ready - await new Promise(res => setTimeout(res, 100)); - const address = serverInstance.address(); - // Type guard for address - if (!address || typeof address === 'string') { - throw new Error('Server did not start with a valid address'); - } - const baseUrl = `http://127.0.0.1:${address.port}`; - const agent = request(baseUrl); - const response = await agent.get('/health').timeout({ deadline: 3000 }); - expect(response.status).toBe(200); - serverInstance.close(); - }, 3000); -}); diff --git a/cli/src/server/cli-server.ts b/cli/src/server/cli-server.ts deleted file mode 100644 index 2dbec9950..000000000 --- a/cli/src/server/cli-server.ts +++ /dev/null @@ -1,18 +0,0 @@ -import express, { Application } from 'express'; -import { CLIServerRoutes } from './routes/routes'; -import { initLogger, SchemaDirectory } from '@finos/calm-shared'; -import { Server } from 'http'; - -export function startServer(port: string, schemaDirectory: SchemaDirectory, verbose: boolean): Server { - const app: Application = express(); - const cliServerRoutesInstance = new CLIServerRoutes(schemaDirectory, verbose); - const allRoutes = cliServerRoutesInstance.router; - - app.use(express.json()); - app.use('/', allRoutes); - - return app.listen(port, () => { - const logger = initLogger(verbose, 'calm-server'); - logger.info(`CALM Server is running on http://localhost:${port}`); - }); -} \ No newline at end of file diff --git a/cli/src/server/routes/health-route.spec.ts b/cli/src/server/routes/health-route.spec.ts deleted file mode 100644 index 4a7f3b70f..000000000 --- a/cli/src/server/routes/health-route.spec.ts +++ /dev/null @@ -1,27 +0,0 @@ -import request from 'supertest'; - -import express, { Application } from 'express'; -import { HealthRouter } from './health-route'; - -describe('HealthRouter', () => { - let app: Application; - - beforeEach(() => { - app = express(); - app.use(express.json()); - - const router: express.Router = express.Router(); - app.use('/health', router); - new HealthRouter(router); - - }); - - test('should return 200 for health check', async () => { - const response = await request(app) - .get('/health'); - expect(response.status).toBe(200); - expect(response.body).toEqual({ status: 'OK' }); - }); -}); - -// }); \ No newline at end of file diff --git a/cli/src/server/routes/health-route.ts b/cli/src/server/routes/health-route.ts deleted file mode 100644 index fdeff3c14..000000000 --- a/cli/src/server/routes/health-route.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Router, Request, Response } from 'express'; - - -export class HealthRouter { - - constructor(router: Router) { - router.get('/', this.healthCheck); - } - - private healthCheck(_req: Request, res: Response) { - res.status(200).type('json').send(new StatusResponse('OK')); - } - -} - -class StatusResponse { - status: string; - - constructor(status: string) { - this.status = status; - } -} \ No newline at end of file diff --git a/cli/src/server/routes/routes.spec.ts b/cli/src/server/routes/routes.spec.ts deleted file mode 100644 index 0e6617913..000000000 --- a/cli/src/server/routes/routes.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Router } from 'express'; -import { CLIServerRoutes } from './routes'; -import { ValidationRouter } from './validation-route'; -import { HealthRouter } from './health-route'; -import { SchemaDirectory } from '@finos/calm-shared'; - -const mockUse = vi.fn(); -const mockRouter = { - use: mockUse -}; - -vi.mock('express', () => ({ - Router: vi.fn(() => mockRouter) -})); - -vi.mock('./validation-route', () => { - return { - ValidationRouter: vi.fn() - }; -}); - -vi.mock('./health-route', () => { - return { - HealthRouter: vi.fn() - }; -}); - -vi.mock('@finos/calm-shared', () =>{ - return { - SchemaDirectory: vi.fn() - }; -}); -describe('CLIServerRoutes', () => { - const schemaDirectory = {} as SchemaDirectory; - let cliServerRoutes: CLIServerRoutes; - let mockRouter: Router; - - beforeEach(() => { - cliServerRoutes = new CLIServerRoutes(schemaDirectory); - mockRouter = cliServerRoutes.router; - }); - - it('should initialize router', () => { - expect(Router).toHaveBeenCalled(); - }); - - it('should set up validate route', () => { - expect(mockRouter.use).toHaveBeenCalledWith('/calm/validate', mockRouter); - expect(ValidationRouter).toHaveBeenCalled(); - }); - - it('should set up health route', () => { - expect(mockRouter.use).toHaveBeenCalledWith('/health', mockRouter); - expect(HealthRouter).toHaveBeenCalled(); - }); -}); \ No newline at end of file diff --git a/cli/src/server/routes/routes.ts b/cli/src/server/routes/routes.ts deleted file mode 100644 index 693ad9f22..000000000 --- a/cli/src/server/routes/routes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Router } from 'express'; -import { ValidationRouter } from './validation-route'; -import { HealthRouter } from './health-route'; -import { SchemaDirectory } from '@finos/calm-shared'; - -const HEALTH_ROUTE_PATH = '/health'; -const VALIDATE_ROUTE_PATH = '/calm/validate'; - -export class CLIServerRoutes { - router: Router; - - constructor(schemaDirectory: SchemaDirectory, debug: boolean = false) { - this.router = Router(); - const validateRoute = this.router.use(VALIDATE_ROUTE_PATH, this.router); - new ValidationRouter(validateRoute, schemaDirectory, debug); - const healthRoute = this.router.use(HEALTH_ROUTE_PATH, this.router); - new HealthRouter(healthRoute); - } -} \ No newline at end of file diff --git a/cli/src/server/routes/validation-route.spec.ts b/cli/src/server/routes/validation-route.spec.ts deleted file mode 100644 index 93f8ae0fb..000000000 --- a/cli/src/server/routes/validation-route.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import request from 'supertest'; -import * as fs from 'fs'; - -import express, { Application } from 'express'; -import { ValidationRouter } from './validation-route'; -import path from 'path'; -import { SchemaDirectory } from '@finos/calm-shared'; -import { FileSystemDocumentLoader } from '@finos/calm-shared/dist/document-loader/file-system-document-loader'; - -const schemaDirectoryPath: string = __dirname + '/../../../../calm/release'; -const apiGatewayPatternPath: string = - __dirname + '/../../../test_fixtures/api-gateway'; - -describe('ValidationRouter', () => { - let app: Application; - - beforeEach(() => { - app = express(); - app.use(express.json()); - - const router: express.Router = express.Router(); - new ValidationRouter( - router, - new SchemaDirectory( - new FileSystemDocumentLoader( - [schemaDirectoryPath, apiGatewayPatternPath], - false - ) - ) - ); - app.use('/calm/validate', router); - }); - - test('should return 400 when $schema is not specified', async () => { - const expectedFilePath = path.join( - __dirname, - '../../../test_fixtures/validation_route/invalid_api_gateway_instantiation_missing_schema_key.json' - ); - const invalidArchitectureMissingSchema = JSON.parse( - fs.readFileSync(expectedFilePath, 'utf-8') - ); - const response = await request(app) - .post('/calm/validate') - .send(invalidArchitectureMissingSchema); - - expect(response.status).toBe(400); - expect(response.body).toEqual({ - error: 'The "$schema" field is missing from the request body', - }); - }); - - test('should return 400 when the $schema specified in the instantiation is not found', async () => { - const expectedFilePath = path.join( - __dirname, - '../../../test_fixtures/validation_route/invalid_api_gateway_instantiation_schema_points_to_missing_schema.json' - ); - const invalidArchitectureMissingSchema = JSON.parse( - fs.readFileSync(expectedFilePath, 'utf-8') - ); - const response = await request(app) - .post('/calm/validate') - .send(invalidArchitectureMissingSchema); - - expect(response.status).toBe(400); - expect(response.body).toEqual({ - error: 'The "$schema" field referenced is not available to the server', - }); - }); - - test('should return 201 when the schema is valid', async () => { - const expectedFilePath = path.join( - __dirname, - '../../../test_fixtures/validation_route/valid_instantiation.json' - ); - const validArchitecture = JSON.parse( - fs.readFileSync(expectedFilePath, 'utf-8') - ); - const response = await request(app) - .post('/calm/validate') - .send(validArchitecture); - - expect(response.status).toBe(201); - expect(response.body).toHaveProperty('jsonSchemaValidationOutputs'); - expect(response.body).toHaveProperty('spectralSchemaValidationOutputs'); - expect(response.body).toHaveProperty('hasErrors'); - expect(response.body).toHaveProperty('hasWarnings'); - }); -}); diff --git a/cli/src/server/routes/validation-route.ts b/cli/src/server/routes/validation-route.ts deleted file mode 100644 index 3c27d1362..000000000 --- a/cli/src/server/routes/validation-route.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { SchemaDirectory, validate } from '@finos/calm-shared'; -import { Router, Request, Response } from 'express'; -import { ValidationOutcome } from '@finos/calm-shared'; -import rateLimit from 'express-rate-limit'; -import { initLogger, Logger } from '@finos/calm-shared/dist/logger'; - -export class ValidationRouter { - - private schemaDirectory: SchemaDirectory; - private logger: Logger; - - constructor(router: Router, schemaDirectory: SchemaDirectory, debug: boolean = false) { - const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // limit each IP to 100 requests per windowMs - }); - this.schemaDirectory = schemaDirectory; - this.logger = initLogger(debug, 'calm-server'); - router.use(limiter); - this.initializeRoutes(router); - } - - private initializeRoutes(router: Router) { - router.post('/', this.validateSchema); - } - - private validateSchema = async (req: Request, res: Response) => { - let architecture; - try { - architecture = JSON.parse(req.body.architecture); - } catch (error) { - this.logger.error('Invalid JSON format for architecture ' + error); - return res.status(400).type('json').send(new ErrorResponse('Invalid JSON format for architecture')); - } - - const schema = architecture['$schema']; - if (!schema) { - return res.status(400).type('json').send(new ErrorResponse('The "$schema" field is missing from the request body')); - } - - try { - await this.schemaDirectory.loadSchemas(); - } catch (error) { - this.logger.error('Failed to load schemas: ' + error); - return res.status(500).type('json').send(new ErrorResponse('Failed to load schemas')); - } - let foundSchema; - try { - foundSchema = await this.schemaDirectory.getSchema(schema); - if (!foundSchema) { - this.logger.error('Schema with $id ' + schema + ' not found'); - return res.status(400).type('json').send(new ErrorResponse('The "$schema" field referenced is not available to the server')); - } - } catch (err) { - this.logger.error('Failed to load schema: ' + err); - return res.status(500).type('json').send(new ErrorResponse('Failed to load schema: ' + err)); - } - try { - - const outcome = await validate(architecture, foundSchema, undefined, this.schemaDirectory, true); - return res.status(201).type('json').send(outcome); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return res.status(500).type('json').send(new ErrorResponse(message)); - } - }; -} - -class ErrorResponse { - error: string; - constructor(error: string) { - this.error = error; - } -}; - -interface ValidationRequest { - architecture: string; -} diff --git a/docs/docs/working-with-calm/generate.md b/docs/docs/working-with-calm/generate.md index fdbecd732..82dadc757 100644 --- a/docs/docs/working-with-calm/generate.md +++ b/docs/docs/working-with-calm/generate.md @@ -1,7 +1,7 @@ --- id: generate title: Generate -sidebar_position: 4 +sidebar_position: 3 --- # Generate diff --git a/docs/docs/working-with-calm/index.md b/docs/docs/working-with-calm/index.md index 9d14e93f8..ab8204d40 100644 --- a/docs/docs/working-with-calm/index.md +++ b/docs/docs/working-with-calm/index.md @@ -14,6 +14,7 @@ Explore the topics below to get hands-on experience with CALM: - [Using the CLI](using-the-cli): Understand the basic usage of the CALM CLI and how to access its commands. - [Generate](generate): Discover how to generate architectural architectures from predefined patterns. - [Validate](validate): Learn how to validate your architecture against CALM patterns to ensure compliance. +- [Validation Server](validation-server): Run a standalone HTTP server for remote CALM architecture validation. - [CALM AI Tools](calm-ai-tools): Expands the support of additional AI Assistants in CALM architecture modeling. - [Voice Mode](voice-mode): Enable hands-free interaction with CALM Copilot Chat using voice commands. diff --git a/docs/docs/working-with-calm/installation.md b/docs/docs/working-with-calm/installation.md index 0d2f04c1c..845883324 100644 --- a/docs/docs/working-with-calm/installation.md +++ b/docs/docs/working-with-calm/installation.md @@ -1,7 +1,7 @@ --- id: installation title: Installation -sidebar_position: 2 +sidebar_position: 1 --- # Installation diff --git a/docs/docs/working-with-calm/using-the-cli.md b/docs/docs/working-with-calm/using-the-cli.md index c25904575..65eafe0cd 100644 --- a/docs/docs/working-with-calm/using-the-cli.md +++ b/docs/docs/working-with-calm/using-the-cli.md @@ -1,7 +1,7 @@ --- id: using-the-cli title: Using the CLI -sidebar_position: 3 +sidebar_position: 2 --- # Using the CLI @@ -30,7 +30,6 @@ Options: Commands: generate [options] Generate an architecture from a CALM pattern file. validate [options] Validate that an architecture conforms to a given CALM pattern. - server [options] Start a HTTP server to proxy CLI commands. (experimental) template [options] Generate files from a CALM model using a template bundle, a single file, or a directory of templates docify [options] Generate a documentation website from your CALM model using a template or template directory init-ai [options] Augment a git repository with AI assistance for CALM diff --git a/docs/docs/working-with-calm/validate.md b/docs/docs/working-with-calm/validate.md index d902823f2..da9a1e513 100644 --- a/docs/docs/working-with-calm/validate.md +++ b/docs/docs/working-with-calm/validate.md @@ -1,7 +1,7 @@ --- id: validate title: Validate -sidebar_position: 5 +sidebar_position: 4 --- # Validate @@ -85,3 +85,8 @@ For patterns that don't have an `$id` field, the CLI automatically resolves rela ``` The CLI will look for the standard at `patterns/standards/my-standard.json`. + + +### Validation server + +The separate `@finos/calm-server` package provides a network accessible [validation server](validation-server.md). \ No newline at end of file diff --git a/docs/docs/working-with-calm/validation-server.md b/docs/docs/working-with-calm/validation-server.md new file mode 100644 index 000000000..0884519c7 --- /dev/null +++ b/docs/docs/working-with-calm/validation-server.md @@ -0,0 +1,337 @@ +--- +id: validation-server +title: Validation Server +sidebar_position: 5 +--- + +# Validation Server + +The `@finos/calm-server` package provides a standalone HTTP server for CALM architecture validation. It exposes REST API endpoints that allow you to validate CALM architectures remotely. This makes it ideal to include in application containers to provide CALM validation capability, without requiring subprocess invocations. + +The separation from the `@finos/calm-cli` package may also allow organizations to control the server availability. + +## Overview + +While the CLI provides command-line validation, the validation server enables remote validation via HTTP requests. + +All CALM schemas (release and draft versions) are bundled with the server, so no additional schema files are required. + +## Installation + +Install the validation server globally: + +```bash +npm install -g @finos/calm-server +``` + +Or use it within a project: + +```bash +npm install --save-dev @finos/calm-server +``` + +## Starting the Server + +### Basic Usage + +Start the server with default settings (listens on `localhost:3000`): + +```bash +calm-server +``` + +The server will start and display: +``` +CALM Server is running on http://127.0.0.1:3000 +``` + +### With Custom Port + +```bash +calm-server --port 8080 +``` + +### With Verbose Logging + +```bash +calm-server --verbose +``` + +### With Custom Schema Directory + +By default, the server uses bundled CALM schemas. To use custom schemas: + +```bash +calm-server --schema-directory /path/to/calm/schemas +``` + +## Command-Line Options + +| Option | Description | Default | +|--------|-------------|---------| +| `-V, --version` | Output the version number | - | +| `--port ` | Port to run the server on | `3000` | +| `--host ` | Host to bind the server to | `127.0.0.1` | +| `-s, --schema-directory ` | Path to custom CALM schema files | Bundled schemas | +| `-v, --verbose` | Enable verbose logging | `false` | +| `-c, --calm-hub-url ` | URL to CALMHub instance for remote schema resolution | - | +| `--rate-limit-window ` | Rate limit window in milliseconds | `900000` (15 min) | +| `--rate-limit-max ` | Max requests per IP within rate limit window | `100` | +| `-h, --help` | Display help information | - | + +## API Endpoints + +### Health Check + +Check if the server is running and responsive. + +**Endpoint:** `GET /health` + +**Example:** + +```bash +curl http://localhost:3000/health +``` + +**Response:** + +```json +{ + "status": "OK" +} +``` + +### Validate Architecture + +Validate a CALM architecture document against the appropriate schema. + +**Endpoint:** `POST /calm/validate` + +**Headers:** +- `Content-Type: application/json` + +**Request Body:** + +```json +{ + "architecture": "{\"$schema\":\"https://calm.finos.org/release/1.2/meta/calm.json\",\"nodes\":[]}" +} +``` + +The `architecture` field should contain a **stringified JSON** representation of your CALM architecture. + +**Example Request:** + +```bash +curl -X POST http://localhost:3000/calm/validate \ + -H "Content-Type: application/json" \ + -d '{ + "architecture": "{\"$schema\":\"https://calm.finos.org/release/1.2/meta/calm.json\",\"nodes\":[],\"relationships\":[]}" + }' +``` + +**Response (Valid Architecture):** + +```json +{ + "jsonSchemaValidationOutputs": [], + "spectralSchemaValidationOutputs": [], + "hasErrors": false, + "hasWarnings": false +} +``` + +**Response (Invalid Architecture):** + +```json +{ + "jsonSchemaValidationOutputs": [ + { + "valid": false, + "errors": [ + { + "instancePath": "/nodes/0", + "schemaPath": "#/properties/nodes/items/required", + "keyword": "required", + "params": { "missingProperty": "unique-id" }, + "message": "must have required property 'unique-id'" + } + ] + } + ], + "spectralSchemaValidationOutputs": [], + "hasErrors": true, + "hasWarnings": false +} +``` + +## Security Considerations + +:::warning +The validation server has **no built-in authentication or authorization**. It is designed for use in trusted environments. +::: + +### Default Security Settings + +- **Localhost Binding**: By default, the server binds to `127.0.0.1`, making it accessible only from the local machine +- **Rate Limiting**: Enabled by default (100 requests per 15 minutes per IP) +- **Security Warning**: When binding to non-localhost addresses, a warning is logged + +### Network Exposure + +When exposing the server to a network (using `--host 0.0.0.0` or a specific IP): + +```bash +calm-server --host 0.0.0.0 --port 3000 +``` + +The server will log: +``` +⚠️ WARNING: Server is configured to listen on 0.0.0.0 +⚠️ This server has NO authentication or authorization controls. +⚠️ Only bind to non-localhost addresses in trusted network environments. +``` + +**Best Practices:** +- Use a reverse proxy (nginx, Apache) with authentication +- Deploy behind a VPN or firewall +- Use network-level access controls +- Monitor usage and logs + +## Use Cases + +### Web Application Integration + +Use the validation server as a backend for web-based CALM editors: + +```javascript +async function validateArchitecture(architectureJson) { + const response = await fetch('http://localhost:3000/calm/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + architecture: JSON.stringify(architectureJson) + }) + }); + + const result = await response.json(); + + if (result.hasErrors) { + console.error('Validation errors:', result.jsonSchemaValidationOutputs); + } + + return result; +} +``` + +### Docker Deployment + +Create a Dockerfile for containerized deployment: + +```dockerfile +FROM node:22-alpine + +RUN npm install -g @finos/calm-server + +EXPOSE 3000 + +CMD ["calm-server", "--host", "0.0.0.0", "--port", "3000"] +``` + +Build and run: + +```bash +docker build -t calm-server . +docker run -p 3000:3000 calm-server +``` + +## Rate Limiting + +The server includes built-in rate limiting to prevent abuse: + +- **Default Window**: 15 minutes (900,000 ms) +- **Default Max Requests**: 100 per IP address + +### Customizing Rate Limits + +Adjust rate limits based on your use case: + +```bash +# Higher limits for internal team usage +calm-server \ + --rate-limit-window 3600000 \ + --rate-limit-max 1000 + +# Stricter limits for public-facing instances +calm-server \ + --rate-limit-window 600000 \ + --rate-limit-max 50 +``` + +### Rate Limit Response + +When rate limits are exceeded, the server returns: + +**HTTP 429 Too Many Requests** + +```html +Too many requests, please try again later. +``` + +## Troubleshooting + +### Server Won't Start + +**Problem**: Port already in use + +``` +Error: listen EADDRINUSE: address already in use 127.0.0.1:3000 +``` + +**Solution**: Use a different port or stop the process using port 3000: + +```bash +# Use different port +calm-server --port 8080 + +# Or find and stop the process +lsof -i :3000 +kill -9 +``` + +### Validation Fails with Valid Architecture + +**Problem**: Architecture validates with CLI but fails with server + +**Solution**: Ensure the architecture is properly stringified in the request body: + +```javascript +// Correct: Stringified JSON +{ + "architecture": "{\"$schema\":\"...\",\"nodes\":[]}" +} + +// Incorrect: Plain JSON object +{ + "architecture": {"$schema": "...", "nodes": []} +} +``` + +### Connection Refused + +**Problem**: Cannot connect to server + +``` +curl: (7) Failed to connect to localhost port 3000: Connection refused +``` + +**Solution**: +1. Verify the server is running +2. Check the port number +3. Ensure firewall allows connections + +## See Also + +- [Validate Command](validate.md) - CLI-based validation +- [Using the CLI](using-the-cli.md) - General CLI usage +- [Installation](installation.md) - Installing CALM tools diff --git a/docs/sidebars.js b/docs/sidebars.js index 1c1b0b963..ef3bd7c8d 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -49,8 +49,9 @@ const sidebars = { items: [ 'working-with-calm/installation', 'working-with-calm/using-the-cli', - 'working-with-calm/validate', 'working-with-calm/generate', + 'working-with-calm/validate', + 'working-with-calm/validation-server', 'working-with-calm/calm-ai-tools', 'working-with-calm/voice-mode' ], diff --git a/package-lock.json b/package-lock.json index d9e1ea3c2..6f9caeefa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -532,7 +532,6 @@ "commander": "^14.0.0", "copyfiles": "^2.4.1", "execa": "^9.6.0", - "express-rate-limit": "^8.0.0", "mkdirp": "^3.0.1", "ts-node": "10.9.2" }, @@ -544,12 +543,10 @@ "@semantic-release/exec": "^7.1.0", "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.0", - "@types/supertest": "^6.0.3", "@types/xml2js": "^0.4.14", "axios": "^1.13.5", "chokidar": "^4.0.3", "semantic-release": "^25.0.0", - "supertest": "^7.1.0", "tsup": "^8.4.0", "xml2js": "^0.6.2" } @@ -10896,13 +10893,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=10" } @@ -10914,13 +10909,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=10" } @@ -10932,13 +10925,11 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -10950,13 +10941,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -10968,13 +10957,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -10986,13 +10973,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -11004,13 +10989,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" ], - "peer": true, "engines": { "node": ">=10" } @@ -11022,13 +11005,11 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -11040,13 +11021,11 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" } @@ -11058,13 +11037,11 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" ], - "peer": true, "engines": { "node": ">=10" }