get-tbd 0.1.28 → 0.1.29
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/dist/bin.mjs +3 -3
- package/dist/bin.mjs.map +1 -1
- package/dist/cli.mjs +3 -3
- package/dist/cli.mjs.map +1 -1
- package/dist/docs/SKILL.md +0 -4
- package/dist/docs/guidelines/bun-monorepo-patterns.md +92 -289
- package/dist/docs/guidelines/cli-agent-skill-patterns.md +706 -1240
- package/dist/docs/guidelines/pnpm-monorepo-patterns.md +104 -321
- package/dist/docs/guidelines/supply-chain-hardening.md +237 -0
- package/dist/docs/shortcuts/standard/new-validation-plan.md +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-D2xEmH4L.mjs → src-CJyVkC3V.mjs} +2 -2
- package/dist/{src-D2xEmH4L.mjs.map → src-CJyVkC3V.mjs.map} +1 -1
- package/dist/tbd +3 -3
- package/package.json +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Supply-Chain Hardening
|
|
3
|
+
description: Strongly recommended for EVERY repo — apply it if a repo has not been hardened yet. Cross-ecosystem policy for installing dependencies safely (the 14-day cool-off, disabled install scripts, lockfile discipline, untrusted-repo handling). Use whenever a user mentions hardening, security, supply chain, or setting up a new repo; before adding/upgrading dependencies; when auditing for compromised packages; or when reviewing install/build/run commands across npm/pnpm, PyPI, Cargo, or Go.
|
|
4
|
+
author: Joshua Levy (github.com/jlevy) with LLM assistance
|
|
5
|
+
---
|
|
6
|
+
# Supply-Chain Hardening
|
|
7
|
+
|
|
8
|
+
A concise, cross-ecosystem policy for reducing supply-chain risk when installing or
|
|
9
|
+
upgrading dependencies.
|
|
10
|
+
Open-source registries (npm, PyPI, crates.io, Go modules) are under sustained attack:
|
|
11
|
+
malicious package versions are published, exfiltrate credentials or install persistence,
|
|
12
|
+
and are usually yanked within minutes to days.
|
|
13
|
+
|
|
14
|
+
**Strongly recommended for every repo.** If a project has not been hardened yet, apply
|
|
15
|
+
this policy (and the per-ecosystem playbooks it links) as part of setup — it is
|
|
16
|
+
especially worth doing when standing up a new repo or whenever security comes up.
|
|
17
|
+
|
|
18
|
+
**This is the concise policy.** For the full playbooks (per-ecosystem ten-minute setups,
|
|
19
|
+
a zero-dependency audit script, a curated watch list of named incidents, CI/publish-side
|
|
20
|
+
hardening, and the threat-model research), see the **Supply Chain Hardening guidebook**:
|
|
21
|
+
<https://github.com/jlevy/supply-chain-hardening>. For monorepo-specific enforcement,
|
|
22
|
+
see `tbd guidelines bun-monorepo-patterns` or `tbd guidelines pnpm-monorepo-patterns`.
|
|
23
|
+
|
|
24
|
+
**When to use this guideline**: before adding or upgrading any dependency; when
|
|
25
|
+
hardening a workstation, repo, or CI pipeline; when assessing whether an installed
|
|
26
|
+
package is compromised; or when reviewing any `install` / `build` / `run` command —
|
|
27
|
+
especially in a freshly cloned third-party repo.
|
|
28
|
+
|
|
29
|
+
## The Default: a 14-Day Cool-Off
|
|
30
|
+
|
|
31
|
+
**Never install or upgrade to a package version less than 14 days old, unless a
|
|
32
|
+
documented exception applies.** This is the single most effective default.
|
|
33
|
+
It works because registries and researchers detect and yank malicious versions while
|
|
34
|
+
legitimate versions keep accruing age — so the only cost of waiting is slightly staler
|
|
35
|
+
dependencies.
|
|
36
|
+
|
|
37
|
+
**14 days is a floor, not a ceiling.** A 30/60/90-day window is strictly safer; machines
|
|
38
|
+
with publish tokens or production access should go higher.
|
|
39
|
+
Scope: applies to `dependencies`, `devDependencies` (historically *more* dangerous —
|
|
40
|
+
build tooling runs with full privileges), `peer`/`optionalDependencies`, new installs,
|
|
41
|
+
and upgrades.
|
|
42
|
+
Pins resolved before adopting the policy are grandfathered until their next
|
|
43
|
+
planned upgrade.
|
|
44
|
+
|
|
45
|
+
### Per-ecosystem control
|
|
46
|
+
|
|
47
|
+
| Tool | 14-day control |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| npm (any) | `NPM_CONFIG_BEFORE=<now-14d>` (absolute ISO date) |
|
|
50
|
+
| npm 11.10+ | `NPM_CONFIG_MIN_RELEASE_AGE=14` (days) |
|
|
51
|
+
| pnpm 10.16–10.x | `NPM_CONFIG_MINIMUM_RELEASE_AGE=20160` (minutes) |
|
|
52
|
+
| pnpm 11+ | `minimumReleaseAge: 20160` in `pnpm-workspace.yaml` (pnpm 11 ignores `NPM_CONFIG_*`; env prefix is `PNPM_CONFIG_*`) |
|
|
53
|
+
| uv | `UV_EXCLUDE_NEWER="14 days"` |
|
|
54
|
+
| pip 26.1+ | `PIP_UPLOADED_PRIOR_TO="P14D"` |
|
|
55
|
+
| Cargo / Go | no native gate: commit the lockfile, pass `--locked` (Cargo) / keep `go.sum` + `-mod=readonly` (Go), and require human review before re-resolving; gate automated bumps with Renovate/Dependabot release-age policy |
|
|
56
|
+
|
|
57
|
+
At upgrade-decision time, `npm-check-updates --cooldown 14` (pin the `ncu` version)
|
|
58
|
+
gates which upgrades are even offered.
|
|
59
|
+
To check one version’s age: `npm view <pkg> time.<ver>`.
|
|
60
|
+
|
|
61
|
+
## The Other Install Rules
|
|
62
|
+
|
|
63
|
+
1. **Never install unthinkingly.** Before any `pnpm add` / `npm i` / `uv add` /
|
|
64
|
+
`pip install` / `cargo install` / `go install`: confirm the package is needed, the
|
|
65
|
+
name is spelled correctly (typosquats are common), and the version clears the
|
|
66
|
+
cool-off (or is lockfile-pinned, or has a stated exception).
|
|
67
|
+
2. **Disable install/lifecycle scripts by default** — the primary exfiltration vector in
|
|
68
|
+
worm-class attacks (`NPM_CONFIG_IGNORE_SCRIPTS=true`; pnpm `ignoreScripts: true` +
|
|
69
|
+
`allowBuilds` allowlist; refuse PyPI sdist builds with
|
|
70
|
+
`UV_NO_BUILD`/`PIP_ONLY_BINARY`).
|
|
71
|
+
3. **Commit lockfiles; install frozen.** `pnpm install --frozen-lockfile` / `npm ci` /
|
|
72
|
+
`--locked`. Never auto-update without review.
|
|
73
|
+
4. **Audit after every install** — `pnpm audit` / `npm audit` / `pip-audit` /
|
|
74
|
+
`cargo audit` / `govulncheck`; address findings before continuing.
|
|
75
|
+
5. **Don’t update for its own sake.** The safest update is the one you skip — each bump
|
|
76
|
+
is fresh attack surface.
|
|
77
|
+
Bump only for a concrete reason ("show me the commit we need"); prefer fewer,
|
|
78
|
+
vendored/pinned dependencies; let audits + CVE monitoring tell you when a real update
|
|
79
|
+
is warranted.
|
|
80
|
+
6. **No unpinned zero-install runners.** Avoid `npx` / `pnpm dlx` / `bunx` / `uvx` /
|
|
81
|
+
`go run <remote>` without an explicit `@version` pin and a review of the resolved
|
|
82
|
+
`package@version` — they fetch and execute the latest code, bypassing the cool-off.
|
|
83
|
+
(When a skill references a CLI via a runner, pin it — see
|
|
84
|
+
`tbd guidelines cli-agent-skill-patterns` §6.7.)
|
|
85
|
+
7. **No `curl | sh` from untrusted sources.** Verify the installer URL belongs to the
|
|
86
|
+
documented project; check signatures/checksums where available.
|
|
87
|
+
|
|
88
|
+
## The Exception Process
|
|
89
|
+
|
|
90
|
+
When a version inside the 14-day window is genuinely needed (e.g. a CVE patch published
|
|
91
|
+
yesterday), take the exception **explicitly and on the record**:
|
|
92
|
+
|
|
93
|
+
- State the reason in the commit/PR: the CVE ID (or vulnerability description) and a
|
|
94
|
+
`Reviewed-by:` sign-off.
|
|
95
|
+
- Pin the exact `package@version` (not a range); verify it against the authoritative
|
|
96
|
+
sources (OSV, GHSA, maintainer postmortem).
|
|
97
|
+
- Log it, with a follow-up to confirm the version was not yanked afterward.
|
|
98
|
+
|
|
99
|
+
No exception is “trivial” — the rule exists because we don’t trust ourselves to eyeball
|
|
100
|
+
which fresh versions are safe.
|
|
101
|
+
**Agents never self-approve an exception**: prepare the record and a human signs off.
|
|
102
|
+
|
|
103
|
+
## Node / TypeScript Enforcement (npm, pnpm, Bun)
|
|
104
|
+
|
|
105
|
+
The hands-on controls for the rules above in the Node ecosystem.
|
|
106
|
+
Applies to **any** repo, not just new monorepos — drop these into an existing project.
|
|
107
|
+
|
|
108
|
+
**Lifecycle-script hygiene (the highest-value control).** Block install/build scripts by
|
|
109
|
+
default and allowlist only what you trust:
|
|
110
|
+
|
|
111
|
+
- **pnpm 11**: declare allowed packages in `pnpm-workspace.yaml`; keep
|
|
112
|
+
`blockExoticSubdeps` on.
|
|
113
|
+
Everything else installs with no `postinstall`/`preinstall`.
|
|
114
|
+
```yaml
|
|
115
|
+
# pnpm-workspace.yaml
|
|
116
|
+
minimumReleaseAge: 20160 # 14 days in minutes (pnpm 11 default is 1440 = 1 day)
|
|
117
|
+
allowBuilds:
|
|
118
|
+
- esbuild
|
|
119
|
+
- sharp
|
|
120
|
+
```
|
|
121
|
+
- **Bun**: blocks lifecycle scripts by default via an internal allowlist; extend it with
|
|
122
|
+
`trustedDependencies` in `package.json`. Bun has **no native release-age gate** yet,
|
|
123
|
+
so enforce the cool-off at the upgrade-tool layer (below).
|
|
124
|
+
```json
|
|
125
|
+
{ "trustedDependencies": ["esbuild", "sharp"] }
|
|
126
|
+
```
|
|
127
|
+
- **npm**: `NPM_CONFIG_IGNORE_SCRIPTS=true` (or `.npmrc` `ignore-scripts=true`).
|
|
128
|
+
|
|
129
|
+
**Release-age gate** (resolution time): pnpm `minimumReleaseAge` (above); npm
|
|
130
|
+
`NPM_CONFIG_BEFORE` / `NPM_CONFIG_MIN_RELEASE_AGE` (npm 11.10+); Bun none.
|
|
131
|
+
|
|
132
|
+
**Upgrade-time check** (complements resolution-time gating): `npm-check-updates`
|
|
133
|
+
`--cooldown` works on npm/pnpm/Bun/yarn projects.
|
|
134
|
+
Pin the `ncu` version.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
pnpm dlx npm-check-updates@<ver> --cooldown 14 # or: bunx / npx
|
|
138
|
+
pnpm dlx npm-check-updates@<ver> --cooldown 14 --errorLevel 2 # CI: non-zero if fresh upgrades exist
|
|
139
|
+
npm view <pkg> time.<version> # one version's publish time; if < 14d, wait
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Lockfile discipline**: always commit the lockfile (`pnpm-lock.yaml` / `bun.lock` /
|
|
143
|
+
`package-lock.json`); install frozen in CI (`pnpm install --frozen-lockfile` /
|
|
144
|
+
`bun install --frozen-lockfile` / `npm ci`); never `pnpm update` / `bun update` without
|
|
145
|
+
reviewing the lockfile diff like a code diff.
|
|
146
|
+
One root lockfile in a monorepo.
|
|
147
|
+
|
|
148
|
+
**Provenance**: prefer deps that publish
|
|
149
|
+
[npm provenance attestations](https://docs.npmjs.com/generating-provenance-statements)
|
|
150
|
+
(TypeScript, Vitest, Prettier, ESLint do).
|
|
151
|
+
Run `pnpm audit signatures` / `npm audit signatures` periodically.
|
|
152
|
+
A provenance badge is necessary, not sufficient — the @antv worm forged one.
|
|
153
|
+
|
|
154
|
+
**CI audit gate** (alongside lint/test):
|
|
155
|
+
|
|
156
|
+
```yaml
|
|
157
|
+
audit:
|
|
158
|
+
runs-on: ubuntu-latest
|
|
159
|
+
steps:
|
|
160
|
+
- uses: actions/checkout@v6
|
|
161
|
+
- run: pnpm install --frozen-lockfile # or: bun install --frozen-lockfile / npm ci
|
|
162
|
+
- run: pnpm audit --audit-level=moderate # or: bun audit / npm audit
|
|
163
|
+
- run: pnpm audit signatures # provenance check where supported
|
|
164
|
+
- run: pnpm dlx npm-check-updates@<ver> --cooldown 14 --errorLevel 0
|
|
165
|
+
# errorLevel 0 logs but doesn't fail — flip to 2 once the backlog is cleared
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**Pre-push age guard** — a zero-dependency Node script wired into lefthook/husky:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
#!/usr/bin/env tsx
|
|
172
|
+
// Fails if any direct dependency was published < 14 days ago.
|
|
173
|
+
import pkg from '../package.json' with { type: 'json' };
|
|
174
|
+
const COOLDOWN_MS = 14 * 24 * 60 * 60 * 1000;
|
|
175
|
+
const now = Date.now();
|
|
176
|
+
let violations = 0;
|
|
177
|
+
for (const [name, spec] of Object.entries({ ...pkg.dependencies, ...pkg.devDependencies })) {
|
|
178
|
+
const version = String(spec).replace(/^[\^~=<>]+/, '');
|
|
179
|
+
const meta = await (await fetch(`https://registry.npmjs.org/${name}`)).json();
|
|
180
|
+
const publishedAt = meta.time?.[version];
|
|
181
|
+
if (!publishedAt) continue;
|
|
182
|
+
if (now - new Date(publishedAt).getTime() < COOLDOWN_MS) {
|
|
183
|
+
console.error(`✗ ${name}@${version} is < 14 days old`);
|
|
184
|
+
violations++;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
process.exit(violations > 0 ? 1 : 0);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Exception bookkeeping**: when you pin a fresh version under the exception process,
|
|
191
|
+
leave a marker next to the pin (JSONC comment in `package.json`, or a `CHANGELOG.md`
|
|
192
|
+
note for strict JSON parsers): `// Exception: CVE-2026-XXXX patch within 14d window.
|
|
193
|
+
Reviewed <date>.`
|
|
194
|
+
|
|
195
|
+
## Untrusted Repos & Modes
|
|
196
|
+
|
|
197
|
+
- **Treat any freshly-cloned third-party repo as untrusted.** Do not run
|
|
198
|
+
`install`/`build`/`test`/`run`/`npx`/`uvx`/`cargo run`/`go run <remote>` against it on
|
|
199
|
+
a machine with ambient credentials until you’ve reviewed it — ideally in a container
|
|
200
|
+
or namespace-isolated sandbox.
|
|
201
|
+
(`build.rs`, proc-macros, `require()`-time payloads, and test files all execute code.)
|
|
202
|
+
- **Modes**: default to **Balanced** (the policy above).
|
|
203
|
+
Enter **Strict** (no upgrade without reviewing the change set; build-script allowlist
|
|
204
|
+
required; mandatory sandbox; CI scanners checksum-verified) when the repo declares it,
|
|
205
|
+
when the repo is untrusted, or on a machine with publish tokens / production access.
|
|
206
|
+
**Emergency Exception** is a single logged per-command bypass — never self-approved by
|
|
207
|
+
an agent.
|
|
208
|
+
|
|
209
|
+
## What This Does and Doesn’t Cover
|
|
210
|
+
|
|
211
|
+
A cool-off + disabled scripts neutralizes the dominant **fast-yanked-incident** pattern.
|
|
212
|
+
It does **not** stop: long-lived typosquats that survive past the window; a lockfile
|
|
213
|
+
that already captured a bad version; payloads that fire on import/build rather than
|
|
214
|
+
install; or **publish-pipeline compromises** (the May 2026 @antv worm shipped from
|
|
215
|
+
legitimate CI with a forged “verified” provenance badge — a green badge is not proof).
|
|
216
|
+
Those need lockfile review, typosquat checks, build-time controls, and — if you publish
|
|
217
|
+
packages — the publish-side controls in the guidebook’s `hardening-ci-cd.md` (OIDC
|
|
218
|
+
trusted publishing, staged publishing, SHA-pinned actions, runner egress limits,
|
|
219
|
+
provenance monitoring).
|
|
220
|
+
|
|
221
|
+
## Apply It Here (tbd)
|
|
222
|
+
|
|
223
|
+
- The repo root carries `SUPPLY-CHAIN-SECURITY.md` (the portable flag file) referenced
|
|
224
|
+
from `AGENTS.md`/`CLAUDE.md`, so any agent sees the install rules before adding deps.
|
|
225
|
+
- tbd is a pnpm project: the 14-day rule, `ncu --cooldown 14`, and the
|
|
226
|
+
`scripts/check-package-age.mjs` pre-push guard are covered in
|
|
227
|
+
`tbd guidelines pnpm-monorepo-patterns` → Supply-Chain Mitigation.
|
|
228
|
+
|
|
229
|
+
## References
|
|
230
|
+
|
|
231
|
+
- **Supply Chain Hardening guidebook** (full playbooks, audit script, watch list,
|
|
232
|
+
CI/publish hardening, research): <https://github.com/jlevy/supply-chain-hardening>
|
|
233
|
+
- Authoritative sources: [OSV.dev](https://osv.dev), GitHub Advisory DB, `npm audit` /
|
|
234
|
+
`pip-audit` / `cargo audit` / `govulncheck`; incident feeds (Aikido Intel,
|
|
235
|
+
StepSecurity, Socket, Unit 42).
|
|
236
|
+
- Monorepo enforcement: `tbd guidelines pnpm-monorepo-patterns`,
|
|
237
|
+
`tbd guidelines bun-monorepo-patterns`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: New Validation Plan
|
|
3
|
-
description: Create a validation/test plan showing what
|
|
3
|
+
description: Create a validation/test plan showing what’s tested and what remains
|
|
4
4
|
category: planning
|
|
5
5
|
author: Joshua Levy (github.com/jlevy) with LLM assistance
|
|
6
6
|
---
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { A as Ulid, C as LOCAL_STATE_FIELD_ORDER, D as Priority, E as MetaSchema, O as ShortId, S as IssueTitle, T as META_FIELD_ORDER, _ as IdMappingYamlSchema, a as ConfigSchema, b as IssueSchema, c as DocCacheConfigSchema, d as ExternalIssueIdInput, f as GitBranchName, g as ISSUE_TITLE_MAX_LENGTH, h as ISSUE_FIELD_ORDER, i as CONFIG_FIELD_ORDER, j as Version, k as Timestamp, l as DocsCacheSchema, m as ISSUE_BODY_MAX_LENGTH, n as AtticEntrySchema, o as Dependency, p as GitRemoteName, r as BaseEntity, s as DependencyRelationType, t as ATTIC_ENTRY_FIELD_ORDER, u as EntityType, v as IssueId, w as LocalStateSchema, x as IssueStatus, y as IssueKind } from "./schemas-C8mOQykE.mjs";
|
|
2
|
-
import { c as noopLogger, i as serializeIssue, n as parseIssue, t as VERSION } from "./src-
|
|
2
|
+
import { c as noopLogger, i as serializeIssue, n as parseIssue, t as VERSION } from "./src-CJyVkC3V.mjs";
|
|
3
3
|
|
|
4
4
|
export { ATTIC_ENTRY_FIELD_ORDER, AtticEntrySchema, BaseEntity, CONFIG_FIELD_ORDER, ConfigSchema, Dependency, DependencyRelationType, DocCacheConfigSchema, DocsCacheSchema, EntityType, ExternalIssueIdInput, GitBranchName, GitRemoteName, ISSUE_BODY_MAX_LENGTH, ISSUE_FIELD_ORDER, ISSUE_TITLE_MAX_LENGTH, IdMappingYamlSchema, IssueId, IssueKind, IssueSchema, IssueStatus, IssueTitle, LOCAL_STATE_FIELD_ORDER, LocalStateSchema, META_FIELD_ORDER, MetaSchema, Priority, ShortId, Timestamp, Ulid, VERSION, Version, noopLogger, parseIssue, serializeIssue };
|
|
@@ -183,8 +183,8 @@ function serializeIssue(issue) {
|
|
|
183
183
|
* Package version, derived from git at build time.
|
|
184
184
|
* Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
|
|
185
185
|
*/
|
|
186
|
-
const VERSION = "0.1.
|
|
186
|
+
const VERSION = "0.1.29";
|
|
187
187
|
|
|
188
188
|
//#endregion
|
|
189
189
|
export { insertAfterFrontmatter as a, noopLogger as c, serializeIssue as i, parseIssue as n, parseMarkdown as o, parseMarkdownWithFrontmatter as r, stripFrontmatter as s, VERSION as t };
|
|
190
|
-
//# sourceMappingURL=src-
|
|
190
|
+
//# sourceMappingURL=src-CJyVkC3V.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"src-D2xEmH4L.mjs","names":["parseYaml"],"sources":["../src/lib/types.ts","../src/utils/markdown-utils.ts","../src/file/parser.ts","../src/index.ts"],"sourcesContent":["/**\n * TypeScript types derived from Zod schemas.\n *\n * These types are the canonical TypeScript interface for tbd entities.\n */\n\nimport type { z } from 'zod';\n\nimport type {\n IssueSchema,\n IssueStatus,\n IssueKind,\n Priority,\n Dependency,\n ConfigSchema,\n MetaSchema,\n LocalStateSchema,\n AtticEntrySchema,\n} from './schemas.js';\n\n// =============================================================================\n// Entity Types\n// =============================================================================\n\n/**\n * A tbd issue entity.\n */\nexport type Issue = z.infer<typeof IssueSchema>;\n\n/**\n * Issue status enum values.\n */\nexport type IssueStatusType = z.infer<typeof IssueStatus>;\n\n/**\n * Issue kind enum values.\n */\nexport type IssueKindType = z.infer<typeof IssueKind>;\n\n/**\n * Priority level (0-4).\n */\nexport type PriorityType = z.infer<typeof Priority>;\n\n/**\n * A dependency relationship.\n */\nexport type DependencyType = z.infer<typeof Dependency>;\n\n// =============================================================================\n// Configuration Types\n// =============================================================================\n\n/**\n * Project configuration.\n */\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * Shared metadata.\n */\nexport type Meta = z.infer<typeof MetaSchema>;\n\n/**\n * Per-node local state.\n */\nexport type LocalState = z.infer<typeof LocalStateSchema>;\n\n/**\n * Attic entry for conflict losers.\n */\nexport type AtticEntry = z.infer<typeof AtticEntrySchema>;\n\n// =============================================================================\n// Input Types for Commands\n// =============================================================================\n\n/**\n * Options for creating an issue.\n */\nexport interface CreateIssueOptions {\n title: string;\n description?: string;\n kind?: IssueKindType;\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent_id?: string;\n due_date?: string;\n deferred_until?: string;\n}\n\n/**\n * Options for updating an issue.\n */\nexport interface UpdateIssueOptions {\n title?: string;\n description?: string;\n notes?: string;\n kind?: IssueKindType;\n status?: IssueStatusType;\n priority?: PriorityType;\n assignee?: string | null;\n addLabels?: string[];\n removeLabels?: string[];\n parent_id?: string | null;\n due_date?: string | null;\n deferred_until?: string | null;\n}\n\n/**\n * Options for listing issues.\n */\nexport interface ListIssuesOptions {\n status?: IssueStatusType | IssueStatusType[];\n kind?: IssueKindType | IssueKindType[];\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent?: string;\n all?: boolean;\n sort?: 'priority' | 'created' | 'updated';\n limit?: number;\n}\n\n/**\n * Options for searching issues.\n */\nexport interface SearchIssuesOptions {\n query: string;\n status?: IssueStatusType | IssueStatusType[];\n limit?: number;\n}\n\n// =============================================================================\n// CLI Utility Types\n// =============================================================================\n\n/**\n * A documentation section with title and slug.\n * Used by docs and design commands.\n */\nexport interface DocSection {\n title: string;\n slug: string;\n}\n\n/**\n * Logger interface for long-running operations in non-CLI layers.\n *\n * Allows core logic (file/, lib/) to report progress without depending on\n * the CLI output layer. CLI commands create an OperationLogger via\n * `OutputManager.logger(spinner)` and pass it to core functions.\n *\n * All methods are required. Use `noopLogger` when no logging is needed.\n */\nexport interface OperationLogger {\n /** Key milestones — drives the spinner in CLI context */\n progress: (message: string) => void;\n /** Operational detail (shown with --verbose or --debug) */\n info: (message: string) => void;\n /** Non-fatal warnings */\n warn: (message: string) => void;\n /** Internal state for troubleshooting (shown with --debug only) */\n debug: (message: string) => void;\n}\n\n/**\n * No-op logger for when no logging is needed.\n * Analogous to noopSpinner in the CLI layer.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\nexport const noopLogger: OperationLogger = {\n progress: noop,\n info: noop,\n warn: noop,\n debug: noop,\n};\n","/**\n * Markdown utilities for processing markdown content.\n *\n * Uses gray-matter for parsing and centralized yaml-utils for stringify to ensure\n * proper handling of special YAML characters (colons, quotes, etc.).\n */\n\nimport matter from 'gray-matter';\n\nimport { stringifyYamlCompact } from './yaml-utils.js';\n\nexport interface ParsedMarkdown {\n /** Raw frontmatter string (without --- delimiters), or null if no frontmatter */\n frontmatter: string | null;\n /** Body content after frontmatter, with leading newlines trimmed */\n body: string;\n}\n\n/**\n * Normalize line endings to LF.\n */\nexport function normalizeLineEndings(content: string): string {\n return content.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n}\n\n/**\n * Parse markdown content into frontmatter and body.\n * Handles both LF and CRLF line endings.\n *\n * @returns Object with frontmatter (null if none) and body\n */\nexport function parseMarkdown(content: string): ParsedMarkdown {\n const normalized = normalizeLineEndings(content);\n\n if (!matter.test(normalized)) {\n return { frontmatter: null, body: content };\n }\n\n try {\n const parsed = matter(normalized);\n\n // Extract frontmatter from parsed.data by stringifying back to YAML\n // The matter property is unreliable, so we reconstruct from data\n const data = parsed.data;\n let frontmatter: string | null = null;\n\n if (data && Object.keys(data).length > 0) {\n // Use centralized yaml-utils for proper handling of special characters\n // (colons, quotes, multiline strings, etc.)\n frontmatter = stringifyYamlCompact(data).trimEnd();\n } else {\n // Empty frontmatter (just --- followed by ---)\n frontmatter = '';\n }\n\n // Body with leading newlines trimmed\n const body = parsed.content.replace(/^\\n+/, '');\n\n return { frontmatter, body };\n } catch {\n // Invalid/unclosed frontmatter - treat as no frontmatter\n return { frontmatter: null, body: content };\n }\n}\n\n/**\n * Parse YAML frontmatter from markdown content.\n * Returns the frontmatter content (without delimiters) or null if no valid frontmatter.\n * Handles both LF and CRLF line endings.\n */\nexport function parseFrontmatter(content: string): string | null {\n return parseMarkdown(content).frontmatter;\n}\n\n/**\n * Strip YAML frontmatter from markdown content.\n * Returns the body content without frontmatter, with leading newlines trimmed.\n * Handles both LF and CRLF line endings.\n */\nexport function stripFrontmatter(content: string): string {\n return parseMarkdown(content).body;\n}\n\n/**\n * Insert content after YAML frontmatter.\n * If no frontmatter exists, prepends the content.\n * Content is inserted directly after ---. Include leading newlines in toInsert if needed.\n */\nexport function insertAfterFrontmatter(content: string, toInsert: string): string {\n const { frontmatter, body } = parseMarkdown(content);\n\n if (frontmatter === null) {\n return toInsert + content;\n }\n\n const frontmatterBlock = frontmatter ? `---\\n${frontmatter}\\n---` : '---\\n---';\n return `${frontmatterBlock}\\n${toInsert}\\n\\n${body}`;\n}\n","/**\n * YAML front matter parser and serializer for issue files.\n *\n * Issues are stored as Markdown files with YAML front matter:\n * ---\n * type: is\n * id: is-a1b2c3\n * ...\n * ---\n *\n * Description body here.\n *\n * ## Notes\n *\n * Working notes here.\n *\n * See: tbd-design.md §2.1 Markdown + YAML Front Matter Format\n */\n\nimport matter from 'gray-matter';\nimport { parse as parseYaml } from 'yaml';\n\nimport { normalizeLineEndings } from '../utils/markdown-utils.js';\nimport { sortKeys, stringifyYaml } from '../utils/yaml-utils.js';\nimport type { Issue } from '../lib/types.js';\nimport { IssueSchema, ISSUE_FIELD_ORDER } from '../lib/schemas.js';\n\n/**\n * gray-matter options using the 'yaml' package as engine.\n * This preserves date strings instead of converting them to Date objects.\n */\nexport const matterOptions = {\n engines: {\n yaml: {\n parse: (str: string): object => parseYaml(str) as object,\n stringify: (obj: object): string => stringifyYaml(obj),\n },\n },\n};\n\n/**\n * Parsed issue file content.\n */\nexport interface ParsedIssueFile {\n frontmatter: Record<string, unknown>;\n description: string;\n notes: string;\n}\n\n/**\n * Parse a Markdown file with YAML front matter.\n * Uses gray-matter for consistent frontmatter parsing.\n * Handles both LF and CRLF line endings.\n */\nexport function parseMarkdownWithFrontmatter(content: string): ParsedIssueFile {\n // Normalize CRLF to LF before parsing\n const normalizedContent = normalizeLineEndings(content);\n\n // Check for valid frontmatter\n if (!matter.test(normalizedContent)) {\n throw new Error('Invalid format: missing front matter opening delimiter');\n }\n\n const parsed = matter(normalizedContent, matterOptions);\n\n // gray-matter returns empty object if no closing delimiter found\n // but the raw matter string will be empty if parsing failed\n if (parsed.matter === '' && !normalizedContent.includes('---\\n---')) {\n // Check if there's actually a closing delimiter\n const lines = normalizedContent.split('\\n');\n let hasClosing = false;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i]?.trim() === '---') {\n hasClosing = true;\n break;\n }\n }\n if (!hasClosing) {\n throw new Error('Invalid format: missing front matter closing delimiter');\n }\n }\n\n const frontmatter = parsed.data as Record<string, unknown>;\n\n // Parse body - split into description and notes\n const body = parsed.content.trim();\n\n // Find notes section\n const notesMatch = /\\n## Notes\\n/i.exec(body);\n let description = body;\n let notes = '';\n\n if (notesMatch?.index !== undefined) {\n description = body.slice(0, notesMatch.index).trim();\n notes = body.slice(notesMatch.index + notesMatch[0].length).trim();\n }\n\n return { frontmatter, description, notes };\n}\n\n/**\n * Parse an issue from Markdown file content.\n */\nexport function parseIssue(content: string): Issue {\n const { frontmatter, description, notes } = parseMarkdownWithFrontmatter(content);\n\n // Merge body content into frontmatter\n const data = {\n ...frontmatter,\n description: description || undefined,\n notes: notes || undefined,\n };\n\n // Validate and parse with Zod\n return IssueSchema.parse(data);\n}\n\n/**\n * Serialize an issue to Markdown file content.\n * Uses canonical serialization for deterministic output.\n */\nexport function serializeIssue(issue: Issue): string {\n // Extract body fields\n const { description, notes, ...metadata } = issue;\n\n // Sort keys using canonical field order (not alphabetical)\n const sortedMetadata = sortKeys(metadata, ISSUE_FIELD_ORDER);\n\n // Serialize YAML with compact output for frontmatter.\n // sortMapEntries: false preserves our manual ordering.\n const yaml = stringifyYaml(sortedMetadata, {\n lineWidth: 0,\n nullStr: 'null',\n sortMapEntries: false,\n });\n\n // Build the file content\n // Note: No blank line between closing --- and body content\n const parts = ['---', yaml.trim(), '---'];\n\n if (description) {\n parts.push(description.trim());\n }\n\n if (notes) {\n parts.push('');\n parts.push('## Notes');\n parts.push('');\n parts.push(notes.trim());\n }\n\n // Single newline at end\n return parts.join('\\n') + '\\n';\n}\n","/**\n * tbd: Git-native issue tracking for AI agents and humans\n *\n * This is the library entry point. All exports here should be node-free\n * to support browser/edge runtime usage. CLI-specific code is in ./cli/.\n */\n\n// Version injected at build time\ndeclare const __TBD_VERSION__: string;\n\n/**\n * Package version, derived from git at build time.\n * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.\n */\nexport const VERSION: string =\n typeof __TBD_VERSION__ !== 'undefined' ? __TBD_VERSION__ : 'development';\n\n// Re-export schemas for library consumers\nexport * from './lib/schemas.js';\nexport * from './lib/types.js';\n\n// Re-export core operations (these should be node-free)\nexport { parseIssue, serializeIssue } from './file/parser.js';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"src-CJyVkC3V.mjs","names":["parseYaml"],"sources":["../src/lib/types.ts","../src/utils/markdown-utils.ts","../src/file/parser.ts","../src/index.ts"],"sourcesContent":["/**\n * TypeScript types derived from Zod schemas.\n *\n * These types are the canonical TypeScript interface for tbd entities.\n */\n\nimport type { z } from 'zod';\n\nimport type {\n IssueSchema,\n IssueStatus,\n IssueKind,\n Priority,\n Dependency,\n ConfigSchema,\n MetaSchema,\n LocalStateSchema,\n AtticEntrySchema,\n} from './schemas.js';\n\n// =============================================================================\n// Entity Types\n// =============================================================================\n\n/**\n * A tbd issue entity.\n */\nexport type Issue = z.infer<typeof IssueSchema>;\n\n/**\n * Issue status enum values.\n */\nexport type IssueStatusType = z.infer<typeof IssueStatus>;\n\n/**\n * Issue kind enum values.\n */\nexport type IssueKindType = z.infer<typeof IssueKind>;\n\n/**\n * Priority level (0-4).\n */\nexport type PriorityType = z.infer<typeof Priority>;\n\n/**\n * A dependency relationship.\n */\nexport type DependencyType = z.infer<typeof Dependency>;\n\n// =============================================================================\n// Configuration Types\n// =============================================================================\n\n/**\n * Project configuration.\n */\nexport type Config = z.infer<typeof ConfigSchema>;\n\n/**\n * Shared metadata.\n */\nexport type Meta = z.infer<typeof MetaSchema>;\n\n/**\n * Per-node local state.\n */\nexport type LocalState = z.infer<typeof LocalStateSchema>;\n\n/**\n * Attic entry for conflict losers.\n */\nexport type AtticEntry = z.infer<typeof AtticEntrySchema>;\n\n// =============================================================================\n// Input Types for Commands\n// =============================================================================\n\n/**\n * Options for creating an issue.\n */\nexport interface CreateIssueOptions {\n title: string;\n description?: string;\n kind?: IssueKindType;\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent_id?: string;\n due_date?: string;\n deferred_until?: string;\n}\n\n/**\n * Options for updating an issue.\n */\nexport interface UpdateIssueOptions {\n title?: string;\n description?: string;\n notes?: string;\n kind?: IssueKindType;\n status?: IssueStatusType;\n priority?: PriorityType;\n assignee?: string | null;\n addLabels?: string[];\n removeLabels?: string[];\n parent_id?: string | null;\n due_date?: string | null;\n deferred_until?: string | null;\n}\n\n/**\n * Options for listing issues.\n */\nexport interface ListIssuesOptions {\n status?: IssueStatusType | IssueStatusType[];\n kind?: IssueKindType | IssueKindType[];\n priority?: PriorityType;\n assignee?: string;\n labels?: string[];\n parent?: string;\n all?: boolean;\n sort?: 'priority' | 'created' | 'updated';\n limit?: number;\n}\n\n/**\n * Options for searching issues.\n */\nexport interface SearchIssuesOptions {\n query: string;\n status?: IssueStatusType | IssueStatusType[];\n limit?: number;\n}\n\n// =============================================================================\n// CLI Utility Types\n// =============================================================================\n\n/**\n * A documentation section with title and slug.\n * Used by docs and design commands.\n */\nexport interface DocSection {\n title: string;\n slug: string;\n}\n\n/**\n * Logger interface for long-running operations in non-CLI layers.\n *\n * Allows core logic (file/, lib/) to report progress without depending on\n * the CLI output layer. CLI commands create an OperationLogger via\n * `OutputManager.logger(spinner)` and pass it to core functions.\n *\n * All methods are required. Use `noopLogger` when no logging is needed.\n */\nexport interface OperationLogger {\n /** Key milestones — drives the spinner in CLI context */\n progress: (message: string) => void;\n /** Operational detail (shown with --verbose or --debug) */\n info: (message: string) => void;\n /** Non-fatal warnings */\n warn: (message: string) => void;\n /** Internal state for troubleshooting (shown with --debug only) */\n debug: (message: string) => void;\n}\n\n/**\n * No-op logger for when no logging is needed.\n * Analogous to noopSpinner in the CLI layer.\n */\n// eslint-disable-next-line @typescript-eslint/no-empty-function\nconst noop = () => {};\nexport const noopLogger: OperationLogger = {\n progress: noop,\n info: noop,\n warn: noop,\n debug: noop,\n};\n","/**\n * Markdown utilities for processing markdown content.\n *\n * Uses gray-matter for parsing and centralized yaml-utils for stringify to ensure\n * proper handling of special YAML characters (colons, quotes, etc.).\n */\n\nimport matter from 'gray-matter';\n\nimport { stringifyYamlCompact } from './yaml-utils.js';\n\nexport interface ParsedMarkdown {\n /** Raw frontmatter string (without --- delimiters), or null if no frontmatter */\n frontmatter: string | null;\n /** Body content after frontmatter, with leading newlines trimmed */\n body: string;\n}\n\n/**\n * Normalize line endings to LF.\n */\nexport function normalizeLineEndings(content: string): string {\n return content.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n}\n\n/**\n * Parse markdown content into frontmatter and body.\n * Handles both LF and CRLF line endings.\n *\n * @returns Object with frontmatter (null if none) and body\n */\nexport function parseMarkdown(content: string): ParsedMarkdown {\n const normalized = normalizeLineEndings(content);\n\n if (!matter.test(normalized)) {\n return { frontmatter: null, body: content };\n }\n\n try {\n const parsed = matter(normalized);\n\n // Extract frontmatter from parsed.data by stringifying back to YAML\n // The matter property is unreliable, so we reconstruct from data\n const data = parsed.data;\n let frontmatter: string | null = null;\n\n if (data && Object.keys(data).length > 0) {\n // Use centralized yaml-utils for proper handling of special characters\n // (colons, quotes, multiline strings, etc.)\n frontmatter = stringifyYamlCompact(data).trimEnd();\n } else {\n // Empty frontmatter (just --- followed by ---)\n frontmatter = '';\n }\n\n // Body with leading newlines trimmed\n const body = parsed.content.replace(/^\\n+/, '');\n\n return { frontmatter, body };\n } catch {\n // Invalid/unclosed frontmatter - treat as no frontmatter\n return { frontmatter: null, body: content };\n }\n}\n\n/**\n * Parse YAML frontmatter from markdown content.\n * Returns the frontmatter content (without delimiters) or null if no valid frontmatter.\n * Handles both LF and CRLF line endings.\n */\nexport function parseFrontmatter(content: string): string | null {\n return parseMarkdown(content).frontmatter;\n}\n\n/**\n * Strip YAML frontmatter from markdown content.\n * Returns the body content without frontmatter, with leading newlines trimmed.\n * Handles both LF and CRLF line endings.\n */\nexport function stripFrontmatter(content: string): string {\n return parseMarkdown(content).body;\n}\n\n/**\n * Insert content after YAML frontmatter.\n * If no frontmatter exists, prepends the content.\n * Content is inserted directly after ---. Include leading newlines in toInsert if needed.\n */\nexport function insertAfterFrontmatter(content: string, toInsert: string): string {\n const { frontmatter, body } = parseMarkdown(content);\n\n if (frontmatter === null) {\n return toInsert + content;\n }\n\n const frontmatterBlock = frontmatter ? `---\\n${frontmatter}\\n---` : '---\\n---';\n return `${frontmatterBlock}\\n${toInsert}\\n\\n${body}`;\n}\n","/**\n * YAML front matter parser and serializer for issue files.\n *\n * Issues are stored as Markdown files with YAML front matter:\n * ---\n * type: is\n * id: is-a1b2c3\n * ...\n * ---\n *\n * Description body here.\n *\n * ## Notes\n *\n * Working notes here.\n *\n * See: tbd-design.md §2.1 Markdown + YAML Front Matter Format\n */\n\nimport matter from 'gray-matter';\nimport { parse as parseYaml } from 'yaml';\n\nimport { normalizeLineEndings } from '../utils/markdown-utils.js';\nimport { sortKeys, stringifyYaml } from '../utils/yaml-utils.js';\nimport type { Issue } from '../lib/types.js';\nimport { IssueSchema, ISSUE_FIELD_ORDER } from '../lib/schemas.js';\n\n/**\n * gray-matter options using the 'yaml' package as engine.\n * This preserves date strings instead of converting them to Date objects.\n */\nexport const matterOptions = {\n engines: {\n yaml: {\n parse: (str: string): object => parseYaml(str) as object,\n stringify: (obj: object): string => stringifyYaml(obj),\n },\n },\n};\n\n/**\n * Parsed issue file content.\n */\nexport interface ParsedIssueFile {\n frontmatter: Record<string, unknown>;\n description: string;\n notes: string;\n}\n\n/**\n * Parse a Markdown file with YAML front matter.\n * Uses gray-matter for consistent frontmatter parsing.\n * Handles both LF and CRLF line endings.\n */\nexport function parseMarkdownWithFrontmatter(content: string): ParsedIssueFile {\n // Normalize CRLF to LF before parsing\n const normalizedContent = normalizeLineEndings(content);\n\n // Check for valid frontmatter\n if (!matter.test(normalizedContent)) {\n throw new Error('Invalid format: missing front matter opening delimiter');\n }\n\n const parsed = matter(normalizedContent, matterOptions);\n\n // gray-matter returns empty object if no closing delimiter found\n // but the raw matter string will be empty if parsing failed\n if (parsed.matter === '' && !normalizedContent.includes('---\\n---')) {\n // Check if there's actually a closing delimiter\n const lines = normalizedContent.split('\\n');\n let hasClosing = false;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i]?.trim() === '---') {\n hasClosing = true;\n break;\n }\n }\n if (!hasClosing) {\n throw new Error('Invalid format: missing front matter closing delimiter');\n }\n }\n\n const frontmatter = parsed.data as Record<string, unknown>;\n\n // Parse body - split into description and notes\n const body = parsed.content.trim();\n\n // Find notes section\n const notesMatch = /\\n## Notes\\n/i.exec(body);\n let description = body;\n let notes = '';\n\n if (notesMatch?.index !== undefined) {\n description = body.slice(0, notesMatch.index).trim();\n notes = body.slice(notesMatch.index + notesMatch[0].length).trim();\n }\n\n return { frontmatter, description, notes };\n}\n\n/**\n * Parse an issue from Markdown file content.\n */\nexport function parseIssue(content: string): Issue {\n const { frontmatter, description, notes } = parseMarkdownWithFrontmatter(content);\n\n // Merge body content into frontmatter\n const data = {\n ...frontmatter,\n description: description || undefined,\n notes: notes || undefined,\n };\n\n // Validate and parse with Zod\n return IssueSchema.parse(data);\n}\n\n/**\n * Serialize an issue to Markdown file content.\n * Uses canonical serialization for deterministic output.\n */\nexport function serializeIssue(issue: Issue): string {\n // Extract body fields\n const { description, notes, ...metadata } = issue;\n\n // Sort keys using canonical field order (not alphabetical)\n const sortedMetadata = sortKeys(metadata, ISSUE_FIELD_ORDER);\n\n // Serialize YAML with compact output for frontmatter.\n // sortMapEntries: false preserves our manual ordering.\n const yaml = stringifyYaml(sortedMetadata, {\n lineWidth: 0,\n nullStr: 'null',\n sortMapEntries: false,\n });\n\n // Build the file content\n // Note: No blank line between closing --- and body content\n const parts = ['---', yaml.trim(), '---'];\n\n if (description) {\n parts.push(description.trim());\n }\n\n if (notes) {\n parts.push('');\n parts.push('## Notes');\n parts.push('');\n parts.push(notes.trim());\n }\n\n // Single newline at end\n return parts.join('\\n') + '\\n';\n}\n","/**\n * tbd: Git-native issue tracking for AI agents and humans\n *\n * This is the library entry point. All exports here should be node-free\n * to support browser/edge runtime usage. CLI-specific code is in ./cli/.\n */\n\n// Version injected at build time\ndeclare const __TBD_VERSION__: string;\n\n/**\n * Package version, derived from git at build time.\n * Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.\n */\nexport const VERSION: string =\n typeof __TBD_VERSION__ !== 'undefined' ? __TBD_VERSION__ : 'development';\n\n// Re-export schemas for library consumers\nexport * from './lib/schemas.js';\nexport * from './lib/types.js';\n\n// Re-export core operations (these should be node-free)\nexport { parseIssue, serializeIssue } from './file/parser.js';\n"],"mappings":";;;;;;;;;;AA4KA,MAAM,aAAa;AACnB,MAAa,aAA8B;CACzC,UAAU;CACV,MAAM;CACN,MAAM;CACN,OAAO;CACR;;;;;;;;;;;;;AC7JD,SAAgB,qBAAqB,SAAyB;AAC5D,QAAO,QAAQ,QAAQ,SAAS,KAAK,CAAC,QAAQ,OAAO,KAAK;;;;;;;;AAS5D,SAAgB,cAAc,SAAiC;CAC7D,MAAM,aAAa,qBAAqB,QAAQ;AAEhD,KAAI,CAAC,OAAO,KAAK,WAAW,CAC1B,QAAO;EAAE,aAAa;EAAM,MAAM;EAAS;AAG7C,KAAI;EACF,MAAM,SAAS,OAAO,WAAW;EAIjC,MAAM,OAAO,OAAO;EACpB,IAAI,cAA6B;AAEjC,MAAI,QAAQ,OAAO,KAAK,KAAK,CAAC,SAAS,EAGrC,eAAc,qBAAqB,KAAK,CAAC,SAAS;MAGlD,eAAc;EAIhB,MAAM,OAAO,OAAO,QAAQ,QAAQ,QAAQ,GAAG;AAE/C,SAAO;GAAE;GAAa;GAAM;SACtB;AAEN,SAAO;GAAE,aAAa;GAAM,MAAM;GAAS;;;;;;;;AAkB/C,SAAgB,iBAAiB,SAAyB;AACxD,QAAO,cAAc,QAAQ,CAAC;;;;;;;AAQhC,SAAgB,uBAAuB,SAAiB,UAA0B;CAChF,MAAM,EAAE,aAAa,SAAS,cAAc,QAAQ;AAEpD,KAAI,gBAAgB,KAClB,QAAO,WAAW;AAIpB,QAAO,GADkB,cAAc,QAAQ,YAAY,SAAS,WACzC,IAAI,SAAS,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjEhD,MAAa,gBAAgB,EAC3B,SAAS,EACP,MAAM;CACJ,QAAQ,QAAwBA,MAAU,IAAI;CAC9C,YAAY,QAAwB,cAAc,IAAI;CACvD,EACF,EACF;;;;;;AAgBD,SAAgB,6BAA6B,SAAkC;CAE7E,MAAM,oBAAoB,qBAAqB,QAAQ;AAGvD,KAAI,CAAC,OAAO,KAAK,kBAAkB,CACjC,OAAM,IAAI,MAAM,yDAAyD;CAG3E,MAAM,SAAS,OAAO,mBAAmB,cAAc;AAIvD,KAAI,OAAO,WAAW,MAAM,CAAC,kBAAkB,SAAS,WAAW,EAAE;EAEnE,MAAM,QAAQ,kBAAkB,MAAM,KAAK;EAC3C,IAAI,aAAa;AACjB,OAAK,IAAI,IAAI,GAAG,IAAI,MAAM,QAAQ,IAChC,KAAI,MAAM,IAAI,MAAM,KAAK,OAAO;AAC9B,gBAAa;AACb;;AAGJ,MAAI,CAAC,WACH,OAAM,IAAI,MAAM,yDAAyD;;CAI7E,MAAM,cAAc,OAAO;CAG3B,MAAM,OAAO,OAAO,QAAQ,MAAM;CAGlC,MAAM,aAAa,gBAAgB,KAAK,KAAK;CAC7C,IAAI,cAAc;CAClB,IAAI,QAAQ;AAEZ,KAAI,YAAY,UAAU,QAAW;AACnC,gBAAc,KAAK,MAAM,GAAG,WAAW,MAAM,CAAC,MAAM;AACpD,UAAQ,KAAK,MAAM,WAAW,QAAQ,WAAW,GAAG,OAAO,CAAC,MAAM;;AAGpE,QAAO;EAAE;EAAa;EAAa;EAAO;;;;;AAM5C,SAAgB,WAAW,SAAwB;CACjD,MAAM,EAAE,aAAa,aAAa,UAAU,6BAA6B,QAAQ;CAGjF,MAAM,OAAO;EACX,GAAG;EACH,aAAa,eAAe;EAC5B,OAAO,SAAS;EACjB;AAGD,QAAO,YAAY,MAAM,KAAK;;;;;;AAOhC,SAAgB,eAAe,OAAsB;CAEnD,MAAM,EAAE,aAAa,OAAO,GAAG,aAAa;CAe5C,MAAM,QAAQ;EAAC;EARF,cAJU,SAAS,UAAU,kBAAkB,EAIjB;GACzC,WAAW;GACX,SAAS;GACT,gBAAgB;GACjB,CAAC,CAIyB,MAAM;EAAE;EAAM;AAEzC,KAAI,YACF,OAAM,KAAK,YAAY,MAAM,CAAC;AAGhC,KAAI,OAAO;AACT,QAAM,KAAK,GAAG;AACd,QAAM,KAAK,WAAW;AACtB,QAAM,KAAK,GAAG;AACd,QAAM,KAAK,MAAM,MAAM,CAAC;;AAI1B,QAAO,MAAM,KAAK,KAAK,GAAG;;;;;;;;;AC1I5B,MAAa"}
|
package/dist/tbd
CHANGED
|
@@ -14067,7 +14067,7 @@ function serializeIssue(issue) {
|
|
|
14067
14067
|
* Package version, derived from git at build time.
|
|
14068
14068
|
* Format: X.Y.Z for releases, X.Y.Z-dev.N.hash for dev builds.
|
|
14069
14069
|
*/
|
|
14070
|
-
const VERSION$1 = "0.1.
|
|
14070
|
+
const VERSION$1 = "0.1.29";
|
|
14071
14071
|
|
|
14072
14072
|
//#endregion
|
|
14073
14073
|
//#region src/cli/lib/version.ts
|
|
@@ -107283,7 +107283,7 @@ async function loadSkillContent() {
|
|
|
107283
107283
|
const docsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "docs");
|
|
107284
107284
|
const headerPath = join(docsDir, "install", "claude-header.md");
|
|
107285
107285
|
const skillPath = join(docsDir, "shortcuts", "system", "skill-baseline.md");
|
|
107286
|
-
return await readFile(headerPath, "utf-8") + await readFile(skillPath, "utf-8");
|
|
107286
|
+
return await readFile(headerPath, "utf-8") + stripFrontmatter(await readFile(skillPath, "utf-8"));
|
|
107287
107287
|
} catch {
|
|
107288
107288
|
throw new Error("SKILL.md content file not found. Please rebuild the CLI.");
|
|
107289
107289
|
}
|
|
@@ -107566,7 +107566,7 @@ var SkillHandler = class extends BaseCommand {
|
|
|
107566
107566
|
*/
|
|
107567
107567
|
async composeFullSkill() {
|
|
107568
107568
|
const header = await loadDocContent("install/claude-header.md");
|
|
107569
|
-
const baseSkill = await loadDocContent("shortcuts/system/skill-baseline.md");
|
|
107569
|
+
const baseSkill = stripFrontmatter(await loadDocContent("shortcuts/system/skill-baseline.md"));
|
|
107570
107570
|
const directory = await this.getShortcutDirectory();
|
|
107571
107571
|
let result = header + baseSkill;
|
|
107572
107572
|
if (directory) result = result.trimEnd() + "\n\n" + directory;
|
package/package.json
CHANGED