@gmickel/gno 1.33.0 → 1.34.1
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 +7 -2
- package/assets/skill/SKILL.md +19 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.33.0.zip → gno-browser-clipper-v1.34.1.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.1.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +46 -0
- package/spec/mcp.md +21 -0
- package/spec/output-schemas/audit-report.schema.json +284 -0
- package/src/cli/commands/audit.ts +231 -0
- package/src/cli/commands/models/pull.ts +32 -3
- package/src/cli/errors.ts +9 -2
- package/src/cli/program.ts +112 -0
- package/src/core/audit-contract.ts +296 -0
- package/src/core/audit-freshness.ts +233 -0
- package/src/core/audit-links.ts +222 -0
- package/src/core/audit-provenance.ts +154 -0
- package/src/core/audit-report.ts +318 -0
- package/src/core/audit-workspace.ts +678 -0
- package/src/core/audit.ts +569 -0
- package/src/core/capture.ts +196 -3
- package/src/core/document-capabilities.ts +9 -8
- package/src/core/record-metadata.ts +33 -0
- package/src/mcp/http-egress.ts +8 -0
- package/src/mcp/tools/audit.ts +97 -0
- package/src/mcp/tools/index.ts +13 -0
- package/src/store/sqlite/adapter.ts +138 -91
- package/src/store/sqlite/graph-link-bulk-resolver.ts +191 -0
- package/src/store/sqlite/graph-link-resolver.ts +241 -2
- package/browser-extension/artifacts/gno-browser-clipper-v1.33.0.zip.sha256 +0 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/** Read-only knowledge-integrity audit CLI adapter. */
|
|
2
|
+
|
|
3
|
+
// node:fs/promises provides atomic filesystem structure operations with no Bun equivalent.
|
|
4
|
+
import { chmod, mkdtemp, rename, rmdir, unlink } from "node:fs/promises";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
AuditCategory,
|
|
9
|
+
AuditReport,
|
|
10
|
+
AuditRunResult,
|
|
11
|
+
} from "../../core/audit";
|
|
12
|
+
import type { WorkspaceAuditProgress } from "../../core/audit-workspace";
|
|
13
|
+
|
|
14
|
+
import { getIndexDbPath } from "../../app/constants";
|
|
15
|
+
import { loadConfig } from "../../config";
|
|
16
|
+
import {
|
|
17
|
+
auditExitCode,
|
|
18
|
+
AUDIT_CATEGORIES,
|
|
19
|
+
serializeAuditReportCanonical,
|
|
20
|
+
} from "../../core/audit";
|
|
21
|
+
import { runWorkspaceAudit } from "../../core/audit-workspace";
|
|
22
|
+
import { normalizeTag, validateTag } from "../../core/tags";
|
|
23
|
+
import { normalizeCollectionName } from "../../core/validation";
|
|
24
|
+
import { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
25
|
+
|
|
26
|
+
export interface AuditCommandOptions {
|
|
27
|
+
category?: string;
|
|
28
|
+
configPath?: string;
|
|
29
|
+
indexName?: string;
|
|
30
|
+
collections?: string[];
|
|
31
|
+
paths?: string[];
|
|
32
|
+
tags?: string[];
|
|
33
|
+
maxFindings?: number;
|
|
34
|
+
maxAgeDays?: number;
|
|
35
|
+
orphanRoots?: string[];
|
|
36
|
+
orphanIgnorePrefixes?: string[];
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
onProgress?: (progress: WorkspaceAuditProgress) => void | Promise<void>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type AuditCommandResult =
|
|
42
|
+
| { success: true; report: AuditReport; exitCode: number }
|
|
43
|
+
| { success: false; error: string; invalid: boolean };
|
|
44
|
+
|
|
45
|
+
const resolveCategories = (
|
|
46
|
+
value: string | undefined
|
|
47
|
+
): AuditCategory[] | null => {
|
|
48
|
+
const normalized = (value ?? "all").trim().toLowerCase();
|
|
49
|
+
if (normalized === "all") return [...AUDIT_CATEGORIES];
|
|
50
|
+
return (AUDIT_CATEGORIES as readonly string[]).includes(normalized)
|
|
51
|
+
? [normalized as AuditCategory]
|
|
52
|
+
: null;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const invalidPositiveInteger = (value: number | undefined): boolean =>
|
|
56
|
+
value !== undefined && (!Number.isSafeInteger(value) || value < 1);
|
|
57
|
+
|
|
58
|
+
const AUDIT_SCOPE_FILTER_MAX_ITEMS = 256;
|
|
59
|
+
|
|
60
|
+
const oversizedScopeFilter = (
|
|
61
|
+
options: AuditCommandOptions
|
|
62
|
+
): "collections" | "paths" | "tags" | null => {
|
|
63
|
+
for (const name of ["collections", "paths", "tags"] as const) {
|
|
64
|
+
if ((options[name]?.length ?? 0) > AUDIT_SCOPE_FILTER_MAX_ITEMS) {
|
|
65
|
+
return name;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const audit = async (
|
|
72
|
+
options: AuditCommandOptions
|
|
73
|
+
): Promise<AuditCommandResult> => {
|
|
74
|
+
const categories = resolveCategories(options.category);
|
|
75
|
+
if (!categories) {
|
|
76
|
+
return {
|
|
77
|
+
success: false,
|
|
78
|
+
invalid: true,
|
|
79
|
+
error: "category must be links, provenance, freshness, or all",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (invalidPositiveInteger(options.maxFindings)) {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
invalid: true,
|
|
86
|
+
error: "maxFindings must be a positive integer",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (invalidPositiveInteger(options.maxAgeDays)) {
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
invalid: true,
|
|
93
|
+
error: "maxAgeDays must be a positive integer",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const oversizedFilter = oversizedScopeFilter(options);
|
|
97
|
+
if (oversizedFilter) {
|
|
98
|
+
return {
|
|
99
|
+
success: false,
|
|
100
|
+
invalid: true,
|
|
101
|
+
error: `${oversizedFilter} must contain at most ${AUDIT_SCOPE_FILTER_MAX_ITEMS} values`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const configResult = await loadConfig(options.configPath);
|
|
105
|
+
if (!configResult.ok) {
|
|
106
|
+
return { success: false, invalid: true, error: configResult.error.message };
|
|
107
|
+
}
|
|
108
|
+
const config = configResult.value;
|
|
109
|
+
const requestedCollections = (options.collections ?? []).map(
|
|
110
|
+
normalizeCollectionName
|
|
111
|
+
);
|
|
112
|
+
const requestedTags = (options.tags ?? []).map(normalizeTag);
|
|
113
|
+
const invalidTag = requestedTags.find((tag) => !validateTag(tag));
|
|
114
|
+
if (invalidTag) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
invalid: true,
|
|
118
|
+
error: `Invalid tag: "${invalidTag}"`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const knownCollections = new Set(
|
|
122
|
+
config.collections.map((collection) => collection.name)
|
|
123
|
+
);
|
|
124
|
+
const missingCollection = requestedCollections.find(
|
|
125
|
+
(collection) => !knownCollections.has(collection)
|
|
126
|
+
);
|
|
127
|
+
if (missingCollection) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
invalid: true,
|
|
131
|
+
error: `Collection not found: ${missingCollection}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const dbPath = getIndexDbPath(options.indexName);
|
|
135
|
+
if (!(await Bun.file(dbPath).exists())) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
invalid: false,
|
|
139
|
+
error: `Index database not found: ${dbPath}. Run gno index first.`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const store = new SqliteAdapter();
|
|
143
|
+
const opened = store.openReadOnly(dbPath);
|
|
144
|
+
if (!opened.ok) {
|
|
145
|
+
return { success: false, invalid: false, error: opened.error.message };
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const result: AuditRunResult = await runWorkspaceAudit({
|
|
149
|
+
store,
|
|
150
|
+
config,
|
|
151
|
+
collections: config.collections,
|
|
152
|
+
indexName: options.indexName ?? "default",
|
|
153
|
+
categories,
|
|
154
|
+
collectionFilters: requestedCollections,
|
|
155
|
+
pathFilters: options.paths,
|
|
156
|
+
tagFilters: requestedTags,
|
|
157
|
+
maxFindings: options.maxFindings,
|
|
158
|
+
agePolicy:
|
|
159
|
+
options.maxAgeDays === undefined
|
|
160
|
+
? undefined
|
|
161
|
+
: { maxAgeDays: options.maxAgeDays },
|
|
162
|
+
orphanRoots: options.orphanRoots,
|
|
163
|
+
orphanIgnorePrefixes: options.orphanIgnorePrefixes,
|
|
164
|
+
signal: options.signal,
|
|
165
|
+
onProgress: options.onProgress,
|
|
166
|
+
});
|
|
167
|
+
if (!result.ok) {
|
|
168
|
+
return { success: false, invalid: true, error: result.error };
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
success: true,
|
|
172
|
+
report: result.report,
|
|
173
|
+
exitCode: auditExitCode(result.exit),
|
|
174
|
+
};
|
|
175
|
+
} finally {
|
|
176
|
+
await store.close();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const formatAuditReport = (
|
|
181
|
+
report: AuditReport,
|
|
182
|
+
options: { json?: boolean } = {}
|
|
183
|
+
): string => {
|
|
184
|
+
if (options.json) return serializeAuditReportCanonical(report);
|
|
185
|
+
const lines = [
|
|
186
|
+
`Audit: ${report.status}`,
|
|
187
|
+
`Categories: ${report.scope.categories.join(", ")}`,
|
|
188
|
+
`Rules: ${report.counts.rules.total} (${report.counts.rules.fail} failed, ${report.counts.rules.unavailable} unavailable, ${report.counts.rules.inconclusive} inconclusive)`,
|
|
189
|
+
`Findings: ${report.counts.findings.total}${report.counts.findings.truncated ? ` (${report.counts.findings.returned} shown)` : ""}`,
|
|
190
|
+
`Examined: ${report.counts.examined.documents} document/rule observations`,
|
|
191
|
+
`Duration: ${report.durationMs}ms`,
|
|
192
|
+
];
|
|
193
|
+
for (const finding of report.findings) {
|
|
194
|
+
const location = finding.location ? ` ${finding.location}` : "";
|
|
195
|
+
lines.push(
|
|
196
|
+
`- [${finding.severity}] ${finding.ruleId}: ${finding.subject}${location} — ${finding.message}`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
for (const rule of report.rules) {
|
|
200
|
+
if (
|
|
201
|
+
rule.status === "skip" ||
|
|
202
|
+
rule.status === "unavailable" ||
|
|
203
|
+
rule.status === "inconclusive"
|
|
204
|
+
) {
|
|
205
|
+
lines.push(`- [${rule.status}] ${rule.ruleId}: ${rule.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return lines.join("\n");
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const writeAuditReport = async (
|
|
212
|
+
path: string,
|
|
213
|
+
report: AuditReport,
|
|
214
|
+
options: { json?: boolean } = {}
|
|
215
|
+
): Promise<void> => {
|
|
216
|
+
const temporaryDirectory = await mkdtemp(
|
|
217
|
+
join(dirname(path), `.${basename(path)}-`)
|
|
218
|
+
);
|
|
219
|
+
const temporaryPath = join(temporaryDirectory, "report");
|
|
220
|
+
try {
|
|
221
|
+
await Bun.write(temporaryPath, `${formatAuditReport(report, options)}\n`, {
|
|
222
|
+
createPath: false,
|
|
223
|
+
mode: 0o600,
|
|
224
|
+
});
|
|
225
|
+
await chmod(temporaryPath, 0o600);
|
|
226
|
+
await rename(temporaryPath, path);
|
|
227
|
+
} finally {
|
|
228
|
+
await unlink(temporaryPath).catch(() => undefined);
|
|
229
|
+
await rmdir(temporaryDirectory).catch(() => undefined);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
@@ -10,6 +10,7 @@ import type { DownloadProgress, ModelType } from "../../../llm/types";
|
|
|
10
10
|
import { getModelsCachePath } from "../../../app/constants";
|
|
11
11
|
import { loadConfig } from "../../../config";
|
|
12
12
|
import { ModelCache } from "../../../llm/cache";
|
|
13
|
+
import { isHttpRerankUri } from "../../../llm/httpRerank";
|
|
13
14
|
import { getActivePreset } from "../../../llm/registry";
|
|
14
15
|
|
|
15
16
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -46,6 +47,7 @@ export interface ModelPullResult {
|
|
|
46
47
|
error?: string;
|
|
47
48
|
path?: string;
|
|
48
49
|
skipped?: boolean;
|
|
50
|
+
skipReason?: "cached" | "external";
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface ModelsPullResult {
|
|
@@ -117,6 +119,20 @@ export async function modelsPull(
|
|
|
117
119
|
const uri =
|
|
118
120
|
type === "expand" ? (preset.expand ?? preset.gen) : preset[type];
|
|
119
121
|
|
|
122
|
+
// HTTP rerankers are services, not model artifacts. They are loaded
|
|
123
|
+
// directly by LlmAdapter and must never enter the local model cache path.
|
|
124
|
+
if (type === "rerank" && isHttpRerankUri(uri)) {
|
|
125
|
+
results.push({
|
|
126
|
+
type,
|
|
127
|
+
uri,
|
|
128
|
+
ok: true,
|
|
129
|
+
skipped: true,
|
|
130
|
+
skipReason: "external",
|
|
131
|
+
});
|
|
132
|
+
skipped += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
120
136
|
// Check if already cached (skip unless --force)
|
|
121
137
|
if (!options.force) {
|
|
122
138
|
const isCached = await cache.isCached(uri);
|
|
@@ -128,6 +144,7 @@ export async function modelsPull(
|
|
|
128
144
|
ok: true,
|
|
129
145
|
path: path ?? undefined,
|
|
130
146
|
skipped: true,
|
|
147
|
+
skipReason: "cached",
|
|
131
148
|
});
|
|
132
149
|
skipped += 1;
|
|
133
150
|
continue;
|
|
@@ -176,13 +193,17 @@ export async function modelsPull(
|
|
|
176
193
|
*/
|
|
177
194
|
export function formatModelsPull(result: ModelsPullResult): string {
|
|
178
195
|
const lines: string[] = [];
|
|
196
|
+
let externalSkipped = 0;
|
|
179
197
|
const label = (type: ModelType) =>
|
|
180
198
|
type === "gen" ? "answer" : type === "expand" ? "expand" : type;
|
|
181
199
|
|
|
182
200
|
for (const r of result.results) {
|
|
183
201
|
if (r.ok) {
|
|
184
202
|
if (r.skipped) {
|
|
185
|
-
|
|
203
|
+
if (r.skipReason === "external") externalSkipped += 1;
|
|
204
|
+
const reason =
|
|
205
|
+
r.skipReason === "external" ? "external endpoint" : "already cached";
|
|
206
|
+
lines.push(`${label(r.type)}: skipped (${reason})`);
|
|
186
207
|
} else {
|
|
187
208
|
lines.push(`${label(r.type)}: downloaded`);
|
|
188
209
|
}
|
|
@@ -196,10 +217,18 @@ export function formatModelsPull(result: ModelsPullResult): string {
|
|
|
196
217
|
lines.push(`${result.failed} model(s) failed to download.`);
|
|
197
218
|
} else if (result.skipped === result.results.length) {
|
|
198
219
|
lines.push("");
|
|
199
|
-
lines.push(
|
|
220
|
+
lines.push(
|
|
221
|
+
externalSkipped > 0
|
|
222
|
+
? "No model downloads needed."
|
|
223
|
+
: "All models already cached. Use --force to re-download."
|
|
224
|
+
);
|
|
200
225
|
} else {
|
|
201
226
|
lines.push("");
|
|
202
|
-
lines.push(
|
|
227
|
+
lines.push(
|
|
228
|
+
externalSkipped > 0
|
|
229
|
+
? "All downloadable models downloaded successfully."
|
|
230
|
+
: "All models downloaded successfully."
|
|
231
|
+
);
|
|
203
232
|
}
|
|
204
233
|
|
|
205
234
|
return lines.join("\n");
|
package/src/cli/errors.ts
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
// Error Types
|
|
10
10
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
-
export type CliErrorCode =
|
|
12
|
+
export type CliErrorCode =
|
|
13
|
+
| "VALIDATION"
|
|
14
|
+
| "RUNTIME"
|
|
15
|
+
| "NOT_RUNNING"
|
|
16
|
+
| "AUDIT_FINDINGS"
|
|
17
|
+
| "AUDIT_PARTIAL";
|
|
13
18
|
|
|
14
19
|
export interface CliErrorOptions {
|
|
15
20
|
details?: Record<string, unknown>;
|
|
@@ -62,13 +67,15 @@ export class CliError extends Error {
|
|
|
62
67
|
// Exit Codes
|
|
63
68
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
69
|
|
|
65
|
-
export function exitCodeFor(err: CliError): 1 | 2 | 3 {
|
|
70
|
+
export function exitCodeFor(err: CliError): 1 | 2 | 3 | 4 | 5 {
|
|
66
71
|
if (err.code === "VALIDATION") {
|
|
67
72
|
return 1;
|
|
68
73
|
}
|
|
69
74
|
if (err.code === "NOT_RUNNING") {
|
|
70
75
|
return 3;
|
|
71
76
|
}
|
|
77
|
+
if (err.code === "AUDIT_FINDINGS") return 4;
|
|
78
|
+
if (err.code === "AUDIT_PARTIAL") return 5;
|
|
72
79
|
return 2;
|
|
73
80
|
}
|
|
74
81
|
|
package/src/cli/program.ts
CHANGED
|
@@ -1539,6 +1539,118 @@ function wireOnboardingCommands(program: Command): void {
|
|
|
1539
1539
|
}
|
|
1540
1540
|
);
|
|
1541
1541
|
|
|
1542
|
+
// audit - Read-only workspace integrity report
|
|
1543
|
+
program
|
|
1544
|
+
.command("audit [category]")
|
|
1545
|
+
.description("Run read-only workspace integrity audits")
|
|
1546
|
+
.option(
|
|
1547
|
+
"-c, --collection <name>",
|
|
1548
|
+
"collection scope (repeatable)",
|
|
1549
|
+
collectRepeatableValue,
|
|
1550
|
+
[]
|
|
1551
|
+
)
|
|
1552
|
+
.option(
|
|
1553
|
+
"--path <prefix>",
|
|
1554
|
+
"collection-relative path prefix (repeatable)",
|
|
1555
|
+
collectRepeatableValue,
|
|
1556
|
+
[]
|
|
1557
|
+
)
|
|
1558
|
+
.option(
|
|
1559
|
+
"--tag <tag>",
|
|
1560
|
+
"require tag (repeatable, AND semantics)",
|
|
1561
|
+
collectRepeatableValue,
|
|
1562
|
+
[]
|
|
1563
|
+
)
|
|
1564
|
+
.option("--max-findings <count>", "maximum returned findings", Number)
|
|
1565
|
+
.option("--max-age-days <days>", "explicit age review signal", Number)
|
|
1566
|
+
.option(
|
|
1567
|
+
"--orphan-root <uri>",
|
|
1568
|
+
"URI excluded from orphan findings (repeatable)",
|
|
1569
|
+
collectRepeatableValue,
|
|
1570
|
+
[]
|
|
1571
|
+
)
|
|
1572
|
+
.option(
|
|
1573
|
+
"--orphan-ignore-prefix <prefix>",
|
|
1574
|
+
"path prefix excluded from orphan findings (repeatable)",
|
|
1575
|
+
collectRepeatableValue,
|
|
1576
|
+
[]
|
|
1577
|
+
)
|
|
1578
|
+
.option("--output <path>", "also write the rendered report to a file")
|
|
1579
|
+
.option("--no-progress", "disable progress on stderr")
|
|
1580
|
+
.option("--json", "JSON output")
|
|
1581
|
+
.action(
|
|
1582
|
+
async (
|
|
1583
|
+
category: string | undefined,
|
|
1584
|
+
cmdOpts: Record<string, unknown>
|
|
1585
|
+
) => {
|
|
1586
|
+
const { audit, formatAuditReport, writeAuditReport } =
|
|
1587
|
+
await import("./commands/audit");
|
|
1588
|
+
const globals = getGlobals();
|
|
1589
|
+
const json = getFormat(cmdOpts) === "json";
|
|
1590
|
+
const showProgress =
|
|
1591
|
+
cmdOpts.progress !== false &&
|
|
1592
|
+
!globals.quiet &&
|
|
1593
|
+
!json &&
|
|
1594
|
+
process.stderr.isTTY;
|
|
1595
|
+
const controller = new AbortController();
|
|
1596
|
+
const abort = (): void => controller.abort();
|
|
1597
|
+
process.once("SIGINT", abort);
|
|
1598
|
+
try {
|
|
1599
|
+
const result = await audit({
|
|
1600
|
+
category,
|
|
1601
|
+
configPath: globals.config,
|
|
1602
|
+
indexName: globals.index,
|
|
1603
|
+
collections: cmdOpts.collection as string[],
|
|
1604
|
+
paths: cmdOpts.path as string[],
|
|
1605
|
+
tags: cmdOpts.tag as string[],
|
|
1606
|
+
maxFindings: cmdOpts.maxFindings as number | undefined,
|
|
1607
|
+
maxAgeDays: cmdOpts.maxAgeDays as number | undefined,
|
|
1608
|
+
orphanRoots: cmdOpts.orphanRoot as string[],
|
|
1609
|
+
orphanIgnorePrefixes: cmdOpts.orphanIgnorePrefix as string[],
|
|
1610
|
+
signal: controller.signal,
|
|
1611
|
+
onProgress: showProgress
|
|
1612
|
+
? ({ phase, completed, total }) => {
|
|
1613
|
+
process.stderr.write(
|
|
1614
|
+
`\rAudit ${phase}: ${completed}/${Math.max(total, 1)}`
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
: undefined,
|
|
1618
|
+
});
|
|
1619
|
+
if (!result.success) {
|
|
1620
|
+
throw new CliError(
|
|
1621
|
+
result.invalid ? "VALIDATION" : "RUNTIME",
|
|
1622
|
+
result.error
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
const rendered = formatAuditReport(result.report, { json });
|
|
1626
|
+
process.stdout.write(`${rendered}\n`);
|
|
1627
|
+
if (cmdOpts.output) {
|
|
1628
|
+
await writeAuditReport(cmdOpts.output as string, result.report, {
|
|
1629
|
+
json,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
if (result.exitCode === 2) {
|
|
1633
|
+
throw new CliError("RUNTIME", "Audit runtime failure", {
|
|
1634
|
+
silent: true,
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
if (result.exitCode === 4) {
|
|
1638
|
+
throw new CliError("AUDIT_FINDINGS", "Audit findings present", {
|
|
1639
|
+
silent: true,
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
if (result.exitCode === 5) {
|
|
1643
|
+
throw new CliError("AUDIT_PARTIAL", "Audit evidence is partial", {
|
|
1644
|
+
silent: true,
|
|
1645
|
+
});
|
|
1646
|
+
}
|
|
1647
|
+
} finally {
|
|
1648
|
+
if (showProgress) process.stderr.write("\n");
|
|
1649
|
+
process.removeListener("SIGINT", abort);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
);
|
|
1653
|
+
|
|
1542
1654
|
// status - Show index status
|
|
1543
1655
|
program
|
|
1544
1656
|
.command("status")
|