@uns-kit/cli 0.0.3 → 0.0.5

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/README.md CHANGED
@@ -7,7 +7,7 @@ Command line scaffolding tool for the UNS toolkit. It bootstraps a new project w
7
7
  ```bash
8
8
  pnpm --package=@uns-kit/cli dlx uns-kit create my-uns-app
9
9
  # or with npx
10
- npx -p @uns-kit/cli@latest uns-kit create my-uns-app
10
+ npx @uns-kit/cli create my-uns-app
11
11
  # or after installing globally
12
12
  npm install -g @uns-kit/cli
13
13
  uns-kit create my-uns-app
@@ -24,8 +24,25 @@ pnpm run dev
24
24
  ## Commands
25
25
 
26
26
  - `uns-kit create <name>` – create a new UNS project in the specified directory.
27
+ - `uns-kit configure-devops [path]` – add Azure DevOps tooling (dependencies, script, config) to an existing project.
27
28
  - `uns-kit help` – display usage information.
28
29
 
30
+ ### Configure Azure DevOps
31
+
32
+ Run inside a scaffolded project to add the Azure DevOps pull-request tooling:
33
+
34
+ ```bash
35
+ uns-kit configure-devops
36
+ pnpm install
37
+ pnpm run pull-request
38
+ ```
39
+
40
+ The command prompts for your Azure DevOps organization and updates `config.json` along with the necessary dev dependencies.
41
+
42
+ ### Extend the Config Schema
43
+
44
+ Edit `src/config/project.config.extension.ts` inside your generated project and run `pnpm run generate-config-schema`. This regenerates `config.schema.json` and `src/config/app-config.ts` so editors and runtime types stay in sync.
45
+
29
46
  ## License
30
47
 
31
48
  MIT © Aljoša Vister
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { createRequire } from "node:module";
6
6
  import process from "node:process";
7
+ import readline from "node:readline/promises";
7
8
  const __filename = fileURLToPath(import.meta.url);
8
9
  const __dirname = path.dirname(__filename);
9
10
  const require = createRequire(import.meta.url);
@@ -15,6 +16,17 @@ async function main() {
15
16
  printHelp();
16
17
  return;
17
18
  }
19
+ if (command === "configure-devops") {
20
+ const targetPath = args[1];
21
+ try {
22
+ await configureDevops(targetPath);
23
+ }
24
+ catch (error) {
25
+ console.error(error.message);
26
+ process.exitCode = 1;
27
+ }
28
+ return;
29
+ }
18
30
  if (command === "create") {
19
31
  const projectName = args[1];
20
32
  if (!projectName) {
@@ -36,7 +48,7 @@ async function main() {
36
48
  process.exitCode = 1;
37
49
  }
38
50
  function printHelp() {
39
- console.log(`\nUsage: uns-kit <command> [options]\n\nCommands:\n create <name> Scaffold a new UNS application\n help Show this message\n`);
51
+ console.log(`\nUsage: uns-kit <command> [options]\n\nCommands:\n create <name> Scaffold a new UNS application\n configure-devops [dir] Configure Azure DevOps tooling in an existing project\n help Show this message\n`);
40
52
  }
41
53
  async function createProject(projectName) {
42
54
  const targetDir = path.resolve(process.cwd(), projectName);
@@ -53,6 +65,81 @@ async function createProject(projectName) {
53
65
  console.log(" pnpm install");
54
66
  console.log(" pnpm run dev");
55
67
  }
68
+ async function configureDevops(targetPath) {
69
+ const targetDir = path.resolve(process.cwd(), targetPath ?? ".");
70
+ const packagePath = path.join(targetDir, "package.json");
71
+ const configPath = path.join(targetDir, "config.json");
72
+ let pkgRaw;
73
+ try {
74
+ pkgRaw = await readFile(packagePath, "utf8");
75
+ }
76
+ catch (error) {
77
+ if (error.code === "ENOENT") {
78
+ throw new Error(`Could not find package.json in ${targetDir}`);
79
+ }
80
+ throw error;
81
+ }
82
+ let configRaw;
83
+ try {
84
+ configRaw = await readFile(configPath, "utf8");
85
+ }
86
+ catch (error) {
87
+ if (error.code === "ENOENT") {
88
+ throw new Error(`Could not find config.json in ${targetDir}`);
89
+ }
90
+ throw error;
91
+ }
92
+ const pkg = JSON.parse(pkgRaw);
93
+ const config = JSON.parse(configRaw);
94
+ const defaultOrg = config.devops?.organization ?? "sijit";
95
+ const answer = (await promptQuestion(`Azure DevOps organization [${defaultOrg}]: `)).trim();
96
+ const organization = answer || defaultOrg;
97
+ if (!config.devops || typeof config.devops !== "object") {
98
+ config.devops = {};
99
+ }
100
+ config.devops.organization = organization;
101
+ const requiredDevDeps = {
102
+ "azure-devops-node-api": "^15.1.0",
103
+ "simple-git": "^3.27.0",
104
+ "chalk": "^5.4.1",
105
+ "prettier": "^3.5.3"
106
+ };
107
+ let pkgChanged = false;
108
+ const devDeps = (pkg.devDependencies ??= {});
109
+ const deps = pkg.dependencies ?? {};
110
+ for (const [name, version] of Object.entries(requiredDevDeps)) {
111
+ if (!devDeps[name] && !deps[name]) {
112
+ devDeps[name] = version;
113
+ pkgChanged = true;
114
+ }
115
+ }
116
+ const scripts = (pkg.scripts ??= {});
117
+ if (!scripts["pull-request"]) {
118
+ scripts["pull-request"] = "node ./node_modules/@uns-kit/core/dist/tools/pull-request.js";
119
+ pkgChanged = true;
120
+ }
121
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
122
+ if (pkgChanged) {
123
+ await writeFile(packagePath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
124
+ }
125
+ console.log(`\nDevOps tooling configured.`);
126
+ console.log(` Azure organization: ${organization}`);
127
+ if (pkgChanged) {
128
+ console.log(" Updated package.json scripts/devDependencies. Run pnpm install to fetch new packages.");
129
+ }
130
+ else {
131
+ console.log(" Existing package.json already contained required entries.");
132
+ }
133
+ }
134
+ async function promptQuestion(message) {
135
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
136
+ try {
137
+ return await rl.question(message);
138
+ }
139
+ finally {
140
+ rl.close();
141
+ }
142
+ }
56
143
  async function ensureTargetDir(dir) {
57
144
  try {
58
145
  const stats = await stat(dir);
@@ -84,12 +171,20 @@ async function patchPackageJson(targetDir, packageName) {
84
171
  }
85
172
  async function patchConfigJson(targetDir, packageName) {
86
173
  const configFile = path.join(targetDir, "config.json");
87
- const raw = await readFile(configFile, "utf8");
88
- const config = JSON.parse(raw);
89
- if (config.uns && typeof config.uns === "object") {
90
- config.uns.processName = packageName;
174
+ try {
175
+ const raw = await readFile(configFile, "utf8");
176
+ const config = JSON.parse(raw);
177
+ if (config.uns && typeof config.uns === "object") {
178
+ config.uns.processName = packageName;
179
+ }
180
+ await writeFile(configFile, JSON.stringify(config, null, 2) + "\n", "utf8");
181
+ }
182
+ catch (error) {
183
+ if (error.code === "ENOENT") {
184
+ return;
185
+ }
186
+ throw error;
91
187
  }
92
- await writeFile(configFile, JSON.stringify(config, null, 2) + "\n", "utf8");
93
188
  }
94
189
  async function replacePlaceholders(targetDir, packageName) {
95
190
  const replacements = {
@@ -97,7 +192,8 @@ async function replacePlaceholders(targetDir, packageName) {
97
192
  };
98
193
  const filesToUpdate = [
99
194
  path.join(targetDir, "README.md"),
100
- path.join(targetDir, "src/index.ts")
195
+ path.join(targetDir, "src/index.ts"),
196
+ path.join(targetDir, "config.json")
101
197
  ];
102
198
  for (const file of filesToUpdate) {
103
199
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uns-kit/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Command line scaffolding tool for UNS applications",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,6 +8,7 @@ Generated with `@uns-kit/cli`.
8
8
  pnpm run dev # start the local development loop
9
9
  pnpm run build # emit dist/ output
10
10
  pnpm run start # run the compiled entrypoint
11
+ pnpm run generate-config-schema # regenerate config.schema.json and AppConfig types
11
12
  ```
12
13
 
13
14
  ## Configuration
@@ -18,4 +19,6 @@ Update `config.json` with your broker, UNS URLs, and credentials. The generated
18
19
 
19
20
  - Install additional plugins: `pnpm add @uns-kit/api` etc.
20
21
  - Create MQTT proxies or Temporal workflows inside `src/index.ts`.
22
+ - Extend `src/config/project.config.extension.ts` with project-specific sections and run `pnpm run generate-config-schema`.
23
+ - Run `uns-kit configure-devops` to add the Azure DevOps pull-request tooling.
21
24
  - Commit your new project and start building!
@@ -0,0 +1,23 @@
1
+ {
2
+ "uns": {
3
+ "graphql": "http://localhost:3200/graphql",
4
+ "rest": "http://localhost:3200/api",
5
+ "processName": "__APP_NAME__",
6
+ "instanceMode": "wait",
7
+ "handover": true,
8
+ "jwksWellKnownUrl": "http://localhost:3200/api/.well-known/jwks.json",
9
+ "kidWellKnownUrl": "http://localhost:3200/api/.well-known/kid"
10
+ },
11
+ "infra": {
12
+ "host": "localhost:1883"
13
+ },
14
+ "output": {
15
+ "host": "localhost:1883"
16
+ },
17
+ "input": {
18
+ "host": "localhost:1883"
19
+ },
20
+ "devops": {
21
+ "organization": "sijit"
22
+ }
23
+ }
@@ -6,10 +6,12 @@
6
6
  "typecheck": "tsc --noEmit",
7
7
  "build": "tsc -p tsconfig.json",
8
8
  "start": "node ./dist/index.js",
9
- "dev": "tsx watch src/index.ts"
9
+ "dev": "tsx watch src/index.ts",
10
+ "generate-config-schema": "node ./node_modules/@uns-kit/core/dist/tools/generate-config-schema.js"
10
11
  },
11
12
  "dependencies": {
12
- "@uns-kit/core": "__UNS_KIT_CORE_VERSION__"
13
+ "@uns-kit/core": "__UNS_KIT_CORE_VERSION__",
14
+ "zod": "^3.23.8"
13
15
  },
14
16
  "devDependencies": {
15
17
  "tsx": "^4.20.5",
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import { secretValueSchema } from "@uns-kit/core/dist/uns-config/secret-placeholders";
3
+ import { hostValueSchema } from "@uns-kit/core/dist/uns-config/host-placeholders";
4
+
5
+ // Extend this schema with project-specific configuration sections.
6
+ export const projectExtrasSchema = z.object({
7
+ pg: z.object({
8
+ user: z.string().min(1, "pg.user is required"),
9
+ host: hostValueSchema,
10
+ port: z.number().int().positive().default(5432),
11
+ ssl: z.boolean().default(false),
12
+ database: z.string().min(1, "pg.database is required"),
13
+ isPoolConnection: z.boolean().default(false),
14
+ password: secretValueSchema.optional(),
15
+ }),
16
+
17
+ caddy: z.object({
18
+ adminUrl: z.string().url(),
19
+ proxyHost: z.string().url(),
20
+ }),
21
+ });
22
+
23
+ export type ProjectExtras = z.infer<typeof projectExtrasSchema>;
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+
3
+ // Extend this schema with project-specific configuration sections.
4
+ export const projectExtrasSchema = z.object({});
5
+
6
+ export type ProjectExtras = z.infer<typeof projectExtrasSchema>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Change this file according to your specifications and rename it to index.ts
3
+ */
4
+ import UnsProxyProcess from "../uns/uns-proxy-process.js";
5
+ import { ConfigFile } from "../config-file.js";
6
+ import logger from "../logger.js";
7
+ import { IUnsMessage } from "../uns/uns-interfaces.js";
8
+ import { PhysicalMeasurements } from "../uns/uns-measurements.js";
9
+ import { UnsPacket } from "../uns/uns-packet.js";
10
+ import { UnsTags } from "../uns/uns-tags.js";
11
+ import { UnsTopics } from "../uns/uns-topics.js";
12
+
13
+ /**
14
+ * Load the configuration from a file.
15
+ * On the server, this file is provided by the `uns-datahub-controller`.
16
+ * In the development environment, you are responsible for creating and maintaining this file and its contents.
17
+ */
18
+ const config = await ConfigFile.loadConfig();
19
+
20
+ /**
21
+ * Connect to input and output brokers
22
+ */
23
+ const unsProxyProcess = new UnsProxyProcess(config.infra.host, {processName: config.uns.processName});
24
+ const mqttInput = await unsProxyProcess.createUnsMqttProxy(config.input.host, "templateUnsRttInput", config.uns.instanceMode, config.uns.handover, {
25
+ mqttSubToTopics: ["raw/#"],
26
+ });
27
+ const mqttOutput = await unsProxyProcess.createUnsMqttProxy(config.output.host, "templateUnsRttOutput", config.uns.instanceMode, config.uns.handover, { publishThrottlingDelay: 1000});
28
+
29
+
30
+ /**
31
+ * Event listener for input events.
32
+ * Transform an input message and publish it with publishMqttMessage function.
33
+ */
34
+ mqttInput.event.on("input", async (event) => {
35
+ try {
36
+ if (event.topic === "raw/data") {
37
+ const time = UnsPacket.formatToISO8601(new Date());
38
+ const values = event.message.split(",");
39
+ const numberValue: number = parseFloat(values[0]);
40
+ const message: IUnsMessage = { data: { dataGroup:"electricity", time, value: numberValue, uom: PhysicalMeasurements.MiliVolt } };
41
+ const topic: UnsTopics = "sij/";
42
+ const tags: UnsTags[] = [];
43
+ const packet = await UnsPacket.unsPacketFromUnsMessage(message);
44
+ mqttOutput.publishMqttMessage({ topic, attribute: "data-number", packet, description: "Number value", tags });
45
+ }
46
+ } catch (error) {
47
+ logger.error(`Error publishing message to MQTT: ${error.message}`);
48
+ throw error;
49
+ }
50
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Load the configuration from a file.
3
+ * On the server, this file is provided by the `uns-datahub-controller`.
4
+ * In the development environment, you are responsible for creating and maintaining this file and its contents.
5
+ */
6
+ import readline from "readline";
7
+ import { ConfigFile } from "../config-file.js";
8
+ import UnsMqttProxy from "../uns-mqtt/uns-mqtt-proxy.js";
9
+ import logger from "../logger.js";
10
+
11
+
12
+ /**
13
+ * This script initializes an MQTT output proxy for load testing purposes.
14
+ * It sets up a connection to the specified MQTT broker and configures
15
+ * a proxy instance. The load test is designed to evaluate the performance
16
+ * and reliability of the MQTT broker under simulated load conditions.
17
+ */
18
+ async function main() {
19
+ try {
20
+ const config = await ConfigFile.loadConfig();
21
+ const mqttOutput = new UnsMqttProxy(
22
+ config.output.host,
23
+ "loadTest",
24
+ "templateUnsRttLoadTest",
25
+ { publishThrottlingDelay: 0 },
26
+ true
27
+ );
28
+
29
+ const rl = readline.createInterface({
30
+ input: process.stdin,
31
+ output: process.stdout,
32
+ });
33
+
34
+ await new Promise((resolve) => setTimeout(resolve, 1000));
35
+
36
+ rl.question(`Would you like to continue with load-test on ${config.output.host}? (Y/n) `, async (answer) => {
37
+ if (answer.toLowerCase() === "y" || answer.trim() === "") {
38
+ rl.question("How many iterations should be run? (default is 100) ", async (iterations) => {
39
+ const maxIntervals = parseInt(iterations) || 100;
40
+
41
+ rl.question("What should be the delay between intervals in milliseconds? (default is 0 ms) ", async (intervalDelay) => {
42
+ const delay = parseInt(intervalDelay) || 0;
43
+
44
+ logger.info(`Starting load test with ${maxIntervals} messages and ${delay} ms delay...`);
45
+
46
+ let count = 0;
47
+ const startTime = Date.now();
48
+
49
+ while (count < maxIntervals) {
50
+ try {
51
+ const currentDate = new Date();
52
+ const rawData = `${count},${currentDate.getTime()}`;
53
+ await mqttOutput.publishMessage("raw/data", rawData);
54
+ } catch (error) {
55
+ logger.error("Error publishing message:", error.message);
56
+ }
57
+
58
+ count++;
59
+ if (delay > 0) {
60
+ await new Promise((resolve) => setTimeout(resolve, delay));
61
+ }
62
+ }
63
+
64
+ logger.info(`Sleeping for 50ms.`);
65
+ await new Promise((resolve) => setTimeout(resolve, 50));
66
+
67
+ const endTime = Date.now();
68
+ const duration = (endTime - startTime) / 1000;
69
+ const messagesPerSecond = maxIntervals / duration;
70
+
71
+ logger.info(`Load test completed in ${duration.toFixed(2)} seconds.`);
72
+ logger.info(`Message rate: ${messagesPerSecond.toFixed(2)} msg/s.`);
73
+
74
+ rl.close();
75
+ process.exit(0);
76
+ });
77
+ });
78
+ } else {
79
+ logger.info("Load test aborted.");
80
+ rl.close();
81
+ process.exit(0);
82
+ }
83
+ });
84
+ } catch (error) {
85
+ logger.error("Error initializing load test:", error.message);
86
+ process.exit(1);
87
+ }
88
+ }
89
+
90
+ main();
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Change this file according to your specifications and rename it to index.ts
3
+ */
4
+
5
+ import UnsProxyProcess from "../uns/uns-proxy-process.js";
6
+ import { ConfigFile } from "../config-file.js";
7
+ import logger from "../logger.js";
8
+ import { IUnsMessage } from "../uns/uns-interfaces.js";
9
+ import { UnsPacket } from "../uns/uns-packet.js";
10
+ import { UnsTags } from "../uns/uns-tags.js";
11
+ import { UnsTopics } from "../uns/uns-topics.js";
12
+
13
+ /**
14
+ * Load the configuration from a file.
15
+ * On the server, this file is provided by the `uns-datahub-controller`.
16
+ * In the development environment, you are responsible for creating and maintaining this file and its contents.
17
+ */
18
+ const config = await ConfigFile.loadConfig();
19
+
20
+ /**
21
+ * Load and configure input and output brokers from config.json
22
+ */
23
+ if (!config.infra?.host || !config.input?.host || !config.output?.host) {
24
+ throw new Error("Missing required configuration in config.json");
25
+ }
26
+ const unsProxyProcess = new UnsProxyProcess(config.infra.host, {processName: config.uns.processName});
27
+ const mqttInput = await unsProxyProcess.createUnsMqttProxy(config.input.host, "templateUnsRttInput", config.uns.instanceMode, config.uns.handover, {
28
+ mqttSubToTopics: ["iba/zrm"],
29
+ publishThrottlingDelay:0,
30
+ subscribeThrottlingDelay:0
31
+ });
32
+ const mqttOutput = await unsProxyProcess.createUnsMqttProxy(config.output.host, "templateUnsRttOutput", config.uns.instanceMode, config.uns.handover, {
33
+ publishThrottlingDelay:0,
34
+ subscribeThrottlingDelay:0
35
+ });
36
+
37
+ /**
38
+ * The input worker connects to the IBA broker and listens for incoming messages.
39
+ * It processes the messages and transforms them into a table-type IUnsMessage.
40
+ * The resulting message is published to the output broker.
41
+ */
42
+ mqttInput.event.on("input", async (event) => {
43
+ try {
44
+ if (event.topic === "iba/zrm") {
45
+ const jsonObject = JSON.parse(event.message);
46
+ const timestamp = jsonObject.Timestamp;
47
+ delete(jsonObject.Timestamp);
48
+
49
+ const time = UnsPacket.formatToISO8601(new Date(timestamp));
50
+ const message: IUnsMessage = { table: {dataGroup:"iba_test", values:jsonObject, time}};
51
+ const topic: UnsTopics = "sij/acroni/hv/";
52
+ const tags: UnsTags[] = [];
53
+ const packet = await UnsPacket.unsPacketFromUnsMessage(message);
54
+ mqttOutput.publishMqttMessage({ topic, attribute: "zrm", packet, description: "Table", tags });
55
+ }
56
+ } catch (error) {
57
+ logger.error(`Error publishing message to MQTT: ${error.message}`);
58
+ }
59
+ });