opstruth 0.1.1 → 0.1.2
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 +6 -0
- package/package.json +1 -1
- package/src/cli.js +12 -3
- package/src/lib/markdown.js +1 -1
- package/src/lib/terminal.js +119 -0
package/README.md
CHANGED
|
@@ -30,6 +30,12 @@ opstruth routes --base-url https://example.com
|
|
|
30
30
|
opstruth local --port 3000 --health /health
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Terminal Output
|
|
34
|
+
|
|
35
|
+
Human output uses a restrained colour theme for status, warnings, proof gaps, evidence, and next safe steps when the terminal supports ANSI colour.
|
|
36
|
+
|
|
37
|
+
Use `--no-color` or `NO_COLOR=1` to disable colour. Use `--color` to force colour for demos. JSON output remains machine-readable and does not include ANSI codes.
|
|
38
|
+
|
|
33
39
|
## Safety Model
|
|
34
40
|
|
|
35
41
|
opstruth is read-only by default. CLI checks do not deploy, mutate databases, trigger queues or jobs, call OpenAI, restart services, publish content, or print raw secrets.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runLocal } from './commands/local.js';
|
|
|
12
12
|
import { runEvidence } from './commands/evidence.js';
|
|
13
13
|
import { runProbes } from './commands/probes.js';
|
|
14
14
|
import { resultToMarkdown } from './lib/markdown.js';
|
|
15
|
+
import { formatTerminalOutput } from './lib/terminal.js';
|
|
15
16
|
import { writeFileSafe } from './lib/fs.js';
|
|
16
17
|
import { redactObject } from './lib/redact.js';
|
|
17
18
|
import { exitCodeFor } from './lib/result.js';
|
|
@@ -26,6 +27,8 @@ export function parseArgs(argv) {
|
|
|
26
27
|
if (!arg.startsWith('-') && !command && COMMANDS.has(arg)) { command = arg; continue; }
|
|
27
28
|
if (arg === '--help' || arg === '-h') options.help = true;
|
|
28
29
|
if (arg === '--json') options.json = true;
|
|
30
|
+
else if (arg === '--color') options.color = true;
|
|
31
|
+
else if (arg === '--no-color') options.noColor = true;
|
|
29
32
|
else if (arg === '--out') options.out = take(argv, i++);
|
|
30
33
|
else if (arg === '--base-url') options.baseUrl = take(argv, i++);
|
|
31
34
|
else if (arg === '--routes') options.routesFile = take(argv, i++);
|
|
@@ -108,6 +111,8 @@ function helpText(command) {
|
|
|
108
111
|
' --out <file> Write command output/evidence',
|
|
109
112
|
' --skip <area|id> Skip a command or probe area',
|
|
110
113
|
' --only <area|id> Select a probe area or id',
|
|
114
|
+
' --color Force colour for human terminal output',
|
|
115
|
+
' --no-color Disable colour for human terminal output',
|
|
111
116
|
' -h, --help Print help and exit 0'
|
|
112
117
|
];
|
|
113
118
|
const route = [
|
|
@@ -120,6 +125,8 @@ function helpText(command) {
|
|
|
120
125
|
' --base-url <url> Base URL to probe',
|
|
121
126
|
' --routes <file> JSON route config',
|
|
122
127
|
' --json Print JSON output',
|
|
128
|
+
' --color Force colour for human terminal output',
|
|
129
|
+
' --no-color Disable colour for human terminal output',
|
|
123
130
|
' -h, --help Print help and exit 0'
|
|
124
131
|
];
|
|
125
132
|
const repo = [
|
|
@@ -130,6 +137,8 @@ function helpText(command) {
|
|
|
130
137
|
'',
|
|
131
138
|
'Options:',
|
|
132
139
|
' --json Print JSON output',
|
|
140
|
+
' --color Force colour for human terminal output',
|
|
141
|
+
' --no-color Disable colour for human terminal output',
|
|
133
142
|
' -h, --help Print help and exit 0'
|
|
134
143
|
];
|
|
135
144
|
if (command === 'routes') return route.join('\n') + '\n';
|
|
@@ -234,13 +243,13 @@ export async function runCli(argv, cwd = process.cwd()) {
|
|
|
234
243
|
const { command, options } = parseArgs(argv);
|
|
235
244
|
options.cwd = cwd;
|
|
236
245
|
if (options.help) {
|
|
237
|
-
await writeStdout(helpText(command));
|
|
246
|
+
await writeStdout(formatTerminalOutput(helpText(command), options));
|
|
238
247
|
process.exitCode = 0;
|
|
239
248
|
return;
|
|
240
249
|
}
|
|
241
250
|
const result = await dispatch(command, options);
|
|
242
251
|
if (typeof result === 'string') {
|
|
243
|
-
await writeStdout(result);
|
|
252
|
+
await writeStdout(formatTerminalOutput(result, options));
|
|
244
253
|
process.exitCode = 0;
|
|
245
254
|
return;
|
|
246
255
|
}
|
|
@@ -249,6 +258,6 @@ export async function runCli(argv, cwd = process.cwd()) {
|
|
|
249
258
|
const outPath = path.isAbsolute(options.out) ? options.out : path.join(cwd, options.out);
|
|
250
259
|
await writeFileSafe(outPath, output);
|
|
251
260
|
}
|
|
252
|
-
await writeStdout(output);
|
|
261
|
+
await writeStdout(options.json ? output : formatTerminalOutput(output, options));
|
|
253
262
|
process.exitCode = exitCodeFor(result, { strict: options.strict });
|
|
254
263
|
}
|
package/src/lib/markdown.js
CHANGED
|
@@ -9,7 +9,7 @@ const ASCII_HEADER = ` ____ _______ __ __
|
|
|
9
9
|
|
|
10
10
|
Operational truth checks for AI-assisted engineering.`;
|
|
11
11
|
|
|
12
|
-
function statusLabel(status) {
|
|
12
|
+
export function statusLabel(status) {
|
|
13
13
|
if (status === 'pass') return 'Pass';
|
|
14
14
|
if (status === 'warn') return 'Partial pass';
|
|
15
15
|
if (status === 'fail') return 'Fail';
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
2
|
+
|
|
3
|
+
const RESET = '\x1b[0m';
|
|
4
|
+
|
|
5
|
+
// Website-derived terminal approximations from website/src/styles.css.
|
|
6
|
+
// The website tokens are OKLCH; these RGB values keep the same calm dark,
|
|
7
|
+
// muted, evidence-first feel in common ANSI terminals.
|
|
8
|
+
export const THEME = {
|
|
9
|
+
heading: ['\x1b[1m', '\x1b[38;2;235;237;240m'],
|
|
10
|
+
muted: ['\x1b[38;2;150;154;163m'],
|
|
11
|
+
accent: ['\x1b[38;2;126;190;164m'],
|
|
12
|
+
success: ['\x1b[38;2;104;194;142m'],
|
|
13
|
+
warning: ['\x1b[38;2;219;178;80m'],
|
|
14
|
+
failure: ['\x1b[38;2;221;105;92m'],
|
|
15
|
+
code: ['\x1b[38;2;226;229;232m'],
|
|
16
|
+
border: ['\x1b[38;2;82;87;96m']
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function hasAnsi(text) {
|
|
20
|
+
return ANSI_PATTERN.test(text);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function stripAnsi(text) {
|
|
24
|
+
return String(text).replace(ANSI_PATTERN, '');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function supportsColor(options = {}, stream = process.stdout, env = process.env) {
|
|
28
|
+
if (options.json || options.noColor) return false;
|
|
29
|
+
if (options.color) return true;
|
|
30
|
+
if ('NO_COLOR' in env) return false;
|
|
31
|
+
if (env.TERM === 'dumb') return false;
|
|
32
|
+
return Boolean(stream?.isTTY);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function style(text, token, options = {}) {
|
|
36
|
+
if (!options.colorEnabled) return String(text);
|
|
37
|
+
const open = THEME[token] || THEME.heading;
|
|
38
|
+
return open.join('') + text + RESET;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function statusToken(label = '') {
|
|
42
|
+
const normalized = label.toLowerCase();
|
|
43
|
+
if (normalized === 'pass' || normalized === 'verified') return 'success';
|
|
44
|
+
if (normalized === 'partial pass' || normalized === 'warn' || normalized === 'warning') return 'warning';
|
|
45
|
+
if (normalized === 'fail' || normalized === 'failure') return 'failure';
|
|
46
|
+
if (normalized === 'skipped' || normalized === 'not verified' || normalized === 'not_verified') return 'muted';
|
|
47
|
+
return 'heading';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sectionToken(line) {
|
|
51
|
+
if (/^## Verified\b/.test(line)) return 'success';
|
|
52
|
+
if (/^## Warnings\b/.test(line)) return 'warning';
|
|
53
|
+
if (/^## Failures\b/.test(line)) return 'failure';
|
|
54
|
+
if (/^## Skipped\b/.test(line)) return 'muted';
|
|
55
|
+
if (/^## Not Verified\b/.test(line)) return 'warning';
|
|
56
|
+
if (/^## Evidence\b/.test(line)) return 'accent';
|
|
57
|
+
if (/^## Next Safe Step\b/.test(line)) return 'accent';
|
|
58
|
+
if (/^## Overall Confidence\b/.test(line)) return 'accent';
|
|
59
|
+
if (/^## Checks\b|^## Check Summary\b|^## What Matters Most\b/.test(line)) return 'heading';
|
|
60
|
+
return 'heading';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function colorStatusWords(line, options) {
|
|
64
|
+
return line.replace(/\b(Partial pass|Not verified|Pass|Fail|Skipped|pass|warn|fail|skipped|not_verified)\b/g, (label) => {
|
|
65
|
+
return style(label, statusToken(label), options);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function colorTableLine(line, options) {
|
|
70
|
+
if (/^\| [-| ]+\|$/.test(line) || /^\| Status \|/.test(line) || /^\| Area \|/.test(line)) {
|
|
71
|
+
return style(line, 'border', options);
|
|
72
|
+
}
|
|
73
|
+
return line.replace(/\| (Partial pass|Not verified|Pass|Fail|Skipped|pass|warn|fail|skipped|not_verified) \|/g, (_, label) => {
|
|
74
|
+
return '| ' + style(label, statusToken(label), options) + ' |';
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function colorEvidenceLine(line, options) {
|
|
79
|
+
const fileMatch = line.match(/^(\s*evidence: )(file:)(\s+)(.+)$/);
|
|
80
|
+
if (fileMatch) {
|
|
81
|
+
return style(fileMatch[1], 'muted', options) + style(fileMatch[2], 'accent', options) + fileMatch[3] + style(fileMatch[4], 'code', options);
|
|
82
|
+
}
|
|
83
|
+
const writtenMatch = line.match(/^(.*Evidence written to:\s+)(.+)$/);
|
|
84
|
+
if (writtenMatch) {
|
|
85
|
+
return style(writtenMatch[1], 'accent', options) + style(writtenMatch[2], 'code', options);
|
|
86
|
+
}
|
|
87
|
+
let formatted = line.replace(/^(\s*evidence: )([^:]+:)/, (_, prefix, key) => {
|
|
88
|
+
return style(prefix, 'muted', options) + style(key, 'accent', options);
|
|
89
|
+
});
|
|
90
|
+
formatted = formatted.replace(/^(\s*(why it matters|next safe step):)/, (match) => style(match, 'accent', options));
|
|
91
|
+
return colorStatusWords(formatted, options);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function colorLine(line, options) {
|
|
95
|
+
if (!line || hasAnsi(line)) return line;
|
|
96
|
+
if (/^ ____|^ \/|^ \/ |^\/ \/_|^\\____|^ \/_/.test(line)) return style(line, 'accent', options);
|
|
97
|
+
if (/^Operational truth checks/.test(line)) return style(line, 'muted', options);
|
|
98
|
+
if (/^# /.test(line)) return style(line.replace(/^# /, ''), 'heading', options);
|
|
99
|
+
if (/^## /.test(line)) return style(line.replace(/^## /, ''), sectionToken(line), options);
|
|
100
|
+
if (/^STATUS: /.test(line)) {
|
|
101
|
+
const label = line.slice('STATUS: '.length);
|
|
102
|
+
return style('STATUS:', 'muted', options) + ' ' + style(label, statusToken(label), options);
|
|
103
|
+
}
|
|
104
|
+
if (/^\|/.test(line)) return colorTableLine(line, options);
|
|
105
|
+
if (/^- \[(pass|warn|fail|skipped|not_verified)\]/.test(line)) return colorStatusWords(line, options);
|
|
106
|
+
if (/^\s*(evidence|why it matters|next safe step):/.test(line)) return colorEvidenceLine(line, options);
|
|
107
|
+
if (/Evidence written to:|Evidence file was not written/.test(line)) return colorEvidenceLine(line, options);
|
|
108
|
+
if (/^Usage:|^Commands:|^Global options:|^Options:|^Common workflows:|^Safety philosophy:/.test(line)) return style(line, 'heading', options);
|
|
109
|
+
if (/^\s{2}(opstruth|--|-h|npm|npx)\b/.test(line) || /^- opstruth\b/.test(line)) return style(line, 'code', options);
|
|
110
|
+
if (/^- None$|^- No failures$/.test(line)) return style(line, 'muted', options);
|
|
111
|
+
return line;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function formatTerminalOutput(text, options = {}) {
|
|
115
|
+
const colorEnabled = supportsColor(options);
|
|
116
|
+
if (!colorEnabled) return String(text);
|
|
117
|
+
const styleOptions = { ...options, colorEnabled };
|
|
118
|
+
return String(text).split('\n').map((line) => colorLine(line, styleOptions)).join('\n');
|
|
119
|
+
}
|