@ahood/cli 0.3.1 → 0.3.3

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.
@@ -1,7 +1,7 @@
1
1
  import { createInterface } from "node:readline";
2
2
  import { apiJson } from "../http.js";
3
- import { parseOwnerSkill } from "../spec.js";
4
- const USAGE = "Usage: ahood unpublish <owner>/<skill> [--yes]";
3
+ import { parseOwnerSkill, parseOwnerSkillVersion } from "../spec.js";
4
+ const USAGE = "Usage: ahood unpublish <owner>/<skill>[@version] [--yes]";
5
5
  // On closed/non-interactive stdin, readline/promises' rl.question() promise
6
6
  // never settles -- but since nothing else keeps the event loop alive at that
7
7
  // point, the process used to exit 0 anyway once the loop drained, having
@@ -27,11 +27,45 @@ function confirm(promptText) {
27
27
  });
28
28
  });
29
29
  }
30
+ // A bare "owner/skill" has no "@" at all, so lastIndexOf("@") is -1 and this
31
+ // is a whole-skill unpublish (preserves the exact pre-existing behavior).
32
+ // "owner/skill@1.2.3" has an "@" at index > 0 and is a single-version yank
33
+ // instead -- parseOwnerSkillVersion is reused rather than re-parsing "@" by
34
+ // hand, so the same version-format validation (spec.ts's SEMVER_RE / the
35
+ // "latest" literal) applies here as it already does for `add`/`view`.
36
+ function isVersioned(spec) {
37
+ return spec.lastIndexOf("@") > 0;
38
+ }
30
39
  export async function unpublish(args) {
31
40
  const yes = args.includes("--yes");
32
41
  const spec = args.find((a) => !a.startsWith("--"));
33
42
  if (!spec)
34
43
  throw new Error(USAGE);
44
+ if (isVersioned(spec)) {
45
+ const { owner, skill, version } = parseOwnerSkillVersion(spec, USAGE);
46
+ const key = `${owner}/${skill}@${version}`;
47
+ // Yanking is less destructive than a whole-skill delete (existing
48
+ // lockfile-pinned installs keep resolving/verifying against it -- see
49
+ // add.ts's yanked_at warning), so the prompt wording says so instead of
50
+ // reusing the whole-skill "permanently delete ... for everyone" text,
51
+ // which would overstate what actually happens here.
52
+ const confirmed = yes
53
+ ? true
54
+ : await confirm(`Yank ${key}? Existing installs keep working; new installs will be warned. Type "yes" to confirm: `);
55
+ if (!confirmed) {
56
+ console.log("Aborted.");
57
+ return;
58
+ }
59
+ // DELETE on the version resource itself (no separate /yank sub-route):
60
+ // the server treats this the same way whole-skill DELETE already does
61
+ // (a soft delete that sets a timestamp column, never removes the row),
62
+ // just scoped one level down to skill_versions.yanked_at/yanked_reason
63
+ // instead of skills.deleted_at. Mirroring that existing verb/path
64
+ // convention rather than inventing a new one.
65
+ await apiJson(`/api/v1/skills/${encodeURIComponent(owner)}/${encodeURIComponent(skill)}/versions/${encodeURIComponent(version)}`, { method: "DELETE" });
66
+ console.log(`Yanked ${key}. Existing lockfile pins still resolve; new installs will be warned off it.`);
67
+ return;
68
+ }
35
69
  const { owner, skill } = parseOwnerSkill(spec, USAGE);
36
70
  const key = `${owner}/${skill}`;
37
71
  // --yes bypasses the prompt for scripts/CI; otherwise a closed stdin (no
@@ -36,5 +36,11 @@ export async function versions(args) {
36
36
  field("published:", v.created_at);
37
37
  field("size:", formatSize(v.package_size_bytes));
38
38
  field("changelog:", v.changelog_md ? v.changelog_md : "-");
39
+ // Marked right after the version's other details, only when the API
40
+ // actually flags it (issue #58) -- non-yanked versions print exactly as
41
+ // before, no "status:" line at all.
42
+ if (v.yanked_at) {
43
+ field("status:", v.yanked_reason ? `YANKED -- ${v.yanked_reason}` : "YANKED");
44
+ }
39
45
  }
40
46
  }
package/dist/help.js CHANGED
@@ -79,9 +79,12 @@ export const COMMANDS_HELP = [
79
79
  examples: ['ahood edit alice/pdf-tools --tagline "Merge and split PDFs"'],
80
80
  },
81
81
  {
82
- usage: "ahood unpublish <owner>/<skill> [--yes]",
83
- summary: "Delete a skill from the registry for every consumer, not just your local install (prompts for confirmation unless --yes is passed).",
84
- desc: "Delete a skill from the registry for every consumer (not just your local install). Prompts for a typed \"yes\" unless --yes is passed.",
82
+ usage: "ahood unpublish <owner>/<skill>[@version] [--yes]",
83
+ summary: "Delete a skill from the registry for every consumer, or yank a single version, not just your local install (prompts for confirmation unless --yes is passed).",
84
+ desc: "Without @version: delete a skill from the registry for every consumer (not just your local install). " +
85
+ "With @version (e.g. alice/foo@1.2.3): yank that single version instead of deleting the whole skill -- " +
86
+ "it is marked yanked, not removed. Existing lockfile pins still resolve and verify against it, but new " +
87
+ "installs are warned off it. Prompts for a typed \"yes\" unless --yes is passed.",
85
88
  flags: ["--yes Skip the interactive confirmation, for scripts/CI."],
86
89
  },
87
90
  { usage: "ahood star <owner>/<skill>", summary: "Star a skill.", desc: "Star a skill." },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahood/cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "CLI for ahood, a registry for installing and publishing Claude Code skills.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/AlexKay28/ahood-cli",