@dealdeploy/skl 1.2.0 → 1.4.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/index.ts +29 -4
- package/lib.test.ts +62 -0
- package/lib.ts +15 -1
- package/package.json +1 -1
- package/tui.ts +15 -6
- package/update-check.ts +59 -0
package/index.ts
CHANGED
|
@@ -6,14 +6,21 @@ import { join } from "path";
|
|
|
6
6
|
import { homedir } from "os";
|
|
7
7
|
import { getCatalogSkills, removeFromLock, catalogDir, buildAddArgs, readLock, detectProjectAgents } from "./lib.ts";
|
|
8
8
|
import { createTui, type ColId } from "./tui.ts";
|
|
9
|
+
import { checkForUpdate } from "./update-check.ts";
|
|
9
10
|
|
|
10
11
|
// @ts-ignore - bun supports JSON imports
|
|
11
12
|
const { version: VERSION } = await import("./package.json");
|
|
12
13
|
|
|
14
|
+
// Fire update check in background (non-blocking)
|
|
15
|
+
const updateMsg = checkForUpdate(VERSION);
|
|
16
|
+
|
|
13
17
|
// ── Flags ────────────────────────────────────────────────────────────
|
|
18
|
+
const debug = process.argv.includes("--debug");
|
|
14
19
|
const arg = process.argv[2];
|
|
15
20
|
if (arg === "-v" || arg === "--version") {
|
|
16
21
|
console.log(`skl ${VERSION}`);
|
|
22
|
+
const msg = await updateMsg;
|
|
23
|
+
if (msg) console.log(msg);
|
|
17
24
|
process.exit(0);
|
|
18
25
|
}
|
|
19
26
|
if (arg === "-h" || arg === "--help" || arg === "help") {
|
|
@@ -30,21 +37,30 @@ Usage:
|
|
|
30
37
|
|
|
31
38
|
Options:
|
|
32
39
|
-h, --help Show this help message
|
|
33
|
-
-v, --version Show version
|
|
40
|
+
-v, --version Show version
|
|
41
|
+
--debug Show full error output on failures`);
|
|
34
42
|
process.exit(0);
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
// ── Subcommand routing ───────────────────────────────────────────────
|
|
46
|
+
async function printUpdateMsg() {
|
|
47
|
+
const msg = await updateMsg;
|
|
48
|
+
if (msg) console.log(`\n${msg}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
38
51
|
if (arg === "add") {
|
|
39
52
|
await import("./add.ts");
|
|
53
|
+
await printUpdateMsg();
|
|
40
54
|
process.exit(0);
|
|
41
55
|
}
|
|
42
56
|
if (arg === "update") {
|
|
43
57
|
await import("./update.ts");
|
|
58
|
+
await printUpdateMsg();
|
|
44
59
|
process.exit(0);
|
|
45
60
|
}
|
|
46
61
|
if (arg === "migrate") {
|
|
47
62
|
await import("./migrate.ts");
|
|
63
|
+
await printUpdateMsg();
|
|
48
64
|
process.exit(0);
|
|
49
65
|
}
|
|
50
66
|
|
|
@@ -101,8 +117,16 @@ const tui = createTui(renderer, {
|
|
|
101
117
|
stdout: "pipe",
|
|
102
118
|
stderr: "pipe",
|
|
103
119
|
});
|
|
104
|
-
const code = await
|
|
105
|
-
|
|
120
|
+
const [code, stdout, stderr] = await Promise.all([
|
|
121
|
+
proc.exited,
|
|
122
|
+
new Response(proc.stdout).text(),
|
|
123
|
+
new Response(proc.stderr).text(),
|
|
124
|
+
]);
|
|
125
|
+
if (code === 0) return true;
|
|
126
|
+
if (debug) {
|
|
127
|
+
return (stderr || stdout).trim() || `exit code ${code}`;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
106
130
|
},
|
|
107
131
|
|
|
108
132
|
async onDelete(name: string) {
|
|
@@ -141,8 +165,9 @@ const tui = createTui(renderer, {
|
|
|
141
165
|
process.exit(0);
|
|
142
166
|
},
|
|
143
167
|
|
|
144
|
-
onQuit() {
|
|
168
|
+
async onQuit() {
|
|
145
169
|
renderer.destroy();
|
|
170
|
+
await printUpdateMsg();
|
|
146
171
|
process.exit(0);
|
|
147
172
|
},
|
|
148
173
|
});
|
package/lib.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
findSkillEntries,
|
|
17
17
|
parseRepoArg,
|
|
18
18
|
buildAddArgs,
|
|
19
|
+
readSkillFrontmatterName,
|
|
19
20
|
detectProjectAgents,
|
|
20
21
|
parseSkillsListOutput,
|
|
21
22
|
planUpdates,
|
|
@@ -326,6 +327,67 @@ describe("buildAddArgs", () => {
|
|
|
326
327
|
const args = buildAddArgs("/my/catalog", "custom-skill", true);
|
|
327
328
|
expect(args).toEqual(["add", "/my/catalog/custom-skill", "-y", "-g"]);
|
|
328
329
|
});
|
|
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
|
+
|
|
339
|
+
const entry: LockEntry = {
|
|
340
|
+
source: "owner/repo",
|
|
341
|
+
sourceUrl: "https://github.com/owner/repo",
|
|
342
|
+
skillPath: "skills/code-review",
|
|
343
|
+
treeSHA: "abc",
|
|
344
|
+
addedAt: "2026-01-01",
|
|
345
|
+
};
|
|
346
|
+
addToLock("code-review", entry);
|
|
347
|
+
|
|
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();
|
|
390
|
+
});
|
|
329
391
|
});
|
|
330
392
|
|
|
331
393
|
// ── detectProjectAgents ─────────────────────────────────────────────
|
package/lib.ts
CHANGED
|
@@ -160,6 +160,19 @@ export function detectProjectAgents(cwd: string): string[] {
|
|
|
160
160
|
return agents;
|
|
161
161
|
}
|
|
162
162
|
|
|
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
|
+
|
|
163
176
|
/** Build npx skills add args for a skill. */
|
|
164
177
|
export function buildAddArgs(
|
|
165
178
|
catalogPath: string,
|
|
@@ -172,7 +185,8 @@ export function buildAddArgs(
|
|
|
172
185
|
? ["--agent", ...projectAgents]
|
|
173
186
|
: [];
|
|
174
187
|
if (lockEntry) {
|
|
175
|
-
const
|
|
188
|
+
const skillName = readSkillFrontmatterName(catalogPath, name) ?? name;
|
|
189
|
+
const args = ["add", lockEntry.sourceUrl, "--skill", skillName, "-y", ...agentArgs];
|
|
176
190
|
if (isGlobal) args.push("-g");
|
|
177
191
|
return args;
|
|
178
192
|
}
|
package/package.json
CHANGED
package/tui.ts
CHANGED
|
@@ -18,8 +18,8 @@ export type TuiDeps = {
|
|
|
18
18
|
globalInstalled: Set<string>;
|
|
19
19
|
localInstalled: Set<string>;
|
|
20
20
|
catalogPath: string;
|
|
21
|
-
/** Called when a skill toggle is requested. Return true on success. */
|
|
22
|
-
onToggle: (col: ColId, name: string, enable: boolean) => Promise<boolean>;
|
|
21
|
+
/** Called when a skill toggle is requested. Return true on success, false or error string on failure. */
|
|
22
|
+
onToggle: (col: ColId, name: string, enable: boolean) => Promise<boolean | string>;
|
|
23
23
|
/** Called when a skill delete is requested. */
|
|
24
24
|
onDelete: (name: string) => Promise<void>;
|
|
25
25
|
/** Called when edit is requested. Return false to stay in TUI. */
|
|
@@ -239,6 +239,14 @@ 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
|
+
);
|
|
242
250
|
|
|
243
251
|
for (let di = 0; di < displayItems.length; di++) {
|
|
244
252
|
const item = displayItems[di]!;
|
|
@@ -490,7 +498,7 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
|
|
|
490
498
|
if (rows[item.skillIndex]!.row.visible) visibleBefore++;
|
|
491
499
|
}
|
|
492
500
|
}
|
|
493
|
-
scrollBox.scrollTo(Math.max(0, visibleBefore +
|
|
501
|
+
scrollBox.scrollTo(Math.max(0, visibleBefore + 2 - 2));
|
|
494
502
|
}
|
|
495
503
|
}
|
|
496
504
|
|
|
@@ -505,8 +513,8 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
|
|
|
505
513
|
refreshAll();
|
|
506
514
|
|
|
507
515
|
try {
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
516
|
+
const result = await deps.onToggle(col, name, !enabled);
|
|
517
|
+
if (result === true) {
|
|
510
518
|
const set = col === "global" ? globalInstalled : localInstalled;
|
|
511
519
|
if (enabled) {
|
|
512
520
|
set.delete(name);
|
|
@@ -516,7 +524,8 @@ export function createTui(renderer: CliRenderer, deps: TuiDeps) {
|
|
|
516
524
|
setStatus(`Added ${name}`, C.statusOk);
|
|
517
525
|
}
|
|
518
526
|
} else {
|
|
519
|
-
|
|
527
|
+
const detail = typeof result === "string" ? `: ${result}` : "";
|
|
528
|
+
setStatus(`Failed to ${enabled ? "remove" : "add"} ${name}${detail}`, C.statusErr);
|
|
520
529
|
}
|
|
521
530
|
} finally {
|
|
522
531
|
pendingToggles.delete(key);
|
package/update-check.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
|
|
5
|
+
const CACHE_PATH = join(homedir(), ".skl", "update-check.json");
|
|
6
|
+
const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 1 day
|
|
7
|
+
const PKG_NAME = "@dealdeploy/skl";
|
|
8
|
+
|
|
9
|
+
type Cache = { latest: string; checkedAt: number };
|
|
10
|
+
|
|
11
|
+
function readCache(): Cache | null {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(CACHE_PATH, "utf-8"));
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeCache(cache: Cache): void {
|
|
20
|
+
mkdirSync(join(homedir(), ".skl"), { recursive: true });
|
|
21
|
+
writeFileSync(CACHE_PATH, JSON.stringify(cache));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Check npm for the latest version. Non-blocking — fire and forget.
|
|
26
|
+
* Returns a message string if an update is available, or null.
|
|
27
|
+
*/
|
|
28
|
+
export async function checkForUpdate(currentVersion: string): Promise<string | null> {
|
|
29
|
+
const cache = readCache();
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
|
|
32
|
+
// Use cache if fresh
|
|
33
|
+
if (cache && now - cache.checkedAt < CHECK_INTERVAL_MS) {
|
|
34
|
+
return compareVersions(currentVersion, cache.latest);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Fetch from registry
|
|
38
|
+
try {
|
|
39
|
+
const resp = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, {
|
|
40
|
+
signal: AbortSignal.timeout(3000),
|
|
41
|
+
});
|
|
42
|
+
if (!resp.ok) return null;
|
|
43
|
+
const data = (await resp.json()) as { version: string };
|
|
44
|
+
writeCache({ latest: data.version, checkedAt: now });
|
|
45
|
+
return compareVersions(currentVersion, data.version);
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function compareVersions(current: string, latest: string): string | null {
|
|
52
|
+
if (current === latest) return null;
|
|
53
|
+
const [cMaj, cMin, cPat] = current.split(".").map(Number);
|
|
54
|
+
const [lMaj, lMin, lPat] = latest.split(".").map(Number);
|
|
55
|
+
if (lMaj > cMaj || lMin > cMin || lPat > cPat) {
|
|
56
|
+
return `Update available: ${current} → ${latest} (run: bun install -g ${PKG_NAME})`;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|