antikit 1.12.6 → 1.12.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antikit",
3
- "version": "1.12.6",
3
+ "version": "1.12.7",
4
4
  "description": "CLI tool to manage AI agent skills from Anti Gravity skills repository",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -8,7 +8,9 @@
8
8
  "antikit": "src/index.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1",
11
+ "test": "vitest run",
12
+ "test:watch": "vitest",
13
+ "test:coverage": "vitest run --coverage",
12
14
  "release": "commit-and-tag-version",
13
15
  "prepare": "husky",
14
16
  "format": "prettier --write ."
@@ -57,9 +59,11 @@
57
59
  "devDependencies": {
58
60
  "@commitlint/cli": "^20.3.1",
59
61
  "@commitlint/config-conventional": "^20.3.1",
62
+ "@vitest/coverage-v8": "^3.0.0",
60
63
  "commit-and-tag-version": "^12.6.1",
61
64
  "husky": "^9.1.7",
62
65
  "lint-staged": "^16.2.7",
63
- "prettier": "^3.7.4"
66
+ "prettier": "^3.7.4",
67
+ "vitest": "^3.0.0"
64
68
  }
65
69
  }
@@ -1,11 +1,15 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { execSync } from 'child_process';
4
- import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync } from 'fs';
4
+ import { existsSync, mkdirSync, cpSync, rmSync, writeFileSync, readFileSync } from 'fs';
5
5
  import { join } from 'path';
6
6
  import { homedir } from 'os';
7
7
  import { getOrCreateSkillsDir, skillExists } from '../utils/local.js';
8
8
  import { fetchRemoteSkills, getSkillCloneUrl } from '../utils/github.js';
9
+ import { DEFAULT_VERSION, parseVersionFromContent } from '../utils/version.js';
10
+ import { METADATA_FILE, SKILL_MD } from '../utils/constants.js';
11
+ import { debug } from '../utils/logger.js';
12
+ import { AntikitError, ErrorCodes } from '../utils/errors.js';
9
13
 
10
14
  const CACHE_DIR = join(homedir(), '.antikit');
11
15
 
@@ -81,10 +85,8 @@ export async function installSkill(skillName, options = {}) {
81
85
 
82
86
  // --- Check dependencies ---
83
87
  try {
84
- const mdPath = join(sourcePath, 'SKILL.md');
88
+ const mdPath = join(sourcePath, SKILL_MD);
85
89
  if (existsSync(mdPath)) {
86
- // Import locally to avoid cluttering top imports if possible, or just use fs
87
- const { readFileSync } = await import('fs');
88
90
  const content = readFileSync(mdPath, 'utf-8');
89
91
 
90
92
  // Parse frontmatter dependencies
@@ -136,7 +138,7 @@ export async function installSkill(skillName, options = {}) {
136
138
  }
137
139
  } catch (e) {
138
140
  // Dep check failed, but continue installing main skill
139
- // console.error(e);
141
+ debug('Dependency check failed:', e.message);
140
142
  }
141
143
 
142
144
  if (options.force && existsSync(destPath)) {
@@ -147,15 +149,11 @@ export async function installSkill(skillName, options = {}) {
147
149
 
148
150
  // Save skill metadata for future upgrades
149
151
  try {
150
- let version = '0.0.0';
151
- const mdPath = join(destPath, 'SKILL.md');
152
+ let version = DEFAULT_VERSION;
153
+ const mdPath = join(destPath, SKILL_MD);
152
154
  if (existsSync(mdPath)) {
153
- // We likely already imported readFileSync if inside try block,
154
- // but for safety use dynamic import or assume imported
155
- const { readFileSync } = await import('fs');
156
155
  const content = readFileSync(mdPath, 'utf-8');
157
- const vMatch = content.match(/^version:\s*(.+)/m);
158
- if (vMatch) version = vMatch[1].trim();
156
+ version = parseVersionFromContent(content);
159
157
  }
160
158
 
161
159
  const metadata = {
@@ -168,9 +166,9 @@ export async function installSkill(skillName, options = {}) {
168
166
  version,
169
167
  installedAt: Date.now()
170
168
  };
171
- writeFileSync(join(destPath, '.antikit-skill.json'), JSON.stringify(metadata, null, 2));
169
+ writeFileSync(join(destPath, METADATA_FILE), JSON.stringify(metadata, null, 2));
172
170
  } catch (e) {
173
- // Ignore metadata write error, not critical
171
+ debug('Failed to write metadata:', e.message);
174
172
  }
175
173
 
176
174
  // Cleanup temp
@@ -7,17 +7,8 @@ import { skillExists, getOrCreateSkillsDir } from '../utils/local.js';
7
7
  import { installSkill } from './install.js';
8
8
  import { existsSync, readFileSync } from 'fs';
9
9
  import { join } from 'path';
10
-
11
- function compareVersions(v1, v2) {
12
- if (!v1 || !v2) return 0;
13
- const p1 = v1.split('.').map(Number);
14
- const p2 = v2.split('.').map(Number);
15
- for (let i = 0; i < 3; i++) {
16
- if ((p1[i] || 0) > (p2[i] || 0)) return 1;
17
- if ((p1[i] || 0) < (p2[i] || 0)) return -1;
18
- }
19
- return 0;
20
- }
10
+ import { compareVersions, DEFAULT_VERSION } from '../utils/version.js';
11
+ import { METADATA_FILE, OFFICIAL_SOURCE } from '../utils/constants.js';
21
12
 
22
13
  export async function listRemoteSkills(options) {
23
14
  const sourceName = options.source || null;
@@ -49,7 +40,7 @@ export async function listRemoteSkills(options) {
49
40
  const skillsWithInfo = await Promise.all(
50
41
  skills.map(async skill => {
51
42
  let description = skill.description;
52
- let remoteVersion = skill.version || '0.0.0';
43
+ let remoteVersion = skill.version || DEFAULT_VERSION;
53
44
 
54
45
  // Only fetch info if not already fetched (REST fallback)
55
46
  if (description === undefined || description === null) {
@@ -62,19 +53,19 @@ export async function listRemoteSkills(options) {
62
53
  skill.branch
63
54
  );
64
55
  description = info ? info.description : null;
65
- remoteVersion = info ? info.version : '0.0.0';
56
+ remoteVersion = info ? info.version : DEFAULT_VERSION;
66
57
  }
67
58
 
68
59
  const installed = skillExists(skill.name);
69
60
  let updateAvailable = false;
70
- let localVersion = '0.0.0';
61
+ let localVersion = DEFAULT_VERSION;
71
62
 
72
63
  if (installed) {
73
64
  try {
74
- const metaPath = join(skillsDir, skill.name, '.antikit-skill.json');
65
+ const metaPath = join(skillsDir, skill.name, METADATA_FILE);
75
66
  if (existsSync(metaPath)) {
76
67
  const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
77
- localVersion = meta.version || '0.0.0';
68
+ localVersion = meta.version || DEFAULT_VERSION;
78
69
  updateAvailable = compareVersions(remoteVersion, localVersion) > 0;
79
70
  }
80
71
  } catch {}
@@ -118,8 +109,8 @@ function displaySkillsList(skills) {
118
109
 
119
110
  // Sort: Official first, then Source, then Name
120
111
  skills.sort((a, b) => {
121
- if (a.source === 'official' && b.source !== 'official') return -1;
122
- if (b.source === 'official' && a.source !== 'official') return 1;
112
+ if (a.source === OFFICIAL_SOURCE && b.source !== OFFICIAL_SOURCE) return -1;
113
+ if (b.source === OFFICIAL_SOURCE && a.source !== OFFICIAL_SOURCE) return 1;
123
114
  if (a.source !== b.source) return a.source.localeCompare(b.source);
124
115
  return a.name.localeCompare(b.name);
125
116
  });
@@ -130,7 +121,7 @@ function displaySkillsList(skills) {
130
121
  status = skill.updateAvailable ? chalk.yellow('Update') : chalk.green('Installed');
131
122
  }
132
123
 
133
- let versionDisplay = skill.remoteVersion || '0.0.0';
124
+ let versionDisplay = skill.remoteVersion || DEFAULT_VERSION;
134
125
  if (skill.installed && skill.localVersion) {
135
126
  if (skill.updateAvailable) {
136
127
  versionDisplay = `${chalk.dim(skill.localVersion)}→${chalk.yellow(skill.remoteVersion)}`;
@@ -159,8 +150,8 @@ function displaySkillsList(skills) {
159
150
  async function interactiveInstall(skills) {
160
151
  // Sort skills by Source (Official first) then Name
161
152
  skills.sort((a, b) => {
162
- if (a.source === 'official' && b.source !== 'official') return -1;
163
- if (b.source === 'official' && a.source !== 'official') return 1;
153
+ if (a.source === OFFICIAL_SOURCE && b.source !== OFFICIAL_SOURCE) return -1;
154
+ if (b.source === OFFICIAL_SOURCE && a.source !== OFFICIAL_SOURCE) return 1;
164
155
  if (a.source !== b.source) return a.source.localeCompare(b.source);
165
156
  return a.name.localeCompare(b.name);
166
157
  });
@@ -1,6 +1,7 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
+ import { OFFICIAL_SOURCE, DEFAULT_BRANCH } from './constants.js';
4
5
 
5
6
  const CONFIG_DIR = join(homedir(), '.antikit');
6
7
  const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
@@ -8,24 +9,24 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
8
9
  const DEFAULT_CONFIG = {
9
10
  sources: [
10
11
  {
11
- name: 'official',
12
+ name: OFFICIAL_SOURCE,
12
13
  owner: 'vunamhung',
13
14
  repo: 'antiskills',
14
- branch: 'main',
15
+ branch: DEFAULT_BRANCH,
15
16
  default: true
16
17
  },
17
18
  {
18
19
  name: 'claudekit',
19
20
  owner: 'mrgoonie',
20
21
  repo: 'claudekit-skills',
21
- branch: 'main',
22
+ branch: DEFAULT_BRANCH,
22
23
  path: '.claude/skills'
23
24
  },
24
25
  {
25
26
  name: 'ui-ux-pro',
26
27
  owner: 'nextlevelbuilder',
27
28
  repo: 'ui-ux-pro-max-skill',
28
- branch: 'main',
29
+ branch: DEFAULT_BRANCH,
29
30
  path: '.claude/skills'
30
31
  }
31
32
  ]
@@ -76,8 +77,8 @@ export function getSources() {
76
77
 
77
78
  // Enforce 'official' source always at the top
78
79
  return sources.sort((a, b) => {
79
- if (a.name === 'official') return -1;
80
- if (b.name === 'official') return 1;
80
+ if (a.name === OFFICIAL_SOURCE) return -1;
81
+ if (b.name === OFFICIAL_SOURCE) return 1;
81
82
  return 0;
82
83
  });
83
84
  }
@@ -85,7 +86,7 @@ export function getSources() {
85
86
  /**
86
87
  * Add a new source
87
88
  */
88
- export function addSource(name, owner, repo, branch = 'main', path = null) {
89
+ export function addSource(name, owner, repo, branch = DEFAULT_BRANCH, path = null) {
89
90
  const config = loadConfig();
90
91
 
91
92
  // Check if source with same name exists
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Shared constants for antikit
3
+ * Eliminates magic strings throughout the codebase
4
+ */
5
+
6
+ // Source names
7
+ export const OFFICIAL_SOURCE = 'official';
8
+
9
+ // File names
10
+ export const SKILL_MD = 'SKILL.md';
11
+ export const METADATA_FILE = '.antikit-skill.json';
12
+
13
+ // Directory names
14
+ export const SKILLS_DIR = '.agent/skills';
15
+
16
+ // Cache settings
17
+ export const CACHE_TTL = 1000 * 60 * 60; // 1 hour
18
+ export const UPDATE_CHECK_INTERVAL = 1000 * 60 * 60 * 6; // 6 hours
19
+
20
+ // API endpoints
21
+ export const GITHUB_API = 'https://api.github.com';
22
+ export const GITHUB_GRAPHQL = 'https://api.github.com/graphql';
23
+ export const GITHUB_RAW = 'https://raw.githubusercontent.com';
24
+
25
+ // Default branch
26
+ export const DEFAULT_BRANCH = 'main';
27
+
28
+ // Status indicators
29
+ export const STATUS = {
30
+ INSTALLED: 'installed',
31
+ UPDATE_AVAILABLE: 'update',
32
+ NOT_INSTALLED: 'not_installed'
33
+ };
34
+
35
+ // Max limits
36
+ export const MAX_DIRECTORY_DEPTH = 50;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Custom error classes for antikit
3
+ * Provides structured error handling with error codes
4
+ */
5
+
6
+ /**
7
+ * Base error class for antikit
8
+ */
9
+ export class AntikitError extends Error {
10
+ /**
11
+ * @param {string} message - Error message
12
+ * @param {string} code - Error code (e.g., 'SKILL_NOT_FOUND')
13
+ * @param {Object} [context] - Additional context data
14
+ */
15
+ constructor(message, code, context = {}) {
16
+ super(message);
17
+ this.name = 'AntikitError';
18
+ this.code = code;
19
+ this.context = context;
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Error codes enum
25
+ */
26
+ export const ErrorCodes = {
27
+ // Skill errors
28
+ SKILL_NOT_FOUND: 'SKILL_NOT_FOUND',
29
+ SKILL_ALREADY_EXISTS: 'SKILL_ALREADY_EXISTS',
30
+ SKILL_INVALID_METADATA: 'SKILL_INVALID_METADATA',
31
+ SKILL_MISSING_METADATA: 'SKILL_MISSING_METADATA',
32
+
33
+ // Source errors
34
+ SOURCE_NOT_FOUND: 'SOURCE_NOT_FOUND',
35
+ SOURCE_ALREADY_EXISTS: 'SOURCE_ALREADY_EXISTS',
36
+ SOURCE_CANNOT_REMOVE_DEFAULT: 'SOURCE_CANNOT_REMOVE_DEFAULT',
37
+
38
+ // GitHub errors
39
+ GITHUB_RATE_LIMIT: 'GITHUB_RATE_LIMIT',
40
+ GITHUB_NOT_FOUND: 'GITHUB_NOT_FOUND',
41
+ GITHUB_AUTH_FAILED: 'GITHUB_AUTH_FAILED',
42
+ GITHUB_NETWORK_ERROR: 'GITHUB_NETWORK_ERROR',
43
+
44
+ // Config errors
45
+ CONFIG_INVALID: 'CONFIG_INVALID',
46
+ CONFIG_NOT_FOUND: 'CONFIG_NOT_FOUND',
47
+
48
+ // Git errors
49
+ GIT_CLONE_FAILED: 'GIT_CLONE_FAILED',
50
+ GIT_NOT_INSTALLED: 'GIT_NOT_INSTALLED',
51
+
52
+ // General errors
53
+ INVALID_INPUT: 'INVALID_INPUT',
54
+ DIRECTORY_NOT_FOUND: 'DIRECTORY_NOT_FOUND',
55
+ PERMISSION_DENIED: 'PERMISSION_DENIED',
56
+ UNKNOWN: 'UNKNOWN'
57
+ };
58
+
59
+ /**
60
+ * Create a skill not found error
61
+ */
62
+ export function skillNotFoundError(skillName, source = null) {
63
+ return new AntikitError(
64
+ `Skill "${skillName}" not found${source ? ` in source "${source}"` : ''}`,
65
+ ErrorCodes.SKILL_NOT_FOUND,
66
+ { skillName, source }
67
+ );
68
+ }
69
+
70
+ /**
71
+ * Create a rate limit error
72
+ */
73
+ export function rateLimitError() {
74
+ return new AntikitError(
75
+ 'GitHub API rate limit exceeded. Configure a token to increase limits.',
76
+ ErrorCodes.GITHUB_RATE_LIMIT
77
+ );
78
+ }
79
+
80
+ /**
81
+ * Create a source not found error
82
+ */
83
+ export function sourceNotFoundError(sourceName) {
84
+ return new AntikitError(`Source "${sourceName}" not found.`, ErrorCodes.SOURCE_NOT_FOUND, {
85
+ sourceName
86
+ });
87
+ }
88
+
89
+ /**
90
+ * Wrap an unknown error with context
91
+ */
92
+ export function wrapError(error, context = {}) {
93
+ if (error instanceof AntikitError) {
94
+ return error;
95
+ }
96
+ return new AntikitError(error.message || 'Unknown error occurred', ErrorCodes.UNKNOWN, {
97
+ ...context,
98
+ originalError: error
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Check if error is a specific type
104
+ */
105
+ export function isErrorCode(error, code) {
106
+ return error instanceof AntikitError && error.code === code;
107
+ }
@@ -1,7 +1,8 @@
1
1
  import chalk from 'chalk';
2
2
  import { getSources, getToken } from './configManager.js';
3
-
4
- const GITHUB_API = 'https://api.github.com';
3
+ import { debug } from './logger.js';
4
+ import { GITHUB_API, GITHUB_GRAPHQL, GITHUB_RAW, DEFAULT_BRANCH } from './constants.js';
5
+ import { DEFAULT_VERSION } from './version.js';
5
6
 
6
7
  // Global flag to prevent duplicate rate limit logs
7
8
  let hasLoggedRateLimit = false;
@@ -64,11 +65,11 @@ async function fetchSkillsViaGraphQL(source, token) {
64
65
  }
65
66
  `;
66
67
 
67
- const branch = source.branch || 'main';
68
+ const branch = source.branch || DEFAULT_BRANCH;
68
69
  const expression = source.path ? `${branch}:${source.path}` : `${branch}:`;
69
70
 
70
71
  try {
71
- const response = await fetch('https://api.github.com/graphql', {
72
+ const response = await fetch(GITHUB_GRAPHQL, {
72
73
  method: 'POST',
73
74
  headers: {
74
75
  Authorization: `token ${token}`,
@@ -96,7 +97,7 @@ async function fetchSkillsViaGraphQL(source, token) {
96
97
  .filter(item => item.type === 'tree' && !item.name.startsWith('.'))
97
98
  .map(item => {
98
99
  let description = null;
99
- let version = '0.0.0';
100
+ let version = DEFAULT_VERSION;
100
101
 
101
102
  const skillFile = item.object.file && item.object.file[0];
102
103
  if (skillFile && skillFile.object && skillFile.object.text) {
@@ -118,14 +119,15 @@ async function fetchSkillsViaGraphQL(source, token) {
118
119
  source: source.name,
119
120
  owner: source.owner,
120
121
  repo: source.repo,
121
- branch: source.branch || 'main',
122
+ branch: source.branch || DEFAULT_BRANCH,
122
123
  basePath: source.path,
123
124
  description,
124
125
  version
125
126
  };
126
127
  });
127
128
  } catch (e) {
128
- return null; // Fallback
129
+ debug('GraphQL fetch failed:', e.message);
130
+ return null; // Fallback to REST
129
131
  }
130
132
  }
131
133
 
@@ -153,8 +155,8 @@ async function fetchSkillsFromSource(source) {
153
155
  if (!response.ok) {
154
156
  const data = await response.json().catch(() => ({}));
155
157
 
156
- // Check for rate limit
157
- if (response.status === 403 && data.message.includes('rate limit')) {
158
+ // Check for rate limit (with null-safe access)
159
+ if (response.status === 403 && data.message?.includes('rate limit')) {
158
160
  logRateLimitError();
159
161
  }
160
162
 
@@ -182,7 +184,7 @@ async function fetchSkillsFromSource(source) {
182
184
  source: source.name,
183
185
  owner: source.owner,
184
186
  repo: source.repo,
185
- branch: source.branch || 'main',
187
+ branch: source.branch || DEFAULT_BRANCH,
186
188
  basePath: source.path
187
189
  }));
188
190
 
@@ -225,7 +227,7 @@ export async function fetchSkillInfo(skillName, owner, repo, path = null, branch
225
227
 
226
228
  // Optimized: Use Raw URL if branch is known
227
229
  if (branch) {
228
- let rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}`;
230
+ let rawUrl = `${GITHUB_RAW}/${owner}/${repo}/${branch}`;
229
231
  if (path) rawUrl += `/${path}`;
230
232
  rawUrl += `/${skillName}/SKILL.md`;
231
233
 
@@ -236,7 +238,9 @@ export async function fetchSkillInfo(skillName, owner, repo, path = null, branch
236
238
  if (res.ok) {
237
239
  content = await res.text();
238
240
  }
239
- } catch (e) {}
241
+ } catch (e) {
242
+ debug('Raw URL fetch failed:', e.message);
243
+ }
240
244
  }
241
245
 
242
246
  // Fallback: Use API
@@ -258,8 +262,10 @@ export async function fetchSkillInfo(skillName, owner, repo, path = null, branch
258
262
  // But simpler just to ignore or log if we strictly check msg
259
263
  try {
260
264
  const d = await response.json();
261
- if (d.message.includes('rate limit')) logRateLimitError();
262
- } catch {}
265
+ if (d.message?.includes('rate limit')) logRateLimitError();
266
+ } catch (e) {
267
+ debug('Rate limit check failed:', e.message);
268
+ }
263
269
  }
264
270
  return null;
265
271
  }
@@ -276,12 +282,12 @@ export async function fetchSkillInfo(skillName, owner, repo, path = null, branch
276
282
 
277
283
  return {
278
284
  description: descMatch ? descMatch[1].trim() : null,
279
- version: versionMatch ? versionMatch[1].trim() : '0.0.0',
285
+ version: versionMatch ? versionMatch[1].trim() : DEFAULT_VERSION,
280
286
  content // Return raw content
281
287
  };
282
288
  }
283
289
 
284
- return { description: null, version: '0.0.0', content };
290
+ return { description: null, version: DEFAULT_VERSION, content };
285
291
  }
286
292
 
287
293
  /**
@@ -1,19 +1,28 @@
1
1
  import { existsSync, readdirSync, readFileSync, rmSync, mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { CONFIG } from '../config.js';
4
+ import { MAX_DIRECTORY_DEPTH, SKILL_MD } from './constants.js';
5
+ import { debug } from './logger.js';
4
6
 
5
7
  /**
6
8
  * Find local skills directory by traversing up from cwd
9
+ * Limited to MAX_DIRECTORY_DEPTH to prevent potential infinite loops
7
10
  */
8
11
  export function findLocalSkillsDir() {
9
12
  let dir = process.cwd();
13
+ let depth = 0;
10
14
 
11
- while (dir !== '/') {
15
+ while (dir !== '/' && depth < MAX_DIRECTORY_DEPTH) {
12
16
  const skillsPath = join(dir, CONFIG.LOCAL_SKILLS_DIR);
13
17
  if (existsSync(skillsPath)) {
14
18
  return skillsPath;
15
19
  }
16
20
  dir = join(dir, '..');
21
+ depth++;
22
+ }
23
+
24
+ if (depth >= MAX_DIRECTORY_DEPTH) {
25
+ debug('Max directory depth reached while searching for skills directory');
17
26
  }
18
27
 
19
28
  return null;
@@ -51,7 +60,7 @@ export function getLocalSkills() {
51
60
  .filter(entry => entry.isDirectory() && !entry.name.startsWith('.'))
52
61
  .map(entry => {
53
62
  const skillPath = join(skillsDir, entry.name);
54
- const skillMdPath = join(skillPath, 'SKILL.md');
63
+ const skillMdPath = join(skillPath, SKILL_MD);
55
64
 
56
65
  let description = null;
57
66
  if (existsSync(skillMdPath)) {
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Simple logging utility with debug support
3
+ * Enable debug logs with DEBUG=antikit environment variable
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+
8
+ const DEBUG = process.env.DEBUG === 'antikit' || process.env.DEBUG === '*';
9
+
10
+ /**
11
+ * Log debug message (only when DEBUG=antikit)
12
+ */
13
+ export function debug(...args) {
14
+ if (DEBUG) {
15
+ console.error(chalk.dim('[debug]'), ...args);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Log info message
21
+ */
22
+ export function info(...args) {
23
+ console.log(chalk.blue('ℹ'), ...args);
24
+ }
25
+
26
+ /**
27
+ * Log warning message
28
+ */
29
+ export function warn(...args) {
30
+ console.warn(chalk.yellow('⚠'), ...args);
31
+ }
32
+
33
+ /**
34
+ * Log error message
35
+ */
36
+ export function error(...args) {
37
+ console.error(chalk.red('✖'), ...args);
38
+ }
39
+
40
+ /**
41
+ * Log success message
42
+ */
43
+ export function success(...args) {
44
+ console.log(chalk.green('✓'), ...args);
45
+ }
46
+
47
+ /**
48
+ * Create a scoped logger
49
+ */
50
+ export function createLogger(scope) {
51
+ const prefix = chalk.dim(`[${scope}]`);
52
+ return {
53
+ debug: (...args) => debug(prefix, ...args),
54
+ info: (...args) => info(prefix, ...args),
55
+ warn: (...args) => warn(prefix, ...args),
56
+ error: (...args) => error(prefix, ...args),
57
+ success: (...args) => success(prefix, ...args)
58
+ };
59
+ }
60
+
61
+ export default { debug, info, warn, error, success, createLogger };
@@ -4,6 +4,8 @@ import { join, dirname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { spawn } from 'child_process';
6
6
  import chalk from 'chalk';
7
+ import { compareVersions } from './version.js';
8
+ import { debug } from './logger.js';
7
9
 
8
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
11
  const CONFIG_DIR = join(homedir(), '.antikit');
@@ -13,17 +15,12 @@ const UPDATE_CHECK_FILE = join(CONFIG_DIR, 'update-check.json');
13
15
  const CHECK_INTERVAL = 1000 * 60 * 60 * 6;
14
16
 
15
17
  /**
16
- * Compare semantic versions
18
+ * Ensure config directory exists
17
19
  */
18
- function compareVersions(v1, v2) {
19
- const parts1 = v1.split('.').map(Number);
20
- const parts2 = v2.split('.').map(Number);
21
-
22
- for (let i = 0; i < 3; i++) {
23
- if (parts1[i] > parts2[i]) return 1;
24
- if (parts1[i] < parts2[i]) return -1;
20
+ function ensureConfigDir() {
21
+ if (!existsSync(CONFIG_DIR)) {
22
+ mkdirSync(CONFIG_DIR, { recursive: true });
25
23
  }
26
- return 0;
27
24
  }
28
25
 
29
26
  /**
@@ -33,7 +30,45 @@ function loadUpdateCache() {
33
30
  try {
34
31
  if (!existsSync(UPDATE_CHECK_FILE)) return null;
35
32
  return JSON.parse(readFileSync(UPDATE_CHECK_FILE, 'utf-8'));
36
- } catch {
33
+ } catch (e) {
34
+ debug('Failed to load update cache:', e.message);
35
+ return null;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Save update cache data
41
+ */
42
+ function saveUpdateCache(data) {
43
+ try {
44
+ ensureConfigDir();
45
+ writeFileSync(UPDATE_CHECK_FILE, JSON.stringify(data, null, 2));
46
+ } catch (e) {
47
+ debug('Failed to save update cache:', e.message);
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Fetch latest version from npm registry
53
+ * @param {string} packageName
54
+ * @returns {Promise<string|null>}
55
+ */
56
+ async function fetchLatestVersion(packageName) {
57
+ try {
58
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
59
+ headers: { Accept: 'application/json' },
60
+ signal: AbortSignal.timeout(5000)
61
+ });
62
+
63
+ if (!response.ok) {
64
+ debug('NPM registry returned non-ok status:', response.status);
65
+ return null;
66
+ }
67
+
68
+ const data = await response.json();
69
+ return data.version || null;
70
+ } catch (e) {
71
+ debug('Failed to fetch latest version:', e.message);
37
72
  return null;
38
73
  }
39
74
  }
@@ -103,8 +138,8 @@ export function checkForUpdates(packageName, currentVersion) {
103
138
  stdio: 'ignore'
104
139
  });
105
140
  child.unref();
106
- } catch {
107
- // Ignore spawn errors
141
+ } catch (e) {
142
+ debug('Failed to spawn update checker:', e.message);
108
143
  }
109
144
  }
110
145
  }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Version utilities for antikit
3
+ * Centralized version comparison and constants
4
+ */
5
+
6
+ export const DEFAULT_VERSION = '0.0.0';
7
+
8
+ /**
9
+ * Compare two semantic versions
10
+ * @param {string} v1 - First version (e.g., '1.2.3')
11
+ * @param {string} v2 - Second version (e.g., '1.2.4')
12
+ * @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
13
+ */
14
+ export function compareVersions(v1, v2) {
15
+ if (!v1 || !v2) return 0;
16
+
17
+ const parts1 = v1.split('.').map(Number);
18
+ const parts2 = v2.split('.').map(Number);
19
+
20
+ for (let i = 0; i < 3; i++) {
21
+ const p1 = parts1[i] || 0;
22
+ const p2 = parts2[i] || 0;
23
+ if (p1 > p2) return 1;
24
+ if (p1 < p2) return -1;
25
+ }
26
+ return 0;
27
+ }
28
+
29
+ /**
30
+ * Check if version string is valid semantic version
31
+ * @param {string} version
32
+ * @returns {boolean}
33
+ */
34
+ export function isValidVersion(version) {
35
+ if (!version || typeof version !== 'string') return false;
36
+ return /^\d+\.\d+\.\d+(-[\w.]+)?(\+[\w.]+)?$/.test(version);
37
+ }
38
+
39
+ /**
40
+ * Parse version from SKILL.md frontmatter content
41
+ * @param {string} content - SKILL.md content
42
+ * @returns {string} version or DEFAULT_VERSION
43
+ */
44
+ export function parseVersionFromContent(content) {
45
+ if (!content) return DEFAULT_VERSION;
46
+ const match = content.match(/^version:\s*(.+)/m);
47
+ return match ? match[1].trim() : DEFAULT_VERSION;
48
+ }