Skip to content

Commit faefcef

Browse files
committed
feat(cli): scaffold + inspect + report + scenario commands + package config
- ocpp-debugkit inspect <file> — parse + analyze + output summary - ocpp-debugkit report <file> — generate Markdown report (stdout or --output) - ocpp-debugkit scenario list — list all 5 built-in scenarios - ocpp-debugkit scenario run <name> — run scenario through analysis engine, compare detected vs expected failures - Path safety: validated file paths, size limits - Input validation: safe parsing, non-sensitive error messages - 17 integration tests (execa-based) - Converted JSON fixtures to TS modules (fixes Node.js ESM JSON import issue) Closes #25
1 parent 805434f commit faefcef

23 files changed

Lines changed: 1860 additions & 1053 deletions

.changeset/cli-package.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@ocpp-debugkit/cli': minor
3+
---
4+
5+
Create CLI package with inspect, report, and scenario commands.
6+
7+
- `ocpp-debugkit inspect <file>` — parse + analyze + output summary
8+
- `ocpp-debugkit report <file>` — generate Markdown report (stdout or --output)
9+
- `ocpp-debugkit scenario list` — list all 5 built-in scenarios
10+
- `ocpp-debugkit scenario run <name>` — run scenario through analysis engine,
11+
compare detected vs expected failures
12+
- Path safety: validated file paths, size limits
13+
- Input validation: safe parsing, non-sensitive error messages
14+
- 17 integration tests (execa-based)
15+
- Converted JSON fixtures to TS modules (fixes Node.js ESM JSON import issue)

CURRENT_STATE.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,36 @@ report exported. CLI and web inspector.
9999
- ✅ Each scenario's `expectedFailures` aligns with v0.1 detection rules
100100
- ✅ 21 tests (registry, engine integration, synthetic data policy)
101101

102-
### Reporter Package (in progress — this PR)
102+
### Reporter Package (PR #40)
103103

104104
-`packages/reporter/` — new package
105105
-`generateMarkdownReport()` — session overview, timeline summary, failures, suggested steps, event appendix
106106
-`AnalysisResult` input type
107107
- ✅ 11 tests (structure, failure inclusion, readability, metadata, severity)
108108

109+
### CLI Package (in progress — this PR)
110+
111+
-`packages/cli/` — new package
112+
-`ocpp-debugkit inspect <file>` — parse + analyze + output
113+
-`ocpp-debugkit report <file>` — generate Markdown report (stdout or file)
114+
-`ocpp-debugkit scenario list` — list all 5 scenarios
115+
-`ocpp-debugkit scenario run <name>` — run scenario through analysis engine, compare detected vs expected
116+
- ✅ Path safety: validated file paths, size limits
117+
- ✅ Input validation: safe parsing, non-sensitive errors
118+
- ✅ 17 integration tests (execa-based)
119+
- ✅ Converted JSON fixtures to TS modules (fixes Node.js ESM JSON import issue)
120+
109121
## What's Next
110122

111123
1. **Issue #20** → complete (PR #33): data model + parser + normalizer
112124
2. **Issue #21** → complete (PR #34): timeline + detection + summarizer + validator
113125
3. **Issue #22** → complete (PR #35): public API export + package config
114126
4. **Issue #23** → complete (PR #39): scenarios package (format + 5 initial scenarios)
115-
5. **Issue #24** (this PR) → complete: reporter package (Markdown report generator)
116-
6. **Issue #25**: CLI package (scaffold + inspect + report + scenario commands)
127+
5. **Issue #24** → complete (PR #40): reporter package (Markdown report generator)
128+
6. **Issue #25** (this PR) → complete: CLI package (scaffold + inspect + report + scenario commands)
129+
7. **Issue #26**: Next.js app scaffold (Next.js + Nextra + Tailwind + Shadcn)
130+
8. **Issue #27**: Landing page
131+
9. **Issue #28**: Inspector (trace input + timeline + message inspector)
117132

118133
## Known Blockers / Decisions Pending
119134

@@ -126,7 +141,7 @@ report exported. CLI and web inspector.
126141
| `@ocpp-debugkit/core` | in progress (package config finalized, ready for downstream) | 0.0.0 |
127142
| `@ocpp-debugkit/scenarios` | in progress (5 scenarios + registry) | 0.0.0 |
128143
| `@ocpp-debugkit/reporter` | in progress (Markdown generator) | 0.0.0 |
129-
| `@ocpp-debugkit/cli` | not started | |
144+
| `@ocpp-debugkit/cli` | in progress (inspect + report + scenario) | 0.0.0 |
130145
| `@ocpp-debugkit/replay` | not started ||
131146
| `@ocpp-debugkit/react` | not started ||
132147
| `apps/web` | not started ||

packages/cli/package.json

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{
2+
"name": "@ocpp-debugkit/cli",
3+
"version": "0.0.0",
4+
"description": "Command-line interface for OCPP DebugKit — inspect traces, generate reports, run scenarios.",
5+
"license": "Apache-2.0",
6+
"type": "module",
7+
"main": "./dist/index.js",
8+
"types": "./dist/index.d.ts",
9+
"bin": {
10+
"ocpp-debugkit": "./dist/index.js"
11+
},
12+
"files": [
13+
"dist",
14+
"README.md",
15+
"NOTICE"
16+
],
17+
"keywords": [
18+
"ocpp",
19+
"ocpp1.6",
20+
"ev-charging",
21+
"cli",
22+
"debugging",
23+
"trace-inspector"
24+
],
25+
"repository": {
26+
"type": "git",
27+
"url": "https://github.com/ocpp-debugkit/ocpp-debugkit.git",
28+
"directory": "packages/cli"
29+
},
30+
"homepage": "https://github.com/ocpp-debugkit/ocpp-debugkit/tree/main/packages/cli",
31+
"bugs": {
32+
"url": "https://github.com/ocpp-debugkit/ocpp-debugkit/issues"
33+
},
34+
"scripts": {
35+
"build": "tsc -p tsconfig.json",
36+
"typecheck": "tsc --noEmit",
37+
"test": "vitest run",
38+
"clean": "rm -rf dist .turbo"
39+
},
40+
"dependencies": {
41+
"@ocpp-debugkit/core": "workspace:*",
42+
"@ocpp-debugkit/scenarios": "workspace:*",
43+
"@ocpp-debugkit/reporter": "workspace:*",
44+
"commander": "^13.0.0"
45+
},
46+
"devDependencies": {
47+
"typescript": "^5.7.0",
48+
"vitest": "^3.0.0",
49+
"execa": "^9.0.0"
50+
},
51+
"publishConfig": {
52+
"access": "public"
53+
}
54+
}

packages/cli/src/cli.test.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { execa } from 'execa';
3+
import { writeFileSync, mkdtempSync } from 'node:fs';
4+
import { tmpdir } from 'node:os';
5+
import { join } from 'node:path';
6+
import { fixtures } from '@ocpp-debugkit/core';
7+
8+
const CLI_PATH = join(process.cwd(), 'packages/cli/dist/index.js');
9+
10+
// Helper: run the CLI with given args
11+
async function runCli(
12+
...args: string[]
13+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
14+
try {
15+
const result = await execa('node', [CLI_PATH, ...args]);
16+
return { stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode ?? 0 };
17+
} catch (e) {
18+
const error = e as { stdout?: string; stderr?: string; exitCode?: number };
19+
return {
20+
stdout: error.stdout ?? '',
21+
stderr: error.stderr ?? '',
22+
exitCode: error.exitCode ?? 1,
23+
};
24+
}
25+
}
26+
27+
// Helper: write a trace to a temp file
28+
function writeTempTrace(trace: unknown): string {
29+
const dir = mkdtempSync(join(tmpdir(), 'ocpp-test-'));
30+
const filePath = join(dir, 'trace.json');
31+
writeFileSync(filePath, JSON.stringify(trace), 'utf8');
32+
return filePath;
33+
}
34+
35+
describe('CLI integration', () => {
36+
describe('--version', () => {
37+
it('outputs version', async () => {
38+
const result = await runCli('--version');
39+
expect(result.exitCode).toBe(0);
40+
expect(result.stdout).toContain('0.0.0');
41+
});
42+
});
43+
44+
describe('--help', () => {
45+
it('shows help with all commands', async () => {
46+
const result = await runCli('--help');
47+
expect(result.exitCode).toBe(0);
48+
expect(result.stdout).toContain('inspect');
49+
expect(result.stdout).toContain('report');
50+
expect(result.stdout).toContain('scenario');
51+
});
52+
});
53+
54+
describe('inspect', () => {
55+
it('inspects a normal session trace', async () => {
56+
const filePath = writeTempTrace(fixtures.normalSession);
57+
const result = await runCli('inspect', filePath);
58+
expect(result.exitCode).toBe(0);
59+
expect(result.stdout).toContain('Trace Inspection');
60+
expect(result.stdout).toContain('Sessions: 1');
61+
expect(result.stdout).toContain('No failures detected');
62+
});
63+
64+
it('inspects a failed-auth trace and shows failures', async () => {
65+
const filePath = writeTempTrace(fixtures.failedAuth);
66+
const result = await runCli('inspect', filePath);
67+
expect(result.exitCode).toBe(0);
68+
expect(result.stdout).toContain('FAILED_AUTHORIZATION');
69+
});
70+
71+
it('inspects a connector-fault trace and shows failures', async () => {
72+
const filePath = writeTempTrace(fixtures.connectorFault);
73+
const result = await runCli('inspect', filePath);
74+
expect(result.exitCode).toBe(0);
75+
expect(result.stdout).toContain('CONNECTOR_FAULT');
76+
});
77+
78+
it('handles missing file gracefully', async () => {
79+
const result = await runCli('inspect', '/nonexistent/file.json');
80+
expect(result.exitCode).not.toBe(0);
81+
});
82+
83+
it('handles invalid JSON gracefully', async () => {
84+
const dir = mkdtempSync(join(tmpdir(), 'ocpp-test-'));
85+
const filePath = join(dir, 'bad.json');
86+
writeFileSync(filePath, '{ invalid json }', 'utf8');
87+
const result = await runCli('inspect', filePath);
88+
expect(result.exitCode).not.toBe(0);
89+
});
90+
});
91+
92+
describe('report', () => {
93+
it('generates a Markdown report to stdout', async () => {
94+
const filePath = writeTempTrace(fixtures.normalSession);
95+
const result = await runCli('report', filePath);
96+
expect(result.exitCode).toBe(0);
97+
expect(result.stdout).toContain('OCPP DebugKit — Trace Analysis Report');
98+
expect(result.stdout).toContain('## Session Overview');
99+
expect(result.stdout).toContain('## Failures');
100+
});
101+
102+
it('generates a report with failures', async () => {
103+
const filePath = writeTempTrace(fixtures.failedAuth);
104+
const result = await runCli('report', filePath);
105+
expect(result.exitCode).toBe(0);
106+
expect(result.stdout).toContain('FAILED_AUTHORIZATION');
107+
expect(result.stdout).toContain('## Suggested Next Steps');
108+
});
109+
110+
it('writes report to a file with --output', async () => {
111+
const filePath = writeTempTrace(fixtures.normalSession);
112+
const dir = mkdtempSync(join(tmpdir(), 'ocpp-test-'));
113+
const outputPath = join(dir, 'report.md');
114+
const result = await runCli('report', filePath, '--output', outputPath);
115+
expect(result.exitCode).toBe(0);
116+
expect(result.stdout).toContain('Report written to');
117+
});
118+
});
119+
120+
describe('scenario list', () => {
121+
it('lists all available scenarios', async () => {
122+
const result = await runCli('scenario', 'list');
123+
expect(result.exitCode).toBe(0);
124+
expect(result.stdout).toContain('normal-session');
125+
expect(result.stdout).toContain('failed-auth');
126+
expect(result.stdout).toContain('connector-fault');
127+
expect(result.stdout).toContain('station-offline');
128+
expect(result.stdout).toContain('unexpected-stop-reason');
129+
});
130+
});
131+
132+
describe('scenario run', () => {
133+
it('runs normal-session scenario and passes', async () => {
134+
const result = await runCli('scenario', 'run', 'normal-session');
135+
expect(result.exitCode).toBe(0);
136+
expect(result.stdout).toContain('Running Scenario: normal-session');
137+
expect(result.stdout).toContain('No failures expected, none detected');
138+
expect(result.stdout).toContain('PASS');
139+
});
140+
141+
it('runs failed-auth scenario and detects FAILED_AUTHORIZATION', async () => {
142+
const result = await runCli('scenario', 'run', 'failed-auth');
143+
expect(result.exitCode).toBe(0);
144+
expect(result.stdout).toContain('FAILED_AUTHORIZATION');
145+
expect(result.stdout).toContain('PASS');
146+
});
147+
148+
it('runs connector-fault scenario and detects CONNECTOR_FAULT', async () => {
149+
const result = await runCli('scenario', 'run', 'connector-fault');
150+
expect(result.exitCode).toBe(0);
151+
expect(result.stdout).toContain('CONNECTOR_FAULT');
152+
expect(result.stdout).toContain('PASS');
153+
});
154+
155+
it('runs station-offline scenario and detects STATION_OFFLINE_DURING_SESSION', async () => {
156+
const result = await runCli('scenario', 'run', 'station-offline');
157+
expect(result.exitCode).toBe(0);
158+
expect(result.stdout).toContain('STATION_OFFLINE_DURING_SESSION');
159+
expect(result.stdout).toContain('PASS');
160+
});
161+
162+
it('runs unexpected-stop-reason scenario (no failures)', async () => {
163+
const result = await runCli('scenario', 'run', 'unexpected-stop-reason');
164+
expect(result.exitCode).toBe(0);
165+
expect(result.stdout).toContain('No failures expected, none detected');
166+
expect(result.stdout).toContain('PASS');
167+
});
168+
169+
it('handles unknown scenario name', async () => {
170+
const result = await runCli('scenario', 'run', 'nonexistent');
171+
expect(result.exitCode).not.toBe(0);
172+
});
173+
});
174+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* `ocpp-debugkit inspect <file>` — parse and analyze an OCPP trace file.
3+
*/
4+
5+
import {
6+
parseTrace,
7+
buildSessionTimeline,
8+
detectFailures,
9+
summarizeSessions,
10+
ParseError,
11+
} from '@ocpp-debugkit/core';
12+
import { CliError, readTraceFile } from '../utils.js';
13+
14+
export interface InspectOptions {
15+
format: string;
16+
}
17+
18+
export async function inspectCommand(file: string, _options: InspectOptions): Promise<void> {
19+
const content = readTraceFile(file);
20+
21+
let result;
22+
try {
23+
result = parseTrace(content);
24+
} catch (e) {
25+
if (e instanceof ParseError) {
26+
throw new CliError(e.message);
27+
}
28+
if (e instanceof Error) {
29+
throw new CliError(e.message);
30+
}
31+
throw new CliError('Unknown error during parsing');
32+
}
33+
34+
const sessions = buildSessionTimeline(result.events);
35+
const failures = detectFailures(result.events, sessions);
36+
const summaries = summarizeSessions(sessions, failures);
37+
38+
// Output to stdout
39+
console.log('');
40+
console.log('═'.repeat(60));
41+
console.log(' OCPP DebugKit — Trace Inspection');
42+
console.log('═'.repeat(60));
43+
console.log('');
44+
45+
console.log(` Events: ${result.events.length}`);
46+
console.log(` Sessions: ${sessions.length}`);
47+
console.log(` Failures: ${failures.length}`);
48+
console.log(` Warnings: ${result.warnings.length}`);
49+
console.log('');
50+
51+
if (result.warnings.length > 0) {
52+
console.log('── Parse Warnings ──────────────────────────────────────');
53+
for (const w of result.warnings) {
54+
console.log(` ⚠ ${w.message}`);
55+
}
56+
console.log('');
57+
}
58+
59+
console.log('── Sessions ────────────────────────────────────────────');
60+
for (const summary of summaries) {
61+
const duration =
62+
summary.durationMs !== null ? `${(summary.durationMs / 1000).toFixed(1)}s` : 'Unknown';
63+
console.log(
64+
` ${summary.sessionId} | station=${summary.stationId} | conn=${summary.connectorId ?? '-'} | tx=${summary.transactionId ?? '-'} | ${summary.status} | ${duration} | ${summary.eventCount} events | ${summary.failureCount} failures`,
65+
);
66+
}
67+
console.log('');
68+
69+
if (failures.length > 0) {
70+
console.log('── Failures ────────────────────────────────────────────');
71+
for (const failure of failures) {
72+
console.log(` [${failure.severity.toUpperCase()}] ${failure.code}`);
73+
console.log(` ${failure.description}`);
74+
console.log(` Events: ${failure.eventIds.join(', ')}`);
75+
}
76+
console.log('');
77+
} else {
78+
console.log('── Failures ────────────────────────────────────────────');
79+
console.log(' No failures detected. ✅');
80+
console.log('');
81+
}
82+
}

0 commit comments

Comments
 (0)