politty 0.9.0 → 0.9.2

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.
@@ -0,0 +1,432 @@
1
+ import { _ as AnyCommand } from "../arg-registry-DDJpsUea.js";
2
+ import { z } from "zod";
3
+
4
+ //#region src/skill/frontmatter.d.ts
5
+ /**
6
+ * Zod schema for SKILL.md frontmatter.
7
+ *
8
+ * Strictly validates the fields defined in the Agent Skills specification
9
+ * (https://agentskills.io/specification). Unknown fields are preserved via
10
+ * `.passthrough()` so spec extensions and vendor keys round-trip intact.
11
+ *
12
+ * Provenance / ownership for politty-managed installs is recorded under
13
+ * `metadata["politty-cli"]` as `"{packageName}:{cliName}"`.
14
+ */
15
+ declare const skillFrontmatterSchema: z.ZodObject<{
16
+ name: z.ZodString;
17
+ description: z.ZodString;
18
+ license: z.ZodOptional<z.ZodString>;
19
+ compatibility: z.ZodOptional<z.ZodString>;
20
+ metadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
21
+ "allowed-tools": z.ZodOptional<z.ZodString>;
22
+ }, z.core.$loose>;
23
+ /**
24
+ * Result of parsing a SKILL.md file.
25
+ */
26
+ interface ParsedSkillMd {
27
+ /** Parsed and validated frontmatter */
28
+ frontmatter: z.infer<typeof skillFrontmatterSchema>;
29
+ /** Markdown body (content after frontmatter) */
30
+ body: string;
31
+ /** Full raw content */
32
+ rawContent: string;
33
+ }
34
+ /**
35
+ * Parse YAML frontmatter from a SKILL.md string.
36
+ *
37
+ * `parseError` is set when the frontmatter fence was present but the YAML
38
+ * inside failed to parse, so the scanner can distinguish "invalid YAML"
39
+ * from "missing required field" in its diagnostics. A non-object root
40
+ * (e.g. a top-level YAML list) also returns empty `data` without
41
+ * `parseError` — Zod's schema validation surfaces that case clearly
42
+ * enough on its own.
43
+ *
44
+ * @example
45
+ * ```typescript
46
+ * const result = parseFrontmatter(`---
47
+ * name: commit
48
+ * description: Git commit message generation
49
+ * ---
50
+ * # Instructions...`);
51
+ *
52
+ * result.data.name; // "commit"
53
+ * ```
54
+ */
55
+ declare function parseFrontmatter(content: string): {
56
+ data: Record<string, unknown>;
57
+ body: string;
58
+ parseError?: string;
59
+ };
60
+ /**
61
+ * Parse and validate a SKILL.md content string.
62
+ *
63
+ * @returns Parsed skill metadata and body, or `null` if the frontmatter is
64
+ * missing or fails schema validation.
65
+ */
66
+ declare function parseSkillMd(content: string): ParsedSkillMd | null;
67
+ //#endregion
68
+ //#region src/skill/types.d.ts
69
+ /**
70
+ * SKILL.md frontmatter metadata, validated against the Agent Skills
71
+ * specification (https://agentskills.io/specification).
72
+ *
73
+ * Provenance for politty-managed installs is recorded under
74
+ * `metadata["politty-cli"]` as `"{packageName}:{cliName}"`.
75
+ */
76
+ type SkillFrontmatter = z.infer<typeof skillFrontmatterSchema>;
77
+ /**
78
+ * A skill discovered from a source directory (npm package).
79
+ */
80
+ interface DiscoveredSkill {
81
+ /** Parsed frontmatter metadata */
82
+ frontmatter: SkillFrontmatter;
83
+ /** Path to the directory containing SKILL.md */
84
+ sourcePath: string;
85
+ /** Raw SKILL.md content (frontmatter + body) */
86
+ rawContent: string;
87
+ }
88
+ /**
89
+ * All kinds of scan failure, as a runtime tuple so callers can exhaustively
90
+ * iterate (e.g. for message tables). Derived {@link ScanErrorReason} stays
91
+ * the single source of truth for the type-level enum.
92
+ */
93
+ declare const SCAN_ERROR_REASONS: readonly ["parse-failed", "name-mismatch", "read-failed", "missing-source"];
94
+ /** Kind of problem encountered by {@link scanSourceDir}. */
95
+ type ScanErrorReason = (typeof SCAN_ERROR_REASONS)[number];
96
+ /**
97
+ * A non-fatal problem encountered while scanning a source directory.
98
+ *
99
+ * Scan errors (invalid frontmatter, name/parent-dir mismatch, unreadable
100
+ * files) are collected rather than thrown so that a single malformed skill
101
+ * does not hide the rest from CLI commands.
102
+ */
103
+ interface ScanError {
104
+ /** Directory that produced the error. */
105
+ path: string;
106
+ /** Kind of problem encountered. */
107
+ reason: ScanErrorReason;
108
+ /** Human-readable detail, suitable for logging. */
109
+ message: string;
110
+ /**
111
+ * Parsed frontmatter `name`, when the scan got far enough to read it.
112
+ * Currently populated only for `name-mismatch` — both the directory
113
+ * basename and this frontmatter name correspond to plausible existing
114
+ * install slot names (depending on which side the user just renamed),
115
+ * so `sync`'s orphan-retention guard needs both to avoid reaping an
116
+ * installed slot belonging to a source skill that failed this scan.
117
+ */
118
+ skillName?: string;
119
+ }
120
+ /**
121
+ * Result of scanning a source directory for SKILL.md files.
122
+ */
123
+ interface ScanResult {
124
+ /** Valid, spec-compliant skills. */
125
+ skills: DiscoveredSkill[];
126
+ /** Directories that looked like skills but failed validation. */
127
+ errors: ScanError[];
128
+ }
129
+ /**
130
+ * How an install materializes skill files under `.agents/skills/<name>`
131
+ * and each `SYMLINK_TARGETS` entry.
132
+ *
133
+ * - `"symlink"` (default): symlink the source into place. Source updates
134
+ * propagate live. Throws with guidance to retry with `"copy"` when
135
+ * `symlinkSync` fails (e.g. Windows without Developer Mode, filesystems
136
+ * that do not support symlinks).
137
+ * - `"copy"`: recursive copy. Source updates require re-running `skills
138
+ * sync`. Works on any filesystem, trades liveness for portability.
139
+ */
140
+ type InstallMode = "symlink" | "copy";
141
+ /** Options for {@link installSkill}. */
142
+ interface InstallSkillOptions {
143
+ /** Install materialization strategy. Default: `"symlink"`. */
144
+ mode?: InstallMode;
145
+ }
146
+ /** Options for {@link uninstallSkill}. */
147
+ interface UninstallSkillOptions {
148
+ /**
149
+ * If set, `uninstallSkill` also removes a real directory at the install
150
+ * path when its SKILL.md's `metadata["politty-cli"]` matches this stamp
151
+ * (a copy-mode install this CLI owns). Without this option, only
152
+ * symlinks are removed — real directories are assumed to be legacy or
153
+ * manual installs and left untouched.
154
+ */
155
+ expectedOwnership?: string;
156
+ }
157
+ /**
158
+ * Per-flag overrides for the built-in skill subcommand options.
159
+ *
160
+ * Pass through {@link SkillCommandOptions.flags} to resolve collisions with
161
+ * CLI-level global flags. Setting `alias` to `false` disables the short
162
+ * alias entirely; passing a string renames it.
163
+ */
164
+ interface SkillFlagOverrides {
165
+ /**
166
+ * `--exclude` flag on `skills sync`. The default short alias is `-x`.
167
+ */
168
+ exclude?: {
169
+ alias?: string | false;
170
+ };
171
+ }
172
+ /**
173
+ * Options for `withSkillCommand`.
174
+ */
175
+ interface SkillCommandOptions {
176
+ /**
177
+ * Source directory containing SKILL.md files.
178
+ *
179
+ * Each subdirectory whose name matches its `SKILL.md` frontmatter `name`
180
+ * is treated as a skill. Symlinks within the source tree are followed.
181
+ *
182
+ * @example
183
+ * ```typescript
184
+ * // Resolves to ../skills relative to the current file.
185
+ * // Works from both src/ and dist/ if at the same depth.
186
+ * const sourceDir = resolve(dirname(fileURLToPath(import.meta.url)), "../skills");
187
+ * ```
188
+ */
189
+ sourceDir: string;
190
+ /**
191
+ * npm package name that owns this CLI's bundled skills.
192
+ *
193
+ * Each source `SKILL.md` must pre-declare
194
+ * `metadata["politty-cli"]: "{package}:{cliName}"`. The `skills add` and
195
+ * `skills sync` subcommands verify this stamp before installing — two
196
+ * tools managing skills in the same project cannot accidentally clobber
197
+ * each other. `installSkill` itself does not compare ownership;
198
+ * programmatic callers that bypass `withSkillCommand` are responsible
199
+ * for matching the stamp against their own `{package}:{cliName}` up
200
+ * front. (In `mode: "copy"`, `installSkill` additionally requires
201
+ * *some* `politty-cli` stamp on the source and throws otherwise — the
202
+ * caller-side ownership check naturally satisfies that precondition.)
203
+ */
204
+ package: string;
205
+ /**
206
+ * Default install mode for the `skills add` and `skills sync` commands.
207
+ * Defaults to `"symlink"` — install fails with a clear error on
208
+ * filesystems without symlink support (e.g. Windows without Developer
209
+ * Mode). Set to `"copy"` to always copy. See {@link InstallMode}.
210
+ */
211
+ mode?: InstallMode;
212
+ /**
213
+ * Project root directory used by every `skills` subcommand for resolving
214
+ * `.agents/skills/...` install paths.
215
+ *
216
+ * Default: walk up from `process.cwd()` and use the first ancestor that
217
+ * contains `.git/` or `package.json`; fall back to `process.cwd()` when
218
+ * neither is found. This avoids creating `<sub>/.agents/skills/...` when
219
+ * the CLI is invoked from a subdirectory of the project.
220
+ *
221
+ * Pass an explicit absolute (or cwd-relative) path to override — for
222
+ * example, the directory of a CLI-specific config file.
223
+ */
224
+ cwd?: string;
225
+ /**
226
+ * Customize built-in subcommand flags. Use to resolve collisions with
227
+ * the CLI's global flags or to opt out of short aliases.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * // CLI already uses -x globally; rename the exclude alias.
232
+ * withSkillCommand(cmd, {
233
+ * sourceDir, package: "@my-agent/skills",
234
+ * flags: { exclude: { alias: "X" } },
235
+ * });
236
+ *
237
+ * // Disable the short alias entirely.
238
+ * withSkillCommand(cmd, {
239
+ * sourceDir, package: "@my-agent/skills",
240
+ * flags: { exclude: { alias: false } },
241
+ * });
242
+ * ```
243
+ */
244
+ flags?: SkillFlagOverrides;
245
+ /**
246
+ * Append a one-line skills usage hint to the wrapped command's
247
+ * `description` so `--help` advertises the skills subcommand.
248
+ *
249
+ * - `undefined` (default) — append a default hint mentioning the
250
+ * available subcommands.
251
+ * - `string` — append this exact string instead.
252
+ * - `false` — leave the description untouched.
253
+ */
254
+ descriptionAppend?: string | false;
255
+ }
256
+ //#endregion
257
+ //#region src/skill/installer.d.ts
258
+ /**
259
+ * Key used to read provenance off an installed skill. The SKILL.md's
260
+ * `metadata["politty-cli"]` must equal `"{packageName}:{cliName}"` for the
261
+ * owning CLI to manage it. This stamp is authored by the skill package,
262
+ * not rewritten at install time.
263
+ */
264
+ declare const OWNERSHIP_METADATA_KEY = "politty-cli";
265
+ /**
266
+ * Install a skill to the project's agent skill directories.
267
+ *
268
+ * Canonical `.agents/skills/<name>` and each `SYMLINK_TARGETS` entry are
269
+ * populated according to `options.mode`:
270
+ *
271
+ * - `"symlink"` (default): symlink to the source (or to the canonical dir
272
+ * for the agent-specific slots). Source updates propagate live. Throws
273
+ * with guidance to retry with `"copy"` on filesystems without symlink
274
+ * support (e.g. Windows without Developer Mode).
275
+ * - `"copy"`: recursive copy. Works anywhere, but source updates require
276
+ * re-running install.
277
+ *
278
+ * **Symlink target convention.** Symlinks are written with relative
279
+ * targets so an install survives when the project tree is copied or
280
+ * mounted at a different absolute path. The two endpoints are resolved
281
+ * asymmetrically:
282
+ *
283
+ * - The install root (`.agents/skills/`, each `SYMLINK_TARGETS` parent)
284
+ * is passed through `realpathSync` so a symlinked checkout doesn't
285
+ * bake a stale parent path into the relative target.
286
+ * - The source path is resolved by {@link resolveSourcePreservingPackageHop},
287
+ * which walks root→leaf dereferencing every ancestor symlink (a symlinked
288
+ * checkout, macOS `/tmp` → `/private/tmp`, etc.) like `realpathSync` would
289
+ * — EXCEPT a `node_modules/<pkg>` (or `node_modules/@scope/<pkg>`)
290
+ * symlink, which is preserved. Following the package-manager hop would
291
+ * bake pnpm's volatile `node_modules/.pnpm/<pkg>@<version>_<hash>/...`
292
+ * into the link target and a subsequent `pnpm update` would leave every
293
+ * install dangling. Keeping the hop preserves the stable
294
+ * `node_modules/<pkg>` symlink that pnpm keeps repointing, while the
295
+ * project-root portion still matches the install root's realpath style
296
+ * so the install survives copying or remounting the project tree.
297
+ *
298
+ * The overlap guard and copy-mode payload still use the *fully*
299
+ * `realpathSync`-resolved source: the guard must catch a source-side
300
+ * symlink whose target is nested inside the install root, and the
301
+ * copy-mode payload reads through every symlink so a copy install doesn't
302
+ * leave dangling references back into `node_modules`.
303
+ *
304
+ * No absolute-path symlinks are produced by this function.
305
+ *
306
+ * **Atomicity.** This call is *not* transactional across multi-step
307
+ * installs. The canonical slot is cleared then written, and each
308
+ * `SYMLINK_TARGETS` slot is then cleared and written one at a time. A
309
+ * crash mid-install can leave the canonical slot updated and one or
310
+ * more agent-specific slots stale; re-running `installSkill` (or the
311
+ * `skills sync` subcommand, which iterates over multiple skills) is
312
+ * idempotent and converges back to the intended state. Within a single
313
+ * slot's copy-mode write, the staging-and-rename in {@link atomicCopyDir}
314
+ * guarantees the destination is either absent or fully populated — a
315
+ * mid-copy failure never leaves a stamp-less partial directory that the
316
+ * next install's `clearInstallSlot` would refuse to replace. Multi-skill
317
+ * orchestration in {@link createSkillSyncCommand} is fail-fast — the
318
+ * first failed skill aborts the loop without rolling back already-
319
+ * installed siblings, again because re-running converges.
320
+ *
321
+ * The ownership stamp (`metadata["politty-cli"]`) is authored by the skill
322
+ * package; the installer does not modify SKILL.md.
323
+ */
324
+ declare function installSkill(skill: DiscoveredSkill, cwd?: string, options?: InstallSkillOptions): void;
325
+ /**
326
+ * Uninstall a skill from the project's agent skill directories.
327
+ *
328
+ * Each slot is unlinked only when its ownership can be proven:
329
+ * - Agent-specific symlink slots (`.claude/skills/<name>` etc.) — a live
330
+ * symlink is unlinked only when it routes to our canonical slot, so a
331
+ * foreign tool's symlink at the same shared path is left untouched.
332
+ * - The canonical slot (`.agents/skills/<name>`) — a live symlink is
333
+ * unlinked only when its routed-to SKILL.md carries
334
+ * `options.expectedOwnership`, so another politty-based CLI's live
335
+ * install in the same shared namespace is left untouched.
336
+ * - Real directories at any slot are removed only when the directory's
337
+ * SKILL.md carries `options.expectedOwnership`. Unstamped or foreign
338
+ * real directories are left alone so legacy/manual installs are not
339
+ * silently recursively deleted.
340
+ *
341
+ * `skills remove` / `skills sync` always pass `expectedOwnership`. Direct
342
+ * programmatic callers that omit it get the legacy permissive behaviour
343
+ * on symlinks (unconditional unlink) but the conservative behaviour on
344
+ * real directories (no-op). Broken (dangling) canonical symlinks are
345
+ * outside this function's purview — they have no SKILL.md to read, so
346
+ * `cleanupBrokenSlot` handles them with a routing check instead.
347
+ */
348
+ declare function uninstallSkill(name: string, cwd?: string, options?: UninstallSkillOptions): void;
349
+ /**
350
+ * Report whether a skill is currently installed, independent of its
351
+ * ownership stamp. Returns `true` when `.agents/skills/<name>/SKILL.md`
352
+ * resolves to a readable file (through a valid symlink or directly, or
353
+ * via a copy-mode install); returns `false` when the path is absent or
354
+ * the canonical symlink is broken (source package uninstalled).
355
+ *
356
+ * Callers use this to distinguish the two cases where
357
+ * {@link readInstalledOwnership} returns `null` — "not installed" (safe
358
+ * to install fresh) vs. "installed but unstamped" (legacy or manual
359
+ * install that should not be silently clobbered).
360
+ */
361
+ declare function hasInstalledSkill(name: string, cwd?: string): boolean;
362
+ /**
363
+ * Read the ownership stamp off an installed skill's SKILL.md, if any.
364
+ *
365
+ * For symlink-mode installs `.agents/skills/<name>` points at the source,
366
+ * so this reads the package-authored stamp. For copy-mode installs the
367
+ * stamp was captured at install time into the local copy.
368
+ *
369
+ * @returns `metadata["politty-cli"]` as `"{packageName}:{cliName}"`, or
370
+ * `null` if the skill is not installed *or* the stamp is absent/malformed.
371
+ * Use {@link hasInstalledSkill} to distinguish the two cases.
372
+ */
373
+ declare function readInstalledOwnership(name: string, cwd?: string): string | null;
374
+ //#endregion
375
+ //#region src/skill/scanner.d.ts
376
+ /**
377
+ * Scan a source directory for SKILL.md files.
378
+ *
379
+ * Each immediate subdirectory is a candidate skill; its `SKILL.md` is
380
+ * parsed and validated against the Agent Skills specification, and the
381
+ * frontmatter `name` must match the subdirectory name (spec requirement).
382
+ *
383
+ * If `sourceDir` itself contains a `SKILL.md`, it is treated as a
384
+ * single-skill source. The parent-directory-name match is not enforced in
385
+ * that case because the caller chose an arbitrary path.
386
+ *
387
+ * Symlinks within the source tree are followed (symlinked skill dirs and
388
+ * symlinked SKILL.md files are both accepted). npm packages already
389
+ * execute arbitrary JS on install, so additional symlink-based isolation
390
+ * here would not raise the trust boundary in any realistic threat model.
391
+ *
392
+ * @example
393
+ * ```
394
+ * sourceDir: "node_modules/@my-agent/skills/skills"
395
+ *
396
+ * node_modules/@my-agent/skills/skills/
397
+ * ├── commit/
398
+ * │ └── SKILL.md
399
+ * └── review-pr/
400
+ * └── SKILL.md
401
+ * ```
402
+ */
403
+ declare function scanSourceDir(sourceDir: string): ScanResult;
404
+ //#endregion
405
+ //#region src/skill/index.d.ts
406
+ /**
407
+ * Wrap a command with a `skills` subcommand for managing SKILL.md-based skills.
408
+ *
409
+ * Adds `skills sync`, `skills add`, `skills remove`, and `skills list`.
410
+ * The install materialization is controlled by `options.mode`
411
+ * (see {@link SkillCommandOptions}):
412
+ *
413
+ * - `"symlink"` (default) — symlink the source into place. Source updates
414
+ * propagate live. Install errors out with guidance to retry with `"copy"`
415
+ * when `symlinkSync` fails (e.g. Windows without Developer Mode).
416
+ * - `"copy"` — recursive copy. Source updates require re-running sync.
417
+ *
418
+ * Under both modes the canonical slot is `.agents/skills/<name>` and each
419
+ * agent-specific directory (e.g. `.claude/skills/<name>`) is populated
420
+ * from that canonical slot. politty never writes to `SKILL.md`. The
421
+ * ownership stamp `metadata["politty-cli"] = "{package}:{cliName}"` must
422
+ * be authored by the skill package itself; `add` and `sync` verify it
423
+ * before installing and `remove` / `sync` consult it before deleting, so
424
+ * this CLI never clobbers skills another tool installed.
425
+ *
426
+ * @throws if `command.subCommands.skills` already exists — silently
427
+ * overwriting it would hide a configuration bug.
428
+ */
429
+ declare function withSkillCommand<T extends AnyCommand>(command: T, options: SkillCommandOptions): T;
430
+ //#endregion
431
+ export { type DiscoveredSkill, type InstallMode, type InstallSkillOptions, OWNERSHIP_METADATA_KEY, type ParsedSkillMd, SCAN_ERROR_REASONS, type ScanError, type ScanErrorReason, type ScanResult, type SkillCommandOptions, type SkillFlagOverrides, type SkillFrontmatter, type UninstallSkillOptions, hasInstalledSkill, installSkill, parseFrontmatter, parseSkillMd, readInstalledOwnership, scanSourceDir, skillFrontmatterSchema, uninstallSkill, withSkillCommand };
432
+ //# sourceMappingURL=index.d.ts.map