hammoc 1.4.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 -405
- package/bin/hammoc.js +0 -6
- package/package.json +100 -94
- 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 +13 -21
- package/packages/server/dist/app.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/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/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 +15 -0
- package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
- package/packages/server/dist/handlers/websocket.js +79 -0
- package/packages/server/dist/handlers/websocket.js.map +1 -1
- package/packages/server/dist/index.js +5 -0
- package/packages/server/dist/index.js.map +1 -1
- package/packages/server/dist/locales/en/server.json +37 -4
- package/packages/server/dist/locales/es/server.json +0 -4
- package/packages/server/dist/locales/ja/server.json +0 -4
- package/packages/server/dist/locales/ko/server.json +0 -4
- package/packages/server/dist/locales/pt/server.json +0 -4
- package/packages/server/dist/locales/zh-CN/server.json +0 -4
- 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/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/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 +27 -6
- 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/fileWatcherService.d.ts +24 -0
- package/packages/server/dist/services/fileWatcherService.d.ts.map +1 -1
- package/packages/server/dist/services/fileWatcherService.js +192 -1
- package/packages/server/dist/services/fileWatcherService.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/issueService.d.ts.map +1 -1
- package/packages/server/dist/services/issueService.js +2 -1
- 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/queueService.d.ts.map +1 -1
- package/packages/server/dist/services/queueService.js +45 -2
- package/packages/server/dist/services/queueService.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/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/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/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 +4 -1
- 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/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/preferences.d.ts +2 -0
- package/packages/shared/dist/types/preferences.d.ts.map +1 -1
- 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/websocket.d.ts +10 -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/packages/client/dist/assets/index-6jREnVYd.js +0 -2
- package/packages/client/dist/assets/index-BFF0iqyW.css +0 -32
- package/packages/client/dist/assets/index-BcI4y-fU.js +0 -1454
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 29.2: Snippet management service.
|
|
3
|
+
*
|
|
4
|
+
* Manages the three-scope snippet store consumed by `snippetResolver` for
|
|
5
|
+
* `%name%` chat-input expansion:
|
|
6
|
+
*
|
|
7
|
+
* - project → `<projectRoot>/.hammoc/snippets/<name>.md` (mutable)
|
|
8
|
+
* - user → `~/.hammoc/snippets/<name>.md` (mutable)
|
|
9
|
+
* - bundled → `<serverDist>/snippets/<name>[.md]` (read-only)
|
|
10
|
+
*
|
|
11
|
+
* Differences from the harness services (Epic 28):
|
|
12
|
+
* - no YAML frontmatter — body is free-form markdown
|
|
13
|
+
* - no plugin scope — `bundled` replaces it but is structurally simpler
|
|
14
|
+
* (no manifest, no installed_plugins.json lookup)
|
|
15
|
+
* - no watcher integration (Story 29.2 Phase 1) — `noteLocalWrite` is not
|
|
16
|
+
* called because snippets are NOT in the harness watcher's subscription
|
|
17
|
+
* tree (`fileWatcherService` only watches `.claude/`, never `.hammoc/`)
|
|
18
|
+
*
|
|
19
|
+
* STALE_WRITE / HARNESS_FILE_EXISTS contract mirrors `harnessService` so the
|
|
20
|
+
* client can reuse its existing conflict UX.
|
|
21
|
+
*/
|
|
22
|
+
import { type SnippetCopyRequest, type SnippetCopyResponse, type SnippetDeleteRequest, type SnippetDeleteResponse, type SnippetListResponse, type SnippetReadResponse, type SnippetWriteRequest, type SnippetWriteResponse } from '@hammoc/shared';
|
|
23
|
+
import { type SnippetPathRef } from '../utils/snippetPaths.js';
|
|
24
|
+
declare class SnippetService {
|
|
25
|
+
/**
|
|
26
|
+
* List snippets across all three scopes. Names are NOT deduplicated — the
|
|
27
|
+
* client renders one card per (scope, name) combination so users can see
|
|
28
|
+
* shadowing relationships explicitly. Resolution at runtime
|
|
29
|
+
* (snippetResolver) still applies project > user > bundled precedence.
|
|
30
|
+
*/
|
|
31
|
+
list(opts: {
|
|
32
|
+
projectSlug?: string;
|
|
33
|
+
}): Promise<SnippetListResponse>;
|
|
34
|
+
read(ref: SnippetPathRef): Promise<SnippetReadResponse>;
|
|
35
|
+
/** Create a new snippet — fails with HARNESS_FILE_EXISTS when a same-named file exists. */
|
|
36
|
+
create(ref: SnippetPathRef, body: SnippetWriteRequest): Promise<SnippetWriteResponse>;
|
|
37
|
+
/** Update an existing snippet body with optional STALE_WRITE guard. */
|
|
38
|
+
update(ref: SnippetPathRef, body: SnippetWriteRequest): Promise<SnippetWriteResponse>;
|
|
39
|
+
delete(ref: SnippetPathRef, body: SnippetDeleteRequest): Promise<SnippetDeleteResponse>;
|
|
40
|
+
/**
|
|
41
|
+
* Copy a snippet across scopes.
|
|
42
|
+
*
|
|
43
|
+
* project ↔ user → bi-directional
|
|
44
|
+
* bundled → project → one-way clone
|
|
45
|
+
* bundled → user → one-way clone
|
|
46
|
+
*
|
|
47
|
+
* `bundled` is never a target. When the target exists the call resolves
|
|
48
|
+
* according to `onConflict` (default 'abort' → HARNESS_FILE_EXISTS).
|
|
49
|
+
*/
|
|
50
|
+
copy(req: SnippetCopyRequest): Promise<SnippetCopyResponse>;
|
|
51
|
+
}
|
|
52
|
+
export declare const snippetService: SnippetService;
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=snippetService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snippetService.d.ts","sourceRoot":"","sources":["../../src/services/snippetService.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAIH,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EAExB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EAC1B,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAML,KAAK,cAAc,EACpB,MAAM,0BAA0B,CAAC;AA0FlC,cAAM,cAAc;IAClB;;;;;OAKG;IACG,IAAI,CAAC,IAAI,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAgBlE,IAAI,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAsB7D,2FAA2F;IACrF,MAAM,CACV,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,oBAAoB,CAAC;IAyBhC,uEAAuE;IACjE,MAAM,CACV,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,oBAAoB,CAAC;IA4C1B,MAAM,CACV,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,qBAAqB,CAAC;IAiCjC;;;;;;;;;OASG;IACG,IAAI,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;CA4FlE;AAeD,eAAO,MAAM,cAAc,gBAAuB,CAAC"}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 29.2: Snippet management service.
|
|
3
|
+
*
|
|
4
|
+
* Manages the three-scope snippet store consumed by `snippetResolver` for
|
|
5
|
+
* `%name%` chat-input expansion:
|
|
6
|
+
*
|
|
7
|
+
* - project → `<projectRoot>/.hammoc/snippets/<name>.md` (mutable)
|
|
8
|
+
* - user → `~/.hammoc/snippets/<name>.md` (mutable)
|
|
9
|
+
* - bundled → `<serverDist>/snippets/<name>[.md]` (read-only)
|
|
10
|
+
*
|
|
11
|
+
* Differences from the harness services (Epic 28):
|
|
12
|
+
* - no YAML frontmatter — body is free-form markdown
|
|
13
|
+
* - no plugin scope — `bundled` replaces it but is structurally simpler
|
|
14
|
+
* (no manifest, no installed_plugins.json lookup)
|
|
15
|
+
* - no watcher integration (Story 29.2 Phase 1) — `noteLocalWrite` is not
|
|
16
|
+
* called because snippets are NOT in the harness watcher's subscription
|
|
17
|
+
* tree (`fileWatcherService` only watches `.claude/`, never `.hammoc/`)
|
|
18
|
+
*
|
|
19
|
+
* STALE_WRITE / HARNESS_FILE_EXISTS contract mirrors `harnessService` so the
|
|
20
|
+
* client can reuse its existing conflict UX.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs/promises';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { HARNESS_ERRORS, } from '@hammoc/shared';
|
|
25
|
+
import { getBundledSnippetsDir, getProjectSnippetsDir, getUserSnippetsDir, resolveSnippetPath, validateSnippetName, } from '../utils/snippetPaths.js';
|
|
26
|
+
const MAX_FILE_SIZE = 102_400; // 100KB — matches snippetResolver
|
|
27
|
+
const PREVIEW_MAX_LEN = 80;
|
|
28
|
+
function throwMapped(code, message, extras) {
|
|
29
|
+
const err = new Error(message);
|
|
30
|
+
err.code = code;
|
|
31
|
+
if (extras)
|
|
32
|
+
Object.assign(err, extras);
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Read snippet content from a path, transparently handling both `<name>.md`
|
|
37
|
+
* and the legacy extension-less `<name>` form (snippetResolver back-compat).
|
|
38
|
+
* Returns `null` when neither path exists.
|
|
39
|
+
*/
|
|
40
|
+
async function readSnippetFile(primaryPath, legacyPath) {
|
|
41
|
+
for (const candidate of [primaryPath, legacyPath]) {
|
|
42
|
+
try {
|
|
43
|
+
const stat = await fs.stat(candidate);
|
|
44
|
+
if (!stat.isFile())
|
|
45
|
+
continue;
|
|
46
|
+
const content = await fs.readFile(candidate, 'utf-8');
|
|
47
|
+
return {
|
|
48
|
+
content,
|
|
49
|
+
mtime: stat.mtime.toISOString(),
|
|
50
|
+
size: stat.size,
|
|
51
|
+
effectivePath: candidate,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const code = err.code;
|
|
56
|
+
if (code === 'ENOENT')
|
|
57
|
+
continue;
|
|
58
|
+
throw err;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
/** Scan one root for `.md` and extension-less files, returning SnippetCard entries. */
|
|
64
|
+
async function scanSnippetDir(dir, scope) {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
const cards = [];
|
|
73
|
+
// Use `for await` style so we don't touch fs serially in a way that blows up
|
|
74
|
+
// on large dirs — but with awaits inside a loop the cost is bounded by the
|
|
75
|
+
// number of files; the user-facing dir is tiny.
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (!entry.isFile())
|
|
78
|
+
continue;
|
|
79
|
+
const rawName = entry.name;
|
|
80
|
+
const name = rawName.endsWith('.md') ? rawName.slice(0, -3) : rawName;
|
|
81
|
+
// Skip files whose stem fails NAME_RE — e.g. README.md inside the bundled dir.
|
|
82
|
+
try {
|
|
83
|
+
validateSnippetName(name);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const abs = path.join(dir, rawName);
|
|
89
|
+
let stat;
|
|
90
|
+
try {
|
|
91
|
+
stat = await fs.stat(abs);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!stat.isFile())
|
|
97
|
+
continue;
|
|
98
|
+
let preview;
|
|
99
|
+
try {
|
|
100
|
+
const content = await fs.readFile(abs, 'utf-8');
|
|
101
|
+
const firstLine = content.split('\n').find((l) => l.trim().length > 0);
|
|
102
|
+
if (firstLine)
|
|
103
|
+
preview = firstLine.trim().slice(0, PREVIEW_MAX_LEN);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// unreadable — leave preview undefined
|
|
107
|
+
}
|
|
108
|
+
cards.push({
|
|
109
|
+
scope,
|
|
110
|
+
name,
|
|
111
|
+
preview,
|
|
112
|
+
mtime: stat.mtime.toISOString(),
|
|
113
|
+
size: stat.size,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return cards;
|
|
117
|
+
}
|
|
118
|
+
class SnippetService {
|
|
119
|
+
/**
|
|
120
|
+
* List snippets across all three scopes. Names are NOT deduplicated — the
|
|
121
|
+
* client renders one card per (scope, name) combination so users can see
|
|
122
|
+
* shadowing relationships explicitly. Resolution at runtime
|
|
123
|
+
* (snippetResolver) still applies project > user > bundled precedence.
|
|
124
|
+
*/
|
|
125
|
+
async list(opts) {
|
|
126
|
+
const tasks = [
|
|
127
|
+
scanSnippetDir(getUserSnippetsDir(), 'user'),
|
|
128
|
+
scanSnippetDir(getBundledSnippetsDir(), 'bundled'),
|
|
129
|
+
];
|
|
130
|
+
if (opts.projectSlug) {
|
|
131
|
+
tasks.push(getProjectSnippetsDir(opts.projectSlug).then((dir) => scanSnippetDir(dir, 'project')));
|
|
132
|
+
}
|
|
133
|
+
const results = await Promise.all(tasks);
|
|
134
|
+
const merged = results.flat();
|
|
135
|
+
merged.sort((a, b) => a.name.localeCompare(b.name) || a.scope.localeCompare(b.scope));
|
|
136
|
+
return { snippets: merged };
|
|
137
|
+
}
|
|
138
|
+
async read(ref) {
|
|
139
|
+
const { absolutePath, legacyAbsolutePath } = await resolveSnippetPath(ref);
|
|
140
|
+
const file = await readSnippetFile(absolutePath, legacyAbsolutePath);
|
|
141
|
+
if (!file) {
|
|
142
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code, 'snippet not found', {
|
|
143
|
+
absolutePath,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (file.size > MAX_FILE_SIZE) {
|
|
147
|
+
// Truncate to stay consistent with snippetResolver's 100KB limit.
|
|
148
|
+
throwMapped(HARNESS_ERRORS.HARNESS_PARSE_ERROR.code, 'snippet exceeds 100KB limit');
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
scope: ref.scope,
|
|
152
|
+
name: ref.name,
|
|
153
|
+
content: file.content,
|
|
154
|
+
mtime: file.mtime,
|
|
155
|
+
size: file.size,
|
|
156
|
+
absolutePath: file.effectivePath,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Create a new snippet — fails with HARNESS_FILE_EXISTS when a same-named file exists. */
|
|
160
|
+
async create(ref, body) {
|
|
161
|
+
if (ref.scope === 'bundled') {
|
|
162
|
+
throwMapped(HARNESS_ERRORS.HARNESS_BUNDLED_READONLY.code, 'bundled scope is read-only');
|
|
163
|
+
}
|
|
164
|
+
const { absolutePath, legacyAbsolutePath, resolvedRoot } = await resolveSnippetPath(ref);
|
|
165
|
+
// Reject creation if either form already exists.
|
|
166
|
+
for (const candidate of [absolutePath, legacyAbsolutePath]) {
|
|
167
|
+
try {
|
|
168
|
+
const stat = await fs.stat(candidate);
|
|
169
|
+
if (stat.isFile()) {
|
|
170
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FILE_EXISTS.code, 'snippet already exists');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const code = err.code;
|
|
175
|
+
if (code === 'ENOENT')
|
|
176
|
+
continue;
|
|
177
|
+
if (code === HARNESS_ERRORS.HARNESS_FILE_EXISTS.code)
|
|
178
|
+
throw err;
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
await fs.mkdir(resolvedRoot, { recursive: true });
|
|
183
|
+
await writeSnippetFile(absolutePath, body.content);
|
|
184
|
+
const stat = await fs.stat(absolutePath);
|
|
185
|
+
return { success: true, size: stat.size, mtime: stat.mtime.toISOString() };
|
|
186
|
+
}
|
|
187
|
+
/** Update an existing snippet body with optional STALE_WRITE guard. */
|
|
188
|
+
async update(ref, body) {
|
|
189
|
+
if (ref.scope === 'bundled') {
|
|
190
|
+
throwMapped(HARNESS_ERRORS.HARNESS_BUNDLED_READONLY.code, 'bundled scope is read-only');
|
|
191
|
+
}
|
|
192
|
+
const { absolutePath, legacyAbsolutePath, resolvedRoot } = await resolveSnippetPath(ref);
|
|
193
|
+
// STALE_WRITE check — try `.md` first, fall back to legacy ext-less form
|
|
194
|
+
// because the existing file may still be in the legacy layout. If neither
|
|
195
|
+
// exists, treat the write as a fresh create (no guard).
|
|
196
|
+
let targetPath = absolutePath;
|
|
197
|
+
let existing;
|
|
198
|
+
try {
|
|
199
|
+
existing = await fs.stat(absolutePath);
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err.code !== 'ENOENT')
|
|
203
|
+
throw err;
|
|
204
|
+
try {
|
|
205
|
+
existing = await fs.stat(legacyAbsolutePath);
|
|
206
|
+
targetPath = legacyAbsolutePath;
|
|
207
|
+
}
|
|
208
|
+
catch (err2) {
|
|
209
|
+
if (err2.code !== 'ENOENT')
|
|
210
|
+
throw err2;
|
|
211
|
+
// Neither exists — caller should use create(); but to keep update()
|
|
212
|
+
// forgiving we proceed and let it be created at the canonical .md path.
|
|
213
|
+
existing = undefined;
|
|
214
|
+
targetPath = absolutePath;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (body.expectedMtime !== undefined && existing) {
|
|
218
|
+
const currentMtime = existing.mtime.toISOString();
|
|
219
|
+
if (currentMtime !== body.expectedMtime) {
|
|
220
|
+
throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'snippet changed on disk', {
|
|
221
|
+
currentMtime,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!existing) {
|
|
226
|
+
await fs.mkdir(resolvedRoot, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
await writeSnippetFile(targetPath, body.content);
|
|
229
|
+
const stat = await fs.stat(targetPath);
|
|
230
|
+
return { success: true, size: stat.size, mtime: stat.mtime.toISOString() };
|
|
231
|
+
}
|
|
232
|
+
async delete(ref, body) {
|
|
233
|
+
if (ref.scope === 'bundled') {
|
|
234
|
+
throwMapped(HARNESS_ERRORS.HARNESS_BUNDLED_READONLY.code, 'bundled scope is read-only');
|
|
235
|
+
}
|
|
236
|
+
const { absolutePath, legacyAbsolutePath } = await resolveSnippetPath(ref);
|
|
237
|
+
let targetPath = null;
|
|
238
|
+
let stat;
|
|
239
|
+
for (const candidate of [absolutePath, legacyAbsolutePath]) {
|
|
240
|
+
try {
|
|
241
|
+
stat = await fs.stat(candidate);
|
|
242
|
+
if (stat.isFile()) {
|
|
243
|
+
targetPath = candidate;
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
if (err.code === 'ENOENT')
|
|
249
|
+
continue;
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!targetPath || !stat) {
|
|
254
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code, 'snippet not found');
|
|
255
|
+
}
|
|
256
|
+
if (body.expectedMtime !== undefined && stat.mtime.toISOString() !== body.expectedMtime) {
|
|
257
|
+
throwMapped(HARNESS_ERRORS.HARNESS_STALE_WRITE.code, 'snippet changed on disk', {
|
|
258
|
+
currentMtime: stat.mtime.toISOString(),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
await fs.unlink(targetPath);
|
|
262
|
+
return { success: true };
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Copy a snippet across scopes.
|
|
266
|
+
*
|
|
267
|
+
* project ↔ user → bi-directional
|
|
268
|
+
* bundled → project → one-way clone
|
|
269
|
+
* bundled → user → one-way clone
|
|
270
|
+
*
|
|
271
|
+
* `bundled` is never a target. When the target exists the call resolves
|
|
272
|
+
* according to `onConflict` (default 'abort' → HARNESS_FILE_EXISTS).
|
|
273
|
+
*/
|
|
274
|
+
async copy(req) {
|
|
275
|
+
if (req.targetScope !== 'project' && req.targetScope !== 'user') {
|
|
276
|
+
throwMapped(HARNESS_ERRORS.HARNESS_BUNDLED_READONLY.code, 'bundled cannot be a copy target');
|
|
277
|
+
}
|
|
278
|
+
if (req.sourceScope === 'project' && !req.sourceProjectSlug) {
|
|
279
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'sourceProjectSlug is required');
|
|
280
|
+
}
|
|
281
|
+
if (req.targetScope === 'project' && !req.targetProjectSlug) {
|
|
282
|
+
throwMapped(HARNESS_ERRORS.HARNESS_ROOT_MISSING.code, 'targetProjectSlug is required');
|
|
283
|
+
}
|
|
284
|
+
const sourceRef = {
|
|
285
|
+
scope: req.sourceScope,
|
|
286
|
+
projectSlug: req.sourceProjectSlug,
|
|
287
|
+
name: req.sourceName,
|
|
288
|
+
};
|
|
289
|
+
const sourceResolved = await resolveSnippetPath(sourceRef);
|
|
290
|
+
const sourceFile = await readSnippetFile(sourceResolved.absolutePath, sourceResolved.legacyAbsolutePath);
|
|
291
|
+
if (!sourceFile) {
|
|
292
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FILE_NOT_FOUND.code, 'source snippet not found');
|
|
293
|
+
}
|
|
294
|
+
const targetName = req.targetName ?? req.sourceName;
|
|
295
|
+
validateSnippetName(targetName);
|
|
296
|
+
const targetRef = {
|
|
297
|
+
scope: req.targetScope,
|
|
298
|
+
projectSlug: req.targetProjectSlug,
|
|
299
|
+
name: targetName,
|
|
300
|
+
};
|
|
301
|
+
const targetResolved = await resolveSnippetPath(targetRef);
|
|
302
|
+
// Detect target conflict (either canonical or legacy form).
|
|
303
|
+
let targetExisting = false;
|
|
304
|
+
let targetExistingPath = '';
|
|
305
|
+
for (const candidate of [
|
|
306
|
+
targetResolved.absolutePath,
|
|
307
|
+
targetResolved.legacyAbsolutePath,
|
|
308
|
+
]) {
|
|
309
|
+
try {
|
|
310
|
+
const stat = await fs.stat(candidate);
|
|
311
|
+
if (stat.isFile()) {
|
|
312
|
+
targetExisting = true;
|
|
313
|
+
targetExistingPath = candidate;
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
if (err.code === 'ENOENT')
|
|
319
|
+
continue;
|
|
320
|
+
throw err;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const onConflict = req.onConflict ?? 'abort';
|
|
324
|
+
if (targetExisting) {
|
|
325
|
+
if (onConflict === 'abort') {
|
|
326
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FILE_EXISTS.code, 'target snippet already exists');
|
|
327
|
+
}
|
|
328
|
+
if (onConflict === 'rename') {
|
|
329
|
+
// Rename mode requires a fresh targetName, which the caller is
|
|
330
|
+
// responsible for choosing. If the new name still collides, surface
|
|
331
|
+
// the same 409 — the dialog will re-prompt.
|
|
332
|
+
if (!req.targetName || req.targetName === req.sourceName) {
|
|
333
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FILE_EXISTS.code, 'rename requires a distinct targetName');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
// overwrite → fall through; we'll write to the canonical .md path so
|
|
337
|
+
// the target is always normalized to `<name>.md` even if the existing
|
|
338
|
+
// form was legacy.
|
|
339
|
+
}
|
|
340
|
+
await fs.mkdir(targetResolved.resolvedRoot, { recursive: true });
|
|
341
|
+
await writeSnippetFile(targetResolved.absolutePath, sourceFile.content);
|
|
342
|
+
// If the existing file was at the legacy path, leave it in place per the
|
|
343
|
+
// story's "no automatic rename" policy (S4 in AC1.b). The client may end
|
|
344
|
+
// up with two files (legacy + new .md) — listSnippets will show only the
|
|
345
|
+
// canonical card because scanSnippetDir dedupes by stem name.
|
|
346
|
+
void targetExistingPath;
|
|
347
|
+
return {
|
|
348
|
+
success: true,
|
|
349
|
+
target: {
|
|
350
|
+
scope: req.targetScope,
|
|
351
|
+
name: targetName,
|
|
352
|
+
absolutePath: targetResolved.absolutePath,
|
|
353
|
+
},
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/** Atomic-ish file write — `fs.writeFile` with utf-8 encoding. */
|
|
358
|
+
async function writeSnippetFile(absolutePath, content) {
|
|
359
|
+
try {
|
|
360
|
+
await fs.writeFile(absolutePath, content, 'utf-8');
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
const code = err.code;
|
|
364
|
+
if (code === 'EACCES') {
|
|
365
|
+
throwMapped(HARNESS_ERRORS.HARNESS_FORBIDDEN.code, 'permission denied');
|
|
366
|
+
}
|
|
367
|
+
throwMapped(HARNESS_ERRORS.HARNESS_WRITE_ERROR.code, 'failed to write snippet');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export const snippetService = new SnippetService();
|
|
371
|
+
//# sourceMappingURL=snippetService.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snippetService.js","sourceRoot":"","sources":["../../src/services/snippetService.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 28.5: YAML frontmatter round-trip helper.
|
|
3
|
+
*
|
|
4
|
+
* Slash command files (`.claude/commands/**\/*.md`) are markdown documents with
|
|
5
|
+
* an optional `--- ... ---` YAML frontmatter block at the top. To preserve
|
|
6
|
+
* comments / key order / blank lines inside the frontmatter while leaving the
|
|
7
|
+
* markdown body byte-for-byte intact, we round-trip just the frontmatter slice
|
|
8
|
+
* via `yaml`(eemeli) `parseDocument`.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour:
|
|
11
|
+
* - File has no frontmatter + patch is empty (all keys absent) → return source
|
|
12
|
+
* unchanged.
|
|
13
|
+
* - File has no frontmatter + patch contains keys → prepend a
|
|
14
|
+
* fresh `--- ... ---` block.
|
|
15
|
+
* - File has frontmatter + patch is empty → strip the
|
|
16
|
+
* block entirely (matches AC3.a "all four fields absent ⇒ no frontmatter").
|
|
17
|
+
* - File has frontmatter + patch sets keys → mutate keys
|
|
18
|
+
* in place, preserving comments / order / blank lines for untouched keys.
|
|
19
|
+
*
|
|
20
|
+
* The body region (everything after the closing `---`) is taken from the source
|
|
21
|
+
* via raw substring slicing so byte-level equality holds — this is asserted by
|
|
22
|
+
* the unit tests with `result.slice(boundary) === source.slice(boundary)`.
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Apply key-level patches to the YAML frontmatter block of a markdown file.
|
|
26
|
+
* Body markdown is preserved byte-for-byte. Pass `undefined` to delete a key.
|
|
27
|
+
*
|
|
28
|
+
* - When the resulting frontmatter has no keys left, the entire `--- ... ---`
|
|
29
|
+
* block (including the closing newline) is stripped.
|
|
30
|
+
* - When the source has no frontmatter and `newFrontmatter` carries at least
|
|
31
|
+
* one defined value, a `--- ... ---` block is prepended with a single
|
|
32
|
+
* trailing newline before the body.
|
|
33
|
+
*
|
|
34
|
+
* @throws HARNESS_PARSE_ERROR when the existing frontmatter is malformed YAML.
|
|
35
|
+
*/
|
|
36
|
+
export declare function applyYamlFrontmatterPatch(source: string, newFrontmatter: Record<string, unknown>): string;
|
|
37
|
+
/**
|
|
38
|
+
* Strip the `--- ... ---` block (if any) and return the remaining body.
|
|
39
|
+
* Convenience helper — used by the service to surface the body separately
|
|
40
|
+
* from frontmatter on the read path.
|
|
41
|
+
*/
|
|
42
|
+
export declare function splitFrontmatterAndBody(source: string): {
|
|
43
|
+
frontmatterRaw: string | null;
|
|
44
|
+
body: string;
|
|
45
|
+
};
|
|
46
|
+
//# sourceMappingURL=applyYamlFrontmatterPatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applyYamlFrontmatterPatch.d.ts","sourceRoot":"","sources":["../../../src/services/utils/applyYamlFrontmatterPatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAuCH;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACtC,MAAM,CAuDR;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG;IACvD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,IAAI,EAAE,MAAM,CAAC;CACd,CAIA"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story 28.5: YAML frontmatter round-trip helper.
|
|
3
|
+
*
|
|
4
|
+
* Slash command files (`.claude/commands/**\/*.md`) are markdown documents with
|
|
5
|
+
* an optional `--- ... ---` YAML frontmatter block at the top. To preserve
|
|
6
|
+
* comments / key order / blank lines inside the frontmatter while leaving the
|
|
7
|
+
* markdown body byte-for-byte intact, we round-trip just the frontmatter slice
|
|
8
|
+
* via `yaml`(eemeli) `parseDocument`.
|
|
9
|
+
*
|
|
10
|
+
* Behaviour:
|
|
11
|
+
* - File has no frontmatter + patch is empty (all keys absent) → return source
|
|
12
|
+
* unchanged.
|
|
13
|
+
* - File has no frontmatter + patch contains keys → prepend a
|
|
14
|
+
* fresh `--- ... ---` block.
|
|
15
|
+
* - File has frontmatter + patch is empty → strip the
|
|
16
|
+
* block entirely (matches AC3.a "all four fields absent ⇒ no frontmatter").
|
|
17
|
+
* - File has frontmatter + patch sets keys → mutate keys
|
|
18
|
+
* in place, preserving comments / order / blank lines for untouched keys.
|
|
19
|
+
*
|
|
20
|
+
* The body region (everything after the closing `---`) is taken from the source
|
|
21
|
+
* via raw substring slicing so byte-level equality holds — this is asserted by
|
|
22
|
+
* the unit tests with `result.slice(boundary) === source.slice(boundary)`.
|
|
23
|
+
*/
|
|
24
|
+
import { parseDocument } from 'yaml';
|
|
25
|
+
import { HARNESS_ERRORS } from '@hammoc/shared';
|
|
26
|
+
/** Match a leading `---\n...---\n?` frontmatter block. */
|
|
27
|
+
const FRONTMATTER_RE = /^---\s*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n)?/;
|
|
28
|
+
function findFrontmatter(source) {
|
|
29
|
+
const match = FRONTMATTER_RE.exec(source);
|
|
30
|
+
if (!match)
|
|
31
|
+
return null;
|
|
32
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
33
|
+
return { inner: match[1], bodyStart: match[0].length, eol };
|
|
34
|
+
}
|
|
35
|
+
function frontmatterIsEmpty(values) {
|
|
36
|
+
for (const v of Object.values(values)) {
|
|
37
|
+
if (v !== undefined)
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
function throwParseError(cause) {
|
|
43
|
+
const err = new Error(`failed to parse frontmatter: ${cause?.message ?? String(cause)}`);
|
|
44
|
+
err.code = HARNESS_ERRORS.HARNESS_PARSE_ERROR.code;
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Apply key-level patches to the YAML frontmatter block of a markdown file.
|
|
49
|
+
* Body markdown is preserved byte-for-byte. Pass `undefined` to delete a key.
|
|
50
|
+
*
|
|
51
|
+
* - When the resulting frontmatter has no keys left, the entire `--- ... ---`
|
|
52
|
+
* block (including the closing newline) is stripped.
|
|
53
|
+
* - When the source has no frontmatter and `newFrontmatter` carries at least
|
|
54
|
+
* one defined value, a `--- ... ---` block is prepended with a single
|
|
55
|
+
* trailing newline before the body.
|
|
56
|
+
*
|
|
57
|
+
* @throws HARNESS_PARSE_ERROR when the existing frontmatter is malformed YAML.
|
|
58
|
+
*/
|
|
59
|
+
export function applyYamlFrontmatterPatch(source, newFrontmatter) {
|
|
60
|
+
const slice = findFrontmatter(source);
|
|
61
|
+
const allEmpty = frontmatterIsEmpty(newFrontmatter);
|
|
62
|
+
if (!slice) {
|
|
63
|
+
if (allEmpty)
|
|
64
|
+
return source;
|
|
65
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
66
|
+
let doc;
|
|
67
|
+
try {
|
|
68
|
+
doc = parseDocument('', { keepSourceTokens: true });
|
|
69
|
+
}
|
|
70
|
+
catch (cause) {
|
|
71
|
+
throwParseError(cause);
|
|
72
|
+
}
|
|
73
|
+
if (doc.contents == null) {
|
|
74
|
+
doc.contents = doc.createNode({});
|
|
75
|
+
}
|
|
76
|
+
for (const [key, value] of Object.entries(newFrontmatter)) {
|
|
77
|
+
if (value === undefined)
|
|
78
|
+
continue;
|
|
79
|
+
doc.setIn([key], value);
|
|
80
|
+
}
|
|
81
|
+
const yamlText = doc.toString().replace(/\r?\n$/, '');
|
|
82
|
+
return `---${eol}${yamlText}${eol}---${eol}${source}`;
|
|
83
|
+
}
|
|
84
|
+
let doc;
|
|
85
|
+
try {
|
|
86
|
+
doc = parseDocument(slice.inner, { keepSourceTokens: true });
|
|
87
|
+
if (doc.errors.length > 0) {
|
|
88
|
+
throw doc.errors[0];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
throwParseError(cause);
|
|
93
|
+
}
|
|
94
|
+
if (doc.contents == null) {
|
|
95
|
+
doc.contents = doc.createNode({});
|
|
96
|
+
}
|
|
97
|
+
for (const [key, value] of Object.entries(newFrontmatter)) {
|
|
98
|
+
if (value === undefined) {
|
|
99
|
+
if (doc.hasIn([key]))
|
|
100
|
+
doc.deleteIn([key]);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
doc.setIn([key], value);
|
|
104
|
+
}
|
|
105
|
+
// If the patched document is empty (no top-level keys left), strip the block.
|
|
106
|
+
const docContents = doc.contents;
|
|
107
|
+
const remainingKeys = docContents?.items?.length ?? 0;
|
|
108
|
+
if (remainingKeys === 0) {
|
|
109
|
+
return source.slice(slice.bodyStart);
|
|
110
|
+
}
|
|
111
|
+
const yamlText = doc.toString().replace(/\r?\n$/, '');
|
|
112
|
+
return `---${slice.eol}${yamlText}${slice.eol}---${slice.eol}${source.slice(slice.bodyStart)}`;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Strip the `--- ... ---` block (if any) and return the remaining body.
|
|
116
|
+
* Convenience helper — used by the service to surface the body separately
|
|
117
|
+
* from frontmatter on the read path.
|
|
118
|
+
*/
|
|
119
|
+
export function splitFrontmatterAndBody(source) {
|
|
120
|
+
const slice = findFrontmatter(source);
|
|
121
|
+
if (!slice)
|
|
122
|
+
return { frontmatterRaw: null, body: source };
|
|
123
|
+
return { frontmatterRaw: slice.inner, body: source.slice(slice.bodyStart) };
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=applyYamlFrontmatterPatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"applyYamlFrontmatterPatch.js","sourceRoot":"","sources":["../../../src/services/utils/applyYamlFrontmatterPatch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Review the current working tree with `git status` and `git diff` (both staged and unstaged). Group the changes into logically coherent units so that each commit represents a single concern (one feature, one fix, one refactor, etc.). For each group:
|
|
2
|
+
|
|
3
|
+
1. Stage only the files (or hunks, via `git add -p`, when a single file mixes concerns) that belong to the group.
|
|
4
|
+
2. Commit with a clear conventional-commit style message that explains *why* the change exists, not just *what* changed.
|
|
5
|
+
3. Move on to the next group.
|
|
6
|
+
|
|
7
|
+
Do not push. Do not amend existing commits. If the changes are already coherent as a single commit, say so and make one commit instead of forcing a split.
|
|
8
|
+
|
|
9
|
+
When you finish, list the resulting commits (hash + subject line) as a summary.
|