d365fo-mcp 1.0.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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/bridge/bridgeAdapter.d.ts +319 -0
- package/dist/bridge/bridgeAdapter.d.ts.map +1 -0
- package/dist/bridge/bridgeAdapter.js +2003 -0
- package/dist/bridge/bridgeAdapter.js.map +1 -0
- package/dist/bridge/bridgeClient.d.ts +255 -0
- package/dist/bridge/bridgeClient.d.ts.map +1 -0
- package/dist/bridge/bridgeClient.js +741 -0
- package/dist/bridge/bridgeClient.js.map +1 -0
- package/dist/bridge/bridgeTypes.d.ts +561 -0
- package/dist/bridge/bridgeTypes.d.ts.map +1 -0
- package/dist/bridge/bridgeTypes.js +6 -0
- package/dist/bridge/bridgeTypes.js.map +1 -0
- package/dist/bridge/debouncedRefresh.d.ts +16 -0
- package/dist/bridge/debouncedRefresh.d.ts.map +1 -0
- package/dist/bridge/debouncedRefresh.js +77 -0
- package/dist/bridge/debouncedRefresh.js.map +1 -0
- package/dist/bridge/index.d.ts +21 -0
- package/dist/bridge/index.d.ts.map +1 -0
- package/dist/bridge/index.js +20 -0
- package/dist/bridge/index.js.map +1 -0
- package/dist/cli/commands/config.d.ts +8 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +77 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +177 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/indexCmd.d.ts +8 -0
- package/dist/cli/commands/indexCmd.d.ts.map +1 -0
- package/dist/cli/commands/indexCmd.js +78 -0
- package/dist/cli/commands/indexCmd.js.map +1 -0
- package/dist/cli/commands/instance.d.ts +4 -0
- package/dist/cli/commands/instance.d.ts.map +1 -0
- package/dist/cli/commands/instance.js +183 -0
- package/dist/cli/commands/instance.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +4 -0
- package/dist/cli/commands/setup.d.ts.map +1 -0
- package/dist/cli/commands/setup.js +246 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/start.d.ts +2 -0
- package/dist/cli/commands/start.d.ts.map +1 -0
- package/dist/cli/commands/start.js +49 -0
- package/dist/cli/commands/start.js.map +1 -0
- package/dist/cli/commands/update.d.ts +4 -0
- package/dist/cli/commands/update.d.ts.map +1 -0
- package/dist/cli/commands/update.js +55 -0
- package/dist/cli/commands/update.js.map +1 -0
- package/dist/cli/context.d.ts +26 -0
- package/dist/cli/context.d.ts.map +1 -0
- package/dist/cli/context.js +38 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/envFile.d.ts +23 -0
- package/dist/cli/envFile.d.ts.map +1 -0
- package/dist/cli/envFile.js +79 -0
- package/dist/cli/envFile.js.map +1 -0
- package/dist/cli/exec.d.ts +13 -0
- package/dist/cli/exec.d.ts.map +1 -0
- package/dist/cli/exec.js +47 -0
- package/dist/cli/exec.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +122 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/instances.d.ts +19 -0
- package/dist/cli/instances.d.ts.map +1 -0
- package/dist/cli/instances.js +60 -0
- package/dist/cli/instances.js.map +1 -0
- package/dist/cli/mcpJson.d.ts +15 -0
- package/dist/cli/mcpJson.d.ts.map +1 -0
- package/dist/cli/mcpJson.js +44 -0
- package/dist/cli/mcpJson.js.map +1 -0
- package/dist/cli/settingsPrompt.d.ts +20 -0
- package/dist/cli/settingsPrompt.d.ts.map +1 -0
- package/dist/cli/settingsPrompt.js +150 -0
- package/dist/cli/settingsPrompt.js.map +1 -0
- package/dist/cli/settingsStore.d.ts +47 -0
- package/dist/cli/settingsStore.d.ts.map +1 -0
- package/dist/cli/settingsStore.js +144 -0
- package/dist/cli/settingsStore.js.map +1 -0
- package/dist/cli/target.d.ts +23 -0
- package/dist/cli/target.d.ts.map +1 -0
- package/dist/cli/target.js +62 -0
- package/dist/cli/target.js.map +1 -0
- package/dist/cli/ui.d.ts +26 -0
- package/dist/cli/ui.d.ts.map +1 -0
- package/dist/cli/ui.js +48 -0
- package/dist/cli/ui.js.map +1 -0
- package/dist/cli/xppConfig.d.ts +35 -0
- package/dist/cli/xppConfig.d.ts.map +1 -0
- package/dist/cli/xppConfig.js +97 -0
- package/dist/cli/xppConfig.js.map +1 -0
- package/dist/config/configFile.d.ts +50 -0
- package/dist/config/configFile.d.ts.map +1 -0
- package/dist/config/configFile.js +152 -0
- package/dist/config/configFile.js.map +1 -0
- package/dist/config/settings.d.ts +58 -0
- package/dist/config/settings.d.ts.map +1 -0
- package/dist/config/settings.js +641 -0
- package/dist/config/settings.js.map +1 -0
- package/dist/database/download.d.ts +27 -0
- package/dist/database/download.d.ts.map +1 -0
- package/dist/database/download.js +241 -0
- package/dist/database/download.js.map +1 -0
- package/dist/eval/audit/knowledgeAudit.d.ts +66 -0
- package/dist/eval/audit/knowledgeAudit.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeAudit.js +117 -0
- package/dist/eval/audit/knowledgeAudit.js.map +1 -0
- package/dist/eval/audit/knowledgeAuditCli.d.ts +17 -0
- package/dist/eval/audit/knowledgeAuditCli.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeAuditCli.js +83 -0
- package/dist/eval/audit/knowledgeAuditCli.js.map +1 -0
- package/dist/eval/audit/knowledgeRefs.d.ts +37 -0
- package/dist/eval/audit/knowledgeRefs.d.ts.map +1 -0
- package/dist/eval/audit/knowledgeRefs.js +150 -0
- package/dist/eval/audit/knowledgeRefs.js.map +1 -0
- package/dist/eval/audit/symbolLookup.d.ts +23 -0
- package/dist/eval/audit/symbolLookup.d.ts.map +1 -0
- package/dist/eval/audit/symbolLookup.js +80 -0
- package/dist/eval/audit/symbolLookup.js.map +1 -0
- package/dist/eval/coverage/coverage.d.ts +65 -0
- package/dist/eval/coverage/coverage.d.ts.map +1 -0
- package/dist/eval/coverage/coverage.js +136 -0
- package/dist/eval/coverage/coverage.js.map +1 -0
- package/dist/eval/coverage/coverageCli.d.ts +13 -0
- package/dist/eval/coverage/coverageCli.d.ts.map +1 -0
- package/dist/eval/coverage/coverageCli.js +106 -0
- package/dist/eval/coverage/coverageCli.js.map +1 -0
- package/dist/eval/coverage/sources.d.ts +17 -0
- package/dist/eval/coverage/sources.d.ts.map +1 -0
- package/dist/eval/coverage/sources.js +40 -0
- package/dist/eval/coverage/sources.js.map +1 -0
- package/dist/eval/coverage/taxonomy.d.ts +59 -0
- package/dist/eval/coverage/taxonomy.d.ts.map +1 -0
- package/dist/eval/coverage/taxonomy.js +400 -0
- package/dist/eval/coverage/taxonomy.js.map +1 -0
- package/dist/eval/improver/caseMining.d.ts +55 -0
- package/dist/eval/improver/caseMining.d.ts.map +1 -0
- package/dist/eval/improver/caseMining.js +64 -0
- package/dist/eval/improver/caseMining.js.map +1 -0
- package/dist/eval/improver/caseMiningCli.d.ts +10 -0
- package/dist/eval/improver/caseMiningCli.d.ts.map +1 -0
- package/dist/eval/improver/caseMiningCli.js +50 -0
- package/dist/eval/improver/caseMiningCli.js.map +1 -0
- package/dist/eval/improver/cli.d.ts +11 -0
- package/dist/eval/improver/cli.d.ts.map +1 -0
- package/dist/eval/improver/cli.js +73 -0
- package/dist/eval/improver/cli.js.map +1 -0
- package/dist/eval/improver/cluster.d.ts +47 -0
- package/dist/eval/improver/cluster.d.ts.map +1 -0
- package/dist/eval/improver/cluster.js +0 -0
- package/dist/eval/improver/cluster.js.map +1 -0
- package/dist/eval/improver/corpusIO.d.ts +26 -0
- package/dist/eval/improver/corpusIO.d.ts.map +1 -0
- package/dist/eval/improver/corpusIO.js +50 -0
- package/dist/eval/improver/corpusIO.js.map +1 -0
- package/dist/eval/improver/fixBrief.d.ts +34 -0
- package/dist/eval/improver/fixBrief.d.ts.map +1 -0
- package/dist/eval/improver/fixBrief.js +69 -0
- package/dist/eval/improver/fixBrief.js.map +1 -0
- package/dist/eval/improver/fixBriefCli.d.ts +11 -0
- package/dist/eval/improver/fixBriefCli.d.ts.map +1 -0
- package/dist/eval/improver/fixBriefCli.js +46 -0
- package/dist/eval/improver/fixBriefCli.js.map +1 -0
- package/dist/eval/improver/flakeDetection.d.ts +50 -0
- package/dist/eval/improver/flakeDetection.d.ts.map +1 -0
- package/dist/eval/improver/flakeDetection.js +0 -0
- package/dist/eval/improver/flakeDetection.js.map +1 -0
- package/dist/eval/improver/flakeDetectionCli.d.ts +8 -0
- package/dist/eval/improver/flakeDetectionCli.d.ts.map +1 -0
- package/dist/eval/improver/flakeDetectionCli.js +27 -0
- package/dist/eval/improver/flakeDetectionCli.js.map +1 -0
- package/dist/eval/improver/heldout.d.ts +44 -0
- package/dist/eval/improver/heldout.d.ts.map +1 -0
- package/dist/eval/improver/heldout.js +52 -0
- package/dist/eval/improver/heldout.js.map +1 -0
- package/dist/eval/improver/knowledgeFeedback.d.ts +56 -0
- package/dist/eval/improver/knowledgeFeedback.d.ts.map +1 -0
- package/dist/eval/improver/knowledgeFeedback.js +88 -0
- package/dist/eval/improver/knowledgeFeedback.js.map +1 -0
- package/dist/eval/improver/knowledgeFeedbackCli.d.ts +8 -0
- package/dist/eval/improver/knowledgeFeedbackCli.d.ts.map +1 -0
- package/dist/eval/improver/knowledgeFeedbackCli.js +28 -0
- package/dist/eval/improver/knowledgeFeedbackCli.js.map +1 -0
- package/dist/eval/improver/report.d.ts +36 -0
- package/dist/eval/improver/report.d.ts.map +1 -0
- package/dist/eval/improver/report.js +61 -0
- package/dist/eval/improver/report.js.map +1 -0
- package/dist/eval/improver/reportCli.d.ts +7 -0
- package/dist/eval/improver/reportCli.d.ts.map +1 -0
- package/dist/eval/improver/reportCli.js +27 -0
- package/dist/eval/improver/reportCli.js.map +1 -0
- package/dist/eval/oracle/actualArtifactResolution.d.ts +49 -0
- package/dist/eval/oracle/actualArtifactResolution.d.ts.map +1 -0
- package/dist/eval/oracle/actualArtifactResolution.js +73 -0
- package/dist/eval/oracle/actualArtifactResolution.js.map +1 -0
- package/dist/eval/oracle/cli.d.ts +30 -0
- package/dist/eval/oracle/cli.d.ts.map +1 -0
- package/dist/eval/oracle/cli.js +179 -0
- package/dist/eval/oracle/cli.js.map +1 -0
- package/dist/eval/oracle/diff.d.ts +26 -0
- package/dist/eval/oracle/diff.d.ts.map +1 -0
- package/dist/eval/oracle/diff.js +51 -0
- package/dist/eval/oracle/diff.js.map +1 -0
- package/dist/eval/oracle/index.d.ts +81 -0
- package/dist/eval/oracle/index.d.ts.map +1 -0
- package/dist/eval/oracle/index.js +55 -0
- package/dist/eval/oracle/index.js.map +1 -0
- package/dist/eval/oracle/normalize.d.ts +85 -0
- package/dist/eval/oracle/normalize.d.ts.map +1 -0
- package/dist/eval/oracle/normalize.js +321 -0
- package/dist/eval/oracle/normalize.js.map +1 -0
- package/dist/eval/oracle/score.d.ts +26 -0
- package/dist/eval/oracle/score.d.ts.map +1 -0
- package/dist/eval/oracle/score.js +15 -0
- package/dist/eval/oracle/score.js.map +1 -0
- package/dist/eval/oracle/systest.d.ts +30 -0
- package/dist/eval/oracle/systest.d.ts.map +1 -0
- package/dist/eval/oracle/systest.js +98 -0
- package/dist/eval/oracle/systest.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +753 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.js +38 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/customFilters.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.d.ts +21 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.js +168 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/fields.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.js +47 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/panels.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.d.ts +16 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.js +124 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/toolbar.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.d.ts +12 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.js +36 -0
- package/dist/knowledge/formPatterns/catalog/subPatterns/workspaceSections.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.d.ts +11 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.js +37 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/common.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.d.ts +8 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.js +86 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsMaster.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.js +69 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/detailsTransaction.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.d.ts +14 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.js +187 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/dialog.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.js +54 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/factBox.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.js +33 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/listPage.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.d.ts +10 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.js +95 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/lookup.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.d.ts +16 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.js +158 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleDetails.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.js +34 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleList.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.js +99 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/simpleListDetails.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.d.ts +8 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.js +95 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/tableOfContents.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.d.ts +9 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.js +42 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/task.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.d.ts +7 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.js +42 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/wizard.js.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.d.ts +17 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.js +157 -0
- package/dist/knowledge/formPatterns/catalog/topLevel/workspace.js.map +1 -0
- package/dist/knowledge/formPatterns/crossCheck.d.ts +44 -0
- package/dist/knowledge/formPatterns/crossCheck.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/crossCheck.js +109 -0
- package/dist/knowledge/formPatterns/crossCheck.js.map +1 -0
- package/dist/knowledge/formPatterns/index.d.ts +30 -0
- package/dist/knowledge/formPatterns/index.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/index.js +158 -0
- package/dist/knowledge/formPatterns/index.js.map +1 -0
- package/dist/knowledge/formPatterns/methodStubs.d.ts +45 -0
- package/dist/knowledge/formPatterns/methodStubs.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/methodStubs.js +257 -0
- package/dist/knowledge/formPatterns/methodStubs.js.map +1 -0
- package/dist/knowledge/formPatterns/types.d.ts +115 -0
- package/dist/knowledge/formPatterns/types.d.ts.map +1 -0
- package/dist/knowledge/formPatterns/types.js +44 -0
- package/dist/knowledge/formPatterns/types.js.map +1 -0
- package/dist/metadata/enhancedParser.d.ts +91 -0
- package/dist/metadata/enhancedParser.d.ts.map +1 -0
- package/dist/metadata/enhancedParser.js +379 -0
- package/dist/metadata/enhancedParser.js.map +1 -0
- package/dist/metadata/formPatternMiner.d.ts +78 -0
- package/dist/metadata/formPatternMiner.d.ts.map +1 -0
- package/dist/metadata/formPatternMiner.js +220 -0
- package/dist/metadata/formPatternMiner.js.map +1 -0
- package/dist/metadata/labelParser.d.ts +71 -0
- package/dist/metadata/labelParser.d.ts.map +1 -0
- package/dist/metadata/labelParser.js +258 -0
- package/dist/metadata/labelParser.js.map +1 -0
- package/dist/metadata/symbolCountsWorker.d.ts +16 -0
- package/dist/metadata/symbolCountsWorker.d.ts.map +1 -0
- package/dist/metadata/symbolCountsWorker.js +42 -0
- package/dist/metadata/symbolCountsWorker.js.map +1 -0
- package/dist/metadata/symbolIndex.d.ts +474 -0
- package/dist/metadata/symbolIndex.d.ts.map +1 -0
- package/dist/metadata/symbolIndex.js +3152 -0
- package/dist/metadata/symbolIndex.js.map +1 -0
- package/dist/metadata/types.d.ts +267 -0
- package/dist/metadata/types.d.ts.map +1 -0
- package/dist/metadata/types.js +5 -0
- package/dist/metadata/types.js.map +1 -0
- package/dist/metadata/xmlParser.d.ts +232 -0
- package/dist/metadata/xmlParser.d.ts.map +1 -0
- package/dist/metadata/xmlParser.js +1021 -0
- package/dist/metadata/xmlParser.js.map +1 -0
- package/dist/metadata/xppDeclaration.d.ts +116 -0
- package/dist/metadata/xppDeclaration.d.ts.map +1 -0
- package/dist/metadata/xppDeclaration.js +350 -0
- package/dist/metadata/xppDeclaration.js.map +1 -0
- package/dist/middleware/apiKeyAuth.d.ts +7 -0
- package/dist/middleware/apiKeyAuth.d.ts.map +1 -0
- package/dist/middleware/apiKeyAuth.js +70 -0
- package/dist/middleware/apiKeyAuth.js.map +1 -0
- package/dist/middleware/rateLimiter.d.ts +7 -0
- package/dist/middleware/rateLimiter.d.ts.map +1 -0
- package/dist/middleware/rateLimiter.js +78 -0
- package/dist/middleware/rateLimiter.js.map +1 -0
- package/dist/prompts/codeReview.d.ts +8 -0
- package/dist/prompts/codeReview.d.ts.map +1 -0
- package/dist/prompts/codeReview.js +619 -0
- package/dist/prompts/codeReview.js.map +1 -0
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +6 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/systemInstructions.d.ts +33 -0
- package/dist/prompts/systemInstructions.d.ts.map +1 -0
- package/dist/prompts/systemInstructions.js +154 -0
- package/dist/prompts/systemInstructions.js.map +1 -0
- package/dist/resources/classResource.d.ts +17 -0
- package/dist/resources/classResource.d.ts.map +1 -0
- package/dist/resources/classResource.js +33 -0
- package/dist/resources/classResource.js.map +1 -0
- package/dist/resources/index.d.ts +20 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +154 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server/mcpServer.d.ts +11 -0
- package/dist/server/mcpServer.d.ts.map +1 -0
- package/dist/server/mcpServer.js +185 -0
- package/dist/server/mcpServer.js.map +1 -0
- package/dist/server/serverMode.d.ts +53 -0
- package/dist/server/serverMode.d.ts.map +1 -0
- package/dist/server/serverMode.js +72 -0
- package/dist/server/serverMode.js.map +1 -0
- package/dist/server/toolAnnotations.d.ts +28 -0
- package/dist/server/toolAnnotations.d.ts.map +1 -0
- package/dist/server/toolAnnotations.js +70 -0
- package/dist/server/toolAnnotations.js.map +1 -0
- package/dist/server/toolSchemas/analyzeCode.d.ts +71 -0
- package/dist/server/toolSchemas/analyzeCode.d.ts.map +1 -0
- package/dist/server/toolSchemas/analyzeCode.js +50 -0
- package/dist/server/toolSchemas/analyzeCode.js.map +1 -0
- package/dist/server/toolSchemas/batchGetInfo.d.ts +32 -0
- package/dist/server/toolSchemas/batchGetInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/batchGetInfo.js +35 -0
- package/dist/server/toolSchemas/batchGetInfo.js.map +1 -0
- package/dist/server/toolSchemas/buildD365foProject.d.ts +44 -0
- package/dist/server/toolSchemas/buildD365foProject.d.ts.map +1 -0
- package/dist/server/toolSchemas/buildD365foProject.js +47 -0
- package/dist/server/toolSchemas/buildD365foProject.js.map +1 -0
- package/dist/server/toolSchemas/d365foFile.d.ts +99 -0
- package/dist/server/toolSchemas/d365foFile.d.ts.map +1 -0
- package/dist/server/toolSchemas/d365foFile.js +163 -0
- package/dist/server/toolSchemas/d365foFile.js.map +1 -0
- package/dist/server/toolSchemas/extensionInfo.d.ts +65 -0
- package/dist/server/toolSchemas/extensionInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/extensionInfo.js +73 -0
- package/dist/server/toolSchemas/extensionInfo.js.map +1 -0
- package/dist/server/toolSchemas/findReferences.d.ts +35 -0
- package/dist/server/toolSchemas/findReferences.d.ts.map +1 -0
- package/dist/server/toolSchemas/findReferences.js +35 -0
- package/dist/server/toolSchemas/findReferences.js.map +1 -0
- package/dist/server/toolSchemas/generateObject.d.ts +246 -0
- package/dist/server/toolSchemas/generateObject.d.ts.map +1 -0
- package/dist/server/toolSchemas/generateObject.js +158 -0
- package/dist/server/toolSchemas/generateObject.js.map +1 -0
- package/dist/server/toolSchemas/getKnowledge.d.ts +39 -0
- package/dist/server/toolSchemas/getKnowledge.d.ts.map +1 -0
- package/dist/server/toolSchemas/getKnowledge.js +46 -0
- package/dist/server/toolSchemas/getKnowledge.js.map +1 -0
- package/dist/server/toolSchemas/getMethod.d.ts +30 -0
- package/dist/server/toolSchemas/getMethod.d.ts.map +1 -0
- package/dist/server/toolSchemas/getMethod.js +34 -0
- package/dist/server/toolSchemas/getMethod.js.map +1 -0
- package/dist/server/toolSchemas/getObjectInfo.d.ts +24 -0
- package/dist/server/toolSchemas/getObjectInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/getObjectInfo.js +30 -0
- package/dist/server/toolSchemas/getObjectInfo.js.map +1 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.d.ts +29 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.js +29 -0
- package/dist/server/toolSchemas/getWorkspaceInfo.js.map +1 -0
- package/dist/server/toolSchemas/index.d.ts +1142 -0
- package/dist/server/toolSchemas/index.d.ts.map +1 -0
- package/dist/server/toolSchemas/index.js +60 -0
- package/dist/server/toolSchemas/index.js.map +1 -0
- package/dist/server/toolSchemas/labels.d.ts +172 -0
- package/dist/server/toolSchemas/labels.d.ts.map +1 -0
- package/dist/server/toolSchemas/labels.js +159 -0
- package/dist/server/toolSchemas/labels.js.map +1 -0
- package/dist/server/toolSchemas/objectPatterns.d.ts +93 -0
- package/dist/server/toolSchemas/objectPatterns.d.ts.map +1 -0
- package/dist/server/toolSchemas/objectPatterns.js +104 -0
- package/dist/server/toolSchemas/objectPatterns.js.map +1 -0
- package/dist/server/toolSchemas/prepare.d.ts +49 -0
- package/dist/server/toolSchemas/prepare.d.ts.map +1 -0
- package/dist/server/toolSchemas/prepare.js +57 -0
- package/dist/server/toolSchemas/prepare.js.map +1 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.d.ts +20 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.d.ts.map +1 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.js +19 -0
- package/dist/server/toolSchemas/reviewWorkspaceChanges.js.map +1 -0
- package/dist/server/toolSchemas/runBpCheck.d.ts +36 -0
- package/dist/server/toolSchemas/runBpCheck.d.ts.map +1 -0
- package/dist/server/toolSchemas/runBpCheck.js +21 -0
- package/dist/server/toolSchemas/runBpCheck.js.map +1 -0
- package/dist/server/toolSchemas/runSystestClass.d.ts +32 -0
- package/dist/server/toolSchemas/runSystestClass.d.ts.map +1 -0
- package/dist/server/toolSchemas/runSystestClass.js +20 -0
- package/dist/server/toolSchemas/runSystestClass.js.map +1 -0
- package/dist/server/toolSchemas/search.d.ts +109 -0
- package/dist/server/toolSchemas/search.d.ts.map +1 -0
- package/dist/server/toolSchemas/search.js +123 -0
- package/dist/server/toolSchemas/search.js.map +1 -0
- package/dist/server/toolSchemas/securityInfo.d.ts +45 -0
- package/dist/server/toolSchemas/securityInfo.d.ts.map +1 -0
- package/dist/server/toolSchemas/securityInfo.js +39 -0
- package/dist/server/toolSchemas/securityInfo.js.map +1 -0
- package/dist/server/toolSchemas/suggestEdt.d.ts +29 -0
- package/dist/server/toolSchemas/suggestEdt.d.ts.map +1 -0
- package/dist/server/toolSchemas/suggestEdt.js +29 -0
- package/dist/server/toolSchemas/suggestEdt.js.map +1 -0
- package/dist/server/toolSchemas/triggerDbSync.d.ts +47 -0
- package/dist/server/toolSchemas/triggerDbSync.d.ts.map +1 -0
- package/dist/server/toolSchemas/triggerDbSync.js +31 -0
- package/dist/server/toolSchemas/triggerDbSync.js.map +1 -0
- package/dist/server/toolSchemas/undoLastModification.d.ts +20 -0
- package/dist/server/toolSchemas/undoLastModification.d.ts.map +1 -0
- package/dist/server/toolSchemas/undoLastModification.js +17 -0
- package/dist/server/toolSchemas/undoLastModification.js.map +1 -0
- package/dist/server/toolSchemas/updateSymbolIndex.d.ts +19 -0
- package/dist/server/toolSchemas/updateSymbolIndex.d.ts.map +1 -0
- package/dist/server/toolSchemas/updateSymbolIndex.js +18 -0
- package/dist/server/toolSchemas/updateSymbolIndex.js.map +1 -0
- package/dist/server/toolSchemas/validateCode.d.ts +35 -0
- package/dist/server/toolSchemas/validateCode.d.ts.map +1 -0
- package/dist/server/toolSchemas/validateCode.js +38 -0
- package/dist/server/toolSchemas/validateCode.js.map +1 -0
- package/dist/server/toolSchemas/validateObjectNaming.d.ts +33 -0
- package/dist/server/toolSchemas/validateObjectNaming.d.ts.map +1 -0
- package/dist/server/toolSchemas/validateObjectNaming.js +32 -0
- package/dist/server/toolSchemas/validateObjectNaming.js.map +1 -0
- package/dist/server/toolSchemas/verifyD365foProject.d.ts +50 -0
- package/dist/server/toolSchemas/verifyD365foProject.d.ts.map +1 -0
- package/dist/server/toolSchemas/verifyD365foProject.js +46 -0
- package/dist/server/toolSchemas/verifyD365foProject.js.map +1 -0
- package/dist/server/transport.d.ts +30 -0
- package/dist/server/transport.d.ts.map +1 -0
- package/dist/server/transport.js +391 -0
- package/dist/server/transport.js.map +1 -0
- package/dist/tools/analyzeCode.d.ts +17 -0
- package/dist/tools/analyzeCode.d.ts.map +1 -0
- package/dist/tools/analyzeCode.js +63 -0
- package/dist/tools/analyzeCode.js.map +1 -0
- package/dist/tools/analyzeCompleteness.d.ts +20 -0
- package/dist/tools/analyzeCompleteness.d.ts.map +1 -0
- package/dist/tools/analyzeCompleteness.js +103 -0
- package/dist/tools/analyzeCompleteness.js.map +1 -0
- package/dist/tools/analyzeExtensionPoints.d.ts +21 -0
- package/dist/tools/analyzeExtensionPoints.d.ts.map +1 -0
- package/dist/tools/analyzeExtensionPoints.js +343 -0
- package/dist/tools/analyzeExtensionPoints.js.map +1 -0
- package/dist/tools/analyzePatterns.d.ts +20 -0
- package/dist/tools/analyzePatterns.d.ts.map +1 -0
- package/dist/tools/analyzePatterns.js +71 -0
- package/dist/tools/analyzePatterns.js.map +1 -0
- package/dist/tools/apiUsagePatterns.d.ts +20 -0
- package/dist/tools/apiUsagePatterns.d.ts.map +1 -0
- package/dist/tools/apiUsagePatterns.js +136 -0
- package/dist/tools/apiUsagePatterns.js.map +1 -0
- package/dist/tools/batchGetInfo.d.ts +50 -0
- package/dist/tools/batchGetInfo.d.ts.map +1 -0
- package/dist/tools/batchGetInfo.js +52 -0
- package/dist/tools/batchGetInfo.js.map +1 -0
- package/dist/tools/batchSearch.d.ts +64 -0
- package/dist/tools/batchSearch.d.ts.map +1 -0
- package/dist/tools/batchSearch.js +265 -0
- package/dist/tools/batchSearch.js.map +1 -0
- package/dist/tools/buildProject.d.ts +28 -0
- package/dist/tools/buildProject.d.ts.map +1 -0
- package/dist/tools/buildProject.js +929 -0
- package/dist/tools/buildProject.js.map +1 -0
- package/dist/tools/classInfo.d.ts +8 -0
- package/dist/tools/classInfo.d.ts.map +1 -0
- package/dist/tools/classInfo.js +235 -0
- package/dist/tools/classInfo.js.map +1 -0
- package/dist/tools/codeGen.d.ts +19 -0
- package/dist/tools/codeGen.d.ts.map +1 -0
- package/dist/tools/codeGen.js +1956 -0
- package/dist/tools/codeGen.js.map +1 -0
- package/dist/tools/completion.d.ts +8 -0
- package/dist/tools/completion.d.ts.map +1 -0
- package/dist/tools/completion.js +272 -0
- package/dist/tools/completion.js.map +1 -0
- package/dist/tools/configKeyInfo.d.ts +22 -0
- package/dist/tools/configKeyInfo.d.ts.map +1 -0
- package/dist/tools/configKeyInfo.js +74 -0
- package/dist/tools/configKeyInfo.js.map +1 -0
- package/dist/tools/createD365File.d.ts +479 -0
- package/dist/tools/createD365File.d.ts.map +1 -0
- package/dist/tools/createD365File.js +4185 -0
- package/dist/tools/createD365File.js.map +1 -0
- package/dist/tools/createLabel.d.ts +51 -0
- package/dist/tools/createLabel.d.ts.map +1 -0
- package/dist/tools/createLabel.js +761 -0
- package/dist/tools/createLabel.js.map +1 -0
- package/dist/tools/d365foErrorHelp.d.ts +29 -0
- package/dist/tools/d365foErrorHelp.d.ts.map +1 -0
- package/dist/tools/d365foErrorHelp.js +389 -0
- package/dist/tools/d365foErrorHelp.js.map +1 -0
- package/dist/tools/d365foFile.d.ts +20 -0
- package/dist/tools/d365foFile.d.ts.map +1 -0
- package/dist/tools/d365foFile.js +58 -0
- package/dist/tools/d365foFile.js.map +1 -0
- package/dist/tools/d365foFileOpSpecs.d.ts +41 -0
- package/dist/tools/d365foFileOpSpecs.d.ts.map +1 -0
- package/dist/tools/d365foFileOpSpecs.js +336 -0
- package/dist/tools/d365foFileOpSpecs.js.map +1 -0
- package/dist/tools/dataEntityInfo.d.ts +23 -0
- package/dist/tools/dataEntityInfo.d.ts.map +1 -0
- package/dist/tools/dataEntityInfo.js +45 -0
- package/dist/tools/dataEntityInfo.js.map +1 -0
- package/dist/tools/dataEntityXml.d.ts +31 -0
- package/dist/tools/dataEntityXml.d.ts.map +1 -0
- package/dist/tools/dataEntityXml.js +131 -0
- package/dist/tools/dataEntityXml.js.map +1 -0
- package/dist/tools/dbSync.d.ts +8 -0
- package/dist/tools/dbSync.d.ts.map +1 -0
- package/dist/tools/dbSync.js +285 -0
- package/dist/tools/dbSync.js.map +1 -0
- package/dist/tools/edtInfo.d.ts +23 -0
- package/dist/tools/edtInfo.d.ts.map +1 -0
- package/dist/tools/edtInfo.js +122 -0
- package/dist/tools/edtInfo.js.map +1 -0
- package/dist/tools/enumInfo.d.ts +17 -0
- package/dist/tools/enumInfo.d.ts.map +1 -0
- package/dist/tools/enumInfo.js +44 -0
- package/dist/tools/enumInfo.js.map +1 -0
- package/dist/tools/extensionInfo.d.ts +26 -0
- package/dist/tools/extensionInfo.d.ts.map +1 -0
- package/dist/tools/extensionInfo.js +73 -0
- package/dist/tools/extensionInfo.js.map +1 -0
- package/dist/tools/extensionSearch.d.ts +20 -0
- package/dist/tools/extensionSearch.d.ts.map +1 -0
- package/dist/tools/extensionSearch.js +72 -0
- package/dist/tools/extensionSearch.js.map +1 -0
- package/dist/tools/extensionStrategyAdvisor.d.ts +15 -0
- package/dist/tools/extensionStrategyAdvisor.d.ts.map +1 -0
- package/dist/tools/extensionStrategyAdvisor.js +519 -0
- package/dist/tools/extensionStrategyAdvisor.js.map +1 -0
- package/dist/tools/findCocExtensions.d.ts +20 -0
- package/dist/tools/findCocExtensions.d.ts.map +1 -0
- package/dist/tools/findCocExtensions.js +285 -0
- package/dist/tools/findCocExtensions.js.map +1 -0
- package/dist/tools/findEventHandlers.d.ts +21 -0
- package/dist/tools/findEventHandlers.d.ts.map +1 -0
- package/dist/tools/findEventHandlers.js +196 -0
- package/dist/tools/findEventHandlers.js.map +1 -0
- package/dist/tools/findReferences.d.ts +30 -0
- package/dist/tools/findReferences.d.ts.map +1 -0
- package/dist/tools/findReferences.js +551 -0
- package/dist/tools/findReferences.js.map +1 -0
- package/dist/tools/formInfo.d.ts +13 -0
- package/dist/tools/formInfo.d.ts.map +1 -0
- package/dist/tools/formInfo.js +407 -0
- package/dist/tools/formInfo.js.map +1 -0
- package/dist/tools/formPattern.d.ts +19 -0
- package/dist/tools/formPattern.d.ts.map +1 -0
- package/dist/tools/formPattern.js +52 -0
- package/dist/tools/formPattern.js.map +1 -0
- package/dist/tools/generateD365Xml.d.ts +12 -0
- package/dist/tools/generateD365Xml.d.ts.map +1 -0
- package/dist/tools/generateD365Xml.js +1454 -0
- package/dist/tools/generateD365Xml.js.map +1 -0
- package/dist/tools/generateFindMethods.d.ts +69 -0
- package/dist/tools/generateFindMethods.d.ts.map +1 -0
- package/dist/tools/generateFindMethods.js +207 -0
- package/dist/tools/generateFindMethods.js.map +1 -0
- package/dist/tools/generateMetadata.d.ts +37 -0
- package/dist/tools/generateMetadata.d.ts.map +1 -0
- package/dist/tools/generateMetadata.js +213 -0
- package/dist/tools/generateMetadata.js.map +1 -0
- package/dist/tools/generateObject.d.ts +34 -0
- package/dist/tools/generateObject.d.ts.map +1 -0
- package/dist/tools/generateObject.js +47 -0
- package/dist/tools/generateObject.js.map +1 -0
- package/dist/tools/generateRelationXpp.d.ts +45 -0
- package/dist/tools/generateRelationXpp.d.ts.map +1 -0
- package/dist/tools/generateRelationXpp.js +129 -0
- package/dist/tools/generateRelationXpp.js.map +1 -0
- package/dist/tools/generateSmart.d.ts +32 -0
- package/dist/tools/generateSmart.d.ts.map +1 -0
- package/dist/tools/generateSmart.js +52 -0
- package/dist/tools/generateSmart.js.map +1 -0
- package/dist/tools/generateSmartForm.d.ts +82 -0
- package/dist/tools/generateSmartForm.d.ts.map +1 -0
- package/dist/tools/generateSmartForm.js +996 -0
- package/dist/tools/generateSmartForm.js.map +1 -0
- package/dist/tools/generateSmartReport.d.ts +90 -0
- package/dist/tools/generateSmartReport.d.ts.map +1 -0
- package/dist/tools/generateSmartReport.js +1254 -0
- package/dist/tools/generateSmartReport.js.map +1 -0
- package/dist/tools/generateSmartTable.d.ts +138 -0
- package/dist/tools/generateSmartTable.d.ts.map +1 -0
- package/dist/tools/generateSmartTable.js +1305 -0
- package/dist/tools/generateSmartTable.js.map +1 -0
- package/dist/tools/generateTableFields.d.ts +48 -0
- package/dist/tools/generateTableFields.d.ts.map +1 -0
- package/dist/tools/generateTableFields.js +186 -0
- package/dist/tools/generateTableFields.js.map +1 -0
- package/dist/tools/generateTableRelation.d.ts +35 -0
- package/dist/tools/generateTableRelation.d.ts.map +1 -0
- package/dist/tools/generateTableRelation.js +161 -0
- package/dist/tools/generateTableRelation.js.map +1 -0
- package/dist/tools/getFormPatternSpec.d.ts +13 -0
- package/dist/tools/getFormPatternSpec.d.ts.map +1 -0
- package/dist/tools/getFormPatternSpec.js +290 -0
- package/dist/tools/getFormPatternSpec.js.map +1 -0
- package/dist/tools/getFormPatterns.d.ts +68 -0
- package/dist/tools/getFormPatterns.d.ts.map +1 -0
- package/dist/tools/getFormPatterns.js +471 -0
- package/dist/tools/getFormPatterns.js.map +1 -0
- package/dist/tools/getKnowledge.d.ts +27 -0
- package/dist/tools/getKnowledge.d.ts.map +1 -0
- package/dist/tools/getKnowledge.js +50 -0
- package/dist/tools/getKnowledge.js.map +1 -0
- package/dist/tools/getLabelInfo.d.ts +21 -0
- package/dist/tools/getLabelInfo.d.ts.map +1 -0
- package/dist/tools/getLabelInfo.js +138 -0
- package/dist/tools/getLabelInfo.js.map +1 -0
- package/dist/tools/getMethod.d.ts +19 -0
- package/dist/tools/getMethod.d.ts.map +1 -0
- package/dist/tools/getMethod.js +77 -0
- package/dist/tools/getMethod.js.map +1 -0
- package/dist/tools/getMethodSource.d.ts +17 -0
- package/dist/tools/getMethodSource.d.ts.map +1 -0
- package/dist/tools/getMethodSource.js +161 -0
- package/dist/tools/getMethodSource.js.map +1 -0
- package/dist/tools/getObjectInfo.d.ts +17 -0
- package/dist/tools/getObjectInfo.d.ts.map +1 -0
- package/dist/tools/getObjectInfo.js +88 -0
- package/dist/tools/getObjectInfo.js.map +1 -0
- package/dist/tools/getTablePatterns.d.ts +45 -0
- package/dist/tools/getTablePatterns.d.ts.map +1 -0
- package/dist/tools/getTablePatterns.js +235 -0
- package/dist/tools/getTablePatterns.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +11 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/labels.d.ts +25 -0
- package/dist/tools/labels.d.ts.map +1 -0
- package/dist/tools/labels.js +111 -0
- package/dist/tools/labels.js.map +1 -0
- package/dist/tools/macroInfo.d.ts +23 -0
- package/dist/tools/macroInfo.d.ts.map +1 -0
- package/dist/tools/macroInfo.js +62 -0
- package/dist/tools/macroInfo.js.map +1 -0
- package/dist/tools/mapInfo.d.ts +21 -0
- package/dist/tools/mapInfo.d.ts.map +1 -0
- package/dist/tools/mapInfo.js +54 -0
- package/dist/tools/mapInfo.js.map +1 -0
- package/dist/tools/mapXml.d.ts +26 -0
- package/dist/tools/mapXml.d.ts.map +1 -0
- package/dist/tools/mapXml.js +88 -0
- package/dist/tools/mapXml.js.map +1 -0
- package/dist/tools/menuItemInfo.d.ts +20 -0
- package/dist/tools/menuItemInfo.d.ts.map +1 -0
- package/dist/tools/menuItemInfo.js +185 -0
- package/dist/tools/menuItemInfo.js.map +1 -0
- package/dist/tools/methodSignature.d.ts +33 -0
- package/dist/tools/methodSignature.d.ts.map +1 -0
- package/dist/tools/methodSignature.js +348 -0
- package/dist/tools/methodSignature.js.map +1 -0
- package/dist/tools/modifyD365File.d.ts +130 -0
- package/dist/tools/modifyD365File.d.ts.map +1 -0
- package/dist/tools/modifyD365File.js +2179 -0
- package/dist/tools/modifyD365File.js.map +1 -0
- package/dist/tools/objectInfoRegistry.d.ts +36 -0
- package/dist/tools/objectInfoRegistry.d.ts.map +1 -0
- package/dist/tools/objectInfoRegistry.js +98 -0
- package/dist/tools/objectInfoRegistry.js.map +1 -0
- package/dist/tools/objectPatterns.d.ts +16 -0
- package/dist/tools/objectPatterns.d.ts.map +1 -0
- package/dist/tools/objectPatterns.js +84 -0
- package/dist/tools/objectPatterns.js.map +1 -0
- package/dist/tools/prepare.d.ts +18 -0
- package/dist/tools/prepare.d.ts.map +1 -0
- package/dist/tools/prepare.js +47 -0
- package/dist/tools/prepare.js.map +1 -0
- package/dist/tools/prepareChange.d.ts +43 -0
- package/dist/tools/prepareChange.d.ts.map +1 -0
- package/dist/tools/prepareChange.js +327 -0
- package/dist/tools/prepareChange.js.map +1 -0
- package/dist/tools/prepareCreate.d.ts +45 -0
- package/dist/tools/prepareCreate.d.ts.map +1 -0
- package/dist/tools/prepareCreate.js +263 -0
- package/dist/tools/prepareCreate.js.map +1 -0
- package/dist/tools/queryInfo.d.ts +17 -0
- package/dist/tools/queryInfo.d.ts.map +1 -0
- package/dist/tools/queryInfo.js +46 -0
- package/dist/tools/queryInfo.js.map +1 -0
- package/dist/tools/queryViewXml.d.ts +36 -0
- package/dist/tools/queryViewXml.d.ts.map +1 -0
- package/dist/tools/queryViewXml.js +129 -0
- package/dist/tools/queryViewXml.js.map +1 -0
- package/dist/tools/renameLabel.d.ts +29 -0
- package/dist/tools/renameLabel.d.ts.map +1 -0
- package/dist/tools/renameLabel.js +377 -0
- package/dist/tools/renameLabel.js.map +1 -0
- package/dist/tools/repairFormControls.d.ts +21 -0
- package/dist/tools/repairFormControls.d.ts.map +1 -0
- package/dist/tools/repairFormControls.js +151 -0
- package/dist/tools/repairFormControls.js.map +1 -0
- package/dist/tools/reportInfo.d.ts +14 -0
- package/dist/tools/reportInfo.d.ts.map +1 -0
- package/dist/tools/reportInfo.js +302 -0
- package/dist/tools/reportInfo.js.map +1 -0
- package/dist/tools/resolveReferences.d.ts +89 -0
- package/dist/tools/resolveReferences.d.ts.map +1 -0
- package/dist/tools/resolveReferences.js +791 -0
- package/dist/tools/resolveReferences.js.map +1 -0
- package/dist/tools/reviewWorkspaceChanges.d.ts +14 -0
- package/dist/tools/reviewWorkspaceChanges.d.ts.map +1 -0
- package/dist/tools/reviewWorkspaceChanges.js +73 -0
- package/dist/tools/reviewWorkspaceChanges.js.map +1 -0
- package/dist/tools/runBpCheck.d.ts +14 -0
- package/dist/tools/runBpCheck.d.ts.map +1 -0
- package/dist/tools/runBpCheck.js +241 -0
- package/dist/tools/runBpCheck.js.map +1 -0
- package/dist/tools/search.d.ts +20 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +246 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/searchLabels.d.ts +26 -0
- package/dist/tools/searchLabels.d.ts.map +1 -0
- package/dist/tools/searchLabels.js +91 -0
- package/dist/tools/searchLabels.js.map +1 -0
- package/dist/tools/searchUnified.d.ts +28 -0
- package/dist/tools/searchUnified.d.ts.map +1 -0
- package/dist/tools/searchUnified.js +48 -0
- package/dist/tools/searchUnified.js.map +1 -0
- package/dist/tools/securityArtifactInfo.d.ts +14 -0
- package/dist/tools/securityArtifactInfo.d.ts.map +1 -0
- package/dist/tools/securityArtifactInfo.js +192 -0
- package/dist/tools/securityArtifactInfo.js.map +1 -0
- package/dist/tools/securityCoverageInfo.d.ts +21 -0
- package/dist/tools/securityCoverageInfo.d.ts.map +1 -0
- package/dist/tools/securityCoverageInfo.js +248 -0
- package/dist/tools/securityCoverageInfo.js.map +1 -0
- package/dist/tools/securityInfo.d.ts +22 -0
- package/dist/tools/securityInfo.d.ts.map +1 -0
- package/dist/tools/securityInfo.js +43 -0
- package/dist/tools/securityInfo.js.map +1 -0
- package/dist/tools/securityPolicyInfo.d.ts +22 -0
- package/dist/tools/securityPolicyInfo.d.ts.map +1 -0
- package/dist/tools/securityPolicyInfo.js +49 -0
- package/dist/tools/securityPolicyInfo.js.map +1 -0
- package/dist/tools/securityPrivilegeXml.d.ts +21 -0
- package/dist/tools/securityPrivilegeXml.d.ts.map +1 -0
- package/dist/tools/securityPrivilegeXml.js +61 -0
- package/dist/tools/securityPrivilegeXml.js.map +1 -0
- package/dist/tools/serviceInfo.d.ts +25 -0
- package/dist/tools/serviceInfo.d.ts.map +1 -0
- package/dist/tools/serviceInfo.js +93 -0
- package/dist/tools/serviceInfo.js.map +1 -0
- package/dist/tools/suggestEdt.d.ts +15 -0
- package/dist/tools/suggestEdt.d.ts.map +1 -0
- package/dist/tools/suggestEdt.js +348 -0
- package/dist/tools/suggestEdt.js.map +1 -0
- package/dist/tools/suggestImplementation.d.ts +20 -0
- package/dist/tools/suggestImplementation.d.ts.map +1 -0
- package/dist/tools/suggestImplementation.js +144 -0
- package/dist/tools/suggestImplementation.js.map +1 -0
- package/dist/tools/sysTestRunner.d.ts +14 -0
- package/dist/tools/sysTestRunner.d.ts.map +1 -0
- package/dist/tools/sysTestRunner.js +145 -0
- package/dist/tools/sysTestRunner.js.map +1 -0
- package/dist/tools/tableExtensionInfo.d.ts +91 -0
- package/dist/tools/tableExtensionInfo.d.ts.map +1 -0
- package/dist/tools/tableExtensionInfo.js +305 -0
- package/dist/tools/tableExtensionInfo.js.map +1 -0
- package/dist/tools/tableInfo.d.ts +23 -0
- package/dist/tools/tableInfo.d.ts.map +1 -0
- package/dist/tools/tableInfo.js +125 -0
- package/dist/tools/tableInfo.js.map +1 -0
- package/dist/tools/toolHandler.d.ts +4 -0
- package/dist/tools/toolHandler.d.ts.map +1 -0
- package/dist/tools/toolHandler.js +508 -0
- package/dist/tools/toolHandler.js.map +1 -0
- package/dist/tools/undoLastModification.d.ts +15 -0
- package/dist/tools/undoLastModification.d.ts.map +1 -0
- package/dist/tools/undoLastModification.js +147 -0
- package/dist/tools/undoLastModification.js.map +1 -0
- package/dist/tools/updateSymbolIndex.d.ts +15 -0
- package/dist/tools/updateSymbolIndex.d.ts.map +1 -0
- package/dist/tools/updateSymbolIndex.js +520 -0
- package/dist/tools/updateSymbolIndex.js.map +1 -0
- package/dist/tools/validateCode.d.ts +24 -0
- package/dist/tools/validateCode.d.ts.map +1 -0
- package/dist/tools/validateCode.js +201 -0
- package/dist/tools/validateCode.js.map +1 -0
- package/dist/tools/validateFormPattern.d.ts +51 -0
- package/dist/tools/validateFormPattern.d.ts.map +1 -0
- package/dist/tools/validateFormPattern.js +201 -0
- package/dist/tools/validateFormPattern.js.map +1 -0
- package/dist/tools/validateObjectNaming.d.ts +21 -0
- package/dist/tools/validateObjectNaming.d.ts.map +1 -0
- package/dist/tools/validateObjectNaming.js +319 -0
- package/dist/tools/validateObjectNaming.js.map +1 -0
- package/dist/tools/validateXpp.d.ts +79 -0
- package/dist/tools/validateXpp.d.ts.map +1 -0
- package/dist/tools/validateXpp.js +705 -0
- package/dist/tools/validateXpp.js.map +1 -0
- package/dist/tools/verifyD365Project.d.ts +21 -0
- package/dist/tools/verifyD365Project.d.ts.map +1 -0
- package/dist/tools/verifyD365Project.js +299 -0
- package/dist/tools/verifyD365Project.js.map +1 -0
- package/dist/tools/viewInfo.d.ts +17 -0
- package/dist/tools/viewInfo.d.ts.map +1 -0
- package/dist/tools/viewInfo.js +46 -0
- package/dist/tools/viewInfo.js.map +1 -0
- package/dist/tools/xppKnowledge.d.ts +45 -0
- package/dist/tools/xppKnowledge.d.ts.map +1 -0
- package/dist/tools/xppKnowledge.js +2815 -0
- package/dist/tools/xppKnowledge.js.map +1 -0
- package/dist/types/context.d.ts +56 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/context.js +5 -0
- package/dist/types/context.js.map +1 -0
- package/dist/utils/callDedup.d.ts +29 -0
- package/dist/utils/callDedup.d.ts.map +1 -0
- package/dist/utils/callDedup.js +87 -0
- package/dist/utils/callDedup.js.map +1 -0
- package/dist/utils/configManager.d.ts +307 -0
- package/dist/utils/configManager.d.ts.map +1 -0
- package/dist/utils/configManager.js +1169 -0
- package/dist/utils/configManager.js.map +1 -0
- package/dist/utils/d365XmlNormalizer.d.ts +9 -0
- package/dist/utils/d365XmlNormalizer.d.ts.map +1 -0
- package/dist/utils/d365XmlNormalizer.js +24 -0
- package/dist/utils/d365XmlNormalizer.js.map +1 -0
- package/dist/utils/edtExtensionValidator.d.ts +110 -0
- package/dist/utils/edtExtensionValidator.d.ts.map +1 -0
- package/dist/utils/edtExtensionValidator.js +367 -0
- package/dist/utils/edtExtensionValidator.js.map +1 -0
- package/dist/utils/eolUtils.d.ts +15 -0
- package/dist/utils/eolUtils.d.ts.map +1 -0
- package/dist/utils/eolUtils.js +21 -0
- package/dist/utils/eolUtils.js.map +1 -0
- package/dist/utils/extractManifest.d.ts +34 -0
- package/dist/utils/extractManifest.d.ts.map +1 -0
- package/dist/utils/extractManifest.js +43 -0
- package/dist/utils/extractManifest.js.map +1 -0
- package/dist/utils/fieldControlTypes.d.ts +44 -0
- package/dist/utils/fieldControlTypes.d.ts.map +1 -0
- package/dist/utils/fieldControlTypes.js +97 -0
- package/dist/utils/fieldControlTypes.js.map +1 -0
- package/dist/utils/formCloner.d.ts +77 -0
- package/dist/utils/formCloner.d.ts.map +1 -0
- package/dist/utils/formCloner.js +364 -0
- package/dist/utils/formCloner.js.map +1 -0
- package/dist/utils/formControlExpander.d.ts +68 -0
- package/dist/utils/formControlExpander.d.ts.map +1 -0
- package/dist/utils/formControlExpander.js +207 -0
- package/dist/utils/formControlExpander.js.map +1 -0
- package/dist/utils/formControlRepair.d.ts +81 -0
- package/dist/utils/formControlRepair.d.ts.map +1 -0
- package/dist/utils/formControlRepair.js +203 -0
- package/dist/utils/formControlRepair.js.map +1 -0
- package/dist/utils/formExtensionShapeValidator.d.ts +44 -0
- package/dist/utils/formExtensionShapeValidator.d.ts.map +1 -0
- package/dist/utils/formExtensionShapeValidator.js +99 -0
- package/dist/utils/formExtensionShapeValidator.js.map +1 -0
- package/dist/utils/formPatternTemplates.d.ts +77 -0
- package/dist/utils/formPatternTemplates.d.ts.map +1 -0
- package/dist/utils/formPatternTemplates.js +1917 -0
- package/dist/utils/formPatternTemplates.js.map +1 -0
- package/dist/utils/fsExtensionScanner.d.ts +76 -0
- package/dist/utils/fsExtensionScanner.d.ts.map +1 -0
- package/dist/utils/fsExtensionScanner.js +271 -0
- package/dist/utils/fsExtensionScanner.js.map +1 -0
- package/dist/utils/fuzzyMatching.d.ts +41 -0
- package/dist/utils/fuzzyMatching.d.ts.map +1 -0
- package/dist/utils/fuzzyMatching.js +186 -0
- package/dist/utils/fuzzyMatching.js.map +1 -0
- package/dist/utils/indexStaleness.d.ts +30 -0
- package/dist/utils/indexStaleness.d.ts.map +1 -0
- package/dist/utils/indexStaleness.js +104 -0
- package/dist/utils/indexStaleness.js.map +1 -0
- package/dist/utils/loadEnv.d.ts +32 -0
- package/dist/utils/loadEnv.d.ts.map +1 -0
- package/dist/utils/loadEnv.js +83 -0
- package/dist/utils/loadEnv.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +11 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/metadataResolver.d.ts +155 -0
- package/dist/utils/metadataResolver.d.ts.map +1 -0
- package/dist/utils/metadataResolver.js +277 -0
- package/dist/utils/metadataResolver.js.map +1 -0
- package/dist/utils/modelClassifier.d.ts +180 -0
- package/dist/utils/modelClassifier.d.ts.map +1 -0
- package/dist/utils/modelClassifier.js +373 -0
- package/dist/utils/modelClassifier.js.map +1 -0
- package/dist/utils/operationLocks.d.ts +26 -0
- package/dist/utils/operationLocks.d.ts.map +1 -0
- package/dist/utils/operationLocks.js +182 -0
- package/dist/utils/operationLocks.js.map +1 -0
- package/dist/utils/packageResolver.d.ts +52 -0
- package/dist/utils/packageResolver.d.ts.map +1 -0
- package/dist/utils/packageResolver.js +174 -0
- package/dist/utils/packageResolver.js.map +1 -0
- package/dist/utils/pathContainment.d.ts +60 -0
- package/dist/utils/pathContainment.d.ts.map +1 -0
- package/dist/utils/pathContainment.js +270 -0
- package/dist/utils/pathContainment.js.map +1 -0
- package/dist/utils/projectUtils.d.ts +14 -0
- package/dist/utils/projectUtils.d.ts.map +1 -0
- package/dist/utils/projectUtils.js +48 -0
- package/dist/utils/projectUtils.js.map +1 -0
- package/dist/utils/provenanceStore.d.ts +54 -0
- package/dist/utils/provenanceStore.d.ts.map +1 -0
- package/dist/utils/provenanceStore.js +203 -0
- package/dist/utils/provenanceStore.js.map +1 -0
- package/dist/utils/richContext.d.ts +45 -0
- package/dist/utils/richContext.d.ts.map +1 -0
- package/dist/utils/richContext.js +316 -0
- package/dist/utils/richContext.js.map +1 -0
- package/dist/utils/smartXmlBuilder.d.ts +187 -0
- package/dist/utils/smartXmlBuilder.d.ts.map +1 -0
- package/dist/utils/smartXmlBuilder.js +504 -0
- package/dist/utils/smartXmlBuilder.js.map +1 -0
- package/dist/utils/stdioSessionInfo.d.ts +53 -0
- package/dist/utils/stdioSessionInfo.d.ts.map +1 -0
- package/dist/utils/stdioSessionInfo.js +47 -0
- package/dist/utils/stdioSessionInfo.js.map +1 -0
- package/dist/utils/suggestionEngine.d.ts +20 -0
- package/dist/utils/suggestionEngine.d.ts.map +1 -0
- package/dist/utils/suggestionEngine.js +145 -0
- package/dist/utils/suggestionEngine.js.map +1 -0
- package/dist/utils/symbolLookup.d.ts +60 -0
- package/dist/utils/symbolLookup.d.ts.map +1 -0
- package/dist/utils/symbolLookup.js +0 -0
- package/dist/utils/symbolLookup.js.map +1 -0
- package/dist/utils/terminalUi.d.ts +85 -0
- package/dist/utils/terminalUi.d.ts.map +1 -0
- package/dist/utils/terminalUi.js +182 -0
- package/dist/utils/terminalUi.js.map +1 -0
- package/dist/utils/toolMetrics.d.ts +32 -0
- package/dist/utils/toolMetrics.d.ts.map +1 -0
- package/dist/utils/toolMetrics.js +89 -0
- package/dist/utils/toolMetrics.js.map +1 -0
- package/dist/utils/toolProgressMessage.d.ts +8 -0
- package/dist/utils/toolProgressMessage.d.ts.map +1 -0
- package/dist/utils/toolProgressMessage.js +182 -0
- package/dist/utils/toolProgressMessage.js.map +1 -0
- package/dist/utils/workspaceDetector.d.ts +51 -0
- package/dist/utils/workspaceDetector.d.ts.map +1 -0
- package/dist/utils/workspaceDetector.js +328 -0
- package/dist/utils/workspaceDetector.js.map +1 -0
- package/dist/utils/xppConfigProvider.d.ts +48 -0
- package/dist/utils/xppConfigProvider.d.ts.map +1 -0
- package/dist/utils/xppConfigProvider.js +121 -0
- package/dist/utils/xppConfigProvider.js.map +1 -0
- package/dist/utils/xppDocGen.d.ts +39 -0
- package/dist/utils/xppDocGen.d.ts.map +1 -0
- package/dist/utils/xppDocGen.js +518 -0
- package/dist/utils/xppDocGen.js.map +1 -0
- package/dist/utils/xppFormat.d.ts +21 -0
- package/dist/utils/xppFormat.d.ts.map +1 -0
- package/dist/utils/xppFormat.js +102 -0
- package/dist/utils/xppFormat.js.map +1 -0
- package/dist/utils/xppSelectLint.d.ts +18 -0
- package/dist/utils/xppSelectLint.d.ts.map +1 -0
- package/dist/utils/xppSelectLint.js +80 -0
- package/dist/utils/xppSelectLint.js.map +1 -0
- package/dist/validation/formPatternValidator.d.ts +53 -0
- package/dist/validation/formPatternValidator.d.ts.map +1 -0
- package/dist/validation/formPatternValidator.js +454 -0
- package/dist/validation/formPatternValidator.js.map +1 -0
- package/dist/workspace/contextRanker.d.ts +58 -0
- package/dist/workspace/contextRanker.d.ts.map +1 -0
- package/dist/workspace/contextRanker.js +216 -0
- package/dist/workspace/contextRanker.js.map +1 -0
- package/dist/workspace/contextSnapshot.d.ts +68 -0
- package/dist/workspace/contextSnapshot.d.ts.map +1 -0
- package/dist/workspace/contextSnapshot.js +193 -0
- package/dist/workspace/contextSnapshot.js.map +1 -0
- package/dist/workspace/hybridSearch.d.ts +39 -0
- package/dist/workspace/hybridSearch.d.ts.map +1 -0
- package/dist/workspace/hybridSearch.js +96 -0
- package/dist/workspace/hybridSearch.js.map +1 -0
- package/dist/workspace/workspaceScanner.d.ts +95 -0
- package/dist/workspace/workspaceScanner.d.ts.map +1 -0
- package/dist/workspace/workspaceScanner.js +247 -0
- package/dist/workspace/workspaceScanner.js.map +1 -0
- package/dist/workspace/workspaceUtils.d.ts +23 -0
- package/dist/workspace/workspaceUtils.d.ts.map +1 -0
- package/dist/workspace/workspaceUtils.js +77 -0
- package/dist/workspace/workspaceUtils.js.map +1 -0
- package/package.json +94 -0
|
@@ -0,0 +1,2179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modify D365FO File Tool
|
|
3
|
+
* Edit existing D365FO XML files (AxClass, AxTable, AxForm, etc.)
|
|
4
|
+
* Supports atomic operations: add method, add field, modify property
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import { execFile } from 'child_process';
|
|
9
|
+
import util from 'util';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { parseStringPromise } from 'xml2js';
|
|
12
|
+
import { getConfigManager, fallbackPackagePath, extractModelFromFilePath } from '../utils/configManager.js';
|
|
13
|
+
import { isStandardModel, resolveObjectPrefix, applyObjectPrefix } from '../utils/modelClassifier.js';
|
|
14
|
+
import { PackageResolver } from '../utils/packageResolver.js';
|
|
15
|
+
import { resolveDbPathLocally } from '../utils/metadataResolver.js';
|
|
16
|
+
import { assertWritePathAllowed } from '../utils/pathContainment.js';
|
|
17
|
+
import { bridgeValidateAfterWrite, canBridgeModify, bridgeAddMethod, bridgeRemoveMethod, bridgeAddField, bridgeSetProperty, bridgeReplaceCode, bridgeModifyField, bridgeRenameField, bridgeRemoveField, bridgeReplaceAllFields, bridgeAddIndex, bridgeRemoveIndex, bridgeAddRelation, bridgeRemoveRelation, bridgeAddFieldGroup, bridgeRemoveFieldGroup, bridgeAddFieldToFieldGroup, bridgeAddEnumValue, bridgeModifyEnumValue, bridgeRemoveEnumValue, bridgeAddControl, bridgeAddDataSource, bridgeAddFieldModification, bridgeAddMenuItemToMenu, bridgeRefreshProvider, } from '../bridge/index.js';
|
|
18
|
+
import { ProjectFileManager, ProjectFileFinder } from './createD365File.js';
|
|
19
|
+
import { heuristicEdtBaseType } from './generateSmartTable.js';
|
|
20
|
+
import { normalizeD365Xml } from '../utils/d365XmlNormalizer.js';
|
|
21
|
+
import { enforceGrounding } from '../utils/provenanceStore.js';
|
|
22
|
+
import { gateOnReferenceErrors } from './resolveReferences.js';
|
|
23
|
+
import { checkAddControlAgainstParentPattern, isFormPatternEnforceEnabled, } from './validateFormPattern.js';
|
|
24
|
+
import { validateEdtExtensionChange } from '../utils/edtExtensionValidator.js';
|
|
25
|
+
import { lintXppSelect } from '../utils/xppSelectLint.js';
|
|
26
|
+
import { getRequiredParams, renderOpSpec, OP_PARAM_ALIASES } from './d365foFileOpSpecs.js';
|
|
27
|
+
import { lookupSymbolNocase } from '../utils/symbolLookup.js';
|
|
28
|
+
/**
|
|
29
|
+
* Decode the standard XML entities (<, >, ', ", &) and normalise
|
|
30
|
+
* line endings by stripping xml2js's 
 representation of carriage return.
|
|
31
|
+
*
|
|
32
|
+
* IMPORTANT: & is decoded LAST so that sequences like `&quot;` are first
|
|
33
|
+
* turned into `"` and can then, if desired, be decoded to `"`, avoiding
|
|
34
|
+
* incorrect double-unescaping.
|
|
35
|
+
*/
|
|
36
|
+
function decodeStandardXmlEntities(source) {
|
|
37
|
+
return source
|
|
38
|
+
// xml2js Builder escapes \r as 
 — strip it to normalise to LF-only line endings
|
|
39
|
+
.replace(/
/g, '')
|
|
40
|
+
.replace(/</g, '<')
|
|
41
|
+
.replace(/>/g, '>')
|
|
42
|
+
.replace(/'/g, "'")
|
|
43
|
+
.replace(/"/g, '"')
|
|
44
|
+
.replace(/&/g, '&');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Decode XML entities from X++ source code.
|
|
48
|
+
*
|
|
49
|
+
* X++ source should never contain entity-encoded characters — `/// <summary>`
|
|
50
|
+
* doc comments, generic types like `List<str>`, and comparison operators like
|
|
51
|
+
* `x < y` all use literal `<` and `>`. When an AI model copies code from an
|
|
52
|
+
* SSRS report's entity-encoded <Text> block and passes it as `methodCode`, the
|
|
53
|
+
* entities would otherwise survive into the CDATA section and corrupt the source.
|
|
54
|
+
*
|
|
55
|
+
* This function decodes the 5 standard XML entities so that source code always
|
|
56
|
+
* contains proper characters before it is stored in the XML object.
|
|
57
|
+
*/
|
|
58
|
+
export function decodeXmlEntitiesFromXppSource(source) {
|
|
59
|
+
return decodeStandardXmlEntities(source);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Reject an X++ source payload that smuggles XML/CDATA structure.
|
|
63
|
+
*
|
|
64
|
+
* Method source written through the bridge is handed verbatim to the D365FO SDK
|
|
65
|
+
* serializer, which wraps it in `<![CDATA[ … ]]>` and emits the surrounding
|
|
66
|
+
* `<Method>…</Method>` markup itself. If the caller's source already contains
|
|
67
|
+
* the CDATA terminator `]]>` or closing metadata tags, the serializer writes
|
|
68
|
+
* them inside the CDATA block unchanged — producing structurally invalid XML:
|
|
69
|
+
* a premature/doubled `]]>` and a stray `<Method>` that drops the enclosing
|
|
70
|
+
* `</Method>` (exactly the corruption D365FO refuses to deserialize). The
|
|
71
|
+
* direct-XML replace fallback has the same exposure: a literal string replace
|
|
72
|
+
* that injects `]]>` into an existing CDATA block corrupts it too.
|
|
73
|
+
*
|
|
74
|
+
* This always means the AI passed a slice of the .xml file where clean X++ was
|
|
75
|
+
* expected. Reject it here — before it reaches disk — with an actionable
|
|
76
|
+
* message, rather than silently escaping and hiding the mistake.
|
|
77
|
+
*
|
|
78
|
+
* X++ legitimately uses `<`/`>` (generics, comparisons, doc comments), so we
|
|
79
|
+
* only flag the CDATA terminator and the specific opening/closing metadata
|
|
80
|
+
* tokens, never bare angle brackets.
|
|
81
|
+
*/
|
|
82
|
+
export function assertCleanXppSource(source, paramName) {
|
|
83
|
+
if (!source)
|
|
84
|
+
return;
|
|
85
|
+
if (source.includes(']]>')) {
|
|
86
|
+
throw new Error(`⛔ ${paramName} contains the CDATA terminator "]]>" — that is XML, not clean X++ source.\n\n` +
|
|
87
|
+
`Method source is wrapped in <![CDATA[ … ]]> by the metadata serializer, so a "]]>" inside it ` +
|
|
88
|
+
`breaks the CDATA block and yields invalid XML (doubled "]]>" plus a dropped </Method>), which ` +
|
|
89
|
+
`D365FO refuses to load.\n\n` +
|
|
90
|
+
`Pass ONLY the X++ code — no <Source>, <![CDATA[, ]]> or </Method> markup. The tool adds the wrapping itself.`);
|
|
91
|
+
}
|
|
92
|
+
const structuralTag = source.match(/<\/(?:Source|Methods?)>|<Method>|<!\[CDATA\[/);
|
|
93
|
+
if (structuralTag) {
|
|
94
|
+
throw new Error(`⛔ ${paramName} contains the XML metadata token "${structuralTag[0]}" — that is XML, not clean X++ source.\n\n` +
|
|
95
|
+
`You pasted a slice of the .xml file. Pass ONLY the X++ code — the tool emits the ` +
|
|
96
|
+
`<Method>, <Source> and <![CDATA[ … ]]> wrapping itself.`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** Strip X++ line/block/doc comments and string literals so their braces/parens
|
|
100
|
+
* don't skew structural scans. Returns a same-ish-length cleaned string. */
|
|
101
|
+
function stripXppCommentsAndStrings(s) {
|
|
102
|
+
let out = '';
|
|
103
|
+
let i = 0;
|
|
104
|
+
while (i < s.length) {
|
|
105
|
+
const two = s.slice(i, i + 2);
|
|
106
|
+
if (two === '//') { // line comment (incl. /// doc comments)
|
|
107
|
+
const nl = s.indexOf('\n', i);
|
|
108
|
+
i = nl === -1 ? s.length : nl;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (two === '/*') { // block comment
|
|
112
|
+
const end = s.indexOf('*/', i + 2);
|
|
113
|
+
i = end === -1 ? s.length : end + 2;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (s[i] === '"') { // string literal
|
|
117
|
+
i++;
|
|
118
|
+
while (i < s.length && s[i] !== '"') {
|
|
119
|
+
if (s[i] === '\\')
|
|
120
|
+
i++;
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
i++;
|
|
124
|
+
out += '""';
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
out += s[i];
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
/** Count top-level X++ method bodies: a `{` opened at brace-depth 0 immediately
|
|
133
|
+
* after a `)` (a method signature). Nested blocks (if/for/switch) are inside the
|
|
134
|
+
* body (depth > 0) and a class wrapper opens after an identifier, so neither is
|
|
135
|
+
* miscounted. */
|
|
136
|
+
export function countTopLevelMethodBodies(source) {
|
|
137
|
+
const cleaned = stripXppCommentsAndStrings(source);
|
|
138
|
+
let depth = 0;
|
|
139
|
+
let count = 0;
|
|
140
|
+
let prevSignificant = '';
|
|
141
|
+
for (const ch of cleaned) {
|
|
142
|
+
if (ch === '{') {
|
|
143
|
+
if (depth === 0 && prevSignificant === ')')
|
|
144
|
+
count++;
|
|
145
|
+
depth++;
|
|
146
|
+
}
|
|
147
|
+
else if (ch === '}') {
|
|
148
|
+
if (depth > 0)
|
|
149
|
+
depth--;
|
|
150
|
+
}
|
|
151
|
+
if (!/\s/.test(ch))
|
|
152
|
+
prevSignificant = ch;
|
|
153
|
+
}
|
|
154
|
+
return count;
|
|
155
|
+
}
|
|
156
|
+
/** Split a source string containing one or more top-level X++ methods into the
|
|
157
|
+
* individual method sources (each including any leading doc comments / attributes
|
|
158
|
+
* and its full body). Mirrors countTopLevelMethodBodies' brace/comment/string
|
|
159
|
+
* handling. Used to let add-method accept several methods in one call and add them
|
|
160
|
+
* one <Method> at a time. */
|
|
161
|
+
export function splitTopLevelMethodBodies(source) {
|
|
162
|
+
const s = source;
|
|
163
|
+
const methods = [];
|
|
164
|
+
let depth = 0;
|
|
165
|
+
let methodStart = -1;
|
|
166
|
+
let i = 0;
|
|
167
|
+
while (i < s.length) {
|
|
168
|
+
const ch = s[i];
|
|
169
|
+
// Start a method slice at the first significant char (incl. leading /// doc
|
|
170
|
+
// comments and [Attribute] blocks) after the previous method closed.
|
|
171
|
+
if (methodStart === -1 && !/\s/.test(ch))
|
|
172
|
+
methodStart = i;
|
|
173
|
+
const two = s.slice(i, i + 2);
|
|
174
|
+
if (two === '//') {
|
|
175
|
+
const nl = s.indexOf('\n', i);
|
|
176
|
+
i = nl === -1 ? s.length : nl;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (two === '/*') {
|
|
180
|
+
const end = s.indexOf('*/', i + 2);
|
|
181
|
+
i = end === -1 ? s.length : end + 2;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (ch === '"') {
|
|
185
|
+
i++;
|
|
186
|
+
while (i < s.length && s[i] !== '"') {
|
|
187
|
+
if (s[i] === '\\')
|
|
188
|
+
i++;
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
i++;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (ch === '{') {
|
|
195
|
+
depth++;
|
|
196
|
+
}
|
|
197
|
+
else if (ch === '}') {
|
|
198
|
+
if (depth > 0)
|
|
199
|
+
depth--;
|
|
200
|
+
if (depth === 0 && methodStart !== -1) {
|
|
201
|
+
methods.push(s.slice(methodStart, i + 1).trim());
|
|
202
|
+
methodStart = -1;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
i++;
|
|
206
|
+
}
|
|
207
|
+
// Trailing brace-less content (e.g. an interface/abstract method declaration).
|
|
208
|
+
if (methodStart !== -1) {
|
|
209
|
+
const tail = s.slice(methodStart).trim();
|
|
210
|
+
if (tail)
|
|
211
|
+
methods.push(tail);
|
|
212
|
+
}
|
|
213
|
+
return methods.filter(Boolean);
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Reject an add-method payload that contains more than one method. Each add-method
|
|
217
|
+
* call emits a single <Method>; passing two methods drops the second outside the
|
|
218
|
+
* class scope and yields invalid X++ ("Unexpected token 'public' specified outside
|
|
219
|
+
* the scope of any class or model element"). Splitting into separate calls is the fix.
|
|
220
|
+
*/
|
|
221
|
+
export function assertSingleMethodSource(source) {
|
|
222
|
+
if (!source)
|
|
223
|
+
return;
|
|
224
|
+
const count = countTopLevelMethodBodies(source);
|
|
225
|
+
if (count > 1) {
|
|
226
|
+
throw new Error(`⛔ add-method expects exactly ONE method, but ${count} method bodies were detected in the source.\n\n` +
|
|
227
|
+
`Each add-method call adds a single <Method> element. Passing multiple methods puts all but ` +
|
|
228
|
+
`the first OUTSIDE the class scope, producing invalid X++ ("Unexpected token 'public' specified ` +
|
|
229
|
+
`outside the scope of any class or model element").\n\n` +
|
|
230
|
+
`Add each method with its own add-method call — one method per call.`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Derive the method name from a full X++ method source: the identifier immediately
|
|
235
|
+
* before the first '(' of the signature, after stripping comments, strings and
|
|
236
|
+
* attribute blocks (e.g. [ExtensionOf(...)]). Lets add-method callers omit methodName
|
|
237
|
+
* when they already pass the complete source (e.g. "public static X find(...)").
|
|
238
|
+
* Returns null when no signature can be found.
|
|
239
|
+
*/
|
|
240
|
+
export function extractMethodNameFromSource(source) {
|
|
241
|
+
if (!source)
|
|
242
|
+
return null;
|
|
243
|
+
const cleaned = stripXppCommentsAndStrings(source).replace(/\[[^\]]*\]/g, ' ');
|
|
244
|
+
const m = cleaned.match(/\b([A-Za-z_]\w*)\s*\(/);
|
|
245
|
+
return m ? m[1] : null;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Direct XML file-level replace-code fallback, used when the C# bridge can't
|
|
249
|
+
* reach a method (e.g. form/form-extension control overrides not exposed via
|
|
250
|
+
* the Methods API). Last resort — the bridge is always preferred.
|
|
251
|
+
*/
|
|
252
|
+
async function directXmlReplaceCode(filePath, oldCode, newCode) {
|
|
253
|
+
try {
|
|
254
|
+
// Files on disk are CRLF; oldCode from get_method_source is typically LF-only.
|
|
255
|
+
// Normalize both to LF for matching, then normalizeD365Xml restores CRLF on write.
|
|
256
|
+
const rawContent = await fs.readFile(filePath, 'utf-8');
|
|
257
|
+
const content = rawContent.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
258
|
+
const normOld = oldCode.replace(/\r\n/g, '\n');
|
|
259
|
+
const normNew = newCode.replace(/\r\n/g, '\n');
|
|
260
|
+
if (!content.includes(normOld)) {
|
|
261
|
+
return null; // oldCode not found in file at all
|
|
262
|
+
}
|
|
263
|
+
// Without /g, String.replace() only replaces the first occurrence, so require
|
|
264
|
+
// exactly one match to avoid silently picking the wrong one.
|
|
265
|
+
const occurrences = content.split(normOld).length - 1;
|
|
266
|
+
if (occurrences > 1) {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
message: `❌ directXmlReplaceCode: oldCode appears ${occurrences} times in ${filePath} — replacement is ambiguous. Provide a more specific oldCode snippet.`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const updated = content.replace(normOld, normNew);
|
|
273
|
+
if (updated === content) {
|
|
274
|
+
return null; // no change made
|
|
275
|
+
}
|
|
276
|
+
await fs.writeFile(filePath, normalizeD365Xml(updated), 'utf-8');
|
|
277
|
+
console.error(`[modify_d365fo_file] ✅ directXmlReplaceCode fallback: replaced in ${filePath}`);
|
|
278
|
+
return {
|
|
279
|
+
success: true,
|
|
280
|
+
message: `✅ Code replaced via direct XML fallback (bridge was unavailable). File: ${filePath}`,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
console.error(`[modify_d365fo_file] directXmlReplaceCode failed: ${err}`);
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Direct XML fallback for modify-property, used when the bridge rejects
|
|
290
|
+
* modify-property for an object type (e.g. AxForm) even though the property
|
|
291
|
+
* is a plain text element (e.g. <Caption>) editable by string replacement.
|
|
292
|
+
* Only used when the bridge itself reports failure.
|
|
293
|
+
*
|
|
294
|
+
* `propertyPath` is a bare element name (no XPath/dotted-path support).
|
|
295
|
+
* Refuses to act if the element is missing (returns null so the caller
|
|
296
|
+
* surfaces the original bridge error) or ambiguous (appears more than once).
|
|
297
|
+
*/
|
|
298
|
+
async function directXmlModifyProperty(filePath, propertyPath, propertyValue) {
|
|
299
|
+
try {
|
|
300
|
+
const rawContent = await fs.readFile(filePath, 'utf-8');
|
|
301
|
+
const content = rawContent.replace(/^/, '');
|
|
302
|
+
const escapedValue = String(propertyValue)
|
|
303
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
304
|
+
const tagName = propertyPath.split(/[./]/).pop();
|
|
305
|
+
const openTagRe = new RegExp(`<${tagName}\\b[^>]*>[\\s\\S]*?</${tagName}>`, 'g');
|
|
306
|
+
const selfClosingRe = new RegExp(`<${tagName}\\b([^>]*)/>`, 'g');
|
|
307
|
+
const openMatches = content.match(openTagRe) ?? [];
|
|
308
|
+
const selfClosingMatches = content.match(selfClosingRe) ?? [];
|
|
309
|
+
const totalMatches = openMatches.length + selfClosingMatches.length;
|
|
310
|
+
if (totalMatches === 0) {
|
|
311
|
+
return null; // property element not found — let the caller surface the original bridge error
|
|
312
|
+
}
|
|
313
|
+
if (totalMatches > 1) {
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
message: `❌ directXmlModifyProperty: <${tagName}> appears ${totalMatches} times in ${filePath} — ambiguous, refusing to guess which one.`,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const updated = openMatches.length === 1
|
|
320
|
+
? content.replace(openTagRe, m => m.replace(/>[\s\S]*?</, `>${escapedValue}<`))
|
|
321
|
+
: content.replace(selfClosingRe, (_m, attrs) => `<${tagName}${attrs}>${escapedValue}</${tagName}>`);
|
|
322
|
+
await fs.writeFile(filePath, normalizeD365Xml(updated), 'utf-8');
|
|
323
|
+
console.error(`[modify_d365fo_file] ✅ directXmlModifyProperty fallback: set <${tagName}> in ${filePath}`);
|
|
324
|
+
return {
|
|
325
|
+
success: true,
|
|
326
|
+
message: `✅ Property '${propertyPath}' set via direct XML fallback (bridge does not support modify-property for this object type). File: ${filePath}`,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
catch (err) {
|
|
330
|
+
console.error(`[modify_d365fo_file] directXmlModifyProperty failed: ${err}`);
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Direct XML fallback for add-menu-item-to-menu.
|
|
336
|
+
* The C# bridge can only modify menus it has loaded from its startup roots;
|
|
337
|
+
* newly created menus trigger a NullRef because they aren't in the bridge's
|
|
338
|
+
* in-memory model yet (even after update_symbol_index). This function edits
|
|
339
|
+
* the XML file directly as a last-resort fallback.
|
|
340
|
+
*/
|
|
341
|
+
async function directXmlAddMenuItemToMenu(filePath, menuItemToAdd, menuItemToAddType) {
|
|
342
|
+
try {
|
|
343
|
+
const rawContent = await fs.readFile(filePath, 'utf-8');
|
|
344
|
+
const content = rawContent.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
345
|
+
const typeMap = { display: 'Display', action: 'Action', output: 'Output' };
|
|
346
|
+
const menuItemType = typeMap[menuItemToAddType?.toLowerCase()] ?? 'Display';
|
|
347
|
+
const newElement = `\t\t<AxMenuFunctionItem>\n` +
|
|
348
|
+
`\t\t\t<Name>${menuItemToAdd}</Name>\n` +
|
|
349
|
+
`\t\t\t<MenuItemName>${menuItemToAdd}</MenuItemName>\n` +
|
|
350
|
+
`\t\t\t<MenuItemType>${menuItemType}</MenuItemType>\n` +
|
|
351
|
+
`\t\t</AxMenuFunctionItem>`;
|
|
352
|
+
let updated;
|
|
353
|
+
if (content.includes('<Elements />')) {
|
|
354
|
+
updated = content.replace('<Elements />', `<Elements>\n${newElement}\n\t</Elements>`);
|
|
355
|
+
}
|
|
356
|
+
else if (content.includes('</Elements>')) {
|
|
357
|
+
updated = content.replace('</Elements>', `${newElement}\n\t</Elements>`);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
if (updated === content)
|
|
363
|
+
return null;
|
|
364
|
+
await fs.writeFile(filePath, normalizeD365Xml(updated), 'utf-8');
|
|
365
|
+
console.error(`[modify_d365fo_file] ✅ directXmlAddMenuItemToMenu: added '${menuItemToAdd}' to ${filePath}`);
|
|
366
|
+
return {
|
|
367
|
+
success: true,
|
|
368
|
+
message: `✅ Menu item '${menuItemToAdd}' (${menuItemType}) added via direct XML fallback. File: ${filePath}`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
console.error(`[modify_d365fo_file] directXmlAddMenuItemToMenu failed: ${err}`);
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* controlType (as passed to add-control) → the form control element emitted inside
|
|
378
|
+
* a form-extension's <FormControl>, together with its <Type> value. Verified against
|
|
379
|
+
* shipped standard form extensions (e.g. InventItemSampling.AdvancedQualityManagement).
|
|
380
|
+
* NOTE: an integer control is `AxFormIntegerControl` (Type=Integer) — NOT
|
|
381
|
+
* `AxFormIntControl`. Unknown types fall back to String, which is always valid.
|
|
382
|
+
*/
|
|
383
|
+
const CONTROL_TYPE_TO_FORM_CONTROL = {
|
|
384
|
+
string: { iType: 'AxFormStringControl', typeValue: 'String' },
|
|
385
|
+
integer: { iType: 'AxFormIntegerControl', typeValue: 'Integer' },
|
|
386
|
+
int: { iType: 'AxFormIntegerControl', typeValue: 'Integer' },
|
|
387
|
+
int64: { iType: 'AxFormInt64Control', typeValue: 'Int64' },
|
|
388
|
+
real: { iType: 'AxFormRealControl', typeValue: 'Real' },
|
|
389
|
+
date: { iType: 'AxFormDateControl', typeValue: 'Date' },
|
|
390
|
+
datetime: { iType: 'AxFormDateTimeControl', typeValue: 'DateTime' },
|
|
391
|
+
utcdatetime: { iType: 'AxFormDateTimeControl', typeValue: 'DateTime' },
|
|
392
|
+
time: { iType: 'AxFormTimeControl', typeValue: 'Time' },
|
|
393
|
+
guid: { iType: 'AxFormGuidControl', typeValue: 'Guid' },
|
|
394
|
+
checkbox: { iType: 'AxFormCheckBoxControl', typeValue: 'CheckBox' },
|
|
395
|
+
combobox: { iType: 'AxFormComboBoxControl', typeValue: 'ComboBox' },
|
|
396
|
+
button: { iType: 'AxFormButtonControl', typeValue: 'Button' },
|
|
397
|
+
group: { iType: 'AxFormGroupControl', typeValue: 'Group' },
|
|
398
|
+
};
|
|
399
|
+
const DEFAULT_FORM_CONTROL = { iType: 'AxFormStringControl', typeValue: 'String' };
|
|
400
|
+
/** Random 9-char lowercase-alphanumeric suffix, matching the SDK's
|
|
401
|
+
* `FormExtensionControl<rand>` wrapper-name convention (e.g. "fh5riowy1"). */
|
|
402
|
+
function formExtensionControlName() {
|
|
403
|
+
let s = '';
|
|
404
|
+
while (s.length < 9)
|
|
405
|
+
s += Math.random().toString(36).slice(2);
|
|
406
|
+
return `FormExtensionControl${s.slice(0, 9)}`;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Direct XML fallback for add-control on a form-extension.
|
|
410
|
+
*
|
|
411
|
+
* The C# bridge's AddControl resolves its target via _provider.Forms.Read(name),
|
|
412
|
+
* which can NEVER find a form EXTENSION (named "BaseForm.Suffix") — it always
|
|
413
|
+
* reports 'Form "<ext>" not found'. add-control on a form-extension therefore has
|
|
414
|
+
* no working bridge path at all (independent of metadata-root freshness). This
|
|
415
|
+
* writes an <AxFormExtensionControl> element straight into the extension's
|
|
416
|
+
* <Controls> collection, in the exact shape the D365FO SDK serializes (verified
|
|
417
|
+
* against shipped standard extensions): an empty-namespace <FormControl i:type="…">
|
|
418
|
+
* wrapped by <AxFormExtensionControl xmlns=""> with a <Parent> reference. It edits
|
|
419
|
+
* the file on disk, so it is unaffected by what the bridge has loaded.
|
|
420
|
+
*/
|
|
421
|
+
async function directXmlAddControl(filePath, controlName, parentControl, controlType, dataSource, dataField, label) {
|
|
422
|
+
try {
|
|
423
|
+
const rawContent = await fs.readFile(filePath, 'utf-8');
|
|
424
|
+
const content = rawContent.replace(/^/, '').replace(/\r\n/g, '\n');
|
|
425
|
+
// Idempotency: a control with this Name already present → skip.
|
|
426
|
+
// (controlName is a D365 identifier, so a literal substring match is safe.)
|
|
427
|
+
if (content.includes(`<Name>${controlName}</Name>`)) {
|
|
428
|
+
return {
|
|
429
|
+
success: true,
|
|
430
|
+
message: `✅ Control '${controlName}' already present in ${filePath} — skipped (idempotent).`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const { iType, typeValue } = CONTROL_TYPE_TO_FORM_CONTROL[(controlType || 'String').toLowerCase()] ?? DEFAULT_FORM_CONTROL;
|
|
434
|
+
// Inner <FormControl> children, in the order shipped extensions serialize them:
|
|
435
|
+
// Name → Type → FormControlExtension(nil) → DataField → DataSource → Label → [Items].
|
|
436
|
+
const inner = [
|
|
437
|
+
`\t\t\t\t<Name>${controlName}</Name>`,
|
|
438
|
+
`\t\t\t\t<Type>${typeValue}</Type>`,
|
|
439
|
+
`\t\t\t\t<FormControlExtension i:nil="true" />`,
|
|
440
|
+
];
|
|
441
|
+
if (dataField)
|
|
442
|
+
inner.push(`\t\t\t\t<DataField>${dataField}</DataField>`);
|
|
443
|
+
if (dataSource)
|
|
444
|
+
inner.push(`\t\t\t\t<DataSource>${dataSource}</DataSource>`);
|
|
445
|
+
if (label)
|
|
446
|
+
inner.push(`\t\t\t\t<Label>${label}</Label>`);
|
|
447
|
+
if (typeValue === 'ComboBox')
|
|
448
|
+
inner.push(`\t\t\t\t<Items />`);
|
|
449
|
+
const newElement = `\t\t<AxFormExtensionControl xmlns="">\n` +
|
|
450
|
+
`\t\t\t<Name>${formExtensionControlName()}</Name>\n` +
|
|
451
|
+
`\t\t\t<FormControl xmlns="" i:type="${iType}">\n` +
|
|
452
|
+
inner.join('\n') + '\n' +
|
|
453
|
+
`\t\t\t</FormControl>\n` +
|
|
454
|
+
`\t\t\t<Parent>${parentControl}</Parent>\n` +
|
|
455
|
+
`\t\t</AxFormExtensionControl>`;
|
|
456
|
+
let updated;
|
|
457
|
+
if (content.includes('<Controls />')) {
|
|
458
|
+
updated = content.replace('<Controls />', `<Controls>\n${newElement}\n\t</Controls>`);
|
|
459
|
+
}
|
|
460
|
+
else if (content.includes('</Controls>')) {
|
|
461
|
+
updated = content.replace('</Controls>', `${newElement}\n\t</Controls>`);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
return null; // no <Controls> collection — not a form-extension shape we recognise
|
|
465
|
+
}
|
|
466
|
+
if (updated === content)
|
|
467
|
+
return null;
|
|
468
|
+
await fs.writeFile(filePath, normalizeD365Xml(updated), 'utf-8');
|
|
469
|
+
console.error(`[modify_d365fo_file] ✅ directXmlAddControl: added '${controlName}' (${iType}) to ${filePath}`);
|
|
470
|
+
return {
|
|
471
|
+
success: true,
|
|
472
|
+
message: `✅ Control '${controlName}' (${iType}) added to '${parentControl}' via direct XML fallback. File: ${filePath}`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
console.error(`[modify_d365fo_file] directXmlAddControl failed: ${err}`);
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Heuristic: does a bridge failure message indicate the C# provider could not
|
|
482
|
+
* resolve the target object (vs. a genuine operation error like "index already
|
|
483
|
+
* exists")? An unresolved object is the one failure worth a refresh+retry,
|
|
484
|
+
* because an object created this session may not be in the provider's
|
|
485
|
+
* startup-fixed metadata roots yet.
|
|
486
|
+
*/
|
|
487
|
+
export function isUnresolvedObjectError(message) {
|
|
488
|
+
if (!message)
|
|
489
|
+
return false;
|
|
490
|
+
// Content/operation failures that merely contain "not found" are NOT object
|
|
491
|
+
// resolution and must not trigger the refresh+retry or the "could not resolve"
|
|
492
|
+
// guidance: replace-code's "oldCode not found in <obj>.<method>", a missing
|
|
493
|
+
// method/field/index, etc. The object was resolved — only the snippet/member wasn't.
|
|
494
|
+
if (/\b(oldcode|new ?code|code|snippet|method|field|index|relation|element)\b[^.]*\bnot found/i.test(message)) {
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
// Genuine object-resolution failures: a quoted object name "'X' not found",
|
|
498
|
+
// or the explicit resolve/model phrases the bridge emits.
|
|
499
|
+
return /'[^']+'\s+not found|could not resolve|does not exist|not in (the )?(metadata )?model|cannot determine model/i.test(message);
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Build the actionable "object could not be resolved" error for a modify
|
|
503
|
+
* operation. When `bridgeReported` is supplied it is included verbatim so the
|
|
504
|
+
* caller sees exactly what the C# bridge said rather than a generic guess.
|
|
505
|
+
*/
|
|
506
|
+
function unresolvedObjectError(operation, objectType, objectName, actualFilePath, bridgeReported) {
|
|
507
|
+
return (`Bridge operation '${operation}' could not resolve ${objectType} '${objectName}'.\n` +
|
|
508
|
+
(bridgeReported ? `Bridge reported: ${bridgeReported}\n` : '') +
|
|
509
|
+
`Most likely cause: the C# metadata bridge could not find '${objectName}' in its metadata model.\n` +
|
|
510
|
+
`This is typical right after creating an object in the same session — the bridge's metadata ` +
|
|
511
|
+
`roots are fixed at startup, so a file written this session may not be in its model.\n` +
|
|
512
|
+
`Auto-refresh was already attempted once and still failed.\n\n` +
|
|
513
|
+
`Reliable fallback (works for tables, forms, classes created this session):\n` +
|
|
514
|
+
` Read ${objectName}.xml with get_object_info, add the ${operation.replace('add-', '')} ` +
|
|
515
|
+
`element directly to the XML, then re-write the whole file:\n` +
|
|
516
|
+
` d365fo_file(action="create", overwrite=true, xmlContent="<complete updated XML>")\n\n` +
|
|
517
|
+
`Alternative — try in order:\n` +
|
|
518
|
+
` 1. update_symbol_index({ filePath: "<path to ${objectName}.xml>" })\n` +
|
|
519
|
+
` 2. Check D365FO_CUSTOM_PACKAGES_PATH points to the correct metadata folder.\n` +
|
|
520
|
+
` 3. Confirm ${objectName}.xml actually exists on disk.\n` +
|
|
521
|
+
(actualFilePath ? `Resolved file path: ${actualFilePath}\n` : '') +
|
|
522
|
+
`If none of these apply, the object may simply not exist or its name may be misspelled.`);
|
|
523
|
+
}
|
|
524
|
+
const ModifyD365FileArgsSchema = z.object({
|
|
525
|
+
objectType: z.enum([
|
|
526
|
+
'class', 'table', 'form', 'enum', 'query', 'view', 'edt', 'data-entity', 'report',
|
|
527
|
+
'table-extension', 'class-extension', 'form-extension', 'enum-extension', 'edt-extension',
|
|
528
|
+
'data-entity-extension',
|
|
529
|
+
'menu-item-display', 'menu-item-action', 'menu-item-output',
|
|
530
|
+
'menu-item-display-extension', 'menu-item-action-extension', 'menu-item-output-extension',
|
|
531
|
+
'menu', 'menu-extension',
|
|
532
|
+
'security-privilege', 'security-duty', 'security-role',
|
|
533
|
+
]).describe('Type of D365FO object'),
|
|
534
|
+
objectName: z.string().optional().describe('Name of the object to modify. Optional when filePath is provided — it is then ' +
|
|
535
|
+
'derived from the file basename (the bridge resolves objects by name, which the ' +
|
|
536
|
+
'file path already determines).'),
|
|
537
|
+
operation: z.enum([
|
|
538
|
+
'add-method', 'remove-method', 'replace-code',
|
|
539
|
+
'add-field', 'modify-field', 'rename-field', 'replace-all-fields', 'remove-field',
|
|
540
|
+
'add-index', 'remove-index',
|
|
541
|
+
'add-relation', 'remove-relation',
|
|
542
|
+
'add-field-group', 'remove-field-group', 'add-field-to-field-group',
|
|
543
|
+
'add-field-modification',
|
|
544
|
+
'add-data-source',
|
|
545
|
+
'modify-property',
|
|
546
|
+
'add-control',
|
|
547
|
+
'add-enum-value', 'modify-enum-value', 'remove-enum-value',
|
|
548
|
+
'add-display-method', 'add-table-method', 'add-menu-item-to-menu',
|
|
549
|
+
]).describe('Operation to perform. ' +
|
|
550
|
+
'replace-code REQUIRES parameters: oldCode (exact code to find) + newCode (replacement). ' +
|
|
551
|
+
'add-method REQUIRES: methodName + sourceCode. ' +
|
|
552
|
+
'add-table-method: pass tableMethodType (find/exist/findByRecId/validateWrite/validateDelete/initValue) ' +
|
|
553
|
+
'(+ tableKeyField for find/exist) to auto-generate the method, OR methodName + sourceCode for a custom one. ' +
|
|
554
|
+
'add-display-method: pass methodName + displayMethodReturnEdt to auto-generate a stub, OR methodName + sourceCode. ' +
|
|
555
|
+
'For form control override methods with replace-code, use methodName="ControlName.methodName" (e.g. "PostButton.clicked").'),
|
|
556
|
+
// For add-enum-value / modify-enum-value / remove-enum-value
|
|
557
|
+
enumValueName: z.string().optional().describe('Enum value name for add-enum-value / modify-enum-value / remove-enum-value. ' +
|
|
558
|
+
'E.g. "Approved", "Pending", "Rejected". For modify-enum-value this is the EXISTING ' +
|
|
559
|
+
'name used to locate the value — see enumValueNewName to rename it.'),
|
|
560
|
+
enumValueNewName: z.string().optional().describe('modify-enum-value ONLY: renames the enum value (its Name/identifier) from ' +
|
|
561
|
+
'enumValueName to this. Numeric value and label are unaffected unless ' +
|
|
562
|
+
'enumValueInt/enumValueLabel are also given.'),
|
|
563
|
+
enumValueLabel: z.string().optional().describe('Label reference for the enum value (e.g. "@MyModel:Approved"). ' +
|
|
564
|
+
'Used with add-enum-value and modify-enum-value.'),
|
|
565
|
+
enumValueHelpText: z.string().optional().describe('Help text reference for the enum value (e.g. "@MyModel:ApprovedHelp"). Optional.'),
|
|
566
|
+
enumValueInt: z.number().optional().describe('Explicit integer value for the enum value. ' +
|
|
567
|
+
'If omitted for add-enum-value, the next available value is assigned automatically. ' +
|
|
568
|
+
'Use with modify-enum-value to change the integer value (rare — may break existing data).'),
|
|
569
|
+
enumValueCountryRegionCodes: z.string().optional().describe('ISO country/region codes for the enum value, comma-separated (e.g. "CZ", "CZ,SK"). ' +
|
|
570
|
+
'Used with add-enum-value to restrict the value to specific locales.'),
|
|
571
|
+
// For add-display-method
|
|
572
|
+
displayMethodReturnEdt: z.string().optional().describe('EDT or type name the display method returns, e.g. "Name", "AmountMST", "SalesStatus". ' +
|
|
573
|
+
'Used with add-display-method to set the return type automatically.'),
|
|
574
|
+
// For add-table-method
|
|
575
|
+
tableMethodType: z.enum(['find', 'exist', 'findByRecId', 'validateWrite', 'validateDelete', 'initValue']).optional().describe('Standard table method pattern to generate. Used with add-table-method. ' +
|
|
576
|
+
'find: returns a single record by key field. exist: returns true/false. ' +
|
|
577
|
+
'findByRecId: returns record by RecId. validateWrite/validateDelete/initValue: standard overrides.'),
|
|
578
|
+
tableKeyField: z.string().optional().describe('Name of the primary key field for find/exist patterns (e.g. "ItemId", "SalesId"). ' +
|
|
579
|
+
'Used with add-table-method when tableMethodType is find or exist.'),
|
|
580
|
+
// For add-menu-item-to-menu
|
|
581
|
+
menuItemToAdd: z.string().optional().describe('Name of the menu item to add (e.g. "MyCustomForm"). Used with add-menu-item-to-menu.'),
|
|
582
|
+
menuItemToAddType: z.enum(['display', 'action', 'output']).optional().describe('Type of menu item to add: display (form), action (class), output (report). ' +
|
|
583
|
+
'Used with add-menu-item-to-menu. Defaults to display.'),
|
|
584
|
+
// For add-control (form-extension only)
|
|
585
|
+
controlName: z.string().optional().describe('Name of the new form control to add inside the form extension. ' +
|
|
586
|
+
'e.g. "MyCustPriorityTier". Used as <Name> inside <FormControl>.'),
|
|
587
|
+
parentControl: z.string().optional().describe('Name of the existing parent control/tab/group in the base form to insert into. ' +
|
|
588
|
+
'e.g. "TabGeneral", "HeaderGroup", "TabPageSales". ' +
|
|
589
|
+
'Becomes the <Parent> element of the AxFormExtensionControl wrapper.'),
|
|
590
|
+
controlDataSource: z.string().optional().describe('Data source name for the new control binding (e.g. "CustTable"). ' +
|
|
591
|
+
'Required when controlDataField is provided.'),
|
|
592
|
+
controlDataField: z.string().optional().describe('Data field name for the new control binding (e.g. "MyCustPriorityTier"). ' +
|
|
593
|
+
'The field must already exist in the table (extension) before adding the UI control.'),
|
|
594
|
+
controlType: z.string().optional().describe('Form control type (default: String). Determines i:type and <Type> in the XML. ' +
|
|
595
|
+
'Supported values: String, Integer, Real, CheckBox, ComboBox, Date, DateTime, Int64, Group, Button, CommandButton, MenuFunctionButton. ' +
|
|
596
|
+
'Use CheckBox for NoYes/boolean fields. Use ComboBox for enum fields. ' +
|
|
597
|
+
'If omitted the tool auto-picks based on the EDT base type if controlDataField is provided.'),
|
|
598
|
+
controlLabel: z.string().optional().describe('Optional label for the new control (add-control). Becomes the control <Label>.'),
|
|
599
|
+
positionType: z.string().optional().describe('Optional positioning: AfterItem | BeforeItem. Omit to append at the end of the parent.'),
|
|
600
|
+
previousSibling: z.string().optional().describe('Name of the sibling control to position after (used with positionType=AfterItem).'),
|
|
601
|
+
baseFormName: z.string().optional().describe('Base form name used for auto-resolving parentControl when the extension name does not contain it. ' +
|
|
602
|
+
'E.g. if objectName="SalesOrder.MyExt" the base form is auto-detected as "SalesOrder". ' +
|
|
603
|
+
'Pass this only when auto-detection fails (e.g. the extension has a non-standard name).'),
|
|
604
|
+
// For add-method
|
|
605
|
+
methodName: z.string().optional().describe('Name of method to add/remove'),
|
|
606
|
+
methodCode: z.string().optional().describe('X++ code for the method — either the FULL source (access modifiers + return type + name + params + body) ' +
|
|
607
|
+
'or just the method body. When the full source is provided (first real code line contains an access ' +
|
|
608
|
+
'modifier and the method name followed by "("), it is used as-is. When only a body is provided, ' +
|
|
609
|
+
'the signature is assembled from methodModifiers, methodReturnType, methodName, and methodParameters. ' +
|
|
610
|
+
'Alias: sourceCode (preferred when passing a complete CoC skeleton or full method source).'),
|
|
611
|
+
sourceCode: z.string().optional().describe('Alias for methodCode — pass the FULL X++ method source including access modifiers, return type, ' +
|
|
612
|
+
'method name, parameters, attributes (e.g. [ExtensionOf(...)]), and body. ' +
|
|
613
|
+
'This is the preferred parameter when passing a complete CoC skeleton. ' +
|
|
614
|
+
'Either methodCode or sourceCode may be used; sourceCode takes precedence if both are supplied.'),
|
|
615
|
+
// For replace-code (REQUIRED for operation="replace-code" — do NOT use sourceCode for this)
|
|
616
|
+
oldCode: z.string().optional().describe('REQUIRED for replace-code. Exact existing X++ code snippet to find and replace. ' +
|
|
617
|
+
'Must match the source text exactly (leading/trailing whitespace is trimmed for matching). ' +
|
|
618
|
+
'If methodName is also provided the search is scoped to that method\'s Source block only. ' +
|
|
619
|
+
'For form control override methods, use methodName="ControlName.methodName" (e.g. "PostButton.clicked").'),
|
|
620
|
+
newCode: z.string().optional().describe('REQUIRED for replace-code. Replacement X++ code snippet. ' +
|
|
621
|
+
'Replaces the first occurrence of oldCode in the target source block. ' +
|
|
622
|
+
'Pass empty string "" to delete the matched oldCode snippet.'),
|
|
623
|
+
methodModifiers: z.string().optional().describe('Method modifiers (e.g., "public static")'),
|
|
624
|
+
methodReturnType: z.string().optional().describe('Return type of method'),
|
|
625
|
+
methodParameters: z.string().optional().describe('Method parameters (e.g., "str _param1, int _param2")'),
|
|
626
|
+
// For add-field / modify-field (tables)
|
|
627
|
+
fieldName: z.string().optional().describe('Name of field to add/remove/modify/rename'),
|
|
628
|
+
fieldNewName: z.string().optional().describe('New name for the field (required for rename-field operation)'),
|
|
629
|
+
fieldType: z.string().optional().describe('EDT name for the field (for add-field: required — pass the EDT name, e.g. "InventQty", "WHSZoneId"). For modify-field: new EDT to set.'),
|
|
630
|
+
fieldBaseType: z.string().optional().describe('Base type that determines the XML element for add-field: String | Integer | Real | Date | DateTime | Int64 | GUID | Enum. ' +
|
|
631
|
+
'REQUIRED when fieldType is an EDT — pass the EDT base type so the correct AxTableFieldReal/AxTableFieldDate/… is used. ' +
|
|
632
|
+
'Examples: fieldType="InventQty" fieldBaseType="Real"; fieldType="TransDate" fieldBaseType="Date"; fieldType="ItemId" fieldBaseType="String". ' +
|
|
633
|
+
'Without this, all EDT fields default to AxTableFieldString which is WRONG for numeric/date types.'),
|
|
634
|
+
fieldMandatory: z.boolean().optional().describe('Is field mandatory'),
|
|
635
|
+
fieldLabel: z.string().optional().describe('Field label'),
|
|
636
|
+
fieldHelpText: z.string().optional().describe('Field help text (modify-field).'),
|
|
637
|
+
fieldEnumType: z.string().optional().describe('Enum name to set on the field (modify-field, for enum-typed fields).'),
|
|
638
|
+
fieldStringSize: z.string().optional().describe('String size to set on the field (modify-field, for string-typed fields).'),
|
|
639
|
+
fields: z.array(z.object({
|
|
640
|
+
name: z.string(),
|
|
641
|
+
edt: z.string().optional(),
|
|
642
|
+
type: z.string().optional().describe('Base type for the XML element: String|Real|Integer|Date|DateTime|Int64|GUID|Enum. REQUIRED when edt is an EDT name — without it defaults to AxTableFieldString!'),
|
|
643
|
+
mandatory: z.boolean().optional(),
|
|
644
|
+
label: z.string().optional(),
|
|
645
|
+
})).optional().describe('Full list of fields for replace-all-fields operation. Each item: { name, edt?, type?, mandatory?, label? }. ' +
|
|
646
|
+
'IMPORTANT: always pass type= the base type (String/Real/Integer/Date/DateTime/Int64/GUID) alongside edt= so the correct XML element is used. ' +
|
|
647
|
+
'Example: { name: "TransQty", edt: "InventQty", type: "Real" }. ' +
|
|
648
|
+
'All existing fields are replaced atomically.'),
|
|
649
|
+
// For add-index / remove-index (table, table-extension)
|
|
650
|
+
indexName: z.string().optional().describe('Index name for add-index / remove-index.'),
|
|
651
|
+
indexFields: z.array(z.object({
|
|
652
|
+
fieldName: z.string(),
|
|
653
|
+
direction: z.enum(['Asc', 'Desc']).optional(),
|
|
654
|
+
})).optional().describe('Fields that make up the index. Required for add-index.'),
|
|
655
|
+
indexAllowDuplicates: z.boolean().optional().describe('Whether index allows duplicates (default: false = unique).'),
|
|
656
|
+
indexAlternateKey: z.boolean().optional().describe('Whether index is an alternate key.'),
|
|
657
|
+
indexEnabled: z.boolean().optional().describe('Whether index is enabled (default: true).'),
|
|
658
|
+
// For add-relation / remove-relation (table, table-extension)
|
|
659
|
+
relationName: z.string().optional().describe('Relation name for add-relation / remove-relation.'),
|
|
660
|
+
relatedTable: z.string().optional().describe('Name of the related (foreign key) table.'),
|
|
661
|
+
relationConstraints: z.array(z.object({
|
|
662
|
+
fieldName: z.string().describe('Local field name.'),
|
|
663
|
+
relatedFieldName: z.string().describe('Field name in the related table.'),
|
|
664
|
+
})).optional().describe('Field constraints for the relation (field = relatedField pairs).'),
|
|
665
|
+
relationCardinality: z.string().optional().describe('Cardinality on local side: ZeroMore | ZeroOne | ExactlyOne (default: ZeroMore).'),
|
|
666
|
+
relatedTableCardinality: z.string().optional().describe('Cardinality on related side: ZeroMore | ZeroOne | ExactlyOne (default: ExactlyOne).'),
|
|
667
|
+
relationshipType: z.string().optional().describe('Relationship type: Association | Composition | Aggregation | Link | Specialization (default: Association).'),
|
|
668
|
+
// For add-field-group / remove-field-group / add-field-to-field-group (table, table-extension)
|
|
669
|
+
fieldGroupName: z.string().optional().describe('Field group name. For add-field-to-field-group in a table-extension: name of the group (new or existing base-table group).'),
|
|
670
|
+
fieldGroupFields: z.array(z.string()).optional().describe('Initial field names for add-field-group. Can be empty — add fields later with add-field-to-field-group.'),
|
|
671
|
+
fieldGroupLabel: z.string().optional().describe('Label for add-field-group (optional).'),
|
|
672
|
+
extendBaseFieldGroup: z.boolean().optional().describe('Only for table-extension add-field-to-field-group: when true, adds the field to <FieldGroupExtensions> ' +
|
|
673
|
+
'(extending an existing base-table field group). When false/omitted, adds to <FieldGroups> (a new group defined in the extension).'),
|
|
674
|
+
// For add-field-modification (table-extension only)
|
|
675
|
+
// uses fieldName, fieldLabel, fieldMandatory (already defined above)
|
|
676
|
+
// For add-data-source (form-extension)
|
|
677
|
+
dataSourceName: z.string().optional().describe('Data source reference name for add-data-source (e.g. "MyTable_1").'),
|
|
678
|
+
dataSourceTable: z.string().optional().describe('Base table name for add-data-source (e.g. "MyTable").'),
|
|
679
|
+
joinSource: z.string().optional().describe('Optional name of an existing data source on the form to join the new data source to (add-data-source).'),
|
|
680
|
+
linkType: z.string().optional().describe('Optional join/link type when joinSource is set (add-data-source): InnerJoin | OuterJoin | ExistJoin | NotExistJoin | Delayed | Active | Passive.'),
|
|
681
|
+
// For modify-property
|
|
682
|
+
propertyPath: z.string().optional().describe('Property name to set. ' +
|
|
683
|
+
'For tables (AxTable): TableGroup, TitleField1, TitleField2, TableType (TempDB/RegularTable/InMemory), ' +
|
|
684
|
+
'CacheLookup, ClusteredIndex, PrimaryIndex, SaveDataPerCompany, Label, HelpText, Extends, SystemTable. ' +
|
|
685
|
+
'For table-extensions (AxTableExtension): properties are stored inside <PropertyModifications> as ' +
|
|
686
|
+
'<AxPropertyModification> entries. Supported: Label, HelpText, TableGroup, CacheLookup, TitleField1, TitleField2, ' +
|
|
687
|
+
'ClusteredIndex, PrimaryIndex, SaveDataPerCompany, TableType, SystemTable, ' +
|
|
688
|
+
'ModifiedDateTime (Yes/No), CreatedDateTime (Yes/No), ModifiedBy (Yes/No), CreatedBy (Yes/No), ' +
|
|
689
|
+
'CountryRegionCodes (comma-separated, e.g. "CZ,SK"). ' +
|
|
690
|
+
'For EDTs: Extends, StringSize, Label, HelpText, ReferenceTable, ReferenceField. ' +
|
|
691
|
+
'For classes: Extends, Abstract, Final, Label. ' +
|
|
692
|
+
'For nested properties use dot notation, e.g. "Fields.AxTableField.Name" (rare). ' +
|
|
693
|
+
'Examples: propertyPath="TableGroup" propertyValue="Group"; propertyPath="TitleField1" propertyValue="ItemId"; ' +
|
|
694
|
+
'propertyPath="TableType" propertyValue="TempDB"; propertyPath="Extends" propertyValue="WHSZoneId"; ' +
|
|
695
|
+
'propertyPath="ModifiedDateTime" propertyValue="Yes" (table-extension); ' +
|
|
696
|
+
'propertyPath="CountryRegionCodes" propertyValue="CZ,SK" (table-extension)'),
|
|
697
|
+
propertyValue: z.string().optional().describe('New property value'),
|
|
698
|
+
// Options
|
|
699
|
+
createBackup: z.boolean().optional().default(false).describe('Create a .bak backup of the file before modifying it (default: false). Changes can also be reverted with undo_last_modification (git checkout) without a backup. When the file is NOT inside a git repository, a backup is created automatically even with false, since undo_last_modification would not work there.'),
|
|
700
|
+
modelName: z.string().optional().describe('Model name (auto-detected if not provided). Pass this if the file was just created and is not yet indexed.'),
|
|
701
|
+
packageName: z.string().optional().describe('Package name. Auto-resolved if omitted.'),
|
|
702
|
+
packagePath: z.string().optional().describe('Root packages path to search when the object lives outside the default PackagesLocalDirectory ' +
|
|
703
|
+
'(e.g. a repo metadata checkout like "K:\\\\repos\\\\…\\\\metadata"). Used to locate the file for ' +
|
|
704
|
+
'backup and the direct-XML fallback. NOTE: bridge-backed operations (add-method, add-field, etc.) ' +
|
|
705
|
+
'resolve objects via the C# bridge, which is configured with fixed roots at startup — if the model ' +
|
|
706
|
+
'is not under the bridge\'s roots, set context.customPackagesPath / D365FO_CUSTOM_PACKAGES_PATH so ' +
|
|
707
|
+
'the bridge picks it up, or pass an explicit filePath.'),
|
|
708
|
+
workspacePath: z.string().optional().describe('Path to workspace for finding file'),
|
|
709
|
+
filePath: z.string().optional().describe('Absolute path to the XML file. Use this when the object was just created and the path is already known ' +
|
|
710
|
+
'(e.g. from d365fo_file(action="create") output). Bypasses symbol DB lookup entirely.'),
|
|
711
|
+
addToProject: z.boolean().optional().default(false).describe('When true, adds the modified file to the Visual Studio project (.rnrproj). ' +
|
|
712
|
+
'Use this when the file exists on disk but is not yet tracked in the VS project. ' +
|
|
713
|
+
'Requires projectPath or solutionPath (explicit or via .mcp.json). Default: false.'),
|
|
714
|
+
projectPath: z.string().optional().describe('Path to .rnrproj file. Required for addToProject to work. Auto-detected from .mcp.json if omitted.'),
|
|
715
|
+
solutionPath: z.string().optional().describe('Path to VS solution directory. Used to find .rnrproj when projectPath is not given.'),
|
|
716
|
+
groundingToken: z.string().optional().describe('Provenance token returned by prepare(mode="change"). Proves the change was grounded in the indexed codebase. ' +
|
|
717
|
+
'Required for *-extension objectTypes when GROUNDING_ENFORCE=true on the server.'),
|
|
718
|
+
});
|
|
719
|
+
export async function modifyD365FileTool(request, context) {
|
|
720
|
+
try {
|
|
721
|
+
const args = ModifyD365FileArgsSchema.parse(request.params.arguments);
|
|
722
|
+
// Decode XML entities in X++ payloads. An AI that copied entity-encoded code (an
|
|
723
|
+
// SSRS <Text> block, or escaped doc comments like "/// <summary>") sends
|
|
724
|
+
// </>/&/"/'; the serializer would otherwise write them literally
|
|
725
|
+
// into the source. No-op for clean X++ — literal <, >, && and " are not entities.
|
|
726
|
+
if (args.sourceCode)
|
|
727
|
+
args.sourceCode = decodeXmlEntitiesFromXppSource(args.sourceCode);
|
|
728
|
+
if (args.methodCode)
|
|
729
|
+
args.methodCode = decodeXmlEntitiesFromXppSource(args.methodCode);
|
|
730
|
+
if (args.newCode)
|
|
731
|
+
args.newCode = decodeXmlEntitiesFromXppSource(args.newCode);
|
|
732
|
+
if (args.oldCode)
|
|
733
|
+
args.oldCode = decodeXmlEntitiesFromXppSource(args.oldCode);
|
|
734
|
+
// objectName is optional when filePath is given — derive it from the file
|
|
735
|
+
// basename using path.win32.basename (handles Windows backslash paths on
|
|
736
|
+
// all platforms — path.posix.basename treats '\\' as a regular character).
|
|
737
|
+
if (!args.objectName) {
|
|
738
|
+
if (args.filePath) {
|
|
739
|
+
args.objectName = path.win32.basename(args.filePath, '.xml');
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
return {
|
|
743
|
+
content: [{
|
|
744
|
+
type: 'text',
|
|
745
|
+
text: "❌ Provide 'objectName' — or 'filePath', from which the object name is derived.",
|
|
746
|
+
}],
|
|
747
|
+
isError: true,
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// Grounding enforcement: modifying an extension changes the behaviour of an
|
|
752
|
+
// existing base object — when GROUNDING_ENFORCE=true the model must prove
|
|
753
|
+
// (via prepare_change) that it inspected the real object first.
|
|
754
|
+
if (args.objectType.endsWith('-extension')) {
|
|
755
|
+
const groundingError = enforceGrounding(args.groundingToken, `d365fo_file(action="modify", objectType="${args.objectType}", objectName="${args.objectName}", operation="${args.operation}")`, args.objectName);
|
|
756
|
+
if (groundingError)
|
|
757
|
+
return groundingError;
|
|
758
|
+
}
|
|
759
|
+
// Semantic reference gate: when GROUNDING_ENFORCE=true, every identifier in
|
|
760
|
+
// X++ source about to be written must be proven against the symbol index.
|
|
761
|
+
const xppToWrite = args.sourceCode ?? args.methodCode ?? args.newCode;
|
|
762
|
+
const referenceError = gateOnReferenceErrors(xppToWrite, context.symbolIndex, `d365fo_file(action="modify", objectType="${args.objectType}", objectName="${args.objectName}", operation="${args.operation}")`);
|
|
763
|
+
if (referenceError)
|
|
764
|
+
return referenceError;
|
|
765
|
+
// CDATA-corruption guard: any X++ payload that will be CDATA-wrapped by the
|
|
766
|
+
// serializer (add-method's source) or spliced into an existing CDATA block
|
|
767
|
+
// (replace-code's newCode) must be clean X++ — never a slice of .xml markup.
|
|
768
|
+
// A "]]>" or stray <Method>/<Source> in the payload otherwise survives into
|
|
769
|
+
// the file and produces the invalid doubled-"]]>" / dropped-</Method> shape.
|
|
770
|
+
assertCleanXppSource(args.sourceCode, 'sourceCode');
|
|
771
|
+
assertCleanXppSource(args.methodCode, 'methodCode');
|
|
772
|
+
assertCleanXppSource(args.newCode, 'newCode');
|
|
773
|
+
// add-method emits exactly one <Method>; reject a payload carrying multiple
|
|
774
|
+
// methods (the extras would land outside the class → invalid X++). Only for the
|
|
775
|
+
// method-adding operations — replace-code's newCode is a snippet, not a method.
|
|
776
|
+
if (['add-method', 'add-display-method', 'add-table-method'].includes(args.operation)) {
|
|
777
|
+
const methodSrc = args.sourceCode ?? args.methodCode;
|
|
778
|
+
// add-method may carry several methods — they are split and added one <Method>
|
|
779
|
+
// at a time below. add-display-method / add-table-method generate a single
|
|
780
|
+
// method, so multiple bodies there are still a mistake.
|
|
781
|
+
if (args.operation !== 'add-method') {
|
|
782
|
+
assertSingleMethodSource(methodSrc);
|
|
783
|
+
}
|
|
784
|
+
// Derive methodName from the source signature when omitted — the full method
|
|
785
|
+
// source already contains the name (e.g. "public static X find(...)" → "find").
|
|
786
|
+
// Skip derivation when the payload holds multiple methods (handled per-method).
|
|
787
|
+
if (!args.methodName && countTopLevelMethodBodies(methodSrc ?? '') <= 1) {
|
|
788
|
+
const derived = extractMethodNameFromSource(methodSrc);
|
|
789
|
+
if (derived) {
|
|
790
|
+
args.methodName = derived;
|
|
791
|
+
console.error(`[modify_d365fo_file] methodName omitted — derived '${derived}' from the source signature`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
const { symbolIndex } = context;
|
|
796
|
+
let { objectType, operation, createBackup, modelName, workspacePath, filePath: explicitFilePath, } = args;
|
|
797
|
+
// objectName guaranteed non-null by the derivation block above.
|
|
798
|
+
// Declared as `let` so it can be corrected from the resolved file basename below.
|
|
799
|
+
let objectName = args.objectName;
|
|
800
|
+
// ── Auto-resolve parentControl for add-control on form-extension ─────────
|
|
801
|
+
// When `parentControl` is a fuzzy / lowercase string (e.g. "general"), look
|
|
802
|
+
// up the base form XML, walk the control tree, and resolve to the exact name.
|
|
803
|
+
// This makes add-control seamless — no prior get_object_info(form) call required.
|
|
804
|
+
let addControlNote = '';
|
|
805
|
+
let generationNote = '';
|
|
806
|
+
if (operation === 'add-control' && objectType === 'form-extension' && args.parentControl) {
|
|
807
|
+
const resolution = await resolveParentControl(objectName, args.parentControl, symbolIndex, args.baseFormName);
|
|
808
|
+
if (resolution && 'multiple' in resolution) {
|
|
809
|
+
const candidateList = resolution.multiple
|
|
810
|
+
.slice(0, 20)
|
|
811
|
+
.map(c => ` • \`${c.name}\`` +
|
|
812
|
+
(c.parentName ? ` (parent: \`${c.parentName}\`)` : '') +
|
|
813
|
+
` — path: ${c.pathStr}`)
|
|
814
|
+
.join('\n');
|
|
815
|
+
return {
|
|
816
|
+
content: [{
|
|
817
|
+
type: 'text',
|
|
818
|
+
text: `⚠️ **Ambiguous parentControl** — "${args.parentControl}" matches multiple controls in the base form.\n\n` +
|
|
819
|
+
`**Candidates** (${resolution.multiple.length}):\n${candidateList}\n\n` +
|
|
820
|
+
`Re-call \`add-control\` with the exact \`parentControl\` name from the list above.`,
|
|
821
|
+
}],
|
|
822
|
+
isError: true,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
if (resolution && 'resolved' in resolution) {
|
|
826
|
+
if (resolution.resolved !== args.parentControl) {
|
|
827
|
+
addControlNote = `\n\n> 🔍 **parentControl** auto-resolved: \`"${args.parentControl}"\` → \`"${resolution.resolved}"\` (${resolution.pathStr})`;
|
|
828
|
+
}
|
|
829
|
+
else {
|
|
830
|
+
addControlNote = `\n\n> ✅ **parentControl** \`"${resolution.resolved}"\` confirmed in base form (${resolution.pathStr})`;
|
|
831
|
+
}
|
|
832
|
+
args.parentControl = resolution.resolved;
|
|
833
|
+
}
|
|
834
|
+
// null → form not found or no match; proceed with original value (compiler will catch it)
|
|
835
|
+
}
|
|
836
|
+
// ── Form-pattern pre-flight for add-control ───────────────────────────────
|
|
837
|
+
// When the parent container declares a sub-pattern (e.g. FieldsFieldGroups),
|
|
838
|
+
// verify the new control's type is allowed there. Blocking when
|
|
839
|
+
// FORM_PATTERN_ENFORCE is enabled (default); advisory note otherwise.
|
|
840
|
+
if (operation === 'add-control' &&
|
|
841
|
+
(objectType === 'form' || objectType === 'form-extension') &&
|
|
842
|
+
args.parentControl &&
|
|
843
|
+
args.controlType) {
|
|
844
|
+
const baseFormName = objectType === 'form'
|
|
845
|
+
? objectName
|
|
846
|
+
: (args.baseFormName || objectName.split('.')[0]);
|
|
847
|
+
const baseXml = await findBaseFormXml(baseFormName, symbolIndex);
|
|
848
|
+
if (baseXml) {
|
|
849
|
+
const verdict = await checkAddControlAgainstParentPattern(baseXml, args.parentControl, args.controlType);
|
|
850
|
+
if (verdict && !verdict.allowed) {
|
|
851
|
+
const allowedList = verdict.allowedTypes === 'any' ? 'any' : verdict.allowedTypes.join(', ');
|
|
852
|
+
if (isFormPatternEnforceEnabled()) {
|
|
853
|
+
return {
|
|
854
|
+
content: [{
|
|
855
|
+
type: 'text',
|
|
856
|
+
text: `⛔ add-control blocked — parent control "${args.parentControl}" follows sub-pattern ` +
|
|
857
|
+
`**${verdict.parentPattern}**, which does not allow a "${args.controlType}" child.\n\n` +
|
|
858
|
+
`Allowed control types here: ${allowedList}.\n\n` +
|
|
859
|
+
`Options:\n` +
|
|
860
|
+
` 1. Use an allowed control type (e.g. controlType="String" for a bound field).\n` +
|
|
861
|
+
` 2. Target a different parent container (use get_object_info(objectType="form", name=...) to inspect the hierarchy).\n` +
|
|
862
|
+
` 3. Set FORM_PATTERN_ENFORCE=false to bypass pattern enforcement.`,
|
|
863
|
+
}],
|
|
864
|
+
isError: true,
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
addControlNote +=
|
|
868
|
+
`\n\n> ⚠️ Pattern warning: parent "${args.parentControl}" follows ${verdict.parentPattern}, ` +
|
|
869
|
+
`which does not allow "${args.controlType}" children (allowed: ${allowedList}). ` +
|
|
870
|
+
`FORM_PATTERN_ENFORCE is disabled — proceeding anyway.`;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
// 1. Find the file
|
|
875
|
+
const filePath = await findD365File(symbolIndex, objectType, objectName, modelName, workspacePath, explicitFilePath, args.packagePath);
|
|
876
|
+
if (!filePath) {
|
|
877
|
+
throw new Error(`File not found for ${objectType} "${objectName}".\n\n` +
|
|
878
|
+
`Retry options (do NOT use PowerShell — this tool can handle it):\n` +
|
|
879
|
+
` 1. Pass modelName="<YourModel>" — triggers filesystem lookup by path.\n` +
|
|
880
|
+
` 2. Pass packagePath="<root that contains the model>" if the metadata lives outside the default PackagesLocalDirectory (e.g. a repo checkout).\n` +
|
|
881
|
+
` 3. Pass filePath="K:\\\\AosService\\\\PackagesLocalDirectory\\\\<pkg>\\\\<model>\\\\${objectName}.xml" — bypasses all lookup.\n` +
|
|
882
|
+
` 4. If the object was just created, re-run d365fo_file(action="create") first and use the returned path as filePath.\n\n` +
|
|
883
|
+
`If the file exists but bridge operations still fail to resolve the object, the C# bridge's metadata roots ` +
|
|
884
|
+
`(fixed at startup) likely don't include this model — set context.customPackagesPath / D365FO_CUSTOM_PACKAGES_PATH and restart the server.`);
|
|
885
|
+
}
|
|
886
|
+
// 1a. Path containment guard — every write target must live under a configured
|
|
887
|
+
// <PackagesLocalDirectory>/<Package>/<Model>/Ax<Type>/<File>.xml layout.
|
|
888
|
+
// Refuses path traversal via explicit filePath or JSON sourcePath (security-critical).
|
|
889
|
+
// A caller-supplied packagePath (metadata outside PLD, e.g. a repo checkout) is
|
|
890
|
+
// honored as an allowed root — findD365File already resolves against it, so the
|
|
891
|
+
// containment list must include it too or valid writes get wrongly rejected.
|
|
892
|
+
const extraRoots = args.packagePath ? [args.packagePath] : undefined;
|
|
893
|
+
const containment = await assertWritePathAllowed(filePath, modelName, { extraRoots });
|
|
894
|
+
if (!containment.ok) {
|
|
895
|
+
throw new Error(containment.reason || 'Path containment check failed');
|
|
896
|
+
}
|
|
897
|
+
// 1b. Model-ownership guard: refuse to modify objects in standard Microsoft models.
|
|
898
|
+
// This prevents accidental writes to ApplicationSuite, ApplicationFoundation, etc.
|
|
899
|
+
const resolvedModelFromPath = extractModelFromFilePath(filePath);
|
|
900
|
+
if (resolvedModelFromPath && isStandardModel(resolvedModelFromPath)) {
|
|
901
|
+
const configManager = getConfigManager();
|
|
902
|
+
const configuredModel = modelName || configManager.getModelName();
|
|
903
|
+
// Only block if the resolved model differs from the user's explicitly configured model.
|
|
904
|
+
// If user explicitly set modelName=ApplicationSuite, they know what they're doing.
|
|
905
|
+
if (!modelName || modelName !== resolvedModelFromPath) {
|
|
906
|
+
throw new Error(`⛔ Refusing to modify "${objectName}" — the resolved file belongs to standard Microsoft model "${resolvedModelFromPath}".\n\n` +
|
|
907
|
+
`Your configured model is "${configuredModel || '(not set)'}".\n` +
|
|
908
|
+
`Modifying standard objects is not permitted — it can corrupt the base application.\n\n` +
|
|
909
|
+
`To extend a standard object, create an extension instead:\n` +
|
|
910
|
+
` • Table: d365fo_file(action="create", objectType="table-extension", objectName="${objectName}.${configuredModel || 'YourModel'}Extension")\n` +
|
|
911
|
+
` • Class: d365fo_file(action="create", objectType="class-extension", objectName="${objectName}_Extension")\n` +
|
|
912
|
+
` • Form: d365fo_file(action="create", objectType="form-extension", objectName="${objectName}.${configuredModel || 'YourModel'}Extension")`);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
// 2. Resolve actual XML file path (DB may store JSON metadata with sourcePath)
|
|
916
|
+
let actualFilePath = filePath;
|
|
917
|
+
try {
|
|
918
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
919
|
+
const trimmed = fileContent.trimStart();
|
|
920
|
+
if (trimmed.startsWith('{')) {
|
|
921
|
+
const data = JSON.parse(fileContent);
|
|
922
|
+
if (data.sourcePath) {
|
|
923
|
+
// Re-validate the indirect path: sourcePath also comes from user-influenced data.
|
|
924
|
+
const srcContainment = await assertWritePathAllowed(data.sourcePath, modelName, { extraRoots });
|
|
925
|
+
if (!srcContainment.ok) {
|
|
926
|
+
throw new Error(`sourcePath rejected: ${srcContainment.reason}`);
|
|
927
|
+
}
|
|
928
|
+
actualFilePath = srcContainment.canonicalPath || data.sourcePath;
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
throw new Error(`Metadata file has no sourcePath: ${filePath}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
catch (readError) {
|
|
936
|
+
if (readError instanceof SyntaxError || (readError instanceof Error && readError.message.includes('sourcePath'))) {
|
|
937
|
+
throw readError;
|
|
938
|
+
}
|
|
939
|
+
const isRelative = !path.isAbsolute(filePath);
|
|
940
|
+
const hint = isRelative
|
|
941
|
+
? ' The path is relative — the symbol DB returned a build-agent path. ' +
|
|
942
|
+
'Pass filePath="<absolute path>" or modelName="<YourModel>" so the tool can locate the file on disk.'
|
|
943
|
+
: '';
|
|
944
|
+
throw new Error(`Cannot read file: ${filePath}${hint}`);
|
|
945
|
+
}
|
|
946
|
+
// 3. Create backup of the actual XML file. When the target is NOT inside a
|
|
947
|
+
// git work tree, the documented undo path (undo_last_modification →
|
|
948
|
+
// git checkout) cannot revert the change — force a backup even with
|
|
949
|
+
// createBackup=false so a bad modify is never unrecoverable.
|
|
950
|
+
const backupNote = await ensureRecoverableModification(actualFilePath, createBackup);
|
|
951
|
+
// 3b. Derive the authoritative object name from the resolved file path.
|
|
952
|
+
// The caller may pass objectName="RentEquipment" while the file on disk
|
|
953
|
+
// is ContosoRentEquipment.xml (auto-prefixed at create time). The C# bridge
|
|
954
|
+
// resolves objects by name from its metadata model — if the name doesn't
|
|
955
|
+
// match the file it will always return null, regardless of refreshes.
|
|
956
|
+
// Use path.win32.basename so Windows backslash paths are handled correctly
|
|
957
|
+
// on all platforms (path.posix.basename treats '\\' as a regular character).
|
|
958
|
+
const fileBaseName = path.win32.basename(actualFilePath, '.xml');
|
|
959
|
+
const bridgeObjectName = fileBaseName !== objectName ? fileBaseName : objectName;
|
|
960
|
+
if (bridgeObjectName !== objectName) {
|
|
961
|
+
console.error(`[modify_d365fo_file] ℹ️ objectName "${objectName}" → resolved to "${bridgeObjectName}" from file basename`);
|
|
962
|
+
objectName = bridgeObjectName;
|
|
963
|
+
}
|
|
964
|
+
// ── Bridge-only modify via IMetadataProvider.Update() ────────────────────
|
|
965
|
+
// ALL modify operations go through the C# bridge. The bridge reads, modifies,
|
|
966
|
+
// and writes via the official D365FO metadata API — no xml2js needed.
|
|
967
|
+
// If the bridge is unavailable or fails, we throw an error (no fallback).
|
|
968
|
+
if (!context?.bridge) {
|
|
969
|
+
throw new Error('C# metadata bridge is not available. The bridge is required for all modify operations.\n' +
|
|
970
|
+
'Start the bridge by building the D365MetadataBridge project and restarting the MCP server.');
|
|
971
|
+
}
|
|
972
|
+
if (!canBridgeModify(objectType, operation)) {
|
|
973
|
+
throw new Error(`Operation '${operation}' on object type '${objectType}' is not supported by the bridge.`);
|
|
974
|
+
}
|
|
975
|
+
let bridgeResult = null;
|
|
976
|
+
let _bridgeRetried = false;
|
|
977
|
+
// Retry loop: on the first null result with all required params present,
|
|
978
|
+
// refresh the bridge provider (picks up objects created this session) and
|
|
979
|
+
// re-run the operation once. Max 1 auto-refresh retry (_bridgeRetried guard).
|
|
980
|
+
_bridgeRetry: do {
|
|
981
|
+
bridgeResult = null;
|
|
982
|
+
switch (operation) {
|
|
983
|
+
case 'add-method': {
|
|
984
|
+
// sourceCode and methodCode are aliases; sourceCode wins when both are set.
|
|
985
|
+
const methodSource = args.sourceCode ?? args.methodCode;
|
|
986
|
+
if (methodSource) {
|
|
987
|
+
// A single call may carry several methods — split and add each as its own
|
|
988
|
+
// <Method> so callers don't have to issue one tool call per method.
|
|
989
|
+
const bodies = countTopLevelMethodBodies(methodSource) > 1
|
|
990
|
+
? splitTopLevelMethodBodies(methodSource)
|
|
991
|
+
: [methodSource];
|
|
992
|
+
if (bodies.length > 1) {
|
|
993
|
+
const added = [];
|
|
994
|
+
let lastResult = null;
|
|
995
|
+
for (const body of bodies) {
|
|
996
|
+
const mName = extractMethodNameFromSource(body);
|
|
997
|
+
if (!mName) {
|
|
998
|
+
// A body without a derivable method name is typically a class/extension declaration
|
|
999
|
+
// (e.g. "[ExtensionOf(tableStr(T))] final class T_Extension { }"). Such blocks are
|
|
1000
|
+
// NOT methods and do not belong in add-method — skip and continue.
|
|
1001
|
+
// The regex handles: optional attribute block(s) → optional modifiers (final/abstract/
|
|
1002
|
+
// public/static/…) → class or interface keyword.
|
|
1003
|
+
const trimmed = body.trim();
|
|
1004
|
+
if (/^\s*(?:\[[^\]]*\]\s*)*(?:(?:public|private|protected|final|abstract|static|internal|sealed)\s+)*(?:class|interface)\b/i.test(trimmed)) {
|
|
1005
|
+
console.warn(`[add-method] Skipping class/interface declaration block (not a method): ${trimmed.slice(0, 80)}...`);
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
throw new Error(`⛔ add-method: could not derive a method name from one of the ${bodies.length} method bodies. ` +
|
|
1009
|
+
`Ensure each method has a complete signature (e.g. "public void foo()").`);
|
|
1010
|
+
}
|
|
1011
|
+
lastResult = await bridgeAddMethod(context.bridge, objectType, objectName, mName, body);
|
|
1012
|
+
if (!lastResult) {
|
|
1013
|
+
throw new Error(`Bridge add-method failed for '${mName}' (${added.length} of ${bodies.length} method(s) added successfully: ${added.join(', ') || 'none'}).`);
|
|
1014
|
+
}
|
|
1015
|
+
added.push(mName);
|
|
1016
|
+
}
|
|
1017
|
+
// Summarize as a single result for the downstream success message.
|
|
1018
|
+
bridgeResult = lastResult
|
|
1019
|
+
? { ...lastResult, message: `Added ${added.length} methods: ${added.join(', ')}` }
|
|
1020
|
+
: null;
|
|
1021
|
+
}
|
|
1022
|
+
else if (args.methodName) {
|
|
1023
|
+
bridgeResult = await bridgeAddMethod(context.bridge, objectType, objectName, args.methodName, methodSource);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
case 'add-display-method': {
|
|
1029
|
+
// Explicit source wins; otherwise generate a stub from displayMethodReturnEdt.
|
|
1030
|
+
let methodSource = args.sourceCode ?? args.methodCode;
|
|
1031
|
+
const methodName = args.methodName;
|
|
1032
|
+
if (!methodSource && methodName && args.displayMethodReturnEdt) {
|
|
1033
|
+
methodSource = generateDisplayMethodSource(methodName, args.displayMethodReturnEdt);
|
|
1034
|
+
generationNote =
|
|
1035
|
+
`\n\n> 🔧 Display method \`${methodName}\` generated returning ` +
|
|
1036
|
+
`\`${args.displayMethodReturnEdt}\` (stub — fill in the computation).`;
|
|
1037
|
+
}
|
|
1038
|
+
if (methodName && methodSource) {
|
|
1039
|
+
bridgeResult = await bridgeAddMethod(context.bridge, objectType, objectName, methodName, methodSource);
|
|
1040
|
+
}
|
|
1041
|
+
break;
|
|
1042
|
+
}
|
|
1043
|
+
case 'add-table-method': {
|
|
1044
|
+
// Explicit source wins; otherwise generate from tableMethodType.
|
|
1045
|
+
let methodSource = args.sourceCode ?? args.methodCode;
|
|
1046
|
+
let methodName = args.methodName;
|
|
1047
|
+
if (!methodSource && args.tableMethodType) {
|
|
1048
|
+
const gen = generateTableMethodSource(objectName, args.tableMethodType, args.tableKeyField, symbolIndex.getReadDb());
|
|
1049
|
+
methodName = gen.methodName;
|
|
1050
|
+
methodSource = gen.source;
|
|
1051
|
+
generationNote =
|
|
1052
|
+
`\n\n> 🔧 Table method \`${gen.methodName}\` generated from ` +
|
|
1053
|
+
`tableMethodType="${args.tableMethodType}".` +
|
|
1054
|
+
(gen.note ? `\n> ${gen.note}` : '');
|
|
1055
|
+
}
|
|
1056
|
+
// When caller supplies custom source (methodCode/sourceCode) + tableMethodType
|
|
1057
|
+
// but omits methodName, the method name equals the tableMethodType value
|
|
1058
|
+
// (find / exist / findByRecId / validateWrite / validateDelete / initValue).
|
|
1059
|
+
if (!methodName && args.tableMethodType && methodSource) {
|
|
1060
|
+
methodName = args.tableMethodType;
|
|
1061
|
+
}
|
|
1062
|
+
if (methodName && methodSource) {
|
|
1063
|
+
bridgeResult = await bridgeAddMethod(context.bridge, objectType, objectName, methodName, methodSource);
|
|
1064
|
+
}
|
|
1065
|
+
break;
|
|
1066
|
+
}
|
|
1067
|
+
case 'remove-method': {
|
|
1068
|
+
if (args.methodName) {
|
|
1069
|
+
bridgeResult = await bridgeRemoveMethod(context.bridge, objectType, objectName, args.methodName);
|
|
1070
|
+
}
|
|
1071
|
+
break;
|
|
1072
|
+
}
|
|
1073
|
+
case 'add-field': {
|
|
1074
|
+
if (args.fieldName && args.fieldType) {
|
|
1075
|
+
// fieldType is the EDT name; fieldBaseType is the primitive base type.
|
|
1076
|
+
// When fieldBaseType is omitted, auto-resolve it from the symbol index so the
|
|
1077
|
+
// correct AxTableField XML element is emitted. Always pass fieldType as edtName
|
|
1078
|
+
// so the <ExtendedDataType> reference is always written — previously, omitting
|
|
1079
|
+
// fieldBaseType left edtName undefined and produced fields with no EDT reference.
|
|
1080
|
+
const edtName = args.fieldType;
|
|
1081
|
+
let baseType = args.fieldBaseType ?? '';
|
|
1082
|
+
if (!baseType) {
|
|
1083
|
+
try {
|
|
1084
|
+
const rdb = symbolIndex.getReadDb();
|
|
1085
|
+
baseType = resolveEdtBaseTypeForField(edtName, rdb);
|
|
1086
|
+
}
|
|
1087
|
+
catch {
|
|
1088
|
+
baseType = edtName; // bridge will apply its own name heuristics
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
bridgeResult = await bridgeAddField(context.bridge, objectName, args.fieldName, baseType, edtName, args.fieldMandatory, args.fieldLabel);
|
|
1092
|
+
}
|
|
1093
|
+
break;
|
|
1094
|
+
}
|
|
1095
|
+
case 'modify-field': {
|
|
1096
|
+
if (args.fieldName) {
|
|
1097
|
+
// Map the field-* params onto the bare prop keys the bridge expects.
|
|
1098
|
+
const fieldProps = {};
|
|
1099
|
+
if (args.fieldLabel)
|
|
1100
|
+
fieldProps.label = args.fieldLabel;
|
|
1101
|
+
if (args.fieldHelpText)
|
|
1102
|
+
fieldProps.helpText = args.fieldHelpText;
|
|
1103
|
+
if (args.fieldMandatory !== undefined)
|
|
1104
|
+
fieldProps.mandatory = String(args.fieldMandatory);
|
|
1105
|
+
if (args.fieldType)
|
|
1106
|
+
fieldProps.edt = args.fieldType;
|
|
1107
|
+
if (args.fieldEnumType)
|
|
1108
|
+
fieldProps.enumType = args.fieldEnumType;
|
|
1109
|
+
if (args.fieldStringSize)
|
|
1110
|
+
fieldProps.stringSize = String(args.fieldStringSize);
|
|
1111
|
+
bridgeResult = await bridgeModifyField(context.bridge, objectName, args.fieldName, Object.keys(fieldProps).length > 0 ? fieldProps : undefined);
|
|
1112
|
+
}
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
case 'rename-field': {
|
|
1116
|
+
if (args.fieldName && args.fieldNewName) {
|
|
1117
|
+
bridgeResult = await bridgeRenameField(context.bridge, objectName, args.fieldName, args.fieldNewName);
|
|
1118
|
+
}
|
|
1119
|
+
break;
|
|
1120
|
+
}
|
|
1121
|
+
case 'remove-field': {
|
|
1122
|
+
if (args.fieldName) {
|
|
1123
|
+
bridgeResult = await bridgeRemoveField(context.bridge, objectName, args.fieldName);
|
|
1124
|
+
}
|
|
1125
|
+
break;
|
|
1126
|
+
}
|
|
1127
|
+
case 'replace-all-fields': {
|
|
1128
|
+
if (args.fields) {
|
|
1129
|
+
const rawFields = args.fields;
|
|
1130
|
+
const resolvedFields = rawFields.map((f) => {
|
|
1131
|
+
if (!f.type && f.edt) {
|
|
1132
|
+
try {
|
|
1133
|
+
const rdb = symbolIndex.getReadDb();
|
|
1134
|
+
return { ...f, type: resolveEdtBaseTypeForField(f.edt, rdb) };
|
|
1135
|
+
}
|
|
1136
|
+
catch {
|
|
1137
|
+
return f;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
return f;
|
|
1141
|
+
});
|
|
1142
|
+
bridgeResult = await bridgeReplaceAllFields(context.bridge, objectName, resolvedFields);
|
|
1143
|
+
}
|
|
1144
|
+
break;
|
|
1145
|
+
}
|
|
1146
|
+
case 'add-index': {
|
|
1147
|
+
if (args.indexName) {
|
|
1148
|
+
// indexFields is documented (Zod + MCP schema) as [{ fieldName, direction? }],
|
|
1149
|
+
// but the bridge's addIndex expects a flat string[] of field names. Map the
|
|
1150
|
+
// objects to their fieldName here — passing the objects straight through makes
|
|
1151
|
+
// the C# side deserialize [{fieldName:…}] into List<string>, which throws and
|
|
1152
|
+
// surfaces as a null bridge result (misreported as "could not resolve table").
|
|
1153
|
+
const indexFieldNames = Array.isArray(args.indexFields)
|
|
1154
|
+
? args.indexFields.map((f) => (typeof f === 'string' ? f : f?.fieldName)).filter(Boolean)
|
|
1155
|
+
: undefined;
|
|
1156
|
+
bridgeResult = await bridgeAddIndex(context.bridge, objectName, args.indexName, indexFieldNames, args.indexAllowDuplicates, args.indexAlternateKey);
|
|
1157
|
+
}
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
case 'remove-index': {
|
|
1161
|
+
if (args.indexName) {
|
|
1162
|
+
bridgeResult = await bridgeRemoveIndex(context.bridge, objectName, args.indexName);
|
|
1163
|
+
}
|
|
1164
|
+
break;
|
|
1165
|
+
}
|
|
1166
|
+
case 'add-relation': {
|
|
1167
|
+
if (args.relationName && args.relatedTable) {
|
|
1168
|
+
// relationConstraints is documented as [{ fieldName, relatedFieldName }], but the
|
|
1169
|
+
// bridge/C# WriteRelationConstraint deserializes the JSON keys { field, relatedField }.
|
|
1170
|
+
// Without this remap the keys don't match: C# sees no `field`/`relatedField`, leaves
|
|
1171
|
+
// both null, and silently writes a relation with EMPTY constraints (no hard error —
|
|
1172
|
+
// the corruption only surfaces at compile time). Map the field names explicitly.
|
|
1173
|
+
const constraints = Array.isArray(args.relationConstraints)
|
|
1174
|
+
? args.relationConstraints.map((c) => ({
|
|
1175
|
+
field: c?.field ?? c?.fieldName,
|
|
1176
|
+
relatedField: c?.relatedField ?? c?.relatedFieldName,
|
|
1177
|
+
}))
|
|
1178
|
+
: undefined;
|
|
1179
|
+
bridgeResult = await bridgeAddRelation(context.bridge, objectName, args.relationName, args.relatedTable, constraints);
|
|
1180
|
+
}
|
|
1181
|
+
break;
|
|
1182
|
+
}
|
|
1183
|
+
case 'remove-relation': {
|
|
1184
|
+
if (args.relationName) {
|
|
1185
|
+
bridgeResult = await bridgeRemoveRelation(context.bridge, objectName, args.relationName);
|
|
1186
|
+
}
|
|
1187
|
+
break;
|
|
1188
|
+
}
|
|
1189
|
+
case 'add-field-group': {
|
|
1190
|
+
if (args.fieldGroupName) {
|
|
1191
|
+
bridgeResult = await bridgeAddFieldGroup(context.bridge, objectName, args.fieldGroupName, args.fieldGroupLabel, args.fieldGroupFields);
|
|
1192
|
+
}
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
case 'remove-field-group': {
|
|
1196
|
+
if (args.fieldGroupName) {
|
|
1197
|
+
bridgeResult = await bridgeRemoveFieldGroup(context.bridge, objectName, args.fieldGroupName);
|
|
1198
|
+
}
|
|
1199
|
+
break;
|
|
1200
|
+
}
|
|
1201
|
+
case 'add-field-to-field-group': {
|
|
1202
|
+
if (args.fieldGroupName && args.fieldName) {
|
|
1203
|
+
bridgeResult = await bridgeAddFieldToFieldGroup(context.bridge, objectName, args.fieldGroupName, args.fieldName);
|
|
1204
|
+
}
|
|
1205
|
+
break;
|
|
1206
|
+
}
|
|
1207
|
+
case 'modify-property': {
|
|
1208
|
+
if (args.propertyPath && args.propertyValue !== undefined) {
|
|
1209
|
+
// ── Semantic guard for AxEdtExtension property changes ───────────────
|
|
1210
|
+
// D365FO silently accepts illegal extension edits (e.g. widening
|
|
1211
|
+
// StringSize on a derived EDT) but the change is ineffective at
|
|
1212
|
+
// runtime and corrupts the metadata model. Block them here with a
|
|
1213
|
+
// clear explanation of the proper alternative.
|
|
1214
|
+
if (objectType === 'edt-extension') {
|
|
1215
|
+
const guard = await validateEdtExtensionChange(objectName, args.propertyPath, String(args.propertyValue), symbolIndex.getReadDb(), context.bridge);
|
|
1216
|
+
if (!guard.ok) {
|
|
1217
|
+
return {
|
|
1218
|
+
content: [{ type: 'text', text: guard.message ?? 'EDT extension change rejected.' }],
|
|
1219
|
+
isError: true,
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
bridgeResult = await bridgeSetProperty(context.bridge, objectType, objectName, args.propertyPath, args.propertyValue);
|
|
1224
|
+
// Fallback: some object types (confirmed: AxForm) are rejected by the
|
|
1225
|
+
// bridge outright for modify-property even though the property is a
|
|
1226
|
+
// plain text element trivially editable by string replacement.
|
|
1227
|
+
if (!bridgeResult || !bridgeResult.success) {
|
|
1228
|
+
const xmlFallbackResult = await directXmlModifyProperty(actualFilePath, args.propertyPath, String(args.propertyValue));
|
|
1229
|
+
if (xmlFallbackResult) {
|
|
1230
|
+
bridgeResult = xmlFallbackResult;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
break;
|
|
1235
|
+
}
|
|
1236
|
+
case 'replace-code': {
|
|
1237
|
+
// Auto-detect common mistake: agent sends sourceCode/methodCode instead of oldCode/newCode
|
|
1238
|
+
const hasOldNew = args.oldCode && args.newCode !== undefined;
|
|
1239
|
+
const sentSourceCode = args.sourceCode || args.methodCode;
|
|
1240
|
+
if (!hasOldNew && sentSourceCode) {
|
|
1241
|
+
throw new Error(`⛔ replace-code requires 'oldCode' and 'newCode' — NOT 'sourceCode'/'methodCode'.\n\n` +
|
|
1242
|
+
`You sent sourceCode/methodCode but replace-code needs:\n` +
|
|
1243
|
+
` • oldCode = the exact existing snippet to find\n` +
|
|
1244
|
+
` • newCode = the replacement snippet\n\n` +
|
|
1245
|
+
`Example:\n` +
|
|
1246
|
+
` d365fo_file(action="modify", objectType="form", objectName="MyForm",\n` +
|
|
1247
|
+
` operation="replace-code",\n` +
|
|
1248
|
+
` methodName="PostButton.clicked",\n` +
|
|
1249
|
+
` oldCode="ttsbegin;",\n` +
|
|
1250
|
+
` newCode="")\n\n` +
|
|
1251
|
+
`If you want to replace an entire existing method, pass the full old method source as oldCode and the full new method source as newCode so the edit stays in place. Use remove-method + add-method only when you intentionally want a remove/add operation.`);
|
|
1252
|
+
}
|
|
1253
|
+
if (hasOldNew) {
|
|
1254
|
+
// Try bridge first
|
|
1255
|
+
bridgeResult = await bridgeReplaceCode(context.bridge, objectType, objectName, args.methodName, args.oldCode, args.newCode);
|
|
1256
|
+
// Fallback: if bridge returns null (unsupported type or not connected)
|
|
1257
|
+
// or success=false (SDK couldn't find the code — e.g. form control override),
|
|
1258
|
+
// do direct string replacement in the XML file.
|
|
1259
|
+
// This handles form control override methods which the SDK may not expose.
|
|
1260
|
+
if (!bridgeResult || !bridgeResult.success) {
|
|
1261
|
+
const xmlFallbackResult = await directXmlReplaceCode(actualFilePath, args.oldCode, args.newCode);
|
|
1262
|
+
if (xmlFallbackResult) {
|
|
1263
|
+
bridgeResult = xmlFallbackResult;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
else {
|
|
1268
|
+
throw new Error(`replace-code requires both 'oldCode' and 'newCode' parameters.\n` +
|
|
1269
|
+
` oldCode: ${args.oldCode ? 'provided' : '⛔ MISSING'}\n` +
|
|
1270
|
+
` newCode: ${args.newCode !== undefined ? 'provided' : '⛔ MISSING'}\n` +
|
|
1271
|
+
`Note: 'sourceCode' is NOT an alias for replace-code — you must use 'oldCode' and 'newCode'.\n\n` +
|
|
1272
|
+
renderOpSpec('replace-code'));
|
|
1273
|
+
}
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
case 'add-enum-value': {
|
|
1277
|
+
if (args.enumValueName !== undefined) {
|
|
1278
|
+
bridgeResult = await bridgeAddEnumValue(context.bridge, objectName, args.enumValueName, args.enumValueInt ?? 0, args.enumValueLabel, args.enumValueCountryRegionCodes);
|
|
1279
|
+
}
|
|
1280
|
+
break;
|
|
1281
|
+
}
|
|
1282
|
+
case 'modify-enum-value': {
|
|
1283
|
+
if (args.enumValueName) {
|
|
1284
|
+
const evProps = {};
|
|
1285
|
+
if (args.enumValueNewName)
|
|
1286
|
+
evProps.name = args.enumValueNewName;
|
|
1287
|
+
if (args.enumValueLabel)
|
|
1288
|
+
evProps.label = args.enumValueLabel;
|
|
1289
|
+
if (args.enumValueInt !== undefined)
|
|
1290
|
+
evProps.value = String(args.enumValueInt);
|
|
1291
|
+
bridgeResult = await bridgeModifyEnumValue(context.bridge, objectName, args.enumValueName, Object.keys(evProps).length > 0 ? evProps : undefined);
|
|
1292
|
+
}
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
case 'remove-enum-value': {
|
|
1296
|
+
if (args.enumValueName) {
|
|
1297
|
+
bridgeResult = await bridgeRemoveEnumValue(context.bridge, objectName, args.enumValueName);
|
|
1298
|
+
}
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
case 'add-control': {
|
|
1302
|
+
if (args.controlName && args.parentControl) {
|
|
1303
|
+
// The tool never exposes a `controlType` input (not in the Zod schema below),
|
|
1304
|
+
// so this always used to fall back to the literal string "String" — every
|
|
1305
|
+
// control, regardless of the bound field's actual type, was created as a plain
|
|
1306
|
+
// string edit control. Infer a better default from the field name heuristic
|
|
1307
|
+
// (the same one used for table field base-type resolution) when a
|
|
1308
|
+
// controlDataField is given; this is a best-effort name heuristic, not an
|
|
1309
|
+
// index/bridge EDT lookup (that would need resolving controlDataSource, a form
|
|
1310
|
+
// DATA SOURCE name, back to its bound table — out of scope here), but it is
|
|
1311
|
+
// strictly better than always guessing "String".
|
|
1312
|
+
const resolvedControlType = args.controlType
|
|
1313
|
+
?? (args.controlDataField ? heuristicEdtBaseType(args.controlDataField) : undefined)
|
|
1314
|
+
?? 'String';
|
|
1315
|
+
bridgeResult = await bridgeAddControl(context.bridge, objectName, args.controlName, args.parentControl, resolvedControlType, args.controlDataSource, args.controlDataField, args.controlLabel);
|
|
1316
|
+
// Fallback: the bridge's AddControl resolves its target via _provider.Forms,
|
|
1317
|
+
// which can never find a form EXTENSION (named "Base.Suffix") — it always
|
|
1318
|
+
// reports 'Form "<ext>" not found', regardless of metadata-root freshness.
|
|
1319
|
+
// For form extensions, write the control element straight into the XML.
|
|
1320
|
+
if (objectType === 'form-extension' && (!bridgeResult || !bridgeResult.success)) {
|
|
1321
|
+
const xmlFallbackResult = await directXmlAddControl(actualFilePath, args.controlName, args.parentControl, resolvedControlType, args.controlDataSource, args.controlDataField, args.controlLabel);
|
|
1322
|
+
if (xmlFallbackResult)
|
|
1323
|
+
bridgeResult = xmlFallbackResult;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
break;
|
|
1327
|
+
}
|
|
1328
|
+
case 'add-data-source': {
|
|
1329
|
+
if (args.dataSourceName && args.dataSourceTable) {
|
|
1330
|
+
bridgeResult = await bridgeAddDataSource(context.bridge, objectType, objectName, args.dataSourceName, args.dataSourceTable, args.joinSource, args.linkType);
|
|
1331
|
+
}
|
|
1332
|
+
break;
|
|
1333
|
+
}
|
|
1334
|
+
case 'add-field-modification': {
|
|
1335
|
+
if (args.fieldName) {
|
|
1336
|
+
bridgeResult = await bridgeAddFieldModification(context.bridge, objectName, args.fieldName, args.fieldLabel, args.fieldMandatory);
|
|
1337
|
+
}
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
case 'add-menu-item-to-menu': {
|
|
1341
|
+
if (args.menuItemToAdd) {
|
|
1342
|
+
bridgeResult = await bridgeAddMenuItemToMenu(context.bridge, objectName, args.menuItemToAdd, args.menuItemToAddType);
|
|
1343
|
+
// Fallback: bridge requires the menu to exist in its loaded metadata roots.
|
|
1344
|
+
// Newly created menus aren't there yet — write directly to the XML file.
|
|
1345
|
+
if (!bridgeResult || !bridgeResult.success) {
|
|
1346
|
+
const xmlFallbackResult = await directXmlAddMenuItemToMenu(actualFilePath, args.menuItemToAdd, args.menuItemToAddType ?? 'display');
|
|
1347
|
+
if (xmlFallbackResult) {
|
|
1348
|
+
bridgeResult = xmlFallbackResult;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
break;
|
|
1353
|
+
}
|
|
1354
|
+
default:
|
|
1355
|
+
throw new Error(`Unsupported operation: ${operation}`);
|
|
1356
|
+
}
|
|
1357
|
+
if (!bridgeResult) {
|
|
1358
|
+
// Required params + full per-op specs live in the central registry
|
|
1359
|
+
// (d365foFileOpSpecs.ts) — the published schema only advertises a
|
|
1360
|
+
// free-form `params` object, so this error must carry the whole spec.
|
|
1361
|
+
const required = getRequiredParams(operation);
|
|
1362
|
+
// Treat a supplied alias as satisfying the requirement.
|
|
1363
|
+
const isProvided = (p) => args[p] !== undefined || (OP_PARAM_ALIASES[p] ?? []).some(a => args[a] !== undefined);
|
|
1364
|
+
const missing = required.filter(p => !isProvided(p));
|
|
1365
|
+
// Auto-refresh retry: all required params are present but bridge returned
|
|
1366
|
+
// null — the object was likely written this session and isn't in the
|
|
1367
|
+
// bridge's metadata model yet (roots are fixed at startup). Refresh once
|
|
1368
|
+
// and re-run the operation. Afterwards the normal error path takes over.
|
|
1369
|
+
if (missing.length === 0 && !_bridgeRetried && context.bridge) {
|
|
1370
|
+
console.error(`[modify_d365fo_file] ⚠️ '${operation}' on '${objectName}' returned null — ` +
|
|
1371
|
+
`refreshing bridge provider and retrying once`);
|
|
1372
|
+
try {
|
|
1373
|
+
await bridgeRefreshProvider(context.bridge);
|
|
1374
|
+
}
|
|
1375
|
+
catch { /* best-effort */ }
|
|
1376
|
+
_bridgeRetried = true;
|
|
1377
|
+
continue _bridgeRetry;
|
|
1378
|
+
}
|
|
1379
|
+
// Two distinct causes produce a null bridge result; pick the message that
|
|
1380
|
+
// matches reality instead of always blaming missing parameters.
|
|
1381
|
+
if (missing.length > 0) {
|
|
1382
|
+
const missingList = missing.map(p => ` ⛔ ${p}: MISSING`);
|
|
1383
|
+
const providedList = required.filter(isProvided).map(p => ` ✅ ${p}: provided`);
|
|
1384
|
+
throw new Error(`Bridge operation '${operation}' returned null — required parameters are missing.\n` +
|
|
1385
|
+
`Required parameters for '${operation}':\n${[...providedList, ...missingList].join('\n')}\n` +
|
|
1386
|
+
`Provided args: ${Object.keys(args).filter(k => args[k] !== undefined).join(', ')}\n\n` +
|
|
1387
|
+
renderOpSpec(operation));
|
|
1388
|
+
}
|
|
1389
|
+
// All required params were supplied, yet the bridge returned null (it never
|
|
1390
|
+
// attempted the op — e.g. provider not ready). Surface the actionable
|
|
1391
|
+
// same-session resolution guidance.
|
|
1392
|
+
throw new Error(unresolvedObjectError(operation, objectType, objectName, actualFilePath));
|
|
1393
|
+
}
|
|
1394
|
+
// Bridge executed but reported failure. A resolution failure (object created
|
|
1395
|
+
// this session, not yet in the provider's fixed-root model) gets the same
|
|
1396
|
+
// one-shot refresh+retry the null path gets; any other failure falls through
|
|
1397
|
+
// to the real-message throw below — no longer masked as a generic guess.
|
|
1398
|
+
if (!bridgeResult.success &&
|
|
1399
|
+
!_bridgeRetried &&
|
|
1400
|
+
context.bridge &&
|
|
1401
|
+
isUnresolvedObjectError(bridgeResult.message)) {
|
|
1402
|
+
console.error(`[modify_d365fo_file] ⚠️ '${operation}' on '${objectName}' failed (${bridgeResult.message}) — ` +
|
|
1403
|
+
`refreshing bridge provider and retrying once`);
|
|
1404
|
+
try {
|
|
1405
|
+
await bridgeRefreshProvider(context.bridge);
|
|
1406
|
+
}
|
|
1407
|
+
catch { /* best-effort */ }
|
|
1408
|
+
_bridgeRetried = true;
|
|
1409
|
+
continue _bridgeRetry;
|
|
1410
|
+
}
|
|
1411
|
+
break _bridgeRetry;
|
|
1412
|
+
} while (true); // end of retry loop
|
|
1413
|
+
if (!bridgeResult.success) {
|
|
1414
|
+
// Surface the real bridge error. For an object-resolution failure keep the
|
|
1415
|
+
// actionable same-session fallback guidance, now including exactly what the
|
|
1416
|
+
// bridge reported instead of a generic "could not resolve" guess.
|
|
1417
|
+
if (isUnresolvedObjectError(bridgeResult.message)) {
|
|
1418
|
+
throw new Error(unresolvedObjectError(operation, objectType, objectName, actualFilePath, bridgeResult.message));
|
|
1419
|
+
}
|
|
1420
|
+
let opErrorMsg = `Bridge operation '${operation}' failed: ${bridgeResult.message}`;
|
|
1421
|
+
if (operation === 'replace-code' && /oldCode not found/i.test(bridgeResult.message ?? '')) {
|
|
1422
|
+
opErrorMsg +=
|
|
1423
|
+
`\n\n💡 Tip: The oldCode must match the exact source currently on disk.\n` +
|
|
1424
|
+
` • Fetch the current source first: get_object_info(objectType="${objectType}", name="${objectName}")\n` +
|
|
1425
|
+
` • Then copy the exact snippet from the output as oldCode.\n` +
|
|
1426
|
+
` • Alternative: use add-method with the complete new method body — it overwrites the existing method.`;
|
|
1427
|
+
}
|
|
1428
|
+
throw new Error(opErrorMsg);
|
|
1429
|
+
}
|
|
1430
|
+
console.error(`[modify_d365fo_file] ✅ Bridge ${operation}: ${bridgeResult.message}`);
|
|
1431
|
+
// Post-write validation (best-effort, fire-and-forget).
|
|
1432
|
+
// Not awaited: the validation goes through the sequential bridge stdin/stdout
|
|
1433
|
+
// pipe and can take 60s+, which would block all subsequent MCP calls.
|
|
1434
|
+
// See: https://github.com/dynamics365ninja/d365fo-mcp-server/issues/407
|
|
1435
|
+
let bridgeValidation = '';
|
|
1436
|
+
bridgeValidateAfterWrite(context.bridge, objectType, objectName).then(validationMsg => {
|
|
1437
|
+
if (validationMsg) {
|
|
1438
|
+
console.error(`[modify_d365fo_file] Bridge validation: ${validationMsg}`);
|
|
1439
|
+
}
|
|
1440
|
+
}).catch(e => {
|
|
1441
|
+
console.error(`[modify_d365fo_file] Bridge validation skipped: ${e}`);
|
|
1442
|
+
});
|
|
1443
|
+
// Optionally add the file to the Visual Studio project
|
|
1444
|
+
let projectMessage = '';
|
|
1445
|
+
if (args.addToProject) {
|
|
1446
|
+
const configManager = getConfigManager();
|
|
1447
|
+
await configManager.ensureLoaded();
|
|
1448
|
+
let resolvedProjectPath = args.projectPath || await configManager.getProjectPath() || undefined;
|
|
1449
|
+
const resolvedSolutionPath = args.solutionPath || await configManager.getSolutionPath() || undefined;
|
|
1450
|
+
if (!resolvedProjectPath && resolvedSolutionPath) {
|
|
1451
|
+
resolvedProjectPath = await ProjectFileFinder.findProjectInSolution(resolvedSolutionPath, modelName || configManager.getModelName() || '') || undefined;
|
|
1452
|
+
}
|
|
1453
|
+
if (resolvedProjectPath) {
|
|
1454
|
+
try {
|
|
1455
|
+
await fs.access(resolvedProjectPath);
|
|
1456
|
+
const projectManager = new ProjectFileManager();
|
|
1457
|
+
const wasAdded = await projectManager.addToProject(resolvedProjectPath, objectType, objectName, actualFilePath);
|
|
1458
|
+
projectMessage = wasAdded
|
|
1459
|
+
? `\n✅ Added to VS project: \`${resolvedProjectPath}\``
|
|
1460
|
+
: `\n📋 Already in VS project: \`${resolvedProjectPath}\``;
|
|
1461
|
+
}
|
|
1462
|
+
catch (projErr) {
|
|
1463
|
+
const errMsg = projErr instanceof Error ? projErr.message : String(projErr);
|
|
1464
|
+
projectMessage = `\n⚠️ File modified but could not add to VS project: ${errMsg}`;
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
projectMessage =
|
|
1469
|
+
`\n⚠️ addToProject=true but no projectPath could be resolved.\n` +
|
|
1470
|
+
`Add \`projectPath\` to .mcp.json or pass it explicitly.`;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
// Advisory X++ select-statement lint on the source just written (add-method /
|
|
1474
|
+
// replace-code etc.). Non-blocking: surfaces a likely "WHERE after join" mistake
|
|
1475
|
+
// up front instead of letting it become a build error the agent hunts by hand.
|
|
1476
|
+
const xppLintWarnings = lintXppSelect(args.sourceCode ?? args.methodCode ?? args.newCode);
|
|
1477
|
+
const xppLintNote = xppLintWarnings.length > 0 ? `\n\n${xppLintWarnings.join('\n\n')}` : '';
|
|
1478
|
+
return {
|
|
1479
|
+
content: [
|
|
1480
|
+
{
|
|
1481
|
+
type: 'text',
|
|
1482
|
+
text: `✅ ${operation} on ${objectType} "${objectName}" — applied via IMetadataProvider.Update()\n\n` +
|
|
1483
|
+
`**File:** ${actualFilePath}${addControlNote}${generationNote}${bridgeValidation}${projectMessage}\n` +
|
|
1484
|
+
`🔧 API: ${bridgeResult.message}${xppLintNote}${backupNote}\n\n` +
|
|
1485
|
+
`**Next steps:**\n- Review changes in Visual Studio\n- Build the model to validate`,
|
|
1486
|
+
},
|
|
1487
|
+
],
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
catch (error) {
|
|
1491
|
+
return {
|
|
1492
|
+
content: [
|
|
1493
|
+
{
|
|
1494
|
+
type: 'text',
|
|
1495
|
+
text: `❌ Error modifying D365FO file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
1496
|
+
},
|
|
1497
|
+
],
|
|
1498
|
+
isError: true,
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
/**
|
|
1503
|
+
* Find D365FO file path
|
|
1504
|
+
*/
|
|
1505
|
+
async function findD365File(symbolIndex, objectType, objectName, modelName, _workspacePath, explicitFilePath, packagePath) {
|
|
1506
|
+
// Explicit path bypasses all lookup — use when caller knows the exact location
|
|
1507
|
+
// (e.g. the path was returned by create_d365fo_file).
|
|
1508
|
+
if (explicitFilePath) {
|
|
1509
|
+
return explicitFilePath;
|
|
1510
|
+
}
|
|
1511
|
+
// Resolve by the given name first; if that misses, retry once with the model
|
|
1512
|
+
// prefix applied. create_d365fo_file auto-prefixes new object names (e.g.
|
|
1513
|
+
// "RentEquipmentTable" → "ContosoRentEquipmentTable"), so a modify call with the bare
|
|
1514
|
+
// name would otherwise fail to locate the file. The bridge object name is then
|
|
1515
|
+
// re-derived from the resolved file's basename, so the prefixed name flows through.
|
|
1516
|
+
const direct = await resolveD365FileByName(symbolIndex, objectType, objectName, modelName, packagePath);
|
|
1517
|
+
if (direct)
|
|
1518
|
+
return direct;
|
|
1519
|
+
const prefix = resolveObjectPrefix(modelName || getConfigManager().getModelName() || '');
|
|
1520
|
+
if (prefix && !objectType.endsWith('-extension')) {
|
|
1521
|
+
const prefixed = applyObjectPrefix(objectName, prefix);
|
|
1522
|
+
if (prefixed && prefixed.toLowerCase() !== objectName.toLowerCase()) {
|
|
1523
|
+
const viaPrefixed = await resolveD365FileByName(symbolIndex, objectType, prefixed, modelName, packagePath);
|
|
1524
|
+
if (viaPrefixed) {
|
|
1525
|
+
console.error(`[modifyD365File] '${objectName}' not found — resolved via prefixed name '${prefixed}'`);
|
|
1526
|
+
return viaPrefixed;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
return null;
|
|
1531
|
+
}
|
|
1532
|
+
/** Resolve a D365FO file by an exact object name (symbol DB, then filesystem). */
|
|
1533
|
+
async function resolveD365FileByName(symbolIndex, objectType, objectName, modelName, packagePath) {
|
|
1534
|
+
// Symbol DB only indexes a subset of types — for the rest go straight to filesystem.
|
|
1535
|
+
const dbTypeMap = {
|
|
1536
|
+
class: 'class',
|
|
1537
|
+
table: 'table',
|
|
1538
|
+
form: 'form',
|
|
1539
|
+
enum: 'enum',
|
|
1540
|
+
query: 'query',
|
|
1541
|
+
view: 'view',
|
|
1542
|
+
};
|
|
1543
|
+
const symbolType = dbTypeMap[objectType];
|
|
1544
|
+
// Query database when a symbol type mapping exists
|
|
1545
|
+
if (symbolType) {
|
|
1546
|
+
let dbResult = null;
|
|
1547
|
+
const rdb = symbolIndex.getReadDb();
|
|
1548
|
+
if (modelName) {
|
|
1549
|
+
const stmt = rdb.prepare(`
|
|
1550
|
+
SELECT file_path
|
|
1551
|
+
FROM symbols
|
|
1552
|
+
WHERE type = ? AND name = ? AND model = ?
|
|
1553
|
+
LIMIT 1
|
|
1554
|
+
`);
|
|
1555
|
+
const row = stmt.get(symbolType, objectName, modelName);
|
|
1556
|
+
dbResult = row ? row.file_path : null;
|
|
1557
|
+
}
|
|
1558
|
+
else {
|
|
1559
|
+
// No modelName specified — prefer the user's configured model to avoid
|
|
1560
|
+
// accidentally resolving to a standard Microsoft model (issue #369).
|
|
1561
|
+
const configuredModel = getConfigManager().getModelName();
|
|
1562
|
+
if (configuredModel) {
|
|
1563
|
+
const stmtPref = rdb.prepare(`
|
|
1564
|
+
SELECT file_path
|
|
1565
|
+
FROM symbols
|
|
1566
|
+
WHERE type = ? AND name = ? AND model = ?
|
|
1567
|
+
LIMIT 1
|
|
1568
|
+
`);
|
|
1569
|
+
const prefRow = stmtPref.get(symbolType, objectName, configuredModel);
|
|
1570
|
+
dbResult = prefRow ? prefRow.file_path : null;
|
|
1571
|
+
}
|
|
1572
|
+
if (!dbResult) {
|
|
1573
|
+
// Fallback: any model (still guarded by the standard-model check after findD365File)
|
|
1574
|
+
const stmt = rdb.prepare(`
|
|
1575
|
+
SELECT file_path
|
|
1576
|
+
FROM symbols
|
|
1577
|
+
WHERE type = ? AND name = ?
|
|
1578
|
+
ORDER BY model
|
|
1579
|
+
LIMIT 1
|
|
1580
|
+
`);
|
|
1581
|
+
const row = stmt.get(symbolType, objectName);
|
|
1582
|
+
dbResult = row ? row.file_path : null;
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
// Only trust the DB path when it is an absolute path that actually exists on disk.
|
|
1586
|
+
// The DB file_path column stores paths from the CI build agent (e.g. C:\home\vsts\work\...)
|
|
1587
|
+
// which are never accessible at runtime. Relative paths (e.g. "ContosoExt/ContosoExt/AxClass/Foo.xml")
|
|
1588
|
+
// also come from this source and cannot be used directly.
|
|
1589
|
+
// Fall through to findD365FileOnDisk which builds the correct absolute path from config.
|
|
1590
|
+
//
|
|
1591
|
+
// Use cross-platform absolute detection so that Windows-style drive paths (C:\...)
|
|
1592
|
+
// are recognised as absolute even when the server runs on Linux/macOS (path.isAbsolute
|
|
1593
|
+
// returns false for Windows paths on POSIX hosts, causing spurious fallback loops).
|
|
1594
|
+
const isAbsoluteXPlat = (p) => path.isAbsolute(p) || /^[a-zA-Z]:[\\/]/.test(p) || /^\\\\/.test(p);
|
|
1595
|
+
if (dbResult && isAbsoluteXPlat(dbResult)) {
|
|
1596
|
+
try {
|
|
1597
|
+
await import('fs').then(m => m.promises.access(dbResult));
|
|
1598
|
+
return dbResult;
|
|
1599
|
+
}
|
|
1600
|
+
catch {
|
|
1601
|
+
// Absolute path from DB but not accessible — fall through to filesystem lookup
|
|
1602
|
+
console.error(`[modifyD365File] DB path not accessible: ${dbResult} — falling back to filesystem lookup`);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
else if (dbResult) {
|
|
1606
|
+
console.error(`[modifyD365File] DB returned relative path: ${dbResult} — falling back to filesystem lookup`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
// Filesystem fallback: handles newly created files not yet in the symbol index,
|
|
1610
|
+
// and all types not covered by the symbol DB (edt, report, extensions, security, menu …).
|
|
1611
|
+
return findD365FileOnDisk(objectType, objectName, modelName, packagePath);
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Filesystem fallback for findD365File.
|
|
1615
|
+
* Constructs the expected AOT file path from config/env and checks if it exists on disk.
|
|
1616
|
+
* This handles objects that were just created and are not yet indexed in the symbol database.
|
|
1617
|
+
*/
|
|
1618
|
+
export async function findD365FileOnDisk(objectType, objectName, modelName, explicitPackagePath) {
|
|
1619
|
+
const folderMap = {
|
|
1620
|
+
class: 'AxClass',
|
|
1621
|
+
table: 'AxTable',
|
|
1622
|
+
form: 'AxForm',
|
|
1623
|
+
enum: 'AxEnum',
|
|
1624
|
+
query: 'AxQuery',
|
|
1625
|
+
view: 'AxView',
|
|
1626
|
+
edt: 'AxEdt',
|
|
1627
|
+
'data-entity': 'AxDataEntityView',
|
|
1628
|
+
report: 'AxReport',
|
|
1629
|
+
'table-extension': 'AxTableExtension',
|
|
1630
|
+
'class-extension': 'AxClass',
|
|
1631
|
+
'form-extension': 'AxFormExtension',
|
|
1632
|
+
'enum-extension': 'AxEnumExtension',
|
|
1633
|
+
'edt-extension': 'AxEdtExtension',
|
|
1634
|
+
'data-entity-extension': 'AxDataEntityViewExtension',
|
|
1635
|
+
'menu-item-display': 'AxMenuItemDisplay',
|
|
1636
|
+
'menu-item-action': 'AxMenuItemAction',
|
|
1637
|
+
'menu-item-output': 'AxMenuItemOutput',
|
|
1638
|
+
'menu-item-display-extension': 'AxMenuItemDisplayExtension',
|
|
1639
|
+
'menu-item-action-extension': 'AxMenuItemActionExtension',
|
|
1640
|
+
'menu-item-output-extension': 'AxMenuItemOutputExtension',
|
|
1641
|
+
menu: 'AxMenu',
|
|
1642
|
+
'menu-extension': 'AxMenuExtension',
|
|
1643
|
+
'security-privilege': 'AxSecurityPrivilege',
|
|
1644
|
+
'security-duty': 'AxSecurityDuty',
|
|
1645
|
+
'security-role': 'AxSecurityRole',
|
|
1646
|
+
};
|
|
1647
|
+
const objectFolder = folderMap[objectType];
|
|
1648
|
+
if (!objectFolder)
|
|
1649
|
+
return null;
|
|
1650
|
+
const configManager = getConfigManager();
|
|
1651
|
+
// Ensure .mcp.json is loaded — lazy init so this works even when
|
|
1652
|
+
// server startup did not call initializeConfig() before this tool ran.
|
|
1653
|
+
await configManager.ensureLoaded();
|
|
1654
|
+
// Resolve model name (same priority order as generateSmartTable):
|
|
1655
|
+
// 1. Explicit arg (skip placeholders like "any")
|
|
1656
|
+
// 2. .mcp.json context (modelName field or last segment of workspacePath)
|
|
1657
|
+
// 3. Auto-detected model name (async, from .rnrproj scan)
|
|
1658
|
+
// 4. D365FO_MODEL_NAME env var
|
|
1659
|
+
const resolvedModel = (modelName && modelName !== 'any' ? modelName : null) ||
|
|
1660
|
+
configManager.getModelName() ||
|
|
1661
|
+
(await configManager.getAutoDetectedModelName()) ||
|
|
1662
|
+
process.env.D365FO_MODEL_NAME ||
|
|
1663
|
+
null;
|
|
1664
|
+
if (!resolvedModel) {
|
|
1665
|
+
console.error('[modifyD365File] Filesystem fallback: could not resolve model name. ' +
|
|
1666
|
+
'Provide modelName parameter, configure .mcp.json with modelName/projectPath, or set D365FO_MODEL_NAME env var.');
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
const configPackagePath = configManager.getPackagePath() || fallbackPackagePath();
|
|
1670
|
+
// Resolve the custom write root (D365FO_CUSTOM_PACKAGES_PATH).
|
|
1671
|
+
// Try this before the MS PLD so we find repo-tracked objects first.
|
|
1672
|
+
const customWritePath = await configManager.getCustomPackagesPath();
|
|
1673
|
+
// Candidate 0: caller-supplied packagePath. Highest priority — the caller knows
|
|
1674
|
+
// the model lives outside the default PackagesLocalDirectory (e.g. a repo checkout).
|
|
1675
|
+
// Try both the package==model layout and a PackageResolver scan rooted here so a
|
|
1676
|
+
// package!=model layout (e.g. package "ACAUTOCONT" / model "AC AUTOCONT") still resolves.
|
|
1677
|
+
if (explicitPackagePath) {
|
|
1678
|
+
const directPath = path.join(explicitPackagePath, resolvedModel, resolvedModel, objectFolder, `${objectName}.xml`);
|
|
1679
|
+
try {
|
|
1680
|
+
await fs.access(directPath);
|
|
1681
|
+
console.error(`[modifyD365File] Found via explicit packagePath: ${directPath}`);
|
|
1682
|
+
return directPath;
|
|
1683
|
+
}
|
|
1684
|
+
catch { /* try resolver scan below */ }
|
|
1685
|
+
try {
|
|
1686
|
+
const resolver = new PackageResolver([explicitPackagePath]);
|
|
1687
|
+
const resolved = await resolver.resolve(resolvedModel);
|
|
1688
|
+
if (resolved) {
|
|
1689
|
+
const resolvedPath = path.join(resolved.rootPath, resolved.packageName, resolvedModel, objectFolder, `${objectName}.xml`);
|
|
1690
|
+
await fs.access(resolvedPath);
|
|
1691
|
+
console.error(`[modifyD365File] Found via explicit packagePath (PackageResolver): ${resolvedPath}`);
|
|
1692
|
+
return resolvedPath;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
catch { /* not found under explicit packagePath; fall through */ }
|
|
1696
|
+
}
|
|
1697
|
+
// Candidate 1: custom write root, package == model (most common for custom models)
|
|
1698
|
+
if (customWritePath) {
|
|
1699
|
+
const customCandidatePath = path.join(customWritePath, resolvedModel, resolvedModel, objectFolder, `${objectName}.xml`);
|
|
1700
|
+
try {
|
|
1701
|
+
await fs.access(customCandidatePath);
|
|
1702
|
+
console.error(`[modifyD365File] Found via custom packages path: ${customCandidatePath}`);
|
|
1703
|
+
return customCandidatePath;
|
|
1704
|
+
}
|
|
1705
|
+
catch {
|
|
1706
|
+
// Not at custom path; continue
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
// Candidate 2: MS PLD / configured packagePath, package == model (traditional fallback)
|
|
1710
|
+
const candidatePath = path.join(configPackagePath, resolvedModel, resolvedModel, objectFolder, `${objectName}.xml`);
|
|
1711
|
+
try {
|
|
1712
|
+
await fs.access(candidatePath);
|
|
1713
|
+
console.error(`[modifyD365File] Found via filesystem fallback: ${candidatePath}`);
|
|
1714
|
+
return candidatePath;
|
|
1715
|
+
}
|
|
1716
|
+
catch {
|
|
1717
|
+
// Not at the default package==model path; try PackageResolver (UDE or custom layout)
|
|
1718
|
+
}
|
|
1719
|
+
// Candidate 3: PackageResolver scan — handles package != model layouts in both UDE
|
|
1720
|
+
// and traditional setups with a custom root
|
|
1721
|
+
try {
|
|
1722
|
+
const envType = await configManager.getDevEnvironmentType();
|
|
1723
|
+
const msPath = envType === 'ude' ? await configManager.getMicrosoftPackagesPath() : null;
|
|
1724
|
+
const roots = [explicitPackagePath, customWritePath, msPath, configPackagePath].filter(Boolean);
|
|
1725
|
+
const resolver = new PackageResolver(roots);
|
|
1726
|
+
const resolved = await resolver.resolve(resolvedModel);
|
|
1727
|
+
if (resolved) {
|
|
1728
|
+
const resolvedPath = path.join(resolved.rootPath, resolved.packageName, resolvedModel, objectFolder, `${objectName}.xml`);
|
|
1729
|
+
try {
|
|
1730
|
+
await fs.access(resolvedPath);
|
|
1731
|
+
console.error(`[modifyD365File] Found via PackageResolver: ${resolvedPath}`);
|
|
1732
|
+
return resolvedPath;
|
|
1733
|
+
}
|
|
1734
|
+
catch {
|
|
1735
|
+
// Not found at resolved path either
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
catch {
|
|
1740
|
+
// PackageResolver failed — skip silently
|
|
1741
|
+
}
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Create file backup and verify it was written successfully.
|
|
1746
|
+
* Throws if the source file is missing or the copy fails, so callers
|
|
1747
|
+
* always know whether a valid backup exists before overwriting.
|
|
1748
|
+
* Returns the backup file path.
|
|
1749
|
+
*/
|
|
1750
|
+
async function createFileBackup(filePath) {
|
|
1751
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').substring(0, 19);
|
|
1752
|
+
const backupPath = `${filePath}.backup-${timestamp}`;
|
|
1753
|
+
try {
|
|
1754
|
+
await fs.copyFile(filePath, backupPath);
|
|
1755
|
+
// Confirm the backup has non-zero size before proceeding
|
|
1756
|
+
const stat = await fs.stat(backupPath);
|
|
1757
|
+
if (stat.size === 0) {
|
|
1758
|
+
throw new Error('Backup file was created but is empty');
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
catch (error) {
|
|
1762
|
+
throw new Error(`Failed to create backup at "${backupPath}": ${error instanceof Error ? error.message : String(error)}`);
|
|
1763
|
+
}
|
|
1764
|
+
return backupPath;
|
|
1765
|
+
}
|
|
1766
|
+
const execFileAsync = util.promisify(execFile);
|
|
1767
|
+
// Directory → inside-git-work-tree result, cached for the process lifetime so
|
|
1768
|
+
// repeated modifies don't re-spawn git for the same metadata folder.
|
|
1769
|
+
const gitWorkTreeCache = new Map();
|
|
1770
|
+
/**
|
|
1771
|
+
* Cheap check whether a file lives inside a git work tree — i.e. whether
|
|
1772
|
+
* undo_last_modification (git checkout) could revert a change to it.
|
|
1773
|
+
* git not installed, timeout, or any other error → treated as "not a repo".
|
|
1774
|
+
*/
|
|
1775
|
+
async function isInsideGitWorkTree(filePath) {
|
|
1776
|
+
const dir = path.dirname(filePath);
|
|
1777
|
+
const cached = gitWorkTreeCache.get(dir);
|
|
1778
|
+
if (cached !== undefined)
|
|
1779
|
+
return cached;
|
|
1780
|
+
let inside = false;
|
|
1781
|
+
try {
|
|
1782
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
1783
|
+
cwd: dir,
|
|
1784
|
+
timeout: 5_000,
|
|
1785
|
+
windowsHide: true,
|
|
1786
|
+
});
|
|
1787
|
+
inside = stdout.trim() === 'true';
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
inside = false;
|
|
1791
|
+
}
|
|
1792
|
+
gitWorkTreeCache.set(dir, inside);
|
|
1793
|
+
return inside;
|
|
1794
|
+
}
|
|
1795
|
+
/**
|
|
1796
|
+
* Backup guard for modify operations. Honors an explicit createBackup=true;
|
|
1797
|
+
* with createBackup=false it force-enables the backup when the target is not
|
|
1798
|
+
* inside a git work tree, because the documented undo path
|
|
1799
|
+
* (undo_last_modification → git checkout) only works inside a repo.
|
|
1800
|
+
* Returns a note to append to the success response ('' when no forced backup
|
|
1801
|
+
* was needed). Exported for unit tests.
|
|
1802
|
+
*/
|
|
1803
|
+
export async function ensureRecoverableModification(actualFilePath, createBackup) {
|
|
1804
|
+
if (createBackup) {
|
|
1805
|
+
await createFileBackup(actualFilePath);
|
|
1806
|
+
return '';
|
|
1807
|
+
}
|
|
1808
|
+
if (await isInsideGitWorkTree(actualFilePath)) {
|
|
1809
|
+
return '';
|
|
1810
|
+
}
|
|
1811
|
+
const backupPath = await createFileBackup(actualFilePath);
|
|
1812
|
+
return (`\n\nℹ️ Target is not under git — created backup ${backupPath} automatically ` +
|
|
1813
|
+
`(undo_last_modification would not work here).`);
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Recursively walk an AxFormControl node forest and collect every control.
|
|
1817
|
+
*/
|
|
1818
|
+
function walkFormControls(nodes, out, parentName, pathParts) {
|
|
1819
|
+
for (const node of nodes) {
|
|
1820
|
+
if (!node || typeof node !== 'object')
|
|
1821
|
+
continue;
|
|
1822
|
+
const name = Array.isArray(node.Name) ? node.Name[0] : (node.Name ?? '');
|
|
1823
|
+
if (!name)
|
|
1824
|
+
continue;
|
|
1825
|
+
const currentPath = [...pathParts, name];
|
|
1826
|
+
out.push({ name, parentName, pathStr: currentPath.join(' › ') });
|
|
1827
|
+
const cn = Array.isArray(node.Controls) ? node.Controls[0] : node.Controls;
|
|
1828
|
+
if (cn?.AxFormControl) {
|
|
1829
|
+
const children = Array.isArray(cn.AxFormControl) ? cn.AxFormControl : [cn.AxFormControl];
|
|
1830
|
+
walkFormControls(children, out, name, currentPath);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Extract all controls from a parsed AxForm xmlObj.
|
|
1836
|
+
*/
|
|
1837
|
+
function allControlsFromFormXmlObj(xmlObj) {
|
|
1838
|
+
const results = [];
|
|
1839
|
+
const axForm = xmlObj.AxForm;
|
|
1840
|
+
if (!axForm)
|
|
1841
|
+
return results;
|
|
1842
|
+
const designNode = Array.isArray(axForm.Design) ? axForm.Design[0] : axForm.Design;
|
|
1843
|
+
if (!designNode)
|
|
1844
|
+
return results;
|
|
1845
|
+
let rootNodes = [];
|
|
1846
|
+
// AxFormDesign wrapper (standard D365FO 10.0 format)
|
|
1847
|
+
if (designNode.AxFormDesign) {
|
|
1848
|
+
const fds = Array.isArray(designNode.AxFormDesign) ? designNode.AxFormDesign : [designNode.AxFormDesign];
|
|
1849
|
+
for (const fd of fds) {
|
|
1850
|
+
const cn = Array.isArray(fd.Controls) ? fd.Controls[0] : fd.Controls;
|
|
1851
|
+
if (cn?.AxFormControl) {
|
|
1852
|
+
const items = Array.isArray(cn.AxFormControl) ? cn.AxFormControl : [cn.AxFormControl];
|
|
1853
|
+
rootNodes = rootNodes.concat(items);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
else if (designNode.Controls) {
|
|
1858
|
+
const cn = Array.isArray(designNode.Controls) ? designNode.Controls[0] : designNode.Controls;
|
|
1859
|
+
if (cn?.AxFormControl) {
|
|
1860
|
+
rootNodes = Array.isArray(cn.AxFormControl) ? cn.AxFormControl : [cn.AxFormControl];
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
walkFormControls(rootNodes, results, null, []);
|
|
1864
|
+
return results;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Locate the base form XML on disk, trying DB path → remapped path → filesystem scan.
|
|
1868
|
+
* Returns raw XML content, or null if not accessible.
|
|
1869
|
+
*/
|
|
1870
|
+
export async function findBaseFormXml(baseFormName, symbolIndex) {
|
|
1871
|
+
// Helper: read a file, transparently following JSON metadata proxies.
|
|
1872
|
+
async function tryRead(p) {
|
|
1873
|
+
try {
|
|
1874
|
+
const raw = await fs.readFile(p, 'utf-8');
|
|
1875
|
+
if (raw.trimStart().startsWith('{')) {
|
|
1876
|
+
const data = JSON.parse(raw);
|
|
1877
|
+
if (data.sourcePath) {
|
|
1878
|
+
try {
|
|
1879
|
+
return await fs.readFile(data.sourcePath, 'utf-8');
|
|
1880
|
+
}
|
|
1881
|
+
catch {
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
return raw;
|
|
1888
|
+
}
|
|
1889
|
+
catch {
|
|
1890
|
+
return null;
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
// 1. Symbol DB lookup
|
|
1894
|
+
let dbFilePath = null;
|
|
1895
|
+
try {
|
|
1896
|
+
const rdb = symbolIndex.getReadDb();
|
|
1897
|
+
const row = rdb.prepare(`SELECT file_path FROM symbols WHERE type = 'form' AND name = ? LIMIT 1`).get(baseFormName);
|
|
1898
|
+
if (row?.file_path)
|
|
1899
|
+
dbFilePath = row.file_path;
|
|
1900
|
+
}
|
|
1901
|
+
catch { /* ignore */ }
|
|
1902
|
+
if (dbFilePath) {
|
|
1903
|
+
// Try absolute DB path as-is
|
|
1904
|
+
const direct = await tryRead(dbFilePath);
|
|
1905
|
+
if (direct)
|
|
1906
|
+
return direct;
|
|
1907
|
+
// DB stored a relative path — join with configured packagePath
|
|
1908
|
+
if (!path.isAbsolute(dbFilePath)) {
|
|
1909
|
+
const cm = getConfigManager();
|
|
1910
|
+
await cm.ensureLoaded();
|
|
1911
|
+
const pkgPath = cm.getPackagePath() || fallbackPackagePath();
|
|
1912
|
+
const abs = await tryRead(path.join(pkgPath, dbFilePath));
|
|
1913
|
+
if (abs)
|
|
1914
|
+
return abs;
|
|
1915
|
+
}
|
|
1916
|
+
// Build-agent path remapping (e.g. /home/vsts/... → local PackagesLocalDirectory)
|
|
1917
|
+
const remapped = await resolveDbPathLocally(dbFilePath);
|
|
1918
|
+
if (remapped) {
|
|
1919
|
+
const content = await tryRead(remapped);
|
|
1920
|
+
if (content)
|
|
1921
|
+
return content;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
// 2. Filesystem scan using model from config
|
|
1925
|
+
const diskPath = await findD365FileOnDisk('form', baseFormName);
|
|
1926
|
+
if (diskPath)
|
|
1927
|
+
return tryRead(diskPath);
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
/**
|
|
1931
|
+
* Resolve a possibly-fuzzy `parentControl` value to the exact control name in the base form.
|
|
1932
|
+
*
|
|
1933
|
+
* Returns:
|
|
1934
|
+
* { resolved, pathStr } — unique case-insensitive substring match (use this name)
|
|
1935
|
+
* { multiple } — ambiguous (return candidates to caller)
|
|
1936
|
+
* null — form not found or no controls matched; caller uses original value
|
|
1937
|
+
*/
|
|
1938
|
+
async function resolveParentControl(extensionObjectName, parentControlQuery, symbolIndex, explicitBaseFormName) {
|
|
1939
|
+
// Base form name: "CustTable.MyExt" → "CustTable"
|
|
1940
|
+
const baseFormName = explicitBaseFormName || extensionObjectName.split('.')[0];
|
|
1941
|
+
if (!baseFormName)
|
|
1942
|
+
return null;
|
|
1943
|
+
const xmlContent = await findBaseFormXml(baseFormName, symbolIndex);
|
|
1944
|
+
if (!xmlContent)
|
|
1945
|
+
return null;
|
|
1946
|
+
let xmlObj;
|
|
1947
|
+
try {
|
|
1948
|
+
xmlObj = await parseStringPromise(xmlContent);
|
|
1949
|
+
}
|
|
1950
|
+
catch {
|
|
1951
|
+
return null;
|
|
1952
|
+
}
|
|
1953
|
+
const all = allControlsFromFormXmlObj(xmlObj);
|
|
1954
|
+
const lq = parentControlQuery.toLowerCase();
|
|
1955
|
+
const matches = all.filter(c => c.name.toLowerCase().includes(lq));
|
|
1956
|
+
if (matches.length === 0)
|
|
1957
|
+
return null; // No match — caller proceeds with original
|
|
1958
|
+
if (matches.length === 1)
|
|
1959
|
+
return { resolved: matches[0].name, pathStr: matches[0].pathStr };
|
|
1960
|
+
// Multiple substring matches — try an exact case-insensitive match first
|
|
1961
|
+
const exact = matches.filter(c => c.name.toLowerCase() === lq);
|
|
1962
|
+
if (exact.length === 1)
|
|
1963
|
+
return { resolved: exact[0].name, pathStr: exact[0].pathStr };
|
|
1964
|
+
return { multiple: matches };
|
|
1965
|
+
}
|
|
1966
|
+
// Tool registration (name, description, inputSchema) lives inline in
|
|
1967
|
+
// src/server/mcpServer.ts - the single source of truth for tool instructions.
|
|
1968
|
+
/**
|
|
1969
|
+
* Resolve the primitive base type for an EDT by walking the edt_metadata chain.
|
|
1970
|
+
* Used by add-field to auto-fill fieldBaseType when the caller omits it.
|
|
1971
|
+
* A newly-created EDT not yet in the index returns the EDT name so the bridge
|
|
1972
|
+
* can still apply its own name-based heuristics.
|
|
1973
|
+
*/
|
|
1974
|
+
function resolveEdtBaseTypeForField(edtName, db, depth = 0) {
|
|
1975
|
+
const PRIMITIVES = new Set([
|
|
1976
|
+
'String', 'Integer', 'Int64', 'Real', 'Date', 'UtcDateTime', 'DateTime',
|
|
1977
|
+
'Enum', 'Container', 'Guid', 'GUID',
|
|
1978
|
+
]);
|
|
1979
|
+
if (depth > 8)
|
|
1980
|
+
return edtName;
|
|
1981
|
+
if (PRIMITIVES.has(edtName))
|
|
1982
|
+
return edtName;
|
|
1983
|
+
try {
|
|
1984
|
+
const row = db.prepare(`SELECT extends, enum_type FROM edt_metadata WHERE edt_name = ? LIMIT 1`).get(edtName);
|
|
1985
|
+
if (!row)
|
|
1986
|
+
return edtName; // not yet indexed — let bridge use name heuristics
|
|
1987
|
+
if (row.enum_type && !row.extends)
|
|
1988
|
+
return 'Enum';
|
|
1989
|
+
if (!row.extends)
|
|
1990
|
+
return edtName;
|
|
1991
|
+
if (PRIMITIVES.has(row.extends))
|
|
1992
|
+
return row.extends;
|
|
1993
|
+
return resolveEdtBaseTypeForField(row.extends, db, depth + 1);
|
|
1994
|
+
}
|
|
1995
|
+
catch {
|
|
1996
|
+
return edtName;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
/** Lower-case the first character — D365FO convention for a table buffer variable. */
|
|
2000
|
+
function bufferVarName(tableName) {
|
|
2001
|
+
return tableName.charAt(0).toLowerCase() + tableName.slice(1);
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Resolve the EDT/type of a table field from the symbol index so generated
|
|
2005
|
+
* find/exist signatures use the correct parameter type. The field's `signature`
|
|
2006
|
+
* column stores its EDT name. Returns null when the field is not indexed.
|
|
2007
|
+
*/
|
|
2008
|
+
/** Metadata base-type keywords (the stripped i:type, e.g. AxTableFieldString → "String").
|
|
2009
|
+
* These are NOT valid X++ parameter types — a field's signature in the index stores
|
|
2010
|
+
* the base type, not its EDT, so a resolved value matching one of these must be
|
|
2011
|
+
* discarded in favour of the field name (D365 fields are conventionally named after
|
|
2012
|
+
* their EDT, e.g. field ItemId uses EDT ItemId). */
|
|
2013
|
+
const METADATA_BASE_TYPE_KEYWORDS = new Set([
|
|
2014
|
+
'string', 'integer', 'int', 'int64', 'real', 'date', 'time', 'datetime',
|
|
2015
|
+
'utcdatetime', 'guid', 'container', 'enum', 'boolean', 'memo',
|
|
2016
|
+
]);
|
|
2017
|
+
function isMetadataBaseTypeKeyword(t) {
|
|
2018
|
+
return METADATA_BASE_TYPE_KEYWORDS.has(t.trim().toLowerCase());
|
|
2019
|
+
}
|
|
2020
|
+
function resolveFieldEdt(tableName, fieldName, db) {
|
|
2021
|
+
try {
|
|
2022
|
+
// Canonicalize the parent — `parent_name = ? COLLATE NOCASE` cannot use
|
|
2023
|
+
// idx_parent_type_name and scans all 360k field rows (180 s cold). NOCASE
|
|
2024
|
+
// on the field name is fine inside the indexed parent+type range.
|
|
2025
|
+
const canonical = lookupSymbolNocase(db, tableName)?.name ?? tableName;
|
|
2026
|
+
const row = db.prepare(`SELECT signature FROM symbols WHERE type = 'field' AND parent_name = ? AND name = ? COLLATE NOCASE LIMIT 1`).get(canonical, fieldName);
|
|
2027
|
+
const sig = row?.signature?.trim();
|
|
2028
|
+
// The index stores the field's BASE TYPE (e.g. "String"), not its EDT. A base-type
|
|
2029
|
+
// keyword is not a usable X++ parameter type, so treat it as unresolved — the caller
|
|
2030
|
+
// then falls back to the field name, which is conventionally the EDT.
|
|
2031
|
+
if (!sig || isMetadataBaseTypeKeyword(sig))
|
|
2032
|
+
return null;
|
|
2033
|
+
return sig;
|
|
2034
|
+
}
|
|
2035
|
+
catch {
|
|
2036
|
+
return null;
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Generate idiomatic X++ source for a standard table method from a high-level
|
|
2041
|
+
* `tableMethodType`. The method name is implied by the type (find/exist/…), so
|
|
2042
|
+
* callers need not pass methodName or sourceCode — only tableMethodType (plus
|
|
2043
|
+
* tableKeyField for find/exist).
|
|
2044
|
+
*
|
|
2045
|
+
* Returns the generated method name, source, and an optional advisory note
|
|
2046
|
+
* (e.g. when the key field's EDT could not be resolved from the index).
|
|
2047
|
+
*/
|
|
2048
|
+
export function generateTableMethodSource(tableName, methodType, keyField, db) {
|
|
2049
|
+
const buf = bufferVarName(tableName);
|
|
2050
|
+
switch (methodType) {
|
|
2051
|
+
case 'find':
|
|
2052
|
+
case 'exist': {
|
|
2053
|
+
if (!keyField) {
|
|
2054
|
+
throw new Error(`add-table-method with tableMethodType="${methodType}" requires tableKeyField ` +
|
|
2055
|
+
`(the primary key field to select on, e.g. tableKeyField="ItemId").`);
|
|
2056
|
+
}
|
|
2057
|
+
const resolvedEdt = resolveFieldEdt(tableName, keyField, db);
|
|
2058
|
+
const paramType = resolvedEdt ?? keyField;
|
|
2059
|
+
const note = resolvedEdt
|
|
2060
|
+
? undefined
|
|
2061
|
+
: `⚠️ Could not resolve the EDT for field "${keyField}" on "${tableName}" from the index — ` +
|
|
2062
|
+
`the parameter type defaulted to "${keyField}". Verify it compiles, or pass full sourceCode instead.`;
|
|
2063
|
+
const param = `_${bufferVarName(keyField)}`;
|
|
2064
|
+
if (methodType === 'find') {
|
|
2065
|
+
const source = `/// <summary>\n` +
|
|
2066
|
+
`/// Finds the <c>${tableName}</c> record for the given ${keyField}.\n` +
|
|
2067
|
+
`/// </summary>\n` +
|
|
2068
|
+
`/// <param name = "${param}">The ${keyField} value to find.</param>\n` +
|
|
2069
|
+
`/// <param name = "_forUpdate">Select the record for update; optional.</param>\n` +
|
|
2070
|
+
`/// <returns>The matching <c>${tableName}</c> record; an empty buffer if none exists.</returns>\n` +
|
|
2071
|
+
`public static ${tableName} find(${paramType} ${param}, boolean _forUpdate = false)\n` +
|
|
2072
|
+
`{\n` +
|
|
2073
|
+
` ${tableName} ${buf};\n` +
|
|
2074
|
+
`\n` +
|
|
2075
|
+
` if (${param})\n` +
|
|
2076
|
+
` {\n` +
|
|
2077
|
+
` ${buf}.selectForUpdate(_forUpdate);\n` +
|
|
2078
|
+
`\n` +
|
|
2079
|
+
` select firstonly ${buf}\n` +
|
|
2080
|
+
` where ${buf}.${keyField} == ${param};\n` +
|
|
2081
|
+
` }\n` +
|
|
2082
|
+
`\n` +
|
|
2083
|
+
` return ${buf};\n` +
|
|
2084
|
+
`}`;
|
|
2085
|
+
return { methodName: 'find', source, note };
|
|
2086
|
+
}
|
|
2087
|
+
// exist
|
|
2088
|
+
const source = `/// <summary>\n` +
|
|
2089
|
+
`/// Checks whether a <c>${tableName}</c> record exists for the given ${keyField}.\n` +
|
|
2090
|
+
`/// </summary>\n` +
|
|
2091
|
+
`/// <param name = "${param}">The ${keyField} value to check.</param>\n` +
|
|
2092
|
+
`/// <returns>true if a matching record exists; otherwise false.</returns>\n` +
|
|
2093
|
+
`public static boolean exist(${paramType} ${param})\n` +
|
|
2094
|
+
`{\n` +
|
|
2095
|
+
` return ${param} &&\n` +
|
|
2096
|
+
` (select firstonly RecId from ${buf}\n` +
|
|
2097
|
+
` where ${buf}.${keyField} == ${param}).RecId != 0;\n` +
|
|
2098
|
+
`}`;
|
|
2099
|
+
return { methodName: 'exist', source, note };
|
|
2100
|
+
}
|
|
2101
|
+
case 'findByRecId': {
|
|
2102
|
+
const source = `/// <summary>\n` +
|
|
2103
|
+
`/// Finds the <c>${tableName}</c> record for the given RecId.\n` +
|
|
2104
|
+
`/// </summary>\n` +
|
|
2105
|
+
`/// <param name = "_recId">The RecId to find.</param>\n` +
|
|
2106
|
+
`/// <param name = "_forUpdate">Select the record for update; optional.</param>\n` +
|
|
2107
|
+
`/// <returns>The matching <c>${tableName}</c> record; an empty buffer if none exists.</returns>\n` +
|
|
2108
|
+
`public static ${tableName} findByRecId(RefRecId _recId, boolean _forUpdate = false)\n` +
|
|
2109
|
+
`{\n` +
|
|
2110
|
+
` ${tableName} ${buf};\n` +
|
|
2111
|
+
`\n` +
|
|
2112
|
+
` if (_recId)\n` +
|
|
2113
|
+
` {\n` +
|
|
2114
|
+
` ${buf}.selectForUpdate(_forUpdate);\n` +
|
|
2115
|
+
`\n` +
|
|
2116
|
+
` select firstonly ${buf}\n` +
|
|
2117
|
+
` where ${buf}.RecId == _recId;\n` +
|
|
2118
|
+
` }\n` +
|
|
2119
|
+
`\n` +
|
|
2120
|
+
` return ${buf};\n` +
|
|
2121
|
+
`}`;
|
|
2122
|
+
return { methodName: 'findByRecId', source };
|
|
2123
|
+
}
|
|
2124
|
+
case 'validateWrite':
|
|
2125
|
+
case 'validateDelete': {
|
|
2126
|
+
const source = `/// <summary>\n` +
|
|
2127
|
+
`/// Validates the record before ${methodType === 'validateWrite' ? 'writing' : 'deletion'}.\n` +
|
|
2128
|
+
`/// </summary>\n` +
|
|
2129
|
+
`/// <returns>true if the record is valid; otherwise false.</returns>\n` +
|
|
2130
|
+
`public boolean ${methodType}()\n` +
|
|
2131
|
+
`{\n` +
|
|
2132
|
+
` boolean ret;\n` +
|
|
2133
|
+
`\n` +
|
|
2134
|
+
` ret = super();\n` +
|
|
2135
|
+
`\n` +
|
|
2136
|
+
` // TODO: add custom ${methodType} validation\n` +
|
|
2137
|
+
`\n` +
|
|
2138
|
+
` return ret;\n` +
|
|
2139
|
+
`}`;
|
|
2140
|
+
return { methodName: methodType, source };
|
|
2141
|
+
}
|
|
2142
|
+
case 'initValue': {
|
|
2143
|
+
const source = `/// <summary>\n` +
|
|
2144
|
+
`/// Initializes the record with default field values.\n` +
|
|
2145
|
+
`/// </summary>\n` +
|
|
2146
|
+
`public void initValue()\n` +
|
|
2147
|
+
`{\n` +
|
|
2148
|
+
` super();\n` +
|
|
2149
|
+
`\n` +
|
|
2150
|
+
` // TODO: set default field values\n` +
|
|
2151
|
+
`}`;
|
|
2152
|
+
return { methodName: 'initValue', source };
|
|
2153
|
+
}
|
|
2154
|
+
default: {
|
|
2155
|
+
// Exhaustiveness guard — the schema enum should keep this unreachable.
|
|
2156
|
+
throw new Error(`Unsupported tableMethodType: ${methodType}`);
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
/**
|
|
2161
|
+
* Generate an X++ display method stub returning the given EDT/type.
|
|
2162
|
+
* Used when add-display-method is called with displayMethodReturnEdt but no
|
|
2163
|
+
* explicit sourceCode/methodCode.
|
|
2164
|
+
*/
|
|
2165
|
+
export function generateDisplayMethodSource(methodName, returnEdt) {
|
|
2166
|
+
return (`/// <summary>\n` +
|
|
2167
|
+
`/// Display method returning <c>${returnEdt}</c>.\n` +
|
|
2168
|
+
`/// </summary>\n` +
|
|
2169
|
+
`/// <returns>The computed ${returnEdt} value.</returns>\n` +
|
|
2170
|
+
`public display ${returnEdt} ${methodName}()\n` +
|
|
2171
|
+
`{\n` +
|
|
2172
|
+
` ${returnEdt} ret;\n` +
|
|
2173
|
+
`\n` +
|
|
2174
|
+
` // TODO: compute the display value\n` +
|
|
2175
|
+
`\n` +
|
|
2176
|
+
` return ret;\n` +
|
|
2177
|
+
`}`);
|
|
2178
|
+
}
|
|
2179
|
+
//# sourceMappingURL=modifyD365File.js.map
|