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