@hydranium/cli 1.0.0-next.189 → 1.0.0-next.192

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 (71) hide show
  1. package/lib/commands/ast-ground-truth-args.js +5 -5
  2. package/lib/commands/ast-ground-truth-args.js.map +1 -1
  3. package/lib/commands/harness-args.d.ts +19 -35
  4. package/lib/commands/harness-args.d.ts.map +1 -1
  5. package/lib/commands/harness-args.js +38 -16
  6. package/lib/commands/harness-args.js.map +1 -1
  7. package/lib/commands/headless-harness.d.ts +14 -15
  8. package/lib/commands/headless-harness.d.ts.map +1 -1
  9. package/lib/commands/headless-harness.js +8 -8
  10. package/lib/commands/headless-harness.js.map +1 -1
  11. package/lib/commands/lint-grammar-args.js +5 -5
  12. package/lib/commands/lint-grammar-args.js.map +1 -1
  13. package/lib/commands/measure-memory-args.d.ts.map +1 -1
  14. package/lib/commands/measure-memory-args.js +11 -4
  15. package/lib/commands/measure-memory-args.js.map +1 -1
  16. package/lib/commands/model-docs-args.js +5 -5
  17. package/lib/commands/model-docs-args.js.map +1 -1
  18. package/lib/commands/projects-args.d.ts.map +1 -1
  19. package/lib/commands/projects-args.js +5 -5
  20. package/lib/commands/projects-args.js.map +1 -1
  21. package/lib/commands/projects.d.ts +3 -4
  22. package/lib/commands/projects.d.ts.map +1 -1
  23. package/lib/commands/projects.js +2 -2
  24. package/lib/commands/projects.js.map +1 -1
  25. package/lib/commands/query-args.d.ts.map +1 -1
  26. package/lib/commands/query-args.js +5 -5
  27. package/lib/commands/query-args.js.map +1 -1
  28. package/lib/commands/query.d.ts +3 -4
  29. package/lib/commands/query.d.ts.map +1 -1
  30. package/lib/commands/query.js +2 -2
  31. package/lib/commands/query.js.map +1 -1
  32. package/lib/commands/reflect-args.js +5 -5
  33. package/lib/commands/reflect-args.js.map +1 -1
  34. package/lib/commands/save-args.d.ts.map +1 -1
  35. package/lib/commands/save-args.js +6 -4
  36. package/lib/commands/save-args.js.map +1 -1
  37. package/lib/commands/save.d.ts +3 -4
  38. package/lib/commands/save.d.ts.map +1 -1
  39. package/lib/commands/save.js +2 -2
  40. package/lib/commands/save.js.map +1 -1
  41. package/lib/commands/validate-args.js +5 -5
  42. package/lib/commands/validate-args.js.map +1 -1
  43. package/lib/commands/watch-args.d.ts.map +1 -1
  44. package/lib/commands/watch-args.js +6 -4
  45. package/lib/commands/watch-args.js.map +1 -1
  46. package/lib/commands/watch.d.ts +3 -4
  47. package/lib/commands/watch.d.ts.map +1 -1
  48. package/lib/commands/watch.js +2 -2
  49. package/lib/commands/watch.js.map +1 -1
  50. package/lib/log-level.d.ts +21 -11
  51. package/lib/log-level.d.ts.map +1 -1
  52. package/lib/log-level.js +25 -14
  53. package/lib/log-level.js.map +1 -1
  54. package/package.json +9 -9
  55. package/src/commands/ast-ground-truth-args.ts +7 -7
  56. package/src/commands/harness-args.ts +42 -19
  57. package/src/commands/headless-harness.ts +15 -20
  58. package/src/commands/lint-grammar-args.ts +7 -7
  59. package/src/commands/measure-memory-args.ts +14 -6
  60. package/src/commands/model-docs-args.ts +7 -7
  61. package/src/commands/projects-args.ts +5 -4
  62. package/src/commands/projects.ts +4 -6
  63. package/src/commands/query-args.ts +5 -4
  64. package/src/commands/query.ts +4 -6
  65. package/src/commands/reflect-args.ts +7 -7
  66. package/src/commands/save-args.ts +6 -3
  67. package/src/commands/save.ts +4 -6
  68. package/src/commands/validate-args.ts +7 -7
  69. package/src/commands/watch-args.ts +6 -3
  70. package/src/commands/watch.ts +4 -6
  71. package/src/log-level.ts +42 -14
@@ -7,20 +7,30 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
  import { type LogThreshold } from '@hydranium/protocol';
10
+ /** The log flags every subcommand takes, as the env {@link logEnv} hands its child. */
11
+ export interface LogOptions {
12
+ /** Threshold for the client sink; the server's log-level setting replaces it. */
13
+ readonly logLevel?: LogThreshold;
14
+ /** File the server tees its log to; `{workspace}` expands once it is known. */
15
+ readonly logFile?: string;
16
+ /** The file's own threshold, which the log-level setting does not change. */
17
+ readonly logFileLevel?: LogThreshold;
18
+ }
10
19
  /**
11
- * Validate a raw `--log-level` argument into a {@link LogThreshold}. Throws on
12
- * an unrecognised value so the CLI's top-level `catch` surfaces a clean error
13
- * and exits non-zero (mirroring how the other option parsers fail fast). Use
14
- * for the argv-supplied value; the resolved threshold is handed to
15
- * {@link logLevelEnv} to target the spawned server.
20
+ * Validate a raw `--log-level` / `--log-file-level` argument into a
21
+ * {@link LogThreshold}. Throws on an unrecognised value so the CLI's top-level
22
+ * `catch` surfaces a clean error and exits non-zero (mirroring how the other
23
+ * option parsers fail fast). Use for the argv-supplied value; the resolved
24
+ * threshold is handed to {@link logEnv} to target the spawned server.
16
25
  */
17
- export declare function parseLogLevelOption(raw: string): LogThreshold;
26
+ export declare function parseLogLevelOption(raw: string, flag?: string): LogThreshold;
18
27
  /**
19
28
  * Build the environment override that targets *only the spawned server's* log
20
- * threshold. Passed to `spawnDataServer` as `env`, it sets the same
21
- * `HYDRANIUM_LOG_LEVEL` variable the server reads at startup — the CLI's own
22
- * logging is unaffected, unlike an ambient `HYDRANIUM_LOG_LEVEL=…` that the
23
- * child would inherit alongside the parent. The server owns *how* log level
29
+ * configuration, or `undefined` when no log flag was given. Passed to
30
+ * `spawnDataServer` as `env`, it sets the same `HYDRANIUM_LOG_LEVEL` /
31
+ * `HYDRANIUM_LOG_FILE` / `HYDRANIUM_LOG_FILE_LEVEL` variables the server reads
32
+ * at startup — the CLI's own logging is unaffected, unlike ambient variables
33
+ * that the child would inherit alongside the parent. The server owns *how* log level
24
34
  * works; this only routes the CLI's request onto the channel it already reads.
25
35
  *
26
36
  * **The variable is read by `LspLogger`'s constructor**, which is bound on the
@@ -35,5 +45,5 @@ export declare function parseLogLevelOption(raw: string): LogThreshold;
35
45
  * not go to stdout: that is the JSON-RPC channel, so a log line there would
36
46
  * corrupt the very request this flag was set to diagnose.
37
47
  */
38
- export declare function logLevelEnv(level: LogThreshold): Record<string, string>;
48
+ export declare function logEnv(options: LogOptions): Record<string, string> | undefined;
39
49
  //# sourceMappingURL=log-level.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"log-level.d.ts","sourceRoot":"","sources":["../src/log-level.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAqD,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAE3G;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAM7D;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEvE"}
1
+ {"version":3,"file":"log-level.d.ts","sourceRoot":"","sources":["../src/log-level.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAMJ,KAAK,YAAY,EACnB,MAAM,qBAAqB,CAAC;AAE7B,uFAAuF;AACvF,MAAM,WAAW,UAAU;IACxB,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,CAAC,EAAE,YAAY,CAAC;IACjC,+EAA+E;IAC/E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC;CACvC;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,SAAgB,GAAG,YAAY,CAMnF;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAY9E"}
package/lib/log-level.js CHANGED
@@ -6,27 +6,28 @@
6
6
  *
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
- import { DEFAULT_LOG_LEVEL_ENV, LEVEL_ORDER, parseLogLevel } from '@hydranium/protocol';
9
+ import { DEFAULT_LOG_FILE_ENV, DEFAULT_LOG_FILE_LEVEL_ENV, DEFAULT_LOG_LEVEL_ENV, LEVEL_ORDER, parseLogLevel } from '@hydranium/protocol';
10
10
  /**
11
- * Validate a raw `--log-level` argument into a {@link LogThreshold}. Throws on
12
- * an unrecognised value so the CLI's top-level `catch` surfaces a clean error
13
- * and exits non-zero (mirroring how the other option parsers fail fast). Use
14
- * for the argv-supplied value; the resolved threshold is handed to
15
- * {@link logLevelEnv} to target the spawned server.
11
+ * Validate a raw `--log-level` / `--log-file-level` argument into a
12
+ * {@link LogThreshold}. Throws on an unrecognised value so the CLI's top-level
13
+ * `catch` surfaces a clean error and exits non-zero (mirroring how the other
14
+ * option parsers fail fast). Use for the argv-supplied value; the resolved
15
+ * threshold is handed to {@link logEnv} to target the spawned server.
16
16
  */
17
- export function parseLogLevelOption(raw) {
17
+ export function parseLogLevelOption(raw, flag = '--log-level') {
18
18
  const level = parseLogLevel(raw);
19
19
  if (!level) {
20
- throw new Error(`Invalid --log-level: ${raw} (expected one of: ${Object.keys(LEVEL_ORDER).join(', ')})`);
20
+ throw new Error(`Invalid ${flag}: ${raw} (expected one of: ${Object.keys(LEVEL_ORDER).join(', ')})`);
21
21
  }
22
22
  return level;
23
23
  }
24
24
  /**
25
25
  * Build the environment override that targets *only the spawned server's* log
26
- * threshold. Passed to `spawnDataServer` as `env`, it sets the same
27
- * `HYDRANIUM_LOG_LEVEL` variable the server reads at startup — the CLI's own
28
- * logging is unaffected, unlike an ambient `HYDRANIUM_LOG_LEVEL=…` that the
29
- * child would inherit alongside the parent. The server owns *how* log level
26
+ * configuration, or `undefined` when no log flag was given. Passed to
27
+ * `spawnDataServer` as `env`, it sets the same `HYDRANIUM_LOG_LEVEL` /
28
+ * `HYDRANIUM_LOG_FILE` / `HYDRANIUM_LOG_FILE_LEVEL` variables the server reads
29
+ * at startup — the CLI's own logging is unaffected, unlike ambient variables
30
+ * that the child would inherit alongside the parent. The server owns *how* log level
30
31
  * works; this only routes the CLI's request onto the channel it already reads.
31
32
  *
32
33
  * **The variable is read by `LspLogger`'s constructor**, which is bound on the
@@ -41,7 +42,17 @@ export function parseLogLevelOption(raw) {
41
42
  * not go to stdout: that is the JSON-RPC channel, so a log line there would
42
43
  * corrupt the very request this flag was set to diagnose.
43
44
  */
44
- export function logLevelEnv(level) {
45
- return { [DEFAULT_LOG_LEVEL_ENV]: level };
45
+ export function logEnv(options) {
46
+ const env = {};
47
+ if (options.logLevel !== undefined) {
48
+ env[DEFAULT_LOG_LEVEL_ENV] = options.logLevel;
49
+ }
50
+ if (options.logFile !== undefined) {
51
+ env[DEFAULT_LOG_FILE_ENV] = options.logFile;
52
+ }
53
+ if (options.logFileLevel !== undefined) {
54
+ env[DEFAULT_LOG_FILE_LEVEL_ENV] = options.logFileLevel;
55
+ }
56
+ return Object.keys(env).length > 0 ? env : undefined;
46
57
  }
47
58
  //# sourceMappingURL=log-level.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"log-level.js","sourceRoot":"","sources":["../src/log-level.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EAAE,qBAAqB,EAAE,WAAW,EAAE,aAAa,EAAqB,MAAM,qBAAqB,CAAC;AAE3G;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC5C,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,sBAAsB,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5G,CAAC;IACD,OAAO,KAAK,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,WAAW,CAAC,KAAmB;IAC5C,OAAO,EAAE,CAAC,qBAAqB,CAAC,EAAE,KAAK,EAAE,CAAC;AAC7C,CAAC"}
1
+ {"version":3,"file":"log-level.js","sourceRoot":"","sources":["../src/log-level.ts"],"names":[],"mappings":"AAAA;;;;;;;kFAOkF;AAElF,OAAO,EACJ,oBAAoB,EACpB,0BAA0B,EAC1B,qBAAqB,EACrB,WAAW,EACX,aAAa,EAEf,MAAM,qBAAqB,CAAC;AAY7B;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW,EAAE,IAAI,GAAG,aAAa;IAClE,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;IACjC,IAAI,CAAC,KAAK,EAAE,CAAC;QACV,MAAM,IAAI,KAAK,CAAC,WAAW,IAAI,KAAK,GAAG,sBAAsB,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACxG,CAAC;IACD,OAAO,KAAK,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,MAAM,CAAC,OAAmB;IACvC,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,IAAI,OAAO,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;QAClC,GAAG,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IACjD,CAAC;IACD,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACjC,GAAG,CAAC,oBAAoB,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC;IAC/C,CAAC;IACD,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACtC,GAAG,CAAC,0BAA0B,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC;IAC1D,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AACxD,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydranium/cli",
3
- "version": "1.0.0-next.189",
3
+ "version": "1.0.0-next.192",
4
4
  "description": "Build-time codegen tools + data-server subcommands for the hydranium framework.",
5
5
  "keywords": [
6
6
  "cli",
@@ -69,20 +69,20 @@
69
69
  "ts-morph": "^25.0.0"
70
70
  },
71
71
  "devDependencies": {
72
- "@hydranium/core": "1.0.0-next.189",
73
- "@hydranium/data-server": "1.0.0-next.189",
74
- "@hydranium/langium": "1.0.0-next.189",
75
- "@hydranium/protocol": "1.0.0-next.189",
72
+ "@hydranium/core": "1.0.0-next.192",
73
+ "@hydranium/data-server": "1.0.0-next.192",
74
+ "@hydranium/langium": "1.0.0-next.192",
75
+ "@hydranium/protocol": "1.0.0-next.192",
76
76
  "rimraf": "^5.0.0",
77
77
  "typescript": "^7.0.2",
78
78
  "vscode-jsonrpc": "9.0.1",
79
79
  "vscode-languageserver": "~10.0.1"
80
80
  },
81
81
  "peerDependencies": {
82
- "@hydranium/core": "1.0.0-next.189",
83
- "@hydranium/data-server": "1.0.0-next.189",
84
- "@hydranium/langium": "1.0.0-next.189",
85
- "@hydranium/protocol": "1.0.0-next.189",
82
+ "@hydranium/core": "1.0.0-next.192",
83
+ "@hydranium/data-server": "1.0.0-next.192",
84
+ "@hydranium/langium": "1.0.0-next.192",
85
+ "@hydranium/protocol": "1.0.0-next.192",
86
86
  "@memlab/core": "^2.0.3",
87
87
  "@memlab/heap-analysis": "^2.0.3",
88
88
  "vscode-jsonrpc": "9.0.1"
@@ -11,9 +11,9 @@ import { runAstGroundTruth, type AstGroundTruthCommandOptions } from './ast-grou
11
11
  import {
12
12
  exitWithUsage,
13
13
  helpRequested,
14
- LOG_LEVEL_FLAG,
15
- logLevelHelpLine,
16
- logLevelOption,
14
+ LOG_VALUE_FLAGS,
15
+ logHelpLines,
16
+ logOptions,
17
17
  parseHarnessArgs,
18
18
  printHelp,
19
19
  SERVICES_FLAG,
@@ -21,7 +21,7 @@ import {
21
21
  } from './harness-args.js';
22
22
  import { OUT_FILE_FLAG } from './headless-harness.js';
23
23
 
24
- const VALUE_FLAGS = [OUT_FILE_FLAG, LOG_LEVEL_FLAG] as const;
24
+ const VALUE_FLAGS = [OUT_FILE_FLAG, ...LOG_VALUE_FLAGS] as const;
25
25
 
26
26
  /**
27
27
  * Every flag `ast-ground-truth` accepts, derived from the sets the parser is
@@ -35,7 +35,7 @@ export const AST_GROUND_TRUTH_VALUE_FLAGS: readonly string[] = AST_GROUND_TRUTH_
35
35
 
36
36
  /** The `--help` text, as data, held to {@link AST_GROUND_TRUTH_FLAGS} by a test. */
37
37
  export const AST_GROUND_TRUTH_HELP: readonly string[] = [
38
- 'Usage: hydranium-cli ast-ground-truth --services <module> <workspace> [--out-file <file>] [--log-level <lvl>]',
38
+ 'Usage: hydranium-cli ast-ground-truth --services <module> <workspace> [--out-file <file>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
39
39
  '',
40
40
  "Tally a workspace's live-model AST nodes by `$type` — the ground truth that",
41
41
  '`analyze-heap --validate <gt.json>` checks a heap snapshot against. `<module>`',
@@ -46,12 +46,12 @@ export const AST_GROUND_TRUTH_HELP: readonly string[] = [
46
46
  ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
47
47
  ' <workspace> Workspace root (path or file URI) to build (required).',
48
48
  ' --out-file <file> Write the JSON to this file instead of stdout.',
49
- logLevelHelpLine(22)
49
+ ...logHelpLines(22)
50
50
  ];
51
51
 
52
52
  export function parseAstGroundTruthArgs(args: string[], onError: UsageError = exitWithUsage): AstGroundTruthCommandOptions {
53
53
  const { servicesModule, workspace, options } = parseHarnessArgs(args, 'ast-ground-truth', VALUE_FLAGS, [], { onError });
54
- return { servicesModule, workspace, outFile: options[OUT_FILE_FLAG], logLevel: logLevelOption(options[LOG_LEVEL_FLAG]) };
54
+ return { servicesModule, workspace, outFile: options[OUT_FILE_FLAG], ...logOptions(options) };
55
55
  }
56
56
 
57
57
  export function runAstGroundTruthCommand(args: string[]): Promise<void> {
@@ -35,7 +35,7 @@ import type { LogThreshold } from '@hydranium/protocol';
35
35
  import { statSync } from 'node:fs';
36
36
  import * as path from 'node:path';
37
37
  import { fileURLToPath } from 'node:url';
38
- import { parseLogLevelOption } from '../log-level.js';
38
+ import { type LogOptions, parseLogLevelOption } from '../log-level.js';
39
39
 
40
40
  /**
41
41
  * How a parser reports a usage problem. It never returns, so a parser can treat
@@ -68,33 +68,57 @@ export const SERVICES_FLAG = '--services';
68
68
  * on which command a caller happened to reach for.
69
69
  */
70
70
  export const LOG_LEVEL_FLAG = '--log-level';
71
+ export const LOG_FILE_FLAG = '--log-file';
72
+ export const LOG_FILE_LEVEL_FLAG = '--log-file-level';
73
+
74
+ /** The log flags every subcommand takes, read by {@link logOptions}. */
75
+ export const LOG_VALUE_FLAGS = [LOG_LEVEL_FLAG, LOG_FILE_FLAG, LOG_FILE_LEVEL_FLAG] as const;
71
76
 
72
77
  /** The value flags {@link parseServerSpawnOptions} consumes itself. */
73
- export const SERVER_SPAWN_VALUE_FLAGS: readonly string[] = ['--server', '--cwd', LOG_LEVEL_FLAG];
78
+ export const SERVER_SPAWN_VALUE_FLAGS: readonly string[] = ['--server', '--cwd', ...LOG_VALUE_FLAGS];
74
79
 
75
80
  /**
76
- * The `--help` line every subcommand describes {@link LOG_LEVEL_FLAG} with.
81
+ * The `--help` lines every subcommand describes {@link LOG_VALUE_FLAGS} with.
77
82
  *
78
83
  * Shared text, per-command padding: `command-args.test.ts` compares the flags a
79
84
  * help block DESCRIBES against the flags the parser accepts, and it recognises a
80
- * description by the two-space indent — so the line has to be a real option entry
85
+ * description by the two-space indent — so each line has to be a real option entry
81
86
  * in each block, and the column each block aligns its descriptions at is the
82
- * block's own.
87
+ * block's own. A flag too wide for that column takes its description on the
88
+ * next line, at the column.
83
89
  */
84
- export function logLevelHelpLine(padTo: number): string {
85
- return ` ${`${LOG_LEVEL_FLAG} <lvl>`.padEnd(padTo)}Log threshold for the head this boots (off|error|warn|info|debug|trace).`;
90
+ export function logHelpLines(padTo: number, head = 'the head this boots'): string[] {
91
+ const line = (flag: string, text: string): string[] =>
92
+ flag.length < padTo - 1 ? [` ${flag.padEnd(padTo)}${text}`] : [` ${flag}`, `${' '.repeat(padTo + 2)}${text}`];
93
+ return [
94
+ line(`${LOG_LEVEL_FLAG} <lvl>`, `Log threshold for ${head} (off|error|warn|info|debug|trace).`),
95
+ line(`${LOG_FILE_FLAG} <file>`, `Also write ${head}'s log to <file>; {workspace} expands to the workspace.`),
96
+ line(`${LOG_FILE_LEVEL_FLAG} <lvl>`, `Threshold for that file, which the log-level setting does not change.`)
97
+ ].flat();
86
98
  }
87
99
 
88
100
  /**
89
- * Narrow a raw {@link LOG_LEVEL_FLAG} value that {@link parseHarnessArgs}
90
- * collected, leaving an absent flag absent.
101
+ * Narrow the raw {@link LOG_VALUE_FLAGS} values a parser collected, leaving
102
+ * absent flags absent.
103
+ *
104
+ * `--log-file` resolves against the caller's directory: the child may run
105
+ * elsewhere (`--cwd`), and a relative path would land the log there.
91
106
  *
92
107
  * Throws rather than reaching `onError`, matching {@link parseServerSpawnOptions}:
93
108
  * the level vocabulary belongs to the protocol package, which reports it, and the
94
109
  * entry point's catch turns that into the same message-and-exit.
95
110
  */
96
- export function logLevelOption(value: string | undefined): LogThreshold | undefined {
97
- return value === undefined ? undefined : parseLogLevelOption(value);
111
+ export function logOptions(options: Readonly<Record<string, string | undefined>>): LogOptions {
112
+ const level = (flag: string): LogThreshold | undefined => {
113
+ const value = options[flag];
114
+ return value === undefined ? undefined : parseLogLevelOption(value, flag);
115
+ };
116
+ const logFile = options[LOG_FILE_FLAG];
117
+ return {
118
+ logLevel: level(LOG_LEVEL_FLAG),
119
+ logFile: logFile === undefined ? undefined : path.resolve(logFile),
120
+ logFileLevel: level(LOG_FILE_LEVEL_FLAG)
121
+ };
98
122
  }
99
123
 
100
124
  /**
@@ -336,11 +360,10 @@ function kebabToCamel(name: string): string {
336
360
  }
337
361
 
338
362
  /** What {@link parseServerSpawnOptions} recovered from an argv. */
339
- export interface SpawnOptions {
363
+ export interface SpawnOptions extends LogOptions {
340
364
  serverCommand: string;
341
365
  serverArgs: string[];
342
366
  cwd?: string;
343
- logLevel?: LogThreshold;
344
367
  /** The tokens this parser did not claim, for the subcommand's own options. */
345
368
  extra: string[];
346
369
  }
@@ -363,10 +386,10 @@ function isRelativePathToken(token: string, isCommand: boolean): boolean {
363
386
 
364
387
  /**
365
388
  * Shared option parser for subcommands that spawn a data-server child. Consumes
366
- * `--server` / `--cwd` / `--log-level` and returns the surviving tokens as
389
+ * `--server` / `--cwd` / the {@link LOG_VALUE_FLAGS} and returns the surviving tokens as
367
390
  * `extra` for {@link parseFlagOptions} to finish.
368
391
  *
369
- * An unrecognised `--log-level` value throws rather than reaching `onError`: the
392
+ * An unrecognised log level value throws rather than reaching `onError`: the
370
393
  * level vocabulary belongs to the protocol package, which reports it, and the
371
394
  * entry point's catch turns that into the same message-and-exit.
372
395
  *
@@ -383,7 +406,7 @@ function isRelativePathToken(token: string, isCommand: boolean): boolean {
383
406
  export function parseServerSpawnOptions(args: string[], commandName: string, onError: UsageError = exitWithUsage): SpawnOptions {
384
407
  let serverSpec: string | undefined;
385
408
  let cwd: string | undefined;
386
- let logLevel: LogThreshold | undefined;
409
+ const logFlags: Record<string, string> = {};
387
410
  const extra: string[] = [];
388
411
  for (let index = 0; index < args.length; index += 1) {
389
412
  const flag = args[index];
@@ -399,8 +422,8 @@ export function parseServerSpawnOptions(args: string[], commandName: string, onE
399
422
  serverSpec = claim();
400
423
  } else if (flag === '--cwd') {
401
424
  cwd = claim();
402
- } else if (flag === LOG_LEVEL_FLAG) {
403
- logLevel = parseLogLevelOption(claim());
425
+ } else if ((LOG_VALUE_FLAGS as readonly string[]).includes(flag)) {
426
+ logFlags[flag] = claim();
404
427
  } else {
405
428
  extra.push(flag);
406
429
  }
@@ -425,5 +448,5 @@ export function parseServerSpawnOptions(args: string[], commandName: string, onE
425
448
  );
426
449
  }
427
450
  }
428
- return { serverCommand, serverArgs, cwd, logLevel, extra };
451
+ return { serverCommand, serverArgs, cwd, ...logOptions(logFlags), extra };
429
452
  }
@@ -8,14 +8,13 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import type * as HydraniumCoreNode from '@hydranium/core/node';
11
- import type { LogThreshold } from '@hydranium/protocol';
12
11
  import { spawn } from 'node:child_process';
13
12
  import { statSync, writeFileSync } from 'node:fs';
14
13
  import { createRequire } from 'node:module';
15
14
  import * as path from 'node:path';
16
15
  import { pathToFileURL } from 'node:url';
17
16
  import { driverHeapArgs, type HeapReading } from '../driver-heap.js';
18
- import { logLevelEnv } from '../log-level.js';
17
+ import { logEnv, type LogOptions } from '../log-level.js';
19
18
  import { SERVICES_FLAG } from './harness-args.js';
20
19
 
21
20
  /** The flag every report-producing subcommand names its destination file with. */
@@ -131,14 +130,13 @@ export function spawnNodeChild(execArgs: string[], env?: Record<string, string>)
131
130
  /** How a subcommand parent reaches its driver child; the seam `__spawnForTest` replaces. */
132
131
  export type SpawnDriverChild = (execArgs: string[], env?: Record<string, string>) => Promise<number>;
133
132
 
134
- /** What every `--services` subcommand carries for the child it spawns, on top of its own options. */
135
- export interface DriverSpawnOptions {
136
- /**
137
- * Log threshold for the head the driver boots. Absent leaves the child's
138
- * inherited environment alone, so an ambient `HYDRANIUM_LOG_LEVEL` still wins
139
- * where a caller set one.
140
- */
141
- readonly logLevel?: LogThreshold;
133
+ /**
134
+ * What every `--services` subcommand carries for the child it spawns, on top of
135
+ * its own options. The log options apply to the head the driver boots; an
136
+ * absent one leaves the child's inherited environment alone, so an ambient
137
+ * variable still wins where a caller set one.
138
+ */
139
+ export interface DriverSpawnOptions extends LogOptions {
142
140
  /** Test-only: capture the node argv and env instead of spawning the real child. */
143
141
  readonly __spawnForTest?: SpawnDriverChild;
144
142
  /**
@@ -154,12 +152,12 @@ export interface DriverSpawnOptions {
154
152
  * Parent-side: run a subcommand's driver child and propagate its exit code, so a
155
153
  * shell or CI step sees the gate.
156
154
  *
157
- * **The log threshold travels in the child's ENVIRONMENT, not its argv.** The
158
- * head's logger reads `HYDRANIUM_LOG_LEVEL` while `createServices` constructs it,
159
- * which is inside the driver's dynamic import — a flag the driver parsed would
160
- * arrive after the only moment it can be read. Setting it here rather than
161
- * exporting it also leaves the CLI's own output alone, unlike an ambient
162
- * `HYDRANIUM_LOG_LEVEL=…` the parent would carry too.
155
+ * **The log options travel in the child's ENVIRONMENT, not its argv.** The
156
+ * head's logger reads its `HYDRANIUM_LOG_*` variables while `createServices`
157
+ * constructs it, which is inside the driver's dynamic import — a flag the driver
158
+ * parsed would arrive after the only moment it can be read. Setting them here
159
+ * rather than exporting them also leaves the CLI's own output alone, unlike
160
+ * ambient variables the parent would carry too.
163
161
  *
164
162
  * A head that binds a logger of its own decides for itself whether the flag means
165
163
  * anything, which is the intended seam: the CLI is language-agnostic and cannot
@@ -173,10 +171,7 @@ export interface DriverSpawnOptions {
173
171
  */
174
172
  export async function runDriverChild(execArgs: string[], options: DriverSpawnOptions): Promise<void> {
175
173
  const spawnChild = options.__spawnForTest ?? spawnNodeChild;
176
- const code = await spawnChild(
177
- [...driverHeapArgs(options.__heapReadingForTest), ...execArgs],
178
- options.logLevel === undefined ? undefined : logLevelEnv(options.logLevel)
179
- );
174
+ const code = await spawnChild([...driverHeapArgs(options.__heapReadingForTest), ...execArgs], logEnv(options));
180
175
  if (code) {
181
176
  process.exitCode = code;
182
177
  }
@@ -10,9 +10,9 @@
10
10
  import {
11
11
  exitWithUsage,
12
12
  helpRequested,
13
- LOG_LEVEL_FLAG,
14
- logLevelHelpLine,
15
- logLevelOption,
13
+ LOG_VALUE_FLAGS,
14
+ logHelpLines,
15
+ logOptions,
16
16
  parseHarnessArgs,
17
17
  printHelp,
18
18
  SERVICES_FLAG,
@@ -20,7 +20,7 @@ import {
20
20
  } from './harness-args.js';
21
21
  import { runLintGrammar, type LintGrammarCommandOptions } from './lint-grammar.js';
22
22
 
23
- const VALUE_FLAGS = [LOG_LEVEL_FLAG] as const;
23
+ const VALUE_FLAGS = [...LOG_VALUE_FLAGS] as const;
24
24
  const BOOL_FLAGS = ['--strict', '--json'] as const;
25
25
  const REPEATABLE_VALUE_FLAGS = ['--name-property'] as const;
26
26
 
@@ -36,7 +36,7 @@ export const LINT_GRAMMAR_VALUE_FLAGS: readonly string[] = [SERVICES_FLAG, ...RE
36
36
 
37
37
  /** The `--help` text, as data, held to {@link LINT_GRAMMAR_FLAGS} by a test. */
38
38
  export const LINT_GRAMMAR_HELP: readonly string[] = [
39
- 'Usage: hydranium-cli lint-grammar --services <module> [--name-property <p>]... [--strict] [--json] [--log-level <lvl>]',
39
+ 'Usage: hydranium-cli lint-grammar --services <module> [--name-property <p>]... [--strict] [--json] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
40
40
  '',
41
41
  "Check a head's grammar against the framework's conventions — a CI gate: the",
42
42
  'process exits non-zero when a violation is found. Read-only: no workspace is',
@@ -52,7 +52,7 @@ export const LINT_GRAMMAR_HELP: readonly string[] = [
52
52
  ' (repeatable). Default: name.',
53
53
  ' --strict Also fail (non-zero exit) on warnings, not only errors.',
54
54
  ' --json Emit the raw JSON result instead of the human report.',
55
- logLevelHelpLine(23)
55
+ ...logHelpLines(23)
56
56
  ];
57
57
 
58
58
  export function parseLintGrammarArgs(args: string[], onError: UsageError = exitWithUsage): LintGrammarCommandOptions {
@@ -66,7 +66,7 @@ export function parseLintGrammarArgs(args: string[], onError: UsageError = exitW
66
66
  nameProperties: values['--name-property'],
67
67
  strict: options['--strict'] === 'true',
68
68
  json: options['--json'] === 'true',
69
- logLevel: logLevelOption(options[LOG_LEVEL_FLAG])
69
+ ...logOptions(options)
70
70
  };
71
71
  }
72
72
 
@@ -10,9 +10,10 @@
10
10
  import {
11
11
  exitWithUsage,
12
12
  helpRequested,
13
- LOG_LEVEL_FLAG,
14
- logLevelHelpLine,
15
- logLevelOption,
13
+ LOG_FILE_FLAG,
14
+ LOG_VALUE_FLAGS,
15
+ logHelpLines,
16
+ logOptions,
16
17
  numericOption,
17
18
  parseHarnessArgs,
18
19
  printHelp,
@@ -29,7 +30,7 @@ const VALUE_FLAGS = [
29
30
  '--snapshot-path',
30
31
  '--profile',
31
32
  '--session-out',
32
- LOG_LEVEL_FLAG
33
+ ...LOG_VALUE_FLAGS
33
34
  ] as const;
34
35
  const BOOL_FLAGS = ['--snapshot', '--json'] as const;
35
36
 
@@ -67,11 +68,13 @@ export const MEASURE_MEMORY_HELP: readonly string[] = [
67
68
  ' --profile <dims> Capture sampled profiles around the build (+churn) into a',
68
69
  ' profiling session. Comma-separated: cpu,alloc,gc,eld,heap.',
69
70
  ' --session-out <dir> Parent directory for the profiling session folder (with --profile).',
71
+ ' The session logs to its server.log at --log-file-level, else at',
72
+ ' --log-level, else info; --log-file is refused beside --profile.',
70
73
  ' --json Emit the measurement as one JSON document instead of the',
71
74
  ' progress lines: documentCount, buildMs, emptyHeapBytes,',
72
75
  ' afterBuildHeapBytes, and churnGrowthBytes / snapshotPath /',
73
76
  ' profilingSession where the run produced them.',
74
- logLevelHelpLine(22)
77
+ ...logHelpLines(22)
75
78
  ];
76
79
 
77
80
  /**
@@ -98,6 +101,11 @@ function validatedProfile(csv: string | undefined, onError: UsageError): string
98
101
 
99
102
  export function parseMeasureMemoryArgs(args: string[], onError: UsageError = exitWithUsage): MeasureMemoryCommandOptions {
100
103
  const { servicesModule, workspace, options } = parseHarnessArgs(args, 'measure-memory', VALUE_FLAGS, BOOL_FLAGS, { onError });
104
+ // The profiling session points the log at its own `server.log`, which would
105
+ // replace the requested file without a word.
106
+ if (options['--profile'] !== undefined && options[LOG_FILE_FLAG] !== undefined) {
107
+ onError(`${LOG_FILE_FLAG} cannot be combined with --profile, which logs to the session's server.log`);
108
+ }
101
109
  return {
102
110
  servicesModule,
103
111
  workspace,
@@ -110,7 +118,7 @@ export function parseMeasureMemoryArgs(args: string[], onError: UsageError = exi
110
118
  profile: validatedProfile(options['--profile'], onError),
111
119
  sessionOut: options['--session-out'],
112
120
  json: options['--json'] === 'true',
113
- logLevel: logLevelOption(options[LOG_LEVEL_FLAG])
121
+ ...logOptions(options)
114
122
  };
115
123
  }
116
124
 
@@ -10,9 +10,9 @@
10
10
  import {
11
11
  exitWithUsage,
12
12
  helpRequested,
13
- LOG_LEVEL_FLAG,
14
- logLevelHelpLine,
15
- logLevelOption,
13
+ LOG_VALUE_FLAGS,
14
+ logHelpLines,
15
+ logOptions,
16
16
  parseHarnessArgs,
17
17
  printHelp,
18
18
  SERVICES_FLAG,
@@ -21,7 +21,7 @@ import {
21
21
  import { OUT_FILE_FLAG } from './headless-harness.js';
22
22
  import { runModelDocs, type ModelDocsCommandOptions } from './model-docs.js';
23
23
 
24
- const VALUE_FLAGS = [OUT_FILE_FLAG, LOG_LEVEL_FLAG] as const;
24
+ const VALUE_FLAGS = [OUT_FILE_FLAG, ...LOG_VALUE_FLAGS] as const;
25
25
 
26
26
  /**
27
27
  * Every flag `model-docs` accepts, derived from the sets the parser is handed so
@@ -35,7 +35,7 @@ export const MODEL_DOCS_VALUE_FLAGS: readonly string[] = MODEL_DOCS_FLAGS;
35
35
 
36
36
  /** The `--help` text, as data, held to {@link MODEL_DOCS_FLAGS} by a test. */
37
37
  export const MODEL_DOCS_HELP: readonly string[] = [
38
- 'Usage: hydranium-cli model-docs --services <module> [--out-file <file>] [--log-level <lvl>]',
38
+ 'Usage: hydranium-cli model-docs --services <module> [--out-file <file>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
39
39
  '',
40
40
  'Generate a navigable Markdown reference of every AST node type — a type index,',
41
41
  'cross-linked super/sub types, a reverse "referenced by" index, and a per-language',
@@ -49,7 +49,7 @@ export const MODEL_DOCS_HELP: readonly string[] = [
49
49
  ' --out-file <file> Write the Markdown to this file instead of stdout. Written',
50
50
  ' only once the report exists, unlike a shell redirection,',
51
51
  ' which truncates the file before the head is even booted.',
52
- logLevelHelpLine(22)
52
+ ...logHelpLines(22)
53
53
  ];
54
54
 
55
55
  export function parseModelDocsArgs(args: string[], onError: UsageError = exitWithUsage): ModelDocsCommandOptions {
@@ -57,7 +57,7 @@ export function parseModelDocsArgs(args: string[], onError: UsageError = exitWit
57
57
  requireWorkspace: false,
58
58
  onError
59
59
  });
60
- return { servicesModule, outFile: options[OUT_FILE_FLAG], logLevel: logLevelOption(options[LOG_LEVEL_FLAG]) };
60
+ return { servicesModule, outFile: options[OUT_FILE_FLAG], ...logOptions(options) };
61
61
  }
62
62
 
63
63
  export function runModelDocsCommand(args: string[]): Promise<void> {
@@ -10,6 +10,7 @@
10
10
  import {
11
11
  exitWithUsage,
12
12
  helpRequested,
13
+ logHelpLines,
13
14
  parseFlagOptions,
14
15
  parseServerSpawnOptions,
15
16
  printHelp,
@@ -32,7 +33,7 @@ export const PROJECTS_VALUE_FLAGS: readonly string[] = PROJECTS_FLAGS;
32
33
 
33
34
  /** The `--help` text, as data, held to {@link PROJECTS_FLAGS} by a test. */
34
35
  export const PROJECTS_HELP: readonly string[] = [
35
- 'Usage: hydranium-cli projects --server "<cmd> [args...]" [--cwd <dir>] [--log-level <level>]',
36
+ 'Usage: hydranium-cli projects --server "<cmd> [args...]" [--cwd <dir>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
36
37
  '',
37
38
  'List projects exposed by the data-server subprocess. Output is newline-delimited',
38
39
  'JSON — one project envelope per line.',
@@ -40,17 +41,17 @@ export const PROJECTS_HELP: readonly string[] = [
40
41
  'Options:',
41
42
  ' --server "<cmd>" Command-line for the data-server subprocess (required).',
42
43
  ' --cwd <dir> Working directory for the spawned child. Default: cwd.',
43
- ' --log-level <lvl> Log threshold for the spawned server (off|error|warn|info|debug|trace).'
44
+ ...logHelpLines(19, 'the spawned server')
44
45
  ];
45
46
 
46
47
  export function parseProjectsArgs(args: string[], onError: UsageError = exitWithUsage): ProjectsCommandOptions {
47
- const { serverCommand, serverArgs, cwd, logLevel, extra } = parseServerSpawnOptions(args, 'projects', onError);
48
+ const { serverCommand, serverArgs, cwd, logLevel, logFile, logFileLevel, extra } = parseServerSpawnOptions(args, 'projects', onError);
48
49
  // `projects` declares no options beyond the shared ones, so nothing may
49
50
  // survive the spawn parser. Draining `extra` against an empty set is what
50
51
  // makes a typo an error here rather than a flag that silently did nothing —
51
52
  // the siblings get this from the option list they pass.
52
53
  parseFlagOptions(extra, 'projects', [], onError);
53
- return { serverCommand, serverArgs, cwd, logLevel };
54
+ return { serverCommand, serverArgs, cwd, logLevel, logFile, logFileLevel };
54
55
  }
55
56
 
56
57
  export function runProjectsCommand(args: string[]): Promise<void> {
@@ -7,9 +7,9 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import type { LogThreshold, Project, TransferElement } from '@hydranium/protocol';
10
+ import type { Project, TransferElement } from '@hydranium/protocol';
11
11
  import type { DataServerProtocol } from '@hydranium/protocol/data';
12
- import { logLevelEnv } from '../log-level.js';
12
+ import { logEnv, type LogOptions } from '../log-level.js';
13
13
  import { withDataServer } from '../spawn-data-server.js';
14
14
 
15
15
  /**
@@ -17,7 +17,7 @@ import { withDataServer } from '../spawn-data-server.js';
17
17
  * `serverArgs` mirror what an adopter would pass to spawn the data-server
18
18
  * subprocess.
19
19
  */
20
- export interface ProjectsCommandOptions {
20
+ export interface ProjectsCommandOptions extends LogOptions {
21
21
  /**
22
22
  * Command-line tokens used to spawn the data-server child. Mirrors
23
23
  * the `--server "<cmd> [args...]"` shape on the CLI surface — split
@@ -26,8 +26,6 @@ export interface ProjectsCommandOptions {
26
26
  readonly serverCommand: string;
27
27
  readonly serverArgs?: readonly string[];
28
28
  readonly cwd?: string;
29
- /** Log threshold for the spawned server, set on its `HYDRANIUM_LOG_LEVEL` env. */
30
- readonly logLevel?: LogThreshold;
31
29
  /**
32
30
  * Output sink. Default: `process.stdout.write`. Tests inject a
33
31
  * capturing stub.
@@ -60,7 +58,7 @@ export async function runProjects(options: ProjectsCommandOptions): Promise<void
60
58
  command: options.serverCommand,
61
59
  args: options.serverArgs,
62
60
  cwd: options.cwd,
63
- env: options.logLevel ? logLevelEnv(options.logLevel) : undefined
61
+ env: logEnv(options)
64
62
  },
65
63
  async server => {
66
64
  const projects = await server.getProjects();