Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Generated at publish time by `npm shrinkwrap`; not committed (yarn.lock is source of truth)
npm-shrinkwrap.json
package-lock.json
yarn.lock.shrinkwrap-bak

# MCP validation schema cache
.schema/
.DS_Store
Expand Down
59 changes: 30 additions & 29 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@
"format": "prettier --write \"src/**/*.{ts,js,json}\" \"test/**/*.{ts,js,json}\" \"test-utils/**/*.{ts,js,json}\" \"*.{ts,js,json,md}\"",
"format:check": "prettier --check \"src/**/*.{ts,js,json}\" \"test/**/*.{ts,js,json}\" \"test-utils/**/*.{ts,js,json}\" \"*.{ts,js,json,md}\"",
"lint": "eslint . --config eslint.config.mjs",
"postpack": "shx rm -f oclif.manifest.json",
"postpack": "shx rm -f oclif.manifest.json npm-shrinkwrap.json package-lock.json",
"posttest": "yarn lint",
"prepack": "yarn build && oclif readme --multi",
"prepack": "yarn build && oclif readme --multi && node scripts/generate-shrinkwrap.js",
"shrinkwrap": "node scripts/generate-shrinkwrap.js && yarn install",
"pretest": "yarn format:check",
"test": "vitest run --config vitest.config.ts && yarn workspace @devcycle/mcp-worker test",
"test:ci": "vitest run --config vitest.config.ts && yarn workspace @devcycle/mcp-worker test",
Expand All @@ -40,34 +41,34 @@
"version": "oclif readme --multi && git add README.md"
},
"dependencies": {
"@babel/parser": "^7.28.0",
"@modelcontextprotocol/sdk": "^1.27.1",
"@oclif/core": "^2.16.0",
"@oclif/plugin-autocomplete": "^2.3.10",
"@oclif/plugin-help": "^6.2.27",
"@types/estraverse": "^5.1.7",
"@types/inquirer": "^8.2.10",
"@types/inquirer-autocomplete-prompt": "^2.0.2",
"@types/js-yaml": "^4.0.9",
"@types/validator": "^13.12.2",
"@zodios/core": "^10.9.6",
"@babel/parser": "7.28.0",
"@modelcontextprotocol/sdk": "1.27.1",
"@oclif/core": "2.16.0",
"@oclif/plugin-autocomplete": "2.3.10",
"@oclif/plugin-help": "6.2.27",
"@types/estraverse": "5.1.7",
"@types/inquirer": "8.2.10",
"@types/inquirer-autocomplete-prompt": "2.0.2",
"@types/js-yaml": "4.0.9",
"@types/validator": "13.12.2",
"@zodios/core": "10.9.6",
"axios": "1.13.6",
"chalk": "^4.1.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"estraverse": "^5.3.0",
"fuzzy": "^0.1.3",
"inquirer": "^8.2.6",
"inquirer-autocomplete-prompt": "^2.0.1",
"js-sha256": "^0.11.0",
"js-yaml": "^4.1.0",
"lodash": "^4.17.23",
"minimatch": "^9.0.7",
"open": "^8.4.2",
"parse-diff": "^0.9.0",
"recast": "^0.21.5",
"reflect-metadata": "^0.1.14",
"zod": "~3.25.76"
"chalk": "4.1.2",
"class-transformer": "0.5.1",
"class-validator": "0.14.2",
"estraverse": "5.3.0",
"fuzzy": "0.1.3",
"inquirer": "8.2.6",
"inquirer-autocomplete-prompt": "2.0.1",
"js-sha256": "0.11.0",
"js-yaml": "4.1.1",
"lodash": "4.17.23",
"minimatch": "9.0.9",
"open": "8.4.2",
"parse-diff": "0.9.0",
"recast": "0.21.5",
"reflect-metadata": "0.1.14",
"zod": "3.25.76"
},
"devDependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
Expand Down
119 changes: 119 additions & 0 deletions scripts/generate-shrinkwrap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#!/usr/bin/env node
/**
* generate-shrinkwrap.js
*
* Generates npm-shrinkwrap.json while preserving yarn.lock intact.
*
* Why the yarn.lock protection is needed:
* `npm install --package-lock-only` rewrites yarn.lock with a Yarn v1
* format file, corrupting the Yarn Berry lockfile that the project uses
* for development. We save and restore it around the npm operation.
*/

'use strict'

const { execSync } = require('child_process')
const fs = require('fs')
const path = require('path')

const ROOT = path.resolve(__dirname, '..')
const YARN_LOCK = path.join(ROOT, 'yarn.lock')
const YARN_LOCK_BAK = path.join(ROOT, 'yarn.lock.shrinkwrap-bak')
const SHRINKWRAP_PATH = path.join(ROOT, 'npm-shrinkwrap.json')

// ---------------------------------------------------------------------------
// Save yarn.lock before npm touches it
// ---------------------------------------------------------------------------

if (!fs.existsSync(YARN_LOCK)) {
console.error(
'Error: yarn.lock not found. Cannot protect it from npm overwrite.',
)
process.exit(1)
}

const yarnLockContent = fs.readFileSync(YARN_LOCK)

function restoreYarnLock() {
fs.writeFileSync(YARN_LOCK, yarnLockContent)
if (fs.existsSync(YARN_LOCK_BAK)) fs.unlinkSync(YARN_LOCK_BAK)
}

// Write a physical backup too, so a crash mid-run doesn't lose the file.
fs.writeFileSync(YARN_LOCK_BAK, yarnLockContent)

// ---------------------------------------------------------------------------
// Run npm operations, always restoring yarn.lock afterwards
// ---------------------------------------------------------------------------

try {
console.log('Generating package-lock.json from installed node_modules...')
execSync('npm install --package-lock-only --ignore-scripts', {
cwd: ROOT,
stdio: 'inherit',
})

console.log('Converting package-lock.json to npm-shrinkwrap.json...')
execSync('npm shrinkwrap', {
Comment on lines +51 to +57
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This script claims to generate shrinkwrap for production deps only (and verify-shrinkwrap.js assumes --omit=dev), but the npm commands here don't omit dev dependencies. That will include dev packages in package-lock.json/npm-shrinkwrap.json and can publish a larger-than-intended dependency tree. Add --omit=dev (and ensure it applies to both the lock generation and shrinkwrap conversion).

Suggested change
execSync('npm install --package-lock-only --ignore-scripts', {
cwd: ROOT,
stdio: 'inherit',
})
console.log('Converting package-lock.json to npm-shrinkwrap.json...')
execSync('npm shrinkwrap', {
execSync('npm install --package-lock-only --ignore-scripts --omit=dev', {
cwd: ROOT,
stdio: 'inherit',
})
console.log('Converting package-lock.json to npm-shrinkwrap.json...')
execSync('npm shrinkwrap --omit=dev', {

Copilot uses AI. Check for mistakes.
cwd: ROOT,
stdio: 'inherit',
})
} catch (err) {
console.error('Error during shrinkwrap generation:', err.message)
process.exit(1)
} finally {
// Always restore yarn.lock. npm operations rewrite it to Yarn v1 format,
// which corrupts the Yarn Berry lockfile used for development.
restoreYarnLock()
Comment on lines +61 to +67
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process.exit(1) inside the catch prevents the finally block from running, so yarn.lock may remain in the corrupted Yarn v1 format if npm install/npm shrinkwrap fails. Instead, capture the error, restore yarn.lock in finally, then exit (e.g., set process.exitCode = 1 and return, or rethrow after finally).

Copilot uses AI. Check for mistakes.
}

// ---------------------------------------------------------------------------
// Strip workspace entries from npm-shrinkwrap.json
//
// Workspace entries arise because npm reads the `workspaces` field from
// package.json. They take two forms:
// - Keys without a "node_modules/" prefix (e.g. "mcp-worker")
// - Symlink entries with { "link": true } (e.g. "node_modules/@devcycle/mcp-worker")
//
// These entries reference a local directory that does not exist when a
// consumer installs the published package, so they must be removed.
// ---------------------------------------------------------------------------

const shrinkwrap = JSON.parse(fs.readFileSync(SHRINKWRAP_PATH, 'utf8'))
const allPackages = shrinkwrap.packages || {}

let strippedCount = 0
const cleanedPackages = {}

for (const [key, value] of Object.entries(allPackages)) {
// Always keep the root entry (empty string key)
if (key === '') {
cleanedPackages[key] = value
continue
}

// Drop workspace root entries (not under node_modules/)
if (!key.startsWith('node_modules/')) {
strippedCount++
continue
}

// Drop workspace symlink entries
if (value.link === true) {
strippedCount++
continue
}

cleanedPackages[key] = value
}

if (strippedCount > 0) {
console.log(
`Stripped ${strippedCount} workspace entries from npm-shrinkwrap.json.`,
)
shrinkwrap.packages = cleanedPackages
fs.writeFileSync(
SHRINKWRAP_PATH,
JSON.stringify(shrinkwrap, null, 2) + '\n',
)
}
Loading
Loading