@documentation-robotics/cli 0.1.13 → 0.3.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/README.md +74 -2
- package/dist/analysis/relationship-classifier.js +2 -9
- package/dist/analysis/relationship-classifier.js.map +2 -2
- package/dist/analyzers/call-graph-utils.js +2 -2
- package/dist/analyzers/call-graph-utils.js.map +2 -2
- package/dist/analyzers/cards/build.js +171 -0
- package/dist/analyzers/cards/build.js.map +7 -0
- package/dist/analyzers/cards/classify.js +42 -0
- package/dist/analyzers/cards/classify.js.map +7 -0
- package/dist/analyzers/cards/edge-index.js +44 -0
- package/dist/analyzers/cards/edge-index.js.map +7 -0
- package/dist/analyzers/cards/facts.js +46 -0
- package/dist/analyzers/cards/facts.js.map +7 -0
- package/dist/analyzers/cards/graph.js +42 -0
- package/dist/analyzers/cards/graph.js.map +7 -0
- package/dist/analyzers/cards/index.js +8 -0
- package/dist/analyzers/cards/index.js.map +7 -0
- package/dist/analyzers/cards/tier0.js +18 -0
- package/dist/analyzers/cards/tier0.js.map +7 -0
- package/dist/analyzers/cards/types.js +1 -0
- package/dist/analyzers/cards/types.js.map +7 -0
- package/dist/analyzers/cbm-analyzer.js +195 -171
- package/dist/analyzers/cbm-analyzer.js.map +2 -2
- package/dist/analyzers/cbm-responses.js +336 -0
- package/dist/analyzers/cbm-responses.js.map +7 -0
- package/dist/analyzers/edge-relationships.js +45 -0
- package/dist/analyzers/edge-relationships.js.map +7 -0
- package/dist/analyzers/mapping-loader.js +9 -0
- package/dist/analyzers/mapping-loader.js.map +2 -2
- package/dist/analyzers/tier0-rules.js +153 -0
- package/dist/analyzers/tier0-rules.js.map +7 -0
- package/dist/analyzers/types.js.map +1 -1
- package/dist/analyzers/verify-engine.js +22 -19
- package/dist/analyzers/verify-engine.js.map +2 -2
- package/dist/audit/relationships/ai/prompts.js +10 -10
- package/dist/audit/relationships/ai/prompts.js.map +2 -2
- package/dist/audit/relationships/analysis/layer-templates.js +52 -52
- package/dist/audit/relationships/analysis/layer-templates.js.map +1 -1
- package/dist/cli.js +112 -28
- package/dist/cli.js.map +3 -3
- package/dist/coding-agents/agents/claude-code.js +3 -1
- package/dist/coding-agents/agents/claude-code.js.map +2 -2
- package/dist/coding-agents/claude-code-client.js +2 -1
- package/dist/coding-agents/claude-code-client.js.map +2 -2
- package/dist/coding-agents/copilot-client.js +17 -12
- package/dist/coding-agents/copilot-client.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +152 -0
- package/dist/coding-agents/default-permissions.js.map +7 -0
- package/dist/coding-agents/drbot-mcp.js +167 -0
- package/dist/coding-agents/drbot-mcp.js.map +7 -0
- package/dist/commands/add.js +12 -7
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +134 -23
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/audit.js +6 -6
- package/dist/commands/audit.js.map +2 -2
- package/dist/commands/changeset-verify.js +55 -0
- package/dist/commands/changeset-verify.js.map +7 -0
- package/dist/commands/changeset.js +261 -147
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +52 -19
- package/dist/commands/delete.js.map +3 -3
- package/dist/commands/element.js +3 -2
- package/dist/commands/element.js.map +2 -2
- package/dist/commands/farm.js +773 -0
- package/dist/commands/farm.js.map +7 -0
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +122 -0
- package/dist/commands/mcp.js.map +7 -0
- package/dist/commands/relationship.js +251 -47
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +56 -24
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/upgrade.js +99 -11
- package/dist/commands/upgrade.js.map +3 -3
- package/dist/commands/validate.js +41 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +2 -2
- package/dist/commands/version.js.map +2 -2
- package/dist/commands/visualize.js +4 -3
- package/dist/commands/visualize.js.map +2 -2
- package/dist/config.js +83 -0
- package/dist/config.js.map +7 -0
- package/dist/core/annotation-store.js +138 -0
- package/dist/core/annotation-store.js.map +7 -0
- package/dist/core/catalog-migration.js +33 -0
- package/dist/core/catalog-migration.js.map +7 -0
- package/dist/core/changeset-analysis.js +421 -0
- package/dist/core/changeset-analysis.js.map +7 -0
- package/dist/core/changeset-base-index.js +153 -0
- package/dist/core/changeset-base-index.js.map +7 -0
- package/dist/core/changeset-brief-delta.js +102 -0
- package/dist/core/changeset-brief-delta.js.map +7 -0
- package/dist/core/changeset-graph.js +281 -0
- package/dist/core/changeset-graph.js.map +7 -0
- package/dist/core/changeset-only-guard.js +32 -0
- package/dist/core/changeset-only-guard.js.map +7 -0
- package/dist/core/changeset-summary.js +68 -0
- package/dist/core/changeset-summary.js.map +7 -0
- package/dist/core/changeset-validator.js +66 -20
- package/dist/core/changeset-validator.js.map +2 -2
- package/dist/core/changeset-verify.js +518 -0
- package/dist/core/changeset-verify.js.map +7 -0
- package/dist/core/changeset.js +56 -9
- package/dist/core/changeset.js.map +2 -2
- package/dist/core/element-type-migration.js +145 -0
- package/dist/core/element-type-migration.js.map +7 -0
- package/dist/core/farm-manifest.js +140 -0
- package/dist/core/farm-manifest.js.map +7 -0
- package/dist/core/farm-sync-engine.js +346 -0
- package/dist/core/farm-sync-engine.js.map +7 -0
- package/dist/core/farm-sync-state.js +167 -0
- package/dist/core/farm-sync-state.js.map +7 -0
- package/dist/core/manifest.js +6 -3
- package/dist/core/manifest.js.map +2 -2
- package/dist/core/migration-registry.js +84 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model-upgrade.js +102 -0
- package/dist/core/model-upgrade.js.map +7 -0
- package/dist/core/model.js +257 -44
- package/dist/core/model.js.map +3 -3
- package/dist/core/mutation-handler.js +71 -6
- package/dist/core/mutation-handler.js.map +2 -2
- package/dist/core/relationship-migration-table.js +6639 -0
- package/dist/core/relationship-migration-table.js.map +7 -0
- package/dist/core/relationship-migration.js +272 -0
- package/dist/core/relationship-migration.js.map +7 -0
- package/dist/core/relationship-prune.js +49 -0
- package/dist/core/relationship-prune.js.map +7 -0
- package/dist/core/relationship-staging.js +68 -0
- package/dist/core/relationship-staging.js.map +7 -0
- package/dist/core/relationships.js +26 -4
- package/dist/core/relationships.js.map +2 -2
- package/dist/core/report-data-model.js +7 -13
- package/dist/core/report-data-model.js.map +2 -2
- package/dist/core/staged-changeset-storage.js +64 -17
- package/dist/core/staged-changeset-storage.js.map +2 -2
- package/dist/core/staged-source-reference.js +28 -0
- package/dist/core/staged-source-reference.js.map +7 -0
- package/dist/core/staging-area.js +83 -18
- package/dist/core/staging-area.js.map +2 -2
- package/dist/core/virtual-projection.js +14 -5
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/export/archimate-exporter.js +1 -0
- package/dist/export/archimate-exporter.js.map +2 -2
- package/dist/generated/compiled-validators.js +2 -2
- package/dist/generated/compiled-validators.js.map +2 -2
- package/dist/generated/layer-registry.js +16 -16
- package/dist/generated/layer-registry.js.map +2 -2
- package/dist/generated/node-types.js +186 -168
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +2209 -7727
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/agents/dr-architect.md +16 -3
- package/dist/integrations/claude_code/agents/dr-extractor.md +1 -1
- package/dist/integrations/claude_code/commands/dr-analyzer.md +565 -0
- package/dist/integrations/claude_code/commands/dr-map.md +13 -0
- package/dist/integrations/claude_code/commands/dr-plan.md +40 -0
- package/dist/integrations/claude_code/commands/dr-relate.md +106 -96
- package/dist/integrations/claude_code/commands/dr-verify.md +45 -2
- package/dist/integrations/claude_code/docs/END_TO_END_WALKTHROUGH.md +14 -0
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +79 -0
- package/dist/integrations/claude_code/docs/QUICK_START.md +37 -8
- package/dist/integrations/claude_code/docs/README.md +52 -1
- package/dist/integrations/claude_code/skills/dr_01_motivation_layer/SKILL.md +93 -72
- package/dist/integrations/claude_code/skills/dr_02_business_layer/SKILL.md +163 -126
- package/dist/integrations/claude_code/skills/dr_03_product_layer/SKILL.md +161 -126
- package/dist/integrations/claude_code/skills/dr_04_security_layer/SKILL.md +153 -133
- package/dist/integrations/claude_code/skills/dr_05_application_layer/SKILL.md +129 -100
- package/dist/integrations/claude_code/skills/dr_06_technology_layer/SKILL.md +111 -94
- package/dist/integrations/claude_code/skills/dr_07_api_layer/SKILL.md +143 -157
- package/dist/integrations/claude_code/skills/dr_08_data_model_layer/SKILL.md +117 -33
- package/dist/integrations/claude_code/skills/dr_09_dataset_layer/SKILL.md +74 -25
- package/dist/integrations/claude_code/skills/dr_10_ux_layer/SKILL.md +88 -12
- package/dist/integrations/claude_code/skills/dr_11_navigation_layer/SKILL.md +83 -24
- package/dist/integrations/claude_code/skills/dr_12_apm_layer/SKILL.md +57 -12
- package/dist/integrations/claude_code/skills/dr_13_testing_layer/SKILL.md +92 -15
- package/dist/integrations/claude_code/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/claude_code/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/claude_code/skills/dr_engineering_guide/SKILL.md +1 -1
- package/dist/integrations/github_copilot/agents/dr-architect.md +1 -1
- package/dist/integrations/github_copilot/skills/dr_01_motivation_layer/SKILL.md +182 -102
- package/dist/integrations/github_copilot/skills/dr_02_business_layer/SKILL.md +266 -148
- package/dist/integrations/github_copilot/skills/dr_03_product_layer/SKILL.md +547 -86
- package/dist/integrations/github_copilot/skills/dr_04_security_layer/SKILL.md +164 -169
- package/dist/integrations/github_copilot/skills/dr_05_application_layer/SKILL.md +420 -150
- package/dist/integrations/github_copilot/skills/dr_06_technology_layer/SKILL.md +331 -138
- package/dist/integrations/github_copilot/skills/dr_07_api_layer/SKILL.md +265 -201
- package/dist/integrations/github_copilot/skills/dr_08_data_model_layer/SKILL.md +181 -58
- package/dist/integrations/github_copilot/skills/dr_09_dataset_layer/SKILL.md +209 -57
- package/dist/integrations/github_copilot/skills/dr_10_ux_layer/SKILL.md +277 -112
- package/dist/integrations/github_copilot/skills/dr_11_navigation_layer/SKILL.md +219 -70
- package/dist/integrations/github_copilot/skills/dr_12_apm_layer/SKILL.md +189 -105
- package/dist/integrations/github_copilot/skills/dr_13_testing_layer/SKILL.md +222 -102
- package/dist/integrations/github_copilot/skills/dr_change_planning/SKILL.md +119 -0
- package/dist/integrations/github_copilot/skills/dr_changeset_reviewer/SKILL.md +3 -0
- package/dist/integrations/hash-utils.js.map +2 -2
- package/dist/mcp/api-key-manager.js +111 -0
- package/dist/mcp/api-key-manager.js.map +7 -0
- package/dist/mcp/changeset-only.js +34 -0
- package/dist/mcp/changeset-only.js.map +7 -0
- package/dist/mcp/http-transport.js +176 -0
- package/dist/mcp/http-transport.js.map +7 -0
- package/dist/mcp/resource-registry.js +97 -0
- package/dist/mcp/resource-registry.js.map +7 -0
- package/dist/mcp/tool-registry.js +60 -0
- package/dist/mcp/tool-registry.js.map +7 -0
- package/dist/mcp/tools/annotation-create.js +39 -0
- package/dist/mcp/tools/annotation-create.js.map +7 -0
- package/dist/mcp/tools/annotation-delete.js +29 -0
- package/dist/mcp/tools/annotation-delete.js.map +7 -0
- package/dist/mcp/tools/annotation-get.js +29 -0
- package/dist/mcp/tools/annotation-get.js.map +7 -0
- package/dist/mcp/tools/annotation-list.js +24 -0
- package/dist/mcp/tools/annotation-list.js.map +7 -0
- package/dist/mcp/tools/annotation-reply.js +34 -0
- package/dist/mcp/tools/annotation-reply.js.map +7 -0
- package/dist/mcp/tools/annotation-shared.js +10 -0
- package/dist/mcp/tools/annotation-shared.js.map +7 -0
- package/dist/mcp/tools/annotation-update.js +39 -0
- package/dist/mcp/tools/annotation-update.js.map +7 -0
- package/dist/mcp/tools/changeset-analysis-tools.js +78 -0
- package/dist/mcp/tools/changeset-analysis-tools.js.map +7 -0
- package/dist/mcp/tools/changeset-editing.js +184 -0
- package/dist/mcp/tools/changeset-editing.js.map +7 -0
- package/dist/mcp/tools/changeset-lifecycle.js +83 -0
- package/dist/mcp/tools/changeset-lifecycle.js.map +7 -0
- package/dist/mcp/tools/changeset-list.js +33 -0
- package/dist/mcp/tools/changeset-list.js.map +7 -0
- package/dist/mcp/tools/changeset-shared.js +31 -0
- package/dist/mcp/tools/changeset-shared.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +55 -0
- package/dist/mcp/tools/changeset-show.js.map +7 -0
- package/dist/mcp/tools/changeset-verify.js +43 -0
- package/dist/mcp/tools/changeset-verify.js.map +7 -0
- package/dist/mcp/tools/chat-launch.js +50 -0
- package/dist/mcp/tools/chat-launch.js.map +7 -0
- package/dist/mcp/tools/chat-status.js +26 -0
- package/dist/mcp/tools/chat-status.js.map +7 -0
- package/dist/mcp/tools/index.js +65 -0
- package/dist/mcp/tools/index.js.map +7 -0
- package/dist/mcp/tools/model-add.js +107 -0
- package/dist/mcp/tools/model-add.js.map +7 -0
- package/dist/mcp/tools/model-delete.js +99 -0
- package/dist/mcp/tools/model-delete.js.map +7 -0
- package/dist/mcp/tools/model-export.js +72 -0
- package/dist/mcp/tools/model-export.js.map +7 -0
- package/dist/mcp/tools/model-info.js +51 -0
- package/dist/mcp/tools/model-info.js.map +7 -0
- package/dist/mcp/tools/model-list.js +53 -0
- package/dist/mcp/tools/model-list.js.map +7 -0
- package/dist/mcp/tools/model-reload.js +34 -0
- package/dist/mcp/tools/model-reload.js.map +7 -0
- package/dist/mcp/tools/model-search.js +102 -0
- package/dist/mcp/tools/model-search.js.map +7 -0
- package/dist/mcp/tools/model-show.js +68 -0
- package/dist/mcp/tools/model-show.js.map +7 -0
- package/dist/mcp/tools/model-stats.js +22 -0
- package/dist/mcp/tools/model-stats.js.map +7 -0
- package/dist/mcp/tools/model-trace.js +118 -0
- package/dist/mcp/tools/model-trace.js.map +7 -0
- package/dist/mcp/tools/model-update.js +121 -0
- package/dist/mcp/tools/model-update.js.map +7 -0
- package/dist/mcp/tools/model-validate.js +78 -0
- package/dist/mcp/tools/model-validate.js.map +7 -0
- package/dist/mcp/tools/relationship-tools.js +98 -0
- package/dist/mcp/tools/relationship-tools.js.map +7 -0
- package/dist/mcp/tools/shared.js +126 -0
- package/dist/mcp/tools/shared.js.map +7 -0
- package/dist/mcp/tools/spec-type.js +90 -0
- package/dist/mcp/tools/spec-type.js.map +7 -0
- package/dist/reports/model-layer-report-generator.js +5 -3
- package/dist/reports/model-layer-report-generator.js.map +2 -2
- package/dist/reports/model-readme-generator.js +116 -0
- package/dist/reports/model-readme-generator.js.map +7 -0
- package/dist/reports/model-report-data.js +37 -1
- package/dist/reports/model-report-data.js.map +2 -2
- package/dist/reports/model-report-orchestrator.js +29 -6
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/reports/model-report-utils.js +9 -0
- package/dist/reports/model-report-utils.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +1186 -68
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1029 -1658
- package/dist/schemas/bundled/apm.json +499 -734
- package/dist/schemas/bundled/application.json +745 -370
- package/dist/schemas/bundled/base.json +3 -220
- package/dist/schemas/bundled/business.json +452 -366
- package/dist/schemas/bundled/data-model.json +403 -383
- package/dist/schemas/bundled/data-store.json +334 -558
- package/dist/schemas/bundled/manifest.json +18 -18
- package/dist/schemas/bundled/motivation.json +349 -368
- package/dist/schemas/bundled/navigation.json +415 -731
- package/dist/schemas/bundled/product.json +325 -159
- package/dist/schemas/bundled/security.json +864 -1232
- package/dist/schemas/bundled/technology.json +574 -703
- package/dist/schemas/bundled/testing.json +718 -1643
- package/dist/schemas/bundled/ux.json +739 -1352
- package/dist/server/schemas.js +8 -0
- package/dist/server/schemas.js.map +2 -2
- package/dist/server/server.js +234 -31
- package/dist/server/server.js.map +2 -2
- package/dist/server-entry.js +2 -1
- package/dist/server-entry.js.map +2 -2
- package/dist/telemetry/config.js +2 -40
- package/dist/telemetry/config.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/types/index.js.map +2 -2
- package/dist/utils/ansi-suppressor.js +23 -0
- package/dist/utils/ansi-suppressor.js.map +7 -0
- package/dist/utils/attribute-patch.js +41 -0
- package/dist/utils/attribute-patch.js.map +7 -0
- package/dist/utils/color-utils.js +34 -0
- package/dist/utils/color-utils.js.map +7 -0
- package/dist/utils/element-reference-shape.js +15 -1
- package/dist/utils/element-reference-shape.js.map +3 -3
- package/dist/utils/error-hints.js +29 -0
- package/dist/utils/error-hints.js.map +7 -0
- package/dist/utils/errors.js +75 -10
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/globals.js +17 -1
- package/dist/utils/globals.js.map +2 -2
- package/dist/utils/project-paths.js +50 -3
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/reference-path-parser.js +49 -0
- package/dist/utils/reference-path-parser.js.map +7 -0
- package/dist/utils/source-reference.js +17 -10
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +10 -2
- package/dist/utils/spec-version.js.map +2 -2
- package/dist/validators/composed-reference-validator.js +240 -0
- package/dist/validators/composed-reference-validator.js.map +7 -0
- package/dist/validators/reference-validator.js +57 -13
- package/dist/validators/reference-validator.js.map +3 -3
- package/dist/validators/relationship-schema-validator.js +33 -5
- package/dist/validators/relationship-schema-validator.js.map +3 -3
- package/dist/validators/schema-validator.js +27 -9
- package/dist/validators/schema-validator.js.map +2 -2
- package/dist/validators/types.js.map +1 -1
- package/dist/viewer/README.md +6 -6
- package/dist/viewer/assets/main-CxzcuU_o.js +84 -0
- package/dist/viewer/assets/main-j-7ULYpm.css +1 -0
- package/dist/viewer/index.html +2 -2
- package/dist/viewer/manifest.json +10 -10
- package/dist/viewer/version.json +1 -1
- package/package.json +7 -3
- package/dist/viewer/assets/main-B7AOQXXX.js +0 -81
- package/dist/viewer/assets/main-vdrFrG7q.css +0 -1
|
@@ -33,7 +33,7 @@ async function visualizeCommand(options) {
|
|
|
33
33
|
port = portNum;
|
|
34
34
|
}
|
|
35
35
|
port = await findAvailablePort(port), port !== (options.port ? parseInt(String(options.port), 10) : 8080) && console.log(ansis.yellow(` Port ${options.port ?? 8080} is in use, using port ${port} instead`));
|
|
36
|
-
const authEnabled = !options.noAuth, authToken = options.token, withDanger = options.withDanger || !1;
|
|
36
|
+
const authEnabled = !options.noAuth, authToken = options.token, withDanger = options.withDanger || !1, withChangesets = (options.withChangesets || !1) && !withDanger;
|
|
37
37
|
logDebug(`Starting visualization server on port ${port}`), logDebug(`Authentication: ${authEnabled ? "enabled" : "disabled"}`), authEnabled && authToken ? logDebug("Using provided token for authentication") : authEnabled && logDebug("Token will be auto-generated"), withDanger && logDebug("Danger mode enabled - chat permissions will be skipped");
|
|
38
38
|
const commandDir = dirname(fileURLToPath(import.meta.url)), serverEntryPath = `${dirname(commandDir)}/server-entry.js`;
|
|
39
39
|
logDebug(`Server entry path: ${serverEntryPath}`);
|
|
@@ -53,7 +53,8 @@ async function visualizeCommand(options) {
|
|
|
53
53
|
DR_VISUALIZE_PORT: String(port),
|
|
54
54
|
DR_PROJECT_PATH: process.cwd(),
|
|
55
55
|
DR_AUTH_ENABLED: String(authEnabled),
|
|
56
|
-
DR_WITH_DANGER: String(withDanger)
|
|
56
|
+
DR_WITH_DANGER: String(withDanger),
|
|
57
|
+
DR_WITH_CHANGESETS: String(withChangesets)
|
|
57
58
|
}, { isDebug: getDebugState, isVerbose } = await import("../utils/globals.js");
|
|
58
59
|
getDebugState() && (env.DEBUG = "1", env.DR_TELEMETRY_DEBUG = "1", logDebug("Passing DEBUG=1 and DR_TELEMETRY_DEBUG=1 to subprocess")), isVerbose() && (env.VERBOSE = "1", logDebug("Passing VERBOSE=1 to subprocess")), authToken && (env.DR_AUTH_TOKEN = authToken), options.viewerPath && (env.DR_VIEWER_PATH = options.viewerPath, logDebug(`Using custom viewer from: ${options.viewerPath}`));
|
|
59
60
|
const serverProcess = spawn(process.execPath, [serverEntryPath], {
|
|
@@ -91,7 +92,7 @@ async function visualizeCommand(options) {
|
|
|
91
92
|
authTokenFromServer ? (console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${token}`)), console.log(ansis.dim(` Auth token: ${token}`))) : (console.log(ansis.dim(` Open http://localhost:${port} in your browser`)), logDebug(" (waiting for auth token...)"));
|
|
92
93
|
} else
|
|
93
94
|
console.log(ansis.dim(` Open http://localhost:${port} in your browser`)), console.log(ansis.yellow(" \u26A0 Authentication disabled"));
|
|
94
|
-
withDanger
|
|
95
|
+
withDanger ? (console.log(ansis.yellow(" \u26A0 Danger mode enabled - chat permissions will be skipped")), options.withChangesets && console.log(ansis.dim(" (--with-changesets ignored: danger mode already allows all writes)"))) : withChangesets && console.log(ansis.yellow(" \u26A0 Changeset mode enabled - chat can stage model changes in a changeset (no commit/apply/delete)")), console.log(ansis.blueBright(`Model name: ${model.manifest.name}`));
|
|
95
96
|
const drFolder = await getSpecReferencePath();
|
|
96
97
|
console.log(`DR folder: ${ansis.cyan(drFolder || "Not found")}`), console.log(`Model path: ${ansis.cyan(await getModelPath() || "Not found")}`);
|
|
97
98
|
const shouldOpenBrowser = !options.noBrowser;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/commands/visualize.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Visualize command - Launch visualization server\n */\n\nimport ansis from \"ansis\"\nimport { spawn } from \"child_process\"\nimport { fileURLToPath } from \"node:url\"\nimport { dirname } from \"node:path\"\nimport { Model } from \"../core/model.js\"\nimport { logVerbose, logDebug } from \"../utils/globals.js\"\nimport { CLIError } from \"../utils/errors.js\"\nimport { getSpecReferencePath, getModelPath } from \"../utils/project-paths.js\"\nimport { getErrorMessage } from \"../utils/errors.js\"\n\n// Conditional telemetry import based on compile-time flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false\n\nexport interface VisualizeOptions {\n port?: number\n noBrowser?: boolean\n noAuth?: boolean\n token?: string\n verbose?: boolean\n debug?: boolean\n withDanger?: boolean\n viewerPath?: string\n}\n\nasync function findAvailablePort(startPort: number): Promise<number> {\n const { createServer } = await import(\"net\")\n for (let port = startPort; port <= 65535; port++) {\n const available = await new Promise<boolean>((resolve) => {\n const server = createServer()\n server.once(\"error\", () => resolve(false))\n server.once(\"listening\", () => { server.close(); resolve(true) })\n server.listen(port, \"127.0.0.1\")\n })\n if (available) return port\n }\n throw new Error(\"No available ports found\")\n}\n\nexport async function visualizeCommand(\n options: VisualizeOptions & { port?: string }\n): Promise<void> {\n try {\n logDebug(\"Loading model for validation...\")\n\n // Load model to validate it exists (without full content to be quick)\n const model = await Model.load(process.cwd(), { lazyLoad: true })\n\n logVerbose(`Model validated: ${model.manifest.name}`)\n\n // Parse and validate port\n let port = 8080\n if (options.port) {\n const portNum = parseInt(String(options.port), 10)\n if (isNaN(portNum) || portNum < 1 || portNum > 65535) {\n throw new Error(`Invalid port number: ${options.port}. Port must be between 1 and 65535.`)\n }\n port = portNum\n }\n\n // Find an available port starting from the requested port\n port = await findAvailablePort(port)\n if (port !== (options.port ? parseInt(String(options.port), 10) : 8080)) {\n console.log(ansis.yellow(` Port ${options.port ?? 8080} is in use, using port ${port} instead`))\n }\n\n // Determine auth settings\n const authEnabled = !options.noAuth // Auth enabled by default unless --no-auth flag\n const authToken = options.token // Optional token for testing\n const withDanger = options.withDanger || false // Danger mode disabled by default\n\n logDebug(`Starting visualization server on port ${port}`)\n logDebug(`Authentication: ${authEnabled ? \"enabled\" : \"disabled\"}`)\n if (authEnabled && authToken) {\n logDebug(`Using provided token for authentication`)\n } else if (authEnabled) {\n logDebug(`Token will be auto-generated`)\n }\n if (withDanger) {\n logDebug(`Danger mode enabled - chat permissions will be skipped`)\n }\n\n // Get the directory where this command file is located (dist/commands/)\n const commandDir = dirname(fileURLToPath(import.meta.url))\n const distDir = dirname(commandDir) // Go up to dist/\n const serverEntryPath = `${distDir}/server-entry.js`\n\n logDebug(`Server entry path: ${serverEntryPath}`)\n\n // Create telemetry span for server startup\n let serverStartupSpan: any = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Creating visualize.server.startup span\")\n const { startSpan } = await import(\"../telemetry/index.js\")\n serverStartupSpan = startSpan(\"visualize.server.startup\", {\n \"server.port\": port,\n \"server.auth_enabled\": authEnabled,\n \"server.with_danger\": withDanger,\n })\n logDebug(`[Telemetry] Span created: ${serverStartupSpan ? \"success\" : \"failed\"}`)\n } else {\n logDebug(\"[Telemetry] TELEMETRY_ENABLED is false - no spans will be created\")\n }\n\n // Build environment variables for the server subprocess\n const env: Record<string, string> = {\n ...process.env,\n DR_VISUALIZE_PORT: String(port),\n DR_PROJECT_PATH: process.cwd(),\n DR_AUTH_ENABLED: String(authEnabled),\n DR_WITH_DANGER: String(withDanger),\n }\n\n // Pass debug/verbose flags to subprocess\n // The CLI --debug flag sets globalOptions.debug, but subprocess checks process.env.DEBUG\n const { isDebug: getDebugState, isVerbose } = await import(\"../utils/globals.js\")\n if (getDebugState()) {\n env.DEBUG = \"1\"\n env.DR_TELEMETRY_DEBUG = \"1\" // Enable telemetry exporter debug logging\n logDebug(\"Passing DEBUG=1 and DR_TELEMETRY_DEBUG=1 to subprocess\")\n }\n if (isVerbose()) {\n env.VERBOSE = \"1\"\n logDebug(\"Passing VERBOSE=1 to subprocess\")\n }\n\n // Only set auth token if provided\n if (authToken) {\n env.DR_AUTH_TOKEN = authToken\n }\n\n // Set custom viewer path if provided\n if (options.viewerPath) {\n env.DR_VIEWER_PATH = options.viewerPath\n logDebug(`Using custom viewer from: ${options.viewerPath}`)\n }\n\n // Reuse whichever runtime is currently executing the CLI (node or bun) to launch\n // the server subprocess. server-entry.ts has no Bun-specific dependencies (see #800),\n // so this works identically under Node.js or Bun \u2014 and unlike spawning a literal\n // \"bun\" binary, process.execPath is guaranteed to exist since it's already running.\n const serverProcess = spawn(process.execPath, [serverEntryPath], {\n cwd: process.cwd(),\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env,\n })\n\n let authTokenFromServer = \"\"\n let serverOutput = \"\"\n let serverStartupShown = false\n\n // Capture output from server process\n if (serverProcess.stdout) {\n serverProcess.stdout.on(\"data\", async (data) => {\n const chunk = data.toString()\n serverOutput += chunk\n\n // Process each line individually: stdio pipe buffering can batch multiple\n // console.log calls (e.g. \"running at\" + \"TOKEN:\") into a single data event,\n // regardless of runtime.\n // Using startsWith/substring on the whole chunk would either miss the TOKEN line\n // (when \"running at\" is first) or extract a malformed token (when trailing lines\n // are appended), causing the browser to open with the wrong token and get a 403.\n const lines = chunk.split(\"\\n\")\n for (const rawLine of lines) {\n const line = rawLine.trim()\n if (!line) continue\n\n // Extract token from output\n if (line.startsWith(\"TOKEN:\")) {\n authTokenFromServer = line.substring(6)\n logDebug(`Received auth token from server: ${authTokenFromServer.substring(0, 8)}...`)\n\n // If we haven't shown startup info yet and now have the token, show it\n if (serverStartupShown && authEnabled && authTokenFromServer) {\n // Update the URL display with actual token\n console.log(\n ansis.cyan(` Access URL: http://localhost:${port}?token=${authTokenFromServer}`)\n )\n console.log(ansis.dim(` Auth token: ${authTokenFromServer}`))\n\n // Now that we have the token, open browser if needed\n if (!options.noBrowser) {\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}?token=${authTokenFromServer}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n }\n }\n }\n\n // Server started message - defer detailed output if auth enabled\n if (line.includes(\"running at http://localhost\")) {\n serverStartupShown = true\n\n // End telemetry span for server startup\n if (isTelemetryEnabled && serverStartupSpan) {\n logDebug(\n \"[Telemetry] Ending visualize.server.startup span (server started successfully)\"\n )\n const { endSpan } = await import(\"../telemetry/index.js\")\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n logDebug(\"[Telemetry] Span ended\")\n }\n\n console.log(ansis.green(`\u2713 Visualization server started`))\n\n // Display access information (but may be incomplete if auth enabled and token not yet received)\n if (authEnabled) {\n const token = authTokenFromServer || authToken || \"(pending)\"\n if (authTokenFromServer) {\n // Token already received (same chunk), show full info immediately\n console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${token}`))\n console.log(ansis.dim(` Auth token: ${token}`))\n } else {\n // Token not yet received, just show port for now\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n logDebug(` (waiting for auth token...)`)\n }\n } else {\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n console.log(ansis.yellow(` \u26A0 Authentication disabled`))\n }\n\n if (withDanger) {\n console.log(\n ansis.yellow(` \u26A0 Danger mode enabled - chat permissions will be skipped`)\n )\n }\n\n console.log(ansis.blueBright(`Model name: ${model.manifest.name}`))\n const drFolder = await getSpecReferencePath()\n console.log(\n `DR folder: ${ansis.cyan(drFolder || \"Not found\")}`\n )\n console.log(`Model path: ${ansis.cyan((await getModelPath()) || \"Not found\")}`)\n\n // Optionally open browser\n // If auth is enabled, delay opening until we have the token\n const shouldOpenBrowser = !options.noBrowser\n logDebug(\n `Browser auto-open: ${shouldOpenBrowser ? \"enabled\" : \"disabled\"} (noBrowser=${options.noBrowser})`\n )\n\n if (shouldOpenBrowser && !authEnabled) {\n // Auth disabled: open immediately\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n } else if (shouldOpenBrowser && authEnabled && !authTokenFromServer) {\n // Auth enabled but token not yet in this chunk: will open when TOKEN line is processed\n logDebug(\"Browser auto-open deferred until token is received\")\n } else {\n logDebug(\"Browser auto-open disabled\")\n }\n }\n\n // Log other output\n if (!line.startsWith(\"TOKEN:\")) {\n logDebug(`[server] ${line}`)\n }\n }\n })\n }\n\n // Capture stderr\n if (serverProcess.stderr) {\n serverProcess.stderr.on(\"data\", (data) => {\n const output = data.toString().trim()\n if (output) {\n serverOutput += output + \"\\n\"\n // Log stderr at debug level so --debug flag shows telemetry exporter logs\n logDebug(`[server stderr] ${output}`)\n }\n })\n }\n\n // Handle process close (fires after all stdio streams have closed)\n let rejectKeepAlive: ((reason: Error) => void) | null = null\n\n serverProcess.on(\"close\", (code) => {\n if (code !== 0) {\n // Include server output in error message if available (avoid separate logDebug to prevent minification issues)\n const message = serverOutput\n ? `Server exited with code ${code}\\nServer output:\\n${serverOutput}`\n : `Server exited with code ${code}`\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(new Error(message));\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Print error before rejecting (CLI wrapper expects CLIError messages to be pre-printed)\n console.error(message)\n const error = new CLIError(message, code || 1)\n if (rejectKeepAlive) {\n rejectKeepAlive(error)\n }\n }\n })\n\n // Handle process errors\n serverProcess.on(\"error\", (error) => {\n const errorCode = (error as NodeJS.ErrnoException).code\n let message: string\n let suggestions: string[] | undefined\n\n // The server subprocess is launched via process.execPath (the same node/bun\n // binary already running this CLI), so it's always present - ENOENT here would\n // indicate something unusual about the runtime environment rather than a missing\n // dependency (see #800: this used to hard-depend on a separately installed `bun`).\n if (errorCode === \"EACCES\") {\n message = `dr visualize could not run the visualization server: permission denied executing ${process.execPath}.`\n suggestions = [`Check that ${process.execPath} is executable (chmod +x).`]\n } else {\n message = `Failed to start visualization server: ${error.message}`\n }\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(error);\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Don't print here: rejecting propagates this CLIError up through the catch block\n // below to the top-level CLI handler, which prints `.format()` (including\n // `suggestions`) exactly once. Printing here too would duplicate the whole block.\n const cliError = new CLIError(message, 1, suggestions)\n if (rejectKeepAlive) {\n rejectKeepAlive(cliError)\n }\n })\n\n // Setup periodic telemetry flushing for long-running command\n // Since the visualize command never completes (keep-alive pattern),\n // the cli.execute span would never end and export. We flush periodically\n // to ensure active spans are exported to the telemetry backend.\n let flushInterval: NodeJS.Timeout | null = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Setting up periodic flush (every 15 seconds)\")\n const { flushTelemetry } = await import(\"../telemetry/index.js\")\n\n flushInterval = setInterval(async () => {\n logDebug(\"[Telemetry] Periodic flush: flushing pending spans...\")\n try {\n await flushTelemetry()\n logDebug(\"[Telemetry] Periodic flush complete\")\n } catch (error) {\n // Silently ignore flush errors - telemetry should never break the app\n logDebug(`[Telemetry] Periodic flush failed: ${error}`)\n }\n }, 15000) // Flush every 15 seconds\n }\n\n // Handle graceful shutdown on SIGINT (Ctrl-C) and SIGTERM\n const handleShutdown = async (signal: string) => {\n logDebug(`\\n[${signal}] Shutting down visualization server gracefully...`)\n\n // Clear periodic flush interval\n if (flushInterval) {\n logDebug(\"[Telemetry] Stopping periodic flush\")\n clearInterval(flushInterval)\n flushInterval = null\n }\n\n // Forward signal to server process for graceful shutdown\n if (serverProcess && !serverProcess.killed) {\n logDebug(\"Sending termination signal to server process...\")\n serverProcess.kill(signal as NodeJS.Signals)\n\n // Wait for server to exit (with timeout)\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n logDebug(\"Server shutdown timeout, forcing exit...\")\n if (!serverProcess.killed) {\n serverProcess.kill(\"SIGKILL\")\n }\n resolve()\n }, 3000) // 3 second timeout\n\n serverProcess.on(\"exit\", () => {\n clearTimeout(timeout)\n resolve()\n })\n })\n }\n\n logDebug(\"Visualization server stopped\")\n process.exit(0)\n }\n\n // Register signal handlers\n process.on(\"SIGINT\", () => handleShutdown(\"SIGINT\"))\n process.on(\"SIGTERM\", () => handleShutdown(\"SIGTERM\"))\n\n // Keep parent process alive - reject promise if server fails\n await new Promise((_, reject) => {\n rejectKeepAlive = reject\n })\n } catch (error) {\n // Throw CLIError instead of process.exit to allow telemetry shutdown\n // Note: Avoid side effects (like logDebug) before throw to prevent minification issues\n if (error instanceof CLIError) {\n // CLIError should have already printed its message\n throw error\n }\n // Non-CLIError needs to be printed and wrapped\n const message = getErrorMessage(error)\n console.error(message)\n throw new CLIError(message, 1)\n }\n}\n\n/**\n * Get the appropriate command to open a URL\n */\nfunction getOpenCommand(): string {\n const platform = process.platform\n\n if (platform === \"darwin\") {\n return \"open\"\n }\n if (platform === \"win32\") {\n return \"start\"\n }\n // Linux and others\n return \"xdg-open\"\n}\n"],
|
|
5
|
-
"mappings": "AAIA,OAAO,WAAW;AAClB,SAAS,aAAa;AACtB,SAAS,qBAAqB;AAC9B,SAAS,eAAe;AACxB,SAAS,aAAa;AACtB,SAAS,YAAY,gBAAgB;AACrC,SAAS,gBAAgB;AACzB,SAAS,sBAAsB,oBAAoB;AACnD,SAAS,uBAAuB;AAIhC,MAAM,qBAAgE;
|
|
4
|
+
"sourcesContent": ["/**\n * Visualize command - Launch visualization server\n */\n\nimport ansis from \"ansis\"\nimport { spawn } from \"child_process\"\nimport { fileURLToPath } from \"node:url\"\nimport { dirname } from \"node:path\"\nimport { Model } from \"../core/model.js\"\nimport { logVerbose, logDebug } from \"../utils/globals.js\"\nimport { CLIError } from \"../utils/errors.js\"\nimport { getSpecReferencePath, getModelPath } from \"../utils/project-paths.js\"\nimport { getErrorMessage } from \"../utils/errors.js\"\n\n// Conditional telemetry import based on compile-time flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== \"undefined\" ? TELEMETRY_ENABLED : false\n\nexport interface VisualizeOptions {\n port?: number\n noBrowser?: boolean\n noAuth?: boolean\n token?: string\n verbose?: boolean\n debug?: boolean\n withDanger?: boolean\n withChangesets?: boolean\n viewerPath?: string\n}\n\nasync function findAvailablePort(startPort: number): Promise<number> {\n const { createServer } = await import(\"net\")\n for (let port = startPort; port <= 65535; port++) {\n const available = await new Promise<boolean>((resolve) => {\n const server = createServer()\n server.once(\"error\", () => resolve(false))\n server.once(\"listening\", () => { server.close(); resolve(true) })\n server.listen(port, \"127.0.0.1\")\n })\n if (available) return port\n }\n throw new Error(\"No available ports found\")\n}\n\nexport async function visualizeCommand(\n options: VisualizeOptions & { port?: string }\n): Promise<void> {\n try {\n logDebug(\"Loading model for validation...\")\n\n // Load model to validate it exists (without full content to be quick)\n const model = await Model.load(process.cwd(), { lazyLoad: true })\n\n logVerbose(`Model validated: ${model.manifest.name}`)\n\n // Parse and validate port\n let port = 8080\n if (options.port) {\n const portNum = parseInt(String(options.port), 10)\n if (isNaN(portNum) || portNum < 1 || portNum > 65535) {\n throw new Error(`Invalid port number: ${options.port}. Port must be between 1 and 65535.`)\n }\n port = portNum\n }\n\n // Find an available port starting from the requested port\n port = await findAvailablePort(port)\n if (port !== (options.port ? parseInt(String(options.port), 10) : 8080)) {\n console.log(ansis.yellow(` Port ${options.port ?? 8080} is in use, using port ${port} instead`))\n }\n\n // Determine auth settings\n const authEnabled = !options.noAuth // Auth enabled by default unless --no-auth flag\n const authToken = options.token // Optional token for testing\n const withDanger = options.withDanger || false // Danger mode disabled by default\n // Changeset-only writes; danger mode already allows everything, so it wins\n const withChangesets = (options.withChangesets || false) && !withDanger\n\n logDebug(`Starting visualization server on port ${port}`)\n logDebug(`Authentication: ${authEnabled ? \"enabled\" : \"disabled\"}`)\n if (authEnabled && authToken) {\n logDebug(`Using provided token for authentication`)\n } else if (authEnabled) {\n logDebug(`Token will be auto-generated`)\n }\n if (withDanger) {\n logDebug(`Danger mode enabled - chat permissions will be skipped`)\n }\n\n // Get the directory where this command file is located (dist/commands/)\n const commandDir = dirname(fileURLToPath(import.meta.url))\n const distDir = dirname(commandDir) // Go up to dist/\n const serverEntryPath = `${distDir}/server-entry.js`\n\n logDebug(`Server entry path: ${serverEntryPath}`)\n\n // Create telemetry span for server startup\n let serverStartupSpan: any = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Creating visualize.server.startup span\")\n const { startSpan } = await import(\"../telemetry/index.js\")\n serverStartupSpan = startSpan(\"visualize.server.startup\", {\n \"server.port\": port,\n \"server.auth_enabled\": authEnabled,\n \"server.with_danger\": withDanger,\n })\n logDebug(`[Telemetry] Span created: ${serverStartupSpan ? \"success\" : \"failed\"}`)\n } else {\n logDebug(\"[Telemetry] TELEMETRY_ENABLED is false - no spans will be created\")\n }\n\n // Build environment variables for the server subprocess\n const env: Record<string, string> = {\n ...process.env,\n DR_VISUALIZE_PORT: String(port),\n DR_PROJECT_PATH: process.cwd(),\n DR_AUTH_ENABLED: String(authEnabled),\n DR_WITH_DANGER: String(withDanger),\n DR_WITH_CHANGESETS: String(withChangesets),\n }\n\n // Pass debug/verbose flags to subprocess\n // The CLI --debug flag sets globalOptions.debug, but subprocess checks process.env.DEBUG\n const { isDebug: getDebugState, isVerbose } = await import(\"../utils/globals.js\")\n if (getDebugState()) {\n env.DEBUG = \"1\"\n env.DR_TELEMETRY_DEBUG = \"1\" // Enable telemetry exporter debug logging\n logDebug(\"Passing DEBUG=1 and DR_TELEMETRY_DEBUG=1 to subprocess\")\n }\n if (isVerbose()) {\n env.VERBOSE = \"1\"\n logDebug(\"Passing VERBOSE=1 to subprocess\")\n }\n\n // Only set auth token if provided\n if (authToken) {\n env.DR_AUTH_TOKEN = authToken\n }\n\n // Set custom viewer path if provided\n if (options.viewerPath) {\n env.DR_VIEWER_PATH = options.viewerPath\n logDebug(`Using custom viewer from: ${options.viewerPath}`)\n }\n\n // Reuse whichever runtime is currently executing the CLI (node or bun) to launch\n // the server subprocess. server-entry.ts has no Bun-specific dependencies (see #800),\n // so this works identically under Node.js or Bun \u2014 and unlike spawning a literal\n // \"bun\" binary, process.execPath is guaranteed to exist since it's already running.\n const serverProcess = spawn(process.execPath, [serverEntryPath], {\n cwd: process.cwd(),\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env,\n })\n\n let authTokenFromServer = \"\"\n let serverOutput = \"\"\n let serverStartupShown = false\n\n // Capture output from server process\n if (serverProcess.stdout) {\n serverProcess.stdout.on(\"data\", async (data) => {\n const chunk = data.toString()\n serverOutput += chunk\n\n // Process each line individually: stdio pipe buffering can batch multiple\n // console.log calls (e.g. \"running at\" + \"TOKEN:\") into a single data event,\n // regardless of runtime.\n // Using startsWith/substring on the whole chunk would either miss the TOKEN line\n // (when \"running at\" is first) or extract a malformed token (when trailing lines\n // are appended), causing the browser to open with the wrong token and get a 403.\n const lines = chunk.split(\"\\n\")\n for (const rawLine of lines) {\n const line = rawLine.trim()\n if (!line) continue\n\n // Extract token from output\n if (line.startsWith(\"TOKEN:\")) {\n authTokenFromServer = line.substring(6)\n logDebug(`Received auth token from server: ${authTokenFromServer.substring(0, 8)}...`)\n\n // If we haven't shown startup info yet and now have the token, show it\n if (serverStartupShown && authEnabled && authTokenFromServer) {\n // Update the URL display with actual token\n console.log(\n ansis.cyan(` Access URL: http://localhost:${port}?token=${authTokenFromServer}`)\n )\n console.log(ansis.dim(` Auth token: ${authTokenFromServer}`))\n\n // Now that we have the token, open browser if needed\n if (!options.noBrowser) {\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}?token=${authTokenFromServer}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n }\n }\n }\n\n // Server started message - defer detailed output if auth enabled\n if (line.includes(\"running at http://localhost\")) {\n serverStartupShown = true\n\n // End telemetry span for server startup\n if (isTelemetryEnabled && serverStartupSpan) {\n logDebug(\n \"[Telemetry] Ending visualize.server.startup span (server started successfully)\"\n )\n const { endSpan } = await import(\"../telemetry/index.js\")\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n logDebug(\"[Telemetry] Span ended\")\n }\n\n console.log(ansis.green(`\u2713 Visualization server started`))\n\n // Display access information (but may be incomplete if auth enabled and token not yet received)\n if (authEnabled) {\n const token = authTokenFromServer || authToken || \"(pending)\"\n if (authTokenFromServer) {\n // Token already received (same chunk), show full info immediately\n console.log(ansis.cyan(` Access URL: http://localhost:${port}?token=${token}`))\n console.log(ansis.dim(` Auth token: ${token}`))\n } else {\n // Token not yet received, just show port for now\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n logDebug(` (waiting for auth token...)`)\n }\n } else {\n console.log(ansis.dim(` Open http://localhost:${port} in your browser`))\n console.log(ansis.yellow(` \u26A0 Authentication disabled`))\n }\n\n if (withDanger) {\n console.log(\n ansis.yellow(` \u26A0 Danger mode enabled - chat permissions will be skipped`)\n )\n if (options.withChangesets) {\n console.log(ansis.dim(` (--with-changesets ignored: danger mode already allows all writes)`))\n }\n } else if (withChangesets) {\n console.log(\n ansis.yellow(` \u26A0 Changeset mode enabled - chat can stage model changes in a changeset (no commit/apply/delete)`)\n )\n }\n\n console.log(ansis.blueBright(`Model name: ${model.manifest.name}`))\n const drFolder = await getSpecReferencePath()\n console.log(\n `DR folder: ${ansis.cyan(drFolder || \"Not found\")}`\n )\n console.log(`Model path: ${ansis.cyan((await getModelPath()) || \"Not found\")}`)\n\n // Optionally open browser\n // If auth is enabled, delay opening until we have the token\n const shouldOpenBrowser = !options.noBrowser\n logDebug(\n `Browser auto-open: ${shouldOpenBrowser ? \"enabled\" : \"disabled\"} (noBrowser=${options.noBrowser})`\n )\n\n if (shouldOpenBrowser && !authEnabled) {\n // Auth disabled: open immediately\n try {\n const command = getOpenCommand()\n const url = `http://localhost:${port}`\n logDebug(`Opening browser with command: ${command} ${url}`)\n spawn(command, [url], {\n detached: true,\n stdio: \"ignore\",\n })\n } catch (error) {\n logVerbose(\"Could not auto-open browser (not critical)\")\n }\n } else if (shouldOpenBrowser && authEnabled && !authTokenFromServer) {\n // Auth enabled but token not yet in this chunk: will open when TOKEN line is processed\n logDebug(\"Browser auto-open deferred until token is received\")\n } else {\n logDebug(\"Browser auto-open disabled\")\n }\n }\n\n // Log other output\n if (!line.startsWith(\"TOKEN:\")) {\n logDebug(`[server] ${line}`)\n }\n }\n })\n }\n\n // Capture stderr\n if (serverProcess.stderr) {\n serverProcess.stderr.on(\"data\", (data) => {\n const output = data.toString().trim()\n if (output) {\n serverOutput += output + \"\\n\"\n // Log stderr at debug level so --debug flag shows telemetry exporter logs\n logDebug(`[server stderr] ${output}`)\n }\n })\n }\n\n // Handle process close (fires after all stdio streams have closed)\n let rejectKeepAlive: ((reason: Error) => void) | null = null\n\n serverProcess.on(\"close\", (code) => {\n if (code !== 0) {\n // Include server output in error message if available (avoid separate logDebug to prevent minification issues)\n const message = serverOutput\n ? `Server exited with code ${code}\\nServer output:\\n${serverOutput}`\n : `Server exited with code ${code}`\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(new Error(message));\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Print error before rejecting (CLI wrapper expects CLIError messages to be pre-printed)\n console.error(message)\n const error = new CLIError(message, code || 1)\n if (rejectKeepAlive) {\n rejectKeepAlive(error)\n }\n }\n })\n\n // Handle process errors\n serverProcess.on(\"error\", (error) => {\n const errorCode = (error as NodeJS.ErrnoException).code\n let message: string\n let suggestions: string[] | undefined\n\n // The server subprocess is launched via process.execPath (the same node/bun\n // binary already running this CLI), so it's always present - ENOENT here would\n // indicate something unusual about the runtime environment rather than a missing\n // dependency (see #800: this used to hard-depend on a separately installed `bun`).\n if (errorCode === \"EACCES\") {\n message = `dr visualize could not run the visualization server: permission denied executing ${process.execPath}.`\n suggestions = [`Check that ${process.execPath} is executable (chmod +x).`]\n } else {\n message = `Failed to start visualization server: ${error.message}`\n }\n\n // Record error in startup span if still active\n if (isTelemetryEnabled && serverStartupSpan) {\n (async () => {\n const { endSpan } = await import(\"../telemetry/index.js\")\n if (\"recordException\" in serverStartupSpan) {\n (serverStartupSpan as any).recordException(error);\n (serverStartupSpan as any).setStatus({ code: 2, message }) // ERROR\n }\n endSpan(serverStartupSpan)\n serverStartupSpan = null\n })()\n }\n\n // Don't print here: rejecting propagates this CLIError up through the catch block\n // below to the top-level CLI handler, which prints `.format()` (including\n // `suggestions`) exactly once. Printing here too would duplicate the whole block.\n const cliError = new CLIError(message, 1, suggestions)\n if (rejectKeepAlive) {\n rejectKeepAlive(cliError)\n }\n })\n\n // Setup periodic telemetry flushing for long-running command\n // Since the visualize command never completes (keep-alive pattern),\n // the cli.execute span would never end and export. We flush periodically\n // to ensure active spans are exported to the telemetry backend.\n let flushInterval: NodeJS.Timeout | null = null\n if (isTelemetryEnabled) {\n logDebug(\"[Telemetry] Setting up periodic flush (every 15 seconds)\")\n const { flushTelemetry } = await import(\"../telemetry/index.js\")\n\n flushInterval = setInterval(async () => {\n logDebug(\"[Telemetry] Periodic flush: flushing pending spans...\")\n try {\n await flushTelemetry()\n logDebug(\"[Telemetry] Periodic flush complete\")\n } catch (error) {\n // Silently ignore flush errors - telemetry should never break the app\n logDebug(`[Telemetry] Periodic flush failed: ${error}`)\n }\n }, 15000) // Flush every 15 seconds\n }\n\n // Handle graceful shutdown on SIGINT (Ctrl-C) and SIGTERM\n const handleShutdown = async (signal: string) => {\n logDebug(`\\n[${signal}] Shutting down visualization server gracefully...`)\n\n // Clear periodic flush interval\n if (flushInterval) {\n logDebug(\"[Telemetry] Stopping periodic flush\")\n clearInterval(flushInterval)\n flushInterval = null\n }\n\n // Forward signal to server process for graceful shutdown\n if (serverProcess && !serverProcess.killed) {\n logDebug(\"Sending termination signal to server process...\")\n serverProcess.kill(signal as NodeJS.Signals)\n\n // Wait for server to exit (with timeout)\n await new Promise<void>((resolve) => {\n const timeout = setTimeout(() => {\n logDebug(\"Server shutdown timeout, forcing exit...\")\n if (!serverProcess.killed) {\n serverProcess.kill(\"SIGKILL\")\n }\n resolve()\n }, 3000) // 3 second timeout\n\n serverProcess.on(\"exit\", () => {\n clearTimeout(timeout)\n resolve()\n })\n })\n }\n\n logDebug(\"Visualization server stopped\")\n process.exit(0)\n }\n\n // Register signal handlers\n process.on(\"SIGINT\", () => handleShutdown(\"SIGINT\"))\n process.on(\"SIGTERM\", () => handleShutdown(\"SIGTERM\"))\n\n // Keep parent process alive - reject promise if server fails\n await new Promise((_, reject) => {\n rejectKeepAlive = reject\n })\n } catch (error) {\n // Throw CLIError instead of process.exit to allow telemetry shutdown\n // Note: Avoid side effects (like logDebug) before throw to prevent minification issues\n if (error instanceof CLIError) {\n // CLIError should have already printed its message\n throw error\n }\n // Non-CLIError needs to be printed and wrapped\n const message = getErrorMessage(error)\n console.error(message)\n throw new CLIError(message, 1)\n }\n}\n\n/**\n * Get the appropriate command to open a URL\n */\nfunction getOpenCommand(): string {\n const platform = process.platform\n\n if (platform === \"darwin\") {\n return \"open\"\n }\n if (platform === \"win32\") {\n return \"start\"\n }\n // Linux and others\n return \"xdg-open\"\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { parse, stringify } from "yaml";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
const DRConfigSchema = z.object({
|
|
8
|
+
telemetry: z.object({
|
|
9
|
+
otlp: z.object({
|
|
10
|
+
endpoint: z.string().optional(),
|
|
11
|
+
logs_endpoint: z.string().optional(),
|
|
12
|
+
service_name: z.string().optional()
|
|
13
|
+
}).passthrough().optional()
|
|
14
|
+
}).passthrough().optional(),
|
|
15
|
+
mcp: z.object({
|
|
16
|
+
api_key_path: z.string().optional()
|
|
17
|
+
}).passthrough().optional()
|
|
18
|
+
}).passthrough(), CONFIG_FILENAME = ".dr-config.yaml";
|
|
19
|
+
class DRConfigParseError extends Error {
|
|
20
|
+
configPath;
|
|
21
|
+
constructor(configPath, options) {
|
|
22
|
+
super(`Failed to parse ${configPath}`, options), this.configPath = configPath, this.name = "DRConfigParseError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function getDRConfigPath() {
|
|
26
|
+
return process.env.DR_CONFIG_PATH ?? join(homedir(), CONFIG_FILENAME);
|
|
27
|
+
}
|
|
28
|
+
async function loadDRConfig(options) {
|
|
29
|
+
const configPath = getDRConfigPath();
|
|
30
|
+
if (!existsSync(configPath))
|
|
31
|
+
return {};
|
|
32
|
+
try {
|
|
33
|
+
const content = await readFile(configPath, "utf-8"), parsed = parse(content);
|
|
34
|
+
if (parsed == null)
|
|
35
|
+
return {};
|
|
36
|
+
const result = DRConfigSchema.safeParse(parsed);
|
|
37
|
+
if (!result.success)
|
|
38
|
+
throw new Error(`Invalid config shape: ${result.error.message}`);
|
|
39
|
+
return result.data;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (reportConfigLoadError(configPath, error), options?.strict)
|
|
42
|
+
throw new DRConfigParseError(configPath, { cause: error });
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function saveDRConfig(config) {
|
|
47
|
+
const configPath = getDRConfigPath();
|
|
48
|
+
await mkdir(dirname(configPath), { recursive: !0 }), await writeFile(configPath, stringify(config), "utf-8");
|
|
49
|
+
}
|
|
50
|
+
function reportConfigLoadError(configPath, error) {
|
|
51
|
+
if (!(error instanceof Error)) {
|
|
52
|
+
process.stderr.write(`Warning: Failed to parse ${configPath}, using defaults
|
|
53
|
+
`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const errorMsg = error.message;
|
|
57
|
+
errorMsg.includes("EACCES") || errorMsg.includes("permission denied") ? (process.stderr.write(`Error: Cannot read config file ${configPath} - permission denied
|
|
58
|
+
`), process.stderr.write(`Suggestions:
|
|
59
|
+
`), process.stderr.write(` \u2022 Check file permissions with: ls -l ${configPath}
|
|
60
|
+
`), process.stderr.write(` \u2022 Ensure you have read access to the file
|
|
61
|
+
`), process.stderr.write(` \u2022 Try: chmod 644 ${configPath}
|
|
62
|
+
`)) : errorMsg.includes("YAMLException") || error.name === "YAMLException" || errorMsg.includes("bad indentation") || errorMsg.includes("unexpected") ? (process.stderr.write(`Error: Invalid YAML syntax in ${configPath}
|
|
63
|
+
`), process.stderr.write(`Details: ${errorMsg}
|
|
64
|
+
`), process.stderr.write(`Suggestions:
|
|
65
|
+
`), process.stderr.write(` \u2022 Validate your YAML syntax at https://www.yamllint.com/
|
|
66
|
+
`), process.stderr.write(` \u2022 Check for proper indentation (use spaces, not tabs)
|
|
67
|
+
`), process.stderr.write(` \u2022 Verify colons have spaces after them
|
|
68
|
+
`)) : (process.stderr.write(`Error: Failed to load config file ${configPath}
|
|
69
|
+
`), process.stderr.write(`Details: ${errorMsg}
|
|
70
|
+
`), process.stderr.write(`Suggestions:
|
|
71
|
+
`), process.stderr.write(` \u2022 Verify the file is valid UTF-8 encoded text
|
|
72
|
+
`), process.stderr.write(` \u2022 Check if the file system is accessible
|
|
73
|
+
`), process.stderr.write(` \u2022 Try recreating the file if it may be corrupted
|
|
74
|
+
`)), process.stderr.write(`Using default configuration due to config file error
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
DRConfigParseError,
|
|
79
|
+
getDRConfigPath,
|
|
80
|
+
loadDRConfig,
|
|
81
|
+
saveDRConfig
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/config.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Shared DR CLI configuration loader\n *\n * Reads and writes `~/.dr-config.yaml` (overridable via DR_CONFIG_PATH, primarily for\n * tests). This is the single source of truth for the config file's shape and I/O \u2014\n * feature-specific loaders (telemetry, MCP, ...) should read their section from the\n * object returned by `loadDRConfig()` rather than parsing the file themselves.\n *\n * Configuration file format (~/.dr-config.yaml):\n * ```yaml\n * telemetry:\n * otlp:\n * endpoint: 'http://localhost:4318/v1/traces'\n * mcp:\n * api_key_path: '/home/user/.dr-mcp-key'\n * ```\n */\n\nimport { homedir } from \"node:os\";\nimport { existsSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { parse, stringify } from \"yaml\";\nimport { z } from \"zod\";\n\n/**\n * Runtime schema for ~/.dr-config.yaml, used to validate parsed YAML before it\n * is trusted as a `DRConfig`. Keep in sync with the shape documented above.\n */\nconst DRConfigSchema = z\n .object({\n telemetry: z\n .object({\n otlp: z\n .object({\n endpoint: z.string().optional(),\n logs_endpoint: z.string().optional(),\n service_name: z.string().optional(),\n })\n .passthrough()\n .optional(),\n })\n .passthrough()\n .optional(),\n mcp: z\n .object({\n api_key_path: z.string().optional(),\n })\n .passthrough()\n .optional(),\n })\n .passthrough();\n\n/**\n * Configuration structure parsed from ~/.dr-config.yaml\n */\nexport type DRConfig = z.infer<typeof DRConfigSchema>;\n\nconst CONFIG_FILENAME = \".dr-config.yaml\";\n\n/**\n * Thrown by `loadDRConfig({ strict: true })` when the config file exists but\n * cannot be parsed. Callers that would otherwise silently treat a corrupted\n * config as \"nothing configured\" \u2014 and take a destructive action like\n * regenerating a secret \u2014 should opt into strict mode and handle this.\n */\nexport class DRConfigParseError extends Error {\n constructor(\n public readonly configPath: string,\n options?: { cause?: unknown }\n ) {\n super(`Failed to parse ${configPath}`, options);\n this.name = \"DRConfigParseError\";\n }\n}\n\n/**\n * Resolve the path to the DR config file.\n * Supports DR_CONFIG_PATH override for testing; defaults to ~/.dr-config.yaml.\n */\nexport function getDRConfigPath(): string {\n return process.env.DR_CONFIG_PATH ?? join(homedir(), CONFIG_FILENAME);\n}\n\n/**\n * Load the DR config file, returning an empty object if it does not exist or\n * cannot be parsed. Errors are reported to stderr but never thrown by default,\n * since a malformed config file should degrade to defaults rather than crash\n * the CLI for most callers (e.g. telemetry).\n *\n * Pass `{ strict: true }` to instead throw a `DRConfigParseError` when the\n * file exists but fails to parse. Use this for callers where treating a\n * corrupted config the same as \"nothing configured\" would cause data loss\n * (e.g. regenerating and overwriting a secret whose path is recorded here).\n */\nexport async function loadDRConfig(options?: { strict?: boolean }): Promise<DRConfig> {\n const configPath = getDRConfigPath();\n\n if (!existsSync(configPath)) {\n return {};\n }\n\n try {\n const content = await readFile(configPath, \"utf-8\");\n const parsed = parse(content);\n if (parsed === null || parsed === undefined) {\n return {};\n }\n\n const result = DRConfigSchema.safeParse(parsed);\n if (!result.success) {\n throw new Error(`Invalid config shape: ${result.error.message}`);\n }\n return result.data;\n } catch (error) {\n reportConfigLoadError(configPath, error);\n if (options?.strict) {\n throw new DRConfigParseError(configPath, { cause: error });\n }\n return {};\n }\n}\n\n/**\n * Persist the given config object to the DR config file, creating the parent\n * directory if necessary. Callers are responsible for merging with any existing\n * config (typically via `loadDRConfig()`) before calling this.\n */\nexport async function saveDRConfig(config: DRConfig): Promise<void> {\n const configPath = getDRConfigPath();\n await mkdir(dirname(configPath), { recursive: true });\n await writeFile(configPath, stringify(config), \"utf-8\");\n}\n\nfunction reportConfigLoadError(configPath: string, error: unknown): void {\n if (!(error instanceof Error)) {\n process.stderr.write(`Warning: Failed to parse ${configPath}, using defaults\\n`);\n return;\n }\n\n const errorMsg = error.message;\n\n if (errorMsg.includes(\"EACCES\") || errorMsg.includes(\"permission denied\")) {\n process.stderr.write(`Error: Cannot read config file ${configPath} - permission denied\\n`);\n process.stderr.write(\"Suggestions:\\n\");\n process.stderr.write(` \u2022 Check file permissions with: ls -l ${configPath}\\n`);\n process.stderr.write(\" \u2022 Ensure you have read access to the file\\n\");\n process.stderr.write(` \u2022 Try: chmod 644 ${configPath}\\n`);\n } else if (\n errorMsg.includes(\"YAMLException\") ||\n error.name === \"YAMLException\" ||\n errorMsg.includes(\"bad indentation\") ||\n errorMsg.includes(\"unexpected\")\n ) {\n process.stderr.write(`Error: Invalid YAML syntax in ${configPath}\\n`);\n process.stderr.write(`Details: ${errorMsg}\\n`);\n process.stderr.write(\"Suggestions:\\n\");\n process.stderr.write(\" \u2022 Validate your YAML syntax at https://www.yamllint.com/\\n\");\n process.stderr.write(\" \u2022 Check for proper indentation (use spaces, not tabs)\\n\");\n process.stderr.write(\" \u2022 Verify colons have spaces after them\\n\");\n } else {\n process.stderr.write(`Error: Failed to load config file ${configPath}\\n`);\n process.stderr.write(`Details: ${errorMsg}\\n`);\n process.stderr.write(\"Suggestions:\\n\");\n process.stderr.write(\" \u2022 Verify the file is valid UTF-8 encoded text\\n\");\n process.stderr.write(\" \u2022 Check if the file system is accessible\\n\");\n process.stderr.write(\" \u2022 Try recreating the file if it may be corrupted\\n\");\n }\n process.stderr.write(`Using default configuration due to config file error\\n`);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { readFile, readdir, rm } from "fs/promises";
|
|
2
|
+
import { randomBytes } from "crypto";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import yaml from "yaml";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { atomicWrite, ensureDir, fileExists } from "../utils/file-io.js";
|
|
7
|
+
import { FileLock } from "../utils/file-lock.js";
|
|
8
|
+
import { getErrorMessage } from "../utils/errors.js";
|
|
9
|
+
const AnnotationReplySchema = z.object({
|
|
10
|
+
id: z.string(),
|
|
11
|
+
author: z.string(),
|
|
12
|
+
content: z.string(),
|
|
13
|
+
createdAt: z.string()
|
|
14
|
+
}), AnnotationSchema = z.object({
|
|
15
|
+
id: z.string(),
|
|
16
|
+
elementId: z.string(),
|
|
17
|
+
author: z.string(),
|
|
18
|
+
content: z.string(),
|
|
19
|
+
createdAt: z.string(),
|
|
20
|
+
updatedAt: z.string().optional(),
|
|
21
|
+
tags: z.array(z.string()),
|
|
22
|
+
resolved: z.boolean(),
|
|
23
|
+
replies: z.array(AnnotationReplySchema)
|
|
24
|
+
});
|
|
25
|
+
class AnnotationStore {
|
|
26
|
+
annotationsDir;
|
|
27
|
+
constructor(rootPath) {
|
|
28
|
+
this.annotationsDir = path.join(rootPath, "documentation-robotics", "annotations");
|
|
29
|
+
}
|
|
30
|
+
/** Lists all annotations, optionally filtered to a single element, oldest first. Skips (and warns on) individual files that fail to load rather than failing the whole list. */
|
|
31
|
+
async list(elementId) {
|
|
32
|
+
if (!await fileExists(this.annotationsDir))
|
|
33
|
+
return [];
|
|
34
|
+
const entries = await readdir(this.annotationsDir, { withFileTypes: !0 }), annotations = [];
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (!entry.isFile() || !entry.name.endsWith(".yaml"))
|
|
37
|
+
continue;
|
|
38
|
+
const id = entry.name.slice(0, -5);
|
|
39
|
+
let annotation;
|
|
40
|
+
try {
|
|
41
|
+
annotation = await this.get(id);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
process.stderr.write(`Warning: Skipping annotation '${id}': ${getErrorMessage(error)}
|
|
44
|
+
`);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
annotation && (!elementId || annotation.elementId === elementId) && annotations.push(annotation);
|
|
48
|
+
}
|
|
49
|
+
return annotations.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
50
|
+
}
|
|
51
|
+
async get(id) {
|
|
52
|
+
const filePath = this.getPath(id);
|
|
53
|
+
if (!await fileExists(filePath))
|
|
54
|
+
return null;
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
const content = await readFile(filePath, "utf-8");
|
|
58
|
+
parsed = yaml.parse(content);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
throw new Error(`Failed to load annotation '${id}': ${getErrorMessage(error)}`);
|
|
61
|
+
}
|
|
62
|
+
const result = AnnotationSchema.safeParse(parsed);
|
|
63
|
+
if (!result.success)
|
|
64
|
+
throw new Error(`Failed to load annotation '${id}': invalid annotation data - ${result.error.message}`);
|
|
65
|
+
return result.data;
|
|
66
|
+
}
|
|
67
|
+
async create(input) {
|
|
68
|
+
const annotation = {
|
|
69
|
+
id: this.generateAnnotationId(),
|
|
70
|
+
elementId: input.elementId,
|
|
71
|
+
author: input.author?.trim() || "Anonymous",
|
|
72
|
+
content: input.content,
|
|
73
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
74
|
+
tags: input.tags ?? [],
|
|
75
|
+
resolved: !1,
|
|
76
|
+
replies: []
|
|
77
|
+
};
|
|
78
|
+
return await ensureDir(this.annotationsDir), await atomicWrite(this.getPath(annotation.id), yaml.stringify(annotation)), annotation;
|
|
79
|
+
}
|
|
80
|
+
/** Partially updates an annotation's content, tags, and/or resolved state. Returns null if not found. */
|
|
81
|
+
async update(id, updates) {
|
|
82
|
+
const filePath = this.getPath(id);
|
|
83
|
+
return await ensureDir(this.annotationsDir), new FileLock(filePath).withLock(async () => {
|
|
84
|
+
const annotation = await this.get(id);
|
|
85
|
+
if (!annotation)
|
|
86
|
+
return null;
|
|
87
|
+
const updated = {
|
|
88
|
+
...annotation,
|
|
89
|
+
content: updates.content ?? annotation.content,
|
|
90
|
+
tags: updates.tags ?? annotation.tags,
|
|
91
|
+
resolved: updates.resolved ?? annotation.resolved,
|
|
92
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
93
|
+
};
|
|
94
|
+
return await atomicWrite(filePath, yaml.stringify(updated)), updated;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** Deletes an annotation and its replies. Returns false if it didn't exist. */
|
|
98
|
+
async delete(id) {
|
|
99
|
+
const filePath = this.getPath(id);
|
|
100
|
+
return await fileExists(filePath) ? (await rm(filePath, { force: !0 }), !0) : !1;
|
|
101
|
+
}
|
|
102
|
+
/** Appends a reply to an annotation. Returns null if the annotation doesn't exist. */
|
|
103
|
+
async addReply(id, input) {
|
|
104
|
+
const filePath = this.getPath(id);
|
|
105
|
+
return await ensureDir(this.annotationsDir), new FileLock(filePath).withLock(async () => {
|
|
106
|
+
const annotation = await this.get(id);
|
|
107
|
+
if (!annotation)
|
|
108
|
+
return null;
|
|
109
|
+
const reply = {
|
|
110
|
+
id: this.generateReplyId(),
|
|
111
|
+
author: input.author,
|
|
112
|
+
content: input.content,
|
|
113
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
114
|
+
};
|
|
115
|
+
return annotation.replies.push(reply), await atomicWrite(filePath, yaml.stringify(annotation)), reply;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
getPath(id) {
|
|
119
|
+
return path.join(this.annotationsDir, `${this.sanitizeId(id)}.yaml`);
|
|
120
|
+
}
|
|
121
|
+
/** Whitelists safe characters so an annotation ID can't escape annotationsDir via path traversal. */
|
|
122
|
+
sanitizeId(id) {
|
|
123
|
+
const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, "");
|
|
124
|
+
if (!sanitized)
|
|
125
|
+
throw new Error(`Invalid annotation ID '${id}'`);
|
|
126
|
+
return sanitized;
|
|
127
|
+
}
|
|
128
|
+
generateAnnotationId() {
|
|
129
|
+
return `ann-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
130
|
+
}
|
|
131
|
+
generateReplyId() {
|
|
132
|
+
return `reply-${Date.now()}-${randomBytes(4).toString("hex")}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export {
|
|
136
|
+
AnnotationStore
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=annotation-store.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/core/annotation-store.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * AnnotationStore - filesystem-backed storage for element annotations.\n *\n * Persists annotations under documentation-robotics/annotations/{id}.yaml, one\n * file per annotation with its replies embedded. This is what gives the MCP\n * annotation_* tools (a separate stdio process per session, with no shared\n * memory with a running `dr server`) the same annotation capabilities as the\n * REST server's in-memory /api/annotations endpoints, backed by durable\n * storage that survives process restarts.\n */\n\nimport { readFile, readdir, rm } from \"fs/promises\";\nimport { randomBytes } from \"crypto\";\nimport path from \"path\";\nimport yaml from \"yaml\";\nimport { z } from \"zod\";\nimport { atomicWrite, ensureDir, fileExists } from \"../utils/file-io.js\";\nimport { FileLock } from \"../utils/file-lock.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\n\nconst AnnotationReplySchema = z.object({\n id: z.string(),\n author: z.string(),\n content: z.string(),\n createdAt: z.string(),\n});\n\n/**\n * Runtime schema for an annotation file on disk, used to validate parsed YAML\n * before it is trusted as an `Annotation` \u2014 hand-edited files can otherwise\n * omit required fields (e.g. `tags`) and crash downstream consumers.\n */\nconst AnnotationSchema = z.object({\n id: z.string(),\n elementId: z.string(),\n author: z.string(),\n content: z.string(),\n createdAt: z.string(),\n updatedAt: z.string().optional(),\n tags: z.array(z.string()),\n resolved: z.boolean(),\n replies: z.array(AnnotationReplySchema),\n});\n\nexport type AnnotationReply = z.infer<typeof AnnotationReplySchema>;\n\nexport type Annotation = z.infer<typeof AnnotationSchema>;\n\nexport interface AnnotationCreateInput {\n elementId: string;\n author?: string;\n content: string;\n tags?: string[];\n}\n\nexport interface AnnotationUpdateInput {\n content?: string;\n tags?: string[];\n resolved?: boolean;\n}\n\nexport interface AnnotationReplyCreateInput {\n author: string;\n content: string;\n}\n\nexport class AnnotationStore {\n private annotationsDir: string;\n\n constructor(rootPath: string) {\n this.annotationsDir = path.join(rootPath, \"documentation-robotics\", \"annotations\");\n }\n\n /** Lists all annotations, optionally filtered to a single element, oldest first. Skips (and warns on) individual files that fail to load rather than failing the whole list. */\n async list(elementId?: string): Promise<Annotation[]> {\n if (!(await fileExists(this.annotationsDir))) {\n return [];\n }\n\n const entries = await readdir(this.annotationsDir, { withFileTypes: true });\n const annotations: Annotation[] = [];\n\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith(\".yaml\")) continue;\n const id = entry.name.slice(0, -\".yaml\".length);\n\n let annotation: Annotation | null;\n try {\n annotation = await this.get(id);\n } catch (error) {\n process.stderr.write(`Warning: Skipping annotation '${id}': ${getErrorMessage(error)}\\n`);\n continue;\n }\n\n if (annotation && (!elementId || annotation.elementId === elementId)) {\n annotations.push(annotation);\n }\n }\n\n return annotations.sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n async get(id: string): Promise<Annotation | null> {\n const filePath = this.getPath(id);\n if (!(await fileExists(filePath))) {\n return null;\n }\n\n let parsed: unknown;\n try {\n const content = await readFile(filePath, \"utf-8\");\n parsed = yaml.parse(content);\n } catch (error) {\n throw new Error(`Failed to load annotation '${id}': ${getErrorMessage(error)}`);\n }\n\n const result = AnnotationSchema.safeParse(parsed);\n if (!result.success) {\n throw new Error(\n `Failed to load annotation '${id}': invalid annotation data - ${result.error.message}`\n );\n }\n return result.data;\n }\n\n async create(input: AnnotationCreateInput): Promise<Annotation> {\n const annotation: Annotation = {\n id: this.generateAnnotationId(),\n elementId: input.elementId,\n author: input.author?.trim() || \"Anonymous\",\n content: input.content,\n createdAt: new Date().toISOString(),\n tags: input.tags ?? [],\n resolved: false,\n replies: [],\n };\n\n await ensureDir(this.annotationsDir);\n await atomicWrite(this.getPath(annotation.id), yaml.stringify(annotation));\n return annotation;\n }\n\n /** Partially updates an annotation's content, tags, and/or resolved state. Returns null if not found. */\n async update(id: string, updates: AnnotationUpdateInput): Promise<Annotation | null> {\n const filePath = this.getPath(id);\n await ensureDir(this.annotationsDir);\n const lock = new FileLock(filePath);\n\n return lock.withLock(async () => {\n const annotation = await this.get(id);\n if (!annotation) return null;\n\n const updated: Annotation = {\n ...annotation,\n content: updates.content ?? annotation.content,\n tags: updates.tags ?? annotation.tags,\n resolved: updates.resolved ?? annotation.resolved,\n updatedAt: new Date().toISOString(),\n };\n\n await atomicWrite(filePath, yaml.stringify(updated));\n return updated;\n });\n }\n\n /** Deletes an annotation and its replies. Returns false if it didn't exist. */\n async delete(id: string): Promise<boolean> {\n const filePath = this.getPath(id);\n if (!(await fileExists(filePath))) {\n return false;\n }\n await rm(filePath, { force: true });\n return true;\n }\n\n /** Appends a reply to an annotation. Returns null if the annotation doesn't exist. */\n async addReply(id: string, input: AnnotationReplyCreateInput): Promise<AnnotationReply | null> {\n const filePath = this.getPath(id);\n await ensureDir(this.annotationsDir);\n const lock = new FileLock(filePath);\n\n return lock.withLock(async () => {\n const annotation = await this.get(id);\n if (!annotation) return null;\n\n const reply: AnnotationReply = {\n id: this.generateReplyId(),\n author: input.author,\n content: input.content,\n createdAt: new Date().toISOString(),\n };\n\n annotation.replies.push(reply);\n await atomicWrite(filePath, yaml.stringify(annotation));\n return reply;\n });\n }\n\n private getPath(id: string): string {\n return path.join(this.annotationsDir, `${this.sanitizeId(id)}.yaml`);\n }\n\n /** Whitelists safe characters so an annotation ID can't escape annotationsDir via path traversal. */\n private sanitizeId(id: string): string {\n const sanitized = id.replace(/[^a-zA-Z0-9_-]/g, \"\");\n if (!sanitized) {\n throw new Error(`Invalid annotation ID '${id}'`);\n }\n return sanitized;\n }\n\n private generateAnnotationId(): string {\n return `ann-${Date.now()}-${randomBytes(4).toString(\"hex\")}`;\n }\n\n private generateReplyId(): string {\n return `reply-${Date.now()}-${randomBytes(4).toString(\"hex\")}`;\n }\n}\n"],
|
|
5
|
+
"mappings": "AAWA,SAAS,UAAU,SAAS,UAAU;AACtC,SAAS,mBAAmB;AAC5B,OAAO,UAAU;AACjB,OAAO,UAAU;AACjB,SAAS,SAAS;AAClB,SAAS,aAAa,WAAW,kBAAkB;AACnD,SAAS,gBAAgB;AACzB,SAAS,uBAAuB;AAEhC,MAAM,wBAAwB,EAAE,OAAO;EACrC,IAAI,EAAE,OAAM;EACZ,QAAQ,EAAE,OAAM;EAChB,SAAS,EAAE,OAAM;EACjB,WAAW,EAAE,OAAM;CACpB,GAOK,mBAAmB,EAAE,OAAO;EAChC,IAAI,EAAE,OAAM;EACZ,WAAW,EAAE,OAAM;EACnB,QAAQ,EAAE,OAAM;EAChB,SAAS,EAAE,OAAM;EACjB,WAAW,EAAE,OAAM;EACnB,WAAW,EAAE,OAAM,EAAG,SAAQ;EAC9B,MAAM,EAAE,MAAM,EAAE,OAAM,CAAE;EACxB,UAAU,EAAE,QAAO;EACnB,SAAS,EAAE,MAAM,qBAAqB;CACvC;AAwBK,MAAO,gBAAe;EAClB;EAER,YAAY,UAAgB;AAC1B,SAAK,iBAAiB,KAAK,KAAK,UAAU,0BAA0B,aAAa;EACnF;;EAGA,MAAM,KAAK,WAAkB;AAC3B,QAAI,CAAE,MAAM,WAAW,KAAK,cAAc;AACxC,aAAO,CAAA;AAGT,UAAM,UAAU,MAAM,QAAQ,KAAK,gBAAgB,EAAE,eAAe,GAAI,CAAE,GACpE,cAA4B,CAAA;AAElC,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,OAAM,KAAM,CAAC,MAAM,KAAK,SAAS,OAAO;AAAG;AACtD,YAAM,KAAK,MAAM,KAAK,MAAM,GAAG,EAAe;AAE9C,UAAI;AACJ,UAAI;AACF,qBAAa,MAAM,KAAK,IAAI,EAAE;MAChC,SAAS,OAAO;AACd,gBAAQ,OAAO,MAAM,iCAAiC,EAAE,MAAM,gBAAgB,KAAK,CAAC;CAAI;AACxF;MACF;AAEA,MAAI,eAAe,CAAC,aAAa,WAAW,cAAc,cACxD,YAAY,KAAK,UAAU;IAE/B;AAEA,WAAO,YAAY,KAAK,CAAC,GAAG,MAAM,EAAE,UAAU,cAAc,EAAE,SAAS,CAAC;EAC1E;EAEA,MAAM,IAAI,IAAU;AAClB,UAAM,WAAW,KAAK,QAAQ,EAAE;AAChC,QAAI,CAAE,MAAM,WAAW,QAAQ;AAC7B,aAAO;AAGT,QAAI;AACJ,QAAI;AACF,YAAM,UAAU,MAAM,SAAS,UAAU,OAAO;AAChD,eAAS,KAAK,MAAM,OAAO;IAC7B,SAAS,OAAO;AACd,YAAM,IAAI,MAAM,8BAA8B,EAAE,MAAM,gBAAgB,KAAK,CAAC,EAAE;IAChF;AAEA,UAAM,SAAS,iBAAiB,UAAU,MAAM;AAChD,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MACR,8BAA8B,EAAE,gCAAgC,OAAO,MAAM,OAAO,EAAE;AAG1F,WAAO,OAAO;EAChB;EAEA,MAAM,OAAO,OAA4B;AACvC,UAAM,aAAyB;MAC7B,IAAI,KAAK,qBAAoB;MAC7B,WAAW,MAAM;MACjB,QAAQ,MAAM,QAAQ,KAAI,KAAM;MAChC,SAAS,MAAM;MACf,YAAW,oBAAI,KAAI,GAAG,YAAW;MACjC,MAAM,MAAM,QAAQ,CAAA;MACpB,UAAU;MACV,SAAS,CAAA;;AAGX,iBAAM,UAAU,KAAK,cAAc,GACnC,MAAM,YAAY,KAAK,QAAQ,WAAW,EAAE,GAAG,KAAK,UAAU,UAAU,CAAC,GAClE;EACT;;EAGA,MAAM,OAAO,IAAY,SAA8B;AACrD,UAAM,WAAW,KAAK,QAAQ,EAAE;AAChC,iBAAM,UAAU,KAAK,cAAc,GACtB,IAAI,SAAS,QAAQ,EAEtB,SAAS,YAAW;AAC9B,YAAM,aAAa,MAAM,KAAK,IAAI,EAAE;AACpC,UAAI,CAAC;AAAY,eAAO;AAExB,YAAM,UAAsB;QAC1B,GAAG;QACH,SAAS,QAAQ,WAAW,WAAW;QACvC,MAAM,QAAQ,QAAQ,WAAW;QACjC,UAAU,QAAQ,YAAY,WAAW;QACzC,YAAW,oBAAI,KAAI,GAAG,YAAW;;AAGnC,mBAAM,YAAY,UAAU,KAAK,UAAU,OAAO,CAAC,GAC5C;IACT,CAAC;EACH;;EAGA,MAAM,OAAO,IAAU;AACrB,UAAM,WAAW,KAAK,QAAQ,EAAE;AAChC,WAAM,MAAM,WAAW,QAAQ,KAG/B,MAAM,GAAG,UAAU,EAAE,OAAO,GAAI,CAAE,GAC3B,MAHE;EAIX;;EAGA,MAAM,SAAS,IAAY,OAAiC;AAC1D,UAAM,WAAW,KAAK,QAAQ,EAAE;AAChC,iBAAM,UAAU,KAAK,cAAc,GACtB,IAAI,SAAS,QAAQ,EAEtB,SAAS,YAAW;AAC9B,YAAM,aAAa,MAAM,KAAK,IAAI,EAAE;AACpC,UAAI,CAAC;AAAY,eAAO;AAExB,YAAM,QAAyB;QAC7B,IAAI,KAAK,gBAAe;QACxB,QAAQ,MAAM;QACd,SAAS,MAAM;QACf,YAAW,oBAAI,KAAI,GAAG,YAAW;;AAGnC,wBAAW,QAAQ,KAAK,KAAK,GAC7B,MAAM,YAAY,UAAU,KAAK,UAAU,UAAU,CAAC,GAC/C;IACT,CAAC;EACH;EAEQ,QAAQ,IAAU;AACxB,WAAO,KAAK,KAAK,KAAK,gBAAgB,GAAG,KAAK,WAAW,EAAE,CAAC,OAAO;EACrE;;EAGQ,WAAW,IAAU;AAC3B,UAAM,YAAY,GAAG,QAAQ,mBAAmB,EAAE;AAClD,QAAI,CAAC;AACH,YAAM,IAAI,MAAM,0BAA0B,EAAE,GAAG;AAEjD,WAAO;EACT;EAEQ,uBAAoB;AAC1B,WAAO,OAAO,KAAK,IAAG,CAAE,IAAI,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;EAC5D;EAEQ,kBAAe;AACrB,WAAO,SAAS,KAAK,IAAG,CAAE,IAAI,YAAY,CAAC,EAAE,SAAS,KAAK,CAAC;EAC9D;;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { migrateElementTypes, summarizeElementTypeChanges } from "./element-type-migration.js";
|
|
2
|
+
import { migrateRelationshipsFile, migrateStagedChangesets, summarizeRelationshipChanges } from "./relationship-migration.js";
|
|
3
|
+
async function applyCatalogMigrations(model, options = {}) {
|
|
4
|
+
const dryRun = options.dryRun ?? !1, types = await migrateElementTypes(model, void 0, { dryRun }), relationships = types.errors.length ? { filesModified: 0, changes: [], committed: { before: [], after: [] }, error: void 0 } : await migrateRelationshipsFile(model, void 0, { dryRun }), staged = relationships.error ? { changesetsModified: 0, changes: [], errors: [] } : await migrateStagedChangesets(model, relationships.committed, void 0, { dryRun }), summaries = [
|
|
5
|
+
...types.changes.length > 0 ? [`element types renamed: ${summarizeElementTypeChanges(types.changes)}`] : [],
|
|
6
|
+
relationships.changes.length > 0 ? summarizeRelationshipChanges(relationships.changes) : "no relationships to rewrite",
|
|
7
|
+
...staged.changes.length > 0 ? [summarizeRelationshipChanges(staged.changes, "staged relationship changes")] : []
|
|
8
|
+
];
|
|
9
|
+
return {
|
|
10
|
+
filesModified: types.filesModified + types.changesetsModified + relationships.filesModified + staged.changesetsModified,
|
|
11
|
+
summaries,
|
|
12
|
+
errors: [...types.errors, ...relationships.error ? [relationships.error] : [], ...staged.errors],
|
|
13
|
+
details: {
|
|
14
|
+
elementTypeChanges: types.changes,
|
|
15
|
+
relationshipChanges: relationships.changes,
|
|
16
|
+
stagedRelationshipChanges: staged.changes
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function pendingCatalogMigrations(model) {
|
|
21
|
+
const r = await applyCatalogMigrations(model, { dryRun: !0 });
|
|
22
|
+
return {
|
|
23
|
+
elementTypes: r.details.elementTypeChanges.length,
|
|
24
|
+
relationships: r.details.relationshipChanges.length,
|
|
25
|
+
stagedRelationships: r.details.stagedRelationshipChanges.length,
|
|
26
|
+
errors: r.errors
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export {
|
|
30
|
+
applyCatalogMigrations,
|
|
31
|
+
pendingCatalogMigrations
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=catalog-migration.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/core/catalog-migration.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Catalog migration: brings a model's elements and relationships in line with the current spec\n * catalog, using the generated rewrite table (relationship-migration-table.ts).\n *\n * 1. element type renames (element-type-migration.ts), so the rules below see the new types\n * 2. relationship rewrites in relationships.yaml (relationship-migration.ts)\n * 3. the same rewrites in staged changesets\n *\n * It runs inside the 0.9.0 -> 0.10.0 and 0.10.0 -> 0.11.0 migrations, and on its own when `dr upgrade` finds a model\n * already at the current spec version that still names retired types or relationships: the\n * catalog can change between spec version bumps, and a model stamped with the version before\n * a change would otherwise never receive it. Every step only rewrites what the table matches,\n * so running it again on a migrated model changes nothing.\n */\nimport type { Model } from \"./model.js\";\nimport { migrateElementTypes, summarizeElementTypeChanges, type ElementTypeRenameChange } from \"./element-type-migration.js\";\nimport {\n migrateRelationshipsFile,\n migrateStagedChangesets,\n summarizeRelationshipChanges,\n type RelationshipRewriteChange,\n type StagedRelationshipRewriteChange,\n} from \"./relationship-migration.js\";\n\nexport interface CatalogMigrationResult {\n filesModified: number;\n /** One phrase per kind of change, for the migration description */\n summaries: string[];\n errors: string[];\n details: {\n elementTypeChanges: ElementTypeRenameChange[];\n relationshipChanges: RelationshipRewriteChange[];\n stagedRelationshipChanges: StagedRelationshipRewriteChange[];\n };\n}\n\nexport async function applyCatalogMigrations(\n model: Model,\n options: { dryRun?: boolean } = {}\n): Promise<CatalogMigrationResult> {\n const dryRun = options.dryRun ?? false;\n // Renamed element types first (product.capability -> product.softwarecapability), so the\n // relationship rules, which name the new types, resolve the renamed elements.\n const types = await migrateElementTypes(model, undefined, { dryRun });\n\n const relationships = types.errors.length\n ? { filesModified: 0, changes: [], committed: { before: [], after: [] }, error: undefined }\n : await migrateRelationshipsFile(model, undefined, { dryRun });\n const staged = relationships.error\n ? { changesetsModified: 0, changes: [], errors: [] }\n : await migrateStagedChangesets(model, relationships.committed, undefined, { dryRun });\n\n const summaries = [\n ...(types.changes.length > 0 ? [`element types renamed: ${summarizeElementTypeChanges(types.changes)}`] : []),\n relationships.changes.length > 0 ? summarizeRelationshipChanges(relationships.changes) : \"no relationships to rewrite\",\n ...(staged.changes.length > 0 ? [summarizeRelationshipChanges(staged.changes, \"staged relationship changes\")] : []),\n ];\n\n return {\n filesModified: types.filesModified + types.changesetsModified + relationships.filesModified + staged.changesetsModified,\n summaries,\n errors: [...types.errors, ...(relationships.error ? [relationships.error] : []), ...staged.errors],\n details: {\n elementTypeChanges: types.changes,\n relationshipChanges: relationships.changes,\n stagedRelationshipChanges: staged.changes,\n },\n };\n}\n\n/** How many catalog changes the model still needs; all zero when it is in line with the catalog */\nexport async function pendingCatalogMigrations(\n model: Model\n): Promise<{ elementTypes: number; relationships: number; stagedRelationships: number; errors: string[] }> {\n const r = await applyCatalogMigrations(model, { dryRun: true });\n return {\n elementTypes: r.details.elementTypeChanges.length,\n relationships: r.details.relationshipChanges.length,\n stagedRelationships: r.details.stagedRelationshipChanges.length,\n errors: r.errors,\n };\n}\n"],
|
|
5
|
+
"mappings": "AAeA,SAAS,qBAAqB,mCAAiE;AAC/F,SACE,0BACA,yBACA,oCAGK;AAcP,eAAsB,uBACpB,OACA,UAAgC,CAAA,GAAE;AAElC,QAAM,SAAS,QAAQ,UAAU,IAG3B,QAAQ,MAAM,oBAAoB,OAAO,QAAW,EAAE,OAAM,CAAE,GAE9D,gBAAgB,MAAM,OAAO,SAC/B,EAAE,eAAe,GAAG,SAAS,CAAA,GAAI,WAAW,EAAE,QAAQ,CAAA,GAAI,OAAO,CAAA,EAAE,GAAI,OAAO,OAAS,IACvF,MAAM,yBAAyB,OAAO,QAAW,EAAE,OAAM,CAAE,GACzD,SAAS,cAAc,QACzB,EAAE,oBAAoB,GAAG,SAAS,CAAA,GAAI,QAAQ,CAAA,EAAE,IAChD,MAAM,wBAAwB,OAAO,cAAc,WAAW,QAAW,EAAE,OAAM,CAAE,GAEjF,YAAY;IAChB,GAAI,MAAM,QAAQ,SAAS,IAAI,CAAC,0BAA0B,4BAA4B,MAAM,OAAO,CAAC,EAAE,IAAI,CAAA;IAC1G,cAAc,QAAQ,SAAS,IAAI,6BAA6B,cAAc,OAAO,IAAI;IACzF,GAAI,OAAO,QAAQ,SAAS,IAAI,CAAC,6BAA6B,OAAO,SAAS,6BAA6B,CAAC,IAAI,CAAA;;AAGlH,SAAO;IACL,eAAe,MAAM,gBAAgB,MAAM,qBAAqB,cAAc,gBAAgB,OAAO;IACrG;IACA,QAAQ,CAAC,GAAG,MAAM,QAAQ,GAAI,cAAc,QAAQ,CAAC,cAAc,KAAK,IAAI,CAAA,GAAK,GAAG,OAAO,MAAM;IACjG,SAAS;MACP,oBAAoB,MAAM;MAC1B,qBAAqB,cAAc;MACnC,2BAA2B,OAAO;;;AAGxC;AAGA,eAAsB,yBACpB,OAAY;AAEZ,QAAM,IAAI,MAAM,uBAAuB,OAAO,EAAE,QAAQ,GAAI,CAAE;AAC9D,SAAO;IACL,cAAc,EAAE,QAAQ,mBAAmB;IAC3C,eAAe,EAAE,QAAQ,oBAAoB;IAC7C,qBAAqB,EAAE,QAAQ,0BAA0B;IACzD,QAAQ,EAAE;;AAEd;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|