@lumerahq/cli 0.34.4 → 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.
- package/dist/{chunk-W57NUJFJ.js → chunk-2A2DSLIE.js} +18 -0
- package/dist/{chunk-P6GDSHYC.js → chunk-5TQ3BQ2X.js} +1 -1
- package/dist/{chunk-5JNNUIVJ.js → chunk-A27HZEU6.js} +22 -10
- package/dist/{deps-TNGT6JVD.js → deps-DLJUDFNK.js} +2 -2
- package/dist/{dev-24PCZUAT.js → dev-E2O6R7WW.js} +3 -3
- package/dist/index.js +14 -13
- package/dist/{init-S42P4A5J.js → init-MJEVA5HK.js} +1 -1
- package/dist/{register-GMWA4D4C.js → register-RUDM4ITV.js} +1 -1
- package/dist/{resources-IGHWATTH.js → resources-X7K436TK.js} +244 -117
- package/dist/{run-4XFA5ZDJ.js → run-BJ7PXSEH.js} +1 -1
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
import {
|
|
5
5
|
createApiClient,
|
|
6
6
|
isApiErrorStatus
|
|
7
|
-
} from "./chunk-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
5
|
+
} from "./chunk-A27HZEU6.js";
|
|
6
6
|
import "./chunk-2CR762KB.js";
|
|
7
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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-
|
|
246
|
+
await import("./resources-X7K436TK.js").then((m) => m.plan(args.slice(1)));
|
|
246
247
|
break;
|
|
247
248
|
case "apply":
|
|
248
|
-
await import("./resources-
|
|
249
|
+
await import("./resources-X7K436TK.js").then((m) => m.apply(args.slice(1)));
|
|
249
250
|
break;
|
|
250
251
|
case "pull":
|
|
251
|
-
await import("./resources-
|
|
252
|
+
await import("./resources-X7K436TK.js").then((m) => m.pull(args.slice(1)));
|
|
252
253
|
break;
|
|
253
254
|
case "destroy":
|
|
254
|
-
await import("./resources-
|
|
255
|
+
await import("./resources-X7K436TK.js").then((m) => m.destroy(args.slice(1)));
|
|
255
256
|
break;
|
|
256
257
|
case "list":
|
|
257
|
-
await import("./resources-
|
|
258
|
+
await import("./resources-X7K436TK.js").then((m) => m.list(args.slice(1)));
|
|
258
259
|
break;
|
|
259
260
|
case "show":
|
|
260
|
-
await import("./resources-
|
|
261
|
+
await import("./resources-X7K436TK.js").then((m) => m.show(args.slice(1)));
|
|
261
262
|
break;
|
|
262
263
|
case "diff":
|
|
263
|
-
await import("./resources-
|
|
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-
|
|
268
|
+
await import("./dev-E2O6R7WW.js").then((m) => m.dev(args.slice(1)));
|
|
268
269
|
break;
|
|
269
270
|
case "run":
|
|
270
|
-
await import("./run-
|
|
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-
|
|
285
|
+
await import("./init-MJEVA5HK.js").then((m) => m.init(args.slice(1)));
|
|
285
286
|
break;
|
|
286
287
|
case "register":
|
|
287
|
-
await import("./register-
|
|
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-
|
|
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":
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deploy,
|
|
3
3
|
deployAppAPI
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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
|
|
41
|
-
import { dirname as dirname2, join as
|
|
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) {
|
|
@@ -762,7 +845,7 @@ function messageForIssue(issue) {
|
|
|
762
845
|
}
|
|
763
846
|
var hookVerifyRule = {
|
|
764
847
|
id: "hook-verify",
|
|
765
|
-
description: "Validates hook file structure (config export, trigger, known keys, default handler) and
|
|
848
|
+
description: "Validates hook file structure (config export, trigger, known keys, default handler) and checks on the backend that the script compiles and uses only what the hook runtime provides.",
|
|
766
849
|
appliesTo: ["hook"],
|
|
767
850
|
async check(target, ctx) {
|
|
768
851
|
if (target.filePath.endsWith(".ts")) {
|
|
@@ -835,7 +918,7 @@ var hookVerifyRule = {
|
|
|
835
918
|
const position = line ? ` at line ${line}${diagnostic.column ? `, column ${diagnostic.column}` : ""}` : "";
|
|
836
919
|
return error2(
|
|
837
920
|
target,
|
|
838
|
-
`Hook
|
|
921
|
+
`Hook script is invalid${position}: ${diagnostic.message}`,
|
|
839
922
|
line
|
|
840
923
|
);
|
|
841
924
|
})
|
|
@@ -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:
|
|
1052
|
+
configFilePath: join2(dirname(a.filePath), "config.json")
|
|
970
1053
|
}));
|
|
971
1054
|
}
|
|
972
1055
|
function buildCollectionTargets(platformDir, _filterName) {
|
|
973
|
-
const collectionsDir =
|
|
974
|
-
if (!
|
|
1056
|
+
const collectionsDir = join2(platformDir, "collections");
|
|
1057
|
+
if (!existsSync2(collectionsDir)) return [];
|
|
975
1058
|
const targets = [];
|
|
976
|
-
for (const entry of
|
|
1059
|
+
for (const entry of readdirSync2(collectionsDir, { withFileTypes: true })) {
|
|
977
1060
|
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
978
|
-
const filePath =
|
|
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:
|
|
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 =
|
|
990
|
-
if (!
|
|
1072
|
+
const hooksDir = join2(platformDir, "hooks");
|
|
1073
|
+
if (!existsSync2(hooksDir)) return [];
|
|
991
1074
|
const targets = [];
|
|
992
|
-
for (const entry of
|
|
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 =
|
|
1080
|
+
const filePath = join2(hooksDir, entry.name);
|
|
998
1081
|
targets.push({
|
|
999
1082
|
kind: "hook",
|
|
1000
1083
|
name: base,
|
|
1001
1084
|
filePath,
|
|
1002
|
-
source:
|
|
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 =
|
|
1009
|
-
if (!
|
|
1091
|
+
const agentsDir = join2(platformDir, "agents");
|
|
1092
|
+
if (!existsSync2(agentsDir)) return [];
|
|
1010
1093
|
const agents = [];
|
|
1011
|
-
for (const entry of
|
|
1094
|
+
for (const entry of readdirSync2(agentsDir, { withFileTypes: true })) {
|
|
1012
1095
|
if (!entry.isDirectory()) continue;
|
|
1013
|
-
const directoryPath =
|
|
1014
|
-
const configPath =
|
|
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 (
|
|
1021
|
-
configSource =
|
|
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 =
|
|
1069
|
-
const hasPolicyFile =
|
|
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 ?
|
|
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(
|
|
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 (
|
|
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
|
|
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
|
|
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 (
|
|
1722
|
-
return
|
|
1810
|
+
if (existsSync3(join3(process.cwd(), "platform"))) {
|
|
1811
|
+
return join3(process.cwd(), "platform");
|
|
1723
1812
|
}
|
|
1724
|
-
if (
|
|
1725
|
-
return
|
|
1813
|
+
if (existsSync3(join3(process.cwd(), "lumera_platform"))) {
|
|
1814
|
+
return join3(process.cwd(), "lumera_platform");
|
|
1726
1815
|
}
|
|
1727
|
-
return
|
|
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 =
|
|
1734
|
-
if (!
|
|
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
|
|
1828
|
+
for (const file of readdirSync3(collectionsDir)) {
|
|
1740
1829
|
if (!file.endsWith(".json")) continue;
|
|
1741
|
-
const filePath =
|
|
1830
|
+
const filePath = join3(collectionsDir, file);
|
|
1742
1831
|
try {
|
|
1743
|
-
const content =
|
|
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 =
|
|
1820
|
-
if (!
|
|
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
|
|
1914
|
+
for (const entry of readdirSync3(automationsDir, { withFileTypes: true })) {
|
|
1826
1915
|
if (!entry.isDirectory()) continue;
|
|
1827
|
-
const automationDir =
|
|
1828
|
-
const configPath =
|
|
1829
|
-
const mainPath =
|
|
1830
|
-
if (!
|
|
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 (!
|
|
1923
|
+
if (!existsSync3(mainPath)) {
|
|
1835
1924
|
errors.push(`${entry.name}: missing main.py`);
|
|
1836
1925
|
continue;
|
|
1837
1926
|
}
|
|
1838
1927
|
try {
|
|
1839
|
-
const configContent =
|
|
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 =
|
|
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 =
|
|
1886
|
-
if (!
|
|
1974
|
+
const hooksDir = join3(platformDir, "hooks");
|
|
1975
|
+
if (!existsSync3(hooksDir)) {
|
|
1887
1976
|
return [];
|
|
1888
1977
|
}
|
|
1889
1978
|
const hooks = [];
|
|
1890
|
-
for (const file of
|
|
1979
|
+
for (const file of readdirSync3(hooksDir)) {
|
|
1891
1980
|
if (!file.endsWith(".js") && !file.endsWith(".ts")) continue;
|
|
1892
|
-
const filePath =
|
|
1893
|
-
const content =
|
|
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 =
|
|
2225
|
-
if (!
|
|
2313
|
+
const mailboxesDir = join3(platformDir, "mailboxes");
|
|
2314
|
+
if (!existsSync3(mailboxesDir)) {
|
|
2226
2315
|
return [];
|
|
2227
2316
|
}
|
|
2228
2317
|
const mailboxes = [];
|
|
2229
|
-
for (const file of
|
|
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 =
|
|
2328
|
+
const filePath = join3(mailboxesDir, file);
|
|
2240
2329
|
let raw;
|
|
2241
2330
|
try {
|
|
2242
|
-
raw = JSON.parse(
|
|
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 =
|
|
2322
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
2717
|
-
writeFileSync(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2881
|
-
if (!
|
|
2969
|
+
const skillsDir = join3(platformDir, "skills");
|
|
2970
|
+
if (!existsSync3(skillsDir)) return [];
|
|
2882
2971
|
const skills = [];
|
|
2883
2972
|
const errors = [];
|
|
2884
|
-
for (const entry of
|
|
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 =
|
|
2892
|
-
const path =
|
|
2893
|
-
if (!
|
|
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 =
|
|
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 =
|
|
3083
|
-
const sha256 =
|
|
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 (
|
|
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 =
|
|
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 =
|
|
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 && !
|
|
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 =
|
|
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 =
|
|
3342
|
+
const skillDir = join3(skillsDir, localSlug);
|
|
3254
3343
|
mkdirSync(skillsDir, { recursive: true });
|
|
3255
|
-
const stagingDir = mkdtempSync(
|
|
3344
|
+
const stagingDir = mkdtempSync(join3(platformDir, `.skill-${localSlug}-pull-`));
|
|
3256
3345
|
try {
|
|
3257
|
-
writeFileSync(
|
|
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,
|
|
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 =
|
|
3279
|
-
if (!
|
|
3367
|
+
const agentsDir = join3(platformDir, "agents");
|
|
3368
|
+
if (!existsSync3(agentsDir)) return [];
|
|
3280
3369
|
const agents = [];
|
|
3281
3370
|
const errors = [];
|
|
3282
|
-
for (const entry of
|
|
3371
|
+
for (const entry of readdirSync3(agentsDir, { withFileTypes: true })) {
|
|
3283
3372
|
if (!entry.isDirectory()) continue;
|
|
3284
|
-
const agentDir =
|
|
3285
|
-
const configPath =
|
|
3286
|
-
const promptPath =
|
|
3287
|
-
const policyPath =
|
|
3288
|
-
if (!
|
|
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 (!
|
|
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(
|
|
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 =
|
|
3328
|
-
let policyScript =
|
|
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 =
|
|
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 =
|
|
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(
|
|
3574
|
-
writeFileSync(
|
|
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(
|
|
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 (
|
|
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;
|