md4ai 0.10.2 → 0.10.3
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/README.md +145 -0
- package/dist/index.bundled.js +307 -158
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# md4ai
|
|
2
|
+
|
|
3
|
+
CLI for [MD4AI](https://www.md4ai.com) — scan your Claude Code projects and sync results to a web dashboard.
|
|
4
|
+
|
|
5
|
+
Discovers Claude configuration files, builds dependency graphs, detects orphans and broken references, catalogues skills and marketplace plugins, and pushes everything to a shared dashboard for team visibility.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g md4ai
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires **Node.js 22** or later.
|
|
14
|
+
|
|
15
|
+
## Setup
|
|
16
|
+
|
|
17
|
+
1. **Create an account** at [md4ai.com](https://www.md4ai.com) and create a project.
|
|
18
|
+
|
|
19
|
+
2. **Set the Supabase key** in your shell profile:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
export MD4AI_SUPABASE_ANON_KEY="your-anon-key"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. **Log in:**
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
md4ai login
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
4. **Link your project:**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd /path/to/your-claude-project
|
|
35
|
+
md4ai link <project-id>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The project ID is in the URL when viewing your project on the dashboard.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
### Scanning & Syncing
|
|
43
|
+
|
|
44
|
+
| Command | Description |
|
|
45
|
+
|---------|-------------|
|
|
46
|
+
| `md4ai scan [path]` | Scan a Claude project and push results to the dashboard. Defaults to current directory. |
|
|
47
|
+
| `md4ai scan --offline` | Scan locally only — generates `output/index.html` without pushing to Supabase. |
|
|
48
|
+
| `md4ai sync` | Re-push the most recent scan data for the current project. |
|
|
49
|
+
| `md4ai sync --all` | Re-scan and sync all linked projects on this device. |
|
|
50
|
+
| `md4ai link <project-id>` | Link the current directory to a dashboard project and run an initial scan. |
|
|
51
|
+
|
|
52
|
+
### Analysis
|
|
53
|
+
|
|
54
|
+
| Command | Description |
|
|
55
|
+
|---------|-------------|
|
|
56
|
+
| `md4ai simulate <prompt>` | Show which files Claude Code would load for a given prompt. |
|
|
57
|
+
| `md4ai print <title>` | Generate a printable A3 wall-chart HTML with the dependency graph and skills table. |
|
|
58
|
+
| `md4ai init-manifest` | Scaffold an `env-manifest.md` from detected `.env` files in the project. |
|
|
59
|
+
|
|
60
|
+
### Account & Device Management
|
|
61
|
+
|
|
62
|
+
| Command | Description |
|
|
63
|
+
|---------|-------------|
|
|
64
|
+
| `md4ai login` | Authenticate with email and password. |
|
|
65
|
+
| `md4ai logout` | Clear stored credentials. |
|
|
66
|
+
| `md4ai status` | Show login status, linked folders, and last sync time. |
|
|
67
|
+
| `md4ai add-folder` | Create a new project folder on the dashboard. |
|
|
68
|
+
| `md4ai add-device` | Add a device path to an existing project. |
|
|
69
|
+
| `md4ai list-devices` | List all devices and their linked projects. |
|
|
70
|
+
|
|
71
|
+
### Monitoring
|
|
72
|
+
|
|
73
|
+
| Command | Description |
|
|
74
|
+
|---------|-------------|
|
|
75
|
+
| `md4ai mcp-watch` | Monitor MCP server status on this device (runs until Ctrl+C, polls every 30s). |
|
|
76
|
+
|
|
77
|
+
### Other
|
|
78
|
+
|
|
79
|
+
| Command | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| `md4ai import <zipfile>` | Import an exported team bundle. |
|
|
82
|
+
| `md4ai update` | Check for updates and install if available. |
|
|
83
|
+
| `md4ai config set <key> <value>` | Set a configuration value (e.g. `vercel-token`). |
|
|
84
|
+
|
|
85
|
+
## What Gets Scanned
|
|
86
|
+
|
|
87
|
+
Running `md4ai scan` discovers files in `.claude/`, `CLAUDE.md`, `skills.md`, and `docs/plans/`. It then:
|
|
88
|
+
|
|
89
|
+
- **Builds a dependency graph** by parsing markdown links, bare file paths, `$CLAUDE_PROJECT_DIR` references, and JSON hook configurations.
|
|
90
|
+
- **Detects orphans** — files not reachable from any root configuration.
|
|
91
|
+
- **Finds broken references** — links pointing to files that don't exist on disk.
|
|
92
|
+
- **Catalogues skills** — both project-specific and machine-wide, including marketplace plugins.
|
|
93
|
+
- **Flags stale files** — anything not modified in over 90 days.
|
|
94
|
+
- **Scans environment variables** — if an `env-manifest.md` is present, checks local `.env` files, Vercel, and GitHub Secrets for drift.
|
|
95
|
+
- **Detects tooling** — frameworks, runtimes, and packages from `package.json` and MCP settings.
|
|
96
|
+
|
|
97
|
+
Scan output:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
Files found: 41
|
|
101
|
+
References: 41
|
|
102
|
+
Broken refs: 2
|
|
103
|
+
Orphans: 3
|
|
104
|
+
Stale files: 0
|
|
105
|
+
Skills: 19
|
|
106
|
+
Toolings: 55
|
|
107
|
+
Env Vars: 24 (manifest found)
|
|
108
|
+
Plugins: 17 (17 skills)
|
|
109
|
+
Data hash: 9868c4a8b50f...
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## File Locations
|
|
113
|
+
|
|
114
|
+
| Item | Path |
|
|
115
|
+
|------|------|
|
|
116
|
+
| Credentials | `~/.md4ai/credentials.json` |
|
|
117
|
+
| State | `~/.md4ai/state.json` |
|
|
118
|
+
| Local scan preview | `output/index.html` (in scanned project) |
|
|
119
|
+
| Print exports | `output/print-<timestamp>.html` |
|
|
120
|
+
|
|
121
|
+
## Web Dashboard
|
|
122
|
+
|
|
123
|
+
All scan data syncs to [md4ai.com](https://www.md4ai.com) where you can:
|
|
124
|
+
|
|
125
|
+
- Browse the dependency graph interactively
|
|
126
|
+
- View file contents with structure navigation
|
|
127
|
+
- Track environment variable drift across local, Vercel, and GitHub
|
|
128
|
+
- Monitor MCP server status per device
|
|
129
|
+
- Share projects with team members
|
|
130
|
+
- Compare skills across machines
|
|
131
|
+
|
|
132
|
+
## Tech Stack
|
|
133
|
+
|
|
134
|
+
- **TypeScript** (strict, ESM)
|
|
135
|
+
- **Commander** for CLI parsing
|
|
136
|
+
- **Supabase** for auth and data storage
|
|
137
|
+
- **esbuild** for bundling
|
|
138
|
+
|
|
139
|
+
## Support
|
|
140
|
+
|
|
141
|
+
For questions, feedback, or bug reports: [richard@mediahq2.com](mailto:richard@mediahq2.com)
|
|
142
|
+
|
|
143
|
+
## Licence
|
|
144
|
+
|
|
145
|
+
MIT — see [LICENCE](https://github.com/Media-HQ-2-Ltd/MD4AI/blob/main/LICENSE) for details.
|
package/dist/index.bundled.js
CHANGED
|
@@ -83,12 +83,8 @@ var init_dist = __esm({
|
|
|
83
83
|
import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
|
|
84
84
|
import { join } from "node:path";
|
|
85
85
|
import { homedir } from "node:os";
|
|
86
|
-
import { existsSync } from "node:fs";
|
|
87
86
|
async function ensureConfigDir() {
|
|
88
|
-
|
|
89
|
-
await mkdir(configPath, { recursive: true });
|
|
90
|
-
await chmod(configPath, 448);
|
|
91
|
-
}
|
|
87
|
+
await mkdir(configPath, { recursive: true, mode: 448 });
|
|
92
88
|
}
|
|
93
89
|
async function saveCredentials(creds) {
|
|
94
90
|
await ensureConfigDir();
|
|
@@ -115,7 +111,7 @@ async function mergeCredentials(partial) {
|
|
|
115
111
|
}
|
|
116
112
|
async function clearCredentials() {
|
|
117
113
|
try {
|
|
118
|
-
await writeFile(credentialsPath, "{}",
|
|
114
|
+
await writeFile(credentialsPath, "{}", { mode: 384 });
|
|
119
115
|
} catch {
|
|
120
116
|
}
|
|
121
117
|
}
|
|
@@ -290,7 +286,7 @@ var init_auth = __esm({
|
|
|
290
286
|
|
|
291
287
|
// dist/scanner/file-parser.js
|
|
292
288
|
import { readFile as readFile2 } from "node:fs/promises";
|
|
293
|
-
import { existsSync
|
|
289
|
+
import { existsSync } from "node:fs";
|
|
294
290
|
import { resolve as resolve2, dirname, join as join2 } from "node:path";
|
|
295
291
|
import { homedir as homedir2 } from "node:os";
|
|
296
292
|
async function parseFileReferences(filePath, projectRoot) {
|
|
@@ -318,7 +314,7 @@ async function parseFileReferences(filePath, projectRoot) {
|
|
|
318
314
|
if (!target.includes("/") && /^[A-Z]/.test(baseName))
|
|
319
315
|
continue;
|
|
320
316
|
const resolved = resolveTarget(target, filePath, projectRoot);
|
|
321
|
-
if (resolved &&
|
|
317
|
+
if (resolved && existsSync(resolved)) {
|
|
322
318
|
addRef(refs, relPath, resolved, projectRoot);
|
|
323
319
|
} else if (resolved) {
|
|
324
320
|
const targetRel = resolved.startsWith(projectRoot) ? resolved.slice(projectRoot.length + 1) : resolved;
|
|
@@ -361,7 +357,7 @@ function resolveTarget(target, sourceFilePath, projectRoot) {
|
|
|
361
357
|
}
|
|
362
358
|
if (target.includes("/")) {
|
|
363
359
|
const fromRoot = join2(projectRoot, target);
|
|
364
|
-
if (
|
|
360
|
+
if (existsSync(fromRoot))
|
|
365
361
|
return fromRoot;
|
|
366
362
|
}
|
|
367
363
|
return resolve2(dirname(sourceFilePath), target);
|
|
@@ -387,7 +383,7 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs, brokenRefs
|
|
|
387
383
|
while ((match = pattern.exec(value)) !== null) {
|
|
388
384
|
const relTarget = match[1];
|
|
389
385
|
const resolved = join2(projectRoot, relTarget);
|
|
390
|
-
if (
|
|
386
|
+
if (existsSync(resolved)) {
|
|
391
387
|
addRef(refs, fromRel, resolved, projectRoot);
|
|
392
388
|
} else {
|
|
393
389
|
brokenRefs.push({ from: fromRel, to: relTarget, rawRef: relTarget });
|
|
@@ -401,7 +397,7 @@ function parseJsonPathReferences(content, fromRel, projectRoot, refs, brokenRefs
|
|
|
401
397
|
if (!target || target.startsWith("http"))
|
|
402
398
|
continue;
|
|
403
399
|
const resolved = join2(projectRoot, target);
|
|
404
|
-
if (
|
|
400
|
+
if (existsSync(resolved)) {
|
|
405
401
|
addRef(refs, fromRel, resolved, projectRoot);
|
|
406
402
|
} else {
|
|
407
403
|
brokenRefs.push({ from: fromRel, to: target, rawRef: target });
|
|
@@ -609,7 +605,7 @@ var init_orphan_detector = __esm({
|
|
|
609
605
|
// dist/scanner/skills-parser.js
|
|
610
606
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
611
607
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
612
|
-
import { existsSync as
|
|
608
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
613
609
|
import { join as join5 } from "node:path";
|
|
614
610
|
import { homedir as homedir3 } from "node:os";
|
|
615
611
|
async function parseSkills(projectRoot) {
|
|
@@ -638,7 +634,7 @@ async function parseSkills(projectRoot) {
|
|
|
638
634
|
await parseSettingsForPlugins(projectSettings, skills, false);
|
|
639
635
|
await parseSettingsForPlugins(projectLocalSettings, skills, false);
|
|
640
636
|
const skillsMd = join5(projectRoot, "skills.md");
|
|
641
|
-
if (
|
|
637
|
+
if (existsSync2(skillsMd)) {
|
|
642
638
|
const content = await readFile3(skillsMd, "utf-8");
|
|
643
639
|
const skillNames = content.match(/^#+\s+(.+)$/gm);
|
|
644
640
|
if (skillNames) {
|
|
@@ -661,7 +657,7 @@ async function parseSkills(projectRoot) {
|
|
|
661
657
|
return Array.from(skills.values());
|
|
662
658
|
}
|
|
663
659
|
async function parseSettingsForPlugins(settingsPath, skills, isMachineWide) {
|
|
664
|
-
if (!
|
|
660
|
+
if (!existsSync2(settingsPath))
|
|
665
661
|
return;
|
|
666
662
|
try {
|
|
667
663
|
const content = await readFile3(settingsPath, "utf-8");
|
|
@@ -698,7 +694,7 @@ var init_skills_parser = __esm({
|
|
|
698
694
|
|
|
699
695
|
// dist/scanner/tooling-detector.js
|
|
700
696
|
import { readFile as readFile4, readdir } from "node:fs/promises";
|
|
701
|
-
import { existsSync as
|
|
697
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
702
698
|
import { join as join6 } from "node:path";
|
|
703
699
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
704
700
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -715,7 +711,7 @@ async function detectToolings(projectRoot) {
|
|
|
715
711
|
async function detectFromPackageJson(projectRoot) {
|
|
716
712
|
const toolings = [];
|
|
717
713
|
const pkgPath = join6(projectRoot, "package.json");
|
|
718
|
-
if (!
|
|
714
|
+
if (!existsSync3(pkgPath))
|
|
719
715
|
return toolings;
|
|
720
716
|
const resolvedVersions = await getResolvedVersions(projectRoot);
|
|
721
717
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -759,7 +755,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
|
|
|
759
755
|
const cleanPattern = pattern.replace(/\/\*\*?$/, "");
|
|
760
756
|
if (isGlob) {
|
|
761
757
|
const parentDir = join6(projectRoot, cleanPattern);
|
|
762
|
-
if (!
|
|
758
|
+
if (!existsSync3(parentDir))
|
|
763
759
|
continue;
|
|
764
760
|
try {
|
|
765
761
|
const entries = await readdir(parentDir, { withFileTypes: true });
|
|
@@ -767,7 +763,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
|
|
|
767
763
|
if (!entry.isDirectory())
|
|
768
764
|
continue;
|
|
769
765
|
const pkgPath = join6(parentDir, entry.name, "package.json");
|
|
770
|
-
if (
|
|
766
|
+
if (existsSync3(pkgPath)) {
|
|
771
767
|
results.push(pkgPath);
|
|
772
768
|
}
|
|
773
769
|
}
|
|
@@ -775,7 +771,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
|
|
|
775
771
|
}
|
|
776
772
|
} else {
|
|
777
773
|
const pkgPath = join6(projectRoot, cleanPattern, "package.json");
|
|
778
|
-
if (
|
|
774
|
+
if (existsSync3(pkgPath)) {
|
|
779
775
|
results.push(pkgPath);
|
|
780
776
|
}
|
|
781
777
|
}
|
|
@@ -784,7 +780,7 @@ async function discoverWorkspacePackageJsons(projectRoot) {
|
|
|
784
780
|
}
|
|
785
781
|
async function getWorkspacePatterns(projectRoot) {
|
|
786
782
|
const pnpmWorkspace = join6(projectRoot, "pnpm-workspace.yaml");
|
|
787
|
-
if (
|
|
783
|
+
if (existsSync3(pnpmWorkspace)) {
|
|
788
784
|
try {
|
|
789
785
|
const content = await readFile4(pnpmWorkspace, "utf-8");
|
|
790
786
|
const patterns = [];
|
|
@@ -827,7 +823,7 @@ function stripVersionPrefix(version) {
|
|
|
827
823
|
async function getResolvedVersions(projectRoot) {
|
|
828
824
|
const versions = /* @__PURE__ */ new Map();
|
|
829
825
|
const pnpmLock = join6(projectRoot, "pnpm-lock.yaml");
|
|
830
|
-
if (
|
|
826
|
+
if (existsSync3(pnpmLock)) {
|
|
831
827
|
try {
|
|
832
828
|
const content = await readFile4(pnpmLock, "utf-8");
|
|
833
829
|
const versionPattern = /^\s{4}'?(@?[^@\s:]+)(?:@[^:]+)?'?:\s*(?:version:\s*)?'?(\d+\.\d+[^'\s]*)/gm;
|
|
@@ -843,7 +839,7 @@ async function getResolvedVersions(projectRoot) {
|
|
|
843
839
|
}
|
|
844
840
|
if (versions.size === 0) {
|
|
845
841
|
const npmLock = join6(projectRoot, "package-lock.json");
|
|
846
|
-
if (
|
|
842
|
+
if (existsSync3(npmLock)) {
|
|
847
843
|
try {
|
|
848
844
|
const content = await readFile4(npmLock, "utf-8");
|
|
849
845
|
const lock = JSON.parse(content);
|
|
@@ -864,7 +860,7 @@ async function getResolvedVersions(projectRoot) {
|
|
|
864
860
|
function detectFromCli(projectRoot) {
|
|
865
861
|
const toolings = [];
|
|
866
862
|
for (const { name, command, args, conditionFile } of CLI_VERSION_COMMANDS) {
|
|
867
|
-
if (conditionFile && !
|
|
863
|
+
if (conditionFile && !existsSync3(join6(projectRoot, conditionFile))) {
|
|
868
864
|
continue;
|
|
869
865
|
}
|
|
870
866
|
try {
|
|
@@ -897,7 +893,7 @@ async function detectFromMcpSettings(projectRoot) {
|
|
|
897
893
|
];
|
|
898
894
|
const seen = /* @__PURE__ */ new Set();
|
|
899
895
|
for (const settingsPath of settingsPaths) {
|
|
900
|
-
if (!
|
|
896
|
+
if (!existsSync3(settingsPath))
|
|
901
897
|
continue;
|
|
902
898
|
try {
|
|
903
899
|
const content = await readFile4(settingsPath, "utf-8");
|
|
@@ -1042,13 +1038,24 @@ async function fetchVercelEnvVars(projectId, orgId, token) {
|
|
|
1042
1038
|
}
|
|
1043
1039
|
const data = await res.json();
|
|
1044
1040
|
const envs = data.envs ?? [];
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
targets
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1041
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1042
|
+
for (const e of envs) {
|
|
1043
|
+
const targets = e.target ?? ["production", "preview", "development"];
|
|
1044
|
+
const existing = merged.get(e.key);
|
|
1045
|
+
if (existing) {
|
|
1046
|
+
const targetSet = /* @__PURE__ */ new Set([...existing.targets, ...targets]);
|
|
1047
|
+
existing.targets = Array.from(targetSet);
|
|
1048
|
+
} else {
|
|
1049
|
+
merged.set(e.key, {
|
|
1050
|
+
key: e.key,
|
|
1051
|
+
targets: [...targets],
|
|
1052
|
+
type: e.type ?? "plain",
|
|
1053
|
+
inManifest: false
|
|
1054
|
+
// populated later by the scanner
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return Array.from(merged.values());
|
|
1052
1059
|
}
|
|
1053
1060
|
var VercelApiError;
|
|
1054
1061
|
var init_fetch_env_vars = __esm({
|
|
@@ -1074,11 +1081,11 @@ import { readFile as readFile7, glob as glob2 } from "node:fs/promises";
|
|
|
1074
1081
|
import { execFile } from "node:child_process";
|
|
1075
1082
|
import { promisify } from "node:util";
|
|
1076
1083
|
import { join as join9, relative as relative2 } from "node:path";
|
|
1077
|
-
import { existsSync as
|
|
1084
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1078
1085
|
import chalk8 from "chalk";
|
|
1079
1086
|
async function scanEnvManifest(projectRoot) {
|
|
1080
1087
|
const manifestFullPath = join9(projectRoot, MANIFEST_PATH);
|
|
1081
|
-
if (!
|
|
1088
|
+
if (!existsSync4(manifestFullPath)) {
|
|
1082
1089
|
return null;
|
|
1083
1090
|
}
|
|
1084
1091
|
const manifestContent = await readFile7(manifestFullPath, "utf-8");
|
|
@@ -1320,7 +1327,7 @@ async function checkLocalEnvFiles(projectRoot, apps) {
|
|
|
1320
1327
|
for (const app of apps) {
|
|
1321
1328
|
const envPath = join9(projectRoot, app.envFilePath);
|
|
1322
1329
|
const varNames = /* @__PURE__ */ new Set();
|
|
1323
|
-
if (
|
|
1330
|
+
if (existsSync4(envPath)) {
|
|
1324
1331
|
const content = await readFile7(envPath, "utf-8");
|
|
1325
1332
|
for (const line of content.split("\n")) {
|
|
1326
1333
|
const m = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
@@ -1335,7 +1342,7 @@ async function checkLocalEnvFiles(projectRoot, apps) {
|
|
|
1335
1342
|
}
|
|
1336
1343
|
async function parseWorkflowSecrets(projectRoot) {
|
|
1337
1344
|
const workflowsDir = join9(projectRoot, ".github", "workflows");
|
|
1338
|
-
if (!
|
|
1345
|
+
if (!existsSync4(workflowsDir))
|
|
1339
1346
|
return [];
|
|
1340
1347
|
const refs = [];
|
|
1341
1348
|
const secretRe = /\$\{\{\s*secrets\.([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g;
|
|
@@ -1416,11 +1423,111 @@ var init_env_manifest_scanner = __esm({
|
|
|
1416
1423
|
}
|
|
1417
1424
|
});
|
|
1418
1425
|
|
|
1426
|
+
// dist/scanner/marketplace-scanner.js
|
|
1427
|
+
import { readFile as readFile8, readdir as readdir2, stat } from "node:fs/promises";
|
|
1428
|
+
import { join as join10, resolve as resolve3 } from "node:path";
|
|
1429
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
1430
|
+
import { homedir as homedir6 } from "node:os";
|
|
1431
|
+
async function scanMarketplacePlugins() {
|
|
1432
|
+
const pluginsDir = join10(homedir6(), ".claude", "plugins");
|
|
1433
|
+
const installedPath = join10(pluginsDir, "installed_plugins.json");
|
|
1434
|
+
const marketplacesPath = join10(pluginsDir, "known_marketplaces.json");
|
|
1435
|
+
if (!existsSync5(installedPath))
|
|
1436
|
+
return [];
|
|
1437
|
+
let installed;
|
|
1438
|
+
let knownMarketplaces = {};
|
|
1439
|
+
try {
|
|
1440
|
+
installed = JSON.parse(await readFile8(installedPath, "utf-8"));
|
|
1441
|
+
} catch {
|
|
1442
|
+
return [];
|
|
1443
|
+
}
|
|
1444
|
+
try {
|
|
1445
|
+
if (existsSync5(marketplacesPath)) {
|
|
1446
|
+
knownMarketplaces = JSON.parse(await readFile8(marketplacesPath, "utf-8"));
|
|
1447
|
+
}
|
|
1448
|
+
} catch {
|
|
1449
|
+
}
|
|
1450
|
+
const results = [];
|
|
1451
|
+
const expectedBase = resolve3(pluginsDir);
|
|
1452
|
+
for (const [key, entries] of Object.entries(installed.plugins ?? {})) {
|
|
1453
|
+
if (!entries?.length)
|
|
1454
|
+
continue;
|
|
1455
|
+
const entry = entries[0];
|
|
1456
|
+
const resolvedInstallPath = resolve3(entry.installPath);
|
|
1457
|
+
if (!resolvedInstallPath.startsWith(expectedBase + "/"))
|
|
1458
|
+
continue;
|
|
1459
|
+
const atIdx = key.lastIndexOf("@");
|
|
1460
|
+
if (atIdx === -1)
|
|
1461
|
+
continue;
|
|
1462
|
+
const pluginName = key.slice(0, atIdx);
|
|
1463
|
+
const marketplace = key.slice(atIdx + 1);
|
|
1464
|
+
const homepageUrl = buildHomepageUrl(knownMarketplaces, marketplace, pluginName);
|
|
1465
|
+
const skills = await discoverSkills(entry.installPath);
|
|
1466
|
+
results.push({
|
|
1467
|
+
marketplace,
|
|
1468
|
+
pluginName,
|
|
1469
|
+
version: entry.version ?? null,
|
|
1470
|
+
homepageUrl,
|
|
1471
|
+
installedAt: entry.installedAt ?? null,
|
|
1472
|
+
lastUpdated: entry.lastUpdated ?? null,
|
|
1473
|
+
skills
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
results.sort((a, b) => a.marketplace.localeCompare(b.marketplace) || a.pluginName.localeCompare(b.pluginName));
|
|
1477
|
+
return results;
|
|
1478
|
+
}
|
|
1479
|
+
function buildHomepageUrl(knownMarketplaces, marketplace, pluginName) {
|
|
1480
|
+
const mp = knownMarketplaces[marketplace];
|
|
1481
|
+
if (!mp?.source?.repo)
|
|
1482
|
+
return null;
|
|
1483
|
+
const repo = mp.source.repo;
|
|
1484
|
+
const baseUrl = `https://github.com/${repo}`;
|
|
1485
|
+
if (marketplace === "claude-plugins-official") {
|
|
1486
|
+
return `${baseUrl}/tree/main/plugins/${pluginName}`;
|
|
1487
|
+
}
|
|
1488
|
+
return baseUrl;
|
|
1489
|
+
}
|
|
1490
|
+
async function discoverSkills(installPath) {
|
|
1491
|
+
const skillsDir = join10(installPath, "skills");
|
|
1492
|
+
if (!existsSync5(skillsDir))
|
|
1493
|
+
return [];
|
|
1494
|
+
const skills = [];
|
|
1495
|
+
try {
|
|
1496
|
+
const entries = await readdir2(skillsDir, { withFileTypes: true });
|
|
1497
|
+
for (const entry of entries) {
|
|
1498
|
+
if (!entry.isDirectory())
|
|
1499
|
+
continue;
|
|
1500
|
+
const skillMd = join10(skillsDir, entry.name, "SKILL.md");
|
|
1501
|
+
if (!existsSync5(skillMd))
|
|
1502
|
+
continue;
|
|
1503
|
+
let lastModified = null;
|
|
1504
|
+
try {
|
|
1505
|
+
const fileStat = await stat(skillMd);
|
|
1506
|
+
lastModified = fileStat.mtime.toISOString();
|
|
1507
|
+
} catch {
|
|
1508
|
+
}
|
|
1509
|
+
skills.push({
|
|
1510
|
+
name: entry.name,
|
|
1511
|
+
skillPath: `skills/${entry.name}/SKILL.md`,
|
|
1512
|
+
lastModified
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
} catch {
|
|
1516
|
+
}
|
|
1517
|
+
skills.sort((a, b) => a.name.localeCompare(b.name));
|
|
1518
|
+
return skills;
|
|
1519
|
+
}
|
|
1520
|
+
var init_marketplace_scanner = __esm({
|
|
1521
|
+
"dist/scanner/marketplace-scanner.js"() {
|
|
1522
|
+
"use strict";
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
|
|
1419
1526
|
// dist/scanner/index.js
|
|
1420
|
-
import { readdir as
|
|
1421
|
-
import { join as
|
|
1527
|
+
import { readdir as readdir3 } from "node:fs/promises";
|
|
1528
|
+
import { join as join11, relative as relative3 } from "node:path";
|
|
1422
1529
|
import { existsSync as existsSync6 } from "node:fs";
|
|
1423
|
-
import { homedir as
|
|
1530
|
+
import { homedir as homedir7 } from "node:os";
|
|
1424
1531
|
import { createHash } from "node:crypto";
|
|
1425
1532
|
async function scanProject(projectRoot) {
|
|
1426
1533
|
const allFiles = await discoverFiles(projectRoot);
|
|
@@ -1428,7 +1535,7 @@ async function scanProject(projectRoot) {
|
|
|
1428
1535
|
const allRefs = [];
|
|
1429
1536
|
const allBrokenRefs = [];
|
|
1430
1537
|
for (const file of allFiles) {
|
|
1431
|
-
const fullPath = file.startsWith("/") ? file :
|
|
1538
|
+
const fullPath = file.startsWith("/") ? file : join11(projectRoot, file);
|
|
1432
1539
|
try {
|
|
1433
1540
|
const { refs, brokenRefs: brokenRefs2 } = await parseFileReferences(fullPath, projectRoot);
|
|
1434
1541
|
allRefs.push(...refs);
|
|
@@ -1442,6 +1549,7 @@ async function scanProject(projectRoot) {
|
|
|
1442
1549
|
const skills = await parseSkills(projectRoot);
|
|
1443
1550
|
const toolings = await detectToolings(projectRoot);
|
|
1444
1551
|
const envManifest = await scanEnvManifest(projectRoot);
|
|
1552
|
+
const marketplacePlugins = await scanMarketplacePlugins();
|
|
1445
1553
|
const depthMap = /* @__PURE__ */ new Map();
|
|
1446
1554
|
const queue = [...rootFiles];
|
|
1447
1555
|
for (const r of queue)
|
|
@@ -1462,7 +1570,7 @@ async function scanProject(projectRoot) {
|
|
|
1462
1570
|
depth: depthMap.get(br.from) ?? 999
|
|
1463
1571
|
}));
|
|
1464
1572
|
brokenRefs.sort((a, b) => a.depth - b.depth || a.from.localeCompare(b.from));
|
|
1465
|
-
const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest });
|
|
1573
|
+
const scanData = JSON.stringify({ graph, orphans, brokenRefs, skills, staleFiles, toolings, envManifest, marketplacePlugins });
|
|
1466
1574
|
const dataHash = createHash("sha256").update(scanData).digest("hex");
|
|
1467
1575
|
return {
|
|
1468
1576
|
graph,
|
|
@@ -1472,32 +1580,33 @@ async function scanProject(projectRoot) {
|
|
|
1472
1580
|
staleFiles,
|
|
1473
1581
|
toolings,
|
|
1474
1582
|
envManifest,
|
|
1583
|
+
marketplacePlugins,
|
|
1475
1584
|
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1476
1585
|
dataHash
|
|
1477
1586
|
};
|
|
1478
1587
|
}
|
|
1479
1588
|
async function discoverFiles(projectRoot) {
|
|
1480
1589
|
const files = [];
|
|
1481
|
-
const claudeDir =
|
|
1590
|
+
const claudeDir = join11(projectRoot, ".claude");
|
|
1482
1591
|
if (existsSync6(claudeDir)) {
|
|
1483
1592
|
await walkDir(claudeDir, projectRoot, files);
|
|
1484
1593
|
}
|
|
1485
|
-
if (existsSync6(
|
|
1594
|
+
if (existsSync6(join11(projectRoot, "CLAUDE.md"))) {
|
|
1486
1595
|
files.push("CLAUDE.md");
|
|
1487
1596
|
}
|
|
1488
|
-
if (existsSync6(
|
|
1597
|
+
if (existsSync6(join11(projectRoot, "skills.md"))) {
|
|
1489
1598
|
files.push("skills.md");
|
|
1490
1599
|
}
|
|
1491
|
-
const plansDir =
|
|
1600
|
+
const plansDir = join11(projectRoot, "docs", "plans");
|
|
1492
1601
|
if (existsSync6(plansDir)) {
|
|
1493
1602
|
await walkDir(plansDir, projectRoot, files);
|
|
1494
1603
|
}
|
|
1495
1604
|
return [...new Set(files)];
|
|
1496
1605
|
}
|
|
1497
1606
|
async function walkDir(dir, projectRoot, files) {
|
|
1498
|
-
const entries = await
|
|
1607
|
+
const entries = await readdir3(dir, { withFileTypes: true });
|
|
1499
1608
|
for (const entry of entries) {
|
|
1500
|
-
const fullPath =
|
|
1609
|
+
const fullPath = join11(dir, entry.name);
|
|
1501
1610
|
if (entry.isDirectory()) {
|
|
1502
1611
|
if (["node_modules", ".git", ".turbo", "cache", "session-env"].includes(entry.name))
|
|
1503
1612
|
continue;
|
|
@@ -1516,71 +1625,43 @@ function identifyRoots(allFiles, projectRoot) {
|
|
|
1516
1625
|
}
|
|
1517
1626
|
}
|
|
1518
1627
|
for (const globalFile of GLOBAL_ROOT_FILES) {
|
|
1519
|
-
const expanded = globalFile.replace("~",
|
|
1628
|
+
const expanded = globalFile.replace("~", homedir7());
|
|
1520
1629
|
if (existsSync6(expanded)) {
|
|
1521
1630
|
roots.push(globalFile);
|
|
1522
1631
|
}
|
|
1523
1632
|
}
|
|
1524
1633
|
return roots;
|
|
1525
1634
|
}
|
|
1526
|
-
async function readClaudeConfigFiles(projectRoot) {
|
|
1527
|
-
const { readFile:
|
|
1528
|
-
const {
|
|
1529
|
-
const
|
|
1530
|
-
const configPatterns = [
|
|
1531
|
-
"CLAUDE.md",
|
|
1532
|
-
".claude/CLAUDE.md",
|
|
1533
|
-
".claude/settings.json",
|
|
1534
|
-
".claude/commands/*.md",
|
|
1535
|
-
".claude/skills/*.md"
|
|
1536
|
-
];
|
|
1635
|
+
async function readClaudeConfigFiles(projectRoot, graphFilePaths) {
|
|
1636
|
+
const { readFile: readFile13, stat: stat2 } = await import("node:fs/promises");
|
|
1637
|
+
const { existsSync: existsSync14 } = await import("node:fs");
|
|
1638
|
+
const filePaths = graphFilePaths ?? await discoverFiles(projectRoot);
|
|
1537
1639
|
const files = [];
|
|
1538
1640
|
const seen = /* @__PURE__ */ new Set();
|
|
1539
|
-
|
|
1540
|
-
const relPath = relative5(projectRoot, fullPath);
|
|
1641
|
+
for (const relPath of filePaths) {
|
|
1541
1642
|
if (seen.has(relPath))
|
|
1542
|
-
|
|
1643
|
+
continue;
|
|
1644
|
+
if (relPath.startsWith("~") || relPath.startsWith("/"))
|
|
1645
|
+
continue;
|
|
1543
1646
|
seen.add(relPath);
|
|
1647
|
+
const fullPath = join11(projectRoot, relPath);
|
|
1648
|
+
if (!existsSync14(fullPath))
|
|
1649
|
+
continue;
|
|
1544
1650
|
try {
|
|
1545
|
-
const content = await
|
|
1546
|
-
const fileStat = await
|
|
1651
|
+
const content = await readFile13(fullPath, "utf-8");
|
|
1652
|
+
const fileStat = await stat2(fullPath);
|
|
1547
1653
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
1548
1654
|
files.push({ filePath: relPath, content, sizeBytes: fileStat.size, lastModified: lastMod });
|
|
1549
1655
|
} catch {
|
|
1550
1656
|
}
|
|
1551
1657
|
}
|
|
1552
|
-
for (const pattern of configPatterns) {
|
|
1553
|
-
for await (const fullPath of glob3(join16(projectRoot, pattern))) {
|
|
1554
|
-
if (!existsSync13(fullPath))
|
|
1555
|
-
continue;
|
|
1556
|
-
await addFile(fullPath);
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
const pkgPath = join16(projectRoot, "package.json");
|
|
1560
|
-
if (existsSync13(pkgPath)) {
|
|
1561
|
-
try {
|
|
1562
|
-
const pkgContent = await readFile12(pkgPath, "utf-8");
|
|
1563
|
-
const pkg = JSON.parse(pkgContent);
|
|
1564
|
-
const preflightCmd = pkg.scripts?.preflight;
|
|
1565
|
-
if (preflightCmd) {
|
|
1566
|
-
const match = preflightCmd.match(/(?:bash\s+|sh\s+|\.\/)?(\S+\.(?:sh|bash|ts|js|mjs))/);
|
|
1567
|
-
if (match) {
|
|
1568
|
-
const scriptPath = join16(projectRoot, match[1]);
|
|
1569
|
-
if (existsSync13(scriptPath)) {
|
|
1570
|
-
await addFile(scriptPath);
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
}
|
|
1574
|
-
} catch {
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
1658
|
return files;
|
|
1578
1659
|
}
|
|
1579
1660
|
function detectStaleFiles(allFiles, projectRoot) {
|
|
1580
1661
|
const stale = [];
|
|
1581
1662
|
const now = Date.now();
|
|
1582
1663
|
for (const file of allFiles) {
|
|
1583
|
-
const fullPath =
|
|
1664
|
+
const fullPath = join11(projectRoot, file);
|
|
1584
1665
|
const lastMod = getGitLastModified(fullPath, projectRoot);
|
|
1585
1666
|
if (lastMod) {
|
|
1586
1667
|
const days = Math.floor((now - new Date(lastMod).getTime()) / (1e3 * 60 * 60 * 24));
|
|
@@ -1602,6 +1683,7 @@ var init_scanner = __esm({
|
|
|
1602
1683
|
init_git_dates();
|
|
1603
1684
|
init_tooling_detector();
|
|
1604
1685
|
init_env_manifest_scanner();
|
|
1686
|
+
init_marketplace_scanner();
|
|
1605
1687
|
}
|
|
1606
1688
|
});
|
|
1607
1689
|
|
|
@@ -1708,7 +1790,7 @@ function generateOfflineHtml(result, projectRoot) {
|
|
|
1708
1790
|
}
|
|
1709
1791
|
</script>
|
|
1710
1792
|
|
|
1711
|
-
<script type="application/json" id="scan-data">${dataJson}</script>
|
|
1793
|
+
<script type="application/json" id="scan-data">${dataJson.replace(/<\//g, "<\\/")}</script>
|
|
1712
1794
|
</body>
|
|
1713
1795
|
</html>`;
|
|
1714
1796
|
}
|
|
@@ -1785,7 +1867,7 @@ var CURRENT_VERSION;
|
|
|
1785
1867
|
var init_check_update = __esm({
|
|
1786
1868
|
"dist/check-update.js"() {
|
|
1787
1869
|
"use strict";
|
|
1788
|
-
CURRENT_VERSION = true ? "0.10.
|
|
1870
|
+
CURRENT_VERSION = true ? "0.10.3" : "0.0.0-dev";
|
|
1789
1871
|
}
|
|
1790
1872
|
});
|
|
1791
1873
|
|
|
@@ -1820,7 +1902,7 @@ async function pushHealthResults(supabase, folderId, manifest) {
|
|
|
1820
1902
|
if (!checks?.length)
|
|
1821
1903
|
return;
|
|
1822
1904
|
const results = [];
|
|
1823
|
-
const ranAt =
|
|
1905
|
+
const ranAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1824
1906
|
for (const chk of checks) {
|
|
1825
1907
|
const config2 = chk.check_config;
|
|
1826
1908
|
if (chk.check_type === "env_var_set") {
|
|
@@ -1885,6 +1967,22 @@ async function pushHealthResults(supabase, folderId, manifest) {
|
|
|
1885
1967
|
console.error(chalk10.yellow(`Health results warning: ${error.message}`));
|
|
1886
1968
|
} else {
|
|
1887
1969
|
console.log(chalk10.green(` Updated ${results.length} health check result(s).`));
|
|
1970
|
+
const checkIds = results.map((r) => r.check_id);
|
|
1971
|
+
const { data: allResults } = await supabase.from("env_health_results").select("id, check_id, ran_at").in("check_id", checkIds).order("ran_at", { ascending: false });
|
|
1972
|
+
if (allResults) {
|
|
1973
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1974
|
+
const toDelete = [];
|
|
1975
|
+
for (const r of allResults) {
|
|
1976
|
+
if (seen.has(r.check_id)) {
|
|
1977
|
+
toDelete.push(r.id);
|
|
1978
|
+
} else {
|
|
1979
|
+
seen.add(r.check_id);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
if (toDelete.length > 0) {
|
|
1983
|
+
await supabase.from("env_health_results").delete().in("id", toDelete);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1888
1986
|
}
|
|
1889
1987
|
}
|
|
1890
1988
|
}
|
|
@@ -1937,14 +2035,14 @@ var map_exports = {};
|
|
|
1937
2035
|
__export(map_exports, {
|
|
1938
2036
|
mapCommand: () => mapCommand
|
|
1939
2037
|
});
|
|
1940
|
-
import { resolve as
|
|
2038
|
+
import { resolve as resolve4, basename } from "node:path";
|
|
1941
2039
|
import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
1942
2040
|
import { existsSync as existsSync7 } from "node:fs";
|
|
1943
2041
|
import chalk11 from "chalk";
|
|
1944
2042
|
import { select as select3, input as input5, confirm as confirm2 } from "@inquirer/prompts";
|
|
1945
2043
|
async function mapCommand(path, options) {
|
|
1946
2044
|
await checkForUpdate();
|
|
1947
|
-
const projectRoot =
|
|
2045
|
+
const projectRoot = resolve4(path ?? process.cwd());
|
|
1948
2046
|
if (!existsSync7(projectRoot)) {
|
|
1949
2047
|
console.error(chalk11.red(`Path not found: ${projectRoot}`));
|
|
1950
2048
|
process.exit(1);
|
|
@@ -1960,6 +2058,7 @@ async function mapCommand(path, options) {
|
|
|
1960
2058
|
console.log(` Skills: ${result.skills.length}`);
|
|
1961
2059
|
console.log(` Toolings: ${result.toolings.length}`);
|
|
1962
2060
|
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
2061
|
+
console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
|
|
1963
2062
|
console.log(` Data hash: ${result.dataHash.slice(0, 12)}...`);
|
|
1964
2063
|
if (result.brokenRefs.length > 0) {
|
|
1965
2064
|
console.log(chalk11.red(`
|
|
@@ -1971,11 +2070,11 @@ async function mapCommand(path, options) {
|
|
|
1971
2070
|
console.log(chalk11.red(` ... and ${result.brokenRefs.length - 5} more`));
|
|
1972
2071
|
}
|
|
1973
2072
|
}
|
|
1974
|
-
const outputDir =
|
|
2073
|
+
const outputDir = resolve4(projectRoot, "output");
|
|
1975
2074
|
if (!existsSync7(outputDir)) {
|
|
1976
2075
|
await mkdir2(outputDir, { recursive: true });
|
|
1977
2076
|
}
|
|
1978
|
-
const htmlPath =
|
|
2077
|
+
const htmlPath = resolve4(outputDir, "index.html");
|
|
1979
2078
|
const html = generateOfflineHtml(result, projectRoot);
|
|
1980
2079
|
await writeFile2(htmlPath, html, "utf-8");
|
|
1981
2080
|
console.log(chalk11.green(`
|
|
@@ -1999,15 +2098,20 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
1999
2098
|
value: f
|
|
2000
2099
|
}))
|
|
2001
2100
|
});
|
|
2101
|
+
const resolvedRoot = resolve4(projectRoot);
|
|
2002
2102
|
for (const file of toDelete) {
|
|
2003
|
-
const fullPath =
|
|
2103
|
+
const fullPath = resolve4(projectRoot, file.file_path);
|
|
2104
|
+
if (!fullPath.startsWith(resolvedRoot + "/")) {
|
|
2105
|
+
console.error(chalk11.red(` Blocked path traversal: ${file.file_path}`));
|
|
2106
|
+
continue;
|
|
2107
|
+
}
|
|
2004
2108
|
try {
|
|
2005
2109
|
const { unlink } = await import("node:fs/promises");
|
|
2006
2110
|
await unlink(fullPath);
|
|
2007
2111
|
await supabase.from("folder_files").delete().eq("id", file.id);
|
|
2008
2112
|
console.log(chalk11.green(` Deleted: ${file.file_path}`));
|
|
2009
2113
|
} catch (err) {
|
|
2010
|
-
console.error(chalk11.red(` Failed to delete ${file.file_path}: ${err}`));
|
|
2114
|
+
console.error(chalk11.red(` Failed to delete ${file.file_path}: ${err instanceof Error ? err.message : String(err)}`));
|
|
2011
2115
|
}
|
|
2012
2116
|
}
|
|
2013
2117
|
const keptIds = proposedFiles.filter((f) => !toDelete.some((d) => d.id === f.id)).map((f) => f.id);
|
|
@@ -2021,22 +2125,34 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2021
2125
|
} else if (proposedFiles?.length) {
|
|
2022
2126
|
console.log(chalk11.dim(` ${proposedFiles.length} file(s) proposed for deletion \u2014 skipped (non-interactive mode).`));
|
|
2023
2127
|
}
|
|
2024
|
-
|
|
2128
|
+
let storedManifest = null;
|
|
2129
|
+
if (!result.envManifest) {
|
|
2130
|
+
const { data: stored, error: fetchErr } = await supabase.from("claude_folders").select("env_manifest_json").eq("id", folder_id).single();
|
|
2131
|
+
if (!fetchErr && stored?.env_manifest_json) {
|
|
2132
|
+
storedManifest = stored.env_manifest_json;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
const updatePayload = {
|
|
2025
2136
|
graph_json: result.graph,
|
|
2026
2137
|
orphans_json: result.orphans,
|
|
2027
2138
|
broken_refs_json: result.brokenRefs,
|
|
2028
2139
|
skills_table_json: result.skills,
|
|
2029
2140
|
stale_files_json: result.staleFiles,
|
|
2030
|
-
|
|
2141
|
+
marketplace_plugins_json: result.marketplacePlugins,
|
|
2031
2142
|
last_scanned: result.scannedAt,
|
|
2032
2143
|
data_hash: result.dataHash
|
|
2033
|
-
}
|
|
2144
|
+
};
|
|
2145
|
+
if (result.envManifest) {
|
|
2146
|
+
updatePayload.env_manifest_json = result.envManifest;
|
|
2147
|
+
}
|
|
2148
|
+
const { error } = await supabase.from("claude_folders").update(updatePayload).eq("id", folder_id);
|
|
2034
2149
|
if (error) {
|
|
2035
2150
|
console.error(chalk11.yellow(`Sync warning: ${error.message}`));
|
|
2036
2151
|
} else {
|
|
2037
2152
|
await pushToolings(supabase, folder_id, result.toolings);
|
|
2038
|
-
|
|
2039
|
-
|
|
2153
|
+
const manifestForHealth = result.envManifest ?? storedManifest;
|
|
2154
|
+
if (manifestForHealth) {
|
|
2155
|
+
await pushHealthResults(supabase, folder_id, manifestForHealth);
|
|
2040
2156
|
}
|
|
2041
2157
|
await supabase.from("device_paths").update({ last_synced: (/* @__PURE__ */ new Date()).toISOString() }).eq("folder_id", folder_id).eq("device_name", device_name);
|
|
2042
2158
|
await saveState({
|
|
@@ -2048,7 +2164,8 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2048
2164
|
console.log(chalk11.cyan(`
|
|
2049
2165
|
https://www.md4ai.com/project/${folder_id}
|
|
2050
2166
|
`));
|
|
2051
|
-
const
|
|
2167
|
+
const graphPaths = result.graph.nodes.map((n) => n.filePath);
|
|
2168
|
+
const configFiles = await readClaudeConfigFiles(projectRoot, graphPaths);
|
|
2052
2169
|
if (configFiles.length > 0) {
|
|
2053
2170
|
for (const file of configFiles) {
|
|
2054
2171
|
await supabase.from("folder_files").upsert({
|
|
@@ -2120,6 +2237,7 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2120
2237
|
skills_table_json: result.skills,
|
|
2121
2238
|
stale_files_json: result.staleFiles,
|
|
2122
2239
|
env_manifest_json: result.envManifest,
|
|
2240
|
+
marketplace_plugins_json: result.marketplacePlugins,
|
|
2123
2241
|
last_scanned: result.scannedAt,
|
|
2124
2242
|
data_hash: result.dataHash
|
|
2125
2243
|
}).eq("id", folderId);
|
|
@@ -2127,7 +2245,8 @@ ${proposedFiles.length} file(s) proposed for deletion:
|
|
|
2127
2245
|
if (result.envManifest) {
|
|
2128
2246
|
await pushHealthResults(sb, folderId, result.envManifest);
|
|
2129
2247
|
}
|
|
2130
|
-
const
|
|
2248
|
+
const graphPaths2 = result.graph.nodes.map((n) => n.filePath);
|
|
2249
|
+
const configFiles = await readClaudeConfigFiles(projectRoot, graphPaths2);
|
|
2131
2250
|
for (const file of configFiles) {
|
|
2132
2251
|
await sb.from("folder_files").upsert({
|
|
2133
2252
|
folder_id: folderId,
|
|
@@ -2172,7 +2291,13 @@ var sync_exports = {};
|
|
|
2172
2291
|
__export(sync_exports, {
|
|
2173
2292
|
syncCommand: () => syncCommand
|
|
2174
2293
|
});
|
|
2294
|
+
import { resolve as resolve5 } from "node:path";
|
|
2295
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
2175
2296
|
import chalk14 from "chalk";
|
|
2297
|
+
function isValidProjectPath(p) {
|
|
2298
|
+
const resolved = resolve5(p);
|
|
2299
|
+
return resolved.startsWith("/") && !resolved.includes("..") && existsSync10(resolved);
|
|
2300
|
+
}
|
|
2176
2301
|
async function syncCommand(options) {
|
|
2177
2302
|
const { supabase } = await getAuthenticatedClient();
|
|
2178
2303
|
if (options.all) {
|
|
@@ -2182,6 +2307,10 @@ async function syncCommand(options) {
|
|
|
2182
2307
|
process.exit(1);
|
|
2183
2308
|
}
|
|
2184
2309
|
for (const device of devices) {
|
|
2310
|
+
if (!isValidProjectPath(device.path)) {
|
|
2311
|
+
console.error(chalk14.red(` Skipping invalid path: ${device.path}`));
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2185
2314
|
console.log(chalk14.blue(`Syncing: ${device.path}`));
|
|
2186
2315
|
const { data: proposedAll } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
|
|
2187
2316
|
if (proposedAll?.length) {
|
|
@@ -2219,6 +2348,10 @@ async function syncCommand(options) {
|
|
|
2219
2348
|
console.error(chalk14.red("Could not find last synced device/folder."));
|
|
2220
2349
|
process.exit(1);
|
|
2221
2350
|
}
|
|
2351
|
+
if (!isValidProjectPath(device.path)) {
|
|
2352
|
+
console.error(chalk14.red(`Invalid project path: ${device.path}`));
|
|
2353
|
+
process.exit(1);
|
|
2354
|
+
}
|
|
2222
2355
|
console.log(chalk14.blue(`Syncing: ${device.path}`));
|
|
2223
2356
|
const { data: proposedSingle } = await supabase.from("folder_files").select("file_path").eq("folder_id", device.folder_id).eq("proposed_for_deletion", true);
|
|
2224
2357
|
if (proposedSingle?.length) {
|
|
@@ -2251,16 +2384,16 @@ var init_sync = __esm({
|
|
|
2251
2384
|
});
|
|
2252
2385
|
|
|
2253
2386
|
// dist/mcp/read-configs.js
|
|
2254
|
-
import { readFile as
|
|
2387
|
+
import { readFile as readFile11 } from "node:fs/promises";
|
|
2255
2388
|
import { join as join14 } from "node:path";
|
|
2256
|
-
import { homedir as
|
|
2257
|
-
import { existsSync as
|
|
2258
|
-
import { readdir as
|
|
2389
|
+
import { homedir as homedir9 } from "node:os";
|
|
2390
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
2391
|
+
import { readdir as readdir4 } from "node:fs/promises";
|
|
2259
2392
|
async function readJsonSafe(path) {
|
|
2260
2393
|
try {
|
|
2261
|
-
if (!
|
|
2394
|
+
if (!existsSync12(path))
|
|
2262
2395
|
return null;
|
|
2263
|
-
const raw = await
|
|
2396
|
+
const raw = await readFile11(path, "utf-8");
|
|
2264
2397
|
return JSON.parse(raw);
|
|
2265
2398
|
} catch {
|
|
2266
2399
|
return null;
|
|
@@ -2320,7 +2453,7 @@ function parseFlatConfig(data, source) {
|
|
|
2320
2453
|
return entries;
|
|
2321
2454
|
}
|
|
2322
2455
|
async function readAllMcpConfigs() {
|
|
2323
|
-
const home =
|
|
2456
|
+
const home = homedir9();
|
|
2324
2457
|
const entries = [];
|
|
2325
2458
|
const userConfig = await readJsonSafe(join14(home, ".claude.json"));
|
|
2326
2459
|
entries.push(...parseServers(userConfig, "global"));
|
|
@@ -2329,16 +2462,16 @@ async function readAllMcpConfigs() {
|
|
|
2329
2462
|
const cwdMcp = await readJsonSafe(join14(process.cwd(), ".mcp.json"));
|
|
2330
2463
|
entries.push(...parseServers(cwdMcp, "project"));
|
|
2331
2464
|
const pluginsBase = join14(home, ".claude", "plugins", "marketplaces");
|
|
2332
|
-
if (
|
|
2465
|
+
if (existsSync12(pluginsBase)) {
|
|
2333
2466
|
try {
|
|
2334
|
-
const marketplaces = await
|
|
2467
|
+
const marketplaces = await readdir4(pluginsBase, { withFileTypes: true });
|
|
2335
2468
|
for (const mp of marketplaces) {
|
|
2336
2469
|
if (!mp.isDirectory())
|
|
2337
2470
|
continue;
|
|
2338
2471
|
const extDir = join14(pluginsBase, mp.name, "external_plugins");
|
|
2339
|
-
if (!
|
|
2472
|
+
if (!existsSync12(extDir))
|
|
2340
2473
|
continue;
|
|
2341
|
-
const plugins = await
|
|
2474
|
+
const plugins = await readdir4(extDir, { withFileTypes: true });
|
|
2342
2475
|
for (const plugin of plugins) {
|
|
2343
2476
|
if (!plugin.isDirectory())
|
|
2344
2477
|
continue;
|
|
@@ -2350,18 +2483,18 @@ async function readAllMcpConfigs() {
|
|
|
2350
2483
|
}
|
|
2351
2484
|
}
|
|
2352
2485
|
const cacheBase = join14(home, ".claude", "plugins", "cache");
|
|
2353
|
-
if (
|
|
2486
|
+
if (existsSync12(cacheBase)) {
|
|
2354
2487
|
try {
|
|
2355
|
-
const registries = await
|
|
2488
|
+
const registries = await readdir4(cacheBase, { withFileTypes: true });
|
|
2356
2489
|
for (const reg of registries) {
|
|
2357
2490
|
if (!reg.isDirectory())
|
|
2358
2491
|
continue;
|
|
2359
2492
|
const regDir = join14(cacheBase, reg.name);
|
|
2360
|
-
const plugins = await
|
|
2493
|
+
const plugins = await readdir4(regDir, { withFileTypes: true });
|
|
2361
2494
|
for (const plugin of plugins) {
|
|
2362
2495
|
if (!plugin.isDirectory())
|
|
2363
2496
|
continue;
|
|
2364
|
-
const versionDirs = await
|
|
2497
|
+
const versionDirs = await readdir4(join14(regDir, plugin.name), { withFileTypes: true });
|
|
2365
2498
|
for (const ver of versionDirs) {
|
|
2366
2499
|
if (!ver.isDirectory())
|
|
2367
2500
|
continue;
|
|
@@ -2769,7 +2902,9 @@ async function mcpWatchCommand() {
|
|
|
2769
2902
|
console.log(chalk20.yellow(` Replacing ${existingWatchers.length} existing watcher${existingWatchers.length !== 1 ? "s" : ""} on this device...`));
|
|
2770
2903
|
for (const w of existingWatchers) {
|
|
2771
2904
|
try {
|
|
2772
|
-
|
|
2905
|
+
if (typeof w.pid === "number" && w.pid > 1 && w.pid !== process.pid) {
|
|
2906
|
+
process.kill(w.pid, "SIGTERM");
|
|
2907
|
+
}
|
|
2773
2908
|
} catch {
|
|
2774
2909
|
}
|
|
2775
2910
|
}
|
|
@@ -3181,41 +3316,41 @@ init_map();
|
|
|
3181
3316
|
|
|
3182
3317
|
// dist/commands/simulate.js
|
|
3183
3318
|
init_dist();
|
|
3184
|
-
import { join as
|
|
3319
|
+
import { join as join12 } from "node:path";
|
|
3185
3320
|
import { existsSync as existsSync8 } from "node:fs";
|
|
3186
|
-
import { homedir as
|
|
3321
|
+
import { homedir as homedir8 } from "node:os";
|
|
3187
3322
|
import chalk12 from "chalk";
|
|
3188
3323
|
async function simulateCommand(prompt) {
|
|
3189
3324
|
const projectRoot = process.cwd();
|
|
3190
3325
|
console.log(chalk12.blue(`Simulating prompt: "${prompt}"
|
|
3191
3326
|
`));
|
|
3192
3327
|
console.log(chalk12.dim("Files Claude would load:\n"));
|
|
3193
|
-
const globalClaude =
|
|
3328
|
+
const globalClaude = join12(homedir8(), ".claude", "CLAUDE.md");
|
|
3194
3329
|
if (existsSync8(globalClaude)) {
|
|
3195
3330
|
console.log(chalk12.green(" \u2713 ~/.claude/CLAUDE.md (global)"));
|
|
3196
3331
|
}
|
|
3197
3332
|
for (const rootFile of ROOT_FILES) {
|
|
3198
|
-
const fullPath =
|
|
3333
|
+
const fullPath = join12(projectRoot, rootFile);
|
|
3199
3334
|
if (existsSync8(fullPath)) {
|
|
3200
3335
|
console.log(chalk12.green(` \u2713 ${rootFile} (project root)`));
|
|
3201
3336
|
}
|
|
3202
3337
|
}
|
|
3203
3338
|
const settingsFiles = [
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3339
|
+
join12(homedir8(), ".claude", "settings.json"),
|
|
3340
|
+
join12(homedir8(), ".claude", "settings.local.json"),
|
|
3341
|
+
join12(projectRoot, ".claude", "settings.json"),
|
|
3342
|
+
join12(projectRoot, ".claude", "settings.local.json")
|
|
3208
3343
|
];
|
|
3209
3344
|
for (const sf of settingsFiles) {
|
|
3210
3345
|
if (existsSync8(sf)) {
|
|
3211
|
-
const display = sf.startsWith(
|
|
3346
|
+
const display = sf.startsWith(homedir8()) ? sf.replace(homedir8(), "~") : sf.replace(projectRoot + "/", "");
|
|
3212
3347
|
console.log(chalk12.green(` \u2713 ${display} (settings)`));
|
|
3213
3348
|
}
|
|
3214
3349
|
}
|
|
3215
3350
|
const words = prompt.split(/\s+/);
|
|
3216
3351
|
for (const word of words) {
|
|
3217
3352
|
const cleaned = word.replace(/['"]/g, "");
|
|
3218
|
-
const candidatePath =
|
|
3353
|
+
const candidatePath = join12(projectRoot, cleaned);
|
|
3219
3354
|
if (existsSync8(candidatePath) && cleaned.includes("/")) {
|
|
3220
3355
|
console.log(chalk12.cyan(` \u2192 ${cleaned} (referenced in prompt)`));
|
|
3221
3356
|
}
|
|
@@ -3224,18 +3359,18 @@ async function simulateCommand(prompt) {
|
|
|
3224
3359
|
}
|
|
3225
3360
|
|
|
3226
3361
|
// dist/commands/print.js
|
|
3227
|
-
import { join as
|
|
3228
|
-
import { readFile as
|
|
3362
|
+
import { join as join13 } from "node:path";
|
|
3363
|
+
import { readFile as readFile9, writeFile as writeFile3 } from "node:fs/promises";
|
|
3229
3364
|
import { existsSync as existsSync9 } from "node:fs";
|
|
3230
3365
|
import chalk13 from "chalk";
|
|
3231
3366
|
async function printCommand(title) {
|
|
3232
3367
|
const projectRoot = process.cwd();
|
|
3233
|
-
const scanDataPath =
|
|
3368
|
+
const scanDataPath = join13(projectRoot, "output", "index.html");
|
|
3234
3369
|
if (!existsSync9(scanDataPath)) {
|
|
3235
3370
|
console.error(chalk13.red("No scan data found. Run: md4ai scan"));
|
|
3236
3371
|
process.exit(1);
|
|
3237
3372
|
}
|
|
3238
|
-
const html = await
|
|
3373
|
+
const html = await readFile9(scanDataPath, "utf-8");
|
|
3239
3374
|
const match = html.match(/<script type="application\/json" id="scan-data">([\s\S]*?)<\/script>/);
|
|
3240
3375
|
if (!match) {
|
|
3241
3376
|
console.error(chalk13.red("Could not extract scan data from output/index.html"));
|
|
@@ -3243,7 +3378,7 @@ async function printCommand(title) {
|
|
|
3243
3378
|
}
|
|
3244
3379
|
const result = JSON.parse(match[1]);
|
|
3245
3380
|
const printHtml = generatePrintHtml(result, title);
|
|
3246
|
-
const outputPath =
|
|
3381
|
+
const outputPath = join13(projectRoot, "output", `print-${Date.now()}.html`);
|
|
3247
3382
|
await writeFile3(outputPath, printHtml, "utf-8");
|
|
3248
3383
|
console.log(chalk13.green(`Print-ready wall sheet: ${outputPath}`));
|
|
3249
3384
|
}
|
|
@@ -3310,11 +3445,11 @@ init_config();
|
|
|
3310
3445
|
init_scanner();
|
|
3311
3446
|
init_push_toolings();
|
|
3312
3447
|
init_device_utils();
|
|
3313
|
-
import { resolve as
|
|
3448
|
+
import { resolve as resolve6 } from "node:path";
|
|
3314
3449
|
import chalk15 from "chalk";
|
|
3315
3450
|
async function linkCommand(projectId) {
|
|
3316
3451
|
const { supabase, userId } = await getAuthenticatedClient();
|
|
3317
|
-
const cwd =
|
|
3452
|
+
const cwd = resolve6(process.cwd());
|
|
3318
3453
|
const deviceName = detectDeviceName();
|
|
3319
3454
|
const osType = detectOs2();
|
|
3320
3455
|
const { data: folder, error: folderErr } = await supabase.from("claude_folders").select("id, name").eq("id", projectId).single();
|
|
@@ -3360,6 +3495,7 @@ Linking "${folder.name}" to this device...
|
|
|
3360
3495
|
console.log(` Skills: ${result.skills.length}`);
|
|
3361
3496
|
console.log(` Toolings: ${result.toolings.length}`);
|
|
3362
3497
|
console.log(` Env Vars: ${result.envManifest?.variables.length ?? 0} (${result.envManifest ? "manifest found" : "no manifest"})`);
|
|
3498
|
+
console.log(` Plugins: ${result.marketplacePlugins.length} (${result.marketplacePlugins.reduce((n, p) => n + p.skills.length, 0)} skills)`);
|
|
3363
3499
|
const { error: scanErr } = await supabase.from("claude_folders").update({
|
|
3364
3500
|
graph_json: result.graph,
|
|
3365
3501
|
orphans_json: result.orphans,
|
|
@@ -3367,6 +3503,7 @@ Linking "${folder.name}" to this device...
|
|
|
3367
3503
|
skills_table_json: result.skills,
|
|
3368
3504
|
stale_files_json: result.staleFiles,
|
|
3369
3505
|
env_manifest_json: result.envManifest,
|
|
3506
|
+
marketplace_plugins_json: result.marketplacePlugins,
|
|
3370
3507
|
last_scanned: result.scannedAt,
|
|
3371
3508
|
data_hash: result.dataHash
|
|
3372
3509
|
}).eq("id", folder.id);
|
|
@@ -3374,7 +3511,8 @@ Linking "${folder.name}" to this device...
|
|
|
3374
3511
|
console.error(chalk15.yellow(`Scan upload warning: ${scanErr.message}`));
|
|
3375
3512
|
}
|
|
3376
3513
|
await pushToolings(supabase, folder.id, result.toolings);
|
|
3377
|
-
const
|
|
3514
|
+
const graphPaths = result.graph.nodes.map((n) => n.filePath);
|
|
3515
|
+
const configFiles = await readClaudeConfigFiles(cwd, graphPaths);
|
|
3378
3516
|
if (configFiles.length > 0) {
|
|
3379
3517
|
for (const file of configFiles) {
|
|
3380
3518
|
await supabase.from("folder_files").upsert({
|
|
@@ -3402,18 +3540,24 @@ Linking "${folder.name}" to this device...
|
|
|
3402
3540
|
}
|
|
3403
3541
|
|
|
3404
3542
|
// dist/commands/import-bundle.js
|
|
3405
|
-
import { readFile as
|
|
3406
|
-
import {
|
|
3407
|
-
import { existsSync as
|
|
3543
|
+
import { readFile as readFile10, writeFile as writeFile4, mkdir as mkdir3, stat as fsStat } from "node:fs/promises";
|
|
3544
|
+
import { dirname as dirname3, resolve as resolve7 } from "node:path";
|
|
3545
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
3408
3546
|
import chalk16 from "chalk";
|
|
3409
3547
|
import { confirm as confirm3, input as input6 } from "@inquirer/prompts";
|
|
3548
|
+
var MAX_BUNDLE_SIZE_BYTES = 50 * 1024 * 1024;
|
|
3410
3549
|
async function importBundleCommand(zipPath) {
|
|
3411
|
-
if (!
|
|
3550
|
+
if (!existsSync11(zipPath)) {
|
|
3412
3551
|
console.error(chalk16.red(`File not found: ${zipPath}`));
|
|
3413
3552
|
process.exit(1);
|
|
3414
3553
|
}
|
|
3554
|
+
const fileStat = await fsStat(zipPath);
|
|
3555
|
+
if (fileStat.size > MAX_BUNDLE_SIZE_BYTES) {
|
|
3556
|
+
console.error(chalk16.red(`Bundle too large (${Math.round(fileStat.size / 1024 / 1024)} MB). Maximum is 50 MB.`));
|
|
3557
|
+
process.exit(1);
|
|
3558
|
+
}
|
|
3415
3559
|
const JSZip = (await import("jszip")).default;
|
|
3416
|
-
const zipData = await
|
|
3560
|
+
const zipData = await readFile10(zipPath);
|
|
3417
3561
|
const zip = await JSZip.loadAsync(zipData);
|
|
3418
3562
|
const manifestFile = zip.file("manifest.json");
|
|
3419
3563
|
if (!manifestFile) {
|
|
@@ -3454,10 +3598,15 @@ Files to extract:`));
|
|
|
3454
3598
|
console.log(chalk16.yellow("Cancelled."));
|
|
3455
3599
|
return;
|
|
3456
3600
|
}
|
|
3601
|
+
const resolvedTarget = resolve7(targetDir);
|
|
3457
3602
|
for (const file of files) {
|
|
3458
|
-
const fullPath =
|
|
3603
|
+
const fullPath = resolve7(targetDir, file.filePath);
|
|
3604
|
+
if (!fullPath.startsWith(resolvedTarget + "/") && fullPath !== resolvedTarget) {
|
|
3605
|
+
console.error(chalk16.red(` Blocked path traversal: ${file.filePath}`));
|
|
3606
|
+
continue;
|
|
3607
|
+
}
|
|
3459
3608
|
const dir = dirname3(fullPath);
|
|
3460
|
-
if (!
|
|
3609
|
+
if (!existsSync11(dir)) {
|
|
3461
3610
|
await mkdir3(dir, { recursive: true });
|
|
3462
3611
|
}
|
|
3463
3612
|
await writeFile4(fullPath, file.content, "utf-8");
|
|
@@ -3762,9 +3911,9 @@ async function fetchGitHubVersions(repo, signal) {
|
|
|
3762
3911
|
init_mcp_watch();
|
|
3763
3912
|
|
|
3764
3913
|
// dist/commands/init-manifest.js
|
|
3765
|
-
import { resolve as
|
|
3766
|
-
import { readFile as
|
|
3767
|
-
import { existsSync as
|
|
3914
|
+
import { resolve as resolve8, join as join15, relative as relative4, dirname as dirname4 } from "node:path";
|
|
3915
|
+
import { readFile as readFile12, writeFile as writeFile5, mkdir as mkdir4 } from "node:fs/promises";
|
|
3916
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
3768
3917
|
import chalk21 from "chalk";
|
|
3769
3918
|
var SECRET_PATTERNS = [
|
|
3770
3919
|
/_KEY$/i,
|
|
@@ -3782,7 +3931,7 @@ async function discoverEnvFiles(projectRoot) {
|
|
|
3782
3931
|
const apps = [];
|
|
3783
3932
|
for (const envName of [".env", ".env.local", ".env.example"]) {
|
|
3784
3933
|
const envPath = join15(projectRoot, envName);
|
|
3785
|
-
if (
|
|
3934
|
+
if (existsSync13(envPath)) {
|
|
3786
3935
|
const vars = await extractVarNames(envPath);
|
|
3787
3936
|
if (vars.length > 0) {
|
|
3788
3937
|
apps.push({ name: "root", envFilePath: envName, vars });
|
|
@@ -3793,11 +3942,11 @@ async function discoverEnvFiles(projectRoot) {
|
|
|
3793
3942
|
const subdirs = ["web", "cli", "api", "app", "server", "packages"];
|
|
3794
3943
|
for (const sub of subdirs) {
|
|
3795
3944
|
const subDir = join15(projectRoot, sub);
|
|
3796
|
-
if (!
|
|
3945
|
+
if (!existsSync13(subDir))
|
|
3797
3946
|
continue;
|
|
3798
3947
|
for (const envName of [".env.local", ".env", ".env.example"]) {
|
|
3799
3948
|
const envPath = join15(subDir, envName);
|
|
3800
|
-
if (
|
|
3949
|
+
if (existsSync13(envPath)) {
|
|
3801
3950
|
const vars = await extractVarNames(envPath);
|
|
3802
3951
|
if (vars.length > 0) {
|
|
3803
3952
|
apps.push({
|
|
@@ -3813,7 +3962,7 @@ async function discoverEnvFiles(projectRoot) {
|
|
|
3813
3962
|
return apps;
|
|
3814
3963
|
}
|
|
3815
3964
|
async function extractVarNames(envPath) {
|
|
3816
|
-
const content = await
|
|
3965
|
+
const content = await readFile12(envPath, "utf-8");
|
|
3817
3966
|
const vars = [];
|
|
3818
3967
|
for (const line of content.split("\n")) {
|
|
3819
3968
|
const trimmed = line.trim();
|
|
@@ -3857,9 +4006,9 @@ function generateManifest(apps) {
|
|
|
3857
4006
|
return lines.join("\n");
|
|
3858
4007
|
}
|
|
3859
4008
|
async function initManifestCommand() {
|
|
3860
|
-
const projectRoot =
|
|
4009
|
+
const projectRoot = resolve8(process.cwd());
|
|
3861
4010
|
const manifestPath = join15(projectRoot, "docs", "reference", "env-manifest.md");
|
|
3862
|
-
if (
|
|
4011
|
+
if (existsSync13(manifestPath)) {
|
|
3863
4012
|
console.log(chalk21.yellow(`Manifest already exists: ${relative4(projectRoot, manifestPath)}`));
|
|
3864
4013
|
console.log(chalk21.dim("Edit it directly to make changes."));
|
|
3865
4014
|
return;
|
|
@@ -3876,7 +4025,7 @@ async function initManifestCommand() {
|
|
|
3876
4025
|
}
|
|
3877
4026
|
const content = generateManifest(apps);
|
|
3878
4027
|
const dir = dirname4(manifestPath);
|
|
3879
|
-
if (!
|
|
4028
|
+
if (!existsSync13(dir)) {
|
|
3880
4029
|
await mkdir4(dir, { recursive: true });
|
|
3881
4030
|
}
|
|
3882
4031
|
await writeFile5(manifestPath, content, "utf-8");
|
|
@@ -4036,7 +4185,7 @@ init_check_update();
|
|
|
4036
4185
|
init_map();
|
|
4037
4186
|
init_mcp_watch();
|
|
4038
4187
|
import chalk23 from "chalk";
|
|
4039
|
-
import { resolve as
|
|
4188
|
+
import { resolve as resolve9 } from "node:path";
|
|
4040
4189
|
async function fetchLatestVersion2() {
|
|
4041
4190
|
try {
|
|
4042
4191
|
const controller = new AbortController();
|
|
@@ -4065,7 +4214,7 @@ function isNewer3(a, b) {
|
|
|
4065
4214
|
return false;
|
|
4066
4215
|
}
|
|
4067
4216
|
async function startCommand() {
|
|
4068
|
-
const projectRoot =
|
|
4217
|
+
const projectRoot = resolve9(process.cwd());
|
|
4069
4218
|
console.log("");
|
|
4070
4219
|
console.log(chalk23.bold.cyan(` MD4AI v${CURRENT_VERSION}`));
|
|
4071
4220
|
console.log(chalk23.dim(` ${projectRoot}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md4ai",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "CLI for MD4AI — scan Claude projects and sync to your dashboard",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@md4ai/shared": "workspace:*",
|
|
44
|
-
"@types/node": "^22.15
|
|
44
|
+
"@types/node": "^22.19.15",
|
|
45
45
|
"esbuild": "^0.27.3",
|
|
46
46
|
"typescript": "^5.7.0"
|
|
47
47
|
}
|