@kici-dev/shared 0.1.3 → 0.1.6

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/logger.js CHANGED
@@ -17,9 +17,10 @@ let _serviceName;
17
17
  */
18
18
  const _pendingFileTransportLoggers = /* @__PURE__ */ new Set();
19
19
  function buildFileTransport() {
20
- if (!process.env.KICI_LOG_DIR) return void 0;
20
+ const dir = process.env.KICI_LOG_DIR;
21
+ if (!dir || dir === "undefined") return void 0;
21
22
  return new DailyRotateFile({
22
- dirname: process.env.KICI_LOG_DIR,
23
+ dirname: dir,
23
24
  filename: buildLogFilename(_serviceName),
24
25
  datePattern: "YYYY-MM-DD",
25
26
  maxSize: process.env.KICI_LOG_MAX_SIZE ?? "500m",
@@ -66,13 +67,26 @@ function buildLogFilename(serviceName) {
66
67
  return suffix ? `${base}-${suffix}-%DATE%.log` : `${base}-%DATE%.log`;
67
68
  }
68
69
  /**
70
+ * Pick the default JSON-vs-plain selection for `createLogger` callers that do
71
+ * not pass an explicit `json` option. Honours the operator-controlled
72
+ * `KICI_LOG_FORMAT` env var (`json` / `plain` / `auto`); anything else
73
+ * (including typos and unset) falls back to TTY detection so a piped CLI
74
+ * still produces machine-readable JSON.
75
+ */
76
+ function pickJsonDefault() {
77
+ const envFormat = process.env.KICI_LOG_FORMAT;
78
+ if (envFormat === "json") return true;
79
+ if (envFormat === "plain") return false;
80
+ return !process.stdout.isTTY;
81
+ }
82
+ /**
69
83
  * Create a winston logger instance.
70
84
  *
71
85
  * @param options - Logger configuration options
72
86
  * @returns Configured winston logger
73
87
  */
74
88
  function createLogger(options = {}) {
75
- const { json = !process.stdout.isTTY, level = "info", prefix } = options;
89
+ const { json = pickJsonDefault(), level = "info", prefix } = options;
76
90
  const TOKEN_MASK_RE = /kat_[0-9a-f]{64}/gi;
77
91
  const maskTokens = (value) => {
78
92
  if (typeof value === "string") return value.replace(TOKEN_MASK_RE, "kat_***");
@@ -103,13 +117,10 @@ function createLogger(options = {}) {
103
117
  if (ctx.spanId) info["spanId"] = ctx.spanId;
104
118
  return info;
105
119
  });
106
- const prettyFormat = winston.format.printf(({ level, message, timestamp, requestId }) => {
107
- const time = new Date(timestamp).toLocaleTimeString("en-US", {
108
- hour12: false,
109
- hour: "2-digit",
110
- minute: "2-digit",
111
- second: "2-digit"
112
- });
120
+ const prettyFormat = winston.format.printf(({ level, message, requestId }) => {
121
+ const traceStr = typeof requestId === "string" ? `${pc.dim(`[${requestId.slice(0, 8)}]`)} ` : "";
122
+ const prefixStr = prefix ? `${prefix} ` : "";
123
+ if (level === "info") return `${traceStr}${prefixStr}${message}`;
113
124
  let coloredLevel;
114
125
  switch (level) {
115
126
  case "error":
@@ -118,20 +129,15 @@ function createLogger(options = {}) {
118
129
  case "warn":
119
130
  coloredLevel = pc.yellow(level);
120
131
  break;
121
- case "info":
122
- coloredLevel = pc.cyan(level);
123
- break;
124
132
  case "debug":
125
133
  coloredLevel = pc.gray(level);
126
134
  break;
127
135
  default: coloredLevel = level;
128
136
  }
129
- const traceStr = typeof requestId === "string" ? ` ${pc.dim(`[${requestId.slice(0, 8)}]`)}` : "";
130
- const prefixStr = prefix ? `${prefix} ` : "";
131
- return `${pc.dim(`[${time}]`)}${traceStr} ${coloredLevel}: ${prefixStr}${message}`;
137
+ return `${traceStr}${coloredLevel}: ${prefixStr}${message}`;
132
138
  });
133
139
  const jsonFormat = winston.format.combine(winston.format.timestamp(), traceContextFormat(), tokenMaskFormat(), winston.format.json());
134
- const prettyPipeline = winston.format.combine(winston.format.timestamp(), traceContextFormat(), tokenMaskFormat(), prettyFormat);
140
+ const prettyPipeline = winston.format.combine(traceContextFormat(), tokenMaskFormat(), prettyFormat);
135
141
  const loggerInstance = winston.createLogger({
136
142
  level,
137
143
  format: json ? jsonFormat : prettyPipeline,
@@ -1,3 +1,3 @@
1
- export { initTelemetry, getPrometheusExporter, type TelemetryConfig } from './init.js';
1
+ export { initTelemetry, getPrometheusExporter, collectRuntimeMetricNames, type TelemetryConfig, } from './init.js';
2
2
  export { createMeter } from './metrics.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,4 @@
1
1
  import "../chunk-gOLHoazu.js";
2
- import { getPrometheusExporter, initTelemetry } from "./init.js";
2
+ import { collectRuntimeMetricNames, getPrometheusExporter, initTelemetry } from "./init.js";
3
3
  import { createMeter } from "./metrics.js";
4
- export { createMeter, getPrometheusExporter, initTelemetry };
4
+ export { collectRuntimeMetricNames, createMeter, getPrometheusExporter, initTelemetry };
@@ -17,4 +17,14 @@ export interface TelemetryConfig {
17
17
  export declare function initTelemetry(config: TelemetryConfig): NodeSDK;
18
18
  /** Get the PrometheusExporter instance created by initTelemetry(). */
19
19
  export declare function getPrometheusExporter(): PrometheusExporter | undefined;
20
+ /**
21
+ * Boot `RuntimeNodeInstrumentation` standalone, exercise the event loop and
22
+ * garbage collector, collect once, and return the set of dotted instrument
23
+ * names it emitted. This is the ground truth for the curated runtime-metrics
24
+ * catalog drift guard (`scripts/generate-prometheus.ts`). It deliberately
25
+ * does NOT touch the singleton `_prometheusExporter` — it spins up an
26
+ * isolated SDK so it can be called from tooling without affecting a running
27
+ * service's telemetry.
28
+ */
29
+ export declare function collectRuntimeMetricNames(): Promise<string[]>;
20
30
  //# sourceMappingURL=init.d.ts.map
@@ -36,7 +36,38 @@ function initTelemetry(config) {
36
36
  function getPrometheusExporter() {
37
37
  return _prometheusExporter;
38
38
  }
39
+ /**
40
+ * Boot `RuntimeNodeInstrumentation` standalone, exercise the event loop and
41
+ * garbage collector, collect once, and return the set of dotted instrument
42
+ * names it emitted. This is the ground truth for the curated runtime-metrics
43
+ * catalog drift guard (`scripts/generate-prometheus.ts`). It deliberately
44
+ * does NOT touch the singleton `_prometheusExporter` — it spins up an
45
+ * isolated SDK so it can be called from tooling without affecting a running
46
+ * service's telemetry.
47
+ */
48
+ async function collectRuntimeMetricNames() {
49
+ const exporter = new PrometheusExporter({ preventServerStart: true });
50
+ const sdk = new NodeSDK({
51
+ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: "runtime-metrics-drift-guard" }),
52
+ metricReader: exporter,
53
+ instrumentations: [new RuntimeNodeInstrumentation({ monitoringPrecision: 100 })]
54
+ });
55
+ sdk.start();
56
+ for (let round = 0; round < 5; round++) {
57
+ const junk = [];
58
+ for (let i = 0; i < 100; i++) junk.push(new Array(1e4).fill(i));
59
+ junk.length;
60
+ await new Promise((r) => setTimeout(r, 300));
61
+ }
62
+ if (typeof global.gc === "function") global.gc();
63
+ await new Promise((r) => setTimeout(r, 500));
64
+ const names = /* @__PURE__ */ new Set();
65
+ const { resourceMetrics } = await exporter.collect();
66
+ for (const scopeMetrics of resourceMetrics.scopeMetrics) for (const metric of scopeMetrics.metrics) names.add(metric.descriptor.name);
67
+ await sdk.shutdown();
68
+ return [...names].sort();
69
+ }
39
70
  //#endregion
40
- export { getPrometheusExporter, initTelemetry };
71
+ export { collectRuntimeMetricNames, getPrometheusExporter, initTelemetry };
41
72
 
42
73
  //# sourceMappingURL=init.js.map
@@ -1,6 +1,6 @@
1
1
  import "./chunk-gOLHoazu.js";
2
- import { execFileSync } from "node:child_process";
3
2
  import { accessSync, constants } from "node:fs";
3
+ import { delimiter, join } from "node:path";
4
4
  //#region src/tool-check.ts
5
5
  /**
6
6
  * Startup validation for required external tools and binaries.
@@ -40,19 +40,39 @@ function validateRequiredTools(requirements) {
40
40
  }
41
41
  /**
42
42
  * Check if a binary is available on PATH.
43
- * Uses `where` on Windows, `which` on Unix.
43
+ *
44
+ * Walks the directories in `process.env.PATH` and tests each candidate via
45
+ * `accessSync(..., X_OK)`. Pure-Node implementation — no `which` / `where`
46
+ * dependency, so the check works on minimal base images (Docker Hardened
47
+ * Images, distroless, alpine variants) that strip the `which` binary even
48
+ * when `debianutils` is technically installed.
49
+ *
50
+ * Windows: tries `name` + `name.exe`/`.cmd`/`.bat`/`.com` extensions (mirrors
51
+ * PATHEXT defaults) so `git` resolves to `git.exe` without the caller having
52
+ * to pre-suffix.
44
53
  */
45
54
  function isBinaryOnPath(name) {
46
- const cmd = process.platform === "win32" ? "where" : "which";
47
- try {
48
- execFileSync(cmd, [name], {
49
- stdio: "pipe",
50
- timeout: 5e3
51
- });
52
- return true;
53
- } catch {
54
- return false;
55
+ const pathEnv = process.env.PATH;
56
+ if (!pathEnv) return false;
57
+ const dirs = pathEnv.split(delimiter);
58
+ const exts = process.platform === "win32" ? [
59
+ "",
60
+ ".exe",
61
+ ".cmd",
62
+ ".bat",
63
+ ".com"
64
+ ] : [""];
65
+ for (const dir of dirs) {
66
+ if (!dir) continue;
67
+ for (const ext of exts) {
68
+ const candidate = join(dir, name + ext);
69
+ try {
70
+ accessSync(candidate, constants.X_OK);
71
+ return true;
72
+ } catch {}
73
+ }
55
74
  }
75
+ return false;
56
76
  }
57
77
  //#endregion
58
78
  export { validateRequiredTools };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/shared",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Shared utilities for the KiCI CI/CD stack — logging, zx setup, crypto, telemetry, health and metrics routes. No business logic.",
5
5
  "keywords": [
6
6
  "kici",
@@ -53,6 +53,18 @@
53
53
  "./idempotency-files": {
54
54
  "import": "./dist/idempotency-files.js",
55
55
  "types": "./dist/idempotency-files.d.ts"
56
+ },
57
+ "./idempotency-env-diff": {
58
+ "import": "./dist/idempotency-env-diff.js",
59
+ "types": "./dist/idempotency-env-diff.d.ts"
60
+ },
61
+ "./idempotency-yaml-diff": {
62
+ "import": "./dist/idempotency-yaml-diff.js",
63
+ "types": "./dist/idempotency-yaml-diff.d.ts"
64
+ },
65
+ "./db-collation": {
66
+ "import": "./dist/db-collation.js",
67
+ "types": "./dist/db-collation.d.ts"
56
68
  }
57
69
  },
58
70
  "dependencies": {
@@ -73,6 +85,7 @@
73
85
  "picocolors": "^1.1.1",
74
86
  "winston": "^3.19.0",
75
87
  "winston-daily-rotate-file": "^5.0.0",
88
+ "yaml": "^2.8.3",
76
89
  "zod": "^4.3.6",
77
90
  "zx": "^8.8.5"
78
91
  },
package/sbom.spdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "spdxVersion": "SPDX-2.3",
3
3
  "dataLicense": "CC0-1.0",
4
4
  "SPDXID": "SPDXRef-DOCUMENT",
5
- "name": "@kici-dev/shared@0.1.3",
6
- "documentNamespace": "https://kici.dev/sbom/%40kici-dev%2Fshared/0.1.3/166084de-5305-4703-8b1b-51cc4b1cd5b8",
5
+ "name": "@kici-dev/shared@0.1.6",
6
+ "documentNamespace": "https://kici.dev/sbom/%40kici-dev%2Fshared/0.1.6/04b549ae-92dc-4b0c-b014-f325f8eb99d0",
7
7
  "creationInfo": {
8
- "created": "2026-05-15T14:16:47Z",
8
+ "created": "2026-05-26T05:00:00Z",
9
9
  "creators": [
10
10
  "Tool: kici-sbom-generator"
11
11
  ]
@@ -947,7 +947,7 @@
947
947
  {
948
948
  "SPDXID": "SPDXRef-RootPackage",
949
949
  "name": "@kici-dev/shared",
950
- "versionInfo": "0.1.3",
950
+ "versionInfo": "0.1.6",
951
951
  "downloadLocation": "NOASSERTION",
952
952
  "filesAnalyzed": false,
953
953
  "licenseConcluded": "NOASSERTION",
@@ -958,7 +958,7 @@
958
958
  {
959
959
  "referenceCategory": "PACKAGE-MANAGER",
960
960
  "referenceType": "purl",
961
- "referenceLocator": "pkg:npm/%40kici-dev/shared@0.1.3"
961
+ "referenceLocator": "pkg:npm/%40kici-dev/shared@0.1.6"
962
962
  }
963
963
  ],
964
964
  "description": "Shared utilities for the KiCI CI/CD stack — logging, zx setup, crypto, telemetry, health and metrics routes. No business logic.",
@@ -6506,6 +6506,11 @@
6506
6506
  "relatedSpdxElement": "SPDXRef-Package-winston-3.19.0",
6507
6507
  "relationshipType": "DEPENDS_ON"
6508
6508
  },
6509
+ {
6510
+ "spdxElementId": "SPDXRef-RootPackage",
6511
+ "relatedSpdxElement": "SPDXRef-Package-yaml-2.8.3",
6512
+ "relationshipType": "DEPENDS_ON"
6513
+ },
6509
6514
  {
6510
6515
  "spdxElementId": "SPDXRef-RootPackage",
6511
6516
  "relatedSpdxElement": "SPDXRef-Package-zod-4.3.6",