@uns-kit/cli 2.0.52 → 2.0.55
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/index.js +101 -2
- package/dist/service-bundle.d.ts +15 -0
- package/dist/service-bundle.js +81 -12
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import process from "node:process";
|
|
|
9
9
|
import readline from "node:readline/promises";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
11
|
import * as azdev from "azure-devops-node-api";
|
|
12
|
-
import { generateAgentsMarkdown, generateServiceSpecMarkdown, readAndValidateServiceBundle, } from "./service-bundle.js";
|
|
12
|
+
import { generateAgentsMarkdown, generateServiceSpecMarkdown, getServiceBundleOutputPublisherContracts, readAndValidateServiceBundle, } from "./service-bundle.js";
|
|
13
13
|
const __filename = fileURLToPath(import.meta.url);
|
|
14
14
|
const __dirname = path.dirname(__filename);
|
|
15
15
|
const require = createRequire(import.meta.url);
|
|
@@ -254,6 +254,7 @@ async function createProjectFromBundle(options) {
|
|
|
254
254
|
allowExisting: options.allowExisting,
|
|
255
255
|
templateName: bundle.scaffold.template,
|
|
256
256
|
});
|
|
257
|
+
await writeBundlePublisherExample(targetDir, bundle);
|
|
257
258
|
await applyTsBundleFeatures(targetDir, bundle);
|
|
258
259
|
await writeServiceBundleArtifacts(targetDir, bundle, raw);
|
|
259
260
|
printTsCreateSuccess(targetDir, result.packageName, initializedGitNextSteps(result.initializedGit, "pnpm run dev"));
|
|
@@ -277,8 +278,13 @@ async function scaffoldTsProject(projectName, targetDir, options = {}) {
|
|
|
277
278
|
return { packageName, initializedGit };
|
|
278
279
|
}
|
|
279
280
|
async function applyTsBundleFeatures(targetDir, bundle) {
|
|
281
|
+
const appliedFeatures = new Set();
|
|
280
282
|
for (const featureName of bundle.scaffold.features) {
|
|
281
|
-
const resolvedFeature =
|
|
283
|
+
const resolvedFeature = resolveBundleConfigureFeatureName(featureName);
|
|
284
|
+
if (!resolvedFeature || appliedFeatures.has(resolvedFeature)) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
appliedFeatures.add(resolvedFeature);
|
|
282
288
|
if (resolvedFeature === "devops") {
|
|
283
289
|
await configureDevopsFromBundle(targetDir, bundle);
|
|
284
290
|
continue;
|
|
@@ -305,6 +311,90 @@ async function configureDevopsFromBundle(targetDir, bundle) {
|
|
|
305
311
|
});
|
|
306
312
|
logDevopsResult(result, { includeRemoteDetails: false });
|
|
307
313
|
}
|
|
314
|
+
async function writeBundlePublisherExample(targetDir, bundle) {
|
|
315
|
+
const contracts = getServiceBundleOutputPublisherContracts(bundle);
|
|
316
|
+
if (!contracts.length) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
await writeFile(path.join(targetDir, "src", "index.ts"), renderBundlePublisherExample(bundle, contracts), "utf8");
|
|
320
|
+
}
|
|
321
|
+
function renderBundlePublisherExample(bundle, contracts) {
|
|
322
|
+
const outputContracts = contracts.map((contract) => ({
|
|
323
|
+
fullPath: contract.fullPath,
|
|
324
|
+
topicPrefix: contract.pathParts.topicPrefix,
|
|
325
|
+
asset: contract.pathParts.asset,
|
|
326
|
+
objectType: contract.pathParts.objectType,
|
|
327
|
+
objectId: contract.pathParts.objectId,
|
|
328
|
+
attribute: contract.pathParts.attribute,
|
|
329
|
+
expectedIntervalMs: contract.expectedIntervalMs,
|
|
330
|
+
}));
|
|
331
|
+
const processName = JSON.stringify(bundle.metadata.name);
|
|
332
|
+
const contractJson = JSON.stringify(outputContracts, null, 2)
|
|
333
|
+
.split("\n")
|
|
334
|
+
.map((line) => ` ${line}`)
|
|
335
|
+
.join("\n")
|
|
336
|
+
.trimStart();
|
|
337
|
+
return `import { ConfigFile, UnsProxyProcess } from "@uns-kit/core";
|
|
338
|
+
import type { ISO8601 } from "@uns-kit/core/uns/uns-interfaces.js";
|
|
339
|
+
|
|
340
|
+
type OutputContract = {
|
|
341
|
+
fullPath: string;
|
|
342
|
+
topicPrefix: string;
|
|
343
|
+
asset: string;
|
|
344
|
+
objectType: string;
|
|
345
|
+
objectId: string;
|
|
346
|
+
attribute: string;
|
|
347
|
+
expectedIntervalMs: number;
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const outputContracts = ${contractJson} satisfies readonly OutputContract[];
|
|
351
|
+
|
|
352
|
+
function topicPrefixForPublish(topicPrefix: string): string {
|
|
353
|
+
return topicPrefix.endsWith("/") ? topicPrefix : \`\${topicPrefix}/\`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function main(): Promise<void> {
|
|
357
|
+
const config = await ConfigFile.loadConfig();
|
|
358
|
+
const processName = config.uns.processName ?? ${processName};
|
|
359
|
+
const unsProcess = new UnsProxyProcess(config.infra.host ?? "localhost", {
|
|
360
|
+
processName,
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
const mqttOutput = await unsProcess.createUnsMqttProxy(
|
|
364
|
+
config.output?.host ?? "localhost",
|
|
365
|
+
"defaultOutput",
|
|
366
|
+
config.uns.instanceMode ?? "wait",
|
|
367
|
+
config.uns.handover ?? true,
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
const time = new Date().toISOString() as ISO8601;
|
|
371
|
+
|
|
372
|
+
for (const output of outputContracts) {
|
|
373
|
+
await mqttOutput.publishMqttMessage({
|
|
374
|
+
topic: topicPrefixForPublish(output.topicPrefix),
|
|
375
|
+
asset: output.asset,
|
|
376
|
+
objectType: output.objectType,
|
|
377
|
+
objectId: output.objectId,
|
|
378
|
+
attributes: {
|
|
379
|
+
attribute: output.attribute,
|
|
380
|
+
description: \`Generated example publisher for \${output.fullPath}\`,
|
|
381
|
+
data: {
|
|
382
|
+
time,
|
|
383
|
+
value: 0,
|
|
384
|
+
dataGroup: "service-bundle-output",
|
|
385
|
+
},
|
|
386
|
+
validityMode: "interval",
|
|
387
|
+
expectedIntervalMs: output.expectedIntervalMs,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
console.log(\`UNS process '\${processName}' published \${outputContracts.length} interval example output(s).\`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
void main();
|
|
396
|
+
`;
|
|
397
|
+
}
|
|
308
398
|
async function writeServiceBundleArtifacts(targetDir, bundle, rawBundle) {
|
|
309
399
|
await writeFile(path.join(targetDir, "service.bundle.json"), rawBundle, "utf8");
|
|
310
400
|
await writeFile(path.join(targetDir, "SERVICE_SPEC.md"), generateServiceSpecMarkdown(bundle), "utf8");
|
|
@@ -1078,6 +1168,15 @@ function resolveConfigureFeatureName(input) {
|
|
|
1078
1168
|
}
|
|
1079
1169
|
return feature;
|
|
1080
1170
|
}
|
|
1171
|
+
function resolveBundleConfigureFeatureName(input) {
|
|
1172
|
+
if (typeof input === "string") {
|
|
1173
|
+
const normalized = input.trim().toLowerCase();
|
|
1174
|
+
if (normalized === "workspace" || normalized === "configure-workspace") {
|
|
1175
|
+
return "vscode";
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
return resolveConfigureFeatureName(input);
|
|
1179
|
+
}
|
|
1081
1180
|
async function ensureGitRepository(dir) {
|
|
1082
1181
|
try {
|
|
1083
1182
|
const { stdout } = await execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], {
|
package/dist/service-bundle.d.ts
CHANGED
|
@@ -41,6 +41,20 @@ export type ServiceBundle = {
|
|
|
41
41
|
};
|
|
42
42
|
};
|
|
43
43
|
};
|
|
44
|
+
export type ServiceBundlePathParts = {
|
|
45
|
+
topicPrefix: string;
|
|
46
|
+
asset: string;
|
|
47
|
+
objectType: string;
|
|
48
|
+
objectId: string;
|
|
49
|
+
attribute: string;
|
|
50
|
+
};
|
|
51
|
+
export type ServiceBundlePublisherContract = {
|
|
52
|
+
mode: "uns-topic-publish";
|
|
53
|
+
fullPath: string;
|
|
54
|
+
pathParts: ServiceBundlePathParts;
|
|
55
|
+
validityMode: "interval";
|
|
56
|
+
expectedIntervalMs: number;
|
|
57
|
+
};
|
|
44
58
|
type ReadBundleOptions = {
|
|
45
59
|
expectedStack: SupportedBundleStack;
|
|
46
60
|
cliName: string;
|
|
@@ -52,4 +66,5 @@ export declare function readAndValidateServiceBundle(bundlePath: string, options
|
|
|
52
66
|
}>;
|
|
53
67
|
export declare function generateServiceSpecMarkdown(bundle: ServiceBundle): string;
|
|
54
68
|
export declare function generateAgentsMarkdown(bundle: ServiceBundle): string;
|
|
69
|
+
export declare function getServiceBundleOutputPublisherContracts(bundle: ServiceBundle): ServiceBundlePublisherContract[];
|
|
55
70
|
export {};
|
package/dist/service-bundle.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
|
+
const DEFAULT_PUBLISHER_EXPECTED_INTERVAL_MS = 60_000;
|
|
2
3
|
export async function readAndValidateServiceBundle(bundlePath, options) {
|
|
3
4
|
const raw = await readFile(bundlePath, "utf8");
|
|
4
5
|
const parsed = parseJsonObject(raw, bundlePath);
|
|
@@ -45,12 +46,17 @@ export function generateServiceSpecMarkdown(bundle) {
|
|
|
45
46
|
]));
|
|
46
47
|
}
|
|
47
48
|
if (bundle.domain) {
|
|
49
|
+
const outputPublisherContracts = getServiceBundleOutputPublisherContracts(bundle);
|
|
48
50
|
lines.push("", "## Domain Inputs", ...renderUnknownList(bundle.domain.inputs), "", "## Domain Outputs", ...renderUnknownList(bundle.domain.outputs));
|
|
51
|
+
if (outputPublisherContracts.length) {
|
|
52
|
+
lines.push("", "## Output Publisher Contracts", ...outputPublisherContracts.map((contract) => `- ${contract.fullPath}: validityMode: "interval", expectedIntervalMs: ${contract.expectedIntervalMs}`));
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
55
|
lines.push("", "## Goals", ...renderStringList(bundle.docs.serviceSpec.goals), "", "## Non-Goals", ...renderStringList(bundle.docs.serviceSpec.nonGoals), "", "## Acceptance Criteria", ...renderStringList(bundle.docs.serviceSpec.acceptanceCriteria), "");
|
|
51
56
|
return lines.join("\n");
|
|
52
57
|
}
|
|
53
58
|
export function generateAgentsMarkdown(bundle) {
|
|
59
|
+
const outputPublisherContracts = getServiceBundleOutputPublisherContracts(bundle);
|
|
54
60
|
const lines = [
|
|
55
61
|
"# AGENTS",
|
|
56
62
|
"",
|
|
@@ -69,21 +75,84 @@ export function generateAgentsMarkdown(bundle) {
|
|
|
69
75
|
"- Read `service.bundle.json` first when planning or generating service-specific code; it is the project source of truth.",
|
|
70
76
|
"- Keep `SERVICE_SPEC.md` and this file aligned with `service.bundle.json` when the bundle changes.",
|
|
71
77
|
"",
|
|
72
|
-
"## Project Context",
|
|
73
|
-
...renderStringList(bundle.docs.agents.projectContext),
|
|
74
|
-
"",
|
|
75
|
-
"## Guardrails",
|
|
76
|
-
...renderStringList(bundle.docs.agents.guardrails),
|
|
77
|
-
"",
|
|
78
|
-
"## First Tasks",
|
|
79
|
-
...renderStringList(bundle.docs.agents.firstTasks),
|
|
80
|
-
"",
|
|
81
|
-
"## Verification",
|
|
82
|
-
...renderStringList(bundle.docs.agents.verification),
|
|
83
|
-
"",
|
|
84
78
|
];
|
|
79
|
+
if (outputPublisherContracts.length) {
|
|
80
|
+
lines.push("## Output Publishing", ...outputPublisherContracts.flatMap((contract) => [
|
|
81
|
+
`- ${contract.fullPath} publishes interval data with validityMode: "interval" and expectedIntervalMs: ${contract.expectedIntervalMs}.`,
|
|
82
|
+
"- Do not publish interval attributes without both fields.",
|
|
83
|
+
]), "");
|
|
84
|
+
}
|
|
85
|
+
lines.push("## Project Context", ...renderStringList(bundle.docs.agents.projectContext), "", "## Guardrails", ...renderStringList(bundle.docs.agents.guardrails), "", "## First Tasks", ...renderStringList(bundle.docs.agents.firstTasks), "", "## Verification", ...renderStringList(bundle.docs.agents.verification), "");
|
|
85
86
|
return lines.join("\n");
|
|
86
87
|
}
|
|
88
|
+
export function getServiceBundleOutputPublisherContracts(bundle) {
|
|
89
|
+
const outputs = Array.isArray(bundle.domain?.outputs) ? bundle.domain.outputs : [];
|
|
90
|
+
return outputs
|
|
91
|
+
.map((output) => readOutputPublisherContract(output))
|
|
92
|
+
.filter((contract) => Boolean(contract));
|
|
93
|
+
}
|
|
94
|
+
function readOutputPublisherContract(output) {
|
|
95
|
+
if (!isRecord(output)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const rawPublisherContract = isRecord(output.publisherContract) ? output.publisherContract : null;
|
|
99
|
+
if (!rawPublisherContract) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const mode = readOptionalString(rawPublisherContract.mode);
|
|
103
|
+
if (mode && mode !== "uns-topic-publish") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const validityMode = readOptionalString(rawPublisherContract.validityMode);
|
|
107
|
+
if (validityMode && validityMode !== "interval") {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const fullPath = readOptionalString(rawPublisherContract.fullPath) ?? readOptionalString(output.topic);
|
|
111
|
+
const pathParts = readPathParts(rawPublisherContract.pathParts) ?? readPathParts(output.pathParts);
|
|
112
|
+
if (!fullPath || !pathParts) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
mode: "uns-topic-publish",
|
|
117
|
+
fullPath,
|
|
118
|
+
pathParts,
|
|
119
|
+
validityMode: "interval",
|
|
120
|
+
expectedIntervalMs: readPositiveInteger(rawPublisherContract.expectedIntervalMs) ??
|
|
121
|
+
readPositiveInteger(output.expectedIntervalMs) ??
|
|
122
|
+
DEFAULT_PUBLISHER_EXPECTED_INTERVAL_MS,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function readPathParts(value) {
|
|
126
|
+
if (!isRecord(value)) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const topicPrefix = readOptionalString(value.topicPrefix);
|
|
130
|
+
const asset = readOptionalString(value.asset);
|
|
131
|
+
const objectType = readOptionalString(value.objectType);
|
|
132
|
+
const objectId = readOptionalString(value.objectId);
|
|
133
|
+
const attribute = readOptionalString(value.attribute);
|
|
134
|
+
if (!topicPrefix || !asset || !objectType || !objectId || !attribute) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return { topicPrefix, asset, objectType, objectId, attribute };
|
|
138
|
+
}
|
|
139
|
+
function readOptionalString(value) {
|
|
140
|
+
return typeof value === "string" && value.trim().length ? value.trim() : undefined;
|
|
141
|
+
}
|
|
142
|
+
function readPositiveInteger(value) {
|
|
143
|
+
const parsed = typeof value === "number"
|
|
144
|
+
? value
|
|
145
|
+
: typeof value === "string" && value.trim().length
|
|
146
|
+
? Number(value.trim())
|
|
147
|
+
: NaN;
|
|
148
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return Math.floor(parsed);
|
|
152
|
+
}
|
|
153
|
+
function isRecord(value) {
|
|
154
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
155
|
+
}
|
|
87
156
|
function validateServiceBundle(value, options) {
|
|
88
157
|
const schemaVersion = value.schemaVersion;
|
|
89
158
|
if (schemaVersion !== 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uns-kit/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.55",
|
|
4
4
|
"description": "Command line scaffolding tool for UNS applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"azure-devops-node-api": "^15.1.1",
|
|
29
|
-
"@uns-kit/core": "2.0.
|
|
29
|
+
"@uns-kit/core": "2.0.55"
|
|
30
30
|
},
|
|
31
31
|
"unsKitPackages": {
|
|
32
|
-
"@uns-kit/core": "2.0.
|
|
33
|
-
"@uns-kit/api": "2.0.
|
|
34
|
-
"@uns-kit/cron": "2.0.
|
|
35
|
-
"@uns-kit/database": "2.0.
|
|
32
|
+
"@uns-kit/core": "2.0.55",
|
|
33
|
+
"@uns-kit/api": "2.0.55",
|
|
34
|
+
"@uns-kit/cron": "2.0.54",
|
|
35
|
+
"@uns-kit/database": "2.0.55"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -p tsconfig.build.json",
|