@mcptoolshop/mcpt-publishing 0.2.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/CHANGELOG.md +36 -0
- package/LICENSE +21 -0
- package/README.md +237 -0
- package/bin/mcpt-publishing.mjs +3 -0
- package/docs/CONTRACT.md +109 -0
- package/logo.png +0 -0
- package/package.json +51 -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 +178 -0
- package/scripts/lib/providers/nuget.mjs +198 -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/scripts/lib/shell.mjs +64 -0
- package/src/cli/exit-codes.mjs +17 -0
- package/src/cli/help.mjs +38 -0
- package/src/cli/router.mjs +121 -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 +215 -0
- package/src/commands/verify-receipt.mjs +175 -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,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI router — zero-dependency subcommand parser and dispatcher.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join, dirname } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { GLOBAL_HELP } from "./help.mjs";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
/** Lazy-loaded command map. Keys are subcommand names. */
|
|
13
|
+
const COMMANDS = {
|
|
14
|
+
audit: () => import("../commands/audit.mjs"),
|
|
15
|
+
init: () => import("../commands/init.mjs"),
|
|
16
|
+
plan: () => import("../commands/plan.mjs"),
|
|
17
|
+
publish: () => import("../commands/publish.mjs"),
|
|
18
|
+
providers: () => import("../commands/providers.mjs"),
|
|
19
|
+
"verify-receipt": () => import("../commands/verify-receipt.mjs"),
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Parse CLI flags from an argv slice (after the subcommand).
|
|
24
|
+
*
|
|
25
|
+
* --flag → { flag: true }
|
|
26
|
+
* --key value → { key: "value" }
|
|
27
|
+
* bare → pushed to _positionals
|
|
28
|
+
*
|
|
29
|
+
* @param {string[]} args
|
|
30
|
+
* @returns {object}
|
|
31
|
+
*/
|
|
32
|
+
export function parseFlags(args) {
|
|
33
|
+
const flags = { _positionals: [] };
|
|
34
|
+
for (let i = 0; i < args.length; i++) {
|
|
35
|
+
const arg = args[i];
|
|
36
|
+
if (arg === "--") {
|
|
37
|
+
flags._positionals.push(...args.slice(i + 1));
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
if (arg.startsWith("--")) {
|
|
41
|
+
const key = arg.slice(2);
|
|
42
|
+
const next = args[i + 1];
|
|
43
|
+
if (!next || next.startsWith("--")) {
|
|
44
|
+
flags[key] = true;
|
|
45
|
+
} else {
|
|
46
|
+
flags[key] = next;
|
|
47
|
+
i++;
|
|
48
|
+
}
|
|
49
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
50
|
+
// Short flag aliases
|
|
51
|
+
const SHORT = { h: "help", v: "version", j: "json" };
|
|
52
|
+
const expanded = SHORT[arg[1]];
|
|
53
|
+
if (expanded) flags[expanded] = true;
|
|
54
|
+
else flags._positionals.push(arg);
|
|
55
|
+
} else {
|
|
56
|
+
flags._positionals.push(arg);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return flags;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Main entry point — parse argv and dispatch to the matching command.
|
|
64
|
+
* @param {string[]} argv - process.argv
|
|
65
|
+
*/
|
|
66
|
+
export async function run(argv) {
|
|
67
|
+
const args = argv.slice(2);
|
|
68
|
+
const subcommand = args[0];
|
|
69
|
+
|
|
70
|
+
// --help / -h at global level (before subcommand lookup)
|
|
71
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
72
|
+
// If a valid subcommand precedes --help, show per-command help
|
|
73
|
+
if (subcommand && COMMANDS[subcommand]) {
|
|
74
|
+
// handled below after flag parsing
|
|
75
|
+
} else {
|
|
76
|
+
process.stdout.write(GLOBAL_HELP + "\n");
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// --version at global level
|
|
82
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
83
|
+
const pkgPath = join(__dirname, "..", "..", "package.json");
|
|
84
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
85
|
+
process.stdout.write(pkg.version + "\n");
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// No subcommand or unknown → global help
|
|
90
|
+
if (!subcommand || !COMMANDS[subcommand]) {
|
|
91
|
+
if (subcommand && !subcommand.startsWith("-") && !COMMANDS[subcommand]) {
|
|
92
|
+
process.stderr.write(`Unknown command: ${subcommand}\n\n`);
|
|
93
|
+
}
|
|
94
|
+
process.stdout.write(GLOBAL_HELP + "\n");
|
|
95
|
+
process.exit(!subcommand || subcommand.startsWith("-") ? 0 : 3);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Parse flags after the subcommand
|
|
99
|
+
const flags = parseFlags(args.slice(1));
|
|
100
|
+
|
|
101
|
+
// Per-command help
|
|
102
|
+
if (flags.help) {
|
|
103
|
+
const mod = await COMMANDS[subcommand]();
|
|
104
|
+
if (mod.helpText) {
|
|
105
|
+
process.stdout.write(mod.helpText + "\n");
|
|
106
|
+
} else {
|
|
107
|
+
process.stdout.write(GLOBAL_HELP + "\n");
|
|
108
|
+
}
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Dispatch
|
|
113
|
+
try {
|
|
114
|
+
const mod = await COMMANDS[subcommand]();
|
|
115
|
+
const exitCode = await mod.execute(flags);
|
|
116
|
+
process.exit(exitCode ?? 0);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
119
|
+
process.exit(3);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mcpt-publishing audit` — run publishing health audit across all registries.
|
|
3
|
+
*
|
|
4
|
+
* Loads config → reads manifest → imports providers from scripts/lib/registry.mjs
|
|
5
|
+
* → runs orchestration loop → writes reports → emits audit receipt.
|
|
6
|
+
*
|
|
7
|
+
* Exit codes:
|
|
8
|
+
* 0 — all clean
|
|
9
|
+
* 2 — RED-severity drift found
|
|
10
|
+
* 3 — config or file error
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
14
|
+
import { join, dirname } from "node:path";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
import { loadConfig } from "../config/loader.mjs";
|
|
17
|
+
import { emitAuditReceipt } from "../receipts/audit-receipt.mjs";
|
|
18
|
+
import { EXIT } from "../cli/exit-codes.mjs";
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
// Ecosystem labels for markdown headings
|
|
23
|
+
const ECOSYSTEM_LABELS = {
|
|
24
|
+
npm: "npm",
|
|
25
|
+
nuget: "NuGet",
|
|
26
|
+
pypi: "PyPI",
|
|
27
|
+
ghcr: "GHCR",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const helpText = `
|
|
31
|
+
mcpt-publishing audit — Run publishing health audit.
|
|
32
|
+
|
|
33
|
+
Usage:
|
|
34
|
+
mcpt-publishing audit [flags]
|
|
35
|
+
|
|
36
|
+
Flags:
|
|
37
|
+
--json Output JSON to stdout (skip markdown reports)
|
|
38
|
+
--config Explicit path to publishing.config.json
|
|
39
|
+
--help Show this help
|
|
40
|
+
|
|
41
|
+
Exit codes:
|
|
42
|
+
0 All packages clean
|
|
43
|
+
2 RED-severity drift detected (CI-friendly non-zero)
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
mcpt-publishing audit # writes reports/latest.md + .json
|
|
47
|
+
mcpt-publishing audit --json # JSON to stdout only
|
|
48
|
+
`.trim();
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Execute the audit command.
|
|
52
|
+
* @param {object} flags - Parsed CLI flags
|
|
53
|
+
* @returns {number} Exit code
|
|
54
|
+
*/
|
|
55
|
+
export async function execute(flags) {
|
|
56
|
+
// Load config
|
|
57
|
+
const config = flags.config
|
|
58
|
+
? loadConfig(dirname(flags.config))
|
|
59
|
+
: loadConfig();
|
|
60
|
+
|
|
61
|
+
// Read manifest
|
|
62
|
+
const manifestPath = join(config.profilesDir, "manifest.json");
|
|
63
|
+
if (!existsSync(manifestPath)) {
|
|
64
|
+
process.stderr.write(`Error: Manifest not found at ${manifestPath}\n`);
|
|
65
|
+
process.stderr.write(`Run 'mcpt-publishing init' to scaffold the project.\n`);
|
|
66
|
+
return EXIT.CONFIG_ERROR;
|
|
67
|
+
}
|
|
68
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
69
|
+
|
|
70
|
+
// Import provider registry from scripts/lib (reuse existing working code)
|
|
71
|
+
const registryPath = join(__dirname, "..", "..", "scripts", "lib", "registry.mjs");
|
|
72
|
+
const registryUrl = pathToFileURL(registryPath).href;
|
|
73
|
+
const { loadProviders, matchProviders } = await import(registryUrl);
|
|
74
|
+
|
|
75
|
+
const providers = await loadProviders();
|
|
76
|
+
|
|
77
|
+
// Optional: filter by enabledProviders config
|
|
78
|
+
const enabled = config.enabledProviders ?? [];
|
|
79
|
+
const activeProviders = enabled.length > 0
|
|
80
|
+
? providers.filter(p => enabled.includes(p.name))
|
|
81
|
+
: providers;
|
|
82
|
+
|
|
83
|
+
// Shared context for tag/release caching
|
|
84
|
+
const ctx = {
|
|
85
|
+
tags: new Map(),
|
|
86
|
+
releases: new Map(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Find the GitHub provider (context loader) — must run before ecosystem providers
|
|
90
|
+
const ghProvider = activeProviders.find(p => p.name === "github");
|
|
91
|
+
|
|
92
|
+
// Build results object with an array per ecosystem key present in the manifest
|
|
93
|
+
const results = {};
|
|
94
|
+
for (const key of Object.keys(manifest)) {
|
|
95
|
+
if (Array.isArray(manifest[key])) results[key] = [];
|
|
96
|
+
}
|
|
97
|
+
results.generated = new Date().toISOString();
|
|
98
|
+
|
|
99
|
+
const allFindings = [];
|
|
100
|
+
|
|
101
|
+
// Process each ecosystem section from the manifest
|
|
102
|
+
for (const [ecosystem, packages] of Object.entries(manifest)) {
|
|
103
|
+
if (!Array.isArray(packages)) continue;
|
|
104
|
+
process.stderr.write(`Auditing ${packages.length} ${ECOSYSTEM_LABELS[ecosystem] ?? ecosystem} packages...\n`);
|
|
105
|
+
|
|
106
|
+
for (const pkg of packages) {
|
|
107
|
+
const entry = { ...pkg, ecosystem };
|
|
108
|
+
|
|
109
|
+
// Ensure GitHub context (tags + releases) is loaded for this repo
|
|
110
|
+
if (ghProvider && ghProvider.detect(entry)) {
|
|
111
|
+
await ghProvider.audit(entry, ctx);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Find the ecosystem-specific provider(s)
|
|
115
|
+
const ecosystemProviders = matchProviders(activeProviders, entry).filter(p => p.name !== "github");
|
|
116
|
+
|
|
117
|
+
let version = "?";
|
|
118
|
+
const findings = [];
|
|
119
|
+
|
|
120
|
+
for (const provider of ecosystemProviders) {
|
|
121
|
+
const result = await provider.audit(entry, ctx);
|
|
122
|
+
if (result.version && result.version !== "?") version = result.version;
|
|
123
|
+
findings.push(...result.findings);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const resultEntry = {
|
|
127
|
+
name: pkg.name,
|
|
128
|
+
version,
|
|
129
|
+
repo: pkg.repo,
|
|
130
|
+
audience: pkg.audience,
|
|
131
|
+
findings,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
if (results[ecosystem]) {
|
|
135
|
+
results[ecosystem].push(resultEntry);
|
|
136
|
+
}
|
|
137
|
+
allFindings.push(...findings.map(f => ({ ...f, pkg: pkg.name, ecosystem })));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Counts
|
|
142
|
+
const red = allFindings.filter(f => f.severity === "RED");
|
|
143
|
+
const yellow = allFindings.filter(f => f.severity === "YELLOW");
|
|
144
|
+
const gray = allFindings.filter(f => f.severity === "GRAY");
|
|
145
|
+
const info = allFindings.filter(f => f.severity === "INFO");
|
|
146
|
+
results.counts = { RED: red.length, YELLOW: yellow.length, GRAY: gray.length, INFO: info.length };
|
|
147
|
+
|
|
148
|
+
// Count total packages
|
|
149
|
+
let totalPackages = 0;
|
|
150
|
+
for (const [, val] of Object.entries(results)) {
|
|
151
|
+
if (Array.isArray(val)) totalPackages += val.length;
|
|
152
|
+
}
|
|
153
|
+
results.totalPackages = totalPackages;
|
|
154
|
+
|
|
155
|
+
// JSON-only mode
|
|
156
|
+
if (flags.json) {
|
|
157
|
+
process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
158
|
+
emitAuditReceipt(config, results);
|
|
159
|
+
return red.length > 0 ? EXIT.DRIFT_FOUND : EXIT.SUCCESS;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Generate markdown report
|
|
163
|
+
const md = buildMarkdownReport(results, manifest, allFindings, red, yellow, gray, info);
|
|
164
|
+
|
|
165
|
+
// Write reports
|
|
166
|
+
mkdirSync(config.reportsDir, { recursive: true });
|
|
167
|
+
writeFileSync(join(config.reportsDir, "latest.md"), md);
|
|
168
|
+
writeFileSync(join(config.reportsDir, "latest.json"), JSON.stringify(results, null, 2));
|
|
169
|
+
|
|
170
|
+
const infoSuffix = info.length > 0 ? ` INFO=${info.length} (indexing — retry later)` : "";
|
|
171
|
+
process.stderr.write(`\nDone. RED=${red.length} YELLOW=${yellow.length} GRAY=${gray.length}${infoSuffix}\n`);
|
|
172
|
+
process.stderr.write(`Reports written to ${config.reportsDir}/latest.md and latest.json\n`);
|
|
173
|
+
|
|
174
|
+
// Emit audit receipt
|
|
175
|
+
emitAuditReceipt(config, results);
|
|
176
|
+
|
|
177
|
+
return red.length > 0 ? EXIT.DRIFT_FOUND : EXIT.SUCCESS;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ─── Internal ────────────────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
function buildMarkdownReport(results, manifest, allFindings, red, yellow, gray, info) {
|
|
183
|
+
const lines = [];
|
|
184
|
+
lines.push("# Publishing Health Report");
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push(`> Generated: ${results.generated}`);
|
|
187
|
+
lines.push("");
|
|
188
|
+
const infoLabel = info.length > 0 ? ` | **INFO: ${info.length}** (indexing)` : "";
|
|
189
|
+
lines.push(`**RED: ${red.length}** | **YELLOW: ${yellow.length}** | **GRAY: ${gray.length}**${infoLabel}`);
|
|
190
|
+
lines.push("");
|
|
191
|
+
|
|
192
|
+
if (red.length + yellow.length + info.length > 0) {
|
|
193
|
+
lines.push("## Top Actions");
|
|
194
|
+
lines.push("");
|
|
195
|
+
for (const f of [...red, ...yellow, ...info].slice(0, 10)) {
|
|
196
|
+
lines.push(`- **${f.severity}** ${f.msg}`);
|
|
197
|
+
}
|
|
198
|
+
lines.push("");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Group by package
|
|
202
|
+
const byRepo = {};
|
|
203
|
+
for (const f of allFindings) {
|
|
204
|
+
const key = f.pkg;
|
|
205
|
+
if (!byRepo[key]) byRepo[key] = [];
|
|
206
|
+
byRepo[key].push(f);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (Object.keys(byRepo).length > 0) {
|
|
210
|
+
lines.push("## Findings by Package");
|
|
211
|
+
lines.push("");
|
|
212
|
+
for (const [pkg, findings] of Object.entries(byRepo)) {
|
|
213
|
+
lines.push(`### ${pkg}`);
|
|
214
|
+
for (const f of findings) {
|
|
215
|
+
lines.push(`- **${f.severity}** [${f.code}] ${f.msg}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push("");
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Summary tables per ecosystem
|
|
222
|
+
for (const [ecosystem, packages] of Object.entries(manifest)) {
|
|
223
|
+
if (!Array.isArray(packages) || packages.length === 0) continue;
|
|
224
|
+
const label = ECOSYSTEM_LABELS[ecosystem] ?? ecosystem;
|
|
225
|
+
|
|
226
|
+
lines.push(`## ${label} Packages`);
|
|
227
|
+
lines.push("");
|
|
228
|
+
lines.push("| Package | Version | Audience | Issues |");
|
|
229
|
+
lines.push("|---------|---------|----------|--------|");
|
|
230
|
+
for (const e of results[ecosystem] ?? []) {
|
|
231
|
+
const issues = e.findings.length === 0 ? "clean" : e.findings.map(f => f.severity).join(", ");
|
|
232
|
+
lines.push(`| ${e.name} | ${e.version} | ${e.audience} | ${issues} |`);
|
|
233
|
+
}
|
|
234
|
+
lines.push("");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return lines.join("\n");
|
|
238
|
+
}
|
|
@@ -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
|
+
}
|