@mcptoolshop/mcpt-publishing 1.0.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/LICENSE +21 -0
- package/README.md +223 -0
- package/bin/mcpt-publishing.mjs +3 -0
- package/docs/CONTRACT.md +109 -0
- package/logo.png +0 -0
- package/package.json +50 -0
- package/profiles/example.json +23 -0
- package/profiles/manifest.json +49 -0
- package/schemas/audit-receipt.schema.json +52 -0
- package/schemas/profile.schema.json +71 -0
- package/schemas/publishing-config.schema.json +50 -0
- package/schemas/receipt.schema.json +91 -0
- package/scripts/lib/github-glue.mjs +101 -0
- package/scripts/lib/provider.mjs +63 -0
- package/scripts/lib/providers/ghcr.mjs +112 -0
- package/scripts/lib/providers/github.mjs +52 -0
- package/scripts/lib/providers/npm.mjs +100 -0
- package/scripts/lib/providers/nuget.mjs +115 -0
- package/scripts/lib/providers/pypi.mjs +102 -0
- package/scripts/lib/receipt-writer.mjs +123 -0
- package/scripts/lib/registry.mjs +65 -0
- package/src/cli/exit-codes.mjs +15 -0
- package/src/cli/help.mjs +34 -0
- package/src/cli/router.mjs +120 -0
- package/src/commands/audit.mjs +238 -0
- package/src/commands/init.mjs +107 -0
- package/src/commands/plan.mjs +36 -0
- package/src/commands/providers.mjs +81 -0
- package/src/commands/publish.mjs +37 -0
- package/src/config/defaults.mjs +14 -0
- package/src/config/loader.mjs +86 -0
- package/src/config/schema.mjs +70 -0
- package/src/receipts/audit-receipt.mjs +53 -0
- package/src/receipts/index-writer.mjs +65 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mcpt-publishing init` — scaffold publishing config and starter directories.
|
|
3
|
+
*
|
|
4
|
+
* Creates:
|
|
5
|
+
* - publishing.config.json (with $schema pointer)
|
|
6
|
+
* - profiles/manifest.json (empty skeleton)
|
|
7
|
+
* - receipts/ (empty dir)
|
|
8
|
+
* - reports/ (empty dir)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { EXIT } from "../cli/exit-codes.mjs";
|
|
14
|
+
|
|
15
|
+
export const helpText = `
|
|
16
|
+
mcpt-publishing init — Scaffold publishing config in current directory.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
mcpt-publishing init [flags]
|
|
20
|
+
|
|
21
|
+
Flags:
|
|
22
|
+
--force Overwrite existing publishing.config.json
|
|
23
|
+
--json Output result as JSON
|
|
24
|
+
--help Show this help
|
|
25
|
+
|
|
26
|
+
Creates:
|
|
27
|
+
publishing.config.json Configuration file with schema pointer
|
|
28
|
+
profiles/manifest.json Empty package inventory
|
|
29
|
+
receipts/ Receipt output directory
|
|
30
|
+
reports/ Report output directory
|
|
31
|
+
`.trim();
|
|
32
|
+
|
|
33
|
+
const STARTER_CONFIG = {
|
|
34
|
+
$schema: "https://github.com/mcp-tool-shop/mcpt-publishing/schemas/publishing-config.schema.json",
|
|
35
|
+
profilesDir: "profiles",
|
|
36
|
+
receiptsDir: "receipts",
|
|
37
|
+
reportsDir: "reports",
|
|
38
|
+
github: {
|
|
39
|
+
updateIssue: true,
|
|
40
|
+
attachReceipts: true,
|
|
41
|
+
},
|
|
42
|
+
enabledProviders: [],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const STARTER_MANIFEST = {
|
|
46
|
+
$comment: "Machine-readable inventory of all published packages. Source of truth for audit.",
|
|
47
|
+
npm: [],
|
|
48
|
+
nuget: [],
|
|
49
|
+
pypi: [],
|
|
50
|
+
ghcr: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Execute the init command.
|
|
55
|
+
* @param {object} flags - Parsed CLI flags
|
|
56
|
+
* @returns {number} Exit code
|
|
57
|
+
*/
|
|
58
|
+
export async function execute(flags) {
|
|
59
|
+
const cwd = process.cwd();
|
|
60
|
+
const configPath = join(cwd, "publishing.config.json");
|
|
61
|
+
const created = [];
|
|
62
|
+
|
|
63
|
+
// Config file
|
|
64
|
+
if (existsSync(configPath) && !flags.force) {
|
|
65
|
+
if (flags.json) {
|
|
66
|
+
process.stdout.write(JSON.stringify({ error: "publishing.config.json already exists. Use --force to overwrite." }) + "\n");
|
|
67
|
+
} else {
|
|
68
|
+
process.stderr.write(`publishing.config.json already exists. Use --force to overwrite.\n`);
|
|
69
|
+
}
|
|
70
|
+
return EXIT.CONFIG_ERROR;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
writeFileSync(configPath, JSON.stringify(STARTER_CONFIG, null, 2) + "\n");
|
|
74
|
+
created.push("publishing.config.json");
|
|
75
|
+
|
|
76
|
+
// Profiles directory + manifest
|
|
77
|
+
const profilesDir = join(cwd, "profiles");
|
|
78
|
+
mkdirSync(profilesDir, { recursive: true });
|
|
79
|
+
const manifestPath = join(profilesDir, "manifest.json");
|
|
80
|
+
if (!existsSync(manifestPath) || flags.force) {
|
|
81
|
+
writeFileSync(manifestPath, JSON.stringify(STARTER_MANIFEST, null, 2) + "\n");
|
|
82
|
+
created.push("profiles/manifest.json");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Receipts directory
|
|
86
|
+
const receiptsDir = join(cwd, "receipts");
|
|
87
|
+
mkdirSync(receiptsDir, { recursive: true });
|
|
88
|
+
created.push("receipts/");
|
|
89
|
+
|
|
90
|
+
// Reports directory
|
|
91
|
+
const reportsDir = join(cwd, "reports");
|
|
92
|
+
mkdirSync(reportsDir, { recursive: true });
|
|
93
|
+
created.push("reports/");
|
|
94
|
+
|
|
95
|
+
if (flags.json) {
|
|
96
|
+
process.stdout.write(JSON.stringify({ created, path: cwd }) + "\n");
|
|
97
|
+
} else {
|
|
98
|
+
process.stderr.write(`Initialized mcpt-publishing in ${cwd}\n`);
|
|
99
|
+
for (const f of created) {
|
|
100
|
+
process.stderr.write(` + ${f}\n`);
|
|
101
|
+
}
|
|
102
|
+
process.stderr.write(`\nNext: edit profiles/manifest.json to add your packages, then run:\n`);
|
|
103
|
+
process.stderr.write(` mcpt-publishing audit\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return EXIT.SUCCESS;
|
|
107
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mcpt-publishing plan` — dry-run publish plan (stub).
|
|
3
|
+
*
|
|
4
|
+
* Future: compares manifest → registries → generates a publish plan
|
|
5
|
+
* showing what would be published, skipped, or blocked.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EXIT } from "../cli/exit-codes.mjs";
|
|
9
|
+
|
|
10
|
+
export const helpText = `
|
|
11
|
+
mcpt-publishing plan — Generate a dry-run publish plan.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
mcpt-publishing plan [flags]
|
|
15
|
+
|
|
16
|
+
Flags:
|
|
17
|
+
--json Output as JSON
|
|
18
|
+
--help Show this help
|
|
19
|
+
|
|
20
|
+
Status: Not yet implemented. Coming in a future release.
|
|
21
|
+
`.trim();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute the plan command (stub).
|
|
25
|
+
* @param {object} flags - Parsed CLI flags
|
|
26
|
+
* @returns {number} Exit code
|
|
27
|
+
*/
|
|
28
|
+
export async function execute(flags) {
|
|
29
|
+
if (flags.json) {
|
|
30
|
+
process.stdout.write(JSON.stringify({ status: "not_implemented", message: "Plan command is not yet implemented." }) + "\n");
|
|
31
|
+
} else {
|
|
32
|
+
process.stderr.write("Plan command is not yet implemented.\n");
|
|
33
|
+
process.stderr.write("This will generate a dry-run publish plan in a future release.\n");
|
|
34
|
+
}
|
|
35
|
+
return EXIT.SUCCESS;
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mcpt-publishing providers` — list registered providers and their status.
|
|
3
|
+
*
|
|
4
|
+
* Loads providers from scripts/lib/registry.mjs, optionally filters by
|
|
5
|
+
* enabledProviders config, and prints a summary table.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import { loadConfig } from "../config/loader.mjs";
|
|
11
|
+
import { EXIT } from "../cli/exit-codes.mjs";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
export const helpText = `
|
|
16
|
+
mcpt-publishing providers — List registered providers.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
mcpt-publishing providers [flags]
|
|
20
|
+
|
|
21
|
+
Flags:
|
|
22
|
+
--json Output as JSON array
|
|
23
|
+
--config Explicit path to publishing.config.json
|
|
24
|
+
--help Show this help
|
|
25
|
+
|
|
26
|
+
Output:
|
|
27
|
+
Shows each provider name, supported ecosystems, and enabled/disabled status.
|
|
28
|
+
`.trim();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute the providers command.
|
|
32
|
+
* @param {object} flags - Parsed CLI flags
|
|
33
|
+
* @returns {number} Exit code
|
|
34
|
+
*/
|
|
35
|
+
export async function execute(flags) {
|
|
36
|
+
const config = flags.config
|
|
37
|
+
? loadConfig(dirname(flags.config))
|
|
38
|
+
: loadConfig();
|
|
39
|
+
|
|
40
|
+
// Import provider registry from scripts/lib
|
|
41
|
+
const registryPath = join(__dirname, "..", "..", "scripts", "lib", "registry.mjs");
|
|
42
|
+
const registryUrl = pathToFileURL(registryPath).href;
|
|
43
|
+
const { loadProviders } = await import(registryUrl);
|
|
44
|
+
|
|
45
|
+
const providers = await loadProviders();
|
|
46
|
+
const enabled = config.enabledProviders ?? [];
|
|
47
|
+
const allEnabled = enabled.length === 0; // empty = all enabled
|
|
48
|
+
|
|
49
|
+
const rows = providers.map(p => {
|
|
50
|
+
const isEnabled = allEnabled || enabled.includes(p.name);
|
|
51
|
+
return {
|
|
52
|
+
name: p.name,
|
|
53
|
+
ecosystem: p.ecosystem ?? p.name,
|
|
54
|
+
enabled: isEnabled,
|
|
55
|
+
status: isEnabled ? "active" : "disabled",
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (flags.json) {
|
|
60
|
+
process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
|
|
61
|
+
return EXIT.SUCCESS;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Human-readable table
|
|
65
|
+
process.stdout.write("\n");
|
|
66
|
+
process.stdout.write(" Provider Ecosystem Status\n");
|
|
67
|
+
process.stdout.write(" ───────── ───────── ──────\n");
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const name = row.name.padEnd(12);
|
|
70
|
+
const eco = row.ecosystem.padEnd(12);
|
|
71
|
+
const status = row.enabled ? "active" : "disabled";
|
|
72
|
+
process.stdout.write(` ${name} ${eco} ${status}\n`);
|
|
73
|
+
}
|
|
74
|
+
process.stdout.write("\n");
|
|
75
|
+
|
|
76
|
+
if (!allEnabled) {
|
|
77
|
+
process.stdout.write(` Filter: enabledProviders = [${enabled.join(", ")}]\n\n`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return EXIT.SUCCESS;
|
|
81
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mcpt-publishing publish` — execute publish and generate receipts (stub).
|
|
3
|
+
*
|
|
4
|
+
* Future: reads a publish plan, executes it against registries,
|
|
5
|
+
* generates immutable publish receipts, and updates the receipts index.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { EXIT } from "../cli/exit-codes.mjs";
|
|
9
|
+
|
|
10
|
+
export const helpText = `
|
|
11
|
+
mcpt-publishing publish — Execute a publish plan and generate receipts.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
mcpt-publishing publish [flags]
|
|
15
|
+
|
|
16
|
+
Flags:
|
|
17
|
+
--json Output as JSON
|
|
18
|
+
--dry-run Show what would be published without executing
|
|
19
|
+
--help Show this help
|
|
20
|
+
|
|
21
|
+
Status: Not yet implemented. Coming in a future release.
|
|
22
|
+
`.trim();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Execute the publish command (stub).
|
|
26
|
+
* @param {object} flags - Parsed CLI flags
|
|
27
|
+
* @returns {number} Exit code
|
|
28
|
+
*/
|
|
29
|
+
export async function execute(flags) {
|
|
30
|
+
if (flags.json) {
|
|
31
|
+
process.stdout.write(JSON.stringify({ status: "not_implemented", message: "Publish command is not yet implemented." }) + "\n");
|
|
32
|
+
} else {
|
|
33
|
+
process.stderr.write("Publish command is not yet implemented.\n");
|
|
34
|
+
process.stderr.write("This will execute publishes and generate receipts in a future release.\n");
|
|
35
|
+
}
|
|
36
|
+
return EXIT.SUCCESS;
|
|
37
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default config values used when no publishing.config.json is found
|
|
3
|
+
* or when fields are omitted from the config file.
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULTS = {
|
|
6
|
+
profilesDir: "profiles",
|
|
7
|
+
receiptsDir: "receipts",
|
|
8
|
+
reportsDir: "reports",
|
|
9
|
+
github: {
|
|
10
|
+
updateIssue: true,
|
|
11
|
+
attachReceipts: true,
|
|
12
|
+
},
|
|
13
|
+
enabledProviders: [], // empty = all providers enabled
|
|
14
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config loader — discovers and parses publishing.config.json.
|
|
3
|
+
*
|
|
4
|
+
* Discovery order:
|
|
5
|
+
* 1. PUBLISHING_CONFIG env var (explicit path)
|
|
6
|
+
* 2. Walk up from startDir looking for publishing.config.json
|
|
7
|
+
* 3. Fall back to defaults rooted at startDir
|
|
8
|
+
*
|
|
9
|
+
* All directory paths (profilesDir, receiptsDir, reportsDir) resolve
|
|
10
|
+
* relative to the config file location, not process.cwd().
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
14
|
+
import { join, dirname, resolve, isAbsolute } from "node:path";
|
|
15
|
+
import { DEFAULTS } from "./defaults.mjs";
|
|
16
|
+
import { validateConfig } from "./schema.mjs";
|
|
17
|
+
|
|
18
|
+
const CONFIG_FILENAME = "publishing.config.json";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Load config from the filesystem.
|
|
22
|
+
* @param {string} [startDir=process.cwd()] - Directory to start searching from
|
|
23
|
+
* @returns {object} Resolved config with absolute paths
|
|
24
|
+
*/
|
|
25
|
+
export function loadConfig(startDir = process.cwd()) {
|
|
26
|
+
// 1. Env override takes priority
|
|
27
|
+
const envPath = process.env.PUBLISHING_CONFIG;
|
|
28
|
+
if (envPath) {
|
|
29
|
+
const resolved = resolve(envPath);
|
|
30
|
+
if (!existsSync(resolved)) {
|
|
31
|
+
throw new Error(`PUBLISHING_CONFIG points to non-existent file: ${resolved}`);
|
|
32
|
+
}
|
|
33
|
+
return parseAndResolve(resolved);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. Walk up from startDir
|
|
37
|
+
let dir = resolve(startDir);
|
|
38
|
+
while (true) {
|
|
39
|
+
const candidate = join(dir, CONFIG_FILENAME);
|
|
40
|
+
if (existsSync(candidate)) {
|
|
41
|
+
return parseAndResolve(candidate);
|
|
42
|
+
}
|
|
43
|
+
const parent = dirname(dir);
|
|
44
|
+
if (parent === dir) break; // filesystem root reached
|
|
45
|
+
dir = parent;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. No config found — return defaults rooted at startDir
|
|
49
|
+
const resolvedStart = resolve(startDir);
|
|
50
|
+
return {
|
|
51
|
+
...DEFAULTS,
|
|
52
|
+
github: { ...DEFAULTS.github },
|
|
53
|
+
profilesDir: join(resolvedStart, DEFAULTS.profilesDir),
|
|
54
|
+
receiptsDir: join(resolvedStart, DEFAULTS.receiptsDir),
|
|
55
|
+
reportsDir: join(resolvedStart, DEFAULTS.reportsDir),
|
|
56
|
+
_configDir: resolvedStart,
|
|
57
|
+
_configFile: null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse a config file and resolve all paths relative to its location.
|
|
63
|
+
* @param {string} filePath - Absolute path to the config file
|
|
64
|
+
* @returns {object}
|
|
65
|
+
*/
|
|
66
|
+
function parseAndResolve(filePath) {
|
|
67
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
68
|
+
validateConfig(raw);
|
|
69
|
+
|
|
70
|
+
const configDir = dirname(filePath);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
...DEFAULTS,
|
|
74
|
+
...raw,
|
|
75
|
+
github: { ...DEFAULTS.github, ...raw.github },
|
|
76
|
+
profilesDir: resolvePath(configDir, raw.profilesDir ?? DEFAULTS.profilesDir),
|
|
77
|
+
receiptsDir: resolvePath(configDir, raw.receiptsDir ?? DEFAULTS.receiptsDir),
|
|
78
|
+
reportsDir: resolvePath(configDir, raw.reportsDir ?? DEFAULTS.reportsDir),
|
|
79
|
+
_configDir: configDir,
|
|
80
|
+
_configFile: filePath,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolvePath(base, p) {
|
|
85
|
+
return isAbsolute(p) ? p : resolve(base, p);
|
|
86
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight config validation (zero dependencies).
|
|
3
|
+
*
|
|
4
|
+
* Validates publishing.config.json structure and types.
|
|
5
|
+
* Throws on invalid input with a clear message.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const KNOWN_KEYS = new Set([
|
|
9
|
+
"$schema", "profilesDir", "receiptsDir", "reportsDir",
|
|
10
|
+
"github", "enabledProviders",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const KNOWN_GITHUB_KEYS = new Set([
|
|
14
|
+
"updateIssue", "attachReceipts",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Validate a parsed config object.
|
|
19
|
+
* @param {object} config
|
|
20
|
+
* @throws {Error} on invalid config
|
|
21
|
+
*/
|
|
22
|
+
export function validateConfig(config) {
|
|
23
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
24
|
+
throw new Error("Config must be a JSON object");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Check for unknown top-level keys
|
|
28
|
+
for (const key of Object.keys(config)) {
|
|
29
|
+
if (!KNOWN_KEYS.has(key)) {
|
|
30
|
+
throw new Error(`Unknown config key: "${key}"`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// String path fields
|
|
35
|
+
for (const field of ["profilesDir", "receiptsDir", "reportsDir"]) {
|
|
36
|
+
if (field in config && typeof config[field] !== "string") {
|
|
37
|
+
throw new Error(`Config "${field}" must be a string`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// github section
|
|
42
|
+
if ("github" in config) {
|
|
43
|
+
if (typeof config.github !== "object" || Array.isArray(config.github)) {
|
|
44
|
+
throw new Error('Config "github" must be an object');
|
|
45
|
+
}
|
|
46
|
+
for (const key of Object.keys(config.github)) {
|
|
47
|
+
if (!KNOWN_GITHUB_KEYS.has(key)) {
|
|
48
|
+
throw new Error(`Unknown config github key: "${key}"`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if ("updateIssue" in config.github && typeof config.github.updateIssue !== "boolean") {
|
|
52
|
+
throw new Error('Config "github.updateIssue" must be a boolean');
|
|
53
|
+
}
|
|
54
|
+
if ("attachReceipts" in config.github && typeof config.github.attachReceipts !== "boolean") {
|
|
55
|
+
throw new Error('Config "github.attachReceipts" must be a boolean');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// enabledProviders
|
|
60
|
+
if ("enabledProviders" in config) {
|
|
61
|
+
if (!Array.isArray(config.enabledProviders)) {
|
|
62
|
+
throw new Error('Config "enabledProviders" must be an array');
|
|
63
|
+
}
|
|
64
|
+
for (const p of config.enabledProviders) {
|
|
65
|
+
if (typeof p !== "string") {
|
|
66
|
+
throw new Error('Config "enabledProviders" entries must be strings');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit receipt builder — emits a receipt after each audit run.
|
|
3
|
+
*
|
|
4
|
+
* Path: receipts/audit/<YYYY-MM-DD>.json
|
|
5
|
+
* Date-keyed: latest run of the day overwrites (not immutable like publish receipts).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { updateReceiptsIndex } from "./index-writer.mjs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build and write an audit receipt from audit results.
|
|
14
|
+
* @param {object} config - Resolved config (needs config.receiptsDir)
|
|
15
|
+
* @param {object} results - Audit results { npm:[], nuget:[], counts:{}, generated }
|
|
16
|
+
* @returns {string} Path to the written receipt file
|
|
17
|
+
*/
|
|
18
|
+
export function emitAuditReceipt(config, results) {
|
|
19
|
+
const auditDir = join(config.receiptsDir, "audit");
|
|
20
|
+
mkdirSync(auditDir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
// Count packages per ecosystem
|
|
23
|
+
const ecosystems = {};
|
|
24
|
+
let totalPackages = 0;
|
|
25
|
+
for (const [key, val] of Object.entries(results)) {
|
|
26
|
+
if (Array.isArray(val)) {
|
|
27
|
+
ecosystems[key] = val.length;
|
|
28
|
+
totalPackages += val.length;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const receipt = {
|
|
33
|
+
schemaVersion: "1.0.0",
|
|
34
|
+
type: "audit",
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
counts: results.counts,
|
|
37
|
+
ecosystems,
|
|
38
|
+
totalPackages,
|
|
39
|
+
reportFiles: {
|
|
40
|
+
json: "reports/latest.json",
|
|
41
|
+
markdown: "reports/latest.md",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const date = receipt.timestamp.slice(0, 10); // YYYY-MM-DD
|
|
46
|
+
const filePath = join(auditDir, `${date}.json`);
|
|
47
|
+
writeFileSync(filePath, JSON.stringify(receipt, null, 2) + "\n");
|
|
48
|
+
|
|
49
|
+
// Update the receipts index
|
|
50
|
+
updateReceiptsIndex(config.receiptsDir, receipt);
|
|
51
|
+
|
|
52
|
+
return filePath;
|
|
53
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receipts index maintainer — keeps receipts/index.json current.
|
|
3
|
+
*
|
|
4
|
+
* The index provides a single-file view of:
|
|
5
|
+
* - Latest audit run (date, counts, totalPackages)
|
|
6
|
+
* - Latest publish per target/package (version, timestamp, commitSha)
|
|
7
|
+
*
|
|
8
|
+
* The Receipt Factory site can consume this file to render dashboards.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { join, dirname } from "node:path";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Update the index with the latest audit receipt data.
|
|
16
|
+
* @param {string} receiptsDir - Absolute path to receipts directory
|
|
17
|
+
* @param {object} auditReceipt - The audit receipt object
|
|
18
|
+
*/
|
|
19
|
+
export function updateReceiptsIndex(receiptsDir, auditReceipt) {
|
|
20
|
+
const index = loadIndex(receiptsDir);
|
|
21
|
+
|
|
22
|
+
index.latestAudit = {
|
|
23
|
+
date: auditReceipt.timestamp,
|
|
24
|
+
counts: auditReceipt.counts,
|
|
25
|
+
totalPackages: auditReceipt.totalPackages,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
saveIndex(receiptsDir, index);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Update the index with a publish receipt entry.
|
|
33
|
+
* @param {string} receiptsDir - Absolute path to receipts directory
|
|
34
|
+
* @param {object} publishReceipt - A publish receipt object
|
|
35
|
+
*/
|
|
36
|
+
export function updatePublishEntry(receiptsDir, publishReceipt) {
|
|
37
|
+
const index = loadIndex(receiptsDir);
|
|
38
|
+
|
|
39
|
+
if (!index.publish) index.publish = {};
|
|
40
|
+
|
|
41
|
+
const key = `${publishReceipt.target}/${publishReceipt.packageName}`;
|
|
42
|
+
index.publish[key] = {
|
|
43
|
+
version: publishReceipt.version,
|
|
44
|
+
timestamp: publishReceipt.timestamp,
|
|
45
|
+
commitSha: publishReceipt.commitSha,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
saveIndex(receiptsDir, index);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
function loadIndex(receiptsDir) {
|
|
54
|
+
const indexPath = join(receiptsDir, "index.json");
|
|
55
|
+
if (existsSync(indexPath)) {
|
|
56
|
+
return JSON.parse(readFileSync(indexPath, "utf8"));
|
|
57
|
+
}
|
|
58
|
+
return { latestAudit: null, publish: {} };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function saveIndex(receiptsDir, index) {
|
|
62
|
+
mkdirSync(receiptsDir, { recursive: true });
|
|
63
|
+
const indexPath = join(receiptsDir, "index.json");
|
|
64
|
+
writeFileSync(indexPath, JSON.stringify(index, null, 2) + "\n");
|
|
65
|
+
}
|