@ahood/cli 0.5.1 → 0.6.0
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 +6 -3
- package/dist/commands/add.js +265 -9
- package/dist/commands/completion.js +2 -1
- package/dist/commands/edit.js +5 -4
- package/dist/commands/group.js +8 -7
- package/dist/commands/init.js +8 -7
- package/dist/commands/list.js +5 -1
- package/dist/commands/mcp.js +7 -0
- package/dist/commands/publish.js +52 -13
- package/dist/commands/read.js +26 -8
- package/dist/commands/remove.js +44 -5
- package/dist/commands/search.js +17 -11
- package/dist/commands/share.js +5 -4
- package/dist/commands/star.js +2 -2
- package/dist/commands/token.js +4 -3
- package/dist/commands/unpublish.js +5 -4
- package/dist/commands/update.js +20 -3
- package/dist/commands/versions.js +8 -3
- package/dist/commands/view.js +8 -4
- package/dist/commands/whoami.js +58 -33
- package/dist/config.js +3 -2
- package/dist/exit-code.js +9 -0
- package/dist/flags.js +2 -1
- package/dist/help.js +95 -18
- package/dist/index.js +2 -0
- package/dist/lockfile.js +16 -7
- package/dist/mcp/safe-tool.js +46 -0
- package/dist/mcp/server.js +20 -0
- package/dist/mcp/tools.js +61 -0
- package/dist/secret-prompt.js +74 -0
- package/dist/spec.js +33 -9
- package/dist/usage-error.js +8 -0
- package/package.json +4 -2
package/dist/commands/publish.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createGzip } from "node:zlib";
|
|
|
5
5
|
import { apiJson, ApiError, sanitizeErrorMessage } from "../http.js";
|
|
6
6
|
import { flagValue } from "../flags.js";
|
|
7
7
|
import { parseOwnerSkill, SEMVER_RE, validateExternalUrl } from "../spec.js";
|
|
8
|
+
import { UsageError } from "../usage-error.js";
|
|
8
9
|
// Processing (decompress/validate/checksum/scan/upload) now happens in a
|
|
9
10
|
// background workflow, not inline in versions/complete's response (server
|
|
10
11
|
// issue #120) -- complete returns 202 immediately, so this polls
|
|
@@ -24,10 +25,10 @@ async function pollVersionStatus(owner, skill, version) {
|
|
|
24
25
|
return result;
|
|
25
26
|
await sleep(POLL_INTERVAL_MS);
|
|
26
27
|
}
|
|
27
|
-
throw new Error(`Still processing after ${POLL_TIMEOUT_MS / 1000}s -- check \`ahood view ${owner}/${skill}\` later for the result.`);
|
|
28
|
+
throw new Error(`Still processing after ${POLL_TIMEOUT_MS / 1000}s -- check \`ahood skill view ${owner}/${skill}\` later for the result.`);
|
|
28
29
|
}
|
|
29
|
-
const USAGE = "Usage: ahood publish <owner>/<skill>@<version> [--path <dir>] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--json]\n" +
|
|
30
|
-
" or: ahood publish <path> --owner <owner> --slug <skill> --version <x.y.z> [--json]";
|
|
30
|
+
const USAGE = "Usage: ahood skill publish <owner>/<skill>@<version> [--path <dir>] [--kind skill|agent|mcp] [--name <text>] [--tagline <text>] [--tags <comma,separated>] [--license <id>] [--homepage <url>] [--repository <url>] [--json]\n" +
|
|
31
|
+
" or: ahood skill publish <path> --owner <owner> --slug <skill> --version <x.y.z> [--json]";
|
|
31
32
|
// Matched by ENTRY NAME at every depth, not by path prefix, so a nested
|
|
32
33
|
// `vendor/thing/.git` is skipped the same as a top-level one. Not a
|
|
33
34
|
// .gitignore parser (out of scope) -- just the fixed set of names that must
|
|
@@ -109,25 +110,29 @@ function parsePublishArgs(args) {
|
|
|
109
110
|
const license = flagValue(args, "--license");
|
|
110
111
|
const homepage = flagValue(args, "--homepage");
|
|
111
112
|
const repository = flagValue(args, "--repository");
|
|
113
|
+
const kind = flagValue(args, "--kind");
|
|
112
114
|
let legacyPath;
|
|
113
115
|
const first = args[0];
|
|
114
116
|
if (first && !first.startsWith("--")) {
|
|
115
117
|
if (first.includes("/") && first.includes("@")) {
|
|
116
|
-
// Primary form: ahood publish <owner>/<skill>@<version>
|
|
118
|
+
// Primary form: ahood skill publish <owner>/<skill>@<version>
|
|
117
119
|
const parsed = parseOwnerSkill(first.slice(0, first.lastIndexOf("@")), USAGE);
|
|
118
120
|
owner = owner ?? parsed.owner;
|
|
119
121
|
skill = skill ?? parsed.skill;
|
|
120
122
|
version = version ?? first.slice(first.lastIndexOf("@") + 1);
|
|
121
123
|
}
|
|
122
124
|
else {
|
|
123
|
-
// Legacy form: ahood publish <path> --owner ... --slug ... --version ...
|
|
125
|
+
// Legacy form: ahood skill publish <path> --owner ... --slug ... --version ...
|
|
124
126
|
legacyPath = first;
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
if (!owner || !skill || !version)
|
|
128
|
-
throw new
|
|
130
|
+
throw new UsageError(USAGE);
|
|
129
131
|
if (!SEMVER_RE.test(version)) {
|
|
130
|
-
throw new
|
|
132
|
+
throw new UsageError(`--version must be a semver like 1.2.3 (got "${version}").\n${USAGE}`);
|
|
133
|
+
}
|
|
134
|
+
if (kind !== undefined && kind !== "skill" && kind !== "agent" && kind !== "mcp") {
|
|
135
|
+
throw new UsageError(`--kind must be "skill", "agent", or "mcp" (got "${kind}").\n${USAGE}`);
|
|
131
136
|
}
|
|
132
137
|
// Validated here, before any network call (including the tar/gzip of the
|
|
133
138
|
// skill directory) -- these only ever reach the server via
|
|
@@ -136,7 +141,7 @@ function parsePublishArgs(args) {
|
|
|
136
141
|
validateExternalUrl(homepage, "--homepage");
|
|
137
142
|
if (repository !== undefined)
|
|
138
143
|
validateExternalUrl(repository, "--repository");
|
|
139
|
-
return { owner, skill, version, path: pathFlag ?? legacyPath ?? ".", name, tagline, tags, license, homepage, repository };
|
|
144
|
+
return { owner, skill, version, path: pathFlag ?? legacyPath ?? ".", name, tagline, tags, license, homepage, repository, kind };
|
|
140
145
|
}
|
|
141
146
|
// Creates the skill under the caller's own account when versions/init 404s
|
|
142
147
|
// -- skill creation is CLI-only now (there is no separate `ahood create`;
|
|
@@ -159,6 +164,8 @@ async function createSkillForPublish(owner, skill, opts, jsonOutput) {
|
|
|
159
164
|
body.homepage = opts.homepage;
|
|
160
165
|
if (opts.repository !== undefined)
|
|
161
166
|
body.repository = opts.repository;
|
|
167
|
+
if (opts.kind !== undefined)
|
|
168
|
+
body.kind = opts.kind;
|
|
162
169
|
const created = await apiJson("/api/v1/skills", {
|
|
163
170
|
method: "POST",
|
|
164
171
|
headers: { "Content-Type": "application/json" },
|
|
@@ -171,7 +178,7 @@ async function createSkillForPublish(owner, skill, opts, jsonOutput) {
|
|
|
171
178
|
if (created.owner !== owner) {
|
|
172
179
|
throw new Error(`Created ${created.owner}/${created.slug}, but this publish targeted "${owner}/${skill}" -- skills ` +
|
|
173
180
|
`are always created under your own account (${created.owner}). Retry with: ` +
|
|
174
|
-
`ahood publish ${created.owner}/${skill}@<version>.`);
|
|
181
|
+
`ahood skill publish ${created.owner}/${skill}@<version>.`);
|
|
175
182
|
}
|
|
176
183
|
if (!jsonOutput) {
|
|
177
184
|
console.log(`Created ${created.owner}/${created.slug} -- publishing its first version now.`);
|
|
@@ -205,13 +212,45 @@ async function initVersion(owner, skill, version, packageSizeBytes, createOpts,
|
|
|
205
212
|
export async function publish(args) {
|
|
206
213
|
const jsonOutput = args.includes("--json");
|
|
207
214
|
try {
|
|
208
|
-
const { owner, skill, version, path, name, tagline, tags, license, homepage, repository } = parsePublishArgs(args);
|
|
215
|
+
const { owner, skill, version, path, name, tagline, tags, license, homepage, repository, kind } = parsePublishArgs(args);
|
|
209
216
|
const skillMdPath = join(path, "SKILL.md");
|
|
210
|
-
|
|
211
|
-
|
|
217
|
+
const agentMdPath = join(path, "AGENT.md");
|
|
218
|
+
const serverJsonPath = join(path, "server.json");
|
|
219
|
+
const hasSkillMd = existsSync(skillMdPath);
|
|
220
|
+
const hasAgentMd = existsSync(agentMdPath);
|
|
221
|
+
const hasServerJson = existsSync(serverJsonPath);
|
|
222
|
+
let resolvedKind = kind;
|
|
223
|
+
if (kind === "agent") {
|
|
224
|
+
if (!hasAgentMd)
|
|
225
|
+
throw new Error(`No AGENT.md found at ${agentMdPath} -- publish --kind agent must point at a folder containing AGENT.md.`);
|
|
226
|
+
}
|
|
227
|
+
else if (kind === "skill") {
|
|
228
|
+
if (!hasSkillMd)
|
|
229
|
+
throw new Error(`No SKILL.md found at ${skillMdPath} -- publish --kind skill must point at a folder containing SKILL.md.`);
|
|
230
|
+
}
|
|
231
|
+
else if (kind === "mcp") {
|
|
232
|
+
if (!hasServerJson)
|
|
233
|
+
throw new Error(`No server.json found at ${serverJsonPath} -- publish --kind mcp must point at a folder containing server.json.`);
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
// kind === undefined -- infer from whichever root file(s) are actually present
|
|
237
|
+
const present = [hasSkillMd && "SKILL.md", hasAgentMd && "AGENT.md", hasServerJson && "server.json"].filter(Boolean);
|
|
238
|
+
if (present.length > 1) {
|
|
239
|
+
throw new Error(`Multiple root files found at ${path} (${present.join(", ")}) -- pass --kind skill, --kind agent, or --kind mcp to disambiguate.`);
|
|
240
|
+
}
|
|
241
|
+
if (present.length === 0) {
|
|
242
|
+
throw new Error(`No SKILL.md, AGENT.md, or server.json found at ${path} -- publish must point at a skill, agent, or mcp folder's root.`);
|
|
243
|
+
}
|
|
244
|
+
if (hasAgentMd) {
|
|
245
|
+
resolvedKind = "agent"; // inferred: only AGENT.md present, no --kind needed
|
|
246
|
+
}
|
|
247
|
+
else if (hasServerJson) {
|
|
248
|
+
resolvedKind = "mcp"; // inferred: only server.json present, no --kind needed
|
|
249
|
+
}
|
|
250
|
+
// else: hasSkillMd only -- resolvedKind stays undefined (unchanged existing behavior, server defaults to 'skill')
|
|
212
251
|
}
|
|
213
252
|
const archive = await tarGzDirectory(path);
|
|
214
|
-
const { init, created } = await initVersion(owner, skill, version, archive.length, { name, tagline, tags, license, homepage, repository }, jsonOutput);
|
|
253
|
+
const { init, created } = await initVersion(owner, skill, version, archive.length, { name, tagline, tags, license, homepage, repository, kind: resolvedKind }, jsonOutput);
|
|
215
254
|
// TS's lib.dom BodyInit (in scope here since tsconfig has no explicit
|
|
216
255
|
// "lib" override, so DOM is included alongside the Node types) type-checks
|
|
217
256
|
// Buffer/Uint8Array against `Uint8Array<ArrayBuffer>` specifically as of
|
package/dist/commands/read.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { apiJson } from "../http.js";
|
|
2
2
|
import { parseOwnerSkill } from "../spec.js";
|
|
3
|
+
import { UsageError } from "../usage-error.js";
|
|
3
4
|
const USAGE = "Usage: ahood skill read <owner>/<skill> [--json]";
|
|
4
|
-
export async function
|
|
5
|
-
const jsonOutput = args.includes("--json");
|
|
6
|
-
const spec = args.find((a) => !a.startsWith("--"));
|
|
7
|
-
if (!spec)
|
|
8
|
-
throw new Error(USAGE);
|
|
9
|
-
const { owner, skill } = parseOwnerSkill(spec, USAGE);
|
|
5
|
+
export async function readSkillMd(owner, skill) {
|
|
10
6
|
const detail = await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}`);
|
|
11
7
|
if (!detail.skill_versions) {
|
|
12
8
|
throw new Error(`${owner}/${skill} has no published version.`);
|
|
13
9
|
}
|
|
14
10
|
// Mirrors view.ts's exact wording/tone for the same warning -- this
|
|
15
11
|
// command reads straight from the same endpoint/version, so it must not
|
|
16
|
-
// be silent about handing back a yanked version's content.
|
|
12
|
+
// be silent about handing back a yanked version's content. Written to
|
|
13
|
+
// stderr, not stdout -- safe for MCP callers too (only stdout is reserved
|
|
14
|
+
// for the JSON-RPC stream).
|
|
17
15
|
if (detail.skill_versions.yanked_at) {
|
|
18
16
|
console.warn(`WARNING: ${detail.owner}/${detail.slug}@${detail.skill_versions.version} has been yanked${detail.skill_versions.yanked_reason ? `: ${detail.skill_versions.yanked_reason}` : "."}`);
|
|
19
17
|
}
|
|
@@ -21,8 +19,28 @@ export async function read(args) {
|
|
|
21
19
|
if (!content) {
|
|
22
20
|
throw new Error(`${owner}/${skill}@${detail.skill_versions.version} has no SKILL.md content available.`);
|
|
23
21
|
}
|
|
22
|
+
return {
|
|
23
|
+
version: detail.skill_versions.version,
|
|
24
|
+
content,
|
|
25
|
+
// The CLI already warns about a yanked version on stderr above, but an
|
|
26
|
+
// MCP caller has no visibility into stderr -- unlike skill_view, whose
|
|
27
|
+
// returned object already surfaces yanked_at/yanked_reason, skill_read
|
|
28
|
+
// was silently dropping this signal for MCP callers entirely. read()
|
|
29
|
+
// below still destructures only {version, content} for the CLI's
|
|
30
|
+
// --json output, so this addition doesn't change that shape.
|
|
31
|
+
yanked_at: detail.skill_versions.yanked_at ?? null,
|
|
32
|
+
yanked_reason: detail.skill_versions.yanked_reason ?? null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function read(args) {
|
|
36
|
+
const jsonOutput = args.includes("--json");
|
|
37
|
+
const spec = args.find((a) => !a.startsWith("--"));
|
|
38
|
+
if (!spec)
|
|
39
|
+
throw new UsageError(USAGE);
|
|
40
|
+
const { owner, skill } = parseOwnerSkill(spec, USAGE);
|
|
41
|
+
const { version, content } = await readSkillMd(owner, skill);
|
|
24
42
|
if (jsonOutput) {
|
|
25
|
-
console.log(JSON.stringify({ version
|
|
43
|
+
console.log(JSON.stringify({ version, content }));
|
|
26
44
|
return;
|
|
27
45
|
}
|
|
28
46
|
// Plain mode prints the raw content verbatim -- no formatting/labels/
|
package/dist/commands/remove.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { existsSync, readdirSync, rmSync, rmdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, rmdirSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { removeLockfileEntry } from "../lockfile.js";
|
|
4
|
-
import { LOCKFILE_PATH, parseOwnerSkill, skillDir } from "../spec.js";
|
|
5
|
-
|
|
4
|
+
import { LOCKFILE_PATH, parseOwnerSkill, skillDir, agentPath, MCP_CONFIG_PATH } from "../spec.js";
|
|
5
|
+
import { UsageError } from "../usage-error.js";
|
|
6
|
+
const USAGE = "Usage: ahood skill remove <owner>/<skill>";
|
|
6
7
|
export async function remove(args) {
|
|
7
8
|
const spec = args[0];
|
|
8
9
|
if (!spec)
|
|
9
|
-
throw new
|
|
10
|
+
throw new UsageError(USAGE);
|
|
10
11
|
const { owner, skill } = parseOwnerSkill(spec, USAGE);
|
|
11
12
|
const key = `${owner}/${skill}`;
|
|
12
13
|
const dir = skillDir(owner, skill);
|
|
@@ -20,11 +21,49 @@ export async function remove(args) {
|
|
|
20
21
|
const ownerDir = dirname(dir);
|
|
21
22
|
if (existsSync(ownerDir) && readdirSync(ownerDir).length === 0)
|
|
22
23
|
rmdirSync(ownerDir);
|
|
24
|
+
// An agent installs as a single flat file (.claude/agents/<owner>@<skill>.md),
|
|
25
|
+
// never a directory under .claude/skills/ -- skillDir's rmSync above never
|
|
26
|
+
// touches it. Without this, removing an installed agent silently left the
|
|
27
|
+
// file on disk (still loaded by Claude Code forever) while reporting
|
|
28
|
+
// success and clearing the lockfile entry, so a later `update` wouldn't
|
|
29
|
+
// catch it either (ahood-cli final review finding #2).
|
|
30
|
+
const agentFile = agentPath(owner, skill);
|
|
31
|
+
const agentExisted = existsSync(agentFile);
|
|
32
|
+
if (agentExisted)
|
|
33
|
+
unlinkSync(agentFile);
|
|
23
34
|
const hadLockfileEntry = removeLockfileEntry(LOCKFILE_PATH, key);
|
|
24
|
-
if (!dirExisted && !hadLockfileEntry) {
|
|
35
|
+
if (!dirExisted && !agentExisted && !hadLockfileEntry) {
|
|
25
36
|
console.error(`${key} was not installed -- nothing to remove.`);
|
|
26
37
|
process.exitCode = 1;
|
|
27
38
|
return;
|
|
28
39
|
}
|
|
40
|
+
// An mcp-kind install has no directory or agent file on disk -- its only
|
|
41
|
+
// footprint here is the lockfile entry just cleared above and a live
|
|
42
|
+
// entry in .mcp.json (which add.ts's installMcpEntry merged in, possibly
|
|
43
|
+
// holding a resolved secret in its `env`). remove() doesn't attempt a real
|
|
44
|
+
// mcp removal, but staying silent about that entry repeats the exact
|
|
45
|
+
// false-assurance bug already fixed once for agent installs above: a user
|
|
46
|
+
// who removes an mcp artifact because they no longer trust it would be
|
|
47
|
+
// told it's gone while the MCP server (and its credential) still runs on
|
|
48
|
+
// the next Claude Code start -- and since the lockfile pin is now cleared,
|
|
49
|
+
// nothing will ever surface this again via `list`/`update`. So: warn
|
|
50
|
+
// instead of pretending it's gone.
|
|
51
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
52
|
+
try {
|
|
53
|
+
const parsed = JSON.parse(readFileSync(MCP_CONFIG_PATH, "utf-8"));
|
|
54
|
+
const mcpServers = parsed?.mcpServers;
|
|
55
|
+
if (mcpServers &&
|
|
56
|
+
typeof mcpServers === "object" &&
|
|
57
|
+
!Array.isArray(mcpServers) &&
|
|
58
|
+
Object.prototype.hasOwnProperty.call(mcpServers, skill)) {
|
|
59
|
+
console.warn(`WARNING: ${key} still has an entry in ${MCP_CONFIG_PATH} (which may contain secrets you entered) -- remove it manually.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// A malformed .mcp.json isn't this command's problem to fix or crash
|
|
64
|
+
// on -- add.ts's readMcpConfig is the strict validator for that path.
|
|
65
|
+
// Skip the warning rather than throw here.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
29
68
|
console.log(`Removed ${key}`);
|
|
30
69
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -1,23 +1,29 @@
|
|
|
1
1
|
import { apiJson } from "../http.js";
|
|
2
2
|
import { flagValue } from "../flags.js";
|
|
3
|
-
|
|
3
|
+
import { UsageError } from "../usage-error.js";
|
|
4
|
+
const USAGE = "Usage: ahood skill search <query> [--json] [--limit <n>]";
|
|
5
|
+
export async function searchSkills(query, limit) {
|
|
6
|
+
const qs = new URLSearchParams({ q: query });
|
|
7
|
+
if (limit !== undefined)
|
|
8
|
+
qs.set("per_page", String(limit));
|
|
9
|
+
const { skills } = await apiJson(`/api/v1/skills?${qs}`);
|
|
10
|
+
return skills ?? [];
|
|
11
|
+
}
|
|
4
12
|
export async function search(args) {
|
|
5
13
|
const jsonOutput = args.includes("--json");
|
|
6
|
-
const
|
|
14
|
+
const limitStr = flagValue(args, "--limit");
|
|
7
15
|
const queryParts = args.filter((a, i) => a !== "--json" && a !== "--limit" && args[i - 1] !== "--limit");
|
|
8
16
|
const unknownFlag = queryParts.find((a) => a.startsWith("--"));
|
|
9
17
|
if (unknownFlag)
|
|
10
|
-
throw new
|
|
18
|
+
throw new UsageError(`Unknown flag: ${unknownFlag}\n${USAGE}`);
|
|
11
19
|
const query = queryParts.join(" ");
|
|
12
20
|
if (!query)
|
|
13
|
-
throw new
|
|
14
|
-
if (
|
|
15
|
-
throw new
|
|
21
|
+
throw new UsageError(USAGE);
|
|
22
|
+
if (limitStr !== undefined && (!/^\d+$/.test(limitStr) || Number(limitStr) < 1)) {
|
|
23
|
+
throw new UsageError(`--limit must be a positive integer (got "${limitStr}").\n${USAGE}`);
|
|
16
24
|
}
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
qs.set("per_page", limit);
|
|
20
|
-
const { skills } = await apiJson(`/api/v1/skills?${qs}`);
|
|
25
|
+
const limit = limitStr !== undefined ? Number(limitStr) : undefined;
|
|
26
|
+
const skills = await searchSkills(query, limit);
|
|
21
27
|
if (jsonOutput) {
|
|
22
28
|
console.log(JSON.stringify(skills));
|
|
23
29
|
return;
|
|
@@ -29,7 +35,7 @@ export async function search(args) {
|
|
|
29
35
|
for (const skill of skills) {
|
|
30
36
|
console.log(`${skill.profiles.username}/${skill.slug} - ${skill.name}${skill.tagline ? `: ${skill.tagline}` : ""} (${skill.downloads_count} downloads)`);
|
|
31
37
|
}
|
|
32
|
-
if (limit !== undefined && skills.length >=
|
|
38
|
+
if (limit !== undefined && skills.length >= limit) {
|
|
33
39
|
console.log(`(showing up to ${limit} results -- pass a higher --limit for more)`);
|
|
34
40
|
}
|
|
35
41
|
}
|
package/dist/commands/share.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { apiJson } from "../http.js";
|
|
2
2
|
import { flagValue } from "../flags.js";
|
|
3
3
|
import { parseOwnerSkill } from "../spec.js";
|
|
4
|
+
import { UsageError } from "../usage-error.js";
|
|
4
5
|
// These are skill-entity verbs (reached as `ahood skill share`/`ahood skill
|
|
5
6
|
// unshare`, registered in SKILL_COMMANDS in index.ts), not group verbs --
|
|
6
7
|
// they mutate a skill's sharing, not a group. Grouped together in one file
|
|
@@ -16,11 +17,11 @@ const UNSHARE_USAGE = "Usage: ahood skill unshare <owner>/<skill> --group <group
|
|
|
16
17
|
export async function share(args) {
|
|
17
18
|
const spec = args[0];
|
|
18
19
|
if (!spec || spec.startsWith("--"))
|
|
19
|
-
throw new
|
|
20
|
+
throw new UsageError(SHARE_USAGE);
|
|
20
21
|
const { owner, skill } = parseOwnerSkill(spec, SHARE_USAGE);
|
|
21
22
|
const groupSlug = flagValue(args, "--group");
|
|
22
23
|
if (!groupSlug)
|
|
23
|
-
throw new
|
|
24
|
+
throw new UsageError(SHARE_USAGE);
|
|
24
25
|
await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/share`, {
|
|
25
26
|
method: "POST",
|
|
26
27
|
headers: { "Content-Type": "application/json" },
|
|
@@ -31,11 +32,11 @@ export async function share(args) {
|
|
|
31
32
|
export async function unshare(args) {
|
|
32
33
|
const spec = args[0];
|
|
33
34
|
if (!spec || spec.startsWith("--"))
|
|
34
|
-
throw new
|
|
35
|
+
throw new UsageError(UNSHARE_USAGE);
|
|
35
36
|
const { owner, skill } = parseOwnerSkill(spec, UNSHARE_USAGE);
|
|
36
37
|
const groupSlug = flagValue(args, "--group");
|
|
37
38
|
if (!groupSlug)
|
|
38
|
-
throw new
|
|
39
|
+
throw new UsageError(UNSHARE_USAGE);
|
|
39
40
|
await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/share/${encodeURIComponent(groupSlug)}`, { method: "DELETE" });
|
|
40
41
|
console.log(`Unshared ${owner}/${skill} from ${groupSlug}.`);
|
|
41
42
|
}
|
package/dist/commands/star.js
CHANGED
|
@@ -6,7 +6,7 @@ import { parseOwnerSkill } from "../spec.js";
|
|
|
6
6
|
// deletes any matching row), so starring an already-starred skill or
|
|
7
7
|
// unstarring one you never starred is a no-op 200, not an error.
|
|
8
8
|
export async function star(args) {
|
|
9
|
-
const USAGE = "Usage: ahood star <owner>/<skill>";
|
|
9
|
+
const USAGE = "Usage: ahood skill star <owner>/<skill>";
|
|
10
10
|
const { owner, skill } = parseOwnerSkill(args[0] ?? "", USAGE);
|
|
11
11
|
await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/star`, {
|
|
12
12
|
method: "POST",
|
|
@@ -14,7 +14,7 @@ export async function star(args) {
|
|
|
14
14
|
console.log(`Starred ${owner}/${skill}.`);
|
|
15
15
|
}
|
|
16
16
|
export async function unstar(args) {
|
|
17
|
-
const USAGE = "Usage: ahood unstar <owner>/<skill>";
|
|
17
|
+
const USAGE = "Usage: ahood skill unstar <owner>/<skill>";
|
|
18
18
|
const { owner, skill } = parseOwnerSkill(args[0] ?? "", USAGE);
|
|
19
19
|
await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/star`, {
|
|
20
20
|
method: "DELETE",
|
package/dist/commands/token.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { apiJson } from "../http.js";
|
|
2
2
|
import { confirm } from "../confirm.js";
|
|
3
|
+
import { UsageError } from "../usage-error.js";
|
|
3
4
|
export async function token(args) {
|
|
4
5
|
const [subcommand, ...rest] = args;
|
|
5
6
|
switch (subcommand) {
|
|
@@ -10,13 +11,13 @@ export async function token(args) {
|
|
|
10
11
|
case "revoke":
|
|
11
12
|
return tokenRevoke(rest);
|
|
12
13
|
default:
|
|
13
|
-
throw new
|
|
14
|
+
throw new UsageError("Usage: ahood token create <name>|list [--json]|revoke <id> [--yes]");
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
async function tokenCreate(args) {
|
|
17
18
|
const name = args[0];
|
|
18
19
|
if (!name)
|
|
19
|
-
throw new
|
|
20
|
+
throw new UsageError("Usage: ahood token create <name>");
|
|
20
21
|
// Note: this itself requires an existing session-backed token or a
|
|
21
22
|
// browser login -- per docs/adr/backend/0001-backend-services.md's Phase
|
|
22
23
|
// 3 section, tokens can't mint tokens (POST /auth/tokens is session-only,
|
|
@@ -52,7 +53,7 @@ async function tokenRevoke(args) {
|
|
|
52
53
|
const yes = args.includes("--yes");
|
|
53
54
|
const id = args.find((a) => !a.startsWith("--"));
|
|
54
55
|
if (!id)
|
|
55
|
-
throw new
|
|
56
|
+
throw new UsageError("Usage: ahood token revoke <id> [--yes]");
|
|
56
57
|
// --yes bypasses both the lookup and the prompt entirely, preserving the
|
|
57
58
|
// original one-shot behavior (a single DELETE call) for scripts/CI that
|
|
58
59
|
// already call `ahood token revoke <id> --yes` unattended.
|
|
@@ -2,7 +2,8 @@ import { apiJson } from "../http.js";
|
|
|
2
2
|
import { confirm } from "../confirm.js";
|
|
3
3
|
import { parseOwnerSkill, parseOwnerSkillVersion } from "../spec.js";
|
|
4
4
|
import { fetchVersionMeta } from "./add.js";
|
|
5
|
-
|
|
5
|
+
import { UsageError } from "../usage-error.js";
|
|
6
|
+
const USAGE = "Usage: ahood skill unpublish <owner>/<skill>[@version] [--yes]";
|
|
6
7
|
// A bare "owner/skill" has no "@" at all, so lastIndexOf("@") is -1 and this
|
|
7
8
|
// is a whole-skill unpublish (preserves the exact pre-existing behavior).
|
|
8
9
|
// "owner/skill@1.2.3" has an "@" at index > 0 and is a single-version yank
|
|
@@ -16,7 +17,7 @@ export async function unpublish(args) {
|
|
|
16
17
|
const yes = args.includes("--yes");
|
|
17
18
|
const spec = args.find((a) => !a.startsWith("--"));
|
|
18
19
|
if (!spec)
|
|
19
|
-
throw new
|
|
20
|
+
throw new UsageError(USAGE);
|
|
20
21
|
if (isVersioned(spec)) {
|
|
21
22
|
const { owner, skill, version: parsedVersion } = parseOwnerSkillVersion(spec, USAGE);
|
|
22
23
|
// parseOwnerSkillVersion only validates *syntax* -- an omitted "@version"
|
|
@@ -63,7 +64,7 @@ export async function unpublish(args) {
|
|
|
63
64
|
// the whole request, since the yank already took effect. Discarding the
|
|
64
65
|
// response body would silently drop that signal (ahood-cli#68): the
|
|
65
66
|
// pointer could now be stale with nothing in the terminal to say so, and
|
|
66
|
-
// a later `ahood add owner/skill` trusts latest_version_id completely.
|
|
67
|
+
// a later `ahood skill add owner/skill` trusts latest_version_id completely.
|
|
67
68
|
if (result.latest_version_warning) {
|
|
68
69
|
console.warn(`WARNING: ${result.latest_version_warning}`);
|
|
69
70
|
}
|
|
@@ -82,5 +83,5 @@ export async function unpublish(args) {
|
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}`, { method: "DELETE" });
|
|
85
|
-
console.log(`Unpublished ${key}. Run \`ahood remove ${key}\` to also remove your local copy.`);
|
|
86
|
+
console.log(`Unpublished ${key}. Run \`ahood skill remove ${key}\` to also remove your local copy.`);
|
|
86
87
|
}
|
package/dist/commands/update.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { LOCKFILE_PATH, parseOwnerSkill } from "../spec.js";
|
|
2
2
|
import { readLockfile } from "../lockfile.js";
|
|
3
3
|
import { add, fetchVersionMeta } from "./add.js";
|
|
4
|
-
|
|
4
|
+
import { UsageError } from "../usage-error.js";
|
|
5
|
+
const USAGE = "Usage: ahood skill update [<owner>/<skill> ...] [--dry-run] [--json]";
|
|
5
6
|
async function previewSkill(ownerSlashSkill, currentVersion) {
|
|
6
7
|
const { owner, skill } = parseOwnerSkill(ownerSlashSkill, USAGE);
|
|
7
8
|
const meta = await fetchVersionMeta(owner, skill, "latest");
|
|
@@ -47,7 +48,7 @@ export async function update(args) {
|
|
|
47
48
|
const dryRun = args.includes("--dry-run");
|
|
48
49
|
const jsonOutput = args.includes("--json");
|
|
49
50
|
if (jsonOutput && !dryRun) {
|
|
50
|
-
throw new
|
|
51
|
+
throw new UsageError(`--json is only supported together with --dry-run.\n${USAGE}`);
|
|
51
52
|
}
|
|
52
53
|
const targets = args.filter((a) => a !== "--dry-run" && a !== "--json");
|
|
53
54
|
const lockfile = readLockfile(LOCKFILE_PATH);
|
|
@@ -88,7 +89,7 @@ export async function update(args) {
|
|
|
88
89
|
for (const ownerSlashSkill of skillKeys) {
|
|
89
90
|
if (!lockfile[ownerSlashSkill]) {
|
|
90
91
|
// Reachable two ways: an explicitly-named skill that was never
|
|
91
|
-
// installed (`ahood add` is the right command for that -- `update`
|
|
92
|
+
// installed (`ahood skill add` is the right command for that -- `update`
|
|
92
93
|
// must not silently install and pin it), or -- when targets came from
|
|
93
94
|
// the lockfile itself -- the lockfile changing out from under us
|
|
94
95
|
// mid-loop. Either way: skip, don't abort.
|
|
@@ -96,6 +97,22 @@ export async function update(args) {
|
|
|
96
97
|
continue;
|
|
97
98
|
}
|
|
98
99
|
try {
|
|
100
|
+
// add() always hits the .mcp.json collision check for an mcp-kind
|
|
101
|
+
// entry that's already installed (that's the very definition of
|
|
102
|
+
// "already installed" for that kind), so calling it unconditionally
|
|
103
|
+
// here would make `ahood skill update` with no arguments permanently
|
|
104
|
+
// report a failure and exit 1 for every user who has ever installed
|
|
105
|
+
// an mcp artifact, even when nothing needs updating. fetchVersionMeta
|
|
106
|
+
// already resolves `kind` as part of the normal "latest" lookup add()
|
|
107
|
+
// itself does, so resolving it here first lets this skip cleanly
|
|
108
|
+
// instead of updating and hitting that guaranteed error. This is a
|
|
109
|
+
// skip-and-report-cleanly fix, not real mcp-update support.
|
|
110
|
+
const { owner, skill } = parseOwnerSkill(ownerSlashSkill, USAGE);
|
|
111
|
+
const meta = await fetchVersionMeta(owner, skill, "latest");
|
|
112
|
+
if (meta.kind === "mcp") {
|
|
113
|
+
console.warn(`Skipping ${ownerSlashSkill}: mcp artifacts aren't updatable via this command yet.`);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
99
116
|
await add([ownerSlashSkill]); // no @version -- resolves to latest again
|
|
100
117
|
}
|
|
101
118
|
catch (error) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { apiJson } from "../http.js";
|
|
2
2
|
import { parseOwnerSkill } from "../spec.js";
|
|
3
|
-
|
|
3
|
+
import { UsageError } from "../usage-error.js";
|
|
4
|
+
const USAGE = "Usage: ahood skill versions <owner>/<skill> [--json]";
|
|
4
5
|
function formatSize(bytes) {
|
|
5
6
|
if (bytes < 1024)
|
|
6
7
|
return `${bytes} B`;
|
|
@@ -13,13 +14,17 @@ function formatSize(bytes) {
|
|
|
13
14
|
}
|
|
14
15
|
return `${value.toFixed(1)} ${units[unitIndex]}`;
|
|
15
16
|
}
|
|
17
|
+
export async function listSkillVersions(owner, skill) {
|
|
18
|
+
const { versions } = await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/versions`);
|
|
19
|
+
return versions ?? [];
|
|
20
|
+
}
|
|
16
21
|
export async function versions(args) {
|
|
17
22
|
const jsonOutput = args.includes("--json");
|
|
18
23
|
const spec = args.find((a) => !a.startsWith("--"));
|
|
19
24
|
if (!spec)
|
|
20
|
-
throw new
|
|
25
|
+
throw new UsageError(USAGE);
|
|
21
26
|
const { owner, skill } = parseOwnerSkill(spec, USAGE);
|
|
22
|
-
const
|
|
27
|
+
const list = await listSkillVersions(owner, skill);
|
|
23
28
|
if (jsonOutput) {
|
|
24
29
|
console.log(JSON.stringify(list));
|
|
25
30
|
return;
|
package/dist/commands/view.js
CHANGED
|
@@ -2,7 +2,8 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { apiJson } from "../http.js";
|
|
3
3
|
import { getApiUrl } from "../config.js";
|
|
4
4
|
import { parseOwnerSkill } from "../spec.js";
|
|
5
|
-
|
|
5
|
+
import { UsageError } from "../usage-error.js";
|
|
6
|
+
const USAGE = "Usage: ahood skill view <owner>/<skill> [--json] [--web]";
|
|
6
7
|
// Spawned with argv as an array (not a shell string), so a crafted owner/skill
|
|
7
8
|
// can never inject shell syntax -- and both segments are already restricted
|
|
8
9
|
// to a safe charset by parseOwnerSkill before this ever runs.
|
|
@@ -22,26 +23,29 @@ function openBrowser(url) {
|
|
|
22
23
|
console.log(`Open this URL in your browser: ${url}`);
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
export async function viewSkill(owner, skill) {
|
|
27
|
+
return apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}`);
|
|
28
|
+
}
|
|
25
29
|
export async function view(args) {
|
|
26
30
|
const jsonOutput = args.includes("--json");
|
|
27
31
|
const web = args.includes("--web");
|
|
28
32
|
const spec = args.find((a) => !a.startsWith("--"));
|
|
29
33
|
if (!spec)
|
|
30
|
-
throw new
|
|
34
|
+
throw new UsageError(USAGE);
|
|
31
35
|
const { owner, skill } = parseOwnerSkill(spec, USAGE);
|
|
32
36
|
const url = `${getApiUrl()}/${owner}/${skill}`;
|
|
33
37
|
if (web) {
|
|
34
38
|
openBrowser(url);
|
|
35
39
|
return;
|
|
36
40
|
}
|
|
37
|
-
const detail = await
|
|
41
|
+
const detail = await viewSkill(owner, skill);
|
|
38
42
|
if (jsonOutput) {
|
|
39
43
|
console.log(JSON.stringify(detail));
|
|
40
44
|
return;
|
|
41
45
|
}
|
|
42
46
|
const field = (label, value) => console.log(` ${label.padEnd(13)}${value}`);
|
|
43
47
|
console.log(`${detail.owner}/${detail.slug}`);
|
|
44
|
-
// Mirrors the wording/tone of `ahood add`'s yanked warning (src/commands/add.ts)
|
|
48
|
+
// Mirrors the wording/tone of `ahood skill add`'s yanked warning (src/commands/add.ts)
|
|
45
49
|
// and the website's skill detail page banner -- this is the one CLI surface
|
|
46
50
|
// whose entire job is "let me check this out before deciding to install it",
|
|
47
51
|
// so it must not be the one place that stays silent about a yanked version.
|