@supa-magic/spm 0.2.3 → 0.2.5

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.
Files changed (2) hide show
  1. package/dist/bin/spm.js +48 -17
  2. package/package.json +4 -3
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, rm } from "node:fs/promises";
6
+ import { execFileSync, execSync, spawn } from "node:child_process";
7
+ import { readFile, mkdir, writeFile, rm, readdir } 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 fullPath = join(root, path);
26
- if (existsSync(fullPath)) {
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;
@@ -436,7 +454,7 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir, model) => new P
436
454
  return;
437
455
  }
438
456
  if (state.setupReached || currentStepHeader.startsWith("Cleaning")) return;
439
- const context = stepItems.length === 1 ? stepItems[0] : void 0;
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");
@@ -477,9 +495,11 @@ const spawnClaude = (instructionsFilePath, stepper, providerDir, model) => new P
477
495
  state.setupReached = true;
478
496
  }
479
497
  stepper.start(trimmed, stepCategory(trimmed));
480
- } else if (!currentStepHeader.startsWith("Integrating")) {
498
+ } else {
481
499
  stepItems.push(trimmed);
482
- stepper.item(trimmed);
500
+ if (currentStepHeader.startsWith("Integrating")) {
501
+ stepper.item(trimmed);
502
+ }
483
503
  }
484
504
  });
485
505
  }
@@ -800,7 +820,10 @@ const renderTree = (node, prefix = "") => node.children.flatMap((child, i) => {
800
820
  });
801
821
  const stripProviderPrefix = (file, prefix) => {
802
822
  const norm = prefix.replace(/\\/g, "/").replace(/^\.\//, "");
803
- return file.startsWith(`${norm}/`) ? file.slice(norm.length + 1) : file;
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;
804
827
  };
805
828
  const printSummary = (files, providerPath) => {
806
829
  if (files.length === 0) return;
@@ -847,20 +870,26 @@ const registerInstallCommand = (program2) => {
847
870
  }
848
871
  const projectRoot = getProjectRoot();
849
872
  const downloadDir = join(projectRoot, ".spm", skillset.name);
850
- const skillsetDir = location.path.replace(/\/[^/]+$/, "");
851
- const stripPrefix = (p) => p.startsWith(`${skillsetDir}/`) ? p.slice(skillsetDir.length + 1) : p;
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
+ };
852
881
  stepper.start(`Downloading ${entries.length} file(s)...`, "packages");
853
882
  const results = await downloadEntries(
854
883
  entries,
855
884
  location,
856
- (type, path) => stepper.item(`${type} ${stripPrefix(path)}`)
885
+ (type, path) => stepper.item(`${type} ${path.split("/").pop() ?? path}`)
857
886
  );
858
887
  const setupResults = results.filter((r) => r.type === "setup");
859
888
  const installResults = results.filter((r) => r.type !== "setup");
860
889
  await Promise.all(
861
890
  installResults.map(async (result2) => {
862
- const relative2 = stripPrefix(result2.path);
863
- const filePath = safePath(downloadDir, relative2);
891
+ const target = toTargetPath(result2);
892
+ const filePath = safePath(downloadDir, target);
864
893
  await mkdir(dirname(filePath), { recursive: true });
865
894
  await writeFile(filePath, result2.content, "utf-8");
866
895
  })
@@ -868,13 +897,12 @@ const registerInstallCommand = (program2) => {
868
897
  let setupFile;
869
898
  if (setupResults.length > 0) {
870
899
  const setup = setupResults[0];
871
- const relative2 = stripPrefix(setup.path);
872
- setupFile = join(downloadDir, relative2);
900
+ setupFile = join(downloadDir, "SETUP.md");
873
901
  await mkdir(dirname(setupFile), { recursive: true });
874
902
  await writeFile(setupFile, setup.content, "utf-8");
875
903
  }
876
904
  stepper.succeed(`Downloaded ${results.length} file(s)`);
877
- const downloadedPaths = installResults.map((r) => stripPrefix(r.path));
905
+ const downloadedPaths = installResults.map((r) => toTargetPath(r));
878
906
  const providerFullPath = join(projectRoot, provider.path);
879
907
  const pruned = pruneUnchanged(downloadDir, providerFullPath);
880
908
  if (pruned > 0) {
@@ -895,6 +923,9 @@ const registerInstallCommand = (program2) => {
895
923
  stepper
896
924
  );
897
925
  await rm(downloadDir, { recursive: true, force: true });
926
+ const spmDir = join(projectRoot, ".spm");
927
+ const remaining = await readdir(spmDir).catch(() => []);
928
+ if (remaining.length === 0) await rm(spmDir, { force: true });
898
929
  stepper.stop();
899
930
  const elapsed = Math.round((Date.now() - startedAt) / 1e3);
900
931
  const timeStr = elapsed >= 60 ? `${Math.floor(elapsed / 60)}m${(elapsed % 60).toString().padStart(2, "0")}s` : `${elapsed}s`;
@@ -932,7 +963,7 @@ const banner = (version2) => [
932
963
  `${shade}▝▜${light}████▛▘ ${reset$1}${dim}v${version2} ✨ supa-magic${reset$1}`,
933
964
  `${shade} ▘${light} ▝`
934
965
  ].join("\n");
935
- const version = "0.2.3";
966
+ const version = "0.2.5";
936
967
  const program = new Command();
937
968
  const gray = "\x1B[90m";
938
969
  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",
3
+ "version": "0.2.5",
4
4
  "description": "CLI tool for managing AI skillsets",
5
5
  "license": "MIT",
6
6
  "contributors": [
@@ -28,17 +28,18 @@
28
28
  },
29
29
  "scripts": {
30
30
  "build": "vite build",
31
+ "link": "npm link",
31
32
  "pretest": "npm run build",
32
33
  "start": "clear && claude --dangerously-skip-permissions",
33
34
  "test": "vitest run",
34
35
  "test:watch": "vitest",
35
- "tokscale": "npx tokscale@latest"
36
+ "un": "npm uninstall @supa-magic/spm",
37
+ "unlink": "npm unlink spm"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@biomejs/biome": "2.4.6",
39
41
  "@types/node": "^25.4.0",
40
42
  "typescript": "5.8.3",
41
- "typescript-language-server": "^5.1.3",
42
43
  "vite": "^7.3.1",
43
44
  "vitest": "^4.0.18"
44
45
  },