@lumerahq/cli 0.13.1 → 0.14.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
@@ -23,12 +23,15 @@ lumera dev # Start dev server
23
23
  lumera apply app # Deploy frontend
24
24
  lumera destroy app # Delete app from Lumera
25
25
 
26
- lumera plan # Preview infrastructure changes
27
- lumera apply # Apply collections, automations, hooks
26
+ lumera plan # Preview changes (with inline diffs)
27
+ lumera diff <resource> # Full diff between local and remote
28
+ lumera apply # Apply collections, automations, hooks, agents
28
29
  lumera pull # Pull remote state to local
30
+ lumera list # List resources with sync status
31
+ lumera show <resource> # Show resource details
29
32
  lumera destroy # Delete remote resources
30
33
 
31
- lumera run <script> # Run Python scripts locally
34
+ lumera run <target> # Run script, automation, or invoke agent
32
35
  ```
33
36
 
34
37
  ## Scaffolding Projects
@@ -5,10 +5,19 @@ import {
5
5
  // src/lib/api.ts
6
6
  import { readFileSync } from "fs";
7
7
  import { fileURLToPath } from "url";
8
- import { dirname, join } from "path";
8
+ import { dirname, resolve } from "path";
9
9
  var __filename = fileURLToPath(import.meta.url);
10
- var __dirname = dirname(__filename);
11
- var pkg = JSON.parse(readFileSync(join(__dirname, "../../package.json"), "utf-8"));
10
+ var __pkgDir = dirname(__filename);
11
+ while (__pkgDir !== "/") {
12
+ try {
13
+ const candidate = resolve(__pkgDir, "package.json");
14
+ const parsed = JSON.parse(readFileSync(candidate, "utf-8"));
15
+ if (parsed.name === "@lumerahq/cli") break;
16
+ } catch {
17
+ }
18
+ __pkgDir = resolve(__pkgDir, "..");
19
+ }
20
+ var pkg = JSON.parse(readFileSync(resolve(__pkgDir, "package.json"), "utf-8"));
12
21
  var CLI_USER_AGENT = `lumera-cli/${pkg.version}`;
13
22
  var ApiClient = class {
14
23
  baseUrl;
@@ -190,10 +199,10 @@ function createApiClient(token, baseUrl) {
190
199
  // src/lib/env.ts
191
200
  import { config } from "dotenv";
192
201
  import { existsSync } from "fs";
193
- import { resolve } from "path";
202
+ import { resolve as resolve2 } from "path";
194
203
  function loadEnv(cwd = process.cwd()) {
195
- const envPath = resolve(cwd, ".env");
196
- const envLocalPath = resolve(cwd, ".env.local");
204
+ const envPath = resolve2(cwd, ".env");
205
+ const envLocalPath = resolve2(cwd, ".env.local");
197
206
  if (existsSync(envPath)) {
198
207
  config({ path: envPath });
199
208
  }
@@ -136,20 +136,37 @@ async function registerDevApp(apiBase, token, appName, appTitle, appUrl) {
136
136
  throw new Error(`Failed to register app: ${await res.text()}`);
137
137
  }
138
138
  }
139
+ function detectRunner(projectRoot) {
140
+ if (existsSync(resolve(projectRoot, "bun.lockb")) || existsSync(resolve(projectRoot, "bun.lock"))) {
141
+ return ["bunx"];
142
+ }
143
+ if (existsSync(resolve(projectRoot, "pnpm-lock.yaml"))) {
144
+ return ["pnpm", "exec"];
145
+ }
146
+ try {
147
+ execSync("pnpm --version", { stdio: "ignore" });
148
+ return ["pnpm", "exec"];
149
+ } catch {
150
+ return ["bunx"];
151
+ }
152
+ }
139
153
  async function dev(options) {
140
154
  const {
141
155
  token,
142
156
  appName,
143
157
  appTitle,
144
158
  port,
159
+ host,
145
160
  appUrl = `http://localhost:${port}`,
146
161
  apiUrl
147
162
  } = options;
163
+ const projectRoot = process.cwd();
148
164
  console.log();
149
165
  console.log(pc.cyan(pc.bold(` ${appTitle} - Dev Server`)));
150
166
  console.log();
151
167
  console.log(pc.dim(" Configuration:"));
152
168
  console.log(pc.dim(` Port: ${port}`));
169
+ if (host) console.log(pc.dim(` Host: ${host}`));
153
170
  console.log(pc.dim(` URL: ${appUrl}`));
154
171
  console.log();
155
172
  console.log(pc.dim(" Registering app with Lumera..."));
@@ -175,7 +192,17 @@ async function dev(options) {
175
192
  ([key]) => safeEnvPrefixes.some((prefix) => key.startsWith(prefix)) || safeEnvKeys.includes(key)
176
193
  )
177
194
  );
178
- const vite = spawn("pnpm", ["exec", "vite", "--port", String(port)], {
195
+ const runner = detectRunner(projectRoot);
196
+ const viteArgs = ["vite", "--port", String(port)];
197
+ const sandboxConfig = resolve(projectRoot, "vite.sandbox.config.ts");
198
+ if (existsSync(sandboxConfig)) {
199
+ viteArgs.push("--config", "vite.sandbox.config.ts");
200
+ }
201
+ if (host) {
202
+ viteArgs.push("--host", host);
203
+ }
204
+ const [cmd, ...cmdArgs] = runner;
205
+ const vite = spawn(cmd, [...cmdArgs, ...viteArgs], {
179
206
  stdio: "inherit",
180
207
  env: {
181
208
  ...filteredEnv,
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-CDZZ3JYU.js";
3
+ } from "./chunk-XTRDJLIA.js";
4
4
  import {
5
5
  createApiClient,
6
6
  loadEnv
7
- } from "./chunk-7ZGIC6F7.js";
7
+ } from "./chunk-NL6MEHA3.js";
8
8
  import {
9
9
  getToken
10
10
  } from "./chunk-NDLYGKS6.js";
@@ -49,6 +49,7 @@ ${pc.dim("Description:")}
49
49
 
50
50
  ${pc.dim("Options:")}
51
51
  --port <number> Dev server port (default: 8080)
52
+ --host <host> Host to bind to (e.g., 0.0.0.0 for all interfaces)
52
53
  --url <url> App URL for dev mode (default: http://localhost:{port})
53
54
  --skip-setup Skip auto-apply on first run
54
55
  --help, -h Show this help
@@ -140,12 +141,14 @@ async function dev2(args) {
140
141
  }
141
142
  }
142
143
  const port = Number(flags.port || process.env.PORT || "8080");
144
+ const host = typeof flags.host === "string" ? flags.host : void 0;
143
145
  const appUrl = typeof flags.url === "string" ? flags.url : process.env.APP_URL;
144
146
  await dev({
145
147
  token,
146
148
  appName,
147
149
  appTitle,
148
150
  port,
151
+ host,
149
152
  appUrl,
150
153
  apiUrl
151
154
  });
package/dist/index.js CHANGED
@@ -81,6 +81,7 @@ var COMMANDS = [
81
81
  "destroy",
82
82
  "list",
83
83
  "show",
84
+ "diff",
84
85
  "dev",
85
86
  "run",
86
87
  "init",
@@ -136,6 +137,7 @@ ${pc.dim("Resource Commands:")}
136
137
  ${pc.cyan("destroy")} [resource] Delete resources
137
138
  ${pc.cyan("list")} [type] List resources with status
138
139
  ${pc.cyan("show")} <resource> Show resource details
140
+ ${pc.cyan("diff")} <resource> Show full diff between local and remote
139
141
 
140
142
  ${pc.dim("Development:")}
141
143
  ${pc.cyan("dev")} Start dev server
@@ -210,29 +212,32 @@ async function main() {
210
212
  switch (command) {
211
213
  // Resource commands
212
214
  case "plan":
213
- await import("./resources-PKLA2XDG.js").then((m) => m.plan(args.slice(1)));
215
+ await import("./resources-QKEUX3C3.js").then((m) => m.plan(args.slice(1)));
214
216
  break;
215
217
  case "apply":
216
- await import("./resources-PKLA2XDG.js").then((m) => m.apply(args.slice(1)));
218
+ await import("./resources-QKEUX3C3.js").then((m) => m.apply(args.slice(1)));
217
219
  break;
218
220
  case "pull":
219
- await import("./resources-PKLA2XDG.js").then((m) => m.pull(args.slice(1)));
221
+ await import("./resources-QKEUX3C3.js").then((m) => m.pull(args.slice(1)));
220
222
  break;
221
223
  case "destroy":
222
- await import("./resources-PKLA2XDG.js").then((m) => m.destroy(args.slice(1)));
224
+ await import("./resources-QKEUX3C3.js").then((m) => m.destroy(args.slice(1)));
223
225
  break;
224
226
  case "list":
225
- await import("./resources-PKLA2XDG.js").then((m) => m.list(args.slice(1)));
227
+ await import("./resources-QKEUX3C3.js").then((m) => m.list(args.slice(1)));
226
228
  break;
227
229
  case "show":
228
- await import("./resources-PKLA2XDG.js").then((m) => m.show(args.slice(1)));
230
+ await import("./resources-QKEUX3C3.js").then((m) => m.show(args.slice(1)));
231
+ break;
232
+ case "diff":
233
+ await import("./resources-QKEUX3C3.js").then((m) => m.diff(args.slice(1)));
229
234
  break;
230
235
  // Development
231
236
  case "dev":
232
- await import("./dev-LNH47WSS.js").then((m) => m.dev(args.slice(1)));
237
+ await import("./dev-5EAZUQ2S.js").then((m) => m.dev(args.slice(1)));
233
238
  break;
234
239
  case "run":
235
- await import("./run-3UBV3SVA.js").then((m) => m.run(args.slice(1)));
240
+ await import("./run-SPC4YXWR.js").then((m) => m.run(args.slice(1)));
236
241
  break;
237
242
  // Project
238
243
  case "init":
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-CDZZ3JYU.js";
3
+ } from "./chunk-XTRDJLIA.js";
4
4
  import {
5
5
  createApiClient,
6
6
  loadEnv
7
- } from "./chunk-7ZGIC6F7.js";
7
+ } from "./chunk-NL6MEHA3.js";
8
8
  import {
9
9
  getToken
10
10
  } from "./chunk-NDLYGKS6.js";
@@ -31,6 +31,55 @@ function detectPackageManager() {
31
31
  }
32
32
  return "npm";
33
33
  }
34
+ function computeLineDiff(oldText, newText) {
35
+ const oldLines = (oldText || "").trimEnd().split("\n");
36
+ const newLines = (newText || "").trimEnd().split("\n");
37
+ const m = oldLines.length;
38
+ const n = newLines.length;
39
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
40
+ for (let i2 = 1; i2 <= m; i2++) {
41
+ for (let j2 = 1; j2 <= n; j2++) {
42
+ dp[i2][j2] = oldLines[i2 - 1] === newLines[j2 - 1] ? dp[i2 - 1][j2 - 1] + 1 : Math.max(dp[i2 - 1][j2], dp[i2][j2 - 1]);
43
+ }
44
+ }
45
+ const result = [];
46
+ let i = m, j = n;
47
+ const stack = [];
48
+ while (i > 0 || j > 0) {
49
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
50
+ stack.push({ type: " ", line: oldLines[i - 1] });
51
+ i--;
52
+ j--;
53
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
54
+ stack.push({ type: "+", line: newLines[j - 1] });
55
+ j--;
56
+ } else {
57
+ stack.push({ type: "-", line: oldLines[i - 1] });
58
+ i--;
59
+ }
60
+ }
61
+ stack.reverse();
62
+ const contextLines = 2;
63
+ const changeIndices = /* @__PURE__ */ new Set();
64
+ stack.forEach((entry, idx) => {
65
+ if (entry.type !== " ") {
66
+ for (let c = Math.max(0, idx - contextLines); c <= Math.min(stack.length - 1, idx + contextLines); c++) {
67
+ changeIndices.add(c);
68
+ }
69
+ }
70
+ });
71
+ let lastIncluded = -2;
72
+ for (let idx = 0; idx < stack.length; idx++) {
73
+ if (changeIndices.has(idx)) {
74
+ if (lastIncluded < idx - 1) {
75
+ result.push({ type: " ", line: "..." });
76
+ }
77
+ result.push(stack[idx]);
78
+ lastIncluded = idx;
79
+ }
80
+ }
81
+ return result;
82
+ }
34
83
  function showPlanHelp() {
35
84
  console.log(`
36
85
  ${pc.dim("Usage:")}
@@ -89,10 +138,15 @@ ${pc.dim("Examples:")}
89
138
  function showPullHelp() {
90
139
  console.log(`
91
140
  ${pc.dim("Usage:")}
92
- lumera pull [resource]
141
+ lumera pull [resource] [--force]
93
142
 
94
143
  ${pc.dim("Description:")}
95
144
  Download remote state to local files.
145
+ Refuses to overwrite local files that have uncommitted changes
146
+ (use --force to override, or 'lumera diff' to inspect first).
147
+
148
+ ${pc.dim("Options:")}
149
+ --force, -f Overwrite local files even if they have changes
96
150
 
97
151
  ${pc.dim("Resources:")}
98
152
  (none) Pull all resources
@@ -105,9 +159,10 @@ ${pc.dim("Resources:")}
105
159
  agents/<name> Pull single agent
106
160
 
107
161
  ${pc.dim("Examples:")}
108
- lumera pull # Pull all resources
109
- lumera pull collections # Pull only collections
110
- lumera pull automations/sync # Pull single automation
162
+ lumera pull # Pull all (safe \u2014 warns on conflicts)
163
+ lumera pull agents # Pull only agents
164
+ lumera pull --force # Pull all, overwrite local changes
165
+ lumera diff agents/my_agent # Inspect before pulling
111
166
  `);
112
167
  }
113
168
  function showDestroyHelp() {
@@ -502,12 +557,17 @@ async function planAutomations(api, localAutomations) {
502
557
  if (codeChanged) details.push("code");
503
558
  if (nameChanged) details.push("name");
504
559
  if (descChanged) details.push("description");
560
+ const textDiffs = [];
561
+ if (codeChanged) {
562
+ textDiffs.push({ field: "main.py", oldText: remote.code || "", newText: code });
563
+ }
505
564
  changes.push({
506
565
  type: "update",
507
566
  resource: "automation",
508
567
  id: automation.external_id,
509
568
  name: automation.name,
510
- details: `changed: ${details.join(", ")}`
569
+ details: `changed: ${details.join(", ")}`,
570
+ textDiffs
511
571
  });
512
572
  }
513
573
  }
@@ -539,12 +599,17 @@ async function planHooks(api, localHooks, collections) {
539
599
  const details = [];
540
600
  if (scriptChanged) details.push("script");
541
601
  if (eventChanged) details.push("trigger");
602
+ const textDiffs = [];
603
+ if (scriptChanged) {
604
+ textDiffs.push({ field: fileName, oldText: remote.script || "", newText: script });
605
+ }
542
606
  changes.push({
543
607
  type: "update",
544
608
  resource: "hook",
545
609
  id: hook.external_id,
546
610
  name: `${hook.collection}.${hook.trigger}`,
547
- details: `changed: ${details.join(", ")}`
611
+ details: `changed: ${details.join(", ")}`,
612
+ textDiffs
548
613
  });
549
614
  }
550
615
  }
@@ -854,6 +919,7 @@ function loadLocalAgents(platformDir, filterName, appName) {
854
919
  const agentDir = join(agentsDir, entry.name);
855
920
  const configPath = join(agentDir, "config.json");
856
921
  const promptPath = join(agentDir, "system_prompt.md");
922
+ const policyPath = join(agentDir, "policy.js");
857
923
  if (!existsSync(configPath)) {
858
924
  errors.push(`${entry.name}: missing config.json`);
859
925
  continue;
@@ -880,7 +946,8 @@ function loadLocalAgents(platformDir, filterName, appName) {
880
946
  continue;
881
947
  }
882
948
  const systemPrompt = readFileSync(promptPath, "utf-8");
883
- agents.push({ agent: config, systemPrompt });
949
+ const policyScript = existsSync(policyPath) ? readFileSync(policyPath, "utf-8") : "";
950
+ agents.push({ agent: config, systemPrompt, policyScript });
884
951
  } catch (e) {
885
952
  errors.push(`${entry.name}: failed to parse config.json - ${e}`);
886
953
  }
@@ -900,7 +967,7 @@ async function planAgents(api, localAgents) {
900
967
  const remoteByExternalId = new Map(
901
968
  remoteAgents.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a])
902
969
  );
903
- for (const { agent, systemPrompt } of localAgents) {
970
+ for (const { agent, systemPrompt, policyScript } of localAgents) {
904
971
  const remote = remoteByExternalId.get(agent.external_id);
905
972
  if (!remote) {
906
973
  changes.push({ type: "create", resource: "agent", id: agent.external_id, name: agent.name });
@@ -909,8 +976,18 @@ async function planAgents(api, localAgents) {
909
976
  if (remote.name !== agent.name) diffs.push("name");
910
977
  if ((remote.description || "") !== (agent.description || "")) diffs.push("description");
911
978
  if ((remote.system_prompt || "").trim() !== systemPrompt.trim()) diffs.push("system_prompt");
979
+ if ((remote.model || "") !== (agent.model || "")) diffs.push("model");
980
+ if ((remote.policy_script || "").trim() !== (policyScript || "").trim()) diffs.push("policy_script");
981
+ if ((remote.policy_enabled || false) !== (agent.policy_enabled || false)) diffs.push("policy_enabled");
912
982
  if (diffs.length > 0) {
913
- changes.push({ type: "update", resource: "agent", id: agent.external_id, name: agent.name, details: `changed: ${diffs.join(", ")}` });
983
+ const textDiffs = [];
984
+ if (diffs.includes("system_prompt")) {
985
+ textDiffs.push({ field: "system_prompt.md", oldText: remote.system_prompt || "", newText: systemPrompt });
986
+ }
987
+ if (diffs.includes("policy_script")) {
988
+ textDiffs.push({ field: "policy.js", oldText: remote.policy_script || "", newText: policyScript });
989
+ }
990
+ changes.push({ type: "update", resource: "agent", id: agent.external_id, name: agent.name, details: `changed: ${diffs.join(", ")}`, textDiffs });
914
991
  }
915
992
  }
916
993
  }
@@ -932,7 +1009,7 @@ async function applyAgents(api, localAgents) {
932
1009
  console.log(pc.yellow(` \u26A0 Could not fetch skills for resolution: ${e}`));
933
1010
  }
934
1011
  }
935
- for (const { agent, systemPrompt } of localAgents) {
1012
+ for (const { agent, systemPrompt, policyScript } of localAgents) {
936
1013
  const remote = remoteByExternalId.get(agent.external_id);
937
1014
  const skillIds = [];
938
1015
  if (agent.skills) {
@@ -950,7 +1027,10 @@ async function applyAgents(api, localAgents) {
950
1027
  name: agent.name,
951
1028
  description: agent.description || "",
952
1029
  system_prompt: systemPrompt,
953
- skill_ids: skillIds
1030
+ model: agent.model || "",
1031
+ skill_ids: skillIds,
1032
+ policy_script: policyScript || "",
1033
+ policy_enabled: agent.policy_enabled || false
954
1034
  };
955
1035
  try {
956
1036
  if (remote) {
@@ -997,9 +1077,14 @@ async function pullAgents(api, platformDir, filterName) {
997
1077
  name: agent.name
998
1078
  };
999
1079
  if (agent.description) config.description = agent.description;
1080
+ if (agent.model) config.model = agent.model;
1000
1081
  if (skillSlugs.length > 0) config.skills = skillSlugs;
1082
+ if (agent.policy_enabled) config.policy_enabled = true;
1001
1083
  writeFileSync(join(agentDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
1002
1084
  writeFileSync(join(agentDir, "system_prompt.md"), agent.system_prompt || "");
1085
+ if (agent.policy_script) {
1086
+ writeFileSync(join(agentDir, "policy.js"), agent.policy_script);
1087
+ }
1003
1088
  console.log(pc.green(" \u2713"), `${agent.name} \u2192 agents/${dirName}/`);
1004
1089
  }
1005
1090
  }
@@ -1619,6 +1704,26 @@ async function plan(args) {
1619
1704
  }
1620
1705
  console.log();
1621
1706
  }
1707
+ if (change.textDiffs && change.textDiffs.length > 0) {
1708
+ const maxDiffLines = 20;
1709
+ for (const td of change.textDiffs) {
1710
+ const diffLines = computeLineDiff(td.oldText, td.newText);
1711
+ if (diffLines.length > 0) {
1712
+ console.log(pc.dim(` --- ${td.field}`));
1713
+ const shown = diffLines.slice(0, maxDiffLines);
1714
+ for (const dl of shown) {
1715
+ if (dl.type === "+") console.log(pc.green(` + ${dl.line}`));
1716
+ else if (dl.type === "-") console.log(pc.red(` - ${dl.line}`));
1717
+ else console.log(pc.dim(` ${dl.line}`));
1718
+ }
1719
+ if (diffLines.length > maxDiffLines) {
1720
+ const remaining = diffLines.length - maxDiffLines;
1721
+ console.log(pc.dim(` ... ${remaining} more lines \u2014 use ${pc.cyan(`lumera diff ${change.resource}s/${change.name}`)} for full diff`));
1722
+ }
1723
+ console.log();
1724
+ }
1725
+ }
1726
+ }
1622
1727
  }
1623
1728
  console.log();
1624
1729
  console.log(pc.dim(` Run 'lumera apply' to apply these changes.`));
@@ -1725,10 +1830,63 @@ async function pull(args) {
1725
1830
  showPullHelp();
1726
1831
  return;
1727
1832
  }
1728
- loadEnv();
1833
+ const force = args.includes("--force") || args.includes("-f");
1834
+ const filteredArgs = args.filter((a) => a !== "--force" && a !== "-f");
1835
+ const projectRoot = findProjectRoot();
1836
+ loadEnv(projectRoot);
1729
1837
  const platformDir = getPlatformDir();
1730
1838
  const api = createApiClient();
1731
- const { type, name } = parseResource(args[0]);
1839
+ const appName = getAppName(projectRoot);
1840
+ const { type, name } = parseResource(filteredArgs[0]);
1841
+ if (!force) {
1842
+ const conflicts = [];
1843
+ let collections;
1844
+ try {
1845
+ const remoteCollections = await api.listCollections();
1846
+ collections = new Map(remoteCollections.map((c) => [c.name, c.id]));
1847
+ } catch {
1848
+ collections = /* @__PURE__ */ new Map();
1849
+ }
1850
+ if (!type || type === "agents") {
1851
+ const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
1852
+ if (localAgents.length > 0) {
1853
+ const changes = await planAgents(api, localAgents);
1854
+ for (const c of changes) {
1855
+ if (c.type === "update") conflicts.push(`agents/${c.name}`);
1856
+ }
1857
+ }
1858
+ }
1859
+ if (!type || type === "automations") {
1860
+ const localAutomations = loadLocalAutomations(platformDir, name || void 0, appName);
1861
+ if (localAutomations.length > 0) {
1862
+ const changes = await planAutomations(api, localAutomations);
1863
+ for (const c of changes) {
1864
+ if (c.type === "update") conflicts.push(`automations/${c.name}`);
1865
+ }
1866
+ }
1867
+ }
1868
+ if (!type || type === "hooks") {
1869
+ const localHooks = loadLocalHooks(platformDir, name || void 0, appName);
1870
+ if (localHooks.length > 0) {
1871
+ const changes = await planHooks(api, localHooks, collections);
1872
+ for (const c of changes) {
1873
+ if (c.type === "update") conflicts.push(`hooks/${c.name}`);
1874
+ }
1875
+ }
1876
+ }
1877
+ if (conflicts.length > 0) {
1878
+ console.log();
1879
+ console.log(pc.yellow(` \u26A0 ${conflicts.length} local file${conflicts.length > 1 ? "s have" : " has"} changes that would be lost:`));
1880
+ for (const f of conflicts) {
1881
+ console.log(pc.dim(` ${f}`));
1882
+ }
1883
+ console.log();
1884
+ console.log(pc.dim(` Use ${pc.cyan("lumera diff <resource>")} to inspect changes.`));
1885
+ console.log(pc.dim(` Use ${pc.cyan("lumera pull --force")} to overwrite local files.`));
1886
+ console.log();
1887
+ process.exit(1);
1888
+ }
1889
+ }
1732
1890
  console.log();
1733
1891
  console.log(pc.cyan(pc.bold(" Pull")));
1734
1892
  console.log(pc.dim(` Downloading remote state to ${platformDir}/...`));
@@ -1885,9 +2043,164 @@ async function show(args) {
1885
2043
  await showResource(api, platformDir, type, name, appName);
1886
2044
  }
1887
2045
  }
2046
+ function showDiffHelp() {
2047
+ console.log(`
2048
+ ${pc.bold("lumera diff")} - Show full diff between local and remote state
2049
+
2050
+ ${pc.dim("Usage:")}
2051
+ lumera diff <resource>
2052
+
2053
+ ${pc.dim("Resources:")}
2054
+ agents/<name> Diff agent (system_prompt, policy_script)
2055
+ automations/<name> Diff automation code
2056
+ hooks/<name> Diff hook script
2057
+
2058
+ ${pc.dim("Examples:")}
2059
+ lumera diff agents/bank_activity_matcher
2060
+ lumera diff automations/sync
2061
+ lumera diff hooks/encoding_protect_create
2062
+ `);
2063
+ }
2064
+ function renderFullDiff(field, oldText, newText) {
2065
+ const diffLines = computeLineDiff(oldText, newText);
2066
+ if (diffLines.length === 0) {
2067
+ console.log(pc.dim(` ${field}: no changes`));
2068
+ return;
2069
+ }
2070
+ console.log(pc.bold(` --- ${field}`));
2071
+ for (const dl of diffLines) {
2072
+ if (dl.type === "+") console.log(pc.green(` + ${dl.line}`));
2073
+ else if (dl.type === "-") console.log(pc.red(` - ${dl.line}`));
2074
+ else console.log(pc.dim(` ${dl.line}`));
2075
+ }
2076
+ console.log();
2077
+ }
2078
+ async function diff(args) {
2079
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
2080
+ showDiffHelp();
2081
+ if (args.length === 0) process.exit(1);
2082
+ return;
2083
+ }
2084
+ const projectRoot = findProjectRoot();
2085
+ loadEnv(projectRoot);
2086
+ const platformDir = getPlatformDir();
2087
+ const api = createApiClient();
2088
+ const appName = getAppName(projectRoot);
2089
+ const { type, name } = parseResource(args[0]);
2090
+ if (!type || !name) {
2091
+ console.log(pc.red(` Invalid resource path: ${args[0]}`));
2092
+ console.log(pc.dim(" Use format: <type>/<name> (e.g., agents/bank_activity_matcher)"));
2093
+ process.exit(1);
2094
+ }
2095
+ console.log();
2096
+ if (type === "agents") {
2097
+ const localAgents = loadLocalAgents(platformDir, name, appName);
2098
+ const remoteAgents = await api.listAgents();
2099
+ const local = localAgents[0];
2100
+ const localExtId = local?.agent.external_id;
2101
+ const remote = remoteAgents.find(
2102
+ (a) => a.external_id === name || a.name === name || localExtId && a.external_id === localExtId
2103
+ );
2104
+ if (!local && !remote) {
2105
+ console.log(pc.red(` Agent "${name}" not found locally or remotely`));
2106
+ process.exit(1);
2107
+ }
2108
+ if (!local) {
2109
+ console.log(pc.cyan(` Agent "${name}" exists only remotely (not in local files)`));
2110
+ process.exit(0);
2111
+ }
2112
+ if (!remote) {
2113
+ console.log(pc.yellow(` Agent "${name}" exists only locally (not yet deployed)`));
2114
+ process.exit(0);
2115
+ }
2116
+ console.log(pc.bold(` Agent: ${local.agent.name}`));
2117
+ console.log();
2118
+ if (remote.name !== local.agent.name)
2119
+ console.log(` name: ${pc.red(remote.name)} \u2192 ${pc.green(local.agent.name)}`);
2120
+ if ((remote.description || "") !== (local.agent.description || ""))
2121
+ console.log(` description: ${pc.red(remote.description || "(empty)")} \u2192 ${pc.green(local.agent.description || "(empty)")}`);
2122
+ if ((remote.model || "") !== (local.agent.model || ""))
2123
+ console.log(` model: ${pc.red(remote.model || "(default)")} \u2192 ${pc.green(local.agent.model || "(default)")}`);
2124
+ if ((remote.policy_enabled || false) !== (local.agent.policy_enabled || false))
2125
+ console.log(` policy_enabled: ${pc.red(String(remote.policy_enabled || false))} \u2192 ${pc.green(String(local.agent.policy_enabled || false))}`);
2126
+ const promptChanged = (remote.system_prompt || "").trim() !== local.systemPrompt.trim();
2127
+ const policyChanged = (remote.policy_script || "").trim() !== (local.policyScript || "").trim();
2128
+ if (!promptChanged && !policyChanged && remote.name === local.agent.name && (remote.description || "") === (local.agent.description || "") && (remote.model || "") === (local.agent.model || "") && (remote.policy_enabled || false) === (local.agent.policy_enabled || false)) {
2129
+ console.log(pc.green(` \u2713 No changes`));
2130
+ } else {
2131
+ if (promptChanged) renderFullDiff("system_prompt.md", remote.system_prompt || "", local.systemPrompt);
2132
+ if (policyChanged) renderFullDiff("policy.js", remote.policy_script || "", local.policyScript);
2133
+ }
2134
+ } else if (type === "automations") {
2135
+ const localAutomations = loadLocalAutomations(platformDir, name, appName);
2136
+ const remoteAutomations = await api.listAutomations({ include_code: true });
2137
+ const local = localAutomations[0];
2138
+ const localExtId = local?.automation.external_id;
2139
+ const remote = remoteAutomations.find(
2140
+ (a) => a.external_id === name || a.name === name || localExtId && a.external_id === localExtId
2141
+ );
2142
+ if (!local && !remote) {
2143
+ console.log(pc.red(` Automation "${name}" not found locally or remotely`));
2144
+ process.exit(1);
2145
+ }
2146
+ if (!local) {
2147
+ console.log(pc.cyan(` Automation "${name}" exists only remotely`));
2148
+ process.exit(0);
2149
+ }
2150
+ if (!remote) {
2151
+ console.log(pc.yellow(` Automation "${name}" exists only locally (not yet deployed)`));
2152
+ process.exit(0);
2153
+ }
2154
+ console.log(pc.bold(` Automation: ${local.automation.name}`));
2155
+ console.log();
2156
+ if (remote.name !== local.automation.name)
2157
+ console.log(` name: ${pc.red(remote.name)} \u2192 ${pc.green(local.automation.name)}`);
2158
+ const codeChanged = (remote.code || "") !== local.code;
2159
+ if (!codeChanged && remote.name === local.automation.name) {
2160
+ console.log(pc.green(` \u2713 No changes`));
2161
+ } else if (codeChanged) {
2162
+ renderFullDiff("main.py", remote.code || "", local.code);
2163
+ }
2164
+ } else if (type === "hooks") {
2165
+ const localHooks = loadLocalHooks(platformDir, name, appName);
2166
+ const remoteHooks = await api.listHooks();
2167
+ const local = localHooks[0];
2168
+ const localExtId = local?.hook.external_id;
2169
+ const remote = remoteHooks.find(
2170
+ (h) => h.external_id === name || h.name === name || localExtId && h.external_id === localExtId
2171
+ );
2172
+ if (!local && !remote) {
2173
+ console.log(pc.red(` Hook "${name}" not found locally or remotely`));
2174
+ process.exit(1);
2175
+ }
2176
+ if (!local) {
2177
+ console.log(pc.cyan(` Hook "${name}" exists only remotely`));
2178
+ process.exit(0);
2179
+ }
2180
+ if (!remote) {
2181
+ console.log(pc.yellow(` Hook "${name}" exists only locally (not yet deployed)`));
2182
+ process.exit(0);
2183
+ }
2184
+ console.log(pc.bold(` Hook: ${local.hook.external_id}`));
2185
+ console.log();
2186
+ if (remote.event !== local.hook.trigger)
2187
+ console.log(` trigger: ${pc.red(remote.event)} \u2192 ${pc.green(local.hook.trigger)}`);
2188
+ const scriptChanged = (remote.script || "").trim() !== local.script.trim();
2189
+ if (!scriptChanged && remote.event === local.hook.trigger) {
2190
+ console.log(pc.green(` \u2713 No changes`));
2191
+ } else if (scriptChanged) {
2192
+ renderFullDiff(local.fileName, remote.script || "", local.script);
2193
+ }
2194
+ } else {
2195
+ console.log(pc.red(` Diff not supported for "${type}" \u2014 use agents, automations, or hooks`));
2196
+ process.exit(1);
2197
+ }
2198
+ console.log();
2199
+ }
1888
2200
  export {
1889
2201
  apply,
1890
2202
  destroy,
2203
+ diff,
1891
2204
  list,
1892
2205
  plan,
1893
2206
  pull,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createApiClient,
3
3
  loadEnv
4
- } from "./chunk-7ZGIC6F7.js";
4
+ } from "./chunk-NL6MEHA3.js";
5
5
  import {
6
6
  getToken
7
7
  } from "./chunk-NDLYGKS6.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {