@eka-care/ekam-cli
v0.1.9
Published
Ekam app developer CLI — scaffold, build, dev, publish
Downloads
916
Maintainers
Keywords
Readme
@eka-care/ekam-cli
Official CLI for developing Ekam apps — scaffold, build, dev server, and publish.
Installation
As a project dependency (recommended)
npm install --save-dev @eka-care/ekam-cliGlobal installation
npm install -g @eka-care/ekam-cliVia npx (no installation)
npx ekam <command>Commands
ekam create <app-name>
Scaffold a new Ekam app with best practices and ready-to-use templates.
ekam create my-vitals-app
cd my-vitals-app
npm installShadow DOM only. Every app created by
ekam createis mounted inside a Shadow DOM container managed by ekam-runtime. Themount(container, sdk, shadowRoot)signature is mandatory — do not remove theshadowRootparameter.
Interactive prompt:
When no flag is provided, the CLI asks:
? Use Aakaar design system? (Y/n)- Yes — scaffolds an app that uses
@eka/aakaarcomponents wrapped inAakaarProvider. Aakaar's styles are injected into the shadow root by ekam-runtime beforemount()runs; the app itself does not calladoptedStyleSheets. - No — scaffolds a minimal React app with a plain
<button>. No design-system dependency.
Non-interactive flags (for CI or scripts):
ekam create my-app --aakaar # Aakaar setup, no prompt
ekam create my-app --no-aakaar # Plain React, no promptGenerated structure:
my-vitals-app/
├── src/
│ ├── App.tsx # Main component
│ ├── App.module.css # CSS modules
│ └── index.ts # Entry point (mount/unmount/onContextChange)
├── types/
│ ├── ekam-sdk.d.ts # SDK type definitions
│ └── css-modules.d.ts # CSS module types
├── assets/
│ └── icon.svg # App icon
├── eka.manifest.yaml # App manifest (metadata, permissions, slots)
├── package.json
├── tsconfig.json
└── .gitignoreOptions:
- App name must be in kebab-case (e.g.,
my-app,growth-chart)
ekam dev
Start a local dev server. Three modes, pick based on what you're doing:
ekam dev # Default: open hosted runtime (recommended)
ekam dev --attach https://staging.eka.care # Custom: attach to a specific runtime URL
ekam dev --standalone # Standalone: direct mount, no runtime, fastest HMRThree modes:
| Mode | Flag | What it runs | When to use |
| --- | --- | --- | --- |
| Hosted (default) | (none) | Builds UMD locally + opens the hosted ekam-runtime page. Devtools auto-attach; real shell, real terminology, full FHIR stack. | Default starting point — closest to production without any setup. |
| Custom runtime | --attach <url> | Same as hosted, but targets a URL you provide (staging, cockpit, local runtime build, ngrok tunnel). | Testing against a specific runtime version or environment. |
| Standalone | --standalone | Vite serves dev/index.html directly. Mounts your app with a mock SDK into a real ShadowRoot. Full HMR, no UMD build step. | Day-to-day UI work — ~10× faster iteration than hosted. |
Hosted mode (default) opens https://elixir-dr.dev.eka.care/ekam-runtime/main/?__ekam_cli=http://localhost:5173 in your browser. The runtime's built-in devtools panel auto-attaches and hot-reloads your app whenever you save a file.
Custom runtime — use --attach when you need a specific runtime:
# Staging runtime
ekam dev --attach https://staging.eka.care/runtime/
# Local runtime build
ekam dev --attach http://localhost:4000/
# Remote machine via ngrok tunnel
ngrok http 5173
ekam dev --attach https://abc123.ngrok-free.app --no-open
# Then open: https://your-runtime.eka.care/?__ekam_cli=https://abc123.ngrok-free.appStandalone scaffolding — --standalone lazily creates dev/ (never overwrites):
dev/
├── index.html # Host page (real ShadowRoot)
├── bootstrap.ts # Wires ShadowRoot + mock SDK + HMR
├── mock-sdk.ts # Customisable mock SDK
├── README.md
└── fixtures/
└── patient.jsonEdit dev/mock-sdk.ts to mirror the SDK surface your app consumes.
Options:
--port <port>— Dev server port (default: 5173)--attach <url>— Attach to a specific runtime URL instead of the hosted default--standalone— Use standalone mode (see above)--no-open— Do not open the browser automatically (useful with ngrok / CI)Mock encounter context
Host controls to switch patient / end encounter
Runtime controls (destroy / reload)
ekam validate
Run all build-time checks without bundling. Fast — meant for CI and pre-commit hooks.
ekam validateChecks:
eka.manifest.yamlparses + passes schema- No banned imports (e.g.,
antddirectly) - Lint rules (warnings only — never blocks)
Exits non-zero on any blocker. Use this in CI before ekam build to fail fast.
ekam info
Print metadata from eka.manifest.yaml plus the local build status. Read-only.
ekam infoShows: app id/name/version, framework version, slot, FHIR resources, event subscriptions, capabilities, Aakaar usage, and (if dist/ exists) bundle size + gzipped size + last-build time.
ekam build
Build production-ready UMD bundle.
ekam buildOutput:
dist/
├── index.js # UMD bundle (minified)
├── index.js.map # Source map
└── index.css # Extracted CSSFeatures:
- ✅ Manifest validation
- 🚫 Banned imports check (no node builtins)
- 🔍 ESLint validation
- 📦 UMD format with externalized deps
- 🗜️ Minification & tree-shaking
- 📏 Bundle size validation (max 200KB gzipped)
- ✓ Export validation (mount/unmount)
Build process:
- Validates
eka.manifest.yaml - Checks for banned imports (fs, path, etc.)
- Runs lint checks
- Builds UMD bundle with Vite
- Validates bundle size
- Verifies required exports exist
ekam publish (out of scope for v1)
The marketplace upload flow is not implemented in this CLI. Until the registry contract lands, publish your app by:
- Running
ekam build - Uploading
dist/index.js(anddist/index.cssif present) to your CDN - Pointing the host's app-loader at that URL
Login + automated upload (
ekam login,ekam publishto a registry) are deferred until the marketplace API is finalised.
Manifest (eka.manifest.yaml)
Every Ekam app requires a manifest file:
# Identity
app:
id: "com.eka.vitals"
name: "Vitals"
version: "1.0.0"
description: "Record patient vitals"
author:
name: "Developer"
email: "[email protected]"
icon: "./assets/icon.svg"
category: "general-medicine"
tags: ["vitals", "observations"]
# Framework version
framework:
version: "1.0.0"
# UI slots
slots:
primary: "patient-detail-panel"
trigger:
type: "menu"
label: "Vitals"
# FHIR permissions
fhir:
resources:
- type: "Patient"
access: "read"
- type: "Observation"
access: "read-write"
# Event subscriptions
events:
subscribes:
- resource: "Observation"
on: ["created", "updated"]
# Capabilities
capabilities:
storage: true
media: false
print: false
notifications: false
# Aakaar UI library (only present when --aakaar was chosen)
aakaar:
packages:
- "@eka/aakaar"
# Constraints
constraints:
maxBundleSize: "200KB"App API
All Ekam apps run inside a Shadow DOM managed by ekam-runtime. The runtime creates the shadow root, injects Aakaar's stylesheets if needed, and then calls
mount(container, sdk, shadowRoot). Your app'ssrc/index.tsmust export at minimummountandunmount.
Required Exports — with Aakaar
import React from 'react'
import ReactDOM from 'react-dom/client'
import { AakaarProvider } from '@eka/aakaar'
import { App } from './App'
import type { EkamSDK } from '@ekam/sdk'
let root: ReactDOM.Root | null = null
let shadowRoot: ShadowRoot | null = null
// Mount the app — shadow is always provided by ekam-runtime
export function mount(
container: HTMLElement,
sdk: EkamSDK,
shadow: ShadowRoot // required — never optional
): void {
shadowRoot = shadow
root = ReactDOM.createRoot(container)
root.render(
React.createElement(AakaarProvider, { shadowRoot },
React.createElement(App, { sdk })
)
)
}
export function unmount(_container: HTMLElement): void {
if (root) {
root.unmount()
root = null
shadowRoot = null
}
}
export function onContextChange(sdk: EkamSDK): void {
if (root && shadowRoot) {
root.render(
React.createElement(AakaarProvider, { shadowRoot },
React.createElement(App, { sdk })
)
)
}
}Required Exports — without Aakaar
import React from 'react'
import ReactDOM from 'react-dom/client'
import { App } from './App'
import type { EkamSDK } from '@ekam/sdk'
let root: ReactDOM.Root | null = null
// _shadow is received but unused — kept for contract compliance
export function mount(
container: HTMLElement,
sdk: EkamSDK,
_shadow: ShadowRoot
): void {
root = ReactDOM.createRoot(container)
root.render(React.createElement(App, { sdk }))
}
export function unmount(_container: HTMLElement): void {
if (root) {
root.unmount()
root = null
}
}
export function onContextChange(sdk: EkamSDK): void {
if (root) {
root.render(React.createElement(App, { sdk }))
}
}Optional Export
// Optional: Prevent unmount
export function canUnmount(): boolean | string {
// return false to prevent unmount
// return "Unsaved changes. Continue?" for confirmation
return true
}SDK API
The sdk object passed to your app:
// Context (read-only + onChange)
sdk.context.patient // Current patient
sdk.context.encounter // Current encounter
sdk.context.practitioner // Logged-in user
sdk.context.organization // Organization
sdk.context.locale // User locale
sdk.context.slot // UI slot name
sdk.context.onChange((ctx) => {
// Handle patient/encounter changes
})
// FHIR operations
await sdk.fhir.read('Patient', '123')
await sdk.fhir.search('Observation', { patient: '123' })
await sdk.fhir.create('Observation', resource)
await sdk.fhir.update('Observation', 'id', resource)
await sdk.fhir.delete('Observation', 'id')
// Event subscriptions
const unsub = sdk.events.on('Observation', 'created', (resource) => {
// Handle new observation
})
unsub() // Unsubscribe
// Storage (scoped to your app)
await sdk.storage.set('key', value)
const value = await sdk.storage.get('key')
await sdk.storage.delete('key')
const keys = await sdk.storage.list('prefix')
// UI helpers
sdk.ui.toast('Saved!', { type: 'success' })
const confirmed = await sdk.ui.confirm({
title: 'Delete?',
message: 'This cannot be undone',
})CSS Modules
Use CSS modules for scoped styling:
// App.tsx
import classes from './App.module.css'
export function App() {
return (
<div className={classes.root}>
<h2 className={classes.title}>My App</h2>
</div>
)
}/* App.module.css */
.root {
padding: 24px;
}
.title {
font-size: 18px;
color: #1e293b;
}TypeScript definitions are auto-generated via types/css-modules.d.ts.
External Dependencies
These packages are provided by the runtime and should NOT be bundled:
reactreact-domreact-dom/clientreact/jsx-runtime@ekam/sdk@eka/aakaar
They are automatically externalized during build.
Bundle Constraints
- Max size: 200KB gzipped (configurable in manifest)
- No Node.js builtins: Cannot import
fs,path,crypto, etc. - No dynamic imports: Use static imports only
- No global pollution: Apps run in shadow DOM
Monorepo Setup
For monorepo development with a local runtime build, point EKAM_RUNTIME_URL at the local file served by any static server:
Build ekam-runtime and serve it:
cd ekam-runtime npm run build npx serve dist --cors -p 4000Build ekam-cli:
cd ekam-cli npm run buildLink CLI:
cd ekam-cli npm linkCreate app and run dev with local runtime:
ekam create my-app cd my-app npm install EKAM_RUNTIME_URL=http://localhost:4000/ekam-runtime.js ekam dev
Troubleshooting
CSS not loading in dev mode
- Check that
dist/index.cssexists after build - Ensure hot reload plugin is active (added in CLI v0.1.0)
- Try hard refresh (Cmd+Shift+R / Ctrl+Shift+R)
"Cannot find module '@ekam/sdk'"
- SDK types are in
types/ekam-sdk.d.ts - Not a real npm package, provided by runtime
- If missing, run
ekam createagain or copy from template
Build fails with "Banned import"
- Cannot import Node.js built-ins (
fs,path, etc.) - Use browser-compatible alternatives
- Check manifest for allowed packages
Bundle size too large
- Check
dist/index.js.mapto see what's bundled - Externalize large dependencies in manifest
- Use code splitting (if supported in future)
- Remove unused imports
Development
Build CLI
cd ekam-cli
npm install
npm run buildWatch mode
npm run devLink locally
npm linkLicense
Proprietary - Eka Care
Support
- Issues: Contact Eka Care development team
- Docs: See Ekam developer portal
- Examples: Check
vitalsandbmi-calculatorsample apps
