@holo-js/cli 0.1.2 → 0.1.4
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/bin/holo.mjs +533 -4616
- package/dist/broadcast-CSSARTSA.mjs +84 -0
- package/dist/broadcast-YSIJCL3R.mjs +85 -0
- package/dist/cache-4G6QGIZO.mjs +66 -0
- package/dist/cache-OWQY4E7W.mjs +67 -0
- package/dist/cache-migrations-NATT5WPQ.mjs +154 -0
- package/dist/cache-migrations-RVEA6CEU.mjs +155 -0
- package/dist/chunk-3OTCSFDG.mjs +849 -0
- package/dist/chunk-66FHW725.mjs +465 -0
- package/dist/chunk-BWW5TDFI.mjs +4 -0
- package/dist/chunk-CUL4RJTG.mjs +22 -0
- package/dist/chunk-D4GG556Y.mjs +23 -0
- package/dist/chunk-D7O4SU6N.mjs +2 -0
- package/dist/chunk-DMH2B4UQ.mjs +343 -0
- package/dist/chunk-ET7UXHHQ.mjs +166 -0
- package/dist/chunk-EUIVXVJL.mjs +25 -0
- package/dist/chunk-G5ADO27Q.mjs +463 -0
- package/dist/chunk-GSQ3HTRO.mjs +165 -0
- package/dist/chunk-H7TJ4FB3.mjs +848 -0
- package/dist/chunk-ICJR7TS4.mjs +66 -0
- package/dist/chunk-JX2ZH6XY.mjs +270 -0
- package/dist/chunk-M7J3YTHR.mjs +26 -0
- package/dist/chunk-MZXN2YMI.mjs +3236 -0
- package/dist/chunk-Q5F6C2D4.mjs +65 -0
- package/dist/chunk-QFUSWV3J.mjs +3237 -0
- package/dist/chunk-QYLSMF7V.mjs +539 -0
- package/dist/chunk-S7P7EBM3.mjs +787 -0
- package/dist/chunk-SRWJU3A5.mjs +11 -0
- package/dist/chunk-URK7C3VQ.mjs +538 -0
- package/dist/chunk-VT5IDQG6.mjs +788 -0
- package/dist/chunk-XUYKPU5Q.mjs +272 -0
- package/dist/chunk-ZLRO7HXY.mjs +342 -0
- package/dist/chunk-ZXDU7RHU.mjs +9 -0
- package/dist/config-DMWBMMGD.mjs +26 -0
- package/dist/config-LS5USBRB.mjs +25 -0
- package/dist/dev-KQFT7RHR.mjs +43 -0
- package/dist/dev-LZ3O2E3U.mjs +42 -0
- package/dist/discovery-GBLAUTXS.mjs +28 -0
- package/dist/discovery-R733D2PO.mjs +29 -0
- package/dist/generators-DSN4GWJI.mjs +425 -0
- package/dist/generators-WX45BI4U.mjs +426 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +536 -4618
- package/dist/queue-6OG7VJ34.mjs +626 -0
- package/dist/queue-FV35LLPR.mjs +625 -0
- package/dist/queue-migrations-NK2EYX3J.mjs +163 -0
- package/dist/queue-migrations-SSIYKK5S.mjs +162 -0
- package/dist/runtime-4BV3JODY.mjs +56 -0
- package/dist/runtime-ANBO7VQM.mjs +33 -0
- package/dist/runtime-EFZ5H5IL.mjs +55 -0
- package/dist/runtime-OOSJ5JBY.mjs +32 -0
- package/dist/scaffold-7OTDH4UR.mjs +121 -0
- package/dist/scaffold-DRKBGS2K.mjs +120 -0
- package/dist/security-ATKDC26E.mjs +68 -0
- package/dist/security-R7VH6W5H.mjs +69 -0
- package/package.json +12 -11
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadGeneratedProjectRegistry,
|
|
3
|
+
writeGeneratedProjectRegistry
|
|
4
|
+
} from "./chunk-H7TJ4FB3.mjs";
|
|
5
|
+
import {
|
|
6
|
+
COMMAND_FILE_PATTERN,
|
|
7
|
+
MIGRATION_NAME_PATTERN,
|
|
8
|
+
hasEventDefinitionMarker,
|
|
9
|
+
hasListenerDefinitionMarker,
|
|
10
|
+
importProjectModule,
|
|
11
|
+
isRecord,
|
|
12
|
+
loadAuthorizationDiscoveryModule,
|
|
13
|
+
loadBroadcastDiscoveryModule,
|
|
14
|
+
loadEventsDiscoveryModule,
|
|
15
|
+
loadQueueDiscoveryModule,
|
|
16
|
+
makeProjectRelativePath,
|
|
17
|
+
pathExists,
|
|
18
|
+
readTextFile,
|
|
19
|
+
toPosixPath
|
|
20
|
+
} from "./chunk-G5ADO27Q.mjs";
|
|
21
|
+
|
|
22
|
+
// src/project/discovery.ts
|
|
23
|
+
import { readdir } from "fs/promises";
|
|
24
|
+
import { basename, dirname, extname, join, relative, resolve } from "path";
|
|
25
|
+
import { loadConfigDirectory } from "@holo-js/config";
|
|
26
|
+
import {
|
|
27
|
+
normalizeHoloProjectConfig
|
|
28
|
+
} from "@holo-js/db";
|
|
29
|
+
async function collectFiles(root) {
|
|
30
|
+
if (!await pathExists(root)) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
34
|
+
const files = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const target = join(root, entry.name);
|
|
37
|
+
if (entry.isDirectory()) {
|
|
38
|
+
files.push(...await collectFiles(target));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (entry.isFile() && COMMAND_FILE_PATTERN.test(entry.name)) {
|
|
42
|
+
files.push(target);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return files;
|
|
46
|
+
}
|
|
47
|
+
function deriveCommandNameFromPath(commandsRoot, sourcePath) {
|
|
48
|
+
const relativePath = toPosixPath(relative(commandsRoot, sourcePath));
|
|
49
|
+
return relativePath.replace(COMMAND_FILE_PATTERN, "").split("/").filter(Boolean).join(":");
|
|
50
|
+
}
|
|
51
|
+
function deriveJobNameFromPath(jobsRoot, sourcePath) {
|
|
52
|
+
const relativePath = toPosixPath(relative(jobsRoot, sourcePath));
|
|
53
|
+
return relativePath.replace(COMMAND_FILE_PATTERN, "").split("/").filter(Boolean).join(".");
|
|
54
|
+
}
|
|
55
|
+
function deriveEventNameFromPath(eventsRoot, sourcePath) {
|
|
56
|
+
const relativePath = toPosixPath(relative(eventsRoot, sourcePath)).replace(COMMAND_FILE_PATTERN, "");
|
|
57
|
+
const derived = relativePath.split("/").map((part) => part.trim()).filter(Boolean).join(".");
|
|
58
|
+
if (!derived) {
|
|
59
|
+
throw new Error("[Holo Events] Derived event names require a non-empty source path.");
|
|
60
|
+
}
|
|
61
|
+
return derived;
|
|
62
|
+
}
|
|
63
|
+
function deriveListenerIdFromPath(listenersRoot, sourcePath) {
|
|
64
|
+
const relativePath = toPosixPath(relative(listenersRoot, sourcePath));
|
|
65
|
+
const derived = relativePath.replace(COMMAND_FILE_PATTERN, "").split("/").map((part) => part.trim()).filter(Boolean).join(".");
|
|
66
|
+
if (!derived) {
|
|
67
|
+
throw new Error("[Holo Events] Derived listener identifiers require a non-empty source path.");
|
|
68
|
+
}
|
|
69
|
+
return derived;
|
|
70
|
+
}
|
|
71
|
+
function deriveBroadcastNameFromPath(root, sourcePath) {
|
|
72
|
+
const relativePath = toPosixPath(relative(root, sourcePath)).replace(COMMAND_FILE_PATTERN, "");
|
|
73
|
+
const derived = relativePath.split("/").map((part) => part.trim()).filter(Boolean).join(".");
|
|
74
|
+
if (!derived) {
|
|
75
|
+
throw new Error("[Holo Broadcast] Derived broadcast names require a non-empty source path.");
|
|
76
|
+
}
|
|
77
|
+
return derived;
|
|
78
|
+
}
|
|
79
|
+
function deriveChannelPatternFromPath(root, sourcePath) {
|
|
80
|
+
const relativePath = toPosixPath(relative(root, sourcePath)).replace(COMMAND_FILE_PATTERN, "");
|
|
81
|
+
const derived = relativePath.split("/").map((part) => part.trim()).filter(Boolean).join(".");
|
|
82
|
+
if (!derived) {
|
|
83
|
+
throw new Error("[Holo Broadcast] Derived channel patterns require a non-empty source path.");
|
|
84
|
+
}
|
|
85
|
+
return derived;
|
|
86
|
+
}
|
|
87
|
+
function resolveDiscoveredJobMetadata(job, sourcePath, derivedName, queueConfig) {
|
|
88
|
+
const connection = job.connection ?? queueConfig.default;
|
|
89
|
+
let queue = job.queue;
|
|
90
|
+
if (!queue) {
|
|
91
|
+
const configuredConnection = queueConfig.connections[connection];
|
|
92
|
+
queue = configuredConnection ? configuredConnection.queue : "default";
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
sourcePath,
|
|
96
|
+
name: derivedName,
|
|
97
|
+
connection,
|
|
98
|
+
queue,
|
|
99
|
+
...typeof job.tries === "number" ? { tries: job.tries } : {},
|
|
100
|
+
...typeof job.backoff !== "undefined" ? { backoff: job.backoff } : {},
|
|
101
|
+
...typeof job.timeout === "number" ? { timeout: job.timeout } : {}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function isAppCommand(value) {
|
|
105
|
+
return isRecord(value) && typeof value.description === "string" && typeof value.run === "function";
|
|
106
|
+
}
|
|
107
|
+
function resolveCommandExport(moduleValue) {
|
|
108
|
+
if (isRecord(moduleValue) && isAppCommand(moduleValue.default)) {
|
|
109
|
+
return moduleValue.default;
|
|
110
|
+
}
|
|
111
|
+
if (isRecord(moduleValue)) {
|
|
112
|
+
for (const value of Object.values(moduleValue)) {
|
|
113
|
+
if (isAppCommand(value)) {
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
function normalizeCommandAliases(value) {
|
|
121
|
+
if (!value) {
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
const normalized = [...new Set(value.map((alias) => alias.trim()).filter(Boolean))];
|
|
125
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
126
|
+
}
|
|
127
|
+
function assertUniqueEntries(kind, entries) {
|
|
128
|
+
const seen = /* @__PURE__ */ new Map();
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const existing = seen.get(entry.name);
|
|
131
|
+
if (existing) {
|
|
132
|
+
throw new Error(`Discovered duplicate ${kind} "${entry.name}" in "${existing}" and "${entry.sourcePath}".`);
|
|
133
|
+
}
|
|
134
|
+
seen.set(entry.name, entry.sourcePath);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function assertUniqueCommandTokens(entries) {
|
|
138
|
+
const seen = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
for (const token of [entry.name, ...entry.aliases]) {
|
|
141
|
+
const existing = seen.get(token);
|
|
142
|
+
if (existing) {
|
|
143
|
+
throw new Error(`Discovered duplicate command token "${token}" in "${existing}" and "${entry.sourcePath}".`);
|
|
144
|
+
}
|
|
145
|
+
seen.set(token, entry.sourcePath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function resolveRegisteredPath(projectRoot, entry) {
|
|
150
|
+
return resolve(projectRoot, entry);
|
|
151
|
+
}
|
|
152
|
+
function resolveNamedExport(moduleValue, matcher) {
|
|
153
|
+
if (!isRecord(moduleValue)) return void 0;
|
|
154
|
+
if (matcher(moduleValue.default)) return moduleValue.default;
|
|
155
|
+
for (const value of Object.values(moduleValue)) {
|
|
156
|
+
if (matcher(value)) return value;
|
|
157
|
+
}
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
function resolveNamedExportEntry(moduleValue, matcher) {
|
|
161
|
+
if (!isRecord(moduleValue)) return void 0;
|
|
162
|
+
if (matcher(moduleValue.default)) {
|
|
163
|
+
return { exportName: "default", value: moduleValue.default };
|
|
164
|
+
}
|
|
165
|
+
for (const [exportName, value] of Object.entries(moduleValue)) {
|
|
166
|
+
if (matcher(value)) return { exportName, value };
|
|
167
|
+
}
|
|
168
|
+
return void 0;
|
|
169
|
+
}
|
|
170
|
+
function isCliModelReference(value) {
|
|
171
|
+
return isRecord(value) && isRecord(value.definition) && value.definition.kind === "model" && typeof value.definition.name === "string" && typeof value.prune === "function";
|
|
172
|
+
}
|
|
173
|
+
function isMissingGeneratedSchemaModelError(error) {
|
|
174
|
+
return error instanceof Error && error.message.includes("is not present in the generated schema registry");
|
|
175
|
+
}
|
|
176
|
+
function isInactiveGeneratedModelModule(value) {
|
|
177
|
+
return isRecord(value) && value.holoModelPendingSchema === true;
|
|
178
|
+
}
|
|
179
|
+
function isMigrationDefinition(value) {
|
|
180
|
+
return isRecord(value) && typeof value.up === "function";
|
|
181
|
+
}
|
|
182
|
+
function isSeederDefinition(value) {
|
|
183
|
+
return isRecord(value) && typeof value.name === "string" && typeof value.run === "function";
|
|
184
|
+
}
|
|
185
|
+
function resolveListenerEventNamesForDiscovery(listener, eventNamesByReference = /* @__PURE__ */ new Map()) {
|
|
186
|
+
return Object.freeze([...new Set(listener.listensTo.map((reference) => {
|
|
187
|
+
if (typeof reference === "string") {
|
|
188
|
+
return reference.trim();
|
|
189
|
+
}
|
|
190
|
+
if (typeof reference.name === "string" && reference.name.trim()) {
|
|
191
|
+
return reference.name.trim();
|
|
192
|
+
}
|
|
193
|
+
if (eventNamesByReference.has(reference)) {
|
|
194
|
+
return eventNamesByReference.get(reference);
|
|
195
|
+
}
|
|
196
|
+
throw new Error("[Holo Events] Listener event references must resolve to explicit event names before discovery registration.");
|
|
197
|
+
}))]);
|
|
198
|
+
}
|
|
199
|
+
function resolveAuthorizationTargetName(target) {
|
|
200
|
+
const modelFacadeTarget = target;
|
|
201
|
+
if (typeof modelFacadeTarget.definition?.name === "string" && modelFacadeTarget.definition.name.trim()) {
|
|
202
|
+
return modelFacadeTarget.definition.name.trim();
|
|
203
|
+
}
|
|
204
|
+
const namedTarget = target;
|
|
205
|
+
return typeof namedTarget.name === "string" && namedTarget.name.trim() ? namedTarget.name.trim() : void 0;
|
|
206
|
+
}
|
|
207
|
+
function captureAuthorizationDefinitionNames(definitions) {
|
|
208
|
+
return new Set(definitions.keys());
|
|
209
|
+
}
|
|
210
|
+
function findAddedAuthorizationDefinitionNames(definitions, existingNames) {
|
|
211
|
+
return [...definitions.keys()].filter((name) => !existingNames.has(name));
|
|
212
|
+
}
|
|
213
|
+
function unregisterAuthorizationDefinitionNames(authorizationDiscovery, policyNames, abilityNames) {
|
|
214
|
+
if (typeof authorizationDiscovery.authorizationInternals.unregisterPolicyDefinition !== "function" || typeof authorizationDiscovery.authorizationInternals.unregisterAbilityDefinition !== "function") {
|
|
215
|
+
authorizationDiscovery.authorizationInternals.resetAuthorizationRuntimeState?.();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
for (const policyName of policyNames) {
|
|
219
|
+
authorizationDiscovery.authorizationInternals.unregisterPolicyDefinition(policyName);
|
|
220
|
+
}
|
|
221
|
+
for (const abilityName of abilityNames) {
|
|
222
|
+
authorizationDiscovery.authorizationInternals.unregisterAbilityDefinition(abilityName);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function collectImportedBindingsBySource(sourceText) {
|
|
226
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
227
|
+
const importPattern = /import\s+([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
|
|
228
|
+
for (const match of sourceText.matchAll(importPattern)) {
|
|
229
|
+
const clause = match[1]?.trim();
|
|
230
|
+
const source = match[2]?.trim();
|
|
231
|
+
if (!clause || !source) {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const namedMatch = clause.match(/\{([\s\S]+)\}/);
|
|
235
|
+
const defaultClause = clause.replace(/\{[\s\S]+\}/, "").replace(/,$/, "").trim();
|
|
236
|
+
if (defaultClause && defaultClause !== "*") {
|
|
237
|
+
bindings.set(defaultClause, source);
|
|
238
|
+
}
|
|
239
|
+
if (namedMatch?.[1]) {
|
|
240
|
+
for (const specifier of namedMatch[1].split(",")) {
|
|
241
|
+
const trimmed = specifier.trim();
|
|
242
|
+
if (!trimmed) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const [imported, local] = trimmed.split(/\s+as\s+/);
|
|
246
|
+
const bindingName = (local ?? imported)?.trim();
|
|
247
|
+
if (bindingName) {
|
|
248
|
+
bindings.set(bindingName, source);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return bindings;
|
|
254
|
+
}
|
|
255
|
+
function extractListensToItems(sourceText) {
|
|
256
|
+
const markerIndex = sourceText.indexOf("listensTo");
|
|
257
|
+
if (markerIndex < 0) {
|
|
258
|
+
return [];
|
|
259
|
+
}
|
|
260
|
+
const colonIndex = sourceText.indexOf(":", markerIndex);
|
|
261
|
+
if (colonIndex < 0) {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
let cursor = colonIndex + 1;
|
|
265
|
+
while (cursor < sourceText.length && /\s/.test(sourceText[cursor])) {
|
|
266
|
+
cursor += 1;
|
|
267
|
+
}
|
|
268
|
+
const startChar = sourceText[cursor];
|
|
269
|
+
let depth = 0;
|
|
270
|
+
let inString;
|
|
271
|
+
let expression = "";
|
|
272
|
+
for (; cursor < sourceText.length; cursor += 1) {
|
|
273
|
+
const char = sourceText[cursor];
|
|
274
|
+
expression += char;
|
|
275
|
+
if (inString) {
|
|
276
|
+
if (char === inString && sourceText[cursor - 1] !== "\\") {
|
|
277
|
+
inString = void 0;
|
|
278
|
+
}
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
282
|
+
inString = char;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (char === "[" || char === "{" || char === "(") {
|
|
286
|
+
depth += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (char === "]" || char === "}" || char === ")") {
|
|
290
|
+
depth -= 1;
|
|
291
|
+
if (depth === 0 && startChar === "[") {
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (depth === 0 && startChar !== "[" && (char === "," || char === "\n" || char === "\r")) {
|
|
297
|
+
expression = expression.slice(0, -1);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (startChar !== "[") {
|
|
302
|
+
const item = expression.trim().replace(/\s+as\s+const$/, "");
|
|
303
|
+
return item ? [item] : [];
|
|
304
|
+
}
|
|
305
|
+
return expression.slice(1, -1).split(",").map((item) => item.trim().replace(/\s+as\s+const$/, "")).filter(Boolean);
|
|
306
|
+
}
|
|
307
|
+
async function resolveListenerEventNamesFromSource(projectRoot, listenerPath, discoveredEventNamesBySourcePath) {
|
|
308
|
+
const sourceText = await readTextFile(listenerPath) ?? "";
|
|
309
|
+
const bindingsByName = collectImportedBindingsBySource(sourceText);
|
|
310
|
+
const discoveredEventNamesByExtensionlessSourcePath = new Map(
|
|
311
|
+
[...discoveredEventNamesBySourcePath.entries()].map(([sourcePath, eventName]) => {
|
|
312
|
+
return [sourcePath.replace(/\.[^.]+$/, ""), eventName];
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
const resolvedEventNames = [];
|
|
316
|
+
for (const item of extractListensToItems(sourceText)) {
|
|
317
|
+
const quoted = item.match(/^['"](.+)['"]$/);
|
|
318
|
+
if (quoted) {
|
|
319
|
+
resolvedEventNames.push(quoted[1].trim());
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const importSource = bindingsByName.get(item);
|
|
323
|
+
if (!importSource) {
|
|
324
|
+
throw new Error("[Holo Events] Listener event references must resolve to explicit event names before discovery registration.");
|
|
325
|
+
}
|
|
326
|
+
const importedPath = makeProjectRelativePath(projectRoot, resolve(dirname(listenerPath), importSource));
|
|
327
|
+
const eventName = discoveredEventNamesBySourcePath.get(importedPath) ?? discoveredEventNamesByExtensionlessSourcePath.get(importedPath.replace(/\.[^.]+$/, ""));
|
|
328
|
+
if (!eventName) {
|
|
329
|
+
throw new Error("[Holo Events] Listener event references must resolve to explicit event names before discovery registration.");
|
|
330
|
+
}
|
|
331
|
+
resolvedEventNames.push(eventName);
|
|
332
|
+
}
|
|
333
|
+
return Object.freeze([...new Set(resolvedEventNames)]);
|
|
334
|
+
}
|
|
335
|
+
function resolveBroadcastArtifactsPath(config, key) {
|
|
336
|
+
const configuredPaths = config.paths;
|
|
337
|
+
return configuredPaths[key] ?? `server/${key}`;
|
|
338
|
+
}
|
|
339
|
+
async function prepareProjectDiscovery(projectRoot, config = normalizeHoloProjectConfig()) {
|
|
340
|
+
const loadedConfig = await loadConfigDirectory(projectRoot, {
|
|
341
|
+
processEnv: process.env
|
|
342
|
+
});
|
|
343
|
+
const modelsRoot = resolve(projectRoot, config.paths.models);
|
|
344
|
+
const migrationsRoot = resolve(projectRoot, config.paths.migrations);
|
|
345
|
+
const seedersRoot = resolve(projectRoot, config.paths.seeders);
|
|
346
|
+
const commandsRoot = resolve(projectRoot, config.paths.commands);
|
|
347
|
+
const jobsRoot = resolve(projectRoot, config.paths.jobs);
|
|
348
|
+
const eventsRoot = resolve(projectRoot, config.paths.events);
|
|
349
|
+
const listenersRoot = resolve(projectRoot, config.paths.listeners);
|
|
350
|
+
const broadcastPath = resolveBroadcastArtifactsPath(config, "broadcast");
|
|
351
|
+
const channelsPath = resolveBroadcastArtifactsPath(config, "channels");
|
|
352
|
+
const broadcastRoot = resolve(projectRoot, broadcastPath);
|
|
353
|
+
const channelsRoot = resolve(projectRoot, channelsPath);
|
|
354
|
+
const policiesRoot = resolve(projectRoot, config.paths.authorizationPolicies ?? "server/policies");
|
|
355
|
+
const abilitiesRoot = resolve(projectRoot, config.paths.authorizationAbilities ?? "server/abilities");
|
|
356
|
+
const [modelFiles, migrationFiles, seederFiles, commandFiles, jobFiles, eventFiles, listenerFiles, broadcastFiles, channelFiles] = await Promise.all([
|
|
357
|
+
collectFiles(modelsRoot),
|
|
358
|
+
collectFiles(migrationsRoot),
|
|
359
|
+
collectFiles(seedersRoot),
|
|
360
|
+
collectFiles(commandsRoot),
|
|
361
|
+
collectFiles(jobsRoot),
|
|
362
|
+
collectFiles(eventsRoot),
|
|
363
|
+
collectFiles(listenersRoot),
|
|
364
|
+
collectFiles(broadcastRoot),
|
|
365
|
+
collectFiles(channelsRoot)
|
|
366
|
+
]);
|
|
367
|
+
const [policyFiles, abilityFiles] = await Promise.all([
|
|
368
|
+
collectFiles(policiesRoot),
|
|
369
|
+
collectFiles(abilitiesRoot)
|
|
370
|
+
]);
|
|
371
|
+
const models = [];
|
|
372
|
+
for (const filePath of modelFiles) {
|
|
373
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
374
|
+
try {
|
|
375
|
+
const moduleValue = await importProjectModule(projectRoot, filePath);
|
|
376
|
+
const model = resolveNamedExport(moduleValue, isCliModelReference);
|
|
377
|
+
if (!model) {
|
|
378
|
+
if (isInactiveGeneratedModelModule(moduleValue)) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
throw new Error(`Discovered model "${relativePath}" does not export a Holo model.`);
|
|
382
|
+
}
|
|
383
|
+
models.push({
|
|
384
|
+
sourcePath: relativePath,
|
|
385
|
+
name: model.definition.name,
|
|
386
|
+
prunable: Boolean(model.definition.prunable)
|
|
387
|
+
});
|
|
388
|
+
} catch (error) {
|
|
389
|
+
if (!isMissingGeneratedSchemaModelError(error)) {
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
assertUniqueEntries("model", models);
|
|
395
|
+
const migrations = [];
|
|
396
|
+
for (const filePath of migrationFiles) {
|
|
397
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
398
|
+
const moduleValue = await importProjectModule(projectRoot, filePath);
|
|
399
|
+
const migration = resolveNamedExport(moduleValue, isMigrationDefinition);
|
|
400
|
+
if (!migration) {
|
|
401
|
+
throw new Error(`Discovered migration "${relativePath}" does not export a Holo migration.`);
|
|
402
|
+
}
|
|
403
|
+
migrations.push({
|
|
404
|
+
sourcePath: relativePath,
|
|
405
|
+
name: migration.name ? validateMigrationName(migration.name) : inferMigrationNameFromEntry(relativePath)
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
assertUniqueEntries("migration", migrations);
|
|
409
|
+
const seeders = [];
|
|
410
|
+
for (const filePath of seederFiles) {
|
|
411
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
412
|
+
const moduleValue = await importProjectModule(projectRoot, filePath);
|
|
413
|
+
const seeder = resolveNamedExport(moduleValue, isSeederDefinition);
|
|
414
|
+
if (!seeder) {
|
|
415
|
+
throw new Error(`Discovered seeder "${relativePath}" does not export a Holo seeder.`);
|
|
416
|
+
}
|
|
417
|
+
seeders.push({
|
|
418
|
+
sourcePath: relativePath,
|
|
419
|
+
name: seeder.name
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
assertUniqueEntries("seeder", seeders);
|
|
423
|
+
const commands = [];
|
|
424
|
+
for (const filePath of commandFiles) {
|
|
425
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
426
|
+
const moduleValue = await importProjectModule(projectRoot, filePath);
|
|
427
|
+
const command = resolveCommandExport(moduleValue);
|
|
428
|
+
if (!command) {
|
|
429
|
+
throw new Error(`Discovered command "${relativePath}" does not export a Holo command.`);
|
|
430
|
+
}
|
|
431
|
+
const aliases = normalizeCommandAliases(command.aliases) ?? [];
|
|
432
|
+
commands.push({
|
|
433
|
+
sourcePath: relativePath,
|
|
434
|
+
name: command.name?.trim() || deriveCommandNameFromPath(commandsRoot, filePath),
|
|
435
|
+
aliases,
|
|
436
|
+
description: command.description,
|
|
437
|
+
...command.usage ? { usage: command.usage } : {}
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
assertUniqueEntries("command", commands);
|
|
441
|
+
assertUniqueCommandTokens(commands);
|
|
442
|
+
const jobs = [];
|
|
443
|
+
const queueDiscovery = jobFiles.length > 0 ? await loadQueueDiscoveryModule(projectRoot) : void 0;
|
|
444
|
+
for (const filePath of jobFiles) {
|
|
445
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
446
|
+
const moduleValue = await importProjectModule(projectRoot, filePath);
|
|
447
|
+
const exportedJob = resolveNamedExportEntry(
|
|
448
|
+
moduleValue,
|
|
449
|
+
(value) => queueDiscovery.isQueueJobDefinition(value)
|
|
450
|
+
);
|
|
451
|
+
if (!exportedJob) {
|
|
452
|
+
throw new Error(`Discovered job "${relativePath}" does not export a Holo job.`);
|
|
453
|
+
}
|
|
454
|
+
const normalizedJob = queueDiscovery.normalizeQueueJobDefinition(exportedJob.value);
|
|
455
|
+
jobs.push({
|
|
456
|
+
...resolveDiscoveredJobMetadata(
|
|
457
|
+
normalizedJob,
|
|
458
|
+
relativePath,
|
|
459
|
+
deriveJobNameFromPath(jobsRoot, filePath),
|
|
460
|
+
loadedConfig.queue
|
|
461
|
+
),
|
|
462
|
+
exportName: exportedJob.exportName
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
assertUniqueEntries("job", jobs);
|
|
466
|
+
const events = [];
|
|
467
|
+
const eventsDiscovery = eventFiles.length > 0 || listenerFiles.length > 0 ? await loadEventsDiscoveryModule(projectRoot) : void 0;
|
|
468
|
+
const eventNamesByReference = /* @__PURE__ */ new Map();
|
|
469
|
+
const discoveredEventNamesBySourcePath = /* @__PURE__ */ new Map();
|
|
470
|
+
for (const filePath of eventFiles) {
|
|
471
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
472
|
+
const exportedEvent = resolveNamedExportEntry(
|
|
473
|
+
await importProjectModule(projectRoot, filePath),
|
|
474
|
+
(value) => hasEventDefinitionMarker(value)
|
|
475
|
+
);
|
|
476
|
+
if (!exportedEvent || !eventsDiscovery.isEventDefinition(exportedEvent.value)) {
|
|
477
|
+
throw new Error(`Discovered event "${relativePath}" does not export a Holo event.`);
|
|
478
|
+
}
|
|
479
|
+
const normalizedEvent = eventsDiscovery.normalizeEventDefinition(exportedEvent.value);
|
|
480
|
+
const name = normalizedEvent.name?.trim() || deriveEventNameFromPath(eventsRoot, filePath);
|
|
481
|
+
eventNamesByReference.set(exportedEvent.value, name);
|
|
482
|
+
discoveredEventNamesBySourcePath.set(relativePath, name);
|
|
483
|
+
events.push({
|
|
484
|
+
sourcePath: relativePath,
|
|
485
|
+
name,
|
|
486
|
+
exportName: exportedEvent.exportName
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
assertUniqueEntries("event", events);
|
|
490
|
+
const discoveredEventNames = new Set(events.map((entry) => entry.name));
|
|
491
|
+
const listeners = [];
|
|
492
|
+
for (const filePath of listenerFiles) {
|
|
493
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
494
|
+
const exportedListener = resolveNamedExportEntry(
|
|
495
|
+
await importProjectModule(projectRoot, filePath),
|
|
496
|
+
(value) => hasListenerDefinitionMarker(value)
|
|
497
|
+
);
|
|
498
|
+
if (!exportedListener || !eventsDiscovery.isListenerDefinition(exportedListener.value)) {
|
|
499
|
+
throw new Error(`Discovered listener "${relativePath}" does not export a Holo listener.`);
|
|
500
|
+
}
|
|
501
|
+
let eventNames;
|
|
502
|
+
try {
|
|
503
|
+
eventNames = resolveListenerEventNamesForDiscovery(
|
|
504
|
+
exportedListener.value,
|
|
505
|
+
eventNamesByReference
|
|
506
|
+
);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
if (!(error instanceof Error) || error.message !== "[Holo Events] Listener event references must resolve to explicit event names before discovery registration.") {
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
eventNames = await resolveListenerEventNamesFromSource(projectRoot, filePath, discoveredEventNamesBySourcePath);
|
|
512
|
+
}
|
|
513
|
+
const normalizedListener = eventsDiscovery.normalizeListenerDefinition(exportedListener.value);
|
|
514
|
+
const listenerId = normalizedListener.name?.trim() || deriveListenerIdFromPath(listenersRoot, filePath);
|
|
515
|
+
for (const eventName of eventNames) {
|
|
516
|
+
if (!discoveredEventNames.has(eventName)) {
|
|
517
|
+
throw new Error(`Listener "${listenerId}" references unknown event "${eventName}".`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
listeners.push({
|
|
521
|
+
sourcePath: relativePath,
|
|
522
|
+
id: listenerId,
|
|
523
|
+
eventNames,
|
|
524
|
+
exportName: exportedListener.exportName
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
assertUniqueEntries("listener", listeners.map((entry) => ({
|
|
528
|
+
name: entry.id,
|
|
529
|
+
sourcePath: entry.sourcePath
|
|
530
|
+
})));
|
|
531
|
+
listeners.sort((left, right) => left.id.localeCompare(right.id));
|
|
532
|
+
const broadcastDiscovery = broadcastFiles.length > 0 || channelFiles.length > 0 ? await loadBroadcastDiscoveryModule(projectRoot) : void 0;
|
|
533
|
+
const broadcast = [];
|
|
534
|
+
for (const filePath of broadcastFiles) {
|
|
535
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
536
|
+
const exportedBroadcast = resolveNamedExportEntry(
|
|
537
|
+
await importProjectModule(projectRoot, filePath),
|
|
538
|
+
(value) => broadcastDiscovery.isBroadcastDefinition(value)
|
|
539
|
+
);
|
|
540
|
+
if (!exportedBroadcast) {
|
|
541
|
+
throw new Error(`Discovered broadcast "${relativePath}" does not export a Holo broadcast definition.`);
|
|
542
|
+
}
|
|
543
|
+
const normalizedBroadcast = exportedBroadcast.value;
|
|
544
|
+
broadcast.push({
|
|
545
|
+
sourcePath: relativePath,
|
|
546
|
+
name: normalizedBroadcast.name?.trim() || deriveBroadcastNameFromPath(broadcastRoot, filePath),
|
|
547
|
+
exportName: exportedBroadcast.exportName,
|
|
548
|
+
channels: normalizedBroadcast.channels.map((channel) => ({
|
|
549
|
+
type: channel.type,
|
|
550
|
+
pattern: channel.pattern
|
|
551
|
+
}))
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
assertUniqueEntries("broadcast", broadcast);
|
|
555
|
+
broadcast.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
|
|
556
|
+
const channels = [];
|
|
557
|
+
for (const filePath of channelFiles) {
|
|
558
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
559
|
+
const exportedChannel = resolveNamedExportEntry(
|
|
560
|
+
await importProjectModule(projectRoot, filePath),
|
|
561
|
+
(value) => broadcastDiscovery.isChannelDefinition(value)
|
|
562
|
+
);
|
|
563
|
+
if (!exportedChannel) {
|
|
564
|
+
throw new Error(`Discovered channel "${relativePath}" does not export a Holo channel definition.`);
|
|
565
|
+
}
|
|
566
|
+
const normalizedChannel = exportedChannel.value;
|
|
567
|
+
const pattern = normalizedChannel.pattern || deriveChannelPatternFromPath(channelsRoot, filePath);
|
|
568
|
+
channels.push({
|
|
569
|
+
sourcePath: relativePath,
|
|
570
|
+
pattern,
|
|
571
|
+
exportName: exportedChannel.exportName,
|
|
572
|
+
type: normalizedChannel.type,
|
|
573
|
+
params: broadcastDiscovery.broadcastInternals.extractChannelPatternParamNames(pattern),
|
|
574
|
+
whispers: Object.freeze(Object.keys(normalizedChannel.whispers))
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
assertUniqueEntries("channel", channels.map((entry) => ({
|
|
578
|
+
name: entry.pattern,
|
|
579
|
+
sourcePath: entry.sourcePath
|
|
580
|
+
})));
|
|
581
|
+
channels.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
|
|
582
|
+
const authorizationDiscovery = policyFiles.length > 0 || abilityFiles.length > 0 ? await loadAuthorizationDiscoveryModule(projectRoot) : void 0;
|
|
583
|
+
const authorizationPolicies = [];
|
|
584
|
+
for (const filePath of policyFiles) {
|
|
585
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
586
|
+
const authorizationStateBeforeImport = authorizationDiscovery.authorizationInternals.getAuthorizationRuntimeState();
|
|
587
|
+
const existingPolicyNames = captureAuthorizationDefinitionNames(authorizationStateBeforeImport.policiesByName);
|
|
588
|
+
const existingAbilityNames = captureAuthorizationDefinitionNames(authorizationStateBeforeImport.abilitiesByName);
|
|
589
|
+
try {
|
|
590
|
+
const exportedPolicy = resolveNamedExportEntry(
|
|
591
|
+
await importProjectModule(projectRoot, filePath),
|
|
592
|
+
(value) => authorizationDiscovery.isAuthorizationPolicyDefinition(value)
|
|
593
|
+
);
|
|
594
|
+
if (!exportedPolicy) {
|
|
595
|
+
throw new Error(`Discovered policy "${relativePath}" does not export a Holo policy.`);
|
|
596
|
+
}
|
|
597
|
+
const authorizationStateAfterImport = authorizationDiscovery.authorizationInternals.getAuthorizationRuntimeState();
|
|
598
|
+
const addedPolicyNames = findAddedAuthorizationDefinitionNames(authorizationStateAfterImport.policiesByName, existingPolicyNames);
|
|
599
|
+
const addedAbilityNames = findAddedAuthorizationDefinitionNames(authorizationStateAfterImport.abilitiesByName, existingAbilityNames);
|
|
600
|
+
if (addedPolicyNames.length !== 1 || addedAbilityNames.length !== 0) {
|
|
601
|
+
throw new Error(
|
|
602
|
+
`Discovered policy "${relativePath}" must register exactly one Holo policy and zero Holo abilities (found ${addedPolicyNames.length} policies and ${addedAbilityNames.length} abilities).`
|
|
603
|
+
);
|
|
604
|
+
}
|
|
605
|
+
const normalizedPolicy = exportedPolicy.value;
|
|
606
|
+
authorizationPolicies.push({
|
|
607
|
+
sourcePath: relativePath,
|
|
608
|
+
name: normalizedPolicy.name.trim(),
|
|
609
|
+
exportName: exportedPolicy.exportName,
|
|
610
|
+
target: resolveAuthorizationTargetName(normalizedPolicy.target) ?? "Object",
|
|
611
|
+
classActions: Object.freeze(Object.keys(normalizedPolicy.class ?? {})),
|
|
612
|
+
recordActions: Object.freeze(Object.keys(normalizedPolicy.record ?? {}))
|
|
613
|
+
});
|
|
614
|
+
} finally {
|
|
615
|
+
const authorizationStateAfterDiscovery = authorizationDiscovery.authorizationInternals.getAuthorizationRuntimeState();
|
|
616
|
+
unregisterAuthorizationDefinitionNames(
|
|
617
|
+
authorizationDiscovery,
|
|
618
|
+
findAddedAuthorizationDefinitionNames(authorizationStateAfterDiscovery.policiesByName, existingPolicyNames),
|
|
619
|
+
findAddedAuthorizationDefinitionNames(authorizationStateAfterDiscovery.abilitiesByName, existingAbilityNames)
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
assertUniqueEntries("policy", authorizationPolicies.map((entry) => ({
|
|
624
|
+
name: entry.name,
|
|
625
|
+
sourcePath: entry.sourcePath
|
|
626
|
+
})));
|
|
627
|
+
authorizationPolicies.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
|
|
628
|
+
const authorizationAbilities = [];
|
|
629
|
+
for (const filePath of abilityFiles) {
|
|
630
|
+
const relativePath = makeProjectRelativePath(projectRoot, filePath);
|
|
631
|
+
const authorizationStateBeforeImport = authorizationDiscovery.authorizationInternals.getAuthorizationRuntimeState();
|
|
632
|
+
const existingPolicyNames = captureAuthorizationDefinitionNames(authorizationStateBeforeImport.policiesByName);
|
|
633
|
+
const existingAbilityNames = captureAuthorizationDefinitionNames(authorizationStateBeforeImport.abilitiesByName);
|
|
634
|
+
try {
|
|
635
|
+
const exportedAbility = resolveNamedExportEntry(
|
|
636
|
+
await importProjectModule(projectRoot, filePath),
|
|
637
|
+
(value) => authorizationDiscovery.isAuthorizationAbilityDefinition(value)
|
|
638
|
+
);
|
|
639
|
+
if (!exportedAbility) {
|
|
640
|
+
throw new Error(`Discovered ability "${relativePath}" does not export a Holo ability.`);
|
|
641
|
+
}
|
|
642
|
+
const authorizationStateAfterImport = authorizationDiscovery.authorizationInternals.getAuthorizationRuntimeState();
|
|
643
|
+
const addedPolicyNames = findAddedAuthorizationDefinitionNames(authorizationStateAfterImport.policiesByName, existingPolicyNames);
|
|
644
|
+
const addedAbilityNames = findAddedAuthorizationDefinitionNames(authorizationStateAfterImport.abilitiesByName, existingAbilityNames);
|
|
645
|
+
if (addedPolicyNames.length !== 0 || addedAbilityNames.length !== 1) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`Discovered ability "${relativePath}" must register exactly one Holo ability and zero Holo policies (found ${addedPolicyNames.length} policies and ${addedAbilityNames.length} abilities).`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
const normalizedAbility = exportedAbility.value;
|
|
651
|
+
authorizationAbilities.push({
|
|
652
|
+
sourcePath: relativePath,
|
|
653
|
+
name: normalizedAbility.name.trim(),
|
|
654
|
+
exportName: exportedAbility.exportName
|
|
655
|
+
});
|
|
656
|
+
} finally {
|
|
657
|
+
const authorizationStateAfterDiscovery = authorizationDiscovery.authorizationInternals.getAuthorizationRuntimeState();
|
|
658
|
+
unregisterAuthorizationDefinitionNames(
|
|
659
|
+
authorizationDiscovery,
|
|
660
|
+
findAddedAuthorizationDefinitionNames(authorizationStateAfterDiscovery.policiesByName, existingPolicyNames),
|
|
661
|
+
findAddedAuthorizationDefinitionNames(authorizationStateAfterDiscovery.abilitiesByName, existingAbilityNames)
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
assertUniqueEntries("ability", authorizationAbilities.map((entry) => ({
|
|
666
|
+
name: entry.name,
|
|
667
|
+
sourcePath: entry.sourcePath
|
|
668
|
+
})));
|
|
669
|
+
authorizationAbilities.sort((left, right) => left.sourcePath.localeCompare(right.sourcePath));
|
|
670
|
+
const registry = {
|
|
671
|
+
version: 1,
|
|
672
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
673
|
+
paths: {
|
|
674
|
+
models: config.paths.models,
|
|
675
|
+
migrations: config.paths.migrations,
|
|
676
|
+
seeders: config.paths.seeders,
|
|
677
|
+
commands: config.paths.commands,
|
|
678
|
+
jobs: config.paths.jobs,
|
|
679
|
+
events: config.paths.events,
|
|
680
|
+
listeners: config.paths.listeners,
|
|
681
|
+
broadcast: broadcastPath,
|
|
682
|
+
channels: channelsPath,
|
|
683
|
+
authorizationPolicies: config.paths.authorizationPolicies ?? "server/policies",
|
|
684
|
+
authorizationAbilities: config.paths.authorizationAbilities ?? "server/abilities",
|
|
685
|
+
generatedSchema: config.paths.generatedSchema
|
|
686
|
+
},
|
|
687
|
+
models,
|
|
688
|
+
migrations,
|
|
689
|
+
seeders,
|
|
690
|
+
commands,
|
|
691
|
+
jobs,
|
|
692
|
+
events,
|
|
693
|
+
listeners,
|
|
694
|
+
broadcast,
|
|
695
|
+
channels,
|
|
696
|
+
authorizationPolicies,
|
|
697
|
+
authorizationAbilities
|
|
698
|
+
};
|
|
699
|
+
await writeGeneratedProjectRegistry(projectRoot, registry);
|
|
700
|
+
return registry;
|
|
701
|
+
}
|
|
702
|
+
async function discoverAppCommands(projectRoot, config = normalizeHoloProjectConfig()) {
|
|
703
|
+
const registry = await loadGeneratedProjectRegistry(projectRoot) ?? await prepareProjectDiscovery(projectRoot, config);
|
|
704
|
+
return [...registry.commands].map((entry) => ({
|
|
705
|
+
sourcePath: entry.sourcePath,
|
|
706
|
+
name: entry.name,
|
|
707
|
+
aliases: entry.aliases,
|
|
708
|
+
description: entry.description,
|
|
709
|
+
...entry.usage ? { usage: entry.usage } : {},
|
|
710
|
+
async load() {
|
|
711
|
+
const moduleValue = await importProjectModule(projectRoot, resolve(projectRoot, entry.sourcePath));
|
|
712
|
+
const command = resolveCommandExport(moduleValue);
|
|
713
|
+
if (!command) {
|
|
714
|
+
throw new Error(`Discovered command "${entry.sourcePath}" does not export a Holo command.`);
|
|
715
|
+
}
|
|
716
|
+
return command;
|
|
717
|
+
}
|
|
718
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
719
|
+
}
|
|
720
|
+
async function loadRegisteredModels(projectRoot, config) {
|
|
721
|
+
const models = [];
|
|
722
|
+
for (const entry of config.models) {
|
|
723
|
+
const moduleValue = await importProjectModule(projectRoot, resolveRegisteredPath(projectRoot, entry));
|
|
724
|
+
const model = resolveNamedExport(moduleValue, isCliModelReference);
|
|
725
|
+
if (!model) {
|
|
726
|
+
throw new Error(`Registered model "${entry}" does not export a Holo model.`);
|
|
727
|
+
}
|
|
728
|
+
models.push(model);
|
|
729
|
+
}
|
|
730
|
+
return models;
|
|
731
|
+
}
|
|
732
|
+
async function loadRegisteredMigrations(projectRoot, config) {
|
|
733
|
+
const migrations = [];
|
|
734
|
+
for (const entry of config.migrations) {
|
|
735
|
+
const moduleValue = await importProjectModule(projectRoot, resolveRegisteredPath(projectRoot, entry));
|
|
736
|
+
const migration = resolveNamedExport(moduleValue, isMigrationDefinition);
|
|
737
|
+
if (!migration) {
|
|
738
|
+
throw new Error(`Registered migration "${entry}" does not export a Holo migration.`);
|
|
739
|
+
}
|
|
740
|
+
migrations.push({
|
|
741
|
+
...migration,
|
|
742
|
+
name: migration.name ? validateMigrationName(migration.name) : inferMigrationNameFromEntry(entry)
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
return migrations;
|
|
746
|
+
}
|
|
747
|
+
function inferMigrationNameFromEntry(entry) {
|
|
748
|
+
const fileName = basename(entry, extname(entry));
|
|
749
|
+
return validateMigrationName(
|
|
750
|
+
fileName,
|
|
751
|
+
`Registered migration "${entry}" must use a timestamped file name matching YYYY_MM_DD_HHMMSS_description.`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
function validateMigrationName(name, message) {
|
|
755
|
+
if (!MIGRATION_NAME_PATTERN.test(name)) {
|
|
756
|
+
throw new Error(
|
|
757
|
+
message ?? `Migration name "${name}" must match YYYY_MM_DD_HHMMSS_description.`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
return name;
|
|
761
|
+
}
|
|
762
|
+
async function loadRegisteredSeeders(projectRoot, config) {
|
|
763
|
+
const seeders = [];
|
|
764
|
+
for (const entry of config.seeders) {
|
|
765
|
+
const moduleValue = await importProjectModule(projectRoot, resolveRegisteredPath(projectRoot, entry));
|
|
766
|
+
const seeder = resolveNamedExport(moduleValue, isSeederDefinition);
|
|
767
|
+
if (!seeder) {
|
|
768
|
+
throw new Error(`Registered seeder "${entry}" does not export a Holo seeder.`);
|
|
769
|
+
}
|
|
770
|
+
seeders.push(seeder);
|
|
771
|
+
}
|
|
772
|
+
return seeders;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
export {
|
|
776
|
+
resolveNamedExport,
|
|
777
|
+
resolveNamedExportEntry,
|
|
778
|
+
resolveListenerEventNamesForDiscovery,
|
|
779
|
+
collectImportedBindingsBySource,
|
|
780
|
+
extractListensToItems,
|
|
781
|
+
resolveListenerEventNamesFromSource,
|
|
782
|
+
prepareProjectDiscovery,
|
|
783
|
+
discoverAppCommands,
|
|
784
|
+
loadRegisteredModels,
|
|
785
|
+
loadRegisteredMigrations,
|
|
786
|
+
loadRegisteredSeeders
|
|
787
|
+
};
|