@lumerahq/cli 0.9.3 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,14 +19,14 @@ lumera logout # Clear stored credentials
19
19
  lumera whoami # Show current user
20
20
  lumera status # Show project info
21
21
 
22
- lumera app dev # Start dev server
23
- lumera app deploy # Deploy frontend to S3
24
- lumera app destroy # Delete app from Lumera
22
+ lumera dev # Start dev server
23
+ lumera apply app # Deploy frontend
24
+ lumera destroy app # Delete app from Lumera
25
25
 
26
- lumera platform plan # Preview infrastructure changes
27
- lumera platform apply # Apply collections, automations, hooks
28
- lumera platform pull # Pull remote state to local
29
- lumera platform destroy # Delete remote resources
26
+ lumera plan # Preview infrastructure changes
27
+ lumera apply # Apply collections, automations, hooks
28
+ lumera pull # Pull remote state to local
29
+ lumera destroy # Delete remote resources
30
30
 
31
31
  lumera run <script> # Run Python scripts locally
32
32
  ```
@@ -84,7 +84,7 @@ For automated environments, use the `LUMERA_TOKEN` environment variable:
84
84
 
85
85
  ```bash
86
86
  export LUMERA_TOKEN=your_api_token
87
- lumera app deploy
87
+ lumera apply app
88
88
  ```
89
89
 
90
90
  The CLI checks for credentials in this order:
@@ -56,10 +56,11 @@ var ApiClient = class {
56
56
  }
57
57
  // Automations
58
58
  async listAutomations(params) {
59
- let path = "/api/automations";
60
- if (params?.external_id) {
61
- path += `?external_id=${encodeURIComponent(params.external_id)}`;
62
- }
59
+ const qs = new URLSearchParams();
60
+ if (params?.external_id) qs.set("external_id", params.external_id);
61
+ if (params?.include_code) qs.set("include_code", "true");
62
+ const query = qs.toString();
63
+ const path = `/api/automations${query ? `?${query}` : ""}`;
63
64
  const result = await this.request(path);
64
65
  return result.automations || [];
65
66
  }
package/dist/index.js CHANGED
@@ -92,33 +92,33 @@ async function main() {
92
92
  switch (command) {
93
93
  // Resource commands
94
94
  case "plan":
95
- await import("./resources-2IHBFKMX.js").then((m) => m.plan(args.slice(1)));
95
+ await import("./resources-PGBVCS2K.js").then((m) => m.plan(args.slice(1)));
96
96
  break;
97
97
  case "apply":
98
- await import("./resources-2IHBFKMX.js").then((m) => m.apply(args.slice(1)));
98
+ await import("./resources-PGBVCS2K.js").then((m) => m.apply(args.slice(1)));
99
99
  break;
100
100
  case "pull":
101
- await import("./resources-2IHBFKMX.js").then((m) => m.pull(args.slice(1)));
101
+ await import("./resources-PGBVCS2K.js").then((m) => m.pull(args.slice(1)));
102
102
  break;
103
103
  case "destroy":
104
- await import("./resources-2IHBFKMX.js").then((m) => m.destroy(args.slice(1)));
104
+ await import("./resources-PGBVCS2K.js").then((m) => m.destroy(args.slice(1)));
105
105
  break;
106
106
  case "list":
107
- await import("./resources-2IHBFKMX.js").then((m) => m.list(args.slice(1)));
107
+ await import("./resources-PGBVCS2K.js").then((m) => m.list(args.slice(1)));
108
108
  break;
109
109
  case "show":
110
- await import("./resources-2IHBFKMX.js").then((m) => m.show(args.slice(1)));
110
+ await import("./resources-PGBVCS2K.js").then((m) => m.show(args.slice(1)));
111
111
  break;
112
112
  // Development
113
113
  case "dev":
114
114
  await import("./dev-BHBF4ECH.js").then((m) => m.dev(args.slice(1)));
115
115
  break;
116
116
  case "run":
117
- await import("./run-4NDI2CN4.js").then((m) => m.run(args.slice(1)));
117
+ await import("./run-WIRQDYYX.js").then((m) => m.run(args.slice(1)));
118
118
  break;
119
119
  // Project
120
120
  case "init":
121
- await import("./init-LVO3ZMG7.js").then((m) => m.init(args.slice(1)));
121
+ await import("./init-EDSRR3YM.js").then((m) => m.init(args.slice(1)));
122
122
  break;
123
123
  case "status":
124
124
  await import("./status-E4IHEUKO.js").then((m) => m.status(args.slice(1)));
@@ -332,6 +332,15 @@ async function init(args) {
332
332
  console.log(pc.green(" \u2713"), pc.dim(`${installed} Lumera skills installed`));
333
333
  }
334
334
  syncClaudeMd(targetDir);
335
+ if (isGitInstalled()) {
336
+ try {
337
+ execSync('git add -A && git commit -m "chore: install lumera skills"', {
338
+ cwd: targetDir,
339
+ stdio: "ignore"
340
+ });
341
+ } catch {
342
+ }
343
+ }
335
344
  } catch (err) {
336
345
  console.log(pc.yellow(" \u26A0"), pc.dim(`Failed to install skills: ${err}`));
337
346
  }
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-CDZZ3JYU.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-V2XXMMEI.js";
6
+ } from "./chunk-WRAZC6SJ.js";
7
7
  import {
8
8
  loadEnv
9
9
  } from "./chunk-2CR762KB.js";
@@ -452,7 +452,7 @@ async function planCollections(api, localCollections) {
452
452
  }
453
453
  async function planAutomations(api, localAutomations) {
454
454
  const changes = [];
455
- const remoteAutomations = await api.listAutomations();
455
+ const remoteAutomations = await api.listAutomations({ include_code: true });
456
456
  const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id).map((a) => [a.external_id, a]));
457
457
  for (const { automation, code } of localAutomations) {
458
458
  const remote = remoteByExternalId.get(automation.external_id);
@@ -526,9 +526,11 @@ async function applyCollections(api, localCollections) {
526
526
  const hasRelations = localCollections.some((c) => c.fields.some((f) => f.type === "relation"));
527
527
  if (hasRelations) {
528
528
  for (const local of localCollections) {
529
+ const relationFieldNames = new Set(local.fields.filter((f) => f.type === "relation").map((f) => f.name));
529
530
  const withoutRelations = {
530
531
  ...local,
531
- fields: local.fields.filter((f) => f.type !== "relation")
532
+ fields: local.fields.filter((f) => f.type !== "relation"),
533
+ indexes: local.indexes?.filter((idx) => !idx.fields.some((f) => relationFieldNames.has(f)))
532
534
  };
533
535
  const apiFormat = convertCollectionToApiFormat(withoutRelations);
534
536
  try {
@@ -742,7 +744,7 @@ async function pullCollections(api, platformDir, filterName) {
742
744
  async function pullAutomations(api, platformDir, filterName) {
743
745
  const automationsDir = join(platformDir, "automations");
744
746
  mkdirSync(automationsDir, { recursive: true });
745
- const automations = await api.listAutomations();
747
+ const automations = await api.listAutomations({ include_code: true });
746
748
  for (const automation of automations) {
747
749
  if (!automation.external_id || automation.managed) continue;
748
750
  if (filterName && automation.external_id !== filterName && automation.name !== filterName) {
@@ -847,7 +849,7 @@ async function listResources(api, platformDir, filterType) {
847
849
  }
848
850
  if (!filterType || filterType === "automations") {
849
851
  const localAutomations = loadLocalAutomations(platformDir);
850
- const remoteAutomations = await api.listAutomations();
852
+ const remoteAutomations = await api.listAutomations({ include_code: true });
851
853
  const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a]));
852
854
  const localIds = new Set(localAutomations.map((a) => a.automation.external_id));
853
855
  for (const { automation, code } of localAutomations) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-V2XXMMEI.js";
3
+ } from "./chunk-WRAZC6SJ.js";
4
4
  import {
5
5
  loadEnv
6
6
  } from "./chunk-2CR762KB.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.9.3",
3
+ "version": "0.10.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -0,0 +1,80 @@
1
+ # {{projectTitle}} — Architecture
2
+
3
+ ## Overview
4
+
5
+ {{projectTitle}} is a Lumera embedded app — a React frontend served inside the Lumera platform iframe, backed by collections, hooks, and automations managed through the Lumera CLI.
6
+
7
+ ## System Diagram
8
+
9
+ ```
10
+ ┌─────────────────────────────────────────────────┐
11
+ │ Lumera Platform │
12
+ │ │
13
+ │ ┌───────────┐ postMessage ┌────────────┐ │
14
+ │ │ Host UI │ ◄──────────────► │ App │ │
15
+ │ │ │ (auth, init) │ (iframe) │ │
16
+ │ └─────┬─────┘ └─────┬──────┘ │
17
+ │ │ │ │
18
+ │ │ REST API │ │
19
+ │ ▼ ▼ │
20
+ │ ┌──────────────────────────────────────────┐ │
21
+ │ │ Lumera API │ │
22
+ │ │ - Collections (CRUD, SQL, search) │ │
23
+ │ │ - Automations (run, poll, cancel) │ │
24
+ │ │ - File storage (upload, download) │ │
25
+ │ └──────────────┬───────────────────────────┘ │
26
+ │ │ │
27
+ │ ▼ │
28
+ │ ┌──────────────────────────────────────────┐ │
29
+ │ │ Tenant Database (PocketBase/SQLite) │ │
30
+ │ │ - example_items │ │
31
+ │ │ - (add your collections here) │ │
32
+ │ └──────────────────────────────────────────┘ │
33
+ └─────────────────────────────────────────────────┘
34
+ ```
35
+
36
+ ## Frontend (`src/`)
37
+
38
+ React app using TanStack Router (file-based routing) and TanStack Query for data fetching. Embedded in Lumera via iframe with postMessage bridge for authentication.
39
+
40
+ | Directory | Purpose |
41
+ |------------------|--------------------------------------|
42
+ | `src/routes/` | Pages — file names map to URL paths |
43
+ | `src/components/`| Shared React components |
44
+ | `src/lib/` | API helpers, query functions |
45
+ | `src/main.tsx` | App entry — auth bridge, router init |
46
+
47
+ **Key patterns:**
48
+ - Auth context flows from `main.tsx` via `AuthContext`
49
+ - Data fetching uses `pbList`, `pbSql` from `@lumerahq/ui/lib`
50
+ - Styling via Tailwind CSS 4 with theme tokens in `styles.css`
51
+
52
+ ## Platform Resources (`platform/`)
53
+
54
+ Declarative definitions deployed via `lumera apply`.
55
+
56
+ | Directory | Purpose |
57
+ |--------------------------|----------------------------------|
58
+ | `platform/collections/` | Collection schemas (JSON) |
59
+ | `platform/automations/` | Background Python scripts |
60
+ | `platform/hooks/` | Server-side JS on collection events |
61
+
62
+ ## Scripts (`scripts/`)
63
+
64
+ Local Python scripts run via `lumera run`. Used for seeding data, migrations, and ad-hoc operations. All scripts should be idempotent.
65
+
66
+ ## Data Flow
67
+
68
+ 1. **User opens app** → Lumera host sends auth payload via postMessage
69
+ 2. **App authenticates** → Stores session token in `AuthContext`
70
+ 3. **App fetches data** → Calls Lumera API via `@lumerahq/ui/lib` helpers
71
+ 4. **Data mutations** → API calls trigger collection hooks if configured
72
+ 5. **Background work** → Automations run async via `createRun` / `pollRun`
73
+
74
+ ## Collections
75
+
76
+ | Collection | Purpose |
77
+ |------------------|----------------------------|
78
+ | `example_items` | Starter collection (replace with your own) |
79
+
80
+ _Update this table as you add collections._
@@ -33,9 +33,9 @@ pnpm dev # Start dev server
33
33
  pnpm deploy # Deploy frontend
34
34
 
35
35
  # All other commands
36
- pnpm dlx @lumerahq/cli platform plan
37
- pnpm dlx @lumerahq/cli platform apply
38
- pnpm dlx @lumerahq/cli platform destroy
36
+ pnpm dlx @lumerahq/cli plan
37
+ pnpm dlx @lumerahq/cli apply
38
+ pnpm dlx @lumerahq/cli destroy
39
39
  pnpm dlx @lumerahq/cli run scripts/seed-demo.py
40
40
  pnpm dlx @lumerahq/cli status
41
41
  ```
@@ -50,10 +50,10 @@ lumera login
50
50
  pnpm dev
51
51
 
52
52
  # With custom port
53
- lumera app dev --port 3000
53
+ lumera dev --port 3000
54
54
 
55
55
  # With ngrok tunnel
56
- lumera app dev --url https://my-tunnel.ngrok.io
56
+ lumera dev --url https://my-tunnel.ngrok.io
57
57
 
58
58
  # Plain vite (no Lumera registration)
59
59
  pnpm dev:vite
@@ -71,21 +71,21 @@ pnpm lint
71
71
  # Format code
72
72
  pnpm format
73
73
 
74
- # Run all checks (lint + typecheck) - use in CI
74
+ # Run all checks (lint + format + typecheck) - use in CI
75
75
  pnpm check:ci
76
76
  ```
77
77
 
78
78
  ### Deploying
79
79
 
80
80
  ```bash
81
- # Deploy frontend to S3
82
- lumera app deploy
81
+ # Deploy frontend
82
+ lumera apply app
83
83
 
84
- # Apply platform resources (collections, automations, hooks)
85
- lumera platform apply
84
+ # Apply all resources (collections, automations, hooks, app)
85
+ lumera apply
86
86
 
87
- # Preview platform changes first
88
- lumera platform plan
87
+ # Preview changes first
88
+ lumera plan
89
89
  ```
90
90
 
91
91
  ### Running Scripts
@@ -129,7 +129,7 @@ EOF
129
129
 
130
130
  ## Important Rules
131
131
 
132
- 1. **Authenticate first** - Before running any CLI or SDK commands, ensure the user has run `lumera login --local`. This stores credentials in `.lumera/credentials.json` which the SDK reads automatically.
132
+ 1. **Authenticate first** - Before running any CLI or SDK commands, ensure the user has run `lumera login`. This stores credentials in `.lumera/credentials.json` which the SDK reads automatically.
133
133
 
134
134
  2. **Source of truth is code** - `platform/` contains all schemas, automations, hooks. Update local code first, then deploy.
135
135
 
@@ -23,13 +23,13 @@ Lumera custom embedded app.
23
23
 
24
24
  ```bash
25
25
  # Development
26
- lumera app dev # Start dev server
27
- lumera app dev --port 3000 # Custom port
26
+ lumera dev # Start dev server
27
+ lumera dev --port 3000 # Custom port
28
28
 
29
29
  # Deployment
30
- lumera app deploy # Build and deploy frontend
31
- lumera platform apply # Apply collections, automations, hooks
32
- lumera platform plan # Preview platform changes
30
+ lumera apply app # Build and deploy frontend
31
+ lumera apply # Apply all resources
32
+ lumera plan # Preview changes
33
33
 
34
34
  # Scripts
35
35
  lumera run scripts/seed-demo.py # Run seed script
@@ -29,5 +29,10 @@
29
29
  "semicolons": "always",
30
30
  "trailingCommas": "es5"
31
31
  }
32
+ },
33
+ "css": {
34
+ "parser": {
35
+ "tailwindDirectives": true
36
+ }
32
37
  }
33
38
  }
@@ -12,16 +12,16 @@
12
12
  "name": "{{projectTitle}}"
13
13
  },
14
14
  "scripts": {
15
- "dev": "pnpm dlx @lumerahq/cli app dev",
16
- "deploy": "pnpm dlx @lumerahq/cli app deploy",
15
+ "dev": "pnpm dlx @lumerahq/cli dev",
16
+ "deploy": "pnpm dlx @lumerahq/cli apply app",
17
17
  "dev:vite": "vite",
18
18
  "build": "vite build && tsc",
19
19
  "preview": "vite preview",
20
- "typecheck": "tsc --noEmit",
20
+ "typecheck": "tsr generate && tsc --noEmit",
21
21
  "lint": "biome lint --write .",
22
22
  "format": "biome format --write .",
23
23
  "check": "biome check --write .",
24
- "check:ci": "biome check . && tsc --noEmit"
24
+ "check:ci": "biome check . && tsr generate && tsc --noEmit"
25
25
  },
26
26
  "dependencies": {
27
27
  "@lumerahq/ui": "^0.5.0",
@@ -37,6 +37,7 @@
37
37
  "devDependencies": {
38
38
  "@biomejs/biome": "^2.0.0",
39
39
  "@tailwindcss/vite": "^4.0.6",
40
+ "@tanstack/router-cli": "1.155.0",
40
41
  "@types/react": "^19.2.0",
41
42
  "@types/react-dom": "^19.2.0",
42
43
  "@vitejs/plugin-react": "^5.0.4",
@@ -22,7 +22,5 @@
22
22
  "type": "text"
23
23
  }
24
24
  ],
25
- "indexes": [
26
- { "fields": ["source_id"], "unique": true }
27
- ]
25
+ "indexes": [{ "fields": ["source_id"], "unique": true }]
28
26
  }
@@ -1,5 +1,5 @@
1
1
  # /// script
2
- # dependencies = ["lumera-sdk"]
2
+ # dependencies = ["lumera"]
3
3
  # ///
4
4
  """
5
5
  Seed demo data into Lumera (idempotent - safe to run multiple times).
@@ -1,7 +1,7 @@
1
+ import { cn } from '@lumerahq/ui/lib';
1
2
  import { Link, useRouterState } from '@tanstack/react-router';
2
- import { LayoutDashboard, Settings, ChevronLeft, ChevronRight } from 'lucide-react';
3
+ import { ChevronLeft, ChevronRight, LayoutDashboard, Settings } from 'lucide-react';
3
4
  import { useState } from 'react';
4
- import { cn } from '@lumerahq/ui/lib';
5
5
 
6
6
  type NavItem = {
7
7
  to: string;
@@ -37,9 +37,7 @@ export function Sidebar() {
37
37
  <div className="size-8 rounded-lg bg-primary flex items-center justify-center text-primary-foreground font-bold text-sm">
38
38
  {{projectInitial}}
39
39
  </div>
40
- {!collapsed && (
41
- <span className="font-semibold text-sm">{{projectTitle}}</span>
42
- )}
40
+ {!collapsed && <span className="font-semibold text-sm">{{projectTitle}}</span>}
43
41
  </Link>
44
42
  </div>
45
43
 
@@ -16,15 +16,9 @@ export function StatCard({ title, value, subtitle, icon, className }: StatCardPr
16
16
  <div className="space-y-1">
17
17
  <p className="text-sm font-medium text-muted-foreground">{title}</p>
18
18
  <p className="text-2xl font-semibold tracking-tight">{value}</p>
19
- {subtitle && (
20
- <p className="text-sm text-muted-foreground">{subtitle}</p>
21
- )}
19
+ {subtitle && <p className="text-sm text-muted-foreground">{subtitle}</p>}
22
20
  </div>
23
- {icon && (
24
- <div className="rounded-lg bg-muted p-2.5 text-muted-foreground">
25
- {icon}
26
- </div>
27
- )}
21
+ {icon && <div className="rounded-lg bg-muted p-2.5 text-muted-foreground">{icon}</div>}
28
22
  </div>
29
23
  </div>
30
24
  );
@@ -1,4 +1,4 @@
1
- import { pbList, type PbRecord } from '@lumerahq/ui/lib';
1
+ import { type PbRecord, pbList } from '@lumerahq/ui/lib';
2
2
 
3
3
  export type ExampleRecord = PbRecord & {
4
4
  name: string;
@@ -1,16 +1,10 @@
1
- import { StrictMode, createContext, useEffect, useRef, useState } from 'react';
2
- import ReactDOM from 'react-dom/client';
1
+ import { type HostPayload, isEmbedded, onInitMessage, postReadyMessage } from '@lumerahq/ui/lib';
3
2
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
- import { RouterProvider, createRouter, createHashHistory } from '@tanstack/react-router';
3
+ import { createHashHistory, createRouter, RouterProvider } from '@tanstack/react-router';
4
+ import { createContext, StrictMode, useEffect, useRef, useState } from 'react';
5
+ import ReactDOM from 'react-dom/client';
5
6
  import { Toaster } from 'sonner';
6
7
 
7
- import {
8
- isEmbedded,
9
- onInitMessage,
10
- postReadyMessage,
11
- type HostPayload,
12
- } from '@lumerahq/ui/lib';
13
-
14
8
  import { routeTree } from './routeTree.gen';
15
9
  import '@lumerahq/ui/styles.css';
16
10
  import './styles.css';
@@ -1,4 +1,4 @@
1
- import { Outlet, createRootRoute } from '@tanstack/react-router';
1
+ import { createRootRoute, Outlet } from '@tanstack/react-router';
2
2
  import { AppLayout } from '../components/layout';
3
3
 
4
4
  export const Route = createRootRoute({
@@ -1,8 +1,8 @@
1
1
  import { createFileRoute } from '@tanstack/react-router';
2
+ import { Activity, FileText, TrendingUp, Users } from 'lucide-react';
2
3
  import { useContext } from 'react';
3
- import { Users, FileText, Activity, TrendingUp } from 'lucide-react';
4
- import { AuthContext } from '../main';
5
4
  import { StatCard } from '../components/StatCard';
5
+ import { AuthContext } from '../main';
6
6
 
7
7
  export const Route = createFileRoute('/')({
8
8
  component: HomePage,
@@ -16,9 +16,7 @@ function HomePage() {
16
16
  {/* Header */}
17
17
  <div>
18
18
  <h1 className="text-2xl font-semibold">Dashboard</h1>
19
- <p className="text-muted-foreground mt-1">
20
- Welcome back, {auth?.user?.name ?? 'User'}
21
- </p>
19
+ <p className="text-muted-foreground mt-1">Welcome back, {auth?.user?.name ?? 'User'}</p>
22
20
  </div>
23
21
 
24
22
  {/* Stats Grid */}
@@ -9,16 +9,12 @@ function SettingsPage() {
9
9
  <div className="space-y-6">
10
10
  <div>
11
11
  <h1 className="text-2xl font-semibold">Settings</h1>
12
- <p className="text-muted-foreground mt-1">
13
- Manage your application settings
14
- </p>
12
+ <p className="text-muted-foreground mt-1">Manage your application settings</p>
15
13
  </div>
16
14
 
17
15
  <div className="rounded-xl border bg-card p-6">
18
16
  <h2 className="font-semibold mb-4">General</h2>
19
- <p className="text-sm text-muted-foreground">
20
- Add your settings UI here
21
- </p>
17
+ <p className="text-sm text-muted-foreground">Add your settings UI here</p>
22
18
  </div>
23
19
  </div>
24
20
  );
@@ -35,6 +35,10 @@ body {
35
35
  }
36
36
 
37
37
  @layer base {
38
- * { @apply border-border; }
39
- body { @apply bg-background text-foreground; }
38
+ * {
39
+ @apply border-border;
40
+ }
41
+ body {
42
+ @apply bg-background text-foreground;
43
+ }
40
44
  }
@@ -1,7 +1,7 @@
1
- import { defineConfig } from 'vite';
2
- import viteReact from '@vitejs/plugin-react';
3
1
  import tailwindcss from '@tailwindcss/vite';
4
2
  import { tanstackRouter } from '@tanstack/router-plugin/vite';
3
+ import viteReact from '@vitejs/plugin-react';
4
+ import { defineConfig } from 'vite';
5
5
 
6
6
  export default defineConfig({
7
7
  base: './', // Use relative paths for S3 hosting at subpaths
@@ -22,6 +22,7 @@ export default defineConfig({
22
22
  server: {
23
23
  allowedHosts: [
24
24
  'mac.lumerahq.com',
25
+ 'untunable-del-nonephemerally.ngrok-free.dev',
25
26
  ],
26
27
  },
27
28
  });