gitea-cli-skill 0.3.1 → 0.4.0

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/assets/SKILL.md CHANGED
@@ -15,7 +15,8 @@ Resolve `gitea-cli` to the platform binary in skill directory `~/.claude/skills/
15
15
  # Linux / macOS
16
16
  SKILL_DIR="$HOME/.claude/skills/gitea-cli"
17
17
  OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
18
- ARCH="$(uname -m)" && [[ "$ARCH" == "x86_64" ]] && ARCH="amd64" && [[ "$ARCH" == "aarch64" || "$ARCH" == "arm64" ]] && ARCH="arm64"
18
+ ARCH="$(uname -m)"
19
+ case "$ARCH" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; esac
19
20
  GITEA_CLI="$SKILL_DIR/scripts/${OS}-${ARCH}/gitea-cli"
20
21
  # Windows: %USERPROFILE%\.claude\skills\gitea-cli\scripts\windows-amd64\gitea-cli.exe
21
22
  ```
@@ -61,7 +62,7 @@ webhook list/get/create/update/delete/test — Webhooks
61
62
 
62
63
  ## Common Workflows
63
64
 
64
- When performing a task, follow the matching workflow. Read `REFERENCE.md` in the same skill directory for full command details and flags.
65
+ When performing a task, follow the matching workflow. Read `references/command-reference.md` in the same skill directory for full command details and flags.
65
66
 
66
67
  **Issue** — `issue list` → `issue get <n>` → `comment create <n>` → `issue close <n>`
67
68
  **PR** — `branch create <name> --from <base>` → `pr create` → `actions run list` → `pr merge <idx>`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitea-cli-skill",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Install gitea-cli as a Claude Code skill",
5
5
  "bin": {
6
6
  "gitea-cli-skill": "src/index.js"
package/src/index.js CHANGED
@@ -4,7 +4,8 @@
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
- const { execSync } = require('child_process');
7
+ const crypto = require('crypto');
8
+ const { execFileSync, execSync } = require('child_process');
8
9
 
9
10
  // ── Config ──────────────────────────────────────────────────────────────────
10
11
 
@@ -62,7 +63,6 @@ function resolveSkillDir(platformName, targetPath) {
62
63
  if (platformName && PLATFORMS[platformName]) {
63
64
  return path.join(os.homedir(), PLATFORMS[platformName].dir, SKILL_NAME);
64
65
  }
65
- // Default: claude
66
66
  return path.join(os.homedir(), PLATFORMS.claude.dir, SKILL_NAME);
67
67
  }
68
68
 
@@ -75,38 +75,34 @@ function findGiteaCliBin(platformName, targetPath) {
75
75
  return path.join(skillDir, 'scripts', `${osArch.os}-${osArch.arch}`, binaryName);
76
76
  }
77
77
 
78
- // ── HTTP via curl (robust cross-platform) ───────────────────────────────────
78
+ // ── HTTP via curl (execFileSync — no shell injection) ────────────────────────
79
79
 
80
- function curlGet(url, token) {
81
- const args = ['-sS', '-f', '-k', '-H', 'User-Agent: gitea-cli-skill-installer'];
82
- if (token) {
83
- args.push('-H', `Authorization: token ${token}`);
84
- }
80
+ function curlGet(url, token, insecure) {
81
+ const args = ['-sS', '-f', '-H', 'User-Agent: gitea-cli-skill-installer'];
82
+ if (insecure) args.push('-k');
83
+ if (token) args.push('-H', `Authorization: token ${token}`);
85
84
  args.push(url);
86
- return execSync(`curl ${args.map((a) => `"${a}"`).join(' ')}`, { encoding: 'utf-8', timeout: 30000 });
85
+ return execFileSync('curl', args, { encoding: 'utf-8', timeout: 30000 });
87
86
  }
88
87
 
89
- function curlDownload(url, destPath, token) {
90
- const args = ['-sS', '-f', '-k', '-L', '-o', destPath, '-H', 'User-Agent: gitea-cli-skill-installer'];
91
- if (token) {
92
- args.push('-H', `Authorization: token ${token}`);
93
- }
88
+ function curlDownload(url, destPath, token, insecure) {
89
+ const args = ['-sS', '-f', '-L', '-o', destPath, '-H', 'User-Agent: gitea-cli-skill-installer'];
90
+ if (insecure) args.push('-k');
91
+ if (token) args.push('-H', `Authorization: token ${token}`);
94
92
  args.push(url);
95
- execSync(`curl ${args.map((a) => `"${a}"`).join(' ')}`, { encoding: 'utf-8', timeout: 120000 });
93
+ execFileSync('curl', args, { encoding: 'utf-8', timeout: 120000 });
96
94
  }
97
95
 
98
96
  // ── Gitea API ────────────────────────────────────────────────────────────────
99
97
 
100
- function getLatestRelease(host, owner, repo, token) {
101
- // Try /latest first
98
+ function getLatestRelease(host, owner, repo, token, insecure) {
102
99
  try {
103
- const data = curlGet(`${host}/api/v1/repos/${owner}/${repo}/releases/latest`, token);
100
+ const data = curlGet(`${host}/api/v1/repos/${owner}/${repo}/releases/latest`, token, insecure);
104
101
  const release = JSON.parse(data);
105
102
  if (release.assets && release.assets.length > 0) return release;
106
103
  } catch { /* fall through */ }
107
104
 
108
- // List all releases, find latest with assets
109
- const data = curlGet(`${host}/api/v1/repos/${owner}/${repo}/releases?limit=10`, token);
105
+ const data = curlGet(`${host}/api/v1/repos/${owner}/${repo}/releases?limit=10`, token, insecure);
110
106
  const releases = JSON.parse(data);
111
107
  for (const r of releases) {
112
108
  if (r.assets && r.assets.length > 0) return r;
@@ -120,14 +116,41 @@ function findAsset(release, osArch) {
120
116
  return release.assets.find((a) => a.name.endsWith(pattern) && !a.name.includes('checksum'));
121
117
  }
122
118
 
119
+ function findChecksumAsset(release) {
120
+ return release.assets.find((a) => a.name.includes('checksums'));
121
+ }
122
+
123
+ // ── Checksum verification ────────────────────────────────────────────────────
124
+
125
+ function verifyChecksum(archivePath, checksumContent, filename) {
126
+ for (const line of checksumContent.split('\n')) {
127
+ const trimmed = line.trim();
128
+ if (!trimmed) continue;
129
+ const parts = trimmed.split(/\s+/);
130
+ if (parts.length < 2) continue;
131
+ const [hash, name] = parts;
132
+ if (name === filename || path.basename(name) === filename) {
133
+ const actual = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex');
134
+ if (actual !== hash) {
135
+ throw new Error(`Checksum mismatch for ${filename}: expected ${hash}, got ${actual}`);
136
+ }
137
+ console.log(' Checksum verified (SHA256).');
138
+ return;
139
+ }
140
+ }
141
+ console.warn(' ⚠ Checksum entry not found for ${filename}, skipping verification.');
142
+ }
143
+
123
144
  // ── Archive extraction ──────────────────────────────────────────────────────
124
145
 
125
146
  function extractArchive(archivePath, destDir, osArch) {
126
147
  fs.mkdirSync(destDir, { recursive: true });
127
148
  if (osArch.os === 'windows') {
128
- execSync(`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, { stdio: 'pipe' });
149
+ execFileSync('powershell', [
150
+ '-Command', 'Expand-Archive', '-Path', archivePath, '-DestinationPath', destDir, '-Force',
151
+ ], { stdio: 'pipe' });
129
152
  } else {
130
- execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'pipe' });
153
+ execFileSync('tar', ['-xzf', archivePath, '-C', destDir], { stdio: 'pipe' });
131
154
  }
132
155
 
133
156
  const binaryName = osArch.os === 'windows' ? 'gitea-cli.exe' : 'gitea-cli';
@@ -147,7 +170,6 @@ function extractArchive(archivePath, destDir, osArch) {
147
170
  function configureContext({ giteaCliBin, host, token, contextName, defaultOwner, defaultRepo }) {
148
171
  const ctxName = contextName || 'default';
149
172
  const cmdArgs = [
150
- `"${giteaCliBin}"`,
151
173
  'config', 'set',
152
174
  '--name', ctxName,
153
175
  '--host', host.replace(/\/$/, ''),
@@ -156,7 +178,11 @@ function configureContext({ giteaCliBin, host, token, contextName, defaultOwner,
156
178
  if (defaultOwner) cmdArgs.push('--default-owner', defaultOwner);
157
179
  if (defaultRepo) cmdArgs.push('--default-repo', defaultRepo);
158
180
 
159
- execSync(cmdArgs.join(' '), { stdio: 'inherit', timeout: 10000 });
181
+ execFileSync(giteaCliBin, cmdArgs, {
182
+ stdio: 'inherit',
183
+ timeout: 10000,
184
+ env: { ...process.env }, // token passed via CLI flag, not env
185
+ });
160
186
  }
161
187
 
162
188
  // ── Version consistency check ────────────────────────────────────────────────
@@ -164,8 +190,7 @@ function configureContext({ giteaCliBin, host, token, contextName, defaultOwner,
164
190
  function checkVersionConsistency(binaryPath) {
165
191
  if (!fs.existsSync(binaryPath)) return;
166
192
  try {
167
- const raw = execSync(`"${binaryPath}" --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
168
- // Parse version from output like "gitea-cli version 0.3.0" or just "0.3.0"
193
+ const raw = execFileSync(binaryPath, ['--version'], { encoding: 'utf-8', timeout: 5000 }).trim();
169
194
  const match = raw.match(/(\d+\.\d+\.\d+)/);
170
195
  if (!match) return;
171
196
  const binVersion = match[1];
@@ -179,7 +204,7 @@ function checkVersionConsistency(binaryPath) {
179
204
  // ── Install command ─────────────────────────────────────────────────────────
180
205
 
181
206
  async function install(args) {
182
- const { token: explicitToken, host: explicitHost, platform: platformName, target: targetPath, local: localDir, force, name: contextName, 'default-owner': defaultOwner, 'default-repo': defaultRepo } = args;
207
+ const { token: explicitToken, host: explicitHost, platform: platformName, target: targetPath, local: localDir, force, name: contextName, 'default-owner': defaultOwner, 'default-repo': defaultRepo, insecure } = args;
183
208
  const giteaHost = explicitHost || GITEA_HOST;
184
209
  const token = explicitToken || process.env.GITEA_TOKEN || null;
185
210
  const osArch = detectOSArch();
@@ -189,26 +214,7 @@ async function install(args) {
189
214
  console.log(`Installing gitea-cli skill (${osArch.os}-${osArch.arch}) to ${skillDir}...`);
190
215
 
191
216
  // 1. Copy SKILL.md and references/
192
- fs.mkdirSync(skillDir, { recursive: true });
193
- const srcSkill = path.resolve(__dirname, '..', 'assets', 'SKILL.md');
194
- const destSkill = path.join(skillDir, 'SKILL.md');
195
- fs.copyFileSync(srcSkill, destSkill);
196
- console.log(` SKILL.md -> ${destSkill}`);
197
-
198
- const srcRef = path.resolve(__dirname, '..', 'assets', 'references', 'command-reference.md');
199
- const destRef = path.join(skillDir, 'references', 'command-reference.md');
200
- if (fs.existsSync(srcRef)) {
201
- fs.mkdirSync(path.dirname(destRef), { recursive: true });
202
- fs.copyFileSync(srcRef, destRef);
203
- console.log(` references/command-reference.md -> ${destRef}`);
204
- }
205
-
206
- // Cleanup: remove legacy REFERENCE.md from old location
207
- const legacyRef = path.join(skillDir, 'REFERENCE.md');
208
- if (fs.existsSync(legacyRef)) {
209
- fs.unlinkSync(legacyRef);
210
- console.log(' Removed legacy REFERENCE.md');
211
- }
217
+ copySkillFiles(skillDir);
212
218
 
213
219
  // 2. Check existing binary
214
220
  const binaryName = osArch.os === 'windows' ? 'gitea-cli.exe' : 'gitea-cli';
@@ -217,11 +223,9 @@ async function install(args) {
217
223
  if (fs.existsSync(destBinary) && !force) {
218
224
  console.log(` Binary already exists: ${destBinary} (use --force to overwrite)`);
219
225
  } else if (localDir) {
220
- // --local: copy from local directory
221
226
  installFromLocal(localDir, scriptsDir, destBinary, osArch);
222
227
  } else {
223
- // Download from Gitea releases
224
- await installFromRelease(giteaHost, token, scriptsDir, destBinary, osArch);
228
+ await installFromRelease(giteaHost, token, scriptsDir, destBinary, osArch, insecure);
225
229
  }
226
230
 
227
231
  // 3. Configure context if token provided
@@ -246,26 +250,48 @@ async function install(args) {
246
250
  console.log(`\nDone!`);
247
251
  }
248
252
 
253
+ // ── Shared: copy SKILL.md + references + cleanup legacy ─────────────────────
254
+
255
+ function copySkillFiles(skillDir) {
256
+ fs.mkdirSync(skillDir, { recursive: true });
257
+
258
+ const srcSkill = path.resolve(__dirname, '..', 'assets', 'SKILL.md');
259
+ const destSkill = path.join(skillDir, 'SKILL.md');
260
+ fs.copyFileSync(srcSkill, destSkill);
261
+ console.log(` SKILL.md -> ${destSkill}`);
262
+
263
+ const srcRef = path.resolve(__dirname, '..', 'assets', 'references', 'command-reference.md');
264
+ const destRef = path.join(skillDir, 'references', 'command-reference.md');
265
+ if (fs.existsSync(srcRef)) {
266
+ fs.mkdirSync(path.dirname(destRef), { recursive: true });
267
+ fs.copyFileSync(srcRef, destRef);
268
+ console.log(` references/command-reference.md -> ${destRef}`);
269
+ }
270
+
271
+ // Cleanup legacy file
272
+ const legacyRef = path.join(skillDir, 'REFERENCE.md');
273
+ if (fs.existsSync(legacyRef)) {
274
+ fs.unlinkSync(legacyRef);
275
+ console.log(' Removed legacy REFERENCE.md');
276
+ }
277
+ }
278
+
249
279
  // ── Install from local goreleaser dist ───────────────────────────────────────
250
280
 
251
281
  function installFromLocal(localDir, scriptsDir, destBinary, osArch) {
252
- // Goreleaser dist structure: dist/gitea-cli_<version>_<os>_<arch>/gitea-cli[.exe]
253
282
  const ext = osArch.os === 'windows' ? 'zip' : 'tar.gz';
254
-
255
283
  const resolvedDir = localDir.replace(/^~/, os.homedir());
256
284
 
257
- // Try to find archive in dist/
258
285
  const files = fs.readdirSync(resolvedDir);
259
286
  const extRe = ext === 'tar.gz' ? 'tar\\.gz' : 'zip';
260
287
  const pattern = new RegExp('gitea-cli_\\d+\\.\\d+\\.\\d+(-\\w+)?_' + osArch.os + '_' + osArch.arch + '\\.' + extRe + '$');
261
288
  const match = files.find((f) => pattern.test(f));
262
289
  if (!match) {
263
- // Try finding extracted binary directly
264
290
  const binaryName = osArch.os === 'windows' ? 'gitea-cli.exe' : 'gitea-cli';
265
291
  const subDirs = files.filter((f) => f.includes(`${osArch.os}_${osArch.arch}`) && fs.statSync(path.join(resolvedDir, f)).isDirectory());
266
292
  for (const d of subDirs) {
267
293
  const candidate = path.join(resolvedDir, d, binaryName);
268
- if (fs.existsSync(candidate)) {
294
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
269
295
  fs.mkdirSync(scriptsDir, { recursive: true });
270
296
  fs.copyFileSync(candidate, destBinary);
271
297
  if (osArch.os !== 'windows') fs.chmodSync(destBinary, 0o755);
@@ -292,12 +318,12 @@ function installFromLocal(localDir, scriptsDir, destBinary, osArch) {
292
318
 
293
319
  // ── Install from Gitea release ───────────────────────────────────────────────
294
320
 
295
- async function installFromRelease(giteaHost, token, scriptsDir, destBinary, osArch) {
321
+ async function installFromRelease(giteaHost, token, scriptsDir, destBinary, osArch, insecure) {
296
322
  console.log(` Fetching latest release from ${giteaHost}...`);
297
323
 
298
324
  let release;
299
325
  try {
300
- release = getLatestRelease(giteaHost, GITEA_OWNER, GITEA_REPO, token);
326
+ release = getLatestRelease(giteaHost, GITEA_OWNER, GITEA_REPO, token, insecure);
301
327
  } catch (err) {
302
328
  if (!token) {
303
329
  console.error(` Failed to fetch release without token: ${err.message}`);
@@ -322,7 +348,21 @@ async function installFromRelease(giteaHost, token, scriptsDir, destBinary, osAr
322
348
  const tmpArchive = path.join(tmpDir, asset.name);
323
349
 
324
350
  try {
325
- curlDownload(asset.browser_download_url, tmpArchive, token);
351
+ curlDownload(asset.browser_download_url, tmpArchive, token, insecure);
352
+
353
+ // Verify checksum if available
354
+ const checksumAsset = findChecksumAsset(release);
355
+ if (checksumAsset) {
356
+ try {
357
+ const tmpChecksum = path.join(tmpDir, checksumAsset.name);
358
+ curlDownload(checksumAsset.browser_download_url, tmpChecksum, token, insecure);
359
+ const checksumContent = fs.readFileSync(tmpChecksum, 'utf-8');
360
+ verifyChecksum(tmpArchive, checksumContent, asset.name);
361
+ } catch (err) {
362
+ console.warn(` ⚠ Checksum verification skipped: ${err.message}`);
363
+ }
364
+ }
365
+
326
366
  console.log(' Extracting...');
327
367
  const binaryPath = extractArchive(tmpArchive, path.join(tmpDir, 'extracted'), osArch);
328
368
  fs.mkdirSync(scriptsDir, { recursive: true });
@@ -363,8 +403,9 @@ async function addContext(args) {
363
403
  // ── Update command ────────────────────────────────────────────────────────────
364
404
 
365
405
  async function update(args) {
366
- const { host: explicitHost, platform: platformName, target: targetPath, local: localDir } = args;
406
+ const { token: explicitToken, host: explicitHost, platform: platformName, target: targetPath, local: localDir, insecure } = args;
367
407
  const giteaHost = explicitHost || GITEA_HOST;
408
+ const token = explicitToken || process.env.GITEA_TOKEN || null;
368
409
  const osArch = detectOSArch();
369
410
  const skillDir = resolveSkillDir(platformName, targetPath);
370
411
  const scriptsDir = path.join(skillDir, 'scripts', `${osArch.os}-${osArch.arch}`);
@@ -372,28 +413,7 @@ async function update(args) {
372
413
  console.log(`Updating gitea-cli skill (${osArch.os}-${osArch.arch}) at ${skillDir}...`);
373
414
 
374
415
  // 1. Refresh SKILL.md and references/
375
- const srcSkill = path.resolve(__dirname, '..', 'assets', 'SKILL.md');
376
- const destSkill = path.join(skillDir, 'SKILL.md');
377
- if (fs.existsSync(srcSkill)) {
378
- fs.mkdirSync(skillDir, { recursive: true });
379
- fs.copyFileSync(srcSkill, destSkill);
380
- console.log(` SKILL.md -> ${destSkill}`);
381
- }
382
-
383
- const srcRef = path.resolve(__dirname, '..', 'assets', 'references', 'command-reference.md');
384
- const destRef = path.join(skillDir, 'references', 'command-reference.md');
385
- if (fs.existsSync(srcRef)) {
386
- fs.mkdirSync(path.dirname(destRef), { recursive: true });
387
- fs.copyFileSync(srcRef, destRef);
388
- console.log(` references/command-reference.md -> ${destRef}`);
389
- }
390
-
391
- // Cleanup: remove legacy REFERENCE.md from old location
392
- const legacyRef = path.join(skillDir, 'REFERENCE.md');
393
- if (fs.existsSync(legacyRef)) {
394
- fs.unlinkSync(legacyRef);
395
- console.log(' Removed legacy REFERENCE.md');
396
- }
416
+ copySkillFiles(skillDir);
397
417
 
398
418
  // 2. Re-download / re-copy binary (always overwrite)
399
419
  const binaryName = osArch.os === 'windows' ? 'gitea-cli.exe' : 'gitea-cli';
@@ -402,8 +422,7 @@ async function update(args) {
402
422
  if (localDir) {
403
423
  installFromLocal(localDir, scriptsDir, destBinary, osArch);
404
424
  } else {
405
- const token = process.env.GITEA_TOKEN || null;
406
- await installFromRelease(giteaHost, token, scriptsDir, destBinary, osArch);
425
+ await installFromRelease(giteaHost, token, scriptsDir, destBinary, osArch, insecure);
407
426
  }
408
427
 
409
428
  // 3. Version consistency check
@@ -415,11 +434,12 @@ async function update(args) {
415
434
  // ── CLI ──────────────────────────────────────────────────────────────────────
416
435
 
417
436
  function parseArgs(argv) {
418
- const args = { token: null, host: null, platform: null, target: null, local: null, force: false, command: null, subcommand: null, name: null, 'default-owner': null, 'default-repo': null };
437
+ const args = { token: null, host: null, platform: null, target: null, local: null, force: false, insecure: false, command: null, subcommand: null, name: null, 'default-owner': null, 'default-repo': null };
419
438
 
420
439
  for (let i = 2; i < argv.length; i++) {
421
440
  const arg = argv[i];
422
- if (arg === '--token' && i + 1 < argv.length) args.token = argv[++i];
441
+ if (arg === '--') break; // end of options
442
+ else if (arg === '--token' && i + 1 < argv.length) args.token = argv[++i];
423
443
  else if (arg === '--host' && i + 1 < argv.length) args.host = argv[++i];
424
444
  else if (arg === '--platform' && i + 1 < argv.length) args.platform = argv[++i];
425
445
  else if (arg === '--target' && i + 1 < argv.length) args.target = argv[++i];
@@ -428,6 +448,8 @@ function parseArgs(argv) {
428
448
  else if (arg === '--default-owner' && i + 1 < argv.length) args['default-owner'] = argv[++i];
429
449
  else if (arg === '--default-repo' && i + 1 < argv.length) args['default-repo'] = argv[++i];
430
450
  else if (arg === '--force' || arg === '-f') args.force = true;
451
+ else if (arg === '--insecure') args.insecure = true;
452
+ else if (arg === '--version' || arg === '-v') { console.log(`gitea-cli-skill v${PKG_VERSION}`); process.exit(0); }
431
453
  else if (arg === '--help' || arg === '-h') { printHelp(); process.exit(0); }
432
454
  else if (!arg.startsWith('-') && !args.command) args.command = arg;
433
455
  else if (!arg.startsWith('-') && args.command && !args.subcommand) args.subcommand = arg;
@@ -440,7 +462,7 @@ function printHelp() {
440
462
  .map(([k, v]) => ` ${k.padEnd(10)} ${v.desc}`)
441
463
  .join('\n');
442
464
 
443
- console.log(`gitea-cli-skill - Install gitea-cli as an AI agent skill
465
+ console.log(`gitea-cli-skill v${PKG_VERSION} - Install gitea-cli as an AI agent skill
444
466
 
445
467
  Usage:
446
468
  npx gitea-cli-skill init [flags] # Install + configure
@@ -464,6 +486,7 @@ Flags (init):
464
486
  --force Overwrite existing binary
465
487
 
466
488
  Flags (update):
489
+ --token <t> Gitea API token (overrides GITEA_TOKEN env var)
467
490
  --host <url> Gitea host URL (default: ${GITEA_HOST})
468
491
  --local <dir> Update from local goreleaser dist directory (skip download)
469
492
  --platform <name> Target AI platform (default: claude)
@@ -479,6 +502,8 @@ Flags (add context):
479
502
 
480
503
  Global:
481
504
  -h, --help Show this help
505
+ -v, --version Show version
506
+ --insecure Skip SSL certificate verification (not recommended)
482
507
 
483
508
  Platforms:
484
509
  ${platformList}
@@ -516,7 +541,6 @@ async function main() {
516
541
  if (!args.command) { printHelp(); process.exit(0); }
517
542
 
518
543
  if (args.command === 'init') {
519
- // Validate platform name
520
544
  if (args.platform && !PLATFORMS[args.platform] && !args.target) {
521
545
  console.error(`Unknown platform: ${args.platform}`);
522
546
  console.error(`Available: ${Object.keys(PLATFORMS).join(', ')}, or use --target for custom path.`);