@entelligentsia/forgecli 0.7.10 → 0.9.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/CHANGELOG.md +127 -0
- package/dist/CHANGELOG-forge-plugin.md +70 -0
- package/dist/CHANGELOG-pi.md +63 -0
- package/dist/bin/argv.d.ts +2 -2
- package/dist/bin/argv.js +27 -0
- package/dist/bin/argv.js.map +1 -1
- package/dist/bin/config.d.ts +69 -0
- package/dist/bin/config.js +315 -0
- package/dist/bin/config.js.map +1 -0
- package/dist/bin/doctor.d.ts +1 -0
- package/dist/bin/doctor.js +12 -0
- package/dist/bin/doctor.js.map +1 -1
- package/dist/bin/env-defaults.d.ts +1 -0
- package/dist/bin/env-defaults.js +13 -0
- package/dist/bin/env-defaults.js.map +1 -0
- package/dist/bin/forge.js +16 -0
- package/dist/bin/forge.js.map +1 -1
- package/dist/bin/update-cli.d.ts +9 -0
- package/dist/bin/update-cli.js +120 -0
- package/dist/bin/update-cli.js.map +1 -0
- package/dist/extensions/forgecli/config-command.d.ts +8 -0
- package/dist/extensions/forgecli/config-command.js +66 -0
- package/dist/extensions/forgecli/config-command.js.map +1 -0
- package/dist/extensions/forgecli/config-layer.d.ts +38 -0
- package/dist/extensions/forgecli/config-layer.js +68 -0
- package/dist/extensions/forgecli/config-layer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/component.d.ts +35 -0
- package/dist/extensions/forgecli/config-tui/component.js +236 -0
- package/dist/extensions/forgecli/config-tui/component.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/handler.d.ts +40 -0
- package/dist/extensions/forgecli/config-tui/handler.js +240 -0
- package/dist/extensions/forgecli/config-tui/handler.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/index.d.ts +5 -0
- package/dist/extensions/forgecli/config-tui/index.js +5 -0
- package/dist/extensions/forgecli/config-tui/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/keys.d.ts +26 -0
- package/dist/extensions/forgecli/config-tui/keys.js +33 -0
- package/dist/extensions/forgecli/config-tui/keys.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js +58 -0
- package/dist/extensions/forgecli/config-tui/plugin-config-reader.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js +83 -0
- package/dist/extensions/forgecli/config-tui/screens/advanced-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js +54 -0
- package/dist/extensions/forgecli/config-tui/screens/confirm-quit.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js +233 -0
- package/dist/extensions/forgecli/config-tui/screens/override-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js +91 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list-phases.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js +71 -0
- package/dist/extensions/forgecli/config-tui/screens/overrides-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.d.ts +10 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js +182 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-editor.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js +76 -0
- package/dist/extensions/forgecli/config-tui/screens/persona-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js +98 -0
- package/dist/extensions/forgecli/config-tui/screens/personas-list.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.d.ts +29 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js +100 -0
- package/dist/extensions/forgecli/config-tui/screens/shared.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.d.ts +23 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js +128 -0
- package/dist/extensions/forgecli/config-tui/screens/show-resolved.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.d.ts +7 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js +135 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-menu.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.d.ts +9 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js +122 -0
- package/dist/extensions/forgecli/config-tui/screens/tier-picker.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens/types.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js +5 -0
- package/dist/extensions/forgecli/config-tui/screens/types.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/screens.d.ts +24 -0
- package/dist/extensions/forgecli/config-tui/screens.js +78 -0
- package/dist/extensions/forgecli/config-tui/screens.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.d.ts +11 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js +91 -0
- package/dist/extensions/forgecli/config-tui/state/buffer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/constants.d.ts +4 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js +14 -0
- package/dist/extensions/forgecli/config-tui/state/constants.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/index.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state/index.js +9 -0
- package/dist/extensions/forgecli/config-tui/state/index.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/init.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/init.js +30 -0
- package/dist/extensions/forgecli/config-tui/state/init.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/model.d.ts +192 -0
- package/dist/extensions/forgecli/config-tui/state/model.js +4 -0
- package/dist/extensions/forgecli/config-tui/state/model.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.d.ts +2 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js +212 -0
- package/dist/extensions/forgecli/config-tui/state/reducer.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.d.ts +91 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js +231 -0
- package/dist/extensions/forgecli/config-tui/state/selectors.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/state.d.ts +6 -0
- package/dist/extensions/forgecli/config-tui/state.js +11 -0
- package/dist/extensions/forgecli/config-tui/state.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/theme.d.ts +37 -0
- package/dist/extensions/forgecli/config-tui/theme.js +88 -0
- package/dist/extensions/forgecli/config-tui/theme.js.map +1 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.d.ts +28 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js +69 -0
- package/dist/extensions/forgecli/config-tui/tier-meta.js.map +1 -0
- package/dist/extensions/forgecli/config-writer.d.ts +16 -0
- package/dist/extensions/forgecli/config-writer.js +63 -0
- package/dist/extensions/forgecli/config-writer.js.map +1 -0
- package/dist/extensions/forgecli/fix-bug.js +85 -1
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-cli-schema.json +54 -0
- package/dist/extensions/forgecli/forge-commands.js +3 -8
- package/dist/extensions/forgecli/forge-commands.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +13 -0
- package/dist/extensions/forgecli/forge-subagent.js +19 -0
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/index.js +19 -3
- package/dist/extensions/forgecli/index.js.map +1 -1
- package/dist/extensions/forgecli/input-router.d.ts +33 -0
- package/dist/extensions/forgecli/input-router.js +133 -0
- package/dist/extensions/forgecli/input-router.js.map +1 -0
- package/dist/extensions/forgecli/model-resolver.d.ts +32 -0
- package/dist/extensions/forgecli/model-resolver.js +65 -0
- package/dist/extensions/forgecli/model-resolver.js.map +1 -0
- package/dist/extensions/forgecli/model-validator.d.ts +29 -0
- package/dist/extensions/forgecli/model-validator.js +107 -0
- package/dist/extensions/forgecli/model-validator.js.map +1 -0
- package/dist/extensions/forgecli/run-sprint.js +59 -0
- package/dist/extensions/forgecli/run-sprint.js.map +1 -1
- package/dist/extensions/forgecli/run-task.js +93 -1
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/thread-switcher.js +5 -2
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/update-check.js +1 -1
- package/dist/extensions/forgecli/update-check.js.map +1 -1
- package/dist/extensions/forgecli/whats-new-widget.d.ts +5 -5
- package/dist/extensions/forgecli/whats-new-widget.js +16 -13
- package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
- package/dist/extensions/forgecli/whats-new.js +6 -5
- package/dist/extensions/forgecli/whats-new.js.map +1 -1
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +27 -98
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +62 -132
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +25 -15
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js +17 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js +8 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js +17 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js +8 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/node_modules/@earendil-works/pi-ai/package.json +2 -2
- package/node_modules/@earendil-works/pi-coding-agent/CHANGELOG.md +63 -0
- package/node_modules/@earendil-works/pi-coding-agent/README.md +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli/config-selector.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli/config-selector.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli/config-selector.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js +6 -10
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/config.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +12 -3
- package/node_modules/@earendil-works/pi-coding-agent/dist/config.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts +1 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +30 -15
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.d.ts +3 -3
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +23 -13
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.d.ts +4 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js +58 -38
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/slash-commands.js +0 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +3 -2
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.d.ts +2 -2
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js +7 -4
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -2
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js +3 -4
- package/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/changelog.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/changelog.js +2 -2
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/changelog.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/child-process.d.ts +7 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/child-process.d.ts.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/child-process.js +60 -7
- package/node_modules/@earendil-works/pi-coding-agent/dist/utils/child-process.js.map +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/docs/packages.md +2 -2
- package/node_modules/@earendil-works/pi-coding-agent/docs/settings.md +1 -3
- package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/sandbox/package.json +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/with-deps/package.json +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/package.json +6 -6
- package/node_modules/@earendil-works/pi-tui/package.json +2 -2
- package/node_modules/@protobufjs/fetch/CHANGELOG.md +8 -0
- package/node_modules/@protobufjs/fetch/index.d.ts +7 -7
- package/node_modules/@protobufjs/fetch/index.js +4 -7
- package/node_modules/@protobufjs/fetch/package.json +7 -5
- package/node_modules/@protobufjs/fetch/tests/data/file.txt +1 -0
- package/node_modules/@protobufjs/fetch/tests/index.js +150 -8
- package/node_modules/@protobufjs/fetch/util/fs.js +11 -0
- package/node_modules/@protobufjs/inquire/CHANGELOG.md +8 -0
- package/node_modules/@protobufjs/inquire/index.d.ts +1 -0
- package/node_modules/@protobufjs/inquire/index.js +1 -0
- package/node_modules/@protobufjs/inquire/package.json +1 -1
- package/node_modules/protobufjs/dist/light/protobuf.js +187 -153
- package/node_modules/protobufjs/dist/light/protobuf.js.map +1 -1
- package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/light/protobuf.min.js.map +1 -1
- package/node_modules/protobufjs/dist/minimal/protobuf.js +14 -5
- package/node_modules/protobufjs/dist/minimal/protobuf.js.map +1 -1
- package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/minimal/protobuf.min.js.map +1 -1
- package/node_modules/protobufjs/dist/protobuf.js +207 -173
- package/node_modules/protobufjs/dist/protobuf.js.map +1 -1
- package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
- package/node_modules/protobufjs/dist/protobuf.min.js.map +1 -1
- package/node_modules/protobufjs/package.json +6 -3
- package/node_modules/protobufjs/src/util/fs.js +11 -0
- package/node_modules/protobufjs/src/util/minimal.js +10 -2
- package/node_modules/protobufjs/src/util.js +1 -1
- package/node_modules/undici/README.md +14 -5
- package/node_modules/undici/docs/docs/api/Client.md +4 -2
- package/node_modules/undici/docs/docs/api/Dispatcher.md +62 -27
- package/node_modules/undici/docs/docs/api/GlobalInstallation.md +7 -5
- package/node_modules/undici/docs/docs/api/H2CClient.md +1 -1
- package/node_modules/undici/docs/docs/api/RedirectHandler.md +14 -9
- package/node_modules/undici/docs/docs/api/RetryAgent.md +0 -1
- package/node_modules/undici/docs/docs/api/RetryHandler.md +12 -14
- package/node_modules/undici/docs/docs/api/SnapshotAgent.md +23 -0
- package/node_modules/undici/docs/docs/best-practices/migrating-from-v7-to-v8.md +231 -0
- package/node_modules/undici/index.js +4 -2
- package/node_modules/undici/lib/api/api-connect.js +13 -11
- package/node_modules/undici/lib/api/api-pipeline.js +26 -13
- package/node_modules/undici/lib/api/api-request.js +45 -21
- package/node_modules/undici/lib/api/api-stream.js +81 -20
- package/node_modules/undici/lib/api/api-upgrade.js +21 -11
- package/node_modules/undici/lib/api/readable.js +3 -2
- package/node_modules/undici/lib/cache/memory-cache-store.js +1 -1
- package/node_modules/undici/lib/cache/sqlite-cache-store.js +6 -4
- package/node_modules/undici/lib/core/connect.js +17 -1
- package/node_modules/undici/lib/core/constants.js +1 -24
- package/node_modules/undici/lib/core/errors.js +2 -2
- package/node_modules/undici/lib/core/request.js +115 -18
- package/node_modules/undici/lib/core/socks5-client.js +24 -9
- package/node_modules/undici/lib/core/socks5-utils.js +32 -23
- package/node_modules/undici/lib/core/symbols.js +1 -0
- package/node_modules/undici/lib/core/util.js +70 -43
- package/node_modules/undici/lib/dispatcher/agent.js +47 -33
- package/node_modules/undici/lib/dispatcher/balanced-pool.js +21 -26
- package/node_modules/undici/lib/dispatcher/client-h1.js +98 -39
- package/node_modules/undici/lib/dispatcher/client-h2.js +603 -272
- package/node_modules/undici/lib/dispatcher/client.js +12 -5
- package/node_modules/undici/lib/dispatcher/dispatcher-base.js +24 -5
- package/node_modules/undici/lib/dispatcher/dispatcher.js +0 -4
- package/node_modules/undici/lib/dispatcher/dispatcher1-wrapper.js +107 -0
- package/node_modules/undici/lib/dispatcher/h2c-client.js +5 -5
- package/node_modules/undici/lib/dispatcher/pool-base.js +28 -10
- package/node_modules/undici/lib/dispatcher/pool.js +31 -6
- package/node_modules/undici/lib/dispatcher/proxy-agent.js +38 -13
- package/node_modules/undici/lib/dispatcher/round-robin-pool.js +31 -9
- package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +95 -80
- package/node_modules/undici/lib/global.js +13 -1
- package/node_modules/undici/lib/handler/cache-handler.js +16 -8
- package/node_modules/undici/lib/handler/decorator-handler.js +1 -2
- package/node_modules/undici/lib/handler/redirect-handler.js +5 -51
- package/node_modules/undici/lib/handler/retry-handler.js +15 -2
- package/node_modules/undici/lib/interceptor/cache.js +30 -17
- package/node_modules/undici/lib/interceptor/decompress.js +28 -2
- package/node_modules/undici/lib/interceptor/dns.js +1 -1
- package/node_modules/undici/lib/interceptor/redirect.js +3 -3
- package/node_modules/undici/lib/llhttp/llhttp-wasm.js +1 -1
- package/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +1 -1
- package/node_modules/undici/lib/mock/mock-agent.js +8 -8
- package/node_modules/undici/lib/mock/mock-call-history.js +15 -15
- package/node_modules/undici/lib/mock/mock-utils.js +37 -22
- package/node_modules/undici/lib/mock/snapshot-agent.js +16 -6
- package/node_modules/undici/lib/mock/snapshot-recorder.js +38 -3
- package/node_modules/undici/lib/util/cache.js +8 -7
- package/node_modules/undici/lib/util/runtime-features.js +3 -34
- package/node_modules/undici/lib/web/cache/cache.js +6 -8
- package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +245 -150
- package/node_modules/undici/lib/web/fetch/body.js +3 -9
- package/node_modules/undici/lib/web/fetch/formdata-parser.js +17 -6
- package/node_modules/undici/lib/web/fetch/formdata.js +21 -2
- package/node_modules/undici/lib/web/fetch/index.js +214 -221
- package/node_modules/undici/lib/web/webidl/index.js +7 -9
- package/node_modules/undici/lib/web/websocket/frame.js +1 -7
- package/node_modules/undici/lib/web/websocket/permessage-deflate.js +13 -31
- package/node_modules/undici/lib/web/websocket/receiver.js +62 -22
- package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +11 -17
- package/node_modules/undici/lib/web/websocket/websocket.js +6 -1
- package/node_modules/undici/package.json +9 -9
- package/node_modules/undici/types/agent.d.ts +0 -2
- package/node_modules/undici/types/client.d.ts +25 -19
- package/node_modules/undici/types/dispatcher.d.ts +7 -27
- package/node_modules/undici/types/dispatcher1-wrapper.d.ts +7 -0
- package/node_modules/undici/types/formdata.d.ts +0 -6
- package/node_modules/undici/types/h2c-client.d.ts +6 -6
- package/node_modules/undici/types/header.d.ts +5 -0
- package/node_modules/undici/types/index.d.ts +3 -1
- package/node_modules/undici/types/interceptors.d.ts +1 -1
- package/node_modules/undici/types/pool.d.ts +0 -2
- package/node_modules/undici/types/proxy-agent.d.ts +2 -2
- package/node_modules/undici/types/round-robin-pool.d.ts +0 -2
- package/node_modules/undici/types/snapshot-agent.d.ts +4 -0
- package/node_modules/undici/types/socks5-proxy-agent.d.ts +2 -2
- package/node_modules/undici/types/webidl.d.ts +0 -1
- package/package.json +16 -9
- package/dist/extensions/forgecli/review-command.d.ts +0 -2
- package/dist/extensions/forgecli/review-command.js +0 -184
- package/dist/extensions/forgecli/review-command.js.map +0 -1
- package/dist/forge-payload/.tools/banners.cjs +0 -435
- package/dist/forge-payload/.tools/build-context-pack.cjs +0 -290
- package/dist/forge-payload/.tools/build-init-context.cjs +0 -322
- package/dist/forge-payload/.tools/build-overlay.cjs +0 -326
- package/dist/forge-payload/.tools/build-persona-pack.cjs +0 -226
- package/dist/forge-payload/.tools/collate.cjs +0 -1041
- package/dist/forge-payload/.tools/generation-manifest.cjs +0 -311
- package/dist/forge-payload/.tools/lib/forge-root.cjs +0 -59
- package/dist/forge-payload/.tools/lib/paths.cjs +0 -29
- package/dist/forge-payload/.tools/lib/pricing.cjs +0 -165
- package/dist/forge-payload/.tools/lib/project-root.cjs +0 -32
- package/dist/forge-payload/.tools/lib/result.js +0 -40
- package/dist/forge-payload/.tools/lib/store-facade.cjs +0 -162
- package/dist/forge-payload/.tools/lib/store-nlp.cjs +0 -250
- package/dist/forge-payload/.tools/lib/store-query-exec.cjs +0 -272
- package/dist/forge-payload/.tools/lib/validate.js +0 -141
- package/dist/forge-payload/.tools/manage-config.cjs +0 -340
- package/dist/forge-payload/.tools/manage-versions.cjs +0 -365
- package/dist/forge-payload/.tools/package.json +0 -3
- package/dist/forge-payload/.tools/parse-gates.cjs +0 -151
- package/dist/forge-payload/.tools/parse-verdict.cjs +0 -67
- package/dist/forge-payload/.tools/preflight-gate.cjs +0 -350
- package/dist/forge-payload/.tools/prompts/sprint-plan-prompt.md +0 -70
- package/dist/forge-payload/.tools/schemas/task-list.schema.json +0 -53
- package/dist/forge-payload/.tools/seed-store.cjs +0 -237
- package/dist/forge-payload/.tools/store-cli.cjs +0 -1226
- package/dist/forge-payload/.tools/store-query.cjs +0 -319
- package/dist/forge-payload/.tools/store.cjs +0 -315
- package/dist/forge-payload/.tools/substitute-placeholders.cjs +0 -625
- package/dist/forge-payload/.tools/validate-store.cjs +0 -593
- package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +0 -24
- package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/sandbox/package-lock.json +0 -92
- package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/with-deps/package-lock.json +0 -31
- package/node_modules/undici/lib/handler/unwrap-handler.js +0 -100
- package/node_modules/undici/lib/handler/wrap-handler.js +0 -105
- package/node_modules/undici/lib/llhttp/.gitkeep +0 -0
- package/node_modules/undici/lib/util/promise.js +0 -28
- package/skills/refresh-kb-links/SKILL.md +0 -217
- package/skills/store-custodian/SKILL.md +0 -163
- package/skills/store-query-grammar/SKILL.md +0 -145
- package/skills/store-query-nlp/SKILL.md +0 -110
|
@@ -1,1041 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
// Forge tool: collate
|
|
5
|
-
// Regenerate markdown views from the JSON store. Deterministic — no AI needed.
|
|
6
|
-
// Usage: collate [SPRINT_ID | BUG_ID] [--dry-run] [--purge-events]
|
|
7
|
-
//
|
|
8
|
-
// Positional argument can be a sprint ID (e.g. FORGE-S12) or a bug ID
|
|
9
|
-
// (e.g. FORGE-BUG-007, HELLO-B02). Bug IDs are first-class arguments:
|
|
10
|
-
// passing a bug ID automatically enables event purging for that entity,
|
|
11
|
-
// so `collate HELLO-B02` works identically to `collate HELLO-B02 --purge-events`.
|
|
12
|
-
//
|
|
13
|
-
// --purge-events After generating COST_REPORT.md for the given SPRINT_ID,
|
|
14
|
-
// delete .forge/store/events/{SPRINT_ID}/ entirely.
|
|
15
|
-
// If SPRINT_ID is not a known sprint (e.g. a bug ID), no
|
|
16
|
-
// COST_REPORT is generated but the event directory is still
|
|
17
|
-
// purged. Requires a positional argument. Safe to combine with
|
|
18
|
-
// --dry-run (reports what would be deleted without deleting).
|
|
19
|
-
|
|
20
|
-
const fs = require('fs');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const { ok: resultOk, fail: resultFail, RESULT_CODES } = require('./lib/result.js');
|
|
23
|
-
const { computeCost, canonicalizeModel } = require('./lib/pricing.cjs');
|
|
24
|
-
|
|
25
|
-
let _store;
|
|
26
|
-
function _getStore() { return _store || (_store = require('./store.cjs')); }
|
|
27
|
-
|
|
28
|
-
const GENERATED = '<!-- GENERATED by forge/collate — do not edit manually -->';
|
|
29
|
-
|
|
30
|
-
// --- Pure helpers (exported for testing) ---
|
|
31
|
-
|
|
32
|
-
function statusBadge(status) {
|
|
33
|
-
const map = {
|
|
34
|
-
committed: '✅', approved: '✅', completed: '✅',
|
|
35
|
-
'retrospective-done': '✅', fixed: '✅', verified: '✅',
|
|
36
|
-
implemented: '🔧', implementing: '🔧',
|
|
37
|
-
'in-progress': '🔵', active: '🔵',
|
|
38
|
-
'plan-approved': '📋', 'review-approved': '📋', planned: '📋',
|
|
39
|
-
planning: '📝', draft: '📝',
|
|
40
|
-
triaged: '🟡', 'partially-completed': '🟡',
|
|
41
|
-
'plan-revision-required': '🔄', 'code-revision-required': '🔄',
|
|
42
|
-
reported: '🔴', blocked: '🚫', escalated: '⚠️',
|
|
43
|
-
abandoned: '❌',
|
|
44
|
-
};
|
|
45
|
-
const badge = map[status];
|
|
46
|
-
return badge ? `${badge} ${status}` : status;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function padTable(rows) {
|
|
50
|
-
if (rows.length === 0) return '';
|
|
51
|
-
const cols = rows[0].length;
|
|
52
|
-
const widths = Array.from({ length: cols }, (_, i) => Math.max(...rows.map(r => String(r[i] ?? '').length)));
|
|
53
|
-
return rows.map((r, ri) => {
|
|
54
|
-
const cells = r.map((c, i) => String(c ?? '').padEnd(widths[i]));
|
|
55
|
-
const line = `| ${cells.join(' | ')} |`;
|
|
56
|
-
if (ri === 0) return line + '\n' + `| ${widths.map(w => '-'.repeat(w)).join(' | ')} |`;
|
|
57
|
-
return line;
|
|
58
|
-
}).join('\n');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function fmtTokens(n) {
|
|
62
|
-
if (n === undefined || n === null) return '—';
|
|
63
|
-
return n.toLocaleString('en-US');
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function fmtCost(n) {
|
|
67
|
-
if (n === undefined || n === null) return '—';
|
|
68
|
-
return `$${n.toFixed(4)}`;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function sourceLabel(sources) {
|
|
72
|
-
// sources: Set of tokenSource values (may include undefined)
|
|
73
|
-
const vals = [...sources];
|
|
74
|
-
const hasReported = vals.includes('reported');
|
|
75
|
-
const hasEstimated = vals.includes('estimated');
|
|
76
|
-
const hasUnknown = vals.some(v => v === undefined || v === null);
|
|
77
|
-
|
|
78
|
-
if (hasReported && !hasEstimated && !hasUnknown) return '(reported)';
|
|
79
|
-
if (hasEstimated && !hasReported && !hasUnknown) return '(estimated)';
|
|
80
|
-
if (!hasReported && !hasEstimated && hasUnknown) return '(unknown)';
|
|
81
|
-
return '(mixed)';
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const SPRINT_DOCS = [
|
|
85
|
-
{ file: 'SPRINT_PLAN.md', label: 'Sprint Plan', purpose: 'Sprint scope and task definitions' },
|
|
86
|
-
{ file: 'SPRINT_REQUIREMENTS.md', label: 'Sprint Requirements', purpose: 'Requirements and acceptance criteria' },
|
|
87
|
-
{ file: 'COST_REPORT.md', label: 'Cost Report', purpose: 'Token usage and cost analysis' },
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
const TASK_DOCS = [
|
|
91
|
-
{ file: 'TASK_PROMPT.md', label: 'Task Prompt', purpose: 'Task definition and prompt' },
|
|
92
|
-
{ file: 'PLAN.md', label: 'Plan', purpose: 'Implementation plan' },
|
|
93
|
-
{ file: 'PROGRESS.md', label: 'Progress', purpose: 'Implementation progress log' },
|
|
94
|
-
{ file: 'ARCHITECT_APPROVAL.md', label: 'Architect Approval', purpose: 'Architecture review' },
|
|
95
|
-
{ file: 'CODE_REVIEW.md', label: 'Code Review', purpose: 'Code review' },
|
|
96
|
-
{ file: 'VALIDATION_REPORT.md', label: 'Validation Report', purpose: 'Validation results' },
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
// Resolve the task directory name within a sprint directory.
|
|
100
|
-
// Returns the directory name string, or null if no directory can be found.
|
|
101
|
-
//
|
|
102
|
-
// Resolution order:
|
|
103
|
-
// 1. If task.path is under engPath (an engineering KB path) — basename of that path.
|
|
104
|
-
// 2. Filesystem scan of sprintDirPath for a directory whose name starts with taskId
|
|
105
|
-
// or whose leading integer matches that of taskId (slug-named dirs).
|
|
106
|
-
// 3. null if nothing matches.
|
|
107
|
-
function resolveTaskDir(task, sprintDirPath, engPath) {
|
|
108
|
-
const normalizedTaskPath = task.path ? task.path.replace(/\\/g, '/').replace(/\/$/, '') : null;
|
|
109
|
-
const normalizedEngPath = engPath ? engPath.replace(/\\/g, '/').replace(/\/$/, '') : '';
|
|
110
|
-
|
|
111
|
-
// Case 1: path is under the engineering root — it IS the task directory
|
|
112
|
-
if (normalizedTaskPath && normalizedEngPath && normalizedTaskPath.startsWith(normalizedEngPath + '/')) {
|
|
113
|
-
return resultOk(path.basename(normalizedTaskPath));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Case 2 (and fallback for case 1 missing): filesystem scan
|
|
117
|
-
if (fs.existsSync(sprintDirPath)) {
|
|
118
|
-
const entries = fs.readdirSync(sprintDirPath).sort();
|
|
119
|
-
// Prefer exact match first
|
|
120
|
-
if (entries.includes(task.taskId)) return resultOk(task.taskId);
|
|
121
|
-
// Then prefix match (slug-named dirs like TST-S01-T01-my-feature)
|
|
122
|
-
const prefix = task.taskId + '-';
|
|
123
|
-
const slugMatch = entries.find(e => e.startsWith(prefix) && fs.statSync(path.join(sprintDirPath, e)).isDirectory());
|
|
124
|
-
if (slugMatch) return resultOk(slugMatch);
|
|
125
|
-
// Numeric fallback: match first integer in taskId
|
|
126
|
-
const numMatch = task.taskId.match(/\d+/g);
|
|
127
|
-
const targetNum = numMatch ? parseInt(numMatch[numMatch.length - 1], 10) : null;
|
|
128
|
-
if (targetNum !== null) {
|
|
129
|
-
for (const e of entries) {
|
|
130
|
-
const em = e.match(/\d+/g);
|
|
131
|
-
if (em && parseInt(em[em.length - 1], 10) === targetNum && fs.statSync(path.join(sprintDirPath, e)).isDirectory()) {
|
|
132
|
-
return resultOk(e);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return resultFail(RESULT_CODES.MISSING_DIR, `No task directory found for ${task.taskId} in ${sprintDirPath}`);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function buildSprintIndex(sprint, tasks, availableDocs) {
|
|
142
|
-
const avail = new Set(availableDocs);
|
|
143
|
-
const lines = [GENERATED, '', `# Sprint: ${sprint.title || sprint.sprintId}`, '',
|
|
144
|
-
`> Sprint ID: ${sprint.sprintId}`,
|
|
145
|
-
`> Status: ${statusBadge(sprint.status)}`,
|
|
146
|
-
sprint.executionMode ? `> Execution Mode: ${sprint.executionMode}` : null,
|
|
147
|
-
''].filter(l => l !== null);
|
|
148
|
-
|
|
149
|
-
if (sprint.goal) lines.push('## Goal', '', sprint.goal, '');
|
|
150
|
-
if (sprint.description && sprint.description !== sprint.goal) lines.push('## Description', '', sprint.description, '');
|
|
151
|
-
|
|
152
|
-
const presentDocs = SPRINT_DOCS.filter(d => avail.has(d.file));
|
|
153
|
-
if (presentDocs.length > 0) {
|
|
154
|
-
lines.push('## Sprint Documents', '');
|
|
155
|
-
const rows = [['Document', 'Purpose']];
|
|
156
|
-
for (const d of presentDocs) rows.push([`[${d.label}](${d.file})`, d.purpose]);
|
|
157
|
-
lines.push(padTable(rows), '');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
lines.push('## Tasks', '');
|
|
161
|
-
if (tasks.length > 0) {
|
|
162
|
-
const rows = [['Task', 'Title', 'Status', 'Estimate']];
|
|
163
|
-
for (const t of tasks) {
|
|
164
|
-
const linkDir = t._taskDir || t.taskId;
|
|
165
|
-
rows.push([`[${t.taskId}](${linkDir}/INDEX.md)`, t.title || '—', statusBadge(t.status), t.estimate || '—']);
|
|
166
|
-
}
|
|
167
|
-
lines.push(padTable(rows), '');
|
|
168
|
-
} else {
|
|
169
|
-
lines.push('_No tasks._', '');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return lines.join('\n') + '\n';
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function buildTaskIndex(task, availableDocs) {
|
|
176
|
-
const avail = new Set(availableDocs);
|
|
177
|
-
const lines = [GENERATED, '', `# Task: ${task.title || task.taskId}`, '',
|
|
178
|
-
`> Task ID: ${task.taskId}`,
|
|
179
|
-
`> Sprint: [${task.sprintId}](../INDEX.md)`,
|
|
180
|
-
`> Status: ${statusBadge(task.status)}`,
|
|
181
|
-
task.estimate ? `> Estimate: ${task.estimate}` : null,
|
|
182
|
-
''].filter(l => l !== null);
|
|
183
|
-
|
|
184
|
-
if (task.description) lines.push('## Description', '', task.description, '');
|
|
185
|
-
|
|
186
|
-
const presentDocs = TASK_DOCS.filter(d => avail.has(d.file));
|
|
187
|
-
if (presentDocs.length > 0) {
|
|
188
|
-
lines.push('## Task Documents', '');
|
|
189
|
-
const rows = [['Document', 'Purpose']];
|
|
190
|
-
for (const d of presentDocs) rows.push([`[${d.label}](${d.file})`, d.purpose]);
|
|
191
|
-
lines.push(padTable(rows), '');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return lines.join('\n') + '\n';
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const BUG_DOCS = [
|
|
198
|
-
{ file: 'ANALYSIS.md', label: 'Analysis', purpose: 'Bug analysis and triage' },
|
|
199
|
-
{ file: 'BUG_FIX_PLAN.md', label: 'Fix Plan', purpose: 'Fix implementation plan' },
|
|
200
|
-
{ file: 'PLAN.md', label: 'Plan', purpose: 'Fix implementation plan' },
|
|
201
|
-
{ file: 'PLAN_REVIEW.md', label: 'Plan Review', purpose: 'Fix plan review' },
|
|
202
|
-
{ file: 'PROGRESS.md', label: 'Progress', purpose: 'Fix progress log' },
|
|
203
|
-
{ file: 'APPROVAL.md', label: 'Approval', purpose: 'Architecture approval' },
|
|
204
|
-
{ file: 'ARCHITECT_APPROVAL.md', label: 'Architect Approval', purpose: 'Architecture approval' },
|
|
205
|
-
{ file: 'CODE_REVIEW.md', label: 'Code Review', purpose: 'Code review' },
|
|
206
|
-
{ file: 'VALIDATION_REPORT.md', label: 'Validation Report', purpose: 'Fix validation' },
|
|
207
|
-
];
|
|
208
|
-
|
|
209
|
-
function buildBugIndex(bug, availableDocs, costTotals) {
|
|
210
|
-
const avail = new Set(availableDocs);
|
|
211
|
-
const lines = [GENERATED, '', `# Bug: ${bug.title || bug.bugId}`, '',
|
|
212
|
-
`> Bug ID: ${bug.bugId}`,
|
|
213
|
-
`> Severity: ${bug.severity || '—'}`,
|
|
214
|
-
`> Status: ${statusBadge(bug.status)}`,
|
|
215
|
-
bug.reportedAt ? `> Reported: ${bug.reportedAt.slice(0, 10)}` : null,
|
|
216
|
-
bug.resolvedAt ? `> Resolved: ${bug.resolvedAt.slice(0, 10)}` : null,
|
|
217
|
-
''].filter(l => l !== null);
|
|
218
|
-
|
|
219
|
-
if (bug.description) lines.push('## Description', '', bug.description, '');
|
|
220
|
-
|
|
221
|
-
// Cost aggregation section — included when costTotals is provided
|
|
222
|
-
// (typically when --purge-events aggregates event costs before deletion).
|
|
223
|
-
if (costTotals && costTotals.inputTokens !== undefined) {
|
|
224
|
-
lines.push('## Cost', '');
|
|
225
|
-
const rows = [
|
|
226
|
-
['Input Tokens', 'Output Tokens', 'Cache Read', 'Cache Write', 'Est. Cost USD', 'Source'],
|
|
227
|
-
[
|
|
228
|
-
fmtTokens(costTotals.inputTokens),
|
|
229
|
-
fmtTokens(costTotals.outputTokens),
|
|
230
|
-
fmtTokens(costTotals.cacheReadTokens),
|
|
231
|
-
fmtTokens(costTotals.cacheWriteTokens),
|
|
232
|
-
fmtCost(costTotals.estimatedCostUSD),
|
|
233
|
-
costTotals.sourceLabel || '—',
|
|
234
|
-
],
|
|
235
|
-
];
|
|
236
|
-
lines.push(padTable(rows), '');
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const presentDocs = BUG_DOCS.filter(d => avail.has(d.file));
|
|
240
|
-
if (presentDocs.length > 0) {
|
|
241
|
-
lines.push('## Bug Documents', '');
|
|
242
|
-
const rows = [['Document', 'Purpose']];
|
|
243
|
-
for (const d of presentDocs) rows.push([`[${d.label}](${d.file})`, d.purpose]);
|
|
244
|
-
lines.push(padTable(rows), '');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return lines.join('\n') + '\n';
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Detect whether a string matches the bug-ID pattern.
|
|
252
|
-
* Bug IDs match one of these patterns:
|
|
253
|
-
* 1. Contains 'BUG-' followed by digits (e.g., BUG-001, FORGE-BUG-007)
|
|
254
|
-
* 2. Contains '-B' followed by digits at a segment boundary (e.g., HELLO-B02)
|
|
255
|
-
* Sprint IDs (FORGE-S12) and task IDs (FORGE-S12-T03) are excluded.
|
|
256
|
-
*/
|
|
257
|
-
function isBugId(id) {
|
|
258
|
-
if (!id || typeof id !== 'string') return false;
|
|
259
|
-
return /BUG-\d+/.test(id) || /-B\d+\b/.test(id);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* mergeSidecarEvents(primaryEvents, sidecars)
|
|
264
|
-
*
|
|
265
|
-
* Merges sidecar token data onto matching primary events by eventId.
|
|
266
|
-
* Returns { events, orphanSidecars, huskPrimaries } where:
|
|
267
|
-
* - events: all primary events (merged primaries have token fields from sidecar)
|
|
268
|
-
* - orphanSidecars: sidecars with no matching primary
|
|
269
|
-
* - huskPrimaries: primaries that still have no token data after merge attempt
|
|
270
|
-
*
|
|
271
|
-
* On merge:
|
|
272
|
-
* - Token fields (inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens,
|
|
273
|
-
* estimatedCostUSD) are copied from the sidecar onto the primary.
|
|
274
|
-
* - If all four token counts are present after merge and the primary has a known
|
|
275
|
-
* model, estimatedCostUSD is recomputed via pricing.computeCost (overwrites
|
|
276
|
-
* any fabricated value from the sidecar).
|
|
277
|
-
* - The model field on the primary is canonicalized if possible.
|
|
278
|
-
*
|
|
279
|
-
* Pure function — no filesystem I/O.
|
|
280
|
-
*/
|
|
281
|
-
function mergeSidecarEvents(primaryEvents, sidecars) {
|
|
282
|
-
// Build sidecar lookup map by eventId
|
|
283
|
-
const sidecarMap = new Map();
|
|
284
|
-
for (const s of sidecars) {
|
|
285
|
-
if (s && s.eventId) {
|
|
286
|
-
sidecarMap.set(s.eventId, s);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const usedSidecarIds = new Set();
|
|
291
|
-
const events = [];
|
|
292
|
-
const huskPrimaries = [];
|
|
293
|
-
|
|
294
|
-
for (const primary of primaryEvents) {
|
|
295
|
-
if (!primary) continue;
|
|
296
|
-
const merged = Object.assign({}, primary);
|
|
297
|
-
|
|
298
|
-
// Canonicalize model on read
|
|
299
|
-
if (merged.model) {
|
|
300
|
-
const canonResult = canonicalizeModel(merged.model);
|
|
301
|
-
if (canonResult !== null) {
|
|
302
|
-
merged.model = canonResult.canonical;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Attempt sidecar merge
|
|
307
|
-
const sidecar = sidecarMap.get(merged.eventId);
|
|
308
|
-
if (sidecar) {
|
|
309
|
-
usedSidecarIds.add(merged.eventId);
|
|
310
|
-
// Merge token fields from sidecar
|
|
311
|
-
if (sidecar.inputTokens !== undefined) merged.inputTokens = sidecar.inputTokens;
|
|
312
|
-
if (sidecar.outputTokens !== undefined) merged.outputTokens = sidecar.outputTokens;
|
|
313
|
-
if (sidecar.cacheReadTokens !== undefined) merged.cacheReadTokens = sidecar.cacheReadTokens;
|
|
314
|
-
if (sidecar.cacheWriteTokens !== undefined) merged.cacheWriteTokens = sidecar.cacheWriteTokens;
|
|
315
|
-
if (sidecar.estimatedCostUSD !== undefined) merged.estimatedCostUSD = sidecar.estimatedCostUSD;
|
|
316
|
-
if (sidecar.tokenSource !== undefined) merged.tokenSource = sidecar.tokenSource;
|
|
317
|
-
if (sidecar.model && !merged.model) merged.model = sidecar.model;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// Recompute cost when all four counts are present and model is known
|
|
321
|
-
if (
|
|
322
|
-
merged.inputTokens !== undefined &&
|
|
323
|
-
merged.outputTokens !== undefined &&
|
|
324
|
-
merged.cacheReadTokens !== undefined &&
|
|
325
|
-
merged.cacheWriteTokens !== undefined &&
|
|
326
|
-
merged.model
|
|
327
|
-
) {
|
|
328
|
-
const recomputed = computeCost({
|
|
329
|
-
inputTokens: merged.inputTokens,
|
|
330
|
-
outputTokens: merged.outputTokens,
|
|
331
|
-
cacheReadTokens: merged.cacheReadTokens,
|
|
332
|
-
cacheWriteTokens: merged.cacheWriteTokens,
|
|
333
|
-
model: merged.model,
|
|
334
|
-
});
|
|
335
|
-
if (recomputed !== null) {
|
|
336
|
-
merged.estimatedCostUSD = recomputed;
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
events.push(merged);
|
|
341
|
-
|
|
342
|
-
// Classify as husk if no token data after merge
|
|
343
|
-
if (merged.inputTokens === undefined) {
|
|
344
|
-
huskPrimaries.push(merged);
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Collect orphan sidecars (no matching primary)
|
|
349
|
-
const orphanSidecars = sidecars.filter(s => s && s.eventId && !usedSidecarIds.has(s.eventId));
|
|
350
|
-
|
|
351
|
-
return { events, orphanSidecars, huskPrimaries };
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* loadSprintEvents(sprintId)
|
|
356
|
-
*
|
|
357
|
-
* Reads primary and sidecar event files for a sprint from the store.
|
|
358
|
-
* Returns { events, orphanSidecars, huskPrimaries } — see mergeSidecarEvents.
|
|
359
|
-
*
|
|
360
|
-
* Primary files: .json files NOT starting with '_'
|
|
361
|
-
* Sidecar files: .json files starting with '_' and ending with '_usage.json'
|
|
362
|
-
*/
|
|
363
|
-
function loadSprintEvents(sprintId) {
|
|
364
|
-
const store = _getStore();
|
|
365
|
-
const allFilenames = store.listEventFilenames(sprintId);
|
|
366
|
-
|
|
367
|
-
const primaryFilenames = allFilenames.filter(({ filename }) => !filename.startsWith('_'));
|
|
368
|
-
const sidecarFilenames = allFilenames.filter(({ filename }) =>
|
|
369
|
-
filename.startsWith('_') && filename.endsWith('_usage.json')
|
|
370
|
-
);
|
|
371
|
-
|
|
372
|
-
// Read each primary event individually (avoids re-reading sidecars)
|
|
373
|
-
const primaryEvents = primaryFilenames
|
|
374
|
-
.map(({ id }) => store.getEvent(id, sprintId))
|
|
375
|
-
.filter(Boolean);
|
|
376
|
-
|
|
377
|
-
// Read sidecar files via getEvent using the id (filename without .json)
|
|
378
|
-
const sidecars = sidecarFilenames
|
|
379
|
-
.map(({ id }) => store.getEvent(id, sprintId))
|
|
380
|
-
.filter(Boolean);
|
|
381
|
-
|
|
382
|
-
return mergeSidecarEvents(primaryEvents, sidecars);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* buildIngestionQuality(orphanSidecars, huskPrimaries, noTaskEvents, unmappedModels, totalEvents, tokenSourceCounts)
|
|
387
|
-
*
|
|
388
|
-
* Builds the "## Ingestion Quality" section for COST_REPORT.md.
|
|
389
|
-
* Returns a markdown string (without trailing newline; caller adds one).
|
|
390
|
-
*
|
|
391
|
-
* @param {object[]} orphanSidecars - sidecars with no matching primary
|
|
392
|
-
* @param {object[]} huskPrimaries - primaries with no token data
|
|
393
|
-
* @param {object[]} noTaskEvents - token events with no taskId
|
|
394
|
-
* @param {string[]} unmappedModels - raw model strings that did not canonicalize
|
|
395
|
-
* @param {number} totalEvents - total primary events loaded (with and without token data)
|
|
396
|
-
* @param {{reported: number, estimated: number, missing: number}} tokenSourceCounts - tokenSource tallies
|
|
397
|
-
*/
|
|
398
|
-
function buildIngestionQuality(orphanSidecars, huskPrimaries, noTaskEvents, unmappedModels, totalEvents, tokenSourceCounts) {
|
|
399
|
-
const lines = ['## Ingestion Quality', ''];
|
|
400
|
-
|
|
401
|
-
const orphanCount = orphanSidecars ? orphanSidecars.length : 0;
|
|
402
|
-
const huskCount = huskPrimaries ? huskPrimaries.length : 0;
|
|
403
|
-
const noTaskCount = noTaskEvents ? noTaskEvents.length : 0;
|
|
404
|
-
const unmappedCount = unmappedModels ? unmappedModels.length : 0;
|
|
405
|
-
const total = (typeof totalEvents === 'number') ? totalEvents : null;
|
|
406
|
-
const tsc = tokenSourceCounts || null;
|
|
407
|
-
|
|
408
|
-
const hasIssues = orphanCount > 0 || huskCount > 0 || noTaskCount > 0 || unmappedCount > 0;
|
|
409
|
-
|
|
410
|
-
if (!hasIssues && total === null && tsc === null) {
|
|
411
|
-
lines.push('_All events attributed cleanly — no data gaps detected._', '');
|
|
412
|
-
return lines.join('\n');
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const rows = [['Metric', 'Count', 'Detail']];
|
|
416
|
-
|
|
417
|
-
// Total events row — always rendered when totalEvents is provided
|
|
418
|
-
if (total !== null) {
|
|
419
|
-
const withTokenData = total - huskCount;
|
|
420
|
-
rows.push(['Total events', String(total), `${withTokenData} with token data`]);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Token source breakdown — always rendered when tokenSourceCounts is provided
|
|
424
|
-
if (tsc !== null) {
|
|
425
|
-
const reported = tsc.reported || 0;
|
|
426
|
-
const estimated = tsc.estimated || 0;
|
|
427
|
-
const missing = tsc.missing || 0;
|
|
428
|
-
rows.push(['Token source breakdown', String(reported + estimated + missing), `reported: ${reported}, estimated: ${estimated}, missing: ${missing}`]);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
if (orphanCount > 0) {
|
|
432
|
-
const ids = orphanSidecars.map(s => s.eventId || '?').join(', ');
|
|
433
|
-
rows.push(['Orphan sidecars (no matching primary)', String(orphanCount), ids.length > 80 ? ids.slice(0, 77) + '…' : ids]);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
if (huskCount > 0) {
|
|
437
|
-
const ids = huskPrimaries.map(h => h.eventId || '?').join(', ');
|
|
438
|
-
rows.push(['Primary events with no token data (husks)', String(huskCount), ids.length > 80 ? ids.slice(0, 77) + '…' : ids]);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
if (noTaskCount > 0) {
|
|
442
|
-
rows.push(['Primary events with no taskId (no-task)', String(noTaskCount), 'Attributed to "no-task" row in Per-Task Totals']);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (unmappedCount > 0) {
|
|
446
|
-
const raw = unmappedModels.join(', ');
|
|
447
|
-
rows.push(['Unmapped model names (cost not recomputed)', String(unmappedCount), raw.length > 80 ? raw.slice(0, 77) + '…' : raw]);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
lines.push(padTable(rows), '');
|
|
451
|
-
return lines.join('\n');
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* buildCostReport(sprint, events, orphanSidecars, huskPrimaries)
|
|
456
|
-
*
|
|
457
|
-
* Pure function — builds the full COST_REPORT.md content string for a sprint.
|
|
458
|
-
*
|
|
459
|
-
* @param {object} sprint - sprint record with sprintId and title
|
|
460
|
-
* @param {object[]} events - merged primary events (from mergeSidecarEvents)
|
|
461
|
-
* @param {object[]} orphanSidecars - sidecars with no matching primary
|
|
462
|
-
* @param {object[]} huskPrimaries - primaries with no token data
|
|
463
|
-
* @returns {string} - full markdown content for COST_REPORT.md
|
|
464
|
-
*/
|
|
465
|
-
function buildCostReport(sprint, events, orphanSidecars, huskPrimaries) {
|
|
466
|
-
const REVIEW_PHASES = new Set(['review', 'review-plan', 'review-code', 'review-implementation']);
|
|
467
|
-
|
|
468
|
-
// Only events that have at least inputTokens present
|
|
469
|
-
const tokenEvents = events.filter(e => e.inputTokens !== undefined);
|
|
470
|
-
|
|
471
|
-
const lines = [
|
|
472
|
-
GENERATED,
|
|
473
|
-
'',
|
|
474
|
-
`# Cost Report — ${sprint.sprintId}`,
|
|
475
|
-
'',
|
|
476
|
-
`> Generated: ${new Date().toISOString().slice(0, 10)}`,
|
|
477
|
-
`> Sprint: ${sprint.title || sprint.sprintId}`,
|
|
478
|
-
'',
|
|
479
|
-
];
|
|
480
|
-
|
|
481
|
-
// Tally total events and tokenSource counts over all primary events (including husks)
|
|
482
|
-
const totalEvents = events.length;
|
|
483
|
-
const tokenSourceCounts = { reported: 0, estimated: 0, missing: 0 };
|
|
484
|
-
for (const e of events) {
|
|
485
|
-
if (e.tokenSource === 'reported') tokenSourceCounts.reported++;
|
|
486
|
-
else if (e.tokenSource === 'estimated') tokenSourceCounts.estimated++;
|
|
487
|
-
else tokenSourceCounts.missing++;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (tokenEvents.length === 0) {
|
|
491
|
-
lines.push('_No token data available for this sprint._', '');
|
|
492
|
-
lines.push(buildIngestionQuality(orphanSidecars || [], huskPrimaries || [], [], [], totalEvents, tokenSourceCounts), '');
|
|
493
|
-
return lines.join('\n') + '\n';
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Collect no-task events and unmapped models for IQ section
|
|
497
|
-
const noTaskEvents = tokenEvents.filter(e => !e.taskId);
|
|
498
|
-
const unmappedModels = [];
|
|
499
|
-
const seenUnmapped = new Set();
|
|
500
|
-
for (const e of tokenEvents) {
|
|
501
|
-
if (e.model) {
|
|
502
|
-
const c = canonicalizeModel(e.model);
|
|
503
|
-
if (c === null && !seenUnmapped.has(e.model)) {
|
|
504
|
-
seenUnmapped.add(e.model);
|
|
505
|
-
unmappedModels.push(e.model);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
// --- Section 1: Per-task totals ---
|
|
511
|
-
lines.push('## Per-Task Totals', '');
|
|
512
|
-
{
|
|
513
|
-
const byTask = {};
|
|
514
|
-
for (const e of tokenEvents) {
|
|
515
|
-
const tid = e.taskId || 'no-task';
|
|
516
|
-
if (!byTask[tid]) byTask[tid] = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, estimatedCostUSD: 0, sources: new Set() };
|
|
517
|
-
const g = byTask[tid];
|
|
518
|
-
g.inputTokens += e.inputTokens || 0;
|
|
519
|
-
g.outputTokens += e.outputTokens || 0;
|
|
520
|
-
g.cacheReadTokens += e.cacheReadTokens || 0;
|
|
521
|
-
g.cacheWriteTokens+= e.cacheWriteTokens || 0;
|
|
522
|
-
g.estimatedCostUSD+= e.estimatedCostUSD || 0;
|
|
523
|
-
g.sources.add(e.tokenSource);
|
|
524
|
-
}
|
|
525
|
-
const rows = [['Task', 'Input Tokens', 'Output Tokens', 'Cache Read', 'Cache Write', 'Est. Cost USD', 'Source']];
|
|
526
|
-
for (const [tid, g] of Object.entries(byTask).sort(([a], [b]) => a.localeCompare(b))) {
|
|
527
|
-
rows.push([
|
|
528
|
-
tid,
|
|
529
|
-
fmtTokens(g.inputTokens),
|
|
530
|
-
fmtTokens(g.outputTokens),
|
|
531
|
-
fmtTokens(g.cacheReadTokens),
|
|
532
|
-
fmtTokens(g.cacheWriteTokens),
|
|
533
|
-
fmtCost(g.estimatedCostUSD),
|
|
534
|
-
sourceLabel(g.sources),
|
|
535
|
-
]);
|
|
536
|
-
}
|
|
537
|
-
lines.push(padTable(rows), '');
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
// --- Section 2: Per-role breakdown ---
|
|
541
|
-
lines.push('## Per-Role Breakdown', '');
|
|
542
|
-
{
|
|
543
|
-
const byRole = {};
|
|
544
|
-
for (const e of tokenEvents) {
|
|
545
|
-
const role = e.role || '(unknown)';
|
|
546
|
-
if (!byRole[role]) byRole[role] = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, estimatedCostUSD: 0 };
|
|
547
|
-
const g = byRole[role];
|
|
548
|
-
g.inputTokens += e.inputTokens || 0;
|
|
549
|
-
g.outputTokens += e.outputTokens || 0;
|
|
550
|
-
g.cacheReadTokens += e.cacheReadTokens || 0;
|
|
551
|
-
g.cacheWriteTokens+= e.cacheWriteTokens || 0;
|
|
552
|
-
g.estimatedCostUSD+= e.estimatedCostUSD || 0;
|
|
553
|
-
}
|
|
554
|
-
const rows = [['Role', 'Input Tokens', 'Output Tokens', 'Cache Read', 'Cache Write', 'Est. Cost USD']];
|
|
555
|
-
for (const [role, g] of Object.entries(byRole).sort(([a], [b]) => a.localeCompare(b))) {
|
|
556
|
-
rows.push([
|
|
557
|
-
role,
|
|
558
|
-
fmtTokens(g.inputTokens),
|
|
559
|
-
fmtTokens(g.outputTokens),
|
|
560
|
-
fmtTokens(g.cacheReadTokens),
|
|
561
|
-
fmtTokens(g.cacheWriteTokens),
|
|
562
|
-
fmtCost(g.estimatedCostUSD),
|
|
563
|
-
]);
|
|
564
|
-
}
|
|
565
|
-
lines.push(padTable(rows), '');
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// --- Section 3: Revision waste ---
|
|
569
|
-
lines.push('## Revision Waste', '');
|
|
570
|
-
{
|
|
571
|
-
const revisionEvents = tokenEvents.filter(e => (e.iteration || 1) > 1 && REVIEW_PHASES.has(e.phase));
|
|
572
|
-
if (revisionEvents.length === 0) {
|
|
573
|
-
lines.push('_No revision waste in this sprint._', '');
|
|
574
|
-
} else {
|
|
575
|
-
const byTask = {};
|
|
576
|
-
for (const e of revisionEvents) {
|
|
577
|
-
const tid = e.taskId || 'no-task';
|
|
578
|
-
if (!byTask[tid]) byTask[tid] = { iterations: new Set(), inputTokens: 0, outputTokens: 0, estimatedCostUSD: 0 };
|
|
579
|
-
const g = byTask[tid];
|
|
580
|
-
g.iterations.add(e.iteration);
|
|
581
|
-
g.inputTokens += e.inputTokens || 0;
|
|
582
|
-
g.outputTokens += e.outputTokens || 0;
|
|
583
|
-
g.estimatedCostUSD+= e.estimatedCostUSD || 0;
|
|
584
|
-
}
|
|
585
|
-
const rows = [['Task', 'Revision Iterations', 'Input Tokens', 'Output Tokens', 'Est. Cost USD']];
|
|
586
|
-
for (const [tid, g] of Object.entries(byTask).sort(([a], [b]) => a.localeCompare(b))) {
|
|
587
|
-
rows.push([
|
|
588
|
-
tid,
|
|
589
|
-
g.iterations.size,
|
|
590
|
-
fmtTokens(g.inputTokens),
|
|
591
|
-
fmtTokens(g.outputTokens),
|
|
592
|
-
fmtCost(g.estimatedCostUSD),
|
|
593
|
-
]);
|
|
594
|
-
}
|
|
595
|
-
lines.push(padTable(rows), '');
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
// --- Section 4: Model split ---
|
|
600
|
-
lines.push('## Model Split', '');
|
|
601
|
-
{
|
|
602
|
-
const byModel = {};
|
|
603
|
-
for (const e of tokenEvents) {
|
|
604
|
-
const model = e.model || '(unknown)';
|
|
605
|
-
if (!byModel[model]) byModel[model] = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, estimatedCostUSD: 0 };
|
|
606
|
-
const g = byModel[model];
|
|
607
|
-
g.inputTokens += e.inputTokens || 0;
|
|
608
|
-
g.outputTokens += e.outputTokens || 0;
|
|
609
|
-
g.cacheReadTokens += e.cacheReadTokens || 0;
|
|
610
|
-
g.cacheWriteTokens+= e.cacheWriteTokens || 0;
|
|
611
|
-
g.estimatedCostUSD+= e.estimatedCostUSD || 0;
|
|
612
|
-
}
|
|
613
|
-
const rows = [['Model', 'Input Tokens', 'Output Tokens', 'Cache Read', 'Cache Write', 'Est. Cost USD']];
|
|
614
|
-
for (const [model, g] of Object.entries(byModel).sort(([a], [b]) => a.localeCompare(b))) {
|
|
615
|
-
rows.push([
|
|
616
|
-
model,
|
|
617
|
-
fmtTokens(g.inputTokens),
|
|
618
|
-
fmtTokens(g.outputTokens),
|
|
619
|
-
fmtTokens(g.cacheReadTokens),
|
|
620
|
-
fmtTokens(g.cacheWriteTokens),
|
|
621
|
-
fmtCost(g.estimatedCostUSD),
|
|
622
|
-
]);
|
|
623
|
-
}
|
|
624
|
-
lines.push(padTable(rows), '');
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// --- Section 5: Ingestion Quality ---
|
|
628
|
-
lines.push(buildIngestionQuality(orphanSidecars || [], huskPrimaries || [], noTaskEvents, unmappedModels, totalEvents, tokenSourceCounts), '');
|
|
629
|
-
|
|
630
|
-
return lines.join('\n') + '\n';
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
module.exports = { statusBadge, padTable, fmtTokens, fmtCost, sourceLabel, GENERATED, buildSprintIndex, buildTaskIndex, buildBugIndex, resolveTaskDir, isBugId, mergeSidecarEvents, buildIngestionQuality, buildCostReport };
|
|
634
|
-
|
|
635
|
-
// --- CLI ---
|
|
636
|
-
if (require.main === module) {
|
|
637
|
-
|
|
638
|
-
const DRY_RUN = process.argv.includes('--dry-run');
|
|
639
|
-
const SPRINT_ARG = process.argv.slice(2).find(a => !a.startsWith('--'));
|
|
640
|
-
|
|
641
|
-
// Bug IDs are first-class arguments — auto-enable purge when a bug ID is passed.
|
|
642
|
-
// This makes `collate.cjs HELLO-B02` work identically to `collate.cjs HELLO-B02 --purge-events`.
|
|
643
|
-
const IS_BUG_ARG = isBugId(SPRINT_ARG);
|
|
644
|
-
const PURGE_EVENTS = process.argv.includes('--purge-events') || IS_BUG_ARG;
|
|
645
|
-
|
|
646
|
-
if (PURGE_EVENTS && !SPRINT_ARG) {
|
|
647
|
-
console.error('Error: --purge-events requires a sprint or bug ID argument');
|
|
648
|
-
process.exit(1);
|
|
649
|
-
}
|
|
650
|
-
const cwd = process.cwd();
|
|
651
|
-
|
|
652
|
-
function writeFile(filePath, content) {
|
|
653
|
-
if (DRY_RUN) { console.log(`[dry-run] would write: ${path.relative(cwd, filePath)}`); return; }
|
|
654
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
655
|
-
fs.writeFileSync(filePath, content, 'utf8');
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// Resolve a directory name under `base` by trying candidates in order.
|
|
659
|
-
// Returns the first candidate whose directory exists. If none exist,
|
|
660
|
-
// attempts a numeric glob: finds the first alphabetically-sorted dir
|
|
661
|
-
// in `base` whose first integer matches the first integer in the last
|
|
662
|
-
// candidate. Falls back to the last candidate if no match is found.
|
|
663
|
-
function resolveDir(base, ...candidates) {
|
|
664
|
-
for (const c of candidates) {
|
|
665
|
-
if (fs.existsSync(path.join(base, c))) return c;
|
|
666
|
-
}
|
|
667
|
-
// Numeric glob fallback
|
|
668
|
-
const last = candidates[candidates.length - 1];
|
|
669
|
-
const numMatch = last.match(/\d+/);
|
|
670
|
-
if (numMatch && fs.existsSync(base)) {
|
|
671
|
-
const target = parseInt(numMatch[0], 10);
|
|
672
|
-
const dirs = fs.readdirSync(base).sort();
|
|
673
|
-
for (const d of dirs) {
|
|
674
|
-
const m = d.match(/\d+/);
|
|
675
|
-
if (m && parseInt(m[0], 10) === target) return d;
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
return last;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// --- Load store ---
|
|
682
|
-
function readConfig() {
|
|
683
|
-
const p = path.join(cwd, '.forge', 'config.json');
|
|
684
|
-
if (!fs.existsSync(p)) { console.error('Error: .forge/config.json not found'); process.exit(1); }
|
|
685
|
-
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (e) {
|
|
686
|
-
console.error(`Error: .forge/config.json is not valid JSON: ${e.message}`); process.exit(1);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const config = readConfig();
|
|
691
|
-
const storePath = config.paths?.store || '.forge/store';
|
|
692
|
-
const engPath = config.paths?.engineering || 'engineering';
|
|
693
|
-
const projectName = config.project?.name || config.project?.prefix || 'Project';
|
|
694
|
-
|
|
695
|
-
const storeRoot = path.join(cwd, storePath);
|
|
696
|
-
const engRoot = path.join(cwd, engPath);
|
|
697
|
-
|
|
698
|
-
const store = _getStore();
|
|
699
|
-
const allSprints = store.listSprints()
|
|
700
|
-
.sort((a, b) => a.sprintId.localeCompare(b.sprintId));
|
|
701
|
-
const allTasks = store.listTasks();
|
|
702
|
-
const allBugs = store.listBugs();
|
|
703
|
-
const allFeatures= store.listFeatures()
|
|
704
|
-
.sort((a, b) => (a.id || '').localeCompare(b.id || ''));
|
|
705
|
-
|
|
706
|
-
// Group tasks by sprint
|
|
707
|
-
const tasksBySprint = {};
|
|
708
|
-
for (const t of allTasks) {
|
|
709
|
-
if (!tasksBySprint[t.sprintId]) tasksBySprint[t.sprintId] = [];
|
|
710
|
-
tasksBySprint[t.sprintId].push(t);
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// Sprints to process
|
|
714
|
-
const targetSprints = SPRINT_ARG
|
|
715
|
-
? allSprints.filter(s => s.sprintId === SPRINT_ARG || s.sprintId.endsWith(`-${SPRINT_ARG}`))
|
|
716
|
-
: allSprints;
|
|
717
|
-
|
|
718
|
-
if (SPRINT_ARG && targetSprints.length === 0) {
|
|
719
|
-
if (IS_BUG_ARG || PURGE_EVENTS) {
|
|
720
|
-
// Bug ID or non-sprint entity with --purge-events — skip sprint processing,
|
|
721
|
-
// fall through to bug INDEX generation and purge step.
|
|
722
|
-
} else {
|
|
723
|
-
console.error(`Error: '${SPRINT_ARG}' not found as a sprint or bug in store`);
|
|
724
|
-
process.exit(1);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
// --- Generate MASTER_INDEX.md ---
|
|
729
|
-
{
|
|
730
|
-
const masterPath = path.join(engRoot, 'MASTER_INDEX.md');
|
|
731
|
-
let preserved = {};
|
|
732
|
-
|
|
733
|
-
// Extract preserved sections (## headings not in GENERATED marker block)
|
|
734
|
-
if (fs.existsSync(masterPath)) {
|
|
735
|
-
const existing = fs.readFileSync(masterPath, 'utf8');
|
|
736
|
-
const isGenerated = existing.trimStart().startsWith(GENERATED.trim().slice(0, 10));
|
|
737
|
-
if (!isGenerated) {
|
|
738
|
-
// Preserve all sections from manually written file
|
|
739
|
-
const sections = existing.split(/^(?=## )/m);
|
|
740
|
-
for (const s of sections) {
|
|
741
|
-
const m = s.match(/^## (.+)/);
|
|
742
|
-
if (m) preserved[m[1].trim()] = s;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const lines = [GENERATED, '', `# ${projectName} — Master Index`, '',
|
|
748
|
-
`> Generated: ${new Date().toISOString().slice(0, 10)}`, ''];
|
|
749
|
-
|
|
750
|
-
// Feature Registry
|
|
751
|
-
lines.push('## Feature Registry', '');
|
|
752
|
-
if (allFeatures.length > 0) {
|
|
753
|
-
lines.push(`[Browse all ${allFeatures.length} features ↗](features/INDEX.md)`, '');
|
|
754
|
-
} else {
|
|
755
|
-
lines.push(`_[No features yet ↗](features/INDEX.md)_`, '');
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
// Sprint registry
|
|
759
|
-
lines.push('## Sprint Registry', '');
|
|
760
|
-
if (allSprints.length > 0) {
|
|
761
|
-
const rows = [['Sprint', 'Title', 'Status', 'Tasks']];
|
|
762
|
-
for (const s of [...allSprints].reverse()) {
|
|
763
|
-
const tasks = tasksBySprint[s.sprintId] || [];
|
|
764
|
-
const completed = tasks.filter(t => t.status === 'committed').length;
|
|
765
|
-
rows.push([
|
|
766
|
-
s.sprintId, s.title || '—', statusBadge(s.status),
|
|
767
|
-
`${completed}/${tasks.length}`,
|
|
768
|
-
]);
|
|
769
|
-
}
|
|
770
|
-
lines.push(padTable(rows), '');
|
|
771
|
-
} else {
|
|
772
|
-
lines.push('_No sprints yet._', '');
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// Task registry — most recent sprint first
|
|
776
|
-
lines.push('## Task Registry', '');
|
|
777
|
-
for (const sprint of [...allSprints].reverse()) {
|
|
778
|
-
const tasks = (tasksBySprint[sprint.sprintId] || []).sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
779
|
-
if (tasks.length === 0) continue;
|
|
780
|
-
lines.push(`### ${sprint.sprintId}`, '');
|
|
781
|
-
const rows = [['Task', 'Title', 'Status', 'Estimate']];
|
|
782
|
-
const sprintDir = sprint.path
|
|
783
|
-
? path.basename(sprint.path.replace(/\/$/, ''))
|
|
784
|
-
: resolveDir(path.join(engRoot, 'sprints'), sprint.sprintId, sprint.sprintId.split('-').pop());
|
|
785
|
-
for (const t of tasks) {
|
|
786
|
-
// Derive task link from t.path if available (most reliable — already has correct dir names).
|
|
787
|
-
// Fall back to filesystem detection against full task ID vs. trailing segment.
|
|
788
|
-
let taskLink;
|
|
789
|
-
if (t.path) {
|
|
790
|
-
// If path is under the engineering root, it IS the task directory — link directly.
|
|
791
|
-
// Otherwise (e.g. forge/tools/seed-store.cjs), it's a plugin source reference;
|
|
792
|
-
// strip the filename to get the containing directory.
|
|
793
|
-
const normalizedPath = t.path.replace(/\\/g, '/').replace(/\/$/, '');
|
|
794
|
-
const normalizedEngPath = engPath.replace(/\\/g, '/').replace(/\/$/, '');
|
|
795
|
-
if (normalizedPath.startsWith(normalizedEngPath + '/')) {
|
|
796
|
-
const rel = path.relative(engPath, t.path).replace(/\\/g, '/');
|
|
797
|
-
taskLink = rel + '/INDEX.md';
|
|
798
|
-
} else {
|
|
799
|
-
const rel = path.relative(engPath, t.path).replace(/\\/g, '/');
|
|
800
|
-
taskLink = path.dirname(rel).replace(/\\/g, '/') + '/INDEX.md';
|
|
801
|
-
}
|
|
802
|
-
} else {
|
|
803
|
-
const taskDir = resolveDir(path.join(engRoot, 'sprints', sprintDir), t.taskId, t.taskId.split('-').pop());
|
|
804
|
-
taskLink = `sprints/${sprintDir}/${taskDir}/INDEX.md`;
|
|
805
|
-
}
|
|
806
|
-
rows.push([
|
|
807
|
-
`[${t.taskId}](${taskLink})`,
|
|
808
|
-
t.title || '—', statusBadge(t.status), t.estimate || '—',
|
|
809
|
-
]);
|
|
810
|
-
}
|
|
811
|
-
lines.push(padTable(rows), '');
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Bug registry
|
|
815
|
-
if (allBugs.length > 0) {
|
|
816
|
-
lines.push('## Bug Registry', '');
|
|
817
|
-
const open = allBugs.filter(b => !['fixed', 'verified'].includes(b.status));
|
|
818
|
-
const closed = allBugs.filter(b => ['fixed', 'verified'].includes(b.status));
|
|
819
|
-
const rows = [['Bug', 'Title', 'Severity', 'Status']];
|
|
820
|
-
for (const b of [...open, ...closed]) {
|
|
821
|
-
const bugDir = resolveDir(path.join(engRoot, 'bugs'), b.bugId, b.bugId.split('-').pop());
|
|
822
|
-
rows.push([
|
|
823
|
-
`[${b.bugId}](bugs/${bugDir}/INDEX.md)`,
|
|
824
|
-
b.title || '—', b.severity || '—', statusBadge(b.status),
|
|
825
|
-
]);
|
|
826
|
-
}
|
|
827
|
-
lines.push(padTable(rows), '');
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// Re-append preserved sections not already present
|
|
831
|
-
for (const [heading, content] of Object.entries(preserved)) {
|
|
832
|
-
if (!['Feature Registry', 'Sprint Registry', 'Task Registry', 'Bug Registry'].includes(heading)) {
|
|
833
|
-
lines.push(content);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
writeFile(masterPath, lines.join('\n') + '\n');
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// --- Generate features/INDEX.md and per-feature pages ---
|
|
841
|
-
{
|
|
842
|
-
const featRoot = path.join(engRoot, 'features');
|
|
843
|
-
|
|
844
|
-
const featLines = [GENERATED, '', `# Feature Registry`, '',
|
|
845
|
-
`> Generated: ${new Date().toISOString().slice(0, 10)}`, ''];
|
|
846
|
-
|
|
847
|
-
if (allFeatures.length > 0) {
|
|
848
|
-
const rows = [['Feature ID', 'Title', 'Status', 'Sprints', 'Tasks']];
|
|
849
|
-
for (const f of allFeatures) {
|
|
850
|
-
if (!f.id) continue;
|
|
851
|
-
const sp = f.sprints || [];
|
|
852
|
-
const ts = f.tasks || [];
|
|
853
|
-
rows.push([
|
|
854
|
-
`[${f.id}](${f.id}.md)`,
|
|
855
|
-
f.title || '—',
|
|
856
|
-
statusBadge(f.status || 'draft'),
|
|
857
|
-
sp.length ? sp.join(', ') : '—',
|
|
858
|
-
ts.length ? ts.join(', ') : '—'
|
|
859
|
-
]);
|
|
860
|
-
|
|
861
|
-
// Per-feature page
|
|
862
|
-
const pfLines = [GENERATED, '', `# Feature: ${f.title || f.id}`, '',
|
|
863
|
-
`> Feature ID: ${f.id}`,
|
|
864
|
-
`> Status: ${statusBadge(f.status || 'draft')}`,
|
|
865
|
-
`> Created: ${f.created_at || '—'}`,
|
|
866
|
-
''];
|
|
867
|
-
|
|
868
|
-
if (f.description) pfLines.push('## Description', '', f.description, '');
|
|
869
|
-
|
|
870
|
-
if (f.requirements && f.requirements.length > 0) {
|
|
871
|
-
pfLines.push('## Requirements', '');
|
|
872
|
-
f.requirements.forEach(req => pfLines.push(`- ${req}`));
|
|
873
|
-
pfLines.push('');
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
if (sp.length > 0) {
|
|
877
|
-
pfLines.push('## Linked Sprints', '');
|
|
878
|
-
sp.forEach(s => pfLines.push(`- ${s}`));
|
|
879
|
-
pfLines.push('');
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
if (ts.length > 0) {
|
|
883
|
-
pfLines.push('## Linked Tasks', '');
|
|
884
|
-
ts.forEach(t => pfLines.push(`- ${t}`));
|
|
885
|
-
pfLines.push('');
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
writeFile(path.join(featRoot, `${f.id}.md`), pfLines.join('\n') + '\n');
|
|
889
|
-
}
|
|
890
|
-
featLines.push(padTable(rows), '');
|
|
891
|
-
} else {
|
|
892
|
-
featLines.push('_No features yet._', '');
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
writeFile(path.join(featRoot, 'INDEX.md'), featLines.join('\n') + '\n');
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// --- Generate Sprint INDEX.md and Task INDEX.md ---
|
|
899
|
-
function availableDocsIn(dir, knownDocs) {
|
|
900
|
-
if (!fs.existsSync(dir)) return [];
|
|
901
|
-
return knownDocs.map(d => d.file).filter(f => fs.existsSync(path.join(dir, f)));
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
let sprintIndexesWritten = 0;
|
|
905
|
-
let taskIndexesWritten = 0;
|
|
906
|
-
|
|
907
|
-
for (const sprint of targetSprints) {
|
|
908
|
-
let sprintDirName;
|
|
909
|
-
if (sprint.path) {
|
|
910
|
-
sprintDirName = path.basename(sprint.path.replace(/\/$/, ''));
|
|
911
|
-
} else {
|
|
912
|
-
sprintDirName = resolveDir(
|
|
913
|
-
path.join(engRoot, 'sprints'),
|
|
914
|
-
sprint.sprintId,
|
|
915
|
-
sprint.sprintId.split('-').pop()
|
|
916
|
-
);
|
|
917
|
-
}
|
|
918
|
-
const sprintDir = path.join(engRoot, 'sprints', sprintDirName);
|
|
919
|
-
|
|
920
|
-
// Resolve task directories for all tasks in this sprint
|
|
921
|
-
const rawSprintTasks = (tasksBySprint[sprint.sprintId] || []).sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
922
|
-
const sprintTasks = rawSprintTasks.map(t => {
|
|
923
|
-
const dirResult = resolveTaskDir(t, sprintDir, engPath);
|
|
924
|
-
return dirResult.ok ? Object.assign({}, t, { _taskDir: dirResult.value }) : t;
|
|
925
|
-
});
|
|
926
|
-
|
|
927
|
-
// Sprint INDEX.md — pass tasks with _taskDir so links resolve correctly
|
|
928
|
-
const sprintAvailDocs = availableDocsIn(sprintDir, SPRINT_DOCS);
|
|
929
|
-
writeFile(path.join(sprintDir, 'INDEX.md'), buildSprintIndex(sprint, sprintTasks, sprintAvailDocs));
|
|
930
|
-
sprintIndexesWritten++;
|
|
931
|
-
|
|
932
|
-
// Task INDEX.md files — generate for every task that has a KB directory
|
|
933
|
-
for (const task of sprintTasks) {
|
|
934
|
-
if (!task._taskDir) continue;
|
|
935
|
-
const taskDir = path.join(sprintDir, task._taskDir);
|
|
936
|
-
if (!fs.existsSync(taskDir)) continue;
|
|
937
|
-
const taskAvailDocs = availableDocsIn(taskDir, TASK_DOCS);
|
|
938
|
-
writeFile(path.join(taskDir, 'INDEX.md'), buildTaskIndex(task, taskAvailDocs));
|
|
939
|
-
taskIndexesWritten++;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
// --- Generate Bug INDEX.md files ---
|
|
944
|
-
let bugIndexesWritten = 0;
|
|
945
|
-
for (const bug of allBugs) {
|
|
946
|
-
const bugDirName = resolveDir(path.join(engRoot, 'bugs'), bug.bugId, bug.bugId.split('-').pop());
|
|
947
|
-
const bugDir = path.join(engRoot, 'bugs', bugDirName);
|
|
948
|
-
if (!fs.existsSync(bugDir)) continue;
|
|
949
|
-
const bugAvailDocs = availableDocsIn(bugDir, BUG_DOCS);
|
|
950
|
-
|
|
951
|
-
// When purging events for a bug, aggregate cost data from event files
|
|
952
|
-
// before they are deleted. The aggregated cost summary is embedded in
|
|
953
|
-
// the bug's INDEX.md so the information survives the purge.
|
|
954
|
-
let costTotals;
|
|
955
|
-
if (PURGE_EVENTS && SPRINT_ARG && SPRINT_ARG === bug.bugId) {
|
|
956
|
-
const { events } = loadSprintEvents(bug.bugId);
|
|
957
|
-
const tokenEvents = events.filter(e => e.inputTokens !== undefined);
|
|
958
|
-
if (tokenEvents.length > 0) {
|
|
959
|
-
const totals = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, estimatedCostUSD: 0, sources: new Set() };
|
|
960
|
-
for (const e of tokenEvents) {
|
|
961
|
-
totals.inputTokens += e.inputTokens || 0;
|
|
962
|
-
totals.outputTokens += e.outputTokens || 0;
|
|
963
|
-
totals.cacheReadTokens += e.cacheReadTokens || 0;
|
|
964
|
-
totals.cacheWriteTokens+= e.cacheWriteTokens || 0;
|
|
965
|
-
totals.estimatedCostUSD+= e.estimatedCostUSD || 0;
|
|
966
|
-
totals.sources.add(e.tokenSource);
|
|
967
|
-
}
|
|
968
|
-
costTotals = {
|
|
969
|
-
inputTokens: totals.inputTokens,
|
|
970
|
-
outputTokens: totals.outputTokens,
|
|
971
|
-
cacheReadTokens: totals.cacheReadTokens,
|
|
972
|
-
cacheWriteTokens: totals.cacheWriteTokens,
|
|
973
|
-
estimatedCostUSD: totals.estimatedCostUSD,
|
|
974
|
-
sourceLabel: sourceLabel(totals.sources),
|
|
975
|
-
};
|
|
976
|
-
}
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
writeFile(path.join(bugDir, 'INDEX.md'), buildBugIndex(bug, bugAvailDocs, costTotals));
|
|
980
|
-
bugIndexesWritten++;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
// --- Generate COST_REPORT.md per sprint ---
|
|
984
|
-
let costReportsWritten = 0;
|
|
985
|
-
|
|
986
|
-
for (const sprint of targetSprints) {
|
|
987
|
-
const { events, orphanSidecars, huskPrimaries } = loadSprintEvents(sprint.sprintId);
|
|
988
|
-
|
|
989
|
-
let sprintDirName;
|
|
990
|
-
if (sprint.path) {
|
|
991
|
-
sprintDirName = path.basename(sprint.path.replace(/\/$/, ''));
|
|
992
|
-
} else {
|
|
993
|
-
sprintDirName = resolveDir(
|
|
994
|
-
path.join(engRoot, 'sprints'),
|
|
995
|
-
sprint.sprintId,
|
|
996
|
-
sprint.sprintId.split('-').pop()
|
|
997
|
-
);
|
|
998
|
-
}
|
|
999
|
-
const reportPath = path.join(engRoot, 'sprints', sprintDirName, 'COST_REPORT.md');
|
|
1000
|
-
const reportContent = buildCostReport(sprint, events, orphanSidecars, huskPrimaries);
|
|
1001
|
-
|
|
1002
|
-
writeFile(reportPath, reportContent);
|
|
1003
|
-
costReportsWritten++;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
// --- Write COLLATION_STATE.json ---
|
|
1007
|
-
const stateData = {
|
|
1008
|
-
collatedAt: new Date().toISOString(),
|
|
1009
|
-
featureCount: allFeatures.length,
|
|
1010
|
-
sprintCount: targetSprints.length,
|
|
1011
|
-
taskCount: allTasks.filter(t => targetSprints.some(s => s.sprintId === t.sprintId)).length,
|
|
1012
|
-
bugCount: allBugs.length,
|
|
1013
|
-
};
|
|
1014
|
-
if (DRY_RUN) {
|
|
1015
|
-
console.log(`[dry-run] would write: ${path.relative(cwd, path.join(storeRoot, 'COLLATION_STATE.json'))}`);
|
|
1016
|
-
} else {
|
|
1017
|
-
_getStore().writeCollationState(stateData);
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const tag = DRY_RUN ? '[dry-run] ' : '';
|
|
1021
|
-
console.log(`${tag}Collated: ${targetSprints.length} sprint(s), ${allBugs.length} bug(s) → MASTER_INDEX.md updated, ${sprintIndexesWritten} sprint INDEX(es), ${taskIndexesWritten} task INDEX(es), ${bugIndexesWritten} bug INDEX(es), ${costReportsWritten} COST_REPORT(s) written`);
|
|
1022
|
-
|
|
1023
|
-
// --- Purge event directory if requested ---
|
|
1024
|
-
if (PURGE_EVENTS) {
|
|
1025
|
-
const relDir = path.relative(cwd, path.join(storeRoot, 'events', SPRINT_ARG));
|
|
1026
|
-
try {
|
|
1027
|
-
const result = _getStore().purgeEvents(SPRINT_ARG, { dryRun: DRY_RUN });
|
|
1028
|
-
if (result.fileCount === 0) {
|
|
1029
|
-
console.log(`${tag}Purge: no events directory found for '${SPRINT_ARG}' — nothing to delete`);
|
|
1030
|
-
} else if (DRY_RUN) {
|
|
1031
|
-
console.log(`[dry-run] would purge: ${relDir}/ (${result.fileCount} file(s))`);
|
|
1032
|
-
} else {
|
|
1033
|
-
console.log(`Purged: ${relDir}/ (${result.fileCount} event file(s) deleted)`);
|
|
1034
|
-
}
|
|
1035
|
-
} catch (err) {
|
|
1036
|
-
console.error(`Error: ${err.message}`);
|
|
1037
|
-
process.exit(1);
|
|
1038
|
-
}
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
} // end if (require.main === module)
|