scc-universal 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +44 -0
- package/.cursor/agents/deep-researcher.md +142 -0
- package/.cursor/agents/doc-updater.md +219 -0
- package/.cursor/agents/eval-runner.md +335 -0
- package/.cursor/agents/learning-engine.md +210 -0
- package/.cursor/agents/loop-operator.md +245 -0
- package/.cursor/agents/refactor-cleaner.md +119 -0
- package/.cursor/agents/sf-admin-agent.md +127 -0
- package/.cursor/agents/sf-agentforce-agent.md +126 -0
- package/.cursor/agents/sf-apex-agent.md +117 -0
- package/.cursor/agents/sf-architect.md +426 -0
- package/.cursor/agents/sf-aura-reviewer.md +369 -0
- package/.cursor/agents/sf-bugfix-agent.md +101 -0
- package/.cursor/agents/sf-flow-agent.md +155 -0
- package/.cursor/agents/sf-integration-agent.md +141 -0
- package/.cursor/agents/sf-lwc-agent.md +123 -0
- package/.cursor/agents/sf-review-agent.md +357 -0
- package/.cursor/agents/sf-visualforce-reviewer.md +465 -0
- package/.cursor/hooks/adapter.js +81 -0
- package/.cursor/hooks/after-file-edit.js +26 -0
- package/.cursor/hooks/after-mcp-execution.js +12 -0
- package/.cursor/hooks/after-shell-execution.js +30 -0
- package/.cursor/hooks/after-tab-file-edit.js +12 -0
- package/.cursor/hooks/before-mcp-execution.js +11 -0
- package/.cursor/hooks/before-read-file.js +13 -0
- package/.cursor/hooks/before-shell-execution.js +29 -0
- package/.cursor/hooks/before-submit-prompt.js +23 -0
- package/.cursor/hooks/pre-compact.js +7 -0
- package/.cursor/hooks/session-end.js +10 -0
- package/.cursor/hooks/session-start.js +10 -0
- package/.cursor/hooks/stop.js +18 -0
- package/.cursor/hooks/subagent-start.js +10 -0
- package/.cursor/hooks/subagent-stop.js +10 -0
- package/.cursor/hooks.json +107 -0
- package/.cursor/skills/aside/SKILL.md +115 -0
- package/.cursor/skills/checkpoint/SKILL.md +50 -0
- package/.cursor/skills/configure-scc/SKILL.md +160 -0
- package/.cursor/skills/continuous-agent-loop/SKILL.md +260 -0
- package/.cursor/skills/mcp-server-patterns/SKILL.md +142 -0
- package/.cursor/skills/model-route/SKILL.md +81 -0
- package/.cursor/skills/prompt-optimizer/SKILL.md +366 -0
- package/.cursor/skills/refactor-clean/SKILL.md +133 -0
- package/.cursor/skills/resume-session/SKILL.md +111 -0
- package/.cursor/skills/save-session/SKILL.md +183 -0
- package/.cursor/skills/search-first/SKILL.md +140 -0
- package/.cursor/skills/security-scan/SKILL.md +142 -0
- package/.cursor/skills/sessions/SKILL.md +124 -0
- package/.cursor/skills/sf-agentforce-development/SKILL.md +449 -0
- package/.cursor/skills/sf-apex-async-patterns/SKILL.md +324 -0
- package/.cursor/skills/sf-apex-best-practices/SKILL.md +421 -0
- package/.cursor/skills/sf-apex-constraints/SKILL.md +79 -0
- package/.cursor/skills/sf-apex-cursor/SKILL.md +336 -0
- package/.cursor/skills/sf-apex-enterprise-patterns/SKILL.md +344 -0
- package/.cursor/skills/sf-apex-testing/SKILL.md +407 -0
- package/.cursor/skills/sf-api-design/SKILL.md +237 -0
- package/.cursor/skills/sf-approval-processes/SKILL.md +312 -0
- package/.cursor/skills/sf-aura-development/SKILL.md +260 -0
- package/.cursor/skills/sf-build-fix/SKILL.md +120 -0
- package/.cursor/skills/sf-data-modeling/SKILL.md +274 -0
- package/.cursor/skills/sf-debugging/SKILL.md +362 -0
- package/.cursor/skills/sf-deployment/SKILL.md +291 -0
- package/.cursor/skills/sf-deployment-constraints/SKILL.md +153 -0
- package/.cursor/skills/sf-devops-ci-cd/SKILL.md +322 -0
- package/.cursor/skills/sf-docs-lookup/SKILL.md +100 -0
- package/.cursor/skills/sf-e2e-testing/SKILL.md +321 -0
- package/.cursor/skills/sf-experience-cloud/SKILL.md +248 -0
- package/.cursor/skills/sf-flow-development/SKILL.md +376 -0
- package/.cursor/skills/sf-governor-limits/SKILL.md +319 -0
- package/.cursor/skills/sf-harness-audit/SKILL.md +139 -0
- package/.cursor/skills/sf-help/SKILL.md +156 -0
- package/.cursor/skills/sf-integration/SKILL.md +479 -0
- package/.cursor/skills/sf-lwc-constraints/SKILL.md +128 -0
- package/.cursor/skills/sf-lwc-development/SKILL.md +302 -0
- package/.cursor/skills/sf-lwc-testing/SKILL.md +387 -0
- package/.cursor/skills/sf-metadata-management/SKILL.md +285 -0
- package/.cursor/skills/sf-platform-events-cdc/SKILL.md +372 -0
- package/.cursor/skills/sf-quickstart/SKILL.md +170 -0
- package/.cursor/skills/sf-security/SKILL.md +330 -0
- package/.cursor/skills/sf-security-constraints/SKILL.md +125 -0
- package/.cursor/skills/sf-soql-constraints/SKILL.md +129 -0
- package/.cursor/skills/sf-soql-optimization/SKILL.md +353 -0
- package/.cursor/skills/sf-tdd-workflow/SKILL.md +332 -0
- package/.cursor/skills/sf-testing-constraints/SKILL.md +198 -0
- package/.cursor/skills/sf-trigger-constraints/SKILL.md +88 -0
- package/.cursor/skills/sf-trigger-frameworks/SKILL.md +343 -0
- package/.cursor/skills/sf-visualforce-development/SKILL.md +259 -0
- package/.cursor/skills/strategic-compact/SKILL.md +205 -0
- package/.cursor/skills/update-docs/SKILL.md +162 -0
- package/.cursor/skills/update-platform-docs/SKILL.md +86 -0
- package/.cursor-plugin/plugin.json +26 -0
- package/LICENSE +21 -0
- package/README.md +522 -0
- package/agents/deep-researcher.md +145 -0
- package/agents/doc-updater.md +222 -0
- package/agents/eval-runner.md +340 -0
- package/agents/learning-engine.md +211 -0
- package/agents/loop-operator.md +247 -0
- package/agents/refactor-cleaner.md +122 -0
- package/agents/sf-admin-agent.md +131 -0
- package/agents/sf-agentforce-agent.md +132 -0
- package/agents/sf-apex-agent.md +124 -0
- package/agents/sf-architect.md +435 -0
- package/agents/sf-aura-reviewer.md +372 -0
- package/agents/sf-bugfix-agent.md +105 -0
- package/agents/sf-flow-agent.md +159 -0
- package/agents/sf-integration-agent.md +146 -0
- package/agents/sf-lwc-agent.md +127 -0
- package/agents/sf-review-agent.md +366 -0
- package/agents/sf-visualforce-reviewer.md +468 -0
- package/assets/logo.svg +18 -0
- package/docs/ARCHITECTURE.md +133 -0
- package/docs/authoring-guide.md +373 -0
- package/docs/hook-development.md +578 -0
- package/docs/token-optimization.md +139 -0
- package/docs/workflow-examples.md +645 -0
- package/examples/agentforce-action/README.md +227 -0
- package/examples/apex-trigger-handler/README.md +114 -0
- package/examples/devops-pipeline/README.md +325 -0
- package/examples/flow-automation/README.md +188 -0
- package/examples/integration-pattern/README.md +416 -0
- package/examples/lwc-component/README.md +180 -0
- package/examples/platform-events/README.md +492 -0
- package/examples/scratch-org-setup/README.md +138 -0
- package/examples/security-audit/README.md +244 -0
- package/examples/visualforce-migration/README.md +314 -0
- package/hooks/hooks.json +338 -0
- package/hooks/memory-persistence/README.md +73 -0
- package/manifests/install-modules.json +217 -0
- package/manifests/install-profiles.json +17 -0
- package/mcp-configs/mcp-servers.json +19 -0
- package/package.json +89 -0
- package/schemas/hooks.schema.json +123 -0
- package/schemas/install-modules.schema.json +76 -0
- package/schemas/install-profiles.schema.json +28 -0
- package/schemas/install-state.schema.json +73 -0
- package/schemas/package-manager.schema.json +18 -0
- package/schemas/plugin.schema.json +112 -0
- package/schemas/scc-install-config.schema.json +29 -0
- package/schemas/state-store.schema.json +111 -0
- package/scripts/cli/install-apply.js +170 -0
- package/scripts/cli/uninstall.js +193 -0
- package/scripts/hooks/check-console-log.js +101 -0
- package/scripts/hooks/check-hook-enabled.js +17 -0
- package/scripts/hooks/check-platform-docs-age.js +48 -0
- package/scripts/hooks/cost-tracker.js +78 -0
- package/scripts/hooks/doc-file-warning.js +63 -0
- package/scripts/hooks/evaluate-session.js +98 -0
- package/scripts/hooks/governor-check.js +220 -0
- package/scripts/hooks/learning-observe.sh +206 -0
- package/scripts/hooks/mcp-health-check.js +588 -0
- package/scripts/hooks/post-bash-build-complete.js +34 -0
- package/scripts/hooks/post-bash-pr-created.js +43 -0
- package/scripts/hooks/post-edit-console-warn.js +61 -0
- package/scripts/hooks/post-edit-format.js +79 -0
- package/scripts/hooks/post-edit-typecheck.js +98 -0
- package/scripts/hooks/post-write.js +168 -0
- package/scripts/hooks/pre-bash-git-push-reminder.js +35 -0
- package/scripts/hooks/pre-bash-tmux-reminder.js +47 -0
- package/scripts/hooks/pre-compact.js +51 -0
- package/scripts/hooks/pre-tool-use.js +163 -0
- package/scripts/hooks/pre-write-doc-warn.js +9 -0
- package/scripts/hooks/quality-gate.js +251 -0
- package/scripts/hooks/run-with-flags-shell.sh +32 -0
- package/scripts/hooks/run-with-flags.js +135 -0
- package/scripts/hooks/session-end-marker.js +29 -0
- package/scripts/hooks/session-end.js +311 -0
- package/scripts/hooks/session-start.js +202 -0
- package/scripts/hooks/sfdx-scanner-check.js +142 -0
- package/scripts/hooks/sfdx-validate.js +119 -0
- package/scripts/hooks/stop-hook.js +170 -0
- package/scripts/hooks/suggest-compact.js +67 -0
- package/scripts/lib/agent-adapter.js +82 -0
- package/scripts/lib/apex-analysis.js +194 -0
- package/scripts/lib/hook-flags.js +74 -0
- package/scripts/lib/install-config.js +73 -0
- package/scripts/lib/install-executor.js +363 -0
- package/scripts/lib/install-state.js +121 -0
- package/scripts/lib/orchestration-session.js +299 -0
- package/scripts/lib/package-manager.js +124 -0
- package/scripts/lib/project-detect.js +228 -0
- package/scripts/lib/schema-validator.js +190 -0
- package/scripts/lib/skill-adapter.js +100 -0
- package/scripts/lib/state-store.js +376 -0
- package/scripts/lib/tmux-worktree-orchestrator.js +598 -0
- package/scripts/lib/utils.js +313 -0
- package/scripts/scc.js +164 -0
- package/skills/_reference/AGENTFORCE_PATTERNS.md +112 -0
- package/skills/_reference/APEX_CURSOR.md +159 -0
- package/skills/_reference/API_VERSIONS.md +78 -0
- package/skills/_reference/APPROVAL_PROCESSES.md +105 -0
- package/skills/_reference/ASYNC_PATTERNS.md +163 -0
- package/skills/_reference/AURA_COMPONENTS.md +146 -0
- package/skills/_reference/DATA_MIGRATION_PATTERNS.md +151 -0
- package/skills/_reference/DATA_MODELING.md +124 -0
- package/skills/_reference/DEBUGGING_TOOLS.md +140 -0
- package/skills/_reference/DEPLOYMENT_CHECKLIST.md +87 -0
- package/skills/_reference/DEPRECATIONS.md +79 -0
- package/skills/_reference/DOCKER_CI_PATTERNS.md +138 -0
- package/skills/_reference/ENTERPRISE_PATTERNS.md +122 -0
- package/skills/_reference/EXPERIENCE_CLOUD.md +143 -0
- package/skills/_reference/FLOW_PATTERNS.md +113 -0
- package/skills/_reference/GOVERNOR_LIMITS.md +77 -0
- package/skills/_reference/INTEGRATION_PATTERNS.md +105 -0
- package/skills/_reference/LWC_PATTERNS.md +79 -0
- package/skills/_reference/METADATA_TYPES.md +115 -0
- package/skills/_reference/NAMING_CONVENTIONS.md +84 -0
- package/skills/_reference/PACKAGE_DEVELOPMENT.md +150 -0
- package/skills/_reference/PLATFORM_EVENTS.md +121 -0
- package/skills/_reference/REPORTING_API.md +143 -0
- package/skills/_reference/SCRATCH_ORG_PATTERNS.md +126 -0
- package/skills/_reference/SECURITY_PATTERNS.md +127 -0
- package/skills/_reference/SHARING_MODEL.md +120 -0
- package/skills/_reference/SOQL_PATTERNS.md +119 -0
- package/skills/_reference/TESTING_STANDARDS.md +96 -0
- package/skills/_reference/TRIGGER_PATTERNS.md +114 -0
- package/skills/_reference/VISUALFORCE_PATTERNS.md +121 -0
- package/skills/aside/SKILL.md +118 -0
- package/skills/checkpoint/SKILL.md +53 -0
- package/skills/configure-scc/SKILL.md +163 -0
- package/skills/continuous-agent-loop/SKILL.md +264 -0
- package/skills/mcp-server-patterns/SKILL.md +146 -0
- package/skills/model-route/SKILL.md +84 -0
- package/skills/prompt-optimizer/SKILL.md +369 -0
- package/skills/refactor-clean/SKILL.md +136 -0
- package/skills/resume-session/SKILL.md +114 -0
- package/skills/save-session/SKILL.md +186 -0
- package/skills/search-first/SKILL.md +144 -0
- package/skills/security-scan/SKILL.md +146 -0
- package/skills/sessions/SKILL.md +127 -0
- package/skills/sf-agentforce-development/SKILL.md +450 -0
- package/skills/sf-apex-async-patterns/SKILL.md +326 -0
- package/skills/sf-apex-best-practices/SKILL.md +425 -0
- package/skills/sf-apex-constraints/SKILL.md +81 -0
- package/skills/sf-apex-cursor/SKILL.md +338 -0
- package/skills/sf-apex-enterprise-patterns/SKILL.md +348 -0
- package/skills/sf-apex-testing/SKILL.md +409 -0
- package/skills/sf-api-design/SKILL.md +238 -0
- package/skills/sf-approval-processes/SKILL.md +315 -0
- package/skills/sf-aura-development/SKILL.md +263 -0
- package/skills/sf-build-fix/SKILL.md +121 -0
- package/skills/sf-data-modeling/SKILL.md +278 -0
- package/skills/sf-debugging/SKILL.md +363 -0
- package/skills/sf-deployment/SKILL.md +295 -0
- package/skills/sf-deployment-constraints/SKILL.md +155 -0
- package/skills/sf-devops-ci-cd/SKILL.md +325 -0
- package/skills/sf-docs-lookup/SKILL.md +103 -0
- package/skills/sf-e2e-testing/SKILL.md +324 -0
- package/skills/sf-experience-cloud/SKILL.md +249 -0
- package/skills/sf-flow-development/SKILL.md +377 -0
- package/skills/sf-governor-limits/SKILL.md +323 -0
- package/skills/sf-harness-audit/SKILL.md +142 -0
- package/skills/sf-help/SKILL.md +159 -0
- package/skills/sf-integration/SKILL.md +483 -0
- package/skills/sf-lwc-constraints/SKILL.md +130 -0
- package/skills/sf-lwc-development/SKILL.md +303 -0
- package/skills/sf-lwc-testing/SKILL.md +388 -0
- package/skills/sf-metadata-management/SKILL.md +288 -0
- package/skills/sf-platform-events-cdc/SKILL.md +375 -0
- package/skills/sf-quickstart/SKILL.md +173 -0
- package/skills/sf-security/SKILL.md +334 -0
- package/skills/sf-security-constraints/SKILL.md +127 -0
- package/skills/sf-soql-constraints/SKILL.md +131 -0
- package/skills/sf-soql-optimization/SKILL.md +354 -0
- package/skills/sf-tdd-workflow/SKILL.md +336 -0
- package/skills/sf-testing-constraints/SKILL.md +200 -0
- package/skills/sf-trigger-constraints/SKILL.md +90 -0
- package/skills/sf-trigger-frameworks/SKILL.md +347 -0
- package/skills/sf-visualforce-development/SKILL.md +260 -0
- package/skills/strategic-compact/SKILL.md +208 -0
- package/skills/update-docs/SKILL.md +165 -0
- package/skills/update-platform-docs/SKILL.md +90 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SFDX Validate Hook
|
|
4
|
+
*
|
|
5
|
+
* PreToolUse hook that validates Salesforce CLI commands before execution.
|
|
6
|
+
* Checks for:
|
|
7
|
+
* - Commands that should use --dry-run first
|
|
8
|
+
* - Destructive operations that need confirmation
|
|
9
|
+
* - Common parameter mistakes
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const readline = require('readline');
|
|
15
|
+
|
|
16
|
+
const MAX_STDIN = 1024 * 1024;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validation rules for SF CLI commands.
|
|
20
|
+
*/
|
|
21
|
+
const VALIDATION_RULES = [
|
|
22
|
+
{
|
|
23
|
+
pattern: /sf project deploy start(?!.*--dry-run)(?!.*--validate)/,
|
|
24
|
+
check: (cmd) => !cmd.includes('--test-level') && !cmd.includes('--dry-run'),
|
|
25
|
+
message: 'Deployment without --test-level specified. Consider adding --test-level RunLocalTests',
|
|
26
|
+
severity: 'warning',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /sf data delete bulk/,
|
|
30
|
+
message: 'Bulk delete operation — this permanently removes records. Use --dry-run first.',
|
|
31
|
+
severity: 'warning',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
pattern: /sf org delete scratch/,
|
|
35
|
+
check: (cmd) => !cmd.includes('--no-prompt'),
|
|
36
|
+
message: 'Scratch org deletion — ensure you have pushed all source changes before deleting.',
|
|
37
|
+
severity: 'info',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
pattern: /sf project deploy start.*--ignore-conflicts/,
|
|
41
|
+
message: 'Deploying with --ignore-conflicts may overwrite changes made directly in the org.',
|
|
42
|
+
severity: 'warning',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
pattern: /sf data import tree/,
|
|
46
|
+
check: (cmd) => !cmd.includes('--plan'),
|
|
47
|
+
message: 'Data import without --plan. Consider using a plan file for repeatable imports.',
|
|
48
|
+
severity: 'info',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
pattern: /sf package version create(?!.*--skip-validation)/,
|
|
52
|
+
check: (cmd) => !cmd.includes('--code-coverage'),
|
|
53
|
+
message: 'Package version creation — remember that managed packages require 75% code coverage.',
|
|
54
|
+
severity: 'info',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
pattern: /sf project deploy start.*--source-dir.*destructiveChanges/i,
|
|
58
|
+
message: 'Destructive changes deployment detected. Ensure you have verified the manifest.',
|
|
59
|
+
severity: 'warning',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function validateCommand(command) {
|
|
64
|
+
if (!command) return [];
|
|
65
|
+
|
|
66
|
+
// Only check SF CLI commands
|
|
67
|
+
if (!command.includes('sf ') && !command.includes('sfdx ')) return [];
|
|
68
|
+
|
|
69
|
+
const warnings = [];
|
|
70
|
+
for (const rule of VALIDATION_RULES) {
|
|
71
|
+
if (rule.pattern.test(command)) {
|
|
72
|
+
if (rule.check && !rule.check(command)) continue;
|
|
73
|
+
warnings.push({
|
|
74
|
+
severity: rule.severity,
|
|
75
|
+
message: rule.message,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return warnings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Read JSON from stdin
|
|
83
|
+
let rawInput = '';
|
|
84
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
85
|
+
rl.on('line', line => {
|
|
86
|
+
if (rawInput.length < MAX_STDIN) {
|
|
87
|
+
rawInput += line + '\n';
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
rl.on('close', () => {
|
|
92
|
+
let input = {};
|
|
93
|
+
try {
|
|
94
|
+
input = JSON.parse(rawInput.trim() || '{}');
|
|
95
|
+
} catch {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (input.tool_name !== 'Bash' && input.tool_name !== 'bash') {
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const command = (input.tool_input && input.tool_input.command) || '';
|
|
104
|
+
const warnings = validateCommand(command);
|
|
105
|
+
|
|
106
|
+
if (warnings.length === 0) {
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
console.error('\n[SCC Validate] SF CLI Command Check:');
|
|
111
|
+
for (const w of warnings) {
|
|
112
|
+
const prefix = w.severity === 'warning' ? 'WARNING' : 'INFO';
|
|
113
|
+
console.error(` ${prefix}: ${w.message}`);
|
|
114
|
+
}
|
|
115
|
+
console.error();
|
|
116
|
+
|
|
117
|
+
// Exit 0 — we warn but don't block
|
|
118
|
+
process.exit(0);
|
|
119
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* stop-hook.js — Stop hook for SCC.
|
|
6
|
+
*
|
|
7
|
+
* Runs when Claude Code is about to stop/complete a session.
|
|
8
|
+
* Checks for uncommitted Apex/LWC changes and reminds about:
|
|
9
|
+
* - Running tests before committing
|
|
10
|
+
* - Deploying to a scratch org to verify
|
|
11
|
+
* - Checking test coverage
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const CWD = process.cwd();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run a command and return { status, stdout, stderr }.
|
|
22
|
+
*/
|
|
23
|
+
function run(cmd, args, cwd) {
|
|
24
|
+
const result = spawnSync(cmd, args, {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
timeout: 10000,
|
|
27
|
+
cwd: cwd || CWD,
|
|
28
|
+
env: process.env,
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
status: result.status,
|
|
32
|
+
stdout: result.stdout || '',
|
|
33
|
+
stderr: result.stderr || '',
|
|
34
|
+
error: result.error,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get uncommitted files via git status --porcelain.
|
|
40
|
+
*/
|
|
41
|
+
function getUncommittedFiles() {
|
|
42
|
+
const result = run('git', ['status', '--porcelain']);
|
|
43
|
+
if (result.status !== 0 || result.error) return null;
|
|
44
|
+
|
|
45
|
+
return result.stdout
|
|
46
|
+
.split('\n')
|
|
47
|
+
.filter(line => line.trim().length > 0)
|
|
48
|
+
.map(line => {
|
|
49
|
+
const statusCode = line.slice(0, 2).trim();
|
|
50
|
+
const filePath = line.slice(3).trim();
|
|
51
|
+
return { statusCode, filePath };
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if this is a Salesforce project.
|
|
57
|
+
*/
|
|
58
|
+
function isSalesforceProject(dir) {
|
|
59
|
+
let d = dir;
|
|
60
|
+
for (let i = 0; i < 5; i++) {
|
|
61
|
+
if (fs.existsSync(path.join(d, 'sfdx-project.json'))) return true;
|
|
62
|
+
const parent = path.dirname(d);
|
|
63
|
+
if (parent === d) break;
|
|
64
|
+
d = parent;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Classify Salesforce file types.
|
|
71
|
+
*/
|
|
72
|
+
function classifyFiles(files) {
|
|
73
|
+
const classified = { apex: [], lwc: [], aura: [], other: [] };
|
|
74
|
+
for (const f of files) {
|
|
75
|
+
const fp = f.filePath;
|
|
76
|
+
const ext = path.extname(fp).toLowerCase();
|
|
77
|
+
const parts = fp.split('/');
|
|
78
|
+
|
|
79
|
+
if (ext === '.cls' || ext === '.trigger') {
|
|
80
|
+
classified.apex.push(f);
|
|
81
|
+
} else if (parts.includes('lwc')) {
|
|
82
|
+
classified.lwc.push(f);
|
|
83
|
+
} else if (parts.includes('aura')) {
|
|
84
|
+
classified.aura.push(f);
|
|
85
|
+
} else {
|
|
86
|
+
classified.other.push(f);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return classified;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
// Only run in Salesforce projects
|
|
95
|
+
if (!isSalesforceProject(CWD)) {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const uncommittedFiles = getUncommittedFiles();
|
|
100
|
+
|
|
101
|
+
// Can't check git or no changes
|
|
102
|
+
if (uncommittedFiles === null || uncommittedFiles.length === 0) {
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const classified = classifyFiles(uncommittedFiles);
|
|
107
|
+
const hasSfChanges = classified.apex.length > 0 || classified.lwc.length > 0 || classified.aura.length > 0;
|
|
108
|
+
|
|
109
|
+
if (!hasSfChanges) {
|
|
110
|
+
process.exit(0);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Print a summary reminder
|
|
114
|
+
console.log('\n── SCC: Uncommitted Salesforce Changes ────────────────');
|
|
115
|
+
|
|
116
|
+
if (classified.apex.length > 0) {
|
|
117
|
+
console.log(`\nApex changes (${classified.apex.length} file(s)):`);
|
|
118
|
+
for (const f of classified.apex.slice(0, 5)) {
|
|
119
|
+
console.log(` ${f.statusCode} ${f.filePath}`);
|
|
120
|
+
}
|
|
121
|
+
if (classified.apex.length > 5) console.log(` ... and ${classified.apex.length - 5} more`);
|
|
122
|
+
|
|
123
|
+
console.log('\nApex reminders:');
|
|
124
|
+
console.log(' • Run Apex tests before committing:');
|
|
125
|
+
console.log(' sf apex run test --result-format human --code-coverage');
|
|
126
|
+
console.log(' • Ensure 75% code coverage across your org');
|
|
127
|
+
console.log(' • Deploy to scratch org to verify: sf project deploy start');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (classified.lwc.length > 0) {
|
|
131
|
+
console.log(`\nLWC changes (${classified.lwc.length} file(s)):`);
|
|
132
|
+
for (const f of classified.lwc.slice(0, 5)) {
|
|
133
|
+
console.log(` ${f.statusCode} ${f.filePath}`);
|
|
134
|
+
}
|
|
135
|
+
if (classified.lwc.length > 5) console.log(` ... and ${classified.lwc.length - 5} more`);
|
|
136
|
+
|
|
137
|
+
console.log('\nLWC reminders:');
|
|
138
|
+
console.log(' • Run Jest unit tests: npm run test:unit');
|
|
139
|
+
console.log(' • Validate component: sf project deploy start --dry-run');
|
|
140
|
+
console.log(' • Test in browser: sf org open');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (classified.aura.length > 0) {
|
|
144
|
+
console.log(`\nAura changes (${classified.aura.length} file(s)):`);
|
|
145
|
+
for (const f of classified.aura.slice(0, 3)) {
|
|
146
|
+
console.log(` ${f.statusCode} ${f.filePath}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check if there's a package.json with test scripts (for LWC Jest)
|
|
151
|
+
const packageJsonPath = path.join(CWD, 'package.json');
|
|
152
|
+
let hasJestScript = false;
|
|
153
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
154
|
+
try {
|
|
155
|
+
const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
156
|
+
hasJestScript = !!(pkg.scripts && (pkg.scripts['test:unit'] || pkg.scripts['jest'] || pkg.scripts['test']));
|
|
157
|
+
} catch { /* ignore */ }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log('\nNext steps:');
|
|
161
|
+
if (classified.apex.length > 0) {
|
|
162
|
+
console.log(' 1. sf apex run test --result-format human --code-coverage');
|
|
163
|
+
}
|
|
164
|
+
if (classified.lwc.length > 0 && hasJestScript) {
|
|
165
|
+
console.log(' 2. npm run test:unit');
|
|
166
|
+
}
|
|
167
|
+
console.log(` ${classified.apex.length > 0 ? '3' : '1'}. git add . && git commit -m "feat: <your message>"`);
|
|
168
|
+
console.log('────────────────────────────────────────────────────────\n');
|
|
169
|
+
|
|
170
|
+
process.exit(0);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Strategic Compact Suggester
|
|
4
|
+
*
|
|
5
|
+
* Tracks tool call count and suggests manual /compact at logical intervals.
|
|
6
|
+
* Runs on PreToolUse to count invocations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
function getTempDir() {
|
|
16
|
+
return os.tmpdir();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function log(msg) {
|
|
20
|
+
process.stderr.write(`${msg}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function main() {
|
|
24
|
+
const sessionId = (process.env.CLAUDE_SESSION_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '') || 'default';
|
|
25
|
+
const counterFile = path.join(getTempDir(), `scc-tool-count-${sessionId}`);
|
|
26
|
+
const rawThreshold = parseInt(process.env.COMPACT_THRESHOLD || '50', 10);
|
|
27
|
+
const threshold = Number.isFinite(rawThreshold) && rawThreshold > 0 && rawThreshold <= 10000
|
|
28
|
+
? rawThreshold
|
|
29
|
+
: 50;
|
|
30
|
+
|
|
31
|
+
let count = 1;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const fd = fs.openSync(counterFile, 'a+');
|
|
35
|
+
try {
|
|
36
|
+
const buf = Buffer.alloc(64);
|
|
37
|
+
const bytesRead = fs.readSync(fd, buf, 0, 64, 0);
|
|
38
|
+
if (bytesRead > 0) {
|
|
39
|
+
const parsed = parseInt(buf.toString('utf8', 0, bytesRead).trim(), 10);
|
|
40
|
+
count = (Number.isFinite(parsed) && parsed > 0 && parsed <= 1000000)
|
|
41
|
+
? parsed + 1
|
|
42
|
+
: 1;
|
|
43
|
+
}
|
|
44
|
+
fs.ftruncateSync(fd, 0);
|
|
45
|
+
fs.writeSync(fd, String(count), 0);
|
|
46
|
+
} finally {
|
|
47
|
+
fs.closeSync(fd);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
try { fs.writeFileSync(counterFile, String(count)); } catch { /* ignore */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (count === threshold) {
|
|
54
|
+
log(`[SCC Compact] ${threshold} tool calls reached — consider /compact if transitioning phases`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (count > threshold && (count - threshold) % 25 === 0) {
|
|
58
|
+
log(`[SCC Compact] ${count} tool calls — good checkpoint for /compact if context is stale`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
main().catch(err => {
|
|
65
|
+
console.error('[SCC Compact] Error:', err.message);
|
|
66
|
+
process.exit(0);
|
|
67
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agent-adapter.js — Transforms SCC agents from Claude Code format to Cursor format.
|
|
5
|
+
*
|
|
6
|
+
* Claude Code agents use: name, description, tools, model (sonnet/opus/haiku), origin,
|
|
7
|
+
* disallowedTools, permissionMode, maxTurns, skills, mcpServers, hooks, memory, etc.
|
|
8
|
+
* Cursor agents use: name, description, model (fast/inherit/specific ID), readonly, is_background.
|
|
9
|
+
*
|
|
10
|
+
* This adapter strips Claude-only fields, maps model aliases, and outputs
|
|
11
|
+
* clean Cursor-compatible agent .md files.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { parseFrontmatter, serializeFrontmatter, ensureDir } = require('./utils');
|
|
17
|
+
|
|
18
|
+
// Fields that Cursor recognizes in agent frontmatter
|
|
19
|
+
const CURSOR_ALLOWED_FIELDS = new Set([
|
|
20
|
+
'name',
|
|
21
|
+
'description',
|
|
22
|
+
'model',
|
|
23
|
+
'readonly',
|
|
24
|
+
'is_background',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// Map Claude Code model aliases to Cursor equivalents
|
|
28
|
+
const MODEL_MAP = {
|
|
29
|
+
sonnet: 'inherit',
|
|
30
|
+
opus: 'inherit',
|
|
31
|
+
haiku: 'fast',
|
|
32
|
+
'claude-sonnet': 'inherit',
|
|
33
|
+
'claude-opus': 'inherit',
|
|
34
|
+
'claude-haiku': 'fast',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Transform a single agent .md content string from Claude Code to Cursor format.
|
|
39
|
+
* @param {string} content - raw agent .md content
|
|
40
|
+
* @returns {string} - transformed agent .md content for Cursor
|
|
41
|
+
*/
|
|
42
|
+
function transformAgent(content) {
|
|
43
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
44
|
+
|
|
45
|
+
const cursorFm = {};
|
|
46
|
+
|
|
47
|
+
// Copy allowed fields
|
|
48
|
+
for (const key of Object.keys(frontmatter)) {
|
|
49
|
+
if (CURSOR_ALLOWED_FIELDS.has(key)) {
|
|
50
|
+
cursorFm[key] = frontmatter[key];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Map model aliases to Cursor equivalents
|
|
55
|
+
if (cursorFm.model) {
|
|
56
|
+
const mapped = MODEL_MAP[String(cursorFm.model).toLowerCase()];
|
|
57
|
+
if (mapped) {
|
|
58
|
+
cursorFm.model = mapped;
|
|
59
|
+
}
|
|
60
|
+
// Full model IDs (e.g. claude-sonnet-4-6) pass through unchanged
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return serializeFrontmatter(cursorFm, body);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Transform and write a single agent file.
|
|
68
|
+
* @param {string} srcPath - source agent .md file
|
|
69
|
+
* @param {string} destPath - destination file path
|
|
70
|
+
*/
|
|
71
|
+
function transformAgentFile(srcPath, destPath) {
|
|
72
|
+
if (!fs.existsSync(srcPath)) {
|
|
73
|
+
throw new Error(`Source agent file not found: ${srcPath}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const content = fs.readFileSync(srcPath, 'utf8');
|
|
77
|
+
const transformed = transformAgent(content);
|
|
78
|
+
ensureDir(path.dirname(destPath));
|
|
79
|
+
fs.writeFileSync(destPath, transformed, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { transformAgent, transformAgentFile, CURSOR_ALLOWED_FIELDS, MODEL_MAP };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* apex-analysis.js — Shared Apex static analysis utilities
|
|
6
|
+
*
|
|
7
|
+
* Provides preprocessing (comment/string stripping), loop depth tracking,
|
|
8
|
+
* and test class detection for use by governor-check.js and quality-gate.js.
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
* - trackLoopDepth uses an activeLoopDepths stack + globalBraceDepth counter
|
|
12
|
+
* to correctly handle if/try/catch blocks inside loops (their closing braces
|
|
13
|
+
* don't prematurely end the enclosing loop).
|
|
14
|
+
* - Linear 6-step pipeline per line — no continue statements that could skip
|
|
15
|
+
* brace tracking and desync globalBraceDepth.
|
|
16
|
+
*
|
|
17
|
+
* Known limitations:
|
|
18
|
+
* - do { } while(cond) — the 'do' keyword isn't followed by '(', so the loop
|
|
19
|
+
* regex doesn't match it. Brace tracking still manages depth correctly, but
|
|
20
|
+
* 'do' itself isn't recognized as a loop start.
|
|
21
|
+
* - Class-level static initializer blocks may affect globalBraceDepth.
|
|
22
|
+
* - Anonymous inner classes with { } may affect brace counting.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Strip comments and string literals from Apex code.
|
|
27
|
+
* Replaces removed content with whitespace to preserve line alignment.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} code - Raw Apex source code
|
|
30
|
+
* @returns {string} Code with comments and strings replaced by spaces
|
|
31
|
+
*/
|
|
32
|
+
function preprocessApex(code) {
|
|
33
|
+
let result = code;
|
|
34
|
+
// Remove block comments (preserve newlines for line alignment)
|
|
35
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, m => m.replace(/[^\n]/g, ' '));
|
|
36
|
+
// Remove single-line comments
|
|
37
|
+
result = result.replace(/\/\/.*$/gm, m => ' '.repeat(m.length));
|
|
38
|
+
// Remove string literals (handle escaped quotes)
|
|
39
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, m => ' '.repeat(m.length));
|
|
40
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, m => ' '.repeat(m.length));
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Detect whether an Apex source file is a test class.
|
|
46
|
+
* Test classes don't run in production, so governor limit scanning
|
|
47
|
+
* on them creates noise.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} code - Raw or preprocessed Apex source code
|
|
50
|
+
* @returns {boolean} True if the class has @IsTest annotation at class level
|
|
51
|
+
*/
|
|
52
|
+
function isTestClass(code) {
|
|
53
|
+
// Match @IsTest before the class keyword (class-level annotation)
|
|
54
|
+
const beforeFirstBrace = code.split('{')[0] || '';
|
|
55
|
+
return /@[Ii]s[Tt]est\b/.test(beforeFirstBrace);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Track loop depth for each line of preprocessed Apex code.
|
|
60
|
+
*
|
|
61
|
+
* Returns an array of integers where depths[i] is the loop nesting depth
|
|
62
|
+
* at line i. depths[i] > 0 means line i is inside a loop.
|
|
63
|
+
*
|
|
64
|
+
* Uses an activeLoopDepths stack bound to globalBraceDepth to correctly
|
|
65
|
+
* handle if/try/catch blocks inside loops. Uses unbracedStack counter
|
|
66
|
+
* for nested unbraced loops. Uses waitingForBody state for Allman brace style.
|
|
67
|
+
*
|
|
68
|
+
* CRITICAL: Linear 6-step pipeline — every non-empty line flows through all
|
|
69
|
+
* steps. No continue statements that could skip brace tracking.
|
|
70
|
+
*
|
|
71
|
+
* @param {string[]} processedLines - Lines from preprocessApex(code).split('\n')
|
|
72
|
+
* @returns {number[]} Array of loop depth per line
|
|
73
|
+
*/
|
|
74
|
+
function trackLoopDepth(processedLines) {
|
|
75
|
+
const depths = [];
|
|
76
|
+
let globalBraceDepth = 0; // total brace depth in file
|
|
77
|
+
const activeLoopDepths = []; // stack: globalBraceDepth where each braced loop started
|
|
78
|
+
let unbracedStack = 0; // counter for nested unbraced loops
|
|
79
|
+
let parenDepth = 0; // for multi-line for/while declarations
|
|
80
|
+
let pendingLoop = false; // SCANNING_DECLARATION state
|
|
81
|
+
let waitingForBody = false; // WAITING_FOR_BODY state
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < processedLines.length; i++) {
|
|
84
|
+
const line = processedLines[i];
|
|
85
|
+
const trimmed = line.trim();
|
|
86
|
+
let singleLineLoopBonus = 0;
|
|
87
|
+
let skipLoopDetection = false;
|
|
88
|
+
|
|
89
|
+
// Empty lines: no braces, safe to skip
|
|
90
|
+
if (!trimmed) {
|
|
91
|
+
depths.push(activeLoopDepths.length + unbracedStack);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ═══════════ STEP 1: Resolve pending multi-line declarations ═══════════
|
|
96
|
+
if (pendingLoop) {
|
|
97
|
+
let closingIdx = -1;
|
|
98
|
+
for (let j = 0; j < trimmed.length; j++) {
|
|
99
|
+
if (trimmed[j] === '(') parenDepth++;
|
|
100
|
+
if (trimmed[j] === ')') {
|
|
101
|
+
parenDepth--;
|
|
102
|
+
if (parenDepth === 0) { closingIdx = j; break; }
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (closingIdx === -1) {
|
|
106
|
+
// Still reading declaration — inside parens, no block braces matter
|
|
107
|
+
depths.push(activeLoopDepths.length + unbracedStack);
|
|
108
|
+
continue; // safe: inside parens
|
|
109
|
+
}
|
|
110
|
+
pendingLoop = false;
|
|
111
|
+
const afterClose = trimmed.substring(closingIdx + 1).trim();
|
|
112
|
+
if (afterClose.startsWith('{') || afterClose.includes('{')) {
|
|
113
|
+
activeLoopDepths.push(globalBraceDepth);
|
|
114
|
+
} else if (/\S/.test(afterClose) && /;\s*$/.test(afterClose)) {
|
|
115
|
+
singleLineLoopBonus = 1;
|
|
116
|
+
} else if (/\S/.test(afterClose)) {
|
|
117
|
+
unbracedStack++;
|
|
118
|
+
} else {
|
|
119
|
+
waitingForBody = true;
|
|
120
|
+
}
|
|
121
|
+
skipLoopDetection = true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ═══════════ STEP 2: Resolve WAITING_FOR_BODY ═══════════
|
|
125
|
+
if (waitingForBody) {
|
|
126
|
+
waitingForBody = false;
|
|
127
|
+
if (trimmed.startsWith('{')) {
|
|
128
|
+
// Allman braced loop
|
|
129
|
+
activeLoopDepths.push(globalBraceDepth);
|
|
130
|
+
} else {
|
|
131
|
+
// Unbraced loop — this line IS the body (or start of it)
|
|
132
|
+
unbracedStack++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ═══════════ STEP 3: Detect new loop starts ═══════════
|
|
137
|
+
const loopMatch = !skipLoopDetection && trimmed.match(/\b(for|while|do)\s*\(/);
|
|
138
|
+
if (loopMatch) {
|
|
139
|
+
// Token-aware paren scan from match point (NOT lastIndexOf)
|
|
140
|
+
let localParenDepth = 0;
|
|
141
|
+
let closingIdx = -1;
|
|
142
|
+
for (let j = loopMatch.index; j < trimmed.length; j++) {
|
|
143
|
+
if (trimmed[j] === '(') localParenDepth++;
|
|
144
|
+
if (trimmed[j] === ')') {
|
|
145
|
+
localParenDepth--;
|
|
146
|
+
if (localParenDepth === 0) { closingIdx = j; break; }
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (closingIdx === -1) {
|
|
151
|
+
// Multi-line declaration — parens still open
|
|
152
|
+
parenDepth = localParenDepth;
|
|
153
|
+
pendingLoop = true;
|
|
154
|
+
} else {
|
|
155
|
+
// CRITICAL: use closingIdx from paren scan, NOT lastIndexOf(')')
|
|
156
|
+
const afterClose = trimmed.substring(closingIdx + 1).trim();
|
|
157
|
+
if (afterClose.startsWith('{') || afterClose.includes('{')) {
|
|
158
|
+
activeLoopDepths.push(globalBraceDepth);
|
|
159
|
+
} else if (/\S/.test(afterClose) && /;\s*$/.test(afterClose)) {
|
|
160
|
+
singleLineLoopBonus = 1;
|
|
161
|
+
} else if (/\S/.test(afterClose)) {
|
|
162
|
+
unbracedStack++;
|
|
163
|
+
} else {
|
|
164
|
+
waitingForBody = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ═══════════ STEP 4: Record current line depth ═══════════
|
|
170
|
+
// Single-line loop bonus adds +1 for the current line only
|
|
171
|
+
depths.push(activeLoopDepths.length + unbracedStack + singleLineLoopBonus);
|
|
172
|
+
|
|
173
|
+
// ═══════════ STEP 5: Update globalBraceDepth + reconcile ═══════════
|
|
174
|
+
const opens = (trimmed.match(/\{/g) || []).length;
|
|
175
|
+
const closes = (trimmed.match(/\}/g) || []).length;
|
|
176
|
+
globalBraceDepth += (opens - closes);
|
|
177
|
+
|
|
178
|
+
// If brace depth dropped to/below where a loop started, that loop closed
|
|
179
|
+
while (
|
|
180
|
+
activeLoopDepths.length > 0 &&
|
|
181
|
+
globalBraceDepth <= activeLoopDepths[activeLoopDepths.length - 1]
|
|
182
|
+
) {
|
|
183
|
+
activeLoopDepths.pop();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ═══════════ STEP 6: Drain unbraced stack (LAST — after braces counted) ═══════════
|
|
187
|
+
if (unbracedStack > 0 && /;\s*$/.test(trimmed) && singleLineLoopBonus === 0) {
|
|
188
|
+
unbracedStack = 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return depths;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = { preprocessApex, isTestClass, trackLoopDepth };
|