@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.
Files changed (117) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/src/commands/audit.d.ts +19 -0
  3. package/dist/src/commands/audit.d.ts.map +1 -0
  4. package/dist/src/commands/audit.js +134 -0
  5. package/dist/src/commands/audit.js.map +1 -0
  6. package/dist/src/commands/audit.test.d.ts +8 -0
  7. package/dist/src/commands/audit.test.d.ts.map +1 -0
  8. package/dist/src/commands/audit.test.js +97 -0
  9. package/dist/src/commands/audit.test.js.map +1 -0
  10. package/dist/src/commands/author/init.d.ts.map +1 -1
  11. package/dist/src/commands/author/init.js +4 -9
  12. package/dist/src/commands/author/init.js.map +1 -1
  13. package/dist/src/commands/create.d.ts +42 -0
  14. package/dist/src/commands/create.d.ts.map +1 -0
  15. package/dist/src/commands/create.js +325 -0
  16. package/dist/src/commands/create.js.map +1 -0
  17. package/dist/src/commands/diff.d.ts +17 -0
  18. package/dist/src/commands/diff.d.ts.map +1 -0
  19. package/dist/src/commands/diff.js +191 -0
  20. package/dist/src/commands/diff.js.map +1 -0
  21. package/dist/src/commands/diff.test.d.ts +6 -0
  22. package/dist/src/commands/diff.test.d.ts.map +1 -0
  23. package/dist/src/commands/diff.test.js +275 -0
  24. package/dist/src/commands/diff.test.js.map +1 -0
  25. package/dist/src/commands/index.d.ts +5 -0
  26. package/dist/src/commands/index.d.ts.map +1 -1
  27. package/dist/src/commands/index.js +10 -1
  28. package/dist/src/commands/index.js.map +1 -1
  29. package/dist/src/commands/install-skill.d.ts.map +1 -1
  30. package/dist/src/commands/install-skill.js +10 -3
  31. package/dist/src/commands/install-skill.js.map +1 -1
  32. package/dist/src/commands/install.d.ts +13 -0
  33. package/dist/src/commands/install.d.ts.map +1 -0
  34. package/dist/src/commands/install.js +213 -0
  35. package/dist/src/commands/install.js.map +1 -0
  36. package/dist/src/commands/manage.d.ts.map +1 -1
  37. package/dist/src/commands/manage.js +71 -27
  38. package/dist/src/commands/manage.js.map +1 -1
  39. package/dist/src/commands/pin.d.ts +24 -0
  40. package/dist/src/commands/pin.d.ts.map +1 -0
  41. package/dist/src/commands/pin.js +123 -0
  42. package/dist/src/commands/pin.js.map +1 -0
  43. package/dist/src/commands/pin.test.d.ts +6 -0
  44. package/dist/src/commands/pin.test.d.ts.map +1 -0
  45. package/dist/src/commands/pin.test.js +206 -0
  46. package/dist/src/commands/pin.test.js.map +1 -0
  47. package/dist/src/commands/search.d.ts.map +1 -1
  48. package/dist/src/commands/search.js +43 -3
  49. package/dist/src/commands/search.js.map +1 -1
  50. package/dist/src/commands/sync.d.ts.map +1 -1
  51. package/dist/src/commands/sync.js +3 -2
  52. package/dist/src/commands/sync.js.map +1 -1
  53. package/dist/src/index.d.ts +2 -1
  54. package/dist/src/index.d.ts.map +1 -1
  55. package/dist/src/index.js +16 -3
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/templates/changelog.md.template.d.ts +8 -0
  58. package/dist/src/templates/changelog.md.template.d.ts.map +1 -0
  59. package/dist/src/templates/changelog.md.template.js +19 -0
  60. package/dist/src/templates/changelog.md.template.js.map +1 -0
  61. package/dist/src/templates/index.d.ts +1 -0
  62. package/dist/src/templates/index.d.ts.map +1 -1
  63. package/dist/src/templates/index.js +1 -0
  64. package/dist/src/templates/index.js.map +1 -1
  65. package/dist/src/templates/readme.md.template.d.ts +1 -1
  66. package/dist/src/templates/readme.md.template.d.ts.map +1 -1
  67. package/dist/src/templates/readme.md.template.js +1 -1
  68. package/dist/src/templates/skill.md.template.d.ts +1 -1
  69. package/dist/src/templates/skill.md.template.d.ts.map +1 -1
  70. package/dist/src/templates/skill.md.template.js +2 -4
  71. package/dist/src/templates/skill.md.template.js.map +1 -1
  72. package/dist/src/utils/license-validation.js +1 -1
  73. package/dist/src/utils/license-validation.js.map +1 -1
  74. package/dist/src/utils/manifest.d.ts +46 -0
  75. package/dist/src/utils/manifest.d.ts.map +1 -0
  76. package/dist/src/utils/manifest.js +55 -0
  77. package/dist/src/utils/manifest.js.map +1 -0
  78. package/dist/src/utils/skill-name.d.ts +19 -0
  79. package/dist/src/utils/skill-name.d.ts.map +1 -0
  80. package/dist/src/utils/skill-name.js +26 -0
  81. package/dist/src/utils/skill-name.js.map +1 -0
  82. package/dist/src/utils/skills-directory.d.ts +12 -3
  83. package/dist/src/utils/skills-directory.d.ts.map +1 -1
  84. package/dist/src/utils/skills-directory.js +63 -7
  85. package/dist/src/utils/skills-directory.js.map +1 -1
  86. package/dist/tests/create.test.d.ts +5 -0
  87. package/dist/tests/create.test.d.ts.map +1 -0
  88. package/dist/tests/create.test.js +449 -0
  89. package/dist/tests/create.test.js.map +1 -0
  90. package/dist/tests/install-skill.test.js +9 -4
  91. package/dist/tests/install-skill.test.js.map +1 -1
  92. package/dist/tests/license-validation.test.d.ts +8 -0
  93. package/dist/tests/license-validation.test.d.ts.map +1 -0
  94. package/dist/tests/license-validation.test.js +186 -0
  95. package/dist/tests/license-validation.test.js.map +1 -0
  96. package/dist/tests/manage.test.js +84 -0
  97. package/dist/tests/manage.test.js.map +1 -1
  98. package/dist/tests/recommend-helpers.test.d.ts +9 -0
  99. package/dist/tests/recommend-helpers.test.d.ts.map +1 -0
  100. package/dist/tests/recommend-helpers.test.js +308 -0
  101. package/dist/tests/recommend-helpers.test.js.map +1 -0
  102. package/dist/tests/recommend-scoring.test.d.ts +8 -0
  103. package/dist/tests/recommend-scoring.test.d.ts.map +1 -0
  104. package/dist/tests/recommend-scoring.test.js +184 -0
  105. package/dist/tests/recommend-scoring.test.js.map +1 -0
  106. package/dist/tests/tool-analyzer.test.d.ts +8 -0
  107. package/dist/tests/tool-analyzer.test.d.ts.map +1 -0
  108. package/dist/tests/tool-analyzer.test.js +149 -0
  109. package/dist/tests/tool-analyzer.test.js.map +1 -0
  110. package/dist/tests/unit/commands/install.test.d.ts +7 -0
  111. package/dist/tests/unit/commands/install.test.d.ts.map +1 -0
  112. package/dist/tests/unit/commands/install.test.js +407 -0
  113. package/dist/tests/unit/commands/install.test.js.map +1 -0
  114. package/dist/vitest.config.d.ts.map +1 -1
  115. package/dist/vitest.config.js +9 -3
  116. package/dist/vitest.config.js.map +1 -1
  117. 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;AAKH,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAE9D,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;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAqDzF;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC,CAqBpE"}
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: false, // Would check remote for updates
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":"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"}
1
+ {"version":3,"file":"skills-directory.js","sourceRoot":"","sources":["../../../src/utils/skills-directory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * SMI-3083: skillsmith create command tests
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=create.test.d.ts.map
@@ -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