@poncho-ai/cli 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +7 -7
- package/CHANGELOG.md +264 -0
- package/dist/{chunk-GUGBKAIM.js → chunk-W7SQVUB4.js} +6166 -4694
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +197 -128
- package/dist/index.js +111 -5
- package/dist/run-interactive-ink-UKPUGCDW.js +679 -0
- package/package.json +4 -4
- package/src/cron-helpers.ts +183 -0
- package/src/http-utils.ts +220 -0
- package/src/index.ts +1071 -4754
- package/src/logger.ts +9 -0
- package/src/mcp-commands.ts +283 -0
- package/src/project-init.ts +150 -0
- package/src/run-commands.ts +145 -0
- package/src/scaffolding.ts +528 -0
- package/src/skills.ts +372 -0
- package/src/templates.ts +563 -0
- package/src/testing.ts +108 -0
- package/src/web-ui-client.ts +845 -94
- package/src/web-ui-styles.ts +269 -1
- package/src/web-ui.ts +23 -0
- package/test/cli.test.ts +52 -1
- package/dist/run-interactive-ink-75GKYSEC.js +0 -2115
- package/test/run-orchestration.test.ts +0 -171
package/src/skills.ts
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { access, cp, mkdir, readdir, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, normalize, relative, resolve } from "node:path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
|
|
9
|
+
export const runPnpmInstall = async (workingDir: string): Promise<void> =>
|
|
10
|
+
await new Promise<void>((resolveInstall, rejectInstall) => {
|
|
11
|
+
const child = spawn("pnpm", ["install"], {
|
|
12
|
+
cwd: workingDir,
|
|
13
|
+
stdio: "inherit",
|
|
14
|
+
env: process.env,
|
|
15
|
+
});
|
|
16
|
+
child.on("exit", (code) => {
|
|
17
|
+
if (code === 0) {
|
|
18
|
+
resolveInstall();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
rejectInstall(new Error(`pnpm install failed with exit code ${code ?? -1}`));
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const runInstallCommand = async (
|
|
26
|
+
workingDir: string,
|
|
27
|
+
packageNameOrPath: string,
|
|
28
|
+
): Promise<void> =>
|
|
29
|
+
await new Promise<void>((resolveInstall, rejectInstall) => {
|
|
30
|
+
const child = spawn("pnpm", ["add", packageNameOrPath], {
|
|
31
|
+
cwd: workingDir,
|
|
32
|
+
stdio: "inherit",
|
|
33
|
+
env: process.env,
|
|
34
|
+
});
|
|
35
|
+
child.on("exit", (code) => {
|
|
36
|
+
if (code === 0) {
|
|
37
|
+
resolveInstall();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
rejectInstall(new Error(`pnpm add failed with exit code ${code ?? -1}`));
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Resolve the installed npm package name from a package specifier.
|
|
46
|
+
* Handles local paths, scoped packages, and GitHub shorthand (e.g.
|
|
47
|
+
* "vercel-labs/agent-skills" installs as "agent-skills").
|
|
48
|
+
*/
|
|
49
|
+
export const resolveInstalledPackageName = (packageNameOrPath: string): string | null => {
|
|
50
|
+
if (packageNameOrPath.startsWith(".") || packageNameOrPath.startsWith("/")) {
|
|
51
|
+
return null; // local path — handled separately
|
|
52
|
+
}
|
|
53
|
+
// Scoped package: @scope/name
|
|
54
|
+
if (packageNameOrPath.startsWith("@")) {
|
|
55
|
+
return packageNameOrPath;
|
|
56
|
+
}
|
|
57
|
+
// GitHub shorthand: owner/repo — npm installs as the repo name
|
|
58
|
+
if (packageNameOrPath.includes("/")) {
|
|
59
|
+
return packageNameOrPath.split("/").pop() ?? packageNameOrPath;
|
|
60
|
+
}
|
|
61
|
+
return packageNameOrPath;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Locate the root directory of an installed skill package.
|
|
66
|
+
* Handles local paths, normal npm packages, and GitHub repos (which may
|
|
67
|
+
* lack a root package.json).
|
|
68
|
+
*/
|
|
69
|
+
export const resolveSkillRoot = (
|
|
70
|
+
workingDir: string,
|
|
71
|
+
packageNameOrPath: string,
|
|
72
|
+
): string => {
|
|
73
|
+
// Local path
|
|
74
|
+
if (packageNameOrPath.startsWith(".") || packageNameOrPath.startsWith("/")) {
|
|
75
|
+
return resolve(workingDir, packageNameOrPath);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const moduleName =
|
|
79
|
+
resolveInstalledPackageName(packageNameOrPath) ?? packageNameOrPath;
|
|
80
|
+
|
|
81
|
+
// Try require.resolve first (works for packages with a package.json)
|
|
82
|
+
try {
|
|
83
|
+
const packageJsonPath = require.resolve(`${moduleName}/package.json`, {
|
|
84
|
+
paths: [workingDir],
|
|
85
|
+
});
|
|
86
|
+
return resolve(packageJsonPath, "..");
|
|
87
|
+
} catch {
|
|
88
|
+
// Fall back to looking in node_modules directly (GitHub repos may lack
|
|
89
|
+
// a root package.json)
|
|
90
|
+
const candidate = resolve(workingDir, "node_modules", moduleName);
|
|
91
|
+
if (existsSync(candidate)) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Could not locate installed package "${moduleName}" in ${workingDir}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const normalizeSkillSourceName = (value: string): string => {
|
|
101
|
+
const normalized = value
|
|
102
|
+
.trim()
|
|
103
|
+
.replace(/\\/g, "/")
|
|
104
|
+
.replace(/^@/, "")
|
|
105
|
+
.replace(/[\/\s]+/g, "-")
|
|
106
|
+
.replace(/[^a-zA-Z0-9._-]/g, "-")
|
|
107
|
+
.replace(/-+/g, "-")
|
|
108
|
+
.replace(/^-|-$/g, "");
|
|
109
|
+
return normalized.length > 0 ? normalized : "skills";
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export const collectSkillManifests = async (dir: string, depth = 2): Promise<string[]> => {
|
|
113
|
+
const manifests: string[] = [];
|
|
114
|
+
const localManifest = resolve(dir, "SKILL.md");
|
|
115
|
+
try {
|
|
116
|
+
await access(localManifest);
|
|
117
|
+
manifests.push(localManifest);
|
|
118
|
+
} catch {
|
|
119
|
+
// Not found at this level — look one level deeper (e.g. skills/<name>/SKILL.md)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (depth <= 0) return manifests;
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
128
|
+
|
|
129
|
+
let isDir = entry.isDirectory();
|
|
130
|
+
// Dirent reports symlinks separately; resolve target type via stat()
|
|
131
|
+
if (!isDir && entry.isSymbolicLink()) {
|
|
132
|
+
try {
|
|
133
|
+
const s = await stat(resolve(dir, entry.name));
|
|
134
|
+
isDir = s.isDirectory();
|
|
135
|
+
} catch {
|
|
136
|
+
continue; // broken symlink — skip
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (isDir) {
|
|
141
|
+
manifests.push(...(await collectSkillManifests(resolve(dir, entry.name), depth - 1)));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// ignore read errors
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return manifests;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export const validateSkillPackage = async (
|
|
152
|
+
workingDir: string,
|
|
153
|
+
packageNameOrPath: string,
|
|
154
|
+
): Promise<{ skillRoot: string; manifests: string[] }> => {
|
|
155
|
+
const skillRoot = resolveSkillRoot(workingDir, packageNameOrPath);
|
|
156
|
+
const manifests = await collectSkillManifests(skillRoot);
|
|
157
|
+
if (manifests.length === 0) {
|
|
158
|
+
throw new Error(`Skill validation failed: no SKILL.md found in ${skillRoot}`);
|
|
159
|
+
}
|
|
160
|
+
return { skillRoot, manifests };
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const selectSkillManifests = async (
|
|
164
|
+
skillRoot: string,
|
|
165
|
+
manifests: string[],
|
|
166
|
+
relativeSkillPath?: string,
|
|
167
|
+
): Promise<string[]> => {
|
|
168
|
+
if (!relativeSkillPath) return manifests;
|
|
169
|
+
|
|
170
|
+
const normalized = normalize(relativeSkillPath);
|
|
171
|
+
if (normalized.startsWith("..") || normalized.startsWith("/")) {
|
|
172
|
+
throw new Error(`Invalid skill path "${relativeSkillPath}": path must be within package root.`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const candidate = resolve(skillRoot, normalized);
|
|
176
|
+
const relativeToRoot = relative(skillRoot, candidate).split("\\").join("/");
|
|
177
|
+
if (relativeToRoot.startsWith("..") || relativeToRoot.startsWith("/")) {
|
|
178
|
+
throw new Error(`Invalid skill path "${relativeSkillPath}": path escapes package root.`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const candidateAsFile = candidate.toLowerCase().endsWith("skill.md")
|
|
182
|
+
? candidate
|
|
183
|
+
: resolve(candidate, "SKILL.md");
|
|
184
|
+
if (!existsSync(candidateAsFile)) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Skill path "${relativeSkillPath}" does not point to a directory (or file) containing SKILL.md.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const selected = manifests.filter((manifest) => resolve(manifest) === resolve(candidateAsFile));
|
|
191
|
+
if (selected.length === 0) {
|
|
192
|
+
throw new Error(`Skill path "${relativeSkillPath}" was not discovered as a valid skill manifest.`);
|
|
193
|
+
}
|
|
194
|
+
return selected;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export const copySkillsIntoProject = async (
|
|
198
|
+
workingDir: string,
|
|
199
|
+
manifests: string[],
|
|
200
|
+
sourceName: string,
|
|
201
|
+
): Promise<string[]> => {
|
|
202
|
+
const skillsDir = resolve(workingDir, "skills", normalizeSkillSourceName(sourceName));
|
|
203
|
+
await mkdir(skillsDir, { recursive: true });
|
|
204
|
+
|
|
205
|
+
const destinations = new Map<string, string>();
|
|
206
|
+
for (const manifest of manifests) {
|
|
207
|
+
const sourceSkillDir = dirname(manifest);
|
|
208
|
+
const skillFolderName = basename(sourceSkillDir);
|
|
209
|
+
if (destinations.has(skillFolderName)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Skill copy failed: multiple skill directories map to "skills/${skillFolderName}" (${destinations.get(skillFolderName)} and ${sourceSkillDir}).`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
destinations.set(skillFolderName, sourceSkillDir);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const copied: string[] = [];
|
|
218
|
+
for (const [skillFolderName, sourceSkillDir] of destinations.entries()) {
|
|
219
|
+
const destinationSkillDir = resolve(skillsDir, skillFolderName);
|
|
220
|
+
if (existsSync(destinationSkillDir)) {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Skill copy failed: destination already exists at ${destinationSkillDir}. Remove or rename it and try again.`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
await cp(sourceSkillDir, destinationSkillDir, {
|
|
226
|
+
recursive: true,
|
|
227
|
+
dereference: true,
|
|
228
|
+
force: false,
|
|
229
|
+
errorOnExist: true,
|
|
230
|
+
});
|
|
231
|
+
copied.push(relative(workingDir, destinationSkillDir).split("\\").join("/"));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return copied.sort();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
export const copySkillsFromPackage = async (
|
|
238
|
+
workingDir: string,
|
|
239
|
+
packageNameOrPath: string,
|
|
240
|
+
options?: { path?: string },
|
|
241
|
+
): Promise<string[]> => {
|
|
242
|
+
const { skillRoot, manifests } = await validateSkillPackage(workingDir, packageNameOrPath);
|
|
243
|
+
const selected = await selectSkillManifests(skillRoot, manifests, options?.path);
|
|
244
|
+
const sourceName = resolveInstalledPackageName(packageNameOrPath) ?? basename(skillRoot);
|
|
245
|
+
return await copySkillsIntoProject(workingDir, selected, sourceName);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
export const addSkill = async (
|
|
249
|
+
workingDir: string,
|
|
250
|
+
packageNameOrPath: string,
|
|
251
|
+
options?: { path?: string },
|
|
252
|
+
): Promise<void> => {
|
|
253
|
+
await runInstallCommand(workingDir, packageNameOrPath);
|
|
254
|
+
const copiedSkills = await copySkillsFromPackage(workingDir, packageNameOrPath, options);
|
|
255
|
+
process.stdout.write(
|
|
256
|
+
`Added ${copiedSkills.length} skill${copiedSkills.length === 1 ? "" : "s"} from ${packageNameOrPath}:\n`,
|
|
257
|
+
);
|
|
258
|
+
for (const copied of copiedSkills) {
|
|
259
|
+
process.stdout.write(`- ${copied}\n`);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const getSkillFolderNames = (manifests: string[]): string[] => {
|
|
264
|
+
const names = new Set<string>();
|
|
265
|
+
for (const manifest of manifests) {
|
|
266
|
+
names.add(basename(dirname(manifest)));
|
|
267
|
+
}
|
|
268
|
+
return Array.from(names).sort();
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export const removeSkillsFromPackage = async (
|
|
272
|
+
workingDir: string,
|
|
273
|
+
packageNameOrPath: string,
|
|
274
|
+
options?: { path?: string },
|
|
275
|
+
): Promise<{ removed: string[]; missing: string[] }> => {
|
|
276
|
+
const { skillRoot, manifests } = await validateSkillPackage(workingDir, packageNameOrPath);
|
|
277
|
+
const selected = await selectSkillManifests(skillRoot, manifests, options?.path);
|
|
278
|
+
const skillsDir = resolve(workingDir, "skills");
|
|
279
|
+
const sourceName = normalizeSkillSourceName(
|
|
280
|
+
resolveInstalledPackageName(packageNameOrPath) ?? basename(skillRoot),
|
|
281
|
+
);
|
|
282
|
+
const sourceSkillsDir = resolve(skillsDir, sourceName);
|
|
283
|
+
const skillNames = getSkillFolderNames(selected);
|
|
284
|
+
|
|
285
|
+
const removed: string[] = [];
|
|
286
|
+
const missing: string[] = [];
|
|
287
|
+
|
|
288
|
+
if (!options?.path && existsSync(sourceSkillsDir)) {
|
|
289
|
+
await rm(sourceSkillsDir, { recursive: true, force: false });
|
|
290
|
+
removed.push(`skills/${sourceName}`);
|
|
291
|
+
return { removed, missing };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
for (const skillName of skillNames) {
|
|
295
|
+
const destinationSkillDir = resolve(sourceSkillsDir, skillName);
|
|
296
|
+
const normalized = relative(skillsDir, destinationSkillDir).split("\\").join("/");
|
|
297
|
+
if (normalized.startsWith("..") || normalized.startsWith("/")) {
|
|
298
|
+
throw new Error(`Refusing to remove path outside skills directory: ${destinationSkillDir}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!existsSync(destinationSkillDir)) {
|
|
302
|
+
missing.push(`skills/${sourceName}/${skillName}`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await rm(destinationSkillDir, { recursive: true, force: false });
|
|
307
|
+
removed.push(`skills/${sourceName}/${skillName}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return { removed, missing };
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export const removeSkillPackage = async (
|
|
314
|
+
workingDir: string,
|
|
315
|
+
packageNameOrPath: string,
|
|
316
|
+
options?: { path?: string },
|
|
317
|
+
): Promise<void> => {
|
|
318
|
+
const result = await removeSkillsFromPackage(workingDir, packageNameOrPath, options);
|
|
319
|
+
process.stdout.write(
|
|
320
|
+
`Removed ${result.removed.length} skill${result.removed.length === 1 ? "" : "s"} from ${packageNameOrPath}:\n`,
|
|
321
|
+
);
|
|
322
|
+
for (const removed of result.removed) {
|
|
323
|
+
process.stdout.write(`- ${removed}\n`);
|
|
324
|
+
}
|
|
325
|
+
if (result.missing.length > 0) {
|
|
326
|
+
process.stdout.write(
|
|
327
|
+
`Skipped ${result.missing.length} missing skill${result.missing.length === 1 ? "" : "s"}:\n`,
|
|
328
|
+
);
|
|
329
|
+
for (const missing of result.missing) {
|
|
330
|
+
process.stdout.write(`- ${missing}\n`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
export const listInstalledSkills = async (
|
|
336
|
+
workingDir: string,
|
|
337
|
+
sourceName?: string,
|
|
338
|
+
): Promise<string[]> => {
|
|
339
|
+
const skillsRoot = resolve(workingDir, "skills");
|
|
340
|
+
const resolvedSourceName = sourceName
|
|
341
|
+
? resolveInstalledPackageName(sourceName) ?? sourceName
|
|
342
|
+
: undefined;
|
|
343
|
+
const targetRoot = sourceName
|
|
344
|
+
? resolve(skillsRoot, normalizeSkillSourceName(resolvedSourceName ?? sourceName))
|
|
345
|
+
: skillsRoot;
|
|
346
|
+
if (!existsSync(targetRoot)) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
const manifests = await collectSkillManifests(targetRoot, sourceName ? 1 : 2);
|
|
350
|
+
return manifests
|
|
351
|
+
.map((manifest) => relative(workingDir, dirname(manifest)).split("\\").join("/"))
|
|
352
|
+
.sort();
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
export const listSkills = async (workingDir: string, sourceName?: string): Promise<void> => {
|
|
356
|
+
const skills = await listInstalledSkills(workingDir, sourceName);
|
|
357
|
+
if (skills.length === 0) {
|
|
358
|
+
process.stdout.write("No installed skills found.\n");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const resolvedSourceName = sourceName
|
|
362
|
+
? resolveInstalledPackageName(sourceName) ?? sourceName
|
|
363
|
+
: undefined;
|
|
364
|
+
process.stdout.write(
|
|
365
|
+
sourceName
|
|
366
|
+
? `Installed skills for ${normalizeSkillSourceName(resolvedSourceName ?? sourceName)}:\n`
|
|
367
|
+
: "Installed skills:\n",
|
|
368
|
+
);
|
|
369
|
+
for (const skill of skills) {
|
|
370
|
+
process.stdout.write(`- ${skill}\n`);
|
|
371
|
+
}
|
|
372
|
+
};
|