@kici-dev/shared 0.1.13 → 0.1.15
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 +13 -1
- package/dist/cold-store/chunk-encoder.js +1 -1
- package/dist/cold-store/chunk-id.js +1 -1
- package/dist/cold-store/cold-store.js +1 -1
- package/dist/db-admin.d.ts +39 -0
- package/dist/db-admin.js +60 -5
- package/dist/db.d.ts +20 -1
- package/dist/db.js +34 -2
- package/dist/db.test.d.ts +2 -0
- package/dist/env/define-env.js +27 -6
- package/dist/env/logger-env.d.ts +1 -1
- package/dist/graceful-shutdown.d.ts +13 -1
- package/dist/graceful-shutdown.js +24 -10
- package/dist/idempotency.d.ts +1 -67
- package/dist/idempotency.js +2 -48
- package/dist/index.d.ts +3 -9
- package/dist/index.js +3 -9
- package/dist/package-manager-types.d.ts +1 -21
- package/dist/package-manager-types.js +2 -34
- package/dist/package-manager.d.ts +1 -51
- package/dist/package-manager.js +2 -130
- package/dist/s3-client.js +4 -1
- package/dist/s3-client.test.d.ts +2 -0
- package/dist/ts-loader-hook.d.ts +1 -25
- package/dist/ts-loader-hook.js +2 -47
- package/package.json +12 -7
- package/sbom.spdx.json +60 -5
- package/dist/crypto.d.ts +0 -33
- package/dist/crypto.js +0 -67
- package/dist/error.d.ts +0 -16
- package/dist/error.js +0 -58
- package/dist/error.test.d.ts +0 -2
- package/dist/format-bytes.d.ts +0 -5
- package/dist/format-bytes.js +0 -15
- package/dist/format-bytes.test.d.ts +0 -2
- package/dist/format-duration.d.ts +0 -11
- package/dist/format-duration.js +0 -32
- package/dist/format-duration.test.d.ts +0 -2
- package/dist/idempotency.test.d.ts +0 -2
- package/dist/logger.d.ts +0 -60
- package/dist/logger.js +0 -181
- package/dist/logger.test.d.ts +0 -2
- package/dist/package-manager.test.d.ts +0 -2
- package/dist/request-context.d.ts +0 -42
- package/dist/request-context.js +0 -37
- package/dist/zx.d.ts +0 -8
- package/dist/zx.js +0 -78
package/dist/crypto.js
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import "./chunk-gOLHoazu.js";
|
|
2
|
-
import crypto, { createHash } from "node:crypto";
|
|
3
|
-
import { readFile } from "node:fs/promises";
|
|
4
|
-
//#region src/crypto.ts
|
|
5
|
-
/** Compute SHA-256 hex digest of a string or Buffer. */
|
|
6
|
-
function sha256(input) {
|
|
7
|
-
return createHash("sha256").update(input).digest("hex");
|
|
8
|
-
}
|
|
9
|
-
/** Compute SHA-256 hex digest of a file's contents. */
|
|
10
|
-
async function sha256File(filePath) {
|
|
11
|
-
return sha256(await readFile(filePath));
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Normalize line endings to LF so hashes computed on different platforms agree.
|
|
15
|
-
*
|
|
16
|
-
* Git for Windows ships with `core.autocrlf=true` in the system gitconfig, so a
|
|
17
|
-
* `git clone` of a Linux-authored repo on a Windows host checks out text files
|
|
18
|
-
* with CRLF in the working tree. The compiler hashed the LF source on Linux,
|
|
19
|
-
* but the agent on Windows reads CRLF and computes a different hash — every
|
|
20
|
-
* dispatch fails with a "lock file is out of date" error even though the
|
|
21
|
-
* semantic content is identical.
|
|
22
|
-
*
|
|
23
|
-
* Applied at the boundaries where source / asset content enters the hash:
|
|
24
|
-
* - raw workflow source (`.kici/workflows/*.ts`) at hash time, in both the
|
|
25
|
-
* compiler (lockfile generation) and the agent (drift verification).
|
|
26
|
-
* - file content portions of the asset digest (`hashFiles` resolution) on
|
|
27
|
-
* both sides.
|
|
28
|
-
*
|
|
29
|
-
* Standalone `\r` is also collapsed to `\n` for safety against legacy
|
|
30
|
-
* Mac-style endings, though TypeScript source files essentially never carry
|
|
31
|
-
* those in practice.
|
|
32
|
-
*/
|
|
33
|
-
function normalizeLineEndings(input) {
|
|
34
|
-
return input.replace(/\r\n?/g, "\n");
|
|
35
|
-
}
|
|
36
|
-
/** HKDF info string for ECDH-derived AES keys (upload encryption). */
|
|
37
|
-
const HKDF_INFO = "kici-upload-encryption";
|
|
38
|
-
/** Empty salt — ECDH output is already high entropy. */
|
|
39
|
-
const HKDF_SALT = Buffer.alloc(0);
|
|
40
|
-
/**
|
|
41
|
-
* Derive an AES-256 key from an X25519 ECDH shared secret using HKDF.
|
|
42
|
-
*
|
|
43
|
-
* Used by the compiler (encrypt) and agent (decrypt) for tarball uploads.
|
|
44
|
-
* Keys must be DER-encoded (PKCS8 for private, SPKI for public).
|
|
45
|
-
*/
|
|
46
|
-
function deriveSharedSecret(ourPrivateKey, theirPublicKey) {
|
|
47
|
-
const ourKeyObj = crypto.createPrivateKey({
|
|
48
|
-
key: ourPrivateKey,
|
|
49
|
-
format: "der",
|
|
50
|
-
type: "pkcs8"
|
|
51
|
-
});
|
|
52
|
-
const theirKeyObj = crypto.createPublicKey({
|
|
53
|
-
key: theirPublicKey,
|
|
54
|
-
format: "der",
|
|
55
|
-
type: "spki"
|
|
56
|
-
});
|
|
57
|
-
const sharedSecret = crypto.diffieHellman({
|
|
58
|
-
publicKey: theirKeyObj,
|
|
59
|
-
privateKey: ourKeyObj
|
|
60
|
-
});
|
|
61
|
-
const derivedKey = crypto.hkdfSync("sha256", sharedSecret, HKDF_SALT, HKDF_INFO, 32);
|
|
62
|
-
return Buffer.from(derivedKey);
|
|
63
|
-
}
|
|
64
|
-
//#endregion
|
|
65
|
-
export { deriveSharedSecret, normalizeLineEndings, sha256, sha256File };
|
|
66
|
-
|
|
67
|
-
//# sourceMappingURL=crypto.js.map
|
package/dist/error.d.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Extract a human-readable error message from an unknown thrown value.
|
|
3
|
-
*/
|
|
4
|
-
export declare function toErrorMessage(err: unknown): string;
|
|
5
|
-
/**
|
|
6
|
-
* Serialize an error into a structured object suitable for logging.
|
|
7
|
-
*
|
|
8
|
-
* Captures the message, error type name, common diagnostic fields
|
|
9
|
-
* (`code`, `status`, response details), and the chained `cause`.
|
|
10
|
-
* Falls back to a non-empty descriptor when `err.message` is empty —
|
|
11
|
-
* an empty message field is a debugging dead end (we've hit it on
|
|
12
|
-
* sync failures where the underlying library throws errors with no
|
|
13
|
-
* message but populated `.code` / `.response.status`).
|
|
14
|
-
*/
|
|
15
|
-
export declare function serializeError(err: unknown): Record<string, unknown>;
|
|
16
|
-
//# sourceMappingURL=error.d.ts.map
|
package/dist/error.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import "./chunk-gOLHoazu.js";
|
|
2
|
-
//#region src/error.ts
|
|
3
|
-
/**
|
|
4
|
-
* Extract a human-readable error message from an unknown thrown value.
|
|
5
|
-
*/
|
|
6
|
-
function toErrorMessage(err) {
|
|
7
|
-
return err instanceof Error ? err.message : String(err);
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Serialize an error into a structured object suitable for logging.
|
|
11
|
-
*
|
|
12
|
-
* Captures the message, error type name, common diagnostic fields
|
|
13
|
-
* (`code`, `status`, response details), and the chained `cause`.
|
|
14
|
-
* Falls back to a non-empty descriptor when `err.message` is empty —
|
|
15
|
-
* an empty message field is a debugging dead end (we've hit it on
|
|
16
|
-
* sync failures where the underlying library throws errors with no
|
|
17
|
-
* message but populated `.code` / `.response.status`).
|
|
18
|
-
*/
|
|
19
|
-
function serializeError(err) {
|
|
20
|
-
if (err === null || err === void 0) return { message: String(err) };
|
|
21
|
-
if (typeof err !== "object") return { message: String(err) };
|
|
22
|
-
const e = err;
|
|
23
|
-
const out = {};
|
|
24
|
-
const rawMessage = err instanceof Error ? err.message : e.message;
|
|
25
|
-
out.message = rawMessage && rawMessage.length > 0 ? rawMessage : `<${describeShape(err)}>`;
|
|
26
|
-
if (err instanceof Error && err.name && err.name !== "Error") out.name = err.name;
|
|
27
|
-
if (typeof e.code === "string" || typeof e.code === "number") out.code = e.code;
|
|
28
|
-
const response = e.response;
|
|
29
|
-
if (response && typeof response === "object") {
|
|
30
|
-
if (typeof response.status === "number") out.status = response.status;
|
|
31
|
-
if (response.statusText) out.statusText = response.statusText;
|
|
32
|
-
if (response.data !== void 0) out.responseData = trimResponseData(response.data);
|
|
33
|
-
}
|
|
34
|
-
if (e.cause !== void 0) out.cause = serializeError(e.cause);
|
|
35
|
-
return out;
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Describe an error's shape when its `.message` is empty so logs
|
|
39
|
-
* still convey something useful (vs. a bare `error: ""`).
|
|
40
|
-
*/
|
|
41
|
-
function describeShape(err) {
|
|
42
|
-
const ctor = err.constructor?.name;
|
|
43
|
-
if (ctor && ctor !== "Object") return `${ctor} with empty message`;
|
|
44
|
-
const keys = Object.keys(err).slice(0, 5).join(",");
|
|
45
|
-
return keys ? `object{${keys}}` : "empty error";
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Cap response payload size to keep logs readable.
|
|
49
|
-
*/
|
|
50
|
-
function trimResponseData(data) {
|
|
51
|
-
const s = typeof data === "string" ? data : JSON.stringify(data);
|
|
52
|
-
if (s.length <= 500) return data;
|
|
53
|
-
return s.slice(0, 500) + `…(+${s.length - 500} chars)`;
|
|
54
|
-
}
|
|
55
|
-
//#endregion
|
|
56
|
-
export { serializeError, toErrorMessage };
|
|
57
|
-
|
|
58
|
-
//# sourceMappingURL=error.js.map
|
package/dist/error.test.d.ts
DELETED
package/dist/format-bytes.d.ts
DELETED
package/dist/format-bytes.js
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import "./chunk-gOLHoazu.js";
|
|
2
|
-
//#region src/format-bytes.ts
|
|
3
|
-
/**
|
|
4
|
-
* Format bytes to a human-readable string.
|
|
5
|
-
*/
|
|
6
|
-
function formatBytes(bytes) {
|
|
7
|
-
if (bytes < 1024) return `${bytes} B`;
|
|
8
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
9
|
-
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
10
|
-
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
11
|
-
}
|
|
12
|
-
//#endregion
|
|
13
|
-
export { formatBytes };
|
|
14
|
-
|
|
15
|
-
//# sourceMappingURL=format-bytes.js.map
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Format seconds into human-readable uptime.
|
|
3
|
-
* Examples: "45s", "3m 12s", "2h 15m", "1d 3h 12m"
|
|
4
|
-
*/
|
|
5
|
-
export declare function formatUptime(seconds: number): string;
|
|
6
|
-
/**
|
|
7
|
-
* Format a duration in milliseconds to a human-readable string.
|
|
8
|
-
* Examples: "0s", "0.3s", "12.3s", "1m 23s", "1h 2m 3s"
|
|
9
|
-
*/
|
|
10
|
-
export declare function formatDuration(ms: number): string;
|
|
11
|
-
//# sourceMappingURL=format-duration.d.ts.map
|
package/dist/format-duration.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import "./chunk-gOLHoazu.js";
|
|
2
|
-
//#region src/format-duration.ts
|
|
3
|
-
/**
|
|
4
|
-
* Format seconds into human-readable uptime.
|
|
5
|
-
* Examples: "45s", "3m 12s", "2h 15m", "1d 3h 12m"
|
|
6
|
-
*/
|
|
7
|
-
function formatUptime(seconds) {
|
|
8
|
-
if (seconds < 60) return `${seconds}s`;
|
|
9
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
10
|
-
const hours = Math.floor(seconds / 3600);
|
|
11
|
-
const mins = Math.floor(seconds % 3600 / 60);
|
|
12
|
-
if (hours < 24) return `${hours}h ${mins}m`;
|
|
13
|
-
return `${Math.floor(hours / 24)}d ${hours % 24}h ${mins}m`;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Format a duration in milliseconds to a human-readable string.
|
|
17
|
-
* Examples: "0s", "0.3s", "12.3s", "1m 23s", "1h 2m 3s"
|
|
18
|
-
*/
|
|
19
|
-
function formatDuration(ms) {
|
|
20
|
-
if (ms < 0) return "0s";
|
|
21
|
-
const totalSeconds = ms / 1e3;
|
|
22
|
-
if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s`;
|
|
23
|
-
const hours = Math.floor(totalSeconds / 3600);
|
|
24
|
-
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
25
|
-
const seconds = Math.floor(totalSeconds % 60);
|
|
26
|
-
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
|
|
27
|
-
return `${minutes}m ${seconds}s`;
|
|
28
|
-
}
|
|
29
|
-
//#endregion
|
|
30
|
-
export { formatDuration, formatUptime };
|
|
31
|
-
|
|
32
|
-
//# sourceMappingURL=format-duration.js.map
|
package/dist/logger.d.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import winston from 'winston';
|
|
2
|
-
/** Set the service name for all loggers in this process. Call once at startup. */
|
|
3
|
-
export declare function setServiceName(name: 'platform' | 'orchestrator' | 'agent'): void;
|
|
4
|
-
/** Get the current service name (for testing/inspection). */
|
|
5
|
-
export declare function getServiceName(): string | undefined;
|
|
6
|
-
/**
|
|
7
|
-
* Build the rotated log filename. When a stable instance ID is available in the
|
|
8
|
-
* environment, append it so multiple processes (e.g. several orchestrators or
|
|
9
|
-
* agents) can safely share one KICI_LOG_DIR without racing on the same file.
|
|
10
|
-
*
|
|
11
|
-
* Precedence matches the tier that owns each variable:
|
|
12
|
-
* orchestrator (KICI_CLUSTER_INSTANCE_ID) > agent (KICI_AGENT_ID) > platform
|
|
13
|
-
* (KICI_PLATFORM_INSTANCE_ID). Sanitize defensively to filesystem-safe characters.
|
|
14
|
-
*/
|
|
15
|
-
export declare function buildLogFilename(serviceName: string | undefined): string;
|
|
16
|
-
/** Log level type */
|
|
17
|
-
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
18
|
-
/** Minimal logger interface for dependency injection (avoids coupling to winston). */
|
|
19
|
-
export interface Logger {
|
|
20
|
-
info(message: string, meta?: Record<string, unknown>): void;
|
|
21
|
-
warn(message: string, meta?: Record<string, unknown>): void;
|
|
22
|
-
error(message: string, meta?: Record<string, unknown>): void;
|
|
23
|
-
}
|
|
24
|
-
/** Logger creation options */
|
|
25
|
-
interface LoggerOptions {
|
|
26
|
-
/**
|
|
27
|
-
* Use JSON format (default: auto-detected from KICI_LOG_FORMAT env var, with
|
|
28
|
-
* a TTY fallback). Passing an explicit boolean wins over the env var.
|
|
29
|
-
*/
|
|
30
|
-
json?: boolean;
|
|
31
|
-
/** Log level (default: 'info') */
|
|
32
|
-
level?: LogLevel;
|
|
33
|
-
/** Optional prefix for all messages */
|
|
34
|
-
prefix?: string;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Create a winston logger instance.
|
|
38
|
-
*
|
|
39
|
-
* @param options - Logger configuration options
|
|
40
|
-
* @returns Configured winston logger
|
|
41
|
-
*/
|
|
42
|
-
export declare function createLogger(options?: LoggerOptions): winston.Logger;
|
|
43
|
-
/**
|
|
44
|
-
* Default singleton logger instance for simple use cases. If KICI_LOG_DIR is
|
|
45
|
-
* set but setServiceName() hasn't been called yet (CLI tools, scripts),
|
|
46
|
-
* the file transport stays deferred and is attached only once a service
|
|
47
|
-
* name is known — protecting against writing to `kici-<instanceId>-*.log`.
|
|
48
|
-
*/
|
|
49
|
-
export declare const logger: winston.Logger;
|
|
50
|
-
/**
|
|
51
|
-
* Wrap an async startup function so that any thrown error is logged
|
|
52
|
-
* through the structured (JSON-aware) logger before the process exits.
|
|
53
|
-
*
|
|
54
|
-
* Without this guard, a top-level `await` rejection in an ESM entry
|
|
55
|
-
* point is printed by Node.js's default handler (multi-line, not JSON),
|
|
56
|
-
* which breaks log aggregators like ELK.
|
|
57
|
-
*/
|
|
58
|
-
export declare function guardStartup(log: winston.Logger, fn: () => Promise<void>): Promise<void>;
|
|
59
|
-
export {};
|
|
60
|
-
//# sourceMappingURL=logger.d.ts.map
|
package/dist/logger.js
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import "./chunk-gOLHoazu.js";
|
|
2
|
-
import { toErrorMessage } from "./error.js";
|
|
3
|
-
import { getRequestContext } from "./request-context.js";
|
|
4
|
-
import winston from "winston";
|
|
5
|
-
import pc from "picocolors";
|
|
6
|
-
import DailyRotateFile from "winston-daily-rotate-file";
|
|
7
|
-
//#region src/logger.ts
|
|
8
|
-
let _serviceName;
|
|
9
|
-
/**
|
|
10
|
-
* Tracked set of loggers still waiting for the service name so they can
|
|
11
|
-
* add their rotated-file transport with the right filename. Module-level
|
|
12
|
-
* `createLogger()` calls resolve before the service's `setServiceName()`
|
|
13
|
-
* runs; if we built the file transport eagerly, every such logger would
|
|
14
|
-
* write to `kici-<instanceId>-*.log` (undefined service name) instead of
|
|
15
|
-
* `<service>-<instanceId>-*.log`. Holding them here lets setServiceName
|
|
16
|
-
* attach the correct transport once, in one place.
|
|
17
|
-
*/
|
|
18
|
-
const _pendingFileTransportLoggers = /* @__PURE__ */ new Set();
|
|
19
|
-
function buildFileTransport() {
|
|
20
|
-
const dir = process.env.KICI_LOG_DIR;
|
|
21
|
-
if (!dir || dir === "undefined") return void 0;
|
|
22
|
-
return new DailyRotateFile({
|
|
23
|
-
dirname: dir,
|
|
24
|
-
filename: buildLogFilename(_serviceName),
|
|
25
|
-
datePattern: "YYYY-MM-DD",
|
|
26
|
-
maxSize: process.env.KICI_LOG_MAX_SIZE ?? "500m",
|
|
27
|
-
maxFiles: `${process.env.KICI_LOG_RETENTION_DAYS ?? "7"}d`,
|
|
28
|
-
format: winston.format.combine(winston.format.timestamp(), winston.format((info) => {
|
|
29
|
-
if (_serviceName) info["service"] = _serviceName;
|
|
30
|
-
const ctx = getRequestContext();
|
|
31
|
-
if (ctx.requestId) info["requestId"] = ctx.requestId;
|
|
32
|
-
if (ctx.runId) info["runId"] = ctx.runId;
|
|
33
|
-
if (ctx.jobId) info["jobId"] = ctx.jobId;
|
|
34
|
-
if (ctx.routingKey) info["routingKey"] = ctx.routingKey;
|
|
35
|
-
if (ctx.traceId) info["traceId"] = ctx.traceId;
|
|
36
|
-
if (ctx.spanId) info["spanId"] = ctx.spanId;
|
|
37
|
-
return info;
|
|
38
|
-
})(), winston.format.json()),
|
|
39
|
-
zippedArchive: true
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/** Set the service name for all loggers in this process. Call once at startup. */
|
|
43
|
-
function setServiceName(name) {
|
|
44
|
-
_serviceName = name;
|
|
45
|
-
for (const logger of _pendingFileTransportLoggers) {
|
|
46
|
-
const transport = buildFileTransport();
|
|
47
|
-
if (transport) logger.add(transport);
|
|
48
|
-
}
|
|
49
|
-
_pendingFileTransportLoggers.clear();
|
|
50
|
-
}
|
|
51
|
-
/** Get the current service name (for testing/inspection). */
|
|
52
|
-
function getServiceName() {
|
|
53
|
-
return _serviceName;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Build the rotated log filename. When a stable instance ID is available in the
|
|
57
|
-
* environment, append it so multiple processes (e.g. several orchestrators or
|
|
58
|
-
* agents) can safely share one KICI_LOG_DIR without racing on the same file.
|
|
59
|
-
*
|
|
60
|
-
* Precedence matches the tier that owns each variable:
|
|
61
|
-
* orchestrator (KICI_CLUSTER_INSTANCE_ID) > agent (KICI_AGENT_ID) > platform
|
|
62
|
-
* (KICI_PLATFORM_INSTANCE_ID). Sanitize defensively to filesystem-safe characters.
|
|
63
|
-
*/
|
|
64
|
-
function buildLogFilename(serviceName) {
|
|
65
|
-
const base = serviceName ?? "kici";
|
|
66
|
-
const suffix = (process.env.KICI_CLUSTER_INSTANCE_ID || process.env.KICI_AGENT_ID || process.env.KICI_PLATFORM_INSTANCE_ID || "").replace(/[^A-Za-z0-9_.-]+/g, "_");
|
|
67
|
-
return suffix ? `${base}-${suffix}-%DATE%.log` : `${base}-%DATE%.log`;
|
|
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
|
-
/**
|
|
83
|
-
* Create a winston logger instance.
|
|
84
|
-
*
|
|
85
|
-
* @param options - Logger configuration options
|
|
86
|
-
* @returns Configured winston logger
|
|
87
|
-
*/
|
|
88
|
-
function createLogger(options = {}) {
|
|
89
|
-
const { json = pickJsonDefault(), level = "info", prefix } = options;
|
|
90
|
-
const TOKEN_MASK_RE = /kat_[0-9a-f]{64}/gi;
|
|
91
|
-
const maskTokens = (value) => {
|
|
92
|
-
if (typeof value === "string") return value.replace(TOKEN_MASK_RE, "kat_***");
|
|
93
|
-
if (Array.isArray(value)) return value.map(maskTokens);
|
|
94
|
-
if (value !== null && typeof value === "object") {
|
|
95
|
-
const masked = {};
|
|
96
|
-
for (const [k, v] of Object.entries(value)) masked[k] = maskTokens(v);
|
|
97
|
-
return masked;
|
|
98
|
-
}
|
|
99
|
-
return value;
|
|
100
|
-
};
|
|
101
|
-
const tokenMaskFormat = winston.format((info) => {
|
|
102
|
-
if (typeof info.message === "string") info.message = info.message.replace(TOKEN_MASK_RE, "kat_***");
|
|
103
|
-
for (const key of Object.keys(info)) {
|
|
104
|
-
if (key === "level" || key === "message") continue;
|
|
105
|
-
info[key] = maskTokens(info[key]);
|
|
106
|
-
}
|
|
107
|
-
return info;
|
|
108
|
-
});
|
|
109
|
-
const traceContextFormat = winston.format((info) => {
|
|
110
|
-
if (_serviceName) info["service"] = _serviceName;
|
|
111
|
-
const ctx = getRequestContext();
|
|
112
|
-
if (ctx.requestId) info["requestId"] = ctx.requestId;
|
|
113
|
-
if (ctx.runId) info["runId"] = ctx.runId;
|
|
114
|
-
if (ctx.jobId) info["jobId"] = ctx.jobId;
|
|
115
|
-
if (ctx.routingKey) info["routingKey"] = ctx.routingKey;
|
|
116
|
-
if (ctx.traceId) info["traceId"] = ctx.traceId;
|
|
117
|
-
if (ctx.spanId) info["spanId"] = ctx.spanId;
|
|
118
|
-
return info;
|
|
119
|
-
});
|
|
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}`;
|
|
124
|
-
let coloredLevel;
|
|
125
|
-
switch (level) {
|
|
126
|
-
case "error":
|
|
127
|
-
coloredLevel = pc.red(level);
|
|
128
|
-
break;
|
|
129
|
-
case "warn":
|
|
130
|
-
coloredLevel = pc.yellow(level);
|
|
131
|
-
break;
|
|
132
|
-
case "debug":
|
|
133
|
-
coloredLevel = pc.gray(level);
|
|
134
|
-
break;
|
|
135
|
-
default: coloredLevel = level;
|
|
136
|
-
}
|
|
137
|
-
return `${traceStr}${coloredLevel}: ${prefixStr}${message}`;
|
|
138
|
-
});
|
|
139
|
-
const jsonFormat = winston.format.combine(winston.format.timestamp(), traceContextFormat(), tokenMaskFormat(), winston.format.json());
|
|
140
|
-
const prettyPipeline = winston.format.combine(traceContextFormat(), tokenMaskFormat(), prettyFormat);
|
|
141
|
-
const loggerInstance = winston.createLogger({
|
|
142
|
-
level,
|
|
143
|
-
format: json ? jsonFormat : prettyPipeline,
|
|
144
|
-
transports: [new winston.transports.Console()]
|
|
145
|
-
});
|
|
146
|
-
if (process.env.KICI_LOG_DIR) if (_serviceName) {
|
|
147
|
-
const transport = buildFileTransport();
|
|
148
|
-
if (transport) loggerInstance.add(transport);
|
|
149
|
-
} else _pendingFileTransportLoggers.add(loggerInstance);
|
|
150
|
-
return loggerInstance;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Default singleton logger instance for simple use cases. If KICI_LOG_DIR is
|
|
154
|
-
* set but setServiceName() hasn't been called yet (CLI tools, scripts),
|
|
155
|
-
* the file transport stays deferred and is attached only once a service
|
|
156
|
-
* name is known — protecting against writing to `kici-<instanceId>-*.log`.
|
|
157
|
-
*/
|
|
158
|
-
const logger = createLogger();
|
|
159
|
-
/**
|
|
160
|
-
* Wrap an async startup function so that any thrown error is logged
|
|
161
|
-
* through the structured (JSON-aware) logger before the process exits.
|
|
162
|
-
*
|
|
163
|
-
* Without this guard, a top-level `await` rejection in an ESM entry
|
|
164
|
-
* point is printed by Node.js's default handler (multi-line, not JSON),
|
|
165
|
-
* which breaks log aggregators like ELK.
|
|
166
|
-
*/
|
|
167
|
-
async function guardStartup(log, fn) {
|
|
168
|
-
try {
|
|
169
|
-
await fn();
|
|
170
|
-
} catch (error) {
|
|
171
|
-
log.error("Fatal startup error", {
|
|
172
|
-
error: toErrorMessage(error),
|
|
173
|
-
stack: error instanceof Error ? error.stack : void 0
|
|
174
|
-
});
|
|
175
|
-
process.exit(1);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
//#endregion
|
|
179
|
-
export { buildLogFilename, createLogger, getServiceName, guardStartup, logger, setServiceName };
|
|
180
|
-
|
|
181
|
-
//# sourceMappingURL=logger.js.map
|
package/dist/logger.test.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
-
/** Trace context fields propagated through the request lifecycle. */
|
|
3
|
-
export interface RequestContext {
|
|
4
|
-
/** Unique trace ID for a webhook event (UUIDv4). Always present once set. */
|
|
5
|
-
requestId: string;
|
|
6
|
-
/** Workflow run ID, set when a workflow run is created. */
|
|
7
|
-
runId?: string;
|
|
8
|
-
/** Job ID, set when processing a specific job. */
|
|
9
|
-
jobId?: string;
|
|
10
|
-
/** Routing key (e.g. "github:12345"), set when handling a webhook for a source. */
|
|
11
|
-
routingKey?: string;
|
|
12
|
-
/** OTel trace ID, set when telemetry is active. */
|
|
13
|
-
traceId?: string;
|
|
14
|
-
/** OTel span ID, set when telemetry is active. */
|
|
15
|
-
spanId?: string;
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* AsyncLocalStorage instance for propagating request context
|
|
19
|
-
* through async call chains without explicit parameter passing.
|
|
20
|
-
*
|
|
21
|
-
* Usage:
|
|
22
|
-
* ```ts
|
|
23
|
-
* requestContext.run({ requestId: crypto.randomUUID() }, async () => {
|
|
24
|
-
* // All code in this callback (and its async descendants) can read the context
|
|
25
|
-
* log.info('Processing webhook'); // auto-enriched with requestId
|
|
26
|
-
* });
|
|
27
|
-
* ```
|
|
28
|
-
*/
|
|
29
|
-
export declare const requestContext: AsyncLocalStorage<RequestContext>;
|
|
30
|
-
/**
|
|
31
|
-
* Get the current request context, or an empty object if outside a `run()` scope.
|
|
32
|
-
* Safe to call anywhere -- never throws.
|
|
33
|
-
*/
|
|
34
|
-
export declare function getRequestContext(): Partial<RequestContext>;
|
|
35
|
-
/**
|
|
36
|
-
* Merge additional fields into the current request context.
|
|
37
|
-
* No-op if called outside a `run()` scope.
|
|
38
|
-
*
|
|
39
|
-
* @param fields - Partial context fields to merge into the current store
|
|
40
|
-
*/
|
|
41
|
-
export declare function enrichRequestContext(fields: Partial<RequestContext>): void;
|
|
42
|
-
//# sourceMappingURL=request-context.d.ts.map
|
package/dist/request-context.js
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import "./chunk-gOLHoazu.js";
|
|
2
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
//#region src/request-context.ts
|
|
4
|
-
/**
|
|
5
|
-
* AsyncLocalStorage instance for propagating request context
|
|
6
|
-
* through async call chains without explicit parameter passing.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* ```ts
|
|
10
|
-
* requestContext.run({ requestId: crypto.randomUUID() }, async () => {
|
|
11
|
-
* // All code in this callback (and its async descendants) can read the context
|
|
12
|
-
* log.info('Processing webhook'); // auto-enriched with requestId
|
|
13
|
-
* });
|
|
14
|
-
* ```
|
|
15
|
-
*/
|
|
16
|
-
const requestContext = new AsyncLocalStorage();
|
|
17
|
-
/**
|
|
18
|
-
* Get the current request context, or an empty object if outside a `run()` scope.
|
|
19
|
-
* Safe to call anywhere -- never throws.
|
|
20
|
-
*/
|
|
21
|
-
function getRequestContext() {
|
|
22
|
-
return requestContext.getStore() ?? {};
|
|
23
|
-
}
|
|
24
|
-
/**
|
|
25
|
-
* Merge additional fields into the current request context.
|
|
26
|
-
* No-op if called outside a `run()` scope.
|
|
27
|
-
*
|
|
28
|
-
* @param fields - Partial context fields to merge into the current store
|
|
29
|
-
*/
|
|
30
|
-
function enrichRequestContext(fields) {
|
|
31
|
-
const store = requestContext.getStore();
|
|
32
|
-
if (store) Object.assign(store, fields);
|
|
33
|
-
}
|
|
34
|
-
//#endregion
|
|
35
|
-
export { enrichRequestContext, getRequestContext, requestContext };
|
|
36
|
-
|
|
37
|
-
//# sourceMappingURL=request-context.js.map
|
package/dist/zx.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Initialize zx for cross-platform execution.
|
|
3
|
-
* Sets the quote function required by zx 8+ on all platforms.
|
|
4
|
-
* On Windows, also configures pwsh as shell.
|
|
5
|
-
* Call this at the start of any script/binary entry point using zx.
|
|
6
|
-
*/
|
|
7
|
-
export declare function initZx(): void;
|
|
8
|
-
//# sourceMappingURL=zx.d.ts.map
|
package/dist/zx.js
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import "./chunk-gOLHoazu.js";
|
|
2
|
-
import { execSync } from "node:child_process";
|
|
3
|
-
import { $, quote, quotePowerShell, usePwsh } from "zx";
|
|
4
|
-
//#region src/zx.ts
|
|
5
|
-
const PWSH_INSTALL_DOCS = "https://learn.microsoft.com/en-us/powershell/scripting/install/install-powershell-on-windows";
|
|
6
|
-
/**
|
|
7
|
-
* Initialize zx for cross-platform execution.
|
|
8
|
-
* Sets the quote function required by zx 8+ on all platforms.
|
|
9
|
-
* On Windows, also configures pwsh as shell.
|
|
10
|
-
* Call this at the start of any script/binary entry point using zx.
|
|
11
|
-
*/
|
|
12
|
-
function initZx() {
|
|
13
|
-
if (process.platform === "win32") {
|
|
14
|
-
try {
|
|
15
|
-
usePwsh();
|
|
16
|
-
} catch {
|
|
17
|
-
ensurePwshWindows();
|
|
18
|
-
usePwsh();
|
|
19
|
-
}
|
|
20
|
-
$.quote = quotePowerShell;
|
|
21
|
-
} else $.quote = quote;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Ensure PowerShell Core (pwsh) is installed on Windows.
|
|
25
|
-
* Attempts automatic installation via winget using the built-in powershell.exe.
|
|
26
|
-
* If winget is unavailable, prints installation instructions and exits.
|
|
27
|
-
*/
|
|
28
|
-
function ensurePwshWindows() {
|
|
29
|
-
console.error("PowerShell Core (pwsh) is required but not found in PATH.\nKiCI uses pwsh for cross-platform command execution.\n");
|
|
30
|
-
let hasWinget = false;
|
|
31
|
-
try {
|
|
32
|
-
execSync("powershell.exe -NoProfile -Command \"Get-Command winget -ErrorAction Stop\"", {
|
|
33
|
-
stdio: "ignore",
|
|
34
|
-
timeout: 1e4
|
|
35
|
-
});
|
|
36
|
-
hasWinget = true;
|
|
37
|
-
} catch {}
|
|
38
|
-
if (!hasWinget) {
|
|
39
|
-
console.error(`Automatic installation is not possible (winget not found).
|
|
40
|
-
Please install PowerShell Core manually:
|
|
41
|
-
${PWSH_INSTALL_DOCS}\n`);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
console.error("Attempting to install PowerShell Core via winget...\n");
|
|
45
|
-
try {
|
|
46
|
-
execSync("powershell.exe -NoProfile -Command \"winget install --id Microsoft.PowerShell --accept-source-agreements --accept-package-agreements -e --silent\"", {
|
|
47
|
-
stdio: "inherit",
|
|
48
|
-
timeout: 3e5
|
|
49
|
-
});
|
|
50
|
-
} catch {
|
|
51
|
-
console.error(`
|
|
52
|
-
Automatic installation failed.
|
|
53
|
-
Please install PowerShell Core manually:
|
|
54
|
-
${PWSH_INSTALL_DOCS}\n`);
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
const pwshPath = execSync("powershell.exe -NoProfile -Command \"(Get-Command pwsh -ErrorAction Stop).Source\"", {
|
|
59
|
-
encoding: "utf-8",
|
|
60
|
-
timeout: 1e4
|
|
61
|
-
}).trim();
|
|
62
|
-
if (pwshPath) {
|
|
63
|
-
const pwshDir = pwshPath.replace(/\\pwsh\.exe$/i, "");
|
|
64
|
-
process.env.PATH = `${pwshDir};${process.env.PATH}`;
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
console.error(`
|
|
68
|
-
PowerShell Core was installed but cannot be found in PATH.
|
|
69
|
-
Please restart your terminal or add pwsh to PATH manually.
|
|
70
|
-
See: ${PWSH_INSTALL_DOCS}\n`);
|
|
71
|
-
process.exit(1);
|
|
72
|
-
}
|
|
73
|
-
console.error("PowerShell Core installed successfully.\n");
|
|
74
|
-
}
|
|
75
|
-
//#endregion
|
|
76
|
-
export { initZx };
|
|
77
|
-
|
|
78
|
-
//# sourceMappingURL=zx.js.map
|