@gmickel/gno 1.32.0 → 1.34.0
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 +17 -3
- package/assets/skill/SKILL.md +30 -0
- package/assets/skill/cli-reference.md +10 -2
- package/browser-extension/artifacts/{gno-browser-clipper-v1.32.0.zip → gno-browser-clipper-v1.34.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.34.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +5 -1
- package/spec/cli.md +60 -1
- package/spec/mcp.md +21 -0
- package/spec/output-schemas/audit-report.schema.json +284 -0
- package/spec/output-schemas/publish-artifact.schema.json +76 -1
- package/src/cli/commands/audit.ts +231 -0
- package/src/cli/commands/publish.ts +43 -7
- 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/ingestion/strip.ts +152 -26
- 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/publish/artifact-asset-codec.ts +75 -0
- package/src/publish/artifact-asset-contract.ts +152 -0
- package/src/publish/artifact-asset-parse.ts +401 -0
- package/src/publish/artifact-asset-sniff.ts +108 -0
- package/src/publish/artifact-asset-validate.ts +209 -0
- package/src/publish/artifact-assets.ts +58 -0
- package/src/publish/artifact-validation.ts +32 -6
- package/src/publish/artifact.ts +50 -3
- package/src/publish/attachment-bundle.ts +145 -0
- package/src/publish/attachment-discover.ts +203 -0
- package/src/publish/attachment-load.ts +133 -0
- package/src/publish/attachment-obsidian.ts +45 -0
- package/src/publish/attachment-path.ts +334 -0
- package/src/publish/attachment-raster.ts +852 -0
- package/src/publish/attachment-resolver.ts +280 -0
- package/src/publish/attachment-types.ts +54 -0
- package/src/publish/encrypted-export.ts +121 -44
- package/src/publish/export-attachments.ts +224 -0
- package/src/publish/export-service.ts +142 -80
- package/src/publish/obsidian-sanitize.ts +121 -13
- package/src/serve/routes/api.ts +2 -1
- package/src/store/sqlite/adapter.ts +82 -0
- 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.32.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
|
+
};
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @module src/cli/commands/publish
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// node:fs/promises — directory creation has no Bun-native equivalent.
|
|
8
|
+
import { mkdir } from "node:fs/promises";
|
|
8
9
|
import { dirname } from "node:path";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
|
|
@@ -12,10 +13,15 @@ import type {
|
|
|
12
13
|
PublishArtifact,
|
|
13
14
|
PublishVisibility,
|
|
14
15
|
} from "../../publish/artifact";
|
|
16
|
+
import type { PublishAssetEgressSummary } from "../../publish/attachment-resolver";
|
|
15
17
|
import type { SanitizeWarning } from "../../publish/obsidian-sanitize";
|
|
16
18
|
|
|
17
19
|
import { resolveDownloadsDir } from "../../core/user-dirs";
|
|
18
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
derivePublishArtifactFilename,
|
|
22
|
+
serializePublishArtifact,
|
|
23
|
+
slugify,
|
|
24
|
+
} from "../../publish/artifact";
|
|
19
25
|
import { exportPublishArtifact } from "../../publish/export-service";
|
|
20
26
|
import { formatSanitizeWarnings } from "../../publish/obsidian-sanitize";
|
|
21
27
|
import { initStore } from "./shared";
|
|
@@ -37,6 +43,7 @@ export type PublishExportResult =
|
|
|
37
43
|
success: true;
|
|
38
44
|
data: {
|
|
39
45
|
artifact: PublishArtifact;
|
|
46
|
+
assetSummary: PublishAssetEgressSummary;
|
|
40
47
|
outPath: string;
|
|
41
48
|
preview?: string;
|
|
42
49
|
uploadUrl: string;
|
|
@@ -64,6 +71,15 @@ export async function buildDefaultPublishExportPath(
|
|
|
64
71
|
);
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
/** Write exactly the canonical byte sequence used by upload-size accounting. */
|
|
75
|
+
export async function writePublishArtifactFile(
|
|
76
|
+
outPath: string,
|
|
77
|
+
artifact: PublishArtifact
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
80
|
+
await Bun.write(outPath, serializePublishArtifact(artifact));
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
export async function publishExport(
|
|
68
84
|
target: string,
|
|
69
85
|
options: PublishExportOptions
|
|
@@ -79,7 +95,7 @@ export async function publishExport(
|
|
|
79
95
|
const { collections, store } = initResult;
|
|
80
96
|
|
|
81
97
|
try {
|
|
82
|
-
const { artifact, warnings } = await exportPublishArtifact({
|
|
98
|
+
const { artifact, assetSummary, warnings } = await exportPublishArtifact({
|
|
83
99
|
collections,
|
|
84
100
|
options: {
|
|
85
101
|
routeSlug: options.slug,
|
|
@@ -104,6 +120,7 @@ export async function publishExport(
|
|
|
104
120
|
success: true,
|
|
105
121
|
data: {
|
|
106
122
|
artifact,
|
|
123
|
+
assetSummary,
|
|
107
124
|
outPath: "",
|
|
108
125
|
preview,
|
|
109
126
|
uploadUrl: "https://gno.sh/studio",
|
|
@@ -116,13 +133,13 @@ export async function publishExport(
|
|
|
116
133
|
const outPath =
|
|
117
134
|
options.out?.trim() || (await buildDefaultPublishExportPath(artifact));
|
|
118
135
|
|
|
119
|
-
await
|
|
120
|
-
await writeFile(outPath, JSON.stringify(artifact, null, 2));
|
|
136
|
+
await writePublishArtifactFile(outPath, artifact);
|
|
121
137
|
|
|
122
138
|
return {
|
|
123
139
|
success: true,
|
|
124
140
|
data: {
|
|
125
141
|
artifact,
|
|
142
|
+
assetSummary,
|
|
126
143
|
outPath,
|
|
127
144
|
uploadUrl: "https://gno.sh/studio",
|
|
128
145
|
warnings,
|
|
@@ -159,19 +176,37 @@ export function formatPublishExport(
|
|
|
159
176
|
return JSON.stringify(result.data, null, 2);
|
|
160
177
|
}
|
|
161
178
|
|
|
162
|
-
const {
|
|
163
|
-
|
|
179
|
+
const {
|
|
180
|
+
artifact,
|
|
181
|
+
assetSummary,
|
|
182
|
+
outPath,
|
|
183
|
+
preview,
|
|
184
|
+
uploadUrl,
|
|
185
|
+
warningsDisplay,
|
|
186
|
+
} = result.data;
|
|
164
187
|
const space = artifact.spaces[0];
|
|
165
188
|
const warningsSection =
|
|
166
189
|
warningsDisplay.length > 0
|
|
167
190
|
? ["", "Preprocessor notes:", ...warningsDisplay]
|
|
168
191
|
: [];
|
|
192
|
+
const assetSection = [
|
|
193
|
+
"",
|
|
194
|
+
"Asset summary:",
|
|
195
|
+
` assets=${assetSummary.assetCount} refs=${assetSummary.referenceCount} external=${assetSummary.externalCount}`,
|
|
196
|
+
` rawBytes=${assetSummary.rawBytes} encodedBytes=${assetSummary.encodedBytes} finalBytes=${assetSummary.finalUploadBytes}`,
|
|
197
|
+
` dedupSavedBytes=${assetSummary.dedupSavedBytes} unresolved=${assetSummary.diagnostics.length}`,
|
|
198
|
+
...assetSummary.diagnostics.map(
|
|
199
|
+
(diagnostic) =>
|
|
200
|
+
` [${diagnostic.code}] ${diagnostic.noteSlug}: ${diagnostic.sourceRef} — ${diagnostic.message}`
|
|
201
|
+
),
|
|
202
|
+
];
|
|
169
203
|
|
|
170
204
|
if (preview !== undefined) {
|
|
171
205
|
return [
|
|
172
206
|
`Preview (no file written) — ${space?.sourceType ?? "artifact"}`,
|
|
173
207
|
`Route slug: ${space?.routeSlug ?? slugify(artifact.source)}`,
|
|
174
208
|
`Visibility: ${space?.visibility ?? "public"}`,
|
|
209
|
+
...assetSection,
|
|
175
210
|
...warningsSection,
|
|
176
211
|
"",
|
|
177
212
|
"─── sanitized markdown ───",
|
|
@@ -185,6 +220,7 @@ export function formatPublishExport(
|
|
|
185
220
|
`Visibility: ${space?.visibility ?? "public"}`,
|
|
186
221
|
`Filename: ${derivePublishArtifactFilename(artifact)}`,
|
|
187
222
|
`Next: open ${uploadUrl} and drop ${outPath} into the upload zone.`,
|
|
223
|
+
...assetSection,
|
|
188
224
|
...warningsSection,
|
|
189
225
|
].join("\n");
|
|
190
226
|
}
|
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")
|