@uns-kit/cli 2.0.51 → 2.0.53

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 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 = resolveConfigureFeatureName(featureName);
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"], {
@@ -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 {};
@@ -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
  "",
@@ -64,21 +70,89 @@ export function generateAgentsMarkdown(bundle) {
64
70
  ["Template", bundle.scaffold.template],
65
71
  ]),
66
72
  "",
67
- "## Project Context",
68
- ...renderStringList(bundle.docs.agents.projectContext),
69
- "",
70
- "## Guardrails",
71
- ...renderStringList(bundle.docs.agents.guardrails),
72
- "",
73
- "## First Tasks",
74
- ...renderStringList(bundle.docs.agents.firstTasks),
73
+ "## Bundle Source",
75
74
  "",
76
- "## Verification",
77
- ...renderStringList(bundle.docs.agents.verification),
75
+ "- Read `service.bundle.json` first when planning or generating service-specific code; it is the project source of truth.",
76
+ "- Keep `SERVICE_SPEC.md` and this file aligned with `service.bundle.json` when the bundle changes.",
78
77
  "",
79
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), "");
80
86
  return lines.join("\n");
81
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
+ }
82
156
  function validateServiceBundle(value, options) {
83
157
  const schemaVersion = value.schemaVersion;
84
158
  if (schemaVersion !== 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uns-kit/cli",
3
- "version": "2.0.51",
3
+ "version": "2.0.53",
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.51"
29
+ "@uns-kit/core": "2.0.53"
30
30
  },
31
31
  "unsKitPackages": {
32
- "@uns-kit/core": "2.0.51",
33
- "@uns-kit/api": "2.0.51",
34
- "@uns-kit/cron": "2.0.51",
35
- "@uns-kit/database": "2.0.51"
32
+ "@uns-kit/core": "2.0.53",
33
+ "@uns-kit/api": "2.0.53",
34
+ "@uns-kit/cron": "2.0.53",
35
+ "@uns-kit/database": "2.0.53"
36
36
  },
37
37
  "scripts": {
38
38
  "build": "tsc -p tsconfig.build.json",
@@ -19,13 +19,13 @@
19
19
  "password": "secret"
20
20
  },
21
21
  "infra": {
22
- "host": "localhost:1883"
22
+ "host": "localhost"
23
23
  },
24
24
  "output": {
25
- "host": "localhost:1883"
25
+ "host": "localhost"
26
26
  },
27
27
  "input": {
28
- "host": "localhost:1883"
28
+ "host": "localhost"
29
29
  },
30
30
  "devops": {
31
31
  "provider": "azure-devops",
@@ -1,14 +1,23 @@
1
- import { UnsProxyProcess } from "@uns-kit/core";
1
+ import { ConfigFile, UnsProxyProcess } from "@uns-kit/core";
2
+ import type { ISO8601 } from "@uns-kit/core/uns/uns-interfaces.js";
2
3
 
3
4
  async function main(): Promise<void> {
4
- const name = "__APP_NAME__";
5
- const process = new UnsProxyProcess("localhost:1883", {
6
- processName: name,
5
+ const config = await ConfigFile.loadConfig();
6
+ const processName = config.uns.processName ?? "__APP_NAME__";
7
+ const unsProcess = new UnsProxyProcess(config.infra.host ?? "localhost", {
8
+ processName,
7
9
  });
8
10
 
9
- const proxy = await process.createMqttProxy("ts-output");
11
+ const mqttOutput = await unsProcess.createUnsMqttProxy(
12
+ config.output?.host ?? "localhost",
13
+ "defaultOutput",
14
+ config.uns.instanceMode ?? "wait",
15
+ config.uns.handover ?? true,
16
+ );
10
17
 
11
- await proxy.publishMqttMessage({
18
+ const time = new Date().toISOString() as ISO8601;
19
+
20
+ await mqttOutput.publishMqttMessage({
12
21
  topic: "example/site/area/line/",
13
22
  asset: "demo-asset",
14
23
  objectType: "utility-resource",
@@ -17,9 +26,8 @@ async function main(): Promise<void> {
17
26
  attribute: "status",
18
27
  description: "Service startup marker",
19
28
  data: {
20
- time: new Date().toISOString(),
29
+ time,
21
30
  value: "started",
22
- uom: "state",
23
31
  dataGroup: "runtime",
24
32
  },
25
33
  validityMode: "lifecycle",
@@ -27,7 +35,7 @@ async function main(): Promise<void> {
27
35
  },
28
36
  });
29
37
 
30
- console.log(`UNS process '${name}' is ready. Edit src/index.ts to add your logic.`);
38
+ console.log(`UNS process '${processName}' is ready. Edit src/index.ts to add your logic.`);
31
39
  }
32
40
 
33
41
  void main();