Setting the file. One moment. Package Skill · Skill Creator · google-gemini/gemini-cli · Skills Docs(opens in a new tab)
scripts/package_skill.cjs
JavaScript·131 lines·4 KB
require
(
'node:path'
);
17const { spawnSync } = require('node:child_process');
18const { validateSkill } = require('./validate_skill.cjs');
19
20async function main() {
21 const args = process.argv.slice(2);
22 if (args.length < 1) {
23 console.log(
24 'Usage: node package_skill.js <path/to/skill-folder> [output-directory]',
25 );
26 process.exit(1);
27 }
28
29 const skillPathArg = args[0];
30 const outputDirArg = args[1];
31
32 if (
33 skillPathArg.includes('..') ||
34 (outputDirArg && outputDirArg.includes('..'))
35 ) {
36 console.error('❌ Error: Path traversal detected in arguments.');
37 process.exit(1);
38 }
39
40 const skillPath = path.resolve(skillPathArg);
41 const outputDir = outputDirArg ? path.resolve(outputDirArg) : process.cwd();
42 const skillName = path.basename(skillPath);
43
44 // 1. Validate first
45 console.log('🔍 Validating skill...');
46 const result = validateSkill(skillPath);
47 if (!result.valid) {
48 console.error(`❌ Validation failed: ${result.message}`);
49 process.exit(1);
50 }
51
52 if (result.warning) {
53 console.warn(`⚠️ ${result.warning}`);
54 console.log('Please resolve all TODOs before packaging.');
55 process.exit(1);
56 }
57 console.log('✅ Skill is valid!');
58
59 // 2. Package
60 const outputFilename = path.join(outputDir, `${skillName}.skill`);
61
62 try {
63 // Zip everything except junk, keeping the folder structure
64 // We'll use the native 'zip' command for simplicity in a CLI environment
65 // or we could use a JS library, but zip is ubiquitous on darwin/linux.
66
67 // Command to zip:
68 // -r: recursive
69 // -x: exclude patterns
70 // Run the zip command from within the directory to avoid parent folder nesting
71 let zipProcess = spawnSync('zip', ['-r', outputFilename, '.'], {
72 cwd: skillPath,
73 stdio: 'inherit',
74 });
75
76 if (zipProcess.error || zipProcess.status !== 0) {
77 if (process.platform === 'win32') {
78 // Fallback to PowerShell Compress-Archive on Windows
79 // Note: Compress-Archive only supports .zip extension, so we zip to .zip and rename
80 console.log('zip command not found, falling back to PowerShell...');
81 const tempZip = outputFilename + '.zip';
82 // Escape single quotes for PowerShell (replace ' with '') and use single quotes for the path
83 const safeTempZip = tempZip.replace(/'/g, "''");
84 zipProcess = spawnSync(
85 'powershell.exe',
86 [
87 '-NoProfile',
88 '-Command',
89 `Compress-Archive -Path .\\* -DestinationPath '${safeTempZip}' -Force`,
90 ],
91 {
92 cwd: skillPath,
93 stdio: 'inherit',
94 },
95 );
96
97 if (zipProcess.status === 0 && require('node:fs').existsSync(tempZip)) {
98 require('node:fs').renameSync(tempZip, outputFilename);
99 }
100 } else {
101 // Fallback to tar on Unix-like systems
102 console.log('zip command not found, falling back to tar...');
103 zipProcess = spawnSync(
104 'tar',
105 ['-a', '-c', '--format=zip', '-f', outputFilename, '.'],
106 {
107 cwd: skillPath,
108 stdio: 'inherit',
109 },
110 );
111 }
112 }
113
114 if (zipProcess.error) {
115 throw zipProcess.error;
116 }
117
118 if (zipProcess.status !== 0) {
119 throw new Error(
120 `Packaging command failed with exit code ${zipProcess.status}`,
121 );
122 }
123
124 console.log(`✅ Successfully packaged skill to: ${outputFilename}`);
125 } catch (err) {
126 console.error(`❌ Error packaging: ${err.message}`);
127 process.exit(1);
128 }
129}
130
131main();