bmad-module-skill-forge 2.0.0 → 2.0.1
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 +1 -1
- package/.nvmrc +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/package.json +2 -2
- package/src/knowledge/skill-lifecycle.md +1 -1
- package/src/module-help.csv +2 -2
- package/src/shared/scripts/skf-comention-pairs.py +350 -0
- package/src/shared/scripts/skf-count-tokens.py +222 -0
- package/src/shared/scripts/skf-detect-docs.py +232 -1
- package/src/shared/scripts/skf-detect-language.py +77 -0
- package/src/shared/scripts/skf-detect-tools.py +105 -2
- package/src/shared/scripts/skf-enumerate-stack-skills.py +111 -2
- package/src/shared/scripts/skf-find-cycles.py +246 -0
- package/src/shared/scripts/skf-hash-content.py +487 -2
- package/src/shared/scripts/skf-manifest-ops.py +67 -1
- package/src/shared/scripts/skf-rebuild-managed-sections.py +180 -0
- package/src/shared/scripts/skf-recommend-scope-type.py +1 -2
- package/src/shared/scripts/skf-render-metadata-stats.py +499 -0
- package/src/shared/scripts/skf-rewrite-skill-name.py +308 -0
- package/src/shared/scripts/skf-shard-body.py +461 -0
- package/src/shared/scripts/skf-skill-inventory.py +139 -3
- package/src/shared/scripts/skf-structural-diff.py +307 -67
- package/src/shared/scripts/skf-validate-brief-schema.py +39 -1
- package/src/shared/scripts/skf-validate-feasibility-report.py +234 -0
- package/src/shared/scripts/skf-validate-frontmatter.py +17 -12
- package/src/shared/scripts/skf-validate-output.py +443 -10
- package/src/shared/scripts/skf-validate-pins.py +1 -1
- package/src/shared/scripts/skf-verify-no-trace.py +214 -0
- package/src/shared/scripts/skf-verify-provenance-completeness.py +439 -0
- package/src/skf-analyze-source/SKILL.md +17 -32
- package/src/skf-analyze-source/customize.toml +2 -0
- package/src/skf-analyze-source/references/auto-docs-only.md +116 -0
- package/src/skf-analyze-source/references/continue.md +8 -13
- package/src/skf-analyze-source/references/generate-briefs.md +28 -12
- package/src/skf-analyze-source/references/headless-contract.md +33 -0
- package/src/skf-analyze-source/references/health-check.md +1 -2
- package/src/skf-analyze-source/references/identify-units.md +25 -36
- package/src/skf-analyze-source/references/init.md +12 -25
- package/src/skf-analyze-source/references/map-and-detect.md +8 -20
- package/src/skf-analyze-source/references/recommend.md +4 -14
- package/src/skf-analyze-source/references/scan-project.md +3 -11
- package/src/skf-analyze-source/references/step-auto-scope.md +42 -180
- package/src/skf-analyze-source/references/unit-detection-heuristics.md +2 -2
- package/src/skf-audit-skill/SKILL.md +9 -6
- package/src/skf-audit-skill/customize.toml +2 -0
- package/src/skf-audit-skill/references/init.md +58 -32
- package/src/skf-audit-skill/references/re-index.md +15 -34
- package/src/skf-audit-skill/references/report.md +11 -58
- package/src/skf-audit-skill/references/semantic-diff.md +4 -22
- package/src/skf-audit-skill/references/severity-classify.md +53 -87
- package/src/skf-audit-skill/references/step-doc-drift.md +49 -19
- package/src/skf-audit-skill/references/structural-diff.md +43 -68
- package/src/skf-brief-skill/SKILL.md +7 -36
- package/src/skf-brief-skill/assets/scope-templates.md +1 -1
- package/src/skf-brief-skill/customize.toml +14 -0
- package/src/skf-brief-skill/references/analyze-target.md +6 -14
- package/src/skf-brief-skill/references/confirm-brief.md +2 -14
- package/src/skf-brief-skill/references/draft-checkpoint.md +8 -8
- package/src/skf-brief-skill/references/gather-intent.md +19 -22
- package/src/skf-brief-skill/references/headless-args.md +1 -1
- package/src/skf-brief-skill/references/health-check.md +1 -22
- package/src/skf-brief-skill/references/invocation-contract.md +37 -0
- package/src/skf-brief-skill/references/scope-definition.md +4 -14
- package/src/skf-brief-skill/references/step-auto-brief.md +1 -5
- package/src/skf-brief-skill/references/step-auto-validate.md +1 -5
- package/src/skf-brief-skill/references/version-resolution.md +1 -1
- package/src/skf-brief-skill/references/write-brief.md +17 -10
- package/src/skf-campaign/SKILL.md +15 -84
- package/src/skf-campaign/customize.toml +2 -0
- package/src/skf-campaign/manifest.yaml +4 -0
- package/src/skf-campaign/references/campaign-contracts.md +67 -0
- package/src/skf-campaign/references/campaign-directive-spec.md +14 -103
- package/src/skf-campaign/references/step-01-setup.md +4 -5
- package/src/skf-campaign/references/step-02-strategy.md +8 -3
- package/src/skf-campaign/references/step-03-pins.md +1 -1
- package/src/skf-campaign/references/step-04-provenance.md +1 -1
- package/src/skf-campaign/references/step-05-skill-loop.md +1 -1
- package/src/skf-campaign/references/step-06-batch.md +7 -6
- package/src/skf-campaign/references/step-11-maintenance.md +2 -2
- package/src/skf-campaign/references/step-resume.md +9 -8
- package/src/skf-campaign/scripts/campaign-deps.py +10 -1
- package/src/skf-campaign/scripts/campaign-render-batch.py +199 -0
- package/src/skf-campaign/scripts/campaign-status.py +162 -0
- package/src/skf-campaign/templates/campaign-brief-template.yaml +1 -1
- package/src/skf-create-skill/SKILL.md +14 -3
- package/src/skf-create-skill/assets/compile-assembly-rules.md +17 -38
- package/src/skf-create-skill/assets/skill-sections.md +9 -100
- package/src/skf-create-skill/assets/tessl-dismissal-rules.md +4 -13
- package/src/skf-create-skill/customize.toml +13 -0
- package/src/skf-create-skill/references/authoritative-files-protocol.md +7 -7
- package/src/skf-create-skill/references/compile.md +34 -47
- package/src/skf-create-skill/references/component-extraction.md +8 -35
- package/src/skf-create-skill/references/ecosystem-check.md +9 -25
- package/src/skf-create-skill/references/enrich.md +4 -17
- package/src/skf-create-skill/references/extract.md +14 -29
- package/src/skf-create-skill/references/extraction-patterns-tracing.md +1 -1
- package/src/skf-create-skill/references/extraction-patterns.md +7 -7
- package/src/skf-create-skill/references/generate-artifacts.md +9 -23
- package/src/skf-create-skill/references/health-check.md +1 -1
- package/src/skf-create-skill/references/load-brief.md +16 -18
- package/src/skf-create-skill/references/report.md +12 -15
- package/src/skf-create-skill/references/source-resolution-protocols.md +3 -3
- package/src/skf-create-skill/references/step-auto-shard.md +52 -25
- package/src/skf-create-skill/references/step-doc-rot.md +30 -12
- package/src/skf-create-skill/references/step-doc-sources.md +2 -2
- package/src/skf-create-skill/references/sub/ccc-discover.md +2 -13
- package/src/skf-create-skill/references/sub/fetch-docs.md +8 -50
- package/src/skf-create-skill/references/sub/fetch-temporal.md +5 -20
- package/src/skf-create-skill/references/tier-degradation-rules.md +6 -8
- package/src/skf-create-skill/references/validate.md +59 -28
- package/src/skf-create-skill/scripts/scan-doc-rot.py +245 -0
- package/src/skf-create-stack-skill/SKILL.md +15 -9
- package/src/skf-create-stack-skill/assets/provenance-map-schema.md +1 -1
- package/src/skf-create-stack-skill/customize.toml +10 -0
- package/src/skf-create-stack-skill/references/compile-stack.md +7 -8
- package/src/skf-create-stack-skill/references/compose-mode-rules.md +2 -13
- package/src/skf-create-stack-skill/references/detect-integrations.md +49 -54
- package/src/skf-create-stack-skill/references/detect-manifests.md +37 -30
- package/src/skf-create-stack-skill/references/generate-output.md +24 -75
- package/src/skf-create-stack-skill/references/init.md +9 -81
- package/src/skf-create-stack-skill/references/manifest-patterns.md +5 -35
- package/src/skf-create-stack-skill/references/parallel-extract.md +12 -27
- package/src/skf-create-stack-skill/references/rank-and-confirm.md +6 -6
- package/src/skf-create-stack-skill/references/report.md +27 -62
- package/src/skf-create-stack-skill/references/validate.md +35 -31
- package/src/skf-drop-skill/SKILL.md +20 -23
- package/src/skf-drop-skill/customize.toml +10 -0
- package/src/skf-drop-skill/references/execute.md +51 -31
- package/src/skf-drop-skill/references/headless-contract.md +18 -0
- package/src/skf-drop-skill/references/health-check.md +1 -13
- package/src/skf-drop-skill/references/report.md +42 -10
- package/src/skf-drop-skill/references/select.md +88 -48
- package/src/skf-drop-skill/scripts/dir-sizes.py +116 -0
- package/src/skf-export-skill/SKILL.md +17 -14
- package/src/skf-export-skill/assets/managed-section-format.md +2 -5
- package/src/skf-export-skill/customize.toml +10 -6
- package/src/skf-export-skill/references/generate-snippet.md +21 -35
- package/src/skf-export-skill/references/load-skill.md +44 -42
- package/src/skf-export-skill/references/multi-skill-mode.md +1 -1
- package/src/skf-export-skill/references/orphan-context-detection.md +11 -3
- package/src/skf-export-skill/references/package.md +27 -33
- package/src/skf-export-skill/references/preflight-snippet-root-probe.md +24 -5
- package/src/skf-export-skill/references/result-envelope.md +28 -0
- package/src/skf-export-skill/references/summary.md +11 -5
- package/src/skf-export-skill/references/token-report.md +42 -24
- package/src/skf-export-skill/references/update-context.md +32 -33
- package/src/skf-forger/SKILL.md +27 -39
- package/src/skf-forger/references/pipeline-mode.md +34 -0
- package/src/skf-forger/scripts/parse-pipeline.py +270 -0
- package/src/skf-quick-skill/SKILL.md +10 -60
- package/src/skf-quick-skill/customize.toml +9 -0
- package/src/skf-quick-skill/references/batch-mode.md +6 -4
- package/src/skf-quick-skill/references/compile.md +15 -52
- package/src/skf-quick-skill/references/ecosystem-check.md +5 -20
- package/src/skf-quick-skill/references/finalize.md +12 -6
- package/src/skf-quick-skill/references/halt-contract.md +54 -0
- package/src/skf-quick-skill/references/health-check.md +3 -4
- package/src/skf-quick-skill/references/quick-extract.md +7 -18
- package/src/skf-quick-skill/references/registry-resolution.md +1 -23
- package/src/skf-quick-skill/references/resolve-target.md +27 -34
- package/src/skf-quick-skill/references/write-and-validate.md +10 -25
- package/src/skf-refine-architecture/SKILL.md +14 -7
- package/src/skf-refine-architecture/customize.toml +9 -0
- package/src/skf-refine-architecture/references/compile.md +9 -7
- package/src/skf-refine-architecture/references/gap-analysis.md +21 -50
- package/src/skf-refine-architecture/references/health-check.md +1 -1
- package/src/skf-refine-architecture/references/improvements.md +9 -25
- package/src/skf-refine-architecture/references/init.md +9 -16
- package/src/skf-refine-architecture/references/issue-detection.md +10 -25
- package/src/skf-refine-architecture/references/refinement-rules.md +4 -16
- package/src/skf-refine-architecture/references/report.md +13 -10
- package/src/skf-rename-skill/SKILL.md +11 -21
- package/src/skf-rename-skill/customize.toml +10 -0
- package/src/skf-rename-skill/references/execute.md +107 -78
- package/src/skf-rename-skill/references/exit-codes.md +12 -0
- package/src/skf-rename-skill/references/report.md +16 -8
- package/src/skf-rename-skill/references/select.md +69 -42
- package/src/skf-rename-skill/scripts/skf-validate-rename-name.py +161 -0
- package/src/skf-setup/SKILL.md +20 -12
- package/src/skf-setup/customize.toml +10 -0
- package/src/skf-setup/references/auto-index.md +9 -10
- package/src/skf-setup/references/ccc-index.md +14 -15
- package/src/skf-setup/references/detect-and-tier.md +6 -8
- package/src/skf-setup/references/report.md +22 -31
- package/src/skf-setup/references/tier-rules.md +2 -32
- package/src/skf-setup/references/write-config.md +9 -10
- package/src/skf-test-skill/SKILL.md +10 -8
- package/src/skf-test-skill/customize.toml +2 -0
- package/src/skf-test-skill/references/coherence-check.md +19 -31
- package/src/skf-test-skill/references/coverage-check.md +109 -75
- package/src/skf-test-skill/references/detect-mode.md +10 -41
- package/src/skf-test-skill/references/external-validators.md +11 -19
- package/src/skf-test-skill/references/init.md +38 -20
- package/src/skf-test-skill/references/migration-section-rules.md +4 -4
- package/src/skf-test-skill/references/report.md +60 -40
- package/src/skf-test-skill/references/score.md +32 -64
- package/src/skf-test-skill/references/scoring-rules.md +6 -62
- package/src/skf-test-skill/references/source-access-protocol.md +6 -6
- package/src/skf-test-skill/references/step-hard-gate.md +1 -3
- package/src/skf-test-skill/scripts/aggregate-coherence.py +276 -0
- package/src/skf-test-skill/scripts/check-metadata-coherence.py +359 -0
- package/src/skf-test-skill/scripts/combine-external-scores.py +190 -0
- package/src/skf-test-skill/scripts/compute-score.py +131 -1
- package/src/skf-test-skill/scripts/reconcile-coverage.py +327 -0
- package/src/skf-test-skill/scripts/validate-inventory.py +251 -0
- package/src/skf-test-skill/scripts/verify-declared-numerator.py +192 -0
- package/src/skf-update-skill/SKILL.md +17 -5
- package/src/skf-update-skill/customize.toml +14 -0
- package/src/skf-update-skill/references/detect-changes.md +19 -37
- package/src/skf-update-skill/references/health-check.md +3 -9
- package/src/skf-update-skill/references/init.md +39 -32
- package/src/skf-update-skill/references/manual-section-rules.md +1 -2
- package/src/skf-update-skill/references/merge-conflict-rules.md +1 -52
- package/src/skf-update-skill/references/merge.md +13 -57
- package/src/skf-update-skill/references/re-extract.md +12 -24
- package/src/skf-update-skill/references/report.md +14 -7
- package/src/skf-update-skill/references/validate.md +32 -30
- package/src/skf-update-skill/references/write.md +86 -51
- package/src/skf-update-skill/scripts/skf-new-file-diff.py +167 -0
- package/src/skf-verify-stack/SKILL.md +11 -21
- package/src/skf-verify-stack/customize.toml +12 -3
- package/src/skf-verify-stack/references/coverage-patterns.md +1 -29
- package/src/skf-verify-stack/references/coverage.md +21 -10
- package/src/skf-verify-stack/references/exit-codes.md +14 -0
- package/src/skf-verify-stack/references/health-check.md +1 -1
- package/src/skf-verify-stack/references/init.md +20 -25
- package/src/skf-verify-stack/references/integration-verification-rules.md +10 -19
- package/src/skf-verify-stack/references/integrations.md +44 -49
- package/src/skf-verify-stack/references/report.md +28 -34
- package/src/skf-verify-stack/references/requirements.md +3 -3
- package/src/skf-verify-stack/references/synthesize.md +36 -39
- package/src/skf-verify-stack/scripts/skf-coverage-tally.py +196 -0
- package/src/skf-verify-stack/scripts/skf-report-delta.py +256 -0
- package/src/skf-verify-stack/scripts/skf-verdict-rollup.py +258 -0
- package/src/skf-rename-skill/references/rebuild-context.md +0 -110
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = []
|
|
5
|
+
# ///
|
|
6
|
+
"""SKF Shard Body — deterministic auto-shard of an oversized SKILL.md body.
|
|
7
|
+
|
|
8
|
+
Replaces the in-prompt line-counting / boundary-detection / size-sort /
|
|
9
|
+
file-write / blockquote-replacement surgery that `skf-create-skill`'s
|
|
10
|
+
`references/step-auto-shard.md` §1–§5 and `references/validate.md` §4
|
|
11
|
+
otherwise perform by hand (LLM line-counting is the least reliable
|
|
12
|
+
deterministic op — it silently ships an over-budget body or wrongly HALTs).
|
|
13
|
+
|
|
14
|
+
What it does (single positional invocation):
|
|
15
|
+
|
|
16
|
+
1. Split frontmatter (leading `---` … `---`) from body. Frontmatter is
|
|
17
|
+
NEVER modified.
|
|
18
|
+
2. Count body lines between the frontmatter close and EOF, excluding
|
|
19
|
+
trailing blank lines → `body_lines_before`.
|
|
20
|
+
3. If `body_lines_before` <= `--budget` (default 400): emit
|
|
21
|
+
`action: "skip"`, write nothing.
|
|
22
|
+
4. Otherwise enumerate Tier-2 sections — level-2 headings whose text
|
|
23
|
+
starts with `Full` (`## Full API Reference`, `## Full Type
|
|
24
|
+
Definitions`, …). Each section spans its heading through the line
|
|
25
|
+
before the next level-1/level-2 heading (or EOF).
|
|
26
|
+
5. Sort candidates by line count descending (largest first, stable) and
|
|
27
|
+
greedily extract the largest, re-counting after each, stopping the
|
|
28
|
+
moment the body fits under budget. If selective extraction of every
|
|
29
|
+
Tier-2 section still cannot fit, extract them all and report
|
|
30
|
+
`under_budget: false` (the caller then trims Tier-1 by editing
|
|
31
|
+
judgment — that fallback stays in the prompt).
|
|
32
|
+
6. Each extracted section is written to `references/{kebab(heading)}.md`
|
|
33
|
+
(heading preserved) via `skf-atomic-write.py`, and the section in
|
|
34
|
+
SKILL.md is replaced by a `> See [Title](references/…md)` blockquote.
|
|
35
|
+
The rewritten SKILL.md is itself written via `skf-atomic-write.py`.
|
|
36
|
+
7. Verify every Tier-1 heading that was inline before extraction is still
|
|
37
|
+
inline (`tier1_preserved`, with `tier1_missing[]`), and every emitted
|
|
38
|
+
blockquote resolves to a written reference file (`xref_ok`).
|
|
39
|
+
|
|
40
|
+
Deterministic: same input + budget → same JSON and same on-disk result.
|
|
41
|
+
|
|
42
|
+
Output JSON (stdout, or `-o`):
|
|
43
|
+
{
|
|
44
|
+
"action": "skip" | "shard",
|
|
45
|
+
"body_lines_before": <int>,
|
|
46
|
+
"body_lines_after": <int>,
|
|
47
|
+
"sections_extracted": [
|
|
48
|
+
{"heading": "Full API Reference",
|
|
49
|
+
"file": "references/full-api-reference.md",
|
|
50
|
+
"lines": <int>}
|
|
51
|
+
],
|
|
52
|
+
"tier1_preserved": <bool>,
|
|
53
|
+
"tier1_missing": ["Overview", ...],
|
|
54
|
+
"xref_ok": <bool>,
|
|
55
|
+
"under_budget": <bool>,
|
|
56
|
+
"dry_run": <bool>,
|
|
57
|
+
"budget": <int>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
Heading parsing reuses `skf-scan-skill-md-structure.py`'s `_HEADING_RE`
|
|
61
|
+
(single source of truth for what counts as a heading), imported from the
|
|
62
|
+
adjacent script; a local copy of the same pattern is the fallback if the
|
|
63
|
+
sibling is unavailable.
|
|
64
|
+
|
|
65
|
+
Exit codes:
|
|
66
|
+
0 — analysis succeeded (skip, shard, or a tier1-violation report the
|
|
67
|
+
caller HALTs on). Emitting a valid report is success; the caller
|
|
68
|
+
gates on `tier1_preserved` / `xref_ok`.
|
|
69
|
+
1 — user error (file not found, unreadable, bad frontmatter).
|
|
70
|
+
2 — operation failure (a reference/SKILL.md write could not complete).
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
from __future__ import annotations
|
|
74
|
+
|
|
75
|
+
import argparse
|
|
76
|
+
import importlib.util
|
|
77
|
+
import json
|
|
78
|
+
import re
|
|
79
|
+
import subprocess
|
|
80
|
+
import sys
|
|
81
|
+
from pathlib import Path
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# --------------------------------------------------------------------------
|
|
85
|
+
# Heading regex — reuse the sibling scanner's single source of truth
|
|
86
|
+
# --------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _load_heading_re() -> "re.Pattern[str]":
|
|
90
|
+
"""Import `_HEADING_RE` from the adjacent structure-scanner script.
|
|
91
|
+
|
|
92
|
+
Both scripts install into the same directory (dev `src/shared/scripts/`
|
|
93
|
+
and installed `_bmad/skf/shared/scripts/`), so the sibling is always
|
|
94
|
+
adjacent. Fall back to an identical literal if the import fails, so the
|
|
95
|
+
script still runs standalone (graceful degradation).
|
|
96
|
+
"""
|
|
97
|
+
sibling = Path(__file__).resolve().parent / "skf-scan-skill-md-structure.py"
|
|
98
|
+
try:
|
|
99
|
+
spec = importlib.util.spec_from_file_location(
|
|
100
|
+
"skf_scan_skill_md_structure", sibling
|
|
101
|
+
)
|
|
102
|
+
if spec is None or spec.loader is None:
|
|
103
|
+
raise ImportError("no import spec")
|
|
104
|
+
mod = importlib.util.module_from_spec(spec)
|
|
105
|
+
spec.loader.exec_module(mod)
|
|
106
|
+
return mod._HEADING_RE # type: ignore[attr-defined]
|
|
107
|
+
except Exception:
|
|
108
|
+
return re.compile(r"^\s*(#+)\s+(.*?)\s*$")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
_HEADING_RE = _load_heading_re()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# --------------------------------------------------------------------------
|
|
115
|
+
# Canonical Tier-1 headings (mirror step-auto-shard.md §3). Any that were
|
|
116
|
+
# inline before extraction must remain inline after.
|
|
117
|
+
# --------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
TIER1_HEADINGS: list[str] = [
|
|
121
|
+
"Overview",
|
|
122
|
+
"Quick Start",
|
|
123
|
+
"Common Workflows",
|
|
124
|
+
"Key API Summary",
|
|
125
|
+
"Component Catalog",
|
|
126
|
+
"Migration & Deprecation Warnings",
|
|
127
|
+
"Key Types",
|
|
128
|
+
"Architecture at a Glance",
|
|
129
|
+
"CLI",
|
|
130
|
+
"Scripts & Assets",
|
|
131
|
+
"Manual Sections",
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# --------------------------------------------------------------------------
|
|
136
|
+
# Parsing helpers
|
|
137
|
+
# --------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Section:
|
|
141
|
+
"""A Tier-2 (`## Full …`) section as a half-open line range [start, end)."""
|
|
142
|
+
|
|
143
|
+
__slots__ = ("start", "end", "heading", "kebab")
|
|
144
|
+
|
|
145
|
+
def __init__(self, start: int, end: int, heading: str) -> None:
|
|
146
|
+
self.start = start
|
|
147
|
+
self.end = end
|
|
148
|
+
self.heading = heading
|
|
149
|
+
self.kebab = kebab_case(heading)
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def lines(self) -> int:
|
|
153
|
+
return self.end - self.start
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def blockquote(self) -> str:
|
|
157
|
+
return f"> See [{self.heading}](references/{self.kebab}.md)"
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def ref_rel(self) -> str:
|
|
161
|
+
return f"references/{self.kebab}.md"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def kebab_case(text: str) -> str:
|
|
165
|
+
"""Lowercase, collapse non-alphanumeric runs to single hyphens, trim."""
|
|
166
|
+
return re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def split_frontmatter(text: str) -> tuple[list[str], list[str]]:
|
|
170
|
+
"""Return (frontmatter_lines, body_lines).
|
|
171
|
+
|
|
172
|
+
Frontmatter is a leading `---` … `---` block. `frontmatter_lines`
|
|
173
|
+
includes both fences. When no frontmatter is present, it is empty and
|
|
174
|
+
the whole file is the body.
|
|
175
|
+
"""
|
|
176
|
+
lines = text.splitlines()
|
|
177
|
+
if lines and lines[0].strip() == "---":
|
|
178
|
+
for i in range(1, len(lines)):
|
|
179
|
+
if lines[i].strip() == "---":
|
|
180
|
+
return lines[: i + 1], lines[i + 1 :]
|
|
181
|
+
return [], lines
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def count_body_lines(body: list[str]) -> int:
|
|
185
|
+
"""Number of body lines excluding trailing blank lines."""
|
|
186
|
+
end = len(body)
|
|
187
|
+
while end > 0 and body[end - 1].strip() == "":
|
|
188
|
+
end -= 1
|
|
189
|
+
return end
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _headings(body: list[str]) -> list[tuple[int, int, str]]:
|
|
193
|
+
"""Return [(line_index, level, heading_text)] for every heading line."""
|
|
194
|
+
out: list[tuple[int, int, str]] = []
|
|
195
|
+
for idx, line in enumerate(body):
|
|
196
|
+
m = _HEADING_RE.match(line)
|
|
197
|
+
if m:
|
|
198
|
+
out.append((idx, len(m.group(1)), m.group(2).strip()))
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def find_full_sections(body: list[str]) -> list[Section]:
|
|
203
|
+
"""Enumerate Tier-2 `## Full …` sections in document order.
|
|
204
|
+
|
|
205
|
+
A section spans its `## Full …` heading through the line before the next
|
|
206
|
+
level-1 or level-2 heading (or EOF), so nested `###` sub-headings stay
|
|
207
|
+
with their section.
|
|
208
|
+
"""
|
|
209
|
+
heads = _headings(body)
|
|
210
|
+
sections: list[Section] = []
|
|
211
|
+
for hi, (idx, level, htext) in enumerate(heads):
|
|
212
|
+
if level != 2:
|
|
213
|
+
continue
|
|
214
|
+
if not (htext == "Full" or htext.startswith("Full ")):
|
|
215
|
+
continue
|
|
216
|
+
end = len(body)
|
|
217
|
+
for nidx, nlevel, _ in heads[hi + 1 :]:
|
|
218
|
+
if nlevel <= 2:
|
|
219
|
+
end = nidx
|
|
220
|
+
break
|
|
221
|
+
sections.append(Section(idx, end, htext))
|
|
222
|
+
return sections
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def present_tier1(body: list[str]) -> set[str]:
|
|
226
|
+
"""Set of canonical Tier-1 headings present as level-2 headings."""
|
|
227
|
+
canonical = set(TIER1_HEADINGS)
|
|
228
|
+
found: set[str] = set()
|
|
229
|
+
for _, level, htext in _headings(body):
|
|
230
|
+
if level == 2 and htext in canonical:
|
|
231
|
+
found.add(htext)
|
|
232
|
+
return found
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def rebuild_body(body: list[str], selected: list[Section]) -> list[str]:
|
|
236
|
+
"""Replace each selected section's line range with its blockquote."""
|
|
237
|
+
by_start = {s.start: s for s in selected}
|
|
238
|
+
out: list[str] = []
|
|
239
|
+
i = 0
|
|
240
|
+
n = len(body)
|
|
241
|
+
while i < n:
|
|
242
|
+
sec = by_start.get(i)
|
|
243
|
+
if sec is not None:
|
|
244
|
+
out.append(sec.blockquote)
|
|
245
|
+
i = sec.end
|
|
246
|
+
else:
|
|
247
|
+
out.append(body[i])
|
|
248
|
+
i += 1
|
|
249
|
+
return out
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def select_sections(
|
|
253
|
+
body: list[str], sections: list[Section], budget: int
|
|
254
|
+
) -> tuple[list[Section], list[str], int]:
|
|
255
|
+
"""Greedily extract largest-first until the body fits, or all are taken.
|
|
256
|
+
|
|
257
|
+
Returns (selected, new_body, body_lines_after). `sorted(reverse=True)` is
|
|
258
|
+
stable, so equal-size sections keep document order — deterministic.
|
|
259
|
+
"""
|
|
260
|
+
candidates = sorted(sections, key=lambda s: s.lines, reverse=True)
|
|
261
|
+
selected: list[Section] = []
|
|
262
|
+
new_body = body
|
|
263
|
+
after = count_body_lines(body)
|
|
264
|
+
for sec in candidates:
|
|
265
|
+
selected.append(sec)
|
|
266
|
+
new_body = rebuild_body(body, selected)
|
|
267
|
+
after = count_body_lines(new_body)
|
|
268
|
+
if after <= budget:
|
|
269
|
+
break
|
|
270
|
+
return selected, new_body, after
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# --------------------------------------------------------------------------
|
|
274
|
+
# Writing (routed through skf-atomic-write.py)
|
|
275
|
+
# --------------------------------------------------------------------------
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _atomic_write(target: Path, content: str) -> None:
|
|
279
|
+
"""Write `content` to `target` atomically via the shared atomic-write helper."""
|
|
280
|
+
helper = Path(__file__).resolve().parent / "skf-atomic-write.py"
|
|
281
|
+
proc = subprocess.run(
|
|
282
|
+
[sys.executable, str(helper), "write", "--target", str(target)],
|
|
283
|
+
input=content.encode("utf-8"),
|
|
284
|
+
capture_output=True,
|
|
285
|
+
)
|
|
286
|
+
if proc.returncode != 0:
|
|
287
|
+
detail = proc.stderr.decode("utf-8", "replace").strip() or proc.stdout.decode(
|
|
288
|
+
"utf-8", "replace"
|
|
289
|
+
).strip()
|
|
290
|
+
raise OSError(f"atomic write failed for {target}: {detail}")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _section_content(body: list[str], sec: Section) -> str:
|
|
294
|
+
"""Section text (heading preserved), trailing blanks trimmed, one newline."""
|
|
295
|
+
chunk = body[sec.start : sec.end]
|
|
296
|
+
end = len(chunk)
|
|
297
|
+
while end > 0 and chunk[end - 1].strip() == "":
|
|
298
|
+
end -= 1
|
|
299
|
+
return "\n".join(chunk[:end]) + "\n"
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
# --------------------------------------------------------------------------
|
|
303
|
+
# CLI
|
|
304
|
+
# --------------------------------------------------------------------------
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _emit(payload: dict, out_path: str | None) -> None:
|
|
308
|
+
text = json.dumps(payload, indent=2) + "\n"
|
|
309
|
+
if out_path:
|
|
310
|
+
Path(out_path).write_text(text, encoding="utf-8")
|
|
311
|
+
else:
|
|
312
|
+
sys.stdout.write(text)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _cmd_shard(args: argparse.Namespace) -> int:
|
|
316
|
+
skill_md = Path(args.skill_md)
|
|
317
|
+
if not skill_md.is_file():
|
|
318
|
+
print(f"error: file not found: {skill_md}", file=sys.stderr)
|
|
319
|
+
return 1
|
|
320
|
+
try:
|
|
321
|
+
text = skill_md.read_text(encoding="utf-8")
|
|
322
|
+
except OSError as exc:
|
|
323
|
+
print(f"error: cannot read {skill_md}: {exc}", file=sys.stderr)
|
|
324
|
+
return 1
|
|
325
|
+
|
|
326
|
+
budget: int = args.budget
|
|
327
|
+
dry_run: bool = args.dry_run
|
|
328
|
+
|
|
329
|
+
frontmatter, body = split_frontmatter(text)
|
|
330
|
+
body_lines_before = count_body_lines(body)
|
|
331
|
+
tier1_before = present_tier1(body)
|
|
332
|
+
|
|
333
|
+
if args.verbose:
|
|
334
|
+
print(
|
|
335
|
+
f"skf-shard-body: body_lines_before={body_lines_before} budget={budget}",
|
|
336
|
+
file=sys.stderr,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# --- Skip path: already within budget -----------------------------------
|
|
340
|
+
if body_lines_before <= budget:
|
|
341
|
+
_emit(
|
|
342
|
+
{
|
|
343
|
+
"action": "skip",
|
|
344
|
+
"body_lines_before": body_lines_before,
|
|
345
|
+
"body_lines_after": body_lines_before,
|
|
346
|
+
"sections_extracted": [],
|
|
347
|
+
"tier1_preserved": True,
|
|
348
|
+
"tier1_missing": [],
|
|
349
|
+
"xref_ok": True,
|
|
350
|
+
"under_budget": True,
|
|
351
|
+
"dry_run": dry_run,
|
|
352
|
+
"budget": budget,
|
|
353
|
+
},
|
|
354
|
+
args.output,
|
|
355
|
+
)
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
# --- Shard path ---------------------------------------------------------
|
|
359
|
+
sections = find_full_sections(body)
|
|
360
|
+
selected, new_body, body_lines_after = select_sections(body, sections, budget)
|
|
361
|
+
|
|
362
|
+
tier1_after = present_tier1(new_body)
|
|
363
|
+
tier1_missing = [
|
|
364
|
+
h for h in TIER1_HEADINGS if h in tier1_before and h not in tier1_after
|
|
365
|
+
]
|
|
366
|
+
tier1_preserved = not tier1_missing
|
|
367
|
+
under_budget = body_lines_after <= budget
|
|
368
|
+
|
|
369
|
+
references_dir = skill_md.parent / "references"
|
|
370
|
+
|
|
371
|
+
# Compute the plan fully before touching disk. Only write when we have a
|
|
372
|
+
# real extraction AND Tier-1 survived — never leave a half-mutated tree.
|
|
373
|
+
should_write = (not dry_run) and bool(selected) and tier1_preserved
|
|
374
|
+
if should_write:
|
|
375
|
+
try:
|
|
376
|
+
for sec in selected:
|
|
377
|
+
_atomic_write(
|
|
378
|
+
references_dir / f"{sec.kebab}.md", _section_content(body, sec)
|
|
379
|
+
)
|
|
380
|
+
new_text = "\n".join(frontmatter + new_body) + "\n"
|
|
381
|
+
_atomic_write(skill_md, new_text)
|
|
382
|
+
except OSError as exc:
|
|
383
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
384
|
+
return 2
|
|
385
|
+
|
|
386
|
+
blockquotes_ok = all(sec.blockquote in new_body for sec in selected)
|
|
387
|
+
if should_write:
|
|
388
|
+
xref_ok = blockquotes_ok and all(
|
|
389
|
+
(references_dir / f"{sec.kebab}.md").is_file() for sec in selected
|
|
390
|
+
)
|
|
391
|
+
else:
|
|
392
|
+
# dry-run, nothing-to-extract, or aborted tier1 violation
|
|
393
|
+
xref_ok = blockquotes_ok and tier1_preserved
|
|
394
|
+
|
|
395
|
+
_emit(
|
|
396
|
+
{
|
|
397
|
+
"action": "shard",
|
|
398
|
+
"body_lines_before": body_lines_before,
|
|
399
|
+
"body_lines_after": body_lines_after,
|
|
400
|
+
"sections_extracted": [
|
|
401
|
+
{"heading": sec.heading, "file": sec.ref_rel, "lines": sec.lines}
|
|
402
|
+
for sec in selected
|
|
403
|
+
],
|
|
404
|
+
"tier1_preserved": tier1_preserved,
|
|
405
|
+
"tier1_missing": tier1_missing,
|
|
406
|
+
"xref_ok": xref_ok,
|
|
407
|
+
"under_budget": under_budget,
|
|
408
|
+
"dry_run": dry_run,
|
|
409
|
+
"budget": budget,
|
|
410
|
+
},
|
|
411
|
+
args.output,
|
|
412
|
+
)
|
|
413
|
+
return 0
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
417
|
+
parser = argparse.ArgumentParser(
|
|
418
|
+
prog="skf-shard-body",
|
|
419
|
+
description=(
|
|
420
|
+
"Deterministically shard an oversized SKILL.md body: extract the "
|
|
421
|
+
"largest `## Full …` Tier-2 sections to references/ until the body "
|
|
422
|
+
"fits under --budget, replacing each with a cross-reference "
|
|
423
|
+
"blockquote, and emit a JSON report the auto-shard / validate "
|
|
424
|
+
"steps consume instead of counting and splitting by hand."
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
parser.add_argument("skill_md", help="path to the staging SKILL.md file")
|
|
428
|
+
parser.add_argument(
|
|
429
|
+
"--budget",
|
|
430
|
+
type=int,
|
|
431
|
+
default=400,
|
|
432
|
+
help="body line budget below which no shard occurs (default 400)",
|
|
433
|
+
)
|
|
434
|
+
parser.add_argument(
|
|
435
|
+
"--dry-run",
|
|
436
|
+
action="store_true",
|
|
437
|
+
help="compute and report the plan without writing any files",
|
|
438
|
+
)
|
|
439
|
+
parser.add_argument(
|
|
440
|
+
"-o",
|
|
441
|
+
"--output",
|
|
442
|
+
default=None,
|
|
443
|
+
help="write JSON report to this path (default stdout)",
|
|
444
|
+
)
|
|
445
|
+
parser.add_argument(
|
|
446
|
+
"--verbose",
|
|
447
|
+
action="store_true",
|
|
448
|
+
help="emit progress diagnostics to stderr",
|
|
449
|
+
)
|
|
450
|
+
parser.set_defaults(func=_cmd_shard)
|
|
451
|
+
return parser
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def main(argv: list[str] | None = None) -> int:
|
|
455
|
+
parser = _build_parser()
|
|
456
|
+
args = parser.parse_args(argv)
|
|
457
|
+
return args.func(args)
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
if __name__ == "__main__":
|
|
461
|
+
raise SystemExit(main())
|
|
@@ -10,12 +10,21 @@ versions via symlinks, and outputs a JSON inventory. Reused by 9+ skills.
|
|
|
10
10
|
CLI: python3 skf-skill-inventory.py <skills-output-folder>
|
|
11
11
|
python3 skf-skill-inventory.py <skills-output-folder> --skill <name>
|
|
12
12
|
python3 skf-skill-inventory.py <skills-output-folder> --manifest-only
|
|
13
|
+
python3 skf-skill-inventory.py <skills-output-folder> --match-target <url-or-name>
|
|
14
|
+
|
|
15
|
+
The --match-target mode deterministically computes coexistence matches: it
|
|
16
|
+
normalizes scheme / trailing .git / trailing slash, derives the expected kebab
|
|
17
|
+
skill name, compares case-insensitively, and emits a top-level `matches[]`
|
|
18
|
+
array. This replaces the equivalent normalize/derive/compare that a consuming
|
|
19
|
+
prompt would otherwise perform by hand (identical (target, inventory) always
|
|
20
|
+
yields the same match set).
|
|
13
21
|
"""
|
|
14
22
|
|
|
15
23
|
from __future__ import annotations
|
|
16
24
|
|
|
17
25
|
import json
|
|
18
26
|
import os
|
|
27
|
+
import re
|
|
19
28
|
import sys
|
|
20
29
|
from pathlib import Path
|
|
21
30
|
|
|
@@ -115,7 +124,115 @@ def scan_skill_group(skill_group_dir, skill_name):
|
|
|
115
124
|
return entry
|
|
116
125
|
|
|
117
126
|
|
|
118
|
-
def
|
|
127
|
+
def normalize_url(value):
|
|
128
|
+
"""Normalize a URL / path / source_repo for case-insensitive comparison.
|
|
129
|
+
|
|
130
|
+
Lowercase; strip an ``http://`` / ``https://`` scheme; strip a trailing
|
|
131
|
+
``.git`` suffix and any trailing slashes. Deterministic and idempotent, so
|
|
132
|
+
``https://github.com/Foo/Bar.git/`` and ``github.com/foo/bar`` normalize to
|
|
133
|
+
the same value. Mirrors step-auto-scope §0c's URL-match normalization.
|
|
134
|
+
"""
|
|
135
|
+
if not value:
|
|
136
|
+
return ""
|
|
137
|
+
s = str(value).strip().lower()
|
|
138
|
+
for scheme in ("https://", "http://"):
|
|
139
|
+
if s.startswith(scheme):
|
|
140
|
+
s = s[len(scheme):]
|
|
141
|
+
break
|
|
142
|
+
s = s.rstrip("/")
|
|
143
|
+
if s.endswith(".git"):
|
|
144
|
+
s = s[:-4]
|
|
145
|
+
s = s.rstrip("/")
|
|
146
|
+
return s
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _kebab(segment):
|
|
150
|
+
"""Kebab-case, lowercase a single name segment (§6 skill-name rule).
|
|
151
|
+
|
|
152
|
+
Lowercase, drop a trailing ``.git``, collapse every run of non-alphanumeric
|
|
153
|
+
characters (``.``, ``_``, spaces, ...) to a single hyphen, and strip leading
|
|
154
|
+
/ trailing hyphens. ``docs.example.com`` -> ``docs-example-com`` (mirrors
|
|
155
|
+
§0a's dot-to-hyphen), ``Bar`` -> ``bar``, ``my_project`` -> ``my-project``.
|
|
156
|
+
"""
|
|
157
|
+
s = str(segment).strip().lower()
|
|
158
|
+
if s.endswith(".git"):
|
|
159
|
+
s = s[:-4]
|
|
160
|
+
s = re.sub(r"[^a-z0-9]+", "-", s)
|
|
161
|
+
return s.strip("-")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def derive_name(target):
|
|
165
|
+
"""Derive the expected skill name from a target URL / path / bare name.
|
|
166
|
+
|
|
167
|
+
Mirrors step-auto-scope §6 (repo/package name = the last non-empty path
|
|
168
|
+
segment, kebab-lowercased) and §0a (a bare doc hostname has its dots turned
|
|
169
|
+
into hyphens). Both cases funnel through :func:`_kebab`, so
|
|
170
|
+
``github.com/x/bar-baz`` -> ``bar-baz`` and ``docs.example.com`` ->
|
|
171
|
+
``docs-example-com``. Returns "" when no segment can be derived.
|
|
172
|
+
"""
|
|
173
|
+
if not target:
|
|
174
|
+
return ""
|
|
175
|
+
s = str(target).strip().lower()
|
|
176
|
+
for scheme in ("https://", "http://"):
|
|
177
|
+
if s.startswith(scheme):
|
|
178
|
+
s = s[len(scheme):]
|
|
179
|
+
break
|
|
180
|
+
s = s.rstrip("/")
|
|
181
|
+
if s.endswith(".git"):
|
|
182
|
+
s = s[:-4]
|
|
183
|
+
s = s.rstrip("/")
|
|
184
|
+
segments = [seg for seg in s.split("/") if seg]
|
|
185
|
+
if not segments:
|
|
186
|
+
return ""
|
|
187
|
+
return _kebab(segments[-1])
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def compute_matches(skills, target):
|
|
191
|
+
"""Return the deterministic coexistence match set for ``target``.
|
|
192
|
+
|
|
193
|
+
For each inventory skill, a hit fires when either the normalized
|
|
194
|
+
``metadata.source_repo`` equals the normalized target (URL match) or the
|
|
195
|
+
derived expected name equals the skill's name, case-insensitively (name
|
|
196
|
+
match). Each match entry is
|
|
197
|
+
``{name, active_version, source_repo, active_path, match_reason}`` where
|
|
198
|
+
``match_reason`` is ``"url"``, ``"name"``, or ``"both"``.
|
|
199
|
+
"""
|
|
200
|
+
norm_target = normalize_url(target)
|
|
201
|
+
derived = derive_name(target)
|
|
202
|
+
matches = []
|
|
203
|
+
for entry in skills:
|
|
204
|
+
source_repo = None
|
|
205
|
+
meta = entry.get("metadata")
|
|
206
|
+
if meta:
|
|
207
|
+
source_repo = meta.get("source_repo")
|
|
208
|
+
url_match = (
|
|
209
|
+
bool(source_repo)
|
|
210
|
+
and norm_target != ""
|
|
211
|
+
and normalize_url(source_repo) == norm_target
|
|
212
|
+
)
|
|
213
|
+
name_match = (
|
|
214
|
+
bool(derived)
|
|
215
|
+
and str(entry.get("name") or "").strip().lower() == derived
|
|
216
|
+
)
|
|
217
|
+
if not (url_match or name_match):
|
|
218
|
+
continue
|
|
219
|
+
if url_match and name_match:
|
|
220
|
+
reason = "both"
|
|
221
|
+
elif url_match:
|
|
222
|
+
reason = "url"
|
|
223
|
+
else:
|
|
224
|
+
reason = "name"
|
|
225
|
+
matches.append({
|
|
226
|
+
"name": entry.get("name"),
|
|
227
|
+
"active_version": entry.get("active_version"),
|
|
228
|
+
"source_repo": source_repo,
|
|
229
|
+
"active_path": entry.get("active_path"),
|
|
230
|
+
"match_reason": reason,
|
|
231
|
+
})
|
|
232
|
+
return matches
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def scan_inventory(skills_folder, skill_filter=None, manifest_only=False, match_target=None):
|
|
119
236
|
"""Scan the skills output folder and produce an inventory."""
|
|
120
237
|
skills_dir = Path(skills_folder)
|
|
121
238
|
|
|
@@ -188,23 +305,42 @@ def scan_inventory(skills_folder, skill_filter=None, manifest_only=False):
|
|
|
188
305
|
result["summary"]["with_metadata"] = sum(1 for s in result["skills"] if s["metadata"])
|
|
189
306
|
result["summary"]["with_provenance"] = sum(1 for s in result["skills"] if s["has_provenance_map"])
|
|
190
307
|
|
|
308
|
+
# Coexistence matching (opt-in via --match-target; additive top-level key).
|
|
309
|
+
if match_target is not None:
|
|
310
|
+
result["matches"] = compute_matches(result["skills"], match_target)
|
|
311
|
+
|
|
191
312
|
return result
|
|
192
313
|
|
|
193
314
|
|
|
194
315
|
if __name__ == "__main__":
|
|
195
316
|
if len(sys.argv) < 2:
|
|
196
|
-
print(
|
|
317
|
+
print(
|
|
318
|
+
"Usage: python3 skf-skill-inventory.py <skills-output-folder> "
|
|
319
|
+
"[--skill <name>] [--manifest-only] [--match-target <url-or-name>]",
|
|
320
|
+
file=sys.stderr,
|
|
321
|
+
)
|
|
197
322
|
sys.exit(1)
|
|
198
323
|
|
|
199
324
|
folder = sys.argv[1]
|
|
200
325
|
skill = None
|
|
201
326
|
manifest_only = "--manifest-only" in sys.argv
|
|
327
|
+
match_target = None
|
|
202
328
|
|
|
203
329
|
if "--skill" in sys.argv:
|
|
204
330
|
idx = sys.argv.index("--skill")
|
|
205
331
|
if idx + 1 < len(sys.argv):
|
|
206
332
|
skill = sys.argv[idx + 1]
|
|
207
333
|
|
|
208
|
-
|
|
334
|
+
if "--match-target" in sys.argv:
|
|
335
|
+
idx = sys.argv.index("--match-target")
|
|
336
|
+
if idx + 1 < len(sys.argv):
|
|
337
|
+
match_target = sys.argv[idx + 1]
|
|
338
|
+
|
|
339
|
+
result = scan_inventory(
|
|
340
|
+
folder,
|
|
341
|
+
skill_filter=skill,
|
|
342
|
+
manifest_only=manifest_only,
|
|
343
|
+
match_target=match_target,
|
|
344
|
+
)
|
|
209
345
|
print(json.dumps(result, indent=2))
|
|
210
346
|
sys.exit(0 if result["status"] == "ok" else 1)
|