feat(cli): --print-config emits ready-to-paste .mcp.json for every detected install
New flags: - --print-config: scan installs and emit a JSON block per install with derived server names (codesys-sp21-patch5, codesys-sp22-patch1, etc.) - --sp <n>: filter to one SP family; collapses entry name to 'codesys' when exactly one install matches - --name <name>: override the entry name (only valid with --sp narrowing to one) Side effect: --detect now reuses the same detector and additionally prints the derived profile name + suggested server entry name per install, so even users sticking to manual config get the values without guessing. Refactored install discovery into src/detect.ts so both --detect and --print-config share one implementation. New unit test fixture covers version parsing, missing-exe, dedup, sort order, --sp filter behaviour, --name override constraints, and verifies the emitted JSON parses back once // comments are stripped. The output also surfaces the multi-install caveat from launcher.ts: the double-spawn guard refuses to start a second CODESYS.exe even on a different exe path, so only one configured entry can be active at a time.
This commit is contained in:
parent
d5731512a2
commit
9c98e61974
4 changed files with 431 additions and 54 deletions
44
README.md
44
README.md
|
|
@ -21,43 +21,38 @@ Install globally from npm:
|
|||
npm install -g codesys-mcp-sp21-plus
|
||||
```
|
||||
|
||||
Add to your `.mcp.json` (Claude Code configuration) — pick the block matching your installed CODESYS version, or adjust the path/profile to match.
|
||||
Generate the `.mcp.json` snippet automatically — `--print-config` scans your installed CODESYS versions and emits a ready-to-paste block per install:
|
||||
|
||||
**CODESYS V3.5 SP21 Patch 5:**
|
||||
```bash
|
||||
codesys-mcp-sp21-plus --print-config # one entry per detected install
|
||||
codesys-mcp-sp21-plus --print-config --sp 21 # only the SP21 entry, named "codesys"
|
||||
```
|
||||
|
||||
Output looks like this on a machine with two installs:
|
||||
|
||||
```jsonc
|
||||
// Auto-generated by `codesys-mcp-sp21-plus --print-config` on 2026-04-27.
|
||||
// Detected 2 CODESYS installations. Add the entries you want; remove the rest.
|
||||
//
|
||||
// CAVEAT: the launcher refuses to spawn alongside any other CODESYS.exe,
|
||||
// so only ONE of these can be active at a time. ...
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"codesys": {
|
||||
"codesys-sp21-patch5": {
|
||||
"command": "codesys-mcp-sp21-plus",
|
||||
"args": [
|
||||
"--codesys-path", "C:\\Program Files\\CODESYS 3.5.21.50\\CODESYS\\Common\\CODESYS.exe",
|
||||
"--codesys-profile", "CODESYS V3.5 SP21 Patch 5",
|
||||
"--mode", "persistent"
|
||||
]
|
||||
}
|
||||
},
|
||||
"codesys-sp22-patch1": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**CODESYS V3.5 SP22 Patch 1:**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"codesys": {
|
||||
"command": "codesys-mcp-sp21-plus",
|
||||
"args": [
|
||||
"--codesys-path", "C:\\Program Files\\CODESYS 3.5.22.10\\CODESYS\\Common\\CODESYS.exe",
|
||||
"--codesys-profile", "CODESYS V3.5 SP22 Patch 1",
|
||||
"--mode", "persistent"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Run `codesys-mcp-sp21-plus --detect` to print the exact path/profile values for every CODESYS install on your machine — copy from there rather than guessing.
|
||||
Drop the relevant entries into `.mcp.json` (the snippet is JSON with `//` comment headers — strip the comments before parsing if your tooling is strict). `--detect` alone (no `--print-config`) prints just the inventory with paths/profile/suggested-name per install.
|
||||
|
||||
See [Installation](#installation) for source-install / upgrade / multi-install setups.
|
||||
|
||||
|
|
@ -226,6 +221,9 @@ Notes:
|
|||
| `--keep-alive` | Keep CODESYS running after server stops | `false` |
|
||||
| `--timeout <ms>` | Default command timeout | `60000` |
|
||||
| `--detect` | List installed CODESYS versions and exit | — |
|
||||
| `--print-config` | Print a ready-to-paste `.mcp.json` snippet for every detected install and exit | — |
|
||||
| `--sp <number>` | With `--print-config`: emit only the entry for CODESYS V3.5 SP`<n>` | — |
|
||||
| `--name <name>` | With `--print-config --sp <n>`: override the MCP server entry name | — |
|
||||
| `--verbose` | Enable verbose logging | — |
|
||||
| `--debug` | Enable debug logging | — |
|
||||
| `-V, --version` | Show version number | — |
|
||||
|
|
|
|||
66
src/bin.ts
66
src/bin.ts
|
|
@ -6,6 +6,7 @@
|
|||
import { program } from 'commander';
|
||||
import { startMcpServer } from './server';
|
||||
import { ServerConfig, ExecutionMode } from './types';
|
||||
import { detectInstalls, printConfig } from './detect';
|
||||
|
||||
let version = '0.1.0';
|
||||
try {
|
||||
|
|
@ -47,42 +48,45 @@ program
|
|||
.option('--verbose', 'Enable verbose logging')
|
||||
.option('--debug', 'Enable debug logging (more verbose)')
|
||||
.option('--detect', 'Detect installed CODESYS versions and exit')
|
||||
.option('--print-config', 'Print a ready-to-paste .mcp.json snippet for every detected install and exit')
|
||||
.option('--sp <number>', 'With --print-config: emit only the entry for CODESYS V3.5 SP<number>')
|
||||
.option('--name <name>', 'With --print-config --sp <n>: override the MCP server entry name')
|
||||
.parse(process.argv);
|
||||
|
||||
const opts = program.opts();
|
||||
|
||||
// Handle --detect flag
|
||||
if (opts.detect) {
|
||||
import('fs').then((fs) => {
|
||||
import('path').then((pathMod) => {
|
||||
const dirs = [
|
||||
'C:\\Program Files',
|
||||
'C:\\Program Files (x86)',
|
||||
];
|
||||
process.stderr.write('Scanning for CODESYS installations...\n\n');
|
||||
let found = 0;
|
||||
for (const base of dirs) {
|
||||
try {
|
||||
const entries = fs.readdirSync(base);
|
||||
for (const entry of entries) {
|
||||
if (entry.toLowerCase().includes('codesys')) {
|
||||
const commonExe = pathMod.join(base, entry, 'CODESYS', 'Common', 'CODESYS.exe');
|
||||
const exists = fs.existsSync(commonExe);
|
||||
process.stderr.write(` ${exists ? '[OK]' : '[--]'} ${pathMod.join(base, entry)}\n`);
|
||||
if (exists) {
|
||||
process.stderr.write(` Exe: ${commonExe}\n`);
|
||||
found++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// dir doesn't exist
|
||||
}
|
||||
}
|
||||
process.stderr.write(`\nFound ${found} CODESYS installation(s).\n`);
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
const installs = detectInstalls();
|
||||
process.stderr.write('Scanning for CODESYS installations...\n\n');
|
||||
if (installs.length === 0) {
|
||||
process.stderr.write(' (no installations matching "CODESYS X.Y.Z.W" found)\n');
|
||||
} else {
|
||||
for (const i of installs) {
|
||||
process.stderr.write(` [OK] ${i.installDir}\n`);
|
||||
process.stderr.write(` Exe: ${i.exePath}\n`);
|
||||
process.stderr.write(` Profile: ${i.profileName}\n`);
|
||||
process.stderr.write(` Suggested entry name: ${i.serverName}\n`);
|
||||
}
|
||||
}
|
||||
process.stderr.write(`\nFound ${installs.length} CODESYS installation(s).\n`);
|
||||
process.exit(0);
|
||||
} else if (opts.printConfig) {
|
||||
const installs = detectInstalls();
|
||||
let sp: number | undefined;
|
||||
if (opts.sp !== undefined) {
|
||||
sp = parseInt(opts.sp, 10);
|
||||
if (Number.isNaN(sp)) {
|
||||
process.stderr.write(`--sp must be a number (e.g. --sp 21). Got "${opts.sp}".\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
try {
|
||||
process.stdout.write(printConfig(installs, { sp, name: opts.name }) + '\n');
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
process.stderr.write(`${(err as Error).message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Build server config
|
||||
const config: ServerConfig = {
|
||||
|
|
|
|||
162
src/detect.ts
Normal file
162
src/detect.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export interface CodesysInstall {
|
||||
installDir: string;
|
||||
exePath: string;
|
||||
version: string;
|
||||
major: number;
|
||||
minor: number;
|
||||
sp: number;
|
||||
patch: number;
|
||||
profileName: string;
|
||||
serverName: string;
|
||||
}
|
||||
|
||||
const VERSION_RE = /^CODESYS\s+(\d+)\.(\d+)\.(\d+)\.(\d+)$/i;
|
||||
|
||||
function deriveProfileName(major: number, minor: number, sp: number, patch: number): string {
|
||||
const head = `CODESYS V${major}.${minor} SP${sp}`;
|
||||
return patch === 0 ? head : `${head} Patch ${patch}`;
|
||||
}
|
||||
|
||||
function deriveServerName(sp: number, patch: number): string {
|
||||
const head = `codesys-sp${sp}`;
|
||||
return patch === 0 ? head : `${head}-patch${patch}`;
|
||||
}
|
||||
|
||||
export function detectInstalls(
|
||||
searchDirs: string[] = ['C:\\Program Files', 'C:\\Program Files (x86)'],
|
||||
fsApi: { readdirSync: typeof fs.readdirSync; existsSync: typeof fs.existsSync } = fs
|
||||
): CodesysInstall[] {
|
||||
const installs: CodesysInstall[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
for (const base of searchDirs) {
|
||||
let entries: string[];
|
||||
try {
|
||||
entries = fsApi.readdirSync(base);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const m = VERSION_RE.exec(entry);
|
||||
if (!m) continue;
|
||||
const major = parseInt(m[1], 10);
|
||||
const minor = parseInt(m[2], 10);
|
||||
const sp = parseInt(m[3], 10);
|
||||
const rawPatch = parseInt(m[4], 10);
|
||||
const patch = Math.floor(rawPatch / 10);
|
||||
|
||||
const exePath = path.join(base, entry, 'CODESYS', 'Common', 'CODESYS.exe');
|
||||
if (!fsApi.existsSync(exePath)) continue;
|
||||
if (seen.has(exePath.toLowerCase())) continue;
|
||||
seen.add(exePath.toLowerCase());
|
||||
|
||||
installs.push({
|
||||
installDir: path.join(base, entry),
|
||||
exePath,
|
||||
version: `${major}.${minor}.${sp}.${rawPatch}`,
|
||||
major,
|
||||
minor,
|
||||
sp,
|
||||
patch,
|
||||
profileName: deriveProfileName(major, minor, sp, patch),
|
||||
serverName: deriveServerName(sp, patch),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
installs.sort((a, b) => {
|
||||
if (a.sp !== b.sp) return a.sp - b.sp;
|
||||
return a.patch - b.patch;
|
||||
});
|
||||
|
||||
return installs;
|
||||
}
|
||||
|
||||
export interface PrintConfigOptions {
|
||||
sp?: number;
|
||||
name?: string;
|
||||
date?: string;
|
||||
}
|
||||
|
||||
export function printConfig(installs: CodesysInstall[], opts: PrintConfigOptions = {}): string {
|
||||
let filtered = installs;
|
||||
if (opts.sp !== undefined) {
|
||||
filtered = installs.filter((i) => i.sp === opts.sp);
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
if (opts.sp !== undefined) {
|
||||
throw new Error(
|
||||
`No CODESYS V3.5 SP${opts.sp} installation detected. Run \`codesys-mcp-sp21-plus --detect\` to see what's available.`
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
`No CODESYS installations detected. Looked under "C:\\Program Files" and "C:\\Program Files (x86)" for "CODESYS X.Y.Z.W"-named directories with a CODESYS\\Common\\CODESYS.exe inside. Run \`codesys-mcp-sp21-plus --detect\` for raw output.`
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.name !== undefined && filtered.length !== 1) {
|
||||
throw new Error(
|
||||
`--name only works when exactly one install is selected. Got ${filtered.length}. Combine with --sp <n> to narrow down.`
|
||||
);
|
||||
}
|
||||
|
||||
const today = opts.date ?? new Date().toISOString().slice(0, 10);
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`// Auto-generated by \`codesys-mcp-sp21-plus --print-config\` on ${today}.`);
|
||||
if (filtered.length === 1) {
|
||||
lines.push(`// Detected 1 CODESYS installation.`);
|
||||
} else {
|
||||
lines.push(`// Detected ${filtered.length} CODESYS installations. Add the entries you want; remove the rest.`);
|
||||
lines.push(`//`);
|
||||
lines.push(`// CAVEAT: the launcher refuses to spawn alongside any other CODESYS.exe,`);
|
||||
lines.push(`// so only ONE of these can be active at a time. Adding multiple entries is`);
|
||||
lines.push(`// fine -- Claude can call them by name -- but call shutdown_codesys before`);
|
||||
lines.push(`// switching to a different one.`);
|
||||
}
|
||||
lines.push(`//`);
|
||||
lines.push(`// Profile names are derived from the install directory version. If CODESYS's`);
|
||||
lines.push(`// own Profile dialog shows a different name (e.g. localised), edit the`);
|
||||
lines.push(`// --codesys-profile value to match exactly.`);
|
||||
lines.push('');
|
||||
|
||||
const usedNames = new Set<string>();
|
||||
const namedEntries = filtered.map((install, idx) => {
|
||||
let name: string;
|
||||
if (opts.name !== undefined) {
|
||||
name = opts.name;
|
||||
} else if (filtered.length === 1) {
|
||||
name = 'codesys';
|
||||
} else {
|
||||
name = install.serverName;
|
||||
}
|
||||
let unique = name;
|
||||
let suffix = 2;
|
||||
while (usedNames.has(unique)) {
|
||||
unique = `${name}-${suffix++}`;
|
||||
}
|
||||
usedNames.add(unique);
|
||||
return { name: unique, install, isLast: idx === filtered.length - 1 };
|
||||
});
|
||||
|
||||
lines.push('{');
|
||||
lines.push(' "mcpServers": {');
|
||||
for (const { name, install, isLast } of namedEntries) {
|
||||
lines.push(` ${JSON.stringify(name)}: {`);
|
||||
lines.push(` "command": "codesys-mcp-sp21-plus",`);
|
||||
lines.push(` "args": [`);
|
||||
lines.push(` "--codesys-path", ${JSON.stringify(install.exePath)},`);
|
||||
lines.push(` "--codesys-profile", ${JSON.stringify(install.profileName)},`);
|
||||
lines.push(` "--mode", "persistent"`);
|
||||
lines.push(` ]`);
|
||||
lines.push(` }${isLast ? '' : ','}`);
|
||||
}
|
||||
lines.push(' }');
|
||||
lines.push('}');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
213
tests/unit/detect.test.ts
Normal file
213
tests/unit/detect.test.ts
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { detectInstalls, printConfig, CodesysInstall } from '../../src/detect';
|
||||
|
||||
function makeFakeFs(installDirs: { base: string; entries: string[]; existsExe: (p: string) => boolean }[]) {
|
||||
return {
|
||||
readdirSync: ((dir: string) => {
|
||||
const match = installDirs.find((d) => d.base === dir);
|
||||
if (!match) {
|
||||
const err: NodeJS.ErrnoException = new Error('ENOENT') as NodeJS.ErrnoException;
|
||||
err.code = 'ENOENT';
|
||||
throw err;
|
||||
}
|
||||
return match.entries;
|
||||
}) as unknown as typeof import('fs').readdirSync,
|
||||
existsSync: ((p: string | Buffer | URL) => {
|
||||
const s = String(p);
|
||||
for (const d of installDirs) {
|
||||
if (d.existsExe(s)) return true;
|
||||
}
|
||||
return false;
|
||||
}) as typeof import('fs').existsSync,
|
||||
};
|
||||
}
|
||||
|
||||
describe('detectInstalls', () => {
|
||||
it('parses 3.5.21.50 -> SP21 Patch 5 with derived names', () => {
|
||||
const fakeFs = makeFakeFs([
|
||||
{
|
||||
base: 'C:\\Program Files',
|
||||
entries: ['CODESYS 3.5.21.50', 'Some Other App'],
|
||||
existsExe: (p) => p.toLowerCase().includes('codesys 3.5.21.50') && p.endsWith('CODESYS.exe'),
|
||||
},
|
||||
]);
|
||||
const installs = detectInstalls(['C:\\Program Files'], fakeFs);
|
||||
expect(installs).toHaveLength(1);
|
||||
expect(installs[0].sp).toBe(21);
|
||||
expect(installs[0].patch).toBe(5);
|
||||
expect(installs[0].profileName).toBe('CODESYS V3.5 SP21 Patch 5');
|
||||
expect(installs[0].serverName).toBe('codesys-sp21-patch5');
|
||||
});
|
||||
|
||||
it('omits Patch suffix when raw patch is 0', () => {
|
||||
const fakeFs = makeFakeFs([
|
||||
{
|
||||
base: 'C:\\Program Files',
|
||||
entries: ['CODESYS 3.5.21.0'],
|
||||
existsExe: (p) => p.endsWith('CODESYS.exe'),
|
||||
},
|
||||
]);
|
||||
const installs = detectInstalls(['C:\\Program Files'], fakeFs);
|
||||
expect(installs).toHaveLength(1);
|
||||
expect(installs[0].profileName).toBe('CODESYS V3.5 SP21');
|
||||
expect(installs[0].serverName).toBe('codesys-sp21');
|
||||
});
|
||||
|
||||
it('skips dirs that do not match CODESYS X.Y.Z.W pattern', () => {
|
||||
const fakeFs = makeFakeFs([
|
||||
{
|
||||
base: 'C:\\Program Files (x86)',
|
||||
entries: ['CODESYS', 'CODESYS Old', 'CODESYS 3.5'],
|
||||
existsExe: (p) => p.endsWith('CODESYS.exe'),
|
||||
},
|
||||
]);
|
||||
const installs = detectInstalls(['C:\\Program Files (x86)'], fakeFs);
|
||||
expect(installs).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('skips dirs where the exe is missing', () => {
|
||||
const fakeFs = makeFakeFs([
|
||||
{
|
||||
base: 'C:\\Program Files',
|
||||
entries: ['CODESYS 3.5.21.50', 'CODESYS 3.5.22.10'],
|
||||
existsExe: (p) => p.toLowerCase().includes('3.5.22.10'),
|
||||
},
|
||||
]);
|
||||
const installs = detectInstalls(['C:\\Program Files'], fakeFs);
|
||||
expect(installs).toHaveLength(1);
|
||||
expect(installs[0].sp).toBe(22);
|
||||
});
|
||||
|
||||
it('sorts by SP then patch ascending', () => {
|
||||
const fakeFs = makeFakeFs([
|
||||
{
|
||||
base: 'C:\\Program Files',
|
||||
entries: ['CODESYS 3.5.22.10', 'CODESYS 3.5.21.50', 'CODESYS 3.5.21.30'],
|
||||
existsExe: (p) => p.endsWith('CODESYS.exe'),
|
||||
},
|
||||
]);
|
||||
const installs = detectInstalls(['C:\\Program Files'], fakeFs);
|
||||
expect(installs.map((i) => `${i.sp}.${i.patch}`)).toEqual(['21.3', '21.5', '22.1']);
|
||||
});
|
||||
|
||||
it('deduplicates the same exe across search dirs', () => {
|
||||
const fakeFs = makeFakeFs([
|
||||
{
|
||||
base: 'C:\\Program Files',
|
||||
entries: ['CODESYS 3.5.21.50'],
|
||||
existsExe: (p) => p.endsWith('CODESYS.exe'),
|
||||
},
|
||||
{
|
||||
base: 'C:\\Program Files',
|
||||
entries: ['CODESYS 3.5.21.50'],
|
||||
existsExe: (p) => p.endsWith('CODESYS.exe'),
|
||||
},
|
||||
]);
|
||||
const installs = detectInstalls(['C:\\Program Files', 'C:\\Program Files'], fakeFs);
|
||||
expect(installs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
const fixture = (overrides: Partial<CodesysInstall> = {}): CodesysInstall => ({
|
||||
installDir: 'C:\\Program Files\\CODESYS 3.5.22.10',
|
||||
exePath: 'C:\\Program Files\\CODESYS 3.5.22.10\\CODESYS\\Common\\CODESYS.exe',
|
||||
version: '3.5.22.10',
|
||||
major: 3,
|
||||
minor: 5,
|
||||
sp: 22,
|
||||
patch: 1,
|
||||
profileName: 'CODESYS V3.5 SP22 Patch 1',
|
||||
serverName: 'codesys-sp22-patch1',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('printConfig', () => {
|
||||
const sp19 = fixture({
|
||||
installDir: 'C:\\Program Files\\CODESYS 3.5.19.40',
|
||||
exePath: 'C:\\Program Files\\CODESYS 3.5.19.40\\CODESYS\\Common\\CODESYS.exe',
|
||||
version: '3.5.19.40',
|
||||
sp: 19,
|
||||
patch: 4,
|
||||
profileName: 'CODESYS V3.5 SP19 Patch 4',
|
||||
serverName: 'codesys-sp19-patch4',
|
||||
});
|
||||
const sp21 = fixture({
|
||||
installDir: 'C:\\Program Files\\CODESYS 3.5.21.50',
|
||||
exePath: 'C:\\Program Files\\CODESYS 3.5.21.50\\CODESYS\\Common\\CODESYS.exe',
|
||||
version: '3.5.21.50',
|
||||
sp: 21,
|
||||
patch: 5,
|
||||
profileName: 'CODESYS V3.5 SP21 Patch 5',
|
||||
serverName: 'codesys-sp21-patch5',
|
||||
});
|
||||
const sp22 = fixture();
|
||||
|
||||
it('emits one entry named "codesys" when there is exactly one install', () => {
|
||||
const out = printConfig([sp22], { date: '2026-04-27' });
|
||||
expect(out).toContain('Detected 1 CODESYS installation');
|
||||
expect(out).not.toContain('CAVEAT');
|
||||
expect(out).toContain('"codesys": {');
|
||||
expect(out).toContain('"--codesys-profile", "CODESYS V3.5 SP22 Patch 1"');
|
||||
});
|
||||
|
||||
it('emits one block per install with derived names when multiple', () => {
|
||||
const out = printConfig([sp19, sp21, sp22], { date: '2026-04-27' });
|
||||
expect(out).toContain('Detected 3 CODESYS installations');
|
||||
expect(out).toContain('CAVEAT');
|
||||
expect(out).toContain('"codesys-sp19-patch4": {');
|
||||
expect(out).toContain('"codesys-sp21-patch5": {');
|
||||
expect(out).toContain('"codesys-sp22-patch1": {');
|
||||
});
|
||||
|
||||
it('--sp filters to a single SP family and collapses the name to "codesys" when one match', () => {
|
||||
const out = printConfig([sp19, sp21, sp22], { sp: 21, date: '2026-04-27' });
|
||||
expect(out).toContain('"codesys": {');
|
||||
expect(out).toContain('"--codesys-profile", "CODESYS V3.5 SP21 Patch 5"');
|
||||
expect(out).not.toContain('SP22');
|
||||
expect(out).not.toContain('SP19');
|
||||
});
|
||||
|
||||
it('--sp keeps descriptive names when multiple patches match', () => {
|
||||
const sp21patch3 = fixture({
|
||||
installDir: 'C:\\Program Files\\CODESYS 3.5.21.30',
|
||||
exePath: 'C:\\Program Files\\CODESYS 3.5.21.30\\CODESYS\\Common\\CODESYS.exe',
|
||||
version: '3.5.21.30',
|
||||
sp: 21,
|
||||
patch: 3,
|
||||
profileName: 'CODESYS V3.5 SP21 Patch 3',
|
||||
serverName: 'codesys-sp21-patch3',
|
||||
});
|
||||
const out = printConfig([sp19, sp21, sp21patch3, sp22], { sp: 21, date: '2026-04-27' });
|
||||
expect(out).toContain('"codesys-sp21-patch3": {');
|
||||
expect(out).toContain('"codesys-sp21-patch5": {');
|
||||
});
|
||||
|
||||
it('throws a clear error when --sp matches no install', () => {
|
||||
expect(() => printConfig([sp22], { sp: 21 })).toThrow(/SP21/);
|
||||
});
|
||||
|
||||
it('throws when no installs detected at all', () => {
|
||||
expect(() => printConfig([])).toThrow(/No CODESYS installations detected/);
|
||||
});
|
||||
|
||||
it('--name overrides the entry name when paired with --sp narrowing to one', () => {
|
||||
const out = printConfig([sp19, sp22], { sp: 22, name: 'production', date: '2026-04-27' });
|
||||
expect(out).toContain('"production": {');
|
||||
expect(out).not.toContain('"codesys-sp22-patch1": {');
|
||||
});
|
||||
|
||||
it('--name without single-install narrowing throws', () => {
|
||||
expect(() => printConfig([sp19, sp22], { name: 'whatever' })).toThrow(/--name only works/);
|
||||
});
|
||||
|
||||
it('produces parseable JSON when comments are stripped', () => {
|
||||
const out = printConfig([sp19, sp22], { date: '2026-04-27' });
|
||||
const stripped = out
|
||||
.split('\n')
|
||||
.filter((l) => !l.trim().startsWith('//'))
|
||||
.join('\n');
|
||||
const parsed = JSON.parse(stripped);
|
||||
expect(parsed.mcpServers['codesys-sp19-patch4'].args).toContain('--codesys-path');
|
||||
expect(parsed.mcpServers['codesys-sp22-patch1'].args).toContain('--codesys-profile');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue