@lumerahq/cli 0.13.2 → 0.15.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
  }
@@ -4,7 +4,7 @@ import {
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";
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-TCYJ5AEO.js").then((m) => m.plan(args.slice(1)));
215
+ await import("./resources-BFT7V6UR.js").then((m) => m.plan(args.slice(1)));
214
216
  break;
215
217
  case "apply":
216
- await import("./resources-TCYJ5AEO.js").then((m) => m.apply(args.slice(1)));
218
+ await import("./resources-BFT7V6UR.js").then((m) => m.apply(args.slice(1)));
217
219
  break;
218
220
  case "pull":
219
- await import("./resources-TCYJ5AEO.js").then((m) => m.pull(args.slice(1)));
221
+ await import("./resources-BFT7V6UR.js").then((m) => m.pull(args.slice(1)));
220
222
  break;
221
223
  case "destroy":
222
- await import("./resources-TCYJ5AEO.js").then((m) => m.destroy(args.slice(1)));
224
+ await import("./resources-BFT7V6UR.js").then((m) => m.destroy(args.slice(1)));
223
225
  break;
224
226
  case "list":
225
- await import("./resources-TCYJ5AEO.js").then((m) => m.list(args.slice(1)));
227
+ await import("./resources-BFT7V6UR.js").then((m) => m.list(args.slice(1)));
226
228
  break;
227
229
  case "show":
228
- await import("./resources-TCYJ5AEO.js").then((m) => m.show(args.slice(1)));
230
+ await import("./resources-BFT7V6UR.js").then((m) => m.show(args.slice(1)));
231
+ break;
232
+ case "diff":
233
+ await import("./resources-BFT7V6UR.js").then((m) => m.diff(args.slice(1)));
229
234
  break;
230
235
  // Development
231
236
  case "dev":
232
- await import("./dev-LBWA7G6T.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":
@@ -4,7 +4,7 @@ import {
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:")}
@@ -76,12 +125,14 @@ ${pc.dim("Resources:")}
76
125
  app Deploy the frontend app
77
126
 
78
127
  ${pc.dim("Options:")}
128
+ --yes, -y Skip confirmation prompt (for CI/CD)
79
129
  --skip-build Skip build step when applying app
80
130
 
81
131
  ${pc.dim("Examples:")}
82
- lumera apply # Apply everything
132
+ lumera apply # Apply everything (shows plan, asks to confirm)
83
133
  lumera apply collections # Apply all collections
84
134
  lumera apply collections/users # Apply single collection
135
+ lumera apply agents -y # Apply agents without confirmation
85
136
  lumera apply app # Deploy frontend
86
137
  lumera apply app --skip-build # Deploy without rebuilding
87
138
  `);
@@ -89,10 +140,15 @@ ${pc.dim("Examples:")}
89
140
  function showPullHelp() {
90
141
  console.log(`
91
142
  ${pc.dim("Usage:")}
92
- lumera pull [resource]
143
+ lumera pull [resource] [--force]
93
144
 
94
145
  ${pc.dim("Description:")}
95
146
  Download remote state to local files.
147
+ Refuses to overwrite local files that have uncommitted changes
148
+ (use --force to override, or 'lumera diff' to inspect first).
149
+
150
+ ${pc.dim("Options:")}
151
+ --force, -f Overwrite local files even if they have changes
96
152
 
97
153
  ${pc.dim("Resources:")}
98
154
  (none) Pull all resources
@@ -105,9 +161,10 @@ ${pc.dim("Resources:")}
105
161
  agents/<name> Pull single agent
106
162
 
107
163
  ${pc.dim("Examples:")}
108
- lumera pull # Pull all resources
109
- lumera pull collections # Pull only collections
110
- lumera pull automations/sync # Pull single automation
164
+ lumera pull # Pull all (safe \u2014 warns on conflicts)
165
+ lumera pull agents # Pull only agents
166
+ lumera pull --force # Pull all, overwrite local changes
167
+ lumera diff agents/my_agent # Inspect before pulling
111
168
  `);
112
169
  }
113
170
  function showDestroyHelp() {
@@ -427,6 +484,30 @@ function mapFieldType(type) {
427
484
  };
428
485
  return typeMap[type] || type;
429
486
  }
487
+ function fieldsDiffer(local, remote) {
488
+ if (mapFieldType(local.type) !== remote.type) return true;
489
+ if ((local.required || false) !== (remote.required || false)) return true;
490
+ const opts = remote.options || {};
491
+ if (local.type === "select") {
492
+ const localValues = [...local.values || []].sort();
493
+ const remoteValues = [...opts.values || []].sort();
494
+ if (localValues.join(",") !== remoteValues.join(",")) return true;
495
+ const localMultiple = local.multiple || false;
496
+ const remoteMaxSelect = opts.maxSelect || 1;
497
+ if (localMultiple && remoteMaxSelect <= 1) return true;
498
+ if (!localMultiple && remoteMaxSelect > 1) return true;
499
+ }
500
+ if (local.type === "relation") {
501
+ if (local.collection && local.collection !== opts.collectionId) return true;
502
+ const localMultiple = local.multiple || false;
503
+ const remoteMaxSelect = opts.maxSelect || 1;
504
+ if (localMultiple && remoteMaxSelect <= 1) return true;
505
+ if (!localMultiple && remoteMaxSelect > 1) return true;
506
+ }
507
+ if (local.min !== void 0 && local.min !== opts.min) return true;
508
+ if (local.max !== void 0 && local.max !== opts.max) return true;
509
+ return false;
510
+ }
430
511
  async function planCollections(api, localCollections) {
431
512
  const changes = [];
432
513
  const remoteCollections = await api.listCollections();
@@ -454,10 +535,20 @@ async function planCollections(api, localCollections) {
454
535
  const remoteFieldMap = new Map(remote.schema.map((f) => [f.name, f]));
455
536
  const added = [...localFieldNames].filter((n) => !remoteFieldNames.has(n));
456
537
  const removed = [...remoteFieldNames].filter((n) => !localFieldNames.has(n));
457
- if (added.length > 0 || removed.length > 0) {
538
+ const modified = [];
539
+ for (const name of localFieldNames) {
540
+ if (!remoteFieldNames.has(name)) continue;
541
+ const localField = localFieldMap.get(name);
542
+ const remoteField = remoteFieldMap.get(name);
543
+ if (fieldsDiffer(localField, remoteField)) {
544
+ modified.push(name);
545
+ }
546
+ }
547
+ if (added.length > 0 || removed.length > 0 || modified.length > 0) {
458
548
  const details = [];
459
549
  if (added.length > 0) details.push(`+${added.length} field${added.length > 1 ? "s" : ""}`);
460
550
  if (removed.length > 0) details.push(`-${removed.length} field${removed.length > 1 ? "s" : ""}`);
551
+ if (modified.length > 0) details.push(`~${modified.length} field${modified.length > 1 ? "s" : ""} (${modified.join(", ")})`);
461
552
  const fieldDetails = [];
462
553
  for (const name of added) {
463
554
  const f = localFieldMap.get(name);
@@ -502,12 +593,17 @@ async function planAutomations(api, localAutomations) {
502
593
  if (codeChanged) details.push("code");
503
594
  if (nameChanged) details.push("name");
504
595
  if (descChanged) details.push("description");
596
+ const textDiffs = [];
597
+ if (codeChanged) {
598
+ textDiffs.push({ field: "main.py", oldText: remote.code || "", newText: code });
599
+ }
505
600
  changes.push({
506
601
  type: "update",
507
602
  resource: "automation",
508
603
  id: automation.external_id,
509
604
  name: automation.name,
510
- details: `changed: ${details.join(", ")}`
605
+ details: `changed: ${details.join(", ")}`,
606
+ textDiffs
511
607
  });
512
608
  }
513
609
  }
@@ -539,12 +635,17 @@ async function planHooks(api, localHooks, collections) {
539
635
  const details = [];
540
636
  if (scriptChanged) details.push("script");
541
637
  if (eventChanged) details.push("trigger");
638
+ const textDiffs = [];
639
+ if (scriptChanged) {
640
+ textDiffs.push({ field: fileName, oldText: remote.script || "", newText: script });
641
+ }
542
642
  changes.push({
543
643
  type: "update",
544
644
  resource: "hook",
545
645
  id: hook.external_id,
546
646
  name: `${hook.collection}.${hook.trigger}`,
547
- details: `changed: ${details.join(", ")}`
647
+ details: `changed: ${details.join(", ")}`,
648
+ textDiffs
548
649
  });
549
650
  }
550
651
  }
@@ -854,6 +955,7 @@ function loadLocalAgents(platformDir, filterName, appName) {
854
955
  const agentDir = join(agentsDir, entry.name);
855
956
  const configPath = join(agentDir, "config.json");
856
957
  const promptPath = join(agentDir, "system_prompt.md");
958
+ const policyPath = join(agentDir, "policy.js");
857
959
  if (!existsSync(configPath)) {
858
960
  errors.push(`${entry.name}: missing config.json`);
859
961
  continue;
@@ -880,7 +982,8 @@ function loadLocalAgents(platformDir, filterName, appName) {
880
982
  continue;
881
983
  }
882
984
  const systemPrompt = readFileSync(promptPath, "utf-8");
883
- agents.push({ agent: config, systemPrompt });
985
+ const policyScript = existsSync(policyPath) ? readFileSync(policyPath, "utf-8") : "";
986
+ agents.push({ agent: config, systemPrompt, policyScript });
884
987
  } catch (e) {
885
988
  errors.push(`${entry.name}: failed to parse config.json - ${e}`);
886
989
  }
@@ -900,7 +1003,18 @@ async function planAgents(api, localAgents) {
900
1003
  const remoteByExternalId = new Map(
901
1004
  remoteAgents.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a])
902
1005
  );
903
- for (const { agent, systemPrompt } of localAgents) {
1006
+ let skillSlugToId = /* @__PURE__ */ new Map();
1007
+ let skillIdToSlug = /* @__PURE__ */ new Map();
1008
+ const hasSkillRefs = localAgents.some((a) => a.agent.skills && a.agent.skills.length > 0) || remoteAgents.some((a) => a.skill_ids && a.skill_ids.length > 0);
1009
+ if (hasSkillRefs) {
1010
+ try {
1011
+ const skills = await api.listAgentSkills();
1012
+ skillSlugToId = new Map(skills.map((s) => [s.slug, s.id]));
1013
+ skillIdToSlug = new Map(skills.map((s) => [s.id, s.slug]));
1014
+ } catch {
1015
+ }
1016
+ }
1017
+ for (const { agent, systemPrompt, policyScript } of localAgents) {
904
1018
  const remote = remoteByExternalId.get(agent.external_id);
905
1019
  if (!remote) {
906
1020
  changes.push({ type: "create", resource: "agent", id: agent.external_id, name: agent.name });
@@ -910,8 +1024,27 @@ async function planAgents(api, localAgents) {
910
1024
  if ((remote.description || "") !== (agent.description || "")) diffs.push("description");
911
1025
  if ((remote.system_prompt || "").trim() !== systemPrompt.trim()) diffs.push("system_prompt");
912
1026
  if ((remote.model || "") !== (agent.model || "")) diffs.push("model");
1027
+ if ((remote.policy_script || "").trim() !== (policyScript || "").trim()) diffs.push("policy_script");
1028
+ if ((remote.policy_enabled || false) !== (agent.policy_enabled || false)) diffs.push("policy_enabled");
1029
+ const localSkillIds = (agent.skills || []).map((s) => skillSlugToId.get(s) || s).sort();
1030
+ const remoteSkillIds = [...remote.skill_ids || []].sort();
1031
+ if (localSkillIds.join(",") !== remoteSkillIds.join(",")) {
1032
+ const addedSlugs = localSkillIds.filter((id) => !remoteSkillIds.includes(id)).map((id) => skillIdToSlug.get(id) || id);
1033
+ const removedSlugs = remoteSkillIds.filter((id) => !localSkillIds.includes(id)).map((id) => skillIdToSlug.get(id) || id);
1034
+ const parts = [];
1035
+ if (addedSlugs.length) parts.push(`+${addedSlugs.join(", +")}`);
1036
+ if (removedSlugs.length) parts.push(`-${removedSlugs.join(", -")}`);
1037
+ diffs.push(`skills (${parts.join(", ")})`);
1038
+ }
913
1039
  if (diffs.length > 0) {
914
- changes.push({ type: "update", resource: "agent", id: agent.external_id, name: agent.name, details: `changed: ${diffs.join(", ")}` });
1040
+ const textDiffs = [];
1041
+ if (diffs.includes("system_prompt")) {
1042
+ textDiffs.push({ field: "system_prompt.md", oldText: remote.system_prompt || "", newText: systemPrompt });
1043
+ }
1044
+ if (diffs.includes("policy_script")) {
1045
+ textDiffs.push({ field: "policy.js", oldText: remote.policy_script || "", newText: policyScript });
1046
+ }
1047
+ changes.push({ type: "update", resource: "agent", id: agent.external_id, name: agent.name, details: `changed: ${diffs.join(", ")}`, textDiffs });
915
1048
  }
916
1049
  }
917
1050
  }
@@ -933,7 +1066,7 @@ async function applyAgents(api, localAgents) {
933
1066
  console.log(pc.yellow(` \u26A0 Could not fetch skills for resolution: ${e}`));
934
1067
  }
935
1068
  }
936
- for (const { agent, systemPrompt } of localAgents) {
1069
+ for (const { agent, systemPrompt, policyScript } of localAgents) {
937
1070
  const remote = remoteByExternalId.get(agent.external_id);
938
1071
  const skillIds = [];
939
1072
  if (agent.skills) {
@@ -952,7 +1085,9 @@ async function applyAgents(api, localAgents) {
952
1085
  description: agent.description || "",
953
1086
  system_prompt: systemPrompt,
954
1087
  model: agent.model || "",
955
- skill_ids: skillIds
1088
+ skill_ids: skillIds,
1089
+ policy_script: policyScript || "",
1090
+ policy_enabled: agent.policy_enabled || false
956
1091
  };
957
1092
  try {
958
1093
  if (remote) {
@@ -999,9 +1134,14 @@ async function pullAgents(api, platformDir, filterName) {
999
1134
  name: agent.name
1000
1135
  };
1001
1136
  if (agent.description) config.description = agent.description;
1137
+ if (agent.model) config.model = agent.model;
1002
1138
  if (skillSlugs.length > 0) config.skills = skillSlugs;
1139
+ if (agent.policy_enabled) config.policy_enabled = true;
1003
1140
  writeFileSync(join(agentDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
1004
1141
  writeFileSync(join(agentDir, "system_prompt.md"), agent.system_prompt || "");
1142
+ if (agent.policy_script) {
1143
+ writeFileSync(join(agentDir, "policy.js"), agent.policy_script);
1144
+ }
1005
1145
  console.log(pc.green(" \u2713"), `${agent.name} \u2192 agents/${dirName}/`);
1006
1146
  }
1007
1147
  }
@@ -1621,6 +1761,26 @@ async function plan(args) {
1621
1761
  }
1622
1762
  console.log();
1623
1763
  }
1764
+ if (change.textDiffs && change.textDiffs.length > 0) {
1765
+ const maxDiffLines = 20;
1766
+ for (const td of change.textDiffs) {
1767
+ const diffLines = computeLineDiff(td.oldText, td.newText);
1768
+ if (diffLines.length > 0) {
1769
+ console.log(pc.dim(` --- ${td.field}`));
1770
+ const shown = diffLines.slice(0, maxDiffLines);
1771
+ for (const dl of shown) {
1772
+ if (dl.type === "+") console.log(pc.green(` + ${dl.line}`));
1773
+ else if (dl.type === "-") console.log(pc.red(` - ${dl.line}`));
1774
+ else console.log(pc.dim(` ${dl.line}`));
1775
+ }
1776
+ if (diffLines.length > maxDiffLines) {
1777
+ const remaining = diffLines.length - maxDiffLines;
1778
+ console.log(pc.dim(` ... ${remaining} more lines \u2014 use ${pc.cyan(`lumera diff ${change.resource}s/${change.name}`)} for full diff`));
1779
+ }
1780
+ console.log();
1781
+ }
1782
+ }
1783
+ }
1624
1784
  }
1625
1785
  console.log();
1626
1786
  console.log(pc.dim(` Run 'lumera apply' to apply these changes.`));
@@ -1636,11 +1796,12 @@ async function apply(args) {
1636
1796
  const platformDir = getPlatformDir();
1637
1797
  const api = createApiClient();
1638
1798
  const appName = getAppName(projectRoot);
1639
- const { type, name } = parseResource(args[0]);
1640
- console.log();
1641
- console.log(pc.cyan(pc.bold(" Apply")));
1642
- console.log();
1799
+ const { type, name } = parseResource(args.filter((a) => a !== "--yes" && a !== "-y")[0]);
1800
+ const autoConfirm = args.includes("--yes") || args.includes("-y") || !!process.env.CI;
1643
1801
  if (type === "app") {
1802
+ console.log();
1803
+ console.log(pc.cyan(pc.bold(" Apply")));
1804
+ console.log();
1644
1805
  console.log(pc.bold(" App:"));
1645
1806
  await applyApp(args);
1646
1807
  console.log();
@@ -1649,77 +1810,131 @@ async function apply(args) {
1649
1810
  return;
1650
1811
  }
1651
1812
  let collections;
1652
- let totalErrors = 0;
1653
- if (!type || type === "collections") {
1654
- const localCollections = loadLocalCollections(platformDir, name || void 0);
1655
- if (localCollections.length > 0) {
1656
- console.log(pc.bold(" Collections:"));
1657
- totalErrors += await applyCollections(api, localCollections);
1658
- console.log();
1659
- } else if (name) {
1660
- console.log(pc.red(` Collection "${name}" not found locally`));
1661
- process.exit(1);
1662
- }
1663
- }
1664
1813
  try {
1665
1814
  const remoteCollections = await api.listCollections();
1666
1815
  collections = new Map(remoteCollections.map((c) => [c.name, c.id]));
1667
- for (const c of remoteCollections) {
1668
- collections.set(c.id, c.id);
1669
- }
1816
+ for (const c of remoteCollections) collections.set(c.id, c.id);
1670
1817
  } catch {
1671
1818
  collections = /* @__PURE__ */ new Map();
1672
1819
  }
1673
- if (!type || type === "automations") {
1674
- const localAutomations = loadLocalAutomations(platformDir, name || void 0, appName);
1675
- if (localAutomations.length > 0) {
1676
- console.log(pc.bold(" Automations:"));
1677
- totalErrors += await applyAutomations(api, localAutomations);
1678
- console.log();
1679
- } else if (name) {
1680
- console.log(pc.red(` Automation "${name}" not found locally`));
1681
- process.exit(1);
1682
- }
1683
- }
1684
- if (!type || type === "hooks") {
1685
- const localHooks = loadLocalHooks(platformDir, name || void 0, appName);
1686
- if (localHooks.length > 0) {
1687
- console.log(pc.bold(" Hooks:"));
1688
- totalErrors += await applyHooks(api, localHooks, collections);
1689
- console.log();
1690
- } else if (name) {
1691
- console.log(pc.red(` Hook "${name}" not found locally`));
1692
- process.exit(1);
1693
- }
1694
- }
1695
- if (!type || type === "agents") {
1696
- const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
1697
- if (localAgents.length > 0) {
1698
- console.log(pc.bold(" Agents:"));
1699
- totalErrors += await applyAgents(api, localAgents);
1820
+ const allChanges = [];
1821
+ const localCollections = !type || type === "collections" ? loadLocalCollections(platformDir, name || void 0) : [];
1822
+ const localAutomations = !type || type === "automations" ? loadLocalAutomations(platformDir, name || void 0, appName) : [];
1823
+ const localHooks = !type || type === "hooks" ? loadLocalHooks(platformDir, name || void 0, appName) : [];
1824
+ const localAgents = !type || type === "agents" ? loadLocalAgents(platformDir, name || void 0, appName) : [];
1825
+ if (localCollections.length > 0) allChanges.push(...await planCollections(api, localCollections));
1826
+ if (localAutomations.length > 0) allChanges.push(...await planAutomations(api, localAutomations));
1827
+ if (localHooks.length > 0) allChanges.push(...await planHooks(api, localHooks, collections));
1828
+ if (localAgents.length > 0) allChanges.push(...await planAgents(api, localAgents));
1829
+ if (name) {
1830
+ const hasLocal = localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localAgents.length > 0;
1831
+ if (!hasLocal) {
1700
1832
  console.log();
1701
- } else if (name) {
1702
- console.log(pc.red(` Agent "${name}" not found locally`));
1833
+ console.log(pc.red(` Resource "${name}" not found locally`));
1703
1834
  process.exit(1);
1704
1835
  }
1705
1836
  }
1837
+ let willDeployApp = false;
1706
1838
  if (!type) {
1707
1839
  try {
1708
- const projectRoot2 = findProjectRoot();
1709
- if (existsSync(join(projectRoot2, "dist")) || existsSync(join(projectRoot2, "src"))) {
1710
- console.log(pc.bold(" App:"));
1711
- await applyApp(args);
1712
- console.log();
1840
+ if (existsSync(join(projectRoot, "dist")) || existsSync(join(projectRoot, "src"))) {
1841
+ willDeployApp = true;
1713
1842
  }
1714
1843
  } catch {
1715
1844
  }
1716
1845
  }
1846
+ if (allChanges.length === 0 && !willDeployApp) {
1847
+ console.log();
1848
+ console.log(pc.green(" \u2713 Nothing to apply \u2014 local and remote are in sync."));
1849
+ console.log();
1850
+ return;
1851
+ }
1852
+ console.log();
1853
+ console.log(pc.cyan(pc.bold(" Apply")));
1854
+ console.log();
1855
+ const creates = allChanges.filter((c) => c.type === "create");
1856
+ const updates = allChanges.filter((c) => c.type === "update");
1857
+ if (allChanges.length > 0) {
1858
+ console.log(pc.bold(" Plan:"));
1859
+ for (const change of allChanges) {
1860
+ const icon = change.type === "create" ? "+" : "~";
1861
+ const color = change.type === "create" ? pc.green : pc.yellow;
1862
+ const details = change.details ? ` (${change.details})` : "";
1863
+ console.log(` ${color(icon)} ${change.resource}: ${change.name}${pc.dim(details)}`);
1864
+ }
1865
+ if (willDeployApp) console.log(` ${pc.blue("\u25CF")} app: frontend build + deploy`);
1866
+ console.log();
1867
+ const parts = [];
1868
+ if (creates.length > 0) parts.push(pc.green(`${creates.length} create`));
1869
+ if (updates.length > 0) parts.push(pc.yellow(`${updates.length} update`));
1870
+ if (willDeployApp) parts.push(pc.blue("1 app deploy"));
1871
+ console.log(` ${parts.join(", ")}`);
1872
+ console.log();
1873
+ } else if (willDeployApp) {
1874
+ console.log(pc.dim(" No infrastructure changes \u2014 deploying app only."));
1875
+ console.log();
1876
+ }
1877
+ if (!autoConfirm && allChanges.length > 0) {
1878
+ const { confirm } = await prompts({
1879
+ type: "confirm",
1880
+ name: "confirm",
1881
+ message: " Proceed with apply?",
1882
+ initial: true
1883
+ });
1884
+ if (!confirm) {
1885
+ console.log(pc.dim(" Cancelled."));
1886
+ console.log();
1887
+ return;
1888
+ }
1889
+ console.log();
1890
+ }
1891
+ let totalErrors = 0;
1892
+ let totalCreated = 0;
1893
+ let totalUpdated = 0;
1894
+ let totalSkipped = 0;
1895
+ if (localCollections.length > 0) {
1896
+ console.log(pc.bold(" Collections:"));
1897
+ totalErrors += await applyCollections(api, localCollections);
1898
+ console.log();
1899
+ }
1900
+ try {
1901
+ const remoteCollections = await api.listCollections();
1902
+ collections = new Map(remoteCollections.map((c) => [c.name, c.id]));
1903
+ for (const c of remoteCollections) collections.set(c.id, c.id);
1904
+ } catch {
1905
+ }
1906
+ if (localAutomations.length > 0) {
1907
+ console.log(pc.bold(" Automations:"));
1908
+ totalErrors += await applyAutomations(api, localAutomations);
1909
+ console.log();
1910
+ }
1911
+ if (localHooks.length > 0) {
1912
+ console.log(pc.bold(" Hooks:"));
1913
+ totalErrors += await applyHooks(api, localHooks, collections);
1914
+ console.log();
1915
+ }
1916
+ if (localAgents.length > 0) {
1917
+ console.log(pc.bold(" Agents:"));
1918
+ totalErrors += await applyAgents(api, localAgents);
1919
+ console.log();
1920
+ }
1921
+ if (willDeployApp) {
1922
+ console.log(pc.bold(" App:"));
1923
+ await applyApp(args);
1924
+ console.log();
1925
+ }
1926
+ totalCreated = creates.length;
1927
+ totalUpdated = updates.length;
1717
1928
  if (totalErrors > 0) {
1718
- console.log(pc.red(` Failed with ${totalErrors} error${totalErrors > 1 ? "s" : ""}.`));
1929
+ console.log(pc.red(` \u2717 ${totalErrors} error${totalErrors > 1 ? "s" : ""} during apply.`));
1719
1930
  console.log();
1720
1931
  process.exit(1);
1721
1932
  }
1722
- console.log(pc.green(" Done!"));
1933
+ const summary = [];
1934
+ if (totalCreated > 0) summary.push(pc.green(`${totalCreated} created`));
1935
+ if (totalUpdated > 0) summary.push(pc.yellow(`${totalUpdated} updated`));
1936
+ if (willDeployApp) summary.push(pc.blue("app deployed"));
1937
+ console.log(pc.green(" \u2713 Done!") + (summary.length > 0 ? ` ${pc.dim("\u2014")} ${summary.join(", ")}` : ""));
1723
1938
  console.log();
1724
1939
  }
1725
1940
  async function pull(args) {
@@ -1727,10 +1942,63 @@ async function pull(args) {
1727
1942
  showPullHelp();
1728
1943
  return;
1729
1944
  }
1730
- loadEnv();
1945
+ const force = args.includes("--force") || args.includes("-f");
1946
+ const filteredArgs = args.filter((a) => a !== "--force" && a !== "-f");
1947
+ const projectRoot = findProjectRoot();
1948
+ loadEnv(projectRoot);
1731
1949
  const platformDir = getPlatformDir();
1732
1950
  const api = createApiClient();
1733
- const { type, name } = parseResource(args[0]);
1951
+ const appName = getAppName(projectRoot);
1952
+ const { type, name } = parseResource(filteredArgs[0]);
1953
+ if (!force) {
1954
+ const conflicts = [];
1955
+ let collections;
1956
+ try {
1957
+ const remoteCollections = await api.listCollections();
1958
+ collections = new Map(remoteCollections.map((c) => [c.name, c.id]));
1959
+ } catch {
1960
+ collections = /* @__PURE__ */ new Map();
1961
+ }
1962
+ if (!type || type === "agents") {
1963
+ const localAgents = loadLocalAgents(platformDir, name || void 0, appName);
1964
+ if (localAgents.length > 0) {
1965
+ const changes = await planAgents(api, localAgents);
1966
+ for (const c of changes) {
1967
+ if (c.type === "update") conflicts.push(`agents/${c.name}`);
1968
+ }
1969
+ }
1970
+ }
1971
+ if (!type || type === "automations") {
1972
+ const localAutomations = loadLocalAutomations(platformDir, name || void 0, appName);
1973
+ if (localAutomations.length > 0) {
1974
+ const changes = await planAutomations(api, localAutomations);
1975
+ for (const c of changes) {
1976
+ if (c.type === "update") conflicts.push(`automations/${c.name}`);
1977
+ }
1978
+ }
1979
+ }
1980
+ if (!type || type === "hooks") {
1981
+ const localHooks = loadLocalHooks(platformDir, name || void 0, appName);
1982
+ if (localHooks.length > 0) {
1983
+ const changes = await planHooks(api, localHooks, collections);
1984
+ for (const c of changes) {
1985
+ if (c.type === "update") conflicts.push(`hooks/${c.name}`);
1986
+ }
1987
+ }
1988
+ }
1989
+ if (conflicts.length > 0) {
1990
+ console.log();
1991
+ console.log(pc.yellow(` \u26A0 ${conflicts.length} local file${conflicts.length > 1 ? "s have" : " has"} changes that would be lost:`));
1992
+ for (const f of conflicts) {
1993
+ console.log(pc.dim(` ${f}`));
1994
+ }
1995
+ console.log();
1996
+ console.log(pc.dim(` Use ${pc.cyan("lumera diff <resource>")} to inspect changes.`));
1997
+ console.log(pc.dim(` Use ${pc.cyan("lumera pull --force")} to overwrite local files.`));
1998
+ console.log();
1999
+ process.exit(1);
2000
+ }
2001
+ }
1734
2002
  console.log();
1735
2003
  console.log(pc.cyan(pc.bold(" Pull")));
1736
2004
  console.log(pc.dim(` Downloading remote state to ${platformDir}/...`));
@@ -1887,9 +2155,164 @@ async function show(args) {
1887
2155
  await showResource(api, platformDir, type, name, appName);
1888
2156
  }
1889
2157
  }
2158
+ function showDiffHelp() {
2159
+ console.log(`
2160
+ ${pc.bold("lumera diff")} - Show full diff between local and remote state
2161
+
2162
+ ${pc.dim("Usage:")}
2163
+ lumera diff <resource>
2164
+
2165
+ ${pc.dim("Resources:")}
2166
+ agents/<name> Diff agent (system_prompt, policy_script)
2167
+ automations/<name> Diff automation code
2168
+ hooks/<name> Diff hook script
2169
+
2170
+ ${pc.dim("Examples:")}
2171
+ lumera diff agents/bank_activity_matcher
2172
+ lumera diff automations/sync
2173
+ lumera diff hooks/encoding_protect_create
2174
+ `);
2175
+ }
2176
+ function renderFullDiff(field, oldText, newText) {
2177
+ const diffLines = computeLineDiff(oldText, newText);
2178
+ if (diffLines.length === 0) {
2179
+ console.log(pc.dim(` ${field}: no changes`));
2180
+ return;
2181
+ }
2182
+ console.log(pc.bold(` --- ${field}`));
2183
+ for (const dl of diffLines) {
2184
+ if (dl.type === "+") console.log(pc.green(` + ${dl.line}`));
2185
+ else if (dl.type === "-") console.log(pc.red(` - ${dl.line}`));
2186
+ else console.log(pc.dim(` ${dl.line}`));
2187
+ }
2188
+ console.log();
2189
+ }
2190
+ async function diff(args) {
2191
+ if (args.includes("--help") || args.includes("-h") || args.length === 0) {
2192
+ showDiffHelp();
2193
+ if (args.length === 0) process.exit(1);
2194
+ return;
2195
+ }
2196
+ const projectRoot = findProjectRoot();
2197
+ loadEnv(projectRoot);
2198
+ const platformDir = getPlatformDir();
2199
+ const api = createApiClient();
2200
+ const appName = getAppName(projectRoot);
2201
+ const { type, name } = parseResource(args[0]);
2202
+ if (!type || !name) {
2203
+ console.log(pc.red(` Invalid resource path: ${args[0]}`));
2204
+ console.log(pc.dim(" Use format: <type>/<name> (e.g., agents/bank_activity_matcher)"));
2205
+ process.exit(1);
2206
+ }
2207
+ console.log();
2208
+ if (type === "agents") {
2209
+ const localAgents = loadLocalAgents(platformDir, name, appName);
2210
+ const remoteAgents = await api.listAgents();
2211
+ const local = localAgents[0];
2212
+ const localExtId = local?.agent.external_id;
2213
+ const remote = remoteAgents.find(
2214
+ (a) => a.external_id === name || a.name === name || localExtId && a.external_id === localExtId
2215
+ );
2216
+ if (!local && !remote) {
2217
+ console.log(pc.red(` Agent "${name}" not found locally or remotely`));
2218
+ process.exit(1);
2219
+ }
2220
+ if (!local) {
2221
+ console.log(pc.cyan(` Agent "${name}" exists only remotely (not in local files)`));
2222
+ process.exit(0);
2223
+ }
2224
+ if (!remote) {
2225
+ console.log(pc.yellow(` Agent "${name}" exists only locally (not yet deployed)`));
2226
+ process.exit(0);
2227
+ }
2228
+ console.log(pc.bold(` Agent: ${local.agent.name}`));
2229
+ console.log();
2230
+ if (remote.name !== local.agent.name)
2231
+ console.log(` name: ${pc.red(remote.name)} \u2192 ${pc.green(local.agent.name)}`);
2232
+ if ((remote.description || "") !== (local.agent.description || ""))
2233
+ console.log(` description: ${pc.red(remote.description || "(empty)")} \u2192 ${pc.green(local.agent.description || "(empty)")}`);
2234
+ if ((remote.model || "") !== (local.agent.model || ""))
2235
+ console.log(` model: ${pc.red(remote.model || "(default)")} \u2192 ${pc.green(local.agent.model || "(default)")}`);
2236
+ if ((remote.policy_enabled || false) !== (local.agent.policy_enabled || false))
2237
+ console.log(` policy_enabled: ${pc.red(String(remote.policy_enabled || false))} \u2192 ${pc.green(String(local.agent.policy_enabled || false))}`);
2238
+ const promptChanged = (remote.system_prompt || "").trim() !== local.systemPrompt.trim();
2239
+ const policyChanged = (remote.policy_script || "").trim() !== (local.policyScript || "").trim();
2240
+ 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)) {
2241
+ console.log(pc.green(` \u2713 No changes`));
2242
+ } else {
2243
+ if (promptChanged) renderFullDiff("system_prompt.md", remote.system_prompt || "", local.systemPrompt);
2244
+ if (policyChanged) renderFullDiff("policy.js", remote.policy_script || "", local.policyScript);
2245
+ }
2246
+ } else if (type === "automations") {
2247
+ const localAutomations = loadLocalAutomations(platformDir, name, appName);
2248
+ const remoteAutomations = await api.listAutomations({ include_code: true });
2249
+ const local = localAutomations[0];
2250
+ const localExtId = local?.automation.external_id;
2251
+ const remote = remoteAutomations.find(
2252
+ (a) => a.external_id === name || a.name === name || localExtId && a.external_id === localExtId
2253
+ );
2254
+ if (!local && !remote) {
2255
+ console.log(pc.red(` Automation "${name}" not found locally or remotely`));
2256
+ process.exit(1);
2257
+ }
2258
+ if (!local) {
2259
+ console.log(pc.cyan(` Automation "${name}" exists only remotely`));
2260
+ process.exit(0);
2261
+ }
2262
+ if (!remote) {
2263
+ console.log(pc.yellow(` Automation "${name}" exists only locally (not yet deployed)`));
2264
+ process.exit(0);
2265
+ }
2266
+ console.log(pc.bold(` Automation: ${local.automation.name}`));
2267
+ console.log();
2268
+ if (remote.name !== local.automation.name)
2269
+ console.log(` name: ${pc.red(remote.name)} \u2192 ${pc.green(local.automation.name)}`);
2270
+ const codeChanged = (remote.code || "") !== local.code;
2271
+ if (!codeChanged && remote.name === local.automation.name) {
2272
+ console.log(pc.green(` \u2713 No changes`));
2273
+ } else if (codeChanged) {
2274
+ renderFullDiff("main.py", remote.code || "", local.code);
2275
+ }
2276
+ } else if (type === "hooks") {
2277
+ const localHooks = loadLocalHooks(platformDir, name, appName);
2278
+ const remoteHooks = await api.listHooks();
2279
+ const local = localHooks[0];
2280
+ const localExtId = local?.hook.external_id;
2281
+ const remote = remoteHooks.find(
2282
+ (h) => h.external_id === name || h.name === name || localExtId && h.external_id === localExtId
2283
+ );
2284
+ if (!local && !remote) {
2285
+ console.log(pc.red(` Hook "${name}" not found locally or remotely`));
2286
+ process.exit(1);
2287
+ }
2288
+ if (!local) {
2289
+ console.log(pc.cyan(` Hook "${name}" exists only remotely`));
2290
+ process.exit(0);
2291
+ }
2292
+ if (!remote) {
2293
+ console.log(pc.yellow(` Hook "${name}" exists only locally (not yet deployed)`));
2294
+ process.exit(0);
2295
+ }
2296
+ console.log(pc.bold(` Hook: ${local.hook.external_id}`));
2297
+ console.log();
2298
+ if (remote.event !== local.hook.trigger)
2299
+ console.log(` trigger: ${pc.red(remote.event)} \u2192 ${pc.green(local.hook.trigger)}`);
2300
+ const scriptChanged = (remote.script || "").trim() !== local.script.trim();
2301
+ if (!scriptChanged && remote.event === local.hook.trigger) {
2302
+ console.log(pc.green(` \u2713 No changes`));
2303
+ } else if (scriptChanged) {
2304
+ renderFullDiff(local.fileName, remote.script || "", local.script);
2305
+ }
2306
+ } else {
2307
+ console.log(pc.red(` Diff not supported for "${type}" \u2014 use agents, automations, or hooks`));
2308
+ process.exit(1);
2309
+ }
2310
+ console.log();
2311
+ }
1890
2312
  export {
1891
2313
  apply,
1892
2314
  destroy,
2315
+ diff,
1893
2316
  list,
1894
2317
  plan,
1895
2318
  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.2",
3
+ "version": "0.15.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {