faces-cli 1.5.6 → 1.5.8
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.
|
@@ -10,7 +10,7 @@ export default class AuthWhoami extends BaseCommand {
|
|
|
10
10
|
const client = this.makeClient(flags);
|
|
11
11
|
let data;
|
|
12
12
|
try {
|
|
13
|
-
data = await client.get('/auth/me'
|
|
13
|
+
data = await client.get('/auth/me');
|
|
14
14
|
}
|
|
15
15
|
catch (err) {
|
|
16
16
|
if (err instanceof FacesAPIError)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base.js';
|
|
2
|
+
export default class CatalogManyfaced extends BaseCommand {
|
|
3
|
+
static description: string;
|
|
4
|
+
static flags: {
|
|
5
|
+
skill: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
6
|
+
install: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
'skills-dir': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
8
|
+
refresh: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
'base-url': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
|
+
token: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
11
|
+
'api-key': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
};
|
|
13
|
+
run(): Promise<unknown>;
|
|
14
|
+
private fetchIndex;
|
|
15
|
+
private listSkills;
|
|
16
|
+
private showSkill;
|
|
17
|
+
private installSkill;
|
|
18
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Flags } from '@oclif/core';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { BaseCommand } from '../../base.js';
|
|
6
|
+
const GITHUB_REPO = 'facessh/manyfaced';
|
|
7
|
+
const GITHUB_API = `https://api.github.com/repos/${GITHUB_REPO}`;
|
|
8
|
+
const CACHE_PATH = path.join(os.homedir(), '.faces', 'manyfaced-index.json');
|
|
9
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
10
|
+
function parseFrontmatter(content) {
|
|
11
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
12
|
+
if (!match)
|
|
13
|
+
return {};
|
|
14
|
+
const fm = {};
|
|
15
|
+
for (const line of match[1].split('\n')) {
|
|
16
|
+
const ci = line.indexOf(':');
|
|
17
|
+
if (ci < 0)
|
|
18
|
+
continue;
|
|
19
|
+
const key = line.slice(0, ci).trim();
|
|
20
|
+
const val = line.slice(ci + 1).trim().replace(/^["']|["']$/g, '');
|
|
21
|
+
if (key && val)
|
|
22
|
+
fm[key] = val;
|
|
23
|
+
}
|
|
24
|
+
return fm;
|
|
25
|
+
}
|
|
26
|
+
async function githubGet(urlPath) {
|
|
27
|
+
const resp = await fetch(`${GITHUB_API}${urlPath}`, {
|
|
28
|
+
headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'faces-cli' },
|
|
29
|
+
});
|
|
30
|
+
if (!resp.ok) {
|
|
31
|
+
const body = await resp.text().catch(() => '');
|
|
32
|
+
throw new Error(`GitHub API ${resp.status}: ${body.slice(0, 200)}`);
|
|
33
|
+
}
|
|
34
|
+
return resp.json();
|
|
35
|
+
}
|
|
36
|
+
async function githubGetFile(filePath) {
|
|
37
|
+
const data = (await githubGet(`/contents/${filePath}`));
|
|
38
|
+
if (!data.content)
|
|
39
|
+
throw new Error(`No content for ${filePath}`);
|
|
40
|
+
return Buffer.from(data.content, 'base64').toString('utf8');
|
|
41
|
+
}
|
|
42
|
+
function loadCache() {
|
|
43
|
+
try {
|
|
44
|
+
if (!fs.existsSync(CACHE_PATH))
|
|
45
|
+
return null;
|
|
46
|
+
const raw = JSON.parse(fs.readFileSync(CACHE_PATH, 'utf8'));
|
|
47
|
+
if (Date.now() - raw.fetched_at > CACHE_TTL_MS)
|
|
48
|
+
return null;
|
|
49
|
+
return raw;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function saveCache(index) {
|
|
56
|
+
const dir = path.dirname(CACHE_PATH);
|
|
57
|
+
if (!fs.existsSync(dir))
|
|
58
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
59
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(index, null, 2));
|
|
60
|
+
}
|
|
61
|
+
export default class CatalogManyfaced extends BaseCommand {
|
|
62
|
+
static description = 'Browse and install community manyfaced skills from github.com/facessh/manyfaced';
|
|
63
|
+
static flags = {
|
|
64
|
+
...BaseCommand.baseFlags,
|
|
65
|
+
skill: Flags.string({ description: 'Show details for a specific skill' }),
|
|
66
|
+
install: Flags.string({ description: 'Install a skill by name' }),
|
|
67
|
+
'skills-dir': Flags.string({
|
|
68
|
+
description: 'Directory to install skills into (required for --install)',
|
|
69
|
+
}),
|
|
70
|
+
refresh: Flags.boolean({
|
|
71
|
+
description: 'Force refresh the skill index (ignore cache)',
|
|
72
|
+
default: false,
|
|
73
|
+
}),
|
|
74
|
+
};
|
|
75
|
+
async run() {
|
|
76
|
+
const { flags } = await this.parse(CatalogManyfaced);
|
|
77
|
+
if (flags.install) {
|
|
78
|
+
return this.installSkill(flags.install, flags['skills-dir']);
|
|
79
|
+
}
|
|
80
|
+
if (flags.skill) {
|
|
81
|
+
return this.showSkill(flags.skill);
|
|
82
|
+
}
|
|
83
|
+
return this.listSkills(flags.refresh);
|
|
84
|
+
}
|
|
85
|
+
async fetchIndex(forceRefresh) {
|
|
86
|
+
if (!forceRefresh) {
|
|
87
|
+
const cached = loadCache();
|
|
88
|
+
if (cached)
|
|
89
|
+
return cached.skills;
|
|
90
|
+
}
|
|
91
|
+
const entries = (await githubGet('/contents/'));
|
|
92
|
+
const dirs = entries.filter((e) => e.type === 'dir' && !e.name.startsWith('.'));
|
|
93
|
+
const skills = [];
|
|
94
|
+
for (const dir of dirs) {
|
|
95
|
+
try {
|
|
96
|
+
const skillMd = await githubGetFile(`${dir.name}/SKILL.md`);
|
|
97
|
+
const fm = parseFrontmatter(skillMd);
|
|
98
|
+
skills.push({
|
|
99
|
+
name: dir.name,
|
|
100
|
+
description: fm.description || '(no description)',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
skills.push({ name: dir.name, description: '(could not read SKILL.md)' });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
saveCache({ fetched_at: Date.now(), skills });
|
|
108
|
+
return skills;
|
|
109
|
+
}
|
|
110
|
+
async listSkills(refresh) {
|
|
111
|
+
const skills = await this.fetchIndex(refresh);
|
|
112
|
+
if (skills.length === 0) {
|
|
113
|
+
this.log('No manyfaced skills found in the community catalog.');
|
|
114
|
+
return { skills: [] };
|
|
115
|
+
}
|
|
116
|
+
if (this.jsonEnabled())
|
|
117
|
+
return { skills };
|
|
118
|
+
const nameWidth = Math.max(30, ...skills.map((s) => s.name.length + 2));
|
|
119
|
+
this.log('');
|
|
120
|
+
this.log(`${'NAME'.padEnd(nameWidth)}DESCRIPTION`);
|
|
121
|
+
this.log(`${'-'.repeat(nameWidth)}${'-'.repeat(50)}`);
|
|
122
|
+
for (const s of skills) {
|
|
123
|
+
this.log(`${s.name.padEnd(nameWidth)}${s.description.slice(0, 70)}`);
|
|
124
|
+
}
|
|
125
|
+
this.log('');
|
|
126
|
+
this.log(`${skills.length} skills available. Use --skill NAME for details, --install NAME to install.`);
|
|
127
|
+
return { skills };
|
|
128
|
+
}
|
|
129
|
+
async showSkill(name) {
|
|
130
|
+
let readme;
|
|
131
|
+
try {
|
|
132
|
+
readme = await githubGetFile(`${name}/README.md`);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
this.error(`Skill '${name}' not found. Run 'faces catalog:manyfaced' to see available skills.`);
|
|
136
|
+
}
|
|
137
|
+
if (this.jsonEnabled()) {
|
|
138
|
+
let skillMd = '';
|
|
139
|
+
try {
|
|
140
|
+
skillMd = await githubGetFile(`${name}/SKILL.md`);
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ }
|
|
143
|
+
const fm = parseFrontmatter(skillMd);
|
|
144
|
+
return { name, description: fm.description, readme };
|
|
145
|
+
}
|
|
146
|
+
this.log('');
|
|
147
|
+
this.log(readme);
|
|
148
|
+
return { name, readme };
|
|
149
|
+
}
|
|
150
|
+
async installSkill(name, skillsDir) {
|
|
151
|
+
if (!skillsDir) {
|
|
152
|
+
this.error('--skills-dir is required for install. Specify where skills should be installed.\n' +
|
|
153
|
+
'Example: faces catalog:manyfaced --install ' + name + ' --skills-dir ~/.claude/skills');
|
|
154
|
+
}
|
|
155
|
+
// Verify skill exists
|
|
156
|
+
const skills = await this.fetchIndex(false);
|
|
157
|
+
const found = skills.find((s) => s.name === name);
|
|
158
|
+
if (!found) {
|
|
159
|
+
this.error(`Skill '${name}' not found. Available skills:\n` +
|
|
160
|
+
skills.map((s) => ` ${s.name}`).join('\n'));
|
|
161
|
+
}
|
|
162
|
+
// Create skills-dir if needed
|
|
163
|
+
if (!fs.existsSync(skillsDir)) {
|
|
164
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
165
|
+
}
|
|
166
|
+
const installDir = path.join(skillsDir, name);
|
|
167
|
+
// Fetch the full directory tree for this skill
|
|
168
|
+
process.stderr.write(`Fetching ${name}...\n`);
|
|
169
|
+
const tree = (await githubGet('/git/trees/main?recursive=1'));
|
|
170
|
+
const skillFiles = tree.tree.filter((f) => f.path.startsWith(`${name}/`) && f.type === 'blob');
|
|
171
|
+
if (skillFiles.length === 0) {
|
|
172
|
+
this.error(`No files found for skill '${name}'.`);
|
|
173
|
+
}
|
|
174
|
+
// Download and write each file
|
|
175
|
+
const faceMdFiles = [];
|
|
176
|
+
for (const file of skillFiles) {
|
|
177
|
+
const relativePath = file.path.slice(name.length + 1); // remove skill prefix
|
|
178
|
+
const destPath = path.join(installDir, relativePath);
|
|
179
|
+
const destDir = path.dirname(destPath);
|
|
180
|
+
if (!fs.existsSync(destDir))
|
|
181
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
182
|
+
process.stderr.write(` ${relativePath}\n`);
|
|
183
|
+
const content = await githubGetFile(file.path);
|
|
184
|
+
fs.writeFileSync(destPath, content);
|
|
185
|
+
// Track FACE.md files for catalog copy
|
|
186
|
+
if (relativePath.endsWith('FACE.md') && relativePath.includes('catalog/')) {
|
|
187
|
+
faceMdFiles.push(destPath);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
// Copy FACE.md files to local catalog
|
|
191
|
+
const catalogDir = path.join(os.homedir(), '.faces', 'catalog');
|
|
192
|
+
const installedFaces = [];
|
|
193
|
+
for (const faceMdPath of faceMdFiles) {
|
|
194
|
+
const content = fs.readFileSync(faceMdPath, 'utf8');
|
|
195
|
+
const fm = parseFrontmatter(content);
|
|
196
|
+
const alias = fm.alias || fm.facename || path.basename(path.dirname(faceMdPath));
|
|
197
|
+
const catalogFaceDir = path.join(catalogDir, alias);
|
|
198
|
+
if (!fs.existsSync(catalogFaceDir))
|
|
199
|
+
fs.mkdirSync(catalogFaceDir, { recursive: true });
|
|
200
|
+
fs.writeFileSync(path.join(catalogFaceDir, 'FACE.md'), content);
|
|
201
|
+
installedFaces.push(alias);
|
|
202
|
+
}
|
|
203
|
+
if (!this.jsonEnabled()) {
|
|
204
|
+
this.log('');
|
|
205
|
+
this.log(`Installed ${name} to ${installDir}/`);
|
|
206
|
+
this.log(` ${skillFiles.length} files written`);
|
|
207
|
+
if (installedFaces.length > 0) {
|
|
208
|
+
this.log('');
|
|
209
|
+
this.log(`Faces added to local catalog (need creation + compilation):`);
|
|
210
|
+
for (const alias of installedFaces) {
|
|
211
|
+
this.log(` faces face:create --alias ${alias} --name "${alias}"`);
|
|
212
|
+
}
|
|
213
|
+
this.log('');
|
|
214
|
+
this.log('After creating each face, compile source material:');
|
|
215
|
+
this.log(' faces compile:doc ALIAS --file source.txt');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
skill: name,
|
|
220
|
+
install_dir: installDir,
|
|
221
|
+
files: skillFiles.length,
|
|
222
|
+
faces: installedFaces,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|