@lumerahq/cli 0.34.5 → 0.35.2

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.
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  deploy,
3
3
  deployAppAPI
4
- } from "./chunk-P6GDSHYC.js";
4
+ } from "./chunk-5TQ3BQ2X.js";
5
5
  import "./chunk-UGAIWMQV.js";
6
6
  import "./chunk-2EHJH2QU.js";
7
7
  import "./chunk-XZ5VCTGY.js";
8
8
  import "./chunk-5FGDEZSJ.js";
9
9
  import {
10
10
  syncDeps
11
- } from "./chunk-5JNNUIVJ.js";
11
+ } from "./chunk-A27HZEU6.js";
12
12
  import {
13
13
  loadEnv
14
14
  } from "./chunk-2CR762KB.js";
@@ -17,7 +17,7 @@ import {
17
17
  createApiClient,
18
18
  isApiErrorStatus,
19
19
  validateCollectionNameLength
20
- } from "./chunk-W57NUJFJ.js";
20
+ } from "./chunk-2A2DSLIE.js";
21
21
  import {
22
22
  findProjectRoot,
23
23
  getApiUrl,
@@ -34,15 +34,98 @@ import "./chunk-PNKVD2UK.js";
34
34
 
35
35
  // src/commands/resources.ts
36
36
  import pc2 from "picocolors";
37
+
38
+ // src/lib/views.ts
39
+ import { createHash } from "crypto";
40
+ import { existsSync, readdirSync, readFileSync } from "fs";
41
+ import { join } from "path";
42
+ function loadLocalViews(platformDir, filterName) {
43
+ const directory = join(platformDir, "views");
44
+ if (!existsSync(directory)) return [];
45
+ const result = [];
46
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
47
+ if (!entry.isDirectory() || entry.name === "shared" || filterName && entry.name !== filterName)
48
+ continue;
49
+ const configPath = join(directory, entry.name, "config.json");
50
+ const queryPath = join(directory, entry.name, "query.sql");
51
+ if (!existsSync(configPath) || !existsSync(queryPath))
52
+ throw new Error(`View ${entry.name} requires config.json and query.sql`);
53
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
54
+ if (config.schema_version !== 1 || config.name !== entry.name || !/^[a-z][a-z0-9_]{0,29}$/.test(config.name) || config.name.startsWith("lm_") || config.name.includes("__"))
55
+ throw new Error(
56
+ `Invalid view config for ${entry.name}; schema_version must be 1 and name must match the folder`
57
+ );
58
+ result.push({ config, query: readFileSync(queryPath, "utf8").trim() });
59
+ }
60
+ return result.sort((a, b) => a.config.name.localeCompare(b.config.name));
61
+ }
62
+ function viewDefinitionHash(view) {
63
+ const config = { schema_version: 1, name: view.config.name };
64
+ if (view.config.display_name) config.display_name = view.config.display_name;
65
+ if (view.config.description) config.description = view.config.description;
66
+ if (view.config.row_key) config.row_key = view.config.row_key;
67
+ const encoded = JSON.stringify(config).replace(
68
+ /[<>&\u2028\u2029]/g,
69
+ (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`
70
+ );
71
+ return createHash("sha256").update(encoded + "\n" + view.query.trim()).digest("hex");
72
+ }
73
+ async function planViews(api, local, projectId) {
74
+ const remote = (await api.listViews()).filter(
75
+ (view) => !projectId || view.project_id === projectId
76
+ );
77
+ return local.flatMap((view) => {
78
+ const current = remote.find(
79
+ (candidate) => candidate.name === view.config.name
80
+ );
81
+ if (current?.definition_hash === viewDefinitionHash(view)) return [];
82
+ return [
83
+ {
84
+ type: current ? "update" : "create",
85
+ resource: "view",
86
+ id: current?.id ?? "",
87
+ name: view.config.name,
88
+ details: "Git-owned, read only; publishing requires a human source-project admin",
89
+ textDiffs: [
90
+ { field: "SQL", oldText: current?.query ?? "", newText: view.query }
91
+ ]
92
+ }
93
+ ];
94
+ });
95
+ }
96
+ async function applyViews(api, local, projectKey, projectId) {
97
+ const remote = (await api.listViews()).filter(
98
+ (view) => !projectId || view.project_id === projectId
99
+ );
100
+ let errors = 0;
101
+ for (const view of local) {
102
+ const current = remote.find(
103
+ (candidate) => candidate.name === view.config.name
104
+ );
105
+ if (current?.definition_hash === viewDefinitionHash(view)) continue;
106
+ try {
107
+ await api.publishView(projectKey, view, current?.definition_hash ?? "");
108
+ console.log(` \u2713 Published view ${view.config.name}`);
109
+ } catch (error3) {
110
+ errors++;
111
+ console.error(
112
+ ` \u2717 ${view.config.name}: ${error3 instanceof Error ? error3.message : String(error3)}. A source-project admin can publish the definition through Resource shares.`
113
+ );
114
+ }
115
+ }
116
+ return errors;
117
+ }
118
+
119
+ // src/commands/resources.ts
37
120
  import prompts from "prompts";
38
121
  import { execFileSync, execSync } from "child_process";
39
- import { createHash } from "crypto";
40
- import { existsSync as existsSync2, lstatSync, mkdirSync, mkdtempSync, readdirSync as readdirSync2, readFileSync as readFileSync2, renameSync, rmSync, writeFileSync } from "fs";
41
- import { dirname as dirname2, join as join2, resolve } from "path";
122
+ import { createHash as createHash2 } from "crypto";
123
+ import { existsSync as existsSync3, lstatSync, mkdirSync, mkdtempSync, readdirSync as readdirSync3, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync } from "fs";
124
+ import { dirname as dirname2, join as join3, resolve } from "path";
42
125
 
43
126
  // src/lib/lint/index.ts
44
- import { existsSync, readFileSync, readdirSync } from "fs";
45
- import { basename, dirname, join } from "path";
127
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
128
+ import { basename, dirname, join as join2 } from "path";
46
129
 
47
130
  // src/lib/lint/rules/agent-deprecated-model.ts
48
131
  function configuredModel(config) {
@@ -299,7 +382,7 @@ var automationMainGuardRule = {
299
382
  if (entrypoint === null) return [];
300
383
  const guard = findTopLevelMainGuard(target.source);
301
384
  if (guard === null) return [];
302
- const warning2 = {
385
+ const warning3 = {
303
386
  ruleId: "automation-main-guard",
304
387
  target,
305
388
  severity: "warning",
@@ -307,7 +390,7 @@ var automationMainGuardRule = {
307
390
  snippet: guard.snippet,
308
391
  message: `Function automations must not contain a top-level __main__ guard. Lumera invokes "${entrypoint}" automatically; remove the guard.`
309
392
  };
310
- return [warning2];
393
+ return [warning3];
311
394
  }
312
395
  };
313
396
 
@@ -324,6 +407,14 @@ function error(target, message, snippet) {
324
407
  snippet
325
408
  };
326
409
  }
410
+ function warning(target, message) {
411
+ return {
412
+ ruleId: "collection-schema",
413
+ target,
414
+ severity: "warning",
415
+ message
416
+ };
417
+ }
327
418
  var collectionSchemaRule = {
328
419
  id: "collection-schema",
329
420
  description: "Validates local collection JSON files before planning or applying resources.",
@@ -383,6 +474,19 @@ var collectionSchemaRule = {
383
474
  if (typeof field.type !== "string" || field.type.trim() === "") {
384
475
  issues.push(error(target, `Field ${typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`} is missing required string field "type".`));
385
476
  }
477
+ if (field.type === "lumera_file" || field.type === "file") {
478
+ const label = typeof field.name === "string" ? `"${field.name}"` : `at index ${i}`;
479
+ const ignored = [
480
+ ...isRecord3(field.options) && field.options.maxSelect !== void 0 ? ['"options.maxSelect"'] : [],
481
+ ...field.multiple !== void 0 ? ['"multiple"'] : []
482
+ ];
483
+ if (ignored.length > 0) {
484
+ issues.push(warning(
485
+ target,
486
+ `File field ${label} sets ${ignored.join(" and ")}, which collection files ignore; it deploys with maxSelect ${field.maxSelect ?? 1}. Set top-level "maxSelect" instead. A file field's maxSelect cannot change after it is first deployed.`
487
+ ));
488
+ }
489
+ }
386
490
  }
387
491
  }
388
492
  if (audit !== void 0 && typeof audit !== "boolean") {
@@ -734,7 +838,7 @@ function error2(target, message, line) {
734
838
  ...line !== void 0 ? { line } : {}
735
839
  };
736
840
  }
737
- function warning(target, message, line) {
841
+ function warning2(target, message, line) {
738
842
  return {
739
843
  ruleId: "hook-verify",
740
844
  target,
@@ -814,7 +918,7 @@ var hookVerifyRule = {
814
918
  if (errors.length > 0) return errors;
815
919
  const advisories = [];
816
920
  if (finding.legacyHandlerPattern) {
817
- advisories.push(warning(target, LEGACY_HANDLER_MIGRATION_HINT));
921
+ advisories.push(warning2(target, LEGACY_HANDLER_MIGRATION_HINT));
818
922
  }
819
923
  let script = finding.scriptBody ?? "";
820
924
  if (ctx.appName) {
@@ -966,59 +1070,59 @@ function buildAutomationTargets(localAutomations) {
966
1070
  source: a.code,
967
1071
  config: a.automation.inputs?.schema,
968
1072
  // a.filePath is main.py; config.json is its sibling.
969
- configFilePath: join(dirname(a.filePath), "config.json")
1073
+ configFilePath: join2(dirname(a.filePath), "config.json")
970
1074
  }));
971
1075
  }
972
1076
  function buildCollectionTargets(platformDir, _filterName) {
973
- const collectionsDir = join(platformDir, "collections");
974
- if (!existsSync(collectionsDir)) return [];
1077
+ const collectionsDir = join2(platformDir, "collections");
1078
+ if (!existsSync2(collectionsDir)) return [];
975
1079
  const targets = [];
976
- for (const entry of readdirSync(collectionsDir, { withFileTypes: true })) {
1080
+ for (const entry of readdirSync2(collectionsDir, { withFileTypes: true })) {
977
1081
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
978
- const filePath = join(collectionsDir, entry.name);
1082
+ const filePath = join2(collectionsDir, entry.name);
979
1083
  targets.push({
980
1084
  kind: "collection",
981
1085
  name: basename(entry.name, ".json"),
982
1086
  filePath,
983
- source: readFileSync(filePath, "utf-8")
1087
+ source: readFileSync2(filePath, "utf-8")
984
1088
  });
985
1089
  }
986
1090
  return targets;
987
1091
  }
988
1092
  function buildHookTargets(platformDir, filterName) {
989
- const hooksDir = join(platformDir, "hooks");
990
- if (!existsSync(hooksDir)) return [];
1093
+ const hooksDir = join2(platformDir, "hooks");
1094
+ if (!existsSync2(hooksDir)) return [];
991
1095
  const targets = [];
992
- for (const entry of readdirSync(hooksDir, { withFileTypes: true })) {
1096
+ for (const entry of readdirSync2(hooksDir, { withFileTypes: true })) {
993
1097
  if (!entry.isFile()) continue;
994
1098
  if (!entry.name.endsWith(".js") && !entry.name.endsWith(".ts")) continue;
995
1099
  const base = entry.name.replace(/\.(js|ts)$/, "");
996
1100
  if (filterName && base !== filterName) continue;
997
- const filePath = join(hooksDir, entry.name);
1101
+ const filePath = join2(hooksDir, entry.name);
998
1102
  targets.push({
999
1103
  kind: "hook",
1000
1104
  name: base,
1001
1105
  filePath,
1002
- source: readFileSync(filePath, "utf-8")
1106
+ source: readFileSync2(filePath, "utf-8")
1003
1107
  });
1004
1108
  }
1005
1109
  return targets;
1006
1110
  }
1007
1111
  function resolveAgentLintFiles(platformDir, filterName, appName) {
1008
- const agentsDir = join(platformDir, "agents");
1009
- if (!existsSync(agentsDir)) return [];
1112
+ const agentsDir = join2(platformDir, "agents");
1113
+ if (!existsSync2(agentsDir)) return [];
1010
1114
  const agents = [];
1011
- for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
1115
+ for (const entry of readdirSync2(agentsDir, { withFileTypes: true })) {
1012
1116
  if (!entry.isDirectory()) continue;
1013
- const directoryPath = join(agentsDir, entry.name);
1014
- const configPath = join(directoryPath, "config.json");
1117
+ const directoryPath = join2(agentsDir, entry.name);
1118
+ const configPath = join2(directoryPath, "config.json");
1015
1119
  let configSource;
1016
1120
  let config;
1017
1121
  let externalID = appName ? `${appName}:${entry.name}` : "";
1018
1122
  let displayName = "";
1019
1123
  let policyEnabled = false;
1020
- if (existsSync(configPath)) {
1021
- configSource = readFileSync(configPath, "utf-8");
1124
+ if (existsSync2(configPath)) {
1125
+ configSource = readFileSync2(configPath, "utf-8");
1022
1126
  try {
1023
1127
  config = JSON.parse(configSource);
1024
1128
  if (typeof config === "object" && config !== null && !Array.isArray(config)) {
@@ -1065,14 +1169,14 @@ function agentConfigTargets(agents) {
1065
1169
  function agentPolicyTargets(agents) {
1066
1170
  const targets = [];
1067
1171
  for (const agent of agents) {
1068
- const policyPath = join(agent.directoryPath, "policy.js");
1069
- const hasPolicyFile = existsSync(policyPath);
1172
+ const policyPath = join2(agent.directoryPath, "policy.js");
1173
+ const hasPolicyFile = existsSync2(policyPath);
1070
1174
  if (!hasPolicyFile && !agent.policyEnabled) continue;
1071
1175
  targets.push({
1072
1176
  kind: "agent-policy",
1073
1177
  name: agent.name,
1074
1178
  filePath: policyPath,
1075
- source: hasPolicyFile ? readFileSync(policyPath, "utf-8") : "",
1179
+ source: hasPolicyFile ? readFileSync2(policyPath, "utf-8") : "",
1076
1180
  config: { policyEnabled: agent.policyEnabled }
1077
1181
  });
1078
1182
  }
@@ -1110,16 +1214,19 @@ async function safeLintPass(label, build) {
1110
1214
  return null;
1111
1215
  }
1112
1216
  }
1113
- async function lintCollectionFiles(projectRoot, platformDir, filterName) {
1217
+ async function lintCollectionFiles(projectRoot, platformDir, filterName, printWarnings = true) {
1114
1218
  const result = await safeLintPass(
1115
1219
  "collection",
1116
1220
  async () => runLint({ projectRoot, targets: buildCollectionTargets(platformDir, filterName) })
1117
1221
  );
1118
- if (!result || result.issues.length === 0) return;
1119
- safePrintLint(result.issues, projectRoot);
1222
+ if (!result || result.issues.length === 0) return [];
1223
+ if (printWarnings || result.errors.length > 0) {
1224
+ safePrintLint(result.issues, projectRoot);
1225
+ }
1120
1226
  if (result.errors.length > 0) {
1121
1227
  throw new Error(`Found ${result.errors.length} collection lint error(s)`);
1122
1228
  }
1229
+ return result.issues;
1123
1230
  }
1124
1231
  async function lintHookFiles(api, projectRoot, platformDir, appName, filterName) {
1125
1232
  const result = await safeLintPass("hook", async () => {
@@ -1179,7 +1286,7 @@ function parsePackageManagerSpecifier(value) {
1179
1286
  }
1180
1287
  function packageManagerFromPackageJson(projectRoot) {
1181
1288
  try {
1182
- const pkg = JSON.parse(readFileSync2(join2(projectRoot, "package.json"), "utf-8"));
1289
+ const pkg = JSON.parse(readFileSync3(join3(projectRoot, "package.json"), "utf-8"));
1183
1290
  return parsePackageManagerSpecifier(pkg.packageManager);
1184
1291
  } catch {
1185
1292
  return void 0;
@@ -1230,7 +1337,7 @@ function detectPackageManager(projectRoot, override) {
1230
1337
  { pm: "npm", file: "package-lock.json" }
1231
1338
  ];
1232
1339
  for (const { pm, file } of lockfileManagers) {
1233
- if (existsSync2(join2(projectRoot, file)) && commandAvailable(pm)) return pm;
1340
+ if (existsSync3(join3(projectRoot, file)) && commandAvailable(pm)) return pm;
1234
1341
  }
1235
1342
  for (const pm of ["pnpm", "bun", "yarn", "npm"]) {
1236
1343
  if (commandAvailable(pm)) return pm;
@@ -1523,6 +1630,8 @@ ${pc2.dim("Resources:")}
1523
1630
  (none) Plan all resources
1524
1631
  collections Plan only collections
1525
1632
  collections/<name> Plan single collection
1633
+ views Plan Git-owned SQL views
1634
+ views/<name> Plan a single view
1526
1635
  automations Plan only automations
1527
1636
  automations/<name> Plan single automation
1528
1637
  hooks Plan only hooks
@@ -1550,9 +1659,11 @@ ${pc2.dim("Description:")}
1550
1659
  Create or update resources from local files.
1551
1660
 
1552
1661
  ${pc2.dim("Resources:")}
1553
- (none) Apply all resources
1662
+ (none) Apply resources and app; views require an explicit target
1554
1663
  collections Apply only collections
1555
1664
  collections/<name> Apply single collection
1665
+ views Publish Git-owned views (human source admin)
1666
+ views/<name> Publish a single view
1556
1667
  automations Apply only automations
1557
1668
  automations/<name> Apply single automation
1558
1669
  hooks Apply only hooks
@@ -1574,7 +1685,8 @@ ${pc2.dim("Options:")}
1574
1685
  --package-manager Package manager for app builds (pnpm, bun, yarn, npm)
1575
1686
 
1576
1687
  ${pc2.dim("Examples:")}
1577
- lumera apply # Apply everything (shows plan, asks to confirm)
1688
+ lumera apply # Apply resources and app (excludes views)
1689
+ lumera apply views # Publish views with human-admin credentials
1578
1690
  lumera apply collections # Apply all collections
1579
1691
  lumera apply collections/users # Apply single collection
1580
1692
  lumera apply agents -y # Apply agents without confirmation
@@ -1593,6 +1705,7 @@ ${pc2.dim("Usage:")}
1593
1705
 
1594
1706
  ${pc2.dim("Description:")}
1595
1707
  Download remote state to local files.
1708
+ Views remain Git-owned. Use git pull to sync their source files.
1596
1709
  Refuses to overwrite local files that have uncommitted changes
1597
1710
  (use --force to override, or 'lumera diff' to inspect first).
1598
1711
 
@@ -1710,7 +1823,7 @@ function parseResource(resourcePath) {
1710
1823
  const parts = resourcePath.split("/");
1711
1824
  const type = parts[0];
1712
1825
  const name = parts.slice(1).join("/") || null;
1713
- const validTypes = ["collections", "automations", "hooks", "agents", "skills", "mailboxes", "app"];
1826
+ const validTypes = ["collections", "automations", "hooks", "agents", "skills", "mailboxes", "app", "views"];
1714
1827
  if (!validTypes.includes(type)) {
1715
1828
  console.log(pc2.red(` Unknown resource type "${type}". Valid types: ${validTypes.join(", ")}`));
1716
1829
  process.exit(1);
@@ -1718,29 +1831,29 @@ function parseResource(resourcePath) {
1718
1831
  return { type, name };
1719
1832
  }
1720
1833
  function getPlatformDir() {
1721
- if (existsSync2(join2(process.cwd(), "platform"))) {
1722
- return join2(process.cwd(), "platform");
1834
+ if (existsSync3(join3(process.cwd(), "platform"))) {
1835
+ return join3(process.cwd(), "platform");
1723
1836
  }
1724
- if (existsSync2(join2(process.cwd(), "lumera_platform"))) {
1725
- return join2(process.cwd(), "lumera_platform");
1837
+ if (existsSync3(join3(process.cwd(), "lumera_platform"))) {
1838
+ return join3(process.cwd(), "lumera_platform");
1726
1839
  }
1727
- return join2(process.cwd(), "platform");
1840
+ return join3(process.cwd(), "platform");
1728
1841
  }
1729
1842
  function toSafeFilename(name) {
1730
1843
  return name.replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase();
1731
1844
  }
1732
1845
  function loadLocalCollections(platformDir, filterName) {
1733
- const collectionsDir = join2(platformDir, "collections");
1734
- if (!existsSync2(collectionsDir)) {
1846
+ const collectionsDir = join3(platformDir, "collections");
1847
+ if (!existsSync3(collectionsDir)) {
1735
1848
  return [];
1736
1849
  }
1737
1850
  const collections = [];
1738
1851
  const errors = [];
1739
- for (const file of readdirSync2(collectionsDir)) {
1852
+ for (const file of readdirSync3(collectionsDir)) {
1740
1853
  if (!file.endsWith(".json")) continue;
1741
- const filePath = join2(collectionsDir, file);
1854
+ const filePath = join3(collectionsDir, file);
1742
1855
  try {
1743
- const content = readFileSync2(filePath, "utf-8");
1856
+ const content = readFileSync3(filePath, "utf-8");
1744
1857
  const collection = JSON.parse(content);
1745
1858
  if (filterName && collection.name !== filterName && collection.id !== filterName) {
1746
1859
  continue;
@@ -1816,27 +1929,27 @@ function loadLocalCollections(platformDir, filterName) {
1816
1929
  return collections;
1817
1930
  }
1818
1931
  function loadLocalAutomations(platformDir, filterName, appName) {
1819
- const automationsDir = join2(platformDir, "automations");
1820
- if (!existsSync2(automationsDir)) {
1932
+ const automationsDir = join3(platformDir, "automations");
1933
+ if (!existsSync3(automationsDir)) {
1821
1934
  return [];
1822
1935
  }
1823
1936
  const automations = [];
1824
1937
  const errors = [];
1825
- for (const entry of readdirSync2(automationsDir, { withFileTypes: true })) {
1938
+ for (const entry of readdirSync3(automationsDir, { withFileTypes: true })) {
1826
1939
  if (!entry.isDirectory()) continue;
1827
- const automationDir = join2(automationsDir, entry.name);
1828
- const configPath = join2(automationDir, "config.json");
1829
- const mainPath = join2(automationDir, "main.py");
1830
- if (!existsSync2(configPath)) {
1940
+ const automationDir = join3(automationsDir, entry.name);
1941
+ const configPath = join3(automationDir, "config.json");
1942
+ const mainPath = join3(automationDir, "main.py");
1943
+ if (!existsSync3(configPath)) {
1831
1944
  errors.push(`${entry.name}: missing config.json`);
1832
1945
  continue;
1833
1946
  }
1834
- if (!existsSync2(mainPath)) {
1947
+ if (!existsSync3(mainPath)) {
1835
1948
  errors.push(`${entry.name}: missing main.py`);
1836
1949
  continue;
1837
1950
  }
1838
1951
  try {
1839
- const configContent = readFileSync2(configPath, "utf-8");
1952
+ const configContent = readFileSync3(configPath, "utf-8");
1840
1953
  const rawConfig = JSON.parse(configContent);
1841
1954
  if (filterName && rawConfig.external_id !== filterName && rawConfig.name !== filterName && entry.name !== filterName) {
1842
1955
  continue;
@@ -1863,7 +1976,7 @@ function loadLocalAutomations(platformDir, filterName, appName) {
1863
1976
  errors.push(`${entry.name}: duplicate external_id "${config.external_id}" (also defined in ${existingByExtId.automation.name})`);
1864
1977
  continue;
1865
1978
  }
1866
- let code = readFileSync2(mainPath, "utf-8");
1979
+ let code = readFileSync3(mainPath, "utf-8");
1867
1980
  if (appName) {
1868
1981
  code = code.replaceAll("{{app}}", appName);
1869
1982
  }
@@ -1882,15 +1995,15 @@ function loadLocalAutomations(platformDir, filterName, appName) {
1882
1995
  return automations;
1883
1996
  }
1884
1997
  function loadLocalHooks(platformDir, filterName, appName) {
1885
- const hooksDir = join2(platformDir, "hooks");
1886
- if (!existsSync2(hooksDir)) {
1998
+ const hooksDir = join3(platformDir, "hooks");
1999
+ if (!existsSync3(hooksDir)) {
1887
2000
  return [];
1888
2001
  }
1889
2002
  const hooks = [];
1890
- for (const file of readdirSync2(hooksDir)) {
2003
+ for (const file of readdirSync3(hooksDir)) {
1891
2004
  if (!file.endsWith(".js") && !file.endsWith(".ts")) continue;
1892
- const filePath = join2(hooksDir, file);
1893
- const content = readFileSync2(filePath, "utf-8");
2005
+ const filePath = join3(hooksDir, file);
2006
+ const content = readFileSync3(filePath, "utf-8");
1894
2007
  const config = parseHookConfig(content);
1895
2008
  if (!config) {
1896
2009
  console.log(pc2.yellow(` \u26A0 Skipping ${file}: could not parse config export`));
@@ -2022,6 +2135,20 @@ function fieldsDiffer(local, remote) {
2022
2135
  }
2023
2136
  return false;
2024
2137
  }
2138
+ function lumeraFileMaxSelectErrors(local, remote) {
2139
+ const remoteFields = new Map(remote.schema.map((f) => [f.name, f]));
2140
+ const errors = [];
2141
+ for (const field of local.fields) {
2142
+ const deployed = remoteFields.get(field.name);
2143
+ if (mapFieldType(field.type) !== "lumera_file" || deployed?.type !== "lumera_file") continue;
2144
+ const deployedMaxSelect = Math.max(Number(normalizeFieldOption("lumera_file", "maxSelect", deployed.options?.maxSelect)), 1);
2145
+ const requestedMaxSelect = Math.max(Number(normalizeFieldOption("lumera_file", "maxSelect", field.maxSelect)), 1);
2146
+ if (deployedMaxSelect === requestedMaxSelect) continue;
2147
+ const requested = field.maxSelect === void 0 ? "omitting maxSelect (defaults to 1)" : `maxSelect ${requestedMaxSelect}`;
2148
+ errors.push(`File field "${field.name}" is deployed with maxSelect ${deployedMaxSelect}; ${requested} would change it, which is not supported. Keep maxSelect ${deployedMaxSelect}.`);
2149
+ }
2150
+ return errors;
2151
+ }
2025
2152
  function localIndexesToSql(local, appName) {
2026
2153
  if (!local.indexes || local.indexes.length === 0) return [];
2027
2154
  return local.indexes.map((idx) => buildIndexSql(local, idx, appName)).sort();
@@ -2135,13 +2262,15 @@ async function planCollections(api, localCollections, appName, projectId) {
2135
2262
  const f = remoteFieldMap.get(name);
2136
2263
  fieldDetails.push({ action: "-", name: f.name, type: f.type, required: f.required });
2137
2264
  }
2265
+ const errors = lumeraFileMaxSelectErrors(local, remote);
2138
2266
  changes.push({
2139
2267
  type: "update",
2140
2268
  resource: "collection",
2141
2269
  id: local.id,
2142
2270
  name: local.name,
2143
2271
  details: details.join(", "),
2144
- fieldDetails
2272
+ fieldDetails,
2273
+ ...errors.length > 0 ? { errors } : {}
2145
2274
  });
2146
2275
  }
2147
2276
  }
@@ -2221,12 +2350,12 @@ async function planAutomations(api, localAutomations, projectId) {
2221
2350
  return changes;
2222
2351
  }
2223
2352
  function loadLocalMailboxes(platformDir, filterName) {
2224
- const mailboxesDir = join2(platformDir, "mailboxes");
2225
- if (!existsSync2(mailboxesDir)) {
2353
+ const mailboxesDir = join3(platformDir, "mailboxes");
2354
+ if (!existsSync3(mailboxesDir)) {
2226
2355
  return [];
2227
2356
  }
2228
2357
  const mailboxes = [];
2229
- for (const file of readdirSync2(mailboxesDir)) {
2358
+ for (const file of readdirSync3(mailboxesDir)) {
2230
2359
  if (!file.endsWith(".json")) continue;
2231
2360
  const slug = file.replace(/\.json$/, "").trim();
2232
2361
  if (!slug) {
@@ -2236,10 +2365,10 @@ function loadLocalMailboxes(platformDir, filterName) {
2236
2365
  if (filterName && slug !== filterName) {
2237
2366
  continue;
2238
2367
  }
2239
- const filePath = join2(mailboxesDir, file);
2368
+ const filePath = join3(mailboxesDir, file);
2240
2369
  let raw;
2241
2370
  try {
2242
- raw = JSON.parse(readFileSync2(filePath, "utf-8"));
2371
+ raw = JSON.parse(readFileSync3(filePath, "utf-8"));
2243
2372
  } catch (e) {
2244
2373
  console.log(pc2.yellow(` \u26A0 Skipping ${file}: invalid JSON (${e.message})`));
2245
2374
  continue;
@@ -2318,8 +2447,8 @@ async function pullMailboxes(api, platformDir, filterName) {
2318
2447
  console.log(pc2.dim(" (no mailboxes)"));
2319
2448
  return;
2320
2449
  }
2321
- const outDir = join2(platformDir, "mailboxes");
2322
- if (!existsSync2(outDir)) {
2450
+ const outDir = join3(platformDir, "mailboxes");
2451
+ if (!existsSync3(outDir)) {
2323
2452
  mkdirSync(outDir, { recursive: true });
2324
2453
  }
2325
2454
  let count = 0;
@@ -2327,7 +2456,7 @@ async function pullMailboxes(api, platformDir, filterName) {
2327
2456
  if (filterName && mb.slug !== filterName) continue;
2328
2457
  const body = {};
2329
2458
  if (mb.description) body.description = mb.description;
2330
- const file = join2(outDir, `${toSafeFilename(mb.slug)}.json`);
2459
+ const file = join3(outDir, `${toSafeFilename(mb.slug)}.json`);
2331
2460
  writeFileSync(file, JSON.stringify(body, null, 2) + "\n");
2332
2461
  console.log(pc2.green(" \u2713"), `pulled mailbox ${mb.slug} ${pc2.dim(`\u2192 ${file}`)}`);
2333
2462
  count++;
@@ -2634,7 +2763,7 @@ async function applyAppAPI(projectRoot) {
2634
2763
  });
2635
2764
  }
2636
2765
  async function pullCollections(api, platformDir, filterName, projectId) {
2637
- const collectionsDir = join2(platformDir, "collections");
2766
+ const collectionsDir = join3(platformDir, "collections");
2638
2767
  mkdirSync(collectionsDir, { recursive: true });
2639
2768
  const collections = await api.listCollections();
2640
2769
  for (const collection of collections) {
@@ -2660,13 +2789,13 @@ async function pullCollections(api, platformDir, filterName, projectId) {
2660
2789
  search: collection.search
2661
2790
  };
2662
2791
  const fileName = toSafeFilename(localName);
2663
- const filePath = join2(collectionsDir, `${fileName}.json`);
2792
+ const filePath = join3(collectionsDir, `${fileName}.json`);
2664
2793
  writeFileSync(filePath, JSON.stringify(localFormat, null, 2) + "\n");
2665
2794
  console.log(pc2.green(" \u2713"), `${localName} \u2192 collections/${fileName}.json`);
2666
2795
  }
2667
2796
  }
2668
2797
  async function pullAutomations(api, platformDir, filterName, projectId) {
2669
- const automationsDir = join2(platformDir, "automations");
2798
+ const automationsDir = join3(platformDir, "automations");
2670
2799
  mkdirSync(automationsDir, { recursive: true });
2671
2800
  const automations = await api.listAutomations({
2672
2801
  include_code: true,
@@ -2680,7 +2809,7 @@ async function pullAutomations(api, platformDir, filterName, projectId) {
2680
2809
  continue;
2681
2810
  }
2682
2811
  const dirName = automation.external_id.replace(/[^a-zA-Z0-9_-]/g, "_");
2683
- const automationDir = join2(automationsDir, dirName);
2812
+ const automationDir = join3(automationsDir, dirName);
2684
2813
  mkdirSync(automationDir, { recursive: true });
2685
2814
  const config = {
2686
2815
  external_id: automation.external_id,
@@ -2713,13 +2842,13 @@ async function pullAutomations(api, platformDir, filterName, projectId) {
2713
2842
  }
2714
2843
  } catch {
2715
2844
  }
2716
- writeFileSync(join2(automationDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
2717
- writeFileSync(join2(automationDir, "main.py"), automation.code || "");
2845
+ writeFileSync(join3(automationDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
2846
+ writeFileSync(join3(automationDir, "main.py"), automation.code || "");
2718
2847
  console.log(pc2.green(" \u2713"), `${automation.name} \u2192 automations/${dirName}/`);
2719
2848
  }
2720
2849
  }
2721
2850
  async function pullHooks(api, platformDir, filterName, appName, projectId) {
2722
- const hooksDir = join2(platformDir, "hooks");
2851
+ const hooksDir = join3(platformDir, "hooks");
2723
2852
  mkdirSync(hooksDir, { recursive: true });
2724
2853
  const hooks = await api.listHooks(projectId ? { project_id: projectId } : void 0);
2725
2854
  for (const hook of hooks) {
@@ -2741,7 +2870,7 @@ export default async function handler({ record, app, http }) {
2741
2870
  ${hook.script.split("\n").map((line) => " " + line).join("\n")}
2742
2871
  }
2743
2872
  `;
2744
- writeFileSync(join2(hooksDir, fileName), content);
2873
+ writeFileSync(join3(hooksDir, fileName), content);
2745
2874
  console.log(pc2.green(" \u2713"), `${hook.name} \u2192 hooks/${fileName}`);
2746
2875
  }
2747
2876
  }
@@ -2804,7 +2933,7 @@ function localSkillResources(skillDir) {
2804
2933
  let fileCount = 0;
2805
2934
  const decoder = new TextDecoder("utf-8", { fatal: true });
2806
2935
  const scan = (directory, parentParts) => {
2807
- const names = readdirSync2(directory, { encoding: "buffer" }).map((rawName) => {
2936
+ const names = readdirSync3(directory, { encoding: "buffer" }).map((rawName) => {
2808
2937
  try {
2809
2938
  return decoder.decode(rawName);
2810
2939
  } catch {
@@ -2816,7 +2945,7 @@ function localSkillResources(skillDir) {
2816
2945
  if (parentParts.length === 0 && name === "SKILL.md") continue;
2817
2946
  const parts = [...parentParts, name];
2818
2947
  const resourcePath = parts.join("/");
2819
- const filePath = join2(directory, name);
2948
+ const filePath = join3(directory, name);
2820
2949
  const stat = lstatSync(filePath);
2821
2950
  if (stat.isSymbolicLink()) {
2822
2951
  errors.push(`${resourcePath}: symlinks are not supported`);
@@ -2846,7 +2975,7 @@ function localSkillResources(skillDir) {
2846
2975
  errors.push(`${resourcePath}: path conflicts ignoring letter case with ${conflict.path}`);
2847
2976
  continue;
2848
2977
  }
2849
- const sha256 = createHash("sha256").update(readFileSync2(filePath)).digest("hex");
2978
+ const sha256 = createHash2("sha256").update(readFileSync3(filePath)).digest("hex");
2850
2979
  resources.push({ path: resourcePath, filePath, size: stat.size, sha256 });
2851
2980
  }
2852
2981
  };
@@ -2877,20 +3006,20 @@ function skillResourceChangesSummary(changes) {
2877
3006
  ].join(", ");
2878
3007
  }
2879
3008
  function loadLocalSkills(platformDir, filterName) {
2880
- const skillsDir = join2(platformDir, "skills");
2881
- if (!existsSync2(skillsDir)) return [];
3009
+ const skillsDir = join3(platformDir, "skills");
3010
+ if (!existsSync3(skillsDir)) return [];
2882
3011
  const skills = [];
2883
3012
  const errors = [];
2884
- for (const entry of readdirSync2(skillsDir, { withFileTypes: true })) {
3013
+ for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
2885
3014
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
2886
3015
  if (filterName && entry.name !== filterName) continue;
2887
3016
  if (!SKILL_SLUG_RE.test(entry.name)) {
2888
3017
  errors.push(`${entry.name}: directory name must be a lowercase slug`);
2889
3018
  continue;
2890
3019
  }
2891
- const skillDir = join2(skillsDir, entry.name);
2892
- const path = join2(skillDir, "SKILL.md");
2893
- if (!existsSync2(path)) {
3020
+ const skillDir = join3(skillsDir, entry.name);
3021
+ const path = join3(skillDir, "SKILL.md");
3022
+ if (!existsSync3(path)) {
2894
3023
  errors.push(`${entry.name}: missing SKILL.md`);
2895
3024
  continue;
2896
3025
  }
@@ -2901,7 +3030,7 @@ function loadLocalSkills(platformDir, filterName) {
2901
3030
  }
2902
3031
  const { resources, errors: resourceErrors } = localSkillResources(skillDir);
2903
3032
  errors.push(...resourceErrors.map((error3) => `${entry.name}: ${error3}`));
2904
- const content = readFileSync2(path, "utf-8");
3033
+ const content = readFileSync3(path, "utf-8");
2905
3034
  const separator = /\r?\n---\r?\n/.exec(content);
2906
3035
  const parts = separator ? [content.slice(0, separator.index), content.slice(separator.index + separator[0].length)] : [content];
2907
3036
  if (parts.length !== 2) {
@@ -3079,8 +3208,8 @@ async function planSkills(api, localSkills, projectId, appName) {
3079
3208
  return changes;
3080
3209
  }
3081
3210
  async function uploadSkillResource(api, skillID, resource) {
3082
- const bytes = readFileSync2(resource.filePath);
3083
- const sha256 = createHash("sha256").update(bytes).digest("hex");
3211
+ const bytes = readFileSync3(resource.filePath);
3212
+ const sha256 = createHash2("sha256").update(bytes).digest("hex");
3084
3213
  if (bytes.byteLength !== resource.size || sha256 !== resource.sha256) {
3085
3214
  throw new Error(`${resource.path} changed while apply was running`);
3086
3215
  }
@@ -3180,10 +3309,10 @@ function validateRemoteSkillResources(resources) {
3180
3309
  }
3181
3310
  function writeVerifiedSkillResource(resource, bytes, skillDir, source) {
3182
3311
  if (bytes.byteLength !== resource.size) throw new Error(`${resource.path}: ${source} size does not match`);
3183
- if (createHash("sha256").update(bytes).digest("hex") !== resource.sha256) {
3312
+ if (createHash2("sha256").update(bytes).digest("hex") !== resource.sha256) {
3184
3313
  throw new Error(`${resource.path}: ${source} checksum does not match`);
3185
3314
  }
3186
- const destination = join2(skillDir, ...resource.path.split("/"));
3315
+ const destination = join3(skillDir, ...resource.path.split("/"));
3187
3316
  mkdirSync(dirname2(destination), { recursive: true });
3188
3317
  writeFileSync(destination, bytes);
3189
3318
  }
@@ -3200,12 +3329,12 @@ async function downloadSkillResource(resource, skillDir) {
3200
3329
  }
3201
3330
  function replaceSkillDirectory(stagingDir, skillDir) {
3202
3331
  const backupDir = `${stagingDir}.previous`;
3203
- const hadExisting = existsSync2(skillDir);
3332
+ const hadExisting = existsSync3(skillDir);
3204
3333
  if (hadExisting) renameSync(skillDir, backupDir);
3205
3334
  try {
3206
3335
  renameSync(stagingDir, skillDir);
3207
3336
  } catch (error3) {
3208
- if (hadExisting && !existsSync2(skillDir)) renameSync(backupDir, skillDir);
3337
+ if (hadExisting && !existsSync3(skillDir)) renameSync(backupDir, skillDir);
3209
3338
  throw error3;
3210
3339
  }
3211
3340
  if (hadExisting) rmSync(backupDir, { recursive: true, force: true });
@@ -3216,7 +3345,7 @@ async function pullSkills(api, platformDir, filterName, projectId, appName, chec
3216
3345
  return;
3217
3346
  }
3218
3347
  const scopedProjectID = requireProjectSkillsProjectID(projectId);
3219
- const skillsDir = join2(platformDir, "skills");
3348
+ const skillsDir = join3(platformDir, "skills");
3220
3349
  const remoteSkills = remoteSkillsByLocalSlug(
3221
3350
  await api.listAgentSkills({ project_id: scopedProjectID }),
3222
3351
  appName
@@ -3250,11 +3379,11 @@ async function pullSkills(api, platformDir, filterName, projectId, appName, chec
3250
3379
  const resources = [...skill.resources ?? []].sort((a, b) => a.path.localeCompare(b.path));
3251
3380
  validateRemoteSkillResources(resources);
3252
3381
  const localResources = new Map(local?.resources.map((resource) => [resource.path, resource]) ?? []);
3253
- const skillDir = join2(skillsDir, localSlug);
3382
+ const skillDir = join3(skillsDir, localSlug);
3254
3383
  mkdirSync(skillsDir, { recursive: true });
3255
- const stagingDir = mkdtempSync(join2(platformDir, `.skill-${localSlug}-pull-`));
3384
+ const stagingDir = mkdtempSync(join3(platformDir, `.skill-${localSlug}-pull-`));
3256
3385
  try {
3257
- writeFileSync(join2(stagingDir, "SKILL.md"), skillMarkdown({
3386
+ writeFileSync(join3(stagingDir, "SKILL.md"), skillMarkdown({
3258
3387
  name: skill.name,
3259
3388
  summary: skill.summary || "",
3260
3389
  instructions: skill.instructions || ""
@@ -3262,7 +3391,7 @@ async function pullSkills(api, platformDir, filterName, projectId, appName, chec
3262
3391
  for (const resource of resources) {
3263
3392
  const localResource = localResources.get(resource.path);
3264
3393
  if (localResource?.size === resource.size && localResource.sha256 === resource.sha256) {
3265
- writeVerifiedSkillResource(resource, readFileSync2(localResource.filePath), stagingDir, "local");
3394
+ writeVerifiedSkillResource(resource, readFileSync3(localResource.filePath), stagingDir, "local");
3266
3395
  } else {
3267
3396
  await downloadSkillResource(resource, stagingDir);
3268
3397
  }
@@ -3275,26 +3404,26 @@ async function pullSkills(api, platformDir, filterName, projectId, appName, chec
3275
3404
  }
3276
3405
  }
3277
3406
  function loadLocalAgents(platformDir, filterName, appName) {
3278
- const agentsDir = join2(platformDir, "agents");
3279
- if (!existsSync2(agentsDir)) return [];
3407
+ const agentsDir = join3(platformDir, "agents");
3408
+ if (!existsSync3(agentsDir)) return [];
3280
3409
  const agents = [];
3281
3410
  const errors = [];
3282
- for (const entry of readdirSync2(agentsDir, { withFileTypes: true })) {
3411
+ for (const entry of readdirSync3(agentsDir, { withFileTypes: true })) {
3283
3412
  if (!entry.isDirectory()) continue;
3284
- const agentDir = join2(agentsDir, entry.name);
3285
- const configPath = join2(agentDir, "config.json");
3286
- const promptPath = join2(agentDir, "system_prompt.md");
3287
- const policyPath = join2(agentDir, "policy.js");
3288
- if (!existsSync2(configPath)) {
3413
+ const agentDir = join3(agentsDir, entry.name);
3414
+ const configPath = join3(agentDir, "config.json");
3415
+ const promptPath = join3(agentDir, "system_prompt.md");
3416
+ const policyPath = join3(agentDir, "policy.js");
3417
+ if (!existsSync3(configPath)) {
3289
3418
  errors.push(`${entry.name}: missing config.json`);
3290
3419
  continue;
3291
3420
  }
3292
- if (!existsSync2(promptPath)) {
3421
+ if (!existsSync3(promptPath)) {
3293
3422
  errors.push(`${entry.name}: missing system_prompt.md`);
3294
3423
  continue;
3295
3424
  }
3296
3425
  try {
3297
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
3426
+ const config = JSON.parse(readFileSync3(configPath, "utf-8"));
3298
3427
  if (filterName && config.external_id !== filterName && config.name !== filterName && entry.name !== filterName) {
3299
3428
  continue;
3300
3429
  }
@@ -3324,8 +3453,8 @@ function loadLocalAgents(platformDir, filterName, appName) {
3324
3453
  continue;
3325
3454
  }
3326
3455
  }
3327
- let systemPrompt = readFileSync2(promptPath, "utf-8");
3328
- let policyScript = existsSync2(policyPath) ? readFileSync2(policyPath, "utf-8") : "";
3456
+ let systemPrompt = readFileSync3(promptPath, "utf-8");
3457
+ let policyScript = existsSync3(policyPath) ? readFileSync3(policyPath, "utf-8") : "";
3329
3458
  if (appName) {
3330
3459
  systemPrompt = systemPrompt.replaceAll("{{app}}", appName);
3331
3460
  policyScript = policyScript.replaceAll("{{app}}", appName);
@@ -3528,7 +3657,7 @@ function allowedFunctionsChanged(remote, local) {
3528
3657
  return JSON.stringify([...remote ?? []].sort()) !== JSON.stringify([...local].sort());
3529
3658
  }
3530
3659
  async function pullAgents(api, platformDir, filterName, projectId, appName) {
3531
- const agentsDir = join2(platformDir, "agents");
3660
+ const agentsDir = join3(platformDir, "agents");
3532
3661
  mkdirSync(agentsDir, { recursive: true });
3533
3662
  const agents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
3534
3663
  const skillIdToSlug = /* @__PURE__ */ new Map();
@@ -3550,7 +3679,7 @@ async function pullAgents(api, platformDir, filterName, projectId, appName) {
3550
3679
  continue;
3551
3680
  }
3552
3681
  const dirName = agent.external_id.replace(/[^a-zA-Z0-9_-]/g, "_");
3553
- const agentDir = join2(agentsDir, dirName);
3682
+ const agentDir = join3(agentsDir, dirName);
3554
3683
  mkdirSync(agentDir, { recursive: true });
3555
3684
  const skillSlugs = [];
3556
3685
  if (agent.skill_ids) {
@@ -3570,16 +3699,27 @@ async function pullAgents(api, platformDir, filterName, projectId, appName) {
3570
3699
  if (agent.idle_reap_timeout_seconds) config.idle_reap_timeout_seconds = agent.idle_reap_timeout_seconds;
3571
3700
  if (skillSlugs.length > 0) config.skills = skillSlugs;
3572
3701
  if (agent.policy_enabled) config.policy_enabled = true;
3573
- writeFileSync(join2(agentDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
3574
- writeFileSync(join2(agentDir, "system_prompt.md"), agent.system_prompt || "");
3702
+ writeFileSync(join3(agentDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
3703
+ writeFileSync(join3(agentDir, "system_prompt.md"), agent.system_prompt || "");
3575
3704
  if (agent.policy_script) {
3576
- writeFileSync(join2(agentDir, "policy.js"), agent.policy_script);
3705
+ writeFileSync(join3(agentDir, "policy.js"), agent.policy_script);
3577
3706
  }
3578
3707
  console.log(pc2.green(" \u2713"), `${agent.name} \u2192 agents/${dirName}/`);
3579
3708
  }
3580
3709
  }
3581
3710
  async function listResources(api, platformDir, filterType, appName, projectId) {
3582
3711
  const results = [];
3712
+ if (!filterType || filterType === "views") {
3713
+ const local = loadLocalViews(platformDir);
3714
+ const remote = (await api.listViews()).filter((view) => !projectId || view.project_id === projectId);
3715
+ for (const view of local) {
3716
+ const deployed = remote.find((candidate) => candidate.name === view.config.name);
3717
+ let status = "local-only";
3718
+ if (deployed) status = deployed.definition_hash === viewDefinitionHash(view) ? "synced" : "changed";
3719
+ results.push({ name: view.config.name, type: "views", status });
3720
+ }
3721
+ for (const view of remote) if (!local.some((candidate) => candidate.config.name === view.name)) results.push({ name: view.name, type: "views", status: "remote-only" });
3722
+ }
3583
3723
  if (!filterType || filterType === "collections") {
3584
3724
  const localCollections = loadLocalCollections(platformDir);
3585
3725
  const remoteCollections = await api.listCollections();
@@ -4067,6 +4207,13 @@ async function destroyApp(skipConfirm) {
4067
4207
  console.log(pc2.green(" \u2713"), `App "${appRecord.name}" deleted from Lumera.`);
4068
4208
  }
4069
4209
  async function showResource(api, platformDir, resourceType, resourceName, appName, projectId) {
4210
+ if (resourceType === "views") {
4211
+ const local = loadLocalViews(platformDir, resourceName)[0];
4212
+ const remote = (await api.listViews()).find((view) => view.name === resourceName && (!projectId || view.project_id === projectId));
4213
+ if (!local && !remote) throw new Error(`View ${resourceName} not found`);
4214
+ console.log(JSON.stringify({ local, deployed: remote }, null, 2));
4215
+ return;
4216
+ }
4070
4217
  if (resourceType === "collections") {
4071
4218
  const localCollections = loadLocalCollections(platformDir, resourceName);
4072
4219
  const remoteCollections = await api.listCollections();
@@ -4315,8 +4462,9 @@ async function plan(args) {
4315
4462
  console.log(pc2.cyan(pc2.bold(" Plan")));
4316
4463
  console.log(pc2.dim(" Comparing local files to remote state..."));
4317
4464
  console.log();
4465
+ let collectionLintWarnings = [];
4318
4466
  if (!type || type === "collections") {
4319
- await lintCollectionFiles(projectRoot, platformDir, name || void 0);
4467
+ collectionLintWarnings = await lintCollectionFiles(projectRoot, platformDir, name || void 0, false);
4320
4468
  }
4321
4469
  if (!type || type === "hooks") {
4322
4470
  await lintHookFiles(api, projectRoot, platformDir, appName, name || void 0);
@@ -4349,6 +4497,10 @@ async function plan(args) {
4349
4497
  allChanges.push(...changes);
4350
4498
  }
4351
4499
  }
4500
+ if (!type || type === "views") {
4501
+ const views = loadLocalViews(platformDir, name || void 0);
4502
+ if (views.length) allChanges.push(...await planViews(api, views, projectId));
4503
+ }
4352
4504
  let localAutomations = [];
4353
4505
  if (!type || type === "automations") {
4354
4506
  localAutomations = loadLocalAutomations(platformDir, name || void 0, appName);
@@ -4387,6 +4539,7 @@ async function plan(args) {
4387
4539
  }
4388
4540
  }
4389
4541
  const lintWarnings = [
4542
+ ...collectionLintWarnings,
4390
4543
  ...agentLintWarnings,
4391
4544
  ...await safeLint(projectRoot, localAutomations)
4392
4545
  ];
@@ -4407,6 +4560,7 @@ async function plan(args) {
4407
4560
  safePrintLint(lintWarnings, projectRoot);
4408
4561
  return;
4409
4562
  }
4563
+ const collectionErrorCount = allChanges.reduce((count, change) => count + (change.errors?.length ?? 0), 0);
4410
4564
  if (jsonOutput) {
4411
4565
  const warnings = [];
4412
4566
  for (const change of allChanges) {
@@ -4417,8 +4571,9 @@ async function plan(args) {
4417
4571
  }
4418
4572
  }
4419
4573
  }
4420
- console.log(JSON.stringify({ changes: allChanges, warnings, validationErrors: planValidationErrors, lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
4421
- if (planValidationErrors > 0) process.exitCode = 1;
4574
+ const validationErrors = planValidationErrors + collectionErrorCount;
4575
+ console.log(JSON.stringify({ changes: allChanges, warnings, validationErrors, lintWarnings: serializeLintWarnings(lintWarnings, projectRoot) }));
4576
+ if (validationErrors > 0) process.exitCode = 1;
4422
4577
  return;
4423
4578
  }
4424
4579
  console.log(pc2.bold(" Changes:"));
@@ -4436,6 +4591,10 @@ async function plan(args) {
4436
4591
  }
4437
4592
  console.log();
4438
4593
  }
4594
+ if (change.errors && change.errors.length > 0) {
4595
+ for (const message of change.errors) console.log(pc2.red(` \u2717 ${message}`));
4596
+ console.log();
4597
+ }
4439
4598
  if (change.textDiffs && change.textDiffs.length > 0) {
4440
4599
  const maxDiffLines = 20;
4441
4600
  for (const td of change.textDiffs) {
@@ -4461,6 +4620,11 @@ async function plan(args) {
4461
4620
  safePrintLint(lintWarnings, projectRoot);
4462
4621
  if (planValidationErrors > 0) {
4463
4622
  console.log(pc2.red(` \u2717 Fix ${planValidationErrors} agent skill validation error${planValidationErrors === 1 ? "" : "s"} before applying.`));
4623
+ }
4624
+ if (collectionErrorCount > 0) {
4625
+ console.log(pc2.red(` \u2717 Fix ${collectionErrorCount} collection error${collectionErrorCount === 1 ? "" : "s"} before applying.`));
4626
+ }
4627
+ if (planValidationErrors > 0 || collectionErrorCount > 0) {
4464
4628
  process.exitCode = 1;
4465
4629
  } else {
4466
4630
  console.log(pc2.dim(` Run 'lumera apply' to apply these changes.`));
@@ -4530,6 +4694,11 @@ async function apply(args) {
4530
4694
  const allChanges = [];
4531
4695
  let planValidationErrors = 0;
4532
4696
  const localCollections = !type || type === "collections" ? loadLocalCollections(platformDir, name || void 0) : [];
4697
+ const localViews = type === "views" ? loadLocalViews(platformDir, name || void 0) : [];
4698
+ const viewsDir = join3(platformDir, "views");
4699
+ if (!type && existsSync3(viewsDir) && readdirSync3(viewsDir, { withFileTypes: true }).some((entry) => entry.isDirectory() && entry.name !== "shared")) {
4700
+ console.log(pc2.dim(" View publishing skipped. A human source-project admin can use lumera apply views or Resource shares."));
4701
+ }
4533
4702
  const localAutomations = !type || type === "automations" ? loadLocalAutomations(platformDir, name || void 0, appName) : [];
4534
4703
  const localHooks = !type || type === "hooks" ? loadLocalHooks(platformDir, name || void 0, appName) : [];
4535
4704
  const localSkills = !type || type === "skills" ? loadLocalSkills(platformDir, name || void 0) : [];
@@ -4539,6 +4708,7 @@ async function apply(args) {
4539
4708
  const localAgents = !type || type === "agents" ? loadLocalAgents(platformDir, name || void 0, appName) : [];
4540
4709
  const localMailboxes = !type || type === "mailboxes" ? loadLocalMailboxes(platformDir, name || void 0) : [];
4541
4710
  if (localCollections.length > 0) allChanges.push(...await planCollections(api, localCollections, appName, projectId));
4711
+ if (localViews.length > 0) allChanges.push(...await planViews(api, localViews, projectId));
4542
4712
  if (localAutomations.length > 0) allChanges.push(...await planAutomations(api, localAutomations, projectId));
4543
4713
  if (localHooks.length > 0) allChanges.push(...await planHooks(api, localHooks, collections, projectId));
4544
4714
  if (localSkills.length > 0) allChanges.push(...await planSkills(api, localSkills, projectId, appName));
@@ -4549,7 +4719,7 @@ async function apply(args) {
4549
4719
  }
4550
4720
  if (localMailboxes.length > 0) allChanges.push(...await planMailboxes(api, localMailboxes));
4551
4721
  if (name) {
4552
- const hasLocal = localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localSkills.length > 0 || localAgents.length > 0 || localMailboxes.length > 0;
4722
+ const hasLocal = localViews.length > 0 || localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localSkills.length > 0 || localAgents.length > 0 || localMailboxes.length > 0;
4553
4723
  if (!hasLocal) {
4554
4724
  console.log();
4555
4725
  console.log(pc2.red(` Resource "${name}" not found locally`));
@@ -4562,10 +4732,19 @@ async function apply(args) {
4562
4732
  console.log();
4563
4733
  process.exit(1);
4564
4734
  }
4735
+ const collectionErrors = allChanges.flatMap((change) => (change.errors ?? []).map((message) => `${change.resource} ${change.name}: ${message}`));
4736
+ if (collectionErrors.length > 0) {
4737
+ console.log();
4738
+ for (const message of collectionErrors) console.log(pc2.red(` \u2717 ${message}`));
4739
+ console.log();
4740
+ console.log(pc2.red(` \u2717 Apply aborted: fix ${collectionErrors.length} collection error${collectionErrors.length === 1 ? "" : "s"} first. Nothing was applied.`));
4741
+ console.log();
4742
+ process.exit(1);
4743
+ }
4565
4744
  let willDeployApp = false;
4566
4745
  if (!type && !skipApp) {
4567
4746
  try {
4568
- if (existsSync2(join2(projectRoot, "dist")) || existsSync2(join2(projectRoot, "src"))) {
4747
+ if (existsSync3(join3(projectRoot, "dist")) || existsSync3(join3(projectRoot, "src"))) {
4569
4748
  willDeployApp = true;
4570
4749
  }
4571
4750
  } catch {
@@ -4627,6 +4806,11 @@ async function apply(args) {
4627
4806
  totalErrors += await applyCollections(api, localCollections, appName);
4628
4807
  console.log();
4629
4808
  }
4809
+ if (localViews.length > 0) {
4810
+ if (!appName) throw new Error("View publishing requires a project context");
4811
+ console.log(pc2.bold(" Views:"));
4812
+ totalErrors += await applyViews(api, localViews, appName, projectId);
4813
+ }
4630
4814
  try {
4631
4815
  const remoteCollections = await api.listCollections();
4632
4816
  collections = buildCollectionIdMap(remoteCollections, appName);
@@ -4693,6 +4877,10 @@ async function pull(args) {
4693
4877
  const api = createApiClient(void 0, void 0, appName);
4694
4878
  const projectId = getProjectId(projectRoot);
4695
4879
  const { type, name } = parseResource(filteredArgs[0]);
4880
+ if (type === "views") {
4881
+ console.log(" Views are authored in Git. Run git pull to sync platform/views and platform/project.json.");
4882
+ return;
4883
+ }
4696
4884
  let checkedLocalSkills;
4697
4885
  if (!force) {
4698
4886
  const conflicts = [];
@@ -4814,6 +5002,7 @@ async function pull(args) {
4814
5002
  console.log();
4815
5003
  }
4816
5004
  async function destroy(args) {
5005
+ if (args[0]?.split("/")[0] === "views") throw new Error("Delete Git-owned views through project Resource shares so source and access are removed together.");
4817
5006
  if (args.includes("--help") || args.includes("-h")) {
4818
5007
  showDestroyHelp();
4819
5008
  return;
@@ -5170,6 +5359,7 @@ export {
5170
5359
  loadLocalCollections,
5171
5360
  loadLocalSkills,
5172
5361
  localProjectSkillSlug,
5362
+ lumeraFileMaxSelectErrors,
5173
5363
  plan,
5174
5364
  planAgents,
5175
5365
  planCollections,