vskill 0.2.44 → 0.2.46

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.
@@ -297,40 +297,47 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
297
297
  for (const unreg of selectedUnregistered) {
298
298
  const pluginPath = unreg.source.replace(/^\.\//, "");
299
299
  try {
300
- // Discover skills in this plugin to submit each one
300
+ // Discover skills: try nested {pluginPath}/skills/ first, fall back to flat {pluginPath}/SKILL.md
301
+ const skillsToSubmit = [];
301
302
  const skillsUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${pluginPath}/skills`;
302
303
  const skillsRes = await fetch(skillsUrl, { headers: { "User-Agent": "vskill-cli" } });
303
- if (!skillsRes.ok)
304
- continue;
305
- const skillDirs = (await skillsRes.json())
306
- .filter((e) => e.type === "dir");
307
- for (const sd of skillDirs) {
308
- const skillFilePath = `${pluginPath}/skills/${sd.name}/SKILL.md`;
304
+ if (skillsRes.ok) {
305
+ const skillDirs = (await skillsRes.json())
306
+ .filter((e) => e.type === "dir");
307
+ for (const sd of skillDirs) {
308
+ skillsToSubmit.push({ name: sd.name, path: `${pluginPath}/skills/${sd.name}/SKILL.md` });
309
+ }
310
+ }
311
+ else {
312
+ // Flat layout: SKILL.md at plugin root
313
+ skillsToSubmit.push({ name: unreg.name, path: `${pluginPath}/SKILL.md` });
314
+ }
315
+ for (const skill of skillsToSubmit) {
309
316
  try {
310
- const sub = await submitSkill({ repoUrl, skillName: sd.name, skillPath: skillFilePath });
317
+ const sub = await submitSkill({ repoUrl, skillName: skill.name, skillPath: skill.path });
311
318
  if (sub.alreadyVerified) {
312
- const skillUrl = `https://verified-skill.com/skills/${sd.name}`;
313
- console.log(green(` ${bold(sd.name)} is already verified.`));
319
+ const skillUrl = `https://verified-skill.com/skills/${encodeURIComponent(skill.name)}`;
320
+ console.log(green(` ${bold(skill.name)} is already verified.`));
314
321
  console.log(dim(" View: ") + link(skillUrl, skillUrl));
315
322
  }
316
323
  else if (sub.blocked) {
317
- console.log(red(` ${bold(sd.name)} is blocked.`));
324
+ console.log(red(` ${bold(skill.name)} is blocked.`));
318
325
  }
319
326
  else {
320
327
  const subId = sub.id ?? sub.submissionId;
321
328
  const trackUrl = `https://verified-skill.com/submit/${subId}`;
322
329
  if (sub.duplicate) {
323
- console.log(yellow(` ${bold(sd.name)} is already in the queue.`));
330
+ console.log(yellow(` ${bold(skill.name)} is already in the queue.`));
324
331
  }
325
332
  else {
326
- console.log(green(` Submitted ${bold(sd.name)} for scanning.`));
333
+ console.log(green(` Submitted ${bold(skill.name)} for scanning.`));
327
334
  }
328
335
  console.log(dim(" Track: ") + link(trackUrl, trackUrl));
329
336
  }
330
337
  }
331
338
  catch {
332
339
  const submitUrl = `https://verified-skill.com/submit`;
333
- console.log(yellow(` Could not submit ${sd.name} automatically.`));
340
+ console.log(yellow(` Could not submit ${skill.name} automatically.`));
334
341
  console.log(dim(" Submit manually: ") + link(submitUrl, submitUrl));
335
342
  }
336
343
  }
@@ -352,95 +359,91 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
352
359
  selectedUnregistered = [];
353
360
  }
354
361
  }
362
+ // ── Step 1: Optional Claude Code native plugin install ──────────────
355
363
  let marketplaceRegistered = false;
356
- if (hasClaude) {
357
- // Register marketplace via git URL — Claude Code clones to its own
358
- // persistent location, avoiding the stale-temp-dir bug.
359
- const gitUrl = `https://github.com/${owner}/${repo}`;
360
- const regSpin = spinner("Registering marketplace with Claude Code");
361
- let regResult = registerMarketplace(gitUrl);
362
- // Retry once after deregistering stale entry
363
- if (!regResult.success) {
364
- deregisterMarketplace(gitUrl);
365
- regResult = registerMarketplace(gitUrl);
366
- }
367
- marketplaceRegistered = regResult.success;
368
- regSpin.stop();
369
- if (!marketplaceRegistered) {
370
- console.log(yellow(" Failed to register marketplace — will use extraction fallback."));
371
- if (regResult.stderr) {
372
- console.log(dim(` Reason: ${regResult.stderr}`));
373
- }
374
- }
375
- }
376
- // Install registered plugins
377
364
  const results = [];
378
- for (const plugin of selectedPlugins) {
379
- if (hasClaude && marketplaceRegistered && marketplaceName) {
380
- // Native install
381
- const installSpin = spinner(`Installing ${bold(plugin.name)} via Claude Code plugin system`);
382
- const ok = installNativePlugin(plugin.name, marketplaceName, opts.global ? "user" : "project");
383
- installSpin.stop();
384
- if (ok) {
385
- console.log(green(` ✓ ${bold(plugin.name)}`) + dim(` (${marketplaceName}:${plugin.name})`));
386
- results.push({ name: plugin.name, installed: true, method: "native" });
387
- }
388
- else {
389
- console.log(red(` ✗ ${bold(plugin.name)}`) + dim(" — native install failed, trying extraction..."));
390
- // Fallback to extraction
391
- try {
392
- await installRepoPlugin(`${owner}/${repo}`, plugin.name, opts);
393
- results.push({ name: plugin.name, installed: true, method: "extraction" });
394
- }
395
- catch {
396
- results.push({ name: plugin.name, installed: false, method: "failed" });
397
- }
398
- }
365
+ if (hasClaude && selectedPlugins.length > 0) {
366
+ let wantNative = false;
367
+ if (isTTY() && !opts.yes) {
368
+ const prompter = createPrompter();
369
+ wantNative = await prompter.promptConfirm("Claude Code detected. Install selected plugins as native Claude Code plugins?", true);
399
370
  }
400
371
  else {
401
- // No Claude CLI extraction fallback
402
- try {
403
- await installRepoPlugin(`${owner}/${repo}`, plugin.name, opts);
404
- results.push({ name: plugin.name, installed: true, method: "extraction" });
372
+ // Non-interactive or --yes: install as native plugin by default
373
+ wantNative = true;
374
+ }
375
+ if (wantNative && marketplaceName) {
376
+ const gitUrl = `https://github.com/${owner}/${repo}`;
377
+ const regSpin = spinner("Registering marketplace with Claude Code");
378
+ let regResult = registerMarketplace(gitUrl);
379
+ if (!regResult.success) {
380
+ deregisterMarketplace(gitUrl);
381
+ regResult = registerMarketplace(gitUrl);
405
382
  }
406
- catch (err) {
407
- console.error(red(` ✗ ${plugin.name}: ${err.message}`));
408
- results.push({ name: plugin.name, installed: false, method: "failed" });
383
+ marketplaceRegistered = regResult.success;
384
+ regSpin.stop();
385
+ if (marketplaceRegistered) {
386
+ for (const plugin of selectedPlugins) {
387
+ const installSpin = spinner(`Installing ${bold(plugin.name)} via Claude Code plugin system`);
388
+ const ok = installNativePlugin(plugin.name, marketplaceName, opts.global ? "user" : "project");
389
+ installSpin.stop();
390
+ if (ok) {
391
+ console.log(green(` ✓ ${bold(plugin.name)}`) + dim(` (${marketplaceName}:${plugin.name})`));
392
+ }
393
+ else {
394
+ console.log(yellow(` ⚠ ${bold(plugin.name)}`) + dim(" — native install failed"));
395
+ }
396
+ }
397
+ }
398
+ else {
399
+ console.log(yellow(" Failed to register marketplace — skipping native install."));
409
400
  }
410
401
  }
411
402
  }
412
- // Install unregistered plugins fetch skills and copy directly (no nesting)
413
- if (selectedUnregistered.length > 0) {
414
- const branch = await getDefaultBranch(owner, repo);
415
- let agents = await detectInstalledAgents();
416
- const selections = await promptInstallOptions(agents, opts);
417
- agents = selections.agents;
418
- if (selections.global)
419
- opts.global = true;
420
- if (!selections.symlink)
421
- opts.copy = true;
422
- for (const unreg of selectedUnregistered) {
423
- const pluginPath = unreg.source.replace(/^\.\//, "");
424
- const installSpin = spinner(`Installing unverified plugin: ${bold(unreg.name)}`);
425
- try {
426
- // Discover skills inside the plugin directory
427
- const skillsUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${pluginPath}/skills`;
428
- const skillsRes = await fetch(skillsUrl, { headers: { "User-Agent": "vskill-cli" } });
429
- if (!skillsRes.ok)
430
- throw new Error(`No skills found in ${pluginPath}/skills`);
403
+ // ── Step 2: Agent selection + skill file install (ALWAYS) ──────────
404
+ const branch = await getDefaultBranch(owner, repo);
405
+ let agents = await detectInstalledAgents();
406
+ const selections = await promptInstallOptions(agents, opts);
407
+ agents = selections.agents;
408
+ if (selections.global)
409
+ opts.global = true;
410
+ if (!selections.symlink)
411
+ opts.copy = true;
412
+ // Combine all selected plugins (registered + unregistered) for skill file install
413
+ const allPluginsToInstall = [
414
+ ...selectedPlugins.map((p) => ({
415
+ name: p.name,
416
+ source: getPluginSource(p.name, manifestContent || "") || "",
417
+ isUnregistered: false,
418
+ })),
419
+ ...selectedUnregistered.map((u) => ({
420
+ name: u.name,
421
+ source: u.source,
422
+ isUnregistered: true,
423
+ })),
424
+ ];
425
+ for (const plugin of allPluginsToInstall) {
426
+ const pluginPath = plugin.source.replace(/^\.\//, "");
427
+ if (!pluginPath) {
428
+ results.push({ name: plugin.name, installed: false, method: "failed" });
429
+ continue;
430
+ }
431
+ const installSpin = spinner(`Installing skills: ${bold(plugin.name)}`);
432
+ try {
433
+ // Discover skills: try {pluginPath}/skills/ first, then fall back to {pluginPath}/SKILL.md
434
+ const installedSkillNames = [];
435
+ const skillsUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${pluginPath}/skills`;
436
+ const skillsRes = await fetch(skillsUrl, { headers: { "User-Agent": "vskill-cli" } });
437
+ if (skillsRes.ok) {
438
+ // Nested layout: {pluginPath}/skills/{skillName}/SKILL.md
431
439
  const skillDirs = (await skillsRes.json())
432
440
  .filter((e) => e.type === "dir");
433
- if (skillDirs.length === 0)
434
- throw new Error(`No skill directories in ${pluginPath}/skills`);
435
- // Fetch each skill's SKILL.md and install directly to agent dirs
436
- let installedSkillNames = [];
437
441
  for (const sd of skillDirs) {
438
442
  const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${pluginPath}/skills/${sd.name}/SKILL.md`;
439
443
  const contentRes = await fetch(rawUrl);
440
444
  if (!contentRes.ok)
441
445
  continue;
442
446
  const content = await contentRes.text();
443
- // Write to each agent's skills dir: {agent-dir}/{skillName}/SKILL.md
444
447
  for (const agent of agents) {
445
448
  const baseDir = resolveInstallBase(opts, agent);
446
449
  const skillDir = join(baseDir, sd.name);
@@ -449,22 +452,38 @@ async function installMarketplaceRepo(owner, repo, manifestContent, opts, preSel
449
452
  }
450
453
  installedSkillNames.push(sd.name);
451
454
  }
452
- installSpin.stop();
453
- if (installedSkillNames.length > 0) {
454
- console.log(green(` ✓ ${bold(unreg.name)}`) + dim(` (${installedSkillNames.join(", ")})`));
455
- results.push({ name: unreg.name, installed: true, method: "extraction-unregistered" });
456
- }
457
- else {
458
- console.log(red(` ✗ ${bold(unreg.name)}`) + dim(" — no skills found"));
459
- results.push({ name: unreg.name, installed: false, method: "failed" });
455
+ }
456
+ else {
457
+ // Flat layout: {pluginPath}/SKILL.md directly in the plugin root
458
+ const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${pluginPath}/SKILL.md`;
459
+ const contentRes = await fetch(rawUrl);
460
+ if (contentRes.ok) {
461
+ const content = await contentRes.text();
462
+ for (const agent of agents) {
463
+ const baseDir = resolveInstallBase(opts, agent);
464
+ const skillDir = join(baseDir, plugin.name);
465
+ mkdirSync(skillDir, { recursive: true });
466
+ writeFileSync(join(skillDir, "SKILL.md"), content, "utf-8");
467
+ }
468
+ installedSkillNames.push(plugin.name);
460
469
  }
461
470
  }
462
- catch (err) {
463
- installSpin.stop();
464
- console.error(red(` ✗ ${unreg.name}: ${err.message}`));
465
- results.push({ name: unreg.name, installed: false, method: "failed" });
471
+ installSpin.stop();
472
+ if (installedSkillNames.length > 0) {
473
+ const tier = plugin.isUnregistered ? "unverified" : "verified";
474
+ console.log(green(` ✓ ${bold(plugin.name)}`) + dim(` (${installedSkillNames.join(", ")}) [${tier}]`));
475
+ results.push({ name: plugin.name, installed: true, method: plugin.isUnregistered ? "extraction-unregistered" : "extraction" });
476
+ }
477
+ else {
478
+ console.log(red(` ✗ ${bold(plugin.name)}`) + dim(" — no SKILL.md found"));
479
+ results.push({ name: plugin.name, installed: false, method: "failed" });
466
480
  }
467
481
  }
482
+ catch (err) {
483
+ installSpin.stop();
484
+ console.error(red(` ✗ ${plugin.name}: ${err.message}`));
485
+ results.push({ name: plugin.name, installed: false, method: "failed" });
486
+ }
468
487
  }
469
488
  // Update lockfile
470
489
  const lockForWrite = ensureLockfile(lockDir);
@@ -1305,6 +1324,7 @@ async function installRepoPlugin(ownerRepo, pluginName, opts, overrideSource) {
1305
1324
  // Discover skills via GitHub Contents API
1306
1325
  const discoverSpin = spinner(`Discovering skills in ${pluginName}`);
1307
1326
  let skillEntries = [];
1327
+ let flatLayout = false;
1308
1328
  try {
1309
1329
  const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${pluginPath}/skills`, { headers: { "User-Agent": "vskill-cli" } });
1310
1330
  if (res.ok) {
@@ -1312,6 +1332,17 @@ async function installRepoPlugin(ownerRepo, pluginName, opts, overrideSource) {
1312
1332
  }
1313
1333
  }
1314
1334
  catch { /* skills dir might not exist */ }
1335
+ // Flat layout fallback: check for SKILL.md directly at plugin root
1336
+ if (skillEntries.length === 0) {
1337
+ try {
1338
+ const res = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${pluginPath}/SKILL.md`);
1339
+ if (res.ok) {
1340
+ flatLayout = true;
1341
+ skillEntries = [{ name: pluginName, type: "dir" }];
1342
+ }
1343
+ }
1344
+ catch { /* flat layout not available */ }
1345
+ }
1315
1346
  let cmdEntries = [];
1316
1347
  try {
1317
1348
  const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${pluginPath}/commands`, { headers: { "User-Agent": "vskill-cli" } });
@@ -1337,7 +1368,9 @@ async function installRepoPlugin(ownerRepo, pluginName, opts, overrideSource) {
1337
1368
  const allContent = [];
1338
1369
  const skills = [];
1339
1370
  for (const entry of skillEntries) {
1340
- const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${pluginPath}/skills/${entry.name}/SKILL.md`;
1371
+ const rawUrl = flatLayout
1372
+ ? `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${pluginPath}/SKILL.md`
1373
+ : `https://raw.githubusercontent.com/${owner}/${repo}/${branch}/${pluginPath}/skills/${entry.name}/SKILL.md`;
1341
1374
  try {
1342
1375
  const res = await fetch(rawUrl);
1343
1376
  if (res.ok) {