pikakit 1.0.25 → 1.0.27

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.
@@ -12,7 +12,7 @@ import boxen from "boxen";
12
12
  import { parseSkillSpec, merkleHash } from "../helpers.js";
13
13
  import { parseSkillMdFrontmatter } from "../skills.js";
14
14
  import { step, activeStep, stepLine, S, c, fatal, spinner, multiselect, select, confirm, isCancel, cancel } from "../ui.js";
15
- import { WORKSPACE, GLOBAL_DIR, OFFLINE, GLOBAL } from "../config.js";
15
+ import { WORKSPACE, GLOBAL_DIR, OFFLINE, GLOBAL, REPO_CACHE_DIR, CACHE_TTL_MS, FORCE_REFRESH } from "../config.js";
16
16
  import { installSkill } from "../installer.js";
17
17
 
18
18
  /**
@@ -47,31 +47,92 @@ export async function run(spec) {
47
47
  step("Source: " + c.cyan(url));
48
48
 
49
49
  const s = spinner();
50
- s.start("Cloning repository");
51
-
52
50
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "add-skill-"));
53
51
 
54
- // Retry logic with exponential backoff
55
- const MAX_RETRIES = 3;
52
+ // --- Repository Caching Logic ---
53
+ const cacheDir = path.join(REPO_CACHE_DIR, org, repo);
54
+ const cacheMetaFile = path.join(cacheDir, ".cache-meta.json");
55
+ let useCache = false;
56
+ let cacheHit = false;
57
+
58
+ // Check if cache exists and is valid
59
+ if (!FORCE_REFRESH && fs.existsSync(cacheDir) && fs.existsSync(cacheMetaFile)) {
60
+ try {
61
+ const meta = JSON.parse(fs.readFileSync(cacheMetaFile, "utf-8"));
62
+ const cacheAge = Date.now() - new Date(meta.timestamp).getTime();
63
+
64
+ if (cacheAge < CACHE_TTL_MS) {
65
+ useCache = true;
66
+ cacheHit = true;
67
+ s.start("Checking cache...");
68
+ }
69
+ } catch {
70
+ // Invalid cache, will refresh
71
+ }
72
+ }
73
+
56
74
  let lastError = null;
75
+ const MAX_RETRIES = 3;
57
76
 
58
- for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
77
+ if (useCache) {
78
+ // Fast path: Update cache with git fetch (much faster than clone)
59
79
  try {
60
- await execAsync(`git clone --depth=1 ${url} "${tmp}"`, { timeout: 60000 });
61
- if (ref) await execAsync(`git -C "${tmp}" checkout ${ref}`, { timeout: 30000 });
62
- lastError = null;
63
- break;
80
+ s.message("Updating from cache...");
81
+ await execAsync(`git -C "${cacheDir}" fetch --depth=1 origin HEAD`, { timeout: 30000 });
82
+ await execAsync(`git -C "${cacheDir}" reset --hard FETCH_HEAD`, { timeout: 10000 });
83
+
84
+ // Copy from cache to tmp
85
+ await fs.promises.cp(cacheDir, tmp, { recursive: true });
86
+
87
+ // Update cache metadata
88
+ fs.writeFileSync(cacheMetaFile, JSON.stringify({
89
+ timestamp: new Date().toISOString(),
90
+ org, repo, ref: ref || "HEAD"
91
+ }));
92
+
93
+ s.stop("Repository cloned");
64
94
  } catch (err) {
65
- lastError = err;
95
+ // Cache update failed, fall back to fresh clone
96
+ s.message("Cache outdated, cloning fresh...");
97
+ useCache = false;
98
+ cacheHit = false;
99
+ }
100
+ }
66
101
 
67
- if (attempt < MAX_RETRIES) {
68
- const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
69
- s.message(`Retry ${attempt}/${MAX_RETRIES} in ${delay / 1000}s...`);
70
- await new Promise(r => setTimeout(r, delay));
102
+ if (!useCache) {
103
+ // Fresh clone with retry logic
104
+ s.start(FORCE_REFRESH ? "Force refreshing repository..." : "Cloning repository");
71
105
 
72
- // Clean up failed attempt
73
- try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { }
74
- fs.mkdirSync(tmp, { recursive: true });
106
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
107
+ try {
108
+ await execAsync(`git clone --depth=1 ${url} "${tmp}"`, { timeout: 60000 });
109
+ if (ref) await execAsync(`git -C "${tmp}" checkout ${ref}`, { timeout: 30000 });
110
+ lastError = null;
111
+
112
+ // Save to cache for next time
113
+ try {
114
+ fs.mkdirSync(path.dirname(cacheDir), { recursive: true });
115
+ if (fs.existsSync(cacheDir)) fs.rmSync(cacheDir, { recursive: true, force: true });
116
+ await fs.promises.cp(tmp, cacheDir, { recursive: true });
117
+ fs.writeFileSync(path.join(cacheDir, ".cache-meta.json"), JSON.stringify({
118
+ timestamp: new Date().toISOString(),
119
+ org, repo, ref: ref || "HEAD"
120
+ }));
121
+ } catch { /* Cache write failed, non-fatal */ }
122
+
123
+ break;
124
+ } catch (err) {
125
+ lastError = err;
126
+
127
+ if (attempt < MAX_RETRIES) {
128
+ const delay = Math.pow(2, attempt) * 1000; // 2s, 4s
129
+ s.message(`Retry ${attempt}/${MAX_RETRIES} in ${delay / 1000}s...`);
130
+ await new Promise(r => setTimeout(r, delay));
131
+
132
+ // Clean up failed attempt
133
+ try { fs.rmSync(tmp, { recursive: true, force: true }); } catch { }
134
+ fs.mkdirSync(tmp, { recursive: true });
135
+ }
75
136
  }
76
137
  }
77
138
  }
@@ -99,7 +160,9 @@ export async function run(spec) {
99
160
  return;
100
161
  }
101
162
 
102
- s.stop("Repository cloned");
163
+ if (!cacheHit) {
164
+ s.stop("Repository cloned");
165
+ }
103
166
 
104
167
  // Find skills in repo - check multiple possible locations
105
168
  const skillsInRepo = [];
@@ -399,12 +462,13 @@ export async function run(spec) {
399
462
 
400
463
  const installResults = { success: [], failed: [] };
401
464
 
402
- for (const sn of selectedSkills) {
403
- const src = skillPathMap[sn] || path.join(skillsDir || tmp, sn);
404
-
405
- const is = spinner();
406
- is.start(`Installing ${sn} to ${selectedAgents.length} agents`);
465
+ // --- Parallel Skill Installation (Batch processing) ---
466
+ const CONCURRENCY_LIMIT = 5; // Process 5 skills at a time
467
+ const totalSkills = selectedSkills.length;
407
468
 
469
+ // Create installation function
470
+ async function installSingleSkill(sn) {
471
+ const src = skillPathMap[sn] || path.join(skillsDir || tmp, sn);
408
472
  const result = await installSkillForAgents(src, sn, selectedAgents, {
409
473
  method: installMethod,
410
474
  scope: isGlobal ? "global" : "project",
@@ -413,17 +477,30 @@ export async function run(spec) {
413
477
  ref: ref || null
414
478
  }
415
479
  });
480
+ return { skill: sn, ...result };
481
+ }
416
482
 
417
- installResults.success.push(...result.success);
418
- installResults.failed.push(...result.failed);
483
+ // Progress spinner for batch installation
484
+ const batchSpinner = spinner();
485
+ batchSpinner.start(`Installing ${totalSkills} skills to ${selectedAgents.length} agents (parallel)...`);
419
486
 
420
- if (result.failed.length === 0) {
421
- is.stop(`Installed ${sn} (${result.success.length} agents)`);
422
- } else {
423
- is.stop(`${sn}: ${result.success.length} success, ${result.failed.length} failed`);
487
+ // Process in batches
488
+ let completed = 0;
489
+ for (let i = 0; i < selectedSkills.length; i += CONCURRENCY_LIMIT) {
490
+ const batch = selectedSkills.slice(i, i + CONCURRENCY_LIMIT);
491
+ const batchResults = await Promise.all(batch.map(installSingleSkill));
492
+
493
+ for (const r of batchResults) {
494
+ installResults.success.push(...r.success);
495
+ installResults.failed.push(...r.failed);
496
+ completed++;
424
497
  }
498
+
499
+ batchSpinner.message(`Installing skills... ${completed}/${totalSkills}`);
425
500
  }
426
501
 
502
+ batchSpinner.stop(`Installed ${completed} skills (${installResults.success.length} agents, ${installResults.failed.length} failed)`);
503
+
427
504
 
428
505
  // Derive base .agent directory from skillsDir
429
506
  // If skillsDir is .../skills, then baseAgentDir is parent (.agent)
package/bin/lib/config.js CHANGED
@@ -26,6 +26,12 @@ export const REGISTRIES_FILE = path.join(CACHE_ROOT, "registries.json");
26
26
  /** Backup directory */
27
27
  export const BACKUP_DIR = path.join(CACHE_ROOT, "backups");
28
28
 
29
+ /** Repository cache directory for git clone caching */
30
+ export const REPO_CACHE_DIR = path.join(CACHE_ROOT, "repos");
31
+
32
+ /** Cache TTL in milliseconds (24 hours) */
33
+ export const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
34
+
29
35
  // --- Argument Parsing ---
30
36
 
31
37
  const args = process.argv.slice(2);
@@ -68,6 +74,9 @@ export const LOCKED = flags.has("--locked");
68
74
  /** @type {boolean} Offline mode (skip network operations) */
69
75
  export const OFFLINE = flags.has("--offline");
70
76
 
77
+ /** @type {boolean} Force refresh cache (re-clone from remote) */
78
+ export const FORCE_REFRESH = flags.has("--force-refresh") || flags.has("--no-cache");
79
+
71
80
  // --- Package Info ---
72
81
 
73
82
  import { createRequire } from "module";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikakit",
3
- "version": "1.0.25",
3
+ "version": "1.0.27",
4
4
  "description": "Enterprise-grade Agent Skill Manager with Antigravity Skills support, Progressive Disclosure detection, and semantic routing validation",
5
5
  "license": "MIT",
6
6
  "author": "pikakit <pikakit@gmail.com>",