diff --git a/action.yml b/action.yml index c0a716a..f044f39 100644 --- a/action.yml +++ b/action.yml @@ -30,6 +30,10 @@ outputs: runs: using: composite steps: + - name: Install PolicyMesh dependencies + shell: bash + working-directory: ${{ github.action_path }} + run: npm ci --omit=dev --no-audit --no-fund - name: Run PolicyMesh agent policy audit id: run shell: bash diff --git a/dist/discovery.js b/dist/discovery.js index dcaf17d..d430345 100644 --- a/dist/discovery.js +++ b/dist/discovery.js @@ -1,5 +1,6 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { stripJsonComments, lineOfJsonKey as coreLineOfJsonKey, lineOfJsonStringValue as coreLineOfJsonStringValue, } from 'agent-gov-core'; export async function readJsonObject(path) { return (await readJsonObjectWithSource(path)).json; } @@ -33,106 +34,6 @@ export async function readJsonObjectWithSource(path) { throw error; } } -// VS Code and Cursor both ship MCP configs as JSONC — comments and the -// occasional trailing comma are normal, not malformed. We strip them -// before JSON.parse so those files audit cleanly. Replacing comment -// bytes with spaces (and preserving newlines in block comments) keeps -// the original byte/line positions intact for error reporting and the -// downstream line locators in lineOfJsonKey / lineOfJsonStringValue. -function stripJsonComments(raw) { - let out = ''; - let inString = false; - let escape = false; - for (let index = 0; index < raw.length; index += 1) { - const char = raw[index]; - const next = raw[index + 1]; - if (inString) { - out += char; - if (escape) { - escape = false; - } - else if (char === '\\') { - escape = true; - } - else if (char === '"') { - inString = false; - } - continue; - } - if (char === '"') { - inString = true; - out += char; - continue; - } - if (char === '/' && next === '/') { - while (index < raw.length && raw[index] !== '\n') { - out += ' '; - index += 1; - } - // restore loop invariant: the for-loop's index++ will advance past '\n' - if (index < raw.length) { - out += raw[index]; - } - continue; - } - if (char === '/' && next === '*') { - out += ' '; - index += 2; - while (index < raw.length && !(raw[index] === '*' && raw[index + 1] === '/')) { - out += raw[index] === '\n' ? '\n' : ' '; - index += 1; - } - if (index < raw.length) { - out += ' '; - index += 1; // for-loop will advance past the '/' - } - continue; - } - out += char; - } - return stripTrailingCommas(out); -} -// Trailing commas before `]` or `}` are legal in JSONC; JSON.parse rejects -// them. Removing them after comment-stripping keeps byte positions stable -// because we replace each removed comma with a space. -function stripTrailingCommas(raw) { - let out = ''; - let inString = false; - let escape = false; - for (let index = 0; index < raw.length; index += 1) { - const char = raw[index]; - if (inString) { - out += char; - if (escape) { - escape = false; - } - else if (char === '\\') { - escape = true; - } - else if (char === '"') { - inString = false; - } - continue; - } - if (char === '"') { - inString = true; - out += char; - continue; - } - if (char === ',') { - let look = index + 1; - while (look < raw.length && /\s/.test(raw[look])) { - look += 1; - } - if (raw[look] === ']' || raw[look] === '}') { - out += ' '; - continue; - } - } - out += char; - } - return out; -} export function configPath(root, relativePath) { return join(root, relativePath); } @@ -140,20 +41,12 @@ export function isRecord(value) { return typeof value === 'object' && value !== null && !Array.isArray(value); } export function lineOfJsonKey(text, key) { - const keyPattern = new RegExp(`"${escapeRegExp(key)}"\\s*:`); - return lineOfPattern(text, keyPattern); + const line = coreLineOfJsonKey(text, key); + return line === 0 ? undefined : line; } export function lineOfJsonStringValue(text, value) { - const encoded = JSON.stringify(value); - return lineOfPattern(text, new RegExp(escapeRegExp(encoded))); -} -function lineOfPattern(text, pattern) { - const lines = text.split(/\r?\n/); - const index = lines.findIndex((line) => pattern.test(line)); - return index === -1 ? undefined : index + 1; -} -function escapeRegExp(value) { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const line = coreLineOfJsonStringValue(text, value); + return line === 0 ? undefined : line; } function lineOfJsonParseError(text, error) { const positionMatch = /position (\d+)/.exec(error.message); diff --git a/package-lock.json b/package-lock.json index 64fee24..ea2e9e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "policymesh", "version": "0.1.18", "license": "MIT", + "dependencies": { + "agent-gov-core": "github:Conalh/agent-gov-core#v0.1.1" + }, "bin": { "policymesh": "dist/index.js" }, @@ -26,6 +29,14 @@ "undici-types": "~7.16.0" } }, + "node_modules/agent-gov-core": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/Conalh/agent-gov-core.git#503b30f5aebf2eb0ebe6f4a60cd3cde14068670b", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", diff --git a/package.json b/package.json index 50d2f84..3b0a841 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,9 @@ "build": "tsc -p tsconfig.json", "test": "node --test" }, + "dependencies": { + "agent-gov-core": "github:Conalh/agent-gov-core#v0.1.1" + }, "devDependencies": { "@types/node": "^24.0.0", "typescript": "^5.9.3" diff --git a/src/discovery.ts b/src/discovery.ts index 29c4aa4..175a175 100644 --- a/src/discovery.ts +++ b/src/discovery.ts @@ -1,5 +1,10 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; +import { + stripJsonComments, + lineOfJsonKey as coreLineOfJsonKey, + lineOfJsonStringValue as coreLineOfJsonStringValue, +} from 'agent-gov-core'; export async function readJsonObject(path: string): Promise> { return (await readJsonObjectWithSource(path)).json; @@ -49,117 +54,6 @@ export async function readJsonObjectWithSource(path: string): Promise { } export function lineOfJsonKey(text: string, key: string): number | undefined { - const keyPattern = new RegExp(`"${escapeRegExp(key)}"\\s*:`); - return lineOfPattern(text, keyPattern); + const line = coreLineOfJsonKey(text, key); + return line === 0 ? undefined : line; } export function lineOfJsonStringValue(text: string, value: string): number | undefined { - const encoded = JSON.stringify(value); - return lineOfPattern(text, new RegExp(escapeRegExp(encoded))); -} - -function lineOfPattern(text: string, pattern: RegExp): number | undefined { - const lines = text.split(/\r?\n/); - const index = lines.findIndex((line) => pattern.test(line)); - return index === -1 ? undefined : index + 1; -} - -function escapeRegExp(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const line = coreLineOfJsonStringValue(text, value); + return line === 0 ? undefined : line; } function lineOfJsonParseError(text: string, error: SyntaxError): number | undefined { diff --git a/test/workflow.test.mjs b/test/workflow.test.mjs index 9012bbd..6c6fc86 100644 --- a/test/workflow.test.mjs +++ b/test/workflow.test.mjs @@ -67,7 +67,7 @@ test('published Action runs the bundled CLI without installing or rebuilding its const trackedDistFiles = stdout.trim().split(/\r?\n/).filter(Boolean); assert.match(action, /node "\$GITHUB_ACTION_PATH\/dist\/index\.js" audit --repo/); - assert.doesNotMatch(action, /npm ci/); + assert.match(action, /npm ci .*--omit=dev/); assert.doesNotMatch(action, /npm run build/); assert.doesNotMatch(gitignore, /^dist\/\s*$/m); assert.ok(trackedDistFiles.includes('dist/index.js'));