@lumen-mirror/cli
v1.0.2
Published
CLI for scaffolding and developing Lumen Mirror smart mirror apps.
Readme
@lumen-mirror/cli
Create and run Lumen Mirror smart mirror apps from the terminal.
Scaffolds projects, generates modules, and runs Vite / SCSS / decorators for you — no vite.config.ts in user apps.
Runtime checklist (via @lumen-mirror/core)
- Features with shared state + typed events (
createFeature/injectFeature) - Own signals (
signal,computed,fromHttp, …) - Templates:
{{ }},@if/@for,(click),[attr.*]bindings,<GroupModule> - Security first — allowlisted attribute writes, closed expressions, URL scheme checks
- Focus mode + typed CSS grid layout
- Small public API (renderer / binder stay internal)
See the core README for the feature/state/events model.
Install
npm install -g @lumen-mirror/cliOr use it once without installing:
npx @lumen-mirror/cli new my-mirrorCreate a project
lumen new my-mirror
cd my-mirror
npm install
npm run devlumen new asks which package manager to use and writes a default config for that choice before install:
| Choice | Config |
| ------ | ------ |
| npm | .npmrc with engine-strict=true |
| pnpm | .npmrc with engine-strict=true and auto-install-peers=true |
| yarn | .yarnrc.yml with nodeLinker: node-modules (Vite cannot use Plug'n'Play) |
The scaffold also sets package.json "packageManager" to the installed version, and "engines" to Node 20+. Creating the app through pnpm or yarn skips the prompt and uses that manager.
Open the URL printed in the terminal (default http://localhost:5173). You get a working mirror with four starter widgets on a CSS grid — the clock is the main reference example (alarms, routine profiles, voices, focus mode, and localStorage persistence).
Starter modules
| Widget | What it demonstrates |
| ------------- | ----------------------------------------------------------------------------------------------------------------------- |
| clock | Reference module: (click), @if / @for, focus mode, trackInterval, nested imports, feature state, localStorage |
| weather | HTTP signals, nested tab modules, SVG chart bindings, layout expand / focus |
| welcome | Group host — projects Home + Quotes into <GroupModule>; Home listens across features |
| news-feed | @for list + events consumed by Welcome Home |
Open the clock gear first — add a routine profile, create an alarm, and pick a voice. Use the welcome ellipsis to swap Home / Quotes.
Electron (optional, dev shell)
Run the same app inside an Electron window during development:
npm run dev:electronThat is lumen serve --electron — Vite + a Chromium window. It is not a packaged installer.
Shipping a kiosk build today:
lumen build→ output indist/(seelumen.jsonbuild.outDir)- Point Electron at the built files with
LUMEN_DEV_SERVER_URLunset so the shell loadsindex.htmlfrom the app cwd (typicallydist/)
There is no electron-builder / asar pipeline in the CLI yet — treat Electron as a secure browser shell around your Vite build.
Window size and display options live in lumen.json under electron. Security defaults are locked down and iframe-friendly:
| Flag (electron.security.*) | Default | What it does |
| ---------------------------- | ------- | ------------------------------------------------------- |
| blockWindowOpen | true | Denies window.open / target="_blank" popups |
| blockTopLevelNavigation | true | Denies main-frame navigations away from the app |
| denyPermissions | true | Denies cam/mic/geo/notification prompts |
| allowWebviewTag | false | Chromium <webview> off — prefer <iframe> in modules |
<iframe src="…"> inside a module template is not blocked by these. Opt out only when needed:
"electron": {
"security": {
"denyPermissions": false
}
}Day-to-day scripts
| Script | What it does |
| ---------------------- | ------------------------------------ |
| npm run dev | Start the dev server with hot reload |
| npm run dev:electron | Dev server + Electron window |
| npm run build | Production build to dist/ |
All three delegate to the lumen CLI (serve / build).
Project layout
After lumen new, your app looks like this:
my-mirror/
lumen.json # platform config (serve, build, electron)
index.html
src/
main.ts # bootstraps the mirror
lumen.config.ts # typed CSS grid layout (+ optional security)
global.scss # base styles
modules/ # one folder per widget
MODULE_CONVENTIONS.md
clock/
weather/
welcome/
news-feed/src/main.ts
Entry point — loads your layout and modules, then starts the runtime:
import lumenProject from '../lumen.json';
import { bootstrap } from '@lumen-mirror/core';
import appConfig from './lumen.config.js';
import {
ClockModule,
WeatherModule,
WelcomeModule,
NewsFeedModule,
} from './modules/index.js';
await bootstrap({
root: lumenProject.root,
modules: [ClockModule, WeatherModule, WelcomeModule, NewsFeedModule],
config: appConfig,
});src/lumen.config.ts
Define where widgets sit on the mirror grid. Rows, columns, and cell names are type-checked:
import { defineLayout, defineLumenConfig } from '@lumen-mirror/core';
export default defineLumenConfig({
layout: defineLayout(4, 4, [
['clock', 'clock', '.', 'weather'],
['welcome', 'welcome', 'welcome', 'welcome'],
['welcome', 'welcome', 'welcome', 'welcome'],
['news-feed', 'news-feed', 'news-feed', 'news-feed'],
] as const),
});Use . for empty cells. Each name must match a module's host.classes entry (e.g. host: { classes: ['clock'] }).
Optional attribute security (replaces the default [attr.*] allowlist):
security: {
attributeAllowlist: ['href', 'src', 'aria-*', 'data-*', 'viewBox', 'd', 'y1', 'y2'],
},lumen.json
Platform config the CLI reads — like a slim angular.json. Layout and modules live in TypeScript, not here.
Lumen assumes Electron + modern Chromium. There is no legacy browser target, differential loading, or polyfill pipeline. lumen serve stays fast; lumen build always optimizes unless you opt out.
{
"name": "my-mirror",
"root": "#app",
"index": "index.html",
"serve": { "port": 5173, "host": "localhost" },
"build": {
"target": "esnext",
"minify": true,
"sourcemap": false,
"outDir": "dist"
},
"electron": {
"width": 1080,
"height": 1920,
"fullscreen": false,
"security": {
"blockWindowOpen": true,
"blockTopLevelNavigation": true,
"denyPermissions": true,
"allowWebviewTag": false
}
}
}| Field | Role |
| --------------------------- | -------------------------------------------------------------------- |
| root | DOM selector passed to bootstrap |
| index | HTML shell Vite uses as the app entry |
| serve.port / serve.host | Dev server bind address |
| build.target | JS target (esnext by default) |
| build.minify | Minify JS + CSS on lumen build (default true) |
| build.sourcemap | Emit sourcemaps on lumen build (default false) |
| build.outDir | Output folder (default dist) |
| electron.* | BrowserWindow options for lumen serve --electron |
| electron.security.* | Renderer lockdown (popups / top-level nav / permissions); iframes OK |
Escape hatches for debugging a packaged build: set "minify": false and/or "sourcemap": true. Do not add a vite.config.ts — the CLI owns the bundler config.
Build-time template checks
When Vite transforms a *.lumen.ts file, Lumen reads its templateUrl and validates:
- control flow (
@if/@else/@for/@in) - interpolations (
{{ ... }}) - event handlers (
(click)="save()", etc.) - nested module tags / projection (
<GroupModule>…</GroupModule>)
Invalid templates fail lumen build and show in the Vite overlay during lumen serve, with file:line:column so the overlay can jump to the error.
Add a module
From your project root:
lumen generate quotesThis creates a convention-aligned module folder and a barrel export in src/modules/index.ts:
src/modules/quotes/
quotes.lumen.ts
quotes.lumen.html
quotes.lumen.scss
types/quotes.types.ts
consts/quotes.consts.ts
features/quotes.feature.ts
components/ # nested child modulesBy default it does not add the module to src/main.ts or the grid — use that when the widget is nested inside another module. Put nested modules under the parent's components/ folder, declare them in imports, and use them in the parent template:
import { QuotesModule } from './components/quotes/quotes.lumen.js';
@Module({
templateUrl: './dashboard.lumen.html',
styleUrls: ['./dashboard.lumen.scss'],
imports: [QuotesModule],
host: { classes: ['dashboard'] },
})
export class DashboardModule extends LumenModule {}<QuotesModule />To share one grid cell between several modules, project them into a group (list every member in imports):
<GroupModule>
<WelcomeHomeModule label="Welcome" />
<QuotesModule label="Quotes" />
</GroupModule>Nested modules do not need to be listed in bootstrap({ modules }) — only in the parent's imports.
If you confirm the prompt, generate will also register the module in src/main.ts and src/lumen.config.ts for top-level grid placement.
See src/modules/MODULE_CONVENTIONS.md in a scaffolded app for visibility, timers (trackInterval), groups, and file-layout rules.
Put layout and panel styles on the module host via host.classes — do not repeat that class on a wrapper inside the template (the renderer already applies it to the host element).
Templates use {{ signalName }}, @if, and @for. See @lumen-mirror/core for the full template and signals API.
Commands
| Command | Description |
| ------------------------ | ------------------------------------------------ |
| lumen new <name> | Scaffold a new mirror app |
| lumen generate [name] | Add a module (alias: lumen g) |
| lumen serve | Dev server with hot reload |
| lumen serve --electron | Dev server + Electron shell (not a packaged app) |
| lumen build | Production build |
Requirements
- Node.js 20+
- npm, pnpm, or yarn (for installing scaffolded app dependencies)
Learn more
- Runtime & templates: @lumen-mirror/core on npm
