@lumerahq/cli 0.34.5 → 0.35.1

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.
@@ -148,8 +148,26 @@ var ApiClient = class {
148
148
  }
149
149
  async listCollections() {
150
150
  const result = await this.request("/api/pb/collections");
151
+ return result.items.filter((collection) => collection.type !== "view");
152
+ }
153
+ async listViews() {
154
+ const result = await this.request("/api/pb/views");
151
155
  return result.items;
152
156
  }
157
+ async publishView(project, view, expectedHash) {
158
+ const task = await this.request(`/api/projects/${encodeURIComponent(project)}/views`, { method: "POST", body: JSON.stringify({ config: view.config, query: view.query, expected_definition_hash: expectedHash }) });
159
+ const deadline = Date.now() + 18e4;
160
+ while (Date.now() < deadline) {
161
+ const status = await this.request(task.status_url);
162
+ if (status.terminal) {
163
+ if (status.state !== "completed") throw new Error(`View publishing failed: ${JSON.stringify(status.failure)}. Task: ${task.task_id}`);
164
+ if (status.result?.value?.status === "superseded") throw new Error("View source changed before publishing. Refresh and retry.");
165
+ return;
166
+ }
167
+ await new Promise((resolve2) => setTimeout(resolve2, 1e3));
168
+ }
169
+ throw new Error(`View publishing continues in the background. Task: ${task.task_id}`);
170
+ }
153
171
  async getCollection(id) {
154
172
  return this.request(`/api/pb/collections/${id}`);
155
173
  }
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-2EHJH2QU.js";
9
9
  import {
10
10
  userContextHeaders
11
- } from "./chunk-W57NUJFJ.js";
11
+ } from "./chunk-2A2DSLIE.js";
12
12
  import {
13
13
  fetchWithRetry
14
14
  } from "./chunk-FJFIWC7G.js";
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  createApiClient,
6
6
  isApiErrorStatus
7
- } from "./chunk-W57NUJFJ.js";
7
+ } from "./chunk-2A2DSLIE.js";
8
8
  import {
9
9
  findProjectRoot,
10
10
  getAppName
@@ -16,6 +16,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from
16
16
  import { join } from "path";
17
17
  var SHARED_COLLECTIONS_DIR = "platform/collections/shared";
18
18
  var SHARED_FUNCTIONS_DIR = "platform/functions/shared";
19
+ var SHARED_VIEWS_DIR = "platform/views/shared";
19
20
  function sharedCollectionsDir(projectRoot) {
20
21
  return join(projectRoot, SHARED_COLLECTIONS_DIR);
21
22
  }
@@ -96,7 +97,7 @@ function declarationForFunctionImport(item, existing) {
96
97
  }
97
98
  async function listIncomingCollectionShares(api, projectExternalId) {
98
99
  const result = await api.listProjectResourceShares(projectExternalId, "incoming");
99
- return (result.shares ?? []).filter((share) => share.object_type === "collection").sort((a, b) => {
100
+ return (result.shares ?? []).filter((share) => share.object_type === "collection" || share.object_type === "view").sort((a, b) => {
100
101
  const bySource = a.source_project.external_id.localeCompare(b.source_project.external_id);
101
102
  if (bySource !== 0) return bySource;
102
103
  return shareCollectionName(a).localeCompare(shareCollectionName(b));
@@ -137,10 +138,12 @@ async function syncResourceShareDeps(projectRoot, api, projectExternalId, opts)
137
138
  }
138
139
  const dir = sharedCollectionsDir(projectRoot);
139
140
  const functionsDir = sharedFunctionsDir(projectRoot);
141
+ const viewsDir = join(projectRoot, SHARED_VIEWS_DIR);
140
142
  const missing = [];
141
143
  if (opts.write) {
142
144
  mkdirSync(dir, { recursive: true });
143
145
  mkdirSync(functionsDir, { recursive: true });
146
+ mkdirSync(viewsDir, { recursive: true });
144
147
  }
145
148
  for (const item of functionImports) {
146
149
  const fileName = functionImportFileName(item);
@@ -159,19 +162,20 @@ async function syncResourceShareDeps(projectRoot, api, projectExternalId, opts)
159
162
  }
160
163
  }
161
164
  for (const share of shares) {
165
+ const isView = share.object_type === "view";
162
166
  const fileName = shareFileName(share);
163
- const filePath = join(dir, fileName);
167
+ const filePath = join(isView ? viewsDir : dir, fileName);
164
168
  const exists = existsSync(filePath);
165
169
  if (!opts.write) {
166
- if (!exists) missing.push(fileName);
170
+ if (!exists) missing.push(`${isView ? "views" : "collections"}/shared/${fileName}`);
167
171
  continue;
168
172
  }
169
173
  const existing = exists ? readSharedCollectionFile(filePath) ?? void 0 : void 0;
170
- const declaration = declarationForShare(share, existing);
174
+ const declaration = isView ? { source_project: share.source_project.external_id, view: shareCollectionName(share), privilege: "view.read", reason: existing?.reason ?? "" } : declarationForShare(share, existing);
171
175
  writeFileSync(filePath, JSON.stringify(declaration, null, 2) + "\n");
172
176
  if (!opts.quiet) {
173
177
  const action = exists ? pc.yellow("\u21BB") : pc.green("+");
174
- console.log(` ${action} collections/shared/${fileName}`);
178
+ console.log(` ${action} ${isView ? "views" : "collections"}/shared/${fileName}`);
175
179
  }
176
180
  }
177
181
  if (!opts.write && missing.length > 0) {
@@ -209,7 +213,7 @@ async function deps(args) {
209
213
  if (sub === "sync") {
210
214
  console.log();
211
215
  console.log(pc.cyan(pc.bold(" Deps Sync")));
212
- console.log(pc.dim(" Syncing incoming resource shares into platform/collections/shared..."));
216
+ console.log(pc.dim(" Syncing incoming resource shares into platform/*/shared..."));
213
217
  console.log();
214
218
  const ok = await syncDeps(projectRoot, { write: true });
215
219
  if (!ok) {
@@ -225,7 +229,9 @@ async function deps(args) {
225
229
  const sharedDeps = loadSharedCollectionDeps(projectRoot);
226
230
  const functionDir = sharedFunctionsDir(projectRoot);
227
231
  const functionFiles = existsSync(functionDir) ? readdirSync(functionDir).filter((entry) => entry.endsWith(".json")).sort() : [];
228
- if (sharedDeps.length === 0 && functionFiles.length === 0) {
232
+ const viewsDir = join(projectRoot, SHARED_VIEWS_DIR);
233
+ const viewFiles = existsSync(viewsDir) ? readdirSync(viewsDir).filter((entry) => entry.endsWith(".json")).sort() : [];
234
+ if (sharedDeps.length === 0 && functionFiles.length === 0 && viewFiles.length === 0) {
229
235
  console.log(pc.dim(" No shared resource declarations in platform/*/shared"));
230
236
  return;
231
237
  }
@@ -246,13 +252,19 @@ async function deps(args) {
246
252
  ` ${dep?.source_project ?? "unknown-source"}/${dep?.function ?? "unknown-function"} ${pc.dim(`(${dep?.alias ?? "unknown-alias"})`)}`
247
253
  );
248
254
  }
255
+ for (const file of viewFiles) {
256
+ const dep = JSON.parse(readFileSync(join(viewsDir, file), "utf8"));
257
+ console.log(` ${pc.bold(file)}`);
258
+ console.log(` ${dep.source_project}/${dep.view} ${pc.dim("(view.read)")}`);
259
+ }
249
260
  console.log();
250
261
  return;
251
262
  }
252
263
  if (sub === "init") {
253
264
  mkdirSync(sharedCollectionsDir(projectRoot), { recursive: true });
254
265
  mkdirSync(sharedFunctionsDir(projectRoot), { recursive: true });
255
- console.log(pc.green(" \u2713"), "Created platform/collections/shared/ and platform/functions/shared/");
266
+ mkdirSync(join(projectRoot, SHARED_VIEWS_DIR), { recursive: true });
267
+ console.log(pc.green(" \u2713"), "Created shared collection, view, and function dependency folders");
256
268
  return;
257
269
  }
258
270
  console.log(`
@@ -263,7 +275,7 @@ async function deps(args) {
263
275
  list Show declared dependencies
264
276
  init Create dependency storage
265
277
 
266
- ${pc.bold("Dependencies:")} platform/collections/shared/*.json and platform/functions/shared/*.json
278
+ ${pc.bold("Dependencies:")} platform/{collections,views,functions}/shared/*.json
267
279
  `);
268
280
  }
269
281
 
@@ -2,9 +2,9 @@ import {
2
2
  deps,
3
3
  syncDeps,
4
4
  syncResourceShareDeps
5
- } from "./chunk-5JNNUIVJ.js";
5
+ } from "./chunk-A27HZEU6.js";
6
6
  import "./chunk-2CR762KB.js";
7
- import "./chunk-W57NUJFJ.js";
7
+ import "./chunk-2A2DSLIE.js";
8
8
  import "./chunk-JLVVHTBY.js";
9
9
  import "./chunk-FJFIWC7G.js";
10
10
  import "./chunk-PNKVD2UK.js";
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-P6GDSHYC.js";
3
+ } from "./chunk-5TQ3BQ2X.js";
4
4
  import "./chunk-UGAIWMQV.js";
5
5
  import "./chunk-2EHJH2QU.js";
6
6
  import "./chunk-XZ5VCTGY.js";
7
7
  import "./chunk-5FGDEZSJ.js";
8
8
  import {
9
9
  syncDeps
10
- } from "./chunk-5JNNUIVJ.js";
10
+ } from "./chunk-A27HZEU6.js";
11
11
  import {
12
12
  loadEnv
13
13
  } from "./chunk-2CR762KB.js";
14
14
  import {
15
15
  createApiClient
16
- } from "./chunk-W57NUJFJ.js";
16
+ } from "./chunk-2A2DSLIE.js";
17
17
  import {
18
18
  findProjectRoot,
19
19
  getApiUrl,
package/dist/index.js CHANGED
@@ -197,7 +197,8 @@ ${pc.dim("Resource Types:")}
197
197
  app-api FastAPI application backend
198
198
 
199
199
  ${pc.dim("Examples:")}
200
- lumera apply # Apply all resources
200
+ lumera apply # Apply resources and app (excludes views)
201
+ lumera apply views # Publish views (human source admin)
201
202
  lumera apply collections/users # Apply single collection
202
203
  lumera apply app # Deploy frontend and App API
203
204
  lumera apply app-api # Deploy only the App API
@@ -242,32 +243,32 @@ async function main() {
242
243
  switch (command) {
243
244
  // Resource commands
244
245
  case "plan":
245
- await import("./resources-55CTS5MT.js").then((m) => m.plan(args.slice(1)));
246
+ await import("./resources-X7K436TK.js").then((m) => m.plan(args.slice(1)));
246
247
  break;
247
248
  case "apply":
248
- await import("./resources-55CTS5MT.js").then((m) => m.apply(args.slice(1)));
249
+ await import("./resources-X7K436TK.js").then((m) => m.apply(args.slice(1)));
249
250
  break;
250
251
  case "pull":
251
- await import("./resources-55CTS5MT.js").then((m) => m.pull(args.slice(1)));
252
+ await import("./resources-X7K436TK.js").then((m) => m.pull(args.slice(1)));
252
253
  break;
253
254
  case "destroy":
254
- await import("./resources-55CTS5MT.js").then((m) => m.destroy(args.slice(1)));
255
+ await import("./resources-X7K436TK.js").then((m) => m.destroy(args.slice(1)));
255
256
  break;
256
257
  case "list":
257
- await import("./resources-55CTS5MT.js").then((m) => m.list(args.slice(1)));
258
+ await import("./resources-X7K436TK.js").then((m) => m.list(args.slice(1)));
258
259
  break;
259
260
  case "show":
260
- await import("./resources-55CTS5MT.js").then((m) => m.show(args.slice(1)));
261
+ await import("./resources-X7K436TK.js").then((m) => m.show(args.slice(1)));
261
262
  break;
262
263
  case "diff":
263
- await import("./resources-55CTS5MT.js").then((m) => m.diff(args.slice(1)));
264
+ await import("./resources-X7K436TK.js").then((m) => m.diff(args.slice(1)));
264
265
  break;
265
266
  // Development
266
267
  case "dev":
267
- await import("./dev-24PCZUAT.js").then((m) => m.dev(args.slice(1)));
268
+ await import("./dev-E2O6R7WW.js").then((m) => m.dev(args.slice(1)));
268
269
  break;
269
270
  case "run":
270
- await import("./run-4XFA5ZDJ.js").then((m) => m.run(args.slice(1)));
271
+ await import("./run-BJ7PXSEH.js").then((m) => m.run(args.slice(1)));
271
272
  break;
272
273
  case "functions":
273
274
  await import("./functions-5OOHGQT7.js").then(
@@ -281,10 +282,10 @@ async function main() {
281
282
  break;
282
283
  // Project
283
284
  case "init":
284
- await import("./init-S42P4A5J.js").then((m) => m.init(args.slice(1)));
285
+ await import("./init-MJEVA5HK.js").then((m) => m.init(args.slice(1)));
285
286
  break;
286
287
  case "register":
287
- await import("./register-GMWA4D4C.js").then((m) => m.register(args.slice(1)));
288
+ await import("./register-RUDM4ITV.js").then((m) => m.register(args.slice(1)));
288
289
  break;
289
290
  case "templates":
290
291
  await import("./templates-LNUOTNLN.js").then((m) => m.templates(subcommand, args.slice(2)));
@@ -301,7 +302,7 @@ async function main() {
301
302
  break;
302
303
  // Dependencies
303
304
  case "deps":
304
- await import("./deps-TNGT6JVD.js").then((m) => m.deps(args.slice(1)));
305
+ await import("./deps-DLJUDFNK.js").then((m) => m.deps(args.slice(1)));
305
306
  break;
306
307
  // Feature flags (read the in-sandbox snapshot)
307
308
  case "flags":
@@ -12,7 +12,7 @@ import {
12
12
  import {
13
13
  createApiClient,
14
14
  validateProjectNameLength
15
- } from "./chunk-W57NUJFJ.js";
15
+ } from "./chunk-2A2DSLIE.js";
16
16
  import {
17
17
  getToken,
18
18
  init_auth,
@@ -7,7 +7,7 @@ import {
7
7
  import {
8
8
  createApiClient,
9
9
  validateProjectExternalId
10
- } from "./chunk-W57NUJFJ.js";
10
+ } from "./chunk-2A2DSLIE.js";
11
11
  import {
12
12
  findProjectRoot,
13
13
  getAppName,
@@ -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) {
@@ -966,59 +1049,59 @@ function buildAutomationTargets(localAutomations) {
966
1049
  source: a.code,
967
1050
  config: a.automation.inputs?.schema,
968
1051
  // a.filePath is main.py; config.json is its sibling.
969
- configFilePath: join(dirname(a.filePath), "config.json")
1052
+ configFilePath: join2(dirname(a.filePath), "config.json")
970
1053
  }));
971
1054
  }
972
1055
  function buildCollectionTargets(platformDir, _filterName) {
973
- const collectionsDir = join(platformDir, "collections");
974
- if (!existsSync(collectionsDir)) return [];
1056
+ const collectionsDir = join2(platformDir, "collections");
1057
+ if (!existsSync2(collectionsDir)) return [];
975
1058
  const targets = [];
976
- for (const entry of readdirSync(collectionsDir, { withFileTypes: true })) {
1059
+ for (const entry of readdirSync2(collectionsDir, { withFileTypes: true })) {
977
1060
  if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
978
- const filePath = join(collectionsDir, entry.name);
1061
+ const filePath = join2(collectionsDir, entry.name);
979
1062
  targets.push({
980
1063
  kind: "collection",
981
1064
  name: basename(entry.name, ".json"),
982
1065
  filePath,
983
- source: readFileSync(filePath, "utf-8")
1066
+ source: readFileSync2(filePath, "utf-8")
984
1067
  });
985
1068
  }
986
1069
  return targets;
987
1070
  }
988
1071
  function buildHookTargets(platformDir, filterName) {
989
- const hooksDir = join(platformDir, "hooks");
990
- if (!existsSync(hooksDir)) return [];
1072
+ const hooksDir = join2(platformDir, "hooks");
1073
+ if (!existsSync2(hooksDir)) return [];
991
1074
  const targets = [];
992
- for (const entry of readdirSync(hooksDir, { withFileTypes: true })) {
1075
+ for (const entry of readdirSync2(hooksDir, { withFileTypes: true })) {
993
1076
  if (!entry.isFile()) continue;
994
1077
  if (!entry.name.endsWith(".js") && !entry.name.endsWith(".ts")) continue;
995
1078
  const base = entry.name.replace(/\.(js|ts)$/, "");
996
1079
  if (filterName && base !== filterName) continue;
997
- const filePath = join(hooksDir, entry.name);
1080
+ const filePath = join2(hooksDir, entry.name);
998
1081
  targets.push({
999
1082
  kind: "hook",
1000
1083
  name: base,
1001
1084
  filePath,
1002
- source: readFileSync(filePath, "utf-8")
1085
+ source: readFileSync2(filePath, "utf-8")
1003
1086
  });
1004
1087
  }
1005
1088
  return targets;
1006
1089
  }
1007
1090
  function resolveAgentLintFiles(platformDir, filterName, appName) {
1008
- const agentsDir = join(platformDir, "agents");
1009
- if (!existsSync(agentsDir)) return [];
1091
+ const agentsDir = join2(platformDir, "agents");
1092
+ if (!existsSync2(agentsDir)) return [];
1010
1093
  const agents = [];
1011
- for (const entry of readdirSync(agentsDir, { withFileTypes: true })) {
1094
+ for (const entry of readdirSync2(agentsDir, { withFileTypes: true })) {
1012
1095
  if (!entry.isDirectory()) continue;
1013
- const directoryPath = join(agentsDir, entry.name);
1014
- const configPath = join(directoryPath, "config.json");
1096
+ const directoryPath = join2(agentsDir, entry.name);
1097
+ const configPath = join2(directoryPath, "config.json");
1015
1098
  let configSource;
1016
1099
  let config;
1017
1100
  let externalID = appName ? `${appName}:${entry.name}` : "";
1018
1101
  let displayName = "";
1019
1102
  let policyEnabled = false;
1020
- if (existsSync(configPath)) {
1021
- configSource = readFileSync(configPath, "utf-8");
1103
+ if (existsSync2(configPath)) {
1104
+ configSource = readFileSync2(configPath, "utf-8");
1022
1105
  try {
1023
1106
  config = JSON.parse(configSource);
1024
1107
  if (typeof config === "object" && config !== null && !Array.isArray(config)) {
@@ -1065,14 +1148,14 @@ function agentConfigTargets(agents) {
1065
1148
  function agentPolicyTargets(agents) {
1066
1149
  const targets = [];
1067
1150
  for (const agent of agents) {
1068
- const policyPath = join(agent.directoryPath, "policy.js");
1069
- const hasPolicyFile = existsSync(policyPath);
1151
+ const policyPath = join2(agent.directoryPath, "policy.js");
1152
+ const hasPolicyFile = existsSync2(policyPath);
1070
1153
  if (!hasPolicyFile && !agent.policyEnabled) continue;
1071
1154
  targets.push({
1072
1155
  kind: "agent-policy",
1073
1156
  name: agent.name,
1074
1157
  filePath: policyPath,
1075
- source: hasPolicyFile ? readFileSync(policyPath, "utf-8") : "",
1158
+ source: hasPolicyFile ? readFileSync2(policyPath, "utf-8") : "",
1076
1159
  config: { policyEnabled: agent.policyEnabled }
1077
1160
  });
1078
1161
  }
@@ -1179,7 +1262,7 @@ function parsePackageManagerSpecifier(value) {
1179
1262
  }
1180
1263
  function packageManagerFromPackageJson(projectRoot) {
1181
1264
  try {
1182
- const pkg = JSON.parse(readFileSync2(join2(projectRoot, "package.json"), "utf-8"));
1265
+ const pkg = JSON.parse(readFileSync3(join3(projectRoot, "package.json"), "utf-8"));
1183
1266
  return parsePackageManagerSpecifier(pkg.packageManager);
1184
1267
  } catch {
1185
1268
  return void 0;
@@ -1230,7 +1313,7 @@ function detectPackageManager(projectRoot, override) {
1230
1313
  { pm: "npm", file: "package-lock.json" }
1231
1314
  ];
1232
1315
  for (const { pm, file } of lockfileManagers) {
1233
- if (existsSync2(join2(projectRoot, file)) && commandAvailable(pm)) return pm;
1316
+ if (existsSync3(join3(projectRoot, file)) && commandAvailable(pm)) return pm;
1234
1317
  }
1235
1318
  for (const pm of ["pnpm", "bun", "yarn", "npm"]) {
1236
1319
  if (commandAvailable(pm)) return pm;
@@ -1523,6 +1606,8 @@ ${pc2.dim("Resources:")}
1523
1606
  (none) Plan all resources
1524
1607
  collections Plan only collections
1525
1608
  collections/<name> Plan single collection
1609
+ views Plan Git-owned SQL views
1610
+ views/<name> Plan a single view
1526
1611
  automations Plan only automations
1527
1612
  automations/<name> Plan single automation
1528
1613
  hooks Plan only hooks
@@ -1550,9 +1635,11 @@ ${pc2.dim("Description:")}
1550
1635
  Create or update resources from local files.
1551
1636
 
1552
1637
  ${pc2.dim("Resources:")}
1553
- (none) Apply all resources
1638
+ (none) Apply resources and app; views require an explicit target
1554
1639
  collections Apply only collections
1555
1640
  collections/<name> Apply single collection
1641
+ views Publish Git-owned views (human source admin)
1642
+ views/<name> Publish a single view
1556
1643
  automations Apply only automations
1557
1644
  automations/<name> Apply single automation
1558
1645
  hooks Apply only hooks
@@ -1574,7 +1661,8 @@ ${pc2.dim("Options:")}
1574
1661
  --package-manager Package manager for app builds (pnpm, bun, yarn, npm)
1575
1662
 
1576
1663
  ${pc2.dim("Examples:")}
1577
- lumera apply # Apply everything (shows plan, asks to confirm)
1664
+ lumera apply # Apply resources and app (excludes views)
1665
+ lumera apply views # Publish views with human-admin credentials
1578
1666
  lumera apply collections # Apply all collections
1579
1667
  lumera apply collections/users # Apply single collection
1580
1668
  lumera apply agents -y # Apply agents without confirmation
@@ -1593,6 +1681,7 @@ ${pc2.dim("Usage:")}
1593
1681
 
1594
1682
  ${pc2.dim("Description:")}
1595
1683
  Download remote state to local files.
1684
+ Views remain Git-owned. Use git pull to sync their source files.
1596
1685
  Refuses to overwrite local files that have uncommitted changes
1597
1686
  (use --force to override, or 'lumera diff' to inspect first).
1598
1687
 
@@ -1710,7 +1799,7 @@ function parseResource(resourcePath) {
1710
1799
  const parts = resourcePath.split("/");
1711
1800
  const type = parts[0];
1712
1801
  const name = parts.slice(1).join("/") || null;
1713
- const validTypes = ["collections", "automations", "hooks", "agents", "skills", "mailboxes", "app"];
1802
+ const validTypes = ["collections", "automations", "hooks", "agents", "skills", "mailboxes", "app", "views"];
1714
1803
  if (!validTypes.includes(type)) {
1715
1804
  console.log(pc2.red(` Unknown resource type "${type}". Valid types: ${validTypes.join(", ")}`));
1716
1805
  process.exit(1);
@@ -1718,29 +1807,29 @@ function parseResource(resourcePath) {
1718
1807
  return { type, name };
1719
1808
  }
1720
1809
  function getPlatformDir() {
1721
- if (existsSync2(join2(process.cwd(), "platform"))) {
1722
- return join2(process.cwd(), "platform");
1810
+ if (existsSync3(join3(process.cwd(), "platform"))) {
1811
+ return join3(process.cwd(), "platform");
1723
1812
  }
1724
- if (existsSync2(join2(process.cwd(), "lumera_platform"))) {
1725
- return join2(process.cwd(), "lumera_platform");
1813
+ if (existsSync3(join3(process.cwd(), "lumera_platform"))) {
1814
+ return join3(process.cwd(), "lumera_platform");
1726
1815
  }
1727
- return join2(process.cwd(), "platform");
1816
+ return join3(process.cwd(), "platform");
1728
1817
  }
1729
1818
  function toSafeFilename(name) {
1730
1819
  return name.replace(/\s+/g, "_").replace(/[^a-zA-Z0-9_-]/g, "").toLowerCase();
1731
1820
  }
1732
1821
  function loadLocalCollections(platformDir, filterName) {
1733
- const collectionsDir = join2(platformDir, "collections");
1734
- if (!existsSync2(collectionsDir)) {
1822
+ const collectionsDir = join3(platformDir, "collections");
1823
+ if (!existsSync3(collectionsDir)) {
1735
1824
  return [];
1736
1825
  }
1737
1826
  const collections = [];
1738
1827
  const errors = [];
1739
- for (const file of readdirSync2(collectionsDir)) {
1828
+ for (const file of readdirSync3(collectionsDir)) {
1740
1829
  if (!file.endsWith(".json")) continue;
1741
- const filePath = join2(collectionsDir, file);
1830
+ const filePath = join3(collectionsDir, file);
1742
1831
  try {
1743
- const content = readFileSync2(filePath, "utf-8");
1832
+ const content = readFileSync3(filePath, "utf-8");
1744
1833
  const collection = JSON.parse(content);
1745
1834
  if (filterName && collection.name !== filterName && collection.id !== filterName) {
1746
1835
  continue;
@@ -1816,27 +1905,27 @@ function loadLocalCollections(platformDir, filterName) {
1816
1905
  return collections;
1817
1906
  }
1818
1907
  function loadLocalAutomations(platformDir, filterName, appName) {
1819
- const automationsDir = join2(platformDir, "automations");
1820
- if (!existsSync2(automationsDir)) {
1908
+ const automationsDir = join3(platformDir, "automations");
1909
+ if (!existsSync3(automationsDir)) {
1821
1910
  return [];
1822
1911
  }
1823
1912
  const automations = [];
1824
1913
  const errors = [];
1825
- for (const entry of readdirSync2(automationsDir, { withFileTypes: true })) {
1914
+ for (const entry of readdirSync3(automationsDir, { withFileTypes: true })) {
1826
1915
  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)) {
1916
+ const automationDir = join3(automationsDir, entry.name);
1917
+ const configPath = join3(automationDir, "config.json");
1918
+ const mainPath = join3(automationDir, "main.py");
1919
+ if (!existsSync3(configPath)) {
1831
1920
  errors.push(`${entry.name}: missing config.json`);
1832
1921
  continue;
1833
1922
  }
1834
- if (!existsSync2(mainPath)) {
1923
+ if (!existsSync3(mainPath)) {
1835
1924
  errors.push(`${entry.name}: missing main.py`);
1836
1925
  continue;
1837
1926
  }
1838
1927
  try {
1839
- const configContent = readFileSync2(configPath, "utf-8");
1928
+ const configContent = readFileSync3(configPath, "utf-8");
1840
1929
  const rawConfig = JSON.parse(configContent);
1841
1930
  if (filterName && rawConfig.external_id !== filterName && rawConfig.name !== filterName && entry.name !== filterName) {
1842
1931
  continue;
@@ -1863,7 +1952,7 @@ function loadLocalAutomations(platformDir, filterName, appName) {
1863
1952
  errors.push(`${entry.name}: duplicate external_id "${config.external_id}" (also defined in ${existingByExtId.automation.name})`);
1864
1953
  continue;
1865
1954
  }
1866
- let code = readFileSync2(mainPath, "utf-8");
1955
+ let code = readFileSync3(mainPath, "utf-8");
1867
1956
  if (appName) {
1868
1957
  code = code.replaceAll("{{app}}", appName);
1869
1958
  }
@@ -1882,15 +1971,15 @@ function loadLocalAutomations(platformDir, filterName, appName) {
1882
1971
  return automations;
1883
1972
  }
1884
1973
  function loadLocalHooks(platformDir, filterName, appName) {
1885
- const hooksDir = join2(platformDir, "hooks");
1886
- if (!existsSync2(hooksDir)) {
1974
+ const hooksDir = join3(platformDir, "hooks");
1975
+ if (!existsSync3(hooksDir)) {
1887
1976
  return [];
1888
1977
  }
1889
1978
  const hooks = [];
1890
- for (const file of readdirSync2(hooksDir)) {
1979
+ for (const file of readdirSync3(hooksDir)) {
1891
1980
  if (!file.endsWith(".js") && !file.endsWith(".ts")) continue;
1892
- const filePath = join2(hooksDir, file);
1893
- const content = readFileSync2(filePath, "utf-8");
1981
+ const filePath = join3(hooksDir, file);
1982
+ const content = readFileSync3(filePath, "utf-8");
1894
1983
  const config = parseHookConfig(content);
1895
1984
  if (!config) {
1896
1985
  console.log(pc2.yellow(` \u26A0 Skipping ${file}: could not parse config export`));
@@ -2221,12 +2310,12 @@ async function planAutomations(api, localAutomations, projectId) {
2221
2310
  return changes;
2222
2311
  }
2223
2312
  function loadLocalMailboxes(platformDir, filterName) {
2224
- const mailboxesDir = join2(platformDir, "mailboxes");
2225
- if (!existsSync2(mailboxesDir)) {
2313
+ const mailboxesDir = join3(platformDir, "mailboxes");
2314
+ if (!existsSync3(mailboxesDir)) {
2226
2315
  return [];
2227
2316
  }
2228
2317
  const mailboxes = [];
2229
- for (const file of readdirSync2(mailboxesDir)) {
2318
+ for (const file of readdirSync3(mailboxesDir)) {
2230
2319
  if (!file.endsWith(".json")) continue;
2231
2320
  const slug = file.replace(/\.json$/, "").trim();
2232
2321
  if (!slug) {
@@ -2236,10 +2325,10 @@ function loadLocalMailboxes(platformDir, filterName) {
2236
2325
  if (filterName && slug !== filterName) {
2237
2326
  continue;
2238
2327
  }
2239
- const filePath = join2(mailboxesDir, file);
2328
+ const filePath = join3(mailboxesDir, file);
2240
2329
  let raw;
2241
2330
  try {
2242
- raw = JSON.parse(readFileSync2(filePath, "utf-8"));
2331
+ raw = JSON.parse(readFileSync3(filePath, "utf-8"));
2243
2332
  } catch (e) {
2244
2333
  console.log(pc2.yellow(` \u26A0 Skipping ${file}: invalid JSON (${e.message})`));
2245
2334
  continue;
@@ -2318,8 +2407,8 @@ async function pullMailboxes(api, platformDir, filterName) {
2318
2407
  console.log(pc2.dim(" (no mailboxes)"));
2319
2408
  return;
2320
2409
  }
2321
- const outDir = join2(platformDir, "mailboxes");
2322
- if (!existsSync2(outDir)) {
2410
+ const outDir = join3(platformDir, "mailboxes");
2411
+ if (!existsSync3(outDir)) {
2323
2412
  mkdirSync(outDir, { recursive: true });
2324
2413
  }
2325
2414
  let count = 0;
@@ -2327,7 +2416,7 @@ async function pullMailboxes(api, platformDir, filterName) {
2327
2416
  if (filterName && mb.slug !== filterName) continue;
2328
2417
  const body = {};
2329
2418
  if (mb.description) body.description = mb.description;
2330
- const file = join2(outDir, `${toSafeFilename(mb.slug)}.json`);
2419
+ const file = join3(outDir, `${toSafeFilename(mb.slug)}.json`);
2331
2420
  writeFileSync(file, JSON.stringify(body, null, 2) + "\n");
2332
2421
  console.log(pc2.green(" \u2713"), `pulled mailbox ${mb.slug} ${pc2.dim(`\u2192 ${file}`)}`);
2333
2422
  count++;
@@ -2634,7 +2723,7 @@ async function applyAppAPI(projectRoot) {
2634
2723
  });
2635
2724
  }
2636
2725
  async function pullCollections(api, platformDir, filterName, projectId) {
2637
- const collectionsDir = join2(platformDir, "collections");
2726
+ const collectionsDir = join3(platformDir, "collections");
2638
2727
  mkdirSync(collectionsDir, { recursive: true });
2639
2728
  const collections = await api.listCollections();
2640
2729
  for (const collection of collections) {
@@ -2660,13 +2749,13 @@ async function pullCollections(api, platformDir, filterName, projectId) {
2660
2749
  search: collection.search
2661
2750
  };
2662
2751
  const fileName = toSafeFilename(localName);
2663
- const filePath = join2(collectionsDir, `${fileName}.json`);
2752
+ const filePath = join3(collectionsDir, `${fileName}.json`);
2664
2753
  writeFileSync(filePath, JSON.stringify(localFormat, null, 2) + "\n");
2665
2754
  console.log(pc2.green(" \u2713"), `${localName} \u2192 collections/${fileName}.json`);
2666
2755
  }
2667
2756
  }
2668
2757
  async function pullAutomations(api, platformDir, filterName, projectId) {
2669
- const automationsDir = join2(platformDir, "automations");
2758
+ const automationsDir = join3(platformDir, "automations");
2670
2759
  mkdirSync(automationsDir, { recursive: true });
2671
2760
  const automations = await api.listAutomations({
2672
2761
  include_code: true,
@@ -2680,7 +2769,7 @@ async function pullAutomations(api, platformDir, filterName, projectId) {
2680
2769
  continue;
2681
2770
  }
2682
2771
  const dirName = automation.external_id.replace(/[^a-zA-Z0-9_-]/g, "_");
2683
- const automationDir = join2(automationsDir, dirName);
2772
+ const automationDir = join3(automationsDir, dirName);
2684
2773
  mkdirSync(automationDir, { recursive: true });
2685
2774
  const config = {
2686
2775
  external_id: automation.external_id,
@@ -2713,13 +2802,13 @@ async function pullAutomations(api, platformDir, filterName, projectId) {
2713
2802
  }
2714
2803
  } catch {
2715
2804
  }
2716
- writeFileSync(join2(automationDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
2717
- writeFileSync(join2(automationDir, "main.py"), automation.code || "");
2805
+ writeFileSync(join3(automationDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
2806
+ writeFileSync(join3(automationDir, "main.py"), automation.code || "");
2718
2807
  console.log(pc2.green(" \u2713"), `${automation.name} \u2192 automations/${dirName}/`);
2719
2808
  }
2720
2809
  }
2721
2810
  async function pullHooks(api, platformDir, filterName, appName, projectId) {
2722
- const hooksDir = join2(platformDir, "hooks");
2811
+ const hooksDir = join3(platformDir, "hooks");
2723
2812
  mkdirSync(hooksDir, { recursive: true });
2724
2813
  const hooks = await api.listHooks(projectId ? { project_id: projectId } : void 0);
2725
2814
  for (const hook of hooks) {
@@ -2741,7 +2830,7 @@ export default async function handler({ record, app, http }) {
2741
2830
  ${hook.script.split("\n").map((line) => " " + line).join("\n")}
2742
2831
  }
2743
2832
  `;
2744
- writeFileSync(join2(hooksDir, fileName), content);
2833
+ writeFileSync(join3(hooksDir, fileName), content);
2745
2834
  console.log(pc2.green(" \u2713"), `${hook.name} \u2192 hooks/${fileName}`);
2746
2835
  }
2747
2836
  }
@@ -2804,7 +2893,7 @@ function localSkillResources(skillDir) {
2804
2893
  let fileCount = 0;
2805
2894
  const decoder = new TextDecoder("utf-8", { fatal: true });
2806
2895
  const scan = (directory, parentParts) => {
2807
- const names = readdirSync2(directory, { encoding: "buffer" }).map((rawName) => {
2896
+ const names = readdirSync3(directory, { encoding: "buffer" }).map((rawName) => {
2808
2897
  try {
2809
2898
  return decoder.decode(rawName);
2810
2899
  } catch {
@@ -2816,7 +2905,7 @@ function localSkillResources(skillDir) {
2816
2905
  if (parentParts.length === 0 && name === "SKILL.md") continue;
2817
2906
  const parts = [...parentParts, name];
2818
2907
  const resourcePath = parts.join("/");
2819
- const filePath = join2(directory, name);
2908
+ const filePath = join3(directory, name);
2820
2909
  const stat = lstatSync(filePath);
2821
2910
  if (stat.isSymbolicLink()) {
2822
2911
  errors.push(`${resourcePath}: symlinks are not supported`);
@@ -2846,7 +2935,7 @@ function localSkillResources(skillDir) {
2846
2935
  errors.push(`${resourcePath}: path conflicts ignoring letter case with ${conflict.path}`);
2847
2936
  continue;
2848
2937
  }
2849
- const sha256 = createHash("sha256").update(readFileSync2(filePath)).digest("hex");
2938
+ const sha256 = createHash2("sha256").update(readFileSync3(filePath)).digest("hex");
2850
2939
  resources.push({ path: resourcePath, filePath, size: stat.size, sha256 });
2851
2940
  }
2852
2941
  };
@@ -2877,20 +2966,20 @@ function skillResourceChangesSummary(changes) {
2877
2966
  ].join(", ");
2878
2967
  }
2879
2968
  function loadLocalSkills(platformDir, filterName) {
2880
- const skillsDir = join2(platformDir, "skills");
2881
- if (!existsSync2(skillsDir)) return [];
2969
+ const skillsDir = join3(platformDir, "skills");
2970
+ if (!existsSync3(skillsDir)) return [];
2882
2971
  const skills = [];
2883
2972
  const errors = [];
2884
- for (const entry of readdirSync2(skillsDir, { withFileTypes: true })) {
2973
+ for (const entry of readdirSync3(skillsDir, { withFileTypes: true })) {
2885
2974
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
2886
2975
  if (filterName && entry.name !== filterName) continue;
2887
2976
  if (!SKILL_SLUG_RE.test(entry.name)) {
2888
2977
  errors.push(`${entry.name}: directory name must be a lowercase slug`);
2889
2978
  continue;
2890
2979
  }
2891
- const skillDir = join2(skillsDir, entry.name);
2892
- const path = join2(skillDir, "SKILL.md");
2893
- if (!existsSync2(path)) {
2980
+ const skillDir = join3(skillsDir, entry.name);
2981
+ const path = join3(skillDir, "SKILL.md");
2982
+ if (!existsSync3(path)) {
2894
2983
  errors.push(`${entry.name}: missing SKILL.md`);
2895
2984
  continue;
2896
2985
  }
@@ -2901,7 +2990,7 @@ function loadLocalSkills(platformDir, filterName) {
2901
2990
  }
2902
2991
  const { resources, errors: resourceErrors } = localSkillResources(skillDir);
2903
2992
  errors.push(...resourceErrors.map((error3) => `${entry.name}: ${error3}`));
2904
- const content = readFileSync2(path, "utf-8");
2993
+ const content = readFileSync3(path, "utf-8");
2905
2994
  const separator = /\r?\n---\r?\n/.exec(content);
2906
2995
  const parts = separator ? [content.slice(0, separator.index), content.slice(separator.index + separator[0].length)] : [content];
2907
2996
  if (parts.length !== 2) {
@@ -3079,8 +3168,8 @@ async function planSkills(api, localSkills, projectId, appName) {
3079
3168
  return changes;
3080
3169
  }
3081
3170
  async function uploadSkillResource(api, skillID, resource) {
3082
- const bytes = readFileSync2(resource.filePath);
3083
- const sha256 = createHash("sha256").update(bytes).digest("hex");
3171
+ const bytes = readFileSync3(resource.filePath);
3172
+ const sha256 = createHash2("sha256").update(bytes).digest("hex");
3084
3173
  if (bytes.byteLength !== resource.size || sha256 !== resource.sha256) {
3085
3174
  throw new Error(`${resource.path} changed while apply was running`);
3086
3175
  }
@@ -3180,10 +3269,10 @@ function validateRemoteSkillResources(resources) {
3180
3269
  }
3181
3270
  function writeVerifiedSkillResource(resource, bytes, skillDir, source) {
3182
3271
  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) {
3272
+ if (createHash2("sha256").update(bytes).digest("hex") !== resource.sha256) {
3184
3273
  throw new Error(`${resource.path}: ${source} checksum does not match`);
3185
3274
  }
3186
- const destination = join2(skillDir, ...resource.path.split("/"));
3275
+ const destination = join3(skillDir, ...resource.path.split("/"));
3187
3276
  mkdirSync(dirname2(destination), { recursive: true });
3188
3277
  writeFileSync(destination, bytes);
3189
3278
  }
@@ -3200,12 +3289,12 @@ async function downloadSkillResource(resource, skillDir) {
3200
3289
  }
3201
3290
  function replaceSkillDirectory(stagingDir, skillDir) {
3202
3291
  const backupDir = `${stagingDir}.previous`;
3203
- const hadExisting = existsSync2(skillDir);
3292
+ const hadExisting = existsSync3(skillDir);
3204
3293
  if (hadExisting) renameSync(skillDir, backupDir);
3205
3294
  try {
3206
3295
  renameSync(stagingDir, skillDir);
3207
3296
  } catch (error3) {
3208
- if (hadExisting && !existsSync2(skillDir)) renameSync(backupDir, skillDir);
3297
+ if (hadExisting && !existsSync3(skillDir)) renameSync(backupDir, skillDir);
3209
3298
  throw error3;
3210
3299
  }
3211
3300
  if (hadExisting) rmSync(backupDir, { recursive: true, force: true });
@@ -3216,7 +3305,7 @@ async function pullSkills(api, platformDir, filterName, projectId, appName, chec
3216
3305
  return;
3217
3306
  }
3218
3307
  const scopedProjectID = requireProjectSkillsProjectID(projectId);
3219
- const skillsDir = join2(platformDir, "skills");
3308
+ const skillsDir = join3(platformDir, "skills");
3220
3309
  const remoteSkills = remoteSkillsByLocalSlug(
3221
3310
  await api.listAgentSkills({ project_id: scopedProjectID }),
3222
3311
  appName
@@ -3250,11 +3339,11 @@ async function pullSkills(api, platformDir, filterName, projectId, appName, chec
3250
3339
  const resources = [...skill.resources ?? []].sort((a, b) => a.path.localeCompare(b.path));
3251
3340
  validateRemoteSkillResources(resources);
3252
3341
  const localResources = new Map(local?.resources.map((resource) => [resource.path, resource]) ?? []);
3253
- const skillDir = join2(skillsDir, localSlug);
3342
+ const skillDir = join3(skillsDir, localSlug);
3254
3343
  mkdirSync(skillsDir, { recursive: true });
3255
- const stagingDir = mkdtempSync(join2(platformDir, `.skill-${localSlug}-pull-`));
3344
+ const stagingDir = mkdtempSync(join3(platformDir, `.skill-${localSlug}-pull-`));
3256
3345
  try {
3257
- writeFileSync(join2(stagingDir, "SKILL.md"), skillMarkdown({
3346
+ writeFileSync(join3(stagingDir, "SKILL.md"), skillMarkdown({
3258
3347
  name: skill.name,
3259
3348
  summary: skill.summary || "",
3260
3349
  instructions: skill.instructions || ""
@@ -3262,7 +3351,7 @@ async function pullSkills(api, platformDir, filterName, projectId, appName, chec
3262
3351
  for (const resource of resources) {
3263
3352
  const localResource = localResources.get(resource.path);
3264
3353
  if (localResource?.size === resource.size && localResource.sha256 === resource.sha256) {
3265
- writeVerifiedSkillResource(resource, readFileSync2(localResource.filePath), stagingDir, "local");
3354
+ writeVerifiedSkillResource(resource, readFileSync3(localResource.filePath), stagingDir, "local");
3266
3355
  } else {
3267
3356
  await downloadSkillResource(resource, stagingDir);
3268
3357
  }
@@ -3275,26 +3364,26 @@ async function pullSkills(api, platformDir, filterName, projectId, appName, chec
3275
3364
  }
3276
3365
  }
3277
3366
  function loadLocalAgents(platformDir, filterName, appName) {
3278
- const agentsDir = join2(platformDir, "agents");
3279
- if (!existsSync2(agentsDir)) return [];
3367
+ const agentsDir = join3(platformDir, "agents");
3368
+ if (!existsSync3(agentsDir)) return [];
3280
3369
  const agents = [];
3281
3370
  const errors = [];
3282
- for (const entry of readdirSync2(agentsDir, { withFileTypes: true })) {
3371
+ for (const entry of readdirSync3(agentsDir, { withFileTypes: true })) {
3283
3372
  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)) {
3373
+ const agentDir = join3(agentsDir, entry.name);
3374
+ const configPath = join3(agentDir, "config.json");
3375
+ const promptPath = join3(agentDir, "system_prompt.md");
3376
+ const policyPath = join3(agentDir, "policy.js");
3377
+ if (!existsSync3(configPath)) {
3289
3378
  errors.push(`${entry.name}: missing config.json`);
3290
3379
  continue;
3291
3380
  }
3292
- if (!existsSync2(promptPath)) {
3381
+ if (!existsSync3(promptPath)) {
3293
3382
  errors.push(`${entry.name}: missing system_prompt.md`);
3294
3383
  continue;
3295
3384
  }
3296
3385
  try {
3297
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
3386
+ const config = JSON.parse(readFileSync3(configPath, "utf-8"));
3298
3387
  if (filterName && config.external_id !== filterName && config.name !== filterName && entry.name !== filterName) {
3299
3388
  continue;
3300
3389
  }
@@ -3324,8 +3413,8 @@ function loadLocalAgents(platformDir, filterName, appName) {
3324
3413
  continue;
3325
3414
  }
3326
3415
  }
3327
- let systemPrompt = readFileSync2(promptPath, "utf-8");
3328
- let policyScript = existsSync2(policyPath) ? readFileSync2(policyPath, "utf-8") : "";
3416
+ let systemPrompt = readFileSync3(promptPath, "utf-8");
3417
+ let policyScript = existsSync3(policyPath) ? readFileSync3(policyPath, "utf-8") : "";
3329
3418
  if (appName) {
3330
3419
  systemPrompt = systemPrompt.replaceAll("{{app}}", appName);
3331
3420
  policyScript = policyScript.replaceAll("{{app}}", appName);
@@ -3528,7 +3617,7 @@ function allowedFunctionsChanged(remote, local) {
3528
3617
  return JSON.stringify([...remote ?? []].sort()) !== JSON.stringify([...local].sort());
3529
3618
  }
3530
3619
  async function pullAgents(api, platformDir, filterName, projectId, appName) {
3531
- const agentsDir = join2(platformDir, "agents");
3620
+ const agentsDir = join3(platformDir, "agents");
3532
3621
  mkdirSync(agentsDir, { recursive: true });
3533
3622
  const agents = await api.listAgents(projectId ? { project_id: projectId } : void 0);
3534
3623
  const skillIdToSlug = /* @__PURE__ */ new Map();
@@ -3550,7 +3639,7 @@ async function pullAgents(api, platformDir, filterName, projectId, appName) {
3550
3639
  continue;
3551
3640
  }
3552
3641
  const dirName = agent.external_id.replace(/[^a-zA-Z0-9_-]/g, "_");
3553
- const agentDir = join2(agentsDir, dirName);
3642
+ const agentDir = join3(agentsDir, dirName);
3554
3643
  mkdirSync(agentDir, { recursive: true });
3555
3644
  const skillSlugs = [];
3556
3645
  if (agent.skill_ids) {
@@ -3570,16 +3659,27 @@ async function pullAgents(api, platformDir, filterName, projectId, appName) {
3570
3659
  if (agent.idle_reap_timeout_seconds) config.idle_reap_timeout_seconds = agent.idle_reap_timeout_seconds;
3571
3660
  if (skillSlugs.length > 0) config.skills = skillSlugs;
3572
3661
  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 || "");
3662
+ writeFileSync(join3(agentDir, "config.json"), JSON.stringify(config, null, 2) + "\n");
3663
+ writeFileSync(join3(agentDir, "system_prompt.md"), agent.system_prompt || "");
3575
3664
  if (agent.policy_script) {
3576
- writeFileSync(join2(agentDir, "policy.js"), agent.policy_script);
3665
+ writeFileSync(join3(agentDir, "policy.js"), agent.policy_script);
3577
3666
  }
3578
3667
  console.log(pc2.green(" \u2713"), `${agent.name} \u2192 agents/${dirName}/`);
3579
3668
  }
3580
3669
  }
3581
3670
  async function listResources(api, platformDir, filterType, appName, projectId) {
3582
3671
  const results = [];
3672
+ if (!filterType || filterType === "views") {
3673
+ const local = loadLocalViews(platformDir);
3674
+ const remote = (await api.listViews()).filter((view) => !projectId || view.project_id === projectId);
3675
+ for (const view of local) {
3676
+ const deployed = remote.find((candidate) => candidate.name === view.config.name);
3677
+ let status = "local-only";
3678
+ if (deployed) status = deployed.definition_hash === viewDefinitionHash(view) ? "synced" : "changed";
3679
+ results.push({ name: view.config.name, type: "views", status });
3680
+ }
3681
+ for (const view of remote) if (!local.some((candidate) => candidate.config.name === view.name)) results.push({ name: view.name, type: "views", status: "remote-only" });
3682
+ }
3583
3683
  if (!filterType || filterType === "collections") {
3584
3684
  const localCollections = loadLocalCollections(platformDir);
3585
3685
  const remoteCollections = await api.listCollections();
@@ -4067,6 +4167,13 @@ async function destroyApp(skipConfirm) {
4067
4167
  console.log(pc2.green(" \u2713"), `App "${appRecord.name}" deleted from Lumera.`);
4068
4168
  }
4069
4169
  async function showResource(api, platformDir, resourceType, resourceName, appName, projectId) {
4170
+ if (resourceType === "views") {
4171
+ const local = loadLocalViews(platformDir, resourceName)[0];
4172
+ const remote = (await api.listViews()).find((view) => view.name === resourceName && (!projectId || view.project_id === projectId));
4173
+ if (!local && !remote) throw new Error(`View ${resourceName} not found`);
4174
+ console.log(JSON.stringify({ local, deployed: remote }, null, 2));
4175
+ return;
4176
+ }
4070
4177
  if (resourceType === "collections") {
4071
4178
  const localCollections = loadLocalCollections(platformDir, resourceName);
4072
4179
  const remoteCollections = await api.listCollections();
@@ -4349,6 +4456,10 @@ async function plan(args) {
4349
4456
  allChanges.push(...changes);
4350
4457
  }
4351
4458
  }
4459
+ if (!type || type === "views") {
4460
+ const views = loadLocalViews(platformDir, name || void 0);
4461
+ if (views.length) allChanges.push(...await planViews(api, views, projectId));
4462
+ }
4352
4463
  let localAutomations = [];
4353
4464
  if (!type || type === "automations") {
4354
4465
  localAutomations = loadLocalAutomations(platformDir, name || void 0, appName);
@@ -4530,6 +4641,11 @@ async function apply(args) {
4530
4641
  const allChanges = [];
4531
4642
  let planValidationErrors = 0;
4532
4643
  const localCollections = !type || type === "collections" ? loadLocalCollections(platformDir, name || void 0) : [];
4644
+ const localViews = type === "views" ? loadLocalViews(platformDir, name || void 0) : [];
4645
+ const viewsDir = join3(platformDir, "views");
4646
+ if (!type && existsSync3(viewsDir) && readdirSync3(viewsDir, { withFileTypes: true }).some((entry) => entry.isDirectory() && entry.name !== "shared")) {
4647
+ console.log(pc2.dim(" View publishing skipped. A human source-project admin can use lumera apply views or Resource shares."));
4648
+ }
4533
4649
  const localAutomations = !type || type === "automations" ? loadLocalAutomations(platformDir, name || void 0, appName) : [];
4534
4650
  const localHooks = !type || type === "hooks" ? loadLocalHooks(platformDir, name || void 0, appName) : [];
4535
4651
  const localSkills = !type || type === "skills" ? loadLocalSkills(platformDir, name || void 0) : [];
@@ -4539,6 +4655,7 @@ async function apply(args) {
4539
4655
  const localAgents = !type || type === "agents" ? loadLocalAgents(platformDir, name || void 0, appName) : [];
4540
4656
  const localMailboxes = !type || type === "mailboxes" ? loadLocalMailboxes(platformDir, name || void 0) : [];
4541
4657
  if (localCollections.length > 0) allChanges.push(...await planCollections(api, localCollections, appName, projectId));
4658
+ if (localViews.length > 0) allChanges.push(...await planViews(api, localViews, projectId));
4542
4659
  if (localAutomations.length > 0) allChanges.push(...await planAutomations(api, localAutomations, projectId));
4543
4660
  if (localHooks.length > 0) allChanges.push(...await planHooks(api, localHooks, collections, projectId));
4544
4661
  if (localSkills.length > 0) allChanges.push(...await planSkills(api, localSkills, projectId, appName));
@@ -4549,7 +4666,7 @@ async function apply(args) {
4549
4666
  }
4550
4667
  if (localMailboxes.length > 0) allChanges.push(...await planMailboxes(api, localMailboxes));
4551
4668
  if (name) {
4552
- const hasLocal = localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localSkills.length > 0 || localAgents.length > 0 || localMailboxes.length > 0;
4669
+ const hasLocal = localViews.length > 0 || localCollections.length > 0 || localAutomations.length > 0 || localHooks.length > 0 || localSkills.length > 0 || localAgents.length > 0 || localMailboxes.length > 0;
4553
4670
  if (!hasLocal) {
4554
4671
  console.log();
4555
4672
  console.log(pc2.red(` Resource "${name}" not found locally`));
@@ -4565,7 +4682,7 @@ async function apply(args) {
4565
4682
  let willDeployApp = false;
4566
4683
  if (!type && !skipApp) {
4567
4684
  try {
4568
- if (existsSync2(join2(projectRoot, "dist")) || existsSync2(join2(projectRoot, "src"))) {
4685
+ if (existsSync3(join3(projectRoot, "dist")) || existsSync3(join3(projectRoot, "src"))) {
4569
4686
  willDeployApp = true;
4570
4687
  }
4571
4688
  } catch {
@@ -4627,6 +4744,11 @@ async function apply(args) {
4627
4744
  totalErrors += await applyCollections(api, localCollections, appName);
4628
4745
  console.log();
4629
4746
  }
4747
+ if (localViews.length > 0) {
4748
+ if (!appName) throw new Error("View publishing requires a project context");
4749
+ console.log(pc2.bold(" Views:"));
4750
+ totalErrors += await applyViews(api, localViews, appName, projectId);
4751
+ }
4630
4752
  try {
4631
4753
  const remoteCollections = await api.listCollections();
4632
4754
  collections = buildCollectionIdMap(remoteCollections, appName);
@@ -4693,6 +4815,10 @@ async function pull(args) {
4693
4815
  const api = createApiClient(void 0, void 0, appName);
4694
4816
  const projectId = getProjectId(projectRoot);
4695
4817
  const { type, name } = parseResource(filteredArgs[0]);
4818
+ if (type === "views") {
4819
+ console.log(" Views are authored in Git. Run git pull to sync platform/views and platform/project.json.");
4820
+ return;
4821
+ }
4696
4822
  let checkedLocalSkills;
4697
4823
  if (!force) {
4698
4824
  const conflicts = [];
@@ -4814,6 +4940,7 @@ async function pull(args) {
4814
4940
  console.log();
4815
4941
  }
4816
4942
  async function destroy(args) {
4943
+ 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
4944
  if (args.includes("--help") || args.includes("-h")) {
4818
4945
  showDestroyHelp();
4819
4946
  return;
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2CR762KB.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-W57NUJFJ.js";
6
+ } from "./chunk-2A2DSLIE.js";
7
7
  import {
8
8
  findProjectRoot,
9
9
  getApiUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.34.5",
3
+ "version": "0.35.1",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {