@myna-sh/cli 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/main.js +735 -11
- package/dist/main.js.map +1 -1
- package/package.json +3 -3
package/dist/main.js
CHANGED
|
@@ -431,6 +431,45 @@ var ManagementClient = class {
|
|
|
431
431
|
mutate(method, path, body, query) {
|
|
432
432
|
return this.http.request(method, path, { body, query, idempotencyKey: this.idem() });
|
|
433
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* Escape hatch: call any management endpoint, wrapped or not.
|
|
436
|
+
*
|
|
437
|
+
* The typed methods above cover the API, but never all of it at once — a
|
|
438
|
+
* route ships before its wrapper, or a caller needs a header the wrapper does
|
|
439
|
+
* not take. Without a documented way around the client, that caller hand-rolls
|
|
440
|
+
* `fetch` and loses everything this class provides: credential handling,
|
|
441
|
+
* retry policy, and RFC 9457 errors thrown as `MynaApiError` rather than left
|
|
442
|
+
* as an opaque body to parse. The escape hatch keeps all of it.
|
|
443
|
+
*
|
|
444
|
+
* `path` is version-relative (`/projects/x`), and a leading `/v1` is accepted
|
|
445
|
+
* and stripped — this is the one method whose paths are written by hand, and
|
|
446
|
+
* every example a caller copies from the docs carries the prefix.
|
|
447
|
+
*
|
|
448
|
+
* Unlike the typed mutations, no idempotency key is generated: sending one
|
|
449
|
+
* makes a request retry-eligible, and only the caller knows whether replaying
|
|
450
|
+
* this particular one is safe. Pass `idempotencyKey` to opt in.
|
|
451
|
+
*
|
|
452
|
+
* Returns the response envelope untouched — `{ data, pagination }` and all —
|
|
453
|
+
* because an escape hatch that reshapes the response is not one.
|
|
454
|
+
*/
|
|
455
|
+
async raw(method, path, options = {}) {
|
|
456
|
+
const relative = path.startsWith("/v1/") ? path.slice(3) : path.startsWith("/") ? path : `/${path}`;
|
|
457
|
+
const response = await this.http.requestRaw(method, relative, options);
|
|
458
|
+
const headers = {};
|
|
459
|
+
response.headers.forEach((value, key) => {
|
|
460
|
+
headers[key] = value;
|
|
461
|
+
});
|
|
462
|
+
if (response.status === 204 || response.status === 304) {
|
|
463
|
+
return { status: response.status, headers, body: void 0 };
|
|
464
|
+
}
|
|
465
|
+
const text = await response.text();
|
|
466
|
+
if (!text) return { status: response.status, headers, body: void 0 };
|
|
467
|
+
try {
|
|
468
|
+
return { status: response.status, headers, body: JSON.parse(text) };
|
|
469
|
+
} catch {
|
|
470
|
+
return { status: response.status, headers, body: text };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
434
473
|
// --- Organizations --------------------------------------------------------
|
|
435
474
|
organizations = {
|
|
436
475
|
list: (signal) => this.get("/organizations", void 0, signal),
|
|
@@ -971,6 +1010,24 @@ function readCredentialFile() {
|
|
|
971
1010
|
return {};
|
|
972
1011
|
}
|
|
973
1012
|
}
|
|
1013
|
+
function mcpConfigFile() {
|
|
1014
|
+
return process.env.MYNA_MCP_CONFIG ?? join(configDir(), "mcp.json");
|
|
1015
|
+
}
|
|
1016
|
+
function readMcpConfig() {
|
|
1017
|
+
try {
|
|
1018
|
+
return JSON.parse(readFileSync(mcpConfigFile(), "utf8"));
|
|
1019
|
+
} catch {
|
|
1020
|
+
return {};
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function writeMcpConfig(patch) {
|
|
1024
|
+
const next = { ...readMcpConfig(), ...patch };
|
|
1025
|
+
const file = mcpConfigFile();
|
|
1026
|
+
ensureDir(dirname(file));
|
|
1027
|
+
writeFileSync(file, JSON.stringify(next, null, 2) + "\n");
|
|
1028
|
+
chmodSync(file, 384);
|
|
1029
|
+
return next;
|
|
1030
|
+
}
|
|
974
1031
|
function resolveContext(flags) {
|
|
975
1032
|
const link = findLinkedProject();
|
|
976
1033
|
const user = readUserConfig();
|
|
@@ -3027,6 +3084,8 @@ var ID_PREFIXES = {
|
|
|
3027
3084
|
mcpAuthorizationCode: "mac",
|
|
3028
3085
|
mcpToken: "mtk",
|
|
3029
3086
|
deviceAuthorization: "dev",
|
|
3087
|
+
webauthnCredential: "pky",
|
|
3088
|
+
webauthnChallenge: "wac",
|
|
3030
3089
|
githubInstallation: "ghi",
|
|
3031
3090
|
projectRepository: "prp"
|
|
3032
3091
|
};
|
|
@@ -4064,9 +4123,671 @@ function registerBilling(program) {
|
|
|
4064
4123
|
);
|
|
4065
4124
|
}
|
|
4066
4125
|
|
|
4126
|
+
// src/commands/api.ts
|
|
4127
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
4128
|
+
var METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]);
|
|
4129
|
+
var SAFE = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
4130
|
+
function parseTarget(args) {
|
|
4131
|
+
const [first, second] = args;
|
|
4132
|
+
if (!first) throw new UsageError("Provide a path, e.g. `myna api /projects`.");
|
|
4133
|
+
const asMethod = first.toUpperCase();
|
|
4134
|
+
if (METHODS.has(asMethod)) {
|
|
4135
|
+
if (!second) throw new UsageError(`Provide a path after ${asMethod}, e.g. \`myna api ${asMethod} /projects\`.`);
|
|
4136
|
+
return { method: asMethod, path: second };
|
|
4137
|
+
}
|
|
4138
|
+
if (second) {
|
|
4139
|
+
throw new UsageError(
|
|
4140
|
+
`Unknown method "${first}". Expected one of ${[...METHODS].join(", ")}, or a single path argument.`
|
|
4141
|
+
);
|
|
4142
|
+
}
|
|
4143
|
+
return { method: "GET", path: first };
|
|
4144
|
+
}
|
|
4145
|
+
function parseBody(input) {
|
|
4146
|
+
if (input === void 0) return void 0;
|
|
4147
|
+
const raw = input.startsWith("@") ? readFileSync6(input.slice(1), "utf8") : input;
|
|
4148
|
+
try {
|
|
4149
|
+
return JSON.parse(raw);
|
|
4150
|
+
} catch (error) {
|
|
4151
|
+
throw new UsageError(`Invalid JSON for --data: ${error instanceof Error ? error.message : String(error)}`);
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
function parseHeaders(values) {
|
|
4155
|
+
const headers = {};
|
|
4156
|
+
for (const value of values ?? []) {
|
|
4157
|
+
const colon = value.indexOf(":");
|
|
4158
|
+
if (colon === -1) throw new UsageError(`Invalid --header "${value}". Expected name:value.`);
|
|
4159
|
+
headers[value.slice(0, colon).trim().toLowerCase()] = value.slice(colon + 1).trim();
|
|
4160
|
+
}
|
|
4161
|
+
return headers;
|
|
4162
|
+
}
|
|
4163
|
+
function registerApi(program) {
|
|
4164
|
+
program.command("api").description("Call a management API endpoint directly, using the resolved credential").argument("<method-or-path>", "HTTP method, or the path when the method is GET").argument("[path]", "request path, e.g. /projects/my-site/entries?collection=posts").option("--data <json>", "request body as inline JSON or @file").option("--header <name:value>", "extra request header (repeatable)", (value, previous = []) => [...previous, value], []).option(
|
|
4165
|
+
"--idempotency-key <key>",
|
|
4166
|
+
"send Idempotency-Key, which also makes the request eligible for automatic retry"
|
|
4167
|
+
).option("--include", "also report the response status and headers on stderr").action(
|
|
4168
|
+
handle(async (ctx, args, opts) => {
|
|
4169
|
+
const { method, path } = parseTarget(args);
|
|
4170
|
+
const body = parseBody(opts.data);
|
|
4171
|
+
if (body !== void 0 && SAFE.has(method)) {
|
|
4172
|
+
throw new UsageError(`--data cannot be sent with ${method}.`);
|
|
4173
|
+
}
|
|
4174
|
+
const response = await ctx.management().raw(method, path, {
|
|
4175
|
+
headers: parseHeaders(opts.header),
|
|
4176
|
+
...body !== void 0 ? { body } : {},
|
|
4177
|
+
...opts.idempotencyKey ? { idempotencyKey: opts.idempotencyKey } : {}
|
|
4178
|
+
});
|
|
4179
|
+
if (opts.include) {
|
|
4180
|
+
diag(`${response.status}`);
|
|
4181
|
+
for (const [name, value] of Object.entries(response.headers)) diag(`${name}: ${value}`);
|
|
4182
|
+
diag("");
|
|
4183
|
+
}
|
|
4184
|
+
emit(response.body ?? { status: response.status }, () => {
|
|
4185
|
+
process.stdout.write(
|
|
4186
|
+
(response.body === void 0 ? `${response.status} (no content)` : JSON.stringify(response.body, null, 2)) + "\n"
|
|
4187
|
+
);
|
|
4188
|
+
});
|
|
4189
|
+
})
|
|
4190
|
+
);
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
// src/agent-clients.ts
|
|
4194
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
4195
|
+
import { homedir as homedir2 } from "os";
|
|
4196
|
+
import { dirname as dirname3, join as join9 } from "path";
|
|
4197
|
+
var AGENT_CLIENTS = [
|
|
4198
|
+
{
|
|
4199
|
+
id: "claude-code",
|
|
4200
|
+
label: "Claude Code",
|
|
4201
|
+
dialect: "standard",
|
|
4202
|
+
projectPath: ".mcp.json",
|
|
4203
|
+
userPath: ".claude.json",
|
|
4204
|
+
marker: ".claude"
|
|
4205
|
+
},
|
|
4206
|
+
{
|
|
4207
|
+
id: "cursor",
|
|
4208
|
+
label: "Cursor",
|
|
4209
|
+
dialect: "standard",
|
|
4210
|
+
projectPath: ".cursor/mcp.json",
|
|
4211
|
+
userPath: ".cursor/mcp.json",
|
|
4212
|
+
marker: ".cursor"
|
|
4213
|
+
},
|
|
4214
|
+
{
|
|
4215
|
+
id: "vscode",
|
|
4216
|
+
label: "VS Code",
|
|
4217
|
+
dialect: "vscode",
|
|
4218
|
+
projectPath: ".vscode/mcp.json",
|
|
4219
|
+
marker: ".vscode"
|
|
4220
|
+
},
|
|
4221
|
+
{
|
|
4222
|
+
id: "windsurf",
|
|
4223
|
+
label: "Windsurf",
|
|
4224
|
+
dialect: "standard",
|
|
4225
|
+
userPath: ".codeium/windsurf/mcp_config.json",
|
|
4226
|
+
marker: ".codeium"
|
|
4227
|
+
},
|
|
4228
|
+
{
|
|
4229
|
+
id: "codex",
|
|
4230
|
+
label: "Codex",
|
|
4231
|
+
dialect: "toml",
|
|
4232
|
+
userPath: ".codex/config.toml",
|
|
4233
|
+
marker: ".codex",
|
|
4234
|
+
note: "Codex configuration is TOML; the CLI prints the block to paste rather than editing it."
|
|
4235
|
+
}
|
|
4236
|
+
];
|
|
4237
|
+
function findClient(id) {
|
|
4238
|
+
const client = AGENT_CLIENTS.find((c) => c.id === id);
|
|
4239
|
+
if (!client) {
|
|
4240
|
+
throw new UsageError(
|
|
4241
|
+
`Unknown client "${id}". Known clients: ${AGENT_CLIENTS.map((c) => c.id).join(", ")}.`
|
|
4242
|
+
);
|
|
4243
|
+
}
|
|
4244
|
+
return client;
|
|
4245
|
+
}
|
|
4246
|
+
function isDetected(client) {
|
|
4247
|
+
return existsSync7(join9(homedir2(), client.marker));
|
|
4248
|
+
}
|
|
4249
|
+
function scopesFor(client) {
|
|
4250
|
+
const scopes = [];
|
|
4251
|
+
if (client.projectPath) scopes.push("project");
|
|
4252
|
+
if (client.userPath) scopes.push("user");
|
|
4253
|
+
return scopes;
|
|
4254
|
+
}
|
|
4255
|
+
function configPath(client, scope, projectRoot) {
|
|
4256
|
+
if (scope === "project") {
|
|
4257
|
+
if (!client.projectPath) {
|
|
4258
|
+
throw new UsageError(
|
|
4259
|
+
`${client.label} has no project-scoped configuration file. Use --scope user.`
|
|
4260
|
+
);
|
|
4261
|
+
}
|
|
4262
|
+
return join9(projectRoot, client.projectPath);
|
|
4263
|
+
}
|
|
4264
|
+
if (!client.userPath) {
|
|
4265
|
+
throw new UsageError(
|
|
4266
|
+
`${client.label} has no user-scoped configuration file. Use --scope project.`
|
|
4267
|
+
);
|
|
4268
|
+
}
|
|
4269
|
+
return join9(homedir2(), client.userPath);
|
|
4270
|
+
}
|
|
4271
|
+
function serverEntry(client, spec) {
|
|
4272
|
+
if (spec.transport === "http") {
|
|
4273
|
+
return { type: "http", url: spec.url };
|
|
4274
|
+
}
|
|
4275
|
+
const stdio = {
|
|
4276
|
+
command: "npx",
|
|
4277
|
+
args: ["-y", "@myna-sh/mcp"]
|
|
4278
|
+
};
|
|
4279
|
+
if (client.dialect === "vscode") stdio.type = "stdio";
|
|
4280
|
+
if (Object.keys(spec.env).length > 0) stdio.env = spec.env;
|
|
4281
|
+
return stdio;
|
|
4282
|
+
}
|
|
4283
|
+
function tomlBlock(name, spec) {
|
|
4284
|
+
const lines = [`[mcp_servers.${name}]`];
|
|
4285
|
+
if (spec.transport === "http") {
|
|
4286
|
+
lines.push(`url = ${JSON.stringify(spec.url)}`);
|
|
4287
|
+
} else {
|
|
4288
|
+
lines.push(`command = "npx"`, `args = ["-y", "@myna-sh/mcp"]`);
|
|
4289
|
+
for (const [key, value] of Object.entries(spec.env)) {
|
|
4290
|
+
lines.push(`env.${key} = ${JSON.stringify(value)}`);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
return lines.join("\n");
|
|
4294
|
+
}
|
|
4295
|
+
function serversKey(client) {
|
|
4296
|
+
return client.dialect === "vscode" ? "servers" : "mcpServers";
|
|
4297
|
+
}
|
|
4298
|
+
function readDocument(path) {
|
|
4299
|
+
if (!existsSync7(path)) return {};
|
|
4300
|
+
const raw = readFileSync7(path, "utf8");
|
|
4301
|
+
if (raw.trim() === "") return {};
|
|
4302
|
+
try {
|
|
4303
|
+
const parsed = JSON.parse(raw);
|
|
4304
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
4305
|
+
throw new Error("expected a JSON object");
|
|
4306
|
+
}
|
|
4307
|
+
return parsed;
|
|
4308
|
+
} catch (error) {
|
|
4309
|
+
throw new CliError(
|
|
4310
|
+
`${path} is not valid JSON (${error instanceof Error ? error.message : String(error)}). Fix or move it; refusing to overwrite a file that may hold configuration.`
|
|
4311
|
+
);
|
|
4312
|
+
}
|
|
4313
|
+
}
|
|
4314
|
+
function existingEntry(client, path, name) {
|
|
4315
|
+
const servers = readDocument(path)[serversKey(client)];
|
|
4316
|
+
if (typeof servers !== "object" || servers === null) return void 0;
|
|
4317
|
+
const entry = servers[name];
|
|
4318
|
+
return typeof entry === "object" && entry !== null ? entry : void 0;
|
|
4319
|
+
}
|
|
4320
|
+
function writeEntry(client, path, name, entry) {
|
|
4321
|
+
const doc = readDocument(path);
|
|
4322
|
+
const key = serversKey(client);
|
|
4323
|
+
const servers = typeof doc[key] === "object" && doc[key] !== null ? doc[key] : {};
|
|
4324
|
+
servers[name] = entry;
|
|
4325
|
+
doc[key] = servers;
|
|
4326
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
4327
|
+
writeFileSync5(path, JSON.stringify(doc, null, 2) + "\n");
|
|
4328
|
+
}
|
|
4329
|
+
function removeEntry(client, path, name) {
|
|
4330
|
+
if (!existsSync7(path)) return false;
|
|
4331
|
+
const doc = readDocument(path);
|
|
4332
|
+
const key = serversKey(client);
|
|
4333
|
+
const servers = doc[key];
|
|
4334
|
+
if (typeof servers !== "object" || servers === null) return false;
|
|
4335
|
+
const map = servers;
|
|
4336
|
+
if (!(name in map)) return false;
|
|
4337
|
+
delete map[name];
|
|
4338
|
+
writeFileSync5(path, JSON.stringify(doc, null, 2) + "\n");
|
|
4339
|
+
return true;
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
// src/commands/mcp.ts
|
|
4343
|
+
var HOSTED_PATH = "/mcp";
|
|
4344
|
+
function resolveTransport(value) {
|
|
4345
|
+
if (value === void 0) return "stdio";
|
|
4346
|
+
if (value === "stdio" || value === "http") return value;
|
|
4347
|
+
throw new UsageError(`--transport must be "stdio" or "http" (got "${String(value)}").`);
|
|
4348
|
+
}
|
|
4349
|
+
function resolveScope(client, requested) {
|
|
4350
|
+
const supported = scopesFor(client);
|
|
4351
|
+
if (requested === void 0) return supported[0];
|
|
4352
|
+
if (requested !== "project" && requested !== "user") {
|
|
4353
|
+
throw new UsageError(`--scope must be "project" or "user" (got "${String(requested)}").`);
|
|
4354
|
+
}
|
|
4355
|
+
if (!supported.includes(requested)) {
|
|
4356
|
+
throw new UsageError(
|
|
4357
|
+
`${client.label} has no ${requested}-scoped configuration file. Supported: ${supported.join(", ")}.`
|
|
4358
|
+
);
|
|
4359
|
+
}
|
|
4360
|
+
return requested;
|
|
4361
|
+
}
|
|
4362
|
+
function resolveClients(names) {
|
|
4363
|
+
if (names.length > 0) return names.map(findClient);
|
|
4364
|
+
const detected = AGENT_CLIENTS.filter(isDetected);
|
|
4365
|
+
if (detected.length === 0) {
|
|
4366
|
+
throw new CliError(
|
|
4367
|
+
`No MCP client detected. Name one explicitly: ${AGENT_CLIENTS.map((c) => c.id).join(", ")}.`
|
|
4368
|
+
);
|
|
4369
|
+
}
|
|
4370
|
+
return detected;
|
|
4371
|
+
}
|
|
4372
|
+
function buildSpec(ctx, transport) {
|
|
4373
|
+
const env = {};
|
|
4374
|
+
if (ctx.apiUrl !== DEFAULT_API_URL2) env.MYNA_API_URL = ctx.apiUrl;
|
|
4375
|
+
if (ctx.organization) env.MYNA_ORGANIZATION = ctx.organization;
|
|
4376
|
+
if (ctx.project) env.MYNA_PROJECT = ctx.project;
|
|
4377
|
+
return { transport, url: `${ctx.apiUrl}${HOSTED_PATH}`, env };
|
|
4378
|
+
}
|
|
4379
|
+
function registerMcp(program) {
|
|
4380
|
+
const mcp = program.command("mcp").description("Connect coding agents to Myna's MCP server");
|
|
4381
|
+
mcp.command("install").description("Write Myna's MCP server into the configuration of one or more coding agents").argument("[clients...]", "client ids (default: every client detected on this machine)").option("--transport <mode>", "stdio (local server, default) or http (hosted server over OAuth)").option("--scope <scope>", "project or user (default: the most specific the client supports)").option("--name <name>", "server name in the client's configuration", "myna").option("--key <token>", "credential to store for the local server (default: the resolved credential)").option("--no-credential", "do not write ~/.config/myna/mcp.json").option("--dir <dir>", "project root for project-scoped files (default: the linked project or cwd)").option("--print", "show what would be written, and write nothing").option("--force", "replace an existing server entry of the same name").action(
|
|
4382
|
+
handle(async (ctx, args, opts) => {
|
|
4383
|
+
const transport = resolveTransport(opts.transport);
|
|
4384
|
+
const name = opts.name;
|
|
4385
|
+
const root = opts.dir ?? ctx.linkedRoot ?? process.cwd();
|
|
4386
|
+
const clients = resolveClients(args[0] ?? []);
|
|
4387
|
+
const spec = buildSpec(ctx, transport);
|
|
4388
|
+
const dryRun = Boolean(opts.print);
|
|
4389
|
+
const token = transport === "stdio" ? opts.key ?? ctx.token : void 0;
|
|
4390
|
+
if (transport === "stdio" && !token) {
|
|
4391
|
+
throw new CliError(
|
|
4392
|
+
"No credential to give the local MCP server. Run `myna login`, pass --key, or use --transport http."
|
|
4393
|
+
);
|
|
4394
|
+
}
|
|
4395
|
+
const results = [];
|
|
4396
|
+
for (const client of clients) {
|
|
4397
|
+
if (client.dialect === "toml") {
|
|
4398
|
+
results.push({ client: client.id, scope: null, path: client.userPath ?? "", action: "printed", reason: client.note });
|
|
4399
|
+
continue;
|
|
4400
|
+
}
|
|
4401
|
+
const scope = resolveScope(client, opts.scope);
|
|
4402
|
+
const path = configPath(client, scope, root);
|
|
4403
|
+
const entry = serverEntry(client, spec);
|
|
4404
|
+
if (!opts.force && existingEntry(client, path, name)) {
|
|
4405
|
+
results.push({
|
|
4406
|
+
client: client.id,
|
|
4407
|
+
scope,
|
|
4408
|
+
path,
|
|
4409
|
+
action: "skipped",
|
|
4410
|
+
reason: `"${name}" is already configured; pass --force to replace it.`
|
|
4411
|
+
});
|
|
4412
|
+
continue;
|
|
4413
|
+
}
|
|
4414
|
+
if (!dryRun) writeEntry(client, path, name, entry);
|
|
4415
|
+
results.push({ client: client.id, scope, path, action: dryRun ? "printed" : "written" });
|
|
4416
|
+
}
|
|
4417
|
+
let credentialPath;
|
|
4418
|
+
if (token && opts.credential !== false && !dryRun) {
|
|
4419
|
+
const patch = { token };
|
|
4420
|
+
if (ctx.apiUrl !== DEFAULT_API_URL2) patch.apiUrl = ctx.apiUrl;
|
|
4421
|
+
writeMcpConfig(patch);
|
|
4422
|
+
credentialPath = mcpConfigFile();
|
|
4423
|
+
}
|
|
4424
|
+
emit({ transport, name, results, credentialPath: credentialPath ?? null }, () => {
|
|
4425
|
+
for (const r of results) {
|
|
4426
|
+
const where = r.scope ? ` (${r.scope})` : "";
|
|
4427
|
+
process.stdout.write(`${r.action === "written" ? "\u2713" : r.action === "skipped" ? "\u2013" : "\xB7"} ${r.client}${where}: ${r.path}
|
|
4428
|
+
`);
|
|
4429
|
+
if (r.reason) process.stdout.write(` ${r.reason}
|
|
4430
|
+
`);
|
|
4431
|
+
}
|
|
4432
|
+
for (const client of clients.filter((c) => c.dialect === "toml")) {
|
|
4433
|
+
process.stdout.write(`
|
|
4434
|
+
Add to ~/${client.userPath}:
|
|
4435
|
+
|
|
4436
|
+
${tomlBlock(name, spec)}
|
|
4437
|
+
`);
|
|
4438
|
+
}
|
|
4439
|
+
if (credentialPath) diag(`
|
|
4440
|
+
Credential written to ${credentialPath} (0600).`);
|
|
4441
|
+
if (transport === "stdio" && token && !token.startsWith("myna_sk_")) {
|
|
4442
|
+
diag(
|
|
4443
|
+
"\nThis is your personal credential, so the agent inherits everything you can do.\nPrefer a scoped key: myna keys create --scopes content:read,content:write,assets:read,assets:write,preview:write,schema:read"
|
|
4444
|
+
);
|
|
4445
|
+
}
|
|
4446
|
+
if (dryRun) {
|
|
4447
|
+
diag("\nNothing written (--print).");
|
|
4448
|
+
} else if (results.some((r) => r.action === "written")) {
|
|
4449
|
+
diag("\nRestart the client to pick up the new server.");
|
|
4450
|
+
}
|
|
4451
|
+
});
|
|
4452
|
+
})
|
|
4453
|
+
);
|
|
4454
|
+
mcp.command("list").description("Show known MCP clients, whether they are installed here, and whether Myna is configured").option("--name <name>", "server name to look for", "myna").option("--dir <dir>", "project root for project-scoped files (default: the linked project or cwd)").action(
|
|
4455
|
+
handle(async (ctx, _args, opts) => {
|
|
4456
|
+
const name = opts.name;
|
|
4457
|
+
const root = opts.dir ?? ctx.linkedRoot ?? process.cwd();
|
|
4458
|
+
const rows = AGENT_CLIENTS.map((client) => {
|
|
4459
|
+
const scopes = scopesFor(client);
|
|
4460
|
+
const configured = client.dialect === "toml" ? null : scopes.filter((scope) => existingEntry(client, configPath(client, scope, root), name) !== void 0);
|
|
4461
|
+
return {
|
|
4462
|
+
id: client.id,
|
|
4463
|
+
label: client.label,
|
|
4464
|
+
detected: isDetected(client),
|
|
4465
|
+
scopes,
|
|
4466
|
+
configured
|
|
4467
|
+
};
|
|
4468
|
+
});
|
|
4469
|
+
emit(
|
|
4470
|
+
rows,
|
|
4471
|
+
() => table(rows, [
|
|
4472
|
+
{ header: "CLIENT", value: (r) => r.id },
|
|
4473
|
+
{ header: "DETECTED", value: (r) => r.detected ? "yes" : "" },
|
|
4474
|
+
{ header: "SCOPES", value: (r) => r.scopes.join(", ") },
|
|
4475
|
+
{
|
|
4476
|
+
header: "MYNA",
|
|
4477
|
+
value: (r) => r.configured === null ? "(manual)" : r.configured.length > 0 ? r.configured.join(", ") : ""
|
|
4478
|
+
}
|
|
4479
|
+
])
|
|
4480
|
+
);
|
|
4481
|
+
})
|
|
4482
|
+
);
|
|
4483
|
+
mcp.command("uninstall").description("Remove Myna's MCP server entry from one or more coding agents").argument("[clients...]", "client ids (default: every client detected on this machine)").option("--scope <scope>", "project or user (default: every scope the client supports)").option("--name <name>", "server name in the client's configuration", "myna").option("--dir <dir>", "project root for project-scoped files (default: the linked project or cwd)").action(
|
|
4484
|
+
handle(async (ctx, args, opts) => {
|
|
4485
|
+
const name = opts.name;
|
|
4486
|
+
const root = opts.dir ?? ctx.linkedRoot ?? process.cwd();
|
|
4487
|
+
const clients = resolveClients(args[0] ?? []);
|
|
4488
|
+
const removed = [];
|
|
4489
|
+
for (const client of clients) {
|
|
4490
|
+
if (client.dialect === "toml") continue;
|
|
4491
|
+
const scopes = opts.scope ? [resolveScope(client, opts.scope)] : scopesFor(client);
|
|
4492
|
+
for (const scope of scopes) {
|
|
4493
|
+
const path = configPath(client, scope, root);
|
|
4494
|
+
if (removeEntry(client, path, name)) removed.push({ client: client.id, scope, path });
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
emit({ name, removed }, () => {
|
|
4498
|
+
if (removed.length === 0) {
|
|
4499
|
+
diag(`No "${name}" server entry found.`);
|
|
4500
|
+
return;
|
|
4501
|
+
}
|
|
4502
|
+
for (const r of removed) process.stdout.write(`\u2713 ${r.client} (${r.scope}): ${r.path}
|
|
4503
|
+
`);
|
|
4504
|
+
diag(`
|
|
4505
|
+
The credential in ${mcpConfigFile()} was left in place. Remove it by hand if nothing else uses it.`);
|
|
4506
|
+
});
|
|
4507
|
+
})
|
|
4508
|
+
);
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
// src/commands/skills.ts
|
|
4512
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync8, writeFileSync as writeFileSync6 } from "fs";
|
|
4513
|
+
import { homedir as homedir3 } from "os";
|
|
4514
|
+
import { join as join10 } from "path";
|
|
4515
|
+
|
|
4516
|
+
// src/skills/index.ts
|
|
4517
|
+
function skill(name, description, lines) {
|
|
4518
|
+
return {
|
|
4519
|
+
name,
|
|
4520
|
+
description,
|
|
4521
|
+
// The description is quoted: these sentences contain colons, and a plain
|
|
4522
|
+
// YAML scalar containing ": " is a mapping, not a string. An unquoted one
|
|
4523
|
+
// makes the whole frontmatter block fail to parse.
|
|
4524
|
+
body: ["---", `name: ${name}`, `description: ${JSON.stringify(description)}`, "---", "", ...lines, ""].join("\n")
|
|
4525
|
+
};
|
|
4526
|
+
}
|
|
4527
|
+
var CHANGE_SETS = skill(
|
|
4528
|
+
"myna-change-sets",
|
|
4529
|
+
"How to write content in Myna: every write is a draft on a change set, validated and previewed before a human publishes it. Use when creating, updating, or deleting entries or assets in Myna.",
|
|
4530
|
+
[
|
|
4531
|
+
"# Writing content in Myna",
|
|
4532
|
+
"",
|
|
4533
|
+
"Myna has one product invariant, and it governs everything below:",
|
|
4534
|
+
"",
|
|
4535
|
+
"> Writes create reviewable drafts on a change set. They never publish implicitly.",
|
|
4536
|
+
"> Validation and preview precede a separately authorized, atomic publish.",
|
|
4537
|
+
"",
|
|
4538
|
+
"There is no way to write directly to published content, and you should not look",
|
|
4539
|
+
"for one. A tool that appears to offer it is staging a draft.",
|
|
4540
|
+
"",
|
|
4541
|
+
"## The workflow",
|
|
4542
|
+
"",
|
|
4543
|
+
"1. **Create a change set.** It is the unit of work \u2014 one coherent edit, however",
|
|
4544
|
+
" many entries it touches. Give it a title a reviewer can act on.",
|
|
4545
|
+
"2. **Stage the writes.** Create, update, or delete entries and assets against",
|
|
4546
|
+
" that change set. Each one becomes a draft item on it.",
|
|
4547
|
+
"3. **Validate the whole set.** Not each entry \u2014 the set. Cross-entry problems",
|
|
4548
|
+
" (a broken reference, a required translation missing) only appear here.",
|
|
4549
|
+
"4. **Create one preview.** One URL covers the entire change set. Creating a",
|
|
4550
|
+
" preview per entry is a misuse of the API and floods the project's tokens.",
|
|
4551
|
+
"5. **Stop.** Report the change set id and the preview URL, and let a human",
|
|
4552
|
+
" review it \u2014 unless publishing was explicitly requested *and* your credential",
|
|
4553
|
+
" holds `content:publish`.",
|
|
4554
|
+
"",
|
|
4555
|
+
"```bash",
|
|
4556
|
+
"myna changes create --set title='Autumn refresh'",
|
|
4557
|
+
"myna entries update posts/welcome --change-set chs_... --set title='New title'",
|
|
4558
|
+
"myna changes validate chs_...",
|
|
4559
|
+
"myna preview create chs_...",
|
|
4560
|
+
"```",
|
|
4561
|
+
"",
|
|
4562
|
+
"The MCP equivalents are `myna_create_change_set`, `myna_update_entry`,",
|
|
4563
|
+
"`myna_validate_change_set`, and `myna_create_preview`.",
|
|
4564
|
+
"",
|
|
4565
|
+
"## Read the collection before you write to it",
|
|
4566
|
+
"",
|
|
4567
|
+
"Collections carry `guidance`: the house style anyone writing into them is",
|
|
4568
|
+
"expected to follow, plus `policy` rules that will fail validation if broken",
|
|
4569
|
+
"(length budgets, required fields, allowed values). Fetch the schema first \u2014",
|
|
4570
|
+
"`myna_get_collection_schema`, or `myna schema get <collection>` \u2014 and follow it.",
|
|
4571
|
+
"A title that busts a length budget fails at validation, not at publish, so",
|
|
4572
|
+
"there is no reason to discover it late.",
|
|
4573
|
+
"",
|
|
4574
|
+
"## Concurrency",
|
|
4575
|
+
"",
|
|
4576
|
+
"- Pass `expectedRevisionId` when updating an entry you have read. Without it,",
|
|
4577
|
+
" two writers silently overwrite each other; with it, the loser gets a clear",
|
|
4578
|
+
" conflict.",
|
|
4579
|
+
"- An entry may be staged on several open change sets at once. Yours builds on",
|
|
4580
|
+
" your change set's own base, not on anyone else's draft.",
|
|
4581
|
+
"- If publish reports `STALE_REVISION`, the entry moved underneath you. Run a",
|
|
4582
|
+
" rebase (`myna changes rebase`, `myna_rebase_change_set`) \u2014 it performs the",
|
|
4583
|
+
" three-way merge and re-opens review, because the merged result is not what",
|
|
4584
|
+
" the reviewer approved. Never work around it by re-staging over the top.",
|
|
4585
|
+
"",
|
|
4586
|
+
"## Deleting",
|
|
4587
|
+
"",
|
|
4588
|
+
"Deletes are staged like any other write, and they need explicit confirmation:",
|
|
4589
|
+
"`--confirm-delete` on the CLI, `confirm: true` on the MCP tool. An unconfirmed",
|
|
4590
|
+
"destructive call is refused, not queued.",
|
|
4591
|
+
"",
|
|
4592
|
+
"## What to report back",
|
|
4593
|
+
"",
|
|
4594
|
+
"The change set id, what it contains, the validation result, and the preview",
|
|
4595
|
+
'URL. Not "published" \u2014 you did not publish.'
|
|
4596
|
+
]
|
|
4597
|
+
);
|
|
4598
|
+
var SCHEMA = skill(
|
|
4599
|
+
"myna-schema",
|
|
4600
|
+
"How Myna collection schemas work: code-defined, immutably versioned, deployed with a reviewed diff. Use when adding or changing a collection, field, or generated content types.",
|
|
4601
|
+
[
|
|
4602
|
+
"# Schemas in Myna",
|
|
4603
|
+
"",
|
|
4604
|
+
"Schemas are code. They live in the project's schema directory (`myna/` by",
|
|
4605
|
+
"default), are written with the DSL from `@myna-sh/sdk/schema`, and are deployed",
|
|
4606
|
+
"by pushing a diff \u2014 never edited through a UI, and never edited in the database.",
|
|
4607
|
+
"Every deployed version is immutable; a change creates a new version.",
|
|
4608
|
+
"",
|
|
4609
|
+
"## Changing a schema",
|
|
4610
|
+
"",
|
|
4611
|
+
"```bash",
|
|
4612
|
+
"myna schema diff # what would change, and how dangerous it is",
|
|
4613
|
+
"myna schema push # deploy it",
|
|
4614
|
+
"myna types generate # regenerate the typed client surface",
|
|
4615
|
+
"```",
|
|
4616
|
+
"",
|
|
4617
|
+
"`schema diff` classifies the change. An **additive** diff is safe. A diff that",
|
|
4618
|
+
"removes a field, narrows a type, or changes a key is **destructive** and",
|
|
4619
|
+
"`push` refuses it without `--allow-destructive`. That flag is a statement that",
|
|
4620
|
+
"the data loss is intended \u2014 check what is stored in the affected fields first.",
|
|
4621
|
+
"",
|
|
4622
|
+
"## Drift goes both ways",
|
|
4623
|
+
"",
|
|
4624
|
+
"`myna schema drift` compares the repository against what is deployed. When they",
|
|
4625
|
+
"disagree, decide which one is right before acting:",
|
|
4626
|
+
"",
|
|
4627
|
+
"- the repository is right \u2192 `myna schema push`",
|
|
4628
|
+
"- the deployed schema is right \u2192 `myna schema pull --open-pr`, which brings it",
|
|
4629
|
+
" back as a reviewable pull request rather than a silent local edit",
|
|
4630
|
+
"",
|
|
4631
|
+
"## Generated types",
|
|
4632
|
+
"",
|
|
4633
|
+
'`myna types generate` writes a file carrying a "do not edit by hand" header.',
|
|
4634
|
+
"Believe the header. Codegen is deterministic, so any hand edit is reverted by",
|
|
4635
|
+
"the next run and `myna doctor` reports the file as stale in the meantime.",
|
|
4636
|
+
"",
|
|
4637
|
+
"## Guidance and policy belong in the schema",
|
|
4638
|
+
"",
|
|
4639
|
+
"If a collection has a rule \u2014 a title budget, a required summary, an allowed set",
|
|
4640
|
+
"of values \u2014 express it as schema `policy` so validation enforces it for every",
|
|
4641
|
+
"writer, human or agent. Do not enforce it in a build script: a build-time gate",
|
|
4642
|
+
"fails after the content is already staged, and only for the one pipeline that",
|
|
4643
|
+
"runs it. A rule that cannot be expressed as policy is a gap in Myna worth",
|
|
4644
|
+
"reporting, not a script worth writing."
|
|
4645
|
+
]
|
|
4646
|
+
);
|
|
4647
|
+
var PUBLISHING = skill(
|
|
4648
|
+
"myna-publishing",
|
|
4649
|
+
"How publishing, releases, and reverts work in Myna, and when an agent may publish. Use before publishing a change set, reverting a release, or reading content as of a past release.",
|
|
4650
|
+
[
|
|
4651
|
+
"# Publishing, releases, and reverts",
|
|
4652
|
+
"",
|
|
4653
|
+
"Publishing is a separate, explicitly authorized action. It is atomic over the",
|
|
4654
|
+
"whole change set, and it assigns the set a sequential release number \u2014 that",
|
|
4655
|
+
"number is what makes it a release.",
|
|
4656
|
+
"",
|
|
4657
|
+
"## Before you publish",
|
|
4658
|
+
"",
|
|
4659
|
+
"Do not publish unless **both** are true:",
|
|
4660
|
+
"",
|
|
4661
|
+
'1. The user asked for it, in this task, in so many words. "Update the pricing',
|
|
4662
|
+
' page" is not a request to publish it.',
|
|
4663
|
+
"2. Your credential holds `content:publish`. Many agent keys deliberately do not,",
|
|
4664
|
+
" and that is the design working, not an obstacle to route around.",
|
|
4665
|
+
"",
|
|
4666
|
+
"Publishing needs explicit confirmation \u2014 `--confirm-publish` on the CLI,",
|
|
4667
|
+
"`confirm: true` on `myna_publish_change_set` \u2014 and it can still be refused by",
|
|
4668
|
+
"the project's gates:",
|
|
4669
|
+
"",
|
|
4670
|
+
"- a number of required approvals",
|
|
4671
|
+
"- the built-in check suite passing",
|
|
4672
|
+
"- any check the project declared `required`, reported by an outside system",
|
|
4673
|
+
"",
|
|
4674
|
+
"A refused publish is information. Report which gate is unmet; do not try to",
|
|
4675
|
+
"disable the gate.",
|
|
4676
|
+
"",
|
|
4677
|
+
"## Reverting",
|
|
4678
|
+
"",
|
|
4679
|
+
"A revert does not undo anything directly. It generates a **new open change set**",
|
|
4680
|
+
"staging the inverse of every item in the release, which then goes through",
|
|
4681
|
+
"validation, review, and the same publish gates as any other change. It needs",
|
|
4682
|
+
"only `content:write`, because it publishes nothing.",
|
|
4683
|
+
"",
|
|
4684
|
+
"```bash",
|
|
4685
|
+
"myna releases list",
|
|
4686
|
+
"myna releases revert <release> # creates a change set; review it, then publish",
|
|
4687
|
+
"```",
|
|
4688
|
+
"",
|
|
4689
|
+
"## Reading a past release",
|
|
4690
|
+
"",
|
|
4691
|
+
"The public content API takes `?at=<release>`, which serves a collection exactly",
|
|
4692
|
+
"as that release served it \u2014 including the slug each entry carried at the time.",
|
|
4693
|
+
"Two properties matter when reasoning about it:",
|
|
4694
|
+
"",
|
|
4695
|
+
"- Visibility is read live, never at the pin. Pinning reproduces content, not",
|
|
4696
|
+
" access decisions: a collection made private since is private now.",
|
|
4697
|
+
"- `?at=` and `?preview=` are mutually exclusive. One reads what shipped, the",
|
|
4698
|
+
" other what has not. Asking for both is an error rather than a guess.",
|
|
4699
|
+
"",
|
|
4700
|
+
"A site that pins its content sets the release in `myna.lock`, and publishing",
|
|
4701
|
+
"opens a pull request moving that pin. Nothing deploys behind the repository's",
|
|
4702
|
+
"back."
|
|
4703
|
+
]
|
|
4704
|
+
);
|
|
4705
|
+
var SKILLS = [CHANGE_SETS, SCHEMA, PUBLISHING];
|
|
4706
|
+
|
|
4707
|
+
// src/commands/skills.ts
|
|
4708
|
+
function destinations(scope, root, extra) {
|
|
4709
|
+
const home = homedir3();
|
|
4710
|
+
if (scope === "project") {
|
|
4711
|
+
return [
|
|
4712
|
+
{ id: "project", dir: join10(root, ".claude", "skills") },
|
|
4713
|
+
...extra.map((dir, i) => ({ id: `target-${i + 1}`, dir }))
|
|
4714
|
+
];
|
|
4715
|
+
}
|
|
4716
|
+
return [
|
|
4717
|
+
// The convention Claude Code, Codex, and others now read from.
|
|
4718
|
+
{ id: "agents", dir: join10(home, ".agents", "skills") },
|
|
4719
|
+
{ id: "claude", dir: join10(home, ".claude", "skills"), requiresExisting: join10(home, ".claude") },
|
|
4720
|
+
...extra.map((dir, i) => ({ id: `target-${i + 1}`, dir }))
|
|
4721
|
+
];
|
|
4722
|
+
}
|
|
4723
|
+
function registerSkills(program) {
|
|
4724
|
+
const skills = program.command("skills").description("Install Myna's agent skills for coding agents");
|
|
4725
|
+
skills.command("install").description("Write Myna's agent skills into the skill directories on this machine").option("--scope <scope>", "user (default) or project", "user").option("--target <dir>", "additional skill directory to write to (repeatable)", (value, previous = []) => [...previous, value], []).option("--dir <dir>", "project root for --scope project (default: the linked project or cwd)").option("--print", "show what would be written, and write nothing").option("--force", "overwrite skill files that have been edited").action(
|
|
4726
|
+
handle(async (ctx, _args, opts) => {
|
|
4727
|
+
const scope = opts.scope;
|
|
4728
|
+
if (scope !== "user" && scope !== "project") {
|
|
4729
|
+
throw new UsageError(`--scope must be "user" or "project" (got "${scope}").`);
|
|
4730
|
+
}
|
|
4731
|
+
const root = opts.dir ?? ctx.linkedRoot ?? process.cwd();
|
|
4732
|
+
const dryRun = Boolean(opts.print);
|
|
4733
|
+
const targets = destinations(scope, root, opts.target ?? []).filter(
|
|
4734
|
+
(d) => !d.requiresExisting || existsSync8(d.requiresExisting)
|
|
4735
|
+
);
|
|
4736
|
+
const results = [];
|
|
4737
|
+
for (const target of targets) {
|
|
4738
|
+
for (const skill2 of SKILLS) {
|
|
4739
|
+
const path = join10(target.dir, skill2.name, "SKILL.md");
|
|
4740
|
+
const current = existsSync8(path) ? readFileSync8(path, "utf8") : void 0;
|
|
4741
|
+
if (current === skill2.body) {
|
|
4742
|
+
results.push({ skill: skill2.name, path, action: "unchanged" });
|
|
4743
|
+
continue;
|
|
4744
|
+
}
|
|
4745
|
+
if (current !== void 0 && !opts.force) {
|
|
4746
|
+
results.push({ skill: skill2.name, path, action: "skipped" });
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4749
|
+
if (!dryRun) {
|
|
4750
|
+
mkdirSync5(join10(target.dir, skill2.name), { recursive: true });
|
|
4751
|
+
writeFileSync6(path, skill2.body);
|
|
4752
|
+
}
|
|
4753
|
+
results.push({ skill: skill2.name, path, action: "written" });
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
const written = results.filter((r) => r.action === "written").length;
|
|
4757
|
+
const skipped = results.filter((r) => r.action === "skipped").length;
|
|
4758
|
+
emit({ scope, written, skipped, results }, () => {
|
|
4759
|
+
if (targets.length === 0) {
|
|
4760
|
+
diag("No skill directory to write to.");
|
|
4761
|
+
return;
|
|
4762
|
+
}
|
|
4763
|
+
for (const r of results) {
|
|
4764
|
+
const icon = r.action === "written" ? "\u2713" : r.action === "unchanged" ? "=" : "\u2013";
|
|
4765
|
+
process.stdout.write(`${icon} ${r.path}
|
|
4766
|
+
`);
|
|
4767
|
+
}
|
|
4768
|
+
if (skipped > 0) diag(`
|
|
4769
|
+
${skipped} file(s) differ from the bundled version and were left alone. Pass --force to update them.`);
|
|
4770
|
+
diag(dryRun ? "\nNothing written (--print)." : `
|
|
4771
|
+
${written} skill file(s) written.`);
|
|
4772
|
+
});
|
|
4773
|
+
})
|
|
4774
|
+
);
|
|
4775
|
+
skills.command("list").description("List the agent skills bundled with this CLI").action(
|
|
4776
|
+
handle(async () => {
|
|
4777
|
+
emit(
|
|
4778
|
+
SKILLS.map((s) => ({ name: s.name, description: s.description })),
|
|
4779
|
+
() => table(SKILLS, [
|
|
4780
|
+
{ header: "SKILL", value: (s) => s.name },
|
|
4781
|
+
{ header: "DESCRIPTION", value: (s) => s.description.split(". ")[0] + "." }
|
|
4782
|
+
])
|
|
4783
|
+
);
|
|
4784
|
+
})
|
|
4785
|
+
);
|
|
4786
|
+
}
|
|
4787
|
+
|
|
4067
4788
|
// src/commands/doctor.ts
|
|
4068
|
-
import { existsSync as
|
|
4069
|
-
import { join as
|
|
4789
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
4790
|
+
import { join as join11 } from "path";
|
|
4070
4791
|
|
|
4071
4792
|
// src/registry.ts
|
|
4072
4793
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
@@ -4105,7 +4826,7 @@ function installCommand(manager, version) {
|
|
|
4105
4826
|
}
|
|
4106
4827
|
|
|
4107
4828
|
// src/version.ts
|
|
4108
|
-
var VERSION = true ? "0.
|
|
4829
|
+
var VERSION = true ? "0.13.0" : "0.0.0-dev";
|
|
4109
4830
|
var IS_RELEASE_BUILD = true;
|
|
4110
4831
|
|
|
4111
4832
|
// src/commands/doctor.ts
|
|
@@ -4279,7 +5000,7 @@ async function checkOrigin(ctx, origin) {
|
|
|
4279
5000
|
}
|
|
4280
5001
|
async function checkSchema(ctx, schemaDir) {
|
|
4281
5002
|
const dir = schemaDirFor(ctx.linkedRoot, schemaDir);
|
|
4282
|
-
if (!
|
|
5003
|
+
if (!existsSync9(dir)) {
|
|
4283
5004
|
return check("schema.drift", "Local schema", "skip", `No schema directory at ${dir}.`);
|
|
4284
5005
|
}
|
|
4285
5006
|
if (!ctx.project) {
|
|
@@ -4329,7 +5050,7 @@ function findGeneratedTypes(root, depth = 4) {
|
|
|
4329
5050
|
const dirs = [];
|
|
4330
5051
|
for (const entry of entries) {
|
|
4331
5052
|
if (SCAN_IGNORE.has(entry) || entry.startsWith(".")) continue;
|
|
4332
|
-
const full =
|
|
5053
|
+
const full = join11(root, entry);
|
|
4333
5054
|
let stats;
|
|
4334
5055
|
try {
|
|
4335
5056
|
stats = statSync2(full);
|
|
@@ -4342,7 +5063,7 @@ function findGeneratedTypes(root, depth = 4) {
|
|
|
4342
5063
|
}
|
|
4343
5064
|
if (!/\.(ts|d\.ts)$/.test(entry)) continue;
|
|
4344
5065
|
try {
|
|
4345
|
-
if (looksGenerated(
|
|
5066
|
+
if (looksGenerated(readFileSync9(full, "utf8"))) return full;
|
|
4346
5067
|
} catch {
|
|
4347
5068
|
}
|
|
4348
5069
|
}
|
|
@@ -4359,16 +5080,16 @@ async function checkTypes(ctx, explicit, schemaDir) {
|
|
|
4359
5080
|
if (!file) {
|
|
4360
5081
|
return check("types.freshness", "Generated types", "skip", "No generated types file found.");
|
|
4361
5082
|
}
|
|
4362
|
-
if (!
|
|
5083
|
+
if (!existsSync9(file)) {
|
|
4363
5084
|
return check("types.freshness", "Generated types", "fail", `${file} does not exist.`);
|
|
4364
5085
|
}
|
|
4365
5086
|
const dir = schemaDirFor(ctx.linkedRoot, schemaDir);
|
|
4366
|
-
if (!
|
|
5087
|
+
if (!existsSync9(dir)) {
|
|
4367
5088
|
return check("types.freshness", "Generated types", "skip", `Found ${file} but no schema directory to compare against.`);
|
|
4368
5089
|
}
|
|
4369
5090
|
try {
|
|
4370
5091
|
const expected = generateTypesModule(await loadLocalSchemas(dir));
|
|
4371
|
-
return
|
|
5092
|
+
return readFileSync9(file, "utf8") === expected ? check("types.freshness", "Generated types", "pass", `${file} matches the local schema.`) : check(
|
|
4372
5093
|
"types.freshness",
|
|
4373
5094
|
"Generated types",
|
|
4374
5095
|
"warn",
|
|
@@ -4484,7 +5205,7 @@ function registerUpdate(program) {
|
|
|
4484
5205
|
|
|
4485
5206
|
// src/commands/sync.ts
|
|
4486
5207
|
import { readdir as readdir2, stat as stat2 } from "fs/promises";
|
|
4487
|
-
import { join as
|
|
5208
|
+
import { join as join12 } from "path";
|
|
4488
5209
|
async function isDirectory(path) {
|
|
4489
5210
|
const info = await stat2(path).catch(() => void 0);
|
|
4490
5211
|
return Boolean(info?.isDirectory());
|
|
@@ -4499,7 +5220,7 @@ async function planDirectories(dir, collection) {
|
|
|
4499
5220
|
const plan = [];
|
|
4500
5221
|
for (const name of children.sort()) {
|
|
4501
5222
|
if (name.startsWith(".")) continue;
|
|
4502
|
-
const full =
|
|
5223
|
+
const full = join12(dir, name);
|
|
4503
5224
|
if (await isDirectory(full)) plan.push({ collection: name, path: full });
|
|
4504
5225
|
}
|
|
4505
5226
|
if (plan.length === 0) {
|
|
@@ -4673,6 +5394,9 @@ function buildProgram() {
|
|
|
4673
5394
|
registerPull(program);
|
|
4674
5395
|
registerPreviews(program);
|
|
4675
5396
|
registerAssets(program);
|
|
5397
|
+
registerMcp(program);
|
|
5398
|
+
registerSkills(program);
|
|
5399
|
+
registerApi(program);
|
|
4676
5400
|
registerAdmin(program);
|
|
4677
5401
|
return program;
|
|
4678
5402
|
}
|