@ariestools/cli-kit-node 1.0.0 → 1.0.1

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  Node.js process adapter for reusable actor command-line applications.
4
4
 
5
- The package supplies `nodeProcessHost`, a production `ProcessHost` backed by the active Node.js process. It exposes arguments and environment values through live getters, routes output through the Node console, creates a scoped readline interface for interactive questions, translates `SIGINT` into the structural interrupt contract, and delegates exit requests to `process.exit`.
5
+ The package supplies `nodeProcessHost`, a production `ProcessHost` backed by the active Node.js process. It exposes arguments and environment values through live getters, routes output through the Node console, creates a scoped readline interface for interactive questions, translates `SIGINT` and `SIGTERM` into the structural interrupt contract, and delegates exit requests to `process.exit`. When the default interrupt signals must be narrowed or extended, `createNodeProcessHost({ signals })` builds a host bound to an explicit signal list; disposal always removes every bound signal listener.
6
6
 
7
7
  ```ts
8
8
  import { runProcessApplication } from '@ariestools/cli-kit'
@@ -16,6 +16,38 @@ await runProcessApplication({
16
16
  })
17
17
  ```
18
18
 
19
+ ## Dotenv composition
20
+
21
+ Prefer loading local `.env` values into the host environment instead of mutating `process.env`. File values act as defaults: defined process environment entries always win.
22
+
23
+ ```ts
24
+ import { createNodeProcessHostWithDotEnv } from '@ariestools/cli-kit-node'
25
+ import { environmentToYargsConfig, runYargsApplication } from '@ariestools/cli-kit-yargs'
26
+
27
+ const host = createNodeProcessHostWithDotEnv()
28
+ // or explicitly:
29
+ // createNodeProcessHost({
30
+ // environmentDefaults: loadDotEnvFile({ path: '.env' }),
31
+ // })
32
+
33
+ await runYargsApplication({
34
+ host,
35
+ configure: parser => parser
36
+ .config(environmentToYargsConfig(host.environment, 'XL1'))
37
+ .scriptName('xl1')
38
+ .help(),
39
+ })
40
+ ```
41
+
42
+ Helpers:
43
+
44
+ | Export | Role |
45
+ |--------|------|
46
+ | `parseDotEnv` | Pure dotenv-format string parser |
47
+ | `loadDotEnvFile` | Read a file (missing → `{}`); never mutates `process.env` |
48
+ | `mergeEnvironments` | Primary-first merge for env layers |
49
+ | `createNodeProcessHostWithDotEnv` | Host with `.env` merged under live `process.env` |
50
+
19
51
  Application code should depend on the `ProcessHost` contract from `@ariestools/cli-kit`. Use this package only at the Node.js composition boundary.
20
52
 
21
53
  ## License
@@ -1,2 +1,5 @@
1
+ export * from './loadDotEnvFile.ts';
2
+ export * from './mergeEnvironments.ts';
1
3
  export * from './nodeProcessHost.ts';
4
+ export * from './parseDotEnv.ts';
2
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,wBAAwB,CAAA;AACtC,cAAc,sBAAsB,CAAA;AACpC,cAAc,kBAAkB,CAAA"}
@@ -1,12 +1,81 @@
1
- // src/nodeProcessHost.ts
1
+ // src/loadDotEnvFile.ts
2
+ import fs from "node:fs";
3
+ import path from "node:path";
2
4
  import PROCESS from "node:process";
5
+
6
+ // src/parseDotEnv.ts
7
+ function parseDotEnv(content) {
8
+ const result = {};
9
+ const text = content.codePointAt(0) === 65279 ? content.slice(1) : content;
10
+ const linePattern = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg;
11
+ let match;
12
+ while ((match = linePattern.exec(text)) !== null) {
13
+ const key = match[1];
14
+ if (key === void 0) continue;
15
+ let value = (match[2] ?? "").trim();
16
+ const isDoubleQuoted = value.startsWith('"') && value.endsWith('"');
17
+ const isSingleQuoted = value.startsWith("'") && value.endsWith("'");
18
+ const isBacktickQuoted = value.startsWith("`") && value.endsWith("`");
19
+ if (isSingleQuoted || isDoubleQuoted || isBacktickQuoted) {
20
+ value = value.slice(1, -1);
21
+ if (isDoubleQuoted) {
22
+ value = value.replaceAll(String.raw`\n`, "\n").replaceAll(String.raw`\r`, "\r").replaceAll(String.raw`\\"`, '"').replaceAll(String.raw`\\`, "\\");
23
+ }
24
+ } else {
25
+ value = value.replace(/\s+#.*$/, "").trim();
26
+ }
27
+ result[key] = value;
28
+ }
29
+ return result;
30
+ }
31
+
32
+ // src/loadDotEnvFile.ts
33
+ function isErrnoException(error) {
34
+ return typeof error === "object" && error !== null && "code" in error;
35
+ }
36
+ function loadDotEnvFile(options = {}) {
37
+ const cwd = options.cwd ?? PROCESS.cwd();
38
+ const relativePath = options.path ?? ".env";
39
+ const absolutePath = path.isAbsolute(relativePath) ? relativePath : path.resolve(cwd, relativePath);
40
+ try {
41
+ return parseDotEnv(fs.readFileSync(absolutePath, "utf8"));
42
+ } catch (error) {
43
+ if (isErrnoException(error) && error.code === "ENOENT") return {};
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ // src/mergeEnvironments.ts
49
+ function resolveEnvironmentValue(key, layers) {
50
+ for (const layer of layers) {
51
+ if (!Object.hasOwn(layer, key)) continue;
52
+ const value = layer[key];
53
+ if (value !== void 0) return value;
54
+ }
55
+ return void 0;
56
+ }
57
+ function mergeEnvironments(primary, ...fallbacks) {
58
+ const layers = [primary, ...fallbacks];
59
+ const keys = /* @__PURE__ */ new Set();
60
+ for (const layer of layers) {
61
+ for (const key of Object.keys(layer)) keys.add(key);
62
+ }
63
+ const result = {};
64
+ for (const key of keys) {
65
+ result[key] = resolveEnvironmentValue(key, layers);
66
+ }
67
+ return result;
68
+ }
69
+
70
+ // src/nodeProcessHost.ts
71
+ import PROCESS2 from "node:process";
3
72
  import { createInterface } from "node:readline/promises";
4
73
  var nodeProcessIO = {
5
74
  get columns() {
6
- return PROCESS.stdout.columns;
75
+ return PROCESS2.stdout.columns;
7
76
  },
8
77
  get isInteractive() {
9
- return PROCESS.stdin.isTTY && PROCESS.stdout.isTTY;
78
+ return PROCESS2.stdin.isTTY && PROCESS2.stdout.isTTY;
10
79
  },
11
80
  error(...values) {
12
81
  console.error(...values);
@@ -15,7 +84,7 @@ var nodeProcessIO = {
15
84
  console.log(...values);
16
85
  },
17
86
  async question(prompt) {
18
- const readline = createInterface({ input: PROCESS.stdin, output: PROCESS.stdout });
87
+ const readline = createInterface({ input: PROCESS2.stdin, output: PROCESS2.stdout });
19
88
  try {
20
89
  return await readline.question(prompt);
21
90
  } finally {
@@ -26,33 +95,64 @@ var nodeProcessIO = {
26
95
  console.warn(...values);
27
96
  }
28
97
  };
29
- var nodeProcessHost = {
30
- get argv() {
31
- return PROCESS.argv;
32
- },
33
- get environment() {
34
- return PROCESS.env;
35
- },
36
- exit(code) {
37
- PROCESS.exit(code);
38
- },
39
- get io() {
40
- return nodeProcessIO;
41
- },
42
- get isDevelopment() {
43
- return PROCESS.env.NODE_ENV === "development";
44
- },
45
- onInterrupt(listener) {
46
- const handleInterrupt = () => {
47
- void listener();
48
- };
49
- PROCESS.on("SIGINT", handleInterrupt);
50
- return () => {
51
- PROCESS.off("SIGINT", handleInterrupt);
52
- };
98
+ var DEFAULT_INTERRUPT_SIGNALS = ["SIGINT", "SIGTERM"];
99
+ function createNodeProcessHost(options) {
100
+ const signals = options?.signals ?? DEFAULT_INTERRUPT_SIGNALS;
101
+ const environmentOverride = options?.environment;
102
+ const environmentDefaults = options?.environmentDefaults;
103
+ function resolveEnvironment() {
104
+ const primary = environmentOverride ?? PROCESS2.env;
105
+ if (environmentDefaults === void 0) return primary;
106
+ return mergeEnvironments(primary, environmentDefaults);
53
107
  }
54
- };
108
+ return {
109
+ get argv() {
110
+ return PROCESS2.argv;
111
+ },
112
+ get environment() {
113
+ return resolveEnvironment();
114
+ },
115
+ exit(code) {
116
+ PROCESS2.exit(code);
117
+ },
118
+ get io() {
119
+ return nodeProcessIO;
120
+ },
121
+ get isDevelopment() {
122
+ return resolveEnvironment().NODE_ENV === "development";
123
+ },
124
+ onInterrupt(listener) {
125
+ const handleInterrupt = () => {
126
+ void listener();
127
+ };
128
+ for (const signal of signals) PROCESS2.on(signal, handleInterrupt);
129
+ return () => {
130
+ for (const signal of signals) PROCESS2.off(signal, handleInterrupt);
131
+ };
132
+ }
133
+ };
134
+ }
135
+ function createNodeProcessHostWithDotEnv(options = {}) {
136
+ const {
137
+ cwd,
138
+ environment,
139
+ path: path2,
140
+ signals
141
+ } = options;
142
+ return createNodeProcessHost({
143
+ environment,
144
+ environmentDefaults: loadDotEnvFile({ cwd, path: path2 }),
145
+ signals
146
+ });
147
+ }
148
+ var nodeProcessHost = createNodeProcessHost();
55
149
  export {
56
- nodeProcessHost
150
+ DEFAULT_INTERRUPT_SIGNALS,
151
+ createNodeProcessHost,
152
+ createNodeProcessHostWithDotEnv,
153
+ loadDotEnvFile,
154
+ mergeEnvironments,
155
+ nodeProcessHost,
156
+ parseDotEnv
57
157
  };
58
158
  //# sourceMappingURL=index.mjs.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../src/nodeProcessHost.ts"],
4
- "sourcesContent": ["import PROCESS from 'node:process'\nimport { createInterface } from 'node:readline/promises'\n\nimport type { ProcessHost, ProcessIO } from '@ariestools/cli-kit'\n\nconst nodeProcessIO: ProcessIO = {\n get columns(): number | undefined {\n return PROCESS.stdout.columns\n },\n get isInteractive(): boolean {\n return PROCESS.stdin.isTTY && PROCESS.stdout.isTTY\n },\n error(...values: readonly unknown[]): void {\n console.error(...values)\n },\n log(...values: readonly unknown[]): void {\n console.log(...values)\n },\n async question(prompt: string): Promise<string> {\n const readline = createInterface({ input: PROCESS.stdin, output: PROCESS.stdout })\n try {\n return await readline.question(prompt)\n } finally {\n readline.close()\n }\n },\n warn(...values: readonly unknown[]): void {\n console.warn(...values)\n },\n}\n\n/** Node.js adapter for the reusable CLI process boundary. */\nexport const nodeProcessHost: ProcessHost = {\n get argv(): readonly string[] {\n return PROCESS.argv\n },\n get environment(): Readonly<Record<string, string | undefined>> {\n return PROCESS.env\n },\n exit(code: number): void {\n PROCESS.exit(code)\n },\n get io(): ProcessIO {\n return nodeProcessIO\n },\n get isDevelopment(): boolean {\n return PROCESS.env.NODE_ENV === 'development'\n },\n onInterrupt(listener: () => Promise<void> | void): () => void {\n const handleInterrupt = (): void => {\n void listener()\n }\n PROCESS.on('SIGINT', handleInterrupt)\n return () => {\n PROCESS.off('SIGINT', handleInterrupt)\n }\n },\n}\n"],
5
- "mappings": ";AAAA,OAAO,aAAa;AACpB,SAAS,uBAAuB;AAIhC,IAAM,gBAA2B;AAAA,EAC/B,IAAI,UAA8B;AAChC,WAAO,QAAQ,OAAO;AAAA,EACxB;AAAA,EACA,IAAI,gBAAyB;AAC3B,WAAO,QAAQ,MAAM,SAAS,QAAQ,OAAO;AAAA,EAC/C;AAAA,EACA,SAAS,QAAkC;AACzC,YAAQ,MAAM,GAAG,MAAM;AAAA,EACzB;AAAA,EACA,OAAO,QAAkC;AACvC,YAAQ,IAAI,GAAG,MAAM;AAAA,EACvB;AAAA,EACA,MAAM,SAAS,QAAiC;AAC9C,UAAM,WAAW,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AACjF,QAAI;AACF,aAAO,MAAM,SAAS,SAAS,MAAM;AAAA,IACvC,UAAE;AACA,eAAS,MAAM;AAAA,IACjB;AAAA,EACF;AAAA,EACA,QAAQ,QAAkC;AACxC,YAAQ,KAAK,GAAG,MAAM;AAAA,EACxB;AACF;AAGO,IAAM,kBAA+B;AAAA,EAC1C,IAAI,OAA0B;AAC5B,WAAO,QAAQ;AAAA,EACjB;AAAA,EACA,IAAI,cAA4D;AAC9D,WAAO,QAAQ;AAAA,EACjB;AAAA,EACA,KAAK,MAAoB;AACvB,YAAQ,KAAK,IAAI;AAAA,EACnB;AAAA,EACA,IAAI,KAAgB;AAClB,WAAO;AAAA,EACT;AAAA,EACA,IAAI,gBAAyB;AAC3B,WAAO,QAAQ,IAAI,aAAa;AAAA,EAClC;AAAA,EACA,YAAY,UAAkD;AAC5D,UAAM,kBAAkB,MAAY;AAClC,WAAK,SAAS;AAAA,IAChB;AACA,YAAQ,GAAG,UAAU,eAAe;AACpC,WAAO,MAAM;AACX,cAAQ,IAAI,UAAU,eAAe;AAAA,IACvC;AAAA,EACF;AACF;",
6
- "names": []
3
+ "sources": ["../../src/loadDotEnvFile.ts", "../../src/parseDotEnv.ts", "../../src/mergeEnvironments.ts", "../../src/nodeProcessHost.ts"],
4
+ "sourcesContent": ["import fs from 'node:fs'\nimport path from 'node:path'\nimport PROCESS from 'node:process'\n\nimport { parseDotEnv } from './parseDotEnv.ts'\n\nexport interface LoadDotEnvFileOptions {\n /**\n * Directory used to resolve a relative {@link path}. Defaults to\n * `process.cwd()`.\n */\n readonly cwd?: string\n /**\n * Absolute or cwd-relative path to the dotenv file. Defaults to `.env`.\n */\n readonly path?: string\n}\n\nfunction isErrnoException(error: unknown): error is NodeJS.ErrnoException {\n return typeof error === 'object' && error !== null && 'code' in error\n}\n\n/**\n * Reads and parses a dotenv file without mutating `process.env`.\n *\n * Missing files resolve to an empty object. Other filesystem errors propagate.\n */\nexport function loadDotEnvFile(options: LoadDotEnvFileOptions = {}): Record<string, string> {\n const cwd = options.cwd ?? PROCESS.cwd()\n const relativePath = options.path ?? '.env'\n const absolutePath = path.isAbsolute(relativePath)\n ? relativePath\n : path.resolve(cwd, relativePath)\n\n try {\n return parseDotEnv(fs.readFileSync(absolutePath, 'utf8'))\n } catch (error) {\n if (isErrnoException(error) && error.code === 'ENOENT') return {}\n throw error\n }\n}\n", "/**\n * Parses dotenv-format content into a plain key/value map.\n *\n * Supports the common dotenv grammar used by local CLI configuration:\n * - optional `export` prefix\n * - single-quoted, double-quoted, and unquoted values\n * - `#` comments (including trailing comments on unquoted values)\n * - multi-line double-quoted values with `\\\\n` escape sequences\n *\n * Does not expand variable references (`$VAR` / `${VAR}`). Does not mutate\n * `process.env`.\n */\nexport function parseDotEnv(content: string): Record<string, string> {\n const result: Record<string, string> = {}\n const text = content.codePointAt(0) === 0xFE_FF ? content.slice(1) : content\n const linePattern = /(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?\\s*(?:#.*)?(?:$|$)/mg\n\n let match: RegExpExecArray | null\n while ((match = linePattern.exec(text)) !== null) {\n const key = match[1]\n if (key === undefined) continue\n let value = (match[2] ?? '').trim()\n\n const isDoubleQuoted = value.startsWith('\"') && value.endsWith('\"')\n const isSingleQuoted = value.startsWith(\"'\") && value.endsWith(\"'\")\n const isBacktickQuoted = value.startsWith('`') && value.endsWith('`')\n\n if (isSingleQuoted || isDoubleQuoted || isBacktickQuoted) {\n value = value.slice(1, -1)\n if (isDoubleQuoted) {\n value = value\n .replaceAll(String.raw`\\n`, '\\n')\n .replaceAll(String.raw`\\r`, '\\r')\n .replaceAll(String.raw`\\\\\"`, '\"')\n .replaceAll(String.raw`\\\\`, '\\\\')\n }\n } else {\n value = value.replace(/\\s+#.*$/, '').trim()\n }\n\n result[key] = value\n }\n\n return result\n}\n", "/** Environment map accepted by process hosts and dotenv composition helpers. */\nexport type ProcessEnvironment = Readonly<Record<string, string | undefined>>\n\nfunction resolveEnvironmentValue(\n key: string,\n layers: readonly ProcessEnvironment[],\n): string | undefined {\n for (const layer of layers) {\n if (!Object.hasOwn(layer, key)) continue\n const value = layer[key]\n if (value !== undefined) return value\n }\n return undefined\n}\n\n/**\n * Merges environment layers with primary-first precedence.\n *\n * For each key, the first layer that owns the key with a value other than\n * `undefined` wins. Later layers only fill missing or undefined entries.\n *\n * Standard dotenv non-override composition (process wins over file):\n *\n * ```ts\n * mergeEnvironments(process.env, loadDotEnvFile())\n * ```\n */\nexport function mergeEnvironments(\n primary: ProcessEnvironment,\n ...fallbacks: readonly ProcessEnvironment[]\n): Record<string, string | undefined> {\n const layers = [primary, ...fallbacks]\n const keys = new Set<string>()\n for (const layer of layers) {\n for (const key of Object.keys(layer)) keys.add(key)\n }\n\n const result: Record<string, string | undefined> = {}\n for (const key of keys) {\n result[key] = resolveEnvironmentValue(key, layers)\n }\n return result\n}\n", "import PROCESS from 'node:process'\nimport { createInterface } from 'node:readline/promises'\n\nimport type { ProcessHost, ProcessIO } from '@ariestools/cli-kit'\n\nimport { loadDotEnvFile, type LoadDotEnvFileOptions } from './loadDotEnvFile.ts'\nimport { mergeEnvironments, type ProcessEnvironment } from './mergeEnvironments.ts'\n\nconst nodeProcessIO: ProcessIO = {\n get columns(): number | undefined {\n return PROCESS.stdout.columns\n },\n get isInteractive(): boolean {\n return PROCESS.stdin.isTTY && PROCESS.stdout.isTTY\n },\n error(...values: readonly unknown[]): void {\n console.error(...values)\n },\n log(...values: readonly unknown[]): void {\n console.log(...values)\n },\n async question(prompt: string): Promise<string> {\n const readline = createInterface({ input: PROCESS.stdin, output: PROCESS.stdout })\n try {\n return await readline.question(prompt)\n } finally {\n readline.close()\n }\n },\n warn(...values: readonly unknown[]): void {\n console.warn(...values)\n },\n}\n\n/**\n * Signal name accepted for interrupt binding.\n *\n * A self-contained subset of Node's `NodeJS.Signals` so the public\n * declaration surface stays resolvable in consumers without `@types/node`.\n * Every member is a valid `NodeJS.Signals` value. `SIGUSR1` is deliberately\n * excluded: Node.js reserves it for starting the debugger, and binding a\n * listener to it can interfere with debugger attach.\n */\nexport type NodeInterruptSignal = 'SIGBREAK' | 'SIGHUP' | 'SIGINT' | 'SIGQUIT' | 'SIGTERM' | 'SIGUSR2'\n\n/** Signals translated into the structural interrupt contract by default. */\nexport const DEFAULT_INTERRUPT_SIGNALS: readonly NodeInterruptSignal[] = ['SIGINT', 'SIGTERM']\n\nexport interface NodeProcessHostOptions {\n /**\n * Full environment override. When set, `process.env` is not read for\n * {@link ProcessHost.environment} or {@link ProcessHost.isDevelopment}\n * unless the caller passed `process.env` itself.\n */\n readonly environment?: ProcessEnvironment\n /**\n * Values merged under the active environment (`process.env` or\n * {@link environment}). The active environment wins for defined keys, so\n * this is the right place for dotenv file values under non-override\n * semantics. The map is fixed at host construction; the host does not\n * re-read files.\n */\n readonly environmentDefaults?: ProcessEnvironment\n /**\n * Signals bound to interrupt listeners registered through `onInterrupt`.\n * Defaults to {@link DEFAULT_INTERRUPT_SIGNALS} (`SIGINT` and `SIGTERM`).\n * An empty list is honored literally: `onInterrupt` binds nothing, the\n * listener is never invoked, and the returned disposer is a no-op.\n */\n readonly signals?: readonly NodeInterruptSignal[]\n}\n\nexport interface NodeProcessHostWithDotEnvOptions extends\n NodeProcessHostOptions, LoadDotEnvFileOptions {}\n\n/**\n * Creates a Node.js adapter for the reusable CLI process boundary.\n *\n * Use this factory when interrupt signals must be narrowed or extended, when\n * a custom environment must be supplied, or when dotenv defaults should be\n * merged under the live process environment. Otherwise prefer the shared\n * {@link nodeProcessHost} instance.\n */\nexport function createNodeProcessHost(options?: NodeProcessHostOptions): ProcessHost {\n const signals = options?.signals ?? DEFAULT_INTERRUPT_SIGNALS\n const environmentOverride = options?.environment\n const environmentDefaults = options?.environmentDefaults\n\n function resolveEnvironment(): ProcessEnvironment {\n const primary = environmentOverride ?? PROCESS.env\n if (environmentDefaults === undefined) return primary\n return mergeEnvironments(primary, environmentDefaults)\n }\n\n return {\n get argv(): readonly string[] {\n return PROCESS.argv\n },\n get environment(): ProcessEnvironment {\n return resolveEnvironment()\n },\n exit(code: number): void {\n PROCESS.exit(code)\n },\n get io(): ProcessIO {\n return nodeProcessIO\n },\n get isDevelopment(): boolean {\n return resolveEnvironment().NODE_ENV === 'development'\n },\n onInterrupt(listener: () => Promise<void> | void): () => void {\n const handleInterrupt = (): void => {\n void listener()\n }\n for (const signal of signals) PROCESS.on(signal, handleInterrupt)\n return () => {\n for (const signal of signals) PROCESS.off(signal, handleInterrupt)\n }\n },\n }\n}\n\n/**\n * Creates a Node process host that loads a dotenv file as\n * {@link NodeProcessHostOptions.environmentDefaults}.\n *\n * The file is read once at construction. Real process environment values (or\n * an explicit {@link NodeProcessHostOptions.environment} override) win over\n * file values for shared keys. `process.env` is never mutated.\n *\n * ```ts\n * const host = createNodeProcessHostWithDotEnv()\n * // equivalent to:\n * // createNodeProcessHost({ environmentDefaults: loadDotEnvFile() })\n * ```\n */\nexport function createNodeProcessHostWithDotEnv(\n options: NodeProcessHostWithDotEnvOptions = {},\n): ProcessHost {\n const {\n cwd, environment, path, signals,\n } = options\n return createNodeProcessHost({\n environment,\n environmentDefaults: loadDotEnvFile({ cwd, path }),\n signals,\n })\n}\n\n/** Node.js adapter for the reusable CLI process boundary. */\nexport const nodeProcessHost: ProcessHost = createNodeProcessHost()\n"],
5
+ "mappings": "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",
6
+ "names": ["PROCESS", "PROCESS", "path"]
7
7
  }
@@ -0,0 +1,18 @@
1
+ export interface LoadDotEnvFileOptions {
2
+ /**
3
+ * Directory used to resolve a relative {@link path}. Defaults to
4
+ * `process.cwd()`.
5
+ */
6
+ readonly cwd?: string;
7
+ /**
8
+ * Absolute or cwd-relative path to the dotenv file. Defaults to `.env`.
9
+ */
10
+ readonly path?: string;
11
+ }
12
+ /**
13
+ * Reads and parses a dotenv file without mutating `process.env`.
14
+ *
15
+ * Missing files resolve to an empty object. Other filesystem errors propagate.
16
+ */
17
+ export declare function loadDotEnvFile(options?: LoadDotEnvFileOptions): Record<string, string>;
18
+ //# sourceMappingURL=loadDotEnvFile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loadDotEnvFile.d.ts","sourceRoot":"","sources":["../../src/loadDotEnvFile.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAA;IACrB;;OAEG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CACvB;AAMD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAa1F"}
@@ -0,0 +1,16 @@
1
+ /** Environment map accepted by process hosts and dotenv composition helpers. */
2
+ export type ProcessEnvironment = Readonly<Record<string, string | undefined>>;
3
+ /**
4
+ * Merges environment layers with primary-first precedence.
5
+ *
6
+ * For each key, the first layer that owns the key with a value other than
7
+ * `undefined` wins. Later layers only fill missing or undefined entries.
8
+ *
9
+ * Standard dotenv non-override composition (process wins over file):
10
+ *
11
+ * ```ts
12
+ * mergeEnvironments(process.env, loadDotEnvFile())
13
+ * ```
14
+ */
15
+ export declare function mergeEnvironments(primary: ProcessEnvironment, ...fallbacks: readonly ProcessEnvironment[]): Record<string, string | undefined>;
16
+ //# sourceMappingURL=mergeEnvironments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mergeEnvironments.d.ts","sourceRoot":"","sources":["../../src/mergeEnvironments.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,MAAM,kBAAkB,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAA;AAc7E;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,kBAAkB,EAC3B,GAAG,SAAS,EAAE,SAAS,kBAAkB,EAAE,GAC1C,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAYpC"}
@@ -1,4 +1,67 @@
1
1
  import type { ProcessHost } from '@ariestools/cli-kit';
2
+ import { type LoadDotEnvFileOptions } from './loadDotEnvFile.ts';
3
+ import { type ProcessEnvironment } from './mergeEnvironments.ts';
4
+ /**
5
+ * Signal name accepted for interrupt binding.
6
+ *
7
+ * A self-contained subset of Node's `NodeJS.Signals` so the public
8
+ * declaration surface stays resolvable in consumers without `@types/node`.
9
+ * Every member is a valid `NodeJS.Signals` value. `SIGUSR1` is deliberately
10
+ * excluded: Node.js reserves it for starting the debugger, and binding a
11
+ * listener to it can interfere with debugger attach.
12
+ */
13
+ export type NodeInterruptSignal = 'SIGBREAK' | 'SIGHUP' | 'SIGINT' | 'SIGQUIT' | 'SIGTERM' | 'SIGUSR2';
14
+ /** Signals translated into the structural interrupt contract by default. */
15
+ export declare const DEFAULT_INTERRUPT_SIGNALS: readonly NodeInterruptSignal[];
16
+ export interface NodeProcessHostOptions {
17
+ /**
18
+ * Full environment override. When set, `process.env` is not read for
19
+ * {@link ProcessHost.environment} or {@link ProcessHost.isDevelopment}
20
+ * unless the caller passed `process.env` itself.
21
+ */
22
+ readonly environment?: ProcessEnvironment;
23
+ /**
24
+ * Values merged under the active environment (`process.env` or
25
+ * {@link environment}). The active environment wins for defined keys, so
26
+ * this is the right place for dotenv file values under non-override
27
+ * semantics. The map is fixed at host construction; the host does not
28
+ * re-read files.
29
+ */
30
+ readonly environmentDefaults?: ProcessEnvironment;
31
+ /**
32
+ * Signals bound to interrupt listeners registered through `onInterrupt`.
33
+ * Defaults to {@link DEFAULT_INTERRUPT_SIGNALS} (`SIGINT` and `SIGTERM`).
34
+ * An empty list is honored literally: `onInterrupt` binds nothing, the
35
+ * listener is never invoked, and the returned disposer is a no-op.
36
+ */
37
+ readonly signals?: readonly NodeInterruptSignal[];
38
+ }
39
+ export interface NodeProcessHostWithDotEnvOptions extends NodeProcessHostOptions, LoadDotEnvFileOptions {
40
+ }
41
+ /**
42
+ * Creates a Node.js adapter for the reusable CLI process boundary.
43
+ *
44
+ * Use this factory when interrupt signals must be narrowed or extended, when
45
+ * a custom environment must be supplied, or when dotenv defaults should be
46
+ * merged under the live process environment. Otherwise prefer the shared
47
+ * {@link nodeProcessHost} instance.
48
+ */
49
+ export declare function createNodeProcessHost(options?: NodeProcessHostOptions): ProcessHost;
50
+ /**
51
+ * Creates a Node process host that loads a dotenv file as
52
+ * {@link NodeProcessHostOptions.environmentDefaults}.
53
+ *
54
+ * The file is read once at construction. Real process environment values (or
55
+ * an explicit {@link NodeProcessHostOptions.environment} override) win over
56
+ * file values for shared keys. `process.env` is never mutated.
57
+ *
58
+ * ```ts
59
+ * const host = createNodeProcessHostWithDotEnv()
60
+ * // equivalent to:
61
+ * // createNodeProcessHost({ environmentDefaults: loadDotEnvFile() })
62
+ * ```
63
+ */
64
+ export declare function createNodeProcessHostWithDotEnv(options?: NodeProcessHostWithDotEnvOptions): ProcessHost;
2
65
  /** Node.js adapter for the reusable CLI process boundary. */
3
66
  export declare const nodeProcessHost: ProcessHost;
4
67
  //# sourceMappingURL=nodeProcessHost.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"nodeProcessHost.d.ts","sourceRoot":"","sources":["../../src/nodeProcessHost.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,qBAAqB,CAAA;AA4BjE,6DAA6D;AAC7D,eAAO,MAAM,eAAe,EAAE,WAyB7B,CAAA"}
1
+ {"version":3,"file":"nodeProcessHost.d.ts","sourceRoot":"","sources":["../../src/nodeProcessHost.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,qBAAqB,CAAA;AAEjE,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAChF,OAAO,EAAqB,KAAK,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AA4BnF;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAAA;AAEtG,4EAA4E;AAC5E,eAAO,MAAM,yBAAyB,EAAE,SAAS,mBAAmB,EAA0B,CAAA;AAE9F,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC;;;;;;OAMG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,kBAAkB,CAAA;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,mBAAmB,EAAE,CAAA;CAClD;AAED,MAAM,WAAW,gCAAiC,SAChD,sBAAsB,EAAE,qBAAqB;CAAG;AAElD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,CAAC,EAAE,sBAAsB,GAAG,WAAW,CAqCnF;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,GAAE,gCAAqC,GAC7C,WAAW,CASb;AAED,6DAA6D;AAC7D,eAAO,MAAM,eAAe,EAAE,WAAqC,CAAA"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Parses dotenv-format content into a plain key/value map.
3
+ *
4
+ * Supports the common dotenv grammar used by local CLI configuration:
5
+ * - optional `export` prefix
6
+ * - single-quoted, double-quoted, and unquoted values
7
+ * - `#` comments (including trailing comments on unquoted values)
8
+ * - multi-line double-quoted values with `\\n` escape sequences
9
+ *
10
+ * Does not expand variable references (`$VAR` / `${VAR}`). Does not mutate
11
+ * `process.env`.
12
+ */
13
+ export declare function parseDotEnv(content: string): Record<string, string>;
14
+ //# sourceMappingURL=parseDotEnv.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseDotEnv.d.ts","sourceRoot":"","sources":["../../src/parseDotEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgCnE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ariestools/cli-kit-node",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Node.js process adapter for reusable actor command-line applications",
5
5
  "keywords": [
6
6
  "ariestools",
@@ -44,17 +44,17 @@
44
44
  "LICENSE"
45
45
  ],
46
46
  "dependencies": {
47
- "@ariestools/cli-kit": "~1.0.0"
47
+ "@ariestools/cli-kit": "~1.0.1"
48
48
  },
49
49
  "devDependencies": {
50
50
  "@ariestools/toolchain": "~8.7.21",
51
51
  "@ariestools/tsconfig": "~8.7.21",
52
52
  "@types/node": "~26.1.1",
53
53
  "eslint": "~10.7.0",
54
+ "eslint-import-resolver-typescript": "~4.4.5",
54
55
  "typescript": "~6.0.3",
55
56
  "vite": "~8.1.5",
56
- "vitest": "~4.1.10",
57
- "eslint-import-resolver-typescript": "~4.4.5"
57
+ "vitest": "~4.1.10"
58
58
  },
59
59
  "engines": {
60
60
  "node": ">=24"