@cursor/july 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agent-serve.js +20 -3
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.BR5EdVSq.js → app.CrsWMchO.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.BnHRjfoe.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CNCzfUKC.js → VPLocalSearchBox.D1JqzSh8.js} +1 -1
- package/dist/docs/assets/chunks/{theme.CwKvMSWv.js → theme.DaBvZYwl.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/cli-skills.d.ts +19 -0
- package/dist/internal/cli-skills.d.ts.map +1 -0
- package/dist/internal/cli-skills.js +72 -0
- package/dist/internal/install-cursor-skills.d.ts +46 -4
- package/dist/internal/install-cursor-skills.d.ts.map +1 -1
- package/dist/internal/install-cursor-skills.js +117 -49
- package/dist/playground/assets/{index-DRjS5klQ.js → index-C2SU2xV5.js} +23 -23
- package/dist/playground/assets/index-CidizGZv.css +1 -0
- package/dist/playground/index.html +2 -2
- package/package.json +1 -1
- package/src/bin/agent-serve.ts +21 -3
- package/src/internal/cli-skills.ts +95 -0
- package/src/internal/install-cursor-skills.ts +168 -60
- package/dist/docs/assets/chunks/@localSearchIndexroot.BOzkeUi8.js +0 -1
- package/dist/playground/assets/index-TCDs-UhZ.css +0 -1
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
/>
|
|
9
9
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
10
10
|
<title>agent-serve playground</title>
|
|
11
|
-
<script type="module" crossorigin src="./assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
11
|
+
<script type="module" crossorigin src="./assets/index-C2SU2xV5.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="./assets/index-CidizGZv.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cursor/july",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"repository": {
|
package/src/bin/agent-serve.ts
CHANGED
|
@@ -94,6 +94,7 @@ Usage:
|
|
|
94
94
|
${CLI} call <tool> [--input <json>] [--dir <path> | --url <baseUrl> | --prod]
|
|
95
95
|
[--session <id>] [--slug <slug>] [--team <id>]
|
|
96
96
|
${CLI} init [dir] [--json]
|
|
97
|
+
${CLI} install-skills [--print] [--json]
|
|
97
98
|
${CLI} mcp --prod [--slug <slug>] [--team <id>] [--dir <path>]
|
|
98
99
|
| --url <baseUrl> [--bearer-token <t>]
|
|
99
100
|
${CLI} mcp install [--name <serverName>] [--print] [--json]
|
|
@@ -173,6 +174,12 @@ Commands:
|
|
|
173
174
|
run login when unsigned, then print next steps. --json is
|
|
174
175
|
machine-readable and never blocks on login. No directory:
|
|
175
176
|
print the setup guide.
|
|
177
|
+
install-skills
|
|
178
|
+
Install (or refresh) the agentkit skills for coding agents in
|
|
179
|
+
~/.cursor/skills/agentkit and print where they landed. \`init\`
|
|
180
|
+
offers this once, interactively, and skips it when the prompt
|
|
181
|
+
goes unanswered; this command never asks. --print previews
|
|
182
|
+
without writing.
|
|
176
183
|
mcp Serve this agent's MCP endpoint over stdio for MCP clients
|
|
177
184
|
that spawn local servers (e.g. Cursor). Proxies
|
|
178
185
|
newline-delimited JSON-RPC to <target>/v1/mcp. --prod resolves
|
|
@@ -291,6 +298,8 @@ Options:
|
|
|
291
298
|
--once logs: print current progress / log buffer once and exit
|
|
292
299
|
(default is to follow).
|
|
293
300
|
--print playground: print the URL only; do not open a browser.
|
|
301
|
+
install-skills: list the skills and the install path
|
|
302
|
+
without writing them.
|
|
294
303
|
--no-wait deploy/stop/eval: return right after the request is
|
|
295
304
|
accepted instead of polling until a terminal status.
|
|
296
305
|
For eval, prints the runId (deployment / --url only).
|
|
@@ -332,9 +341,10 @@ Options:
|
|
|
332
341
|
--no-artifacts eval: do not write run artifacts.
|
|
333
342
|
--skip-report eval: ignore reporters from evals.config.ts / eval files.
|
|
334
343
|
--no-stream run/eval: silence live progress on stderr (JSON still prints).
|
|
335
|
-
--json JSON output (init/info/chat/eval/logs/
|
|
336
|
-
github; run defaults to JSON). logs:
|
|
337
|
-
events. session: trajectory JSON
|
|
344
|
+
--json JSON output (init/install-skills/info/chat/eval/logs/
|
|
345
|
+
sessions/session/github; run defaults to JSON). logs:
|
|
346
|
+
NDJSON progress/log events. session: trajectory JSON
|
|
347
|
+
(not raw events).
|
|
338
348
|
--no-color chat: force plain output.
|
|
339
349
|
--cursor-events serve: pull SCM webhook events from /v0/scm-events. The
|
|
340
350
|
stream is read as your Cursor user, so serve requires
|
|
@@ -558,6 +568,14 @@ async function main(argv: string[]): Promise<number> {
|
|
|
558
568
|
});
|
|
559
569
|
}
|
|
560
570
|
|
|
571
|
+
case "install-skills": {
|
|
572
|
+
const { cmdInstallSkills } = await import("../internal/cli-skills.js");
|
|
573
|
+
return cmdInstallSkills({
|
|
574
|
+
json: values.json,
|
|
575
|
+
print: values.print,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
|
|
561
579
|
case "mcp": {
|
|
562
580
|
const { cmdMcp } = await import("../internal/cli-mcp.js");
|
|
563
581
|
const mcpSub = positionals[0];
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `install-skills` — install the package's coding-agent skills into
|
|
3
|
+
* `~/.cursor/skills/agentkit/` without running `init`.
|
|
4
|
+
*
|
|
5
|
+
* Never prompts: running the command is the confirmation. The interactive
|
|
6
|
+
* offer lives in `init`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CLI_COMMAND_NAME } from "./distribution.js";
|
|
10
|
+
import {
|
|
11
|
+
formatSkillInstall,
|
|
12
|
+
installCursorSkills,
|
|
13
|
+
planSkillInstall,
|
|
14
|
+
type SkillInstallPlan,
|
|
15
|
+
} from "./install-cursor-skills.js";
|
|
16
|
+
import { stdoutPalette } from "./terminal-style.js";
|
|
17
|
+
|
|
18
|
+
export interface InstallSkillsCliOptions {
|
|
19
|
+
json?: boolean;
|
|
20
|
+
/** Preview the skills, the removals, and the install address; write nothing. */
|
|
21
|
+
print?: boolean;
|
|
22
|
+
/** Injected seams for tests. */
|
|
23
|
+
packageRoot?: string;
|
|
24
|
+
skillsHome?: string;
|
|
25
|
+
out?: (text: string) => void;
|
|
26
|
+
err?: (text: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function cmdInstallSkills(
|
|
30
|
+
options: InstallSkillsCliOptions = {}
|
|
31
|
+
): Promise<number> {
|
|
32
|
+
const out = options.out ?? ((text) => process.stdout.write(text));
|
|
33
|
+
const err = options.err ?? ((text) => process.stderr.write(text));
|
|
34
|
+
const plan = await planSkillInstall({
|
|
35
|
+
packageRoot: options.packageRoot,
|
|
36
|
+
skillsHome: options.skillsHome,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (plan.shipped.length === 0) {
|
|
40
|
+
err(`${CLI_COMMAND_NAME} ships no skills to install.\n`);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const emit = (run: { dryRun: boolean; removed: string[] }): void => {
|
|
45
|
+
const payload: SkillInstallPayload = {
|
|
46
|
+
ok: true,
|
|
47
|
+
dryRun: run.dryRun,
|
|
48
|
+
directory: plan.directory,
|
|
49
|
+
firstInstall: plan.firstInstall,
|
|
50
|
+
skills: plan.leaves,
|
|
51
|
+
removed: run.removed,
|
|
52
|
+
};
|
|
53
|
+
out(`${JSON.stringify(payload, null, 2)}\n`);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const noun = plan.shipped.length === 1 ? "skill" : "skills";
|
|
57
|
+
if (options.print === true) {
|
|
58
|
+
if (options.json === true) {
|
|
59
|
+
emit({ dryRun: true, removed: plan.orphans });
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
const action = plan.firstInstall ? "install" : "update";
|
|
63
|
+
out(`Would ${action} ${plan.leaves.length} agentkit ${noun}\n`);
|
|
64
|
+
out(previewBlock(plan));
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = await installCursorSkills(plan.shipped, plan.skillsHome);
|
|
69
|
+
if (options.json === true) {
|
|
70
|
+
emit({ dryRun: false, removed: result.removed });
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
out(formatSkillInstall(plan, result));
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface SkillInstallPayload {
|
|
78
|
+
ok: true;
|
|
79
|
+
/** True when `--print` previewed the install instead of running it. */
|
|
80
|
+
dryRun: boolean;
|
|
81
|
+
directory: string;
|
|
82
|
+
firstInstall: boolean;
|
|
83
|
+
skills: string[];
|
|
84
|
+
removed: string[];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function previewBlock(plan: SkillInstallPlan): string {
|
|
88
|
+
const c = stdoutPalette();
|
|
89
|
+
const lines = [c.dim(` skills: ${plan.leaves.join(", ")}`)];
|
|
90
|
+
if (plan.orphans.length > 0) {
|
|
91
|
+
lines.push(` removes: ${plan.orphans.join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
lines.push(` location: ${plan.directory}`);
|
|
94
|
+
return `${lines.join("\n")}\n`;
|
|
95
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "node:fs/promises";
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
-
import { packageRootDir } from "./distribution.js";
|
|
21
|
+
import { CLI_COMMAND_NAME, packageRootDir } from "./distribution.js";
|
|
22
22
|
import { stdoutPalette } from "./terminal-style.js";
|
|
23
23
|
|
|
24
24
|
/** Monorepo-only skill; excluded from the published package. */
|
|
@@ -27,6 +27,9 @@ const SKIP_SKILL_DIRS = new Set(["operate"]);
|
|
|
27
27
|
/** Category folder under ~/.cursor/skills (Cursor discovers nested skills). */
|
|
28
28
|
export const CURSOR_SKILLS_NAMESPACE = "agentkit";
|
|
29
29
|
|
|
30
|
+
/** How long the `init` offer waits for an answer before skipping the install. */
|
|
31
|
+
export const SKILL_PROMPT_TIMEOUT_MS = 45_000;
|
|
32
|
+
|
|
30
33
|
export interface PackageSkill {
|
|
31
34
|
/** Frontmatter `name` (e.g. agentkit-create-agent). */
|
|
32
35
|
name: string;
|
|
@@ -115,6 +118,13 @@ export async function findMissingCursorSkills(options?: {
|
|
|
115
118
|
return missing;
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
export interface SkillInstallResult {
|
|
122
|
+
/** Leaf dirs written. */
|
|
123
|
+
installed: string[];
|
|
124
|
+
/** Leaf dirs deleted because the package no longer ships them. */
|
|
125
|
+
removed: string[];
|
|
126
|
+
}
|
|
127
|
+
|
|
118
128
|
/**
|
|
119
129
|
* Copy each skill directory into `skillsHome/agentkit/<leafDir>/`.
|
|
120
130
|
* Always overwrites so a package upgrade refreshes local Cursor skills.
|
|
@@ -123,24 +133,13 @@ export async function findMissingCursorSkills(options?: {
|
|
|
123
133
|
export async function installCursorSkills(
|
|
124
134
|
skills: readonly PackageSkill[],
|
|
125
135
|
skillsHome: string = cursorSkillsHome()
|
|
126
|
-
): Promise<
|
|
136
|
+
): Promise<SkillInstallResult> {
|
|
127
137
|
const namespaceDir = agentkitCursorSkillsDir(skillsHome);
|
|
128
138
|
await mkdir(namespaceDir, { recursive: true });
|
|
129
139
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const entries = await readdir(namespaceDir, { withFileTypes: true });
|
|
134
|
-
existingLeaves = entries
|
|
135
|
-
.filter((entry) => entry.isDirectory())
|
|
136
|
-
.map((entry) => entry.name);
|
|
137
|
-
} catch {
|
|
138
|
-
existingLeaves = [];
|
|
139
|
-
}
|
|
140
|
-
for (const leaf of existingLeaves) {
|
|
141
|
-
if (!shippedLeaves.has(leaf)) {
|
|
142
|
-
await rm(join(namespaceDir, leaf), { recursive: true, force: true });
|
|
143
|
-
}
|
|
140
|
+
const removed = await orphanLeaves(namespaceDir, skills);
|
|
141
|
+
for (const leaf of removed) {
|
|
142
|
+
await rm(join(namespaceDir, leaf), { recursive: true, force: true });
|
|
144
143
|
}
|
|
145
144
|
|
|
146
145
|
const installed: string[] = [];
|
|
@@ -155,7 +154,24 @@ export async function installCursorSkills(
|
|
|
155
154
|
}
|
|
156
155
|
installed.push(skill.leafDir);
|
|
157
156
|
}
|
|
158
|
-
return installed;
|
|
157
|
+
return { installed, removed };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Installed leaf dirs the package no longer ships. Installing deletes these. */
|
|
161
|
+
async function orphanLeaves(
|
|
162
|
+
namespaceDir: string,
|
|
163
|
+
skills: readonly PackageSkill[]
|
|
164
|
+
): Promise<string[]> {
|
|
165
|
+
const shipped = new Set(skills.map((skill) => skill.leafDir));
|
|
166
|
+
let entries: Array<{ name: string; isDirectory: () => boolean }>;
|
|
167
|
+
try {
|
|
168
|
+
entries = await readdir(namespaceDir, { withFileTypes: true });
|
|
169
|
+
} catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
return entries
|
|
173
|
+
.filter((entry) => entry.isDirectory() && !shipped.has(entry.name))
|
|
174
|
+
.map((entry) => entry.name);
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
/** Drop monorepo `paths` globs and fix package doc links for ~/.cursor installs. */
|
|
@@ -191,20 +207,72 @@ function stripPathsFromFrontmatter(frontmatter: string): string {
|
|
|
191
207
|
return out.join("\n");
|
|
192
208
|
}
|
|
193
209
|
|
|
210
|
+
export interface SkillInstallPlan {
|
|
211
|
+
shipped: PackageSkill[];
|
|
212
|
+
/** Resolved `~/.cursor/skills`, to install into. */
|
|
213
|
+
skillsHome: string;
|
|
214
|
+
/** Install target: `<skillsHome>/agentkit`. */
|
|
215
|
+
directory: string;
|
|
216
|
+
/** Leaf dirs the package ships. */
|
|
217
|
+
leaves: string[];
|
|
218
|
+
/** Nothing installed yet, so this is a fresh install rather than a refresh. */
|
|
219
|
+
firstInstall: boolean;
|
|
220
|
+
/** Installed leaf dirs the package dropped; installing deletes them. */
|
|
221
|
+
orphans: string[];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** What an install would do, for the offer and for `install-skills`. */
|
|
225
|
+
export async function planSkillInstall(options?: {
|
|
226
|
+
packageRoot?: string;
|
|
227
|
+
skillsHome?: string;
|
|
228
|
+
}): Promise<SkillInstallPlan> {
|
|
229
|
+
const skillsHome = options?.skillsHome ?? cursorSkillsHome();
|
|
230
|
+
const directory = agentkitCursorSkillsDir(skillsHome);
|
|
231
|
+
const shipped = await listShippedPackageSkills(options?.packageRoot);
|
|
232
|
+
const missing = await findMissingCursorSkills({
|
|
233
|
+
packageRoot: options?.packageRoot,
|
|
234
|
+
skillsHome,
|
|
235
|
+
});
|
|
236
|
+
return {
|
|
237
|
+
shipped,
|
|
238
|
+
skillsHome,
|
|
239
|
+
directory,
|
|
240
|
+
leaves: shipped.map((skill) => skill.leafDir),
|
|
241
|
+
firstInstall: missing.length === shipped.length,
|
|
242
|
+
orphans: await orphanLeaves(directory, shipped),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** `Installed 9 agentkit skills`, what it dropped, and where it went. */
|
|
247
|
+
export function formatSkillInstall(
|
|
248
|
+
plan: SkillInstallPlan,
|
|
249
|
+
result: SkillInstallResult
|
|
250
|
+
): string {
|
|
251
|
+
const noun = result.installed.length === 1 ? "skill" : "skills";
|
|
252
|
+
const verb = plan.firstInstall ? "Installed" : "Updated";
|
|
253
|
+
const lines = [`${verb} ${result.installed.length} agentkit ${noun}`];
|
|
254
|
+
if (result.removed.length > 0) {
|
|
255
|
+
lines.push(` removed: ${result.removed.join(", ")}`);
|
|
256
|
+
}
|
|
257
|
+
lines.push(` location: ${plan.directory}`);
|
|
258
|
+
return `${lines.join("\n")}\n`;
|
|
259
|
+
}
|
|
260
|
+
|
|
194
261
|
export interface OfferInstallCursorSkillsOptions {
|
|
195
262
|
/** Skip when false (non-TTY / `--json`). */
|
|
196
263
|
interactive?: boolean;
|
|
197
264
|
packageRoot?: string;
|
|
198
265
|
skillsHome?: string;
|
|
199
|
-
/** Injected yes/no for tests. */
|
|
200
|
-
confirm?: (question: string) => Promise<boolean>;
|
|
266
|
+
/** Injected yes/no for tests. `undefined` means the prompt went unanswered. */
|
|
267
|
+
confirm?: (question: string) => Promise<boolean | undefined>;
|
|
201
268
|
out?: (text: string) => void;
|
|
202
269
|
}
|
|
203
270
|
|
|
204
271
|
/**
|
|
205
272
|
* On a TTY, offer to install/update every shipped package skill into
|
|
206
273
|
* ~/.cursor/skills/agentkit (always overwriting). No-op when non-interactive
|
|
207
|
-
* or the package ships no skills.
|
|
274
|
+
* or the package ships no skills. An unanswered prompt skips the install
|
|
275
|
+
* rather than blocking the caller — see `SKILL_PROMPT_TIMEOUT_MS`.
|
|
208
276
|
*/
|
|
209
277
|
export async function offerInstallCursorSkills(
|
|
210
278
|
options: OfferInstallCursorSkillsOptions = {}
|
|
@@ -213,48 +281,60 @@ export async function offerInstallCursorSkills(
|
|
|
213
281
|
const interactive =
|
|
214
282
|
options.interactive ??
|
|
215
283
|
(process.stdin.isTTY === true && process.stdout.isTTY === true);
|
|
216
|
-
|
|
217
|
-
if (shipped.length === 0) {
|
|
284
|
+
if (!interactive) {
|
|
218
285
|
return { installed: [], skipped: true };
|
|
219
286
|
}
|
|
220
287
|
|
|
221
|
-
const
|
|
222
|
-
const targetDir = agentkitCursorSkillsDir(home);
|
|
223
|
-
const missing = await findMissingCursorSkills({
|
|
288
|
+
const plan = await planSkillInstall({
|
|
224
289
|
packageRoot: options.packageRoot,
|
|
225
|
-
skillsHome:
|
|
290
|
+
skillsHome: options.skillsHome,
|
|
226
291
|
});
|
|
227
|
-
|
|
228
|
-
const noun = shipped.length === 1 ? "skill" : "skills";
|
|
229
|
-
const verb = missing.length === shipped.length ? "Install" : "Update";
|
|
230
|
-
const question = `${verb} ${shipped.length} agentkit ${noun} in ${targetDir}?`;
|
|
231
|
-
|
|
232
|
-
if (!interactive) {
|
|
292
|
+
if (plan.shipped.length === 0) {
|
|
233
293
|
return { installed: [], skipped: true };
|
|
234
294
|
}
|
|
235
295
|
|
|
296
|
+
const noun = plan.shipped.length === 1 ? "skill" : "skills";
|
|
297
|
+
const verb = plan.firstInstall ? "Install" : "Update";
|
|
236
298
|
const c = stdoutPalette();
|
|
237
|
-
out(
|
|
238
|
-
|
|
239
|
-
|
|
299
|
+
out(
|
|
300
|
+
`${verb} ${plan.shipped.length} agentkit ${noun} in ${plan.directory}?\n`
|
|
301
|
+
);
|
|
302
|
+
out(c.dim(` ${plan.leaves.join(", ")}\n`));
|
|
303
|
+
if (!plan.firstInstall) {
|
|
240
304
|
out(c.dim(" (overwrites existing copies with the package version)\n"));
|
|
241
305
|
}
|
|
306
|
+
if (plan.orphans.length > 0) {
|
|
307
|
+
out(c.dim(` (deletes ${plan.orphans.join(", ")})\n`));
|
|
308
|
+
}
|
|
242
309
|
|
|
243
310
|
const confirm =
|
|
244
|
-
options.confirm ??
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
311
|
+
options.confirm ??
|
|
312
|
+
((q) =>
|
|
313
|
+
promptYesNo(q, {
|
|
314
|
+
defaultYes: true,
|
|
315
|
+
out,
|
|
316
|
+
timeoutMs: SKILL_PROMPT_TIMEOUT_MS,
|
|
317
|
+
}));
|
|
318
|
+
const accepted = await confirm(`${verb} now?`);
|
|
319
|
+
|
|
320
|
+
if (accepted !== true) {
|
|
321
|
+
// `undefined` is the prompt timing out or stdin closing; both mean nobody
|
|
322
|
+
// said yes, so do not claim a duration neither case is guaranteed to have.
|
|
323
|
+
const why =
|
|
324
|
+
accepted === undefined
|
|
325
|
+
? "No answer — skipped the skill install."
|
|
326
|
+
: "Skipped skill install.";
|
|
327
|
+
out(
|
|
328
|
+
c.dim(
|
|
329
|
+
`${why}\nInstall later: ${CLI_COMMAND_NAME} install-skills (${plan.directory})\n\n`
|
|
330
|
+
)
|
|
331
|
+
);
|
|
250
332
|
return { installed: [], skipped: true };
|
|
251
333
|
}
|
|
252
334
|
|
|
253
|
-
const
|
|
254
|
-
out(
|
|
255
|
-
|
|
256
|
-
);
|
|
257
|
-
return { installed, skipped: false };
|
|
335
|
+
const result = await installCursorSkills(plan.shipped, plan.skillsHome);
|
|
336
|
+
out(`${formatSkillInstall(plan, result)}\n`);
|
|
337
|
+
return { installed: result.installed, skipped: false };
|
|
258
338
|
}
|
|
259
339
|
|
|
260
340
|
function parseSkillName(skillMd: string): string | undefined {
|
|
@@ -274,19 +354,34 @@ function parseSkillName(skillMd: string): string | undefined {
|
|
|
274
354
|
return raw.replace(/^["']|["']$/g, "");
|
|
275
355
|
}
|
|
276
356
|
|
|
277
|
-
|
|
357
|
+
export interface PromptYesNoOptions {
|
|
358
|
+
defaultYes: boolean;
|
|
359
|
+
out: (text: string) => void;
|
|
360
|
+
/** Give up after this long; `0` (or omitted) waits forever. */
|
|
361
|
+
timeoutMs?: number;
|
|
362
|
+
/** Answer source; defaults to `process.stdin`. */
|
|
363
|
+
input?: NodeJS.ReadableStream;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Ask a yes/no question. Resolves `undefined` when the answer never comes —
|
|
368
|
+
* the prompt timed out, or stdin closed on an empty line.
|
|
369
|
+
*/
|
|
370
|
+
export async function promptYesNo(
|
|
278
371
|
question: string,
|
|
279
|
-
options:
|
|
280
|
-
|
|
281
|
-
out: (text: string) => void;
|
|
282
|
-
}
|
|
283
|
-
): Promise<boolean> {
|
|
372
|
+
options: PromptYesNoOptions
|
|
373
|
+
): Promise<boolean | undefined> {
|
|
284
374
|
const hint = options.defaultYes ? "Y/n" : "y/N";
|
|
375
|
+
const input = options.input ?? process.stdin;
|
|
285
376
|
options.out(`${question} [${hint}] `);
|
|
286
|
-
|
|
287
|
-
const line = await readOneLine(
|
|
288
|
-
|
|
289
|
-
if (line === undefined
|
|
377
|
+
input.resume();
|
|
378
|
+
const line = await readOneLine(input, options.timeoutMs);
|
|
379
|
+
input.pause();
|
|
380
|
+
if (line === undefined) {
|
|
381
|
+
options.out("\n");
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
if (line.trim() === "") {
|
|
290
385
|
return options.defaultYes;
|
|
291
386
|
}
|
|
292
387
|
const answer = line.trim().toLowerCase();
|
|
@@ -299,11 +394,14 @@ async function promptYesNo(
|
|
|
299
394
|
return options.defaultYes;
|
|
300
395
|
}
|
|
301
396
|
|
|
397
|
+
/** The line, or `undefined` when the stream ends or the clock runs out first. */
|
|
302
398
|
function readOneLine(
|
|
303
|
-
stream: NodeJS.ReadableStream
|
|
399
|
+
stream: NodeJS.ReadableStream,
|
|
400
|
+
timeoutMs?: number
|
|
304
401
|
): Promise<string | undefined> {
|
|
305
402
|
return new Promise((resolve) => {
|
|
306
403
|
let buffer = "";
|
|
404
|
+
let timer: NodeJS.Timeout | undefined;
|
|
307
405
|
const onData = (chunk: string | Buffer): void => {
|
|
308
406
|
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf8");
|
|
309
407
|
const newline = buffer.indexOf("\n");
|
|
@@ -313,15 +411,25 @@ function readOneLine(
|
|
|
313
411
|
cleanup();
|
|
314
412
|
resolve(buffer.slice(0, newline).replace(/\r$/, ""));
|
|
315
413
|
};
|
|
316
|
-
|
|
414
|
+
// EOF and the timeout both land here. A tty holds a half-typed line until
|
|
415
|
+
// Enter, so a non-empty buffer only ever happens on piped input.
|
|
416
|
+
const giveUp = (): void => {
|
|
317
417
|
cleanup();
|
|
318
418
|
resolve(buffer === "" ? undefined : buffer.replace(/\r$/, ""));
|
|
319
419
|
};
|
|
320
420
|
const cleanup = (): void => {
|
|
421
|
+
if (timer !== undefined) {
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
}
|
|
321
424
|
stream.off("data", onData);
|
|
322
|
-
stream.off("end",
|
|
425
|
+
stream.off("end", giveUp);
|
|
323
426
|
};
|
|
324
427
|
stream.on("data", onData);
|
|
325
|
-
stream.on("end",
|
|
428
|
+
stream.on("end", giveUp);
|
|
429
|
+
if (timeoutMs !== undefined && timeoutMs > 0) {
|
|
430
|
+
timer = setTimeout(giveUp, timeoutMs);
|
|
431
|
+
// Never keep the process alive purely to wait for an answer.
|
|
432
|
+
timer.unref();
|
|
433
|
+
}
|
|
326
434
|
});
|
|
327
435
|
}
|