@mr.dj2u/cli 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/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +475 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/agent.d.ts +41 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +661 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/continue.d.ts +59 -0
- package/dist/commands/continue.d.ts.map +1 -0
- package/dist/commands/continue.js +336 -0
- package/dist/commands/continue.js.map +1 -0
- package/dist/commands/dev-tools.d.ts +14 -0
- package/dist/commands/dev-tools.d.ts.map +1 -0
- package/dist/commands/dev-tools.js +283 -0
- package/dist/commands/dev-tools.js.map +1 -0
- package/dist/commands/explain.d.ts +29 -0
- package/dist/commands/explain.d.ts.map +1 -0
- package/dist/commands/explain.js +216 -0
- package/dist/commands/explain.js.map +1 -0
- package/dist/commands/mcp-install.d.ts +28 -0
- package/dist/commands/mcp-install.d.ts.map +1 -0
- package/dist/commands/mcp-install.js +265 -0
- package/dist/commands/mcp-install.js.map +1 -0
- package/dist/commands/onboard.d.ts +71 -0
- package/dist/commands/onboard.d.ts.map +1 -0
- package/dist/commands/onboard.js +552 -0
- package/dist/commands/onboard.js.map +1 -0
- package/dist/commands/report.d.ts +12 -0
- package/dist/commands/report.d.ts.map +1 -0
- package/dist/commands/report.js +70 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/skills.d.ts +19 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +81 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/test-and-iterate.d.ts +9 -0
- package/dist/commands/test-and-iterate.d.ts.map +1 -0
- package/dist/commands/test-and-iterate.js +90 -0
- package/dist/commands/test-and-iterate.js.map +1 -0
- package/dist/continue.d.ts +3 -0
- package/dist/continue.d.ts.map +1 -0
- package/dist/continue.js +2 -0
- package/dist/continue.js.map +1 -0
- package/dist/project-memory.d.ts +58 -0
- package/dist/project-memory.d.ts.map +1 -0
- package/dist/project-memory.js +2161 -0
- package/dist/project-memory.js.map +1 -0
- package/package.json +64 -0
- package/templates/project/guidelines.md +88 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
import { cp, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { runDoctor } from '@mr.dj2u/doctor';
|
|
7
|
+
import { readKnowledgeResource } from '@mr.dj2u/knowledge';
|
|
8
|
+
import { buildContinueSessionBrief } from '../continue.js';
|
|
9
|
+
import { installVscodeUserMcp, resolveServerInvocation, writeCodexConfig, writeJsonMcpConfig, writeVscodeMcpConfig, } from './mcp-install.js';
|
|
10
|
+
const INSTRUCTIONS_BEGIN = '<!-- BEGIN MDS COPILOT INSTRUCTIONS -->';
|
|
11
|
+
const INSTRUCTIONS_END = '<!-- END MDS COPILOT INSTRUCTIONS -->';
|
|
12
|
+
const CLAUDE_INSTRUCTIONS_BEGIN = '<!-- BEGIN MDS CLAUDE INSTRUCTIONS -->';
|
|
13
|
+
const CLAUDE_INSTRUCTIONS_END = '<!-- END MDS CLAUDE INSTRUCTIONS -->';
|
|
14
|
+
const MDS_SERVER_KEY = 'mds-dev-suite';
|
|
15
|
+
const CODEX_PLUGIN_NAME = 'mds-dev-suite';
|
|
16
|
+
const CODEX_PLUGIN_SOURCE_PATH = `./plugins/${CODEX_PLUGIN_NAME}`;
|
|
17
|
+
export async function runAgentCommand(argv) {
|
|
18
|
+
if ((argv.action ?? 'install') === 'verify') {
|
|
19
|
+
await runAgentVerifyCommand(argv);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
await runAgentInstallCommand(argv);
|
|
23
|
+
}
|
|
24
|
+
export async function runAgentInstallCommand(argv) {
|
|
25
|
+
const client = argv.client ?? 'vscode';
|
|
26
|
+
const scope = argv.scope ?? 'project';
|
|
27
|
+
const dryRun = Boolean(argv.dryRun);
|
|
28
|
+
const bundleRoot = resolvePluginBundleRoot(client, argv.bundlePath);
|
|
29
|
+
const server = resolveServerInvocation({
|
|
30
|
+
serverPath: argv.serverPath,
|
|
31
|
+
command: argv.command,
|
|
32
|
+
});
|
|
33
|
+
const writtenPaths = [];
|
|
34
|
+
console.log(chalk.bold('mds agent install'));
|
|
35
|
+
console.log(chalk.dim(`client: ${client}`));
|
|
36
|
+
console.log(chalk.dim(`scope: ${scope}`));
|
|
37
|
+
console.log(chalk.dim(`bundle: ${bundleRoot}`));
|
|
38
|
+
console.log();
|
|
39
|
+
if (client === 'vscode') {
|
|
40
|
+
return await installVscodeAgent(argv, scope, dryRun, bundleRoot, server, writtenPaths);
|
|
41
|
+
}
|
|
42
|
+
if (client === 'claude') {
|
|
43
|
+
return await installClaudeAgent(argv, scope, dryRun, bundleRoot, server, writtenPaths);
|
|
44
|
+
}
|
|
45
|
+
return await installCodexAgent(argv, scope, dryRun, bundleRoot, server, writtenPaths);
|
|
46
|
+
}
|
|
47
|
+
export async function runAgentVerifyCommand(argv) {
|
|
48
|
+
const client = argv.client ?? 'vscode';
|
|
49
|
+
const target = path.resolve(argv.target ?? '.');
|
|
50
|
+
const result = await verifyAgentInstall(client, target);
|
|
51
|
+
console.log(chalk.bold('mds agent verify'));
|
|
52
|
+
console.log(chalk.dim(`client: ${client}`));
|
|
53
|
+
console.log(chalk.dim(`target: ${target}`));
|
|
54
|
+
console.log();
|
|
55
|
+
for (const check of result.checks) {
|
|
56
|
+
const label = check.status === 'pass' ? chalk.green('PASS') : chalk.red('FAIL');
|
|
57
|
+
console.log(`${label} ${check.name}: ${check.message}`);
|
|
58
|
+
console.log(chalk.dim(` ${check.path}`));
|
|
59
|
+
}
|
|
60
|
+
if (!result.passed) {
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
export async function verifyAgentInstall(client, target) {
|
|
66
|
+
switch (client) {
|
|
67
|
+
case 'vscode':
|
|
68
|
+
return await verifyVscodeAgentInstall(target);
|
|
69
|
+
case 'claude':
|
|
70
|
+
return await verifyClaudeAgentInstall(target);
|
|
71
|
+
case 'codex':
|
|
72
|
+
return await verifyCodexAgentInstall(target);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
export async function verifyVscodeAgentInstall(target) {
|
|
76
|
+
const checks = [];
|
|
77
|
+
const mcpPath = path.join(target, '.vscode', 'mcp.json');
|
|
78
|
+
const settingsPath = path.join(target, '.vscode', 'settings.json');
|
|
79
|
+
const instructionsPath = path.join(target, '.github', 'copilot-instructions.md');
|
|
80
|
+
const agentPath = path.join(target, '.github', 'agents', 'mds.agent.md');
|
|
81
|
+
const promptsPath = path.join(target, '.github', 'prompts');
|
|
82
|
+
const skillsPath = path.join(target, '.github', 'skills');
|
|
83
|
+
checks.push(await checkVscodeMcp(mcpPath));
|
|
84
|
+
checks.push(await checkVscodeSettings(settingsPath));
|
|
85
|
+
checks.push(await checkContainsMarker('copilot instructions', instructionsPath, INSTRUCTIONS_BEGIN));
|
|
86
|
+
checks.push(await checkExists('MDS custom agent', agentPath));
|
|
87
|
+
checks.push(await checkDirectoryContains('prompt files', promptsPath, '.prompt.md'));
|
|
88
|
+
checks.push(await checkDirectoryContains('skill files', skillsPath, 'SKILL.md'));
|
|
89
|
+
checks.push(...(await runValidationChecks(target)));
|
|
90
|
+
return {
|
|
91
|
+
client: 'vscode',
|
|
92
|
+
target,
|
|
93
|
+
passed: checks.every((check) => check.status === 'pass'),
|
|
94
|
+
checks,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
export async function verifyClaudeAgentInstall(target) {
|
|
98
|
+
const checks = [];
|
|
99
|
+
const mcpPath = path.join(target, '.mcp.json');
|
|
100
|
+
const instructionsPath = path.join(target, 'CLAUDE.md');
|
|
101
|
+
const agentPath = path.join(target, '.claude', 'agents', 'mds.md');
|
|
102
|
+
const commandsPath = path.join(target, '.claude', 'commands');
|
|
103
|
+
const skillsPath = path.join(target, '.claude', 'skills');
|
|
104
|
+
checks.push(await checkJsonMcp('Claude MCP config', mcpPath));
|
|
105
|
+
checks.push(await checkContainsMarker('Claude instructions', instructionsPath, CLAUDE_INSTRUCTIONS_BEGIN));
|
|
106
|
+
checks.push(await checkExists('MDS Claude agent', agentPath));
|
|
107
|
+
checks.push(await checkDirectoryContains('Claude slash commands', commandsPath, '.md'));
|
|
108
|
+
checks.push(await checkDirectoryContains('Claude skills', skillsPath, 'SKILL.md'));
|
|
109
|
+
checks.push(...(await runValidationChecks(target)));
|
|
110
|
+
return {
|
|
111
|
+
client: 'claude',
|
|
112
|
+
target,
|
|
113
|
+
passed: checks.every((check) => check.status === 'pass'),
|
|
114
|
+
checks,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export async function verifyCodexAgentInstall(target) {
|
|
118
|
+
const checks = [];
|
|
119
|
+
const configPath = path.join(target, '.codex', 'config.toml');
|
|
120
|
+
const marketplacePath = path.join(target, '.agents', 'plugins', 'marketplace.json');
|
|
121
|
+
const pluginRoot = path.join(target, 'plugins', CODEX_PLUGIN_NAME);
|
|
122
|
+
checks.push(await checkCodexConfig(configPath));
|
|
123
|
+
checks.push(await checkMarketplaceEntry(marketplacePath));
|
|
124
|
+
checks.push(await checkExists('Codex plugin manifest', path.join(pluginRoot, '.codex-plugin', 'plugin.json')));
|
|
125
|
+
checks.push(await checkDirectoryContains('Codex plugin skills', path.join(pluginRoot, 'skills'), 'SKILL.md'));
|
|
126
|
+
checks.push(...(await runValidationChecks(target)));
|
|
127
|
+
return {
|
|
128
|
+
client: 'codex',
|
|
129
|
+
target,
|
|
130
|
+
passed: checks.every((check) => check.status === 'pass'),
|
|
131
|
+
checks,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
async function installVscodeAgent(argv, scope, dryRun, bundleRoot, server, writtenPaths) {
|
|
135
|
+
if (scope === 'user') {
|
|
136
|
+
const target = path.join(os.homedir(), '.copilot');
|
|
137
|
+
await installVscodeUserAssets(bundleRoot, target, dryRun, writtenPaths);
|
|
138
|
+
await installVscodeUserMcp(server, dryRun);
|
|
139
|
+
return {
|
|
140
|
+
client: 'vscode',
|
|
141
|
+
scope,
|
|
142
|
+
target,
|
|
143
|
+
dryRun,
|
|
144
|
+
writtenPaths,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
const target = path.resolve(argv.target ?? '.');
|
|
148
|
+
await writeVscodeMcpConfig(path.join(target, '.vscode', 'mcp.json'), server, dryRun);
|
|
149
|
+
writtenPaths.push(path.join(target, '.vscode', 'mcp.json'));
|
|
150
|
+
await mergeJsonFile(path.join(bundleRoot, '.vscode', 'settings.json'), path.join(target, '.vscode', 'settings.json'), dryRun, writtenPaths);
|
|
151
|
+
await installVscodeProjectAssets(bundleRoot, target, dryRun, writtenPaths);
|
|
152
|
+
printProjectInstallFollowup(target, 'vscode');
|
|
153
|
+
return {
|
|
154
|
+
client: 'vscode',
|
|
155
|
+
scope,
|
|
156
|
+
target,
|
|
157
|
+
dryRun,
|
|
158
|
+
writtenPaths,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function installClaudeAgent(argv, scope, dryRun, bundleRoot, server, writtenPaths) {
|
|
162
|
+
if (scope === 'user') {
|
|
163
|
+
const target = path.join(os.homedir(), '.claude');
|
|
164
|
+
await writeJsonMcpConfig(path.join(os.homedir(), '.claude.json'), server, dryRun);
|
|
165
|
+
writtenPaths.push(path.join(os.homedir(), '.claude.json'));
|
|
166
|
+
await installClaudeSharedAssets(bundleRoot, target, dryRun, writtenPaths);
|
|
167
|
+
await upsertInstructions(path.join(bundleRoot, 'CLAUDE.md'), path.join(target, 'CLAUDE.md'), dryRun, writtenPaths, { begin: CLAUDE_INSTRUCTIONS_BEGIN, end: CLAUDE_INSTRUCTIONS_END });
|
|
168
|
+
printUserInstallFollowup(target, 'claude');
|
|
169
|
+
return {
|
|
170
|
+
client: 'claude',
|
|
171
|
+
scope,
|
|
172
|
+
target,
|
|
173
|
+
dryRun,
|
|
174
|
+
writtenPaths,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const target = path.resolve(argv.target ?? '.');
|
|
178
|
+
await writeJsonMcpConfig(path.join(target, '.mcp.json'), server, dryRun);
|
|
179
|
+
writtenPaths.push(path.join(target, '.mcp.json'));
|
|
180
|
+
await installClaudeSharedAssets(bundleRoot, path.join(target, '.claude'), dryRun, writtenPaths);
|
|
181
|
+
await upsertInstructions(path.join(bundleRoot, 'CLAUDE.md'), path.join(target, 'CLAUDE.md'), dryRun, writtenPaths, { begin: CLAUDE_INSTRUCTIONS_BEGIN, end: CLAUDE_INSTRUCTIONS_END });
|
|
182
|
+
printProjectInstallFollowup(target, 'claude');
|
|
183
|
+
return {
|
|
184
|
+
client: 'claude',
|
|
185
|
+
scope,
|
|
186
|
+
target,
|
|
187
|
+
dryRun,
|
|
188
|
+
writtenPaths,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function installCodexAgent(argv, scope, dryRun, bundleRoot, server, writtenPaths) {
|
|
192
|
+
const target = scope === 'user' ? os.homedir() : path.resolve(argv.target ?? '.');
|
|
193
|
+
await writeCodexConfig(path.join(target, '.codex', 'config.toml'), server, dryRun);
|
|
194
|
+
writtenPaths.push(path.join(target, '.codex', 'config.toml'));
|
|
195
|
+
await installCodexPluginAssets(bundleRoot, target, dryRun, writtenPaths);
|
|
196
|
+
if (scope === 'user') {
|
|
197
|
+
printUserInstallFollowup(target, 'codex');
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
printProjectInstallFollowup(target, 'codex');
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
client: 'codex',
|
|
204
|
+
scope,
|
|
205
|
+
target,
|
|
206
|
+
dryRun,
|
|
207
|
+
writtenPaths,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async function runValidationChecks(target) {
|
|
211
|
+
const checks = [];
|
|
212
|
+
try {
|
|
213
|
+
const report = await runDoctor(target, { mode: 'fast', runScripts: false });
|
|
214
|
+
checks.push({
|
|
215
|
+
name: 'Doctor validation',
|
|
216
|
+
status: 'pass',
|
|
217
|
+
path: target,
|
|
218
|
+
message: `Doctor ran: ${report.summary.errors} errors, ${report.summary.warnings} warnings.`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
checks.push({
|
|
223
|
+
name: 'Doctor validation',
|
|
224
|
+
status: 'fail',
|
|
225
|
+
path: target,
|
|
226
|
+
message: error instanceof Error ? error.message : String(error),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
try {
|
|
230
|
+
const guide = await readKnowledgeResource('mds://guides/post-create-onboarding');
|
|
231
|
+
checks.push({
|
|
232
|
+
name: 'Knowledge guide validation',
|
|
233
|
+
status: guide?.content ? 'pass' : 'fail',
|
|
234
|
+
path: 'mds://guides/post-create-onboarding',
|
|
235
|
+
message: guide?.content ? 'Fetched a bundled knowledge guide.' : 'Could not fetch the bundled guide.',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
checks.push({
|
|
240
|
+
name: 'Knowledge guide validation',
|
|
241
|
+
status: 'fail',
|
|
242
|
+
path: 'mds://guides/post-create-onboarding',
|
|
243
|
+
message: error instanceof Error ? error.message : String(error),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const brief = await buildContinueSessionBrief(target);
|
|
248
|
+
checks.push({
|
|
249
|
+
name: 'CLI workflow validation',
|
|
250
|
+
status: 'pass',
|
|
251
|
+
path: target,
|
|
252
|
+
message: `MDS Continue workflow ran with recommendation "${brief.recommendation.priority}".`,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
checks.push({
|
|
257
|
+
name: 'CLI workflow validation',
|
|
258
|
+
status: 'fail',
|
|
259
|
+
path: target,
|
|
260
|
+
message: error instanceof Error ? error.message : String(error),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return checks;
|
|
264
|
+
}
|
|
265
|
+
export function resolveVscodeBundleRoot(bundlePath) {
|
|
266
|
+
return resolvePluginBundleRoot('vscode', bundlePath);
|
|
267
|
+
}
|
|
268
|
+
export function resolvePluginBundleRoot(client, bundlePath) {
|
|
269
|
+
if (bundlePath) {
|
|
270
|
+
return path.resolve(bundlePath);
|
|
271
|
+
}
|
|
272
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
273
|
+
const repoRoot = path.resolve(moduleDir, '..', '..', '..', '..');
|
|
274
|
+
switch (client) {
|
|
275
|
+
case 'vscode':
|
|
276
|
+
return path.join(repoRoot, 'plugins', 'vscode-copilot');
|
|
277
|
+
case 'claude':
|
|
278
|
+
return path.join(repoRoot, 'plugins', 'claude-code');
|
|
279
|
+
case 'codex':
|
|
280
|
+
return path.join(repoRoot, 'plugins', 'codex');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function installVscodeProjectAssets(bundleRoot, target, dryRun, writtenPaths) {
|
|
284
|
+
await upsertInstructions(path.join(bundleRoot, '.github', 'copilot-instructions.md'), path.join(target, '.github', 'copilot-instructions.md'), dryRun, writtenPaths, { begin: INSTRUCTIONS_BEGIN, end: INSTRUCTIONS_END });
|
|
285
|
+
await copyTree(path.join(bundleRoot, '.github', 'agents'), path.join(target, '.github', 'agents'), dryRun, writtenPaths);
|
|
286
|
+
await copyTree(path.join(bundleRoot, '.github', 'prompts'), path.join(target, '.github', 'prompts'), dryRun, writtenPaths);
|
|
287
|
+
await copyTree(path.join(bundleRoot, '.github', 'skills'), path.join(target, '.github', 'skills'), dryRun, writtenPaths);
|
|
288
|
+
}
|
|
289
|
+
async function installVscodeUserAssets(bundleRoot, target, dryRun, writtenPaths) {
|
|
290
|
+
await upsertInstructions(path.join(bundleRoot, 'user', '.copilot', 'instructions.md'), path.join(target, 'instructions.md'), dryRun, writtenPaths, { begin: INSTRUCTIONS_BEGIN, end: INSTRUCTIONS_END });
|
|
291
|
+
await copyTree(path.join(bundleRoot, 'user', '.copilot', 'agents'), path.join(target, 'agents'), dryRun, writtenPaths);
|
|
292
|
+
await copyTree(path.join(bundleRoot, 'user', '.copilot', 'skills'), path.join(target, 'skills'), dryRun, writtenPaths);
|
|
293
|
+
}
|
|
294
|
+
async function installClaudeSharedAssets(bundleRoot, claudeRoot, dryRun, writtenPaths) {
|
|
295
|
+
await copyTree(path.join(bundleRoot, 'commands'), path.join(claudeRoot, 'commands'), dryRun, writtenPaths);
|
|
296
|
+
await copyTree(path.join(bundleRoot, 'agents'), path.join(claudeRoot, 'agents'), dryRun, writtenPaths);
|
|
297
|
+
await copyTree(path.join(bundleRoot, 'skills'), path.join(claudeRoot, 'skills'), dryRun, writtenPaths);
|
|
298
|
+
}
|
|
299
|
+
async function installCodexPluginAssets(bundleRoot, installRoot, dryRun, writtenPaths) {
|
|
300
|
+
const pluginRoot = path.join(installRoot, 'plugins', CODEX_PLUGIN_NAME);
|
|
301
|
+
await copyTree(bundleRoot, pluginRoot, dryRun, writtenPaths);
|
|
302
|
+
await upsertCodexMarketplace(path.join(installRoot, '.agents', 'plugins', 'marketplace.json'), CODEX_PLUGIN_SOURCE_PATH, dryRun, writtenPaths);
|
|
303
|
+
}
|
|
304
|
+
async function upsertInstructions(sourcePath, destinationPath, dryRun, writtenPaths, markers) {
|
|
305
|
+
const source = await readFile(sourcePath, 'utf8');
|
|
306
|
+
const existing = await readTextIfExists(destinationPath);
|
|
307
|
+
const sourceBlock = source.includes(markers.begin)
|
|
308
|
+
? source
|
|
309
|
+
: `${markers.begin}\n${source.trim()}\n${markers.end}\n`;
|
|
310
|
+
const next = upsertMarkedBlock(existing ?? '', sourceBlock, markers);
|
|
311
|
+
await writeText(destinationPath, next, dryRun, writtenPaths);
|
|
312
|
+
}
|
|
313
|
+
async function mergeJsonFile(sourcePath, destinationPath, dryRun, writtenPaths) {
|
|
314
|
+
const source = JSON.parse(await readFile(sourcePath, 'utf8'));
|
|
315
|
+
const existingRaw = await readTextIfExists(destinationPath);
|
|
316
|
+
const existing = existingRaw ? JSON.parse(existingRaw) : {};
|
|
317
|
+
const next = deepMerge(existing, source);
|
|
318
|
+
await writeText(destinationPath, `${JSON.stringify(next, null, 2)}\n`, dryRun, writtenPaths);
|
|
319
|
+
}
|
|
320
|
+
async function upsertCodexMarketplace(marketplacePath, pluginSourcePath, dryRun, writtenPaths) {
|
|
321
|
+
const existingRaw = await readTextIfExists(marketplacePath);
|
|
322
|
+
const existing = existingRaw ? parseJsonObject(existingRaw, marketplacePath) : {};
|
|
323
|
+
const marketplace = {
|
|
324
|
+
...existing,
|
|
325
|
+
name: typeof existing.name === 'string' ? existing.name : 'mds-local',
|
|
326
|
+
interface: isRecord(existing.interface) ? existing.interface : { displayName: 'MDS Local Plugins' },
|
|
327
|
+
};
|
|
328
|
+
const existingPlugins = Array.isArray(existing.plugins) ? existing.plugins : [];
|
|
329
|
+
const nextEntry = {
|
|
330
|
+
name: CODEX_PLUGIN_NAME,
|
|
331
|
+
source: {
|
|
332
|
+
source: 'local',
|
|
333
|
+
path: pluginSourcePath,
|
|
334
|
+
},
|
|
335
|
+
policy: {
|
|
336
|
+
installation: 'AVAILABLE',
|
|
337
|
+
authentication: 'ON_INSTALL',
|
|
338
|
+
},
|
|
339
|
+
category: 'Coding',
|
|
340
|
+
};
|
|
341
|
+
const plugins = existingPlugins.filter((plugin) => !isRecord(plugin) || plugin.name !== CODEX_PLUGIN_NAME);
|
|
342
|
+
plugins.push(nextEntry);
|
|
343
|
+
marketplace.plugins = plugins;
|
|
344
|
+
await writeText(marketplacePath, `${JSON.stringify(marketplace, null, 2)}\n`, dryRun, writtenPaths);
|
|
345
|
+
}
|
|
346
|
+
function upsertMarkedBlock(existing, block, markers) {
|
|
347
|
+
const trimmedBlock = block.trim();
|
|
348
|
+
const start = existing.indexOf(markers.begin);
|
|
349
|
+
const end = existing.indexOf(markers.end);
|
|
350
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
351
|
+
const afterEnd = end + markers.end.length;
|
|
352
|
+
return `${existing.slice(0, start).trimEnd()}\n\n${trimmedBlock}\n\n${existing.slice(afterEnd).trimStart()}`.trimStart();
|
|
353
|
+
}
|
|
354
|
+
return existing.trim().length > 0 ? `${existing.trimEnd()}\n\n${trimmedBlock}\n` : `${trimmedBlock}\n`;
|
|
355
|
+
}
|
|
356
|
+
function deepMerge(target, source) {
|
|
357
|
+
const merged = { ...target };
|
|
358
|
+
for (const [key, value] of Object.entries(source)) {
|
|
359
|
+
const current = merged[key];
|
|
360
|
+
if (isRecord(current) && isRecord(value)) {
|
|
361
|
+
merged[key] = deepMerge(current, value);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
merged[key] = value;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return merged;
|
|
368
|
+
}
|
|
369
|
+
async function copyTree(sourceDir, destinationDir, dryRun, writtenPaths) {
|
|
370
|
+
if (dryRun) {
|
|
371
|
+
const files = await listFiles(sourceDir);
|
|
372
|
+
for (const file of files) {
|
|
373
|
+
writtenPaths.push(path.join(destinationDir, path.relative(sourceDir, file)));
|
|
374
|
+
}
|
|
375
|
+
printDryRunTree(sourceDir, destinationDir);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
await mkdir(destinationDir, { recursive: true });
|
|
379
|
+
await cp(sourceDir, destinationDir, { recursive: true, force: true });
|
|
380
|
+
const files = await listFiles(destinationDir);
|
|
381
|
+
writtenPaths.push(...files);
|
|
382
|
+
console.log(chalk.green(`copied ${sourceDir} -> ${destinationDir}`));
|
|
383
|
+
}
|
|
384
|
+
async function writeText(filePath, content, dryRun, writtenPaths) {
|
|
385
|
+
writtenPaths.push(filePath);
|
|
386
|
+
if (dryRun) {
|
|
387
|
+
console.log(chalk.cyan('--dry-run output:'));
|
|
388
|
+
console.log(chalk.gray(`# would write ${filePath}`));
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
392
|
+
await writeFile(filePath, content, 'utf8');
|
|
393
|
+
console.log(chalk.green(`wrote ${filePath}`));
|
|
394
|
+
}
|
|
395
|
+
async function listFiles(root) {
|
|
396
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
397
|
+
const files = [];
|
|
398
|
+
for (const entry of entries) {
|
|
399
|
+
const filePath = path.join(root, entry.name);
|
|
400
|
+
if (entry.isDirectory()) {
|
|
401
|
+
files.push(...(await listFiles(filePath)));
|
|
402
|
+
}
|
|
403
|
+
else if (entry.isFile()) {
|
|
404
|
+
files.push(filePath);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return files;
|
|
408
|
+
}
|
|
409
|
+
function printDryRunTree(sourceDir, destinationDir) {
|
|
410
|
+
console.log(chalk.cyan('--dry-run output:'));
|
|
411
|
+
console.log(chalk.gray(`# would copy ${sourceDir} -> ${destinationDir}`));
|
|
412
|
+
}
|
|
413
|
+
function printProjectInstallFollowup(target, client) {
|
|
414
|
+
console.log();
|
|
415
|
+
if (client === 'vscode') {
|
|
416
|
+
console.log(chalk.bold('Next steps for VS Code Copilot (project scope):'));
|
|
417
|
+
console.log(` 1. Open ${target} in VS Code.`);
|
|
418
|
+
console.log(' 2. Confirm the mdsDevSuite MCP server is enabled when Copilot prompts for MCP trust.');
|
|
419
|
+
console.log(' 3. Run `mds agent verify --client vscode --target .` from the project root.');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (client === 'claude') {
|
|
423
|
+
console.log(chalk.bold('Next steps for Claude Code (project scope):'));
|
|
424
|
+
console.log(` 1. Open ${target} in Claude Code or restart Claude Code if it is already open.`);
|
|
425
|
+
console.log(' 2. Run `/mcp` to confirm the mds-dev-suite server is listed.');
|
|
426
|
+
console.log(' 3. Use the `mds` agent or MDS slash commands such as `/run-doctor`.');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
console.log(chalk.bold('Next steps for Codex (project scope):'));
|
|
430
|
+
console.log(` 1. Open ${target} in Codex.`);
|
|
431
|
+
console.log(' 2. Install or enable the mds-dev-suite local plugin from the local marketplace.');
|
|
432
|
+
console.log(' 3. Run `mds agent verify --client codex --target .` from the project root.');
|
|
433
|
+
}
|
|
434
|
+
function printUserInstallFollowup(target, client) {
|
|
435
|
+
console.log();
|
|
436
|
+
if (client === 'claude') {
|
|
437
|
+
console.log(chalk.bold('Next steps for Claude Code (user scope):'));
|
|
438
|
+
console.log(` 1. Restart Claude Code so it picks up assets in ${target}.`);
|
|
439
|
+
console.log(' 2. Run `/mcp` to confirm mds-dev-suite is listed.');
|
|
440
|
+
console.log(' 3. Use the `mds` agent or MDS slash commands from any workspace.');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
console.log(chalk.bold('Next steps for Codex (user scope):'));
|
|
444
|
+
console.log(` 1. Restart Codex so it picks up ${path.join(target, '.agents', 'plugins', 'marketplace.json')}.`);
|
|
445
|
+
console.log(' 2. Install or enable the mds-dev-suite local plugin from the local marketplace.');
|
|
446
|
+
console.log(' 3. Confirm the mds-dev-suite MCP server is configured in Codex settings.');
|
|
447
|
+
}
|
|
448
|
+
async function checkVscodeMcp(filePath) {
|
|
449
|
+
const raw = await readTextIfExists(filePath);
|
|
450
|
+
if (!raw) {
|
|
451
|
+
return {
|
|
452
|
+
name: 'VS Code MCP config',
|
|
453
|
+
status: 'fail',
|
|
454
|
+
path: filePath,
|
|
455
|
+
message: '.vscode/mcp.json is missing.',
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
const parsed = JSON.parse(raw);
|
|
460
|
+
if (parsed.servers?.mdsDevSuite) {
|
|
461
|
+
return {
|
|
462
|
+
name: 'VS Code MCP config',
|
|
463
|
+
status: 'pass',
|
|
464
|
+
path: filePath,
|
|
465
|
+
message: 'mdsDevSuite server is configured.',
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return {
|
|
471
|
+
name: 'VS Code MCP config',
|
|
472
|
+
status: 'fail',
|
|
473
|
+
path: filePath,
|
|
474
|
+
message: '.vscode/mcp.json is not valid JSON.',
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
name: 'VS Code MCP config',
|
|
479
|
+
status: 'fail',
|
|
480
|
+
path: filePath,
|
|
481
|
+
message: 'mdsDevSuite server is missing from the servers object.',
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
async function checkJsonMcp(name, filePath) {
|
|
485
|
+
const raw = await readTextIfExists(filePath);
|
|
486
|
+
if (!raw) {
|
|
487
|
+
return {
|
|
488
|
+
name,
|
|
489
|
+
status: 'fail',
|
|
490
|
+
path: filePath,
|
|
491
|
+
message: 'MCP config file is missing.',
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
try {
|
|
495
|
+
const parsed = JSON.parse(raw);
|
|
496
|
+
if (parsed.mcpServers?.[MDS_SERVER_KEY]) {
|
|
497
|
+
return {
|
|
498
|
+
name,
|
|
499
|
+
status: 'pass',
|
|
500
|
+
path: filePath,
|
|
501
|
+
message: `${MDS_SERVER_KEY} server is configured.`,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return {
|
|
507
|
+
name,
|
|
508
|
+
status: 'fail',
|
|
509
|
+
path: filePath,
|
|
510
|
+
message: 'MCP config file is not valid JSON.',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
name,
|
|
515
|
+
status: 'fail',
|
|
516
|
+
path: filePath,
|
|
517
|
+
message: `${MDS_SERVER_KEY} server is missing from mcpServers.`,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
async function checkCodexConfig(filePath) {
|
|
521
|
+
const raw = await readTextIfExists(filePath);
|
|
522
|
+
const hasServer = raw?.includes(`[mcp_servers.${MDS_SERVER_KEY}]`) ?? false;
|
|
523
|
+
return {
|
|
524
|
+
name: 'Codex MCP config',
|
|
525
|
+
status: hasServer ? 'pass' : 'fail',
|
|
526
|
+
path: filePath,
|
|
527
|
+
message: hasServer ? `${MDS_SERVER_KEY} server is configured.` : 'Codex MCP server block is missing.',
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
async function checkMarketplaceEntry(filePath) {
|
|
531
|
+
const raw = await readTextIfExists(filePath);
|
|
532
|
+
if (!raw) {
|
|
533
|
+
return {
|
|
534
|
+
name: 'Codex marketplace entry',
|
|
535
|
+
status: 'fail',
|
|
536
|
+
path: filePath,
|
|
537
|
+
message: 'Marketplace file is missing.',
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
const parsed = JSON.parse(raw);
|
|
542
|
+
const plugin = parsed.plugins?.find((entry) => isRecord(entry) && entry.name === CODEX_PLUGIN_NAME);
|
|
543
|
+
const source = isRecord(plugin) && isRecord(plugin.source) ? plugin.source : null;
|
|
544
|
+
const valid = source?.source === 'local' && source.path === CODEX_PLUGIN_SOURCE_PATH;
|
|
545
|
+
return {
|
|
546
|
+
name: 'Codex marketplace entry',
|
|
547
|
+
status: valid ? 'pass' : 'fail',
|
|
548
|
+
path: filePath,
|
|
549
|
+
message: valid ? 'mds-dev-suite local plugin is registered.' : 'mds-dev-suite local plugin entry is missing or invalid.',
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
return {
|
|
554
|
+
name: 'Codex marketplace entry',
|
|
555
|
+
status: 'fail',
|
|
556
|
+
path: filePath,
|
|
557
|
+
message: 'Marketplace file is not valid JSON.',
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
async function checkVscodeSettings(filePath) {
|
|
562
|
+
const raw = await readTextIfExists(filePath);
|
|
563
|
+
if (!raw) {
|
|
564
|
+
return {
|
|
565
|
+
name: 'VS Code Copilot settings',
|
|
566
|
+
status: 'fail',
|
|
567
|
+
path: filePath,
|
|
568
|
+
message: '.vscode/settings.json is missing.',
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const parsed = JSON.parse(raw);
|
|
573
|
+
const promptLocations = parsed['chat.promptFilesLocations'];
|
|
574
|
+
const agentLocations = parsed['chat.agentFilesLocations'];
|
|
575
|
+
const skillLocations = parsed['chat.agentSkillsLocations'];
|
|
576
|
+
const valid = parsed['github.copilot.chat.codeGeneration.useInstructionFiles'] === true &&
|
|
577
|
+
parsed['chat.useAgentSkills'] === true &&
|
|
578
|
+
isRecord(promptLocations) &&
|
|
579
|
+
promptLocations['.github/prompts'] === true &&
|
|
580
|
+
isRecord(agentLocations) &&
|
|
581
|
+
agentLocations['.github/agents'] === true &&
|
|
582
|
+
isRecord(skillLocations) &&
|
|
583
|
+
skillLocations['.github/skills'] === true;
|
|
584
|
+
return {
|
|
585
|
+
name: 'VS Code Copilot settings',
|
|
586
|
+
status: valid ? 'pass' : 'fail',
|
|
587
|
+
path: filePath,
|
|
588
|
+
message: valid ? 'Copilot customization discovery settings are installed.' : 'Required Copilot settings are missing.',
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return {
|
|
593
|
+
name: 'VS Code Copilot settings',
|
|
594
|
+
status: 'fail',
|
|
595
|
+
path: filePath,
|
|
596
|
+
message: '.vscode/settings.json is not valid JSON.',
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async function checkContainsMarker(name, filePath, marker) {
|
|
601
|
+
const content = await readTextIfExists(filePath);
|
|
602
|
+
return {
|
|
603
|
+
name,
|
|
604
|
+
status: content?.includes(marker) ? 'pass' : 'fail',
|
|
605
|
+
path: filePath,
|
|
606
|
+
message: content?.includes(marker) ? 'MDS instructions are installed.' : 'MDS instructions are missing.',
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
async function checkExists(name, filePath) {
|
|
610
|
+
try {
|
|
611
|
+
await stat(filePath);
|
|
612
|
+
return { name, status: 'pass', path: filePath, message: 'File exists.' };
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
return { name, status: 'fail', path: filePath, message: 'File is missing.' };
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function checkDirectoryContains(name, dirPath, suffix) {
|
|
619
|
+
try {
|
|
620
|
+
const files = await listFiles(dirPath);
|
|
621
|
+
const hasMatch = files.some((file) => file.endsWith(suffix));
|
|
622
|
+
return {
|
|
623
|
+
name,
|
|
624
|
+
status: hasMatch ? 'pass' : 'fail',
|
|
625
|
+
path: dirPath,
|
|
626
|
+
message: hasMatch ? `Found ${suffix} assets.` : `No ${suffix} assets found.`,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
catch {
|
|
630
|
+
return {
|
|
631
|
+
name,
|
|
632
|
+
status: 'fail',
|
|
633
|
+
path: dirPath,
|
|
634
|
+
message: 'Directory is missing.',
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function readTextIfExists(filePath) {
|
|
639
|
+
try {
|
|
640
|
+
return await readFile(filePath, 'utf8');
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
function parseJsonObject(raw, filePath) {
|
|
647
|
+
try {
|
|
648
|
+
const parsed = JSON.parse(raw);
|
|
649
|
+
if (isRecord(parsed)) {
|
|
650
|
+
return parsed;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
// Throw below with a stable message.
|
|
655
|
+
}
|
|
656
|
+
throw new Error(`${filePath} is not a JSON object.`);
|
|
657
|
+
}
|
|
658
|
+
function isRecord(value) {
|
|
659
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
660
|
+
}
|
|
661
|
+
//# sourceMappingURL=agent.js.map
|