playbooks 0.1.7 → 0.1.9
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/README.md +38 -2
- package/dist/index.js +510 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Skills let your agents perform specialized tasks like:
|
|
|
39
39
|
|
|
40
40
|
## Usage
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
playbooks uses an action/type command structure:
|
|
43
43
|
- `npx playbooks add skill <source>`
|
|
44
44
|
- `npx playbooks find skill`
|
|
45
45
|
- `npx playbooks list skill`
|
|
@@ -94,6 +94,12 @@ npx playbooks add skill git@github.com:anthropics/skills.git
|
|
|
94
94
|
npx playbooks add skill https://docs.example.com/skills/my-skill/SKILL.md
|
|
95
95
|
```
|
|
96
96
|
|
|
97
|
+
- Docs URL (well-known skills discovery)
|
|
98
|
+
```bash
|
|
99
|
+
npx playbooks add skill https://mintlify.com/docs
|
|
100
|
+
npx playbooks add skill mintlify.com/docs
|
|
101
|
+
```
|
|
102
|
+
|
|
97
103
|
- Marketplace.json (path)
|
|
98
104
|
```bash
|
|
99
105
|
npx playbooks add skill ./path/to/.claude-plugin/marketplace.json
|
|
@@ -109,6 +115,36 @@ npx playbooks add skill https://raw.githubusercontent.com/org/repo/main/.claude-
|
|
|
109
115
|
npx playbooks add skill org/repo/.claude-plugin/marketplace.json
|
|
110
116
|
```
|
|
111
117
|
|
|
118
|
+
### Well-known skills discovery (RFC 8615)
|
|
119
|
+
|
|
120
|
+
If a docs site publishes a skills index at a predictable path, playbooks can discover and install skills from the site URL directly. The CLI looks for:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
https://example.com/docs/.well-known/skills/index.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The index lists one or more skills and the files for each skill:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"skills": [
|
|
131
|
+
{
|
|
132
|
+
"name": "mintlify",
|
|
133
|
+
"description": "Build and maintain documentation sites with Mintlify.",
|
|
134
|
+
"files": ["SKILL.md"]
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
When you run `npx playbooks add skill <docs-url>`, playbooks fetches the index and then downloads each skill from:
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
https://example.com/docs/.well-known/skills/<skill-name>/SKILL.md
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Multiple skills can be listed in the same index and will be shown in the selection screen.
|
|
147
|
+
|
|
112
148
|
### Options (add skill)
|
|
113
149
|
|
|
114
150
|
| Option | Description |
|
|
@@ -167,7 +203,7 @@ npx playbooks manage skill
|
|
|
167
203
|
|
|
168
204
|
## Marketplace.json support
|
|
169
205
|
|
|
170
|
-
|
|
206
|
+
playbooks can ingest a Claude-style `marketplace.json` and pull skills from the plugins it lists.
|
|
171
207
|
|
|
172
208
|
What it scans:
|
|
173
209
|
- The plugin root (if it contains `SKILL.md`)
|
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { program } from "commander";
|
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "playbooks",
|
|
11
|
-
version: "0.1.
|
|
11
|
+
version: "0.1.9",
|
|
12
12
|
description: "Install agent skills, MCPs and docs into your coding agents from any git repository.",
|
|
13
13
|
type: "module",
|
|
14
14
|
bin: {
|
|
@@ -1759,8 +1759,9 @@ function isDirectSkillUrl(input) {
|
|
|
1759
1759
|
return true;
|
|
1760
1760
|
}
|
|
1761
1761
|
function parseSource(input) {
|
|
1762
|
-
|
|
1763
|
-
|
|
1762
|
+
let normalizedInput = input;
|
|
1763
|
+
if (isLocalPath(normalizedInput)) {
|
|
1764
|
+
const resolvedPath = resolve4(normalizedInput);
|
|
1764
1765
|
return {
|
|
1765
1766
|
type: "local",
|
|
1766
1767
|
url: resolvedPath,
|
|
@@ -1768,13 +1769,18 @@ function parseSource(input) {
|
|
|
1768
1769
|
localPath: resolvedPath
|
|
1769
1770
|
};
|
|
1770
1771
|
}
|
|
1771
|
-
if (
|
|
1772
|
+
if (shouldPrefixHttps(normalizedInput)) {
|
|
1773
|
+
normalizedInput = `https://${normalizedInput}`;
|
|
1774
|
+
}
|
|
1775
|
+
if (isDirectSkillUrl(normalizedInput)) {
|
|
1772
1776
|
return {
|
|
1773
1777
|
type: "direct-url",
|
|
1774
|
-
url:
|
|
1778
|
+
url: normalizedInput
|
|
1775
1779
|
};
|
|
1776
1780
|
}
|
|
1777
|
-
const githubTreeWithPathMatch =
|
|
1781
|
+
const githubTreeWithPathMatch = normalizedInput.match(
|
|
1782
|
+
/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)\/(.+)/
|
|
1783
|
+
);
|
|
1778
1784
|
if (githubTreeWithPathMatch) {
|
|
1779
1785
|
const [, owner, repo, ref, subpath] = githubTreeWithPathMatch;
|
|
1780
1786
|
return {
|
|
@@ -1784,7 +1790,7 @@ function parseSource(input) {
|
|
|
1784
1790
|
subpath
|
|
1785
1791
|
};
|
|
1786
1792
|
}
|
|
1787
|
-
const githubTreeMatch =
|
|
1793
|
+
const githubTreeMatch = normalizedInput.match(/github\.com\/([^/]+)\/([^/]+)\/tree\/([^/]+)$/);
|
|
1788
1794
|
if (githubTreeMatch) {
|
|
1789
1795
|
const [, owner, repo, ref] = githubTreeMatch;
|
|
1790
1796
|
return {
|
|
@@ -1793,7 +1799,7 @@ function parseSource(input) {
|
|
|
1793
1799
|
ref
|
|
1794
1800
|
};
|
|
1795
1801
|
}
|
|
1796
|
-
const githubRepoMatch =
|
|
1802
|
+
const githubRepoMatch = normalizedInput.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
1797
1803
|
if (githubRepoMatch) {
|
|
1798
1804
|
const [, owner, repo] = githubRepoMatch;
|
|
1799
1805
|
const cleanRepo = repo?.replace(/\.git$/, "");
|
|
@@ -1802,7 +1808,7 @@ function parseSource(input) {
|
|
|
1802
1808
|
url: `https://github.com/${owner}/${cleanRepo}.git`
|
|
1803
1809
|
};
|
|
1804
1810
|
}
|
|
1805
|
-
const gitlabTreeWithPathMatch =
|
|
1811
|
+
const gitlabTreeWithPathMatch = normalizedInput.match(
|
|
1806
1812
|
/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)\/(.+)/
|
|
1807
1813
|
);
|
|
1808
1814
|
if (gitlabTreeWithPathMatch) {
|
|
@@ -1814,7 +1820,7 @@ function parseSource(input) {
|
|
|
1814
1820
|
subpath
|
|
1815
1821
|
};
|
|
1816
1822
|
}
|
|
1817
|
-
const gitlabTreeMatch =
|
|
1823
|
+
const gitlabTreeMatch = normalizedInput.match(/gitlab\.com\/([^/]+)\/([^/]+)\/-\/tree\/([^/]+)$/);
|
|
1818
1824
|
if (gitlabTreeMatch) {
|
|
1819
1825
|
const [, owner, repo, ref] = gitlabTreeMatch;
|
|
1820
1826
|
return {
|
|
@@ -1823,7 +1829,7 @@ function parseSource(input) {
|
|
|
1823
1829
|
ref
|
|
1824
1830
|
};
|
|
1825
1831
|
}
|
|
1826
|
-
const gitlabRepoMatch =
|
|
1832
|
+
const gitlabRepoMatch = normalizedInput.match(/gitlab\.com\/([^/]+)\/([^/]+)/);
|
|
1827
1833
|
if (gitlabRepoMatch) {
|
|
1828
1834
|
const [, owner, repo] = gitlabRepoMatch;
|
|
1829
1835
|
const cleanRepo = repo?.replace(/\.git$/, "");
|
|
@@ -1832,8 +1838,8 @@ function parseSource(input) {
|
|
|
1832
1838
|
url: `https://gitlab.com/${owner}/${cleanRepo}.git`
|
|
1833
1839
|
};
|
|
1834
1840
|
}
|
|
1835
|
-
const shorthandMatch =
|
|
1836
|
-
if (shorthandMatch && !
|
|
1841
|
+
const shorthandMatch = normalizedInput.match(/^([^/]+)\/([^/]+)(?:\/(.+))?$/);
|
|
1842
|
+
if (shorthandMatch && !normalizedInput.includes(":") && !normalizedInput.startsWith(".") && !normalizedInput.startsWith("/")) {
|
|
1837
1843
|
const [, owner, repo, subpath] = shorthandMatch;
|
|
1838
1844
|
return {
|
|
1839
1845
|
type: "github",
|
|
@@ -1841,11 +1847,53 @@ function parseSource(input) {
|
|
|
1841
1847
|
subpath
|
|
1842
1848
|
};
|
|
1843
1849
|
}
|
|
1850
|
+
if (isWellKnownUrl(normalizedInput)) {
|
|
1851
|
+
return {
|
|
1852
|
+
type: "well-known",
|
|
1853
|
+
url: normalizedInput
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1844
1856
|
return {
|
|
1845
1857
|
type: "git",
|
|
1846
|
-
url:
|
|
1858
|
+
url: normalizedInput
|
|
1847
1859
|
};
|
|
1848
1860
|
}
|
|
1861
|
+
function isWellKnownUrl(input) {
|
|
1862
|
+
if (!input.startsWith("http://") && !input.startsWith("https://")) {
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
try {
|
|
1866
|
+
const parsed = new URL(input);
|
|
1867
|
+
const excludedHosts = [
|
|
1868
|
+
"github.com",
|
|
1869
|
+
"gitlab.com",
|
|
1870
|
+
"huggingface.co",
|
|
1871
|
+
"raw.githubusercontent.com"
|
|
1872
|
+
];
|
|
1873
|
+
if (excludedHosts.includes(parsed.hostname)) {
|
|
1874
|
+
return false;
|
|
1875
|
+
}
|
|
1876
|
+
if (input.toLowerCase().endsWith("/skill.md")) {
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
if (input.endsWith(".git")) {
|
|
1880
|
+
return false;
|
|
1881
|
+
}
|
|
1882
|
+
return true;
|
|
1883
|
+
} catch {
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
function shouldPrefixHttps(input) {
|
|
1888
|
+
if (input.startsWith("http://") || input.startsWith("https://")) {
|
|
1889
|
+
return false;
|
|
1890
|
+
}
|
|
1891
|
+
const firstSegment = input.split("/")[0] ?? "";
|
|
1892
|
+
if (!firstSegment || firstSegment.includes("@")) {
|
|
1893
|
+
return false;
|
|
1894
|
+
}
|
|
1895
|
+
return firstSegment.includes(".");
|
|
1896
|
+
}
|
|
1849
1897
|
|
|
1850
1898
|
// src/flows/install-tracking.ts
|
|
1851
1899
|
async function recordMarketplaceTracking(skills, results, installGlobally, originBySkillName) {
|
|
@@ -2363,9 +2411,9 @@ function AddScopeScreen() {
|
|
|
2363
2411
|
|
|
2364
2412
|
// src/tui/screens/AddSkillSelect.tsx
|
|
2365
2413
|
import { existsSync as existsSync5 } from "fs";
|
|
2366
|
-
import { mkdir as
|
|
2367
|
-
import { tmpdir as
|
|
2368
|
-
import {
|
|
2414
|
+
import { mkdir as mkdir5, mkdtemp as mkdtemp4, writeFile as writeFile4 } from "fs/promises";
|
|
2415
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
2416
|
+
import { join as join13 } from "path";
|
|
2369
2417
|
import { Box as Box15, Text as Text13 } from "ink";
|
|
2370
2418
|
import React11 from "react";
|
|
2371
2419
|
|
|
@@ -2620,9 +2668,231 @@ var RawSkillProvider = class {
|
|
|
2620
2668
|
};
|
|
2621
2669
|
var rawSkillProvider = new RawSkillProvider();
|
|
2622
2670
|
|
|
2671
|
+
// src/providers/wellknown.ts
|
|
2672
|
+
import matter6 from "gray-matter";
|
|
2673
|
+
var WellKnownProvider = class {
|
|
2674
|
+
id = "well-known";
|
|
2675
|
+
displayName = "Well-Known Skills";
|
|
2676
|
+
WELL_KNOWN_PATH = ".well-known/skills";
|
|
2677
|
+
INDEX_FILE = "index.json";
|
|
2678
|
+
match(url) {
|
|
2679
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
2680
|
+
return { matches: false };
|
|
2681
|
+
}
|
|
2682
|
+
if (!url.includes(`/${this.WELL_KNOWN_PATH}/`)) {
|
|
2683
|
+
return { matches: false };
|
|
2684
|
+
}
|
|
2685
|
+
return { matches: true, sourceIdentifier: this.getSourceIdentifier(url) };
|
|
2686
|
+
}
|
|
2687
|
+
async fetchIndex(baseUrl) {
|
|
2688
|
+
try {
|
|
2689
|
+
const parsed = new URL(baseUrl);
|
|
2690
|
+
const basePath = parsed.pathname.replace(/\/$/, "");
|
|
2691
|
+
const urlsToTry = [
|
|
2692
|
+
{
|
|
2693
|
+
indexUrl: `${parsed.protocol}//${parsed.host}${basePath}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`,
|
|
2694
|
+
baseUrl: `${parsed.protocol}//${parsed.host}${basePath}`
|
|
2695
|
+
}
|
|
2696
|
+
];
|
|
2697
|
+
if (basePath && basePath !== "") {
|
|
2698
|
+
urlsToTry.push({
|
|
2699
|
+
indexUrl: `${parsed.protocol}//${parsed.host}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`,
|
|
2700
|
+
baseUrl: `${parsed.protocol}//${parsed.host}`
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
for (const { indexUrl, baseUrl: resolvedBase } of urlsToTry) {
|
|
2704
|
+
try {
|
|
2705
|
+
const response = await fetch(indexUrl);
|
|
2706
|
+
if (!response.ok) {
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
const index = await response.json();
|
|
2710
|
+
if (!index.skills || !Array.isArray(index.skills)) {
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
let allValid = true;
|
|
2714
|
+
for (const entry of index.skills) {
|
|
2715
|
+
if (!this.isValidSkillEntry(entry)) {
|
|
2716
|
+
allValid = false;
|
|
2717
|
+
break;
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
if (allValid) {
|
|
2721
|
+
return { index, resolvedBaseUrl: resolvedBase };
|
|
2722
|
+
}
|
|
2723
|
+
} catch {
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
return null;
|
|
2727
|
+
} catch {
|
|
2728
|
+
return null;
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
isValidSkillEntry(entry) {
|
|
2732
|
+
if (!entry || typeof entry !== "object") return false;
|
|
2733
|
+
const e = entry;
|
|
2734
|
+
if (typeof e.name !== "string" || !e.name) return false;
|
|
2735
|
+
if (typeof e.description !== "string" || !e.description) return false;
|
|
2736
|
+
if (!Array.isArray(e.files) || e.files.length === 0) return false;
|
|
2737
|
+
const nameRegex = /^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
2738
|
+
if (!nameRegex.test(e.name) && e.name.length > 1) {
|
|
2739
|
+
if (e.name.length === 1 && !/^[a-z0-9]$/.test(e.name)) {
|
|
2740
|
+
return false;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
for (const file of e.files) {
|
|
2744
|
+
if (typeof file !== "string") return false;
|
|
2745
|
+
if (file.startsWith("/") || file.startsWith("\\") || file.includes("..")) return false;
|
|
2746
|
+
}
|
|
2747
|
+
const hasSkillMd2 = e.files.some((f) => typeof f === "string" && f.toLowerCase() === "skill.md");
|
|
2748
|
+
if (!hasSkillMd2) return false;
|
|
2749
|
+
return true;
|
|
2750
|
+
}
|
|
2751
|
+
async fetchSkill(url) {
|
|
2752
|
+
try {
|
|
2753
|
+
const parsed = new URL(url);
|
|
2754
|
+
const skillMatch = parsed.pathname.match(/^(.*)\/\.well-known\/skills\/([^/]+)\/SKILL\.md$/i);
|
|
2755
|
+
if (skillMatch) {
|
|
2756
|
+
const baseUrl = `${parsed.protocol}//${parsed.host}${skillMatch[1] || ""}`;
|
|
2757
|
+
const skillName2 = skillMatch[2];
|
|
2758
|
+
const result2 = await this.fetchIndex(baseUrl);
|
|
2759
|
+
if (result2) {
|
|
2760
|
+
const entry = result2.index.skills.find((s) => s.name === skillName2);
|
|
2761
|
+
if (entry) {
|
|
2762
|
+
const fetched = await this.fetchSkillByEntry(result2.resolvedBaseUrl, entry);
|
|
2763
|
+
if (fetched) {
|
|
2764
|
+
return fetched;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
const result = await this.fetchIndex(url);
|
|
2770
|
+
if (!result) {
|
|
2771
|
+
return null;
|
|
2772
|
+
}
|
|
2773
|
+
const { index, resolvedBaseUrl } = result;
|
|
2774
|
+
let skillName = null;
|
|
2775
|
+
const pathMatch = parsed.pathname.match(/\/.well-known\/skills\/([^/]+)\/?$/);
|
|
2776
|
+
if (pathMatch?.[1] && pathMatch[1] !== "index.json") {
|
|
2777
|
+
skillName = pathMatch[1];
|
|
2778
|
+
} else if (index.skills.length === 1) {
|
|
2779
|
+
skillName = index.skills[0]?.name ?? null;
|
|
2780
|
+
}
|
|
2781
|
+
if (!skillName) {
|
|
2782
|
+
return null;
|
|
2783
|
+
}
|
|
2784
|
+
const skillEntry = index.skills.find((s) => s.name === skillName);
|
|
2785
|
+
if (!skillEntry) {
|
|
2786
|
+
return null;
|
|
2787
|
+
}
|
|
2788
|
+
return await this.fetchSkillByEntry(resolvedBaseUrl, skillEntry);
|
|
2789
|
+
} catch {
|
|
2790
|
+
return null;
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
async fetchSkillByEntry(baseUrl, entry) {
|
|
2794
|
+
try {
|
|
2795
|
+
const skillBaseUrl = `${baseUrl.replace(/\/$/, "")}/${this.WELL_KNOWN_PATH}/${entry.name}`;
|
|
2796
|
+
const skillMdUrl = `${skillBaseUrl}/SKILL.md`;
|
|
2797
|
+
const response = await fetch(skillMdUrl);
|
|
2798
|
+
if (!response.ok) {
|
|
2799
|
+
return null;
|
|
2800
|
+
}
|
|
2801
|
+
const content = await response.text();
|
|
2802
|
+
const { data } = matter6(content);
|
|
2803
|
+
if (!data.name || !data.description) {
|
|
2804
|
+
return null;
|
|
2805
|
+
}
|
|
2806
|
+
const files = /* @__PURE__ */ new Map();
|
|
2807
|
+
files.set("SKILL.md", content);
|
|
2808
|
+
const otherFiles = entry.files.filter((f) => f.toLowerCase() !== "skill.md");
|
|
2809
|
+
const filePromises = otherFiles.map(async (filePath) => {
|
|
2810
|
+
try {
|
|
2811
|
+
const fileUrl = `${skillBaseUrl}/${filePath}`;
|
|
2812
|
+
const fileResponse = await fetch(fileUrl);
|
|
2813
|
+
if (fileResponse.ok) {
|
|
2814
|
+
const fileContent = await fileResponse.text();
|
|
2815
|
+
return { path: filePath, content: fileContent };
|
|
2816
|
+
}
|
|
2817
|
+
} catch {
|
|
2818
|
+
return null;
|
|
2819
|
+
}
|
|
2820
|
+
return null;
|
|
2821
|
+
});
|
|
2822
|
+
const fileResults = await Promise.all(filePromises);
|
|
2823
|
+
for (const result of fileResults) {
|
|
2824
|
+
if (result) {
|
|
2825
|
+
files.set(result.path, result.content);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
return {
|
|
2829
|
+
name: data.name,
|
|
2830
|
+
description: data.description,
|
|
2831
|
+
content,
|
|
2832
|
+
installName: entry.name,
|
|
2833
|
+
sourceUrl: skillMdUrl,
|
|
2834
|
+
metadata: data.metadata,
|
|
2835
|
+
files,
|
|
2836
|
+
indexEntry: entry
|
|
2837
|
+
};
|
|
2838
|
+
} catch {
|
|
2839
|
+
return null;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
async fetchAllSkills(url) {
|
|
2843
|
+
try {
|
|
2844
|
+
const result = await this.fetchIndex(url);
|
|
2845
|
+
if (!result) {
|
|
2846
|
+
return [];
|
|
2847
|
+
}
|
|
2848
|
+
const { index, resolvedBaseUrl } = result;
|
|
2849
|
+
const skillPromises = index.skills.map(
|
|
2850
|
+
(entry) => this.fetchSkillByEntry(resolvedBaseUrl, entry)
|
|
2851
|
+
);
|
|
2852
|
+
const results = await Promise.all(skillPromises);
|
|
2853
|
+
return results.filter((skill) => Boolean(skill));
|
|
2854
|
+
} catch {
|
|
2855
|
+
return [];
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
toRawUrl(url) {
|
|
2859
|
+
try {
|
|
2860
|
+
const parsed = new URL(url);
|
|
2861
|
+
if (url.toLowerCase().endsWith("/skill.md")) {
|
|
2862
|
+
return url;
|
|
2863
|
+
}
|
|
2864
|
+
const pathMatch = parsed.pathname.match(/\/.well-known\/skills\/([^/]+)\/?$/);
|
|
2865
|
+
if (pathMatch?.[1]) {
|
|
2866
|
+
const basePath2 = parsed.pathname.replace(/\/.well-known\/skills\/.*$/, "");
|
|
2867
|
+
return `${parsed.protocol}//${parsed.host}${basePath2}/${this.WELL_KNOWN_PATH}/${pathMatch[1]}/SKILL.md`;
|
|
2868
|
+
}
|
|
2869
|
+
const basePath = parsed.pathname.replace(/\/$/, "");
|
|
2870
|
+
return `${parsed.protocol}//${parsed.host}${basePath}/${this.WELL_KNOWN_PATH}/${this.INDEX_FILE}`;
|
|
2871
|
+
} catch {
|
|
2872
|
+
return url;
|
|
2873
|
+
}
|
|
2874
|
+
}
|
|
2875
|
+
getSourceIdentifier(url) {
|
|
2876
|
+
try {
|
|
2877
|
+
const parsed = new URL(url);
|
|
2878
|
+
const hostParts = parsed.hostname.split(".");
|
|
2879
|
+
if (hostParts.length >= 2) {
|
|
2880
|
+
const tld = hostParts[hostParts.length - 1];
|
|
2881
|
+
const sld = hostParts[hostParts.length - 2];
|
|
2882
|
+
return `${sld}/${tld}`;
|
|
2883
|
+
}
|
|
2884
|
+
return parsed.hostname.replace(".", "/");
|
|
2885
|
+
} catch {
|
|
2886
|
+
return "unknown/unknown";
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
};
|
|
2890
|
+
var wellKnownProvider = new WellKnownProvider();
|
|
2891
|
+
|
|
2623
2892
|
// src/providers/index.ts
|
|
2624
2893
|
registerProvider(mintlifyProvider);
|
|
2625
2894
|
registerProvider(huggingFaceProvider);
|
|
2895
|
+
registerProvider(wellKnownProvider);
|
|
2626
2896
|
registerProvider(rawSkillProvider);
|
|
2627
2897
|
|
|
2628
2898
|
// src/flows/remote-skill.ts
|
|
@@ -2678,10 +2948,56 @@ async function resolveRemoteSkill(url) {
|
|
|
2678
2948
|
};
|
|
2679
2949
|
}
|
|
2680
2950
|
|
|
2951
|
+
// src/flows/well-known-skills.ts
|
|
2952
|
+
import { mkdir as mkdir4, mkdtemp as mkdtemp3, writeFile as writeFile3 } from "fs/promises";
|
|
2953
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
2954
|
+
import { dirname as dirname3, join as join11 } from "path";
|
|
2955
|
+
async function prepareWellKnownSkills(sourceUrl) {
|
|
2956
|
+
const discovered = await wellKnownProvider.fetchAllSkills(sourceUrl);
|
|
2957
|
+
if (discovered.length === 0) {
|
|
2958
|
+
throw new Error(
|
|
2959
|
+
"No skills found at this URL. Make sure it exposes /.well-known/skills/index.json."
|
|
2960
|
+
);
|
|
2961
|
+
}
|
|
2962
|
+
const tempDir = await mkdtemp3(join11(tmpdir4(), "playbooks-skill-"));
|
|
2963
|
+
registerTempDir(tempDir);
|
|
2964
|
+
await mkdir4(tempDir, { recursive: true });
|
|
2965
|
+
const originMap = /* @__PURE__ */ new Map();
|
|
2966
|
+
const skills = [];
|
|
2967
|
+
const sourceIdentifier = wellKnownProvider.getSourceIdentifier(sourceUrl);
|
|
2968
|
+
for (const skill of discovered) {
|
|
2969
|
+
const skillDir = join11(tempDir, skill.installName);
|
|
2970
|
+
await mkdir4(skillDir, { recursive: true });
|
|
2971
|
+
for (const [filePath, content] of skill.files.entries()) {
|
|
2972
|
+
const targetPath = join11(skillDir, filePath);
|
|
2973
|
+
if (!isPathSafe(skillDir, targetPath)) {
|
|
2974
|
+
throw new Error("Invalid file path in well-known skill index.");
|
|
2975
|
+
}
|
|
2976
|
+
await mkdir4(dirname3(targetPath), { recursive: true });
|
|
2977
|
+
await writeFile3(targetPath, content, "utf-8");
|
|
2978
|
+
}
|
|
2979
|
+
const localSkill = {
|
|
2980
|
+
name: skill.installName,
|
|
2981
|
+
description: skill.description,
|
|
2982
|
+
path: skillDir,
|
|
2983
|
+
rawContent: skill.content,
|
|
2984
|
+
metadata: skill.metadata
|
|
2985
|
+
};
|
|
2986
|
+
skills.push(localSkill);
|
|
2987
|
+
originMap.set(getSkillDisplayName(localSkill), {
|
|
2988
|
+
sourceType: "well-known",
|
|
2989
|
+
sourceUrl: skill.sourceUrl,
|
|
2990
|
+
source: sourceIdentifier,
|
|
2991
|
+
skillPath: skill.sourceUrl
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
return { tempDir, skills, originBySkillName: originMap };
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2681
2997
|
// src/marketplace.ts
|
|
2682
2998
|
import { existsSync as existsSync4, statSync } from "fs";
|
|
2683
2999
|
import { readFile as readFile3 } from "fs/promises";
|
|
2684
|
-
import { dirname as
|
|
3000
|
+
import { dirname as dirname4, join as join12, posix } from "path";
|
|
2685
3001
|
function toRecord(value) {
|
|
2686
3002
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2687
3003
|
return value;
|
|
@@ -2752,7 +3068,7 @@ function resolveLocalMarketplacePath(input) {
|
|
|
2752
3068
|
return input;
|
|
2753
3069
|
}
|
|
2754
3070
|
if (stats.isDirectory()) {
|
|
2755
|
-
const candidate =
|
|
3071
|
+
const candidate = join12(input, ".claude-plugin", "marketplace.json");
|
|
2756
3072
|
if (existsSync4(candidate)) return candidate;
|
|
2757
3073
|
}
|
|
2758
3074
|
return null;
|
|
@@ -2769,7 +3085,7 @@ async function loadMarketplace(input, ref) {
|
|
|
2769
3085
|
}
|
|
2770
3086
|
const content = await readFile3(localPath, "utf-8");
|
|
2771
3087
|
const json = JSON.parse(content);
|
|
2772
|
-
return { json, context: { kind: "local", baseDir:
|
|
3088
|
+
return { json, context: { kind: "local", baseDir: dirname4(localPath) } };
|
|
2773
3089
|
}
|
|
2774
3090
|
if (isOwnerRepoShorthand(input)) {
|
|
2775
3091
|
const [owner, repo] = input.split("/");
|
|
@@ -2881,8 +3197,8 @@ function resolvePluginSource(plugin, context) {
|
|
|
2881
3197
|
const src = plugin.source;
|
|
2882
3198
|
if (typeof src === "string") {
|
|
2883
3199
|
if (context.kind === "local" && context.baseDir) {
|
|
2884
|
-
const base =
|
|
2885
|
-
return { kind: "local", localDir:
|
|
3200
|
+
const base = join12(context.baseDir, pluginRoot);
|
|
3201
|
+
return { kind: "local", localDir: join12(base, src), overrides };
|
|
2886
3202
|
}
|
|
2887
3203
|
if (context.kind === "github" && context.gh) {
|
|
2888
3204
|
const basePath = posix.join(context.gh.basePath || "", pluginRoot || "");
|
|
@@ -3056,6 +3372,34 @@ function MultiSelect({
|
|
|
3056
3372
|
] });
|
|
3057
3373
|
}
|
|
3058
3374
|
|
|
3375
|
+
// src/tui/utils/skill-selection.ts
|
|
3376
|
+
import { basename as basename3 } from "path";
|
|
3377
|
+
function matchesSkillName(skill, input) {
|
|
3378
|
+
const normalized = input.toLowerCase();
|
|
3379
|
+
const byName = skill.name.toLowerCase() === normalized;
|
|
3380
|
+
const byPath = basename3(skill.path).toLowerCase() === normalized;
|
|
3381
|
+
return byName || byPath;
|
|
3382
|
+
}
|
|
3383
|
+
function autoSelect(skills, options) {
|
|
3384
|
+
if (options.skill && options.skill.length > 0) {
|
|
3385
|
+
const selected = skills.filter((s) => options.skill?.some((name) => matchesSkillName(s, name)));
|
|
3386
|
+
if (selected.length === 0) {
|
|
3387
|
+
return {
|
|
3388
|
+
status: "prompt",
|
|
3389
|
+
message: `No matching skills found for: ${options.skill.join(", ")}`
|
|
3390
|
+
};
|
|
3391
|
+
}
|
|
3392
|
+
return { status: "selected", skills: selected };
|
|
3393
|
+
}
|
|
3394
|
+
if (skills.length === 1) {
|
|
3395
|
+
return { status: "selected", skills };
|
|
3396
|
+
}
|
|
3397
|
+
if (options.yes) {
|
|
3398
|
+
return { status: "selected", skills };
|
|
3399
|
+
}
|
|
3400
|
+
return { status: "prompt" };
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3059
3403
|
// src/tui/screens/AddSkillSelect.tsx
|
|
3060
3404
|
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
3061
3405
|
function AddSkillSelectScreen() {
|
|
@@ -3113,11 +3457,11 @@ function AddSkillSelectScreen() {
|
|
|
3113
3457
|
if (!resolved) {
|
|
3114
3458
|
throw new Error("Unable to fetch SKILL.md from that URL.");
|
|
3115
3459
|
}
|
|
3116
|
-
const tempDir2 = await
|
|
3460
|
+
const tempDir2 = await mkdtemp4(join13(tmpdir5(), "playbooks-skill-"));
|
|
3117
3461
|
registerTempDir(tempDir2);
|
|
3118
3462
|
tempDirForCleanup = tempDir2;
|
|
3119
|
-
await
|
|
3120
|
-
await
|
|
3463
|
+
await mkdir5(tempDir2, { recursive: true });
|
|
3464
|
+
await writeFile4(join13(tempDir2, "SKILL.md"), resolved.remoteSkill.content, "utf-8");
|
|
3121
3465
|
const skill = {
|
|
3122
3466
|
name: resolved.remoteSkill.installName,
|
|
3123
3467
|
description: resolved.remoteSkill.description,
|
|
@@ -3143,6 +3487,38 @@ function AddSkillSelectScreen() {
|
|
|
3143
3487
|
navigateTo("add-targets");
|
|
3144
3488
|
return;
|
|
3145
3489
|
}
|
|
3490
|
+
if (parsed.type === "well-known") {
|
|
3491
|
+
const prepared = await prepareWellKnownSkills(parsed.url);
|
|
3492
|
+
tempDirForCleanup = prepared.tempDir;
|
|
3493
|
+
updateAddSkill({
|
|
3494
|
+
parsed,
|
|
3495
|
+
tempDir: prepared.tempDir,
|
|
3496
|
+
skills: prepared.skills,
|
|
3497
|
+
originBySkillName: prepared.originBySkillName
|
|
3498
|
+
});
|
|
3499
|
+
if (options.list) {
|
|
3500
|
+
keepTempDir = true;
|
|
3501
|
+
setListMode(true);
|
|
3502
|
+
setStatus("list");
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
const autoSelection2 = autoSelect(prepared.skills, options);
|
|
3506
|
+
if (autoSelection2.status === "selected") {
|
|
3507
|
+
keepTempDir = true;
|
|
3508
|
+
updateAddSkill({ selectedSkills: autoSelection2.skills });
|
|
3509
|
+
navigateTo("add-targets");
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
if (autoSelection2.status === "error") {
|
|
3513
|
+
throw new Error(autoSelection2.message);
|
|
3514
|
+
}
|
|
3515
|
+
if (autoSelection2.status === "prompt" && autoSelection2.message) {
|
|
3516
|
+
setFlash(autoSelection2.message);
|
|
3517
|
+
}
|
|
3518
|
+
keepTempDir = true;
|
|
3519
|
+
setStatus("ready");
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3146
3522
|
let skillsDir = "";
|
|
3147
3523
|
let tempDir = null;
|
|
3148
3524
|
if (parsed.type === "local") {
|
|
@@ -3317,31 +3693,6 @@ function AddSkillSelectScreen() {
|
|
|
3317
3693
|
)
|
|
3318
3694
|
] });
|
|
3319
3695
|
}
|
|
3320
|
-
function matchesSkillName(skill, input) {
|
|
3321
|
-
const normalized = input.toLowerCase();
|
|
3322
|
-
const byName = skill.name.toLowerCase() === normalized;
|
|
3323
|
-
const byPath = basename3(skill.path).toLowerCase() === normalized;
|
|
3324
|
-
return byName || byPath;
|
|
3325
|
-
}
|
|
3326
|
-
function autoSelect(skills, options) {
|
|
3327
|
-
if (options.skill && options.skill.length > 0) {
|
|
3328
|
-
const selected = skills.filter((s) => options.skill?.some((name) => matchesSkillName(s, name)));
|
|
3329
|
-
if (selected.length === 0) {
|
|
3330
|
-
return {
|
|
3331
|
-
status: "prompt",
|
|
3332
|
-
message: `No matching skills found for: ${options.skill.join(", ")}`
|
|
3333
|
-
};
|
|
3334
|
-
}
|
|
3335
|
-
return { status: "selected", skills: selected };
|
|
3336
|
-
}
|
|
3337
|
-
if (skills.length === 1) {
|
|
3338
|
-
return { status: "selected", skills };
|
|
3339
|
-
}
|
|
3340
|
-
if (options.yes) {
|
|
3341
|
-
return { status: "selected", skills };
|
|
3342
|
-
}
|
|
3343
|
-
return { status: "prompt" };
|
|
3344
|
-
}
|
|
3345
3696
|
|
|
3346
3697
|
// src/tui/screens/AddSource.tsx
|
|
3347
3698
|
import { Box as Box16, Text as Text14 } from "ink";
|
|
@@ -3431,10 +3782,11 @@ function AddSourceScreen() {
|
|
|
3431
3782
|
// src/tui/screens/AddTargets.tsx
|
|
3432
3783
|
import { Box as Box17, Text as Text15 } from "ink";
|
|
3433
3784
|
import React14 from "react";
|
|
3434
|
-
import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3785
|
+
import { Fragment, jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3435
3786
|
function AddTargetsScreen() {
|
|
3436
3787
|
const { invocation, addSkill, updateAddSkill, navigateTo, setFlash, navAction } = useNavigation();
|
|
3437
3788
|
const [status, setStatus] = React14.useState("loading");
|
|
3789
|
+
const [mode, setMode] = React14.useState("choice");
|
|
3438
3790
|
const [availableAgents, setAvailableAgents] = React14.useState([]);
|
|
3439
3791
|
const [showLoading, setShowLoading] = React14.useState(false);
|
|
3440
3792
|
const spinner = useSpinnerFrame(status === "loading");
|
|
@@ -3447,6 +3799,7 @@ function AddTargetsScreen() {
|
|
|
3447
3799
|
const list = installed.length > 0 ? installed : Object.keys(agents);
|
|
3448
3800
|
setAvailableAgents(list);
|
|
3449
3801
|
setStatus("ready");
|
|
3802
|
+
setMode("choice");
|
|
3450
3803
|
if (navAction !== "pop" && addSkill.targetAgents && addSkill.targetAgents.length > 0) {
|
|
3451
3804
|
navigateTo("add-scope");
|
|
3452
3805
|
return;
|
|
@@ -3505,7 +3858,40 @@ function AddTargetsScreen() {
|
|
|
3505
3858
|
value: agent,
|
|
3506
3859
|
label: agents[agent].displayName
|
|
3507
3860
|
}));
|
|
3508
|
-
return /* @__PURE__ */
|
|
3861
|
+
return /* @__PURE__ */ jsx19(Box17, { flexDirection: "column", padding: 1, children: mode === "choice" ? /* @__PURE__ */ jsxs15(Fragment, { children: [
|
|
3862
|
+
/* @__PURE__ */ jsx19(AddFlowHeader, { title: "Install to" }),
|
|
3863
|
+
/* @__PURE__ */ jsx19(
|
|
3864
|
+
SingleSelect,
|
|
3865
|
+
{
|
|
3866
|
+
items: [
|
|
3867
|
+
{
|
|
3868
|
+
label: "All detected agents (Recommended)",
|
|
3869
|
+
value: "all",
|
|
3870
|
+
hint: `Install to all ${availableAgents.length} detected agents`
|
|
3871
|
+
},
|
|
3872
|
+
{
|
|
3873
|
+
label: "Select specific agents",
|
|
3874
|
+
value: "select",
|
|
3875
|
+
hint: "Choose a subset of detected agents"
|
|
3876
|
+
}
|
|
3877
|
+
],
|
|
3878
|
+
initialValue: "all",
|
|
3879
|
+
onSubmit: (value) => {
|
|
3880
|
+
if (value === "all") {
|
|
3881
|
+
updateAddSkill({
|
|
3882
|
+
targetAgents: availableAgents,
|
|
3883
|
+
installGlobally: void 0,
|
|
3884
|
+
installMode: void 0,
|
|
3885
|
+
planLines: void 0
|
|
3886
|
+
});
|
|
3887
|
+
navigateTo("add-scope");
|
|
3888
|
+
return;
|
|
3889
|
+
}
|
|
3890
|
+
setMode("select");
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
)
|
|
3894
|
+
] }) : /* @__PURE__ */ jsxs15(Fragment, { children: [
|
|
3509
3895
|
/* @__PURE__ */ jsx19(AddFlowHeader, { title: "Select agents" }),
|
|
3510
3896
|
/* @__PURE__ */ jsx19(
|
|
3511
3897
|
MultiSelect,
|
|
@@ -3527,7 +3913,7 @@ function AddTargetsScreen() {
|
|
|
3527
3913
|
}
|
|
3528
3914
|
}
|
|
3529
3915
|
)
|
|
3530
|
-
] });
|
|
3916
|
+
] }) });
|
|
3531
3917
|
}
|
|
3532
3918
|
|
|
3533
3919
|
// src/tui/screens/FindSkillResults.tsx
|
|
@@ -3796,16 +4182,16 @@ import React18 from "react";
|
|
|
3796
4182
|
|
|
3797
4183
|
// src/installed-skills.ts
|
|
3798
4184
|
import { lstat as lstat2, readFile as readFile4, readdir as readdir3, stat as stat2 } from "fs/promises";
|
|
3799
|
-
import { basename as basename4, join as
|
|
3800
|
-
import
|
|
4185
|
+
import { basename as basename4, join as join14 } from "path";
|
|
4186
|
+
import matter7 from "gray-matter";
|
|
3801
4187
|
function getAgentSkillsDir(agent, scope, cwd) {
|
|
3802
4188
|
const config = agents[agent];
|
|
3803
|
-
return scope === "global" ? config.globalSkillsDir :
|
|
4189
|
+
return scope === "global" ? config.globalSkillsDir : join14(cwd, config.skillsDir);
|
|
3804
4190
|
}
|
|
3805
4191
|
async function readSkillFrontmatter(skillMdPath) {
|
|
3806
4192
|
try {
|
|
3807
4193
|
const content = await readFile4(skillMdPath, "utf-8");
|
|
3808
|
-
const { data } =
|
|
4194
|
+
const { data } = matter7(content);
|
|
3809
4195
|
return {
|
|
3810
4196
|
name: typeof data.name === "string" ? data.name : void 0,
|
|
3811
4197
|
description: typeof data.description === "string" ? data.description : void 0
|
|
@@ -3824,7 +4210,7 @@ async function listSkillsForAgent(agent, scope, cwd = process.cwd()) {
|
|
|
3824
4210
|
continue;
|
|
3825
4211
|
}
|
|
3826
4212
|
const slug = entry.name;
|
|
3827
|
-
const skillDir =
|
|
4213
|
+
const skillDir = join14(dir, slug);
|
|
3828
4214
|
const isSymlink = entry.isSymbolicLink();
|
|
3829
4215
|
let isBroken = false;
|
|
3830
4216
|
if (isSymlink) {
|
|
@@ -3834,7 +4220,7 @@ async function listSkillsForAgent(agent, scope, cwd = process.cwd()) {
|
|
|
3834
4220
|
isBroken = true;
|
|
3835
4221
|
}
|
|
3836
4222
|
}
|
|
3837
|
-
const skillMdPath =
|
|
4223
|
+
const skillMdPath = join14(skillDir, "SKILL.md");
|
|
3838
4224
|
if (isBroken) {
|
|
3839
4225
|
skills.push({
|
|
3840
4226
|
slug,
|
|
@@ -3877,7 +4263,7 @@ async function findSkillInstallations(skillName, scope, cwd = process.cwd()) {
|
|
|
3877
4263
|
}
|
|
3878
4264
|
for (const agent of Object.keys(agents)) {
|
|
3879
4265
|
const baseDir = getAgentSkillsDir(agent, scope, cwd);
|
|
3880
|
-
const skillDir =
|
|
4266
|
+
const skillDir = join14(baseDir, sanitized);
|
|
3881
4267
|
try {
|
|
3882
4268
|
const stats = await lstat2(skillDir);
|
|
3883
4269
|
installs.push({
|
|
@@ -4291,14 +4677,14 @@ import { Box as Box25, Text as Text22 } from "ink";
|
|
|
4291
4677
|
import React21 from "react";
|
|
4292
4678
|
|
|
4293
4679
|
// src/flows/marketplace.ts
|
|
4294
|
-
import { dirname as
|
|
4680
|
+
import { dirname as dirname5, join as join15, relative as relative2 } from "path";
|
|
4295
4681
|
function normalizeCandidatePath(basePath, candidate) {
|
|
4296
4682
|
if (!candidate) return basePath;
|
|
4297
4683
|
const cleaned = candidate.replace(/\\/g, "/");
|
|
4298
4684
|
if (cleaned.endsWith(".md")) {
|
|
4299
|
-
return
|
|
4685
|
+
return join15(basePath, dirname5(cleaned));
|
|
4300
4686
|
}
|
|
4301
|
-
return
|
|
4687
|
+
return join15(basePath, cleaned);
|
|
4302
4688
|
}
|
|
4303
4689
|
function collectOverridePaths(overrides) {
|
|
4304
4690
|
const paths = ["skills", "commands", "agents", "hooks"];
|
|
@@ -4375,7 +4761,7 @@ async function collectMarketplaceSkills(plugins, context) {
|
|
|
4375
4761
|
const key = `github:${owner}/${repo}@${ref}`;
|
|
4376
4762
|
const repoUrl = `https://github.com/${owner}/${repo}.git`;
|
|
4377
4763
|
const { tempDir } = await ensureRepoClone(key, repoUrl, ref);
|
|
4378
|
-
const repoRoot =
|
|
4764
|
+
const repoRoot = join15(tempDir, path || "");
|
|
4379
4765
|
const baseDir = normalizeCandidatePath(repoRoot, plugin.pluginRoot || "");
|
|
4380
4766
|
const overridePaths = collectOverridePaths(resolved.overrides);
|
|
4381
4767
|
const scanned = /* @__PURE__ */ new Set();
|
|
@@ -4407,7 +4793,7 @@ async function collectMarketplaceSkills(plugins, context) {
|
|
|
4407
4793
|
const key = `gitlab:${namespacePath}/${repo}@${ref}`;
|
|
4408
4794
|
const repoUrl = `https://gitlab.com/${namespacePath}/${repo}.git`;
|
|
4409
4795
|
const { tempDir } = await ensureRepoClone(key, repoUrl, ref);
|
|
4410
|
-
const repoRoot =
|
|
4796
|
+
const repoRoot = join15(tempDir, path || "");
|
|
4411
4797
|
const baseDir = normalizeCandidatePath(repoRoot, plugin.pluginRoot || "");
|
|
4412
4798
|
const overridePaths = collectOverridePaths(resolved.overrides);
|
|
4413
4799
|
const scanned = /* @__PURE__ */ new Set();
|
|
@@ -4594,9 +4980,9 @@ import { Box as Box26, Text as Text23, useInput as useInput5 } from "ink";
|
|
|
4594
4980
|
import React22 from "react";
|
|
4595
4981
|
|
|
4596
4982
|
// src/flows/update-skills.ts
|
|
4597
|
-
import { mkdir as
|
|
4598
|
-
import { tmpdir as
|
|
4599
|
-
import { dirname as
|
|
4983
|
+
import { mkdir as mkdir6, mkdtemp as mkdtemp5, rm as rm6, stat as stat3, writeFile as writeFile5 } from "fs/promises";
|
|
4984
|
+
import { tmpdir as tmpdir6 } from "os";
|
|
4985
|
+
import { dirname as dirname6, join as join16 } from "path";
|
|
4600
4986
|
var repoTreeCache = /* @__PURE__ */ new Map();
|
|
4601
4987
|
function normalizeSkillFolderPath(skillPath) {
|
|
4602
4988
|
let folderPath = skillPath;
|
|
@@ -4691,8 +5077,8 @@ async function updateFromRepo(target) {
|
|
|
4691
5077
|
let sourceDir = null;
|
|
4692
5078
|
if (target.entry.skillPath) {
|
|
4693
5079
|
const normalizedPath = target.entry.skillPath.replace(/\\/g, "/");
|
|
4694
|
-
const skillDir =
|
|
4695
|
-
if (await pathExists(
|
|
5080
|
+
const skillDir = join16(tempDir, dirname6(normalizedPath));
|
|
5081
|
+
if (await pathExists(join16(skillDir, "SKILL.md"))) {
|
|
4696
5082
|
sourceDir = skillDir;
|
|
4697
5083
|
}
|
|
4698
5084
|
}
|
|
@@ -4714,10 +5100,15 @@ async function updateFromRepo(target) {
|
|
|
4714
5100
|
async function updateFromRemote(target) {
|
|
4715
5101
|
const provider = findProvider(target.entry.sourceUrl);
|
|
4716
5102
|
let content = null;
|
|
5103
|
+
let files = null;
|
|
4717
5104
|
if (provider) {
|
|
4718
5105
|
const skill = await provider.fetchSkill(target.entry.sourceUrl);
|
|
4719
5106
|
if (skill) {
|
|
4720
|
-
|
|
5107
|
+
if ("files" in skill && skill.files instanceof Map) {
|
|
5108
|
+
files = skill.files;
|
|
5109
|
+
} else {
|
|
5110
|
+
content = skill.content;
|
|
5111
|
+
}
|
|
4721
5112
|
}
|
|
4722
5113
|
} else if (target.entry.sourceType === "mintlify") {
|
|
4723
5114
|
const legacy = await fetchMintlifySkill(target.entry.sourceUrl);
|
|
@@ -4725,14 +5116,25 @@ async function updateFromRemote(target) {
|
|
|
4725
5116
|
content = legacy.content;
|
|
4726
5117
|
}
|
|
4727
5118
|
}
|
|
4728
|
-
if (!content) {
|
|
5119
|
+
if (!content && !files) {
|
|
4729
5120
|
return false;
|
|
4730
5121
|
}
|
|
4731
|
-
const tempDir = await
|
|
5122
|
+
const tempDir = await mkdtemp5(join16(tmpdir6(), "playbooks-skill-"));
|
|
4732
5123
|
registerTempDir(tempDir);
|
|
4733
5124
|
try {
|
|
4734
|
-
await
|
|
4735
|
-
|
|
5125
|
+
await mkdir6(tempDir, { recursive: true });
|
|
5126
|
+
if (files) {
|
|
5127
|
+
for (const [filePath, fileContent] of files.entries()) {
|
|
5128
|
+
const targetPath = join16(tempDir, filePath);
|
|
5129
|
+
if (!isPathSafe(tempDir, targetPath)) {
|
|
5130
|
+
continue;
|
|
5131
|
+
}
|
|
5132
|
+
await mkdir6(dirname6(targetPath), { recursive: true });
|
|
5133
|
+
await writeFile5(targetPath, fileContent, "utf-8");
|
|
5134
|
+
}
|
|
5135
|
+
} else if (content) {
|
|
5136
|
+
await writeFile5(join16(tempDir, "SKILL.md"), content, "utf-8");
|
|
5137
|
+
}
|
|
4736
5138
|
return await applyUpdateFromDir(target.name, target.scope, tempDir);
|
|
4737
5139
|
} finally {
|
|
4738
5140
|
await cleanupTempDir(tempDir);
|
|
@@ -5003,7 +5405,7 @@ function sortTargets(a, b) {
|
|
|
5003
5405
|
}
|
|
5004
5406
|
|
|
5005
5407
|
// src/tui/ScreenRouter.tsx
|
|
5006
|
-
import { Fragment, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
5408
|
+
import { Fragment as Fragment2, jsx as jsx29, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
5007
5409
|
function ScreenRouter() {
|
|
5008
5410
|
const { screen } = useNavigation();
|
|
5009
5411
|
const render2 = (s) => {
|
|
@@ -5046,7 +5448,7 @@ function ScreenRouter() {
|
|
|
5046
5448
|
return null;
|
|
5047
5449
|
}
|
|
5048
5450
|
};
|
|
5049
|
-
return /* @__PURE__ */ jsxs24(
|
|
5451
|
+
return /* @__PURE__ */ jsxs24(Fragment2, { children: [
|
|
5050
5452
|
/* @__PURE__ */ jsx29(BrandHeader, {}),
|
|
5051
5453
|
render2(screen),
|
|
5052
5454
|
/* @__PURE__ */ jsx29(FlashBar, { align: "center" })
|
|
@@ -5145,7 +5547,7 @@ function runApp(initialInvocation, initialScreen) {
|
|
|
5145
5547
|
var version = package_default.version;
|
|
5146
5548
|
setVersion(version);
|
|
5147
5549
|
setupTempDirCleanup();
|
|
5148
|
-
program.name("playbooks").description("
|
|
5550
|
+
program.name("playbooks").description("playbooks CLI").version(version);
|
|
5149
5551
|
program.addHelpCommand();
|
|
5150
5552
|
var applyAddSkillOptions = (cmd) => cmd.option("-g, --global", "Install globally (user-level) instead of project-level").option(
|
|
5151
5553
|
"-a, --agent <agents...>",
|
|
@@ -5283,6 +5685,36 @@ program.command("get <url> [outKeyword] [outPath]").description("Fetch a URL as
|
|
|
5283
5685
|
}
|
|
5284
5686
|
outputPath = outPath;
|
|
5285
5687
|
}
|
|
5688
|
+
const shouldUseTui = Boolean(process.stdout.isTTY) && !outputPath;
|
|
5689
|
+
if (!shouldUseTui) {
|
|
5690
|
+
try {
|
|
5691
|
+
const data = await fetchUrlMarkdown(url);
|
|
5692
|
+
if (outputPath) {
|
|
5693
|
+
if (options.json) {
|
|
5694
|
+
writeFileSync(outputPath, `${JSON.stringify(data, null, 2)}
|
|
5695
|
+
`, "utf8");
|
|
5696
|
+
return;
|
|
5697
|
+
}
|
|
5698
|
+
const body3 = data.markdown.endsWith("\n") ? data.markdown : `${data.markdown}
|
|
5699
|
+
`;
|
|
5700
|
+
writeFileSync(outputPath, body3, "utf8");
|
|
5701
|
+
return;
|
|
5702
|
+
}
|
|
5703
|
+
if (options.json) {
|
|
5704
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}
|
|
5705
|
+
`);
|
|
5706
|
+
return;
|
|
5707
|
+
}
|
|
5708
|
+
const body2 = data.markdown.endsWith("\n") ? data.markdown : `${data.markdown}
|
|
5709
|
+
`;
|
|
5710
|
+
process.stdout.write(body2);
|
|
5711
|
+
return;
|
|
5712
|
+
} catch (error) {
|
|
5713
|
+
const message = error instanceof Error ? error.message : "Failed to fetch markdown.";
|
|
5714
|
+
console.error(`Failed to fetch markdown: ${message}`);
|
|
5715
|
+
process.exit(1);
|
|
5716
|
+
}
|
|
5717
|
+
}
|
|
5286
5718
|
await runApp(
|
|
5287
5719
|
{ intent: "get-url", source: url, options: { json: options.json, output: outputPath } },
|
|
5288
5720
|
"get-url"
|