@dealdeploy/skl 1.4.0 → 1.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/add-tui.ts CHANGED
@@ -202,7 +202,9 @@ export function createAddTui(renderer: CliRenderer, deps: AddTuiDeps) {
202
202
  renderer.keyInput.on("keypress", (key: KeyEvent) => {
203
203
  const prevCursor = cursor;
204
204
 
205
- switch (key.name) {
205
+ const navKey = (key.ctrl && { h: "left", j: "down", k: "up", l: "right" }[key.name]) || key.name;
206
+
207
+ switch (navKey) {
206
208
  case "j":
207
209
  case "down":
208
210
  if (cursor < skills.length - 1) cursor++;
package/index.ts CHANGED
@@ -4,7 +4,7 @@ import { createCliRenderer } from "@opentui/core";
4
4
  import { existsSync, rmSync, mkdirSync, readdirSync, statSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { homedir } from "os";
7
- import { getCatalogSkills, removeFromLock, catalogDir, buildAddArgs, readLock, detectProjectAgents } from "./lib.ts";
7
+ import { getCatalogSkills, removeFromLock, removeFromProjectLock, catalogDir, buildAddArgs, readLock, detectProjectAgents } from "./lib.ts";
8
8
  import { createTui, type ColId } from "./tui.ts";
9
9
  import { checkForUpdate } from "./update-check.ts";
10
10
 
@@ -122,7 +122,10 @@ const tui = createTui(renderer, {
122
122
  new Response(proc.stdout).text(),
123
123
  new Response(proc.stderr).text(),
124
124
  ]);
125
- if (code === 0) return true;
125
+ if (code === 0) {
126
+ if (!enable && !isGlobal) removeFromProjectLock(name, process.cwd());
127
+ return true;
128
+ }
126
129
  if (debug) {
127
130
  return (stderr || stdout).trim() || `exit code ${code}`;
128
131
  }
@@ -148,6 +151,7 @@ const tui = createTui(renderer, {
148
151
  for (const p of procs) await p.exited;
149
152
  rmSync(join(CATALOG, name), { recursive: true, force: true });
150
153
  removeFromLock(name);
154
+ removeFromProjectLock(name, process.cwd());
151
155
  },
152
156
 
153
157
  async onEdit(name: string) {
package/lib.test.ts CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  findSkillEntries,
17
17
  parseRepoArg,
18
18
  buildAddArgs,
19
- readSkillFrontmatterName,
20
19
  detectProjectAgents,
21
20
  parseSkillsListOutput,
22
21
  planUpdates,
@@ -289,7 +288,7 @@ describe("parseRepoArg", () => {
289
288
  // ── buildAddArgs ────────────────────────────────────────────────────
290
289
 
291
290
  describe("buildAddArgs", () => {
292
- test("uses lock entry sourceUrl for remote skill with project agents", () => {
291
+ test("uses owner/repo/skillPath shorthand for remote skill with project agents", () => {
293
292
  const entry: LockEntry = {
294
293
  source: "owner/repo",
295
294
  sourceUrl: "https://github.com/owner/repo",
@@ -300,7 +299,7 @@ describe("buildAddArgs", () => {
300
299
  addToLock("myskill", entry);
301
300
 
302
301
  const args = buildAddArgs("/catalog", "myskill", false, ["claude-code", "roo"]);
303
- expect(args).toEqual(["add", "https://github.com/owner/repo", "--skill", "myskill", "-y", "--agent", "claude-code", "roo"]);
302
+ expect(args).toEqual(["add", "owner/repo/skills/myskill", "-y", "--agent", "claude-code", "roo"]);
304
303
  });
305
304
 
306
305
  test("global install does not pass --agent", () => {
@@ -316,6 +315,7 @@ describe("buildAddArgs", () => {
316
315
  const args = buildAddArgs("/catalog", "myskill", true, ["claude-code"]);
317
316
  expect(args).toContain("-g");
318
317
  expect(args).not.toContain("--agent");
318
+ expect(args).toContain("owner/repo/skills/myskill");
319
319
  });
320
320
 
321
321
  test("uses catalog path for hand-authored skill with project agents", () => {
@@ -328,14 +328,7 @@ describe("buildAddArgs", () => {
328
328
  expect(args).toEqual(["add", "/my/catalog/custom-skill", "-y", "-g"]);
329
329
  });
330
330
 
331
- test("uses SKILL.md frontmatter name for --skill flag when it differs from dir name", () => {
332
- const catalog = catalogDir();
333
- mkdirSync(join(catalog, "code-review"), { recursive: true });
334
- writeFileSync(
335
- join(catalog, "code-review", "SKILL.md"),
336
- "---\nname: code-review:code-review\ndescription: Review PRs\n---\n# Code Review"
337
- );
338
-
331
+ test("uses subpath for nested skill paths", () => {
339
332
  const entry: LockEntry = {
340
333
  source: "owner/repo",
341
334
  sourceUrl: "https://github.com/owner/repo",
@@ -345,48 +338,9 @@ describe("buildAddArgs", () => {
345
338
  };
346
339
  addToLock("code-review", entry);
347
340
 
348
- const args = buildAddArgs(catalog, "code-review", true);
349
- expect(args).toContain("--skill");
350
- expect(args).toContain("code-review:code-review");
351
- expect(args).not.toContain("--agent");
352
- expect(args).toContain("-g");
353
- });
354
- });
355
-
356
- // ── readSkillFrontmatterName ────────────────────────────────────────
357
-
358
- describe("readSkillFrontmatterName", () => {
359
- test("reads name from valid frontmatter", () => {
360
- const catalog = catalogDir();
361
- mkdirSync(join(catalog, "my-skill"), { recursive: true });
362
- writeFileSync(
363
- join(catalog, "my-skill", "SKILL.md"),
364
- "---\nname: my-skill:variant\ndescription: test\n---\n# Content"
365
- );
366
- expect(readSkillFrontmatterName(catalog, "my-skill")).toBe("my-skill:variant");
367
- });
368
-
369
- test("returns null when no frontmatter", () => {
370
- const catalog = catalogDir();
371
- mkdirSync(join(catalog, "no-fm"), { recursive: true });
372
- writeFileSync(join(catalog, "no-fm", "SKILL.md"), "# Just markdown");
373
- expect(readSkillFrontmatterName(catalog, "no-fm")).toBeNull();
374
- });
375
-
376
- test("returns null when SKILL.md missing", () => {
377
- const catalog = catalogDir();
378
- mkdirSync(join(catalog, "empty"), { recursive: true });
379
- expect(readSkillFrontmatterName(catalog, "empty")).toBeNull();
380
- });
381
-
382
- test("returns null when name field missing from frontmatter", () => {
383
- const catalog = catalogDir();
384
- mkdirSync(join(catalog, "no-name"), { recursive: true });
385
- writeFileSync(
386
- join(catalog, "no-name", "SKILL.md"),
387
- "---\ndescription: has no name\n---\n# Content"
388
- );
389
- expect(readSkillFrontmatterName(catalog, "no-name")).toBeNull();
341
+ const args = buildAddArgs("/catalog", "code-review", true);
342
+ expect(args).toEqual(["add", "owner/repo/skills/code-review", "-y", "-g"]);
343
+ expect(args).not.toContain("--skill");
390
344
  });
391
345
  });
392
346
 
package/lib.ts CHANGED
@@ -52,6 +52,22 @@ export function removeFromLock(name: string): void {
52
52
  writeLock(lock);
53
53
  }
54
54
 
55
+ export function removeFromProjectLock(name: string, cwd: string): void {
56
+ const lockFile = join(cwd, "skills-lock.json");
57
+ try {
58
+ const data = JSON.parse(readFileSync(lockFile, "utf-8"));
59
+ if (data?.skills?.[name]) {
60
+ delete data.skills[name];
61
+ const sorted: Record<string, unknown> = {};
62
+ for (const key of Object.keys(data.skills).sort()) {
63
+ sorted[key] = data.skills[key];
64
+ }
65
+ data.skills = sorted;
66
+ writeFileSync(lockFile, JSON.stringify(data, null, 2) + "\n");
67
+ }
68
+ } catch {}
69
+ }
70
+
55
71
  export function getLockEntry(name: string): LockEntry | null {
56
72
  const lock = readLock();
57
73
  return lock.skills[name] ?? null;
@@ -160,19 +176,6 @@ export function detectProjectAgents(cwd: string): string[] {
160
176
  return agents;
161
177
  }
162
178
 
163
- /** Read the frontmatter `name` from a skill's SKILL.md in the catalog. */
164
- export function readSkillFrontmatterName(catalogPath: string, dirName: string): string | null {
165
- try {
166
- const content = readFileSync(join(catalogPath, dirName, "SKILL.md"), "utf-8");
167
- const match = content.match(/^---\s*\n([\s\S]*?)\n---/);
168
- if (!match) return null;
169
- const nameMatch = match[1]!.match(/^name:\s*(.+)$/m);
170
- return nameMatch ? nameMatch[1]!.trim() : null;
171
- } catch {
172
- return null;
173
- }
174
- }
175
-
176
179
  /** Build npx skills add args for a skill. */
177
180
  export function buildAddArgs(
178
181
  catalogPath: string,
@@ -185,8 +188,9 @@ export function buildAddArgs(
185
188
  ? ["--agent", ...projectAgents]
186
189
  : [];
187
190
  if (lockEntry) {
188
- const skillName = readSkillFrontmatterName(catalogPath, name) ?? name;
189
- const args = ["add", lockEntry.sourceUrl, "--skill", skillName, "-y", ...agentArgs];
191
+ // Use owner/repo/skillPath shorthand so the CLI narrows via subpath
192
+ const source = `${lockEntry.source}/${lockEntry.skillPath}`;
193
+ const args = ["add", source, "-y", ...agentArgs];
190
194
  if (isGlobal) args.push("-g");
191
195
  return args;
192
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dealdeploy/skl",
3
- "version": "1.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "TUI skill manager for Claude Code agents",
5
5
  "module": "index.ts",
6
6
  "bin": {
package/tui.ts CHANGED
@@ -227,7 +227,7 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
227
227
  width: "100%",
228
228
  });
229
229
 
230
- type HeaderRef = { spacer: BoxRenderable | null; row: BoxRenderable; text: TextRenderable };
230
+ type HeaderRef = { spacer: BoxRenderable; row: BoxRenderable; text: TextRenderable };
231
231
  const headerRefs = new Map<number, HeaderRef>();
232
232
 
233
233
  type RowRefs = {
@@ -239,28 +239,17 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
239
239
  const rows: RowRefs[] = new Array(allSkills.length);
240
240
 
241
241
  scrollBox.add(searchRow);
242
- scrollBox.add(
243
- new BoxRenderable(renderer, {
244
- id: "spacer-search",
245
- height: 1,
246
- width: "100%",
247
- backgroundColor: C.rowBg,
248
- })
249
- );
250
242
 
251
243
  for (let di = 0; di < displayItems.length; di++) {
252
244
  const item = displayItems[di]!;
253
245
  if (item.type === "header") {
254
- let spacer: BoxRenderable | null = null;
255
- if (di > 0) {
256
- spacer = new BoxRenderable(renderer, {
257
- id: `spacer-${di}`,
258
- height: 1,
259
- width: "100%",
260
- backgroundColor: C.rowBg,
261
- });
262
- scrollBox.add(spacer);
263
- }
246
+ const spacer = new BoxRenderable(renderer, {
247
+ id: `spacer-${di}`,
248
+ height: 1,
249
+ width: "100%",
250
+ backgroundColor: C.rowBg,
251
+ });
252
+ scrollBox.add(spacer);
264
253
  const hRow = new BoxRenderable(renderer, {
265
254
  id: `header-${di}`,
266
255
  flexDirection: "row",
@@ -376,7 +365,7 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
376
365
  const visible = visibleRepos.has(item.repo);
377
366
  const ref = headerRefs.get(di)!;
378
367
  ref.row.visible = visible;
379
- if (ref.spacer) ref.spacer.visible = visible;
368
+ ref.spacer.visible = visible;
380
369
  } else {
381
370
  const visible = matchingSkills.has(item.skillIndex);
382
371
  rows[item.skillIndex]!.row.visible = visible;
@@ -447,7 +436,7 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
447
436
  statusLine.fg = color;
448
437
  if (statusTimeout) clearTimeout(statusTimeout);
449
438
  statusTimeout = setTimeout(() => {
450
- statusLine.content = "";
439
+ try { statusLine.content = ""; } catch {}
451
440
  }, 3000);
452
441
  }
453
442
 
@@ -492,7 +481,7 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
492
481
  const ref = headerRefs.get(di)!;
493
482
  if (ref.row.visible) {
494
483
  visibleBefore++;
495
- if (ref.spacer?.visible) visibleBefore++;
484
+ if (ref.spacer.visible) visibleBefore++;
496
485
  }
497
486
  } else {
498
487
  if (rows[item.skillIndex]!.row.visible) visibleBefore++;
@@ -622,7 +611,7 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
622
611
  }
623
612
  return;
624
613
  }
625
- if (key.name === "down" || key.name === "return") {
614
+ if (key.name === "down" || key.name === "return" || (key.ctrl && key.name === "j")) {
626
615
  focusArea = "grid";
627
616
  refreshAll();
628
617
  ensureVisible();
@@ -642,7 +631,15 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
642
631
  // ── Grid-focused input ──
643
632
 
644
633
  if (key.name === "backspace") return;
645
- if (key.ctrl) return;
634
+ const ctrlNav: Record<string, string> = { h: "left", j: "down", k: "up", l: "right" };
635
+ let navKey = key.name;
636
+ if (key.ctrl) {
637
+ if (ctrlNav[key.name]) {
638
+ navKey = ctrlNav[key.name]!;
639
+ } else {
640
+ return;
641
+ }
642
+ }
646
643
 
647
644
  if (key.sequence === "q") {
648
645
  deps.onQuit();
@@ -672,7 +669,7 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
672
669
  }
673
670
 
674
671
  // ── Navigation & actions ──
675
- switch (key.name) {
672
+ switch (navKey) {
676
673
  case "down":
677
674
  if (cursor < filteredDisplayIndices.length - 1) cursor++;
678
675
  break;