@kici-dev/shared 0.0.0 → 0.1.0

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.
Files changed (105) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +1 -6
  3. package/dist/chunk-gOLHoazu.js +4 -0
  4. package/dist/cold-store/bucket.d.ts +26 -0
  5. package/dist/cold-store/bucket.js +40 -0
  6. package/dist/cold-store/bucket.test.d.ts +2 -0
  7. package/dist/cold-store/chunk-encoder.d.ts +63 -0
  8. package/dist/cold-store/chunk-encoder.js +94 -0
  9. package/dist/cold-store/chunk-encoder.test.d.ts +2 -0
  10. package/dist/cold-store/chunk-id.d.ts +10 -0
  11. package/dist/cold-store/chunk-id.js +29 -0
  12. package/dist/cold-store/chunk-id.test.d.ts +2 -0
  13. package/dist/cold-store/cold-store.d.ts +345 -0
  14. package/dist/cold-store/cold-store.js +1078 -0
  15. package/dist/cold-store/cold-store.test.d.ts +2 -0
  16. package/dist/cold-store/config.d.ts +62 -0
  17. package/dist/cold-store/config.js +28 -0
  18. package/dist/cold-store/index.d.ts +20 -0
  19. package/dist/cold-store/index.js +11 -0
  20. package/dist/cold-store/key.d.ts +89 -0
  21. package/dist/cold-store/key.js +88 -0
  22. package/dist/cold-store/key.test.d.ts +2 -0
  23. package/dist/cold-store/lru.d.ts +33 -0
  24. package/dist/cold-store/lru.js +59 -0
  25. package/dist/cold-store/lru.test.d.ts +2 -0
  26. package/dist/cold-store/manifest.d.ts +11 -0
  27. package/dist/cold-store/manifest.js +57 -0
  28. package/dist/cold-store/manifest.test.d.ts +2 -0
  29. package/dist/cold-store/metrics.d.ts +103 -0
  30. package/dist/cold-store/metrics.js +179 -0
  31. package/dist/cold-store/table-adapter.d.ts +229 -0
  32. package/dist/cold-store/table-adapter.js +2 -0
  33. package/dist/cold-store/types.d.ts +111 -0
  34. package/dist/cold-store/types.js +2 -0
  35. package/dist/crypto.d.ts +33 -0
  36. package/dist/crypto.js +67 -0
  37. package/dist/db-admin.d.ts +1422 -0
  38. package/dist/db-admin.js +2525 -0
  39. package/dist/db-admin.test.d.ts +2 -0
  40. package/dist/db.d.ts +14 -0
  41. package/dist/db.js +23 -0
  42. package/dist/env/allowlist.d.ts +79 -0
  43. package/dist/env/allowlist.js +86 -0
  44. package/dist/env/define-env.d.ts +172 -0
  45. package/dist/env/define-env.js +295 -0
  46. package/dist/env/define-env.test.d.ts +2 -0
  47. package/dist/env/env-rule-allowlist.test.d.ts +2 -0
  48. package/dist/env/index.d.ts +11 -0
  49. package/dist/env/index.js +4 -0
  50. package/dist/env/logger-env.d.ts +30 -0
  51. package/dist/env/logger-env.js +95 -0
  52. package/dist/error.d.ts +16 -0
  53. package/dist/error.js +58 -0
  54. package/dist/error.test.d.ts +2 -0
  55. package/dist/format-bytes.d.ts +5 -0
  56. package/dist/format-bytes.js +15 -0
  57. package/dist/format-bytes.test.d.ts +2 -0
  58. package/dist/format-duration.d.ts +11 -0
  59. package/dist/format-duration.js +32 -0
  60. package/dist/format-duration.test.d.ts +2 -0
  61. package/dist/graceful-shutdown.d.ts +58 -0
  62. package/dist/graceful-shutdown.js +71 -0
  63. package/dist/graceful-shutdown.test.d.ts +2 -0
  64. package/dist/idempotency-files.d.ts +113 -0
  65. package/dist/idempotency-files.js +190 -0
  66. package/dist/idempotency-files.test.d.ts +2 -0
  67. package/dist/idempotency.d.ts +46 -0
  68. package/dist/idempotency.js +45 -0
  69. package/dist/idempotency.test.d.ts +2 -0
  70. package/dist/index.d.ts +19 -0
  71. package/dist/index.js +31 -0
  72. package/dist/logger.d.ts +57 -0
  73. package/dist/logger.js +175 -0
  74. package/dist/logger.test.d.ts +2 -0
  75. package/dist/reconnect-delay.d.ts +11 -0
  76. package/dist/reconnect-delay.js +22 -0
  77. package/dist/request-context.d.ts +42 -0
  78. package/dist/request-context.js +37 -0
  79. package/dist/ring-buffer.d.ts +35 -0
  80. package/dist/ring-buffer.js +62 -0
  81. package/dist/ring-buffer.test.d.ts +2 -0
  82. package/dist/routes/health.d.ts +26 -0
  83. package/dist/routes/health.js +45 -0
  84. package/dist/routes/health.test.d.ts +2 -0
  85. package/dist/routes/metrics.d.ts +20 -0
  86. package/dist/routes/metrics.js +38 -0
  87. package/dist/s3-client.d.ts +42 -0
  88. package/dist/s3-client.js +25 -0
  89. package/dist/telemetry/index.d.ts +3 -0
  90. package/dist/telemetry/index.js +4 -0
  91. package/dist/telemetry/init.d.ts +20 -0
  92. package/dist/telemetry/init.js +42 -0
  93. package/dist/telemetry/init.test.d.ts +2 -0
  94. package/dist/telemetry/metrics.d.ts +8 -0
  95. package/dist/telemetry/metrics.js +16 -0
  96. package/dist/tool-check.d.ts +33 -0
  97. package/dist/tool-check.js +60 -0
  98. package/dist/tool-check.test.d.ts +2 -0
  99. package/dist/ts-loader-hook.d.ts +26 -0
  100. package/dist/ts-loader-hook.js +48 -0
  101. package/dist/zx.d.ts +8 -0
  102. package/dist/zx.js +78 -0
  103. package/package.json +69 -5
  104. package/sbom.spdx.json +8775 -0
  105. package/index.js +0 -3
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=env-rule-allowlist.test.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @kici-dev/shared/env — env schema helpers shared across services.
3
+ *
4
+ * Subpath export so server-side packages can `import { defineEnv } from
5
+ * '@kici-dev/shared/env'`. This subpath is intentionally NOT re-exported
6
+ * from the package root: it pulls Zod and is only meaningful in Node-side
7
+ * config loaders, not from browser code.
8
+ */
9
+ export { defineEnv, validateUnknownKiciVars, RESERVED_NON_SCHEMA_KICI_VARS, RESERVED_NON_SCHEMA_KICI_PREFIXES, type DefineEnvOptions, type DefineEnvResult, type EnvFieldSpec, type EnvMap, type EnvMapValue, type ValidateUnknownKiciVarsOptions, } from './define-env.js';
10
+ export { LoggerEnvSchema, LOGGER_ENV_VARS, LOGGER_ENV_FIELD_SPECS } from './logger-env.js';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,4 @@
1
+ import "../chunk-gOLHoazu.js";
2
+ import { RESERVED_NON_SCHEMA_KICI_PREFIXES, RESERVED_NON_SCHEMA_KICI_VARS, defineEnv, validateUnknownKiciVars } from "./define-env.js";
3
+ import { LOGGER_ENV_FIELD_SPECS, LOGGER_ENV_VARS, LoggerEnvSchema } from "./logger-env.js";
4
+ export { LOGGER_ENV_FIELD_SPECS, LOGGER_ENV_VARS, LoggerEnvSchema, RESERVED_NON_SCHEMA_KICI_PREFIXES, RESERVED_NON_SCHEMA_KICI_VARS, defineEnv, validateUnknownKiciVars };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Shared LoggerEnv schema.
3
+ *
4
+ * The logger (packages/shared/src/logger.ts) reads these env vars *before*
5
+ * the per-service config loads, so we can't include them in the service
6
+ * schemas the normal way. We still want them in `docs/operator/env-reference.md`
7
+ * and in `validateUnknownKiciVars()`'s known-var set, so this schema documents
8
+ * them in one place. Each service includes the keys here when computing its
9
+ * "known KICI_* vars" list, and the docs generator emits a "Logger / shared"
10
+ * section from this schema.
11
+ *
12
+ * IMPORTANT: do not change the runtime behaviour of `logger.ts` from this
13
+ * schema — the schema is documentation + the unknown-var allowlist, not the
14
+ * source of truth for the logger.
15
+ */
16
+ import { z } from 'zod';
17
+ import type { EnvFieldSpec } from './define-env.js';
18
+ export declare const LoggerEnvSchema: z.ZodObject<{
19
+ KICI_LOG_DIR: z.ZodOptional<z.ZodString>;
20
+ KICI_LOG_MAX_SIZE: z.ZodDefault<z.ZodString>;
21
+ KICI_LOG_RETENTION_DAYS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
22
+ KICI_CLUSTER_INSTANCE_ID: z.ZodOptional<z.ZodString>;
23
+ KICI_AGENT_ID: z.ZodOptional<z.ZodString>;
24
+ KICI_PLATFORM_INSTANCE_ID: z.ZodOptional<z.ZodString>;
25
+ }, z.core.$strip>;
26
+ /** All env vars the logger reads, for the unknown-KICI-var scanner. */
27
+ export declare const LOGGER_ENV_VARS: readonly ["KICI_LOG_DIR", "KICI_LOG_MAX_SIZE", "KICI_LOG_RETENTION_DAYS", "KICI_CLUSTER_INSTANCE_ID", "KICI_AGENT_ID", "KICI_PLATFORM_INSTANCE_ID"];
28
+ /** Doc-friendly description map (consumed by the env-reference generator). */
29
+ export declare const LOGGER_ENV_FIELD_SPECS: EnvFieldSpec[];
30
+ //# sourceMappingURL=logger-env.d.ts.map
@@ -0,0 +1,95 @@
1
+ import "../chunk-gOLHoazu.js";
2
+ import { z } from "zod";
3
+ //#region src/env/logger-env.ts
4
+ /**
5
+ * Shared LoggerEnv schema.
6
+ *
7
+ * The logger (packages/shared/src/logger.ts) reads these env vars *before*
8
+ * the per-service config loads, so we can't include them in the service
9
+ * schemas the normal way. We still want them in `docs/operator/env-reference.md`
10
+ * and in `validateUnknownKiciVars()`'s known-var set, so this schema documents
11
+ * them in one place. Each service includes the keys here when computing its
12
+ * "known KICI_* vars" list, and the docs generator emits a "Logger / shared"
13
+ * section from this schema.
14
+ *
15
+ * IMPORTANT: do not change the runtime behaviour of `logger.ts` from this
16
+ * schema — the schema is documentation + the unknown-var allowlist, not the
17
+ * source of truth for the logger.
18
+ */
19
+ const LoggerEnvSchema = z.object({
20
+ KICI_LOG_DIR: z.string().optional(),
21
+ KICI_LOG_MAX_SIZE: z.string().default("500m"),
22
+ KICI_LOG_RETENTION_DAYS: z.coerce.number().default(7),
23
+ /** Set by the orchestrator process; used as a filename suffix. */
24
+ KICI_CLUSTER_INSTANCE_ID: z.string().optional(),
25
+ /** Set by the agent process; used as a filename suffix. */
26
+ KICI_AGENT_ID: z.string().optional(),
27
+ /** Set by the platform process; used as a filename suffix. */
28
+ KICI_PLATFORM_INSTANCE_ID: z.string().optional()
29
+ });
30
+ /** All env vars the logger reads, for the unknown-KICI-var scanner. */
31
+ const LOGGER_ENV_VARS = [
32
+ "KICI_LOG_DIR",
33
+ "KICI_LOG_MAX_SIZE",
34
+ "KICI_LOG_RETENTION_DAYS",
35
+ "KICI_CLUSTER_INSTANCE_ID",
36
+ "KICI_AGENT_ID",
37
+ "KICI_PLATFORM_INSTANCE_ID"
38
+ ];
39
+ /** Doc-friendly description map (consumed by the env-reference generator). */
40
+ const LOGGER_ENV_FIELD_SPECS = [
41
+ {
42
+ envVar: "KICI_CLUSTER_INSTANCE_ID",
43
+ aliases: [],
44
+ fieldPath: "KICI_CLUSTER_INSTANCE_ID",
45
+ required: false,
46
+ type: "string",
47
+ description: "Stable orchestrator identifier; appended to the log filename so multiple instances can share one KICI_LOG_DIR."
48
+ },
49
+ {
50
+ envVar: "KICI_AGENT_ID",
51
+ aliases: [],
52
+ fieldPath: "KICI_AGENT_ID",
53
+ required: false,
54
+ type: "string",
55
+ description: "Stable agent identifier; appended to the agent log filename so multiple agents can share one KICI_LOG_DIR."
56
+ },
57
+ {
58
+ envVar: "KICI_LOG_DIR",
59
+ aliases: [],
60
+ fieldPath: "KICI_LOG_DIR",
61
+ required: false,
62
+ type: "string",
63
+ description: "Directory for rotated JSON log files. When unset, the logger only writes to stdout/stderr."
64
+ },
65
+ {
66
+ envVar: "KICI_LOG_MAX_SIZE",
67
+ aliases: [],
68
+ fieldPath: "KICI_LOG_MAX_SIZE",
69
+ required: false,
70
+ defaultValue: "\"500m\"",
71
+ type: "string",
72
+ description: "Per-file size cap for rotated logs. Accepts a numeric byte count or a size suffix (`k`, `m`, `g`)."
73
+ },
74
+ {
75
+ envVar: "KICI_LOG_RETENTION_DAYS",
76
+ aliases: [],
77
+ fieldPath: "KICI_LOG_RETENTION_DAYS",
78
+ required: false,
79
+ defaultValue: "7",
80
+ type: "number",
81
+ description: "How many days of rotated logs to keep before deletion."
82
+ },
83
+ {
84
+ envVar: "KICI_PLATFORM_INSTANCE_ID",
85
+ aliases: [],
86
+ fieldPath: "KICI_PLATFORM_INSTANCE_ID",
87
+ required: false,
88
+ type: "string",
89
+ description: "Stable Platform identifier; appended to the platform log filename so multiple instances can share one KICI_LOG_DIR."
90
+ }
91
+ ];
92
+ //#endregion
93
+ export { LOGGER_ENV_FIELD_SPECS, LOGGER_ENV_VARS, LoggerEnvSchema };
94
+
95
+ //# sourceMappingURL=logger-env.js.map
@@ -0,0 +1,16 @@
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 ADDED
@@ -0,0 +1,58 @@
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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=error.test.d.ts.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Format bytes to a human-readable string.
3
+ */
4
+ export declare function formatBytes(bytes: number): string;
5
+ //# sourceMappingURL=format-bytes.d.ts.map
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=format-bytes.test.d.ts.map
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,32 @@
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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=format-duration.test.d.ts.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * A single teardown step in the graceful shutdown sequence.
3
+ */
4
+ export interface ShutdownStep {
5
+ /** Human-readable name logged before/after execution. */
6
+ name: string;
7
+ /** Teardown function. Runs sequentially; errors are caught per-step. */
8
+ fn: () => Promise<void> | void;
9
+ }
10
+ /**
11
+ * Logger interface accepted by {@link setupGracefulShutdown}.
12
+ * Compatible with Winston and any logger that exposes `info`, `warn`, and `error`
13
+ * with a structured-metadata second argument.
14
+ */
15
+ export interface ShutdownLogger {
16
+ info(msg: string, meta?: Record<string, unknown>): void;
17
+ warn(msg: string, meta?: Record<string, unknown>): void;
18
+ error(msg: string, meta?: Record<string, unknown>): void;
19
+ }
20
+ export interface GracefulShutdownOptions {
21
+ /** Logger used for all shutdown messages. */
22
+ logger: ShutdownLogger;
23
+ /** Ordered list of teardown steps executed sequentially. */
24
+ steps: ShutdownStep[];
25
+ /** Force-exit timeout in milliseconds (default: 30 000). */
26
+ timeoutMs?: number;
27
+ /**
28
+ * Optional callback invoked when the force-exit timeout fires.
29
+ * If the callback returns `true`, the default `process.exit(1)` is
30
+ * suppressed — the callback is responsible for exiting the process.
31
+ * Use this for last-resort cleanup (e.g., killing child processes
32
+ * and deferring exit to let abort handlers run).
33
+ */
34
+ onForceExit?: () => boolean | void;
35
+ /**
36
+ * If `true`, `uncaughtException` and `unhandledRejection` handlers
37
+ * are NOT registered. Useful when the caller manages those separately.
38
+ * Default: `false` (handlers are registered).
39
+ */
40
+ skipErrorHandlers?: boolean;
41
+ }
42
+ /**
43
+ * Return value from {@link setupGracefulShutdown} so callers can
44
+ * trigger shutdown programmatically (e.g., from SIGUSR1 drain handlers).
45
+ */
46
+ export interface ShutdownHandle {
47
+ /** Trigger shutdown with the given reason string. */
48
+ shutdown(signal: string): Promise<void>;
49
+ }
50
+ /**
51
+ * Wire up SIGTERM / SIGINT (and optionally uncaughtException /
52
+ * unhandledRejection) handlers that execute the provided teardown
53
+ * steps sequentially, then `process.exit(0)`.
54
+ *
55
+ * A force-exit timer ensures the process terminates even if a step hangs.
56
+ */
57
+ export declare function setupGracefulShutdown(options: GracefulShutdownOptions): ShutdownHandle;
58
+ //# sourceMappingURL=graceful-shutdown.d.ts.map
@@ -0,0 +1,71 @@
1
+ import "./chunk-gOLHoazu.js";
2
+ import { toErrorMessage } from "./error.js";
3
+ //#region src/graceful-shutdown.ts
4
+ /**
5
+ * Wire up SIGTERM / SIGINT (and optionally uncaughtException /
6
+ * unhandledRejection) handlers that execute the provided teardown
7
+ * steps sequentially, then `process.exit(0)`.
8
+ *
9
+ * A force-exit timer ensures the process terminates even if a step hangs.
10
+ */
11
+ function setupGracefulShutdown(options) {
12
+ const { logger, steps, timeoutMs = 3e4, onForceExit, skipErrorHandlers = false } = options;
13
+ let isShuttingDown = false;
14
+ async function gracefulShutdown(signal) {
15
+ if (isShuttingDown) {
16
+ logger.warn("Shutdown already in progress, ignoring signal", { signal });
17
+ return;
18
+ }
19
+ isShuttingDown = true;
20
+ logger.info(`Received ${signal}, starting graceful shutdown...`);
21
+ const forceExitTimeout = setTimeout(() => {
22
+ logger.error(`Graceful shutdown timed out after ${timeoutMs / 1e3}s, forcing exit`);
23
+ if (onForceExit?.() !== true) process.exit(1);
24
+ }, timeoutMs);
25
+ try {
26
+ for (const step of steps) try {
27
+ logger.info(`${step.name}...`);
28
+ await step.fn();
29
+ logger.info(`${step.name} done`);
30
+ } catch (stepError) {
31
+ logger.error(`${step.name} failed`, {
32
+ error: toErrorMessage(stepError),
33
+ stack: stepError instanceof Error ? stepError.stack : void 0
34
+ });
35
+ }
36
+ clearTimeout(forceExitTimeout);
37
+ logger.info("Graceful shutdown complete");
38
+ process.exit(0);
39
+ } catch (error) {
40
+ logger.error("Error during graceful shutdown", {
41
+ error: toErrorMessage(error),
42
+ stack: error instanceof Error ? error.stack : void 0
43
+ });
44
+ clearTimeout(forceExitTimeout);
45
+ process.exit(1);
46
+ }
47
+ }
48
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
49
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
50
+ if (!skipErrorHandlers) {
51
+ process.on("uncaughtException", (error) => {
52
+ logger.error("Uncaught exception", {
53
+ error: error.message,
54
+ stack: error.stack
55
+ });
56
+ gracefulShutdown("uncaughtException");
57
+ });
58
+ process.on("unhandledRejection", (reason) => {
59
+ logger.error("Unhandled rejection", {
60
+ reason: toErrorMessage(reason),
61
+ stack: reason instanceof Error ? reason.stack : void 0
62
+ });
63
+ gracefulShutdown("unhandledRejection");
64
+ });
65
+ }
66
+ return { shutdown: gracefulShutdown };
67
+ }
68
+ //#endregion
69
+ export { setupGracefulShutdown };
70
+
71
+ //# sourceMappingURL=graceful-shutdown.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=graceful-shutdown.test.d.ts.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * File-drift preview primitive for idempotent steps that copy or rsync
3
+ * files to a remote target. Companion to `idempotency.ts`: a `check()`
4
+ * that ships files can call into the helper layer (e.g. `previewRsync*`
5
+ * in `packages/ci/src/deploy-prod/remote.ts`) to get a typed list of
6
+ * per-file changes, then surface those entries inside the step's drift
7
+ * value so the confirm prompt shows the operator exactly which files
8
+ * would change and how.
9
+ *
10
+ * The source of truth for the categorisation is rsync's own
11
+ * `--itemize-changes` (`-i`) output, captured under `--dry-run` (`-n`).
12
+ * Running rsync with the same flags the apply path uses guarantees the
13
+ * preview and the actual transfer agree.
14
+ *
15
+ * The itemize code rsync prints is 11 characters: `YXcstpoguax`.
16
+ * Y = update type (`<` sent, `>` received, `.` no transfer, `*` message, `c`/`h`)
17
+ * X = file type (`f` file, `d` directory, `L` symlink, …)
18
+ * c = checksum mismatch (or `+` block when newly created)
19
+ * s = size mismatch
20
+ * t = mtime mismatch
21
+ * p = perms mismatch
22
+ * o = owner mismatch
23
+ * g = group mismatch
24
+ * u = (sub-second) mtime mismatch
25
+ * a = ACL mismatch
26
+ * x = xattr mismatch
27
+ * Newly-created files render as `<f+++++++++` (or `cf+++++++++` for local
28
+ * creates). See `rsync(1)` § "ITEMIZED OUTPUT".
29
+ */
30
+ export type FileChangeCategory = 'new' | 'content' | 'mode' | 'time-only';
31
+ export type ContentSkipReason = 'binary' | 'too-large' | 'read-failed' | 'sensitive';
32
+ /**
33
+ * Per-file drift entry as carried inside a phase's Drift value.
34
+ * `localPath` and `remotePath` are filled in by the caller that owns the
35
+ * rsync invocation (the parser only sees the relative path rsync prints).
36
+ *
37
+ * `localContent` / `remoteContent` are populated by the caller when the
38
+ * payload is small UTF-8 text — they drive the inline unified-diff block
39
+ * in the confirm prompt. `contentSkipped` flags the cases where content
40
+ * was intentionally not captured (binary, too large, read failure) so
41
+ * the renderer can show an explicit "no diff available" row instead of
42
+ * silently dropping the entry.
43
+ */
44
+ export interface FileDriftEntry {
45
+ localPath: string;
46
+ remotePath: string;
47
+ category: FileChangeCategory;
48
+ /** Raw 11-char rsync itemize code, kept for `--debug`-style operator output. */
49
+ itemizeCode: string;
50
+ /** Local file bytes (UTF-8). Set for category `new` / `content` when content was successfully captured. */
51
+ localContent?: string;
52
+ /** Remote file bytes (UTF-8). Set for category `content` when content was successfully captured. Undefined for `new`. */
53
+ remoteContent?: string;
54
+ /** Set when content capture was intentionally skipped — used by the renderer to explain the gap. */
55
+ contentSkipped?: ContentSkipReason;
56
+ }
57
+ /** Low-level parse output: category + the relative path rsync printed. */
58
+ export interface ItemizeRecord {
59
+ itemizeCode: string;
60
+ relativePath: string;
61
+ category: FileChangeCategory;
62
+ }
63
+ /**
64
+ * Parse one line of `rsync --itemize-changes` output. Returns null for
65
+ * lines that are not actionable file drift entries:
66
+ * - "sending incremental file list" header line
67
+ * - blank / whitespace-only lines
68
+ * - summary lines ("sent N bytes received M bytes …", "total size is X")
69
+ * - directory metadata entries (e.g. `.d..t...... ./`) — directories
70
+ * are created by rsync as a side-effect of file entries, no separate
71
+ * reporting needed
72
+ * - in-sync entries (rsync emits these only at -vv; we run at -v, so
73
+ * they normally don't appear — but we filter defensively)
74
+ */
75
+ export declare function parseItemizeLine(line: string): ItemizeRecord | null;
76
+ export interface RenderDriftOpts {
77
+ /** When true, append a unified-diff block under each `new`/`content` entry. */
78
+ withContent?: boolean;
79
+ /** When true, color the diff with ANSI red/green via picocolors. */
80
+ color?: boolean;
81
+ /** Cap on diff body lines per entry (excluding header). Default 200. */
82
+ maxLines?: number;
83
+ }
84
+ /**
85
+ * Render the unified-diff block for a single FileDriftEntry. Returns an
86
+ * array of pre-indented lines (no surrounding indent applied by this
87
+ * function — the caller decides nesting). The first line is the diff
88
+ * header (`--- a/path` / `+++ b/path`), followed by hunk bodies.
89
+ *
90
+ * `mode` and `time-only` entries return an empty array — no diff to show.
91
+ * Entries with `contentSkipped` set return a single line explaining the
92
+ * skip reason. Entries with no content captured (older callers that
93
+ * didn't fill the fields) also return empty — the caller falls back to
94
+ * the label-only row.
95
+ */
96
+ export declare function renderFileDriftWithDiff(entry: FileDriftEntry, opts?: Pick<RenderDriftOpts, 'color' | 'maxLines'>): string[];
97
+ /**
98
+ * Render a FileDriftEntry[] as an aligned multi-line block suitable for
99
+ * inclusion in a confirm-prompt summary.
100
+ *
101
+ * 3 file(s) drifted: 1 new, 1 content, 1 mode
102
+ * NEW placeholder.html
103
+ * CONTENT haproxy.cfg
104
+ * MODE scripts/certbot-pre-stop.sh
105
+ *
106
+ * When `opts.withContent` is true, each `new`/`content` row is followed
107
+ * by an indented unified-diff block (colored if `opts.color`).
108
+ *
109
+ * Returns the rendered lines (no surrounding indent applied). Caller
110
+ * decides how to nest the block in a wider prompt.
111
+ */
112
+ export declare function renderFileDrifts(entries: FileDriftEntry[], opts?: RenderDriftOpts): string[];
113
+ //# sourceMappingURL=idempotency-files.d.ts.map