mogmd 0.1.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/dist/bin.js +1488 -0
- package/package.json +45 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,1488 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/auth.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import chalk2 from "chalk";
|
|
9
|
+
import ora from "ora";
|
|
10
|
+
import open from "open";
|
|
11
|
+
|
|
12
|
+
// src/config.ts
|
|
13
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
14
|
+
import { join } from "path";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
var CONFIG_DIR = join(homedir(), ".config", "mog");
|
|
17
|
+
var CREDENTIALS_FILE = join(CONFIG_DIR, "credentials.json");
|
|
18
|
+
var API_BASE = process.env.MOG_API_URL ?? "https://api.mog.md";
|
|
19
|
+
var APP_BASE = process.env.MOG_APP_URL ?? "https://mog.md";
|
|
20
|
+
var LOCKFILE_NAME = "mog.lock.json";
|
|
21
|
+
function readCredentials() {
|
|
22
|
+
try {
|
|
23
|
+
if (!existsSync(CREDENTIALS_FILE)) return null;
|
|
24
|
+
return JSON.parse(readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function writeCredentials(creds) {
|
|
30
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
31
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { mode: 384 });
|
|
32
|
+
}
|
|
33
|
+
function clearCredentials() {
|
|
34
|
+
try {
|
|
35
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
36
|
+
writeFileSync(CREDENTIALS_FILE, "{}");
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/api.ts
|
|
43
|
+
var ApiError = class extends Error {
|
|
44
|
+
constructor(status, message, body) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.status = status;
|
|
47
|
+
this.body = body;
|
|
48
|
+
this.name = "ApiError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
async function request(method, path, options = {}) {
|
|
52
|
+
const creds = readCredentials();
|
|
53
|
+
const token = options.token ?? creds?.token;
|
|
54
|
+
const headers = {};
|
|
55
|
+
if (token && !options.noAuth) {
|
|
56
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
57
|
+
}
|
|
58
|
+
let body;
|
|
59
|
+
if (options.multipart) {
|
|
60
|
+
body = options.multipart;
|
|
61
|
+
} else if (options.body !== void 0) {
|
|
62
|
+
headers["Content-Type"] = "application/json";
|
|
63
|
+
body = JSON.stringify(options.body);
|
|
64
|
+
}
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
67
|
+
let res;
|
|
68
|
+
try {
|
|
69
|
+
res = await fetch(`${API_BASE}${path}`, {
|
|
70
|
+
method,
|
|
71
|
+
headers,
|
|
72
|
+
body,
|
|
73
|
+
signal: controller.signal
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err?.name === "AbortError") {
|
|
77
|
+
throw new ApiError(408, "Request timed out (30s)");
|
|
78
|
+
}
|
|
79
|
+
throw err;
|
|
80
|
+
} finally {
|
|
81
|
+
clearTimeout(timeout);
|
|
82
|
+
}
|
|
83
|
+
const data = await res.json().catch(() => ({ error: res.statusText }));
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
if (res.status === 401) {
|
|
86
|
+
throw new ApiError(
|
|
87
|
+
401,
|
|
88
|
+
"Your session has expired or is invalid. Run `mog auth` to sign in again.",
|
|
89
|
+
data
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
throw new ApiError(res.status, data.error ?? res.statusText, data);
|
|
93
|
+
}
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
var api = {
|
|
97
|
+
get: (path) => request("GET", path),
|
|
98
|
+
post: (path, body) => request("POST", path, { body }),
|
|
99
|
+
postNoAuth: (path, body) => request("POST", path, { body, noAuth: true }),
|
|
100
|
+
patch: (path, body) => request("PATCH", path, { body }),
|
|
101
|
+
upload: (path, form) => request("POST", path, { multipart: form })
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// src/output.ts
|
|
105
|
+
import chalk from "chalk";
|
|
106
|
+
var jsonMode = false;
|
|
107
|
+
function setJsonMode(val) {
|
|
108
|
+
jsonMode = val;
|
|
109
|
+
}
|
|
110
|
+
var EXIT = {
|
|
111
|
+
SUCCESS: 0,
|
|
112
|
+
ERROR: 1,
|
|
113
|
+
APPROVAL_REQUIRED: 2
|
|
114
|
+
};
|
|
115
|
+
function output(envelope) {
|
|
116
|
+
if (jsonMode) {
|
|
117
|
+
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
118
|
+
}
|
|
119
|
+
process.exit(envelope.ok ? EXIT.SUCCESS : envelope.approvalUrl ? EXIT.APPROVAL_REQUIRED : EXIT.ERROR);
|
|
120
|
+
}
|
|
121
|
+
function success(command, data) {
|
|
122
|
+
if (jsonMode) {
|
|
123
|
+
output({ ok: true, command, data, errors: [], approvalUrl: null });
|
|
124
|
+
}
|
|
125
|
+
process.exit(EXIT.SUCCESS);
|
|
126
|
+
}
|
|
127
|
+
function fail(command, message, approvalUrl) {
|
|
128
|
+
if (jsonMode) {
|
|
129
|
+
output({
|
|
130
|
+
ok: false,
|
|
131
|
+
command,
|
|
132
|
+
errors: [message],
|
|
133
|
+
approvalUrl: approvalUrl ?? null
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
console.error(chalk.red(`\u2717 ${message}`));
|
|
137
|
+
if (approvalUrl) {
|
|
138
|
+
console.error(chalk.yellow(` Approval required: ${approvalUrl}`));
|
|
139
|
+
}
|
|
140
|
+
process.exit(approvalUrl ? EXIT.APPROVAL_REQUIRED : EXIT.ERROR);
|
|
141
|
+
}
|
|
142
|
+
function info(msg) {
|
|
143
|
+
if (!jsonMode) console.log(msg);
|
|
144
|
+
}
|
|
145
|
+
function warn(msg) {
|
|
146
|
+
if (!jsonMode) console.warn(chalk.yellow(`\u26A0 ${msg}`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/commands/auth.ts
|
|
150
|
+
function sleep(ms) {
|
|
151
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
152
|
+
}
|
|
153
|
+
function decodeTokenPayload(token) {
|
|
154
|
+
try {
|
|
155
|
+
const parts = token.split(".");
|
|
156
|
+
if (parts.length !== 3) return null;
|
|
157
|
+
const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
|
|
158
|
+
return JSON.parse(payload);
|
|
159
|
+
} catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function formatTimeRemaining(ms) {
|
|
164
|
+
if (ms <= 0) return "expired";
|
|
165
|
+
const days = Math.floor(ms / (1e3 * 60 * 60 * 24));
|
|
166
|
+
const hours = Math.floor(ms % (1e3 * 60 * 60 * 24) / (1e3 * 60 * 60));
|
|
167
|
+
if (days > 0) return `${days}d ${hours}h remaining`;
|
|
168
|
+
const minutes = Math.floor(ms % (1e3 * 60 * 60) / (1e3 * 60));
|
|
169
|
+
if (hours > 0) return `${hours}h ${minutes}m remaining`;
|
|
170
|
+
return `${minutes}m remaining`;
|
|
171
|
+
}
|
|
172
|
+
var authCommand = new Command("auth").description("Authenticate with the mog marketplace").option("--logout", "Log out and remove stored credentials").option("--status", "Show current auth status").option("--force", "Re-authenticate even if already signed in").option("--json", "Output as JSON").action(async (opts) => {
|
|
173
|
+
if (opts.json) setJsonMode(true);
|
|
174
|
+
if (opts.logout) {
|
|
175
|
+
clearCredentials();
|
|
176
|
+
info(chalk2.green("\u2713 Logged out"));
|
|
177
|
+
success("auth", { loggedOut: true });
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (opts.status) {
|
|
181
|
+
const creds = readCredentials();
|
|
182
|
+
if (creds?.token) {
|
|
183
|
+
const payload = decodeTokenPayload(creds.token);
|
|
184
|
+
const exp = payload?.exp;
|
|
185
|
+
const sub = payload?.sub;
|
|
186
|
+
const scopes = payload?.scopes;
|
|
187
|
+
if (exp) {
|
|
188
|
+
const expiresAt = new Date(exp * 1e3);
|
|
189
|
+
const remaining = expiresAt.getTime() - Date.now();
|
|
190
|
+
if (remaining <= 0) {
|
|
191
|
+
info(chalk2.yellow("\u26A0 Token expired. Run `mog auth` to sign in again."));
|
|
192
|
+
success("auth", { authenticated: false, expired: true });
|
|
193
|
+
} else {
|
|
194
|
+
info(chalk2.green("\u2713 Authenticated"));
|
|
195
|
+
info(` Expires: ${expiresAt.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })} (${formatTimeRemaining(remaining)})`);
|
|
196
|
+
if (scopes) info(` Scopes: ${scopes.join(", ")}`);
|
|
197
|
+
success("auth", {
|
|
198
|
+
authenticated: true,
|
|
199
|
+
userId: sub,
|
|
200
|
+
scopes,
|
|
201
|
+
expiresAt: expiresAt.toISOString(),
|
|
202
|
+
remainingMs: remaining,
|
|
203
|
+
issuedAt: creds.issuedAt
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
info(chalk2.green("\u2713 Authenticated"));
|
|
208
|
+
success("auth", { authenticated: true, issuedAt: creds.issuedAt });
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
info(chalk2.yellow("Not authenticated. Run: mog auth"));
|
|
212
|
+
success("auth", { authenticated: false });
|
|
213
|
+
}
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const existing = readCredentials();
|
|
217
|
+
if (existing?.token && !opts.force) {
|
|
218
|
+
const payload = decodeTokenPayload(existing.token);
|
|
219
|
+
const exp = payload?.exp;
|
|
220
|
+
const isExpired = exp ? exp * 1e3 < Date.now() : false;
|
|
221
|
+
if (isExpired) {
|
|
222
|
+
info(chalk2.yellow("\u26A0 Your token has expired. Starting re-authentication..."));
|
|
223
|
+
} else {
|
|
224
|
+
info(chalk2.green("\u2713 Already authenticated. Use --force to re-authenticate, or --logout to sign out."));
|
|
225
|
+
success("auth", { authenticated: true });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const spinner = ora("Requesting device code...").start();
|
|
230
|
+
let deviceData;
|
|
231
|
+
try {
|
|
232
|
+
deviceData = await api.postNoAuth("/v1/auth/device/start");
|
|
233
|
+
spinner.stop();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
spinner.fail("Failed to connect to mog API");
|
|
236
|
+
fail("auth", String(err));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(chalk2.bold("To authenticate, visit:"));
|
|
241
|
+
console.log(chalk2.cyan(` ${deviceData.verificationUri}`));
|
|
242
|
+
console.log();
|
|
243
|
+
console.log(chalk2.bold("Enter code:"), chalk2.yellow.bold(deviceData.userCode));
|
|
244
|
+
console.log();
|
|
245
|
+
try {
|
|
246
|
+
await open(deviceData.verificationUri);
|
|
247
|
+
info(chalk2.dim("(Opened in browser)"));
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
const pollSpinner = ora("Waiting for approval...").start();
|
|
251
|
+
const deadline = Date.now() + deviceData.expiresIn * 1e3;
|
|
252
|
+
while (Date.now() < deadline) {
|
|
253
|
+
await sleep(deviceData.interval * 1e3);
|
|
254
|
+
try {
|
|
255
|
+
const result = await api.postNoAuth("/v1/auth/device/poll", {
|
|
256
|
+
deviceCode: deviceData.deviceCode
|
|
257
|
+
});
|
|
258
|
+
if (result.status === "approved" && result.token) {
|
|
259
|
+
pollSpinner.succeed(chalk2.green("Authenticated!"));
|
|
260
|
+
writeCredentials({ token: result.token, issuedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
261
|
+
info(chalk2.green("\n\u2713 You are now signed in to mog.md"));
|
|
262
|
+
success("auth", { authenticated: true });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (result.status === "expired") {
|
|
266
|
+
pollSpinner.fail("Code expired");
|
|
267
|
+
fail("auth", "Authentication timed out. Please try again.");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
} catch (err) {
|
|
271
|
+
if (err instanceof ApiError && err.status === 400) {
|
|
272
|
+
pollSpinner.fail("Code expired");
|
|
273
|
+
fail("auth", "Authentication timed out. Please try again.");
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
pollSpinner.fail("Timed out");
|
|
279
|
+
fail("auth", "Authentication timed out.");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// src/commands/search.ts
|
|
283
|
+
import { Command as Command2 } from "commander";
|
|
284
|
+
import chalk3 from "chalk";
|
|
285
|
+
|
|
286
|
+
// ../../packages/shared/dist/schemas/mog-yaml.js
|
|
287
|
+
import { z } from "zod";
|
|
288
|
+
var PackageTypeSchema = z.enum(["skill", "rule", "bundle", "template", "mcp"]);
|
|
289
|
+
var TargetSchema = z.enum(["cursor", "claude-code", "codex", "gemini-cli", "windsurf", "generic"]);
|
|
290
|
+
var InstallMapSchema = z.object({
|
|
291
|
+
cursor: z.string().optional(),
|
|
292
|
+
"claude-code": z.string().optional(),
|
|
293
|
+
codex: z.string().optional(),
|
|
294
|
+
"gemini-cli": z.string().optional(),
|
|
295
|
+
windsurf: z.string().optional(),
|
|
296
|
+
generic: z.string().optional()
|
|
297
|
+
});
|
|
298
|
+
var McpEnvVarSchema = z.object({
|
|
299
|
+
name: z.string().min(1).max(128).regex(/^[A-Z_][A-Z0-9_]*$/, "Must be an uppercase env var name (e.g. GITHUB_TOKEN)"),
|
|
300
|
+
description: z.string().max(512).optional(),
|
|
301
|
+
required: z.boolean().default(true),
|
|
302
|
+
secret: z.boolean().default(false)
|
|
303
|
+
});
|
|
304
|
+
var McpConfigSchema = z.object({
|
|
305
|
+
transport: z.enum(["stdio", "http"]).default("stdio"),
|
|
306
|
+
command: z.string().min(1).max(256),
|
|
307
|
+
args: z.array(z.string().max(512)).default([]),
|
|
308
|
+
env: z.array(McpEnvVarSchema).max(50).optional(),
|
|
309
|
+
// http transport only
|
|
310
|
+
url: z.string().url().optional(),
|
|
311
|
+
headers: z.record(z.string(), z.string()).optional()
|
|
312
|
+
});
|
|
313
|
+
var MogYamlSourceSchema = z.object({
|
|
314
|
+
url: z.string().url().optional(),
|
|
315
|
+
provider: z.string().max(128).optional(),
|
|
316
|
+
stars: z.number().int().nonnegative().optional()
|
|
317
|
+
});
|
|
318
|
+
var MogYamlSchema = z.object({
|
|
319
|
+
name: z.string().min(1).max(128).regex(/^[a-z0-9-]+\/[a-z0-9-]+$/, "Must be in format vendor/package-name (lowercase letters, numbers, hyphens)"),
|
|
320
|
+
version: z.string().regex(/^\d+\.\d+\.\d+$/, "Must be a valid semver (e.g. 1.0.0)"),
|
|
321
|
+
type: PackageTypeSchema,
|
|
322
|
+
description: z.string().min(1).max(1024),
|
|
323
|
+
targets: z.array(TargetSchema).min(1),
|
|
324
|
+
install_map: InstallMapSchema.optional(),
|
|
325
|
+
entrypoint: z.string().optional(),
|
|
326
|
+
requires: z.object({
|
|
327
|
+
tools: z.array(z.string()).optional()
|
|
328
|
+
}).optional(),
|
|
329
|
+
license: z.string().optional().default("MIT"),
|
|
330
|
+
readme: z.string().optional(),
|
|
331
|
+
mcp: McpConfigSchema.optional(),
|
|
332
|
+
source: MogYamlSourceSchema.optional()
|
|
333
|
+
}).superRefine((data, ctx) => {
|
|
334
|
+
if (data.type === "mcp" && !data.mcp) {
|
|
335
|
+
ctx.addIssue({
|
|
336
|
+
code: z.ZodIssueCode.custom,
|
|
337
|
+
message: 'mcp field is required when type is "mcp"',
|
|
338
|
+
path: ["mcp"]
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
if (data.type !== "mcp" && data.mcp) {
|
|
342
|
+
ctx.addIssue({
|
|
343
|
+
code: z.ZodIssueCode.custom,
|
|
344
|
+
message: 'mcp field is only allowed when type is "mcp"',
|
|
345
|
+
path: ["mcp"]
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (data.mcp?.transport === "http" && !data.mcp.url) {
|
|
349
|
+
ctx.addIssue({
|
|
350
|
+
code: z.ZodIssueCode.custom,
|
|
351
|
+
message: 'mcp.url is required when transport is "http"',
|
|
352
|
+
path: ["mcp", "url"]
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (data.mcp?.transport === "stdio" && !data.mcp.command) {
|
|
356
|
+
ctx.addIssue({
|
|
357
|
+
code: z.ZodIssueCode.custom,
|
|
358
|
+
message: 'mcp.command is required when transport is "stdio"',
|
|
359
|
+
path: ["mcp", "command"]
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ../../packages/shared/dist/schemas/lockfile.js
|
|
365
|
+
import { z as z2 } from "zod";
|
|
366
|
+
var LockfileEntrySchema = z2.object({
|
|
367
|
+
name: z2.string(),
|
|
368
|
+
vendor: z2.string(),
|
|
369
|
+
slug: z2.string(),
|
|
370
|
+
version: z2.string(),
|
|
371
|
+
listingId: z2.string().uuid(),
|
|
372
|
+
releaseId: z2.string().uuid(),
|
|
373
|
+
entitlementId: z2.string().uuid(),
|
|
374
|
+
installedAt: z2.string().datetime(),
|
|
375
|
+
target: z2.string(),
|
|
376
|
+
installPath: z2.string(),
|
|
377
|
+
archiveSha256: z2.string(),
|
|
378
|
+
files: z2.array(z2.object({
|
|
379
|
+
path: z2.string(),
|
|
380
|
+
sha256: z2.string(),
|
|
381
|
+
size: z2.number()
|
|
382
|
+
})),
|
|
383
|
+
updateChannel: z2.enum(["patch", "minor", "major"]).default("minor"),
|
|
384
|
+
// For MCP packages: tracks which client config files were written
|
|
385
|
+
installType: z2.enum(["files", "mcp-config"]).default("files"),
|
|
386
|
+
mcpConfigPaths: z2.array(z2.string()).optional()
|
|
387
|
+
});
|
|
388
|
+
var LockfileSchema = z2.object({
|
|
389
|
+
version: z2.literal(1),
|
|
390
|
+
lockfileVersion: z2.string().default("1.0.0"),
|
|
391
|
+
generatedAt: z2.string().datetime(),
|
|
392
|
+
packages: z2.record(z2.string(), LockfileEntrySchema)
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ../../packages/shared/dist/schemas/api.js
|
|
396
|
+
import { z as z3 } from "zod";
|
|
397
|
+
var DeviceStartResponseSchema = z3.object({
|
|
398
|
+
deviceCode: z3.string(),
|
|
399
|
+
userCode: z3.string(),
|
|
400
|
+
verificationUri: z3.string().url(),
|
|
401
|
+
expiresIn: z3.number(),
|
|
402
|
+
interval: z3.number()
|
|
403
|
+
});
|
|
404
|
+
var DevicePollRequestSchema = z3.object({
|
|
405
|
+
deviceCode: z3.string()
|
|
406
|
+
});
|
|
407
|
+
var DevicePollResponseSchema = z3.discriminatedUnion("status", [
|
|
408
|
+
z3.object({ status: z3.literal("authorization_pending") }),
|
|
409
|
+
z3.object({ status: z3.literal("expired") }),
|
|
410
|
+
z3.object({
|
|
411
|
+
status: z3.literal("approved"),
|
|
412
|
+
token: z3.string(),
|
|
413
|
+
tokenType: z3.literal("Bearer"),
|
|
414
|
+
expiresIn: z3.number()
|
|
415
|
+
})
|
|
416
|
+
]);
|
|
417
|
+
var ListingSchema = z3.object({
|
|
418
|
+
id: z3.string().uuid(),
|
|
419
|
+
vendorSlug: z3.string(),
|
|
420
|
+
slug: z3.string(),
|
|
421
|
+
title: z3.string(),
|
|
422
|
+
description: z3.string(),
|
|
423
|
+
type: PackageTypeSchema,
|
|
424
|
+
tags: z3.array(z3.string()),
|
|
425
|
+
targets: z3.array(TargetSchema),
|
|
426
|
+
priceCents: z3.number(),
|
|
427
|
+
currency: z3.string().default("usd"),
|
|
428
|
+
latestVersion: z3.string().nullable(),
|
|
429
|
+
installCount: z3.number().default(0),
|
|
430
|
+
rating: z3.number().nullable(),
|
|
431
|
+
ratingCount: z3.number().default(0),
|
|
432
|
+
createdAt: z3.string().datetime()
|
|
433
|
+
});
|
|
434
|
+
var SearchResponseSchema = z3.object({
|
|
435
|
+
results: z3.array(ListingSchema),
|
|
436
|
+
total: z3.number(),
|
|
437
|
+
page: z3.number(),
|
|
438
|
+
perPage: z3.number()
|
|
439
|
+
});
|
|
440
|
+
var PurchaseRequestSchema = z3.object({
|
|
441
|
+
listingId: z3.string().uuid(),
|
|
442
|
+
releaseId: z3.string().uuid().optional(),
|
|
443
|
+
maxPriceCents: z3.number().optional()
|
|
444
|
+
});
|
|
445
|
+
var PurchaseResponseSchema = z3.discriminatedUnion("status", [
|
|
446
|
+
z3.object({
|
|
447
|
+
status: z3.literal("purchased"),
|
|
448
|
+
entitlementId: z3.string().uuid(),
|
|
449
|
+
orderId: z3.string().uuid(),
|
|
450
|
+
amountCents: z3.number()
|
|
451
|
+
}),
|
|
452
|
+
z3.object({
|
|
453
|
+
status: z3.literal("already_owned"),
|
|
454
|
+
entitlementId: z3.string().uuid()
|
|
455
|
+
}),
|
|
456
|
+
z3.object({
|
|
457
|
+
status: z3.literal("approval_required"),
|
|
458
|
+
approvalUrl: z3.string().url(),
|
|
459
|
+
reason: z3.string()
|
|
460
|
+
})
|
|
461
|
+
]);
|
|
462
|
+
var DownloadTokenRequestSchema = z3.object({
|
|
463
|
+
releaseId: z3.string().uuid()
|
|
464
|
+
});
|
|
465
|
+
var DownloadTokenResponseSchema = z3.object({
|
|
466
|
+
url: z3.string().url(),
|
|
467
|
+
sha256: z3.string(),
|
|
468
|
+
expiresAt: z3.string().datetime()
|
|
469
|
+
});
|
|
470
|
+
var EntitlementSchema = z3.object({
|
|
471
|
+
id: z3.string().uuid(),
|
|
472
|
+
listingId: z3.string().uuid(),
|
|
473
|
+
releaseId: z3.string().uuid(),
|
|
474
|
+
vendorSlug: z3.string(),
|
|
475
|
+
listingSlug: z3.string(),
|
|
476
|
+
version: z3.string(),
|
|
477
|
+
grantedAt: z3.string().datetime()
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// ../../packages/shared/dist/index.js
|
|
481
|
+
var MIN_PRICE_CENTS = 99;
|
|
482
|
+
var MAX_PRICE_CENTS = 5e4;
|
|
483
|
+
function resolveInstallPath(installMap, target, slug) {
|
|
484
|
+
const defaults = {
|
|
485
|
+
cursor: ".cursor/skills/{slug}/",
|
|
486
|
+
"claude-code": ".claude/skills/{slug}/",
|
|
487
|
+
codex: "mog_modules/{slug}/",
|
|
488
|
+
"gemini-cli": ".gemini/skills/{slug}/",
|
|
489
|
+
windsurf: ".windsurf/skills/{slug}/",
|
|
490
|
+
generic: "mog_modules/{slug}/"
|
|
491
|
+
};
|
|
492
|
+
const template = installMap?.[target] ?? defaults[target] ?? "mog_modules/{slug}/";
|
|
493
|
+
return template.replace("{slug}", slug).replace("{name}", slug);
|
|
494
|
+
}
|
|
495
|
+
function formatPrice(cents, currency = "usd") {
|
|
496
|
+
if (cents === 0)
|
|
497
|
+
return "Free";
|
|
498
|
+
return new Intl.NumberFormat("en-US", {
|
|
499
|
+
style: "currency",
|
|
500
|
+
currency: currency.toUpperCase()
|
|
501
|
+
}).format(cents / 100);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// src/commands/search.ts
|
|
505
|
+
var searchCommand = new Command2("search").description("Search the mog marketplace").argument("[query]", "Search query").option("--type <type>", "Filter by type: skill, rule, bundle, template").option("--target <target>", "Filter by target: cursor, claude-code, codex, generic").option("--sort <sort>", "Sort by: popular, recent, price_asc, price_desc", "popular").option("--free", "Show only free packages").option("--page <n>", "Page number", "1").option("--json", "Output as JSON").action(
|
|
506
|
+
async (query, opts) => {
|
|
507
|
+
if (opts.json) setJsonMode(true);
|
|
508
|
+
const params = new URLSearchParams();
|
|
509
|
+
if (query) params.set("q", query);
|
|
510
|
+
if (opts.type) params.set("type", opts.type);
|
|
511
|
+
if (opts.target) params.set("target", opts.target);
|
|
512
|
+
if (opts.sort) params.set("sort", opts.sort);
|
|
513
|
+
if (opts.free) params.set("free", "true");
|
|
514
|
+
if (opts.page) params.set("page", opts.page);
|
|
515
|
+
let data;
|
|
516
|
+
try {
|
|
517
|
+
data = await api.get(`/v1/search?${params.toString()}`);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
fail("search", `Search failed: ${String(err)}`);
|
|
520
|
+
}
|
|
521
|
+
if (opts.json) {
|
|
522
|
+
success("search", data);
|
|
523
|
+
}
|
|
524
|
+
console.log(chalk3.dim(`${data.total} result${data.total !== 1 ? "s" : ""}
|
|
525
|
+
`));
|
|
526
|
+
if (data.results.length === 0) {
|
|
527
|
+
info(chalk3.yellow("No results found. Try a different query or remove filters."));
|
|
528
|
+
process.exit(0);
|
|
529
|
+
}
|
|
530
|
+
for (const pkg of data.results) {
|
|
531
|
+
const price = formatPrice(pkg.priceCents, pkg.currency);
|
|
532
|
+
const verified = pkg.vendorVerified ? chalk3.blue(" \u2713") : "";
|
|
533
|
+
const rating = pkg.rating ? chalk3.yellow(` \u2605${pkg.rating}`) : "";
|
|
534
|
+
console.log(
|
|
535
|
+
chalk3.bold(`${pkg.vendorSlug}/${pkg.slug}`) + chalk3.dim(`@${pkg.latestVersion ?? "?"}`) + ` ${chalk3.green(price)}${rating}`
|
|
536
|
+
);
|
|
537
|
+
console.log(` ${chalk3.cyan(pkg.type.padEnd(8))} ${pkg.targets.join(", ")} by ${pkg.vendorName}${verified}`);
|
|
538
|
+
console.log(` ${chalk3.dim(pkg.description.slice(0, 90))}`);
|
|
539
|
+
console.log();
|
|
540
|
+
}
|
|
541
|
+
console.log(chalk3.dim(`Install with: mog install <vendor>/<package>`));
|
|
542
|
+
process.exit(0);
|
|
543
|
+
}
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// src/commands/buy.ts
|
|
547
|
+
import { Command as Command3 } from "commander";
|
|
548
|
+
import chalk4 from "chalk";
|
|
549
|
+
import ora2 from "ora";
|
|
550
|
+
var buyCommand = new Command3("buy").description("Purchase a package from the mog marketplace").argument("<package>", "Package to buy (e.g. acme/router-eval)").option("--max-price <cents>", "Maximum price in cents to auto-approve").option("--auto", "Allow automatic purchase if within policy limits").option("--json", "Output as JSON").action(
|
|
551
|
+
async (pkg, opts) => {
|
|
552
|
+
if (opts.json) setJsonMode(true);
|
|
553
|
+
const creds = readCredentials();
|
|
554
|
+
if (!creds?.token) {
|
|
555
|
+
fail("buy", "Not authenticated. Run: mog auth");
|
|
556
|
+
}
|
|
557
|
+
const [vendor, slug] = pkg.split("/");
|
|
558
|
+
if (!vendor || !slug) {
|
|
559
|
+
fail("buy", `Invalid package format: "${pkg}". Expected: vendor/slug`);
|
|
560
|
+
}
|
|
561
|
+
let listing;
|
|
562
|
+
try {
|
|
563
|
+
listing = await api.get(`/v1/listings/${vendor}/${slug}`);
|
|
564
|
+
} catch (err) {
|
|
565
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
566
|
+
fail("buy", `Package "${pkg}" not found`);
|
|
567
|
+
}
|
|
568
|
+
fail("buy", `Failed to fetch package: ${String(err)}`);
|
|
569
|
+
}
|
|
570
|
+
info(`
|
|
571
|
+
${chalk4.bold(pkg)} \u2014 ${chalk4.green(formatPrice(listing.priceCents, listing.currency))}`);
|
|
572
|
+
info(`${chalk4.dim(listing.title)}
|
|
573
|
+
`);
|
|
574
|
+
if (listing.priceCents === 0) {
|
|
575
|
+
info(chalk4.dim("Free package \u2014 no payment required"));
|
|
576
|
+
}
|
|
577
|
+
const spinner = ora2("Purchasing...").start();
|
|
578
|
+
const body = { listingId: listing.id };
|
|
579
|
+
if (opts.maxPrice) body.maxPriceCents = parseInt(opts.maxPrice, 10);
|
|
580
|
+
let result;
|
|
581
|
+
try {
|
|
582
|
+
result = await api.post("/v1/purchases", body);
|
|
583
|
+
spinner.stop();
|
|
584
|
+
} catch (err) {
|
|
585
|
+
spinner.stop();
|
|
586
|
+
if (err instanceof ApiError && err.status === 402) {
|
|
587
|
+
const body2 = err.body;
|
|
588
|
+
if (body2?.approvalUrl) {
|
|
589
|
+
fail("buy", body2.reason ?? "Human approval required", body2.approvalUrl);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
fail("buy", `Purchase failed: ${String(err)}`);
|
|
593
|
+
}
|
|
594
|
+
if (result.status === "already_owned") {
|
|
595
|
+
spinner.stop();
|
|
596
|
+
info(chalk4.green(`\u2713 Already owned (entitlement: ${result.entitlementId})`));
|
|
597
|
+
success("buy", result);
|
|
598
|
+
}
|
|
599
|
+
if (result.status === "approval_required") {
|
|
600
|
+
spinner.stop();
|
|
601
|
+
fail("buy", result.reason ?? "Approval required", result.approvalUrl);
|
|
602
|
+
}
|
|
603
|
+
spinner.succeed(chalk4.green(`Purchased ${pkg}!`));
|
|
604
|
+
info(` Entitlement: ${result.entitlementId}`);
|
|
605
|
+
if (result.amountCents && result.amountCents > 0) {
|
|
606
|
+
info(` Charged: ${formatPrice(result.amountCents)}`);
|
|
607
|
+
}
|
|
608
|
+
info(chalk4.dim(`
|
|
609
|
+
Install with: mog install ${pkg}`));
|
|
610
|
+
success("buy", result);
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
// src/commands/install.ts
|
|
615
|
+
import { Command as Command4 } from "commander";
|
|
616
|
+
import chalk5 from "chalk";
|
|
617
|
+
import ora3 from "ora";
|
|
618
|
+
import { createHash } from "crypto";
|
|
619
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, existsSync as existsSync4, rmSync } from "fs";
|
|
620
|
+
import { join as join4, dirname as dirname3, resolve } from "path";
|
|
621
|
+
import { createInterface } from "readline";
|
|
622
|
+
import AdmZip from "adm-zip";
|
|
623
|
+
import yaml from "js-yaml";
|
|
624
|
+
|
|
625
|
+
// src/lockfile.ts
|
|
626
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, renameSync } from "fs";
|
|
627
|
+
import { join as join2 } from "path";
|
|
628
|
+
function readLockfile(cwd = process.cwd()) {
|
|
629
|
+
const path = join2(cwd, LOCKFILE_NAME);
|
|
630
|
+
if (!existsSync2(path)) {
|
|
631
|
+
return {
|
|
632
|
+
version: 1,
|
|
633
|
+
lockfileVersion: "1.0.0",
|
|
634
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
635
|
+
packages: {}
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
const raw = JSON.parse(readFileSync2(path, "utf-8"));
|
|
639
|
+
const result = LockfileSchema.safeParse(raw);
|
|
640
|
+
if (!result.success) {
|
|
641
|
+
return raw;
|
|
642
|
+
}
|
|
643
|
+
return result.data;
|
|
644
|
+
}
|
|
645
|
+
function writeLockfile(lockfile, cwd = process.cwd()) {
|
|
646
|
+
const finalPath = join2(cwd, LOCKFILE_NAME);
|
|
647
|
+
const tmpPath = finalPath + ".tmp";
|
|
648
|
+
lockfile.generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
649
|
+
writeFileSync2(tmpPath, JSON.stringify(lockfile, null, 2) + "\n");
|
|
650
|
+
renameSync(tmpPath, finalPath);
|
|
651
|
+
}
|
|
652
|
+
function addToLockfile(entry, cwd = process.cwd()) {
|
|
653
|
+
const lock = readLockfile(cwd);
|
|
654
|
+
const key = `${entry.vendor}/${entry.slug}`;
|
|
655
|
+
lock.packages[key] = entry;
|
|
656
|
+
writeLockfile(lock, cwd);
|
|
657
|
+
}
|
|
658
|
+
function removeFromLockfile(key, cwd = process.cwd()) {
|
|
659
|
+
const lock = readLockfile(cwd);
|
|
660
|
+
delete lock.packages[key];
|
|
661
|
+
writeLockfile(lock, cwd);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// src/mcp-config.ts
|
|
665
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync3 } from "fs";
|
|
666
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
667
|
+
import { homedir as homedir2, platform } from "os";
|
|
668
|
+
function readJsonConfig(filePath) {
|
|
669
|
+
if (!existsSync3(filePath)) return { mcpServers: {} };
|
|
670
|
+
try {
|
|
671
|
+
const raw = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
672
|
+
if (typeof raw === "object" && raw !== null) {
|
|
673
|
+
return { mcpServers: {}, ...raw };
|
|
674
|
+
}
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
return { mcpServers: {} };
|
|
678
|
+
}
|
|
679
|
+
function writeJsonConfig(filePath, config) {
|
|
680
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
681
|
+
writeFileSync3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
682
|
+
}
|
|
683
|
+
function upsertJsonMcpServer(filePath, serverName, entry) {
|
|
684
|
+
const config = readJsonConfig(filePath);
|
|
685
|
+
config.mcpServers = { ...config.mcpServers, [serverName]: entry };
|
|
686
|
+
writeJsonConfig(filePath, config);
|
|
687
|
+
}
|
|
688
|
+
function removeJsonMcpServer(filePath, serverName) {
|
|
689
|
+
if (!existsSync3(filePath)) return;
|
|
690
|
+
const config = readJsonConfig(filePath);
|
|
691
|
+
delete config.mcpServers[serverName];
|
|
692
|
+
writeJsonConfig(filePath, config);
|
|
693
|
+
}
|
|
694
|
+
function serializeTomlValue(value) {
|
|
695
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
696
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
697
|
+
if (Array.isArray(value)) {
|
|
698
|
+
return "[" + value.map(serializeTomlValue).join(", ") + "]";
|
|
699
|
+
}
|
|
700
|
+
return JSON.stringify(value);
|
|
701
|
+
}
|
|
702
|
+
function buildCodexMcpSection(serverName, entry) {
|
|
703
|
+
const lines = [];
|
|
704
|
+
lines.push(`[mcp_servers.${serverName}]`);
|
|
705
|
+
if (entry.command) lines.push(`command = ${serializeTomlValue(entry.command)}`);
|
|
706
|
+
if (entry.args?.length) lines.push(`args = ${serializeTomlValue(entry.args)}`);
|
|
707
|
+
if (entry.url) lines.push(`url = ${serializeTomlValue(entry.url)}`);
|
|
708
|
+
if (entry.env && Object.keys(entry.env).length > 0) {
|
|
709
|
+
lines.push(`
|
|
710
|
+
[mcp_servers.${serverName}.env]`);
|
|
711
|
+
for (const [k, v] of Object.entries(entry.env)) {
|
|
712
|
+
lines.push(`${k} = ${serializeTomlValue(v)}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (entry.headers && Object.keys(entry.headers).length > 0) {
|
|
716
|
+
lines.push(`
|
|
717
|
+
[mcp_servers.${serverName}.headers]`);
|
|
718
|
+
for (const [k, v] of Object.entries(entry.headers)) {
|
|
719
|
+
lines.push(`${k} = ${serializeTomlValue(v)}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return lines.join("\n");
|
|
723
|
+
}
|
|
724
|
+
function upsertTomlMcpServer(filePath, serverName, entry) {
|
|
725
|
+
mkdirSync2(dirname2(filePath), { recursive: true });
|
|
726
|
+
let existing = "";
|
|
727
|
+
if (existsSync3(filePath)) {
|
|
728
|
+
existing = readFileSync3(filePath, "utf-8");
|
|
729
|
+
}
|
|
730
|
+
existing = removeTomlSection(existing, serverName);
|
|
731
|
+
const newSection = buildCodexMcpSection(serverName, entry);
|
|
732
|
+
const separator = existing.endsWith("\n\n") ? "" : existing.length > 0 ? "\n\n" : "";
|
|
733
|
+
writeFileSync3(filePath, existing + separator + newSection + "\n", "utf-8");
|
|
734
|
+
}
|
|
735
|
+
function removeTomlSection(content, serverName) {
|
|
736
|
+
const sectionPattern = new RegExp(
|
|
737
|
+
`\\[mcp_servers\\.${escapeRegex(serverName)}\\](?:\\n(?!\\[)[^\\n]*)*\\n?`,
|
|
738
|
+
"g"
|
|
739
|
+
);
|
|
740
|
+
const subSectionPattern = new RegExp(
|
|
741
|
+
`\\[mcp_servers\\.${escapeRegex(serverName)}\\.[^\\]]+\\](?:\\n(?!\\[)[^\\n]*)*\\n?`,
|
|
742
|
+
"g"
|
|
743
|
+
);
|
|
744
|
+
return content.replace(sectionPattern, "").replace(subSectionPattern, "").replace(/\n{3,}/g, "\n\n");
|
|
745
|
+
}
|
|
746
|
+
function removeTomlMcpServer(filePath, serverName) {
|
|
747
|
+
if (!existsSync3(filePath)) return;
|
|
748
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
749
|
+
const updated = removeTomlSection(content, serverName);
|
|
750
|
+
writeFileSync3(filePath, updated, "utf-8");
|
|
751
|
+
}
|
|
752
|
+
function escapeRegex(s) {
|
|
753
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
754
|
+
}
|
|
755
|
+
function getMcpConfigPath(target, cwd) {
|
|
756
|
+
const home = homedir2();
|
|
757
|
+
switch (target) {
|
|
758
|
+
case "cursor":
|
|
759
|
+
return join3(cwd, ".cursor", "mcp.json");
|
|
760
|
+
case "claude-code":
|
|
761
|
+
return join3(cwd, ".mcp.json");
|
|
762
|
+
case "codex":
|
|
763
|
+
return join3(cwd, ".codex", "config.toml");
|
|
764
|
+
case "gemini-cli":
|
|
765
|
+
return join3(cwd, ".gemini", "settings.json");
|
|
766
|
+
case "windsurf": {
|
|
767
|
+
if (platform() === "win32") {
|
|
768
|
+
return join3(process.env.USERPROFILE ?? home, ".codeium", "windsurf", "mcp_config.json");
|
|
769
|
+
}
|
|
770
|
+
return join3(home, ".codeium", "windsurf", "mcp_config.json");
|
|
771
|
+
}
|
|
772
|
+
default:
|
|
773
|
+
return join3(cwd, ".mcp.json");
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
function upsertMcpServer(target, cwd, serverName, entry) {
|
|
777
|
+
const configPath = getMcpConfigPath(target, cwd);
|
|
778
|
+
if (target === "codex") {
|
|
779
|
+
upsertTomlMcpServer(configPath, serverName, entry);
|
|
780
|
+
} else {
|
|
781
|
+
upsertJsonMcpServer(configPath, serverName, entry);
|
|
782
|
+
}
|
|
783
|
+
return configPath;
|
|
784
|
+
}
|
|
785
|
+
function removeMcpServer(target, cwd, serverName) {
|
|
786
|
+
const configPath = getMcpConfigPath(target, cwd);
|
|
787
|
+
if (target === "codex") {
|
|
788
|
+
removeTomlMcpServer(configPath, serverName);
|
|
789
|
+
} else {
|
|
790
|
+
removeJsonMcpServer(configPath, serverName);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function mcpServerName(vendorSlug, packageSlug) {
|
|
794
|
+
return `${vendorSlug}-${packageSlug}`;
|
|
795
|
+
}
|
|
796
|
+
function interpolateMcpArgs(args, version) {
|
|
797
|
+
return args.map((a) => a.replace(/\{version\}/g, version));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/commands/install.ts
|
|
801
|
+
function safeJoin(base, unsafePath) {
|
|
802
|
+
const resolved = resolve(base, unsafePath);
|
|
803
|
+
if (!resolved.startsWith(resolve(base) + "/") && resolved !== resolve(base)) {
|
|
804
|
+
throw new Error(`Path traversal detected: ${unsafePath}`);
|
|
805
|
+
}
|
|
806
|
+
return resolved;
|
|
807
|
+
}
|
|
808
|
+
function detectTarget() {
|
|
809
|
+
const cwd = process.cwd();
|
|
810
|
+
if (existsSync4(join4(cwd, ".cursor"))) return "cursor";
|
|
811
|
+
if (existsSync4(join4(cwd, ".claude"))) return "claude-code";
|
|
812
|
+
if (existsSync4(join4(cwd, ".windsurf"))) return "windsurf";
|
|
813
|
+
if (existsSync4(join4(cwd, ".gemini"))) return "gemini-cli";
|
|
814
|
+
if (existsSync4(join4(cwd, ".codex"))) return "codex";
|
|
815
|
+
return "generic";
|
|
816
|
+
}
|
|
817
|
+
async function promptEnvVar(varName, description) {
|
|
818
|
+
return new Promise((resolve3) => {
|
|
819
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
820
|
+
const hint = description ? chalk5.dim(` (${description})`) : "";
|
|
821
|
+
rl.question(` ${chalk5.cyan(varName)}${hint}: `, (answer) => {
|
|
822
|
+
rl.close();
|
|
823
|
+
resolve3(answer.trim());
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
async function collectEnvVars(envVarDefs, provided, interactive) {
|
|
828
|
+
const result = { ...provided };
|
|
829
|
+
for (const def of envVarDefs) {
|
|
830
|
+
if (result[def.name]) continue;
|
|
831
|
+
if (!def.required) continue;
|
|
832
|
+
if (!interactive) {
|
|
833
|
+
fail(
|
|
834
|
+
"install",
|
|
835
|
+
`Required env var ${def.name} not provided. Use --env ${def.name}=<value> to supply it non-interactively.`
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
const value = await promptEnvVar(def.name, def.description);
|
|
839
|
+
if (!value) {
|
|
840
|
+
fail("install", `${def.name} is required but was left blank.`);
|
|
841
|
+
}
|
|
842
|
+
result[def.name] = value;
|
|
843
|
+
}
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
async function downloadAndVerify(url, expectedSha256) {
|
|
847
|
+
const res = await fetch(url);
|
|
848
|
+
if (!res.ok) throw new Error(`Download failed: ${res.status} ${res.statusText}`);
|
|
849
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
850
|
+
const actual = createHash("sha256").update(buffer).digest("hex");
|
|
851
|
+
if (actual !== expectedSha256) {
|
|
852
|
+
throw new Error(`Hash mismatch! Expected ${expectedSha256}, got ${actual}`);
|
|
853
|
+
}
|
|
854
|
+
return buffer;
|
|
855
|
+
}
|
|
856
|
+
var installCommand = new Command4("install").description("Install a package from the mog marketplace").argument("<package>", "Package to install (e.g. acme/router-eval)").option("--target <target>", "Installation target: cursor, claude-code, codex, gemini-cli, windsurf, generic").option("--auto-buy", "Automatically purchase if not owned").option("--max-price <cents>", "Maximum price in cents when using --auto-buy").option("--preflight", "Show what would happen without making changes").option("--dir <path>", "Installation directory (default: current directory)").option("--env <KEY=VALUE>", "Set an env var for MCP packages (repeatable)", (v, prev) => [...prev, v], []).option("--json", "Output as JSON").action(
|
|
857
|
+
async (pkg, opts) => {
|
|
858
|
+
if (opts.json) setJsonMode(true);
|
|
859
|
+
const creds = readCredentials();
|
|
860
|
+
if (!creds?.token) {
|
|
861
|
+
fail("install", "Not authenticated. Run: mog auth");
|
|
862
|
+
}
|
|
863
|
+
const [vendor, slug] = pkg.split("/");
|
|
864
|
+
if (!vendor || !slug) {
|
|
865
|
+
fail("install", `Invalid package format: "${pkg}". Expected: vendor/slug`);
|
|
866
|
+
}
|
|
867
|
+
const cwd = opts.dir ?? process.cwd();
|
|
868
|
+
const target = opts.target ?? detectTarget();
|
|
869
|
+
info(chalk5.dim(`Target: ${target}`));
|
|
870
|
+
let listing;
|
|
871
|
+
try {
|
|
872
|
+
listing = await api.get(`/v1/listings/${vendor}/${slug}`);
|
|
873
|
+
} catch (err) {
|
|
874
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
875
|
+
fail("install", `Package "${pkg}" not found`);
|
|
876
|
+
}
|
|
877
|
+
fail("install", `Failed to fetch package: ${String(err)}`);
|
|
878
|
+
}
|
|
879
|
+
if (!listing.targets.includes(target) && !listing.targets.includes("generic")) {
|
|
880
|
+
warn(`Package may not support target "${target}". Proceeding with generic install.`);
|
|
881
|
+
}
|
|
882
|
+
const releasesData = await api.get(
|
|
883
|
+
`/v1/listings/${vendor}/${slug}/releases`
|
|
884
|
+
);
|
|
885
|
+
const [latestRelease] = releasesData.releases;
|
|
886
|
+
if (!latestRelease) {
|
|
887
|
+
fail("install", "No published releases available");
|
|
888
|
+
}
|
|
889
|
+
const entCheck = await api.get(
|
|
890
|
+
`/v1/entitlements/${listing.id}`
|
|
891
|
+
).catch(() => ({ owned: false, entitlement: void 0 }));
|
|
892
|
+
if (!entCheck.owned) {
|
|
893
|
+
if (!opts.autoBuy && listing.priceCents > 0) {
|
|
894
|
+
fail(
|
|
895
|
+
"install",
|
|
896
|
+
`You don't own "${pkg}". Purchase it first with: mog buy ${pkg}
|
|
897
|
+
Or add --auto-buy to install and purchase in one step.`
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
const purchaseBody = {
|
|
901
|
+
listingId: listing.id,
|
|
902
|
+
releaseId: latestRelease.id
|
|
903
|
+
};
|
|
904
|
+
if (opts.maxPrice) purchaseBody.maxPriceCents = parseInt(opts.maxPrice, 10);
|
|
905
|
+
const buySpinner = ora3(listing.priceCents === 0 ? "Claiming free package..." : "Purchasing...").start();
|
|
906
|
+
let purchaseResult;
|
|
907
|
+
try {
|
|
908
|
+
purchaseResult = await api.post("/v1/purchases", purchaseBody);
|
|
909
|
+
buySpinner.stop();
|
|
910
|
+
} catch (err) {
|
|
911
|
+
buySpinner.stop();
|
|
912
|
+
if (err instanceof ApiError && err.status === 402) {
|
|
913
|
+
const body = err.body;
|
|
914
|
+
if (body?.approvalUrl) {
|
|
915
|
+
fail("install", body.reason ?? "Approval required", body.approvalUrl);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
fail("install", `Purchase failed: ${String(err)}`);
|
|
919
|
+
}
|
|
920
|
+
if (purchaseResult.status === "approval_required") {
|
|
921
|
+
fail("install", purchaseResult.reason ?? "Approval required", purchaseResult.approvalUrl);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
if (opts.preflight) {
|
|
925
|
+
const installPath2 = resolveInstallPath(void 0, target, `${vendor}/${slug}`);
|
|
926
|
+
info("\n" + chalk5.bold("Preflight plan:"));
|
|
927
|
+
info(` Package: ${pkg}@${latestRelease.version}`);
|
|
928
|
+
info(` Target: ${target}`);
|
|
929
|
+
info(` Install to: ${join4(cwd, installPath2)}`);
|
|
930
|
+
info(` SHA-256: ${latestRelease.archiveSha256}`);
|
|
931
|
+
info(` Price: ${listing.priceCents === 0 ? "Free" : `${listing.priceCents}\xA2`}
|
|
932
|
+
`);
|
|
933
|
+
success("install", {
|
|
934
|
+
preflight: true,
|
|
935
|
+
package: pkg,
|
|
936
|
+
version: latestRelease.version,
|
|
937
|
+
target,
|
|
938
|
+
installPath: join4(cwd, installPath2),
|
|
939
|
+
sha256: latestRelease.archiveSha256
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
const downloadSpinner = ora3("Fetching download link...").start();
|
|
943
|
+
let downloadToken;
|
|
944
|
+
try {
|
|
945
|
+
downloadToken = await api.post("/v1/downloads", {
|
|
946
|
+
releaseId: latestRelease.id
|
|
947
|
+
});
|
|
948
|
+
downloadSpinner.stop();
|
|
949
|
+
} catch (err) {
|
|
950
|
+
downloadSpinner.stop();
|
|
951
|
+
fail("install", `Failed to get download URL: ${String(err)}`);
|
|
952
|
+
}
|
|
953
|
+
const dlSpinner = ora3("Downloading...").start();
|
|
954
|
+
let buffer;
|
|
955
|
+
try {
|
|
956
|
+
buffer = await downloadAndVerify(downloadToken.url, downloadToken.sha256);
|
|
957
|
+
dlSpinner.succeed(chalk5.dim(`Downloaded (${(buffer.length / 1024).toFixed(1)}KB)`));
|
|
958
|
+
} catch (err) {
|
|
959
|
+
dlSpinner.fail("Download failed");
|
|
960
|
+
fail("install", String(err));
|
|
961
|
+
}
|
|
962
|
+
const zip = new AdmZip(buffer);
|
|
963
|
+
const mogYamlEntry = zip.getEntries().find((e) => e.entryName === "mog.yaml" || e.entryName.endsWith("/mog.yaml"));
|
|
964
|
+
let manifest = null;
|
|
965
|
+
if (mogYamlEntry) {
|
|
966
|
+
const parsed = MogYamlSchema.safeParse(yaml.load(mogYamlEntry.getData().toString("utf-8")));
|
|
967
|
+
if (parsed.success) manifest = parsed.data;
|
|
968
|
+
}
|
|
969
|
+
if (manifest?.type === "mcp") {
|
|
970
|
+
const mcpConfig = manifest.mcp;
|
|
971
|
+
const serverKey = mcpServerName(vendor, slug);
|
|
972
|
+
const providedEnv = {};
|
|
973
|
+
for (const pair of opts.env ?? []) {
|
|
974
|
+
const eqIdx = pair.indexOf("=");
|
|
975
|
+
if (eqIdx < 1) {
|
|
976
|
+
warn(`Ignoring malformed --env value: "${pair}" (expected KEY=VALUE)`);
|
|
977
|
+
continue;
|
|
978
|
+
}
|
|
979
|
+
providedEnv[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
|
|
980
|
+
}
|
|
981
|
+
if (opts.preflight) {
|
|
982
|
+
info("\n" + chalk5.bold("Preflight plan (MCP server):"));
|
|
983
|
+
info(` Package: ${pkg}@${latestRelease.version}`);
|
|
984
|
+
info(` Target: ${target}`);
|
|
985
|
+
info(` Server key: ${serverKey}`);
|
|
986
|
+
info(` Command: ${mcpConfig.command} ${mcpConfig.args?.join(" ") ?? ""}`);
|
|
987
|
+
if (mcpConfig.env?.length) {
|
|
988
|
+
info(` Env vars: ${mcpConfig.env.map((e) => `${e.name}${e.required ? "" : " (optional)"}`).join(", ")}`);
|
|
989
|
+
}
|
|
990
|
+
success("install", {
|
|
991
|
+
preflight: true,
|
|
992
|
+
package: pkg,
|
|
993
|
+
version: latestRelease.version,
|
|
994
|
+
target,
|
|
995
|
+
type: "mcp",
|
|
996
|
+
serverKey
|
|
997
|
+
});
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
const isInteractive = process.stdin.isTTY && !opts.json;
|
|
1001
|
+
if (mcpConfig.env?.length) {
|
|
1002
|
+
info(chalk5.bold("\nEnvironment variables required for this MCP server:"));
|
|
1003
|
+
}
|
|
1004
|
+
const resolvedEnv = await collectEnvVars(mcpConfig.env ?? [], providedEnv, isInteractive);
|
|
1005
|
+
const serverEntry = {
|
|
1006
|
+
command: mcpConfig.transport === "stdio" ? mcpConfig.command : void 0,
|
|
1007
|
+
args: mcpConfig.transport === "stdio" ? interpolateMcpArgs(mcpConfig.args ?? [], latestRelease.version) : void 0,
|
|
1008
|
+
url: mcpConfig.transport === "http" ? mcpConfig.url : void 0,
|
|
1009
|
+
headers: mcpConfig.transport === "http" ? mcpConfig.headers : void 0,
|
|
1010
|
+
env: Object.keys(resolvedEnv).length > 0 ? resolvedEnv : void 0
|
|
1011
|
+
};
|
|
1012
|
+
const configSpinner = ora3(`Configuring MCP server for ${target}...`).start();
|
|
1013
|
+
let configPath;
|
|
1014
|
+
try {
|
|
1015
|
+
configPath = upsertMcpServer(target, cwd, serverKey, serverEntry);
|
|
1016
|
+
configSpinner.succeed(chalk5.green(`MCP server configured`));
|
|
1017
|
+
} catch (err) {
|
|
1018
|
+
configSpinner.fail("Failed to write MCP config");
|
|
1019
|
+
fail("install", String(err));
|
|
1020
|
+
}
|
|
1021
|
+
addToLockfile({
|
|
1022
|
+
name: pkg,
|
|
1023
|
+
vendor,
|
|
1024
|
+
slug,
|
|
1025
|
+
version: latestRelease.version,
|
|
1026
|
+
listingId: listing.id,
|
|
1027
|
+
releaseId: latestRelease.id,
|
|
1028
|
+
entitlementId: entCheck.entitlement?.id ?? "",
|
|
1029
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1030
|
+
target,
|
|
1031
|
+
installPath: configPath,
|
|
1032
|
+
archiveSha256: downloadToken.sha256,
|
|
1033
|
+
files: [],
|
|
1034
|
+
updateChannel: "minor",
|
|
1035
|
+
installType: "mcp-config",
|
|
1036
|
+
mcpConfigPaths: [configPath]
|
|
1037
|
+
}, cwd);
|
|
1038
|
+
info(chalk5.dim(` \u2192 ${configPath}`));
|
|
1039
|
+
info(chalk5.dim(` Lockfile updated: mog.lock.json`));
|
|
1040
|
+
info(chalk5.yellow(` Restart your IDE for the MCP server to take effect.`));
|
|
1041
|
+
success("install", {
|
|
1042
|
+
package: pkg,
|
|
1043
|
+
version: latestRelease.version,
|
|
1044
|
+
target,
|
|
1045
|
+
type: "mcp",
|
|
1046
|
+
serverKey,
|
|
1047
|
+
configPath
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
const installPath = resolveInstallPath(
|
|
1052
|
+
manifest?.install_map,
|
|
1053
|
+
target,
|
|
1054
|
+
slug
|
|
1055
|
+
);
|
|
1056
|
+
const fullInstallPath = join4(cwd, installPath);
|
|
1057
|
+
const extractSpinner = ora3(`Installing to ${installPath}...`).start();
|
|
1058
|
+
const extractedPaths = [];
|
|
1059
|
+
try {
|
|
1060
|
+
mkdirSync3(fullInstallPath, { recursive: true });
|
|
1061
|
+
const files = [];
|
|
1062
|
+
for (const entry of zip.getEntries()) {
|
|
1063
|
+
if (entry.isDirectory) continue;
|
|
1064
|
+
const data = entry.getData();
|
|
1065
|
+
const hash = createHash("sha256").update(data).digest("hex");
|
|
1066
|
+
const entryName = entry.entryName.replace(/^[^/]+\//, "");
|
|
1067
|
+
const destPath = safeJoin(fullInstallPath, entryName);
|
|
1068
|
+
mkdirSync3(dirname3(destPath), { recursive: true });
|
|
1069
|
+
writeFileSync4(destPath, data);
|
|
1070
|
+
extractedPaths.push(destPath);
|
|
1071
|
+
files.push({ path: entryName, sha256: hash, size: data.length });
|
|
1072
|
+
}
|
|
1073
|
+
extractSpinner.succeed(chalk5.green(`Installed ${pkg}@${latestRelease.version}`));
|
|
1074
|
+
try {
|
|
1075
|
+
addToLockfile({
|
|
1076
|
+
name: pkg,
|
|
1077
|
+
vendor,
|
|
1078
|
+
slug,
|
|
1079
|
+
version: latestRelease.version,
|
|
1080
|
+
listingId: listing.id,
|
|
1081
|
+
releaseId: latestRelease.id,
|
|
1082
|
+
entitlementId: entCheck.entitlement?.id ?? "",
|
|
1083
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1084
|
+
target,
|
|
1085
|
+
installPath: fullInstallPath,
|
|
1086
|
+
archiveSha256: downloadToken.sha256,
|
|
1087
|
+
files,
|
|
1088
|
+
updateChannel: "minor",
|
|
1089
|
+
installType: "files"
|
|
1090
|
+
}, cwd);
|
|
1091
|
+
} catch (lockErr) {
|
|
1092
|
+
for (const p of extractedPaths) {
|
|
1093
|
+
try {
|
|
1094
|
+
rmSync(p);
|
|
1095
|
+
} catch {
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
throw lockErr;
|
|
1099
|
+
}
|
|
1100
|
+
info(chalk5.dim(` \u2192 ${fullInstallPath}`));
|
|
1101
|
+
info(chalk5.dim(` Lockfile updated: mog.lock.json`));
|
|
1102
|
+
success("install", {
|
|
1103
|
+
package: pkg,
|
|
1104
|
+
version: latestRelease.version,
|
|
1105
|
+
target,
|
|
1106
|
+
installPath: fullInstallPath,
|
|
1107
|
+
files: files.length
|
|
1108
|
+
});
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
extractSpinner.fail("Installation failed");
|
|
1111
|
+
fail("install", String(err));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
);
|
|
1115
|
+
|
|
1116
|
+
// src/commands/ls.ts
|
|
1117
|
+
import { Command as Command5 } from "commander";
|
|
1118
|
+
import chalk6 from "chalk";
|
|
1119
|
+
var lsCommand = new Command5("ls").description("List installed packages").option("--dir <path>", "Project directory (default: current directory)").option("--json", "Output as JSON").action(async (opts) => {
|
|
1120
|
+
if (opts.json) setJsonMode(true);
|
|
1121
|
+
const cwd = opts.dir ?? process.cwd();
|
|
1122
|
+
const lockfile = readLockfile(cwd);
|
|
1123
|
+
const packages = Object.values(lockfile.packages);
|
|
1124
|
+
if (opts.json) {
|
|
1125
|
+
success("ls", { packages, count: packages.length });
|
|
1126
|
+
}
|
|
1127
|
+
if (packages.length === 0) {
|
|
1128
|
+
info(chalk6.dim("No packages installed. Run: mog install <vendor>/<package>"));
|
|
1129
|
+
process.exit(0);
|
|
1130
|
+
}
|
|
1131
|
+
console.log(chalk6.bold(`
|
|
1132
|
+
${packages.length} installed package${packages.length !== 1 ? "s" : ""}
|
|
1133
|
+
`));
|
|
1134
|
+
for (const pkg of packages) {
|
|
1135
|
+
console.log(
|
|
1136
|
+
chalk6.bold(`${pkg.vendor}/${pkg.slug}`) + chalk6.dim(`@${pkg.version}`) + ` ${chalk6.cyan(pkg.target)}`
|
|
1137
|
+
);
|
|
1138
|
+
console.log(chalk6.dim(` ${pkg.installPath}`));
|
|
1139
|
+
console.log(chalk6.dim(` Installed: ${new Date(pkg.installedAt).toLocaleDateString()}`));
|
|
1140
|
+
console.log();
|
|
1141
|
+
}
|
|
1142
|
+
process.exit(0);
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
// src/commands/update.ts
|
|
1146
|
+
import { Command as Command6 } from "commander";
|
|
1147
|
+
import chalk7 from "chalk";
|
|
1148
|
+
import ora4 from "ora";
|
|
1149
|
+
import { createHash as createHash2 } from "crypto";
|
|
1150
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
1151
|
+
import { join as join5, dirname as dirname4 } from "path";
|
|
1152
|
+
import AdmZip2 from "adm-zip";
|
|
1153
|
+
import semver from "semver";
|
|
1154
|
+
var updateCommand = new Command6("update").description("Update installed packages to latest compatible versions").argument("[package]", "Specific package to update (e.g. acme/router-eval)").option("--dir <path>", "Project directory (default: current directory)").option("--dry-run", "Show available updates without installing").option("--json", "Output as JSON").action(async (pkg, opts) => {
|
|
1155
|
+
if (opts.json) setJsonMode(true);
|
|
1156
|
+
const creds = readCredentials();
|
|
1157
|
+
if (!creds?.token) {
|
|
1158
|
+
fail("update", "Not authenticated. Run: mog auth");
|
|
1159
|
+
}
|
|
1160
|
+
const cwd = opts.dir ?? process.cwd();
|
|
1161
|
+
const lockfile = readLockfile(cwd);
|
|
1162
|
+
const packages = Object.values(lockfile.packages);
|
|
1163
|
+
if (packages.length === 0) {
|
|
1164
|
+
info(chalk7.dim("No installed packages to update"));
|
|
1165
|
+
success("update", { updated: [], skipped: [] });
|
|
1166
|
+
}
|
|
1167
|
+
const toCheck = pkg ? packages.filter((p) => `${p.vendor}/${p.slug}` === pkg) : packages;
|
|
1168
|
+
if (toCheck.length === 0) {
|
|
1169
|
+
fail("update", `Package "${pkg}" not found in lockfile`);
|
|
1170
|
+
}
|
|
1171
|
+
const updates = [];
|
|
1172
|
+
const skipped = [];
|
|
1173
|
+
for (const installed of toCheck) {
|
|
1174
|
+
const name = `${installed.vendor}/${installed.slug}`;
|
|
1175
|
+
try {
|
|
1176
|
+
const releasesData = await api.get(
|
|
1177
|
+
`/v1/listings/${installed.vendor}/${installed.slug}/releases`
|
|
1178
|
+
);
|
|
1179
|
+
const [latest] = releasesData.releases;
|
|
1180
|
+
if (!latest) {
|
|
1181
|
+
skipped.push({ name, version: installed.version, reason: "No releases found" });
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (latest.version === installed.version) {
|
|
1185
|
+
info(chalk7.dim(` ${name}: already at ${installed.version}`));
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
const currentVersion = installed.version;
|
|
1189
|
+
const newVersion = latest.version;
|
|
1190
|
+
const channel = installed.updateChannel ?? "minor";
|
|
1191
|
+
if (channel === "patch" && !semver.satisfies(newVersion, `~${currentVersion}`)) {
|
|
1192
|
+
skipped.push({ name, version: newVersion, reason: `Blocked by update channel (patch only). New: ${newVersion}` });
|
|
1193
|
+
warn(` ${name}: ${newVersion} available but blocked (patch-only channel). Use --major to upgrade.`);
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
if (channel === "minor" && semver.major(newVersion) > semver.major(currentVersion)) {
|
|
1197
|
+
skipped.push({ name, version: newVersion, reason: `Major upgrade requires explicit opt-in. New: ${newVersion}` });
|
|
1198
|
+
warn(` ${name}: ${newVersion} available but blocked (major upgrade). Add @${newVersion} to upgrade.`);
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
updates.push({ name, from: currentVersion, to: newVersion });
|
|
1202
|
+
if (opts.dryRun) {
|
|
1203
|
+
info(` ${chalk7.bold(name)}: ${chalk7.dim(currentVersion)} \u2192 ${chalk7.green(newVersion)}`);
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
const spinner = ora4(`Updating ${name}...`).start();
|
|
1207
|
+
try {
|
|
1208
|
+
const downloadToken = await api.post("/v1/downloads", {
|
|
1209
|
+
releaseId: latest.id
|
|
1210
|
+
});
|
|
1211
|
+
const res = await fetch(downloadToken.url);
|
|
1212
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
1213
|
+
const actualHash = createHash2("sha256").update(buffer).digest("hex");
|
|
1214
|
+
if (actualHash !== downloadToken.sha256) {
|
|
1215
|
+
throw new Error("Hash verification failed");
|
|
1216
|
+
}
|
|
1217
|
+
const zip = new AdmZip2(buffer);
|
|
1218
|
+
const fullInstallPath = installed.installPath;
|
|
1219
|
+
mkdirSync4(fullInstallPath, { recursive: true });
|
|
1220
|
+
const files = [];
|
|
1221
|
+
for (const entry of zip.getEntries()) {
|
|
1222
|
+
if (entry.isDirectory) continue;
|
|
1223
|
+
const data = entry.getData();
|
|
1224
|
+
const hash = createHash2("sha256").update(data).digest("hex");
|
|
1225
|
+
const entryName = entry.entryName.replace(/^[^/]+\//, "");
|
|
1226
|
+
const destPath = join5(fullInstallPath, entryName);
|
|
1227
|
+
mkdirSync4(dirname4(destPath), { recursive: true });
|
|
1228
|
+
writeFileSync5(destPath, data);
|
|
1229
|
+
files.push({ path: entryName, sha256: hash, size: data.length });
|
|
1230
|
+
}
|
|
1231
|
+
addToLockfile({
|
|
1232
|
+
...installed,
|
|
1233
|
+
version: latest.version,
|
|
1234
|
+
releaseId: latest.id,
|
|
1235
|
+
archiveSha256: downloadToken.sha256,
|
|
1236
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1237
|
+
files
|
|
1238
|
+
}, cwd);
|
|
1239
|
+
spinner.succeed(`${name}: ${currentVersion} \u2192 ${chalk7.green(newVersion)}`);
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
spinner.fail(`Failed to update ${name}`);
|
|
1242
|
+
warn(String(err));
|
|
1243
|
+
}
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
skipped.push({ name, version: installed.version, reason: String(err) });
|
|
1246
|
+
warn(` ${name}: ${String(err)}`);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
if (opts.dryRun) {
|
|
1250
|
+
info(chalk7.dim(`
|
|
1251
|
+
${updates.length} update(s) available. Remove --dry-run to apply.`));
|
|
1252
|
+
} else if (updates.length === 0) {
|
|
1253
|
+
info(chalk7.green("\n\u2713 All packages up to date"));
|
|
1254
|
+
}
|
|
1255
|
+
success("update", { updated: updates, skipped });
|
|
1256
|
+
});
|
|
1257
|
+
|
|
1258
|
+
// src/commands/uninstall.ts
|
|
1259
|
+
import { Command as Command7 } from "commander";
|
|
1260
|
+
import chalk8 from "chalk";
|
|
1261
|
+
import { rmSync as rmSync2, existsSync as existsSync6 } from "fs";
|
|
1262
|
+
var uninstallCommand = new Command7("uninstall").description("Remove an installed package and update the lockfile").argument("<package>", "Package to uninstall (e.g. acme/router-eval)").option("--dir <path>", "Installation directory (default: current directory)").option("--json", "Output as JSON").action(async (pkg, opts) => {
|
|
1263
|
+
if (opts.json) setJsonMode(true);
|
|
1264
|
+
const [vendor, slug] = pkg.split("/");
|
|
1265
|
+
if (!vendor || !slug) {
|
|
1266
|
+
fail("uninstall", `Invalid package format: "${pkg}". Expected: vendor/slug`);
|
|
1267
|
+
}
|
|
1268
|
+
const cwd = opts.dir ?? process.cwd();
|
|
1269
|
+
const lockfile = readLockfile(cwd);
|
|
1270
|
+
const key = `${vendor}/${slug}`;
|
|
1271
|
+
const entry = lockfile.packages[key];
|
|
1272
|
+
if (!entry) {
|
|
1273
|
+
fail("uninstall", `Package "${pkg}" is not in the lockfile`);
|
|
1274
|
+
}
|
|
1275
|
+
if (entry.installType === "mcp-config") {
|
|
1276
|
+
const serverKey = mcpServerName(vendor, slug);
|
|
1277
|
+
info(chalk8.dim(`Removing MCP server "${serverKey}" from ${entry.target} config\u2026`));
|
|
1278
|
+
try {
|
|
1279
|
+
removeMcpServer(entry.target, cwd, serverKey);
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
info(chalk8.dim(`Could not remove from config file: ${String(err)}`));
|
|
1282
|
+
}
|
|
1283
|
+
removeFromLockfile(key, cwd);
|
|
1284
|
+
info(chalk8.yellow(` Restart your IDE for the change to take effect.`));
|
|
1285
|
+
success("uninstall", {
|
|
1286
|
+
package: pkg,
|
|
1287
|
+
type: "mcp",
|
|
1288
|
+
serverKey,
|
|
1289
|
+
removedPath: entry.installPath ?? null
|
|
1290
|
+
});
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const installPath = entry.installPath;
|
|
1294
|
+
if (installPath && existsSync6(installPath)) {
|
|
1295
|
+
info(chalk8.dim(`Removing ${installPath}\u2026`));
|
|
1296
|
+
rmSync2(installPath, { recursive: true, force: true });
|
|
1297
|
+
} else {
|
|
1298
|
+
info(chalk8.dim(`Install path not found, removing from lockfile only`));
|
|
1299
|
+
}
|
|
1300
|
+
removeFromLockfile(key, cwd);
|
|
1301
|
+
success("uninstall", {
|
|
1302
|
+
package: pkg,
|
|
1303
|
+
removedPath: installPath ?? null
|
|
1304
|
+
});
|
|
1305
|
+
});
|
|
1306
|
+
|
|
1307
|
+
// src/commands/publish.ts
|
|
1308
|
+
import { Command as Command8 } from "commander";
|
|
1309
|
+
import chalk9 from "chalk";
|
|
1310
|
+
import ora5 from "ora";
|
|
1311
|
+
import { createHash as createHash3 } from "crypto";
|
|
1312
|
+
import { readFileSync as readFileSync4, readdirSync, statSync, existsSync as existsSync7 } from "fs";
|
|
1313
|
+
import { join as join6, relative, resolve as resolve2 } from "path";
|
|
1314
|
+
import AdmZip3 from "adm-zip";
|
|
1315
|
+
import yaml2 from "js-yaml";
|
|
1316
|
+
function addDirectoryToZip(zip, dirPath, baseDir) {
|
|
1317
|
+
const entries = readdirSync(dirPath);
|
|
1318
|
+
for (const entry of entries) {
|
|
1319
|
+
if (entry === ".git" || entry === "node_modules" || entry === ".DS_Store") continue;
|
|
1320
|
+
const fullPath = join6(dirPath, entry);
|
|
1321
|
+
const relativePath = relative(baseDir, fullPath);
|
|
1322
|
+
const stat = statSync(fullPath);
|
|
1323
|
+
if (stat.isDirectory()) {
|
|
1324
|
+
addDirectoryToZip(zip, fullPath, baseDir);
|
|
1325
|
+
} else {
|
|
1326
|
+
const content = readFileSync4(fullPath);
|
|
1327
|
+
zip.addFile(relativePath, content);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
function sha256Hex(buf) {
|
|
1332
|
+
return createHash3("sha256").update(buf).digest("hex");
|
|
1333
|
+
}
|
|
1334
|
+
var publishCommand = new Command8("publish").description("Build, upload, and publish a package from a local directory").option("--dir <path>", "Package directory (default: current directory)").option("--price <dollars>", "Price in USD (0 for free, $0.99\u2013$500 for paid). Only applied on first upload.").option("--yes", "Skip confirmation prompts").option("--json", "Output as JSON").action(
|
|
1335
|
+
async (opts) => {
|
|
1336
|
+
if (opts.json) setJsonMode(true);
|
|
1337
|
+
const creds = readCredentials();
|
|
1338
|
+
if (!creds?.token) {
|
|
1339
|
+
fail("publish", "Not authenticated. Run: mog auth");
|
|
1340
|
+
}
|
|
1341
|
+
const pkgDir = resolve2(opts.dir ?? process.cwd());
|
|
1342
|
+
if (!existsSync7(pkgDir)) {
|
|
1343
|
+
fail("publish", `Directory not found: ${pkgDir}`);
|
|
1344
|
+
}
|
|
1345
|
+
const mogYamlPath = join6(pkgDir, "mog.yaml");
|
|
1346
|
+
if (!existsSync7(mogYamlPath)) {
|
|
1347
|
+
fail("publish", `mog.yaml not found in ${pkgDir}`);
|
|
1348
|
+
}
|
|
1349
|
+
let rawYaml;
|
|
1350
|
+
try {
|
|
1351
|
+
rawYaml = yaml2.load(readFileSync4(mogYamlPath, "utf-8"));
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
fail("publish", `Failed to parse mog.yaml: ${String(err)}`);
|
|
1354
|
+
}
|
|
1355
|
+
const parsed = MogYamlSchema.safeParse(rawYaml);
|
|
1356
|
+
if (!parsed.success) {
|
|
1357
|
+
const errors = Object.entries(parsed.error.flatten().fieldErrors).map(([k, v]) => ` ${k}: ${v.join(", ")}`).join("\n");
|
|
1358
|
+
fail("publish", `Invalid mog.yaml:
|
|
1359
|
+
${errors}`);
|
|
1360
|
+
}
|
|
1361
|
+
const manifest = parsed.data;
|
|
1362
|
+
info(chalk9.bold(`
|
|
1363
|
+
Publishing ${chalk9.cyan(manifest.name)} v${chalk9.yellow(manifest.version)}`));
|
|
1364
|
+
info(chalk9.dim(` Type: ${manifest.type}`));
|
|
1365
|
+
info(chalk9.dim(` Targets: ${manifest.targets.join(", ")}`));
|
|
1366
|
+
const buildSpinner = ora5("Building archive\u2026").start();
|
|
1367
|
+
let buffer;
|
|
1368
|
+
try {
|
|
1369
|
+
const zip = new AdmZip3();
|
|
1370
|
+
addDirectoryToZip(zip, pkgDir, pkgDir);
|
|
1371
|
+
buffer = zip.toBuffer();
|
|
1372
|
+
buildSpinner.succeed(chalk9.dim(`Archive built (${(buffer.length / 1024).toFixed(1)} KB, ${sha256Hex(buffer).slice(0, 12)}\u2026)`));
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
buildSpinner.fail("Build failed");
|
|
1375
|
+
fail("publish", String(err));
|
|
1376
|
+
}
|
|
1377
|
+
if (!opts.yes && !opts.json) {
|
|
1378
|
+
const readline = await import("readline/promises");
|
|
1379
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1380
|
+
const ans = await rl.question(chalk9.yellow(`
|
|
1381
|
+
Upload and publish? [y/N] `));
|
|
1382
|
+
rl.close();
|
|
1383
|
+
if (!ans.toLowerCase().startsWith("y")) {
|
|
1384
|
+
info(chalk9.dim("Aborted."));
|
|
1385
|
+
process.exit(0);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const uploadSpinner = ora5("Uploading\u2026").start();
|
|
1389
|
+
let uploaded;
|
|
1390
|
+
try {
|
|
1391
|
+
const form = new FormData();
|
|
1392
|
+
form.append("archive", new Blob([buffer], { type: "application/zip" }), "package.zip");
|
|
1393
|
+
if (opts.price !== void 0) {
|
|
1394
|
+
const priceCents = Math.round(parseFloat(opts.price) * 100);
|
|
1395
|
+
if (isNaN(priceCents) || priceCents !== 0 && priceCents < MIN_PRICE_CENTS) {
|
|
1396
|
+
fail("publish", `Minimum price is $${(MIN_PRICE_CENTS / 100).toFixed(2)} (or $0 for free)`);
|
|
1397
|
+
}
|
|
1398
|
+
if (priceCents > MAX_PRICE_CENTS) {
|
|
1399
|
+
fail("publish", `Maximum price is $${(MAX_PRICE_CENTS / 100).toFixed(2)}`);
|
|
1400
|
+
}
|
|
1401
|
+
form.append("priceCents", String(priceCents));
|
|
1402
|
+
}
|
|
1403
|
+
uploaded = await api.upload("/v1/vendor/releases", form);
|
|
1404
|
+
uploadSpinner.succeed(chalk9.dim(`Uploaded release ${uploaded.release.id.slice(0, 8)}\u2026`));
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
uploadSpinner.fail("Upload failed");
|
|
1407
|
+
if (err instanceof ApiError) {
|
|
1408
|
+
fail("publish", err.message);
|
|
1409
|
+
}
|
|
1410
|
+
fail("publish", String(err));
|
|
1411
|
+
}
|
|
1412
|
+
const scanSpinner = ora5("Scanning package\u2026").start();
|
|
1413
|
+
const releaseId = uploaded.release.id;
|
|
1414
|
+
const listingSlug = uploaded.listing.slug;
|
|
1415
|
+
const [vendorSlug] = manifest.name.split("/");
|
|
1416
|
+
const POLL_INTERVAL_MS = 3e3;
|
|
1417
|
+
const POLL_TIMEOUT_MS = 12e4;
|
|
1418
|
+
const start = Date.now();
|
|
1419
|
+
let finalScanStatus = uploaded.release.scanStatus;
|
|
1420
|
+
let scanResult = null;
|
|
1421
|
+
while (finalScanStatus === "pending" || finalScanStatus === "scanning") {
|
|
1422
|
+
if (Date.now() - start > POLL_TIMEOUT_MS) {
|
|
1423
|
+
scanSpinner.fail("Scan timed out after 2 minutes");
|
|
1424
|
+
fail("publish", "Package scan is taking too long. Check the dashboard for status.");
|
|
1425
|
+
}
|
|
1426
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1427
|
+
try {
|
|
1428
|
+
const releasesData = await api.get(
|
|
1429
|
+
`/v1/listings/${vendorSlug}/${listingSlug}/releases`
|
|
1430
|
+
);
|
|
1431
|
+
const rel = releasesData.releases.find((r) => r.id === releaseId);
|
|
1432
|
+
if (rel) {
|
|
1433
|
+
finalScanStatus = rel.scanStatus;
|
|
1434
|
+
scanResult = rel.scanResult;
|
|
1435
|
+
}
|
|
1436
|
+
} catch {
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
if (finalScanStatus === "failed") {
|
|
1440
|
+
scanSpinner.fail("Scan failed");
|
|
1441
|
+
let reason = "Unknown reason";
|
|
1442
|
+
if (scanResult) {
|
|
1443
|
+
try {
|
|
1444
|
+
reason = JSON.parse(scanResult).reason ?? reason;
|
|
1445
|
+
} catch {
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
fail("publish", `Package failed security scan: ${reason}`);
|
|
1449
|
+
}
|
|
1450
|
+
scanSpinner.succeed(chalk9.green("Scan passed"));
|
|
1451
|
+
const publishSpinner = ora5("Publishing\u2026").start();
|
|
1452
|
+
try {
|
|
1453
|
+
await api.patch(`/v1/vendor/releases/${releaseId}/publish`);
|
|
1454
|
+
publishSpinner.succeed(chalk9.green(`Published ${manifest.name}@${manifest.version}`));
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
publishSpinner.fail("Publish failed");
|
|
1457
|
+
if (err instanceof ApiError) {
|
|
1458
|
+
fail("publish", err.message);
|
|
1459
|
+
}
|
|
1460
|
+
fail("publish", String(err));
|
|
1461
|
+
}
|
|
1462
|
+
info("");
|
|
1463
|
+
info(chalk9.bold("Install command:"));
|
|
1464
|
+
info(chalk9.cyan(` mog install ${manifest.name}`));
|
|
1465
|
+
info("");
|
|
1466
|
+
success("publish", {
|
|
1467
|
+
name: manifest.name,
|
|
1468
|
+
version: manifest.version,
|
|
1469
|
+
releaseId,
|
|
1470
|
+
listingId: uploaded.listing.id
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
// src/bin.ts
|
|
1476
|
+
program.name("mog").description("Marketplace for agent skills \u2014 buy and sell .md files autonomously").version("0.1.0");
|
|
1477
|
+
program.addCommand(authCommand);
|
|
1478
|
+
program.addCommand(searchCommand);
|
|
1479
|
+
program.addCommand(buyCommand);
|
|
1480
|
+
program.addCommand(installCommand);
|
|
1481
|
+
program.addCommand(uninstallCommand);
|
|
1482
|
+
program.addCommand(lsCommand);
|
|
1483
|
+
program.addCommand(updateCommand);
|
|
1484
|
+
program.addCommand(publishCommand);
|
|
1485
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
1486
|
+
console.error("Error:", err);
|
|
1487
|
+
process.exit(1);
|
|
1488
|
+
});
|