md4ai 0.3.2 → 0.5.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/dist/index.bundled.js +622 -60
- package/package.json +1 -2
package/dist/index.bundled.js
CHANGED
|
@@ -319,14 +319,14 @@ ${deviceName}`) + chalk7.dim(` (${first.os_type})`));
|
|
|
319
319
|
// dist/commands/map.js
|
|
320
320
|
import { resolve as resolve3 } from "node:path";
|
|
321
321
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
322
|
-
import { existsSync as
|
|
322
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
323
323
|
import chalk9 from "chalk";
|
|
324
324
|
|
|
325
325
|
// dist/scanner/index.js
|
|
326
|
-
import { readdir } from "node:fs/promises";
|
|
327
|
-
import { join as
|
|
328
|
-
import { existsSync as
|
|
329
|
-
import { homedir as
|
|
326
|
+
import { readdir as readdir2 } from "node:fs/promises";
|
|
327
|
+
import { join as join7, relative } from "node:path";
|
|
328
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
329
|
+
import { homedir as homedir5 } from "node:os";
|
|
330
330
|
import { createHash } from "node:crypto";
|
|
331
331
|
|
|
332
332
|
// dist/scanner/file-parser.js
|
|
@@ -582,9 +582,15 @@ function detectOrphans(allFiles, references, rootFiles, projectRoot) {
|
|
|
582
582
|
sizeBytes = statSync3(fullPath).size;
|
|
583
583
|
} catch {
|
|
584
584
|
}
|
|
585
|
+
let createdAt = null;
|
|
586
|
+
try {
|
|
587
|
+
createdAt = statSync3(fullPath).birthtime.toISOString();
|
|
588
|
+
} catch {
|
|
589
|
+
}
|
|
585
590
|
return {
|
|
586
591
|
path: f,
|
|
587
592
|
lastModified: getGitLastModified(fullPath, projectRoot),
|
|
593
|
+
createdAt,
|
|
588
594
|
sizeBytes
|
|
589
595
|
};
|
|
590
596
|
});
|
|
@@ -675,13 +681,253 @@ async function parseSettingsForPlugins(settingsPath, skills, isMachineWide) {
|
|
|
675
681
|
}
|
|
676
682
|
}
|
|
677
683
|
|
|
684
|
+
// dist/scanner/tooling-detector.js
|
|
685
|
+
import { readFile as readFile4, readdir } from "node:fs/promises";
|
|
686
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
687
|
+
import { join as join6 } from "node:path";
|
|
688
|
+
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
689
|
+
import { homedir as homedir4 } from "node:os";
|
|
690
|
+
var CLI_VERSION_COMMANDS = [
|
|
691
|
+
{ name: "node", command: "node", args: ["--version"] },
|
|
692
|
+
{ name: "npm", command: "npm", args: ["--version"] },
|
|
693
|
+
{ name: "pnpm", command: "pnpm", args: ["--version"], conditionFile: "pnpm-lock.yaml" },
|
|
694
|
+
{ name: "supabase-cli", command: "npx", args: ["supabase", "--version"] },
|
|
695
|
+
{ name: "claude-code", command: "claude", args: ["--version"] }
|
|
696
|
+
];
|
|
697
|
+
async function detectToolings(projectRoot) {
|
|
698
|
+
const toolings = [];
|
|
699
|
+
const pkgToolings = await detectFromPackageJson(projectRoot);
|
|
700
|
+
toolings.push(...pkgToolings);
|
|
701
|
+
const cliToolings = detectFromCli(projectRoot);
|
|
702
|
+
toolings.push(...cliToolings);
|
|
703
|
+
const mcpToolings = await detectFromMcpSettings(projectRoot);
|
|
704
|
+
toolings.push(...mcpToolings);
|
|
705
|
+
return toolings;
|
|
706
|
+
}
|
|
707
|
+
async function detectFromPackageJson(projectRoot) {
|
|
708
|
+
const toolings = [];
|
|
709
|
+
const pkgPath = join6(projectRoot, "package.json");
|
|
710
|
+
if (!existsSync4(pkgPath))
|
|
711
|
+
return toolings;
|
|
712
|
+
const resolvedVersions = await getResolvedVersions(projectRoot);
|
|
713
|
+
const seen = /* @__PURE__ */ new Set();
|
|
714
|
+
const pkgPaths = [pkgPath];
|
|
715
|
+
const workspacePaths = await discoverWorkspacePackageJsons(projectRoot);
|
|
716
|
+
pkgPaths.push(...workspacePaths);
|
|
717
|
+
for (const path of pkgPaths) {
|
|
718
|
+
try {
|
|
719
|
+
const content = await readFile4(path, "utf-8");
|
|
720
|
+
const pkg = JSON.parse(content);
|
|
721
|
+
const allDeps = {
|
|
722
|
+
...pkg.dependencies ?? {},
|
|
723
|
+
...pkg.devDependencies ?? {}
|
|
724
|
+
};
|
|
725
|
+
for (const [name, specifier] of Object.entries(allDeps)) {
|
|
726
|
+
if (seen.has(name))
|
|
727
|
+
continue;
|
|
728
|
+
seen.add(name);
|
|
729
|
+
if (specifier.startsWith("workspace:"))
|
|
730
|
+
continue;
|
|
731
|
+
const resolved = resolvedVersions.get(name);
|
|
732
|
+
const version = resolved ?? stripVersionPrefix(specifier);
|
|
733
|
+
toolings.push({
|
|
734
|
+
tool_name: name,
|
|
735
|
+
detected_version: version,
|
|
736
|
+
detection_source: "package.json"
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
} catch {
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return toolings;
|
|
743
|
+
}
|
|
744
|
+
async function discoverWorkspacePackageJsons(projectRoot) {
|
|
745
|
+
const patterns = await getWorkspacePatterns(projectRoot);
|
|
746
|
+
if (patterns.length === 0)
|
|
747
|
+
return [];
|
|
748
|
+
const results = [];
|
|
749
|
+
for (const pattern of patterns) {
|
|
750
|
+
const isGlob = /\/\*\*?$/.test(pattern) || pattern.includes("*");
|
|
751
|
+
const cleanPattern = pattern.replace(/\/\*\*?$/, "");
|
|
752
|
+
if (isGlob) {
|
|
753
|
+
const parentDir = join6(projectRoot, cleanPattern);
|
|
754
|
+
if (!existsSync4(parentDir))
|
|
755
|
+
continue;
|
|
756
|
+
try {
|
|
757
|
+
const entries = await readdir(parentDir, { withFileTypes: true });
|
|
758
|
+
for (const entry of entries) {
|
|
759
|
+
if (!entry.isDirectory())
|
|
760
|
+
continue;
|
|
761
|
+
const pkgPath = join6(parentDir, entry.name, "package.json");
|
|
762
|
+
if (existsSync4(pkgPath)) {
|
|
763
|
+
results.push(pkgPath);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
} catch {
|
|
767
|
+
}
|
|
768
|
+
} else {
|
|
769
|
+
const pkgPath = join6(projectRoot, cleanPattern, "package.json");
|
|
770
|
+
if (existsSync4(pkgPath)) {
|
|
771
|
+
results.push(pkgPath);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return results;
|
|
776
|
+
}
|
|
777
|
+
async function getWorkspacePatterns(projectRoot) {
|
|
778
|
+
const pnpmWorkspace = join6(projectRoot, "pnpm-workspace.yaml");
|
|
779
|
+
if (existsSync4(pnpmWorkspace)) {
|
|
780
|
+
try {
|
|
781
|
+
const content = await readFile4(pnpmWorkspace, "utf-8");
|
|
782
|
+
const patterns = [];
|
|
783
|
+
let inPackages = false;
|
|
784
|
+
for (const line of content.split("\n")) {
|
|
785
|
+
const trimmed = line.trim();
|
|
786
|
+
if (trimmed === "packages:") {
|
|
787
|
+
inPackages = true;
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
if (inPackages) {
|
|
791
|
+
if (trimmed.startsWith("- ")) {
|
|
792
|
+
patterns.push(trimmed.slice(2).replace(/^["']|["']$/g, ""));
|
|
793
|
+
} else if (trimmed && !trimmed.startsWith("#")) {
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (patterns.length > 0)
|
|
799
|
+
return patterns;
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const pkgPath = join6(projectRoot, "package.json");
|
|
804
|
+
try {
|
|
805
|
+
const content = await readFile4(pkgPath, "utf-8");
|
|
806
|
+
const pkg = JSON.parse(content);
|
|
807
|
+
const workspaces = pkg.workspaces;
|
|
808
|
+
if (Array.isArray(workspaces))
|
|
809
|
+
return workspaces;
|
|
810
|
+
if (workspaces?.packages && Array.isArray(workspaces.packages))
|
|
811
|
+
return workspaces.packages;
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
function stripVersionPrefix(version) {
|
|
817
|
+
return version.replace(/^[\^~>=<]+/, "").split(" ")[0];
|
|
818
|
+
}
|
|
819
|
+
async function getResolvedVersions(projectRoot) {
|
|
820
|
+
const versions = /* @__PURE__ */ new Map();
|
|
821
|
+
const pnpmLock = join6(projectRoot, "pnpm-lock.yaml");
|
|
822
|
+
if (existsSync4(pnpmLock)) {
|
|
823
|
+
try {
|
|
824
|
+
const content = await readFile4(pnpmLock, "utf-8");
|
|
825
|
+
const versionPattern = /^\s{4}'?(@?[^@\s:]+)(?:@[^:]+)?'?:\s*(?:version:\s*)?'?(\d+\.\d+[^'\s]*)/gm;
|
|
826
|
+
let match;
|
|
827
|
+
while ((match = versionPattern.exec(content)) !== null) {
|
|
828
|
+
const [, name, version] = match;
|
|
829
|
+
if (name && version && !versions.has(name)) {
|
|
830
|
+
versions.set(name, version);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
} catch {
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (versions.size === 0) {
|
|
837
|
+
const npmLock = join6(projectRoot, "package-lock.json");
|
|
838
|
+
if (existsSync4(npmLock)) {
|
|
839
|
+
try {
|
|
840
|
+
const content = await readFile4(npmLock, "utf-8");
|
|
841
|
+
const lock = JSON.parse(content);
|
|
842
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
843
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
844
|
+
const name = key.replace(/^node_modules\//, "");
|
|
845
|
+
const ver = value.version;
|
|
846
|
+
if (name && ver && !versions.has(name)) {
|
|
847
|
+
versions.set(name, ver);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return versions;
|
|
855
|
+
}
|
|
856
|
+
function detectFromCli(projectRoot) {
|
|
857
|
+
const toolings = [];
|
|
858
|
+
for (const { name, command, args, conditionFile } of CLI_VERSION_COMMANDS) {
|
|
859
|
+
if (conditionFile && !existsSync4(join6(projectRoot, conditionFile))) {
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
try {
|
|
863
|
+
const output = execFileSync3(command, args, {
|
|
864
|
+
encoding: "utf-8",
|
|
865
|
+
timeout: 3e3,
|
|
866
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
867
|
+
}).trim();
|
|
868
|
+
const version = output.replace(/^v/, "").split("\n")[0].trim();
|
|
869
|
+
if (version) {
|
|
870
|
+
toolings.push({
|
|
871
|
+
tool_name: name,
|
|
872
|
+
detected_version: version,
|
|
873
|
+
detection_source: "cli"
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
} catch {
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return toolings;
|
|
880
|
+
}
|
|
881
|
+
async function detectFromMcpSettings(projectRoot) {
|
|
882
|
+
const toolings = [];
|
|
883
|
+
const settingsPaths = [
|
|
884
|
+
join6(projectRoot, ".claude", "settings.json"),
|
|
885
|
+
join6(projectRoot, ".claude", "settings.local.json"),
|
|
886
|
+
join6(homedir4(), ".claude", "settings.json")
|
|
887
|
+
];
|
|
888
|
+
const seen = /* @__PURE__ */ new Set();
|
|
889
|
+
for (const settingsPath of settingsPaths) {
|
|
890
|
+
if (!existsSync4(settingsPath))
|
|
891
|
+
continue;
|
|
892
|
+
try {
|
|
893
|
+
const content = await readFile4(settingsPath, "utf-8");
|
|
894
|
+
const settings = JSON.parse(content);
|
|
895
|
+
const mcpServers = settings.mcpServers ?? {};
|
|
896
|
+
for (const serverName of Object.keys(mcpServers)) {
|
|
897
|
+
if (seen.has(serverName))
|
|
898
|
+
continue;
|
|
899
|
+
seen.add(serverName);
|
|
900
|
+
let version = null;
|
|
901
|
+
const config = mcpServers[serverName];
|
|
902
|
+
if (config?.command && typeof config.command === "string") {
|
|
903
|
+
const args = config.args ?? [];
|
|
904
|
+
for (const arg of args) {
|
|
905
|
+
const versionMatch = arg.match(/@(\d+\.\d+[^\s]*?)$/);
|
|
906
|
+
if (versionMatch) {
|
|
907
|
+
version = versionMatch[1];
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
toolings.push({
|
|
913
|
+
tool_name: serverName,
|
|
914
|
+
detected_version: version,
|
|
915
|
+
detection_source: "mcp_settings"
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
} catch {
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return toolings;
|
|
922
|
+
}
|
|
923
|
+
|
|
678
924
|
// dist/scanner/index.js
|
|
679
925
|
async function scanProject(projectRoot) {
|
|
680
926
|
const allFiles = await discoverFiles(projectRoot);
|
|
681
927
|
const rootFiles = identifyRoots(allFiles, projectRoot);
|
|
682
928
|
const allRefs = [];
|
|
683
929
|
for (const file of allFiles) {
|
|
684
|
-
const fullPath = file.startsWith("/") ? file :
|
|
930
|
+
const fullPath = file.startsWith("/") ? file : join7(projectRoot, file);
|
|
685
931
|
try {
|
|
686
932
|
const refs = await parseFileReferences(fullPath, projectRoot);
|
|
687
933
|
allRefs.push(...refs);
|
|
@@ -692,39 +938,41 @@ async function scanProject(projectRoot) {
|
|
|
692
938
|
const orphans = detectOrphans(allFiles, allRefs, rootFiles, projectRoot);
|
|
693
939
|
const staleFiles = detectStaleFiles(allFiles, projectRoot);
|
|
694
940
|
const skills = await parseSkills(projectRoot);
|
|
695
|
-
const
|
|
941
|
+
const toolings = await detectToolings(projectRoot);
|
|
942
|
+
const scanData = JSON.stringify({ graph, orphans, skills, staleFiles, toolings });
|
|
696
943
|
const dataHash = createHash("sha256").update(scanData).digest("hex");
|
|
697
944
|
return {
|
|
698
945
|
graph,
|
|
699
946
|
orphans,
|
|
700
947
|
skills,
|
|
701
948
|
staleFiles,
|
|
949
|
+
toolings,
|
|
702
950
|
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
703
951
|
dataHash
|
|
704
952
|
};
|
|
705
953
|
}
|
|
706
954
|
async function discoverFiles(projectRoot) {
|
|
707
955
|
const files = [];
|
|
708
|
-
const claudeDir =
|
|
709
|
-
if (
|
|
956
|
+
const claudeDir = join7(projectRoot, ".claude");
|
|
957
|
+
if (existsSync5(claudeDir)) {
|
|
710
958
|
await walkDir(claudeDir, projectRoot, files);
|
|
711
959
|
}
|
|
712
|
-
if (
|
|
960
|
+
if (existsSync5(join7(projectRoot, "CLAUDE.md"))) {
|
|
713
961
|
files.push("CLAUDE.md");
|
|
714
962
|
}
|
|
715
|
-
if (
|
|
963
|
+
if (existsSync5(join7(projectRoot, "skills.md"))) {
|
|
716
964
|
files.push("skills.md");
|
|
717
965
|
}
|
|
718
|
-
const plansDir =
|
|
719
|
-
if (
|
|
966
|
+
const plansDir = join7(projectRoot, "docs", "plans");
|
|
967
|
+
if (existsSync5(plansDir)) {
|
|
720
968
|
await walkDir(plansDir, projectRoot, files);
|
|
721
969
|
}
|
|
722
970
|
return [...new Set(files)];
|
|
723
971
|
}
|
|
724
972
|
async function walkDir(dir, projectRoot, files) {
|
|
725
|
-
const entries = await
|
|
973
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
726
974
|
for (const entry of entries) {
|
|
727
|
-
const fullPath =
|
|
975
|
+
const fullPath = join7(dir, entry.name);
|
|
728
976
|
if (entry.isDirectory()) {
|
|
729
977
|
if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
|
|
730
978
|
continue;
|
|
@@ -743,18 +991,17 @@ function identifyRoots(allFiles, projectRoot) {
|
|
|
743
991
|
}
|
|
744
992
|
}
|
|
745
993
|
for (const globalFile of GLOBAL_ROOT_FILES) {
|
|
746
|
-
const expanded = globalFile.replace("~",
|
|
747
|
-
if (
|
|
994
|
+
const expanded = globalFile.replace("~", homedir5());
|
|
995
|
+
if (existsSync5(expanded)) {
|
|
748
996
|
roots.push(globalFile);
|
|
749
997
|
}
|
|
750
998
|
}
|
|
751
999
|
return roots;
|
|
752
1000
|
}
|
|
753
1001
|
async function readClaudeConfigFiles(projectRoot) {
|
|
754
|
-
const { readFile:
|
|
755
|
-
const { join:
|
|
756
|
-
const { existsSync:
|
|
757
|
-
const { glob } = await import("glob");
|
|
1002
|
+
const { readFile: readFile7, stat, glob } = await import("node:fs/promises");
|
|
1003
|
+
const { join: join11, relative: relative2 } = await import("node:path");
|
|
1004
|
+
const { existsSync: existsSync10 } = await import("node:fs");
|
|
758
1005
|
const configPatterns = [
|
|
759
1006
|
"CLAUDE.md",
|
|
760
1007
|
".claude/CLAUDE.md",
|
|
@@ -764,17 +1011,15 @@ async function readClaudeConfigFiles(projectRoot) {
|
|
|
764
1011
|
];
|
|
765
1012
|
const files = [];
|
|
766
1013
|
for (const pattern of configPatterns) {
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const fullPath = join10(projectRoot, match);
|
|
770
|
-
if (!existsSync9(fullPath))
|
|
1014
|
+
for await (const fullPath of glob(join11(projectRoot, pattern))) {
|
|
1015
|
+
if (!existsSync10(fullPath))
|
|
771
1016
|
continue;
|
|
772
1017
|
try {
|
|
773
|
-
const content = await
|
|
1018
|
+
const content = await readFile7(fullPath, "utf-8");
|
|
774
1019
|
const fileStat = await stat(fullPath);
|
|
775
1020
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
776
1021
|
files.push({
|
|
777
|
-
filePath:
|
|
1022
|
+
filePath: relative2(projectRoot, fullPath),
|
|
778
1023
|
content,
|
|
779
1024
|
sizeBytes: fileStat.size,
|
|
780
1025
|
lastModified: lastMod
|
|
@@ -789,7 +1034,7 @@ function detectStaleFiles(allFiles, projectRoot) {
|
|
|
789
1034
|
const stale = [];
|
|
790
1035
|
const now = Date.now();
|
|
791
1036
|
for (const file of allFiles) {
|
|
792
|
-
const fullPath =
|
|
1037
|
+
const fullPath = join7(projectRoot, file);
|
|
793
1038
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
794
1039
|
if (lastMod) {
|
|
795
1040
|
const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
|
|
@@ -914,7 +1159,7 @@ function escapeHtml(text) {
|
|
|
914
1159
|
|
|
915
1160
|
// dist/check-update.js
|
|
916
1161
|
import chalk8 from "chalk";
|
|
917
|
-
var CURRENT_VERSION = "0.
|
|
1162
|
+
var CURRENT_VERSION = "0.5.0";
|
|
918
1163
|
async function checkForUpdate() {
|
|
919
1164
|
try {
|
|
920
1165
|
const controller = new AbortController();
|
|
@@ -957,11 +1202,30 @@ function isNewer(a, b) {
|
|
|
957
1202
|
return false;
|
|
958
1203
|
}
|
|
959
1204
|
|
|
1205
|
+
// dist/commands/push-toolings.js
|
|
1206
|
+
async function pushToolings(supabase, folderId, toolings) {
|
|
1207
|
+
if (!toolings.length)
|
|
1208
|
+
return;
|
|
1209
|
+
const { data: registry } = await supabase.from("tools_registry").select("id, name");
|
|
1210
|
+
const registryMap = new Map((registry ?? []).map((r) => [r.name, r.id]));
|
|
1211
|
+
for (const tooling of toolings) {
|
|
1212
|
+
const toolId = registryMap.get(tooling.tool_name) ?? null;
|
|
1213
|
+
await supabase.from("project_toolings").upsert({
|
|
1214
|
+
folder_id: folderId,
|
|
1215
|
+
tool_id: toolId,
|
|
1216
|
+
tool_name: tooling.tool_name,
|
|
1217
|
+
detected_version: tooling.detected_version,
|
|
1218
|
+
detection_source: tooling.detection_source,
|
|
1219
|
+
detected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1220
|
+
}, { onConflict: "folder_id,tool_name,detection_source" });
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
960
1224
|
// dist/commands/map.js
|
|
961
1225
|
async function mapCommand(path, options) {
|
|
962
1226
|
await checkForUpdate();
|
|
963
1227
|
const projectRoot = resolve3(path ?? process.cwd());
|
|
964
|
-
if (!
|
|
1228
|
+
if (!existsSync6(projectRoot)) {
|
|
965
1229
|
console.error(chalk9.red(`Path not found: ${projectRoot}`));
|
|
966
1230
|
process.exit(1);
|
|
967
1231
|
}
|
|
@@ -973,9 +1237,10 @@ async function mapCommand(path, options) {
|
|
|
973
1237
|
console.log(` Orphans: ${result.orphans.length}`);
|
|
974
1238
|
console.log(` Stale files: ${result.staleFiles.length}`);
|
|
975
1239
|
console.log(` Skills: ${result.skills.length}`);
|
|
1240
|
+
console.log(` Toolings: ${result.toolings.length}`);
|
|
976
1241
|
console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
|
|
977
1242
|
const outputDir = resolve3(projectRoot, "output");
|
|
978
|
-
if (!
|
|
1243
|
+
if (!existsSync6(outputDir)) {
|
|
979
1244
|
await mkdir2(outputDir, { recursive: true });
|
|
980
1245
|
}
|
|
981
1246
|
const htmlPath = resolve3(outputDir, "index.html");
|
|
@@ -1033,6 +1298,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
1033
1298
|
if (error) {
|
|
1034
1299
|
console.error(chalk9.yellow(`Sync warning: ${error.message}`));
|
|
1035
1300
|
} else {
|
|
1301
|
+
await pushToolings(supabase, folder_id, result.toolings);
|
|
1036
1302
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
|
|
1037
1303
|
await saveState({
|
|
1038
1304
|
lastFolderId: folder_id,
|
|
@@ -1067,42 +1333,42 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
1067
1333
|
}
|
|
1068
1334
|
|
|
1069
1335
|
// dist/commands/simulate.js
|
|
1070
|
-
import { join as
|
|
1071
|
-
import { existsSync as
|
|
1072
|
-
import { homedir as
|
|
1336
|
+
import { join as join8 } from "node:path";
|
|
1337
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1338
|
+
import { homedir as homedir6 } from "node:os";
|
|
1073
1339
|
import chalk10 from "chalk";
|
|
1074
1340
|
async function simulateCommand(prompt) {
|
|
1075
1341
|
const projectRoot = process.cwd();
|
|
1076
1342
|
console.log(chalk10.blue(`Simulating prompt: "${prompt}"
|
|
1077
1343
|
`));
|
|
1078
1344
|
console.log(chalk10.dim("Files Claude would load:\n"));
|
|
1079
|
-
const globalClaude =
|
|
1080
|
-
if (
|
|
1345
|
+
const globalClaude = join8(homedir6(), ".claude", "CLAUDE.md");
|
|
1346
|
+
if (existsSync7(globalClaude)) {
|
|
1081
1347
|
console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
|
|
1082
1348
|
}
|
|
1083
1349
|
for (const rootFile of ROOT_FILES) {
|
|
1084
|
-
const fullPath =
|
|
1085
|
-
if (
|
|
1350
|
+
const fullPath = join8(projectRoot, rootFile);
|
|
1351
|
+
if (existsSync7(fullPath)) {
|
|
1086
1352
|
console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
|
|
1087
1353
|
}
|
|
1088
1354
|
}
|
|
1089
1355
|
const settingsFiles = [
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1356
|
+
join8(homedir6(), ".claude", "settings.json"),
|
|
1357
|
+
join8(homedir6(), ".claude", "settings.local.json"),
|
|
1358
|
+
join8(projectRoot, ".claude", "settings.json"),
|
|
1359
|
+
join8(projectRoot, ".claude", "settings.local.json")
|
|
1094
1360
|
];
|
|
1095
1361
|
for (const sf of settingsFiles) {
|
|
1096
|
-
if (
|
|
1097
|
-
const display = sf.startsWith(
|
|
1362
|
+
if (existsSync7(sf)) {
|
|
1363
|
+
const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
|
|
1098
1364
|
console.log(chalk10.green(` \u2713 ${display} (settings)`));
|
|
1099
1365
|
}
|
|
1100
1366
|
}
|
|
1101
1367
|
const words = prompt.split(/\s+/);
|
|
1102
1368
|
for (const word of words) {
|
|
1103
1369
|
const cleaned = word.replace(/['"]/g, "");
|
|
1104
|
-
const candidatePath =
|
|
1105
|
-
if (
|
|
1370
|
+
const candidatePath = join8(projectRoot, cleaned);
|
|
1371
|
+
if (existsSync7(candidatePath) && cleaned.includes("/")) {
|
|
1106
1372
|
console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
|
|
1107
1373
|
}
|
|
1108
1374
|
}
|
|
@@ -1110,18 +1376,18 @@ async function simulateCommand(prompt) {
|
|
|
1110
1376
|
}
|
|
1111
1377
|
|
|
1112
1378
|
// dist/commands/print.js
|
|
1113
|
-
import { join as
|
|
1114
|
-
import { readFile as
|
|
1115
|
-
import { existsSync as
|
|
1379
|
+
import { join as join9 } from "node:path";
|
|
1380
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
1381
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1116
1382
|
import chalk11 from "chalk";
|
|
1117
1383
|
async function printCommand(title) {
|
|
1118
1384
|
const projectRoot = process.cwd();
|
|
1119
|
-
const scanDataPath =
|
|
1120
|
-
if (!
|
|
1385
|
+
const scanDataPath = join9(projectRoot, "output", "index.html");
|
|
1386
|
+
if (!existsSync8(scanDataPath)) {
|
|
1121
1387
|
console.error(chalk11.red("No scan data found. Run: md4ai scan"));
|
|
1122
1388
|
process.exit(1);
|
|
1123
1389
|
}
|
|
1124
|
-
const html = await
|
|
1390
|
+
const html = await readFile5(scanDataPath, "utf-8");
|
|
1125
1391
|
const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
|
|
1126
1392
|
if (!match) {
|
|
1127
1393
|
console.error(chalk11.red("Could not extract scan data from output/index.html"));
|
|
@@ -1129,7 +1395,7 @@ async function printCommand(title) {
|
|
|
1129
1395
|
}
|
|
1130
1396
|
const result = JSON.parse(match[1]);
|
|
1131
1397
|
const printHtml = generatePrintHtml(result, title);
|
|
1132
|
-
const outputPath =
|
|
1398
|
+
const outputPath = join9(projectRoot, "output", `print-${Date.now()}.html`);
|
|
1133
1399
|
await writeFile3(outputPath, printHtml, "utf-8");
|
|
1134
1400
|
console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
|
|
1135
1401
|
}
|
|
@@ -1213,6 +1479,7 @@ async function syncCommand(options) {
|
|
|
1213
1479
|
last_scanned: result.scannedAt,
|
|
1214
1480
|
data_hash: result.dataHash
|
|
1215
1481
|
}).eq("id", device.folder_id);
|
|
1482
|
+
await pushToolings(supabase, device.folder_id, result.toolings);
|
|
1216
1483
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
|
|
1217
1484
|
console.log(chalk12.green(` Done: ${device.device_name}`));
|
|
1218
1485
|
} catch (err) {
|
|
@@ -1246,6 +1513,7 @@ async function syncCommand(options) {
|
|
|
1246
1513
|
last_scanned: result.scannedAt,
|
|
1247
1514
|
data_hash: result.dataHash
|
|
1248
1515
|
}).eq("id", device.folder_id);
|
|
1516
|
+
await pushToolings(supabase, device.folder_id, result.toolings);
|
|
1249
1517
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
|
|
1250
1518
|
await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1251
1519
|
console.log(chalk12.green("Synced."));
|
|
@@ -1318,6 +1586,7 @@ Linking "${folder.name}" to this device...
|
|
|
1318
1586
|
console.log(` References:${result.graph.edges.length}`);
|
|
1319
1587
|
console.log(` Orphans: ${result.orphans.length}`);
|
|
1320
1588
|
console.log(` Skills: ${result.skills.length}`);
|
|
1589
|
+
console.log(` Toolings: ${result.toolings.length}`);
|
|
1321
1590
|
const { error: scanErr } = await supabase.from("claude_folders").update({
|
|
1322
1591
|
graph_json: result.graph,
|
|
1323
1592
|
orphans_json: result.orphans,
|
|
@@ -1329,6 +1598,7 @@ Linking "${folder.name}" to this device...
|
|
|
1329
1598
|
if (scanErr) {
|
|
1330
1599
|
console.error(chalk13.yellow(`Scan upload warning: ${scanErr.message}`));
|
|
1331
1600
|
}
|
|
1601
|
+
await pushToolings(supabase, folder.id, result.toolings);
|
|
1332
1602
|
const configFiles = await readClaudeConfigFiles(cwd);
|
|
1333
1603
|
if (configFiles.length > 0) {
|
|
1334
1604
|
for (const file of configFiles) {
|
|
@@ -1357,18 +1627,18 @@ Linking "${folder.name}" to this device...
|
|
|
1357
1627
|
}
|
|
1358
1628
|
|
|
1359
1629
|
// dist/commands/import-bundle.js
|
|
1360
|
-
import { readFile as
|
|
1361
|
-
import { join as
|
|
1362
|
-
import { existsSync as
|
|
1630
|
+
import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
|
|
1631
|
+
import { join as join10, dirname as dirname2 } from "node:path";
|
|
1632
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1363
1633
|
import chalk14 from "chalk";
|
|
1364
1634
|
import { confirm, input as input4 } from "@inquirer/prompts";
|
|
1365
1635
|
async function importBundleCommand(zipPath) {
|
|
1366
|
-
if (!
|
|
1636
|
+
if (!existsSync9(zipPath)) {
|
|
1367
1637
|
console.error(chalk14.red(`File not found: ${zipPath}`));
|
|
1368
1638
|
process.exit(1);
|
|
1369
1639
|
}
|
|
1370
1640
|
const JSZip = (await import("jszip")).default;
|
|
1371
|
-
const zipData = await
|
|
1641
|
+
const zipData = await readFile6(zipPath);
|
|
1372
1642
|
const zip = await JSZip.loadAsync(zipData);
|
|
1373
1643
|
const manifestFile = zip.file("manifest.json");
|
|
1374
1644
|
if (!manifestFile) {
|
|
@@ -1410,9 +1680,9 @@ Files to extract:`));
|
|
|
1410
1680
|
return;
|
|
1411
1681
|
}
|
|
1412
1682
|
for (const file of files) {
|
|
1413
|
-
const fullPath =
|
|
1683
|
+
const fullPath = join10(targetDir, file.filePath);
|
|
1414
1684
|
const dir = dirname2(fullPath);
|
|
1415
|
-
if (!
|
|
1685
|
+
if (!existsSync9(dir)) {
|
|
1416
1686
|
await mkdir3(dir, { recursive: true });
|
|
1417
1687
|
}
|
|
1418
1688
|
await writeFile4(fullPath, file.content, "utf-8");
|
|
@@ -1422,6 +1692,294 @@ Files to extract:`));
|
|
|
1422
1692
|
Done! ${files.length} file(s) extracted to ${targetDir}`));
|
|
1423
1693
|
}
|
|
1424
1694
|
|
|
1695
|
+
// dist/commands/admin-update-tool.js
|
|
1696
|
+
import chalk15 from "chalk";
|
|
1697
|
+
var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
|
|
1698
|
+
async function adminUpdateToolCommand(options) {
|
|
1699
|
+
const { supabase } = await getAuthenticatedClient();
|
|
1700
|
+
if (!options.name) {
|
|
1701
|
+
console.error(chalk15.red("--name is required."));
|
|
1702
|
+
process.exit(1);
|
|
1703
|
+
}
|
|
1704
|
+
if (options.category && !VALID_CATEGORIES.includes(options.category)) {
|
|
1705
|
+
console.error(chalk15.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
|
|
1706
|
+
process.exit(1);
|
|
1707
|
+
}
|
|
1708
|
+
const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
|
|
1709
|
+
if (existing) {
|
|
1710
|
+
const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1711
|
+
if (options.display)
|
|
1712
|
+
updates.display_name = options.display;
|
|
1713
|
+
if (options.category)
|
|
1714
|
+
updates.category = options.category;
|
|
1715
|
+
if (options.stable)
|
|
1716
|
+
updates.latest_stable = options.stable;
|
|
1717
|
+
if (options.beta)
|
|
1718
|
+
updates.latest_beta = options.beta;
|
|
1719
|
+
if (options.source)
|
|
1720
|
+
updates.source_url = options.source;
|
|
1721
|
+
if (options.install)
|
|
1722
|
+
updates.install_url = options.install;
|
|
1723
|
+
if (options.notes)
|
|
1724
|
+
updates.notes = options.notes;
|
|
1725
|
+
const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
|
|
1726
|
+
if (error) {
|
|
1727
|
+
console.error(chalk15.red(`Failed to update: ${error.message}`));
|
|
1728
|
+
process.exit(1);
|
|
1729
|
+
}
|
|
1730
|
+
console.log(chalk15.green(`Updated "${existing.display_name}" in the registry.`));
|
|
1731
|
+
} else {
|
|
1732
|
+
if (!options.display || !options.category) {
|
|
1733
|
+
console.error(chalk15.red("New tools require --display and --category."));
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
const { error } = await supabase.from("tools_registry").insert({
|
|
1737
|
+
name: options.name,
|
|
1738
|
+
display_name: options.display,
|
|
1739
|
+
category: options.category,
|
|
1740
|
+
latest_stable: options.stable ?? null,
|
|
1741
|
+
latest_beta: options.beta ?? null,
|
|
1742
|
+
source_url: options.source ?? null,
|
|
1743
|
+
install_url: options.install ?? null,
|
|
1744
|
+
notes: options.notes ?? null
|
|
1745
|
+
});
|
|
1746
|
+
if (error) {
|
|
1747
|
+
console.error(chalk15.red(`Failed to create: ${error.message}`));
|
|
1748
|
+
process.exit(1);
|
|
1749
|
+
}
|
|
1750
|
+
console.log(chalk15.green(`Added "${options.display}" to the registry.`));
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
// dist/commands/admin-list-tools.js
|
|
1755
|
+
import chalk16 from "chalk";
|
|
1756
|
+
async function adminListToolsCommand() {
|
|
1757
|
+
const { supabase } = await getAuthenticatedClient();
|
|
1758
|
+
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
|
|
1759
|
+
if (error) {
|
|
1760
|
+
console.error(chalk16.red(`Failed to fetch tools: ${error.message}`));
|
|
1761
|
+
process.exit(1);
|
|
1762
|
+
}
|
|
1763
|
+
if (!tools?.length) {
|
|
1764
|
+
console.log(chalk16.yellow("No tools in the registry."));
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
|
|
1768
|
+
const catW = Math.max(10, ...tools.map((t) => t.category.length));
|
|
1769
|
+
const stableW = Math.max(8, ...tools.map((t) => (t.latest_stable ?? "\u2014").length));
|
|
1770
|
+
const betaW = Math.max(8, ...tools.map((t) => (t.latest_beta ?? "\u2014").length));
|
|
1771
|
+
const header = [
|
|
1772
|
+
"Name".padEnd(nameW),
|
|
1773
|
+
"Category".padEnd(catW),
|
|
1774
|
+
"Stable".padEnd(stableW),
|
|
1775
|
+
"Beta".padEnd(betaW),
|
|
1776
|
+
"Last Checked"
|
|
1777
|
+
].join(" ");
|
|
1778
|
+
console.log(chalk16.bold(header));
|
|
1779
|
+
console.log("\u2500".repeat(header.length));
|
|
1780
|
+
for (const tool of tools) {
|
|
1781
|
+
const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
|
|
1782
|
+
const row = [
|
|
1783
|
+
tool.display_name.padEnd(nameW),
|
|
1784
|
+
tool.category.padEnd(catW),
|
|
1785
|
+
(tool.latest_stable ?? "\u2014").padEnd(stableW),
|
|
1786
|
+
(tool.latest_beta ?? "\u2014").padEnd(betaW),
|
|
1787
|
+
lastChecked
|
|
1788
|
+
].join(" ");
|
|
1789
|
+
console.log(row);
|
|
1790
|
+
}
|
|
1791
|
+
console.log(chalk16.grey(`
|
|
1792
|
+
${tools.length} tool(s) in registry.`));
|
|
1793
|
+
}
|
|
1794
|
+
function formatRelative(date) {
|
|
1795
|
+
const diff = Date.now() - date.getTime();
|
|
1796
|
+
const hours = Math.floor(diff / (1e3 * 60 * 60));
|
|
1797
|
+
if (hours < 1)
|
|
1798
|
+
return "just now";
|
|
1799
|
+
if (hours < 24)
|
|
1800
|
+
return `${hours}h ago`;
|
|
1801
|
+
const days = Math.floor(hours / 24);
|
|
1802
|
+
if (days < 7)
|
|
1803
|
+
return `${days}d ago`;
|
|
1804
|
+
return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
// dist/commands/admin-fetch-versions.js
|
|
1808
|
+
import chalk17 from "chalk";
|
|
1809
|
+
|
|
1810
|
+
// dist/commands/version-sources.js
|
|
1811
|
+
var VERSION_SOURCES = {
|
|
1812
|
+
"next": { type: "npm", package: "next" },
|
|
1813
|
+
"react": { type: "npm", package: "react" },
|
|
1814
|
+
"react-dom": { type: "npm", package: "react-dom" },
|
|
1815
|
+
"typescript": { type: "npm", package: "typescript" },
|
|
1816
|
+
"tailwindcss": { type: "npm", package: "tailwindcss" },
|
|
1817
|
+
"esbuild": { type: "npm", package: "esbuild" },
|
|
1818
|
+
"chalk": { type: "npm", package: "chalk" },
|
|
1819
|
+
"commander": { type: "npm", package: "commander" },
|
|
1820
|
+
"inquirer": { type: "npm", package: "inquirer" },
|
|
1821
|
+
"playwright": { type: "npm", package: "playwright" },
|
|
1822
|
+
"resend": { type: "npm", package: "resend" },
|
|
1823
|
+
"@supabase/supabase-js": { type: "npm", package: "@supabase/supabase-js" },
|
|
1824
|
+
"pnpm": { type: "npm", package: "pnpm" },
|
|
1825
|
+
"claude-code": { type: "npm", package: "@anthropic-ai/claude-code" },
|
|
1826
|
+
"turborepo": { type: "npm", package: "turbo" },
|
|
1827
|
+
"npm": { type: "npm", package: "npm" },
|
|
1828
|
+
"node": { type: "github", repo: "nodejs/node" },
|
|
1829
|
+
"supabase-cli": { type: "github", repo: "supabase/cli" }
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1832
|
+
// dist/commands/admin-fetch-versions.js
|
|
1833
|
+
async function adminFetchVersionsCommand() {
|
|
1834
|
+
const { supabase } = await getAuthenticatedClient();
|
|
1835
|
+
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
|
|
1836
|
+
if (error) {
|
|
1837
|
+
console.error(chalk17.red(`Failed to fetch tools: ${error.message}`));
|
|
1838
|
+
process.exit(1);
|
|
1839
|
+
}
|
|
1840
|
+
if (!tools?.length) {
|
|
1841
|
+
console.log(chalk17.yellow("No tools in the registry."));
|
|
1842
|
+
}
|
|
1843
|
+
const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
|
|
1844
|
+
const registeredNames = new Set((tools ?? []).map((t) => t.name));
|
|
1845
|
+
const unregisteredNames = /* @__PURE__ */ new Set();
|
|
1846
|
+
for (const pt of allProjectToolings ?? []) {
|
|
1847
|
+
if (!registeredNames.has(pt.tool_name) && pt.detection_source === "package.json") {
|
|
1848
|
+
unregisteredNames.add(pt.tool_name);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (unregisteredNames.size > 0) {
|
|
1852
|
+
console.log(chalk17.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
|
|
1853
|
+
`));
|
|
1854
|
+
for (const name of unregisteredNames) {
|
|
1855
|
+
const displayName = name;
|
|
1856
|
+
const { data: inserted, error: insertError } = await supabase.from("tools_registry").upsert({
|
|
1857
|
+
name,
|
|
1858
|
+
display_name: displayName,
|
|
1859
|
+
category: "package",
|
|
1860
|
+
source_url: `https://www.npmjs.com/package/${name}`,
|
|
1861
|
+
install_url: `https://www.npmjs.com/package/${name}`
|
|
1862
|
+
}, { onConflict: "name" }).select().single();
|
|
1863
|
+
if (!insertError && inserted) {
|
|
1864
|
+
tools.push(inserted);
|
|
1865
|
+
await supabase.from("project_toolings").update({ tool_id: inserted.id }).eq("tool_name", name).is("tool_id", null);
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (!tools?.length) {
|
|
1870
|
+
console.log(chalk17.yellow("No tools to fetch."));
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1873
|
+
console.log(chalk17.bold(`Fetching versions for ${tools.length} tool(s)...
|
|
1874
|
+
`));
|
|
1875
|
+
const results = await Promise.all(tools.map(async (tool) => {
|
|
1876
|
+
const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
|
|
1877
|
+
try {
|
|
1878
|
+
const { stable, beta } = await fetchVersions(source);
|
|
1879
|
+
const stableChanged = stable !== tool.latest_stable;
|
|
1880
|
+
const betaChanged = beta !== tool.latest_beta;
|
|
1881
|
+
const updates = {
|
|
1882
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1883
|
+
};
|
|
1884
|
+
if (stableChanged)
|
|
1885
|
+
updates.latest_stable = stable;
|
|
1886
|
+
if (betaChanged)
|
|
1887
|
+
updates.latest_beta = beta;
|
|
1888
|
+
const { error: updateError } = await supabase.from("tools_registry").update(updates).eq("id", tool.id);
|
|
1889
|
+
if (updateError) {
|
|
1890
|
+
return { displayName: tool.display_name, stable, beta, status: "failed" };
|
|
1891
|
+
}
|
|
1892
|
+
return {
|
|
1893
|
+
displayName: tool.display_name,
|
|
1894
|
+
stable,
|
|
1895
|
+
beta,
|
|
1896
|
+
status: stableChanged || betaChanged ? "updated" : "unchanged"
|
|
1897
|
+
};
|
|
1898
|
+
} catch {
|
|
1899
|
+
return { displayName: tool.display_name, stable: null, beta: null, status: "failed" };
|
|
1900
|
+
}
|
|
1901
|
+
}));
|
|
1902
|
+
const nameW = Math.max(16, ...results.map((r) => r.displayName.length));
|
|
1903
|
+
const stableW = Math.max(8, ...results.map((r) => (r.stable ?? "\u2014").length));
|
|
1904
|
+
const betaW = Math.max(8, ...results.map((r) => (r.beta ?? "\u2014").length));
|
|
1905
|
+
const statusW = 10;
|
|
1906
|
+
const header = [
|
|
1907
|
+
"Tool".padEnd(nameW),
|
|
1908
|
+
"Stable".padEnd(stableW),
|
|
1909
|
+
"Beta".padEnd(betaW),
|
|
1910
|
+
"Status".padEnd(statusW)
|
|
1911
|
+
].join(" ");
|
|
1912
|
+
console.log(chalk17.bold(header));
|
|
1913
|
+
console.log("\u2500".repeat(header.length));
|
|
1914
|
+
for (const result of results) {
|
|
1915
|
+
const statusColour = result.status === "updated" ? chalk17.green : result.status === "unchanged" ? chalk17.grey : result.status === "failed" ? chalk17.red : chalk17.yellow;
|
|
1916
|
+
const row = [
|
|
1917
|
+
result.displayName.padEnd(nameW),
|
|
1918
|
+
(result.stable ?? "\u2014").padEnd(stableW),
|
|
1919
|
+
(result.beta ?? "\u2014").padEnd(betaW),
|
|
1920
|
+
statusColour(result.status.padEnd(statusW))
|
|
1921
|
+
].join(" ");
|
|
1922
|
+
console.log(row);
|
|
1923
|
+
}
|
|
1924
|
+
const updated = results.filter((r) => r.status === "updated").length;
|
|
1925
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
1926
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
1927
|
+
const noSource = results.filter((r) => r.status === "no source").length;
|
|
1928
|
+
console.log(chalk17.grey(`
|
|
1929
|
+
${results.length} tool(s): `) + chalk17.green(`${updated} updated`) + ", " + chalk17.grey(`${unchanged} unchanged`) + ", " + chalk17.red(`${failed} failed`) + ", " + chalk17.yellow(`${noSource} no source`));
|
|
1930
|
+
}
|
|
1931
|
+
async function fetchVersions(source) {
|
|
1932
|
+
const controller = new AbortController();
|
|
1933
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
1934
|
+
try {
|
|
1935
|
+
if (source.type === "npm") {
|
|
1936
|
+
return await fetchNpmVersions(source.package, controller.signal);
|
|
1937
|
+
} else {
|
|
1938
|
+
return await fetchGitHubVersions(source.repo, controller.signal);
|
|
1939
|
+
}
|
|
1940
|
+
} finally {
|
|
1941
|
+
clearTimeout(timeout);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
async function fetchNpmVersions(packageName, signal) {
|
|
1945
|
+
const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
|
|
1946
|
+
const res = await fetch(url, { signal });
|
|
1947
|
+
if (!res.ok) {
|
|
1948
|
+
throw new Error(`npm registry returned ${res.status}`);
|
|
1949
|
+
}
|
|
1950
|
+
const tags = await res.json();
|
|
1951
|
+
const stable = tags.latest ?? null;
|
|
1952
|
+
const beta = tags.next ?? tags.beta ?? tags.rc ?? null;
|
|
1953
|
+
return { stable, beta };
|
|
1954
|
+
}
|
|
1955
|
+
async function fetchGitHubVersions(repo, signal) {
|
|
1956
|
+
const url = `https://api.github.com/repos/${repo}/releases?per_page=20`;
|
|
1957
|
+
const res = await fetch(url, {
|
|
1958
|
+
signal,
|
|
1959
|
+
headers: { Accept: "application/vnd.github+json" }
|
|
1960
|
+
});
|
|
1961
|
+
if (!res.ok) {
|
|
1962
|
+
throw new Error(`GitHub API returned ${res.status}`);
|
|
1963
|
+
}
|
|
1964
|
+
const releases = await res.json();
|
|
1965
|
+
let stable = null;
|
|
1966
|
+
let beta = null;
|
|
1967
|
+
for (const release of releases) {
|
|
1968
|
+
if (release.draft)
|
|
1969
|
+
continue;
|
|
1970
|
+
const version = release.tag_name.replace(/^v/, "");
|
|
1971
|
+
if (!release.prerelease && !stable) {
|
|
1972
|
+
stable = version;
|
|
1973
|
+
}
|
|
1974
|
+
if (release.prerelease && !beta) {
|
|
1975
|
+
beta = version;
|
|
1976
|
+
}
|
|
1977
|
+
if (stable && beta)
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
return { stable, beta };
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1425
1983
|
// dist/index.js
|
|
1426
1984
|
var program = new Command();
|
|
1427
1985
|
program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
|
|
@@ -1443,4 +2001,8 @@ program.command("print <title>").description("Generate a printable wall-chart HT
|
|
|
1443
2001
|
program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
|
|
1444
2002
|
program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
|
|
1445
2003
|
program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
|
|
2004
|
+
var admin = program.command("admin").description("Admin commands for managing the tools registry");
|
|
2005
|
+
admin.command("update-tool").description("Add or update a tool in the master registry").requiredOption("--name <name>", "Canonical tool name (e.g. next, playwright)").option("--display <display>", 'Human-friendly display name (e.g. "Next.js")').option("--category <category>", "Tool category (framework|runtime|cli|mcp|package|database|other)").option("--stable <version>", "Latest stable version").option("--beta <version>", "Latest beta/RC version").option("--source <url>", "Source of truth URL for checking versions").option("--install <url>", "Download/install link").option("--notes <text>", "Compatibility notes or warnings").action(adminUpdateToolCommand);
|
|
2006
|
+
admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
|
|
2007
|
+
admin.command("fetch-versions").description("Fetch latest stable and beta versions from npm and GitHub").action(adminFetchVersionsCommand);
|
|
1446
2008
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md4ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"@supabase/supabase-js": "^2.98.0",
|
|
33
33
|
"chalk": "^5.4.0",
|
|
34
34
|
"commander": "^14.0.0",
|
|
35
|
-
"glob": "^13.0.0",
|
|
36
35
|
"jszip": "^3.10.1"
|
|
37
36
|
},
|
|
38
37
|
"devDependencies": {
|