hammoc 1.3.0 → 1.5.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 +422 -403
- package/bin/hammoc.js +0 -6
- package/package.json +100 -93
- package/packages/client/dist/assets/agentExampleHighlight-BgwTm15v.js +1 -0
- package/packages/client/dist/assets/commandTokenHighlight-BljHwnrK.js +1 -0
- package/packages/client/dist/assets/index-CjyjnXB8.css +32 -0
- package/packages/client/dist/assets/index-D3LxqW3f.js +2 -0
- package/packages/client/dist/assets/index-NqJdhlek.js +1498 -0
- package/packages/client/dist/assets/snippetTokenHighlight-DWsaQXX0.js +1 -0
- package/packages/client/dist/index.html +2 -2
- package/packages/client/dist/sw.js +1 -1
- package/packages/server/dist/app.d.ts.map +1 -1
- package/packages/server/dist/app.js +24 -24
- package/packages/server/dist/app.js.map +1 -1
- package/packages/server/dist/controllers/boardController.d.ts.map +1 -1
- package/packages/server/dist/controllers/boardController.js +0 -5
- package/packages/server/dist/controllers/boardController.js.map +1 -1
- package/packages/server/dist/controllers/claudeMdController.d.ts +26 -0
- package/packages/server/dist/controllers/claudeMdController.d.ts.map +1 -0
- package/packages/server/dist/controllers/claudeMdController.js +158 -0
- package/packages/server/dist/controllers/claudeMdController.js.map +1 -0
- package/packages/server/dist/controllers/fileSystemController.d.ts +4 -0
- package/packages/server/dist/controllers/fileSystemController.d.ts.map +1 -1
- package/packages/server/dist/controllers/fileSystemController.js +20 -2
- package/packages/server/dist/controllers/fileSystemController.js.map +1 -1
- package/packages/server/dist/controllers/harnessAgentController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessAgentController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessAgentController.js +339 -0
- package/packages/server/dist/controllers/harnessAgentController.js.map +1 -0
- package/packages/server/dist/controllers/harnessCommandController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessCommandController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessCommandController.js +382 -0
- package/packages/server/dist/controllers/harnessCommandController.js.map +1 -0
- package/packages/server/dist/controllers/harnessController.d.ts +21 -0
- package/packages/server/dist/controllers/harnessController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessController.js +176 -0
- package/packages/server/dist/controllers/harnessController.js.map +1 -0
- package/packages/server/dist/controllers/harnessHookController.d.ts +32 -0
- package/packages/server/dist/controllers/harnessHookController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessHookController.js +363 -0
- package/packages/server/dist/controllers/harnessHookController.js.map +1 -0
- package/packages/server/dist/controllers/harnessLintController.d.ts +18 -0
- package/packages/server/dist/controllers/harnessLintController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessLintController.js +72 -0
- package/packages/server/dist/controllers/harnessLintController.js.map +1 -0
- package/packages/server/dist/controllers/harnessMcpController.d.ts +28 -0
- package/packages/server/dist/controllers/harnessMcpController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessMcpController.js +310 -0
- package/packages/server/dist/controllers/harnessMcpController.js.map +1 -0
- package/packages/server/dist/controllers/harnessPluginController.d.ts +17 -0
- package/packages/server/dist/controllers/harnessPluginController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessPluginController.js +115 -0
- package/packages/server/dist/controllers/harnessPluginController.js.map +1 -0
- package/packages/server/dist/controllers/harnessShareScopeController.d.ts +15 -0
- package/packages/server/dist/controllers/harnessShareScopeController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessShareScopeController.js +73 -0
- package/packages/server/dist/controllers/harnessShareScopeController.js.map +1 -0
- package/packages/server/dist/controllers/harnessSkillController.d.ts +32 -0
- package/packages/server/dist/controllers/harnessSkillController.d.ts.map +1 -0
- package/packages/server/dist/controllers/harnessSkillController.js +453 -0
- package/packages/server/dist/controllers/harnessSkillController.js.map +1 -0
- package/packages/server/dist/controllers/projectController.d.ts.map +1 -1
- package/packages/server/dist/controllers/projectController.js +11 -0
- package/packages/server/dist/controllers/projectController.js.map +1 -1
- package/packages/server/dist/controllers/serverController.d.ts.map +1 -1
- package/packages/server/dist/controllers/serverController.js +84 -49
- package/packages/server/dist/controllers/serverController.js.map +1 -1
- package/packages/server/dist/controllers/snippetController.d.ts +35 -0
- package/packages/server/dist/controllers/snippetController.d.ts.map +1 -0
- package/packages/server/dist/controllers/snippetController.js +294 -0
- package/packages/server/dist/controllers/snippetController.js.map +1 -0
- package/packages/server/dist/handlers/websocket.d.ts +16 -0
- package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
- package/packages/server/dist/handlers/websocket.js +221 -8
- package/packages/server/dist/handlers/websocket.js.map +1 -1
- package/packages/server/dist/index.js +66 -0
- package/packages/server/dist/index.js.map +1 -1
- package/packages/server/dist/locales/en/server.json +41 -6
- package/packages/server/dist/locales/es/server.json +3 -5
- package/packages/server/dist/locales/ja/server.json +3 -5
- package/packages/server/dist/locales/ko/server.json +4 -6
- package/packages/server/dist/locales/pt/server.json +3 -5
- package/packages/server/dist/locales/zh-CN/server.json +3 -5
- package/packages/server/dist/routes/account.d.ts +7 -0
- package/packages/server/dist/routes/account.d.ts.map +1 -0
- package/packages/server/dist/routes/account.js +35 -0
- package/packages/server/dist/routes/account.js.map +1 -0
- package/packages/server/dist/routes/debug.d.ts +1 -1
- package/packages/server/dist/routes/debug.d.ts.map +1 -1
- package/packages/server/dist/routes/debug.js +60 -1
- package/packages/server/dist/routes/debug.js.map +1 -1
- package/packages/server/dist/routes/harness.d.ts +8 -0
- package/packages/server/dist/routes/harness.d.ts.map +1 -0
- package/packages/server/dist/routes/harness.js +92 -0
- package/packages/server/dist/routes/harness.js.map +1 -0
- package/packages/server/dist/routes/preferences.d.ts.map +1 -1
- package/packages/server/dist/routes/preferences.js +11 -2
- package/packages/server/dist/routes/preferences.js.map +1 -1
- package/packages/server/dist/routes/projects.d.ts.map +1 -1
- package/packages/server/dist/routes/projects.js +5 -60
- package/packages/server/dist/routes/projects.js.map +1 -1
- package/packages/server/dist/routes/snippets.d.ts +14 -0
- package/packages/server/dist/routes/snippets.d.ts.map +1 -0
- package/packages/server/dist/routes/snippets.js +27 -0
- package/packages/server/dist/routes/snippets.js.map +1 -0
- package/packages/server/dist/services/accountInfoService.d.ts +38 -0
- package/packages/server/dist/services/accountInfoService.d.ts.map +1 -0
- package/packages/server/dist/services/accountInfoService.js +118 -0
- package/packages/server/dist/services/accountInfoService.js.map +1 -0
- package/packages/server/dist/services/bmadStatusService.d.ts +6 -2
- package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
- package/packages/server/dist/services/bmadStatusService.js +88 -32
- package/packages/server/dist/services/bmadStatusService.js.map +1 -1
- package/packages/server/dist/services/chatService.d.ts +3 -0
- package/packages/server/dist/services/chatService.d.ts.map +1 -1
- package/packages/server/dist/services/chatService.js +36 -8
- package/packages/server/dist/services/chatService.js.map +1 -1
- package/packages/server/dist/services/claudeMdService.d.ts +48 -0
- package/packages/server/dist/services/claudeMdService.d.ts.map +1 -0
- package/packages/server/dist/services/claudeMdService.js +240 -0
- package/packages/server/dist/services/claudeMdService.js.map +1 -0
- package/packages/server/dist/services/commandService.d.ts +10 -0
- package/packages/server/dist/services/commandService.d.ts.map +1 -1
- package/packages/server/dist/services/commandService.js +129 -4
- package/packages/server/dist/services/commandService.js.map +1 -1
- package/packages/server/dist/services/fileSystemService.d.ts +7 -1
- package/packages/server/dist/services/fileSystemService.d.ts.map +1 -1
- package/packages/server/dist/services/fileSystemService.js +67 -8
- package/packages/server/dist/services/fileSystemService.js.map +1 -1
- package/packages/server/dist/services/fileWatcherService.d.ts +59 -0
- package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -0
- package/packages/server/dist/services/fileWatcherService.js +329 -0
- package/packages/server/dist/services/fileWatcherService.js.map +1 -0
- package/packages/server/dist/services/gitService.d.ts.map +1 -1
- package/packages/server/dist/services/gitService.js +67 -7
- package/packages/server/dist/services/gitService.js.map +1 -1
- package/packages/server/dist/services/harnessAgentService.d.ts +79 -0
- package/packages/server/dist/services/harnessAgentService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessAgentService.js +933 -0
- package/packages/server/dist/services/harnessAgentService.js.map +1 -0
- package/packages/server/dist/services/harnessCommandService.d.ts +60 -0
- package/packages/server/dist/services/harnessCommandService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessCommandService.js +853 -0
- package/packages/server/dist/services/harnessCommandService.js.map +1 -0
- package/packages/server/dist/services/harnessHookService.d.ts +55 -0
- package/packages/server/dist/services/harnessHookService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessHookService.js +1060 -0
- package/packages/server/dist/services/harnessHookService.js.map +1 -0
- package/packages/server/dist/services/harnessLintService.d.ts +49 -0
- package/packages/server/dist/services/harnessLintService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessLintService.js +628 -0
- package/packages/server/dist/services/harnessLintService.js.map +1 -0
- package/packages/server/dist/services/harnessMcpService.d.ts +77 -0
- package/packages/server/dist/services/harnessMcpService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessMcpService.js +814 -0
- package/packages/server/dist/services/harnessMcpService.js.map +1 -0
- package/packages/server/dist/services/harnessPluginService.d.ts +66 -0
- package/packages/server/dist/services/harnessPluginService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessPluginService.js +559 -0
- package/packages/server/dist/services/harnessPluginService.js.map +1 -0
- package/packages/server/dist/services/harnessService.d.ts +40 -0
- package/packages/server/dist/services/harnessService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessService.js +222 -0
- package/packages/server/dist/services/harnessService.js.map +1 -0
- package/packages/server/dist/services/harnessShareScopeService.d.ts +31 -0
- package/packages/server/dist/services/harnessShareScopeService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessShareScopeService.js +93 -0
- package/packages/server/dist/services/harnessShareScopeService.js.map +1 -0
- package/packages/server/dist/services/harnessSkillService.d.ts +70 -0
- package/packages/server/dist/services/harnessSkillService.d.ts.map +1 -0
- package/packages/server/dist/services/harnessSkillService.js +636 -0
- package/packages/server/dist/services/harnessSkillService.js.map +1 -0
- package/packages/server/dist/services/historyParser.d.ts +4 -14
- package/packages/server/dist/services/historyParser.d.ts.map +1 -1
- package/packages/server/dist/services/historyParser.js +60 -5
- package/packages/server/dist/services/historyParser.js.map +1 -1
- package/packages/server/dist/services/issueService.d.ts.map +1 -1
- package/packages/server/dist/services/issueService.js +10 -2
- package/packages/server/dist/services/issueService.js.map +1 -1
- package/packages/server/dist/services/manualSyncService.d.ts +19 -0
- package/packages/server/dist/services/manualSyncService.d.ts.map +1 -0
- package/packages/server/dist/services/manualSyncService.js +110 -0
- package/packages/server/dist/services/manualSyncService.js.map +1 -0
- package/packages/server/dist/services/notificationService.d.ts.map +1 -1
- package/packages/server/dist/services/notificationService.js +34 -9
- package/packages/server/dist/services/notificationService.js.map +1 -1
- package/packages/server/dist/services/preferencesService.d.ts.map +1 -1
- package/packages/server/dist/services/preferencesService.js +8 -1
- package/packages/server/dist/services/preferencesService.js.map +1 -1
- package/packages/server/dist/services/projectService.d.ts +5 -0
- package/packages/server/dist/services/projectService.d.ts.map +1 -1
- package/packages/server/dist/services/projectService.js +42 -2
- package/packages/server/dist/services/projectService.js.map +1 -1
- package/packages/server/dist/services/ptyService.d.ts +1 -0
- package/packages/server/dist/services/ptyService.d.ts.map +1 -1
- package/packages/server/dist/services/ptyService.js +36 -5
- package/packages/server/dist/services/ptyService.js.map +1 -1
- package/packages/server/dist/services/queueService.d.ts.map +1 -1
- package/packages/server/dist/services/queueService.js +83 -14
- package/packages/server/dist/services/queueService.js.map +1 -1
- package/packages/server/dist/services/sessionBufferManager.d.ts.map +1 -1
- package/packages/server/dist/services/sessionBufferManager.js +26 -0
- package/packages/server/dist/services/sessionBufferManager.js.map +1 -1
- package/packages/server/dist/services/sessionService.d.ts +4 -3
- package/packages/server/dist/services/sessionService.d.ts.map +1 -1
- package/packages/server/dist/services/sessionService.js +5 -4
- package/packages/server/dist/services/sessionService.js.map +1 -1
- package/packages/server/dist/services/snippetService.d.ts +54 -0
- package/packages/server/dist/services/snippetService.d.ts.map +1 -0
- package/packages/server/dist/services/snippetService.js +371 -0
- package/packages/server/dist/services/snippetService.js.map +1 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts +46 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.d.ts.map +1 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js +125 -0
- package/packages/server/dist/services/utils/applyYamlFrontmatterPatch.js.map +1 -0
- package/packages/server/dist/services/webPushService.d.ts.map +1 -1
- package/packages/server/dist/services/webPushService.js +8 -1
- package/packages/server/dist/services/webPushService.js.map +1 -1
- package/packages/server/dist/snippets/split-commit +9 -0
- package/packages/server/dist/utils/applySecretsPolicy.d.ts +53 -0
- package/packages/server/dist/utils/applySecretsPolicy.d.ts.map +1 -0
- package/packages/server/dist/utils/applySecretsPolicy.js +204 -0
- package/packages/server/dist/utils/applySecretsPolicy.js.map +1 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.d.ts +40 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.d.ts.map +1 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.js +47 -0
- package/packages/server/dist/utils/assertNoSecretOnShared.js.map +1 -0
- package/packages/server/dist/utils/effortUtils.d.ts +21 -0
- package/packages/server/dist/utils/effortUtils.d.ts.map +1 -0
- package/packages/server/dist/utils/effortUtils.js +36 -0
- package/packages/server/dist/utils/effortUtils.js.map +1 -0
- package/packages/server/dist/utils/gitignoreFilter.d.ts +23 -0
- package/packages/server/dist/utils/gitignoreFilter.d.ts.map +1 -0
- package/packages/server/dist/utils/gitignoreFilter.js +42 -0
- package/packages/server/dist/utils/gitignoreFilter.js.map +1 -0
- package/packages/server/dist/utils/harnessBundleSchema.d.ts +105 -0
- package/packages/server/dist/utils/harnessBundleSchema.d.ts.map +1 -0
- package/packages/server/dist/utils/harnessBundleSchema.js +79 -0
- package/packages/server/dist/utils/harnessBundleSchema.js.map +1 -0
- package/packages/server/dist/utils/harnessPaths.d.ts +34 -0
- package/packages/server/dist/utils/harnessPaths.d.ts.map +1 -0
- package/packages/server/dist/utils/harnessPaths.js +124 -0
- package/packages/server/dist/utils/harnessPaths.js.map +1 -0
- package/packages/server/dist/utils/pathUtils.d.ts +3 -2
- package/packages/server/dist/utils/pathUtils.d.ts.map +1 -1
- package/packages/server/dist/utils/pathUtils.js +26 -2
- package/packages/server/dist/utils/pathUtils.js.map +1 -1
- package/packages/server/dist/utils/secretHeuristic.d.ts +72 -0
- package/packages/server/dist/utils/secretHeuristic.d.ts.map +1 -0
- package/packages/server/dist/utils/secretHeuristic.js +163 -0
- package/packages/server/dist/utils/secretHeuristic.js.map +1 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.d.ts +41 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.d.ts.map +1 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.js +81 -0
- package/packages/server/dist/utils/secretPlaceholderNamer.js.map +1 -0
- package/packages/server/dist/utils/serverPathResolver.d.ts +29 -0
- package/packages/server/dist/utils/serverPathResolver.d.ts.map +1 -0
- package/packages/server/dist/utils/serverPathResolver.js +59 -0
- package/packages/server/dist/utils/serverPathResolver.js.map +1 -0
- package/packages/server/dist/utils/snippetPaths.d.ts +61 -0
- package/packages/server/dist/utils/snippetPaths.d.ts.map +1 -0
- package/packages/server/dist/utils/snippetPaths.js +123 -0
- package/packages/server/dist/utils/snippetPaths.js.map +1 -0
- package/packages/server/dist/utils/structuredEditor.d.ts +34 -0
- package/packages/server/dist/utils/structuredEditor.d.ts.map +1 -0
- package/packages/server/dist/utils/structuredEditor.js +111 -0
- package/packages/server/dist/utils/structuredEditor.js.map +1 -0
- package/packages/server/package.json +6 -2
- package/packages/server/resources/internals/INDEX.md +23 -0
- package/packages/server/resources/internals/harness-files.md +63 -0
- package/packages/server/resources/internals/image-storage.md +43 -0
- package/packages/server/resources/manual/01-getting-started.md +104 -0
- package/packages/server/resources/manual/02-chat.md +285 -0
- package/packages/server/resources/manual/03-sessions.md +48 -0
- package/packages/server/resources/manual/04-slash-commands-favorites.md +152 -0
- package/packages/server/resources/manual/05-projects.md +74 -0
- package/packages/server/resources/manual/06-file-explorer-editor.md +90 -0
- package/packages/server/resources/manual/07-git.md +94 -0
- package/packages/server/resources/manual/08-terminal.md +59 -0
- package/packages/server/resources/manual/09-queue-runner.md +262 -0
- package/packages/server/resources/manual/10-project-board.md +193 -0
- package/packages/server/resources/manual/11-bmad-method-integration.md +128 -0
- package/packages/server/resources/manual/12-harness-workbench.md +175 -0
- package/packages/server/resources/manual/13-settings.md +241 -0
- package/packages/server/resources/manual/14-keyboard-shortcuts.md +68 -0
- package/packages/server/resources/manual/15-environment-variables.md +28 -0
- package/packages/server/resources/manual/16-troubleshooting.md +110 -0
- package/packages/server/resources/manual/INDEX.md +60 -0
- package/packages/shared/dist/index.d.ts +3 -0
- package/packages/shared/dist/index.d.ts.map +1 -1
- package/packages/shared/dist/index.js +6 -0
- package/packages/shared/dist/index.js.map +1 -1
- package/packages/shared/dist/types/command.d.ts +3 -3
- package/packages/shared/dist/types/command.d.ts.map +1 -1
- package/packages/shared/dist/types/fileSystem.d.ts +19 -0
- package/packages/shared/dist/types/fileSystem.d.ts.map +1 -1
- package/packages/shared/dist/types/fileSystem.js +5 -0
- package/packages/shared/dist/types/fileSystem.js.map +1 -1
- package/packages/shared/dist/types/git.d.ts +6 -1
- package/packages/shared/dist/types/git.d.ts.map +1 -1
- package/packages/shared/dist/types/git.js.map +1 -1
- package/packages/shared/dist/types/harness.d.ts +1211 -0
- package/packages/shared/dist/types/harness.d.ts.map +1 -0
- package/packages/shared/dist/types/harness.js +107 -0
- package/packages/shared/dist/types/harness.js.map +1 -0
- package/packages/shared/dist/types/harnessBundle.d.ts +170 -0
- package/packages/shared/dist/types/harnessBundle.d.ts.map +1 -0
- package/packages/shared/dist/types/harnessBundle.js +18 -0
- package/packages/shared/dist/types/harnessBundle.js.map +1 -0
- package/packages/shared/dist/types/history.d.ts +7 -0
- package/packages/shared/dist/types/history.d.ts.map +1 -1
- package/packages/shared/dist/types/preferences.d.ts +4 -1
- package/packages/shared/dist/types/preferences.d.ts.map +1 -1
- package/packages/shared/dist/types/preferences.js +1 -0
- package/packages/shared/dist/types/preferences.js.map +1 -1
- package/packages/shared/dist/types/queue.d.ts +9 -0
- package/packages/shared/dist/types/queue.d.ts.map +1 -1
- package/packages/shared/dist/types/sdk.d.ts +42 -1
- package/packages/shared/dist/types/sdk.d.ts.map +1 -1
- package/packages/shared/dist/types/sdk.js +26 -2
- package/packages/shared/dist/types/sdk.js.map +1 -1
- package/packages/shared/dist/types/websocket.d.ts +24 -0
- package/packages/shared/dist/types/websocket.d.ts.map +1 -1
- package/packages/shared/dist/utils/markdownSections.d.ts +50 -0
- package/packages/shared/dist/utils/markdownSections.d.ts.map +1 -0
- package/packages/shared/dist/utils/markdownSections.js +111 -0
- package/packages/shared/dist/utils/markdownSections.js.map +1 -0
- package/packages/shared/dist/utils/queueParser.d.ts.map +1 -1
- package/packages/shared/dist/utils/queueParser.js +104 -0
- package/packages/shared/dist/utils/queueParser.js.map +1 -1
- package/scripts/build-manual-shards.mjs +100 -0
- package/scripts/mock-telegram.mjs +172 -0
- package/scripts/run-integration-test.mjs +362 -0
- package/packages/client/dist/assets/index-Bf0D9oVJ.css +0 -32
- package/packages/client/dist/assets/index-CRmzoqHy.js +0 -2
- package/packages/client/dist/assets/index-CszGQ29O.js +0 -1432
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 28.5: Harness slash-command service.
|
|
3
|
+
*
|
|
4
|
+
* Combines three sources of `.claude/commands/**\/*.md` files into a single tree:
|
|
5
|
+
* - <projectRoot>/.claude/commands/ (project scope)
|
|
6
|
+
* - ~/.claude/commands/ (user scope)
|
|
7
|
+
* - <pluginInstallPath>/commands/ (plugin scope, read-only)
|
|
8
|
+
*
|
|
9
|
+
* Each leaf .md file becomes one card. Frontmatter is YAML (round-trip via
|
|
10
|
+
* `yaml`(eemeli) — see `applyYamlFrontmatterPatch`); body is plain markdown.
|
|
11
|
+
* Path enumeration runs in parallel both across scopes and across files within
|
|
12
|
+
* each scope (Risk #4 mitigation — Promise.all, no serial for-await).
|
|
13
|
+
*/
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import fs from 'fs/promises';
|
|
16
|
+
import yaml from 'yaml';
|
|
17
|
+
import { HARNESS_ERRORS, } from '@hammoc/shared';
|
|
18
|
+
import { harnessService } from './harnessService.js';
|
|
19
|
+
import { projectService } from './projectService.js';
|
|
20
|
+
import { getUserHarnessRoot } from '../utils/harnessPaths.js';
|
|
21
|
+
import { detectSecretsInText as detectSecretsInTextCanonical } from '../utils/secretHeuristic.js';
|
|
22
|
+
import { assertNoSecretOnShared } from '../utils/assertNoSecretOnShared.js';
|
|
23
|
+
import { applyYamlFrontmatterPatch, splitFrontmatterAndBody, } from './utils/applyYamlFrontmatterPatch.js';
|
|
24
|
+
const SCOPE_PRIORITY = {
|
|
25
|
+
project: 0,
|
|
26
|
+
user: 1,
|
|
27
|
+
plugin: 2,
|
|
28
|
+
};
|
|
29
|
+
const ALLOWED_MODELS = new Set([
|
|
30
|
+
'inherit',
|
|
31
|
+
'sonnet',
|
|
32
|
+
'opus',
|
|
33
|
+
'haiku',
|
|
34
|
+
]);
|
|
35
|
+
const COMMANDS_DIR = 'commands';
|
|
36
|
+
const MAX_WALK_DEPTH = 32;
|
|
37
|
+
// Story 30.1 (Task 1.2): SECRET_PATTERNS / ENV_REF_RE moved to
|
|
38
|
+
// `utils/secretHeuristic.ts`. The wrappers below adapt the canonical entry
|
|
39
|
+
// point to this service's existing `{ matched, lines }` shape.
|
|
40
|
+
const POSITIONAL_ARG_RE = /\$([1-9]\d*)\b/;
|
|
41
|
+
const ARGUMENTS_ALL_RE = /\$ARGUMENTS\b/;
|
|
42
|
+
const FILE_REF_RE = /(?:^|\s)@([\w./-]+)/;
|
|
43
|
+
const BASH_EXEC_RE = /!`[^`]+`/;
|
|
44
|
+
const PLUGIN_ROOT_RE = /\$\{CLAUDE_PLUGIN_ROOT\}/;
|
|
45
|
+
const BMAD_MARKER_RE = /<!--\s*Powered\s+by\s+BMAD™?\s+Core\s*-->/i;
|
|
46
|
+
// eslint-disable-next-line no-control-regex -- OS-reserved control chars are intentional here
|
|
47
|
+
const RESERVED_CHARS_RE = /[\\<>:"|?*\x00-\x1F]/;
|
|
48
|
+
const TRAILING_DOT_OR_SPACE_RE = /[. ]$/;
|
|
49
|
+
function throwMapped(code, message, extras) {
|
|
50
|
+
const err = new Error(message);
|
|
51
|
+
err.code = code;
|
|
52
|
+
if (extras)
|
|
53
|
+
Object.assign(err, extras);
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
function isFileNotFound(err) {
|
|
57
|
+
const code = err?.code;
|
|
58
|
+
return code === 'ENOENT' || code === HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code;
|
|
59
|
+
}
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Path / slash-name conversion
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
/** `.md` relative path under commands root → `/A:B:foo` slash name. */
|
|
64
|
+
function deriveSlashName(relPathPosix) {
|
|
65
|
+
const noExt = relPathPosix.replace(/\.md$/i, '');
|
|
66
|
+
return `/${noExt.replace(/\//g, ':')}`;
|
|
67
|
+
}
|
|
68
|
+
function toPosixRelative(rel) {
|
|
69
|
+
return rel.replace(/\\/g, '/');
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate a relative path under the commands root: forward slashes allowed,
|
|
73
|
+
* OS reserved chars / trailing space-or-dot / `..` traversal rejected,
|
|
74
|
+
* `.md` extension required.
|
|
75
|
+
*/
|
|
76
|
+
function validateRelativePath(relPosix) {
|
|
77
|
+
if (!relPosix.endsWith('.md')) {
|
|
78
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'relativePath must end in .md');
|
|
79
|
+
}
|
|
80
|
+
if (relPosix.includes('..')) {
|
|
81
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'path traversal denied');
|
|
82
|
+
}
|
|
83
|
+
for (const segment of relPosix.split('/')) {
|
|
84
|
+
if (segment.length === 0) {
|
|
85
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'empty path segment not allowed');
|
|
86
|
+
}
|
|
87
|
+
if (RESERVED_CHARS_RE.test(segment)) {
|
|
88
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'OS-reserved characters not allowed in path segments');
|
|
89
|
+
}
|
|
90
|
+
if (TRAILING_DOT_OR_SPACE_RE.test(segment)) {
|
|
91
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'path segments cannot end with space or dot');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Token / secret / parse helpers
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
function analyzeTokens(body) {
|
|
99
|
+
return {
|
|
100
|
+
usesPositionalArgs: POSITIONAL_ARG_RE.test(body),
|
|
101
|
+
usesArgumentsAll: ARGUMENTS_ALL_RE.test(body),
|
|
102
|
+
usesFileRefs: FILE_REF_RE.test(body),
|
|
103
|
+
usesBashExec: BASH_EXEC_RE.test(body),
|
|
104
|
+
usesPluginRoot: PLUGIN_ROOT_RE.test(body),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/** Detect the BMad mirror marker within the first 10 lines of the body. */
|
|
108
|
+
function detectBmadMirror(body) {
|
|
109
|
+
const head = body.split(/\r?\n/).slice(0, 10).join('\n');
|
|
110
|
+
return BMAD_MARKER_RE.test(head);
|
|
111
|
+
}
|
|
112
|
+
function detectSecretsInText(text) {
|
|
113
|
+
const { matched, lines } = detectSecretsInTextCanonical(text);
|
|
114
|
+
return { matched, lines };
|
|
115
|
+
}
|
|
116
|
+
function detectSecretsInRaw(raw) {
|
|
117
|
+
return detectSecretsInText(raw);
|
|
118
|
+
}
|
|
119
|
+
function parseFrontmatterYaml(raw) {
|
|
120
|
+
if (raw === null || raw.trim().length === 0)
|
|
121
|
+
return {};
|
|
122
|
+
let parsed;
|
|
123
|
+
try {
|
|
124
|
+
parsed = yaml.parse(raw);
|
|
125
|
+
}
|
|
126
|
+
catch (cause) {
|
|
127
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, `failed to parse frontmatter: ${cause.message}`);
|
|
128
|
+
}
|
|
129
|
+
if (parsed === null || parsed === undefined)
|
|
130
|
+
return {};
|
|
131
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
132
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'frontmatter must be a YAML mapping');
|
|
133
|
+
}
|
|
134
|
+
const obj = parsed;
|
|
135
|
+
const out = {};
|
|
136
|
+
if (typeof obj.description === 'string')
|
|
137
|
+
out.description = obj.description;
|
|
138
|
+
if (typeof obj['argument-hint'] === 'string')
|
|
139
|
+
out['argument-hint'] = obj['argument-hint'];
|
|
140
|
+
if (typeof obj['allowed-tools'] === 'string')
|
|
141
|
+
out['allowed-tools'] = obj['allowed-tools'];
|
|
142
|
+
if (typeof obj.model === 'string' && ALLOWED_MODELS.has(obj.model)) {
|
|
143
|
+
out.model = obj.model;
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
function frontmatterToPatchObject(fm) {
|
|
148
|
+
return {
|
|
149
|
+
description: fm.description,
|
|
150
|
+
'argument-hint': fm['argument-hint'],
|
|
151
|
+
'allowed-tools': fm['allowed-tools'],
|
|
152
|
+
model: fm.model,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Roots / containment
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
function userCommandsRoot() {
|
|
159
|
+
return path.join(getUserHarnessRoot(), COMMANDS_DIR);
|
|
160
|
+
}
|
|
161
|
+
async function projectCommandsRoot(projectSlug) {
|
|
162
|
+
const projectRoot = await projectService.resolveOriginalPath(projectSlug);
|
|
163
|
+
return path.join(projectRoot, '.claude', COMMANDS_DIR);
|
|
164
|
+
}
|
|
165
|
+
function pluginCommandsRoots(installPath) {
|
|
166
|
+
return [path.join(installPath, COMMANDS_DIR)];
|
|
167
|
+
}
|
|
168
|
+
function withinRoot(absolute, root) {
|
|
169
|
+
const resolved = path.resolve(absolute);
|
|
170
|
+
const resolvedRoot = path.resolve(root);
|
|
171
|
+
return resolved === resolvedRoot || resolved.startsWith(resolvedRoot + path.sep);
|
|
172
|
+
}
|
|
173
|
+
async function readInstalledPlugins() {
|
|
174
|
+
try {
|
|
175
|
+
const res = await harnessService.read({
|
|
176
|
+
scope: 'user',
|
|
177
|
+
relativePath: 'plugins/installed_plugins.json',
|
|
178
|
+
});
|
|
179
|
+
const trimmed = (res.content ?? '').trim();
|
|
180
|
+
if (!trimmed)
|
|
181
|
+
return {};
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(trimmed);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
if (isFileNotFound(err))
|
|
191
|
+
return {};
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function walkMdFiles(root, depth = 0) {
|
|
196
|
+
if (depth > MAX_WALK_DEPTH)
|
|
197
|
+
return [];
|
|
198
|
+
let entries;
|
|
199
|
+
try {
|
|
200
|
+
entries = await fs.readdir(root, { withFileTypes: true });
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
const promises = entries.map(async (entry) => {
|
|
206
|
+
const abs = path.join(root, entry.name);
|
|
207
|
+
if (entry.isDirectory()) {
|
|
208
|
+
return walkMdFiles(abs, depth + 1);
|
|
209
|
+
}
|
|
210
|
+
if (entry.isFile() && entry.name.endsWith('.md')) {
|
|
211
|
+
return [abs];
|
|
212
|
+
}
|
|
213
|
+
return [];
|
|
214
|
+
});
|
|
215
|
+
const nested = await Promise.all(promises);
|
|
216
|
+
return nested.flat();
|
|
217
|
+
}
|
|
218
|
+
async function readMdFile(absolute) {
|
|
219
|
+
let stat;
|
|
220
|
+
try {
|
|
221
|
+
stat = await fs.stat(absolute);
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
if (err.code === 'ENOENT') {
|
|
225
|
+
return { malformed: 'file not found' };
|
|
226
|
+
}
|
|
227
|
+
throw err;
|
|
228
|
+
}
|
|
229
|
+
if (!stat.isFile()) {
|
|
230
|
+
return { malformed: 'not a regular file' };
|
|
231
|
+
}
|
|
232
|
+
let raw;
|
|
233
|
+
try {
|
|
234
|
+
raw = await fs.readFile(absolute, 'utf-8');
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return { malformed: 'failed to read' };
|
|
238
|
+
}
|
|
239
|
+
const { frontmatterRaw, body } = splitFrontmatterAndBody(raw);
|
|
240
|
+
let frontmatter;
|
|
241
|
+
try {
|
|
242
|
+
frontmatter = parseFrontmatterYaml(frontmatterRaw);
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
return { malformed: err.message };
|
|
246
|
+
}
|
|
247
|
+
return { raw, mtime: stat.mtime.toISOString(), frontmatter, body };
|
|
248
|
+
}
|
|
249
|
+
function makeCard(scope, rootAbs, fileAbs, frontmatter, body, mtime, extra) {
|
|
250
|
+
const relativePath = toPosixRelative(path.relative(rootAbs, fileAbs));
|
|
251
|
+
return {
|
|
252
|
+
scope,
|
|
253
|
+
absoluteFile: fileAbs,
|
|
254
|
+
pluginKey: extra.pluginKey,
|
|
255
|
+
projectSlug: extra.projectSlug,
|
|
256
|
+
relativePath,
|
|
257
|
+
slashName: deriveSlashName(relativePath),
|
|
258
|
+
frontmatter,
|
|
259
|
+
tokens: analyzeTokens(body),
|
|
260
|
+
mtime,
|
|
261
|
+
isBmadMirror: detectBmadMirror(body),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
async function enumerateScopeCommands(scope, rootAbs, extra) {
|
|
265
|
+
const files = await walkMdFiles(rootAbs);
|
|
266
|
+
const reads = files.map(async (abs) => {
|
|
267
|
+
const result = await readMdFile(abs);
|
|
268
|
+
if ('malformed' in result) {
|
|
269
|
+
return { kind: 'malformed', entry: { abs, reason: result.malformed } };
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
kind: 'card',
|
|
273
|
+
card: makeCard(scope, rootAbs, abs, result.frontmatter, result.body, result.mtime, extra),
|
|
274
|
+
};
|
|
275
|
+
});
|
|
276
|
+
const settled = await Promise.all(reads);
|
|
277
|
+
const cards = [];
|
|
278
|
+
const malformed = [];
|
|
279
|
+
for (const r of settled) {
|
|
280
|
+
if (r.kind === 'card')
|
|
281
|
+
cards.push(r.card);
|
|
282
|
+
else
|
|
283
|
+
malformed.push({
|
|
284
|
+
scope,
|
|
285
|
+
absoluteFile: r.entry.abs,
|
|
286
|
+
pluginKey: extra.pluginKey,
|
|
287
|
+
projectSlug: extra.projectSlug,
|
|
288
|
+
reason: r.entry.reason,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return { cards, malformed };
|
|
292
|
+
}
|
|
293
|
+
async function enumeratePluginCommands() {
|
|
294
|
+
const installed = await readInstalledPlugins();
|
|
295
|
+
const plugins = installed.plugins ?? {};
|
|
296
|
+
const tasks = [];
|
|
297
|
+
for (const [pluginKey, value] of Object.entries(plugins)) {
|
|
298
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
if (!entry?.installPath || typeof entry.installPath !== 'string')
|
|
301
|
+
continue;
|
|
302
|
+
for (const root of pluginCommandsRoots(entry.installPath)) {
|
|
303
|
+
tasks.push((async () => {
|
|
304
|
+
// Path containment: every walked file must remain under installPath.
|
|
305
|
+
try {
|
|
306
|
+
const stat = await fs.stat(root);
|
|
307
|
+
if (!stat.isDirectory())
|
|
308
|
+
return { cards: [], malformed: [] };
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return { cards: [], malformed: [] };
|
|
312
|
+
}
|
|
313
|
+
const installRoot = path.resolve(entry.installPath);
|
|
314
|
+
if (!withinRoot(root, installRoot)) {
|
|
315
|
+
return { cards: [], malformed: [] };
|
|
316
|
+
}
|
|
317
|
+
const out = await enumerateScopeCommands('plugin', root, { pluginKey });
|
|
318
|
+
// Filter any walked file that escaped containment (defensive).
|
|
319
|
+
out.cards = out.cards.filter((c) => withinRoot(c.absoluteFile, installRoot));
|
|
320
|
+
return out;
|
|
321
|
+
})());
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const settled = await Promise.all(tasks);
|
|
326
|
+
const cards = [];
|
|
327
|
+
const malformed = [];
|
|
328
|
+
for (const part of settled) {
|
|
329
|
+
cards.push(...part.cards);
|
|
330
|
+
malformed.push(...part.malformed);
|
|
331
|
+
}
|
|
332
|
+
return { cards, malformed };
|
|
333
|
+
}
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Source resolution
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
async function resolveCommandsRoot(scope, projectSlug, pluginKey) {
|
|
338
|
+
if (scope === 'project') {
|
|
339
|
+
if (!projectSlug) {
|
|
340
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for scope=project');
|
|
341
|
+
}
|
|
342
|
+
return { root: await projectCommandsRoot(projectSlug) };
|
|
343
|
+
}
|
|
344
|
+
if (scope === 'user') {
|
|
345
|
+
return { root: userCommandsRoot() };
|
|
346
|
+
}
|
|
347
|
+
if (!pluginKey) {
|
|
348
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'pluginKey required for scope=plugin');
|
|
349
|
+
}
|
|
350
|
+
const installed = await readInstalledPlugins();
|
|
351
|
+
const raw = installed.plugins?.[pluginKey];
|
|
352
|
+
const entries = raw ? (Array.isArray(raw) ? raw : [raw]) : [];
|
|
353
|
+
const first = entries.find((e) => typeof e?.installPath === 'string');
|
|
354
|
+
if (!first?.installPath) {
|
|
355
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PLUGIN_NOT_FOUND.code, `plugin not installed: ${pluginKey}`);
|
|
356
|
+
}
|
|
357
|
+
return { root: path.join(first.installPath, COMMANDS_DIR), pluginInstallRoot: path.resolve(first.installPath) };
|
|
358
|
+
}
|
|
359
|
+
async function resolveAbsoluteFile(scope, relPosix, projectSlug, pluginKey) {
|
|
360
|
+
validateRelativePath(relPosix);
|
|
361
|
+
const { root, pluginInstallRoot } = await resolveCommandsRoot(scope, projectSlug, pluginKey);
|
|
362
|
+
const native = relPosix.split('/').join(path.sep);
|
|
363
|
+
const abs = path.resolve(root, native);
|
|
364
|
+
if (!withinRoot(abs, root)) {
|
|
365
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'path escapes commands root');
|
|
366
|
+
}
|
|
367
|
+
if (pluginInstallRoot && !withinRoot(abs, pluginInstallRoot)) {
|
|
368
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'plugin file escapes installPath');
|
|
369
|
+
}
|
|
370
|
+
return { abs, root, pluginInstallRoot };
|
|
371
|
+
}
|
|
372
|
+
function makeSourceLocation(scope, abs, relPosix, projectSlug, pluginKey) {
|
|
373
|
+
return {
|
|
374
|
+
scope,
|
|
375
|
+
absoluteFile: abs,
|
|
376
|
+
pluginKey,
|
|
377
|
+
projectSlug,
|
|
378
|
+
relativePath: relPosix,
|
|
379
|
+
slashName: deriveSlashName(relPosix),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
async function scanBmadCoreSlashNames(projectSlug) {
|
|
383
|
+
const projectRoot = await projectService.resolveOriginalPath(projectSlug);
|
|
384
|
+
const bmadRoot = path.join(projectRoot, '.bmad-core');
|
|
385
|
+
try {
|
|
386
|
+
const stat = await fs.stat(bmadRoot);
|
|
387
|
+
if (!stat.isDirectory())
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
let slashPrefix = 'BMad';
|
|
394
|
+
try {
|
|
395
|
+
const cfg = await fs.readFile(path.join(bmadRoot, 'core-config.yaml'), 'utf-8');
|
|
396
|
+
const parsed = yaml.parse(cfg);
|
|
397
|
+
if (parsed && typeof parsed.slashPrefix === 'string' && parsed.slashPrefix.length > 0) {
|
|
398
|
+
slashPrefix = parsed.slashPrefix;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch {
|
|
402
|
+
// default 'BMad'
|
|
403
|
+
}
|
|
404
|
+
const agentIds = new Set();
|
|
405
|
+
try {
|
|
406
|
+
const files = await fs.readdir(path.join(bmadRoot, 'agents'));
|
|
407
|
+
for (const f of files) {
|
|
408
|
+
if (!f.endsWith('.md'))
|
|
409
|
+
continue;
|
|
410
|
+
// Use file basename as agent id fallback — commandService uses YAML
|
|
411
|
+
// parse, but for de-dup we only need the slash-name string match. The
|
|
412
|
+
// BMad mirror file name is `<id>.md` so the basename equals the id.
|
|
413
|
+
agentIds.add(f.replace(/\.md$/, ''));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// no agents dir
|
|
418
|
+
}
|
|
419
|
+
const taskNames = new Set();
|
|
420
|
+
try {
|
|
421
|
+
const files = await fs.readdir(path.join(bmadRoot, 'tasks'));
|
|
422
|
+
for (const f of files) {
|
|
423
|
+
if (f.endsWith('.md'))
|
|
424
|
+
taskNames.add(f.replace(/\.md$/, ''));
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch {
|
|
428
|
+
// no tasks dir
|
|
429
|
+
}
|
|
430
|
+
return { slashPrefix, agentIds, taskNames };
|
|
431
|
+
}
|
|
432
|
+
function bmadPaletteSlashNames(scan) {
|
|
433
|
+
const out = new Set();
|
|
434
|
+
for (const id of scan.agentIds)
|
|
435
|
+
out.add(`/${scan.slashPrefix}:agents:${id}`);
|
|
436
|
+
for (const name of scan.taskNames)
|
|
437
|
+
out.add(`/${scan.slashPrefix}:tasks:${name}`);
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// Service
|
|
442
|
+
// ---------------------------------------------------------------------------
|
|
443
|
+
class HarnessCommandService {
|
|
444
|
+
async listCards(currentProjectSlug) {
|
|
445
|
+
const projectTask = currentProjectSlug
|
|
446
|
+
? (async () => {
|
|
447
|
+
try {
|
|
448
|
+
const root = await projectCommandsRoot(currentProjectSlug);
|
|
449
|
+
return enumerateScopeCommands('project', root, { projectSlug: currentProjectSlug });
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
if (err?.code === HARNESS_ERRORS.HARNESS_ROOT_MISSING.code) {
|
|
453
|
+
return { cards: [], malformed: [] };
|
|
454
|
+
}
|
|
455
|
+
throw err;
|
|
456
|
+
}
|
|
457
|
+
})()
|
|
458
|
+
: Promise.resolve({ cards: [], malformed: [] });
|
|
459
|
+
const userTask = enumerateScopeCommands('user', userCommandsRoot(), {});
|
|
460
|
+
const pluginTask = enumeratePluginCommands();
|
|
461
|
+
const bmadTask = currentProjectSlug ? scanBmadCoreSlashNames(currentProjectSlug) : Promise.resolve(null);
|
|
462
|
+
const [projectPart, userPart, pluginPart, bmadScan] = await Promise.all([
|
|
463
|
+
projectTask,
|
|
464
|
+
userTask,
|
|
465
|
+
pluginTask,
|
|
466
|
+
bmadTask,
|
|
467
|
+
]);
|
|
468
|
+
const cards = [...projectPart.cards, ...userPart.cards, ...pluginPart.cards];
|
|
469
|
+
const malformed = [...projectPart.malformed, ...userPart.malformed, ...pluginPart.malformed];
|
|
470
|
+
cards.sort((a, b) => {
|
|
471
|
+
const sd = SCOPE_PRIORITY[a.scope] - SCOPE_PRIORITY[b.scope];
|
|
472
|
+
if (sd !== 0)
|
|
473
|
+
return sd;
|
|
474
|
+
return a.relativePath.localeCompare(b.relativePath);
|
|
475
|
+
});
|
|
476
|
+
// De-dup count vs BMad palette
|
|
477
|
+
const bmadSlashes = bmadScan ? bmadPaletteSlashNames(bmadScan) : new Set();
|
|
478
|
+
const seen = new Set();
|
|
479
|
+
let paletteVisibleCount = 0;
|
|
480
|
+
for (const c of cards) {
|
|
481
|
+
if (bmadSlashes.has(c.slashName))
|
|
482
|
+
continue;
|
|
483
|
+
if (seen.has(c.slashName))
|
|
484
|
+
continue;
|
|
485
|
+
seen.add(c.slashName);
|
|
486
|
+
paletteVisibleCount += 1;
|
|
487
|
+
}
|
|
488
|
+
return { cards, malformed, paletteVisibleCount };
|
|
489
|
+
}
|
|
490
|
+
async readCommand(loc) {
|
|
491
|
+
const result = await readMdFile(loc.absoluteFile);
|
|
492
|
+
if ('malformed' in result) {
|
|
493
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NOT_FOUND.code, result.malformed);
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
source: loc,
|
|
497
|
+
frontmatter: result.frontmatter,
|
|
498
|
+
body: result.body,
|
|
499
|
+
raw: result.raw,
|
|
500
|
+
mtime: result.mtime,
|
|
501
|
+
isBmadMirror: detectBmadMirror(result.body),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async createCommand(req) {
|
|
505
|
+
const relPosix = toPosixRelative(req.relativePath);
|
|
506
|
+
const { abs } = await resolveAbsoluteFile(req.scope, relPosix, req.projectSlug);
|
|
507
|
+
// Refuse if the target exists.
|
|
508
|
+
try {
|
|
509
|
+
await fs.stat(abs);
|
|
510
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, `command already exists at ${relPosix}`);
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
if (err.code === HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code) {
|
|
514
|
+
throw err;
|
|
515
|
+
}
|
|
516
|
+
// ENOENT — proceed.
|
|
517
|
+
}
|
|
518
|
+
// Build initial body. Frontmatter only emitted when at least one field is set.
|
|
519
|
+
const fm = req.frontmatter ?? {};
|
|
520
|
+
const body = req.body ?? '';
|
|
521
|
+
const initial = applyYamlFrontmatterPatch(body, frontmatterToPatchObject(fm));
|
|
522
|
+
// Ensure parent directory exists (recursive) — ~/.claude/commands/ may not
|
|
523
|
+
// exist yet on a fresh disk per AC1(a) "global directory empty / create on
|
|
524
|
+
// first card".
|
|
525
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
526
|
+
// Use harnessService.write so the existing watcher self-write suppression
|
|
527
|
+
// applies. It requires a valid HarnessPathRef → translate from scope.
|
|
528
|
+
const ref = await this.buildEditableRef(req.scope, relPosix, req.projectSlug);
|
|
529
|
+
const written = await harnessService.write(ref, { content: initial });
|
|
530
|
+
return {
|
|
531
|
+
success: true,
|
|
532
|
+
source: makeSourceLocation(req.scope, abs, relPosix, req.projectSlug),
|
|
533
|
+
mtime: written.mtime,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
async updateCommand(loc, body) {
|
|
537
|
+
if (loc.scope === 'plugin') {
|
|
538
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'plugin-scope commands are read-only');
|
|
539
|
+
}
|
|
540
|
+
const editableScope = loc.scope;
|
|
541
|
+
const provided = [body.frontmatter, body.body, body.raw].filter((x) => x !== undefined);
|
|
542
|
+
if (provided.length !== 1) {
|
|
543
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'exactly one of frontmatter / body / raw is required');
|
|
544
|
+
}
|
|
545
|
+
const ref = await this.buildEditableRef(editableScope, loc.relativePath, loc.projectSlug);
|
|
546
|
+
const current = await harnessService.read(ref);
|
|
547
|
+
const sourceText = current.content ?? '';
|
|
548
|
+
if (body.expectedMtime !== undefined && body.expectedMtime !== current.mtime) {
|
|
549
|
+
throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
|
|
550
|
+
currentMtime: current.mtime,
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
let nextText;
|
|
554
|
+
if (body.frontmatter !== undefined) {
|
|
555
|
+
nextText = applyYamlFrontmatterPatch(sourceText, frontmatterToPatchObject(body.frontmatter));
|
|
556
|
+
}
|
|
557
|
+
else if (body.body !== undefined) {
|
|
558
|
+
// Replace just the markdown portion. Keep frontmatter byte-for-byte.
|
|
559
|
+
const { frontmatterRaw } = splitFrontmatterAndBody(sourceText);
|
|
560
|
+
if (frontmatterRaw === null) {
|
|
561
|
+
nextText = body.body;
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
// Find the closing `---\n?` delimiter end position in the source.
|
|
565
|
+
const re = /^---\s*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n)?/;
|
|
566
|
+
const match = re.exec(sourceText);
|
|
567
|
+
const head = match ? sourceText.slice(0, match[0].length) : '';
|
|
568
|
+
nextText = `${head}${body.body}`;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
else {
|
|
572
|
+
// raw — replace the entire file (frontmatter + body in one pass).
|
|
573
|
+
nextText = body.raw;
|
|
574
|
+
}
|
|
575
|
+
// Story 30.1 (AC4.b): block writes to git-tracked command files when
|
|
576
|
+
// a plaintext secret is detected (matches the existing copy-flow
|
|
577
|
+
// detection scope — full-text scan).
|
|
578
|
+
if (editableScope === 'project') {
|
|
579
|
+
const secrets = detectSecretsInText(nextText);
|
|
580
|
+
await assertNoSecretOnShared({
|
|
581
|
+
scope: 'project',
|
|
582
|
+
projectSlug: loc.projectSlug,
|
|
583
|
+
relativePath: `.claude/commands/${loc.relativePath}`,
|
|
584
|
+
secretDetected: secrets.matched,
|
|
585
|
+
detectedAt: { lines: secrets.lines },
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
const written = await harnessService.write(ref, {
|
|
589
|
+
content: nextText,
|
|
590
|
+
expectedMtime: current.mtime,
|
|
591
|
+
});
|
|
592
|
+
const { body: nextBody } = splitFrontmatterAndBody(nextText);
|
|
593
|
+
return {
|
|
594
|
+
success: true,
|
|
595
|
+
mtime: written.mtime,
|
|
596
|
+
slashName: deriveSlashName(loc.relativePath),
|
|
597
|
+
tokens: analyzeTokens(nextBody),
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
async copyCommand(req) {
|
|
601
|
+
const sourceRel = toPosixRelative(req.sourceRelativePath);
|
|
602
|
+
const sourceResolved = await resolveAbsoluteFile(req.sourceScope, sourceRel, req.sourceProjectSlug, req.sourcePluginKey);
|
|
603
|
+
const sourceFile = await readMdFile(sourceResolved.abs);
|
|
604
|
+
if ('malformed' in sourceFile) {
|
|
605
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NOT_FOUND.code, 'source command not found');
|
|
606
|
+
}
|
|
607
|
+
const secrets = detectSecretsInRaw(sourceFile.raw);
|
|
608
|
+
if (secrets.matched && req.acknowledgedSecret !== true) {
|
|
609
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'client must show the secret modal and echo acknowledgedSecret', {
|
|
610
|
+
cause: 'secret-not-acknowledged',
|
|
611
|
+
details: { secretLines: secrets.lines },
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
const targetRel = toPosixRelative(req.targetRelativePath ?? sourceRel);
|
|
615
|
+
const { abs: targetAbs } = await resolveAbsoluteFile(req.targetScope, targetRel, req.targetProjectSlug);
|
|
616
|
+
let exists = false;
|
|
617
|
+
try {
|
|
618
|
+
const stat = await fs.stat(targetAbs);
|
|
619
|
+
if (stat.isFile())
|
|
620
|
+
exists = true;
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
// missing
|
|
624
|
+
}
|
|
625
|
+
if (exists) {
|
|
626
|
+
if (req.onConflict === 'skip') {
|
|
627
|
+
return {
|
|
628
|
+
success: true,
|
|
629
|
+
target: makeSourceLocation(req.targetScope, targetAbs, targetRel, req.targetProjectSlug),
|
|
630
|
+
skipped: true,
|
|
631
|
+
...(this.collectCopyWarnings(req, sourceFile.raw)),
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
if (req.onConflict === 'rename') {
|
|
635
|
+
if (req.targetRelativePath === undefined) {
|
|
636
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, 'rename requires targetRelativePath');
|
|
637
|
+
}
|
|
638
|
+
// Rename → write to the new path. If the rename target also exists,
|
|
639
|
+
// surface the conflict so the client can re-prompt.
|
|
640
|
+
if (toPosixRelative(req.targetRelativePath) === sourceRel) {
|
|
641
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, 'target path equals existing conflicting path');
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// overwrite → fall through
|
|
645
|
+
}
|
|
646
|
+
await fs.mkdir(path.dirname(targetAbs), { recursive: true });
|
|
647
|
+
const targetRef = await this.buildEditableRef(req.targetScope, targetRel, req.targetProjectSlug);
|
|
648
|
+
await harnessService.write(targetRef, { content: sourceFile.raw });
|
|
649
|
+
return {
|
|
650
|
+
success: true,
|
|
651
|
+
target: makeSourceLocation(req.targetScope, targetAbs, targetRel, req.targetProjectSlug),
|
|
652
|
+
skipped: false,
|
|
653
|
+
...(this.collectCopyWarnings(req, sourceFile.raw)),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
async copyDirectory(req) {
|
|
657
|
+
const sourceDirPosix = toPosixRelative(req.sourceDirectoryPath).replace(/\/+$/, '');
|
|
658
|
+
if (sourceDirPosix.length === 0) {
|
|
659
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'sourceDirectoryPath cannot be empty');
|
|
660
|
+
}
|
|
661
|
+
const { root: sourceRoot } = await resolveCommandsRoot(req.sourceScope, req.sourceProjectSlug, req.sourcePluginKey);
|
|
662
|
+
const sourceDirAbs = path.resolve(sourceRoot, sourceDirPosix.split('/').join(path.sep));
|
|
663
|
+
if (!withinRoot(sourceDirAbs, sourceRoot)) {
|
|
664
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'source path escapes commands root');
|
|
665
|
+
}
|
|
666
|
+
const stat = await fs.stat(sourceDirAbs).catch(() => null);
|
|
667
|
+
if (!stat?.isDirectory()) {
|
|
668
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NOT_FOUND.code, 'source directory not found');
|
|
669
|
+
}
|
|
670
|
+
const files = await walkMdFiles(sourceDirAbs);
|
|
671
|
+
if (files.length === 0) {
|
|
672
|
+
return { success: true, copied: [], skipped: [] };
|
|
673
|
+
}
|
|
674
|
+
const targetDirPosix = toPosixRelative(req.targetDirectoryPath ?? sourceDirPosix).replace(/\/+$/, '');
|
|
675
|
+
const { root: targetRoot } = await resolveCommandsRoot(req.targetScope, req.targetProjectSlug);
|
|
676
|
+
// Read all source files first (for secret aggregation + body access).
|
|
677
|
+
const fileResults = await Promise.all(files.map(async (abs) => {
|
|
678
|
+
const result = await readMdFile(abs);
|
|
679
|
+
return { abs, result };
|
|
680
|
+
}));
|
|
681
|
+
let aggregateSecret = false;
|
|
682
|
+
for (const f of fileResults) {
|
|
683
|
+
if ('malformed' in f.result)
|
|
684
|
+
continue;
|
|
685
|
+
const sec = detectSecretsInRaw(f.result.raw);
|
|
686
|
+
if (sec.matched) {
|
|
687
|
+
aggregateSecret = true;
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (aggregateSecret && req.acknowledgedSecret !== true) {
|
|
692
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'client must show the secret modal and echo acknowledgedSecret', { cause: 'secret-not-acknowledged' });
|
|
693
|
+
}
|
|
694
|
+
// Build (sourceFileAbs → targetRel) mapping.
|
|
695
|
+
const mappings = files.map((srcAbs) => {
|
|
696
|
+
const relWithinSourceDir = toPosixRelative(path.relative(sourceDirAbs, srcAbs));
|
|
697
|
+
const targetRel = `${targetDirPosix}/${relWithinSourceDir}`;
|
|
698
|
+
const native = targetRel.split('/').join(path.sep);
|
|
699
|
+
const targetAbs = path.resolve(targetRoot, native);
|
|
700
|
+
return { srcAbs, sourceRelInDir: relWithinSourceDir, targetRel, targetAbs };
|
|
701
|
+
});
|
|
702
|
+
const conflicts = [];
|
|
703
|
+
for (const m of mappings) {
|
|
704
|
+
if (!withinRoot(m.targetAbs, targetRoot)) {
|
|
705
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'target path escapes commands root');
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
await fs.stat(m.targetAbs);
|
|
709
|
+
conflicts.push(m.targetRel);
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// missing — no conflict
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (conflicts.length > 0 && req.onConflict === 'per-file') {
|
|
716
|
+
const choices = req.perFileChoices ?? {};
|
|
717
|
+
const missing = conflicts.filter((c) => !choices[c]);
|
|
718
|
+
if (missing.length > 0) {
|
|
719
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, 'per-file decisions required', { details: { conflicts: missing } });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
const copied = [];
|
|
723
|
+
const skipped = [];
|
|
724
|
+
let warnPluginRoot = false;
|
|
725
|
+
for (const m of mappings) {
|
|
726
|
+
const fileEntry = fileResults.find((f) => f.abs === m.srcAbs)?.result;
|
|
727
|
+
if (!fileEntry || 'malformed' in fileEntry) {
|
|
728
|
+
skipped.push(m.targetRel);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const conflict = conflicts.includes(m.targetRel);
|
|
732
|
+
let writeAbs = m.targetAbs;
|
|
733
|
+
let writeRel = m.targetRel;
|
|
734
|
+
if (conflict) {
|
|
735
|
+
const decision = req.onConflict === 'overwrite-all'
|
|
736
|
+
? 'overwrite'
|
|
737
|
+
: req.onConflict === 'skip-all'
|
|
738
|
+
? 'skip'
|
|
739
|
+
: (req.perFileChoices?.[m.targetRel] ?? 'skip');
|
|
740
|
+
if (decision === 'skip') {
|
|
741
|
+
skipped.push(m.targetRel);
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
if (decision === 'rename') {
|
|
745
|
+
const renamed = req.perFileRenames?.[m.targetRel];
|
|
746
|
+
if (!renamed) {
|
|
747
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NAME_CONFLICT.code, `rename target missing for ${m.targetRel}`);
|
|
748
|
+
}
|
|
749
|
+
const renamedPosix = toPosixRelative(renamed);
|
|
750
|
+
validateRelativePath(renamedPosix);
|
|
751
|
+
writeRel = renamedPosix;
|
|
752
|
+
writeAbs = path.resolve(targetRoot, renamedPosix.split('/').join(path.sep));
|
|
753
|
+
if (!withinRoot(writeAbs, targetRoot)) {
|
|
754
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PATH_DENIED.code, 'rename escapes commands root');
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (req.sourceScope === 'plugin' && PLUGIN_ROOT_RE.test(fileEntry.raw)) {
|
|
759
|
+
warnPluginRoot = true;
|
|
760
|
+
}
|
|
761
|
+
await fs.mkdir(path.dirname(writeAbs), { recursive: true });
|
|
762
|
+
const targetRef = await this.buildEditableRef(req.targetScope, writeRel, req.targetProjectSlug);
|
|
763
|
+
await harnessService.write(targetRef, { content: fileEntry.raw });
|
|
764
|
+
copied.push(makeSourceLocation(req.targetScope, writeAbs, writeRel, req.targetProjectSlug));
|
|
765
|
+
}
|
|
766
|
+
const warnings = warnPluginRoot ? ['plugin-root-reference'] : [];
|
|
767
|
+
return {
|
|
768
|
+
success: true,
|
|
769
|
+
copied,
|
|
770
|
+
skipped,
|
|
771
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
async deleteCommand(req) {
|
|
775
|
+
// The plugin scope is rejected upstream by the Zod editableScopeSchema, so
|
|
776
|
+
// by the time we get here `req.scope` can only be 'project' | 'user'. No
|
|
777
|
+
// runtime guard is needed.
|
|
778
|
+
const relPosix = toPosixRelative(req.relativePath);
|
|
779
|
+
const { abs, root } = await resolveAbsoluteFile(req.scope, relPosix, req.projectSlug);
|
|
780
|
+
let stat;
|
|
781
|
+
try {
|
|
782
|
+
stat = await fs.stat(abs);
|
|
783
|
+
}
|
|
784
|
+
catch (err) {
|
|
785
|
+
if (err.code === 'ENOENT') {
|
|
786
|
+
throwMapped(HARNESS_ERRORS.HARNESS_COMMAND_NOT_FOUND.code, 'command not found');
|
|
787
|
+
}
|
|
788
|
+
throw err;
|
|
789
|
+
}
|
|
790
|
+
if (req.expectedMtime !== undefined && req.expectedMtime !== stat.mtime.toISOString()) {
|
|
791
|
+
throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'file changed on disk', {
|
|
792
|
+
currentMtime: stat.mtime.toISOString(),
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
await fs.unlink(abs);
|
|
796
|
+
// Best-effort prune of empty parent directories up to the commands root.
|
|
797
|
+
let dir = path.dirname(abs);
|
|
798
|
+
while (withinRoot(dir, root) && path.resolve(dir) !== path.resolve(root)) {
|
|
799
|
+
try {
|
|
800
|
+
await fs.rmdir(dir);
|
|
801
|
+
}
|
|
802
|
+
catch {
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
dir = path.dirname(dir);
|
|
806
|
+
}
|
|
807
|
+
return { success: true };
|
|
808
|
+
}
|
|
809
|
+
// -------------------------------------------------------------------------
|
|
810
|
+
// Helpers
|
|
811
|
+
// -------------------------------------------------------------------------
|
|
812
|
+
async buildEditableRef(scope, relPosix, projectSlug) {
|
|
813
|
+
if (scope === 'project') {
|
|
814
|
+
if (!projectSlug) {
|
|
815
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'projectSlug required for scope=project');
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
scope: 'project',
|
|
819
|
+
projectSlug,
|
|
820
|
+
relativePath: `${COMMANDS_DIR}/${relPosix}`,
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
scope: 'user',
|
|
825
|
+
relativePath: `${COMMANDS_DIR}/${relPosix}`,
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
collectCopyWarnings(req, raw) {
|
|
829
|
+
if (req.sourceScope === 'plugin' && PLUGIN_ROOT_RE.test(raw)) {
|
|
830
|
+
return { warnings: ['plugin-root-reference'] };
|
|
831
|
+
}
|
|
832
|
+
return {};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
// ---------------------------------------------------------------------------
|
|
836
|
+
// resolveSourceLocation — used by the controller to validate path/scope tuple
|
|
837
|
+
// ---------------------------------------------------------------------------
|
|
838
|
+
export async function resolveCommandSourceLocation(input) {
|
|
839
|
+
const relPosix = toPosixRelative(input.relativePath);
|
|
840
|
+
const { abs } = await resolveAbsoluteFile(input.scope, relPosix, input.projectSlug, input.pluginKey);
|
|
841
|
+
return makeSourceLocation(input.scope, abs, relPosix, input.projectSlug, input.pluginKey);
|
|
842
|
+
}
|
|
843
|
+
// Exposed for unit tests + the chat slash-palette integration (Task 12).
|
|
844
|
+
export const harnessCommandInternals = {
|
|
845
|
+
analyzeTokens,
|
|
846
|
+
detectBmadMirror,
|
|
847
|
+
detectSecretsInRaw,
|
|
848
|
+
deriveSlashName,
|
|
849
|
+
validateRelativePath,
|
|
850
|
+
walkMdFiles,
|
|
851
|
+
};
|
|
852
|
+
export const harnessCommandService = new HarnessCommandService();
|
|
853
|
+
//# sourceMappingURL=harnessCommandService.js.map
|