Skip to content

Instantly share code, notes, and snippets.

@wagenet
Last active February 27, 2026 17:41
Show Gist options
  • Select an option

  • Save wagenet/b75c40c304bff26ec86ddc0c177214bd to your computer and use it in GitHub Desktop.

Select an option

Save wagenet/b75c40c304bff26ec86ddc0c177214bd to your computer and use it in GitHub Desktop.
Oxlint migration scripts and plan for auditboard-backend
#!/usr/bin/env node
/**
* Rewrites a migration-tool-generated oxlint.config.ts:
* - Fixes indentation
* - Adds globals import + CJS override (if CJS files present)
* - Removes JSON-file overrides (tsconfig*.json)
* - Removes @auditboard/eslint-plugin from jsPlugins
* - Adds no-unsafe-assignment disable comment
*/
const fs = require('fs');
const path = process.argv[2] || 'oxlint.config.ts';
const hasCjs = process.argv[3] === 'true';
const content = fs.readFileSync(path, 'utf8');
// Extract ignorePatterns
const ignoreMatch = content.match(/ignorePatterns:\s*\[([^\]]*)\]/);
let ignorePatterns = ['build'];
if (ignoreMatch) {
const items = ignoreMatch[1].match(/["']([^"']+)["']/g);
if (items) ignorePatterns = items.map((s) => s.replace(/["']/g, ''));
}
// Extract overrides using bracket counting
function extractArray(str, startKeyword) {
const idx = str.indexOf(startKeyword);
if (idx === -1) return null;
// Find the opening bracket
let bracketStart = str.indexOf('[', idx + startKeyword.length);
if (bracketStart === -1) return null;
let depth = 0;
let start = bracketStart;
for (let i = start; i < str.length; i++) {
if (str[i] === '[') depth++;
else if (str[i] === ']') {
depth--;
if (depth === 0) {
return str.substring(start + 1, i);
}
}
}
return null;
}
// Extract individual objects from an array string using bracket counting
function extractObjects(arrayContent) {
const objects = [];
let depth = 0;
let current = '';
for (let i = 0; i < arrayContent.length; i++) {
const ch = arrayContent[i];
if (ch === '{') {
depth++;
current += ch;
} else if (ch === '}') {
depth--;
current += ch;
if (depth === 0) {
objects.push(current.trim());
current = '';
}
} else {
if (depth > 0) current += ch;
}
}
return objects;
}
// Extract content between matching braces { ... }
function extractBraces(str, startKeyword) {
const idx = str.indexOf(startKeyword);
if (idx === -1) return null;
let braceStart = str.indexOf('{', idx + startKeyword.length);
if (braceStart === -1) return null;
let depth = 0;
for (let i = braceStart; i < str.length; i++) {
if (str[i] === '{') depth++;
else if (str[i] === '}') {
depth--;
if (depth === 0) {
return str.substring(braceStart + 1, i);
}
}
}
return null;
}
// Parse an override object to extract files and rules
function parseOverride(obj) {
const filesMatch = obj.match(/files:\s*\[([^\]]*)\]/);
const rulesContent = extractBraces(obj, 'rules:');
const files = [];
if (filesMatch) {
const items = filesMatch[1].match(/["']([^"']+)["']/g);
if (items) files.push(...items.map((s) => s.replace(/["']/g, '')));
}
const rules = {};
if (rulesContent) {
const ruleLines = rulesContent.split('\n');
for (const line of ruleLines) {
const ruleMatch = line.match(/["']([^"']+)["']\s*:\s*["']([^"']+)["']/);
if (ruleMatch) {
rules[ruleMatch[1]] = ruleMatch[2];
}
}
}
return { files, rules };
}
// Extract and filter overrides
const overridesContent = extractArray(content, 'overrides:');
const jsOverrides = [];
if (overridesContent) {
const overrideObjects = extractObjects(overridesContent);
for (const obj of overrideObjects) {
const parsed = parseOverride(obj);
// Skip JSON-only overrides
if (
parsed.files.some(
(f) => f.includes('tsconfig') && f.includes('.json'),
)
) {
console.error(
` Removed JSON-only override: ${parsed.files.join(', ')}`,
);
continue;
}
jsOverrides.push(parsed);
}
}
// Extract and filter jsPlugins
const jsPluginsContent = extractArray(content, 'jsPlugins:');
const jsPlugins = [];
if (jsPluginsContent) {
const items = extractObjects(jsPluginsContent);
// Also get string literals
const stringItems =
jsPluginsContent.match(
/(?:^|,)\s*["']([^"']+)["']\s*(?:,|$)/gm,
) || [];
for (const item of items) {
if (item.includes('@auditboard/eslint-plugin')) {
console.error(
' Removed @auditboard/eslint-plugin from jsPlugins',
);
continue;
}
const nameMatch = item.match(/name:\s*["']([^"']+)["']/);
const specMatch = item.match(/specifier:\s*["']([^"']+)["']/);
if (nameMatch && specMatch) {
jsPlugins.push({ name: nameMatch[1], specifier: specMatch[1] });
}
}
// Get top-level string plugins (not inside objects)
let d = 0;
let buf = '';
for (let i = 0; i < jsPluginsContent.length; i++) {
const ch = jsPluginsContent[i];
if (ch === '{') d++;
else if (ch === '}') d--;
else if (d === 0) buf += ch;
}
const topStrings = buf.match(/["']([^"']+)["']/g) || [];
for (const s of topStrings) {
const name = s.replace(/["']/g, '');
if (name === '@auditboard/eslint-plugin') {
console.error(
' Removed @auditboard/eslint-plugin from jsPlugins',
);
continue;
}
jsPlugins.push(name);
}
}
// Build output
let jsPluginsCode = '';
for (const p of jsPlugins) {
if (typeof p === 'object') {
jsPluginsCode += `\t\t\t{\n\t\t\t\tname: '${p.name}',\n\t\t\t\tspecifier: '${p.specifier}',\n\t\t\t},\n`;
} else {
jsPluginsCode += `\t\t\t'${p}',\n`;
}
}
let overridesCode = '';
for (const ov of jsOverrides) {
const filesStr = ov.files.map((f) => `'${f}'`).join(', ');
const ruleEntries = Object.entries(ov.rules);
if (ruleEntries.length > 0) {
const rulesStr = ruleEntries
.map(([k, v]) => `\t\t\t\t'${k}': '${v}',`)
.join('\n');
overridesCode += `\t\t\t{\n\t\t\t\tfiles: [${filesStr}],\n\t\t\t\trules: {\n${rulesStr}\n\t\t\t\t},\n\t\t\t},\n`;
} else {
overridesCode += `\t\t\t{\n\t\t\t\tfiles: [${filesStr}],\n\t\t\t},\n`;
}
}
// Add CJS globals override only if CJS files are present
if (hasCjs) {
overridesCode += `\t\t\t{\n\t\t\t\tfiles: ['**/*.cjs'],\n\t\t\t\tglobals: globals.commonjs,\n\t\t\t},\n`;
}
const ignoreStr = ignorePatterns.map((p) => `'${p}'`).join(', ');
const globalsImport = hasCjs ? `import globals from 'globals';\n` : '';
const output = `${globalsImport}import { defineConfig } from 'oxlint';
import { mergeConfigs, node } from '@soxhub/lint/oxlint';
// oxlint-disable-next-line typescript/no-unsafe-assignment -- For some reason we don't get the types correctly here
const baseConfig = node({ typescript: true, esm: true });
export default defineConfig(
\tmergeConfigs(baseConfig, {
\t\tplugins: ['import', 'node', 'typescript', 'unicorn'],
\t\tjsPlugins: [
${jsPluginsCode}\t\t],
\t\tignorePatterns: [${ignoreStr}],
\t\toverrides: [
${overridesCode}\t\t],
\t}),
);
`;
fs.writeFileSync(path, output);
console.error(` Wrote cleaned ${path}`);

Oxlint Migration Plan for Workspace Packages

Context

50 workspace packages still use ESLint for JS/TS linting. One package (tools/require-exhaustive-property-definitions) has been migrated and serves as the reference. The goal is to migrate each package so that oxlint handles JS/TS linting and ESLint is retained only for JSON/JSONC files.

Per-Package Migration Procedure

For each package, repeat these steps:

Step 1: Run the migration tool

cd <package-dir>
pnpm exec soxhub-oxlint-migrate --minimal

This generates oxlint.config.ts and oxlint-migration-report.md. Review the report for unsupported rules — most plugins are supported via jsPlugins in the @soxhub/lint/oxlint base config or the migration tool output.

Step 2: Update eslint.config.mjs to JSON-only

Replace the content to restrict ESLint to JSON/JSONC files only. Use the reference pattern:

import { defineConfig, globalIgnores } from "eslint/config";
import soxhubLint from "@soxhub/lint/eslint";
const { node } = soxhubLint;
const nodeConfig = node({ root: true, path: import.meta.dirname });
export default defineConfig([
  globalIgnores(["**/*", "!**/*.json", "!**/*.jsonc"]),
  globalIgnores(["build", "dist"]), // keep package-specific ignores
  nodeConfig,
]);

Key: globalIgnores(['**/*', '!**/*.json', '!**/*.jsonc']) ensures ESLint only processes JSON files.

If the package had custom rules in ESLint that applied to JSON files specifically (e.g., json-schema-validator/no-invalid on package.json), keep those overrides. If the package had JS/TS-only custom rules, those should already be in the generated oxlint.config.ts.

Step 3: Update package.json scripts

Replace lint:js / lint:js:fix and add lint:json / lint:json:fix:

"lint:js": "NODE_OPTIONS=\"--import @oxc-node/core/register\" oxlint --deny-warnings --type-aware --report-unused-disable-directives $FILES",
"lint:js:fix": "NODE_OPTIONS=\"--import @oxc-node/core/register\" oxlint --deny-warnings --type-aware --report-unused-disable-directives --fix $FILES",
"lint:json": "eslint --max-warnings=0 --cache --cache-location=.eslintcache.json --cache-strategy=content ${FILES:-.}",
"lint:json:fix": "eslint --fix --max-warnings=0 --cache --cache-location=.eslintcache.json --cache-strategy=content ${FILES:-.}",

Remove any NODE_OPTIONS='--max-old-space-size=8192' from old lint:js — oxlint is native and doesn't need it.

Step 4: Add devDependencies

Ensure these are in devDependencies: @oxc-node/core@^0.0.35, oxlint@^1.50.0, oxlint-tsgolint@^0.15.0.

Step 5: Verify

pnpm lint

Fix any failures (unused disable directives, rule name changes, etc.).

Package List

All 50 packages that need migration, grouped by directory:

utils/ (9 packages)

  • utils/promise, utils/array, utils/b-tree, utils/ipc, utils/redis-client, utils/redis-streams, utils/logger, utils/config, utils/common

common/ (8 packages)

  • common/consts, common/di, common/errors, common/permissions, common/routing-layer, common/shared-config, common/ml-service-local-client, common/permissions-service-client

contexts/ (7 packages)

  • contexts/auth (special: has no eslint config — may need no migration or needs config created from scratch)
  • contexts/service-layer, contexts/compliance, contexts/scoring, contexts/versioning, contexts/workflow-engine, contexts/regulatory-compliance

data-access-layer/ (4 packages)

  • data-access-layer/data-layer, data-access-layer/json-api, data-access-layer/legacy-data-layer, data-access-layer/legacy-json-api

tools/ (3 packages)

  • tools/eslint-rules, tools/template-lint-rules, tools/codemods

integrations/ (1 package)

  • integrations/storage

test-runner/ (1 package)

  • test-runner

templates (2 packages)

  • create-new-package, create-new-dal-package

Automation Script

Two scripts at /tmp/ (not committed) handle the mechanical migration:

  • /tmp/migrate-to-oxlint.sh <package-dir> — full pipeline for a single package
  • /tmp/fix-oxlint-config.js — helper to rewrite the migration-tool-generated oxlint.config.ts

What the script does (6 steps):

  1. Run soxhub-oxlint-migrate --minimal
  2. Update package.json: replace lint:js scripts, add lint:json scripts, add devDeps (@oxc-node/core, oxlint, oxlint-tsgolint, globals), remove lint:ox
  3. Rewrite oxlint.config.ts via fix-oxlint-config.js:
    • Fix indentation, add globals import and CJS override
    • Remove JSON-file overrides (tsconfig*.json), remove @auditboard/eslint-plugin from jsPlugins
    • Add no-unsafe-assignment disable comment
  4. Write JSON-only eslint.config.mjs (detects @auditboard/no-dependency-tsconfig-paths rule automatically)
  5. Autofix: lint:json:fix, lint:js:fix, lint:prettier:fix, clean up generated files
  6. Run final lint checks (lint:js, lint:json, lint:prettier, lint:types) and report pass/fail

What the script does NOT handle (Claude does these):

  • Git branching/committing/PR creation
  • Fixing remaining lint failures after autofix
  • Decisions about unsupported plugins or rule behavior differences

Execution Approach

Each package gets its own PR targeted at the oxlint-packages branch. We ramp up in batches:

Batch 1 — One at a time, validate output (3 packages)

  1. utils/promise
  2. utils/array
  3. utils/b-tree

Batch 2 — 5 packages

  1. utils/ipc
  2. utils/redis-client
  3. utils/redis-streams
  4. utils/logger
  5. utils/config

Batch 3 — 10 packages

  1. utils/common
  2. common/consts
  3. common/di
  4. common/errors
  5. common/shared-config
  6. common/ml-service-local-client
  7. common/permissions-service-client
  8. common/routing-layer
  9. common/permissions
  10. integrations/storage

Batch 4 — Remaining packages

  1. tools/eslint-rules
  2. tools/template-lint-rules
  3. tools/codemods
  4. contexts/service-layer
  5. data-access-layer/data-layer
  6. data-access-layer/json-api
  7. data-access-layer/legacy-data-layer
  8. data-access-layer/legacy-json-api
  9. contexts/compliance
  10. contexts/scoring
  11. contexts/versioning
  12. contexts/workflow-engine
  13. contexts/regulatory-compliance
  14. test-runner
  15. create-new-package
  16. create-new-dal-package
  17. contexts/auth (special case — investigate first)

Update this plan with learnings after each batch.

Special Cases

  • contexts/auth: Has no eslint config or lint:js script. Currently linted by root config only. Investigate whether it needs its own oxlint config or can remain as-is.
  • tools/eslint-rules: This IS @auditboard/eslint-plugin. Migrating its own linting is fine, but the package must continue to work as an ESLint plugin.
  • create-new-package / create-new-dal-package: Templates for new packages. After migration, new packages scaffolded from these will have oxlint configs.
  • Packages with unsupported plugins (tsdoc, decorator-position, simple-import-sort): The migration tool will flag these. Most plugins used in this repo (freeze-global, formatjs, no-only-tests, etc.) are already supported as jsPlugins in the @soxhub/lint/oxlint base config. For truly unsupported plugins, decide per-package whether to drop, keep in a hybrid ESLint config, or add as jsPlugin.

Verification

After each package migration, pnpm lint in that package must pass. After all packages are migrated, run pnpm lint from the root to verify the full monorepo.

Progress

Batch 1 (complete)

  • utils/promise — PR #31259
  • utils/array — PR #31260
  • utils/b-tree — PR #31261

Batch 2 (in progress)

  • utils/ipc — PR #31264
  • utils/redis-client — pending
  • utils/redis-streams — pending
  • utils/logger — pending
  • utils/config — pending

Learnings

CJS globals

oxlint's ESM mode doesn't know about CJS globals (require, module, exports). Files like .prettierrc.cjs will fail no-undef. Fix: add globals package as devDep and use globals.commonjs in an override for **/*.cjs files. The script handles this automatically.

Import sorting and array-type

oxlint autofixes Array<T>T[] and import sorting. Running lint:js:fix before final checks handles these automatically.

Inline disables needed

Some legitimate patterns trigger oxlint rules:

  • unicorn/no-thenable on test objects implementing .then() — needs oxlint-disable-next-line
  • unicorn/no-new-array on intentional pre-allocations — needs oxlint-disable-next-line
  • oxlint uses oxlint-disable-next-line syntax, not eslint-disable-next-line

Pre-commit hook in worktrees

The repo's pre-commit hook uses readlink -f "$0" which resolves to the main worktree path (auditboard-backend) rather than the current worktree (auditboard-backend-tertiary). When pnpm-lock.yaml is staged, the hook fails with "outside repository". Workaround: commit code changes first (without lockfile), then commit lockfile separately (which succeeds because the node-interfaces/DEPRULES checks pass on the second attempt).

Script evolution

The script no longer manages pnpm overrides (add/remove @soxhub/lint and @soxhub/eslint-plugin file overrides). The @soxhub/lint version in the repo now supports the migration tool directly.

@auditboard/eslint-plugin is JSON-only

The @auditboard/no-dependency-tsconfig-paths rule only applies to tsconfig*.json files. The script detects this rule in the existing eslint config and preserves it in the JSON-only eslint config. The plugin must be removed from oxlint's jsPlugins.

#!/bin/bash
set -euo pipefail
PACKAGE_DIR="$1"
REPO_ROOT="$(git rev-parse --show-toplevel)"
FULL_PATH="$REPO_ROOT/$PACKAGE_DIR"
if [ -z "$PACKAGE_DIR" ]; then
echo "Usage: $0 <package-dir>"
exit 1
fi
if [ ! -d "$FULL_PATH" ]; then
echo "ERROR: Directory not found: $FULL_PATH"
exit 1
fi
cd "$REPO_ROOT"
echo "=== Migrating $PACKAGE_DIR ==="
# ============================================================
# Step 1: Run migration tool
# ============================================================
echo ""
echo "--- Step 1: Running soxhub-oxlint-migrate --minimal ---"
cd "$FULL_PATH"
pnpm exec soxhub-oxlint-migrate --minimal 2>&1 || {
echo "WARNING: Migration tool failed. oxlint.config.ts may need manual creation."
}
# ============================================================
# Detect CJS files
# ============================================================
HAS_CJS=false
if find . -name "*.cjs" -not -path "*/node_modules/*" -not -path "*/build/*" | grep -q .; then
HAS_CJS=true
echo "CJS files detected — will add globals override"
else
echo "No CJS files detected — skipping globals override"
fi
# ============================================================
# Step 2: Update package.json (scripts + devDeps, remove lint:ox)
# ============================================================
echo ""
echo "--- Step 2: Updating package.json ---"
TMPFILE=$(mktemp)
jq --argjson hasCjs "$HAS_CJS" '
.scripts["lint:js"] = "NODE_OPTIONS=\"--import @oxc-node/core/register\" oxlint --deny-warnings --type-aware --report-unused-disable-directives $FILES" |
.scripts["lint:js:fix"] = "NODE_OPTIONS=\"--import @oxc-node/core/register\" oxlint --deny-warnings --type-aware --report-unused-disable-directives --fix $FILES" |
.scripts["lint:json"] = "eslint --max-warnings=0 --cache --cache-location=.eslintcache.json --cache-strategy=content ${FILES:-.}" |
.scripts["lint:json:fix"] = "eslint --fix --max-warnings=0 --cache --cache-location=.eslintcache.json --cache-strategy=content ${FILES:-.}" |
del(.scripts["lint:ox"]) |
.devDependencies["@oxc-node/core"] //= "^0.0.35" |
.devDependencies["oxlint"] //= "^1.50.0" |
.devDependencies["oxlint-tsgolint"] //= "^0.15.0" |
(if $hasCjs then .devDependencies["globals"] //= "^17.3.0" else . end)
' package.json > "$TMPFILE" && mv "$TMPFILE" package.json
echo "Updated package.json"
# ============================================================
# Step 3: Rewrite oxlint.config.ts using external helper
# ============================================================
echo ""
echo "--- Step 3: Rewriting oxlint.config.ts ---"
if [ -f "oxlint.config.ts" ]; then
node /tmp/fix-oxlint-config.js oxlint.config.ts "$HAS_CJS" 2>&1
echo "Rewrote oxlint.config.ts"
else
echo "WARNING: No oxlint.config.ts found"
fi
# ============================================================
# Step 4: Write eslint.config.mjs (JSON-only)
# ============================================================
echo ""
echo "--- Step 4: Writing JSON-only eslint.config.mjs ---"
# Check if existing config has @auditboard/no-dependency-tsconfig-paths
HAS_TSCONFIG_RULE=false
if [ -f "eslint.config.mjs" ] && grep -q "no-dependency-tsconfig-paths" eslint.config.mjs; then
HAS_TSCONFIG_RULE=true
fi
# Extract ignorePatterns for eslint globalIgnores
IGNORE_PATTERNS=$(node -e "
const fs = require('fs');
const content = fs.readFileSync('oxlint.config.ts', 'utf8');
const match = content.match(/ignorePatterns:\\s*\\[(.*?)\\]/);
if (match) {
const items = match[1].match(/'([^']+)'/g);
if (items) console.log(items.map(i => i.replace(/'/g, '')).join(','));
} else {
console.log('build');
}
" 2>/dev/null || echo "build")
IFS=',' read -ra IGNORE_ARRAY <<< "$IGNORE_PATTERNS"
IGNORE_JS=""
for pattern in "${IGNORE_ARRAY[@]}"; do
pattern=$(echo "$pattern" | xargs)
IGNORE_JS="$IGNORE_JS'$pattern', "
done
IGNORE_JS="${IGNORE_JS%, }"
if [ "$HAS_TSCONFIG_RULE" = true ]; then
cat > eslint.config.mjs << 'ESLINT_EOF'
import { defineConfig, globalIgnores } from 'eslint/config';
import jsoncParser from 'jsonc-eslint-parser';
import auditboardPlugin from '@auditboard/eslint-plugin';
import soxhubLint from '@soxhub/lint/eslint';
const { node } = soxhubLint;
const nodeConfig = node({ root: true, path: import.meta.dirname });
export default defineConfig([
globalIgnores(['**/*', '!**/*.json', '!**/*.jsonc']),
ESLINT_EOF
echo " globalIgnores([$IGNORE_JS])," >> eslint.config.mjs
cat >> eslint.config.mjs << 'ESLINT_EOF'
nodeConfig,
{
files: ['**/tsconfig*.json'],
plugins: {
'@auditboard': auditboardPlugin,
},
languageOptions: {
parser: jsoncParser,
},
rules: {
'@auditboard/no-dependency-tsconfig-paths': 'error',
},
},
]);
ESLINT_EOF
else
cat > eslint.config.mjs << 'ESLINT_EOF'
import { defineConfig, globalIgnores } from 'eslint/config';
import soxhubLint from '@soxhub/lint/eslint';
const { node } = soxhubLint;
const nodeConfig = node({ root: true, path: import.meta.dirname });
export default defineConfig([
globalIgnores(['**/*', '!**/*.json', '!**/*.jsonc']),
ESLINT_EOF
echo " globalIgnores([$IGNORE_JS])," >> eslint.config.mjs
cat >> eslint.config.mjs << 'ESLINT_EOF'
nodeConfig,
]);
ESLINT_EOF
fi
echo "Wrote JSON-only eslint.config.mjs (has tsconfig rule: $HAS_TSCONFIG_RULE)"
# ============================================================
# Step 5: Fix JSON sort order, run autofix, prettier, clean up
# ============================================================
echo ""
echo "--- Step 5: Autofixing and cleaning up ---"
cd "$FULL_PATH"
# Fix JSON sort order
echo ">> lint:json:fix"
pnpm lint:json:fix 2>&1 || true
rm -f .eslintcache.json
# Run oxlint autofix (import sorting, array-type, etc.)
echo ">> lint:js:fix"
pnpm lint:js:fix 2>&1 || true
# Run prettier fix (fixes formatting from autofix changes)
echo ">> lint:prettier:fix"
pnpm lint:prettier:fix 2>&1 || true
# Clean up generated files
rm -f oxlint-migration-report.md .eslintcache.json
# ============================================================
# Step 6: Run lint checks
# ============================================================
echo ""
echo "--- Step 6: Running final lint checks ---"
LINT_PASS=true
echo ">> lint:js"
if ! pnpm lint:js 2>&1; then
LINT_PASS=false
echo "FAILED: lint:js"
fi
echo ">> lint:json"
if ! pnpm lint:json 2>&1; then
LINT_PASS=false
echo "FAILED: lint:json"
fi
rm -f .eslintcache.json
echo ">> lint:prettier"
if ! pnpm lint:prettier 2>&1; then
LINT_PASS=false
echo "FAILED: lint:prettier"
fi
echo ">> lint:types"
if ! pnpm lint:types 2>&1; then
LINT_PASS=false
echo "FAILED: lint:types"
fi
echo ""
if [ "$LINT_PASS" = true ]; then
echo "=== ALL LINT CHECKS PASSED ==="
else
echo "=== SOME LINT CHECKS FAILED — manual fixes needed ==="
fi
echo ""
echo "=== REMAINING STEPS ==="
echo "1. Review oxlint.config.ts for any package-specific adjustments"
echo "2. Fix any remaining lint failures above"
echo "3. git add + commit (include pnpm-lock.yaml)"
echo "4. Push and create PR"
echo "==========================="
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment