skill-os 0.1.1 → 0.1.2

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": "skill-os",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Skill-OS CLI for managing OS skills",
5
5
  "main": "skill-os.js",
6
6
  "bin": {
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "chalk": "^4.1.2",
21
21
  "commander": "^13.1.0",
22
- "inquirer": "^8.2.6",
22
+ "form-data": "^4.0.5",
23
23
  "js-yaml": "^4.1.0",
24
24
  "jsonschema": "^1.4.1",
25
25
  "node-fetch": "^2.7.0"
package/skill-os.js CHANGED
@@ -5,7 +5,6 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const yaml = require('js-yaml');
7
7
  const chalk = require('chalk');
8
- const inquirer = require('inquirer');
9
8
  const { execSync } = require('child_process');
10
9
 
11
10
  // Define specific layer icons matching original python CLI
@@ -227,76 +226,10 @@ async function cmdInfo(skillPath, options) {
227
226
  console.log();
228
227
  }
229
228
 
230
- function cmdInstall(skillPath, options) {
231
- const index = loadIndex();
232
- const skills = index.skills || {};
233
229
 
234
- // Expand ~ relative to HOME
235
- const homeDir = require('os').homedir();
236
- const rawTarget = options.target.startsWith('~/') ?
237
- path.join(homeDir, options.target.slice(2)) :
238
- path.resolve(options.target);
239
-
240
- const targetDir = path.resolve(rawTarget);
241
-
242
- if (!skills[skillPath]) {
243
- console.error(chalk.red(`Error: Skill '${skillPath}' not found`));
244
- console.log(chalk.dim("Use 'skill-os list' to see available skills"));
245
- process.exit(1);
246
- }
247
-
248
- const info = skills[skillPath];
249
- const repoRoot = getRepoRoot();
250
- const sourceDir = path.join(repoRoot, skillPath);
251
-
252
- if (!fs.existsSync(sourceDir)) {
253
- console.error(chalk.red(`Error: Skill directory not found: ${sourceDir}`));
254
- process.exit(1);
255
- }
256
-
257
- const skillName = path.basename(skillPath);
258
- const installTarget = path.join(targetDir, skillName);
259
-
260
- if (fs.existsSync(installTarget)) {
261
- if (options.force) {
262
- console.log(chalk.yellow(`Removing existing: ${installTarget}`));
263
- fs.rmSync(installTarget, { recursive: true, force: true });
264
- } else {
265
- console.error(chalk.red(`Error: Target already exists: ${installTarget}`));
266
- console.log(chalk.dim("Use --force to overwrite"));
267
- process.exit(1);
268
- }
269
- }
270
-
271
- fs.mkdirSync(targetDir, { recursive: true });
272
-
273
- console.log(`\n${chalk.bold(`📥 Installing ${info.name || skillPath}...`)}`);
274
- console.log(` Source: ${chalk.cyan(sourceDir)}`);
275
- console.log(` Target: ${chalk.cyan(installTarget)}`);
276
-
277
- try {
278
- // Simple recursive copy
279
- fs.cpSync(sourceDir, installTarget, { recursive: true });
280
- console.log(`\n${chalk.green('✓ Successfully installed!')}`);
281
-
282
- console.log(`\n${chalk.bold('📋 Usage:')}`);
283
- console.log(` SKILL.md: ${installTarget}/SKILL.md`);
284
-
285
- const deps = info.dependencies || [];
286
- if (deps.length > 0) {
287
- console.log(`\n${chalk.yellow('⚠️ Dependencies required:')}`);
288
- deps.forEach(dep => console.log(` - ${dep}`));
289
- console.log(`\n Install with: ${chalk.dim(`pip install ${deps.join(' ')}`)}`);
290
- }
291
- } catch (e) {
292
- console.error(chalk.red(`Error during installation: ${e.message}`));
293
- process.exit(1);
294
- }
295
- }
296
230
 
297
231
  function cmdCreate(skillPath, options) {
298
- const repoRoot = getRepoRoot();
299
- const targetDir = path.join(repoRoot, 'skills', skillPath);
232
+ const targetDir = path.resolve(skillPath);
300
233
 
301
234
  if (fs.existsSync(targetDir)) {
302
235
  if (!options.force) {
@@ -425,9 +358,11 @@ function cmdDownload(skillPath, options) {
425
358
  }
426
359
 
427
360
  // ----------------------------------------------------------------------
428
- // YAML / Sync Helpers
361
+ // Publishing / Uploading
429
362
  // ----------------------------------------------------------------------
430
363
 
364
+ const FormData = require('form-data');
365
+
431
366
  function parseSkillMdFrontmatter(skillMdPath) {
432
367
  if (!fs.existsSync(skillMdPath)) return {};
433
368
  const content = fs.readFileSync(skillMdPath, 'utf8');
@@ -441,284 +376,120 @@ function parseSkillMdFrontmatter(skillMdPath) {
441
376
  }
442
377
  }
443
378
 
444
- function loadYamlIndex() {
445
- const repoRoot = getRepoRoot();
446
- const yamlPath = path.join(repoRoot, 'SKILL_INDEX.yaml');
447
- if (!fs.existsSync(yamlPath)) {
448
- console.error(chalk.red('Error: SKILL_INDEX.yaml not found'));
449
- process.exit(1);
450
- }
451
- const raw = fs.readFileSync(yamlPath, 'utf8');
452
- return { data: yaml.load(raw), yamlPath };
453
- }
454
-
455
- function syncToJson(repoRoot) {
456
- const yamlPath = path.join(repoRoot, 'SKILL_INDEX.yaml');
457
- const jsonPath = path.join(repoRoot, 'SKILL_INDEX.json');
458
-
459
- const data = yaml.load(fs.readFileSync(yamlPath, 'utf8'));
460
- data.generated_at = new Date().toISOString();
461
-
462
- fs.writeFileSync(jsonPath, JSON.stringify(data, null, 2), 'utf8');
463
- }
464
-
465
- function appendSkillToYaml(skillPath, skillEntry, yamlPath) {
466
- let content = fs.readFileSync(yamlPath, 'utf8');
467
- const escapedPath = skillPath.replace(/[.*+?^$\\{}()|[\\]\\\\]/g, '\\\\$&');
468
-
469
- // JS Regex to match existing block (simplified approximation matching python's pattern)
470
- // Find " skill/path:" and everything indented under it until the next line that isn't indented by at least 4 spaces
471
- const pattern = new RegExp(`(^|\\n) ${escapedPath}:\\n(?: .*\\n)*`);
472
-
473
- let newBlock = `
474
- ${skillPath}:
475
- name: ${skillEntry.name}
476
- version: "${skillEntry.version}"
477
- description: ${skillEntry.description}
478
- status: ${skillEntry.status}
479
- layer: ${skillEntry.layer}
480
- lifecycle: ${skillEntry.lifecycle}
481
- category: ${skillEntry.category}
482
- tags:
483
- `;
484
- const tags = skillEntry.tags || [];
485
- if (tags.length) {
486
- tags.forEach(t => newBlock += ` - ${t}\n`);
487
- } else {
488
- newBlock += ` tags: []\n`;
489
- }
490
-
491
- const deps = skillEntry.dependencies || [];
492
- if (deps.length) {
493
- newBlock += ` dependencies:\n`;
494
- deps.forEach(d => newBlock += ` - ${d}\n`);
495
- } else {
496
- newBlock += ` dependencies: []\n`;
497
- }
498
-
499
- if (pattern.test(content)) {
500
- content = content.replace(pattern, `$1${newBlock.trimStart()}`);
501
- } else {
502
- content = content.trimEnd() + '\n' + newBlock;
503
- }
504
-
505
- fs.writeFileSync(yamlPath, content, 'utf8');
506
- }
379
+ async function cmdUpload(skillPath, options) {
380
+ const skillDir = path.resolve(skillPath);
381
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
507
382
 
508
- async function cmdSync(skillPath) {
509
- const repoRoot = getRepoRoot();
510
- const skillDir = path.join(repoRoot, 'skills', skillPath);
383
+ console.log(`\n${chalk.bold(`🚀 Publishing Skill: ${skillPath}`)}`);
384
+ console.log(`${chalk.dim('─'.repeat(50))}\n`);
511
385
 
512
- if (!fs.existsSync(skillDir)) {
513
- console.error(chalk.red(`Error: Skill directory not found: ${skillDir}`));
514
- console.log(chalk.dim(`Create it first with: skill-os create ${skillPath}`));
386
+ // 1. Validation (Requires hoisted functions)
387
+ const { isValid: isDirValid, errors: dirErrors } = validateDirectoryStructure(skillDir);
388
+ if (!isDirValid) {
389
+ console.error(chalk.red('✗ Validation failed: Directory structure errors:'));
390
+ dirErrors.forEach(err => console.error(` - ${err}`));
515
391
  process.exit(1);
516
392
  }
517
393
 
518
- const skillMdPath = path.join(skillDir, 'SKILL.md');
519
394
  if (!fs.existsSync(skillMdPath)) {
520
- console.error(chalk.red(`Error: SKILL.md not found in ${skillDir}`));
395
+ console.log(`${chalk.red('✗ Fatal: SKILL.md not found')}`);
521
396
  process.exit(1);
522
397
  }
523
398
 
524
- console.log(`\n${chalk.bold('📝 Skill-OS Sync')}`);
525
- console.log(chalk.dim('─'.repeat(50)));
526
- console.log(`Skill path: ${chalk.cyan(skillPath)}\n`);
527
-
528
399
  const fm = parseSkillMdFrontmatter(skillMdPath);
529
- const { data: indexData, yamlPath } = loadYamlIndex();
530
- const skills = indexData.skills || {};
531
- const existing = skills[skillPath] || {};
532
- const isUpdate = !!existing.name;
533
-
534
- if (isUpdate) {
535
- console.log(`${chalk.yellow('⚠ Skill already registered, updating...')}\n`);
536
- } else {
537
- console.log(`${chalk.green('✨ Registering new skill...')}\n`);
400
+ if (!fm || Object.keys(fm).length === 0) {
401
+ console.log(`${chalk.red('✗ SKILL.md missing or invalid frontmatter')}`);
402
+ process.exit(1);
538
403
  }
539
404
 
540
- console.log(`${chalk.bold('Please confirm or enter skill metadata:')}\n`);
541
-
542
- const pathParts = skillPath.split('/');
543
- const inferredCategory = pathParts.length >= 2 ? pathParts[1] : '';
544
-
545
- const questions = [
546
- { name: 'name', message: ' name', default: existing.name || fm.name || pathParts[pathParts.length - 1] },
547
- { name: 'description', message: ' description', default: existing.description || fm.description || '' },
548
- { name: 'version', message: ' version', default: existing.version || fm.version || '1.0.0' },
549
- { name: 'status', message: ` status ${chalk.dim('(stable, beta, placeholder)')}`, default: existing.status || fm.status || 'beta' },
550
- { name: 'dependencies', message: ` dependencies ${chalk.dim('(comma-separated)')}`, default: (existing.dependencies || fm.dependencies || []).join(', ') }
551
- ];
552
-
553
- const answers = await inquirer.prompt(questions);
554
-
555
- const depsArr = answers.dependencies.split(',').map(d => d.trim()).filter(Boolean);
556
- const category = existing.category || fm.category || inferredCategory;
557
- let tags = existing.tags || fm.tags || [];
558
-
559
- if (category && (!tags.length || tags[0] !== category)) {
560
- tags = [category, ...tags.filter(t => t !== category)];
405
+ const v = new Validator();
406
+ const result = v.validate(fm, skillSchema);
407
+ if (fm.category && fm.tags && fm.tags.length > 0 && fm.tags[0] !== fm.category) {
408
+ result.errors.push({ stack: `tags[0] must equal category name '${fm.category}'` });
561
409
  }
562
410
 
563
- const skillEntry = {
564
- name: answers.name,
565
- version: answers.version,
566
- description: answers.description,
567
- status: answers.status,
568
- layer: existing.layer || fm.layer || pathParts[0],
569
- lifecycle: existing.lifecycle || fm.lifecycle || 'usage',
570
- category: category,
571
- tags: tags,
572
- dependencies: depsArr
573
- };
574
-
575
- console.log(`\n${chalk.bold('📋 Summary:')}`);
576
- console.log(` ${chalk.cyan(skillPath)}:`);
577
- for (const [k, v] of Object.entries(skillEntry)) {
578
- console.log(` ${k}: ${Array.isArray(v) ? (v.length ? JSON.stringify(v) : '[]') : v}`);
411
+ if (!result.valid || result.errors.length > 0) {
412
+ console.error(chalk.red('✗ Validation failed: SKILL.md has errors:'));
413
+ result.errors.forEach(err => console.error(` - ${err.stack.replace('instance.', '')}`));
414
+ process.exit(1);
579
415
  }
580
- console.log();
581
-
582
- const { confirm } = await inquirer.prompt([{
583
- type: 'confirm',
584
- name: 'confirm',
585
- message: 'Confirm and save?',
586
- default: true
587
- }]);
588
416
 
589
- if (!confirm) {
590
- console.log(chalk.yellow('Cancelled'));
591
- process.exit(0);
592
- }
417
+ console.log(chalk.green('✓ Local validation passed.'));
593
418
 
594
- console.log(`\n${chalk.dim('Saving SKILL_INDEX.yaml...')}`);
595
- appendSkillToYaml(skillPath, skillEntry, yamlPath);
419
+ // 2. Prepare tarball
420
+ const tarFilename = `${fm.name}-${fm.version}.tar.gz`;
421
+ const tmpDir = require('os').tmpdir();
422
+ const tarPath = path.join(tmpDir, tarFilename);
596
423
 
597
- console.log(chalk.dim('Syncing to SKILL_INDEX.json...'));
598
- syncToJson(repoRoot);
424
+ console.log(chalk.dim(`📦 Creating package archive...`));
425
+ try {
426
+ // -C changes to directory, . archives contents
427
+ // COPYFILE_DISABLE=1 prevents macOS tar from including ._* extended attribute files
428
+ execSync(`tar -czf "${tarPath}" -C "${skillDir}" .`, {
429
+ env: { ...process.env, COPYFILE_DISABLE: '1' }
430
+ });
431
+ } catch (e) {
432
+ console.error(chalk.red('✗ Failed to create tar archive.'));
433
+ process.exit(1);
434
+ }
599
435
 
600
- console.log(`\n${chalk.green(`✅ Skill ${isUpdate ? 'updated' : 'registered'} successfully!`)}`);
601
- console.log(`\n${chalk.bold('📋 Next steps:')}`);
602
- console.log(" git add SKILL_INDEX.yaml SKILL_INDEX.json");
603
- console.log(` git commit -m 'feat: ${isUpdate ? 'update' : 'add'} ${skillPath} skill'`);
604
- }
436
+ // 3. Prepare Form Data
437
+ const form = new FormData();
438
+ form.append('package', fs.createReadStream(tarPath), {
439
+ filename: tarFilename,
440
+ contentType: 'application/gzip'
441
+ });
442
+ form.append('metadata', JSON.stringify(fm));
605
443
 
606
- function findSkillMds(dir, fileList = []) {
607
- const files = fs.readdirSync(dir);
608
- for (const file of files) {
609
- const stat = fs.statSync(path.join(dir, file));
610
- if (stat.isDirectory()) {
611
- findSkillMds(path.join(dir, file), fileList);
612
- } else if (file === 'SKILL.md') {
613
- fileList.push(path.join(dir, file));
614
- }
444
+ if (options.update) {
445
+ form.append('is_update', 'true');
446
+ console.log(chalk.yellow(`⚠ Uploading as an UPDATE (Version: ${fm.version}). Ensure the version number has been bumped!`));
447
+ } else {
448
+ console.log(chalk.cyan(`✨ Uploading as a NEW skill (Version: ${fm.version}).`));
615
449
  }
616
- return fileList;
617
- }
618
450
 
619
- function cmdSyncAll(options) {
620
- const repoRoot = getRepoRoot();
621
- const skillsDir = path.join(repoRoot, 'skills');
451
+ const serverUrl = getApiBase(options);
452
+ const uploadUrl = `${serverUrl}/skills/upload`;
622
453
 
623
- console.log(`\n${chalk.bold('🔄 Skill-OS Sync All')}`);
624
- console.log(chalk.dim(''.repeat(50)));
625
- console.log(`Scanning: ${chalk.cyan(skillsDir)}\n`);
454
+ const fetchOptions = {
455
+ method: 'POST',
456
+ body: form,
457
+ headers: form.getHeaders()
458
+ };
626
459
 
627
- const skillMds = findSkillMds(skillsDir);
628
- if (skillMds.length === 0) {
629
- console.log(chalk.yellow('No SKILL.md files found'));
630
- return;
460
+ if (options.token) {
461
+ fetchOptions.headers['Authorization'] = `Bearer ${options.token}`;
631
462
  }
632
463
 
633
- console.log(`Found ${chalk.green(skillMds.length)} skills\n`);
634
-
635
- const newSkills = {};
636
- const errors = [];
464
+ console.log(chalk.dim(`\n📡 Uploading to ${uploadUrl}...`));
637
465
 
638
- skillMds.sort().forEach(skillMd => {
639
- const relPath = path.relative(skillsDir, path.dirname(skillMd));
640
- const fm = parseSkillMdFrontmatter(skillMd);
466
+ let fetchFn = require('node-fetch');
467
+ if (typeof fetchFn !== 'function' && fetchFn.default) {
468
+ fetchFn = fetchFn.default;
469
+ }
641
470
 
642
- if (!fm || Object.keys(fm).length === 0) {
643
- errors.push(` ⚠ ${relPath}: Failed to parse frontmatter`);
644
- return;
471
+ try {
472
+ const response = await fetchFn(uploadUrl, fetchOptions);
473
+ if (!response.ok) {
474
+ let errorMsg = response.statusText;
475
+ try {
476
+ const errBody = await response.json();
477
+ if (errBody.error || errBody.message) errorMsg = errBody.error || errBody.message;
478
+ } catch (e) { }
479
+ throw new Error(`HTTP ${response.status}: ${errorMsg}`);
645
480
  }
646
-
647
- let status = fm.status || 'beta';
648
- let icon = '✓';
649
- if (status === 'placeholder') {
650
- if (!options.includePlaceholder) {
651
- status = 'beta'; // Default placeholder -> beta
652
- }
653
- icon = '🔸';
481
+ console.log(`\n${chalk.green('✅ Publish successful!')}`);
482
+ } catch (error) {
483
+ console.error(chalk.red(`\n✗ Publish failed.`));
484
+ console.error(chalk.red(` ${error.message}`));
485
+ if (!options.update && error.message.includes('already exists')) {
486
+ console.log(chalk.yellow(`\n💡 If you intended to update an existing skill, use the --update flag.`));
654
487
  }
655
-
656
- const name = fm.name || path.basename(path.dirname(skillMd));
657
- const layer = fm.layer || relPath.split('/')[0];
658
- const category = fm.category || (relPath.includes('/') ? relPath.split('/')[1] : '');
659
- let tags = fm.tags || [];
660
-
661
- if (tags && category && tags[0] !== category) {
662
- tags = [category, ...tags.filter(t => t !== category)];
488
+ process.exit(1);
489
+ } finally {
490
+ if (fs.existsSync(tarPath)) {
491
+ fs.unlinkSync(tarPath);
663
492
  }
664
-
665
- let description = fm.description || '';
666
- if (description.length > 200) description = description.substring(0, 200);
667
-
668
- newSkills[relPath] = {
669
- name,
670
- version: fm.version || '0.1.0',
671
- description,
672
- status,
673
- layer,
674
- lifecycle: fm.lifecycle || 'usage',
675
- category,
676
- tags: tags.slice(0, 5),
677
- dependencies: fm.dependencies || []
678
- };
679
-
680
- console.log(` ${icon} ${chalk.cyan(relPath)} (${layer}/${newSkills[relPath].lifecycle})`);
681
- });
682
-
683
- if (errors.length) {
684
- console.log(`\n${chalk.yellow('Warnings:')}`);
685
- errors.forEach(e => console.log(e));
686
- }
687
-
688
- const { data: indexData, yamlPath } = loadYamlIndex();
689
- indexData.skills = newSkills;
690
- indexData.version = "3.0";
691
-
692
- const yamlDump = yaml.dump(indexData, {
693
- sortKeys: false,
694
- noCompatMode: true
695
- });
696
-
697
- const header = `# Skill-OS 技能索引
698
- # ====================
699
- # Generated: ${new Date().toISOString()}
700
- # Layers: core | system | runtime | application | meta
701
- # Lifecycle: production | maintenance | operations | usage | meta
702
-
703
- `;
704
-
705
- fs.writeFileSync(yamlPath, header + yamlDump, 'utf8');
706
- console.log(`\n${chalk.green(`✓ Updated ${path.basename(yamlPath)}`)}`);
707
-
708
- syncToJson(repoRoot);
709
- console.log(`${chalk.green(`✓ Updated SKILL_INDEX.json`)}`);
710
-
711
- console.log(`\n${chalk.bold('📊 Summary:')}`);
712
- console.log(` Total skills: ${Object.keys(newSkills).length}`);
713
-
714
- const layerCounts = {};
715
- for (const info of Object.values(newSkills)) {
716
- const layer = info.layer || 'unknown';
717
- layerCounts[layer] = (layerCounts[layer] || 0) + 1;
718
- }
719
-
720
- for (const layer of Object.keys(layerCounts).sort()) {
721
- console.log(` ${formatLayerIcon(layer + '/')} ${layer}: ${layerCounts[layer]}`);
722
493
  }
723
494
  }
724
495
 
@@ -772,8 +543,7 @@ function validateDirectoryStructure(skillDir) {
772
543
  }
773
544
 
774
545
  function cmdValidate(skillPath) {
775
- const repoRoot = getRepoRoot();
776
- const skillDir = path.join(repoRoot, 'skills', skillPath);
546
+ const skillDir = path.resolve(skillPath);
777
547
  const skillMdPath = path.join(skillDir, 'SKILL.md');
778
548
 
779
549
  console.log(`\n${chalk.bold(`🔍 Validating Skill: ${skillPath}`)}`);
@@ -856,13 +626,6 @@ program.command('info')
856
626
  .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
857
627
  .action(cmdInfo);
858
628
 
859
- program.command('install')
860
- .description('Install a skill to target directory locally from repository')
861
- .argument('<path>', 'Skill path to install')
862
- .option('-t, --target <dir>', 'Target directory', '~/.skills')
863
- .option('-f, --force', 'Force overwrite if exists', false)
864
- .action(cmdInstall);
865
-
866
629
  program.command('create')
867
630
  .description('Create a new skill scaffold locally')
868
631
  .argument('<path>', 'Skill path to create (e.g., system/migration)')
@@ -877,15 +640,14 @@ program.command('download')
877
640
  .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
878
641
  .action(cmdDownload);
879
642
 
880
- program.command('sync')
881
- .description('Register or update a skill in the index with interactive prompts')
882
- .argument('<path>', 'Skill path to sync (e.g., cloud/alicloud-api)')
883
- .action(cmdSync);
884
-
885
- program.command('sync-all')
886
- .description('Regenerate index from all SKILL.md files (one-click sync)')
887
- .option('--include-placeholder', 'Include placeholder skills in output', false)
888
- .action(cmdSyncAll);
643
+ program.command('upload')
644
+ .alias('publish')
645
+ .description('Upload a local skill to the remote registry')
646
+ .argument('<path>', 'Skill path to upload (e.g., system/migration)')
647
+ .option('-u, --update', 'Publish as an update to an existing skill', false)
648
+ .option('--token <token>', 'Authentication token for the remote registry')
649
+ .option('--url <url>', 'Override the base URL for the registry', DEFAULT_API_BASE)
650
+ .action(cmdUpload);
889
651
 
890
652
  program.command('validate')
891
653
  .description('Validate a skill against spec')
@@ -1,97 +0,0 @@
1
- ---
2
- name: rpm-find
3
- version: 1.0.0
4
- description: 专业的 Alinux 系统 RPM 软件包搜索工具。支持解析复杂的包名查询,根据用户网络环境(内网/公网)智能提供有效下载链接。当用户请求查找、下载或查询 RPM 软件包时使用此技能。
5
-
6
-
7
- layer: runtime
8
- category: package
9
- lifecycle: usage
10
-
11
- # Tags: first tag MUST be the category name
12
- tags:
13
- - package
14
- - rpm
15
- - alinux
16
- - search
17
-
18
- status: stable
19
- dependencies:
20
- - httpx>=0.24.0
21
- - requests>=2.28.0
22
- ---
23
-
24
- # RPM 软件包搜索技能
25
-
26
- 当用户请求查找、下载或查询 Alinux 系统的 RPM 软件包时使用此技能。
27
-
28
- ## 能力概览
29
-
30
- 此技能能够解析模糊的包名(如 "nginx", "kernel 5.10"),并调用工具搜索实际的下载地址。它能根据用户的身份和网络环境,动态调整搜索策略和回复语气。
31
-
32
- ## 核心流程
33
-
34
- ### 步骤 1: 知识库加载 (必选)
35
-
36
- 在开始任何搜索之前,必须使用 `read_file` 工具读取以下文档以了解 Alinux 的内核与版本命名规则:
37
-
38
- - [Alinux 内核与版本识别](./references/kernel_kb.md)
39
-
40
- ### 步骤 2: 意图识别与扩展 (可选分支)
41
-
42
- 在进行常规搜索前,请检查用户的具体意图。如果用户明确咨询 **"debuginfo"**, **"崩溃调试"**, **"内核调试"**, 或 **"debug 仓库"** 相关问题:
43
-
44
- - **加载**: [Alinux Debuginfo 包查找指南](./references/debuginfo_guide.md)
45
- - **执行**: 根据该指南中的 "标准用法" 或 "URL拼接" 规则回答用户的问题。
46
-
47
- ### 步骤 3: 环境判断与 SOP 加载 (分支)
48
-
49
- 请根据用户的 `source` 字段或当前对话上下文判断用户环境,并加载对应的 SOP 文档:
50
-
51
- - **情形 A: 内部开发环境** (例如 `source` 为 `cardbot_in` 或提到 "内部源")
52
- - 加载: [内网 RPM 搜索 SOP](./references/inner_rules.md)
53
- - 执行重点: 优先使用内网源,不过滤闭源包,回复简洁。
54
-
55
- - **情形 B: 外部公网环境** (默认情况,或 `source` 为 `other`)
56
- - 加载: [公网 RPM 搜索 SOP](./references/outer_rules.md)
57
- - 执行重点: **严禁泄露内网 IP**,必须转换为 `mirrors.aliyun.com`,语气亲切。
58
-
59
- ### 步骤 4: 工具调用 (CLI 模式)
60
-
61
- 使用 `execute_shell` 运行 Python 脚本执行操作。
62
-
63
- **脚本路径**: `./scripts/rpm_tool.py`
64
-
65
- #### 4.1 解析查询
66
-
67
- ```bash
68
- uv run ./scripts/rpm_tool.py parse "kernel 5.10"
69
- ```
70
-
71
- #### 4.2 搜索 RPM (API)
72
-
73
- 将解析结果 (JSON) 中的字段传入:
74
-
75
- ```bash
76
- uv run ./scripts/rpm_tool.py search "kernel" --version "5.10.134" --release "13.al8" --arch "x86_64"
77
- ```
78
-
79
- #### 4.3 验证 URL
80
-
81
- 将找到的 URL 列表传入验证:
82
-
83
- ```bash
84
- uv run ./scripts/rpm_tool.py verify http://url1 http://url2 ...
85
- ```
86
-
87
- #### 4.4 备用: URL 拼接 (仅当 API 无结果时)
88
-
89
- ```bash
90
- uv run ./scripts/rpm_tool.py construct "kernel-debuginfo" "5.10.134" "13.al8" "x86_64" --is_kernel --alinux_version "3"
91
- ```
92
-
93
- ## 错误处理
94
-
95
- - 如果 API 返回空结果,尝试使用 `construct` 命令拼接 URL
96
- - 如果所有 URL 验证失败,建议用户检查包名或版本号是否正确
97
- - 对于闭源包(含 `ali3000`, `ali4000` 等标识),提示用户联系 OS 团队
@@ -1,12 +0,0 @@
1
- [project]
2
- name = "package/rpm_search-skill"
3
- version = "1.0.0"
4
- description = "RPM package search skill for Alinux systems"
5
- requires-python = ">=3.9"
6
- dependencies = [
7
- "httpx>=0.24",
8
- "requests>=2.28",
9
- ]
10
-
11
- [project.scripts]
12
- rpm-tool = "scripts.rpm_tool:main"
@@ -1,29 +0,0 @@
1
- # Alinux Debuginfo 包查找指南
2
-
3
- ## 标准用法 (YUM/DNF)
4
- Alinux 系统找包的标准用法是通过 YUM/DNF 工具。对于 debuginfo 包,需要使用 `yum-utils` 提供的 `debuginfo-install` 命令,因为 debug 仓库默认未开启。
5
-
6
- ### 常用命令
7
- - **安装常规包**: `debuginfo-install <pkg>` (例如 `debuginfo-install tree`)
8
- - **安装内核包**: `debuginfo-install kernel-debuginfo` (必须带后缀,否则安装不上)
9
- - **仅下载不安装**: `debuginfo-install --downloadonly kernel-debuginfo-$(uname -r)`
10
- - 下载位置: `/var/cache/dnf/`
11
-
12
- ## URL 拼接用法 (不推荐但可用)
13
- 仅在不得不手动下载时使用。**注意**: 仅适用于公网镜像 `mirrors.aliyun.com`。闭源组件 (`ali3000`等) 不适用此法。
14
-
15
- ### 拼接规则
16
- 1. **架构推断**:
17
- - `.al7` -> Alinux 2
18
- - `.al8` -> Alinux 3
19
- - `.alnx4` -> Alinux 4
20
- 2. **内核包 URL**:
21
- - **Alinux 2/3**: `<dist>/plus/<arch>/debug/kernels/`
22
- - **Alinux 4**: `<dist>/os/<arch>/debug/` 或 `<dist>/updates/<arch>/debug/`
23
- - 文件名: `kernel-debuginfo-<version>-<release>.rpm` 和 `kernel-debuginfo-common-<arch>-<version>-<release>.rpm`
24
- 3. **非内核包 URL**:
25
- - 路径: `<dist>/<repo>/<arch>/debug/<pkg>-debuginfo-<version>-<release>.rpm`
26
-
27
- ### 注意事项
28
- - 如果需要查找 debuginfo,若是内核包,需同时提供 `kernel-debuginfo` 和 `kernel-debuginfo-common`。
29
- - 若闭源包无 debuginfo,需联系 OS 团队。
@@ -1,17 +0,0 @@
1
- # 内网 RPM 搜索 SOP (Inner)
2
-
3
- **适用场景**: 用户身份为内部员工 (`source: cardbot_in`) 或检测到内网环境。
4
-
5
- ## 核心准则
6
- 你是一名顶级的 Alinux 系统 RPM 包管理专家智能体。
7
- 1. **提供有效链接**: 必须提供 `yum.tbsite.net` 等内网源链接。
8
- 2. **验证**: 所有链接必须先通过 `check_url_availability_batch` 验证。
9
- 3. **语气**: 专业、直接、技术导向。
10
-
11
- ## 执行步骤
12
- 1. **解析**: 调用 `parse_user_query_to_rpm_info` 解析包名。
13
- 2. **查询**: 调用 `get_rpm_list` 获取内网链接。
14
- 3. **闭源包处理**:
15
- - 如果发现 `apsara` 或 `aliXXXX` 等闭源包,**可以**显示,并标记为“内部闭源”。
16
- 4. **验证**: 并行验证所有 URL。
17
- 5. **输出**: 直接列出 Markdown 格式的下载链接。
@@ -1,16 +0,0 @@
1
- # Alinux 内核与版本识别知识库
2
-
3
- 本参考文档包含识别 Alinux 内核及对应系统版本的核心规则。
4
-
5
- ## 内核识别规则
6
- - **内核判断**: 版本号以 5.10, 6.6, 4.19, 4.9 等主线版本开头,或包含 `ali3000`, `ali4000`, `ali5000`, `ali6000`, `kernel`, `kmod` 字样,判定为内核。
7
- - 注意:`ali5000`, `ali6000` 是内核标识符,而不是软件包名称。
8
- - **内核包名**: 内核对应 `kernel`, 包名一般为 `kernel`, `kernel-devel`, `kernel-core`, `kernel-modules` 等。
9
- - **闭源内核**: 包含 `ali3000`, `ali4000`, `ali5000`, `ali6000` 等字样, 或API返回的仓库路径包含 `apsara-xxxx` 等特殊标识, 代表内部闭源包。
10
-
11
- ## 系统版本对应表
12
- | 标识符 | Alinux 版本 | 备注 |
13
- | :--- | :--- | :--- |
14
- | `.al7` | Alinux 2 | |
15
- | `.al8` | Alinux 3 | 内核主版本号 5.10 也对应 Alinux 3 |
16
- | `.alnx4`| Alinux 4 | 内核主版本号 6.6 也对应 Alinux 4 |
@@ -1,20 +0,0 @@
1
- # 公网 RPM 搜索 SOP (Outer)
2
-
3
- **适用场景**: 用户身份为外部客户 (`source: other`) 或公网环境。
4
-
5
- ## 核心准则
6
- 你是一名服务于外部客户的 Alinux 系统专家。
7
- 1. **只提供公网链接**: *绝对禁止* 泄露 `yum.tbsite.net` 等内网链接。必须转换为 `mirrors.aliyun.com`。
8
- 2. **验证**: 所有链接必须通过 `check_url_availability_batch` 验证。
9
- 3. **语气**: 亲切、耐心、解释性强。
10
-
11
- ## 执行步骤
12
- 1. **解析**: 调用 `parse_user_query_to_rpm_info`。
13
- 2. **查询**: 调用 `get_rpm_list`。
14
- 3. **安全过滤 (关键)**:
15
- - 检查是否为闭源包 (`apsara`, `aliXXXX`)。若是,**绝对不能**提供链接,告知用户“需联系售后获取”。
16
- - 将所有内网 URL (`http://yum.tbsite.net/...`) 替换为公网镜像 (`https://mirrors.aliyun.com/...`)。
17
- 4. **兜底**: 如果转换失败,调用 `construct_url` 尝试拼接。
18
- 5. **输出**:
19
- - 使用表格形式展示结果。
20
- - 对每个步骤做简单解释,帮助客户理解。
@@ -1,417 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- RPM Search Tool Script
4
- 此脚本封装了 RPM 搜索相关的核心功能,供 AI Agent 通过命令行调用。
5
- """
6
-
7
- import argparse
8
- import asyncio
9
- import json
10
- import logging
11
- import re
12
-
13
- from typing import Any
14
-
15
- import httpx
16
- import requests
17
-
18
-
19
- # 设置日志
20
- logging.basicConfig(level=logging.ERROR, format="%(message)s")
21
- logger = logging.getLogger(__name__)
22
-
23
- # --- Core Functions from example_agent.py ---
24
-
25
-
26
- def fetch_webpage_content(
27
- url: str, params: dict | None = None, timeout: int = 10
28
- ) -> str:
29
- """获取url返回的数据"""
30
- try:
31
- response = requests.get(url=url, params=params, timeout=timeout)
32
- response.raise_for_status()
33
- return response.text
34
- except requests.exceptions.RequestException as e:
35
- logger.error(f"网络请求失败: {e}")
36
- return ""
37
-
38
-
39
- def parse_user_query_to_rpm_info(query: str) -> list[dict[str, Any]]:
40
- query = query.replace("内核", " kernel ")
41
- cleaned_query = re.sub(r"[\u4e00-\u9fa5]", " ", query)
42
- work_str = " ".join(cleaned_query.split())
43
- package_queries = re.findall(
44
- r"[\w\.~-]*?(?:\d{1,2}\.\d{1,2}|al[78]|alnx4|x86_64|aarch64)[\w\.~-]*",
45
- work_str,
46
- )
47
- if not package_queries:
48
- package_queries = [work_str]
49
- results = []
50
- for part in package_queries:
51
- if not part or len(part) < 5:
52
- continue
53
- info = {
54
- "name": "",
55
- "version": "",
56
- "release": "",
57
- "arch": "",
58
- "type": "standard",
59
- "is_kernel": False,
60
- "alinux_version": "unknown",
61
- "original_text": part,
62
- }
63
- arch_match = re.search(r"\b(x86_64|aarch64)\b", part)
64
- if arch_match:
65
- info["arch"] = arch_match.group(1)
66
- part = part.replace(arch_match.group(0), "").strip()
67
- dist_match = re.search(r"[\.-](al[78]|alnx4)", part)
68
- if dist_match:
69
- info["alinux_version"] = {"al7": "2", "al8": "3", "alnx4": "4"}.get(
70
- dist_match.group(1)
71
- )
72
- part = part[: dist_match.start()]
73
- ver_match = re.search(r"(\d{1,2}\.\d{1,2}(?:\.\d{1,3})?)", part)
74
- if ver_match:
75
- name_candidate = part[: ver_match.start()].strip("-_ ")
76
- version_release_candidate = part[ver_match.start() :]
77
- vr_parts = version_release_candidate.split("-", 1)
78
- info["version"] = vr_parts[0]
79
- if len(vr_parts) > 1:
80
- info["release"] = vr_parts[1]
81
- else:
82
- final_vr_parts = info["version"].rsplit("-", 1)
83
- if len(final_vr_parts) == 2:
84
- info["version"] = final_vr_parts[0]
85
- info["release"] = final_vr_parts[1]
86
- info["name"] = name_candidate
87
- else:
88
- name_ver_parts = part.rsplit("-", 1)
89
- if len(name_ver_parts) == 2 and re.match(r"\d", name_ver_parts[1]):
90
- info["name"] = name_ver_parts[0]
91
- ver_rel_parts = name_ver_parts[1].rsplit("-", 1)
92
- info["version"] = ver_rel_parts[0]
93
- if len(ver_rel_parts) > 1:
94
- info["release"] = ver_rel_parts[1]
95
- else:
96
- info["name"] = part
97
- final_name = info["name"].replace(".rpm", "").strip("-_ ")
98
- if not final_name and ("kernel" in work_str or "vmlinux" in work_str):
99
- final_name = "kernel"
100
- kernel_markers = [
101
- "ali3000",
102
- "ali4000",
103
- "ali5000",
104
- "ali6000",
105
- "kernel",
106
- "kmod",
107
- ]
108
- if any(marker in final_name for marker in kernel_markers) or (
109
- ver_match and ver_match.group(1).startswith(("5.10", "4.19", "6.6"))
110
- ):
111
- info["is_kernel"] = True
112
- if info["alinux_version"] == "unknown":
113
- if info.get("release") and "al8" in info["release"]:
114
- info["alinux_version"] = "3"
115
- elif info.get("release") and "al7" in info["release"]:
116
- info["alinux_version"] = "2"
117
- elif info.get("release") and "alnx4" in info["release"]:
118
- info["alinux_version"] = "4"
119
- elif info["is_kernel"] and info.get("version", "").startswith(
120
- "5.10"
121
- ):
122
- info["alinux_version"] = "3"
123
- elif info["is_kernel"] and info.get("version", "").startswith(
124
- "6.6"
125
- ):
126
- info["alinux_version"] = "4"
127
- if (
128
- "debug" in work_str
129
- or "vmlinux" in work_str
130
- or "debuginfo" in final_name
131
- ):
132
- info["type"] = "debuginfo"
133
- if info["type"] == "debuginfo" and "debuginfo" not in final_name:
134
- final_name += "-debuginfo"
135
- info["name"] = final_name.strip("-")
136
- if info["name"]:
137
- results.append(info)
138
- return results
139
-
140
-
141
- # 全局共享的AsyncClient实例,配置连接池以支持高并发
142
- _shared_http_client: httpx.AsyncClient | None = None
143
-
144
-
145
- def _get_shared_http_client() -> httpx.AsyncClient:
146
- """获取共享的httpx AsyncClient实例,配置连接池支持高并发"""
147
- global _shared_http_client
148
- if _shared_http_client is None:
149
- # 配置连接池限制,支持高并发请求
150
- limits = httpx.Limits(
151
- max_connections=200, # 最大连接数
152
- max_keepalive_connections=50, # 保持活动的连接数
153
- )
154
- _shared_http_client = httpx.AsyncClient(
155
- limits=limits,
156
- timeout=1.5,
157
- follow_redirects=True,
158
- )
159
- return _shared_http_client
160
-
161
-
162
- async def _check_single_url(client: httpx.AsyncClient, url: str) -> dict:
163
- """
164
- 异步检查单个URL的可用性
165
- """
166
- headers = {
167
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
168
- }
169
- result = {"url": url}
170
- try:
171
- response = await client.head(url, headers=headers)
172
- if response.status_code == 200:
173
- result["status"] = "valid"
174
- result["status_code"] = "200"
175
- else:
176
- result["status"] = "invalid"
177
- result["status_code"] = str(response.status_code)
178
- except httpx.TimeoutException:
179
- result["status"] = "error"
180
- result["reason"] = "Request timed out"
181
- except httpx.ConnectError as e:
182
- result["status"] = "error"
183
- result["reason"] = f"Connection error: {e}"
184
- except httpx.HTTPError as e:
185
- result["status"] = "error"
186
- result["reason"] = f"An unexpected error occurred: {e}"
187
- return result
188
-
189
-
190
- async def check_urls_availability_batch(
191
- urls: list[str],
192
- ) -> list[dict[str, Any]]:
193
- """
194
- 并发检查一批URLs是否有效且可访问。
195
- """
196
- if not urls:
197
- return []
198
-
199
- client = _get_shared_http_client()
200
- # 使用asyncio.gather并发执行所有URL检查
201
- tasks = [_check_single_url(client, url) for url in urls]
202
- results = await asyncio.gather(*tasks)
203
- return list(results)
204
-
205
-
206
- def get_rpm_list(
207
- name: str, version: str = "", release: str = "", arch: str = ""
208
- ) -> list[dict[str, str]]:
209
- """
210
- 通过内部API查询RPM包的详细信息。返回结果是内网URL,需要后续处理。
211
- """
212
- base_url = "http://opsx.vip.tbsite.net/gapi/v1/cms/rpm/list?"
213
- # 确保version包含2位小数点,避免精确查询失败
214
- if version and version.count(".") != 2:
215
- version = ""
216
- params = {
217
- "name": name,
218
- "version": version,
219
- "release": release,
220
- "arch": arch,
221
- }
222
-
223
- rep_text = fetch_webpage_content(base_url, params=params)
224
- if not rep_text:
225
- return []
226
-
227
- try:
228
- rep_json = json.loads(rep_text)
229
- rpms = rep_json.get("data", {}).get("rpms", [])
230
- return [
231
- {
232
- "name": item.get("name", ""),
233
- "version": item.get("version", ""),
234
- "release": item.get("release", ""),
235
- "arch": item.get("arch", ""),
236
- "internal_url": item.get("download_url", ""),
237
- }
238
- for item in rpms
239
- if "alinux" in item.get("download_url", "")
240
- ]
241
- except (json.JSONDecodeError, TypeError) as e:
242
- logger.error(f"API响应解析失败: {e}")
243
- return []
244
-
245
-
246
- async def construct_url(
247
- name: str,
248
- version: str,
249
- release: str,
250
- arch: str,
251
- is_kernel: bool = False,
252
- alinux_version: str = "3",
253
- ) -> list[str]:
254
- """
255
- 根据包信息拼接公网URL作为备用方案,name大小写敏感。
256
- """
257
- # 步骤1: 输入验证
258
- if (
259
- not all([name, version, release, arch, alinux_version])
260
- or alinux_version == "unknown"
261
- ):
262
- return []
263
-
264
- urls = []
265
- full_rpm_filename = f"{name}-{version}-{release}.{arch}.rpm"
266
-
267
- # 步骤2: 根据包类型选择不同的拼接逻辑
268
- # 逻辑分支 A: 内核Debuginfo包
269
- if is_kernel and "debuginfo" in name:
270
- full_ver_rel = f"{version}-{release}"
271
- if alinux_version in ["2", "3"]:
272
- # Alinux 2/3 的 debuginfo 在 plus 仓库的特定子目录
273
- base_url = f"https://mirrors.aliyun.com/alinux/{alinux_version}/plus/{arch}/debug/kernels"
274
- urls.append(
275
- f"{base_url}/kernel-debuginfo-{full_ver_rel}.{arch}.rpm"
276
- )
277
- # 内核debuginfo需要common包
278
- urls.append(
279
- f"{base_url}/kernel-debuginfo-common-{arch}-{version}-{release}.rpm"
280
- )
281
- elif alinux_version == "4":
282
- # Alinux 4 可能在 os 或 updates 仓库
283
- for repo in ["os", "updates"]:
284
- base_url = f"https://mirrors.aliyun.com/alinux/{alinux_version}/{repo}/{arch}/debug"
285
- urls.append(
286
- f"{base_url}/kernel-debuginfo-{full_ver_rel}.{arch}.rpm"
287
- )
288
- urls.append(
289
- f"{base_url}/kernel-debuginfo-common-{arch}-{version}-{release}.rpm"
290
- )
291
-
292
- # 逻辑分支 B: 普通RPM包
293
- else:
294
- # common_repos = ['os', 'plus', 'updates', 'powertools', 'extras']
295
- common_repos = ["os"]
296
- for repo in common_repos:
297
- url = f"https://mirrors.aliyun.com/alinux/{alinux_version}/{repo}/{arch}/Packages/{full_rpm_filename}"
298
- urls.append(url)
299
- # 对于debuginfo包,有可能在debug目录下
300
- if "debuginfo" in name:
301
- url_debug_repo = f"https://mirrors.aliyun.com/alinux/{alinux_version}/{repo}/{arch}/debug/Packages/{full_rpm_filename}"
302
- urls.append(url_debug_repo)
303
- # 使用并发批量检查所有URL
304
- client = _get_shared_http_client()
305
- tasks = [_check_single_url(client, url) for url in urls]
306
- check_results = await asyncio.gather(*tasks)
307
-
308
- return [
309
- result["url"]
310
- for result in check_results
311
- if result.get("status") == "valid"
312
- ]
313
-
314
-
315
- # --- CLI Wrappers ---
316
-
317
-
318
- def cmd_parse(args):
319
- results = parse_user_query_to_rpm_info(args.query)
320
- print(json.dumps(results, ensure_ascii=False, indent=2))
321
-
322
-
323
- def cmd_search(args):
324
- results = get_rpm_list(
325
- name=args.name,
326
- version=args.version,
327
- release=args.release,
328
- arch=args.arch,
329
- )
330
- print(json.dumps(results, ensure_ascii=False, indent=2))
331
-
332
-
333
- async def async_cmd_verify(args):
334
- urls = args.urls
335
- results = await check_urls_availability_batch(urls)
336
- print(json.dumps(results, ensure_ascii=False, indent=2))
337
-
338
-
339
- def cmd_verify(args):
340
- asyncio.run(async_cmd_verify(args))
341
-
342
-
343
- async def async_cmd_construct(args):
344
- results = await construct_url(
345
- name=args.name,
346
- version=args.version,
347
- release=args.release,
348
- arch=args.arch,
349
- is_kernel=args.is_kernel,
350
- alinux_version=args.alinux_version,
351
- )
352
- print(json.dumps(results, ensure_ascii=False, indent=2))
353
-
354
-
355
- def cmd_construct(args):
356
- asyncio.run(async_cmd_construct(args))
357
-
358
-
359
- def main():
360
- parser = argparse.ArgumentParser(description="RPM Search Tool CLI")
361
- subparsers = parser.add_subparsers(
362
- dest="command", help="Available commands"
363
- )
364
-
365
- # Command: parse
366
- parser_parse = subparsers.add_parser(
367
- "parse", help="Parse user query to RPM info"
368
- )
369
- parser_parse.add_argument("query", type=str, help="User query string")
370
- parser_parse.set_defaults(func=cmd_parse)
371
-
372
- # Command: search
373
- parser_search = subparsers.add_parser("search", help="Search RPM via API")
374
- parser_search.add_argument("name", type=str, help="Package name")
375
- parser_search.add_argument(
376
- "--version", type=str, default="", help="Package version"
377
- )
378
- parser_search.add_argument(
379
- "--release", type=str, default="", help="Package release"
380
- )
381
- parser_search.add_argument(
382
- "--arch", type=str, default="", help="Architecture"
383
- )
384
- parser_search.set_defaults(func=cmd_search)
385
-
386
- # Command: verify
387
- parser_verify = subparsers.add_parser(
388
- "verify", help="Verify URLs availability"
389
- )
390
- parser_verify.add_argument("urls", nargs="+", help="List of URLs to verify")
391
- parser_verify.set_defaults(func=cmd_verify)
392
-
393
- # Command: construct
394
- parser_construct = subparsers.add_parser(
395
- "construct", help="Construct URL fallback"
396
- )
397
- parser_construct.add_argument("name", type=str, help="Package name")
398
- parser_construct.add_argument("version", type=str, help="Package version")
399
- parser_construct.add_argument("release", type=str, help="Package release")
400
- parser_construct.add_argument("arch", type=str, help="Architecture")
401
- parser_construct.add_argument(
402
- "--is_kernel", action="store_true", help="Is kernel package"
403
- )
404
- parser_construct.add_argument(
405
- "--alinux_version", type=str, default="3", help="Alinux version"
406
- )
407
- parser_construct.set_defaults(func=cmd_construct)
408
-
409
- args = parser.parse_args()
410
- if hasattr(args, "func"):
411
- args.func(args)
412
- else:
413
- parser.print_help()
414
-
415
-
416
- if __name__ == "__main__":
417
- main()