kodevu 0.1.54 → 0.1.56

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
@@ -1,5 +1,7 @@
1
1
  # Kodevu
2
2
 
3
+ > The name **Kodevu** is a phonetic play on "code review".
4
+
3
5
  A Node.js tool that fetches Git commits or SVN revisions, sends the diff to a supported AI reviewer CLI, and writes review results to report files.
4
6
 
5
7
  ## Pure & Zero Config
@@ -19,6 +21,7 @@ npx kodevu .
19
21
  ```
20
22
 
21
23
  Review reports are saved to `~/.kodevu/` by default.
24
+ Console output is intentionally concise by default; detailed execution logs are written to `~/.kodevu/logs/`.
22
25
 
23
26
  ## Usage
24
27
 
@@ -42,7 +45,7 @@ npx kodevu [target] [options]
42
45
  - `--openai-model`: Model used when `--reviewer openai` (default: `gpt-5-mini`).
43
46
  - `--openai-org`: Optional OpenAI organization ID.
44
47
  - `--openai-project`: Optional OpenAI project ID.
45
- - `--debug, -d`: Print debug information.
48
+ - `--debug, -d`: Show extra debug information on the console.
46
49
  - `--version, -V`: Print the current version and exit.
47
50
 
48
51
  > [!IMPORTANT]
@@ -127,13 +130,6 @@ npx kodevu . \
127
130
  --openai-model gpt-5-mini
128
131
  ```
129
132
 
130
- ## How it Works
131
-
132
- - **Git Targets**: `target` must be a local repository or subdirectory.
133
- - **SVN Targets**: `target` can be a working copy path or repository URL.
134
- - **Reviewer "auto"**: Probes `codex`, `gemini`, and `copilot` in your `PATH` and selects one.
135
- - **Reviewer "openai"**: Calls the OpenAI Chat Completions API directly. `auto` does not select `openai`, so API-based use stays explicit.
136
- - **Contextual Review**: For local repositories, the reviewer can inspect related files beyond the diff to provide deeper insights.
137
133
 
138
134
  ## License
139
135
 
package/SKILL.md CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  name: kodevu
3
- description: A tool to fetch Git commits or SVN revisions, send the diff to a supported AI reviewer CLI, and write configurable review reports.
3
+ description: A tool to fetch Git/SVN diffs, send them to an AI reviewer, and generate configurable code review reports.
4
4
  ---
5
5
 
6
6
  # Kodevu Skill
7
7
 
8
- Kodevu is a Node.js tool that fetches Git commits or SVN revisions, sends the diff to a supported AI reviewer CLI, and writes review results to report files.
8
+ Kodevu is a Node.js tool that fetches Git commits or SVN revisions, sends the diff to a supported AI reviewer CLI, and writes review results to report files. It is designed to be **stateless** and requires **no configuration files**.
9
9
 
10
10
  ## Usage
11
11
 
12
- Use `npx kodevu` to review a codebase. It is designed to be stateless and requires no configuration files.
12
+ Use `npx kodevu` to review a codebase.
13
13
 
14
14
  ### Reviewing the latest commit
15
15
 
@@ -29,13 +29,19 @@ npx kodevu . --rev <commit-hash>
29
29
  npx kodevu . --last 3
30
30
  ```
31
31
 
32
+ ### Reviewing uncommitted changes
33
+
34
+ ```bash
35
+ npx kodevu . --uncommitted
36
+ ```
37
+
32
38
  ### Supported Reviewers
33
39
 
34
- `kodevu` supports several AI reviewer CLIs: `auto`, `openai`, `gemini`, `codex`, `copilot`. The default is `auto`. Use the `--reviewer` option to override.
40
+ `kodevu` supports several AI reviewer backends: `auto`, `openai`, `gemini`, `codex`, `copilot`. The default is `auto`, which probes available CLI tools in your `PATH`.
35
41
 
36
42
  Example using OpenAI:
37
43
  ```bash
38
- npx kodevu . --reviewer openai --openai-api-key <YOUR_API_KEY> --openai-model gpt-4o
44
+ npx kodevu . --reviewer openai --openai-api-key <YOUR_API_KEY> --openai-model gpt-5-mini
39
45
  ```
40
46
 
41
47
  ### Generating JSON Reports
@@ -45,19 +51,31 @@ By default, review reports are generated as Markdown files in `~/.kodevu/`. You
45
51
  npx kodevu . --format json --output ./reports
46
52
  ```
47
53
 
48
- ### Formatting the Prompt
54
+ ### Custom Prompts
49
55
 
50
- You can provide clear instructions to the reviewer using `--prompt`:
56
+ You can provide additional instructions to the reviewer using `--prompt`:
51
57
  ```bash
52
58
  npx kodevu . --prompt "Focus on security issues and suggest optimizations."
53
59
  ```
54
60
  Or from a file: `--prompt @my-rules.txt`
55
61
 
62
+ ### Environment Variables
63
+
64
+ All options can also be set via environment variables to avoid repetitive flags:
65
+
66
+ - `KODEVU_REVIEWER` – Default reviewer.
67
+ - `KODEVU_LANG` – Default output language.
68
+ - `KODEVU_OUTPUT_DIR` – Default output directory.
69
+ - `KODEVU_PROMPT` – Default prompt instructions.
70
+ - `KODEVU_OPENAI_API_KEY` – API key for `openai`.
71
+ - `KODEVU_OPENAI_BASE_URL` – Base URL for `openai`.
72
+ - `KODEVU_OPENAI_MODEL` – Model for `openai`.
73
+
56
74
  ## Working with Target Repositories
57
75
 
58
- - `target`: Repository path (Git) or SVN URL/Working copy (default: `.`).
76
+ - **Git**: `target` must be a local repository or subdirectory.
77
+ - **SVN**: `target` can be a working copy path or repository URL.
59
78
 
60
- For example, to run on a specific path, you can use:
61
79
  ```bash
62
80
  npx kodevu /path/to/project --last 1
63
81
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodevu",
3
- "version": "0.1.54",
3
+ "version": "0.1.56",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Poll SVN revisions or Git commits, send each change diff to a reviewer CLI, and write configurable review reports.",
package/src/config.js CHANGED
@@ -373,7 +373,7 @@ Options:
373
373
  --openai-model Model used when reviewer=openai (default: gpt-5-mini)
374
374
  --openai-org Optional OpenAI organization ID
375
375
  --openai-project Optional OpenAI project ID
376
- --debug, -d Print extra debug information
376
+ --debug, -d Show extra debug information on the console
377
377
  --help, -h Show help
378
378
  --version, -V Show version
379
379
 
package/src/index.js CHANGED
@@ -30,33 +30,57 @@ try {
30
30
 
31
31
  if (config.reviewerWasAutoSelected) {
32
32
  logger.info(
33
- `Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`
34
- );
35
- }
36
-
37
- if (config.debug) {
38
- logger.debug(
39
- `Resolved config: ${JSON.stringify({
33
+ `Reviewer "auto" selected ${config.reviewer}${config.reviewerCommandPath ? ` (${config.reviewerCommandPath})` : ""}.`,
34
+ {
35
+ scope: "session",
40
36
  reviewer: config.reviewer,
41
- reviewerCommandPath: config.reviewerCommandPath,
42
- reviewerWasAutoSelected: config.reviewerWasAutoSelected,
43
- openaiBaseUrl: config.openaiBaseUrl,
44
- openaiModel: config.openaiModel,
45
- openaiOrganization: config.openaiOrganization,
46
- openaiProject: config.openaiProject,
47
- target: config.target,
48
- outputDir: config.outputDir,
49
- lang: config.lang,
50
- debug: config.debug
51
- })}`
37
+ reviewerCommandPath: config.reviewerCommandPath || "",
38
+ console: true
39
+ }
52
40
  );
53
41
  }
54
42
 
55
- logger.info(`Session started. Target: ${config.target}`);
43
+ logger.debug("Resolved config", {
44
+ scope: "session",
45
+ reviewer: config.reviewer,
46
+ reviewerCommandPath: config.reviewerCommandPath || "",
47
+ reviewerWasAutoSelected: config.reviewerWasAutoSelected || false,
48
+ openaiBaseUrl: config.openaiBaseUrl,
49
+ openaiModel: config.openaiModel,
50
+ openaiOrganization: config.openaiOrganization || "",
51
+ openaiProject: config.openaiProject || "",
52
+ target: config.target,
53
+ outputDir: config.outputDir,
54
+ lang: config.lang,
55
+ resolvedLang: config.resolvedLang,
56
+ debug: config.debug,
57
+ outputFormats: config.outputFormats,
58
+ mode: config.uncommitted ? "uncommitted" : config.rev ? "rev" : "last",
59
+ rev: config.rev || "",
60
+ last: config.last || 0,
61
+ console: "debug"
62
+ });
63
+
64
+ logger.info("Session started", {
65
+ scope: "session",
66
+ target: config.target,
67
+ reviewer: config.reviewer,
68
+ outputDir: config.outputDir,
69
+ mode: config.uncommitted ? "uncommitted" : config.rev ? "rev" : "last",
70
+ rev: config.rev || "",
71
+ last: config.last || 0
72
+ });
56
73
  await runReviewCycle(config);
57
- logger.info("Session completed successfully.");
74
+ logger.info("Session completed successfully", {
75
+ scope: "session",
76
+ target: config.target,
77
+ reviewer: config.reviewer
78
+ });
58
79
  } catch (error) {
59
- logger.error("Session failed with error", error);
80
+ logger.error("Session failed", error, {
81
+ scope: "session",
82
+ console: true
83
+ });
60
84
  process.exitCode = 1;
61
85
  }
62
86
  process.exit(process.exitCode || 0);
package/src/logger.js CHANGED
@@ -2,16 +2,87 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { formatDate } from "./utils.js";
4
4
 
5
+ function formatValue(value) {
6
+ if (value == null) {
7
+ return "";
8
+ }
9
+
10
+ if (typeof value === "string") {
11
+ return value.trim();
12
+ }
13
+
14
+ if (typeof value === "number" || typeof value === "boolean") {
15
+ return String(value);
16
+ }
17
+
18
+ try {
19
+ return JSON.stringify(value);
20
+ } catch {
21
+ return String(value);
22
+ }
23
+ }
24
+
25
+ function formatMeta(meta = {}) {
26
+ const parts = [];
27
+
28
+ for (const [key, rawValue] of Object.entries(meta)) {
29
+ if (rawValue == null || rawValue === "") {
30
+ continue;
31
+ }
32
+
33
+ const value = formatValue(rawValue);
34
+ if (!value) {
35
+ continue;
36
+ }
37
+
38
+ if (typeof rawValue === "string") {
39
+ parts.push(`${key}=${JSON.stringify(value)}`);
40
+ continue;
41
+ }
42
+
43
+ if (typeof rawValue === "number" || typeof rawValue === "boolean") {
44
+ parts.push(`${key}=${String(rawValue)}`);
45
+ continue;
46
+ }
47
+
48
+ parts.push(`${key}=${value}`);
49
+ }
50
+
51
+ return parts.join(" ");
52
+ }
53
+
54
+ function formatErrorDetails(error) {
55
+ if (!error) {
56
+ return "";
57
+ }
58
+
59
+ if (error instanceof Error) {
60
+ return error.stack || error.message || String(error);
61
+ }
62
+
63
+ if (typeof error === "string") {
64
+ return error;
65
+ }
66
+
67
+ try {
68
+ return JSON.stringify(error, null, 2);
69
+ } catch {
70
+ return String(error);
71
+ }
72
+ }
73
+
5
74
  class Logger {
6
75
  constructor() {
7
76
  this.config = null;
8
77
  this.logFile = null;
78
+ this.sessionId = null;
9
79
  this.initialized = false;
10
80
  }
11
81
 
12
82
  init(config) {
13
83
  if (this.initialized) return;
14
84
  this.config = config;
85
+ this.sessionId = this._createSessionId();
15
86
 
16
87
  if (config.logsDir) {
17
88
  try {
@@ -19,7 +90,7 @@ class Logger {
19
90
  fs.mkdirSync(config.logsDir, { recursive: true });
20
91
  }
21
92
  const date = formatDate(new Date()).split(" ")[0];
22
- this.logFile = path.join(config.logsDir, `run-${date}.log`);
93
+ this.logFile = path.join(config.logsDir, `run-${date}-${this.sessionId}.log`);
23
94
 
24
95
  // Simple rotation: Clean up logs older than 7 days
25
96
  this._cleanupOldLogs(config.logsDir);
@@ -30,33 +101,36 @@ class Logger {
30
101
  }
31
102
  }
32
103
 
33
- info(message) {
34
- this._log("INFO", message);
104
+ info(message, meta) {
105
+ this._log("INFO", message, meta);
35
106
  }
36
107
 
37
- warn(message) {
38
- this._log("WARN", message);
108
+ warn(message, meta) {
109
+ this._log("WARN", message, { ...meta, console: meta?.console ?? true });
39
110
  }
40
111
 
41
- error(message, error) {
42
- let msg = message;
43
- if (error) {
44
- msg += `\n${error.stack || error}`;
45
- }
46
- this._log("ERROR", msg);
112
+ error(message, error, meta) {
113
+ this._log("ERROR", message, {
114
+ ...meta,
115
+ console: meta?.console ?? true,
116
+ error: formatErrorDetails(error)
117
+ });
47
118
  }
48
119
 
49
- debug(message) {
50
- if (this.config?.debug) {
51
- this._log("DEBUG", message);
52
- }
120
+ debug(message, meta) {
121
+ this._log("DEBUG", message, meta);
53
122
  }
54
123
 
55
- _log(level, message) {
124
+ _log(level, message, meta = {}) {
56
125
  const timestamp = formatDate(new Date());
57
- const logLine = `[${timestamp}] [${level}] ${message}`;
126
+ const { console: consoleMode, ...details } = meta;
127
+ const fields = {
128
+ session: this.sessionId || "uninitialized",
129
+ ...details
130
+ };
131
+ const metaSuffix = formatMeta(fields);
132
+ const logLine = `[${timestamp}] [${level}] ${message}${metaSuffix ? ` | ${metaSuffix}` : ""}`;
58
133
 
59
- // Write to file
60
134
  if (this.logFile) {
61
135
  try {
62
136
  fs.appendFileSync(this.logFile, logLine + "\n");
@@ -65,21 +139,52 @@ class Logger {
65
139
  }
66
140
  }
67
141
 
68
- // Console output
69
- const isDebug = level === "DEBUG";
70
- const isError = level === "ERROR";
71
- const isWarn = level === "WARN";
72
-
73
- // If it's debug and debug mode is off, skip console
74
- if (isDebug && !this.config?.debug) return;
142
+ if (!this._shouldWriteToConsole(level, consoleMode)) {
143
+ return;
144
+ }
75
145
 
76
- if (isError || isWarn) {
146
+ if (level === "ERROR" || level === "WARN") {
77
147
  console.error(logLine);
78
148
  } else {
79
149
  console.log(logLine);
80
150
  }
81
151
  }
82
152
 
153
+ _shouldWriteToConsole(level, consoleMode) {
154
+ if (consoleMode === false) {
155
+ return false;
156
+ }
157
+
158
+ if (level === "ERROR" || level === "WARN") {
159
+ return true;
160
+ }
161
+
162
+ if (level === "DEBUG") {
163
+ if (consoleMode === true) {
164
+ return Boolean(this.config?.debug);
165
+ }
166
+ return false;
167
+ }
168
+
169
+ if (consoleMode === true) {
170
+ return true;
171
+ }
172
+
173
+ if (consoleMode === "debug") {
174
+ return Boolean(this.config?.debug);
175
+ }
176
+
177
+ return false;
178
+ }
179
+
180
+ _createSessionId() {
181
+ return [
182
+ Date.now().toString(36),
183
+ process.pid.toString(36),
184
+ Math.random().toString(36).slice(2, 8)
185
+ ].join("-");
186
+ }
187
+
83
188
  _cleanupOldLogs(logsDir) {
84
189
  try {
85
190
  const files = fs.readdirSync(logsDir);
@@ -18,7 +18,11 @@ import { runReviewerPrompt } from "./reviewers.js";
18
18
 
19
19
  async function reviewChange(config, backend, targetInfo, changeId, progress) {
20
20
  const displayId = backend.formatChangeId(changeId);
21
- logger.info(`Starting review for ${backend.changeName} ${displayId}`);
21
+ logger.info(`Starting review for ${backend.changeName} ${displayId}`, {
22
+ scope: "review",
23
+ repository: backend.kind,
24
+ changeId: displayId
25
+ });
22
26
  progress?.update(0.05, "loading change details");
23
27
  const details = await backend.getChangeDetails(config, targetInfo, changeId);
24
28
  const resolvedChangeId = details.id;
@@ -68,7 +72,13 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
68
72
 
69
73
  for (const reviewerName of reviewersToTry) {
70
74
  currentReviewerConfig = { ...config, reviewer: reviewerName };
71
- logger.debug(`Trying reviewer: ${reviewerName}`);
75
+ logger.debug(`Trying reviewer: ${reviewerName}`, {
76
+ scope: "review",
77
+ repository: backend.kind,
78
+ changeId: details.displayId,
79
+ reviewer: reviewerName,
80
+ console: "debug"
81
+ });
72
82
  progress?.update(0.45, `running reviewer ${reviewerName}`);
73
83
 
74
84
  try {
@@ -88,13 +98,24 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
88
98
  break;
89
99
  }
90
100
  } catch (err) {
91
- logger.error(`Reviewer prompt failed for ${reviewerName}: ${err.message}`);
101
+ logger.error(`Reviewer prompt failed for ${reviewerName}`, err, {
102
+ scope: "review",
103
+ repository: backend.kind,
104
+ changeId: details.displayId,
105
+ reviewer: reviewerName,
106
+ console: false
107
+ });
92
108
  // If it's the last one, it will throw below or break loop anyway
93
109
  }
94
110
 
95
111
  if (reviewerName !== reviewersToTry[reviewersToTry.length - 1]) {
96
112
  const msg = `${reviewer?.displayName || reviewerName} failed for ${details.displayId}; trying next reviewer...`;
97
- logger.warn(msg);
113
+ logger.warn(msg, {
114
+ scope: "review",
115
+ repository: backend.kind,
116
+ changeId: details.displayId,
117
+ reviewer: reviewerName
118
+ });
98
119
  }
99
120
  }
100
121
 
@@ -105,7 +126,17 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
105
126
  }
106
127
 
107
128
  progress?.update(0.82, "writing report");
108
- logger.debug(`Token usage: input=${tokenUsage.inputTokens} output=${tokenUsage.outputTokens} total=${tokenUsage.totalTokens} source=${tokenUsage.source}`);
129
+ logger.debug("Token usage recorded", {
130
+ scope: "review",
131
+ repository: backend.kind,
132
+ changeId: details.displayId,
133
+ reviewer: reviewer.displayName,
134
+ inputTokens: tokenUsage.inputTokens,
135
+ outputTokens: tokenUsage.outputTokens,
136
+ totalTokens: tokenUsage.totalTokens,
137
+ source: tokenUsage.source,
138
+ console: "debug"
139
+ });
109
140
  const report = buildReport(currentReviewerConfig, backend, targetInfo, details, diffPayloads, reviewer, reviewerResult, tokenUsage);
110
141
  const outputFile = path.join(config.outputDir, backend.getReportFileName(resolvedChangeId));
111
142
  const jsonOutputFile = outputFile.replace(/\.md$/i, ".json");
@@ -126,7 +157,13 @@ async function reviewChange(config, backend, targetInfo, changeId, progress) {
126
157
  shouldWriteFormat(config, "json") ? `json: ${jsonOutputFile}` : null
127
158
  ].filter(Boolean);
128
159
 
129
- logger.info(`Completed review for ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`);
160
+ logger.info(`Completed review for ${displayId}: ${outputLabels.join(" | ") || "(no report file generated)"}`, {
161
+ scope: "review",
162
+ repository: backend.kind,
163
+ changeId: displayId,
164
+ reviewer: reviewer.displayName,
165
+ console: true
166
+ });
130
167
 
131
168
  return {
132
169
  success: true,
@@ -149,9 +186,12 @@ export async function runReviewCycle(config) {
149
186
  await ensureDir(config.outputDir);
150
187
 
151
188
  const { backend, targetInfo } = await resolveRepositoryContext(config);
152
- logger.debug(
153
- `Resolved repository context: backend=${backend.kind} target=${targetInfo.targetDisplay || config.target}`
154
- );
189
+ logger.debug("Resolved repository context", {
190
+ scope: "session",
191
+ backend: backend.kind,
192
+ target: targetInfo.targetDisplay || config.target,
193
+ console: "debug"
194
+ });
155
195
 
156
196
  let changeIdsToReview = [];
157
197
 
@@ -167,7 +207,12 @@ export async function runReviewCycle(config) {
167
207
  }
168
208
 
169
209
  if (changeIdsToReview.length === 0) {
170
- logger.info("No changes found to review.");
210
+ logger.info("No changes found to review.", {
211
+ scope: "session",
212
+ backend: backend.kind,
213
+ target: targetInfo.targetDisplay || config.target,
214
+ console: true
215
+ });
171
216
  return;
172
217
  }
173
218
 
@@ -176,12 +221,22 @@ export async function runReviewCycle(config) {
176
221
  const batchSummary = isUncommittedBatch
177
222
  ? `Reviewing ${backend.displayName} uncommitted changes`
178
223
  : `Reviewing ${backend.displayName} ${backend.changeName}s ${formatChangeList(backend, changeIdsToReview)}`;
179
- logger.info(batchSummary);
224
+ logger.info(batchSummary, {
225
+ scope: "session",
226
+ backend: backend.kind,
227
+ count: changeIdsToReview.length,
228
+ console: true
229
+ });
180
230
  const progress = createProgressReporter(`${backend.displayName} ${backend.changeName} batch`);
181
231
  progress.update(0, `0/${changeIdsToReview.length} completed`);
182
232
 
183
233
  for (const [index, changeId] of changeIdsToReview.entries()) {
184
- logger.debug(`Starting review for ${backend.formatChangeId(changeId)}.`);
234
+ logger.debug(`Starting review for ${backend.formatChangeId(changeId)}`, {
235
+ scope: "review",
236
+ repository: backend.kind,
237
+ changeId: backend.formatChangeId(changeId),
238
+ console: "debug"
239
+ });
185
240
  const displayId = backend.formatChangeId(changeId);
186
241
  updateOverallProgress(progress, index, changeIdsToReview.length, 0, `starting ${displayId}`);
187
242
 
package/src/shell.js CHANGED
@@ -24,13 +24,18 @@ export async function runCommand(command, args = [], options = {}) {
24
24
  debug = false
25
25
  } = options;
26
26
 
27
- logger.debug(
28
- `run: ${command} ${args.join(" ")}${cwd ? ` | cwd=${cwd}` : ""}${timeoutMs > 0 ? ` | timeoutMs=${timeoutMs}` : ""}${
29
- input ? ` | input=${summarizeOutput(input)}` : ""
30
- }`
31
- );
27
+ logger.debug(`run: ${command}`, {
28
+ scope: "command",
29
+ command,
30
+ args,
31
+ cwd: cwd || "",
32
+ timeoutMs: timeoutMs || 0,
33
+ input: input ? summarizeOutput(input) : "",
34
+ console: debug ? "debug" : false
35
+ });
32
36
 
33
37
  return await new Promise((resolve, reject) => {
38
+ const startedAt = Date.now();
34
39
  const child = spawn(command, args, {
35
40
  cwd,
36
41
  env: {
@@ -54,7 +59,12 @@ export async function runCommand(command, args = [], options = {}) {
54
59
  });
55
60
 
56
61
  child.on("error", (err) => {
57
- logger.error(`spawn error: ${command}`, err);
62
+ logger.error(`spawn error: ${command}`, err, {
63
+ scope: "command",
64
+ command,
65
+ args,
66
+ cwd: cwd || ""
67
+ });
58
68
  reject(err);
59
69
  });
60
70
 
@@ -71,14 +81,26 @@ export async function runCommand(command, args = [], options = {}) {
71
81
  stdout: trim ? stdout.trim() : stdout,
72
82
  stderr: trim ? stderr.trim() : stderr
73
83
  };
74
-
75
- const level = (result.code !== 0 || result.timedOut) && !allowFailure ? "ERROR" : "DEBUG";
76
- const exitMsg = `exit: ${command} code=${result.code} timedOut=${result.timedOut} stdout=${summarizeOutput(result.stdout)} stderr=${summarizeOutput(result.stderr)}`;
84
+ const durationMs = Date.now() - startedAt;
85
+
86
+ const exitMeta = {
87
+ scope: "command",
88
+ command,
89
+ args,
90
+ cwd: cwd || "",
91
+ code: result.code,
92
+ timedOut: result.timedOut,
93
+ allowFailure,
94
+ durationMs,
95
+ stdout: summarizeOutput(result.stdout),
96
+ stderr: summarizeOutput(result.stderr),
97
+ console: debug ? "debug" : false
98
+ };
77
99
 
78
- if (level === "ERROR") {
79
- logger.error(exitMsg);
100
+ if ((result.code !== 0 || result.timedOut) && !allowFailure) {
101
+ logger.error(`exit: ${command}`, null, exitMeta);
80
102
  } else {
81
- logger.debug(exitMsg);
103
+ logger.debug(`exit: ${command}`, exitMeta);
82
104
  }
83
105
 
84
106
  if ((result.code !== 0 || result.timedOut) && !allowFailure) {
package/src/utils.js CHANGED
@@ -50,9 +50,10 @@ export function formatDate(dateInput) {
50
50
  const hours = pad(d.getHours());
51
51
  const minutes = pad(d.getMinutes());
52
52
  const seconds = pad(d.getSeconds());
53
+ const milliseconds = String(d.getMilliseconds()).padStart(3, "0");
53
54
  const offset = `${sign}${pad(offsetHours)}:${pad(offsetMins)}`;
54
55
 
55
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} ${offset}`;
56
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds} ${offset}`;
56
57
  }
57
58
  export function getTimestampPrefix() {
58
59
  const now = new Date();