Skip to content

feat: implement stages 9-20#9

Open
pmclSF wants to merge 4 commits intomainfrom
feat/stages-9-20
Open

feat: implement stages 9-20#9
pmclSF wants to merge 4 commits intomainfrom
feat/stages-9-20

Conversation

@pmclSF
Copy link
Owner

@pmclSF pmclSF commented Feb 28, 2026

Summary

  • Stages 9 & 17: Enhanced 8-step wizard UI with new reusable components (SeverityBadge, DiffViewer, TreePreview, FindingsFilter) and deeper integration across AssessPage, ConfigurePage, MergePage, and OperatePage
  • Stage 10: History preservation prerequisites checking and dry-run reporting (commit count, contributors, estimated time)
  • Stage 11: Full lifecycle CLI commands (add, archive, migrate-branch) with typed plan artifacts (AddPlan, ArchivePlan, BranchPlan)
  • Stage 12: Extended analysis engine with 6 new analyzers (environment, tooling, CI, publishing, repo risks, risk summary) wired into the analyze command
  • Stage 13: Path-filtered GitHub Actions workflow generation with dorny/paths-filter and matrix strategy
  • Stage 14: Configure engine for safe scaffolding of Prettier, ESLint (JSON only), and TypeScript project references
  • Stage 15: Dependency enforcement via pnpm overrides / yarn resolutions / npm overrides with workspace protocol normalization
  • Stage 16: Cross-platform path normalization utility
  • Stage 18: Smart defaults with evidence-based suggestions for package manager, workspace tool, and dependency strategy, plus ActionableError shaping
  • Stage 19: Multi-language detection and scaffolding for Go (go.work), Rust (Cargo.toml workspace), and Python (recommendations)
  • Stage 20: Performance utilities including limited-concurrency mapper (pMap), cross-platform disk space check, and progress event emitter

Test plan

  • 674 unit tests passing across 49 test files
  • TypeScript type-check (tsc --noEmit) passes cleanly
  • All new source files have corresponding test coverage
  • Manual smoke test: node bin/monorepo.js --help shows all commands
  • Manual smoke test: node bin/monorepo.js ui serves working wizard

🤖 Generated with Claude Code

…s, and UI wizard

Add 12 stages of functionality including:
- Stage 9/17: Enhanced 8-step wizard UI with SeverityBadge, DiffViewer, TreePreview,
  FindingsFilter components and deeper page integration
- Stage 10: History preservation prerequisites and dry-run reporting
- Stage 11: Full lifecycle CLI commands (add, archive, migrate-branch) with plan types
- Stage 12: Extended analysis (environment, tooling, CI, publishing, repo risks) with
  risk classification
- Stage 13: Path-filtered GitHub Actions workflow generation
- Stage 14: Configure engine for Prettier/ESLint/TypeScript scaffolding
- Stage 15: Dependency enforcement via overrides/resolutions
- Stage 16: Cross-platform path normalization
- Stage 18: Smart defaults with evidence-based suggestions and error shaping
- Stage 19: Multi-language detection and scaffolding (Go, Rust, Python)
- Stage 20: Performance utilities (pMap concurrency, disk space check, progress emitter)

674 unit tests passing across 49 test files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

// Fetch
logger.info(`Fetching ${branch}...`);
await safeExecFile('git', ['fetch', remoteName, branch], {

Check failure

Code scanning / CodeQL

Second order command injection High

Command line argument that depends on
a user-provided value
can execute an arbitrary command if --upload-pack is used with git.

Copilot Autofix

AI 17 days ago

In general: to fix this issue, we must validate and constrain user-controlled values before they reach git. For branch, that means only permitting branch-like names (e.g., alphanumerics plus a small set of punctuation such as /, -, _, ., limiting length, and rejecting values that start with - or contain whitespace or globbing characters). Similarly, we should restrict subdir (coming from options.subdir or defaulting to plan.sourceRepo) to a safe relative path pattern and disallow .., absolute paths, or path separators that could escape the repo. sourceRepo and targetMonorepo appear to be local filesystem paths; these are used as cwd and remote URL for git remote add, so at minimum we should ensure they are strings and non-empty, but the second-order injection highlighted by CodeQL is primarily about the branch argument.

Best single fix without changing existing behavior: add explicit validation helpers in src/server/routes/migrate-branch.ts and apply them to branch and options.subdir before we ever call generateBranchPlan/applyBranchPlan. This keeps all logic at the HTTP boundary and avoids modifying the strategy logic. Concretely:

  • In src/server/routes/migrate-branch.ts, define:
    • isValidBranchName(name: string): boolean using a conservative regex (e.g., /^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/) and additional checks like “not starting with -” and no consecutive .. to keep it simple and safe.
    • isValidSubdir(subdir: string): boolean that ensures it’s a relative directory name without .., without starting with / or \, and containing only safe characters like A-Za-z0-9._/-.
  • In the POST handler:
    • After checking branch is a string, call isValidBranchName(branch); if it fails, return HTTP 400 with a clear error.
    • If options?.subdir is provided, check it is a string and passes isValidSubdir; otherwise return 400.
  • Then pass branch and options.subdir as before. Because we are only rejecting obviously unsafe names and not transforming values, normal use (with typical git-style branch names) will continue to work.

No changes to imports are strictly required (we can implement these helpers inline). We do not need to alter src/strategies/migrate-branch.ts for this fix; validating at the route layer is sufficient to break the tainted flow.


Suggested changeset 1
src/server/routes/migrate-branch.ts
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/server/routes/migrate-branch.ts b/src/server/routes/migrate-branch.ts
--- a/src/server/routes/migrate-branch.ts
+++ b/src/server/routes/migrate-branch.ts
@@ -5,6 +5,49 @@
 import { generateBranchPlan, applyBranchPlan } from '../../strategies/migrate-branch.js';
 import type { BranchMigrateStrategy } from '../../types/index.js';
 
+function isValidBranchName(branch: string): boolean {
+  // Conservative allowlist for git branch names:
+  // - start with an alphanumeric character
+  // - allow alphanumerics plus '.', '_', '-', '/'
+  // - disallow leading '-' and whitespace or control characters
+  if (!branch || typeof branch !== 'string') {
+    return false;
+  }
+  if (branch.length > 255) {
+    return false;
+  }
+  if (branch.startsWith('-')) {
+    return false;
+  }
+  if (!/^[A-Za-z0-9][A-Za-z0-9._/-]*$/.test(branch)) {
+    return false;
+  }
+  return true;
+}
+
+function isValidSubdir(subdir: string): boolean {
+  // Only allow simple, relative paths inside the repo:
+  // - no absolute paths
+  // - no path traversal
+  // - limited character set
+  if (!subdir || typeof subdir !== 'string') {
+    return false;
+  }
+  if (subdir.length > 255) {
+    return false;
+  }
+  if (subdir.startsWith('/') || subdir.startsWith('\\')) {
+    return false;
+  }
+  if (subdir.includes('..')) {
+    return false;
+  }
+  if (!/^[A-Za-z0-9._/-]+$/.test(subdir)) {
+    return false;
+  }
+  return true;
+}
+
 export function migrateBranchRoute(hub: WsHub): Router {
   const router = Router();
 
@@ -16,6 +59,11 @@
       return;
     }
 
+    if (!isValidBranchName(branch)) {
+      res.status(400).json({ error: 'Invalid branch name' });
+      return;
+    }
+
     if (!sourceRepo || typeof sourceRepo !== 'string') {
       res.status(400).json({ error: 'Request body must include a "sourceRepo" string' });
       return;
@@ -26,6 +74,13 @@
       return;
     }
 
+    if (options?.subdir !== undefined) {
+      if (typeof options.subdir !== 'string' || !isValidSubdir(options.subdir)) {
+        res.status(400).json({ error: 'Invalid "subdir" option' });
+        return;
+      }
+    }
+
     const opId = crypto.randomUUID();
     hub.createOperation(opId);
     res.status(202).json({ opId });
EOF
Copilot is powered by AI and may make mistakes. Always verify output.
// Use wmic on Windows
const drive = path.parse(path.resolve(dirPath)).root;
const { stdout } = await execFileAsync('wmic', [
'logicaldisk', 'where', `DeviceID='${drive.replace('\\', '')}'`,

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High

This replaces only the first occurrence of '\'.

Copilot Autofix

AI 17 days ago

In general, when sanitizing strings by removing or escaping certain characters, use a global regular expression (/pattern/g) so that all occurrences are handled, not just the first. For backslash removal, this means using .replace(/\\/g, '') instead of .replace('\\', '').

For this specific code in src/utils/disk.ts, update the construction of the DeviceID filter on line 18 so that all backslashes are removed from drive. Change:

`DeviceID='${drive.replace('\\', '')}'`,

to:

`DeviceID='${drive.replace(/\\/g, '')}'`,

This preserves the intended functionality—turning "C:\\" into "C:"—while correctly handling any unexpected extra backslashes in drive. No new imports or auxiliary methods are needed; this is a localized change within the existing function.

Suggested changeset 1
src/utils/disk.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/utils/disk.ts b/src/utils/disk.ts
--- a/src/utils/disk.ts
+++ b/src/utils/disk.ts
@@ -15,7 +15,7 @@
       // Use wmic on Windows
       const drive = path.parse(path.resolve(dirPath)).root;
       const { stdout } = await execFileAsync('wmic', [
-        'logicaldisk', 'where', `DeviceID='${drive.replace('\\', '')}'`,
+        'logicaldisk', 'where', `DeviceID='${drive.replace(/\\/g, '')}'`,
         'get', 'FreeSpace', '/format:value',
       ]);
       const match = stdout.match(/FreeSpace=(\d+)/);
EOF
@@ -15,7 +15,7 @@
// Use wmic on Windows
const drive = path.parse(path.resolve(dirPath)).root;
const { stdout } = await execFileAsync('wmic', [
'logicaldisk', 'where', `DeviceID='${drive.replace('\\', '')}'`,
'logicaldisk', 'where', `DeviceID='${drive.replace(/\\/g, '')}'`,
'get', 'FreeSpace', '/format:value',
]);
const match = stdout.match(/FreeSpace=(\d+)/);
Copilot is powered by AI and may make mistakes. Always verify output.
pmclSF and others added 3 commits February 28, 2026 04:39
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ckers

SEC-01: Sanitize Python injection in history-preserve commitPrefix
SEC-02: Add path traversal prevention in apply command
SEC-03: Add Bearer token auth to server API and WebSocket
SEC-04: Allowlist install command executables in apply
SEC-05: Replace shell exec() with execFile() in ui command
SEC-06: Cap concurrent operations and event buffer in WsHub

Publishing: rename to monotize, add LICENSE/SECURITY.md/CHANGELOG.md,
add files field, author, repository metadata, semver and js-yaml deps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant