mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 00:16:46 +00:00
chore(hardening): add debt inventory and async io kickoff
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
# Hardening Debt Burndown Tracker
|
||||
|
||||
Last Updated: 2026-02-12
|
||||
Owner: Stream D (`#542`)
|
||||
|
||||
## Scope
|
||||
|
||||
Maintainability hardening groundwork with low-risk changes:
|
||||
|
||||
- Inventory legacy shims/compatibility markers
|
||||
- Inventory sync filesystem usage, especially runtime hotpaths
|
||||
- Incrementally migrate hotpath sync I/O to async I/O with tests
|
||||
|
||||
## How to Measure
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
bun run report:hardening
|
||||
```
|
||||
|
||||
Generated artifacts:
|
||||
|
||||
- `docs/reports/hardening-inventory.json`
|
||||
- `docs/reports/hardening-inventory.md`
|
||||
|
||||
## Kickoff Baseline (Issue #542 Stream D)
|
||||
|
||||
The current baseline is sourced from `docs/reports/hardening-inventory.json` after running `bun run report:hardening`.
|
||||
Baseline captured: `2026-02-12`.
|
||||
|
||||
| Metric | Baseline |
|
||||
|---|---:|
|
||||
| Sync fs occurrences (all) | 841 |
|
||||
| Sync fs files affected (all) | 100 |
|
||||
| Sync fs occurrences (runtime hotpaths) | 730 |
|
||||
| Sync fs files affected (runtime hotpaths) | 89 |
|
||||
| Legacy shim markers | 131 |
|
||||
| Legacy shim files affected | 56 |
|
||||
|
||||
## Initial Async I/O Migration Log
|
||||
|
||||
| Date | Area | Change | Safety Notes |
|
||||
|---|---|---|---|
|
||||
| 2026-02-12 | `src/web-server/jsonl-parser.ts` | Migrated `parseProjectDirectory()` directory listing from sync `readdirSync` to async `fs.promises.readdir` | Existing behavior kept (same filtering/fallback); covered by `tests/unit/jsonl-parser.test.ts` |
|
||||
+11
-1
@@ -1,6 +1,6 @@
|
||||
# CCS Project Roadmap
|
||||
|
||||
Last Updated: 2026-02-04
|
||||
Last Updated: 2026-02-12
|
||||
|
||||
Forward-looking roadmap documenting current priorities, GitHub issues, and future feature plans.
|
||||
|
||||
@@ -39,6 +39,15 @@ All major modularization work is complete. The codebase evolved from monolithic
|
||||
|
||||
## Current Status
|
||||
|
||||
### Maintainability Hardening Kickoff
|
||||
|
||||
- Issue owner: Stream D for **#542**
|
||||
- Automated inventory command: `bun run report:hardening`
|
||||
- Generated report artifacts:
|
||||
- `docs/reports/hardening-inventory.json`
|
||||
- `docs/reports/hardening-inventory.md`
|
||||
- Debt burndown tracker: [Hardening Debt Burndown Tracker](./hardening-debt-burndown.md)
|
||||
|
||||
### Remaining Large Files (Acceptable)
|
||||
|
||||
**CLI** (complex core logic):
|
||||
@@ -198,4 +207,5 @@ All criteria achieved:
|
||||
- [Codebase Summary](./codebase-summary.md) - Current structure
|
||||
- [Code Standards](./code-standards.md) - Patterns and conventions
|
||||
- [System Architecture](./system-architecture.md) - Architecture diagrams
|
||||
- [Hardening Debt Burndown Tracker](./hardening-debt-burndown.md) - Legacy shim + sync-fs debt tracking
|
||||
- [CLAUDE.md](../CLAUDE.md) - AI development guidance
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
{
|
||||
"scope": "src/**/*.{ts,tsx,js,jsx,mjs,cjs}",
|
||||
"syncFs": {
|
||||
"totalOccurrences": 841,
|
||||
"filesAffected": 100,
|
||||
"hotpathOccurrences": 730,
|
||||
"hotpathFilesAffected": 89,
|
||||
"topHotpathFiles": [
|
||||
{
|
||||
"file": "src/management/shared-manager.ts",
|
||||
"count": 60,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"cpSync",
|
||||
"existsSync",
|
||||
"lstatSync",
|
||||
"mkdirSync",
|
||||
"readdirSync",
|
||||
"readFileSync",
|
||||
"readlinkSync",
|
||||
"rmSync",
|
||||
"statSync",
|
||||
"symlinkSync",
|
||||
"unlinkSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/utils/claude-symlink-manager.ts",
|
||||
"count": 27,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"lstatSync",
|
||||
"mkdirSync",
|
||||
"readdirSync",
|
||||
"readlinkSync",
|
||||
"renameSync",
|
||||
"rmSync",
|
||||
"statSync",
|
||||
"symlinkSync",
|
||||
"unlinkSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/utils/claude-dir-installer.ts",
|
||||
"count": 23,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"cpSync",
|
||||
"existsSync",
|
||||
"lstatSync",
|
||||
"mkdirSync",
|
||||
"readdirSync",
|
||||
"renameSync",
|
||||
"rmSync",
|
||||
"statSync",
|
||||
"unlinkSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/utils/shell-completion.ts",
|
||||
"count": 23,
|
||||
"calls": [
|
||||
"appendFileSync",
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readFileSync",
|
||||
"statSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/web-server/routes/settings-routes.ts",
|
||||
"count": 23,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readFileSync",
|
||||
"renameSync",
|
||||
"statSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/cliproxy/binary/version-cache.ts",
|
||||
"count": 20,
|
||||
"calls": [
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readFileSync",
|
||||
"unlinkSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/management/recovery-manager.ts",
|
||||
"count": 20,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"renameSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/web-server/routes/cliproxy-stats-routes.ts",
|
||||
"count": 20,
|
||||
"calls": [
|
||||
"closeSync",
|
||||
"existsSync",
|
||||
"fstatSync",
|
||||
"mkdirSync",
|
||||
"openSync",
|
||||
"readdirSync",
|
||||
"readFileSync",
|
||||
"readSync",
|
||||
"renameSync",
|
||||
"statSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/web-server/routes/misc-routes.ts",
|
||||
"count": 20,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readdirSync",
|
||||
"readFileSync",
|
||||
"renameSync",
|
||||
"statSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/web-server/routes/persist-routes.ts",
|
||||
"count": 17,
|
||||
"calls": [
|
||||
"closeSync",
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"lstatSync",
|
||||
"openSync",
|
||||
"readdirSync",
|
||||
"readSync",
|
||||
"renameSync",
|
||||
"unlinkSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
],
|
||||
"topFilesOverall": [
|
||||
{
|
||||
"file": "src/management/shared-manager.ts",
|
||||
"count": 60,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"cpSync",
|
||||
"existsSync",
|
||||
"lstatSync",
|
||||
"mkdirSync",
|
||||
"readdirSync",
|
||||
"readFileSync",
|
||||
"readlinkSync",
|
||||
"rmSync",
|
||||
"statSync",
|
||||
"symlinkSync",
|
||||
"unlinkSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/config/migration-manager.ts",
|
||||
"count": 27,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readdirSync",
|
||||
"readFileSync",
|
||||
"renameSync",
|
||||
"rmdirSync",
|
||||
"unlinkSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/utils/claude-symlink-manager.ts",
|
||||
"count": 27,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"lstatSync",
|
||||
"mkdirSync",
|
||||
"readdirSync",
|
||||
"readlinkSync",
|
||||
"renameSync",
|
||||
"rmSync",
|
||||
"statSync",
|
||||
"symlinkSync",
|
||||
"unlinkSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/utils/claude-dir-installer.ts",
|
||||
"count": 23,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"cpSync",
|
||||
"existsSync",
|
||||
"lstatSync",
|
||||
"mkdirSync",
|
||||
"readdirSync",
|
||||
"renameSync",
|
||||
"rmSync",
|
||||
"statSync",
|
||||
"unlinkSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/utils/shell-completion.ts",
|
||||
"count": 23,
|
||||
"calls": [
|
||||
"appendFileSync",
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readFileSync",
|
||||
"statSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/web-server/routes/settings-routes.ts",
|
||||
"count": 23,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readFileSync",
|
||||
"renameSync",
|
||||
"statSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/cliproxy/binary/version-cache.ts",
|
||||
"count": 20,
|
||||
"calls": [
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readFileSync",
|
||||
"unlinkSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/copilot/copilot-package-manager.ts",
|
||||
"count": 20,
|
||||
"calls": [
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"readFileSync",
|
||||
"rmSync",
|
||||
"unlinkSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/management/recovery-manager.ts",
|
||||
"count": 20,
|
||||
"calls": [
|
||||
"copyFileSync",
|
||||
"existsSync",
|
||||
"mkdirSync",
|
||||
"renameSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
},
|
||||
{
|
||||
"file": "src/web-server/routes/cliproxy-stats-routes.ts",
|
||||
"count": 20,
|
||||
"calls": [
|
||||
"closeSync",
|
||||
"existsSync",
|
||||
"fstatSync",
|
||||
"mkdirSync",
|
||||
"openSync",
|
||||
"readdirSync",
|
||||
"readFileSync",
|
||||
"readSync",
|
||||
"renameSync",
|
||||
"statSync",
|
||||
"writeFileSync"
|
||||
],
|
||||
"markers": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"legacyShim": {
|
||||
"totalMarkers": 131,
|
||||
"filesAffected": 56,
|
||||
"topFiles": [
|
||||
{
|
||||
"file": "src/utils/config-manager.ts",
|
||||
"count": 13,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* Get config file path (legacy JSON path)",
|
||||
"* Precedence: --config-dir flag > CCS_DIR env > CCS_HOME env (legacy, appends .ccs) > ~/.ccs default",
|
||||
"* Read and parse config (legacy compatibility)",
|
||||
"* Returns Config with profiles from unified config.yaml or legacy config.json.",
|
||||
"* Returns config.yaml in unified mode, config.json in legacy mode.",
|
||||
"* then falls back to config.json for backward compatibility.",
|
||||
"// Convert unified cliproxy variants to legacy format",
|
||||
"// Convert unified profiles to legacy format for compatibility",
|
||||
"// If not found in unified config, try legacy config.json as fallback",
|
||||
"// Legacy config is invalid JSON - that's OK in unified mode",
|
||||
"// Legacy mode - read from config.json only",
|
||||
"// Legacy mode: read config.json",
|
||||
"// Merge legacy profiles into available list (avoid duplicates)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/auth/profile-detector.ts",
|
||||
"count": 11,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* - Legacy JSON format (config.json, profiles.json) as fallback",
|
||||
"* 2. User-defined CLIProxy variants (config.cliproxy section) [legacy]",
|
||||
"* 3. Settings-based profiles (config.profiles section) [legacy]",
|
||||
"* 4. Account-based profiles (profiles.json) [legacy]",
|
||||
"* Priority: settings-based profiles (glm/kimi) checked FIRST for backward compatibility.",
|
||||
"// Check if account-based default exists (legacy)",
|
||||
"// Fall back to legacy config",
|
||||
"// Fall back to legacy config display",
|
||||
"// Fall through to legacy if not found in unified config",
|
||||
"// Priority 3: Check settings-based profiles (glm, kimi) - LEGACY FALLBACK",
|
||||
"// Priority 4: Check account-based profiles (work, personal) - LEGACY FALLBACK"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/config/unified-config-loader.ts",
|
||||
"count": 9,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* 'json' if only legacy config exists,",
|
||||
"* Check if legacy config.json exists",
|
||||
"* Get path to legacy config.json",
|
||||
"* Provides fallback to legacy JSON format for backward compatibility.",
|
||||
"// Legacy field for backwards compatibility",
|
||||
"// Legacy fields (deprecated)",
|
||||
"// Legacy fields (keep for backwards compatibility during read)",
|
||||
"partial.websearch?.gemini?.enabled ?? // Legacy fallback",
|
||||
"partial.websearch?.gemini?.timeout ?? // Legacy fallback"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/commands/setup-command.ts",
|
||||
"count": 7,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* 3. Legacy config.json profiles (GLM, Kimi)",
|
||||
"* 4. Legacy profiles.json accounts",
|
||||
"// Also check legacy config.json for existing profiles",
|
||||
"// Has legacy accounts - NOT first time",
|
||||
"// Has legacy profiles - NOT first time",
|
||||
"// Legacy config exists but is invalid - ignore and continue",
|
||||
"// Legacy profiles exists but is invalid - ignore and continue"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/management/checks/config-check.ts",
|
||||
"count": 6,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* - Prefers config.yaml (v2) over config.json (legacy)",
|
||||
"// Fallback to config.json (legacy format)",
|
||||
"// Inform if legacy config.json also exists (purely informational, not a check)",
|
||||
"console.log(` ${info('config.json'.padEnd(22))} Legacy (ignored)`);",
|
||||
"console.log(` ${ok('config.json'.padEnd(22))} Valid (legacy)`);",
|
||||
"info: 'Valid (legacy)',"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/web-server/routes/account-routes.ts",
|
||||
"count": 6,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* Uses ProfileRegistry to read from both legacy (profiles.json)",
|
||||
"// Add legacy profiles first",
|
||||
"// Delete from appropriate config (unified and/or legacy)",
|
||||
"// Get default from unified config first, fallback to legacy",
|
||||
"// Get profiles from both legacy and unified config (same logic as CLI)",
|
||||
"// Use unified config if in unified mode, otherwise use legacy"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/config/migration-manager.ts",
|
||||
"count": 5,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* Check if there are legacy profiles that haven't been migrated to config.yaml.",
|
||||
"* Handles migration from legacy JSON config (v1) to unified YAML config (v2).",
|
||||
"`Profile \"${name}\" exists in both configs with different settings - keeping existing (${existingSettings}), skipping legacy (${pathStr})`",
|
||||
"console.log(infoBox('Migrated legacy profiles to config.yaml', 'SUCCESS'));",
|
||||
"console.log(infoBox('Migration failed - using legacy config', 'WARNING'));"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/api/services/profile-writer.ts",
|
||||
"count": 4,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* Supports both unified YAML config and legacy JSON config.",
|
||||
"/** Create settings.json file for API profile (legacy format) */",
|
||||
"/** Remove API profile from legacy config */",
|
||||
"/** Update config.json with new API profile (legacy format) */"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/cliproxy/quota-fetcher-gemini-cli.ts",
|
||||
"count": 4,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"// Legacy pattern: gemini-email.json",
|
||||
"// Must match account AND be gemini type (or legacy gemini- prefix)",
|
||||
"// Try exact legacy match first",
|
||||
"`gemini-${sanitizedId}.json`, // Legacy format"
|
||||
]
|
||||
},
|
||||
{
|
||||
"file": "src/auth/profile-registry.ts",
|
||||
"count": 3,
|
||||
"calls": [],
|
||||
"markers": [
|
||||
"* Get all profiles merged from both legacy and unified config.",
|
||||
"* Get resolved default profile from unified config first, fallback to legacy.",
|
||||
"// Start with legacy profiles"
|
||||
]
|
||||
}
|
||||
],
|
||||
"explicitShimFiles": [
|
||||
"src/cliproxy/openai-compat-manager.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
# Hardening Inventory Report
|
||||
|
||||
Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
|
||||
|
||||
## Summary
|
||||
|
||||
| Metric | Value |
|
||||
|---|---:|
|
||||
| Sync fs occurrences (all) | 841 |
|
||||
| Sync fs files affected (all) | 100 |
|
||||
| Sync fs occurrences (runtime hotpaths) | 730 |
|
||||
| Sync fs files affected (runtime hotpaths) | 89 |
|
||||
| Legacy shim markers | 131 |
|
||||
| Legacy shim files affected | 56 |
|
||||
|
||||
## Top Runtime Hotpath Sync fs Files
|
||||
|
||||
| File | Sync Calls | API Names |
|
||||
|---|---:|---|
|
||||
| `src/management/shared-manager.ts` | 60 | copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync |
|
||||
| `src/utils/claude-symlink-manager.ts` | 27 | copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync |
|
||||
| `src/utils/claude-dir-installer.ts` | 23 | copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync |
|
||||
| `src/utils/shell-completion.ts` | 23 | appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync |
|
||||
| `src/web-server/routes/settings-routes.ts` | 23 | copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync |
|
||||
| `src/cliproxy/binary/version-cache.ts` | 20 | existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync |
|
||||
| `src/management/recovery-manager.ts` | 20 | copyFileSync, existsSync, mkdirSync, renameSync, writeFileSync |
|
||||
| `src/web-server/routes/cliproxy-stats-routes.ts` | 20 | closeSync, existsSync, fstatSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, statSync, writeFileSync |
|
||||
| `src/web-server/routes/misc-routes.ts` | 20 | copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync |
|
||||
| `src/web-server/routes/persist-routes.ts` | 17 | closeSync, copyFileSync, existsSync, lstatSync, openSync, readdirSync, readSync, renameSync, unlinkSync, writeFileSync |
|
||||
|
||||
## Top Legacy Shim Marker Files
|
||||
|
||||
| File | Marker Count |
|
||||
|---|---:|
|
||||
| `src/utils/config-manager.ts` | 13 |
|
||||
| `src/auth/profile-detector.ts` | 11 |
|
||||
| `src/config/unified-config-loader.ts` | 9 |
|
||||
| `src/commands/setup-command.ts` | 7 |
|
||||
| `src/management/checks/config-check.ts` | 6 |
|
||||
| `src/web-server/routes/account-routes.ts` | 6 |
|
||||
| `src/config/migration-manager.ts` | 5 |
|
||||
| `src/api/services/profile-writer.ts` | 4 |
|
||||
| `src/cliproxy/quota-fetcher-gemini-cli.ts` | 4 |
|
||||
| `src/auth/profile-registry.ts` | 3 |
|
||||
|
||||
## Explicit Shim/Re-export Files
|
||||
|
||||
- `src/cliproxy/openai-compat-manager.ts`
|
||||
@@ -73,6 +73,7 @@
|
||||
"test:npm": "bun test tests/npm/",
|
||||
"test:native": "bash tests/native/unix/edge-cases.sh",
|
||||
"test:e2e": "bun test tests/e2e/ --bail --timeout 60000",
|
||||
"report:hardening": "node scripts/hardening-inventory.js",
|
||||
"dev": "bun run build:server && bun dist/ccs.js config --dev",
|
||||
"dev:symlink": "bash scripts/dev-symlink.sh",
|
||||
"dev:unlink": "bash scripts/dev-symlink.sh --restore",
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const ROOT_DIR = path.resolve(__dirname, '..');
|
||||
const SRC_DIR = path.join(ROOT_DIR, 'src');
|
||||
const REPORT_DIR = path.join(ROOT_DIR, 'docs', 'reports');
|
||||
const JSON_REPORT_PATH = path.join(REPORT_DIR, 'hardening-inventory.json');
|
||||
const MD_REPORT_PATH = path.join(REPORT_DIR, 'hardening-inventory.md');
|
||||
|
||||
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
|
||||
const HOTPATH_PATTERNS = [
|
||||
/^src\/web-server\//,
|
||||
/^src\/commands\//,
|
||||
/^src\/cliproxy\//,
|
||||
/^src\/management\//,
|
||||
/^src\/auth\//,
|
||||
/^src\/delegation\//,
|
||||
/^src\/utils\//,
|
||||
/^src\/ccs\.ts$/,
|
||||
];
|
||||
|
||||
const SYNC_CALL_NAMES = [
|
||||
'accessSync',
|
||||
'appendFileSync',
|
||||
'chmodSync',
|
||||
'chownSync',
|
||||
'closeSync',
|
||||
'copyFileSync',
|
||||
'cpSync',
|
||||
'existsSync',
|
||||
'fstatSync',
|
||||
'fsyncSync',
|
||||
'ftruncateSync',
|
||||
'futimesSync',
|
||||
'lchmodSync',
|
||||
'lchownSync',
|
||||
'linkSync',
|
||||
'lstatSync',
|
||||
'mkdirSync',
|
||||
'mkdtempSync',
|
||||
'openSync',
|
||||
'opendirSync',
|
||||
'readFileSync',
|
||||
'readdirSync',
|
||||
'readlinkSync',
|
||||
'readSync',
|
||||
'readvSync',
|
||||
'realpathSync',
|
||||
'renameSync',
|
||||
'rmSync',
|
||||
'rmdirSync',
|
||||
'statSync',
|
||||
'symlinkSync',
|
||||
'truncateSync',
|
||||
'unlinkSync',
|
||||
'utimesSync',
|
||||
'writeFileSync',
|
||||
'writeSync',
|
||||
'writevSync',
|
||||
];
|
||||
|
||||
const SYNC_CALL_LINE_REGEX = new RegExp(`\\b(?:fs\\.)?(?:${SYNC_CALL_NAMES.join('|')})\\b`);
|
||||
const SYNC_CALL_CAPTURE_REGEX = new RegExp(`\\b(?:fs\\.)?(${SYNC_CALL_NAMES.join('|')})\\b`, 'g');
|
||||
const LEGACY_MARKER_REGEX =
|
||||
/(?:\blegacy\b|\bshim\b|backward compatibility|backwards compatibility|compatibility layer|deprecated.*re-export|re-export.*compatibility)/i;
|
||||
|
||||
function toPosixPath(filePath) {
|
||||
return filePath.split(path.sep).join('/');
|
||||
}
|
||||
|
||||
function relativePath(filePath) {
|
||||
return toPosixPath(path.relative(ROOT_DIR, filePath));
|
||||
}
|
||||
|
||||
function isSourceFile(filePath) {
|
||||
return SOURCE_EXTENSIONS.has(path.extname(filePath));
|
||||
}
|
||||
|
||||
function isHotpath(filePath) {
|
||||
return HOTPATH_PATTERNS.some((pattern) => pattern.test(filePath));
|
||||
}
|
||||
|
||||
function walkFiles(dirPath) {
|
||||
const output = [];
|
||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
output.push(...walkFiles(fullPath));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile() && isSourceFile(fullPath)) {
|
||||
output.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function uniqueSorted(values) {
|
||||
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function sortByCountDesc(items) {
|
||||
return [...items].sort((a, b) => {
|
||||
if (b.count !== a.count) return b.count - a.count;
|
||||
return a.file.localeCompare(b.file);
|
||||
});
|
||||
}
|
||||
|
||||
function summarize(items, limit = 10) {
|
||||
return sortByCountDesc(items)
|
||||
.slice(0, limit)
|
||||
.map((item) => ({
|
||||
file: item.file,
|
||||
count: item.count,
|
||||
calls: uniqueSorted(item.calls || []),
|
||||
markers: uniqueSorted(item.markers || []),
|
||||
}));
|
||||
}
|
||||
|
||||
function buildReport() {
|
||||
const files = walkFiles(SRC_DIR);
|
||||
const syncEntries = [];
|
||||
const legacyEntries = [];
|
||||
|
||||
for (const fullPath of files) {
|
||||
const file = relativePath(fullPath);
|
||||
const lines = fs.readFileSync(fullPath, 'utf8').split(/\r?\n/);
|
||||
|
||||
let syncCount = 0;
|
||||
const syncCalls = [];
|
||||
let legacyCount = 0;
|
||||
const legacyMarkers = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (SYNC_CALL_LINE_REGEX.test(line)) {
|
||||
const matches = [...line.matchAll(SYNC_CALL_CAPTURE_REGEX)];
|
||||
syncCount += matches.length;
|
||||
for (const match of matches) {
|
||||
syncCalls.push(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if (LEGACY_MARKER_REGEX.test(line)) {
|
||||
legacyCount += 1;
|
||||
const normalized = line.trim();
|
||||
if (normalized.length > 0) {
|
||||
legacyMarkers.push(normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (syncCount > 0) {
|
||||
syncEntries.push({
|
||||
file,
|
||||
count: syncCount,
|
||||
calls: syncCalls,
|
||||
hotpath: isHotpath(file),
|
||||
});
|
||||
}
|
||||
|
||||
if (legacyCount > 0) {
|
||||
legacyEntries.push({
|
||||
file,
|
||||
count: legacyCount,
|
||||
markers: legacyMarkers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const syncHotpathEntries = syncEntries.filter((entry) => entry.hotpath);
|
||||
const totalSyncCount = syncEntries.reduce((acc, entry) => acc + entry.count, 0);
|
||||
const totalSyncHotpathCount = syncHotpathEntries.reduce((acc, entry) => acc + entry.count, 0);
|
||||
const totalLegacyMarkers = legacyEntries.reduce((acc, entry) => acc + entry.count, 0);
|
||||
|
||||
return {
|
||||
scope: 'src/**/*.{ts,tsx,js,jsx,mjs,cjs}',
|
||||
syncFs: {
|
||||
totalOccurrences: totalSyncCount,
|
||||
filesAffected: syncEntries.length,
|
||||
hotpathOccurrences: totalSyncHotpathCount,
|
||||
hotpathFilesAffected: syncHotpathEntries.length,
|
||||
topHotpathFiles: summarize(syncHotpathEntries),
|
||||
topFilesOverall: summarize(syncEntries),
|
||||
},
|
||||
legacyShim: {
|
||||
totalMarkers: totalLegacyMarkers,
|
||||
filesAffected: legacyEntries.length,
|
||||
topFiles: summarize(legacyEntries),
|
||||
explicitShimFiles: uniqueSorted(
|
||||
legacyEntries
|
||||
.map((entry) => entry.file)
|
||||
.filter((file) => /shim|re-export|compat/i.test(path.basename(file)))
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function renderMarkdown(report) {
|
||||
const lines = [];
|
||||
|
||||
lines.push('# Hardening Inventory Report');
|
||||
lines.push('');
|
||||
lines.push(`Scope: \`${report.scope}\``);
|
||||
lines.push('');
|
||||
lines.push('## Summary');
|
||||
lines.push('');
|
||||
lines.push('| Metric | Value |');
|
||||
lines.push('|---|---:|');
|
||||
lines.push(`| Sync fs occurrences (all) | ${report.syncFs.totalOccurrences} |`);
|
||||
lines.push(`| Sync fs files affected (all) | ${report.syncFs.filesAffected} |`);
|
||||
lines.push(`| Sync fs occurrences (runtime hotpaths) | ${report.syncFs.hotpathOccurrences} |`);
|
||||
lines.push(`| Sync fs files affected (runtime hotpaths) | ${report.syncFs.hotpathFilesAffected} |`);
|
||||
lines.push(`| Legacy shim markers | ${report.legacyShim.totalMarkers} |`);
|
||||
lines.push(`| Legacy shim files affected | ${report.legacyShim.filesAffected} |`);
|
||||
lines.push('');
|
||||
|
||||
lines.push('## Top Runtime Hotpath Sync fs Files');
|
||||
lines.push('');
|
||||
lines.push('| File | Sync Calls | API Names |');
|
||||
lines.push('|---|---:|---|');
|
||||
|
||||
for (const item of report.syncFs.topHotpathFiles) {
|
||||
lines.push(`| \`${item.file}\` | ${item.count} | ${item.calls.join(', ')} |`);
|
||||
}
|
||||
|
||||
if (report.syncFs.topHotpathFiles.length === 0) {
|
||||
lines.push('| _none_ | 0 | - |');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('## Top Legacy Shim Marker Files');
|
||||
lines.push('');
|
||||
lines.push('| File | Marker Count |');
|
||||
lines.push('|---|---:|');
|
||||
|
||||
for (const item of report.legacyShim.topFiles) {
|
||||
lines.push(`| \`${item.file}\` | ${item.count} |`);
|
||||
}
|
||||
|
||||
if (report.legacyShim.topFiles.length === 0) {
|
||||
lines.push('| _none_ | 0 |');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push('## Explicit Shim/Re-export Files');
|
||||
lines.push('');
|
||||
for (const file of report.legacyShim.explicitShimFiles) {
|
||||
lines.push(`- \`${file}\``);
|
||||
}
|
||||
if (report.legacyShim.explicitShimFiles.length === 0) {
|
||||
lines.push('- _none_');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function main() {
|
||||
const report = buildReport();
|
||||
|
||||
fs.mkdirSync(REPORT_DIR, { recursive: true });
|
||||
fs.writeFileSync(JSON_REPORT_PATH, JSON.stringify(report, null, 2) + '\n', 'utf8');
|
||||
fs.writeFileSync(MD_REPORT_PATH, renderMarkdown(report), 'utf8');
|
||||
|
||||
const relJson = relativePath(JSON_REPORT_PATH);
|
||||
const relMd = relativePath(MD_REPORT_PATH);
|
||||
|
||||
console.log(`[hardening-inventory] generatedAt=${new Date().toISOString()}`);
|
||||
console.log(
|
||||
`[hardening-inventory] sync-fs total=${report.syncFs.totalOccurrences}, hotpath=${report.syncFs.hotpathOccurrences}`
|
||||
);
|
||||
console.log(
|
||||
`[hardening-inventory] legacy markers total=${report.legacyShim.totalMarkers}, files=${report.legacyShim.filesAffected}`
|
||||
);
|
||||
console.log(`[hardening-inventory] wrote ${relJson}`);
|
||||
console.log(`[hardening-inventory] wrote ${relMd}`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -146,7 +146,7 @@ export async function parseProjectDirectory(projectDir: string): Promise<RawUsag
|
||||
const projectPath = path.basename(projectDir).replace(/-/g, '/');
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(projectDir);
|
||||
const files = await fs.promises.readdir(projectDir);
|
||||
const jsonlFiles = files.filter((f) => f.endsWith('.jsonl'));
|
||||
|
||||
// Parse files sequentially within a project to avoid too many open handles
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Unit tests for JSONL Parser
|
||||
*/
|
||||
|
||||
import { describe, expect, test, beforeEach, afterEach } from 'bun:test';
|
||||
import { describe, expect, test, beforeEach, afterEach, spyOn } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
@@ -254,6 +254,19 @@ describe('parseProjectDirectory', () => {
|
||||
const entries = await parseProjectDirectory('/nonexistent/dir');
|
||||
expect(entries.length).toBe(0);
|
||||
});
|
||||
|
||||
test('returns empty array when directory read fails', async () => {
|
||||
const existsSyncSpy = spyOn(fs, 'existsSync').mockReturnValue(true);
|
||||
const readdirSpy = spyOn(fs.promises, 'readdir').mockRejectedValue(new Error('EACCES'));
|
||||
|
||||
try {
|
||||
const entries = await parseProjectDirectory('/protected/dir');
|
||||
expect(entries).toEqual([]);
|
||||
} finally {
|
||||
existsSyncSpy.mockRestore();
|
||||
readdirSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('findProjectDirectories', () => {
|
||||
|
||||
Reference in New Issue
Block a user