md4ai 0.4.0 → 0.5.1
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 +623 -56
- package/package.json +1 -1
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,255 @@ 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 firstLine = output.replace(/^v/, "").split("\n")[0].trim();
|
|
869
|
+
const versionMatch = firstLine.match(/^(\d+\.\d+[^\s]*)/);
|
|
870
|
+
const version = versionMatch?.[1] ?? firstLine;
|
|
871
|
+
if (version) {
|
|
872
|
+
toolings.push({
|
|
873
|
+
tool_name: name,
|
|
874
|
+
detected_version: version,
|
|
875
|
+
detection_source: "cli"
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return toolings;
|
|
882
|
+
}
|
|
883
|
+
async function detectFromMcpSettings(projectRoot) {
|
|
884
|
+
const toolings = [];
|
|
885
|
+
const settingsPaths = [
|
|
886
|
+
join6(projectRoot, ".claude", "settings.json"),
|
|
887
|
+
join6(projectRoot, ".claude", "settings.local.json"),
|
|
888
|
+
join6(homedir4(), ".claude", "settings.json")
|
|
889
|
+
];
|
|
890
|
+
const seen = /* @__PURE__ */ new Set();
|
|
891
|
+
for (const settingsPath of settingsPaths) {
|
|
892
|
+
if (!existsSync4(settingsPath))
|
|
893
|
+
continue;
|
|
894
|
+
try {
|
|
895
|
+
const content = await readFile4(settingsPath, "utf-8");
|
|
896
|
+
const settings = JSON.parse(content);
|
|
897
|
+
const mcpServers = settings.mcpServers ?? {};
|
|
898
|
+
for (const serverName of Object.keys(mcpServers)) {
|
|
899
|
+
if (seen.has(serverName))
|
|
900
|
+
continue;
|
|
901
|
+
seen.add(serverName);
|
|
902
|
+
let version = null;
|
|
903
|
+
const config = mcpServers[serverName];
|
|
904
|
+
if (config?.command && typeof config.command === "string") {
|
|
905
|
+
const args = config.args ?? [];
|
|
906
|
+
for (const arg of args) {
|
|
907
|
+
const versionMatch = arg.match(/@(\d+\.\d+[^\s]*?)$/);
|
|
908
|
+
if (versionMatch) {
|
|
909
|
+
version = versionMatch[1];
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
toolings.push({
|
|
915
|
+
tool_name: serverName,
|
|
916
|
+
detected_version: version,
|
|
917
|
+
detection_source: "mcp_settings"
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
} catch {
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return toolings;
|
|
924
|
+
}
|
|
925
|
+
|
|
678
926
|
// dist/scanner/index.js
|
|
679
927
|
async function scanProject(projectRoot) {
|
|
680
928
|
const allFiles = await discoverFiles(projectRoot);
|
|
681
929
|
const rootFiles = identifyRoots(allFiles, projectRoot);
|
|
682
930
|
const allRefs = [];
|
|
683
931
|
for (const file of allFiles) {
|
|
684
|
-
const fullPath = file.startsWith("/") ? file :
|
|
932
|
+
const fullPath = file.startsWith("/") ? file : join7(projectRoot, file);
|
|
685
933
|
try {
|
|
686
934
|
const refs = await parseFileReferences(fullPath, projectRoot);
|
|
687
935
|
allRefs.push(...refs);
|
|
@@ -692,39 +940,41 @@ async function scanProject(projectRoot) {
|
|
|
692
940
|
const orphans = detectOrphans(allFiles, allRefs, rootFiles, projectRoot);
|
|
693
941
|
const staleFiles = detectStaleFiles(allFiles, projectRoot);
|
|
694
942
|
const skills = await parseSkills(projectRoot);
|
|
695
|
-
const
|
|
943
|
+
const toolings = await detectToolings(projectRoot);
|
|
944
|
+
const scanData = JSON.stringify({ graph, orphans, skills, staleFiles, toolings });
|
|
696
945
|
const dataHash = createHash("sha256").update(scanData).digest("hex");
|
|
697
946
|
return {
|
|
698
947
|
graph,
|
|
699
948
|
orphans,
|
|
700
949
|
skills,
|
|
701
950
|
staleFiles,
|
|
951
|
+
toolings,
|
|
702
952
|
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
703
953
|
dataHash
|
|
704
954
|
};
|
|
705
955
|
}
|
|
706
956
|
async function discoverFiles(projectRoot) {
|
|
707
957
|
const files = [];
|
|
708
|
-
const claudeDir =
|
|
709
|
-
if (
|
|
958
|
+
const claudeDir = join7(projectRoot, ".claude");
|
|
959
|
+
if (existsSync5(claudeDir)) {
|
|
710
960
|
await walkDir(claudeDir, projectRoot, files);
|
|
711
961
|
}
|
|
712
|
-
if (
|
|
962
|
+
if (existsSync5(join7(projectRoot, "CLAUDE.md"))) {
|
|
713
963
|
files.push("CLAUDE.md");
|
|
714
964
|
}
|
|
715
|
-
if (
|
|
965
|
+
if (existsSync5(join7(projectRoot, "skills.md"))) {
|
|
716
966
|
files.push("skills.md");
|
|
717
967
|
}
|
|
718
|
-
const plansDir =
|
|
719
|
-
if (
|
|
968
|
+
const plansDir = join7(projectRoot, "docs", "plans");
|
|
969
|
+
if (existsSync5(plansDir)) {
|
|
720
970
|
await walkDir(plansDir, projectRoot, files);
|
|
721
971
|
}
|
|
722
972
|
return [...new Set(files)];
|
|
723
973
|
}
|
|
724
974
|
async function walkDir(dir, projectRoot, files) {
|
|
725
|
-
const entries = await
|
|
975
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
726
976
|
for (const entry of entries) {
|
|
727
|
-
const fullPath =
|
|
977
|
+
const fullPath = join7(dir, entry.name);
|
|
728
978
|
if (entry.isDirectory()) {
|
|
729
979
|
if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
|
|
730
980
|
continue;
|
|
@@ -743,17 +993,17 @@ function identifyRoots(allFiles, projectRoot) {
|
|
|
743
993
|
}
|
|
744
994
|
}
|
|
745
995
|
for (const globalFile of GLOBAL_ROOT_FILES) {
|
|
746
|
-
const expanded = globalFile.replace("~",
|
|
747
|
-
if (
|
|
996
|
+
const expanded = globalFile.replace("~", homedir5());
|
|
997
|
+
if (existsSync5(expanded)) {
|
|
748
998
|
roots.push(globalFile);
|
|
749
999
|
}
|
|
750
1000
|
}
|
|
751
1001
|
return roots;
|
|
752
1002
|
}
|
|
753
1003
|
async function readClaudeConfigFiles(projectRoot) {
|
|
754
|
-
const { readFile:
|
|
755
|
-
const { join:
|
|
756
|
-
const { existsSync:
|
|
1004
|
+
const { readFile: readFile7, stat, glob } = await import("node:fs/promises");
|
|
1005
|
+
const { join: join11, relative: relative2 } = await import("node:path");
|
|
1006
|
+
const { existsSync: existsSync10 } = await import("node:fs");
|
|
757
1007
|
const configPatterns = [
|
|
758
1008
|
"CLAUDE.md",
|
|
759
1009
|
".claude/CLAUDE.md",
|
|
@@ -763,11 +1013,11 @@ async function readClaudeConfigFiles(projectRoot) {
|
|
|
763
1013
|
];
|
|
764
1014
|
const files = [];
|
|
765
1015
|
for (const pattern of configPatterns) {
|
|
766
|
-
for await (const fullPath of glob(
|
|
767
|
-
if (!
|
|
1016
|
+
for await (const fullPath of glob(join11(projectRoot, pattern))) {
|
|
1017
|
+
if (!existsSync10(fullPath))
|
|
768
1018
|
continue;
|
|
769
1019
|
try {
|
|
770
|
-
const content = await
|
|
1020
|
+
const content = await readFile7(fullPath, "utf-8");
|
|
771
1021
|
const fileStat = await stat(fullPath);
|
|
772
1022
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
773
1023
|
files.push({
|
|
@@ -786,7 +1036,7 @@ function detectStaleFiles(allFiles, projectRoot) {
|
|
|
786
1036
|
const stale = [];
|
|
787
1037
|
const now = Date.now();
|
|
788
1038
|
for (const file of allFiles) {
|
|
789
|
-
const fullPath =
|
|
1039
|
+
const fullPath = join7(projectRoot, file);
|
|
790
1040
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
791
1041
|
if (lastMod) {
|
|
792
1042
|
const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
|
|
@@ -911,7 +1161,7 @@ function escapeHtml(text) {
|
|
|
911
1161
|
|
|
912
1162
|
// dist/check-update.js
|
|
913
1163
|
import chalk8 from "chalk";
|
|
914
|
-
var CURRENT_VERSION = "0.
|
|
1164
|
+
var CURRENT_VERSION = "0.5.1";
|
|
915
1165
|
async function checkForUpdate() {
|
|
916
1166
|
try {
|
|
917
1167
|
const controller = new AbortController();
|
|
@@ -954,11 +1204,30 @@ function isNewer(a, b) {
|
|
|
954
1204
|
return false;
|
|
955
1205
|
}
|
|
956
1206
|
|
|
1207
|
+
// dist/commands/push-toolings.js
|
|
1208
|
+
async function pushToolings(supabase, folderId, toolings) {
|
|
1209
|
+
if (!toolings.length)
|
|
1210
|
+
return;
|
|
1211
|
+
const { data: registry } = await supabase.from("tools_registry").select("id, name");
|
|
1212
|
+
const registryMap = new Map((registry ?? []).map((r) => [r.name, r.id]));
|
|
1213
|
+
for (const tooling of toolings) {
|
|
1214
|
+
const toolId = registryMap.get(tooling.tool_name) ?? null;
|
|
1215
|
+
await supabase.from("project_toolings").upsert({
|
|
1216
|
+
folder_id: folderId,
|
|
1217
|
+
tool_id: toolId,
|
|
1218
|
+
tool_name: tooling.tool_name,
|
|
1219
|
+
detected_version: tooling.detected_version,
|
|
1220
|
+
detection_source: tooling.detection_source,
|
|
1221
|
+
detected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1222
|
+
}, { onConflict: "folder_id,tool_name,detection_source" });
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
957
1226
|
// dist/commands/map.js
|
|
958
1227
|
async function mapCommand(path, options) {
|
|
959
1228
|
await checkForUpdate();
|
|
960
1229
|
const projectRoot = resolve3(path ?? process.cwd());
|
|
961
|
-
if (!
|
|
1230
|
+
if (!existsSync6(projectRoot)) {
|
|
962
1231
|
console.error(chalk9.red(`Path not found: ${projectRoot}`));
|
|
963
1232
|
process.exit(1);
|
|
964
1233
|
}
|
|
@@ -970,9 +1239,10 @@ async function mapCommand(path, options) {
|
|
|
970
1239
|
console.log(` Orphans: ${result.orphans.length}`);
|
|
971
1240
|
console.log(` Stale files: ${result.staleFiles.length}`);
|
|
972
1241
|
console.log(` Skills: ${result.skills.length}`);
|
|
1242
|
+
console.log(` Toolings: ${result.toolings.length}`);
|
|
973
1243
|
console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
|
|
974
1244
|
const outputDir = resolve3(projectRoot, "output");
|
|
975
|
-
if (!
|
|
1245
|
+
if (!existsSync6(outputDir)) {
|
|
976
1246
|
await mkdir2(outputDir, { recursive: true });
|
|
977
1247
|
}
|
|
978
1248
|
const htmlPath = resolve3(outputDir, "index.html");
|
|
@@ -1030,6 +1300,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
1030
1300
|
if (error) {
|
|
1031
1301
|
console.error(chalk9.yellow(`Sync warning: ${error.message}`));
|
|
1032
1302
|
} else {
|
|
1303
|
+
await pushToolings(supabase, folder_id, result.toolings);
|
|
1033
1304
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
|
|
1034
1305
|
await saveState({
|
|
1035
1306
|
lastFolderId: folder_id,
|
|
@@ -1064,42 +1335,42 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
1064
1335
|
}
|
|
1065
1336
|
|
|
1066
1337
|
// dist/commands/simulate.js
|
|
1067
|
-
import { join as
|
|
1068
|
-
import { existsSync as
|
|
1069
|
-
import { homedir as
|
|
1338
|
+
import { join as join8 } from "node:path";
|
|
1339
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1340
|
+
import { homedir as homedir6 } from "node:os";
|
|
1070
1341
|
import chalk10 from "chalk";
|
|
1071
1342
|
async function simulateCommand(prompt) {
|
|
1072
1343
|
const projectRoot = process.cwd();
|
|
1073
1344
|
console.log(chalk10.blue(`Simulating prompt: "${prompt}"
|
|
1074
1345
|
`));
|
|
1075
1346
|
console.log(chalk10.dim("Files Claude would load:\n"));
|
|
1076
|
-
const globalClaude =
|
|
1077
|
-
if (
|
|
1347
|
+
const globalClaude = join8(homedir6(), ".claude", "CLAUDE.md");
|
|
1348
|
+
if (existsSync7(globalClaude)) {
|
|
1078
1349
|
console.log(chalk10.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
|
|
1079
1350
|
}
|
|
1080
1351
|
for (const rootFile of ROOT_FILES) {
|
|
1081
|
-
const fullPath =
|
|
1082
|
-
if (
|
|
1352
|
+
const fullPath = join8(projectRoot, rootFile);
|
|
1353
|
+
if (existsSync7(fullPath)) {
|
|
1083
1354
|
console.log(chalk10.green(` \u2713 ${rootFile} (project root)`));
|
|
1084
1355
|
}
|
|
1085
1356
|
}
|
|
1086
1357
|
const settingsFiles = [
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1358
|
+
join8(homedir6(), ".claude", "settings.json"),
|
|
1359
|
+
join8(homedir6(), ".claude", "settings.local.json"),
|
|
1360
|
+
join8(projectRoot, ".claude", "settings.json"),
|
|
1361
|
+
join8(projectRoot, ".claude", "settings.local.json")
|
|
1091
1362
|
];
|
|
1092
1363
|
for (const sf of settingsFiles) {
|
|
1093
|
-
if (
|
|
1094
|
-
const display = sf.startsWith(
|
|
1364
|
+
if (existsSync7(sf)) {
|
|
1365
|
+
const display = sf.startsWith(homedir6()) ? sf.replace(homedir6(), "~") : sf.replace(projectRoot + "/", "");
|
|
1095
1366
|
console.log(chalk10.green(` \u2713 ${display} (settings)`));
|
|
1096
1367
|
}
|
|
1097
1368
|
}
|
|
1098
1369
|
const words = prompt.split(/\s+/);
|
|
1099
1370
|
for (const word of words) {
|
|
1100
1371
|
const cleaned = word.replace(/['"]/g, "");
|
|
1101
|
-
const candidatePath =
|
|
1102
|
-
if (
|
|
1372
|
+
const candidatePath = join8(projectRoot, cleaned);
|
|
1373
|
+
if (existsSync7(candidatePath) && cleaned.includes("/")) {
|
|
1103
1374
|
console.log(chalk10.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
|
|
1104
1375
|
}
|
|
1105
1376
|
}
|
|
@@ -1107,18 +1378,18 @@ async function simulateCommand(prompt) {
|
|
|
1107
1378
|
}
|
|
1108
1379
|
|
|
1109
1380
|
// dist/commands/print.js
|
|
1110
|
-
import { join as
|
|
1111
|
-
import { readFile as
|
|
1112
|
-
import { existsSync as
|
|
1381
|
+
import { join as join9 } from "node:path";
|
|
1382
|
+
import { readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
1383
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
1113
1384
|
import chalk11 from "chalk";
|
|
1114
1385
|
async function printCommand(title) {
|
|
1115
1386
|
const projectRoot = process.cwd();
|
|
1116
|
-
const scanDataPath =
|
|
1117
|
-
if (!
|
|
1387
|
+
const scanDataPath = join9(projectRoot, "output", "index.html");
|
|
1388
|
+
if (!existsSync8(scanDataPath)) {
|
|
1118
1389
|
console.error(chalk11.red("No scan data found. Run: md4ai scan"));
|
|
1119
1390
|
process.exit(1);
|
|
1120
1391
|
}
|
|
1121
|
-
const html = await
|
|
1392
|
+
const html = await readFile5(scanDataPath, "utf-8");
|
|
1122
1393
|
const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
|
|
1123
1394
|
if (!match) {
|
|
1124
1395
|
console.error(chalk11.red("Could not extract scan data from output/index.html"));
|
|
@@ -1126,7 +1397,7 @@ async function printCommand(title) {
|
|
|
1126
1397
|
}
|
|
1127
1398
|
const result = JSON.parse(match[1]);
|
|
1128
1399
|
const printHtml = generatePrintHtml(result, title);
|
|
1129
|
-
const outputPath =
|
|
1400
|
+
const outputPath = join9(projectRoot, "output", `print-${Date.now()}.html`);
|
|
1130
1401
|
await writeFile3(outputPath, printHtml, "utf-8");
|
|
1131
1402
|
console.log(chalk11.green(`Print-ready wall sheet: ${outputPath}`));
|
|
1132
1403
|
}
|
|
@@ -1210,6 +1481,7 @@ async function syncCommand(options) {
|
|
|
1210
1481
|
last_scanned: result.scannedAt,
|
|
1211
1482
|
data_hash: result.dataHash
|
|
1212
1483
|
}).eq("id", device.folder_id);
|
|
1484
|
+
await pushToolings(supabase, device.folder_id, result.toolings);
|
|
1213
1485
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
|
|
1214
1486
|
console.log(chalk12.green(` Done: ${device.device_name}`));
|
|
1215
1487
|
} catch (err) {
|
|
@@ -1243,6 +1515,7 @@ async function syncCommand(options) {
|
|
|
1243
1515
|
last_scanned: result.scannedAt,
|
|
1244
1516
|
data_hash: result.dataHash
|
|
1245
1517
|
}).eq("id", device.folder_id);
|
|
1518
|
+
await pushToolings(supabase, device.folder_id, result.toolings);
|
|
1246
1519
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", device.folder_id).eq("device_name", device.device_name);
|
|
1247
1520
|
await saveState({ lastSyncAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1248
1521
|
console.log(chalk12.green("Synced."));
|
|
@@ -1315,6 +1588,7 @@ Linking "${folder.name}" to this device...
|
|
|
1315
1588
|
console.log(` References:${result.graph.edges.length}`);
|
|
1316
1589
|
console.log(` Orphans: ${result.orphans.length}`);
|
|
1317
1590
|
console.log(` Skills: ${result.skills.length}`);
|
|
1591
|
+
console.log(` Toolings: ${result.toolings.length}`);
|
|
1318
1592
|
const { error: scanErr } = await supabase.from("claude_folders").update({
|
|
1319
1593
|
graph_json: result.graph,
|
|
1320
1594
|
orphans_json: result.orphans,
|
|
@@ -1326,6 +1600,7 @@ Linking "${folder.name}" to this device...
|
|
|
1326
1600
|
if (scanErr) {
|
|
1327
1601
|
console.error(chalk13.yellow(`Scan upload warning: ${scanErr.message}`));
|
|
1328
1602
|
}
|
|
1603
|
+
await pushToolings(supabase, folder.id, result.toolings);
|
|
1329
1604
|
const configFiles = await readClaudeConfigFiles(cwd);
|
|
1330
1605
|
if (configFiles.length > 0) {
|
|
1331
1606
|
for (const file of configFiles) {
|
|
@@ -1354,18 +1629,18 @@ Linking "${folder.name}" to this device...
|
|
|
1354
1629
|
}
|
|
1355
1630
|
|
|
1356
1631
|
// dist/commands/import-bundle.js
|
|
1357
|
-
import { readFile as
|
|
1358
|
-
import { join as
|
|
1359
|
-
import { existsSync as
|
|
1632
|
+
import { readFile as readFile6, writeFile as writeFile4, mkdir as mkdir3 } from "node:fs/promises";
|
|
1633
|
+
import { join as join10, dirname as dirname2 } from "node:path";
|
|
1634
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1360
1635
|
import chalk14 from "chalk";
|
|
1361
1636
|
import { confirm, input as input4 } from "@inquirer/prompts";
|
|
1362
1637
|
async function importBundleCommand(zipPath) {
|
|
1363
|
-
if (!
|
|
1638
|
+
if (!existsSync9(zipPath)) {
|
|
1364
1639
|
console.error(chalk14.red(`File not found: ${zipPath}`));
|
|
1365
1640
|
process.exit(1);
|
|
1366
1641
|
}
|
|
1367
1642
|
const JSZip = (await import("jszip")).default;
|
|
1368
|
-
const zipData = await
|
|
1643
|
+
const zipData = await readFile6(zipPath);
|
|
1369
1644
|
const zip = await JSZip.loadAsync(zipData);
|
|
1370
1645
|
const manifestFile = zip.file("manifest.json");
|
|
1371
1646
|
if (!manifestFile) {
|
|
@@ -1407,9 +1682,9 @@ Files to extract:`));
|
|
|
1407
1682
|
return;
|
|
1408
1683
|
}
|
|
1409
1684
|
for (const file of files) {
|
|
1410
|
-
const fullPath =
|
|
1685
|
+
const fullPath = join10(targetDir, file.filePath);
|
|
1411
1686
|
const dir = dirname2(fullPath);
|
|
1412
|
-
if (!
|
|
1687
|
+
if (!existsSync9(dir)) {
|
|
1413
1688
|
await mkdir3(dir, { recursive: true });
|
|
1414
1689
|
}
|
|
1415
1690
|
await writeFile4(fullPath, file.content, "utf-8");
|
|
@@ -1419,6 +1694,294 @@ Files to extract:`));
|
|
|
1419
1694
|
Done! ${files.length} file(s) extracted to ${targetDir}`));
|
|
1420
1695
|
}
|
|
1421
1696
|
|
|
1697
|
+
// dist/commands/admin-update-tool.js
|
|
1698
|
+
import chalk15 from "chalk";
|
|
1699
|
+
var VALID_CATEGORIES = ["framework", "runtime", "cli", "mcp", "package", "database", "other"];
|
|
1700
|
+
async function adminUpdateToolCommand(options) {
|
|
1701
|
+
const { supabase } = await getAuthenticatedClient();
|
|
1702
|
+
if (!options.name) {
|
|
1703
|
+
console.error(chalk15.red("--name is required."));
|
|
1704
|
+
process.exit(1);
|
|
1705
|
+
}
|
|
1706
|
+
if (options.category && !VALID_CATEGORIES.includes(options.category)) {
|
|
1707
|
+
console.error(chalk15.red(`Invalid category. Must be one of: ${VALID_CATEGORIES.join(", ")}`));
|
|
1708
|
+
process.exit(1);
|
|
1709
|
+
}
|
|
1710
|
+
const { data: existing } = await supabase.from("tools_registry").select("id, name, display_name, category").eq("name", options.name).maybeSingle();
|
|
1711
|
+
if (existing) {
|
|
1712
|
+
const updates = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1713
|
+
if (options.display)
|
|
1714
|
+
updates.display_name = options.display;
|
|
1715
|
+
if (options.category)
|
|
1716
|
+
updates.category = options.category;
|
|
1717
|
+
if (options.stable)
|
|
1718
|
+
updates.latest_stable = options.stable;
|
|
1719
|
+
if (options.beta)
|
|
1720
|
+
updates.latest_beta = options.beta;
|
|
1721
|
+
if (options.source)
|
|
1722
|
+
updates.source_url = options.source;
|
|
1723
|
+
if (options.install)
|
|
1724
|
+
updates.install_url = options.install;
|
|
1725
|
+
if (options.notes)
|
|
1726
|
+
updates.notes = options.notes;
|
|
1727
|
+
const { error } = await supabase.from("tools_registry").update(updates).eq("id", existing.id);
|
|
1728
|
+
if (error) {
|
|
1729
|
+
console.error(chalk15.red(`Failed to update: ${error.message}`));
|
|
1730
|
+
process.exit(1);
|
|
1731
|
+
}
|
|
1732
|
+
console.log(chalk15.green(`Updated "${existing.display_name}" in the registry.`));
|
|
1733
|
+
} else {
|
|
1734
|
+
if (!options.display || !options.category) {
|
|
1735
|
+
console.error(chalk15.red("New tools require --display and --category."));
|
|
1736
|
+
process.exit(1);
|
|
1737
|
+
}
|
|
1738
|
+
const { error } = await supabase.from("tools_registry").insert({
|
|
1739
|
+
name: options.name,
|
|
1740
|
+
display_name: options.display,
|
|
1741
|
+
category: options.category,
|
|
1742
|
+
latest_stable: options.stable ?? null,
|
|
1743
|
+
latest_beta: options.beta ?? null,
|
|
1744
|
+
source_url: options.source ?? null,
|
|
1745
|
+
install_url: options.install ?? null,
|
|
1746
|
+
notes: options.notes ?? null
|
|
1747
|
+
});
|
|
1748
|
+
if (error) {
|
|
1749
|
+
console.error(chalk15.red(`Failed to create: ${error.message}`));
|
|
1750
|
+
process.exit(1);
|
|
1751
|
+
}
|
|
1752
|
+
console.log(chalk15.green(`Added "${options.display}" to the registry.`));
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// dist/commands/admin-list-tools.js
|
|
1757
|
+
import chalk16 from "chalk";
|
|
1758
|
+
async function adminListToolsCommand() {
|
|
1759
|
+
const { supabase } = await getAuthenticatedClient();
|
|
1760
|
+
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("category").order("display_name");
|
|
1761
|
+
if (error) {
|
|
1762
|
+
console.error(chalk16.red(`Failed to fetch tools: ${error.message}`));
|
|
1763
|
+
process.exit(1);
|
|
1764
|
+
}
|
|
1765
|
+
if (!tools?.length) {
|
|
1766
|
+
console.log(chalk16.yellow("No tools in the registry."));
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
const nameW = Math.max(16, ...tools.map((t) => t.display_name.length));
|
|
1770
|
+
const catW = Math.max(10, ...tools.map((t) => t.category.length));
|
|
1771
|
+
const stableW = Math.max(8, ...tools.map((t) => (t.latest_stable ?? "\u2014").length));
|
|
1772
|
+
const betaW = Math.max(8, ...tools.map((t) => (t.latest_beta ?? "\u2014").length));
|
|
1773
|
+
const header = [
|
|
1774
|
+
"Name".padEnd(nameW),
|
|
1775
|
+
"Category".padEnd(catW),
|
|
1776
|
+
"Stable".padEnd(stableW),
|
|
1777
|
+
"Beta".padEnd(betaW),
|
|
1778
|
+
"Last Checked"
|
|
1779
|
+
].join(" ");
|
|
1780
|
+
console.log(chalk16.bold(header));
|
|
1781
|
+
console.log("\u2500".repeat(header.length));
|
|
1782
|
+
for (const tool of tools) {
|
|
1783
|
+
const lastChecked = tool.updated_at ? formatRelative(new Date(tool.updated_at)) : "\u2014";
|
|
1784
|
+
const row = [
|
|
1785
|
+
tool.display_name.padEnd(nameW),
|
|
1786
|
+
tool.category.padEnd(catW),
|
|
1787
|
+
(tool.latest_stable ?? "\u2014").padEnd(stableW),
|
|
1788
|
+
(tool.latest_beta ?? "\u2014").padEnd(betaW),
|
|
1789
|
+
lastChecked
|
|
1790
|
+
].join(" ");
|
|
1791
|
+
console.log(row);
|
|
1792
|
+
}
|
|
1793
|
+
console.log(chalk16.grey(`
|
|
1794
|
+
${tools.length} tool(s) in registry.`));
|
|
1795
|
+
}
|
|
1796
|
+
function formatRelative(date) {
|
|
1797
|
+
const diff = Date.now() - date.getTime();
|
|
1798
|
+
const hours = Math.floor(diff / (1e3 * 60 * 60));
|
|
1799
|
+
if (hours < 1)
|
|
1800
|
+
return "just now";
|
|
1801
|
+
if (hours < 24)
|
|
1802
|
+
return `${hours}h ago`;
|
|
1803
|
+
const days = Math.floor(hours / 24);
|
|
1804
|
+
if (days < 7)
|
|
1805
|
+
return `${days}d ago`;
|
|
1806
|
+
return date.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// dist/commands/admin-fetch-versions.js
|
|
1810
|
+
import chalk17 from "chalk";
|
|
1811
|
+
|
|
1812
|
+
// dist/commands/version-sources.js
|
|
1813
|
+
var VERSION_SOURCES = {
|
|
1814
|
+
"next": { type: "npm", package: "next" },
|
|
1815
|
+
"react": { type: "npm", package: "react" },
|
|
1816
|
+
"react-dom": { type: "npm", package: "react-dom" },
|
|
1817
|
+
"typescript": { type: "npm", package: "typescript" },
|
|
1818
|
+
"tailwindcss": { type: "npm", package: "tailwindcss" },
|
|
1819
|
+
"esbuild": { type: "npm", package: "esbuild" },
|
|
1820
|
+
"chalk": { type: "npm", package: "chalk" },
|
|
1821
|
+
"commander": { type: "npm", package: "commander" },
|
|
1822
|
+
"inquirer": { type: "npm", package: "inquirer" },
|
|
1823
|
+
"playwright": { type: "npm", package: "playwright" },
|
|
1824
|
+
"resend": { type: "npm", package: "resend" },
|
|
1825
|
+
"@supabase/supabase-js": { type: "npm", package: "@supabase/supabase-js" },
|
|
1826
|
+
"pnpm": { type: "npm", package: "pnpm" },
|
|
1827
|
+
"claude-code": { type: "npm", package: "@anthropic-ai/claude-code" },
|
|
1828
|
+
"turborepo": { type: "npm", package: "turbo" },
|
|
1829
|
+
"npm": { type: "npm", package: "npm" },
|
|
1830
|
+
"node": { type: "github", repo: "nodejs/node" },
|
|
1831
|
+
"supabase-cli": { type: "github", repo: "supabase/cli" }
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
// dist/commands/admin-fetch-versions.js
|
|
1835
|
+
async function adminFetchVersionsCommand() {
|
|
1836
|
+
const { supabase } = await getAuthenticatedClient();
|
|
1837
|
+
const { data: tools, error } = await supabase.from("tools_registry").select("*").order("display_name");
|
|
1838
|
+
if (error) {
|
|
1839
|
+
console.error(chalk17.red(`Failed to fetch tools: ${error.message}`));
|
|
1840
|
+
process.exit(1);
|
|
1841
|
+
}
|
|
1842
|
+
if (!tools?.length) {
|
|
1843
|
+
console.log(chalk17.yellow("No tools in the registry."));
|
|
1844
|
+
}
|
|
1845
|
+
const { data: allProjectToolings } = await supabase.from("project_toolings").select("tool_name, detection_source").is("tool_id", null);
|
|
1846
|
+
const registeredNames = new Set((tools ?? []).map((t) => t.name));
|
|
1847
|
+
const unregisteredNames = /* @__PURE__ */ new Set();
|
|
1848
|
+
for (const pt of allProjectToolings ?? []) {
|
|
1849
|
+
if (!registeredNames.has(pt.tool_name) && pt.detection_source === "package.json") {
|
|
1850
|
+
unregisteredNames.add(pt.tool_name);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
if (unregisteredNames.size > 0) {
|
|
1854
|
+
console.log(chalk17.blue(`Auto-registering ${unregisteredNames.size} unverified package(s)...
|
|
1855
|
+
`));
|
|
1856
|
+
for (const name of unregisteredNames) {
|
|
1857
|
+
const displayName = name;
|
|
1858
|
+
const { data: inserted, error: insertError } = await supabase.from("tools_registry").upsert({
|
|
1859
|
+
name,
|
|
1860
|
+
display_name: displayName,
|
|
1861
|
+
category: "package",
|
|
1862
|
+
source_url: `https://www.npmjs.com/package/${name}`,
|
|
1863
|
+
install_url: `https://www.npmjs.com/package/${name}`
|
|
1864
|
+
}, { onConflict: "name" }).select().single();
|
|
1865
|
+
if (!insertError && inserted) {
|
|
1866
|
+
tools.push(inserted);
|
|
1867
|
+
await supabase.from("project_toolings").update({ tool_id: inserted.id }).eq("tool_name", name).is("tool_id", null);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
if (!tools?.length) {
|
|
1872
|
+
console.log(chalk17.yellow("No tools to fetch."));
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
console.log(chalk17.bold(`Fetching versions for ${tools.length} tool(s)...
|
|
1876
|
+
`));
|
|
1877
|
+
const results = await Promise.all(tools.map(async (tool) => {
|
|
1878
|
+
const source = VERSION_SOURCES[tool.name] ?? { type: "npm", package: tool.name };
|
|
1879
|
+
try {
|
|
1880
|
+
const { stable, beta } = await fetchVersions(source);
|
|
1881
|
+
const stableChanged = stable !== tool.latest_stable;
|
|
1882
|
+
const betaChanged = beta !== tool.latest_beta;
|
|
1883
|
+
const updates = {
|
|
1884
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1885
|
+
};
|
|
1886
|
+
if (stableChanged)
|
|
1887
|
+
updates.latest_stable = stable;
|
|
1888
|
+
if (betaChanged)
|
|
1889
|
+
updates.latest_beta = beta;
|
|
1890
|
+
const { error: updateError } = await supabase.from("tools_registry").update(updates).eq("id", tool.id);
|
|
1891
|
+
if (updateError) {
|
|
1892
|
+
return { displayName: tool.display_name, stable, beta, status: "failed" };
|
|
1893
|
+
}
|
|
1894
|
+
return {
|
|
1895
|
+
displayName: tool.display_name,
|
|
1896
|
+
stable,
|
|
1897
|
+
beta,
|
|
1898
|
+
status: stableChanged || betaChanged ? "updated" : "unchanged"
|
|
1899
|
+
};
|
|
1900
|
+
} catch {
|
|
1901
|
+
return { displayName: tool.display_name, stable: null, beta: null, status: "failed" };
|
|
1902
|
+
}
|
|
1903
|
+
}));
|
|
1904
|
+
const nameW = Math.max(16, ...results.map((r) => r.displayName.length));
|
|
1905
|
+
const stableW = Math.max(8, ...results.map((r) => (r.stable ?? "\u2014").length));
|
|
1906
|
+
const betaW = Math.max(8, ...results.map((r) => (r.beta ?? "\u2014").length));
|
|
1907
|
+
const statusW = 10;
|
|
1908
|
+
const header = [
|
|
1909
|
+
"Tool".padEnd(nameW),
|
|
1910
|
+
"Stable".padEnd(stableW),
|
|
1911
|
+
"Beta".padEnd(betaW),
|
|
1912
|
+
"Status".padEnd(statusW)
|
|
1913
|
+
].join(" ");
|
|
1914
|
+
console.log(chalk17.bold(header));
|
|
1915
|
+
console.log("\u2500".repeat(header.length));
|
|
1916
|
+
for (const result of results) {
|
|
1917
|
+
const statusColour = result.status === "updated" ? chalk17.green : result.status === "unchanged" ? chalk17.grey : result.status === "failed" ? chalk17.red : chalk17.yellow;
|
|
1918
|
+
const row = [
|
|
1919
|
+
result.displayName.padEnd(nameW),
|
|
1920
|
+
(result.stable ?? "\u2014").padEnd(stableW),
|
|
1921
|
+
(result.beta ?? "\u2014").padEnd(betaW),
|
|
1922
|
+
statusColour(result.status.padEnd(statusW))
|
|
1923
|
+
].join(" ");
|
|
1924
|
+
console.log(row);
|
|
1925
|
+
}
|
|
1926
|
+
const updated = results.filter((r) => r.status === "updated").length;
|
|
1927
|
+
const unchanged = results.filter((r) => r.status === "unchanged").length;
|
|
1928
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
1929
|
+
const noSource = results.filter((r) => r.status === "no source").length;
|
|
1930
|
+
console.log(chalk17.grey(`
|
|
1931
|
+
${results.length} tool(s): `) + chalk17.green(`${updated} updated`) + ", " + chalk17.grey(`${unchanged} unchanged`) + ", " + chalk17.red(`${failed} failed`) + ", " + chalk17.yellow(`${noSource} no source`));
|
|
1932
|
+
}
|
|
1933
|
+
async function fetchVersions(source) {
|
|
1934
|
+
const controller = new AbortController();
|
|
1935
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
1936
|
+
try {
|
|
1937
|
+
if (source.type === "npm") {
|
|
1938
|
+
return await fetchNpmVersions(source.package, controller.signal);
|
|
1939
|
+
} else {
|
|
1940
|
+
return await fetchGitHubVersions(source.repo, controller.signal);
|
|
1941
|
+
}
|
|
1942
|
+
} finally {
|
|
1943
|
+
clearTimeout(timeout);
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
async function fetchNpmVersions(packageName, signal) {
|
|
1947
|
+
const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
|
|
1948
|
+
const res = await fetch(url, { signal });
|
|
1949
|
+
if (!res.ok) {
|
|
1950
|
+
throw new Error(`npm registry returned ${res.status}`);
|
|
1951
|
+
}
|
|
1952
|
+
const tags = await res.json();
|
|
1953
|
+
const stable = tags.latest ?? null;
|
|
1954
|
+
const beta = tags.next ?? tags.beta ?? tags.rc ?? null;
|
|
1955
|
+
return { stable, beta };
|
|
1956
|
+
}
|
|
1957
|
+
async function fetchGitHubVersions(repo, signal) {
|
|
1958
|
+
const url = `https://api.github.com/repos/${repo}/releases?per_page=20`;
|
|
1959
|
+
const res = await fetch(url, {
|
|
1960
|
+
signal,
|
|
1961
|
+
headers: { Accept: "application/vnd.github+json" }
|
|
1962
|
+
});
|
|
1963
|
+
if (!res.ok) {
|
|
1964
|
+
throw new Error(`GitHub API returned ${res.status}`);
|
|
1965
|
+
}
|
|
1966
|
+
const releases = await res.json();
|
|
1967
|
+
let stable = null;
|
|
1968
|
+
let beta = null;
|
|
1969
|
+
for (const release of releases) {
|
|
1970
|
+
if (release.draft)
|
|
1971
|
+
continue;
|
|
1972
|
+
const version = release.tag_name.replace(/^v/, "");
|
|
1973
|
+
if (!release.prerelease && !stable) {
|
|
1974
|
+
stable = version;
|
|
1975
|
+
}
|
|
1976
|
+
if (release.prerelease && !beta) {
|
|
1977
|
+
beta = version;
|
|
1978
|
+
}
|
|
1979
|
+
if (stable && beta)
|
|
1980
|
+
break;
|
|
1981
|
+
}
|
|
1982
|
+
return { stable, beta };
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1422
1985
|
// dist/index.js
|
|
1423
1986
|
var program = new Command();
|
|
1424
1987
|
program.name("md4ai").description("MD4AI \u2014 Claude tooling visualiser").version(CURRENT_VERSION).addHelpText("after", `
|
|
@@ -1440,4 +2003,8 @@ program.command("print <title>").description("Generate a printable wall-chart HT
|
|
|
1440
2003
|
program.command("sync").description("Re-push latest scan data to Supabase").option("--all", "Sync all folders on all devices").action(syncCommand);
|
|
1441
2004
|
program.command("import <zipfile>").description("Import a Claude setup bundle exported from the web dashboard").action(importBundleCommand);
|
|
1442
2005
|
program.command("check-update").description("Check if a newer version of md4ai is available").action(checkForUpdate);
|
|
2006
|
+
var admin = program.command("admin").description("Admin commands for managing the tools registry");
|
|
2007
|
+
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);
|
|
2008
|
+
admin.command("list-tools").description("List all tools in the master registry").action(adminListToolsCommand);
|
|
2009
|
+
admin.command("fetch-versions").description("Fetch latest stable and beta versions from npm and GitHub").action(adminFetchVersionsCommand);
|
|
1443
2010
|
program.parse();
|