@v1nvn/agentic-core 0.41.0 → 0.42.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.
package/dist/index.js CHANGED
@@ -2,27 +2,51 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  //#region src/cli.ts
5
- function parseQuietly(program, args, recover) {
6
- try {
7
- program.allowExcessArguments(false).exitOverride().configureOutput({
5
+ var HelpRequested = class extends Error {
6
+ command;
7
+ constructor(command) {
8
+ super(`${command.name()} --help`);
9
+ this.command = command;
10
+ }
11
+ };
12
+ function parseQuietly(program, args) {
13
+ function quiet(command) {
14
+ command.configureOutput({
8
15
  writeOut: () => void 0,
9
16
  writeErr: () => void 0
10
- }).parse([...args], { from: "user" });
17
+ });
18
+ command.exitOverride((err) => {
19
+ const code = err.code;
20
+ if (code === "commander.help" || code === "commander.helpDisplayed") throw new HelpRequested(command);
21
+ throw err;
22
+ });
23
+ command.commands.forEach(quiet);
24
+ }
25
+ try {
26
+ quiet(program);
27
+ program.parse([...args], { from: "user" });
11
28
  return program;
12
29
  } catch (err) {
13
- return recover?.(err);
30
+ return err instanceof HelpRequested ? err : void 0;
14
31
  }
15
32
  }
33
+ function exitWithCode(code) {
34
+ process.exit(code);
35
+ }
36
+ function printHelpAndExit(command) {
37
+ console.log(command.helpInformation());
38
+ exitWithCode(0);
39
+ }
16
40
  function printUsageAndExit(program) {
17
41
  console.error(program.helpInformation());
18
- process.exit(1);
42
+ exitWithCode(1);
19
43
  }
20
44
  async function runMain(main) {
21
45
  try {
22
46
  await main();
23
47
  } catch (e) {
24
48
  console.error(e.message);
25
- process.exit(1);
49
+ exitWithCode(1);
26
50
  }
27
51
  }
28
52
  //#endregion
@@ -140,9 +164,6 @@ var LEVEL_RANK = {
140
164
  error: 40,
141
165
  silent: Number.MAX_SAFE_INTEGER
142
166
  };
143
- function levelEnabled(active, wanted) {
144
- return LEVEL_RANK[wanted] >= LEVEL_RANK[active];
145
- }
146
167
  var LEVEL_LABEL = {
147
168
  debug: "DEBUG",
148
169
  error: "ERROR",
@@ -169,7 +190,7 @@ var Logger = class {
169
190
  this.log("warn", message);
170
191
  }
171
192
  log(level, message) {
172
- if (levelEnabled(this.activeLevel, level)) this.stderr(`${LEVEL_LABEL[level]} ${message}`);
193
+ if (LEVEL_RANK[level] >= LEVEL_RANK[this.activeLevel]) this.stderr(`${LEVEL_LABEL[level]} ${message}`);
173
194
  }
174
195
  };
175
196
  //#endregion
@@ -188,6 +209,6 @@ function shutdownOnSignals(closables, signals = ["SIGINT", "SIGTERM"]) {
188
209
  for (const signal of signals) process.on(signal, shutdown);
189
210
  }
190
211
  //#endregion
191
- export { Logger, claudeProjectsDir, lastReply, levelEnabled, parseQuietly, printUsageAndExit, readMarkdownFile, readStdin, runMain, shutdownOnSignals, watchWithReload };
212
+ export { HelpRequested, Logger, claudeProjectsDir, lastReply, parseQuietly, printHelpAndExit, printUsageAndExit, readMarkdownFile, readStdin, runMain, shutdownOnSignals, watchWithReload };
192
213
 
193
214
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/cli.ts","../src/dev.ts","../src/input.ts","../src/last-reply.ts","../src/logger.ts","../src/shutdown.ts"],"sourcesContent":["import type { Command } from 'commander';\n\nexport function parseQuietly<T = never>(\n program: Command,\n args: readonly string[],\n recover?: (err: unknown) => T | undefined,\n): Command | T | undefined {\n try {\n program\n .allowExcessArguments(false)\n .exitOverride()\n .configureOutput({ writeOut: () => undefined, writeErr: () => undefined })\n .parse([...args], { from: 'user' });\n return program;\n } catch (err) {\n return recover?.(err);\n }\n}\n\nexport function printUsageAndExit(program: Command): never {\n console.error(program.helpInformation());\n // CLIs report failure through the exit code; the rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(1);\n}\n\nexport async function runMain(main: () => Promise<void> | void): Promise<void> {\n try {\n await main();\n } catch (e) {\n console.error((e as Error).message);\n // CLIs report failure through the exit code; the rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(1);\n }\n}\n","// Structural stand-ins for vite's dev server and module runner — the shared\n// hot-reload wiring below must not pull vite into core's runtime surface; the\n// MCP packages pass their real instances through.\nexport interface ReloadableRunner {\n readonly evaluatedModules: { clear(): void };\n import<T>(entry: string): Promise<T>;\n}\n\nexport interface WatchableDevServer {\n close(): Promise<unknown>;\n readonly watcher: {\n on(event: 'change', cb: (file: string) => void): unknown;\n };\n}\n\nconst RELOAD_DEBOUNCE_MS = 150;\n\nconst IGNORED_SEGMENTS = ['/node_modules/', '/dist/', '/.git/', '/coverage/'];\n\nfunction shouldReload(file: string): boolean {\n return IGNORED_SEGMENTS.every(seg => !file.includes(seg));\n}\n\n/**\n * Debounced file-change reloads, serialized via a promise chain: concurrent\n * saves never overlap, and a mid-reload save still produces a follow-up.\n */\nexport function watchWithReload(\n vite: WatchableDevServer,\n reload: () => Promise<void>,\n): void {\n let chain: Promise<void> = Promise.resolve();\n let timer: NodeJS.Timeout | undefined;\n vite.watcher.on('change', file => {\n if (!shouldReload(file)) {\n return;\n }\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = undefined;\n chain = chain.then(reload);\n }, RELOAD_DEBOUNCE_MS);\n });\n}\n","import { readFileSync, statSync } from 'node:fs';\n\nfunction readAll(stream: NodeJS.ReadableStream): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n stream.setEncoding('utf8');\n stream.on('data', (chunk: string) => {\n data += chunk;\n });\n stream.on('end', () => {\n resolve(data);\n });\n stream.on('error', reject);\n });\n}\n\nexport function readStdin(\n stream: NodeJS.ReadableStream = process.stdin,\n): Promise<string> {\n return readAll(stream);\n}\n\n/** A markdown file's contents, with the CLI's no-such-file error. */\nexport function readMarkdownFile(path: string): string {\n const stats = statSync(path, { throwIfNoEntry: false });\n if (!stats?.isFile()) {\n throw new Error(`no such file: ${path}`);\n }\n return readFileSync(path, 'utf8');\n}\n","import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nfunction isFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\n\ninterface ContentBlock {\n text?: string;\n type: string;\n}\n\ninterface TranscriptEntry {\n message?: { content?: unknown };\n type?: string;\n}\n\nfunction textBlocks(entry: TranscriptEntry): ContentBlock[] | undefined {\n const content = entry.message?.content;\n if (entry.type !== 'assistant' || !Array.isArray(content)) {\n return undefined;\n }\n const blocks = content.filter(\n (b): b is ContentBlock =>\n typeof b === 'object' &&\n b !== null &&\n (b as ContentBlock).type === 'text',\n );\n return blocks.length > 0 ? blocks : undefined;\n}\n\nexport function claudeProjectsDir(): string {\n const claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), '.claude');\n return join(claudeDir, 'projects');\n}\n\n/**\n * The last assistant text reply from a Claude Code session, reproducing Claude\n * Code's `/copy` byte-for-byte: the last assistant entry that contains a\n * `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n * joined with a blank line, and no trailing newline.\n *\n * @param arg a transcript file, a session UUID under the project dir, or\n * nothing to use the newest session for the current project.\n */\nexport function lastReply(arg?: string): string {\n // Claude Code keys transcripts under the project root with \"/\" → \"-\".\n // Prefer $CLAUDE_PROJECT_DIR (exported to hook processes) over $PWD.\n const proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\n '/',\n '-',\n );\n const projDir = join(claudeProjectsDir(), proj);\n\n let file: string | undefined;\n if (arg !== undefined) {\n file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n } else if (existsSync(projDir)) {\n const newest = readdirSync(projDir)\n .filter(f => f.endsWith('.jsonl'))\n .map(f => ({ f, mtimeMs: statSync(join(projDir, f)).mtimeMs }))\n .sort((a, b) => b.mtimeMs - a.mtimeMs)\n .at(0);\n if (newest) {\n file = join(projDir, newest.f);\n }\n }\n\n if (file === undefined || !isFile(file)) {\n throw new Error(`no session transcript found in ${projDir}`);\n }\n\n let last: ContentBlock[] | undefined;\n for (const line of readFileSync(file, 'utf8').split('\\n')) {\n if (!line.includes('\"assistant\"')) {\n continue;\n }\n let entry: TranscriptEntry;\n try {\n entry = JSON.parse(line) as TranscriptEntry;\n } catch {\n continue;\n }\n const blocks = textBlocks(entry);\n if (blocks) {\n last = blocks;\n }\n }\n return (last ?? []).map(b => b.text ?? '').join('\\n\\n');\n}\n","export type LogLevel = 'debug' | 'error' | 'info' | 'silent' | 'warn';\n\nconst LEVEL_RANK: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: Number.MAX_SAFE_INTEGER,\n};\n\nexport function levelEnabled(\n active: LogLevel,\n wanted: Exclude<LogLevel, 'silent'>,\n): boolean {\n return LEVEL_RANK[wanted] >= LEVEL_RANK[active];\n}\n\nconst LEVEL_LABEL: Record<Exclude<LogLevel, 'silent'>, string> = {\n debug: 'DEBUG',\n error: 'ERROR',\n info: 'INFO',\n warn: 'WARN',\n};\n\ntype Writer = (message: string) => void;\n\n// stdout carries the MCP transport; everything here goes to stderr.\nexport class Logger {\n private readonly stderr: Writer;\n\n constructor(\n readonly activeLevel: LogLevel,\n stderr: Writer = line => process.stderr.write(`${line}\\n`),\n ) {\n this.stderr = stderr;\n }\n\n debug(message: string): void {\n this.log('debug', message);\n }\n\n error(message: string): void {\n this.log('error', message);\n }\n\n info(message: string): void {\n this.log('info', message);\n }\n\n warn(message: string): void {\n this.log('warn', message);\n }\n\n private log(level: Exclude<LogLevel, 'silent'>, message: string): void {\n if (levelEnabled(this.activeLevel, level)) {\n this.stderr(`${LEVEL_LABEL[level]} ${message}`);\n }\n }\n}\n","/** Close each closable in order (best-effort), then force-exit. */\nexport function shutdownOnSignals(\n closables: readonly { close(): Promise<unknown> }[],\n signals: readonly NodeJS.Signals[] = ['SIGINT', 'SIGTERM'],\n): void {\n function shutdown(): void {\n void closables\n .reduce(\n (acc: Promise<void>, closable) =>\n acc.then(async () => {\n try {\n await closable.close();\n } catch {\n // best-effort; exiting regardless\n }\n return;\n }),\n Promise.resolve(),\n )\n .finally(() => {\n // Force-exit on signal; the n/no-process-exit rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(0);\n });\n }\n for (const signal of signals) {\n process.on(signal, shutdown);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/cli.ts","../src/dev.ts","../src/input.ts","../src/last-reply.ts","../src/logger.ts","../src/shutdown.ts"],"sourcesContent":["import type { Command } from 'commander';\n\n// A --help ask caught mid-parse: parseQuietly returns it instead of exiting,\n// so a spec can drive the parse and the bin decides to print and exit.\nexport class HelpRequested extends Error {\n constructor(readonly command: Command) {\n super(`${command.name()} --help`);\n }\n}\n\nexport function parseQuietly(\n program: Command,\n args: readonly string[],\n): Command | HelpRequested | undefined {\n function quiet(command: Command): void {\n command.configureOutput({\n writeOut: () => undefined,\n writeErr: () => undefined,\n });\n command.exitOverride(err => {\n const code = (err as { code?: string }).code;\n if (code === 'commander.help' || code === 'commander.helpDisplayed') {\n throw new HelpRequested(command);\n }\n throw err;\n });\n command.commands.forEach(quiet);\n }\n try {\n quiet(program);\n program.parse([...args], { from: 'user' });\n return program;\n } catch (err) {\n return err instanceof HelpRequested ? err : undefined;\n }\n}\n\nfunction exitWithCode(code: number): never {\n // eslint-disable-next-line n/no-process-exit\n process.exit(code);\n}\n\nexport function printHelpAndExit(command: Command): never {\n console.log(command.helpInformation());\n exitWithCode(0);\n}\n\nexport function printUsageAndExit(program: Command): never {\n console.error(program.helpInformation());\n exitWithCode(1);\n}\n\nexport async function runMain(main: () => Promise<void> | void): Promise<void> {\n try {\n await main();\n } catch (e) {\n console.error((e as Error).message);\n exitWithCode(1);\n }\n}\n","// Structural stand-ins for vite's dev server and module runner — the shared\n// hot-reload wiring below must not pull vite into core's runtime surface; the\n// MCP packages pass their real instances through.\nexport interface ReloadableRunner {\n readonly evaluatedModules: { clear(): void };\n import<T>(entry: string): Promise<T>;\n}\n\nexport interface WatchableDevServer {\n close(): Promise<unknown>;\n readonly watcher: {\n on(event: 'change', cb: (file: string) => void): unknown;\n };\n}\n\nconst RELOAD_DEBOUNCE_MS = 150;\n\nconst IGNORED_SEGMENTS = ['/node_modules/', '/dist/', '/.git/', '/coverage/'];\n\nfunction shouldReload(file: string): boolean {\n return IGNORED_SEGMENTS.every(seg => !file.includes(seg));\n}\n\n/**\n * Debounced file-change reloads, serialized via a promise chain: concurrent\n * saves never overlap, and a mid-reload save still produces a follow-up.\n */\nexport function watchWithReload(\n vite: WatchableDevServer,\n reload: () => Promise<void>,\n): void {\n let chain: Promise<void> = Promise.resolve();\n let timer: NodeJS.Timeout | undefined;\n vite.watcher.on('change', file => {\n if (!shouldReload(file)) {\n return;\n }\n if (timer) {\n clearTimeout(timer);\n }\n timer = setTimeout(() => {\n timer = undefined;\n chain = chain.then(reload);\n }, RELOAD_DEBOUNCE_MS);\n });\n}\n","import { readFileSync, statSync } from 'node:fs';\n\nfunction readAll(stream: NodeJS.ReadableStream): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n stream.setEncoding('utf8');\n stream.on('data', (chunk: string) => {\n data += chunk;\n });\n stream.on('end', () => {\n resolve(data);\n });\n stream.on('error', reject);\n });\n}\n\nexport function readStdin(\n stream: NodeJS.ReadableStream = process.stdin,\n): Promise<string> {\n return readAll(stream);\n}\n\n/** A markdown file's contents, with the CLI's no-such-file error. */\nexport function readMarkdownFile(path: string): string {\n const stats = statSync(path, { throwIfNoEntry: false });\n if (!stats?.isFile()) {\n throw new Error(`no such file: ${path}`);\n }\n return readFileSync(path, 'utf8');\n}\n","import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\nfunction isFile(path: string): boolean {\n try {\n return statSync(path).isFile();\n } catch {\n return false;\n }\n}\n\ninterface ContentBlock {\n text?: string;\n type: string;\n}\n\ninterface TranscriptEntry {\n message?: { content?: unknown };\n type?: string;\n}\n\nfunction textBlocks(entry: TranscriptEntry): ContentBlock[] | undefined {\n const content = entry.message?.content;\n if (entry.type !== 'assistant' || !Array.isArray(content)) {\n return undefined;\n }\n const blocks = content.filter(\n (b): b is ContentBlock =>\n typeof b === 'object' &&\n b !== null &&\n (b as ContentBlock).type === 'text',\n );\n return blocks.length > 0 ? blocks : undefined;\n}\n\nexport function claudeProjectsDir(): string {\n const claudeDir = process.env.CLAUDE_DIR ?? join(homedir(), '.claude');\n return join(claudeDir, 'projects');\n}\n\n/**\n * The last assistant text reply from a Claude Code session, reproducing Claude\n * Code's `/copy` byte-for-byte: the last assistant entry that contains a\n * `text` block, only its `text` block(s) (tool_use / thinking dropped), blocks\n * joined with a blank line, and no trailing newline.\n *\n * @param arg a transcript file, a session UUID under the project dir, or\n * nothing to use the newest session for the current project.\n */\nexport function lastReply(arg?: string): string {\n // Claude Code keys transcripts under the project root with \"/\" → \"-\".\n // Prefer $CLAUDE_PROJECT_DIR (exported to hook processes) over $PWD.\n const proj = (process.env.CLAUDE_PROJECT_DIR ?? process.cwd()).replaceAll(\n '/',\n '-',\n );\n const projDir = join(claudeProjectsDir(), proj);\n\n let file: string | undefined;\n if (arg !== undefined) {\n file = isFile(arg) ? arg : join(projDir, `${arg}.jsonl`);\n } else if (existsSync(projDir)) {\n const newest = readdirSync(projDir)\n .filter(f => f.endsWith('.jsonl'))\n .map(f => ({ f, mtimeMs: statSync(join(projDir, f)).mtimeMs }))\n .sort((a, b) => b.mtimeMs - a.mtimeMs)\n .at(0);\n if (newest) {\n file = join(projDir, newest.f);\n }\n }\n\n if (file === undefined || !isFile(file)) {\n throw new Error(`no session transcript found in ${projDir}`);\n }\n\n let last: ContentBlock[] | undefined;\n for (const line of readFileSync(file, 'utf8').split('\\n')) {\n if (!line.includes('\"assistant\"')) {\n continue;\n }\n let entry: TranscriptEntry;\n try {\n entry = JSON.parse(line) as TranscriptEntry;\n } catch {\n continue;\n }\n const blocks = textBlocks(entry);\n if (blocks) {\n last = blocks;\n }\n }\n return (last ?? []).map(b => b.text ?? '').join('\\n\\n');\n}\n","export type LogLevel = 'debug' | 'error' | 'info' | 'silent' | 'warn';\n\nconst LEVEL_RANK: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n silent: Number.MAX_SAFE_INTEGER,\n};\n\nconst LEVEL_LABEL: Record<Exclude<LogLevel, 'silent'>, string> = {\n debug: 'DEBUG',\n error: 'ERROR',\n info: 'INFO',\n warn: 'WARN',\n};\n\ntype Writer = (message: string) => void;\n\n// stdout carries the MCP transport; everything here goes to stderr.\nexport class Logger {\n private readonly stderr: Writer;\n\n constructor(\n readonly activeLevel: LogLevel,\n stderr: Writer = line => process.stderr.write(`${line}\\n`),\n ) {\n this.stderr = stderr;\n }\n\n debug(message: string): void {\n this.log('debug', message);\n }\n\n error(message: string): void {\n this.log('error', message);\n }\n\n info(message: string): void {\n this.log('info', message);\n }\n\n warn(message: string): void {\n this.log('warn', message);\n }\n\n private log(level: Exclude<LogLevel, 'silent'>, message: string): void {\n if (LEVEL_RANK[level] >= LEVEL_RANK[this.activeLevel]) {\n this.stderr(`${LEVEL_LABEL[level]} ${message}`);\n }\n }\n}\n","/** Close each closable in order (best-effort), then force-exit. */\nexport function shutdownOnSignals(\n closables: readonly { close(): Promise<unknown> }[],\n signals: readonly NodeJS.Signals[] = ['SIGINT', 'SIGTERM'],\n): void {\n function shutdown(): void {\n void closables\n .reduce(\n (acc: Promise<void>, closable) =>\n acc.then(async () => {\n try {\n await closable.close();\n } catch {\n // best-effort; exiting regardless\n }\n return;\n }),\n Promise.resolve(),\n )\n .finally(() => {\n // Force-exit on signal; the n/no-process-exit rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(0);\n });\n }\n for (const signal of signals) {\n process.on(signal, shutdown);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@v1nvn/agentic-core",
3
- "version": "0.41.0",
4
- "description": "Shared runtime for the @v1nvn agent tools — last-reply transcript reader and fixed-width text formatting.",
3
+ "version": "0.42.0",
4
+ "description": "Shared runtime for the @v1nvn agent tools — last-reply transcript reader.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "exports": {
package/src/cli.ts CHANGED
@@ -1,27 +1,53 @@
1
1
  import type { Command } from 'commander';
2
2
 
3
- export function parseQuietly<T = never>(
3
+ // A --help ask caught mid-parse: parseQuietly returns it instead of exiting,
4
+ // so a spec can drive the parse and the bin decides to print and exit.
5
+ export class HelpRequested extends Error {
6
+ constructor(readonly command: Command) {
7
+ super(`${command.name()} --help`);
8
+ }
9
+ }
10
+
11
+ export function parseQuietly(
4
12
  program: Command,
5
13
  args: readonly string[],
6
- recover?: (err: unknown) => T | undefined,
7
- ): Command | T | undefined {
14
+ ): Command | HelpRequested | undefined {
15
+ function quiet(command: Command): void {
16
+ command.configureOutput({
17
+ writeOut: () => undefined,
18
+ writeErr: () => undefined,
19
+ });
20
+ command.exitOverride(err => {
21
+ const code = (err as { code?: string }).code;
22
+ if (code === 'commander.help' || code === 'commander.helpDisplayed') {
23
+ throw new HelpRequested(command);
24
+ }
25
+ throw err;
26
+ });
27
+ command.commands.forEach(quiet);
28
+ }
8
29
  try {
9
- program
10
- .allowExcessArguments(false)
11
- .exitOverride()
12
- .configureOutput({ writeOut: () => undefined, writeErr: () => undefined })
13
- .parse([...args], { from: 'user' });
30
+ quiet(program);
31
+ program.parse([...args], { from: 'user' });
14
32
  return program;
15
33
  } catch (err) {
16
- return recover?.(err);
34
+ return err instanceof HelpRequested ? err : undefined;
17
35
  }
18
36
  }
19
37
 
38
+ function exitWithCode(code: number): never {
39
+ // eslint-disable-next-line n/no-process-exit
40
+ process.exit(code);
41
+ }
42
+
43
+ export function printHelpAndExit(command: Command): never {
44
+ console.log(command.helpInformation());
45
+ exitWithCode(0);
46
+ }
47
+
20
48
  export function printUsageAndExit(program: Command): never {
21
49
  console.error(program.helpInformation());
22
- // CLIs report failure through the exit code; the rule targets libraries.
23
- // eslint-disable-next-line n/no-process-exit
24
- process.exit(1);
50
+ exitWithCode(1);
25
51
  }
26
52
 
27
53
  export async function runMain(main: () => Promise<void> | void): Promise<void> {
@@ -29,8 +55,6 @@ export async function runMain(main: () => Promise<void> | void): Promise<void> {
29
55
  await main();
30
56
  } catch (e) {
31
57
  console.error((e as Error).message);
32
- // CLIs report failure through the exit code; the rule targets libraries.
33
- // eslint-disable-next-line n/no-process-exit
34
- process.exit(1);
58
+ exitWithCode(1);
35
59
  }
36
60
  }
package/src/logger.ts CHANGED
@@ -8,13 +8,6 @@ const LEVEL_RANK: Record<LogLevel, number> = {
8
8
  silent: Number.MAX_SAFE_INTEGER,
9
9
  };
10
10
 
11
- export function levelEnabled(
12
- active: LogLevel,
13
- wanted: Exclude<LogLevel, 'silent'>,
14
- ): boolean {
15
- return LEVEL_RANK[wanted] >= LEVEL_RANK[active];
16
- }
17
-
18
11
  const LEVEL_LABEL: Record<Exclude<LogLevel, 'silent'>, string> = {
19
12
  debug: 'DEBUG',
20
13
  error: 'ERROR',
@@ -52,7 +45,7 @@ export class Logger {
52
45
  }
53
46
 
54
47
  private log(level: Exclude<LogLevel, 'silent'>, message: string): void {
55
- if (levelEnabled(this.activeLevel, level)) {
48
+ if (LEVEL_RANK[level] >= LEVEL_RANK[this.activeLevel]) {
56
49
  this.stderr(`${LEVEL_LABEL[level]} ${message}`);
57
50
  }
58
51
  }