@supa-magic/spm 0.2.2 → 0.2.4
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/dist/bin/spm.js +102 -43
- package/package.json +6 -2
package/dist/bin/spm.js
CHANGED
|
@@ -3,8 +3,8 @@ import { Command } from "commander";
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
4
4
|
import { join, dirname, relative, resolve, normalize } from "node:path";
|
|
5
5
|
import { parse, stringify } from "yaml";
|
|
6
|
-
import { execSync, spawn } from "node:child_process";
|
|
7
|
-
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
7
|
+
import { readFile, mkdir, writeFile, rm } from "node:fs/promises";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
const registerDoctorCommand = (program2) => {
|
|
@@ -20,10 +20,28 @@ const knownProviders = {
|
|
|
20
20
|
codeium: ".codeium",
|
|
21
21
|
cody: ".cody"
|
|
22
22
|
};
|
|
23
|
+
const cliCommands = {
|
|
24
|
+
claude: "claude",
|
|
25
|
+
aider: "aider"
|
|
26
|
+
};
|
|
27
|
+
const hasCliInstalled = (command) => {
|
|
28
|
+
try {
|
|
29
|
+
execFileSync(command, ["--version"], {
|
|
30
|
+
stdio: "ignore",
|
|
31
|
+
timeout: 5e3,
|
|
32
|
+
shell: process.platform === "win32"
|
|
33
|
+
});
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
23
39
|
const detectProviders = (root) => Object.entries(knownProviders).reduce(
|
|
24
40
|
(providers, [name, path]) => {
|
|
25
|
-
const
|
|
26
|
-
|
|
41
|
+
const hasDir = existsSync(join(root, path));
|
|
42
|
+
const cli = cliCommands[name];
|
|
43
|
+
const hasCli = cli ? hasCliInstalled(cli) : false;
|
|
44
|
+
if (hasDir || hasCli) {
|
|
27
45
|
providers[name] = { path };
|
|
28
46
|
}
|
|
29
47
|
return providers;
|
|
@@ -350,7 +368,7 @@ const downloadFile = async (source) => {
|
|
|
350
368
|
const content = await getContent(source);
|
|
351
369
|
return { content, source };
|
|
352
370
|
};
|
|
353
|
-
const template = '# Skillset Integration\n\nYou are integrating a new skillset into the project. Analyze the existing project setup and intelligently integrate the new files.\n\n## Context\n\n- **Downloaded files**: `{{downloadDir}}`\n- **Provider directory**: `{{providerDir}}`\n- **Skillset**: `{{skillsetName}}` v`{{skillsetVersion}}`\n- **Source**: `{{source}}`\n- **Config file**: `{{configPath}}`\n\nIdentical files have already been removed from the download folder. Only files that need action remain. If the download folder is empty, skip to the config update step.\n\n## Output Format\n\nCRITICAL — follow exactly:\n- NEVER wrap output in code blocks or backticks\n- NEVER write conversational text ("Now let me...", "Let me check...", "The files are...")\n- NEVER use emojis\n- Output ONLY structured log lines\n- Step headers are plain text ending with `...` — these are parsed by the CLI to drive spinner states\n- Items below headers are indented with 2 spaces and use `• ` prefix\n- File lists use ASCII tree: `├─`, `└─`, `│`\n- End with `Done` on its own line\n\nStep headers (use these exactly):\n\n1. `Analyzing existing setup...`\n2. `Analyzing downloaded files...`\n3. `Detecting conflicts...`\n4. `Integrating...`\n5. `Updating config...`\n6. `Running setup...` (only if a setup file exists)\n7. `
|
|
371
|
+
const template = '# Skillset Integration\n\nYou are integrating a new skillset into the project. Analyze the existing project setup and intelligently integrate the new files.\n\n## Context\n\n- **Downloaded files**: `{{downloadDir}}`\n- **Provider directory**: `{{providerDir}}`\n- **Skillset**: `{{skillsetName}}` v`{{skillsetVersion}}`\n- **Source**: `{{source}}`\n- **Config file**: `{{configPath}}`\n\nIdentical files have already been removed from the download folder. Only files that need action remain. If the download folder is empty, skip to the config update step.\n\n## Output Format\n\nCRITICAL — follow exactly:\n- NEVER wrap output in code blocks or backticks\n- NEVER write conversational text ("Now let me...", "Let me check...", "The files are...")\n- NEVER use emojis\n- Output ONLY structured log lines\n- Step headers are plain text ending with `...` — these are parsed by the CLI to drive spinner states\n- Items below headers are indented with 2 spaces and use `• ` prefix\n- File lists use ASCII tree: `├─`, `└─`, `│`\n- End with `Done` on its own line\n\nStep headers (use these exactly):\n\n1. `Analyzing existing setup...`\n2. `Analyzing downloaded files...`\n3. `Detecting conflicts...`\n4. `Integrating...`\n5. `Updating config...`\n6. `Running setup...` (only if a setup file exists)\n7. `Done`\n\nExample output:\n\nAnalyzing existing setup...\n • 4 skills, 2 rules, 1 hook\n\nAnalyzing downloaded files...\n • skills: git, github, implement\n • rules: coding\n • hook: biome-format\n\nDetecting conflicts...\n • No conflicts\n\nIntegrating...\n • skills/git/SKILL.md\n • skills/git/branch.md\n • skills/github/SKILL.md\n\nUpdating config...\n • skillset: skill-creator@1.0.0\n\nRunning setup...\n • Configured MCP server: biome\n\nDone\n\n## Step 1: Analyzing existing setup\n\nRead the provider directory (`{{providerDir}}`) and catalog what is already in place: skills, rules, agents, hooks, mcp servers files.\n\n## Step 2: Analyzing downloaded files\n\nRead all files in `{{downloadDir}}`. Only files present in this folder need to be installed.\n\n## Step 3: Detecting conflicts\n\nFor each downloaded file, check if a file with the same name exists in the provider directory.\n\n- **No existing file** → install as new\n- **Different skillset version** → replace silently (new version supersedes)\n- **Same version or no version info** → conflict — ask the user:\n\nDetecting conflicts...\n • rules/coding.md — local has custom rules\n • Choose: (r)eplace / (s)kip / (m)erge\n\nWait for response before proceeding.\n\n## Step 4: Integrating\n\nInstall files into `{{providerDir}}`, following the directory structure from the download folder.\n\n**Integrate, don\'t copy.** When a new skill can leverage existing project conventions, adapt it:\n\n- If the project has `rules/coding.md` with specific conventions → make new skills follow those rules instead of their own defaults\n- If existing skills handle branching or committing → reference them instead of duplicating instructions\n- If the project has naming conventions, testing patterns, or architectural rules → align new skills with them\n\n## Step 5: Updating config\n\nUpdate `{{configPath}}`:\n\n- Under the provider with path `{{providerDir}}`, add the skillset entry under `skillsets`:\n `{{skillsetName}}: "{{source}}@{{skillsetVersion}}"`\n- Do NOT add individual skill, agent, or file names to the config\n- The `skills` map is reserved for standalone skill installations\n\n{{setupSection}}\n\n## Rules\n\n- Setup files are NOT installed — they contain instructions to configure the project\n- Do not delete or modify existing files in the provider directory unless resolving a conflict\n- Do NOT delete the download folder — cleanup is handled externally\n';
|
|
354
372
|
const buildSetupSection = (setupFile) => setupFile ? [
|
|
355
373
|
"## Step 6: Running setup",
|
|
356
374
|
"",
|
|
@@ -365,7 +383,6 @@ const writeInstructionsFile = (input) => {
|
|
|
365
383
|
writeFileSync(filePath, buildInstructions(input), "utf-8");
|
|
366
384
|
return filePath;
|
|
367
385
|
};
|
|
368
|
-
const TIMEOUT_MS = 12e4;
|
|
369
386
|
const isNoise = (line) => line === "```" || line.startsWith("```") || /^(now |let me |the files? |i'll |i will |looking |checking )/i.test(line);
|
|
370
387
|
const isStepHeader = (raw, trimmed) => !raw.startsWith(" ") && !raw.startsWith(" ") && trimmed.endsWith("...");
|
|
371
388
|
const stepCategory = (header) => {
|
|
@@ -392,7 +409,7 @@ const toRelativePath = (filePath, providerDir) => {
|
|
|
392
409
|
if (idx !== -1) return normalized.slice(idx + marker.length);
|
|
393
410
|
return normalized.split("/").pop() ?? normalized;
|
|
394
411
|
};
|
|
395
|
-
const spawnClaude = (instructionsFilePath, stepper, providerDir) => new Promise((resolve2, reject) => {
|
|
412
|
+
const spawnClaude = (instructionsFilePath, stepper, providerDir, model) => new Promise((resolve2, reject) => {
|
|
396
413
|
const args = [
|
|
397
414
|
"-p",
|
|
398
415
|
"Install the skill as instructed.",
|
|
@@ -402,11 +419,11 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir) => new Promise(
|
|
|
402
419
|
"--output-format",
|
|
403
420
|
"stream-json",
|
|
404
421
|
"--allowedTools",
|
|
405
|
-
"Read,Write,Edit,Bash,Glob,Grep"
|
|
422
|
+
"Read,Write,Edit,Bash,Glob,Grep",
|
|
423
|
+
...model ? ["--model", model] : []
|
|
406
424
|
];
|
|
407
425
|
const isWindows = process.platform === "win32";
|
|
408
426
|
const child = spawn("claude", args, {
|
|
409
|
-
timeout: TIMEOUT_MS,
|
|
410
427
|
shell: isWindows,
|
|
411
428
|
stdio: ["ignore", "pipe", "pipe"]
|
|
412
429
|
});
|
|
@@ -415,7 +432,7 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir) => new Promise(
|
|
|
415
432
|
let currentStepHeader = "Analyzing existing setup...";
|
|
416
433
|
let stepItems = [];
|
|
417
434
|
let stepFileCount = 0;
|
|
418
|
-
const state = { doneReceived: false };
|
|
435
|
+
const state = { doneReceived: false, setupReached: false };
|
|
419
436
|
const writtenFiles = [];
|
|
420
437
|
const succeedCurrentStep = () => {
|
|
421
438
|
if (!currentStepHeader) return;
|
|
@@ -432,11 +449,12 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir) => new Promise(
|
|
|
432
449
|
return;
|
|
433
450
|
}
|
|
434
451
|
if (currentStepHeader.startsWith("Running setup")) {
|
|
435
|
-
|
|
436
|
-
|
|
452
|
+
stepper.succeed("Skillset setup completed");
|
|
453
|
+
currentStepHeader = "";
|
|
437
454
|
return;
|
|
438
455
|
}
|
|
439
|
-
|
|
456
|
+
if (state.setupReached || currentStepHeader.startsWith("Cleaning")) return;
|
|
457
|
+
const context = stepItems.length > 0 ? stepItems.join(", ") : void 0;
|
|
440
458
|
stepper.succeed(base, context);
|
|
441
459
|
};
|
|
442
460
|
stepper.start("Analyzing existing setup...", "skills");
|
|
@@ -468,13 +486,20 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir) => new Promise(
|
|
|
468
486
|
if (isStepHeader(ln, trimmed)) {
|
|
469
487
|
if (trimmed === currentStepHeader) return;
|
|
470
488
|
succeedCurrentStep();
|
|
489
|
+
if (state.setupReached || trimmed.startsWith("Cleaning"))
|
|
490
|
+
return;
|
|
471
491
|
currentStepHeader = trimmed;
|
|
472
492
|
stepItems = [];
|
|
473
493
|
stepFileCount = 0;
|
|
494
|
+
if (trimmed.startsWith("Running setup")) {
|
|
495
|
+
state.setupReached = true;
|
|
496
|
+
}
|
|
474
497
|
stepper.start(trimmed, stepCategory(trimmed));
|
|
475
|
-
} else
|
|
498
|
+
} else {
|
|
476
499
|
stepItems.push(trimmed);
|
|
477
|
-
|
|
500
|
+
if (currentStepHeader.startsWith("Integrating")) {
|
|
501
|
+
stepper.item(trimmed);
|
|
502
|
+
}
|
|
478
503
|
}
|
|
479
504
|
});
|
|
480
505
|
}
|
|
@@ -497,7 +522,9 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir) => new Promise(
|
|
|
497
522
|
}
|
|
498
523
|
});
|
|
499
524
|
});
|
|
500
|
-
|
|
525
|
+
let stderrBuffer = "";
|
|
526
|
+
child.stderr.on("data", (chunk) => {
|
|
527
|
+
stderrBuffer += chunk.toString();
|
|
501
528
|
});
|
|
502
529
|
child.on("error", (err) => {
|
|
503
530
|
if ("code" in err && err.code === "ENOENT") {
|
|
@@ -514,13 +541,20 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir) => new Promise(
|
|
|
514
541
|
if (currentStepHeader) {
|
|
515
542
|
succeedCurrentStep();
|
|
516
543
|
}
|
|
517
|
-
|
|
544
|
+
state.doneReceived || resultText.length > 0;
|
|
545
|
+
const stderr = stderrBuffer.trim();
|
|
518
546
|
if (code !== 0 && code !== null) {
|
|
519
|
-
|
|
547
|
+
const detail = stderr ? `
|
|
548
|
+
${stderr}` : "";
|
|
549
|
+
reject(new Error(`Claude CLI exited with code ${code}${detail}`));
|
|
520
550
|
return;
|
|
521
551
|
}
|
|
522
|
-
if (code === null
|
|
523
|
-
|
|
552
|
+
if (code === null) {
|
|
553
|
+
const detail = stderr ? `
|
|
554
|
+
${stderr}` : "";
|
|
555
|
+
reject(
|
|
556
|
+
new Error(`Claude CLI was interrupted before completing${detail}`)
|
|
557
|
+
);
|
|
524
558
|
return;
|
|
525
559
|
}
|
|
526
560
|
resolve2({
|
|
@@ -530,7 +564,12 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir) => new Promise(
|
|
|
530
564
|
});
|
|
531
565
|
});
|
|
532
566
|
});
|
|
533
|
-
const installSkillset = (input, stepper) => spawnClaude(
|
|
567
|
+
const installSkillset = (input, stepper) => spawnClaude(
|
|
568
|
+
writeInstructionsFile(input),
|
|
569
|
+
stepper,
|
|
570
|
+
input.providerDir,
|
|
571
|
+
input.model
|
|
572
|
+
);
|
|
534
573
|
const hash = (content) => createHash("sha256").update(content).digest("hex");
|
|
535
574
|
const collectFiles = (dir) => readdirSync(dir).flatMap((entry) => {
|
|
536
575
|
const fullPath = join(dir, entry);
|
|
@@ -779,12 +818,21 @@ const renderTree = (node, prefix = "") => node.children.flatMap((child, i) => {
|
|
|
779
818
|
const name = isDir ? `${child.name}/` : child.name;
|
|
780
819
|
return [`${prefix}${connector} ${name}`, ...renderTree(child, childPrefix)];
|
|
781
820
|
});
|
|
782
|
-
const
|
|
821
|
+
const stripProviderPrefix = (file, prefix) => {
|
|
822
|
+
const norm = prefix.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
823
|
+
if (file.startsWith(`${norm}/`)) return file.slice(norm.length + 1);
|
|
824
|
+
const bare = norm.replace(/^\./, "");
|
|
825
|
+
if (bare && file.startsWith(`${bare}/`)) return file.slice(bare.length + 1);
|
|
826
|
+
return file;
|
|
827
|
+
};
|
|
828
|
+
const printSummary = (files, providerPath) => {
|
|
783
829
|
if (files.length === 0) return;
|
|
784
|
-
const unique = [...new Set(files)]
|
|
830
|
+
const unique = [...new Set(files)].map(
|
|
831
|
+
(f) => stripProviderPrefix(f, providerPath)
|
|
832
|
+
);
|
|
785
833
|
const tree = buildFileTree(unique);
|
|
786
834
|
process.stdout.write(`
|
|
787
|
-
|
|
835
|
+
📂${providerPath}
|
|
788
836
|
`);
|
|
789
837
|
renderTree(tree).forEach((line) => {
|
|
790
838
|
process.stdout.write(` ${dim$1}${line}${reset$2}
|
|
@@ -796,7 +844,7 @@ const registerInstallCommand = (program2) => {
|
|
|
796
844
|
const stepper = createStepper();
|
|
797
845
|
const startedAt = Date.now();
|
|
798
846
|
try {
|
|
799
|
-
|
|
847
|
+
readConfig();
|
|
800
848
|
stepper.start("Resolving endpoint...", "packages");
|
|
801
849
|
const location = await resolveIdentifier(input);
|
|
802
850
|
const locationRef = `${location.owner}/${location.repository}@${location.ref}`;
|
|
@@ -807,15 +855,13 @@ const registerInstallCommand = (program2) => {
|
|
|
807
855
|
`Fetched skillset manifest`,
|
|
808
856
|
`${skillset.name} v${skillset.version}`
|
|
809
857
|
);
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
);
|
|
813
|
-
if (!providerEntry) {
|
|
858
|
+
const providerPath = knownProviders[skillset.provider];
|
|
859
|
+
if (!providerPath) {
|
|
814
860
|
throw new Error(
|
|
815
|
-
`
|
|
861
|
+
`Unknown provider "${skillset.provider}". Known providers: ${Object.keys(knownProviders).join(", ")}`
|
|
816
862
|
);
|
|
817
863
|
}
|
|
818
|
-
const
|
|
864
|
+
const provider = { path: providerPath };
|
|
819
865
|
const entries = resolveSkillset(skillset, location);
|
|
820
866
|
if (entries.length === 0) {
|
|
821
867
|
stepper.succeed("No files to download");
|
|
@@ -824,20 +870,26 @@ const registerInstallCommand = (program2) => {
|
|
|
824
870
|
}
|
|
825
871
|
const projectRoot = getProjectRoot();
|
|
826
872
|
const downloadDir = join(projectRoot, ".spm", skillset.name);
|
|
827
|
-
const
|
|
828
|
-
|
|
873
|
+
const toTargetPath = (entry) => {
|
|
874
|
+
if (entry.type === "skill" && entry.skillName) {
|
|
875
|
+
const fileName = entry.path.split("/").pop() ?? entry.path;
|
|
876
|
+
return `skills/${entry.skillName}/${fileName}`;
|
|
877
|
+
}
|
|
878
|
+
const skillsetDir = location.path.replace(/\/[^/]+$/, "");
|
|
879
|
+
return entry.path.startsWith(`${skillsetDir}/`) ? entry.path.slice(skillsetDir.length + 1) : entry.path;
|
|
880
|
+
};
|
|
829
881
|
stepper.start(`Downloading ${entries.length} file(s)...`, "packages");
|
|
830
882
|
const results = await downloadEntries(
|
|
831
883
|
entries,
|
|
832
884
|
location,
|
|
833
|
-
(type, path) => stepper.item(`${type} ${
|
|
885
|
+
(type, path) => stepper.item(`${type} ${path.split("/").pop() ?? path}`)
|
|
834
886
|
);
|
|
835
887
|
const setupResults = results.filter((r) => r.type === "setup");
|
|
836
888
|
const installResults = results.filter((r) => r.type !== "setup");
|
|
837
889
|
await Promise.all(
|
|
838
890
|
installResults.map(async (result2) => {
|
|
839
|
-
const
|
|
840
|
-
const filePath = safePath(downloadDir,
|
|
891
|
+
const target = toTargetPath(result2);
|
|
892
|
+
const filePath = safePath(downloadDir, target);
|
|
841
893
|
await mkdir(dirname(filePath), { recursive: true });
|
|
842
894
|
await writeFile(filePath, result2.content, "utf-8");
|
|
843
895
|
})
|
|
@@ -845,30 +897,32 @@ const registerInstallCommand = (program2) => {
|
|
|
845
897
|
let setupFile;
|
|
846
898
|
if (setupResults.length > 0) {
|
|
847
899
|
const setup = setupResults[0];
|
|
848
|
-
|
|
849
|
-
setupFile = join(downloadDir, relative2);
|
|
900
|
+
setupFile = join(downloadDir, "SETUP.md");
|
|
850
901
|
await mkdir(dirname(setupFile), { recursive: true });
|
|
851
902
|
await writeFile(setupFile, setup.content, "utf-8");
|
|
852
903
|
}
|
|
853
904
|
stepper.succeed(`Downloaded ${results.length} file(s)`);
|
|
854
|
-
const downloadedPaths = installResults.map((r) =>
|
|
905
|
+
const downloadedPaths = installResults.map((r) => toTargetPath(r));
|
|
855
906
|
const providerFullPath = join(projectRoot, provider.path);
|
|
856
907
|
const pruned = pruneUnchanged(downloadDir, providerFullPath);
|
|
857
908
|
if (pruned > 0) {
|
|
858
909
|
stepper.succeed(`Skipped ${pruned} unchanged file(s)`);
|
|
859
910
|
}
|
|
911
|
+
const model = skillset.provider === "claude" ? "haiku" : void 0;
|
|
860
912
|
const result = await installSkillset(
|
|
861
913
|
{
|
|
862
914
|
downloadDir,
|
|
863
915
|
setupFile,
|
|
864
|
-
providerDir:
|
|
916
|
+
providerDir: providerFullPath,
|
|
865
917
|
skillsetName: skillset.name,
|
|
866
918
|
skillsetVersion: skillset.version,
|
|
867
919
|
source: `@${location.owner}/${location.repository}`,
|
|
868
|
-
configPath: getConfigPath()
|
|
920
|
+
configPath: getConfigPath(),
|
|
921
|
+
model
|
|
869
922
|
},
|
|
870
923
|
stepper
|
|
871
924
|
);
|
|
925
|
+
await rm(downloadDir, { recursive: true, force: true });
|
|
872
926
|
stepper.stop();
|
|
873
927
|
const elapsed = Math.round((Date.now() - startedAt) / 1e3);
|
|
874
928
|
const timeStr = elapsed >= 60 ? `${Math.floor(elapsed / 60)}m${(elapsed % 60).toString().padStart(2, "0")}s` : `${elapsed}s`;
|
|
@@ -877,7 +931,12 @@ const registerInstallCommand = (program2) => {
|
|
|
877
931
|
`
|
|
878
932
|
);
|
|
879
933
|
const summaryFiles = result.files.length > 0 ? result.files : downloadedPaths;
|
|
880
|
-
printSummary(summaryFiles);
|
|
934
|
+
printSummary(summaryFiles, provider.path);
|
|
935
|
+
process.stdout.write(
|
|
936
|
+
`
|
|
937
|
+
🪄 ${cyan}Restart your AI agent to apply the new skills.${reset$2}
|
|
938
|
+
`
|
|
939
|
+
);
|
|
881
940
|
} catch (err) {
|
|
882
941
|
const message = err instanceof Error ? err.message : String(err);
|
|
883
942
|
stepper.fail(message);
|
|
@@ -901,7 +960,7 @@ const banner = (version2) => [
|
|
|
901
960
|
`${shade}▝▜${light}████▛▘ ${reset$1}${dim}v${version2} ✨ supa-magic${reset$1}`,
|
|
902
961
|
`${shade} ▘${light} ▝`
|
|
903
962
|
].join("\n");
|
|
904
|
-
const version = "0.2.
|
|
963
|
+
const version = "0.2.4";
|
|
905
964
|
const program = new Command();
|
|
906
965
|
const gray = "\x1B[90m";
|
|
907
966
|
const reset = "\x1B[0m";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supa-magic/spm",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "CLI tool for managing AI skillsets",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"contributors": [
|
|
@@ -28,11 +28,14 @@
|
|
|
28
28
|
},
|
|
29
29
|
"scripts": {
|
|
30
30
|
"build": "vite build",
|
|
31
|
+
"install": "npm install @supa-magic/spm",
|
|
32
|
+
"link": "npm link",
|
|
31
33
|
"pretest": "npm run build",
|
|
32
34
|
"start": "clear && claude --dangerously-skip-permissions",
|
|
33
35
|
"test": "vitest run",
|
|
34
36
|
"test:watch": "vitest",
|
|
35
|
-
"
|
|
37
|
+
"un": "npm uninstall @supa-magic/spm",
|
|
38
|
+
"unlink": "npm unlink spm"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"@biomejs/biome": "2.4.6",
|
|
@@ -43,6 +46,7 @@
|
|
|
43
46
|
"vitest": "^4.0.18"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
49
|
+
"@supa-magic/spm": "^0.2.3",
|
|
46
50
|
"commander": "^14.0.3",
|
|
47
51
|
"yaml": "^2.8.2"
|
|
48
52
|
}
|