@opensip-tools/contracts 1.0.4
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/LICENSE +21 -0
- package/dist/__tests__/dashboard.test.d.ts +2 -0
- package/dist/__tests__/dashboard.test.d.ts.map +1 -0
- package/dist/__tests__/dashboard.test.js +85 -0
- package/dist/__tests__/dashboard.test.js.map +1 -0
- package/dist/__tests__/exit-codes.test.d.ts +2 -0
- package/dist/__tests__/exit-codes.test.d.ts.map +1 -0
- package/dist/__tests__/exit-codes.test.js +73 -0
- package/dist/__tests__/exit-codes.test.js.map +1 -0
- package/dist/__tests__/store.test.d.ts +2 -0
- package/dist/__tests__/store.test.d.ts.map +1 -0
- package/dist/__tests__/store.test.js +169 -0
- package/dist/__tests__/store.test.js.map +1 -0
- package/dist/exit-codes.d.ts +14 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +61 -0
- package/dist/exit-codes.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/dashboard/checks.d.ts +7 -0
- package/dist/persistence/dashboard/checks.d.ts.map +1 -0
- package/dist/persistence/dashboard/checks.js +279 -0
- package/dist/persistence/dashboard/checks.js.map +1 -0
- package/dist/persistence/dashboard/css.d.ts +6 -0
- package/dist/persistence/dashboard/css.d.ts.map +1 -0
- package/dist/persistence/dashboard/css.js +141 -0
- package/dist/persistence/dashboard/css.js.map +1 -0
- package/dist/persistence/dashboard/generator.d.ts +9 -0
- package/dist/persistence/dashboard/generator.d.ts.map +1 -0
- package/dist/persistence/dashboard/generator.js +79 -0
- package/dist/persistence/dashboard/generator.js.map +1 -0
- package/dist/persistence/dashboard/index.d.ts +5 -0
- package/dist/persistence/dashboard/index.d.ts.map +1 -0
- package/dist/persistence/dashboard/index.js +5 -0
- package/dist/persistence/dashboard/index.js.map +1 -0
- package/dist/persistence/dashboard/overview.d.ts +6 -0
- package/dist/persistence/dashboard/overview.d.ts.map +1 -0
- package/dist/persistence/dashboard/overview.js +65 -0
- package/dist/persistence/dashboard/overview.js.map +1 -0
- package/dist/persistence/dashboard/recipes.d.ts +6 -0
- package/dist/persistence/dashboard/recipes.d.ts.map +1 -0
- package/dist/persistence/dashboard/recipes.js +68 -0
- package/dist/persistence/dashboard/recipes.js.map +1 -0
- package/dist/persistence/dashboard/sessions.d.ts +6 -0
- package/dist/persistence/dashboard/sessions.d.ts.map +1 -0
- package/dist/persistence/dashboard/sessions.js +205 -0
- package/dist/persistence/dashboard/sessions.js.map +1 -0
- package/dist/persistence/dashboard/shared.d.ts +6 -0
- package/dist/persistence/dashboard/shared.d.ts.map +1 -0
- package/dist/persistence/dashboard/shared.js +211 -0
- package/dist/persistence/dashboard/shared.js.map +1 -0
- package/dist/persistence/dashboard/tool-tabs.d.ts +6 -0
- package/dist/persistence/dashboard/tool-tabs.d.ts.map +1 -0
- package/dist/persistence/dashboard/tool-tabs.js +102 -0
- package/dist/persistence/dashboard/tool-tabs.js.map +1 -0
- package/dist/persistence/store.d.ts +103 -0
- package/dist/persistence/store.d.ts.map +1 -0
- package/dist/persistence/store.js +156 -0
- package/dist/persistence/store.js.map +1 -0
- package/dist/types.d.ts +279 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +35 -0
- package/src/__tests__/dashboard.test.ts +102 -0
- package/src/__tests__/exit-codes.test.ts +87 -0
- package/src/__tests__/store.test.ts +213 -0
- package/src/exit-codes.ts +74 -0
- package/src/index.ts +71 -0
- package/src/persistence/dashboard/checks.ts +279 -0
- package/src/persistence/dashboard/css.ts +141 -0
- package/src/persistence/dashboard/generator.ts +89 -0
- package/src/persistence/dashboard/index.ts +5 -0
- package/src/persistence/dashboard/overview.ts +65 -0
- package/src/persistence/dashboard/recipes.ts +68 -0
- package/src/persistence/dashboard/sessions.ts +205 -0
- package/src/persistence/dashboard/shared.ts +211 -0
- package/src/persistence/dashboard/tool-tabs.ts +102 -0
- package/src/persistence/store.ts +233 -0
- package/src/types.ts +306 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON file persistence for opensip-tools results.
|
|
3
|
+
*
|
|
4
|
+
* Sessions land at `<project>/opensip-tools/.runtime/sessions/`
|
|
5
|
+
* (per-project). Each run creates one file:
|
|
6
|
+
* `{timestamp}-{tool}-{recipe}.json`.
|
|
7
|
+
*
|
|
8
|
+
* The CLI bootstrap calls `configurePersistencePaths(projectPaths)`
|
|
9
|
+
* once on startup with paths from `resolveProjectPaths(cwd)`. Until
|
|
10
|
+
* that call, the module falls back to a user-global location
|
|
11
|
+
* (`~/.opensip-tools/`) so any caller who imports persistence helpers
|
|
12
|
+
* before the CLI's preAction hook still gets a valid path. The
|
|
13
|
+
* fallback is also exercised by tests that don't bootstrap a CLI.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
import { basename, join } from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { logger } from '@opensip-tools/core';
|
|
22
|
+
|
|
23
|
+
import type { ProjectPaths } from '@opensip-tools/core';
|
|
24
|
+
|
|
25
|
+
export interface StoredSession {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
readonly tool: 'fit' | 'sim';
|
|
28
|
+
readonly timestamp: string;
|
|
29
|
+
readonly cwd: string;
|
|
30
|
+
readonly recipe?: string;
|
|
31
|
+
readonly score: number;
|
|
32
|
+
readonly passed: boolean;
|
|
33
|
+
readonly summary: {
|
|
34
|
+
readonly total: number;
|
|
35
|
+
readonly passed: number;
|
|
36
|
+
readonly failed: number;
|
|
37
|
+
readonly errors: number;
|
|
38
|
+
readonly warnings: number;
|
|
39
|
+
};
|
|
40
|
+
readonly checks: readonly {
|
|
41
|
+
readonly checkSlug: string;
|
|
42
|
+
readonly passed: boolean;
|
|
43
|
+
readonly violationCount?: number;
|
|
44
|
+
readonly findings: readonly {
|
|
45
|
+
readonly ruleId: string;
|
|
46
|
+
readonly message: string;
|
|
47
|
+
readonly severity: string;
|
|
48
|
+
readonly filePath?: string;
|
|
49
|
+
readonly line?: number;
|
|
50
|
+
readonly column?: number;
|
|
51
|
+
readonly suggestion?: string;
|
|
52
|
+
readonly category?: string;
|
|
53
|
+
}[];
|
|
54
|
+
readonly durationMs: number;
|
|
55
|
+
}[];
|
|
56
|
+
readonly durationMs: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Check catalog entry for dashboard display */
|
|
60
|
+
export interface CheckCatalogEntry {
|
|
61
|
+
readonly slug: string;
|
|
62
|
+
readonly name: string;
|
|
63
|
+
readonly icon: string;
|
|
64
|
+
readonly description: string;
|
|
65
|
+
readonly longDescription?: string;
|
|
66
|
+
readonly tags: readonly string[];
|
|
67
|
+
readonly confidence: 'high' | 'medium' | 'low';
|
|
68
|
+
readonly source: 'built-in' | 'community';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Recipe catalog entry for dashboard display */
|
|
72
|
+
export interface RecipeCatalogEntry {
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly displayName: string;
|
|
75
|
+
readonly description: string;
|
|
76
|
+
readonly tags: readonly string[];
|
|
77
|
+
readonly selectorType: string;
|
|
78
|
+
readonly mode: string;
|
|
79
|
+
readonly timeout: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fallback path: user-global `~/.opensip-tools/`, used by tests and
|
|
84
|
+
* any code path that imports persistence helpers before the CLI has
|
|
85
|
+
* called `configurePersistencePaths`. New code should not rely on
|
|
86
|
+
* this fallback — call `configurePersistencePaths` first.
|
|
87
|
+
*/
|
|
88
|
+
export const TOOLS_HOME = join(homedir(), '.opensip-tools');
|
|
89
|
+
|
|
90
|
+
/** Mutable per-process state — set by `configurePersistencePaths`. */
|
|
91
|
+
let storeDir: string = join(TOOLS_HOME, 'sessions');
|
|
92
|
+
let reportsDir: string = join(TOOLS_HOME, 'reports');
|
|
93
|
+
const MAX_SESSIONS = 100;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Configure where this module writes sessions and reports. Called
|
|
97
|
+
* once by the CLI bootstrap with the project paths. Idempotent and
|
|
98
|
+
* safe to call repeatedly (e.g. tests that switch project dirs).
|
|
99
|
+
*/
|
|
100
|
+
export function configurePersistencePaths(paths: Pick<ProjectPaths, 'sessionsDir' | 'reportsDir'>): void {
|
|
101
|
+
storeDir = paths.sessionsDir;
|
|
102
|
+
reportsDir = paths.reportsDir;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Ensure directory exists — mkdirSync with recursive is idempotent */
|
|
106
|
+
function ensureDir(dir: string): void {
|
|
107
|
+
mkdirSync(dir, { recursive: true });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Sanitize a string for use in a filename — strip path separators and special chars */
|
|
111
|
+
export function sanitizeForFilename(s: string): string {
|
|
112
|
+
return s.replaceAll('..', '-').replaceAll(/[/\\:*?"<>|.]/g, '-');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Save a session result to disk */
|
|
116
|
+
export function saveSession(session: StoredSession): string {
|
|
117
|
+
ensureDir(storeDir);
|
|
118
|
+
const safeRecipe = session.recipe ? `-${sanitizeForFilename(session.recipe)}` : '';
|
|
119
|
+
const filename = `${session.timestamp.replaceAll(/[:.]/g, '-')}-${session.tool}${safeRecipe}.json`;
|
|
120
|
+
// Ensure filename stays within the sessions directory
|
|
121
|
+
const filepath = join(storeDir, basename(filename));
|
|
122
|
+
writeFileSync(filepath, JSON.stringify(session, null, 2), 'utf8');
|
|
123
|
+
|
|
124
|
+
pruneOldSessions();
|
|
125
|
+
return filepath;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Count session files in the store directory */
|
|
129
|
+
export function countSessions(): number {
|
|
130
|
+
ensureDir(storeDir);
|
|
131
|
+
return readdirSync(storeDir).filter(f => f.endsWith('.json')).length;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Delete all sessions. Returns the number of files deleted. */
|
|
135
|
+
export function clearAllSessions(): number {
|
|
136
|
+
ensureDir(storeDir);
|
|
137
|
+
const files = readdirSync(storeDir).filter(f => f.endsWith('.json'));
|
|
138
|
+
for (const file of files) {
|
|
139
|
+
unlinkSync(join(storeDir, file));
|
|
140
|
+
}
|
|
141
|
+
return files.length;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Delete sessions older than the given number of days. Returns the number of files deleted. */
|
|
145
|
+
export function clearSessionsOlderThan(days: number): number {
|
|
146
|
+
ensureDir(storeDir);
|
|
147
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
148
|
+
const files = readdirSync(storeDir).filter(f => f.endsWith('.json'));
|
|
149
|
+
let deleted = 0;
|
|
150
|
+
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
try {
|
|
153
|
+
const filepath = join(storeDir, file);
|
|
154
|
+
const raw = readFileSync(filepath, 'utf8');
|
|
155
|
+
const session = JSON.parse(raw) as { timestamp?: string };
|
|
156
|
+
if (session.timestamp) {
|
|
157
|
+
const sessionTime = new Date(session.timestamp).getTime();
|
|
158
|
+
if (!Number.isNaN(sessionTime) && sessionTime < cutoff) {
|
|
159
|
+
unlinkSync(filepath);
|
|
160
|
+
deleted++;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// Skip files that can't be read/parsed
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return deleted;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Load all sessions, newest first. Optional limit to avoid reading everything. */
|
|
172
|
+
export function loadSessions(limit?: number): StoredSession[] {
|
|
173
|
+
ensureDir(storeDir);
|
|
174
|
+
const files = readdirSync(storeDir)
|
|
175
|
+
.filter(f => f.endsWith('.json'))
|
|
176
|
+
.sort()
|
|
177
|
+
// eslint-disable-next-line unicorn/no-array-reverse -- target ES2022; Array#toReversed is ES2023 and not in the lib
|
|
178
|
+
.reverse();
|
|
179
|
+
|
|
180
|
+
const toRead = limit ? files.slice(0, limit) : files;
|
|
181
|
+
const sessions: StoredSession[] = [];
|
|
182
|
+
for (const file of toRead) {
|
|
183
|
+
try {
|
|
184
|
+
const raw = readFileSync(join(storeDir, file), 'utf8');
|
|
185
|
+
sessions.push(JSON.parse(raw) as StoredSession);
|
|
186
|
+
} catch {
|
|
187
|
+
// Warn about corrupted files — don't crash
|
|
188
|
+
logger.warn({ evt: 'cli.session.corrupted', module: 'cli:persistence', msg: `Skipping corrupted session file: ${file}`, file });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return sessions;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Load the most recent session */
|
|
195
|
+
export function loadLatestSession(): StoredSession | null {
|
|
196
|
+
const sessions = loadSessions(1);
|
|
197
|
+
return sessions[0] ?? null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Prune sessions beyond the max count */
|
|
201
|
+
function pruneOldSessions(): void {
|
|
202
|
+
const files = readdirSync(storeDir)
|
|
203
|
+
.filter(f => f.endsWith('.json'))
|
|
204
|
+
.sort()
|
|
205
|
+
// eslint-disable-next-line unicorn/no-array-reverse -- target ES2022; Array#toReversed is ES2023 and not in the lib
|
|
206
|
+
.reverse();
|
|
207
|
+
|
|
208
|
+
if (files.length <= MAX_SESSIONS) return;
|
|
209
|
+
|
|
210
|
+
for (const file of files.slice(MAX_SESSIONS)) {
|
|
211
|
+
try {
|
|
212
|
+
unlinkSync(join(storeDir, file));
|
|
213
|
+
} catch {
|
|
214
|
+
// Best effort
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Get the store directory path */
|
|
220
|
+
export function getStoreDir(): string {
|
|
221
|
+
return storeDir;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Get the reports directory path, creating it if needed */
|
|
225
|
+
export function getReportsDir(): string {
|
|
226
|
+
ensureDir(reportsDir);
|
|
227
|
+
return reportsDir;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Generate a unique session ID */
|
|
231
|
+
export function generateSessionId(): string {
|
|
232
|
+
return randomUUID();
|
|
233
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import type { StoredSession } from './persistence/store.js';
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// CLI OPTIONS TYPES
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
/** Options for the `fit` subcommand (derived from Commander flags). */
|
|
8
|
+
export interface FitOptions {
|
|
9
|
+
recipe?: string;
|
|
10
|
+
check?: string;
|
|
11
|
+
tags?: string;
|
|
12
|
+
list: boolean;
|
|
13
|
+
recipes: boolean;
|
|
14
|
+
json: boolean;
|
|
15
|
+
verbose: boolean;
|
|
16
|
+
findings: boolean;
|
|
17
|
+
reportTo?: string;
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
exclude: string[];
|
|
20
|
+
cwd: string;
|
|
21
|
+
/** Explicit path to opensip-tools.config.yml (overrides package.json pointer and default location). */
|
|
22
|
+
config?: string;
|
|
23
|
+
debug: boolean;
|
|
24
|
+
/** Architecture-gate: save the current run's findings as a baseline. Mutually exclusive with --gate-compare. */
|
|
25
|
+
gateSave?: boolean;
|
|
26
|
+
/** Architecture-gate: compare current findings against a saved baseline; exit 1 if regressions found. Mutually exclusive with --gate-save. */
|
|
27
|
+
gateCompare?: boolean;
|
|
28
|
+
/** Path to the baseline file used by --gate-save / --gate-compare. Default: opensip-tools/.runtime/baseline.sarif */
|
|
29
|
+
baseline?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Options for the `init` subcommand. */
|
|
33
|
+
export interface InitOptions {
|
|
34
|
+
cwd: string;
|
|
35
|
+
json: boolean;
|
|
36
|
+
debug: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Comma-separated language list. When omitted, init detects the
|
|
39
|
+
* project's primary language(s) by inspecting filesystem markers
|
|
40
|
+
* (Cargo.toml, pyproject.toml, etc.) and exits 2 with a prompt if
|
|
41
|
+
* the result is ambiguous.
|
|
42
|
+
*/
|
|
43
|
+
language?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Overwrite an existing opensip-tools.config.yml or example files
|
|
46
|
+
* without prompting. Default false — the safe behavior is to refuse
|
|
47
|
+
* overwriting.
|
|
48
|
+
*/
|
|
49
|
+
force: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Options for `sim` subcommand. */
|
|
53
|
+
export interface ToolOptions {
|
|
54
|
+
cwd: string;
|
|
55
|
+
json: boolean;
|
|
56
|
+
debug: boolean;
|
|
57
|
+
/** Recipe name to run. Defaults to the built-in `default` if omitted. */
|
|
58
|
+
recipe?: string;
|
|
59
|
+
/** Filter by scenario kind (load / chaos / invariant / fix-evaluation). */
|
|
60
|
+
kind?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Backwards-compatible alias — commands that previously accepted CliArgs
|
|
65
|
+
* can accept this union instead. The shape covers all fields used by any command.
|
|
66
|
+
*/
|
|
67
|
+
export interface CliArgs {
|
|
68
|
+
command: string;
|
|
69
|
+
json: boolean;
|
|
70
|
+
check?: string;
|
|
71
|
+
recipe?: string;
|
|
72
|
+
cwd: string;
|
|
73
|
+
help: boolean;
|
|
74
|
+
list: boolean;
|
|
75
|
+
listRecipes: boolean;
|
|
76
|
+
verbose: boolean;
|
|
77
|
+
reportTo?: string;
|
|
78
|
+
apiKey?: string;
|
|
79
|
+
exclude: string[];
|
|
80
|
+
findings: boolean;
|
|
81
|
+
tags?: string;
|
|
82
|
+
/** Suppress banner/boxes; show only the pass-fail summary line. */
|
|
83
|
+
quiet?: boolean;
|
|
84
|
+
/** Open the HTML dashboard in the default browser after a successful run. */
|
|
85
|
+
open?: boolean;
|
|
86
|
+
/** Explicit opensip-tools.config.yml path from --config flag. */
|
|
87
|
+
config?: string;
|
|
88
|
+
/** Architecture-gate flags — see FitOptions for details. */
|
|
89
|
+
gateSave?: boolean;
|
|
90
|
+
gateCompare?: boolean;
|
|
91
|
+
baseline?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Sim-only: filter scenarios by kind.
|
|
94
|
+
* One of 'load' | 'chaos' | 'invariant' | 'fix-evaluation', or undefined for all.
|
|
95
|
+
*/
|
|
96
|
+
kind?: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Structured JSON output format */
|
|
100
|
+
export interface CliOutput {
|
|
101
|
+
readonly version: '1.0';
|
|
102
|
+
readonly tool: 'fit' | 'sim';
|
|
103
|
+
readonly timestamp: string;
|
|
104
|
+
readonly recipe?: string;
|
|
105
|
+
readonly score: number;
|
|
106
|
+
readonly passed: boolean;
|
|
107
|
+
readonly summary: { total: number; passed: number; failed: number; errors: number; warnings: number };
|
|
108
|
+
readonly checks: readonly CheckOutput[];
|
|
109
|
+
readonly durationMs: number;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface CheckOutput {
|
|
113
|
+
readonly checkSlug: string;
|
|
114
|
+
readonly passed: boolean;
|
|
115
|
+
readonly violationCount?: number;
|
|
116
|
+
readonly findings: readonly FindingOutput[];
|
|
117
|
+
readonly durationMs: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface FindingOutput {
|
|
121
|
+
readonly ruleId: string;
|
|
122
|
+
readonly message: string;
|
|
123
|
+
readonly severity: 'error' | 'warning';
|
|
124
|
+
readonly filePath?: string;
|
|
125
|
+
readonly line?: number;
|
|
126
|
+
readonly column?: number;
|
|
127
|
+
readonly suggestion?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface TableRow {
|
|
131
|
+
check: string;
|
|
132
|
+
status: 'PASS' | 'FAIL' | 'TIMEOUT';
|
|
133
|
+
errors: number;
|
|
134
|
+
warnings: number;
|
|
135
|
+
validated: string;
|
|
136
|
+
ignored: number;
|
|
137
|
+
duration: string;
|
|
138
|
+
durationMs: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface SummaryOptions {
|
|
142
|
+
passed: number;
|
|
143
|
+
failed: number;
|
|
144
|
+
totalErrors: number;
|
|
145
|
+
totalWarnings: number;
|
|
146
|
+
totalIgnored: number;
|
|
147
|
+
durationMs: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// =============================================================================
|
|
151
|
+
// CommandResult — union type for all command results
|
|
152
|
+
// =============================================================================
|
|
153
|
+
|
|
154
|
+
/** Union type for all command results — App.tsx dispatches on result.type */
|
|
155
|
+
export type CommandResult =
|
|
156
|
+
| FitDoneResult
|
|
157
|
+
| SimDoneResult
|
|
158
|
+
| ListChecksResult
|
|
159
|
+
| ListRecipesResult
|
|
160
|
+
| HistoryResult
|
|
161
|
+
| DashboardResult
|
|
162
|
+
| InitResult
|
|
163
|
+
| ExperimentalResult
|
|
164
|
+
| PluginResult
|
|
165
|
+
| ClearDoneResult
|
|
166
|
+
| HelpResult
|
|
167
|
+
| ErrorResult;
|
|
168
|
+
|
|
169
|
+
export interface ClearDoneResult {
|
|
170
|
+
type: 'clear-done';
|
|
171
|
+
action: 'done' | 'cancelled' | 'empty';
|
|
172
|
+
deletedCount: number;
|
|
173
|
+
sessionCount: number;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface FitDoneResult {
|
|
177
|
+
type: 'fit-done';
|
|
178
|
+
rows: TableRow[];
|
|
179
|
+
summary: SummaryOptions;
|
|
180
|
+
label: string;
|
|
181
|
+
cwd: string;
|
|
182
|
+
findings?: {
|
|
183
|
+
checks: {
|
|
184
|
+
checkSlug: string;
|
|
185
|
+
errorCount: number;
|
|
186
|
+
warningCount: number;
|
|
187
|
+
error?: string;
|
|
188
|
+
violations?: {
|
|
189
|
+
severity: 'error' | 'warning';
|
|
190
|
+
message: string;
|
|
191
|
+
file?: string;
|
|
192
|
+
line?: number;
|
|
193
|
+
suggestion?: string;
|
|
194
|
+
}[];
|
|
195
|
+
}[];
|
|
196
|
+
};
|
|
197
|
+
reportStatus?: {
|
|
198
|
+
url: string;
|
|
199
|
+
findingCount: number;
|
|
200
|
+
runCount: number;
|
|
201
|
+
success: boolean;
|
|
202
|
+
error?: string;
|
|
203
|
+
chunksTotal?: number;
|
|
204
|
+
chunksSucceeded?: number;
|
|
205
|
+
};
|
|
206
|
+
/** Whether the run should cause a non-zero exit code (based on failOnErrors/failOnWarnings config) */
|
|
207
|
+
shouldFail?: boolean;
|
|
208
|
+
/** Whether an opensip-tools.config.yml was found in the target directory */
|
|
209
|
+
configFound?: boolean;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ListChecksResult {
|
|
213
|
+
type: 'list-checks';
|
|
214
|
+
checks: { slug: string; description: string; tags: string[] }[];
|
|
215
|
+
totalCount: number;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface ListRecipesResult {
|
|
219
|
+
type: 'list-recipes';
|
|
220
|
+
recipes: { name: string; description: string; checkCount: string }[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface HistoryResult {
|
|
224
|
+
type: 'history';
|
|
225
|
+
sessions: StoredSession[];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface DashboardResult {
|
|
229
|
+
type: 'dashboard';
|
|
230
|
+
path: string;
|
|
231
|
+
opened: boolean;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface InitResult {
|
|
235
|
+
type: 'init';
|
|
236
|
+
created: boolean;
|
|
237
|
+
path: string;
|
|
238
|
+
alreadyExists: boolean;
|
|
239
|
+
cwd: string;
|
|
240
|
+
configFilename: string;
|
|
241
|
+
/** Languages selected for this scaffold (post-detection or from --language). */
|
|
242
|
+
languages?: readonly ('typescript' | 'rust' | 'python' | 'go' | 'java' | 'cpp')[];
|
|
243
|
+
/**
|
|
244
|
+
* Every file init created, in display order. Includes the config
|
|
245
|
+
* file plus example check / recipe / scenario scaffolds. Empty
|
|
246
|
+
* when alreadyExists is true (nothing was written).
|
|
247
|
+
*/
|
|
248
|
+
createdFiles?: readonly string[];
|
|
249
|
+
/** True when init appended `opensip-tools/.runtime/` to .gitignore. */
|
|
250
|
+
gitignoreUpdated?: boolean;
|
|
251
|
+
/**
|
|
252
|
+
* When detection is ambiguous and --language wasn't passed, init
|
|
253
|
+
* exits without writing anything and surfaces this error so the
|
|
254
|
+
* user can re-invoke with --language <list>.
|
|
255
|
+
*/
|
|
256
|
+
ambiguousLanguageError?: {
|
|
257
|
+
detected: readonly string[];
|
|
258
|
+
message: string;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export interface ExperimentalResult {
|
|
263
|
+
type: 'experimental';
|
|
264
|
+
tool: 'sim';
|
|
265
|
+
cwd: string;
|
|
266
|
+
/** Optional `--kind` filter (load / chaos / invariant / fix-evaluation). */
|
|
267
|
+
kind?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Outcome of a `sim --recipe <name>` run. */
|
|
271
|
+
export interface SimDoneResult {
|
|
272
|
+
type: 'sim-done';
|
|
273
|
+
recipeName: string;
|
|
274
|
+
cwd: string;
|
|
275
|
+
totalScenarios: number;
|
|
276
|
+
passedScenarios: number;
|
|
277
|
+
failedScenarios: number;
|
|
278
|
+
scenarios: {
|
|
279
|
+
scenarioId: string;
|
|
280
|
+
scenarioName: string;
|
|
281
|
+
kind: 'load' | 'chaos' | 'invariant' | 'fix-evaluation';
|
|
282
|
+
passed: boolean;
|
|
283
|
+
durationMs: number;
|
|
284
|
+
error?: string;
|
|
285
|
+
}[];
|
|
286
|
+
durationMs: number;
|
|
287
|
+
/** Whether the run should cause a non-zero exit code (any scenario failed). */
|
|
288
|
+
shouldFail?: boolean;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export interface PluginResult {
|
|
292
|
+
type: 'plugin';
|
|
293
|
+
action: 'list' | 'install' | 'remove' | 'sync' | 'add';
|
|
294
|
+
[key: string]: unknown;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface HelpResult {
|
|
298
|
+
type: 'help';
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface ErrorResult {
|
|
302
|
+
type: 'error';
|
|
303
|
+
message: string;
|
|
304
|
+
suggestion?: string;
|
|
305
|
+
exitCode: number;
|
|
306
|
+
}
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
export default defineConfig({
|
|
3
|
+
test: {
|
|
4
|
+
include: ['src/**/*.test.ts'],
|
|
5
|
+
passWithNoTests: true,
|
|
6
|
+
coverage: {
|
|
7
|
+
include: ['src/**'],
|
|
8
|
+
exclude: [
|
|
9
|
+
'src/**/*.test.ts',
|
|
10
|
+
'src/**/__tests__/**',
|
|
11
|
+
'src/**/index.ts',
|
|
12
|
+
'src/types.ts',
|
|
13
|
+
],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|