sapper-ai 0.4.0 → 0.6.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/harden.js ADDED
@@ -0,0 +1,309 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.getHardenPlanSummary = getHardenPlanSummary;
40
+ exports.runHarden = runHarden;
41
+ const node_fs_1 = require("node:fs");
42
+ const node_os_1 = require("node:os");
43
+ const node_path_1 = require("node:path");
44
+ const readline = __importStar(require("node:readline"));
45
+ const package_json_1 = __importDefault(require("../package.json"));
46
+ const policyYaml_1 = require("./policyYaml");
47
+ const env_1 = require("./utils/env");
48
+ const fs_1 = require("./utils/fs");
49
+ const repoRoot_1 = require("./utils/repoRoot");
50
+ const semver_1 = require("./utils/semver");
51
+ const wrapConfig_1 = require("./mcp/wrapConfig");
52
+ function isInteractivePromptAllowed(options) {
53
+ if (options.noPrompt === true)
54
+ return false;
55
+ if ((0, env_1.isCiEnv)(options.env))
56
+ return false;
57
+ return process.stdout.isTTY === true && process.stdin.isTTY === true;
58
+ }
59
+ async function promptYesNo(question, defaultYes) {
60
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
61
+ const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] ';
62
+ const answer = await new Promise((res) => rl.question(`${question}${suffix}`, res));
63
+ rl.close();
64
+ const normalized = answer.trim().toLowerCase();
65
+ if (!normalized)
66
+ return defaultYes;
67
+ if (normalized === 'y' || normalized === 'yes')
68
+ return true;
69
+ if (normalized === 'n' || normalized === 'no')
70
+ return false;
71
+ return defaultYes;
72
+ }
73
+ function getWorkflowVersion(options) {
74
+ const fromFlag = options.workflowVersion?.trim();
75
+ const fromEnv = options.env?.SAPPERAI_WORKFLOW_VERSION?.trim();
76
+ const candidate = fromFlag || fromEnv || (typeof package_json_1.default.version === 'string' ? package_json_1.default.version : '');
77
+ return (0, semver_1.isSemver)(candidate) ? candidate : null;
78
+ }
79
+ function getMcpVersion(options) {
80
+ const fromFlag = options.mcpVersion?.trim();
81
+ const fromEnv = options.env?.SAPPERAI_MCP_VERSION?.trim();
82
+ const candidate = fromFlag || fromEnv || (0, wrapConfig_1.resolveInstalledPackageVersion)('@sapper-ai/mcp') || '';
83
+ return (0, semver_1.isSemver)(candidate) ? candidate : null;
84
+ }
85
+ function buildWorkflowYaml(version) {
86
+ return ([
87
+ 'name: SapperAI Scan',
88
+ '',
89
+ 'on:',
90
+ ' push:',
91
+ ' pull_request:',
92
+ '',
93
+ 'jobs:',
94
+ ' scan:',
95
+ ' runs-on: ubuntu-latest',
96
+ ' steps:',
97
+ ' - uses: actions/checkout@v4',
98
+ ' - uses: actions/setup-node@v4',
99
+ ' with:',
100
+ " node-version: '20'",
101
+ ' - name: SapperAI Scan',
102
+ ` run: npx -y sapper-ai@${version} scan --policy ./sapperai.config.yaml --no-prompt --no-open --no-save`,
103
+ '',
104
+ ].join('\n') + '\n');
105
+ }
106
+ async function planActions(options) {
107
+ const cwd = options.cwd ?? process.cwd();
108
+ const env = options.env ?? process.env;
109
+ const repoRoot = (0, repoRoot_1.findRepoRoot)(cwd);
110
+ const notes = [];
111
+ const workflowVersion = getWorkflowVersion({ ...options, env });
112
+ if (!workflowVersion) {
113
+ notes.push('Workflow version is unavailable (expected semver). Skipping CI workflow generation.');
114
+ }
115
+ const npxAvailable = (0, wrapConfig_1.checkNpxAvailable)();
116
+ if (!npxAvailable) {
117
+ notes.push("npx is not available on PATH. Skipping MCP config wrapping.");
118
+ }
119
+ const mcpVersion = getMcpVersion({ ...options, env });
120
+ if (!mcpVersion) {
121
+ notes.push('MCP version is unavailable (expected semver). Skipping MCP config wrapping.');
122
+ }
123
+ const actions = [];
124
+ const policyPathYaml = (0, node_path_1.join)(repoRoot, 'sapperai.config.yaml');
125
+ const policyPathYml = (0, node_path_1.join)(repoRoot, 'sapperai.config.yml');
126
+ if (!(0, node_fs_1.existsSync)(policyPathYaml) && !(0, node_fs_1.existsSync)(policyPathYml)) {
127
+ actions.push({
128
+ id: 'project.policy',
129
+ scope: 'project',
130
+ title: 'Create project policy (sapperai.config.yaml)',
131
+ paths: [policyPathYaml],
132
+ apply: async () => {
133
+ const header = ['# SapperAI Configuration', '# Generated by: sapper-ai harden', ''].join('\n') + '\n';
134
+ await (0, fs_1.atomicWriteFile)(policyPathYaml, header + (0, policyYaml_1.renderPolicyYaml)('standard'));
135
+ return { changed: true };
136
+ },
137
+ });
138
+ }
139
+ const workflowPath = (0, node_path_1.join)(repoRoot, '.github', 'workflows', 'sapperai.yml');
140
+ if (!(0, node_fs_1.existsSync)(workflowPath) && workflowVersion) {
141
+ actions.push({
142
+ id: 'project.ci_workflow',
143
+ scope: 'project',
144
+ title: 'Add GitHub Actions scan workflow (.github/workflows/sapperai.yml)',
145
+ paths: [workflowPath],
146
+ apply: async () => {
147
+ await (0, fs_1.atomicWriteFile)(workflowPath, buildWorkflowYaml(workflowVersion));
148
+ return { changed: true };
149
+ },
150
+ });
151
+ }
152
+ const includeSystem = options.includeSystem === true;
153
+ if (includeSystem) {
154
+ const home = (0, node_os_1.homedir)();
155
+ const globalPolicyPath = (0, node_path_1.join)(home, '.sapperai', 'policy.yaml');
156
+ if (!(0, node_fs_1.existsSync)(globalPolicyPath)) {
157
+ actions.push({
158
+ id: 'system.global_policy',
159
+ scope: 'system',
160
+ title: 'Create global policy (~/.sapperai/policy.yaml)',
161
+ paths: [globalPolicyPath],
162
+ apply: async () => {
163
+ const header = ['# SapperAI Global Policy', '# Generated by: sapper-ai harden', ''].join('\n') + '\n';
164
+ await (0, fs_1.atomicWriteFile)(globalPolicyPath, header + (0, policyYaml_1.renderPolicyYaml)('standard'));
165
+ return { changed: true };
166
+ },
167
+ });
168
+ }
169
+ const claudeConfigPath = (0, node_path_1.join)(home, '.config', 'claude-code', 'config.json');
170
+ if ((0, node_fs_1.existsSync)(claudeConfigPath) && npxAvailable && mcpVersion) {
171
+ let preview = null;
172
+ try {
173
+ preview = await (0, wrapConfig_1.wrapMcpConfigFile)({
174
+ filePath: claudeConfigPath,
175
+ mcpVersion,
176
+ format: 'jsonc',
177
+ dryRun: true,
178
+ });
179
+ }
180
+ catch (error) {
181
+ const msg = error instanceof Error ? error.message : String(error);
182
+ notes.push(`Failed to parse Claude Code config for MCP wrapping. path=${claudeConfigPath} error=${msg}`);
183
+ }
184
+ if (preview?.changed) {
185
+ actions.push({
186
+ id: 'system.mcp_wrap_claude',
187
+ scope: 'system',
188
+ title: 'Wrap Claude Code MCP servers with sapperai-proxy (~/.config/claude-code/config.json)',
189
+ paths: [claudeConfigPath],
190
+ apply: async () => {
191
+ const result = await (0, wrapConfig_1.wrapMcpConfigFile)({
192
+ filePath: claudeConfigPath,
193
+ mcpVersion,
194
+ format: 'jsonc',
195
+ });
196
+ return { changed: result.changed, note: result.backupPath ? `Backup: ${result.backupPath}` : undefined };
197
+ },
198
+ });
199
+ }
200
+ }
201
+ else if ((0, node_fs_1.existsSync)(claudeConfigPath) && (!npxAvailable || !mcpVersion)) {
202
+ notes.push('Claude Code config found, but MCP wrapping prerequisites are missing. Provide --mcp-version and ensure npx exists.');
203
+ }
204
+ }
205
+ // Deterministic ordering for readability and testability.
206
+ const scopeOrder = { project: 0, system: 1 };
207
+ actions.sort((a, b) => {
208
+ const diff = scopeOrder[a.scope] - scopeOrder[b.scope];
209
+ if (diff !== 0)
210
+ return diff;
211
+ return a.id.localeCompare(b.id);
212
+ });
213
+ return { actions, notes, repoRoot: (0, node_path_1.resolve)(repoRoot) };
214
+ }
215
+ async function getHardenPlanSummary(options = {}) {
216
+ const { actions, notes, repoRoot } = await planActions(options);
217
+ return {
218
+ repoRoot,
219
+ notes,
220
+ actions: actions.map((a) => ({ id: a.id, scope: a.scope, title: a.title, paths: a.paths.slice() })),
221
+ };
222
+ }
223
+ async function runHarden(options = {}) {
224
+ const env = options.env ?? process.env;
225
+ const write = options.write ?? ((text) => process.stdout.write(text));
226
+ const { actions, notes, repoRoot } = await planActions({ ...options, env });
227
+ write('\n SapperAI Hardening\n\n');
228
+ write(` Repo root: ${repoRoot}\n`);
229
+ if (actions.length === 0) {
230
+ write('\n No hardening actions recommended.\n\n');
231
+ for (const note of notes) {
232
+ write(` Note: ${note}\n`);
233
+ }
234
+ if (notes.length > 0)
235
+ write('\n');
236
+ return 0;
237
+ }
238
+ write('\n Planned actions:\n');
239
+ for (const action of actions) {
240
+ const prefix = action.scope === 'project' ? ' [project]' : ' [system]';
241
+ write(`${prefix} ${action.title}\n`);
242
+ for (const p of action.paths) {
243
+ write(` - ${p}\n`);
244
+ }
245
+ }
246
+ for (const note of notes) {
247
+ write(`\n Note: ${note}\n`);
248
+ }
249
+ write('\n');
250
+ if (options.apply !== true) {
251
+ write(" Run 'npx sapper-ai harden --apply' to apply project changes.\n");
252
+ write(" Add '--include-system' to include system changes.\n\n");
253
+ return 0;
254
+ }
255
+ const canPrompt = isInteractivePromptAllowed({ ...options, env });
256
+ if (!options.yes && !canPrompt) {
257
+ write(" Refusing to apply changes without confirmation. Use '--yes' (and '--include-system' for system).\n\n");
258
+ return 1;
259
+ }
260
+ const projectActions = actions.filter((a) => a.scope === 'project');
261
+ const systemActions = actions.filter((a) => a.scope === 'system');
262
+ let applyProject = options.yes === true;
263
+ if (!applyProject && projectActions.length > 0) {
264
+ applyProject = await promptYesNo('Apply project changes now?', true);
265
+ }
266
+ if (applyProject) {
267
+ for (const action of projectActions) {
268
+ write(` Applying: ${action.title}\n`);
269
+ try {
270
+ await action.apply();
271
+ }
272
+ catch (error) {
273
+ const msg = error instanceof Error ? error.message : String(error);
274
+ write(` Error applying action (${action.id}): ${msg}\n\n`);
275
+ return 1;
276
+ }
277
+ }
278
+ write(' Project hardening complete.\n');
279
+ }
280
+ else {
281
+ write(' Skipped project changes.\n');
282
+ }
283
+ if (options.includeSystem !== true || systemActions.length === 0) {
284
+ write('\n');
285
+ return 0;
286
+ }
287
+ // Two-stage confirmation: system writes are confirmed separately.
288
+ let applySystem = options.yes === true;
289
+ if (!applySystem) {
290
+ applySystem = await promptYesNo('Also apply system changes (writes to your home directory)?', true);
291
+ }
292
+ if (!applySystem) {
293
+ write(' Skipped system changes.\n\n');
294
+ return 0;
295
+ }
296
+ for (const action of systemActions) {
297
+ write(` Applying: ${action.title}\n`);
298
+ try {
299
+ await action.apply();
300
+ }
301
+ catch (error) {
302
+ const msg = error instanceof Error ? error.message : String(error);
303
+ write(` Error applying action (${action.id}): ${msg}\n\n`);
304
+ return 1;
305
+ }
306
+ }
307
+ write(' System hardening complete.\n\n');
308
+ return 0;
309
+ }
@@ -0,0 +1,3 @@
1
+ export declare function stripJsonc(input: string): string;
2
+ export declare function parseJsonc<T = unknown>(input: string): T;
3
+ //# sourceMappingURL=jsonc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonc.d.ts","sourceRoot":"","sources":["../../src/mcp/jsonc.ts"],"names":[],"mappings":"AAiIA,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIhD;AAED,wBAAgB,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,CAExD"}
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stripJsonc = stripJsonc;
4
+ exports.parseJsonc = parseJsonc;
5
+ function stripBom(input) {
6
+ if (input.charCodeAt(0) === 0xfeff) {
7
+ return input.slice(1);
8
+ }
9
+ return input;
10
+ }
11
+ function stripComments(input) {
12
+ let out = '';
13
+ let inString = false;
14
+ let stringQuote = '"';
15
+ let escaped = false;
16
+ let inLineComment = false;
17
+ let inBlockComment = false;
18
+ for (let i = 0; i < input.length; i += 1) {
19
+ const ch = input[i];
20
+ const next = i + 1 < input.length ? input[i + 1] : '';
21
+ if (inLineComment) {
22
+ if (ch === '\n') {
23
+ inLineComment = false;
24
+ out += ch;
25
+ }
26
+ continue;
27
+ }
28
+ if (inBlockComment) {
29
+ if (ch === '*' && next === '/') {
30
+ inBlockComment = false;
31
+ i += 1;
32
+ }
33
+ continue;
34
+ }
35
+ if (inString) {
36
+ out += ch;
37
+ if (escaped) {
38
+ escaped = false;
39
+ continue;
40
+ }
41
+ if (ch === '\\') {
42
+ escaped = true;
43
+ continue;
44
+ }
45
+ if (ch === stringQuote) {
46
+ inString = false;
47
+ }
48
+ continue;
49
+ }
50
+ if (ch === '/' && next === '/') {
51
+ inLineComment = true;
52
+ i += 1;
53
+ continue;
54
+ }
55
+ if (ch === '/' && next === '*') {
56
+ inBlockComment = true;
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (ch === '"' || ch === "'") {
61
+ inString = true;
62
+ stringQuote = ch;
63
+ out += ch;
64
+ continue;
65
+ }
66
+ out += ch;
67
+ }
68
+ return out;
69
+ }
70
+ function stripTrailingCommas(input) {
71
+ let out = '';
72
+ let inString = false;
73
+ let stringQuote = '"';
74
+ let escaped = false;
75
+ for (let i = 0; i < input.length; i += 1) {
76
+ const ch = input[i];
77
+ if (inString) {
78
+ out += ch;
79
+ if (escaped) {
80
+ escaped = false;
81
+ continue;
82
+ }
83
+ if (ch === '\\') {
84
+ escaped = true;
85
+ continue;
86
+ }
87
+ if (ch === stringQuote) {
88
+ inString = false;
89
+ }
90
+ continue;
91
+ }
92
+ if (ch === '"' || ch === "'") {
93
+ inString = true;
94
+ stringQuote = ch;
95
+ out += ch;
96
+ continue;
97
+ }
98
+ if (ch === ',') {
99
+ let j = i + 1;
100
+ while (j < input.length && /\s/.test(input[j])) {
101
+ j += 1;
102
+ }
103
+ const nextNonWs = j < input.length ? input[j] : '';
104
+ if (nextNonWs === ']' || nextNonWs === '}') {
105
+ continue;
106
+ }
107
+ }
108
+ out += ch;
109
+ }
110
+ return out;
111
+ }
112
+ function stripJsonc(input) {
113
+ const withoutBom = stripBom(input);
114
+ const withoutComments = stripComments(withoutBom);
115
+ return stripTrailingCommas(withoutComments);
116
+ }
117
+ function parseJsonc(input) {
118
+ return JSON.parse(stripJsonc(input));
119
+ }
@@ -0,0 +1,22 @@
1
+ export declare function checkNpxAvailable(): boolean;
2
+ export declare function resolveInstalledPackageVersion(packageName: string): string | null;
3
+ export interface WrapMcpConfigOptions {
4
+ filePath: string;
5
+ mcpVersion: string;
6
+ format?: 'json' | 'jsonc';
7
+ dryRun?: boolean;
8
+ }
9
+ export interface WrapMcpConfigResult {
10
+ changed: boolean;
11
+ changedServers: string[];
12
+ backupPath?: string;
13
+ restoredFromBackupPath?: string;
14
+ }
15
+ export declare function wrapMcpConfigFile(options: WrapMcpConfigOptions): Promise<WrapMcpConfigResult>;
16
+ export interface UnwrapMcpConfigOptions {
17
+ filePath: string;
18
+ format?: 'json' | 'jsonc';
19
+ dryRun?: boolean;
20
+ }
21
+ export declare function unwrapMcpConfigFile(options: UnwrapMcpConfigOptions): Promise<WrapMcpConfigResult>;
22
+ //# sourceMappingURL=wrapConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrapConfig.d.ts","sourceRoot":"","sources":["../../src/mcp/wrapConfig.ts"],"names":[],"mappings":"AAgBA,wBAAgB,iBAAiB,IAAI,OAAO,CAO3C;AAED,wBAAgB,8BAA8B,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASjF;AAkCD,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACzB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAsDnG;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACzB,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAuEvG"}
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.checkNpxAvailable = checkNpxAvailable;
4
+ exports.resolveInstalledPackageVersion = resolveInstalledPackageVersion;
5
+ exports.wrapMcpConfigFile = wrapMcpConfigFile;
6
+ exports.unwrapMcpConfigFile = unwrapMcpConfigFile;
7
+ const node_child_process_1 = require("node:child_process");
8
+ const node_fs_1 = require("node:fs");
9
+ const fs_1 = require("../utils/fs");
10
+ const semver_1 = require("../utils/semver");
11
+ const jsonc_1 = require("./jsonc");
12
+ function isRecord(value) {
13
+ return typeof value === 'object' && value !== null;
14
+ }
15
+ function isStringArray(value) {
16
+ return Array.isArray(value) && value.every((v) => typeof v === 'string');
17
+ }
18
+ function checkNpxAvailable() {
19
+ try {
20
+ const result = (0, node_child_process_1.spawnSync)('npx', ['--version'], { stdio: 'ignore' });
21
+ return result.status === 0;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ function resolveInstalledPackageVersion(packageName) {
28
+ try {
29
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
30
+ const pkg = require(`${packageName}/package.json`);
31
+ const version = typeof pkg.version === 'string' ? pkg.version : null;
32
+ return version && version.length > 0 ? version : null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function isWrappedBySapperProxy(command, args) {
39
+ if (command !== 'npx')
40
+ return false;
41
+ if (!isStringArray(args))
42
+ return false;
43
+ if (args.length < 6)
44
+ return false;
45
+ if (!(args[0] === '-y' || args[0] === '--yes'))
46
+ return false;
47
+ if (args[1] !== '--package')
48
+ return false;
49
+ if (typeof args[2] !== 'string' || !args[2].startsWith('@sapper-ai/mcp@'))
50
+ return false;
51
+ if (args[3] !== 'sapperai-proxy')
52
+ return false;
53
+ const sepIndex = args.indexOf('--');
54
+ return sepIndex >= 0 && sepIndex + 1 < args.length;
55
+ }
56
+ function buildWrappedCommand(originalCommand, originalArgs, mcpVersion) {
57
+ return {
58
+ command: 'npx',
59
+ args: ['-y', '--package', `@sapper-ai/mcp@${mcpVersion}`, 'sapperai-proxy', '--', originalCommand, ...originalArgs],
60
+ };
61
+ }
62
+ function unwrapWrappedCommand(args) {
63
+ const sepIndex = args.indexOf('--');
64
+ if (sepIndex < 0)
65
+ return null;
66
+ const originalCommand = args[sepIndex + 1];
67
+ if (!originalCommand)
68
+ return null;
69
+ const originalArgs = args.slice(sepIndex + 2);
70
+ return { command: originalCommand, args: originalArgs };
71
+ }
72
+ async function wrapMcpConfigFile(options) {
73
+ if (!(0, semver_1.isSemver)(options.mcpVersion)) {
74
+ throw new Error(`Invalid MCP version (expected semver): ${options.mcpVersion}`);
75
+ }
76
+ if (!(0, node_fs_1.existsSync)(options.filePath)) {
77
+ throw new Error(`Config file not found: ${options.filePath}`);
78
+ }
79
+ const raw = await (0, fs_1.readFileIfExists)(options.filePath);
80
+ if (raw === null) {
81
+ throw new Error(`Unable to read config file: ${options.filePath}`);
82
+ }
83
+ const parsed = options.format === 'jsonc' ? (0, jsonc_1.parseJsonc)(raw) : JSON.parse(raw);
84
+ if (!isRecord(parsed)) {
85
+ throw new Error(`Invalid config format (expected object): ${options.filePath}`);
86
+ }
87
+ const mcpServers = parsed.mcpServers;
88
+ if (!isRecord(mcpServers)) {
89
+ return { changed: false, changedServers: [] };
90
+ }
91
+ const changedServers = [];
92
+ for (const [name, config] of Object.entries(mcpServers)) {
93
+ if (!isRecord(config))
94
+ continue;
95
+ const command = config.command;
96
+ const args = config.args;
97
+ if (typeof command !== 'string')
98
+ continue;
99
+ const argList = isStringArray(args) ? args : [];
100
+ if (isWrappedBySapperProxy(command, argList)) {
101
+ continue;
102
+ }
103
+ const wrapped = buildWrappedCommand(command, argList, options.mcpVersion);
104
+ config.command = wrapped.command;
105
+ config.args = wrapped.args;
106
+ changedServers.push(name);
107
+ }
108
+ if (changedServers.length === 0) {
109
+ return { changed: false, changedServers: [] };
110
+ }
111
+ if (options.dryRun === true) {
112
+ return { changed: true, changedServers };
113
+ }
114
+ const backupPath = await (0, fs_1.backupFile)(options.filePath);
115
+ await (0, fs_1.atomicWriteFile)(options.filePath, `${JSON.stringify(parsed, null, 2)}\n`);
116
+ return { changed: true, changedServers, backupPath };
117
+ }
118
+ async function unwrapMcpConfigFile(options) {
119
+ if (!(0, node_fs_1.existsSync)(options.filePath)) {
120
+ throw new Error(`Config file not found: ${options.filePath}`);
121
+ }
122
+ const raw = await (0, fs_1.readFileIfExists)(options.filePath);
123
+ if (raw === null) {
124
+ throw new Error(`Unable to read config file: ${options.filePath}`);
125
+ }
126
+ let parsed;
127
+ try {
128
+ parsed = options.format === 'jsonc' ? (0, jsonc_1.parseJsonc)(raw) : JSON.parse(raw);
129
+ }
130
+ catch (error) {
131
+ const restoredFromBackupPath = findLatestBackupPath(options.filePath);
132
+ if (!restoredFromBackupPath) {
133
+ throw error;
134
+ }
135
+ if (options.dryRun === true) {
136
+ return { changed: true, changedServers: [], restoredFromBackupPath };
137
+ }
138
+ const backupPath = await (0, fs_1.backupFile)(options.filePath);
139
+ const backupRaw = await (0, fs_1.readFileIfExists)(restoredFromBackupPath);
140
+ if (backupRaw === null) {
141
+ throw error;
142
+ }
143
+ await (0, fs_1.atomicWriteFile)(options.filePath, backupRaw);
144
+ return { changed: true, changedServers: [], backupPath, restoredFromBackupPath };
145
+ }
146
+ if (!isRecord(parsed)) {
147
+ throw new Error(`Invalid config format (expected object): ${options.filePath}`);
148
+ }
149
+ const mcpServers = parsed.mcpServers;
150
+ if (!isRecord(mcpServers)) {
151
+ return { changed: false, changedServers: [] };
152
+ }
153
+ const changedServers = [];
154
+ for (const [name, config] of Object.entries(mcpServers)) {
155
+ if (!isRecord(config))
156
+ continue;
157
+ const command = config.command;
158
+ const args = config.args;
159
+ if (!isWrappedBySapperProxy(command, args)) {
160
+ continue;
161
+ }
162
+ const unwrapped = unwrapWrappedCommand(args);
163
+ if (!unwrapped)
164
+ continue;
165
+ config.command = unwrapped.command;
166
+ config.args = unwrapped.args;
167
+ changedServers.push(name);
168
+ }
169
+ if (changedServers.length === 0) {
170
+ return { changed: false, changedServers: [] };
171
+ }
172
+ if (options.dryRun === true) {
173
+ return { changed: true, changedServers };
174
+ }
175
+ const backupPath = await (0, fs_1.backupFile)(options.filePath);
176
+ await (0, fs_1.atomicWriteFile)(options.filePath, `${JSON.stringify(parsed, null, 2)}\n`);
177
+ return { changed: true, changedServers, backupPath };
178
+ }
179
+ function findLatestBackupPath(originalPath) {
180
+ let latest = null;
181
+ const first = `${originalPath}.bak`;
182
+ if ((0, node_fs_1.existsSync)(first)) {
183
+ latest = first;
184
+ }
185
+ for (let i = 1; i < 1000; i += 1) {
186
+ const candidate = `${originalPath}.bak.${i}`;
187
+ if ((0, node_fs_1.existsSync)(candidate)) {
188
+ latest = candidate;
189
+ }
190
+ }
191
+ return latest;
192
+ }
@@ -0,0 +1,3 @@
1
+ import type { PresetName } from './presets';
2
+ export declare function renderPolicyYaml(preset: PresetName, auditLogPath?: string): string;
3
+ //# sourceMappingURL=policyYaml.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policyYaml.d.ts","sourceRoot":"","sources":["../src/policyYaml.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AAG3C,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CA2BlF"}