@skillsmith/cli 0.4.3 → 0.5.1
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/.tsbuildinfo +1 -1
- package/dist/src/commands/audit.d.ts +19 -0
- package/dist/src/commands/audit.d.ts.map +1 -0
- package/dist/src/commands/audit.js +134 -0
- package/dist/src/commands/audit.js.map +1 -0
- package/dist/src/commands/audit.test.d.ts +8 -0
- package/dist/src/commands/audit.test.d.ts.map +1 -0
- package/dist/src/commands/audit.test.js +97 -0
- package/dist/src/commands/audit.test.js.map +1 -0
- package/dist/src/commands/author/init.d.ts.map +1 -1
- package/dist/src/commands/author/init.js +4 -9
- package/dist/src/commands/author/init.js.map +1 -1
- package/dist/src/commands/create.d.ts +42 -0
- package/dist/src/commands/create.d.ts.map +1 -0
- package/dist/src/commands/create.js +325 -0
- package/dist/src/commands/create.js.map +1 -0
- package/dist/src/commands/diff.d.ts +17 -0
- package/dist/src/commands/diff.d.ts.map +1 -0
- package/dist/src/commands/diff.js +191 -0
- package/dist/src/commands/diff.js.map +1 -0
- package/dist/src/commands/diff.test.d.ts +6 -0
- package/dist/src/commands/diff.test.d.ts.map +1 -0
- package/dist/src/commands/diff.test.js +275 -0
- package/dist/src/commands/diff.test.js.map +1 -0
- package/dist/src/commands/index.d.ts +5 -0
- package/dist/src/commands/index.d.ts.map +1 -1
- package/dist/src/commands/index.js +10 -1
- package/dist/src/commands/index.js.map +1 -1
- package/dist/src/commands/install-skill.d.ts.map +1 -1
- package/dist/src/commands/install-skill.js +10 -3
- package/dist/src/commands/install-skill.js.map +1 -1
- package/dist/src/commands/install.d.ts +13 -0
- package/dist/src/commands/install.d.ts.map +1 -0
- package/dist/src/commands/install.js +213 -0
- package/dist/src/commands/install.js.map +1 -0
- package/dist/src/commands/manage.d.ts.map +1 -1
- package/dist/src/commands/manage.js +71 -27
- package/dist/src/commands/manage.js.map +1 -1
- package/dist/src/commands/pin.d.ts +24 -0
- package/dist/src/commands/pin.d.ts.map +1 -0
- package/dist/src/commands/pin.js +123 -0
- package/dist/src/commands/pin.js.map +1 -0
- package/dist/src/commands/pin.test.d.ts +6 -0
- package/dist/src/commands/pin.test.d.ts.map +1 -0
- package/dist/src/commands/pin.test.js +206 -0
- package/dist/src/commands/pin.test.js.map +1 -0
- package/dist/src/commands/search.d.ts.map +1 -1
- package/dist/src/commands/search.js +43 -3
- package/dist/src/commands/search.js.map +1 -1
- package/dist/src/commands/sync.d.ts.map +1 -1
- package/dist/src/commands/sync.js +3 -2
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +16 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/templates/changelog.md.template.d.ts +8 -0
- package/dist/src/templates/changelog.md.template.d.ts.map +1 -0
- package/dist/src/templates/changelog.md.template.js +19 -0
- package/dist/src/templates/changelog.md.template.js.map +1 -0
- package/dist/src/templates/index.d.ts +1 -0
- package/dist/src/templates/index.d.ts.map +1 -1
- package/dist/src/templates/index.js +1 -0
- package/dist/src/templates/index.js.map +1 -1
- package/dist/src/templates/readme.md.template.d.ts +1 -1
- package/dist/src/templates/readme.md.template.d.ts.map +1 -1
- package/dist/src/templates/readme.md.template.js +1 -1
- package/dist/src/templates/skill.md.template.d.ts +1 -1
- package/dist/src/templates/skill.md.template.d.ts.map +1 -1
- package/dist/src/templates/skill.md.template.js +2 -4
- package/dist/src/templates/skill.md.template.js.map +1 -1
- package/dist/src/utils/license-validation.js +1 -1
- package/dist/src/utils/license-validation.js.map +1 -1
- package/dist/src/utils/manifest.d.ts +46 -0
- package/dist/src/utils/manifest.d.ts.map +1 -0
- package/dist/src/utils/manifest.js +55 -0
- package/dist/src/utils/manifest.js.map +1 -0
- package/dist/src/utils/skill-name.d.ts +19 -0
- package/dist/src/utils/skill-name.d.ts.map +1 -0
- package/dist/src/utils/skill-name.js +26 -0
- package/dist/src/utils/skill-name.js.map +1 -0
- package/dist/src/utils/skills-directory.d.ts +12 -3
- package/dist/src/utils/skills-directory.d.ts.map +1 -1
- package/dist/src/utils/skills-directory.js +63 -7
- package/dist/src/utils/skills-directory.js.map +1 -1
- package/dist/tests/create.test.d.ts +5 -0
- package/dist/tests/create.test.d.ts.map +1 -0
- package/dist/tests/create.test.js +449 -0
- package/dist/tests/create.test.js.map +1 -0
- package/dist/tests/install-skill.test.js +9 -4
- package/dist/tests/install-skill.test.js.map +1 -1
- package/dist/tests/license-validation.test.d.ts +8 -0
- package/dist/tests/license-validation.test.d.ts.map +1 -0
- package/dist/tests/license-validation.test.js +186 -0
- package/dist/tests/license-validation.test.js.map +1 -0
- package/dist/tests/manage.test.js +84 -0
- package/dist/tests/manage.test.js.map +1 -1
- package/dist/tests/recommend-helpers.test.d.ts +9 -0
- package/dist/tests/recommend-helpers.test.d.ts.map +1 -0
- package/dist/tests/recommend-helpers.test.js +308 -0
- package/dist/tests/recommend-helpers.test.js.map +1 -0
- package/dist/tests/recommend-scoring.test.d.ts +8 -0
- package/dist/tests/recommend-scoring.test.d.ts.map +1 -0
- package/dist/tests/recommend-scoring.test.js +184 -0
- package/dist/tests/recommend-scoring.test.js.map +1 -0
- package/dist/tests/tool-analyzer.test.d.ts +8 -0
- package/dist/tests/tool-analyzer.test.d.ts.map +1 -0
- package/dist/tests/tool-analyzer.test.js +149 -0
- package/dist/tests/tool-analyzer.test.js.map +1 -0
- package/dist/tests/unit/commands/install.test.d.ts +7 -0
- package/dist/tests/unit/commands/install.test.d.ts.map +1 -0
- package/dist/tests/unit/commands/install.test.js +407 -0
- package/dist/tests/unit/commands/install.test.js.map +1 -0
- package/dist/vitest.config.d.ts.map +1 -1
- package/dist/vitest.config.js +9 -3
- package/dist/vitest.config.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared skill name validation utilities.
|
|
3
|
+
*
|
|
4
|
+
* Used by both `skillsmith author init` and `skillsmith create` to enforce
|
|
5
|
+
* a consistent, registry-safe naming convention across all scaffolding paths.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Valid skill names: lowercase letters, digits, and hyphens only.
|
|
9
|
+
* Must start with a lowercase letter.
|
|
10
|
+
* Matches the Skillsmith registry slug format.
|
|
11
|
+
*/
|
|
12
|
+
export const VALID_SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
13
|
+
/**
|
|
14
|
+
* Validate a skill name against the canonical Skillsmith naming convention.
|
|
15
|
+
*
|
|
16
|
+
* @returns `true` if valid, or a string error message if invalid.
|
|
17
|
+
*/
|
|
18
|
+
export function validateSkillName(name) {
|
|
19
|
+
if (!name.trim())
|
|
20
|
+
return 'Skill name is required';
|
|
21
|
+
if (!VALID_SKILL_NAME_RE.test(name)) {
|
|
22
|
+
return 'Skill name must start with a lowercase letter and contain only lowercase letters, digits, and hyphens (e.g. my-skill)';
|
|
23
|
+
}
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=skill-name.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-name.js","sourceRoot":"","sources":["../../../src/utils/skill-name.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,mBAAmB,CAAA;AAEtD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,wBAAwB,CAAA;IACjD,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,OAAO,uHAAuH,CAAA;IAChI,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
|
|
@@ -12,14 +12,23 @@ export interface InstalledSkill {
|
|
|
12
12
|
hasUpdates: boolean;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Get skills from a specific directory
|
|
15
|
+
* Get skills from a specific directory.
|
|
16
|
+
*
|
|
17
|
+
* When dbPath is provided, opens the skill_versions table to determine
|
|
18
|
+
* whether a newer content hash has been recorded since the skill was installed.
|
|
19
|
+
* Falls back to hasUpdates: false when the database is unavailable.
|
|
20
|
+
*
|
|
21
|
+
* @param skillsDir Directory to scan for installed skills
|
|
22
|
+
* @param dbPath Optional path to the Skillsmith SQLite database
|
|
16
23
|
*/
|
|
17
|
-
export declare function getSkillsFromDirectory(skillsDir: string): Promise<InstalledSkill[]>;
|
|
24
|
+
export declare function getSkillsFromDirectory(skillsDir: string, dbPath?: string): Promise<InstalledSkill[]>;
|
|
18
25
|
/**
|
|
19
26
|
* Get list of installed skills from both global (~/.claude/skills) and
|
|
20
27
|
* local (.claude/skills) directories.
|
|
21
28
|
*
|
|
22
29
|
* SMI-1630: Local skills take precedence over global skills with the same name.
|
|
30
|
+
*
|
|
31
|
+
* @param dbPath Optional path to the Skillsmith SQLite database for update detection
|
|
23
32
|
*/
|
|
24
|
-
export declare function getInstalledSkills(): Promise<InstalledSkill[]>;
|
|
33
|
+
export declare function getInstalledSkills(dbPath?: string): Promise<InstalledSkill[]>;
|
|
25
34
|
//# sourceMappingURL=skills-directory.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills-directory.d.ts","sourceRoot":"","sources":["../../../src/utils/skills-directory.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
1
|
+
{"version":3,"file":"skills-directory.d.ts","sourceRoot":"","sources":["../../../src/utils/skills-directory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,OAAO,EAML,KAAK,SAAS,EACf,MAAM,kBAAkB,CAAA;AAGzB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,SAAS,EAAE,SAAS,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;CACpB;AAoBD;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAC1C,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,cAAc,EAAE,CAAC,CAiG3B;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAsBnF"}
|
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
* from the global and local ~/.claude/skills directories.
|
|
4
4
|
*/
|
|
5
5
|
import { readdir, readFile, stat } from 'fs/promises';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
6
7
|
import { join } from 'path';
|
|
7
8
|
import { homedir } from 'os';
|
|
8
|
-
import { SkillParser } from '@skillsmith/core';
|
|
9
|
+
import { SkillParser, createDatabaseAsync, initializeSchema, SkillVersionRepository, } from '@skillsmith/core';
|
|
10
|
+
import { DEFAULT_DB_PATH } from '../config.js';
|
|
9
11
|
/**
|
|
10
12
|
* SMI-1630: Search both global and local skill directories
|
|
11
13
|
*
|
|
@@ -23,10 +25,32 @@ function getLocalSkillsDir() {
|
|
|
23
25
|
return join(process.cwd(), '.claude', 'skills');
|
|
24
26
|
}
|
|
25
27
|
/**
|
|
26
|
-
* Get skills from a specific directory
|
|
28
|
+
* Get skills from a specific directory.
|
|
29
|
+
*
|
|
30
|
+
* When dbPath is provided, opens the skill_versions table to determine
|
|
31
|
+
* whether a newer content hash has been recorded since the skill was installed.
|
|
32
|
+
* Falls back to hasUpdates: false when the database is unavailable.
|
|
33
|
+
*
|
|
34
|
+
* @param skillsDir Directory to scan for installed skills
|
|
35
|
+
* @param dbPath Optional path to the Skillsmith SQLite database
|
|
27
36
|
*/
|
|
28
|
-
export async function getSkillsFromDirectory(skillsDir) {
|
|
37
|
+
export async function getSkillsFromDirectory(skillsDir, dbPath) {
|
|
29
38
|
const skills = [];
|
|
39
|
+
// Open the version repository if a db path was provided
|
|
40
|
+
let versionRepo = null;
|
|
41
|
+
let dbConn = null;
|
|
42
|
+
if (dbPath) {
|
|
43
|
+
try {
|
|
44
|
+
dbConn = await createDatabaseAsync(dbPath);
|
|
45
|
+
initializeSchema(dbConn);
|
|
46
|
+
versionRepo = new SkillVersionRepository(dbConn);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// DB not available yet — fall back to hasUpdates: false
|
|
50
|
+
versionRepo = null;
|
|
51
|
+
dbConn = null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
30
54
|
try {
|
|
31
55
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
32
56
|
for (const entry of entries) {
|
|
@@ -38,13 +62,39 @@ export async function getSkillsFromDirectory(skillsDir) {
|
|
|
38
62
|
const content = await readFile(skillMdPath, 'utf-8');
|
|
39
63
|
const parser = new SkillParser();
|
|
40
64
|
const parsed = parser.parse(content);
|
|
65
|
+
// Determine hasUpdates by comparing the current SKILL.md hash to the
|
|
66
|
+
// most-recently recorded hash in skill_versions for this skill id.
|
|
67
|
+
let hasUpdates = false;
|
|
68
|
+
if (versionRepo && parsed) {
|
|
69
|
+
try {
|
|
70
|
+
const parsedAny = parsed;
|
|
71
|
+
const skillId = parsedAny['id'] ?? entry.name;
|
|
72
|
+
const latestVersion = await versionRepo.getLatestVersion(skillId);
|
|
73
|
+
if (latestVersion) {
|
|
74
|
+
const currentHash = createHash('sha256').update(content, 'utf8').digest('hex');
|
|
75
|
+
const storedHash = parsedAny['contentHash'] ??
|
|
76
|
+
parsedAny['originalContentHash'] ??
|
|
77
|
+
'';
|
|
78
|
+
// hasUpdates = latest recorded hash differs from what we have locally
|
|
79
|
+
hasUpdates = storedHash !== '' && latestVersion.content_hash !== storedHash;
|
|
80
|
+
// If we have no stored hash, compare against current content hash
|
|
81
|
+
if (!storedHash) {
|
|
82
|
+
hasUpdates = latestVersion.content_hash !== currentHash;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Version check failed — safe to ignore, fall back to false
|
|
88
|
+
hasUpdates = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
41
91
|
skills.push({
|
|
42
92
|
name: parsed?.name || entry.name,
|
|
43
93
|
path: skillPath,
|
|
44
94
|
version: parsed?.version || null,
|
|
45
95
|
trustTier: parsed ? parser.inferTrustTier(parsed) : 'unknown',
|
|
46
96
|
installDate: skillMdStat.mtime.toISOString().split('T')[0] || 'Unknown',
|
|
47
|
-
hasUpdates
|
|
97
|
+
hasUpdates,
|
|
48
98
|
});
|
|
49
99
|
}
|
|
50
100
|
catch (error) {
|
|
@@ -73,6 +123,9 @@ export async function getSkillsFromDirectory(skillsDir) {
|
|
|
73
123
|
throw error;
|
|
74
124
|
}
|
|
75
125
|
}
|
|
126
|
+
finally {
|
|
127
|
+
dbConn?.close();
|
|
128
|
+
}
|
|
76
129
|
return skills;
|
|
77
130
|
}
|
|
78
131
|
/**
|
|
@@ -80,12 +133,15 @@ export async function getSkillsFromDirectory(skillsDir) {
|
|
|
80
133
|
* local (.claude/skills) directories.
|
|
81
134
|
*
|
|
82
135
|
* SMI-1630: Local skills take precedence over global skills with the same name.
|
|
136
|
+
*
|
|
137
|
+
* @param dbPath Optional path to the Skillsmith SQLite database for update detection
|
|
83
138
|
*/
|
|
84
|
-
export async function getInstalledSkills() {
|
|
139
|
+
export async function getInstalledSkills(dbPath) {
|
|
140
|
+
const resolvedDbPath = dbPath ?? DEFAULT_DB_PATH;
|
|
85
141
|
// Get skills from both directories
|
|
86
142
|
const [globalSkills, localSkills] = await Promise.all([
|
|
87
|
-
getSkillsFromDirectory(GLOBAL_SKILLS_DIR),
|
|
88
|
-
getSkillsFromDirectory(getLocalSkillsDir()),
|
|
143
|
+
getSkillsFromDirectory(GLOBAL_SKILLS_DIR, resolvedDbPath),
|
|
144
|
+
getSkillsFromDirectory(getLocalSkillsDir(), resolvedDbPath),
|
|
89
145
|
]);
|
|
90
146
|
// Create a map for deduplication, local skills take precedence
|
|
91
147
|
const skillMap = new Map();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skills-directory.js","sourceRoot":"","sources":["../../../src/utils/skills-directory.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,aAAa,CAAA;AACrD,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAA;AAC5B,OAAO,
|
|
1
|
+
{"version":3,"file":"skills-directory.js","sourceRoot":"","sources":["../../../src/utils/skills-directory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create.test.d.ts","sourceRoot":"","sources":["../../tests/create.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-3083: skillsmith create command tests
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
// Mock file system
|
|
7
|
+
vi.mock('fs/promises', () => ({
|
|
8
|
+
mkdir: vi.fn(),
|
|
9
|
+
writeFile: vi.fn(),
|
|
10
|
+
stat: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
// Mock inquirer
|
|
13
|
+
vi.mock('@inquirer/prompts', () => ({
|
|
14
|
+
input: vi.fn(),
|
|
15
|
+
confirm: vi.fn(),
|
|
16
|
+
select: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
// Mock ora
|
|
19
|
+
vi.mock('ora', () => ({
|
|
20
|
+
default: vi.fn(() => ({
|
|
21
|
+
start: vi.fn().mockReturnThis(),
|
|
22
|
+
stop: vi.fn().mockReturnThis(),
|
|
23
|
+
succeed: vi.fn().mockReturnThis(),
|
|
24
|
+
fail: vi.fn().mockReturnThis(),
|
|
25
|
+
warn: vi.fn().mockReturnThis(),
|
|
26
|
+
text: '',
|
|
27
|
+
})),
|
|
28
|
+
}));
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// validateSkillName (re-exported from utils/skill-name via create.ts)
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
describe('SMI-3083: validateSkillName', () => {
|
|
33
|
+
it('accepts valid lowercase-hyphen names', async () => {
|
|
34
|
+
const { validateSkillName } = await import('../src/commands/create.js');
|
|
35
|
+
expect(validateSkillName('my-skill')).toBe(true);
|
|
36
|
+
expect(validateSkillName('skill')).toBe(true);
|
|
37
|
+
expect(validateSkillName('a1b2-c3')).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('rejects names with uppercase letters', async () => {
|
|
40
|
+
const { validateSkillName } = await import('../src/commands/create.js');
|
|
41
|
+
const result = validateSkillName('My-Skill');
|
|
42
|
+
expect(result).not.toBe(true);
|
|
43
|
+
expect(typeof result).toBe('string');
|
|
44
|
+
});
|
|
45
|
+
it('rejects names with spaces', async () => {
|
|
46
|
+
const { validateSkillName } = await import('../src/commands/create.js');
|
|
47
|
+
expect(validateSkillName('my skill')).not.toBe(true);
|
|
48
|
+
});
|
|
49
|
+
it('rejects names starting with a digit', async () => {
|
|
50
|
+
const { validateSkillName } = await import('../src/commands/create.js');
|
|
51
|
+
expect(validateSkillName('1skill')).not.toBe(true);
|
|
52
|
+
});
|
|
53
|
+
it('rejects empty string', async () => {
|
|
54
|
+
const { validateSkillName } = await import('../src/commands/create.js');
|
|
55
|
+
expect(validateSkillName('')).not.toBe(true);
|
|
56
|
+
});
|
|
57
|
+
it('rejects names with underscores', async () => {
|
|
58
|
+
const { validateSkillName } = await import('../src/commands/create.js');
|
|
59
|
+
expect(validateSkillName('my_skill')).not.toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// createCreateCommand structure
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
describe('SMI-3083: createCreateCommand', () => {
|
|
66
|
+
it('creates a command with name "create"', async () => {
|
|
67
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
68
|
+
const cmd = createCreateCommand();
|
|
69
|
+
expect(cmd).toBeInstanceOf(Command);
|
|
70
|
+
expect(cmd.name()).toBe('create');
|
|
71
|
+
});
|
|
72
|
+
it('has --output option with no baked-in default', async () => {
|
|
73
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
74
|
+
const cmd = createCreateCommand();
|
|
75
|
+
const opt = cmd.options.find((o) => o.long === '--output');
|
|
76
|
+
expect(opt).toBeDefined();
|
|
77
|
+
// default must be undefined — resolved lazily inside createSkill()
|
|
78
|
+
expect(opt?.defaultValue).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
it('has --type option', async () => {
|
|
81
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
82
|
+
const cmd = createCreateCommand();
|
|
83
|
+
expect(cmd.options.find((o) => o.long === '--type')).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
it('has --behavior option', async () => {
|
|
86
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
87
|
+
const cmd = createCreateCommand();
|
|
88
|
+
expect(cmd.options.find((o) => o.long === '--behavior')).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
it('has --description option', async () => {
|
|
91
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
92
|
+
const cmd = createCreateCommand();
|
|
93
|
+
expect(cmd.options.find((o) => o.long === '--description')).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
it('has --author option', async () => {
|
|
96
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
97
|
+
const cmd = createCreateCommand();
|
|
98
|
+
expect(cmd.options.find((o) => o.long === '--author')).toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
it('has --category option', async () => {
|
|
101
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
102
|
+
const cmd = createCreateCommand();
|
|
103
|
+
expect(cmd.options.find((o) => o.long === '--category')).toBeDefined();
|
|
104
|
+
});
|
|
105
|
+
it('has --scripts flag', async () => {
|
|
106
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
107
|
+
const cmd = createCreateCommand();
|
|
108
|
+
expect(cmd.options.find((o) => o.long === '--scripts')).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
it('has --yes flag', async () => {
|
|
111
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
112
|
+
const cmd = createCreateCommand();
|
|
113
|
+
expect(cmd.options.find((o) => o.long === '--yes')).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
it('has --dry-run flag', async () => {
|
|
116
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
117
|
+
const cmd = createCreateCommand();
|
|
118
|
+
expect(cmd.options.find((o) => o.long === '--dry-run')).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
it('has description mentioning ~/.claude/skills', async () => {
|
|
121
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
122
|
+
const cmd = createCreateCommand();
|
|
123
|
+
expect(cmd.description()).toContain('~/.claude/skills');
|
|
124
|
+
});
|
|
125
|
+
it('has description mentioning skillsmith author init', async () => {
|
|
126
|
+
const { createCreateCommand } = await import('../src/commands/create.js');
|
|
127
|
+
const cmd = createCreateCommand();
|
|
128
|
+
expect(cmd.description()).toContain('author init');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// createSkill scaffold behaviour
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
describe('SMI-3083: createSkill scaffold', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
vi.clearAllMocks();
|
|
137
|
+
});
|
|
138
|
+
afterEach(() => {
|
|
139
|
+
vi.restoreAllMocks();
|
|
140
|
+
});
|
|
141
|
+
/**
|
|
142
|
+
* Set up prompt mocks for tests that provide type/behavior/scripts via options.
|
|
143
|
+
* Prompt order (when those options are not provided via flags):
|
|
144
|
+
* input: description, author
|
|
145
|
+
* select: category, [type, behavior if not in options]
|
|
146
|
+
* confirm: [scripts if not in options], [overwrite if dir exists and !yes]
|
|
147
|
+
*/
|
|
148
|
+
async function setupMocks(overrides = {}) {
|
|
149
|
+
const { mkdir, writeFile, stat } = await import('fs/promises');
|
|
150
|
+
const { input, confirm, select } = await import('@inquirer/prompts');
|
|
151
|
+
if (overrides.statRejects !== false) {
|
|
152
|
+
vi.mocked(stat).mockRejectedValue(new Error('ENOENT'));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
vi.mocked(stat).mockResolvedValue({ isDirectory: () => true });
|
|
156
|
+
}
|
|
157
|
+
vi.mocked(mkdir).mockResolvedValue(undefined);
|
|
158
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
159
|
+
// Defaults assume type/behavior/scripts are passed via options (skips those prompts).
|
|
160
|
+
// Only description (input), author (input), and category (select) are prompted.
|
|
161
|
+
const selectResponses = overrides.selectResponses ?? ['development'];
|
|
162
|
+
const confirmResponses = overrides.confirmResponses ?? [];
|
|
163
|
+
const inputResponses = overrides.inputResponses ?? ['A test skill', 'testuser'];
|
|
164
|
+
for (const val of selectResponses) {
|
|
165
|
+
vi.mocked(select).mockResolvedValueOnce(val);
|
|
166
|
+
}
|
|
167
|
+
for (const val of confirmResponses) {
|
|
168
|
+
vi.mocked(confirm).mockResolvedValueOnce(val);
|
|
169
|
+
}
|
|
170
|
+
for (const val of inputResponses) {
|
|
171
|
+
vi.mocked(input).mockResolvedValueOnce(val);
|
|
172
|
+
}
|
|
173
|
+
return { mkdir, writeFile, stat };
|
|
174
|
+
}
|
|
175
|
+
it('scaffolds exactly 4 files when scripts=false (SKILL.md, README.md, CHANGELOG.md, .gitignore)', async () => {
|
|
176
|
+
const { writeFile } = await setupMocks();
|
|
177
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
178
|
+
await createSkill('test-skill', {
|
|
179
|
+
output: '/tmp/test-skills',
|
|
180
|
+
type: 'basic',
|
|
181
|
+
behavior: 'autonomous',
|
|
182
|
+
scripts: false,
|
|
183
|
+
});
|
|
184
|
+
expect(vi.mocked(writeFile)).toHaveBeenCalledTimes(4);
|
|
185
|
+
});
|
|
186
|
+
it('scaffolds exactly 5 files when scripts=true (+ scripts/example.js)', async () => {
|
|
187
|
+
const { writeFile } = await setupMocks();
|
|
188
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
189
|
+
await createSkill('test-skill', {
|
|
190
|
+
output: '/tmp/test-skills',
|
|
191
|
+
type: 'basic',
|
|
192
|
+
behavior: 'autonomous',
|
|
193
|
+
scripts: true,
|
|
194
|
+
});
|
|
195
|
+
expect(vi.mocked(writeFile)).toHaveBeenCalledTimes(5);
|
|
196
|
+
});
|
|
197
|
+
it('creates resources/ directory', async () => {
|
|
198
|
+
const { mkdir } = await setupMocks();
|
|
199
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
200
|
+
await createSkill('test-skill', {
|
|
201
|
+
output: '/tmp/test-skills',
|
|
202
|
+
type: 'basic',
|
|
203
|
+
behavior: 'autonomous',
|
|
204
|
+
scripts: false,
|
|
205
|
+
});
|
|
206
|
+
const mkdirCalls = vi.mocked(mkdir).mock.calls.map((c) => c[0]);
|
|
207
|
+
expect(mkdirCalls.some((p) => p.endsWith('resources'))).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
it('CHANGELOG.md content contains [1.0.0]', async () => {
|
|
210
|
+
const { writeFile } = await setupMocks();
|
|
211
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
212
|
+
await createSkill('test-skill', {
|
|
213
|
+
output: '/tmp/test-skills',
|
|
214
|
+
type: 'basic',
|
|
215
|
+
behavior: 'autonomous',
|
|
216
|
+
scripts: false,
|
|
217
|
+
});
|
|
218
|
+
const call = vi
|
|
219
|
+
.mocked(writeFile)
|
|
220
|
+
.mock.calls.find((c) => c[0].endsWith('CHANGELOG.md'));
|
|
221
|
+
expect(call).toBeDefined();
|
|
222
|
+
expect(call[1]).toContain('[1.0.0]');
|
|
223
|
+
});
|
|
224
|
+
it('SKILL.md content contains Behavioral Classification section', async () => {
|
|
225
|
+
const { writeFile } = await setupMocks();
|
|
226
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
227
|
+
await createSkill('test-skill', {
|
|
228
|
+
output: '/tmp/test-skills',
|
|
229
|
+
type: 'advanced',
|
|
230
|
+
behavior: 'guided',
|
|
231
|
+
scripts: false,
|
|
232
|
+
});
|
|
233
|
+
const call = vi.mocked(writeFile).mock.calls.find((c) => c[0].endsWith('SKILL.md'));
|
|
234
|
+
expect(call).toBeDefined();
|
|
235
|
+
expect(call[1]).toContain('Behavioral Classification');
|
|
236
|
+
});
|
|
237
|
+
it('.gitignore is written with node_modules entry', async () => {
|
|
238
|
+
const { writeFile } = await setupMocks();
|
|
239
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
240
|
+
await createSkill('test-skill', {
|
|
241
|
+
output: '/tmp/test-skills',
|
|
242
|
+
type: 'basic',
|
|
243
|
+
behavior: 'autonomous',
|
|
244
|
+
scripts: false,
|
|
245
|
+
});
|
|
246
|
+
const call = vi
|
|
247
|
+
.mocked(writeFile)
|
|
248
|
+
.mock.calls.find((c) => c[0].endsWith('.gitignore'));
|
|
249
|
+
expect(call).toBeDefined();
|
|
250
|
+
expect(call[1]).toContain('node_modules/');
|
|
251
|
+
});
|
|
252
|
+
it('scripts/example.js uses JSON.stringify (safe for skill names with quotes)', async () => {
|
|
253
|
+
const { writeFile } = await setupMocks();
|
|
254
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
255
|
+
await createSkill('test-skill', {
|
|
256
|
+
output: '/tmp/test-skills',
|
|
257
|
+
type: 'basic',
|
|
258
|
+
behavior: 'autonomous',
|
|
259
|
+
scripts: true,
|
|
260
|
+
});
|
|
261
|
+
const call = vi
|
|
262
|
+
.mocked(writeFile)
|
|
263
|
+
.mock.calls.find((c) => c[0].endsWith('example.js'));
|
|
264
|
+
expect(call).toBeDefined();
|
|
265
|
+
// JSON.stringify wraps the string in double quotes — no raw single-quote interpolation
|
|
266
|
+
expect(call[1]).toContain('"test-skill script executed"');
|
|
267
|
+
});
|
|
268
|
+
it('does not write files when user declines overwrite', async () => {
|
|
269
|
+
// stat resolves (dir exists); confirm returns false (decline)
|
|
270
|
+
const { writeFile, stat } = await setupMocks({
|
|
271
|
+
statRejects: false,
|
|
272
|
+
confirmResponses: [false],
|
|
273
|
+
});
|
|
274
|
+
vi.mocked(stat).mockResolvedValue({ isDirectory: () => true });
|
|
275
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
276
|
+
await createSkill('existing-skill', {
|
|
277
|
+
output: '/tmp/test-skills',
|
|
278
|
+
type: 'basic',
|
|
279
|
+
behavior: 'autonomous',
|
|
280
|
+
scripts: false,
|
|
281
|
+
});
|
|
282
|
+
expect(vi.mocked(writeFile)).not.toHaveBeenCalled();
|
|
283
|
+
});
|
|
284
|
+
it('--yes always overwrites without prompting even if directory exists', async () => {
|
|
285
|
+
const { writeFile, stat, mkdir } = await setupMocks();
|
|
286
|
+
vi.mocked(stat).mockResolvedValue({ isDirectory: () => true });
|
|
287
|
+
vi.mocked(mkdir).mockResolvedValue(undefined);
|
|
288
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
289
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
290
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
291
|
+
await createSkill('existing-skill', {
|
|
292
|
+
output: '/tmp/test-skills',
|
|
293
|
+
type: 'basic',
|
|
294
|
+
behavior: 'autonomous',
|
|
295
|
+
scripts: false,
|
|
296
|
+
yes: true,
|
|
297
|
+
});
|
|
298
|
+
expect(vi.mocked(confirm)).not.toHaveBeenCalled();
|
|
299
|
+
expect(vi.mocked(writeFile)).toHaveBeenCalledTimes(4);
|
|
300
|
+
});
|
|
301
|
+
it('--dry-run prints preview without writing any files', async () => {
|
|
302
|
+
await setupMocks();
|
|
303
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
304
|
+
const { writeFile, mkdir } = await import('fs/promises');
|
|
305
|
+
await createSkill('test-skill', {
|
|
306
|
+
output: '/tmp/test-skills',
|
|
307
|
+
type: 'basic',
|
|
308
|
+
behavior: 'autonomous',
|
|
309
|
+
scripts: false,
|
|
310
|
+
dryRun: true,
|
|
311
|
+
});
|
|
312
|
+
expect(vi.mocked(writeFile)).not.toHaveBeenCalled();
|
|
313
|
+
expect(vi.mocked(mkdir)).not.toHaveBeenCalled();
|
|
314
|
+
});
|
|
315
|
+
it('supports fully non-interactive mode via --description, --author, --category, --type, --behavior, --scripts', async () => {
|
|
316
|
+
const { mkdir, writeFile, stat } = await import('fs/promises');
|
|
317
|
+
const { input, confirm, select } = await import('@inquirer/prompts');
|
|
318
|
+
vi.mocked(stat).mockRejectedValue(new Error('ENOENT'));
|
|
319
|
+
vi.mocked(mkdir).mockResolvedValue(undefined);
|
|
320
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
321
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
322
|
+
await createSkill('test-skill', {
|
|
323
|
+
output: '/tmp/test-skills',
|
|
324
|
+
description: 'A test skill',
|
|
325
|
+
author: 'testuser',
|
|
326
|
+
category: 'development',
|
|
327
|
+
type: 'basic',
|
|
328
|
+
behavior: 'autonomous',
|
|
329
|
+
scripts: false,
|
|
330
|
+
});
|
|
331
|
+
// No prompts should have been called
|
|
332
|
+
expect(vi.mocked(input)).not.toHaveBeenCalled();
|
|
333
|
+
expect(vi.mocked(select)).not.toHaveBeenCalled();
|
|
334
|
+
expect(vi.mocked(confirm)).not.toHaveBeenCalled();
|
|
335
|
+
expect(vi.mocked(writeFile)).toHaveBeenCalledTimes(4);
|
|
336
|
+
});
|
|
337
|
+
it('calls spinner.fail and rethrows when mkdir fails', async () => {
|
|
338
|
+
const { mkdir, writeFile, stat } = await import('fs/promises');
|
|
339
|
+
const { input, select } = await import('@inquirer/prompts');
|
|
340
|
+
const ora = await import('ora');
|
|
341
|
+
const failMock = vi.fn().mockReturnThis();
|
|
342
|
+
vi.mocked(ora.default).mockReturnValue({
|
|
343
|
+
start: vi.fn().mockReturnThis(),
|
|
344
|
+
stop: vi.fn().mockReturnThis(),
|
|
345
|
+
succeed: vi.fn().mockReturnThis(),
|
|
346
|
+
fail: failMock,
|
|
347
|
+
warn: vi.fn().mockReturnThis(),
|
|
348
|
+
text: '',
|
|
349
|
+
});
|
|
350
|
+
vi.mocked(stat).mockRejectedValue(new Error('ENOENT'));
|
|
351
|
+
vi.mocked(mkdir).mockRejectedValue(new Error('Permission denied'));
|
|
352
|
+
vi.mocked(writeFile).mockResolvedValue(undefined);
|
|
353
|
+
// All input prompts return a valid value; all select prompts return a valid category
|
|
354
|
+
vi.mocked(input).mockResolvedValue('desc');
|
|
355
|
+
vi.mocked(select).mockResolvedValue('development');
|
|
356
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
357
|
+
await expect(createSkill('fail-skill', {
|
|
358
|
+
output: '/tmp/test-skills',
|
|
359
|
+
type: 'basic',
|
|
360
|
+
behavior: 'autonomous',
|
|
361
|
+
scripts: false,
|
|
362
|
+
})).rejects.toThrow();
|
|
363
|
+
expect(failMock).toHaveBeenCalled();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// Validation — CLI flag guards
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
describe('SMI-3083: createSkill validation guards', () => {
|
|
370
|
+
beforeEach(() => {
|
|
371
|
+
vi.clearAllMocks();
|
|
372
|
+
});
|
|
373
|
+
it('rejects invalid --type value and exits', async () => {
|
|
374
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
375
|
+
throw new Error('process.exit called');
|
|
376
|
+
});
|
|
377
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
378
|
+
await expect(createSkill('test-skill', {
|
|
379
|
+
output: '/tmp/test-skills',
|
|
380
|
+
type: 'invalid-type',
|
|
381
|
+
behavior: 'autonomous',
|
|
382
|
+
description: 'desc',
|
|
383
|
+
author: 'testuser',
|
|
384
|
+
category: 'development',
|
|
385
|
+
scripts: false,
|
|
386
|
+
})).rejects.toThrow();
|
|
387
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
388
|
+
exitSpy.mockRestore();
|
|
389
|
+
});
|
|
390
|
+
it('rejects invalid --behavior value and exits', async () => {
|
|
391
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
392
|
+
throw new Error('process.exit called');
|
|
393
|
+
});
|
|
394
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
395
|
+
await expect(createSkill('test-skill', {
|
|
396
|
+
output: '/tmp/test-skills',
|
|
397
|
+
type: 'basic',
|
|
398
|
+
behavior: 'invalid-behavior',
|
|
399
|
+
description: 'desc',
|
|
400
|
+
author: 'testuser',
|
|
401
|
+
category: 'development',
|
|
402
|
+
scripts: false,
|
|
403
|
+
})).rejects.toThrow();
|
|
404
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
405
|
+
exitSpy.mockRestore();
|
|
406
|
+
});
|
|
407
|
+
it('rejects invalid --category value and exits', async () => {
|
|
408
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
|
|
409
|
+
throw new Error('process.exit called');
|
|
410
|
+
});
|
|
411
|
+
const { createSkill } = await import('../src/commands/create.js');
|
|
412
|
+
await expect(createSkill('test-skill', {
|
|
413
|
+
output: '/tmp/test-skills',
|
|
414
|
+
type: 'basic',
|
|
415
|
+
behavior: 'autonomous',
|
|
416
|
+
description: 'desc',
|
|
417
|
+
author: 'testuser',
|
|
418
|
+
category: 'invalid-category',
|
|
419
|
+
scripts: false,
|
|
420
|
+
})).rejects.toThrow();
|
|
421
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
422
|
+
exitSpy.mockRestore();
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Template exports
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
describe('SMI-3083: CHANGELOG_MD_TEMPLATE', () => {
|
|
429
|
+
it('is exported from templates/index', async () => {
|
|
430
|
+
const { CHANGELOG_MD_TEMPLATE } = await import('../src/templates/index.js');
|
|
431
|
+
expect(typeof CHANGELOG_MD_TEMPLATE).toBe('string');
|
|
432
|
+
});
|
|
433
|
+
it('contains [1.0.0] placeholder pattern', async () => {
|
|
434
|
+
const { CHANGELOG_MD_TEMPLATE } = await import('../src/templates/index.js');
|
|
435
|
+
expect(CHANGELOG_MD_TEMPLATE).toContain('[1.0.0]');
|
|
436
|
+
});
|
|
437
|
+
it('contains {{name}} and {{date}} placeholders', async () => {
|
|
438
|
+
const { CHANGELOG_MD_TEMPLATE } = await import('../src/templates/index.js');
|
|
439
|
+
expect(CHANGELOG_MD_TEMPLATE).toContain('{{name}}');
|
|
440
|
+
expect(CHANGELOG_MD_TEMPLATE).toContain('{{date}}');
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
describe('SMI-3083: SKILL_MD_TEMPLATE has {{behavioralClassification}} placeholder', () => {
|
|
444
|
+
it('contains {{behavioralClassification}} for create/init branching', async () => {
|
|
445
|
+
const { SKILL_MD_TEMPLATE } = await import('../src/templates/index.js');
|
|
446
|
+
expect(SKILL_MD_TEMPLATE).toContain('{{behavioralClassification}}');
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
//# sourceMappingURL=create.test.js.map
|