@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.
@@ -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
+ }