bmad-module-skill-forge 0.10.0 → 1.0.0-rc.3
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/.claude-plugin/marketplace.json +56 -0
- package/.gitattributes +16 -0
- package/README.md +105 -38
- package/docs/404.md +13 -10
- package/docs/RELEASING.md +616 -0
- package/docs/STABILITY.md +123 -0
- package/docs/_data/pinned.yaml +98 -0
- package/docs/agents.md +16 -11
- package/docs/architecture.md +221 -0
- package/docs/bmad-synergy.md +11 -13
- package/docs/concepts.md +18 -108
- package/docs/examples.md +121 -80
- package/docs/getting-started.md +85 -147
- package/docs/how-it-works.md +25 -536
- package/docs/index.md +44 -27
- package/docs/skill-model.md +328 -0
- package/docs/troubleshooting.md +39 -0
- package/docs/verifying-a-skill.md +232 -0
- package/docs/why-skf.md +93 -0
- package/docs/workflows.md +119 -13
- package/package.json +30 -9
- package/release-audits/v1.0.0-launch-audit.md +207 -0
- package/src/README.md +23 -25
- package/src/forger/forge-tier.yaml +4 -2
- package/src/forger/preferences.yaml +7 -1
- package/src/knowledge/agentskills-spec.md +1 -1
- package/src/knowledge/architecture-verification.md +1 -1
- package/src/knowledge/ccc-bridge.md +16 -15
- package/src/knowledge/overview.md +11 -11
- package/src/knowledge/progressive-capability.md +3 -3
- package/src/knowledge/provenance-tracking.md +9 -3
- package/src/knowledge/qmd-registry.md +8 -8
- package/src/knowledge/skf-knowledge-index.csv +2 -2
- package/src/knowledge/skill-lifecycle.md +16 -1
- package/src/knowledge/split-body-strategy.md +1 -1
- package/src/knowledge/version-paths.md +17 -12
- package/src/module-help.csv +18 -17
- package/src/shared/health-check.md +372 -0
- package/src/shared/references/feasibility-report-schema.md +86 -0
- package/src/shared/references/headless-gate-convention.md +62 -0
- package/src/shared/references/output-contract-schema.md +35 -0
- package/src/shared/references/pipeline-contracts.md +102 -0
- package/src/shared/scripts/skf-atomic-write.py +370 -0
- package/src/shared/scripts/skf-manifest-ops.py +236 -0
- package/src/shared/scripts/skf-preflight.py +164 -0
- package/src/shared/scripts/skf-rebuild-managed-sections.py +201 -0
- package/src/shared/scripts/skf-severity-classify.py +163 -0
- package/src/shared/scripts/skf-skill-inventory.py +210 -0
- package/src/shared/scripts/skf-structural-diff.py +257 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +318 -0
- package/src/shared/scripts/skf-validate-output.py +247 -0
- package/src/skf-analyze-source/SKILL.md +57 -0
- package/src/skf-analyze-source/assets/skill-brief-schema.md +125 -0
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +124 -0
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01-init.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-01b-continue.md +4 -59
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-02-scan-project.md +7 -68
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-03-identify-units.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-04-map-and-detect.md +7 -68
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-05-recommend.md +6 -66
- package/src/{workflows/analyze-source → skf-analyze-source}/steps-c/step-06-generate-briefs.md +14 -71
- package/src/skf-analyze-source/steps-c/step-07-health-check.md +22 -0
- package/src/skf-audit-skill/SKILL.md +59 -0
- package/src/{workflows/audit-skill/data → skf-audit-skill/assets}/drift-report-template.md +7 -7
- package/src/{workflows/audit-skill/data → skf-audit-skill/references}/severity-rules.md +11 -11
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-01-init.md +24 -68
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-02-re-index.md +36 -71
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-03-structural-diff.md +21 -63
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-04-semantic-diff.md +6 -65
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-05-severity-classify.md +5 -63
- package/src/{workflows/audit-skill → skf-audit-skill}/steps-c/step-06-report.md +10 -64
- package/src/skf-audit-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-brief-skill/SKILL.md +53 -0
- package/src/{workflows/brief-skill/data → skf-brief-skill/assets}/scope-templates.md +23 -0
- package/src/skf-brief-skill/assets/skill-brief-schema.md +206 -0
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-01-gather-intent.md +6 -67
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-02-analyze-target.md +6 -66
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-03-scope-definition.md +12 -68
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-04-confirm-brief.md +5 -63
- package/src/{workflows/brief-skill → skf-brief-skill}/steps-c/step-05-write-brief.md +10 -70
- package/src/skf-brief-skill/steps-c/step-06-health-check.md +22 -0
- package/src/skf-create-skill/SKILL.md +63 -0
- package/src/{workflows/create-skill/data → skf-create-skill/assets}/compile-assembly-rules.md +60 -0
- package/src/{workflows/create-skill/data → skf-create-skill/assets}/skill-sections.md +92 -23
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +102 -0
- package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns-tracing.md +13 -13
- package/src/{workflows/create-skill/data → skf-create-skill/references}/extraction-patterns.md +8 -3
- package/src/skf-create-skill/references/source-resolution-protocols.md +239 -0
- package/src/{workflows/create-skill/data → skf-create-skill/references}/tier-degradation-rules.md +8 -7
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-01-load-brief.md +25 -63
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-02-ecosystem-check.md +8 -64
- package/src/skf-create-skill/steps-c/step-03-extract.md +379 -0
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-03d-component-extraction.md +18 -35
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-04-enrich.md +12 -69
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-05-compile.md +71 -75
- package/src/skf-create-skill/steps-c/step-06-validate.md +267 -0
- package/src/{workflows/create-skill → skf-create-skill}/steps-c/step-07-generate-artifacts.md +30 -74
- package/src/skf-create-skill/steps-c/step-08-report.md +145 -0
- package/src/skf-create-skill/steps-c/step-09-health-check.md +23 -0
- package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-02b-ccc-discover.md +8 -66
- package/src/skf-create-skill/steps-c/sub/step-03b-fetch-temporal.md +229 -0
- package/src/{workflows/create-skill/steps-c → skf-create-skill/steps-c/sub}/step-03c-fetch-docs.md +18 -42
- package/src/skf-create-stack-skill/SKILL.md +64 -0
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/assets}/stack-skill-template.md +2 -1
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/compose-mode-rules.md +19 -5
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/integration-patterns.md +1 -1
- package/src/{workflows/create-stack-skill/data → skf-create-stack-skill/references}/manifest-patterns.md +10 -10
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-01-init.md +14 -55
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-02-detect-manifests.md +29 -63
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-03-rank-and-confirm.md +9 -59
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-04-parallel-extract.md +29 -64
- package/src/skf-create-stack-skill/steps-c/step-05-detect-integrations.md +164 -0
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-06-compile-stack.md +7 -60
- package/src/skf-create-stack-skill/steps-c/step-07-generate-output.md +328 -0
- package/src/{workflows/create-stack-skill → skf-create-stack-skill}/steps-c/step-08-validate.md +17 -63
- package/src/skf-create-stack-skill/steps-c/step-09-report.md +128 -0
- package/src/skf-create-stack-skill/steps-c/step-10-health-check.md +22 -0
- package/src/skf-drop-skill/SKILL.md +57 -0
- package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-01-select.md +9 -69
- package/src/{workflows/drop-skill → skf-drop-skill}/steps-c/step-02-execute.md +30 -106
- package/src/skf-drop-skill/steps-c/step-03-report.md +83 -0
- package/src/skf-drop-skill/steps-c/step-04-health-check.md +22 -0
- package/src/skf-export-skill/SKILL.md +57 -0
- package/src/skf-export-skill/assets/managed-section-format.md +138 -0
- package/src/{workflows/export-skill/data → skf-export-skill/assets}/snippet-format.md +8 -12
- package/src/skf-export-skill/steps-c/step-01-load-skill.md +247 -0
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-02-package.md +5 -58
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-03-generate-snippet.md +15 -77
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-04-update-context.md +59 -133
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-05-token-report.md +4 -58
- package/src/{workflows/export-skill → skf-export-skill}/steps-c/step-06-summary.md +16 -68
- package/src/skf-export-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-forger/SKILL.md +110 -0
- package/src/skf-forger/bmad-skill-manifest.yaml +11 -0
- package/src/skf-quick-skill/SKILL.md +56 -0
- package/src/{workflows/quick-skill/data → skf-quick-skill/references}/registry-resolution.md +6 -6
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-01-resolve-target.md +9 -62
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-02-ecosystem-check.md +5 -59
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-03-quick-extract.md +18 -61
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-04-compile.md +6 -60
- package/src/{workflows/quick-skill → skf-quick-skill}/steps-c/step-05-validate.md +58 -78
- package/src/skf-quick-skill/steps-c/step-06-write.md +73 -0
- package/src/skf-quick-skill/steps-c/step-07-health-check.md +22 -0
- package/src/skf-refine-architecture/SKILL.md +57 -0
- package/src/{workflows/refine-architecture/data → skf-refine-architecture/references}/refinement-rules.md +16 -16
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-01-init.md +6 -63
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-02-gap-analysis.md +29 -71
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-03-issue-detection.md +5 -65
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-04-improvements.md +5 -64
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-05-compile.md +5 -56
- package/src/{workflows/refine-architecture → skf-refine-architecture}/steps-c/step-06-report.md +11 -64
- package/src/skf-refine-architecture/steps-c/step-07-health-check.md +22 -0
- package/src/skf-rename-skill/SKILL.md +58 -0
- package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-01-select.md +12 -73
- package/src/{workflows/rename-skill → skf-rename-skill}/steps-c/step-02-execute.md +32 -113
- package/src/skf-rename-skill/steps-c/step-03-report.md +83 -0
- package/src/skf-rename-skill/steps-c/step-04-health-check.md +22 -0
- package/src/skf-setup/SKILL.md +55 -0
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01-detect-and-tier.md +6 -62
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-01b-ccc-index.md +9 -65
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-02-write-config.md +15 -60
- package/src/{workflows/setup-forge → skf-setup}/steps-c/step-03-auto-index.md +7 -66
- package/src/skf-setup/steps-c/step-04-report.md +92 -0
- package/src/skf-setup/steps-c/step-05-health-check.md +22 -0
- package/src/skf-test-skill/SKILL.md +59 -0
- package/src/skf-test-skill/references/migration-section-rules.md +114 -0
- package/src/skf-test-skill/references/scoring-rules.md +207 -0
- package/src/skf-test-skill/references/source-access-protocol.md +109 -0
- package/src/skf-test-skill/scripts/compute-score.py +310 -0
- package/src/skf-test-skill/steps-c/step-01-init.md +238 -0
- package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-02-detect-mode.md +4 -62
- package/src/skf-test-skill/steps-c/step-03-coverage-check.md +347 -0
- package/src/skf-test-skill/steps-c/step-04-coherence-check.md +239 -0
- package/src/{workflows/test-skill → skf-test-skill}/steps-c/step-04b-external-validators.md +42 -77
- package/src/skf-test-skill/steps-c/step-05-score.md +266 -0
- package/src/skf-test-skill/steps-c/step-06-report.md +290 -0
- package/src/skf-test-skill/steps-c/step-07-health-check.md +25 -0
- package/src/skf-test-skill/templates/test-report-template.md +58 -0
- package/src/skf-update-skill/SKILL.md +58 -0
- package/src/{workflows/update-skill/data → skf-update-skill/references}/manual-section-rules.md +6 -6
- package/src/{workflows/update-skill/data → skf-update-skill/references}/merge-conflict-rules.md +5 -5
- package/src/skf-update-skill/references/remote-source-resolution.md +94 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-01-init.md +22 -69
- package/src/skf-update-skill/steps-c/step-02-detect-changes.md +255 -0
- package/src/skf-update-skill/steps-c/step-03-re-extract.md +336 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-04-merge.md +60 -72
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-05-validate.md +5 -62
- package/src/skf-update-skill/steps-c/step-06-write.md +278 -0
- package/src/{workflows/update-skill → skf-update-skill}/steps-c/step-07-report.md +11 -69
- package/src/skf-update-skill/steps-c/step-08-health-check.md +22 -0
- package/src/skf-verify-stack/SKILL.md +60 -0
- package/src/skf-verify-stack/assets/feasibility-report-template.md +76 -0
- package/src/{workflows/verify-stack/data → skf-verify-stack/references}/coverage-patterns.md +12 -4
- package/src/skf-verify-stack/references/integration-verification-rules.md +78 -0
- package/src/skf-verify-stack/steps-c/step-01-init.md +178 -0
- package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-02-coverage.md +27 -72
- package/src/skf-verify-stack/steps-c/step-03-integrations.md +179 -0
- package/src/{workflows/verify-stack → skf-verify-stack}/steps-c/step-04-requirements.md +14 -67
- package/src/skf-verify-stack/steps-c/step-05-synthesize.md +147 -0
- package/src/skf-verify-stack/steps-c/step-06-report.md +156 -0
- package/src/skf-verify-stack/steps-c/step-07-health-check.md +25 -0
- package/tools/cli/commands/install.js +4 -1
- package/tools/cli/commands/status.js +21 -23
- package/tools/cli/commands/uninstall.js +11 -19
- package/tools/cli/commands/update.js +6 -4
- package/tools/cli/lib/ide-skills.js +216 -0
- package/tools/cli/lib/installer.js +29 -52
- package/tools/cli/lib/manifest.js +11 -5
- package/tools/cli/lib/platform-codes.yaml +223 -0
- package/tools/cli/lib/ui.js +76 -47
- package/tools/validate-docs-drift.js +193 -0
- package/src/agents/forger.agent.yaml +0 -114
- package/src/workflows/README.md +0 -174
- package/src/workflows/analyze-source/data/skill-brief-schema.md +0 -125
- package/src/workflows/analyze-source/data/unit-detection-heuristics.md +0 -124
- package/src/workflows/analyze-source/workflow.md +0 -61
- package/src/workflows/audit-skill/workflow.md +0 -64
- package/src/workflows/brief-skill/data/skill-brief-schema.md +0 -159
- package/src/workflows/brief-skill/workflow.md +0 -58
- package/src/workflows/create-skill/data/source-resolution-protocols.md +0 -200
- package/src/workflows/create-skill/steps-c/step-03-extract.md +0 -301
- package/src/workflows/create-skill/steps-c/step-03b-fetch-temporal.md +0 -253
- package/src/workflows/create-skill/steps-c/step-06-validate.md +0 -254
- package/src/workflows/create-skill/steps-c/step-08-report.md +0 -179
- package/src/workflows/create-skill/workflow.md +0 -53
- package/src/workflows/create-stack-skill/steps-c/step-05-detect-integrations.md +0 -196
- package/src/workflows/create-stack-skill/steps-c/step-07-generate-output.md +0 -270
- package/src/workflows/create-stack-skill/steps-c/step-09-report.md +0 -150
- package/src/workflows/create-stack-skill/workflow.md +0 -59
- package/src/workflows/drop-skill/steps-c/step-03-report.md +0 -136
- package/src/workflows/drop-skill/workflow.md +0 -63
- package/src/workflows/export-skill/data/managed-section-format.md +0 -103
- package/src/workflows/export-skill/steps-c/step-01-load-skill.md +0 -237
- package/src/workflows/export-skill/workflow.md +0 -58
- package/src/workflows/quick-skill/steps-c/step-06-write.md +0 -178
- package/src/workflows/quick-skill/workflow.md +0 -58
- package/src/workflows/refine-architecture/workflow.md +0 -61
- package/src/workflows/rename-skill/steps-c/step-03-report.md +0 -133
- package/src/workflows/rename-skill/workflow.md +0 -64
- package/src/workflows/setup-forge/steps-c/step-04-report.md +0 -159
- package/src/workflows/setup-forge/workflow.md +0 -51
- package/src/workflows/shared/health-check.md +0 -231
- package/src/workflows/test-skill/data/scoring-rules.md +0 -123
- package/src/workflows/test-skill/data/source-access-protocol.md +0 -51
- package/src/workflows/test-skill/steps-c/step-01-init.md +0 -217
- package/src/workflows/test-skill/steps-c/step-03-coverage-check.md +0 -280
- package/src/workflows/test-skill/steps-c/step-04-coherence-check.md +0 -271
- package/src/workflows/test-skill/steps-c/step-05-score.md +0 -222
- package/src/workflows/test-skill/steps-c/step-06-report.md +0 -208
- package/src/workflows/test-skill/templates/test-report-template.md +0 -28
- package/src/workflows/test-skill/workflow.md +0 -57
- package/src/workflows/update-skill/data/remote-source-resolution.md +0 -57
- package/src/workflows/update-skill/steps-c/step-02-detect-changes.md +0 -221
- package/src/workflows/update-skill/steps-c/step-03-re-extract.md +0 -256
- package/src/workflows/update-skill/steps-c/step-06-write.md +0 -265
- package/src/workflows/update-skill/workflow.md +0 -56
- package/src/workflows/verify-stack/data/feasibility-report-template.md +0 -63
- package/src/workflows/verify-stack/data/integration-verification-rules.md +0 -73
- package/src/workflows/verify-stack/steps-c/step-01-init.md +0 -179
- package/src/workflows/verify-stack/steps-c/step-03-integrations.md +0 -195
- package/src/workflows/verify-stack/steps-c/step-05-synthesize.md +0 -191
- package/src/workflows/verify-stack/steps-c/step-06-report.md +0 -200
- package/src/workflows/verify-stack/workflow.md +0 -61
- package/tools/cli/lib/compiler.js +0 -306
- package/tools/cli/lib/ide-commands.js +0 -244
- /package/src/{workflows/analyze-source → skf-analyze-source}/templates/analysis-report-template.md +0 -0
- /package/src/{workflows/quick-skill/data → skf-quick-skill/assets}/skill-template.md +0 -0
- /package/src/{workflows/setup-forge/data → skf-setup/references}/tier-rules.md +0 -0
- /package/src/{workflows/test-skill/data → skf-test-skill/assets}/output-section-formats.md +0 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Manifest Ops — CRUD operations on the export manifest.
|
|
6
|
+
|
|
7
|
+
Reads, adds, updates, removes entries in .export-manifest.json.
|
|
8
|
+
Used by export-skill, drop-skill, and rename-skill.
|
|
9
|
+
|
|
10
|
+
CLI: python3 skf-manifest-ops.py <skills-folder> <command> [args]
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
read — Read entire manifest
|
|
14
|
+
get <skill-name> — Get a single skill entry
|
|
15
|
+
set <skill-name> <version> — Add/update skill with active version
|
|
16
|
+
remove <skill-name> — Remove skill from manifest
|
|
17
|
+
deprecate <skill-name> [ver] — Mark skill or version as deprecated
|
|
18
|
+
rename <old-name> <new-name> — Rename a skill entry
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import tempfile
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def read_manifest(manifest_path):
|
|
32
|
+
"""Read manifest file, returning (data, None) or (None, error).
|
|
33
|
+
|
|
34
|
+
Handles both v1 (flat list) and v2 (dict) manifest formats.
|
|
35
|
+
V1 manifests are migrated in-place to v2 on next write.
|
|
36
|
+
Legacy `platforms` field at the version level is renamed to `ides`.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
with open(manifest_path, encoding="utf-8") as f:
|
|
40
|
+
data = json.load(f)
|
|
41
|
+
if data.get("schema_version") != "2":
|
|
42
|
+
data = _migrate_v1_to_v2(data)
|
|
43
|
+
data = _normalize_platforms_to_ides(data)
|
|
44
|
+
return data, None
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
return {"schema_version": "2", "exports": {}, "updated_at": None}, None
|
|
47
|
+
except json.JSONDecodeError as e:
|
|
48
|
+
return None, f"Manifest JSON parse error: {e}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _migrate_v1_to_v2(data):
|
|
52
|
+
"""Migrate a v1 manifest to v2 format in memory."""
|
|
53
|
+
data["schema_version"] = "2"
|
|
54
|
+
for skill_name, entry in data.get("exports", {}).items():
|
|
55
|
+
versions = entry.get("versions", [])
|
|
56
|
+
if isinstance(versions, list):
|
|
57
|
+
active = entry.get("active_version", "")
|
|
58
|
+
deprecated = entry.get("deprecated", False)
|
|
59
|
+
new_versions = {}
|
|
60
|
+
for v in versions:
|
|
61
|
+
status = "active" if v == active and not deprecated else "archived"
|
|
62
|
+
new_versions[v] = {
|
|
63
|
+
"ides": [],
|
|
64
|
+
"last_exported": data.get("updated_at", ""),
|
|
65
|
+
"status": "deprecated" if deprecated and v == active else status,
|
|
66
|
+
}
|
|
67
|
+
entry["versions"] = new_versions
|
|
68
|
+
entry.pop("deprecated", None)
|
|
69
|
+
entry.pop("deprecated_versions", None)
|
|
70
|
+
return data
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _normalize_platforms_to_ides(data):
|
|
74
|
+
"""Rename legacy `platforms` key → `ides` on each version entry.
|
|
75
|
+
|
|
76
|
+
Pre-rename v2 manifests used `platforms` for the list of IDE identifiers
|
|
77
|
+
the version was exported to. The field was ambiguous (IDE name vs context
|
|
78
|
+
file vs skill root), so it was renamed to `ides` to match
|
|
79
|
+
`config.yaml.ides`. Existing manifests are upgraded on next read.
|
|
80
|
+
"""
|
|
81
|
+
for entry in data.get("exports", {}).values():
|
|
82
|
+
versions = entry.get("versions", {})
|
|
83
|
+
if not isinstance(versions, dict):
|
|
84
|
+
continue
|
|
85
|
+
for v_data in versions.values():
|
|
86
|
+
if not isinstance(v_data, dict):
|
|
87
|
+
continue
|
|
88
|
+
if "platforms" in v_data:
|
|
89
|
+
legacy = v_data.pop("platforms")
|
|
90
|
+
if "ides" not in v_data:
|
|
91
|
+
v_data["ides"] = legacy
|
|
92
|
+
return data
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def write_manifest(manifest_path, data):
|
|
96
|
+
"""Write manifest file atomically via write-to-temp-then-rename."""
|
|
97
|
+
data["updated_at"] = datetime.now(timezone.utc).isoformat()
|
|
98
|
+
manifest_path = Path(manifest_path)
|
|
99
|
+
tmp_fd, tmp_path = tempfile.mkstemp(dir=manifest_path.parent, suffix=".tmp")
|
|
100
|
+
try:
|
|
101
|
+
with os.fdopen(tmp_fd, "w", encoding="utf-8") as f:
|
|
102
|
+
json.dump(data, f, indent=2)
|
|
103
|
+
f.write("\n")
|
|
104
|
+
os.replace(tmp_path, manifest_path)
|
|
105
|
+
except Exception:
|
|
106
|
+
os.unlink(tmp_path)
|
|
107
|
+
raise
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def cmd_read(manifest_path):
|
|
111
|
+
data, err = read_manifest(manifest_path)
|
|
112
|
+
if err:
|
|
113
|
+
return {"status": "error", "error": err}
|
|
114
|
+
return {"status": "ok", "manifest": data}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def cmd_get(manifest_path, skill_name):
|
|
118
|
+
data, err = read_manifest(manifest_path)
|
|
119
|
+
if err:
|
|
120
|
+
return {"status": "error", "error": err}
|
|
121
|
+
exports = data.get("exports", {})
|
|
122
|
+
if skill_name not in exports:
|
|
123
|
+
return {"status": "not_found", "skill": skill_name, "available": sorted(exports.keys())}
|
|
124
|
+
return {"status": "ok", "skill": skill_name, "entry": exports[skill_name]}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def cmd_set(manifest_path, skill_name, version):
|
|
128
|
+
data, err = read_manifest(manifest_path)
|
|
129
|
+
if err:
|
|
130
|
+
return {"status": "error", "error": err}
|
|
131
|
+
exports = data.setdefault("exports", {})
|
|
132
|
+
existing = exports.get(skill_name, {})
|
|
133
|
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
134
|
+
|
|
135
|
+
# Preserve existing versions dict, archive the previously-active version
|
|
136
|
+
versions = existing.get("versions", {})
|
|
137
|
+
if isinstance(versions, list):
|
|
138
|
+
versions = {} # Safety: handle any residual v1 data
|
|
139
|
+
old_active = existing.get("active_version")
|
|
140
|
+
if old_active and old_active in versions and old_active != version:
|
|
141
|
+
versions[old_active]["status"] = "archived"
|
|
142
|
+
|
|
143
|
+
# Add or update the new active version
|
|
144
|
+
existing_version = versions.get(version, {})
|
|
145
|
+
versions[version] = {
|
|
146
|
+
"ides": existing_version.get("ides", existing_version.get("platforms", [])),
|
|
147
|
+
"last_exported": today,
|
|
148
|
+
"status": "active",
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
exports[skill_name] = {
|
|
152
|
+
"active_version": version,
|
|
153
|
+
"versions": versions,
|
|
154
|
+
}
|
|
155
|
+
write_manifest(manifest_path, data)
|
|
156
|
+
return {"status": "ok", "action": "set", "skill": skill_name, "version": version}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def cmd_remove(manifest_path, skill_name):
|
|
160
|
+
data, err = read_manifest(manifest_path)
|
|
161
|
+
if err:
|
|
162
|
+
return {"status": "error", "error": err}
|
|
163
|
+
exports = data.get("exports", {})
|
|
164
|
+
if skill_name not in exports:
|
|
165
|
+
return {"status": "not_found", "skill": skill_name}
|
|
166
|
+
removed = exports.pop(skill_name)
|
|
167
|
+
write_manifest(manifest_path, data)
|
|
168
|
+
return {"status": "ok", "action": "removed", "skill": skill_name, "removed_entry": removed}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def cmd_deprecate(manifest_path, skill_name, version=None):
|
|
172
|
+
data, err = read_manifest(manifest_path)
|
|
173
|
+
if err:
|
|
174
|
+
return {"status": "error", "error": err}
|
|
175
|
+
exports = data.get("exports", {})
|
|
176
|
+
if skill_name not in exports:
|
|
177
|
+
return {"status": "not_found", "skill": skill_name}
|
|
178
|
+
versions = exports[skill_name].get("versions", {})
|
|
179
|
+
if version:
|
|
180
|
+
if version not in versions:
|
|
181
|
+
return {"status": "error", "error": f"Version '{version}' not found for '{skill_name}'"}
|
|
182
|
+
versions[version]["status"] = "deprecated"
|
|
183
|
+
else:
|
|
184
|
+
# Deprecate all versions
|
|
185
|
+
for v in versions.values():
|
|
186
|
+
v["status"] = "deprecated"
|
|
187
|
+
write_manifest(manifest_path, data)
|
|
188
|
+
return {"status": "ok", "action": "deprecated", "skill": skill_name, "version": version}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def cmd_rename(manifest_path, old_name, new_name):
|
|
192
|
+
data, err = read_manifest(manifest_path)
|
|
193
|
+
if err:
|
|
194
|
+
return {"status": "error", "error": err}
|
|
195
|
+
exports = data.get("exports", {})
|
|
196
|
+
if old_name not in exports:
|
|
197
|
+
return {"status": "not_found", "skill": old_name}
|
|
198
|
+
if new_name in exports:
|
|
199
|
+
return {"status": "error", "error": f"Target name '{new_name}' already exists in manifest"}
|
|
200
|
+
exports[new_name] = exports.pop(old_name)
|
|
201
|
+
write_manifest(manifest_path, data)
|
|
202
|
+
return {"status": "ok", "action": "renamed", "from": old_name, "to": new_name}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main():
|
|
206
|
+
if len(sys.argv) < 3:
|
|
207
|
+
print("Usage: python3 skf-manifest-ops.py <skills-folder> <command> [args]", file=sys.stderr)
|
|
208
|
+
print("Commands: read, get <name>, set <name> <version>, remove <name>, deprecate <name> [version], rename <old> <new>", file=sys.stderr)
|
|
209
|
+
sys.exit(1)
|
|
210
|
+
|
|
211
|
+
skills_folder = Path(sys.argv[1])
|
|
212
|
+
command = sys.argv[2]
|
|
213
|
+
manifest_path = skills_folder / ".export-manifest.json"
|
|
214
|
+
|
|
215
|
+
if command == "read":
|
|
216
|
+
result = cmd_read(manifest_path)
|
|
217
|
+
elif command == "get" and len(sys.argv) >= 4:
|
|
218
|
+
result = cmd_get(manifest_path, sys.argv[3])
|
|
219
|
+
elif command == "set" and len(sys.argv) >= 5:
|
|
220
|
+
result = cmd_set(manifest_path, sys.argv[3], sys.argv[4])
|
|
221
|
+
elif command == "remove" and len(sys.argv) >= 4:
|
|
222
|
+
result = cmd_remove(manifest_path, sys.argv[3])
|
|
223
|
+
elif command == "deprecate" and len(sys.argv) >= 4:
|
|
224
|
+
ver = sys.argv[4] if len(sys.argv) >= 5 else None
|
|
225
|
+
result = cmd_deprecate(manifest_path, sys.argv[3], ver)
|
|
226
|
+
elif command == "rename" and len(sys.argv) >= 5:
|
|
227
|
+
result = cmd_rename(manifest_path, sys.argv[3], sys.argv[4])
|
|
228
|
+
else:
|
|
229
|
+
result = {"status": "error", "error": f"Unknown command or missing args: {command}"}
|
|
230
|
+
|
|
231
|
+
print(json.dumps(result, indent=2))
|
|
232
|
+
sys.exit(0 if result["status"] == "ok" else 1)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
if __name__ == "__main__":
|
|
236
|
+
main()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = ["pyyaml"]
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Preflight — Config and sidecar loader for all SKF skills.
|
|
6
|
+
|
|
7
|
+
Loads config.yaml, validates sidecar_path, loads preferences.yaml and
|
|
8
|
+
forge-tier.yaml, and outputs a unified JSON blob for step consumption.
|
|
9
|
+
|
|
10
|
+
CLI: python3 skf-preflight.py <project-root>
|
|
11
|
+
python3 skf-preflight.py <project-root> --config-path <alt-config>
|
|
12
|
+
|
|
13
|
+
Output: JSON to stdout with all resolved config variables and sidecar state.
|
|
14
|
+
Exit 0 on success, exit 1 on hard-halt conditions (missing config, bad sidecar).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import yaml
|
|
25
|
+
except ImportError:
|
|
26
|
+
print(
|
|
27
|
+
json.dumps({"error": "PyYAML not installed. Run: pip install pyyaml", "code": "MISSING_DEPENDENCY"}),
|
|
28
|
+
)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def load_yaml_file(path):
|
|
33
|
+
"""Load a YAML file, returning (data, None) or (None, error_string)."""
|
|
34
|
+
try:
|
|
35
|
+
with open(path, encoding="utf-8") as f:
|
|
36
|
+
data = yaml.safe_load(f)
|
|
37
|
+
return data or {}, None
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
return None, f"File not found: {path}"
|
|
40
|
+
except yaml.YAMLError as e:
|
|
41
|
+
return None, f"YAML parse error in {path}: {e}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def run_preflight(project_root, config_path=None):
|
|
45
|
+
"""Run preflight checks and return a result dict."""
|
|
46
|
+
project_root = Path(project_root).resolve()
|
|
47
|
+
|
|
48
|
+
# 1. Load config.yaml
|
|
49
|
+
if config_path:
|
|
50
|
+
cfg_path = Path(config_path)
|
|
51
|
+
else:
|
|
52
|
+
cfg_path = project_root / "_bmad" / "skf" / "config.yaml"
|
|
53
|
+
|
|
54
|
+
config, err = load_yaml_file(cfg_path)
|
|
55
|
+
if err:
|
|
56
|
+
return {
|
|
57
|
+
"status": "hard-halt",
|
|
58
|
+
"error": f"Cannot initialize. SKF config not found at {cfg_path}. Run the skf-setup skill to initialize your forge environment.",
|
|
59
|
+
"code": "CONFIG_MISSING",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# 2. Resolve config variables
|
|
63
|
+
resolved = {
|
|
64
|
+
"project_root": str(project_root),
|
|
65
|
+
"config_path": str(cfg_path),
|
|
66
|
+
"project_name": config.get("project_name", ""),
|
|
67
|
+
"output_folder": config.get("output_folder", ""),
|
|
68
|
+
"user_name": config.get("user_name", ""),
|
|
69
|
+
"communication_language": config.get("communication_language", "English"),
|
|
70
|
+
"document_output_language": config.get("document_output_language", "English"),
|
|
71
|
+
"sidecar_path": config.get("sidecar_path", ""),
|
|
72
|
+
"skills_output_folder": config.get("skills_output_folder", ""),
|
|
73
|
+
"forge_data_folder": config.get("forge_data_folder", ""),
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# 3. Validate sidecar_path
|
|
77
|
+
sidecar_path = resolved["sidecar_path"]
|
|
78
|
+
if not sidecar_path or sidecar_path == "{sidecar_path}":
|
|
79
|
+
return {
|
|
80
|
+
"status": "hard-halt",
|
|
81
|
+
"error": (
|
|
82
|
+
"Cannot initialize. sidecar_path is not defined in your installed config.yaml. "
|
|
83
|
+
f"Add sidecar_path: {project_root}/_bmad/_memory/forger-sidecar to your project "
|
|
84
|
+
"config.yaml and retry. This is a known installer issue with prompt: false config variables."
|
|
85
|
+
),
|
|
86
|
+
"code": "SIDECAR_UNDEFINED",
|
|
87
|
+
"config": resolved,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
sidecar_dir = Path(sidecar_path)
|
|
91
|
+
if not sidecar_dir.is_absolute():
|
|
92
|
+
sidecar_dir = project_root / sidecar_path
|
|
93
|
+
|
|
94
|
+
if not sidecar_dir.is_dir():
|
|
95
|
+
return {
|
|
96
|
+
"status": "hard-halt",
|
|
97
|
+
"error": f"Sidecar directory not found: {sidecar_dir}. Run skf-setup to initialize.",
|
|
98
|
+
"code": "SIDECAR_MISSING",
|
|
99
|
+
"config": resolved,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
resolved["sidecar_path_resolved"] = str(sidecar_dir)
|
|
103
|
+
|
|
104
|
+
# 4. Load sidecar files
|
|
105
|
+
sidecar = {}
|
|
106
|
+
|
|
107
|
+
prefs_path = sidecar_dir / "preferences.yaml"
|
|
108
|
+
prefs, prefs_err = load_yaml_file(prefs_path)
|
|
109
|
+
if prefs_err:
|
|
110
|
+
sidecar["preferences"] = None
|
|
111
|
+
sidecar["preferences_error"] = prefs_err
|
|
112
|
+
else:
|
|
113
|
+
sidecar["preferences"] = prefs
|
|
114
|
+
|
|
115
|
+
tier_path = sidecar_dir / "forge-tier.yaml"
|
|
116
|
+
tier, tier_err = load_yaml_file(tier_path)
|
|
117
|
+
if tier_err:
|
|
118
|
+
sidecar["forge_tier"] = None
|
|
119
|
+
sidecar["forge_tier_error"] = tier_err
|
|
120
|
+
else:
|
|
121
|
+
sidecar["forge_tier"] = tier
|
|
122
|
+
|
|
123
|
+
# 5. Derive convenience fields
|
|
124
|
+
tier_value = None
|
|
125
|
+
if tier:
|
|
126
|
+
tier_value = tier.get("tier")
|
|
127
|
+
|
|
128
|
+
compact_greeting = False
|
|
129
|
+
if prefs:
|
|
130
|
+
compact_greeting = prefs.get("compact_greeting", False) is True
|
|
131
|
+
|
|
132
|
+
tier_override = None
|
|
133
|
+
if prefs:
|
|
134
|
+
tier_override = prefs.get("tier_override")
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
"status": "ok",
|
|
138
|
+
"config": resolved,
|
|
139
|
+
"sidecar": sidecar,
|
|
140
|
+
"derived": {
|
|
141
|
+
"tier": tier_override or tier_value,
|
|
142
|
+
"tier_source": "override" if tier_override else ("detected" if tier_value else None),
|
|
143
|
+
"compact_greeting": compact_greeting,
|
|
144
|
+
"is_first_run": tier_value is None,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
if len(sys.argv) < 2:
|
|
151
|
+
print("Usage: python3 skf-preflight.py <project-root> [--config-path <path>]", file=sys.stderr)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
proj_root = sys.argv[1]
|
|
155
|
+
cfg_path = None
|
|
156
|
+
|
|
157
|
+
if "--config-path" in sys.argv:
|
|
158
|
+
idx = sys.argv.index("--config-path")
|
|
159
|
+
if idx + 1 < len(sys.argv):
|
|
160
|
+
cfg_path = sys.argv[idx + 1]
|
|
161
|
+
|
|
162
|
+
result = run_preflight(proj_root, cfg_path)
|
|
163
|
+
print(json.dumps(result, indent=2))
|
|
164
|
+
sys.exit(1 if result["status"] == "hard-halt" else 0)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.10"
|
|
3
|
+
# dependencies = []
|
|
4
|
+
# ///
|
|
5
|
+
"""SKF Rebuild Managed Sections — Context file marker surgery.
|
|
6
|
+
|
|
7
|
+
Reads a context file (CLAUDE.md/AGENTS.md/.cursorrules), finds the
|
|
8
|
+
<!-- SKF:BEGIN --> / <!-- SKF:END --> markers, and replaces the managed
|
|
9
|
+
section with new content. Preserves all content outside the markers.
|
|
10
|
+
|
|
11
|
+
CLI: python3 skf-rebuild-managed-sections.py <context-file> <action> [args]
|
|
12
|
+
|
|
13
|
+
Actions:
|
|
14
|
+
read — Extract current managed section content
|
|
15
|
+
replace — Replace managed section with content from stdin or --content
|
|
16
|
+
clear — Remove managed section entirely (markers + content)
|
|
17
|
+
insert — Insert managed section if not present (at end of file)
|
|
18
|
+
check — Check if managed section exists, report status
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from datetime import datetime, timezone
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
BEGIN_MARKER_PREFIX = "<!-- SKF:BEGIN"
|
|
30
|
+
END_MARKER = "<!-- SKF:END -->"
|
|
31
|
+
MARKER_PATTERN = re.compile(
|
|
32
|
+
r"(<!-- SKF:BEGIN[^>]*-->)(.*?)(<!-- SKF:END -->)",
|
|
33
|
+
re.DOTALL,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def read_context_file(file_path):
|
|
38
|
+
"""Read a context file. Returns (content, None) or (None, error)."""
|
|
39
|
+
try:
|
|
40
|
+
return Path(file_path).read_text(encoding="utf-8"), None
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
return None, f"File not found: {file_path}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def find_managed_section(content):
|
|
46
|
+
"""Find the managed section in content. Returns match or None."""
|
|
47
|
+
return MARKER_PATTERN.search(content)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_check(file_path):
|
|
51
|
+
"""Check if managed section exists."""
|
|
52
|
+
content, err = read_context_file(file_path)
|
|
53
|
+
if err:
|
|
54
|
+
return {"status": "error", "error": err}
|
|
55
|
+
|
|
56
|
+
has_begin = BEGIN_MARKER_PREFIX in content
|
|
57
|
+
has_end = END_MARKER in content
|
|
58
|
+
match = find_managed_section(content)
|
|
59
|
+
|
|
60
|
+
if match:
|
|
61
|
+
section_content = match.group(2)
|
|
62
|
+
return {
|
|
63
|
+
"status": "ok",
|
|
64
|
+
"has_managed_section": True,
|
|
65
|
+
"markers_valid": True,
|
|
66
|
+
"section_length": len(section_content.strip()),
|
|
67
|
+
"section_line_count": len(section_content.strip().split("\n")) if section_content.strip() else 0,
|
|
68
|
+
}
|
|
69
|
+
elif has_begin and not has_end:
|
|
70
|
+
return {
|
|
71
|
+
"status": "ok",
|
|
72
|
+
"has_managed_section": False,
|
|
73
|
+
"markers_valid": False,
|
|
74
|
+
"error_detail": "Found <!-- SKF:BEGIN but no matching <!-- SKF:END -->",
|
|
75
|
+
}
|
|
76
|
+
elif not has_begin and has_end:
|
|
77
|
+
return {
|
|
78
|
+
"status": "ok",
|
|
79
|
+
"has_managed_section": False,
|
|
80
|
+
"markers_valid": False,
|
|
81
|
+
"error_detail": "Found <!-- SKF:END --> but no matching <!-- SKF:BEGIN",
|
|
82
|
+
}
|
|
83
|
+
else:
|
|
84
|
+
return {"status": "ok", "has_managed_section": False, "markers_valid": True}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def cmd_read(file_path):
|
|
88
|
+
"""Extract the managed section content."""
|
|
89
|
+
content, err = read_context_file(file_path)
|
|
90
|
+
if err:
|
|
91
|
+
return {"status": "error", "error": err}
|
|
92
|
+
|
|
93
|
+
match = find_managed_section(content)
|
|
94
|
+
if not match:
|
|
95
|
+
return {"status": "ok", "has_managed_section": False, "content": None}
|
|
96
|
+
|
|
97
|
+
return {"status": "ok", "has_managed_section": True, "content": match.group(2)}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def cmd_replace(file_path, new_content):
|
|
101
|
+
"""Replace managed section content between markers."""
|
|
102
|
+
content, err = read_context_file(file_path)
|
|
103
|
+
if err:
|
|
104
|
+
return {"status": "error", "error": err}
|
|
105
|
+
|
|
106
|
+
match = find_managed_section(content)
|
|
107
|
+
if not match:
|
|
108
|
+
return {"status": "error", "error": "No managed section found. Use 'insert' to create one."}
|
|
109
|
+
|
|
110
|
+
# Replace content between markers with fresh timestamp
|
|
111
|
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
112
|
+
new_section = f"<!-- SKF:BEGIN updated:{today} -->\n{new_content}\n{END_MARKER}"
|
|
113
|
+
updated = content[: match.start()] + new_section + content[match.end() :]
|
|
114
|
+
|
|
115
|
+
Path(file_path).write_text(updated, encoding="utf-8")
|
|
116
|
+
return {"status": "ok", "action": "replaced", "bytes_written": len(updated)}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cmd_clear(file_path):
|
|
120
|
+
"""Remove managed section entirely (markers + content)."""
|
|
121
|
+
content, err = read_context_file(file_path)
|
|
122
|
+
if err:
|
|
123
|
+
return {"status": "error", "error": err}
|
|
124
|
+
|
|
125
|
+
match = find_managed_section(content)
|
|
126
|
+
if not match:
|
|
127
|
+
return {"status": "ok", "action": "no_change", "reason": "No managed section found"}
|
|
128
|
+
|
|
129
|
+
# Remove the entire marker block, plus any surrounding blank lines
|
|
130
|
+
before = content[: match.start()].rstrip("\n")
|
|
131
|
+
after = content[match.end() :].lstrip("\n")
|
|
132
|
+
updated = before + ("\n\n" if before and after else "") + after
|
|
133
|
+
|
|
134
|
+
Path(file_path).write_text(updated, encoding="utf-8")
|
|
135
|
+
return {"status": "ok", "action": "cleared", "bytes_written": len(updated)}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def cmd_insert(file_path, new_content):
|
|
139
|
+
"""Insert managed section at end of file if not present."""
|
|
140
|
+
content, err = read_context_file(file_path)
|
|
141
|
+
if err:
|
|
142
|
+
# File doesn't exist — create it
|
|
143
|
+
if "not found" in err.lower():
|
|
144
|
+
content = ""
|
|
145
|
+
else:
|
|
146
|
+
return {"status": "error", "error": err}
|
|
147
|
+
|
|
148
|
+
match = find_managed_section(content)
|
|
149
|
+
if match:
|
|
150
|
+
return {"status": "error", "error": "Managed section already exists. Use 'replace' instead."}
|
|
151
|
+
|
|
152
|
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
153
|
+
section = f"\n<!-- SKF:BEGIN updated:{today} -->\n{new_content}\n{END_MARKER}\n"
|
|
154
|
+
updated = content.rstrip("\n") + "\n" + section if content.strip() else section.lstrip("\n")
|
|
155
|
+
|
|
156
|
+
Path(file_path).write_text(updated, encoding="utf-8")
|
|
157
|
+
return {"status": "ok", "action": "inserted", "bytes_written": len(updated)}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
if len(sys.argv) < 3:
|
|
162
|
+
print("Usage: python3 skf-rebuild-managed-sections.py <context-file> <action> [--content <text>]", file=sys.stderr)
|
|
163
|
+
print("Actions: read, replace, clear, insert, check", file=sys.stderr)
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
|
|
166
|
+
file_path = sys.argv[1]
|
|
167
|
+
action = sys.argv[2]
|
|
168
|
+
|
|
169
|
+
content_arg = None
|
|
170
|
+
if "--content" in sys.argv:
|
|
171
|
+
idx = sys.argv.index("--content")
|
|
172
|
+
if idx + 1 < len(sys.argv):
|
|
173
|
+
content_arg = sys.argv[idx + 1]
|
|
174
|
+
elif action in ("replace", "insert") and not sys.stdin.isatty():
|
|
175
|
+
content_arg = sys.stdin.read()
|
|
176
|
+
|
|
177
|
+
if action == "check":
|
|
178
|
+
result = cmd_check(file_path)
|
|
179
|
+
elif action == "read":
|
|
180
|
+
result = cmd_read(file_path)
|
|
181
|
+
elif action == "replace":
|
|
182
|
+
if content_arg is None:
|
|
183
|
+
result = {"status": "error", "error": "replace requires --content or stdin"}
|
|
184
|
+
else:
|
|
185
|
+
result = cmd_replace(file_path, content_arg)
|
|
186
|
+
elif action == "clear":
|
|
187
|
+
result = cmd_clear(file_path)
|
|
188
|
+
elif action == "insert":
|
|
189
|
+
if content_arg is None:
|
|
190
|
+
result = {"status": "error", "error": "insert requires --content or stdin"}
|
|
191
|
+
else:
|
|
192
|
+
result = cmd_insert(file_path, content_arg)
|
|
193
|
+
else:
|
|
194
|
+
result = {"status": "error", "error": f"Unknown action: {action}"}
|
|
195
|
+
|
|
196
|
+
print(json.dumps(result, indent=2))
|
|
197
|
+
sys.exit(0 if result["status"] == "ok" else 1)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
main()
|