@uns-kit/core 2.0.40 → 2.0.41
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/LICENSE +21 -21
- package/README.md +170 -147
- package/dist/index.d.ts +9 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -7
- package/dist/index.js.map +1 -1
- package/dist/tools/datahub/datahub-client.js.map +1 -1
- package/dist/tools/file-utils.d.ts +2 -2
- package/dist/tools/file-utils.js +24 -24
- package/dist/tools/generate-uns-dictionary.d.ts +23 -23
- package/dist/tools/generate-uns-dictionary.js +154 -154
- package/dist/tools/generate-uns-measurements.d.ts +21 -21
- package/dist/tools/generate-uns-measurements.js +126 -126
- package/dist/tools/generate-uns-reference.d.ts +2 -2
- package/dist/tools/generate-uns-reference.js +85 -85
- package/dist/tools/refresh-uns.js +50 -50
- package/dist/tools/sync-uns-schema.d.ts +2 -2
- package/dist/tools/sync-uns-schema.js +489 -489
- package/dist/uns/service-metadata.d.ts +53 -0
- package/dist/uns/service-metadata.d.ts.map +1 -0
- package/dist/uns/service-metadata.js +56 -0
- package/dist/uns/service-metadata.js.map +1 -0
- package/dist/uns/uns-asset.d.ts +1 -1
- package/dist/uns/uns-asset.js +1 -1
- package/dist/uns/uns-attributes.d.ts +6 -6
- package/dist/uns/uns-attributes.js +9 -9
- package/dist/uns/uns-dictionary-registry.d.ts +4 -4
- package/dist/uns/uns-dictionary-registry.js +20 -20
- package/dist/uns/uns-dictionary.generated.d.ts +663 -663
- package/dist/uns/uns-dictionary.generated.js +745 -745
- package/dist/uns/uns-interfaces.d.ts +365 -365
- package/dist/uns/uns-measurements.generated.d.ts +362 -362
- package/dist/uns/uns-measurements.generated.js +359 -359
- package/dist/uns/uns-object.d.ts +20 -20
- package/dist/uns/uns-object.js +9 -9
- package/dist/uns/uns-packet.js +263 -263
- package/dist/uns/uns-path.d.ts +2 -2
- package/dist/uns/uns-path.js +10 -10
- package/dist/uns/uns-proxy-process.d.ts +11 -0
- package/dist/uns/uns-proxy-process.d.ts.map +1 -1
- package/dist/uns/uns-proxy-process.js +224 -203
- package/dist/uns/uns-proxy-process.js.map +1 -1
- package/dist/uns/uns-proxy.d.ts +51 -51
- package/dist/uns/uns-proxy.js +190 -190
- package/dist/uns-config/uns-core-schema.d.ts +3276 -3276
- package/dist/uns-grpc/uns-gateway.proto +104 -104
- package/dist/uns-mqtt/mqtt-topic-builder.d.ts +6 -0
- package/dist/uns-mqtt/mqtt-topic-builder.d.ts.map +1 -1
- package/dist/uns-mqtt/mqtt-topic-builder.js +8 -0
- package/dist/uns-mqtt/mqtt-topic-builder.js.map +1 -1
- package/dist/uns-mqtt/uns-mqtt-proxy.js +477 -477
- package/package.json +1 -1
- package/dist/app-config.d.ts.map +0 -1
- package/dist/app-config.js.map +0 -1
|
@@ -1,490 +1,490 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { access, readFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import process from "node:process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { readTextFileIfExists, writeTextFileIfChanged } from "./file-utils.js";
|
|
7
|
-
import { renderDictionaryTs } from "./generate-uns-dictionary.js";
|
|
8
|
-
import { renderMeasurementsTs } from "./generate-uns-measurements.js";
|
|
9
|
-
const DEFAULT_STATUS = "active";
|
|
10
|
-
const GENERATOR_LANG = "sl";
|
|
11
|
-
const STATUS_VALUES = ["active", "draft", "deprecated", "all"];
|
|
12
|
-
const isDirectExecution = process.argv[1]
|
|
13
|
-
? path.resolve(process.argv[1]).endsWith(path.basename(fileURLToPath(import.meta.url)))
|
|
14
|
-
: false;
|
|
15
|
-
async function main() {
|
|
16
|
-
const args = await parseArgs(process.argv.slice(2));
|
|
17
|
-
if (args.help) {
|
|
18
|
-
printHelp();
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
const target = await resolveSyncTarget(args);
|
|
22
|
-
const shouldSyncDictionary = !args.measurementsOnly;
|
|
23
|
-
const shouldSyncMeasurements = !args.dictionaryOnly;
|
|
24
|
-
const [dictionaryDocument, measurementsDocument] = await Promise.all([
|
|
25
|
-
shouldSyncDictionary ? fetchDictionaryDocument(args.controllerUrl, args.token, args.status) : Promise.resolve(undefined),
|
|
26
|
-
shouldSyncMeasurements ? fetchMeasurementsDocument(args.controllerUrl, args.token) : Promise.resolve(undefined),
|
|
27
|
-
]);
|
|
28
|
-
let dictionarySummary;
|
|
29
|
-
let measurementsSummary;
|
|
30
|
-
if (dictionaryDocument) {
|
|
31
|
-
const content = formatJsonDocument(dictionaryDocument);
|
|
32
|
-
const jsonResult = await updateFile(target.dictionaryJson, content, target.rootDir, args.dryRun);
|
|
33
|
-
const generatedChanged = args.skipGenerate
|
|
34
|
-
? []
|
|
35
|
-
: await updateGeneratedFiles(target.dictionaryGenerated, renderDictionaryTs(dictionaryDocument, GENERATOR_LANG), target.rootDir, args.dryRun);
|
|
36
|
-
dictionarySummary = {
|
|
37
|
-
objectTypeCount: Object.keys(dictionaryDocument.objectTypes ?? {}).length,
|
|
38
|
-
attributeCount: Object.keys(dictionaryDocument.attributes ?? {}).length,
|
|
39
|
-
jsonChanged: jsonResult.changed,
|
|
40
|
-
generatedChanged,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
if (measurementsDocument) {
|
|
44
|
-
const content = formatJsonDocument(measurementsDocument);
|
|
45
|
-
const jsonResult = await updateFile(target.measurementsJson, content, target.rootDir, args.dryRun);
|
|
46
|
-
const generatedChanged = args.skipGenerate
|
|
47
|
-
? []
|
|
48
|
-
: await updateGeneratedFiles(target.measurementsGenerated, renderMeasurementsTs(measurementsDocument, GENERATOR_LANG), target.rootDir, args.dryRun);
|
|
49
|
-
measurementsSummary = {
|
|
50
|
-
categoryCount: countMeasurementCategories(measurementsDocument),
|
|
51
|
-
jsonChanged: jsonResult.changed,
|
|
52
|
-
generatedChanged,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
printSummary({
|
|
56
|
-
controllerUrl: args.controllerUrl,
|
|
57
|
-
targetLabel: target.label,
|
|
58
|
-
targetRoot: target.rootDir,
|
|
59
|
-
dryRun: args.dryRun,
|
|
60
|
-
skipGenerate: args.skipGenerate,
|
|
61
|
-
status: args.status,
|
|
62
|
-
dictionary: dictionarySummary,
|
|
63
|
-
measurements: measurementsSummary,
|
|
64
|
-
});
|
|
65
|
-
}
|
|
66
|
-
async function getControllerUrlFromConfig() {
|
|
67
|
-
const currentDir = process.cwd();
|
|
68
|
-
const configPath = path.join(currentDir, "config.json");
|
|
69
|
-
try {
|
|
70
|
-
const raw = await readFile(configPath, "utf8");
|
|
71
|
-
const config = JSON.parse(raw);
|
|
72
|
-
const unsConfig = config.uns;
|
|
73
|
-
const restUrl = unsConfig?.rest;
|
|
74
|
-
if (restUrl && typeof restUrl === "string") {
|
|
75
|
-
// Remove trailing /api from the rest URL to get the base controller URL
|
|
76
|
-
return restUrl.endsWith("/api") ? restUrl.slice(0, -4) : restUrl;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
catch (error) {
|
|
80
|
-
// Ignore missing or invalid config.json
|
|
81
|
-
}
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
async function promptForToken() {
|
|
85
|
-
return new Promise((resolve) => {
|
|
86
|
-
const stdin = process.stdin;
|
|
87
|
-
const wasRaw = stdin.isRaw;
|
|
88
|
-
stdin.setRawMode(true);
|
|
89
|
-
stdin.resume();
|
|
90
|
-
process.stderr.write("Enter controller token: ");
|
|
91
|
-
let token = "";
|
|
92
|
-
stdin.on("data", onChar);
|
|
93
|
-
function onChar(char) {
|
|
94
|
-
const code = char[0];
|
|
95
|
-
// Enter (13) or newline (10) — done
|
|
96
|
-
if (code === 13 || code === 10) {
|
|
97
|
-
stdin.removeListener("data", onChar);
|
|
98
|
-
stdin.setRawMode(wasRaw ?? false);
|
|
99
|
-
stdin.pause();
|
|
100
|
-
process.stderr.write("\n");
|
|
101
|
-
resolve(token);
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
// Ctrl+C — abort
|
|
105
|
-
if (code === 3) {
|
|
106
|
-
stdin.removeListener("data", onChar);
|
|
107
|
-
stdin.setRawMode(wasRaw ?? false);
|
|
108
|
-
process.stderr.write("\n");
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
111
|
-
// Backspace (127) or Delete (8)
|
|
112
|
-
if (code === 127 || code === 8) {
|
|
113
|
-
token = token.slice(0, -1);
|
|
114
|
-
}
|
|
115
|
-
else if (code >= 32) {
|
|
116
|
-
token += char.toString("utf8");
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
async function parseArgs(argv) {
|
|
122
|
-
let controllerUrl = process.env.UNS_CONTROLLER_URL?.trim() ?? "";
|
|
123
|
-
let token = process.env.UNS_CONTROLLER_TOKEN?.trim() ?? "";
|
|
124
|
-
let status = normalizeStatus(process.env.UNS_SCHEMA_STATUS, DEFAULT_STATUS);
|
|
125
|
-
let projectRoot = process.env.UNS_SCHEMA_PROJECT_ROOT?.trim() || undefined;
|
|
126
|
-
let dryRun = false;
|
|
127
|
-
let dictionaryOnly = false;
|
|
128
|
-
let measurementsOnly = false;
|
|
129
|
-
let skipGenerate = false;
|
|
130
|
-
let help = false;
|
|
131
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
132
|
-
const arg = argv[index];
|
|
133
|
-
if (arg === "--help" || arg === "-h") {
|
|
134
|
-
help = true;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (arg === "--dry-run") {
|
|
138
|
-
dryRun = true;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (arg === "--dictionary-only") {
|
|
142
|
-
dictionaryOnly = true;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
if (arg === "--measurements-only") {
|
|
146
|
-
measurementsOnly = true;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
if (arg === "--skip-generate") {
|
|
150
|
-
skipGenerate = true;
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (arg === "--project-root") {
|
|
154
|
-
projectRoot = readRequiredValue(argv, ++index, "--project-root");
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (arg.startsWith("--project-root=")) {
|
|
158
|
-
projectRoot = arg.slice("--project-root=".length);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (arg === "--controller-url") {
|
|
162
|
-
controllerUrl = readRequiredValue(argv, ++index, "--controller-url");
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (arg.startsWith("--controller-url=")) {
|
|
166
|
-
controllerUrl = arg.slice("--controller-url=".length);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (arg === "--token") {
|
|
170
|
-
token = readRequiredValue(argv, ++index, "--token");
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
if (arg.startsWith("--token=")) {
|
|
174
|
-
token = arg.slice("--token=".length);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
if (arg === "--status") {
|
|
178
|
-
status = normalizeStatus(readRequiredValue(argv, ++index, "--status"), DEFAULT_STATUS);
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (arg.startsWith("--status=")) {
|
|
182
|
-
status = normalizeStatus(arg.slice("--status=".length), DEFAULT_STATUS);
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
throw new Error(`Unknown argument: ${arg}`);
|
|
186
|
-
}
|
|
187
|
-
// Try to load controller URL from config.json if not provided
|
|
188
|
-
if (!controllerUrl) {
|
|
189
|
-
const configUrl = await getControllerUrlFromConfig();
|
|
190
|
-
if (configUrl) {
|
|
191
|
-
controllerUrl = configUrl;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
if (!help) {
|
|
195
|
-
if (dictionaryOnly && measurementsOnly) {
|
|
196
|
-
throw new Error("Choose either --dictionary-only or --measurements-only, not both.");
|
|
197
|
-
}
|
|
198
|
-
if (!controllerUrl) {
|
|
199
|
-
throw new Error("Missing controller URL. Use --controller-url, set UNS_CONTROLLER_URL, or provide config.json with uns.rest.");
|
|
200
|
-
}
|
|
201
|
-
if (!token) {
|
|
202
|
-
// Prompt for token if stdin is a TTY (interactive terminal)
|
|
203
|
-
if (process.stdin.isTTY) {
|
|
204
|
-
token = await promptForToken();
|
|
205
|
-
}
|
|
206
|
-
if (!token) {
|
|
207
|
-
throw new Error("Missing controller token. Use --token, set UNS_CONTROLLER_TOKEN, or provide it interactively.");
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
// Strip "Bearer " prefix if the user included it
|
|
211
|
-
if (token.toLowerCase().startsWith("bearer ")) {
|
|
212
|
-
token = token.slice(7).trim();
|
|
213
|
-
}
|
|
214
|
-
assertValidUrl(controllerUrl);
|
|
215
|
-
}
|
|
216
|
-
return {
|
|
217
|
-
controllerUrl,
|
|
218
|
-
token,
|
|
219
|
-
status,
|
|
220
|
-
projectRoot,
|
|
221
|
-
dryRun,
|
|
222
|
-
dictionaryOnly,
|
|
223
|
-
measurementsOnly,
|
|
224
|
-
skipGenerate,
|
|
225
|
-
help,
|
|
226
|
-
};
|
|
227
|
-
}
|
|
228
|
-
function readRequiredValue(argv, index, flag) {
|
|
229
|
-
const value = argv[index];
|
|
230
|
-
if (!value) {
|
|
231
|
-
throw new Error(`Missing value for ${flag}.`);
|
|
232
|
-
}
|
|
233
|
-
return value;
|
|
234
|
-
}
|
|
235
|
-
function normalizeStatus(value, fallback) {
|
|
236
|
-
if (!value)
|
|
237
|
-
return fallback;
|
|
238
|
-
const normalized = value.trim().toLowerCase();
|
|
239
|
-
if (STATUS_VALUES.includes(normalized)) {
|
|
240
|
-
return normalized;
|
|
241
|
-
}
|
|
242
|
-
throw new Error(`Invalid --status value "${value}". Expected one of: ${STATUS_VALUES.join(", ")}.`);
|
|
243
|
-
}
|
|
244
|
-
function assertValidUrl(value) {
|
|
245
|
-
try {
|
|
246
|
-
new URL(value);
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
throw new Error(`Invalid controller URL "${value}". Expected an absolute URL such as http://localhost:3200.`);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
function printHelp() {
|
|
253
|
-
console.log(`Usage: tsx packages/uns-core/src/tools/sync-uns-schema.ts [options]
|
|
254
|
-
|
|
255
|
-
Pull the canonical UNS schema export from a controller and refresh the local
|
|
256
|
-
JSON files plus generated TypeScript artifacts.
|
|
257
|
-
|
|
258
|
-
Options:
|
|
259
|
-
--controller-url <url> Controller base URL (env: UNS_CONTROLLER_URL, or config.json > uns.rest)
|
|
260
|
-
--token <token> Bearer token for REST export (env: UNS_CONTROLLER_TOKEN)
|
|
261
|
-
--status <value> Dictionary status filter: ${STATUS_VALUES.join("|")} (default: ${DEFAULT_STATUS}, env: UNS_SCHEMA_STATUS)
|
|
262
|
-
--project-root <dir> Write into a generated microservice project root (env: UNS_SCHEMA_PROJECT_ROOT).
|
|
263
|
-
When omitted, the tool auto-detects a generated project from the current working directory
|
|
264
|
-
and otherwise updates the uns-kit repo templates.
|
|
265
|
-
--dry-run Report file changes without writing anything
|
|
266
|
-
--dictionary-only Sync only the UNS dictionary export
|
|
267
|
-
--measurements-only Sync only the UNS measurements export
|
|
268
|
-
--skip-generate Skip generated TypeScript refresh
|
|
269
|
-
--help, -h Show this help
|
|
270
|
-
`);
|
|
271
|
-
}
|
|
272
|
-
async function findRepoRoot() {
|
|
273
|
-
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
274
|
-
let probeDir = currentDir;
|
|
275
|
-
while (true) {
|
|
276
|
-
const packageJsonPath = path.join(probeDir, "package.json");
|
|
277
|
-
try {
|
|
278
|
-
const raw = await readFile(packageJsonPath, "utf8");
|
|
279
|
-
const pkg = JSON.parse(raw);
|
|
280
|
-
if (pkg.name === "@uns-kit/core") {
|
|
281
|
-
return path.resolve(probeDir, "../..");
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
catch (error) {
|
|
285
|
-
// Ignore missing package.json and continue walking upward.
|
|
286
|
-
}
|
|
287
|
-
const parentDir = path.dirname(probeDir);
|
|
288
|
-
if (parentDir === probeDir) {
|
|
289
|
-
break;
|
|
290
|
-
}
|
|
291
|
-
probeDir = parentDir;
|
|
292
|
-
}
|
|
293
|
-
throw new Error("Could not locate the @uns-kit/core package directory from sync-uns-schema.ts.");
|
|
294
|
-
}
|
|
295
|
-
async function resolveSyncTarget(args) {
|
|
296
|
-
if (args.projectRoot) {
|
|
297
|
-
return buildProjectTarget(path.resolve(process.cwd(), args.projectRoot));
|
|
298
|
-
}
|
|
299
|
-
const currentDir = process.cwd();
|
|
300
|
-
if (await looksLikeGeneratedProjectRoot(currentDir)) {
|
|
301
|
-
return buildProjectTarget(currentDir);
|
|
302
|
-
}
|
|
303
|
-
return buildRepoTarget(await findRepoRoot());
|
|
304
|
-
}
|
|
305
|
-
function buildRepoTarget(repoRoot) {
|
|
306
|
-
return {
|
|
307
|
-
rootDir: repoRoot,
|
|
308
|
-
label: "uns-kit repo templates",
|
|
309
|
-
dictionaryJson: path.join(repoRoot, "packages/uns-cli/templates/uns-dictionary/uns-dictionary.json"),
|
|
310
|
-
measurementsJson: path.join(repoRoot, "packages/uns-cli/templates/uns-measurements/uns-measurements.json"),
|
|
311
|
-
dictionaryGenerated: [
|
|
312
|
-
path.join(repoRoot, "packages/uns-core/src/uns/uns-dictionary.generated.ts"),
|
|
313
|
-
path.join(repoRoot, "packages/uns-cli/templates/default/src/uns/uns-dictionary.generated.ts"),
|
|
314
|
-
],
|
|
315
|
-
measurementsGenerated: [
|
|
316
|
-
path.join(repoRoot, "packages/uns-core/src/uns/uns-measurements.generated.ts"),
|
|
317
|
-
path.join(repoRoot, "packages/uns-cli/templates/default/src/uns/uns-measurements.generated.ts"),
|
|
318
|
-
],
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function buildProjectTarget(projectRoot) {
|
|
322
|
-
return {
|
|
323
|
-
rootDir: projectRoot,
|
|
324
|
-
label: "generated microservice project",
|
|
325
|
-
dictionaryJson: path.join(projectRoot, "uns-dictionary.json"),
|
|
326
|
-
measurementsJson: path.join(projectRoot, "uns-measurements.json"),
|
|
327
|
-
dictionaryGenerated: [path.join(projectRoot, "src/uns/uns-dictionary.generated.ts")],
|
|
328
|
-
measurementsGenerated: [path.join(projectRoot, "src/uns/uns-measurements.generated.ts")],
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
async function looksLikeGeneratedProjectRoot(rootDir) {
|
|
332
|
-
const [hasPackageJson, hasConfigJson, hasUnsDir] = await Promise.all([
|
|
333
|
-
pathExists(path.join(rootDir, "package.json")),
|
|
334
|
-
pathExists(path.join(rootDir, "config.json")),
|
|
335
|
-
pathExists(path.join(rootDir, "src/uns")),
|
|
336
|
-
]);
|
|
337
|
-
return hasPackageJson && hasConfigJson && hasUnsDir;
|
|
338
|
-
}
|
|
339
|
-
async function pathExists(targetPath) {
|
|
340
|
-
try {
|
|
341
|
-
await access(targetPath);
|
|
342
|
-
return true;
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
function buildControllerUrl(controllerUrl, relativePath) {
|
|
349
|
-
const base = new URL(controllerUrl);
|
|
350
|
-
if (!base.pathname.endsWith("/")) {
|
|
351
|
-
base.pathname = `${base.pathname}/`;
|
|
352
|
-
}
|
|
353
|
-
return new URL(relativePath, base);
|
|
354
|
-
}
|
|
355
|
-
async function fetchDictionaryDocument(controllerUrl, token, status) {
|
|
356
|
-
const url = buildControllerUrl(controllerUrl, `api/schema/export/uns-dictionary?status=${encodeURIComponent(status)}`);
|
|
357
|
-
const document = await fetchJson(url, token, "UNS dictionary");
|
|
358
|
-
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
359
|
-
throw new Error(`Controller returned an invalid UNS dictionary document from ${url.toString()}.`);
|
|
360
|
-
}
|
|
361
|
-
return document;
|
|
362
|
-
}
|
|
363
|
-
async function fetchMeasurementsDocument(controllerUrl, token) {
|
|
364
|
-
const url = buildControllerUrl(controllerUrl, "api/schema/export/uns-measurements");
|
|
365
|
-
const document = await fetchJson(url, token, "UNS measurements");
|
|
366
|
-
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
367
|
-
throw new Error(`Controller returned an invalid UNS measurements document from ${url.toString()}.`);
|
|
368
|
-
}
|
|
369
|
-
return document;
|
|
370
|
-
}
|
|
371
|
-
async function fetchJson(url, token, label) {
|
|
372
|
-
let response;
|
|
373
|
-
try {
|
|
374
|
-
response = await fetch(url, {
|
|
375
|
-
headers: {
|
|
376
|
-
Accept: "application/json",
|
|
377
|
-
Authorization: `Bearer ${token}`,
|
|
378
|
-
},
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
catch (error) {
|
|
382
|
-
throw new Error(`Network failure while fetching ${label} export from ${url.toString()}: ${error.message}`);
|
|
383
|
-
}
|
|
384
|
-
const text = await response.text();
|
|
385
|
-
if (!response.ok) {
|
|
386
|
-
throw new Error(formatHttpError(response, url, label, text));
|
|
387
|
-
}
|
|
388
|
-
try {
|
|
389
|
-
return JSON.parse(text);
|
|
390
|
-
}
|
|
391
|
-
catch (error) {
|
|
392
|
-
throw new Error(`Controller returned invalid JSON for the ${label} export from ${url.toString()}.`);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
function formatHttpError(response, url, label, body) {
|
|
396
|
-
if (response.status === 401) {
|
|
397
|
-
return `Token is invalid or expired (401). Provide a valid token via --token, UNS_CONTROLLER_TOKEN, or the interactive prompt.`;
|
|
398
|
-
}
|
|
399
|
-
if (response.status === 403) {
|
|
400
|
-
return `Forbidden (403) while fetching ${label} export from ${url.toString()}. The token must have admin access.`;
|
|
401
|
-
}
|
|
402
|
-
const detail = extractErrorDetail(body);
|
|
403
|
-
return `Failed to fetch ${label} export from ${url.toString()}: HTTP ${response.status}${detail ? ` - ${detail}` : ""}`;
|
|
404
|
-
}
|
|
405
|
-
function extractErrorDetail(body) {
|
|
406
|
-
const trimmed = body.trim();
|
|
407
|
-
if (!trimmed)
|
|
408
|
-
return "";
|
|
409
|
-
try {
|
|
410
|
-
const parsed = JSON.parse(trimmed);
|
|
411
|
-
if (typeof parsed.error === "string" && parsed.error.trim().length > 0) {
|
|
412
|
-
return parsed.error.trim();
|
|
413
|
-
}
|
|
414
|
-
if (typeof parsed.message === "string" && parsed.message.trim().length > 0) {
|
|
415
|
-
return parsed.message.trim();
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
catch (error) {
|
|
419
|
-
// Fall back to plain text handling below.
|
|
420
|
-
}
|
|
421
|
-
return trimmed.replace(/\s+/g, " ").slice(0, 200);
|
|
422
|
-
}
|
|
423
|
-
function formatJsonDocument(document) {
|
|
424
|
-
return `${JSON.stringify(document, null, 2)}\n`;
|
|
425
|
-
}
|
|
426
|
-
async function updateGeneratedFiles(filePaths, content, repoRoot, dryRun) {
|
|
427
|
-
return Promise.all(filePaths.map((filePath) => updateFile(filePath, content, repoRoot, dryRun)));
|
|
428
|
-
}
|
|
429
|
-
async function updateFile(filePath, content, repoRoot, dryRun) {
|
|
430
|
-
const current = await readTextFileIfExists(filePath);
|
|
431
|
-
const changed = current !== content;
|
|
432
|
-
const relativePath = path.relative(repoRoot, filePath);
|
|
433
|
-
if (dryRun) {
|
|
434
|
-
console.log(`${changed ? "Would update" : "No change"} ${relativePath}`);
|
|
435
|
-
return { changed, relativePath };
|
|
436
|
-
}
|
|
437
|
-
if (!changed) {
|
|
438
|
-
console.log(`Unchanged ${relativePath}`);
|
|
439
|
-
return { changed: false, relativePath };
|
|
440
|
-
}
|
|
441
|
-
await writeTextFileIfChanged(filePath, content);
|
|
442
|
-
console.log(`Updated ${relativePath}`);
|
|
443
|
-
return { changed: true, relativePath };
|
|
444
|
-
}
|
|
445
|
-
function countMeasurementCategories(document) {
|
|
446
|
-
return Object.entries(document).filter(([key, value]) => {
|
|
447
|
-
if (key === "schemaVersion")
|
|
448
|
-
return false;
|
|
449
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
450
|
-
}).length;
|
|
451
|
-
}
|
|
452
|
-
function printSummary(summary) {
|
|
453
|
-
console.log("");
|
|
454
|
-
console.log(summary.dryRun ? "Dry-run summary" : "Sync summary");
|
|
455
|
-
console.log(` Controller: ${summary.controllerUrl}`);
|
|
456
|
-
console.log(` Target: ${summary.targetLabel}`);
|
|
457
|
-
console.log(` Root: ${summary.targetRoot}`);
|
|
458
|
-
console.log(` Dictionary status: ${summary.status}`);
|
|
459
|
-
if (summary.dictionary) {
|
|
460
|
-
console.log(` Dictionary: ${summary.dictionary.objectTypeCount} object types, ${summary.dictionary.attributeCount} attributes, JSON ${summary.dictionary.jsonChanged ? "changed" : "unchanged"}`);
|
|
461
|
-
if (summary.skipGenerate) {
|
|
462
|
-
console.log(" Dictionary TS: skipped");
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
console.log(` Dictionary TS: ${renderGeneratedSummary(summary.dictionary.generatedChanged)}`);
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
if (summary.measurements) {
|
|
469
|
-
console.log(` Measurements: ${summary.measurements.categoryCount} categories, JSON ${summary.measurements.jsonChanged ? "changed" : "unchanged"}`);
|
|
470
|
-
if (summary.skipGenerate) {
|
|
471
|
-
console.log(" Measurements TS: skipped");
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
console.log(` Measurements TS: ${renderGeneratedSummary(summary.measurements.generatedChanged)}`);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
}
|
|
478
|
-
function renderGeneratedSummary(results) {
|
|
479
|
-
if (!results.length)
|
|
480
|
-
return "none";
|
|
481
|
-
const changedCount = results.filter((result) => result.changed).length;
|
|
482
|
-
return `${changedCount}/${results.length} file${results.length === 1 ? "" : "s"} changed`;
|
|
483
|
-
}
|
|
484
|
-
if (isDirectExecution) {
|
|
485
|
-
main().catch((error) => {
|
|
486
|
-
console.error("Failed to sync UNS schema:", error instanceof Error ? error.message : error);
|
|
487
|
-
process.exitCode = 1;
|
|
488
|
-
});
|
|
489
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { access, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { readTextFileIfExists, writeTextFileIfChanged } from "./file-utils.js";
|
|
7
|
+
import { renderDictionaryTs } from "./generate-uns-dictionary.js";
|
|
8
|
+
import { renderMeasurementsTs } from "./generate-uns-measurements.js";
|
|
9
|
+
const DEFAULT_STATUS = "active";
|
|
10
|
+
const GENERATOR_LANG = "sl";
|
|
11
|
+
const STATUS_VALUES = ["active", "draft", "deprecated", "all"];
|
|
12
|
+
const isDirectExecution = process.argv[1]
|
|
13
|
+
? path.resolve(process.argv[1]).endsWith(path.basename(fileURLToPath(import.meta.url)))
|
|
14
|
+
: false;
|
|
15
|
+
async function main() {
|
|
16
|
+
const args = await parseArgs(process.argv.slice(2));
|
|
17
|
+
if (args.help) {
|
|
18
|
+
printHelp();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const target = await resolveSyncTarget(args);
|
|
22
|
+
const shouldSyncDictionary = !args.measurementsOnly;
|
|
23
|
+
const shouldSyncMeasurements = !args.dictionaryOnly;
|
|
24
|
+
const [dictionaryDocument, measurementsDocument] = await Promise.all([
|
|
25
|
+
shouldSyncDictionary ? fetchDictionaryDocument(args.controllerUrl, args.token, args.status) : Promise.resolve(undefined),
|
|
26
|
+
shouldSyncMeasurements ? fetchMeasurementsDocument(args.controllerUrl, args.token) : Promise.resolve(undefined),
|
|
27
|
+
]);
|
|
28
|
+
let dictionarySummary;
|
|
29
|
+
let measurementsSummary;
|
|
30
|
+
if (dictionaryDocument) {
|
|
31
|
+
const content = formatJsonDocument(dictionaryDocument);
|
|
32
|
+
const jsonResult = await updateFile(target.dictionaryJson, content, target.rootDir, args.dryRun);
|
|
33
|
+
const generatedChanged = args.skipGenerate
|
|
34
|
+
? []
|
|
35
|
+
: await updateGeneratedFiles(target.dictionaryGenerated, renderDictionaryTs(dictionaryDocument, GENERATOR_LANG), target.rootDir, args.dryRun);
|
|
36
|
+
dictionarySummary = {
|
|
37
|
+
objectTypeCount: Object.keys(dictionaryDocument.objectTypes ?? {}).length,
|
|
38
|
+
attributeCount: Object.keys(dictionaryDocument.attributes ?? {}).length,
|
|
39
|
+
jsonChanged: jsonResult.changed,
|
|
40
|
+
generatedChanged,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (measurementsDocument) {
|
|
44
|
+
const content = formatJsonDocument(measurementsDocument);
|
|
45
|
+
const jsonResult = await updateFile(target.measurementsJson, content, target.rootDir, args.dryRun);
|
|
46
|
+
const generatedChanged = args.skipGenerate
|
|
47
|
+
? []
|
|
48
|
+
: await updateGeneratedFiles(target.measurementsGenerated, renderMeasurementsTs(measurementsDocument, GENERATOR_LANG), target.rootDir, args.dryRun);
|
|
49
|
+
measurementsSummary = {
|
|
50
|
+
categoryCount: countMeasurementCategories(measurementsDocument),
|
|
51
|
+
jsonChanged: jsonResult.changed,
|
|
52
|
+
generatedChanged,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
printSummary({
|
|
56
|
+
controllerUrl: args.controllerUrl,
|
|
57
|
+
targetLabel: target.label,
|
|
58
|
+
targetRoot: target.rootDir,
|
|
59
|
+
dryRun: args.dryRun,
|
|
60
|
+
skipGenerate: args.skipGenerate,
|
|
61
|
+
status: args.status,
|
|
62
|
+
dictionary: dictionarySummary,
|
|
63
|
+
measurements: measurementsSummary,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function getControllerUrlFromConfig() {
|
|
67
|
+
const currentDir = process.cwd();
|
|
68
|
+
const configPath = path.join(currentDir, "config.json");
|
|
69
|
+
try {
|
|
70
|
+
const raw = await readFile(configPath, "utf8");
|
|
71
|
+
const config = JSON.parse(raw);
|
|
72
|
+
const unsConfig = config.uns;
|
|
73
|
+
const restUrl = unsConfig?.rest;
|
|
74
|
+
if (restUrl && typeof restUrl === "string") {
|
|
75
|
+
// Remove trailing /api from the rest URL to get the base controller URL
|
|
76
|
+
return restUrl.endsWith("/api") ? restUrl.slice(0, -4) : restUrl;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
// Ignore missing or invalid config.json
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
async function promptForToken() {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
const stdin = process.stdin;
|
|
87
|
+
const wasRaw = stdin.isRaw;
|
|
88
|
+
stdin.setRawMode(true);
|
|
89
|
+
stdin.resume();
|
|
90
|
+
process.stderr.write("Enter controller token: ");
|
|
91
|
+
let token = "";
|
|
92
|
+
stdin.on("data", onChar);
|
|
93
|
+
function onChar(char) {
|
|
94
|
+
const code = char[0];
|
|
95
|
+
// Enter (13) or newline (10) — done
|
|
96
|
+
if (code === 13 || code === 10) {
|
|
97
|
+
stdin.removeListener("data", onChar);
|
|
98
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
99
|
+
stdin.pause();
|
|
100
|
+
process.stderr.write("\n");
|
|
101
|
+
resolve(token);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
// Ctrl+C — abort
|
|
105
|
+
if (code === 3) {
|
|
106
|
+
stdin.removeListener("data", onChar);
|
|
107
|
+
stdin.setRawMode(wasRaw ?? false);
|
|
108
|
+
process.stderr.write("\n");
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
// Backspace (127) or Delete (8)
|
|
112
|
+
if (code === 127 || code === 8) {
|
|
113
|
+
token = token.slice(0, -1);
|
|
114
|
+
}
|
|
115
|
+
else if (code >= 32) {
|
|
116
|
+
token += char.toString("utf8");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async function parseArgs(argv) {
|
|
122
|
+
let controllerUrl = process.env.UNS_CONTROLLER_URL?.trim() ?? "";
|
|
123
|
+
let token = process.env.UNS_CONTROLLER_TOKEN?.trim() ?? "";
|
|
124
|
+
let status = normalizeStatus(process.env.UNS_SCHEMA_STATUS, DEFAULT_STATUS);
|
|
125
|
+
let projectRoot = process.env.UNS_SCHEMA_PROJECT_ROOT?.trim() || undefined;
|
|
126
|
+
let dryRun = false;
|
|
127
|
+
let dictionaryOnly = false;
|
|
128
|
+
let measurementsOnly = false;
|
|
129
|
+
let skipGenerate = false;
|
|
130
|
+
let help = false;
|
|
131
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
132
|
+
const arg = argv[index];
|
|
133
|
+
if (arg === "--help" || arg === "-h") {
|
|
134
|
+
help = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (arg === "--dry-run") {
|
|
138
|
+
dryRun = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (arg === "--dictionary-only") {
|
|
142
|
+
dictionaryOnly = true;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (arg === "--measurements-only") {
|
|
146
|
+
measurementsOnly = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (arg === "--skip-generate") {
|
|
150
|
+
skipGenerate = true;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (arg === "--project-root") {
|
|
154
|
+
projectRoot = readRequiredValue(argv, ++index, "--project-root");
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (arg.startsWith("--project-root=")) {
|
|
158
|
+
projectRoot = arg.slice("--project-root=".length);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (arg === "--controller-url") {
|
|
162
|
+
controllerUrl = readRequiredValue(argv, ++index, "--controller-url");
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (arg.startsWith("--controller-url=")) {
|
|
166
|
+
controllerUrl = arg.slice("--controller-url=".length);
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (arg === "--token") {
|
|
170
|
+
token = readRequiredValue(argv, ++index, "--token");
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (arg.startsWith("--token=")) {
|
|
174
|
+
token = arg.slice("--token=".length);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (arg === "--status") {
|
|
178
|
+
status = normalizeStatus(readRequiredValue(argv, ++index, "--status"), DEFAULT_STATUS);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (arg.startsWith("--status=")) {
|
|
182
|
+
status = normalizeStatus(arg.slice("--status=".length), DEFAULT_STATUS);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
186
|
+
}
|
|
187
|
+
// Try to load controller URL from config.json if not provided
|
|
188
|
+
if (!controllerUrl) {
|
|
189
|
+
const configUrl = await getControllerUrlFromConfig();
|
|
190
|
+
if (configUrl) {
|
|
191
|
+
controllerUrl = configUrl;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (!help) {
|
|
195
|
+
if (dictionaryOnly && measurementsOnly) {
|
|
196
|
+
throw new Error("Choose either --dictionary-only or --measurements-only, not both.");
|
|
197
|
+
}
|
|
198
|
+
if (!controllerUrl) {
|
|
199
|
+
throw new Error("Missing controller URL. Use --controller-url, set UNS_CONTROLLER_URL, or provide config.json with uns.rest.");
|
|
200
|
+
}
|
|
201
|
+
if (!token) {
|
|
202
|
+
// Prompt for token if stdin is a TTY (interactive terminal)
|
|
203
|
+
if (process.stdin.isTTY) {
|
|
204
|
+
token = await promptForToken();
|
|
205
|
+
}
|
|
206
|
+
if (!token) {
|
|
207
|
+
throw new Error("Missing controller token. Use --token, set UNS_CONTROLLER_TOKEN, or provide it interactively.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Strip "Bearer " prefix if the user included it
|
|
211
|
+
if (token.toLowerCase().startsWith("bearer ")) {
|
|
212
|
+
token = token.slice(7).trim();
|
|
213
|
+
}
|
|
214
|
+
assertValidUrl(controllerUrl);
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
controllerUrl,
|
|
218
|
+
token,
|
|
219
|
+
status,
|
|
220
|
+
projectRoot,
|
|
221
|
+
dryRun,
|
|
222
|
+
dictionaryOnly,
|
|
223
|
+
measurementsOnly,
|
|
224
|
+
skipGenerate,
|
|
225
|
+
help,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function readRequiredValue(argv, index, flag) {
|
|
229
|
+
const value = argv[index];
|
|
230
|
+
if (!value) {
|
|
231
|
+
throw new Error(`Missing value for ${flag}.`);
|
|
232
|
+
}
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
function normalizeStatus(value, fallback) {
|
|
236
|
+
if (!value)
|
|
237
|
+
return fallback;
|
|
238
|
+
const normalized = value.trim().toLowerCase();
|
|
239
|
+
if (STATUS_VALUES.includes(normalized)) {
|
|
240
|
+
return normalized;
|
|
241
|
+
}
|
|
242
|
+
throw new Error(`Invalid --status value "${value}". Expected one of: ${STATUS_VALUES.join(", ")}.`);
|
|
243
|
+
}
|
|
244
|
+
function assertValidUrl(value) {
|
|
245
|
+
try {
|
|
246
|
+
new URL(value);
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
throw new Error(`Invalid controller URL "${value}". Expected an absolute URL such as http://localhost:3200.`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function printHelp() {
|
|
253
|
+
console.log(`Usage: tsx packages/uns-core/src/tools/sync-uns-schema.ts [options]
|
|
254
|
+
|
|
255
|
+
Pull the canonical UNS schema export from a controller and refresh the local
|
|
256
|
+
JSON files plus generated TypeScript artifacts.
|
|
257
|
+
|
|
258
|
+
Options:
|
|
259
|
+
--controller-url <url> Controller base URL (env: UNS_CONTROLLER_URL, or config.json > uns.rest)
|
|
260
|
+
--token <token> Bearer token for REST export (env: UNS_CONTROLLER_TOKEN)
|
|
261
|
+
--status <value> Dictionary status filter: ${STATUS_VALUES.join("|")} (default: ${DEFAULT_STATUS}, env: UNS_SCHEMA_STATUS)
|
|
262
|
+
--project-root <dir> Write into a generated microservice project root (env: UNS_SCHEMA_PROJECT_ROOT).
|
|
263
|
+
When omitted, the tool auto-detects a generated project from the current working directory
|
|
264
|
+
and otherwise updates the uns-kit repo templates.
|
|
265
|
+
--dry-run Report file changes without writing anything
|
|
266
|
+
--dictionary-only Sync only the UNS dictionary export
|
|
267
|
+
--measurements-only Sync only the UNS measurements export
|
|
268
|
+
--skip-generate Skip generated TypeScript refresh
|
|
269
|
+
--help, -h Show this help
|
|
270
|
+
`);
|
|
271
|
+
}
|
|
272
|
+
async function findRepoRoot() {
|
|
273
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
274
|
+
let probeDir = currentDir;
|
|
275
|
+
while (true) {
|
|
276
|
+
const packageJsonPath = path.join(probeDir, "package.json");
|
|
277
|
+
try {
|
|
278
|
+
const raw = await readFile(packageJsonPath, "utf8");
|
|
279
|
+
const pkg = JSON.parse(raw);
|
|
280
|
+
if (pkg.name === "@uns-kit/core") {
|
|
281
|
+
return path.resolve(probeDir, "../..");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
// Ignore missing package.json and continue walking upward.
|
|
286
|
+
}
|
|
287
|
+
const parentDir = path.dirname(probeDir);
|
|
288
|
+
if (parentDir === probeDir) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
probeDir = parentDir;
|
|
292
|
+
}
|
|
293
|
+
throw new Error("Could not locate the @uns-kit/core package directory from sync-uns-schema.ts.");
|
|
294
|
+
}
|
|
295
|
+
async function resolveSyncTarget(args) {
|
|
296
|
+
if (args.projectRoot) {
|
|
297
|
+
return buildProjectTarget(path.resolve(process.cwd(), args.projectRoot));
|
|
298
|
+
}
|
|
299
|
+
const currentDir = process.cwd();
|
|
300
|
+
if (await looksLikeGeneratedProjectRoot(currentDir)) {
|
|
301
|
+
return buildProjectTarget(currentDir);
|
|
302
|
+
}
|
|
303
|
+
return buildRepoTarget(await findRepoRoot());
|
|
304
|
+
}
|
|
305
|
+
function buildRepoTarget(repoRoot) {
|
|
306
|
+
return {
|
|
307
|
+
rootDir: repoRoot,
|
|
308
|
+
label: "uns-kit repo templates",
|
|
309
|
+
dictionaryJson: path.join(repoRoot, "packages/uns-cli/templates/uns-dictionary/uns-dictionary.json"),
|
|
310
|
+
measurementsJson: path.join(repoRoot, "packages/uns-cli/templates/uns-measurements/uns-measurements.json"),
|
|
311
|
+
dictionaryGenerated: [
|
|
312
|
+
path.join(repoRoot, "packages/uns-core/src/uns/uns-dictionary.generated.ts"),
|
|
313
|
+
path.join(repoRoot, "packages/uns-cli/templates/default/src/uns/uns-dictionary.generated.ts"),
|
|
314
|
+
],
|
|
315
|
+
measurementsGenerated: [
|
|
316
|
+
path.join(repoRoot, "packages/uns-core/src/uns/uns-measurements.generated.ts"),
|
|
317
|
+
path.join(repoRoot, "packages/uns-cli/templates/default/src/uns/uns-measurements.generated.ts"),
|
|
318
|
+
],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function buildProjectTarget(projectRoot) {
|
|
322
|
+
return {
|
|
323
|
+
rootDir: projectRoot,
|
|
324
|
+
label: "generated microservice project",
|
|
325
|
+
dictionaryJson: path.join(projectRoot, "uns-dictionary.json"),
|
|
326
|
+
measurementsJson: path.join(projectRoot, "uns-measurements.json"),
|
|
327
|
+
dictionaryGenerated: [path.join(projectRoot, "src/uns/uns-dictionary.generated.ts")],
|
|
328
|
+
measurementsGenerated: [path.join(projectRoot, "src/uns/uns-measurements.generated.ts")],
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async function looksLikeGeneratedProjectRoot(rootDir) {
|
|
332
|
+
const [hasPackageJson, hasConfigJson, hasUnsDir] = await Promise.all([
|
|
333
|
+
pathExists(path.join(rootDir, "package.json")),
|
|
334
|
+
pathExists(path.join(rootDir, "config.json")),
|
|
335
|
+
pathExists(path.join(rootDir, "src/uns")),
|
|
336
|
+
]);
|
|
337
|
+
return hasPackageJson && hasConfigJson && hasUnsDir;
|
|
338
|
+
}
|
|
339
|
+
async function pathExists(targetPath) {
|
|
340
|
+
try {
|
|
341
|
+
await access(targetPath);
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function buildControllerUrl(controllerUrl, relativePath) {
|
|
349
|
+
const base = new URL(controllerUrl);
|
|
350
|
+
if (!base.pathname.endsWith("/")) {
|
|
351
|
+
base.pathname = `${base.pathname}/`;
|
|
352
|
+
}
|
|
353
|
+
return new URL(relativePath, base);
|
|
354
|
+
}
|
|
355
|
+
async function fetchDictionaryDocument(controllerUrl, token, status) {
|
|
356
|
+
const url = buildControllerUrl(controllerUrl, `api/schema/export/uns-dictionary?status=${encodeURIComponent(status)}`);
|
|
357
|
+
const document = await fetchJson(url, token, "UNS dictionary");
|
|
358
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
359
|
+
throw new Error(`Controller returned an invalid UNS dictionary document from ${url.toString()}.`);
|
|
360
|
+
}
|
|
361
|
+
return document;
|
|
362
|
+
}
|
|
363
|
+
async function fetchMeasurementsDocument(controllerUrl, token) {
|
|
364
|
+
const url = buildControllerUrl(controllerUrl, "api/schema/export/uns-measurements");
|
|
365
|
+
const document = await fetchJson(url, token, "UNS measurements");
|
|
366
|
+
if (!document || typeof document !== "object" || Array.isArray(document)) {
|
|
367
|
+
throw new Error(`Controller returned an invalid UNS measurements document from ${url.toString()}.`);
|
|
368
|
+
}
|
|
369
|
+
return document;
|
|
370
|
+
}
|
|
371
|
+
async function fetchJson(url, token, label) {
|
|
372
|
+
let response;
|
|
373
|
+
try {
|
|
374
|
+
response = await fetch(url, {
|
|
375
|
+
headers: {
|
|
376
|
+
Accept: "application/json",
|
|
377
|
+
Authorization: `Bearer ${token}`,
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
throw new Error(`Network failure while fetching ${label} export from ${url.toString()}: ${error.message}`);
|
|
383
|
+
}
|
|
384
|
+
const text = await response.text();
|
|
385
|
+
if (!response.ok) {
|
|
386
|
+
throw new Error(formatHttpError(response, url, label, text));
|
|
387
|
+
}
|
|
388
|
+
try {
|
|
389
|
+
return JSON.parse(text);
|
|
390
|
+
}
|
|
391
|
+
catch (error) {
|
|
392
|
+
throw new Error(`Controller returned invalid JSON for the ${label} export from ${url.toString()}.`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
function formatHttpError(response, url, label, body) {
|
|
396
|
+
if (response.status === 401) {
|
|
397
|
+
return `Token is invalid or expired (401). Provide a valid token via --token, UNS_CONTROLLER_TOKEN, or the interactive prompt.`;
|
|
398
|
+
}
|
|
399
|
+
if (response.status === 403) {
|
|
400
|
+
return `Forbidden (403) while fetching ${label} export from ${url.toString()}. The token must have admin access.`;
|
|
401
|
+
}
|
|
402
|
+
const detail = extractErrorDetail(body);
|
|
403
|
+
return `Failed to fetch ${label} export from ${url.toString()}: HTTP ${response.status}${detail ? ` - ${detail}` : ""}`;
|
|
404
|
+
}
|
|
405
|
+
function extractErrorDetail(body) {
|
|
406
|
+
const trimmed = body.trim();
|
|
407
|
+
if (!trimmed)
|
|
408
|
+
return "";
|
|
409
|
+
try {
|
|
410
|
+
const parsed = JSON.parse(trimmed);
|
|
411
|
+
if (typeof parsed.error === "string" && parsed.error.trim().length > 0) {
|
|
412
|
+
return parsed.error.trim();
|
|
413
|
+
}
|
|
414
|
+
if (typeof parsed.message === "string" && parsed.message.trim().length > 0) {
|
|
415
|
+
return parsed.message.trim();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
// Fall back to plain text handling below.
|
|
420
|
+
}
|
|
421
|
+
return trimmed.replace(/\s+/g, " ").slice(0, 200);
|
|
422
|
+
}
|
|
423
|
+
function formatJsonDocument(document) {
|
|
424
|
+
return `${JSON.stringify(document, null, 2)}\n`;
|
|
425
|
+
}
|
|
426
|
+
async function updateGeneratedFiles(filePaths, content, repoRoot, dryRun) {
|
|
427
|
+
return Promise.all(filePaths.map((filePath) => updateFile(filePath, content, repoRoot, dryRun)));
|
|
428
|
+
}
|
|
429
|
+
async function updateFile(filePath, content, repoRoot, dryRun) {
|
|
430
|
+
const current = await readTextFileIfExists(filePath);
|
|
431
|
+
const changed = current !== content;
|
|
432
|
+
const relativePath = path.relative(repoRoot, filePath);
|
|
433
|
+
if (dryRun) {
|
|
434
|
+
console.log(`${changed ? "Would update" : "No change"} ${relativePath}`);
|
|
435
|
+
return { changed, relativePath };
|
|
436
|
+
}
|
|
437
|
+
if (!changed) {
|
|
438
|
+
console.log(`Unchanged ${relativePath}`);
|
|
439
|
+
return { changed: false, relativePath };
|
|
440
|
+
}
|
|
441
|
+
await writeTextFileIfChanged(filePath, content);
|
|
442
|
+
console.log(`Updated ${relativePath}`);
|
|
443
|
+
return { changed: true, relativePath };
|
|
444
|
+
}
|
|
445
|
+
function countMeasurementCategories(document) {
|
|
446
|
+
return Object.entries(document).filter(([key, value]) => {
|
|
447
|
+
if (key === "schemaVersion")
|
|
448
|
+
return false;
|
|
449
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
450
|
+
}).length;
|
|
451
|
+
}
|
|
452
|
+
function printSummary(summary) {
|
|
453
|
+
console.log("");
|
|
454
|
+
console.log(summary.dryRun ? "Dry-run summary" : "Sync summary");
|
|
455
|
+
console.log(` Controller: ${summary.controllerUrl}`);
|
|
456
|
+
console.log(` Target: ${summary.targetLabel}`);
|
|
457
|
+
console.log(` Root: ${summary.targetRoot}`);
|
|
458
|
+
console.log(` Dictionary status: ${summary.status}`);
|
|
459
|
+
if (summary.dictionary) {
|
|
460
|
+
console.log(` Dictionary: ${summary.dictionary.objectTypeCount} object types, ${summary.dictionary.attributeCount} attributes, JSON ${summary.dictionary.jsonChanged ? "changed" : "unchanged"}`);
|
|
461
|
+
if (summary.skipGenerate) {
|
|
462
|
+
console.log(" Dictionary TS: skipped");
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
console.log(` Dictionary TS: ${renderGeneratedSummary(summary.dictionary.generatedChanged)}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (summary.measurements) {
|
|
469
|
+
console.log(` Measurements: ${summary.measurements.categoryCount} categories, JSON ${summary.measurements.jsonChanged ? "changed" : "unchanged"}`);
|
|
470
|
+
if (summary.skipGenerate) {
|
|
471
|
+
console.log(" Measurements TS: skipped");
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
console.log(` Measurements TS: ${renderGeneratedSummary(summary.measurements.generatedChanged)}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function renderGeneratedSummary(results) {
|
|
479
|
+
if (!results.length)
|
|
480
|
+
return "none";
|
|
481
|
+
const changedCount = results.filter((result) => result.changed).length;
|
|
482
|
+
return `${changedCount}/${results.length} file${results.length === 1 ? "" : "s"} changed`;
|
|
483
|
+
}
|
|
484
|
+
if (isDirectExecution) {
|
|
485
|
+
main().catch((error) => {
|
|
486
|
+
console.error("Failed to sync UNS schema:", error instanceof Error ? error.message : error);
|
|
487
|
+
process.exitCode = 1;
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
490
|
//# sourceMappingURL=sync-uns-schema.js.map
|